diff options
| author | Adam Malczewski <[email protected]> | 2026-05-23 05:06:12 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-05-23 05:06:12 +0900 |
| commit | 9287cccb29d135ea19f2612c26f3090c94820d8c (patch) | |
| tree | 2d68e8cacf6d71786f305d5f4a512a68f19137c5 /packages/core/src | |
| parent | ef427d3eae77fca716c203dd8bd84939710c518a (diff) | |
| download | dispatch-9287cccb29d135ea19f2612c26f3090c94820d8c.tar.gz dispatch-9287cccb29d135ea19f2612c26f3090c94820d8c.zip | |
feat: add is_subagent flag to agents, fix all lint/type/test issues
- Add is_subagent checkbox to agent editor; subagents are hidden from Chat Settings
- Add is_subagent field to AgentDefinition type, TOML serialization, and API route
- Filter subagents from ModelSelector agent list
- Fix all biome lint/format errors across codebase (useLiteralKeys, noNonNullAssertion, noExplicitAny, formatting, import sorting)
- Fix svelte-check errors (type narrowing in SkillsBrowser, ToolPermissions, SidebarPanel)
- Fix a11y warnings in App.svelte (label-control associations)
- Fix test mocks missing BackgroundShellStore, BackgroundTranscriptStore, createWebSearchTool, createYoutubeTranscribeTool
- Update stale 409 test to match current message-queuing behavior
- Exclude packaging/ and release/ dirs from biome to avoid linting stale build artifacts
Diffstat (limited to 'packages/core/src')
| -rw-r--r-- | packages/core/src/agent/agent.ts | 102 | ||||
| -rw-r--r-- | packages/core/src/agents/loader.ts | 5 | ||||
| -rw-r--r-- | packages/core/src/config/schema.ts | 36 | ||||
| -rw-r--r-- | packages/core/src/index.ts | 5 | ||||
| -rw-r--r-- | packages/core/src/skills/parser.ts | 14 | ||||
| -rw-r--r-- | packages/core/src/tools/retrieve.ts | 32 | ||||
| -rw-r--r-- | packages/core/src/tools/web-search.ts | 6 | ||||
| -rw-r--r-- | packages/core/src/tools/youtube-transcribe.ts | 6 | ||||
| -rw-r--r-- | packages/core/src/types/index.ts | 2 |
9 files changed, 108 insertions, 100 deletions
diff --git a/packages/core/src/agent/agent.ts b/packages/core/src/agent/agent.ts index 64a602b..6f1a5a4 100644 --- a/packages/core/src/agent/agent.ts +++ b/packages/core/src/agent/agent.ts @@ -188,12 +188,12 @@ export class Agent { } try { - const execPromise = tool.execute(tc.arguments, { - onOutput: (data: string, stream: "stdout" | "stderr") => { - shellOutputQueue.push({ data, stream }); - }, - queueCallbacks: this.queueCallbacks, - }); + const execPromise = tool.execute(tc.arguments, { + onOutput: (data: string, stream: "stdout" | "stderr") => { + shellOutputQueue.push({ data, stream }); + }, + queueCallbacks: this.queueCallbacks, + }); const rawResult = await execPromise; const resultStr = typeof rawResult === "string" ? rawResult : JSON.stringify(rawResult); @@ -320,9 +320,7 @@ export class Agent { } } catch (streamErr) { const errMsg = streamErr instanceof Error ? streamErr.message : String(streamErr); - const unavailMatch = errMsg.match( - /tried to call unavailable tool '([^']+)'/i, - ); + const unavailMatch = errMsg.match(/tried to call unavailable tool '([^']+)'/i); if (!unavailMatch) throw streamErr; // Model tried to call an unavailable tool. @@ -391,8 +389,8 @@ export class Agent { let toolResult: ToolResult | undefined; while (toolResult === undefined) { if (shellOutputQueue.length > 0) { - const item = shellOutputQueue.shift()!; - yield { type: "shell-output", data: item.data, stream: item.stream }; + const item = shellOutputQueue.shift(); + if (item) yield { type: "shell-output", data: item.data, stream: item.stream }; continue; } const raceResult = await Promise.race([ @@ -406,30 +404,28 @@ export class Agent { } } - // Drain any remaining shell output emitted before we read the result - while (shellOutputQueue.length > 0) { - const item = shellOutputQueue.shift()!; - yield { type: "shell-output", data: item.data, stream: item.stream }; - } + // Drain any remaining shell output emitted before we read the result + while (shellOutputQueue.length > 0) { + const item = shellOutputQueue.shift(); + if (item) yield { type: "shell-output", data: item.data, stream: item.stream }; + } - // Check for queued user messages and append them to the tool result - let finalToolResult = toolResult; - if (this.queueCallbacks) { - const queuedMsgs = this.queueCallbacks.dequeueMessages(); - if (queuedMsgs.length > 0) { - const userMessages = queuedMsgs - .map((m) => m.message) - .join("\n---\n"); - finalToolResult = { - ...toolResult, - result: `${toolResult.result}\n\n[USER INTERRUPT]\nThe user has sent you message(s) while you were working. You MUST address these before continuing with your current task:\n\n${userMessages}`, - }; + // Check for queued user messages and append them to the tool result + let finalToolResult = toolResult; + if (this.queueCallbacks) { + const queuedMsgs = this.queueCallbacks.dequeueMessages(); + if (queuedMsgs.length > 0) { + const userMessages = queuedMsgs.map((m) => m.message).join("\n---\n"); + finalToolResult = { + ...toolResult, + result: `${toolResult.result}\n\n[USER INTERRUPT]\nThe user has sent you message(s) while you were working. You MUST address these before continuing with your current task:\n\n${userMessages}`, + }; + } } - } - stepToolResults.push(finalToolResult); - allToolResults.push(finalToolResult); - yield { type: "tool-result", toolResult: finalToolResult }; + stepToolResults.push(finalToolResult); + allToolResults.push(finalToolResult); + yield { type: "tool-result", toolResult: finalToolResult }; } // Add tool results back to step messages so LLM can see them @@ -451,29 +447,29 @@ export class Agent { } } - const assistantMessage: ChatMessage = { - role: "assistant", - content: finalText, - toolCalls: allToolCalls.length > 0 ? allToolCalls : undefined, - toolResults: allToolResults.length > 0 ? allToolResults : undefined, - }; - this.messages.push(assistantMessage); - - // Drain any remaining queued messages that arrived after the last tool call - if (this.queueCallbacks) { - const remaining = this.queueCallbacks.dequeueMessages(); - if (remaining.length > 0) { - // These messages arrived too late to be injected into a tool result. - // Append them as a user message to the conversation so they're not lost. - const userMessages = remaining.map(m => m.message).join("\n---\n"); - this.messages.push({ - role: "user", - content: userMessages, - }); + const assistantMessage: ChatMessage = { + role: "assistant", + content: finalText, + toolCalls: allToolCalls.length > 0 ? allToolCalls : undefined, + toolResults: allToolResults.length > 0 ? allToolResults : undefined, + }; + this.messages.push(assistantMessage); + + // Drain any remaining queued messages that arrived after the last tool call + if (this.queueCallbacks) { + const remaining = this.queueCallbacks.dequeueMessages(); + if (remaining.length > 0) { + // These messages arrived too late to be injected into a tool result. + // Append them as a user message to the conversation so they're not lost. + const userMessages = remaining.map((m) => m.message).join("\n---\n"); + this.messages.push({ + role: "user", + content: userMessages, + }); + } } - } - yield { type: "done", message: assistantMessage }; + yield { type: "done", message: assistantMessage }; } catch (err) { const errorMsg = formatError(err, this.config); yield { type: "error", error: errorMsg }; diff --git a/packages/core/src/agents/loader.ts b/packages/core/src/agents/loader.ts index ec29983..cf84381 100644 --- a/packages/core/src/agents/loader.ts +++ b/packages/core/src/agents/loader.ts @@ -112,6 +112,10 @@ export function saveAgent(agent: AgentDefinition): void { tomlContent.cwd = agent.cwd; } + if (agent.is_subagent) { + tomlContent.is_subagent = true; + } + // smol-toml handles [[models]] array-of-tables if (agent.models.length > 0) { tomlContent.models = agent.models.map((m) => ({ @@ -202,6 +206,7 @@ function loadAgentsFromDir(dir: string, scope: string): AgentDefinition[] { scope, slug, ...(typeof parsed.cwd === "string" && parsed.cwd ? { cwd: parsed.cwd } : {}), + ...(parsed.is_subagent === true ? { is_subagent: true } : {}), }); } catch { // Skip unparseable files diff --git a/packages/core/src/config/schema.ts b/packages/core/src/config/schema.ts index ef10b65..a459a4d 100644 --- a/packages/core/src/config/schema.ts +++ b/packages/core/src/config/schema.ts @@ -66,37 +66,37 @@ function validateKey(raw: unknown, path: string, errors: ConfigError[]): KeyDefi errors.push({ path, message: "must be an object" }); return null; } - if (typeof raw["id"] !== "string") { + if (typeof raw.id !== "string") { errors.push({ path: `${path}.id`, message: "must be a string" }); return null; } - if (typeof raw["provider"] !== "string") { + if (typeof raw.provider !== "string") { errors.push({ path: `${path}.provider`, message: "must be a string" }); return null; } - if (typeof raw["base_url"] !== "string") { + if (typeof raw.base_url !== "string") { errors.push({ path: `${path}.base_url`, message: "must be a string" }); return null; } // "anthropic" provider uses credentials_file instead of env - if (raw["provider"] === "anthropic") { + if (raw.provider === "anthropic") { return { - id: raw["id"] as string, - provider: raw["provider"] as string, - base_url: raw["base_url"] as string, - ...(typeof raw["credentials_file"] === "string" - ? ({ credentials_file: raw["credentials_file"] } as Pick<KeyDefinition, "credentials_file">) + id: raw.id as string, + provider: raw.provider as string, + base_url: raw.base_url as string, + ...(typeof raw.credentials_file === "string" + ? ({ credentials_file: raw.credentials_file } as Pick<KeyDefinition, "credentials_file">) : {}), }; } // Other providers: env is optional (keys can be stored in DB) return { - id: raw["id"] as string, - provider: raw["provider"] as string, - base_url: raw["base_url"] as string, - ...(typeof raw["env"] === "string" ? { env: raw["env"] } : {}), + id: raw.id as string, + provider: raw.provider as string, + base_url: raw.base_url as string, + ...(typeof raw.env === "string" ? { env: raw.env } : {}), }; } @@ -109,17 +109,17 @@ export function validateConfig(raw: unknown): { config: DispatchConfig; errors: } // permissions (required, but can be empty) - const permissions = validatePermissions(raw["permissions"] ?? {}, "permissions", errors); + const permissions = validatePermissions(raw.permissions ?? {}, "permissions", errors); // keys (optional) let keys: KeyDefinition[] | undefined; - if (raw["keys"] !== undefined) { - if (!Array.isArray(raw["keys"])) { + if (raw.keys !== undefined) { + if (!Array.isArray(raw.keys)) { errors.push({ path: "keys", message: "must be an array" }); } else { keys = []; - for (let i = 0; i < raw["keys"].length; i++) { - const key = validateKey(raw["keys"][i], `keys[${i}]`, errors); + for (let i = 0; i < raw.keys.length; i++) { + const key = validateKey(raw.keys[i], `keys[${i}]`, errors); if (key) keys.push(key); } } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 8d5db16..1e55425 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -57,6 +57,9 @@ export { createSummonTool, type SummonCallbacks } from "./tools/summon.js"; export { createTaskListTool, TaskList } from "./tools/task-list.js"; export { createWebSearchTool } from "./tools/web-search.js"; export { createWriteFileTool } from "./tools/write-file.js"; -export { BackgroundTranscriptStore, createYoutubeTranscribeTool } from "./tools/youtube-transcribe.js"; +export { + BackgroundTranscriptStore, + createYoutubeTranscribeTool, +} from "./tools/youtube-transcribe.js"; // Types & Permissions export * from "./types/index.js"; diff --git a/packages/core/src/skills/parser.ts b/packages/core/src/skills/parser.ts index 289073f..bd1205e 100644 --- a/packages/core/src/skills/parser.ts +++ b/packages/core/src/skills/parser.ts @@ -30,16 +30,14 @@ export function parseSkillFile( try { const frontmatter = parse(tomlSource); - if (typeof frontmatter["name"] === "string") { - name = frontmatter["name"]; + if (typeof frontmatter.name === "string") { + name = frontmatter.name; } - if (typeof frontmatter["description"] === "string") { - description = frontmatter["description"]; + if (typeof frontmatter.description === "string") { + description = frontmatter.description; } - if (Array.isArray(frontmatter["tags"])) { - tags = (frontmatter["tags"] as unknown[]).filter( - (t): t is string => typeof t === "string", - ); + if (Array.isArray(frontmatter.tags)) { + tags = (frontmatter.tags as unknown[]).filter((t): t is string => typeof t === "string"); } } catch { // Malformed TOML — fall through with defaults diff --git a/packages/core/src/tools/retrieve.ts b/packages/core/src/tools/retrieve.ts index 021d1b7..80c3715 100644 --- a/packages/core/src/tools/retrieve.ts +++ b/packages/core/src/tools/retrieve.ts @@ -28,29 +28,33 @@ export function createRetrieveTool(callbacks: RetrieveCallbacks): ToolDefinition parameters: z.object({ agent_id: z.string().describe("The agent_id returned by a previous summon call."), }), - execute: async (args: Record<string, unknown>, context?: ToolExecuteContext): Promise<string> => { + execute: async ( + args: Record<string, unknown>, + context?: ToolExecuteContext, + ): Promise<string> => { const agentId = args.agent_id as string; const queueCallbacks = context?.queueCallbacks; try { let outcome: { status: "done"; result: string } | { status: "error"; error: string }; - if (queueCallbacks) { - const childPromise = callbacks.getResult(agentId); - const { promise: queuePromise, cancel: cancelQueueWait } = queueCallbacks.waitForQueuedMessage(); - const queueSignal = queuePromise.then(() => "QUEUE_INTERRUPT" as const); + if (queueCallbacks) { + const childPromise = callbacks.getResult(agentId); + const { promise: queuePromise, cancel: cancelQueueWait } = + queueCallbacks.waitForQueuedMessage(); + const queueSignal = queuePromise.then(() => "QUEUE_INTERRUPT" as const); - const raceResult = await Promise.race([childPromise, queueSignal]); + const raceResult = await Promise.race([childPromise, queueSignal]); - if (raceResult === "QUEUE_INTERRUPT") { - const queuedMsgs = queueCallbacks.dequeueMessages(); - const userMessages = queuedMsgs.map((m) => m.message).join("\n---\n"); - return `The subagent (agent_id: ${agentId}) has not completed its task yet. You will need to call retrieve with this agent_id again later to get the result.\n\n[USER INTERRUPT]\nThe user has sent you message(s) while you were working. You MUST address these before continuing with your current task:\n\n${userMessages}`; - } + if (raceResult === "QUEUE_INTERRUPT") { + const queuedMsgs = queueCallbacks.dequeueMessages(); + const userMessages = queuedMsgs.map((m) => m.message).join("\n---\n"); + return `The subagent (agent_id: ${agentId}) has not completed its task yet. You will need to call retrieve with this agent_id again later to get the result.\n\n[USER INTERRUPT]\nThe user has sent you message(s) while you were working. You MUST address these before continuing with your current task:\n\n${userMessages}`; + } - // Child finished first — clean up the queue listener - cancelQueueWait(); - outcome = raceResult; + // Child finished first — clean up the queue listener + cancelQueueWait(); + outcome = raceResult; } else { outcome = await callbacks.getResult(agentId); } diff --git a/packages/core/src/tools/web-search.ts b/packages/core/src/tools/web-search.ts index 4265aa7..7f061a5 100644 --- a/packages/core/src/tools/web-search.ts +++ b/packages/core/src/tools/web-search.ts @@ -70,7 +70,9 @@ export function createWebSearchTool(): ToolDefinition { return `Error: Firecrawl returned HTTP ${response.status} ${response.statusText}${text ? `: ${text}` : ""}`; } - let json: { data?: Array<{ title?: string; url?: string; description?: string; markdown?: string }> }; + let json: { + data?: Array<{ title?: string; url?: string; description?: string; markdown?: string }>; + }; try { json = await response.json(); } catch { @@ -96,7 +98,7 @@ export function createWebSearchTool(): ToolDefinition { let output = parts.join("\n\n---\n\n"); if (output.length > MAX_OUTPUT_CHARS) { - output = output.slice(0, MAX_OUTPUT_CHARS) + "\n\n[Output truncated]"; + output = `${output.slice(0, MAX_OUTPUT_CHARS)}\n\n[Output truncated]`; } return output; }, diff --git a/packages/core/src/tools/youtube-transcribe.ts b/packages/core/src/tools/youtube-transcribe.ts index cfa006d..ea8ed43 100644 --- a/packages/core/src/tools/youtube-transcribe.ts +++ b/packages/core/src/tools/youtube-transcribe.ts @@ -41,9 +41,7 @@ function formatTime(seconds: number): string { function formatTranscript(data: TranscriptResponse): string { const segments = data.segments ?? []; - const segmentsText = segments - .map((seg) => `[${formatTime(seg.start)}] ${seg.text}`) - .join("\n"); + const segmentsText = segments.map((seg) => `[${formatTime(seg.start)}] ${seg.text}`).join("\n"); const output = [ `Video ID: ${data.video_id}`, @@ -58,7 +56,7 @@ function formatTranscript(data: TranscriptResponse): string { ].join("\n"); return output.length > MAX_OUTPUT_CHARS - ? output.slice(0, MAX_OUTPUT_CHARS) + "\n\n[Transcript truncated]" + ? `${output.slice(0, MAX_OUTPUT_CHARS)}\n\n[Transcript truncated]` : output; } diff --git a/packages/core/src/types/index.ts b/packages/core/src/types/index.ts index be37674..ff47f49 100644 --- a/packages/core/src/types/index.ts +++ b/packages/core/src/types/index.ts @@ -188,4 +188,6 @@ export interface AgentDefinition { slug: string; /** Default working directory for this agent (optional, absolute path) */ cwd?: string; + /** Whether this agent is a subagent (hidden from Chat Settings) */ + is_subagent?: boolean; } |
