From b548173593b4cf5f548a48e32a71445c2b2371e7 Mon Sep 17 00:00:00 2001 From: Gagan Date: Thu, 2 Jul 2026 02:31:00 +0530 Subject: [PATCH] feat(mini-apps): capable models for app bg-tasks - list-models tool: returns allowed model IDs (gateway for signed-in, config for BYOK) + a safe defaultModel, so the copilot picks a valid capable model instead of guessing rejected IDs - patch-background-task: clearModel option to reset a bad model/provider override (falls back to default); patchTask can now delete fields - build-mini-app skill: use list-models + set a capable model on data tasks --- .../assistant/skills/build-mini-app/skill.ts | 16 ++++++++---- .../core/src/application/lib/builtin-tools.ts | 25 +++++++++++++++++-- .../core/src/background-tasks/fileops.ts | 9 ++++++- 3 files changed, 42 insertions(+), 8 deletions(-) diff --git a/apps/x/packages/core/src/application/assistant/skills/build-mini-app/skill.ts b/apps/x/packages/core/src/application/assistant/skills/build-mini-app/skill.ts index 6f9b2706..a30c4a63 100644 --- a/apps/x/packages/core/src/application/assistant/skills/build-mini-app/skill.ts +++ b/apps/x/packages/core/src/application/assistant/skills/build-mini-app/skill.ts @@ -112,10 +112,15 @@ buggy run can't corrupt the app with the wrong shape or wipe series with empties The agent only RETURNS the data; \`mini-app-set-data\` writes \`data.json\` atomically (deterministic path + write — the agent never touches files). Set the -manifest's \`agent\` field to the task's slug, and give the task a **capable model** -(e.g. a Claude Sonnet / GPT-class model) via its model override — the default -light model fabricates output and hallucinates tool names on side-effect tasks. -Give it a sensible trigger (cron/window) from the background-task skill. +manifest's \`agent\` field to the task's slug. + +**Give the task a capable model.** The default bg-task model is a light one that +fabricates output and hallucinates tool names on data/side-effect tasks. Set the +task's \`model\` (via \`create-background-task\`) to a strong reasoning model — +but only to an **allowed ID**: call **\`list-models\`** first and pick from what it +returns (arbitrary IDs are rejected). \`list-models.defaultModel\` is always a safe +capable choice. Give the task a sensible trigger (cron/window) from the +background-task skill. ## 5. Finalize @@ -142,7 +147,8 @@ do this once the app is installed AND its data is populated, so it renders ready - **bg-task has no shell:** don't generate \`refresh.sh\` / \`executeCommand\` steps for the data agent — it can't run them headlessly. - **model:** set a capable model on any data/side-effect bg-task; the default is - too weak and will fabricate results. + too weak and will fabricate results. Use \`list-models\` to get allowed IDs — + don't guess model IDs (unknown ones are rejected as "Model not allowed"). ## Manifest schema (manifest.json) diff --git a/apps/x/packages/core/src/application/lib/builtin-tools.ts b/apps/x/packages/core/src/application/lib/builtin-tools.ts index d91fa176..1bccb69e 100644 --- a/apps/x/packages/core/src/application/lib/builtin-tools.ts +++ b/apps/x/packages/core/src/application/lib/builtin-tools.ts @@ -52,6 +52,7 @@ const PatchBackgroundTaskInput = BackgroundTaskSchema.pick({ slug: z.string().describe('The slug of the task to update (the folder name under bg-tasks/).'), triggers: TriggersSchema.optional().describe('Replace the triggers object. To remove all triggers (make manual-only) pass an empty object.'), projectDir: z.string().optional().describe("Point an existing task at a code repo (or change which one) to make it a coding task. Absolute path or ~/… to a local git repository with at least one commit. Same rules as on create."), + clearModel: z.boolean().optional().describe("Reset the task's model/provider override so it falls back to the default. Use this to unstick a bad/rejected model value (do not also pass model)."), }); // Turn a user-supplied directory into a registered code project id. Reuses the @@ -93,6 +94,7 @@ import type { ToolContext } from "./exec-tool.js"; import { generateText } from "ai"; import { createProvider } from "../../models/models.js"; import { getDefaultModelAndProvider, resolveProviderConfig } from "../../models/defaults.js"; +import { listGatewayModels } from "../../models/gateway.js"; import { captureLlmUsage } from "../../analytics/usage.js"; import { getCurrentUseCase, withUseCase } from "../../analytics/use_case.js"; import { isSignedIn } from "../../account/account.js"; @@ -1596,6 +1598,25 @@ export const BuiltinTools: z.infer = { } }, }, + 'list-models': { + description: "List model IDs available for model overrides (e.g. to set a capable model on a background task). Signed-in users get the Rowboat gateway's allowed models; BYOK users get their configured model. Call this BEFORE setting a bg-task `model` so you pick a valid, allowed ID (arbitrary IDs are rejected). Returns { defaultModel, models }.", + inputSchema: z.object({}), + execute: async () => { + try { + if (await isSignedIn()) { + const { providers } = await listGatewayModels(); + const models = providers.flatMap((p) => p.models.map((m) => m.id)); + const { model: defaultModel } = await getDefaultModelAndProvider(); + return { signedIn: true, defaultModel, models }; + } + const { model, provider } = await getDefaultModelAndProvider(); + return { signedIn: false, defaultModel: model, provider, models: [model] }; + } catch (e) { + return { error: e instanceof Error ? e.message : String(e) }; + } + }, + isAvailable: async () => true, + }, 'fetch-url': { description: "Fetch an HTTP(S) URL and return the response body as text. Use this to pull data from web APIs or pages (e.g. a JSON endpoint) — especially in background tasks, which have no shell. GET by default; supports POST with a body. Returns { ok, status, statusText, body } (body truncated if very large). For JSON, parse the returned body.", inputSchema: z.object({ @@ -1690,7 +1711,7 @@ export const BuiltinTools: z.infer = { execute: async (input: z.infer) => { try { const { patchTask } = await import("../../background-tasks/fileops.js"); - const { slug, projectDir, ...partial } = input; + const { slug, projectDir, clearModel, ...partial } = input; let warning: string | undefined; if (projectDir) { const r = await resolveCodeProject(projectDir); @@ -1698,7 +1719,7 @@ export const BuiltinTools: z.infer = { (partial as { projectId?: string }).projectId = r.projectId; warning = r.warning; } - const result = await patchTask(slug, partial); + const result = await patchTask(slug, partial, clearModel ? ['model', 'provider'] : []); return { success: true, task: result, ...(warning ? { warning } : {}) }; } catch (err) { return { success: false, error: err instanceof Error ? err.message : String(err) }; diff --git a/apps/x/packages/core/src/background-tasks/fileops.ts b/apps/x/packages/core/src/background-tasks/fileops.ts index 03d66dce..a771c75a 100644 --- a/apps/x/packages/core/src/background-tasks/fileops.ts +++ b/apps/x/packages/core/src/background-tasks/fileops.ts @@ -81,13 +81,20 @@ export async function fetchTask(slug: string): Promise { * structural edits (active toggle, instructions, triggers, model) and by the * runner for the `lastRun*` runtime fields. */ -export async function patchTask(slug: string, partial: Partial): Promise { +export async function patchTask( + slug: string, + partial: Partial, + clear: Array = [], +): Promise { return withFileLock(taskYamlPath(slug), async () => { const current = await fetchTask(slug); if (!current) { throw new Error(`Task '${slug}' not found`); } const next: BackgroundTask = { ...current, ...partial }; + // Allow explicitly clearing a field (e.g. reset model → falls back to the + // default). A plain merge can't remove a key. + for (const key of clear) delete next[key]; await fs.writeFile(taskYamlPath(slug), stringifyYaml(next), 'utf-8'); return next; });