apex-code 0.0.5 → 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.
- package/CHANGELOG.md +14 -1
- package/dist/cli/agent-lifecycle.d.ts +24 -0
- package/dist/cli/agent-lifecycle.d.ts.map +1 -0
- package/dist/cli/agent-lifecycle.js +126 -0
- package/dist/cli/agent-lifecycle.js.map +1 -0
- package/dist/cli/args.d.ts +13 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +80 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/core/agent-session.d.ts +160 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +265 -0
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/delegation/runtime.d.ts +682 -1
- package/dist/core/delegation/runtime.d.ts.map +1 -1
- package/dist/core/delegation/runtime.js +1339 -66
- package/dist/core/delegation/runtime.js.map +1 -1
- package/dist/core/sdk.d.ts +61 -3
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +316 -22
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/tools/delegate.d.ts +8 -0
- package/dist/core/tools/delegate.d.ts.map +1 -1
- package/dist/core/tools/delegate.js +33 -3
- package/dist/core/tools/delegate.js.map +1 -1
- package/dist/core/workspace/git-observer.d.ts +16 -0
- package/dist/core/workspace/git-observer.d.ts.map +1 -1
- package/dist/core/workspace/git-observer.js +8 -1
- package/dist/core/workspace/git-observer.js.map +1 -1
- package/dist/core/workspace/git-worktree-owner.d.ts +89 -0
- package/dist/core/workspace/git-worktree-owner.d.ts.map +1 -0
- package/dist/core/workspace/git-worktree-owner.js +240 -0
- package/dist/core/workspace/git-worktree-owner.js.map +1 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +20 -0
- package/dist/main.js.map +1 -1
- package/dist/modes/acp/server.d.ts +43 -1
- package/dist/modes/acp/server.d.ts.map +1 -1
- package/dist/modes/acp/server.js +78 -0
- package/dist/modes/acp/server.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +38 -0
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +110 -0
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/npm-shrinkwrap.json +5 -5
- 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
|
-
//
|
|
239
|
-
// print, JSON, RPC, ACP, SDK) runs through
|
|
240
|
-
//
|
|
241
|
-
|
|
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
|
-
|
|
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:
|
|
415
|
+
maxDelegationDepth: delegationMaxDepth,
|
|
360
416
|
getParentSessionDir: () => sessionManager.getSessionDir(),
|
|
361
|
-
|
|
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
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
|
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
|
-
|
|
424
|
-
|
|
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,
|