summaryrefslogtreecommitdiffhomepage
path: root/packages/core/src
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-05-23 05:06:12 +0900
committerAdam Malczewski <[email protected]>2026-05-23 05:06:12 +0900
commit9287cccb29d135ea19f2612c26f3090c94820d8c (patch)
tree2d68e8cacf6d71786f305d5f4a512a68f19137c5 /packages/core/src
parentef427d3eae77fca716c203dd8bd84939710c518a (diff)
downloaddispatch-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.ts102
-rw-r--r--packages/core/src/agents/loader.ts5
-rw-r--r--packages/core/src/config/schema.ts36
-rw-r--r--packages/core/src/index.ts5
-rw-r--r--packages/core/src/skills/parser.ts14
-rw-r--r--packages/core/src/tools/retrieve.ts32
-rw-r--r--packages/core/src/tools/web-search.ts6
-rw-r--r--packages/core/src/tools/youtube-transcribe.ts6
-rw-r--r--packages/core/src/types/index.ts2
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;
}