@vellumai/assistant 0.3.4 → 0.3.6
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/Dockerfile +2 -0
- package/README.md +88 -2
- package/eslint.config.mjs +31 -0
- package/package.json +1 -1
- package/scripts/ipc/check-swift-decoder-drift.ts +4 -1
- package/scripts/ipc/generate-swift.ts +31 -2
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +438 -1
- package/src/__tests__/approval-conversation-turn.test.ts +214 -0
- package/src/__tests__/approval-hardcoded-copy-guard.test.ts +41 -0
- package/src/__tests__/approval-message-composer.test.ts +253 -0
- package/src/__tests__/browser-manager.test.ts +1 -0
- package/src/__tests__/call-conversation-messages.test.ts +130 -0
- package/src/__tests__/call-domain.test.ts +12 -2
- package/src/__tests__/call-orchestrator.test.ts +799 -249
- package/src/__tests__/call-pointer-messages.test.ts +148 -0
- package/src/__tests__/call-recovery.test.ts +3 -0
- package/src/__tests__/call-routes-http.test.ts +32 -2
- package/src/__tests__/call-store.test.ts +3 -0
- package/src/__tests__/channel-approval-routes.test.ts +1277 -98
- package/src/__tests__/channel-approval.test.ts +37 -0
- package/src/__tests__/channel-approvals.test.ts +36 -50
- package/src/__tests__/channel-guardian.test.ts +630 -22
- package/src/__tests__/channel-readiness-service.test.ts +324 -0
- package/src/__tests__/checker.test.ts +14 -7
- package/src/__tests__/clarification-resolver.test.ts +44 -24
- package/src/__tests__/commit-message-enrichment-service.test.ts +9 -4
- package/src/__tests__/computer-use-session-working-dir.test.ts +8 -0
- package/src/__tests__/config-schema.test.ts +14 -8
- package/src/__tests__/context-window-manager.test.ts +30 -2
- package/src/__tests__/contradiction-checker.test.ts +20 -5
- package/src/__tests__/credential-security-invariants.test.ts +7 -2
- package/src/__tests__/daemon-lifecycle.test.ts +13 -12
- package/src/__tests__/db-migration-rollback.test.ts +752 -0
- package/src/__tests__/dictation-mode-detection.test.ts +63 -0
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +2 -0
- package/src/__tests__/entity-search.test.ts +615 -0
- package/src/__tests__/fuzzy-match-property.test.ts +5 -5
- package/src/__tests__/guardian-action-store.test.ts +123 -0
- package/src/__tests__/guardian-action-sweep.test.ts +277 -0
- package/src/__tests__/guardian-dispatch.test.ts +389 -0
- package/src/__tests__/guardian-question-copy.test.ts +47 -0
- package/src/__tests__/handlers-telegram-config.test.ts +4 -2
- package/src/__tests__/handlers-twilio-config.test.ts +533 -0
- package/src/__tests__/intent-routing.test.ts +2 -0
- package/src/__tests__/ipc-snapshot.test.ts +291 -1
- package/src/__tests__/memory-upsert-concurrency.test.ts +828 -0
- package/src/__tests__/messaging-send-tool.test.ts +65 -0
- package/src/__tests__/model-intents.test.ts +96 -0
- package/src/__tests__/no-direct-anthropic-sdk-imports.test.ts +42 -0
- package/src/__tests__/oauth2-gateway-transport.test.ts +130 -0
- package/src/__tests__/onboarding-starter-tasks.test.ts +2 -0
- package/src/__tests__/provider-commit-message-generator.test.ts +89 -13
- package/src/__tests__/provider-error-scenarios.test.ts +621 -0
- package/src/__tests__/provider-fail-open-selection.test.ts +119 -0
- package/src/__tests__/qdrant-manager.test.ts +27 -20
- package/src/__tests__/relay-server.test.ts +779 -40
- package/src/__tests__/run-orchestrator-assistant-events.test.ts +6 -0
- package/src/__tests__/run-orchestrator.test.ts +42 -4
- package/src/__tests__/runtime-runs-http.test.ts +17 -1
- package/src/__tests__/runtime-runs.test.ts +16 -0
- package/src/__tests__/schedule-store.test.ts +18 -4
- package/src/__tests__/scheduler-recurrence.test.ts +13 -4
- package/src/__tests__/session-abort-tool-results.test.ts +6 -0
- package/src/__tests__/session-agent-loop.test.ts +857 -0
- package/src/__tests__/session-conflict-gate.test.ts +6 -0
- package/src/__tests__/session-pre-run-repair.test.ts +6 -0
- package/src/__tests__/session-profile-injection.test.ts +6 -0
- package/src/__tests__/session-provider-retry-repair.test.ts +6 -0
- package/src/__tests__/session-queue.test.ts +6 -0
- package/src/__tests__/session-runtime-assembly.test.ts +321 -13
- package/src/__tests__/session-slash-known.test.ts +6 -0
- package/src/__tests__/session-slash-queue.test.ts +6 -0
- package/src/__tests__/session-slash-unknown.test.ts +6 -0
- package/src/__tests__/session-surfaces-task-progress.test.ts +2 -0
- package/src/__tests__/session-tool-setup-app-refresh.test.ts +1 -0
- package/src/__tests__/session-tool-setup-memory-scope.test.ts +1 -0
- package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +1 -0
- package/src/__tests__/session-workspace-injection.test.ts +6 -0
- package/src/__tests__/session-workspace-tool-tracking.test.ts +6 -0
- package/src/__tests__/skills.test.ts +2 -0
- package/src/__tests__/sms-messaging-provider.test.ts +126 -0
- package/src/__tests__/starter-task-flow.test.ts +2 -0
- package/src/__tests__/swarm-dag-pathological.test.ts +535 -0
- package/src/__tests__/system-prompt.test.ts +2 -0
- package/src/__tests__/task-management-tools.test.ts +2 -2
- package/src/__tests__/task-runner.test.ts +14 -4
- package/src/__tests__/terminal-tools.test.ts +25 -19
- package/src/__tests__/tool-execution-abort-cleanup.test.ts +545 -0
- package/src/__tests__/tool-executor-shell-integration.test.ts +11 -11
- package/src/__tests__/tool-executor.test.ts +23 -24
- package/src/__tests__/trust-store.test.ts +3 -3
- package/src/__tests__/twilio-rest.test.ts +29 -0
- package/src/__tests__/twilio-routes-elevenlabs.test.ts +3 -0
- package/src/__tests__/twilio-routes-twiml.test.ts +11 -0
- package/src/__tests__/twilio-routes.test.ts +167 -11
- package/src/__tests__/twitter-cli-error-shaping.test.ts +2 -2
- package/src/__tests__/user-reference.test.ts +2 -0
- package/src/__tests__/voice-quality.test.ts +222 -0
- package/src/__tests__/web-search.test.ts +46 -30
- package/src/__tests__/work-item-output.test.ts +110 -0
- package/src/agent/loop.ts +1 -1
- package/src/agent-heartbeat/agent-heartbeat-service.ts +2 -10
- package/src/amazon/client.ts +1418 -0
- package/src/amazon/request-extractor.ts +135 -0
- package/src/amazon/session.ts +109 -0
- package/src/autonomy/autonomy-store.ts +5 -5
- package/src/browser-extension-relay/client.ts +124 -0
- package/src/browser-extension-relay/protocol.ts +63 -0
- package/src/browser-extension-relay/server.ts +177 -0
- package/src/bundler/app-bundler.ts +3 -3
- package/src/bundler/bundle-signer.ts +1 -1
- package/src/bundler/signature-verifier.ts +1 -1
- package/src/calls/call-conversation-messages.ts +33 -0
- package/src/calls/call-domain.ts +114 -10
- package/src/calls/call-orchestrator.ts +268 -59
- package/src/calls/call-pointer-messages.ts +53 -0
- package/src/calls/call-recovery.ts +3 -8
- package/src/calls/call-store.ts +69 -87
- package/src/calls/elevenlabs-config.ts +3 -2
- package/src/calls/guardian-action-sweep.ts +105 -0
- package/src/calls/guardian-dispatch.ts +203 -0
- package/src/calls/guardian-question-copy.ts +133 -0
- package/src/calls/relay-server.ts +466 -8
- package/src/calls/speaker-identification.ts +1 -1
- package/src/calls/twilio-config.ts +22 -14
- package/src/calls/twilio-provider.ts +6 -4
- package/src/calls/twilio-rest.ts +308 -7
- package/src/calls/twilio-routes.ts +65 -12
- package/src/calls/types.ts +3 -1
- package/src/channels/types.ts +25 -0
- package/src/cli/amazon.ts +815 -0
- package/src/cli/config-commands.ts +2 -2
- package/src/cli/core-commands.ts +4 -3
- package/src/cli/influencer.ts +244 -0
- package/src/cli/map.ts +89 -6
- package/src/cli.ts +1 -1
- package/src/config/agent-schema.ts +171 -0
- package/src/config/bundled-skills/amazon/SKILL.md +127 -0
- package/src/config/bundled-skills/amazon/icon.svg +13 -0
- package/src/config/bundled-skills/api-mapping/SKILL.md +78 -0
- package/src/config/bundled-skills/browser/SKILL.md +1 -0
- package/src/config/bundled-skills/browser/TOOLS.json +17 -0
- package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +25 -0
- package/src/config/bundled-skills/doordash/SKILL.md +51 -51
- package/src/config/bundled-skills/email-setup/SKILL.md +14 -5
- package/src/config/bundled-skills/google-oauth-setup/SKILL.md +183 -0
- package/src/config/bundled-skills/influencer/SKILL.md +144 -0
- package/src/config/bundled-skills/knowledge-graph/SKILL.md +15 -0
- package/src/config/bundled-skills/knowledge-graph/TOOLS.json +56 -0
- package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +185 -0
- package/src/config/bundled-skills/macos-automation/icon.svg +12 -0
- package/src/config/bundled-skills/media-processing/SKILL.md +176 -0
- package/src/config/bundled-skills/media-processing/TOOLS.json +230 -0
- package/src/config/bundled-skills/media-processing/__tests__/concurrency-pool.test.ts +77 -0
- package/src/config/bundled-skills/media-processing/__tests__/cost-tracker.test.ts +69 -0
- package/src/config/bundled-skills/media-processing/__tests__/preprocess.test.ts +303 -0
- package/src/config/bundled-skills/media-processing/services/concurrency-pool.ts +55 -0
- package/src/config/bundled-skills/media-processing/services/cost-tracker.ts +86 -0
- package/src/config/bundled-skills/media-processing/services/gemini-map.ts +339 -0
- package/src/config/bundled-skills/media-processing/services/preprocess.ts +551 -0
- package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +259 -0
- package/src/config/bundled-skills/media-processing/services/reduce.ts +197 -0
- package/src/config/bundled-skills/media-processing/tools/analyze-keyframes.ts +136 -0
- package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +59 -0
- package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +195 -0
- package/src/config/bundled-skills/media-processing/tools/ingest-media.ts +197 -0
- package/src/config/bundled-skills/media-processing/tools/media-diagnostics.ts +143 -0
- package/src/config/bundled-skills/media-processing/tools/media-status.ts +75 -0
- package/src/config/bundled-skills/media-processing/tools/query-media-events.ts +65 -0
- package/src/config/bundled-skills/messaging/SKILL.md +33 -8
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +4 -7
- package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +2 -1
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -1
- package/src/config/bundled-skills/phone-calls/SKILL.md +88 -23
- package/src/config/bundled-skills/twitter/SKILL.md +19 -3
- package/src/config/bundled-skills/twitter/icon.svg +14 -0
- package/src/config/bundled-tool-registry.ts +310 -0
- package/src/config/calls-schema.ts +181 -0
- package/src/config/core-schema.ts +309 -0
- package/src/config/defaults.ts +28 -3
- package/src/config/env-registry.ts +162 -0
- package/src/config/env.ts +175 -0
- package/src/config/loader.ts +6 -6
- package/src/config/memory-schema.ts +528 -0
- package/src/config/sandbox-schema.ts +55 -0
- package/src/config/schema.ts +158 -1133
- package/src/config/skill-state.ts +1 -1
- package/src/config/skills-schema.ts +32 -0
- package/src/config/skills.ts +35 -24
- package/src/config/system-prompt.ts +131 -56
- package/src/config/templates/IDENTITY.md +2 -2
- package/src/config/templates/SOUL.md +1 -1
- package/src/config/types.ts +1 -0
- package/src/config/user-reference.ts +4 -9
- package/src/config/vellum-skills/catalog.json +6 -7
- package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +5 -1
- package/src/config/vellum-skills/slack-oauth-setup/SKILL.md +4 -3
- package/src/config/vellum-skills/sms-setup/SKILL.md +216 -0
- package/src/config/vellum-skills/twilio-setup/SKILL.md +40 -8
- package/src/context/window-manager.ts +27 -7
- package/src/daemon/approval-generators.ts +186 -0
- package/src/daemon/approved-devices-store.ts +140 -0
- package/src/daemon/assistant-attachments.ts +1 -1
- package/src/daemon/classifier.ts +35 -32
- package/src/daemon/config-watcher.ts +1 -1
- package/src/daemon/daemon-control.ts +217 -0
- package/src/daemon/handlers/apps.ts +2 -3
- package/src/daemon/handlers/config-channels.ts +158 -0
- package/src/daemon/handlers/config-inbox.ts +540 -0
- package/src/daemon/handlers/config-ingress.ts +231 -0
- package/src/daemon/handlers/config-integrations.ts +258 -0
- package/src/daemon/handlers/config-model.ts +143 -0
- package/src/daemon/handlers/config-parental.ts +163 -0
- package/src/daemon/handlers/config-scheduling.ts +172 -0
- package/src/daemon/handlers/config-slack.ts +92 -0
- package/src/daemon/handlers/config-telegram.ts +301 -0
- package/src/daemon/handlers/config-tools.ts +177 -0
- package/src/daemon/handlers/config-trust.ts +104 -0
- package/src/daemon/handlers/config-twilio.ts +1080 -0
- package/src/daemon/handlers/config.ts +53 -1689
- package/src/daemon/handlers/diagnostics.ts +1 -1
- package/src/daemon/handlers/dictation.ts +180 -0
- package/src/daemon/handlers/documents.ts +18 -32
- package/src/daemon/handlers/identity.ts +14 -23
- package/src/daemon/handlers/index.ts +11 -0
- package/src/daemon/handlers/misc.ts +3 -5
- package/src/daemon/handlers/pairing.ts +98 -0
- package/src/daemon/handlers/sessions.ts +56 -5
- package/src/daemon/handlers/shared.ts +6 -1
- package/src/daemon/handlers/skills.ts +1 -1
- package/src/daemon/handlers/twitter-auth.ts +2 -0
- package/src/daemon/handlers/work-items.ts +17 -9
- package/src/daemon/handlers/workspace-files.ts +4 -3
- package/src/daemon/install-cli-launchers.ts +113 -0
- package/src/daemon/ipc-contract/apps.ts +356 -0
- package/src/daemon/ipc-contract/browser.ts +74 -0
- package/src/daemon/ipc-contract/computer-use.ts +151 -0
- package/src/daemon/ipc-contract/diagnostics.ts +56 -0
- package/src/daemon/ipc-contract/documents.ts +74 -0
- package/src/daemon/ipc-contract/inbox.ts +209 -0
- package/src/daemon/ipc-contract/integrations.ts +284 -0
- package/src/daemon/ipc-contract/memory.ts +48 -0
- package/src/daemon/ipc-contract/messages.ts +211 -0
- package/src/daemon/ipc-contract/pairing.ts +45 -0
- package/src/daemon/ipc-contract/parental-control.ts +95 -0
- package/src/daemon/ipc-contract/schedules.ts +97 -0
- package/src/daemon/ipc-contract/sessions.ts +315 -0
- package/src/daemon/ipc-contract/shared.ts +42 -0
- package/src/daemon/ipc-contract/skills.ts +120 -0
- package/src/daemon/ipc-contract/subagents.ts +58 -0
- package/src/daemon/ipc-contract/surfaces.ts +250 -0
- package/src/daemon/ipc-contract/trust.ts +60 -0
- package/src/daemon/ipc-contract/work-items.ts +225 -0
- package/src/daemon/ipc-contract/workspace.ts +113 -0
- package/src/daemon/ipc-contract-inventory.json +70 -0
- package/src/daemon/ipc-contract-inventory.ts +55 -29
- package/src/daemon/ipc-contract.ts +229 -2426
- package/src/daemon/ipc-protocol.ts +1 -1
- package/src/daemon/ipc-validate.ts +7 -0
- package/src/daemon/lifecycle.ts +97 -377
- package/src/daemon/pairing-store.ts +177 -0
- package/src/daemon/providers-setup.ts +43 -0
- package/src/daemon/ride-shotgun-handler.ts +68 -3
- package/src/daemon/server.ts +66 -46
- package/src/daemon/session-agent-loop-handlers.ts +421 -0
- package/src/daemon/session-agent-loop.ts +117 -275
- package/src/daemon/session-dynamic-profile.ts +1 -1
- package/src/daemon/session-history.ts +1 -1
- package/src/daemon/session-media-retry.ts +1 -1
- package/src/daemon/session-messaging.ts +37 -2
- package/src/daemon/session-notifiers.ts +5 -25
- package/src/daemon/session-process.ts +99 -59
- package/src/daemon/session-queue-manager.ts +96 -4
- package/src/daemon/session-runtime-assembly.ts +199 -10
- package/src/daemon/session-surfaces.ts +19 -4
- package/src/daemon/session-tool-setup.ts +30 -30
- package/src/daemon/session-workspace.ts +1 -1
- package/src/daemon/session.ts +35 -2
- package/src/daemon/shutdown-handlers.ts +122 -0
- package/src/daemon/trace-emitter.ts +1 -1
- package/src/daemon/watch-handler.ts +36 -33
- package/src/doordash/cart-queries.ts +787 -0
- package/src/doordash/client.ts +144 -127
- package/src/doordash/order-queries.ts +85 -0
- package/src/doordash/queries.ts +10 -1308
- package/src/doordash/search-queries.ts +203 -0
- package/src/doordash/session.ts +3 -2
- package/src/doordash/store-queries.ts +246 -0
- package/src/doordash/types.ts +367 -0
- package/src/email/providers/agentmail.ts +2 -1
- package/src/email/providers/index.ts +3 -2
- package/src/email/service.ts +3 -2
- package/src/errors.ts +43 -0
- package/src/home-base/prebuilt/seed.ts +1 -1
- package/src/hooks/cli.ts +6 -5
- package/src/hooks/config.ts +6 -8
- package/src/hooks/discovery.ts +6 -5
- package/src/hooks/manager.ts +4 -3
- package/src/hooks/runner.ts +2 -2
- package/src/hooks/templates.ts +5 -5
- package/src/inbound/public-ingress-urls.ts +6 -4
- package/src/index.ts +4 -2
- package/src/influencer/client.ts +1104 -0
- package/src/instrument.ts +4 -3
- package/src/logfire.ts +4 -3
- package/src/memory/admin.ts +25 -35
- package/src/memory/attachments-store.ts +4 -7
- package/src/memory/channel-delivery-store.ts +30 -1
- package/src/memory/channel-guardian-store.ts +202 -2
- package/src/memory/clarification-resolver.ts +37 -33
- package/src/memory/conflict-store.ts +67 -61
- package/src/memory/contradiction-checker.ts +141 -117
- package/src/memory/conversation-store.ts +335 -51
- package/src/memory/db-connection.ts +27 -4
- package/src/memory/db-init.ts +265 -4
- package/src/memory/db.ts +14 -1
- package/src/memory/embedding-backend.ts +27 -5
- package/src/memory/embedding-ollama.ts +2 -1
- package/src/memory/entity-extractor.ts +38 -35
- package/src/memory/guardian-action-store.ts +430 -0
- package/src/memory/inbox-escalation-projection.ts +59 -0
- package/src/memory/inbox-thread-store.ts +218 -0
- package/src/memory/ingress-invite-store.ts +338 -0
- package/src/memory/ingress-member-store.ts +350 -0
- package/src/memory/items-extractor.ts +91 -97
- package/src/memory/job-handlers/index-maintenance.ts +3 -3
- package/src/memory/job-handlers/media-processing.ts +69 -0
- package/src/memory/job-handlers/summarization.ts +32 -26
- package/src/memory/job-utils.ts +3 -10
- package/src/memory/jobs-store.ts +8 -10
- package/src/memory/jobs-worker.ts +55 -36
- package/src/memory/media-store.ts +759 -0
- package/src/memory/migrations/001-job-deferrals.ts +45 -0
- package/src/memory/migrations/002-tool-invocations-fk.ts +43 -0
- package/src/memory/migrations/003-memory-fts-backfill.ts +24 -0
- package/src/memory/migrations/004-entity-relation-dedup.ts +87 -0
- package/src/memory/migrations/005-fingerprint-scope-unique.ts +80 -0
- package/src/memory/migrations/006-scope-salted-fingerprints.ts +62 -0
- package/src/memory/migrations/007-assistant-id-to-self.ts +254 -0
- package/src/memory/migrations/008-remove-assistant-id-columns.ts +208 -0
- package/src/memory/migrations/009-llm-usage-events-drop-assistant-id.ts +83 -0
- package/src/memory/migrations/010-ext-conv-bindings-channel-chat-unique.ts +56 -0
- package/src/memory/migrations/011-call-sessions-provider-sid-dedup.ts +63 -0
- package/src/memory/migrations/012-call-sessions-add-initiated-from.ts +19 -0
- package/src/memory/migrations/013-guardian-action-tables.ts +68 -0
- package/src/memory/migrations/014-backfill-inbox-thread-state.ts +76 -0
- package/src/memory/migrations/015-drop-active-search-index.ts +27 -0
- package/src/memory/migrations/016-memory-segments-indexes.ts +11 -0
- package/src/memory/migrations/017-memory-items-indexes.ts +10 -0
- package/src/memory/migrations/018-remaining-table-indexes.ts +13 -0
- package/src/memory/migrations/index.ts +24 -0
- package/src/memory/migrations/registry.ts +79 -0
- package/src/memory/migrations/validate-migration-state.ts +69 -0
- package/src/memory/qdrant-manager.ts +49 -8
- package/src/memory/query-builder.ts +1 -1
- package/src/memory/raw-query.ts +119 -0
- package/src/memory/recall-cache.ts +4 -1
- package/src/memory/retriever.ts +165 -47
- package/src/memory/schema-migration.ts +25 -984
- package/src/memory/schema.ts +228 -7
- package/src/memory/search/entity.ts +205 -31
- package/src/memory/search/lexical.ts +81 -52
- package/src/memory/search/ranking.ts +27 -23
- package/src/memory/search/semantic.ts +157 -19
- package/src/memory/search/types.ts +24 -0
- package/src/memory/shared-app-links-store.ts +4 -5
- package/src/memory/validation.ts +19 -0
- package/src/messaging/draft-store.ts +5 -6
- package/src/messaging/provider-types.ts +2 -0
- package/src/messaging/providers/sms/adapter.ts +201 -0
- package/src/messaging/providers/sms/client.ts +93 -0
- package/src/messaging/providers/sms/types.ts +7 -0
- package/src/messaging/providers/telegram-bot/adapter.ts +2 -5
- package/src/messaging/providers/whatsapp/adapter.ts +136 -0
- package/src/messaging/providers/whatsapp/client.ts +67 -0
- package/src/messaging/style-analyzer.ts +5 -4
- package/src/messaging/thread-summarizer.ts +61 -69
- package/src/messaging/triage-engine.ts +62 -71
- package/src/migrations/config-merge.ts +53 -0
- package/src/migrations/data-layout.ts +68 -0
- package/src/migrations/data-merge.ts +33 -0
- package/src/migrations/hooks-merge.ts +90 -0
- package/src/migrations/index.ts +6 -0
- package/src/migrations/log.ts +23 -0
- package/src/migrations/skills-merge.ts +33 -0
- package/src/migrations/workspace-layout.ts +79 -0
- package/src/permissions/checker.ts +133 -11
- package/src/permissions/prompter.ts +14 -0
- package/src/permissions/shell-identity.ts +31 -1
- package/src/permissions/trust-store.ts +21 -1
- package/src/providers/anthropic/client.ts +4 -4
- package/src/providers/failover.ts +2 -2
- package/src/providers/model-intents.ts +70 -0
- package/src/providers/ollama/client.ts +2 -1
- package/src/providers/provider-send-message.ts +176 -0
- package/src/providers/registry.ts +71 -30
- package/src/providers/retry.ts +35 -1
- package/src/providers/types.ts +12 -1
- package/src/runtime/approval-conversation-turn.ts +97 -0
- package/src/runtime/approval-message-composer.ts +253 -0
- package/src/runtime/channel-approval-parser.ts +36 -2
- package/src/runtime/channel-approvals.ts +11 -24
- package/src/runtime/channel-guardian-service.ts +88 -21
- package/src/runtime/channel-readiness-service.ts +418 -0
- package/src/runtime/channel-readiness-types.ts +35 -0
- package/src/runtime/channel-retry-sweep.ts +184 -0
- package/src/runtime/guardian-context-resolver.ts +108 -0
- package/src/runtime/http-server.ts +275 -717
- package/src/runtime/http-types.ts +59 -3
- package/src/runtime/middleware/auth.ts +116 -0
- package/src/runtime/middleware/error-handler.ts +33 -0
- package/src/runtime/middleware/twilio-validation.ts +127 -0
- package/src/runtime/routes/app-routes.ts +1 -1
- package/src/runtime/routes/call-routes.ts +51 -7
- package/src/runtime/routes/channel-delivery-routes.ts +170 -0
- package/src/runtime/routes/channel-guardian-routes.ts +1191 -0
- package/src/runtime/routes/channel-inbound-routes.ts +1152 -0
- package/src/runtime/routes/channel-route-shared.ts +144 -0
- package/src/runtime/routes/channel-routes.ts +32 -1588
- package/src/runtime/routes/conversation-routes.ts +50 -7
- package/src/runtime/routes/events-routes.ts +2 -2
- package/src/runtime/routes/identity-routes.ts +126 -0
- package/src/runtime/routes/pairing-routes.ts +143 -0
- package/src/runtime/routes/run-routes.ts +15 -1
- package/src/runtime/run-orchestrator.ts +86 -35
- package/src/schedule/schedule-store.ts +36 -32
- package/src/schedule/scheduler.ts +3 -3
- package/src/security/encrypted-store.ts +5 -7
- package/src/security/oauth2.ts +45 -15
- package/src/security/parental-control-store.ts +183 -0
- package/src/security/secret-allowlist.ts +4 -3
- package/src/security/secret-scanner.ts +5 -5
- package/src/security/secure-keys.ts +1 -1
- package/src/security/token-manager.ts +3 -2
- package/src/services/vercel-deploy.ts +6 -2
- package/src/skills/tool-manifest.ts +3 -3
- package/src/skills/vellum-catalog-remote.ts +75 -16
- package/src/slack/slack-webhook.ts +2 -1
- package/src/swarm/orchestrator.ts +92 -1
- package/src/swarm/router-planner.ts +6 -9
- package/src/swarm/worker-prompts.ts +9 -12
- package/src/tasks/task-compiler.ts +19 -28
- package/src/tasks/task-runner.ts +1 -1
- package/src/tools/assets/materialize.ts +2 -2
- package/src/tools/assets/search.ts +15 -14
- package/src/tools/browser/__tests__/auth-detector.test.ts +1 -0
- package/src/tools/browser/auto-navigate.ts +1 -0
- package/src/tools/browser/browser-execution.ts +10 -1
- package/src/tools/browser/browser-manager.ts +119 -4
- package/src/tools/browser/network-recorder.ts +5 -0
- package/src/tools/calls/call-start.ts +1 -0
- package/src/tools/credentials/broker.ts +11 -2
- package/src/tools/credentials/metadata-store.ts +18 -14
- package/src/tools/credentials/post-connect-hooks.ts +61 -0
- package/src/tools/credentials/vault.ts +49 -23
- package/src/tools/execution-target.ts +11 -1
- package/src/tools/executor.ts +68 -9
- package/src/tools/host-terminal/cli-discover.ts +1 -1
- package/src/tools/network/script-proxy/http-forwarder.ts +1 -1
- package/src/tools/network/script-proxy/mitm-handler.ts +1 -1
- package/src/tools/network/script-proxy/server.ts +1 -1
- package/src/tools/network/script-proxy/session-manager.ts +6 -5
- package/src/tools/network/web-fetch.ts +18 -2
- package/src/tools/network/web-search.ts +8 -4
- package/src/tools/reminder/reminder-store.ts +14 -15
- package/src/tools/schedule/create.ts +1 -0
- package/src/tools/schedule/list.ts +2 -1
- package/src/tools/shared/filesystem/file-ops-service.ts +5 -7
- package/src/tools/skills/skill-script-runner.ts +24 -9
- package/src/tools/skills/skill-tool-factory.ts +1 -0
- package/src/tools/tasks/work-item-enqueue.ts +2 -2
- package/src/tools/terminal/evaluate-typescript.ts +21 -12
- package/src/tools/terminal/parser.ts +50 -0
- package/src/tools/types.ts +2 -0
- package/src/tools/watcher/delete.ts +6 -0
- package/src/tools/weather/service.ts +1 -1
- package/src/twitter/client.ts +190 -24
- package/src/twitter/router.ts +1 -1
- package/src/twitter/session.ts +4 -3
- package/src/util/clipboard.ts +1 -1
- package/src/util/errors.ts +65 -8
- package/src/util/fs.ts +40 -0
- package/src/util/json.ts +10 -0
- package/src/util/log-redact.ts +189 -0
- package/src/util/logger.ts +19 -17
- package/src/util/object.ts +3 -0
- package/src/util/platform.ts +105 -363
- package/src/util/pricing.ts +1 -1
- package/src/util/promise-guard.ts +1 -1
- package/src/util/retry.ts +19 -0
- package/src/util/row-mapper.ts +79 -0
- package/src/util/silently.ts +21 -0
- package/src/watcher/engine.ts +5 -1
- package/src/watcher/provider-types.ts +20 -0
- package/src/watcher/providers/github.ts +156 -0
- package/src/watcher/providers/gmail.ts +1 -0
- package/src/watcher/providers/google-calendar.ts +1 -0
- package/src/watcher/providers/linear.ts +460 -0
- package/src/watcher/providers/slack.ts +1 -0
- package/src/work-items/work-item-runner.ts +1 -1
- package/src/workspace/git-service.ts +1 -1
- package/src/workspace/provider-commit-message-generator.ts +51 -22
- package/src/__tests__/call-bridge.test.ts +0 -517
- package/src/__tests__/session-process-bridge.test.ts +0 -244
- package/src/calls/call-bridge.ts +0 -168
- package/src/config/vellum-skills/google-oauth-setup/SKILL.md +0 -199
|
@@ -0,0 +1,621 @@
|
|
|
1
|
+
import { describe, test, expect, mock } from 'bun:test';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
const retryModulePath = resolve(import.meta.dir, '../util/retry.ts');
|
|
5
|
+
|
|
6
|
+
mock.module('../util/logger.js', () => ({
|
|
7
|
+
getLogger: () =>
|
|
8
|
+
new Proxy({} as Record<string, unknown>, { get: () => () => {} }),
|
|
9
|
+
isDebug: () => false,
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
// Only mock sleep so retries complete instantly; keep real retry logic
|
|
13
|
+
mock.module('../util/retry.js', async () => {
|
|
14
|
+
const real = await import(retryModulePath);
|
|
15
|
+
return {
|
|
16
|
+
...real,
|
|
17
|
+
sleep: () => Promise.resolve(),
|
|
18
|
+
};
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
import { RetryProvider } from '../providers/retry.js';
|
|
22
|
+
import { FailoverProvider } from '../providers/failover.js';
|
|
23
|
+
import { createStreamTimeout } from '../providers/stream-timeout.js';
|
|
24
|
+
import { ProviderError } from '../util/errors.js';
|
|
25
|
+
import { DEFAULT_MAX_RETRIES } from '../util/retry.js';
|
|
26
|
+
import type {
|
|
27
|
+
Provider,
|
|
28
|
+
ProviderResponse,
|
|
29
|
+
Message,
|
|
30
|
+
ProviderEvent,
|
|
31
|
+
} from '../providers/types.js';
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Helpers
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
const MESSAGES: Message[] = [
|
|
38
|
+
{ role: 'user', content: [{ type: 'text', text: 'Hello' }] },
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
function successResponse(overrides?: Partial<ProviderResponse>): ProviderResponse {
|
|
42
|
+
return {
|
|
43
|
+
content: [{ type: 'text', text: 'ok' }],
|
|
44
|
+
model: 'test-model',
|
|
45
|
+
usage: { inputTokens: 10, outputTokens: 5 },
|
|
46
|
+
stopReason: 'end_turn',
|
|
47
|
+
...overrides,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function makeProvider(name = 'mock'): Provider & { calls: number } {
|
|
52
|
+
const p = {
|
|
53
|
+
name,
|
|
54
|
+
calls: 0,
|
|
55
|
+
async sendMessage(): Promise<ProviderResponse> {
|
|
56
|
+
p.calls++;
|
|
57
|
+
return successResponse();
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
return p;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Provider that fails N times then succeeds. */
|
|
64
|
+
function makeFlaky(
|
|
65
|
+
failCount: number,
|
|
66
|
+
error: Error,
|
|
67
|
+
name = 'flaky',
|
|
68
|
+
): Provider & { calls: number } {
|
|
69
|
+
const p = {
|
|
70
|
+
name,
|
|
71
|
+
calls: 0,
|
|
72
|
+
async sendMessage(): Promise<ProviderResponse> {
|
|
73
|
+
p.calls++;
|
|
74
|
+
if (p.calls <= failCount) throw error;
|
|
75
|
+
return successResponse();
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
return p;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Provider that always fails. */
|
|
82
|
+
function makeFailing(error: Error, name = 'failing'): Provider & { calls: number } {
|
|
83
|
+
const p = {
|
|
84
|
+
name,
|
|
85
|
+
calls: 0,
|
|
86
|
+
async sendMessage(): Promise<ProviderResponse> {
|
|
87
|
+
p.calls++;
|
|
88
|
+
throw error;
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
return p;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// RetryProvider — rate limit backoff
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
describe('RetryProvider — rate limit backoff', () => {
|
|
99
|
+
test('retries on 429 and succeeds after transient rate limit', async () => {
|
|
100
|
+
const inner = makeFlaky(2, new ProviderError('rate limited', 'test', 429));
|
|
101
|
+
const provider = new RetryProvider(inner);
|
|
102
|
+
|
|
103
|
+
const result = await provider.sendMessage(MESSAGES);
|
|
104
|
+
|
|
105
|
+
expect(result.content[0]).toMatchObject({ type: 'text', text: 'ok' });
|
|
106
|
+
// 2 failures + 1 success = 3 calls
|
|
107
|
+
expect(inner.calls).toBe(3);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('throws after exhausting all retries on persistent 429', async () => {
|
|
111
|
+
const inner = makeFailing(new ProviderError('rate limited', 'test', 429));
|
|
112
|
+
const provider = new RetryProvider(inner);
|
|
113
|
+
|
|
114
|
+
await expect(provider.sendMessage(MESSAGES)).rejects.toThrow('rate limited');
|
|
115
|
+
// 1 initial + DEFAULT_MAX_RETRIES retries
|
|
116
|
+
expect(inner.calls).toBe(DEFAULT_MAX_RETRIES + 1);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('preserves ProviderError properties through retry exhaustion', async () => {
|
|
120
|
+
const inner = makeFailing(new ProviderError('quota exceeded', 'anthropic', 429));
|
|
121
|
+
const provider = new RetryProvider(inner);
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
await provider.sendMessage(MESSAGES);
|
|
125
|
+
expect(true).toBe(false); // should not reach
|
|
126
|
+
} catch (err) {
|
|
127
|
+
expect(err).toBeInstanceOf(ProviderError);
|
|
128
|
+
const pe = err as ProviderError;
|
|
129
|
+
expect(pe.provider).toBe('anthropic');
|
|
130
|
+
expect(pe.statusCode).toBe(429);
|
|
131
|
+
expect(pe.message).toBe('quota exceeded');
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
// RetryProvider — server error retries
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
describe('RetryProvider — server error retries', () => {
|
|
141
|
+
test('retries on 500 Internal Server Error', async () => {
|
|
142
|
+
const inner = makeFlaky(1, new ProviderError('internal error', 'test', 500));
|
|
143
|
+
const provider = new RetryProvider(inner);
|
|
144
|
+
|
|
145
|
+
const result = await provider.sendMessage(MESSAGES);
|
|
146
|
+
expect(result.stopReason).toBe('end_turn');
|
|
147
|
+
expect(inner.calls).toBe(2);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('retries on 502 Bad Gateway', async () => {
|
|
151
|
+
const inner = makeFlaky(1, new ProviderError('bad gateway', 'test', 502));
|
|
152
|
+
const provider = new RetryProvider(inner);
|
|
153
|
+
|
|
154
|
+
const result = await provider.sendMessage(MESSAGES);
|
|
155
|
+
expect(inner.calls).toBe(2);
|
|
156
|
+
expect(result.model).toBe('test-model');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('retries on 503 Service Unavailable', async () => {
|
|
160
|
+
const inner = makeFlaky(1, new ProviderError('unavailable', 'test', 503));
|
|
161
|
+
const provider = new RetryProvider(inner);
|
|
162
|
+
|
|
163
|
+
const result = await provider.sendMessage(MESSAGES);
|
|
164
|
+
expect(inner.calls).toBe(2);
|
|
165
|
+
expect(result.content).toHaveLength(1);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('does not retry on 400 Bad Request', async () => {
|
|
169
|
+
const inner = makeFailing(new ProviderError('bad request', 'test', 400));
|
|
170
|
+
const provider = new RetryProvider(inner);
|
|
171
|
+
|
|
172
|
+
await expect(provider.sendMessage(MESSAGES)).rejects.toThrow('bad request');
|
|
173
|
+
expect(inner.calls).toBe(1);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('does not retry on 401 Unauthorized', async () => {
|
|
177
|
+
const inner = makeFailing(new ProviderError('unauthorized', 'test', 401));
|
|
178
|
+
const provider = new RetryProvider(inner);
|
|
179
|
+
|
|
180
|
+
await expect(provider.sendMessage(MESSAGES)).rejects.toThrow('unauthorized');
|
|
181
|
+
expect(inner.calls).toBe(1);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('does not retry on 403 Forbidden', async () => {
|
|
185
|
+
const inner = makeFailing(new ProviderError('forbidden', 'test', 403));
|
|
186
|
+
const provider = new RetryProvider(inner);
|
|
187
|
+
|
|
188
|
+
await expect(provider.sendMessage(MESSAGES)).rejects.toThrow('forbidden');
|
|
189
|
+
expect(inner.calls).toBe(1);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('does not retry on 422 Unprocessable Entity', async () => {
|
|
193
|
+
const inner = makeFailing(new ProviderError('invalid input', 'test', 422));
|
|
194
|
+
const provider = new RetryProvider(inner);
|
|
195
|
+
|
|
196
|
+
await expect(provider.sendMessage(MESSAGES)).rejects.toThrow('invalid input');
|
|
197
|
+
expect(inner.calls).toBe(1);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
// RetryProvider — network error retries
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
|
|
205
|
+
describe('RetryProvider — network error retries', () => {
|
|
206
|
+
test('retries on ECONNRESET', async () => {
|
|
207
|
+
const err = new Error('connection reset');
|
|
208
|
+
(err as NodeJS.ErrnoException).code = 'ECONNRESET';
|
|
209
|
+
const inner = makeFlaky(1, err);
|
|
210
|
+
const provider = new RetryProvider(inner);
|
|
211
|
+
|
|
212
|
+
const result = await provider.sendMessage(MESSAGES);
|
|
213
|
+
expect(inner.calls).toBe(2);
|
|
214
|
+
expect(result.stopReason).toBe('end_turn');
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test('retries on ECONNREFUSED', async () => {
|
|
218
|
+
const err = new Error('connection refused');
|
|
219
|
+
(err as NodeJS.ErrnoException).code = 'ECONNREFUSED';
|
|
220
|
+
const inner = makeFlaky(1, err);
|
|
221
|
+
const provider = new RetryProvider(inner);
|
|
222
|
+
|
|
223
|
+
const result = await provider.sendMessage(MESSAGES);
|
|
224
|
+
expect(inner.calls).toBe(2);
|
|
225
|
+
expect(result.model).toBe('test-model');
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test('retries on ETIMEDOUT', async () => {
|
|
229
|
+
const err = new Error('timed out');
|
|
230
|
+
(err as NodeJS.ErrnoException).code = 'ETIMEDOUT';
|
|
231
|
+
const inner = makeFlaky(1, err);
|
|
232
|
+
const provider = new RetryProvider(inner);
|
|
233
|
+
|
|
234
|
+
const _result = await provider.sendMessage(MESSAGES);
|
|
235
|
+
expect(inner.calls).toBe(2);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test('retries on ECONNRESET in error cause chain', async () => {
|
|
239
|
+
const cause = new Error('socket hangup');
|
|
240
|
+
(cause as NodeJS.ErrnoException).code = 'ECONNRESET';
|
|
241
|
+
const outer = new Error('fetch failed', { cause });
|
|
242
|
+
const inner = makeFlaky(1, outer);
|
|
243
|
+
const provider = new RetryProvider(inner);
|
|
244
|
+
|
|
245
|
+
const result = await provider.sendMessage(MESSAGES);
|
|
246
|
+
expect(inner.calls).toBe(2);
|
|
247
|
+
expect(result.content[0]).toMatchObject({ type: 'text', text: 'ok' });
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test('does not retry on non-retryable errors', async () => {
|
|
251
|
+
const inner = makeFailing(new Error('unexpected error'));
|
|
252
|
+
const provider = new RetryProvider(inner);
|
|
253
|
+
|
|
254
|
+
await expect(provider.sendMessage(MESSAGES)).rejects.toThrow('unexpected error');
|
|
255
|
+
expect(inner.calls).toBe(1);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test('does not retry on ProviderError without status code (non-network)', async () => {
|
|
259
|
+
// ProviderError without a statusCode and without a retryable network code
|
|
260
|
+
const err = new ProviderError('model not found', 'test');
|
|
261
|
+
const inner = makeFailing(err);
|
|
262
|
+
const provider = new RetryProvider(inner);
|
|
263
|
+
|
|
264
|
+
await expect(provider.sendMessage(MESSAGES)).rejects.toThrow('model not found');
|
|
265
|
+
expect(inner.calls).toBe(1);
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// ---------------------------------------------------------------------------
|
|
270
|
+
// RetryProvider — streaming + options passthrough
|
|
271
|
+
// ---------------------------------------------------------------------------
|
|
272
|
+
|
|
273
|
+
describe('RetryProvider — streaming response handling', () => {
|
|
274
|
+
test('passes onEvent callback through to inner provider', async () => {
|
|
275
|
+
const events: ProviderEvent[] = [];
|
|
276
|
+
const inner: Provider = {
|
|
277
|
+
name: 'streaming-mock',
|
|
278
|
+
async sendMessage(_m, _t, _s, options) {
|
|
279
|
+
options?.onEvent?.({ type: 'text_delta', text: 'hello ' });
|
|
280
|
+
options?.onEvent?.({ type: 'text_delta', text: 'world' });
|
|
281
|
+
return successResponse();
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
const provider = new RetryProvider(inner);
|
|
285
|
+
|
|
286
|
+
await provider.sendMessage(MESSAGES, undefined, undefined, {
|
|
287
|
+
onEvent: (e) => events.push(e),
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
expect(events).toHaveLength(2);
|
|
291
|
+
expect(events[0]).toMatchObject({ type: 'text_delta', text: 'hello ' });
|
|
292
|
+
expect(events[1]).toMatchObject({ type: 'text_delta', text: 'world' });
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test('passes signal through to inner provider', async () => {
|
|
296
|
+
let receivedSignal: AbortSignal | undefined;
|
|
297
|
+
const inner: Provider = {
|
|
298
|
+
name: 'signal-mock',
|
|
299
|
+
async sendMessage(_m, _t, _s, options) {
|
|
300
|
+
receivedSignal = options?.signal;
|
|
301
|
+
return successResponse();
|
|
302
|
+
},
|
|
303
|
+
};
|
|
304
|
+
const provider = new RetryProvider(inner);
|
|
305
|
+
const controller = new AbortController();
|
|
306
|
+
|
|
307
|
+
await provider.sendMessage(MESSAGES, undefined, undefined, {
|
|
308
|
+
signal: controller.signal,
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
expect(receivedSignal).toBe(controller.signal);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test('events accumulate across retries (each attempt delivers events independently)', async () => {
|
|
315
|
+
let callCount = 0;
|
|
316
|
+
const inner: Provider = {
|
|
317
|
+
name: 'retry-stream',
|
|
318
|
+
async sendMessage(_m, _t, _s, options) {
|
|
319
|
+
callCount++;
|
|
320
|
+
options?.onEvent?.({ type: 'text_delta', text: `attempt${callCount} ` });
|
|
321
|
+
if (callCount <= 1) {
|
|
322
|
+
throw new ProviderError('overloaded', 'test', 529);
|
|
323
|
+
}
|
|
324
|
+
return successResponse();
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
const provider = new RetryProvider(inner);
|
|
328
|
+
const events: ProviderEvent[] = [];
|
|
329
|
+
|
|
330
|
+
await provider.sendMessage(MESSAGES, undefined, undefined, {
|
|
331
|
+
onEvent: (e) => events.push(e),
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// Events from both attempts are delivered
|
|
335
|
+
expect(events).toHaveLength(2);
|
|
336
|
+
expect(events[0]).toMatchObject({ type: 'text_delta', text: 'attempt1 ' });
|
|
337
|
+
expect(events[1]).toMatchObject({ type: 'text_delta', text: 'attempt2 ' });
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
// ---------------------------------------------------------------------------
|
|
342
|
+
// FailoverProvider — model unavailability fallback
|
|
343
|
+
// ---------------------------------------------------------------------------
|
|
344
|
+
|
|
345
|
+
describe('FailoverProvider — model unavailability fallback', () => {
|
|
346
|
+
test('falls back to secondary when primary returns 500', async () => {
|
|
347
|
+
const primary = makeFailing(new ProviderError('down', 'primary', 500), 'primary');
|
|
348
|
+
const secondary = makeProvider('secondary');
|
|
349
|
+
const provider = new FailoverProvider([primary, secondary]);
|
|
350
|
+
|
|
351
|
+
const result = await provider.sendMessage(MESSAGES);
|
|
352
|
+
|
|
353
|
+
expect(primary.calls).toBe(1);
|
|
354
|
+
expect(secondary.calls).toBe(1);
|
|
355
|
+
expect(result.stopReason).toBe('end_turn');
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
test('falls back to secondary when primary returns 429', async () => {
|
|
359
|
+
const primary = makeFailing(new ProviderError('rate limited', 'primary', 429), 'primary');
|
|
360
|
+
const secondary = makeProvider('secondary');
|
|
361
|
+
const provider = new FailoverProvider([primary, secondary]);
|
|
362
|
+
|
|
363
|
+
const result = await provider.sendMessage(MESSAGES);
|
|
364
|
+
|
|
365
|
+
expect(primary.calls).toBe(1);
|
|
366
|
+
expect(secondary.calls).toBe(1);
|
|
367
|
+
expect(result.model).toBe('test-model');
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
test('falls back on ECONNREFUSED network error', async () => {
|
|
371
|
+
const err = new Error('connection refused');
|
|
372
|
+
(err as NodeJS.ErrnoException).code = 'ECONNREFUSED';
|
|
373
|
+
const primary = makeFailing(err, 'primary');
|
|
374
|
+
const secondary = makeProvider('secondary');
|
|
375
|
+
const provider = new FailoverProvider([primary, secondary]);
|
|
376
|
+
|
|
377
|
+
const result = await provider.sendMessage(MESSAGES);
|
|
378
|
+
|
|
379
|
+
expect(primary.calls).toBe(1);
|
|
380
|
+
expect(secondary.calls).toBe(1);
|
|
381
|
+
expect(result.content[0]).toMatchObject({ type: 'text', text: 'ok' });
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test('falls back on ProviderError without status code (connection failure)', async () => {
|
|
385
|
+
const primary = makeFailing(new ProviderError('connection failed', 'primary'), 'primary');
|
|
386
|
+
const secondary = makeProvider('secondary');
|
|
387
|
+
const provider = new FailoverProvider([primary, secondary]);
|
|
388
|
+
|
|
389
|
+
const _result = await provider.sendMessage(MESSAGES);
|
|
390
|
+
expect(primary.calls).toBe(1);
|
|
391
|
+
expect(secondary.calls).toBe(1);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
test('does NOT fall back on 400 Bad Request', async () => {
|
|
395
|
+
const primary = makeFailing(new ProviderError('bad request', 'primary', 400), 'primary');
|
|
396
|
+
const secondary = makeProvider('secondary');
|
|
397
|
+
const provider = new FailoverProvider([primary, secondary]);
|
|
398
|
+
|
|
399
|
+
await expect(provider.sendMessage(MESSAGES)).rejects.toThrow('bad request');
|
|
400
|
+
expect(primary.calls).toBe(1);
|
|
401
|
+
expect(secondary.calls).toBe(0);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
test('does NOT fall back on 401 Unauthorized', async () => {
|
|
405
|
+
const primary = makeFailing(new ProviderError('unauthorized', 'primary', 401), 'primary');
|
|
406
|
+
const secondary = makeProvider('secondary');
|
|
407
|
+
const provider = new FailoverProvider([primary, secondary]);
|
|
408
|
+
|
|
409
|
+
await expect(provider.sendMessage(MESSAGES)).rejects.toThrow('unauthorized');
|
|
410
|
+
expect(secondary.calls).toBe(0);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
test('throws last error when all providers fail', async () => {
|
|
414
|
+
const p1 = makeFailing(new ProviderError('p1 down', 'p1', 500), 'p1');
|
|
415
|
+
const p2 = makeFailing(new ProviderError('p2 down', 'p2', 503), 'p2');
|
|
416
|
+
const p3 = makeFailing(new ProviderError('p3 down', 'p3', 502), 'p3');
|
|
417
|
+
const provider = new FailoverProvider([p1, p2, p3]);
|
|
418
|
+
|
|
419
|
+
try {
|
|
420
|
+
await provider.sendMessage(MESSAGES);
|
|
421
|
+
expect(true).toBe(false);
|
|
422
|
+
} catch (err) {
|
|
423
|
+
expect(err).toBeInstanceOf(ProviderError);
|
|
424
|
+
// Last provider's error is thrown
|
|
425
|
+
expect((err as ProviderError).message).toBe('p3 down');
|
|
426
|
+
}
|
|
427
|
+
expect(p1.calls).toBe(1);
|
|
428
|
+
expect(p2.calls).toBe(1);
|
|
429
|
+
expect(p3.calls).toBe(1);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
test('chains through three providers when first two fail', async () => {
|
|
433
|
+
const p1 = makeFailing(new ProviderError('p1 error', 'p1', 500), 'p1');
|
|
434
|
+
const p2 = makeFailing(new ProviderError('p2 error', 'p2', 502), 'p2');
|
|
435
|
+
const p3 = makeProvider('p3');
|
|
436
|
+
const provider = new FailoverProvider([p1, p2, p3]);
|
|
437
|
+
|
|
438
|
+
const result = await provider.sendMessage(MESSAGES);
|
|
439
|
+
|
|
440
|
+
expect(p1.calls).toBe(1);
|
|
441
|
+
expect(p2.calls).toBe(1);
|
|
442
|
+
expect(p3.calls).toBe(1);
|
|
443
|
+
expect(result.stopReason).toBe('end_turn');
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
test('requires at least one provider', () => {
|
|
447
|
+
expect(() => new FailoverProvider([])).toThrow(
|
|
448
|
+
'FailoverProvider requires at least one provider',
|
|
449
|
+
);
|
|
450
|
+
});
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
// ---------------------------------------------------------------------------
|
|
454
|
+
// FailoverProvider — cooldown and recovery
|
|
455
|
+
// ---------------------------------------------------------------------------
|
|
456
|
+
|
|
457
|
+
describe('FailoverProvider — cooldown and recovery', () => {
|
|
458
|
+
test('skips provider in cooldown period', async () => {
|
|
459
|
+
const primary = makeFailing(new ProviderError('down', 'primary', 500), 'primary');
|
|
460
|
+
const secondary = makeProvider('secondary');
|
|
461
|
+
// Use a long cooldown so primary stays unhealthy
|
|
462
|
+
const provider = new FailoverProvider([primary, secondary], 60_000);
|
|
463
|
+
|
|
464
|
+
// First call: primary fails, secondary succeeds
|
|
465
|
+
await provider.sendMessage(MESSAGES);
|
|
466
|
+
expect(primary.calls).toBe(1);
|
|
467
|
+
expect(secondary.calls).toBe(1);
|
|
468
|
+
|
|
469
|
+
// Second call: primary is in cooldown, skipped — goes straight to secondary
|
|
470
|
+
await provider.sendMessage(MESSAGES);
|
|
471
|
+
expect(primary.calls).toBe(1); // not called again
|
|
472
|
+
expect(secondary.calls).toBe(2);
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
test('retries provider after cooldown expires', async () => {
|
|
476
|
+
let primaryCallCount = 0;
|
|
477
|
+
const primary: Provider = {
|
|
478
|
+
name: 'primary',
|
|
479
|
+
async sendMessage() {
|
|
480
|
+
primaryCallCount++;
|
|
481
|
+
if (primaryCallCount === 1) {
|
|
482
|
+
throw new ProviderError('temporarily down', 'primary', 500);
|
|
483
|
+
}
|
|
484
|
+
return successResponse();
|
|
485
|
+
},
|
|
486
|
+
};
|
|
487
|
+
const secondary = makeProvider('secondary');
|
|
488
|
+
// Very short cooldown
|
|
489
|
+
const provider = new FailoverProvider([primary, secondary], 1);
|
|
490
|
+
|
|
491
|
+
// First call: primary fails, marked unhealthy, secondary succeeds
|
|
492
|
+
await provider.sendMessage(MESSAGES);
|
|
493
|
+
expect(primaryCallCount).toBe(1);
|
|
494
|
+
|
|
495
|
+
// Wait for cooldown to expire
|
|
496
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
497
|
+
|
|
498
|
+
// Second call: primary should be retried after cooldown expired
|
|
499
|
+
await provider.sendMessage(MESSAGES);
|
|
500
|
+
expect(primaryCallCount).toBe(2);
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
test('marks provider healthy after successful recovery', async () => {
|
|
504
|
+
let primaryCallCount = 0;
|
|
505
|
+
const primary: Provider = {
|
|
506
|
+
name: 'primary',
|
|
507
|
+
async sendMessage() {
|
|
508
|
+
primaryCallCount++;
|
|
509
|
+
if (primaryCallCount === 1) {
|
|
510
|
+
throw new ProviderError('blip', 'primary', 500);
|
|
511
|
+
}
|
|
512
|
+
return successResponse();
|
|
513
|
+
},
|
|
514
|
+
};
|
|
515
|
+
const secondary = makeProvider('secondary');
|
|
516
|
+
const provider = new FailoverProvider([primary, secondary], 1);
|
|
517
|
+
|
|
518
|
+
// First call: primary fails
|
|
519
|
+
await provider.sendMessage(MESSAGES);
|
|
520
|
+
expect(primaryCallCount).toBe(1);
|
|
521
|
+
|
|
522
|
+
// Wait for cooldown
|
|
523
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
524
|
+
|
|
525
|
+
// Second call: primary recovers
|
|
526
|
+
await provider.sendMessage(MESSAGES);
|
|
527
|
+
expect(primaryCallCount).toBe(2);
|
|
528
|
+
|
|
529
|
+
// Third call: primary is healthy, used directly
|
|
530
|
+
await provider.sendMessage(MESSAGES);
|
|
531
|
+
expect(primaryCallCount).toBe(3);
|
|
532
|
+
expect(secondary.calls).toBe(1); // only called once during initial failover
|
|
533
|
+
});
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
// ---------------------------------------------------------------------------
|
|
537
|
+
// createStreamTimeout — edge cases
|
|
538
|
+
// ---------------------------------------------------------------------------
|
|
539
|
+
|
|
540
|
+
describe('createStreamTimeout — edge cases', () => {
|
|
541
|
+
test('propagates already-aborted external signal immediately', () => {
|
|
542
|
+
const external = new AbortController();
|
|
543
|
+
external.abort(new Error('already cancelled'));
|
|
544
|
+
|
|
545
|
+
const { signal, cleanup } = createStreamTimeout(60_000, external.signal);
|
|
546
|
+
|
|
547
|
+
expect(signal.aborted).toBe(true);
|
|
548
|
+
expect(signal.reason).toBeInstanceOf(Error);
|
|
549
|
+
expect((signal.reason as Error).message).toBe('already cancelled');
|
|
550
|
+
cleanup();
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
test('cleanup prevents timeout from firing', async () => {
|
|
554
|
+
const { signal, cleanup } = createStreamTimeout(50);
|
|
555
|
+
cleanup();
|
|
556
|
+
|
|
557
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
558
|
+
expect(signal.aborted).toBe(false);
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
test('cleanup removes external signal listener', () => {
|
|
562
|
+
const external = new AbortController();
|
|
563
|
+
const { signal, cleanup } = createStreamTimeout(60_000, external.signal);
|
|
564
|
+
|
|
565
|
+
cleanup();
|
|
566
|
+
|
|
567
|
+
// Aborting external after cleanup should NOT propagate
|
|
568
|
+
external.abort(new Error('late abort'));
|
|
569
|
+
expect(signal.aborted).toBe(false);
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
test('timeout error message includes duration', async () => {
|
|
573
|
+
const { signal, cleanup } = createStreamTimeout(100);
|
|
574
|
+
|
|
575
|
+
await new Promise<void>((resolve) => {
|
|
576
|
+
signal.addEventListener('abort', () => resolve(), { once: true });
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
expect(signal.reason).toBeInstanceOf(Error);
|
|
580
|
+
expect((signal.reason as Error).message).toContain('0.1s');
|
|
581
|
+
cleanup();
|
|
582
|
+
});
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
// ---------------------------------------------------------------------------
|
|
586
|
+
// RetryProvider + FailoverProvider — combined scenarios
|
|
587
|
+
// ---------------------------------------------------------------------------
|
|
588
|
+
|
|
589
|
+
describe('RetryProvider + FailoverProvider — combined', () => {
|
|
590
|
+
test('failover wrapping retry: each provider in the chain retries independently', async () => {
|
|
591
|
+
// Primary always fails with 500, secondary succeeds
|
|
592
|
+
const primary = makeFailing(new ProviderError('primary down', 'primary', 500), 'primary');
|
|
593
|
+
const secondary = makeProvider('secondary');
|
|
594
|
+
|
|
595
|
+
// Wrap each in RetryProvider, then combine with FailoverProvider
|
|
596
|
+
const retryPrimary = new RetryProvider(primary);
|
|
597
|
+
const retrySecondary = new RetryProvider(secondary);
|
|
598
|
+
const failover = new FailoverProvider([retryPrimary, retrySecondary]);
|
|
599
|
+
|
|
600
|
+
const result = await failover.sendMessage(MESSAGES);
|
|
601
|
+
expect(result.stopReason).toBe('end_turn');
|
|
602
|
+
// Primary should have been retried MAX_RETRIES + 1 times before failover
|
|
603
|
+
expect(primary.calls).toBe(DEFAULT_MAX_RETRIES + 1);
|
|
604
|
+
expect(secondary.calls).toBe(1);
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
test('single provider: retry exhaustion produces the original error', async () => {
|
|
608
|
+
const inner = makeFailing(new ProviderError('always fail', 'solo', 500));
|
|
609
|
+
const retrying = new RetryProvider(inner);
|
|
610
|
+
|
|
611
|
+
try {
|
|
612
|
+
await retrying.sendMessage(MESSAGES);
|
|
613
|
+
expect(true).toBe(false);
|
|
614
|
+
} catch (err) {
|
|
615
|
+
expect(err).toBeInstanceOf(ProviderError);
|
|
616
|
+
expect((err as ProviderError).message).toBe('always fail');
|
|
617
|
+
expect((err as ProviderError).statusCode).toBe(500);
|
|
618
|
+
}
|
|
619
|
+
expect(inner.calls).toBe(DEFAULT_MAX_RETRIES + 1);
|
|
620
|
+
});
|
|
621
|
+
});
|