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 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.1
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`, 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` before work and to use web research when external evidence is needed. 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, one million tokens, and $10 per child. Esc cancellation terminates active children and escalates after five seconds.
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.2`, publish after the validation checks pass:
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.2",
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
+ }
@@ -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: 1_000_000,
12
- quotaUsd: 10,
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 = (options.spawnProcess ?? defaultSpawnProcess)(args, options.cwd);
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: 1_000_000,
33
- quotaUsd: 10,
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, { encoding: "utf8", mode: 0o600 });
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();