@cohortapp/agent-sdk 2.17.0 → 2.18.4

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