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
@@ -26,6 +26,7 @@ import { createSessionCheckpoints } from "./checkpoints/session-checkpoints.js";
26
26
  import { calculateContextTokens, collectEntriesForBranchSummary, compact, estimateContextTokens, estimateTokens, generateBranchSummary, prepareCompaction, shouldCompact, } from "./compaction/index.js";
27
27
  import { evictionBudget, installContextPipeline, isDefaultStreamFunction } from "./context/pipeline.js";
28
28
  import { DEFAULT_THINKING_LEVEL, THINKING_LEVEL_OPTIONS } from "./defaults.js";
29
+ import { RESUME_CHILD_PROMPT, runDelegation } from "./delegation/runtime.js";
29
30
  import { SessionEvidenceSink } from "./evidence.js";
30
31
  import { exportSessionToHtml } from "./export-html/index.js";
31
32
  import { createToolHtmlRenderer } from "./export-html/tool-renderer.js";
@@ -88,6 +89,12 @@ function estimateMessagesTokens(messages) {
88
89
  return tokens;
89
90
  }
90
91
  export class AgentSession {
92
+ _lifecycle = "created";
93
+ _activePrompt = false;
94
+ /** Lifecycle is deliberately retained after completion/close for protocol callers. */
95
+ get lifecycle() {
96
+ return this._lifecycle;
97
+ }
91
98
  agent;
92
99
  sessionManager;
93
100
  settingsManager;
@@ -159,6 +166,9 @@ export class AgentSession {
159
166
  _checkpoints;
160
167
  _hookRuntime;
161
168
  _backgroundShellRegistry;
169
+ _childRunRegistry;
170
+ _delegationRuntime;
171
+ _aggregateBudgetUsage;
162
172
  _permissionResponderFactory;
163
173
  /** Worktree checkpoints for this session. Inert unless `checkpoints.enabled` is set. */
164
174
  get checkpoints() {
@@ -211,6 +221,21 @@ export class AgentSession {
211
221
  });
212
222
  this._hookRuntime = config.hookRuntime;
213
223
  this._backgroundShellRegistry = config.backgroundShellRegistry;
224
+ this._childRunRegistry = config.childRunRegistry;
225
+ this._delegationRuntime = config.delegationRuntime;
226
+ this._aggregateBudgetUsage = config.aggregateBudgetUsage;
227
+ if (this._childRunRegistry) {
228
+ // Install persistence before restoring history. Restoration is normally
229
+ // passive, but registries may reconcile lifecycle state while loading;
230
+ // no child transition should be able to observe an unwired registry.
231
+ this._childRunRegistry.setPersistence((record) => this.sessionManager.appendCustomEntry("child_run", record));
232
+ const records = this.sessionManager
233
+ .getEntries()
234
+ .filter((entry) => entry.type === "custom" && entry.customType === "child_run")
235
+ .map((entry) => entry.data)
236
+ .filter((value) => !!value && typeof value === "object");
237
+ this._childRunRegistry.restore(records);
238
+ }
214
239
  this._permissionResponderFactory = config.permissionResponderFactory;
215
240
  this._workspaceComparePending = hasWorkspaceObservationAwaitingComparison(this.sessionManager);
216
241
  // Always subscribe to agent events for internal handling
@@ -791,6 +816,196 @@ export class AgentSession {
791
816
  }
792
817
  };
793
818
  }
