@cohortapp/agent-sdk 2.3.0
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/commands/init-agent.md +104 -0
- package/.claude/commands/init-maestro.md +1187 -0
- package/.claude/settings.json +161 -0
- package/.env.example +216 -0
- package/README.md +632 -0
- package/agents/browser-operator/agent.md +52 -0
- package/agents/calendar-ops/agent.md +50 -0
- package/agents/communications/agent.md +96 -0
- package/agents/decision-log/agent.md +65 -0
- package/agents/desktop-operator/agent.md +59 -0
- package/agents/gmail-operator/agent.md +62 -0
- package/agents/inbound-dispatcher/agent.md +66 -0
- package/agents/inbox-processor/agent.md +39 -0
- package/agents/pmo-execution/agent.md +60 -0
- package/agents/session-spawner/agent.md +64 -0
- package/agents/slack-operator/agent.md +60 -0
- package/agents/whatsapp-operator/agent.md +60 -0
- package/agents/workflow-automation/agent.md +61 -0
- package/archetypes/altitudes/c-suite.yaml +58 -0
- package/archetypes/altitudes/founder.yaml +68 -0
- package/archetypes/altitudes/senior-manager.yaml +63 -0
- package/archetypes/altitudes/svp.yaml +60 -0
- package/archetypes/altitudes/vp.yaml +50 -0
- package/archetypes/archetype.schema.json +77 -0
- package/archetypes/base.yaml +47 -0
- package/archetypes/capabilities/commercial-leader.yaml +159 -0
- package/archetypes/capabilities/compliance-officer.yaml +159 -0
- package/archetypes/capabilities/executive-operator.yaml +169 -0
- package/archetypes/capabilities/finance-leader.yaml +162 -0
- package/archetypes/capabilities/operations-leader.yaml +154 -0
- package/archetypes/capabilities/product-leader.yaml +148 -0
- package/archetypes/capabilities/technical-leader.yaml +146 -0
- package/archetypes/functions/commercial-leader.yaml +62 -0
- package/archetypes/functions/compliance-officer.yaml +64 -0
- package/archetypes/functions/executive-operator.yaml +70 -0
- package/archetypes/functions/finance-leader.yaml +70 -0
- package/archetypes/functions/operations-leader.yaml +62 -0
- package/archetypes/functions/product-leader.yaml +61 -0
- package/archetypes/functions/technical-leader.yaml +57 -0
- package/bin/cohort-mcp.mjs +81 -0
- package/bin/maestro.mjs +3516 -0
- package/bin/maestro.test.mjs +1015 -0
- package/desktop-control/README.md +56 -0
- package/desktop-control/app-profiles/gmail.yaml +120 -0
- package/desktop-control/app-profiles/slack.yaml +315 -0
- package/desktop-control/app-profiles/whatsapp.yaml +107 -0
- package/docs/architecture/agent-topology.md +2239 -0
- package/docs/architecture/archetype-agent-factory.md +110 -0
- package/docs/architecture/collective-memory-and-org-mesh.md +115 -0
- package/docs/architecture/continuous-monitoring.md +221 -0
- package/docs/architecture/mcp-capability-map.md +585 -0
- package/docs/architecture/system-architecture.md +1272 -0
- package/docs/company-context/README.md +40 -0
- package/docs/guides/agent-persona-setup.md +600 -0
- package/docs/guides/agents-observe-setup.md +64 -0
- package/docs/guides/billing-console-keys.md +88 -0
- package/docs/guides/ccxray-diagnostics.md +65 -0
- package/docs/guides/channel-bus.md +127 -0
- package/docs/guides/claude-mem-setup.md +79 -0
- package/docs/guides/claude-pace-setup.md +56 -0
- package/docs/guides/claudraband-sessions.md +98 -0
- package/docs/guides/clawteam-swarm.md +116 -0
- package/docs/guides/code-review-graph-setup.md +86 -0
- package/docs/guides/email-setup.md +431 -0
- package/docs/guides/mac-mini.md +119 -0
- package/docs/guides/media-generation-setup.md +349 -0
- package/docs/guides/model-routing.md +162 -0
- package/docs/guides/observability-otel.md +265 -0
- package/docs/guides/org-onboarding.md +132 -0
- package/docs/guides/outbound-governance-setup.md +437 -0
- package/docs/guides/pdf-generation-setup.md +315 -0
- package/docs/guides/poller-daemon-setup.md +563 -0
- package/docs/guides/rag-context-setup.md +459 -0
- package/docs/guides/self-optimization-pattern.md +82 -0
- package/docs/guides/setup-wizard.md +178 -0
- package/docs/guides/slack-setup.md +350 -0
- package/docs/guides/telegram-setup.md +227 -0
- package/docs/guides/twilio-subaccounts-setup.md +223 -0
- package/docs/guides/verification.md +128 -0
- package/docs/guides/voice-mode.md +188 -0
- package/docs/guides/voice-sms-setup.md +698 -0
- package/docs/guides/webhook-relay-setup.md +349 -0
- package/docs/guides/whatsapp-setup.md +288 -0
- package/docs/prompts/board-pack-cover-template.md +36 -0
- package/docs/prompts/decision-recommendation-template.md +88 -0
- package/docs/prompts/followup-message-template.md +141 -0
- package/docs/prompts/investor-letter-template.md +52 -0
- package/docs/prompts/morning-brief-template.md +82 -0
- package/docs/prompts/presentation-template.md +58 -0
- package/docs/prompts/weekly-strategic-memo-template.md +104 -0
- package/docs/research/hallucinated-tool-output-investigation.md +151 -0
- package/docs/runbooks/backup-restore.md +205 -0
- package/docs/runbooks/cohort-cutover.md +129 -0
- package/docs/runbooks/fleet-operations.md +200 -0
- package/docs/runbooks/incident-response.md +226 -0
- package/docs/runbooks/mac-mini-bootstrap.md +431 -0
- package/docs/runbooks/perpetual-operations.md +509 -0
- package/docs/runbooks/recovery-and-failover.md +260 -0
- package/framework-features.json +267 -0
- package/ingest/README.md +87 -0
- package/lib/action-executor.js +689 -0
- package/lib/action-executor.test.mjs +871 -0
- package/lib/agent-root.mjs +37 -0
- package/lib/archetype.mjs +236 -0
- package/lib/archetype.test.mjs +132 -0
- package/lib/autonomy.mjs +114 -0
- package/lib/autonomy.test.mjs +66 -0
- package/lib/backlog.mjs +358 -0
- package/lib/backlog.test.mjs +266 -0
- package/lib/budget-guard.mjs +279 -0
- package/lib/budget-guard.test.mjs +291 -0
- package/lib/cadence-bus-schedule.test.mjs +194 -0
- package/lib/cadence-bus.mjs +1120 -0
- package/lib/cadence-bus.test.mjs +720 -0
- package/lib/cadences.mjs +205 -0
- package/lib/cadences.test.mjs +125 -0
- package/lib/capability.mjs +154 -0
- package/lib/capability.test.mjs +78 -0
- package/lib/channels/base-adapter.mjs +719 -0
- package/lib/channels/base-adapter.test.mjs +590 -0
- package/lib/channels/channel.mjs +128 -0
- package/lib/channels/channels.test.mjs +371 -0
- package/lib/channels/contract.mjs +215 -0
- package/lib/channels/contract.test.mjs +137 -0
- package/lib/channels/conversation-resolver.mjs +95 -0
- package/lib/channels/gmail/adapter.mjs +87 -0
- package/lib/channels/inbox-item.mjs +255 -0
- package/lib/channels/inbox-item.test.mjs +335 -0
- package/lib/channels/index.mjs +94 -0
- package/lib/channels/orgmail/adapter.mjs +353 -0
- package/lib/channels/orgmail/adapter.test.mjs +311 -0
- package/lib/channels/pairing.mjs +363 -0
- package/lib/channels/pairing.test.mjs +270 -0
- package/lib/channels/registry.mjs +164 -0
- package/lib/channels/slack/adapter.mjs +317 -0
- package/lib/channels/slack-adapter.test.mjs +212 -0
- package/lib/channels/sms/adapter.mjs +43 -0
- package/lib/channels/telegram/adapter.mjs +432 -0
- package/lib/channels/telegram-adapter.test.mjs +306 -0
- package/lib/channels/voice/adapter.mjs +301 -0
- package/lib/channels/voice/adapter.test.mjs +278 -0
- package/lib/channels/whatsapp/adapter-baileys.mjs +587 -0
- package/lib/channels/whatsapp/adapter-baileys.test.mjs +359 -0
- package/lib/channels/whatsapp/adapter-twilio.mjs +65 -0
- package/lib/channels/whatsapp/baileys-typing.test.mjs +154 -0
- package/lib/charter.mjs +256 -0
- package/lib/charter.test.mjs +89 -0
- package/lib/claude-bin.mjs +134 -0
- package/lib/claude-bin.test.mjs +75 -0
- package/lib/collective/capture.mjs +185 -0
- package/lib/collective/capture.test.mjs +121 -0
- package/lib/collective/cards.mjs +201 -0
- package/lib/collective/cards.test.mjs +114 -0
- package/lib/collective/config.mjs +186 -0
- package/lib/collective/config.test.mjs +123 -0
- package/lib/collective/global-config.mjs +113 -0
- package/lib/collective/global-config.test.mjs +75 -0
- package/lib/collective/presence.mjs +201 -0
- package/lib/collective/presence.test.mjs +95 -0
- package/lib/collective/recall.mjs +215 -0
- package/lib/collective/recall.test.mjs +116 -0
- package/lib/comms/send-gate.mjs +554 -0
- package/lib/comms/send-gate.test.mjs +577 -0
- package/lib/comms.mjs +67 -0
- package/lib/comms.test.mjs +41 -0
- package/lib/diagnostics/alerts.mjs +424 -0
- package/lib/diagnostics/alerts.test.mjs +318 -0
- package/lib/diagnostics/backup-freshness.mjs +188 -0
- package/lib/diagnostics/backup-freshness.test.mjs +185 -0
- package/lib/diagnostics/counters.mjs +269 -0
- package/lib/diagnostics/counters.test.mjs +206 -0
- package/lib/diagnostics/events.mjs +188 -0
- package/lib/diagnostics/events.test.mjs +290 -0
- package/lib/diagnostics/otel.mjs +237 -0
- package/lib/diagnostics/otel.test.mjs +196 -0
- package/lib/diagnostics/trace.mjs +216 -0
- package/lib/diagnostics/trace.test.mjs +251 -0
- package/lib/env-compat.mjs +74 -0
- package/lib/env-compat.test.mjs +104 -0
- package/lib/feature-init.mjs +331 -0
- package/lib/fs-atomic.mjs +112 -0
- package/lib/fs-atomic.test.mjs +72 -0
- package/lib/fs-ownership.mjs +111 -0
- package/lib/fs-ownership.test.mjs +158 -0
- package/lib/hooks/bus.mjs +347 -0
- package/lib/hooks/bus.test.mjs +387 -0
- package/lib/index.js +16 -0
- package/lib/learning/config.mjs +106 -0
- package/lib/learning/config.test.mjs +75 -0
- package/lib/learning/counters.mjs +156 -0
- package/lib/learning/counters.test.mjs +69 -0
- package/lib/learning/curator-consolidate.test.mjs +238 -0
- package/lib/learning/curator.mjs +453 -0
- package/lib/learning/curator.test.mjs +106 -0
- package/lib/learning/log.mjs +40 -0
- package/lib/learning/reflect.mjs +534 -0
- package/lib/learning/reflect.test.mjs +0 -0
- package/lib/learning/session-index.mjs +352 -0
- package/lib/learning/session-index.test.mjs +125 -0
- package/lib/learning/skill-writer.mjs +474 -0
- package/lib/learning/skill-writer.test.mjs +210 -0
- package/lib/mcp/server.mjs +328 -0
- package/lib/mcp/server.test.mjs +400 -0
- package/lib/model-router/auth-profiles.mjs +758 -0
- package/lib/model-router/auth-profiles.test.mjs +580 -0
- package/lib/model-router/catalog/anthropic.yaml +153 -0
- package/lib/model-router/catalog/deepseek.yaml +86 -0
- package/lib/model-router/catalog/moonshot.yaml +81 -0
- package/lib/model-router/catalog/qwen.yaml +114 -0
- package/lib/model-router/catalog.mjs +925 -0
- package/lib/model-router/catalog.test.mjs +385 -0
- package/lib/model-router/economics.mjs +564 -0
- package/lib/model-router/economics.test.mjs +344 -0
- package/lib/model-router/failover.mjs +298 -0
- package/lib/model-router/failover.test.mjs +439 -0
- package/lib/model-router/health.mjs +453 -0
- package/lib/model-router/health.test.mjs +338 -0
- package/lib/model-router/integration-coverage.test.mjs +829 -0
- package/lib/model-router/integration.test.mjs +564 -0
- package/lib/model-router/ledger.mjs +402 -0
- package/lib/model-router/ledger.test.mjs +382 -0
- package/lib/model-router/llm-task.mjs +515 -0
- package/lib/model-router/llm-task.test.mjs +392 -0
- package/lib/model-router/org-credentials.mjs +260 -0
- package/lib/model-router/org-credentials.test.mjs +265 -0
- package/lib/model-router/pricing-refresh.mjs +463 -0
- package/lib/model-router/pricing-refresh.test.mjs +286 -0
- package/lib/model-router/reconcile.mjs +429 -0
- package/lib/model-router/reconcile.test.mjs +316 -0
- package/lib/model-router/repair.mjs +471 -0
- package/lib/model-router/repair.test.mjs +180 -0
- package/lib/model-router/resolve.mjs +1206 -0
- package/lib/model-router/spawn.mjs +497 -0
- package/lib/model-router/spawn.test.mjs +425 -0
- package/lib/model-router/taxonomy.mjs +893 -0
- package/lib/model-router/taxonomy.test.mjs +410 -0
- package/lib/model-router.mjs +677 -0
- package/lib/model-router.test.mjs +907 -0
- package/lib/org/activity.mjs +211 -0
- package/lib/org/activity.test.mjs +134 -0
- package/lib/org/approvals.mjs +448 -0
- package/lib/org/approvals.test.mjs +216 -0
- package/lib/org/awareness.mjs +222 -0
- package/lib/org/awareness.test.mjs +159 -0
- package/lib/org/board.mjs +229 -0
- package/lib/org/board.test.mjs +177 -0
- package/lib/org/bootstrap-context.mjs +169 -0
- package/lib/org/bootstrap-context.test.mjs +153 -0
- package/lib/org/client.mjs +1628 -0
- package/lib/org/client.test.mjs +1107 -0
- package/lib/org/cohort-client.mjs +67 -0
- package/lib/org/cohort-client.test.mjs +126 -0
- package/lib/org/cost-sync.mjs +227 -0
- package/lib/org/cost-sync.test.mjs +153 -0
- package/lib/org/doctor.mjs +212 -0
- package/lib/org/doctor.test.mjs +212 -0
- package/lib/org/handoff.mjs +293 -0
- package/lib/org/handoff.test.mjs +269 -0
- package/lib/org/integration-tools.mjs +182 -0
- package/lib/org/integration-tools.test.mjs +160 -0
- package/lib/org/keys.mjs +131 -0
- package/lib/org/keys.test.mjs +92 -0
- package/lib/org/knowledge.mjs +463 -0
- package/lib/org/knowledge.test.mjs +319 -0
- package/lib/org/leases.mjs +335 -0
- package/lib/org/leases.test.mjs +235 -0
- package/lib/org/mesh-integration.test.mjs +127 -0
- package/lib/org/mesh.mjs +459 -0
- package/lib/org/mesh.test.mjs +345 -0
- package/lib/org/messaging.mjs +503 -0
- package/lib/org/messaging.test.mjs +238 -0
- package/lib/org/policy.mjs +345 -0
- package/lib/org/policy.test.mjs +237 -0
- package/lib/org/protocol.checksum +1 -0
- package/lib/org/protocol.checksum.test.mjs +90 -0
- package/lib/org/protocol.mjs +967 -0
- package/lib/org/protocol.test.mjs +264 -0
- package/lib/org/registry.mjs +194 -0
- package/lib/org/registry.test.mjs +100 -0
- package/lib/org/tool-surface-integration.test.mjs +120 -0
- package/lib/org/tool-surface.mjs +2535 -0
- package/lib/org/tool-surface.test.mjs +589 -0
- package/lib/org/ui-parity.mjs +3236 -0
- package/lib/org/ui-parity.test.mjs +348 -0
- package/lib/org/verify.mjs +176 -0
- package/lib/org/verify.test.mjs +194 -0
- package/lib/rag/embed.mjs +188 -0
- package/lib/rag/indexer.mjs +425 -0
- package/lib/rag/rag.test.mjs +505 -0
- package/lib/rag/search.mjs +475 -0
- package/lib/rate-guard.mjs +246 -0
- package/lib/rate-guard.test.mjs +201 -0
- package/lib/render.mjs +112 -0
- package/lib/render.test.mjs +68 -0
- package/lib/resource-governor.mjs +297 -0
- package/lib/resource-governor.test.mjs +262 -0
- package/lib/scheduling/dynamic-jobs.mjs +675 -0
- package/lib/scheduling/dynamic-jobs.test.mjs +344 -0
- package/lib/scheduling/jitter.mjs +0 -0
- package/lib/scheduling/jitter.test.mjs +140 -0
- package/lib/secrets/broker.mjs +315 -0
- package/lib/secrets/broker.test.mjs +280 -0
- package/lib/secrets/providers.mjs +461 -0
- package/lib/secrets/providers.test.mjs +274 -0
- package/lib/security/audit-engine.mjs +684 -0
- package/lib/security/audit-engine.test.mjs +389 -0
- package/lib/security/coerce-args.mjs +552 -0
- package/lib/security/coerce-args.test.mjs +281 -0
- package/lib/security/dangerous-tools.mjs +97 -0
- package/lib/security/dangerous-tools.test.mjs +68 -0
- package/lib/security/external-content.mjs +145 -0
- package/lib/security/external-content.test.mjs +67 -0
- package/lib/security/redact.mjs +592 -0
- package/lib/security/redact.test.mjs +441 -0
- package/lib/security/secret-equal.mjs +73 -0
- package/lib/security/secret-equal.test.mjs +55 -0
- package/lib/session-permissions.mjs +101 -0
- package/lib/session-permissions.test.mjs +100 -0
- package/lib/setup/claude-probe.mjs +74 -0
- package/lib/setup/completeness.mjs +175 -0
- package/lib/setup/completeness.test.mjs +110 -0
- package/lib/setup/context-pack.mjs +173 -0
- package/lib/setup/context-pack.test.mjs +89 -0
- package/lib/setup/enrich.mjs +277 -0
- package/lib/setup/enrich.test.mjs +115 -0
- package/lib/setup/enroll-from-cohort.mjs +441 -0
- package/lib/setup/enroll-from-cohort.test.mjs +233 -0
- package/lib/setup/integration.test.mjs +162 -0
- package/lib/setup/io.mjs +360 -0
- package/lib/setup/io.test.mjs +77 -0
- package/lib/setup/run-generator.mjs +81 -0
- package/lib/setup/runner.mjs +244 -0
- package/lib/setup/runner.test.mjs +132 -0
- package/lib/setup/sections/comms.mjs +173 -0
- package/lib/setup/sections/company.mjs +120 -0
- package/lib/setup/sections/enrich.mjs +138 -0
- package/lib/setup/sections/identity.mjs +182 -0
- package/lib/setup/sections/identity.test.mjs +140 -0
- package/lib/setup/sections/learning.mjs +153 -0
- package/lib/setup/sections/learning.test.mjs +81 -0
- package/lib/setup/sections/messaging.mjs +219 -0
- package/lib/setup/sections/messaging.test.mjs +127 -0
- package/lib/setup/sections/model.mjs +102 -0
- package/lib/setup/sections/operating-model.mjs +78 -0
- package/lib/setup/sections/org.mjs +475 -0
- package/lib/setup/sections/org.test.mjs +313 -0
- package/lib/setup/sections/orgmail.mjs +173 -0
- package/lib/setup/sections/orgmail.test.mjs +118 -0
- package/lib/setup/sections/recovery.mjs +159 -0
- package/lib/setup/sections/recovery.test.mjs +98 -0
- package/lib/setup/sections/tools.mjs +132 -0
- package/lib/setup/sections/verify.mjs +97 -0
- package/lib/setup/sot.mjs +205 -0
- package/lib/setup/sot.test.mjs +81 -0
- package/lib/setup/state.mjs +151 -0
- package/lib/setup/state.test.mjs +92 -0
- package/lib/singleton.js +229 -0
- package/lib/singleton.test.mjs +135 -0
- package/lib/telemetry/alerts.mjs +216 -0
- package/lib/telemetry/alerts.test.mjs +109 -0
- package/lib/telemetry/collect.mjs +512 -0
- package/lib/telemetry/collect.test.mjs +202 -0
- package/lib/tool-definitions-integration.test.mjs +83 -0
- package/lib/tool-definitions.js +738 -0
- package/lib/tool-definitions.test.mjs +437 -0
- package/lib/util/fetch-timeout.mjs +136 -0
- package/lib/util/fetch-timeout.test.mjs +202 -0
- package/lib/util/reconnect.mjs +343 -0
- package/lib/util/reconnect.test.mjs +369 -0
- package/lib/util/unhandled.mjs +205 -0
- package/lib/util/unhandled.test.mjs +216 -0
- package/lib/voice/context-loader.mjs +466 -0
- package/lib/voice/index.mjs +100 -0
- package/lib/voice/openai-realtime.mjs +510 -0
- package/lib/voice/outbound.mjs +542 -0
- package/lib/voice/outbound.test.mjs +69 -0
- package/lib/voice/post-call-brief.mjs +428 -0
- package/lib/voice/provider.mjs +52 -0
- package/lib/voice/session-rotation.mjs +257 -0
- package/lib/voice/stt.mjs +161 -0
- package/lib/voice/stt.test.mjs +226 -0
- package/lib/voice/tool-bridge.mjs +370 -0
- package/lib/voice/tts.mjs +104 -0
- package/lib/voice/twilio-sip-bridge.mjs +288 -0
- package/lib/voice/voice.test.mjs +990 -0
- package/mcp/README.md +80 -0
- package/package.json +151 -0
- package/plugins/maestro-skills/plugin.json +139 -0
- package/plugins/maestro-skills/skills/agents-observe.md +110 -0
- package/plugins/maestro-skills/skills/board-deck.md +68 -0
- package/plugins/maestro-skills/skills/books-close.md +77 -0
- package/plugins/maestro-skills/skills/brand-steward.md +121 -0
- package/plugins/maestro-skills/skills/calendar-plan.md +57 -0
- package/plugins/maestro-skills/skills/call-working-sessions.md +124 -0
- package/plugins/maestro-skills/skills/ccxray-diagnostics.md +91 -0
- package/plugins/maestro-skills/skills/claude-pace.md +61 -0
- package/plugins/maestro-skills/skills/code-review-graph.md +99 -0
- package/plugins/maestro-skills/skills/crm-pipeline.md +65 -0
- package/plugins/maestro-skills/skills/decision-brief.md +89 -0
- package/plugins/maestro-skills/skills/directory-hygiene.md +125 -0
- package/plugins/maestro-skills/skills/draft-comms.md +84 -0
- package/plugins/maestro-skills/skills/evening-wrap.md +53 -0
- package/plugins/maestro-skills/skills/files-find.md +65 -0
- package/plugins/maestro-skills/skills/generative-ui.md +228 -0
- package/plugins/maestro-skills/skills/hiring-triage.md +74 -0
- package/plugins/maestro-skills/skills/inbox-triage.md +61 -0
- package/plugins/maestro-skills/skills/mail-triage.md +86 -0
- package/plugins/maestro-skills/skills/morning-brief.md +54 -0
- package/plugins/maestro-skills/skills/native-artifacts.md +157 -0
- package/plugins/maestro-skills/skills/org-board.md +133 -0
- package/plugins/maestro-skills/skills/org-credential.md +68 -0
- package/plugins/maestro-skills/skills/org-recall.md +81 -0
- package/plugins/maestro-skills/skills/pipeline-review.md +76 -0
- package/plugins/maestro-skills/skills/regulatory-status.md +81 -0
- package/plugins/maestro-skills/skills/router-why.md +78 -0
- package/plugins/maestro-skills/skills/schedule-meeting.md +91 -0
- package/plugins/maestro-skills/skills/session-search.md +71 -0
- package/plugins/maestro-skills/skills/set-reminder.md +93 -0
- package/plugins/maestro-skills/skills/slack-followup.md +64 -0
- package/plugins/maestro-skills/skills/team-activity.md +86 -0
- package/plugins/maestro-skills/skills/weekly-memo.md +70 -0
- package/policies/action-classification.yaml +114 -0
- package/policies/ai-disclosure.yaml +294 -0
- package/policies/communication-style.md +139 -0
- package/policies/information-barriers.yaml +118 -0
- package/policies/prompt-injection-defence.yaml +138 -0
- package/public/assets/icon-dark.png +0 -0
- package/public/assets/icon-dark.svg +9 -0
- package/public/assets/icon-light.svg +9 -0
- package/public/assets/logo-dark.svg +15 -0
- package/public/assets/logo-light.svg +15 -0
- package/scaffold/.mcp.json +7 -0
- package/scaffold/CLAUDE.md +368 -0
- package/scaffold/config/agent.json +55 -0
- package/scaffold/config/agent.ts +76 -0
- package/scaffold/config/agent.ts.example +89 -0
- package/scaffold/config/alerts.yaml +23 -0
- package/scaffold/config/allowlist.yaml.example +25 -0
- package/scaffold/config/caller-id-map.yaml +46 -0
- package/scaffold/config/collective.yaml +49 -0
- package/scaffold/config/company.json +20 -0
- package/scaffold/config/known-agents.json +6 -0
- package/scaffold/config/learning.yaml +55 -0
- package/scaffold/config/model-routing.yaml.example +104 -0
- package/scaffold/config/org.yaml +25 -0
- package/scaffold/config/orgmail.yaml.example +19 -0
- package/scaffold/config/recovery.yaml +72 -0
- package/scaffold/config/secrets.yaml +27 -0
- package/scaffold/config/slack.yaml.example +35 -0
- package/scaffold/config/telegram.yaml.example +38 -0
- package/scaffold/config/voice.yaml.example +89 -0
- package/scaffold/config/whatsapp.yaml.example +39 -0
- package/schedules/README.md +49 -0
- package/schedules/triggers/backlog-executor.md +102 -0
- package/schedules/triggers/brand-steward.md +72 -0
- package/schedules/triggers/daily-evening-wrap.md +159 -0
- package/schedules/triggers/daily-midday-sweep.md +58 -0
- package/schedules/triggers/daily-morning-brief.md +55 -0
- package/schedules/triggers/directory-hygiene.md +81 -0
- package/schedules/triggers/dynamic-jobs.md +40 -0
- package/schedules/triggers/inbox-processor.md +115 -0
- package/schedules/triggers/meeting-action-capture.md +60 -0
- package/schedules/triggers/meeting-prep.md +69 -0
- package/schedules/triggers/messaging-inbound.md +50 -0
- package/schedules/triggers/org-pulse.md +24 -0
- package/schedules/triggers/quarterly-self-assessment.md +54 -0
- package/schedules/triggers/weekly-engineering-health.md +37 -0
- package/schedules/triggers/weekly-execution.md +65 -0
- package/schedules/triggers/weekly-hiring.md +53 -0
- package/schedules/triggers/weekly-priorities.md +38 -0
- package/schedules/triggers/weekly-strategic-memo.md +124 -0
- package/scripts/archive-email.sh +55 -0
- package/scripts/cadence/cadence-status.mjs +36 -0
- package/scripts/cadence/enqueue-cadence-tick.mjs +174 -0
- package/scripts/cadence/enqueue-cadence-tick.test.mjs +187 -0
- package/scripts/cadence/launchd-cadence-wrapper.sh +85 -0
- package/scripts/cadence/launchd-cloud-relay-wrapper.sh +95 -0
- package/scripts/cadence/launchd-socket-mode-wrapper.sh +95 -0
- package/scripts/ci/check-docs-accuracy.mjs +493 -0
- package/scripts/ci/check-docs-accuracy.test.mjs +409 -0
- package/scripts/ci/check-exports-exist.mjs +140 -0
- package/scripts/ci/check-files-exist.mjs +107 -0
- package/scripts/ci/check-no-build-artifacts.mjs +111 -0
- package/scripts/ci/check-no-build-artifacts.test.mjs +71 -0
- package/scripts/ci/check-no-confidential.mjs +198 -0
- package/scripts/ci/check-no-conflict-markers.mjs +169 -0
- package/scripts/ci/check-no-residual-identity.mjs +163 -0
- package/scripts/ci/check-no-residual-identity.test.mjs +89 -0
- package/scripts/ci/check-tarball-fidelity.mjs +205 -0
- package/scripts/ci/check-unresolved-tokens.mjs +83 -0
- package/scripts/ci/check.mjs +109 -0
- package/scripts/ci/check.test.mjs +194 -0
- package/scripts/ci/run-coverage.mjs +82 -0
- package/scripts/ci/run-tests.mjs +71 -0
- package/scripts/cloud-relay/README.md +59 -0
- package/scripts/cloud-relay/index.mjs +233 -0
- package/scripts/cloud-relay/package.json +15 -0
- package/scripts/cloud-relay/railway.json +13 -0
- package/scripts/cloud-relay/voice/README.md +94 -0
- package/scripts/cloud-relay/voice/package-lock.json +39 -0
- package/scripts/cloud-relay/voice/package.json +16 -0
- package/scripts/cloud-relay/voice/railway.json +13 -0
- package/scripts/cloud-relay/voice/server.mjs +532 -0
- package/scripts/collective/hook-runner.mjs +211 -0
- package/scripts/collective/hook-runner.test.mjs +90 -0
- package/scripts/collective/org-pulse.mjs +72 -0
- package/scripts/collective/org-sync.mjs +61 -0
- package/scripts/collective/recall.mjs +45 -0
- package/scripts/collective/who.mjs +30 -0
- package/scripts/comms-monitor.sh +288 -0
- package/scripts/configure-whatsapp-sandbox.sh +201 -0
- package/scripts/continuous-monitor.sh +91 -0
- package/scripts/cost/fleet-digest.mjs +407 -0
- package/scripts/cost/fleet-digest.test.mjs +207 -0
- package/scripts/cost/track-claude-usage.mjs +169 -0
- package/scripts/daemon/agent-daemon.mjs +989 -0
- package/scripts/daemon/agent-daemon.test.mjs +525 -0
- package/scripts/daemon/cadence-consumer-governance.test.mjs +220 -0
- package/scripts/daemon/cadence-consumer.mjs +1080 -0
- package/scripts/daemon/cadence-consumer.test.mjs +770 -0
- package/scripts/daemon/cadence-handlers.mjs +1121 -0
- package/scripts/daemon/cadence-handlers.test.mjs +617 -0
- package/scripts/daemon/classifier.mjs +704 -0
- package/scripts/daemon/classifier.test.mjs +238 -0
- package/scripts/daemon/classify-kind.mjs +54 -0
- package/scripts/daemon/classify-kind.test.mjs +40 -0
- package/scripts/daemon/context-compiler.mjs +605 -0
- package/scripts/daemon/context-compiler.test.mjs +300 -0
- package/scripts/daemon/dispatcher-cooldown.test.mjs +122 -0
- package/scripts/daemon/dispatcher-governance.test.mjs +886 -0
- package/scripts/daemon/dispatcher.mjs +1516 -0
- package/scripts/daemon/health.mjs +72 -0
- package/scripts/daemon/inbox-deferral.mjs +210 -0
- package/scripts/daemon/inbox-deferral.test.mjs +242 -0
- package/scripts/daemon/integration.test.mjs +149 -0
- package/scripts/daemon/launchd-wrapper-generic.sh +96 -0
- package/scripts/daemon/launchd-wrapper-slack-events.sh +37 -0
- package/scripts/daemon/launchd-wrapper.sh +91 -0
- package/scripts/daemon/lib/session-router.mjs +274 -0
- package/scripts/daemon/lib/session-router.test.mjs +295 -0
- package/scripts/daemon/maestro-daemon.mjs +275 -0
- package/scripts/daemon/prompt-builder.mjs +685 -0
- package/scripts/daemon/prompt-builder.test.mjs +213 -0
- package/scripts/daemon/responder.mjs +854 -0
- package/scripts/daemon/session-lock.mjs +721 -0
- package/scripts/daemon/session-lock.test.mjs +252 -0
- package/scripts/daemon/session-outcomes.mjs +640 -0
- package/scripts/daemon/session-outcomes.test.mjs +533 -0
- package/scripts/daemon/typing-registry.mjs +90 -0
- package/scripts/daemon/typing-registry.test.mjs +77 -0
- package/scripts/daemon/voice-webhook-server.mjs +804 -0
- package/scripts/decisions/capture-decision.mjs +116 -0
- package/scripts/disclosure_assessment.py +873 -0
- package/scripts/disclosure_boundaries.py +562 -0
- package/scripts/email-signature-principal.html +52 -0
- package/scripts/email-signature.html +60 -0
- package/scripts/email_quote_thread.py +167 -0
- package/scripts/email_thread_dedup.py +362 -0
- package/scripts/emergency-stop.sh +81 -0
- package/scripts/healthcheck.sh +116 -0
- package/scripts/hooks/block-mcp-cohort-send.sh +15 -0
- package/scripts/hooks/block-mcp-slack-send.sh +7 -0
- package/scripts/hooks/post-action-log.sh +126 -0
- package/scripts/hooks/pre-send-audit.sh +174 -0
- package/scripts/hooks/pre-send-audit.test.mjs +215 -0
- package/scripts/hooks/session-end-log.sh +27 -0
- package/scripts/hooks/session-start-banner.sh +115 -0
- package/scripts/huddle/audio-bridge.mjs +664 -0
- package/scripts/huddle/boot-slack-cdp.sh +102 -0
- package/scripts/huddle/huddle-controller.mjs +942 -0
- package/scripts/huddle/huddle-server.mjs +1229 -0
- package/scripts/huddle/launch-slack.sh +232 -0
- package/scripts/huddle/openai-realtime-bridge.mjs +462 -0
- package/scripts/huddle/package-lock.json +62 -0
- package/scripts/huddle/package.json +22 -0
- package/scripts/huddle/setup-audio.sh +239 -0
- package/scripts/huddle/start-call.mjs +318 -0
- package/scripts/huddle/test-pipeline.mjs +263 -0
- package/scripts/learning/consolidate-skills.mjs +72 -0
- package/scripts/learning/session-search.mjs +125 -0
- package/scripts/llm_email_dedup.py +442 -0
- package/scripts/local-triggers/generate-plists.sh +432 -0
- package/scripts/local-triggers/generate-plists.test.mjs +413 -0
- package/scripts/local-triggers/install-all.sh +49 -0
- package/scripts/local-triggers/plists/.gitkeep +0 -0
- package/scripts/local-triggers/run-trigger.sh +63 -0
- package/scripts/local-triggers/templates/rag-reindex.plist.template +47 -0
- package/scripts/local-triggers/templates/voice-relay-poller.plist.template +54 -0
- package/scripts/local-triggers/templates/voice-tunnel.plist.template +55 -0
- package/scripts/local-triggers/templates/voice-webhook.plist.template +51 -0
- package/scripts/maintenance/backup-to-cloud.sh +124 -0
- package/scripts/maintenance/health-check.sh +377 -0
- package/scripts/media-generation/README.md +105 -0
- package/scripts/media-generation/gemini-image-client.mjs +173 -0
- package/scripts/media-generation/generate-assets.mjs +289 -0
- package/scripts/media-generation/veo-video-client.mjs +219 -0
- package/scripts/org/send-orgmail.mjs +227 -0
- package/scripts/outbound-dedup-cleanup.sh +43 -0
- package/scripts/outbound-dedup.sh +477 -0
- package/scripts/outbound_dedup.py +115 -0
- package/scripts/parse-voice-transcript.mjs +481 -0
- package/scripts/pdf-generation/README.md +63 -0
- package/scripts/pdf-generation/build-document.mjs +247 -0
- package/scripts/pdf-generation/templates/board-pack.latex +136 -0
- package/scripts/pdf-generation/templates/corporate-letter.latex +126 -0
- package/scripts/pdf-generation/templates/memo.latex +114 -0
- package/scripts/poll-slack-events.sh +35 -0
- package/scripts/poller/calendar-poller.mjs +12 -0
- package/scripts/poller/gmail-poller.mjs +192 -0
- package/scripts/poller/imap-client.mjs +289 -0
- package/scripts/poller/inbox-scan-poller.mjs +156 -0
- package/scripts/poller/inbox-scan-poller.test.mjs +231 -0
- package/scripts/poller/index.mjs +73 -0
- package/scripts/poller/intra-session-check.mjs +285 -0
- package/scripts/poller/lib/cloud-relay-dedup.mjs +88 -0
- package/scripts/poller/lib/cloud-relay-dedup.test.mjs +133 -0
- package/scripts/poller/lib/slash-command-handlers.mjs +177 -0
- package/scripts/poller/secondary-gmail-poller.mjs +132 -0
- package/scripts/poller/slack-cloud-relay-client.mjs +368 -0
- package/scripts/poller/slack-poller.mjs +854 -0
- package/scripts/poller/slack-socket-mode.mjs +917 -0
- package/scripts/poller/slack-socket-mode.test.mjs +753 -0
- package/scripts/poller/trigger.mjs +75 -0
- package/scripts/poller/utils.mjs +371 -0
- package/scripts/poller/voice-cloud-relay-client.mjs +179 -0
- package/scripts/poller/voice-poller.mjs +236 -0
- package/scripts/poller-launchd/install.sh +66 -0
- package/scripts/poller-launchd/poller.plist.template +40 -0
- package/scripts/poller-launchd/whatsapp-handler.plist.template +39 -0
- package/scripts/post-interaction-indexer.py +1598 -0
- package/scripts/pre-draft-context.py +994 -0
- package/scripts/pre_draft_lookup.py +258 -0
- package/scripts/rag/build-index.mjs +47 -0
- package/scripts/rag/ingest.mjs +111 -0
- package/scripts/rag/search.mjs +119 -0
- package/scripts/rag-indexer.py +629 -0
- package/scripts/restore-from-backup.sh +248 -0
- package/scripts/restore-from-backup.test.mjs +178 -0
- package/scripts/resume-operations.sh +80 -0
- package/scripts/search-secondary-inbox.py +181 -0
- package/scripts/secondary-inbox-poller.py +437 -0
- package/scripts/self-optimization/compute-metrics.py +398 -0
- package/scripts/send-email-as-principal.py +369 -0
- package/scripts/send-email-threaded.py +392 -0
- package/scripts/send-email-with-attachment.py +377 -0
- package/scripts/send-email.sh +131 -0
- package/scripts/send-sms.sh +175 -0
- package/scripts/send-whatsapp.sh +292 -0
- package/scripts/session-start.sh +106 -0
- package/scripts/setup/boot-claude-session.sh +94 -0
- package/scripts/setup/configure-macos.sh +674 -0
- package/scripts/setup/configure-twilio-sip-trunk.mjs +207 -0
- package/scripts/setup/configure-voice-tunnel.mjs +182 -0
- package/scripts/setup/generate-agent-env.mjs +92 -0
- package/scripts/setup/generate-agent-package-json.mjs +222 -0
- package/scripts/setup/generate-agent-package-json.test.mjs +143 -0
- package/scripts/setup/generate-autonomy.mjs +60 -0
- package/scripts/setup/generate-backlog.mjs +101 -0
- package/scripts/setup/generate-cadences.mjs +92 -0
- package/scripts/setup/generate-capability.mjs +162 -0
- package/scripts/setup/generate-charter.mjs +76 -0
- package/scripts/setup/generate-comms.mjs +59 -0
- package/scripts/setup/generate-company.mjs +92 -0
- package/scripts/setup/init-agent.sh +547 -0
- package/scripts/setup/init-agent.test.mjs +151 -0
- package/scripts/setup/init-archetype.mjs +74 -0
- package/scripts/setup/init-backup.mjs +54 -0
- package/scripts/setup/init-cadence-bus.mjs +60 -0
- package/scripts/setup/init-channel-bus.mjs +46 -0
- package/scripts/setup/init-cost-tracking.mjs +45 -0
- package/scripts/setup/init-decision-capture.mjs +66 -0
- package/scripts/setup/init-known-agents.mjs +57 -0
- package/scripts/setup/init-learning.mjs +70 -0
- package/scripts/setup/init-memory-executive.mjs +45 -0
- package/scripts/setup/init-model-router.mjs +124 -0
- package/scripts/setup/init-rag.mjs +174 -0
- package/scripts/setup/init-session-router.mjs +38 -0
- package/scripts/setup/init-slack-socket-mode.mjs +260 -0
- package/scripts/setup/init-telegram.mjs +165 -0
- package/scripts/setup/init-voice-realtime.mjs +204 -0
- package/scripts/setup/init-whatsapp-baileys.mjs +77 -0
- package/scripts/setup/install-dev-tools.sh +150 -0
- package/scripts/setup/lib/install-plist.mjs +95 -0
- package/scripts/setup/migrate-agent-to-sot.mjs +192 -0
- package/scripts/setup/render-environment-yaml.mjs +133 -0
- package/scripts/slack-events-ctl.sh +177 -0
- package/scripts/slack-events-server.mjs +1045 -0
- package/scripts/slack-react.mjs +89 -0
- package/scripts/slack-responded.sh +232 -0
- package/scripts/slack-send.sh +287 -0
- package/scripts/slack-typing.mjs +196 -0
- package/scripts/slack-upload-v2.py +95 -0
- package/scripts/sms-handler.mjs +450 -0
- package/scripts/spawn-session.sh +120 -0
- package/scripts/sync-protocol.mjs +217 -0
- package/scripts/system-verify.sh +184 -0
- package/scripts/test-email-thread-dedup.py +239 -0
- package/scripts/test-information-barriers.py +484 -0
- package/scripts/test-llm-email-dedup.py +251 -0
- package/scripts/test-pre-draft-integration.py +203 -0
- package/scripts/test-rag-phase2.sh +442 -0
- package/scripts/test-rag-search.sh +251 -0
- package/scripts/test-voice-parser.mjs +316 -0
- package/scripts/user-context-search.py +659 -0
- package/scripts/validate_outbound.py +1504 -0
- package/scripts/watchdog/ai.maestro.memory-watchdog.plist +41 -0
- package/scripts/watchdog/force-reboot.sh +157 -0
- package/scripts/watchdog/memory-watchdog.sh +473 -0
- package/scripts/whatsapp-handler.mjs +538 -0
- package/teams/desktop-operations.yaml +34 -0
- package/teams/executive-office.yaml +27 -0
- package/teams/legal-and-regulatory.yaml +24 -0
- package/teams/platform-and-engineering.yaml +23 -0
- package/teams/strategy-and-growth.yaml +29 -0
- package/workflows/continuous/backlog-executor.yaml +141 -0
- package/workflows/continuous/inbound-monitor.yaml +168 -0
- package/workflows/daily/applicant-triage.yaml +197 -0
- package/workflows/daily/comms-triage.yaml +80 -0
- package/workflows/daily/evening-wrap.yaml +105 -0
- package/workflows/daily/morning-brief.yaml +164 -0
- package/workflows/daily/slack-followup-sweep.yaml +87 -0
- package/workflows/event-driven/README.md +50 -0
- package/workflows/event-driven/agent-failure-investigation.yaml +137 -0
- package/workflows/event-driven/pr-review.yaml +107 -0
- package/workflows/monthly/board-readiness.yaml +76 -0
- package/workflows/quarterly/strategic-scenario-analysis.yaml +85 -0
- package/workflows/session-protocol.md +171 -0
- package/workflows/weekly/engineering-health.yaml +154 -0
- package/workflows/weekly/hiring-review.yaml +169 -0
- package/workflows/weekly/rollup-pipeline-review.yaml +76 -0
- package/workflows/weekly/strategic-memo.yaml +79 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maestro — Voice subsystem (provider-agnostic facade)
|
|
3
|
+
*
|
|
4
|
+
* Exports the `VoiceProvider` interface and the concrete implementations
|
|
5
|
+
* we ship. Today: OpenAI Realtime. Tomorrow: Gemini Live, Claude Voice
|
|
6
|
+
* (when GA). The interface is small on purpose:
|
|
7
|
+
*
|
|
8
|
+
* connect(session) → opens the upstream session
|
|
9
|
+
* appendAudio(pcm16Chunk) → streams an audio chunk to the model
|
|
10
|
+
* commitTurn() → tells the model the user's turn is over
|
|
11
|
+
* interrupt() → barge-in: stop current model output
|
|
12
|
+
* sendToolResult(callId, output) → returns a tool-call result + resumes
|
|
13
|
+
* sendText(text) → optional: send user text directly
|
|
14
|
+
* close() → tear the session down
|
|
15
|
+
* on(event, handler) → audio.delta, transcript.delta, tool.call, …
|
|
16
|
+
*
|
|
17
|
+
* The maestro daemon never imports this directly; instead, the
|
|
18
|
+
* `voice-handler.mjs` daemon worker creates a provider via createProvider()
|
|
19
|
+
* and pipes between the channel (Twilio SIP, browser WebRTC, etc.) and the
|
|
20
|
+
* provider. That keeps the audio plumbing out of the core daemon and makes
|
|
21
|
+
* the provider swappable per agent.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { VoiceProvider, VOICE_EVENTS } from "./provider.mjs";
|
|
25
|
+
import { createOpenAIRealtimeProvider } from "./openai-realtime.mjs";
|
|
26
|
+
|
|
27
|
+
export { VoiceProvider, VOICE_EVENTS };
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Factory for the configured provider. Reads `provider` from the session
|
|
31
|
+
* config and instantiates the matching implementation. Used by the
|
|
32
|
+
* daemon's voice worker so we never hard-code OpenAI.
|
|
33
|
+
*
|
|
34
|
+
* Supported provider names:
|
|
35
|
+
* "openai-realtime" (default) → OpenAI gpt-realtime-2 over WebSocket
|
|
36
|
+
* "openai-realtime-webrtc" → same model, WebRTC transport
|
|
37
|
+
* "gemini-live" → stub (throws not_implemented)
|
|
38
|
+
*
|
|
39
|
+
* @param {string} name
|
|
40
|
+
* @param {object} opts
|
|
41
|
+
* @returns {VoiceProvider}
|
|
42
|
+
*/
|
|
43
|
+
export function createProvider(name, opts = {}) {
|
|
44
|
+
switch ((name || "openai-realtime").toLowerCase()) {
|
|
45
|
+
case "openai-realtime":
|
|
46
|
+
case "openai-realtime-ws":
|
|
47
|
+
return createOpenAIRealtimeProvider({ transport: "ws", ...opts });
|
|
48
|
+
case "openai-realtime-webrtc":
|
|
49
|
+
return createOpenAIRealtimeProvider({ transport: "webrtc", ...opts });
|
|
50
|
+
case "gemini-live":
|
|
51
|
+
throw new Error("provider 'gemini-live' is stubbed — see lib/voice/gemini-live.mjs (planned 1.13.0)");
|
|
52
|
+
case "claude-voice":
|
|
53
|
+
throw new Error("provider 'claude-voice' is unavailable — Anthropic has not yet shipped a streaming voice API");
|
|
54
|
+
default:
|
|
55
|
+
throw new Error(`unknown voice provider: ${name}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Helper: build a Realtime session config from a partial spec + agent
|
|
61
|
+
* identity. Centralises the defaults so every caller doesn't reinvent
|
|
62
|
+
* them.
|
|
63
|
+
*/
|
|
64
|
+
export function buildSessionConfig(partial = {}, identity = {}) {
|
|
65
|
+
const voice = partial.voice || "cedar"; // GA-only voice from May 2026 launch
|
|
66
|
+
const instructions = partial.instructions ||
|
|
67
|
+
`You are ${identity.name || "this agent"}, ${identity.role || "an executive operator"} at ${identity.company || "the company"}. ` +
|
|
68
|
+
`Reply briefly in ${identity.name || "first-person"} voice. Honour the same action-classification policy as the Slack/Gmail channels — verbally confirm any high-risk action before invoking its tool.`;
|
|
69
|
+
return {
|
|
70
|
+
type: "realtime",
|
|
71
|
+
model: partial.model || "gpt-realtime-2",
|
|
72
|
+
output_modalities: partial.output_modalities || ["audio"],
|
|
73
|
+
instructions,
|
|
74
|
+
voice,
|
|
75
|
+
input_audio_format: partial.input_audio_format || "pcm16",
|
|
76
|
+
output_audio_format: partial.output_audio_format || "pcm16",
|
|
77
|
+
input_audio_transcription: partial.input_audio_transcription ?? {
|
|
78
|
+
model: "gpt-realtime-whisper",
|
|
79
|
+
},
|
|
80
|
+
turn_detection: partial.turn_detection ?? {
|
|
81
|
+
type: "server_vad",
|
|
82
|
+
threshold: 0.5,
|
|
83
|
+
prefix_padding_ms: 300,
|
|
84
|
+
silence_duration_ms: 500,
|
|
85
|
+
},
|
|
86
|
+
tools: partial.tools || [],
|
|
87
|
+
tool_choice: partial.tool_choice || "auto",
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export { createOpenAIRealtimeProvider } from "./openai-realtime.mjs";
|
|
92
|
+
export { actionsToRealtimeTools, registerToolBridge } from "./tool-bridge.mjs";
|
|
93
|
+
export { rotateSessionIfStale, SESSION_ROTATION_DEFAULTS } from "./session-rotation.mjs";
|
|
94
|
+
|
|
95
|
+
export default {
|
|
96
|
+
VoiceProvider,
|
|
97
|
+
VOICE_EVENTS,
|
|
98
|
+
createProvider,
|
|
99
|
+
buildSessionConfig,
|
|
100
|
+
};
|
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maestro — OpenAI Realtime voice provider (May 2026 GA)
|
|
3
|
+
*
|
|
4
|
+
* Thin, typed-by-convention wrapper around OpenAI's Realtime API. Emits
|
|
5
|
+
* the events declared in `lib/voice/index.mjs:VOICE_EVENTS` so the daemon's
|
|
6
|
+
* voice worker can stay provider-agnostic.
|
|
7
|
+
*
|
|
8
|
+
* Two transports:
|
|
9
|
+
* - WebSocket (server-side agents) — wss://api.openai.com/v1/realtime
|
|
10
|
+
* - WebRTC (browser/low-latency) — POST /v1/realtime/calls
|
|
11
|
+
*
|
|
12
|
+
* Audio is PCM16 mono @ 24 kHz on the WebSocket path; WebRTC negotiates
|
|
13
|
+
* Opus automatically. For µ-law (Twilio Voice / SIP bridges), set the
|
|
14
|
+
* session input/output audio_format to "g711_ulaw" and route µ-law
|
|
15
|
+
* frames through directly — no resampling on our side.
|
|
16
|
+
*
|
|
17
|
+
* Implementation notes:
|
|
18
|
+
* - We don't take a hard dependency on the `openai` npm package. If it
|
|
19
|
+
* is installed, we use its typed OpenAIRealtimeWS helper. Otherwise we
|
|
20
|
+
* fall back to a thin `ws` client. Either way, the public interface
|
|
21
|
+
* here is identical.
|
|
22
|
+
* - For barge-in (user interrupts the agent), the caller MUST emit
|
|
23
|
+
* `interrupt()` from a USER_SPEECH_STARTED handler, AND must track how
|
|
24
|
+
* many ms of agent audio actually played before the interruption so
|
|
25
|
+
* `conversation.item.truncate` can keep the model's internal
|
|
26
|
+
* transcript in sync with what the human heard. Skipping the truncate
|
|
27
|
+
* is the #1 production bug per the OpenAI docs.
|
|
28
|
+
*
|
|
29
|
+
* Auth:
|
|
30
|
+
* - OPENAI_API_KEY (required)
|
|
31
|
+
* - OPENAI_SAFETY_IDENTIFIER (optional; hashed user ID for safety telemetry)
|
|
32
|
+
*
|
|
33
|
+
* Refs:
|
|
34
|
+
* https://developers.openai.com/api/docs/guides/realtime-conversations
|
|
35
|
+
* https://developers.openai.com/api/docs/guides/realtime-vad
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import { VoiceProvider, VOICE_EVENTS } from "./provider.mjs";
|
|
39
|
+
|
|
40
|
+
export const OPENAI_REALTIME_WS_URL = "wss://api.openai.com/v1/realtime";
|
|
41
|
+
export const OPENAI_REALTIME_DEFAULT_MODEL = "gpt-realtime-2";
|
|
42
|
+
|
|
43
|
+
// WebSocket.readyState OPEN value (same for browser WebSocket and the `ws`
|
|
44
|
+
// package). We compare numerically rather than importing a constructor so the
|
|
45
|
+
// SDK / `ws` / injected-fake socket paths all work uniformly.
|
|
46
|
+
const WS_OPEN = 1;
|
|
47
|
+
|
|
48
|
+
// Default backpressure ceiling (bytes). When socket.bufferedAmount climbs above
|
|
49
|
+
// this, the kernel/ws send buffer is backing up faster than the wire can drain
|
|
50
|
+
// — typically because the upstream stalled. Rather than queueing unbounded
|
|
51
|
+
// audio (which adds latency the user hears as lag and can OOM a long call), we
|
|
52
|
+
// drop new *audio append* frames until the buffer drains (drop-oldest-style for
|
|
53
|
+
// a live stream). Control frames are never dropped. Tunable via
|
|
54
|
+
// opts.maxBufferedBytes. 0 disables the check.
|
|
55
|
+
const DEFAULT_MAX_BUFFERED_BYTES = 1 << 20; // 1 MiB
|
|
56
|
+
|
|
57
|
+
// Lazy-load WebSocket. We prefer the official `openai` SDK helper if
|
|
58
|
+
// available, but fall back to plain `ws` so the framework doesn't force
|
|
59
|
+
// the user to install a new dependency until they actually opt in.
|
|
60
|
+
async function loadWsCtor() {
|
|
61
|
+
try {
|
|
62
|
+
const sdk = await import("openai/realtime/ws");
|
|
63
|
+
return { type: "sdk", ctor: sdk.OpenAIRealtimeWS };
|
|
64
|
+
} catch { /* fall through */ }
|
|
65
|
+
try {
|
|
66
|
+
const wsMod = await import("ws");
|
|
67
|
+
const WebSocket = wsMod.default || wsMod.WebSocket;
|
|
68
|
+
return { type: "ws", ctor: WebSocket };
|
|
69
|
+
} catch (err) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
"Neither `openai/realtime/ws` nor `ws` is installed. " +
|
|
72
|
+
"Install one: `npm i openai` (recommended) or `npm i ws`."
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
class OpenAIRealtimeProvider extends VoiceProvider {
|
|
78
|
+
constructor(opts = {}) {
|
|
79
|
+
super(opts);
|
|
80
|
+
this.transport = opts.transport || "ws";
|
|
81
|
+
this.model = opts.model || OPENAI_REALTIME_DEFAULT_MODEL;
|
|
82
|
+
this.apiKey = opts.apiKey || process.env.OPENAI_API_KEY;
|
|
83
|
+
this.safetyIdentifier = opts.safetyIdentifier || process.env.OPENAI_SAFETY_IDENTIFIER;
|
|
84
|
+
this.socket = null; // underlying WS instance
|
|
85
|
+
this.sessionConfig = null;
|
|
86
|
+
this.sessionId = null; // server-assigned session id
|
|
87
|
+
this.responseId = null; // current in-flight response (if any)
|
|
88
|
+
this.lastAgentAudioStartedAt = null; // ms since-connect when agent started speaking
|
|
89
|
+
this.lastAgentAudioPlayedMs = 0; // ms of agent audio the user has heard
|
|
90
|
+
this._connectedAt = null;
|
|
91
|
+
this._turnAudioStartedAt = null;
|
|
92
|
+
|
|
93
|
+
// ── Reconnect / upstream-drop handling (H4) ──────────────────────────
|
|
94
|
+
// close() flips this so _onClose can tell a deliberate client close
|
|
95
|
+
// apart from an unexpected upstream socket drop. Only the latter
|
|
96
|
+
// triggers a bounded reconnect.
|
|
97
|
+
this._clientClosed = false;
|
|
98
|
+
this._reconnecting = false;
|
|
99
|
+
this._reconnectAttempts = 0;
|
|
100
|
+
this.maxReconnectAttempts = opts.maxReconnectAttempts ?? 3;
|
|
101
|
+
this.reconnectBaseDelayMs = opts.reconnectBaseDelayMs ?? 250;
|
|
102
|
+
// Seams so tests can drive reconnect without real sockets/timers.
|
|
103
|
+
this._setTimeout = opts.setTimeout || setTimeout;
|
|
104
|
+
this._clearTimeout = opts.clearTimeout || clearTimeout;
|
|
105
|
+
this._reconnectTimer = null;
|
|
106
|
+
// Optional injected WebSocket constructor — when provided, _connectWebSocket
|
|
107
|
+
// uses it instead of dynamically loading `openai`/`ws`. Lets tests drive a
|
|
108
|
+
// fake socket with no real network. Shape: (url, { headers }) => socketLike.
|
|
109
|
+
this._wsCtorOverride = opts.wsCtor || null;
|
|
110
|
+
|
|
111
|
+
// ── Backpressure (L12) ───────────────────────────────────────────────
|
|
112
|
+
// Ceiling on socket.bufferedAmount before we start dropping *audio* frames.
|
|
113
|
+
// Control frames (session.update, response.*, tool results) always go out.
|
|
114
|
+
this.maxBufferedBytes = opts.maxBufferedBytes ?? DEFAULT_MAX_BUFFERED_BYTES;
|
|
115
|
+
// Count of audio frames dropped due to backpressure / non-open socket, for
|
|
116
|
+
// observability. Not part of any wire protocol.
|
|
117
|
+
this.droppedAudioFrames = 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Open the upstream session. Awaits the server's session.created event. */
|
|
121
|
+
async connect(sessionConfig) {
|
|
122
|
+
if (this.state !== "idle" && this.state !== "closed") {
|
|
123
|
+
throw new Error(`connect() called from invalid state: ${this.state}`);
|
|
124
|
+
}
|
|
125
|
+
if (!this.apiKey) {
|
|
126
|
+
throw new Error("OPENAI_API_KEY not set");
|
|
127
|
+
}
|
|
128
|
+
this.sessionConfig = sessionConfig;
|
|
129
|
+
|
|
130
|
+
if (this.transport === "ws") {
|
|
131
|
+
await this._connectWebSocket();
|
|
132
|
+
} else if (this.transport === "webrtc") {
|
|
133
|
+
throw new Error("WebRTC transport in this provider is not wired in yet — use 'ws' for server-side agents");
|
|
134
|
+
} else {
|
|
135
|
+
throw new Error(`unknown transport: ${this.transport}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Send the initial session.update so the model knows our voice / tools / VAD.
|
|
139
|
+
this._send({
|
|
140
|
+
type: "session.update",
|
|
141
|
+
session: sessionConfig,
|
|
142
|
+
});
|
|
143
|
+
this.state = "connected";
|
|
144
|
+
this.emit(VOICE_EVENTS.CONNECTED, { transport: this.transport, model: this.model });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
_connectWebSocket() {
|
|
148
|
+
const loader = this._wsCtorOverride
|
|
149
|
+
? Promise.resolve({ type: "factory", ctor: this._wsCtorOverride })
|
|
150
|
+
: loadWsCtor();
|
|
151
|
+
return loader.then(({ type, ctor }) => {
|
|
152
|
+
return new Promise((resolve, reject) => {
|
|
153
|
+
const url = `${OPENAI_REALTIME_WS_URL}?model=${encodeURIComponent(this.model)}`;
|
|
154
|
+
const headers = {
|
|
155
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
156
|
+
"OpenAI-Beta": "realtime=v1",
|
|
157
|
+
};
|
|
158
|
+
if (this.safetyIdentifier) {
|
|
159
|
+
headers["OpenAI-Safety-Identifier"] = this.safetyIdentifier;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
let socket;
|
|
163
|
+
if (type === "sdk") {
|
|
164
|
+
// The official OpenAIRealtimeWS instance manages its own WebSocket;
|
|
165
|
+
// its .socket property is the raw WS. We wire to the underlying.
|
|
166
|
+
const rt = new ctor({ model: this.model, apiKey: this.apiKey });
|
|
167
|
+
socket = rt.socket;
|
|
168
|
+
this._sdkRt = rt;
|
|
169
|
+
} else if (type === "factory") {
|
|
170
|
+
// Injected socket factory (tests): called as a plain function,
|
|
171
|
+
// NOT with `new`, so a fake socket can be returned directly.
|
|
172
|
+
socket = ctor(url, { headers });
|
|
173
|
+
} else {
|
|
174
|
+
socket = new ctor(url, { headers });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
this.socket = socket;
|
|
178
|
+
this._connectedAt = Date.now();
|
|
179
|
+
|
|
180
|
+
const onOpen = () => resolve();
|
|
181
|
+
const onError = (err) => reject(err instanceof Error ? err : new Error(String(err?.message || err)));
|
|
182
|
+
if (typeof socket.addEventListener === "function") {
|
|
183
|
+
socket.addEventListener("open", onOpen, { once: true });
|
|
184
|
+
socket.addEventListener("error", onError, { once: true });
|
|
185
|
+
} else {
|
|
186
|
+
socket.once?.("open", onOpen);
|
|
187
|
+
socket.once?.("error", onError);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const handleMessage = (raw) => this._onServerEvent(raw);
|
|
191
|
+
if (typeof socket.addEventListener === "function") {
|
|
192
|
+
socket.addEventListener("message", (evt) => handleMessage(evt.data));
|
|
193
|
+
socket.addEventListener("close", (evt) => this._onClose(evt.reason || "closed"));
|
|
194
|
+
socket.addEventListener("error", (evt) => this.emit(VOICE_EVENTS.ERROR, evt));
|
|
195
|
+
} else {
|
|
196
|
+
socket.on?.("message", handleMessage);
|
|
197
|
+
socket.on?.("close", (code, reason) => this._onClose(reason || `code ${code}`));
|
|
198
|
+
socket.on?.("error", (err) => this.emit(VOICE_EVENTS.ERROR, err));
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Stream PCM16 audio (base64-encoded chunk OR raw Buffer/Uint8Array). */
|
|
205
|
+
appendAudio(pcm16) {
|
|
206
|
+
if (this.state !== "connected" && this.state !== "speaking" && this.state !== "listening") return;
|
|
207
|
+
// Backpressure: if the socket's send buffer is already backed up, drop this
|
|
208
|
+
// audio frame instead of queueing it. Audio is a live stream — a stale
|
|
209
|
+
// chunk delivered late is worse than a dropped one, and unbounded queueing
|
|
210
|
+
// adds audible latency and can exhaust memory on a long stalled call. Only
|
|
211
|
+
// audio is shed; control frames in _send() are never dropped.
|
|
212
|
+
if (this._isBackpressured()) {
|
|
213
|
+
this.droppedAudioFrames += 1;
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const b64 = typeof pcm16 === "string" ? pcm16 : Buffer.from(pcm16).toString("base64");
|
|
217
|
+
this._send({ type: "input_audio_buffer.append", audio: b64 });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* True when the underlying socket is not OPEN, or its send buffer has grown
|
|
222
|
+
* past maxBufferedBytes (i.e. we're writing faster than the wire drains).
|
|
223
|
+
* Robust to fake/SDK sockets that don't expose readyState/bufferedAmount.
|
|
224
|
+
* @returns {boolean}
|
|
225
|
+
*/
|
|
226
|
+
_isBackpressured() {
|
|
227
|
+
const s = this.socket;
|
|
228
|
+
if (!s) return true;
|
|
229
|
+
if (typeof s.readyState === "number" && s.readyState !== WS_OPEN) return true;
|
|
230
|
+
if (
|
|
231
|
+
this.maxBufferedBytes > 0 &&
|
|
232
|
+
typeof s.bufferedAmount === "number" &&
|
|
233
|
+
s.bufferedAmount > this.maxBufferedBytes
|
|
234
|
+
) {
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Commit the current audio buffer as one user turn. */
|
|
241
|
+
commitTurn() {
|
|
242
|
+
this._send({ type: "input_audio_buffer.commit" });
|
|
243
|
+
this._send({ type: "response.create" }); // ask the model to respond
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Send a text-only user message (no audio). */
|
|
247
|
+
sendText(text) {
|
|
248
|
+
this._send({
|
|
249
|
+
type: "conversation.item.create",
|
|
250
|
+
item: {
|
|
251
|
+
type: "message",
|
|
252
|
+
role: "user",
|
|
253
|
+
content: [{ type: "input_text", text }],
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
this._send({ type: "response.create" });
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Barge-in. The caller passes the number of milliseconds of agent audio
|
|
261
|
+
* the user actually heard before interrupting; we truncate the model's
|
|
262
|
+
* server-side conversation item to match so subsequent turns don't
|
|
263
|
+
* "remember" content the human never heard.
|
|
264
|
+
*/
|
|
265
|
+
interrupt(opts = {}) {
|
|
266
|
+
const playedMs = Number(opts.playedMs ?? this.lastAgentAudioPlayedMs ?? 0);
|
|
267
|
+
if (this.responseId) {
|
|
268
|
+
this._send({ type: "response.cancel" });
|
|
269
|
+
}
|
|
270
|
+
if (opts.itemId && playedMs > 0) {
|
|
271
|
+
this._send({
|
|
272
|
+
type: "conversation.item.truncate",
|
|
273
|
+
item_id: opts.itemId,
|
|
274
|
+
content_index: 0,
|
|
275
|
+
audio_end_ms: playedMs,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
this.responseId = null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Return a tool-call result and resume the model. Output must be a
|
|
283
|
+
* JSON-serialisable value (object or scalar) — we stringify it for you.
|
|
284
|
+
*/
|
|
285
|
+
sendToolResult(callId, output) {
|
|
286
|
+
if (!callId) throw new Error("sendToolResult requires a call_id");
|
|
287
|
+
this._send({
|
|
288
|
+
type: "conversation.item.create",
|
|
289
|
+
item: {
|
|
290
|
+
type: "function_call_output",
|
|
291
|
+
call_id: callId,
|
|
292
|
+
output: typeof output === "string" ? output : JSON.stringify(output),
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
this._send({ type: "response.create" });
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Close the upstream session cleanly. */
|
|
299
|
+
close(reason) {
|
|
300
|
+
// Mark this as a deliberate, client-initiated close so an ensuing
|
|
301
|
+
// socket 'close' event is NOT treated as an upstream drop (H4).
|
|
302
|
+
this._clientClosed = true;
|
|
303
|
+
if (this._reconnectTimer) {
|
|
304
|
+
try { this._clearTimeout(this._reconnectTimer); } catch { /* */ }
|
|
305
|
+
this._reconnectTimer = null;
|
|
306
|
+
}
|
|
307
|
+
this._reconnecting = false;
|
|
308
|
+
if (this.state === "closed") return;
|
|
309
|
+
try { this.socket?.close?.(); } catch { /* ignore */ }
|
|
310
|
+
this.state = "closed";
|
|
311
|
+
this.emit(VOICE_EVENTS.CLOSED, { reason: reason || "client_close" });
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ── Internals ────────────────────────────────────────────────────────────
|
|
315
|
+
|
|
316
|
+
_send(payload) {
|
|
317
|
+
const s = this.socket;
|
|
318
|
+
if (!s || this.state === "closed") return;
|
|
319
|
+
// Don't write to a socket that isn't OPEN (CONNECTING / CLOSING / CLOSED):
|
|
320
|
+
// socket.send() would otherwise throw on the `ws` package. We still allow
|
|
321
|
+
// sends in the "reconnecting" state because _attemptReconnect re-seeds
|
|
322
|
+
// session.update on a freshly-opened socket (readyState OPEN) before
|
|
323
|
+
// flipping state back to "connected". Fake/SDK sockets without a numeric
|
|
324
|
+
// readyState fall through to the send (preserving existing behaviour).
|
|
325
|
+
if (typeof s.readyState === "number" && s.readyState !== WS_OPEN) return;
|
|
326
|
+
try {
|
|
327
|
+
s.send(JSON.stringify(payload));
|
|
328
|
+
} catch (err) {
|
|
329
|
+
this.emit(VOICE_EVENTS.ERROR, err);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
_onClose(reason) {
|
|
334
|
+
// A deliberate client close() — keep the existing behaviour: tear down
|
|
335
|
+
// and emit CLOSED exactly once. close() already set state=closed and
|
|
336
|
+
// emitted CLOSED, so don't double-emit.
|
|
337
|
+
if (this._clientClosed) {
|
|
338
|
+
this.state = "closed";
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
// Unexpected upstream drop. Don't silently flip to "closed" (which would
|
|
342
|
+
// make appendAudio/_send early-return → dead air). Attempt a bounded
|
|
343
|
+
// reconnect; only on exhaustion do we tear down deterministically (H4).
|
|
344
|
+
this._scheduleReconnect(reason);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Schedule a bounded reconnect after an upstream drop. Backs off
|
|
349
|
+
* exponentially from reconnectBaseDelayMs. On exhaustion, tears down
|
|
350
|
+
* deterministically with a clear error + CLOSED event rather than going
|
|
351
|
+
* silent. No-ops if a client close() already happened.
|
|
352
|
+
*/
|
|
353
|
+
_scheduleReconnect(reason) {
|
|
354
|
+
if (this._clientClosed) return;
|
|
355
|
+
if (this._reconnecting) return; // a reconnect cycle is already in flight
|
|
356
|
+
if (this._reconnectAttempts >= this.maxReconnectAttempts) {
|
|
357
|
+
this._failReconnect(reason);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
this._reconnecting = true;
|
|
361
|
+
this.state = "reconnecting";
|
|
362
|
+
const attempt = this._reconnectAttempts + 1;
|
|
363
|
+
const delay = this.reconnectBaseDelayMs * Math.pow(2, this._reconnectAttempts);
|
|
364
|
+
this.emit(VOICE_EVENTS.ERROR, new Error(
|
|
365
|
+
`upstream session dropped (${reason}); reconnect attempt ${attempt}/${this.maxReconnectAttempts} in ${delay}ms`,
|
|
366
|
+
));
|
|
367
|
+
this._reconnectTimer = this._setTimeout(() => {
|
|
368
|
+
this._reconnectTimer = null;
|
|
369
|
+
this._attemptReconnect(reason);
|
|
370
|
+
}, delay);
|
|
371
|
+
if (this._reconnectTimer && typeof this._reconnectTimer.unref === "function") {
|
|
372
|
+
this._reconnectTimer.unref();
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** Perform one reconnect attempt: reopen WS + re-send the stored config. */
|
|
377
|
+
async _attemptReconnect(reason) {
|
|
378
|
+
if (this._clientClosed) { this._reconnecting = false; return; }
|
|
379
|
+
this._reconnectAttempts += 1;
|
|
380
|
+
try {
|
|
381
|
+
this.socket = null;
|
|
382
|
+
await this._connectWebSocket();
|
|
383
|
+
// Re-seed the new socket with the session config the model needs.
|
|
384
|
+
if (this.sessionConfig) {
|
|
385
|
+
this._send({ type: "session.update", session: this.sessionConfig });
|
|
386
|
+
}
|
|
387
|
+
// Back to a live state — clear the reconnect bookkeeping.
|
|
388
|
+
this.state = "connected";
|
|
389
|
+
this._reconnecting = false;
|
|
390
|
+
this._reconnectAttempts = 0;
|
|
391
|
+
this.emit(VOICE_EVENTS.CONNECTED, {
|
|
392
|
+
transport: this.transport,
|
|
393
|
+
model: this.model,
|
|
394
|
+
reconnected: true,
|
|
395
|
+
});
|
|
396
|
+
} catch (err) {
|
|
397
|
+
// This attempt failed — schedule the next one (or fail if exhausted).
|
|
398
|
+
this._reconnecting = false;
|
|
399
|
+
if (this._reconnectAttempts >= this.maxReconnectAttempts) {
|
|
400
|
+
this._failReconnect(err?.message || reason);
|
|
401
|
+
} else {
|
|
402
|
+
this._scheduleReconnect(err?.message || reason);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** Give up after exhausting reconnects: deterministic teardown + error. */
|
|
408
|
+
_failReconnect(reason) {
|
|
409
|
+
this._reconnecting = false;
|
|
410
|
+
if (this._reconnectTimer) {
|
|
411
|
+
try { this._clearTimeout(this._reconnectTimer); } catch { /* */ }
|
|
412
|
+
this._reconnectTimer = null;
|
|
413
|
+
}
|
|
414
|
+
this.state = "closed";
|
|
415
|
+
this.emit(VOICE_EVENTS.ERROR, new Error(
|
|
416
|
+
`upstream session lost and reconnect exhausted after ${this.maxReconnectAttempts} attempts (${reason})`,
|
|
417
|
+
));
|
|
418
|
+
this.emit(VOICE_EVENTS.CLOSED, { reason: `upstream_drop_reconnect_exhausted: ${reason}` });
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
_onServerEvent(raw) {
|
|
422
|
+
let evt;
|
|
423
|
+
try {
|
|
424
|
+
evt = typeof raw === "string" ? JSON.parse(raw) : JSON.parse(Buffer.from(raw).toString("utf-8"));
|
|
425
|
+
} catch (err) {
|
|
426
|
+
this.emit(VOICE_EVENTS.ERROR, new Error(`bad server frame: ${err.message}`));
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
// Re-emit using our normalised event names. We pass through anything
|
|
430
|
+
// unrecognised under its native `type` so downstream consumers can
|
|
431
|
+
// still observe vendor-specific events.
|
|
432
|
+
switch (evt.type) {
|
|
433
|
+
case "session.created":
|
|
434
|
+
this.sessionId = evt.session?.id || null;
|
|
435
|
+
this.emit(VOICE_EVENTS.SESSION_CREATED, evt.session || {});
|
|
436
|
+
break;
|
|
437
|
+
case "input_audio_buffer.speech_started":
|
|
438
|
+
this.emit(VOICE_EVENTS.USER_SPEECH_STARTED, evt);
|
|
439
|
+
break;
|
|
440
|
+
case "input_audio_buffer.speech_stopped":
|
|
441
|
+
this.emit(VOICE_EVENTS.USER_SPEECH_STOPPED, evt);
|
|
442
|
+
break;
|
|
443
|
+
case "response.created":
|
|
444
|
+
this.responseId = evt.response?.id || null;
|
|
445
|
+
break;
|
|
446
|
+
case "response.audio.delta":
|
|
447
|
+
if (!this._turnAudioStartedAt) this._turnAudioStartedAt = Date.now();
|
|
448
|
+
this.emit(VOICE_EVENTS.AUDIO_DELTA, {
|
|
449
|
+
delta: evt.delta,
|
|
450
|
+
item_id: evt.item_id,
|
|
451
|
+
response_id: evt.response_id,
|
|
452
|
+
});
|
|
453
|
+
break;
|
|
454
|
+
case "response.audio.done":
|
|
455
|
+
this._turnAudioStartedAt = null;
|
|
456
|
+
this.emit(VOICE_EVENTS.AUDIO_DONE, evt);
|
|
457
|
+
break;
|
|
458
|
+
case "response.audio_transcript.delta":
|
|
459
|
+
case "response.output_text.delta":
|
|
460
|
+
this.emit(VOICE_EVENTS.TRANSCRIPT_DELTA, {
|
|
461
|
+
delta: evt.delta,
|
|
462
|
+
role: evt.type.includes("audio_transcript") ? "assistant_voice" : "assistant_text",
|
|
463
|
+
item_id: evt.item_id,
|
|
464
|
+
response_id: evt.response_id,
|
|
465
|
+
});
|
|
466
|
+
break;
|
|
467
|
+
case "response.audio_transcript.done":
|
|
468
|
+
case "response.output_text.done":
|
|
469
|
+
this.emit(VOICE_EVENTS.TRANSCRIPT_DONE, evt);
|
|
470
|
+
break;
|
|
471
|
+
case "response.function_call_arguments.delta":
|
|
472
|
+
this.emit(VOICE_EVENTS.TOOL_CALL_DELTA, {
|
|
473
|
+
call_id: evt.call_id,
|
|
474
|
+
name: evt.name,
|
|
475
|
+
delta: evt.delta,
|
|
476
|
+
});
|
|
477
|
+
break;
|
|
478
|
+
case "response.function_call_arguments.done":
|
|
479
|
+
this.emit(VOICE_EVENTS.TOOL_CALL, {
|
|
480
|
+
call_id: evt.call_id,
|
|
481
|
+
name: evt.name,
|
|
482
|
+
arguments: safeParseJson(evt.arguments),
|
|
483
|
+
response_id: evt.response_id,
|
|
484
|
+
});
|
|
485
|
+
break;
|
|
486
|
+
case "response.done":
|
|
487
|
+
this.responseId = null;
|
|
488
|
+
this.emit(VOICE_EVENTS.TURN_DONE, evt.response || {});
|
|
489
|
+
break;
|
|
490
|
+
case "error":
|
|
491
|
+
this.emit(VOICE_EVENTS.ERROR, new Error(evt.error?.message || "unknown"));
|
|
492
|
+
break;
|
|
493
|
+
default:
|
|
494
|
+
// Surface as a generic event so callers wanting raw access can subscribe.
|
|
495
|
+
this.emit(evt.type, evt);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function safeParseJson(s) {
|
|
501
|
+
if (typeof s !== "string") return s;
|
|
502
|
+
try { return JSON.parse(s); } catch { return { _raw: s }; }
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
export function createOpenAIRealtimeProvider(opts = {}) {
|
|
506
|
+
return new OpenAIRealtimeProvider(opts);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
export { OpenAIRealtimeProvider };
|
|
510
|
+
export default createOpenAIRealtimeProvider;
|