mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-07-12 21:02:17 +02:00
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
This commit is contained in:
parent
61f2bcff5a
commit
b548173593
3 changed files with 42 additions and 8 deletions
|
|
@ -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\`
|
The agent only RETURNS the data; \`mini-app-set-data\` writes \`data.json\`
|
||||||
atomically (deterministic path + write — the agent never touches files). Set the
|
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**
|
manifest's \`agent\` field to the task's slug.
|
||||||
(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 the task a capable model.** The default bg-task model is a light one that
|
||||||
Give it a sensible trigger (cron/window) from the background-task skill.
|
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
|
## 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\`
|
- **bg-task has no shell:** don't generate \`refresh.sh\` / \`executeCommand\`
|
||||||
steps for the data agent — it can't run them headlessly.
|
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
|
- **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)
|
## Manifest schema (manifest.json)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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/).'),
|
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.'),
|
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."),
|
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
|
// 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 { generateText } from "ai";
|
||||||
import { createProvider } from "../../models/models.js";
|
import { createProvider } from "../../models/models.js";
|
||||||
import { getDefaultModelAndProvider, resolveProviderConfig } from "../../models/defaults.js";
|
import { getDefaultModelAndProvider, resolveProviderConfig } from "../../models/defaults.js";
|
||||||
|
import { listGatewayModels } from "../../models/gateway.js";
|
||||||
import { captureLlmUsage } from "../../analytics/usage.js";
|
import { captureLlmUsage } from "../../analytics/usage.js";
|
||||||
import { getCurrentUseCase, withUseCase } from "../../analytics/use_case.js";
|
import { getCurrentUseCase, withUseCase } from "../../analytics/use_case.js";
|
||||||
import { isSignedIn } from "../../account/account.js";
|
import { isSignedIn } from "../../account/account.js";
|
||||||
|
|
@ -1596,6 +1598,25 @@ export const BuiltinTools: z.infer<typeof BuiltinToolsSchema> = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
'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': {
|
'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.",
|
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({
|
inputSchema: z.object({
|
||||||
|
|
@ -1690,7 +1711,7 @@ export const BuiltinTools: z.infer<typeof BuiltinToolsSchema> = {
|
||||||
execute: async (input: z.infer<typeof PatchBackgroundTaskInput>) => {
|
execute: async (input: z.infer<typeof PatchBackgroundTaskInput>) => {
|
||||||
try {
|
try {
|
||||||
const { patchTask } = await import("../../background-tasks/fileops.js");
|
const { patchTask } = await import("../../background-tasks/fileops.js");
|
||||||
const { slug, projectDir, ...partial } = input;
|
const { slug, projectDir, clearModel, ...partial } = input;
|
||||||
let warning: string | undefined;
|
let warning: string | undefined;
|
||||||
if (projectDir) {
|
if (projectDir) {
|
||||||
const r = await resolveCodeProject(projectDir);
|
const r = await resolveCodeProject(projectDir);
|
||||||
|
|
@ -1698,7 +1719,7 @@ export const BuiltinTools: z.infer<typeof BuiltinToolsSchema> = {
|
||||||
(partial as { projectId?: string }).projectId = r.projectId;
|
(partial as { projectId?: string }).projectId = r.projectId;
|
||||||
warning = r.warning;
|
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 } : {}) };
|
return { success: true, task: result, ...(warning ? { warning } : {}) };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return { success: false, error: err instanceof Error ? err.message : String(err) };
|
return { success: false, error: err instanceof Error ? err.message : String(err) };
|
||||||
|
|
|
||||||
|
|
@ -81,13 +81,20 @@ export async function fetchTask(slug: string): Promise<BackgroundTask | null> {
|
||||||
* structural edits (active toggle, instructions, triggers, model) and by the
|
* structural edits (active toggle, instructions, triggers, model) and by the
|
||||||
* runner for the `lastRun*` runtime fields.
|
* runner for the `lastRun*` runtime fields.
|
||||||
*/
|
*/
|
||||||
export async function patchTask(slug: string, partial: Partial<BackgroundTask>): Promise<BackgroundTask> {
|
export async function patchTask(
|
||||||
|
slug: string,
|
||||||
|
partial: Partial<BackgroundTask>,
|
||||||
|
clear: Array<keyof BackgroundTask> = [],
|
||||||
|
): Promise<BackgroundTask> {
|
||||||
return withFileLock(taskYamlPath(slug), async () => {
|
return withFileLock(taskYamlPath(slug), async () => {
|
||||||
const current = await fetchTask(slug);
|
const current = await fetchTask(slug);
|
||||||
if (!current) {
|
if (!current) {
|
||||||
throw new Error(`Task '${slug}' not found`);
|
throw new Error(`Task '${slug}' not found`);
|
||||||
}
|
}
|
||||||
const next: BackgroundTask = { ...current, ...partial };
|
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');
|
await fs.writeFile(taskYamlPath(slug), stringifyYaml(next), 'utf-8');
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue