@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
|
@@ -140,53 +140,19 @@ describe("resolveSlash command contract", () => {
|
|
|
140
140
|
});
|
|
141
141
|
});
|
|
142
142
|
|
|
143
|
-
describe("resolveSlash /compact
|
|
144
|
-
test("plain /compact
|
|
143
|
+
describe("resolveSlash /compact", () => {
|
|
144
|
+
test("plain /compact resolves to kind=compact", async () => {
|
|
145
145
|
const result = await resolveSlash("/compact");
|
|
146
146
|
expect(result).toEqual({ kind: "compact" });
|
|
147
147
|
});
|
|
148
148
|
|
|
149
|
-
test("/compact
|
|
149
|
+
test("/compact rejects arguments with usage hint", async () => {
|
|
150
150
|
const result = await resolveSlash("/compact 30000");
|
|
151
|
-
expect(result).toEqual({
|
|
152
|
-
kind: "compact",
|
|
153
|
-
targetInputTokensOverride: 30000,
|
|
154
|
-
});
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
test("/compact <n>k expands to thousands", async () => {
|
|
158
|
-
const result = await resolveSlash("/compact 30k");
|
|
159
|
-
expect(result).toEqual({
|
|
160
|
-
kind: "compact",
|
|
161
|
-
targetInputTokensOverride: 30_000,
|
|
162
|
-
});
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
test("/compact <n>m expands to millions", async () => {
|
|
166
|
-
const result = await resolveSlash("/compact 1.5M");
|
|
167
|
-
expect(result).toEqual({
|
|
168
|
-
kind: "compact",
|
|
169
|
-
targetInputTokensOverride: 1_500_000,
|
|
170
|
-
});
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
test("/compact rejects malformed args with usage hint", async () => {
|
|
174
|
-
const result = await resolveSlash("/compact bogus");
|
|
175
151
|
expect(result.kind).toBe("unknown");
|
|
176
152
|
if (result.kind !== "unknown") throw new Error("expected unknown");
|
|
177
|
-
expect(result.message).toContain("
|
|
153
|
+
expect(result.message).toContain("does not take arguments");
|
|
178
154
|
expect(result.message).toContain("/compact");
|
|
179
155
|
});
|
|
180
|
-
|
|
181
|
-
test("/compact rejects zero", async () => {
|
|
182
|
-
const result = await resolveSlash("/compact 0");
|
|
183
|
-
expect(result.kind).toBe("unknown");
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
test("/compact rejects negative numbers", async () => {
|
|
187
|
-
const result = await resolveSlash("/compact -50");
|
|
188
|
-
expect(result.kind).toBe("unknown");
|
|
189
|
-
});
|
|
190
156
|
});
|
|
191
157
|
|
|
192
158
|
describe("resolveSlash /clean", () => {
|
|
@@ -227,9 +193,9 @@ describe("classifySlash is a pure classifier matching resolveSlash kinds", () =>
|
|
|
227
193
|
{ input: "/status", kind: "unknown" },
|
|
228
194
|
{ input: "/commands", kind: "unknown" },
|
|
229
195
|
{ input: "/compact", kind: "compact" },
|
|
230
|
-
{ input: "/compact 30000", kind: "
|
|
231
|
-
{ input: "/compact 30k", kind: "
|
|
232
|
-
{ input: "/compact 1.5M", kind: "
|
|
196
|
+
{ input: "/compact 30000", kind: "unknown" },
|
|
197
|
+
{ input: "/compact 30k", kind: "unknown" },
|
|
198
|
+
{ input: "/compact 1.5M", kind: "unknown" },
|
|
233
199
|
{ input: "/compact bogus", kind: "unknown" },
|
|
234
200
|
{ input: "/clean", kind: "clean" },
|
|
235
201
|
{ input: " /clean ", kind: "clean" },
|
|
@@ -239,7 +205,7 @@ describe("classifySlash is a pure classifier matching resolveSlash kinds", () =>
|
|
|
239
205
|
{ input: "/opus", kind: "unknown" },
|
|
240
206
|
{ input: "hello", kind: "passthrough" },
|
|
241
207
|
{ input: " /compact ", kind: "compact" },
|
|
242
|
-
{ input: " /compact 50k ", kind: "
|
|
208
|
+
{ input: " /compact 50k ", kind: "unknown" },
|
|
243
209
|
{ input: "/models foo", kind: "passthrough" },
|
|
244
210
|
];
|
|
245
211
|
|
|
@@ -228,7 +228,12 @@ mock.module("../agent/loop.js", () => ({
|
|
|
228
228
|
const history = await new Promise<Message[]>((resolve) => {
|
|
229
229
|
pendingRuns.push({ resolve, messages, onEvent });
|
|
230
230
|
});
|
|
231
|
-
return {
|
|
231
|
+
return {
|
|
232
|
+
history,
|
|
233
|
+
exitReason: null,
|
|
234
|
+
appendedNewMessages: history.length > messages.length,
|
|
235
|
+
newMessages: history.slice(messages.length),
|
|
236
|
+
};
|
|
232
237
|
}
|
|
233
238
|
},
|
|
234
239
|
}));
|
|
@@ -366,6 +366,90 @@ describe("surface action delivery to assistant", () => {
|
|
|
366
366
|
expect(JSON.stringify(completeMsg)).not.toContain(largeBase64);
|
|
367
367
|
});
|
|
368
368
|
|
|
369
|
+
test("choice surface broadcasts ui_surface_complete on action", async () => {
|
|
370
|
+
const sent: ServerMessage[] = [];
|
|
371
|
+
const ctx = makeContext(sent);
|
|
372
|
+
|
|
373
|
+
const showResult = await surfaceProxyResolver(ctx, "ui_show", {
|
|
374
|
+
surface_type: "choice",
|
|
375
|
+
title: "Pick an outcome",
|
|
376
|
+
data: {
|
|
377
|
+
options: [
|
|
378
|
+
{ id: "inbox", title: "Clean up my inbox" },
|
|
379
|
+
{ id: "calendar", title: "Plan my week" },
|
|
380
|
+
],
|
|
381
|
+
},
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
expect(showResult.isError).toBe(false);
|
|
385
|
+
expect(showResult.yieldToUser).toBe(true);
|
|
386
|
+
|
|
387
|
+
const showMessage = sent.find(
|
|
388
|
+
(msg): msg is UiSurfaceShow => msg.type === "ui_surface_show",
|
|
389
|
+
) as UiSurfaceShow;
|
|
390
|
+
const surfaceId = showMessage.surfaceId;
|
|
391
|
+
expect(ctx.pendingSurfaceActions.has(surfaceId)).toBe(true);
|
|
392
|
+
|
|
393
|
+
await handleSurfaceAction(ctx, surfaceId, "inbox", {
|
|
394
|
+
choiceId: "inbox",
|
|
395
|
+
choiceTitle: "Clean up my inbox",
|
|
396
|
+
selectedIds: ["inbox"],
|
|
397
|
+
selectedTitles: ["Clean up my inbox"],
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
const completeMsg = broadcastedMessages.find(
|
|
401
|
+
(m) =>
|
|
402
|
+
(m as unknown as Record<string, unknown>).type ===
|
|
403
|
+
"ui_surface_complete" &&
|
|
404
|
+
(m as unknown as Record<string, unknown>).surfaceId === surfaceId,
|
|
405
|
+
) as unknown as Record<string, unknown> | undefined;
|
|
406
|
+
expect(completeMsg).toBeDefined();
|
|
407
|
+
expect(completeMsg?.conversationId).toBe("conv-1");
|
|
408
|
+
expect(completeMsg?.summary).toBe('User chose: "Clean up my inbox"');
|
|
409
|
+
expect(ctx.pendingSurfaceActions.has(surfaceId)).toBe(false);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
test("oauth_connect surface broadcasts ui_surface_complete on action", async () => {
|
|
413
|
+
const sent: ServerMessage[] = [];
|
|
414
|
+
const ctx = makeContext(sent);
|
|
415
|
+
|
|
416
|
+
const showResult = await surfaceProxyResolver(ctx, "ui_show", {
|
|
417
|
+
surface_type: "oauth_connect",
|
|
418
|
+
title: "Connect Google",
|
|
419
|
+
data: {
|
|
420
|
+
providerKey: "google",
|
|
421
|
+
displayName: "Google",
|
|
422
|
+
},
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
expect(showResult.isError).toBe(false);
|
|
426
|
+
expect(showResult.yieldToUser).toBe(true);
|
|
427
|
+
|
|
428
|
+
const showMessage = sent.find(
|
|
429
|
+
(msg): msg is UiSurfaceShow => msg.type === "ui_surface_show",
|
|
430
|
+
) as UiSurfaceShow;
|
|
431
|
+
const surfaceId = showMessage.surfaceId;
|
|
432
|
+
expect(ctx.pendingSurfaceActions.has(surfaceId)).toBe(true);
|
|
433
|
+
|
|
434
|
+
await handleSurfaceAction(ctx, surfaceId, "connect", {
|
|
435
|
+
status: "connected",
|
|
436
|
+
providerKey: "google",
|
|
437
|
+
providerLabel: "Google",
|
|
438
|
+
accountLabel: "user@example.com",
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
const completeMsg = broadcastedMessages.find(
|
|
442
|
+
(m) =>
|
|
443
|
+
(m as unknown as Record<string, unknown>).type ===
|
|
444
|
+
"ui_surface_complete" &&
|
|
445
|
+
(m as unknown as Record<string, unknown>).surfaceId === surfaceId,
|
|
446
|
+
) as unknown as Record<string, unknown> | undefined;
|
|
447
|
+
expect(completeMsg).toBeDefined();
|
|
448
|
+
expect(completeMsg?.conversationId).toBe("conv-1");
|
|
449
|
+
expect(completeMsg?.summary).toBe("Connected Google: user@example.com");
|
|
450
|
+
expect(ctx.pendingSurfaceActions.has(surfaceId)).toBe(false);
|
|
451
|
+
});
|
|
452
|
+
|
|
369
453
|
test("table surface does NOT broadcast ui_surface_complete (not one-shot)", async () => {
|
|
370
454
|
const sent: ServerMessage[] = [];
|
|
371
455
|
const ctx = makeContext(sent);
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
1
|
import { afterAll, beforeEach, describe, expect, test } from "bun:test";
|
|
4
2
|
|
|
5
3
|
import {
|
|
@@ -18,6 +16,7 @@ import { assistantEventHub } from "../runtime/assistant-event-hub.js";
|
|
|
18
16
|
import { ROUTES as CONVERSATION_LIST_ROUTES } from "../runtime/routes/conversation-list-routes.js";
|
|
19
17
|
import { ROUTES as CONVERSATION_MANAGEMENT_ROUTES } from "../runtime/routes/conversation-management-routes.js";
|
|
20
18
|
import type { RouteDefinition } from "../runtime/routes/types.js";
|
|
19
|
+
import { publishConversationTitleChanged } from "../runtime/sync/resource-sync-events.js";
|
|
21
20
|
import { resetDbForTesting } from "./db-test-helpers.js";
|
|
22
21
|
import { waitFor } from "./helpers/wait-for.js";
|
|
23
22
|
|
|
@@ -122,20 +121,33 @@ describe("conversation sync tags", () => {
|
|
|
122
121
|
).toBe(false);
|
|
123
122
|
});
|
|
124
123
|
|
|
125
|
-
test("
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
124
|
+
test("auto-title generation emits the typed title event and a metadata-only sync tag (no list umbrella)", async () => {
|
|
125
|
+
// Auto-title generation (first-pass on prompt submit, second-pass
|
|
126
|
+
// regeneration, bootstrap, and voice) persists via the title service and
|
|
127
|
+
// broadcasts through `publishConversationTitleChanged` — the same helper
|
|
128
|
+
// the rename route uses. Like a rename, generation is content-only: the
|
|
129
|
+
// row stays in place and only the title flips, so web patches the cached
|
|
130
|
+
// row from the typed `conversation_title_updated` event and the per-
|
|
131
|
+
// conversation metadata tag is the belt-and-suspenders signal. The list
|
|
132
|
+
// umbrella is deliberately omitted so web never redrains the paginated
|
|
133
|
+
// list for a title change.
|
|
134
|
+
const conversation = createConversation("Generating…");
|
|
135
|
+
|
|
136
|
+
const received = await captureEvents(() => {
|
|
137
|
+
publishConversationTitleChanged(conversation.id, "Generated title");
|
|
138
|
+
}, 2);
|
|
139
|
+
|
|
140
|
+
expect(received.map((event) => event.message.type)).toEqual([
|
|
141
|
+
"conversation_title_updated",
|
|
142
|
+
"sync_changed",
|
|
143
|
+
]);
|
|
144
|
+
expect(received[1]!.message).toEqual({
|
|
145
|
+
type: "sync_changed",
|
|
146
|
+
tags: [conversationMetadataSyncTag(conversation.id)],
|
|
147
|
+
});
|
|
148
|
+
expect((received[1]!.message as { tags: string[] }).tags).not.toContain(
|
|
149
|
+
SYNC_TAGS.conversationsList,
|
|
129
150
|
);
|
|
130
|
-
const titleUpdateBlocks =
|
|
131
|
-
source.match(
|
|
132
|
-
/type: "conversation_title_updated"[\s\S]{0,500}?type: "sync_changed"[\s\S]{0,250}?tags: \[[\s\S]*?\]/g,
|
|
133
|
-
) ?? [];
|
|
134
|
-
|
|
135
|
-
expect(titleUpdateBlocks.length).toBeGreaterThanOrEqual(2);
|
|
136
|
-
for (const block of titleUpdateBlocks) {
|
|
137
|
-
expect(block).not.toContain("SYNC_TAGS.conversationsList");
|
|
138
|
-
}
|
|
139
151
|
});
|
|
140
152
|
|
|
141
153
|
test("create emits a sync_changed with the conversationsList umbrella tag", async () => {
|
|
@@ -51,9 +51,18 @@ mock.module("../util/logger.js", () => ({
|
|
|
51
51
|
}),
|
|
52
52
|
}));
|
|
53
53
|
|
|
54
|
+
const mockPublishConversationTitleChanged = mock(
|
|
55
|
+
(_conversationId: string, _title: string) => {},
|
|
56
|
+
);
|
|
57
|
+
mock.module("../runtime/sync/resource-sync-events.js", () => ({
|
|
58
|
+
publishConversationTitleChanged: mockPublishConversationTitleChanged,
|
|
59
|
+
}));
|
|
60
|
+
|
|
54
61
|
import {
|
|
55
62
|
generateAndPersistConversationTitle,
|
|
63
|
+
queueGenerateConversationTitle,
|
|
56
64
|
regenerateConversationTitle,
|
|
65
|
+
titleMutex,
|
|
57
66
|
} from "../memory/conversation-title-service.js";
|
|
58
67
|
|
|
59
68
|
describe("conversation-title-service", () => {
|
|
@@ -63,6 +72,7 @@ describe("conversation-title-service", () => {
|
|
|
63
72
|
mockGetMessages.mockClear();
|
|
64
73
|
mockUpdateConversationTitle.mockClear();
|
|
65
74
|
mockGetConfiguredProvider.mockClear();
|
|
75
|
+
mockPublishConversationTitleChanged.mockClear();
|
|
66
76
|
});
|
|
67
77
|
|
|
68
78
|
test("uses the BTW side-chain helper for initial title generation", async () => {
|
|
@@ -87,7 +97,7 @@ describe("conversation-title-service", () => {
|
|
|
87
97
|
systemPrompt: expect.stringContaining("conversation titles"),
|
|
88
98
|
tools: [],
|
|
89
99
|
callSite: "conversationTitle",
|
|
90
|
-
timeoutMs:
|
|
100
|
+
timeoutMs: 15_000,
|
|
91
101
|
}),
|
|
92
102
|
);
|
|
93
103
|
expect(mockUpdateConversationTitle).toHaveBeenCalledWith(
|
|
@@ -95,6 +105,12 @@ describe("conversation-title-service", () => {
|
|
|
95
105
|
"Project kickoff",
|
|
96
106
|
1,
|
|
97
107
|
);
|
|
108
|
+
// Emit is service-native: persisting a title broadcasts the update so
|
|
109
|
+
// every title origin (agent loop, bootstrap, voice) updates clients live.
|
|
110
|
+
expect(mockPublishConversationTitleChanged).toHaveBeenCalledWith(
|
|
111
|
+
"conv-1",
|
|
112
|
+
"Project kickoff",
|
|
113
|
+
);
|
|
98
114
|
});
|
|
99
115
|
|
|
100
116
|
test("regeneration extracts text from JSON content blocks", async () => {
|
|
@@ -205,7 +221,7 @@ describe("conversation-title-service", () => {
|
|
|
205
221
|
systemPrompt: expect.stringContaining("conversation titles"),
|
|
206
222
|
tools: [],
|
|
207
223
|
callSite: "conversationTitle",
|
|
208
|
-
timeoutMs:
|
|
224
|
+
timeoutMs: 15_000,
|
|
209
225
|
}),
|
|
210
226
|
);
|
|
211
227
|
expect(mockUpdateConversationTitle).toHaveBeenCalledWith(
|
|
@@ -354,4 +370,121 @@ describe("conversation-title-service", () => {
|
|
|
354
370
|
expect(call.content).not.toContain("do NOT respond");
|
|
355
371
|
expect(call.systemPrompt).toContain("Do NOT respond");
|
|
356
372
|
});
|
|
373
|
+
|
|
374
|
+
test("queueGenerateConversationTitle serializes concurrent calls", async () => {
|
|
375
|
+
const callOrder: string[] = [];
|
|
376
|
+
let resolveFirst!: () => void;
|
|
377
|
+
const firstBlocked = new Promise<void>((r) => {
|
|
378
|
+
resolveFirst = r;
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
// First call: blocks until we release it
|
|
382
|
+
mockRunBtwSidechain.mockImplementationOnce(async () => {
|
|
383
|
+
callOrder.push("first:start");
|
|
384
|
+
await firstBlocked;
|
|
385
|
+
callOrder.push("first:end");
|
|
386
|
+
return {
|
|
387
|
+
text: "Title One",
|
|
388
|
+
hadTextDeltas: true,
|
|
389
|
+
response: {
|
|
390
|
+
content: [{ type: "text", text: "Title One" }],
|
|
391
|
+
model: "test-model",
|
|
392
|
+
usage: { inputTokens: 10, outputTokens: 5 },
|
|
393
|
+
stopReason: "end_turn",
|
|
394
|
+
},
|
|
395
|
+
};
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
// Second call: resolves immediately
|
|
399
|
+
mockRunBtwSidechain.mockImplementationOnce(async () => {
|
|
400
|
+
callOrder.push("second:start");
|
|
401
|
+
return {
|
|
402
|
+
text: "Title Two",
|
|
403
|
+
hadTextDeltas: true,
|
|
404
|
+
response: {
|
|
405
|
+
content: [{ type: "text", text: "Title Two" }],
|
|
406
|
+
model: "test-model",
|
|
407
|
+
usage: { inputTokens: 10, outputTokens: 5 },
|
|
408
|
+
stopReason: "end_turn",
|
|
409
|
+
},
|
|
410
|
+
};
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
const provider = {
|
|
414
|
+
name: "test-provider",
|
|
415
|
+
sendMessage: mock(async () => {
|
|
416
|
+
throw new Error("should not call directly");
|
|
417
|
+
}),
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
// Fire both calls — without serialization both would start immediately
|
|
421
|
+
queueGenerateConversationTitle({
|
|
422
|
+
conversationId: "conv-1",
|
|
423
|
+
provider,
|
|
424
|
+
userMessage: "first message",
|
|
425
|
+
});
|
|
426
|
+
queueGenerateConversationTitle({
|
|
427
|
+
conversationId: "conv-2",
|
|
428
|
+
provider,
|
|
429
|
+
userMessage: "second message",
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
// Let microtasks settle — only the first call should have started
|
|
433
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
434
|
+
expect(callOrder).toEqual(["first:start"]);
|
|
435
|
+
|
|
436
|
+
// Release the first call
|
|
437
|
+
resolveFirst();
|
|
438
|
+
await titleMutex.withLock(async () => {});
|
|
439
|
+
|
|
440
|
+
// Second should have started only after first finished
|
|
441
|
+
expect(callOrder).toEqual(["first:start", "first:end", "second:start"]);
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
test("queue continues processing after a failed call", async () => {
|
|
445
|
+
// First call: throws
|
|
446
|
+
mockRunBtwSidechain.mockImplementationOnce(async () => {
|
|
447
|
+
throw new Error("provider timeout");
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
// Second call: succeeds
|
|
451
|
+
mockRunBtwSidechain.mockImplementationOnce(async () => ({
|
|
452
|
+
text: "Recovery Title",
|
|
453
|
+
hadTextDeltas: true,
|
|
454
|
+
response: {
|
|
455
|
+
content: [{ type: "text", text: "Recovery Title" }],
|
|
456
|
+
model: "test-model",
|
|
457
|
+
usage: { inputTokens: 10, outputTokens: 5 },
|
|
458
|
+
stopReason: "end_turn",
|
|
459
|
+
},
|
|
460
|
+
}));
|
|
461
|
+
|
|
462
|
+
const provider = {
|
|
463
|
+
name: "test-provider",
|
|
464
|
+
sendMessage: mock(async () => {
|
|
465
|
+
throw new Error("should not call directly");
|
|
466
|
+
}),
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
queueGenerateConversationTitle({
|
|
470
|
+
conversationId: "conv-1",
|
|
471
|
+
provider,
|
|
472
|
+
userMessage: "will fail",
|
|
473
|
+
});
|
|
474
|
+
queueGenerateConversationTitle({
|
|
475
|
+
conversationId: "conv-2",
|
|
476
|
+
provider,
|
|
477
|
+
userMessage: "will succeed",
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
await titleMutex.withLock(async () => {});
|
|
481
|
+
|
|
482
|
+
// Both calls went through — failure didn't break the chain
|
|
483
|
+
expect(mockRunBtwSidechain).toHaveBeenCalledTimes(2);
|
|
484
|
+
// Second conversation got a proper title
|
|
485
|
+
const secondUpdate = (
|
|
486
|
+
mockUpdateConversationTitle.mock.calls as unknown as string[][]
|
|
487
|
+
).find((c) => c[0] === "conv-2" && c[1] === "Recovery Title");
|
|
488
|
+
expect(secondUpdate).toBeTruthy();
|
|
489
|
+
});
|
|
357
490
|
});
|
|
@@ -265,7 +265,12 @@ mock.module("../agent/loop.js", () => ({
|
|
|
265
265
|
content: [{ type: "text", text: "ok" }],
|
|
266
266
|
};
|
|
267
267
|
onEvent({ type: "message_complete", message: assistantMessage });
|
|
268
|
-
return {
|
|
268
|
+
return {
|
|
269
|
+
history: [...messages, assistantMessage],
|
|
270
|
+
exitReason: null,
|
|
271
|
+
appendedNewMessages: true,
|
|
272
|
+
newMessages: [assistantMessage],
|
|
273
|
+
};
|
|
269
274
|
}
|
|
270
275
|
},
|
|
271
276
|
}));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
2
|
|
|
3
3
|
import type {
|
|
4
4
|
ContentBlock,
|
|
@@ -221,6 +221,64 @@ import {
|
|
|
221
221
|
OpenAIResponsesProvider,
|
|
222
222
|
} from "../providers/openai/client.js";
|
|
223
223
|
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
// App-side web_search provider adapters (Brave/Perplexity/Tavily)
|
|
226
|
+
//
|
|
227
|
+
// Exercise the real `web-search.ts` execute path with a mocked config, provider
|
|
228
|
+
// key, and global fetch. The logger is mocked to capture structured warnings so
|
|
229
|
+
// we can assert the `web_search_backend_failure` telemetry (ATL-727).
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
|
|
232
|
+
let mockWebSearchProvider: string = "brave";
|
|
233
|
+
let mockProviderKey: string | undefined = "test-key";
|
|
234
|
+
const capturedWarnLogs: Record<string, unknown>[] = [];
|
|
235
|
+
|
|
236
|
+
const realConfigLoader = await import("../config/loader.js");
|
|
237
|
+
mock.module("../config/loader.js", () => ({
|
|
238
|
+
...realConfigLoader,
|
|
239
|
+
getConfig: () => ({
|
|
240
|
+
services: { "web-search": { provider: mockWebSearchProvider } },
|
|
241
|
+
}),
|
|
242
|
+
}));
|
|
243
|
+
|
|
244
|
+
const realSecureKeys = await import("../security/secure-keys.js");
|
|
245
|
+
mock.module("../security/secure-keys.js", () => ({
|
|
246
|
+
...realSecureKeys,
|
|
247
|
+
getProviderKeyAsync: async () => mockProviderKey,
|
|
248
|
+
}));
|
|
249
|
+
|
|
250
|
+
const realLogger = await import("../util/logger.js");
|
|
251
|
+
mock.module("../util/logger.js", () => ({
|
|
252
|
+
...realLogger,
|
|
253
|
+
getLogger: () =>
|
|
254
|
+
new Proxy({} as Record<string, unknown>, {
|
|
255
|
+
get: (_target, prop) => {
|
|
256
|
+
if (prop === "warn") {
|
|
257
|
+
return (obj: Record<string, unknown>) => {
|
|
258
|
+
capturedWarnLogs.push(obj);
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
return () => {};
|
|
262
|
+
},
|
|
263
|
+
}),
|
|
264
|
+
}));
|
|
265
|
+
|
|
266
|
+
const { webSearchTool } = await import("../tools/network/web-search.js");
|
|
267
|
+
const { WEB_SEARCH_BACKEND_FAILURE_MESSAGE } = await import(
|
|
268
|
+
"../tools/network/web-search-error.js"
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
function executeWebSearch(input: Record<string, unknown>) {
|
|
272
|
+
return webSearchTool.execute(input, {} as never);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function executeWebSearchWithSignal(
|
|
276
|
+
input: Record<string, unknown>,
|
|
277
|
+
signal: AbortSignal,
|
|
278
|
+
) {
|
|
279
|
+
return webSearchTool.execute(input, { signal } as never);
|
|
280
|
+
}
|
|
281
|
+
|
|
224
282
|
// ---------------------------------------------------------------------------
|
|
225
283
|
// OpenAI Responses API provider tests
|
|
226
284
|
// ---------------------------------------------------------------------------
|
|
@@ -604,3 +662,158 @@ describe("Cross-Provider Web Search — Gemini", () => {
|
|
|
604
662
|
expect(functionCallParts).toHaveLength(0);
|
|
605
663
|
});
|
|
606
664
|
});
|
|
665
|
+
|
|
666
|
+
// ---------------------------------------------------------------------------
|
|
667
|
+
// App-side provider backend-failure normalization (ATL-727)
|
|
668
|
+
// ---------------------------------------------------------------------------
|
|
669
|
+
|
|
670
|
+
describe("Cross-Provider Web Search — app-side backend failure normalization", () => {
|
|
671
|
+
let originalFetch: typeof globalThis.fetch;
|
|
672
|
+
|
|
673
|
+
beforeEach(() => {
|
|
674
|
+
originalFetch = globalThis.fetch;
|
|
675
|
+
mockWebSearchProvider = "brave";
|
|
676
|
+
mockProviderKey = "test-key";
|
|
677
|
+
capturedWarnLogs.length = 0;
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
afterEach(() => {
|
|
681
|
+
globalThis.fetch = originalFetch;
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
function backendFailureLog() {
|
|
685
|
+
return capturedWarnLogs.find(
|
|
686
|
+
(entry) => entry.event === "web_search_backend_failure",
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
test("503 from provider yields friendly recoverable copy in content + errorMessage, logs raw 503, no body leak", async () => {
|
|
691
|
+
const rawBody = '{"error":"upstream exploded","trace":"do-not-leak"}';
|
|
692
|
+
globalThis.fetch = (async () =>
|
|
693
|
+
new Response(rawBody, { status: 503 })) as unknown as typeof fetch;
|
|
694
|
+
|
|
695
|
+
const result = await executeWebSearch({ query: "needle in a haystack" });
|
|
696
|
+
|
|
697
|
+
expect(result.isError).toBe(true);
|
|
698
|
+
expect(result.content).toBe(WEB_SEARCH_BACKEND_FAILURE_MESSAGE);
|
|
699
|
+
const meta = result.activityMetadata?.webSearch;
|
|
700
|
+
expect(meta?.errorMessage).toBe(WEB_SEARCH_BACKEND_FAILURE_MESSAGE);
|
|
701
|
+
expect(meta?.results).toEqual([]);
|
|
702
|
+
expect(meta?.resultCount).toBe(0);
|
|
703
|
+
|
|
704
|
+
const logEntry = backendFailureLog();
|
|
705
|
+
expect(logEntry).toBeDefined();
|
|
706
|
+
expect(logEntry!.provider).toBe("brave");
|
|
707
|
+
expect(logEntry!.errorCategory).toBe("backend_unavailable");
|
|
708
|
+
expect(logEntry!.fallbackShown).toBe(true);
|
|
709
|
+
expect(logEntry!.queryLength).toBe("needle in a haystack".length);
|
|
710
|
+
expect(String(logEntry!.rawDetail)).toContain("503");
|
|
711
|
+
// Provider diagnostic body is preserved in internal telemetry rawDetail.
|
|
712
|
+
expect(String(logEntry!.rawDetail)).toContain("upstream exploded");
|
|
713
|
+
expect(String(logEntry!.rawDetail)).toContain("do-not-leak");
|
|
714
|
+
|
|
715
|
+
// Raw provider body must never reach user-facing fields.
|
|
716
|
+
expect(result.content).not.toContain("upstream exploded");
|
|
717
|
+
expect(result.content).not.toContain("do-not-leak");
|
|
718
|
+
expect(meta?.errorMessage).not.toContain("upstream exploded");
|
|
719
|
+
expect(meta?.errorMessage).not.toContain("do-not-leak");
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
test("thrown network error yields the same friendly backend result", async () => {
|
|
723
|
+
globalThis.fetch = (async () => {
|
|
724
|
+
throw new TypeError("fetch failed");
|
|
725
|
+
}) as unknown as typeof fetch;
|
|
726
|
+
|
|
727
|
+
const result = await executeWebSearch({ query: "offline" });
|
|
728
|
+
|
|
729
|
+
expect(result.isError).toBe(true);
|
|
730
|
+
expect(result.content).toBe(WEB_SEARCH_BACKEND_FAILURE_MESSAGE);
|
|
731
|
+
expect(result.activityMetadata?.webSearch?.errorMessage).toBe(
|
|
732
|
+
WEB_SEARCH_BACKEND_FAILURE_MESSAGE,
|
|
733
|
+
);
|
|
734
|
+
const logEntry = backendFailureLog();
|
|
735
|
+
expect(logEntry).toBeDefined();
|
|
736
|
+
expect(logEntry!.errorCategory).toBe("backend_unavailable");
|
|
737
|
+
expect(result.content).not.toContain("fetch failed");
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
test("401 invalid-key preserves the specific message, not the backend copy", async () => {
|
|
741
|
+
globalThis.fetch = (async () =>
|
|
742
|
+
new Response("Unauthorized", { status: 401 })) as unknown as typeof fetch;
|
|
743
|
+
|
|
744
|
+
const result = await executeWebSearch({ query: "bad key" });
|
|
745
|
+
|
|
746
|
+
expect(result.isError).toBe(true);
|
|
747
|
+
expect(result.content).toContain("Invalid or expired Brave Search API key");
|
|
748
|
+
expect(result.content).not.toBe(WEB_SEARCH_BACKEND_FAILURE_MESSAGE);
|
|
749
|
+
expect(result.activityMetadata?.webSearch?.errorMessage).not.toBe(
|
|
750
|
+
WEB_SEARCH_BACKEND_FAILURE_MESSAGE,
|
|
751
|
+
);
|
|
752
|
+
expect(backendFailureLog()).toBeUndefined();
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
test("HTTP 200 with zero results stays a success (unchanged)", async () => {
|
|
756
|
+
globalThis.fetch = (async () =>
|
|
757
|
+
new Response(JSON.stringify({ web: { results: [] } }), {
|
|
758
|
+
status: 200,
|
|
759
|
+
headers: { "content-type": "application/json" },
|
|
760
|
+
})) as unknown as typeof fetch;
|
|
761
|
+
|
|
762
|
+
const result = await executeWebSearch({ query: "no hits" });
|
|
763
|
+
|
|
764
|
+
expect(result.isError).toBe(false);
|
|
765
|
+
expect(result.activityMetadata?.webSearch?.errorMessage).toBeUndefined();
|
|
766
|
+
expect(result.content).toContain("No results found");
|
|
767
|
+
expect(backendFailureLog()).toBeUndefined();
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
test("post-retry 429 yields the friendly recoverable copy and preserves body in rawDetail", async () => {
|
|
771
|
+
const rawBody = '{"error":"quota burned","retryHint":"do-not-leak-429"}';
|
|
772
|
+
globalThis.fetch = (async () =>
|
|
773
|
+
new Response(rawBody, {
|
|
774
|
+
status: 429,
|
|
775
|
+
headers: { "retry-after": "0" },
|
|
776
|
+
})) as unknown as typeof fetch;
|
|
777
|
+
|
|
778
|
+
const result = await executeWebSearch({ query: "rate limited" });
|
|
779
|
+
|
|
780
|
+
expect(result.isError).toBe(true);
|
|
781
|
+
expect(result.content).toBe(WEB_SEARCH_BACKEND_FAILURE_MESSAGE);
|
|
782
|
+
const meta = result.activityMetadata?.webSearch;
|
|
783
|
+
expect(meta?.errorMessage).toBe(WEB_SEARCH_BACKEND_FAILURE_MESSAGE);
|
|
784
|
+
const logEntry = backendFailureLog();
|
|
785
|
+
expect(logEntry).toBeDefined();
|
|
786
|
+
expect(logEntry!.errorCategory).toBe("rate_limited");
|
|
787
|
+
expect(String(logEntry!.rawDetail)).toContain("429");
|
|
788
|
+
// Provider diagnostic body is preserved in internal telemetry rawDetail.
|
|
789
|
+
expect(String(logEntry!.rawDetail)).toContain("quota burned");
|
|
790
|
+
expect(String(logEntry!.rawDetail)).toContain("do-not-leak-429");
|
|
791
|
+
|
|
792
|
+
// Raw provider body must never reach user-facing fields.
|
|
793
|
+
expect(result.content).not.toContain("quota burned");
|
|
794
|
+
expect(result.content).not.toContain("do-not-leak-429");
|
|
795
|
+
expect(meta?.errorMessage).not.toContain("quota burned");
|
|
796
|
+
expect(meta?.errorMessage).not.toContain("do-not-leak-429");
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
test("caller abort re-throws instead of producing a backend failure (no telemetry)", async () => {
|
|
800
|
+
const controller = new AbortController();
|
|
801
|
+
controller.abort();
|
|
802
|
+
|
|
803
|
+
// A caller-aborted request surfaces an AbortError from fetch.
|
|
804
|
+
globalThis.fetch = (async () => {
|
|
805
|
+
const abortError = new Error("The operation was aborted");
|
|
806
|
+
abortError.name = "AbortError";
|
|
807
|
+
throw abortError;
|
|
808
|
+
}) as unknown as typeof fetch;
|
|
809
|
+
|
|
810
|
+
// The cancellation must re-throw so the executor's abort handling takes
|
|
811
|
+
// over — NOT resolve to the friendly backend-failure result.
|
|
812
|
+
await expect(
|
|
813
|
+
executeWebSearchWithSignal({ query: "cancel me" }, controller.signal),
|
|
814
|
+
).rejects.toThrow();
|
|
815
|
+
|
|
816
|
+
// No spurious backend-failure telemetry for a user/external cancellation.
|
|
817
|
+
expect(backendFailureLog()).toBeUndefined();
|
|
818
|
+
});
|
|
819
|
+
});
|