@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,925 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/model-router/catalog.mjs — typed model catalog (v2).
|
|
3
|
+
*
|
|
4
|
+
* The catalog is the source of truth for every routable model: its identity,
|
|
5
|
+
* status, context/window limits, $/MTok cost, provenance, wire/compat flags,
|
|
6
|
+
* tool-reliability grade, and harness affordances. `resolveChain` (the resolve
|
|
7
|
+
* lane, built in parallel) consults the merged catalog to gate candidates;
|
|
8
|
+
* `ledger.estimateCost` prices a unit of work from a row's `cost` block.
|
|
9
|
+
*
|
|
10
|
+
* Authority merge (highest wins; the pricing cache updates `cost` fields ONLY —
|
|
11
|
+
* capabilities / compat / tool_reliability / harness are human-curated):
|
|
12
|
+
*
|
|
13
|
+
* 0 agent config/model-routing.yaml catalog_overrides: (full row patch)
|
|
14
|
+
* 1 agent config/models.local.yaml (full row patch)
|
|
15
|
+
* 2 org policy overlay catalog_patch (TIGHTEN-ONLY: disable rows /
|
|
16
|
+
* lower grades; never add providers
|
|
17
|
+
* nor raise tool_reliability)
|
|
18
|
+
* 3 state/model-router/pricing-cache.json (COST FIELDS ONLY, provenance-stamped)
|
|
19
|
+
* 4 bundled lib/model-router/catalog/*.yaml (ships with @cohortapp/agent-sdk)
|
|
20
|
+
*
|
|
21
|
+
* Design constraints (CLAUDE.md / house style — see lib/cadence-bus.mjs,
|
|
22
|
+
* lib/rate-guard.mjs, lib/model-router.mjs):
|
|
23
|
+
* - ESM .mjs, Node 20, near-zero-dep (js-yaml is the one parse dependency).
|
|
24
|
+
* - Everything injectable so tests are hermetic: `opts.now`, `opts.bundledDir`,
|
|
25
|
+
* `opts.catalogText` (map of provider -> raw YAML), `opts.overlay`,
|
|
26
|
+
* `opts.agentConfig`, `opts.localConfig`, `opts.pricingCache` all bypass the FS.
|
|
27
|
+
* - NEVER throws on the read path. `loadCatalog` always returns a typed
|
|
28
|
+
* result `{ catalog, models, byRef, errors, warnings, sources, loadedAt }`;
|
|
29
|
+
* a bad row is dropped with an entry in `errors`, never an exception. A
|
|
30
|
+
* corrupt user file degrades to the bundled rows + a warning.
|
|
31
|
+
* - mtime-aware hot reload: the process-level cache invalidates when any
|
|
32
|
+
* contributing file's mtime advances; SIGHUP forces a full invalidate.
|
|
33
|
+
*
|
|
34
|
+
* @module lib/model-router/catalog
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import {
|
|
38
|
+
existsSync,
|
|
39
|
+
readFileSync,
|
|
40
|
+
readdirSync,
|
|
41
|
+
statSync,
|
|
42
|
+
} from "node:fs";
|
|
43
|
+
import { join, resolve, dirname, basename } from "node:path";
|
|
44
|
+
import { fileURLToPath } from "node:url";
|
|
45
|
+
|
|
46
|
+
// js-yaml is a hard dependency of @cohortapp/agent-sdk (package.json), but we still
|
|
47
|
+
// import it defensively so the read path can degrade rather than crash if the
|
|
48
|
+
// dep tree is ever broken on a machine.
|
|
49
|
+
let yamlParse = null;
|
|
50
|
+
try {
|
|
51
|
+
const y = await import("js-yaml");
|
|
52
|
+
yamlParse = (text) => y.load(text);
|
|
53
|
+
} catch {
|
|
54
|
+
yamlParse = null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
58
|
+
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// Vocabulary (capability typo detection is part of strict validation)
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
const VALID_STATUS = new Set(["available", "preview", "deprecated", "disabled"]);
|
|
64
|
+
const VALID_GRADES = new Set(["A", "B", "C"]);
|
|
65
|
+
const GRADE_RANK = { A: 3, B: 2, C: 1 };
|
|
66
|
+
|
|
67
|
+
const VALID_COMPAT_KEYS = new Set([
|
|
68
|
+
"cache_control",
|
|
69
|
+
"max_tokens_field",
|
|
70
|
+
"thinking_format",
|
|
71
|
+
"requires_reasoning_echo",
|
|
72
|
+
"rejects_duplicate_tool_names",
|
|
73
|
+
"omit_temperature",
|
|
74
|
+
"supports_prompt_cache_key",
|
|
75
|
+
]);
|
|
76
|
+
const VALID_CACHE_CONTROL = new Set(["anthropic", "implicit", "none"]);
|
|
77
|
+
|
|
78
|
+
const VALID_HARNESS_KEYS = new Set(["session", "direct", "batch"]);
|
|
79
|
+
|
|
80
|
+
const VALID_COST_KEYS = new Set([
|
|
81
|
+
"input",
|
|
82
|
+
"output",
|
|
83
|
+
"cache_read",
|
|
84
|
+
"cache_write",
|
|
85
|
+
"tiered",
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
const COST_FIELDS = ["input", "output", "cache_read", "cache_write"];
|
|
89
|
+
|
|
90
|
+
// Bundled providers we recognise. The authority-merge rule "never add providers"
|
|
91
|
+
// is enforced for the overlay (tighten-only); agent config MAY introduce a new
|
|
92
|
+
// provider via catalog_overrides (it is the highest authority and the agent owns
|
|
93
|
+
// its own machine), but we surface a warning so a typo'd provider name is loud.
|
|
94
|
+
const BUNDLED_PROVIDERS = new Set([
|
|
95
|
+
"anthropic",
|
|
96
|
+
"deepseek",
|
|
97
|
+
"moonshot",
|
|
98
|
+
"qwen",
|
|
99
|
+
]);
|
|
100
|
+
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
// Paths
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
function bundledDir(opts) {
|
|
106
|
+
if (opts && opts.bundledDir) return resolve(opts.bundledDir);
|
|
107
|
+
return join(__dirname, "catalog");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function agentRootOf(agentRoot, opts) {
|
|
111
|
+
return resolve(
|
|
112
|
+
(opts && opts.agentRoot) ||
|
|
113
|
+
agentRoot ||
|
|
114
|
+
process.env.AGENT_ROOT ||
|
|
115
|
+
process.env.AGENT_DIR ||
|
|
116
|
+
process.cwd()
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function pricingCachePath(root) {
|
|
121
|
+
return join(root, "state", "model-router", "pricing-cache.json");
|
|
122
|
+
}
|
|
123
|
+
function agentConfigPath(root) {
|
|
124
|
+
return join(root, "config", "model-routing.yaml");
|
|
125
|
+
}
|
|
126
|
+
function localConfigPath(root) {
|
|
127
|
+
return join(root, "config", "models.local.yaml");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// Safe file IO (never throws; records the failure to a collector)
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
function parseYamlSafe(text, label, errors) {
|
|
135
|
+
if (text == null) return null;
|
|
136
|
+
if (!yamlParse) {
|
|
137
|
+
errors.push({ source: label, error: "js-yaml not installed; cannot parse YAML" });
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
return yamlParse(text);
|
|
142
|
+
} catch (err) {
|
|
143
|
+
errors.push({ source: label, error: `parse failed: ${err.message}` });
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function readTextSafe(path) {
|
|
149
|
+
try {
|
|
150
|
+
if (!existsSync(path)) return null;
|
|
151
|
+
return readFileSync(path, "utf-8");
|
|
152
|
+
} catch {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function mtimeSafe(path) {
|
|
158
|
+
try {
|
|
159
|
+
return statSync(path).mtimeMs;
|
|
160
|
+
} catch {
|
|
161
|
+
return 0;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
// Row normalisation + validation (strict: typos surface, never crash)
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
function normaliseCost(rawCost, errors, label) {
|
|
170
|
+
const cost = {
|
|
171
|
+
input: null,
|
|
172
|
+
output: null,
|
|
173
|
+
cache_read: null,
|
|
174
|
+
cache_write: null,
|
|
175
|
+
tiered: [],
|
|
176
|
+
};
|
|
177
|
+
if (rawCost == null) return cost;
|
|
178
|
+
if (typeof rawCost !== "object") {
|
|
179
|
+
errors.push({ source: label, error: "cost must be an object" });
|
|
180
|
+
return cost;
|
|
181
|
+
}
|
|
182
|
+
for (const k of Object.keys(rawCost)) {
|
|
183
|
+
if (!VALID_COST_KEYS.has(k)) {
|
|
184
|
+
errors.push({ source: label, error: `unknown cost field "${k}"` });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
for (const f of COST_FIELDS) {
|
|
188
|
+
const v = rawCost[f];
|
|
189
|
+
if (v == null) {
|
|
190
|
+
cost[f] = null;
|
|
191
|
+
} else if (typeof v === "number" && Number.isFinite(v) && v >= 0) {
|
|
192
|
+
cost[f] = v;
|
|
193
|
+
} else {
|
|
194
|
+
errors.push({ source: label, error: `cost.${f} must be a non-negative number or null` });
|
|
195
|
+
cost[f] = null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (Array.isArray(rawCost.tiered)) {
|
|
199
|
+
cost.tiered = rawCost.tiered.map((t) => ({ ...t }));
|
|
200
|
+
} else if (rawCost.tiered != null) {
|
|
201
|
+
errors.push({ source: label, error: "cost.tiered must be an array" });
|
|
202
|
+
}
|
|
203
|
+
return cost;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function normaliseCompat(rawCompat, errors, label) {
|
|
207
|
+
const compat = {};
|
|
208
|
+
if (rawCompat == null) return compat;
|
|
209
|
+
if (typeof rawCompat !== "object") {
|
|
210
|
+
errors.push({ source: label, error: "compat must be an object" });
|
|
211
|
+
return compat;
|
|
212
|
+
}
|
|
213
|
+
for (const [k, v] of Object.entries(rawCompat)) {
|
|
214
|
+
if (!VALID_COMPAT_KEYS.has(k)) {
|
|
215
|
+
errors.push({ source: label, error: `unknown compat flag "${k}" (capability typo?)` });
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
compat[k] = v;
|
|
219
|
+
}
|
|
220
|
+
if (compat.cache_control != null && !VALID_CACHE_CONTROL.has(compat.cache_control)) {
|
|
221
|
+
errors.push({
|
|
222
|
+
source: label,
|
|
223
|
+
error: `compat.cache_control "${compat.cache_control}" not in {anthropic,implicit,none}`,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
return compat;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function normaliseHarness(rawHarness, errors, label) {
|
|
230
|
+
const harness = { session: false, direct: false, batch: false };
|
|
231
|
+
if (rawHarness == null) return harness;
|
|
232
|
+
if (typeof rawHarness !== "object") {
|
|
233
|
+
errors.push({ source: label, error: "harness must be an object" });
|
|
234
|
+
return harness;
|
|
235
|
+
}
|
|
236
|
+
for (const [k, v] of Object.entries(rawHarness)) {
|
|
237
|
+
if (!VALID_HARNESS_KEYS.has(k)) {
|
|
238
|
+
errors.push({ source: label, error: `unknown harness key "${k}"` });
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
harness[k] = v === true;
|
|
242
|
+
}
|
|
243
|
+
return harness;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function normaliseProvenance(rawProv) {
|
|
247
|
+
const p = rawProv && typeof rawProv === "object" ? rawProv : {};
|
|
248
|
+
return {
|
|
249
|
+
source: typeof p.source === "string" ? p.source : "unknown",
|
|
250
|
+
fetched: typeof p.fetched === "string" ? p.fetched : null,
|
|
251
|
+
volatile: p.volatile === true,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Validate + normalise one model row. Returns `{ row, ok }`. On a fatal
|
|
257
|
+
* problem (no id, bad status) `ok:false` and the row is dropped from the
|
|
258
|
+
* catalog; non-fatal issues are accumulated in `errors` but the row survives.
|
|
259
|
+
*/
|
|
260
|
+
function normaliseRow(provider, rawRow, errors) {
|
|
261
|
+
const id = rawRow && typeof rawRow.id === "string" ? rawRow.id.trim() : "";
|
|
262
|
+
const label = `${provider}/${id || "<no-id>"}`;
|
|
263
|
+
if (!id) {
|
|
264
|
+
errors.push({ source: provider, error: "model row missing string `id` — dropped" });
|
|
265
|
+
return { ok: false };
|
|
266
|
+
}
|
|
267
|
+
let status = rawRow.status;
|
|
268
|
+
if (status == null) status = "available";
|
|
269
|
+
if (!VALID_STATUS.has(status)) {
|
|
270
|
+
errors.push({
|
|
271
|
+
source: label,
|
|
272
|
+
error: `status "${status}" not in {available,preview,deprecated,disabled} — dropped`,
|
|
273
|
+
});
|
|
274
|
+
return { ok: false };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const tool_reliability = rawRow.tool_reliability;
|
|
278
|
+
if (tool_reliability != null && !VALID_GRADES.has(tool_reliability)) {
|
|
279
|
+
errors.push({ source: label, error: `tool_reliability "${tool_reliability}" not in {A,B,C}` });
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const row = {
|
|
283
|
+
provider,
|
|
284
|
+
ref: `${provider}/${id}`,
|
|
285
|
+
id,
|
|
286
|
+
status,
|
|
287
|
+
replaced_by: rawRow.replaced_by ?? null,
|
|
288
|
+
context_window: numOrNull(rawRow.context_window),
|
|
289
|
+
context_tokens: numOrNull(rawRow.context_tokens),
|
|
290
|
+
max_tokens: numOrNull(rawRow.max_tokens),
|
|
291
|
+
cost: normaliseCost(rawRow.cost, errors, label),
|
|
292
|
+
cost_provenance: normaliseProvenance(rawRow.cost_provenance),
|
|
293
|
+
compat: normaliseCompat(rawRow.compat, errors, label),
|
|
294
|
+
tool_reliability: VALID_GRADES.has(tool_reliability) ? tool_reliability : null,
|
|
295
|
+
harness: normaliseHarness(rawRow.harness, errors, label),
|
|
296
|
+
quality: rawRow.quality && typeof rawRow.quality === "object" ? { ...rawRow.quality } : {},
|
|
297
|
+
};
|
|
298
|
+
return { ok: true, row };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function numOrNull(v) {
|
|
302
|
+
if (v == null) return null;
|
|
303
|
+
const n = Number(v);
|
|
304
|
+
return Number.isFinite(n) ? n : null;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ---------------------------------------------------------------------------
|
|
308
|
+
// Provider-file parsing → array of normalised rows
|
|
309
|
+
// ---------------------------------------------------------------------------
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Parse a single provider catalog document (the shape of a bundled YAML file)
|
|
313
|
+
* into normalised rows. Returns `{ provider, auth_env, endpoints,
|
|
314
|
+
* data_residency, rows: Map<id,row> }` or null on a fatal structural problem.
|
|
315
|
+
*/
|
|
316
|
+
function parseProviderDoc(doc, label, errors) {
|
|
317
|
+
if (!doc || typeof doc !== "object") {
|
|
318
|
+
errors.push({ source: label, error: "catalog document is empty or not an object" });
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
const provider = typeof doc.provider === "string" ? doc.provider.trim() : "";
|
|
322
|
+
if (!provider) {
|
|
323
|
+
errors.push({ source: label, error: "catalog document missing string `provider`" });
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
const rows = new Map();
|
|
327
|
+
const models = Array.isArray(doc.models) ? doc.models : [];
|
|
328
|
+
if (!Array.isArray(doc.models)) {
|
|
329
|
+
errors.push({ source: label, error: "catalog `models` is not an array" });
|
|
330
|
+
}
|
|
331
|
+
for (const rawRow of models) {
|
|
332
|
+
const { ok, row } = normaliseRow(provider, rawRow, errors);
|
|
333
|
+
if (!ok) continue;
|
|
334
|
+
// Provider-level metadata is hoisted onto each row so resolveChain has it
|
|
335
|
+
// in one place (rows carry their own auth_env/endpoints/data_residency).
|
|
336
|
+
row.auth_env = typeof doc.auth_env === "string" ? doc.auth_env : null;
|
|
337
|
+
row.endpoints = doc.endpoints && typeof doc.endpoints === "object" ? { ...doc.endpoints } : {};
|
|
338
|
+
row.data_residency =
|
|
339
|
+
typeof doc.data_residency === "string" ? doc.data_residency : null;
|
|
340
|
+
rows.set(row.id, row);
|
|
341
|
+
}
|
|
342
|
+
return {
|
|
343
|
+
provider,
|
|
344
|
+
auth_env: typeof doc.auth_env === "string" ? doc.auth_env : null,
|
|
345
|
+
endpoints: doc.endpoints && typeof doc.endpoints === "object" ? { ...doc.endpoints } : {},
|
|
346
|
+
data_residency: typeof doc.data_residency === "string" ? doc.data_residency : null,
|
|
347
|
+
rows,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ---------------------------------------------------------------------------
|
|
352
|
+
// Source gathering (each returns provider docs + records its file mtime)
|
|
353
|
+
// ---------------------------------------------------------------------------
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* @typedef {Object} CatalogSource
|
|
357
|
+
* @property {string} layer "bundled"|"pricing-cache"|"overlay"|"local"|"agent"
|
|
358
|
+
* @property {string} [path] file path if FS-backed
|
|
359
|
+
* @property {number} [mtime]
|
|
360
|
+
* @property {Map<string,object>} providers provider -> parsed provider doc
|
|
361
|
+
*/
|
|
362
|
+
|
|
363
|
+
function gatherBundled(opts, errors, mtimes) {
|
|
364
|
+
const providers = new Map();
|
|
365
|
+
// Injected text map bypasses the FS entirely (hermetic tests).
|
|
366
|
+
if (opts && opts.catalogText && typeof opts.catalogText === "object") {
|
|
367
|
+
for (const [name, text] of Object.entries(opts.catalogText)) {
|
|
368
|
+
const doc = parseYamlSafe(text, `bundled:${name}`, errors);
|
|
369
|
+
const parsed = parseProviderDoc(doc, `bundled:${name}`, errors);
|
|
370
|
+
if (parsed) providers.set(parsed.provider, parsed);
|
|
371
|
+
}
|
|
372
|
+
return { layer: "bundled", providers };
|
|
373
|
+
}
|
|
374
|
+
const dir = bundledDir(opts);
|
|
375
|
+
let files = [];
|
|
376
|
+
try {
|
|
377
|
+
files = readdirSync(dir).filter((f) => f.endsWith(".yaml") || f.endsWith(".yml"));
|
|
378
|
+
} catch (err) {
|
|
379
|
+
errors.push({ source: "bundled", error: `cannot read catalog dir ${dir}: ${err.message}` });
|
|
380
|
+
return { layer: "bundled", providers };
|
|
381
|
+
}
|
|
382
|
+
for (const f of files.sort()) {
|
|
383
|
+
const path = join(dir, f);
|
|
384
|
+
const text = readTextSafe(path);
|
|
385
|
+
if (text == null) continue;
|
|
386
|
+
mtimes.push(mtimeSafe(path));
|
|
387
|
+
const doc = parseYamlSafe(text, basename(f), errors);
|
|
388
|
+
const parsed = parseProviderDoc(doc, basename(f), errors);
|
|
389
|
+
if (parsed) providers.set(parsed.provider, parsed);
|
|
390
|
+
}
|
|
391
|
+
return { layer: "bundled", providers, path: dir };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Read the pricing cache (COST FIELDS ONLY). Shape:
|
|
396
|
+
* { "<provider>/<id>": { input, output, cache_read, cache_write, tiered?,
|
|
397
|
+
* provenance:{source,fetched,volatile} }, ... }
|
|
398
|
+
* Injected via opts.pricingCache (object) or read from
|
|
399
|
+
* state/model-router/pricing-cache.json.
|
|
400
|
+
*/
|
|
401
|
+
function gatherPricingCache(root, opts, errors, mtimes) {
|
|
402
|
+
let obj = null;
|
|
403
|
+
if (opts && opts.pricingCache !== undefined) {
|
|
404
|
+
obj = opts.pricingCache;
|
|
405
|
+
} else {
|
|
406
|
+
const path = pricingCachePath(root);
|
|
407
|
+
const text = readTextSafe(path);
|
|
408
|
+
if (text != null) {
|
|
409
|
+
mtimes.push(mtimeSafe(path));
|
|
410
|
+
try {
|
|
411
|
+
obj = JSON.parse(text);
|
|
412
|
+
} catch (err) {
|
|
413
|
+
errors.push({ source: "pricing-cache", error: `parse failed: ${err.message}` });
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
if (!obj || typeof obj !== "object") return null;
|
|
419
|
+
// The cache may wrap entries under a top-level key; accept both `{models:{}}`
|
|
420
|
+
// and a flat `{ "<ref>": {...} }`.
|
|
421
|
+
const entries = obj.models && typeof obj.models === "object" ? obj.models : obj;
|
|
422
|
+
return { layer: "pricing-cache", entries };
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function gatherUserConfigPatch(root, opts, key, fileFn, layer, errors, mtimes) {
|
|
426
|
+
// key = "agentConfig" | "localConfig"; produces a list of catalog_overrides rows.
|
|
427
|
+
let doc = null;
|
|
428
|
+
if (opts && opts[key] !== undefined) {
|
|
429
|
+
doc = opts[key];
|
|
430
|
+
} else {
|
|
431
|
+
const path = fileFn(root);
|
|
432
|
+
const text = readTextSafe(path);
|
|
433
|
+
if (text != null) {
|
|
434
|
+
mtimes.push(mtimeSafe(path));
|
|
435
|
+
doc = parseYamlSafe(text, layer, errors);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
if (!doc || typeof doc !== "object") return null;
|
|
439
|
+
// Agent config carries the patch under `catalog_overrides`; models.local.yaml
|
|
440
|
+
// may either be a bare provider doc OR also use `catalog_overrides`.
|
|
441
|
+
const patch =
|
|
442
|
+
doc.catalog_overrides !== undefined ? doc.catalog_overrides : doc.providers ?? doc;
|
|
443
|
+
return { layer, patch };
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// ---------------------------------------------------------------------------
|
|
447
|
+
// Merge engine
|
|
448
|
+
// ---------------------------------------------------------------------------
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Apply a cost-only patch from the pricing cache onto the working byRef map.
|
|
452
|
+
* Capabilities/compat/grade are NEVER touched here.
|
|
453
|
+
*/
|
|
454
|
+
function applyPricingCache(byRef, cacheLayer, warnings) {
|
|
455
|
+
if (!cacheLayer || !cacheLayer.entries) return;
|
|
456
|
+
for (const [ref, patch] of Object.entries(cacheLayer.entries)) {
|
|
457
|
+
const row = byRef.get(ref);
|
|
458
|
+
if (!row) {
|
|
459
|
+
warnings.push({ source: "pricing-cache", warning: `price for unknown row "${ref}" ignored` });
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
if (!patch || typeof patch !== "object") continue;
|
|
463
|
+
for (const f of COST_FIELDS) {
|
|
464
|
+
if (patch[f] === undefined) continue;
|
|
465
|
+
const v = patch[f];
|
|
466
|
+
if (v == null) {
|
|
467
|
+
row.cost[f] = null;
|
|
468
|
+
} else if (typeof v === "number" && Number.isFinite(v) && v >= 0) {
|
|
469
|
+
row.cost[f] = v;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
if (Array.isArray(patch.tiered)) row.cost.tiered = patch.tiered.map((t) => ({ ...t }));
|
|
473
|
+
if (patch.provenance && typeof patch.provenance === "object") {
|
|
474
|
+
row.cost_provenance = normaliseProvenance(patch.provenance);
|
|
475
|
+
} else {
|
|
476
|
+
// A price arrived from the cache; stamp provenance so staleness checks see it.
|
|
477
|
+
row.cost_provenance = {
|
|
478
|
+
...row.cost_provenance,
|
|
479
|
+
source: "pricing-cache",
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Apply the TIGHTEN-ONLY org overlay catalog_patch. May:
|
|
487
|
+
* - set status to a more-restrictive value (e.g. -> disabled)
|
|
488
|
+
* - LOWER tool_reliability (A->B->C)
|
|
489
|
+
* May NOT:
|
|
490
|
+
* - add a provider/model the bundled+user layers don't define
|
|
491
|
+
* - RAISE tool_reliability
|
|
492
|
+
* - widen status back to available from disabled (tighten-only)
|
|
493
|
+
* Violations are recorded as errors and the offending field is left untouched.
|
|
494
|
+
*/
|
|
495
|
+
function applyOverlayPatch(byRef, overlay, errors) {
|
|
496
|
+
if (!overlay) return;
|
|
497
|
+
const patch = Array.isArray(overlay.catalog_patch)
|
|
498
|
+
? overlay.catalog_patch
|
|
499
|
+
: Array.isArray(overlay)
|
|
500
|
+
? overlay
|
|
501
|
+
: [];
|
|
502
|
+
for (const entry of patch) {
|
|
503
|
+
if (!entry || typeof entry !== "object" || typeof entry.ref !== "string") continue;
|
|
504
|
+
const row = byRef.get(entry.ref);
|
|
505
|
+
if (!row) {
|
|
506
|
+
errors.push({
|
|
507
|
+
source: "overlay",
|
|
508
|
+
error: `overlay patch references unknown row "${entry.ref}" — cannot add providers (tighten-only)`,
|
|
509
|
+
});
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
if (entry.status !== undefined) {
|
|
513
|
+
if (!VALID_STATUS.has(entry.status)) {
|
|
514
|
+
errors.push({ source: "overlay", error: `overlay status "${entry.status}" invalid for ${entry.ref}` });
|
|
515
|
+
} else if (entry.status === "disabled" || entry.status === "deprecated") {
|
|
516
|
+
row.status = entry.status; // tightening is allowed
|
|
517
|
+
} else {
|
|
518
|
+
errors.push({
|
|
519
|
+
source: "overlay",
|
|
520
|
+
error: `overlay cannot widen status of ${entry.ref} to "${entry.status}" (tighten-only)`,
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
if (entry.tool_reliability !== undefined) {
|
|
525
|
+
if (!VALID_GRADES.has(entry.tool_reliability)) {
|
|
526
|
+
errors.push({ source: "overlay", error: `overlay grade "${entry.tool_reliability}" invalid for ${entry.ref}` });
|
|
527
|
+
} else {
|
|
528
|
+
const cur = row.tool_reliability ? GRADE_RANK[row.tool_reliability] : 0;
|
|
529
|
+
const next = GRADE_RANK[entry.tool_reliability];
|
|
530
|
+
if (next <= cur) {
|
|
531
|
+
row.tool_reliability = entry.tool_reliability; // lowering allowed
|
|
532
|
+
} else {
|
|
533
|
+
errors.push({
|
|
534
|
+
source: "overlay",
|
|
535
|
+
error: `overlay cannot raise tool_reliability of ${entry.ref} from ${row.tool_reliability} to ${entry.tool_reliability} (tighten-only)`,
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Apply a user-config patch (agent catalog_overrides / models.local.yaml).
|
|
545
|
+
* These are the HIGHEST authority and the agent owns its own machine, so a full
|
|
546
|
+
* row patch is permitted (capabilities, compat, cost, grade). A patch that adds
|
|
547
|
+
* an entirely new provider is allowed but warned (typo guard).
|
|
548
|
+
*/
|
|
549
|
+
function applyUserPatch(byRef, providersByName, layer, patch, errors, warnings) {
|
|
550
|
+
if (!patch) return;
|
|
551
|
+
// Patch may be: an array of partial rows ({ref, ...overrides}),
|
|
552
|
+
// OR a map provider -> provider-doc, OR a single provider doc.
|
|
553
|
+
if (Array.isArray(patch)) {
|
|
554
|
+
for (const entry of patch) {
|
|
555
|
+
if (!entry || typeof entry !== "object") continue;
|
|
556
|
+
const ref = typeof entry.ref === "string" ? entry.ref : null;
|
|
557
|
+
if (!ref) {
|
|
558
|
+
errors.push({ source: layer, error: "catalog_overrides entry missing `ref`" });
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
mergePartialRow(byRef, ref, entry, layer, errors, warnings);
|
|
562
|
+
}
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
if (typeof patch === "object") {
|
|
566
|
+
// Provider-doc form (bare or under provider keys).
|
|
567
|
+
const docs =
|
|
568
|
+
patch.provider !== undefined
|
|
569
|
+
? [patch]
|
|
570
|
+
: Object.values(patch).filter((v) => v && typeof v === "object" && v.provider);
|
|
571
|
+
for (const doc of docs) {
|
|
572
|
+
const parsed = parseProviderDoc(doc, layer, errors);
|
|
573
|
+
if (!parsed) continue;
|
|
574
|
+
if (!BUNDLED_PROVIDERS.has(parsed.provider) && !providersByName.has(parsed.provider)) {
|
|
575
|
+
warnings.push({
|
|
576
|
+
source: layer,
|
|
577
|
+
warning: `introduces non-bundled provider "${parsed.provider}" (typo? deliberate?)`,
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
providersByName.set(parsed.provider, parsed);
|
|
581
|
+
for (const row of parsed.rows.values()) byRef.set(row.ref, row);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function mergePartialRow(byRef, ref, entry, layer, errors, warnings) {
|
|
587
|
+
const existing = byRef.get(ref);
|
|
588
|
+
if (!existing) {
|
|
589
|
+
// Allowed (agent owns the machine) but loud — a typo'd ref would otherwise
|
|
590
|
+
// silently create a phantom row.
|
|
591
|
+
warnings.push({ source: layer, warning: `catalog_overrides creates new row "${ref}"` });
|
|
592
|
+
const [provider, ...idParts] = ref.split("/");
|
|
593
|
+
const id = idParts.join("/");
|
|
594
|
+
const { ok, row } = normaliseRow(provider, { ...entry, id }, errors);
|
|
595
|
+
if (ok) {
|
|
596
|
+
row.auth_env = entry.auth_env ?? null;
|
|
597
|
+
row.endpoints = entry.endpoints ?? {};
|
|
598
|
+
row.data_residency = entry.data_residency ?? null;
|
|
599
|
+
byRef.set(ref, row);
|
|
600
|
+
}
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
// Field-wise override on the existing row.
|
|
604
|
+
if (entry.status !== undefined) {
|
|
605
|
+
if (VALID_STATUS.has(entry.status)) existing.status = entry.status;
|
|
606
|
+
else errors.push({ source: layer, error: `override status "${entry.status}" invalid for ${ref}` });
|
|
607
|
+
}
|
|
608
|
+
if (entry.tool_reliability !== undefined) {
|
|
609
|
+
if (VALID_GRADES.has(entry.tool_reliability)) existing.tool_reliability = entry.tool_reliability;
|
|
610
|
+
else errors.push({ source: layer, error: `override grade "${entry.tool_reliability}" invalid for ${ref}` });
|
|
611
|
+
}
|
|
612
|
+
for (const f of ["context_window", "context_tokens", "max_tokens", "replaced_by"]) {
|
|
613
|
+
if (entry[f] !== undefined) existing[f] = f === "replaced_by" ? entry[f] : numOrNull(entry[f]);
|
|
614
|
+
}
|
|
615
|
+
if (entry.cost !== undefined) {
|
|
616
|
+
const c = normaliseCost(entry.cost, errors, `${layer}:${ref}`);
|
|
617
|
+
for (const f of COST_FIELDS) if (entry.cost[f] !== undefined) existing.cost[f] = c[f];
|
|
618
|
+
if (Array.isArray(entry.cost.tiered)) existing.cost.tiered = c.tiered;
|
|
619
|
+
}
|
|
620
|
+
if (entry.compat !== undefined) {
|
|
621
|
+
Object.assign(existing.compat, normaliseCompat(entry.compat, errors, `${layer}:${ref}`));
|
|
622
|
+
}
|
|
623
|
+
if (entry.harness !== undefined) {
|
|
624
|
+
Object.assign(existing.harness, normaliseHarness(entry.harness, errors, `${layer}:${ref}`));
|
|
625
|
+
}
|
|
626
|
+
if (entry.cost_provenance !== undefined) {
|
|
627
|
+
existing.cost_provenance = normaliseProvenance(entry.cost_provenance);
|
|
628
|
+
}
|
|
629
|
+
if (entry.auth_env !== undefined) existing.auth_env = entry.auth_env;
|
|
630
|
+
if (entry.endpoints !== undefined) existing.endpoints = { ...entry.endpoints };
|
|
631
|
+
if (entry.data_residency !== undefined) existing.data_residency = entry.data_residency;
|
|
632
|
+
if (entry.quality !== undefined && typeof entry.quality === "object") {
|
|
633
|
+
Object.assign(existing.quality, entry.quality);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// ---------------------------------------------------------------------------
|
|
638
|
+
// Public: loadCatalog
|
|
639
|
+
// ---------------------------------------------------------------------------
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* @typedef {Object} Catalog
|
|
643
|
+
* @property {object[]} models normalised rows (array, stable order)
|
|
644
|
+
* @property {Map<string,object>} byRef "provider/id" -> row
|
|
645
|
+
* @property {Map<string,object>} providers provider -> { auth_env, endpoints, data_residency }
|
|
646
|
+
*/
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* @typedef {Object} CatalogResult
|
|
650
|
+
* @property {Catalog} catalog
|
|
651
|
+
* @property {object[]} models convenience: catalog.models
|
|
652
|
+
* @property {Map<string,object>} byRef convenience: catalog.byRef
|
|
653
|
+
* @property {{source:string,error:string}[]} errors
|
|
654
|
+
* @property {{source:string,warning:string}[]} warnings
|
|
655
|
+
* @property {string[]} sources layers that contributed
|
|
656
|
+
* @property {number} loadedAt epoch ms
|
|
657
|
+
*/
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Load + authority-merge the full catalog. NEVER throws.
|
|
661
|
+
*
|
|
662
|
+
* @param {string} [agentRoot]
|
|
663
|
+
* @param {object} [opts]
|
|
664
|
+
* @param {string} [opts.agentRoot]
|
|
665
|
+
* @param {string} [opts.bundledDir] override bundled-rows dir
|
|
666
|
+
* @param {object} [opts.catalogText] { provider: rawYaml } — bypass FS for bundled rows
|
|
667
|
+
* @param {object} [opts.pricingCache] cost-only patch object (bypass FS)
|
|
668
|
+
* @param {object} [opts.overlay] signed org overlay (already-verified object)
|
|
669
|
+
* @param {object} [opts.agentConfig] config/model-routing.yaml doc (bypass FS)
|
|
670
|
+
* @param {object} [opts.localConfig] config/models.local.yaml doc (bypass FS)
|
|
671
|
+
* @param {()=>number} [opts.now]
|
|
672
|
+
* @returns {CatalogResult}
|
|
673
|
+
*/
|
|
674
|
+
export function loadCatalog(agentRoot, opts = {}) {
|
|
675
|
+
const now = (opts.now || Date.now)();
|
|
676
|
+
const root = agentRootOf(agentRoot, opts);
|
|
677
|
+
const errors = [];
|
|
678
|
+
const warnings = [];
|
|
679
|
+
const mtimes = [];
|
|
680
|
+
const sources = [];
|
|
681
|
+
|
|
682
|
+
// Layer 4: bundled rows (the floor).
|
|
683
|
+
const bundled = gatherBundled(opts, errors, mtimes);
|
|
684
|
+
sources.push("bundled");
|
|
685
|
+
const byRef = new Map();
|
|
686
|
+
const providersByName = new Map();
|
|
687
|
+
for (const parsed of bundled.providers.values()) {
|
|
688
|
+
providersByName.set(parsed.provider, parsed);
|
|
689
|
+
for (const row of parsed.rows.values()) byRef.set(row.ref, row);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// Layer 3: pricing cache (COST ONLY).
|
|
693
|
+
const pricing = gatherPricingCache(root, opts, errors, mtimes);
|
|
694
|
+
if (pricing) {
|
|
695
|
+
applyPricingCache(byRef, pricing, warnings);
|
|
696
|
+
sources.push("pricing-cache");
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// Layer 2: org overlay catalog_patch (TIGHTEN-ONLY).
|
|
700
|
+
const overlay = opts.overlay ?? null;
|
|
701
|
+
if (overlay) {
|
|
702
|
+
applyOverlayPatch(byRef, overlay, errors);
|
|
703
|
+
sources.push("overlay");
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Layer 1: config/models.local.yaml.
|
|
707
|
+
const local = gatherUserConfigPatch(root, opts, "localConfig", localConfigPath, "models.local.yaml", errors, mtimes);
|
|
708
|
+
if (local) {
|
|
709
|
+
applyUserPatch(byRef, providersByName, "models.local.yaml", local.patch, errors, warnings);
|
|
710
|
+
sources.push("models.local.yaml");
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// Layer 0: agent config/model-routing.yaml catalog_overrides (HIGHEST).
|
|
714
|
+
const agent = gatherUserConfigPatch(root, opts, "agentConfig", agentConfigPath, "model-routing.yaml", errors, mtimes);
|
|
715
|
+
if (agent) {
|
|
716
|
+
applyUserPatch(byRef, providersByName, "model-routing.yaml", agent.patch, errors, warnings);
|
|
717
|
+
sources.push("model-routing.yaml");
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Re-validate cross-cutting invariants after merge (e.g. price sanity).
|
|
721
|
+
for (const row of byRef.values()) {
|
|
722
|
+
if (row.cost.input != null && row.cost.output != null && row.cost.input > row.cost.output * 5) {
|
|
723
|
+
warnings.push({
|
|
724
|
+
source: row.ref,
|
|
725
|
+
warning: `input price (${row.cost.input}) > 5x output (${row.cost.output}) — suspicious`,
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
if (
|
|
729
|
+
row.context_tokens != null &&
|
|
730
|
+
row.context_window != null &&
|
|
731
|
+
row.context_tokens > row.context_window
|
|
732
|
+
) {
|
|
733
|
+
warnings.push({
|
|
734
|
+
source: row.ref,
|
|
735
|
+
warning: `context_tokens (${row.context_tokens}) exceeds context_window (${row.context_window})`,
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
const models = [...byRef.values()];
|
|
741
|
+
const providers = new Map();
|
|
742
|
+
for (const [name, doc] of providersByName) {
|
|
743
|
+
providers.set(name, {
|
|
744
|
+
provider: name,
|
|
745
|
+
auth_env: doc.auth_env,
|
|
746
|
+
endpoints: doc.endpoints,
|
|
747
|
+
data_residency: doc.data_residency,
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
const catalog = { models, byRef, providers };
|
|
752
|
+
return {
|
|
753
|
+
catalog,
|
|
754
|
+
models,
|
|
755
|
+
byRef,
|
|
756
|
+
errors,
|
|
757
|
+
warnings,
|
|
758
|
+
sources,
|
|
759
|
+
loadedAt: now,
|
|
760
|
+
_mtimeKey: maxMtime(mtimes),
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function maxMtime(mtimes) {
|
|
765
|
+
let m = 0;
|
|
766
|
+
for (const t of mtimes) if (t > m) m = t;
|
|
767
|
+
return m;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// ---------------------------------------------------------------------------
|
|
771
|
+
// Public: lookupModel / listModels
|
|
772
|
+
// ---------------------------------------------------------------------------
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* Resolve a "provider/model" ref against a loaded catalog. NEVER throws.
|
|
776
|
+
*
|
|
777
|
+
* @param {Catalog|CatalogResult} catalog result of loadCatalog (or its .catalog)
|
|
778
|
+
* @param {string} ref "provider/model-id"
|
|
779
|
+
* @returns {object|null} the normalised row, or null if absent
|
|
780
|
+
*/
|
|
781
|
+
export function lookupModel(catalog, ref) {
|
|
782
|
+
const byRef = catalog && (catalog.byRef || (catalog.catalog && catalog.catalog.byRef));
|
|
783
|
+
if (!byRef || typeof ref !== "string") return null;
|
|
784
|
+
return byRef.get(ref) || null;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* List catalog rows matching an optional filter. NEVER throws.
|
|
789
|
+
*
|
|
790
|
+
* @param {Catalog|CatalogResult} catalog
|
|
791
|
+
* @param {object} [filter]
|
|
792
|
+
* @param {string} [filter.provider]
|
|
793
|
+
* @param {string|string[]} [filter.status] default: any
|
|
794
|
+
* @param {"A"|"B"|"C"} [filter.minGrade] tool_reliability >= this
|
|
795
|
+
* @param {"session"|"direct"|"batch"} [filter.harness] harness[x] === true
|
|
796
|
+
* @param {boolean} [filter.available] shorthand for status:"available"
|
|
797
|
+
* @returns {object[]}
|
|
798
|
+
*/
|
|
799
|
+
export function listModels(catalog, filter = {}) {
|
|
800
|
+
const models =
|
|
801
|
+
(catalog && (catalog.models || (catalog.catalog && catalog.catalog.models))) || [];
|
|
802
|
+
const wantStatus =
|
|
803
|
+
filter.available === true
|
|
804
|
+
? new Set(["available"])
|
|
805
|
+
: filter.status
|
|
806
|
+
? new Set(Array.isArray(filter.status) ? filter.status : [filter.status])
|
|
807
|
+
: null;
|
|
808
|
+
const minRank = filter.minGrade ? GRADE_RANK[filter.minGrade] || 0 : 0;
|
|
809
|
+
return models.filter((row) => {
|
|
810
|
+
if (filter.provider && row.provider !== filter.provider) return false;
|
|
811
|
+
if (wantStatus && !wantStatus.has(row.status)) return false;
|
|
812
|
+
if (minRank > 0) {
|
|
813
|
+
const r = row.tool_reliability ? GRADE_RANK[row.tool_reliability] : 0;
|
|
814
|
+
if (r < minRank) return false;
|
|
815
|
+
}
|
|
816
|
+
if (filter.harness && row.harness[filter.harness] !== true) return false;
|
|
817
|
+
return true;
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// ---------------------------------------------------------------------------
|
|
822
|
+
// Hot reload: mtime-aware process cache + SIGHUP invalidate
|
|
823
|
+
// ---------------------------------------------------------------------------
|
|
824
|
+
|
|
825
|
+
/** @type {Map<string, {result: CatalogResult, signature: string}>} */
|
|
826
|
+
const _cache = new Map();
|
|
827
|
+
let _sighupGeneration = 0;
|
|
828
|
+
let _sighupArmed = false;
|
|
829
|
+
|
|
830
|
+
function armSighup() {
|
|
831
|
+
if (_sighupArmed) return;
|
|
832
|
+
_sighupArmed = true;
|
|
833
|
+
try {
|
|
834
|
+
process.on("SIGHUP", () => {
|
|
835
|
+
_sighupGeneration += 1;
|
|
836
|
+
_cache.clear();
|
|
837
|
+
});
|
|
838
|
+
} catch {
|
|
839
|
+
/* environments without signal support (e.g. some test runners) just skip */
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* Compute the mtime signature of every file that could contribute to the
|
|
845
|
+
* catalog for `root`. Cheap (stat only). Used to decide whether the cached
|
|
846
|
+
* load is still valid.
|
|
847
|
+
*/
|
|
848
|
+
function mtimeSignature(root, opts) {
|
|
849
|
+
const parts = [`gen:${_sighupGeneration}`];
|
|
850
|
+
// Bundled dir: stat each file (its set rarely changes, but a price edit during
|
|
851
|
+
// dev should hot-reload).
|
|
852
|
+
if (opts.catalogText) {
|
|
853
|
+
parts.push("injected-bundled");
|
|
854
|
+
} else {
|
|
855
|
+
const dir = bundledDir(opts);
|
|
856
|
+
try {
|
|
857
|
+
for (const f of readdirSync(dir).sort()) {
|
|
858
|
+
if (!/\.ya?ml$/.test(f)) continue;
|
|
859
|
+
parts.push(`b:${f}:${mtimeSafe(join(dir, f))}`);
|
|
860
|
+
}
|
|
861
|
+
} catch {
|
|
862
|
+
parts.push("b:err");
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
for (const [tag, fn, injected] of [
|
|
866
|
+
["price", pricingCachePath, opts.pricingCache !== undefined],
|
|
867
|
+
["local", localConfigPath, opts.localConfig !== undefined],
|
|
868
|
+
["agent", agentConfigPath, opts.agentConfig !== undefined],
|
|
869
|
+
]) {
|
|
870
|
+
if (injected) parts.push(`${tag}:injected`);
|
|
871
|
+
else parts.push(`${tag}:${mtimeSafe(fn(root))}`);
|
|
872
|
+
}
|
|
873
|
+
// Overlay is passed by value (already verified); fold its version in.
|
|
874
|
+
if (opts.overlay) parts.push(`ovl:${opts.overlay.version || "?"}`);
|
|
875
|
+
return parts.join("|");
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* Cached, mtime-aware catalog load. Returns the same `CatalogResult` shape as
|
|
880
|
+
* `loadCatalog`, but reuses a previously parsed result when no contributing
|
|
881
|
+
* file's mtime has advanced (and no SIGHUP has fired). NEVER throws.
|
|
882
|
+
*
|
|
883
|
+
* Note: when any of the bypass-the-FS injection opts are supplied (catalogText,
|
|
884
|
+
* pricingCache, overlay, agentConfig, localConfig) the signature folds them in
|
|
885
|
+
* by identity/version so hermetic tests still behave; pass a unique `cacheKey`
|
|
886
|
+
* to force isolation between test cases.
|
|
887
|
+
*
|
|
888
|
+
* @param {string} [agentRoot]
|
|
889
|
+
* @param {object} [opts] same as loadCatalog, plus:
|
|
890
|
+
* @param {string} [opts.cacheKey] override the cache bucket (test isolation)
|
|
891
|
+
* @param {boolean} [opts.force] bypass the cache for this call
|
|
892
|
+
* @returns {CatalogResult}
|
|
893
|
+
*/
|
|
894
|
+
export function loadCatalogCached(agentRoot, opts = {}) {
|
|
895
|
+
armSighup();
|
|
896
|
+
const root = agentRootOf(agentRoot, opts);
|
|
897
|
+
const bucket = opts.cacheKey || root;
|
|
898
|
+
const signature = mtimeSignature(root, opts);
|
|
899
|
+
if (!opts.force) {
|
|
900
|
+
const hit = _cache.get(bucket);
|
|
901
|
+
if (hit && hit.signature === signature) return hit.result;
|
|
902
|
+
}
|
|
903
|
+
const result = loadCatalog(agentRoot, opts);
|
|
904
|
+
_cache.set(bucket, { result, signature });
|
|
905
|
+
return result;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/** Force-invalidate the catalog cache (all buckets, or one). */
|
|
909
|
+
export function invalidateCatalogCache(bucket) {
|
|
910
|
+
if (bucket) _cache.delete(bucket);
|
|
911
|
+
else _cache.clear();
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
export const _internals = {
|
|
915
|
+
parseProviderDoc,
|
|
916
|
+
normaliseRow,
|
|
917
|
+
applyPricingCache,
|
|
918
|
+
applyOverlayPatch,
|
|
919
|
+
applyUserPatch,
|
|
920
|
+
GRADE_RANK,
|
|
921
|
+
VALID_STATUS,
|
|
922
|
+
mtimeSignature,
|
|
923
|
+
};
|
|
924
|
+
|
|
925
|
+
export default { loadCatalog, loadCatalogCached, invalidateCatalogCache, lookupModel, listModels };
|