@mystilleef/pi-subagent 0.10.2 → 0.12.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 +84 -10
- package/package.json +8 -8
- package/src/agent/agents.ts +75 -9
- 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 +114 -0
- package/src/child/process.ts +225 -471
- package/src/child/prompt-contract.ts +22 -10
- 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/limits.ts +81 -0
- package/src/shared/message-utils.ts +56 -0
- package/src/shared/resource-resolution.ts +289 -0
- package/src/shared/sampling.ts +56 -0
- package/src/shared/types.ts +1 -0
- package/src/shared/utils.ts +31 -204
package/src/child/process.ts
CHANGED
|
@@ -5,38 +5,25 @@
|
|
|
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,
|
|
24
|
+
resolveAgentExtensionPaths,
|
|
36
25
|
resolveAgentSkillArgs,
|
|
37
26
|
subagentDepthEnv,
|
|
38
|
-
truncateOutput,
|
|
39
|
-
writePromptToTempFile,
|
|
40
27
|
} from "../shared/utils.js";
|
|
41
28
|
import {
|
|
42
29
|
type ChildEventParseResult,
|
|
@@ -44,7 +31,35 @@ import {
|
|
|
44
31
|
parseChildEventLine,
|
|
45
32
|
TOOL_EXECUTION_UPDATE_EVENT,
|
|
46
33
|
} from "./child-events.js";
|
|
47
|
-
import {
|
|
34
|
+
import { getLatestOutcomeFromMessages } from "./complete-outcome.js";
|
|
35
|
+
import {
|
|
36
|
+
buildModelDisplay,
|
|
37
|
+
type ChildModelSettings,
|
|
38
|
+
resolveEffectiveChildModelSettings,
|
|
39
|
+
resolveThinkingLevel,
|
|
40
|
+
} from "./model-resolution.js";
|
|
41
|
+
import {
|
|
42
|
+
appendWithByteLimit,
|
|
43
|
+
resolveCompleteExtensionPath,
|
|
44
|
+
resolvePackageExtensionPath,
|
|
45
|
+
resolveSamplingExtensionPath,
|
|
46
|
+
} from "./process-utils.js";
|
|
47
|
+
import { SUBAGENT_RESULT_CONTRACT } from "./prompt-contract.js";
|
|
48
|
+
import {
|
|
49
|
+
beginPromptSetup,
|
|
50
|
+
cleanupPromptSetupResult,
|
|
51
|
+
cleanupTempPrompt,
|
|
52
|
+
} from "./prompt-setup.js";
|
|
53
|
+
import {
|
|
54
|
+
addMessageToResult,
|
|
55
|
+
createErrorResult,
|
|
56
|
+
errorForDepthLimit,
|
|
57
|
+
errorForUnknownAgent,
|
|
58
|
+
initRuntimeResult,
|
|
59
|
+
type RuntimeResult,
|
|
60
|
+
rebuildResultFromMessages,
|
|
61
|
+
} from "./result-builder.js";
|
|
62
|
+
import { type EmitUpdateFn, makeEmitUpdate } from "./streaming-progress.js";
|
|
48
63
|
import {
|
|
49
64
|
acquireChildSleepInhibitor,
|
|
50
65
|
getProcessTreeSpawnOptions,
|
|
@@ -54,35 +69,14 @@ import {
|
|
|
54
69
|
terminateChildProcess,
|
|
55
70
|
} from "./termination.js";
|
|
56
71
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
}
|
|
72
|
+
const COMPLETE_EXTENSION_PATH = resolveCompleteExtensionPath();
|
|
73
|
+
const SAMPLING_EXTENSION_PATH = resolveSamplingExtensionPath();
|
|
74
|
+
const PACKAGE_EXTENSION_PATH = resolvePackageExtensionPath();
|
|
75
|
+
|
|
76
|
+
export { resolveThinkingLevel } from "./model-resolution.js";
|
|
77
|
+
export { makeEmitUpdate } from "./streaming-progress.js";
|
|
79
78
|
|
|
80
79
|
type RuntimeLimits = ReturnType<typeof getSubagentRuntimeLimits>;
|
|
81
|
-
type RuntimeResult = SingleResult & { messages: Message[] };
|
|
82
|
-
type ChildModelSettings = {
|
|
83
|
-
provider?: string | undefined;
|
|
84
|
-
id?: string | undefined;
|
|
85
|
-
};
|
|
86
80
|
type SleepInhibitorAcquirer = (pid: number) => Promise<SleepInhibitorHandle>;
|
|
87
81
|
|
|
88
82
|
type RunSingleAgentOptions = {
|
|
@@ -90,18 +84,9 @@ type RunSingleAgentOptions = {
|
|
|
90
84
|
getOrchestratorPid?: () => unknown;
|
|
91
85
|
};
|
|
92
86
|
|
|
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 };
|
|
87
|
+
export type RunSingleAgentResult =
|
|
88
|
+
| { kind: "completed"; result: SingleResult }
|
|
89
|
+
| { kind: "aborted"; result: SingleResult };
|
|
105
90
|
|
|
106
91
|
interface SubagentState {
|
|
107
92
|
result: RuntimeResult;
|
|
@@ -112,53 +97,6 @@ interface SubagentState {
|
|
|
112
97
|
terminationPromise?: Promise<unknown>;
|
|
113
98
|
}
|
|
114
99
|
|
|
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
100
|
function getAbortReason(signal: AbortSignal): string {
|
|
163
101
|
const { reason } = signal;
|
|
164
102
|
if (reason instanceof Error && reason.message) return reason.message;
|
|
@@ -216,15 +154,13 @@ function startSleepInhibitorRelease(
|
|
|
216
154
|
return acquisitionPromise.then(releaseSleepInhibitor, () => {});
|
|
217
155
|
}
|
|
218
156
|
|
|
219
|
-
function hasCompletedAgentOutput(
|
|
157
|
+
export function hasCompletedAgentOutput(
|
|
158
|
+
result: RuntimeResult,
|
|
159
|
+
outcome?: string,
|
|
160
|
+
): boolean {
|
|
220
161
|
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
|
-
);
|
|
162
|
+
if (outcome?.trim()) return true;
|
|
163
|
+
return getFinalOutput(result.messages).trim().length > 0;
|
|
228
164
|
}
|
|
229
165
|
|
|
230
166
|
/**
|
|
@@ -232,15 +168,16 @@ function hasCompletedAgentOutput(result: RuntimeResult): boolean {
|
|
|
232
168
|
* we force-kill them after a grace period and treat it as success (0) if they
|
|
233
169
|
* actually produced output.
|
|
234
170
|
*/
|
|
235
|
-
function getAgentEndTimeoutExitCode(
|
|
171
|
+
export function getAgentEndTimeoutExitCode(
|
|
236
172
|
result: RuntimeResult,
|
|
237
173
|
spawnError: Error | undefined,
|
|
174
|
+
outcome: string | undefined,
|
|
238
175
|
): number | undefined {
|
|
239
176
|
if (result.termination?.cancelReason !== "agent_end_timeout") return;
|
|
240
177
|
if (spawnError) return;
|
|
241
178
|
if (result.stopReason === "error" || result.stopReason === "aborted") return;
|
|
242
179
|
if (result.errorMessage?.trim()) return;
|
|
243
|
-
return hasCompletedAgentOutput(result) ? 0 : 1;
|
|
180
|
+
return hasCompletedAgentOutput(result, outcome) ? 0 : 1;
|
|
244
181
|
}
|
|
245
182
|
|
|
246
183
|
/**
|
|
@@ -303,287 +240,6 @@ async function waitForSubagentProcess(
|
|
|
303
240
|
});
|
|
304
241
|
}
|
|
305
242
|
|
|
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
243
|
function makeRequestTerminator(
|
|
588
244
|
proc: ChildProcess,
|
|
589
245
|
terminateOptions: {
|
|
@@ -613,10 +269,7 @@ function clearGraceTimer(state: SubagentState): void {
|
|
|
613
269
|
function handleMessageEvent(
|
|
614
270
|
event: ChildKnownEvent,
|
|
615
271
|
state: SubagentState,
|
|
616
|
-
emitUpdate:
|
|
617
|
-
toolActivity?: ToolActivity;
|
|
618
|
-
toolResultCompleted?: boolean;
|
|
619
|
-
}) => void,
|
|
272
|
+
emitUpdate: EmitUpdateFn,
|
|
620
273
|
): void {
|
|
621
274
|
if (event.type !== "message_end" && event.type !== "tool_result_end") return;
|
|
622
275
|
if (event.message) {
|
|
@@ -628,10 +281,7 @@ function handleMessageEvent(
|
|
|
628
281
|
|
|
629
282
|
function handleToolExecutionUpdateEvent(
|
|
630
283
|
event: ChildKnownEvent,
|
|
631
|
-
emitUpdate:
|
|
632
|
-
toolActivity?: ToolActivity;
|
|
633
|
-
toolResultCompleted?: boolean;
|
|
634
|
-
}) => void,
|
|
284
|
+
emitUpdate: EmitUpdateFn,
|
|
635
285
|
): void {
|
|
636
286
|
if (event.type !== TOOL_EXECUTION_UPDATE_EVENT) return;
|
|
637
287
|
emitUpdate({ toolActivity: event.toolActivity });
|
|
@@ -640,17 +290,12 @@ function handleToolExecutionUpdateEvent(
|
|
|
640
290
|
function handleAgentEndEvent(
|
|
641
291
|
event: ChildKnownEvent,
|
|
642
292
|
state: SubagentState,
|
|
643
|
-
emitUpdate:
|
|
644
|
-
toolActivity?: ToolActivity;
|
|
645
|
-
toolResultCompleted?: boolean;
|
|
646
|
-
}) => void,
|
|
293
|
+
emitUpdate: EmitUpdateFn,
|
|
647
294
|
requestTermination: (reason: string) => Promise<unknown>,
|
|
648
295
|
): void {
|
|
649
296
|
if (event.type !== "agent_end") return;
|
|
650
|
-
if (
|
|
651
|
-
|
|
652
|
-
addMessageToResult(state.result, msg);
|
|
653
|
-
}
|
|
297
|
+
if (Array.isArray(event.messages) && event.messages.length > 0) {
|
|
298
|
+
rebuildResultFromMessages(state.result, event.messages as Message[]);
|
|
654
299
|
emitUpdate();
|
|
655
300
|
}
|
|
656
301
|
if (state.agentEndGraceTimer || state.terminationPromise) return;
|
|
@@ -677,10 +322,7 @@ function formatUnknownEventDiagnostic(
|
|
|
677
322
|
function processEventLine(
|
|
678
323
|
line: string,
|
|
679
324
|
state: SubagentState,
|
|
680
|
-
emitUpdate:
|
|
681
|
-
toolActivity?: ToolActivity;
|
|
682
|
-
toolResultCompleted?: boolean;
|
|
683
|
-
}) => void,
|
|
325
|
+
emitUpdate: EmitUpdateFn,
|
|
684
326
|
requestTermination: (reason: string) => Promise<unknown>,
|
|
685
327
|
debugEventDiagnostics: boolean,
|
|
686
328
|
): void {
|
|
@@ -719,50 +361,101 @@ function setupAbortHandler(
|
|
|
719
361
|
return onAbort;
|
|
720
362
|
}
|
|
721
363
|
|
|
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
|
-
};
|
|
364
|
+
function buildSamplingEnv(agent: AgentConfig): string | undefined {
|
|
365
|
+
return serializeSamplingParams({
|
|
366
|
+
temperature: agent.temperature,
|
|
367
|
+
topP: agent.topP,
|
|
368
|
+
});
|
|
732
369
|
}
|
|
733
370
|
|
|
734
|
-
|
|
735
|
-
agent: AgentConfig
|
|
736
|
-
task: string
|
|
737
|
-
effectiveModel: ChildModelSettings
|
|
738
|
-
thinking: ThinkingLevel
|
|
739
|
-
resolvedSkills: { args: string[] }
|
|
740
|
-
tmpPrompt: { filePath: string } | null
|
|
741
|
-
|
|
742
|
-
|
|
371
|
+
export interface BuildPiArgsConfig {
|
|
372
|
+
agent: AgentConfig;
|
|
373
|
+
task: string;
|
|
374
|
+
effectiveModel: ChildModelSettings;
|
|
375
|
+
thinking: ThinkingLevel;
|
|
376
|
+
resolvedSkills: { args: string[] };
|
|
377
|
+
tmpPrompt: { filePath: string } | null;
|
|
378
|
+
resolvedExtensionPaths?: string[] | undefined;
|
|
379
|
+
samplingEnv?: string | undefined;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export function buildPiArgs(config: BuildPiArgsConfig): string[] {
|
|
383
|
+
const {
|
|
384
|
+
agent,
|
|
385
|
+
task,
|
|
386
|
+
effectiveModel,
|
|
387
|
+
thinking,
|
|
388
|
+
resolvedSkills,
|
|
389
|
+
tmpPrompt,
|
|
390
|
+
resolvedExtensionPaths,
|
|
391
|
+
samplingEnv,
|
|
392
|
+
} = config;
|
|
393
|
+
const args: string[] = [
|
|
394
|
+
"--mode",
|
|
395
|
+
"json",
|
|
396
|
+
"-p",
|
|
397
|
+
"--no-session",
|
|
398
|
+
"--approve",
|
|
399
|
+
"--no-themes",
|
|
400
|
+
"--no-prompt-templates",
|
|
401
|
+
];
|
|
402
|
+
if (agent.extensions !== undefined) {
|
|
403
|
+
args.push("--no-extensions");
|
|
404
|
+
}
|
|
743
405
|
if (effectiveModel.provider && effectiveModel.id)
|
|
744
406
|
args.push("--provider", effectiveModel.provider);
|
|
745
407
|
if (effectiveModel.id) args.push("--model", effectiveModel.id);
|
|
746
408
|
args.push("--thinking", thinking);
|
|
747
|
-
if (agent.tools
|
|
748
|
-
|
|
409
|
+
if (agent.tools) {
|
|
410
|
+
const tools = new Set(agent.tools);
|
|
411
|
+
tools.add("complete");
|
|
412
|
+
args.push("--tools", [...tools].join(","));
|
|
413
|
+
}
|
|
414
|
+
if (agent.skills !== undefined)
|
|
415
|
+
args.push("--no-skills", ...resolvedSkills.args);
|
|
416
|
+
if (agent.context === false) args.push("--no-context-files");
|
|
749
417
|
if (tmpPrompt) {
|
|
750
|
-
|
|
418
|
+
if (agent.replacePrompt) {
|
|
419
|
+
args.push("--system-prompt", tmpPrompt.filePath);
|
|
420
|
+
} else {
|
|
421
|
+
args.push("--append-system-prompt", tmpPrompt.filePath);
|
|
422
|
+
}
|
|
751
423
|
}
|
|
424
|
+
if (agent.extensions !== undefined) {
|
|
425
|
+
args.push("--extension", PACKAGE_EXTENSION_PATH);
|
|
426
|
+
if (resolvedExtensionPaths && resolvedExtensionPaths.length > 0) {
|
|
427
|
+
for (const rp of resolvedExtensionPaths) {
|
|
428
|
+
if (rp !== PACKAGE_EXTENSION_PATH) {
|
|
429
|
+
args.push("--extension", rp);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
args.push("--extension", COMPLETE_EXTENSION_PATH);
|
|
435
|
+
if (samplingEnv) {
|
|
436
|
+
args.push("--extension", SAMPLING_EXTENSION_PATH);
|
|
437
|
+
}
|
|
438
|
+
args.push("--append-system-prompt", SUBAGENT_RESULT_CONTRACT);
|
|
752
439
|
const taskPrompt = task
|
|
753
440
|
? `Task: ${task}`
|
|
754
441
|
: "Run according to your system prompt. If no explicit task was provided, use the default context described there.";
|
|
755
|
-
args.push(
|
|
442
|
+
args.push(taskPrompt);
|
|
756
443
|
return args;
|
|
757
444
|
}
|
|
758
445
|
|
|
446
|
+
function buildChildEnv(samplingEnv: string | undefined): NodeJS.ProcessEnv {
|
|
447
|
+
const { PI_SAMPLING_PARAMS: _, ...parentEnv } = process.env;
|
|
448
|
+
return {
|
|
449
|
+
...parentEnv,
|
|
450
|
+
...subagentDepthEnv(),
|
|
451
|
+
...(samplingEnv ? { PI_SAMPLING_PARAMS: samplingEnv } : {}),
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
759
455
|
function setupChildProcess(
|
|
760
456
|
proc: ChildProcess,
|
|
761
457
|
state: SubagentState,
|
|
762
|
-
emitUpdate:
|
|
763
|
-
toolActivity?: ToolActivity;
|
|
764
|
-
toolResultCompleted?: boolean;
|
|
765
|
-
}) => void,
|
|
458
|
+
emitUpdate: EmitUpdateFn,
|
|
766
459
|
requestTermination: (reason: string) => Promise<unknown>,
|
|
767
460
|
debugEventDiagnostics: boolean,
|
|
768
461
|
): void {
|
|
@@ -801,17 +494,16 @@ function setupChildProcess(
|
|
|
801
494
|
async function finalizeResult(
|
|
802
495
|
state: SubagentState,
|
|
803
496
|
startedAt: number,
|
|
804
|
-
): Promise<
|
|
497
|
+
): Promise<RunSingleAgentResult> {
|
|
805
498
|
state.result.durationMs = Date.now() - startedAt;
|
|
806
499
|
clearGraceTimer(state);
|
|
807
500
|
if (state.terminationPromise) await state.terminationPromise;
|
|
808
501
|
if (state.spawnError) state.result.exitCode = 1;
|
|
809
|
-
|
|
810
|
-
state.result.errorMessage ||= TOOL_RESULT_FAILED_MESSAGE;
|
|
811
|
-
}
|
|
502
|
+
const outcome = getLatestOutcomeFromMessages(state.result.messages);
|
|
812
503
|
const agentEndTimeoutExitCode = getAgentEndTimeoutExitCode(
|
|
813
504
|
state.result,
|
|
814
505
|
state.spawnError,
|
|
506
|
+
outcome,
|
|
815
507
|
);
|
|
816
508
|
if (agentEndTimeoutExitCode !== undefined) {
|
|
817
509
|
state.result.exitCode = agentEndTimeoutExitCode;
|
|
@@ -819,11 +511,25 @@ async function finalizeResult(
|
|
|
819
511
|
if (state.result.termination?.cancelReason === "agent_end_timeout") {
|
|
820
512
|
state.result.stderr = state.result.stderr.replace(/^Terminated\r?\n/gm, "");
|
|
821
513
|
}
|
|
514
|
+
// Skip unresolved-error detection when complete succeeded: intermediate tool
|
|
515
|
+
// errors (e.g. a linter exiting non-zero) are expected and already addressed.
|
|
516
|
+
if (!outcome && detectMessageError(state.result.messages)) {
|
|
517
|
+
state.result.errorMessage ||= TOOL_RESULT_FAILED_MESSAGE;
|
|
518
|
+
}
|
|
822
519
|
if (state.wasAborted) {
|
|
823
520
|
state.result.stderr = "";
|
|
824
|
-
|
|
521
|
+
return { kind: "aborted", result: state.result };
|
|
825
522
|
}
|
|
826
|
-
|
|
523
|
+
const isSemanticallySuccessful =
|
|
524
|
+
!state.spawnError &&
|
|
525
|
+
state.result.exitCode === 0 &&
|
|
526
|
+
(Boolean(outcome) ||
|
|
527
|
+
(!state.result.errorMessage?.trim() &&
|
|
528
|
+
!detectMessageError(state.result.messages)));
|
|
529
|
+
if (isSemanticallySuccessful && outcome) {
|
|
530
|
+
state.result.outcome = outcome;
|
|
531
|
+
}
|
|
532
|
+
return { kind: "completed", result: state.result };
|
|
827
533
|
}
|
|
828
534
|
|
|
829
535
|
/**
|
|
@@ -854,19 +560,26 @@ export async function runSingleAgent(
|
|
|
854
560
|
parentThinking: ThinkingLevel,
|
|
855
561
|
debugEventDiagnostics = false,
|
|
856
562
|
options: RunSingleAgentOptions = {},
|
|
857
|
-
): Promise<
|
|
563
|
+
): Promise<RunSingleAgentResult> {
|
|
858
564
|
const agent = agents.find((a) => a.name === agentName);
|
|
859
|
-
if (!agent)
|
|
565
|
+
if (!agent)
|
|
566
|
+
return {
|
|
567
|
+
kind: "completed",
|
|
568
|
+
result: errorForUnknownAgent(agentName, agents, task),
|
|
569
|
+
};
|
|
860
570
|
const runtimeLimits = getSubagentRuntimeLimits();
|
|
861
571
|
const depth = getSubagentDepth();
|
|
862
572
|
if (depth >= runtimeLimits.maxDepth) {
|
|
863
|
-
return
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
573
|
+
return {
|
|
574
|
+
kind: "completed",
|
|
575
|
+
result: errorForDepthLimit(
|
|
576
|
+
agentName,
|
|
577
|
+
agent.source,
|
|
578
|
+
task,
|
|
579
|
+
depth,
|
|
580
|
+
runtimeLimits.maxDepth,
|
|
581
|
+
),
|
|
582
|
+
};
|
|
870
583
|
}
|
|
871
584
|
const requestedThinking = agent.thinking ?? parentThinking;
|
|
872
585
|
const effectiveModel = resolveEffectiveChildModelSettings(agent, parentModel);
|
|
@@ -883,21 +596,55 @@ export async function runSingleAgent(
|
|
|
883
596
|
agent.skills
|
|
884
597
|
? resolveAgentSkillArgs(defaultCwd, agent.skills)
|
|
885
598
|
: Promise.resolve({ args: [] });
|
|
599
|
+
const extensionNames =
|
|
600
|
+
agent.extensions && agent.extensions.length > 0
|
|
601
|
+
? agent.extensions
|
|
602
|
+
: undefined;
|
|
603
|
+
const resolvedExtensionsPromise: Promise<
|
|
604
|
+
{ resolvedPaths: string[] } | { error: string }
|
|
605
|
+
> = extensionNames
|
|
606
|
+
? resolveAgentExtensionPaths(defaultCwd, extensionNames)
|
|
607
|
+
: Promise.resolve({ resolvedPaths: [] });
|
|
886
608
|
const promptSetupPromise = beginPromptSetup(agent);
|
|
887
|
-
const resolvedSkills = await
|
|
888
|
-
|
|
889
|
-
|
|
609
|
+
const [resolvedSkills, resolvedExtensions, promptSetup] = await Promise.all([
|
|
610
|
+
resolvedSkillsPromise,
|
|
611
|
+
resolvedExtensionsPromise,
|
|
612
|
+
promptSetupPromise,
|
|
613
|
+
]);
|
|
614
|
+
const abortWithError = async (
|
|
615
|
+
error: string,
|
|
616
|
+
): Promise<RunSingleAgentResult> => {
|
|
890
617
|
await cleanupPromptSetupResult(promptSetup);
|
|
891
|
-
return
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
618
|
+
return {
|
|
619
|
+
kind: "completed",
|
|
620
|
+
result: createErrorResult(
|
|
621
|
+
agentName,
|
|
622
|
+
agent.source,
|
|
623
|
+
task,
|
|
624
|
+
error,
|
|
625
|
+
modelDisplay,
|
|
626
|
+
),
|
|
627
|
+
};
|
|
628
|
+
};
|
|
629
|
+
if ("error" in resolvedSkills) return abortWithError(resolvedSkills.error);
|
|
630
|
+
if ("error" in resolvedExtensions)
|
|
631
|
+
return abortWithError(resolvedExtensions.error);
|
|
632
|
+
if ("error" in promptSetup) {
|
|
633
|
+
return {
|
|
634
|
+
kind: "completed",
|
|
635
|
+
result: createErrorResult(
|
|
636
|
+
agentName,
|
|
637
|
+
agent.source,
|
|
638
|
+
task,
|
|
639
|
+
`Failed to write prompt: ${
|
|
640
|
+
promptSetup.error instanceof Error
|
|
641
|
+
? promptSetup.error.message
|
|
642
|
+
: String(promptSetup.error)
|
|
643
|
+
}`,
|
|
644
|
+
modelDisplay,
|
|
645
|
+
),
|
|
646
|
+
};
|
|
898
647
|
}
|
|
899
|
-
const promptSetup = await promptSetupPromise;
|
|
900
|
-
if ("error" in promptSetup) throw promptSetup.error;
|
|
901
648
|
const startedAt = Date.now();
|
|
902
649
|
const state: SubagentState = {
|
|
903
650
|
result: initRuntimeResult(agentName, agent.source, task, modelDisplay),
|
|
@@ -906,26 +653,33 @@ export async function runSingleAgent(
|
|
|
906
653
|
};
|
|
907
654
|
if (thinkingWarning) state.result.thinkingWarning = thinkingWarning;
|
|
908
655
|
const tmpPrompt = promptSetup.tmpPrompt;
|
|
656
|
+
const samplingEnv = buildSamplingEnv(agent);
|
|
909
657
|
try {
|
|
910
|
-
const args = buildPiArgs(
|
|
658
|
+
const args = buildPiArgs({
|
|
911
659
|
agent,
|
|
912
660
|
task,
|
|
913
661
|
effectiveModel,
|
|
914
662
|
thinking,
|
|
915
663
|
resolvedSkills,
|
|
916
664
|
tmpPrompt,
|
|
917
|
-
|
|
665
|
+
resolvedExtensionPaths:
|
|
666
|
+
agent.extensions !== undefined
|
|
667
|
+
? resolvedExtensions.resolvedPaths
|
|
668
|
+
: undefined,
|
|
669
|
+
samplingEnv,
|
|
670
|
+
});
|
|
918
671
|
const invocation = getPiInvocation(args);
|
|
919
672
|
const terminateOptions = {
|
|
920
673
|
tree: true,
|
|
921
674
|
platform: process.platform,
|
|
922
675
|
processTreeDetached: process.platform !== "win32",
|
|
923
676
|
};
|
|
677
|
+
const childEnv = buildChildEnv(samplingEnv);
|
|
924
678
|
const proc = spawn(invocation.command, invocation.args, {
|
|
925
679
|
cwd: defaultCwd,
|
|
926
680
|
shell: invocation.command === "pi" && process.platform === "win32",
|
|
927
681
|
stdio: ["ignore", "pipe", "pipe"],
|
|
928
|
-
env:
|
|
682
|
+
env: childEnv,
|
|
929
683
|
...getProcessTreeSpawnOptions(terminateOptions.tree),
|
|
930
684
|
});
|
|
931
685
|
const processDone = waitForSubagentProcess(proc);
|