diff options
| author | Adam Malczewski <[email protected]> | 2026-05-23 16:59:20 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-05-23 16:59:20 +0900 |
| commit | 236beefb708a6cd91b673978ddf4ebf045a9844c (patch) | |
| tree | 6522c65a0d490b41cc01297f2444f160f8dbb7f8 /packages/core/src/tools | |
| parent | 225d3ea65cfc35d211fc66e30cf05cbc693d37e4 (diff) | |
| download | dispatch-236beefb708a6cd91b673978ddf4ebf045a9844c.tar.gz dispatch-236beefb708a6cd91b673978ddf4ebf045a9844c.zip | |
feat: key fallback using agent models[] hierarchy, background tool modes, copy truncation
- Agent rate-limit fallback now iterates through agent's configured models[] in strict order
- Frontend sends agentModels with each /chat request; backend uses buildFallbackSequence()
- Emits notice event on fallback so chat shows which key failed and what's being tried next
- Child agents inherit parent's agentModels for fallback
- Added statusCode propagation from AI SDK errors for programmatic 429 detection
- Copy button truncates all tool results at 300 chars (was 200 for 4 specific tools)
- run_shell, summon, youtube_transcribe: background mode support
- summon: blocking mode by default with getResult callback
Diffstat (limited to 'packages/core/src/tools')
| -rw-r--r-- | packages/core/src/tools/run-shell.ts | 24 | ||||
| -rw-r--r-- | packages/core/src/tools/summon.ts | 27 | ||||
| -rw-r--r-- | packages/core/src/tools/youtube-transcribe.ts | 19 |
3 files changed, 68 insertions, 2 deletions
diff --git a/packages/core/src/tools/run-shell.ts b/packages/core/src/tools/run-shell.ts index 6671c31..ec2db9c 100644 --- a/packages/core/src/tools/run-shell.ts +++ b/packages/core/src/tools/run-shell.ts @@ -54,6 +54,12 @@ export function createRunShellTool( parameters: z.object({ command: z.string().describe("The shell command to execute"), timeout: z.number().optional().describe("Timeout in milliseconds (default 2 minutes)"), + background: z + .boolean() + .optional() + .describe( + "If true, the command starts in the background and a job_id is returned immediately. Use the retrieve tool with the job_id to get the result later.", + ), }), execute: async ( args: Record<string, unknown>, @@ -61,6 +67,7 @@ export function createRunShellTool( ): Promise<string> => { const command = args.command as string; const timeout = (args.timeout as number | undefined) ?? DEFAULT_TIMEOUT; + const background = (args.background as boolean | undefined) ?? false; const [shell, shellArgs] = getShell(); const child = spawn(shell, [...shellArgs, command], { @@ -99,6 +106,23 @@ export function createRunShellTool( }); }); + // If background mode requested, register immediately and return job ID + if (background && shellStore) { + const jobId = shellStore.register({ + command, + stdout, + stderr, + completion: completionPromise, + }); + return [ + `Command started in background.`, + `job_id: ${jobId}`, + `command: ${command}`, + ``, + `Use the retrieve tool with this job_id to get the result when ready.`, + ].join("\n"); + } + const queueCallbacks = context?.queueCallbacks; if (queueCallbacks && shellStore) { diff --git a/packages/core/src/tools/summon.ts b/packages/core/src/tools/summon.ts index 29f27d1..22ab35b 100644 --- a/packages/core/src/tools/summon.ts +++ b/packages/core/src/tools/summon.ts @@ -3,6 +3,9 @@ import type { ToolDefinition } from "../types/index.js"; export interface SummonCallbacks { spawn(options: { task: string; tools: string[]; workingDirectory?: string }): Promise<string>; + getResult( + agentId: string, + ): Promise<{ status: "done"; result: string } | { status: "error"; error: string }>; } export function createSummonTool( @@ -12,12 +15,15 @@ export function createSummonTool( return { name: "summon", description: [ - "Spawn a new child agent to work on a task independently. Returns immediately with an agent_id — does NOT wait for the child to finish.", + "Spawn a new child agent to work on a task independently.", + "", + "By default, blocks until the child agent finishes and returns the result directly.", + "Set background=true to return immediately with an agent_id instead — use retrieve to collect the result later.", "", "The child agent runs in its own tab visible to the user. Use the 'retrieve' tool with the returned agent_id to get the result when needed.", "", "Pattern for parallel work:", - " 1. Call summon multiple times to start several agents", + " 1. Call summon multiple times with background=true to start several agents", " 2. Do your own work or wait", " 3. Call retrieve for each agent_id to collect results", "", @@ -64,12 +70,19 @@ export function createSummonTool( .describe( "Absolute path for the child to work in. Defaults to the current working directory.", ), + background: z + .boolean() + .optional() + .describe( + "If true, returns immediately with an agent_id for later retrieval. If false (default), blocks until the child agent finishes and returns the result directly.", + ), }), execute: async (args: Record<string, unknown>): Promise<string> => { const task = args.task as string; const tools = (args.tools as string[] | undefined) ?? ["read_file", "list_files", "todo"]; const workingDirectory = (args.working_directory as string | undefined) ?? defaultWorkingDirectory; + const background = (args.background as boolean | undefined) ?? false; try { const agentId = await callbacks.spawn({ @@ -77,6 +90,16 @@ export function createSummonTool( tools, workingDirectory, }); + + if (!background) { + // Block until the child agent completes + const result = await callbacks.getResult(agentId); + if (result.status === "done") { + return result.result; + } + return `Error from child agent: ${result.error}`; + } + return [ `Agent spawned successfully.`, `agent_id: ${agentId}`, diff --git a/packages/core/src/tools/youtube-transcribe.ts b/packages/core/src/tools/youtube-transcribe.ts index ea8ed43..3a26d6f 100644 --- a/packages/core/src/tools/youtube-transcribe.ts +++ b/packages/core/src/tools/youtube-transcribe.ts @@ -141,17 +141,36 @@ export function createYoutubeTranscribeTool( ].join("\n"), parameters: z.object({ url: z.string().describe("The YouTube video URL to fetch the transcript for."), + background: z + .boolean() + .optional() + .describe( + "If true, the transcription request starts in the background and a job_id is returned immediately. Use the retrieve tool with the job_id to get the transcript later.", + ), }), execute: async ( args: Record<string, unknown>, context?: ToolExecuteContext, ): Promise<string> => { const url = args.url as string; + const background = (args.background as boolean | undefined) ?? false; const queueCallbacks = context?.queueCallbacks; try { const pollPromise = pollUntilReady(url); + // If background mode requested, register immediately and return job ID + if (background && transcriptStore) { + const jobId = transcriptStore.register(url, pollPromise); + return [ + `Transcript request started in background.`, + `job_id: ${jobId}`, + `url: ${url}`, + ``, + `Use the retrieve tool with this job_id to get the transcript when ready.`, + ].join("\n"); + } + if (queueCallbacks && transcriptStore) { const { promise: queuePromise, cancel: cancelQueueWait } = queueCallbacks.waitForQueuedMessage(); |
