@gotgenes/pi-subagents 4.1.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/docs/plans/0054-decompose-index-into-modules.md +302 -0
- package/docs/retro/0053-extract-model-resolution-from-execute.md +30 -0
- package/docs/retro/0054-decompose-index-into-modules.md +38 -0
- package/package.json +6 -6
- package/src/index.ts +88 -1442
- package/src/notification.ts +188 -0
- package/src/renderer.ts +67 -0
- package/src/tools/agent-tool.ts +634 -0
- package/src/tools/get-result-tool.ts +99 -0
- package/src/tools/helpers.ts +21 -0
- package/src/tools/steer-tool.ts +83 -0
- package/src/ui/agent-menu.ts +685 -0
|
@@ -0,0 +1,634 @@
|
|
|
1
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
2
|
+
import { Type } from "@sinclair/typebox";
|
|
3
|
+
import { getDefaultMaxTurns, normalizeMaxTurns } from "../agent-runner.js";
|
|
4
|
+
import { getAgentConfig, resolveType } from "../agent-types.js";
|
|
5
|
+
import { resolveAgentInvocationConfig } from "../invocation-config.js";
|
|
6
|
+
import { resolveInvocationModel } from "../model-resolver.js";
|
|
7
|
+
import { createOutputFilePath, streamToOutputFile, writeInitialEntry } from "../output-file.js";
|
|
8
|
+
import type { AgentInvocation, AgentRecord, SubagentType } from "../types.js";
|
|
9
|
+
import {
|
|
10
|
+
type AgentActivity,
|
|
11
|
+
type AgentDetails,
|
|
12
|
+
buildInvocationTags,
|
|
13
|
+
describeActivity,
|
|
14
|
+
formatMs,
|
|
15
|
+
formatTurns,
|
|
16
|
+
getDisplayName,
|
|
17
|
+
getPromptModeLabel,
|
|
18
|
+
SPINNER,
|
|
19
|
+
type UICtx,
|
|
20
|
+
} from "../ui/agent-widget.js";
|
|
21
|
+
import { addUsage, type LifetimeUsage } from "../usage.js";
|
|
22
|
+
import { formatLifetimeTokens, textResult } from "./helpers.js";
|
|
23
|
+
|
|
24
|
+
// ---- Agent-tool-specific helpers ----
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Create an AgentActivity state and spawn callbacks for tracking tool usage.
|
|
28
|
+
* Used by both foreground and background paths to avoid duplication.
|
|
29
|
+
*/
|
|
30
|
+
export function createActivityTracker(maxTurns?: number, onStreamUpdate?: () => void) {
|
|
31
|
+
const state: AgentActivity = {
|
|
32
|
+
activeTools: new Map(),
|
|
33
|
+
toolUses: 0,
|
|
34
|
+
turnCount: 1,
|
|
35
|
+
maxTurns,
|
|
36
|
+
responseText: "",
|
|
37
|
+
session: undefined,
|
|
38
|
+
lifetimeUsage: { input: 0, output: 0, cacheWrite: 0 },
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const callbacks = {
|
|
42
|
+
onToolActivity: (activity: { type: "start" | "end"; toolName: string }) => {
|
|
43
|
+
if (activity.type === "start") {
|
|
44
|
+
state.activeTools.set(activity.toolName + "_" + Date.now(), activity.toolName);
|
|
45
|
+
} else {
|
|
46
|
+
for (const [key, name] of state.activeTools) {
|
|
47
|
+
if (name === activity.toolName) {
|
|
48
|
+
state.activeTools.delete(key);
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
state.toolUses++;
|
|
53
|
+
}
|
|
54
|
+
onStreamUpdate?.();
|
|
55
|
+
},
|
|
56
|
+
onTextDelta: (_delta: string, fullText: string) => {
|
|
57
|
+
state.responseText = fullText;
|
|
58
|
+
onStreamUpdate?.();
|
|
59
|
+
},
|
|
60
|
+
onTurnEnd: (turnCount: number) => {
|
|
61
|
+
state.turnCount = turnCount;
|
|
62
|
+
onStreamUpdate?.();
|
|
63
|
+
},
|
|
64
|
+
onSessionCreated: (session: any) => {
|
|
65
|
+
state.session = session;
|
|
66
|
+
},
|
|
67
|
+
onAssistantUsage: (usage: { input: number; output: number; cacheWrite: number }) => {
|
|
68
|
+
addUsage(state.lifetimeUsage, usage);
|
|
69
|
+
onStreamUpdate?.();
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
return { state, callbacks };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Parenthetical status note for completed agent result text. */
|
|
77
|
+
export function getStatusNote(status: string): string {
|
|
78
|
+
switch (status) {
|
|
79
|
+
case "aborted":
|
|
80
|
+
return " (aborted — max turns exceeded, output may be incomplete)";
|
|
81
|
+
case "steered":
|
|
82
|
+
return " (wrapped up — reached turn limit)";
|
|
83
|
+
case "stopped":
|
|
84
|
+
return " (stopped by user)";
|
|
85
|
+
default:
|
|
86
|
+
return "";
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Build AgentDetails from a base + record-specific fields. */
|
|
91
|
+
export function buildDetails(
|
|
92
|
+
base: Pick<AgentDetails, "displayName" | "description" | "subagentType" | "modelName" | "tags">,
|
|
93
|
+
record: {
|
|
94
|
+
toolUses: number;
|
|
95
|
+
startedAt: number;
|
|
96
|
+
completedAt?: number;
|
|
97
|
+
status: string;
|
|
98
|
+
error?: string;
|
|
99
|
+
id?: string;
|
|
100
|
+
session?: any;
|
|
101
|
+
lifetimeUsage: LifetimeUsage;
|
|
102
|
+
},
|
|
103
|
+
activity?: AgentActivity,
|
|
104
|
+
overrides?: Partial<AgentDetails>,
|
|
105
|
+
): AgentDetails {
|
|
106
|
+
return {
|
|
107
|
+
...base,
|
|
108
|
+
toolUses: record.toolUses,
|
|
109
|
+
tokens: formatLifetimeTokens(record),
|
|
110
|
+
turnCount: activity?.turnCount,
|
|
111
|
+
maxTurns: activity?.maxTurns,
|
|
112
|
+
durationMs: (record.completedAt ?? Date.now()) - record.startedAt,
|
|
113
|
+
status: record.status as AgentDetails["status"],
|
|
114
|
+
agentId: record.id,
|
|
115
|
+
error: record.error,
|
|
116
|
+
...overrides,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ---- Deps interface ----
|
|
121
|
+
|
|
122
|
+
/** Narrow manager interface — only the methods the Agent tool calls. */
|
|
123
|
+
export interface AgentToolManager {
|
|
124
|
+
spawn: (ctx: unknown, type: string, prompt: string, opts: object) => string;
|
|
125
|
+
spawnAndWait: (ctx: unknown, type: string, prompt: string, opts: object) => Promise<AgentRecord>;
|
|
126
|
+
resume: (id: string, prompt: string, signal: AbortSignal) => Promise<AgentRecord | undefined>;
|
|
127
|
+
getRecord: (id: string) => AgentRecord | undefined;
|
|
128
|
+
getMaxConcurrent: () => number;
|
|
129
|
+
listAgents: () => AgentRecord[];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Narrow widget interface — only the methods the Agent tool calls. */
|
|
133
|
+
export interface AgentToolWidget {
|
|
134
|
+
setUICtx: (ctx: unknown) => void;
|
|
135
|
+
ensureTimer: () => void;
|
|
136
|
+
update: () => void;
|
|
137
|
+
markFinished: (id: string) => void;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface AgentToolDeps {
|
|
141
|
+
manager: AgentToolManager;
|
|
142
|
+
widget: AgentToolWidget;
|
|
143
|
+
agentActivity: Map<string, AgentActivity>;
|
|
144
|
+
emitEvent: (name: string, data: unknown) => void;
|
|
145
|
+
reloadCustomAgents: () => void;
|
|
146
|
+
typeListText: string;
|
|
147
|
+
availableTypesText: string;
|
|
148
|
+
agentDir: string;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ---- Factory ----
|
|
152
|
+
|
|
153
|
+
/** Create the Agent tool definition (without Pi SDK wrapper). */
|
|
154
|
+
export function createAgentTool(deps: AgentToolDeps) {
|
|
155
|
+
return {
|
|
156
|
+
name: "Agent" as const,
|
|
157
|
+
label: "Agent",
|
|
158
|
+
description: `Launch a new agent to handle complex, multi-step tasks autonomously.
|
|
159
|
+
|
|
160
|
+
The Agent tool launches specialized agents that autonomously handle complex tasks. Each agent type has specific capabilities and tools available to it.
|
|
161
|
+
|
|
162
|
+
Available agent types:
|
|
163
|
+
${deps.typeListText}
|
|
164
|
+
|
|
165
|
+
Guidelines:
|
|
166
|
+
- For parallel work, use run_in_background: true on each agent. Foreground calls run sequentially — only one executes at a time.
|
|
167
|
+
- Use Explore for codebase searches and code understanding.
|
|
168
|
+
- Use Plan for architecture and implementation planning.
|
|
169
|
+
- Use general-purpose for complex tasks that need file editing.
|
|
170
|
+
- Provide clear, detailed prompts so the agent can work autonomously.
|
|
171
|
+
- Agent results are returned as text — summarize them for the user.
|
|
172
|
+
- Use run_in_background for work you don't need immediately. You will be notified when it completes.
|
|
173
|
+
- Use resume with an agent ID to continue a previous agent's work.
|
|
174
|
+
- Use steer_subagent to send mid-run messages to a running background agent.
|
|
175
|
+
- Use model to specify a different model (as "provider/modelId", or fuzzy e.g. "haiku", "sonnet").
|
|
176
|
+
- Use thinking to control extended thinking level.
|
|
177
|
+
- Use inherit_context if the agent needs the parent conversation history.
|
|
178
|
+
- Use isolation: "worktree" to run the agent in an isolated git worktree (safe parallel file modifications).`,
|
|
179
|
+
parameters: Type.Object({
|
|
180
|
+
prompt: Type.String({
|
|
181
|
+
description: "The task for the agent to perform.",
|
|
182
|
+
}),
|
|
183
|
+
description: Type.String({
|
|
184
|
+
description: "A short (3-5 word) description of the task (shown in UI).",
|
|
185
|
+
}),
|
|
186
|
+
subagent_type: Type.String({
|
|
187
|
+
description: `The type of specialized agent to use. Available types: ${deps.availableTypesText}. Custom agents from .pi/agents/<name>.md (project) or ${deps.agentDir}/agents/<name>.md (global) are also available.`,
|
|
188
|
+
}),
|
|
189
|
+
model: Type.Optional(
|
|
190
|
+
Type.String({
|
|
191
|
+
description:
|
|
192
|
+
'Optional model override. Accepts "provider/modelId" or fuzzy name (e.g. "haiku", "sonnet"). Omit to use the agent type\'s default.',
|
|
193
|
+
}),
|
|
194
|
+
),
|
|
195
|
+
thinking: Type.Optional(
|
|
196
|
+
Type.String({
|
|
197
|
+
description:
|
|
198
|
+
"Thinking level: off, minimal, low, medium, high, xhigh. Overrides agent default.",
|
|
199
|
+
}),
|
|
200
|
+
),
|
|
201
|
+
max_turns: Type.Optional(
|
|
202
|
+
Type.Number({
|
|
203
|
+
description:
|
|
204
|
+
"Maximum number of agentic turns before stopping. Omit for unlimited (default).",
|
|
205
|
+
minimum: 1,
|
|
206
|
+
}),
|
|
207
|
+
),
|
|
208
|
+
run_in_background: Type.Optional(
|
|
209
|
+
Type.Boolean({
|
|
210
|
+
description:
|
|
211
|
+
"Set to true to run in background. Returns agent ID immediately. You will be notified on completion.",
|
|
212
|
+
}),
|
|
213
|
+
),
|
|
214
|
+
resume: Type.Optional(
|
|
215
|
+
Type.String({
|
|
216
|
+
description: "Optional agent ID to resume from. Continues from previous context.",
|
|
217
|
+
}),
|
|
218
|
+
),
|
|
219
|
+
isolated: Type.Optional(
|
|
220
|
+
Type.Boolean({
|
|
221
|
+
description: "If true, agent gets no extension/MCP tools — only built-in tools.",
|
|
222
|
+
}),
|
|
223
|
+
),
|
|
224
|
+
inherit_context: Type.Optional(
|
|
225
|
+
Type.Boolean({
|
|
226
|
+
description:
|
|
227
|
+
"If true, fork parent conversation into the agent. Default: false (fresh context).",
|
|
228
|
+
}),
|
|
229
|
+
),
|
|
230
|
+
isolation: Type.Optional(
|
|
231
|
+
Type.Literal("worktree", {
|
|
232
|
+
description:
|
|
233
|
+
'Set to "worktree" to run the agent in a temporary git worktree (isolated copy of the repo). Changes are saved to a branch on completion.',
|
|
234
|
+
}),
|
|
235
|
+
),
|
|
236
|
+
}),
|
|
237
|
+
|
|
238
|
+
// ---- Custom rendering: Claude Code style ----
|
|
239
|
+
|
|
240
|
+
renderCall(args: Record<string, unknown>, theme: any) {
|
|
241
|
+
const displayName = args.subagent_type
|
|
242
|
+
? getDisplayName(args.subagent_type as string)
|
|
243
|
+
: "Agent";
|
|
244
|
+
const desc = (args.description as string) ?? "";
|
|
245
|
+
return new Text(
|
|
246
|
+
"▸ " +
|
|
247
|
+
theme.fg("toolTitle", theme.bold(displayName)) +
|
|
248
|
+
(desc ? " " + theme.fg("muted", desc) : ""),
|
|
249
|
+
0,
|
|
250
|
+
0,
|
|
251
|
+
);
|
|
252
|
+
},
|
|
253
|
+
|
|
254
|
+
renderResult(result: any, { expanded, isPartial }: any, theme: any) {
|
|
255
|
+
const details = result.details as AgentDetails | undefined;
|
|
256
|
+
if (!details) {
|
|
257
|
+
const text = result.content[0]?.type === "text" ? result.content[0].text : "";
|
|
258
|
+
return new Text(text, 0, 0);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Helper: build "haiku · thinking: high · ⟳5≤30 · 3 tool uses · 33.8k tokens" stats string
|
|
262
|
+
const stats = (d: AgentDetails) => {
|
|
263
|
+
const parts: string[] = [];
|
|
264
|
+
if (d.modelName) parts.push(d.modelName);
|
|
265
|
+
if (d.tags) parts.push(...d.tags);
|
|
266
|
+
if (d.turnCount != null && d.turnCount > 0) {
|
|
267
|
+
parts.push(formatTurns(d.turnCount, d.maxTurns));
|
|
268
|
+
}
|
|
269
|
+
if (d.toolUses > 0)
|
|
270
|
+
parts.push(`${d.toolUses} tool use${d.toolUses === 1 ? "" : "s"}`);
|
|
271
|
+
if (d.tokens) parts.push(d.tokens);
|
|
272
|
+
return parts
|
|
273
|
+
.map((p) => theme.fg("dim", p))
|
|
274
|
+
.join(" " + theme.fg("dim", "·") + " ");
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
// ---- While running (streaming) ----
|
|
278
|
+
if (isPartial || details.status === "running") {
|
|
279
|
+
const frame = SPINNER[details.spinnerFrame ?? 0];
|
|
280
|
+
const s = stats(details);
|
|
281
|
+
let line = theme.fg("accent", frame) + (s ? " " + s : "");
|
|
282
|
+
line += "\n" + theme.fg("dim", ` ⎿ ${details.activity ?? "thinking…"}`);
|
|
283
|
+
return new Text(line, 0, 0);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ---- Background agent launched ----
|
|
287
|
+
if (details.status === "background") {
|
|
288
|
+
return new Text(
|
|
289
|
+
theme.fg("dim", ` ⎿ Running in background (ID: ${details.agentId})`),
|
|
290
|
+
0,
|
|
291
|
+
0,
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ---- Completed / Steered ----
|
|
296
|
+
if (details.status === "completed" || details.status === "steered") {
|
|
297
|
+
const duration = formatMs(details.durationMs);
|
|
298
|
+
const isSteered = details.status === "steered";
|
|
299
|
+
const icon = isSteered ? theme.fg("warning", "✓") : theme.fg("success", "✓");
|
|
300
|
+
const s = stats(details);
|
|
301
|
+
let line = icon + (s ? " " + s : "");
|
|
302
|
+
line += " " + theme.fg("dim", "·") + " " + theme.fg("dim", duration);
|
|
303
|
+
|
|
304
|
+
if (expanded) {
|
|
305
|
+
const resultText =
|
|
306
|
+
result.content[0]?.type === "text" ? result.content[0].text : "";
|
|
307
|
+
if (resultText) {
|
|
308
|
+
const lines = resultText.split("\n").slice(0, 50);
|
|
309
|
+
for (const l of lines) {
|
|
310
|
+
line += "\n" + theme.fg("dim", ` ${l}`);
|
|
311
|
+
}
|
|
312
|
+
if (resultText.split("\n").length > 50) {
|
|
313
|
+
line +=
|
|
314
|
+
"\n" +
|
|
315
|
+
theme.fg(
|
|
316
|
+
"muted",
|
|
317
|
+
" ... (use get_subagent_result with verbose for full output)",
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
} else {
|
|
322
|
+
const doneText = isSteered ? "Wrapped up (turn limit)" : "Done";
|
|
323
|
+
line += "\n" + theme.fg("dim", ` ⎿ ${doneText}`);
|
|
324
|
+
}
|
|
325
|
+
return new Text(line, 0, 0);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ---- Stopped (user-initiated abort) ----
|
|
329
|
+
if (details.status === "stopped") {
|
|
330
|
+
const s = stats(details);
|
|
331
|
+
let line = theme.fg("dim", "■") + (s ? " " + s : "");
|
|
332
|
+
line += "\n" + theme.fg("dim", " ⎿ Stopped");
|
|
333
|
+
return new Text(line, 0, 0);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ---- Error / Aborted (hard max_turns) ----
|
|
337
|
+
const s = stats(details);
|
|
338
|
+
let line = theme.fg("error", "✗") + (s ? " " + s : "");
|
|
339
|
+
|
|
340
|
+
if (details.status === "error") {
|
|
341
|
+
line += "\n" + theme.fg("error", ` ⎿ Error: ${details.error ?? "unknown"}`);
|
|
342
|
+
} else {
|
|
343
|
+
line += "\n" + theme.fg("warning", " ⎿ Aborted (max turns exceeded)");
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return new Text(line, 0, 0);
|
|
347
|
+
},
|
|
348
|
+
|
|
349
|
+
// ---- Execute ----
|
|
350
|
+
|
|
351
|
+
execute: async (
|
|
352
|
+
toolCallId: string,
|
|
353
|
+
params: Record<string, unknown>,
|
|
354
|
+
signal: AbortSignal,
|
|
355
|
+
onUpdate: ((update: unknown) => void) | undefined,
|
|
356
|
+
ctx: any,
|
|
357
|
+
) => {
|
|
358
|
+
// Ensure we have UI context for widget rendering
|
|
359
|
+
deps.widget.setUICtx(ctx.ui as UICtx);
|
|
360
|
+
|
|
361
|
+
// Reload custom agents so new .pi/agents/*.md files are picked up without restart
|
|
362
|
+
deps.reloadCustomAgents();
|
|
363
|
+
|
|
364
|
+
const rawType = params.subagent_type as SubagentType;
|
|
365
|
+
const resolved = resolveType(rawType);
|
|
366
|
+
const subagentType = resolved ?? "general-purpose";
|
|
367
|
+
const fellBack = resolved === undefined;
|
|
368
|
+
|
|
369
|
+
const displayName = getDisplayName(subagentType);
|
|
370
|
+
|
|
371
|
+
// Get agent config (if any)
|
|
372
|
+
const customConfig = getAgentConfig(subagentType);
|
|
373
|
+
|
|
374
|
+
const resolvedConfig = resolveAgentInvocationConfig(customConfig, params);
|
|
375
|
+
|
|
376
|
+
// Resolve model from agent config first; tool-call params only fill gaps.
|
|
377
|
+
const resolution = resolveInvocationModel(
|
|
378
|
+
ctx.model,
|
|
379
|
+
resolvedConfig.modelInput,
|
|
380
|
+
resolvedConfig.modelFromParams,
|
|
381
|
+
ctx.modelRegistry,
|
|
382
|
+
);
|
|
383
|
+
if (resolution.error) return textResult(resolution.error);
|
|
384
|
+
const model = resolution.model;
|
|
385
|
+
|
|
386
|
+
const thinking = resolvedConfig.thinking;
|
|
387
|
+
const inheritContext = resolvedConfig.inheritContext;
|
|
388
|
+
const runInBackground = resolvedConfig.runInBackground;
|
|
389
|
+
const isolated = resolvedConfig.isolated;
|
|
390
|
+
const isolation = resolvedConfig.isolation;
|
|
391
|
+
|
|
392
|
+
const parentModelId = ctx.model?.id;
|
|
393
|
+
const effectiveModelId = model?.id;
|
|
394
|
+
const modelName =
|
|
395
|
+
effectiveModelId && effectiveModelId !== parentModelId
|
|
396
|
+
? (model?.name ?? effectiveModelId).replace(/^Claude\s+/i, "").toLowerCase()
|
|
397
|
+
: undefined;
|
|
398
|
+
const effectiveMaxTurns = normalizeMaxTurns(
|
|
399
|
+
resolvedConfig.maxTurns ?? getDefaultMaxTurns(),
|
|
400
|
+
);
|
|
401
|
+
const agentInvocation: AgentInvocation = {
|
|
402
|
+
modelName,
|
|
403
|
+
thinking,
|
|
404
|
+
maxTurns: normalizeMaxTurns(resolvedConfig.maxTurns),
|
|
405
|
+
isolated,
|
|
406
|
+
inheritContext,
|
|
407
|
+
runInBackground,
|
|
408
|
+
isolation,
|
|
409
|
+
};
|
|
410
|
+
const modeLabel = getPromptModeLabel(subagentType);
|
|
411
|
+
const { tags: invocationTags } = buildInvocationTags(agentInvocation);
|
|
412
|
+
const agentTags = modeLabel ? [modeLabel, ...invocationTags] : invocationTags;
|
|
413
|
+
const detailBase = {
|
|
414
|
+
displayName,
|
|
415
|
+
description: params.description as string,
|
|
416
|
+
subagentType,
|
|
417
|
+
modelName,
|
|
418
|
+
tags: agentTags.length > 0 ? agentTags : undefined,
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
// Resume existing agent
|
|
422
|
+
if (params.resume) {
|
|
423
|
+
const existing = deps.manager.getRecord(params.resume as string);
|
|
424
|
+
if (!existing) {
|
|
425
|
+
return textResult(
|
|
426
|
+
`Agent not found: "${params.resume}". It may have been cleaned up.`,
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
if (!existing.session) {
|
|
430
|
+
return textResult(
|
|
431
|
+
`Agent "${params.resume}" has no active session to resume.`,
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
const record = await deps.manager.resume(
|
|
435
|
+
params.resume as string,
|
|
436
|
+
params.prompt as string,
|
|
437
|
+
signal,
|
|
438
|
+
);
|
|
439
|
+
if (!record) {
|
|
440
|
+
return textResult(`Failed to resume agent "${params.resume}".`);
|
|
441
|
+
}
|
|
442
|
+
return textResult(
|
|
443
|
+
record.result?.trim() || record.error?.trim() || "No output.",
|
|
444
|
+
buildDetails(detailBase, record),
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Background execution
|
|
449
|
+
if (runInBackground) {
|
|
450
|
+
const { state: bgState, callbacks: bgCallbacks } =
|
|
451
|
+
createActivityTracker(effectiveMaxTurns);
|
|
452
|
+
|
|
453
|
+
// Wrap onSessionCreated to wire output file streaming.
|
|
454
|
+
let id: string;
|
|
455
|
+
const origBgOnSession = bgCallbacks.onSessionCreated;
|
|
456
|
+
bgCallbacks.onSessionCreated = (session: any) => {
|
|
457
|
+
origBgOnSession(session);
|
|
458
|
+
const rec = deps.manager.getRecord(id);
|
|
459
|
+
if (rec?.outputFile) {
|
|
460
|
+
rec.outputCleanup = streamToOutputFile(session, rec.outputFile, id, ctx.cwd);
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
try {
|
|
465
|
+
id = deps.manager.spawn(ctx, subagentType, params.prompt as string, {
|
|
466
|
+
description: params.description,
|
|
467
|
+
model,
|
|
468
|
+
maxTurns: effectiveMaxTurns,
|
|
469
|
+
isolated,
|
|
470
|
+
inheritContext,
|
|
471
|
+
thinkingLevel: thinking,
|
|
472
|
+
isBackground: true,
|
|
473
|
+
isolation,
|
|
474
|
+
invocation: agentInvocation,
|
|
475
|
+
...bgCallbacks,
|
|
476
|
+
});
|
|
477
|
+
} catch (err) {
|
|
478
|
+
return textResult(err instanceof Error ? err.message : String(err));
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Set output file synchronously after spawn
|
|
482
|
+
const record = deps.manager.getRecord(id);
|
|
483
|
+
if (record) {
|
|
484
|
+
record.toolCallId = toolCallId;
|
|
485
|
+
record.outputFile = createOutputFilePath(
|
|
486
|
+
ctx.cwd,
|
|
487
|
+
id,
|
|
488
|
+
ctx.sessionManager.getSessionId(),
|
|
489
|
+
);
|
|
490
|
+
writeInitialEntry(record.outputFile, id, params.prompt as string, ctx.cwd);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
deps.agentActivity.set(id, bgState);
|
|
494
|
+
deps.widget.ensureTimer();
|
|
495
|
+
deps.widget.update();
|
|
496
|
+
|
|
497
|
+
// Emit created event
|
|
498
|
+
deps.emitEvent("subagents:created", {
|
|
499
|
+
id,
|
|
500
|
+
type: subagentType,
|
|
501
|
+
description: params.description,
|
|
502
|
+
isBackground: true,
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
const isQueued = record?.status === "queued";
|
|
506
|
+
return textResult(
|
|
507
|
+
`Agent ${isQueued ? "queued" : "started"} in background.\n` +
|
|
508
|
+
`Agent ID: ${id}\n` +
|
|
509
|
+
`Type: ${displayName}\n` +
|
|
510
|
+
`Description: ${params.description}\n` +
|
|
511
|
+
(record?.outputFile ? `Output file: ${record.outputFile}\n` : "") +
|
|
512
|
+
(isQueued
|
|
513
|
+
? `Position: queued (max ${deps.manager.getMaxConcurrent()} concurrent)\n`
|
|
514
|
+
: "") +
|
|
515
|
+
`\nYou will be notified when this agent completes.\n` +
|
|
516
|
+
`Use get_subagent_result to retrieve full results, or steer_subagent to send it messages.\n` +
|
|
517
|
+
`Do not duplicate this agent's work.`,
|
|
518
|
+
{
|
|
519
|
+
...detailBase,
|
|
520
|
+
toolUses: 0,
|
|
521
|
+
tokens: "",
|
|
522
|
+
durationMs: 0,
|
|
523
|
+
status: "background" as const,
|
|
524
|
+
agentId: id,
|
|
525
|
+
},
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Foreground (synchronous) execution — stream progress via onUpdate
|
|
530
|
+
let spinnerFrame = 0;
|
|
531
|
+
const startedAt = Date.now();
|
|
532
|
+
let fgId: string | undefined;
|
|
533
|
+
|
|
534
|
+
const streamUpdate = () => {
|
|
535
|
+
const details: AgentDetails = {
|
|
536
|
+
...detailBase,
|
|
537
|
+
toolUses: fgState.toolUses,
|
|
538
|
+
tokens: formatLifetimeTokens(fgState),
|
|
539
|
+
turnCount: fgState.turnCount,
|
|
540
|
+
maxTurns: fgState.maxTurns,
|
|
541
|
+
durationMs: Date.now() - startedAt,
|
|
542
|
+
status: "running",
|
|
543
|
+
activity: describeActivity(fgState.activeTools, fgState.responseText),
|
|
544
|
+
spinnerFrame: spinnerFrame % SPINNER.length,
|
|
545
|
+
};
|
|
546
|
+
onUpdate?.({
|
|
547
|
+
content: [{ type: "text", text: `${fgState.toolUses} tool uses...` }],
|
|
548
|
+
details: details as any,
|
|
549
|
+
});
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
const { state: fgState, callbacks: fgCallbacks } = createActivityTracker(
|
|
553
|
+
effectiveMaxTurns,
|
|
554
|
+
streamUpdate,
|
|
555
|
+
);
|
|
556
|
+
|
|
557
|
+
// Wire session creation to register in widget
|
|
558
|
+
const origOnSession = fgCallbacks.onSessionCreated;
|
|
559
|
+
fgCallbacks.onSessionCreated = (session: any) => {
|
|
560
|
+
origOnSession(session);
|
|
561
|
+
for (const a of deps.manager.listAgents()) {
|
|
562
|
+
if (a.session === session) {
|
|
563
|
+
fgId = a.id;
|
|
564
|
+
deps.agentActivity.set(a.id, fgState);
|
|
565
|
+
deps.widget.ensureTimer();
|
|
566
|
+
break;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
// Animate spinner at ~80ms (smooth rotation through 10 braille frames)
|
|
572
|
+
const spinnerInterval = setInterval(() => {
|
|
573
|
+
spinnerFrame++;
|
|
574
|
+
streamUpdate();
|
|
575
|
+
}, 80);
|
|
576
|
+
|
|
577
|
+
streamUpdate();
|
|
578
|
+
|
|
579
|
+
let record: AgentRecord;
|
|
580
|
+
try {
|
|
581
|
+
record = await deps.manager.spawnAndWait(
|
|
582
|
+
ctx,
|
|
583
|
+
subagentType,
|
|
584
|
+
params.prompt as string,
|
|
585
|
+
{
|
|
586
|
+
description: params.description,
|
|
587
|
+
model,
|
|
588
|
+
maxTurns: effectiveMaxTurns,
|
|
589
|
+
isolated,
|
|
590
|
+
inheritContext,
|
|
591
|
+
thinkingLevel: thinking,
|
|
592
|
+
isolation,
|
|
593
|
+
invocation: agentInvocation,
|
|
594
|
+
signal,
|
|
595
|
+
...fgCallbacks,
|
|
596
|
+
},
|
|
597
|
+
);
|
|
598
|
+
} catch (err) {
|
|
599
|
+
clearInterval(spinnerInterval);
|
|
600
|
+
return textResult(err instanceof Error ? err.message : String(err));
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
clearInterval(spinnerInterval);
|
|
604
|
+
|
|
605
|
+
// Clean up foreground agent from widget
|
|
606
|
+
if (fgId) {
|
|
607
|
+
deps.agentActivity.delete(fgId);
|
|
608
|
+
deps.widget.markFinished(fgId);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Get final token count
|
|
612
|
+
const tokenText = formatLifetimeTokens(fgState);
|
|
613
|
+
|
|
614
|
+
const details = buildDetails(detailBase, record, fgState, { tokens: tokenText });
|
|
615
|
+
|
|
616
|
+
const fallbackNote = fellBack
|
|
617
|
+
? `Note: Unknown agent type "${rawType}" — using general-purpose.\n\n`
|
|
618
|
+
: "";
|
|
619
|
+
|
|
620
|
+
if (record.status === "error") {
|
|
621
|
+
return textResult(`${fallbackNote}Agent failed: ${record.error}`, details);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const durationMs = (record.completedAt ?? Date.now()) - record.startedAt;
|
|
625
|
+
const statsParts = [`${record.toolUses} tool uses`];
|
|
626
|
+
if (tokenText) statsParts.push(tokenText);
|
|
627
|
+
return textResult(
|
|
628
|
+
`${fallbackNote}Agent completed in ${formatMs(durationMs)} (${statsParts.join(", ")})${getStatusNote(record.status)}.\n\n` +
|
|
629
|
+
(record.result?.trim() || "No output."),
|
|
630
|
+
details,
|
|
631
|
+
);
|
|
632
|
+
},
|
|
633
|
+
};
|
|
634
|
+
}
|