@bastani/atomic 0.9.12 → 0.9.13-alpha.1

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 (264) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/builtin/intercom/CHANGELOG.md +6 -0
  3. package/dist/builtin/intercom/README.md +5 -12
  4. package/dist/builtin/intercom/broker/client.ts +11 -2
  5. package/dist/builtin/intercom/contact-supervisor-tool.ts +13 -3
  6. package/dist/builtin/intercom/index-heavy.ts +13 -3
  7. package/dist/builtin/intercom/index.ts +43 -19
  8. package/dist/builtin/intercom/intercom-utils.ts +15 -29
  9. package/dist/builtin/intercom/package.json +1 -1
  10. package/dist/builtin/intercom/source-ownership.ts +9 -12
  11. package/dist/builtin/mcp/CHANGELOG.md +6 -0
  12. package/dist/builtin/mcp/README.md +3 -1
  13. package/dist/builtin/mcp/index.ts +10 -16
  14. package/dist/builtin/mcp/package.json +1 -1
  15. package/dist/builtin/mcp/startup-warmup.ts +20 -8
  16. package/dist/builtin/subagents/CHANGELOG.md +30 -0
  17. package/dist/builtin/subagents/README.md +22 -30
  18. package/dist/builtin/subagents/package.json +1 -1
  19. package/dist/builtin/subagents/src/extension/doctor.ts +1 -1
  20. package/dist/builtin/subagents/src/extension/fanout-child.ts +76 -56
  21. package/dist/builtin/subagents/src/extension/index.ts +23 -32
  22. package/dist/builtin/subagents/src/extension/startup-maintenance.ts +25 -44
  23. package/dist/builtin/subagents/src/intercom/result-intercom.ts +7 -7
  24. package/dist/builtin/subagents/src/runs/background/async-job-tracker.ts +166 -434
  25. package/dist/builtin/subagents/src/runs/background/notify.ts +29 -8
  26. package/dist/builtin/subagents/src/runs/foreground/chain-execution-dynamic-step.ts +2 -2
  27. package/dist/builtin/subagents/src/runs/foreground/chain-execution-parallel-runner.ts +4 -4
  28. package/dist/builtin/subagents/src/runs/foreground/chain-execution-parallel-step.ts +2 -2
  29. package/dist/builtin/subagents/src/runs/foreground/chain-execution-sequential-step.ts +2 -2
  30. package/dist/builtin/subagents/src/runs/foreground/execution-run-sync.ts +1 -281
  31. package/dist/builtin/subagents/src/runs/foreground/execution-structured-retries.ts +11 -75
  32. package/dist/builtin/subagents/src/runs/foreground/inprocess-run-sync.ts +457 -0
  33. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-async.ts +119 -246
  34. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-chain.ts +1 -1
  35. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-context.ts +18 -9
  36. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-parallel-task.ts +1 -1
  37. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-parallel.ts +4 -4
  38. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-resume.ts +1 -646
  39. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-runtime.ts +2 -7
  40. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-single.ts +5 -5
  41. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-status.ts +7 -8
  42. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-types.ts +14 -5
  43. package/dist/builtin/subagents/src/runs/foreground/subagent-executor.ts +158 -93
  44. package/dist/builtin/subagents/src/runs/inprocess/background-single.ts +179 -0
  45. package/dist/builtin/subagents/src/runs/inprocess/background.ts +89 -0
  46. package/dist/builtin/subagents/src/runs/inprocess/child-policy.ts +27 -0
  47. package/dist/builtin/subagents/src/runs/inprocess/control-registry.ts +55 -0
  48. package/dist/builtin/subagents/src/runs/inprocess/control-status.ts +106 -0
  49. package/dist/builtin/subagents/src/runs/inprocess/index.ts +50 -0
  50. package/dist/builtin/subagents/src/runs/inprocess/nested-routing.ts +218 -0
  51. package/dist/builtin/subagents/src/runs/{shared/subagent-prompt-runtime.ts → inprocess/prompt-behavior.ts} +37 -75
  52. package/dist/builtin/subagents/src/runs/inprocess/runner.ts +1138 -0
  53. package/dist/builtin/subagents/src/runs/{shared/nested-events.ts → inprocess/runtime-support/nested-api.ts} +13 -7
  54. package/dist/builtin/subagents/src/runs/{shared/nested-events-control.ts → inprocess/runtime-support/nested-control.ts} +18 -17
  55. package/dist/builtin/subagents/src/runs/{shared/nested-events-core.ts → inprocess/runtime-support/nested-core.ts} +14 -61
  56. package/dist/builtin/subagents/src/runs/{shared/nested-events-projection.ts → inprocess/runtime-support/nested-projection.ts} +4 -10
  57. package/dist/builtin/subagents/src/runs/{shared/nested-events-registry.ts → inprocess/runtime-support/nested-registry.ts} +19 -4
  58. package/dist/builtin/subagents/src/runs/{shared/nested-render.ts → inprocess/runtime-support/nested-rendering.ts} +3 -3
  59. package/dist/builtin/subagents/src/runs/{shared/nested-events-sanitize.ts → inprocess/runtime-support/nested-sanitize.ts} +3 -3
  60. package/dist/builtin/subagents/src/runs/inprocess.ts +1 -0
  61. package/dist/builtin/subagents/src/runs/shared/dynamic-fanout.ts +10 -4
  62. package/dist/builtin/subagents/src/runs/shared/model-candidate-filter.ts +0 -1
  63. package/dist/builtin/subagents/src/runs/shared/model-fallback.ts +15 -492
  64. package/dist/builtin/subagents/src/runs/shared/parallel-utils.ts +13 -11
  65. package/dist/builtin/subagents/src/runs/shared/run-history.ts +3 -5
  66. package/dist/builtin/subagents/src/runs/shared/single-output.ts +4 -4
  67. package/dist/builtin/subagents/src/runs/shared/workflow-graph.ts +5 -5
  68. package/dist/builtin/subagents/src/shared/types-async.ts +0 -1
  69. package/dist/builtin/subagents/src/shared/types-config.ts +18 -0
  70. package/dist/builtin/subagents/src/shared/types-results.ts +9 -2
  71. package/dist/builtin/subagents/src/slash/prompt-template-bridge.ts +2 -3
  72. package/dist/builtin/subagents/src/slash/slash-live-state.ts +2 -2
  73. package/dist/builtin/subagents/src/tui/render-chain-graph.ts +10 -4
  74. package/dist/builtin/subagents/src/tui/render-event-formatting.ts +12 -11
  75. package/dist/builtin/subagents/src/tui/render-result-compact.ts +49 -12
  76. package/dist/builtin/subagents/src/tui/render-result.ts +46 -17
  77. package/dist/builtin/subagents/src/tui/render-stable-output.ts +1 -1
  78. package/dist/builtin/subagents/src/tui/render-status-progress.ts +8 -5
  79. package/dist/builtin/subagents/src/tui/render-widget-graph.ts +0 -8
  80. package/dist/builtin/subagents/src/tui/render-widget.ts +0 -24
  81. package/dist/builtin/web-access/package.json +1 -1
  82. package/dist/builtin/workflows/CHANGELOG.md +25 -0
  83. package/dist/builtin/workflows/README.md +13 -3
  84. package/dist/builtin/workflows/package.json +1 -1
  85. package/dist/builtin/workflows/src/durable/backend.ts +7 -0
  86. package/dist/builtin/workflows/src/durable/dbos-metadata.ts +11 -1
  87. package/dist/builtin/workflows/src/durable/resume-runtime.ts +1 -0
  88. package/dist/builtin/workflows/src/durable/tool-primitive.ts +4 -28
  89. package/dist/builtin/workflows/src/durable/types.ts +5 -0
  90. package/dist/builtin/workflows/src/engine/run.ts +17 -1
  91. package/dist/builtin/workflows/src/extension/atomic-stage-session.ts +12 -39
  92. package/dist/builtin/workflows/src/extension/config-file-loader.ts +10 -1
  93. package/dist/builtin/workflows/src/extension/config-loader.ts +1 -1
  94. package/dist/builtin/workflows/src/extension/dispatcher.ts +4 -0
  95. package/dist/builtin/workflows/src/extension/index.bundle.mjs +1151 -737
  96. package/dist/builtin/workflows/src/extension/lifecycle-notifications.ts +303 -38
  97. package/dist/builtin/workflows/src/extension/render-result.ts +4 -0
  98. package/dist/builtin/workflows/src/extension/runtime-durable-resume.ts +6 -3
  99. package/dist/builtin/workflows/src/extension/runtime.ts +7 -1
  100. package/dist/builtin/workflows/src/extension/wiring.ts +53 -11
  101. package/dist/builtin/workflows/src/extension/workflow-command-registration.ts +12 -3
  102. package/dist/builtin/workflows/src/extension/workflow-durable-resume-command.ts +5 -2
  103. package/dist/builtin/workflows/src/extension/workflow-run-control-command.ts +9 -7
  104. package/dist/builtin/workflows/src/extension/workflow-status-summary.ts +11 -0
  105. package/dist/builtin/workflows/src/extension/workflow-tool-content.ts +18 -1
  106. package/dist/builtin/workflows/src/extension/workflow-tool-control.ts +10 -8
  107. package/dist/builtin/workflows/src/extension/workflow-tool.ts +9 -4
  108. package/dist/builtin/workflows/src/runs/background/quit.ts +28 -4
  109. package/dist/builtin/workflows/src/runs/background/resume-acknowledgements.ts +2 -1
  110. package/dist/builtin/workflows/src/runs/background/status.ts +55 -8
  111. package/dist/builtin/workflows/src/runs/foreground/executor-direct-helpers.ts +4 -0
  112. package/dist/builtin/workflows/src/runs/foreground/executor-prompt-nodes.ts +3 -1
  113. package/dist/builtin/workflows/src/runs/foreground/executor-stage-control.ts +1 -1
  114. package/dist/builtin/workflows/src/runs/foreground/stage-runner-context.ts +1 -1
  115. package/dist/builtin/workflows/src/runs/foreground/stage-runner-controller.ts +688 -38
  116. package/dist/builtin/workflows/src/runs/foreground/stage-runner-types.ts +3 -1
  117. package/dist/builtin/workflows/src/runs/shared/model-fallback-failures.ts +19 -514
  118. package/dist/builtin/workflows/src/runs/shared/retry.ts +44 -0
  119. package/dist/builtin/workflows/src/shared/authoring-contract-stage.d.ts +5 -0
  120. package/dist/builtin/workflows/src/shared/authoring-contract-stage.ts +6 -0
  121. package/dist/builtin/workflows/src/shared/authoring-contract-ui.d.ts +5 -1
  122. package/dist/builtin/workflows/src/shared/authoring-contract-ui.ts +5 -0
  123. package/dist/builtin/workflows/src/shared/persistence-restore-helpers.ts +5 -0
  124. package/dist/builtin/workflows/src/shared/persistence-restore.ts +3 -0
  125. package/dist/builtin/workflows/src/shared/persistence-session-entries.ts +4 -0
  126. package/dist/builtin/workflows/src/shared/run-indicator-status.ts +75 -0
  127. package/dist/builtin/workflows/src/shared/store-public-types.ts +43 -7
  128. package/dist/builtin/workflows/src/shared/store-run-methods.ts +42 -8
  129. package/dist/builtin/workflows/src/shared/store-stage-methods.ts +31 -8
  130. package/dist/builtin/workflows/src/shared/store-types.ts +34 -0
  131. package/dist/builtin/workflows/src/shared/timing.ts +10 -0
  132. package/dist/builtin/workflows/src/tui/chat-surface-message.ts +22 -2
  133. package/dist/builtin/workflows/src/tui/session-list.ts +1 -1
  134. package/dist/builtin/workflows/src/tui/session-overlays.ts +14 -10
  135. package/dist/builtin/workflows/src/tui/session-picker.ts +20 -4
  136. package/dist/builtin/workflows/src/tui/status-list.ts +31 -47
  137. package/dist/builtin/workflows/src/tui/store-widget-installer.ts +9 -1
  138. package/dist/builtin/workflows/src/tui/widget.ts +16 -46
  139. package/dist/core/agent-session-auto-compaction.d.ts.map +1 -1
  140. package/dist/core/agent-session-auto-compaction.js +54 -11
  141. package/dist/core/agent-session-auto-compaction.js.map +1 -1
  142. package/dist/core/agent-session-events.d.ts.map +1 -1
  143. package/dist/core/agent-session-events.js +49 -14
  144. package/dist/core/agent-session-events.js.map +1 -1
  145. package/dist/core/agent-session-methods.d.ts +13 -0
  146. package/dist/core/agent-session-methods.d.ts.map +1 -1
  147. package/dist/core/agent-session-methods.js.map +1 -1
  148. package/dist/core/agent-session-models.d.ts.map +1 -1
  149. package/dist/core/agent-session-models.js +12 -1
  150. package/dist/core/agent-session-models.js.map +1 -1
  151. package/dist/core/agent-session-prompt.d.ts.map +1 -1
  152. package/dist/core/agent-session-prompt.js +4 -0
  153. package/dist/core/agent-session-prompt.js.map +1 -1
  154. package/dist/core/agent-session-retry.d.ts +21 -4
  155. package/dist/core/agent-session-retry.d.ts.map +1 -1
  156. package/dist/core/agent-session-retry.js +232 -106
  157. package/dist/core/agent-session-retry.js.map +1 -1
  158. package/dist/core/agent-session-state.d.ts.map +1 -1
  159. package/dist/core/agent-session-state.js +2 -1
  160. package/dist/core/agent-session-state.js.map +1 -1
  161. package/dist/core/agent-session-tool-hooks.d.ts.map +1 -1
  162. package/dist/core/agent-session-tool-hooks.js +12 -0
  163. package/dist/core/agent-session-tool-hooks.js.map +1 -1
  164. package/dist/core/agent-session-tool-registry.d.ts.map +1 -1
  165. package/dist/core/agent-session-tool-registry.js +1 -1
  166. package/dist/core/agent-session-tool-registry.js.map +1 -1
  167. package/dist/core/agent-session-types.d.ts +3 -1
  168. package/dist/core/agent-session-types.d.ts.map +1 -1
  169. package/dist/core/agent-session-types.js.map +1 -1
  170. package/dist/core/agent-session.d.ts +12 -1
  171. package/dist/core/agent-session.d.ts.map +1 -1
  172. package/dist/core/agent-session.js +7 -0
  173. package/dist/core/agent-session.js.map +1 -1
  174. package/dist/core/extensions/context-types.d.ts +25 -0
  175. package/dist/core/extensions/context-types.d.ts.map +1 -1
  176. package/dist/core/extensions/context-types.js.map +1 -1
  177. package/dist/core/extensions/index.d.ts +1 -1
  178. package/dist/core/extensions/index.d.ts.map +1 -1
  179. package/dist/core/extensions/index.js.map +1 -1
  180. package/dist/core/extensions/runner-context.d.ts +2 -1
  181. package/dist/core/extensions/runner-context.d.ts.map +1 -1
  182. package/dist/core/extensions/runner-context.js +4 -0
  183. package/dist/core/extensions/runner-context.js.map +1 -1
  184. package/dist/core/extensions/runner.d.ts +3 -2
  185. package/dist/core/extensions/runner.d.ts.map +1 -1
  186. package/dist/core/extensions/runner.js +3 -1
  187. package/dist/core/extensions/runner.js.map +1 -1
  188. package/dist/core/model-fallback-failures.d.ts +30 -0
  189. package/dist/core/model-fallback-failures.d.ts.map +1 -0
  190. package/dist/core/model-fallback-failures.js +610 -0
  191. package/dist/core/model-fallback-failures.js.map +1 -0
  192. package/dist/core/retry-policy.d.ts +30 -0
  193. package/dist/core/retry-policy.d.ts.map +1 -0
  194. package/dist/core/retry-policy.js +25 -0
  195. package/dist/core/retry-policy.js.map +1 -0
  196. package/dist/core/sdk-types.d.ts +8 -2
  197. package/dist/core/sdk-types.d.ts.map +1 -1
  198. package/dist/core/sdk-types.js.map +1 -1
  199. package/dist/core/sdk.d.ts.map +1 -1
  200. package/dist/core/sdk.js +6 -1
  201. package/dist/core/sdk.js.map +1 -1
  202. package/dist/core/session-manager-core.d.ts +2 -0
  203. package/dist/core/session-manager-core.d.ts.map +1 -1
  204. package/dist/core/session-manager-core.js +7 -0
  205. package/dist/core/session-manager-core.js.map +1 -1
  206. package/dist/index-extensions.d.ts +1 -1
  207. package/dist/index-extensions.d.ts.map +1 -1
  208. package/dist/index-extensions.js.map +1 -1
  209. package/dist/index.d.ts +3 -0
  210. package/dist/index.d.ts.map +1 -1
  211. package/dist/index.js +2 -0
  212. package/dist/index.js.map +1 -1
  213. package/docs/intercom.md +5 -10
  214. package/docs/settings.md +7 -1
  215. package/docs/subagents.md +19 -19
  216. package/docs/workflows.md +21 -5
  217. package/examples/extensions/gondolin/package-lock.json +3 -3
  218. package/npm-shrinkwrap.json +29 -29
  219. package/package.json +2 -2
  220. package/dist/builtin/subagents/src/runs/background/async-event-journal.ts +0 -112
  221. package/dist/builtin/subagents/src/runs/background/async-execution-chain.ts +0 -543
  222. package/dist/builtin/subagents/src/runs/background/async-execution-common.ts +0 -140
  223. package/dist/builtin/subagents/src/runs/background/async-execution-single.ts +0 -279
  224. package/dist/builtin/subagents/src/runs/background/async-execution-types.ts +0 -99
  225. package/dist/builtin/subagents/src/runs/background/async-execution.ts +0 -17
  226. package/dist/builtin/subagents/src/runs/background/async-resume.ts +0 -402
  227. package/dist/builtin/subagents/src/runs/background/async-status.ts +0 -411
  228. package/dist/builtin/subagents/src/runs/background/completion-claims.ts +0 -196
  229. package/dist/builtin/subagents/src/runs/background/completion-dedupe.ts +0 -102
  230. package/dist/builtin/subagents/src/runs/background/parallel-groups.ts +0 -59
  231. package/dist/builtin/subagents/src/runs/background/result-delivery-processor.ts +0 -300
  232. package/dist/builtin/subagents/src/runs/background/result-file-claims.ts +0 -166
  233. package/dist/builtin/subagents/src/runs/background/result-quarantine.ts +0 -77
  234. package/dist/builtin/subagents/src/runs/background/result-retry-scheduler.ts +0 -48
  235. package/dist/builtin/subagents/src/runs/background/result-status.ts +0 -87
  236. package/dist/builtin/subagents/src/runs/background/result-watcher-data.ts +0 -72
  237. package/dist/builtin/subagents/src/runs/background/result-watcher.ts +0 -320
  238. package/dist/builtin/subagents/src/runs/background/run-id-resolver.ts +0 -105
  239. package/dist/builtin/subagents/src/runs/background/run-status.ts +0 -358
  240. package/dist/builtin/subagents/src/runs/background/stale-run-reconciler.ts +0 -457
  241. package/dist/builtin/subagents/src/runs/background/subagent-runner-dynamic.ts +0 -376
  242. package/dist/builtin/subagents/src/runs/background/subagent-runner-finalize.ts +0 -150
  243. package/dist/builtin/subagents/src/runs/background/subagent-runner-output.ts +0 -109
  244. package/dist/builtin/subagents/src/runs/background/subagent-runner-parallel-helpers.ts +0 -128
  245. package/dist/builtin/subagents/src/runs/background/subagent-runner-parallel.ts +0 -309
  246. package/dist/builtin/subagents/src/runs/background/subagent-runner-sequential.ts +0 -173
  247. package/dist/builtin/subagents/src/runs/background/subagent-runner-state.ts +0 -578
  248. package/dist/builtin/subagents/src/runs/background/subagent-runner-step.ts +0 -330
  249. package/dist/builtin/subagents/src/runs/background/subagent-runner-streaming.ts +0 -330
  250. package/dist/builtin/subagents/src/runs/background/subagent-runner-types.ts +0 -210
  251. package/dist/builtin/subagents/src/runs/background/subagent-runner-utils.ts +0 -69
  252. package/dist/builtin/subagents/src/runs/background/subagent-runner.ts +0 -90
  253. package/dist/builtin/subagents/src/runs/background/top-level-async.ts +0 -12
  254. package/dist/builtin/subagents/src/runs/foreground/execution-attempt-control.ts +0 -140
  255. package/dist/builtin/subagents/src/runs/foreground/execution-attempt-finalize.ts +0 -108
  256. package/dist/builtin/subagents/src/runs/foreground/execution-attempt-types.ts +0 -17
  257. package/dist/builtin/subagents/src/runs/foreground/execution-attempt.ts +0 -558
  258. package/dist/builtin/subagents/src/runs/shared/attempt-watchdog.ts +0 -126
  259. package/dist/builtin/subagents/src/runs/shared/final-drain.ts +0 -39
  260. package/dist/builtin/subagents/src/runs/shared/pi-args.ts +0 -339
  261. package/dist/builtin/subagents/src/runs/shared/pi-spawn.ts +0 -165
  262. package/dist/builtin/subagents/src/runs/shared/spawn-env.ts +0 -21
  263. package/dist/builtin/subagents/src/shared/post-exit-stdio-guard.ts +0 -86
  264. /package/dist/builtin/subagents/src/runs/{shared/nested-path.ts → inprocess/runtime-support/nested-paths.ts} +0 -0
