@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,332 @@
1
+ /**
2
+ * lib/engine/wire/openai-chat.mjs — the OpenAI Chat Completions wire.
3
+ *
4
+ * cohort-llm's `/v1/chat/completions` is a zero-translation passthrough to the
5
+ * open-model supplier (design §4.2), so this is the engine's default wire.
6
+ *
7
+ * Three layers, only the last one does I/O:
8
+ * · `buildOpenAIRequest` canonical history → request body (pure)
9
+ * · `createOpenAIAccumulator` SSE frames → one assistant turn (pure state)
10
+ * · `callOpenAIChat` the edge, over `postStream`
11
+ *
12
+ * Prompt caching. The open-model supplier caches by PREFIX automatically; what
13
+ * the engine controls is that the prefix is byte-stable across turns: the
14
+ * system message is first and alone, tools are sent in registry order, and the
15
+ * history only ever grows at the tail. For suppliers that honour explicit
16
+ * breakpoints on this wire (the OpenRouter dialect), `cacheBreakpoints:true`
17
+ * sends the system prompt as a content part carrying `cache_control` — the end
18
+ * of the stable prefix. It is off by default because a strict OpenAI server
19
+ * rejects a content-part array on a system message.
20
+ *
21
+ * `promptCacheKey` is sent as `prompt_cache_key` when the caller has one (the
22
+ * gateway advertised support or config enabled it; see wire/prompt-cache.mjs).
23
+ * With `cacheBreakpoints:true` the final user message is marked too.
24
+ *
25
+ * Images. OpenAI tool messages carry text only, so images from tool results
26
+ * (`images:true`) follow the tool messages as ONE user message of `image_url`
27
+ * data-URL parts. With `images:false` (the default: open text models reject
28
+ * image parts) the tool text says the images were not sent.
29
+ *
30
+ * Reasoning (CF-72): `reasoning_content` and `reasoning_signature` deltas are
31
+ * kept as one canonical thinking block, and an assistant turn sent back echoes
32
+ * both fields (`reasoningFields`), so a leg that needs its signed reasoning on
33
+ * replay gets it — including after a resume, since the block is in the transcript.
34
+ *
35
+ * Usage (design §4.5): arrives on the finish chunk or a trailing chunk
36
+ * (`stream_options.include_usage`); the LAST usage object wins, normalised by
37
+ * `normaliseOpenAIUsage` (prompt_tokens INCLUDES cached tokens).
38
+ *
39
+ * @module lib/engine/wire/openai-chat
40
+ */
41
+
42
+ import { classifyStreamError, protocolError } from "./errors.mjs";
43
+ import { postStream, joinUrl } from "./http.mjs";
44
+ import { normaliseOpenAIUsage, emptyUsage } from "./usage.mjs";
45
+ import { normaliseHistory } from "../messages.mjs";
46
+
47
+ export const OPENAI_CHAT_ROUTE = "/v1/chat/completions";
48
+
49
+ /**
50
+ * @param {object} p
51
+ * @param {string} p.model
52
+ * @param {string} p.system
53
+ * @param {Array<{role:string, content:any[]}>} p.messages
54
+ * @param {Array<{name:string, description:string, input_schema:object}>} [p.tools]
55
+ * @param {number} p.maxTokens
56
+ * @param {boolean} [p.cacheBreakpoints]
57
+ * @param {'low'|'medium'|'high'|null} [p.reasoningEffort]
58
+ * @param {string|null} [p.promptCacheKey]
59
+ * @param {boolean} [p.images]
60
+ */
61
+ export function buildOpenAIRequest({ model, system, messages, tools = [], maxTokens, cacheBreakpoints = false, reasoningEffort = null, promptCacheKey = null, images = false }) {
62
+ /** @type {any[]} */
63
+ const out = [];
64
+ if (system) {
65
+ out.push(
66
+ cacheBreakpoints
67
+ ? { role: "system", content: [{ type: "text", text: system, cache_control: { type: "ephemeral" } }] }
68
+ : { role: "system", content: system },
69
+ );
70
+ }
71
+ for (const m of normaliseHistory(messages)) {
72
+ if (m.role === "assistant") {
73
+ const text = m.content.filter((b) => b.type === "text").map((b) => b.text).join("");
74
+ const calls = m.content
75
+ .filter((b) => b.type === "tool_use")
76
+ .map((b) => ({
77
+ id: b.id,
78
+ type: "function",
79
+ function: {
80
+ name: b.name,
81
+ // A call whose JSON never parsed is echoed as the model sent it, so
82
+ // the model sees its own mistake next to the error result.
83
+ arguments: b.inputError && typeof b.rawInput === "string" ? b.rawInput : JSON.stringify(b.input ?? {}),
84
+ },
85
+ }));
86
+ /** @type {any} */
87
+ const msg = { role: "assistant", content: text === "" ? null : text };
88
+ if (calls.length > 0) msg.tool_calls = calls;
89
+ if (msg.content === null && calls.length === 0) continue;
90
+ Object.assign(msg, reasoningFields(m.content));
91
+ out.push(msg);
92
+ } else {
93
+ /** @type {Array<{mediaType:string, data:string}>} */
94
+ const attached = [];
95
+ for (const b of m.content) {
96
+ if (b.type === "tool_result") {
97
+ let content = b.is_error ? `Error: ${b.content}` : b.content;
98
+ const imgs = Array.isArray(b.images) ? b.images : [];
99
+ if (imgs.length > 0 && images) attached.push(...imgs);
100
+ else if (imgs.length > 0) content = `${content}${imageFallbackNote(imgs.length)}`;
101
+ out.push({ role: "tool", tool_call_id: b.tool_use_id, content });
102
+ }
103
+ }
104
+ const text = m.content.filter((b) => b.type === "text").map((b) => b.text).join("\n\n");
105
+ if (attached.length > 0) {
106
+ out.push({
107
+ role: "user",
108
+ content: [
109
+ { type: "text", text: text !== "" ? text : "Images returned by the tool calls above:" },
110
+ ...attached.map((i) => ({ type: "image_url", image_url: { url: `data:${i.mediaType};base64,${i.data}` } })),
111
+ ],
112
+ });
113
+ } else if (text !== "") out.push({ role: "user", content: text });
114
+ }
115
+ }
116
+ if (cacheBreakpoints) {
117
+ const last = out[out.length - 1];
118
+ if (last && last.role === "user" && typeof last.content === "string") {
119
+ out[out.length - 1] = { role: "user", content: [{ type: "text", text: last.content, cache_control: { type: "ephemeral" } }] };
120
+ }
121
+ }
122
+ /** @type {any} */
123
+ const body = {
124
+ model,
125
+ messages: out,
126
+ max_tokens: maxTokens,
127
+ stream: true,
128
+ stream_options: { include_usage: true },
129
+ };
130
+ if (reasoningEffort) body.reasoning_effort = reasoningEffort; // wire/effort.mjs decides when
131
+ if (tools.length > 0) {
132
+ body.tools = tools.map((t) => ({
133
+ type: "function",
134
+ function: { name: t.name, description: t.description, parameters: t.input_schema },
135
+ }));
136
+ body.tool_choice = "auto";
137
+ }
138
+ if (promptCacheKey) body.prompt_cache_key = promptCacheKey;
139
+ return body;
140
+ }
141
+
142
+ /**
143
+ * CF-72: an assistant turn's thinking blocks as the OpenAI wire's reasoning
144
+ * fields. `reasoning_content` is the thinking text; `reasoning_signature` (the
145
+ * gateway's carrier for an Anthropic-wire leg's thinking signature) is echoed
146
+ * beside it, or that leg's reasoning is dropped on replay. The wire has one
147
+ * field of each per message: texts are joined and the last signature wins.
148
+ * Neither field is sent when the turn has no thinking. Pure.
149
+ * @param {any[]} content
150
+ * @returns {{reasoning_content?:string, reasoning_signature?:string}}
151
+ */
152
+ export function reasoningFields(content) {
153
+ const blocks = content.filter((b) => b && b.type === "thinking");
154
+ if (blocks.length === 0) return {};
155
+ const reasoning = blocks.map((b) => (typeof b.thinking === "string" ? b.thinking : "")).join("");
156
+ const signature = blocks.reduce((s, b) => (typeof b.signature === "string" && b.signature !== "" ? b.signature : s), "");
157
+ if (reasoning === "" && signature === "") return {};
158
+ return signature === "" ? { reasoning_content: reasoning } : { reasoning_content: reasoning, reasoning_signature: signature };
159
+ }
160
+
161
+ /** The text a tool result carries when its images cannot be sent on this connection. @param {number} n */
162
+ export function imageFallbackNote(n) {
163
+ return `\n[${n === 1 ? "1 image was" : `${n} images were`} not shown: this model connection does not accept images]`;
164
+ }
165
+
166
+ const FINISH_TO_STOP = {
167
+ stop: "end_turn",
168
+ tool_calls: "tool_use",
169
+ function_call: "tool_use",
170
+ length: "max_tokens",
171
+ content_filter: "refusal",
172
+ };
173
+
174
+ /**
175
+ * Accumulate one streamed completion into a canonical assistant turn.
176
+ * @param {{ now?: () => number }} [opts]
177
+ */
178
+ export function createOpenAIAccumulator(opts = {}) {
179
+ const now = opts.now || Date.now;
180
+ let text = "";
181
+ // CF-72: reasoning text and its signature, each accumulated across deltas.
182
+ let reasoning = "";
183
+ let signature = "";
184
+ /** @type {Map<number, {id:string, name:string, args:string}>} */
185
+ const calls = new Map();
186
+ let finish = null;
187
+ let usage = null;
188
+ let model = null;
189
+ let sawChunk = false;
190
+ let sawDone = false;
191
+
192
+ return {
193
+ /** @param {import('./sse.mjs').SseFrame} frame */
194
+ onFrame(frame) {
195
+ if (frame.data === "[DONE]") {
196
+ sawDone = true;
197
+ return { ok: true, done: true };
198
+ }
199
+ let chunk;
200
+ try {
201
+ chunk = JSON.parse(frame.data);
202
+ } catch {
203
+ return { ok: false, error: protocolError(`unparseable stream chunk: ${frame.data.slice(0, 120)}`) };
204
+ }
205
+ if (chunk && chunk.error) return { ok: false, error: classifyStreamError(chunk, null, now()) };
206
+ sawChunk = true;
207
+ if (typeof chunk.model === "string") model = chunk.model;
208
+ if (chunk.usage && typeof chunk.usage === "object") usage = chunk.usage;
209
+ for (const choice of Array.isArray(chunk.choices) ? chunk.choices : []) {
210
+ // A non-stream-shaped choice carries `message` instead of `delta`; its fields read the same.
211
+ const d = choice.delta || choice.message || {};
212
+ if (typeof d.content === "string") text += d.content;
213
+ if (typeof d.reasoning_content === "string") reasoning += d.reasoning_content;
214
+ if (typeof d.reasoning_signature === "string") signature += d.reasoning_signature;
215
+ for (const tc of Array.isArray(d.tool_calls) ? d.tool_calls : []) {
216
+ const idx = Number.isInteger(tc.index) ? tc.index : calls.size;
217
+ const cur = calls.get(idx) || { id: "", name: "", args: "" };
218
+ if (typeof tc.id === "string" && tc.id) cur.id = tc.id;
219
+ if (tc.function) {
220
+ if (typeof tc.function.name === "string") cur.name += tc.function.name;
221
+ if (typeof tc.function.arguments === "string") cur.args += tc.function.arguments;
222
+ }
223
+ calls.set(idx, cur);
224
+ }
225
+ if (typeof choice.finish_reason === "string" && choice.finish_reason) finish = choice.finish_reason;
226
+ }
227
+ return { ok: true };
228
+ },
229
+
230
+ finish() {
231
+ if (!sawChunk || (finish === null && !sawDone)) {
232
+ return { ok: false, error: protocolError("stream ended before the completion finished") };
233
+ }
234
+ /** @type {any[]} */
235
+ const content = [];
236
+ // Thinking comes first, as on the Anthropic wire; the signature is kept only when one arrived.
237
+ if (reasoning !== "" || signature !== "") content.push(signature === "" ? { type: "thinking", thinking: reasoning } : { type: "thinking", thinking: reasoning, signature });
238
+ if (text !== "") content.push({ type: "text", text });
239
+ const ordered = [...calls.entries()].sort((a, b) => a[0] - b[0]);
240
+ ordered.forEach(([idx, c]) => {
241
+ const block = parseToolArguments(c.args);
242
+ content.push({ type: "tool_use", id: c.id || `call_${idx}`, name: c.name, ...block });
243
+ });
244
+ // A length stop is kept even when tool calls exist: their arguments may be
245
+ // cut off, and the loop must not run them (it refuses the whole turn).
246
+ const stopReason = finish === "length" ? "max_tokens" : calls.size > 0 ? "tool_use" : (FINISH_TO_STOP[finish] ?? "end_turn");
247
+ return {
248
+ ok: true,
249
+ value: {
250
+ message: { role: "assistant", content },
251
+ stopReason,
252
+ usage: usage ? normaliseOpenAIUsage(usage) : emptyUsage("estimated"),
253
+ model,
254
+ },
255
+ };
256
+ },
257
+ };
258
+ }
259
+
260
+ /**
261
+ * @param {string} raw
262
+ * @returns {{input: object, inputError?: string, rawInput?: string}}
263
+ */
264
+ export function parseToolArguments(raw) {
265
+ const s = (raw ?? "").trim();
266
+ if (s === "") return { input: {} };
267
+ try {
268
+ const v = JSON.parse(s);
269
+ if (v && typeof v === "object" && !Array.isArray(v)) return { input: v };
270
+ return { input: {}, inputError: "tool arguments must be a JSON object", rawInput: raw };
271
+ } catch (e) {
272
+ return { input: {}, inputError: e instanceof Error ? e.message : String(e), rawInput: raw };
273
+ }
274
+ }
275
+
276
+ /**
277
+ * One model turn over the OpenAI wire.
278
+ *
279
+ * @param {object} p
280
+ * @param {string} p.baseUrl
281
+ * @param {string|((o:{refresh:boolean})=>string|Promise<string>)} p.token
282
+ * @param {string} p.model
283
+ * @param {string} p.system
284
+ * @param {any[]} p.messages
285
+ * @param {any[]} [p.tools]
286
+ * @param {number} [p.maxTokens]
287
+ * @param {boolean} [p.cacheBreakpoints]
288
+ * @param {string|null} [p.promptCacheKey]
289
+ * @param {boolean} [p.images]
290
+ * @param {Record<string,string>} [p.headers] attribution headers (x-cohort-*)
291
+ * @param {'low'|'medium'|'high'|null} [p.reasoningEffort] sent as reasoning_effort
292
+ * @param {(frame:any)=>void} [p.onFrame] observer of each stream frame (stream-json partials)
293
+ * @param {AbortSignal} [p.signal]
294
+ * @param {typeof fetch} [p.fetchImpl]
295
+ * @param {number} [p.maxRetries]
296
+ * @param {() => number} [p.now]
297
+ * @param {(ms:number, s?:AbortSignal)=>Promise<void>} [p.sleep]
298
+ * @param {number} [p.idleTimeoutMs] silence budget between SSE events (CF-156); 0 disables
299
+ * @param {number} [p.slowStreamMs] a completed stream slower than this is recorded
300
+ * @param {(d: import('./stall.mjs').StreamDiagnostic) => void} [p.onStreamDiagnostic]
301
+ */
302
+ export async function callOpenAIChat(p) {
303
+ const body = buildOpenAIRequest({
304
+ model: p.model,
305
+ system: p.system,
306
+ messages: p.messages,
307
+ tools: p.tools,
308
+ maxTokens: p.maxTokens ?? 16000,
309
+ cacheBreakpoints: p.cacheBreakpoints,
310
+ reasoningEffort: p.reasoningEffort ?? null,
311
+ promptCacheKey: p.promptCacheKey ?? null,
312
+ images: p.images ?? false,
313
+ });
314
+ return postStream({
315
+ url: joinUrl(p.baseUrl, OPENAI_CHAT_ROUTE),
316
+ headers: { ...(p.headers || {}) },
317
+ body,
318
+ token: p.token,
319
+ createAccumulator: () => createOpenAIAccumulator({ now: p.now }),
320
+ onFrame: p.onFrame,
321
+ signal: p.signal,
322
+ fetchImpl: p.fetchImpl,
323
+ maxRetries: p.maxRetries,
324
+ now: p.now,
325
+ sleep: p.sleep,
326
+ idleTimeoutMs: p.idleTimeoutMs,
327
+ slowStreamMs: p.slowStreamMs,
328
+ onStreamDiagnostic: p.onStreamDiagnostic,
329
+ wire: "openai",
330
+ tier: p.model,
331
+ });
332
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * lib/engine/wire/prompt-cache.mjs — when to send `prompt_cache_key`.
3
+ *
4
+ * `prompt_cache_key` routes requests that share a prefix to the same cache on
5
+ * the OpenAI wire. A gateway or supplier that does not know the field may
6
+ * reject it, so the engine sends it only when:
7
+ *
8
+ * · mode `on` — configuration says the gateway supports it
9
+ * (COHORT_LLM_PROMPT_CACHE_KEY=1, or settings
10
+ * `cohort.promptCacheKey: true`);
11
+ * · mode `auto` — the default: from the first response that carries
12
+ * `x-cohort-prompt-cache-key-supported: 1` (or `true`) on;
13
+ * · mode `off` — never (COHORT_LLM_PROMPT_CACHE_KEY=0, or `false`).
14
+ *
15
+ * The key itself never changes within a run, so turning it on changes no
16
+ * prompt bytes; only routing.
17
+ *
18
+ * @module lib/engine/wire/prompt-cache
19
+ */
20
+
21
+ import { header } from "./errors.mjs";
22
+
23
+ export const PROMPT_CACHE_KEY_HEADER = "x-cohort-prompt-cache-key-supported";
24
+
25
+ /**
26
+ * @param {unknown} envValue COHORT_LLM_PROMPT_CACHE_KEY
27
+ * @param {unknown} settingValue settings cohort.promptCacheKey
28
+ * @returns {'on'|'off'|'auto'}
29
+ */
30
+ export function promptCacheKeyMode(envValue, settingValue) {
31
+ const e = String(envValue ?? "").trim().toLowerCase();
32
+ if (["1", "true", "on", "yes"].includes(e)) return "on";
33
+ if (["0", "false", "off", "no"].includes(e)) return "off";
34
+ if (settingValue === true) return "on";
35
+ if (settingValue === false) return "off";
36
+ return "auto";
37
+ }
38
+
39
+ /** @param {string|null} v */
40
+ function truthy(v) {
41
+ return v != null && ["1", "true", "yes"].includes(String(v).trim().toLowerCase());
42
+ }
43
+
44
+ /**
45
+ * @param {{key:string, mode:'on'|'off'|'auto'}} p
46
+ */
47
+ export function createPromptCacheKeyState({ key, mode }) {
48
+ let supported = mode === "on";
49
+ return {
50
+ key,
51
+ mode,
52
+ /** The key to send on the next request, or null. */
53
+ current: () => (mode !== "off" && supported ? key : null),
54
+ /** @param {any} headers */
55
+ observe(headers) {
56
+ if (mode === "auto" && !supported && truthy(header(headers, PROMPT_CACHE_KEY_HEADER))) supported = true;
57
+ },
58
+ get supported() {
59
+ return supported;
60
+ },
61
+ };
62
+ }
63
+
64
+ /**
65
+ * A fetch that reports every response's headers to the state.
66
+ * @param {typeof fetch|undefined} fetchImpl
67
+ * @param {{observe:(h:any)=>void}} state
68
+ * @returns {typeof fetch}
69
+ */
70
+ export function observingFetch(fetchImpl, state) {
71
+ const f = fetchImpl ?? globalThis.fetch;
72
+ return /** @type {typeof fetch} */ (
73
+ async (url, init) => {
74
+ const res = await f(url, init);
75
+ state.observe(res.headers);
76
+ return res;
77
+ }
78
+ );
79
+ }
@@ -0,0 +1,140 @@
1
+ /**
2
+ * lib/engine/wire/search.mjs — the gateway's web search endpoint (client).
3
+ *
4
+ * Contract (cohort-llm, owned with the gateway; WebSearch is its only caller):
5
+ *
6
+ * POST <base>/cohort/v1/search
7
+ * Authorization: Bearer <seat token>
8
+ * body {"query":"…","maxResults":8,"allowedDomains":["…"],"blockedDomains":["…"]}
9
+ * 200 {"results":[{"title":"…","url":"…","snippet":"…"}],"costMicros":"1200"}
10
+ * (costMicros: decimal integer string of micro-USD, or a safe integer;
11
+ * `x-cohort-request-id` and `x-cohort-cost-micros` headers as on every route)
12
+ * 404 the gateway has no search → the tool reports search as unavailable
13
+ * 4xx/5xx refusals use the §4.6/§4.8 error bodies
14
+ *
15
+ * Expected failures are returned values. A 401 is retried once with a
16
+ * refreshed token when the token is a function; nothing else is retried
17
+ * (a search is cheap to ask for again, and a refusal must not be hammered).
18
+ *
19
+ * @module lib/engine/wire/search
20
+ */
21
+
22
+ import { classifyHttpError, classifyTransportError, tokenFailureError, header } from "./errors.mjs";
23
+
24
+ export const SEARCH_ROUTE = "/cohort/v1/search";
25
+ export const DEFAULT_SEARCH_TIMEOUT_MS = 30_000;
26
+ export const MAX_SEARCH_RESULTS = 20;
27
+
28
+ /** @param {string} baseUrl */
29
+ export function searchUrl(baseUrl) {
30
+ let base = String(baseUrl || "").trim().replace(/\/+$/, "");
31
+ if (base.endsWith("/v1")) base = base.slice(0, -3);
32
+ return `${base}${SEARCH_ROUTE}`;
33
+ }
34
+
35
+ /** @param {unknown} v @returns {string|null} */
36
+ function micros(v) {
37
+ if (typeof v === "number") return Number.isSafeInteger(v) && v >= 0 ? String(v) : null;
38
+ return typeof v === "string" && /^\d+$/.test(v.trim()) ? v.trim() : null;
39
+ }
40
+
41
+ /**
42
+ * Validate the 200 body (pure).
43
+ * @param {unknown} body
44
+ * @returns {{ok:true, results:Array<{title:string, url:string, snippet:string}>, costMicros:string|null}|{ok:false, message:string}}
45
+ */
46
+ export function parseSearchBody(body) {
47
+ if (!body || typeof body !== "object" || !Array.isArray(/** @type any */ (body).results)) return { ok: false, message: "the search response has no results array" };
48
+ const b = /** @type any */ (body);
49
+ const results = b.results
50
+ .filter((r) => r && typeof r === "object" && typeof r.url === "string" && r.url !== "")
51
+ .map((r) => ({ title: typeof r.title === "string" ? r.title : r.url, url: r.url, snippet: typeof r.snippet === "string" ? r.snippet : "" }));
52
+ return { ok: true, results, costMicros: micros(b.costMicros) };
53
+ }
54
+
55
+ /**
56
+ * @param {object} p
57
+ * @param {string} p.baseUrl
58
+ * @param {string|((o:{refresh:boolean})=>string|Promise<string>)} p.token
59
+ * @param {string} p.query
60
+ * @param {number} [p.maxResults]
61
+ * @param {string[]} [p.allowedDomains]
62
+ * @param {string[]} [p.blockedDomains]
63
+ * @param {Record<string,string>} [p.headers]
64
+ * @param {typeof fetch} [p.fetchImpl]
65
+ * @param {AbortSignal} [p.signal]
66
+ * @param {number} [p.timeoutMs]
67
+ * @param {() => number} [p.now]
68
+ * @returns {Promise<{ok:true, results:Array<{title:string,url:string,snippet:string}>, cohort:{requestId:string|null, costMicros:string|null}}
69
+ * | {ok:false, error:{kind:string, code:string, status:number|null, message:string}, cohort:{requestId:string|null, costMicros:string|null}|null, accepted:boolean}>}
70
+ */
71
+ export async function searchWeb(p) {
72
+ const fetchImpl = p.fetchImpl ?? globalThis.fetch;
73
+ const now = p.now ?? Date.now;
74
+ const body = {
75
+ query: p.query,
76
+ maxResults: Math.min(MAX_SEARCH_RESULTS, Math.max(1, p.maxResults ?? 8)),
77
+ allowedDomains: p.allowedDomains ?? [],
78
+ blockedDomains: p.blockedDomains ?? [],
79
+ };
80
+ const controller = new AbortController();
81
+ const onAbort = () => controller.abort();
82
+ if (p.signal?.aborted) controller.abort();
83
+ else p.signal?.addEventListener("abort", onAbort, { once: true });
84
+ const timer = setTimeout(() => controller.abort(), p.timeoutMs ?? DEFAULT_SEARCH_TIMEOUT_MS);
85
+ try {
86
+ let refresh = false;
87
+ /** @type {string|undefined} */
88
+ let rejected;
89
+ for (let attempt = 0; attempt < 2; attempt++) {
90
+ let token;
91
+ try {
92
+ token = typeof p.token === "function" ? await p.token(refresh ? { refresh, rejected } : { refresh }) : p.token;
93
+ } catch (e) {
94
+ // token_unavailable, or a retryable cohort_unavailable when the token endpoint was down (CF-143).
95
+ return { ok: false, error: tokenFailureError(e), cohort: null, accepted: false };
96
+ }
97
+ let res;
98
+ try {
99
+ res = await fetchImpl(searchUrl(p.baseUrl), {
100
+ method: "POST",
101
+ headers: { "content-type": "application/json", accept: "application/json", authorization: `Bearer ${token}`, ...(p.headers ?? {}) },
102
+ body: JSON.stringify(body),
103
+ signal: controller.signal,
104
+ });
105
+ } catch (e) {
106
+ const err = classifyTransportError(e, controller.signal);
107
+ return { ok: false, error: { kind: err.kind, code: err.code, status: null, message: err.message }, cohort: null, accepted: false };
108
+ }
109
+ const cohort = { requestId: header(res.headers, "x-cohort-request-id"), costMicros: micros(header(res.headers, "x-cohort-cost-micros")) };
110
+ if (res.status === 401 && typeof p.token === "function" && !refresh) {
111
+ await res.body?.cancel().catch(() => {});
112
+ refresh = true;
113
+ rejected = token;
114
+ continue;
115
+ }
116
+ if (res.status === 404) {
117
+ await res.body?.cancel().catch(() => {});
118
+ return { ok: false, error: { kind: "unavailable", code: "search_unavailable", status: 404, message: "the gateway does not offer web search" }, cohort, accepted: false };
119
+ }
120
+ const text = await res.text().catch(() => "");
121
+ if (!res.ok) {
122
+ const err = classifyHttpError(res.status, res.headers, text, now());
123
+ return { ok: false, error: { kind: err.kind, code: err.code, status: res.status, message: err.message }, cohort, accepted: false };
124
+ }
125
+ let json;
126
+ try {
127
+ json = JSON.parse(text);
128
+ } catch {
129
+ return { ok: false, error: { kind: "protocol", code: "search_protocol_error", status: res.status, message: "the search response was not JSON" }, cohort, accepted: true };
130
+ }
131
+ const parsed = parseSearchBody(json);
132
+ if (!parsed.ok) return { ok: false, error: { kind: "protocol", code: "search_protocol_error", status: res.status, message: parsed.message }, cohort, accepted: true };
133
+ return { ok: true, results: parsed.results.slice(0, body.maxResults), cohort: { requestId: cohort.requestId, costMicros: parsed.costMicros ?? cohort.costMicros } };
134
+ }
135
+ return { ok: false, error: { kind: "auth", code: "http_401", status: 401, message: "the gateway refused the refreshed token" }, cohort: null, accepted: false };
136
+ } finally {
137
+ clearTimeout(timer);
138
+ p.signal?.removeEventListener("abort", onAbort);
139
+ }
140
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * lib/engine/wire/sse.mjs — a small, strict Server-Sent Events reader.
3
+ *
4
+ * Both gateway wires stream as SSE: OpenAI Chat sends bare `data:` frames and a
5
+ * terminal `data: [DONE]`; Anthropic Messages sends named `event:` frames. One
6
+ * parser serves both. It follows the WHATWG event-stream grammar closely enough
7
+ * for an LLM gateway: LF / CRLF / CR line endings, multi-line `data:` joined
8
+ * with "\n", `:` comment lines (the gateway's 15 s keep-alive pings) ignored,
9
+ * and a frame dispatched on a blank line. `id:` / `retry:` are read and
10
+ * surfaced but never acted on — reconnecting a half-streamed completion would
11
+ * double-bill, so the engine does not.
12
+ *
13
+ * The chunk → frame split is a pure function (`createSseDecoder`) so it can be
14
+ * tested with adversarial chunk boundaries without a socket; `readSse` is the
15
+ * thin async-iterator edge over a fetch body.
16
+ *
17
+ * @module lib/engine/wire/sse
18
+ */
19
+
20
+ /**
21
+ * @typedef {{ event: string, data: string, id?: string }} SseFrame
22
+ */
23
+
24
+ /**
25
+ * Create an incremental decoder. Feed it text chunks; it returns the frames
26
+ * completed by each chunk. Holds at most one partial line and one partial frame.
27
+ *
28
+ * @returns {{ push: (text: string) => SseFrame[], end: () => SseFrame[] }}
29
+ */
30
+ export function createSseDecoder() {
31
+ let buffer = "";
32
+ let dataLines = [];
33
+ let eventName = "";
34
+ let lastId;
35
+ let sawField = false;
36
+
37
+ function dispatch(out) {
38
+ if (!sawField) return;
39
+ if (dataLines.length > 0) {
40
+ const frame = { event: eventName || "message", data: dataLines.join("\n") };
41
+ if (lastId !== undefined) frame.id = lastId;
42
+ out.push(frame);
43
+ }
44
+ dataLines = [];
45
+ eventName = "";
46
+ sawField = false;
47
+ }
48
+
49
+ function line(raw, out) {
50
+ if (raw === "") return dispatch(out);
51
+ if (raw.startsWith(":")) return; // comment / keep-alive ping
52
+ const colon = raw.indexOf(":");
53
+ const field = colon === -1 ? raw : raw.slice(0, colon);
54
+ let value = colon === -1 ? "" : raw.slice(colon + 1);
55
+ if (value.startsWith(" ")) value = value.slice(1);
56
+ sawField = true;
57
+ if (field === "data") dataLines.push(value);
58
+ else if (field === "event") eventName = value;
59
+ else if (field === "id") lastId = value;
60
+ // `retry` and unknown fields are ignored by design (see header).
61
+ }
62
+
63
+ return {
64
+ push(text) {
65
+ const out = [];
66
+ buffer += text;
67
+ // Split on any of CRLF / LF / CR. A trailing lone CR might be the first
68
+ // half of a CRLF split across chunks, so keep it buffered.
69
+ let start = 0;
70
+ for (let i = 0; i < buffer.length; i++) {
71
+ const ch = buffer[i];
72
+ if (ch === "\n") {
73
+ line(buffer.slice(start, i), out);
74
+ start = i + 1;
75
+ } else if (ch === "\r") {
76
+ if (i === buffer.length - 1) break; // wait for the next chunk
77
+ line(buffer.slice(start, i), out);
78
+ if (buffer[i + 1] === "\n") i++;
79
+ start = i + 1;
80
+ }
81
+ }
82
+ buffer = buffer.slice(start);
83
+ return out;
84
+ },
85
+ end() {
86
+ const out = [];
87
+ if (buffer !== "") {
88
+ line(buffer.replace(/\r$/, ""), out);
89
+ buffer = "";
90
+ }
91
+ dispatch(out);
92
+ return out;
93
+ },
94
+ };
95
+ }
96
+
97
+ /**
98
+ * Iterate the SSE frames of a response body (a WHATWG ReadableStream, or any
99
+ * async iterable of Uint8Array / string chunks).
100
+ *
101
+ * @param {AsyncIterable<Uint8Array|string>} body
102
+ * @returns {AsyncGenerator<SseFrame>}
103
+ */
104
+ export async function* readSse(body) {
105
+ const decoder = createSseDecoder();
106
+ const text = new TextDecoder();
107
+ for await (const chunk of body) {
108
+ const str = typeof chunk === "string" ? chunk : text.decode(chunk, { stream: true });
109
+ for (const frame of decoder.push(str)) yield frame;
110
+ }
111
+ const tail = text.decode();
112
+ if (tail) for (const frame of decoder.push(tail)) yield frame;
113
+ for (const frame of decoder.end()) yield frame;
114
+ }