@mystilleef/pi-subagent 0.8.0 → 0.10.1
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 +76 -27
- package/package.json +9 -9
- package/src/agent/agent-cache.ts +1 -0
- package/src/agent/agents.ts +25 -4
- package/src/child/child-events.ts +22 -20
- package/src/child/process.ts +288 -125
- package/src/child/termination.ts +320 -5
- package/src/env.d.ts +15 -0
- package/src/notification/delivery.ts +231 -0
- package/src/notification/desktop-notification.ts +73 -0
- package/src/orchestration/run-command.ts +1 -1
- package/src/orchestration/run-registry.ts +1 -2
- package/src/orchestration/subagent-orchestrator.ts +57 -82
- package/src/output/normalize.ts +2 -2
- package/src/output/ui.ts +17 -28
- package/src/progress/progress-format.ts +111 -0
- package/src/progress/progress-state.ts +55 -140
- package/src/progress/progress.ts +40 -31
- package/src/progress/result-details.ts +146 -39
- package/src/shared/types.ts +18 -16
- package/src/shared/utils.ts +60 -11
- package/tsconfig.json +9 -11
package/src/child/process.ts
CHANGED
|
@@ -17,39 +17,43 @@ import {
|
|
|
17
17
|
import type { AgentConfig, ThinkingLevel } from "../agent/agents.js";
|
|
18
18
|
import { getFinalOutput } from "../output/ui.js";
|
|
19
19
|
import { makeToolPreview, renderToolActivity } from "../progress/progress.js";
|
|
20
|
+
import { SENSITIVE_PATTERN } from "../progress/progress-format.js";
|
|
21
|
+
import { isToolCallPart } from "../progress/progress-state.js";
|
|
20
22
|
import {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
StreamingProgress,
|
|
28
|
-
SubagentDetails,
|
|
29
|
-
ToolActivity,
|
|
23
|
+
type OnUpdateCallback,
|
|
24
|
+
type SingleResult,
|
|
25
|
+
type StreamingProgress,
|
|
26
|
+
type SubagentDetails,
|
|
27
|
+
TOOL_RESULT_FAILED_MESSAGE,
|
|
28
|
+
type ToolActivity,
|
|
30
29
|
} from "../shared/types.js";
|
|
31
30
|
import {
|
|
32
31
|
detectMessageError,
|
|
32
|
+
findLastAssistantTextMessage,
|
|
33
33
|
getPiInvocation,
|
|
34
34
|
getSubagentDepth,
|
|
35
|
+
getSubagentRuntimeLimits,
|
|
35
36
|
resolveAgentSkillArgs,
|
|
36
37
|
subagentDepthEnv,
|
|
37
38
|
truncateOutput,
|
|
38
39
|
writePromptToTempFile,
|
|
39
40
|
} from "../shared/utils.js";
|
|
40
41
|
import {
|
|
42
|
+
type ChildEventParseResult,
|
|
41
43
|
type ChildKnownEvent,
|
|
42
44
|
parseChildEventLine,
|
|
43
45
|
TOOL_EXECUTION_UPDATE_EVENT,
|
|
44
46
|
} from "./child-events.js";
|
|
45
47
|
import { appendSubagentResultContract } from "./prompt-contract.js";
|
|
46
48
|
import {
|
|
49
|
+
acquireChildSleepInhibitor,
|
|
47
50
|
getProcessTreeSpawnOptions,
|
|
51
|
+
isFinitePid,
|
|
52
|
+
makeHostSleepInhibitorAdapter,
|
|
53
|
+
type SleepInhibitorHandle,
|
|
48
54
|
terminateChildProcess,
|
|
49
55
|
} from "./termination.js";
|
|
50
56
|
|
|
51
|
-
const MAX_STDERR_BYTES = 10_000;
|
|
52
|
-
const AGENT_END_GRACE_MS = 250;
|
|
53
57
|
export function resolveThinkingLevel(
|
|
54
58
|
requested: ThinkingLevel,
|
|
55
59
|
provider: string,
|
|
@@ -73,15 +77,25 @@ export function resolveThinkingLevel(
|
|
|
73
77
|
return { level: clamped, warning: mkWarning(clamped) };
|
|
74
78
|
}
|
|
75
79
|
|
|
76
|
-
|
|
77
|
-
export const TOOL_RESULT_FAILED_MESSAGE = "Subagent tool result failed.";
|
|
78
|
-
|
|
80
|
+
type RuntimeLimits = ReturnType<typeof getSubagentRuntimeLimits>;
|
|
79
81
|
type RuntimeResult = SingleResult & { messages: Message[] };
|
|
82
|
+
type ChildModelSettings = {
|
|
83
|
+
provider?: string | undefined;
|
|
84
|
+
id?: string | undefined;
|
|
85
|
+
};
|
|
86
|
+
type SleepInhibitorAcquirer = (pid: number) => Promise<SleepInhibitorHandle>;
|
|
87
|
+
|
|
88
|
+
type RunSingleAgentOptions = {
|
|
89
|
+
acquireSleepInhibitor?: SleepInhibitorAcquirer;
|
|
90
|
+
getOrchestratorPid?: () => unknown;
|
|
91
|
+
};
|
|
80
92
|
|
|
81
93
|
export class SubagentAbortError extends Error {
|
|
82
|
-
|
|
94
|
+
readonly result: SingleResult;
|
|
95
|
+
constructor(result: SingleResult) {
|
|
83
96
|
super("Subagent was aborted");
|
|
84
97
|
this.name = "SubagentAbortError";
|
|
98
|
+
this.result = result;
|
|
85
99
|
}
|
|
86
100
|
}
|
|
87
101
|
|
|
@@ -91,6 +105,7 @@ type PromptSetupResult = { tmpPrompt: TempPrompt | null } | { error: unknown };
|
|
|
91
105
|
|
|
92
106
|
interface SubagentState {
|
|
93
107
|
result: RuntimeResult;
|
|
108
|
+
runtimeLimits: RuntimeLimits;
|
|
94
109
|
spawnError?: Error;
|
|
95
110
|
wasAborted: boolean;
|
|
96
111
|
agentEndGraceTimer?: ReturnType<typeof setTimeout>;
|
|
@@ -99,30 +114,47 @@ interface SubagentState {
|
|
|
99
114
|
|
|
100
115
|
function appendWithByteLimit(
|
|
101
116
|
current: string,
|
|
102
|
-
data: string,
|
|
117
|
+
data: string | Buffer,
|
|
103
118
|
max: number,
|
|
104
119
|
): string {
|
|
105
|
-
|
|
106
|
-
|
|
120
|
+
const currentBytes = Buffer.from(current, "utf-8");
|
|
121
|
+
if (currentBytes.length >= max) return current;
|
|
122
|
+
const incomingBytes = Buffer.isBuffer(data)
|
|
123
|
+
? data
|
|
124
|
+
: Buffer.from(data, "utf-8");
|
|
125
|
+
const combined = Buffer.concat([currentBytes, incomingBytes]);
|
|
126
|
+
if (combined.length <= max) return combined.toString("utf-8");
|
|
127
|
+
return truncateValidUtf8(combined, max);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function truncateValidUtf8(buffer: Buffer, max: number): string {
|
|
131
|
+
let end = Math.min(max, buffer.length);
|
|
132
|
+
while (end > 0) {
|
|
133
|
+
const candidate = buffer.subarray(0, end).toString("utf-8");
|
|
134
|
+
if (!candidate.endsWith("�")) return candidate;
|
|
135
|
+
end -= 1;
|
|
136
|
+
}
|
|
137
|
+
return "";
|
|
107
138
|
}
|
|
108
139
|
|
|
109
140
|
/**
|
|
110
|
-
* Attempts to resolve the context window token limit for a given message's model.
|
|
111
141
|
* Rationale: Subagent usage reporting needs context window awareness to provide
|
|
112
142
|
* meaningful "context full" indicators to the parent.
|
|
113
143
|
*/
|
|
114
144
|
function resolveContextWindowTokens(msg: Message): number | undefined {
|
|
115
145
|
const m = msg as unknown as Record<string, unknown>;
|
|
116
|
-
if (typeof m
|
|
146
|
+
if (typeof m["provider"] !== "string" || typeof m["model"] !== "string")
|
|
147
|
+
return;
|
|
117
148
|
try {
|
|
118
149
|
const contextWindow = getModel(
|
|
119
|
-
m
|
|
120
|
-
m
|
|
150
|
+
m["provider"] as never,
|
|
151
|
+
m["model"] as never,
|
|
121
152
|
)?.contextWindow;
|
|
122
153
|
return Number.isFinite(contextWindow) && contextWindow > 0
|
|
123
154
|
? contextWindow
|
|
124
155
|
: undefined;
|
|
125
156
|
} catch {
|
|
157
|
+
/* model lookup failures return undefined to skip context window tracking */
|
|
126
158
|
return;
|
|
127
159
|
}
|
|
128
160
|
}
|
|
@@ -134,11 +166,56 @@ function getAbortReason(signal: AbortSignal): string {
|
|
|
134
166
|
return "abort";
|
|
135
167
|
}
|
|
136
168
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
169
|
+
const hostSleepInhibitorAdapter = makeHostSleepInhibitorAdapter();
|
|
170
|
+
|
|
171
|
+
async function acquireDefaultSleepInhibitor(
|
|
172
|
+
pid: number,
|
|
173
|
+
): Promise<SleepInhibitorHandle> {
|
|
174
|
+
return acquireChildSleepInhibitor(pid, hostSleepInhibitorAdapter);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function acquireSubagentSleepInhibitor(
|
|
178
|
+
pid: number,
|
|
179
|
+
acquireSleepInhibitor: SleepInhibitorAcquirer,
|
|
180
|
+
): Promise<SleepInhibitorHandle | undefined> {
|
|
181
|
+
try {
|
|
182
|
+
return await acquireSleepInhibitor(pid);
|
|
183
|
+
} catch {
|
|
184
|
+
/* acquisition failures degrade gracefully to no inhibitor */
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function getValidatedOrchestratorPid(
|
|
190
|
+
options: RunSingleAgentOptions,
|
|
191
|
+
): number | undefined {
|
|
192
|
+
try {
|
|
193
|
+
const pid = options.getOrchestratorPid
|
|
194
|
+
? options.getOrchestratorPid()
|
|
195
|
+
: process.pid;
|
|
196
|
+
return isFinitePid(pid) ? pid : undefined;
|
|
197
|
+
} catch {
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function releaseSleepInhibitor(
|
|
203
|
+
handle: SleepInhibitorHandle | undefined,
|
|
204
|
+
): Promise<void> {
|
|
205
|
+
if (!handle) return;
|
|
206
|
+
try {
|
|
207
|
+
await handle.release();
|
|
208
|
+
} catch {
|
|
209
|
+
/* release failures are non-fatal */
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function startSleepInhibitorRelease(
|
|
214
|
+
acquisitionPromise: Promise<SleepInhibitorHandle | undefined>,
|
|
215
|
+
): Promise<void> {
|
|
216
|
+
return acquisitionPromise.then(releaseSleepInhibitor, () => {});
|
|
217
|
+
}
|
|
218
|
+
|
|
142
219
|
function hasCompletedAgentOutput(result: RuntimeResult): boolean {
|
|
143
220
|
if (result.finalOutput.trim()) return true;
|
|
144
221
|
return result.messages.some(
|
|
@@ -151,7 +228,6 @@ function hasCompletedAgentOutput(result: RuntimeResult): boolean {
|
|
|
151
228
|
}
|
|
152
229
|
|
|
153
230
|
/**
|
|
154
|
-
* Determines the exit code for processes terminated via the agent_end timeout.
|
|
155
231
|
* Rationale: `pi` processes in JSON mode might hang after finishing their task;
|
|
156
232
|
* we force-kill them after a grace period and treat it as success (0) if they
|
|
157
233
|
* actually produced output.
|
|
@@ -168,7 +244,6 @@ function getAgentEndTimeoutExitCode(
|
|
|
168
244
|
}
|
|
169
245
|
|
|
170
246
|
/**
|
|
171
|
-
* Orchestrates the cleanup and exit code capture of a child process.
|
|
172
247
|
* Safety: Implements a dual-timer strategy (idle and hard) to ensure streams
|
|
173
248
|
* are destroyed and promises settled even if the process or its pipes hang.
|
|
174
249
|
*/
|
|
@@ -229,13 +304,20 @@ async function waitForSubagentProcess(
|
|
|
229
304
|
}
|
|
230
305
|
|
|
231
306
|
function buildModelDisplay(
|
|
232
|
-
|
|
307
|
+
effectiveModel: ChildModelSettings,
|
|
233
308
|
thinking: ThinkingLevel,
|
|
234
309
|
): string | undefined {
|
|
235
|
-
|
|
236
|
-
|
|
310
|
+
const parts: string[] = [];
|
|
311
|
+
if (effectiveModel.provider) {
|
|
312
|
+
parts.push(effectiveModel.provider);
|
|
313
|
+
}
|
|
314
|
+
if (effectiveModel.id) {
|
|
315
|
+
parts.push(effectiveModel.id);
|
|
316
|
+
}
|
|
317
|
+
if (thinking) {
|
|
318
|
+
parts.push(thinking);
|
|
237
319
|
}
|
|
238
|
-
return
|
|
320
|
+
return parts.length > 0 ? parts.join(" ・ ") : undefined;
|
|
239
321
|
}
|
|
240
322
|
|
|
241
323
|
const EMPTY_USAGE = {
|
|
@@ -278,8 +360,9 @@ function accumulateUsage(result: RuntimeResult, msg: Message): void {
|
|
|
278
360
|
result.usage.cacheWrite += usage.cacheWrite || 0;
|
|
279
361
|
result.usage.cost += usage.cost?.total || 0;
|
|
280
362
|
result.usage.contextTokens = usage.totalTokens || 0;
|
|
281
|
-
|
|
282
|
-
|
|
363
|
+
const ctxWindowTokens = resolveContextWindowTokens(msg);
|
|
364
|
+
if (ctxWindowTokens !== undefined)
|
|
365
|
+
result.usage.contextWindowTokens = ctxWindowTokens;
|
|
283
366
|
}
|
|
284
367
|
|
|
285
368
|
function addMessageToResult(result: RuntimeResult, msg: Message): void {
|
|
@@ -288,7 +371,7 @@ function addMessageToResult(result: RuntimeResult, msg: Message): void {
|
|
|
288
371
|
if (msg.role === "toolResult" && msg.isError) {
|
|
289
372
|
result.errorMessage ||= TOOL_RESULT_FAILED_MESSAGE;
|
|
290
373
|
} else if (result.errorMessage === TOOL_RESULT_FAILED_MESSAGE) {
|
|
291
|
-
result.errorMessage
|
|
374
|
+
delete result.errorMessage;
|
|
292
375
|
}
|
|
293
376
|
if (msg.role === "assistant") {
|
|
294
377
|
accumulateUsage(result, msg);
|
|
@@ -336,13 +419,14 @@ function errorForDepthLimit(
|
|
|
336
419
|
source: "user" | "project" | "unknown",
|
|
337
420
|
task: string,
|
|
338
421
|
depth: number,
|
|
422
|
+
maxDepth: number,
|
|
339
423
|
model?: string,
|
|
340
424
|
): SingleResult {
|
|
341
425
|
return createErrorResult(
|
|
342
426
|
agentName,
|
|
343
427
|
source,
|
|
344
428
|
task,
|
|
345
|
-
`Subagent nesting limit reached (depth ${depth}/${
|
|
429
|
+
`Subagent nesting limit reached (depth ${depth}/${maxDepth}).`,
|
|
346
430
|
model,
|
|
347
431
|
);
|
|
348
432
|
}
|
|
@@ -352,7 +436,7 @@ async function cleanupTempPrompt(tmpPrompt: TempPrompt): Promise<void> {
|
|
|
352
436
|
await fs.promises.unlink(tmpPrompt.filePath);
|
|
353
437
|
await fs.promises.rmdir(tmpPrompt.dir);
|
|
354
438
|
} catch {
|
|
355
|
-
/*
|
|
439
|
+
/* temp file cleanup failures are non-fatal; OS will clean up eventually */
|
|
356
440
|
}
|
|
357
441
|
}
|
|
358
442
|
|
|
@@ -373,26 +457,9 @@ async function cleanupPromptSetupResult(
|
|
|
373
457
|
}
|
|
374
458
|
|
|
375
459
|
function findRecentMessagesAnchor(messages: Message[]): number {
|
|
376
|
-
|
|
377
|
-
const msg = messages[i];
|
|
378
|
-
if (
|
|
379
|
-
msg?.role === "assistant" &&
|
|
380
|
-
msg.content.some(
|
|
381
|
-
(c) => c.type === "text" && (c as { text?: string }).text?.trim(),
|
|
382
|
-
)
|
|
383
|
-
) {
|
|
384
|
-
return i;
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
return -1;
|
|
460
|
+
return findLastAssistantTextMessage(messages);
|
|
388
461
|
}
|
|
389
462
|
|
|
390
|
-
/**
|
|
391
|
-
* Derives current execution progress from accumulated messages.
|
|
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.
|
|
395
|
-
*/
|
|
396
463
|
function deriveStreamingProgress(messages: Message[]): StreamingProgress {
|
|
397
464
|
const toolCalls: { id: string; preview: string }[] = [];
|
|
398
465
|
let lastToolPreview: string | undefined;
|
|
@@ -410,22 +477,76 @@ function deriveStreamingProgress(messages: Message[]): StreamingProgress {
|
|
|
410
477
|
activeToolActivity = { toolName: part.name, inputSummary: preview };
|
|
411
478
|
}
|
|
412
479
|
}
|
|
480
|
+
const activityText = renderToolActivity(activeToolActivity);
|
|
413
481
|
return {
|
|
414
482
|
activeToolActivity,
|
|
415
|
-
activityText
|
|
483
|
+
activityText,
|
|
416
484
|
toolCalls,
|
|
417
485
|
lastToolPreview,
|
|
418
486
|
};
|
|
419
487
|
}
|
|
420
488
|
|
|
421
|
-
/**
|
|
422
|
-
* Prevents leaking secrets in the CLI progress display.
|
|
423
|
-
* Redacts values if the preview contains sensitive keywords.
|
|
424
|
-
*/
|
|
425
489
|
function sanitizeProgressPreview(preview: string, toolName: string): string {
|
|
426
490
|
return SENSITIVE_PATTERN.test(preview) ? toolName : preview;
|
|
427
491
|
}
|
|
428
492
|
|
|
493
|
+
/**
|
|
494
|
+
* Merge incoming tool activity with existing progress activity.
|
|
495
|
+
* When tool names match, prefer richer inputSummary from incoming.
|
|
496
|
+
* Otherwise, replace entirely with incoming activity.
|
|
497
|
+
*/
|
|
498
|
+
function mergeToolActivity(
|
|
499
|
+
existing: ToolActivity | undefined,
|
|
500
|
+
incoming: ToolActivity,
|
|
501
|
+
): ToolActivity {
|
|
502
|
+
if (existing && existing.toolName === incoming.toolName) {
|
|
503
|
+
const incomingSummary = incoming.inputSummary;
|
|
504
|
+
const preferIncoming =
|
|
505
|
+
incomingSummary && incomingSummary !== incoming.toolName;
|
|
506
|
+
return {
|
|
507
|
+
...existing,
|
|
508
|
+
inputSummary: preferIncoming ? incomingSummary : existing.inputSummary,
|
|
509
|
+
instanceName: incoming.instanceName ?? existing.instanceName,
|
|
510
|
+
child: incoming.child ?? existing.child,
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
return incoming;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Apply tool activity and result completion updates to progress state.
|
|
518
|
+
* Handles merging of child events with parent activity tree.
|
|
519
|
+
*/
|
|
520
|
+
function applyActivityUpdates(
|
|
521
|
+
progress: StreamingProgress,
|
|
522
|
+
options: { toolActivity?: ToolActivity; toolResultCompleted?: boolean },
|
|
523
|
+
previousActivity?: ToolActivity,
|
|
524
|
+
): void {
|
|
525
|
+
// Preserve stored activity tree for tool-result completion signals
|
|
526
|
+
// so the parent retains nested context until newer activity arrives
|
|
527
|
+
if (options.toolResultCompleted && previousActivity) {
|
|
528
|
+
progress.activeToolActivity = previousActivity;
|
|
529
|
+
const renderedText = renderToolActivity(previousActivity);
|
|
530
|
+
if (renderedText !== undefined) progress.activityText = renderedText;
|
|
531
|
+
else delete progress.activityText;
|
|
532
|
+
}
|
|
533
|
+
// Handle parsed tool activity from child events
|
|
534
|
+
// Merge with parent activity if this is a nested update
|
|
535
|
+
if (options.toolActivity) {
|
|
536
|
+
progress.activeToolActivity = mergeToolActivity(
|
|
537
|
+
progress.activeToolActivity,
|
|
538
|
+
options.toolActivity,
|
|
539
|
+
);
|
|
540
|
+
const renderedActivity = renderToolActivity(progress.activeToolActivity);
|
|
541
|
+
if (renderedActivity !== undefined)
|
|
542
|
+
progress.activityText = renderedActivity;
|
|
543
|
+
else delete progress.activityText;
|
|
544
|
+
}
|
|
545
|
+
if (options.toolResultCompleted) {
|
|
546
|
+
progress.toolResultCompleted = true;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
429
550
|
export function makeEmitUpdate(
|
|
430
551
|
result: RuntimeResult,
|
|
431
552
|
onUpdate: OnUpdateCallback | undefined,
|
|
@@ -443,41 +564,12 @@ export function makeEmitUpdate(
|
|
|
443
564
|
const recentMessages =
|
|
444
565
|
anchorIdx >= 0 ? msgs.slice(anchorIdx) : msgs.slice(-5);
|
|
445
566
|
const progress = deriveStreamingProgress(msgs);
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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;
|
|
567
|
+
if (options) {
|
|
568
|
+
applyActivityUpdates(
|
|
569
|
+
progress,
|
|
570
|
+
options,
|
|
571
|
+
result.progress?.activeToolActivity,
|
|
572
|
+
);
|
|
481
573
|
}
|
|
482
574
|
result.progress = progress;
|
|
483
575
|
onUpdate?.({
|
|
@@ -515,7 +607,7 @@ function makeRequestTerminator(
|
|
|
515
607
|
function clearGraceTimer(state: SubagentState): void {
|
|
516
608
|
if (!state.agentEndGraceTimer) return;
|
|
517
609
|
clearTimeout(state.agentEndGraceTimer);
|
|
518
|
-
state.agentEndGraceTimer
|
|
610
|
+
delete state.agentEndGraceTimer;
|
|
519
611
|
}
|
|
520
612
|
|
|
521
613
|
function handleMessageEvent(
|
|
@@ -563,12 +655,25 @@ function handleAgentEndEvent(
|
|
|
563
655
|
}
|
|
564
656
|
if (state.agentEndGraceTimer || state.terminationPromise) return;
|
|
565
657
|
state.agentEndGraceTimer = setTimeout(() => {
|
|
566
|
-
state.agentEndGraceTimer
|
|
658
|
+
delete state.agentEndGraceTimer;
|
|
567
659
|
void requestTermination("agent_end_timeout");
|
|
568
|
-
},
|
|
660
|
+
}, state.runtimeLimits.agentEndGraceMs);
|
|
569
661
|
state.agentEndGraceTimer.unref?.();
|
|
570
662
|
}
|
|
571
663
|
|
|
664
|
+
function formatUnknownEventDiagnostic(
|
|
665
|
+
line: string,
|
|
666
|
+
parseResult: Exclude<ChildEventParseResult, { kind: "known" }>,
|
|
667
|
+
): string {
|
|
668
|
+
if (parseResult.kind === "invalid" && !line.trim()) {
|
|
669
|
+
return "[pi-subagent:unknown-event] blank";
|
|
670
|
+
}
|
|
671
|
+
if (parseResult.kind === "invalid") {
|
|
672
|
+
return `[pi-subagent:unknown-event] malformed: ${line}`;
|
|
673
|
+
}
|
|
674
|
+
return `[pi-subagent:unknown-event] unknown: ${JSON.stringify(parseResult.event)}`;
|
|
675
|
+
}
|
|
676
|
+
|
|
572
677
|
function processEventLine(
|
|
573
678
|
line: string,
|
|
574
679
|
state: SubagentState,
|
|
@@ -577,9 +682,17 @@ function processEventLine(
|
|
|
577
682
|
toolResultCompleted?: boolean;
|
|
578
683
|
}) => void,
|
|
579
684
|
requestTermination: (reason: string) => Promise<unknown>,
|
|
685
|
+
debugEventDiagnostics: boolean,
|
|
580
686
|
): void {
|
|
581
687
|
const parseResult = parseChildEventLine(line);
|
|
582
|
-
if (parseResult.kind !== "known")
|
|
688
|
+
if (parseResult.kind !== "known") {
|
|
689
|
+
if (debugEventDiagnostics) {
|
|
690
|
+
process.stderr.write(
|
|
691
|
+
`${formatUnknownEventDiagnostic(line, parseResult)}\n`,
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
583
696
|
const { event } = parseResult;
|
|
584
697
|
handleMessageEvent(event, state, emitUpdate);
|
|
585
698
|
handleToolExecutionUpdateEvent(event, emitUpdate);
|
|
@@ -606,18 +719,30 @@ function setupAbortHandler(
|
|
|
606
719
|
return onAbort;
|
|
607
720
|
}
|
|
608
721
|
|
|
722
|
+
function resolveEffectiveChildModelSettings(
|
|
723
|
+
agent: AgentConfig,
|
|
724
|
+
parentModel: ChildModelSettings | undefined,
|
|
725
|
+
): ChildModelSettings {
|
|
726
|
+
return {
|
|
727
|
+
provider: agent.provider ?? parentModel?.provider,
|
|
728
|
+
id:
|
|
729
|
+
agent.model ??
|
|
730
|
+
(agent.provider === undefined ? parentModel?.id : undefined),
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
|
|
609
734
|
function buildPiArgs(
|
|
610
735
|
agent: AgentConfig,
|
|
611
736
|
task: string,
|
|
612
|
-
|
|
737
|
+
effectiveModel: ChildModelSettings,
|
|
613
738
|
thinking: ThinkingLevel,
|
|
614
739
|
resolvedSkills: { args: string[] },
|
|
615
740
|
tmpPrompt: { filePath: string } | null,
|
|
616
741
|
): string[] {
|
|
617
742
|
const args: string[] = ["--mode", "json", "-p", "--no-session"];
|
|
618
|
-
if (
|
|
619
|
-
args.push("--provider",
|
|
620
|
-
|
|
743
|
+
if (effectiveModel.provider && effectiveModel.id)
|
|
744
|
+
args.push("--provider", effectiveModel.provider);
|
|
745
|
+
if (effectiveModel.id) args.push("--model", effectiveModel.id);
|
|
621
746
|
args.push("--thinking", thinking);
|
|
622
747
|
if (agent.tools?.length) args.push("--tools", agent.tools.join(","));
|
|
623
748
|
if (agent.skills) args.push("--no-skills", ...resolvedSkills.args);
|
|
@@ -639,28 +764,35 @@ function setupChildProcess(
|
|
|
639
764
|
toolResultCompleted?: boolean;
|
|
640
765
|
}) => void,
|
|
641
766
|
requestTermination: (reason: string) => Promise<unknown>,
|
|
767
|
+
debugEventDiagnostics: boolean,
|
|
642
768
|
): void {
|
|
643
769
|
proc.once("error", (error) => {
|
|
644
770
|
state.spawnError = error;
|
|
645
771
|
state.result.stderr = appendWithByteLimit(
|
|
646
772
|
state.result.stderr,
|
|
647
773
|
error.message,
|
|
648
|
-
|
|
774
|
+
state.runtimeLimits.maxStderrBytes,
|
|
649
775
|
);
|
|
650
776
|
});
|
|
651
777
|
if (proc.stdout) {
|
|
652
778
|
readline
|
|
653
779
|
.createInterface({ input: proc.stdout })
|
|
654
780
|
.on("line", (line) =>
|
|
655
|
-
processEventLine(
|
|
781
|
+
processEventLine(
|
|
782
|
+
line,
|
|
783
|
+
state,
|
|
784
|
+
emitUpdate,
|
|
785
|
+
requestTermination,
|
|
786
|
+
debugEventDiagnostics,
|
|
787
|
+
),
|
|
656
788
|
);
|
|
657
789
|
}
|
|
658
790
|
if (proc.stderr) {
|
|
659
|
-
proc.stderr.on("data", (data) => {
|
|
791
|
+
proc.stderr.on("data", (data: Buffer) => {
|
|
660
792
|
state.result.stderr = appendWithByteLimit(
|
|
661
793
|
state.result.stderr,
|
|
662
|
-
data
|
|
663
|
-
|
|
794
|
+
data,
|
|
795
|
+
state.runtimeLimits.maxStderrBytes,
|
|
664
796
|
);
|
|
665
797
|
});
|
|
666
798
|
}
|
|
@@ -684,13 +816,17 @@ async function finalizeResult(
|
|
|
684
816
|
if (agentEndTimeoutExitCode !== undefined) {
|
|
685
817
|
state.result.exitCode = agentEndTimeoutExitCode;
|
|
686
818
|
}
|
|
687
|
-
if (state.
|
|
819
|
+
if (state.result.termination?.cancelReason === "agent_end_timeout") {
|
|
820
|
+
state.result.stderr = state.result.stderr.replace(/^Terminated\r?\n/gm, "");
|
|
821
|
+
}
|
|
822
|
+
if (state.wasAborted) {
|
|
823
|
+
state.result.stderr = "";
|
|
824
|
+
throw new SubagentAbortError(state.result);
|
|
825
|
+
}
|
|
688
826
|
return state.result;
|
|
689
827
|
}
|
|
690
828
|
|
|
691
829
|
/**
|
|
692
|
-
* Executes a single subagent task.
|
|
693
|
-
*
|
|
694
830
|
* Rationale: Subagents run in isolated child processes to protect the parent's
|
|
695
831
|
* context window and allow specialized system prompts/tools without polluting
|
|
696
832
|
* the main conversation.
|
|
@@ -714,24 +850,35 @@ export async function runSingleAgent(
|
|
|
714
850
|
results: RuntimeResult[],
|
|
715
851
|
options?: { includeMessages?: boolean; recentMessages?: Message[] },
|
|
716
852
|
) => SubagentDetails,
|
|
717
|
-
parentModel:
|
|
853
|
+
parentModel: ChildModelSettings | undefined,
|
|
718
854
|
parentThinking: ThinkingLevel,
|
|
855
|
+
debugEventDiagnostics = false,
|
|
856
|
+
options: RunSingleAgentOptions = {},
|
|
719
857
|
): Promise<SingleResult> {
|
|
720
858
|
const agent = agents.find((a) => a.name === agentName);
|
|
721
859
|
if (!agent) return errorForUnknownAgent(agentName, agents, task);
|
|
860
|
+
const runtimeLimits = getSubagentRuntimeLimits();
|
|
722
861
|
const depth = getSubagentDepth();
|
|
723
|
-
if (depth >=
|
|
724
|
-
return errorForDepthLimit(
|
|
862
|
+
if (depth >= runtimeLimits.maxDepth) {
|
|
863
|
+
return errorForDepthLimit(
|
|
864
|
+
agentName,
|
|
865
|
+
agent.source,
|
|
866
|
+
task,
|
|
867
|
+
depth,
|
|
868
|
+
runtimeLimits.maxDepth,
|
|
869
|
+
);
|
|
725
870
|
}
|
|
726
871
|
const requestedThinking = agent.thinking ?? parentThinking;
|
|
727
|
-
const
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
872
|
+
const effectiveModel = resolveEffectiveChildModelSettings(agent, parentModel);
|
|
873
|
+
const { level: thinking, warning: thinkingWarning } =
|
|
874
|
+
effectiveModel.provider && effectiveModel.id
|
|
875
|
+
? resolveThinkingLevel(
|
|
876
|
+
requestedThinking,
|
|
877
|
+
effectiveModel.provider,
|
|
878
|
+
effectiveModel.id,
|
|
879
|
+
)
|
|
880
|
+
: { level: requestedThinking };
|
|
881
|
+
const modelDisplay = buildModelDisplay(effectiveModel, thinking);
|
|
735
882
|
const resolvedSkillsPromise: Promise<{ args: string[] } | { error: string }> =
|
|
736
883
|
agent.skills
|
|
737
884
|
? resolveAgentSkillArgs(defaultCwd, agent.skills)
|
|
@@ -754,6 +901,7 @@ export async function runSingleAgent(
|
|
|
754
901
|
const startedAt = Date.now();
|
|
755
902
|
const state: SubagentState = {
|
|
756
903
|
result: initRuntimeResult(agentName, agent.source, task, modelDisplay),
|
|
904
|
+
runtimeLimits,
|
|
757
905
|
wasAborted: false,
|
|
758
906
|
};
|
|
759
907
|
if (thinkingWarning) state.result.thinkingWarning = thinkingWarning;
|
|
@@ -762,7 +910,7 @@ export async function runSingleAgent(
|
|
|
762
910
|
const args = buildPiArgs(
|
|
763
911
|
agent,
|
|
764
912
|
task,
|
|
765
|
-
|
|
913
|
+
effectiveModel,
|
|
766
914
|
thinking,
|
|
767
915
|
resolvedSkills,
|
|
768
916
|
tmpPrompt,
|
|
@@ -787,19 +935,34 @@ export async function runSingleAgent(
|
|
|
787
935
|
terminateOptions,
|
|
788
936
|
state,
|
|
789
937
|
);
|
|
790
|
-
setupChildProcess(
|
|
938
|
+
setupChildProcess(
|
|
939
|
+
proc,
|
|
940
|
+
state,
|
|
941
|
+
emitUpdate,
|
|
942
|
+
requestTermination,
|
|
943
|
+
debugEventDiagnostics,
|
|
944
|
+
);
|
|
791
945
|
const onAbort = setupAbortHandler(
|
|
792
946
|
signal,
|
|
793
947
|
state,
|
|
794
948
|
() => clearGraceTimer(state),
|
|
795
949
|
requestTermination,
|
|
796
950
|
);
|
|
951
|
+
const orchestratorPid = getValidatedOrchestratorPid(options);
|
|
952
|
+
const sleepInhibitorPromise =
|
|
953
|
+
orchestratorPid !== undefined
|
|
954
|
+
? acquireSubagentSleepInhibitor(
|
|
955
|
+
orchestratorPid,
|
|
956
|
+
options.acquireSleepInhibitor ?? acquireDefaultSleepInhibitor,
|
|
957
|
+
)
|
|
958
|
+
: Promise.resolve(undefined);
|
|
797
959
|
try {
|
|
798
960
|
state.result.exitCode = (await processDone) ?? 0;
|
|
799
961
|
return await finalizeResult(state, startedAt);
|
|
800
962
|
} finally {
|
|
801
963
|
clearGraceTimer(state);
|
|
802
964
|
if (signal && onAbort) signal.removeEventListener("abort", onAbort);
|
|
965
|
+
void startSleepInhibitorRelease(sleepInhibitorPromise);
|
|
803
966
|
}
|
|
804
967
|
} finally {
|
|
805
968
|
if (tmpPrompt) await cleanupTempPrompt(tmpPrompt);
|