@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,560 @@
1
+ /**
2
+ * lib/engine/loop.mjs — the agent loop.
3
+ *
4
+ * One run = model turn → (tool calls → tool results → model turn)* → answer.
5
+ *
6
+ * The loop owns ordering and stopping; it owns no I/O of its own. The model is
7
+ * an injected `callModel` (see `wire/index.mjs`) and tools are injected tool
8
+ * objects (see `tools/index.mjs`), so the whole loop runs in a test with a
9
+ * scripted fake model and no network.
10
+ *
11
+ * What it guarantees:
12
+ * · Stop conditions are a union (`LoopStop`): completed | max_turns |
13
+ * wall_clock | aborted | wire_error | loop_detected. Exactly one ends a run.
14
+ * · Every assistant `tool_use` is answered by a `tool_result` before the next
15
+ * model call or the end of the run, so the transcript is always resumable.
16
+ * · Read-only tool calls that arrive together run in parallel (bounded);
17
+ * anything that can mutate runs alone, in the order the model gave.
18
+ * · Tool-argument JSON that did not parse gets one repair pass; a repaired
19
+ * call is written back into the assistant message as valid JSON.
20
+ * · The same call (tool + identical input) made turn after turn is flagged in
21
+ * its result at `loopWarnAt` and ends the run at `loopStopAt`.
22
+ * · A tool that THROWS (a defect in the tool) becomes an error result; one
23
+ * buggy tool must not take down a seat's run.
24
+ * · A turn that stopped at the output token limit (`stopReason max_tokens`)
25
+ * runs none of its tool calls — a cut-off call can be "repaired" into a
26
+ * valid one that silently lost its trailing fields — and asks for a resend.
27
+ * · `costMicros` is exact or null: a request that failed after the gateway
28
+ * accepted it may be billed with no figure, so it nulls the sum.
29
+ * · The wall-clock timer and the outer abort listener are released even when
30
+ * a defect throws out of the loop.
31
+ * · With a `guard` (guard.mjs), no tool runs until permissions, in-process
32
+ * gates and PreToolUse hooks have passed it; a refusal is the call's error
33
+ * result. A guard that throws refuses the call (fail closed).
34
+ * · MCP tools (`passthroughInput`) get their input checked for shape and
35
+ * required keys only, and forwarded unchanged.
36
+ * · With `onStop`, a Stop hook that asks for more work turns a finished run
37
+ * into another turn, its reason sent as the next user message.
38
+ * · With `context` (context/manager.mjs), each turn boundary may replace the
39
+ * history (compaction), append user messages, and change the tools that
40
+ * are sent (deferred loading, MCP list changes); text it returns after a
41
+ * batch is appended to the tool-results message. Model calls made on the
42
+ * run's behalf outside the turn loop (a compaction summary, WebFetch,
43
+ * WebSearch) are reported through `toolContext.recordModelCall` and join
44
+ * the run's usage, request ids and cost exactly like turns do.
45
+ *
46
+ * The pure decisions (`planBatches`, `nextStreaks`, `repairToolUses`,
47
+ * `failureMayBeBilled`) are exported and tested on their own.
48
+ *
49
+ * @module lib/engine/loop
50
+ */
51
+
52
+ import { toolUses, messageText, stableStringify, userText } from "./messages.mjs";
53
+ import { validateInput, repairToolJson } from "./tools/schema.mjs";
54
+ import { toolDefinitions } from "./tools/index.mjs";
55
+ import { emptyUsage, addUsage, readFrameUsage } from "./wire/usage.mjs";
56
+
57
+ /**
58
+ * @typedef {{kind:'completed'} | {kind:'max_turns', maxTurns:number} | {kind:'wall_clock', wallClockMs:number}
59
+ * | {kind:'aborted'} | {kind:'wire_error', error:any} | {kind:'loop_detected', tool:string, repeats:number}
60
+ * | {kind:'hook_stopped', reason:string} | {kind:'max_budget', maxBudgetUsd:number, spentMicros:string}} LoopStop
61
+ */
62
+
63
+ export const DEFAULTS = Object.freeze({
64
+ maxTurns: 50,
65
+ wallClockMs: 30 * 60_000,
66
+ maxParallel: 8,
67
+ loopWarnAt: 3,
68
+ loopStopAt: 5,
69
+ maxToolResultChars: 100_000,
70
+ });
71
+
72
+ /**
73
+ * Group tool calls into execution batches: runs of consecutive read-only calls
74
+ * (at most `maxParallel` each) run together; every other call is its own batch.
75
+ * @param {Array<{name:string}>} calls
76
+ * @param {(name:string) => boolean} isReadOnly
77
+ * @param {number} maxParallel
78
+ * @returns {number[][]} batches of indices into `calls`, in order
79
+ */
80
+ export function planBatches(calls, isReadOnly, maxParallel) {
81
+ const batches = [];
82
+ let current = [];
83
+ calls.forEach((c, i) => {
84
+ if (isReadOnly(c.name)) {
85
+ if (current.length >= maxParallel) {
86
+ batches.push(current);
87
+ current = [];
88
+ }
89
+ current.push(i);
90
+ } else {
91
+ if (current.length > 0) batches.push(current);
92
+ current = [];
93
+ batches.push([i]);
94
+ }
95
+ });
96
+ if (current.length > 0) batches.push(current);
97
+ return batches;
98
+ }
99
+
100
+ /**
101
+ * Consecutive-turn streaks per call signature. A signature absent from this
102
+ * turn resets to zero (it is simply not carried forward).
103
+ * @param {Map<string, number>} prev
104
+ * @param {string[]} signatures this turn's call signatures
105
+ * @returns {Map<string, number>}
106
+ */
107
+ export function nextStreaks(prev, signatures) {
108
+ const next = new Map();
109
+ for (const s of new Set(signatures)) next.set(s, (prev.get(s) ?? 0) + 1);
110
+ return next;
111
+ }
112
+
113
+ /** @param {{name:string, input:unknown}} call */
114
+ export function callSignature(call) {
115
+ return `${call.name}:${stableStringify(call.input ?? {})}`;
116
+ }
117
+
118
+ /**
119
+ * Repair unparseable tool arguments in an assistant message.
120
+ * @param {{role:string, content:any[]}} message
121
+ * @returns {{ message: {role:string, content:any[]}, unrepaired: Map<string, string> }}
122
+ * `unrepaired` maps tool_use id → the parse error to report back.
123
+ */
124
+ export function repairToolUses(message) {
125
+ const unrepaired = new Map();
126
+ const content = message.content.map((b) => {
127
+ if (b.type !== "tool_use" || !b.inputError) return b;
128
+ const r = repairToolJson(b.rawInput ?? "");
129
+ if (r.ok) return { type: "tool_use", id: b.id, name: b.name, input: r.value };
130
+ unrepaired.set(b.id, b.inputError);
131
+ return b;
132
+ });
133
+ return { message: { ...message, content }, unrepaired };
134
+ }
135
+
136
+ /**
137
+ * @param {object} p
138
+ * @param {(turn:{system:string, messages:any[], tools:any[], signal:AbortSignal}) => Promise<any>} p.callModel
139
+ * @param {string} p.system
140
+ * @param {any[]} p.messages full history INCLUDING the new user prompt
141
+ * @param {import('./tools/index.mjs').Tool[]} p.tools
142
+ * @param {import('./tools/index.mjs').ToolContext} p.toolContext its `signal` is replaced by the run's
143
+ * @param {number} [p.maxTurns]
144
+ * @param {number} [p.wallClockMs]
145
+ * @param {AbortSignal} [p.signal]
146
+ * @param {() => number} [p.now]
147
+ * @param {(message:any) => void} [p.onMessage] called for every message the run appends
148
+ * @param {number} [p.maxParallel]
149
+ * @param {number} [p.loopWarnAt]
150
+ * @param {number} [p.loopStopAt]
151
+ * @param {number} [p.maxToolResultChars]
152
+ * @param {ReturnType<typeof import('./guard.mjs').createToolGuard>|null} [p.guard]
153
+ * @param {(s:{stopHookActive:boolean}) => Promise<{continueWith:string|null}|null>} [p.onStop]
154
+ * @param {() => string[]} [p.drainNotifications] before each model call: messages to add as user turns
155
+ * @param {(signal:AbortSignal) => Promise<string[]>} [p.awaitNotifications] when the model has finished:
156
+ * waits for pending background work; a non-empty answer continues the run with those messages
157
+ * @param {{beforeTurn:(t:{system:string, messages:any[], signal:AbortSignal, recordModelCall:(res:any)=>void})=>Promise<{messages?:any[], append?:any[], tools?:any[], definitions?:any[]}|null>,
158
+ * observe?:(o:{system:string, messages:any[], tools:any[], res:any})=>void,
159
+ * afterTools?:(o:{calls:any[], results:any[], signal:AbortSignal})=>Promise<any[]|null>}|null} [p.context]
160
+ */
161
+ export async function runLoop(p) {
162
+ const maxTurns = p.maxTurns ?? DEFAULTS.maxTurns;
163
+ const wallClockMs = p.wallClockMs ?? DEFAULTS.wallClockMs;
164
+ const maxParallel = p.maxParallel ?? DEFAULTS.maxParallel;
165
+ const loopWarnAt = p.loopWarnAt ?? DEFAULTS.loopWarnAt;
166
+ const loopStopAt = p.loopStopAt ?? DEFAULTS.loopStopAt;
167
+ const maxChars = p.maxToolResultChars ?? DEFAULTS.maxToolResultChars;
168
+ const now = p.now || Date.now;
169
+ const onMessage = p.onMessage || (() => {});
170
+
171
+ const messages = [...p.messages];
172
+ const newMessages = [];
173
+ const append = (m) => {
174
+ messages.push(m);
175
+ newMessages.push(m);
176
+ onMessage(m);
177
+ };
178
+
179
+ // One controller for the run: tripped by the caller's signal or the clock.
180
+ const controller = new AbortController();
181
+ let wallClockHit = false;
182
+ const timer = setTimeout(() => {
183
+ wallClockHit = true;
184
+ controller.abort();
185
+ }, wallClockMs);
186
+ const onOuterAbort = () => controller.abort();
187
+ if (p.signal?.aborted) controller.abort();
188
+ else p.signal?.addEventListener("abort", onOuterAbort, { once: true });
189
+ const signal = controller.signal;
190
+
191
+ // The timer and the outer listener are released however the run ends —
192
+ // including a defect that throws out of callModel, onMessage or a tool
193
+ // executor. A leaked 30-minute timer would hold the CLI process open.
194
+ try {
195
+ return await runTurns(p, {
196
+ signal,
197
+ append,
198
+ messages,
199
+ newMessages,
200
+ maxTurns,
201
+ wallClockMs,
202
+ maxParallel,
203
+ loopWarnAt,
204
+ loopStopAt,
205
+ maxChars,
206
+ isWallClock: () => wallClockHit,
207
+ });
208
+ } finally {
209
+ clearTimeout(timer);
210
+ p.signal?.removeEventListener("abort", onOuterAbort);
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Did a failed model request possibly reach billing? Only a refusal the
216
+ * gateway answered BEFORE accepting the request (a non-2xx status) is known to
217
+ * be free. A failure after acceptance (`accepted:true` — a broken stream, an
218
+ * in-stream error, an abort mid-relay) or a transport fault with no status (the
219
+ * request may have been admitted before the socket died) may be billed, so the
220
+ * run's cost sum is no longer exact.
221
+ * @param {{accepted?:boolean, error?:{status?:number|null}}} res
222
+ */
223
+ export function failureMayBeBilled(res) {
224
+ return res.accepted === true || res.error?.status == null;
225
+ }
226
+
227
+ /**
228
+ * The body of `runLoop`, separated so its caller can release the run's timer
229
+ * and listener in one `finally`.
230
+ * @param {Parameters<typeof runLoop>[0]} p
231
+ * @param {any} r
232
+ */
233
+ async function runTurns(p, r) {
234
+ const { signal, append, messages, newMessages, maxTurns, wallClockMs, maxParallel, loopWarnAt, loopStopAt, maxChars } = r;
235
+ let toolsList = p.tools;
236
+ let toolsByName = new Map(toolsList.map((t) => [t.name, t]));
237
+ let defs = toolDefinitions(toolsList);
238
+ const ctx = { ...p.toolContext, signal };
239
+
240
+ let usage = emptyUsage("provider");
241
+ // CF-158: the gateway's OWN usage, off the terminal `event: cohort` frame,
242
+ // summed over this loop's requests. Deliberately a SECOND accumulator: it is
243
+ // the gateway's view of the same turns, near-equal to `usage` but not the
244
+ // same number, and conflating the two under one name is the CF-158 defect.
245
+ // Stays null while no frame reports any, so "not reported" ≠ "zero".
246
+ let gatewayUsage = null;
247
+ const requestIds = [];
248
+ let costTotal = 0n;
249
+ let costComplete = true;
250
+ let quota = null;
251
+ let modelTier = null;
252
+ let model = null;
253
+ let apiMs = 0;
254
+ let numTurns = 0;
255
+ let streaks = new Map();
256
+ let lastText = "";
257
+ let stopHookActive = false;
258
+ const guard = p.guard ?? null;
259
+
260
+ /**
261
+ * The model gave no tool calls. Ask the Stop hooks whether the run may end.
262
+ * @returns {Promise<LoopStop|null>} null = continue with another turn
263
+ */
264
+ const finishOrContinue = async () => {
265
+ // Background work still running (agents/runtime.mjs): its report becomes
266
+ // the next user message instead of the run ending without it.
267
+ if (p.awaitNotifications && !signal.aborted) {
268
+ const notes = await p.awaitNotifications(signal);
269
+ if (signal.aborted) return interrupted();
270
+ if (notes.length > 0) {
271
+ for (const n of notes) append(userText(n));
272
+ if (numTurns >= maxTurns) return { kind: "max_turns", maxTurns };
273
+ return null;
274
+ }
275
+ }
276
+ if (!p.onStop || signal.aborted) return { kind: "completed" };
277
+ const s = await p.onStop({ stopHookActive });
278
+ if (!s?.continueWith) return { kind: "completed" };
279
+ stopHookActive = true;
280
+ append(userText(`Before finishing, a Stop hook asked for more:\n${s.continueWith}`));
281
+ if (numTurns >= maxTurns) return { kind: "max_turns", maxTurns };
282
+ return null;
283
+ };
284
+
285
+ /** @param {LoopStop} stop */
286
+ const done = (stop) => {
287
+ return {
288
+ stop,
289
+ messages,
290
+ newMessages,
291
+ resultText: lastText,
292
+ numTurns,
293
+ usage,
294
+ // The gateway's own per-request counts (CF-158), or null if it reported
295
+ // none. Never merged into `usage` — that would double-count every turn.
296
+ gatewayUsage,
297
+ requestIds,
298
+ costMicros: costComplete && numTurns > 0 ? costTotal.toString() : null,
299
+ // false when a request may have been billed with no figure; a run with no
300
+ // turn and costComplete true was never billed (agents/usage.mjs).
301
+ costComplete,
302
+ quota,
303
+ modelTier,
304
+ model,
305
+ apiMs,
306
+ };
307
+ };
308
+ const interrupted = () => (r.isWallClock() ? { kind: "wall_clock", wallClockMs } : { kind: "aborted" });
309
+ // W4-E1 (row 24): a spend cap shared with the run's subagents (budget.mjs). Asked before
310
+ // every model call this loop makes; the tool results of the last turn are always finished.
311
+ const budget = p.budget ?? null;
312
+ const overBudget = () =>
313
+ budget && budget.exhausted() ? { kind: "max_budget", maxBudgetUsd: Number(budget.maxMicros) / 1e6, spentMicros: budget.spentMicros().toString() } : null;
314
+
315
+ /**
316
+ * Fold one gateway request into the run's accounting. `turn` is false for a
317
+ * request made on the run's behalf outside the turn loop (it counts toward
318
+ * usage, request ids and cost, but not turns, and does not name the tier).
319
+ * @param {any} res @param {boolean} turn
320
+ */
321
+ const account = (res, turn) => {
322
+ budget?.record(res);
323
+ apiMs += res.apiMs ?? 0;
324
+ if (res.cohort) {
325
+ if (res.cohort.requestId) requestIds.push(res.cohort.requestId);
326
+ if (turn && res.cohort.modelTier) modelTier = res.cohort.modelTier;
327
+ if (turn && hasQuota(res.cohort.quota)) quota = res.cohort.quota;
328
+ // CF-158. Read through readFrameUsage again even though the wire already
329
+ // normalised it: this accumulator must never take a malformed object from
330
+ // a future call site, and an unusable one leaves the total untouched
331
+ // rather than adding zeros.
332
+ const gu = readFrameUsage(res.cohort.usage);
333
+ if (gu) gatewayUsage = addUsage(gatewayUsage ?? emptyUsage("provider"), gu);
334
+ }
335
+ if (!res.ok) {
336
+ // A request that may have been billed has no cost figure, so the sum of
337
+ // the earlier turns would under-report the run: report no total instead.
338
+ if (failureMayBeBilled(res)) costComplete = false;
339
+ return;
340
+ }
341
+ if (res.value?.usage) usage = addUsage(usage, res.value.usage);
342
+ if (turn && res.value?.model) model = res.value.model;
343
+ if (res.cohort?.costMicros != null) costTotal += BigInt(res.cohort.costMicros);
344
+ else costComplete = false;
345
+ };
346
+ ctx.recordModelCall = (/** @type any */ res) => {
347
+ if (res && typeof res === "object") account(res, false);
348
+ };
349
+
350
+ for (;;) {
351
+ if (signal.aborted) return done(interrupted());
352
+ // Background tasks that ended since the last turn report as user messages
353
+ // (before the context manager measures the request, so they count toward it).
354
+ for (const n of p.drainNotifications?.() ?? []) append(userText(n));
355
+ {
356
+ const spent = overBudget();
357
+ if (spent) return done(spent);
358
+ }
359
+
360
+ if (p.context) {
361
+ const prep = await p.context.beforeTurn({ system: p.system, messages, signal, recordModelCall: ctx.recordModelCall });
362
+ if (prep?.messages) messages.splice(0, messages.length, ...prep.messages);
363
+ for (const m of prep?.append ?? []) append(m);
364
+ if (prep?.tools && prep.tools !== toolsList) {
365
+ toolsList = prep.tools;
366
+ toolsByName = new Map(toolsList.map((t) => [t.name, t]));
367
+ }
368
+ defs = prep?.definitions ?? toolDefinitions(toolsList);
369
+ if (signal.aborted) return done(interrupted());
370
+ // A compaction summary is a model call too: it may have spent the rest.
371
+ const spent = overBudget();
372
+ if (spent) return done(spent);
373
+ }
374
+
375
+ const res = await p.callModel({ system: p.system, messages, tools: defs, signal });
376
+ p.context?.observe?.({ system: p.system, messages, tools: defs, res });
377
+ account(res, true);
378
+ if (!res.ok) {
379
+ if (res.error?.kind === "aborted" || signal.aborted) return done(interrupted());
380
+ return done({ kind: "wire_error", error: res.error });
381
+ }
382
+ numTurns++;
383
+
384
+ // Output cut off by the token limit: any tool call in this turn may be
385
+ // missing trailing fields (a `replace_all`, a `timeout`) that bracket repair
386
+ // would silently drop. Nothing runs; the model is asked to resend.
387
+ if (res.value.stopReason === "max_tokens") {
388
+ const message = res.value.message;
389
+ append(message);
390
+ const text = messageText(message);
391
+ if (text !== "") lastText = text;
392
+ const calls = toolUses(message);
393
+ if (calls.length === 0) {
394
+ const stop = await finishOrContinue();
395
+ if (stop) return done(stop);
396
+ continue;
397
+ }
398
+ streaks = nextStreaks(streaks, []);
399
+ append({ role: "user", content: calls.map((c) => errorResult(c.id, TRUNCATED_CALL)) });
400
+ if (signal.aborted) return done(interrupted());
401
+ if (numTurns >= maxTurns) return done({ kind: "max_turns", maxTurns });
402
+ continue;
403
+ }
404
+
405
+ const { message, unrepaired } = repairToolUses(res.value.message);
406
+ append(message);
407
+ const text = messageText(message);
408
+ if (text !== "") lastText = text;
409
+
410
+ const calls = toolUses(message);
411
+ if (calls.length === 0) {
412
+ const stop = await finishOrContinue();
413
+ if (stop) return done(stop);
414
+ continue;
415
+ }
416
+
417
+ streaks = nextStreaks(streaks, calls.filter((c) => !unrepaired.has(c.id)).map(callSignature));
418
+ const worst = calls.reduce(
419
+ (acc, c) => {
420
+ const n = unrepaired.has(c.id) ? 0 : streaks.get(callSignature(c)) ?? 0;
421
+ return n > acc.n ? { n, tool: c.name } : acc;
422
+ },
423
+ { n: 0, tool: "" },
424
+ );
425
+
426
+ /** @type {Array<{type:'tool_result', tool_use_id:string, content:string, is_error?:boolean}>} */
427
+ const results = new Array(calls.length);
428
+ if (worst.n >= loopStopAt) {
429
+ calls.forEach((c, i) => {
430
+ results[i] = errorResult(c.id, "Not run: this exact call was repeated too many times in a row and the run was stopped.");
431
+ });
432
+ append({ role: "user", content: results });
433
+ return done({ kind: "loop_detected", tool: worst.tool, repeats: worst.n });
434
+ }
435
+
436
+ for (const batch of planBatches(calls, (n) => Boolean(toolsByName.get(n)?.readOnly), maxParallel)) {
437
+ await Promise.all(
438
+ batch.map(async (i) => {
439
+ const c = calls[i];
440
+ const r = await executeCall(c, unrepaired.get(c.id), toolsByName, ctx, signal, guard);
441
+ const streak = unrepaired.has(c.id) ? 0 : streaks.get(callSignature(c)) ?? 0;
442
+ const note =
443
+ streak >= loopWarnAt
444
+ ? `\n\n[This identical call has now been made ${streak} times in a row. The result will not change; try a different approach.]`
445
+ : "";
446
+ const body = clip((r.ok ? r.content : r.error) + note, maxChars);
447
+ const images = r.ok && Array.isArray(/** @type any */ (r).images) && /** @type any */ (r).images.length > 0 ? /** @type any */ (r).images : null;
448
+ results[i] = r.ok ? { type: "tool_result", tool_use_id: c.id, content: body, ...(images ? { images } : {}) } : errorResult(c.id, body);
449
+ }),
450
+ );
451
+ }
452
+ const extra = p.context?.afterTools ? await p.context.afterTools({ calls, results, signal }) : null;
453
+ append({ role: "user", content: extra && extra.length > 0 ? [...results, ...extra] : results });
454
+
455
+ if (signal.aborted) return done(interrupted());
456
+ if (guard?.stopReason) return done({ kind: "hook_stopped", reason: guard.stopReason });
457
+ if (numTurns >= maxTurns) return done({ kind: "max_turns", maxTurns });
458
+ }
459
+ }
460
+
461
+ /**
462
+ * The input check for a tool whose schema belongs to someone else (an MCP
463
+ * server): an object carrying its required keys, forwarded unchanged.
464
+ * @param {any} schema @param {unknown} input
465
+ * @returns {{ok:true, value:Record<string,unknown>}|{ok:false, error:string}}
466
+ */
467
+ export function checkPassthroughInput(schema, input) {
468
+ if (!input || typeof input !== "object" || Array.isArray(input)) return { ok: false, error: "input must be a JSON object" };
469
+ const missing = (Array.isArray(schema?.required) ? schema.required : []).filter((k) => /** @type any */ (input)[k] === undefined || /** @type any */ (input)[k] === null);
470
+ if (missing.length > 0) return { ok: false, error: missing.map((k) => `missing required parameter "${k}"`).join("; ") };
471
+ return { ok: true, value: /** @type any */ (input) };
472
+ }
473
+
474
+ export const TRUNCATED_CALL =
475
+ "Not run: your response reached the output token limit while this tool call was being written, so its arguments may be incomplete. " +
476
+ "Send the call again with shorter arguments; split large content across several calls.";
477
+
478
+ /** @param {any} q */
479
+ function hasQuota(q) {
480
+ return Boolean(q && (Object.keys(q.windows || {}).length > 0 || q.creditsRemainingMicros != null || q.funding != null));
481
+ }
482
+
483
+ /** @param {string} id @param {string} content */
484
+ function errorResult(id, content) {
485
+ return { type: "tool_result", tool_use_id: id, content, is_error: true };
486
+ }
487
+
488
+ /** @param {string} s @param {number} max */
489
+ function clip(s, max) {
490
+ return s.length <= max ? s : `${s.slice(0, max)}\n… [tool result truncated at ${max} characters]`;
491
+ }
492
+
493
+ /**
494
+ * @param {{id:string, name:string, input:any}} call
495
+ * @param {string|undefined} parseError
496
+ * @param {Map<string, import('./tools/index.mjs').Tool>} toolsByName
497
+ * @param {import('./tools/index.mjs').ToolContext} ctx
498
+ * @param {AbortSignal} signal
499
+ * @param {ReturnType<typeof import('./guard.mjs').createToolGuard>|null} guard
500
+ * @returns {Promise<{ok:true, content:string}|{ok:false, error:string}>}
501
+ */
502
+ async function executeCall(call, parseError, toolsByName, ctx, signal, guard) {
503
+ if (parseError !== undefined) {
504
+ return {
505
+ ok: false,
506
+ error: `The arguments for ${call.name} were not valid JSON (${parseError}). Send the call again with a JSON object that matches the tool's schema.`,
507
+ };
508
+ }
509
+ const tool = toolsByName.get(call.name);
510
+ if (!tool) {
511
+ return { ok: false, error: `There is no tool named "${call.name}". Available tools: ${[...toolsByName.keys()].join(", ") || "none"}.` };
512
+ }
513
+ if (signal.aborted) return { ok: false, error: "Not run: the run was interrupted." };
514
+ const check = (/** @type unknown */ input) => (/** @type any */ (tool).passthroughInput ? checkPassthroughInput(tool.input_schema, input) : validateInput(tool.input_schema, input));
515
+ const v = check(call.input);
516
+ if (!v.ok) return { ok: false, error: `Invalid input for ${tool.name}: ${v.error}.` };
517
+
518
+ let input = v.value;
519
+ let hookContext = null;
520
+ if (guard) {
521
+ let g;
522
+ try {
523
+ g = await guard.before({ call, tool, input, signal });
524
+ } catch (e) {
525
+ return { ok: false, error: `Not run: the permission check for ${tool.name} failed unexpectedly (${e instanceof Error ? e.message : String(e)}).` };
526
+ }
527
+ if (!g.ok) return { ok: false, error: g.error };
528
+ if (g.input !== input) {
529
+ const again = check(g.input);
530
+ if (!again.ok) return { ok: false, error: `Invalid input for ${tool.name} after a hook changed it: ${again.error}.` };
531
+ input = again.value;
532
+ }
533
+ hookContext = g.context;
534
+ if (signal.aborted) return { ok: false, error: "Not run: the run was interrupted." };
535
+ }
536
+
537
+ /** @type {{ok:true, content:string}|{ok:false, error:string}} */
538
+ let r;
539
+ try {
540
+ r = await tool.run(input, { ...ctx, toolUseId: call.id }); // a subagent's events name the call that started it
541
+ } catch (e) {
542
+ // A tool throwing is a defect in that tool; contain it to this call so the
543
+ // model can route around it and the transcript stays well-formed.
544
+ r = { ok: false, error: `${tool.name} failed unexpectedly: ${e instanceof Error ? e.message : String(e)}` };
545
+ }
546
+
547
+ const notes = [];
548
+ if (hookContext) notes.push(`[PreToolUse hook context]\n${hookContext}`);
549
+ if (guard) {
550
+ try {
551
+ const extra = await guard.after({ call, tool, input, result: r, signal });
552
+ if (extra) notes.push(extra);
553
+ } catch {
554
+ /* a PostToolUse defect cannot undo a call that already ran */
555
+ }
556
+ }
557
+ if (notes.length === 0) return r;
558
+ const tail = `\n\n${notes.join("\n\n")}`;
559
+ return r.ok ? { ...r, content: r.content + tail } : { ok: false, error: r.error + tail };
560
+ }