@cohortapp/agent-sdk 2.17.0 → 2.18.4

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 (526) hide show
  1. package/.claude/settings.json +18 -0
  2. package/.env.example +18 -5
  3. package/README.md +1 -0
  4. package/bin/maestro.mjs +62 -0
  5. package/docs/guides/billing-console-keys.md +60 -0
  6. package/docs/guides/front-door-session.md +54 -9
  7. package/docs/guides/mac-mini.md +20 -25
  8. package/docs/guides/setup-wizard.md +1 -1
  9. package/docs/runbooks/fleet-rollout.md +156 -0
  10. package/docs/runbooks/mac-mini-bootstrap.md +12 -14
  11. package/lib/action-executor.js +19 -3
  12. package/lib/budget-guard.mjs +279 -3
  13. package/lib/channels/base-adapter.mjs +3 -1
  14. package/lib/channels/contract.mjs +2 -1
  15. package/lib/channels/inbox-item.mjs +8 -0
  16. package/lib/claude-bin.mjs +5 -6
  17. package/lib/cli/doctor-checks.mjs +141 -10
  18. package/lib/cli/global-setup-extras.mjs +5 -1
  19. package/lib/cli/inbox.mjs +100 -15
  20. package/lib/cli/seat-auth.mjs +463 -0
  21. package/lib/cli/session.mjs +80 -12
  22. package/lib/collective/capture.mjs +8 -6
  23. package/lib/collective/global-config.mjs +63 -1
  24. package/lib/collective/presence.mjs +142 -5
  25. package/lib/comms/send-gate.mjs +559 -1
  26. package/lib/diagnostics/alerts.mjs +49 -0
  27. package/lib/diagnostics/cadence-output-freshness.mjs +288 -0
  28. package/lib/engine/agents/definitions.mjs +343 -0
  29. package/lib/engine/agents/persist.mjs +275 -0
  30. package/lib/engine/agents/runtime.mjs +748 -0
  31. package/lib/engine/agents/usage.mjs +95 -0
  32. package/lib/engine/auth-status.mjs +139 -0
  33. package/lib/engine/budget.mjs +194 -0
  34. package/lib/engine/cli.mjs +1204 -0
  35. package/lib/engine/commands/index.mjs +269 -0
  36. package/lib/engine/context/budget.mjs +219 -0
  37. package/lib/engine/context/cache.mjs +125 -0
  38. package/lib/engine/context/child-env.mjs +215 -0
  39. package/lib/engine/context/compaction.mjs +342 -0
  40. package/lib/engine/context/images.mjs +90 -0
  41. package/lib/engine/context/instructions.mjs +327 -0
  42. package/lib/engine/context/lazy-instructions.mjs +169 -0
  43. package/lib/engine/context/manager.mjs +182 -0
  44. package/lib/engine/context/real-path.mjs +91 -0
  45. package/lib/engine/context/secret-values.mjs +163 -0
  46. package/lib/engine/context/settings.mjs +274 -0
  47. package/lib/engine/context/stream-input.mjs +159 -0
  48. package/lib/engine/guard.mjs +152 -0
  49. package/lib/engine/hooks.mjs +713 -0
  50. package/lib/engine/loop.mjs +560 -0
  51. package/lib/engine/mcp/client.mjs +254 -0
  52. package/lib/engine/mcp/config.mjs +301 -0
  53. package/lib/engine/mcp/http.mjs +201 -0
  54. package/lib/engine/mcp/index.mjs +146 -0
  55. package/lib/engine/mcp/jsonrpc.mjs +147 -0
  56. package/lib/engine/mcp/naming.mjs +66 -0
  57. package/lib/engine/mcp/resources.mjs +89 -0
  58. package/lib/engine/mcp/results.mjs +133 -0
  59. package/lib/engine/mcp/stdio.mjs +137 -0
  60. package/lib/engine/mcp/supervisor.mjs +116 -0
  61. package/lib/engine/messages.mjs +104 -0
  62. package/lib/engine/output/json.mjs +164 -0
  63. package/lib/engine/output/stream-json.mjs +266 -0
  64. package/lib/engine/permissions.mjs +845 -0
  65. package/lib/engine/process-identity.mjs +164 -0
  66. package/lib/engine/process-tree.mjs +551 -0
  67. package/lib/engine/prompt.mjs +60 -0
  68. package/lib/engine/session/store.mjs +299 -0
  69. package/lib/engine/session-runtime/args.mjs +97 -0
  70. package/lib/engine/session-runtime/host.mjs +143 -0
  71. package/lib/engine/session-runtime/inbox.mjs +122 -0
  72. package/lib/engine/session-runtime/notifications.mjs +129 -0
  73. package/lib/engine/session-runtime/registry.mjs +328 -0
  74. package/lib/engine/session-runtime/runner.mjs +344 -0
  75. package/lib/engine/session-runtime/socket.mjs +212 -0
  76. package/lib/engine/session-runtime/wakeup.mjs +115 -0
  77. package/lib/engine/skills/index.mjs +321 -0
  78. package/lib/engine/tools/bash-background.mjs +533 -0
  79. package/lib/engine/tools/bash.mjs +216 -0
  80. package/lib/engine/tools/edit.mjs +97 -0
  81. package/lib/engine/tools/glob.mjs +81 -0
  82. package/lib/engine/tools/grep.mjs +224 -0
  83. package/lib/engine/tools/index.mjs +84 -0
  84. package/lib/engine/tools/list-agents.mjs +32 -0
  85. package/lib/engine/tools/ls.mjs +127 -0
  86. package/lib/engine/tools/monitor.mjs +82 -0
  87. package/lib/engine/tools/notebook-edit.mjs +218 -0
  88. package/lib/engine/tools/read.mjs +103 -0
  89. package/lib/engine/tools/schedule-wakeup.mjs +45 -0
  90. package/lib/engine/tools/schema.mjs +144 -0
  91. package/lib/engine/tools/send-message.mjs +77 -0
  92. package/lib/engine/tools/session.mjs +70 -0
  93. package/lib/engine/tools/todo.mjs +144 -0
  94. package/lib/engine/tools/toolsearch.mjs +217 -0
  95. package/lib/engine/tools/walk.mjs +193 -0
  96. package/lib/engine/tools/web-switch.mjs +31 -0
  97. package/lib/engine/tools/webfetch-html.mjs +387 -0
  98. package/lib/engine/tools/webfetch-net.mjs +340 -0
  99. package/lib/engine/tools/webfetch.mjs +198 -0
  100. package/lib/engine/tools/websearch.mjs +91 -0
  101. package/lib/engine/tools/workflow.mjs +95 -0
  102. package/lib/engine/tools/write.mjs +76 -0
  103. package/lib/engine/tui/line-editor.mjs +137 -0
  104. package/lib/engine/tui/render.mjs +86 -0
  105. package/lib/engine/tui/tui.mjs +274 -0
  106. package/lib/engine/wire/anthropic-messages.mjs +263 -0
  107. package/lib/engine/wire/effort.mjs +36 -0
  108. package/lib/engine/wire/errors.mjs +496 -0
  109. package/lib/engine/wire/http.mjs +441 -0
  110. package/lib/engine/wire/index.mjs +76 -0
  111. package/lib/engine/wire/openai-chat.mjs +332 -0
  112. package/lib/engine/wire/prompt-cache.mjs +79 -0
  113. package/lib/engine/wire/search.mjs +140 -0
  114. package/lib/engine/wire/sse.mjs +114 -0
  115. package/lib/engine/wire/stall.mjs +349 -0
  116. package/lib/engine/wire/token-provider.mjs +175 -0
  117. package/lib/engine/wire/usage.mjs +192 -0
  118. package/lib/engine/workflow/host.mjs +524 -0
  119. package/lib/engine/workflow/journal.mjs +188 -0
  120. package/lib/engine/workflow/json-schema.mjs +171 -0
  121. package/lib/engine/workflow/meta.mjs +329 -0
  122. package/lib/engine/workflow/notifications.mjs +52 -0
  123. package/lib/engine/workflow/runtime.mjs +447 -0
  124. package/lib/engine/workflow/sandbox.mjs +534 -0
  125. package/lib/engine/workflow/worker.mjs +141 -0
  126. package/lib/engine/workflow/worktree.mjs +74 -0
  127. package/lib/execution/disposition.mjs +1 -1
  128. package/lib/execution/intake.mjs +10 -0
  129. package/lib/execution/surface-policy.mjs +15 -0
  130. package/lib/learning/curator.mjs +8 -6
  131. package/lib/learning/reflect.mjs +8 -6
  132. package/lib/model-router/catalog/cohort.yaml +137 -0
  133. package/lib/model-router/catalog.mjs +118 -1
  134. package/lib/model-router/failover.mjs +67 -16
  135. package/lib/model-router/llm-task.mjs +39 -3
  136. package/lib/model-router/resolve.mjs +89 -3
  137. package/lib/model-router/spawn.mjs +46 -47
  138. package/lib/model-router/taxonomy.mjs +126 -4
  139. package/lib/org/cost-sync.mjs +141 -11
  140. package/lib/org/inbound/broadcast.mjs +289 -0
  141. package/lib/org/inbound/collective.mjs +375 -0
  142. package/lib/org/inbound/directedness.mjs +96 -8
  143. package/lib/org/inbound/facts.mjs +78 -2
  144. package/lib/org/inbound/project.mjs +22 -0
  145. package/lib/org/inbound/surfaces.mjs +14 -0
  146. package/lib/org/llm-token.mjs +879 -0
  147. package/lib/org/mesh.mjs +61 -0
  148. package/lib/org/messaging.mjs +3 -1
  149. package/lib/org/protocol.checksum +1 -1
  150. package/lib/org/protocol.mjs +15 -0
  151. package/lib/org/quota.mjs +520 -0
  152. package/lib/org/tool-surface.mjs +104 -16
  153. package/lib/org/ui-parity.mjs +16 -1
  154. package/lib/org/work-ledger.mjs +37 -6
  155. package/lib/rate-guard.mjs +114 -1
  156. package/lib/resource-governor.mjs +41 -6
  157. package/lib/runtime/adapter.mjs +823 -0
  158. package/lib/runtime/child-env.mjs +191 -0
  159. package/lib/runtime/legacy-shell-guard.mjs +97 -0
  160. package/lib/runtime/seat-engine.mjs +162 -0
  161. package/lib/session/ask-ledger.mjs +271 -0
  162. package/lib/session/current-work.mjs +676 -0
  163. package/lib/session/feed-core.mjs +40 -3
  164. package/lib/session/launch-args.mjs +56 -4
  165. package/lib/session/status-summary.mjs +26 -9
  166. package/lib/session/upgrade-notice.mjs +42 -0
  167. package/lib/setup/claude-probe.mjs +117 -13
  168. package/lib/setup/enrich.mjs +13 -10
  169. package/lib/setup/sections/model.mjs +39 -13
  170. package/lib/telemetry/collect.mjs +208 -9
  171. package/lib/upgrade/ignored-drift.mjs +105 -0
  172. package/lib/voice/post-call-brief.mjs +30 -17
  173. package/package.json +13 -3
  174. package/plugins/maestro-skills/skills/board-work.md +5 -0
  175. package/plugins/maestro-skills/skills/inbound-triage.md +56 -15
  176. package/plugins/maestro-skills/skills/main-session.md +18 -7
  177. package/scripts/ci/check-tarball-fidelity.mjs +126 -2
  178. package/scripts/ci/run-tests.mjs +47 -19
  179. package/scripts/cohort-llm/api-key-helper.mjs +92 -0
  180. package/scripts/collective/hook-runner.mjs +29 -2
  181. package/scripts/continuous-monitor.sh +13 -0
  182. package/scripts/cost/track-claude-usage.mjs +15 -0
  183. package/scripts/daemon/agent-daemon.mjs +408 -20
  184. package/scripts/daemon/assurance.mjs +48 -12
  185. package/scripts/daemon/cadence-consumer.mjs +218 -68
  186. package/scripts/daemon/cadence-handlers.mjs +73 -4
  187. package/scripts/daemon/classifier.mjs +75 -26
  188. package/scripts/daemon/context-compiler.mjs +51 -37
  189. package/scripts/daemon/deliver.mjs +30 -1
  190. package/scripts/daemon/dispatcher.mjs +595 -149
  191. package/scripts/daemon/health.mjs +14 -1
  192. package/scripts/daemon/maestro-daemon.mjs +11 -0
  193. package/scripts/daemon/prompt-builder.mjs +24 -0
  194. package/scripts/daemon/responder.mjs +246 -79
  195. package/scripts/daemon/sdk-version.mjs +98 -16
  196. package/scripts/eval/probe-gateway.mjs +635 -0
  197. package/scripts/eval/replay/extract.mjs +270 -0
  198. package/scripts/eval/replay/grade.mjs +260 -0
  199. package/scripts/eval/replay/lib/config.mjs +50 -0
  200. package/scripts/eval/replay/lib/effects.mjs +65 -0
  201. package/scripts/eval/replay/lib/fixture.mjs +188 -0
  202. package/scripts/eval/replay/lib/judge.mjs +72 -0
  203. package/scripts/eval/replay/lib/redact.mjs +136 -0
  204. package/scripts/eval/replay/lib/sandbox.mjs +170 -0
  205. package/scripts/eval/replay/lib/schema-check.mjs +63 -0
  206. package/scripts/eval/replay/lib/transcript.mjs +76 -0
  207. package/scripts/eval/replay/mcp-replay-stub.mjs +101 -0
  208. package/scripts/eval/replay/report.mjs +185 -0
  209. package/scripts/eval/replay/run.mjs +404 -0
  210. package/scripts/fleet/rollout.mjs +1094 -0
  211. package/scripts/hooks/pre-send-audit.sh +36 -245
  212. package/scripts/hooks/pre-write-yaml-validate.mjs +275 -0
  213. package/scripts/hooks/validate-state-yaml.sh +190 -0
  214. package/scripts/huddle/huddle-llm.mjs +361 -0
  215. package/scripts/huddle/huddle-server.mjs +46 -121
  216. package/scripts/local-triggers/autoupdate.sh +448 -78
  217. package/scripts/local-triggers/run-trigger.sh +13 -0
  218. package/scripts/maintenance/pin-integrity.mjs +364 -0
  219. package/scripts/poll-slack-events.sh +41 -9
  220. package/scripts/poller/slack-socket-mode.mjs +28 -3
  221. package/scripts/session/supervisor.mjs +80 -13
  222. package/scripts/spawn-session.sh +13 -0
  223. package/bin/maestro.test.mjs +0 -1574
  224. package/lib/action-executor.test.mjs +0 -871
  225. package/lib/archetype.test.mjs +0 -132
  226. package/lib/assurance/plan-note.test.mjs +0 -234
  227. package/lib/assurance/room-budget.test.mjs +0 -486
  228. package/lib/assurance/tier.test.mjs +0 -174
  229. package/lib/autonomy.test.mjs +0 -66
  230. package/lib/backlog.test.mjs +0 -302
  231. package/lib/backup/policy.test.mjs +0 -305
  232. package/lib/budget-escalate.test.mjs +0 -232
  233. package/lib/budget-guard.envelope.test.mjs +0 -476
  234. package/lib/budget-guard.test.mjs +0 -427
  235. package/lib/cadence-bus-requeue.test.mjs +0 -83
  236. package/lib/cadence-bus-schedule.test.mjs +0 -194
  237. package/lib/cadence-bus.test.mjs +0 -720
  238. package/lib/cadences.test.mjs +0 -230
  239. package/lib/capability/inventory.test.mjs +0 -232
  240. package/lib/capability.test.mjs +0 -78
  241. package/lib/channels/base-adapter.test.mjs +0 -590
  242. package/lib/channels/channels.test.mjs +0 -371
  243. package/lib/channels/contract.test.mjs +0 -162
  244. package/lib/channels/inbox-item.test.mjs +0 -368
  245. package/lib/channels/orgmail/adapter.test.mjs +0 -448
  246. package/lib/channels/pairing.test.mjs +0 -270
  247. package/lib/channels/repeat-suppressor.test.mjs +0 -134
  248. package/lib/channels/slack-adapter.test.mjs +0 -212
  249. package/lib/channels/telegram-adapter.test.mjs +0 -306
  250. package/lib/channels/voice/adapter.test.mjs +0 -278
  251. package/lib/channels/whatsapp/adapter-baileys.test.mjs +0 -359
  252. package/lib/channels/whatsapp/baileys-typing.test.mjs +0 -154
  253. package/lib/charter.test.mjs +0 -89
  254. package/lib/claude-bin.test.mjs +0 -131
  255. package/lib/cli/board.test.mjs +0 -227
  256. package/lib/cli/design.test.mjs +0 -270
  257. package/lib/cli/doctor-checks.test.mjs +0 -336
  258. package/lib/cli/global-setup-extras.test.mjs +0 -462
  259. package/lib/cli/inbox.test.mjs +0 -230
  260. package/lib/cli/session-ack.test.mjs +0 -63
  261. package/lib/cli/session.test.mjs +0 -613
  262. package/lib/collective/capture.test.mjs +0 -121
  263. package/lib/collective/cards.test.mjs +0 -114
  264. package/lib/collective/config.test.mjs +0 -123
  265. package/lib/collective/global-config.test.mjs +0 -220
  266. package/lib/collective/global-skills.test.mjs +0 -126
  267. package/lib/collective/presence.test.mjs +0 -95
  268. package/lib/collective/recall.test.mjs +0 -116
  269. package/lib/collective/vendor-skills.test.mjs +0 -306
  270. package/lib/comms/send-gate.test.mjs +0 -770
  271. package/lib/comms.test.mjs +0 -41
  272. package/lib/context/budget.test.mjs +0 -252
  273. package/lib/context/history-scope.test.mjs +0 -79
  274. package/lib/cost/ledger-row.test.mjs +0 -183
  275. package/lib/design/design-md.test.mjs +0 -318
  276. package/lib/design/fixtures/DESIGN.golden.md +0 -238
  277. package/lib/design/fixtures/PRODUCT.golden.md +0 -67
  278. package/lib/design/fixtures/foundation.json +0 -133
  279. package/lib/design/refresh-gate.test.mjs +0 -144
  280. package/lib/design/write.test.mjs +0 -241
  281. package/lib/diagnostics/alerts.test.mjs +0 -318
  282. package/lib/diagnostics/backup-freshness.test.mjs +0 -185
  283. package/lib/diagnostics/counters.test.mjs +0 -206
  284. package/lib/diagnostics/events.test.mjs +0 -290
  285. package/lib/diagnostics/otel.test.mjs +0 -196
  286. package/lib/diagnostics/trace.test.mjs +0 -251
  287. package/lib/env-compat.test.mjs +0 -104
  288. package/lib/execution/disposition.test.mjs +0 -553
  289. package/lib/execution/drive.test.mjs +0 -270
  290. package/lib/execution/effects.test.mjs +0 -344
  291. package/lib/execution/intake.test.mjs +0 -389
  292. package/lib/execution/journal.test.mjs +0 -261
  293. package/lib/execution/match.test.mjs +0 -235
  294. package/lib/execution/pipeline.test.mjs +0 -392
  295. package/lib/execution/route.test.mjs +0 -186
  296. package/lib/execution/surface-policy.test.mjs +0 -162
  297. package/lib/fs-atomic.test.mjs +0 -72
  298. package/lib/fs-ownership.test.mjs +0 -158
  299. package/lib/goals/admission.test.mjs +0 -164
  300. package/lib/goals/classify.test.mjs +0 -167
  301. package/lib/goals/collaborate.test.mjs +0 -336
  302. package/lib/goals/gaps.test.mjs +0 -284
  303. package/lib/goals/loop.test.mjs +0 -845
  304. package/lib/hooks/bus.test.mjs +0 -387
  305. package/lib/identity/persona.test.mjs +0 -142
  306. package/lib/kpi-sensors.test.mjs +0 -278
  307. package/lib/kpi.test.mjs +0 -244
  308. package/lib/learning/config.test.mjs +0 -75
  309. package/lib/learning/counters.test.mjs +0 -69
  310. package/lib/learning/curator-consolidate.test.mjs +0 -238
  311. package/lib/learning/curator.test.mjs +0 -106
  312. package/lib/learning/reflect.test.mjs +0 -0
  313. package/lib/learning/session-index.test.mjs +0 -125
  314. package/lib/learning/skill-writer.test.mjs +0 -210
  315. package/lib/mandate/audit.test.mjs +0 -195
  316. package/lib/mandate/contract.test.mjs +0 -185
  317. package/lib/mandate/derive.test.mjs +0 -274
  318. package/lib/mandate/model.test.mjs +0 -164
  319. package/lib/mandate/refresh.test.mjs +0 -389
  320. package/lib/mcp/server.test.mjs +0 -426
  321. package/lib/model-router/auth-profiles.test.mjs +0 -580
  322. package/lib/model-router/catalog.test.mjs +0 -385
  323. package/lib/model-router/economics.test.mjs +0 -438
  324. package/lib/model-router/failover.test.mjs +0 -439
  325. package/lib/model-router/health.test.mjs +0 -338
  326. package/lib/model-router/integration-coverage.test.mjs +0 -831
  327. package/lib/model-router/integration.test.mjs +0 -564
  328. package/lib/model-router/ledger.test.mjs +0 -415
  329. package/lib/model-router/llm-task.test.mjs +0 -392
  330. package/lib/model-router/org-credentials.test.mjs +0 -265
  331. package/lib/model-router/pricing-refresh.test.mjs +0 -286
  332. package/lib/model-router/reconcile.test.mjs +0 -316
  333. package/lib/model-router/repair.test.mjs +0 -180
  334. package/lib/model-router/spawn.test.mjs +0 -446
  335. package/lib/model-router/taxonomy.test.mjs +0 -410
  336. package/lib/model-router.test.mjs +0 -1207
  337. package/lib/org/activity.test.mjs +0 -134
  338. package/lib/org/approvals.test.mjs +0 -216
  339. package/lib/org/awareness.test.mjs +0 -159
  340. package/lib/org/board-mine-cache.test.mjs +0 -53
  341. package/lib/org/board.test.mjs +0 -187
  342. package/lib/org/bootstrap-context.test.mjs +0 -153
  343. package/lib/org/client.test.mjs +0 -1206
  344. package/lib/org/cohort-client.test.mjs +0 -126
  345. package/lib/org/cost-sync.test.mjs +0 -153
  346. package/lib/org/doctor.test.mjs +0 -346
  347. package/lib/org/engagement-ledger.test.mjs +0 -112
  348. package/lib/org/engagement.test.mjs +0 -739
  349. package/lib/org/handoff.test.mjs +0 -269
  350. package/lib/org/inbound/directedness.test.mjs +0 -668
  351. package/lib/org/inbound/facts.test.mjs +0 -471
  352. package/lib/org/inbound/hydrate.test.mjs +0 -908
  353. package/lib/org/inbound/index.test.mjs +0 -429
  354. package/lib/org/inbound/project.test.mjs +0 -287
  355. package/lib/org/integration-tools.test.mjs +0 -160
  356. package/lib/org/keys.test.mjs +0 -92
  357. package/lib/org/knowledge.test.mjs +0 -326
  358. package/lib/org/leases.test.mjs +0 -235
  359. package/lib/org/mesh-directives.test.mjs +0 -110
  360. package/lib/org/mesh-integration.test.mjs +0 -127
  361. package/lib/org/mesh.test.mjs +0 -400
  362. package/lib/org/messaging.test.mjs +0 -471
  363. package/lib/org/param-contract.test.mjs +0 -477
  364. package/lib/org/policy.test.mjs +0 -237
  365. package/lib/org/protocol.checksum.test.mjs +0 -90
  366. package/lib/org/protocol.test.mjs +0 -323
  367. package/lib/org/push.test.mjs +0 -792
  368. package/lib/org/registry.test.mjs +0 -100
  369. package/lib/org/resource-tools.test.mjs +0 -361
  370. package/lib/org/tool-access.test.mjs +0 -144
  371. package/lib/org/tool-surface-integration.test.mjs +0 -120
  372. package/lib/org/tool-surface.test.mjs +0 -1268
  373. package/lib/org/typing.test.mjs +0 -291
  374. package/lib/org/ui-parity.test.mjs +0 -560
  375. package/lib/org/verify.test.mjs +0 -194
  376. package/lib/org/work-ledger.test.mjs +0 -273
  377. package/lib/plan/adoption-e2e.test.mjs +0 -366
  378. package/lib/plan/budget-enforcement.test.mjs +0 -400
  379. package/lib/plan/compile.test.mjs +0 -382
  380. package/lib/plan/emit.test.mjs +0 -269
  381. package/lib/plan/explain.test.mjs +0 -188
  382. package/lib/prompts/parallelism.test.mjs +0 -177
  383. package/lib/rag/rag.test.mjs +0 -505
  384. package/lib/rate-guard.test.mjs +0 -272
  385. package/lib/reactive-gate.test.mjs +0 -57
  386. package/lib/render.test.mjs +0 -68
  387. package/lib/resource-governor.test.mjs +0 -488
  388. package/lib/scheduling/dynamic-jobs.test.mjs +0 -344
  389. package/lib/scheduling/jitter.test.mjs +0 -140
  390. package/lib/secrets/broker.test.mjs +0 -280
  391. package/lib/secrets/providers.test.mjs +0 -274
  392. package/lib/security/audit-engine.test.mjs +0 -424
  393. package/lib/security/coerce-args.test.mjs +0 -281
  394. package/lib/security/dangerous-tools.test.mjs +0 -68
  395. package/lib/security/external-content.test.mjs +0 -84
  396. package/lib/security/redact.test.mjs +0 -441
  397. package/lib/security/secret-equal.test.mjs +0 -55
  398. package/lib/session/config.test.mjs +0 -92
  399. package/lib/session/feed-core.test.mjs +0 -198
  400. package/lib/session/first-run.test.mjs +0 -121
  401. package/lib/session/frontdoor.test.mjs +0 -205
  402. package/lib/session/handoffs.test.mjs +0 -183
  403. package/lib/session/identity.test.mjs +0 -180
  404. package/lib/session/inbox-claims.test.mjs +0 -286
  405. package/lib/session/launch-args.test.mjs +0 -157
  406. package/lib/session/liveness.test.mjs +0 -100
  407. package/lib/session/status-summary.test.mjs +0 -118
  408. package/lib/session-permissions.test.mjs +0 -120
  409. package/lib/setup/claude-probe.test.mjs +0 -187
  410. package/lib/setup/completeness.test.mjs +0 -110
  411. package/lib/setup/context-pack.test.mjs +0 -89
  412. package/lib/setup/enrich.test.mjs +0 -115
  413. package/lib/setup/enroll-from-cohort.test.mjs +0 -300
  414. package/lib/setup/integration.test.mjs +0 -162
  415. package/lib/setup/io.test.mjs +0 -77
  416. package/lib/setup/runner.test.mjs +0 -132
  417. package/lib/setup/sections/identity.test.mjs +0 -234
  418. package/lib/setup/sections/inventory.test.mjs +0 -198
  419. package/lib/setup/sections/learning.test.mjs +0 -81
  420. package/lib/setup/sections/mandate.test.mjs +0 -388
  421. package/lib/setup/sections/messaging.test.mjs +0 -127
  422. package/lib/setup/sections/model.test.mjs +0 -240
  423. package/lib/setup/sections/org.test.mjs +0 -346
  424. package/lib/setup/sections/orgmail.test.mjs +0 -118
  425. package/lib/setup/sections/recovery.test.mjs +0 -98
  426. package/lib/setup/sections/subagents.test.mjs +0 -429
  427. package/lib/setup/sections/verify.test.mjs +0 -175
  428. package/lib/setup/sot.test.mjs +0 -81
  429. package/lib/setup/state.test.mjs +0 -115
  430. package/lib/singleton.test.mjs +0 -151
  431. package/lib/subagents/cli.test.mjs +0 -389
  432. package/lib/subagents/client.test.mjs +0 -309
  433. package/lib/subagents/gap.test.mjs +0 -234
  434. package/lib/subagents/lock.test.mjs +0 -248
  435. package/lib/subagents/manifest.test.mjs +0 -175
  436. package/lib/subagents/refs.test.mjs +0 -204
  437. package/lib/subagents/resolve.test.mjs +0 -422
  438. package/lib/subagents/schema.test.mjs +0 -328
  439. package/lib/telemetry/alerts.test.mjs +0 -109
  440. package/lib/telemetry/collect.test.mjs +0 -1274
  441. package/lib/tool-definitions-integration.test.mjs +0 -83
  442. package/lib/tool-definitions.test.mjs +0 -437
  443. package/lib/upgrade/global-refresh.test.mjs +0 -65
  444. package/lib/upgrade/launchd-reconcile.test.mjs +0 -272
  445. package/lib/upgrade/post-steps.test.mjs +0 -200
  446. package/lib/upgrade/verify.test.mjs +0 -164
  447. package/lib/util/fetch-timeout.test.mjs +0 -202
  448. package/lib/util/reconnect.test.mjs +0 -369
  449. package/lib/util/unhandled.test.mjs +0 -216
  450. package/lib/voice/outbound.test.mjs +0 -69
  451. package/lib/voice/session-rotation.test.mjs +0 -114
  452. package/lib/voice/stt.test.mjs +0 -226
  453. package/lib/voice/voice.test.mjs +0 -990
  454. package/scripts/cadence/enqueue-cadence-tick.test.mjs +0 -187
  455. package/scripts/ci/check-docs-accuracy.test.mjs +0 -409
  456. package/scripts/ci/check-durable-write-seam.test.mjs +0 -90
  457. package/scripts/ci/check-no-build-artifacts.test.mjs +0 -71
  458. package/scripts/ci/check-no-residual-identity.test.mjs +0 -202
  459. package/scripts/ci/check-skill-packs.test.mjs +0 -495
  460. package/scripts/ci/check-subagent-frontmatter.test.mjs +0 -124
  461. package/scripts/ci/check.test.mjs +0 -194
  462. package/scripts/ci/conformance-org-api.test.mjs +0 -425
  463. package/scripts/cloud-relay/voice/relay-identity.test.mjs +0 -96
  464. package/scripts/collective/hook-runner.test.mjs +0 -173
  465. package/scripts/cost/fleet-digest.test.mjs +0 -207
  466. package/scripts/cost/track-claude-usage-pricing.test.mjs +0 -183
  467. package/scripts/cost/track-claude-usage.test.mjs +0 -148
  468. package/scripts/daemon/agent-daemon-board-mine.test.mjs +0 -96
  469. package/scripts/daemon/agent-daemon-design.test.mjs +0 -238
  470. package/scripts/daemon/agent-daemon-frontdoor.test.mjs +0 -60
  471. package/scripts/daemon/agent-daemon.test.mjs +0 -995
  472. package/scripts/daemon/assurance-e2e.test.mjs +0 -613
  473. package/scripts/daemon/assurance.test.mjs +0 -1791
  474. package/scripts/daemon/board-mirror.test.mjs +0 -165
  475. package/scripts/daemon/cadence-consumer-frontdoor.test.mjs +0 -393
  476. package/scripts/daemon/cadence-consumer-governance.test.mjs +0 -276
  477. package/scripts/daemon/cadence-consumer.test.mjs +0 -776
  478. package/scripts/daemon/cadence-handlers.test.mjs +0 -837
  479. package/scripts/daemon/classifier-identity.test.mjs +0 -137
  480. package/scripts/daemon/classifier.test.mjs +0 -266
  481. package/scripts/daemon/classify-kind.test.mjs +0 -40
  482. package/scripts/daemon/context-compiler.test.mjs +0 -406
  483. package/scripts/daemon/deliver.test.mjs +0 -564
  484. package/scripts/daemon/dispatcher-cooldown.test.mjs +0 -122
  485. package/scripts/daemon/dispatcher-governance.test.mjs +0 -1013
  486. package/scripts/daemon/dispatcher-resume.test.mjs +0 -166
  487. package/scripts/daemon/dispatcher-session-continuity.test.mjs +0 -365
  488. package/scripts/daemon/execution-ladder.test.mjs +0 -470
  489. package/scripts/daemon/goal-steward-cadence.test.mjs +0 -312
  490. package/scripts/daemon/inbox-deferral-session.test.mjs +0 -49
  491. package/scripts/daemon/inbox-deferral.test.mjs +0 -336
  492. package/scripts/daemon/inbox-wake.test.mjs +0 -199
  493. package/scripts/daemon/integration.test.mjs +0 -149
  494. package/scripts/daemon/lib/self-echo.test.mjs +0 -153
  495. package/scripts/daemon/lib/session-router.test.mjs +0 -554
  496. package/scripts/daemon/prompt-builder-preamble.test.mjs +0 -210
  497. package/scripts/daemon/prompt-builder.test.mjs +0 -556
  498. package/scripts/daemon/responder-cost.test.mjs +0 -68
  499. package/scripts/daemon/responder-history.test.mjs +0 -221
  500. package/scripts/daemon/sdk-version.test.mjs +0 -31
  501. package/scripts/daemon/session-lock.test.mjs +0 -252
  502. package/scripts/daemon/session-outcomes.test.mjs +0 -533
  503. package/scripts/daemon/typing-registry.test.mjs +0 -102
  504. package/scripts/hooks/pre-send-audit.test.mjs +0 -354
  505. package/scripts/huddle/huddle-prompt.test.mjs +0 -176
  506. package/scripts/local-triggers/autoupdate.test.mjs +0 -518
  507. package/scripts/local-triggers/generate-plists.test.mjs +0 -456
  508. package/scripts/media-generation/brand-clause.test.mjs +0 -135
  509. package/scripts/org/send-orgmail.first-contact.test.mjs +0 -102
  510. package/scripts/poller/inbox-privilege-injection.test.mjs +0 -167
  511. package/scripts/poller/inbox-scan-poller.test.mjs +0 -295
  512. package/scripts/poller/lib/cloud-relay-dedup.test.mjs +0 -133
  513. package/scripts/poller/slack-socket-mode.test.mjs +0 -805
  514. package/scripts/poller-launchd/install.test.mjs +0 -243
  515. package/scripts/restore-from-backup.test.mjs +0 -181
  516. package/scripts/session/feed.test.mjs +0 -196
  517. package/scripts/session/supervisor-sh.test.mjs +0 -218
  518. package/scripts/session/supervisor.test.mjs +0 -482
  519. package/scripts/setup/configure-macos.test.mjs +0 -306
  520. package/scripts/setup/gen-subagent-manifest.test.mjs +0 -124
  521. package/scripts/setup/generate-agent-package-json.test.mjs +0 -143
  522. package/scripts/setup/generate-capability.test.mjs +0 -134
  523. package/scripts/setup/init-agent.test.mjs +0 -370
  524. package/scripts/setup/init-skill-marketplace.test.mjs +0 -193
  525. package/scripts/vendor/sync-skill-packs.test.mjs +0 -103
  526. package/scripts/watchdog/memory-watchdog.test.mjs +0 -64
