@mystilleef/pi-subagent 0.10.2 → 0.11.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 +2 -0
- package/package.json +8 -8
- package/src/agent/agents.ts +33 -8
- package/src/child/complete-extension.ts +36 -0
- package/src/child/complete-outcome.ts +35 -0
- package/src/child/model-resolution.ts +81 -0
- package/src/child/process-utils.ts +103 -0
- package/src/child/process.ts +124 -451
- package/src/child/prompt-contract.ts +14 -6
- package/src/child/prompt-setup.ts +36 -0
- package/src/child/result-builder.ts +142 -0
- package/src/child/sampling-extension.ts +49 -0
- package/src/child/streaming-progress.ts +138 -0
- package/src/orchestration/subagent-orchestrator.ts +20 -16
- package/src/output/normalize.ts +1 -1
- package/src/output/summary.ts +22 -6
- package/src/output/ui.ts +19 -21
- package/src/progress/progress-state.ts +10 -12
- package/src/progress/result-details.ts +54 -45
- package/src/shared/sampling.ts +56 -0
- package/src/shared/types.ts +1 -0
- package/src/shared/utils.ts +10 -0
package/src/child/process.ts
CHANGED
|
@@ -5,38 +5,24 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { type ChildProcess, spawn } from "node:child_process";
|
|
8
|
-
import * as fs from "node:fs";
|
|
9
8
|
import readline from "node:readline";
|
|
10
|
-
import {
|
|
11
|
-
clampThinkingLevel,
|
|
12
|
-
getModel,
|
|
13
|
-
getSupportedThinkingLevels,
|
|
14
|
-
type Message,
|
|
15
|
-
type ModelThinkingLevel,
|
|
16
|
-
} from "@earendil-works/pi-ai";
|
|
9
|
+
import type { Message } from "@earendil-works/pi-ai";
|
|
17
10
|
import type { AgentConfig, ThinkingLevel } from "../agent/agents.js";
|
|
18
11
|
import { getFinalOutput } from "../output/ui.js";
|
|
19
|
-
import {
|
|
20
|
-
import { SENSITIVE_PATTERN } from "../progress/progress-format.js";
|
|
21
|
-
import { isToolCallPart } from "../progress/progress-state.js";
|
|
12
|
+
import { serializeSamplingParams } from "../shared/sampling.js";
|
|
22
13
|
import {
|
|
23
14
|
type OnUpdateCallback,
|
|
24
15
|
type SingleResult,
|
|
25
|
-
type StreamingProgress,
|
|
26
16
|
type SubagentDetails,
|
|
27
17
|
TOOL_RESULT_FAILED_MESSAGE,
|
|
28
|
-
type ToolActivity,
|
|
29
18
|
} from "../shared/types.js";
|
|
30
19
|
import {
|
|
31
20
|
detectMessageError,
|
|
32
|
-
findLastAssistantTextMessage,
|
|
33
21
|
getPiInvocation,
|
|
34
22
|
getSubagentDepth,
|
|
35
23
|
getSubagentRuntimeLimits,
|
|
36
24
|
resolveAgentSkillArgs,
|
|
37
25
|
subagentDepthEnv,
|
|
38
|
-
truncateOutput,
|
|
39
|
-
writePromptToTempFile,
|
|
40
26
|
} from "../shared/utils.js";
|
|
41
27
|
import {
|
|
42
28
|
type ChildEventParseResult,
|
|
@@ -44,7 +30,34 @@ import {
|
|
|
44
30
|
parseChildEventLine,
|
|
45
31
|
TOOL_EXECUTION_UPDATE_EVENT,
|
|
46
32
|
} from "./child-events.js";
|
|
33
|
+
import { getLatestOutcomeFromMessages } from "./complete-outcome.js";
|
|
34
|
+
import {
|
|
35
|
+
buildModelDisplay,
|
|
36
|
+
type ChildModelSettings,
|
|
37
|
+
resolveEffectiveChildModelSettings,
|
|
38
|
+
resolveThinkingLevel,
|
|
39
|
+
} from "./model-resolution.js";
|
|
40
|
+
import {
|
|
41
|
+
appendWithByteLimit,
|
|
42
|
+
resolveCompleteExtensionPath,
|
|
43
|
+
resolveSamplingExtensionPath,
|
|
44
|
+
} from "./process-utils.js";
|
|
47
45
|
import { appendSubagentResultContract } from "./prompt-contract.js";
|
|
46
|
+
import {
|
|
47
|
+
beginPromptSetup,
|
|
48
|
+
cleanupPromptSetupResult,
|
|
49
|
+
cleanupTempPrompt,
|
|
50
|
+
} from "./prompt-setup.js";
|
|
51
|
+
import {
|
|
52
|
+
addMessageToResult,
|
|
53
|
+
createErrorResult,
|
|
54
|
+
errorForDepthLimit,
|
|
55
|
+
errorForUnknownAgent,
|
|
56
|
+
initRuntimeResult,
|
|
57
|
+
type RuntimeResult,
|
|
58
|
+
rebuildResultFromMessages,
|
|
59
|
+
} from "./result-builder.js";
|
|
60
|
+
import { type EmitUpdateFn, makeEmitUpdate } from "./streaming-progress.js";
|
|
48
61
|
import {
|
|
49
62
|
acquireChildSleepInhibitor,
|
|
50
63
|
getProcessTreeSpawnOptions,
|
|
@@ -54,35 +67,13 @@ import {
|
|
|
54
67
|
terminateChildProcess,
|
|
55
68
|
} from "./termination.js";
|
|
56
69
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const model = getModel(provider as never, modelId as never);
|
|
63
|
-
if (!model) return { level: requested };
|
|
64
|
-
const mkWarning = (effective: ThinkingLevel) =>
|
|
65
|
-
`Thinking level "${requested}" not supported by model "${provider}/${modelId}"; using "${effective}" instead`;
|
|
66
|
-
if (model.reasoning === false) {
|
|
67
|
-
return { level: "off", warning: mkWarning("off") };
|
|
68
|
-
}
|
|
69
|
-
if (!model.thinkingLevelMap) return { level: requested };
|
|
70
|
-
const supported = getSupportedThinkingLevels(model);
|
|
71
|
-
if (supported.length === 0) return { level: requested };
|
|
72
|
-
const clamped = clampThinkingLevel(
|
|
73
|
-
model,
|
|
74
|
-
requested as ModelThinkingLevel,
|
|
75
|
-
) as ThinkingLevel;
|
|
76
|
-
if (clamped === requested) return { level: requested };
|
|
77
|
-
return { level: clamped, warning: mkWarning(clamped) };
|
|
78
|
-
}
|
|
70
|
+
const COMPLETE_EXTENSION_PATH = resolveCompleteExtensionPath();
|
|
71
|
+
const SAMPLING_EXTENSION_PATH = resolveSamplingExtensionPath();
|
|
72
|
+
|
|
73
|
+
export { resolveThinkingLevel } from "./model-resolution.js";
|
|
74
|
+
export { makeEmitUpdate } from "./streaming-progress.js";
|
|
79
75
|
|
|
80
76
|
type RuntimeLimits = ReturnType<typeof getSubagentRuntimeLimits>;
|
|
81
|
-
type RuntimeResult = SingleResult & { messages: Message[] };
|
|
82
|
-
type ChildModelSettings = {
|
|
83
|
-
provider?: string | undefined;
|
|
84
|
-
id?: string | undefined;
|
|
85
|
-
};
|
|
86
77
|
type SleepInhibitorAcquirer = (pid: number) => Promise<SleepInhibitorHandle>;
|
|
87
78
|
|
|
88
79
|
type RunSingleAgentOptions = {
|
|
@@ -90,18 +81,9 @@ type RunSingleAgentOptions = {
|
|
|
90
81
|
getOrchestratorPid?: () => unknown;
|
|
91
82
|
};
|
|
92
83
|
|
|
93
|
-
export
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
super("Subagent was aborted");
|
|
97
|
-
this.name = "SubagentAbortError";
|
|
98
|
-
this.result = result;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
type TempPrompt = { dir: string; filePath: string };
|
|
103
|
-
|
|
104
|
-
type PromptSetupResult = { tmpPrompt: TempPrompt | null } | { error: unknown };
|
|
84
|
+
export type RunSingleAgentResult =
|
|
85
|
+
| { kind: "completed"; result: SingleResult }
|
|
86
|
+
| { kind: "aborted"; result: SingleResult };
|
|
105
87
|
|
|
106
88
|
interface SubagentState {
|
|
107
89
|
result: RuntimeResult;
|
|
@@ -112,53 +94,6 @@ interface SubagentState {
|
|
|
112
94
|
terminationPromise?: Promise<unknown>;
|
|
113
95
|
}
|
|
114
96
|
|
|
115
|
-
function appendWithByteLimit(
|
|
116
|
-
current: string,
|
|
117
|
-
data: string | Buffer,
|
|
118
|
-
max: number,
|
|
119
|
-
): string {
|
|
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 "";
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Rationale: Subagent usage reporting needs context window awareness to provide
|
|
142
|
-
* meaningful "context full" indicators to the parent.
|
|
143
|
-
*/
|
|
144
|
-
function resolveContextWindowTokens(msg: Message): number | undefined {
|
|
145
|
-
const m = msg as unknown as Record<string, unknown>;
|
|
146
|
-
if (typeof m["provider"] !== "string" || typeof m["model"] !== "string")
|
|
147
|
-
return;
|
|
148
|
-
try {
|
|
149
|
-
const contextWindow = getModel(
|
|
150
|
-
m["provider"] as never,
|
|
151
|
-
m["model"] as never,
|
|
152
|
-
)?.contextWindow;
|
|
153
|
-
return Number.isFinite(contextWindow) && contextWindow > 0
|
|
154
|
-
? contextWindow
|
|
155
|
-
: undefined;
|
|
156
|
-
} catch {
|
|
157
|
-
/* model lookup failures return undefined to skip context window tracking */
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
97
|
function getAbortReason(signal: AbortSignal): string {
|
|
163
98
|
const { reason } = signal;
|
|
164
99
|
if (reason instanceof Error && reason.message) return reason.message;
|
|
@@ -216,15 +151,13 @@ function startSleepInhibitorRelease(
|
|
|
216
151
|
return acquisitionPromise.then(releaseSleepInhibitor, () => {});
|
|
217
152
|
}
|
|
218
153
|
|
|
219
|
-
function hasCompletedAgentOutput(
|
|
154
|
+
export function hasCompletedAgentOutput(
|
|
155
|
+
result: RuntimeResult,
|
|
156
|
+
outcome?: string,
|
|
157
|
+
): boolean {
|
|
220
158
|
if (result.finalOutput.trim()) return true;
|
|
221
|
-
return
|
|
222
|
-
|
|
223
|
-
msg.role === "assistant" &&
|
|
224
|
-
msg.content.some(
|
|
225
|
-
(part) => part.type === "text" && Boolean(part.text?.trim()),
|
|
226
|
-
),
|
|
227
|
-
);
|
|
159
|
+
if (outcome?.trim()) return true;
|
|
160
|
+
return getFinalOutput(result.messages).trim().length > 0;
|
|
228
161
|
}
|
|
229
162
|
|
|
230
163
|
/**
|
|
@@ -232,15 +165,16 @@ function hasCompletedAgentOutput(result: RuntimeResult): boolean {
|
|
|
232
165
|
* we force-kill them after a grace period and treat it as success (0) if they
|
|
233
166
|
* actually produced output.
|
|
234
167
|
*/
|
|
235
|
-
function getAgentEndTimeoutExitCode(
|
|
168
|
+
export function getAgentEndTimeoutExitCode(
|
|
236
169
|
result: RuntimeResult,
|
|
237
170
|
spawnError: Error | undefined,
|
|
171
|
+
outcome: string | undefined,
|
|
238
172
|
): number | undefined {
|
|
239
173
|
if (result.termination?.cancelReason !== "agent_end_timeout") return;
|
|
240
174
|
if (spawnError) return;
|
|
241
175
|
if (result.stopReason === "error" || result.stopReason === "aborted") return;
|
|
242
176
|
if (result.errorMessage?.trim()) return;
|
|
243
|
-
return hasCompletedAgentOutput(result) ? 0 : 1;
|
|
177
|
+
return hasCompletedAgentOutput(result, outcome) ? 0 : 1;
|
|
244
178
|
}
|
|
245
179
|
|
|
246
180
|
/**
|
|
@@ -303,287 +237,6 @@ async function waitForSubagentProcess(
|
|
|
303
237
|
});
|
|
304
238
|
}
|
|
305
239
|
|
|
306
|
-
function buildModelDisplay(
|
|
307
|
-
effectiveModel: ChildModelSettings,
|
|
308
|
-
thinking: ThinkingLevel,
|
|
309
|
-
): string | undefined {
|
|
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);
|
|
319
|
-
}
|
|
320
|
-
return parts.length > 0 ? parts.join(" ・ ") : undefined;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
const EMPTY_USAGE = {
|
|
324
|
-
input: 0,
|
|
325
|
-
output: 0,
|
|
326
|
-
cacheRead: 0,
|
|
327
|
-
cacheWrite: 0,
|
|
328
|
-
cost: 0,
|
|
329
|
-
contextTokens: 0,
|
|
330
|
-
turns: 0,
|
|
331
|
-
};
|
|
332
|
-
|
|
333
|
-
function initRuntimeResult(
|
|
334
|
-
agentName: string,
|
|
335
|
-
source: "user" | "project" | "unknown",
|
|
336
|
-
task: string,
|
|
337
|
-
modelDisplay: string | undefined,
|
|
338
|
-
): RuntimeResult {
|
|
339
|
-
return {
|
|
340
|
-
agent: agentName,
|
|
341
|
-
agentSource: source,
|
|
342
|
-
task,
|
|
343
|
-
exitCode: 0,
|
|
344
|
-
finalOutput: "",
|
|
345
|
-
messages: [],
|
|
346
|
-
stderr: "",
|
|
347
|
-
usage: { ...EMPTY_USAGE },
|
|
348
|
-
model: modelDisplay,
|
|
349
|
-
};
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
function accumulateUsage(result: RuntimeResult, msg: Message): void {
|
|
353
|
-
if (msg.role !== "assistant") return;
|
|
354
|
-
result.usage.turns++;
|
|
355
|
-
const { usage } = msg;
|
|
356
|
-
if (!usage) return;
|
|
357
|
-
result.usage.input += usage.input || 0;
|
|
358
|
-
result.usage.output += usage.output || 0;
|
|
359
|
-
result.usage.cacheRead += usage.cacheRead || 0;
|
|
360
|
-
result.usage.cacheWrite += usage.cacheWrite || 0;
|
|
361
|
-
result.usage.cost += usage.cost?.total || 0;
|
|
362
|
-
result.usage.contextTokens = usage.totalTokens || 0;
|
|
363
|
-
const ctxWindowTokens = resolveContextWindowTokens(msg);
|
|
364
|
-
if (ctxWindowTokens !== undefined)
|
|
365
|
-
result.usage.contextWindowTokens = ctxWindowTokens;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
function addMessageToResult(result: RuntimeResult, msg: Message): void {
|
|
369
|
-
result.messages.push(msg);
|
|
370
|
-
result.finalOutput = truncateOutput(getFinalOutput(result.messages));
|
|
371
|
-
if (msg.role === "toolResult" && msg.isError) {
|
|
372
|
-
result.errorMessage ||= TOOL_RESULT_FAILED_MESSAGE;
|
|
373
|
-
} else if (result.errorMessage === TOOL_RESULT_FAILED_MESSAGE) {
|
|
374
|
-
delete result.errorMessage;
|
|
375
|
-
}
|
|
376
|
-
if (msg.role === "assistant") {
|
|
377
|
-
accumulateUsage(result, msg);
|
|
378
|
-
if (!result.model && msg.model) result.model = msg.model;
|
|
379
|
-
if (msg.stopReason) result.stopReason = msg.stopReason;
|
|
380
|
-
if (msg.errorMessage) result.errorMessage = msg.errorMessage;
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
function createErrorResult(
|
|
385
|
-
agent: string,
|
|
386
|
-
source: "user" | "project" | "unknown",
|
|
387
|
-
task: string,
|
|
388
|
-
error: string,
|
|
389
|
-
model?: string,
|
|
390
|
-
): SingleResult {
|
|
391
|
-
return {
|
|
392
|
-
agent,
|
|
393
|
-
agentSource: source,
|
|
394
|
-
task,
|
|
395
|
-
exitCode: 1,
|
|
396
|
-
finalOutput: "",
|
|
397
|
-
stderr: error,
|
|
398
|
-
usage: { ...EMPTY_USAGE },
|
|
399
|
-
model,
|
|
400
|
-
};
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
function errorForUnknownAgent(
|
|
404
|
-
agentName: string,
|
|
405
|
-
agents: AgentConfig[],
|
|
406
|
-
task: string,
|
|
407
|
-
): SingleResult {
|
|
408
|
-
const available = agents.map((a) => `"${a.name}"`).join(", ") || "none";
|
|
409
|
-
return createErrorResult(
|
|
410
|
-
agentName,
|
|
411
|
-
"unknown",
|
|
412
|
-
task,
|
|
413
|
-
`Unknown agent: "${agentName}". Available agents: ${available}.`,
|
|
414
|
-
);
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
function errorForDepthLimit(
|
|
418
|
-
agentName: string,
|
|
419
|
-
source: "user" | "project" | "unknown",
|
|
420
|
-
task: string,
|
|
421
|
-
depth: number,
|
|
422
|
-
maxDepth: number,
|
|
423
|
-
model?: string,
|
|
424
|
-
): SingleResult {
|
|
425
|
-
return createErrorResult(
|
|
426
|
-
agentName,
|
|
427
|
-
source,
|
|
428
|
-
task,
|
|
429
|
-
`Subagent nesting limit reached (depth ${depth}/${maxDepth}).`,
|
|
430
|
-
model,
|
|
431
|
-
);
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
async function cleanupTempPrompt(tmpPrompt: TempPrompt): Promise<void> {
|
|
435
|
-
try {
|
|
436
|
-
await fs.promises.unlink(tmpPrompt.filePath);
|
|
437
|
-
await fs.promises.rmdir(tmpPrompt.dir);
|
|
438
|
-
} catch {
|
|
439
|
-
/* temp file cleanup failures are non-fatal; OS will clean up eventually */
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
function beginPromptSetup(agent: AgentConfig): Promise<PromptSetupResult> {
|
|
444
|
-
if (!agent.systemPrompt.trim()) return Promise.resolve({ tmpPrompt: null });
|
|
445
|
-
return writePromptToTempFile(agent.name, agent.systemPrompt).then(
|
|
446
|
-
(tmpPrompt) => ({ tmpPrompt }),
|
|
447
|
-
(error: unknown) => ({ error }),
|
|
448
|
-
);
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
async function cleanupPromptSetupResult(
|
|
452
|
-
setup: PromptSetupResult,
|
|
453
|
-
): Promise<void> {
|
|
454
|
-
if ("tmpPrompt" in setup && setup.tmpPrompt) {
|
|
455
|
-
await cleanupTempPrompt(setup.tmpPrompt);
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
function findRecentMessagesAnchor(messages: Message[]): number {
|
|
460
|
-
return findLastAssistantTextMessage(messages);
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
function deriveStreamingProgress(messages: Message[]): StreamingProgress {
|
|
464
|
-
const toolCalls: { id: string; preview: string }[] = [];
|
|
465
|
-
let lastToolPreview: string | undefined;
|
|
466
|
-
let activeToolActivity: ToolActivity | undefined;
|
|
467
|
-
for (const msg of messages) {
|
|
468
|
-
if (msg.role !== "assistant" || !Array.isArray(msg.content)) continue;
|
|
469
|
-
for (const part of msg.content) {
|
|
470
|
-
if (!isToolCallPart(part)) continue;
|
|
471
|
-
const preview = sanitizeProgressPreview(
|
|
472
|
-
makeToolPreview(part.name, part.arguments),
|
|
473
|
-
part.name,
|
|
474
|
-
);
|
|
475
|
-
toolCalls.push({ id: part.id, preview });
|
|
476
|
-
lastToolPreview = preview;
|
|
477
|
-
activeToolActivity = { toolName: part.name, inputSummary: preview };
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
const activityText = renderToolActivity(activeToolActivity);
|
|
481
|
-
return {
|
|
482
|
-
activeToolActivity,
|
|
483
|
-
activityText,
|
|
484
|
-
toolCalls,
|
|
485
|
-
lastToolPreview,
|
|
486
|
-
};
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
function sanitizeProgressPreview(preview: string, toolName: string): string {
|
|
490
|
-
return SENSITIVE_PATTERN.test(preview) ? toolName : preview;
|
|
491
|
-
}
|
|
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
|
-
|
|
550
|
-
export function makeEmitUpdate(
|
|
551
|
-
result: RuntimeResult,
|
|
552
|
-
onUpdate: OnUpdateCallback | undefined,
|
|
553
|
-
makeDetails: (
|
|
554
|
-
results: RuntimeResult[],
|
|
555
|
-
options?: { includeMessages?: boolean; recentMessages?: Message[] },
|
|
556
|
-
) => SubagentDetails,
|
|
557
|
-
): (options?: {
|
|
558
|
-
toolActivity?: ToolActivity;
|
|
559
|
-
toolResultCompleted?: boolean;
|
|
560
|
-
}) => void {
|
|
561
|
-
return (options) => {
|
|
562
|
-
const msgs = result.messages;
|
|
563
|
-
const anchorIdx = findRecentMessagesAnchor(msgs);
|
|
564
|
-
const recentMessages =
|
|
565
|
-
anchorIdx >= 0 ? msgs.slice(anchorIdx) : msgs.slice(-5);
|
|
566
|
-
const progress = deriveStreamingProgress(msgs);
|
|
567
|
-
if (options) {
|
|
568
|
-
applyActivityUpdates(
|
|
569
|
-
progress,
|
|
570
|
-
options,
|
|
571
|
-
result.progress?.activeToolActivity,
|
|
572
|
-
);
|
|
573
|
-
}
|
|
574
|
-
result.progress = progress;
|
|
575
|
-
onUpdate?.({
|
|
576
|
-
content: [
|
|
577
|
-
{ type: "text", text: progress.activityText ?? "(running...)" },
|
|
578
|
-
],
|
|
579
|
-
details: makeDetails([result], {
|
|
580
|
-
includeMessages: true,
|
|
581
|
-
recentMessages,
|
|
582
|
-
}),
|
|
583
|
-
});
|
|
584
|
-
};
|
|
585
|
-
}
|
|
586
|
-
|
|
587
240
|
function makeRequestTerminator(
|
|
588
241
|
proc: ChildProcess,
|
|
589
242
|
terminateOptions: {
|
|
@@ -613,10 +266,7 @@ function clearGraceTimer(state: SubagentState): void {
|
|
|
613
266
|
function handleMessageEvent(
|
|
614
267
|
event: ChildKnownEvent,
|
|
615
268
|
state: SubagentState,
|
|
616
|
-
emitUpdate:
|
|
617
|
-
toolActivity?: ToolActivity;
|
|
618
|
-
toolResultCompleted?: boolean;
|
|
619
|
-
}) => void,
|
|
269
|
+
emitUpdate: EmitUpdateFn,
|
|
620
270
|
): void {
|
|
621
271
|
if (event.type !== "message_end" && event.type !== "tool_result_end") return;
|
|
622
272
|
if (event.message) {
|
|
@@ -628,10 +278,7 @@ function handleMessageEvent(
|
|
|
628
278
|
|
|
629
279
|
function handleToolExecutionUpdateEvent(
|
|
630
280
|
event: ChildKnownEvent,
|
|
631
|
-
emitUpdate:
|
|
632
|
-
toolActivity?: ToolActivity;
|
|
633
|
-
toolResultCompleted?: boolean;
|
|
634
|
-
}) => void,
|
|
281
|
+
emitUpdate: EmitUpdateFn,
|
|
635
282
|
): void {
|
|
636
283
|
if (event.type !== TOOL_EXECUTION_UPDATE_EVENT) return;
|
|
637
284
|
emitUpdate({ toolActivity: event.toolActivity });
|
|
@@ -640,17 +287,12 @@ function handleToolExecutionUpdateEvent(
|
|
|
640
287
|
function handleAgentEndEvent(
|
|
641
288
|
event: ChildKnownEvent,
|
|
642
289
|
state: SubagentState,
|
|
643
|
-
emitUpdate:
|
|
644
|
-
toolActivity?: ToolActivity;
|
|
645
|
-
toolResultCompleted?: boolean;
|
|
646
|
-
}) => void,
|
|
290
|
+
emitUpdate: EmitUpdateFn,
|
|
647
291
|
requestTermination: (reason: string) => Promise<unknown>,
|
|
648
292
|
): void {
|
|
649
293
|
if (event.type !== "agent_end") return;
|
|
650
|
-
if (
|
|
651
|
-
|
|
652
|
-
addMessageToResult(state.result, msg);
|
|
653
|
-
}
|
|
294
|
+
if (Array.isArray(event.messages) && event.messages.length > 0) {
|
|
295
|
+
rebuildResultFromMessages(state.result, event.messages as Message[]);
|
|
654
296
|
emitUpdate();
|
|
655
297
|
}
|
|
656
298
|
if (state.agentEndGraceTimer || state.terminationPromise) return;
|
|
@@ -677,10 +319,7 @@ function formatUnknownEventDiagnostic(
|
|
|
677
319
|
function processEventLine(
|
|
678
320
|
line: string,
|
|
679
321
|
state: SubagentState,
|
|
680
|
-
emitUpdate:
|
|
681
|
-
toolActivity?: ToolActivity;
|
|
682
|
-
toolResultCompleted?: boolean;
|
|
683
|
-
}) => void,
|
|
322
|
+
emitUpdate: EmitUpdateFn,
|
|
684
323
|
requestTermination: (reason: string) => Promise<unknown>,
|
|
685
324
|
debugEventDiagnostics: boolean,
|
|
686
325
|
): void {
|
|
@@ -719,16 +358,11 @@ function setupAbortHandler(
|
|
|
719
358
|
return onAbort;
|
|
720
359
|
}
|
|
721
360
|
|
|
722
|
-
function
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
provider: agent.provider ?? parentModel?.provider,
|
|
728
|
-
id:
|
|
729
|
-
agent.model ??
|
|
730
|
-
(agent.provider === undefined ? parentModel?.id : undefined),
|
|
731
|
-
};
|
|
361
|
+
function buildSamplingEnv(agent: AgentConfig): string | undefined {
|
|
362
|
+
return serializeSamplingParams({
|
|
363
|
+
temperature: agent.temperature,
|
|
364
|
+
topP: agent.topP,
|
|
365
|
+
});
|
|
732
366
|
}
|
|
733
367
|
|
|
734
368
|
function buildPiArgs(
|
|
@@ -744,11 +378,16 @@ function buildPiArgs(
|
|
|
744
378
|
args.push("--provider", effectiveModel.provider);
|
|
745
379
|
if (effectiveModel.id) args.push("--model", effectiveModel.id);
|
|
746
380
|
args.push("--thinking", thinking);
|
|
747
|
-
if (agent.tools
|
|
381
|
+
if (agent.tools) {
|
|
382
|
+
const tools = new Set(agent.tools);
|
|
383
|
+
tools.add("complete");
|
|
384
|
+
args.push("--tools", [...tools].join(","));
|
|
385
|
+
}
|
|
748
386
|
if (agent.skills) args.push("--no-skills", ...resolvedSkills.args);
|
|
749
387
|
if (tmpPrompt) {
|
|
750
388
|
args.push("--append-system-prompt", tmpPrompt.filePath);
|
|
751
389
|
}
|
|
390
|
+
args.push("--extension", COMPLETE_EXTENSION_PATH);
|
|
752
391
|
const taskPrompt = task
|
|
753
392
|
? `Task: ${task}`
|
|
754
393
|
: "Run according to your system prompt. If no explicit task was provided, use the default context described there.";
|
|
@@ -756,13 +395,19 @@ function buildPiArgs(
|
|
|
756
395
|
return args;
|
|
757
396
|
}
|
|
758
397
|
|
|
398
|
+
function buildChildEnv(samplingEnv: string | undefined): NodeJS.ProcessEnv {
|
|
399
|
+
const { PI_SAMPLING_PARAMS: _, ...parentEnv } = process.env;
|
|
400
|
+
return {
|
|
401
|
+
...parentEnv,
|
|
402
|
+
...subagentDepthEnv(),
|
|
403
|
+
...(samplingEnv ? { PI_SAMPLING_PARAMS: samplingEnv } : {}),
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
759
407
|
function setupChildProcess(
|
|
760
408
|
proc: ChildProcess,
|
|
761
409
|
state: SubagentState,
|
|
762
|
-
emitUpdate:
|
|
763
|
-
toolActivity?: ToolActivity;
|
|
764
|
-
toolResultCompleted?: boolean;
|
|
765
|
-
}) => void,
|
|
410
|
+
emitUpdate: EmitUpdateFn,
|
|
766
411
|
requestTermination: (reason: string) => Promise<unknown>,
|
|
767
412
|
debugEventDiagnostics: boolean,
|
|
768
413
|
): void {
|
|
@@ -801,17 +446,16 @@ function setupChildProcess(
|
|
|
801
446
|
async function finalizeResult(
|
|
802
447
|
state: SubagentState,
|
|
803
448
|
startedAt: number,
|
|
804
|
-
): Promise<
|
|
449
|
+
): Promise<RunSingleAgentResult> {
|
|
805
450
|
state.result.durationMs = Date.now() - startedAt;
|
|
806
451
|
clearGraceTimer(state);
|
|
807
452
|
if (state.terminationPromise) await state.terminationPromise;
|
|
808
453
|
if (state.spawnError) state.result.exitCode = 1;
|
|
809
|
-
|
|
810
|
-
state.result.errorMessage ||= TOOL_RESULT_FAILED_MESSAGE;
|
|
811
|
-
}
|
|
454
|
+
const outcome = getLatestOutcomeFromMessages(state.result.messages);
|
|
812
455
|
const agentEndTimeoutExitCode = getAgentEndTimeoutExitCode(
|
|
813
456
|
state.result,
|
|
814
457
|
state.spawnError,
|
|
458
|
+
outcome,
|
|
815
459
|
);
|
|
816
460
|
if (agentEndTimeoutExitCode !== undefined) {
|
|
817
461
|
state.result.exitCode = agentEndTimeoutExitCode;
|
|
@@ -819,11 +463,25 @@ async function finalizeResult(
|
|
|
819
463
|
if (state.result.termination?.cancelReason === "agent_end_timeout") {
|
|
820
464
|
state.result.stderr = state.result.stderr.replace(/^Terminated\r?\n/gm, "");
|
|
821
465
|
}
|
|
466
|
+
// Skip unresolved-error detection when complete succeeded: intermediate tool
|
|
467
|
+
// errors (e.g. a linter exiting non-zero) are expected and already addressed.
|
|
468
|
+
if (!outcome && detectMessageError(state.result.messages)) {
|
|
469
|
+
state.result.errorMessage ||= TOOL_RESULT_FAILED_MESSAGE;
|
|
470
|
+
}
|
|
822
471
|
if (state.wasAborted) {
|
|
823
472
|
state.result.stderr = "";
|
|
824
|
-
|
|
473
|
+
return { kind: "aborted", result: state.result };
|
|
474
|
+
}
|
|
475
|
+
const isSemanticallySuccessful =
|
|
476
|
+
!state.spawnError &&
|
|
477
|
+
state.result.exitCode === 0 &&
|
|
478
|
+
(Boolean(outcome) ||
|
|
479
|
+
(!state.result.errorMessage?.trim() &&
|
|
480
|
+
!detectMessageError(state.result.messages)));
|
|
481
|
+
if (isSemanticallySuccessful && outcome) {
|
|
482
|
+
state.result.outcome = outcome;
|
|
825
483
|
}
|
|
826
|
-
return state.result;
|
|
484
|
+
return { kind: "completed", result: state.result };
|
|
827
485
|
}
|
|
828
486
|
|
|
829
487
|
/**
|
|
@@ -854,19 +512,26 @@ export async function runSingleAgent(
|
|
|
854
512
|
parentThinking: ThinkingLevel,
|
|
855
513
|
debugEventDiagnostics = false,
|
|
856
514
|
options: RunSingleAgentOptions = {},
|
|
857
|
-
): Promise<
|
|
515
|
+
): Promise<RunSingleAgentResult> {
|
|
858
516
|
const agent = agents.find((a) => a.name === agentName);
|
|
859
|
-
if (!agent)
|
|
517
|
+
if (!agent)
|
|
518
|
+
return {
|
|
519
|
+
kind: "completed",
|
|
520
|
+
result: errorForUnknownAgent(agentName, agents, task),
|
|
521
|
+
};
|
|
860
522
|
const runtimeLimits = getSubagentRuntimeLimits();
|
|
861
523
|
const depth = getSubagentDepth();
|
|
862
524
|
if (depth >= runtimeLimits.maxDepth) {
|
|
863
|
-
return
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
525
|
+
return {
|
|
526
|
+
kind: "completed",
|
|
527
|
+
result: errorForDepthLimit(
|
|
528
|
+
agentName,
|
|
529
|
+
agent.source,
|
|
530
|
+
task,
|
|
531
|
+
depth,
|
|
532
|
+
runtimeLimits.maxDepth,
|
|
533
|
+
),
|
|
534
|
+
};
|
|
870
535
|
}
|
|
871
536
|
const requestedThinking = agent.thinking ?? parentThinking;
|
|
872
537
|
const effectiveModel = resolveEffectiveChildModelSettings(agent, parentModel);
|
|
@@ -888,13 +553,16 @@ export async function runSingleAgent(
|
|
|
888
553
|
if ("error" in resolvedSkills) {
|
|
889
554
|
const promptSetup = await promptSetupPromise;
|
|
890
555
|
await cleanupPromptSetupResult(promptSetup);
|
|
891
|
-
return
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
556
|
+
return {
|
|
557
|
+
kind: "completed",
|
|
558
|
+
result: createErrorResult(
|
|
559
|
+
agentName,
|
|
560
|
+
agent.source,
|
|
561
|
+
task,
|
|
562
|
+
resolvedSkills.error,
|
|
563
|
+
modelDisplay,
|
|
564
|
+
),
|
|
565
|
+
};
|
|
898
566
|
}
|
|
899
567
|
const promptSetup = await promptSetupPromise;
|
|
900
568
|
if ("error" in promptSetup) throw promptSetup.error;
|
|
@@ -906,6 +574,7 @@ export async function runSingleAgent(
|
|
|
906
574
|
};
|
|
907
575
|
if (thinkingWarning) state.result.thinkingWarning = thinkingWarning;
|
|
908
576
|
const tmpPrompt = promptSetup.tmpPrompt;
|
|
577
|
+
const samplingEnv = buildSamplingEnv(agent);
|
|
909
578
|
try {
|
|
910
579
|
const args = buildPiArgs(
|
|
911
580
|
agent,
|
|
@@ -915,17 +584,21 @@ export async function runSingleAgent(
|
|
|
915
584
|
resolvedSkills,
|
|
916
585
|
tmpPrompt,
|
|
917
586
|
);
|
|
587
|
+
if (samplingEnv) {
|
|
588
|
+
args.push("--extension", SAMPLING_EXTENSION_PATH);
|
|
589
|
+
}
|
|
918
590
|
const invocation = getPiInvocation(args);
|
|
919
591
|
const terminateOptions = {
|
|
920
592
|
tree: true,
|
|
921
593
|
platform: process.platform,
|
|
922
594
|
processTreeDetached: process.platform !== "win32",
|
|
923
595
|
};
|
|
596
|
+
const childEnv = buildChildEnv(samplingEnv);
|
|
924
597
|
const proc = spawn(invocation.command, invocation.args, {
|
|
925
598
|
cwd: defaultCwd,
|
|
926
599
|
shell: invocation.command === "pi" && process.platform === "win32",
|
|
927
600
|
stdio: ["ignore", "pipe", "pipe"],
|
|
928
|
-
env:
|
|
601
|
+
env: childEnv,
|
|
929
602
|
...getProcessTreeSpawnOptions(terminateOptions.tree),
|
|
930
603
|
});
|
|
931
604
|
const processDone = waitForSubagentProcess(proc);
|