@mystilleef/pi-subagent 0.6.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 +1 -1
- package/src/agent/agent-cache.ts +225 -8
- package/src/agent/agents.ts +83 -47
- package/src/child/child-events.ts +90 -3
- package/src/child/process.ts +191 -101
- package/src/index.ts +36 -17
- package/src/orchestration/subagent-orchestrator.ts +228 -99
- package/src/output/normalize.ts +35 -28
- package/src/output/ui.ts +25 -20
- package/src/progress/progress-state.ts +163 -51
- package/src/progress/progress.ts +19 -19
- package/src/progress/result-details.ts +46 -8
- package/src/shared/types.ts +10 -0
package/src/child/process.ts
CHANGED
|
@@ -16,13 +16,17 @@ import {
|
|
|
16
16
|
} from "@earendil-works/pi-ai";
|
|
17
17
|
import type { AgentConfig, ThinkingLevel } from "../agent/agents.js";
|
|
18
18
|
import { getFinalOutput } from "../output/ui.js";
|
|
19
|
-
import { makeToolPreview } from "../progress/progress.js";
|
|
20
|
-
import {
|
|
19
|
+
import { makeToolPreview, renderToolActivity } from "../progress/progress.js";
|
|
20
|
+
import {
|
|
21
|
+
isToolCallPart,
|
|
22
|
+
SENSITIVE_PATTERN,
|
|
23
|
+
} from "../progress/progress-state.js";
|
|
21
24
|
import type {
|
|
22
25
|
OnUpdateCallback,
|
|
23
26
|
SingleResult,
|
|
24
27
|
StreamingProgress,
|
|
25
28
|
SubagentDetails,
|
|
29
|
+
ToolActivity,
|
|
26
30
|
} from "../shared/types.js";
|
|
27
31
|
import {
|
|
28
32
|
detectMessageError,
|
|
@@ -33,7 +37,11 @@ import {
|
|
|
33
37
|
truncateOutput,
|
|
34
38
|
writePromptToTempFile,
|
|
35
39
|
} from "../shared/utils.js";
|
|
36
|
-
import {
|
|
40
|
+
import {
|
|
41
|
+
type ChildKnownEvent,
|
|
42
|
+
parseChildEventLine,
|
|
43
|
+
TOOL_EXECUTION_UPDATE_EVENT,
|
|
44
|
+
} from "./child-events.js";
|
|
37
45
|
import { appendSubagentResultContract } from "./prompt-contract.js";
|
|
38
46
|
import {
|
|
39
47
|
getProcessTreeSpawnOptions,
|
|
@@ -42,16 +50,6 @@ import {
|
|
|
42
50
|
|
|
43
51
|
const MAX_STDERR_BYTES = 10_000;
|
|
44
52
|
const AGENT_END_GRACE_MS = 250;
|
|
45
|
-
|
|
46
|
-
function thinkingWarningFor(
|
|
47
|
-
requested: ThinkingLevel,
|
|
48
|
-
effective: ThinkingLevel,
|
|
49
|
-
provider: string,
|
|
50
|
-
modelId: string,
|
|
51
|
-
): string {
|
|
52
|
-
return `Thinking level "${requested}" not supported by model "${provider}/${modelId}"; using "${effective}" instead`;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
53
|
export function resolveThinkingLevel(
|
|
56
54
|
requested: ThinkingLevel,
|
|
57
55
|
provider: string,
|
|
@@ -59,11 +57,10 @@ export function resolveThinkingLevel(
|
|
|
59
57
|
): { level: ThinkingLevel; warning?: string } {
|
|
60
58
|
const model = getModel(provider as never, modelId as never);
|
|
61
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
62
|
if (model.reasoning === false) {
|
|
63
|
-
return {
|
|
64
|
-
level: "off",
|
|
65
|
-
warning: thinkingWarningFor(requested, "off", provider, modelId),
|
|
66
|
-
};
|
|
63
|
+
return { level: "off", warning: mkWarning("off") };
|
|
67
64
|
}
|
|
68
65
|
if (!model.thinkingLevelMap) return { level: requested };
|
|
69
66
|
const supported = getSupportedThinkingLevels(model);
|
|
@@ -73,17 +70,21 @@ export function resolveThinkingLevel(
|
|
|
73
70
|
requested as ModelThinkingLevel,
|
|
74
71
|
) as ThinkingLevel;
|
|
75
72
|
if (clamped === requested) return { level: requested };
|
|
76
|
-
return {
|
|
77
|
-
level: clamped,
|
|
78
|
-
warning: thinkingWarningFor(requested, clamped, provider, modelId),
|
|
79
|
-
};
|
|
73
|
+
return { level: clamped, warning: mkWarning(clamped) };
|
|
80
74
|
}
|
|
81
75
|
|
|
82
|
-
const MAX_SUBAGENT_DEPTH =
|
|
76
|
+
const MAX_SUBAGENT_DEPTH = 2;
|
|
83
77
|
export const TOOL_RESULT_FAILED_MESSAGE = "Subagent tool result failed.";
|
|
84
78
|
|
|
85
79
|
type RuntimeResult = SingleResult & { messages: Message[] };
|
|
86
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
|
+
|
|
87
88
|
type TempPrompt = { dir: string; filePath: string };
|
|
88
89
|
|
|
89
90
|
type PromptSetupResult = { tmpPrompt: TempPrompt | null } | { error: unknown };
|
|
@@ -96,10 +97,6 @@ interface SubagentState {
|
|
|
96
97
|
terminationPromise?: Promise<unknown>;
|
|
97
98
|
}
|
|
98
99
|
|
|
99
|
-
/**
|
|
100
|
-
* Appends data to a string while ensuring the result does not exceed a maximum byte limit.
|
|
101
|
-
* Used to prevent memory exhaustion when capturing child process stderr.
|
|
102
|
-
*/
|
|
103
100
|
function appendWithByteLimit(
|
|
104
101
|
current: string,
|
|
105
102
|
data: string,
|
|
@@ -130,9 +127,6 @@ function resolveContextWindowTokens(msg: Message): number | undefined {
|
|
|
130
127
|
}
|
|
131
128
|
}
|
|
132
129
|
|
|
133
|
-
/**
|
|
134
|
-
* Normalizes AbortSignal reasons into human-readable strings.
|
|
135
|
-
*/
|
|
136
130
|
function getAbortReason(signal: AbortSignal): string {
|
|
137
131
|
const { reason } = signal;
|
|
138
132
|
if (reason instanceof Error && reason.message) return reason.message;
|
|
@@ -178,6 +172,25 @@ function getAgentEndTimeoutExitCode(
|
|
|
178
172
|
* Safety: Implements a dual-timer strategy (idle and hard) to ensure streams
|
|
179
173
|
* are destroyed and promises settled even if the process or its pipes hang.
|
|
180
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
|
+
|
|
181
194
|
async function waitForSubagentProcess(
|
|
182
195
|
proc: ChildProcess,
|
|
183
196
|
idleMs = 100,
|
|
@@ -187,23 +200,13 @@ async function waitForSubagentProcess(
|
|
|
187
200
|
let exitCode: number | null = null;
|
|
188
201
|
let exited = false;
|
|
189
202
|
let settled = false;
|
|
190
|
-
|
|
203
|
+
const cleanup = createProcessCleanup(proc, idleMs);
|
|
191
204
|
const done = () => {
|
|
192
205
|
if (settled) return;
|
|
193
206
|
settled = true;
|
|
194
|
-
|
|
207
|
+
cleanup.clearIdleTimer();
|
|
195
208
|
resolve(exitCode);
|
|
196
209
|
};
|
|
197
|
-
const destroyStreams = () => {
|
|
198
|
-
proc.stdout?.destroy();
|
|
199
|
-
proc.stderr?.destroy();
|
|
200
|
-
};
|
|
201
|
-
const armIdleTimer = () => {
|
|
202
|
-
if (!exited) return;
|
|
203
|
-
if (idleTimer) clearTimeout(idleTimer);
|
|
204
|
-
idleTimer = setTimeout(destroyStreams, idleMs);
|
|
205
|
-
idleTimer.unref?.();
|
|
206
|
-
};
|
|
207
210
|
proc.on("close", done);
|
|
208
211
|
proc.on("error", () => {
|
|
209
212
|
exitCode = 1;
|
|
@@ -213,12 +216,15 @@ async function waitForSubagentProcess(
|
|
|
213
216
|
proc.on("exit", (code) => {
|
|
214
217
|
exitCode = code;
|
|
215
218
|
exited = true;
|
|
216
|
-
armIdleTimer();
|
|
217
|
-
const hardTimer = setTimeout(destroyStreams, hardMs);
|
|
219
|
+
cleanup.armIdleTimer();
|
|
220
|
+
const hardTimer = setTimeout(cleanup.destroyStreams, hardMs);
|
|
218
221
|
hardTimer.unref?.();
|
|
219
222
|
});
|
|
220
|
-
|
|
221
|
-
|
|
223
|
+
const onStreamData = () => {
|
|
224
|
+
if (exited) cleanup.armIdleTimer();
|
|
225
|
+
};
|
|
226
|
+
proc.stdout?.on("data", onStreamData);
|
|
227
|
+
proc.stderr?.on("data", onStreamData);
|
|
222
228
|
});
|
|
223
229
|
}
|
|
224
230
|
|
|
@@ -232,6 +238,16 @@ function buildModelDisplay(
|
|
|
232
238
|
return thinking ? `thinking:${thinking}` : undefined;
|
|
233
239
|
}
|
|
234
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
|
+
|
|
235
251
|
function initRuntimeResult(
|
|
236
252
|
agentName: string,
|
|
237
253
|
source: "user" | "project" | "unknown",
|
|
@@ -246,19 +262,26 @@ function initRuntimeResult(
|
|
|
246
262
|
finalOutput: "",
|
|
247
263
|
messages: [],
|
|
248
264
|
stderr: "",
|
|
249
|
-
usage: {
|
|
250
|
-
input: 0,
|
|
251
|
-
output: 0,
|
|
252
|
-
cacheRead: 0,
|
|
253
|
-
cacheWrite: 0,
|
|
254
|
-
cost: 0,
|
|
255
|
-
contextTokens: 0,
|
|
256
|
-
turns: 0,
|
|
257
|
-
},
|
|
265
|
+
usage: { ...EMPTY_USAGE },
|
|
258
266
|
model: modelDisplay,
|
|
259
267
|
};
|
|
260
268
|
}
|
|
261
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
|
+
|
|
262
285
|
function addMessageToResult(result: RuntimeResult, msg: Message): void {
|
|
263
286
|
result.messages.push(msg);
|
|
264
287
|
result.finalOutput = truncateOutput(getFinalOutput(result.messages));
|
|
@@ -267,27 +290,14 @@ function addMessageToResult(result: RuntimeResult, msg: Message): void {
|
|
|
267
290
|
} else if (result.errorMessage === TOOL_RESULT_FAILED_MESSAGE) {
|
|
268
291
|
result.errorMessage = undefined;
|
|
269
292
|
}
|
|
270
|
-
if (msg.role
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
result.
|
|
275
|
-
result.usage.output += usage.output || 0;
|
|
276
|
-
result.usage.cacheRead += usage.cacheRead || 0;
|
|
277
|
-
result.usage.cacheWrite += usage.cacheWrite || 0;
|
|
278
|
-
result.usage.cost += usage.cost?.total || 0;
|
|
279
|
-
result.usage.contextTokens = usage.totalTokens || 0;
|
|
280
|
-
result.usage.contextWindowTokens =
|
|
281
|
-
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;
|
|
282
298
|
}
|
|
283
|
-
if (!result.model && msg.model) result.model = msg.model;
|
|
284
|
-
if (msg.stopReason) result.stopReason = msg.stopReason;
|
|
285
|
-
if (msg.errorMessage) result.errorMessage = msg.errorMessage;
|
|
286
299
|
}
|
|
287
300
|
|
|
288
|
-
/**
|
|
289
|
-
* Standardized error result generator.
|
|
290
|
-
*/
|
|
291
301
|
function createErrorResult(
|
|
292
302
|
agent: string,
|
|
293
303
|
source: "user" | "project" | "unknown",
|
|
@@ -302,15 +312,7 @@ function createErrorResult(
|
|
|
302
312
|
exitCode: 1,
|
|
303
313
|
finalOutput: "",
|
|
304
314
|
stderr: error,
|
|
305
|
-
usage: {
|
|
306
|
-
input: 0,
|
|
307
|
-
output: 0,
|
|
308
|
-
cacheRead: 0,
|
|
309
|
-
cacheWrite: 0,
|
|
310
|
-
cost: 0,
|
|
311
|
-
contextTokens: 0,
|
|
312
|
-
turns: 0,
|
|
313
|
-
},
|
|
315
|
+
usage: { ...EMPTY_USAGE },
|
|
314
316
|
model,
|
|
315
317
|
};
|
|
316
318
|
}
|
|
@@ -388,10 +390,13 @@ function findRecentMessagesAnchor(messages: Message[]): number {
|
|
|
388
390
|
/**
|
|
389
391
|
* Derives current execution progress from accumulated messages.
|
|
390
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.
|
|
391
395
|
*/
|
|
392
396
|
function deriveStreamingProgress(messages: Message[]): StreamingProgress {
|
|
393
397
|
const toolCalls: { id: string; preview: string }[] = [];
|
|
394
398
|
let lastToolPreview: string | undefined;
|
|
399
|
+
let activeToolActivity: ToolActivity | undefined;
|
|
395
400
|
for (const msg of messages) {
|
|
396
401
|
if (msg.role !== "assistant" || !Array.isArray(msg.content)) continue;
|
|
397
402
|
for (const part of msg.content) {
|
|
@@ -402,9 +407,15 @@ function deriveStreamingProgress(messages: Message[]): StreamingProgress {
|
|
|
402
407
|
);
|
|
403
408
|
toolCalls.push({ id: part.id, preview });
|
|
404
409
|
lastToolPreview = preview;
|
|
410
|
+
activeToolActivity = { toolName: part.name, inputSummary: preview };
|
|
405
411
|
}
|
|
406
412
|
}
|
|
407
|
-
return {
|
|
413
|
+
return {
|
|
414
|
+
activeToolActivity,
|
|
415
|
+
activityText: renderToolActivity(activeToolActivity),
|
|
416
|
+
toolCalls,
|
|
417
|
+
lastToolPreview,
|
|
418
|
+
};
|
|
408
419
|
}
|
|
409
420
|
|
|
410
421
|
/**
|
|
@@ -412,23 +423,62 @@ function deriveStreamingProgress(messages: Message[]): StreamingProgress {
|
|
|
412
423
|
* Redacts values if the preview contains sensitive keywords.
|
|
413
424
|
*/
|
|
414
425
|
function sanitizeProgressPreview(preview: string, toolName: string): string {
|
|
415
|
-
return
|
|
426
|
+
return SENSITIVE_PATTERN.test(preview) ? toolName : preview;
|
|
416
427
|
}
|
|
417
428
|
|
|
418
|
-
function makeEmitUpdate(
|
|
429
|
+
export function makeEmitUpdate(
|
|
419
430
|
result: RuntimeResult,
|
|
420
431
|
onUpdate: OnUpdateCallback | undefined,
|
|
421
432
|
makeDetails: (
|
|
422
433
|
results: RuntimeResult[],
|
|
423
434
|
options?: { includeMessages?: boolean; recentMessages?: Message[] },
|
|
424
435
|
) => SubagentDetails,
|
|
425
|
-
): (
|
|
426
|
-
|
|
436
|
+
): (options?: {
|
|
437
|
+
toolActivity?: ToolActivity;
|
|
438
|
+
toolResultCompleted?: boolean;
|
|
439
|
+
}) => void {
|
|
440
|
+
return (options) => {
|
|
427
441
|
const msgs = result.messages;
|
|
428
442
|
const anchorIdx = findRecentMessagesAnchor(msgs);
|
|
429
443
|
const recentMessages =
|
|
430
444
|
anchorIdx >= 0 ? msgs.slice(anchorIdx) : msgs.slice(-5);
|
|
431
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
|
+
}
|
|
432
482
|
result.progress = progress;
|
|
433
483
|
onUpdate?.({
|
|
434
484
|
content: [
|
|
@@ -468,22 +518,42 @@ function clearGraceTimer(state: SubagentState): void {
|
|
|
468
518
|
state.agentEndGraceTimer = undefined;
|
|
469
519
|
}
|
|
470
520
|
|
|
471
|
-
function
|
|
472
|
-
|
|
521
|
+
function handleMessageEvent(
|
|
522
|
+
event: ChildKnownEvent,
|
|
473
523
|
state: SubagentState,
|
|
474
|
-
emitUpdate: (
|
|
475
|
-
|
|
524
|
+
emitUpdate: (options?: {
|
|
525
|
+
toolActivity?: ToolActivity;
|
|
526
|
+
toolResultCompleted?: boolean;
|
|
527
|
+
}) => void,
|
|
476
528
|
): void {
|
|
477
|
-
|
|
478
|
-
if (
|
|
479
|
-
const { event } = parseResult;
|
|
480
|
-
if (
|
|
481
|
-
(event.type === "message_end" || event.type === "tool_result_end") &&
|
|
482
|
-
event.message
|
|
483
|
-
) {
|
|
529
|
+
if (event.type !== "message_end" && event.type !== "tool_result_end") return;
|
|
530
|
+
if (event.message) {
|
|
484
531
|
addMessageToResult(state.result, event.message as Message);
|
|
485
|
-
|
|
532
|
+
const toolResultCompleted = event.type === "tool_result_end";
|
|
533
|
+
emitUpdate({ toolResultCompleted });
|
|
486
534
|
}
|
|
535
|
+
}
|
|
536
|
+
|
|
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
|
+
}
|
|
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 {
|
|
487
557
|
if (event.type !== "agent_end") return;
|
|
488
558
|
if (state.result.messages.length === 0 && Array.isArray(event.messages)) {
|
|
489
559
|
for (const msg of event.messages as Message[]) {
|
|
@@ -499,6 +569,23 @@ function processEventLine(
|
|
|
499
569
|
state.agentEndGraceTimer.unref?.();
|
|
500
570
|
}
|
|
501
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
|
+
|
|
502
589
|
function setupAbortHandler(
|
|
503
590
|
signal: AbortSignal | undefined,
|
|
504
591
|
state: SubagentState,
|
|
@@ -547,7 +634,10 @@ function buildPiArgs(
|
|
|
547
634
|
function setupChildProcess(
|
|
548
635
|
proc: ChildProcess,
|
|
549
636
|
state: SubagentState,
|
|
550
|
-
emitUpdate: (
|
|
637
|
+
emitUpdate: (options?: {
|
|
638
|
+
toolActivity?: ToolActivity;
|
|
639
|
+
toolResultCompleted?: boolean;
|
|
640
|
+
}) => void,
|
|
551
641
|
requestTermination: (reason: string) => Promise<unknown>,
|
|
552
642
|
): void {
|
|
553
643
|
proc.once("error", (error) => {
|
|
@@ -594,7 +684,7 @@ async function finalizeResult(
|
|
|
594
684
|
if (agentEndTimeoutExitCode !== undefined) {
|
|
595
685
|
state.result.exitCode = agentEndTimeoutExitCode;
|
|
596
686
|
}
|
|
597
|
-
if (state.wasAborted) throw new
|
|
687
|
+
if (state.wasAborted) throw new SubagentAbortError(state.result);
|
|
598
688
|
return state.result;
|
|
599
689
|
}
|
|
600
690
|
|
|
@@ -606,7 +696,7 @@ async function finalizeResult(
|
|
|
606
696
|
* the main conversation.
|
|
607
697
|
*
|
|
608
698
|
* Safety:
|
|
609
|
-
* - Enforces a strict recursion limit (depth
|
|
699
|
+
* - Enforces a strict recursion limit (depth 2) via environment variables.
|
|
610
700
|
* - Uses temporary prompt files to pass large system prompts without shell limits.
|
|
611
701
|
* - Streams JSON events from the child to provide real-time UI updates to the parent.
|
|
612
702
|
* - Implements aggressive process tree termination to prevent orphan processes.
|
package/src/index.ts
CHANGED
|
@@ -6,16 +6,17 @@ import {
|
|
|
6
6
|
getCachedAgentCompletions,
|
|
7
7
|
resetAgentDiscoveryCache,
|
|
8
8
|
} from "./agent/agent-cache.js";
|
|
9
|
+
import { isDirectoryAsync } from "./agent/agents.js";
|
|
9
10
|
import { cancelSubagentCommandHandler } from "./orchestration/cancel-command.js";
|
|
10
11
|
import { jobsCommandHandler } from "./orchestration/jobs-command.js";
|
|
11
12
|
import { renderSubagentResultMessage } from "./orchestration/run.js";
|
|
12
13
|
import { runCommandHandler } from "./orchestration/run-command.js";
|
|
13
14
|
import {
|
|
14
|
-
|
|
15
|
+
formatSubagentToolResult,
|
|
15
16
|
SubagentParams,
|
|
16
17
|
startSubagentJob,
|
|
17
18
|
} from "./orchestration/subagent-orchestrator.js";
|
|
18
|
-
import { renderSubagentCall,
|
|
19
|
+
import { renderSubagentCall, renderSubagentToolResult } from "./output/ui.js";
|
|
19
20
|
import { renderSubagentProgress } from "./progress/progress.js";
|
|
20
21
|
|
|
21
22
|
export { SubagentParams };
|
|
@@ -24,15 +25,40 @@ export function resetAgentCache() {
|
|
|
24
25
|
resetAgentDiscoveryCache();
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
function normalizeWorkspaceRoot(cwd: string | undefined): string | undefined {
|
|
29
|
+
if (typeof cwd !== "string") return undefined;
|
|
30
|
+
const root = cwd.trim();
|
|
31
|
+
return root.length > 0 ? root : undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
27
34
|
export default function registerSubagentExtension(pi: ExtensionAPI) {
|
|
35
|
+
let activeWorkspaceRoot: string | undefined;
|
|
36
|
+
const setActiveWorkspaceRoot = (
|
|
37
|
+
cwd: string | undefined,
|
|
38
|
+
fallback?: string,
|
|
39
|
+
) => {
|
|
40
|
+
activeWorkspaceRoot = normalizeWorkspaceRoot(cwd ?? fallback);
|
|
41
|
+
};
|
|
42
|
+
const getRunArgumentCompletions = async (prefix: string) => {
|
|
43
|
+
if (!activeWorkspaceRoot) return [];
|
|
44
|
+
if (!(await isDirectoryAsync(activeWorkspaceRoot))) return [];
|
|
45
|
+
return getCachedAgentCompletions(prefix, activeWorkspaceRoot);
|
|
46
|
+
};
|
|
47
|
+
pi.on("resources_discover", (event, ctx) => {
|
|
48
|
+
setActiveWorkspaceRoot(ctx.cwd, event.cwd);
|
|
49
|
+
});
|
|
50
|
+
pi.on("session_start", (_event, ctx) => {
|
|
51
|
+
setActiveWorkspaceRoot(ctx.cwd);
|
|
52
|
+
});
|
|
28
53
|
pi.registerMessageRenderer("subagent-progress", renderSubagentProgress);
|
|
29
54
|
pi.registerMessageRenderer("subagent-result", renderSubagentResultMessage);
|
|
30
55
|
pi.registerCommand("run", {
|
|
31
56
|
description: "Run a subagent directly: /run <agent> [task]",
|
|
32
|
-
getArgumentCompletions:
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
runCommandHandler(pi, ctx as ExtensionContext, args)
|
|
57
|
+
getArgumentCompletions: getRunArgumentCompletions,
|
|
58
|
+
handler: async (args, ctx) => {
|
|
59
|
+
setActiveWorkspaceRoot(ctx.cwd);
|
|
60
|
+
return runCommandHandler(pi, ctx as ExtensionContext, args);
|
|
61
|
+
},
|
|
36
62
|
});
|
|
37
63
|
pi.registerCommand("cancel-subagent", {
|
|
38
64
|
description:
|
|
@@ -48,28 +74,21 @@ export default function registerSubagentExtension(pi: ExtensionAPI) {
|
|
|
48
74
|
label: "Subagent",
|
|
49
75
|
description: "Delegate a task to a subagent with isolated context.",
|
|
50
76
|
parameters: SubagentParams,
|
|
51
|
-
async execute(_toolCallId, params, signal,
|
|
77
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
52
78
|
const result = await startSubagentJob(
|
|
53
79
|
pi,
|
|
54
80
|
ctx,
|
|
55
81
|
params,
|
|
56
82
|
signal ?? undefined,
|
|
83
|
+
onUpdate ?? undefined,
|
|
57
84
|
);
|
|
58
|
-
return
|
|
59
|
-
content: [
|
|
60
|
-
{
|
|
61
|
-
type: "text" as const,
|
|
62
|
-
text: formatStartJobStatus(params.agent, result),
|
|
63
|
-
},
|
|
64
|
-
],
|
|
65
|
-
details: result.makeDetails([]),
|
|
66
|
-
};
|
|
85
|
+
return formatSubagentToolResult(params.agent, result);
|
|
67
86
|
},
|
|
68
87
|
renderCall(args, theme, _context) {
|
|
69
88
|
return renderSubagentCall(args, theme);
|
|
70
89
|
},
|
|
71
90
|
renderResult(result, display, theme, _context) {
|
|
72
|
-
return
|
|
91
|
+
return renderSubagentToolResult(result, theme, display);
|
|
73
92
|
},
|
|
74
93
|
});
|
|
75
94
|
}
|