@@ -0,0 +1,748 @@
1
+ /**
2
+ * lib/engine/agents/runtime.mjs — running subagents: the Task tool (also named
3
+ * Agent), TaskOutput, TaskStop, background tasks and their notifications.
4
+ *
5
+ * A subagent run is a child loop (loop.mjs) with:
6
+ * · a fresh context: its first message is the Task prompt; its system prompt
7
+ * is the engine's base prompt, the run's instructions, and the definition's
8
+ * own prompt (see `subagentPrompt`);
9
+ * · the parent's rules, gates and hooks: its guard is built from the same
10
+ * settings, in the STRICTER of the parent's mode and the definition's
11
+ * `permissionMode` — a child can never loosen its parent's posture;
12
+ * · tools: the definition's list intersected with the parent's tools, minus
13
+ * its disallowed tools. A tool with `forChild()` (the session Bash, TodoWrite)
14
+ * is given as the child's own copy;
15
+ * · a tier from its `model` through the tier map (definitions.mjs);
16
+ * · depth: the main run is depth 0 and a child may start children only while
17
+ * depth + 1 < maxDepth (default 1: subagents cannot start subagents);
18
+ * · attribution headers on every model request: x-cohort-session-id (the main
19
+ * session), x-cohort-agent-id (this child), x-cohort-parent-agent-id (the
20
+ * main session id, or the agent that started it);
21
+ * · SubagentStop hooks consulted before it ends (a blocking hook gives the
22
+ * child another turn, exactly as Stop does for the main run); the input adds
23
+ * agent_id, agent_type and agent_transcript_path;
24
+ * · the permission check repeated on the RESOLVED definition name before a
25
+ * child starts (`checkSubagent`), so a padded or unqualified spelling cannot
26
+ * pass a `Task(<type>)` deny or ask rule the guard matched by spelling;
27
+ * · a sidechain transcript: <sessions>/subagents/<session id>/<agent id>.jsonl,
28
+ * whose header record carries isSidechain, parentSessionId, parentAgentId,
29
+ * parentToolUseId and agentType;
30
+ * · a usage record (tier, usage, cost micros, request ids) handed to the owner
31
+ * for the parent's result (usage.mjs); permission refusals are added to the
32
+ * parent's `permission_denials`. Model calls the child's run makes outside
33
+ * its turns (a compaction summary, WebFetch, WebSearch) reach the child
34
+ * loop's `recordModelCall`, so they are in that record too;
35
+ * · its own context (`prepareChild`, W3 integration): a context manager for
36
+ * the child's tier and tools, so a long child compacts on its own, and its
37
+ * own deferred-tool state when its MCP tools are deferred;
38
+ *
39
+ * Foreground: the Task call returns the child's final text. Background
40
+ * (`run_in_background`): the call returns a task id at once; when the child
41
+ * ends, a notification is queued and the loop delivers it as the next user
42
+ * message (`drainNotifications`). A loop whose model has finished while a
43
+ * background task is still running waits for it (`awaitNotifications`), within
44
+ * the run's time limit, so a headless run never ends with unreported work.
45
+ * TaskOutput {task_id, block?, timeout?} reads a task (or a background shell,
46
+ * or a workflow run) early; TaskStop {task_id} stops one. A task read or
47
+ * stopped by the model is not notified again.
48
+ *
49
+ * Surviving a restart (W4-E2, conformance row 23). Every task the Task tool
50
+ * starts keeps its state in <agent id>.state.json beside its transcript
51
+ * (persist.mjs): definition snapshot, prompt, the mode it was clamped to,
52
+ * status, final text, whether it was reported. A runtime created for a
53
+ * continued session reads its session's states back: finished tasks answer
54
+ * TaskOutput as before, and a task recorded as running by a process that is
55
+ * gone is `interrupted` — reported once as a notification and by TaskOutput,
56
+ * with the way to continue it. `Task {resume: <agent id>, prompt}` continues a
57
+ * task that is not running from its sidechain transcript: the same agent id and
58
+ * transcript, the definition of that name (or its snapshot), in the stricter of
59
+ * the mode it ran in and the mode this run would give it, with this run's tools
60
+ * at most. Workflow `agent()` children are not tasks: a workflow recovers from
61
+ * its journal (`resumeFromRunId`).
62
+ *
63
+ * Tool descriptions are original (see docs/engine/clean-room.md).
64
+ *
65
+ * @module lib/engine/agents/runtime
66
+ */
67
+
68
+ import { randomBytes } from "node:crypto";
69
+ import path from "node:path";
70
+ import { runLoop } from "../loop.mjs";
71
+ import { userText } from "../messages.mjs";
72
+ import { classifyStop } from "../output/json.mjs";
73
+ import { appendMessage, appendResult, openSession, sessionPath } from "../session/store.mjs";
74
+ import { AGENT_TOOL_NAMES, DEFAULT_TIER_MAP, childToolNames, clampPermissionMode, findAgent, resolveAgentModel } from "./definitions.mjs";
75
+ import { recordFromOutcome } from "./usage.mjs";
76
+ import { transcriptMessage } from "../context/images.mjs";
77
+ import { resumeHistory } from "../context/compaction.mjs";
78
+ import { TASK_STATE_VERSION, definitionForResume, isAgentId, readTaskState, readTaskStates, reconcileTaskState, subagentDir, taskOwnerState, writeTaskState } from "./persist.mjs";
79
+ import { PROCESS_NONCE, processAlive, processStartToken } from "../process-identity.mjs";
80
+
81
+ /** Agent ids with a child running in THIS process, whichever runtime started it (a host may rebuild one). */
82
+ const PROCESS_LIVE_TASKS = new Set();
83
+
84
+ export const DEFAULT_MAX_AGENT_DEPTH = 1;
85
+ export const TASK_OUTPUT_DEFAULT_TIMEOUT_MS = 30_000;
86
+ export const TASK_OUTPUT_MAX_TIMEOUT_MS = 600_000;
87
+ const STOP_WAIT_MS = 10_000;
88
+ const LISTED_DESCRIPTION_CHARS = 300;
89
+ const WORKFLOW_RUN_ID_RE = /^wf_[0-9a-f]{12}$/;
90
+
91
+ /**
92
+ * The part of a child's system prompt that makes it a subagent.
93
+ * Authorship: written for Cohort Engine by the Cohort team, 2026-09-14.
94
+ * @param {import('./definitions.mjs').AgentDefinition} def
95
+ */
96
+ export function subagentPrompt(def) {
97
+ const own = def.prompt || def.description;
98
+ return `You are running as the "${def.name}" subagent: another agent handed you one task. It cannot see your working, only your final message, so end with a complete report of what you found or did.\n\n${own}`;
99
+ }
100
+
101
+ /** @param {import('./definitions.mjs').AgentDefinition[]} agents */
102
+ export function formatAgentListing(agents) {
103
+ return agents
104
+ .map((a) => {
105
+ const d = a.description.length <= LISTED_DESCRIPTION_CHARS ? a.description : `${a.description.slice(0, LISTED_DESCRIPTION_CHARS - 1)}…`;
106
+ return `- ${a.name}: ${d}`;
107
+ })
108
+ .join("\n");
109
+ }
110
+
111
+ export function newAgentId() {
112
+ return `agent-${randomBytes(8).toString("hex")}`;
113
+ }
114
+
115
+ /**
116
+ * How the model continues an interrupted or finished task (pure).
117
+ * @param {string} id
118
+ */
119
+ export function resumeHint(id) {
120
+ return `Task {"resume":"${id}","description":"<a short label>","prompt":"<what to do next>"} continues it from its transcript.`;
121
+ }
122
+
123
+ /**
124
+ * @typedef {Object} WorkflowRuns the session's workflow runtime, as TaskOutput and TaskStop use it
125
+ * @property {(runId:string) => boolean} has
126
+ * @property {(runId:string, o:{block?:boolean, timeoutMs?:number, signal?:AbortSignal}) => Promise<any>} status
127
+ * @property {(runId:string) => Promise<{ok:true, content:string}|{ok:false, error:string}>} stop
128
+ * @property {(runId:string) => ({ok:true, content:string, running:boolean}|{ok:false, error:string})} [diskStatus]
129
+ */
130
+
131
+ /**
132
+ * @typedef {Object} AgentRuntimeDeps
133
+ * @property {import('./definitions.mjs').AgentDefinition[]} agents
134
+ * @property {string} sessionId
135
+ * @property {string} sessionsDir
136
+ * @property {string} cwd
137
+ * @property {string} wire
138
+ * @property {string} parentModel the tier of the run that owns this runtime
139
+ * @property {string} parentMode
140
+ * @property {string} parentAgentId the session id at depth 0
141
+ * @property {number} [depth]
142
+ * @property {number} [maxDepth]
143
+ * @property {(o:{model:string, headers:Record<string,string>, parentToolUseId:string|null}) => (turn:any)=>Promise<any>} createCaller
144
+ * @property {(o:{mode:string, agentId?:string, cwd?:string}) => any} createGuard
145
+ * @property {(o:{def:import('./definitions.mjs').AgentDefinition, toolNames:string[], notes?:string|null}) => string} buildSystem
146
+ * @property {(o:{headers:Record<string,string>, agentId:string}) => any} createToolContext the child's tool context; web tools attribute their requests with `headers`
147
+ * @property {(o:{def:import('./definitions.mjs').AgentDefinition, model:string, tools:any[], headers:Record<string,string>, agentId:string, parentToolUseId:string|null, onCompact:(message:any)=>void}) => ({tools:any[], sent:any[], notes:string|null, context:any}|null)} [prepareChild]
148
+ * the child's tool list (ToolSearch bound to its own state), the tools whose definitions are sent,
149
+ * system-prompt notes and the loop's `context`; null = the tools as given, no context manager
150
+ * @property {any} [hooks]
151
+ * @property {(message:any, o:{parentToolUseId:string|null, agentId:string}) => void} [onMessage]
152
+ * @property {(denials:any[], meta:{turn:number|null}) => void} [onDenials] `turn`: `currentTurn()` when the child started
153
+ * @property {() => number} [currentTurn]
154
+ * @property {(o:{toolName:string, input:any, mode:string}) => {behavior:'allow'|'deny'|'ask', reason:string}} [checkSubagent]
155
+ * the owner's permission evaluation for a Task call (permissions.mjs evaluatePermission)
156
+ * @property {(record:any) => void} [onRecord]
157
+ * @property {() => void} [onNotify] a background task's notification was queued (W4-A1: wakes an idle `cohort session`)
158
+ * @property {(line:string) => void} [log]
159
+ * @property {Record<string,string>} [tierMap]
160
+ * @property {number} [maxTurns]
161
+ * @property {import('../budget.mjs').BudgetMeter|null} [budget] the run's spend cap, shared with every child (W4-E1)
162
+ * @property {number} [wallClockMs]
163
+ * @property {() => number} [now]
164
+ * @property {{has:(id:string)=>boolean, read:(id:string, f?:string)=>any, wait:(id:string, ms:number, s?:AbortSignal)=>Promise<void>, kill:(id:string)=>Promise<any>}|null} [shells]
165
+ * @property {number} [pid] this process (default process.pid): recorded in task states
166
+ * @property {string} [nonce] this process's nonce (default process-identity.mjs PROCESS_NONCE)
167
+ * @property {(pid:number) => boolean} [isProcessAlive] whether a restored running task's process still lives
168
+ * @property {(pid:number) => string|null} [processStartToken] the OS start time of a PID, to catch a reused PID
169
+ * @property {boolean} [restore] read the session's task states back (default: at depth 0)
170
+ */
171
+
172
+ /**
173
+ * @typedef {Object} BackgroundTask
174
+ * @property {string} id
175
+ * @property {string} agentType
176
+ * @property {string} description
177
+ * @property {'running'|'completed'|'failed'|'stopped'|'interrupted'} status
178
+ * @property {string} text
179
+ * @property {string|null} error
180
+ * @property {boolean} notified
181
+ * @property {boolean} stopRequested
182
+ * @property {AbortController} controller
183
+ * @property {Promise<void>} done
184
+ * @property {boolean} background
185
+ * @property {import('./persist.mjs').TaskState|null} state what is kept on disk
186
+ * @property {boolean} elsewhere restored as running in another live process: not this run's to wait for or stop
187
+ */
188
+
189
+ /** @param {AgentRuntimeDeps} d */
190
+ export function createAgentRuntime(d) {
191
+ const depth = d.depth ?? 0;
192
+ const maxDepth = d.maxDepth ?? DEFAULT_MAX_AGENT_DEPTH;
193
+ const now = d.now ?? Date.now;
194
+ const pid = d.pid ?? process.pid;
195
+ const nonce = d.nonce ?? PROCESS_NONCE;
196
+ const isAlive = d.isProcessAlive ?? processAlive;
197
+ const startTokenOf = d.processStartToken ?? processStartToken;
198
+ /** How a recorded owner is judged (W4-E2 review: PID, nonce and start time, not the PID alone). */
199
+ const ownerCheck = { self: { pid, nonce }, probe: { isAlive, startToken: startTokenOf }, heldHere: (/** @type string */ agentId) => PROCESS_LIVE_TASKS.has(agentId) };
200
+ const iso = () => new Date(now()).toISOString();
201
+ const canSpawn = depth + 1 < maxDepth;
202
+ /** @type {Map<string, BackgroundTask>} */
203
+ const tasks = new Map();
204
+ /** Agent ids with a child running in this process (foreground or background). */
205
+ const live = new Set();
206
+ /** @type {Array<{taskId:string, text:string}>} */
207
+ const queue = [];
208
+ /** @type {Set<() => void>} */
209
+ const waiters = new Set();
210
+ /** Usage records of finished children (this runtime's and nested ones). */
211
+ const records = [];
212
+ /** @type {() => any[]} */
213
+ let parentTools = () => [];
214
+ /** @type {WorkflowRuns|null} */
215
+ let workflowRuns = null;
216
+ let saveFailureLogged = false;
217
+
218
+ const addRecord = (/** @type any */ r) => {
219
+ records.push(r);
220
+ d.onRecord?.(r);
221
+ };
222
+ const wake = () => {
223
+ for (const w of [...waiters]) w();
224
+ waiters.clear();
225
+ };
226
+ /** A task this run is responsible for waiting on and stopping: one it holds a live child for. @param {BackgroundTask} t */
227
+ const runningHere = (t) => t.status === "running" && !t.elsewhere && live.has(t.id);
228
+ /** @param {string} id */
229
+ const hold = (id) => {
230
+ live.add(id);
231
+ PROCESS_LIVE_TASKS.add(id);
232
+ };
233
+ /** @param {string} id */
234
+ const release = (id) => {
235
+ live.delete(id);
236
+ PROCESS_LIVE_TASKS.delete(id);
237
+ };
238
+ /** Where a task restored as running is running, for the model. @param {BackgroundTask} t */
239
+ const elsewhereText = (t) => (t.state?.pid === pid ? "in another session runtime of this engine process" : `in another engine process (pid ${t.state?.pid})`);
240
+
241
+ /** Write a task's state (W4-E2). A failed write is logged once and costs only recoverability. @param {BackgroundTask} t */
242
+ const save = (t) => {
243
+ if (!t.state) return;
244
+ t.state = { ...t.state, status: t.status, text: t.text, error: t.error, notified: t.notified, endedAt: t.status === "running" ? null : (t.state.endedAt ?? iso()) };
245
+ if (!writeTaskState(d.sessionsDir, t.state) && !saveFailureLogged) {
246
+ saveFailureLogged = true;
247
+ d.log?.(`subagent ${t.agentType}: its state could not be written under ${subagentDir(d.sessionsDir, d.sessionId)}; it will not be recoverable after a restart`);
248
+ }
249
+ };
250
+
251
+ /**
252
+ * W4-A2 (workflow children): `extraTools` are added to the child's tools after the definition's
253
+ * filter (a StructuredOutput tool); `effort` overrides the run's --effort for this child; `cwd`
254
+ * runs the child in another directory (a git worktree) — its tools, guard, prompt and Bash.
255
+ * W4-E2: `resume` continues the child's existing transcript (its history is the child's
256
+ * conversation so far, then `prompt`); `ceilingMode` clamps the child's mode further.
257
+ * @param {{def:import('./definitions.mjs').AgentDefinition, prompt:string, description:string, parentToolUseId:string|null, signal?:AbortSignal, agentId?:string, background:boolean, extraTools?:any[], effort?:string|null, cwd?:string|null, resume?:boolean, ceilingMode?:string|null}} p
258
+ * @returns {Promise<{ok:boolean, agentId:string, text:string, error:string|null, stopKind:string, record:any}>}
259
+ */
260
+ async function runChild({ def, prompt, description, parentToolUseId, signal, agentId = newAgentId(), background, extraTools = [], effort = null, cwd = null, resume = false, ceilingMode = null }) {
261
+ const at = cwd ? { cwd } : {};
262
+ const turn = d.currentTurn ? d.currentTurn() : null;
263
+ const { model, note } = resolveAgentModel(def.model, d.parentModel, d.tierMap ?? DEFAULT_TIER_MAP);
264
+ if (note) d.log?.(`subagent ${def.name}: ${note}`);
265
+ const clamped = clampPermissionMode(d.parentMode, def.permissionMode);
266
+ const mode = ceilingMode ? clampPermissionMode(clamped, ceilingMode) : clamped;
267
+
268
+ const opened = openSession({
269
+ dir: subagentDir(d.sessionsDir, d.sessionId),
270
+ sessionId: agentId,
271
+ mode: resume ? "resume" : "new",
272
+ meta: { cwd: cwd ?? d.cwd, model, wire: d.wire, isSidechain: true, parentSessionId: d.sessionId, parentAgentId: d.parentAgentId, parentToolUseId, agentType: def.name, description, background },
273
+ now,
274
+ });
275
+ if (!opened.ok && resume) {
276
+ return { ok: false, agentId, text: "", error: `its transcript could not be reopened (${opened.error.message})`, stopKind: "error", record: null };
277
+ }
278
+ const transcript = opened.ok ? opened.session : null;
279
+ if (!opened.ok) d.log?.(`subagent ${def.name}: transcript not written (${opened.error.message})`);
280
+ const history = resume && transcript ? resumeHistory(transcript.messages) : [];
281
+
282
+ const available = parentTools();
283
+ const { names, missing } = childToolNames(available.map((t) => t.name), def, { canSpawn });
284
+ if (missing.length) d.log?.(`subagent ${def.name}: not available to it (the parent does not have them): ${missing.join(", ")}`);
285
+
286
+ const nested = canSpawn && names.some((n) => AGENT_TOOL_NAMES.includes(n))
287
+ ? createAgentRuntime({ ...d, depth: depth + 1, parentModel: model, parentMode: mode, parentAgentId: agentId, onRecord: addRecord, onDenials: d.onDenials, restore: false })
288
+ : null;
289
+ const nestedTools = new Map((nested?.tools() ?? []).map((t) => [t.name, t]));
290
+ const granted = names
291
+ .map((n) => {
292
+ if (AGENT_TOOL_NAMES.includes(n)) return nestedTools.get(n);
293
+ const t = available.find((x) => x.name === n);
294
+ return typeof t?.forChild === "function" ? t.forChild(cwd ? { cwd } : undefined) : t;
295
+ })
296
+ .filter(Boolean);
297
+ granted.push(...extraTools);
298
+
299
+ const headers = { "x-cohort-surface": "engine", "x-cohort-session-id": d.sessionId, "x-cohort-agent-id": agentId, "x-cohort-parent-agent-id": d.parentAgentId };
300
+ const callModel = d.createCaller({ model, headers, parentToolUseId, ...(effort ? { effort } : {}) });
301
+ const guard = d.createGuard({ mode, agentId, ...at });
302
+
303
+ const first = userText(prompt);
304
+ if (transcript) appendMessage(transcript, first, now);
305
+
306
+ // The child's own context: tools (with its own ToolSearch state), the definitions it is sent, a context manager.
307
+ const prepared = d.prepareChild
308
+ ? d.prepareChild({ def, model, tools: granted, headers, agentId, parentToolUseId, onCompact: (m) => transcript && appendMessage(transcript, transcriptMessage(m), now) })
309
+ : null;
310
+ const tools = prepared?.tools ?? granted;
311
+ nested?.setParentTools(() => tools);
312
+ const system = d.buildSystem({ def, toolNames: (prepared?.sent ?? tools).map((t) => t.name), notes: prepared?.notes ?? null, ...at });
313
+
314
+ let outcome;
315
+ try {
316
+ outcome = await runLoop({
317
+ callModel,
318
+ system,
319
+ messages: [...history, first],
320
+ tools,
321
+ toolContext: d.createToolContext({ headers, agentId, ...at }),
322
+ maxTurns: def.maxTurns ?? d.maxTurns,
323
+ wallClockMs: d.wallClockMs,
324
+ signal,
325
+ now,
326
+ guard,
327
+ onMessage: (m) => {
328
+ if (transcript) appendMessage(transcript, transcriptMessage(m), now);
329
+ d.onMessage?.(m, { parentToolUseId, agentId });
330
+ },
331
+ onStop: d.hooks ? ({ stopHookActive }) => d.hooks.stop({ stopHookActive, event: "SubagentStop", fields: { agent_id: agentId, agent_type: def.name, agent_transcript_path: transcript?.path ?? null } }) : undefined,
332
+ drainNotifications: nested?.drainNotifications,
333
+ awaitNotifications: nested?.awaitNotifications,
334
+ context: prepared?.context ?? null,
335
+ // W4-E1 (row 24): the run's --max-budget-usd meter, shared by every child.
336
+ budget: d.budget ?? null,
337
+ });
338
+ } finally {
339
+ if (nested) await nested.stopAll();
340
+ if (guard.denials.length) d.onDenials?.(guard.denials, { turn });
341
+ }
342
+
343
+ const record = { ...recordFromOutcome(outcome, model), agentId, agentType: def.name, background, stop: outcome.stop.kind, numTurns: outcome.numTurns };
344
+ addRecord(record);
345
+ const c = classifyStop(outcome.stop);
346
+ if (transcript) {
347
+ appendResult(
348
+ transcript,
349
+ { type: "result", subtype: c.subtype, is_error: c.isError, result: c.isError ? c.message : outcome.resultText, agent_id: agentId, num_turns: outcome.numTurns, cohort: { modelTier: record.tier, costMicros: record.costMicros, usage: record.usage, requestIds: record.requestIds, stop: outcome.stop.kind } },
350
+ now,
351
+ );
352
+ }
353
+ return { ok: !c.isError, agentId, text: outcome.resultText, error: c.message, stopKind: outcome.stop.kind, record };
354
+ }
355
+
356
+ /** @param {BackgroundTask} t */
357
+ const notificationText = (t) => {
358
+ const kind = t.background ? "Background task" : "Task";
359
+ if (t.status === "completed") return `[${kind} ${t.id} finished] The ${t.agentType} subagent working on "${t.description}" completed. Its report:\n\n${t.text || "(no final message)"}`;
360
+ if (t.status === "interrupted") return `[${kind} ${t.id} interrupted] The ${t.agentType} subagent working on "${t.description}" did not finish: ${t.error ?? "the engine stopped"}.${t.text ? `\n\nIts last message:\n${t.text}` : ""}\n\n${resumeHint(t.id)}`;
361
+ return `[${kind} ${t.id} ended] The ${t.agentType} subagent working on "${t.description}" did not finish: ${t.error ?? "unknown error"}${t.text ? `\n\nIts last message:\n${t.text}` : ""}`;
362
+ };
363
+
364
+ /** @param {BackgroundTask} t */
365
+ const consume = (t) => {
366
+ t.notified = true;
367
+ const i = queue.findIndex((q) => q.taskId === t.id);
368
+ if (i !== -1) queue.splice(i, 1);
369
+ save(t);
370
+ };
371
+
372
+ // ── W4-E2: the session's earlier tasks, read back when the session is continued ──
373
+ if (d.restore ?? depth === 0) {
374
+ const { states, errors } = readTaskStates(d.sessionsDir, d.sessionId);
375
+ for (const e of errors) d.log?.(`subagent state ${e}`);
376
+ for (const s0 of states) {
377
+ if (s0.parentAgentId !== d.parentAgentId) continue;
378
+ const { state, changed, elsewhere } = reconcileTaskState(s0, { ...ownerCheck, now: iso() });
379
+ /** @type {BackgroundTask} */
380
+ const t = { id: state.agentId, agentType: state.agentType, description: state.description, status: state.status, text: state.text ?? "", error: state.error ?? null, notified: state.notified === true, stopRequested: false, controller: new AbortController(), done: Promise.resolve(), background: state.background === true, state, elsewhere };
381
+ tasks.set(t.id, t);
382
+ if (changed) save(t);
383
+ // Reported once: a task cut off by the restart, and a background report the old process never delivered.
384
+ if (!t.notified && (t.status === "interrupted" || (t.background && (t.status === "completed" || t.status === "failed")))) queue.push({ taskId: t.id, text: notificationText(t) });
385
+ }
386
+ }
387
+
388
+ const taskSchema = {
389
+ type: "object",
390
+ properties: {
391
+ description: { type: "string", description: "A short label for the task (three to five words)." },
392
+ prompt: { type: "string", description: "The complete task for the subagent, with everything it needs to know. With `resume`, what it should do next." },
393
+ subagent_type: { type: "string", description: "Which subagent to use, from the listed types. Not needed with `resume`." },
394
+ run_in_background: { type: "boolean", description: "Return a task id at once; the report arrives in a later message." },
395
+ resume: { type: "string", description: "The id of an earlier task of this session that is not running (finished, stopped or interrupted): continue that subagent from its transcript." },
396
+ },
397
+ required: ["description", "prompt"],
398
+ };
399
+
400
+ /**
401
+ * Check a `resume` request (W4-E2).
402
+ * @param {string} id @param {any} input
403
+ * @returns {{ok:true, def:import('./definitions.mjs').AgentDefinition, state:import('./persist.mjs').TaskState}|{ok:false, error:string}}
404
+ */
405
+ const prepareResume = (id, input) => {
406
+ if (!isAgentId(id)) return { ok: false, error: `resume takes the id of an earlier task (agent- and 16 hex digits), got "${id}".` };
407
+ if (live.has(id)) return { ok: false, error: `Task ${id} is still running; read it with TaskOutput or stop it with TaskStop before resuming it.` };
408
+ const state = readTaskState(d.sessionsDir, d.sessionId, id);
409
+ if (!state || state.parentAgentId !== d.parentAgentId) return { ok: false, error: `There is no task ${id} of this session to resume.` };
410
+ if (state.status === "running") {
411
+ const where = taskOwnerState(state, ownerCheck);
412
+ if (where === "elsewhere") return { ok: false, error: `Task ${id} is still running in another engine process (pid ${state.pid}); it cannot be resumed from here.` };
413
+ if (where === "here-elsewhere") return { ok: false, error: `Task ${id} is still running in another session runtime of this engine process; it cannot be resumed from here.` };
414
+ }
415
+ const def = definitionForResume(state, d.agents, findAgent);
416
+ if (typeof input.subagent_type === "string" && input.subagent_type.trim() !== "") {
417
+ const f = findAgent(d.agents, input.subagent_type);
418
+ const asked = f.ok ? f.agent.name : input.subagent_type.trim();
419
+ if (asked !== state.agentType) return { ok: false, error: `Task ${id} is a ${state.agentType} subagent; resume it without subagent_type, or with that type (got "${input.subagent_type}").` };
420
+ }
421
+ return { ok: true, def, state };
422
+ };
423
+
424
+ /** @param {'Task'|'Agent'} name */
425
+ const startTool = (name) => ({
426
+ name,
427
+ // Starting a subagent changes nothing by itself: every call the subagent
428
+ // makes passes the same guard. Several subagents in one reply run together.
429
+ readOnly: true,
430
+ description:
431
+ name === "Task"
432
+ ? "Hand a self-contained piece of work to a subagent. It runs with a fresh context and its own tools, and returns one final report, which becomes this call's result. " +
433
+ "It cannot see this conversation: put everything it needs in `prompt`. Several Task calls in one reply run at the same time. " +
434
+ "With `run_in_background` the call returns a task id at once and the report arrives in a later message (TaskOutput reads it earlier, TaskStop stops it). " +
435
+ "`resume` with an earlier task's id continues that subagent, with its conversation so far, on a new `prompt`.\n\n" +
436
+ `Subagent types:\n${formatAgentListing(d.agents)}`
437
+ : "The same tool as Task under another name: start a subagent with a fresh context and receive its final report. See Task for the subagent types.",
438
+ input_schema: taskSchema,
439
+ /** @param {{description:string, prompt:string, subagent_type?:string, run_in_background?:boolean, resume?:string}} input @param {any} ctx */
440
+ async run(input, ctx) {
441
+ /** @type {import('./definitions.mjs').AgentDefinition} */
442
+ let def;
443
+ /** @type {import('./persist.mjs').TaskState|null} */
444
+ let resumeState = null;
445
+ if (typeof input.resume === "string" && input.resume.trim() !== "") {
446
+ const r = prepareResume(input.resume.trim(), input);
447
+ if (!r.ok) return r;
448
+ def = r.def;
449
+ resumeState = r.state;
450
+ } else {
451
+ if (typeof input.subagent_type !== "string" || input.subagent_type.trim() === "") return { ok: false, error: "subagent_type is required (or resume, with the id of an earlier task)." };
452
+ const found = findAgent(d.agents, input.subagent_type);
453
+ if (!found.ok) return { ok: false, error: found.error };
454
+ def = found.agent;
455
+ }
456
+ const parentToolUseId = ctx?.toolUseId ?? null;
457
+ if (d.checkSubagent) {
458
+ const v = d.checkSubagent({ toolName: name, input: { ...input, subagent_type: def.name }, mode: d.parentMode });
459
+ if (v.behavior !== "allow") {
460
+ d.onDenials?.([{ tool_name: name, tool_use_id: parentToolUseId ?? "", tool_input: input }], { turn: d.currentTurn ? d.currentTurn() : null });
461
+ return { ok: false, error: `Permission denied: the ${def.name} subagent ${v.behavior === "deny" ? "is refused" : "needs approval, which this run cannot ask for"} (${v.reason}).` };
462
+ }
463
+ }
464
+
465
+ const background = Boolean(input.run_in_background);
466
+ const id = resumeState?.agentId ?? newAgentId();
467
+ const description = typeof input.description === "string" && input.description.trim() !== "" ? input.description : (resumeState?.description ?? def.name);
468
+ // The mode this child runs in; a resumed child never runs looser than it did before.
469
+ const clamped = clampPermissionMode(d.parentMode, def.permissionMode);
470
+ const ceiling = resumeState ? clampPermissionMode(clamped, resumeState.mode) : clamped;
471
+ const { model } = resolveAgentModel(def.model, d.parentModel, d.tierMap ?? DEFAULT_TIER_MAP);
472
+ const controller = new AbortController();
473
+ /** @type {BackgroundTask} */
474
+ const task = {
475
+ id,
476
+ agentType: def.name,
477
+ description,
478
+ status: "running",
479
+ text: "",
480
+ error: null,
481
+ notified: false,
482
+ stopRequested: false,
483
+ controller,
484
+ done: Promise.resolve(),
485
+ background,
486
+ elsewhere: false,
487
+ state: {
488
+ version: TASK_STATE_VERSION,
489
+ agentId: id,
490
+ sessionId: d.sessionId,
491
+ parentAgentId: d.parentAgentId,
492
+ parentToolUseId,
493
+ agentType: def.name,
494
+ definition: { ...def },
495
+ description,
496
+ prompt: resumeState?.prompt ?? input.prompt,
497
+ lastPrompt: resumeState ? input.prompt : null,
498
+ mode: ceiling,
499
+ model,
500
+ background,
501
+ transcriptPath: sessionPath(subagentDir(d.sessionsDir, d.sessionId), id),
502
+ status: "running",
503
+ text: "",
504
+ error: null,
505
+ notified: false,
506
+ pid,
507
+ nonce,
508
+ startToken: startTokenOf(pid),
509
+ resumes: resumeState ? (resumeState.resumes ?? 0) + 1 : 0,
510
+ startedAt: resumeState?.startedAt ?? iso(),
511
+ endedAt: null,
512
+ },
513
+ };
514
+ // A resumed task's restored entry is replaced by the running one (its notification, if queued, is dropped).
515
+ const earlier = tasks.get(id);
516
+ if (earlier) {
517
+ const i = queue.findIndex((q) => q.taskId === id);
518
+ if (i !== -1) queue.splice(i, 1);
519
+ tasks.delete(id);
520
+ }
521
+ hold(id);
522
+ save(task);
523
+ const child = { def, prompt: input.prompt, description, parentToolUseId, agentId: id, background, resume: Boolean(resumeState), ceilingMode: ceiling };
524
+ const resumedNote = resumeState ? ` (resumed from its transcript)` : "";
525
+
526
+ if (!background) {
527
+ /** @type {Awaited<ReturnType<typeof runChild>>} */
528
+ let r;
529
+ try {
530
+ r = await runChild({ ...child, signal: ctx?.signal });
531
+ } catch (e) {
532
+ task.status = "failed";
533
+ task.error = `the subagent failed unexpectedly (${e instanceof Error ? e.message : e})`;
534
+ throw e;
535
+ } finally {
536
+ release(id);
537
+ if (task.status === "running") {
538
+ task.text = r?.text ?? "";
539
+ task.error = r?.ok ? null : (r?.error ?? null);
540
+ task.status = r?.ok ? "completed" : ctx?.signal?.aborted ? "stopped" : "failed";
541
+ }
542
+ task.notified = true; // its result is this call's result
543
+ tasks.set(id, task);
544
+ save(task);
545
+ }
546
+ if (r.ok) return { ok: true, content: `${resumeState ? `[Task ${id}${resumedNote}]\n\n` : ""}${r.text || "(the subagent finished without a final message)"}` };
547
+ return { ok: false, error: `The ${def.name} subagent${resumeState ? ` (task ${id}, resumed)` : ""} did not finish: ${r.error}${r.text ? `\n\nIts last message:\n${r.text}` : ""}` };
548
+ }
549
+
550
+ const outer = /** @type {AbortSignal|undefined} */ (ctx?.signal);
551
+ const onOuter = () => controller.abort();
552
+ if (outer?.aborted) controller.abort();
553
+ else outer?.addEventListener("abort", onOuter, { once: true });
554
+ tasks.set(id, task);
555
+ task.done = runChild({ ...child, signal: controller.signal })
556
+ .then(
557
+ (r) => {
558
+ task.text = r.text;
559
+ task.error = r.ok ? null : r.error;
560
+ task.status = task.stopRequested ? "stopped" : r.ok ? "completed" : "failed";
561
+ },
562
+ (e) => {
563
+ task.error = `the subagent failed unexpectedly (${e instanceof Error ? e.message : e})`;
564
+ task.status = task.stopRequested ? "stopped" : "failed";
565
+ },
566
+ )
567
+ .finally(() => {
568
+ outer?.removeEventListener("abort", onOuter);
569
+ release(id);
570
+ if (!task.notified && task.status !== "stopped") {
571
+ queue.push({ taskId: id, text: notificationText(task) });
572
+ d.onNotify?.();
573
+ }
574
+ save(task);
575
+ wake();
576
+ });
577
+ return {
578
+ ok: true,
579
+ content: `${resumeState ? "Resumed" : "Started"} background task ${id}: the ${def.name} subagent is working on "${description}"${resumedNote}. Its report arrives in a later message when it finishes. TaskOutput {"task_id":"${id}"} waits for it or reads it early; TaskStop {"task_id":"${id}"} stops it.`,
580
+ };
581
+ },
582
+ });
583
+
584
+ /** TaskOutput for a workflow run id (W4-E2). @param {string} runId @param {boolean} block @param {number} ms @param {AbortSignal} [signal] */
585
+ const workflowOutput = async (runId, block, ms, signal) => {
586
+ if (workflowRuns?.has(runId)) {
587
+ const r = await workflowRuns.status(runId, { block, timeoutMs: ms, signal });
588
+ if (!r.ok) return r;
589
+ const v = r.view;
590
+ return { ok: true, content: `Workflow ${runId} ("${v.name}") is ${v.status}.\n\n${JSON.stringify(v, null, 2)}${r.result !== null ? `\n\nResult:\n${r.result}` : ""}` };
591
+ }
592
+ const disk = workflowRuns?.diskStatus?.(runId);
593
+ return disk && disk.ok ? { ok: true, content: disk.content } : null;
594
+ };
595
+
596
+ const outputTool = {
597
+ name: "TaskOutput",
598
+ readOnly: true,
599
+ description:
600
+ "Read the result of a background task: a subagent started with run_in_background, a background shell, or a workflow run (its wf_ id). With `block` (the default) it waits until the task ends or `timeout` milliseconds pass " +
601
+ `(default ${TASK_OUTPUT_DEFAULT_TIMEOUT_MS}). A task read here is not reported again later. A subagent task that was interrupted by an engine restart says how to resume it.`,
602
+ input_schema: {
603
+ type: "object",
604
+ properties: {
605
+ task_id: { type: "string", description: "The task id (or shell id, or workflow run id)." },
606
+ block: { type: "boolean", description: "Wait for the task to end. Default true." },
607
+ timeout: { type: "integer", minimum: 0, maximum: TASK_OUTPUT_MAX_TIMEOUT_MS, description: "How long to wait, in milliseconds." },
608
+ },
609
+ required: ["task_id"],
610
+ },
611
+ /** @param {{task_id:string, block?:boolean, timeout?:number}} input @param {any} ctx */
612
+ async run(input, ctx) {
613
+ const ms = Math.min(input.timeout ?? TASK_OUTPUT_DEFAULT_TIMEOUT_MS, TASK_OUTPUT_MAX_TIMEOUT_MS);
614
+ const block = input.block !== false;
615
+ const task = tasks.get(input.task_id);
616
+ if (!task) {
617
+ if (d.shells?.has(input.task_id)) {
618
+ if (block) await d.shells.wait(input.task_id, ms, ctx?.signal);
619
+ const r = d.shells.read(input.task_id);
620
+ return r.ok ? { ok: true, content: r.content } : r;
621
+ }
622
+ if (WORKFLOW_RUN_ID_RE.test(input.task_id)) {
623
+ const w = await workflowOutput(input.task_id, block, ms, ctx?.signal);
624
+ if (w) return w;
625
+ }
626
+ return { ok: false, error: `There is no background task "${input.task_id}". Known: ${[...tasks.keys()].join(", ") || "none"}.` };
627
+ }
628
+ if (task.elsewhere && task.status === "running") return { ok: true, content: `Task ${task.id} (${task.agentType}, "${task.description}") is running ${elsewhereText(task)}; it can be read here once it has ended.` };
629
+ if (task.status === "running" && block) await waitFor(task.done, ms, ctx?.signal);
630
+ if (task.status === "running") return { ok: true, content: `Task ${task.id} (${task.agentType}, "${task.description}") is still running.` };
631
+ consume(task);
632
+ if (task.status === "completed") return { ok: true, content: `Task ${task.id} completed. Its report:\n\n${task.text || "(no final message)"}` };
633
+ if (task.status === "interrupted") return { ok: true, content: `Task ${task.id} (${task.agentType}, "${task.description}") was interrupted: ${task.error ?? "the engine stopped"}.${task.text ? `\n\nIts last message:\n${task.text}` : ""}\n\n${resumeHint(task.id)}` };
634
+ return { ok: true, content: `Task ${task.id} ${task.status === "stopped" ? "was stopped" : "did not finish"}: ${task.error ?? "stopped on request"}${task.text ? `\n\nIts last message:\n${task.text}` : ""}` };
635
+ },
636
+ };
637
+
638
+ const stopTool = {
639
+ name: "TaskStop",
640
+ // Stops only work this run started.
641
+ readOnly: true,
642
+ description: "Stop a background task: a subagent started with run_in_background, a background shell, or a workflow run (its wf_ id: the run's script and its agents are stopped).",
643
+ input_schema: {
644
+ type: "object",
645
+ properties: { task_id: { type: "string", description: "The task id (or shell id, or workflow run id)." } },
646
+ required: ["task_id"],
647
+ },
648
+ /** @param {{task_id:string}} input */
649
+ async run(input) {
650
+ const task = tasks.get(input.task_id);
651
+ if (!task) {
652
+ if (d.shells?.has(input.task_id)) return d.shells.kill(input.task_id);
653
+ if (WORKFLOW_RUN_ID_RE.test(input.task_id) && workflowRuns) {
654
+ if (workflowRuns.has(input.task_id)) return workflowRuns.stop(input.task_id);
655
+ const disk = workflowRuns.diskStatus?.(input.task_id);
656
+ if (disk?.ok) return disk.running ? { ok: false, error: `Workflow ${input.task_id} is not running in this process, so it cannot be stopped from here.\n\n${disk.content}` } : { ok: true, content: `Workflow ${input.task_id} is not running; nothing to stop.\n\n${disk.content}` };
657
+ }
658
+ return { ok: false, error: `There is no background task "${input.task_id}".` };
659
+ }
660
+ if (task.elsewhere && task.status === "running") return { ok: false, error: `Task ${task.id} is running ${elsewhereText(task)}; it cannot be stopped from here.` };
661
+ if (task.status !== "running") {
662
+ consume(task);
663
+ return { ok: true, content: `Task ${task.id} had already ${task.status === "completed" ? "completed" : task.status === "interrupted" ? `been interrupted; ${resumeHint(task.id)}` : "ended"}.` };
664
+ }
665
+ task.stopRequested = true;
666
+ consume(task);
667
+ task.controller.abort();
668
+ await waitFor(task.done, STOP_WAIT_MS);
669
+ return { ok: true, content: `Stopped task ${task.id} (${task.agentType}, "${task.description}").` };
670
+ },
671
+ };
672
+
673
+ const drainNotifications = () => {
674
+ const out = queue.splice(0);
675
+ for (const n of out) {
676
+ const t = tasks.get(n.taskId);
677
+ if (t) {
678
+ t.notified = true;
679
+ save(t);
680
+ }
681
+ }
682
+ return out.map((n) => n.text);
683
+ };
684
+
685
+ return {
686
+ /** The subagent tools, in a stable order. */
687
+ tools: () => [startTool("Task"), startTool("Agent"), outputTool, stopTool],
688
+ /** @param {() => any[]} fn the tool list the owning run exposes */
689
+ setParentTools(fn) {
690
+ parentTools = fn;
691
+ },
692
+ /** W4-E2: TaskOutput and TaskStop also take this session's workflow run ids. @param {WorkflowRuns|null} w */
693
+ setWorkflowRuns(w) {
694
+ workflowRuns = w;
695
+ },
696
+ drainNotifications,
697
+ /**
698
+ * Notifications ready now; otherwise, while a background task runs, wait
699
+ * for the next one to end (or the signal). Empty when nothing is running.
700
+ * @param {AbortSignal} [signal]
701
+ * @returns {Promise<string[]>}
702
+ */
703
+ async awaitNotifications(signal) {
704
+ if (queue.length) return drainNotifications();
705
+ if (![...tasks.values()].some(runningHere)) return [];
706
+ await new Promise((resolve) => {
707
+ const done = () => {
708
+ signal?.removeEventListener("abort", done);
709
+ waiters.delete(done);
710
+ resolve(undefined);
711
+ };
712
+ waiters.add(done);
713
+ if (signal?.aborted) done();
714
+ else signal?.addEventListener("abort", done, { once: true });
715
+ });
716
+ return drainNotifications();
717
+ },
718
+ hasRunning: () => [...tasks.values()].some(runningHere),
719
+ /** Stop every running background task and wait for it to end. */
720
+ async stopAll() {
721
+ const running = [...tasks.values()].filter(runningHere);
722
+ for (const t of running) {
723
+ t.stopRequested = true;
724
+ t.notified = true;
725
+ t.controller.abort();
726
+ }
727
+ await Promise.all(running.map((t) => waitFor(t.done, STOP_WAIT_MS)));
728
+ },
729
+ /** Usage records of the children that finished since the last call. */
730
+ drainRecords: () => records.splice(0),
731
+ runChild,
732
+ };
733
+ }
734
+
735
+ /** @param {Promise<unknown>} p @param {number} ms @param {AbortSignal} [signal] */
736
+ function waitFor(p, ms, signal) {
737
+ return new Promise((resolve) => {
738
+ const t = setTimeout(done, ms);
739
+ const onAbort = () => done();
740
+ signal?.addEventListener("abort", onAbort, { once: true });
741
+ p.then(done, done);
742
+ function done() {
743
+ clearTimeout(t);
744
+ signal?.removeEventListener("abort", onAbort);
745
+ resolve(undefined);
746
+ }
747
+ });
748
+ }