@cohortapp/agent-sdk 2.16.0 → 2.18.4

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