@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.
- package/.claude/settings.json +18 -0
- package/.env.example +18 -5
- package/README.md +1 -0
- package/bin/maestro.mjs +62 -0
- package/docs/guides/billing-console-keys.md +60 -0
- package/docs/guides/front-door-session.md +54 -9
- package/docs/guides/mac-mini.md +20 -25
- package/docs/guides/setup-wizard.md +1 -1
- package/docs/runbooks/fleet-rollout.md +156 -0
- package/docs/runbooks/mac-mini-bootstrap.md +12 -14
- package/lib/action-executor.js +19 -3
- package/lib/budget-guard.mjs +279 -3
- package/lib/channels/base-adapter.mjs +3 -1
- package/lib/channels/contract.mjs +2 -1
- package/lib/channels/inbox-item.mjs +8 -0
- package/lib/claude-bin.mjs +5 -6
- package/lib/cli/doctor-checks.mjs +141 -10
- package/lib/cli/global-setup-extras.mjs +5 -1
- package/lib/cli/inbox.mjs +100 -15
- package/lib/cli/seat-auth.mjs +463 -0
- package/lib/cli/session.mjs +80 -12
- package/lib/collective/capture.mjs +8 -6
- package/lib/collective/global-config.mjs +63 -1
- package/lib/collective/presence.mjs +142 -5
- package/lib/comms/send-gate.mjs +559 -1
- package/lib/diagnostics/alerts.mjs +49 -0
- package/lib/diagnostics/cadence-output-freshness.mjs +288 -0
- package/lib/engine/agents/definitions.mjs +343 -0
- package/lib/engine/agents/persist.mjs +275 -0
- package/lib/engine/agents/runtime.mjs +748 -0
- package/lib/engine/agents/usage.mjs +95 -0
- package/lib/engine/auth-status.mjs +139 -0
- package/lib/engine/budget.mjs +194 -0
- package/lib/engine/cli.mjs +1204 -0
- package/lib/engine/commands/index.mjs +269 -0
- package/lib/engine/context/budget.mjs +219 -0
- package/lib/engine/context/cache.mjs +125 -0
- package/lib/engine/context/child-env.mjs +215 -0
- package/lib/engine/context/compaction.mjs +342 -0
- package/lib/engine/context/images.mjs +90 -0
- package/lib/engine/context/instructions.mjs +327 -0
- package/lib/engine/context/lazy-instructions.mjs +169 -0
- package/lib/engine/context/manager.mjs +182 -0
- package/lib/engine/context/real-path.mjs +91 -0
- package/lib/engine/context/secret-values.mjs +163 -0
- package/lib/engine/context/settings.mjs +274 -0
- package/lib/engine/context/stream-input.mjs +159 -0
- package/lib/engine/guard.mjs +152 -0
- package/lib/engine/hooks.mjs +713 -0
- package/lib/engine/loop.mjs +560 -0
- package/lib/engine/mcp/client.mjs +254 -0
- package/lib/engine/mcp/config.mjs +301 -0
- package/lib/engine/mcp/http.mjs +201 -0
- package/lib/engine/mcp/index.mjs +146 -0
- package/lib/engine/mcp/jsonrpc.mjs +147 -0
- package/lib/engine/mcp/naming.mjs +66 -0
- package/lib/engine/mcp/resources.mjs +89 -0
- package/lib/engine/mcp/results.mjs +133 -0
- package/lib/engine/mcp/stdio.mjs +137 -0
- package/lib/engine/mcp/supervisor.mjs +116 -0
- package/lib/engine/messages.mjs +104 -0
- package/lib/engine/output/json.mjs +164 -0
- package/lib/engine/output/stream-json.mjs +266 -0
- package/lib/engine/permissions.mjs +845 -0
- package/lib/engine/process-identity.mjs +164 -0
- package/lib/engine/process-tree.mjs +551 -0
- package/lib/engine/prompt.mjs +60 -0
- package/lib/engine/session/store.mjs +299 -0
- package/lib/engine/session-runtime/args.mjs +97 -0
- package/lib/engine/session-runtime/host.mjs +143 -0
- package/lib/engine/session-runtime/inbox.mjs +122 -0
- package/lib/engine/session-runtime/notifications.mjs +129 -0
- package/lib/engine/session-runtime/registry.mjs +328 -0
- package/lib/engine/session-runtime/runner.mjs +344 -0
- package/lib/engine/session-runtime/socket.mjs +212 -0
- package/lib/engine/session-runtime/wakeup.mjs +115 -0
- package/lib/engine/skills/index.mjs +321 -0
- package/lib/engine/tools/bash-background.mjs +533 -0
- package/lib/engine/tools/bash.mjs +216 -0
- package/lib/engine/tools/edit.mjs +97 -0
- package/lib/engine/tools/glob.mjs +81 -0
- package/lib/engine/tools/grep.mjs +224 -0
- package/lib/engine/tools/index.mjs +84 -0
- package/lib/engine/tools/list-agents.mjs +32 -0
- package/lib/engine/tools/ls.mjs +127 -0
- package/lib/engine/tools/monitor.mjs +82 -0
- package/lib/engine/tools/notebook-edit.mjs +218 -0
- package/lib/engine/tools/read.mjs +103 -0
- package/lib/engine/tools/schedule-wakeup.mjs +45 -0
- package/lib/engine/tools/schema.mjs +144 -0
- package/lib/engine/tools/send-message.mjs +77 -0
- package/lib/engine/tools/session.mjs +70 -0
- package/lib/engine/tools/todo.mjs +144 -0
- package/lib/engine/tools/toolsearch.mjs +217 -0
- package/lib/engine/tools/walk.mjs +193 -0
- package/lib/engine/tools/web-switch.mjs +31 -0
- package/lib/engine/tools/webfetch-html.mjs +387 -0
- package/lib/engine/tools/webfetch-net.mjs +340 -0
- package/lib/engine/tools/webfetch.mjs +198 -0
- package/lib/engine/tools/websearch.mjs +91 -0
- package/lib/engine/tools/workflow.mjs +95 -0
- package/lib/engine/tools/write.mjs +76 -0
- package/lib/engine/tui/line-editor.mjs +137 -0
- package/lib/engine/tui/render.mjs +86 -0
- package/lib/engine/tui/tui.mjs +274 -0
- package/lib/engine/wire/anthropic-messages.mjs +263 -0
- package/lib/engine/wire/effort.mjs +36 -0
- package/lib/engine/wire/errors.mjs +496 -0
- package/lib/engine/wire/http.mjs +441 -0
- package/lib/engine/wire/index.mjs +76 -0
- package/lib/engine/wire/openai-chat.mjs +332 -0
- package/lib/engine/wire/prompt-cache.mjs +79 -0
- package/lib/engine/wire/search.mjs +140 -0
- package/lib/engine/wire/sse.mjs +114 -0
- package/lib/engine/wire/stall.mjs +349 -0
- package/lib/engine/wire/token-provider.mjs +175 -0
- package/lib/engine/wire/usage.mjs +192 -0
- package/lib/engine/workflow/host.mjs +524 -0
- package/lib/engine/workflow/journal.mjs +188 -0
- package/lib/engine/workflow/json-schema.mjs +171 -0
- package/lib/engine/workflow/meta.mjs +329 -0
- package/lib/engine/workflow/notifications.mjs +52 -0
- package/lib/engine/workflow/runtime.mjs +447 -0
- package/lib/engine/workflow/sandbox.mjs +534 -0
- package/lib/engine/workflow/worker.mjs +141 -0
- package/lib/engine/workflow/worktree.mjs +74 -0
- package/lib/execution/disposition.mjs +1 -1
- package/lib/execution/intake.mjs +10 -0
- package/lib/execution/surface-policy.mjs +15 -0
- package/lib/learning/curator.mjs +8 -6
- package/lib/learning/reflect.mjs +8 -6
- package/lib/model-router/catalog/cohort.yaml +137 -0
- package/lib/model-router/catalog.mjs +118 -1
- package/lib/model-router/failover.mjs +67 -16
- package/lib/model-router/llm-task.mjs +39 -3
- package/lib/model-router/resolve.mjs +89 -3
- package/lib/model-router/spawn.mjs +46 -47
- package/lib/model-router/taxonomy.mjs +126 -4
- package/lib/org/cost-sync.mjs +141 -11
- package/lib/org/inbound/broadcast.mjs +289 -0
- package/lib/org/inbound/collective.mjs +375 -0
- package/lib/org/inbound/directedness.mjs +96 -8
- package/lib/org/inbound/facts.mjs +78 -2
- package/lib/org/inbound/project.mjs +22 -0
- package/lib/org/inbound/surfaces.mjs +14 -0
- package/lib/org/llm-token.mjs +879 -0
- package/lib/org/mesh.mjs +61 -0
- package/lib/org/messaging.mjs +3 -1
- package/lib/org/protocol.checksum +1 -1
- package/lib/org/protocol.mjs +15 -0
- package/lib/org/quota.mjs +520 -0
- package/lib/org/tool-surface.mjs +104 -16
- package/lib/org/ui-parity.mjs +16 -1
- package/lib/org/work-ledger.mjs +37 -6
- package/lib/rate-guard.mjs +114 -1
- package/lib/resource-governor.mjs +41 -6
- package/lib/runtime/adapter.mjs +823 -0
- package/lib/runtime/child-env.mjs +191 -0
- package/lib/runtime/legacy-shell-guard.mjs +97 -0
- package/lib/runtime/seat-engine.mjs +162 -0
- package/lib/session/ask-ledger.mjs +271 -0
- package/lib/session/current-work.mjs +676 -0
- package/lib/session/feed-core.mjs +40 -3
- package/lib/session/launch-args.mjs +56 -4
- package/lib/session/status-summary.mjs +26 -9
- package/lib/session/upgrade-notice.mjs +42 -0
- package/lib/setup/claude-probe.mjs +117 -13
- package/lib/setup/enrich.mjs +13 -10
- package/lib/setup/sections/model.mjs +39 -13
- package/lib/telemetry/collect.mjs +208 -9
- package/lib/upgrade/ignored-drift.mjs +105 -0
- package/lib/voice/post-call-brief.mjs +30 -17
- package/package.json +13 -3
- package/plugins/maestro-skills/skills/board-work.md +5 -0
- package/plugins/maestro-skills/skills/inbound-triage.md +56 -15
- package/plugins/maestro-skills/skills/main-session.md +18 -7
- package/scripts/ci/check-tarball-fidelity.mjs +126 -2
- package/scripts/ci/run-tests.mjs +47 -19
- package/scripts/cohort-llm/api-key-helper.mjs +92 -0
- package/scripts/collective/hook-runner.mjs +29 -2
- package/scripts/continuous-monitor.sh +13 -0
- package/scripts/cost/track-claude-usage.mjs +15 -0
- package/scripts/daemon/agent-daemon.mjs +408 -20
- package/scripts/daemon/assurance.mjs +48 -12
- package/scripts/daemon/cadence-consumer.mjs +218 -68
- package/scripts/daemon/cadence-handlers.mjs +73 -4
- package/scripts/daemon/classifier.mjs +75 -26
- package/scripts/daemon/context-compiler.mjs +51 -37
- package/scripts/daemon/deliver.mjs +30 -1
- package/scripts/daemon/dispatcher.mjs +595 -149
- package/scripts/daemon/health.mjs +14 -1
- package/scripts/daemon/maestro-daemon.mjs +11 -0
- package/scripts/daemon/prompt-builder.mjs +24 -0
- package/scripts/daemon/responder.mjs +246 -79
- package/scripts/daemon/sdk-version.mjs +98 -16
- package/scripts/eval/probe-gateway.mjs +635 -0
- package/scripts/eval/replay/extract.mjs +270 -0
- package/scripts/eval/replay/grade.mjs +260 -0
- package/scripts/eval/replay/lib/config.mjs +50 -0
- package/scripts/eval/replay/lib/effects.mjs +65 -0
- package/scripts/eval/replay/lib/fixture.mjs +188 -0
- package/scripts/eval/replay/lib/judge.mjs +72 -0
- package/scripts/eval/replay/lib/redact.mjs +136 -0
- package/scripts/eval/replay/lib/sandbox.mjs +170 -0
- package/scripts/eval/replay/lib/schema-check.mjs +63 -0
- package/scripts/eval/replay/lib/transcript.mjs +76 -0
- package/scripts/eval/replay/mcp-replay-stub.mjs +101 -0
- package/scripts/eval/replay/report.mjs +185 -0
- package/scripts/eval/replay/run.mjs +404 -0
- package/scripts/fleet/rollout.mjs +1094 -0
- package/scripts/hooks/pre-send-audit.sh +36 -245
- package/scripts/hooks/pre-write-yaml-validate.mjs +275 -0
- package/scripts/hooks/validate-state-yaml.sh +190 -0
- package/scripts/huddle/huddle-llm.mjs +361 -0
- package/scripts/huddle/huddle-server.mjs +46 -121
- package/scripts/local-triggers/autoupdate.sh +448 -78
- package/scripts/local-triggers/run-trigger.sh +13 -0
- package/scripts/maintenance/pin-integrity.mjs +364 -0
- package/scripts/poll-slack-events.sh +41 -9
- package/scripts/poller/slack-socket-mode.mjs +28 -3
- package/scripts/session/supervisor.mjs +80 -13
- package/scripts/spawn-session.sh +13 -0
- package/bin/maestro.test.mjs +0 -1574
- package/lib/action-executor.test.mjs +0 -871
- package/lib/archetype.test.mjs +0 -132
- package/lib/assurance/plan-note.test.mjs +0 -234
- package/lib/assurance/room-budget.test.mjs +0 -486
- package/lib/assurance/tier.test.mjs +0 -174
- package/lib/autonomy.test.mjs +0 -66
- package/lib/backlog.test.mjs +0 -302
- package/lib/backup/policy.test.mjs +0 -305
- package/lib/budget-escalate.test.mjs +0 -232
- package/lib/budget-guard.envelope.test.mjs +0 -476
- package/lib/budget-guard.test.mjs +0 -427
- package/lib/cadence-bus-requeue.test.mjs +0 -83
- package/lib/cadence-bus-schedule.test.mjs +0 -194
- package/lib/cadence-bus.test.mjs +0 -720
- package/lib/cadences.test.mjs +0 -230
- package/lib/capability/inventory.test.mjs +0 -232
- package/lib/capability.test.mjs +0 -78
- package/lib/channels/base-adapter.test.mjs +0 -590
- package/lib/channels/channels.test.mjs +0 -371
- package/lib/channels/contract.test.mjs +0 -162
- package/lib/channels/inbox-item.test.mjs +0 -368
- package/lib/channels/orgmail/adapter.test.mjs +0 -448
- package/lib/channels/pairing.test.mjs +0 -270
- package/lib/channels/repeat-suppressor.test.mjs +0 -134
- package/lib/channels/slack-adapter.test.mjs +0 -212
- package/lib/channels/telegram-adapter.test.mjs +0 -306
- package/lib/channels/voice/adapter.test.mjs +0 -278
- package/lib/channels/whatsapp/adapter-baileys.test.mjs +0 -359
- package/lib/channels/whatsapp/baileys-typing.test.mjs +0 -154
- package/lib/charter.test.mjs +0 -89
- package/lib/claude-bin.test.mjs +0 -131
- package/lib/cli/board.test.mjs +0 -227
- package/lib/cli/design.test.mjs +0 -270
- package/lib/cli/doctor-checks.test.mjs +0 -336
- package/lib/cli/global-setup-extras.test.mjs +0 -462
- package/lib/cli/inbox.test.mjs +0 -230
- package/lib/cli/session-ack.test.mjs +0 -63
- package/lib/cli/session.test.mjs +0 -613
- package/lib/collective/capture.test.mjs +0 -121
- package/lib/collective/cards.test.mjs +0 -114
- package/lib/collective/config.test.mjs +0 -123
- package/lib/collective/global-config.test.mjs +0 -220
- package/lib/collective/global-skills.test.mjs +0 -126
- package/lib/collective/presence.test.mjs +0 -95
- package/lib/collective/recall.test.mjs +0 -116
- package/lib/collective/vendor-skills.test.mjs +0 -306
- package/lib/comms/send-gate.test.mjs +0 -770
- package/lib/comms.test.mjs +0 -41
- package/lib/context/budget.test.mjs +0 -252
- package/lib/context/history-scope.test.mjs +0 -79
- package/lib/cost/ledger-row.test.mjs +0 -183
- package/lib/design/design-md.test.mjs +0 -318
- package/lib/design/fixtures/DESIGN.golden.md +0 -238
- package/lib/design/fixtures/PRODUCT.golden.md +0 -67
- package/lib/design/fixtures/foundation.json +0 -133
- package/lib/design/refresh-gate.test.mjs +0 -144
- package/lib/design/write.test.mjs +0 -241
- package/lib/diagnostics/alerts.test.mjs +0 -318
- package/lib/diagnostics/backup-freshness.test.mjs +0 -185
- package/lib/diagnostics/counters.test.mjs +0 -206
- package/lib/diagnostics/events.test.mjs +0 -290
- package/lib/diagnostics/otel.test.mjs +0 -196
- package/lib/diagnostics/trace.test.mjs +0 -251
- package/lib/env-compat.test.mjs +0 -104
- package/lib/execution/disposition.test.mjs +0 -553
- package/lib/execution/drive.test.mjs +0 -270
- package/lib/execution/effects.test.mjs +0 -344
- package/lib/execution/intake.test.mjs +0 -389
- package/lib/execution/journal.test.mjs +0 -261
- package/lib/execution/match.test.mjs +0 -235
- package/lib/execution/pipeline.test.mjs +0 -392
- package/lib/execution/route.test.mjs +0 -186
- package/lib/execution/surface-policy.test.mjs +0 -162
- package/lib/fs-atomic.test.mjs +0 -72
- package/lib/fs-ownership.test.mjs +0 -158
- package/lib/goals/admission.test.mjs +0 -164
- package/lib/goals/classify.test.mjs +0 -167
- package/lib/goals/collaborate.test.mjs +0 -336
- package/lib/goals/gaps.test.mjs +0 -284
- package/lib/goals/loop.test.mjs +0 -845
- package/lib/hooks/bus.test.mjs +0 -387
- package/lib/identity/persona.test.mjs +0 -142
- package/lib/kpi-sensors.test.mjs +0 -278
- package/lib/kpi.test.mjs +0 -244
- package/lib/learning/config.test.mjs +0 -75
- package/lib/learning/counters.test.mjs +0 -69
- package/lib/learning/curator-consolidate.test.mjs +0 -238
- package/lib/learning/curator.test.mjs +0 -106
- package/lib/learning/reflect.test.mjs +0 -0
- package/lib/learning/session-index.test.mjs +0 -125
- package/lib/learning/skill-writer.test.mjs +0 -210
- package/lib/mandate/audit.test.mjs +0 -195
- package/lib/mandate/contract.test.mjs +0 -185
- package/lib/mandate/derive.test.mjs +0 -274
- package/lib/mandate/model.test.mjs +0 -164
- package/lib/mandate/refresh.test.mjs +0 -389
- package/lib/mcp/server.test.mjs +0 -426
- package/lib/model-router/auth-profiles.test.mjs +0 -580
- package/lib/model-router/catalog.test.mjs +0 -385
- package/lib/model-router/economics.test.mjs +0 -438
- package/lib/model-router/failover.test.mjs +0 -439
- package/lib/model-router/health.test.mjs +0 -338
- package/lib/model-router/integration-coverage.test.mjs +0 -831
- package/lib/model-router/integration.test.mjs +0 -564
- package/lib/model-router/ledger.test.mjs +0 -415
- package/lib/model-router/llm-task.test.mjs +0 -392
- package/lib/model-router/org-credentials.test.mjs +0 -265
- package/lib/model-router/pricing-refresh.test.mjs +0 -286
- package/lib/model-router/reconcile.test.mjs +0 -316
- package/lib/model-router/repair.test.mjs +0 -180
- package/lib/model-router/spawn.test.mjs +0 -446
- package/lib/model-router/taxonomy.test.mjs +0 -410
- package/lib/model-router.test.mjs +0 -1207
- package/lib/org/activity.test.mjs +0 -134
- package/lib/org/approvals.test.mjs +0 -216
- package/lib/org/awareness.test.mjs +0 -159
- package/lib/org/board-mine-cache.test.mjs +0 -53
- package/lib/org/board.test.mjs +0 -187
- package/lib/org/bootstrap-context.test.mjs +0 -153
- package/lib/org/client.test.mjs +0 -1206
- package/lib/org/cohort-client.test.mjs +0 -126
- package/lib/org/cost-sync.test.mjs +0 -153
- package/lib/org/doctor.test.mjs +0 -346
- package/lib/org/engagement-ledger.test.mjs +0 -112
- package/lib/org/engagement.test.mjs +0 -739
- package/lib/org/handoff.test.mjs +0 -269
- package/lib/org/inbound/directedness.test.mjs +0 -668
- package/lib/org/inbound/facts.test.mjs +0 -471
- package/lib/org/inbound/hydrate.test.mjs +0 -908
- package/lib/org/inbound/index.test.mjs +0 -429
- package/lib/org/inbound/project.test.mjs +0 -287
- package/lib/org/integration-tools.test.mjs +0 -160
- package/lib/org/keys.test.mjs +0 -92
- package/lib/org/knowledge.test.mjs +0 -326
- package/lib/org/leases.test.mjs +0 -235
- package/lib/org/mesh-directives.test.mjs +0 -110
- package/lib/org/mesh-integration.test.mjs +0 -127
- package/lib/org/mesh.test.mjs +0 -400
- package/lib/org/messaging.test.mjs +0 -471
- package/lib/org/param-contract.test.mjs +0 -477
- package/lib/org/policy.test.mjs +0 -237
- package/lib/org/protocol.checksum.test.mjs +0 -90
- package/lib/org/protocol.test.mjs +0 -323
- package/lib/org/push.test.mjs +0 -792
- package/lib/org/registry.test.mjs +0 -100
- package/lib/org/resource-tools.test.mjs +0 -361
- package/lib/org/tool-access.test.mjs +0 -144
- package/lib/org/tool-surface-integration.test.mjs +0 -120
- package/lib/org/tool-surface.test.mjs +0 -1268
- package/lib/org/typing.test.mjs +0 -291
- package/lib/org/ui-parity.test.mjs +0 -560
- package/lib/org/verify.test.mjs +0 -194
- package/lib/org/work-ledger.test.mjs +0 -273
- package/lib/plan/adoption-e2e.test.mjs +0 -366
- package/lib/plan/budget-enforcement.test.mjs +0 -400
- package/lib/plan/compile.test.mjs +0 -382
- package/lib/plan/emit.test.mjs +0 -269
- package/lib/plan/explain.test.mjs +0 -188
- package/lib/prompts/parallelism.test.mjs +0 -177
- package/lib/rag/rag.test.mjs +0 -505
- package/lib/rate-guard.test.mjs +0 -272
- package/lib/reactive-gate.test.mjs +0 -57
- package/lib/render.test.mjs +0 -68
- package/lib/resource-governor.test.mjs +0 -488
- package/lib/scheduling/dynamic-jobs.test.mjs +0 -344
- package/lib/scheduling/jitter.test.mjs +0 -140
- package/lib/secrets/broker.test.mjs +0 -280
- package/lib/secrets/providers.test.mjs +0 -274
- package/lib/security/audit-engine.test.mjs +0 -424
- package/lib/security/coerce-args.test.mjs +0 -281
- package/lib/security/dangerous-tools.test.mjs +0 -68
- package/lib/security/external-content.test.mjs +0 -84
- package/lib/security/redact.test.mjs +0 -441
- package/lib/security/secret-equal.test.mjs +0 -55
- package/lib/session/config.test.mjs +0 -92
- package/lib/session/feed-core.test.mjs +0 -198
- package/lib/session/first-run.test.mjs +0 -121
- package/lib/session/frontdoor.test.mjs +0 -205
- package/lib/session/handoffs.test.mjs +0 -183
- package/lib/session/identity.test.mjs +0 -180
- package/lib/session/inbox-claims.test.mjs +0 -286
- package/lib/session/launch-args.test.mjs +0 -157
- package/lib/session/liveness.test.mjs +0 -100
- package/lib/session/status-summary.test.mjs +0 -118
- package/lib/session-permissions.test.mjs +0 -120
- package/lib/setup/claude-probe.test.mjs +0 -187
- package/lib/setup/completeness.test.mjs +0 -110
- package/lib/setup/context-pack.test.mjs +0 -89
- package/lib/setup/enrich.test.mjs +0 -115
- package/lib/setup/enroll-from-cohort.test.mjs +0 -300
- package/lib/setup/integration.test.mjs +0 -162
- package/lib/setup/io.test.mjs +0 -77
- package/lib/setup/runner.test.mjs +0 -132
- package/lib/setup/sections/identity.test.mjs +0 -234
- package/lib/setup/sections/inventory.test.mjs +0 -198
- package/lib/setup/sections/learning.test.mjs +0 -81
- package/lib/setup/sections/mandate.test.mjs +0 -388
- package/lib/setup/sections/messaging.test.mjs +0 -127
- package/lib/setup/sections/model.test.mjs +0 -240
- package/lib/setup/sections/org.test.mjs +0 -346
- package/lib/setup/sections/orgmail.test.mjs +0 -118
- package/lib/setup/sections/recovery.test.mjs +0 -98
- package/lib/setup/sections/subagents.test.mjs +0 -429
- package/lib/setup/sections/verify.test.mjs +0 -175
- package/lib/setup/sot.test.mjs +0 -81
- package/lib/setup/state.test.mjs +0 -115
- package/lib/singleton.test.mjs +0 -151
- package/lib/subagents/cli.test.mjs +0 -389
- package/lib/subagents/client.test.mjs +0 -309
- package/lib/subagents/gap.test.mjs +0 -234
- package/lib/subagents/lock.test.mjs +0 -248
- package/lib/subagents/manifest.test.mjs +0 -175
- package/lib/subagents/refs.test.mjs +0 -204
- package/lib/subagents/resolve.test.mjs +0 -422
- package/lib/subagents/schema.test.mjs +0 -328
- package/lib/telemetry/alerts.test.mjs +0 -109
- package/lib/telemetry/collect.test.mjs +0 -1274
- package/lib/tool-definitions-integration.test.mjs +0 -83
- package/lib/tool-definitions.test.mjs +0 -437
- package/lib/upgrade/global-refresh.test.mjs +0 -65
- package/lib/upgrade/launchd-reconcile.test.mjs +0 -272
- package/lib/upgrade/post-steps.test.mjs +0 -200
- package/lib/upgrade/verify.test.mjs +0 -164
- package/lib/util/fetch-timeout.test.mjs +0 -202
- package/lib/util/reconnect.test.mjs +0 -369
- package/lib/util/unhandled.test.mjs +0 -216
- package/lib/voice/outbound.test.mjs +0 -69
- package/lib/voice/session-rotation.test.mjs +0 -114
- package/lib/voice/stt.test.mjs +0 -226
- package/lib/voice/voice.test.mjs +0 -990
- package/scripts/cadence/enqueue-cadence-tick.test.mjs +0 -187
- package/scripts/ci/check-docs-accuracy.test.mjs +0 -409
- package/scripts/ci/check-durable-write-seam.test.mjs +0 -90
- package/scripts/ci/check-no-build-artifacts.test.mjs +0 -71
- package/scripts/ci/check-no-residual-identity.test.mjs +0 -202
- package/scripts/ci/check-skill-packs.test.mjs +0 -495
- package/scripts/ci/check-subagent-frontmatter.test.mjs +0 -124
- package/scripts/ci/check.test.mjs +0 -194
- package/scripts/ci/conformance-org-api.test.mjs +0 -425
- package/scripts/cloud-relay/voice/relay-identity.test.mjs +0 -96
- package/scripts/collective/hook-runner.test.mjs +0 -173
- package/scripts/cost/fleet-digest.test.mjs +0 -207
- package/scripts/cost/track-claude-usage-pricing.test.mjs +0 -183
- package/scripts/cost/track-claude-usage.test.mjs +0 -148
- package/scripts/daemon/agent-daemon-board-mine.test.mjs +0 -96
- package/scripts/daemon/agent-daemon-design.test.mjs +0 -238
- package/scripts/daemon/agent-daemon-frontdoor.test.mjs +0 -60
- package/scripts/daemon/agent-daemon.test.mjs +0 -995
- package/scripts/daemon/assurance-e2e.test.mjs +0 -613
- package/scripts/daemon/assurance.test.mjs +0 -1791
- package/scripts/daemon/board-mirror.test.mjs +0 -165
- package/scripts/daemon/cadence-consumer-frontdoor.test.mjs +0 -393
- package/scripts/daemon/cadence-consumer-governance.test.mjs +0 -276
- package/scripts/daemon/cadence-consumer.test.mjs +0 -776
- package/scripts/daemon/cadence-handlers.test.mjs +0 -837
- package/scripts/daemon/classifier-identity.test.mjs +0 -137
- package/scripts/daemon/classifier.test.mjs +0 -266
- package/scripts/daemon/classify-kind.test.mjs +0 -40
- package/scripts/daemon/context-compiler.test.mjs +0 -406
- package/scripts/daemon/deliver.test.mjs +0 -564
- package/scripts/daemon/dispatcher-cooldown.test.mjs +0 -122
- package/scripts/daemon/dispatcher-governance.test.mjs +0 -1013
- package/scripts/daemon/dispatcher-resume.test.mjs +0 -166
- package/scripts/daemon/dispatcher-session-continuity.test.mjs +0 -365
- package/scripts/daemon/execution-ladder.test.mjs +0 -470
- package/scripts/daemon/goal-steward-cadence.test.mjs +0 -312
- package/scripts/daemon/inbox-deferral-session.test.mjs +0 -49
- package/scripts/daemon/inbox-deferral.test.mjs +0 -336
- package/scripts/daemon/inbox-wake.test.mjs +0 -199
- package/scripts/daemon/integration.test.mjs +0 -149
- package/scripts/daemon/lib/self-echo.test.mjs +0 -153
- package/scripts/daemon/lib/session-router.test.mjs +0 -554
- package/scripts/daemon/prompt-builder-preamble.test.mjs +0 -210
- package/scripts/daemon/prompt-builder.test.mjs +0 -556
- package/scripts/daemon/responder-cost.test.mjs +0 -68
- package/scripts/daemon/responder-history.test.mjs +0 -221
- package/scripts/daemon/sdk-version.test.mjs +0 -31
- package/scripts/daemon/session-lock.test.mjs +0 -252
- package/scripts/daemon/session-outcomes.test.mjs +0 -533
- package/scripts/daemon/typing-registry.test.mjs +0 -102
- package/scripts/hooks/pre-send-audit.test.mjs +0 -354
- package/scripts/huddle/huddle-prompt.test.mjs +0 -176
- package/scripts/local-triggers/autoupdate.test.mjs +0 -518
- package/scripts/local-triggers/generate-plists.test.mjs +0 -456
- package/scripts/media-generation/brand-clause.test.mjs +0 -135
- package/scripts/org/send-orgmail.first-contact.test.mjs +0 -102
- package/scripts/poller/inbox-privilege-injection.test.mjs +0 -167
- package/scripts/poller/inbox-scan-poller.test.mjs +0 -295
- package/scripts/poller/lib/cloud-relay-dedup.test.mjs +0 -133
- package/scripts/poller/slack-socket-mode.test.mjs +0 -805
- package/scripts/poller-launchd/install.test.mjs +0 -243
- package/scripts/restore-from-backup.test.mjs +0 -181
- package/scripts/session/feed.test.mjs +0 -196
- package/scripts/session/supervisor-sh.test.mjs +0 -218
- package/scripts/session/supervisor.test.mjs +0 -482
- package/scripts/setup/configure-macos.test.mjs +0 -306
- package/scripts/setup/gen-subagent-manifest.test.mjs +0 -124
- package/scripts/setup/generate-agent-package-json.test.mjs +0 -143
- package/scripts/setup/generate-capability.test.mjs +0 -134
- package/scripts/setup/init-agent.test.mjs +0 -370
- package/scripts/setup/init-skill-marketplace.test.mjs +0 -193
- package/scripts/vendor/sync-skill-packs.test.mjs +0 -103
- 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
|
+
}
|