@cohortapp/agent-sdk 2.17.0 → 2.18.5

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