@cohortapp/agent-sdk 2.16.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 (529) hide show
  1. package/.claude/settings.json +18 -0
  2. package/.env.example +23 -7
  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/poller-daemon-setup.md +4 -1
  9. package/docs/guides/setup-wizard.md +1 -1
  10. package/docs/runbooks/fleet-rollout.md +156 -0
  11. package/docs/runbooks/mac-mini-bootstrap.md +12 -14
  12. package/lib/action-executor.js +19 -3
  13. package/lib/budget-guard.mjs +279 -3
  14. package/lib/channels/base-adapter.mjs +3 -1
  15. package/lib/channels/contract.mjs +2 -1
  16. package/lib/channels/inbox-item.mjs +8 -0
  17. package/lib/claude-bin.mjs +5 -6
  18. package/lib/cli/doctor-checks.mjs +141 -10
  19. package/lib/cli/global-setup-extras.mjs +5 -1
  20. package/lib/cli/inbox.mjs +100 -15
  21. package/lib/cli/seat-auth.mjs +463 -0
  22. package/lib/cli/session.mjs +80 -12
  23. package/lib/collective/capture.mjs +8 -6
  24. package/lib/collective/global-config.mjs +63 -1
  25. package/lib/collective/presence.mjs +142 -5
  26. package/lib/comms/send-gate.mjs +559 -1
  27. package/lib/context/budget.mjs +327 -0
  28. package/lib/context/history-scope.mjs +138 -0
  29. package/lib/diagnostics/alerts.mjs +49 -0
  30. package/lib/diagnostics/cadence-output-freshness.mjs +288 -0
  31. package/lib/engine/agents/definitions.mjs +343 -0
  32. package/lib/engine/agents/persist.mjs +275 -0
  33. package/lib/engine/agents/runtime.mjs +748 -0
  34. package/lib/engine/agents/usage.mjs +95 -0
  35. package/lib/engine/auth-status.mjs +139 -0
  36. package/lib/engine/budget.mjs +194 -0
  37. package/lib/engine/cli.mjs +1204 -0
  38. package/lib/engine/commands/index.mjs +269 -0
  39. package/lib/engine/context/budget.mjs +219 -0
  40. package/lib/engine/context/cache.mjs +125 -0
  41. package/lib/engine/context/child-env.mjs +215 -0
  42. package/lib/engine/context/compaction.mjs +342 -0
  43. package/lib/engine/context/images.mjs +90 -0
  44. package/lib/engine/context/instructions.mjs +327 -0
  45. package/lib/engine/context/lazy-instructions.mjs +169 -0
  46. package/lib/engine/context/manager.mjs +182 -0
  47. package/lib/engine/context/real-path.mjs +91 -0
  48. package/lib/engine/context/secret-values.mjs +163 -0
  49. package/lib/engine/context/settings.mjs +274 -0
  50. package/lib/engine/context/stream-input.mjs +159 -0
  51. package/lib/engine/guard.mjs +152 -0
  52. package/lib/engine/hooks.mjs +713 -0
  53. package/lib/engine/loop.mjs +560 -0
  54. package/lib/engine/mcp/client.mjs +254 -0
  55. package/lib/engine/mcp/config.mjs +301 -0
  56. package/lib/engine/mcp/http.mjs +201 -0
  57. package/lib/engine/mcp/index.mjs +146 -0
  58. package/lib/engine/mcp/jsonrpc.mjs +147 -0
  59. package/lib/engine/mcp/naming.mjs +66 -0
  60. package/lib/engine/mcp/resources.mjs +89 -0
  61. package/lib/engine/mcp/results.mjs +133 -0
  62. package/lib/engine/mcp/stdio.mjs +137 -0
  63. package/lib/engine/mcp/supervisor.mjs +116 -0
  64. package/lib/engine/messages.mjs +104 -0
  65. package/lib/engine/output/json.mjs +164 -0
  66. package/lib/engine/output/stream-json.mjs +266 -0
  67. package/lib/engine/permissions.mjs +845 -0
  68. package/lib/engine/process-identity.mjs +164 -0
  69. package/lib/engine/process-tree.mjs +551 -0
  70. package/lib/engine/prompt.mjs +60 -0
  71. package/lib/engine/session/store.mjs +299 -0
  72. package/lib/engine/session-runtime/args.mjs +97 -0
  73. package/lib/engine/session-runtime/host.mjs +143 -0
  74. package/lib/engine/session-runtime/inbox.mjs +122 -0
  75. package/lib/engine/session-runtime/notifications.mjs +129 -0
  76. package/lib/engine/session-runtime/registry.mjs +328 -0
  77. package/lib/engine/session-runtime/runner.mjs +344 -0
  78. package/lib/engine/session-runtime/socket.mjs +212 -0
  79. package/lib/engine/session-runtime/wakeup.mjs +115 -0
  80. package/lib/engine/skills/index.mjs +321 -0
  81. package/lib/engine/tools/bash-background.mjs +533 -0
  82. package/lib/engine/tools/bash.mjs +216 -0
  83. package/lib/engine/tools/edit.mjs +97 -0
  84. package/lib/engine/tools/glob.mjs +81 -0
  85. package/lib/engine/tools/grep.mjs +224 -0
  86. package/lib/engine/tools/index.mjs +84 -0
  87. package/lib/engine/tools/list-agents.mjs +32 -0
  88. package/lib/engine/tools/ls.mjs +127 -0
  89. package/lib/engine/tools/monitor.mjs +82 -0
  90. package/lib/engine/tools/notebook-edit.mjs +218 -0
  91. package/lib/engine/tools/read.mjs +103 -0
  92. package/lib/engine/tools/schedule-wakeup.mjs +45 -0
  93. package/lib/engine/tools/schema.mjs +144 -0
  94. package/lib/engine/tools/send-message.mjs +77 -0
  95. package/lib/engine/tools/session.mjs +70 -0
  96. package/lib/engine/tools/todo.mjs +144 -0
  97. package/lib/engine/tools/toolsearch.mjs +217 -0
  98. package/lib/engine/tools/walk.mjs +193 -0
  99. package/lib/engine/tools/web-switch.mjs +31 -0
  100. package/lib/engine/tools/webfetch-html.mjs +387 -0
  101. package/lib/engine/tools/webfetch-net.mjs +340 -0
  102. package/lib/engine/tools/webfetch.mjs +198 -0
  103. package/lib/engine/tools/websearch.mjs +91 -0
  104. package/lib/engine/tools/workflow.mjs +95 -0
  105. package/lib/engine/tools/write.mjs +76 -0
  106. package/lib/engine/tui/line-editor.mjs +137 -0
  107. package/lib/engine/tui/render.mjs +86 -0
  108. package/lib/engine/tui/tui.mjs +274 -0
  109. package/lib/engine/wire/anthropic-messages.mjs +263 -0
  110. package/lib/engine/wire/effort.mjs +36 -0
  111. package/lib/engine/wire/errors.mjs +496 -0
  112. package/lib/engine/wire/http.mjs +441 -0
  113. package/lib/engine/wire/index.mjs +76 -0
  114. package/lib/engine/wire/openai-chat.mjs +332 -0
  115. package/lib/engine/wire/prompt-cache.mjs +79 -0
  116. package/lib/engine/wire/search.mjs +140 -0
  117. package/lib/engine/wire/sse.mjs +114 -0
  118. package/lib/engine/wire/stall.mjs +349 -0
  119. package/lib/engine/wire/token-provider.mjs +175 -0
  120. package/lib/engine/wire/usage.mjs +192 -0
  121. package/lib/engine/workflow/host.mjs +524 -0
  122. package/lib/engine/workflow/journal.mjs +188 -0
  123. package/lib/engine/workflow/json-schema.mjs +171 -0
  124. package/lib/engine/workflow/meta.mjs +329 -0
  125. package/lib/engine/workflow/notifications.mjs +52 -0
  126. package/lib/engine/workflow/runtime.mjs +447 -0
  127. package/lib/engine/workflow/sandbox.mjs +534 -0
  128. package/lib/engine/workflow/worker.mjs +141 -0
  129. package/lib/engine/workflow/worktree.mjs +74 -0
  130. package/lib/execution/disposition.mjs +1 -1
  131. package/lib/execution/intake.mjs +10 -0
  132. package/lib/execution/surface-policy.mjs +15 -0
  133. package/lib/learning/curator.mjs +8 -6
  134. package/lib/learning/reflect.mjs +8 -6
  135. package/lib/model-router/catalog/cohort.yaml +137 -0
  136. package/lib/model-router/catalog.mjs +118 -1
  137. package/lib/model-router/economics.mjs +9 -0
  138. package/lib/model-router/failover.mjs +67 -16
  139. package/lib/model-router/llm-task.mjs +39 -3
  140. package/lib/model-router/resolve.mjs +95 -3
  141. package/lib/model-router/spawn.mjs +46 -47
  142. package/lib/model-router/taxonomy.mjs +126 -4
  143. package/lib/org/cost-sync.mjs +141 -11
  144. package/lib/org/inbound/broadcast.mjs +289 -0
  145. package/lib/org/inbound/collective.mjs +375 -0
  146. package/lib/org/inbound/directedness.mjs +96 -8
  147. package/lib/org/inbound/facts.mjs +82 -4
  148. package/lib/org/inbound/hydrate.mjs +555 -51
  149. package/lib/org/inbound/project.mjs +22 -0
  150. package/lib/org/inbound/surfaces.mjs +14 -0
  151. package/lib/org/llm-token.mjs +879 -0
  152. package/lib/org/mesh.mjs +61 -0
  153. package/lib/org/messaging.mjs +3 -1
  154. package/lib/org/protocol.checksum +1 -1
  155. package/lib/org/protocol.mjs +15 -0
  156. package/lib/org/quota.mjs +520 -0
  157. package/lib/org/tool-surface.mjs +104 -16
  158. package/lib/org/ui-parity.mjs +16 -1
  159. package/lib/org/work-ledger.mjs +37 -6
  160. package/lib/rate-guard.mjs +114 -1
  161. package/lib/resource-governor.mjs +41 -6
  162. package/lib/runtime/adapter.mjs +823 -0
  163. package/lib/runtime/child-env.mjs +191 -0
  164. package/lib/runtime/legacy-shell-guard.mjs +97 -0
  165. package/lib/runtime/seat-engine.mjs +162 -0
  166. package/lib/session/ask-ledger.mjs +271 -0
  167. package/lib/session/current-work.mjs +676 -0
  168. package/lib/session/feed-core.mjs +40 -3
  169. package/lib/session/launch-args.mjs +56 -4
  170. package/lib/session/status-summary.mjs +26 -9
  171. package/lib/session/upgrade-notice.mjs +42 -0
  172. package/lib/setup/claude-probe.mjs +117 -13
  173. package/lib/setup/enrich.mjs +13 -10
  174. package/lib/setup/sections/model.mjs +39 -13
  175. package/lib/telemetry/collect.mjs +208 -9
  176. package/lib/upgrade/ignored-drift.mjs +105 -0
  177. package/lib/voice/post-call-brief.mjs +30 -17
  178. package/package.json +15 -3
  179. package/plugins/maestro-skills/skills/board-work.md +5 -0
  180. package/plugins/maestro-skills/skills/inbound-triage.md +56 -15
  181. package/plugins/maestro-skills/skills/main-session.md +18 -7
  182. package/scripts/ci/check-tarball-fidelity.mjs +126 -2
  183. package/scripts/ci/run-tests.mjs +47 -19
  184. package/scripts/cohort-llm/api-key-helper.mjs +92 -0
  185. package/scripts/collective/hook-runner.mjs +29 -2
  186. package/scripts/continuous-monitor.sh +13 -0
  187. package/scripts/cost/track-claude-usage.mjs +15 -0
  188. package/scripts/daemon/agent-daemon.mjs +408 -20
  189. package/scripts/daemon/assurance.mjs +48 -12
  190. package/scripts/daemon/cadence-consumer.mjs +218 -68
  191. package/scripts/daemon/cadence-handlers.mjs +73 -4
  192. package/scripts/daemon/classifier.mjs +75 -26
  193. package/scripts/daemon/context-compiler.mjs +104 -59
  194. package/scripts/daemon/deliver.mjs +30 -1
  195. package/scripts/daemon/dispatcher.mjs +804 -157
  196. package/scripts/daemon/health.mjs +14 -1
  197. package/scripts/daemon/lib/session-router.mjs +310 -42
  198. package/scripts/daemon/maestro-daemon.mjs +11 -0
  199. package/scripts/daemon/prompt-builder.mjs +121 -12
  200. package/scripts/daemon/responder.mjs +315 -146
  201. package/scripts/daemon/sdk-version.mjs +98 -16
  202. package/scripts/eval/probe-gateway.mjs +635 -0
  203. package/scripts/eval/replay/extract.mjs +270 -0
  204. package/scripts/eval/replay/grade.mjs +260 -0
  205. package/scripts/eval/replay/lib/config.mjs +50 -0
  206. package/scripts/eval/replay/lib/effects.mjs +65 -0
  207. package/scripts/eval/replay/lib/fixture.mjs +188 -0
  208. package/scripts/eval/replay/lib/judge.mjs +72 -0
  209. package/scripts/eval/replay/lib/redact.mjs +136 -0
  210. package/scripts/eval/replay/lib/sandbox.mjs +170 -0
  211. package/scripts/eval/replay/lib/schema-check.mjs +63 -0
  212. package/scripts/eval/replay/lib/transcript.mjs +76 -0
  213. package/scripts/eval/replay/mcp-replay-stub.mjs +101 -0
  214. package/scripts/eval/replay/report.mjs +185 -0
  215. package/scripts/eval/replay/run.mjs +404 -0
  216. package/scripts/fleet/rollout.mjs +1094 -0
  217. package/scripts/hooks/pre-send-audit.sh +36 -245
  218. package/scripts/hooks/pre-write-yaml-validate.mjs +275 -0
  219. package/scripts/hooks/validate-state-yaml.sh +190 -0
  220. package/scripts/huddle/huddle-llm.mjs +361 -0
  221. package/scripts/huddle/huddle-server.mjs +46 -121
  222. package/scripts/local-triggers/autoupdate.sh +448 -78
  223. package/scripts/local-triggers/run-trigger.sh +13 -0
  224. package/scripts/maintenance/pin-integrity.mjs +364 -0
  225. package/scripts/poll-slack-events.sh +41 -9
  226. package/scripts/poller/slack-socket-mode.mjs +28 -3
  227. package/scripts/session/supervisor.mjs +80 -13
  228. package/scripts/spawn-session.sh +13 -0
  229. package/bin/maestro.test.mjs +0 -1574
  230. package/lib/action-executor.test.mjs +0 -871
  231. package/lib/archetype.test.mjs +0 -132
  232. package/lib/assurance/plan-note.test.mjs +0 -234
  233. package/lib/assurance/room-budget.test.mjs +0 -486
  234. package/lib/assurance/tier.test.mjs +0 -174
  235. package/lib/autonomy.test.mjs +0 -66
  236. package/lib/backlog.test.mjs +0 -302
  237. package/lib/backup/policy.test.mjs +0 -305
  238. package/lib/budget-escalate.test.mjs +0 -232
  239. package/lib/budget-guard.envelope.test.mjs +0 -476
  240. package/lib/budget-guard.test.mjs +0 -427
  241. package/lib/cadence-bus-requeue.test.mjs +0 -83
  242. package/lib/cadence-bus-schedule.test.mjs +0 -194
  243. package/lib/cadence-bus.test.mjs +0 -720
  244. package/lib/cadences.test.mjs +0 -230
  245. package/lib/capability/inventory.test.mjs +0 -232
  246. package/lib/capability.test.mjs +0 -78
  247. package/lib/channels/base-adapter.test.mjs +0 -590
  248. package/lib/channels/channels.test.mjs +0 -371
  249. package/lib/channels/contract.test.mjs +0 -162
  250. package/lib/channels/inbox-item.test.mjs +0 -368
  251. package/lib/channels/orgmail/adapter.test.mjs +0 -448
  252. package/lib/channels/pairing.test.mjs +0 -270
  253. package/lib/channels/repeat-suppressor.test.mjs +0 -134
  254. package/lib/channels/slack-adapter.test.mjs +0 -212
  255. package/lib/channels/telegram-adapter.test.mjs +0 -306
  256. package/lib/channels/voice/adapter.test.mjs +0 -278
  257. package/lib/channels/whatsapp/adapter-baileys.test.mjs +0 -359
  258. package/lib/channels/whatsapp/baileys-typing.test.mjs +0 -154
  259. package/lib/charter.test.mjs +0 -89
  260. package/lib/claude-bin.test.mjs +0 -131
  261. package/lib/cli/board.test.mjs +0 -227
  262. package/lib/cli/design.test.mjs +0 -270
  263. package/lib/cli/doctor-checks.test.mjs +0 -336
  264. package/lib/cli/global-setup-extras.test.mjs +0 -462
  265. package/lib/cli/inbox.test.mjs +0 -230
  266. package/lib/cli/session-ack.test.mjs +0 -63
  267. package/lib/cli/session.test.mjs +0 -613
  268. package/lib/collective/capture.test.mjs +0 -121
  269. package/lib/collective/cards.test.mjs +0 -114
  270. package/lib/collective/config.test.mjs +0 -123
  271. package/lib/collective/global-config.test.mjs +0 -220
  272. package/lib/collective/global-skills.test.mjs +0 -126
  273. package/lib/collective/presence.test.mjs +0 -95
  274. package/lib/collective/recall.test.mjs +0 -116
  275. package/lib/collective/vendor-skills.test.mjs +0 -306
  276. package/lib/comms/send-gate.test.mjs +0 -770
  277. package/lib/comms.test.mjs +0 -41
  278. package/lib/cost/ledger-row.test.mjs +0 -183
  279. package/lib/design/design-md.test.mjs +0 -318
  280. package/lib/design/fixtures/DESIGN.golden.md +0 -238
  281. package/lib/design/fixtures/PRODUCT.golden.md +0 -67
  282. package/lib/design/fixtures/foundation.json +0 -133
  283. package/lib/design/refresh-gate.test.mjs +0 -144
  284. package/lib/design/write.test.mjs +0 -241
  285. package/lib/diagnostics/alerts.test.mjs +0 -318
  286. package/lib/diagnostics/backup-freshness.test.mjs +0 -185
  287. package/lib/diagnostics/counters.test.mjs +0 -206
  288. package/lib/diagnostics/events.test.mjs +0 -290
  289. package/lib/diagnostics/otel.test.mjs +0 -196
  290. package/lib/diagnostics/trace.test.mjs +0 -251
  291. package/lib/env-compat.test.mjs +0 -104
  292. package/lib/execution/disposition.test.mjs +0 -553
  293. package/lib/execution/drive.test.mjs +0 -270
  294. package/lib/execution/effects.test.mjs +0 -344
  295. package/lib/execution/intake.test.mjs +0 -389
  296. package/lib/execution/journal.test.mjs +0 -261
  297. package/lib/execution/match.test.mjs +0 -235
  298. package/lib/execution/pipeline.test.mjs +0 -392
  299. package/lib/execution/route.test.mjs +0 -186
  300. package/lib/execution/surface-policy.test.mjs +0 -162
  301. package/lib/fs-atomic.test.mjs +0 -72
  302. package/lib/fs-ownership.test.mjs +0 -158
  303. package/lib/goals/admission.test.mjs +0 -164
  304. package/lib/goals/classify.test.mjs +0 -167
  305. package/lib/goals/collaborate.test.mjs +0 -336
  306. package/lib/goals/gaps.test.mjs +0 -284
  307. package/lib/goals/loop.test.mjs +0 -845
  308. package/lib/hooks/bus.test.mjs +0 -387
  309. package/lib/identity/persona.test.mjs +0 -142
  310. package/lib/kpi-sensors.test.mjs +0 -278
  311. package/lib/kpi.test.mjs +0 -244
  312. package/lib/learning/config.test.mjs +0 -75
  313. package/lib/learning/counters.test.mjs +0 -69
  314. package/lib/learning/curator-consolidate.test.mjs +0 -238
  315. package/lib/learning/curator.test.mjs +0 -106
  316. package/lib/learning/reflect.test.mjs +0 -0
  317. package/lib/learning/session-index.test.mjs +0 -125
  318. package/lib/learning/skill-writer.test.mjs +0 -210
  319. package/lib/mandate/audit.test.mjs +0 -195
  320. package/lib/mandate/contract.test.mjs +0 -185
  321. package/lib/mandate/derive.test.mjs +0 -274
  322. package/lib/mandate/model.test.mjs +0 -164
  323. package/lib/mandate/refresh.test.mjs +0 -389
  324. package/lib/mcp/server.test.mjs +0 -426
  325. package/lib/model-router/auth-profiles.test.mjs +0 -580
  326. package/lib/model-router/catalog.test.mjs +0 -385
  327. package/lib/model-router/economics.test.mjs +0 -438
  328. package/lib/model-router/failover.test.mjs +0 -439
  329. package/lib/model-router/health.test.mjs +0 -338
  330. package/lib/model-router/integration-coverage.test.mjs +0 -831
  331. package/lib/model-router/integration.test.mjs +0 -564
  332. package/lib/model-router/ledger.test.mjs +0 -415
  333. package/lib/model-router/llm-task.test.mjs +0 -392
  334. package/lib/model-router/org-credentials.test.mjs +0 -265
  335. package/lib/model-router/pricing-refresh.test.mjs +0 -286
  336. package/lib/model-router/reconcile.test.mjs +0 -316
  337. package/lib/model-router/repair.test.mjs +0 -180
  338. package/lib/model-router/spawn.test.mjs +0 -446
  339. package/lib/model-router/taxonomy.test.mjs +0 -410
  340. package/lib/model-router.test.mjs +0 -1207
  341. package/lib/org/activity.test.mjs +0 -134
  342. package/lib/org/approvals.test.mjs +0 -216
  343. package/lib/org/awareness.test.mjs +0 -159
  344. package/lib/org/board-mine-cache.test.mjs +0 -53
  345. package/lib/org/board.test.mjs +0 -187
  346. package/lib/org/bootstrap-context.test.mjs +0 -153
  347. package/lib/org/client.test.mjs +0 -1206
  348. package/lib/org/cohort-client.test.mjs +0 -126
  349. package/lib/org/cost-sync.test.mjs +0 -153
  350. package/lib/org/doctor.test.mjs +0 -346
  351. package/lib/org/engagement-ledger.test.mjs +0 -112
  352. package/lib/org/engagement.test.mjs +0 -739
  353. package/lib/org/handoff.test.mjs +0 -269
  354. package/lib/org/inbound/directedness.test.mjs +0 -668
  355. package/lib/org/inbound/facts.test.mjs +0 -471
  356. package/lib/org/inbound/hydrate.test.mjs +0 -453
  357. package/lib/org/inbound/index.test.mjs +0 -429
  358. package/lib/org/inbound/project.test.mjs +0 -287
  359. package/lib/org/integration-tools.test.mjs +0 -160
  360. package/lib/org/keys.test.mjs +0 -92
  361. package/lib/org/knowledge.test.mjs +0 -326
  362. package/lib/org/leases.test.mjs +0 -235
  363. package/lib/org/mesh-directives.test.mjs +0 -110
  364. package/lib/org/mesh-integration.test.mjs +0 -127
  365. package/lib/org/mesh.test.mjs +0 -400
  366. package/lib/org/messaging.test.mjs +0 -471
  367. package/lib/org/param-contract.test.mjs +0 -477
  368. package/lib/org/policy.test.mjs +0 -237
  369. package/lib/org/protocol.checksum.test.mjs +0 -90
  370. package/lib/org/protocol.test.mjs +0 -323
  371. package/lib/org/push.test.mjs +0 -792
  372. package/lib/org/registry.test.mjs +0 -100
  373. package/lib/org/resource-tools.test.mjs +0 -361
  374. package/lib/org/tool-access.test.mjs +0 -144
  375. package/lib/org/tool-surface-integration.test.mjs +0 -120
  376. package/lib/org/tool-surface.test.mjs +0 -1268
  377. package/lib/org/typing.test.mjs +0 -291
  378. package/lib/org/ui-parity.test.mjs +0 -560
  379. package/lib/org/verify.test.mjs +0 -194
  380. package/lib/org/work-ledger.test.mjs +0 -273
  381. package/lib/plan/adoption-e2e.test.mjs +0 -366
  382. package/lib/plan/budget-enforcement.test.mjs +0 -400
  383. package/lib/plan/compile.test.mjs +0 -382
  384. package/lib/plan/emit.test.mjs +0 -269
  385. package/lib/plan/explain.test.mjs +0 -188
  386. package/lib/prompts/parallelism.test.mjs +0 -177
  387. package/lib/rag/rag.test.mjs +0 -505
  388. package/lib/rate-guard.test.mjs +0 -272
  389. package/lib/reactive-gate.test.mjs +0 -57
  390. package/lib/render.test.mjs +0 -68
  391. package/lib/resource-governor.test.mjs +0 -488
  392. package/lib/scheduling/dynamic-jobs.test.mjs +0 -344
  393. package/lib/scheduling/jitter.test.mjs +0 -140
  394. package/lib/secrets/broker.test.mjs +0 -280
  395. package/lib/secrets/providers.test.mjs +0 -274
  396. package/lib/security/audit-engine.test.mjs +0 -424
  397. package/lib/security/coerce-args.test.mjs +0 -281
  398. package/lib/security/dangerous-tools.test.mjs +0 -68
  399. package/lib/security/external-content.test.mjs +0 -84
  400. package/lib/security/redact.test.mjs +0 -441
  401. package/lib/security/secret-equal.test.mjs +0 -55
  402. package/lib/session/config.test.mjs +0 -92
  403. package/lib/session/feed-core.test.mjs +0 -198
  404. package/lib/session/first-run.test.mjs +0 -121
  405. package/lib/session/frontdoor.test.mjs +0 -205
  406. package/lib/session/handoffs.test.mjs +0 -183
  407. package/lib/session/identity.test.mjs +0 -180
  408. package/lib/session/inbox-claims.test.mjs +0 -286
  409. package/lib/session/launch-args.test.mjs +0 -157
  410. package/lib/session/liveness.test.mjs +0 -100
  411. package/lib/session/status-summary.test.mjs +0 -118
  412. package/lib/session-permissions.test.mjs +0 -120
  413. package/lib/setup/claude-probe.test.mjs +0 -187
  414. package/lib/setup/completeness.test.mjs +0 -110
  415. package/lib/setup/context-pack.test.mjs +0 -89
  416. package/lib/setup/enrich.test.mjs +0 -115
  417. package/lib/setup/enroll-from-cohort.test.mjs +0 -300
  418. package/lib/setup/integration.test.mjs +0 -162
  419. package/lib/setup/io.test.mjs +0 -77
  420. package/lib/setup/runner.test.mjs +0 -132
  421. package/lib/setup/sections/identity.test.mjs +0 -234
  422. package/lib/setup/sections/inventory.test.mjs +0 -198
  423. package/lib/setup/sections/learning.test.mjs +0 -81
  424. package/lib/setup/sections/mandate.test.mjs +0 -388
  425. package/lib/setup/sections/messaging.test.mjs +0 -127
  426. package/lib/setup/sections/model.test.mjs +0 -240
  427. package/lib/setup/sections/org.test.mjs +0 -346
  428. package/lib/setup/sections/orgmail.test.mjs +0 -118
  429. package/lib/setup/sections/recovery.test.mjs +0 -98
  430. package/lib/setup/sections/subagents.test.mjs +0 -429
  431. package/lib/setup/sections/verify.test.mjs +0 -175
  432. package/lib/setup/sot.test.mjs +0 -81
  433. package/lib/setup/state.test.mjs +0 -115
  434. package/lib/singleton.test.mjs +0 -151
  435. package/lib/subagents/cli.test.mjs +0 -389
  436. package/lib/subagents/client.test.mjs +0 -309
  437. package/lib/subagents/gap.test.mjs +0 -234
  438. package/lib/subagents/lock.test.mjs +0 -248
  439. package/lib/subagents/manifest.test.mjs +0 -175
  440. package/lib/subagents/refs.test.mjs +0 -204
  441. package/lib/subagents/resolve.test.mjs +0 -422
  442. package/lib/subagents/schema.test.mjs +0 -328
  443. package/lib/telemetry/alerts.test.mjs +0 -109
  444. package/lib/telemetry/collect.test.mjs +0 -1274
  445. package/lib/tool-definitions-integration.test.mjs +0 -83
  446. package/lib/tool-definitions.test.mjs +0 -437
  447. package/lib/upgrade/global-refresh.test.mjs +0 -65
  448. package/lib/upgrade/launchd-reconcile.test.mjs +0 -272
  449. package/lib/upgrade/post-steps.test.mjs +0 -200
  450. package/lib/upgrade/verify.test.mjs +0 -164
  451. package/lib/util/fetch-timeout.test.mjs +0 -202
  452. package/lib/util/reconnect.test.mjs +0 -369
  453. package/lib/util/unhandled.test.mjs +0 -216
  454. package/lib/voice/outbound.test.mjs +0 -69
  455. package/lib/voice/session-rotation.test.mjs +0 -114
  456. package/lib/voice/stt.test.mjs +0 -226
  457. package/lib/voice/voice.test.mjs +0 -990
  458. package/scripts/cadence/enqueue-cadence-tick.test.mjs +0 -187
  459. package/scripts/ci/check-docs-accuracy.test.mjs +0 -409
  460. package/scripts/ci/check-durable-write-seam.test.mjs +0 -90
  461. package/scripts/ci/check-no-build-artifacts.test.mjs +0 -71
  462. package/scripts/ci/check-no-residual-identity.test.mjs +0 -202
  463. package/scripts/ci/check-skill-packs.test.mjs +0 -495
  464. package/scripts/ci/check-subagent-frontmatter.test.mjs +0 -124
  465. package/scripts/ci/check.test.mjs +0 -194
  466. package/scripts/ci/conformance-org-api.test.mjs +0 -425
  467. package/scripts/cloud-relay/voice/relay-identity.test.mjs +0 -96
  468. package/scripts/collective/hook-runner.test.mjs +0 -173
  469. package/scripts/cost/fleet-digest.test.mjs +0 -207
  470. package/scripts/cost/track-claude-usage-pricing.test.mjs +0 -183
  471. package/scripts/cost/track-claude-usage.test.mjs +0 -148
  472. package/scripts/daemon/agent-daemon-board-mine.test.mjs +0 -96
  473. package/scripts/daemon/agent-daemon-design.test.mjs +0 -238
  474. package/scripts/daemon/agent-daemon-frontdoor.test.mjs +0 -60
  475. package/scripts/daemon/agent-daemon.test.mjs +0 -995
  476. package/scripts/daemon/assurance-e2e.test.mjs +0 -613
  477. package/scripts/daemon/assurance.test.mjs +0 -1791
  478. package/scripts/daemon/board-mirror.test.mjs +0 -165
  479. package/scripts/daemon/cadence-consumer-frontdoor.test.mjs +0 -393
  480. package/scripts/daemon/cadence-consumer-governance.test.mjs +0 -276
  481. package/scripts/daemon/cadence-consumer.test.mjs +0 -776
  482. package/scripts/daemon/cadence-handlers.test.mjs +0 -837
  483. package/scripts/daemon/classifier-identity.test.mjs +0 -137
  484. package/scripts/daemon/classifier.test.mjs +0 -266
  485. package/scripts/daemon/classify-kind.test.mjs +0 -40
  486. package/scripts/daemon/context-compiler.test.mjs +0 -300
  487. package/scripts/daemon/deliver.test.mjs +0 -564
  488. package/scripts/daemon/dispatcher-cooldown.test.mjs +0 -122
  489. package/scripts/daemon/dispatcher-governance.test.mjs +0 -1013
  490. package/scripts/daemon/dispatcher-resume.test.mjs +0 -166
  491. package/scripts/daemon/execution-ladder.test.mjs +0 -470
  492. package/scripts/daemon/goal-steward-cadence.test.mjs +0 -312
  493. package/scripts/daemon/inbox-deferral-session.test.mjs +0 -49
  494. package/scripts/daemon/inbox-deferral.test.mjs +0 -336
  495. package/scripts/daemon/inbox-wake.test.mjs +0 -199
  496. package/scripts/daemon/integration.test.mjs +0 -149
  497. package/scripts/daemon/lib/self-echo.test.mjs +0 -153
  498. package/scripts/daemon/lib/session-router.test.mjs +0 -295
  499. package/scripts/daemon/prompt-builder-preamble.test.mjs +0 -210
  500. package/scripts/daemon/prompt-builder.test.mjs +0 -344
  501. package/scripts/daemon/responder-cost.test.mjs +0 -68
  502. package/scripts/daemon/responder-history.test.mjs +0 -185
  503. package/scripts/daemon/sdk-version.test.mjs +0 -31
  504. package/scripts/daemon/session-lock.test.mjs +0 -252
  505. package/scripts/daemon/session-outcomes.test.mjs +0 -533
  506. package/scripts/daemon/typing-registry.test.mjs +0 -102
  507. package/scripts/hooks/pre-send-audit.test.mjs +0 -354
  508. package/scripts/huddle/huddle-prompt.test.mjs +0 -176
  509. package/scripts/local-triggers/autoupdate.test.mjs +0 -518
  510. package/scripts/local-triggers/generate-plists.test.mjs +0 -456
  511. package/scripts/media-generation/brand-clause.test.mjs +0 -135
  512. package/scripts/org/send-orgmail.first-contact.test.mjs +0 -102
  513. package/scripts/poller/inbox-privilege-injection.test.mjs +0 -167
  514. package/scripts/poller/inbox-scan-poller.test.mjs +0 -295
  515. package/scripts/poller/lib/cloud-relay-dedup.test.mjs +0 -133
  516. package/scripts/poller/slack-socket-mode.test.mjs +0 -805
  517. package/scripts/poller-launchd/install.test.mjs +0 -243
  518. package/scripts/restore-from-backup.test.mjs +0 -181
  519. package/scripts/session/feed.test.mjs +0 -196
  520. package/scripts/session/supervisor-sh.test.mjs +0 -218
  521. package/scripts/session/supervisor.test.mjs +0 -482
  522. package/scripts/setup/configure-macos.test.mjs +0 -306
  523. package/scripts/setup/gen-subagent-manifest.test.mjs +0 -124
  524. package/scripts/setup/generate-agent-package-json.test.mjs +0 -143
  525. package/scripts/setup/generate-capability.test.mjs +0 -134
  526. package/scripts/setup/init-agent.test.mjs +0 -370
  527. package/scripts/setup/init-skill-marketplace.test.mjs +0 -193
  528. package/scripts/vendor/sync-skill-packs.test.mjs +0 -103
  529. 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
+ }