@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,1206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/model-router/resolve.mjs — the resolveChain "brain" of the router (v2).
|
|
3
|
+
*
|
|
4
|
+
* This is the policy engine that sits on top of the catalog + failover + ledger
|
|
5
|
+
* substrate (catalog.mjs, taxonomy.mjs, failover.mjs, health.mjs, ledger.mjs).
|
|
6
|
+
* `resolveChain(request, opts)` is a PURE function of its injected snapshot —
|
|
7
|
+
* config, catalog, breaker state, budget band, env, clock — so it makes no
|
|
8
|
+
* network call and (when the snapshot is injected) touches no filesystem. It
|
|
9
|
+
* returns a `RouteDecision` describing which model/harness to run a unit of work
|
|
10
|
+
* on, the env to retarget a session spawn, the spawn args, an estimated cost,
|
|
11
|
+
* the candidates it skipped (with reasons), and a MANDATORY one-line `explain`.
|
|
12
|
+
*
|
|
13
|
+
* Routing happens at task-dispatch time, NEVER mid-session. Cache prefixes are
|
|
14
|
+
* model-scoped; a mid-session switch silently re-bills the entire prefix.
|
|
15
|
+
*
|
|
16
|
+
* resolveChain(req):
|
|
17
|
+
* 0 MAESTRO_ROUTER_FORCE_ANTHROPIC kill switch → Anthropic-only chain
|
|
18
|
+
* 1 ensure req.data_class (deny-default "sensitive") + needs_tool_use (W5)
|
|
19
|
+
* 2 first-match over routing_policy (strict-validated structured features)
|
|
20
|
+
* 3 rule.chain aliases → catalog rows
|
|
21
|
+
* 4 gate each candidate IN ORDER, recording rejections in tried[]:
|
|
22
|
+
* status ▸ harness ▸ capability/grade ▸ context ▸ data_class
|
|
23
|
+
* ▸ credential(auth_env) ▸ breaker(health.isOpen) ▸ budget ladder
|
|
24
|
+
* 5 affinity pin moved to head when it still passes the gates
|
|
25
|
+
* 6 fallback_to_anthropic safety net if everything is rejected
|
|
26
|
+
* 7 build chosen + envForSpawn + spawnArgs + estCostUSD + explain + audit
|
|
27
|
+
*
|
|
28
|
+
* House style: ESM .mjs, Node 20, near-zero-dep; mirrors the DI discipline of
|
|
29
|
+
* lib/resource-governor.mjs / lib/rate-guard.mjs — every external dependency
|
|
30
|
+
* (catalog, breaker, env, clock, budget band) is injectable. NEVER throws on the
|
|
31
|
+
* resolve path: a broken snapshot degrades to the Anthropic safety net.
|
|
32
|
+
*
|
|
33
|
+
* @module lib/model-router/resolve
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import { loadCatalogCached, lookupModel } from "./catalog.mjs";
|
|
37
|
+
import { estimateCost } from "./ledger.mjs";
|
|
38
|
+
import { keyFor, isOpen as breakerIsOpen } from "./health.mjs";
|
|
39
|
+
import { nextCredential as authNextCredential, available as authAvailable } from "./auth-profiles.mjs";
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Constants / vocabulary
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
const GRADE_RANK = { A: 3, B: 2, C: 1 };
|
|
46
|
+
const DATA_CLASSES = new Set(["public", "internal", "sensitive"]);
|
|
47
|
+
const DATA_CLASS_RANK = { public: 1, internal: 2, sensitive: 3 };
|
|
48
|
+
const HARNESSES = new Set(["session", "direct", "batch"]);
|
|
49
|
+
|
|
50
|
+
// The session-retarget transport: a row whose chosen harness is `session` runs
|
|
51
|
+
// inside `claude` via ANTHROPIC_BASE_URL. Anthropic rows keep the stock CLI/API;
|
|
52
|
+
// third-party rows that expose an `/anthropic` endpoint are "anthropic-native".
|
|
53
|
+
const TRANSPORT = Object.freeze({
|
|
54
|
+
anthropicCli: "anthropic-cli",
|
|
55
|
+
anthropicApi: "anthropic-api",
|
|
56
|
+
anthropicNative: "anthropic-native",
|
|
57
|
+
anthropicMessages: "anthropic-messages",
|
|
58
|
+
openaiCompletions: "openai-completions",
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// The closed set of routing_policy match keys. A key outside this set is a
|
|
62
|
+
// strict-validation error (W10 — a typo'd safety rule must fail loudly, never
|
|
63
|
+
// silently never-match). All match against STRUCTURED METADATA only — message
|
|
64
|
+
// content never reaches the rule engine (prompt-injection cannot steer T0).
|
|
65
|
+
const VALID_MATCH_KEYS = new Set([
|
|
66
|
+
// v2 features
|
|
67
|
+
"task_class",
|
|
68
|
+
"channel",
|
|
69
|
+
"source",
|
|
70
|
+
"data_class",
|
|
71
|
+
"lane",
|
|
72
|
+
"priority",
|
|
73
|
+
"agent_role",
|
|
74
|
+
"agent_role_in",
|
|
75
|
+
"needs_tool_use",
|
|
76
|
+
"needs_thinking",
|
|
77
|
+
"needs_vision",
|
|
78
|
+
"needs_long_context",
|
|
79
|
+
"needs_parallel_tools",
|
|
80
|
+
"token_estimate_gte",
|
|
81
|
+
"token_estimate_lt",
|
|
82
|
+
"budget_band_gte",
|
|
83
|
+
"pin",
|
|
84
|
+
// glob-friendly alternates handled by matchValue (task_class supports "*")
|
|
85
|
+
]);
|
|
86
|
+
|
|
87
|
+
// Default per-task-class maxTurns; falls back to a generic ceiling.
|
|
88
|
+
const DEFAULT_MAX_TURNS = 40;
|
|
89
|
+
const TASK_CLASS_MAX_TURNS = Object.freeze({
|
|
90
|
+
"classify.inbox": 1,
|
|
91
|
+
"lookup": 2,
|
|
92
|
+
"enrich": 3,
|
|
93
|
+
"voice.brief": 4,
|
|
94
|
+
"learning.reflect": 6,
|
|
95
|
+
"learning.curate": 6,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// Small helpers
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
function nowMs(opts) {
|
|
103
|
+
return (opts && typeof opts.now === "function" ? opts.now : Date.now)();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function envOf(opts) {
|
|
107
|
+
return (opts && opts.env) || process.env || {};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function isForceAnthropic(env) {
|
|
111
|
+
const v = env && env.MAESTRO_ROUTER_FORCE_ANTHROPIC;
|
|
112
|
+
return v === "1" || v === "true";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* A short, sortable, ULID-ish id: 10 chars of base32 time + 8 random base32.
|
|
117
|
+
* Not a real ULID (no Crockford spec guarantees) but monotonic-ish and unique
|
|
118
|
+
* enough to join a decision row ↔ ledger row ↔ resume marker.
|
|
119
|
+
*/
|
|
120
|
+
const B32 = "0123456789abcdefghjkmnpqrstvwxyz";
|
|
121
|
+
function ulidish(now, rnd = Math.random) {
|
|
122
|
+
let t = Math.floor(now);
|
|
123
|
+
let time = "";
|
|
124
|
+
for (let i = 0; i < 10; i++) {
|
|
125
|
+
time = B32[t % 32] + time;
|
|
126
|
+
t = Math.floor(t / 32);
|
|
127
|
+
}
|
|
128
|
+
let rand = "";
|
|
129
|
+
for (let i = 0; i < 8; i++) rand += B32[Math.floor(rnd() * 32)];
|
|
130
|
+
return time + rand;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Resolve the merged catalog from opts (injected catalog wins; else load). */
|
|
134
|
+
function resolveCatalog(agentRoot, opts) {
|
|
135
|
+
if (opts && opts.catalog) {
|
|
136
|
+
// Accept either a CatalogResult or its .catalog
|
|
137
|
+
return opts.catalog.byRef ? opts.catalog : opts.catalog.catalog || opts.catalog;
|
|
138
|
+
}
|
|
139
|
+
return loadCatalogCached(agentRoot, opts || {});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// Policy rule matching (structured metadata only)
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
/** Glob match supporting a single trailing/embedded "*" (task_class "lookup.*"). */
|
|
147
|
+
function globMatch(pattern, value) {
|
|
148
|
+
if (typeof pattern !== "string") return pattern === value;
|
|
149
|
+
if (!pattern.includes("*")) return pattern === value;
|
|
150
|
+
const re = new RegExp(
|
|
151
|
+
"^" + pattern.split("*").map((s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*") + "$"
|
|
152
|
+
);
|
|
153
|
+
return typeof value === "string" && re.test(value);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function matchesRule(match, req) {
|
|
157
|
+
for (const [key, expected] of Object.entries(match || {})) {
|
|
158
|
+
switch (key) {
|
|
159
|
+
case "token_estimate_gte":
|
|
160
|
+
if ((req.token_estimate ?? 0) < expected) return false;
|
|
161
|
+
break;
|
|
162
|
+
case "token_estimate_lt":
|
|
163
|
+
if ((req.token_estimate ?? 0) >= expected) return false;
|
|
164
|
+
break;
|
|
165
|
+
case "budget_band_gte":
|
|
166
|
+
if ((req.budget_band ?? 0) < expected) return false;
|
|
167
|
+
break;
|
|
168
|
+
case "agent_role_in":
|
|
169
|
+
if (!Array.isArray(expected) || !expected.includes(req.agent_role)) return false;
|
|
170
|
+
break;
|
|
171
|
+
case "task_class":
|
|
172
|
+
case "channel":
|
|
173
|
+
case "source":
|
|
174
|
+
case "lane":
|
|
175
|
+
if (!globMatch(expected, req[key])) return false;
|
|
176
|
+
break;
|
|
177
|
+
case "pin":
|
|
178
|
+
if (Boolean(req.pin) !== Boolean(expected)) return false;
|
|
179
|
+
break;
|
|
180
|
+
default:
|
|
181
|
+
if (req[key] !== expected) return false;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** First-match over routing_policy. Returns { rule, index } or the default. */
|
|
188
|
+
function pickRule(policy, req) {
|
|
189
|
+
const rules = Array.isArray(policy) ? policy : [];
|
|
190
|
+
for (let i = 0; i < rules.length; i++) {
|
|
191
|
+
const rule = rules[i];
|
|
192
|
+
if (!rule) continue;
|
|
193
|
+
if (rule.default) return { rule, index: i };
|
|
194
|
+
if (matchesRule(rule.match || {}, req)) return { rule, index: i };
|
|
195
|
+
}
|
|
196
|
+
// No rule matched and no explicit default — synthesize one.
|
|
197
|
+
return { rule: { default: true, synthesized: true }, index: -1 };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
// Alias resolution → catalog refs
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Resolve a chain of aliases/refs to concrete catalog refs, preserving order.
|
|
206
|
+
* A "rules" member (the caller's deterministic fallback as a chain member) is
|
|
207
|
+
* passed through as a sentinel ref so failover.mjs / the caller can treat it as
|
|
208
|
+
* a terminal candidate. Unknown aliases collapse to null (recorded by caller).
|
|
209
|
+
*/
|
|
210
|
+
function resolveChainRefs(chainSpec, aliases) {
|
|
211
|
+
const out = [];
|
|
212
|
+
for (const member of Array.isArray(chainSpec) ? chainSpec : []) {
|
|
213
|
+
if (member === "rules") {
|
|
214
|
+
out.push({ ref: "rules", isRules: true, alias: "rules" });
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (typeof member !== "string") continue;
|
|
218
|
+
// A member may be an alias name OR a literal "provider/model" ref.
|
|
219
|
+
const ref = aliases && Object.prototype.hasOwnProperty.call(aliases, member)
|
|
220
|
+
? aliases[member]
|
|
221
|
+
: member;
|
|
222
|
+
out.push({ ref, isRules: false, alias: member === ref ? null : member });
|
|
223
|
+
}
|
|
224
|
+
return out;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ---------------------------------------------------------------------------
|
|
228
|
+
// Budget ladder (degrade, never cliff) — operates on the alias list
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
|
|
231
|
+
const DEGRADE_ORDER = ["frontier", "default", "fast", "cheap"];
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Apply the budget ladder to a resolved chain of alias-tagged refs. Returns a
|
|
235
|
+
* possibly-reordered/extended ref list + a note for the explain string. Pins
|
|
236
|
+
* and critical priority are exempt below band 100.
|
|
237
|
+
*/
|
|
238
|
+
function applyBudgetLadder(refs, req, aliases, band) {
|
|
239
|
+
if (!band || band < 75) return { refs, note: null };
|
|
240
|
+
const exempt = req.pin || req.priority === "critical";
|
|
241
|
+
if (exempt) return { refs, note: `band ${band} (exempt: ${req.pin ? "pin" : "critical"})` };
|
|
242
|
+
|
|
243
|
+
// Map known aliases to their degrade index; downgrade one tier at >=75.
|
|
244
|
+
const cheapest = (name) => {
|
|
245
|
+
const i = DEGRADE_ORDER.indexOf(name);
|
|
246
|
+
return i === -1 ? name : DEGRADE_ORDER[Math.min(DEGRADE_ORDER.length - 1, i + 1)];
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
if (band >= 90) {
|
|
250
|
+
// cheap/fast only: drop frontier/default aliases from the head, keep cheap/fast.
|
|
251
|
+
const kept = refs.filter((r) => r.alias !== "frontier" && r.alias !== "default");
|
|
252
|
+
const head = kept.length ? kept : refs; // never end up with an empty chain
|
|
253
|
+
return { refs: head, note: `band ${band} downgraded to cheap/fast lane` };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// band >= 75: downgrade one tier where the alias is a known degrade rung.
|
|
257
|
+
let changed = false;
|
|
258
|
+
const next = refs.map((r) => {
|
|
259
|
+
if (r.alias && DEGRADE_ORDER.includes(r.alias)) {
|
|
260
|
+
const downAlias = cheapest(r.alias);
|
|
261
|
+
if (downAlias !== r.alias && aliases[downAlias]) {
|
|
262
|
+
changed = true;
|
|
263
|
+
return { ref: aliases[downAlias], isRules: false, alias: downAlias };
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return r;
|
|
267
|
+
});
|
|
268
|
+
return { refs: next, note: changed ? `band ${band} downgraded one tier` : `band ${band}` };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ---------------------------------------------------------------------------
|
|
272
|
+
// Per-candidate gating
|
|
273
|
+
// ---------------------------------------------------------------------------
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Resolve the credential a row needs, honoring an optional pooled auth-profile
|
|
277
|
+
* set (OpenClaw auth-profile rotation). ADDITIVE + FAIL-OPEN:
|
|
278
|
+
* - No pool wired (ctx.authProfiles absent) OR provider not pooled (<=1 key) →
|
|
279
|
+
* fall back to the single env var exactly as before (token = env[auth_env]).
|
|
280
|
+
* - Pooled (>1 key) → pick the next non-cooled credential round-robin; if every
|
|
281
|
+
* key is cooling, return { token:null, poolExhausted:true } so the gate skips
|
|
282
|
+
* this backend with `missing_credential` (the pool looks dry — failover moves
|
|
283
|
+
* on, the per-key cooldowns self-heal independently).
|
|
284
|
+
*
|
|
285
|
+
* Returns { token: string|null, pooled: boolean, fingerprint: string|null,
|
|
286
|
+
* poolExhausted: boolean }.
|
|
287
|
+
*/
|
|
288
|
+
function resolveRowCredential(row, ctx) {
|
|
289
|
+
const authEnv = row.auth_env;
|
|
290
|
+
if (!authEnv) return { token: null, pooled: false, fingerprint: null, poolExhausted: false };
|
|
291
|
+
|
|
292
|
+
const profiles = ctx && ctx.authProfiles;
|
|
293
|
+
const provider = row.provider;
|
|
294
|
+
const pooled =
|
|
295
|
+
profiles && typeof profiles.isPooled === "function" && profiles.isPooled(provider);
|
|
296
|
+
|
|
297
|
+
// Single-key (or no pool): preserve today's behavior exactly.
|
|
298
|
+
if (!pooled) {
|
|
299
|
+
const v = ctx.env[authEnv];
|
|
300
|
+
const token = typeof v === "string" && v.trim() !== "" ? v : null;
|
|
301
|
+
return { token, pooled: false, fingerprint: null, poolExhausted: false };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Pooled: pick the next non-cooled credential. The pick advances the cursor
|
|
305
|
+
// (persisted under the ledger lock) so repeated resolves rotate.
|
|
306
|
+
const pick = authNextCredential(provider, {
|
|
307
|
+
profiles,
|
|
308
|
+
now: ctx.now,
|
|
309
|
+
deps: ctx.authProfileDeps,
|
|
310
|
+
});
|
|
311
|
+
if (pick && pick.key) {
|
|
312
|
+
return { token: pick.key, pooled: true, fingerprint: pick.fingerprint, poolExhausted: false };
|
|
313
|
+
}
|
|
314
|
+
// Every key in the pool is cooling — the backend is effectively credential-less
|
|
315
|
+
// for now. Surface as missing_credential so the gate skips it.
|
|
316
|
+
return { token: null, pooled: true, fingerprint: null, poolExhausted: true };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Run a candidate row through the ordered gate. Returns { ok:true } or
|
|
321
|
+
* { ok:false, reason } where reason is one of the tried[] reason strings.
|
|
322
|
+
*/
|
|
323
|
+
function gateCandidate(row, req, ctx) {
|
|
324
|
+
// 1. status — only `available` rows are routable.
|
|
325
|
+
if (row.status !== "available") {
|
|
326
|
+
return { ok: false, reason: `status_${row.status}` };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// 2. harness — the requested harness must be offered by the row.
|
|
330
|
+
const harness = ctx.harness;
|
|
331
|
+
if (harness && row.harness && row.harness[harness] !== true) {
|
|
332
|
+
return { ok: false, reason: `harness_unavailable:${harness}` };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// 3. capability + tool-reliability grade.
|
|
336
|
+
// A session that needs tool use requires grade A (§6.6) — grade B/C rows
|
|
337
|
+
// are structurally unreachable for sessions.
|
|
338
|
+
if (req.needs_tool_use && harness === "session") {
|
|
339
|
+
const rank = row.tool_reliability ? GRADE_RANK[row.tool_reliability] : 0;
|
|
340
|
+
if (rank < GRADE_RANK.A) {
|
|
341
|
+
return { ok: false, reason: "grade_too_low" };
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
// Explicit grade floor from the rule (minGrade).
|
|
345
|
+
if (ctx.minGrade) {
|
|
346
|
+
const rank = row.tool_reliability ? GRADE_RANK[row.tool_reliability] : 0;
|
|
347
|
+
if (rank < (GRADE_RANK[ctx.minGrade] || 0)) {
|
|
348
|
+
return { ok: false, reason: "grade_too_low" };
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// 4. context — token_estimate must fit the row's effective runtime cap.
|
|
353
|
+
if (req.token_estimate != null && row.context_tokens != null) {
|
|
354
|
+
if (req.token_estimate > row.context_tokens) {
|
|
355
|
+
return { ok: false, reason: "context_overflow" };
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// 5. data_class — deny-by-default. The provider's allowed_data_classes
|
|
360
|
+
// (config backends ∩ overlay max) must include the request's data_class.
|
|
361
|
+
const allowed = ctx.allowedDataClasses[row.provider];
|
|
362
|
+
if (allowed && !allowed.has(req.data_class)) {
|
|
363
|
+
return { ok: false, reason: "data_class_denied" };
|
|
364
|
+
}
|
|
365
|
+
// Even without a backends entry, anything but Anthropic is deny-by-default for
|
|
366
|
+
// non-public classes unless the config explicitly allows it.
|
|
367
|
+
if (!allowed && row.provider !== "anthropic" && req.data_class !== "public") {
|
|
368
|
+
return { ok: false, reason: "data_class_denied" };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// 6. credential — auth_env must be present & non-empty (key-absence-as-
|
|
372
|
+
// enforcement, §7.1). EXCEPTION: an Anthropic SESSION candidate rides the
|
|
373
|
+
// stock `claude` CLI / keychain-OAuth (or a Console key the spawner owns),
|
|
374
|
+
// so it never needs ANTHROPIC_API_KEY injected — the v1 resolver treated
|
|
375
|
+
// anthropic-cli the same way. Direct-api Anthropic and ALL third-party rows
|
|
376
|
+
// require their credential or they are structurally unreachable.
|
|
377
|
+
const sessionAnthropic = row.provider === "anthropic" && harness === "session";
|
|
378
|
+
if (row.auth_env && !sessionAnthropic) {
|
|
379
|
+
const cred = resolveRowCredential(row, ctx);
|
|
380
|
+
if (!cred.token) {
|
|
381
|
+
return { ok: false, reason: "missing_credential" };
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// 7. breaker — the per-backend AND per-backend:model breaker must be closed.
|
|
386
|
+
const backendKey = keyFor(row.provider);
|
|
387
|
+
const modelKey = keyFor(row.provider, modelClassOf(row));
|
|
388
|
+
if (ctx.isOpen(backendKey).open) return { ok: false, reason: "breaker_open" };
|
|
389
|
+
if (ctx.isOpen(modelKey).open) return { ok: false, reason: "breaker_open" };
|
|
390
|
+
|
|
391
|
+
return { ok: true };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* The model-class segment for the breaker key. We use the bare id; callers that
|
|
396
|
+
* want coarser bucketing (sonnet/haiku) can override via catalog, but the id is
|
|
397
|
+
* the stable, collision-free default.
|
|
398
|
+
*/
|
|
399
|
+
function modelClassOf(row) {
|
|
400
|
+
return row.id;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// ---------------------------------------------------------------------------
|
|
404
|
+
// envForSpawn / transport / wire derivation
|
|
405
|
+
// ---------------------------------------------------------------------------
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Build the env to retarget a `claude` SESSION spawn onto a row's backend.
|
|
409
|
+
* - Anthropic rows: no retarget (stock CLI/OAuth or Console key the caller owns).
|
|
410
|
+
* - Third-party rows with an /anthropic endpoint: ANTHROPIC_BASE_URL retarget +
|
|
411
|
+
* ANTHROPIC_AUTH_TOKEN from auth_env + ANTHROPIC_API_KEY="" (so Claude Code
|
|
412
|
+
* does NOT fall back to keychain OAuth) + ANTHROPIC_MODEL.
|
|
413
|
+
* Returns {} for non-session harnesses (direct-api builds its own request).
|
|
414
|
+
*/
|
|
415
|
+
function buildEnvForSpawn(row, harness, env, config, credCtx) {
|
|
416
|
+
if (harness !== "session") return {};
|
|
417
|
+
if (row.provider === "anthropic") return {};
|
|
418
|
+
|
|
419
|
+
const baseUrl = row.endpoints && row.endpoints.anthropic;
|
|
420
|
+
if (!baseUrl) return {}; // not session-capable on the anthropic wire; caller falls back
|
|
421
|
+
|
|
422
|
+
const out = {
|
|
423
|
+
ANTHROPIC_BASE_URL: baseUrl,
|
|
424
|
+
ANTHROPIC_API_KEY: "", // explicit empty string — NOT unset (W: keychain fallthrough)
|
|
425
|
+
ANTHROPIC_MODEL: row.id,
|
|
426
|
+
ANTHROPIC_SMALL_FAST_MODEL: row.id,
|
|
427
|
+
};
|
|
428
|
+
// Auth token: a pooled auth-profile set picks the next non-cooled credential
|
|
429
|
+
// (additive — a single-key setup falls through to env[auth_env] exactly as
|
|
430
|
+
// before). credCtx carries { authProfiles, authProfileDeps, now, env }.
|
|
431
|
+
if (row.auth_env) {
|
|
432
|
+
const cred = credCtx ? resolveRowCredential(row, credCtx) : null;
|
|
433
|
+
if (cred && cred.pooled && cred.token) {
|
|
434
|
+
out.ANTHROPIC_AUTH_TOKEN = cred.token;
|
|
435
|
+
} else if (typeof env[row.auth_env] === "string") {
|
|
436
|
+
out.ANTHROPIC_AUTH_TOKEN = env[row.auth_env];
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if (!config || config.strip_attribution_header !== false) {
|
|
440
|
+
out.CLAUDE_CODE_ATTRIBUTION_HEADER = "0";
|
|
441
|
+
}
|
|
442
|
+
if (!config || config.disable_experimental_betas !== false) {
|
|
443
|
+
out.CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS = "1";
|
|
444
|
+
}
|
|
445
|
+
return out;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function transportFor(row, harness) {
|
|
449
|
+
if (harness === "session") {
|
|
450
|
+
if (row.provider === "anthropic") return TRANSPORT.anthropicApi;
|
|
451
|
+
return TRANSPORT.anthropicNative;
|
|
452
|
+
}
|
|
453
|
+
// direct-api / batch
|
|
454
|
+
if (row.provider === "anthropic") return TRANSPORT.anthropicMessages;
|
|
455
|
+
return TRANSPORT.openaiCompletions;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function wireFor(row, harness) {
|
|
459
|
+
// The wire dialect the direct-api path speaks. Session spawns ride `claude`,
|
|
460
|
+
// so the wire is informational there.
|
|
461
|
+
if (row.provider === "anthropic") return "anthropic";
|
|
462
|
+
// A row routed on the anthropic-native session transport still speaks the
|
|
463
|
+
// anthropic wire; a direct-api third-party call uses the openai wire.
|
|
464
|
+
if (harness === "session") return "anthropic";
|
|
465
|
+
return "openai";
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function modelFlagFor(row) {
|
|
469
|
+
// For Anthropic the CLI accepts the shorthand; for others Claude Code passes
|
|
470
|
+
// ANTHROPIC_MODEL through, so the flag is the concrete id either way.
|
|
471
|
+
return row.id;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function maxTurnsFor(taskClass) {
|
|
475
|
+
if (!taskClass) return DEFAULT_MAX_TURNS;
|
|
476
|
+
if (TASK_CLASS_MAX_TURNS[taskClass] != null) return TASK_CLASS_MAX_TURNS[taskClass];
|
|
477
|
+
// Prefix match (e.g. "lookup.gmail" → "lookup").
|
|
478
|
+
const head = String(taskClass).split(".")[0];
|
|
479
|
+
if (TASK_CLASS_MAX_TURNS[head] != null) return TASK_CLASS_MAX_TURNS[head];
|
|
480
|
+
return DEFAULT_MAX_TURNS;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// ---------------------------------------------------------------------------
|
|
484
|
+
// data_class resolution from config + overlay
|
|
485
|
+
// ---------------------------------------------------------------------------
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Build provider -> Set(allowed data classes), intersecting config backends with
|
|
489
|
+
* the overlay's allowed_data_classes_max (tighten-only).
|
|
490
|
+
*/
|
|
491
|
+
function buildAllowedDataClasses(config, overlay) {
|
|
492
|
+
const out = {};
|
|
493
|
+
const backends = (config && config.backends) || {};
|
|
494
|
+
for (const [name, b] of Object.entries(backends)) {
|
|
495
|
+
const adc = Array.isArray(b && b.allowed_data_classes) ? b.allowed_data_classes : null;
|
|
496
|
+
if (adc) out[name] = new Set(adc.filter((c) => DATA_CLASSES.has(c)));
|
|
497
|
+
}
|
|
498
|
+
// Anthropic defaults to all classes if unspecified.
|
|
499
|
+
if (!out.anthropic) out.anthropic = new Set(["public", "internal", "sensitive"]);
|
|
500
|
+
|
|
501
|
+
const max = overlay && overlay.constraints && overlay.constraints.allowed_data_classes_max;
|
|
502
|
+
if (max && typeof max === "object") {
|
|
503
|
+
for (const [name, list] of Object.entries(max)) {
|
|
504
|
+
if (!Array.isArray(list)) continue;
|
|
505
|
+
const cap = new Set(list.filter((c) => DATA_CLASSES.has(c)));
|
|
506
|
+
out[name] = out[name] ? intersect(out[name], cap) : cap;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return out;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function intersect(a, b) {
|
|
513
|
+
const out = new Set();
|
|
514
|
+
for (const x of a) if (b.has(x)) out.add(x);
|
|
515
|
+
return out;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// ---------------------------------------------------------------------------
|
|
519
|
+
// resolveChain — the public entry point
|
|
520
|
+
// ---------------------------------------------------------------------------
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* @typedef {object} RouteDecision
|
|
524
|
+
* @property {string} decision_id
|
|
525
|
+
* @property {Array<{ref,harness,transport}>} chain
|
|
526
|
+
* @property {{provider,model,harness,transport,wire,catalogRow}} chosen
|
|
527
|
+
* @property {object} envForSpawn
|
|
528
|
+
* @property {{modelFlag,maxTurns,effort,maxBudgetUsd,agentsJson,bare}} spawnArgs
|
|
529
|
+
* @property {string} lane
|
|
530
|
+
* @property {string} cacheAffinityKey
|
|
531
|
+
* @property {string} cache_ttl
|
|
532
|
+
* @property {number|null} estCostUSD
|
|
533
|
+
* @property {Array<{ref,ok:false,reason}>} tried
|
|
534
|
+
* @property {string} explain MANDATORY one-line human string
|
|
535
|
+
* @property {{rule,fallback_reason,config_source,overlay_version,catalog_provenance}} audit
|
|
536
|
+
*/
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Resolve a RouteRequest to a RouteDecision. NEVER throws — a broken snapshot
|
|
540
|
+
* degrades to an Anthropic safety-net decision (or, under the kill switch, an
|
|
541
|
+
* Anthropic-only chain).
|
|
542
|
+
*
|
|
543
|
+
* @param {object} request RouteRequest (§4.3)
|
|
544
|
+
* @param {object} [opts]
|
|
545
|
+
* @param {string} [opts.agentRoot]
|
|
546
|
+
* @param {object} [opts.config] normalised routing config (v2)
|
|
547
|
+
* @param {object} [opts.catalog] CatalogResult (injected; else loaded)
|
|
548
|
+
* @param {object} [opts.overlay] org policy overlay (already verified)
|
|
549
|
+
* @param {object} [opts.env] env for credential + kill-switch checks
|
|
550
|
+
* @param {(key:string)=>{open:boolean,until,reason,strikes}} [opts.isOpen]
|
|
551
|
+
* breaker probe (defaults to health.isOpen)
|
|
552
|
+
* @param {()=>number} [opts.now]
|
|
553
|
+
* @param {(n)=>number} [opts.rng]
|
|
554
|
+
* @returns {RouteDecision}
|
|
555
|
+
*/
|
|
556
|
+
export function resolveChain(request = {}, opts = {}) {
|
|
557
|
+
const env = envOf(opts);
|
|
558
|
+
const now = nowMs(opts);
|
|
559
|
+
const agentRoot = opts.agentRoot;
|
|
560
|
+
const config = opts.config || null;
|
|
561
|
+
const overlay = opts.overlay || (config && config.overlay) || null;
|
|
562
|
+
|
|
563
|
+
// Breaker probe: injectable; defaults to the on-disk health.mjs breaker.
|
|
564
|
+
const isOpen = typeof opts.isOpen === "function"
|
|
565
|
+
? (key) => opts.isOpen(key, now)
|
|
566
|
+
: (key) => breakerIsOpen(key, now, opts.breakerDeps || { agentRoot });
|
|
567
|
+
|
|
568
|
+
// 1. Normalise the request (deny-default data_class; W5 needs_tool_use).
|
|
569
|
+
const req = normaliseRequest(request, config);
|
|
570
|
+
|
|
571
|
+
const catalogResult = safe(() => resolveCatalog(agentRoot, opts), null);
|
|
572
|
+
const catalog = catalogResult && (catalogResult.byRef ? catalogResult : catalogResult.catalog)
|
|
573
|
+
? catalogResult
|
|
574
|
+
: null;
|
|
575
|
+
|
|
576
|
+
const tried = [];
|
|
577
|
+
const audit = {
|
|
578
|
+
rule: null,
|
|
579
|
+
fallback_reason: null,
|
|
580
|
+
config_source: (config && config.source_path) || (catalogResult && catalogResult.sources && catalogResult.sources.join("+")) || null,
|
|
581
|
+
overlay_version: (overlay && overlay.version) || null,
|
|
582
|
+
catalog_provenance: catalogProvenanceTag(catalog),
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
// 0. Kill switch — force an Anthropic-only chain regardless of policy.
|
|
586
|
+
if (isForceAnthropic(env)) {
|
|
587
|
+
return forceAnthropicDecision(catalog, req, { now, opts, audit, reason: "kill_switch" });
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// No config at all → inert v2 path: an Anthropic safety-net decision so the
|
|
591
|
+
// caller always has a usable chosen (matching the "collapse to today" rule).
|
|
592
|
+
if (!config || (config.schema_version !== 2 && !Array.isArray(config.routing_policy))) {
|
|
593
|
+
return forceAnthropicDecision(catalog, req, { now, opts, audit, reason: "no_v2_config" });
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const aliases = (config.aliases && typeof config.aliases === "object") ? config.aliases : {};
|
|
597
|
+
const allowedDataClasses = buildAllowedDataClasses(config, overlay);
|
|
598
|
+
|
|
599
|
+
// 2. First-match rule.
|
|
600
|
+
const { rule, index } = pickRule(config.routing_policy, req);
|
|
601
|
+
audit.rule = rule.default ? (rule.synthesized ? "<synthesized-default>" : "<default>") : (rule.match ? JSON.stringify(rule.match) : `rule[${index}]`);
|
|
602
|
+
|
|
603
|
+
// Rule-level overrides.
|
|
604
|
+
const ruleHarness = rule.harness || req.harness_hint || (req.lane === "batch" ? "batch" : "session");
|
|
605
|
+
const harness = HARNESSES.has(ruleHarness) ? ruleHarness : "session";
|
|
606
|
+
// A rule may opt out of tool use explicitly (classify/lookup are tool-less).
|
|
607
|
+
if (rule.needs_tool_use === false) req.needs_tool_use = false;
|
|
608
|
+
const minGrade = rule.minGrade || null;
|
|
609
|
+
const lane = req.lane || (harness === "batch" ? "batch" : "realtime");
|
|
610
|
+
|
|
611
|
+
// 3. Alias chain → refs, then budget ladder.
|
|
612
|
+
let refs = resolveChainRefs(rule.chain, aliases);
|
|
613
|
+
const ladder = applyBudgetLadder(refs, req, aliases, req.budget_band);
|
|
614
|
+
refs = ladder.refs;
|
|
615
|
+
|
|
616
|
+
// 4. Gate each candidate; first survivor is chosen.
|
|
617
|
+
const ctx = {
|
|
618
|
+
env,
|
|
619
|
+
harness,
|
|
620
|
+
minGrade,
|
|
621
|
+
allowedDataClasses,
|
|
622
|
+
isOpen,
|
|
623
|
+
// Auth-profile rotation context (additive; absent ⇒ single-key behavior).
|
|
624
|
+
authProfiles: opts.authProfiles || null,
|
|
625
|
+
authProfileDeps: opts.authProfileDeps || (agentRoot ? { agentRoot } : undefined),
|
|
626
|
+
now,
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
let chosenRow = null;
|
|
630
|
+
let chosenHarness = harness;
|
|
631
|
+
const chainOut = [];
|
|
632
|
+
const explainSkips = [];
|
|
633
|
+
|
|
634
|
+
for (const member of refs) {
|
|
635
|
+
if (member.isRules) {
|
|
636
|
+
// The deterministic-fallback sentinel: a terminal candidate the caller
|
|
637
|
+
// owns. It always "passes" (no model gate) but is never the chosen row
|
|
638
|
+
// unless nothing else survived — record it on the chain and continue.
|
|
639
|
+
chainOut.push({ ref: "rules", harness: "direct", transport: "rules" });
|
|
640
|
+
if (!chosenRow) {
|
|
641
|
+
// Keep walking; a real model later in the chain is preferred. We only
|
|
642
|
+
// fall to "rules" if no model survives (handled after the loop).
|
|
643
|
+
}
|
|
644
|
+
continue;
|
|
645
|
+
}
|
|
646
|
+
const row = catalog ? lookupModel(catalog, member.ref) : null;
|
|
647
|
+
if (!row) {
|
|
648
|
+
tried.push({ ref: member.ref, ok: false, reason: "unknown_ref" });
|
|
649
|
+
explainSkips.push(`${member.ref} (unknown_ref)`);
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
652
|
+
const gate = gateCandidate(row, req, ctx);
|
|
653
|
+
if (!gate.ok) {
|
|
654
|
+
tried.push({ ref: member.ref, ok: false, reason: gate.reason });
|
|
655
|
+
explainSkips.push(`${member.ref} (${gate.reason})`);
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
// Survivor. Record it on the visible chain.
|
|
659
|
+
chainOut.push({
|
|
660
|
+
ref: row.ref,
|
|
661
|
+
harness: chosenHarness,
|
|
662
|
+
transport: transportFor(row, chosenHarness),
|
|
663
|
+
});
|
|
664
|
+
if (!chosenRow) {
|
|
665
|
+
chosenRow = row;
|
|
666
|
+
// Continue building the rest of the chain (the failover tail) from the
|
|
667
|
+
// remaining members so spawnRouted has somewhere to go.
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// 4b. If no model survived but the chain had a "rules" sentinel, the caller's
|
|
672
|
+
// deterministic fallback is the answer (direct, no model).
|
|
673
|
+
const hasRules = refs.some((m) => m.isRules);
|
|
674
|
+
|
|
675
|
+
// 5. Affinity pin — if a live pin for the session_key passes the gates, move
|
|
676
|
+
// it to the head; a pin that fails the gates emits pin_overridden.
|
|
677
|
+
let affinityNote = null;
|
|
678
|
+
if (req.pin && req.pin.ref && catalog) {
|
|
679
|
+
const pinRow = lookupModel(catalog, req.pin.ref);
|
|
680
|
+
if (pinRow) {
|
|
681
|
+
const g = gateCandidate(pinRow, req, ctx);
|
|
682
|
+
if (g.ok) {
|
|
683
|
+
chosenRow = pinRow;
|
|
684
|
+
chosenHarness = harness;
|
|
685
|
+
// Move/insert the pin at the chain head.
|
|
686
|
+
const head = { ref: pinRow.ref, harness, transport: transportFor(pinRow, harness) };
|
|
687
|
+
const rest = chainOut.filter((c) => c.ref !== pinRow.ref);
|
|
688
|
+
chainOut.length = 0;
|
|
689
|
+
chainOut.push(head, ...rest);
|
|
690
|
+
affinityNote = `pin ${pinRow.ref}`;
|
|
691
|
+
} else {
|
|
692
|
+
tried.push({ ref: pinRow.ref, ok: false, reason: `pin_overridden:${g.reason}` });
|
|
693
|
+
affinityNote = `pin_overridden (${g.reason})`;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// 6. Fallback to the Anthropic safety net when nothing survived.
|
|
699
|
+
if (!chosenRow) {
|
|
700
|
+
if (hasRules) {
|
|
701
|
+
return rulesDecision(req, { now, opts, audit, tried, chainOut, lane, explainSkips });
|
|
702
|
+
}
|
|
703
|
+
if (config.fallback_to_anthropic !== false) {
|
|
704
|
+
audit.fallback_reason = "no_compatible_candidate";
|
|
705
|
+
return forceAnthropicDecision(catalog, req, {
|
|
706
|
+
now, opts, audit, tried, reason: "no_compatible_candidate", harness, lane,
|
|
707
|
+
explainSkips,
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
// No fallback declared — return an explicit no-route decision.
|
|
711
|
+
return noRouteDecision(req, { now, opts, audit, tried, explainSkips, lane });
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// 7. Build the decision.
|
|
715
|
+
return buildDecision({
|
|
716
|
+
catalog,
|
|
717
|
+
row: chosenRow,
|
|
718
|
+
harness: chosenHarness,
|
|
719
|
+
req,
|
|
720
|
+
config,
|
|
721
|
+
env,
|
|
722
|
+
now,
|
|
723
|
+
opts,
|
|
724
|
+
rule,
|
|
725
|
+
audit,
|
|
726
|
+
tried,
|
|
727
|
+
chain: chainOut,
|
|
728
|
+
lane,
|
|
729
|
+
ladderNote: ladder.note,
|
|
730
|
+
affinityNote,
|
|
731
|
+
explainSkips,
|
|
732
|
+
credCtx: ctx,
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// ---------------------------------------------------------------------------
|
|
737
|
+
// Request normalisation
|
|
738
|
+
// ---------------------------------------------------------------------------
|
|
739
|
+
|
|
740
|
+
function normaliseRequest(request, config) {
|
|
741
|
+
const req = { ...request };
|
|
742
|
+
// data_class deny-default.
|
|
743
|
+
if (!DATA_CLASSES.has(req.data_class)) {
|
|
744
|
+
const dflt = (config && config.defaults && config.defaults.data_class) || "sensitive";
|
|
745
|
+
req.data_class = DATA_CLASSES.has(dflt) ? dflt : "sensitive";
|
|
746
|
+
}
|
|
747
|
+
// needs_tool_use default-true for SESSION work (W5), unless explicitly set.
|
|
748
|
+
const sessionDefaultTools =
|
|
749
|
+
!config || !config.defaults || config.defaults.needs_tool_use_for_sessions !== false;
|
|
750
|
+
if (req.needs_tool_use === undefined) {
|
|
751
|
+
const willBeSession = (req.harness_hint || (req.lane === "batch" ? "batch" : "session")) === "session";
|
|
752
|
+
req.needs_tool_use = willBeSession && sessionDefaultTools;
|
|
753
|
+
}
|
|
754
|
+
return req;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// ---------------------------------------------------------------------------
|
|
758
|
+
// Decision builders
|
|
759
|
+
// ---------------------------------------------------------------------------
|
|
760
|
+
|
|
761
|
+
function buildDecision({ catalog, row, harness, req, config, env, now, opts, rule, audit, tried, chain, lane, ladderNote, affinityNote, explainSkips, credCtx }) {
|
|
762
|
+
const decision_id = ulidish(now, opts.rng);
|
|
763
|
+
const transport = transportFor(row, harness);
|
|
764
|
+
const envForSpawn = buildEnvForSpawn(row, harness, env, config, credCtx);
|
|
765
|
+
|
|
766
|
+
const cache_ttl =
|
|
767
|
+
(config && config.defaults && config.defaults.cache_ttl) ||
|
|
768
|
+
(lane === "realtime" ? "1h" : "1h");
|
|
769
|
+
|
|
770
|
+
const cacheAffinityKey = req.session_key
|
|
771
|
+
? `${req.session_key}:${row.ref}`
|
|
772
|
+
: `${req.task_class || "task"}:${row.ref}`;
|
|
773
|
+
|
|
774
|
+
// Estimate cost (budget projection: volatile rows at steady-state).
|
|
775
|
+
const tokenIn = req.token_estimate ?? 0;
|
|
776
|
+
const tokenOut = req.token_estimate_out ?? Math.floor(tokenIn * 0.4);
|
|
777
|
+
const est = estimateCost(catalog, {
|
|
778
|
+
ref: row.ref,
|
|
779
|
+
inputTokens: tokenIn,
|
|
780
|
+
outputTokens: tokenOut,
|
|
781
|
+
cacheReadTokens: req.cache_read_estimate ?? 0,
|
|
782
|
+
budget: true,
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
const spawnArgs = {
|
|
786
|
+
modelFlag: modelFlagFor(row),
|
|
787
|
+
maxTurns: maxTurnsFor(req.task_class),
|
|
788
|
+
effort: rule && rule.effort ? rule.effort : null,
|
|
789
|
+
maxBudgetUsd: rule && rule.max_budget_usd != null ? rule.max_budget_usd : null,
|
|
790
|
+
agentsJson: rule && rule.agents_json ? rule.agents_json : null,
|
|
791
|
+
bare: true,
|
|
792
|
+
};
|
|
793
|
+
|
|
794
|
+
const explain = buildExplain({
|
|
795
|
+
req, rule, row, harness, ladderNote, affinityNote, explainSkips, est,
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
return {
|
|
799
|
+
decision_id,
|
|
800
|
+
chain,
|
|
801
|
+
chosen: {
|
|
802
|
+
provider: row.provider,
|
|
803
|
+
model: row.id,
|
|
804
|
+
harness,
|
|
805
|
+
transport,
|
|
806
|
+
wire: wireFor(row, harness),
|
|
807
|
+
catalogRow: row,
|
|
808
|
+
},
|
|
809
|
+
envForSpawn,
|
|
810
|
+
spawnArgs,
|
|
811
|
+
lane,
|
|
812
|
+
cacheAffinityKey,
|
|
813
|
+
cache_ttl,
|
|
814
|
+
estCostUSD: est.usd,
|
|
815
|
+
tried,
|
|
816
|
+
explain,
|
|
817
|
+
audit,
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/**
|
|
822
|
+
* The Anthropic-only / safety-net decision. Used by the kill switch, the
|
|
823
|
+
* no-v2-config inert path, and the "nothing survived the gates" fallback. Picks
|
|
824
|
+
* the best available Anthropic row for the request's needs.
|
|
825
|
+
*/
|
|
826
|
+
function forceAnthropicDecision(catalog, req, { now, opts, audit, tried = [], reason, harness = "session", lane = "realtime", explainSkips = [] }) {
|
|
827
|
+
const row = pickAnthropicRow(catalog, req);
|
|
828
|
+
const decision_id = ulidish(now, opts && opts.rng);
|
|
829
|
+
const env = envOf(opts);
|
|
830
|
+
|
|
831
|
+
if (!row) {
|
|
832
|
+
// No catalog at all — degrade to a synthetic chosen so the caller still has
|
|
833
|
+
// a usable model flag (stock `claude` defaults to its account model).
|
|
834
|
+
const synthetic = {
|
|
835
|
+
provider: "anthropic",
|
|
836
|
+
id: req.needs_thinking || req.priority === "critical" ? "claude-opus-4-8" : "claude-sonnet-4-6",
|
|
837
|
+
ref: "anthropic/claude-sonnet-4-6",
|
|
838
|
+
status: "available",
|
|
839
|
+
auth_env: null,
|
|
840
|
+
endpoints: {},
|
|
841
|
+
cost: {},
|
|
842
|
+
cost_provenance: {},
|
|
843
|
+
compat: {},
|
|
844
|
+
harness: { session: true, direct: true, batch: true },
|
|
845
|
+
tool_reliability: "A",
|
|
846
|
+
context_tokens: null,
|
|
847
|
+
};
|
|
848
|
+
return synthDecision(synthetic, req, { now, opts, audit, tried, reason, harness, lane, explainSkips, env, catalog });
|
|
849
|
+
}
|
|
850
|
+
return synthDecision(row, req, { now, opts, audit, tried, reason, harness, lane, explainSkips, env, catalog });
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
function synthDecision(row, req, { now, opts, audit, tried, reason, harness, lane, explainSkips, env, catalog }) {
|
|
854
|
+
const decision_id = ulidish(now, opts && opts.rng);
|
|
855
|
+
const transport = transportFor(row, harness);
|
|
856
|
+
const est = catalog ? estimateCost(catalog, {
|
|
857
|
+
ref: row.ref,
|
|
858
|
+
inputTokens: req.token_estimate ?? 0,
|
|
859
|
+
outputTokens: req.token_estimate_out ?? Math.floor((req.token_estimate ?? 0) * 0.4),
|
|
860
|
+
budget: true,
|
|
861
|
+
}) : { usd: null };
|
|
862
|
+
audit.fallback_reason = audit.fallback_reason || reason;
|
|
863
|
+
|
|
864
|
+
const explainBase =
|
|
865
|
+
reason === "kill_switch"
|
|
866
|
+
? "kill switch (MAESTRO_ROUTER_FORCE_ANTHROPIC) → Anthropic-only"
|
|
867
|
+
: reason === "no_v2_config"
|
|
868
|
+
? "no v2 routing config → Anthropic safety net"
|
|
869
|
+
: `${req.task_class || "task"} → Anthropic safety net (${reason})`;
|
|
870
|
+
const skipNote = explainSkips && explainSkips.length ? `; skipped ${explainSkips.join(", ")}` : "";
|
|
871
|
+
|
|
872
|
+
return {
|
|
873
|
+
decision_id,
|
|
874
|
+
chain: [{ ref: row.ref, harness, transport }],
|
|
875
|
+
chosen: {
|
|
876
|
+
provider: "anthropic",
|
|
877
|
+
model: row.id,
|
|
878
|
+
harness,
|
|
879
|
+
transport,
|
|
880
|
+
wire: "anthropic",
|
|
881
|
+
catalogRow: row,
|
|
882
|
+
},
|
|
883
|
+
envForSpawn: {}, // Anthropic session: stock CLI/Console key, no retarget
|
|
884
|
+
spawnArgs: {
|
|
885
|
+
modelFlag: row.id,
|
|
886
|
+
maxTurns: maxTurnsFor(req.task_class),
|
|
887
|
+
effort: null,
|
|
888
|
+
maxBudgetUsd: null,
|
|
889
|
+
agentsJson: null,
|
|
890
|
+
bare: true,
|
|
891
|
+
},
|
|
892
|
+
lane,
|
|
893
|
+
cacheAffinityKey: req.session_key ? `${req.session_key}:${row.ref}` : `${req.task_class || "task"}:${row.ref}`,
|
|
894
|
+
cache_ttl: "1h",
|
|
895
|
+
estCostUSD: est.usd,
|
|
896
|
+
tried,
|
|
897
|
+
explain: `${explainBase}: ${row.ref}${skipNote}`,
|
|
898
|
+
audit,
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
/** The deterministic-fallback ("rules") decision — no model, caller-owned. */
|
|
903
|
+
function rulesDecision(req, { now, opts, audit, tried, chainOut, lane, explainSkips }) {
|
|
904
|
+
const decision_id = ulidish(now, opts && opts.rng);
|
|
905
|
+
audit.fallback_reason = audit.fallback_reason || "rules_fallback";
|
|
906
|
+
const skipNote = explainSkips && explainSkips.length ? ` (skipped ${explainSkips.join(", ")})` : "";
|
|
907
|
+
return {
|
|
908
|
+
decision_id,
|
|
909
|
+
chain: chainOut,
|
|
910
|
+
chosen: {
|
|
911
|
+
provider: "rules",
|
|
912
|
+
model: "rules",
|
|
913
|
+
harness: "direct",
|
|
914
|
+
transport: "rules",
|
|
915
|
+
wire: "none",
|
|
916
|
+
catalogRow: null,
|
|
917
|
+
},
|
|
918
|
+
envForSpawn: {},
|
|
919
|
+
spawnArgs: { modelFlag: null, maxTurns: 1, effort: null, maxBudgetUsd: null, agentsJson: null, bare: true },
|
|
920
|
+
lane,
|
|
921
|
+
cacheAffinityKey: null,
|
|
922
|
+
cache_ttl: "0",
|
|
923
|
+
estCostUSD: 0,
|
|
924
|
+
tried,
|
|
925
|
+
explain: `${req.task_class || "task"} → deterministic rules fallback${skipNote}`,
|
|
926
|
+
audit,
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
/** No-route decision when no model survives and fallback_to_anthropic is off. */
|
|
931
|
+
function noRouteDecision(req, { now, opts, audit, tried, explainSkips, lane }) {
|
|
932
|
+
const decision_id = ulidish(now, opts && opts.rng);
|
|
933
|
+
audit.fallback_reason = "no_route";
|
|
934
|
+
return {
|
|
935
|
+
decision_id,
|
|
936
|
+
chain: [],
|
|
937
|
+
chosen: null,
|
|
938
|
+
envForSpawn: {},
|
|
939
|
+
spawnArgs: { modelFlag: null, maxTurns: 0, effort: null, maxBudgetUsd: null, agentsJson: null, bare: true },
|
|
940
|
+
lane,
|
|
941
|
+
cacheAffinityKey: null,
|
|
942
|
+
cache_ttl: "0",
|
|
943
|
+
estCostUSD: null,
|
|
944
|
+
tried,
|
|
945
|
+
explain: `${req.task_class || "task"} → NO ROUTE (no compatible candidate, fallback disabled); skipped ${(explainSkips || []).join(", ")}`,
|
|
946
|
+
audit,
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
/**
|
|
951
|
+
* Pick the best available Anthropic row from the catalog for a request:
|
|
952
|
+
* frontier (opus) for thinking/critical, else the default workhorse (sonnet),
|
|
953
|
+
* else any available anthropic row.
|
|
954
|
+
*/
|
|
955
|
+
function pickAnthropicRow(catalog, req) {
|
|
956
|
+
if (!catalog) return null;
|
|
957
|
+
const wantFrontier = req.needs_thinking || req.priority === "critical" || req.agent_role === "regulatory";
|
|
958
|
+
const order = wantFrontier
|
|
959
|
+
? ["anthropic/claude-opus-4-8", "anthropic/claude-sonnet-4-6", "anthropic/claude-haiku-4-5"]
|
|
960
|
+
: ["anthropic/claude-sonnet-4-6", "anthropic/claude-haiku-4-5", "anthropic/claude-opus-4-8"];
|
|
961
|
+
for (const ref of order) {
|
|
962
|
+
const row = lookupModel(catalog, ref);
|
|
963
|
+
if (row && row.status === "available") {
|
|
964
|
+
// Context gate: if the request is too big for this row, try the next.
|
|
965
|
+
if (req.token_estimate != null && row.context_tokens != null && req.token_estimate > row.context_tokens) continue;
|
|
966
|
+
return row;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
// Any available anthropic row.
|
|
970
|
+
const models = (catalog.models || (catalog.catalog && catalog.catalog.models)) || [];
|
|
971
|
+
return models.find((r) => r.provider === "anthropic" && r.status === "available") || null;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// ---------------------------------------------------------------------------
|
|
975
|
+
// Explain string (mandatory, one line)
|
|
976
|
+
// ---------------------------------------------------------------------------
|
|
977
|
+
|
|
978
|
+
function buildExplain({ req, rule, row, harness, ladderNote, affinityNote, explainSkips, est }) {
|
|
979
|
+
const parts = [];
|
|
980
|
+
parts.push(`${req.task_class || req.source || "task"} →`);
|
|
981
|
+
const ruleLabel = rule.default
|
|
982
|
+
? (rule.synthesized ? "synthesized-default" : "default")
|
|
983
|
+
: (rule.match ? `rule ${shortMatch(rule.match)}` : "rule");
|
|
984
|
+
parts.push(ruleLabel);
|
|
985
|
+
if (ladderNote) parts.push(`[${ladderNote}]`);
|
|
986
|
+
if (affinityNote) parts.push(`[${affinityNote}]`);
|
|
987
|
+
if (explainSkips && explainSkips.length) {
|
|
988
|
+
parts.push(`skipped ${explainSkips.join(", ")} →`);
|
|
989
|
+
} else {
|
|
990
|
+
parts.push("→");
|
|
991
|
+
}
|
|
992
|
+
parts.push(`${row.provider}/${row.id} (${harness})`);
|
|
993
|
+
if (est && est.usd != null) parts.push(`~$${est.usd}`);
|
|
994
|
+
return parts.join(" ");
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
function shortMatch(match) {
|
|
998
|
+
const keys = Object.keys(match);
|
|
999
|
+
if (!keys.length) return "{}";
|
|
1000
|
+
const k = keys[0];
|
|
1001
|
+
const v = match[k];
|
|
1002
|
+
return `${k}=${Array.isArray(v) ? `[${v.length}]` : v}${keys.length > 1 ? "…" : ""}`;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// ---------------------------------------------------------------------------
|
|
1006
|
+
// Catalog provenance tag (for the audit + doctor staleness ladder)
|
|
1007
|
+
// ---------------------------------------------------------------------------
|
|
1008
|
+
|
|
1009
|
+
function catalogProvenanceTag(catalog) {
|
|
1010
|
+
if (!catalog) return null;
|
|
1011
|
+
const models = (catalog.models || (catalog.catalog && catalog.catalog.models)) || [];
|
|
1012
|
+
let oldest = null;
|
|
1013
|
+
let anyVolatile = false;
|
|
1014
|
+
for (const r of models) {
|
|
1015
|
+
const p = r.cost_provenance || {};
|
|
1016
|
+
if (p.volatile) anyVolatile = true;
|
|
1017
|
+
if (p.fetched && (oldest == null || p.fetched < oldest)) oldest = p.fetched;
|
|
1018
|
+
}
|
|
1019
|
+
return { oldestFetched: oldest, anyVolatile };
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
function safe(fn, dflt) {
|
|
1023
|
+
try {
|
|
1024
|
+
return fn();
|
|
1025
|
+
} catch {
|
|
1026
|
+
return dflt;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// ---------------------------------------------------------------------------
|
|
1031
|
+
// validateRoutingConfig — strict v2 config validation (W10/W8/G14)
|
|
1032
|
+
// ---------------------------------------------------------------------------
|
|
1033
|
+
|
|
1034
|
+
/**
|
|
1035
|
+
* Validate a v2 routing config against the catalog (+ optional overlay). Returns
|
|
1036
|
+
* `{ errors:[], warnings:[] }`. NEVER throws. Surfaces:
|
|
1037
|
+
* - unknown match keys (capability typos)
|
|
1038
|
+
* - unknown aliases referenced in a chain
|
|
1039
|
+
* - chain refs not present in the catalog
|
|
1040
|
+
* - auth_env unset for a referenced (non-anthropic) provider (warning — the
|
|
1041
|
+
* key may live in .env which we don't load here)
|
|
1042
|
+
* - attempts to widen tool_reliability or add a provider via the overlay
|
|
1043
|
+
* - invalid harness / data_class / budget bands
|
|
1044
|
+
*
|
|
1045
|
+
* @param {object} config normalised v2 routing config
|
|
1046
|
+
* @param {object} [deps]
|
|
1047
|
+
* @param {object} [deps.catalog] CatalogResult (or .catalog)
|
|
1048
|
+
* @param {object} [deps.overlay]
|
|
1049
|
+
* @param {object} [deps.env] env for the auth_env presence check
|
|
1050
|
+
* @returns {{errors:Array<{where:string,error:string}>, warnings:Array<{where:string,warning:string}>}}
|
|
1051
|
+
*/
|
|
1052
|
+
export function validateRoutingConfig(config, deps = {}) {
|
|
1053
|
+
const errors = [];
|
|
1054
|
+
const warnings = [];
|
|
1055
|
+
const env = deps.env || process.env || {};
|
|
1056
|
+
const catalog = deps.catalog && (deps.catalog.byRef ? deps.catalog : deps.catalog.catalog || deps.catalog);
|
|
1057
|
+
const overlay = deps.overlay || null;
|
|
1058
|
+
|
|
1059
|
+
if (!config || typeof config !== "object") {
|
|
1060
|
+
errors.push({ where: "config", error: "config is empty or not an object" });
|
|
1061
|
+
return { errors, warnings };
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// Only v2 configs are strict-validated here; v1 configs flow through the v1
|
|
1065
|
+
// loader's own normalisation.
|
|
1066
|
+
if (config.schema_version !== 2) {
|
|
1067
|
+
return { errors, warnings };
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
const aliases = (config.aliases && typeof config.aliases === "object") ? config.aliases : {};
|
|
1071
|
+
|
|
1072
|
+
// Aliases must resolve to a catalog ref (or be a literal ref).
|
|
1073
|
+
for (const [name, ref] of Object.entries(aliases)) {
|
|
1074
|
+
if (typeof ref !== "string") {
|
|
1075
|
+
errors.push({ where: `aliases.${name}`, error: "alias target must be a string ref" });
|
|
1076
|
+
continue;
|
|
1077
|
+
}
|
|
1078
|
+
if (catalog && !lookupModel(catalog, ref)) {
|
|
1079
|
+
errors.push({ where: `aliases.${name}`, error: `alias points at unknown catalog row "${ref}"` });
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
// backends data-class sanity.
|
|
1084
|
+
for (const [name, b] of Object.entries(config.backends || {})) {
|
|
1085
|
+
if (b && b.allowed_data_classes) {
|
|
1086
|
+
if (!Array.isArray(b.allowed_data_classes)) {
|
|
1087
|
+
errors.push({ where: `backends.${name}`, error: "allowed_data_classes must be an array" });
|
|
1088
|
+
} else {
|
|
1089
|
+
for (const c of b.allowed_data_classes) {
|
|
1090
|
+
if (!DATA_CLASSES.has(c)) errors.push({ where: `backends.${name}`, error: `unknown data_class "${c}"` });
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
// routing_policy rules.
|
|
1097
|
+
const rules = Array.isArray(config.routing_policy) ? config.routing_policy : [];
|
|
1098
|
+
if (!rules.length) {
|
|
1099
|
+
warnings.push({ where: "routing_policy", warning: "no routing rules — every request falls to the Anthropic safety net" });
|
|
1100
|
+
}
|
|
1101
|
+
let sawDefault = false;
|
|
1102
|
+
rules.forEach((rule, i) => {
|
|
1103
|
+
if (!rule || typeof rule !== "object") {
|
|
1104
|
+
errors.push({ where: `routing_policy[${i}]`, error: "rule is not an object" });
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
if (rule.default) sawDefault = true;
|
|
1108
|
+
// match keys.
|
|
1109
|
+
if (rule.match && typeof rule.match === "object") {
|
|
1110
|
+
for (const key of Object.keys(rule.match)) {
|
|
1111
|
+
if (!VALID_MATCH_KEYS.has(key)) {
|
|
1112
|
+
errors.push({ where: `routing_policy[${i}].match`, error: `unknown match key "${key}" (typo?)` });
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
// harness.
|
|
1117
|
+
if (rule.harness !== undefined && !HARNESSES.has(rule.harness)) {
|
|
1118
|
+
errors.push({ where: `routing_policy[${i}]`, error: `unknown harness "${rule.harness}"` });
|
|
1119
|
+
}
|
|
1120
|
+
// minGrade.
|
|
1121
|
+
if (rule.minGrade !== undefined && !GRADE_RANK[rule.minGrade]) {
|
|
1122
|
+
errors.push({ where: `routing_policy[${i}]`, error: `unknown minGrade "${rule.minGrade}"` });
|
|
1123
|
+
}
|
|
1124
|
+
// chain refs.
|
|
1125
|
+
const chain = rule.chain;
|
|
1126
|
+
if (!rule.default && !Array.isArray(chain)) {
|
|
1127
|
+
errors.push({ where: `routing_policy[${i}]`, error: "non-default rule has no chain[]" });
|
|
1128
|
+
}
|
|
1129
|
+
if (Array.isArray(chain)) {
|
|
1130
|
+
for (const member of chain) {
|
|
1131
|
+
if (member === "rules") continue;
|
|
1132
|
+
if (typeof member !== "string") {
|
|
1133
|
+
errors.push({ where: `routing_policy[${i}].chain`, error: "chain member must be a string alias/ref" });
|
|
1134
|
+
continue;
|
|
1135
|
+
}
|
|
1136
|
+
const ref = Object.prototype.hasOwnProperty.call(aliases, member) ? aliases[member] : member;
|
|
1137
|
+
if (!Object.prototype.hasOwnProperty.call(aliases, member) && !member.includes("/")) {
|
|
1138
|
+
errors.push({ where: `routing_policy[${i}].chain`, error: `"${member}" is neither a known alias nor a "provider/model" ref` });
|
|
1139
|
+
continue;
|
|
1140
|
+
}
|
|
1141
|
+
if (catalog && !lookupModel(catalog, ref)) {
|
|
1142
|
+
errors.push({ where: `routing_policy[${i}].chain`, error: `chain ref "${ref}" not in catalog` });
|
|
1143
|
+
} else if (catalog) {
|
|
1144
|
+
// Credential presence (warning only — keys often live in .env, which
|
|
1145
|
+
// doctor/the daemon load at spawn time). Skip Anthropic: a session
|
|
1146
|
+
// candidate rides keychain OAuth, so a missing ANTHROPIC_API_KEY is
|
|
1147
|
+
// not a routing blocker the way a missing third-party key is.
|
|
1148
|
+
const row = lookupModel(catalog, ref);
|
|
1149
|
+
if (
|
|
1150
|
+
row && row.auth_env && row.provider !== "anthropic" &&
|
|
1151
|
+
(typeof env[row.auth_env] !== "string" || env[row.auth_env].trim() === "")
|
|
1152
|
+
) {
|
|
1153
|
+
warnings.push({ where: `routing_policy[${i}].chain`, warning: `${ref} needs ${row.auth_env} — unset in this env (key-absence enforcement will skip it)` });
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
});
|
|
1159
|
+
if (rules.length && !sawDefault) {
|
|
1160
|
+
warnings.push({ where: "routing_policy", warning: "no default rule — unmatched requests fall to the Anthropic safety net" });
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// budget_ladder bands.
|
|
1164
|
+
if (config.budget_ladder && typeof config.budget_ladder === "object") {
|
|
1165
|
+
for (const band of Object.keys(config.budget_ladder)) {
|
|
1166
|
+
if (!/^\d+$/.test(band)) {
|
|
1167
|
+
warnings.push({ where: "budget_ladder", warning: `band key "${band}" is not numeric` });
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
// Overlay tighten-only widening attempts (provider add / grade raise) are
|
|
1173
|
+
// caught by catalog.mjs at merge; surface a duplicate guard here against the
|
|
1174
|
+
// CONFIG (an agent config cannot widen what the overlay tightened).
|
|
1175
|
+
if (overlay && overlay.constraints) {
|
|
1176
|
+
const allowed = Array.isArray(overlay.constraints.backends_allowed)
|
|
1177
|
+
? new Set(overlay.constraints.backends_allowed)
|
|
1178
|
+
: null;
|
|
1179
|
+
if (allowed) {
|
|
1180
|
+
for (const name of Object.keys(config.backends || {})) {
|
|
1181
|
+
if (!allowed.has(name) && name !== "anthropic") {
|
|
1182
|
+
errors.push({ where: `backends.${name}`, error: `backend "${name}" is not in the org overlay's backends_allowed (tighten-only — cannot add)` });
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
return { errors, warnings };
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
export const _internals = {
|
|
1192
|
+
matchesRule,
|
|
1193
|
+
pickRule,
|
|
1194
|
+
resolveChainRefs,
|
|
1195
|
+
applyBudgetLadder,
|
|
1196
|
+
gateCandidate,
|
|
1197
|
+
buildEnvForSpawn,
|
|
1198
|
+
buildAllowedDataClasses,
|
|
1199
|
+
resolveRowCredential,
|
|
1200
|
+
ulidish,
|
|
1201
|
+
pickAnthropicRow,
|
|
1202
|
+
GRADE_RANK,
|
|
1203
|
+
VALID_MATCH_KEYS,
|
|
1204
|
+
};
|
|
1205
|
+
|
|
1206
|
+
export default { resolveChain, validateRoutingConfig };
|