summaryrefslogtreecommitdiffhomepage
path: root/packages/core
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-05-23 16:59:20 +0900
committerAdam Malczewski <[email protected]>2026-05-23 16:59:20 +0900
commit236beefb708a6cd91b673978ddf4ebf045a9844c (patch)
tree6522c65a0d490b41cc01297f2444f160f8dbb7f8 /packages/core
parent225d3ea65cfc35d211fc66e30cf05cbc693d37e4 (diff)
downloaddispatch-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')
-rw-r--r--packages/core/src/agent/agent.ts14
-rw-r--r--packages/core/src/tools/run-shell.ts24
-rw-r--r--packages/core/src/tools/summon.ts27
-rw-r--r--packages/core/src/tools/youtube-transcribe.ts19
-rw-r--r--packages/core/src/types/index.ts3
5 files changed, 82 insertions, 5 deletions
diff --git a/packages/core/src/agent/agent.ts b/packages/core/src/agent/agent.ts
index 6f1a5a4..bf9b22f 100644
--- a/packages/core/src/agent/agent.ts
+++ b/packages/core/src/agent/agent.ts
@@ -311,8 +311,15 @@ export class Agent {
allToolCalls.push(toolCall);
yield { type: "tool-call", toolCall };
} else if (event.type === "error") {
+ const errRecord = event.error as unknown as Record<string, unknown>;
+ const statusCode =
+ typeof errRecord.statusCode === "number" ? errRecord.statusCode : undefined;
const errorMsg = formatError(event.error, this.config);
- yield { type: "error", error: errorMsg };
+ yield {
+ type: "error",
+ error: errorMsg,
+ ...(statusCode !== undefined ? { statusCode } : {}),
+ };
this.status = "error";
yield { type: "status", status: "error" };
return;
@@ -471,8 +478,11 @@ export class Agent {
yield { type: "done", message: assistantMessage };
} catch (err) {
+ const errRecord = err as unknown as Record<string, unknown>;
+ const statusCode =
+ typeof errRecord.statusCode === "number" ? errRecord.statusCode : undefined;
const errorMsg = formatError(err, this.config);
- yield { type: "error", error: errorMsg };
+ yield { type: "error", error: errorMsg, ...(statusCode !== undefined ? { statusCode } : {}) };
this.status = "error";
yield { type: "status", status: "error" };
return;
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();
diff --git a/packages/core/src/types/index.ts b/packages/core/src/types/index.ts
index c301822..b17a720 100644
--- a/packages/core/src/types/index.ts
+++ b/packages/core/src/types/index.ts
@@ -36,7 +36,8 @@ export type AgentEvent =
| { type: "tool-call"; toolCall: ToolCall }
| { type: "tool-result"; toolResult: ToolResult }
| { type: "shell-output"; data: string; stream: "stdout" | "stderr" }
- | { type: "error"; error: string }
+ | { type: "error"; error: string; statusCode?: number }
+ | { type: "notice"; message: string }
| { type: "done"; message: ChatMessage }
| { type: "task-list-update"; tasks: TaskItem[] }
| { type: "config-reload" }