@mystilleef/pi-subagent 0.5.0 → 0.7.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/README.md +1 -1
- package/package.json +13 -11
- package/src/agent/agent-cache.ts +258 -0
- package/src/agent/agents.ts +232 -0
- package/src/child/child-events.ts +119 -0
- package/src/{process.ts → child/process.ts} +268 -122
- package/src/{prompt-contract.ts → child/prompt-contract.ts} +2 -0
- package/src/index.ts +43 -24
- package/src/{jobs-command.ts → orchestration/jobs-command.ts} +2 -3
- package/src/{run.ts → orchestration/run.ts} +2 -2
- package/src/{subagent-orchestrator.ts → orchestration/subagent-orchestrator.ts} +246 -124
- package/src/{normalize.ts → output/normalize.ts} +35 -49
- package/src/{summary.ts → output/summary.ts} +4 -2
- package/src/{ui.ts → output/ui.ts} +45 -48
- package/src/{progress-state.ts → progress/progress-state.ts} +164 -52
- package/src/{progress.ts → progress/progress.ts} +21 -21
- package/src/{result-details.ts → progress/result-details.ts} +55 -17
- package/src/shared/instance-name.ts +117 -0
- package/src/{types.ts → shared/types.ts} +13 -2
- package/src/{utils.ts → shared/utils.ts} +4 -6
- package/src/agent-cache.ts +0 -41
- package/src/agents.ts +0 -178
- package/src/child-events.ts +0 -32
- package/src/instance-name.ts +0 -164
- /package/src/{termination.ts → child/termination.ts} +0 -0
- /package/src/{cancel-command.ts → orchestration/cancel-command.ts} +0 -0
- /package/src/{run-command.ts → orchestration/run-command.ts} +0 -0
- /package/src/{run-registry.ts → orchestration/run-registry.ts} +0 -0
|
@@ -7,23 +7,27 @@
|
|
|
7
7
|
import { type ChildProcess, spawn } from "node:child_process";
|
|
8
8
|
import * as fs from "node:fs";
|
|
9
9
|
import readline from "node:readline";
|
|
10
|
-
import { getModel, type Message } from "@earendil-works/pi-ai";
|
|
11
|
-
import type { AgentConfig, ThinkingLevel } from "./agents.js";
|
|
12
|
-
import { parseChildEventLine } from "./child-events.js";
|
|
13
|
-
import { makeToolPreview } from "./progress.js";
|
|
14
|
-
import { isToolCallPart } from "./progress-state.js";
|
|
15
|
-
import { appendSubagentResultContract } from "./prompt-contract.js";
|
|
16
10
|
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
11
|
+
clampThinkingLevel,
|
|
12
|
+
getModel,
|
|
13
|
+
getSupportedThinkingLevels,
|
|
14
|
+
type Message,
|
|
15
|
+
type ModelThinkingLevel,
|
|
16
|
+
} from "@earendil-works/pi-ai";
|
|
17
|
+
import type { AgentConfig, ThinkingLevel } from "../agent/agents.js";
|
|
18
|
+
import { getFinalOutput } from "../output/ui.js";
|
|
19
|
+
import { makeToolPreview, renderToolActivity } from "../progress/progress.js";
|
|
20
|
+
import {
|
|
21
|
+
isToolCallPart,
|
|
22
|
+
SENSITIVE_PATTERN,
|
|
23
|
+
} from "../progress/progress-state.js";
|
|
20
24
|
import type {
|
|
21
25
|
OnUpdateCallback,
|
|
22
26
|
SingleResult,
|
|
23
27
|
StreamingProgress,
|
|
24
28
|
SubagentDetails,
|
|
25
|
-
|
|
26
|
-
|
|
29
|
+
ToolActivity,
|
|
30
|
+
} from "../shared/types.js";
|
|
27
31
|
import {
|
|
28
32
|
detectMessageError,
|
|
29
33
|
getPiInvocation,
|
|
@@ -32,16 +36,59 @@ import {
|
|
|
32
36
|
subagentDepthEnv,
|
|
33
37
|
truncateOutput,
|
|
34
38
|
writePromptToTempFile,
|
|
35
|
-
} from "
|
|
39
|
+
} from "../shared/utils.js";
|
|
40
|
+
import {
|
|
41
|
+
type ChildKnownEvent,
|
|
42
|
+
parseChildEventLine,
|
|
43
|
+
TOOL_EXECUTION_UPDATE_EVENT,
|
|
44
|
+
} from "./child-events.js";
|
|
45
|
+
import { appendSubagentResultContract } from "./prompt-contract.js";
|
|
46
|
+
import {
|
|
47
|
+
getProcessTreeSpawnOptions,
|
|
48
|
+
terminateChildProcess,
|
|
49
|
+
} from "./termination.js";
|
|
36
50
|
|
|
37
51
|
const MAX_STDERR_BYTES = 10_000;
|
|
38
52
|
const AGENT_END_GRACE_MS = 250;
|
|
53
|
+
export function resolveThinkingLevel(
|
|
54
|
+
requested: ThinkingLevel,
|
|
55
|
+
provider: string,
|
|
56
|
+
modelId: string,
|
|
57
|
+
): { level: ThinkingLevel; warning?: string } {
|
|
58
|
+
const model = getModel(provider as never, modelId as never);
|
|
59
|
+
if (!model) return { level: requested };
|
|
60
|
+
const mkWarning = (effective: ThinkingLevel) =>
|
|
61
|
+
`Thinking level "${requested}" not supported by model "${provider}/${modelId}"; using "${effective}" instead`;
|
|
62
|
+
if (model.reasoning === false) {
|
|
63
|
+
return { level: "off", warning: mkWarning("off") };
|
|
64
|
+
}
|
|
65
|
+
if (!model.thinkingLevelMap) return { level: requested };
|
|
66
|
+
const supported = getSupportedThinkingLevels(model);
|
|
67
|
+
if (supported.length === 0) return { level: requested };
|
|
68
|
+
const clamped = clampThinkingLevel(
|
|
69
|
+
model,
|
|
70
|
+
requested as ModelThinkingLevel,
|
|
71
|
+
) as ThinkingLevel;
|
|
72
|
+
if (clamped === requested) return { level: requested };
|
|
73
|
+
return { level: clamped, warning: mkWarning(clamped) };
|
|
74
|
+
}
|
|
39
75
|
|
|
40
|
-
const MAX_SUBAGENT_DEPTH =
|
|
76
|
+
const MAX_SUBAGENT_DEPTH = 2;
|
|
41
77
|
export const TOOL_RESULT_FAILED_MESSAGE = "Subagent tool result failed.";
|
|
42
78
|
|
|
43
79
|
type RuntimeResult = SingleResult & { messages: Message[] };
|
|
44
80
|
|
|
81
|
+
export class SubagentAbortError extends Error {
|
|
82
|
+
constructor(public readonly result: SingleResult) {
|
|
83
|
+
super("Subagent was aborted");
|
|
84
|
+
this.name = "SubagentAbortError";
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
type TempPrompt = { dir: string; filePath: string };
|
|
89
|
+
|
|
90
|
+
type PromptSetupResult = { tmpPrompt: TempPrompt | null } | { error: unknown };
|
|
91
|
+
|
|
45
92
|
interface SubagentState {
|
|
46
93
|
result: RuntimeResult;
|
|
47
94
|
spawnError?: Error;
|
|
@@ -50,10 +97,6 @@ interface SubagentState {
|
|
|
50
97
|
terminationPromise?: Promise<unknown>;
|
|
51
98
|
}
|
|
52
99
|
|
|
53
|
-
/**
|
|
54
|
-
* Appends data to a string while ensuring the result does not exceed a maximum byte limit.
|
|
55
|
-
* Used to prevent memory exhaustion when capturing child process stderr.
|
|
56
|
-
*/
|
|
57
100
|
function appendWithByteLimit(
|
|
58
101
|
current: string,
|
|
59
102
|
data: string,
|
|
@@ -72,7 +115,10 @@ function resolveContextWindowTokens(msg: Message): number | undefined {
|
|
|
72
115
|
const m = msg as unknown as Record<string, unknown>;
|
|
73
116
|
if (typeof m.provider !== "string" || typeof m.model !== "string") return;
|
|
74
117
|
try {
|
|
75
|
-
const
|
|
118
|
+
const contextWindow = getModel(
|
|
119
|
+
m.provider as never,
|
|
120
|
+
m.model as never,
|
|
121
|
+
)?.contextWindow;
|
|
76
122
|
return Number.isFinite(contextWindow) && contextWindow > 0
|
|
77
123
|
? contextWindow
|
|
78
124
|
: undefined;
|
|
@@ -81,9 +127,6 @@ function resolveContextWindowTokens(msg: Message): number | undefined {
|
|
|
81
127
|
}
|
|
82
128
|
}
|
|
83
129
|
|
|
84
|
-
/**
|
|
85
|
-
* Normalizes AbortSignal reasons into human-readable strings.
|
|
86
|
-
*/
|
|
87
130
|
function getAbortReason(signal: AbortSignal): string {
|
|
88
131
|
const { reason } = signal;
|
|
89
132
|
if (reason instanceof Error && reason.message) return reason.message;
|
|
@@ -129,6 +172,25 @@ function getAgentEndTimeoutExitCode(
|
|
|
129
172
|
* Safety: Implements a dual-timer strategy (idle and hard) to ensure streams
|
|
130
173
|
* are destroyed and promises settled even if the process or its pipes hang.
|
|
131
174
|
*/
|
|
175
|
+
function createProcessCleanup(proc: ChildProcess, idleMs: number) {
|
|
176
|
+
let idleTimer: NodeJS.Timeout | undefined;
|
|
177
|
+
const destroyStreams = () => {
|
|
178
|
+
proc.stdout?.destroy();
|
|
179
|
+
proc.stderr?.destroy();
|
|
180
|
+
};
|
|
181
|
+
return {
|
|
182
|
+
armIdleTimer: () => {
|
|
183
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
184
|
+
idleTimer = setTimeout(destroyStreams, idleMs);
|
|
185
|
+
idleTimer.unref?.();
|
|
186
|
+
},
|
|
187
|
+
clearIdleTimer: () => {
|
|
188
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
189
|
+
},
|
|
190
|
+
destroyStreams,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
132
194
|
async function waitForSubagentProcess(
|
|
133
195
|
proc: ChildProcess,
|
|
134
196
|
idleMs = 100,
|
|
@@ -138,27 +200,13 @@ async function waitForSubagentProcess(
|
|
|
138
200
|
let exitCode: number | null = null;
|
|
139
201
|
let exited = false;
|
|
140
202
|
let settled = false;
|
|
141
|
-
|
|
142
|
-
|
|
203
|
+
const cleanup = createProcessCleanup(proc, idleMs);
|
|
143
204
|
const done = () => {
|
|
144
205
|
if (settled) return;
|
|
145
206
|
settled = true;
|
|
146
|
-
|
|
207
|
+
cleanup.clearIdleTimer();
|
|
147
208
|
resolve(exitCode);
|
|
148
209
|
};
|
|
149
|
-
|
|
150
|
-
const destroyStreams = () => {
|
|
151
|
-
proc.stdout?.destroy();
|
|
152
|
-
proc.stderr?.destroy();
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
const armIdleTimer = () => {
|
|
156
|
-
if (!exited) return;
|
|
157
|
-
if (idleTimer) clearTimeout(idleTimer);
|
|
158
|
-
idleTimer = setTimeout(destroyStreams, idleMs);
|
|
159
|
-
idleTimer.unref?.();
|
|
160
|
-
};
|
|
161
|
-
|
|
162
210
|
proc.on("close", done);
|
|
163
211
|
proc.on("error", () => {
|
|
164
212
|
exitCode = 1;
|
|
@@ -168,12 +216,15 @@ async function waitForSubagentProcess(
|
|
|
168
216
|
proc.on("exit", (code) => {
|
|
169
217
|
exitCode = code;
|
|
170
218
|
exited = true;
|
|
171
|
-
armIdleTimer();
|
|
172
|
-
const hardTimer = setTimeout(destroyStreams, hardMs);
|
|
219
|
+
cleanup.armIdleTimer();
|
|
220
|
+
const hardTimer = setTimeout(cleanup.destroyStreams, hardMs);
|
|
173
221
|
hardTimer.unref?.();
|
|
174
222
|
});
|
|
175
|
-
|
|
176
|
-
|
|
223
|
+
const onStreamData = () => {
|
|
224
|
+
if (exited) cleanup.armIdleTimer();
|
|
225
|
+
};
|
|
226
|
+
proc.stdout?.on("data", onStreamData);
|
|
227
|
+
proc.stderr?.on("data", onStreamData);
|
|
177
228
|
});
|
|
178
229
|
}
|
|
179
230
|
|
|
@@ -187,6 +238,16 @@ function buildModelDisplay(
|
|
|
187
238
|
return thinking ? `thinking:${thinking}` : undefined;
|
|
188
239
|
}
|
|
189
240
|
|
|
241
|
+
const EMPTY_USAGE = {
|
|
242
|
+
input: 0,
|
|
243
|
+
output: 0,
|
|
244
|
+
cacheRead: 0,
|
|
245
|
+
cacheWrite: 0,
|
|
246
|
+
cost: 0,
|
|
247
|
+
contextTokens: 0,
|
|
248
|
+
turns: 0,
|
|
249
|
+
};
|
|
250
|
+
|
|
190
251
|
function initRuntimeResult(
|
|
191
252
|
agentName: string,
|
|
192
253
|
source: "user" | "project" | "unknown",
|
|
@@ -201,52 +262,42 @@ function initRuntimeResult(
|
|
|
201
262
|
finalOutput: "",
|
|
202
263
|
messages: [],
|
|
203
264
|
stderr: "",
|
|
204
|
-
usage: {
|
|
205
|
-
input: 0,
|
|
206
|
-
output: 0,
|
|
207
|
-
cacheRead: 0,
|
|
208
|
-
cacheWrite: 0,
|
|
209
|
-
cost: 0,
|
|
210
|
-
contextTokens: 0,
|
|
211
|
-
turns: 0,
|
|
212
|
-
},
|
|
265
|
+
usage: { ...EMPTY_USAGE },
|
|
213
266
|
model: modelDisplay,
|
|
214
267
|
};
|
|
215
268
|
}
|
|
216
269
|
|
|
270
|
+
function accumulateUsage(result: RuntimeResult, msg: Message): void {
|
|
271
|
+
if (msg.role !== "assistant") return;
|
|
272
|
+
result.usage.turns++;
|
|
273
|
+
const { usage } = msg;
|
|
274
|
+
if (!usage) return;
|
|
275
|
+
result.usage.input += usage.input || 0;
|
|
276
|
+
result.usage.output += usage.output || 0;
|
|
277
|
+
result.usage.cacheRead += usage.cacheRead || 0;
|
|
278
|
+
result.usage.cacheWrite += usage.cacheWrite || 0;
|
|
279
|
+
result.usage.cost += usage.cost?.total || 0;
|
|
280
|
+
result.usage.contextTokens = usage.totalTokens || 0;
|
|
281
|
+
result.usage.contextWindowTokens =
|
|
282
|
+
resolveContextWindowTokens(msg) ?? result.usage.contextWindowTokens;
|
|
283
|
+
}
|
|
284
|
+
|
|
217
285
|
function addMessageToResult(result: RuntimeResult, msg: Message): void {
|
|
218
286
|
result.messages.push(msg);
|
|
219
287
|
result.finalOutput = truncateOutput(getFinalOutput(result.messages));
|
|
220
|
-
|
|
221
288
|
if (msg.role === "toolResult" && msg.isError) {
|
|
222
289
|
result.errorMessage ||= TOOL_RESULT_FAILED_MESSAGE;
|
|
223
290
|
} else if (result.errorMessage === TOOL_RESULT_FAILED_MESSAGE) {
|
|
224
291
|
result.errorMessage = undefined;
|
|
225
292
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
if (usage) {
|
|
232
|
-
result.usage.input += usage.input || 0;
|
|
233
|
-
result.usage.output += usage.output || 0;
|
|
234
|
-
result.usage.cacheRead += usage.cacheRead || 0;
|
|
235
|
-
result.usage.cacheWrite += usage.cacheWrite || 0;
|
|
236
|
-
result.usage.cost += usage.cost?.total || 0;
|
|
237
|
-
result.usage.contextTokens = usage.totalTokens || 0;
|
|
238
|
-
result.usage.contextWindowTokens =
|
|
239
|
-
resolveContextWindowTokens(msg) ?? result.usage.contextWindowTokens;
|
|
293
|
+
if (msg.role === "assistant") {
|
|
294
|
+
accumulateUsage(result, msg);
|
|
295
|
+
if (!result.model && msg.model) result.model = msg.model;
|
|
296
|
+
if (msg.stopReason) result.stopReason = msg.stopReason;
|
|
297
|
+
if (msg.errorMessage) result.errorMessage = msg.errorMessage;
|
|
240
298
|
}
|
|
241
|
-
|
|
242
|
-
if (!result.model && msg.model) result.model = msg.model;
|
|
243
|
-
if (msg.stopReason) result.stopReason = msg.stopReason;
|
|
244
|
-
if (msg.errorMessage) result.errorMessage = msg.errorMessage;
|
|
245
299
|
}
|
|
246
300
|
|
|
247
|
-
/**
|
|
248
|
-
* Standardized error result generator.
|
|
249
|
-
*/
|
|
250
301
|
function createErrorResult(
|
|
251
302
|
agent: string,
|
|
252
303
|
source: "user" | "project" | "unknown",
|
|
@@ -261,15 +312,7 @@ function createErrorResult(
|
|
|
261
312
|
exitCode: 1,
|
|
262
313
|
finalOutput: "",
|
|
263
314
|
stderr: error,
|
|
264
|
-
usage: {
|
|
265
|
-
input: 0,
|
|
266
|
-
output: 0,
|
|
267
|
-
cacheRead: 0,
|
|
268
|
-
cacheWrite: 0,
|
|
269
|
-
cost: 0,
|
|
270
|
-
contextTokens: 0,
|
|
271
|
-
turns: 0,
|
|
272
|
-
},
|
|
315
|
+
usage: { ...EMPTY_USAGE },
|
|
273
316
|
model,
|
|
274
317
|
};
|
|
275
318
|
}
|
|
@@ -304,10 +347,7 @@ function errorForDepthLimit(
|
|
|
304
347
|
);
|
|
305
348
|
}
|
|
306
349
|
|
|
307
|
-
async function cleanupTempPrompt(tmpPrompt: {
|
|
308
|
-
dir: string;
|
|
309
|
-
filePath: string;
|
|
310
|
-
}): Promise<void> {
|
|
350
|
+
async function cleanupTempPrompt(tmpPrompt: TempPrompt): Promise<void> {
|
|
311
351
|
try {
|
|
312
352
|
await fs.promises.unlink(tmpPrompt.filePath);
|
|
313
353
|
await fs.promises.rmdir(tmpPrompt.dir);
|
|
@@ -316,6 +356,22 @@ async function cleanupTempPrompt(tmpPrompt: {
|
|
|
316
356
|
}
|
|
317
357
|
}
|
|
318
358
|
|
|
359
|
+
function beginPromptSetup(agent: AgentConfig): Promise<PromptSetupResult> {
|
|
360
|
+
if (!agent.systemPrompt.trim()) return Promise.resolve({ tmpPrompt: null });
|
|
361
|
+
return writePromptToTempFile(agent.name, agent.systemPrompt).then(
|
|
362
|
+
(tmpPrompt) => ({ tmpPrompt }),
|
|
363
|
+
(error: unknown) => ({ error }),
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async function cleanupPromptSetupResult(
|
|
368
|
+
setup: PromptSetupResult,
|
|
369
|
+
): Promise<void> {
|
|
370
|
+
if ("tmpPrompt" in setup && setup.tmpPrompt) {
|
|
371
|
+
await cleanupTempPrompt(setup.tmpPrompt);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
319
375
|
function findRecentMessagesAnchor(messages: Message[]): number {
|
|
320
376
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
321
377
|
const msg = messages[i];
|
|
@@ -334,10 +390,13 @@ function findRecentMessagesAnchor(messages: Message[]): number {
|
|
|
334
390
|
/**
|
|
335
391
|
* Derives current execution progress from accumulated messages.
|
|
336
392
|
* Maps tool calls to UI-safe previews for real-time feedback.
|
|
393
|
+
* Builds activeToolActivity from the most recent tool call, providing
|
|
394
|
+
* a compact parent summary for subagent tools before nested child data arrives.
|
|
337
395
|
*/
|
|
338
396
|
function deriveStreamingProgress(messages: Message[]): StreamingProgress {
|
|
339
397
|
const toolCalls: { id: string; preview: string }[] = [];
|
|
340
398
|
let lastToolPreview: string | undefined;
|
|
399
|
+
let activeToolActivity: ToolActivity | undefined;
|
|
341
400
|
for (const msg of messages) {
|
|
342
401
|
if (msg.role !== "assistant" || !Array.isArray(msg.content)) continue;
|
|
343
402
|
for (const part of msg.content) {
|
|
@@ -348,9 +407,15 @@ function deriveStreamingProgress(messages: Message[]): StreamingProgress {
|
|
|
348
407
|
);
|
|
349
408
|
toolCalls.push({ id: part.id, preview });
|
|
350
409
|
lastToolPreview = preview;
|
|
410
|
+
activeToolActivity = { toolName: part.name, inputSummary: preview };
|
|
351
411
|
}
|
|
352
412
|
}
|
|
353
|
-
return {
|
|
413
|
+
return {
|
|
414
|
+
activeToolActivity,
|
|
415
|
+
activityText: renderToolActivity(activeToolActivity),
|
|
416
|
+
toolCalls,
|
|
417
|
+
lastToolPreview,
|
|
418
|
+
};
|
|
354
419
|
}
|
|
355
420
|
|
|
356
421
|
/**
|
|
@@ -358,23 +423,62 @@ function deriveStreamingProgress(messages: Message[]): StreamingProgress {
|
|
|
358
423
|
* Redacts values if the preview contains sensitive keywords.
|
|
359
424
|
*/
|
|
360
425
|
function sanitizeProgressPreview(preview: string, toolName: string): string {
|
|
361
|
-
return
|
|
426
|
+
return SENSITIVE_PATTERN.test(preview) ? toolName : preview;
|
|
362
427
|
}
|
|
363
428
|
|
|
364
|
-
function makeEmitUpdate(
|
|
429
|
+
export function makeEmitUpdate(
|
|
365
430
|
result: RuntimeResult,
|
|
366
431
|
onUpdate: OnUpdateCallback | undefined,
|
|
367
432
|
makeDetails: (
|
|
368
433
|
results: RuntimeResult[],
|
|
369
434
|
options?: { includeMessages?: boolean; recentMessages?: Message[] },
|
|
370
435
|
) => SubagentDetails,
|
|
371
|
-
): (
|
|
372
|
-
|
|
436
|
+
): (options?: {
|
|
437
|
+
toolActivity?: ToolActivity;
|
|
438
|
+
toolResultCompleted?: boolean;
|
|
439
|
+
}) => void {
|
|
440
|
+
return (options) => {
|
|
373
441
|
const msgs = result.messages;
|
|
374
442
|
const anchorIdx = findRecentMessagesAnchor(msgs);
|
|
375
443
|
const recentMessages =
|
|
376
444
|
anchorIdx >= 0 ? msgs.slice(anchorIdx) : msgs.slice(-5);
|
|
377
445
|
const progress = deriveStreamingProgress(msgs);
|
|
446
|
+
// Preserve stored activity tree for tool-result completion signals
|
|
447
|
+
// so the parent retains nested context until newer activity arrives
|
|
448
|
+
if (options?.toolResultCompleted && result.progress?.activeToolActivity) {
|
|
449
|
+
progress.activeToolActivity = result.progress.activeToolActivity;
|
|
450
|
+
progress.activityText = renderToolActivity(progress.activeToolActivity);
|
|
451
|
+
}
|
|
452
|
+
// Handle parsed tool activity from child events
|
|
453
|
+
// Merge with parent activity if this is a nested update
|
|
454
|
+
if (options?.toolActivity) {
|
|
455
|
+
if (
|
|
456
|
+
progress.activeToolActivity &&
|
|
457
|
+
progress.activeToolActivity.toolName === options.toolActivity.toolName
|
|
458
|
+
) {
|
|
459
|
+
// Merge: prefer parser inputSummary when non-empty and richer than bare toolName fallback
|
|
460
|
+
const incomingSummary = options.toolActivity.inputSummary;
|
|
461
|
+
const preferIncoming =
|
|
462
|
+
incomingSummary && incomingSummary !== options.toolActivity.toolName;
|
|
463
|
+
progress.activeToolActivity = {
|
|
464
|
+
...progress.activeToolActivity,
|
|
465
|
+
inputSummary: preferIncoming
|
|
466
|
+
? incomingSummary
|
|
467
|
+
: progress.activeToolActivity.inputSummary,
|
|
468
|
+
instanceName:
|
|
469
|
+
options.toolActivity.instanceName ??
|
|
470
|
+
progress.activeToolActivity.instanceName,
|
|
471
|
+
child:
|
|
472
|
+
options.toolActivity.child ?? progress.activeToolActivity.child,
|
|
473
|
+
};
|
|
474
|
+
} else {
|
|
475
|
+
progress.activeToolActivity = options.toolActivity;
|
|
476
|
+
}
|
|
477
|
+
progress.activityText = renderToolActivity(progress.activeToolActivity);
|
|
478
|
+
}
|
|
479
|
+
if (options?.toolResultCompleted) {
|
|
480
|
+
progress.toolResultCompleted = true;
|
|
481
|
+
}
|
|
378
482
|
result.progress = progress;
|
|
379
483
|
onUpdate?.({
|
|
380
484
|
content: [
|
|
@@ -414,33 +518,49 @@ function clearGraceTimer(state: SubagentState): void {
|
|
|
414
518
|
state.agentEndGraceTimer = undefined;
|
|
415
519
|
}
|
|
416
520
|
|
|
417
|
-
function
|
|
418
|
-
|
|
521
|
+
function handleMessageEvent(
|
|
522
|
+
event: ChildKnownEvent,
|
|
419
523
|
state: SubagentState,
|
|
420
|
-
emitUpdate: (
|
|
421
|
-
|
|
524
|
+
emitUpdate: (options?: {
|
|
525
|
+
toolActivity?: ToolActivity;
|
|
526
|
+
toolResultCompleted?: boolean;
|
|
527
|
+
}) => void,
|
|
422
528
|
): void {
|
|
423
|
-
|
|
424
|
-
if (
|
|
425
|
-
const { event } = parseResult;
|
|
426
|
-
|
|
427
|
-
if (
|
|
428
|
-
(event.type === "message_end" || event.type === "tool_result_end") &&
|
|
429
|
-
event.message
|
|
430
|
-
) {
|
|
529
|
+
if (event.type !== "message_end" && event.type !== "tool_result_end") return;
|
|
530
|
+
if (event.message) {
|
|
431
531
|
addMessageToResult(state.result, event.message as Message);
|
|
432
|
-
|
|
532
|
+
const toolResultCompleted = event.type === "tool_result_end";
|
|
533
|
+
emitUpdate({ toolResultCompleted });
|
|
433
534
|
}
|
|
535
|
+
}
|
|
434
536
|
|
|
435
|
-
|
|
537
|
+
function handleToolExecutionUpdateEvent(
|
|
538
|
+
event: ChildKnownEvent,
|
|
539
|
+
emitUpdate: (options?: {
|
|
540
|
+
toolActivity?: ToolActivity;
|
|
541
|
+
toolResultCompleted?: boolean;
|
|
542
|
+
}) => void,
|
|
543
|
+
): void {
|
|
544
|
+
if (event.type !== TOOL_EXECUTION_UPDATE_EVENT) return;
|
|
545
|
+
emitUpdate({ toolActivity: event.toolActivity });
|
|
546
|
+
}
|
|
436
547
|
|
|
548
|
+
function handleAgentEndEvent(
|
|
549
|
+
event: ChildKnownEvent,
|
|
550
|
+
state: SubagentState,
|
|
551
|
+
emitUpdate: (options?: {
|
|
552
|
+
toolActivity?: ToolActivity;
|
|
553
|
+
toolResultCompleted?: boolean;
|
|
554
|
+
}) => void,
|
|
555
|
+
requestTermination: (reason: string) => Promise<unknown>,
|
|
556
|
+
): void {
|
|
557
|
+
if (event.type !== "agent_end") return;
|
|
437
558
|
if (state.result.messages.length === 0 && Array.isArray(event.messages)) {
|
|
438
559
|
for (const msg of event.messages as Message[]) {
|
|
439
560
|
addMessageToResult(state.result, msg);
|
|
440
561
|
}
|
|
441
562
|
emitUpdate();
|
|
442
563
|
}
|
|
443
|
-
|
|
444
564
|
if (state.agentEndGraceTimer || state.terminationPromise) return;
|
|
445
565
|
state.agentEndGraceTimer = setTimeout(() => {
|
|
446
566
|
state.agentEndGraceTimer = undefined;
|
|
@@ -449,6 +569,23 @@ function processEventLine(
|
|
|
449
569
|
state.agentEndGraceTimer.unref?.();
|
|
450
570
|
}
|
|
451
571
|
|
|
572
|
+
function processEventLine(
|
|
573
|
+
line: string,
|
|
574
|
+
state: SubagentState,
|
|
575
|
+
emitUpdate: (options?: {
|
|
576
|
+
toolActivity?: ToolActivity;
|
|
577
|
+
toolResultCompleted?: boolean;
|
|
578
|
+
}) => void,
|
|
579
|
+
requestTermination: (reason: string) => Promise<unknown>,
|
|
580
|
+
): void {
|
|
581
|
+
const parseResult = parseChildEventLine(line);
|
|
582
|
+
if (parseResult.kind !== "known") return;
|
|
583
|
+
const { event } = parseResult;
|
|
584
|
+
handleMessageEvent(event, state, emitUpdate);
|
|
585
|
+
handleToolExecutionUpdateEvent(event, emitUpdate);
|
|
586
|
+
handleAgentEndEvent(event, state, emitUpdate, requestTermination);
|
|
587
|
+
}
|
|
588
|
+
|
|
452
589
|
function setupAbortHandler(
|
|
453
590
|
signal: AbortSignal | undefined,
|
|
454
591
|
state: SubagentState,
|
|
@@ -497,7 +634,10 @@ function buildPiArgs(
|
|
|
497
634
|
function setupChildProcess(
|
|
498
635
|
proc: ChildProcess,
|
|
499
636
|
state: SubagentState,
|
|
500
|
-
emitUpdate: (
|
|
637
|
+
emitUpdate: (options?: {
|
|
638
|
+
toolActivity?: ToolActivity;
|
|
639
|
+
toolResultCompleted?: boolean;
|
|
640
|
+
}) => void,
|
|
501
641
|
requestTermination: (reason: string) => Promise<unknown>,
|
|
502
642
|
): void {
|
|
503
643
|
proc.once("error", (error) => {
|
|
@@ -544,7 +684,7 @@ async function finalizeResult(
|
|
|
544
684
|
if (agentEndTimeoutExitCode !== undefined) {
|
|
545
685
|
state.result.exitCode = agentEndTimeoutExitCode;
|
|
546
686
|
}
|
|
547
|
-
if (state.wasAborted) throw new
|
|
687
|
+
if (state.wasAborted) throw new SubagentAbortError(state.result);
|
|
548
688
|
return state.result;
|
|
549
689
|
}
|
|
550
690
|
|
|
@@ -556,7 +696,7 @@ async function finalizeResult(
|
|
|
556
696
|
* the main conversation.
|
|
557
697
|
*
|
|
558
698
|
* Safety:
|
|
559
|
-
* - Enforces a strict recursion limit (depth
|
|
699
|
+
* - Enforces a strict recursion limit (depth 2) via environment variables.
|
|
560
700
|
* - Uses temporary prompt files to pass large system prompts without shell limits.
|
|
561
701
|
* - Streams JSON events from the child to provide real-time UI updates to the parent.
|
|
562
702
|
* - Implements aggressive process tree termination to prevent orphan processes.
|
|
@@ -579,18 +719,28 @@ export async function runSingleAgent(
|
|
|
579
719
|
): Promise<SingleResult> {
|
|
580
720
|
const agent = agents.find((a) => a.name === agentName);
|
|
581
721
|
if (!agent) return errorForUnknownAgent(agentName, agents, task);
|
|
582
|
-
|
|
583
722
|
const depth = getSubagentDepth();
|
|
584
723
|
if (depth >= MAX_SUBAGENT_DEPTH) {
|
|
585
724
|
return errorForDepthLimit(agentName, agent.source, task, depth);
|
|
586
725
|
}
|
|
587
|
-
|
|
588
|
-
const
|
|
726
|
+
const requestedThinking = agent.thinking ?? parentThinking;
|
|
727
|
+
const { level: thinking, warning: thinkingWarning } = parentModel
|
|
728
|
+
? resolveThinkingLevel(
|
|
729
|
+
requestedThinking,
|
|
730
|
+
parentModel.provider,
|
|
731
|
+
parentModel.id,
|
|
732
|
+
)
|
|
733
|
+
: { level: requestedThinking };
|
|
589
734
|
const modelDisplay = buildModelDisplay(parentModel, thinking);
|
|
590
|
-
const
|
|
591
|
-
|
|
592
|
-
|
|
735
|
+
const resolvedSkillsPromise: Promise<{ args: string[] } | { error: string }> =
|
|
736
|
+
agent.skills
|
|
737
|
+
? resolveAgentSkillArgs(defaultCwd, agent.skills)
|
|
738
|
+
: Promise.resolve({ args: [] });
|
|
739
|
+
const promptSetupPromise = beginPromptSetup(agent);
|
|
740
|
+
const resolvedSkills = await resolvedSkillsPromise;
|
|
593
741
|
if ("error" in resolvedSkills) {
|
|
742
|
+
const promptSetup = await promptSetupPromise;
|
|
743
|
+
await cleanupPromptSetupResult(promptSetup);
|
|
594
744
|
return createErrorResult(
|
|
595
745
|
agentName,
|
|
596
746
|
agent.source,
|
|
@@ -599,18 +749,16 @@ export async function runSingleAgent(
|
|
|
599
749
|
modelDisplay,
|
|
600
750
|
);
|
|
601
751
|
}
|
|
602
|
-
|
|
752
|
+
const promptSetup = await promptSetupPromise;
|
|
753
|
+
if ("error" in promptSetup) throw promptSetup.error;
|
|
603
754
|
const startedAt = Date.now();
|
|
604
755
|
const state: SubagentState = {
|
|
605
756
|
result: initRuntimeResult(agentName, agent.source, task, modelDisplay),
|
|
606
757
|
wasAborted: false,
|
|
607
758
|
};
|
|
608
|
-
|
|
609
|
-
|
|
759
|
+
if (thinkingWarning) state.result.thinkingWarning = thinkingWarning;
|
|
760
|
+
const tmpPrompt = promptSetup.tmpPrompt;
|
|
610
761
|
try {
|
|
611
|
-
tmpPrompt = agent.systemPrompt.trim()
|
|
612
|
-
? await writePromptToTempFile(agent.name, agent.systemPrompt)
|
|
613
|
-
: null;
|
|
614
762
|
const args = buildPiArgs(
|
|
615
763
|
agent,
|
|
616
764
|
task,
|
|
@@ -632,7 +780,6 @@ export async function runSingleAgent(
|
|
|
632
780
|
env: { ...process.env, ...subagentDepthEnv() },
|
|
633
781
|
...getProcessTreeSpawnOptions(terminateOptions.tree),
|
|
634
782
|
});
|
|
635
|
-
|
|
636
783
|
const processDone = waitForSubagentProcess(proc);
|
|
637
784
|
const emitUpdate = makeEmitUpdate(state.result, onUpdate, makeDetails);
|
|
638
785
|
const requestTermination = makeRequestTerminator(
|
|
@@ -641,7 +788,6 @@ export async function runSingleAgent(
|
|
|
641
788
|
state,
|
|
642
789
|
);
|
|
643
790
|
setupChildProcess(proc, state, emitUpdate, requestTermination);
|
|
644
|
-
|
|
645
791
|
const onAbort = setupAbortHandler(
|
|
646
792
|
signal,
|
|
647
793
|
state,
|
|
@@ -3,6 +3,8 @@ export const SUBAGENT_RESULT_CONTRACT = `
|
|
|
3
3
|
- End your final response with exactly one line:
|
|
4
4
|
- Outcome: <short, single, compact lower-case sentence>.
|
|
5
5
|
- Outcome summarizes the result of your task in a single sentence.
|
|
6
|
+
- The outcome line is for internal use by the agent.
|
|
7
|
+
- Don't present outcome line in the main agent's response.
|
|
6
8
|
`;
|
|
7
9
|
|
|
8
10
|
export function appendSubagentResultContract(prompt: string): string {
|