819
+ /** Child handles remain available until explicitly closed or this session is disposed. */
820
+ listChildRuns() {
821
+ return this._childRunRegistry?.list() ?? [];
822
+ }
823
+ /**
824
+ * A point-in-time snapshot of the root aggregate budget's counters for this
825
+ * session's whole delegation tree -- {providerRequests, toolCalls,
826
+ * maintenanceRequests, startedAtMs} (spec 2026-09-09, "Shared budgets").
827
+ * `undefined` when no aggregate budget is configured for the tree.
828
+ */
829
+ aggregateBudgetUsage() {
830
+ return this._aggregateBudgetUsage?.();
831
+ }
832
+ _requireChildRuns() {
833
+ if (!this._childRunRegistry)
834
+ throw new Error("Child run registry is unavailable.");
835
+ return this._childRunRegistry;
836
+ }
837
+ waitChildRun(id) {
838
+ return this._requireChildRuns().wait(id);
839
+ }
840
+ retrieveChildRun(id) {
841
+ return this._requireChildRuns().retrieve(id);
842
+ }
843
+ sendChildInput(id, input) {
844
+ return this._requireChildRuns().sendInput(id, input);
845
+ }
846
+ interruptChildRun(id, reason) {
847
+ this._requireChildRuns().interrupt(id, reason);
848
+ }
849
+ closeChildRun(id) {
850
+ this._requireChildRuns().close(id);
851
+ }
852
+ /**
853
+ * Non-blocking status snapshot for one child run (spec 2026-09-09, pollable
854
+ * status): `{handleId, agentType, task, status, attempt, attempts, ...}` built
855
+ * from the record/entry alone -- this never awaits a turn, so a caller can
856
+ * poll a running child without blocking (`waitChildRun` stays the blocking
857
+ * form). Unknown ids still error; historical records are served from the
858
+ * persisted record. Observing the status lazily interrupts a running child
859
+ * whose wall-clock deadline has passed.
860
+ */
861
+ childRunStatus(id) {
862
+ return this._requireChildRuns().status(id);
863
+ }
864
+ /**
865
+ * The child run's token/cost totals, rolled up from the child's OWN session
866
+ * transcript (`ChildRunRegistry.usageTotals`). `undefined` -- never a throw,
867
+ * never a zero-fill -- when nothing is reachable (an in-memory child without
868
+ * a reachable transcript, or a legacy record without any transcript).
869
+ */
870
+ childRunUsageTotals(id) {
871
+ return this._requireChildRuns().usageTotals(id);
872
+ }
873
+ /**
874
+ * Explicitly verify and reactivate a retained child worktree (spec
875
+ * 2026-09-09, "Workspace states and explicit recovery"): the thinnest
876
+ * pass-through over `ChildRunRegistry.recoverWorkspace`. Read-only git
877
+ * inspection through the workspace owner -- the admin entry, the checked-out
878
+ * branch, and the owner's layout -- never a recreate, checkout, reset, or
879
+ * force. On success the workspace state becomes "active" (persisted) and the
880
+ * child can be resumed in the SAME worktree; `{dirty}` reports preserved
881
+ * uncommitted work. Every failed check throws an actionable error naming the
882
+ * failed check and leaving the state unverified.
883
+ */
884
+ recoverChildWorkspace(id) {
885
+ return this._requireChildRuns().recoverWorkspace(id);
886
+ }
887
+ /**
888
+ * Start a background child run through the session's own delegation runtime.
889
+ * A thin pass-through over `runDelegation` (registry-backed, always
890
+ * background): admission -- agent resolution, depth bound, capability
891
+ * ceiling -- is the same projection the `delegate` tool uses, so a spawned
892
+ * child can never hold authority the parent cannot cover. Foreground
893
+ * spawning is not offered; protocol clients wait on the returned handle.
894
+ *
895
+ * Idempotent spawn: a supplied `idempotencyKey` that already maps to a
896
+ * handle returns the EXISTING handle with `created: false` and never builds
897
+ * a second child (no budget slot consumed twice); the key persists on the
898
+ * record, so a restarted parent dedupes too. `timeoutMs` caps the child's
899
+ * own run budget at the wall-time gate and records the deadline for lazy
900
+ * status observation.
901
+ */
902
+ async startChildRun(agentType, task, request) {
903
+ const runtime = this._delegationRuntime;
904
+ if (!runtime) {
905
+ throw new Error("No delegation runtime is configured; create the session with a permission gate or options.delegation to spawn agents.");
906
+ }
907
+ const existing = request?.idempotencyKey !== undefined
908
+ ? this._childRunRegistry?.handleForIdempotencyKey(request.idempotencyKey)
909
+ : undefined;
910
+ if (existing !== undefined)
911
+ return { handleId: existing, created: false };
912
+ const result = await runDelegation(runtime, agentType, task, {
913
+ background: true,
914
+ workspace: request?.workspace,
915
+ idempotencyKey: request?.idempotencyKey,
916
+ timeoutMs: request?.timeoutMs,
917
+ });
918
+ if (!result.handleId) {
919
+ throw new Error(`Delegation to agent "${agentType}" did not return a background handle.`);
920
+ }
921
+ return { handleId: result.handleId, created: true };
922
+ }
923
+ /**
924
+ * Wait for a child run's latest settled turn and return its payload with the
925
+ * registry status after settlement: `{ status, output, outcome }` plus the
926
+ * run's additive linkage fields (`artifactDir`, `sessionFile`,
927
+ * `parentSessionId`, `policy`, `sandboxEnforced`) when the registry snapshot
928
+ * knows them. A failed settlement surfaces as `outcome: "failed"` with the
929
+ * error message as the output rather than a rejection, so protocol waiters
930
+ * get the same payload shape on every settled turn; unknown handles still
931
+ * throw the registry's actionable error.
932
+ */
933
+ async waitChildRunResult(id) {
934
+ // Linkage comes from the same registry snapshot every status payload is
935
+ // built from -- never recomputed here. Unknown handles throw through the
936
+ // status read below only after wait itself has rethrown, so the error
937
+ // contract is unchanged.
938
+ const linkageOf = () => {
939
+ try {
940
+ const snapshot = this.childRunStatus(id);
941
+ return {
942
+ ...(snapshot.artifactDir !== undefined ? { artifactDir: snapshot.artifactDir } : {}),
943
+ ...(snapshot.sessionFile !== undefined ? { sessionFile: snapshot.sessionFile } : {}),
944
+ ...(snapshot.parentSessionId !== undefined ? { parentSessionId: snapshot.parentSessionId } : {}),
945
+ ...(snapshot.policy ? { policy: snapshot.policy } : {}),
946
+ ...(snapshot.sandboxEnforced !== undefined ? { sandboxEnforced: snapshot.sandboxEnforced } : {}),
947
+ };
948
+ }
949
+ catch {
950
+ return {};
951
+ }
952
+ };
953
+ let output;
954
+ let outcome;
955
+ try {
956
+ const result = await this.waitChildRun(id);
957
+ output = result.output;
958
+ outcome = result.outcome ?? "completed";
959
+ }
960
+ catch (error) {
961
+ const status = this.listChildRuns().find((run) => run.handleId === id)?.status;
962
+ if (status === undefined)
963
+ throw error;
964
+ return {
965
+ status,
966
+ output: error instanceof Error ? error.message : String(error),
967
+ outcome: "failed",
968
+ ...linkageOf(),
969
+ };
970
+ }
971
+ const status = this.listChildRuns().find((run) => run.handleId === id)?.status;
972
+ return { status: status ?? "closed", output, outcome, ...linkageOf() };
973
+ }
974
+ /**
975
+ * Resume an interrupted child run in its existing child session.
976
+ *
977
+ * The thinnest pass-through over the session-owned registry: the child keeps its
978
+ * identity, policy ceiling, and session linkage, and no second service or registry
979
+ * is involved. Returns the observed status after the resumed turn settles. The
980
+ * resumed turn's output becomes the run's stored latest result, so a later
981
+ * `waitChildRun` retrieves it; this call itself returns only the observed status.
982
+ *
983
+ * A handle known only as a persisted record (restored from this session's
984
+ * `child_run` log, no live entry in this process) is historically reattached
985
+ * instead: its recorded child session file is reopened through the same
986
+ * construction seam a live delegation uses, then resumed. Live handles keep
987
+ * the interrupted-only contract below.
988
+ */
989
+ async resumeChildRun(id, input) {
990
+ const registry = this._requireChildRuns();
991
+ const current = registry.list().find((run) => run.handleId === id)?.status;
992
+ if (current === undefined)
993
+ throw new Error(`Unknown delegation handle "${id}".`);
994
+ if (registry.isHistorical(id))
995
+ return registry.resumeHistorical(id, input);
996
+ if (current !== "interrupted")
997
+ throw new Error(`Child run "${id}" is not interrupted (status: ${current}).`);
998
+ // A resume is a new attempt epoch (spec 2026-09-09, "Child lifecycle"):
999
+ // the interrupted attempt closes with its settled outcome and the resuming
1000
+ // turn opens a new one. Historical resume does the same inside
1001
+ // `resumeHistorical`.
1002
+ registry.beginResumeAttempt(id);
1003
+ await registry.sendInput(id, input ?? RESUME_CHILD_PROMPT);
1004
+ const status = registry.list().find((run) => run.handleId === id)?.status;
1005
+ if (status === undefined)
1006
+ throw new Error(`Child run "${id}" disappeared while resuming.`);
1007
+ return status;
1008
+ }
794
1009
  /** Disconnect from agent events during disposal. */
