pi-subagents 0.17.0 → 0.17.2
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/CHANGELOG.md +25 -0
- package/README.md +62 -4
- package/agents/context-builder.md +1 -1
- package/agents/planner.md +1 -1
- package/agents/researcher.md +1 -1
- package/agents/scout.md +1 -1
- package/agents/worker.md +1 -1
- package/async-execution.ts +38 -13
- package/async-job-tracker.ts +36 -18
- package/execution.ts +114 -6
- package/index.ts +1 -1
- package/intercom-bridge.ts +6 -3
- package/notify.ts +2 -1
- package/package.json +1 -1
- package/post-exit-stdio-guard.ts +85 -0
- package/render.ts +58 -24
- package/session-tokens.ts +48 -0
- package/slash-commands.ts +1 -1
- package/subagent-executor.ts +35 -29
- package/subagent-runner.ts +72 -42
- package/subagents-status.ts +7 -1
- package/top-level-async.ts +13 -0
- package/types.ts +7 -3
- package/utils.ts +31 -2
package/intercom-bridge.ts
CHANGED
|
@@ -9,10 +9,13 @@ const DEFAULT_INTERCOM_CONFIG_PATH = path.join(os.homedir(), ".pi", "agent", "in
|
|
|
9
9
|
const DEFAULT_SUBAGENT_CONFIG_DIR = path.join(os.homedir(), ".pi", "agent", "extensions", "subagent");
|
|
10
10
|
const DEFAULT_INTERCOM_TARGET_PREFIX = "subagent-chat";
|
|
11
11
|
const INTERCOM_BRIDGE_MARKER = "Intercom orchestration channel:";
|
|
12
|
-
const DEFAULT_INTERCOM_BRIDGE_TEMPLATE = `
|
|
12
|
+
const DEFAULT_INTERCOM_BRIDGE_TEMPLATE = `The inherited thread is reference-only. Do not continue that conversation or send questions, status updates, or completion handoffs to the orchestrator in normal assistant text.
|
|
13
|
+
|
|
14
|
+
Use intercom only for coordination with the orchestrator session "{orchestratorTarget}".
|
|
13
15
|
- Need a decision or blocked: intercom({ action: "ask", to: "{orchestratorTarget}", message: "<question>" })
|
|
14
|
-
-
|
|
15
|
-
|
|
16
|
+
- Need to report progress or a completion handoff: intercom({ action: "send", to: "{orchestratorTarget}", message: "DONE: <summary>" })
|
|
17
|
+
|
|
18
|
+
If no upstream coordination is needed, continue the task normally and return a focused task result.`;
|
|
16
19
|
|
|
17
20
|
export interface IntercomBridgeState {
|
|
18
21
|
active: boolean;
|
package/notify.ts
CHANGED
|
@@ -54,10 +54,11 @@ export default function registerSubagentNotify(pi: ExtensionAPI): void {
|
|
|
54
54
|
extra.push(`Session file: ${result.sessionFile}`);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
const summary = result.summary.trim() ? result.summary : "(no output)";
|
|
57
58
|
const content = [
|
|
58
59
|
`Background task ${status}: **${agent}**${taskInfo}`,
|
|
59
60
|
"",
|
|
60
|
-
|
|
61
|
+
summary,
|
|
61
62
|
extra.length ? "" : undefined,
|
|
62
63
|
extra.length ? extra.join("\n") : undefined,
|
|
63
64
|
]
|
package/package.json
CHANGED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { ChildProcess } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
interface PostExitStdioGuardOptions {
|
|
4
|
+
idleMs: number;
|
|
5
|
+
hardMs: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface ChildWithPipedStdio {
|
|
9
|
+
stdout: ChildProcess["stdout"];
|
|
10
|
+
stderr: ChildProcess["stderr"];
|
|
11
|
+
on: ChildProcess["on"];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface ChildWithKill {
|
|
15
|
+
kill(signal?: NodeJS.Signals | number): boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function trySignalChild(child: ChildWithKill, signal: NodeJS.Signals): boolean {
|
|
19
|
+
try {
|
|
20
|
+
return child.kill(signal);
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function attachPostExitStdioGuard(
|
|
27
|
+
child: ChildWithPipedStdio,
|
|
28
|
+
options: PostExitStdioGuardOptions,
|
|
29
|
+
): () => void {
|
|
30
|
+
const { idleMs, hardMs } = options;
|
|
31
|
+
let exited = false;
|
|
32
|
+
let stdoutEnded = false;
|
|
33
|
+
let stderrEnded = false;
|
|
34
|
+
let idleTimer: NodeJS.Timeout | undefined;
|
|
35
|
+
let hardTimer: NodeJS.Timeout | undefined;
|
|
36
|
+
|
|
37
|
+
const destroyUnendedStdio = () => {
|
|
38
|
+
if (!stdoutEnded) {
|
|
39
|
+
try { child.stdout?.destroy(); } catch {}
|
|
40
|
+
}
|
|
41
|
+
if (!stderrEnded) {
|
|
42
|
+
try { child.stderr?.destroy(); } catch {}
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const clearTimers = () => {
|
|
47
|
+
if (idleTimer) {
|
|
48
|
+
clearTimeout(idleTimer);
|
|
49
|
+
idleTimer = undefined;
|
|
50
|
+
}
|
|
51
|
+
if (hardTimer) {
|
|
52
|
+
clearTimeout(hardTimer);
|
|
53
|
+
hardTimer = undefined;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const armIdleTimer = () => {
|
|
58
|
+
if (!exited) return;
|
|
59
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
60
|
+
idleTimer = setTimeout(destroyUnendedStdio, idleMs);
|
|
61
|
+
idleTimer.unref?.();
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
child.stdout?.on("data", armIdleTimer);
|
|
65
|
+
child.stderr?.on("data", armIdleTimer);
|
|
66
|
+
child.stdout?.on("end", () => {
|
|
67
|
+
stdoutEnded = true;
|
|
68
|
+
if (stdoutEnded && stderrEnded) clearTimers();
|
|
69
|
+
});
|
|
70
|
+
child.stderr?.on("end", () => {
|
|
71
|
+
stderrEnded = true;
|
|
72
|
+
if (stdoutEnded && stderrEnded) clearTimers();
|
|
73
|
+
});
|
|
74
|
+
child.on("exit", () => {
|
|
75
|
+
exited = true;
|
|
76
|
+
armIdleTimer();
|
|
77
|
+
if (hardTimer) return;
|
|
78
|
+
hardTimer = setTimeout(destroyUnendedStdio, hardMs);
|
|
79
|
+
hardTimer.unref?.();
|
|
80
|
+
});
|
|
81
|
+
child.on("close", clearTimers);
|
|
82
|
+
child.on("error", clearTimers);
|
|
83
|
+
|
|
84
|
+
return clearTimers;
|
|
85
|
+
}
|
package/render.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
|
|
6
6
|
import { getMarkdownTheme, type ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
7
7
|
import { Container, Markdown, Spacer, Text, visibleWidth, type Component } from "@mariozechner/pi-tui";
|
|
8
8
|
import {
|
|
9
|
+
type AgentProgress,
|
|
9
10
|
type AsyncJobState,
|
|
10
11
|
type Details,
|
|
11
12
|
MAX_WIDGET_JOBS,
|
|
@@ -113,6 +114,34 @@ function getToolCallLines(
|
|
|
113
114
|
return result.toolCalls?.map((toolCall) => expanded ? toolCall.expandedText : toolCall.text) ?? [];
|
|
114
115
|
}
|
|
115
116
|
|
|
117
|
+
function formatActivityLabel(lastActivityAt: number | undefined, now = Date.now()): string | undefined {
|
|
118
|
+
if (lastActivityAt === undefined) return undefined;
|
|
119
|
+
const ago = Math.max(0, now - lastActivityAt);
|
|
120
|
+
if (ago < 1000) return "active now";
|
|
121
|
+
if (ago < 60000) return `active ${Math.floor(ago / 1000)}s ago`;
|
|
122
|
+
return `active ${Math.floor(ago / 60000)}m ago`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function formatCurrentToolLine(progress: Pick<AgentProgress, "currentTool" | "currentToolArgs" | "currentToolStartedAt">, availableWidth: number, expanded: boolean): string | undefined {
|
|
126
|
+
if (!progress.currentTool) return undefined;
|
|
127
|
+
const maxToolArgsLen = Math.max(50, availableWidth - 20);
|
|
128
|
+
const toolArgsPreview = progress.currentToolArgs
|
|
129
|
+
? (expanded || progress.currentToolArgs.length <= maxToolArgsLen
|
|
130
|
+
? progress.currentToolArgs
|
|
131
|
+
: `${progress.currentToolArgs.slice(0, maxToolArgsLen)}...`)
|
|
132
|
+
: "";
|
|
133
|
+
const durationSuffix = progress.currentToolStartedAt !== undefined
|
|
134
|
+
? ` | ${formatDuration(Math.max(0, Date.now() - progress.currentToolStartedAt))}`
|
|
135
|
+
: "";
|
|
136
|
+
return toolArgsPreview
|
|
137
|
+
? `${progress.currentTool}: ${toolArgsPreview}${durationSuffix}`
|
|
138
|
+
: `${progress.currentTool}${durationSuffix}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function buildLiveStatusLine(progress: Pick<AgentProgress, "lastActivityAt">): string | undefined {
|
|
142
|
+
return formatActivityLabel(progress.lastActivityAt);
|
|
143
|
+
}
|
|
144
|
+
|
|
116
145
|
/**
|
|
117
146
|
* Render the async jobs widget
|
|
118
147
|
*/
|
|
@@ -226,18 +255,18 @@ export function renderSubagentResult(
|
|
|
226
255
|
c.addChild(new Spacer(1));
|
|
227
256
|
|
|
228
257
|
if (isRunning && r.progress) {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const toolArgsPreview = r.progress.currentToolArgs
|
|
232
|
-
? (expanded || r.progress.currentToolArgs.length <= maxToolArgsLen
|
|
233
|
-
? r.progress.currentToolArgs
|
|
234
|
-
: `${r.progress.currentToolArgs.slice(0, maxToolArgsLen)}...`)
|
|
235
|
-
: "";
|
|
236
|
-
const toolLine = toolArgsPreview
|
|
237
|
-
? `${r.progress.currentTool}: ${toolArgsPreview}`
|
|
238
|
-
: r.progress.currentTool;
|
|
258
|
+
const toolLine = formatCurrentToolLine(r.progress, w, expanded);
|
|
259
|
+
if (toolLine) {
|
|
239
260
|
c.addChild(new Text(fit(theme.fg("warning", `> ${toolLine}`)), 0, 0));
|
|
240
261
|
}
|
|
262
|
+
const liveStatusLine = buildLiveStatusLine(r.progress);
|
|
263
|
+
if (liveStatusLine) {
|
|
264
|
+
c.addChild(new Text(fit(theme.fg("accent", liveStatusLine)), 0, 0));
|
|
265
|
+
}
|
|
266
|
+
c.addChild(new Text(fit(theme.fg("accent", "Press Ctrl+O for live detail")), 0, 0));
|
|
267
|
+
if (r.artifactPaths) {
|
|
268
|
+
c.addChild(new Text(fit(theme.fg("dim", `Artifacts: ${shortenPath(r.artifactPaths.outputPath)}`)), 0, 0));
|
|
269
|
+
}
|
|
241
270
|
if (r.progress.recentTools?.length) {
|
|
242
271
|
for (const t of r.progress.recentTools.slice(-3)) {
|
|
243
272
|
const maxArgsLen = Math.max(40, w - 24);
|
|
@@ -250,7 +279,7 @@ export function renderSubagentResult(
|
|
|
250
279
|
for (const line of (r.progress.recentOutput ?? []).slice(-5)) {
|
|
251
280
|
c.addChild(new Text(fit(theme.fg("dim", ` ${line}`)), 0, 0));
|
|
252
281
|
}
|
|
253
|
-
if (
|
|
282
|
+
if (toolLine || liveStatusLine || r.progress.recentTools?.length || r.progress.recentOutput?.length || r.artifactPaths) {
|
|
254
283
|
c.addChild(new Spacer(1));
|
|
255
284
|
}
|
|
256
285
|
}
|
|
@@ -278,7 +307,7 @@ export function renderSubagentResult(
|
|
|
278
307
|
c.addChild(new Text(fit(theme.fg("dim", `Session: ${shortenPath(r.sessionFile)}`)), 0, 0));
|
|
279
308
|
}
|
|
280
309
|
|
|
281
|
-
if (r.artifactPaths) {
|
|
310
|
+
if (!isRunning && r.artifactPaths) {
|
|
282
311
|
c.addChild(new Spacer(1));
|
|
283
312
|
c.addChild(new Text(fit(theme.fg("dim", `Artifacts: ${shortenPath(r.artifactPaths.outputPath)}`)), 0, 0));
|
|
284
313
|
}
|
|
@@ -391,6 +420,7 @@ export function renderSubagentResult(
|
|
|
391
420
|
|| d.progress?.find((p) => p.agent === r.agent && p.status === "running");
|
|
392
421
|
const rProg = r.progress || progressFromArray || r.progressSummary;
|
|
393
422
|
const rRunning = rProg?.status === "running";
|
|
423
|
+
const stepNumber = typeof rProg?.index === "number" ? rProg.index + 1 : i + 1;
|
|
394
424
|
|
|
395
425
|
const resultOutput = getSingleResultOutput(r);
|
|
396
426
|
const statusIcon = rRunning
|
|
@@ -403,8 +433,8 @@ export function renderSubagentResult(
|
|
|
403
433
|
const stats = rProg ? ` | ${rProg.toolCount} tools, ${formatDuration(rProg.durationMs)}` : "";
|
|
404
434
|
const modelDisplay = r.model ? theme.fg("dim", ` (${r.model})`) : "";
|
|
405
435
|
const stepHeader = rRunning
|
|
406
|
-
? `${statusIcon} Step ${
|
|
407
|
-
: `${statusIcon} Step ${
|
|
436
|
+
? `${statusIcon} Step ${stepNumber}: ${theme.bold(theme.fg("warning", r.agent))}${modelDisplay}${stats}`
|
|
437
|
+
: `${statusIcon} Step ${stepNumber}: ${theme.bold(r.agent)}${modelDisplay}${stats}`;
|
|
408
438
|
const toolCallLines = getToolCallLines(r, expanded);
|
|
409
439
|
c.addChild(new Text(fit(stepHeader), 0, 0));
|
|
410
440
|
|
|
@@ -433,18 +463,18 @@ export function renderSubagentResult(
|
|
|
433
463
|
if (rProg.skills?.length) {
|
|
434
464
|
c.addChild(new Text(fit(theme.fg("accent", ` skills: ${rProg.skills.join(", ")}`)), 0, 0));
|
|
435
465
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
const toolArgsPreview = rProg.currentToolArgs
|
|
439
|
-
? (expanded || rProg.currentToolArgs.length <= maxToolArgsLen
|
|
440
|
-
? rProg.currentToolArgs
|
|
441
|
-
: `${rProg.currentToolArgs.slice(0, maxToolArgsLen)}...`)
|
|
442
|
-
: "";
|
|
443
|
-
const toolLine = toolArgsPreview
|
|
444
|
-
? `${rProg.currentTool}: ${toolArgsPreview}`
|
|
445
|
-
: rProg.currentTool;
|
|
466
|
+
const toolLine = formatCurrentToolLine(rProg, w, expanded);
|
|
467
|
+
if (toolLine) {
|
|
446
468
|
c.addChild(new Text(fit(theme.fg("warning", ` > ${toolLine}`)), 0, 0));
|
|
447
469
|
}
|
|
470
|
+
const liveStatusLine = buildLiveStatusLine(rProg);
|
|
471
|
+
if (liveStatusLine) {
|
|
472
|
+
c.addChild(new Text(fit(theme.fg("accent", ` ${liveStatusLine}`)), 0, 0));
|
|
473
|
+
}
|
|
474
|
+
c.addChild(new Text(fit(theme.fg("accent", " Press Ctrl+O for live detail")), 0, 0));
|
|
475
|
+
if (r.artifactPaths) {
|
|
476
|
+
c.addChild(new Text(fit(theme.fg("dim", ` artifacts: ${shortenPath(r.artifactPaths.outputPath)}`)), 0, 0));
|
|
477
|
+
}
|
|
448
478
|
if (rProg.recentTools?.length) {
|
|
449
479
|
for (const t of rProg.recentTools.slice(-3)) {
|
|
450
480
|
const maxArgsLen = Math.max(40, w - 30);
|
|
@@ -460,6 +490,10 @@ export function renderSubagentResult(
|
|
|
460
490
|
}
|
|
461
491
|
}
|
|
462
492
|
|
|
493
|
+
if (!rRunning && r.artifactPaths) {
|
|
494
|
+
c.addChild(new Text(fit(theme.fg("dim", ` artifacts: ${shortenPath(r.artifactPaths.outputPath)}`)), 0, 0));
|
|
495
|
+
}
|
|
496
|
+
|
|
463
497
|
if (expanded && !rRunning) {
|
|
464
498
|
for (const line of toolCallLines) {
|
|
465
499
|
c.addChild(new Text(fit(theme.fg("muted", ` ${line}`)), 0, 0));
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
|
|
4
|
+
export interface TokenUsage {
|
|
5
|
+
input: number;
|
|
6
|
+
output: number;
|
|
7
|
+
total: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function findLatestSessionFile(sessionDir: string): string | null {
|
|
11
|
+
try {
|
|
12
|
+
const files = fs.readdirSync(sessionDir)
|
|
13
|
+
.filter((f) => f.endsWith(".jsonl"))
|
|
14
|
+
.map((f) => path.join(sessionDir, f));
|
|
15
|
+
if (files.length === 0) return null;
|
|
16
|
+
files.sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs);
|
|
17
|
+
return files[0] ?? null;
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function parseSessionTokens(sessionDir: string): TokenUsage | null {
|
|
24
|
+
const sessionFile = findLatestSessionFile(sessionDir);
|
|
25
|
+
if (!sessionFile) return null;
|
|
26
|
+
try {
|
|
27
|
+
const content = fs.readFileSync(sessionFile, "utf-8");
|
|
28
|
+
let input = 0;
|
|
29
|
+
let output = 0;
|
|
30
|
+
for (const line of content.split("\n")) {
|
|
31
|
+
if (!line.trim()) continue;
|
|
32
|
+
try {
|
|
33
|
+
const entry = JSON.parse(line);
|
|
34
|
+
const usage = entry.usage ?? entry.message?.usage;
|
|
35
|
+
if (usage) {
|
|
36
|
+
input += usage.inputTokens ?? usage.input ?? 0;
|
|
37
|
+
output += usage.outputTokens ?? usage.output ?? 0;
|
|
38
|
+
}
|
|
39
|
+
} catch {
|
|
40
|
+
// Ignore malformed lines while scanning usage entries.
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return { input, output, total: input + output };
|
|
44
|
+
} catch {
|
|
45
|
+
// Usage extraction should not fail the run.
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
package/slash-commands.ts
CHANGED
|
@@ -145,7 +145,7 @@ async function requestSlashRun(
|
|
|
145
145
|
if (!ctx.hasUI) return;
|
|
146
146
|
const tool = update.currentTool ? ` ${update.currentTool}` : "";
|
|
147
147
|
const count = update.toolCount ?? 0;
|
|
148
|
-
ctx.ui.setStatus("subagent-slash", `${count} tools${tool}`);
|
|
148
|
+
ctx.ui.setStatus("subagent-slash", `${count} tools${tool} | Ctrl+O live detail`);
|
|
149
149
|
};
|
|
150
150
|
|
|
151
151
|
const onTerminalInput = ctx.hasUI
|
package/subagent-executor.ts
CHANGED
|
@@ -26,6 +26,7 @@ import { createForkContextResolver } from "./fork-context.ts";
|
|
|
26
26
|
import { applyIntercomBridgeToAgent, resolveIntercomBridge, resolveIntercomSessionTarget } from "./intercom-bridge.ts";
|
|
27
27
|
import { finalizeSingleOutput, injectSingleOutputInstruction, resolveSingleOutputPath } from "./single-output.ts";
|
|
28
28
|
import { compactForegroundDetails, getSingleResultOutput, mapConcurrent, resolveChildCwd } from "./utils.ts";
|
|
29
|
+
import { applyForceTopLevelAsyncOverride } from "./top-level-async.ts";
|
|
29
30
|
import {
|
|
30
31
|
cleanupWorktrees,
|
|
31
32
|
createWorktrees,
|
|
@@ -1209,32 +1210,38 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
1209
1210
|
if (normalized.error) return normalized.error;
|
|
1210
1211
|
const normalizedParams = normalized.params!;
|
|
1211
1212
|
|
|
1212
|
-
const
|
|
1213
|
-
|
|
1213
|
+
const effectiveParams = applyForceTopLevelAsyncOverride(
|
|
1214
|
+
normalizedParams,
|
|
1215
|
+
depth,
|
|
1216
|
+
deps.config.forceTopLevelAsync === true,
|
|
1217
|
+
);
|
|
1218
|
+
|
|
1219
|
+
const scope: AgentScope = resolveExecutionAgentScope(effectiveParams.agentScope);
|
|
1220
|
+
const effectiveCwd = effectiveParams.cwd ?? ctx.cwd;
|
|
1214
1221
|
const parentSessionFile = ctx.sessionManager.getSessionFile() ?? null;
|
|
1215
1222
|
deps.state.currentSessionId = parentSessionFile ?? `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1216
1223
|
const discoveredAgents = deps.discoverAgents(effectiveCwd, scope).agents;
|
|
1217
1224
|
const sessionName = resolveIntercomSessionTarget(deps.pi.getSessionName(), ctx.sessionManager.getSessionId());
|
|
1218
1225
|
const intercomBridge = resolveIntercomBridge({
|
|
1219
1226
|
config: deps.config.intercomBridge,
|
|
1220
|
-
context:
|
|
1227
|
+
context: effectiveParams.context,
|
|
1221
1228
|
orchestratorTarget: sessionName,
|
|
1222
1229
|
});
|
|
1223
1230
|
const agents = intercomBridge.active
|
|
1224
1231
|
? discoveredAgents.map((agent) => applyIntercomBridgeToAgent(agent, intercomBridge))
|
|
1225
1232
|
: discoveredAgents;
|
|
1226
1233
|
const runId = randomUUID().slice(0, 8);
|
|
1227
|
-
const shareEnabled =
|
|
1228
|
-
const hasChain = (
|
|
1229
|
-
const hasTasks = (
|
|
1230
|
-
const hasSingle = Boolean(
|
|
1234
|
+
const shareEnabled = effectiveParams.share === true;
|
|
1235
|
+
const hasChain = (effectiveParams.chain?.length ?? 0) > 0;
|
|
1236
|
+
const hasTasks = (effectiveParams.tasks?.length ?? 0) > 0;
|
|
1237
|
+
const hasSingle = Boolean(effectiveParams.agent && effectiveParams.task);
|
|
1231
1238
|
const allowClarifyTaskPrompt = hasChain
|
|
1232
|
-
&&
|
|
1239
|
+
&& effectiveParams.clarify === true
|
|
1233
1240
|
&& ctx.hasUI
|
|
1234
|
-
&& !(
|
|
1241
|
+
&& !(effectiveParams.chain?.some(isParallelStep) ?? false);
|
|
1235
1242
|
|
|
1236
1243
|
const validationError = validateExecutionInput(
|
|
1237
|
-
|
|
1244
|
+
effectiveParams,
|
|
1238
1245
|
agents,
|
|
1239
1246
|
hasChain,
|
|
1240
1247
|
hasTasks,
|
|
@@ -1245,25 +1252,24 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
1245
1252
|
|
|
1246
1253
|
let sessionFileForIndex: (idx?: number) => string | undefined = () => undefined;
|
|
1247
1254
|
try {
|
|
1248
|
-
sessionFileForIndex = createForkContextResolver(ctx.sessionManager,
|
|
1255
|
+
sessionFileForIndex = createForkContextResolver(ctx.sessionManager, effectiveParams.context).sessionFileForIndex;
|
|
1249
1256
|
} catch (error) {
|
|
1250
|
-
return toExecutionErrorResult(
|
|
1257
|
+
return toExecutionErrorResult(effectiveParams, error);
|
|
1251
1258
|
}
|
|
1252
|
-
|
|
1253
|
-
const
|
|
1254
|
-
const backgroundRequestedWhileClarifying = hasTasks && requestedAsync && normalizedParams.clarify === true;
|
|
1259
|
+
const requestedAsync = effectiveParams.async ?? deps.asyncByDefault;
|
|
1260
|
+
const backgroundRequestedWhileClarifying = hasTasks && requestedAsync && effectiveParams.clarify === true;
|
|
1255
1261
|
const effectiveAsync = requestedAsync
|
|
1256
|
-
&& (hasChain ?
|
|
1262
|
+
&& (hasChain ? effectiveParams.clarify === false : effectiveParams.clarify !== true);
|
|
1257
1263
|
|
|
1258
1264
|
const artifactConfig: ArtifactConfig = {
|
|
1259
1265
|
...DEFAULT_ARTIFACT_CONFIG,
|
|
1260
|
-
enabled:
|
|
1266
|
+
enabled: effectiveParams.artifacts !== false,
|
|
1261
1267
|
};
|
|
1262
1268
|
const artifactsDir = effectiveAsync ? deps.tempArtifactsDir : getArtifactsDir(parentSessionFile);
|
|
1263
1269
|
|
|
1264
1270
|
let sessionRoot: string;
|
|
1265
|
-
if (
|
|
1266
|
-
sessionRoot = path.resolve(deps.expandTilde(
|
|
1271
|
+
if (effectiveParams.sessionDir) {
|
|
1272
|
+
sessionRoot = path.resolve(deps.expandTilde(effectiveParams.sessionDir));
|
|
1267
1273
|
} else {
|
|
1268
1274
|
const baseSessionRoot = deps.config.defaultSessionDir
|
|
1269
1275
|
? path.resolve(deps.expandTilde(deps.config.defaultSessionDir))
|
|
@@ -1275,7 +1281,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
1275
1281
|
} catch (error) {
|
|
1276
1282
|
const message = error instanceof Error ? error.message : String(error);
|
|
1277
1283
|
return toExecutionErrorResult(
|
|
1278
|
-
|
|
1284
|
+
effectiveParams,
|
|
1279
1285
|
new Error(`Failed to create session directory '${sessionRoot}': ${message}`),
|
|
1280
1286
|
);
|
|
1281
1287
|
}
|
|
@@ -1283,11 +1289,11 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
1283
1289
|
path.join(sessionRoot, `run-${idx ?? 0}`);
|
|
1284
1290
|
|
|
1285
1291
|
const onUpdateWithContext = onUpdate
|
|
1286
|
-
? (r: AgentToolResult<Details>) => onUpdate(withForkContext(r,
|
|
1292
|
+
? (r: AgentToolResult<Details>) => onUpdate(withForkContext(r, effectiveParams.context))
|
|
1287
1293
|
: undefined;
|
|
1288
1294
|
|
|
1289
1295
|
const execData: ExecutionContextData = {
|
|
1290
|
-
params:
|
|
1296
|
+
params: effectiveParams,
|
|
1291
1297
|
effectiveCwd,
|
|
1292
1298
|
ctx,
|
|
1293
1299
|
signal,
|
|
@@ -1306,18 +1312,18 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
1306
1312
|
|
|
1307
1313
|
try {
|
|
1308
1314
|
const asyncResult = runAsyncPath(execData, deps);
|
|
1309
|
-
if (asyncResult) return withForkContext(asyncResult,
|
|
1315
|
+
if (asyncResult) return withForkContext(asyncResult, effectiveParams.context);
|
|
1310
1316
|
|
|
1311
|
-
if (hasChain &&
|
|
1312
|
-
return withForkContext(await runChainPath(execData, deps),
|
|
1317
|
+
if (hasChain && effectiveParams.chain) {
|
|
1318
|
+
return withForkContext(await runChainPath(execData, deps), effectiveParams.context);
|
|
1313
1319
|
}
|
|
1314
1320
|
|
|
1315
|
-
if (hasTasks &&
|
|
1316
|
-
return withForkContext(await runParallelPath(execData, deps),
|
|
1321
|
+
if (hasTasks && effectiveParams.tasks) {
|
|
1322
|
+
return withForkContext(await runParallelPath(execData, deps), effectiveParams.context);
|
|
1317
1323
|
}
|
|
1318
1324
|
|
|
1319
1325
|
if (hasSingle) {
|
|
1320
|
-
return withForkContext(await runSinglePath(execData, deps),
|
|
1326
|
+
return withForkContext(await runSinglePath(execData, deps), effectiveParams.context);
|
|
1321
1327
|
}
|
|
1322
1328
|
} catch (error) {
|
|
1323
1329
|
return toExecutionErrorResult(normalizedParams, error);
|
|
@@ -1327,7 +1333,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
1327
1333
|
content: [{ type: "text", text: "Invalid params" }],
|
|
1328
1334
|
isError: true,
|
|
1329
1335
|
details: { mode: "single" as const, results: [] },
|
|
1330
|
-
},
|
|
1336
|
+
}, effectiveParams.context);
|
|
1331
1337
|
};
|
|
1332
1338
|
|
|
1333
1339
|
return { execute };
|
package/subagent-runner.ts
CHANGED
|
@@ -28,7 +28,9 @@ import {
|
|
|
28
28
|
} from "./parallel-utils.ts";
|
|
29
29
|
import { buildPiArgs, cleanupTempDir } from "./pi-args.ts";
|
|
30
30
|
import { formatModelAttemptNote, isRetryableModelFailure } from "./model-fallback.ts";
|
|
31
|
+
import { attachPostExitStdioGuard, trySignalChild } from "./post-exit-stdio-guard.ts";
|
|
31
32
|
import { detectSubagentError, extractTextFromContent, extractToolArgsPreview, getFinalOutput } from "./utils.ts";
|
|
33
|
+
import { parseSessionTokens, type TokenUsage } from "./session-tokens.ts";
|
|
32
34
|
import {
|
|
33
35
|
cleanupWorktrees,
|
|
34
36
|
createWorktrees,
|
|
@@ -89,38 +91,6 @@ function findLatestSessionFile(sessionDir: string): string | null {
|
|
|
89
91
|
}
|
|
90
92
|
}
|
|
91
93
|
|
|
92
|
-
interface TokenUsage {
|
|
93
|
-
input: number;
|
|
94
|
-
output: number;
|
|
95
|
-
total: number;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function parseSessionTokens(sessionDir: string): TokenUsage | null {
|
|
99
|
-
const sessionFile = findLatestSessionFile(sessionDir);
|
|
100
|
-
if (!sessionFile) return null;
|
|
101
|
-
try {
|
|
102
|
-
const content = fs.readFileSync(sessionFile, "utf-8");
|
|
103
|
-
let input = 0;
|
|
104
|
-
let output = 0;
|
|
105
|
-
for (const line of content.split("\n")) {
|
|
106
|
-
if (!line.trim()) continue;
|
|
107
|
-
try {
|
|
108
|
-
const entry = JSON.parse(line);
|
|
109
|
-
if (entry.usage) {
|
|
110
|
-
input += entry.usage.inputTokens ?? entry.usage.input ?? 0;
|
|
111
|
-
output += entry.usage.outputTokens ?? entry.usage.output ?? 0;
|
|
112
|
-
}
|
|
113
|
-
} catch {
|
|
114
|
-
// Ignore malformed lines while scanning usage entries.
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
return { input, output, total: input + output };
|
|
118
|
-
} catch {
|
|
119
|
-
// Usage extraction should not fail the run.
|
|
120
|
-
return null;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
94
|
function emptyUsage(): Usage {
|
|
125
95
|
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
|
|
126
96
|
}
|
|
@@ -248,13 +218,18 @@ function runPiStreaming(
|
|
|
248
218
|
if (event.message.model) model = event.message.model;
|
|
249
219
|
if (event.message.errorMessage) error = event.message.errorMessage;
|
|
250
220
|
const eventUsage = event.message.usage;
|
|
251
|
-
if (
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
221
|
+
if (eventUsage) {
|
|
222
|
+
usage.turns++;
|
|
223
|
+
usage.input += eventUsage.input ?? eventUsage.inputTokens ?? 0;
|
|
224
|
+
usage.output += eventUsage.output ?? eventUsage.outputTokens ?? 0;
|
|
225
|
+
usage.cacheRead += eventUsage.cacheRead ?? 0;
|
|
226
|
+
usage.cacheWrite += eventUsage.cacheWrite ?? 0;
|
|
227
|
+
usage.cost += eventUsage.cost?.total ?? 0;
|
|
228
|
+
}
|
|
229
|
+
const stopReason = (event.message as { stopReason?: string }).stopReason;
|
|
230
|
+
const hasToolCall = Array.isArray(event.message.content)
|
|
231
|
+
&& event.message.content.some((part) => (part as { type?: string }).type === "toolCall");
|
|
232
|
+
if (stopReason === "stop" && !hasToolCall) startFinalDrain();
|
|
258
233
|
}
|
|
259
234
|
};
|
|
260
235
|
|
|
@@ -271,6 +246,16 @@ function runPiStreaming(
|
|
|
271
246
|
}
|
|
272
247
|
};
|
|
273
248
|
|
|
249
|
+
// Guard both cases that can leave the parent waiting on `close` forever:
|
|
250
|
+
// a lingering stdio holder after `exit`, or a child that never exits.
|
|
251
|
+
const FINAL_DRAIN_MS = 5000;
|
|
252
|
+
const HARD_KILL_MS = 3000;
|
|
253
|
+
let childExited = false;
|
|
254
|
+
let forcedTerminationSignal = false;
|
|
255
|
+
let finalDrainTimer: NodeJS.Timeout | undefined;
|
|
256
|
+
let finalHardKillTimer: NodeJS.Timeout | undefined;
|
|
257
|
+
let settled = false;
|
|
258
|
+
const clearStdioGuard = attachPostExitStdioGuard(child, { idleMs: 2000, hardMs: 8000 });
|
|
274
259
|
child.stdout.on("data", (chunk: Buffer) => {
|
|
275
260
|
const text = chunk.toString();
|
|
276
261
|
stdoutBuf += text;
|
|
@@ -282,16 +267,61 @@ function runPiStreaming(
|
|
|
282
267
|
child.stderr.on("data", (chunk: Buffer) => {
|
|
283
268
|
processStderrText(chunk.toString());
|
|
284
269
|
});
|
|
285
|
-
|
|
286
|
-
|
|
270
|
+
const clearDrainTimers = () => {
|
|
271
|
+
if (finalDrainTimer) {
|
|
272
|
+
clearTimeout(finalDrainTimer);
|
|
273
|
+
finalDrainTimer = undefined;
|
|
274
|
+
}
|
|
275
|
+
if (finalHardKillTimer) {
|
|
276
|
+
clearTimeout(finalHardKillTimer);
|
|
277
|
+
finalHardKillTimer = undefined;
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
function startFinalDrain(): void {
|
|
281
|
+
if (childExited || finalDrainTimer || settled) return;
|
|
282
|
+
finalDrainTimer = setTimeout(() => {
|
|
283
|
+
if (settled) return;
|
|
284
|
+
const termSent = trySignalChild(child, "SIGTERM");
|
|
285
|
+
if (!termSent) return;
|
|
286
|
+
forcedTerminationSignal = true;
|
|
287
|
+
if (!error) {
|
|
288
|
+
error = `Subagent process did not exit within ${FINAL_DRAIN_MS}ms after its final message. Forcing termination.`;
|
|
289
|
+
}
|
|
290
|
+
finalHardKillTimer = setTimeout(() => {
|
|
291
|
+
if (settled) return;
|
|
292
|
+
forcedTerminationSignal = trySignalChild(child, "SIGKILL") || forcedTerminationSignal;
|
|
293
|
+
}, HARD_KILL_MS);
|
|
294
|
+
finalHardKillTimer.unref?.();
|
|
295
|
+
}, FINAL_DRAIN_MS);
|
|
296
|
+
finalDrainTimer.unref?.();
|
|
297
|
+
}
|
|
298
|
+
child.on("exit", () => {
|
|
299
|
+
childExited = true;
|
|
300
|
+
clearDrainTimers();
|
|
301
|
+
});
|
|
302
|
+
child.on("close", (exitCode, signal) => {
|
|
303
|
+
settled = true;
|
|
304
|
+
clearDrainTimers();
|
|
305
|
+
clearStdioGuard();
|
|
287
306
|
if (stdoutBuf.trim()) processStdoutLine(stdoutBuf);
|
|
288
307
|
if (stderrBuf.trim()) appendChildLine("subagent.child.stderr", stderrBuf);
|
|
289
308
|
outputStream.end();
|
|
290
309
|
const finalOutput = getFinalOutput(messages) || rawStdoutLines.join("\n").trim();
|
|
291
|
-
resolve({
|
|
310
|
+
resolve({
|
|
311
|
+
stderr,
|
|
312
|
+
exitCode: forcedTerminationSignal || signal ? (exitCode ?? 1) : exitCode,
|
|
313
|
+
messages,
|
|
314
|
+
usage,
|
|
315
|
+
model,
|
|
316
|
+
error,
|
|
317
|
+
finalOutput,
|
|
318
|
+
});
|
|
292
319
|
});
|
|
293
320
|
|
|
294
321
|
child.on("error", (spawnError) => {
|
|
322
|
+
settled = true;
|
|
323
|
+
clearDrainTimers();
|
|
324
|
+
clearStdioGuard();
|
|
295
325
|
outputStream.end();
|
|
296
326
|
const finalOutput = getFinalOutput(messages) || rawStdoutLines.join("\n").trim();
|
|
297
327
|
const spawnErrorMessage = spawnError instanceof Error ? spawnError.message : String(spawnError);
|
package/subagents-status.ts
CHANGED
|
@@ -157,6 +157,12 @@ export class SubagentsStatusComponent implements Component {
|
|
|
157
157
|
const lines = [
|
|
158
158
|
row(`cwd: ${truncateToWidth(shortenPath(run.cwd ?? run.asyncDir), innerW - 5)}`, width, this.theme),
|
|
159
159
|
];
|
|
160
|
+
if (run.outputFile) {
|
|
161
|
+
lines.push(row(`output: ${truncateToWidth(shortenPath(run.outputFile), innerW - 8)}`, width, this.theme));
|
|
162
|
+
}
|
|
163
|
+
if (run.sessionFile) {
|
|
164
|
+
lines.push(row(`session: ${truncateToWidth(shortenPath(run.sessionFile), innerW - 9)}`, width, this.theme));
|
|
165
|
+
}
|
|
160
166
|
for (const step of run.steps) {
|
|
161
167
|
const model = step.model ? ` | ${step.model}` : "";
|
|
162
168
|
const attempts = step.attemptedModels && step.attemptedModels.length > 1
|
|
@@ -226,7 +232,7 @@ export class SubagentsStatusComponent implements Component {
|
|
|
226
232
|
lines.push(row(this.theme.fg("dim", "No runs selected."), w, this.theme));
|
|
227
233
|
}
|
|
228
234
|
|
|
229
|
-
const footer = `↑↓ select esc close ${this.active.length} active / ${this.recent.length} recent`;
|
|
235
|
+
const footer = `↑↓ select esc close summary view ${this.active.length} active / ${this.recent.length} recent`;
|
|
230
236
|
lines.push(renderFooter(truncateToWidth(footer, innerW), w, this.theme));
|
|
231
237
|
return lines;
|
|
232
238
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface AsyncOverrideParams {
|
|
2
|
+
async?: boolean;
|
|
3
|
+
clarify?: boolean;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function applyForceTopLevelAsyncOverride<T extends AsyncOverrideParams>(
|
|
7
|
+
params: T,
|
|
8
|
+
depth: number,
|
|
9
|
+
forceTopLevelAsync: boolean,
|
|
10
|
+
): T {
|
|
11
|
+
if (!(depth === 0 && forceTopLevelAsync)) return params;
|
|
12
|
+
return { ...params, async: true, clarify: false };
|
|
13
|
+
}
|