Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 42 additions & 9 deletions src/lib/admin/author.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,58 @@ You output strict JSON conforming to the PackageDraft schema. Every package must
Type semantics: skill = capability; playbook = multi-step decision flow; soul = personality/values layer; guardrail = safety boundary.
Slug must be lowercase-kebab.`;

// Author model fallback chain. The structured-output path (Output.object →
// tool/function calling) is sensitive to provider/model quirks, so we try a
// short, ordered list before giving up. First success wins. Adding a model
// here is cheap; do NOT silently swallow failures — every attempt logs the
// model + the error so the cause is visible in the server logs.
const AUTHOR_MODEL_FALLBACKS = [
"google/gemini-3-flash-preview",
"google/gemini-2.5-flash",
"google/gemini-2.5-pro",
"openai/gpt-5.1-mini",
] as const;

export async function generateDraft(
brief: string,
type: "skill" | "playbook" | "soul" | "guardrail",
vertical?: string,
grounding?: string
) {
const model = getGatewayModel("google/gemini-3-flash-preview");
const prompt = `Brief:\n${brief}\n\nType: ${type}${vertical ? `\nVertical: ${vertical}` : ""}${
grounding ? `\n\nGrounding research (use as ground truth):\n${grounding.slice(0, 8000)}` : ""
}\n\nDesign a complete, production-ready ${type} package. Return ONLY the JSON.`;

const { experimental_output } = await generateText({
model,
system: META_SYSTEM,
prompt,
experimental_output: Output.object({ schema: PackageDraftSchema }),
});
if (experimental_output.type !== type) experimental_output.type = type;
return experimental_output;
const attempts: Array<{ model: string; error: string }> = [];
for (const modelId of AUTHOR_MODEL_FALLBACKS) {
try {
const model = getGatewayModel(modelId);
const { experimental_output } = await generateText({
model,
system: META_SYSTEM,
prompt,
experimental_output: Output.object({ schema: PackageDraftSchema }),
});
if (experimental_output.type !== type) experimental_output.type = type;
if (attempts.length > 0) {
// We recovered — log so we can spot a regression on the primary model.
console.warn(
`[skillforge.author] recovered on ${modelId} after ${attempts.length} failure(s):`,
attempts,
);
}
return experimental_output;
} catch (e: any) {
const msg = e?.message ?? String(e);
attempts.push({ model: modelId, error: msg });
console.error(`[skillforge.author] ${modelId} failed:`, msg);
}
}
// All fallbacks exhausted. Surface a structured error so the upload
// pipeline can report it back to the caller instead of silently
// recording "failed".
const summary = attempts.map((a) => `${a.model}: ${a.error}`).join(" | ");
throw new Error(`SkillForge author failed across all fallback models — ${summary}`);
}

export async function insertDraftPackage(
Expand Down
30 changes: 25 additions & 5 deletions src/lib/mcp/tools/skills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,19 +317,39 @@ export const uploadPackagesTool = defineTool({
// Always private. Marketplace listing requires an explicit user action in the UI.
const results = await processBulkUpload(supabaseAdmin as any, userId, files);
const ok = results.filter((r) => r.ok).length;
const response = {
const failed = results.length - ok;
// Surface the per-file errors at the top of the payload too. MCP
// clients (and the LLMs reading their output) often render only
// the first ~200 chars of a tool response and miss the buried
// `results[].error`, leaving users with a generic "failed: N"
// and no way to act. The summary line is short enough to survive
// truncation and points at the actual root cause.
const errorMessages = results
.filter((r) => !r.ok)
.map((r) => `${r.name}: ${r.error ?? "unknown"}`);
const response: Record<string, unknown> = {
uploaded: ok,
failed: results.length - ok,
failed,
visibility: "private_draft",
next_step: "Open /account/packages on superagentskill.com to submit a draft for admin review.",
next_step:
ok > 0
? "Open /account/packages on superagentskill.com to submit a draft for admin review."
: "All files failed to normalise. See `error_summary` for the cause and retry.",
results,
};
if (idempotency_key) {
if (failed > 0) {
response.error_summary = errorMessages.slice(0, 3).join(" · ");
}
if (idempotency_key && ok > 0) {
// Only cache successful runs so a transient model failure doesn't
// poison the idempotency key for 24h.
await putIdempotent(userId, "upload_packages", idempotency_key, response);
}
return json(response);
} catch (e: any) {
return json({ error: e?.message ?? "upload_failed" });
const msg = e?.message ?? "upload_failed";
console.error(`[mcp.upload_packages] user=${userId} unexpected:`, e);
return json({ error: msg });
}
},
});
Expand Down
11 changes: 10 additions & 1 deletion src/lib/uploads/uploads.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,16 @@ export async function processBulkUpload(
out.type = inferred;
out.forge_report_url = `/forge/report/${pkg.slug}`;
} catch (e: any) {
out.error = e?.message ?? "failed";
const msg = e?.message ?? "failed";
out.error = msg;
// Server-side visibility — without this the MCP caller sees a
// generic "failed" while the real cause (model rejection, schema
// mismatch, DB error) is invisible. Keep the user/filename so we
// can correlate complaints to actual rows in the logs.
console.error(
`[uploads.processBulkUpload] user=${userId} file=${f.name} type=${f.type ?? "?"}: ${msg}`,
e,
);
}
results.push(out);
}
Expand Down
Loading