killeros 1.4.5 → 1.4.6
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 +9 -0
- package/README.md +3 -3
- package/package.json +1 -1
- package/subagent-process.ts +114 -21
- package/subagents.ts +91 -53
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to KillerOS are documented here.
|
|
4
4
|
|
|
5
|
+
## [1.4.6] - 2026-08-01
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Made the registered task schema use the same ten-task limit as runtime validation.
|
|
10
|
+
- Kept one isolated Pi session ID and session directory across steering restarts so a child retains its conversation.
|
|
11
|
+
- 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`.
|
|
12
|
+
- Kept explicit embedding resource guards and user stops visible as terminal states.
|
|
13
|
+
|
|
5
14
|
## [1.4.5] - 2026-08-01
|
|
6
15
|
|
|
7
16
|
### 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.6
|
|
37
37
|
```
|
|
38
38
|
|
|
39
39
|
Add `-l` to either command for a project-only install. Restart Pi after installing.
|
|
@@ -103,7 +103,7 @@ The tool supports a single `agent` + `task`, parallel `tasks`, or a sequential `
|
|
|
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.6`, 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,
|
|
@@ -511,7 +514,12 @@ function cloneResult(result: SubagentTaskResult): SubagentTaskResult {
|
|
|
511
514
|
};
|
|
512
515
|
}
|
|
513
516
|
|
|
514
|
-
function mergeTaskResults(
|
|
517
|
+
function mergeTaskResults(
|
|
518
|
+
previous: SubagentTaskResult | undefined,
|
|
519
|
+
next: SubagentTaskResult,
|
|
520
|
+
maxTraceBytes?: number,
|
|
521
|
+
maxStderrBytes?: number,
|
|
522
|
+
): SubagentTaskResult {
|
|
515
523
|
if (!previous) return cloneResult(next);
|
|
516
524
|
const merged = cloneResult(next);
|
|
517
525
|
const trace: string[] = [];
|
|
@@ -527,9 +535,11 @@ function mergeTaskResults(previous: SubagentTaskResult | undefined, next: Subage
|
|
|
527
535
|
merged.trace = trace;
|
|
528
536
|
merged.traceBytes = traceBytes;
|
|
529
537
|
merged.traceTruncatedBytes = traceTruncatedBytes;
|
|
530
|
-
|
|
538
|
+
const stderr = [previous.stderr, next.stderr].filter(Boolean).join("\n");
|
|
539
|
+
const retainedStderr = truncateUtf8(stderr, maxStderrBytes === undefined ? Buffer.byteLength(stderr, "utf8") : maxStderrBytes);
|
|
540
|
+
merged.stderr = retainedStderr.text;
|
|
531
541
|
merged.stderrBytes = previous.stderrBytes + next.stderrBytes;
|
|
532
|
-
merged.stderrTruncatedBytes = previous.stderrTruncatedBytes + next.stderrTruncatedBytes;
|
|
542
|
+
merged.stderrTruncatedBytes = previous.stderrTruncatedBytes + next.stderrTruncatedBytes + retainedStderr.omittedBytes;
|
|
533
543
|
merged.output = next.output || previous.output;
|
|
534
544
|
merged.outputBytes = previous.outputBytes + next.outputBytes;
|
|
535
545
|
merged.outputTruncatedBytes = previous.outputTruncatedBytes + next.outputTruncatedBytes;
|
|
@@ -565,6 +575,8 @@ interface RunTaskOptions {
|
|
|
565
575
|
webExtension?: string;
|
|
566
576
|
projectTrusted: boolean;
|
|
567
577
|
limits: SubagentLimits;
|
|
578
|
+
sessionDirectory: string;
|
|
579
|
+
sessionId: string;
|
|
568
580
|
timeoutMs?: number;
|
|
569
581
|
onChange: (result: SubagentTaskResult) => void;
|
|
570
582
|
onHandle?: (handle: SubagentProcessHandle) => void;
|
|
@@ -631,7 +643,8 @@ async function runTask(options: RunTaskOptions): Promise<SubagentTaskResult> {
|
|
|
631
643
|
const args = [
|
|
632
644
|
"--mode", "json",
|
|
633
645
|
"-p",
|
|
634
|
-
"--
|
|
646
|
+
"--session-dir", options.sessionDirectory,
|
|
647
|
+
"--session-id", options.sessionId,
|
|
635
648
|
"--no-extensions",
|
|
636
649
|
"--extension", options.webExtension ?? SUBAGENT_WEB_EXTENSION,
|
|
637
650
|
"--no-prompt-templates",
|
|
@@ -657,6 +670,11 @@ async function runTask(options: RunTaskOptions): Promise<SubagentTaskResult> {
|
|
|
657
670
|
...(limits.quotaUsd === undefined ? {} : { quotaUsd: limits.quotaUsd }),
|
|
658
671
|
killGraceMs: limits.killGraceMs,
|
|
659
672
|
},
|
|
673
|
+
retention: {
|
|
674
|
+
traceBytes: limits.traceRetentionBytes,
|
|
675
|
+
stderrBytes: limits.stderrRetentionBytes,
|
|
676
|
+
outputBytes: limits.taskOutputRetentionBytes,
|
|
677
|
+
},
|
|
660
678
|
onUpdate: (next) => applyProcessResult(result, next, startedAt, options.onChange),
|
|
661
679
|
});
|
|
662
680
|
options.onHandle?.(handle);
|
|
@@ -694,35 +712,35 @@ async function mapReadTasks<T>(items: T[], concurrency: number, run: (item: T, i
|
|
|
694
712
|
await Promise.all(workers);
|
|
695
713
|
}
|
|
696
714
|
|
|
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
|
-
}
|
|
715
|
+
function createSubagentParams(limits: Pick<SubagentLimits, "maxTasks" | "taskCharacters">) {
|
|
716
|
+
const taskSchema = Type.Object({
|
|
717
|
+
agent: Type.String({ minLength: 1, maxLength: 64, description: "Agent role name" }),
|
|
718
|
+
task: Type.String({ minLength: 1, maxLength: limits.taskCharacters, description: "Bounded task for the role" }),
|
|
719
|
+
});
|
|
720
|
+
const chainTaskSchema = Type.Object({
|
|
721
|
+
agent: Type.String({ minLength: 1, maxLength: 64, description: "Agent role name" }),
|
|
722
|
+
task: Type.String({ minLength: 1, maxLength: limits.taskCharacters, description: "Task with optional {previous} handoff placeholder" }),
|
|
723
|
+
});
|
|
724
|
+
return Type.Object({
|
|
725
|
+
action: Type.Optional(StringEnum(["spawn", "list", "inspect", "steer", "interrupt", "collect", "close"] as const, {
|
|
726
|
+
default: "spawn",
|
|
727
|
+
description: "Thread lifecycle action",
|
|
728
|
+
})),
|
|
729
|
+
threadId: Type.Optional(Type.String({ minLength: 1, maxLength: 128, description: "Stable child thread ID" })),
|
|
730
|
+
message: Type.Optional(Type.String({ minLength: 1, maxLength: 4_000, description: "Bounded steering message" })),
|
|
731
|
+
all: Type.Optional(Type.Boolean({ description: "Interrupt every active child thread" })),
|
|
732
|
+
agent: Type.Optional(Type.String({ minLength: 1, maxLength: 64, description: "Agent role for single mode" })),
|
|
733
|
+
task: Type.Optional(Type.String({ minLength: 1, maxLength: limits.taskCharacters, description: "Task for single mode" })),
|
|
734
|
+
tasks: Type.Optional(Type.Array(taskSchema, { minItems: 1, maxItems: limits.maxTasks, description: "Parallel role tasks" })),
|
|
735
|
+
chain: Type.Optional(Type.Array(chainTaskSchema, { minItems: 1, maxItems: limits.maxTasks, description: "Sequential role tasks; {previous} inserts the prior result" })),
|
|
736
|
+
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" })),
|
|
737
|
+
thinking: Type.Optional(Type.String({ minLength: 1, maxLength: 16, description: "Thinking effort for every task: off, minimal, low, medium, high, xhigh, max, or inherit" })),
|
|
738
|
+
agentScope: Type.Optional(StringEnum(["user", "project", "both"] as const, {
|
|
739
|
+
default: "user",
|
|
740
|
+
description: "Role sources: user includes bundled and personal; project includes bundled and trusted project; both includes all",
|
|
741
|
+
})),
|
|
742
|
+
});
|
|
743
|
+
}
|
|
726
744
|
|
|
727
745
|
type TaskInput = { agent: string; task: string };
|
|
728
746
|
|
|
@@ -739,16 +757,12 @@ function clipCharacters(text: string, maxCharacters: number, fromEnd = false): s
|
|
|
739
757
|
return (fromEnd ? characters.slice(-maxCharacters) : characters.slice(0, maxCharacters)).join("");
|
|
740
758
|
}
|
|
741
759
|
|
|
742
|
-
function buildSteeredTask(task: string, steering: readonly string[],
|
|
760
|
+
function buildSteeredTask(task: string, steering: readonly string[], maxCharacters: number): string {
|
|
743
761
|
const steeringLabel = "\n\nParent steering:\n";
|
|
744
762
|
const steeringText = clipCharacters(steering.join("\n"), Math.max(0, maxCharacters - [...steeringLabel].length), true);
|
|
745
|
-
const
|
|
746
|
-
const required = [...steeringLabel, ...steeringText, ...previousLabel].length;
|
|
763
|
+
const required = [...steeringLabel, ...steeringText].length;
|
|
747
764
|
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}`;
|
|
765
|
+
return `${taskText}${steeringLabel}${steeringText}`;
|
|
752
766
|
}
|
|
753
767
|
|
|
754
768
|
function formatUsage(usage: SubagentUsage): string {
|
|
@@ -953,7 +967,7 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
953
967
|
};
|
|
954
968
|
|
|
955
969
|
const syncThread = (threadId: SubagentThreadId, next: SubagentTaskResult, runtime?: ActiveThreadRuntime): SubagentTaskResult => {
|
|
956
|
-
const effective = mergeTaskResults(runtime?.aggregate, next, limits.
|
|
970
|
+
const effective = mergeTaskResults(runtime?.aggregate, next, limits.traceRetentionBytes, limits.stderrRetentionBytes);
|
|
957
971
|
if (runtime?.requestedReason && next.status === "cancelled") effective.terminationReason = runtime.requestedReason;
|
|
958
972
|
savedResults.set(threadId, cloneResult(effective));
|
|
959
973
|
let thread = threads.inspect(threadId);
|
|
@@ -966,9 +980,9 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
966
980
|
const from = runtime?.traceCount ?? 0;
|
|
967
981
|
let retainedTraceBytes = thread.trace.reduce((total, entry) => total + Buffer.byteLength(entry.message ?? "", "utf8"), 0);
|
|
968
982
|
for (const entry of next.trace.slice(from)) {
|
|
969
|
-
const retained = truncateUtf8(entry, limits.
|
|
983
|
+
const retained = truncateUtf8(entry, limits.traceRetentionBytes === undefined
|
|
970
984
|
? Buffer.byteLength(entry, "utf8")
|
|
971
|
-
: Math.max(0, limits.
|
|
985
|
+
: Math.max(0, limits.traceRetentionBytes - retainedTraceBytes));
|
|
972
986
|
if (retained.text) {
|
|
973
987
|
threads.trace(threadId, { kind: "child", message: retained.text });
|
|
974
988
|
retainedTraceBytes += Buffer.byteLength(retained.text, "utf8");
|
|
@@ -1015,7 +1029,7 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1015
1029
|
pi.registerTool({
|
|
1016
1030
|
name: "subagent",
|
|
1017
1031
|
label: "Subagents",
|
|
1018
|
-
description: "Spawn and manage named child threads. Children finish naturally;
|
|
1032
|
+
description: "Spawn and manage named child threads. Children finish naturally; explicit execution guards, task count, and concurrency are the hard edges. Retention only bounds stored detail. Use action list, inspect, steer, interrupt, collect, and close to manage active and completed handoffs.",
|
|
1019
1033
|
promptSnippet: "Delegate bounded specialist work to isolated KillerOS subagents",
|
|
1020
1034
|
promptGuidelines: [
|
|
1021
1035
|
"Use subagent for clearly separable specialist work; prefer read-only scout, planner, reviewer, or security roles before a writer.",
|
|
@@ -1024,7 +1038,7 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1024
1038
|
"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
1039
|
"Keep completed and stopped threads inspectable until the parent explicitly closes them.",
|
|
1026
1040
|
],
|
|
1027
|
-
parameters:
|
|
1041
|
+
parameters: createSubagentParams(limits),
|
|
1028
1042
|
executionMode: "parallel",
|
|
1029
1043
|
|
|
1030
1044
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
@@ -1214,6 +1228,23 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1214
1228
|
emit();
|
|
1215
1229
|
return;
|
|
1216
1230
|
}
|
|
1231
|
+
let sessionDirectory: string;
|
|
1232
|
+
try {
|
|
1233
|
+
sessionDirectory = await mkdtemp(path.join(os.tmpdir(), "killeros-subagent-session-"));
|
|
1234
|
+
} catch (error) {
|
|
1235
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1236
|
+
results[index] = {
|
|
1237
|
+
...results[index]!,
|
|
1238
|
+
status: "failed",
|
|
1239
|
+
terminationReason: "session_error",
|
|
1240
|
+
errorMessage: message,
|
|
1241
|
+
};
|
|
1242
|
+
threads.fail(threadId, { message, code: "session_error" });
|
|
1243
|
+
savedResults.set(threadId, cloneResult(results[index]!));
|
|
1244
|
+
emit();
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
const sessionId = `killeros-${threadId.replace(/[^A-Za-z0-9_.-]/gu, "_")}`;
|
|
1217
1248
|
const controller = new AbortController();
|
|
1218
1249
|
const abortParent = (): void => controller.abort();
|
|
1219
1250
|
if (signal) {
|
|
@@ -1224,7 +1255,7 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1224
1255
|
activeRuntimes.set(threadId, runtime);
|
|
1225
1256
|
const agent = roles.get(input.agent)!;
|
|
1226
1257
|
const queuedSteering = initialThread.steering.map((entry) => entry.message);
|
|
1227
|
-
let currentTask = queuedSteering.length ? buildSteeredTask(task, queuedSteering,
|
|
1258
|
+
let currentTask = queuedSteering.length ? buildSteeredTask(task, queuedSteering, limits.taskCharacters) : task;
|
|
1228
1259
|
const stopForBudget = (reason: string, message: string): void => {
|
|
1229
1260
|
const limited = cloneResult(runtime.aggregate ?? results[index]!);
|
|
1230
1261
|
limited.status = "limited";
|
|
@@ -1248,7 +1279,7 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1248
1279
|
const aggregate = runtime.aggregate;
|
|
1249
1280
|
const wallTimeMs = agent.timeoutMs ?? limits.wallTimeMs;
|
|
1250
1281
|
const remainingWallTimeMs = wallTimeMs === undefined ? undefined : wallTimeMs - (Date.now() - runtime.startedAt);
|
|
1251
|
-
const usedTraceBytes = aggregate?.traceBytes ?? 0;
|
|
1282
|
+
const usedTraceBytes = (aggregate?.traceBytes ?? 0) + (aggregate?.traceTruncatedBytes ?? 0);
|
|
1252
1283
|
const usedStderrBytes = aggregate?.stderrBytes ?? 0;
|
|
1253
1284
|
const usedOutputBytes = aggregate?.outputBytes ?? 0;
|
|
1254
1285
|
const usedTokens = aggregate?.usage.totalTokens ?? 0;
|
|
@@ -1257,15 +1288,15 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1257
1288
|
stopForBudget("wall_time_limit", `Child thread exceeds ${wallTimeMs} ms`);
|
|
1258
1289
|
break;
|
|
1259
1290
|
}
|
|
1260
|
-
if (limits.traceBytes !== undefined &&
|
|
1291
|
+
if (limits.traceBytes !== undefined && usedTraceBytes >= limits.traceBytes) {
|
|
1261
1292
|
stopForBudget("trace_limit", `Child thread retains more than ${limits.traceBytes} trace bytes`);
|
|
1262
1293
|
break;
|
|
1263
1294
|
}
|
|
1264
|
-
if (limits.stderrBytes !== undefined &&
|
|
1295
|
+
if (limits.stderrBytes !== undefined && usedStderrBytes >= limits.stderrBytes) {
|
|
1265
1296
|
stopForBudget("stderr_limit", `Child thread emits more than ${limits.stderrBytes} stderr bytes`);
|
|
1266
1297
|
break;
|
|
1267
1298
|
}
|
|
1268
|
-
if (limits.taskOutputBytes !== undefined &&
|
|
1299
|
+
if (limits.taskOutputBytes !== undefined && usedOutputBytes >= limits.taskOutputBytes) {
|
|
1269
1300
|
stopForBudget("output_limit", `Child thread emits more than ${limits.taskOutputBytes} output bytes`);
|
|
1270
1301
|
break;
|
|
1271
1302
|
}
|
|
@@ -1289,6 +1320,8 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1289
1320
|
webExtension: options.webExtension,
|
|
1290
1321
|
projectTrusted: ctx.isProjectTrusted(),
|
|
1291
1322
|
spawnProcess,
|
|
1323
|
+
sessionDirectory,
|
|
1324
|
+
sessionId,
|
|
1292
1325
|
limits: {
|
|
1293
1326
|
...limits,
|
|
1294
1327
|
...(limits.traceBytes === undefined ? {} : { traceBytes: limits.traceBytes - usedTraceBytes }),
|
|
@@ -1305,7 +1338,7 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1305
1338
|
},
|
|
1306
1339
|
});
|
|
1307
1340
|
next.task = task;
|
|
1308
|
-
runtime.aggregate = mergeTaskResults(runtime.aggregate, next, limits.
|
|
1341
|
+
runtime.aggregate = mergeTaskResults(runtime.aggregate, next, limits.traceRetentionBytes, limits.stderrRetentionBytes);
|
|
1309
1342
|
runtime.aggregate.task = task;
|
|
1310
1343
|
results[index] = cloneResult(runtime.aggregate);
|
|
1311
1344
|
savedResults.set(threadId, cloneResult(runtime.aggregate));
|
|
@@ -1321,11 +1354,16 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1321
1354
|
handoff: runtime.aggregate.output ? { summary: runtime.aggregate.output } : undefined,
|
|
1322
1355
|
});
|
|
1323
1356
|
}
|
|
1324
|
-
currentTask = buildSteeredTask(task, steering,
|
|
1357
|
+
currentTask = buildSteeredTask(task, steering, limits.taskCharacters);
|
|
1325
1358
|
}
|
|
1326
1359
|
} finally {
|
|
1327
1360
|
activeRuntimes.delete(threadId);
|
|
1328
1361
|
signal?.removeEventListener("abort", abortParent);
|
|
1362
|
+
try {
|
|
1363
|
+
await rm(sessionDirectory, { recursive: true, force: true });
|
|
1364
|
+
} catch {
|
|
1365
|
+
// Temporary child session cleanup is best effort after process termination.
|
|
1366
|
+
}
|
|
1329
1367
|
}
|
|
1330
1368
|
emit();
|
|
1331
1369
|
};
|