@@ -0,0 +1,1138 @@
1
+ import { appendFileSync, existsSync, mkdirSync, readdirSync, statSync, writeFileSync } from "node:fs";
2
+ import { dirname, join, relative, resolve } from "node:path";
3
+ import {
4
+ type AgentSession,
5
+ type AgentSessionEvent,
6
+ type AgentSessionEventListener,
7
+ type CreateAgentSessionOptions,
8
+ createAgentSession,
9
+ DefaultResourceLoader,
10
+ getAgentDir,
11
+ SessionManager,
12
+ type SessionStats,
13
+ SettingsManager,
14
+ type SubagentIntercomIdentity,
15
+ } from "@bastani/atomic";
16
+ import {
17
+ type ChildIdentity,
18
+ type NativeAdmissionResult,
19
+ type AgentStatus as NativeAgentStatus,
20
+ type NativeExecutionGuardResult,
21
+ type TerminationCause as NativeTerminationCause,
22
+ SubagentControl,
23
+ } from "@bastani/atomic-natives";
24
+ import type { Api, Model } from "@earendil-works/pi-ai/compat";
25
+ import type { AgentConfig } from "../../agents/agent-types.ts";
26
+ import { ensureArtifactsDir, writeArtifact, writeMetadata } from "../../shared/artifacts.ts";
27
+ import {
28
+ type AgentProgress,
29
+ type ArtifactPaths,
30
+ DEFAULT_MAX_OUTPUT,
31
+ type JsonSchemaObject,
32
+ type MaxOutputConfig,
33
+ truncateOutput,
34
+ } from "../../shared/types.ts";
35
+ import {
36
+ formatStructuredOutputCorrectionPrompt,
37
+ readStructuredOutput,
38
+ STRUCTURED_OUTPUT_MAX_CORRECTIVE_PROMPTS,
39
+ } from "../shared/structured-output.ts";
40
+ import { type ChildModePolicy, resolveChildModePolicy } from "./child-policy.ts";
41
+ import { type InProcessNestedResumeOutcome, registerInProcessNestedAttempt } from "./nested-routing.ts";
42
+ import { createInProcessChildPromptBehavior } from "./prompt-behavior.ts";
43
+
44
+ export type ChildStatus = NativeAgentStatus;
45
+ export type ContinuationReason = "async-requested" | "intercom-coordination";
46
+ export type TerminationCauseName = NativeTerminationCause;
47
+ export type TerminalStatus = "ok" | "error" | "skipped" | "interrupted" | "continued";
48
+
49
+ export interface ParentContext {
50
+ readonly path: string;
51
+ readonly depth: number;
52
+ readonly sessionId?: string;
53
+ readonly intercomGroup?: string;
54
+ readonly orchestrationContext?: CreateAgentSessionOptions["orchestrationContext"];
55
+ }
56
+
57
+ export interface TestSessionOptions {
58
+ readonly output?: string;
59
+ readonly structuredOutputAfterPrompt?: number;
60
+ readonly promptLogPath?: string;
61
+ /** Hold a test prompt open until the caller releases the supplied promise. */
62
+ readonly promptGate?: Promise<void>;
63
+ /** Match AgentSession.abort() settling an active prompt without throwing. */
64
+ readonly abortResolvesPrompt?: boolean;
65
+ }
66
+
67
+ export interface ChildSpec {
68
+ readonly taskName: string;
69
+ readonly task: string;
70
+ readonly agent: AgentConfig;
71
+ readonly cwd: string;
72
+ readonly tools?: string[];
73
+ readonly excludedTools?: string[];
74
+ readonly mcpDirectTools?: string[];
75
+ readonly skills?: string[];
76
+ readonly customTools?: CreateAgentSessionOptions["customTools"];
77
+ readonly model?: Model<Api>;
78
+ readonly thinkingLevel?: CreateAgentSessionOptions["thinkingLevel"];
79
+ readonly parent?: ParentContext;
80
+ /** Typed identity/capability resolved by the parent before admission. */
81
+ readonly intercom?: SubagentIntercomIdentity;
82
+ readonly sessionFile?: string;
83
+ readonly testSession?: boolean | TestSessionOptions;
84
+ readonly structuredOutput?: { readonly schema: JsonSchemaObject; readonly outputPath: string };
85
+ readonly artifactJsonlPath?: string;
86
+ /**
87
+ * Fallback model candidates (full ids, optional `:thinking` suffix) handed to
88
+ * the SDK session so child fallback uses the exact classification and
89
+ * candidate-advancement behavior main chat and workflow stages share.
90
+ */
91
+ readonly fallbackModels?: readonly string[];
92
+ /** Live progress callback for the parent's tool-result rendering. */
93
+ readonly onProgress?: (progress: AgentProgress) => void;
94
+ }
95
+
96
+ export interface ChildPolicy extends ChildModePolicy {
97
+ readonly cwd: string;
98
+ readonly tools?: readonly string[];
99
+ readonly excludedTools?: readonly string[];
100
+ readonly mcpDirectTools?: readonly string[];
101
+ readonly skills: readonly string[];
102
+ readonly customTools?: CreateAgentSessionOptions["customTools"];
103
+ readonly model?: Model<Api>;
104
+ readonly thinkingLevel?: CreateAgentSessionOptions["thinkingLevel"];
105
+ readonly intercomGroup?: string;
106
+ readonly depth: number;
107
+ }
108
+
109
+ export interface ModelCandidate {
110
+ readonly model?: Model<Api>;
111
+ readonly modelId?: string;
112
+ readonly thinkingLevel?: CreateAgentSessionOptions["thinkingLevel"];
113
+ }
114
+
115
+ export interface AttemptSignals {
116
+ readonly abort: AbortSignal;
117
+ readonly interrupt: AbortSignal;
118
+ }
119
+
120
+ export type AttemptStats = SessionStats;
121
+
122
+ export type AttemptOutcome =
123
+ | {
124
+ readonly status: "ok";
125
+ readonly output: string;
126
+ readonly stats: AttemptStats;
127
+ readonly path: string;
128
+ readonly envelope: string;
129
+ readonly sessionFile?: string;
130
+ readonly structuredOutput?: unknown;
131
+ readonly model?: string;
132
+ readonly attemptedModels?: readonly string[];
133
+ }
134
+ | {
135
+ readonly status: "error";
136
+ readonly cause: string;
137
+ readonly stats: AttemptStats;
138
+ readonly path: string;
139
+ readonly envelope: string;
140
+ readonly sessionFile?: string;
141
+ readonly fallbackSignal?: string;
142
+ readonly model?: string;
143
+ readonly attemptedModels?: readonly string[];
144
+ }
145
+ | {
146
+ readonly status: "interrupted";
147
+ readonly stats: AttemptStats;
148
+ readonly path: string;
149
+ readonly envelope: string;
150
+ readonly sessionFile?: string;
151
+ }
152
+ | {
153
+ readonly status: "continued";
154
+ readonly childPath: string;
155
+ readonly stats: AttemptStats;
156
+ readonly path: string;
157
+ readonly envelope: string;
158
+ readonly sessionFile?: string;
159
+ };
160
+
161
+ export interface ResultEnvelope {
162
+ readonly path: string;
163
+ readonly status: TerminalStatus;
164
+ readonly cause?: string;
165
+ readonly stats: AttemptStats;
166
+ readonly envelope: string;
167
+ readonly model?: string;
168
+ readonly modelAttempts?: readonly {
169
+ readonly model: string;
170
+ readonly status: TerminalStatus;
171
+ readonly cause?: string;
172
+ }[];
173
+ readonly sessionFile?: string;
174
+ readonly artifactsDir?: string;
175
+ readonly durationMs?: number;
176
+ readonly timestamp: number;
177
+ }
178
+
179
+ export interface DeliverChildResultOptions {
180
+ readonly artifactsDir?: string;
181
+ readonly artifactPaths?: ArtifactPaths;
182
+ readonly artifactsDisabled?: boolean;
183
+ readonly maxOutput?: MaxOutputConfig;
184
+ }
185
+
186
+ export interface AdmittedResult {
187
+ readonly admitted?: AdmittedChild;
188
+ readonly refusal?: AdmissionRefusal;
189
+ }
190
+
191
+ export interface AdmissionRefusal {
192
+ readonly kind: NativeAdmissionResult["refusal"] extends infer R ? (R extends { kind: infer K } ? K : never) : never;
193
+ readonly reason: string;
194
+ readonly maxDepth?: number;
195
+ }
196
+
197
+ const EMPTY_STATS: AttemptStats = {
198
+ sessionFile: undefined,
199
+ sessionId: "",
200
+ userMessages: 0,
201
+ assistantMessages: 0,
202
+ toolCalls: 0,
203
+ toolResults: 0,
204
+ totalMessages: 0,
205
+ tokens: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
206
+ cost: 0,
207
+ };
208
+
209
+ const CAPACITY_RETRY_INITIAL_DELAY_MS = 5;
210
+ const INTERRUPTED_ENVELOPE = "Interrupted";
211
+ const CAPACITY_RETRY_MAX_DELAY_MS = 100;
212
+
213
+ type CapacityWaitResult = "retry" | "abort" | "interrupt";
214
+
215
+ function waitForExecutionCapacity(delayMs: number, signals: AttemptSignals): Promise<CapacityWaitResult> {
216
+ if (signals.interrupt.aborted) return Promise.resolve("interrupt");
217
+ if (signals.abort.aborted) return Promise.resolve("abort");
218
+ return new Promise((resolveWait) => {
219
+ let settled = false;
220
+ let timer: ReturnType<typeof setTimeout>;
221
+ const settle = (result: CapacityWaitResult) => {
222
+ if (settled) return;
223
+ settled = true;
224
+ clearTimeout(timer);
225
+ signals.abort.removeEventListener("abort", onAbort);
226
+ signals.interrupt.removeEventListener("abort", onInterrupt);
227
+ resolveWait(result);
228
+ };
229
+ const onAbort = () => settle("abort");
230
+ const onInterrupt = () => settle("interrupt");
231
+ signals.abort.addEventListener("abort", onAbort, { once: true });
232
+ signals.interrupt.addEventListener("abort", onInterrupt, { once: true });
233
+ timer = setTimeout(() => settle("retry"), delayMs);
234
+ });
235
+ }
236
+
237
+ function workflowMetadataFromContext(
238
+ context: CreateAgentSessionOptions["orchestrationContext"] | undefined,
239
+ ): { runId: string; stageId: string; stageName: string } | undefined {
240
+ if (context?.kind !== "workflow-stage") return undefined;
241
+ return {
242
+ runId: context.workflowRunId,
243
+ stageId: context.workflowStageId,
244
+ stageName: context.workflowStageName,
245
+ };
246
+ }
247
+
248
+ function createTestSession(sessionManager: SessionManager, spec: ChildSpec): AgentSession {
249
+ const listeners = new Set<AgentSessionEventListener>();
250
+ const testOptions = typeof spec.testSession === "object" ? spec.testSession : {};
251
+ let lastAssistantText = "";
252
+ let aborted = false;
253
+ let settlePromptAbort: (() => void) | undefined;
254
+ const promptAbort = new Promise<"aborted">((resolve, reject) => {
255
+ settlePromptAbort = () => {
256
+ if (testOptions.abortResolvesPrompt) resolve("aborted");
257
+ else reject(new Error("aborted"));
258
+ };
259
+ });
260
+ let promptCount = 0;
261
+ let userMessages = 0;
262
+ let assistantMessages = 0;
263
+ const zeroTokens = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 };
264
+ const zeroCost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 };
265
+ return {
266
+ sessionFile: sessionManager.getSessionFile(),
267
+ abort: async () => {
268
+ aborted = true;
269
+ settlePromptAbort?.();
270
+ },
271
+ subscribe(listener: AgentSessionEventListener) {
272
+ listeners.add(listener);
273
+ return () => listeners.delete(listener);
274
+ },
275
+ prompt: async (text: string) => {
276
+ for (const listener of listeners) listener({ type: "agent_start" } as AgentSessionEvent);
277
+ if (aborted) throw new Error("aborted");
278
+ promptCount += 1;
279
+ if (testOptions.promptLogPath) appendFileSync(testOptions.promptLogPath, `${text}\n---PROMPT---\n`, "utf8");
280
+ sessionManager.appendMessage({ role: "user", content: text, timestamp: Date.now() });
281
+ userMessages += 1;
282
+ if (testOptions.promptGate) {
283
+ const gateResult = await Promise.race([
284
+ testOptions.promptGate.then(() => "released" as const),
285
+ promptAbort,
286
+ ]);
287
+ if (gateResult === "aborted") return "";
288
+ }
289
+ if (
290
+ spec.structuredOutput &&
291
+ testOptions.structuredOutputAfterPrompt !== undefined &&
292
+ promptCount >= testOptions.structuredOutputAfterPrompt
293
+ ) {
294
+ writeFileSync(spec.structuredOutput.outputPath, JSON.stringify({ answer: "ok" }), "utf8");
295
+ }
296
+ lastAssistantText = testOptions.output ?? "done";
297
+ sessionManager.appendMessage({
298
+ role: "assistant",
299
+ content: [{ type: "text", text: lastAssistantText }],
300
+ api: "openai-responses",
301
+ provider: "openai",
302
+ model: "gpt-5.4",
303
+ usage: {
304
+ input: 0,
305
+ output: 0,
306
+ cacheRead: 0,
307
+ cacheWrite: 0,
308
+ totalTokens: 0,
309
+ cost: zeroCost,
310
+ },
311
+ stopReason: "stop",
312
+ timestamp: Date.now(),
313
+ });
314
+ assistantMessages += 1;
315
+ return lastAssistantText;
316
+ },
317
+ getLastAssistantText: () => lastAssistantText,
318
+ getSessionStats: () => ({
319
+ sessionFile: sessionManager.getSessionFile(),
320
+ sessionId: sessionManager.getSessionId(),
321
+ userMessages,
322
+ assistantMessages,
323
+ toolCalls: 0,
324
+ toolResults: 0,
325
+ totalMessages: userMessages + assistantMessages,
326
+ tokens: zeroTokens,
327
+ cost: 0,
328
+ }),
329
+ dispose: () => {},
330
+ } as unknown as AgentSession;
331
+ }
332
+
333
+ function nativeStatus(value: ChildStatus): NativeAgentStatus {
334
+ return value;
335
+ }
336
+
337
+ function nativeCause(value: TerminationCauseName): NativeTerminationCause {
338
+ return value;
339
+ }
340
+
341
+ function refusal(result: NativeAdmissionResult): AdmittedResult {
342
+ if (!result.refusal) return {};
343
+ return {
344
+ refusal: {
345
+ kind: result.refusal.kind,
346
+ reason: result.refusal.reason,
347
+ ...(result.refusal.maxDepth === undefined ? {} : { maxDepth: result.refusal.maxDepth }),
348
+ },
349
+ };
350
+ }
351
+
352
+ function statsFor(session: AgentSession | undefined, fallbackSessionId: string): AttemptStats {
353
+ if (!session) return { ...EMPTY_STATS, sessionId: fallbackSessionId };
354
+ try {
355
+ return session.getSessionStats();
356
+ } catch {
357
+ return {
358
+ ...EMPTY_STATS,
359
+ sessionId: fallbackSessionId,
360
+ sessionFile: session.sessionFile,
361
+ };
362
+ }
363
+ }
364
+
365
+ function outputFor(session: AgentSession | undefined): string {
366
+ return session?.getLastAssistantText() ?? "";
367
+ }
368
+
369
+ function boundedEnvelope(output: string, artifactPath?: string, maxOutput?: MaxOutputConfig): string {
370
+ const config = { ...DEFAULT_MAX_OUTPUT, ...maxOutput };
371
+ const result = truncateOutput(output, config, artifactPath);
372
+ return result.truncated && artifactPath ? `${result.text}\n\nFull output: ${artifactPath}` : result.text;
373
+ }
374
+
375
+ function canonicalArtifactPaths(artifactsDir: string, childPath: string): ArtifactPaths {
376
+ const prefix = childPath.replaceAll("/", "_");
377
+ return {
378
+ inputPath: join(artifactsDir, `${prefix}_input.md`),
379
+ outputPath: join(artifactsDir, `${prefix}_output.md`),
380
+ jsonlPath: join(artifactsDir, `${prefix}.jsonl`),
381
+ metadataPath: join(artifactsDir, `${prefix}_meta.json`),
382
+ };
383
+ }
384
+
385
+ function validatePath(pathValue: string): void {
386
+ if (
387
+ !pathValue ||
388
+ pathValue.startsWith("/") ||
389
+ pathValue.includes("\\") ||
390
+ pathValue.split("/").some((part) => !part || part === "." || part === "..")
391
+ ) {
392
+ throw new Error("child path is outside the trusted session root");
393
+ }
394
+ }
395
+
396
+ function trustedSessionRoot(parent: ParentContext, requestedRoot?: string): string {
397
+ validatePath(parent.path);
398
+ const root = resolve(requestedRoot ?? join(process.cwd(), ".atomic", "subagents"));
399
+ const child = resolve(root, ...parent.path.split("/"));
400
+ if (relative(root, child).startsWith("..")) throw new Error("subagent session root escapes trusted root");
401
+ return root;
402
+ }
403
+
404
+ function sessionDirectory(root: string, pathValue: string): string {
405
+ validatePath(pathValue);
406
+ const directory = resolve(root, ...pathValue.split("/"));
407
+ if (relative(root, directory).startsWith("..")) throw new Error("child session path escapes trusted root");
408
+ return directory;
409
+ }
410
+
411
+ function safeArgsPreview(args: unknown): string {
412
+ if (args === undefined) return "";
413
+ try {
414
+ const text = typeof args === "string" ? args : JSON.stringify(args);
415
+ return text.length > 200 ? `${text.slice(0, 200)}…` : text;
416
+ } catch {
417
+ return "";
418
+ }
419
+ }
420
+
421
+ function writeEvent(pathValue: string | undefined, event: AgentSessionEvent): void {
422
+ if (!pathValue) return;
423
+ mkdirSync(dirname(pathValue), { recursive: true });
424
+ appendFileSync(pathValue, `${JSON.stringify(event)}\n`, "utf8");
425
+ }
426
+
427
+ /**
428
+ * A child identity whose constructor is private. Depth and path validation happen
429
+ * at the Rust admission door, so callers cannot manufacture an admitted depth-6
430
+ * child or bypass canonical identity allocation.
431
+ */
432
+ interface AdmittedChildInput {
433
+ readonly identity: ChildIdentity;
434
+ readonly spec: ChildSpec;
435
+ readonly policy: ChildPolicy;
436
+ readonly sessionDir: string;
437
+ readonly sessionFile?: string;
438
+ readonly control: SubagentControlRuntime;
439
+ }
440
+
441
+ export class AdmittedChild {
442
+ readonly identity: ChildIdentity;
443
+ readonly spec: ChildSpec;
444
+ readonly policy: ChildPolicy;
445
+ readonly sessionDir: string;
446
+ readonly sessionFile?: string;
447
+ readonly control: SubagentControlRuntime;
448
+
449
+ private constructor(input: {
450
+ identity: ChildIdentity;
451
+ spec: ChildSpec;
452
+ policy: ChildPolicy;
453
+ sessionDir: string;
454
+ sessionFile?: string;
455
+ control: SubagentControlRuntime;
456
+ }) {
457
+ this.identity = input.identity;
458
+ this.spec = input.spec;
459
+ this.policy = input.policy;
460
+ this.sessionDir = input.sessionDir;
461
+ this.sessionFile = input.sessionFile;
462
+ this.control = input.control;
463
+ }
464
+
465
+ static create(input: AdmittedChildInput): AdmittedChild {
466
+ if (input.identity.depth > 5) throw new Error("child depth exceeds maximum 5");
467
+ validatePath(input.identity.path);
468
+ return new AdmittedChild(input);
469
+ }
470
+ }
471
+
472
+ export interface RunningAttempt {
473
+ readonly id: number;
474
+ readonly child: AdmittedChild;
475
+ readonly candidate: ModelCandidate;
476
+ readonly startedAt: number;
477
+ status: "running" | ChildStatus;
478
+ promise: Promise<AttemptOutcome>;
479
+ terminate?: (cause: TerminationCauseName) => Promise<void>;
480
+ attemptToken?: number;
481
+ }
482
+
483
+ export class SubagentControlRuntime {
484
+ readonly native: SubagentControl;
485
+ readonly parent: ParentContext;
486
+ readonly sessionRoot: string;
487
+ private readonly sessions = new Map<string, AgentSession>();
488
+ private readonly specs = new Map<string, ChildSpec>();
489
+ private readonly sessionFiles = new Map<string, string>();
490
+ private readonly runningAttempts = new Map<number, RunningAttempt>();
491
+ private readonly attemptTokens = new Map<string, number>();
492
+ private readonly attemptTerminators = new Map<string, (cause: TerminationCauseName) => Promise<void>>();
493
+ private readonly delivered = new Set<string>();
494
+ private readonly deliveredEnvelopes = new Map<string, ResultEnvelope>();
495
+ private nextAttemptId = 1;
496
+
497
+ constructor(parent: ParentContext, sessionRoot?: string) {
498
+ this.parent = { ...parent };
499
+ this.sessionRoot = trustedSessionRoot(this.parent, sessionRoot);
500
+ this.native = new SubagentControl(this.parent.path);
501
+ }
502
+
503
+ registerAgents(agents: readonly AgentConfig[]): void {
504
+ for (const agent of agents) this.native.registerAgent(agent.name);
505
+ }
506
+
507
+ admitChildSession(spec: ChildSpec, parent: ParentContext = this.parent): AdmittedResult {
508
+ if (!existsSync(spec.cwd) || !statSync(spec.cwd).isDirectory()) {
509
+ return {
510
+ refusal: {
511
+ kind: "invalidCwd",
512
+ reason: `cwd is not a directory: ${spec.cwd}`,
513
+ },
514
+ };
515
+ }
516
+ const native = this.native.admitChildSession(
517
+ { taskName: spec.taskName, agentName: spec.agent.name, cwd: spec.cwd },
518
+ { path: parent.path, depth: parent.depth },
519
+ );
520
+ if (!native.child) return refusal(native);
521
+ const identity = native.child;
522
+ const childModePolicy = resolveChildModePolicy(spec);
523
+ const sessionDir = spec.sessionFile
524
+ ? dirname(spec.sessionFile)
525
+ : sessionDirectory(this.sessionRoot, identity.path);
526
+ mkdirSync(sessionDir, { recursive: true });
527
+ this.specs.set(identity.path, spec);
528
+ return {
529
+ admitted: AdmittedChild.create({
530
+ identity,
531
+ spec,
532
+ policy: {
533
+ ...childModePolicy,
534
+ cwd: spec.cwd,
535
+ tools: spec.tools ?? spec.agent.tools,
536
+ excludedTools: spec.excludedTools,
537
+ mcpDirectTools: spec.mcpDirectTools ?? spec.agent.mcpDirectTools,
538
+ skills: [...(spec.skills ?? spec.agent.skills ?? [])],
539
+ customTools: spec.customTools,
540
+ model: spec.model,
541
+ thinkingLevel: spec.thinkingLevel ?? (spec.agent.thinking as ChildPolicy["thinkingLevel"]),
542
+ intercomGroup: parent.intercomGroup,
543
+ intercom: spec.intercom,
544
+ depth: identity.depth,
545
+ },
546
+ sessionDir,
547
+ sessionFile: spec.sessionFile,
548
+ control: this,
549
+ }),
550
+ };
551
+ }
552
+
553
+ async runChildAttempt(
554
+ admitted: AdmittedChild,
555
+ candidate: ModelCandidate,
556
+ signals: AttemptSignals,
557
+ ): Promise<AttemptOutcome> {
558
+ let guard: NativeExecutionGuardResult;
559
+ let retryDelayMs = CAPACITY_RETRY_INITIAL_DELAY_MS;
560
+ for (;;) {
561
+ guard = this.native.beginChildAttempt(admitted.identity.path);
562
+ if (guard.token || guard.refusal?.kind !== "capacityExhausted") break;
563
+ const waitResult = await waitForExecutionCapacity(retryDelayMs, signals);
564
+ if (waitResult !== "retry") {
565
+ const stats = { ...EMPTY_STATS, sessionId: admitted.identity.path };
566
+ const status = waitResult === "interrupt" ? "interrupted" : "error";
567
+ this.native.publishChildStatus(admitted.identity.path, nativeStatus(status));
568
+ return status === "interrupted"
569
+ ? {
570
+ status,
571
+ stats,
572
+ path: admitted.identity.path,
573
+ envelope: INTERRUPTED_ENVELOPE,
574
+ }
575
+ : {
576
+ status,
577
+ cause: waitResult,
578
+ stats,
579
+ path: admitted.identity.path,
580
+ envelope: boundedEnvelope(waitResult),
581
+ };
582
+ }
583
+ retryDelayMs = Math.min(retryDelayMs * 2, CAPACITY_RETRY_MAX_DELAY_MS);
584
+ }
585
+ if (!guard.token) {
586
+ const reason = guard.refusal?.reason ?? "child execution was refused";
587
+ const stats = { ...EMPTY_STATS, sessionId: admitted.identity.path };
588
+ this.native.publishChildStatus(admitted.identity.path, nativeStatus("error"));
589
+ return {
590
+ status: "error",
591
+ cause: reason,
592
+ stats,
593
+ path: admitted.identity.path,
594
+ envelope: boundedEnvelope(reason),
595
+ };
596
+ }
597
+ const token = guard.token;
598
+ this.attemptTokens.set(admitted.identity.path, token);
599
+ let session: AgentSession | undefined;
600
+ let activeSessionManager: SessionManager | undefined;
601
+ let termination: TerminationCauseName | undefined;
602
+ let terminating: Promise<void> | undefined;
603
+ let unsubscribe: (() => void) | undefined;
604
+ const terminate = async (cause: TerminationCauseName): Promise<void> => {
605
+ if (terminating) return terminating;
606
+ termination = cause;
607
+ terminating = (async () => {
608
+ try {
609
+ activeSessionManager?.flush();
610
+ await session?.abort();
611
+ } finally {
612
+ try {
613
+ await this.native.terminateChildAttempt(token, nativeCause(cause));
614
+ } catch {
615
+ // The attempt may have completed between signal delivery and termination.
616
+ }
617
+ }
618
+ })();
619
+ return terminating;
620
+ };
621
+ this.attemptTerminators.set(admitted.identity.path, terminate);
622
+ const abortListener = () => void terminate("abort");
623
+ const interruptListener = () => void terminate("interrupt");
624
+ signals.abort.addEventListener("abort", abortListener, { once: true });
625
+ signals.interrupt.addEventListener("abort", interruptListener, {
626
+ once: true,
627
+ });
628
+ try {
629
+ const workflow = workflowMetadataFromContext(admitted.spec.parent?.orchestrationContext);
630
+ if (admitted.sessionFile) mkdirSync(dirname(admitted.sessionFile), { recursive: true });
631
+ const sessionManager = admitted.sessionFile
632
+ ? SessionManager.open(admitted.sessionFile, admitted.sessionDir, admitted.policy.cwd)
633
+ : SessionManager.create(
634
+ admitted.policy.cwd,
635
+ admitted.sessionDir,
636
+ workflow ? { internal: true, workflow } : { internal: true },
637
+ );
638
+ activeSessionManager = sessionManager;
639
+ if (workflow) sessionManager.markSessionInternal(workflow);
640
+ const settingsManager = SettingsManager.create(admitted.policy.cwd, getAgentDir());
641
+ const agentPrompt = admitted.spec.agent.systemPrompt?.trim();
642
+ const resourceLoader = new DefaultResourceLoader({
643
+ cwd: admitted.policy.cwd,
644
+ agentDir: getAgentDir(),
645
+ settingsManager,
646
+ ...(agentPrompt && admitted.spec.agent.systemPromptMode === "append"
647
+ ? { appendSystemPrompt: [agentPrompt] }
648
+ : agentPrompt
649
+ ? { systemPrompt: agentPrompt }
650
+ : {}),
651
+ });
652
+ await resourceLoader.reload();
653
+ const promptBehavior = createInProcessChildPromptBehavior(admitted.policy);
654
+ const created = admitted.spec.testSession
655
+ ? { session: createTestSession(sessionManager, admitted.spec) }
656
+ : await createAgentSession({
657
+ cwd: admitted.policy.cwd,
658
+ model: candidate.model ?? admitted.policy.model,
659
+ thinkingLevel: candidate.thinkingLevel ?? admitted.policy.thinkingLevel,
660
+ ...(admitted.spec.fallbackModels?.length
661
+ ? { fallbackModels: [...admitted.spec.fallbackModels] }
662
+ : {}),
663
+ tools: admitted.policy.tools ? [...admitted.policy.tools] : undefined,
664
+ excludedTools: admitted.policy.excludedTools ? [...admitted.policy.excludedTools] : undefined,
665
+ customTools: admitted.policy.customTools,
666
+ resourceLoader,
667
+ sessionManager,
668
+ settingsManager,
669
+ orchestrationContext: admitted.spec.parent?.orchestrationContext,
670
+ subagentPolicy: admitted.policy,
671
+ systemPromptTransform: promptBehavior.systemPromptTransform,
672
+ initialContextTransform: promptBehavior.initialContextTransform,
673
+ });
674
+ session = created.session;
675
+ if (session.sessionFile) this.sessionFiles.set(admitted.identity.path, session.sessionFile);
676
+ this.sessions.set(admitted.identity.path, session);
677
+ const initialModelId =
678
+ candidate.modelId ??
679
+ (candidate.model ? `${candidate.model.provider}/${candidate.model.id}` : undefined) ??
680
+ (admitted.policy.model ? `${admitted.policy.model.provider}/${admitted.policy.model.id}` : undefined);
681
+ let effectiveModelId = initialModelId;
682
+ const attemptedModels: string[] = initialModelId ? [initialModelId] : [];
683
+ const progressState: AgentProgress = {
684
+ index: 0,
685
+ agent: admitted.spec.agent.name,
686
+ status: "running",
687
+ task: admitted.spec.task,
688
+ recentTools: [],
689
+ recentOutput: [],
690
+ toolCount: 0,
691
+ tokens: 0,
692
+ durationMs: 0,
693
+ lastActivityAt: Date.now(),
694
+ };
695
+ const attemptStartedAt = Date.now();
696
+ let lastProgressEmit = 0;
697
+ const emitProgress = (force: boolean) => {
698
+ const onProgress = admitted.spec.onProgress;
699
+ if (!onProgress) return;
700
+ const now = Date.now();
701
+ if (!force && now - lastProgressEmit < 400) return;
702
+ lastProgressEmit = now;
703
+ progressState.durationMs = now - attemptStartedAt;
704
+ progressState.lastActivityAt = now;
705
+ onProgress({ ...progressState, recentTools: [...progressState.recentTools] });
706
+ };
707
+ unsubscribe = session.subscribe((event) => {
708
+ writeEvent(admitted.spec.artifactJsonlPath, event);
709
+ if (event.type === "agent_start") {
710
+ this.native.publishChildStatus(admitted.identity.path, nativeStatus("running"));
711
+ emitProgress(true);
712
+ } else if (event.type === "tool_execution_start") {
713
+ progressState.toolCount += 1;
714
+ progressState.currentTool = event.toolName;
715
+ progressState.currentToolArgs = safeArgsPreview(event.args);
716
+ progressState.currentToolStartedAt = Date.now();
717
+ emitProgress(true);
718
+ } else if (event.type === "tool_execution_end") {
719
+ if (progressState.currentTool !== undefined) {
720
+ progressState.recentTools.push({
721
+ tool: progressState.currentTool,
722
+ args: progressState.currentToolArgs ?? "",
723
+ endMs: Date.now(),
724
+ });
725
+ if (progressState.recentTools.length > 5) progressState.recentTools.shift();
726
+ }
727
+ progressState.currentTool = undefined;
728
+ progressState.currentToolArgs = undefined;
729
+ progressState.currentToolStartedAt = undefined;
730
+ emitProgress(true);
731
+ } else if (event.type === "message_end") {
732
+ const message = (event as { message?: { role?: string; usage?: { input?: number; output?: number } } })
733
+ .message;
734
+ if (message?.role === "assistant") {
735
+ progressState.turnCount = (progressState.turnCount ?? 0) + 1;
736
+ const usage = message.usage;
737
+ if (usage) progressState.tokens += (usage.input ?? 0) + (usage.output ?? 0);
738
+ }
739
+ emitProgress(false);
740
+ } else {
741
+ emitProgress(false);
742
+ }
743
+ if (event.type === "model_fallback_start") {
744
+ if (!attemptedModels.includes(event.to)) attemptedModels.push(event.to);
745
+ effectiveModelId = event.to;
746
+ }
747
+ });
748
+ if (signals.abort.aborted) await terminate("abort");
749
+ if (signals.interrupt.aborted) await terminate("interrupt");
750
+ let structuredOutput: unknown;
751
+ let prompt = admitted.spec.task;
752
+ for (let correction = 0; ; correction += 1) {
753
+ await session.prompt(prompt);
754
+ if (!admitted.spec.structuredOutput) break;
755
+ const structured = readStructuredOutput({
756
+ schema: admitted.spec.structuredOutput.schema,
757
+ schemaPath: "",
758
+ outputPath: admitted.spec.structuredOutput.outputPath,
759
+ });
760
+ if (structured.value !== undefined) {
761
+ structuredOutput = structured.value;
762
+ break;
763
+ }
764
+ const error = structured.error ?? "Structured output contract failed.";
765
+ if (correction >= STRUCTURED_OUTPUT_MAX_CORRECTIVE_PROMPTS) throw new Error(error);
766
+ prompt = formatStructuredOutputCorrectionPrompt({
767
+ originalTask: admitted.spec.task,
768
+ error,
769
+ attempt: correction + 1,
770
+ });
771
+ }
772
+ await terminating;
773
+ const stats = statsFor(session, admitted.identity.path);
774
+ const sessionFile = session.sessionFile;
775
+ const output = outputFor(session);
776
+ const status: "ok" | "error" | "interrupted" =
777
+ termination === "interrupt" ? "interrupted" : termination ? "error" : "ok";
778
+ this.native.publishChildStatus(admitted.identity.path, nativeStatus(status));
779
+ this.native.finishChildAttempt(token, nativeStatus(status));
780
+ const envelope =
781
+ status === "interrupted"
782
+ ? INTERRUPTED_ENVELOPE
783
+ : boundedEnvelope(output || termination || "(no output)", undefined);
784
+ if (status === "ok")
785
+ return {
786
+ status,
787
+ output,
788
+ stats,
789
+ path: admitted.identity.path,
790
+ envelope,
791
+ sessionFile,
792
+ ...(effectiveModelId === undefined ? {} : { model: effectiveModelId }),
793
+ ...(attemptedModels.length > 1 ? { attemptedModels: [...attemptedModels] } : {}),
794
+ ...(structuredOutput === undefined ? {} : { structuredOutput }),
795
+ };
796
+ return {
797
+ status,
798
+ cause: termination ?? "agent session ended without a successful completion",
799
+ stats,
800
+ path: admitted.identity.path,
801
+ envelope,
802
+ sessionFile,
803
+ ...(status === "error" && effectiveModelId !== undefined ? { model: effectiveModelId } : {}),
804
+ ...(status === "error" && attemptedModels.length > 1 ? { attemptedModels: [...attemptedModels] } : {}),
805
+ };
806
+ } catch (error) {
807
+ const stats = statsFor(session, admitted.identity.path);
808
+ const cause = error instanceof Error ? error.message : String(error);
809
+ const status: "interrupted" | "error" = termination === "interrupt" ? "interrupted" : "error";
810
+ this.native.publishChildStatus(admitted.identity.path, nativeStatus(status));
811
+ try {
812
+ this.native.finishChildAttempt(token, nativeStatus(status));
813
+ } catch {
814
+ // Preserve the original failure when Rust already stamped termination.
815
+ }
816
+ return status === "interrupted"
817
+ ? {
818
+ status,
819
+ stats,
820
+ path: admitted.identity.path,
821
+ envelope: INTERRUPTED_ENVELOPE,
822
+ sessionFile: session?.sessionFile,
823
+ }
824
+ : {
825
+ status,
826
+ cause,
827
+ stats,
828
+ path: admitted.identity.path,
829
+ envelope: boundedEnvelope(cause),
830
+ sessionFile: session?.sessionFile,
831
+ };
832
+ } finally {
833
+ signals.abort.removeEventListener("abort", abortListener);
834
+ signals.interrupt.removeEventListener("abort", interruptListener);
835
+ try {
836
+ unsubscribe?.();
837
+ } catch {
838
+ // Listener teardown must not replace the attempt result.
839
+ }
840
+ try {
841
+ activeSessionManager?.flush();
842
+ } catch {
843
+ // Persistence teardown must not replace the attempt result.
844
+ }
845
+ try {
846
+ session?.dispose();
847
+ } catch {
848
+ // Session teardown must not replace the attempt result.
849
+ }
850
+ this.sessions.delete(admitted.identity.path);
851
+ if (this.attemptTokens.get(admitted.identity.path) === token)
852
+ this.attemptTokens.delete(admitted.identity.path);
853
+ if (this.attemptTerminators.get(admitted.identity.path) === terminate)
854
+ this.attemptTerminators.delete(admitted.identity.path);
855
+ }
856
+ }
857
+
858
+ startAttempt(admitted: AdmittedChild, candidate: ModelCandidate, signals: AttemptSignals): RunningAttempt {
859
+ const running: RunningAttempt = {
860
+ id: this.nextAttemptId++,
861
+ child: admitted,
862
+ candidate,
863
+ status: "running",
864
+ startedAt: Date.now(),
865
+ promise: Promise.resolve({
866
+ status: "error",
867
+ cause: "uninitialized",
868
+ stats: { ...EMPTY_STATS, sessionId: admitted.identity.path },
869
+ path: admitted.identity.path,
870
+ envelope: "uninitialized",
871
+ }),
872
+ };
873
+ running.promise = this.runChildAttempt(admitted, candidate, signals).then((result) => {
874
+ running.status = result.status;
875
+ this.runningAttempts.delete(running.id);
876
+ return result;
877
+ });
878
+ running.terminate = (cause) => this.terminateRunningAttempt(running, cause);
879
+ running.promise.catch(() => undefined);
880
+ this.runningAttempts.set(running.id, running);
881
+ return running;
882
+ }
883
+
884
+ continueInBackground(running: RunningAttempt, _reason: ContinuationReason): string {
885
+ if (running.status !== "running") throw new Error("continue_in_background requires a running attempt");
886
+ this.native.publishChildStatus(running.child.identity.path, nativeStatus("continued"));
887
+ running.status = "continued";
888
+ running.promise.catch(() => undefined);
889
+ return running.child.identity.path;
890
+ }
891
+
892
+ reloadColdChild(pathValue: string, message: string): AdmittedResult {
893
+ let sessionDir: string;
894
+ try {
895
+ sessionDir = sessionDirectory(this.sessionRoot, pathValue);
896
+ } catch (error) {
897
+ return {
898
+ refusal: {
899
+ kind: "invalidCwd",
900
+ reason: error instanceof Error ? error.message : String(error),
901
+ },
902
+ };
903
+ }
904
+ const identity = this.native.listChildren().find((child) => child.path === pathValue);
905
+ if (!identity)
906
+ return {
907
+ refusal: { kind: "unknownAgent", reason: "unknown child identity" },
908
+ };
909
+ const sessionFile = this.sessionFiles.get(pathValue) ?? this.findSessionFile(sessionDir);
910
+ if (!sessionFile)
911
+ return {
912
+ refusal: {
913
+ kind: "invalidCwd",
914
+ reason: "child session file is missing",
915
+ },
916
+ };
917
+ sessionDir = dirname(sessionFile);
918
+ const native = this.native.reloadColdChild(pathValue, message);
919
+ if (!native.child) return refusal(native);
920
+ const previous = this.specs.get(pathValue);
921
+ if (!previous)
922
+ return {
923
+ refusal: {
924
+ kind: "unknownAgent",
925
+ reason: "child policy is not resident",
926
+ },
927
+ };
928
+ const spec: ChildSpec = { ...previous, task: message };
929
+ this.specs.set(pathValue, spec);
930
+ return {
931
+ admitted: AdmittedChild.create({
932
+ identity: native.child,
933
+ spec,
934
+ policy: { ...this.policyFor(spec, native.child.depth) },
935
+ sessionDir,
936
+ sessionFile,
937
+ control: this,
938
+ }),
939
+ };
940
+ }
941
+
942
+ registerNestedAttempt(runId: string, running: RunningAttempt, candidate: ModelCandidate): void {
943
+ registerInProcessNestedAttempt({
944
+ runId,
945
+ path: running.child.identity.path,
946
+ status: () => running.status,
947
+ interrupt: () => this.terminateChildAttempt(running, "interrupt"),
948
+ resume: async (message): Promise<InProcessNestedResumeOutcome> => {
949
+ const admission = this.reloadColdChild(running.child.identity.path, message);
950
+ if (!admission.admitted) {
951
+ throw new Error(admission.refusal?.reason ?? "nested child cold reload was refused");
952
+ }
953
+ const neverAbort = new AbortController().signal;
954
+ const resumed = this.startAttempt(admission.admitted, candidate, {
955
+ abort: neverAbort,
956
+ interrupt: neverAbort,
957
+ });
958
+ const outcome = await resumed.promise;
959
+ return {
960
+ status: outcome.status,
961
+ path: outcome.path,
962
+ sessionFile: outcome.sessionFile,
963
+ envelope: outcome.envelope,
964
+ };
965
+ },
966
+ });
967
+ }
968
+
969
+ async terminateChildAttempt(running: RunningAttempt, cause: TerminationCauseName): Promise<void> {
970
+ if (running.status !== "running" && running.status !== "continued") return;
971
+ await running.terminate?.(cause);
972
+ await running.promise;
973
+ }
974
+
975
+ async deliverChildResult(envelope: ResultEnvelope, options?: DeliverChildResultOptions): Promise<void> {
976
+ if (this.delivered.has(envelope.path)) return;
977
+ this.delivered.add(envelope.path);
978
+ const artifactDir = options?.artifactsDir ?? envelope.artifactsDir;
979
+ const paths =
980
+ options?.artifactPaths ?? (artifactDir ? canonicalArtifactPaths(artifactDir, envelope.path) : undefined);
981
+ const bounded = boundedEnvelope(envelope.envelope, paths?.outputPath, options?.maxOutput);
982
+ const deliveredEnvelope = { ...envelope, envelope: bounded };
983
+ this.deliveredEnvelopes.set(envelope.path, deliveredEnvelope);
984
+ if (options?.artifactsDisabled) return;
985
+ if (!artifactDir || !paths)
986
+ throw new Error(`Artifact persistence paths are unresolved for child ${envelope.path}`);
987
+ ensureArtifactsDir(artifactDir);
988
+ writeArtifact(paths.outputPath, envelope.envelope);
989
+ writeMetadata(paths.metadataPath, {
990
+ ...deliveredEnvelope,
991
+ outputPath: paths.outputPath,
992
+ });
993
+ appendFileSync(join(artifactDir, "run-history.jsonl"), `${JSON.stringify(deliveredEnvelope)}\n`, "utf8");
994
+ }
995
+
996
+ getDeliveredResult(pathValue: string): ResultEnvelope | undefined {
997
+ return this.deliveredEnvelopes.get(pathValue);
998
+ }
999
+ findChild(pathValue: string): ChildIdentity | undefined {
1000
+ return this.native.listChildren().find((child) => child.path === pathValue);
1001
+ }
1002
+
1003
+ listChildren(): readonly ChildIdentity[] {
1004
+ return this.native.listChildren();
1005
+ }
1006
+
1007
+ async interruptChild(pathValue: string): Promise<boolean> {
1008
+ const running = [...this.runningAttempts.values()].find(
1009
+ (attempt) =>
1010
+ attempt.child.identity.path === pathValue &&
1011
+ (attempt.status === "running" || attempt.status === "continued"),
1012
+ );
1013
+ if (!running) return false;
1014
+ await this.terminateChildAttempt(running, "interrupt");
1015
+ return true;
1016
+ }
1017
+ async resumeChild(
1018
+ pathValue: string,
1019
+ message: string,
1020
+ candidate: ModelCandidate,
1021
+ signals: AttemptSignals = {
1022
+ abort: new AbortController().signal,
1023
+ interrupt: new AbortController().signal,
1024
+ },
1025
+ ): Promise<AttemptOutcome> {
1026
+ const admission = this.reloadColdChild(pathValue, message);
1027
+ if (!admission.admitted) {
1028
+ const reason = admission.refusal?.reason ?? "cold child reload was refused";
1029
+ return {
1030
+ status: "error",
1031
+ cause: reason,
1032
+ stats: { ...EMPTY_STATS, sessionId: pathValue },
1033
+ path: pathValue,
1034
+ envelope: boundedEnvelope(reason),
1035
+ };
1036
+ }
1037
+ const running = this.startAttempt(admission.admitted, candidate, signals);
1038
+ return running.promise;
1039
+ }
1040
+
1041
+ subscribe(pathValue: string, callback: (status: ChildStatus) => void): void {
1042
+ this.native.subscribeChildStatus(pathValue, callback);
1043
+ }
1044
+
1045
+ private policyFor(spec: ChildSpec, depth: number): ChildPolicy {
1046
+ return {
1047
+ ...resolveChildModePolicy(spec),
1048
+ cwd: spec.cwd,
1049
+ tools: spec.tools ?? spec.agent.tools,
1050
+ excludedTools: spec.excludedTools,
1051
+ mcpDirectTools: spec.mcpDirectTools ?? spec.agent.mcpDirectTools,
1052
+ skills: [...(spec.skills ?? spec.agent.skills ?? [])],
1053
+ customTools: spec.customTools,
1054
+ model: spec.model,
1055
+ thinkingLevel: spec.thinkingLevel ?? (spec.agent.thinking as ChildPolicy["thinkingLevel"]),
1056
+ intercomGroup: spec.parent?.intercomGroup,
1057
+ depth,
1058
+ };
1059
+ }
1060
+
1061
+ private async terminateRunningAttempt(running: RunningAttempt, cause: TerminationCauseName): Promise<void> {
1062
+ const terminate = this.attemptTerminators.get(running.child.identity.path);
1063
+ if (terminate) {
1064
+ await terminate(cause);
1065
+ return;
1066
+ }
1067
+ const token = running.attemptToken ?? this.attemptTokens.get(running.child.identity.path);
1068
+ if (token === undefined) {
1069
+ await running.promise;
1070
+ return;
1071
+ }
1072
+ try {
1073
+ await this.native.terminateChildAttempt(token, nativeCause(cause));
1074
+ } catch {
1075
+ // The runner's signal path owns the race with terminal completion.
1076
+ }
1077
+ }
1078
+
1079
+ private findSessionFile(sessionDir: string): string | undefined {
1080
+ try {
1081
+ const entry = readdirSync(sessionDir, { withFileTypes: true }).find(
1082
+ (candidate) => candidate.isFile() && candidate.name.endsWith(".jsonl"),
1083
+ );
1084
+ return entry ? join(sessionDir, entry.name) : undefined;
1085
+ } catch {
1086
+ return undefined;
1087
+ }
1088
+ }
1089
+ }
1090
+
1091
+ export function createSubagentControl(parent: ParentContext, sessionRoot?: string): SubagentControlRuntime {
1092
+ return new SubagentControlRuntime(parent, sessionRoot);
1093
+ }
1094
+
1095
+ export function admit_child_session(
1096
+ control: SubagentControlRuntime,
1097
+ spec: ChildSpec,
1098
+ parent: ParentContext,
1099
+ ): AdmittedResult {
1100
+ return control.admitChildSession(spec, parent);
1101
+ }
1102
+
1103
+ export function run_child_attempt(
1104
+ control: SubagentControlRuntime,
1105
+ admitted: AdmittedChild,
1106
+ candidate: ModelCandidate,
1107
+ signals: AttemptSignals,
1108
+ ): Promise<AttemptOutcome> {
1109
+ return control.runChildAttempt(admitted, candidate, signals);
1110
+ }
1111
+
1112
+ export function continue_in_background(
1113
+ control: SubagentControlRuntime,
1114
+ running: RunningAttempt,
1115
+ reason: ContinuationReason,
1116
+ ): string {
1117
+ return control.continueInBackground(running, reason);
1118
+ }
1119
+
1120
+ export function reload_cold_child(control: SubagentControlRuntime, pathValue: string, message: string): AdmittedResult {
1121
+ return control.reloadColdChild(pathValue, message);
1122
+ }
1123
+
1124
+ export function terminate_child_attempt(
1125
+ control: SubagentControlRuntime,
1126
+ running: RunningAttempt,
1127
+ cause: TerminationCauseName,
1128
+ ): Promise<void> {
1129
+ return control.terminateChildAttempt(running, cause);
1130
+ }
1131
+
1132
+ export function deliver_child_result(
1133
+ control: SubagentControlRuntime,
1134
+ envelope: ResultEnvelope,
1135
+ options?: DeliverChildResultOptions,
1136
+ ): Promise<void> {
1137
+ return control.deliverChildResult(envelope, options);
1138
+ }