apex-code 0.0.4 → 0.0.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.
Files changed (48) hide show
  1. package/CHANGELOG.md +14 -1
  2. package/dist/cli/agent-lifecycle.d.ts +24 -0
  3. package/dist/cli/agent-lifecycle.d.ts.map +1 -0
  4. package/dist/cli/agent-lifecycle.js +126 -0
  5. package/dist/cli/agent-lifecycle.js.map +1 -0
  6. package/dist/cli/args.d.ts +13 -0
  7. package/dist/cli/args.d.ts.map +1 -1
  8. package/dist/cli/args.js +80 -0
  9. package/dist/cli/args.js.map +1 -1
  10. package/dist/core/agent-session.d.ts +160 -0
  11. package/dist/core/agent-session.d.ts.map +1 -1
  12. package/dist/core/agent-session.js +265 -0
  13. package/dist/core/agent-session.js.map +1 -1
  14. package/dist/core/delegation/runtime.d.ts +682 -1
  15. package/dist/core/delegation/runtime.d.ts.map +1 -1
  16. package/dist/core/delegation/runtime.js +1339 -66
  17. package/dist/core/delegation/runtime.js.map +1 -1
  18. package/dist/core/sdk.d.ts +61 -3
  19. package/dist/core/sdk.d.ts.map +1 -1
  20. package/dist/core/sdk.js +316 -22
  21. package/dist/core/sdk.js.map +1 -1
  22. package/dist/core/tools/delegate.d.ts +8 -0
  23. package/dist/core/tools/delegate.d.ts.map +1 -1
  24. package/dist/core/tools/delegate.js +33 -3
  25. package/dist/core/tools/delegate.js.map +1 -1
  26. package/dist/core/workspace/git-observer.d.ts +16 -0
  27. package/dist/core/workspace/git-observer.d.ts.map +1 -1
  28. package/dist/core/workspace/git-observer.js +8 -1
  29. package/dist/core/workspace/git-observer.js.map +1 -1
  30. package/dist/core/workspace/git-worktree-owner.d.ts +89 -0
  31. package/dist/core/workspace/git-worktree-owner.d.ts.map +1 -0
  32. package/dist/core/workspace/git-worktree-owner.js +240 -0
  33. package/dist/core/workspace/git-worktree-owner.js.map +1 -0
  34. package/dist/main.d.ts.map +1 -1
  35. package/dist/main.js +20 -0
  36. package/dist/main.js.map +1 -1
  37. package/dist/modes/acp/server.d.ts +43 -1
  38. package/dist/modes/acp/server.d.ts.map +1 -1
  39. package/dist/modes/acp/server.js +78 -0
  40. package/dist/modes/acp/server.js.map +1 -1
  41. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  42. package/dist/modes/rpc/rpc-mode.js +38 -0
  43. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  44. package/dist/modes/rpc/rpc-types.d.ts +110 -0
  45. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  46. package/dist/modes/rpc/rpc-types.js.map +1 -1
  47. package/npm-shrinkwrap.json +5 -5
  48. package/package.json +2 -2
package/dist/core/sdk.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { join } from "node:path";
2
2
  import { clampThinkingLevel, streamSimple } from "@earendil-works/pi-ai/compat";
3
- import { Agent, setDefaultStreamFn } from "apex-code-agent-core";
3
+ import { Agent, createRunBudgetController, setDefaultStreamFn, } from "apex-code-agent-core";
4
4
  import { getAgentDir } from "../config.js";
5
5
  import { resolvePath } from "../utils/paths.js";
6
6
  import { AgentSession } from "./agent-session.js";
@@ -8,6 +8,7 @@ import { formatNoModelsAvailableMessage } from "./auth-guidance.js";
8
8
  import { AuthStorage } from "./auth-storage.js";
9
9
  import { DEFAULT_THINKING_LEVEL } from "./defaults.js";
10
10
  import { createAgentDefinitionResolver } from "./delegation/agents.js";
11
+ import { ChildRunRegistry, } from "./delegation/runtime.js";
11
12
  import { loadHookRuntime } from "./hooks/loader.js";
12
13
  import { createSessionMcpConnector } from "./mcp/connector.js";
13
14
  import { createMcpRuntime } from "./mcp/runtime.js";
@@ -24,6 +25,7 @@ import { SettingsManager } from "./settings-manager.js";
24
25
  import { time } from "./timings.js";
25
26
  import { createBackgroundShellRegistry } from "./tools/background-shell.js";