795
1010
  _disconnectFromAgent() {
796
1011
  if (this._unsubscribeAgent) {
@@ -803,6 +1018,9 @@ export class AgentSession {
803
1018
  * Call this when completely done with the session.
804
1019
  */
805
1020
  dispose() {
1021
+ if (this._lifecycle === "closed")
1022
+ return;
1023
+ this._lifecycle = "closed";
806
1024
  try {
807
1025
  this.abortRetry();
808
1026
  this.abortCompaction();
@@ -812,6 +1030,7 @@ export class AgentSession {
812
1030
  // Background shell children (spec 2026-08-31-background-shell.md) must
813
1031
  // never outlive the session that launched them.
814
1032
  this._backgroundShellRegistry?.dispose();
1033
+ this._childRunRegistry?.dispose();
815
1034
  }
816
1035
  catch {
817
1036
  // Dispose must succeed even if an abort hook throws.
@@ -1061,6 +1280,33 @@ export class AgentSession {
1061
1280
  * @throws Error if no model selected or no API key available (when not streaming)
1062
1281
  */
1063
1282
  async prompt(text, options) {
1283
+ if (this._lifecycle === "closed")
1284
+ throw new Error("Session is closed.");
1285
+ if (this._activePrompt) {
1286
+ if (!this.isStreaming)
1287
+ throw new Error("Session is already running.");
1288
+ return this._prompt(text, options);
1289
+ }
1290
+ this._activePrompt = true;
1291
+ this._lifecycle = "running";
1292
+ try {
1293
+ await this._prompt(text, options);
1294
+ if (this.lifecycle === "running") {
1295
+ const last = [...this.agent.state.messages].reverse().find((message) => message.role === "assistant");
1296
+ this._lifecycle =
1297
+ last?.stopReason === "error" ? "failed" : last?.stopReason === "aborted" ? "interrupted" : "completed";
1298
+ }
1299
+ }
1300
+ catch (error) {
1301
+ if (this.lifecycle === "running")
1302
+ this._lifecycle = "failed";
1303
+ throw error;
1304
+ }
1305
+ finally {
1306
+ this._activePrompt = false;
1307
+ }
1308
+ }
1309
+ async _prompt(text, options) {
1064
1310
  const expandPromptTemplates = options?.expandPromptTemplates ?? true;
1065
1311
  const preflightResult = options?.preflightResult;
1066
1312
  let messages;
@@ -1190,6 +1436,8 @@ export class AgentSession {
1190
1436
  preflightResult?.(true);
1191
1437
  await this._runWorkspaceComparisonBoundary();
1192
1438
  await this._runAgentPrompt(messages);
1439
+ if (this.lifecycle === "closed" || this.lifecycle === "interrupted")
1440
+ return;
1193
1441
  await this._runVerificationTurnBoundary();
1194
1442
  }
1195
1443
  /**
@@ -1471,6 +1719,8 @@ export class AgentSession {
1471
1719
  * Abort current operation and wait for agent to become idle.
1472
1720
  */
1473
1721
  async abort() {
1722
+ if (this._activePrompt && this._lifecycle !== "closed")
1723
+ this._lifecycle = "interrupted";
1474
1724
  this.abortRetry();
1475
1725
  this.agent.abort();
1476
1726
  await this.waitForIdle();
@@ -1859,6 +2109,21 @@ export class AgentSession {
1859
2109
  verificationStatus() {
1860
2110
  return this._verificationTracker?.completionStatus() ?? "unavailable";
1861
2111
  }
2112
+ /** The configured verification boundary, for execution owners enforcing it. */
2113
+ verificationBoundary() {
2114
+ return this._verificationTracker?.boundary ?? "explicit";
2115
+ }
2116
+ /**
2117
+ * The configured verification policy ids, read through the canonical
2118
+ * tracker (refreshed from settings on each call, like requestVerification).
2119
+ * Execution owners such as the delegation child gate use the count to tell
2120
+ * "no policies configured" (turn proceeds unchanged) from "configured and
2121
+ * not verified" (turn fails) -- the completion status alone cannot express
2122
+ * that difference, because it reports "unavailable" for both.
2123
+ */
2124
+ verificationPolicyIds() {
2125
+ return this._ensureVerificationTracker().policyIds();
2126
+ }
1862
2127
  /** The live verification record with its bounded evidence, if any. */
1863
2128
  verificationRecord() {
1864
2129
  return this._verificationTracker?.latest();