killeros 1.4.2 → 1.4.5
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 +18 -0
- package/README.md +7 -7
- package/agents/debugger.md +0 -1
- package/agents/documenter.md +0 -1
- package/agents/planner.md +0 -1
- package/agents/reviewer.md +1 -2
- package/agents/scout.md +2 -3
- package/agents/security.md +1 -2
- package/agents/tester.md +0 -1
- package/agents/worker.md +0 -1
- package/package.json +1 -1
- package/subagent-process.ts +45 -35
- package/subagent-ui.ts +26 -9
- package/subagents.ts +50 -41
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to KillerOS are documented here.
|
|
4
4
|
|
|
5
|
+
## [1.4.5] - 2026-08-01
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Removed the child-budget extension, its read-tool budget, and the default 250,000-token/$5 quota.
|
|
10
|
+
- Removed default child wall-time, JSONL-line, trace, stderr, returned-output, and model-output-length stops; role `timeoutMs` and other child guards are now opt-in.
|
|
11
|
+
- Removed forced early-report prompt text so roles can finish their assigned work naturally.
|
|
12
|
+
- Treat model stop reason `length` as a completed child process instead of inventing a KillerOS `limited` result.
|
|
13
|
+
- Documented the child lifecycle contract: children complete naturally; explicit user interruptions, configured guards, and real child-process failures remain visible.
|
|
14
|
+
|
|
15
|
+
## [1.4.3] - 2026-08-01
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- Added child-runtime tool budgets for read-only roles, with a soft finalization nudge and hard blocking for read and web tools after 32 calls.
|
|
20
|
+
- Added bounded child report instructions and kept read-tool budgets cumulative across steering restarts.
|
|
21
|
+
- Lowered the default child quota to 250,000 tokens and $5, and exposed child tool-call counts in results.
|
|
22
|
+
|
|
5
23
|
## [1.4.2] - 2026-08-01
|
|
6
24
|
|
|
7
25
|
### Added
|
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
|
-
pi install git:github.com/KyrosHendrix/pi-KillerOS@v1.4.
|
|
36
|
+
pi install git:github.com/KyrosHendrix/pi-KillerOS@v1.4.5
|
|
37
37
|
```
|
|
38
38
|
|
|
39
39
|
Add `-l` to either command for a project-only install. Restart Pi after installing.
|
|
@@ -47,7 +47,7 @@ Add `-l` to either command for a project-only install. Restart Pi after installi
|
|
|
47
47
|
- Responsive footer with polished model/provider identity, plain-language context, and active goal state remaining; reasoning, Git branch, elapsed time, cost, and path cut down by available width
|
|
48
48
|
- `/variants` selector and direct reasoning-level arguments
|
|
49
49
|
- Codex-style `/goal` for durable long-running objectives with pause, resume, edit, clear, automatic continuation, and explicit completion
|
|
50
|
-
- Pi-native `subagent` tool with named, inspectable child threads, Markdown roles, explicit read/write boundaries, parent controls,
|
|
50
|
+
- Pi-native `subagent` tool with named, inspectable child threads, Markdown roles, explicit read/write boundaries, parent controls, natural completion, and cancellation propagation
|
|
51
51
|
- Claude Code-style `/init` that scans the repository and generates a concise root `AGENTS.md` without setup questions
|
|
52
52
|
- `question` tool with filtering, proposal previews, keyboard selection, custom answers, history, cancellation, and resize-safe rendering
|
|
53
53
|
- Mid-prompt slash completion with current Pi `0.82.1` commands, extensions, prompts, and skills
|
|
@@ -89,7 +89,7 @@ KillerOS ships `planner`, `reviewer`, `scout`, and `security` as read-only roles
|
|
|
89
89
|
| `scout` | read | Map unfamiliar code and return an evidence trail |
|
|
90
90
|
| `security` | read | Audit trust boundaries and report concrete security findings |
|
|
91
91
|
| `tester` | write | Add focused coverage and run deterministic verification |
|
|
92
|
-
| `worker` | write | Execute
|
|
92
|
+
| `worker` | write | Execute the assigned repository change |
|
|
93
93
|
|
|
94
94
|
1. Bundled: `<killeros>/agents/*.md`
|
|
95
95
|
2. Personal: `~/.pi/agent/agents/*.md`
|
|
@@ -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 ephemeral `pi --mode json -p --no-session` processes with explicit local tools plus `web_search`, `source_check`, `fetch_content`, and `get_search_content`. 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`
|
|
106
|
+
Use the separate `model` and `thinking` fields for new configuration. The older `provider/model:thinking` model form remains accepted. Children run as ephemeral `pi --mode json -p --no-session` processes with explicit local tools plus `web_search`, `source_check`, `fetch_content`, and `get_search_content`. 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 quota. The parent still limits each request to eight 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
|
|
|
@@ -113,7 +113,7 @@ Threads move through `queued`, `active`, `done`, `failed`, `stopped`, and `close
|
|
|
113
113
|
|
|
114
114
|
The parent can inspect a thread’s prompt, role, model, tools, trace, usage, and handoff; steer an active thread with one bounded follow-up; interrupt one child or all active children; collect a concise handoff into parent context; and close a finished or stopped thread. An interrupt preserves the partial trace, states the reason, and reports the handoff as partial rather than successful.
|
|
115
115
|
|
|
116
|
-
A child completes naturally when it returns a final answer.
|
|
116
|
+
A child completes naturally when it returns a final answer. The default path has no per-child execution quota. Explicit embedding options can add wall-time, output, trace, stderr, JSONL, token, or cost guards; those guards report their cause and return partial work clearly. The parent still bounds task count, reader concurrency, role files, task input, and combined parent output. Explicit user interruptions and real child-process failures remain visible. Esc cancellation terminates active children and escalates after five seconds.
|
|
117
117
|
|
|
118
118
|
The replacement lifecycle has nine phases:
|
|
119
119
|
|
|
@@ -123,7 +123,7 @@ The replacement lifecycle has nine phases:
|
|
|
123
123
|
4. **Steer:** append a bounded parent follow-up to an active thread.
|
|
124
124
|
5. **Interrupt:** stop one or all active children while preserving partial work.
|
|
125
125
|
6. **Collect:** return a concise handoff while retaining the expanded trace.
|
|
126
|
-
7. **
|
|
126
|
+
7. **Guard:** honor only explicitly configured child resource guards; do not impose a routine turn stop.
|
|
127
127
|
8. **Close:** remove a finished or stopped thread from the workspace without deleting its result record.
|
|
128
128
|
9. **Prove:** test identity, visibility, controls, natural completion, guards, partial handoffs, and closure.
|
|
129
129
|
|
|
@@ -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
|
|
166
|
+
For release `1.4.5`, publish after the validation checks pass:
|
|
167
167
|
|
|
168
168
|
```bash
|
|
169
169
|
npm login
|
package/agents/debugger.md
CHANGED
package/agents/documenter.md
CHANGED
package/agents/planner.md
CHANGED
package/agents/reviewer.md
CHANGED
|
@@ -6,7 +6,6 @@ tools: read, grep, find, ls, web_search, source_check, fetch_content, get_search
|
|
|
6
6
|
# Replace inherit with provider/model to pin this role; set thinking separately when needed.
|
|
7
7
|
model: inherit
|
|
8
8
|
thinking: inherit
|
|
9
|
-
timeoutMs: 300000
|
|
10
9
|
---
|
|
11
10
|
|
|
12
11
|
# Role
|
|
@@ -39,7 +38,7 @@ If no concrete issue is found, report the reviewed scope, protections checked, a
|
|
|
39
38
|
|
|
40
39
|
## Boundaries
|
|
41
40
|
|
|
42
|
-
Do not edit files or fix findings yourself. Do not report stylistic preferences as defects. Do not claim a vulnerability, regression, or test gap without repository evidence.
|
|
41
|
+
Do not edit files or fix findings yourself. Do not report stylistic preferences as defects. Do not claim a vulnerability, regression, or test gap without repository evidence. Finish the assigned review when the evidence resolves its scope, report the findings or clean result, and do not chase unrelated work.
|
|
43
42
|
|
|
44
43
|
## Skills and web research
|
|
45
44
|
|
package/agents/scout.md
CHANGED
|
@@ -6,12 +6,11 @@ tools: read, grep, find, ls, web_search, source_check, fetch_content, get_search
|
|
|
6
6
|
# Replace inherit with provider/model to pin this role; set thinking separately when needed.
|
|
7
7
|
model: inherit
|
|
8
8
|
thinking: inherit
|
|
9
|
-
timeoutMs: 300000
|
|
10
9
|
---
|
|
11
10
|
|
|
12
11
|
# Role
|
|
13
12
|
|
|
14
|
-
You are the `scout` role, a
|
|
13
|
+
You are the `scout` role, a focused repository investigator. Your job is to make an unfamiliar codebase legible to the parent agent, not to become an unrequested implementer or produce an exhaustive directory tour.
|
|
15
14
|
|
|
16
15
|
## Mission
|
|
17
16
|
|
|
@@ -23,7 +22,7 @@ Begin with the user’s actual question and define what evidence would answer it
|
|
|
23
22
|
2. **Trace.** Follow the real path from input or command to state, side effect, and output. Follow callers and callees only when the current evidence requires it.
|
|
24
23
|
3. **Compare.** Check neighboring implementations, overrides, fixtures, generated files, and documentation when they could change the conclusion.
|
|
25
24
|
4. **Verify.** Record exact paths, symbols, commands, and conventions. Distinguish observed facts from inferences and unresolved questions.
|
|
26
|
-
5. **
|
|
25
|
+
5. **Finish.** Once evidence closes the parent’s question, report the findings and stop. Do not chase unrelated files, generic architecture advice, or speculative fixes.
|
|
27
26
|
|
|
28
27
|
## Report
|
|
29
28
|
|
package/agents/security.md
CHANGED
|
@@ -6,7 +6,6 @@ tools: read, grep, find, ls, web_search, source_check, fetch_content, get_search
|
|
|
6
6
|
# Replace inherit with provider/model to pin this role; set thinking separately when needed.
|
|
7
7
|
model: inherit
|
|
8
8
|
thinking: inherit
|
|
9
|
-
timeoutMs: 300000
|
|
10
9
|
---
|
|
11
10
|
|
|
12
11
|
# Role
|
|
@@ -35,7 +34,7 @@ Relevant lenses may include:
|
|
|
35
34
|
|
|
36
35
|
## Finding standard
|
|
37
36
|
|
|
38
|
-
Every confirmed finding must name the severity, precondition or trigger, affected path or symbol, evidence, security impact, and smallest safe correction. Separate questions and blind spots from findings. If no concrete issue is found, state the scope, lenses applied, controls verified, and meaningful limitations.
|
|
37
|
+
Every confirmed finding must name the severity, precondition or trigger, affected path or symbol, evidence, security impact, and smallest safe correction. Separate questions and blind spots from findings. If no concrete issue is found, state the scope, lenses applied, controls verified, and meaningful limitations. Finish the assigned review when the evidence resolves its scope, report the findings or clean result, and do not chase unrelated work.
|
|
39
38
|
|
|
40
39
|
## Skills and web research
|
|
41
40
|
|
package/agents/tester.md
CHANGED
package/agents/worker.md
CHANGED
package/package.json
CHANGED
package/subagent-process.ts
CHANGED
|
@@ -3,13 +3,6 @@ import { statSync } from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
5
|
export const SUBAGENT_PROCESS_LIMITS = {
|
|
6
|
-
wallTimeMs: 600_000,
|
|
7
|
-
jsonlLineBytes: 32 * 1024 * 1024,
|
|
8
|
-
traceBytes: 2 * 1024 * 1024,
|
|
9
|
-
stderrBytes: 64 * 1024,
|
|
10
|
-
outputBytes: 50 * 1024,
|
|
11
|
-
quotaTokens: 1_000_000,
|
|
12
|
-
quotaUsd: 10,
|
|
13
6
|
killGraceMs: 5_000,
|
|
14
7
|
} as const;
|
|
15
8
|
|
|
@@ -43,6 +36,7 @@ export interface SubagentProcessResult {
|
|
|
43
36
|
output: string;
|
|
44
37
|
outputBytes: number;
|
|
45
38
|
outputTruncatedBytes: number;
|
|
39
|
+
toolCallCount: number;
|
|
46
40
|
usage: SubagentProcessUsage;
|
|
47
41
|
model?: string;
|
|
48
42
|
stopReason?: string;
|
|
@@ -67,19 +61,20 @@ export interface SubagentProcessOptions {
|
|
|
67
61
|
cwd: string;
|
|
68
62
|
signal?: AbortSignal;
|
|
69
63
|
limits?: Partial<SubagentProcessLimits>;
|
|
64
|
+
environment?: NodeJS.ProcessEnv;
|
|
70
65
|
onUpdate?: (result: Readonly<SubagentProcessResult>) => void;
|
|
71
66
|
/** Test or embed hook. It receives the exact Pi arguments supplied above. */
|
|
72
|
-
spawnProcess?: (args: string[], cwd: string) => SubagentProcessChild;
|
|
67
|
+
spawnProcess?: (args: string[], cwd: string, environment?: NodeJS.ProcessEnv) => SubagentProcessChild;
|
|
73
68
|
}
|
|
74
69
|
|
|
75
70
|
export interface SubagentProcessLimits {
|
|
76
|
-
wallTimeMs
|
|
77
|
-
jsonlLineBytes
|
|
78
|
-
traceBytes
|
|
79
|
-
stderrBytes
|
|
80
|
-
outputBytes
|
|
81
|
-
quotaTokens
|
|
82
|
-
quotaUsd
|
|
71
|
+
wallTimeMs?: number;
|
|
72
|
+
jsonlLineBytes?: number;
|
|
73
|
+
traceBytes?: number;
|
|
74
|
+
stderrBytes?: number;
|
|
75
|
+
outputBytes?: number;
|
|
76
|
+
quotaTokens?: number;
|
|
77
|
+
quotaUsd?: number;
|
|
83
78
|
killGraceMs: number;
|
|
84
79
|
}
|
|
85
80
|
|
|
@@ -161,6 +156,11 @@ function textContent(message: any): string {
|
|
|
161
156
|
.join("\n");
|
|
162
157
|
}
|
|
163
158
|
|
|
159
|
+
function toolCallCount(message: any): number {
|
|
160
|
+
if (!Array.isArray(message?.content)) return 0;
|
|
161
|
+
return message.content.filter((part: any) => part?.type === "toolCall").length;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
164
|
function traceMessage(message: any): string[] {
|
|
165
165
|
if (!Array.isArray(message?.content)) return [];
|
|
166
166
|
const entries: string[] = [];
|
|
@@ -171,11 +171,11 @@ function traceMessage(message: any): string[] {
|
|
|
171
171
|
return entries;
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
function appendTrace(result: SubagentProcessResult, entries: string[], maxBytes: number): boolean {
|
|
174
|
+
function appendTrace(result: SubagentProcessResult, entries: string[], maxBytes: number | undefined): boolean {
|
|
175
175
|
let truncated = false;
|
|
176
176
|
for (const entry of entries) {
|
|
177
177
|
const entryBytes = Buffer.byteLength(entry, "utf8");
|
|
178
|
-
const retained = truncateUtf8(entry, Math.max(0, maxBytes - result.traceBytes));
|
|
178
|
+
const retained = truncateUtf8(entry, maxBytes === undefined ? entryBytes : Math.max(0, maxBytes - result.traceBytes));
|
|
179
179
|
if (retained.text) result.trace.push(retained.text);
|
|
180
180
|
result.traceBytes += Buffer.byteLength(retained.text, "utf8");
|
|
181
181
|
result.traceTruncatedBytes += entryBytes - Buffer.byteLength(retained.text, "utf8");
|
|
@@ -192,11 +192,11 @@ function normalizeLimits(overrides: Partial<SubagentProcessLimits> | undefined):
|
|
|
192
192
|
const limits = { ...SUBAGENT_PROCESS_LIMITS, ...overrides };
|
|
193
193
|
for (const name of ["wallTimeMs", "jsonlLineBytes", "traceBytes", "stderrBytes", "outputBytes", "killGraceMs"] as const) {
|
|
194
194
|
const value = limits[name];
|
|
195
|
-
if (!Number.isSafeInteger(value) || value <= 0) throw new RangeError(`${name} must be a positive safe integer`);
|
|
195
|
+
if (value !== undefined && (!Number.isSafeInteger(value) || value <= 0)) throw new RangeError(`${name} must be a positive safe integer`);
|
|
196
196
|
}
|
|
197
197
|
for (const name of ["quotaTokens", "quotaUsd"] as const) {
|
|
198
198
|
const value = limits[name];
|
|
199
|
-
if (!Number.isFinite(value) || value <= 0) throw new RangeError(`${name} must be a positive finite number`);
|
|
199
|
+
if (value !== undefined && (!Number.isFinite(value) || value <= 0)) throw new RangeError(`${name} must be a positive finite number`);
|
|
200
200
|
}
|
|
201
201
|
return limits;
|
|
202
202
|
}
|
|
@@ -224,12 +224,12 @@ export function subagentProcessEnvironment(environment: NodeJS.ProcessEnv = proc
|
|
|
224
224
|
return childEnvironment;
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
-
function defaultSpawnProcess(args: string[], cwd: string): SubagentProcessChild {
|
|
227
|
+
function defaultSpawnProcess(args: string[], cwd: string, environment?: NodeJS.ProcessEnv): SubagentProcessChild {
|
|
228
228
|
const invocation = getPiInvocation(args);
|
|
229
229
|
return spawn(invocation.command, invocation.args, {
|
|
230
230
|
cwd,
|
|
231
231
|
detached: process.platform !== "win32",
|
|
232
|
-
env: subagentProcessEnvironment(),
|
|
232
|
+
env: subagentProcessEnvironment({ ...process.env, ...environment }),
|
|
233
233
|
shell: false,
|
|
234
234
|
stdio: ["ignore", "pipe", "pipe"],
|
|
235
235
|
windowsHide: true,
|
|
@@ -263,7 +263,7 @@ function terminateProcess(child: SubagentProcessChild, force: boolean): void {
|
|
|
263
263
|
|
|
264
264
|
/**
|
|
265
265
|
* Run one isolated Pi JSON process. The caller owns all Pi arguments, including
|
|
266
|
-
* model, tools, prompt, and extension flags.
|
|
266
|
+
* model, tools, prompt, and extension flags. Resource limits are opt-in.
|
|
267
267
|
*/
|
|
268
268
|
export function runSubagentProcess(options: SubagentProcessOptions): SubagentProcessHandle {
|
|
269
269
|
const args = [...options.args];
|
|
@@ -283,6 +283,7 @@ export function runSubagentProcess(options: SubagentProcessOptions): SubagentPro
|
|
|
283
283
|
output: "",
|
|
284
284
|
outputBytes: 0,
|
|
285
285
|
outputTruncatedBytes: 0,
|
|
286
|
+
toolCallCount: 0,
|
|
286
287
|
usage: emptyUsage(),
|
|
287
288
|
exitCode: null,
|
|
288
289
|
durationMs: 0,
|
|
@@ -323,9 +324,12 @@ export function runSubagentProcess(options: SubagentProcessOptions): SubagentPro
|
|
|
323
324
|
if (requestedStatus) {
|
|
324
325
|
state.status = requestedStatus;
|
|
325
326
|
state.terminationReason = requestedReason;
|
|
326
|
-
} else if (code !== 0 || state.errorMessage || state.stopReason
|
|
327
|
-
state.status = "failed";
|
|
327
|
+
} else if (code !== 0 || state.errorMessage || state.stopReason === "error" || state.stopReason === "aborted") {
|
|
328
|
+
state.status = state.stopReason === "aborted" ? "cancelled" : "failed";
|
|
328
329
|
state.terminationReason ??= code === null ? "process_closed" : `exit_${code}`;
|
|
330
|
+
} else if (state.stopReason !== undefined && !["stop", "toolUse", "length"].includes(state.stopReason)) {
|
|
331
|
+
state.status = "failed";
|
|
332
|
+
state.terminationReason = state.stopReason;
|
|
329
333
|
} else if (state.usage.turns === 0) {
|
|
330
334
|
state.status = "failed";
|
|
331
335
|
state.terminationReason = "missing_assistant_message";
|
|
@@ -373,9 +377,10 @@ export function runSubagentProcess(options: SubagentProcessOptions): SubagentPro
|
|
|
373
377
|
}
|
|
374
378
|
state.usage.turns += 1;
|
|
375
379
|
addUsage(state.usage, { ...message.usage, turns: 0 });
|
|
376
|
-
|
|
380
|
+
state.toolCallCount += toolCallCount(message);
|
|
381
|
+
if (limits.quotaTokens !== undefined && state.usage.totalTokens > limits.quotaTokens) {
|
|
377
382
|
requestTermination("limited", "quota_tokens", `Child token usage exceeds ${limits.quotaTokens}`);
|
|
378
|
-
} else if (state.usage.cost.total > limits.quotaUsd) {
|
|
383
|
+
} else if (limits.quotaUsd !== undefined && state.usage.cost.total > limits.quotaUsd) {
|
|
379
384
|
requestTermination("limited", "quota_cost", `Child cost exceeds $${limits.quotaUsd}`);
|
|
380
385
|
}
|
|
381
386
|
if (appendTrace(state, traceMessage(message), limits.traceBytes)) {
|
|
@@ -383,13 +388,13 @@ export function runSubagentProcess(options: SubagentProcessOptions): SubagentPro
|
|
|
383
388
|
}
|
|
384
389
|
const output = textContent(message);
|
|
385
390
|
if (output) {
|
|
386
|
-
const capped = truncateUtf8(output, limits.outputBytes);
|
|
391
|
+
const capped = truncateUtf8(output, limits.outputBytes ?? Buffer.byteLength(output, "utf8"));
|
|
387
392
|
state.output = capped.text;
|
|
388
393
|
state.outputTruncatedBytes = capped.omittedBytes;
|
|
389
394
|
outputBytesSeen += Buffer.byteLength(output, "utf8");
|
|
390
395
|
state.outputBytes = outputBytesSeen;
|
|
391
|
-
state.outputTruncatedBytes = Math.max(state.outputTruncatedBytes, outputBytesSeen - limits.outputBytes);
|
|
392
|
-
if (outputBytesSeen > limits.outputBytes) requestTermination("limited", "output_limit", `Child output exceeds ${limits.outputBytes} bytes`);
|
|
396
|
+
state.outputTruncatedBytes = Math.max(state.outputTruncatedBytes, limits.outputBytes === undefined ? 0 : outputBytesSeen - limits.outputBytes);
|
|
397
|
+
if (limits.outputBytes !== undefined && outputBytesSeen > limits.outputBytes) requestTermination("limited", "output_limit", `Child output exceeds ${limits.outputBytes} bytes`);
|
|
393
398
|
}
|
|
394
399
|
if (typeof message.model === "string") state.model = message.provider ? `${message.provider}/${message.model}` : message.model;
|
|
395
400
|
if (typeof message.stopReason === "string") {
|
|
@@ -398,7 +403,6 @@ export function runSubagentProcess(options: SubagentProcessOptions): SubagentPro
|
|
|
398
403
|
else state.terminationReason = message.stopReason;
|
|
399
404
|
}
|
|
400
405
|
if (typeof message.errorMessage === "string") state.errorMessage = message.errorMessage;
|
|
401
|
-
if (message.stopReason === "length") requestTermination("limited", "model_output_limit");
|
|
402
406
|
publish();
|
|
403
407
|
} else if (event?.type === "tool_result_end" && event.message) {
|
|
404
408
|
const name = typeof event.message.toolName === "string" ? event.message.toolName : "tool";
|
|
@@ -410,12 +414,14 @@ export function runSubagentProcess(options: SubagentProcessOptions): SubagentPro
|
|
|
410
414
|
};
|
|
411
415
|
const appendStdout = (fragment: Buffer): boolean => {
|
|
412
416
|
const nextBytes = stdoutLineBytes + fragment.length;
|
|
413
|
-
if (nextBytes > limits.jsonlLineBytes) {
|
|
417
|
+
if (limits.jsonlLineBytes !== undefined && nextBytes > limits.jsonlLineBytes) {
|
|
414
418
|
requestTermination("limited", "jsonl_line_limit", `Child JSONL line exceeds ${limits.jsonlLineBytes} bytes`);
|
|
415
419
|
return false;
|
|
416
420
|
}
|
|
417
421
|
if (nextBytes > stdoutLine.length) {
|
|
418
|
-
const nextCapacity =
|
|
422
|
+
const nextCapacity = limits.jsonlLineBytes === undefined
|
|
423
|
+
? Math.max(nextBytes, stdoutLine.length * 2, 4_096)
|
|
424
|
+
: Math.min(limits.jsonlLineBytes, Math.max(nextBytes, stdoutLine.length * 2, 4_096));
|
|
419
425
|
const expanded = Buffer.allocUnsafe(nextCapacity);
|
|
420
426
|
stdoutLine.copy(expanded, 0, 0, stdoutLineBytes);
|
|
421
427
|
stdoutLine = expanded;
|
|
@@ -431,7 +437,9 @@ export function runSubagentProcess(options: SubagentProcessOptions): SubagentPro
|
|
|
431
437
|
abortHandler();
|
|
432
438
|
} else {
|
|
433
439
|
try {
|
|
434
|
-
child =
|
|
440
|
+
child = options.spawnProcess
|
|
441
|
+
? options.spawnProcess(args, options.cwd, options.environment)
|
|
442
|
+
: defaultSpawnProcess(args, options.cwd, options.environment);
|
|
435
443
|
child.stdout.on("data", (chunk: Buffer | string) => {
|
|
436
444
|
if (requestedStatus) return;
|
|
437
445
|
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
@@ -450,14 +458,16 @@ export function runSubagentProcess(options: SubagentProcessOptions): SubagentPro
|
|
|
450
458
|
});
|
|
451
459
|
child.stderr.on("data", (chunk: Buffer | string) => {
|
|
452
460
|
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
453
|
-
const retained =
|
|
461
|
+
const retained = limits.stderrBytes === undefined
|
|
462
|
+
? buffer
|
|
463
|
+
: buffer.subarray(0, Math.max(0, limits.stderrBytes - stderr.length));
|
|
454
464
|
if (retained.length) stderr = Buffer.concat([stderr, retained]);
|
|
455
465
|
state.stderrTruncatedBytes += buffer.length - retained.length;
|
|
456
466
|
if (buffer.length > retained.length) requestTermination("limited", "stderr_limit", `Child stderr exceeds ${limits.stderrBytes} bytes`);
|
|
457
467
|
});
|
|
458
468
|
child.on("error", (error) => requestTermination("failed", "spawn_error", error.message));
|
|
459
469
|
child.once("close", finish);
|
|
460
|
-
timeoutTimer = setTimeout(() => requestTermination("limited", "wall_time_limit"), limits.wallTimeMs);
|
|
470
|
+
if (limits.wallTimeMs !== undefined) timeoutTimer = setTimeout(() => requestTermination("limited", "wall_time_limit"), limits.wallTimeMs);
|
|
461
471
|
options.signal?.addEventListener("abort", abortHandler, { once: true });
|
|
462
472
|
if (options.signal?.aborted) abortHandler();
|
|
463
473
|
} catch (error) {
|
package/subagent-ui.ts
CHANGED
|
@@ -120,14 +120,31 @@ function toRecord(item: ThreadListItem): ThreadRecord {
|
|
|
120
120
|
};
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
|
|
123
|
+
function threadStatus(thread: Pick<ThreadRecord, "status" | "terminationReason">): ThreadStatus {
|
|
124
124
|
const reason = thread.terminationReason;
|
|
125
|
-
if (thread.status === "
|
|
126
|
-
if (thread.status === "
|
|
127
|
-
if (thread.status === "
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
125
|
+
if (thread.status === "failed" || reason === "error" || reason === "spawn_error" || reason === "process_closed" || reason === "missing_assistant_message" || reason === "malformed_jsonl" || reason === "invalid_usage" || reason?.startsWith("exit_")) return "failed";
|
|
126
|
+
if (thread.status === "cancelled" || reason === "abort" || reason === "interrupt") return "cancelled";
|
|
127
|
+
if (thread.status === "complete" || reason === "completed") return "complete";
|
|
128
|
+
return thread.status;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function threadReason(thread: Pick<ThreadRecord, "terminationReason" | "errorMessage">, status: ThreadStatus): string | undefined {
|
|
132
|
+
if (status === "cancelled" && thread.terminationReason === "interrupt") return "Interrupted by user.";
|
|
133
|
+
if (status === "cancelled" && thread.terminationReason === "abort") return "Cancelled by parent.";
|
|
134
|
+
if (status === "failed" && thread.errorMessage && thread.terminationReason) return `${thread.terminationReason}: ${thread.errorMessage}`;
|
|
135
|
+
if (status === "failed") return thread.errorMessage ?? thread.terminationReason ?? "Child process failed.";
|
|
136
|
+
return thread.terminationReason;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function formatThreadState(thread: Pick<ThreadRecord, "status" | "terminationReason" | "errorMessage">): ThreadStateView {
|
|
140
|
+
const status = threadStatus(thread);
|
|
141
|
+
const reason = threadReason(thread, status);
|
|
142
|
+
if (status === "complete") return { status, label: "Complete" };
|
|
143
|
+
if (status === "failed") return { status, label: "Failed", reason, partialWork: "Failed before completion. Any saved output is partial work." };
|
|
144
|
+
if (status === "cancelled") return { status, label: "Stopped", reason, partialWork: "Stopped before completion. Any saved output is partial work." };
|
|
145
|
+
if (status === "limited") return { status, label: "Limited", reason, partialWork: "Stopped at a limit before completion. Any saved output is partial work." };
|
|
146
|
+
if (status === "running") return { status, label: "Running", reason };
|
|
147
|
+
return { status, label: "Queued", reason };
|
|
131
148
|
}
|
|
132
149
|
|
|
133
150
|
export function formatThreadUsage(usage: ThreadUsage): ThreadUsageView {
|
|
@@ -148,9 +165,9 @@ export function formatThreadTrace(thread: Pick<ThreadRecord, "trace" | "traceTru
|
|
|
148
165
|
};
|
|
149
166
|
}
|
|
150
167
|
|
|
151
|
-
export function formatThreadHandoff(thread: Pick<ThreadRecord, "status" | "handoff" | "output">): ThreadHandoffView {
|
|
168
|
+
export function formatThreadHandoff(thread: Pick<ThreadRecord, "status" | "terminationReason" | "handoff" | "output">): ThreadHandoffView {
|
|
152
169
|
const text = thread.handoff ?? thread.output ?? "No handoff yet.";
|
|
153
|
-
return { label: "Handoff", text, isPartial: thread
|
|
170
|
+
return { label: "Handoff", text, isPartial: threadStatus(thread) !== "complete" };
|
|
154
171
|
}
|
|
155
172
|
|
|
156
173
|
export function formatThreadControls(status: ThreadStatus): readonly ThreadBoardControl[] {
|
package/subagents.ts
CHANGED
|
@@ -22,15 +22,7 @@ import { formatThreadBoard, formatThreadInspection, type ThreadRecord as ThreadB
|
|
|
22
22
|
export const SUBAGENT_LIMITS = {
|
|
23
23
|
maxTasks: 8,
|
|
24
24
|
maxReadConcurrency: 4,
|
|
25
|
-
defaultTimeoutMs: 300_000,
|
|
26
|
-
maxTimeoutMs: 600_000,
|
|
27
|
-
jsonlLineBytes: 32 * 1024 * 1024,
|
|
28
|
-
traceBytes: 2 * 1024 * 1024,
|
|
29
|
-
stderrBytes: 64 * 1024,
|
|
30
|
-
taskOutputBytes: 50 * 1024,
|
|
31
25
|
toolOutputBytes: 50 * 1024,
|
|
32
|
-
quotaTokens: 1_000_000,
|
|
33
|
-
quotaUsd: 10,
|
|
34
26
|
roleFileBytes: 64 * 1024,
|
|
35
27
|
taskCharacters: 20_000,
|
|
36
28
|
killGraceMs: 5_000,
|
|
@@ -58,7 +50,7 @@ export interface AgentRole {
|
|
|
58
50
|
tools: string[];
|
|
59
51
|
model?: string;
|
|
60
52
|
thinking?: string;
|
|
61
|
-
timeoutMs
|
|
53
|
+
timeoutMs?: number;
|
|
62
54
|
prompt: string;
|
|
63
55
|
source: AgentSource;
|
|
64
56
|
filePath: string;
|
|
@@ -105,6 +97,7 @@ export interface SubagentTaskResult {
|
|
|
105
97
|
output: string;
|
|
106
98
|
outputBytes: number;
|
|
107
99
|
outputTruncatedBytes: number;
|
|
100
|
+
toolCallCount: number;
|
|
108
101
|
usage: SubagentUsage;
|
|
109
102
|
durationMs: number;
|
|
110
103
|
exitCode: number | null;
|
|
@@ -149,13 +142,21 @@ interface SpawnedProcess {
|
|
|
149
142
|
once(event: "close", listener: (code: number | null) => void): this;
|
|
150
143
|
}
|
|
151
144
|
|
|
152
|
-
type SubagentLimits = { [Key in keyof typeof SUBAGENT_LIMITS]: number }
|
|
145
|
+
type SubagentLimits = { [Key in keyof typeof SUBAGENT_LIMITS]: number } & {
|
|
146
|
+
wallTimeMs?: number;
|
|
147
|
+
jsonlLineBytes?: number;
|
|
148
|
+
traceBytes?: number;
|
|
149
|
+
stderrBytes?: number;
|
|
150
|
+
taskOutputBytes?: number;
|
|
151
|
+
quotaTokens?: number;
|
|
152
|
+
quotaUsd?: number;
|
|
153
|
+
};
|
|
153
154
|
|
|
154
155
|
export interface SubagentRuntimeOptions {
|
|
155
156
|
bundledAgentsDir?: string;
|
|
156
157
|
userAgentsDir?: string;
|
|
157
158
|
webExtension?: string;
|
|
158
|
-
spawnProcess?: (args: string[], cwd: string) => SpawnedProcess;
|
|
159
|
+
spawnProcess?: (args: string[], cwd: string, environment?: NodeJS.ProcessEnv) => SpawnedProcess;
|
|
159
160
|
limits?: Partial<SubagentLimits>;
|
|
160
161
|
}
|
|
161
162
|
|
|
@@ -232,14 +233,15 @@ function optionalPositiveInteger(
|
|
|
232
233
|
frontmatter: Record<string, unknown>,
|
|
233
234
|
filePath: string,
|
|
234
235
|
field: string,
|
|
235
|
-
fallback
|
|
236
|
-
maximum
|
|
237
|
-
): number {
|
|
236
|
+
fallback?: number,
|
|
237
|
+
maximum?: number,
|
|
238
|
+
): number | undefined {
|
|
238
239
|
const value = frontmatter[field];
|
|
239
240
|
if (value === undefined || value === "") return fallback;
|
|
240
241
|
const parsed = typeof value === "number" ? value : typeof value === "string" ? Number(value) : Number.NaN;
|
|
241
|
-
if (!Number.isInteger(parsed) || parsed <= 0 || parsed > maximum) {
|
|
242
|
-
|
|
242
|
+
if (!Number.isInteger(parsed) || parsed <= 0 || maximum !== undefined && parsed > maximum) {
|
|
243
|
+
const bound = maximum === undefined ? "" : " no greater than " + maximum;
|
|
244
|
+
throw new AgentConfigurationError(filePath, field, `must be a positive integer${bound}`);
|
|
243
245
|
}
|
|
244
246
|
return parsed;
|
|
245
247
|
}
|
|
@@ -296,7 +298,7 @@ function parseAgentFile(filePath: string, source: AgentSource, limits: SubagentL
|
|
|
296
298
|
tools,
|
|
297
299
|
model: typeof modelValue === "string" ? modelValue.trim() : undefined,
|
|
298
300
|
thinking: typeof thinkingValue === "string" ? thinkingValue.trim() : undefined,
|
|
299
|
-
timeoutMs: optionalPositiveInteger(frontmatter, filePath, "timeoutMs"
|
|
301
|
+
timeoutMs: optionalPositiveInteger(frontmatter, filePath, "timeoutMs"),
|
|
300
302
|
prompt,
|
|
301
303
|
source,
|
|
302
304
|
filePath,
|
|
@@ -492,6 +494,7 @@ function makeQueuedResult(id: string, agent: string, task: string, step?: number
|
|
|
492
494
|
output: "",
|
|
493
495
|
outputBytes: 0,
|
|
494
496
|
outputTruncatedBytes: 0,
|
|
497
|
+
toolCallCount: 0,
|
|
495
498
|
usage: emptyUsage(),
|
|
496
499
|
durationMs: 0,
|
|
497
500
|
exitCode: null,
|
|
@@ -508,14 +511,14 @@ function cloneResult(result: SubagentTaskResult): SubagentTaskResult {
|
|
|
508
511
|
};
|
|
509
512
|
}
|
|
510
513
|
|
|
511
|
-
function mergeTaskResults(previous: SubagentTaskResult | undefined, next: SubagentTaskResult, maxTraceBytes
|
|
514
|
+
function mergeTaskResults(previous: SubagentTaskResult | undefined, next: SubagentTaskResult, maxTraceBytes?: number): SubagentTaskResult {
|
|
512
515
|
if (!previous) return cloneResult(next);
|
|
513
516
|
const merged = cloneResult(next);
|
|
514
517
|
const trace: string[] = [];
|
|
515
518
|
let traceBytes = 0;
|
|
516
519
|
let traceTruncatedBytes = previous.traceTruncatedBytes + next.traceTruncatedBytes;
|
|
517
520
|
for (const entry of [...previous.trace, ...next.trace]) {
|
|
518
|
-
const retained = truncateUtf8(entry, Math.max(0, maxTraceBytes - traceBytes));
|
|
521
|
+
const retained = truncateUtf8(entry, maxTraceBytes === undefined ? Buffer.byteLength(entry, "utf8") : Math.max(0, maxTraceBytes - traceBytes));
|
|
519
522
|
if (retained.text) trace.push(retained.text);
|
|
520
523
|
const retainedBytes = Buffer.byteLength(retained.text, "utf8");
|
|
521
524
|
traceBytes += retainedBytes;
|
|
@@ -530,6 +533,7 @@ function mergeTaskResults(previous: SubagentTaskResult | undefined, next: Subage
|
|
|
530
533
|
merged.output = next.output || previous.output;
|
|
531
534
|
merged.outputBytes = previous.outputBytes + next.outputBytes;
|
|
532
535
|
merged.outputTruncatedBytes = previous.outputTruncatedBytes + next.outputTruncatedBytes;
|
|
536
|
+
merged.toolCallCount = previous.toolCallCount + next.toolCallCount;
|
|
533
537
|
merged.usage = emptyUsage();
|
|
534
538
|
addUsage(merged.usage, previous.usage);
|
|
535
539
|
addUsage(merged.usage, next.usage);
|
|
@@ -557,7 +561,7 @@ interface RunTaskOptions {
|
|
|
557
561
|
step?: number;
|
|
558
562
|
model: ResolvedModel;
|
|
559
563
|
signal?: AbortSignal;
|
|
560
|
-
spawnProcess: (args: string[], cwd: string) => SpawnedProcess;
|
|
564
|
+
spawnProcess: (args: string[], cwd: string, environment?: NodeJS.ProcessEnv) => SpawnedProcess;
|
|
561
565
|
webExtension?: string;
|
|
562
566
|
projectTrusted: boolean;
|
|
563
567
|
limits: SubagentLimits;
|
|
@@ -582,6 +586,7 @@ function applyProcessResult(
|
|
|
582
586
|
target.output = source.output;
|
|
583
587
|
target.outputBytes = source.outputBytes;
|
|
584
588
|
target.outputTruncatedBytes = source.outputTruncatedBytes;
|
|
589
|
+
target.toolCallCount = source.toolCallCount;
|
|
585
590
|
target.usage = { ...source.usage, cost: { ...source.usage.cost } };
|
|
586
591
|
target.model = source.model ?? target.model;
|
|
587
592
|
target.terminationReason = source.terminationReason;
|
|
@@ -643,13 +648,13 @@ async function runTask(options: RunTaskOptions): Promise<SubagentTaskResult> {
|
|
|
643
648
|
signal: options.signal,
|
|
644
649
|
spawnProcess: options.spawnProcess,
|
|
645
650
|
limits: {
|
|
646
|
-
wallTimeMs: options.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,
|
|
651
|
+
...(options.timeoutMs === undefined ? {} : { wallTimeMs: options.timeoutMs }),
|
|
652
|
+
...(limits.jsonlLineBytes === undefined ? {} : { jsonlLineBytes: limits.jsonlLineBytes }),
|
|
653
|
+
...(limits.traceBytes === undefined ? {} : { traceBytes: limits.traceBytes }),
|
|
654
|
+
...(limits.stderrBytes === undefined ? {} : { stderrBytes: limits.stderrBytes }),
|
|
655
|
+
...(limits.taskOutputBytes === undefined ? {} : { outputBytes: limits.taskOutputBytes }),
|
|
656
|
+
...(limits.quotaTokens === undefined ? {} : { quotaTokens: limits.quotaTokens }),
|
|
657
|
+
...(limits.quotaUsd === undefined ? {} : { quotaUsd: limits.quotaUsd }),
|
|
653
658
|
killGraceMs: limits.killGraceMs,
|
|
654
659
|
},
|
|
655
660
|
onUpdate: (next) => applyProcessResult(result, next, startedAt, options.onChange),
|
|
@@ -861,6 +866,7 @@ function threadResult(thread: SubagentThread, source?: SubagentTaskResult): Suba
|
|
|
861
866
|
turns: thread.usage.turns,
|
|
862
867
|
},
|
|
863
868
|
output: thread.result ?? "",
|
|
869
|
+
toolCallCount: 0,
|
|
864
870
|
terminationReason: thread.stopReason,
|
|
865
871
|
};
|
|
866
872
|
}
|
|
@@ -960,7 +966,9 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
960
966
|
const from = runtime?.traceCount ?? 0;
|
|
961
967
|
let retainedTraceBytes = thread.trace.reduce((total, entry) => total + Buffer.byteLength(entry.message ?? "", "utf8"), 0);
|
|
962
968
|
for (const entry of next.trace.slice(from)) {
|
|
963
|
-
const retained = truncateUtf8(entry,
|
|
969
|
+
const retained = truncateUtf8(entry, limits.traceBytes === undefined
|
|
970
|
+
? Buffer.byteLength(entry, "utf8")
|
|
971
|
+
: Math.max(0, limits.traceBytes - retainedTraceBytes));
|
|
964
972
|
if (retained.text) {
|
|
965
973
|
threads.trace(threadId, { kind: "child", message: retained.text });
|
|
966
974
|
retainedTraceBytes += Buffer.byteLength(retained.text, "utf8");
|
|
@@ -1238,33 +1246,34 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1238
1246
|
try {
|
|
1239
1247
|
while (true) {
|
|
1240
1248
|
const aggregate = runtime.aggregate;
|
|
1241
|
-
const
|
|
1249
|
+
const wallTimeMs = agent.timeoutMs ?? limits.wallTimeMs;
|
|
1250
|
+
const remainingWallTimeMs = wallTimeMs === undefined ? undefined : wallTimeMs - (Date.now() - runtime.startedAt);
|
|
1242
1251
|
const usedTraceBytes = aggregate?.traceBytes ?? 0;
|
|
1243
1252
|
const usedStderrBytes = aggregate?.stderrBytes ?? 0;
|
|
1244
1253
|
const usedOutputBytes = aggregate?.outputBytes ?? 0;
|
|
1245
1254
|
const usedTokens = aggregate?.usage.totalTokens ?? 0;
|
|
1246
1255
|
const usedCost = aggregate?.usage.cost.total ?? 0;
|
|
1247
|
-
if (remainingWallTimeMs <= 0) {
|
|
1248
|
-
stopForBudget("wall_time_limit", `Child thread exceeds ${
|
|
1256
|
+
if (remainingWallTimeMs !== undefined && remainingWallTimeMs <= 0) {
|
|
1257
|
+
stopForBudget("wall_time_limit", `Child thread exceeds ${wallTimeMs} ms`);
|
|
1249
1258
|
break;
|
|
1250
1259
|
}
|
|
1251
|
-
if (usedTraceBytes >= limits.traceBytes || aggregate?.traceTruncatedBytes) {
|
|
1260
|
+
if (limits.traceBytes !== undefined && (usedTraceBytes >= limits.traceBytes || aggregate?.traceTruncatedBytes)) {
|
|
1252
1261
|
stopForBudget("trace_limit", `Child thread retains more than ${limits.traceBytes} trace bytes`);
|
|
1253
1262
|
break;
|
|
1254
1263
|
}
|
|
1255
|
-
if (usedStderrBytes >= limits.stderrBytes || aggregate?.stderrTruncatedBytes) {
|
|
1264
|
+
if (limits.stderrBytes !== undefined && (usedStderrBytes >= limits.stderrBytes || aggregate?.stderrTruncatedBytes)) {
|
|
1256
1265
|
stopForBudget("stderr_limit", `Child thread emits more than ${limits.stderrBytes} stderr bytes`);
|
|
1257
1266
|
break;
|
|
1258
1267
|
}
|
|
1259
|
-
if (usedOutputBytes >= limits.taskOutputBytes || aggregate?.outputTruncatedBytes) {
|
|
1268
|
+
if (limits.taskOutputBytes !== undefined && (usedOutputBytes >= limits.taskOutputBytes || aggregate?.outputTruncatedBytes)) {
|
|
1260
1269
|
stopForBudget("output_limit", `Child thread emits more than ${limits.taskOutputBytes} output bytes`);
|
|
1261
1270
|
break;
|
|
1262
1271
|
}
|
|
1263
|
-
if (usedTokens >= limits.quotaTokens) {
|
|
1272
|
+
if (limits.quotaTokens !== undefined && usedTokens >= limits.quotaTokens) {
|
|
1264
1273
|
stopForBudget("quota_tokens", `Child thread exceeds ${limits.quotaTokens} tokens`);
|
|
1265
1274
|
break;
|
|
1266
1275
|
}
|
|
1267
|
-
if (usedCost >= limits.quotaUsd) {
|
|
1276
|
+
if (limits.quotaUsd !== undefined && usedCost >= limits.quotaUsd) {
|
|
1268
1277
|
stopForBudget("quota_cost", `Child thread exceeds $${limits.quotaUsd}`);
|
|
1269
1278
|
break;
|
|
1270
1279
|
}
|
|
@@ -1282,11 +1291,11 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1282
1291
|
spawnProcess,
|
|
1283
1292
|
limits: {
|
|
1284
1293
|
...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,
|
|
1294
|
+
...(limits.traceBytes === undefined ? {} : { traceBytes: limits.traceBytes - usedTraceBytes }),
|
|
1295
|
+
...(limits.stderrBytes === undefined ? {} : { stderrBytes: limits.stderrBytes - usedStderrBytes }),
|
|
1296
|
+
...(limits.taskOutputBytes === undefined ? {} : { taskOutputBytes: limits.taskOutputBytes - usedOutputBytes }),
|
|
1297
|
+
...(limits.quotaTokens === undefined ? {} : { quotaTokens: limits.quotaTokens - usedTokens }),
|
|
1298
|
+
...(limits.quotaUsd === undefined ? {} : { quotaUsd: limits.quotaUsd - usedCost }),
|
|
1290
1299
|
},
|
|
1291
1300
|
timeoutMs: remainingWallTimeMs,
|
|
1292
1301
|
onHandle: (handle) => { runtime.handle = handle; },
|