@vellumai/assistant 0.8.7 → 0.8.8
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 +20 -4
- package/docker-entrypoint.sh +4 -2
- package/docker-init-apt-root.sh +3 -1
- package/docker-kata-apt-env.sh +3 -1
- package/docker-kata-runtime-family.sh +12 -0
- package/docs/architecture/memory.md +1 -1
- package/docs/plugins.md +75 -79
- package/examples/plugins/echo/README.md +6 -12
- package/examples/plugins/echo/register.ts +0 -41
- package/node_modules/@vellumai/skill-host-contracts/src/server-message.ts +3 -3
- package/openapi.yaml +3381 -348
- package/package.json +1 -1
- package/scripts/generate-openapi.ts +68 -41
- package/src/__tests__/agent-loop-exit-reason.test.ts +34 -39
- package/src/__tests__/agent-loop-provider-error-recording.test.ts +1 -1
- package/src/__tests__/agent-loop.test.ts +37 -87
- package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
- package/src/__tests__/annotate-activity-metadata.test.ts +262 -0
- package/src/__tests__/annotate-risk-options.test.ts +2 -3
- package/src/__tests__/anthropic-provider.test.ts +95 -2
- package/src/__tests__/assistant-event-hub.test.ts +25 -0
- package/src/__tests__/assistant-events-sse-shed.test.ts +8 -0
- package/src/__tests__/{conversation-stream-state.test.ts → assistant-stream-state.test.ts} +252 -91
- package/src/__tests__/auth-fallback-events-store.test.ts +116 -0
- package/src/__tests__/background-workers-disk-pressure.test.ts +6 -0
- package/src/__tests__/btw-routes.test.ts +62 -3
- package/src/__tests__/build-persisted-content.test.ts +184 -0
- package/src/__tests__/catalog-files.test.ts +1 -1
- package/src/__tests__/clawhub-files.test.ts +1 -1
- package/src/__tests__/compaction-pipeline.test.ts +1 -1
- package/src/__tests__/compaction.benchmark.test.ts +0 -30
- package/src/__tests__/config-watcher.test.ts +1 -1
- package/src/__tests__/conversation-abort-tool-results.test.ts +57 -19
- package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +6 -2
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +10 -4
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +313 -1136
- package/src/__tests__/conversation-agent-loop.test.ts +596 -1616
- package/src/__tests__/conversation-analysis-routes.test.ts +6 -0
- package/src/__tests__/conversation-history-web-search.test.ts +11 -1
- package/src/__tests__/conversation-pairing.test.ts +4 -31
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +6 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +26 -5
- package/src/__tests__/conversation-queue.test.ts +2 -0
- package/src/__tests__/conversation-routes-disk-view.test.ts +3 -0
- package/src/__tests__/conversation-routes-slash-commands.test.ts +6 -5
- package/src/__tests__/conversation-runtime-assembly.test.ts +170 -229
- package/src/__tests__/conversation-runtime-workspace.test.ts +3 -24
- package/src/__tests__/conversation-slash-commands.test.ts +8 -42
- package/src/__tests__/conversation-slash-queue.test.ts +6 -1
- package/src/__tests__/conversation-surfaces-action-delivery.test.ts +84 -0
- package/src/__tests__/conversation-sync-tags.test.ts +27 -15
- package/src/__tests__/conversation-title-service.test.ts +135 -2
- package/src/__tests__/conversation-workspace-injection.test.ts +6 -1
- package/src/__tests__/cross-provider-web-search.test.ts +214 -1
- package/src/__tests__/db-schedule-syntax-migration.test.ts +5 -0
- package/src/__tests__/dm-persistence.test.ts +5 -1
- package/src/__tests__/empty-response-hook.test.ts +304 -0
- package/src/__tests__/feature-flag-test-helpers.ts +2 -2
- package/src/__tests__/gemini-image-service.test.ts +13 -0
- package/src/__tests__/helpers/mock-provider.ts +110 -0
- package/src/__tests__/helpers/native-web-search-harness.ts +129 -0
- package/src/__tests__/history-repair-hook.test.ts +1 -0
- package/src/__tests__/identity-intro-cache.test.ts +12 -100
- package/src/__tests__/identity-routes.test.ts +248 -7
- package/src/__tests__/inbound-slack-persistence.test.ts +5 -1
- package/src/__tests__/injector-background-turn.test.ts +2 -8
- package/src/__tests__/injector-chain.test.ts +106 -270
- package/src/__tests__/injector-disk-pressure.test.ts +3 -12
- package/src/__tests__/injector-document-comments.test.ts +2 -2
- package/src/__tests__/injector-pkb-v2-silenced.test.ts +30 -22
- package/src/__tests__/injector-v3-suppression.test.ts +31 -37
- package/src/__tests__/internal-telemetry-routes.test.ts +109 -0
- package/src/__tests__/list-messages-page-latest.test.ts +60 -0
- package/src/__tests__/list-messages-tool-merge.test.ts +20 -0
- package/src/__tests__/llm-usage-store.test.ts +223 -1
- package/src/__tests__/memory-retrieval-hook.test.ts +297 -0
- package/src/__tests__/memory-v2-static-injector.test.ts +103 -35
- package/src/__tests__/native-web-search.test.ts +191 -0
- package/src/__tests__/onboarding-template-contract.test.ts +2 -0
- package/src/__tests__/openai-image-service.test.ts +17 -0
- package/src/__tests__/openai-provider.test.ts +31 -1
- package/src/__tests__/persist-unsendable-image.test.ts +215 -0
- package/src/__tests__/persistence-secret-redaction.test.ts +1 -0
- package/src/__tests__/pipeline-runner.test.ts +29 -39
- package/src/__tests__/pkb-autoinject.test.ts +2 -5
- package/src/__tests__/plugin-bootstrap.test.ts +13 -28
- package/src/__tests__/plugin-registry.test.ts +0 -27
- package/src/__tests__/plugin-types.test.ts +2 -125
- package/src/__tests__/process-message-display-content.test.ts +6 -2
- package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +5 -1
- package/src/__tests__/resolve-trust-class.test.ts +4 -4
- package/src/__tests__/runtime-events-sse-reconnect.test.ts +60 -23
- package/src/__tests__/schedule-routes.test.ts +603 -2
- package/src/__tests__/schedule-store.test.ts +41 -0
- package/src/__tests__/schedule-tools.test.ts +35 -0
- package/src/__tests__/server-history-render.test.ts +314 -1
- package/src/__tests__/skillssh-files.test.ts +1 -1
- package/src/__tests__/system-prompt.test.ts +20 -0
- package/src/__tests__/task-scheduler.test.ts +162 -1
- package/src/__tests__/terminal-tools.test.ts +6 -1
- package/src/__tests__/title-generate-hook.test.ts +319 -0
- package/src/__tests__/tool-error-hook.test.ts +278 -0
- package/src/__tests__/tool-preview-lifecycle.test.ts +468 -5
- package/src/__tests__/tool-result-metadata-plumbing.test.ts +1 -0
- package/src/__tests__/tool-result-truncate-hook.test.ts +127 -0
- package/src/__tests__/tool-result-truncation.test.ts +0 -2
- package/src/__tests__/ui-choice-copy-surfaces.test.ts +254 -0
- package/src/__tests__/ui-work-result-surface.test.ts +159 -0
- package/src/__tests__/usage-routes.test.ts +285 -1
- package/src/__tests__/user-plugin-loader.test.ts +2 -2
- package/src/__tests__/voice-session-bridge.test.ts +6 -3
- package/src/__tests__/web-search-backend-failure.test.ts +166 -0
- package/src/agent/loop.ts +346 -442
- package/src/api/events/assistant-thinking-delta.ts +33 -0
- package/src/api/events/tool-output-chunk.ts +45 -0
- package/src/api/events/tool-use-preview-start.ts +32 -0
- package/src/api/events/trace-event.ts +69 -0
- package/src/api/index.ts +48 -13
- package/src/api/responses/conversation-message.ts +368 -0
- package/src/avatar/__tests__/avatar-store.test.ts +34 -29
- package/src/cli/commands/__tests__/notifications.test.ts +58 -14
- package/src/cli/commands/notifications.ts +112 -60
- package/src/config/assistant-feature-flags.ts +22 -11
- package/src/config/bundled-skills/app-builder/SKILL.md +3 -20
- package/src/config/bundled-skills/app-builder/references/examples/README.md +17 -0
- package/src/config/bundled-skills/app-builder/references/examples/expense-tracker.md +515 -0
- package/src/config/bundled-skills/app-builder/references/examples/focus-timer.md +342 -0
- package/src/config/bundled-skills/app-builder/references/examples/habit-tracker.md +490 -0
- package/src/config/bundled-skills/document-editor/SKILL.md +1 -1
- package/src/config/bundled-skills/messaging/SKILL.md +0 -7
- package/src/config/feature-flag-cache.ts +3 -3
- package/src/config/feature-flag-registry.json +35 -3
- package/src/config/schemas/__tests__/memory-v2.test.ts +1 -0
- package/src/config/schemas/__tests__/memory-v3.test.ts +25 -0
- package/src/config/schemas/llm.ts +1 -0
- package/src/config/schemas/memory-v2.ts +8 -0
- package/src/config/schemas/memory-v3.ts +8 -0
- package/src/config/schemas/platform.ts +8 -0
- package/src/config/seed-inference-profiles.ts +2 -2
- package/src/config/skills.ts +13 -0
- package/src/context/compactor.ts +1 -1
- package/src/context/strip-injections.ts +122 -0
- package/src/context/token-estimator.ts +23 -0
- package/src/context/tool-result-truncation.ts +0 -23
- package/src/context/window-manager.ts +3 -6
- package/src/credential-execution/executable-discovery.ts +16 -0
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +6 -0
- package/src/daemon/__tests__/inference-profile-notification.test.ts +153 -0
- package/src/daemon/__tests__/native-web-search-metadata.test.ts +10 -8
- package/src/daemon/assistant-attachments.ts +1 -1
- package/src/daemon/config-watcher.ts +2 -2
- package/src/daemon/context-overflow-reducer.ts +0 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +605 -153
- package/src/daemon/conversation-agent-loop.ts +281 -760
- package/src/daemon/conversation-history.ts +5 -4
- package/src/daemon/conversation-lifecycle.ts +3 -4
- package/src/daemon/conversation-messaging.ts +7 -6
- package/src/daemon/conversation-process.ts +11 -16
- package/src/daemon/conversation-runtime-assembly.ts +130 -347
- package/src/daemon/conversation-slash.ts +6 -25
- package/src/daemon/conversation-surfaces.ts +222 -4
- package/src/daemon/conversation-tool-setup.ts +2 -29
- package/src/daemon/conversation.ts +32 -14
- package/src/daemon/external-plugins-bootstrap.ts +9 -10
- package/src/daemon/handlers/config-a2a.ts +51 -36
- package/src/daemon/handlers/config-slack-channel.ts +20 -14
- package/src/daemon/handlers/config-telegram.ts +16 -2
- package/src/daemon/handlers/shared.ts +156 -84
- package/src/daemon/handlers/skills.ts +39 -10
- package/src/daemon/lifecycle.ts +4 -0
- package/src/daemon/message-types/apps.ts +1 -29
- package/src/daemon/message-types/messages.ts +9 -57
- package/src/daemon/message-types/skills.ts +2 -0
- package/src/daemon/message-types/surfaces.ts +136 -3
- package/src/daemon/now-scratchpad.ts +21 -0
- package/src/daemon/orphan-reaper.test.ts +210 -0
- package/src/daemon/orphan-reaper.ts +240 -0
- package/src/daemon/persist-unsendable-image.ts +117 -0
- package/src/daemon/process-message.ts +1 -3
- package/src/daemon/trace-emitter.ts +6 -4
- package/src/daemon/trust-context.ts +19 -0
- package/src/daemon/wake-target-adapter.ts +3 -1
- package/src/home/home-greeting-cache.ts +24 -1
- package/src/ipc/gateway-client.test.ts +2 -2
- package/src/ipc/gateway-client.ts +3 -3
- package/src/media/gemini-image-service.ts +15 -0
- package/src/media/openai-image-service.ts +14 -0
- package/src/media/types.ts +34 -0
- package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +56 -0
- package/src/memory/auth-fallback-events-store.ts +94 -0
- package/src/memory/conversation-title-service.ts +65 -41
- package/src/memory/db-init.ts +4 -0
- package/src/memory/graph/__tests__/conversation-graph-memory-registry.test.ts +119 -0
- package/src/memory/graph/conversation-graph-memory.ts +65 -0
- package/src/memory/jobs-store.ts +33 -0
- package/src/memory/jobs-worker.ts +31 -4
- package/src/memory/llm-usage-store.ts +224 -50
- package/src/memory/migrations/222-strip-placeholder-sentinels-from-messages.ts +6 -5
- package/src/memory/migrations/270-schedule-source-conversation.ts +13 -0
- package/src/memory/migrations/271-create-auth-fallback-events.ts +21 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/pkb/autoinject.ts +61 -0
- package/src/memory/pkb/context.ts +50 -0
- package/src/memory/pkb/types.ts +14 -0
- package/src/memory/schedule-attribution-sql.ts +104 -0
- package/src/memory/schema/infrastructure.ts +16 -0
- package/src/memory/usage-grouped-buckets.ts +6 -1
- package/src/memory/v2/__tests__/consolidation-job.test.ts +1 -1
- package/src/memory/v2/consolidation-job.ts +1 -1
- package/src/memory/v3/__tests__/health.test.ts +16 -0
- package/src/memory/v3/__tests__/orchestrate.test.ts +45 -9
- package/src/memory/v3/__tests__/provider-blocks.test.ts +13 -0
- package/src/memory/v3/__tests__/router.test.ts +101 -29
- package/src/memory/v3/__tests__/selector.test.ts +93 -27
- package/src/memory/v3/__tests__/shadow-plugin.test.ts +23 -5
- package/src/memory/v3/health.ts +0 -0
- package/src/memory/v3/llm-retry.ts +32 -0
- package/src/memory/v3/orchestrate.ts +26 -14
- package/src/memory/v3/provider-blocks.ts +15 -5
- package/src/memory/v3/router.ts +48 -42
- package/src/memory/v3/selector.ts +57 -42
- package/src/memory/v3/shadow-plugin.ts +47 -15
- package/src/memory/v3/types.ts +8 -0
- package/src/notifications/conversation-pairing.ts +8 -15
- package/src/notifications/decision-engine.ts +6 -3
- package/src/notifications/home-feed-side-effect.ts +12 -1
- package/src/permissions/prompter.ts +4 -0
- package/src/plugin-api/constants.ts +4 -0
- package/src/plugin-api/index.ts +8 -1
- package/src/plugin-api/types.ts +151 -1
- package/src/plugins/defaults/empty-response/hooks/stop.ts +126 -0
- package/src/plugins/defaults/empty-response/register.ts +8 -13
- package/src/plugins/defaults/index.ts +1 -15
- package/src/plugins/defaults/injectors/register.ts +243 -74
- package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +91 -0
- package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +216 -0
- package/src/plugins/defaults/memory-retrieval/injector-chain.ts +35 -0
- package/src/plugins/defaults/title-generate/hooks/stop.ts +75 -0
- package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +35 -0
- package/src/plugins/defaults/title-generate/package.json +1 -1
- package/src/plugins/defaults/title-generate/register.ts +18 -18
- package/src/plugins/defaults/tool-error/hooks/post-tool-use.ts +118 -0
- package/src/plugins/defaults/tool-error/package.json +1 -1
- package/src/plugins/defaults/tool-error/register.ts +9 -21
- package/src/plugins/defaults/tool-result-truncate/hooks/post-tool-use.ts +32 -0
- package/src/plugins/defaults/tool-result-truncate/register.ts +10 -21
- package/src/plugins/defaults/tool-result-truncate/terminal.ts +37 -18
- package/src/plugins/pipeline.ts +6 -18
- package/src/plugins/registry.ts +8 -25
- package/src/plugins/types.ts +43 -474
- package/src/proactive-artifact/aux-message-injector.ts +3 -3
- package/src/proactive-artifact/job.test.ts +7 -12
- package/src/prompts/__tests__/system-prompt.test.ts +36 -0
- package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +62 -0
- package/src/prompts/templates/BOOTSTRAP.md +2 -2
- package/src/prompts/templates/system-sections.ts +15 -0
- package/src/providers/anthropic/client.ts +37 -29
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +112 -0
- package/src/providers/openai/chat-completions-provider.ts +44 -0
- package/src/providers/openrouter/client.ts +1 -0
- package/src/providers/placeholder-sentinels.ts +35 -0
- package/src/runtime/__tests__/agent-wake.test.ts +5 -1
- package/src/runtime/agent-wake.ts +2 -2
- package/src/runtime/assistant-event-hub.ts +36 -6
- package/src/runtime/{conversation-stream-state.ts → assistant-stream-state.ts} +132 -58
- package/src/runtime/http-router.ts +16 -21
- package/src/runtime/http-types.ts +16 -70
- package/src/runtime/pending-interactions.ts +1 -0
- package/src/runtime/routes/__tests__/consolidation-routes.test.ts +265 -2
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +31 -1
- package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +6 -2
- package/src/runtime/routes/__tests__/tts-routes.test.ts +6 -2
- package/src/runtime/routes/app-management-routes.ts +6 -117
- package/src/runtime/routes/app-routes.ts +13 -15
- package/src/runtime/routes/attachment-routes.ts +26 -15
- package/src/runtime/routes/avatar-routes.ts +26 -0
- package/src/runtime/routes/btw-routes.ts +29 -23
- package/src/runtime/routes/consolidation-routes.ts +120 -20
- package/src/runtime/routes/conversation-query-routes.ts +2 -0
- package/src/runtime/routes/conversation-routes.ts +358 -184
- package/src/runtime/routes/documents-routes.ts +4 -0
- package/src/runtime/routes/domain-routes.ts +51 -37
- package/src/runtime/routes/epoch-millis-range.ts +34 -0
- package/src/runtime/routes/events-routes.ts +28 -34
- package/src/runtime/routes/gateway-log-routes.ts +26 -4
- package/src/runtime/routes/heartbeat-routes.ts +32 -12
- package/src/runtime/routes/identity-intro-cache.ts +11 -34
- package/src/runtime/routes/identity-routes.ts +208 -17
- package/src/runtime/routes/image-generation-routes.ts +40 -2
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/integrations/a2a.ts +12 -10
- package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +16 -0
- package/src/runtime/routes/integrations/slack/channel.ts +4 -0
- package/src/runtime/routes/integrations/slack/share.ts +27 -6
- package/src/runtime/routes/integrations/telegram.ts +6 -0
- package/src/runtime/routes/integrations/twilio.ts +42 -0
- package/src/runtime/routes/internal-telemetry-routes.ts +88 -0
- package/src/runtime/routes/log-export-routes.ts +8 -0
- package/src/runtime/routes/memory-v2-routes.ts +15 -8
- package/src/runtime/routes/memory-v3-routes.ts +50 -28
- package/src/runtime/routes/oauth-apps.ts +66 -12
- package/src/runtime/routes/oauth-providers.ts +44 -5
- package/src/runtime/routes/platform-routes.ts +81 -5
- package/src/runtime/routes/playground/__tests__/force-compact.test.ts +6 -4
- package/src/runtime/routes/playground/force-compact.ts +1 -1
- package/src/runtime/routes/rename-conversation-routes.ts +5 -0
- package/src/runtime/routes/schedule-routes.ts +152 -42
- package/src/runtime/routes/secret-routes.ts +14 -2
- package/src/runtime/routes/skills-routes.ts +43 -14
- package/src/runtime/routes/tool-call-confirmation-enrichment.test.ts +161 -0
- package/src/runtime/routes/tool-call-confirmation-enrichment.ts +107 -0
- package/src/runtime/routes/trust-rules-routes.ts +26 -2
- package/src/runtime/routes/tts-routes.ts +35 -0
- package/src/runtime/routes/types.ts +66 -8
- package/src/runtime/routes/usage-routes.ts +47 -39
- package/src/runtime/routes/webhook-routes.ts +41 -2
- package/src/runtime/routes/workspace-routes.ts +4 -0
- package/src/runtime/services/__tests__/analyze-conversation.test.ts +6 -0
- package/src/runtime/services/analyze-conversation.ts +2 -2
- package/src/schedule/schedule-store.ts +20 -1
- package/src/schedule/schedule-usage-store.ts +83 -0
- package/src/schedule/scheduler.ts +12 -5
- package/src/skills/catalog-files.ts +2 -2
- package/src/skills/catalog-install.ts +3 -0
- package/src/skills/categories-cache.ts +118 -0
- package/src/skills/clawhub-files.ts +1 -2
- package/src/skills/skillssh-files.ts +1 -2
- package/src/telemetry/types.ts +29 -1
- package/src/telemetry/usage-telemetry-reporter.test.ts +112 -3
- package/src/telemetry/usage-telemetry-reporter.ts +57 -2
- package/src/tools/executor.ts +1 -53
- package/src/tools/network/__tests__/web-search-metadata.test.ts +7 -1
- package/src/tools/network/__tests__/web-search.test.ts +11 -3
- package/src/tools/network/web-search-error.test.ts +248 -0
- package/src/tools/network/web-search-error.ts +267 -0
- package/src/tools/network/web-search.ts +207 -48
- package/src/tools/schedule/create.ts +2 -0
- package/src/tools/terminal/safe-env.ts +10 -1
- package/src/tools/ui-surface/definitions.ts +9 -1
- package/src/tts/__tests__/provider-catalog-consistency.test.ts +85 -1
- package/src/tts/provider-catalog.ts +76 -1
- package/src/util/mutex.ts +47 -0
- package/src/workspace/git-service.ts +1 -42
- package/src/workspace/migrations/095-bump-heartbeat-interval-30m-to-60m.ts +51 -0
- package/src/workspace/migrations/096-reduce-quality-profile-effort.ts +72 -0
- package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +93 -0
- package/src/workspace/migrations/registry.ts +6 -0
- package/src/__tests__/bootstrap-turn-cleanup.test.ts +0 -44
- package/src/__tests__/empty-response-pipeline.test.ts +0 -423
- package/src/__tests__/llm-call-pipeline.test.ts +0 -287
- package/src/__tests__/memory-retrieval-pipeline.test.ts +0 -418
- package/src/__tests__/persistence-pipeline.test.ts +0 -503
- package/src/__tests__/title-generate-pipeline.test.ts +0 -211
- package/src/__tests__/token-estimate-pipeline.test.ts +0 -479
- package/src/__tests__/tool-error-pipeline.test.ts +0 -241
- package/src/__tests__/tool-execute-pipeline.test.ts +0 -417
- package/src/__tests__/tool-result-truncate-pipeline.test.ts +0 -341
- package/src/daemon/bootstrap-turn-cleanup.ts +0 -45
- package/src/gallery/default-gallery.ts +0 -1359
- package/src/gallery/gallery-manifest.ts +0 -28
- package/src/home/feature-gate.ts +0 -22
- package/src/plugins/defaults/empty-response/middlewares/emptyResponse.ts +0 -22
- package/src/plugins/defaults/empty-response/terminal.ts +0 -106
- package/src/plugins/defaults/injectors/package.json +0 -15
- package/src/plugins/defaults/llm-call/middlewares/llmCall.ts +0 -17
- package/src/plugins/defaults/llm-call/package.json +0 -15
- package/src/plugins/defaults/llm-call/register.ts +0 -45
- package/src/plugins/defaults/memory-retrieval/middlewares/memoryRetrieval.ts +0 -17
- package/src/plugins/defaults/memory-retrieval/package.json +0 -15
- package/src/plugins/defaults/memory-retrieval/register.ts +0 -181
- package/src/plugins/defaults/persistence/middlewares/persistence.ts +0 -19
- package/src/plugins/defaults/persistence/package.json +0 -15
- package/src/plugins/defaults/persistence/register.ts +0 -38
- package/src/plugins/defaults/persistence/terminal.ts +0 -83
- package/src/plugins/defaults/title-generate/terminal.ts +0 -31
- package/src/plugins/defaults/token-estimate/middlewares/tokenEstimate.ts +0 -23
- package/src/plugins/defaults/token-estimate/package.json +0 -15
- package/src/plugins/defaults/token-estimate/register.ts +0 -34
- package/src/plugins/defaults/token-estimate/terminal.ts +0 -40
- package/src/plugins/defaults/tool-error/middlewares/toolError.ts +0 -21
- package/src/plugins/defaults/tool-error/terminal.ts +0 -47
- package/src/plugins/defaults/tool-execute/middlewares/toolExecute.ts +0 -23
- package/src/plugins/defaults/tool-execute/package.json +0 -15
- package/src/plugins/defaults/tool-execute/register.ts +0 -49
- package/src/plugins/defaults/tool-result-truncate/middlewares/toolResultTruncate.ts +0 -23
- package/src/plugins/defaults/tool-result-truncate/types.ts +0 -22
- package/src/skills/category-inference.ts +0 -111
|
@@ -94,7 +94,15 @@ mock.module("../version.js", () => ({
|
|
|
94
94
|
let mockCollectUsageData = true;
|
|
95
95
|
|
|
96
96
|
mock.module("../config/loader.js", () => ({
|
|
97
|
-
getConfig: () => ({
|
|
97
|
+
getConfig: () => ({
|
|
98
|
+
ui: {},
|
|
99
|
+
model: "test",
|
|
100
|
+
provider: "test",
|
|
101
|
+
memory: { enabled: false },
|
|
102
|
+
rateLimit: { maxRequestsPerMinute: 0 },
|
|
103
|
+
secretDetection: { enabled: false },
|
|
104
|
+
collectUsageData: mockCollectUsageData,
|
|
105
|
+
}),
|
|
98
106
|
}));
|
|
99
107
|
|
|
100
108
|
const mockQueryUnreportedLifecycleEvents = mock(
|
|
@@ -130,13 +138,24 @@ mock.module("../memory/onboarding-events-store.js", () => ({
|
|
|
130
138
|
queryUnreportedOnboardingEvents: mockQueryUnreportedOnboardingEvents,
|
|
131
139
|
}));
|
|
132
140
|
|
|
141
|
+
// The auth-fallback store is intentionally NOT mocked — it has its own
|
|
142
|
+
// DB-backed tests, and Bun's `mock.module` is process-global, so mocking it
|
|
143
|
+
// here would leak into those tests when files share an invocation. We seed the
|
|
144
|
+
// real DB instead so every auth-fallback test stays order-independent.
|
|
145
|
+
|
|
133
146
|
// ---------------------------------------------------------------------------
|
|
134
147
|
// Production import (after mocks)
|
|
135
148
|
// ---------------------------------------------------------------------------
|
|
136
149
|
|
|
150
|
+
import { recordAuthFallbackCounts } from "../memory/auth-fallback-events-store.js";
|
|
151
|
+
import { getDb } from "../memory/db-connection.js";
|
|
152
|
+
import { initializeDb } from "../memory/db-init.js";
|
|
153
|
+
import { authFallbackEvents } from "../memory/schema.js";
|
|
137
154
|
import type { UsageEvent } from "../usage/types.js";
|
|
138
155
|
import { UsageTelemetryReporter } from "./usage-telemetry-reporter.js";
|
|
139
156
|
|
|
157
|
+
initializeDb();
|
|
158
|
+
|
|
140
159
|
// ---------------------------------------------------------------------------
|
|
141
160
|
// Helpers
|
|
142
161
|
// ---------------------------------------------------------------------------
|
|
@@ -201,6 +220,7 @@ beforeEach(() => {
|
|
|
201
220
|
mockQueryUnreportedLifecycleEvents.mockReturnValue([]);
|
|
202
221
|
mockQueryUnreportedOnboardingEvents.mockReset();
|
|
203
222
|
mockQueryUnreportedOnboardingEvents.mockReturnValue([]);
|
|
223
|
+
getDb().delete(authFallbackEvents).run();
|
|
204
224
|
mockPlatformClient = null;
|
|
205
225
|
mockGetPlatformBaseUrl.mockReset();
|
|
206
226
|
mockGetDeviceId.mockReset();
|
|
@@ -909,9 +929,9 @@ describe("UsageTelemetryReporter", () => {
|
|
|
909
929
|
// No HTTP call should have been made
|
|
910
930
|
expect(mockFetch).not.toHaveBeenCalled();
|
|
911
931
|
|
|
912
|
-
// All
|
|
932
|
+
// All 5 timestamp watermarks should have been advanced (IDs left untouched
|
|
913
933
|
// so the compound-cursor branch stays active)
|
|
914
|
-
expect(mockSetMemoryCheckpoint).toHaveBeenCalledTimes(
|
|
934
|
+
expect(mockSetMemoryCheckpoint).toHaveBeenCalledTimes(5);
|
|
915
935
|
|
|
916
936
|
const calls = mockSetMemoryCheckpoint.mock.calls;
|
|
917
937
|
const keys = calls.map((c) => c[0]);
|
|
@@ -919,6 +939,7 @@ describe("UsageTelemetryReporter", () => {
|
|
|
919
939
|
expect(keys).toContain("telemetry:turns:last_reported_at");
|
|
920
940
|
expect(keys).toContain("telemetry:lifecycle:last_reported_at");
|
|
921
941
|
expect(keys).toContain("telemetry:onboarding:last_reported_at");
|
|
942
|
+
expect(keys).toContain("telemetry:auth_fallback:last_reported_at");
|
|
922
943
|
});
|
|
923
944
|
|
|
924
945
|
test("events sent normally after re-enabling collectUsageData", async () => {
|
|
@@ -1075,4 +1096,92 @@ describe("UsageTelemetryReporter", () => {
|
|
|
1075
1096
|
// Envelope still reflects the running binary, not either event.
|
|
1076
1097
|
expect(body.assistant_version).toBe("1.2.3-test");
|
|
1077
1098
|
});
|
|
1099
|
+
|
|
1100
|
+
// -------------------------------------------------------------------------
|
|
1101
|
+
// Auth-fallback events
|
|
1102
|
+
// -------------------------------------------------------------------------
|
|
1103
|
+
|
|
1104
|
+
test("auth_fallback events are included in the events array with type discriminator", async () => {
|
|
1105
|
+
mockQueryUnreportedUsageEvents.mockReturnValue([]);
|
|
1106
|
+
recordAuthFallbackCounts(1700000740000, 1700000800000, [
|
|
1107
|
+
{
|
|
1108
|
+
guard: "edge",
|
|
1109
|
+
path: "/v1/messages",
|
|
1110
|
+
failureKind: "missing_authorization",
|
|
1111
|
+
count: 42,
|
|
1112
|
+
},
|
|
1113
|
+
]);
|
|
1114
|
+
mockFetch.mockImplementation(() =>
|
|
1115
|
+
Promise.resolve(new Response('{"accepted":1}', { status: 200 })),
|
|
1116
|
+
);
|
|
1117
|
+
|
|
1118
|
+
const reporter = new UsageTelemetryReporter();
|
|
1119
|
+
await reporter.flush();
|
|
1120
|
+
|
|
1121
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
1122
|
+
const body = JSON.parse(
|
|
1123
|
+
(mockFetch.mock.calls[0] as [string, RequestInit])[1].body as string,
|
|
1124
|
+
);
|
|
1125
|
+
expect(body.events.length).toBe(1);
|
|
1126
|
+
expect(body.events[0]).toMatchObject({
|
|
1127
|
+
type: "auth_fallback",
|
|
1128
|
+
guard: "edge",
|
|
1129
|
+
path: "/v1/messages",
|
|
1130
|
+
failure_kind: "missing_authorization",
|
|
1131
|
+
count: 42,
|
|
1132
|
+
window_start: 1700000740000,
|
|
1133
|
+
window_end: 1700000800000,
|
|
1134
|
+
assistant_version: "1.2.3-test",
|
|
1135
|
+
});
|
|
1136
|
+
// recorded_at is the row's createdAt (stamped at record time).
|
|
1137
|
+
expect(typeof body.events[0].recorded_at).toBe("number");
|
|
1138
|
+
expect(typeof body.events[0].daemon_event_id).toBe("string");
|
|
1139
|
+
});
|
|
1140
|
+
|
|
1141
|
+
test("auth_fallback watermark advances to the last reported row on success", async () => {
|
|
1142
|
+
mockQueryUnreportedUsageEvents.mockReturnValue([]);
|
|
1143
|
+
recordAuthFallbackCounts(1700000000000, 1700000001000, [
|
|
1144
|
+
{
|
|
1145
|
+
guard: "edge-scoped",
|
|
1146
|
+
path: "/v1/a",
|
|
1147
|
+
failureKind: "insufficient_scope",
|
|
1148
|
+
count: 1,
|
|
1149
|
+
},
|
|
1150
|
+
{
|
|
1151
|
+
guard: "edge-guardian",
|
|
1152
|
+
path: "/v1/b",
|
|
1153
|
+
failureKind: "guardian_mismatch",
|
|
1154
|
+
count: 3,
|
|
1155
|
+
},
|
|
1156
|
+
]);
|
|
1157
|
+
mockFetch.mockImplementation(() =>
|
|
1158
|
+
Promise.resolve(new Response('{"accepted":2}', { status: 200 })),
|
|
1159
|
+
);
|
|
1160
|
+
|
|
1161
|
+
// The last row by the reporter's (createdAt, id) cursor order is the one
|
|
1162
|
+
// whose watermark should be persisted after a successful upload.
|
|
1163
|
+
const rows = getDb()
|
|
1164
|
+
.select()
|
|
1165
|
+
.from(authFallbackEvents)
|
|
1166
|
+
.orderBy(authFallbackEvents.createdAt, authFallbackEvents.id)
|
|
1167
|
+
.all();
|
|
1168
|
+
const lastRow = rows[rows.length - 1];
|
|
1169
|
+
|
|
1170
|
+
const reporter = new UsageTelemetryReporter();
|
|
1171
|
+
await reporter.flush();
|
|
1172
|
+
|
|
1173
|
+
const watermarkCalls = mockSetMemoryCheckpoint.mock.calls.filter(
|
|
1174
|
+
(c) => c[0] === "telemetry:auth_fallback:last_reported_at",
|
|
1175
|
+
);
|
|
1176
|
+
expect(watermarkCalls.length).toBeGreaterThanOrEqual(1);
|
|
1177
|
+
expect(watermarkCalls[watermarkCalls.length - 1][1]).toBe(
|
|
1178
|
+
String(lastRow.createdAt),
|
|
1179
|
+
);
|
|
1180
|
+
|
|
1181
|
+
const idCalls = mockSetMemoryCheckpoint.mock.calls.filter(
|
|
1182
|
+
(c) => c[0] === "telemetry:auth_fallback:last_reported_id",
|
|
1183
|
+
);
|
|
1184
|
+
expect(idCalls.length).toBeGreaterThanOrEqual(1);
|
|
1185
|
+
expect(idCalls[idCalls.length - 1][1]).toBe(lastRow.id);
|
|
1186
|
+
});
|
|
1078
1187
|
});
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
getPlatformUserId,
|
|
16
16
|
} from "../config/env.js";
|
|
17
17
|
import { getConfig } from "../config/loader.js";
|
|
18
|
+
import { queryUnreportedAuthFallbackEvents } from "../memory/auth-fallback-events-store.js";
|
|
18
19
|
import {
|
|
19
20
|
getMemoryCheckpoint,
|
|
20
21
|
setMemoryCheckpoint,
|
|
@@ -47,6 +48,10 @@ const CHECKPOINT_KEY_ONBOARDING_WATERMARK =
|
|
|
47
48
|
"telemetry:onboarding:last_reported_at";
|
|
48
49
|
const CHECKPOINT_KEY_ONBOARDING_WATERMARK_ID =
|
|
49
50
|
"telemetry:onboarding:last_reported_id";
|
|
51
|
+
const CHECKPOINT_KEY_AUTH_FALLBACK_WATERMARK =
|
|
52
|
+
"telemetry:auth_fallback:last_reported_at";
|
|
53
|
+
const CHECKPOINT_KEY_AUTH_FALLBACK_WATERMARK_ID =
|
|
54
|
+
"telemetry:auth_fallback:last_reported_id";
|
|
50
55
|
const REPORT_INTERVAL_MS = 5 * 60 * 1000;
|
|
51
56
|
const INITIAL_FLUSH_DELAY_MS = 30_000; // Delay first flush to let CES handshake complete
|
|
52
57
|
const BATCH_SIZE = 500;
|
|
@@ -139,6 +144,7 @@ export class UsageTelemetryReporter {
|
|
|
139
144
|
setMemoryCheckpoint(CHECKPOINT_KEY_TURN_WATERMARK, now);
|
|
140
145
|
setMemoryCheckpoint(CHECKPOINT_KEY_LIFECYCLE_WATERMARK, now);
|
|
141
146
|
setMemoryCheckpoint(CHECKPOINT_KEY_ONBOARDING_WATERMARK, now);
|
|
147
|
+
setMemoryCheckpoint(CHECKPOINT_KEY_AUTH_FALLBACK_WATERMARK, now);
|
|
142
148
|
return;
|
|
143
149
|
}
|
|
144
150
|
|
|
@@ -171,6 +177,14 @@ export class UsageTelemetryReporter {
|
|
|
171
177
|
getMemoryCheckpoint(CHECKPOINT_KEY_ONBOARDING_WATERMARK_ID) ??
|
|
172
178
|
undefined;
|
|
173
179
|
|
|
180
|
+
// Read auth-fallback watermark (compound cursor: createdAt + id)
|
|
181
|
+
const authFallbackWatermark = Number(
|
|
182
|
+
getMemoryCheckpoint(CHECKPOINT_KEY_AUTH_FALLBACK_WATERMARK) ?? "0",
|
|
183
|
+
);
|
|
184
|
+
const authFallbackWatermarkId =
|
|
185
|
+
getMemoryCheckpoint(CHECKPOINT_KEY_AUTH_FALLBACK_WATERMARK_ID) ??
|
|
186
|
+
undefined;
|
|
187
|
+
|
|
174
188
|
// Query unreported events
|
|
175
189
|
const events = queryUnreportedUsageEvents(
|
|
176
190
|
watermark,
|
|
@@ -192,12 +206,18 @@ export class UsageTelemetryReporter {
|
|
|
192
206
|
onboardingWatermarkId,
|
|
193
207
|
BATCH_SIZE,
|
|
194
208
|
);
|
|
209
|
+
const authFallbackEvents = queryUnreportedAuthFallbackEvents(
|
|
210
|
+
authFallbackWatermark,
|
|
211
|
+
authFallbackWatermarkId,
|
|
212
|
+
BATCH_SIZE,
|
|
213
|
+
);
|
|
195
214
|
|
|
196
215
|
if (
|
|
197
216
|
events.length === 0 &&
|
|
198
217
|
turnEvents.length === 0 &&
|
|
199
218
|
lifecycleEvents.length === 0 &&
|
|
200
|
-
onboardingEvents.length === 0
|
|
219
|
+
onboardingEvents.length === 0 &&
|
|
220
|
+
authFallbackEvents.length === 0
|
|
201
221
|
)
|
|
202
222
|
return;
|
|
203
223
|
|
|
@@ -211,6 +231,7 @@ export class UsageTelemetryReporter {
|
|
|
211
231
|
turnCount: turnEvents.length,
|
|
212
232
|
lifecycleCount: lifecycleEvents.length,
|
|
213
233
|
onboardingCount: onboardingEvents.length,
|
|
234
|
+
authFallbackCount: authFallbackEvents.length,
|
|
214
235
|
},
|
|
215
236
|
"Telemetry flush: resolved auth context",
|
|
216
237
|
);
|
|
@@ -337,6 +358,25 @@ export class UsageTelemetryReporter {
|
|
|
337
358
|
assistant_version: APP_VERSION,
|
|
338
359
|
}),
|
|
339
360
|
),
|
|
361
|
+
...authFallbackEvents.map(
|
|
362
|
+
(e): TelemetryEvent => ({
|
|
363
|
+
type: "auth_fallback",
|
|
364
|
+
daemon_event_id: e.id,
|
|
365
|
+
recorded_at: e.createdAt,
|
|
366
|
+
guard: e.guard,
|
|
367
|
+
path: e.path,
|
|
368
|
+
failure_kind: e.failureKind,
|
|
369
|
+
count: e.count,
|
|
370
|
+
window_start: e.windowStart,
|
|
371
|
+
window_end: e.windowEnd,
|
|
372
|
+
// Aggregated counts forwarded by the gateway carry no record-time
|
|
373
|
+
// binary version; stamp the running binary's `APP_VERSION` so the
|
|
374
|
+
// wire value is concrete rather than an explicit null that would
|
|
375
|
+
// override the envelope under the platform's per-event-wins
|
|
376
|
+
// contract.
|
|
377
|
+
assistant_version: APP_VERSION,
|
|
378
|
+
}),
|
|
379
|
+
),
|
|
340
380
|
];
|
|
341
381
|
|
|
342
382
|
const organizationId = getPlatformOrganizationId() || undefined;
|
|
@@ -422,12 +462,27 @@ export class UsageTelemetryReporter {
|
|
|
422
462
|
);
|
|
423
463
|
}
|
|
424
464
|
|
|
465
|
+
// Advance auth-fallback watermark (compound cursor)
|
|
466
|
+
if (authFallbackEvents.length > 0) {
|
|
467
|
+
const lastAuthFallback =
|
|
468
|
+
authFallbackEvents[authFallbackEvents.length - 1];
|
|
469
|
+
setMemoryCheckpoint(
|
|
470
|
+
CHECKPOINT_KEY_AUTH_FALLBACK_WATERMARK,
|
|
471
|
+
String(lastAuthFallback.createdAt),
|
|
472
|
+
);
|
|
473
|
+
setMemoryCheckpoint(
|
|
474
|
+
CHECKPOINT_KEY_AUTH_FALLBACK_WATERMARK_ID,
|
|
475
|
+
lastAuthFallback.id,
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
|
|
425
479
|
// If we got a full batch of any type, there may be more — recurse
|
|
426
480
|
if (
|
|
427
481
|
events.length === BATCH_SIZE ||
|
|
428
482
|
turnEvents.length === BATCH_SIZE ||
|
|
429
483
|
lifecycleEvents.length === BATCH_SIZE ||
|
|
430
|
-
onboardingEvents.length === BATCH_SIZE
|
|
484
|
+
onboardingEvents.length === BATCH_SIZE ||
|
|
485
|
+
authFallbackEvents.length === BATCH_SIZE
|
|
431
486
|
) {
|
|
432
487
|
await this._doFlush(batchCount + 1);
|
|
433
488
|
}
|
package/src/tools/executor.ts
CHANGED
|
@@ -1,18 +1,10 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
|
|
3
|
-
import { parseChannelId } from "../channels/types.js";
|
|
4
3
|
import { getConfig } from "../config/loader.js";
|
|
5
4
|
import { bridgeCesApproval } from "../credential-execution/approval-bridge.js";
|
|
6
5
|
import { isCesShellLockdownEnabled } from "../credential-execution/feature-gates.js";
|
|
7
6
|
import { PermissionPrompter } from "../permissions/prompter.js";
|
|
8
7
|
import { RiskLevel } from "../permissions/types.js";
|
|
9
|
-
import { runPipeline } from "../plugins/pipeline.js";
|
|
10
|
-
import { getMiddlewaresFor } from "../plugins/registry.js";
|
|
11
|
-
import type {
|
|
12
|
-
ToolExecuteArgs,
|
|
13
|
-
ToolExecuteResult,
|
|
14
|
-
TurnContext,
|
|
15
|
-
} from "../plugins/types.js";
|
|
16
8
|
import { isUntrustedTrustClass } from "../runtime/actor-trust-resolver.js";
|
|
17
9
|
import { redactSensitiveFields } from "../security/redaction.js";
|
|
18
10
|
import { TokenExpiredError } from "../security/token-manager.js";
|
|
@@ -52,54 +44,10 @@ export class ToolExecutor {
|
|
|
52
44
|
name: string,
|
|
53
45
|
input: Record<string, unknown>,
|
|
54
46
|
context: ToolContext,
|
|
55
|
-
/**
|
|
56
|
-
* Optional per-turn context threaded in by the agent loop. Production
|
|
57
|
-
* sites propagate the orchestrator-built `TurnContext` (real
|
|
58
|
-
* `conversationId`, trust cascade, attached `contextWindowManager`) so
|
|
59
|
-
* middleware registered on the `toolExecute` pipeline sees the same
|
|
60
|
-
* context every other pipeline slot uses. When omitted (CLI/test
|
|
61
|
-
* invocations that call `ToolExecutor.execute` directly), the executor
|
|
62
|
-
* synthesizes a fallback context from the {@link ToolContext}.
|
|
63
|
-
*/
|
|
64
|
-
turnContext?: TurnContext,
|
|
65
47
|
): Promise<ToolExecutionResult> {
|
|
66
|
-
// Prefer the orchestrator-supplied `turnContext` so the pipeline sees
|
|
67
|
-
// the real conversation identity, per-turn trust, and context-window
|
|
68
|
-
// manager. When absent (CLI / test invocations that bypass the agent
|
|
69
|
-
// loop), synthesize a minimal context from the `ToolContext`.
|
|
70
|
-
const turnCtx: TurnContext = turnContext ?? {
|
|
71
|
-
requestId: context.requestId ?? "",
|
|
72
|
-
conversationId: context.conversationId,
|
|
73
|
-
turnIndex: 0,
|
|
74
|
-
trust: {
|
|
75
|
-
sourceChannel: parseChannelId(context.executionChannel) ?? "vellum",
|
|
76
|
-
trustClass: context.trustClass,
|
|
77
|
-
},
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
const middlewares = getMiddlewaresFor("toolExecute");
|
|
81
48
|
const { name: executionName, input: executionInput } =
|
|
82
49
|
resolveToolInvocationAlias(name, input, context.allowedToolNames);
|
|
83
|
-
|
|
84
|
-
name: executionName,
|
|
85
|
-
input: executionInput,
|
|
86
|
-
context,
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
// No pipeline-level timeout: `executeInternal` already wraps the real
|
|
90
|
-
// tool invocation in `executeWithTimeout`, which is the sole enforcer
|
|
91
|
-
// of the per-tool budget. The pipeline itself runs untimed so that
|
|
92
|
-
// upstream phases (permission checks, approval waits, middleware) are
|
|
93
|
-
// not racing the tool budget — only the actual tool call is. Runaway
|
|
94
|
-
// middleware is a plugin-health concern handled by per-plugin timeouts,
|
|
95
|
-
// not here.
|
|
96
|
-
return runPipeline<ToolExecuteArgs, ToolExecuteResult>(
|
|
97
|
-
"toolExecute",
|
|
98
|
-
middlewares,
|
|
99
|
-
(args) => this.executeInternal(args.name, args.input, args.context),
|
|
100
|
-
pipelineArgs,
|
|
101
|
-
turnCtx,
|
|
102
|
-
);
|
|
50
|
+
return this.executeInternal(executionName, executionInput, context);
|
|
103
51
|
}
|
|
104
52
|
|
|
105
53
|
private async executeInternal(
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
2
|
|
|
3
|
+
import { WEB_SEARCH_BACKEND_FAILURE_MESSAGE } from "../web-search-error.js";
|
|
4
|
+
|
|
3
5
|
// Mutable mock state - set per test
|
|
4
6
|
let mockWebSearchProvider: string | undefined = "perplexity";
|
|
5
7
|
let mockBraveSecureKey: string | undefined;
|
|
@@ -32,7 +34,9 @@ mock.module("../../../security/secure-keys.js", () => ({
|
|
|
32
34
|
},
|
|
33
35
|
}));
|
|
34
36
|
|
|
37
|
+
const realLogger = await import("../../../util/logger.js");
|
|
35
38
|
mock.module("../../../util/logger.js", () => ({
|
|
39
|
+
...realLogger,
|
|
36
40
|
getLogger: () =>
|
|
37
41
|
new Proxy({} as Record<string, unknown>, {
|
|
38
42
|
get: () => () => {},
|
|
@@ -169,7 +173,9 @@ describe("web_search activity metadata", () => {
|
|
|
169
173
|
expect(meta.provider).toBe("perplexity");
|
|
170
174
|
expect(meta.resultCount).toBe(0);
|
|
171
175
|
expect(meta.results).toEqual([]);
|
|
172
|
-
|
|
176
|
+
// Post-retry rate limits now surface the centralized friendly recoverable
|
|
177
|
+
// copy (ATL-727) rather than provider-specific rate-limit wording.
|
|
178
|
+
expect(meta.errorMessage).toBe(WEB_SEARCH_BACKEND_FAILURE_MESSAGE);
|
|
173
179
|
});
|
|
174
180
|
|
|
175
181
|
// ---- Tavily -------------------------------------------------------------
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
2
|
|
|
3
|
+
import { WEB_SEARCH_BACKEND_FAILURE_MESSAGE } from "../web-search-error.js";
|
|
4
|
+
|
|
3
5
|
// Mutable mock state - set per test
|
|
4
6
|
let mockWebSearchProvider: string | undefined = "perplexity";
|
|
5
7
|
let mockWebSearchMode: string | undefined = "your-own";
|
|
@@ -42,7 +44,9 @@ mock.module("../../../security/secure-keys.js", () => ({
|
|
|
42
44
|
},
|
|
43
45
|
}));
|
|
44
46
|
|
|
47
|
+
const realLogger = await import("../../../util/logger.js");
|
|
45
48
|
mock.module("../../../util/logger.js", () => ({
|
|
49
|
+
...realLogger,
|
|
46
50
|
getLogger: () =>
|
|
47
51
|
new Proxy({} as Record<string, unknown>, {
|
|
48
52
|
get: () => () => {},
|
|
@@ -201,7 +205,8 @@ describe("web_search tool", () => {
|
|
|
201
205
|
|
|
202
206
|
const result = await execute({ query: "test" });
|
|
203
207
|
expect(result.isError).toBe(true);
|
|
204
|
-
|
|
208
|
+
// Post-retry rate limits surface the friendly recoverable copy (ATL-727).
|
|
209
|
+
expect(result.content).toBe(WEB_SEARCH_BACKEND_FAILURE_MESSAGE);
|
|
205
210
|
// 1 initial + 3 retries = 4 calls
|
|
206
211
|
expect(callCount).toBe(4);
|
|
207
212
|
});
|
|
@@ -214,7 +219,9 @@ describe("web_search tool", () => {
|
|
|
214
219
|
|
|
215
220
|
const result = await execute({ query: "test" });
|
|
216
221
|
expect(result.isError).toBe(true);
|
|
217
|
-
|
|
222
|
+
// 5xx is a backend failure -> friendly recoverable copy, no raw status.
|
|
223
|
+
expect(result.content).toBe(WEB_SEARCH_BACKEND_FAILURE_MESSAGE);
|
|
224
|
+
expect(result.content).not.toContain("500");
|
|
218
225
|
});
|
|
219
226
|
|
|
220
227
|
// ---- Brave provider -----------------------------------------------------
|
|
@@ -673,7 +680,8 @@ describe("web_search tool", () => {
|
|
|
673
680
|
|
|
674
681
|
const result = await execute({ query: "test" });
|
|
675
682
|
expect(result.isError).toBe(true);
|
|
676
|
-
|
|
683
|
+
// Post-retry rate limits surface the friendly recoverable copy (ATL-727).
|
|
684
|
+
expect(result.content).toBe(WEB_SEARCH_BACKEND_FAILURE_MESSAGE);
|
|
677
685
|
expect(callCount).toBe(4);
|
|
678
686
|
});
|
|
679
687
|
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { createAbortReason } from "../../util/abort-reasons.js";
|
|
4
|
+
import {
|
|
5
|
+
classifyWebSearchFailure,
|
|
6
|
+
logWebSearchBackendFailure,
|
|
7
|
+
WEB_SEARCH_BACKEND_FAILURE_MESSAGE,
|
|
8
|
+
} from "./web-search-error.js";
|
|
9
|
+
|
|
10
|
+
describe("classifyWebSearchFailure", () => {
|
|
11
|
+
test("Anthropic 'unavailable' code is a backend failure with friendly copy", () => {
|
|
12
|
+
const result = classifyWebSearchFailure({
|
|
13
|
+
isError: true,
|
|
14
|
+
errorCode: "unavailable",
|
|
15
|
+
});
|
|
16
|
+
expect(result.category).toBe("backend_unavailable");
|
|
17
|
+
expect(result.isBackendFailure).toBe(true);
|
|
18
|
+
expect(result.userMessage).toBe(WEB_SEARCH_BACKEND_FAILURE_MESSAGE);
|
|
19
|
+
expect(result.rawDetail).toContain("unavailable");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test.each(["internal_error", "overloaded_error"])(
|
|
23
|
+
"Anthropic '%s' code is a backend failure",
|
|
24
|
+
(errorCode) => {
|
|
25
|
+
const result = classifyWebSearchFailure({ isError: true, errorCode });
|
|
26
|
+
expect(result.category).toBe("backend_unavailable");
|
|
27
|
+
expect(result.isBackendFailure).toBe(true);
|
|
28
|
+
},
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
test("HTTP 503 is a backend failure", () => {
|
|
32
|
+
const result = classifyWebSearchFailure({ isError: true, statusCode: 503 });
|
|
33
|
+
expect(result.category).toBe("backend_unavailable");
|
|
34
|
+
expect(result.userMessage).toBe(WEB_SEARCH_BACKEND_FAILURE_MESSAGE);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("thrown TypeError('fetch failed') is a backend failure", () => {
|
|
38
|
+
const result = classifyWebSearchFailure({
|
|
39
|
+
isError: true,
|
|
40
|
+
error: new TypeError("fetch failed"),
|
|
41
|
+
});
|
|
42
|
+
expect(result.category).toBe("backend_unavailable");
|
|
43
|
+
expect(result.isBackendFailure).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("AbortError-shaped timeout is a backend failure", () => {
|
|
47
|
+
const err = new Error("The operation was aborted due to timeout");
|
|
48
|
+
err.name = "AbortError";
|
|
49
|
+
const result = classifyWebSearchFailure({ isError: true, error: err });
|
|
50
|
+
expect(result.category).toBe("backend_unavailable");
|
|
51
|
+
expect(result.isBackendFailure).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("user-initiated abort is not a backend failure", () => {
|
|
55
|
+
const err = new Error("The operation was aborted");
|
|
56
|
+
err.name = "AbortError";
|
|
57
|
+
Object.assign(err, {
|
|
58
|
+
reason: createAbortReason("user_cancel", "cancelGeneration"),
|
|
59
|
+
});
|
|
60
|
+
const result = classifyWebSearchFailure({ isError: true, error: err });
|
|
61
|
+
expect(result.isBackendFailure).toBe(false);
|
|
62
|
+
expect(result.userMessage).not.toBe(WEB_SEARCH_BACKEND_FAILURE_MESSAGE);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("wrapped ProviderError carrying abortReason is not a backend failure", () => {
|
|
66
|
+
// A provider wrapper erases the AbortError name and re-words the message,
|
|
67
|
+
// but carries the tagged reason on `abortReason` (ProviderError shape).
|
|
68
|
+
const err = Object.assign(new Error("Request was aborted"), {
|
|
69
|
+
name: "ProviderError",
|
|
70
|
+
abortReason: createAbortReason("user_cancel", "cancelGeneration"),
|
|
71
|
+
});
|
|
72
|
+
const result = classifyWebSearchFailure({ isError: true, error: err });
|
|
73
|
+
expect(result.category).not.toBe("backend_unavailable");
|
|
74
|
+
expect(result.isBackendFailure).toBe(false);
|
|
75
|
+
expect(result.userMessage).not.toBe(WEB_SEARCH_BACKEND_FAILURE_MESSAGE);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("tagged abort with a transport-shaped cause is not a backend failure", () => {
|
|
79
|
+
// A user cancellation wrapped as a ProviderError that ALSO carries a
|
|
80
|
+
// transport-shaped `cause` (ECONNRESET). The tagged abort guard must win
|
|
81
|
+
// over transport-retryability so this is not mislabeled a backend outage.
|
|
82
|
+
const err = Object.assign(new Error("Request was aborted"), {
|
|
83
|
+
name: "ProviderError",
|
|
84
|
+
cause: { code: "ECONNRESET" },
|
|
85
|
+
abortReason: createAbortReason("user_cancel", "cancelGeneration"),
|
|
86
|
+
});
|
|
87
|
+
const result = classifyWebSearchFailure({ isError: true, error: err });
|
|
88
|
+
expect(result.category).not.toBe("backend_unavailable");
|
|
89
|
+
expect(result.isBackendFailure).toBe(false);
|
|
90
|
+
expect(result.userMessage).not.toBe(WEB_SEARCH_BACKEND_FAILURE_MESSAGE);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("explicit statusCode wins over a misleading error-body keyword", () => {
|
|
94
|
+
// The provider response body contains "aborted" (a keyword the error-body
|
|
95
|
+
// heuristic would sniff as a non-failure), but the authoritative HTTP 503
|
|
96
|
+
// must classify this as a backend failure.
|
|
97
|
+
const result = classifyWebSearchFailure({
|
|
98
|
+
isError: true,
|
|
99
|
+
statusCode: 503,
|
|
100
|
+
error: new Error("the upstream request was aborted unexpectedly"),
|
|
101
|
+
});
|
|
102
|
+
expect(result.category).toBe("backend_unavailable");
|
|
103
|
+
expect(result.isBackendFailure).toBe(true);
|
|
104
|
+
expect(result.userMessage).toBe(WEB_SEARCH_BACKEND_FAILURE_MESSAGE);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("ECONNRESET network error is a backend failure", () => {
|
|
108
|
+
const err = Object.assign(new Error("socket hang up"), {
|
|
109
|
+
code: "ECONNRESET",
|
|
110
|
+
});
|
|
111
|
+
const result = classifyWebSearchFailure({ isError: true, error: err });
|
|
112
|
+
expect(result.category).toBe("backend_unavailable");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("Anthropic 'too_many_requests' code is rate-limited with backend copy", () => {
|
|
116
|
+
const result = classifyWebSearchFailure({
|
|
117
|
+
isError: true,
|
|
118
|
+
errorCode: "too_many_requests",
|
|
119
|
+
});
|
|
120
|
+
expect(result.category).toBe("rate_limited");
|
|
121
|
+
expect(result.isBackendFailure).toBe(true);
|
|
122
|
+
expect(result.userMessage).toBe(WEB_SEARCH_BACKEND_FAILURE_MESSAGE);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("HTTP 429 is rate-limited with backend copy", () => {
|
|
126
|
+
const result = classifyWebSearchFailure({ isError: true, statusCode: 429 });
|
|
127
|
+
expect(result.category).toBe("rate_limited");
|
|
128
|
+
expect(result.userMessage).toBe(WEB_SEARCH_BACKEND_FAILURE_MESSAGE);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("'query_too_long' has a distinct, non-backend message", () => {
|
|
132
|
+
const result = classifyWebSearchFailure({
|
|
133
|
+
isError: true,
|
|
134
|
+
errorCode: "query_too_long",
|
|
135
|
+
});
|
|
136
|
+
expect(result.category).toBe("query_too_long");
|
|
137
|
+
expect(result.isBackendFailure).toBe(false);
|
|
138
|
+
expect(result.userMessage).not.toBe(WEB_SEARCH_BACKEND_FAILURE_MESSAGE);
|
|
139
|
+
expect(result.userMessage.length).toBeGreaterThan(0);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("'max_uses_exceeded' has a distinct, non-backend message", () => {
|
|
143
|
+
const result = classifyWebSearchFailure({
|
|
144
|
+
isError: true,
|
|
145
|
+
errorCode: "max_uses_exceeded",
|
|
146
|
+
});
|
|
147
|
+
expect(result.category).toBe("max_uses_exceeded");
|
|
148
|
+
expect(result.isBackendFailure).toBe(false);
|
|
149
|
+
expect(result.userMessage).not.toBe(WEB_SEARCH_BACKEND_FAILURE_MESSAGE);
|
|
150
|
+
expect(result.userMessage.length).toBeGreaterThan(0);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("'invalid_input' is unknown and not a backend failure", () => {
|
|
154
|
+
const result = classifyWebSearchFailure({
|
|
155
|
+
isError: true,
|
|
156
|
+
errorCode: "invalid_input",
|
|
157
|
+
});
|
|
158
|
+
expect(result.category).toBe("unknown");
|
|
159
|
+
expect(result.isBackendFailure).toBe(false);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("HTTP 401 is a config failure, not the backend copy", () => {
|
|
163
|
+
const result = classifyWebSearchFailure({ isError: true, statusCode: 401 });
|
|
164
|
+
expect(result.category).toBe("config");
|
|
165
|
+
expect(result.isBackendFailure).toBe(false);
|
|
166
|
+
expect(result.userMessage).not.toBe(WEB_SEARCH_BACKEND_FAILURE_MESSAGE);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("HTTP 403 is a config failure", () => {
|
|
170
|
+
const result = classifyWebSearchFailure({ isError: true, statusCode: 403 });
|
|
171
|
+
expect(result.category).toBe("config");
|
|
172
|
+
expect(result.isBackendFailure).toBe(false);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("successful-but-empty result is no_results, not a failure", () => {
|
|
176
|
+
const result = classifyWebSearchFailure({
|
|
177
|
+
isError: false,
|
|
178
|
+
hasResults: false,
|
|
179
|
+
});
|
|
180
|
+
expect(result.category).toBe("no_results");
|
|
181
|
+
expect(result.isBackendFailure).toBe(false);
|
|
182
|
+
expect(result.userMessage).not.toBe(WEB_SEARCH_BACKEND_FAILURE_MESSAGE);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("rawDetail is truncated to 500 chars", () => {
|
|
186
|
+
const result = classifyWebSearchFailure({
|
|
187
|
+
isError: true,
|
|
188
|
+
error: new Error("x".repeat(1000)),
|
|
189
|
+
});
|
|
190
|
+
expect(result.rawDetail.length).toBeLessThanOrEqual(500 + 40);
|
|
191
|
+
expect(result.rawDetail).toContain("truncated");
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe("WEB_SEARCH_BACKEND_FAILURE_MESSAGE copy safety", () => {
|
|
196
|
+
test("offers retry / continue-without / paste", () => {
|
|
197
|
+
expect(WEB_SEARCH_BACKEND_FAILURE_MESSAGE).toContain("try again");
|
|
198
|
+
expect(WEB_SEARCH_BACKEND_FAILURE_MESSAGE).toContain("continue without");
|
|
199
|
+
expect(WEB_SEARCH_BACKEND_FAILURE_MESSAGE).toContain("paste");
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("contains no raw provider details, JSON, or exception names", () => {
|
|
203
|
+
for (const banned of [
|
|
204
|
+
"{",
|
|
205
|
+
"error_code",
|
|
206
|
+
"Anthropic",
|
|
207
|
+
"web_search_tool_result_error",
|
|
208
|
+
"TypeError",
|
|
209
|
+
"stack",
|
|
210
|
+
]) {
|
|
211
|
+
expect(WEB_SEARCH_BACKEND_FAILURE_MESSAGE).not.toContain(banned);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe("logWebSearchBackendFailure", () => {
|
|
217
|
+
test("captures the event and rawDetail without raw query text", () => {
|
|
218
|
+
const calls: unknown[][] = [];
|
|
219
|
+
const fakeLogger = {
|
|
220
|
+
warn: (...args: unknown[]) => {
|
|
221
|
+
calls.push(args);
|
|
222
|
+
},
|
|
223
|
+
} as unknown as Parameters<typeof logWebSearchBackendFailure>[0];
|
|
224
|
+
|
|
225
|
+
const secretQuery = "super secret user query text";
|
|
226
|
+
logWebSearchBackendFailure(fakeLogger, {
|
|
227
|
+
provider: "anthropic",
|
|
228
|
+
requestId: "req-1",
|
|
229
|
+
errorCategory: "backend_unavailable",
|
|
230
|
+
rawDetail: "errorCode=unavailable",
|
|
231
|
+
fallbackShown: true,
|
|
232
|
+
queryLength: secretQuery.length,
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
expect(calls).toHaveLength(1);
|
|
236
|
+
const [payload, msg] = calls[0] as [Record<string, unknown>, string];
|
|
237
|
+
expect(payload.event).toBe("web_search_backend_failure");
|
|
238
|
+
expect(payload.tool).toBe("web_search");
|
|
239
|
+
expect(payload.provider).toBe("anthropic");
|
|
240
|
+
expect(payload.rawDetail).toBe("errorCode=unavailable");
|
|
241
|
+
expect(payload.queryLength).toBe(secretQuery.length);
|
|
242
|
+
expect(msg).toBe("web_search backend failure");
|
|
243
|
+
|
|
244
|
+
// The raw query text must never appear anywhere in the logged payload.
|
|
245
|
+
const serialized = JSON.stringify(calls);
|
|
246
|
+
expect(serialized).not.toContain(secretQuery);
|
|
247
|
+
});
|
|
248
|
+
});
|