26
27
  import { createAllToolDefinitions, createBashTool, createCodingTools, createDelegateToolDefinition, createEditTool, createFindTool, createGrepTool, createLsTool, createPowerShellTool, createReadOnlyTools, createReadTool, createWriteTool, withFileMutationQueue, } from "./tools/index.js";
28
+ import { GitWorktreeWorkspaceOwner } from "./workspace/git-worktree-owner.js";
27
29
  // Preserve the pre-0.81 fallback for extensions that construct Agent instances
28
30
  // or invoke low-level agent loops without supplying streamFn. Agent core remains
29
31
  // provider-agnostic and does not import pi-ai/compat itself.
@@ -110,6 +112,43 @@ export async function createAgentSession(options = {}) {
110
112
  const settingsManager = options.settingsManager ?? SettingsManager.create(cwd, agentDir);
111
113
  const sessionManager = options.sessionManager ?? SessionManager.create(cwd, getDefaultSessionDir(cwd, agentDir));
112
114
  const backgroundShellRegistry = options.backgroundShellRegistry ?? createBackgroundShellRegistry();
115
+ // One resolved per-run budget policy: the caller's override when supplied,
116
+ // else the settings policy. Children built below inherit this resolved value
117
+ // so a child run carries the same per-run policy as its parent without
118
+ // re-reading settings (spec 2026-09-09, "Shared budgets"). Per-run only: it
119
+ // never creates a shared ceiling.
120
+ const runBudget = options.runBudget ?? settingsManager.getRunBudget();
121
+ // The root aggregate budget (spec 2026-09-09, "Shared budgets"): created
122
+ // ONLY when the caller explicitly configures `aggregateBudget` -- never
123
+ // derived from the per-run policy. One controller per root tree: the
124
+ // parent's own Agent consumes it (composed under its per-prompt controller),
125
+ // and buildChildSession forwards THE SAME instance to every child regardless
126
+ // of depth, so a grandchild's consumption lands on the root ledger. Its wall
127
+ // time starts here, at root session construction, and no descendant can
128
+ // reset it: descendants only consume.
129
+ const aggregateBudgetController = options.aggregateBudget
130
+ ? createRunBudgetController(options.aggregateBudget)
131
+ : undefined;
132
+ // The shared ceiling this session consumes and hands to its delegated
133
+ // children: the root aggregate when configured, else a caller-supplied
134
+ // controller (which is how a delegated child receives its root's aggregate).
135
+ // With neither there is NO shared controller at all: every session keeps
136
+ // local-only budgets under its inherited per-run policy.
137
+ const sharedBudgetController = aggregateBudgetController ?? options.sharedBudgetController;
138
+ // The tree-facing usage snapshot: the aggregate controller's counters cover
139
+ // every accepted provider request, tool call, and maintenance request of the
140
+ // parent's own runs and of every descendant, so one read reports the tree.
141
+ const aggregateBudgetUsage = sharedBudgetController
142
+ ? () => {
143
+ const usage = sharedBudgetController.usage();
144
+ return {
145
+ providerRequests: usage.providerRequests,
146
+ toolCalls: usage.toolCalls,
147
+ maintenanceRequests: usage.maintenanceRequests,
148
+ startedAtMs: usage.startedAt,
149
+ };
150
+ }
151
+ : undefined;
113
152
  if (!resourceLoader) {
114
153
  resourceLoader = new DefaultResourceLoader({ cwd, agentDir, settingsManager });
115
154
  await resourceLoader.reload();
@@ -235,10 +274,16 @@ export async function createAgentSession(options = {}) {
235
274
  thinkingLevel,
236
275
  tools: [],
237
276
  },
238
- // One normalized budget policy from settings; every execution mode (interactive,
239
- // print, JSON, RPC, ACP, SDK) runs through this session, so none counts on its
240
- // own (spec 2026-09-01-tool-reliability-and-execution-budgets.md).
241
- runBudget: settingsManager.getRunBudget(),
277
+ // The resolved budget policy from above (override or settings); every
278
+ // execution mode (interactive, print, JSON, RPC, ACP, SDK) runs through
279
+ // this session, so none counts on its own
280
+ // (spec 2026-09-01-tool-reliability-and-execution-budgets.md).
281
+ runBudget,
282
+ budgetScope: options.budgetScope ?? "prompt",
283
+ // The root aggregate when configured (the parent's own runs consume it,
284
+ // per-prompt local scope unchanged), else a caller-supplied shared
285
+ // controller, else nothing (spec 2026-09-09, "Shared budgets").
286
+ sharedBudgetController,
242
287
  convertToLlm: convertToLlmWithBlockImages,
243
288
  streamFn: async (model, context, options) => {
244
289
  const providerRetrySettings = settingsManager.getProviderRetrySettings();
@@ -313,11 +358,13 @@ export async function createAgentSession(options = {}) {
313
358
  // must read the *live* session (active tools can change after construction),
314
359
  // not a snapshot frozen at this point.
315
360
  const parentSessionRef = {};
361
+ const childRunRegistry = options.childRunRegistry ?? new ChildRunRegistry();
316
362
  const customTools = options.customTools ? [...options.customTools] : [];
317
363
  // A configured permission gate supplies the authority a child must derive, so
318
364
  // discovery is enabled by default in real sessions. Callers that need fixture
319
365
  // definitions may still provide an explicit resolver.
320
366
  const delegation = options.delegation ?? (options.permissionGate ? {} : undefined);
367
+ let delegationRuntime;
321
368
  if (delegation) {
322
369
  const parentPermissionGate = options.permissionGate;
323
370
  const resolveAgent = delegation.resolveAgent ??
@@ -326,8 +373,17 @@ export async function createAgentSession(options = {}) {
326
373
  agentDir,
327
374
  isProjectTrusted: () => settingsManager.isProjectTrusted(),
328
375
  });
329
- const delegationRuntime = {
376
+ let worktreeOwner;
377
+ // One read of the delegation bound for this runtime; the policy snapshot
378
+ // below reports the same value it enforces instead of re-reading settings.
379
+ const delegationMaxDepth = settingsManager.getDelegationMaxDepth();
380
+ delegationRuntime = {
381
+ childRunRegistry,
330
382
  resolveAgent,
383
+ // Concurrency cap (spec 2026-09-09, "Shared budgets"): the registry's
384
+ // admission path reads it from the runtime options. Undefined is
385
+ // unlimited.
386
+ maxConcurrentChildren: options.maxConcurrentChildren,
331
387
  getParentCapabilities: () => {
332
388
  const capabilities = new Set();
333
389
  for (const name of parentSessionRef.current?.getActiveToolNames() ?? []) {
@@ -356,9 +412,23 @@ export async function createAgentSession(options = {}) {
356
412
  // the (possibly delegated) session this createAgentSession call is
357
413
  // building, via the same sessionManager this closure already captures.
358
414
  getDelegationDepth: () => sessionManager.getDelegationDepth(),
359
- maxDelegationDepth: settingsManager.getDelegationMaxDepth(),
415
+ maxDelegationDepth: delegationMaxDepth,
360
416
  getParentSessionDir: () => sessionManager.getSessionDir(),
361
- buildChildSession: async ({ definition, toolNames, depth, sessionId, artifactDir }) => {
417
+ // Child-run records carry the parent session id so durable records and
418
+ // protocol payloads tie each child to this session's own identity.
419
+ getParentSessionId: () => sessionManager.getSessionId(),
420
+ // Worktree isolation (spec 2026-09-09, "Parallel work and ownership"):
421
+ // the production workspace owner, constructed on the first worktree-
422
+ // isolated delegation only. Shared-read delegations never read this
423
+ // property (the runtime consults the owner only for isolation
424
+ // "worktree"), so they never construct an owner and never invoke git.
425
+ get workspaceOwner() {
426
+ if (worktreeOwner === undefined) {
427
+ worktreeOwner = new GitWorktreeWorkspaceOwner(cwd);
428
+ }
429
+ return worktreeOwner;
430
+ },
431
+ buildChildSession: async ({ definition, toolNames, capabilities, depth, sessionId, artifactDir, workspace, reattachSessionPath, timeoutMs, }) => {
362
432
  if (!parentPermissionGate) {
363
433
  throw new Error("delegate requires a permission gate configured on the parent session (ADR 0008).");
364
434
  }
@@ -367,23 +437,83 @@ export async function createAgentSession(options = {}) {
367
437
  if (!childModel) {
368
438
  throw new Error(`Cannot delegate to "${definition.name}": no model is available for the child session.`);
369
439
  }
370
- const childSessionManager = artifactDir
371
- ? SessionManager.create(cwd, artifactDir, {
372
- id: sessionId,
373
- parentSession: sessionManager.getSessionId(),
374
- delegationDepth: depth,
375
- })
376
- : SessionManager.inMemory(cwd, {
377
- parentSession: sessionManager.getSessionId(),
378
- delegationDepth: depth,
379
- });
440
+ // The derived policy snapshot (spec 2026-09-09, "Derive, do not
441
+ // reconstruct"): the exact values this construction used -- the
442
+ // ceiling-checked tool allowlist, the admitted capability set from
443
+ // the runtime's single admission projection (never a second
444
+ // classification, ADR 0010), this session's sandbox contract, the
445
+ // delegation bound it enforces, the child's model, and the budget
446
+ // wiring every child is built with (session-scoped own controller;
447
+ // aggregate only when a shared ceiling was configured at the root).
448
+ // It rides on the returned handle so the registry persists it on the
449
+ // child-run record, and `sandboxEnforced` reports whether the
450
+ // OS-containment supervisor marker check passed for this parent
451
+ // session (always true under the "required" contract, which refuses
452
+ // construction without it).
453
+ const policy = {
454
+ tools: [...toolNames],
455
+ capabilities: [...capabilities].sort(),
456
+ sandbox: sandboxContract,
457
+ maxDelegationDepth: delegationMaxDepth,
458
+ model: childModel.id,
459
+ budgetScope: "session",
460
+ aggregateBudget: sharedBudgetController !== undefined,
461
+ };
462
+ const sandboxEnforced = markerPresent;
463
+ // An isolated child runs inside the workspace owner's prepared root
464
+ // (its linked worktree); a shared child keeps this session's cwd.
465
+ const childCwd = workspace?.root ?? cwd;
466
+ // The one reattachment seam (restart reconstruction): when the request
467
+ // carries the existing child session path, that transcript is OPENED
468
+ // rather than created; identity (header id, parentSession, depth) comes
469
+ // from the file's own header. Every other input below is derived
470
+ // exactly as for a fresh delegation -- permission store, model, tools,
471
+ // budget -- so there is no second construction path.
472
+ const childSessionManager = reattachSessionPath
473
+ ? SessionManager.open(reattachSessionPath, artifactDir)
474
+ : artifactDir
475
+ ? SessionManager.create(childCwd, artifactDir, {
476
+ id: sessionId,
477
+ parentSession: sessionManager.getSessionId(),
478
+ delegationDepth: depth,
479
+ })
480
+ : SessionManager.inMemory(childCwd, {
481
+ parentSession: sessionManager.getSessionId(),
482
+ delegationDepth: depth,
483
+ });
380
484
  const derivedStore = new DerivedPermissionRuleStore({ parent: parentPermissionGate.store });
381
485
  const { session: childSession } = await createAgentSession({
382
- cwd,
486
+ cwd: childCwd,
383
487
  agentDir,
384
488
  model: childModel,
385
489
  modelRuntime,
386
490
  settingsManager,
491
+ // The parent's already-resolved per-run budget (override or settings),
492
+ // not a fresh settings read: every child run carries the same per-run
493
+ // policy as its parent (spec 2026-09-09, "Shared budgets").
494
+ //
495
+ // Session scope + the root aggregate: a resumed or follow-up child
496
+ // turn continues the same child budget (one controller per child
497
+ // Agent, created at its first turn), and every gate also answers
498
+ // to the tree's shared ceiling -- accepted consumption records to
499
+ // both the child's own controller and the aggregate. THE SAME
500
+ // aggregate instance reaches every descendant regardless of
501
+ // depth (a grandchild's parent forwards it onward), so there is
502
+ // exactly ONE aggregate per root tree and no child can reset it
503
+ // by resuming or spawning children (spec 2026-09-09, "Shared
504
+ // budgets"). Without an aggregate there is no shared controller:
505
+ // the child keeps local-only budgets under the inherited policy.
506
+ //
507
+ // A launch timeout (spec 2026-09-09, timeouts) overrides the
508
+ // child's OWN wall-time limit with the requested timeoutMs, so the
509
+ // existing AgentRunBudget wall-time gate enforces it mid-run; the
510
+ // parent policy's other limits still apply unchanged.
511
+ runBudget: timeoutMs === undefined ? runBudget : { ...runBudget, maxWallTimeMs: timeoutMs },
512
+ budgetScope: "session",
513
+ sharedBudgetController,
514
+ // The same per-parent admission cap applies to a child's own
515
+ // delegations (each runtime counts its direct children).
516
+ maxConcurrentChildren: options.maxConcurrentChildren,
387
517
  sessionManager: childSessionManager,
388
518
  resourceLoader,
389
519
  tools: toolNames,
@@ -412,24 +542,187 @@ export async function createAgentSession(options = {}) {
412
542
  sandbox: sandboxContract,
413
543
  });
414
544
  await childSession.bindExtensions({});
545
+ let status = "idle";
546
+ let runPromise;
547
+ let lastTurnResult;
548
+ // The captured structured terminal outcome of the run in flight
549
+ // (`agent_end`'s stopReason). Budget refusals emit agent_end without
550
+ // an assistant message, so this is the only way the settlement below
551
+ // can see and name them.
552
+ let runStopReason;
553
+ const ensureOpen = () => {
554
+ if (status === "closed")
555
+ throw new Error("Child session is closed.");
556
+ };
557
+ const start = (input) => {
558
+ ensureOpen();
559
+ if (runPromise)
560
+ throw new Error("Child session is already running.");
561
+ status = "running";
562
+ runStopReason = undefined;
563
+ // Capture the run's structured terminal outcome: a budget refusal
564
+ // emits agent_end without producing an assistant message, so the
565
+ // message-based checks below cannot see it.
566
+ const unsubscribeStop = childSession.subscribe((event) => {
567
+ if (event.type === "agent_end")
568
+ runStopReason = event.stopReason;
569
+ });
570
+ const pending = childSession
571
+ .prompt(input)
572
+ .then(() => {
573
+ unsubscribeStop();
574
+ if (status === "closed") {
575
+ lastTurnResult = { outcome: "failed", output: "" };
576
+ throw new Error("Child session is closed.");
577
+ }
578
+ if (status === "interrupted") {
579
+ lastTurnResult = { outcome: "interrupted", output: extractFinalAssistantText(childSession) };
580
+ throw new Error("Child session is interrupted.");
581
+ }
582
+ const lastAssistant = [...childSession.agent.state.messages]
583
+ .reverse()
584
+ .find((message) => message.role === "assistant");
585
+ if (lastAssistant?.role === "assistant" &&
586
+ (lastAssistant.stopReason === "aborted" || lastAssistant.stopReason === "error")) {
587
+ status = "interrupted";
588
+ lastTurnResult = {
589
+ outcome: lastAssistant.stopReason === "aborted" ? "interrupted" : "failed",
590
+ output: lastAssistant.errorMessage || extractFinalAssistantText(childSession),
591
+ };
592
+ throw new Error(lastAssistant.errorMessage || "Child session was interrupted.");
593
+ }
594
+ status = "idle";
595
+ lastTurnResult = { outcome: "completed", output: extractFinalAssistantText(childSession) };
596
+ }, (error) => {
597
+ unsubscribeStop();
598
+ if (status !== "closed")
599
+ status = "interrupted";
600
+ lastTurnResult =
601
+ status === "closed"
602
+ ? { outcome: "failed", output: "" }
603
+ : { outcome: "interrupted", output: extractFinalAssistantText(childSession) };
604
+ throw error;
605
+ })
606
+ .finally(() => {
607
+ runPromise = undefined;
608
+ });
609
+ runPromise = pending;
610
+ return pending;
611
+ };
612
+ const sendInput = (input) => {
613
+ ensureOpen();
614
+ if (runPromise) {
615
+ if (status !== "running")
616
+ throw new Error("Child session is still stopping.");
617
+ // A follow-up queued onto a live run settles through that run's
618
+ // own loop; only turns that settle here pass through the gate.
619
+ return childSession.followUp(input);
620
+ }
621
+ return start(input).then(() => requireVerifiedTurn());
622
+ };
623
+ const close = () => {
624
+ if (status === "closed")
625
+ return;
626
+ status = "closed";
627
+ childSession.dispose();
628
+ };
629
+ // Verification gate (spec 2026-09-09, "Verification at completion"):
630
+ // a child with configured verification policies may not settle a turn
631
+ // as successful unless the session's canonical tracker reports
632
+ // "verified". The tracker is the only verifier; no child-local one is
633
+ // introduced. With the "explicit" boundary the gate runs
634
+ // requestVerification once here; with the "post-turn" boundary the
635
+ // session already ran verification at its own turn boundary, so the
636
+ // gate only reads the status. Any other completion status --
637
+ // failed/unavailable/interrupted/continued-unverified -- fails the
638
+ // turn with the status named in the error.
639
+ const requireVerifiedTurn = async () => {
640
+ const policyIds = childSession.verificationPolicyIds();
641
+ if (policyIds.length === 0)
642
+ return;
643
+ const verificationStatus = childSession.verificationBoundary() === "explicit"
644
+ ? await childSession.requestVerification()
645
+ : childSession.verificationStatus();
646
+ if (verificationStatus === "verified")
647
+ return;
648
+ const message = `Child verification failed: ${verificationStatus} (policies: ${policyIds.join(", ")}).`;
649
+ lastTurnResult = { outcome: "failed", output: message };
650
+ throw new Error(message);
651
+ };
415
652
  return {
653
+ get status() {
654
+ return status;
655
+ },
656
+ latestResult: () => lastTurnResult,
657
+ usage: () => childSession.agent.budgetUsage(),
658
+ // The usage-rollup seam (spec 2026-09-09): the registry reads the
659
+ // child's own transcript file when one exists, and falls back to
660
+ // these in-memory entries only for a child that never persisted a
661
+ // transcript. Read-only; the child's SessionManager stays the owner.
662
+ sessionEntries: () => childSessionManager.getEntries(),
416
663
  async run(task) {
417
664
  const prompt = definition.systemPrompt
418
665
  ? `${definition.systemPrompt}\n\nTask: ${task}`
419
666
  : `Task: ${task}`;
420
- await childSession.prompt(prompt);
667
+ await start(prompt);
668
+ if (status === "closed" || status === "interrupted")
669
+ throw new Error(`Child session is ${status}.`);
670
+ // The launch timeout's settlement error path (spec 2026-09-09,
671
+ // timeouts): a run refused at the child's own wall-time gate never
672
+ // started meaningful work, so the delegation settles failed with
673
+ // the exhausted limit named, reusing the budget's existing
674
+ // exhaustedLimit vocabulary. Other budget refusals keep their
675
+ // existing semantics (the refusal is named in the child transcript
676
+ // and the prior result stands).
677
+ if (runStopReason?.kind === "budget-exhausted" && runStopReason.limit === "wall-time") {
678
+ const message = `Child run stopped before completing: the run's ${runStopReason.limit} budget was exhausted.`;
679
+ lastTurnResult = { outcome: "failed", output: message };
680
+ throw new Error(message);
681
+ }
682
+ // Delegated work cannot claim success when the child has a
683
+ // required verification boundary and verification did not pass.
684
+ // Use the session's canonical tracker rather than introducing
685
+ // a second child-only verifier.
686
+ await requireVerifiedTurn();
421
687
  return { output: extractFinalAssistantText(childSession) };
422
688
  },
423
- dispose() {
424
- childSession.dispose();
689
+ wait: async () => {
690
+ while (runPromise)
691
+ await runPromise;
692
+ },
693
+ interrupt: () => {
694
+ ensureOpen();
695
+ if (status === "running") {
696
+ status = "interrupted";
697
+ void childSession.abort().catch(() => undefined);
698
+ }
425
699
  },
700
+ close,
701
+ sendInput,
702
+ followUp: sendInput,
703
+ dispose: close,
704
+ // Construction linkage (spec 2026-09-09, policy/sandbox/artifact
705
+ // linkage): the registry relays these onto the persisted child_run
706
+ // record and every status/wait payload derived from it.
707
+ policy,
708
+ sandboxEnforced,
426
709
  };
427
710
  },
428
711
  };
429
712
  customTools.push(createDelegateToolDefinition(delegationRuntime));
713
+ // Historical resume (restart reconstruction) reattaches a persisted child
714
+ // through THIS runtime's buildChildSession seam. Wiring it here -- not only
715
+ // at the first delegation -- is what makes resume work on a freshly
716
+ // reopened session before any new delegation has run.
717
+ childRunRegistry.setRuntimeOptions(delegationRuntime);
430
718
  }
431
719
  const session = new AgentSession({
720
+ childRunRegistry,
721
+ delegationRuntime,
432
722
  agent,
723
+ // Root aggregate usage (spec 2026-09-09, "Shared budgets"): undefined
724
+ // when the tree has no aggregate budget.
725
+ aggregateBudgetUsage,
433
726
  sessionManager,
434
727
  settingsManager,
435
728
  cwd,
@@ -466,6 +759,7 @@ export async function createAgentSession(options = {}) {
466
759
  return {
467
760
  session,
468
761
  extensionsResult,
762
+ delegationRuntime,
469
763
  modelFallbackMessage,
470
764
  sandboxContract,
471
765
  sandboxDiagnostic,