killeros 1.4.1 → 1.4.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 +12 -0
- package/README.md +26 -4
- package/agents/debugger.md +0 -1
- package/agents/documenter.md +0 -1
- package/agents/planner.md +0 -1
- package/agents/reviewer.md +0 -1
- package/agents/scout.md +0 -1
- package/agents/security.md +0 -1
- package/agents/tester.md +0 -1
- package/agents/worker.md +0 -1
- package/package.json +4 -1
- package/subagent-lifecycle.ts +506 -0
- package/subagent-process.ts +474 -0
- package/subagent-ui.ts +210 -0
- package/subagents.ts +610 -266
package/subagents.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { randomUUID } from "node:crypto";
|
|
3
2
|
import { closeSync, openSync, readSync, readdirSync, statSync } from "node:fs";
|
|
4
3
|
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
5
4
|
import os from "node:os";
|
|
@@ -16,12 +15,13 @@ import {
|
|
|
16
15
|
} from "@earendil-works/pi-coding-agent";
|
|
17
16
|
import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
|
|
18
17
|
import { Type } from "typebox";
|
|
18
|
+
import { SubagentThreadRegistry, type SubagentThread, type SubagentThreadId, type SubagentThreadState } from "./subagent-lifecycle.ts";
|
|
19
|
+
import { runSubagentProcess, type SubagentProcessHandle, type SubagentProcessResult } from "./subagent-process.ts";
|
|
20
|
+
import { formatThreadBoard, formatThreadInspection, type ThreadRecord as ThreadBoardRecord } from "./subagent-ui.ts";
|
|
19
21
|
|
|
20
22
|
export const SUBAGENT_LIMITS = {
|
|
21
23
|
maxTasks: 8,
|
|
22
24
|
maxReadConcurrency: 4,
|
|
23
|
-
defaultTurns: 8,
|
|
24
|
-
maxTurns: 12,
|
|
25
25
|
defaultTimeoutMs: 300_000,
|
|
26
26
|
maxTimeoutMs: 600_000,
|
|
27
27
|
jsonlLineBytes: 32 * 1024 * 1024,
|
|
@@ -29,6 +29,8 @@ export const SUBAGENT_LIMITS = {
|
|
|
29
29
|
stderrBytes: 64 * 1024,
|
|
30
30
|
taskOutputBytes: 50 * 1024,
|
|
31
31
|
toolOutputBytes: 50 * 1024,
|
|
32
|
+
quotaTokens: 1_000_000,
|
|
33
|
+
quotaUsd: 10,
|
|
32
34
|
roleFileBytes: 64 * 1024,
|
|
33
35
|
taskCharacters: 20_000,
|
|
34
36
|
killGraceMs: 5_000,
|
|
@@ -40,7 +42,8 @@ const WRITE_TOOLS = new Set(["bash", "edit", "write"]);
|
|
|
40
42
|
const KNOWN_TOOLS = new Set([...READ_TOOLS, ...WRITE_TOOLS]);
|
|
41
43
|
const SUBAGENT_WEB_EXTENSION = "npm:pi-web-access";
|
|
42
44
|
const INHERIT_SETTING = "inherit";
|
|
43
|
-
const
|
|
45
|
+
const MAX_RUNTIME_STEERING_MESSAGES = 20;
|
|
46
|
+
const ROLE_FIELDS = new Set(["name", "description", "access", "tools", "model", "thinking", "timeoutMs"]);
|
|
44
47
|
|
|
45
48
|
type ThinkingLevel = ModelThinkingLevel;
|
|
46
49
|
export type AgentAccess = "read" | "write";
|
|
@@ -55,7 +58,6 @@ export interface AgentRole {
|
|
|
55
58
|
tools: string[];
|
|
56
59
|
model?: string;
|
|
57
60
|
thinking?: string;
|
|
58
|
-
maxTurns: number;
|
|
59
61
|
timeoutMs: number;
|
|
60
62
|
prompt: string;
|
|
61
63
|
source: AgentSource;
|
|
@@ -98,8 +100,10 @@ export interface SubagentTaskResult {
|
|
|
98
100
|
traceBytes: number;
|
|
99
101
|
traceTruncatedBytes: number;
|
|
100
102
|
stderr: string;
|
|
103
|
+
stderrBytes: number;
|
|
101
104
|
stderrTruncatedBytes: number;
|
|
102
105
|
output: string;
|
|
106
|
+
outputBytes: number;
|
|
103
107
|
outputTruncatedBytes: number;
|
|
104
108
|
usage: SubagentUsage;
|
|
105
109
|
durationMs: number;
|
|
@@ -115,6 +119,11 @@ export interface SubagentDetails {
|
|
|
115
119
|
projectAgentsDir: string | null;
|
|
116
120
|
results: SubagentTaskResult[];
|
|
117
121
|
aggregateUsage: SubagentUsage;
|
|
122
|
+
parentId?: string;
|
|
123
|
+
threads?: SubagentThread[];
|
|
124
|
+
activeThreads?: SubagentThread[];
|
|
125
|
+
doneThreads?: SubagentThread[];
|
|
126
|
+
selectedThreadId?: string;
|
|
118
127
|
}
|
|
119
128
|
|
|
120
129
|
interface ModelContext {
|
|
@@ -147,7 +156,6 @@ export interface SubagentRuntimeOptions {
|
|
|
147
156
|
userAgentsDir?: string;
|
|
148
157
|
webExtension?: string;
|
|
149
158
|
spawnProcess?: (args: string[], cwd: string) => SpawnedProcess;
|
|
150
|
-
createTaskId?: (index: number) => string;
|
|
151
159
|
limits?: Partial<SubagentLimits>;
|
|
152
160
|
}
|
|
153
161
|
|
|
@@ -191,6 +199,14 @@ function aggregateUsage(results: SubagentTaskResult[]): SubagentUsage {
|
|
|
191
199
|
return total;
|
|
192
200
|
}
|
|
193
201
|
|
|
202
|
+
function boundedText(text: string, maxBytes: number, marker: string): string {
|
|
203
|
+
const capped = truncateUtf8(text, maxBytes);
|
|
204
|
+
if (!capped.omittedBytes) return text;
|
|
205
|
+
const markerBytes = Buffer.byteLength(marker, "utf8");
|
|
206
|
+
if (markerBytes >= maxBytes) return truncateUtf8(text, maxBytes).text;
|
|
207
|
+
return `${truncateUtf8(text, maxBytes - markerBytes).text}${marker}`;
|
|
208
|
+
}
|
|
209
|
+
|
|
194
210
|
function readBoundedFile(filePath: string, maxBytes: number): string {
|
|
195
211
|
let descriptor: number | undefined;
|
|
196
212
|
try {
|
|
@@ -280,7 +296,6 @@ function parseAgentFile(filePath: string, source: AgentSource, limits: SubagentL
|
|
|
280
296
|
tools,
|
|
281
297
|
model: typeof modelValue === "string" ? modelValue.trim() : undefined,
|
|
282
298
|
thinking: typeof thinkingValue === "string" ? thinkingValue.trim() : undefined,
|
|
283
|
-
maxTurns: optionalPositiveInteger(frontmatter, filePath, "maxTurns", limits.defaultTurns, limits.maxTurns),
|
|
284
299
|
timeoutMs: optionalPositiveInteger(frontmatter, filePath, "timeoutMs", limits.defaultTimeoutMs, limits.maxTimeoutMs),
|
|
285
300
|
prompt,
|
|
286
301
|
source,
|
|
@@ -452,31 +467,6 @@ function defaultSpawnProcess(args: string[], cwd: string): SpawnedProcess {
|
|
|
452
467
|
}) as unknown as SpawnedProcess;
|
|
453
468
|
}
|
|
454
469
|
|
|
455
|
-
function terminateProcess(child: SpawnedProcess, force: boolean): void {
|
|
456
|
-
if (process.platform === "win32" && force && child.pid) {
|
|
457
|
-
const killer = spawn("taskkill", ["/pid", String(child.pid), "/T", "/F"], {
|
|
458
|
-
shell: false,
|
|
459
|
-
stdio: "ignore",
|
|
460
|
-
windowsHide: true,
|
|
461
|
-
});
|
|
462
|
-
killer.unref();
|
|
463
|
-
return;
|
|
464
|
-
}
|
|
465
|
-
if (process.platform !== "win32" && child.pid) {
|
|
466
|
-
try {
|
|
467
|
-
process.kill(-child.pid, force ? "SIGKILL" : "SIGTERM");
|
|
468
|
-
return;
|
|
469
|
-
} catch {
|
|
470
|
-
// Fall back to the direct child when process-group signaling is unavailable.
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
try {
|
|
474
|
-
child.kill(force ? "SIGKILL" : "SIGTERM");
|
|
475
|
-
} catch {
|
|
476
|
-
// The process may already have exited.
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
|
|
480
470
|
function truncateUtf8(text: string, maxBytes: number): { text: string; omittedBytes: number } {
|
|
481
471
|
const bytes = Buffer.from(text, "utf8");
|
|
482
472
|
if (bytes.length <= maxBytes) return { text, omittedBytes: 0 };
|
|
@@ -485,34 +475,6 @@ function truncateUtf8(text: string, maxBytes: number): { text: string; omittedBy
|
|
|
485
475
|
return { text: truncated, omittedBytes: bytes.length - Buffer.byteLength(truncated, "utf8") };
|
|
486
476
|
}
|
|
487
477
|
|
|
488
|
-
function textContent(message: any): string {
|
|
489
|
-
if (!Array.isArray(message?.content)) return "";
|
|
490
|
-
return message.content.filter((part: any) => part?.type === "text" && typeof part.text === "string").map((part: any) => part.text).join("\n");
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
function traceMessage(message: any): string[] {
|
|
494
|
-
if (!Array.isArray(message?.content)) return [];
|
|
495
|
-
const entries: string[] = [];
|
|
496
|
-
for (const part of message.content) {
|
|
497
|
-
if (part?.type !== "toolCall" || typeof part.name !== "string") continue;
|
|
498
|
-
const args = truncateUtf8(JSON.stringify(part.arguments ?? {}), 2_000).text;
|
|
499
|
-
entries.push(`${part.name} ${args}`);
|
|
500
|
-
}
|
|
501
|
-
return entries;
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
function appendTrace(result: SubagentTaskResult, entries: string[], maxBytes: number): void {
|
|
505
|
-
for (const entry of entries) {
|
|
506
|
-
const entryBytes = Buffer.byteLength(entry, "utf8");
|
|
507
|
-
const remaining = Math.max(0, maxBytes - result.traceBytes);
|
|
508
|
-
const retained = truncateUtf8(entry, remaining).text;
|
|
509
|
-
const retainedBytes = Buffer.byteLength(retained, "utf8");
|
|
510
|
-
if (retained) result.trace.push(retained);
|
|
511
|
-
result.traceBytes += retainedBytes;
|
|
512
|
-
result.traceTruncatedBytes += entryBytes - retainedBytes;
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
|
|
516
478
|
function makeQueuedResult(id: string, agent: string, task: string, step?: number): SubagentTaskResult {
|
|
517
479
|
return {
|
|
518
480
|
id,
|
|
@@ -525,8 +487,10 @@ function makeQueuedResult(id: string, agent: string, task: string, step?: number
|
|
|
525
487
|
traceBytes: 0,
|
|
526
488
|
traceTruncatedBytes: 0,
|
|
527
489
|
stderr: "",
|
|
490
|
+
stderrBytes: 0,
|
|
528
491
|
stderrTruncatedBytes: 0,
|
|
529
492
|
output: "",
|
|
493
|
+
outputBytes: 0,
|
|
530
494
|
outputTruncatedBytes: 0,
|
|
531
495
|
usage: emptyUsage(),
|
|
532
496
|
durationMs: 0,
|
|
@@ -544,6 +508,35 @@ function cloneResult(result: SubagentTaskResult): SubagentTaskResult {
|
|
|
544
508
|
};
|
|
545
509
|
}
|
|
546
510
|
|
|
511
|
+
function mergeTaskResults(previous: SubagentTaskResult | undefined, next: SubagentTaskResult, maxTraceBytes: number): SubagentTaskResult {
|
|
512
|
+
if (!previous) return cloneResult(next);
|
|
513
|
+
const merged = cloneResult(next);
|
|
514
|
+
const trace: string[] = [];
|
|
515
|
+
let traceBytes = 0;
|
|
516
|
+
let traceTruncatedBytes = previous.traceTruncatedBytes + next.traceTruncatedBytes;
|
|
517
|
+
for (const entry of [...previous.trace, ...next.trace]) {
|
|
518
|
+
const retained = truncateUtf8(entry, Math.max(0, maxTraceBytes - traceBytes));
|
|
519
|
+
if (retained.text) trace.push(retained.text);
|
|
520
|
+
const retainedBytes = Buffer.byteLength(retained.text, "utf8");
|
|
521
|
+
traceBytes += retainedBytes;
|
|
522
|
+
traceTruncatedBytes += retained.omittedBytes;
|
|
523
|
+
}
|
|
524
|
+
merged.trace = trace;
|
|
525
|
+
merged.traceBytes = traceBytes;
|
|
526
|
+
merged.traceTruncatedBytes = traceTruncatedBytes;
|
|
527
|
+
merged.stderr = [previous.stderr, next.stderr].filter(Boolean).join("\n");
|
|
528
|
+
merged.stderrBytes = previous.stderrBytes + next.stderrBytes;
|
|
529
|
+
merged.stderrTruncatedBytes = previous.stderrTruncatedBytes + next.stderrTruncatedBytes;
|
|
530
|
+
merged.output = next.output || previous.output;
|
|
531
|
+
merged.outputBytes = previous.outputBytes + next.outputBytes;
|
|
532
|
+
merged.outputTruncatedBytes = previous.outputTruncatedBytes + next.outputTruncatedBytes;
|
|
533
|
+
merged.usage = emptyUsage();
|
|
534
|
+
addUsage(merged.usage, previous.usage);
|
|
535
|
+
addUsage(merged.usage, next.usage);
|
|
536
|
+
merged.durationMs = previous.durationMs + next.durationMs;
|
|
537
|
+
return merged;
|
|
538
|
+
}
|
|
539
|
+
|
|
547
540
|
function cloneDetails(mode: SubagentDetails["mode"], scope: AgentScope, projectAgentsDir: string | null, results: SubagentTaskResult[]): SubagentDetails {
|
|
548
541
|
const cloned = results.map(cloneResult);
|
|
549
542
|
return { mode, agentScope: scope, projectAgentsDir, results: cloned, aggregateUsage: aggregateUsage(cloned) };
|
|
@@ -568,7 +561,34 @@ interface RunTaskOptions {
|
|
|
568
561
|
webExtension?: string;
|
|
569
562
|
projectTrusted: boolean;
|
|
570
563
|
limits: SubagentLimits;
|
|
564
|
+
timeoutMs?: number;
|
|
571
565
|
onChange: (result: SubagentTaskResult) => void;
|
|
566
|
+
onHandle?: (handle: SubagentProcessHandle) => void;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function applyProcessResult(
|
|
570
|
+
target: SubagentTaskResult,
|
|
571
|
+
source: Readonly<SubagentProcessResult>,
|
|
572
|
+
startedAt: number,
|
|
573
|
+
onChange: (result: SubagentTaskResult) => void,
|
|
574
|
+
): void {
|
|
575
|
+
target.status = source.status;
|
|
576
|
+
target.trace = [...source.trace];
|
|
577
|
+
target.traceBytes = source.traceBytes;
|
|
578
|
+
target.traceTruncatedBytes = source.traceTruncatedBytes;
|
|
579
|
+
target.stderr = source.stderr;
|
|
580
|
+
target.stderrBytes = source.stderrBytes;
|
|
581
|
+
target.stderrTruncatedBytes = source.stderrTruncatedBytes;
|
|
582
|
+
target.output = source.output;
|
|
583
|
+
target.outputBytes = source.outputBytes;
|
|
584
|
+
target.outputTruncatedBytes = source.outputTruncatedBytes;
|
|
585
|
+
target.usage = { ...source.usage, cost: { ...source.usage.cost } };
|
|
586
|
+
target.model = source.model ?? target.model;
|
|
587
|
+
target.terminationReason = source.terminationReason;
|
|
588
|
+
target.errorMessage = source.errorMessage;
|
|
589
|
+
target.exitCode = source.exitCode;
|
|
590
|
+
target.durationMs = source.durationMs || Date.now() - startedAt;
|
|
591
|
+
onChange(target);
|
|
572
592
|
}
|
|
573
593
|
|
|
574
594
|
async function runTask(options: RunTaskOptions): Promise<SubagentTaskResult> {
|
|
@@ -593,7 +613,6 @@ async function runTask(options: RunTaskOptions): Promise<SubagentTaskResult> {
|
|
|
593
613
|
}
|
|
594
614
|
|
|
595
615
|
let promptDirectory: string | undefined;
|
|
596
|
-
let child: SpawnedProcess | undefined;
|
|
597
616
|
try {
|
|
598
617
|
const prompt = await writeRolePrompt(agent);
|
|
599
618
|
promptDirectory = prompt.directory;
|
|
@@ -618,175 +637,26 @@ async function runTask(options: RunTaskOptions): Promise<SubagentTaskResult> {
|
|
|
618
637
|
"--append-system-prompt", prompt.filePath,
|
|
619
638
|
`Task: ${options.task}`,
|
|
620
639
|
];
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
if (errorMessage) result.errorMessage = errorMessage;
|
|
638
|
-
terminateProcess(child!, false);
|
|
639
|
-
forceTimer = setTimeout(() => {
|
|
640
|
-
if (closed) return;
|
|
641
|
-
terminateProcess(child!, true);
|
|
642
|
-
settleTimer = setTimeout(() => {
|
|
643
|
-
if (!closed) finish(null);
|
|
644
|
-
}, 1_000);
|
|
645
|
-
}, limits.killGraceMs);
|
|
646
|
-
};
|
|
647
|
-
|
|
648
|
-
const processLine = (line: string): void => {
|
|
649
|
-
if (!line.trim() || requestedStatus) return;
|
|
650
|
-
let event: any;
|
|
651
|
-
try {
|
|
652
|
-
event = JSON.parse(line);
|
|
653
|
-
} catch (error) {
|
|
654
|
-
malformedError = error instanceof Error ? error.message : String(error);
|
|
655
|
-
requestTermination("failed", "malformed_jsonl", `Malformed child JSONL: ${malformedError}`);
|
|
656
|
-
return;
|
|
657
|
-
}
|
|
658
|
-
if (event?.type === "message_end" && event.message?.role === "assistant") {
|
|
659
|
-
const message = event.message;
|
|
660
|
-
result.usage.turns += 1;
|
|
661
|
-
addUsage(result.usage, { ...message.usage, turns: 0 });
|
|
662
|
-
appendTrace(result, traceMessage(message), limits.traceBytes);
|
|
663
|
-
const output = textContent(message);
|
|
664
|
-
if (output) {
|
|
665
|
-
const capped = truncateUtf8(output, limits.taskOutputBytes);
|
|
666
|
-
result.output = capped.text;
|
|
667
|
-
result.outputTruncatedBytes = capped.omittedBytes;
|
|
668
|
-
}
|
|
669
|
-
if (typeof message.model === "string") result.model = message.provider ? `${message.provider}/${message.model}` : message.model;
|
|
670
|
-
const stopReason = typeof message.stopReason === "string" ? message.stopReason : undefined;
|
|
671
|
-
if (stopReason === "stop" || stopReason === "toolUse") {
|
|
672
|
-
result.terminationReason = undefined;
|
|
673
|
-
result.errorMessage = undefined;
|
|
674
|
-
} else if (stopReason) {
|
|
675
|
-
result.terminationReason = stopReason;
|
|
676
|
-
}
|
|
677
|
-
if (typeof message.errorMessage === "string") result.errorMessage = message.errorMessage;
|
|
678
|
-
if (stopReason === "length") requestTermination("limited", "model_output_limit");
|
|
679
|
-
if (result.usage.turns > agent.maxTurns || result.usage.turns >= agent.maxTurns && stopReason === "toolUse") {
|
|
680
|
-
requestTermination("limited", "turn_limit");
|
|
681
|
-
}
|
|
682
|
-
options.onChange(result);
|
|
683
|
-
} else if (event?.type === "agent_end" && event.willRetry === true) {
|
|
684
|
-
if (result.usage.turns >= agent.maxTurns) {
|
|
685
|
-
requestTermination("limited", "turn_limit");
|
|
686
|
-
options.onChange(result);
|
|
687
|
-
}
|
|
688
|
-
} else if (event?.type === "tool_result_end" && event.message) {
|
|
689
|
-
const toolName = typeof event.message.toolName === "string" ? event.message.toolName : "tool";
|
|
690
|
-
appendTrace(result, [`${toolName} result${event.message.isError ? " (error)" : ""}`], limits.traceBytes);
|
|
691
|
-
options.onChange(result);
|
|
692
|
-
}
|
|
693
|
-
};
|
|
694
|
-
|
|
695
|
-
const appendStdoutLine = (fragment: Buffer): boolean => {
|
|
696
|
-
const nextBytes = stdoutLineBytes + fragment.length;
|
|
697
|
-
if (nextBytes > limits.jsonlLineBytes) {
|
|
698
|
-
requestTermination("limited", "jsonl_line_limit", `Child JSONL line exceeds ${limits.jsonlLineBytes} bytes`);
|
|
699
|
-
return false;
|
|
700
|
-
}
|
|
701
|
-
if (nextBytes > stdoutLineBuffer.length) {
|
|
702
|
-
const nextCapacity = Math.min(limits.jsonlLineBytes, Math.max(nextBytes, stdoutLineBuffer.length * 2, 4_096));
|
|
703
|
-
const expanded = Buffer.allocUnsafe(nextCapacity);
|
|
704
|
-
stdoutLineBuffer.copy(expanded, 0, 0, stdoutLineBytes);
|
|
705
|
-
stdoutLineBuffer = expanded;
|
|
706
|
-
}
|
|
707
|
-
fragment.copy(stdoutLineBuffer, stdoutLineBytes);
|
|
708
|
-
stdoutLineBytes = nextBytes;
|
|
709
|
-
return true;
|
|
710
|
-
};
|
|
711
|
-
|
|
712
|
-
const processStdoutLine = (): void => {
|
|
713
|
-
const line = stdoutLineBuffer.toString("utf8", 0, stdoutLineBytes);
|
|
714
|
-
stdoutLineBuffer = Buffer.alloc(0);
|
|
715
|
-
stdoutLineBytes = 0;
|
|
716
|
-
processLine(line);
|
|
717
|
-
};
|
|
718
|
-
|
|
719
|
-
let finish!: (code: number | null) => void;
|
|
720
|
-
const closedPromise = new Promise<void>((resolve) => {
|
|
721
|
-
finish = (code: number | null): void => {
|
|
722
|
-
if (closed) return;
|
|
723
|
-
closed = true;
|
|
724
|
-
if (forceTimer) clearTimeout(forceTimer);
|
|
725
|
-
if (settleTimer) clearTimeout(settleTimer);
|
|
726
|
-
if (stdoutLineBytes > 0 && !requestedStatus) processStdoutLine();
|
|
727
|
-
result.exitCode = code;
|
|
728
|
-
if (requestedStatus) {
|
|
729
|
-
result.status = requestedStatus;
|
|
730
|
-
result.terminationReason = requestedReason;
|
|
731
|
-
} else if (result.terminationReason === "length") {
|
|
732
|
-
result.status = "limited";
|
|
733
|
-
result.terminationReason = "model_output_limit";
|
|
734
|
-
} else if (code !== 0 || result.errorMessage || result.terminationReason) {
|
|
735
|
-
result.status = "failed";
|
|
736
|
-
result.terminationReason ??= code === null ? "process_closed" : `exit_${code}`;
|
|
737
|
-
} else if (result.usage.turns === 0) {
|
|
738
|
-
result.status = "failed";
|
|
739
|
-
result.terminationReason = "missing_assistant_message";
|
|
740
|
-
result.errorMessage = "Child exited without an assistant response";
|
|
741
|
-
} else {
|
|
742
|
-
result.status = "complete";
|
|
743
|
-
result.terminationReason = "completed";
|
|
744
|
-
}
|
|
745
|
-
result.durationMs = Date.now() - startedAt;
|
|
746
|
-
options.onChange(result);
|
|
747
|
-
resolve();
|
|
748
|
-
};
|
|
749
|
-
});
|
|
750
|
-
|
|
751
|
-
child.stdout.on("data", (chunk: Buffer | string) => {
|
|
752
|
-
if (requestedStatus) return;
|
|
753
|
-
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
754
|
-
let offset = 0;
|
|
755
|
-
while (offset < buffer.length && !requestedStatus) {
|
|
756
|
-
const newline = buffer.indexOf(0x0a, offset);
|
|
757
|
-
const end = newline < 0 ? buffer.length : newline;
|
|
758
|
-
if (!appendStdoutLine(buffer.subarray(offset, end))) return;
|
|
759
|
-
if (newline < 0) return;
|
|
760
|
-
processStdoutLine();
|
|
761
|
-
offset = newline + 1;
|
|
762
|
-
}
|
|
640
|
+
const handle = runSubagentProcess({
|
|
641
|
+
args,
|
|
642
|
+
cwd: options.cwd,
|
|
643
|
+
signal: options.signal,
|
|
644
|
+
spawnProcess: options.spawnProcess,
|
|
645
|
+
limits: {
|
|
646
|
+
wallTimeMs: options.timeoutMs ?? agent.timeoutMs,
|
|
647
|
+
jsonlLineBytes: limits.jsonlLineBytes,
|
|
648
|
+
traceBytes: limits.traceBytes,
|
|
649
|
+
stderrBytes: limits.stderrBytes,
|
|
650
|
+
outputBytes: limits.taskOutputBytes,
|
|
651
|
+
quotaTokens: limits.quotaTokens,
|
|
652
|
+
quotaUsd: limits.quotaUsd,
|
|
653
|
+
killGraceMs: limits.killGraceMs,
|
|
654
|
+
},
|
|
655
|
+
onUpdate: (next) => applyProcessResult(result, next, startedAt, options.onChange),
|
|
763
656
|
});
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
const remaining = Math.max(0, limits.stderrBytes - rawStderrBytes);
|
|
768
|
-
rawStderrBytes += buffer.length;
|
|
769
|
-
if (remaining > 0) result.stderr += buffer.subarray(0, remaining).toString("utf8");
|
|
770
|
-
result.stderrTruncatedBytes = Math.max(0, rawStderrBytes - limits.stderrBytes);
|
|
771
|
-
if (rawStderrBytes > limits.stderrBytes) requestTermination("limited", "stderr_limit");
|
|
772
|
-
});
|
|
773
|
-
child.on("error", (error) => requestTermination("failed", "spawn_error", error.message));
|
|
774
|
-
child.once("close", finish);
|
|
775
|
-
|
|
776
|
-
const timeoutTimer = setTimeout(() => requestTermination("limited", "timeout"), agent.timeoutMs);
|
|
777
|
-
const abortHandler = (): void => requestTermination("cancelled", "abort");
|
|
778
|
-
if (options.signal?.aborted) abortHandler();
|
|
779
|
-
else options.signal?.addEventListener("abort", abortHandler, { once: true });
|
|
780
|
-
|
|
781
|
-
try {
|
|
782
|
-
await closedPromise;
|
|
783
|
-
} finally {
|
|
784
|
-
clearTimeout(timeoutTimer);
|
|
785
|
-
options.signal?.removeEventListener("abort", abortHandler);
|
|
786
|
-
}
|
|
787
|
-
if (!result.output && result.stderr && (result.status as SubagentStatus) !== "complete") {
|
|
788
|
-
result.errorMessage ??= result.stderr.trim();
|
|
789
|
-
}
|
|
657
|
+
options.onHandle?.(handle);
|
|
658
|
+
const final = await handle.result;
|
|
659
|
+
applyProcessResult(result, final, startedAt, options.onChange);
|
|
790
660
|
return result;
|
|
791
661
|
} catch (error) {
|
|
792
662
|
result.status = options.signal?.aborted ? "cancelled" : "failed";
|
|
@@ -830,6 +700,13 @@ const ChainTaskSchema = Type.Object({
|
|
|
830
700
|
});
|
|
831
701
|
|
|
832
702
|
const SubagentParams = Type.Object({
|
|
703
|
+
action: Type.Optional(StringEnum(["spawn", "list", "inspect", "steer", "interrupt", "collect", "close"] as const, {
|
|
704
|
+
default: "spawn",
|
|
705
|
+
description: "Thread lifecycle action",
|
|
706
|
+
})),
|
|
707
|
+
threadId: Type.Optional(Type.String({ minLength: 1, maxLength: 128, description: "Stable child thread ID" })),
|
|
708
|
+
message: Type.Optional(Type.String({ minLength: 1, maxLength: 4_000, description: "Bounded steering message" })),
|
|
709
|
+
all: Type.Optional(Type.Boolean({ description: "Interrupt every active child thread" })),
|
|
833
710
|
agent: Type.Optional(Type.String({ minLength: 1, maxLength: 64, description: "Agent role for single mode" })),
|
|
834
711
|
task: Type.Optional(Type.String({ minLength: 1, maxLength: SUBAGENT_LIMITS.taskCharacters, description: "Task for single mode" })),
|
|
835
712
|
tasks: Type.Optional(Type.Array(TaskSchema, { minItems: 1, maxItems: SUBAGENT_LIMITS.maxTasks, description: "Parallel role tasks" })),
|
|
@@ -851,16 +728,30 @@ function requestedAgents(params: { agent?: string; tasks?: TaskInput[]; chain?:
|
|
|
851
728
|
return (params.tasks ?? params.chain ?? []).map((task) => task.agent);
|
|
852
729
|
}
|
|
853
730
|
|
|
731
|
+
function clipCharacters(text: string, maxCharacters: number, fromEnd = false): string {
|
|
732
|
+
const characters = [...text];
|
|
733
|
+
if (characters.length <= maxCharacters) return text;
|
|
734
|
+
return (fromEnd ? characters.slice(-maxCharacters) : characters.slice(0, maxCharacters)).join("");
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function buildSteeredTask(task: string, steering: readonly string[], previousOutput: string | undefined, maxCharacters: number): string {
|
|
738
|
+
const steeringLabel = "\n\nParent steering:\n";
|
|
739
|
+
const steeringText = clipCharacters(steering.join("\n"), Math.max(0, maxCharacters - [...steeringLabel].length), true);
|
|
740
|
+
const previousLabel = previousOutput ? "\n\nPrevious child handoff:\n" : "";
|
|
741
|
+
const required = [...steeringLabel, ...steeringText, ...previousLabel].length;
|
|
742
|
+
const taskText = clipCharacters(task, Math.max(0, maxCharacters - required));
|
|
743
|
+
const previousText = previousOutput
|
|
744
|
+
? clipCharacters(previousOutput, Math.max(0, maxCharacters - [...taskText, ...steeringLabel, ...steeringText, ...previousLabel].length))
|
|
745
|
+
: "";
|
|
746
|
+
return `${taskText}${previousText ? `${previousLabel}${previousText}` : ""}${steeringLabel}${steeringText}`;
|
|
747
|
+
}
|
|
748
|
+
|
|
854
749
|
function formatUsage(usage: SubagentUsage): string {
|
|
855
750
|
const parts = [`${usage.turns} turn${usage.turns === 1 ? "" : "s"}`, `${usage.totalTokens} tokens`];
|
|
856
751
|
if (usage.cost.total) parts.push(`$${usage.cost.total.toFixed(4)}`);
|
|
857
752
|
return parts.join(" · ");
|
|
858
753
|
}
|
|
859
754
|
|
|
860
|
-
function taskSummary(result: SubagentTaskResult): string {
|
|
861
|
-
return `${result.id} · ${result.agent} · ${result.status} · ${formatUsage(result.usage)}`;
|
|
862
|
-
}
|
|
863
|
-
|
|
864
755
|
function buildToolContent(mode: SubagentDetails["mode"], results: SubagentTaskResult[], maxBytes: number): string {
|
|
865
756
|
const sections = results.map((result) => {
|
|
866
757
|
const heading = `### ${result.id} · ${result.agent} · ${result.status}`;
|
|
@@ -872,8 +763,7 @@ function buildToolContent(mode: SubagentDetails["mode"], results: SubagentTaskRe
|
|
|
872
763
|
const complete = results.filter((result) => result.status === "complete").length;
|
|
873
764
|
const text = `${mode}: ${complete}/${results.length} complete · ${formatUsage(aggregateUsage(results))}\n\n${sections.join("\n\n---\n\n")}`;
|
|
874
765
|
const marker = "\n\n[Combined subagent output truncated to 50 KiB; inspect the expanded tool result for bounded per-task details.]";
|
|
875
|
-
|
|
876
|
-
return capped.omittedBytes ? `${capped.text}${marker}` : text;
|
|
766
|
+
return boundedText(text, maxBytes, marker);
|
|
877
767
|
}
|
|
878
768
|
|
|
879
769
|
function statusColor(status: SubagentStatus): "accent" | "success" | "error" | "warning" | "muted" {
|
|
@@ -892,25 +782,321 @@ function statusIcon(status: SubagentStatus): string {
|
|
|
892
782
|
return "!";
|
|
893
783
|
}
|
|
894
784
|
|
|
785
|
+
interface ActiveThreadRuntime {
|
|
786
|
+
controller: AbortController;
|
|
787
|
+
handle?: SubagentProcessHandle;
|
|
788
|
+
steering: string[];
|
|
789
|
+
restarting: boolean;
|
|
790
|
+
traceCount: number;
|
|
791
|
+
startedAt: number;
|
|
792
|
+
aggregate?: SubagentTaskResult;
|
|
793
|
+
requestedReason?: string;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function parentThreadId(ctx: ExtensionContext): string {
|
|
797
|
+
try {
|
|
798
|
+
const id = ctx.sessionManager?.getSessionId?.();
|
|
799
|
+
if (id) return `main:${id}`;
|
|
800
|
+
} catch {
|
|
801
|
+
// Test and RPC contexts may not expose a session manager.
|
|
802
|
+
}
|
|
803
|
+
return "main";
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function threadCapabilityBoundary(agent: AgentRole): {
|
|
807
|
+
filesystem: "read" | "write";
|
|
808
|
+
network: "none" | "read";
|
|
809
|
+
process: "none" | "limited";
|
|
810
|
+
childThreads: false;
|
|
811
|
+
} {
|
|
812
|
+
return {
|
|
813
|
+
filesystem: agent.access,
|
|
814
|
+
network: agent.tools.some((tool) => WEB_TOOLS.has(tool)) ? "read" : "none",
|
|
815
|
+
process: agent.tools.includes("bash") ? "limited" : "none",
|
|
816
|
+
childThreads: false,
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function threadUsage(usage: SubagentUsage): SubagentThread["usage"] {
|
|
821
|
+
return {
|
|
822
|
+
inputTokens: usage.input,
|
|
823
|
+
outputTokens: usage.output,
|
|
824
|
+
cacheReadTokens: usage.cacheRead,
|
|
825
|
+
cacheWriteTokens: usage.cacheWrite,
|
|
826
|
+
totalTokens: usage.totalTokens,
|
|
827
|
+
costUsd: usage.cost.total,
|
|
828
|
+
turns: usage.turns,
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function legacyStatus(state: SubagentThreadState): SubagentStatus {
|
|
833
|
+
if (state === "active") return "running";
|
|
834
|
+
if (state === "done" || state === "closed") return "complete";
|
|
835
|
+
if (state === "failed") return "failed";
|
|
836
|
+
if (state === "stopped") return "cancelled";
|
|
837
|
+
return "queued";
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
function threadResult(thread: SubagentThread, source?: SubagentTaskResult): SubagentTaskResult {
|
|
841
|
+
if (source) {
|
|
842
|
+
const result = cloneResult(source);
|
|
843
|
+
if (thread.state === "queued") result.status = "queued";
|
|
844
|
+
else if (thread.state === "active") result.status = "running";
|
|
845
|
+
return result;
|
|
846
|
+
}
|
|
847
|
+
return {
|
|
848
|
+
...makeQueuedResult(thread.id, thread.role, thread.prompt),
|
|
849
|
+
status: legacyStatus(thread.state),
|
|
850
|
+
agentSource: "unknown",
|
|
851
|
+
model: thread.model,
|
|
852
|
+
tools: [...thread.tools],
|
|
853
|
+
trace: thread.trace.map((event) => event.message ?? event.kind),
|
|
854
|
+
usage: {
|
|
855
|
+
input: thread.usage.inputTokens,
|
|
856
|
+
output: thread.usage.outputTokens,
|
|
857
|
+
cacheRead: thread.usage.cacheReadTokens,
|
|
858
|
+
cacheWrite: thread.usage.cacheWriteTokens,
|
|
859
|
+
totalTokens: thread.usage.totalTokens,
|
|
860
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: thread.usage.costUsd },
|
|
861
|
+
turns: thread.usage.turns,
|
|
862
|
+
},
|
|
863
|
+
output: thread.result ?? "",
|
|
864
|
+
terminationReason: thread.stopReason,
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
function threadBoardRecord(result: SubagentTaskResult): ThreadBoardRecord {
|
|
869
|
+
return {
|
|
870
|
+
id: result.id,
|
|
871
|
+
agent: result.agent,
|
|
872
|
+
task: result.task,
|
|
873
|
+
status: result.status,
|
|
874
|
+
usage: {
|
|
875
|
+
input: result.usage.input,
|
|
876
|
+
output: result.usage.output,
|
|
877
|
+
cacheRead: result.usage.cacheRead,
|
|
878
|
+
cacheWrite: result.usage.cacheWrite,
|
|
879
|
+
totalTokens: result.usage.totalTokens,
|
|
880
|
+
turns: result.usage.turns,
|
|
881
|
+
cost: result.usage.cost.total,
|
|
882
|
+
},
|
|
883
|
+
trace: result.trace,
|
|
884
|
+
traceTruncatedBytes: result.traceTruncatedBytes,
|
|
885
|
+
handoff: result.output,
|
|
886
|
+
output: result.output,
|
|
887
|
+
terminationReason: result.terminationReason,
|
|
888
|
+
errorMessage: result.errorMessage,
|
|
889
|
+
durationMs: result.durationMs,
|
|
890
|
+
step: result.step,
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
|
|
895
894
|
export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeOptions = {}): void {
|
|
896
895
|
const limits = { ...SUBAGENT_LIMITS, ...options.limits };
|
|
897
896
|
const spawnProcess = options.spawnProcess ?? defaultSpawnProcess;
|
|
897
|
+
const threads = new SubagentThreadRegistry();
|
|
898
|
+
const activeRuntimes = new Map<string, ActiveThreadRuntime>();
|
|
899
|
+
const savedResults = new Map<string, SubagentTaskResult>();
|
|
900
|
+
|
|
901
|
+
const detailsFor = (
|
|
902
|
+
parentId: string,
|
|
903
|
+
mode: SubagentDetails["mode"] = "single",
|
|
904
|
+
scope: AgentScope = "user",
|
|
905
|
+
projectAgentsDir: string | null = null,
|
|
906
|
+
selectedThreadId?: string,
|
|
907
|
+
): SubagentDetails => {
|
|
908
|
+
const all = threads.listAll().filter((thread) => thread.parentId === parentId);
|
|
909
|
+
const visible = all.filter((thread) => thread.state !== "closed");
|
|
910
|
+
const results = visible.map((thread) => threadResult(thread, savedResults.get(thread.id)));
|
|
911
|
+
return {
|
|
912
|
+
...cloneDetails(mode, scope, projectAgentsDir, results),
|
|
913
|
+
parentId,
|
|
914
|
+
threads: all,
|
|
915
|
+
activeThreads: all.filter((thread) => thread.state === "active"),
|
|
916
|
+
doneThreads: all.filter((thread) => ["done", "failed", "stopped"].includes(thread.state)),
|
|
917
|
+
selectedThreadId,
|
|
918
|
+
};
|
|
919
|
+
};
|
|
920
|
+
|
|
921
|
+
const threadBoardText = (parentId: string, selectedThreadId?: string): string => {
|
|
922
|
+
const details = detailsFor(parentId, "single", "user", null, selectedThreadId);
|
|
923
|
+
const active = details.activeThreads ?? [];
|
|
924
|
+
const done = details.doneThreads ?? [];
|
|
925
|
+
const row = (thread: SubagentThread): string => `- ${thread.id} · ${thread.role} · ${thread.state} · ${thread.prompt}`;
|
|
926
|
+
const lines = [
|
|
927
|
+
`parent ${parentId}`,
|
|
928
|
+
`Active (${active.length})`,
|
|
929
|
+
...(active.length ? active.map(row) : ["- none"]),
|
|
930
|
+
`Done (${done.length})`,
|
|
931
|
+
...(done.length ? done.map(row) : ["- none"]),
|
|
932
|
+
"Controls: inspect · steer · interrupt · collect · close",
|
|
933
|
+
];
|
|
934
|
+
if (selectedThreadId) {
|
|
935
|
+
const selected = details.threads?.find((thread) => thread.id === selectedThreadId);
|
|
936
|
+
if (selected) {
|
|
937
|
+
lines.push(`Inspect ${selected.id}: ${selected.state}`);
|
|
938
|
+
lines.push(`Role: ${selected.role}`);
|
|
939
|
+
lines.push(`Model: ${selected.model}`);
|
|
940
|
+
lines.push(`Tools: ${selected.tools.join(", ")}`);
|
|
941
|
+
lines.push(`Trace: ${selected.trace.length} entries`);
|
|
942
|
+
if (selected.result) lines.push(`Handoff: ${selected.result}`);
|
|
943
|
+
if (selected.stopReason) lines.push(`Reason: ${selected.stopReason}`);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
return boundedText(lines.join("\n"), limits.toolOutputBytes, "\n\n[Thread board truncated; inspect a child thread for its bounded detail.]");
|
|
947
|
+
};
|
|
948
|
+
|
|
949
|
+
const syncThread = (threadId: SubagentThreadId, next: SubagentTaskResult, runtime?: ActiveThreadRuntime): SubagentTaskResult => {
|
|
950
|
+
const effective = mergeTaskResults(runtime?.aggregate, next, limits.traceBytes);
|
|
951
|
+
if (runtime?.requestedReason && next.status === "cancelled") effective.terminationReason = runtime.requestedReason;
|
|
952
|
+
savedResults.set(threadId, cloneResult(effective));
|
|
953
|
+
let thread = threads.inspect(threadId);
|
|
954
|
+
if (!thread || threads.isDisposed) return effective;
|
|
955
|
+
if (thread.state === "queued" && next.status === "running") {
|
|
956
|
+
thread = threads.begin(threadId);
|
|
957
|
+
}
|
|
958
|
+
if (thread.state !== "active") return effective;
|
|
959
|
+
if (runtime && next.trace.length < runtime.traceCount) runtime.traceCount = 0;
|
|
960
|
+
const from = runtime?.traceCount ?? 0;
|
|
961
|
+
let retainedTraceBytes = thread.trace.reduce((total, entry) => total + Buffer.byteLength(entry.message ?? "", "utf8"), 0);
|
|
962
|
+
for (const entry of next.trace.slice(from)) {
|
|
963
|
+
const retained = truncateUtf8(entry, Math.max(0, limits.traceBytes - retainedTraceBytes));
|
|
964
|
+
if (retained.text) {
|
|
965
|
+
threads.trace(threadId, { kind: "child", message: retained.text });
|
|
966
|
+
retainedTraceBytes += Buffer.byteLength(retained.text, "utf8");
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
if (runtime) runtime.traceCount = next.trace.length;
|
|
970
|
+
const handoff = effective.output ? { summary: effective.output } : undefined;
|
|
971
|
+
thread = threads.patch(threadId, { usage: threadUsage(effective.usage), result: effective.output || undefined, handoff });
|
|
972
|
+
const restartPending = runtime?.restarting === true && ["cancelled", "complete"].includes(next.status);
|
|
973
|
+
if (restartPending) return effective;
|
|
974
|
+
if (effective.status === "complete") {
|
|
975
|
+
threads.complete(threadId, { usage: threadUsage(effective.usage), result: effective.output || undefined, handoff });
|
|
976
|
+
} else if (effective.status === "failed") {
|
|
977
|
+
threads.fail(threadId, {
|
|
978
|
+
usage: threadUsage(effective.usage),
|
|
979
|
+
result: effective.output || undefined,
|
|
980
|
+
handoff,
|
|
981
|
+
message: effective.errorMessage ?? effective.terminationReason ?? "child failed",
|
|
982
|
+
code: effective.terminationReason,
|
|
983
|
+
});
|
|
984
|
+
} else if (effective.status === "cancelled" || effective.status === "limited") {
|
|
985
|
+
threads.stop(threadId, {
|
|
986
|
+
usage: threadUsage(effective.usage),
|
|
987
|
+
result: effective.output || undefined,
|
|
988
|
+
handoff,
|
|
989
|
+
reason: effective.terminationReason ?? (effective.status === "limited" ? "resource_limit" : "interrupted"),
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
return effective;
|
|
993
|
+
};
|
|
994
|
+
|
|
995
|
+
if (typeof pi.on === "function") {
|
|
996
|
+
pi.on("session_shutdown", () => {
|
|
997
|
+
for (const runtime of activeRuntimes.values()) {
|
|
998
|
+
runtime.restarting = false;
|
|
999
|
+
runtime.requestedReason = "session_shutdown";
|
|
1000
|
+
runtime.handle?.stop("session_shutdown");
|
|
1001
|
+
runtime.controller.abort();
|
|
1002
|
+
}
|
|
1003
|
+
threads.dispose();
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
898
1006
|
|
|
899
1007
|
pi.registerTool({
|
|
900
1008
|
name: "subagent",
|
|
901
1009
|
label: "Subagents",
|
|
902
|
-
description: "
|
|
1010
|
+
description: "Spawn and manage named child threads. Children finish naturally; time, output, trace, stderr, quota, task count, and concurrency are the hard edges. Use action list, inspect, steer, interrupt, collect, and close to manage active and completed handoffs.",
|
|
903
1011
|
promptSnippet: "Delegate bounded specialist work to isolated KillerOS subagents",
|
|
904
1012
|
promptGuidelines: [
|
|
905
1013
|
"Use subagent for clearly separable specialist work; prefer read-only scout, planner, reviewer, or security roles before a writer.",
|
|
906
1014
|
"Do not request multiple write-capable subagents in one parallel batch.",
|
|
907
1015
|
"Every child can load relevant skills with read and can use web_search, source_check, fetch_content, and get_search_content for external research.",
|
|
908
1016
|
"When the user names a model or thinking effort, pass model and thinking separately; use inherit when the active parent or role setting should decide.",
|
|
1017
|
+
"Keep completed and stopped threads inspectable until the parent explicitly closes them.",
|
|
909
1018
|
],
|
|
910
1019
|
parameters: SubagentParams,
|
|
911
|
-
executionMode: "
|
|
1020
|
+
executionMode: "parallel",
|
|
912
1021
|
|
|
913
1022
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
1023
|
+
const action = params.action ?? "spawn";
|
|
1024
|
+
const parentId = parentThreadId(ctx);
|
|
1025
|
+
const actionDetails = (selectedThreadId?: string): SubagentDetails => detailsFor(parentId, "single", params.agentScope ?? "user", null, selectedThreadId);
|
|
1026
|
+
const actionResult = (text: string, selectedThreadId?: string) => {
|
|
1027
|
+
const details = actionDetails(selectedThreadId);
|
|
1028
|
+
return {
|
|
1029
|
+
content: [{ type: "text" as const, text: boundedText(text, limits.toolOutputBytes, "\n\n[Thread action output truncated.]") }],
|
|
1030
|
+
details,
|
|
1031
|
+
usage: details.aggregateUsage,
|
|
1032
|
+
};
|
|
1033
|
+
};
|
|
1034
|
+
|
|
1035
|
+
if (action === "list") return actionResult(threadBoardText(parentId));
|
|
1036
|
+
if (action === "inspect") {
|
|
1037
|
+
if (!params.threadId) throw new Error("inspect requires threadId");
|
|
1038
|
+
const thread = threads.inspect(params.threadId as SubagentThreadId);
|
|
1039
|
+
if (!thread || thread.parentId !== parentId) throw new Error(`Unknown child thread ${JSON.stringify(params.threadId)}`);
|
|
1040
|
+
return actionResult(threadBoardText(parentId, params.threadId), params.threadId);
|
|
1041
|
+
}
|
|
1042
|
+
if (action === "steer") {
|
|
1043
|
+
if (!params.threadId || !params.message) throw new Error("steer requires threadId and message");
|
|
1044
|
+
const threadId = params.threadId as SubagentThreadId;
|
|
1045
|
+
const thread = threads.inspect(threadId);
|
|
1046
|
+
if (!thread || thread.parentId !== parentId) throw new Error(`Unknown child thread ${JSON.stringify(params.threadId)}`);
|
|
1047
|
+
threads.steer(threadId, params.message);
|
|
1048
|
+
const runtime = activeRuntimes.get(params.threadId);
|
|
1049
|
+
if (runtime) {
|
|
1050
|
+
runtime.steering.push(params.message);
|
|
1051
|
+
if (runtime.steering.length > MAX_RUNTIME_STEERING_MESSAGES) runtime.steering.splice(0, runtime.steering.length - MAX_RUNTIME_STEERING_MESSAGES);
|
|
1052
|
+
runtime.restarting = true;
|
|
1053
|
+
runtime.requestedReason = "steer";
|
|
1054
|
+
runtime.handle?.stop("steer");
|
|
1055
|
+
}
|
|
1056
|
+
return actionResult(`Steering queued for ${params.threadId}. The child keeps the same thread and handoff record.`, params.threadId);
|
|
1057
|
+
}
|
|
1058
|
+
if (action === "interrupt") {
|
|
1059
|
+
if (!params.all && !params.threadId) throw new Error("interrupt requires threadId or all=true");
|
|
1060
|
+
let targets: SubagentThread[];
|
|
1061
|
+
if (params.all) {
|
|
1062
|
+
targets = threads.listActive().filter((thread) => thread.parentId === parentId);
|
|
1063
|
+
} else {
|
|
1064
|
+
const target = threads.inspect(params.threadId as SubagentThreadId);
|
|
1065
|
+
if (!target || target.parentId !== parentId) throw new Error(`Unknown child thread ${JSON.stringify(params.threadId)}`);
|
|
1066
|
+
if (target.state !== "active" && target.state !== "queued") {
|
|
1067
|
+
throw new Error(`Cannot interrupt thread ${params.threadId} from ${target.state}`);
|
|
1068
|
+
}
|
|
1069
|
+
targets = [target];
|
|
1070
|
+
}
|
|
1071
|
+
for (const thread of targets) {
|
|
1072
|
+
if (thread.parentId !== parentId) continue;
|
|
1073
|
+
const runtime = activeRuntimes.get(thread.id);
|
|
1074
|
+
if (runtime) {
|
|
1075
|
+
runtime.restarting = false;
|
|
1076
|
+
runtime.requestedReason = "interrupt";
|
|
1077
|
+
runtime.handle?.stop("interrupt");
|
|
1078
|
+
runtime.controller.abort();
|
|
1079
|
+
} else if (thread.state === "active" || thread.state === "queued") {
|
|
1080
|
+
threads.stop(thread.id, { reason: "interrupt" });
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
return actionResult(params.all ? "Interrupt requested for all active child threads." : `Interrupt requested for ${params.threadId}.`);
|
|
1084
|
+
}
|
|
1085
|
+
if (action === "collect") {
|
|
1086
|
+
if (!params.threadId) throw new Error("collect requires threadId");
|
|
1087
|
+
const thread = threads.inspect(params.threadId as SubagentThreadId);
|
|
1088
|
+
if (!thread || thread.parentId !== parentId) throw new Error(`Unknown child thread ${JSON.stringify(params.threadId)}`);
|
|
1089
|
+
const collected = threads.collect(params.threadId as SubagentThreadId);
|
|
1090
|
+
return actionResult(`Collected ${params.threadId}: ${collected.result ?? collected.failure?.message ?? collected.stopReason ?? "no handoff"}`, params.threadId);
|
|
1091
|
+
}
|
|
1092
|
+
if (action === "close") {
|
|
1093
|
+
if (!params.threadId) throw new Error("close requires threadId");
|
|
1094
|
+
const thread = threads.inspect(params.threadId as SubagentThreadId);
|
|
1095
|
+
if (!thread || thread.parentId !== parentId) throw new Error(`Unknown child thread ${JSON.stringify(params.threadId)}`);
|
|
1096
|
+
threads.close(params.threadId as SubagentThreadId);
|
|
1097
|
+
return actionResult(`Closed ${params.threadId}. Its result record remains inspectable.`, params.threadId);
|
|
1098
|
+
}
|
|
1099
|
+
|
|
914
1100
|
const scope: AgentScope = params.agentScope ?? "user";
|
|
915
1101
|
const hasSingleFields = params.agent !== undefined || params.task !== undefined;
|
|
916
1102
|
const hasParallel = params.tasks !== undefined;
|
|
@@ -958,22 +1144,56 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
958
1144
|
if (writers.length > 1) throw new Error("Parallel batches may contain at most one write-capable subagent; writers are serialized");
|
|
959
1145
|
}
|
|
960
1146
|
|
|
961
|
-
const
|
|
962
|
-
|
|
963
|
-
|
|
1147
|
+
const inFlight = threads.listAll().filter((thread) => ["queued", "active"].includes(thread.state)).length;
|
|
1148
|
+
if (inFlight + inputs.length > limits.maxTasks) {
|
|
1149
|
+
throw new Error(`At most ${limits.maxTasks} child threads may be active at once`);
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
const threadRecords = inputs.map((input, index) => threads.spawn({
|
|
1153
|
+
parentId: parentId as SubagentThreadId,
|
|
1154
|
+
role: input.agent,
|
|
1155
|
+
prompt: input.task,
|
|
1156
|
+
model: resolvedModels.get(input.agent)!.model,
|
|
1157
|
+
tools: roles.get(input.agent)!.tools,
|
|
1158
|
+
capabilityBoundary: threadCapabilityBoundary(roles.get(input.agent)!),
|
|
1159
|
+
}));
|
|
1160
|
+
const results = threadRecords.map((thread, index) => {
|
|
1161
|
+
return makeQueuedResult(thread.id, inputs[index]!.agent, inputs[index]!.task, hasChain ? index + 1 : undefined);
|
|
1162
|
+
});
|
|
964
1163
|
const emit = (message = `${mode}: ${results.filter((result) => !["queued", "running"].includes(result.status)).length}/${results.length} settled`): void => {
|
|
1164
|
+
const board = detailsFor(parentId, mode, scope, discovery.projectAgentsDir);
|
|
1165
|
+
const currentResults = results.map(cloneResult);
|
|
965
1166
|
(onUpdate as ToolUpdate | undefined)?.({
|
|
966
1167
|
content: [{ type: "text", text: message }],
|
|
967
|
-
details:
|
|
1168
|
+
details: { ...board, results: currentResults, aggregateUsage: aggregateUsage(currentResults) },
|
|
968
1169
|
});
|
|
969
1170
|
};
|
|
970
1171
|
const runAt = async (index: number, task: string): Promise<void> => {
|
|
1172
|
+
const threadId = threadRecords[index]!.id;
|
|
1173
|
+
const initialThread = threads.inspect(threadId);
|
|
971
1174
|
if (signal?.aborted) {
|
|
972
1175
|
results[index] = { ...results[index]!, status: "cancelled", terminationReason: "abort" };
|
|
1176
|
+
if (initialThread?.state === "queued" || initialThread?.state === "active") threads.stop(threadId, { reason: "abort" });
|
|
1177
|
+
savedResults.set(threadId, cloneResult(results[index]!));
|
|
1178
|
+
emit();
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
if (!initialThread) return;
|
|
1182
|
+
if (initialThread.state === "closed") {
|
|
1183
|
+
results[index] = { ...results[index]!, status: "cancelled", terminationReason: initialThread.stopReason ?? "disposed" };
|
|
1184
|
+
savedResults.set(threadId, cloneResult(results[index]!));
|
|
973
1185
|
emit();
|
|
974
1186
|
return;
|
|
975
1187
|
}
|
|
1188
|
+
if (initialThread.state === "stopped") {
|
|
1189
|
+
results[index] = { ...results[index]!, status: "cancelled", terminationReason: initialThread.stopReason ?? "interrupted" };
|
|
1190
|
+
savedResults.set(threadId, cloneResult(results[index]!));
|
|
1191
|
+
emit();
|
|
1192
|
+
return;
|
|
1193
|
+
}
|
|
1194
|
+
if (initialThread.state !== "queued") return;
|
|
976
1195
|
const input = inputs[index]!;
|
|
1196
|
+
threads.begin(threadId);
|
|
977
1197
|
if ([...task].length > limits.taskCharacters) {
|
|
978
1198
|
results[index] = {
|
|
979
1199
|
...results[index]!,
|
|
@@ -981,26 +1201,124 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
981
1201
|
terminationReason: "task_limit",
|
|
982
1202
|
errorMessage: `Expanded task exceeds ${limits.taskCharacters} characters`,
|
|
983
1203
|
};
|
|
1204
|
+
threads.fail(threadId, { message: results[index]!.errorMessage ?? "Expanded task exceeds the task limit", code: "task_limit" });
|
|
1205
|
+
savedResults.set(threadId, cloneResult(results[index]!));
|
|
984
1206
|
emit();
|
|
985
1207
|
return;
|
|
986
1208
|
}
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1209
|
+
const controller = new AbortController();
|
|
1210
|
+
const abortParent = (): void => controller.abort();
|
|
1211
|
+
if (signal) {
|
|
1212
|
+
if (signal.aborted) controller.abort();
|
|
1213
|
+
else signal.addEventListener("abort", abortParent, { once: true });
|
|
1214
|
+
}
|
|
1215
|
+
const runtime: ActiveThreadRuntime = { controller, steering: [], restarting: false, traceCount: 0, startedAt: Date.now() };
|
|
1216
|
+
activeRuntimes.set(threadId, runtime);
|
|
1217
|
+
const agent = roles.get(input.agent)!;
|
|
1218
|
+
const queuedSteering = initialThread.steering.map((entry) => entry.message);
|
|
1219
|
+
let currentTask = queuedSteering.length ? buildSteeredTask(task, queuedSteering, undefined, limits.taskCharacters) : task;
|
|
1220
|
+
const stopForBudget = (reason: string, message: string): void => {
|
|
1221
|
+
const limited = cloneResult(runtime.aggregate ?? results[index]!);
|
|
1222
|
+
limited.status = "limited";
|
|
1223
|
+
limited.terminationReason = reason;
|
|
1224
|
+
limited.errorMessage = message;
|
|
1225
|
+
runtime.aggregate = limited;
|
|
1226
|
+
results[index] = cloneResult(limited);
|
|
1227
|
+
savedResults.set(threadId, cloneResult(limited));
|
|
1228
|
+
if (!threads.isDisposed && threads.inspect(threadId)?.state === "active") {
|
|
1229
|
+
threads.stop(threadId, {
|
|
1230
|
+
usage: threadUsage(limited.usage),
|
|
1231
|
+
result: limited.output || undefined,
|
|
1232
|
+
handoff: limited.output ? { summary: limited.output } : undefined,
|
|
1233
|
+
reason,
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
emit();
|
|
1237
|
+
};
|
|
1238
|
+
try {
|
|
1239
|
+
while (true) {
|
|
1240
|
+
const aggregate = runtime.aggregate;
|
|
1241
|
+
const remainingWallTimeMs = agent.timeoutMs - (Date.now() - runtime.startedAt);
|
|
1242
|
+
const usedTraceBytes = aggregate?.traceBytes ?? 0;
|
|
1243
|
+
const usedStderrBytes = aggregate?.stderrBytes ?? 0;
|
|
1244
|
+
const usedOutputBytes = aggregate?.outputBytes ?? 0;
|
|
1245
|
+
const usedTokens = aggregate?.usage.totalTokens ?? 0;
|
|
1246
|
+
const usedCost = aggregate?.usage.cost.total ?? 0;
|
|
1247
|
+
if (remainingWallTimeMs <= 0) {
|
|
1248
|
+
stopForBudget("wall_time_limit", `Child thread exceeds ${agent.timeoutMs} ms`);
|
|
1249
|
+
break;
|
|
1250
|
+
}
|
|
1251
|
+
if (usedTraceBytes >= limits.traceBytes || aggregate?.traceTruncatedBytes) {
|
|
1252
|
+
stopForBudget("trace_limit", `Child thread retains more than ${limits.traceBytes} trace bytes`);
|
|
1253
|
+
break;
|
|
1254
|
+
}
|
|
1255
|
+
if (usedStderrBytes >= limits.stderrBytes || aggregate?.stderrTruncatedBytes) {
|
|
1256
|
+
stopForBudget("stderr_limit", `Child thread emits more than ${limits.stderrBytes} stderr bytes`);
|
|
1257
|
+
break;
|
|
1258
|
+
}
|
|
1259
|
+
if (usedOutputBytes >= limits.taskOutputBytes || aggregate?.outputTruncatedBytes) {
|
|
1260
|
+
stopForBudget("output_limit", `Child thread emits more than ${limits.taskOutputBytes} output bytes`);
|
|
1261
|
+
break;
|
|
1262
|
+
}
|
|
1263
|
+
if (usedTokens >= limits.quotaTokens) {
|
|
1264
|
+
stopForBudget("quota_tokens", `Child thread exceeds ${limits.quotaTokens} tokens`);
|
|
1265
|
+
break;
|
|
1266
|
+
}
|
|
1267
|
+
if (usedCost >= limits.quotaUsd) {
|
|
1268
|
+
stopForBudget("quota_cost", `Child thread exceeds $${limits.quotaUsd}`);
|
|
1269
|
+
break;
|
|
1270
|
+
}
|
|
1271
|
+
runtime.traceCount = 0;
|
|
1272
|
+
const next = await runTask({
|
|
1273
|
+
cwd: ctx.cwd,
|
|
1274
|
+
agent: roles.get(input.agent)!,
|
|
1275
|
+
task: currentTask,
|
|
1276
|
+
id: results[index]!.id,
|
|
1277
|
+
step: results[index]!.step,
|
|
1278
|
+
model: resolvedModels.get(input.agent)!,
|
|
1279
|
+
signal: controller.signal,
|
|
1280
|
+
webExtension: options.webExtension,
|
|
1281
|
+
projectTrusted: ctx.isProjectTrusted(),
|
|
1282
|
+
spawnProcess,
|
|
1283
|
+
limits: {
|
|
1284
|
+
...limits,
|
|
1285
|
+
traceBytes: limits.traceBytes - usedTraceBytes,
|
|
1286
|
+
stderrBytes: limits.stderrBytes - usedStderrBytes,
|
|
1287
|
+
taskOutputBytes: limits.taskOutputBytes - usedOutputBytes,
|
|
1288
|
+
quotaTokens: limits.quotaTokens - usedTokens,
|
|
1289
|
+
quotaUsd: limits.quotaUsd - usedCost,
|
|
1290
|
+
},
|
|
1291
|
+
timeoutMs: remainingWallTimeMs,
|
|
1292
|
+
onHandle: (handle) => { runtime.handle = handle; },
|
|
1293
|
+
onChange: (changed) => {
|
|
1294
|
+
results[index] = syncThread(threadId, changed, runtime);
|
|
1295
|
+
emit();
|
|
1296
|
+
},
|
|
1297
|
+
});
|
|
1298
|
+
next.task = task;
|
|
1299
|
+
runtime.aggregate = mergeTaskResults(runtime.aggregate, next, limits.traceBytes);
|
|
1300
|
+
runtime.aggregate.task = task;
|
|
1301
|
+
results[index] = cloneResult(runtime.aggregate);
|
|
1302
|
+
savedResults.set(threadId, cloneResult(runtime.aggregate));
|
|
1303
|
+
const shouldRestart = runtime.steering.length > 0 && !controller.signal.aborted && (runtime.restarting || next.status === "complete" || next.status === "cancelled");
|
|
1304
|
+
if (!shouldRestart) break;
|
|
1305
|
+
const steering = runtime.steering.splice(0);
|
|
1306
|
+
runtime.restarting = false;
|
|
1307
|
+
runtime.requestedReason = undefined;
|
|
1308
|
+
if (!threads.isDisposed && threads.inspect(threadId)?.state === "active") {
|
|
1309
|
+
threads.patch(threadId, {
|
|
1310
|
+
usage: threadUsage(runtime.aggregate.usage),
|
|
1311
|
+
result: runtime.aggregate.output || undefined,
|
|
1312
|
+
handoff: runtime.aggregate.output ? { summary: runtime.aggregate.output } : undefined,
|
|
1313
|
+
});
|
|
1314
|
+
}
|
|
1315
|
+
currentTask = buildSteeredTask(task, steering, runtime.aggregate.output, limits.taskCharacters);
|
|
1316
|
+
}
|
|
1317
|
+
} finally {
|
|
1318
|
+
activeRuntimes.delete(threadId);
|
|
1319
|
+
signal?.removeEventListener("abort", abortParent);
|
|
1320
|
+
}
|
|
1321
|
+
emit();
|
|
1004
1322
|
};
|
|
1005
1323
|
|
|
1006
1324
|
emit(`${mode}: ${results.length} queued`);
|
|
@@ -1012,10 +1330,19 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1012
1330
|
if (results[index]!.status !== "complete") break;
|
|
1013
1331
|
previous = results[index]!.output;
|
|
1014
1332
|
}
|
|
1015
|
-
for (
|
|
1333
|
+
for (let index = 0; index < results.length; index += 1) {
|
|
1334
|
+
const result = results[index]!;
|
|
1016
1335
|
if (result.status === "queued") {
|
|
1017
|
-
|
|
1018
|
-
|
|
1336
|
+
const thread = threads.inspect(threadRecords[index]!.id);
|
|
1337
|
+
const alreadyStopped = thread?.state === "stopped";
|
|
1338
|
+
result.status = signal?.aborted || alreadyStopped ? "cancelled" : "failed";
|
|
1339
|
+
result.terminationReason = alreadyStopped
|
|
1340
|
+
? thread.stopReason ?? "interrupted"
|
|
1341
|
+
: signal?.aborted ? "abort" : "chain_stopped";
|
|
1342
|
+
if (thread?.state === "queued" || thread?.state === "active") {
|
|
1343
|
+
threads.stop(threadRecords[index]!.id, { reason: result.terminationReason });
|
|
1344
|
+
}
|
|
1345
|
+
savedResults.set(threadRecords[index]!.id, cloneResult(result));
|
|
1019
1346
|
}
|
|
1020
1347
|
}
|
|
1021
1348
|
} else if (hasParallel) {
|
|
@@ -1027,7 +1354,9 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1027
1354
|
await runAt(0, inputs[0]!.task);
|
|
1028
1355
|
}
|
|
1029
1356
|
|
|
1030
|
-
const
|
|
1357
|
+
const board = detailsFor(parentId, mode, scope, discovery.projectAgentsDir);
|
|
1358
|
+
const currentResults = results.map(cloneResult);
|
|
1359
|
+
const details: SubagentDetails = { ...board, results: currentResults, aggregateUsage: aggregateUsage(currentResults) };
|
|
1031
1360
|
return {
|
|
1032
1361
|
content: [{ type: "text", text: buildToolContent(mode, details.results, limits.toolOutputBytes) }],
|
|
1033
1362
|
details,
|
|
@@ -1037,6 +1366,7 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1037
1366
|
|
|
1038
1367
|
renderCall(args, theme) {
|
|
1039
1368
|
const scope = args.agentScope ?? "user";
|
|
1369
|
+
if (args.action && args.action !== "spawn") return new Text(`${theme.fg("toolTitle", theme.bold("threads "))}${theme.fg("accent", args.action)}${theme.fg("dim", args.threadId ? ` · ${args.threadId}` : "")}`, 0, 0);
|
|
1040
1370
|
if (args.tasks?.length) return new Text(`${theme.fg("toolTitle", theme.bold("subagents "))}${theme.fg("accent", `parallel ${args.tasks.length}`)}${theme.fg("dim", ` · ${scope}`)}`, 0, 0);
|
|
1041
1371
|
if (args.chain?.length) return new Text(`${theme.fg("toolTitle", theme.bold("subagents "))}${theme.fg("accent", `chain ${args.chain.length}`)}${theme.fg("dim", ` · ${scope}`)}`, 0, 0);
|
|
1042
1372
|
return new Text(`${theme.fg("toolTitle", theme.bold("subagent "))}${theme.fg("accent", args.agent ?? "…")}${theme.fg("dim", ` · ${scope}`)}`, 0, 0);
|
|
@@ -1048,17 +1378,31 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1048
1378
|
const first = result.content[0];
|
|
1049
1379
|
return new Text(first?.type === "text" ? first.text : "(no output)", 0, 0);
|
|
1050
1380
|
}
|
|
1381
|
+
const board = formatThreadBoard({
|
|
1382
|
+
title: `Subagents · ${details.mode}`,
|
|
1383
|
+
threads: details.results.map(threadBoardRecord),
|
|
1384
|
+
selectedThreadId: details.selectedThreadId,
|
|
1385
|
+
});
|
|
1051
1386
|
if (!expanded) {
|
|
1052
|
-
const lines =
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1387
|
+
const lines = [
|
|
1388
|
+
theme.fg("toolTitle", theme.bold(`Active (${board.active.length})`)),
|
|
1389
|
+
...board.active.map((task) => `${theme.fg("accent", "✻")} ${theme.fg("toolTitle", theme.bold(task.agent))}${theme.fg("dim", ` · ${task.id} · ${task.state.label} · ${task.usage.text}`)}`),
|
|
1390
|
+
theme.fg("toolTitle", theme.bold(`Done (${board.done.length})`)),
|
|
1391
|
+
...board.done.map((task) => `${theme.fg(task.state.status === "complete" ? "success" : "warning", `${task.state.label}`)} ${theme.fg("toolTitle", theme.bold(task.agent))}${theme.fg("dim", ` · ${task.id} · ${task.usage.text}`)}`),
|
|
1392
|
+
];
|
|
1056
1393
|
lines.push(theme.fg("dim", `Total · ${formatUsage(details.aggregateUsage)} · Ctrl+O to expand`));
|
|
1057
1394
|
return new Text(lines.join("\n"), 0, 0);
|
|
1058
1395
|
}
|
|
1059
1396
|
|
|
1060
1397
|
const container = new Container();
|
|
1061
1398
|
container.addChild(new Text(theme.fg("toolTitle", theme.bold(`Subagents · ${details.mode}`)), 0, 0));
|
|
1399
|
+
container.addChild(new Text(theme.fg("dim", `Active ${board.active.length} · Done ${board.done.length} · Controls: Inspect · Steer · Interrupt · Collect · Close`), 0, 0));
|
|
1400
|
+
if (board.selected) {
|
|
1401
|
+
const inspection = formatThreadInspection(threadBoardRecord(details.results.find((task) => task.id === board.selected!.id)!));
|
|
1402
|
+
container.addChild(new Spacer(1));
|
|
1403
|
+
container.addChild(new Text(theme.fg("accent", `Inspect ${inspection.id} · ${inspection.state.label} · ${inspection.usage.text}`), 0, 0));
|
|
1404
|
+
for (const entry of inspection.trace.entries) container.addChild(new Text(`${theme.fg("muted", "→ ")}${theme.fg("toolOutput", entry)}`, 0, 0));
|
|
1405
|
+
}
|
|
1062
1406
|
for (const task of details.results) {
|
|
1063
1407
|
container.addChild(new Spacer(1));
|
|
1064
1408
|
const status = theme.fg(statusColor(task.status), `${statusIcon(task.status)} ${task.status}`);
|