summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-05-29 15:38:35 +0900
committerAdam Malczewski <[email protected]>2026-05-29 15:38:35 +0900
commit48194753eee9c6cb8995cf52afcbd615cf115491 (patch)
treea4aa2c22ed0db272d6848c28f755ad9950418c54
parentaa230050f4edb7bfc8d3e4d59d95c68c36264b41 (diff)
downloaddispatch-48194753eee9c6cb8995cf52afcbd615cf115491.tar.gz
dispatch-48194753eee9c6cb8995cf52afcbd615cf115491.zip
feat: subagent summon — catalog filter, error hints, system prompt, AgentBuilder default, SubAgent mode display
- 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
-rw-r--r--packages/api/src/agent-manager.ts32
-rw-r--r--packages/core/src/tools/summon.ts30
-rw-r--r--packages/core/src/types/index.ts2
-rw-r--r--packages/frontend/src/App.svelte2
-rw-r--r--packages/frontend/src/lib/components/AgentBuilder.svelte2
-rw-r--r--packages/frontend/src/lib/components/ModelSelector.svelte67
-rw-r--r--packages/frontend/src/lib/components/SidebarPanel.svelte6
-rw-r--r--packages/frontend/src/lib/tabs.svelte.ts6
-rw-r--r--packages/frontend/src/lib/types.ts3
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<string, string[]>();
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<string, string[]>();
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<string, string[]>();
});
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<AgentInfo[]>([]);
let visibleAgents = $derived(agents.filter((a) => !a.is_subagent));
let loadingAgents = $state(false);
@@ -202,6 +210,7 @@ const modelCache = new Map<string, string[]>();
<button
class="btn btn-xs {mode === 'manual' ? 'btn-primary' : 'btn-ghost'}"
onclick={() => { modeOverride = "manual"; onAgentChange(null); }}
+ disabled={mode === 'subagent'}
>
Manual
</button>
@@ -220,9 +229,16 @@ const modelCache = new Map<string, string[]>();
if (cwdEl) cwdEl.value = agentToApply.cwd ?? "";
}
}}
+ disabled={mode === 'subagent'}
>
Agent
</button>
+ <button
+ class="btn btn-xs {mode === 'subagent' ? 'btn-primary' : 'btn-ghost'}"
+ disabled={true}
+ >
+ SubAgent
+ </button>
</div>
{#if mode === "manual"}
@@ -256,6 +272,55 @@ const modelCache = new Map<string, string[]>();
</select>
</div>
{/if}
+ {:else if mode === "subagent"}
+ <!-- SubAgent read-only info -->
+ {@const subModels = activeAgentModels ?? []}
+ {@const hasSubModels = subModels.length > 1}
+ {@const subActiveIdx = subModels.findIndex(
+ (m) => m.key_id === activeKeyId && m.model_id === activeModelId,
+ )}
+ <div class="flex flex-col gap-2">
+ <div class="flex items-center justify-between">
+ <span class="text-sm font-medium">SubAgent</span>
+ <span class="badge badge-sm">{activeAgentSlug}</span>
+ </div>
+ <div class="flex items-center justify-between">
+ <span class="text-sm font-medium">Key</span>
+ <span class="font-mono text-xs">{activeKeyId ?? "default"}</span>
+ </div>
+ <div class="flex items-center justify-between">
+ <span class="text-sm font-medium">Model</span>
+ <span class="font-mono text-xs">{activeModelId ?? "default"}</span>
+ </div>
+ {#if hasSubModels}
+ {@const displayIdx = subActiveIdx >= 0 ? subActiveIdx : 0}
+ <div class="mt-1 pt-2 border-t border-base-content/20">
+ <div class="text-xs font-semibold mb-1">Model fallback chain</div>
+ <input
+ type="range"
+ min="0"
+ max={subModels.length - 1}
+ value={displayIdx}
+ class="range range-xs"
+ step="1"
+ disabled
+ />
+ <div class="flex w-full justify-between px-0.5 text-xs opacity-50 mt-0.5">
+ {#each subModels as _, i}
+ <span>{i + 1}</span>
+ {/each}
+ </div>
+ <div class="mt-1 flex flex-col gap-0.5">
+ {#each subModels as m, i}
+ <div class="text-xs font-mono truncate {i === displayIdx ? 'opacity-100 font-semibold' : 'opacity-50'}">
+ {i + 1}. {m.key_id} / {m.model_id}
+ </div>
+ {/each}
+ </div>
+ </div>
+ {/if}
+ <p class="text-xs text-base-content/50 mt-1">This tab was spawned by a parent agent. Settings cannot be changed.</p>
+ </div>
{:else}
<!-- Agent selection UI -->
{#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[] }