@mystilleef/pi-subagent 0.8.0 → 0.9.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 +26 -5
- package/package.json +4 -4
- package/src/agent/agents.ts +3 -3
- package/src/child/child-events.ts +20 -20
- package/src/child/process.ts +100 -49
- package/src/child/termination.ts +4 -4
- package/src/env.d.ts +13 -0
- package/src/orchestration/run-registry.ts +1 -2
- package/src/orchestration/subagent-orchestrator.ts +12 -7
- package/src/progress/progress-state.ts +42 -39
- package/src/progress/progress.ts +11 -4
- package/src/progress/result-details.ts +55 -11
- package/src/shared/types.ts +16 -16
- package/src/shared/utils.ts +34 -6
- package/tsconfig.json +9 -11
package/README.md
CHANGED
|
@@ -130,8 +130,9 @@ delegation.
|
|
|
130
130
|
- `task`: task prompt for the child agent.
|
|
131
131
|
- `agentScope`: optional lookup scope, one of `user`, `project`, or
|
|
132
132
|
`both`.
|
|
133
|
-
- `debug`: optional flag that
|
|
134
|
-
|
|
133
|
+
- `debug`: optional flag that requests child diagnostic details. Full child
|
|
134
|
+
messages and raw internals require `PI_SUBAGENT_DEBUG_ENABLED=1` in the
|
|
135
|
+
host environment.
|
|
135
136
|
|
|
136
137
|
## Security
|
|
137
138
|
|
|
@@ -143,6 +144,12 @@ executable automation.
|
|
|
143
144
|
|
|
144
145
|
- Review project-local agents before running them.
|
|
145
146
|
- Avoid delegating secrets unless the agent and tools need them.
|
|
147
|
+
- Treat child-agent prompts, tool arguments, stderr, and debug transcripts as
|
|
148
|
+
potentially sensitive.
|
|
149
|
+
- Enable debug details only for trusted investigations. `debug: true` or
|
|
150
|
+
`/run --debug` can expose child conversation transcripts, termination
|
|
151
|
+
internals, and stderr only when the host explicitly sets
|
|
152
|
+
`PI_SUBAGENT_DEBUG_ENABLED=1`.
|
|
146
153
|
- Prefer trusted repositories for shared agent definitions.
|
|
147
154
|
- Remember that child agents can call their configured tools.
|
|
148
155
|
|
|
@@ -150,12 +157,26 @@ executable automation.
|
|
|
150
157
|
|
|
151
158
|
**Environment variables:**
|
|
152
159
|
|
|
153
|
-
- `PI_SUBAGENT_DEPTH`: nested subagent depth
|
|
154
|
-
|
|
160
|
+
- `PI_SUBAGENT_DEPTH`: current nested subagent depth counter set internally
|
|
161
|
+
for child processes.
|
|
162
|
+
- `PI_SUBAGENT_MAX_DEPTH`: max nested subagent depth. Default: `3`. Values
|
|
163
|
+
above `10` clamp to the internal ceiling `10`; deeper nesting increases
|
|
164
|
+
cost, latency, and runaway delegation risk.
|
|
165
|
+
- `PI_SUBAGENT_AGENT_END_GRACE_MS`: child process grace period after
|
|
166
|
+
`agent_end` before forced termination. Default: `250`.
|
|
167
|
+
- `PI_SUBAGENT_MAX_STDERR_BYTES`: max captured child stderr bytes. Default:
|
|
168
|
+
`10000`.
|
|
155
169
|
- `PI_SUBAGENT_MAX_OUTPUT_BYTES`: max returned output bytes. Default:
|
|
156
170
|
`50000`.
|
|
157
171
|
- `PI_SUBAGENT_MAX_OUTPUT_LINES`: max returned output lines. Default:
|
|
158
172
|
`500`.
|
|
173
|
+
- `PI_SUBAGENT_DEBUG_ENABLED`: debug detail authorization. Set to `1` to
|
|
174
|
+
allow `debug: true` or `/run --debug` to include sanitized child messages,
|
|
175
|
+
termination internals, and stderr; unset values keep non-debug detail
|
|
176
|
+
behavior.
|
|
177
|
+
|
|
178
|
+
Limit variables parse as positive integers. Empty, zero, negative, decimal,
|
|
179
|
+
`Infinity`, and non-numeric values fall back to defaults.
|
|
159
180
|
|
|
160
181
|
## Troubleshooting
|
|
161
182
|
|
|
@@ -173,7 +194,7 @@ executable automation.
|
|
|
173
194
|
|
|
174
195
|
**Nested subagent blocked:**
|
|
175
196
|
|
|
176
|
-
- Nested delegation hits the `
|
|
197
|
+
- Nested delegation hits the `PI_SUBAGENT_MAX_DEPTH` safety limit.
|
|
177
198
|
- Run the child task directly from the parent session instead.
|
|
178
199
|
|
|
179
200
|
**Truncated output:**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mystilleef/pi-subagent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Pi subagent for the SPAE Framework",
|
|
5
5
|
"author": "Lateef Alabi-Oki <mystilleef@gmail.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"typecheck": "tsc --noEmit",
|
|
51
51
|
"lint": "biome check --write --unsafe --error-on-warnings .",
|
|
52
52
|
"migrate": "biome migrate --write",
|
|
53
|
-
"coverage": "bun test --
|
|
53
|
+
"coverage": "bun test --parallel --parallel-delay=0",
|
|
54
54
|
"check": "bun lint && bun typecheck",
|
|
55
55
|
"verify": "bun migrate && bun check && bun coverage",
|
|
56
56
|
"pack:smoke": "bun scripts/pack-smoke.ts",
|
|
@@ -70,8 +70,8 @@
|
|
|
70
70
|
"@earendil-works/pi-coding-agent": "^0.78.1",
|
|
71
71
|
"@earendil-works/pi-tui": "^0.78.1",
|
|
72
72
|
"@types/bun": "^1.3.14",
|
|
73
|
-
"@types/node": "^25.9.
|
|
74
|
-
"typebox": "^1.2.
|
|
73
|
+
"@types/node": "^25.9.2",
|
|
74
|
+
"typebox": "^1.2.2",
|
|
75
75
|
"typescript": "^6.0.3"
|
|
76
76
|
}
|
|
77
77
|
}
|
package/src/agent/agents.ts
CHANGED
|
@@ -20,9 +20,9 @@ export type ThinkingLevel = (typeof THINKING_LEVELS)[number];
|
|
|
20
20
|
export interface AgentConfig {
|
|
21
21
|
name: string;
|
|
22
22
|
description: string;
|
|
23
|
-
tools?: string[];
|
|
24
|
-
skills?: string[];
|
|
25
|
-
thinking?: ThinkingLevel;
|
|
23
|
+
tools?: string[] | undefined;
|
|
24
|
+
skills?: string[] | undefined;
|
|
25
|
+
thinking?: ThinkingLevel | undefined;
|
|
26
26
|
systemPrompt: string;
|
|
27
27
|
source: AgentSource;
|
|
28
28
|
filePath: string;
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { makeToolPreview } from "../output/normalize.js";
|
|
2
2
|
import type { ToolActivity } from "../shared/types.js";
|
|
3
3
|
|
|
4
|
-
// Extracts results[0] from details; null-safe for malformed input.
|
|
5
4
|
function tryFirstResult(details: unknown): Record<string, unknown> | null {
|
|
6
5
|
try {
|
|
7
6
|
if (typeof details !== "object" || details === null) return null;
|
|
8
|
-
const results = (details as Record<string, unknown>)
|
|
7
|
+
const results = (details as Record<string, unknown>)["results"];
|
|
9
8
|
if (!Array.isArray(results) || results.length === 0) return null;
|
|
10
9
|
const nested = results[0];
|
|
11
10
|
if (typeof nested !== "object" || nested === null) return null;
|
|
@@ -15,7 +14,6 @@ function tryFirstResult(details: unknown): Record<string, unknown> | null {
|
|
|
15
14
|
}
|
|
16
15
|
}
|
|
17
16
|
|
|
18
|
-
// Malformed details (null result) falls back to { toolName }.
|
|
19
17
|
function parseToolActivity(
|
|
20
18
|
toolName: string,
|
|
21
19
|
partialResult: { content?: unknown; details?: unknown },
|
|
@@ -24,23 +22,25 @@ function parseToolActivity(
|
|
|
24
22
|
if (!nestedRecord) return { toolName, inputSummary: toolName };
|
|
25
23
|
const isSubagent = toolName === "subagent";
|
|
26
24
|
const activity: ToolActivity = { toolName };
|
|
27
|
-
const agent =
|
|
25
|
+
const agent =
|
|
26
|
+
typeof nestedRecord["agent"] === "string" && nestedRecord["agent"];
|
|
28
27
|
activity.inputSummary =
|
|
29
28
|
isSubagent && agent ? makeToolPreview(toolName, nestedRecord) : toolName;
|
|
30
29
|
if (
|
|
31
|
-
typeof nestedRecord
|
|
32
|
-
nestedRecord
|
|
30
|
+
typeof nestedRecord["instanceName"] === "string" &&
|
|
31
|
+
nestedRecord["instanceName"]
|
|
33
32
|
) {
|
|
34
|
-
activity.instanceName = nestedRecord
|
|
33
|
+
activity.instanceName = nestedRecord["instanceName"] as string;
|
|
35
34
|
}
|
|
36
|
-
const progress = nestedRecord
|
|
35
|
+
const progress = nestedRecord["progress"];
|
|
37
36
|
if (typeof progress === "object" && progress !== null) {
|
|
38
|
-
const activeToolActivity = (progress as Record<string, unknown>)
|
|
39
|
-
|
|
37
|
+
const activeToolActivity = (progress as Record<string, unknown>)[
|
|
38
|
+
"activeToolActivity"
|
|
39
|
+
];
|
|
40
40
|
if (
|
|
41
41
|
typeof activeToolActivity === "object" &&
|
|
42
42
|
activeToolActivity !== null &&
|
|
43
|
-
typeof (activeToolActivity as Record<string, unknown>)
|
|
43
|
+
typeof (activeToolActivity as Record<string, unknown>)["toolName"] ===
|
|
44
44
|
"string"
|
|
45
45
|
) {
|
|
46
46
|
const childActivity = activeToolActivity as ToolActivity;
|
|
@@ -96,21 +96,21 @@ export function parseChildEventLine(line: string): ChildEventParseResult {
|
|
|
96
96
|
typeof event === "object" &&
|
|
97
97
|
event !== null &&
|
|
98
98
|
"type" in event &&
|
|
99
|
-
typeof (event as Record<string, unknown>)
|
|
100
|
-
KNOWN_TYPES.has((event as Record<string, unknown>)
|
|
99
|
+
typeof (event as Record<string, unknown>)["type"] === "string" &&
|
|
100
|
+
KNOWN_TYPES.has((event as Record<string, unknown>)["type"] as string)
|
|
101
101
|
) {
|
|
102
102
|
const record = event as Record<string, unknown>;
|
|
103
|
-
if (record
|
|
103
|
+
if (record["type"] === TOOL_EXECUTION_UPDATE_EVENT) {
|
|
104
104
|
if (
|
|
105
|
-
typeof record
|
|
106
|
-
typeof record
|
|
107
|
-
record
|
|
105
|
+
typeof record["toolName"] !== "string" ||
|
|
106
|
+
typeof record["partialResult"] !== "object" ||
|
|
107
|
+
record["partialResult"] === null
|
|
108
108
|
) {
|
|
109
109
|
return { kind: "unknown", event };
|
|
110
110
|
}
|
|
111
|
-
record
|
|
112
|
-
record
|
|
113
|
-
record
|
|
111
|
+
record["toolActivity"] = parseToolActivity(
|
|
112
|
+
record["toolName"] as string,
|
|
113
|
+
record["partialResult"] as { content?: unknown; details?: unknown },
|
|
114
114
|
);
|
|
115
115
|
}
|
|
116
116
|
return { kind: "known", event: event as ChildKnownEvent };
|
package/src/child/process.ts
CHANGED
|
@@ -32,12 +32,14 @@ import {
|
|
|
32
32
|
detectMessageError,
|
|
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,
|
|
@@ -48,8 +50,6 @@ import {
|
|
|
48
50
|
terminateChildProcess,
|
|
49
51
|
} from "./termination.js";
|
|
50
52
|
|
|
51
|
-
const MAX_STDERR_BYTES = 10_000;
|
|
52
|
-
const AGENT_END_GRACE_MS = 250;
|
|
53
53
|
export function resolveThinkingLevel(
|
|
54
54
|
requested: ThinkingLevel,
|
|
55
55
|
provider: string,
|
|
@@ -73,15 +73,17 @@ export function resolveThinkingLevel(
|
|
|
73
73
|
return { level: clamped, warning: mkWarning(clamped) };
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
const MAX_SUBAGENT_DEPTH = 3;
|
|
77
76
|
export const TOOL_RESULT_FAILED_MESSAGE = "Subagent tool result failed.";
|
|
78
77
|
|
|
78
|
+
type RuntimeLimits = ReturnType<typeof getSubagentRuntimeLimits>;
|
|
79
79
|
type RuntimeResult = SingleResult & { messages: Message[] };
|
|
80
80
|
|
|
81
81
|
export class SubagentAbortError extends Error {
|
|
82
|
-
|
|
82
|
+
readonly result: SingleResult;
|
|
83
|
+
constructor(result: SingleResult) {
|
|
83
84
|
super("Subagent was aborted");
|
|
84
85
|
this.name = "SubagentAbortError";
|
|
86
|
+
this.result = result;
|
|
85
87
|
}
|
|
86
88
|
}
|
|
87
89
|
|
|
@@ -91,6 +93,7 @@ type PromptSetupResult = { tmpPrompt: TempPrompt | null } | { error: unknown };
|
|
|
91
93
|
|
|
92
94
|
interface SubagentState {
|
|
93
95
|
result: RuntimeResult;
|
|
96
|
+
runtimeLimits: RuntimeLimits;
|
|
94
97
|
spawnError?: Error;
|
|
95
98
|
wasAborted: boolean;
|
|
96
99
|
agentEndGraceTimer?: ReturnType<typeof setTimeout>;
|
|
@@ -99,25 +102,41 @@ interface SubagentState {
|
|
|
99
102
|
|
|
100
103
|
function appendWithByteLimit(
|
|
101
104
|
current: string,
|
|
102
|
-
data: string,
|
|
105
|
+
data: string | Buffer,
|
|
103
106
|
max: number,
|
|
104
107
|
): string {
|
|
105
|
-
|
|
106
|
-
|
|
108
|
+
const currentBytes = Buffer.from(current, "utf-8");
|
|
109
|
+
if (currentBytes.length >= max) return current;
|
|
110
|
+
const incomingBytes = Buffer.isBuffer(data)
|
|
111
|
+
? data
|
|
112
|
+
: Buffer.from(data, "utf-8");
|
|
113
|
+
const combined = Buffer.concat([currentBytes, incomingBytes]);
|
|
114
|
+
if (combined.length <= max) return combined.toString("utf-8");
|
|
115
|
+
return truncateValidUtf8(combined, max);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function truncateValidUtf8(buffer: Buffer, max: number): string {
|
|
119
|
+
let end = Math.min(max, buffer.length);
|
|
120
|
+
while (end > 0) {
|
|
121
|
+
const candidate = buffer.subarray(0, end).toString("utf-8");
|
|
122
|
+
if (!candidate.endsWith("�")) return candidate;
|
|
123
|
+
end -= 1;
|
|
124
|
+
}
|
|
125
|
+
return "";
|
|
107
126
|
}
|
|
108
127
|
|
|
109
128
|
/**
|
|
110
|
-
* Attempts to resolve the context window token limit for a given message's model.
|
|
111
129
|
* Rationale: Subagent usage reporting needs context window awareness to provide
|
|
112
130
|
* meaningful "context full" indicators to the parent.
|
|
113
131
|
*/
|
|
114
132
|
function resolveContextWindowTokens(msg: Message): number | undefined {
|
|
115
133
|
const m = msg as unknown as Record<string, unknown>;
|
|
116
|
-
if (typeof m
|
|
134
|
+
if (typeof m["provider"] !== "string" || typeof m["model"] !== "string")
|
|
135
|
+
return;
|
|
117
136
|
try {
|
|
118
137
|
const contextWindow = getModel(
|
|
119
|
-
m
|
|
120
|
-
m
|
|
138
|
+
m["provider"] as never,
|
|
139
|
+
m["model"] as never,
|
|
121
140
|
)?.contextWindow;
|
|
122
141
|
return Number.isFinite(contextWindow) && contextWindow > 0
|
|
123
142
|
? contextWindow
|
|
@@ -134,11 +153,6 @@ function getAbortReason(signal: AbortSignal): string {
|
|
|
134
153
|
return "abort";
|
|
135
154
|
}
|
|
136
155
|
|
|
137
|
-
/**
|
|
138
|
-
* Verifies if the agent produced any textual output or final response.
|
|
139
|
-
* Precondition: Called after process exit to distinguish between clean completion
|
|
140
|
-
* and silent failures where the process exited 0 but did nothing.
|
|
141
|
-
*/
|
|
142
156
|
function hasCompletedAgentOutput(result: RuntimeResult): boolean {
|
|
143
157
|
if (result.finalOutput.trim()) return true;
|
|
144
158
|
return result.messages.some(
|
|
@@ -151,7 +165,6 @@ function hasCompletedAgentOutput(result: RuntimeResult): boolean {
|
|
|
151
165
|
}
|
|
152
166
|
|
|
153
167
|
/**
|
|
154
|
-
* Determines the exit code for processes terminated via the agent_end timeout.
|
|
155
168
|
* Rationale: `pi` processes in JSON mode might hang after finishing their task;
|
|
156
169
|
* we force-kill them after a grace period and treat it as success (0) if they
|
|
157
170
|
* actually produced output.
|
|
@@ -168,7 +181,6 @@ function getAgentEndTimeoutExitCode(
|
|
|
168
181
|
}
|
|
169
182
|
|
|
170
183
|
/**
|
|
171
|
-
* Orchestrates the cleanup and exit code capture of a child process.
|
|
172
184
|
* Safety: Implements a dual-timer strategy (idle and hard) to ensure streams
|
|
173
185
|
* are destroyed and promises settled even if the process or its pipes hang.
|
|
174
186
|
*/
|
|
@@ -278,8 +290,9 @@ function accumulateUsage(result: RuntimeResult, msg: Message): void {
|
|
|
278
290
|
result.usage.cacheWrite += usage.cacheWrite || 0;
|
|
279
291
|
result.usage.cost += usage.cost?.total || 0;
|
|
280
292
|
result.usage.contextTokens = usage.totalTokens || 0;
|
|
281
|
-
|
|
282
|
-
|
|
293
|
+
const ctxWindowTokens = resolveContextWindowTokens(msg);
|
|
294
|
+
if (ctxWindowTokens !== undefined)
|
|
295
|
+
result.usage.contextWindowTokens = ctxWindowTokens;
|
|
283
296
|
}
|
|
284
297
|
|
|
285
298
|
function addMessageToResult(result: RuntimeResult, msg: Message): void {
|
|
@@ -288,7 +301,7 @@ function addMessageToResult(result: RuntimeResult, msg: Message): void {
|
|
|
288
301
|
if (msg.role === "toolResult" && msg.isError) {
|
|
289
302
|
result.errorMessage ||= TOOL_RESULT_FAILED_MESSAGE;
|
|
290
303
|
} else if (result.errorMessage === TOOL_RESULT_FAILED_MESSAGE) {
|
|
291
|
-
result.errorMessage
|
|
304
|
+
delete result.errorMessage;
|
|
292
305
|
}
|
|
293
306
|
if (msg.role === "assistant") {
|
|
294
307
|
accumulateUsage(result, msg);
|
|
@@ -336,13 +349,14 @@ function errorForDepthLimit(
|
|
|
336
349
|
source: "user" | "project" | "unknown",
|
|
337
350
|
task: string,
|
|
338
351
|
depth: number,
|
|
352
|
+
maxDepth: number,
|
|
339
353
|
model?: string,
|
|
340
354
|
): SingleResult {
|
|
341
355
|
return createErrorResult(
|
|
342
356
|
agentName,
|
|
343
357
|
source,
|
|
344
358
|
task,
|
|
345
|
-
`Subagent nesting limit reached (depth ${depth}/${
|
|
359
|
+
`Subagent nesting limit reached (depth ${depth}/${maxDepth}).`,
|
|
346
360
|
model,
|
|
347
361
|
);
|
|
348
362
|
}
|
|
@@ -387,12 +401,6 @@ function findRecentMessagesAnchor(messages: Message[]): number {
|
|
|
387
401
|
return -1;
|
|
388
402
|
}
|
|
389
403
|
|
|
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
404
|
function deriveStreamingProgress(messages: Message[]): StreamingProgress {
|
|
397
405
|
const toolCalls: { id: string; preview: string }[] = [];
|
|
398
406
|
let lastToolPreview: string | undefined;
|
|
@@ -410,18 +418,15 @@ function deriveStreamingProgress(messages: Message[]): StreamingProgress {
|
|
|
410
418
|
activeToolActivity = { toolName: part.name, inputSummary: preview };
|
|
411
419
|
}
|
|
412
420
|
}
|
|
421
|
+
const activityText = renderToolActivity(activeToolActivity);
|
|
413
422
|
return {
|
|
414
423
|
activeToolActivity,
|
|
415
|
-
activityText
|
|
424
|
+
activityText,
|
|
416
425
|
toolCalls,
|
|
417
426
|
lastToolPreview,
|
|
418
427
|
};
|
|
419
428
|
}
|
|
420
429
|
|
|
421
|
-
/**
|
|
422
|
-
* Prevents leaking secrets in the CLI progress display.
|
|
423
|
-
* Redacts values if the preview contains sensitive keywords.
|
|
424
|
-
*/
|
|
425
430
|
function sanitizeProgressPreview(preview: string, toolName: string): string {
|
|
426
431
|
return SENSITIVE_PATTERN.test(preview) ? toolName : preview;
|
|
427
432
|
}
|
|
@@ -447,7 +452,9 @@ export function makeEmitUpdate(
|
|
|
447
452
|
// so the parent retains nested context until newer activity arrives
|
|
448
453
|
if (options?.toolResultCompleted && result.progress?.activeToolActivity) {
|
|
449
454
|
progress.activeToolActivity = result.progress.activeToolActivity;
|
|
450
|
-
|
|
455
|
+
const renderedText = renderToolActivity(progress.activeToolActivity);
|
|
456
|
+
if (renderedText !== undefined) progress.activityText = renderedText;
|
|
457
|
+
else delete progress.activityText;
|
|
451
458
|
}
|
|
452
459
|
// Handle parsed tool activity from child events
|
|
453
460
|
// Merge with parent activity if this is a nested update
|
|
@@ -474,7 +481,10 @@ export function makeEmitUpdate(
|
|
|
474
481
|
} else {
|
|
475
482
|
progress.activeToolActivity = options.toolActivity;
|
|
476
483
|
}
|
|
477
|
-
|
|
484
|
+
const renderedActivity = renderToolActivity(progress.activeToolActivity);
|
|
485
|
+
if (renderedActivity !== undefined)
|
|
486
|
+
progress.activityText = renderedActivity;
|
|
487
|
+
else delete progress.activityText;
|
|
478
488
|
}
|
|
479
489
|
if (options?.toolResultCompleted) {
|
|
480
490
|
progress.toolResultCompleted = true;
|
|
@@ -515,7 +525,7 @@ function makeRequestTerminator(
|
|
|
515
525
|
function clearGraceTimer(state: SubagentState): void {
|
|
516
526
|
if (!state.agentEndGraceTimer) return;
|
|
517
527
|
clearTimeout(state.agentEndGraceTimer);
|
|
518
|
-
state.agentEndGraceTimer
|
|
528
|
+
delete state.agentEndGraceTimer;
|
|
519
529
|
}
|
|
520
530
|
|
|
521
531
|
function handleMessageEvent(
|
|
@@ -563,12 +573,25 @@ function handleAgentEndEvent(
|
|
|
563
573
|
}
|
|
564
574
|
if (state.agentEndGraceTimer || state.terminationPromise) return;
|
|
565
575
|
state.agentEndGraceTimer = setTimeout(() => {
|
|
566
|
-
state.agentEndGraceTimer
|
|
576
|
+
delete state.agentEndGraceTimer;
|
|
567
577
|
void requestTermination("agent_end_timeout");
|
|
568
|
-
},
|
|
578
|
+
}, state.runtimeLimits.agentEndGraceMs);
|
|
569
579
|
state.agentEndGraceTimer.unref?.();
|
|
570
580
|
}
|
|
571
581
|
|
|
582
|
+
function formatUnknownEventDiagnostic(
|
|
583
|
+
line: string,
|
|
584
|
+
parseResult: Exclude<ChildEventParseResult, { kind: "known" }>,
|
|
585
|
+
): string {
|
|
586
|
+
if (parseResult.kind === "invalid" && !line.trim()) {
|
|
587
|
+
return "[pi-subagent:unknown-event] blank";
|
|
588
|
+
}
|
|
589
|
+
if (parseResult.kind === "invalid") {
|
|
590
|
+
return `[pi-subagent:unknown-event] malformed: ${line}`;
|
|
591
|
+
}
|
|
592
|
+
return `[pi-subagent:unknown-event] unknown: ${JSON.stringify(parseResult.event)}`;
|
|
593
|
+
}
|
|
594
|
+
|
|
572
595
|
function processEventLine(
|
|
573
596
|
line: string,
|
|
574
597
|
state: SubagentState,
|
|
@@ -577,9 +600,17 @@ function processEventLine(
|
|
|
577
600
|
toolResultCompleted?: boolean;
|
|
578
601
|
}) => void,
|
|
579
602
|
requestTermination: (reason: string) => Promise<unknown>,
|
|
603
|
+
debugEventDiagnostics: boolean,
|
|
580
604
|
): void {
|
|
581
605
|
const parseResult = parseChildEventLine(line);
|
|
582
|
-
if (parseResult.kind !== "known")
|
|
606
|
+
if (parseResult.kind !== "known") {
|
|
607
|
+
if (debugEventDiagnostics) {
|
|
608
|
+
process.stderr.write(
|
|
609
|
+
`${formatUnknownEventDiagnostic(line, parseResult)}\n`,
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
583
614
|
const { event } = parseResult;
|
|
584
615
|
handleMessageEvent(event, state, emitUpdate);
|
|
585
616
|
handleToolExecutionUpdateEvent(event, emitUpdate);
|
|
@@ -639,28 +670,35 @@ function setupChildProcess(
|
|
|
639
670
|
toolResultCompleted?: boolean;
|
|
640
671
|
}) => void,
|
|
641
672
|
requestTermination: (reason: string) => Promise<unknown>,
|
|
673
|
+
debugEventDiagnostics: boolean,
|
|
642
674
|
): void {
|
|
643
675
|
proc.once("error", (error) => {
|
|
644
676
|
state.spawnError = error;
|
|
645
677
|
state.result.stderr = appendWithByteLimit(
|
|
646
678
|
state.result.stderr,
|
|
647
679
|
error.message,
|
|
648
|
-
|
|
680
|
+
state.runtimeLimits.maxStderrBytes,
|
|
649
681
|
);
|
|
650
682
|
});
|
|
651
683
|
if (proc.stdout) {
|
|
652
684
|
readline
|
|
653
685
|
.createInterface({ input: proc.stdout })
|
|
654
686
|
.on("line", (line) =>
|
|
655
|
-
processEventLine(
|
|
687
|
+
processEventLine(
|
|
688
|
+
line,
|
|
689
|
+
state,
|
|
690
|
+
emitUpdate,
|
|
691
|
+
requestTermination,
|
|
692
|
+
debugEventDiagnostics,
|
|
693
|
+
),
|
|
656
694
|
);
|
|
657
695
|
}
|
|
658
696
|
if (proc.stderr) {
|
|
659
|
-
proc.stderr.on("data", (data) => {
|
|
697
|
+
proc.stderr.on("data", (data: Buffer) => {
|
|
660
698
|
state.result.stderr = appendWithByteLimit(
|
|
661
699
|
state.result.stderr,
|
|
662
|
-
data
|
|
663
|
-
|
|
700
|
+
data,
|
|
701
|
+
state.runtimeLimits.maxStderrBytes,
|
|
664
702
|
);
|
|
665
703
|
});
|
|
666
704
|
}
|
|
@@ -689,8 +727,6 @@ async function finalizeResult(
|
|
|
689
727
|
}
|
|
690
728
|
|
|
691
729
|
/**
|
|
692
|
-
* Executes a single subagent task.
|
|
693
|
-
*
|
|
694
730
|
* Rationale: Subagents run in isolated child processes to protect the parent's
|
|
695
731
|
* context window and allow specialized system prompts/tools without polluting
|
|
696
732
|
* the main conversation.
|
|
@@ -716,12 +752,20 @@ export async function runSingleAgent(
|
|
|
716
752
|
) => SubagentDetails,
|
|
717
753
|
parentModel: { provider: string; id: string } | undefined,
|
|
718
754
|
parentThinking: ThinkingLevel,
|
|
755
|
+
debugEventDiagnostics = false,
|
|
719
756
|
): Promise<SingleResult> {
|
|
720
757
|
const agent = agents.find((a) => a.name === agentName);
|
|
721
758
|
if (!agent) return errorForUnknownAgent(agentName, agents, task);
|
|
759
|
+
const runtimeLimits = getSubagentRuntimeLimits();
|
|
722
760
|
const depth = getSubagentDepth();
|
|
723
|
-
if (depth >=
|
|
724
|
-
return errorForDepthLimit(
|
|
761
|
+
if (depth >= runtimeLimits.maxDepth) {
|
|
762
|
+
return errorForDepthLimit(
|
|
763
|
+
agentName,
|
|
764
|
+
agent.source,
|
|
765
|
+
task,
|
|
766
|
+
depth,
|
|
767
|
+
runtimeLimits.maxDepth,
|
|
768
|
+
);
|
|
725
769
|
}
|
|
726
770
|
const requestedThinking = agent.thinking ?? parentThinking;
|
|
727
771
|
const { level: thinking, warning: thinkingWarning } = parentModel
|
|
@@ -754,6 +798,7 @@ export async function runSingleAgent(
|
|
|
754
798
|
const startedAt = Date.now();
|
|
755
799
|
const state: SubagentState = {
|
|
756
800
|
result: initRuntimeResult(agentName, agent.source, task, modelDisplay),
|
|
801
|
+
runtimeLimits,
|
|
757
802
|
wasAborted: false,
|
|
758
803
|
};
|
|
759
804
|
if (thinkingWarning) state.result.thinkingWarning = thinkingWarning;
|
|
@@ -787,7 +832,13 @@ export async function runSingleAgent(
|
|
|
787
832
|
terminateOptions,
|
|
788
833
|
state,
|
|
789
834
|
);
|
|
790
|
-
setupChildProcess(
|
|
835
|
+
setupChildProcess(
|
|
836
|
+
proc,
|
|
837
|
+
state,
|
|
838
|
+
emitUpdate,
|
|
839
|
+
requestTermination,
|
|
840
|
+
debugEventDiagnostics,
|
|
841
|
+
);
|
|
791
842
|
const onAbort = setupAbortHandler(
|
|
792
843
|
signal,
|
|
793
844
|
state,
|
package/src/child/termination.ts
CHANGED
|
@@ -4,12 +4,12 @@ export type TerminationSignal = "SIGTERM" | "SIGKILL";
|
|
|
4
4
|
|
|
5
5
|
export type TerminationMetadata = {
|
|
6
6
|
cancelRequestedAt: number;
|
|
7
|
-
cancelReason?: string;
|
|
8
|
-
terminationSignal?: TerminationSignal;
|
|
7
|
+
cancelReason?: string | undefined;
|
|
8
|
+
terminationSignal?: TerminationSignal | undefined;
|
|
9
9
|
escalated: boolean;
|
|
10
10
|
processTreeKilled: boolean;
|
|
11
11
|
target: "direct" | "tree";
|
|
12
|
-
fallbackCause?: string;
|
|
12
|
+
fallbackCause?: string | undefined;
|
|
13
13
|
};
|
|
14
14
|
|
|
15
15
|
type TimerHandle = unknown;
|
|
@@ -64,7 +64,7 @@ function settleState(state: TerminationState): void {
|
|
|
64
64
|
if (state.settled) return;
|
|
65
65
|
state.settled = true;
|
|
66
66
|
if (state.timer) state.clearTimeout(state.timer);
|
|
67
|
-
state.timer
|
|
67
|
+
delete state.timer;
|
|
68
68
|
state.resolve(state.metadata);
|
|
69
69
|
}
|
|
70
70
|
|
package/src/env.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
declare namespace NodeJS {
|
|
2
|
+
interface ProcessEnv {
|
|
3
|
+
PATH?: string;
|
|
4
|
+
PI_CODING_AGENT_DIR?: string;
|
|
5
|
+
PI_SUBAGENT_DEPTH?: string;
|
|
6
|
+
PI_SUBAGENT_MAX_DEPTH?: string;
|
|
7
|
+
PI_SUBAGENT_MAX_OUTPUT_BYTES?: string;
|
|
8
|
+
PI_SUBAGENT_MAX_OUTPUT_LINES?: string;
|
|
9
|
+
PI_SUBAGENT_AGENT_END_GRACE_MS?: string;
|
|
10
|
+
PI_SUBAGENT_MAX_STDERR_BYTES?: string;
|
|
11
|
+
PI_SUBAGENT_DEBUG_ENABLED?: string;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -48,7 +48,6 @@ export function cancelAllRunJobs(reason = "Cancelled"): number {
|
|
|
48
48
|
return count;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
export function
|
|
51
|
+
export function resetRunRegistry(): void {
|
|
52
52
|
jobs.clear();
|
|
53
53
|
}
|
|
54
|
-
export const resetRunRegistry = clearRunJobsForTests;
|
|
@@ -77,6 +77,10 @@ type DetailsBuilder = (
|
|
|
77
77
|
options?: DetailsOptions,
|
|
78
78
|
) => SubagentDetails;
|
|
79
79
|
|
|
80
|
+
function isDebugDetailsAuthorized(debugRequested: boolean): boolean {
|
|
81
|
+
return debugRequested && process.env.PI_SUBAGENT_DEBUG_ENABLED === "1";
|
|
82
|
+
}
|
|
83
|
+
|
|
80
84
|
interface LifecycleContext {
|
|
81
85
|
pi: ExtensionAPI;
|
|
82
86
|
ctx: ExtensionContext;
|
|
@@ -90,7 +94,7 @@ interface LifecycleContext {
|
|
|
90
94
|
task: string;
|
|
91
95
|
parentModel: { provider: string; id: string } | undefined;
|
|
92
96
|
parentThinking: ThinkingLevel;
|
|
93
|
-
hostOnUpdate?: AgentToolUpdateCallback<SubagentDetails
|
|
97
|
+
hostOnUpdate?: AgentToolUpdateCallback<SubagentDetails> | undefined;
|
|
94
98
|
}
|
|
95
99
|
|
|
96
100
|
function createDetailsBuilder(
|
|
@@ -123,7 +127,7 @@ function sanitizeResultDetails(
|
|
|
123
127
|
usage: { ...usageBase },
|
|
124
128
|
};
|
|
125
129
|
if (contextWindowTokens !== undefined) {
|
|
126
|
-
(sanitized
|
|
130
|
+
(sanitized["usage"] as Record<string, unknown>)["contextWindowTokens"] =
|
|
127
131
|
contextWindowTokens;
|
|
128
132
|
}
|
|
129
133
|
if (progress !== undefined) {
|
|
@@ -134,7 +138,7 @@ function sanitizeResultDetails(
|
|
|
134
138
|
toolResultCompleted,
|
|
135
139
|
...progBase
|
|
136
140
|
} = progress;
|
|
137
|
-
sanitized
|
|
141
|
+
sanitized["progress"] = {
|
|
138
142
|
toolCalls: progBase.toolCalls.map((tc) => ({
|
|
139
143
|
id: tc.id,
|
|
140
144
|
preview: tc.preview,
|
|
@@ -146,7 +150,7 @@ function sanitizeResultDetails(
|
|
|
146
150
|
};
|
|
147
151
|
}
|
|
148
152
|
if (includeMessages) {
|
|
149
|
-
sanitized
|
|
153
|
+
sanitized["messages"] = options?.recentMessages
|
|
150
154
|
? [...options.recentMessages]
|
|
151
155
|
: messages !== undefined
|
|
152
156
|
? [...messages]
|
|
@@ -154,7 +158,7 @@ function sanitizeResultDetails(
|
|
|
154
158
|
if (includeDebugMessages && termination !== undefined) {
|
|
155
159
|
const { cancelReason, terminationSignal, fallbackCause, ...termBase } =
|
|
156
160
|
termination;
|
|
157
|
-
sanitized
|
|
161
|
+
sanitized["termination"] = {
|
|
158
162
|
...termBase,
|
|
159
163
|
...(cancelReason !== undefined && { cancelReason }),
|
|
160
164
|
...(terminationSignal !== undefined && { terminationSignal }),
|
|
@@ -318,6 +322,7 @@ async function runSubagentLifecycle(
|
|
|
318
322
|
lc.makeDetails,
|
|
319
323
|
lc.parentModel,
|
|
320
324
|
lc.parentThinking,
|
|
325
|
+
lc.debug,
|
|
321
326
|
);
|
|
322
327
|
return finishLifecycleResult(lc, result);
|
|
323
328
|
} catch (error) {
|
|
@@ -358,7 +363,7 @@ type PrepareSubagentJobResult =
|
|
|
358
363
|
lc: LifecycleContext;
|
|
359
364
|
instanceName: string;
|
|
360
365
|
requestProgressRender: () => void;
|
|
361
|
-
hostOnUpdate?: AgentToolUpdateCallback<SubagentDetails
|
|
366
|
+
hostOnUpdate?: AgentToolUpdateCallback<SubagentDetails> | undefined;
|
|
362
367
|
}
|
|
363
368
|
| { kind: "not_found"; makeDetails: DetailsBuilder }
|
|
364
369
|
| { kind: "cancelled"; makeDetails: DetailsBuilder }
|
|
@@ -409,7 +414,7 @@ async function prepareSubagentJob(
|
|
|
409
414
|
const agentScope: AgentScope = params.agentScope ?? "both";
|
|
410
415
|
const discovery = await getCachedAgentDiscovery(ctx.cwd, agentScope);
|
|
411
416
|
const agents = discovery.agents;
|
|
412
|
-
const debug = params.debug === true;
|
|
417
|
+
const debug = isDebugDetailsAuthorized(params.debug === true);
|
|
413
418
|
const makeDetails = createDetailsBuilder(
|
|
414
419
|
agentScope,
|
|
415
420
|
discovery.projectAgentsDir,
|
|
@@ -44,21 +44,21 @@ export const STATUS_BG: Record<ProgressStatus, ThemeBg> = {
|
|
|
44
44
|
export interface SubagentProgressState {
|
|
45
45
|
requestId: string;
|
|
46
46
|
agent: string;
|
|
47
|
-
instanceName?: string;
|
|
47
|
+
instanceName?: string | undefined;
|
|
48
48
|
taskPreview: string;
|
|
49
49
|
status: ProgressStatus;
|
|
50
50
|
startTime: number;
|
|
51
|
-
durationMs?: number;
|
|
52
|
-
activeToolActivity?: ToolActivity;
|
|
53
|
-
lastToolPreview?: string;
|
|
54
|
-
toolResultCompleted?: boolean;
|
|
51
|
+
durationMs?: number | undefined;
|
|
52
|
+
activeToolActivity?: ToolActivity | undefined;
|
|
53
|
+
lastToolPreview?: string | undefined;
|
|
54
|
+
toolResultCompleted?: boolean | undefined;
|
|
55
55
|
toolCount: number;
|
|
56
|
-
inputTokens?: number;
|
|
57
|
-
outputTokens?: number;
|
|
58
|
-
contextTokens?: number;
|
|
59
|
-
contextWindowTokens?: number;
|
|
60
|
-
finalOutput?: string;
|
|
61
|
-
errorText?: string;
|
|
56
|
+
inputTokens?: number | undefined;
|
|
57
|
+
outputTokens?: number | undefined;
|
|
58
|
+
contextTokens?: number | undefined;
|
|
59
|
+
contextWindowTokens?: number | undefined;
|
|
60
|
+
finalOutput?: string | undefined;
|
|
61
|
+
errorText?: string | undefined;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
const store = new Map<string, SubagentProgressState>();
|
|
@@ -89,6 +89,23 @@ export function getAllProgressStates(): SubagentProgressState[] {
|
|
|
89
89
|
return [...store.values()].sort((a, b) => b.startTime - a.startTime);
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
type ProgressTransientFields = Pick<
|
|
93
|
+
SubagentProgressState,
|
|
94
|
+
"activeToolActivity" | "lastToolPreview" | "toolResultCompleted"
|
|
95
|
+
>;
|
|
96
|
+
|
|
97
|
+
function stripTransientFields(
|
|
98
|
+
merged: SubagentProgressState,
|
|
99
|
+
): Omit<SubagentProgressState, keyof ProgressTransientFields> {
|
|
100
|
+
const {
|
|
101
|
+
activeToolActivity: _a,
|
|
102
|
+
lastToolPreview: _l,
|
|
103
|
+
toolResultCompleted: _t,
|
|
104
|
+
...base
|
|
105
|
+
} = merged;
|
|
106
|
+
return base;
|
|
107
|
+
}
|
|
108
|
+
|
|
92
109
|
export function patchProgressState(
|
|
93
110
|
requestId: string,
|
|
94
111
|
patch: Partial<SubagentProgressState>,
|
|
@@ -96,13 +113,7 @@ export function patchProgressState(
|
|
|
96
113
|
const state = store.get(requestId);
|
|
97
114
|
if (!state) return;
|
|
98
115
|
if (state.status !== "running") {
|
|
99
|
-
store.set(requestId, {
|
|
100
|
-
...state,
|
|
101
|
-
...patch,
|
|
102
|
-
activeToolActivity: undefined,
|
|
103
|
-
lastToolPreview: undefined,
|
|
104
|
-
toolResultCompleted: undefined,
|
|
105
|
-
});
|
|
116
|
+
store.set(requestId, stripTransientFields({ ...state, ...patch }));
|
|
106
117
|
return;
|
|
107
118
|
}
|
|
108
119
|
store.set(requestId, { ...state, ...patch });
|
|
@@ -115,7 +126,10 @@ function storeTerminalProgressState(
|
|
|
115
126
|
const state = store.get(requestId);
|
|
116
127
|
if (!state) return;
|
|
117
128
|
const durationMs = state.durationMs ?? Date.now() - state.startTime;
|
|
118
|
-
store.set(requestId, {
|
|
129
|
+
store.set(requestId, {
|
|
130
|
+
...stripTransientFields({ ...state, ...patch }),
|
|
131
|
+
durationMs,
|
|
132
|
+
});
|
|
119
133
|
}
|
|
120
134
|
|
|
121
135
|
export function finalizeProgressState(
|
|
@@ -125,9 +139,6 @@ export function finalizeProgressState(
|
|
|
125
139
|
storeTerminalProgressState(requestId, {
|
|
126
140
|
status: "success",
|
|
127
141
|
finalOutput: makeProgressFinalOutput(finalOutput),
|
|
128
|
-
activeToolActivity: undefined,
|
|
129
|
-
lastToolPreview: undefined,
|
|
130
|
-
toolResultCompleted: undefined,
|
|
131
142
|
});
|
|
132
143
|
}
|
|
133
144
|
|
|
@@ -136,21 +147,13 @@ export function failProgressState(requestId: string, errorText: string): void {
|
|
|
136
147
|
storeTerminalProgressState(requestId, {
|
|
137
148
|
status: "error",
|
|
138
149
|
errorText: sentence,
|
|
139
|
-
activeToolActivity: undefined,
|
|
140
|
-
lastToolPreview: undefined,
|
|
141
|
-
toolResultCompleted: undefined,
|
|
142
150
|
});
|
|
143
151
|
}
|
|
144
152
|
|
|
145
153
|
export function cancelProgressState(requestId: string, reason?: string): void {
|
|
146
154
|
storeTerminalProgressState(requestId, {
|
|
147
155
|
status: "cancelled",
|
|
148
|
-
|
|
149
|
-
lastToolPreview: undefined,
|
|
150
|
-
toolResultCompleted: undefined,
|
|
151
|
-
...(reason !== undefined
|
|
152
|
-
? { errorText: normalizeTerminalSentence(reason) }
|
|
153
|
-
: {}),
|
|
156
|
+
errorText: reason ? normalizeTerminalSentence(reason) : undefined,
|
|
154
157
|
});
|
|
155
158
|
}
|
|
156
159
|
|
|
@@ -235,11 +238,11 @@ function trackNewToolCall(
|
|
|
235
238
|
|
|
236
239
|
function extractProgressFromExistingProgress(
|
|
237
240
|
progress: {
|
|
238
|
-
activityText?: string;
|
|
239
|
-
activeToolActivity?: ToolActivity;
|
|
240
|
-
lastToolPreview?: string;
|
|
241
|
+
activityText?: string | undefined;
|
|
242
|
+
activeToolActivity?: ToolActivity | undefined;
|
|
243
|
+
lastToolPreview?: string | undefined;
|
|
241
244
|
toolCalls: { id: string; preview: string }[];
|
|
242
|
-
toolResultCompleted?: boolean;
|
|
245
|
+
toolResultCompleted?: boolean | undefined;
|
|
243
246
|
},
|
|
244
247
|
seenToolCallIds: Set<string>,
|
|
245
248
|
state: DetailsProgress,
|
|
@@ -317,7 +320,7 @@ function isDerivedToolCall(part: unknown): part is {
|
|
|
317
320
|
preview: string;
|
|
318
321
|
} {
|
|
319
322
|
if (!isObjectWith(part)) return false;
|
|
320
|
-
return typeof part
|
|
323
|
+
return typeof part["id"] === "string" && typeof part["preview"] === "string";
|
|
321
324
|
}
|
|
322
325
|
|
|
323
326
|
export function isToolCallPart(part: unknown): part is {
|
|
@@ -328,9 +331,9 @@ export function isToolCallPart(part: unknown): part is {
|
|
|
328
331
|
} {
|
|
329
332
|
if (!isObjectWith(part)) return false;
|
|
330
333
|
return (
|
|
331
|
-
part
|
|
332
|
-
typeof part
|
|
333
|
-
typeof part
|
|
334
|
+
part["type"] === "toolCall" &&
|
|
335
|
+
typeof part["id"] === "string" &&
|
|
336
|
+
typeof part["name"] === "string"
|
|
334
337
|
);
|
|
335
338
|
}
|
|
336
339
|
|
package/src/progress/progress.ts
CHANGED
|
@@ -89,11 +89,18 @@ export function renderSubagentProgress(
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
class DynamicSubagentProgressText implements Component {
|
|
92
|
+
private readonly requestId: string;
|
|
93
|
+
private readonly options: { expanded: boolean };
|
|
94
|
+
private readonly theme: SubagentTheme;
|
|
92
95
|
constructor(
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
) {
|
|
96
|
+
requestId: string,
|
|
97
|
+
options: { expanded: boolean },
|
|
98
|
+
theme: SubagentTheme,
|
|
99
|
+
) {
|
|
100
|
+
this.requestId = requestId;
|
|
101
|
+
this.options = options;
|
|
102
|
+
this.theme = theme;
|
|
103
|
+
}
|
|
97
104
|
invalidate(): void {}
|
|
98
105
|
render(width: number): string[] {
|
|
99
106
|
const state = getProgressState(this.requestId);
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
patchProgressState,
|
|
17
17
|
renderToolActivity,
|
|
18
18
|
} from "./progress.js";
|
|
19
|
+
import { SENSITIVE_PATTERN } from "./progress-state.js";
|
|
19
20
|
|
|
20
21
|
export function hasSubagentFailed(result: SingleResult): boolean {
|
|
21
22
|
return (
|
|
@@ -41,6 +42,46 @@ export function createSubagentError(result: SingleResult): Error {
|
|
|
41
42
|
return new Error(`Agent ${result.stopReason || "failed"}: ${msg}`);
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
const DEBUG_REDACTED_PLACEHOLDER = "[redacted]";
|
|
46
|
+
const SENSITIVE_ASSIGNMENT_PATTERN =
|
|
47
|
+
/\b(?:secret|password|[A-Za-z0-9_-]*token)(?:\s*[:=]\s*)(?:"[^"]*"|'[^']*'|[^\s,;]+)/gi;
|
|
48
|
+
const SENSITIVE_TERM_PATTERN = new RegExp(SENSITIVE_PATTERN.source, "gi");
|
|
49
|
+
const TOKEN_COUNT_KEY_PATTERN = /tokens$/i;
|
|
50
|
+
|
|
51
|
+
function redactSensitiveDebugString(text: string): string {
|
|
52
|
+
return text
|
|
53
|
+
.replace(SENSITIVE_ASSIGNMENT_PATTERN, DEBUG_REDACTED_PLACEHOLDER)
|
|
54
|
+
.replace(SENSITIVE_TERM_PATTERN, DEBUG_REDACTED_PLACEHOLDER);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isSensitiveDebugKey(key: string): boolean {
|
|
58
|
+
const lowerKey = key.toLowerCase();
|
|
59
|
+
if (TOKEN_COUNT_KEY_PATTERN.test(lowerKey)) return false;
|
|
60
|
+
return (
|
|
61
|
+
lowerKey.includes("secret") ||
|
|
62
|
+
lowerKey.includes("password") ||
|
|
63
|
+
lowerKey.endsWith("token")
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function redactSensitiveDebugValue(value: unknown): unknown {
|
|
68
|
+
if (typeof value === "string") return redactSensitiveDebugString(value);
|
|
69
|
+
if (Array.isArray(value)) return value.map(redactSensitiveDebugValue);
|
|
70
|
+
if (typeof value !== "object" || value === null) return value;
|
|
71
|
+
const redacted: Record<string, unknown> = {};
|
|
72
|
+
for (const [key, child] of Object.entries(value)) {
|
|
73
|
+
redacted[key] = isSensitiveDebugKey(key)
|
|
74
|
+
? DEBUG_REDACTED_PLACEHOLDER
|
|
75
|
+
: redactSensitiveDebugValue(child);
|
|
76
|
+
}
|
|
77
|
+
return redacted;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function redactSensitiveDebugMessages(messages: unknown): unknown {
|
|
81
|
+
if (!Array.isArray(messages)) return messages;
|
|
82
|
+
return messages.map(redactSensitiveDebugValue);
|
|
83
|
+
}
|
|
84
|
+
|
|
44
85
|
export function sanitizeDetailsForDisplay(
|
|
45
86
|
details: SubagentDetails,
|
|
46
87
|
includeMessages = false,
|
|
@@ -50,9 +91,11 @@ export function sanitizeDetailsForDisplay(
|
|
|
50
91
|
results: details.results.map(({ messages, termination, ...result }) => ({
|
|
51
92
|
...result,
|
|
52
93
|
stderr: includeMessages ? result.stderr : "",
|
|
53
|
-
...(includeMessages
|
|
94
|
+
...(includeMessages
|
|
95
|
+
? { messages: redactSensitiveDebugMessages(messages), termination }
|
|
96
|
+
: {}),
|
|
54
97
|
})),
|
|
55
|
-
};
|
|
98
|
+
} as SubagentDetails;
|
|
56
99
|
}
|
|
57
100
|
|
|
58
101
|
export function getLatestResult(
|
|
@@ -90,26 +133,27 @@ export function patchProgressFromDetails(
|
|
|
90
133
|
nextActivity = current.activeToolActivity;
|
|
91
134
|
}
|
|
92
135
|
if (toolResultCompleted && nextActivity?.child) {
|
|
93
|
-
|
|
136
|
+
const { child: _child, ...rest } = nextActivity;
|
|
137
|
+
nextActivity = rest;
|
|
94
138
|
} else if (toolResultCompleted) {
|
|
95
139
|
nextActivity = undefined;
|
|
96
140
|
}
|
|
97
|
-
patch
|
|
141
|
+
patch["activeToolActivity"] = nextActivity;
|
|
98
142
|
const renderedPreview = renderToolActivity(nextActivity);
|
|
99
143
|
if (renderedPreview) {
|
|
100
|
-
patch
|
|
144
|
+
patch["lastToolPreview"] = renderedPreview;
|
|
101
145
|
} else if (toolResultCompleted && !nextActivity) {
|
|
102
|
-
patch
|
|
146
|
+
patch["lastToolPreview"] = undefined;
|
|
103
147
|
}
|
|
104
148
|
if (toolResultCompleted) {
|
|
105
|
-
patch
|
|
149
|
+
patch["toolResultCompleted"] = true;
|
|
106
150
|
}
|
|
107
151
|
// Token accounting always applies when usage data is available
|
|
108
152
|
if (latestResult?.usage) {
|
|
109
|
-
patch
|
|
110
|
-
patch
|
|
111
|
-
patch
|
|
112
|
-
patch
|
|
153
|
+
patch["inputTokens"] = latestResult.usage.input;
|
|
154
|
+
patch["outputTokens"] = latestResult.usage.output;
|
|
155
|
+
patch["contextTokens"] = latestResult.usage.contextTokens;
|
|
156
|
+
patch["contextWindowTokens"] = latestResult.usage.contextWindowTokens;
|
|
113
157
|
}
|
|
114
158
|
patchProgressState(
|
|
115
159
|
requestId,
|
package/src/shared/types.ts
CHANGED
|
@@ -15,9 +15,9 @@ export interface UsageStats {
|
|
|
15
15
|
|
|
16
16
|
export interface ToolActivity {
|
|
17
17
|
toolName: string;
|
|
18
|
-
inputSummary?: string;
|
|
19
|
-
instanceName?: string;
|
|
20
|
-
child?: ToolActivity;
|
|
18
|
+
inputSummary?: string | undefined;
|
|
19
|
+
instanceName?: string | undefined;
|
|
20
|
+
child?: ToolActivity | undefined;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
export interface StreamingProgressToolCall {
|
|
@@ -26,30 +26,30 @@ export interface StreamingProgressToolCall {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
export interface StreamingProgress {
|
|
29
|
-
activityText?: string;
|
|
30
|
-
activeToolActivity?: ToolActivity;
|
|
29
|
+
activityText?: string | undefined;
|
|
30
|
+
activeToolActivity?: ToolActivity | undefined;
|
|
31
31
|
toolCalls: StreamingProgressToolCall[];
|
|
32
|
-
lastToolPreview?: string;
|
|
33
|
-
toolResultCompleted?: boolean;
|
|
32
|
+
lastToolPreview?: string | undefined;
|
|
33
|
+
toolResultCompleted?: boolean | undefined;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
export interface SingleResult {
|
|
37
37
|
agent: string;
|
|
38
|
-
instanceName?: string;
|
|
38
|
+
instanceName?: string | undefined;
|
|
39
39
|
agentSource: "user" | "project" | "unknown";
|
|
40
40
|
task: string;
|
|
41
41
|
exitCode: number;
|
|
42
42
|
finalOutput: string;
|
|
43
43
|
stderr: string;
|
|
44
44
|
usage: UsageStats;
|
|
45
|
-
model?: string;
|
|
46
|
-
stopReason?: string;
|
|
47
|
-
errorMessage?: string;
|
|
48
|
-
durationMs?: number;
|
|
49
|
-
progress?: StreamingProgress;
|
|
50
|
-
messages?: Message[];
|
|
51
|
-
termination?: TerminationMetadata;
|
|
52
|
-
thinkingWarning?: string;
|
|
45
|
+
model?: string | undefined;
|
|
46
|
+
stopReason?: string | undefined;
|
|
47
|
+
errorMessage?: string | undefined;
|
|
48
|
+
durationMs?: number | undefined;
|
|
49
|
+
progress?: StreamingProgress | undefined;
|
|
50
|
+
messages?: Message[] | undefined;
|
|
51
|
+
termination?: TerminationMetadata | undefined;
|
|
52
|
+
thinkingWarning?: string | undefined;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
export interface SubagentDetails {
|
package/src/shared/utils.ts
CHANGED
|
@@ -9,35 +9,63 @@ import {
|
|
|
9
9
|
|
|
10
10
|
export const DEFAULT_MAX_OUTPUT_BYTES = 50_000;
|
|
11
11
|
export const DEFAULT_MAX_OUTPUT_LINES = 500;
|
|
12
|
+
export const DEFAULT_AGENT_END_GRACE_MS = 250;
|
|
13
|
+
export const DEFAULT_MAX_STDERR_BYTES = 10_000;
|
|
14
|
+
export const DEFAULT_MAX_SUBAGENT_DEPTH = 3;
|
|
15
|
+
export const MAX_SUBAGENT_DEPTH_CEILING = 10;
|
|
12
16
|
|
|
13
17
|
export interface SubagentOutputLimits {
|
|
14
18
|
maxBytes: number;
|
|
15
19
|
maxLines: number;
|
|
16
20
|
}
|
|
17
21
|
|
|
18
|
-
|
|
22
|
+
export interface SubagentRuntimeLimits {
|
|
23
|
+
agentEndGraceMs: number;
|
|
24
|
+
maxStderrBytes: number;
|
|
25
|
+
maxDepth: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type EnvLimitConfig = Partial<Record<string, string | number | undefined>>;
|
|
19
29
|
|
|
20
30
|
function parsePositiveInteger(
|
|
21
31
|
value: string | number | undefined,
|
|
22
32
|
): number | undefined {
|
|
23
33
|
const parsed = typeof value === "number" ? value : Number(value);
|
|
24
|
-
if (!Number.isFinite(parsed) || parsed < 1)
|
|
25
|
-
|
|
34
|
+
if (!Number.isFinite(parsed) || !Number.isInteger(parsed) || parsed < 1)
|
|
35
|
+
return undefined;
|
|
36
|
+
return parsed;
|
|
26
37
|
}
|
|
27
38
|
|
|
28
39
|
export function getSubagentOutputLimits(
|
|
29
|
-
config:
|
|
40
|
+
config: EnvLimitConfig = process.env,
|
|
30
41
|
): SubagentOutputLimits {
|
|
31
42
|
return {
|
|
32
43
|
maxBytes:
|
|
33
|
-
parsePositiveInteger(config
|
|
44
|
+
parsePositiveInteger(config["PI_SUBAGENT_MAX_OUTPUT_BYTES"]) ??
|
|
34
45
|
DEFAULT_MAX_OUTPUT_BYTES,
|
|
35
46
|
maxLines:
|
|
36
|
-
parsePositiveInteger(config
|
|
47
|
+
parsePositiveInteger(config["PI_SUBAGENT_MAX_OUTPUT_LINES"]) ??
|
|
37
48
|
DEFAULT_MAX_OUTPUT_LINES,
|
|
38
49
|
};
|
|
39
50
|
}
|
|
40
51
|
|
|
52
|
+
export function getSubagentRuntimeLimits(
|
|
53
|
+
config: EnvLimitConfig = process.env,
|
|
54
|
+
): SubagentRuntimeLimits {
|
|
55
|
+
const maxDepth =
|
|
56
|
+
parsePositiveInteger(config["PI_SUBAGENT_MAX_DEPTH"]) ??
|
|
57
|
+
DEFAULT_MAX_SUBAGENT_DEPTH;
|
|
58
|
+
return {
|
|
59
|
+
agentEndGraceMs:
|
|
60
|
+
parsePositiveInteger(config["PI_SUBAGENT_AGENT_END_GRACE_MS"]) ??
|
|
61
|
+
DEFAULT_AGENT_END_GRACE_MS,
|
|
62
|
+
maxStderrBytes:
|
|
63
|
+
parsePositiveInteger(config["PI_SUBAGENT_MAX_STDERR_BYTES"]) ??
|
|
64
|
+
DEFAULT_MAX_STDERR_BYTES,
|
|
65
|
+
maxDepth: Math.min(maxDepth, MAX_SUBAGENT_DEPTH_CEILING),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
41
69
|
export function truncateOutput(
|
|
42
70
|
text: string,
|
|
43
71
|
limits: SubagentOutputLimits = getSubagentOutputLimits(),
|
package/tsconfig.json
CHANGED
|
@@ -1,30 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"compilerOptions": {
|
|
3
|
-
// Environment setup & latest features
|
|
4
3
|
"lib": ["ESNext"],
|
|
5
4
|
"target": "ESNext",
|
|
6
5
|
"module": "Preserve",
|
|
7
6
|
"moduleDetection": "force",
|
|
8
7
|
"jsx": "react-jsx",
|
|
9
|
-
"allowJs": true,
|
|
10
8
|
"types": ["bun"],
|
|
11
|
-
|
|
12
|
-
// Bundler mode
|
|
13
9
|
"moduleResolution": "bundler",
|
|
14
10
|
"allowImportingTsExtensions": true,
|
|
15
11
|
"verbatimModuleSyntax": true,
|
|
16
12
|
"noEmit": true,
|
|
17
|
-
|
|
18
|
-
// Best practices
|
|
19
13
|
"strict": true,
|
|
20
14
|
"skipLibCheck": true,
|
|
21
15
|
"noFallthroughCasesInSwitch": true,
|
|
22
16
|
"noUncheckedIndexedAccess": true,
|
|
23
17
|
"noImplicitOverride": true,
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
18
|
+
"noImplicitReturns": true,
|
|
19
|
+
"erasableSyntaxOnly": true,
|
|
20
|
+
"forceConsistentCasingInFileNames": true,
|
|
21
|
+
"noUnusedLocals": true,
|
|
22
|
+
"noUnusedParameters": true,
|
|
23
|
+
"noPropertyAccessFromIndexSignature": true,
|
|
24
|
+
"exactOptionalPropertyTypes": true,
|
|
25
|
+
"allowUnreachableCode": false,
|
|
26
|
+
"allowUnusedLabels": false
|
|
29
27
|
}
|
|
30
28
|
}
|