From 48194753eee9c6cb8995cf52afcbd615cf115491 Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Fri, 29 May 2026 15:38:35 +0900 Subject: feat: subagent summon — catalog filter, error hints, system prompt, AgentBuilder default, SubAgent mode display MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Filter summon tool catalog to is_subagent-flagged agents only - Return fresh subagent list in error when slug not found - Add subagent hint to system prompt when summon tool available - Default is_subagent checkbox to true in AgentBuilder - Fix tab-created event to include agentSlug and agentModels - Add SubAgent read-only mode to ModelSelector with model slider --- packages/api/src/agent-manager.ts | 32 ++++++++--- packages/core/src/tools/summon.ts | 30 +++++----- packages/core/src/types/index.ts | 2 + packages/frontend/src/App.svelte | 2 + .../src/lib/components/AgentBuilder.svelte | 2 +- .../src/lib/components/ModelSelector.svelte | 67 +++++++++++++++++++++- .../src/lib/components/SidebarPanel.svelte | 6 ++ packages/frontend/src/lib/tabs.svelte.ts | 6 +- packages/frontend/src/lib/types.ts | 3 + 9 files changed, 123 insertions(+), 27 deletions(-) diff --git a/packages/api/src/agent-manager.ts b/packages/api/src/agent-manager.ts index 9ed2f51..69c071d 100644 --- a/packages/api/src/agent-manager.ts +++ b/packages/api/src/agent-manager.ts @@ -134,10 +134,15 @@ function buildSystemPrompt(toolNames: string[], basePrompt?: string): string { if (!toolList) return base; const hasTodo = toolNames.includes("todo"); + const hasSummon = toolNames.includes("summon"); let prompt = `${base}\n\nYou have access to the following tools:\n\n${toolList}\n\nWhen asked to work with files, use these tools. Always confirm what you did after completing an action.`; if (hasTodo) { prompt += `\n\n${TODO_GUIDANCE}`; } + if (hasSummon) { + prompt += + '\n\nYou have pre-configured subagent types. Use summon(agent="slug", task="...") to delegate specialized work to a subagent. Use list_files and read_file to inspect available agent definitions.'; + } return prompt; } @@ -940,9 +945,13 @@ export class AgentManager { if (options.agentSlug) { agentDef = loadAgent(options.agentSlug, parentEffectiveDir); if (!agentDef) { - throw new Error( - `Agent definition not found: "${options.agentSlug}". Inspect the agents directories to see available slugs.`, - ); + const allDefs = loadAgents(parentEffectiveDir); + const subagents = allDefs.filter((d) => d.is_subagent).map((d) => `${d.slug} (${d.name})`); + const hint = + subagents.length > 0 + ? ` Available subagents: ${subagents.join(", ")}.` + : " No subagent definitions exist yet."; + throw new Error(`Agent definition not found: "${options.agentSlug}".${hint}`); } } @@ -989,13 +998,16 @@ export class AgentManager { tabAgent.workingDirectoryOverride = resolvedWorkingDirectory; tabAgent.finalOutput = ""; - if (agentDef && agentDef.models.length > 0) { + const primary = agentDef?.models[0]; + if (agentDef && primary) { // The agent definition specifies its own model fallback chain. - // Clear keyId/modelId so the fallback sequence uses the - // definition's models (matches how a top-level tab using this - // definition would be configured). - tabAgent.keyId = null; - tabAgent.modelId = null; + // Set keyId/modelId to the primary (first) model in the chain so + // the frontend can display the concrete key/model this subagent + // was configured with, while `agentModels` drives the fallback + // sequence (matches how a top-level tab using this definition + // would be configured). + tabAgent.keyId = primary.key_id; + tabAgent.modelId = primary.model_id; tabAgent.agentModels = agentDef.models; } else { // No definition (or definition has no models) → inherit from @@ -1036,7 +1048,9 @@ export class AgentManager { keyId: tabAgent.keyId, modelId: tabAgent.modelId, parentTabId: options.parentTabId ?? null, + agentSlug: options.agentSlug ?? null, workingDirectory: resolvedWorkingDirectory ?? null, + agentModels: tabAgent.agentModels ?? null, }, tabId, ); diff --git a/packages/core/src/tools/summon.ts b/packages/core/src/tools/summon.ts index ed4b080..34109f3 100644 --- a/packages/core/src/tools/summon.ts +++ b/packages/core/src/tools/summon.ts @@ -215,18 +215,20 @@ export function toAvailableAgents( globalDir: string, projectDir: string | null, ): AvailableAgent[] { - return defs.map((d) => { - const baseDir = - d.scope === "global" - ? globalDir - : projectDir - ? `${projectDir.replace(/\/$/, "")}/.dispatch/agents` - : globalDir; - return { - slug: d.slug, - name: d.name, - description: d.description, - path: `${baseDir}/${d.slug}.toml`, - }; - }); + return defs + .filter((d) => d.is_subagent) + .map((d) => { + const baseDir = + d.scope === "global" + ? globalDir + : projectDir + ? `${projectDir.replace(/\/$/, "")}/.dispatch/agents` + : globalDir; + return { + slug: d.slug, + name: d.name, + description: d.description, + path: `${baseDir}/${d.slug}.toml`, + }; + }); } diff --git a/packages/core/src/types/index.ts b/packages/core/src/types/index.ts index 0df56ca..4148498 100644 --- a/packages/core/src/types/index.ts +++ b/packages/core/src/types/index.ts @@ -149,7 +149,9 @@ export type AgentEvent = keyId: string | null; modelId: string | null; parentTabId: string | null; + agentSlug?: string | null; workingDirectory: string | null; + agentModels?: Array<{ key_id: string; model_id: string }> | null; } | { type: "message-queued"; tabId: string; messageId: string; message: string } | { type: "message-consumed"; tabId: string; messageIds: string[] } diff --git a/packages/frontend/src/App.svelte b/packages/frontend/src/App.svelte index 49db4ac..bc6cd02 100644 --- a/packages/frontend/src/App.svelte +++ b/packages/frontend/src/App.svelte @@ -142,6 +142,8 @@ onMount(() => { activeModelId={tabStore.activeTab?.modelId ?? null} reasoningEffort={tabStore.activeTab?.reasoningEffort ?? "max"} activeAgentSlug={tabStore.activeTab?.agentSlug ?? null} + activeTabParentId={tabStore.activeTab?.parentTabId ?? null} + activeAgentModels={tabStore.activeTab?.agentModels ?? null} workingDirectory={tabStore.activeTab?.workingDirectory ?? null} onKeyChange={(keyId) => tabStore.setKey(keyId)} onModelChange={(keyId, modelId) => tabStore.changeModel(keyId, modelId)} diff --git a/packages/frontend/src/lib/components/AgentBuilder.svelte b/packages/frontend/src/lib/components/AgentBuilder.svelte index d85a6a3..d8ae530 100644 --- a/packages/frontend/src/lib/components/AgentBuilder.svelte +++ b/packages/frontend/src/lib/components/AgentBuilder.svelte @@ -124,7 +124,7 @@ const modelCache = new Map(); formSkills = new Set(); formTools = new Set(); formModels = []; - formIsSubagent = false; + formIsSubagent = true; editing = true; // Allow the effect to skip the initial population setTimeout(() => { formReady = true; }, 0); diff --git a/packages/frontend/src/lib/components/ModelSelector.svelte b/packages/frontend/src/lib/components/ModelSelector.svelte index 235cf3b..a752f5d 100644 --- a/packages/frontend/src/lib/components/ModelSelector.svelte +++ b/packages/frontend/src/lib/components/ModelSelector.svelte @@ -36,6 +36,8 @@ const modelCache = new Map(); activeModelId = null, reasoningEffort = "max", activeAgentSlug = null, + activeTabParentId = null as string | null, + activeAgentModels = null as Array<{ key_id: string; model_id: string }> | null, workingDirectory = null, onKeyChange, onModelChange, @@ -48,6 +50,8 @@ const modelCache = new Map(); activeModelId?: string | null; reasoningEffort?: string; activeAgentSlug?: string | null; + activeTabParentId?: string | null; + activeAgentModels?: Array<{ key_id: string; model_id: string }> | null; workingDirectory?: string | null; onKeyChange: (keyId: string) => void; onModelChange: (keyId: string, modelId: string) => void; @@ -91,7 +95,11 @@ const modelCache = new Map(); }); let modeOverride = $state<"manual" | "agent" | null>(null); - let mode = $derived(modeOverride ?? (activeAgentSlug ? "agent" : "manual")); + let mode = $derived( + activeTabParentId && activeAgentSlug + ? "subagent" + : modeOverride ?? (activeAgentSlug ? "agent" : "manual"), + ); let agents = $state([]); let visibleAgents = $derived(agents.filter((a) => !a.is_subagent)); let loadingAgents = $state(false); @@ -202,6 +210,7 @@ const modelCache = new Map(); @@ -220,9 +229,16 @@ const modelCache = new Map(); if (cwdEl) cwdEl.value = agentToApply.cwd ?? ""; } }} + disabled={mode === 'subagent'} > Agent + {#if mode === "manual"} @@ -256,6 +272,55 @@ const modelCache = new Map(); {/if} + {:else if mode === "subagent"} + + {@const subModels = activeAgentModels ?? []} + {@const hasSubModels = subModels.length > 1} + {@const subActiveIdx = subModels.findIndex( + (m) => m.key_id === activeKeyId && m.model_id === activeModelId, + )} +
+
+ SubAgent + {activeAgentSlug} +
+
+ Key + {activeKeyId ?? "default"} +
+
+ Model + {activeModelId ?? "default"} +
+ {#if hasSubModels} + {@const displayIdx = subActiveIdx >= 0 ? subActiveIdx : 0} +
+
Model fallback chain
+ +
+ {#each subModels as _, i} + {i + 1} + {/each} +
+
+ {#each subModels as m, i} +
+ {i + 1}. {m.key_id} / {m.model_id} +
+ {/each} +
+
+ {/if} +

This tab was spawned by a parent agent. Settings cannot be changed.

+
{:else} {#if loadingAgents} diff --git a/packages/frontend/src/lib/components/SidebarPanel.svelte b/packages/frontend/src/lib/components/SidebarPanel.svelte index e68781f..fca53b7 100644 --- a/packages/frontend/src/lib/components/SidebarPanel.svelte +++ b/packages/frontend/src/lib/components/SidebarPanel.svelte @@ -29,6 +29,8 @@ const { activeModelId = null, reasoningEffort = "max", activeAgentSlug = null as string | null, + activeTabParentId = null as string | null, + activeAgentModels = null as Array<{ key_id: string; model_id: string }> | null, workingDirectory = null as string | null, onKeyChange, onModelChange, @@ -45,6 +47,8 @@ const { activeModelId?: string | null; reasoningEffort?: string; activeAgentSlug?: string | null; + activeTabParentId?: string | null; + activeAgentModels?: Array<{ key_id: string; model_id: string }> | null; workingDirectory?: string | null; onKeyChange: (keyId: string) => void; onModelChange: (keyId: string, modelId: string) => void; @@ -144,6 +148,8 @@ function contentClass(selected: string): string { {onModelChange} {onReasoningChange} {activeAgentSlug} + {activeTabParentId} + {activeAgentModels} {onAgentChange} {workingDirectory} {onWorkingDirectoryChange} diff --git a/packages/frontend/src/lib/tabs.svelte.ts b/packages/frontend/src/lib/tabs.svelte.ts index 9b31b3f..9d0040c 100644 --- a/packages/frontend/src/lib/tabs.svelte.ts +++ b/packages/frontend/src/lib/tabs.svelte.ts @@ -967,7 +967,9 @@ export function createTabStore() { keyId: string | null; modelId: string | null; parentTabId: string | null; + agentSlug?: string | null; workingDirectory: string | null; + agentModels?: Array<{ key_id: string; model_id: string }> | null; }; // Only add if we don't already have this tab if (!getTabById(newTabEvent.id)) { @@ -984,9 +986,9 @@ export function createTabStore() { injectedSkills: [], parentTabId: newTabEvent.parentTabId ?? null, persistent: newTabEvent.parentTabId == null, - agentSlug: null, + agentSlug: newTabEvent.agentSlug ?? null, agentScope: null, - agentModels: null, + agentModels: newTabEvent.agentModels ?? null, workingDirectory: newTabEvent.workingDirectory ?? null, queuedMessages: [], chunkLimit: appSettings.chunkLimit, diff --git a/packages/frontend/src/lib/types.ts b/packages/frontend/src/lib/types.ts index f1ae5c4..f9add94 100644 --- a/packages/frontend/src/lib/types.ts +++ b/packages/frontend/src/lib/types.ts @@ -142,6 +142,9 @@ export type AgentEvent = keyId: string | null; modelId: string | null; parentTabId: string | null; + agentSlug?: string | null; + workingDirectory?: string | null; + agentModels?: Array<{ key_id: string; model_id: string }> | null; } | { type: "message-queued"; tabId: string; messageId: string; message: string } | { type: "message-consumed"; tabId: string; messageIds: string[] } -- cgit v1.2.3