killeros 1.4.5 → 1.4.7
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 +17 -0
- package/README.md +4 -4
- package/package.json +1 -1
- package/subagent-process.ts +114 -21
- package/subagents.ts +136 -80
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to KillerOS are documented here.
|
|
4
4
|
|
|
5
|
+
## [1.4.7] - 2026-08-01
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Serialized every write-capable task in a parallel batch in input order instead of rejecting batches with multiple writers.
|
|
10
|
+
- Settled queued tasks on interrupted parallel batches and documented the shared-worktree execution model.
|
|
11
|
+
- Restricted the `message` parameter to `action: "steer"` and added focused regression coverage.
|
|
12
|
+
|
|
13
|
+
## [1.4.6] - 2026-08-01
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Made the registered task schema use the same ten-task limit as runtime validation.
|
|
18
|
+
- Kept one isolated Pi session ID and session directory across steering restarts so a child retains its conversation.
|
|
19
|
+
- Bound retained trace, stderr, and returned text, and spooled large JSONL lines to temporary storage without stopping the child or reporting a retention cutoff as `limited`.
|
|
20
|
+
- Kept explicit embedding resource guards and user stops visible as terminal states.
|
|
21
|
+
|
|
5
22
|
## [1.4.5] - 2026-08-01
|
|
6
23
|
|
|
7
24
|
### Fixed
|
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ pi install git:github.com/KyrosHendrix/pi-KillerOS
|
|
|
33
33
|
Pin an install to a release:
|
|
34
34
|
|
|
35
35
|
```bash
|
|
36
|
-
|
|
36
|
+
pi install git:github.com/KyrosHendrix/pi-KillerOS@v1.4.7
|
|
37
37
|
```
|
|
38
38
|
|
|
39
39
|
Add `-l` to either command for a project-only install. Restart Pi after installing.
|
|
@@ -97,13 +97,13 @@ KillerOS ships `planner`, `reviewer`, `scout`, and `security` as read-only roles
|
|
|
97
97
|
|
|
98
98
|
The default `agentScope: "user"` uses bundled and personal roles. Use `"project"` or `"both"` to opt into trusted project roles; a selected project override requires interactive confirmation. Role frontmatter requires `name`, `description`, `access`, and an explicit `tools` list. Optional fields are `model`, `thinking`, and `timeoutMs`. Every bundled role shows `model: inherit` and `thinking: inherit` as editable placeholders. Replace them with an available `provider/model` and a separate thinking level when you want to pin a role; `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, and `max` are checked against that model’s supported capabilities.
|
|
99
99
|
|
|
100
|
-
The tool supports a single `agent` + `task`, parallel `tasks`, or a sequential `chain` whose task text may include `{previous}`. A call can also set `model` and `thinking` for every task, overriding role settings; use `inherit` to fall back to each role and then the active parent model. For example:
|
|
100
|
+
The tool supports a single `agent` + `task`, parallel `tasks`, or a sequential `chain` whose task text may include `{previous}`. Parallel batches run read-only roles concurrently, up to four at a time, then run every write-capable role serially in input order because all children share the parent worktree. A call can also set `model` and `thinking` for every task, overriding role settings; use `inherit` to fall back to each role and then the active parent model. The `message` field is only valid with `action: "steer"`. For example:
|
|
101
101
|
|
|
102
102
|
```json
|
|
103
103
|
{"agent":"reviewer","task":"Review the change","model":"provider/model","thinking":"high"}
|
|
104
104
|
```
|
|
105
105
|
|
|
106
|
-
Use the separate `model` and `thinking` fields for new configuration. The older `provider/model:thinking` model form remains accepted. Children run as
|
|
106
|
+
Use the separate `model` and `thinking` fields for new configuration. The older `provider/model:thinking` model form remains accepted. Children run as isolated `pi --mode json -p` processes with a private `--session-dir` and `--session-id`, plus explicit local tools and `web_search`, `source_check`, `fetch_content`, and `get_search_content`. Steering restarts the same child session, so the child keeps its prior conversation. Each child explicitly loads `npm:pi-web-access`, discovers available skills, and keeps arbitrary extensions and prompt templates disabled; project-local skills load only when the parent project is trusted. Every bundled role is instructed to load the most relevant `SKILL.md` and report useful evidence. Children have no default token, dollar, turn, tool-call, research, wall-time, JSONL-line, trace, stderr, or returned-output execution quota. KillerOS bounds retained trace, stderr, and returned text and spills a large JSONL line to temporary storage; retention never stops a child or marks it `limited`. The parent limits each request to ten tasks and four parallel readers and bounds role files, task input, and combined parent output. An embedding caller may opt into named child resource guards. Esc cancellation terminates active children and escalates after five seconds.
|
|
107
107
|
|
|
108
108
|
### Thread lifecycle
|
|
109
109
|
|
|
@@ -163,7 +163,7 @@ The package manifest lists Pi’s built-in modules as peer dependencies, so npm
|
|
|
163
163
|
|
|
164
164
|
The [`pi-package`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/packages.md) keyword makes a published npm release visible in Pi’s package catalog.
|
|
165
165
|
|
|
166
|
-
For release `1.4.
|
|
166
|
+
For release `1.4.7`, publish after the validation checks pass:
|
|
167
167
|
|
|
168
168
|
```bash
|
|
169
169
|
npm login
|
package/package.json
CHANGED
package/subagent-process.ts
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { statSync } from "node:fs";
|
|
2
|
+
import { closeSync, mkdtempSync, openSync, readFileSync, rmSync, statSync, writeSync } from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
|
|
5
6
|
export const SUBAGENT_PROCESS_LIMITS = {
|
|
6
7
|
killGraceMs: 5_000,
|
|
7
8
|
} as const;
|
|
8
9
|
|
|
10
|
+
export const SUBAGENT_PROCESS_RETENTION = {
|
|
11
|
+
jsonlMemoryBytes: 1 * 1024 * 1024,
|
|
12
|
+
traceBytes: 2 * 1024 * 1024,
|
|
13
|
+
stderrBytes: 64 * 1024,
|
|
14
|
+
outputBytes: 1 * 1024 * 1024,
|
|
15
|
+
} as const;
|
|
16
|
+
|
|
9
17
|
export type SubagentProcessStatus = "running" | "complete" | "failed" | "cancelled" | "limited";
|
|
10
18
|
|
|
11
19
|
export interface SubagentProcessUsage {
|
|
@@ -56,11 +64,12 @@ export interface SubagentProcessChild {
|
|
|
56
64
|
}
|
|
57
65
|
|
|
58
66
|
export interface SubagentProcessOptions {
|
|
59
|
-
/** Exact Pi arguments. Include `--mode json` and `--no-session
|
|
67
|
+
/** Exact Pi arguments. Include `--mode json` and either `--no-session` or an isolated session id and directory. */
|
|
60
68
|
args: readonly string[];
|
|
61
69
|
cwd: string;
|
|
62
70
|
signal?: AbortSignal;
|
|
63
71
|
limits?: Partial<SubagentProcessLimits>;
|
|
72
|
+
retention?: Partial<SubagentProcessRetention>;
|
|
64
73
|
environment?: NodeJS.ProcessEnv;
|
|
65
74
|
onUpdate?: (result: Readonly<SubagentProcessResult>) => void;
|
|
66
75
|
/** Test or embed hook. It receives the exact Pi arguments supplied above. */
|
|
@@ -78,6 +87,13 @@ export interface SubagentProcessLimits {
|
|
|
78
87
|
killGraceMs: number;
|
|
79
88
|
}
|
|
80
89
|
|
|
90
|
+
export interface SubagentProcessRetention {
|
|
91
|
+
jsonlMemoryBytes: number;
|
|
92
|
+
traceBytes: number;
|
|
93
|
+
stderrBytes: number;
|
|
94
|
+
outputBytes: number;
|
|
95
|
+
}
|
|
96
|
+
|
|
81
97
|
export interface SubagentProcessHandle {
|
|
82
98
|
readonly pid: number | undefined;
|
|
83
99
|
readonly result: Promise<SubagentProcessResult>;
|
|
@@ -188,6 +204,13 @@ function hasJsonMode(args: readonly string[]): boolean {
|
|
|
188
204
|
return args.some((arg, index) => arg === "--mode=json" || arg === "--mode" && args[index + 1] === "json");
|
|
189
205
|
}
|
|
190
206
|
|
|
207
|
+
function hasIsolatedSession(args: readonly string[]): boolean {
|
|
208
|
+
const sessionId = args.indexOf("--session-id");
|
|
209
|
+
const sessionDir = args.indexOf("--session-dir");
|
|
210
|
+
return sessionId >= 0 && typeof args[sessionId + 1] === "string"
|
|
211
|
+
&& sessionDir >= 0 && typeof args[sessionDir + 1] === "string";
|
|
212
|
+
}
|
|
213
|
+
|
|
191
214
|
function normalizeLimits(overrides: Partial<SubagentProcessLimits> | undefined): SubagentProcessLimits {
|
|
192
215
|
const limits = { ...SUBAGENT_PROCESS_LIMITS, ...overrides };
|
|
193
216
|
for (const name of ["wallTimeMs", "jsonlLineBytes", "traceBytes", "stderrBytes", "outputBytes", "killGraceMs"] as const) {
|
|
@@ -201,6 +224,15 @@ function normalizeLimits(overrides: Partial<SubagentProcessLimits> | undefined):
|
|
|
201
224
|
return limits;
|
|
202
225
|
}
|
|
203
226
|
|
|
227
|
+
function normalizeRetention(overrides: Partial<SubagentProcessRetention> | undefined): SubagentProcessRetention {
|
|
228
|
+
const retention = { ...SUBAGENT_PROCESS_RETENTION, ...overrides };
|
|
229
|
+
for (const name of ["jsonlMemoryBytes", "traceBytes", "stderrBytes", "outputBytes"] as const) {
|
|
230
|
+
const value = retention[name];
|
|
231
|
+
if (!Number.isSafeInteger(value) || value <= 0) throw new RangeError(`${name} must be a positive safe integer`);
|
|
232
|
+
}
|
|
233
|
+
return retention;
|
|
234
|
+
}
|
|
235
|
+
|
|
204
236
|
function getPiInvocation(args: string[]): { command: string; args: string[] } {
|
|
205
237
|
const currentScript = process.argv[1];
|
|
206
238
|
const isBunVirtualScript = currentScript?.startsWith("/$bunfs/root/");
|
|
@@ -268,8 +300,11 @@ function terminateProcess(child: SubagentProcessChild, force: boolean): void {
|
|
|
268
300
|
export function runSubagentProcess(options: SubagentProcessOptions): SubagentProcessHandle {
|
|
269
301
|
const args = [...options.args];
|
|
270
302
|
if (!hasJsonMode(args)) throw new Error("Subagent Pi arguments must include --mode json");
|
|
271
|
-
if (!args.includes("--no-session")
|
|
303
|
+
if (!args.includes("--no-session") && !hasIsolatedSession(args)) {
|
|
304
|
+
throw new Error("Subagent Pi arguments must include --no-session or an isolated --session-id and --session-dir");
|
|
305
|
+
}
|
|
272
306
|
const limits = normalizeLimits(options.limits);
|
|
307
|
+
const retention = normalizeRetention(options.retention);
|
|
273
308
|
const startedAt = Date.now();
|
|
274
309
|
const state: SubagentProcessResult = {
|
|
275
310
|
status: "running",
|
|
@@ -295,6 +330,8 @@ export function runSubagentProcess(options: SubagentProcessOptions): SubagentPro
|
|
|
295
330
|
let requestedReason: string | undefined;
|
|
296
331
|
let stdoutLine = Buffer.alloc(0);
|
|
297
332
|
let stdoutLineBytes = 0;
|
|
333
|
+
let stdoutLineSpoolDirectory: string | undefined;
|
|
334
|
+
let stdoutLineSpoolDescriptor: number | undefined;
|
|
298
335
|
let stderr = Buffer.alloc(0);
|
|
299
336
|
let outputBytesSeen = 0;
|
|
300
337
|
let forceTimer: NodeJS.Timeout | undefined;
|
|
@@ -303,16 +340,53 @@ export function runSubagentProcess(options: SubagentProcessOptions): SubagentPro
|
|
|
303
340
|
let resolveResult!: (result: SubagentProcessResult) => void;
|
|
304
341
|
const result = new Promise<SubagentProcessResult>((resolve) => { resolveResult = resolve; });
|
|
305
342
|
|
|
343
|
+
const clearStdoutLine = (): void => {
|
|
344
|
+
if (stdoutLineSpoolDescriptor !== undefined) {
|
|
345
|
+
try {
|
|
346
|
+
closeSync(stdoutLineSpoolDescriptor);
|
|
347
|
+
} catch (error) {
|
|
348
|
+
state.errorMessage ??= `Could not close child JSONL spool: ${error instanceof Error ? error.message : String(error)}`;
|
|
349
|
+
}
|
|
350
|
+
stdoutLineSpoolDescriptor = undefined;
|
|
351
|
+
}
|
|
352
|
+
if (stdoutLineSpoolDirectory) {
|
|
353
|
+
try {
|
|
354
|
+
rmSync(stdoutLineSpoolDirectory, { recursive: true, force: true });
|
|
355
|
+
} catch (error) {
|
|
356
|
+
state.errorMessage ??= `Could not remove child JSONL spool: ${error instanceof Error ? error.message : String(error)}`;
|
|
357
|
+
}
|
|
358
|
+
stdoutLineSpoolDirectory = undefined;
|
|
359
|
+
}
|
|
360
|
+
stdoutLine = Buffer.alloc(0);
|
|
361
|
+
stdoutLineBytes = 0;
|
|
362
|
+
};
|
|
363
|
+
const readStdoutLine = (): string => {
|
|
364
|
+
if (stdoutLineSpoolDirectory) {
|
|
365
|
+
const filePath = path.join(stdoutLineSpoolDirectory, "line.jsonl");
|
|
366
|
+
try {
|
|
367
|
+
if (stdoutLineSpoolDescriptor !== undefined) {
|
|
368
|
+
closeSync(stdoutLineSpoolDescriptor);
|
|
369
|
+
stdoutLineSpoolDescriptor = undefined;
|
|
370
|
+
}
|
|
371
|
+
return readFileSync(filePath, "utf8");
|
|
372
|
+
} catch (error) {
|
|
373
|
+
state.errorMessage ??= `Could not read child JSONL spool: ${error instanceof Error ? error.message : String(error)}`;
|
|
374
|
+
return "";
|
|
375
|
+
} finally {
|
|
376
|
+
clearStdoutLine();
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
const line = stdoutLine.toString("utf8", 0, stdoutLineBytes);
|
|
380
|
+
clearStdoutLine();
|
|
381
|
+
return line;
|
|
382
|
+
};
|
|
383
|
+
|
|
306
384
|
const publish = (): void => options.onUpdate?.(cloneResult(state));
|
|
307
385
|
const finish = (code: number | null): void => {
|
|
308
386
|
if (closed || finishing) return;
|
|
309
387
|
finishing = true;
|
|
310
|
-
if (stdoutLineBytes && !requestedStatus)
|
|
311
|
-
|
|
312
|
-
stdoutLine = Buffer.alloc(0);
|
|
313
|
-
stdoutLineBytes = 0;
|
|
314
|
-
processLine(finalLine);
|
|
315
|
-
}
|
|
388
|
+
if (stdoutLineBytes && !requestedStatus) processLine(readStdoutLine());
|
|
389
|
+
else clearStdoutLine();
|
|
316
390
|
closed = true;
|
|
317
391
|
if (forceTimer) clearTimeout(forceTimer);
|
|
318
392
|
if (settleTimer) clearTimeout(settleTimer);
|
|
@@ -383,17 +457,20 @@ export function runSubagentProcess(options: SubagentProcessOptions): SubagentPro
|
|
|
383
457
|
} else if (limits.quotaUsd !== undefined && state.usage.cost.total > limits.quotaUsd) {
|
|
384
458
|
requestTermination("limited", "quota_cost", `Child cost exceeds $${limits.quotaUsd}`);
|
|
385
459
|
}
|
|
386
|
-
|
|
460
|
+
const traceTruncatedBefore = state.traceTruncatedBytes;
|
|
461
|
+
appendTrace(state, traceMessage(message), Math.min(retention.traceBytes, limits.traceBytes ?? retention.traceBytes));
|
|
462
|
+
if (limits.traceBytes !== undefined && state.traceTruncatedBytes > traceTruncatedBefore && state.traceBytes >= limits.traceBytes) {
|
|
387
463
|
requestTermination("limited", "trace_limit", `Retained child trace exceeds ${limits.traceBytes} bytes`);
|
|
388
464
|
}
|
|
389
465
|
const output = textContent(message);
|
|
390
466
|
if (output) {
|
|
391
|
-
const
|
|
467
|
+
const outputLimit = Math.min(retention.outputBytes, limits.outputBytes ?? retention.outputBytes);
|
|
468
|
+
const capped = truncateUtf8(output, outputLimit);
|
|
392
469
|
state.output = capped.text;
|
|
393
470
|
state.outputTruncatedBytes = capped.omittedBytes;
|
|
394
471
|
outputBytesSeen += Buffer.byteLength(output, "utf8");
|
|
395
472
|
state.outputBytes = outputBytesSeen;
|
|
396
|
-
state.outputTruncatedBytes = Math.max(state.outputTruncatedBytes,
|
|
473
|
+
state.outputTruncatedBytes = Math.max(state.outputTruncatedBytes, outputBytesSeen - (limits.outputBytes ?? retention.outputBytes));
|
|
397
474
|
if (limits.outputBytes !== undefined && outputBytesSeen > limits.outputBytes) requestTermination("limited", "output_limit", `Child output exceeds ${limits.outputBytes} bytes`);
|
|
398
475
|
}
|
|
399
476
|
if (typeof message.model === "string") state.model = message.provider ? `${message.provider}/${message.model}` : message.model;
|
|
@@ -406,7 +483,9 @@ export function runSubagentProcess(options: SubagentProcessOptions): SubagentPro
|
|
|
406
483
|
publish();
|
|
407
484
|
} else if (event?.type === "tool_result_end" && event.message) {
|
|
408
485
|
const name = typeof event.message.toolName === "string" ? event.message.toolName : "tool";
|
|
409
|
-
|
|
486
|
+
const traceTruncatedBefore = state.traceTruncatedBytes;
|
|
487
|
+
appendTrace(state, [`${name} result${event.message.isError ? " (error)" : ""}`], Math.min(retention.traceBytes, limits.traceBytes ?? retention.traceBytes));
|
|
488
|
+
if (limits.traceBytes !== undefined && state.traceTruncatedBytes > traceTruncatedBefore && state.traceBytes >= limits.traceBytes) {
|
|
410
489
|
requestTermination("limited", "trace_limit", `Retained child trace exceeds ${limits.traceBytes} bytes`);
|
|
411
490
|
}
|
|
412
491
|
publish();
|
|
@@ -418,6 +497,22 @@ export function runSubagentProcess(options: SubagentProcessOptions): SubagentPro
|
|
|
418
497
|
requestTermination("limited", "jsonl_line_limit", `Child JSONL line exceeds ${limits.jsonlLineBytes} bytes`);
|
|
419
498
|
return false;
|
|
420
499
|
}
|
|
500
|
+
if (nextBytes > retention.jsonlMemoryBytes) {
|
|
501
|
+
try {
|
|
502
|
+
if (stdoutLineSpoolDescriptor === undefined) {
|
|
503
|
+
stdoutLineSpoolDirectory = mkdtempSync(path.join(os.tmpdir(), "killeros-jsonl-"));
|
|
504
|
+
stdoutLineSpoolDescriptor = openSync(path.join(stdoutLineSpoolDirectory, "line.jsonl"), "w");
|
|
505
|
+
if (stdoutLineBytes) writeSync(stdoutLineSpoolDescriptor, stdoutLine);
|
|
506
|
+
stdoutLine = Buffer.alloc(0);
|
|
507
|
+
}
|
|
508
|
+
if (fragment.length) writeSync(stdoutLineSpoolDescriptor, fragment);
|
|
509
|
+
} catch (error) {
|
|
510
|
+
requestTermination("failed", "jsonl_spool_error", `Could not spool child JSONL: ${error instanceof Error ? error.message : String(error)}`);
|
|
511
|
+
return false;
|
|
512
|
+
}
|
|
513
|
+
stdoutLineBytes = nextBytes;
|
|
514
|
+
return true;
|
|
515
|
+
}
|
|
421
516
|
if (nextBytes > stdoutLine.length) {
|
|
422
517
|
const nextCapacity = limits.jsonlLineBytes === undefined
|
|
423
518
|
? Math.max(nextBytes, stdoutLine.length * 2, 4_096)
|
|
@@ -449,21 +544,19 @@ export function runSubagentProcess(options: SubagentProcessOptions): SubagentPro
|
|
|
449
544
|
const end = newline < 0 ? buffer.length : newline;
|
|
450
545
|
if (!appendStdout(buffer.subarray(offset, end))) return;
|
|
451
546
|
if (newline < 0) return;
|
|
452
|
-
|
|
453
|
-
stdoutLine = Buffer.alloc(0);
|
|
454
|
-
stdoutLineBytes = 0;
|
|
455
|
-
processLine(line);
|
|
547
|
+
processLine(readStdoutLine());
|
|
456
548
|
offset = newline + 1;
|
|
457
549
|
}
|
|
458
550
|
});
|
|
459
551
|
child.stderr.on("data", (chunk: Buffer | string) => {
|
|
460
552
|
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
461
|
-
const
|
|
462
|
-
|
|
463
|
-
: buffer.subarray(0, Math.max(0, limits.stderrBytes - stderr.length));
|
|
553
|
+
const stderrLimit = Math.min(retention.stderrBytes, limits.stderrBytes ?? retention.stderrBytes);
|
|
554
|
+
const retained = buffer.subarray(0, Math.max(0, stderrLimit - stderr.length));
|
|
464
555
|
if (retained.length) stderr = Buffer.concat([stderr, retained]);
|
|
465
556
|
state.stderrTruncatedBytes += buffer.length - retained.length;
|
|
466
|
-
if (
|
|
557
|
+
if (limits.stderrBytes !== undefined && stderr.length + state.stderrTruncatedBytes > limits.stderrBytes) {
|
|
558
|
+
requestTermination("limited", "stderr_limit", `Child stderr exceeds ${limits.stderrBytes} bytes`);
|
|
559
|
+
}
|
|
467
560
|
});
|
|
468
561
|
child.on("error", (error) => requestTermination("failed", "spawn_error", error.message));
|
|
469
562
|
child.once("close", finish);
|
package/subagents.ts
CHANGED
|
@@ -20,9 +20,12 @@ import { runSubagentProcess, type SubagentProcessHandle, type SubagentProcessRes
|
|
|
20
20
|
import { formatThreadBoard, formatThreadInspection, type ThreadRecord as ThreadBoardRecord } from "./subagent-ui.ts";
|
|
21
21
|
|
|
22
22
|
export const SUBAGENT_LIMITS = {
|
|
23
|
-
maxTasks:
|
|
23
|
+
maxTasks: 10,
|
|
24
24
|
maxReadConcurrency: 4,
|
|
25
25
|
toolOutputBytes: 50 * 1024,
|
|
26
|
+
traceRetentionBytes: 8 * 1024 * 1024,
|
|
27
|
+
stderrRetentionBytes: 1 * 1024 * 1024,
|
|
28
|
+
taskOutputRetentionBytes: 1 * 1024 * 1024,
|
|
26
29
|
roleFileBytes: 64 * 1024,
|
|
27
30
|
taskCharacters: 20_000,
|
|
28
31
|
killGraceMs: 5_000,
|
|
@@ -110,6 +113,7 @@ export interface SubagentDetails {
|
|
|
110
113
|
mode: "single" | "parallel" | "chain";
|
|
111
114
|
agentScope: AgentScope;
|
|
112
115
|
projectAgentsDir: string | null;
|
|
116
|
+
executionNote?: string;
|
|
113
117
|
results: SubagentTaskResult[];
|
|
114
118
|
aggregateUsage: SubagentUsage;
|
|
115
119
|
parentId?: string;
|
|
@@ -511,7 +515,12 @@ function cloneResult(result: SubagentTaskResult): SubagentTaskResult {
|
|
|
511
515
|
};
|
|
512
516
|
}
|
|
513
517
|
|
|
514
|
-
function mergeTaskResults(
|
|
518
|
+
function mergeTaskResults(
|
|
519
|
+
previous: SubagentTaskResult | undefined,
|
|
520
|
+
next: SubagentTaskResult,
|
|
521
|
+
maxTraceBytes?: number,
|
|
522
|
+
maxStderrBytes?: number,
|
|
523
|
+
): SubagentTaskResult {
|
|
515
524
|
if (!previous) return cloneResult(next);
|
|
516
525
|
const merged = cloneResult(next);
|
|
517
526
|
const trace: string[] = [];
|
|
@@ -527,9 +536,11 @@ function mergeTaskResults(previous: SubagentTaskResult | undefined, next: Subage
|
|
|
527
536
|
merged.trace = trace;
|
|
528
537
|
merged.traceBytes = traceBytes;
|
|
529
538
|
merged.traceTruncatedBytes = traceTruncatedBytes;
|
|
530
|
-
|
|
539
|
+
const stderr = [previous.stderr, next.stderr].filter(Boolean).join("\n");
|
|
540
|
+
const retainedStderr = truncateUtf8(stderr, maxStderrBytes === undefined ? Buffer.byteLength(stderr, "utf8") : maxStderrBytes);
|
|
541
|
+
merged.stderr = retainedStderr.text;
|
|
531
542
|
merged.stderrBytes = previous.stderrBytes + next.stderrBytes;
|
|
532
|
-
merged.stderrTruncatedBytes = previous.stderrTruncatedBytes + next.stderrTruncatedBytes;
|
|
543
|
+
merged.stderrTruncatedBytes = previous.stderrTruncatedBytes + next.stderrTruncatedBytes + retainedStderr.omittedBytes;
|
|
533
544
|
merged.output = next.output || previous.output;
|
|
534
545
|
merged.outputBytes = previous.outputBytes + next.outputBytes;
|
|
535
546
|
merged.outputTruncatedBytes = previous.outputTruncatedBytes + next.outputTruncatedBytes;
|
|
@@ -565,6 +576,8 @@ interface RunTaskOptions {
|
|
|
565
576
|
webExtension?: string;
|
|
566
577
|
projectTrusted: boolean;
|
|
567
578
|
limits: SubagentLimits;
|
|
579
|
+
sessionDirectory: string;
|
|
580
|
+
sessionId: string;
|
|
568
581
|
timeoutMs?: number;
|
|
569
582
|
onChange: (result: SubagentTaskResult) => void;
|
|
570
583
|
onHandle?: (handle: SubagentProcessHandle) => void;
|
|
@@ -631,7 +644,8 @@ async function runTask(options: RunTaskOptions): Promise<SubagentTaskResult> {
|
|
|
631
644
|
const args = [
|
|
632
645
|
"--mode", "json",
|
|
633
646
|
"-p",
|
|
634
|
-
"--
|
|
647
|
+
"--session-dir", options.sessionDirectory,
|
|
648
|
+
"--session-id", options.sessionId,
|
|
635
649
|
"--no-extensions",
|
|
636
650
|
"--extension", options.webExtension ?? SUBAGENT_WEB_EXTENSION,
|
|
637
651
|
"--no-prompt-templates",
|
|
@@ -657,6 +671,11 @@ async function runTask(options: RunTaskOptions): Promise<SubagentTaskResult> {
|
|
|
657
671
|
...(limits.quotaUsd === undefined ? {} : { quotaUsd: limits.quotaUsd }),
|
|
658
672
|
killGraceMs: limits.killGraceMs,
|
|
659
673
|
},
|
|
674
|
+
retention: {
|
|
675
|
+
traceBytes: limits.traceRetentionBytes,
|
|
676
|
+
stderrBytes: limits.stderrRetentionBytes,
|
|
677
|
+
outputBytes: limits.taskOutputRetentionBytes,
|
|
678
|
+
},
|
|
660
679
|
onUpdate: (next) => applyProcessResult(result, next, startedAt, options.onChange),
|
|
661
680
|
});
|
|
662
681
|
options.onHandle?.(handle);
|
|
@@ -694,35 +713,35 @@ async function mapReadTasks<T>(items: T[], concurrency: number, run: (item: T, i
|
|
|
694
713
|
await Promise.all(workers);
|
|
695
714
|
}
|
|
696
715
|
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
})
|
|
701
|
-
|
|
702
|
-
const
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
});
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
})
|
|
725
|
-
}
|
|
716
|
+
function createSubagentParams(limits: Pick<SubagentLimits, "maxTasks" | "maxReadConcurrency" | "taskCharacters">) {
|
|
717
|
+
const taskSchema = Type.Object({
|
|
718
|
+
agent: Type.String({ minLength: 1, maxLength: 64, description: "Agent role name" }),
|
|
719
|
+
task: Type.String({ minLength: 1, maxLength: limits.taskCharacters, description: "Bounded task for the role" }),
|
|
720
|
+
});
|
|
721
|
+
const chainTaskSchema = Type.Object({
|
|
722
|
+
agent: Type.String({ minLength: 1, maxLength: 64, description: "Agent role name" }),
|
|
723
|
+
task: Type.String({ minLength: 1, maxLength: limits.taskCharacters, description: "Task with optional {previous} handoff placeholder" }),
|
|
724
|
+
});
|
|
725
|
+
return Type.Object({
|
|
726
|
+
action: Type.Optional(StringEnum(["spawn", "list", "inspect", "steer", "interrupt", "collect", "close"] as const, {
|
|
727
|
+
default: "spawn",
|
|
728
|
+
description: "Thread lifecycle action",
|
|
729
|
+
})),
|
|
730
|
+
threadId: Type.Optional(Type.String({ minLength: 1, maxLength: 128, description: "Stable child thread ID" })),
|
|
731
|
+
message: Type.Optional(Type.String({ minLength: 1, maxLength: 4_000, description: "Bounded steering message; only valid with action steer" })),
|
|
732
|
+
all: Type.Optional(Type.Boolean({ description: "Interrupt every active child thread" })),
|
|
733
|
+
agent: Type.Optional(Type.String({ minLength: 1, maxLength: 64, description: "Agent role for single mode" })),
|
|
734
|
+
task: Type.Optional(Type.String({ minLength: 1, maxLength: limits.taskCharacters, description: "Task for single mode" })),
|
|
735
|
+
tasks: Type.Optional(Type.Array(taskSchema, { minItems: 1, maxItems: limits.maxTasks, description: `Parallel role tasks: read-only roles run concurrently up to ${limits.maxReadConcurrency}; write-capable roles run serially in input order because all children share the parent worktree` })),
|
|
736
|
+
chain: Type.Optional(Type.Array(chainTaskSchema, { minItems: 1, maxItems: limits.maxTasks, description: "Sequential role tasks; {previous} inserts the prior result" })),
|
|
737
|
+
model: Type.Optional(Type.String({ minLength: 1, maxLength: 256, description: "Model for every task as provider/model; inherit uses each role setting or the active parent" })),
|
|
738
|
+
thinking: Type.Optional(Type.String({ minLength: 1, maxLength: 16, description: "Thinking effort for every task: off, minimal, low, medium, high, xhigh, max, or inherit" })),
|
|
739
|
+
agentScope: Type.Optional(StringEnum(["user", "project", "both"] as const, {
|
|
740
|
+
default: "user",
|
|
741
|
+
description: "Role sources: user includes bundled and personal; project includes bundled and trusted project; both includes all",
|
|
742
|
+
})),
|
|
743
|
+
});
|
|
744
|
+
}
|
|
726
745
|
|
|
727
746
|
type TaskInput = { agent: string; task: string };
|
|
728
747
|
|
|
@@ -739,16 +758,12 @@ function clipCharacters(text: string, maxCharacters: number, fromEnd = false): s
|
|
|
739
758
|
return (fromEnd ? characters.slice(-maxCharacters) : characters.slice(0, maxCharacters)).join("");
|
|
740
759
|
}
|
|
741
760
|
|
|
742
|
-
function buildSteeredTask(task: string, steering: readonly string[],
|
|
761
|
+
function buildSteeredTask(task: string, steering: readonly string[], maxCharacters: number): string {
|
|
743
762
|
const steeringLabel = "\n\nParent steering:\n";
|
|
744
763
|
const steeringText = clipCharacters(steering.join("\n"), Math.max(0, maxCharacters - [...steeringLabel].length), true);
|
|
745
|
-
const
|
|
746
|
-
const required = [...steeringLabel, ...steeringText, ...previousLabel].length;
|
|
764
|
+
const required = [...steeringLabel, ...steeringText].length;
|
|
747
765
|
const taskText = clipCharacters(task, Math.max(0, maxCharacters - required));
|
|
748
|
-
|
|
749
|
-
? clipCharacters(previousOutput, Math.max(0, maxCharacters - [...taskText, ...steeringLabel, ...steeringText, ...previousLabel].length))
|
|
750
|
-
: "";
|
|
751
|
-
return `${taskText}${previousText ? `${previousLabel}${previousText}` : ""}${steeringLabel}${steeringText}`;
|
|
766
|
+
return `${taskText}${steeringLabel}${steeringText}`;
|
|
752
767
|
}
|
|
753
768
|
|
|
754
769
|
function formatUsage(usage: SubagentUsage): string {
|
|
@@ -953,7 +968,7 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
953
968
|
};
|
|
954
969
|
|
|
955
970
|
const syncThread = (threadId: SubagentThreadId, next: SubagentTaskResult, runtime?: ActiveThreadRuntime): SubagentTaskResult => {
|
|
956
|
-
const effective = mergeTaskResults(runtime?.aggregate, next, limits.
|
|
971
|
+
const effective = mergeTaskResults(runtime?.aggregate, next, limits.traceRetentionBytes, limits.stderrRetentionBytes);
|
|
957
972
|
if (runtime?.requestedReason && next.status === "cancelled") effective.terminationReason = runtime.requestedReason;
|
|
958
973
|
savedResults.set(threadId, cloneResult(effective));
|
|
959
974
|
let thread = threads.inspect(threadId);
|
|
@@ -966,9 +981,9 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
966
981
|
const from = runtime?.traceCount ?? 0;
|
|
967
982
|
let retainedTraceBytes = thread.trace.reduce((total, entry) => total + Buffer.byteLength(entry.message ?? "", "utf8"), 0);
|
|
968
983
|
for (const entry of next.trace.slice(from)) {
|
|
969
|
-
const retained = truncateUtf8(entry, limits.
|
|
984
|
+
const retained = truncateUtf8(entry, limits.traceRetentionBytes === undefined
|
|
970
985
|
? Buffer.byteLength(entry, "utf8")
|
|
971
|
-
: Math.max(0, limits.
|
|
986
|
+
: Math.max(0, limits.traceRetentionBytes - retainedTraceBytes));
|
|
972
987
|
if (retained.text) {
|
|
973
988
|
threads.trace(threadId, { kind: "child", message: retained.text });
|
|
974
989
|
retainedTraceBytes += Buffer.byteLength(retained.text, "utf8");
|
|
@@ -1015,20 +1030,23 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1015
1030
|
pi.registerTool({
|
|
1016
1031
|
name: "subagent",
|
|
1017
1032
|
label: "Subagents",
|
|
1018
|
-
description:
|
|
1033
|
+
description: `Spawn and manage named child threads. Children finish naturally. Parallel tasks run read-only roles concurrently up to ${limits.maxReadConcurrency}, then run write-capable roles serially in input order because all children share the parent worktree. The message parameter is only valid with action steer. Use action list, inspect, steer, interrupt, collect, and close to manage active and completed handoffs.`,
|
|
1019
1034
|
promptSnippet: "Delegate bounded specialist work to isolated KillerOS subagents",
|
|
1020
1035
|
promptGuidelines: [
|
|
1021
1036
|
"Use subagent for clearly separable specialist work; prefer read-only scout, planner, reviewer, or security roles before a writer.",
|
|
1022
|
-
|
|
1037
|
+
`Parallel tasks run read-only roles concurrently up to ${limits.maxReadConcurrency}, then queue write-capable roles in input order because all children share the parent worktree.`,
|
|
1023
1038
|
"Every child can load relevant skills with read and can use web_search, source_check, fetch_content, and get_search_content for external research.",
|
|
1024
1039
|
"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.",
|
|
1025
1040
|
"Keep completed and stopped threads inspectable until the parent explicitly closes them.",
|
|
1026
1041
|
],
|
|
1027
|
-
parameters:
|
|
1042
|
+
parameters: createSubagentParams(limits),
|
|
1028
1043
|
executionMode: "parallel",
|
|
1029
1044
|
|
|
1030
1045
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
1031
1046
|
const action = params.action ?? "spawn";
|
|
1047
|
+
if (params.message !== undefined && action !== "steer") {
|
|
1048
|
+
throw new Error("message is only valid with action steer");
|
|
1049
|
+
}
|
|
1032
1050
|
const parentId = parentThreadId(ctx);
|
|
1033
1051
|
const actionDetails = (selectedThreadId?: string): SubagentDetails => detailsFor(parentId, "single", params.agentScope ?? "user", null, selectedThreadId);
|
|
1034
1052
|
const actionResult = (text: string, selectedThreadId?: string) => {
|
|
@@ -1147,10 +1165,17 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1147
1165
|
? [{ agent: params.agent!, task: params.task! }]
|
|
1148
1166
|
: hasParallel ? params.tasks! : params.chain!;
|
|
1149
1167
|
if (inputs.length > limits.maxTasks) throw new Error(`At most ${limits.maxTasks} subagent tasks are allowed`);
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1168
|
+
const readIndexes = hasParallel
|
|
1169
|
+
? inputs.map((input, index) => ({ input, index })).filter(({ input }) => roles.get(input.agent)!.access === "read")
|
|
1170
|
+
: [];
|
|
1171
|
+
const writerIndexes = hasParallel
|
|
1172
|
+
? inputs.map((input, index) => ({ input, index })).filter(({ input }) => roles.get(input.agent)!.access === "write").map(({ index }) => index)
|
|
1173
|
+
: [];
|
|
1174
|
+
const executionNote = hasParallel
|
|
1175
|
+
? writerIndexes.length
|
|
1176
|
+
? `Parallel schedule: read-only tasks run concurrently up to ${limits.maxReadConcurrency}; write-capable tasks are queued (serialized) in input order because all children share the parent worktree.`
|
|
1177
|
+
: `Parallel schedule: read-only tasks run concurrently up to ${limits.maxReadConcurrency}.`
|
|
1178
|
+
: undefined;
|
|
1154
1179
|
|
|
1155
1180
|
const inFlight = threads.listAll().filter((thread) => ["queued", "active"].includes(thread.state)).length;
|
|
1156
1181
|
if (inFlight + inputs.length > limits.maxTasks) {
|
|
@@ -1173,7 +1198,7 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1173
1198
|
const currentResults = results.map(cloneResult);
|
|
1174
1199
|
(onUpdate as ToolUpdate | undefined)?.({
|
|
1175
1200
|
content: [{ type: "text", text: message }],
|
|
1176
|
-
details: { ...board, results: currentResults, aggregateUsage: aggregateUsage(currentResults) },
|
|
1201
|
+
details: { ...board, executionNote, results: currentResults, aggregateUsage: aggregateUsage(currentResults) },
|
|
1177
1202
|
});
|
|
1178
1203
|
};
|
|
1179
1204
|
const runAt = async (index: number, task: string): Promise<void> => {
|
|
@@ -1214,6 +1239,23 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1214
1239
|
emit();
|
|
1215
1240
|
return;
|
|
1216
1241
|
}
|
|
1242
|
+
let sessionDirectory: string;
|
|
1243
|
+
try {
|
|
1244
|
+
sessionDirectory = await mkdtemp(path.join(os.tmpdir(), "killeros-subagent-session-"));
|
|
1245
|
+
} catch (error) {
|
|
1246
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1247
|
+
results[index] = {
|
|
1248
|
+
...results[index]!,
|
|
1249
|
+
status: "failed",
|
|
1250
|
+
terminationReason: "session_error",
|
|
1251
|
+
errorMessage: message,
|
|
1252
|
+
};
|
|
1253
|
+
threads.fail(threadId, { message, code: "session_error" });
|
|
1254
|
+
savedResults.set(threadId, cloneResult(results[index]!));
|
|
1255
|
+
emit();
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
const sessionId = `killeros-${threadId.replace(/[^A-Za-z0-9_.-]/gu, "_")}`;
|
|
1217
1259
|
const controller = new AbortController();
|
|
1218
1260
|
const abortParent = (): void => controller.abort();
|
|
1219
1261
|
if (signal) {
|
|
@@ -1224,7 +1266,7 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1224
1266
|
activeRuntimes.set(threadId, runtime);
|
|
1225
1267
|
const agent = roles.get(input.agent)!;
|
|
1226
1268
|
const queuedSteering = initialThread.steering.map((entry) => entry.message);
|
|
1227
|
-
let currentTask = queuedSteering.length ? buildSteeredTask(task, queuedSteering,
|
|
1269
|
+
let currentTask = queuedSteering.length ? buildSteeredTask(task, queuedSteering, limits.taskCharacters) : task;
|
|
1228
1270
|
const stopForBudget = (reason: string, message: string): void => {
|
|
1229
1271
|
const limited = cloneResult(runtime.aggregate ?? results[index]!);
|
|
1230
1272
|
limited.status = "limited";
|
|
@@ -1248,7 +1290,7 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1248
1290
|
const aggregate = runtime.aggregate;
|
|
1249
1291
|
const wallTimeMs = agent.timeoutMs ?? limits.wallTimeMs;
|
|
1250
1292
|
const remainingWallTimeMs = wallTimeMs === undefined ? undefined : wallTimeMs - (Date.now() - runtime.startedAt);
|
|
1251
|
-
const usedTraceBytes = aggregate?.traceBytes ?? 0;
|
|
1293
|
+
const usedTraceBytes = (aggregate?.traceBytes ?? 0) + (aggregate?.traceTruncatedBytes ?? 0);
|
|
1252
1294
|
const usedStderrBytes = aggregate?.stderrBytes ?? 0;
|
|
1253
1295
|
const usedOutputBytes = aggregate?.outputBytes ?? 0;
|
|
1254
1296
|
const usedTokens = aggregate?.usage.totalTokens ?? 0;
|
|
@@ -1257,15 +1299,15 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1257
1299
|
stopForBudget("wall_time_limit", `Child thread exceeds ${wallTimeMs} ms`);
|
|
1258
1300
|
break;
|
|
1259
1301
|
}
|
|
1260
|
-
if (limits.traceBytes !== undefined &&
|
|
1302
|
+
if (limits.traceBytes !== undefined && usedTraceBytes >= limits.traceBytes) {
|
|
1261
1303
|
stopForBudget("trace_limit", `Child thread retains more than ${limits.traceBytes} trace bytes`);
|
|
1262
1304
|
break;
|
|
1263
1305
|
}
|
|
1264
|
-
if (limits.stderrBytes !== undefined &&
|
|
1306
|
+
if (limits.stderrBytes !== undefined && usedStderrBytes >= limits.stderrBytes) {
|
|
1265
1307
|
stopForBudget("stderr_limit", `Child thread emits more than ${limits.stderrBytes} stderr bytes`);
|
|
1266
1308
|
break;
|
|
1267
1309
|
}
|
|
1268
|
-
if (limits.taskOutputBytes !== undefined &&
|
|
1310
|
+
if (limits.taskOutputBytes !== undefined && usedOutputBytes >= limits.taskOutputBytes) {
|
|
1269
1311
|
stopForBudget("output_limit", `Child thread emits more than ${limits.taskOutputBytes} output bytes`);
|
|
1270
1312
|
break;
|
|
1271
1313
|
}
|
|
@@ -1289,6 +1331,8 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1289
1331
|
webExtension: options.webExtension,
|
|
1290
1332
|
projectTrusted: ctx.isProjectTrusted(),
|
|
1291
1333
|
spawnProcess,
|
|
1334
|
+
sessionDirectory,
|
|
1335
|
+
sessionId,
|
|
1292
1336
|
limits: {
|
|
1293
1337
|
...limits,
|
|
1294
1338
|
...(limits.traceBytes === undefined ? {} : { traceBytes: limits.traceBytes - usedTraceBytes }),
|
|
@@ -1305,7 +1349,7 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1305
1349
|
},
|
|
1306
1350
|
});
|
|
1307
1351
|
next.task = task;
|
|
1308
|
-
runtime.aggregate = mergeTaskResults(runtime.aggregate, next, limits.
|
|
1352
|
+
runtime.aggregate = mergeTaskResults(runtime.aggregate, next, limits.traceRetentionBytes, limits.stderrRetentionBytes);
|
|
1309
1353
|
runtime.aggregate.task = task;
|
|
1310
1354
|
results[index] = cloneResult(runtime.aggregate);
|
|
1311
1355
|
savedResults.set(threadId, cloneResult(runtime.aggregate));
|
|
@@ -1321,15 +1365,37 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1321
1365
|
handoff: runtime.aggregate.output ? { summary: runtime.aggregate.output } : undefined,
|
|
1322
1366
|
});
|
|
1323
1367
|
}
|
|
1324
|
-
currentTask = buildSteeredTask(task, steering,
|
|
1368
|
+
currentTask = buildSteeredTask(task, steering, limits.taskCharacters);
|
|
1325
1369
|
}
|
|
1326
1370
|
} finally {
|
|
1327
1371
|
activeRuntimes.delete(threadId);
|
|
1328
1372
|
signal?.removeEventListener("abort", abortParent);
|
|
1373
|
+
try {
|
|
1374
|
+
await rm(sessionDirectory, { recursive: true, force: true });
|
|
1375
|
+
} catch {
|
|
1376
|
+
// Temporary child session cleanup is best effort after process termination.
|
|
1377
|
+
}
|
|
1329
1378
|
}
|
|
1330
1379
|
emit();
|
|
1331
1380
|
};
|
|
1332
1381
|
|
|
1382
|
+
const settleQueued = (reason: string): void => {
|
|
1383
|
+
for (let index = 0; index < results.length; index += 1) {
|
|
1384
|
+
const result = results[index]!;
|
|
1385
|
+
if (result.status !== "queued") continue;
|
|
1386
|
+
const thread = threads.inspect(threadRecords[index]!.id);
|
|
1387
|
+
const alreadyStopped = thread?.state === "stopped";
|
|
1388
|
+
result.status = signal?.aborted || alreadyStopped ? "cancelled" : "failed";
|
|
1389
|
+
result.terminationReason = alreadyStopped
|
|
1390
|
+
? thread.stopReason ?? "interrupted"
|
|
1391
|
+
: signal?.aborted ? "abort" : reason;
|
|
1392
|
+
if (thread?.state === "queued" || thread?.state === "active") {
|
|
1393
|
+
threads.stop(threadRecords[index]!.id, { reason: result.terminationReason });
|
|
1394
|
+
}
|
|
1395
|
+
savedResults.set(threadRecords[index]!.id, cloneResult(result));
|
|
1396
|
+
}
|
|
1397
|
+
};
|
|
1398
|
+
|
|
1333
1399
|
emit(`${mode}: ${results.length} queued`);
|
|
1334
1400
|
if (hasChain) {
|
|
1335
1401
|
let previous = "";
|
|
@@ -1339,33 +1405,21 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1339
1405
|
if (results[index]!.status !== "complete") break;
|
|
1340
1406
|
previous = results[index]!.output;
|
|
1341
1407
|
}
|
|
1342
|
-
|
|
1343
|
-
const result = results[index]!;
|
|
1344
|
-
if (result.status === "queued") {
|
|
1345
|
-
const thread = threads.inspect(threadRecords[index]!.id);
|
|
1346
|
-
const alreadyStopped = thread?.state === "stopped";
|
|
1347
|
-
result.status = signal?.aborted || alreadyStopped ? "cancelled" : "failed";
|
|
1348
|
-
result.terminationReason = alreadyStopped
|
|
1349
|
-
? thread.stopReason ?? "interrupted"
|
|
1350
|
-
: signal?.aborted ? "abort" : "chain_stopped";
|
|
1351
|
-
if (thread?.state === "queued" || thread?.state === "active") {
|
|
1352
|
-
threads.stop(threadRecords[index]!.id, { reason: result.terminationReason });
|
|
1353
|
-
}
|
|
1354
|
-
savedResults.set(threadRecords[index]!.id, cloneResult(result));
|
|
1355
|
-
}
|
|
1356
|
-
}
|
|
1408
|
+
settleQueued("chain_stopped");
|
|
1357
1409
|
} else if (hasParallel) {
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1410
|
+
try {
|
|
1411
|
+
await mapReadTasks(readIndexes, limits.maxReadConcurrency, async ({ index }) => runAt(index, inputs[index]!.task));
|
|
1412
|
+
for (const index of writerIndexes) await runAt(index, inputs[index]!.task);
|
|
1413
|
+
} finally {
|
|
1414
|
+
settleQueued("parallel_stopped");
|
|
1415
|
+
}
|
|
1362
1416
|
} else {
|
|
1363
1417
|
await runAt(0, inputs[0]!.task);
|
|
1364
1418
|
}
|
|
1365
1419
|
|
|
1366
1420
|
const board = detailsFor(parentId, mode, scope, discovery.projectAgentsDir);
|
|
1367
1421
|
const currentResults = results.map(cloneResult);
|
|
1368
|
-
const details: SubagentDetails = { ...board, results: currentResults, aggregateUsage: aggregateUsage(currentResults) };
|
|
1422
|
+
const details: SubagentDetails = { ...board, executionNote, results: currentResults, aggregateUsage: aggregateUsage(currentResults) };
|
|
1369
1423
|
return {
|
|
1370
1424
|
content: [{ type: "text", text: buildToolContent(mode, details.results, limits.toolOutputBytes) }],
|
|
1371
1425
|
details,
|
|
@@ -1376,7 +1430,7 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1376
1430
|
renderCall(args, theme) {
|
|
1377
1431
|
const scope = args.agentScope ?? "user";
|
|
1378
1432
|
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);
|
|
1379
|
-
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);
|
|
1433
|
+
if (args.tasks?.length) return new Text(`${theme.fg("toolTitle", theme.bold("subagents "))}${theme.fg("accent", `parallel ${args.tasks.length} · readers first; writers serial`)}${theme.fg("dim", ` · ${scope}`)}`, 0, 0);
|
|
1380
1434
|
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);
|
|
1381
1435
|
return new Text(`${theme.fg("toolTitle", theme.bold("subagent "))}${theme.fg("accent", args.agent ?? "…")}${theme.fg("dim", ` · ${scope}`)}`, 0, 0);
|
|
1382
1436
|
},
|
|
@@ -1399,6 +1453,7 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1399
1453
|
theme.fg("toolTitle", theme.bold(`Done (${board.done.length})`)),
|
|
1400
1454
|
...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}`)}`),
|
|
1401
1455
|
];
|
|
1456
|
+
if (details.executionNote) lines.push(theme.fg("dim", details.executionNote));
|
|
1402
1457
|
lines.push(theme.fg("dim", `Total · ${formatUsage(details.aggregateUsage)} · Ctrl+O to expand`));
|
|
1403
1458
|
return new Text(lines.join("\n"), 0, 0);
|
|
1404
1459
|
}
|
|
@@ -1406,6 +1461,7 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1406
1461
|
const container = new Container();
|
|
1407
1462
|
container.addChild(new Text(theme.fg("toolTitle", theme.bold(`Subagents · ${details.mode}`)), 0, 0));
|
|
1408
1463
|
container.addChild(new Text(theme.fg("dim", `Active ${board.active.length} · Done ${board.done.length} · Controls: Inspect · Steer · Interrupt · Collect · Close`), 0, 0));
|
|
1464
|
+
if (details.executionNote) container.addChild(new Text(theme.fg("dim", details.executionNote), 0, 0));
|
|
1409
1465
|
if (board.selected) {
|
|
1410
1466
|
const inspection = formatThreadInspection(threadBoardRecord(details.results.find((task) => task.id === board.selected!.id)!));
|
|
1411
1467
|
container.addChild(new Spacer(1));
|