killeros 1.4.2 → 1.4.3
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 +8 -0
- package/README.md +4 -4
- package/package.json +2 -1
- package/subagent-budget.ts +65 -0
- package/subagent-process.ts +17 -6
- package/subagents.ts +41 -5
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to KillerOS are documented here.
|
|
4
4
|
|
|
5
|
+
## [1.4.3] - 2026-08-01
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- 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.
|
|
10
|
+
- Added bounded child report instructions and kept read-tool budgets cumulative across steering restarts.
|
|
11
|
+
- Lowered the default child quota to 250,000 tokens and $5, and exposed child tool-call counts in results.
|
|
12
|
+
|
|
5
13
|
## [1.4.2] - 2026-08-01
|
|
6
14
|
|
|
7
15
|
### 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.3
|
|
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 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
|
|
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` and the KillerOS child-budget hook, 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`, work in bounded passes, and report after its first useful evidence. KillerOS allows at most eight tasks, four parallel readers, ten minutes, a 32 MiB JSONL line, 2 MiB retained trace, 64 KiB stderr, 50 KiB returned output, 250,000 tokens, and $5 per child. Read-only children receive a 24-call soft and 32-call hard tool budget that blocks read and web tools after the hard limit; final reports remain allowed. 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. Routine turn caps do not end useful work. Named resource guards still stop unsafe growth: wall time, output bytes, retained trace, stderr, quota, task count, and concurrency. A guard reports its cause and returns any partial work clearly. Esc cancellation terminates active children and escalates after five seconds.
|
|
116
|
+
A child completes naturally when it returns a final answer. Routine turn caps do not end useful work. Named resource guards still stop unsafe growth: wall time, output bytes, retained trace, stderr, quota, read-tool calls, task count, and concurrency. Read-tool budgets are enforced inside the child process, so the model receives a finalization nudge and a blocked-tool result instead of being killed while it is still researching. A guard reports its cause and returns any partial work clearly. Esc cancellation terminates active children and escalates after five seconds.
|
|
117
117
|
|
|
118
118
|
The replacement lifecycle has nine phases:
|
|
119
119
|
|
|
@@ -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 the current unreleased `1.4.
|
|
166
|
+
For the current unreleased `1.4.3`, publish after the validation checks pass:
|
|
167
167
|
|
|
168
168
|
```bash
|
|
169
169
|
npm login
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "killeros",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.3",
|
|
4
4
|
"description": "A production-hardened TUI and workflow extension for the Pi coding agent.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"subagents.ts",
|
|
25
25
|
"subagent-lifecycle.ts",
|
|
26
26
|
"subagent-process.ts",
|
|
27
|
+
"subagent-budget.ts",
|
|
27
28
|
"subagent-ui.ts",
|
|
28
29
|
"agents/*.md",
|
|
29
30
|
"themes/killeros.json",
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { ExtensionAPI, ToolCallEventResult } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
export const CHILD_TOOL_BUDGET_ENV = "PI_KILLEROS_TOOL_BUDGET";
|
|
4
|
+
|
|
5
|
+
export interface ChildToolBudget {
|
|
6
|
+
soft?: number;
|
|
7
|
+
hard: number;
|
|
8
|
+
block: "*" | string[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function parseChildToolBudget(value: string | undefined): ChildToolBudget | undefined {
|
|
12
|
+
if (!value?.trim()) return undefined;
|
|
13
|
+
try {
|
|
14
|
+
const parsed = JSON.parse(value) as Record<string, unknown>;
|
|
15
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
|
|
16
|
+
const hard = parsed.hard;
|
|
17
|
+
const soft = parsed.soft;
|
|
18
|
+
const block = parsed.block;
|
|
19
|
+
if (typeof hard !== "number" || !Number.isSafeInteger(hard) || hard < 1) return undefined;
|
|
20
|
+
if (soft !== undefined && (typeof soft !== "number" || !Number.isSafeInteger(soft) || soft < 1 || soft > hard)) return undefined;
|
|
21
|
+
if (block !== "*" && (!Array.isArray(block) || block.length === 0 || block.some((tool) => typeof tool !== "string" || !tool.trim()))) return undefined;
|
|
22
|
+
return {
|
|
23
|
+
hard,
|
|
24
|
+
...(soft === undefined ? {} : { soft }),
|
|
25
|
+
block: block === "*" ? "*" : [...new Set((block as string[]).map((tool) => tool.trim()))],
|
|
26
|
+
};
|
|
27
|
+
} catch {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function softNudge(budget: ChildToolBudget, calls: number): string {
|
|
33
|
+
return `Tool budget soft limit reached after ${calls} tool call${calls === 1 ? "" : "s"} (soft ${budget.soft}, hard ${budget.hard}). Stop starting new browsing/search work and finalize from the context you already have.`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function shouldBlock(budget: ChildToolBudget, toolName: string, calls: number): boolean {
|
|
37
|
+
return calls > budget.hard && (budget.block === "*" || budget.block.includes(toolName));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default function registerSubagentBudget(pi: ExtensionAPI): void {
|
|
41
|
+
const budget = parseChildToolBudget(process.env[CHILD_TOOL_BUDGET_ENV]);
|
|
42
|
+
if (!budget) return;
|
|
43
|
+
let calls = 0;
|
|
44
|
+
let nudged = false;
|
|
45
|
+
const sendUserMessage = (pi as unknown as {
|
|
46
|
+
sendUserMessage?: (content: string, options: { deliverAs: "steer" }) => unknown;
|
|
47
|
+
}).sendUserMessage;
|
|
48
|
+
|
|
49
|
+
pi.on("tool_call", (event): ToolCallEventResult | void => {
|
|
50
|
+
calls += 1;
|
|
51
|
+
if (!nudged && budget.soft !== undefined && calls >= budget.soft) {
|
|
52
|
+
nudged = true;
|
|
53
|
+
try {
|
|
54
|
+
sendUserMessage?.(softNudge(budget, calls), { deliverAs: "steer" });
|
|
55
|
+
} catch {
|
|
56
|
+
// The hard block below remains authoritative if steering is unavailable.
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (!shouldBlock(budget, event.toolName, calls)) return undefined;
|
|
60
|
+
return {
|
|
61
|
+
block: true,
|
|
62
|
+
reason: `Tool budget hard limit reached after ${calls} tool calls (hard ${budget.hard}). Finalize from the context you already have.`,
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
}
|
package/subagent-process.ts
CHANGED
|
@@ -8,8 +8,8 @@ export const SUBAGENT_PROCESS_LIMITS = {
|
|
|
8
8
|
traceBytes: 2 * 1024 * 1024,
|
|
9
9
|
stderrBytes: 64 * 1024,
|
|
10
10
|
outputBytes: 50 * 1024,
|
|
11
|
-
quotaTokens:
|
|
12
|
-
quotaUsd:
|
|
11
|
+
quotaTokens: 250_000,
|
|
12
|
+
quotaUsd: 5,
|
|
13
13
|
killGraceMs: 5_000,
|
|
14
14
|
} as const;
|
|
15
15
|
|
|
@@ -43,6 +43,7 @@ export interface SubagentProcessResult {
|
|
|
43
43
|
output: string;
|
|
44
44
|
outputBytes: number;
|
|
45
45
|
outputTruncatedBytes: number;
|
|
46
|
+
toolCallCount: number;
|
|
46
47
|
usage: SubagentProcessUsage;
|
|
47
48
|
model?: string;
|
|
48
49
|
stopReason?: string;
|
|
@@ -67,9 +68,10 @@ export interface SubagentProcessOptions {
|
|
|
67
68
|
cwd: string;
|
|
68
69
|
signal?: AbortSignal;
|
|
69
70
|
limits?: Partial<SubagentProcessLimits>;
|
|
71
|
+
environment?: NodeJS.ProcessEnv;
|
|
70
72
|
onUpdate?: (result: Readonly<SubagentProcessResult>) => void;
|
|
71
73
|
/** Test or embed hook. It receives the exact Pi arguments supplied above. */
|
|
72
|
-
spawnProcess?: (args: string[], cwd: string) => SubagentProcessChild;
|
|
74
|
+
spawnProcess?: (args: string[], cwd: string, environment?: NodeJS.ProcessEnv) => SubagentProcessChild;
|
|
73
75
|
}
|
|
74
76
|
|
|
75
77
|
export interface SubagentProcessLimits {
|
|
@@ -161,6 +163,11 @@ function textContent(message: any): string {
|
|
|
161
163
|
.join("\n");
|
|
162
164
|
}
|
|
163
165
|
|
|
166
|
+
function toolCallCount(message: any): number {
|
|
167
|
+
if (!Array.isArray(message?.content)) return 0;
|
|
168
|
+
return message.content.filter((part: any) => part?.type === "toolCall").length;
|
|
169
|
+
}
|
|
170
|
+
|
|
164
171
|
function traceMessage(message: any): string[] {
|
|
165
172
|
if (!Array.isArray(message?.content)) return [];
|
|
166
173
|
const entries: string[] = [];
|
|
@@ -224,12 +231,12 @@ export function subagentProcessEnvironment(environment: NodeJS.ProcessEnv = proc
|
|
|
224
231
|
return childEnvironment;
|
|
225
232
|
}
|
|
226
233
|
|
|
227
|
-
function defaultSpawnProcess(args: string[], cwd: string): SubagentProcessChild {
|
|
234
|
+
function defaultSpawnProcess(args: string[], cwd: string, environment?: NodeJS.ProcessEnv): SubagentProcessChild {
|
|
228
235
|
const invocation = getPiInvocation(args);
|
|
229
236
|
return spawn(invocation.command, invocation.args, {
|
|
230
237
|
cwd,
|
|
231
238
|
detached: process.platform !== "win32",
|
|
232
|
-
env: subagentProcessEnvironment(),
|
|
239
|
+
env: subagentProcessEnvironment({ ...process.env, ...environment }),
|
|
233
240
|
shell: false,
|
|
234
241
|
stdio: ["ignore", "pipe", "pipe"],
|
|
235
242
|
windowsHide: true,
|
|
@@ -283,6 +290,7 @@ export function runSubagentProcess(options: SubagentProcessOptions): SubagentPro
|
|
|
283
290
|
output: "",
|
|
284
291
|
outputBytes: 0,
|
|
285
292
|
outputTruncatedBytes: 0,
|
|
293
|
+
toolCallCount: 0,
|
|
286
294
|
usage: emptyUsage(),
|
|
287
295
|
exitCode: null,
|
|
288
296
|
durationMs: 0,
|
|
@@ -373,6 +381,7 @@ export function runSubagentProcess(options: SubagentProcessOptions): SubagentPro
|
|
|
373
381
|
}
|
|
374
382
|
state.usage.turns += 1;
|
|
375
383
|
addUsage(state.usage, { ...message.usage, turns: 0 });
|
|
384
|
+
state.toolCallCount += toolCallCount(message);
|
|
376
385
|
if (state.usage.totalTokens > limits.quotaTokens) {
|
|
377
386
|
requestTermination("limited", "quota_tokens", `Child token usage exceeds ${limits.quotaTokens}`);
|
|
378
387
|
} else if (state.usage.cost.total > limits.quotaUsd) {
|
|
@@ -431,7 +440,9 @@ export function runSubagentProcess(options: SubagentProcessOptions): SubagentPro
|
|
|
431
440
|
abortHandler();
|
|
432
441
|
} else {
|
|
433
442
|
try {
|
|
434
|
-
child =
|
|
443
|
+
child = options.spawnProcess
|
|
444
|
+
? options.spawnProcess(args, options.cwd, options.environment)
|
|
445
|
+
: defaultSpawnProcess(args, options.cwd, options.environment);
|
|
435
446
|
child.stdout.on("data", (chunk: Buffer | string) => {
|
|
436
447
|
if (requestedStatus) return;
|
|
437
448
|
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
package/subagents.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
} from "@earendil-works/pi-coding-agent";
|
|
16
16
|
import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
|
|
17
17
|
import { Type } from "typebox";
|
|
18
|
+
import { CHILD_TOOL_BUDGET_ENV, type ChildToolBudget } from "./subagent-budget.ts";
|
|
18
19
|
import { SubagentThreadRegistry, type SubagentThread, type SubagentThreadId, type SubagentThreadState } from "./subagent-lifecycle.ts";
|
|
19
20
|
import { runSubagentProcess, type SubagentProcessHandle, type SubagentProcessResult } from "./subagent-process.ts";
|
|
20
21
|
import { formatThreadBoard, formatThreadInspection, type ThreadRecord as ThreadBoardRecord } from "./subagent-ui.ts";
|
|
@@ -29,8 +30,10 @@ export const SUBAGENT_LIMITS = {
|
|
|
29
30
|
stderrBytes: 64 * 1024,
|
|
30
31
|
taskOutputBytes: 50 * 1024,
|
|
31
32
|
toolOutputBytes: 50 * 1024,
|
|
32
|
-
quotaTokens:
|
|
33
|
-
quotaUsd:
|
|
33
|
+
quotaTokens: 250_000,
|
|
34
|
+
quotaUsd: 5,
|
|
35
|
+
readToolBudgetSoft: 24,
|
|
36
|
+
readToolBudgetHard: 32,
|
|
34
37
|
roleFileBytes: 64 * 1024,
|
|
35
38
|
taskCharacters: 20_000,
|
|
36
39
|
killGraceMs: 5_000,
|
|
@@ -41,9 +44,16 @@ const READ_TOOLS = new Set(["read", "grep", "find", "ls", ...WEB_TOOLS]);
|
|
|
41
44
|
const WRITE_TOOLS = new Set(["bash", "edit", "write"]);
|
|
42
45
|
const KNOWN_TOOLS = new Set([...READ_TOOLS, ...WRITE_TOOLS]);
|
|
43
46
|
const SUBAGENT_WEB_EXTENSION = "npm:pi-web-access";
|
|
47
|
+
const SUBAGENT_BUDGET_EXTENSION = fileURLToPath(new URL("./subagent-budget.ts", import.meta.url));
|
|
44
48
|
const INHERIT_SETTING = "inherit";
|
|
45
49
|
const MAX_RUNTIME_STEERING_MESSAGES = 20;
|
|
46
50
|
const ROLE_FIELDS = new Set(["name", "description", "access", "tools", "model", "thinking", "timeoutMs"]);
|
|
51
|
+
const CHILD_REPORT_PROTOCOL = [
|
|
52
|
+
"## Child report protocol",
|
|
53
|
+
"Work in bounded passes. After the first useful evidence, write a concise report with findings, exact files, checks run, and remaining work.",
|
|
54
|
+
"Do not keep opening files or searching after you have enough evidence to answer the task.",
|
|
55
|
+
"If a tool budget notice or blocked-tool message appears, stop research and report from the context you have. A partial report is better than no report.",
|
|
56
|
+
].join("\n");
|
|
47
57
|
|
|
48
58
|
type ThinkingLevel = ModelThinkingLevel;
|
|
49
59
|
export type AgentAccess = "read" | "write";
|
|
@@ -105,6 +115,7 @@ export interface SubagentTaskResult {
|
|
|
105
115
|
output: string;
|
|
106
116
|
outputBytes: number;
|
|
107
117
|
outputTruncatedBytes: number;
|
|
118
|
+
toolCallCount: number;
|
|
108
119
|
usage: SubagentUsage;
|
|
109
120
|
durationMs: number;
|
|
110
121
|
exitCode: number | null;
|
|
@@ -155,7 +166,7 @@ export interface SubagentRuntimeOptions {
|
|
|
155
166
|
bundledAgentsDir?: string;
|
|
156
167
|
userAgentsDir?: string;
|
|
157
168
|
webExtension?: string;
|
|
158
|
-
spawnProcess?: (args: string[], cwd: string) => SpawnedProcess;
|
|
169
|
+
spawnProcess?: (args: string[], cwd: string, environment?: NodeJS.ProcessEnv) => SpawnedProcess;
|
|
159
170
|
limits?: Partial<SubagentLimits>;
|
|
160
171
|
}
|
|
161
172
|
|
|
@@ -492,6 +503,7 @@ function makeQueuedResult(id: string, agent: string, task: string, step?: number
|
|
|
492
503
|
output: "",
|
|
493
504
|
outputBytes: 0,
|
|
494
505
|
outputTruncatedBytes: 0,
|
|
506
|
+
toolCallCount: 0,
|
|
495
507
|
usage: emptyUsage(),
|
|
496
508
|
durationMs: 0,
|
|
497
509
|
exitCode: null,
|
|
@@ -530,6 +542,7 @@ function mergeTaskResults(previous: SubagentTaskResult | undefined, next: Subage
|
|
|
530
542
|
merged.output = next.output || previous.output;
|
|
531
543
|
merged.outputBytes = previous.outputBytes + next.outputBytes;
|
|
532
544
|
merged.outputTruncatedBytes = previous.outputTruncatedBytes + next.outputTruncatedBytes;
|
|
545
|
+
merged.toolCallCount = previous.toolCallCount + next.toolCallCount;
|
|
533
546
|
merged.usage = emptyUsage();
|
|
534
547
|
addUsage(merged.usage, previous.usage);
|
|
535
548
|
addUsage(merged.usage, next.usage);
|
|
@@ -545,7 +558,7 @@ function cloneDetails(mode: SubagentDetails["mode"], scope: AgentScope, projectA
|
|
|
545
558
|
async function writeRolePrompt(agent: AgentRole): Promise<{ directory: string; filePath: string }> {
|
|
546
559
|
const directory = await mkdtemp(path.join(os.tmpdir(), "killeros-subagent-"));
|
|
547
560
|
const filePath = path.join(directory, `${agent.name.replace(/[^A-Za-z0-9_.-]/gu, "_")}.md`);
|
|
548
|
-
await writeFile(filePath, agent.prompt
|
|
561
|
+
await writeFile(filePath, `${agent.prompt}\n\n${CHILD_REPORT_PROTOCOL}`, { encoding: "utf8", mode: 0o600 });
|
|
549
562
|
return { directory, filePath };
|
|
550
563
|
}
|
|
551
564
|
|
|
@@ -557,13 +570,14 @@ interface RunTaskOptions {
|
|
|
557
570
|
step?: number;
|
|
558
571
|
model: ResolvedModel;
|
|
559
572
|
signal?: AbortSignal;
|
|
560
|
-
spawnProcess: (args: string[], cwd: string) => SpawnedProcess;
|
|
573
|
+
spawnProcess: (args: string[], cwd: string, environment?: NodeJS.ProcessEnv) => SpawnedProcess;
|
|
561
574
|
webExtension?: string;
|
|
562
575
|
projectTrusted: boolean;
|
|
563
576
|
limits: SubagentLimits;
|
|
564
577
|
timeoutMs?: number;
|
|
565
578
|
onChange: (result: SubagentTaskResult) => void;
|
|
566
579
|
onHandle?: (handle: SubagentProcessHandle) => void;
|
|
580
|
+
toolBudget?: ChildToolBudget;
|
|
567
581
|
}
|
|
568
582
|
|
|
569
583
|
function applyProcessResult(
|
|
@@ -582,6 +596,7 @@ function applyProcessResult(
|
|
|
582
596
|
target.output = source.output;
|
|
583
597
|
target.outputBytes = source.outputBytes;
|
|
584
598
|
target.outputTruncatedBytes = source.outputTruncatedBytes;
|
|
599
|
+
target.toolCallCount = source.toolCallCount;
|
|
585
600
|
target.usage = { ...source.usage, cost: { ...source.usage.cost } };
|
|
586
601
|
target.model = source.model ?? target.model;
|
|
587
602
|
target.terminationReason = source.terminationReason;
|
|
@@ -629,6 +644,7 @@ async function runTask(options: RunTaskOptions): Promise<SubagentTaskResult> {
|
|
|
629
644
|
"--no-session",
|
|
630
645
|
"--no-extensions",
|
|
631
646
|
"--extension", options.webExtension ?? SUBAGENT_WEB_EXTENSION,
|
|
647
|
+
"--extension", SUBAGENT_BUDGET_EXTENSION,
|
|
632
648
|
"--no-prompt-templates",
|
|
633
649
|
options.projectTrusted ? "--approve" : "--no-approve",
|
|
634
650
|
"--model", options.model.model,
|
|
@@ -652,6 +668,9 @@ async function runTask(options: RunTaskOptions): Promise<SubagentTaskResult> {
|
|
|
652
668
|
quotaUsd: limits.quotaUsd,
|
|
653
669
|
killGraceMs: limits.killGraceMs,
|
|
654
670
|
},
|
|
671
|
+
environment: options.toolBudget ? {
|
|
672
|
+
[CHILD_TOOL_BUDGET_ENV]: JSON.stringify(options.toolBudget),
|
|
673
|
+
} : undefined,
|
|
655
674
|
onUpdate: (next) => applyProcessResult(result, next, startedAt, options.onChange),
|
|
656
675
|
});
|
|
657
676
|
options.onHandle?.(handle);
|
|
@@ -861,6 +880,7 @@ function threadResult(thread: SubagentThread, source?: SubagentTaskResult): Suba
|
|
|
861
880
|
turns: thread.usage.turns,
|
|
862
881
|
},
|
|
863
882
|
output: thread.result ?? "",
|
|
883
|
+
toolCallCount: 0,
|
|
864
884
|
terminationReason: thread.stopReason,
|
|
865
885
|
};
|
|
866
886
|
}
|
|
@@ -1215,6 +1235,9 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1215
1235
|
const runtime: ActiveThreadRuntime = { controller, steering: [], restarting: false, traceCount: 0, startedAt: Date.now() };
|
|
1216
1236
|
activeRuntimes.set(threadId, runtime);
|
|
1217
1237
|
const agent = roles.get(input.agent)!;
|
|
1238
|
+
const baseToolBudget: ChildToolBudget | undefined = agent.access === "read"
|
|
1239
|
+
? { soft: limits.readToolBudgetSoft, hard: limits.readToolBudgetHard, block: [...READ_TOOLS] }
|
|
1240
|
+
: undefined;
|
|
1218
1241
|
const queuedSteering = initialThread.steering.map((entry) => entry.message);
|
|
1219
1242
|
let currentTask = queuedSteering.length ? buildSteeredTask(task, queuedSteering, undefined, limits.taskCharacters) : task;
|
|
1220
1243
|
const stopForBudget = (reason: string, message: string): void => {
|
|
@@ -1242,6 +1265,7 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1242
1265
|
const usedTraceBytes = aggregate?.traceBytes ?? 0;
|
|
1243
1266
|
const usedStderrBytes = aggregate?.stderrBytes ?? 0;
|
|
1244
1267
|
const usedOutputBytes = aggregate?.outputBytes ?? 0;
|
|
1268
|
+
const usedToolCalls = aggregate?.toolCallCount ?? 0;
|
|
1245
1269
|
const usedTokens = aggregate?.usage.totalTokens ?? 0;
|
|
1246
1270
|
const usedCost = aggregate?.usage.cost.total ?? 0;
|
|
1247
1271
|
if (remainingWallTimeMs <= 0) {
|
|
@@ -1260,6 +1284,10 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1260
1284
|
stopForBudget("output_limit", `Child thread emits more than ${limits.taskOutputBytes} output bytes`);
|
|
1261
1285
|
break;
|
|
1262
1286
|
}
|
|
1287
|
+
if (baseToolBudget && usedToolCalls >= baseToolBudget.hard) {
|
|
1288
|
+
stopForBudget("tool_call_limit", `Child thread exceeds ${baseToolBudget.hard} tool calls`);
|
|
1289
|
+
break;
|
|
1290
|
+
}
|
|
1263
1291
|
if (usedTokens >= limits.quotaTokens) {
|
|
1264
1292
|
stopForBudget("quota_tokens", `Child thread exceeds ${limits.quotaTokens} tokens`);
|
|
1265
1293
|
break;
|
|
@@ -1269,6 +1297,13 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1269
1297
|
break;
|
|
1270
1298
|
}
|
|
1271
1299
|
runtime.traceCount = 0;
|
|
1300
|
+
const toolBudget = baseToolBudget
|
|
1301
|
+
? {
|
|
1302
|
+
...baseToolBudget,
|
|
1303
|
+
soft: Math.max(1, (baseToolBudget.soft ?? baseToolBudget.hard) - usedToolCalls),
|
|
1304
|
+
hard: baseToolBudget.hard - usedToolCalls,
|
|
1305
|
+
}
|
|
1306
|
+
: undefined;
|
|
1272
1307
|
const next = await runTask({
|
|
1273
1308
|
cwd: ctx.cwd,
|
|
1274
1309
|
agent: roles.get(input.agent)!,
|
|
@@ -1290,6 +1325,7 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1290
1325
|
},
|
|
1291
1326
|
timeoutMs: remainingWallTimeMs,
|
|
1292
1327
|
onHandle: (handle) => { runtime.handle = handle; },
|
|
1328
|
+
toolBudget,
|
|
1293
1329
|
onChange: (changed) => {
|
|
1294
1330
|
results[index] = syncThread(threadId, changed, runtime);
|
|
1295
1331
|
emit();
|