@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
|
@@ -4,6 +4,7 @@ import { describe, expect, test } from "bun:test";
|
|
|
4
4
|
import { eq } from "drizzle-orm";
|
|
5
5
|
import { drizzle } from "drizzle-orm/bun-sqlite";
|
|
6
6
|
|
|
7
|
+
import { migrateScheduleSourceConversation } from "../memory/migrations/270-schedule-source-conversation.js";
|
|
7
8
|
import * as schema from "../memory/schema.js";
|
|
8
9
|
import { scheduleJobs } from "../memory/schema.js";
|
|
9
10
|
|
|
@@ -38,6 +39,7 @@ describe("schedule_syntax column migration", () => {
|
|
|
38
39
|
retry_count INTEGER NOT NULL DEFAULT 0,
|
|
39
40
|
max_retries INTEGER NOT NULL DEFAULT 3,
|
|
40
41
|
retry_backoff_ms INTEGER NOT NULL DEFAULT 60000,
|
|
42
|
+
created_from_conversation_id TEXT,
|
|
41
43
|
created_by TEXT NOT NULL,
|
|
42
44
|
mode TEXT NOT NULL DEFAULT 'execute',
|
|
43
45
|
routing_intent TEXT NOT NULL DEFAULT 'all_channels',
|
|
@@ -134,6 +136,7 @@ describe("schedule_syntax column migration", () => {
|
|
|
134
136
|
} catch {
|
|
135
137
|
/* already exists */
|
|
136
138
|
}
|
|
139
|
+
migrateScheduleSourceConversation(db);
|
|
137
140
|
|
|
138
141
|
const row = db
|
|
139
142
|
.select()
|
|
@@ -197,6 +200,8 @@ describe("schedule_syntax column migration", () => {
|
|
|
197
200
|
} catch {
|
|
198
201
|
/* ok */
|
|
199
202
|
}
|
|
203
|
+
migrateScheduleSourceConversation(db);
|
|
204
|
+
migrateScheduleSourceConversation(db);
|
|
200
205
|
|
|
201
206
|
const now = Date.now();
|
|
202
207
|
raw.exec(
|
|
@@ -94,10 +94,14 @@ function createSlackTurnContext(): MessagingConversationContext {
|
|
|
94
94
|
drain: () => [],
|
|
95
95
|
size: () => 0,
|
|
96
96
|
} as unknown as MessageQueue;
|
|
97
|
+
let processing = false;
|
|
97
98
|
return {
|
|
98
99
|
conversationId: "conv-dm-test",
|
|
99
100
|
messages: [],
|
|
100
|
-
|
|
101
|
+
isProcessing: () => processing,
|
|
102
|
+
setProcessing: (value: boolean) => {
|
|
103
|
+
processing = value;
|
|
104
|
+
},
|
|
101
105
|
abortController: null,
|
|
102
106
|
queue: queueStub,
|
|
103
107
|
getTurnChannelContext: () => channel,
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the default `empty-response` plugin's `stop` hook.
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - The default hook's binary decision for the canonical cases: empty turn
|
|
6
|
+
* after a prior tool-use turn → continue (with the canonical nudge text);
|
|
7
|
+
* visible text → stop; prior turn already delivered visible text → stop;
|
|
8
|
+
* first model call with no prior turn → stop; provider refusal → continue
|
|
9
|
+
* (with the refusal-specific nudge text); refusal-but-recovered → stop.
|
|
10
|
+
* - The hook scopes its prior-turn signals to the current response cycle (the
|
|
11
|
+
* messages after the last genuine user prompt), so visible text from the
|
|
12
|
+
* inbound conversation does not suppress the nudge.
|
|
13
|
+
* - When the hook continues, it appends the nudge as a `user` message to
|
|
14
|
+
* `messages`.
|
|
15
|
+
* - End-to-end through `runHook` + the registry: registering the default
|
|
16
|
+
* plugin makes the hook fire, and a later-registered user hook chains after
|
|
17
|
+
* it and can read/override the decision.
|
|
18
|
+
*
|
|
19
|
+
* The loop's actual side-effects (retry-budget cap, history splice, log
|
|
20
|
+
* emission) live in `agent/loop.ts` and are covered by integration tests in
|
|
21
|
+
* `conversation-agent-loop.test.ts` / `agent-loop.test.ts`. This file isolates
|
|
22
|
+
* the hook.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { beforeEach, describe, expect, test } from "bun:test";
|
|
26
|
+
|
|
27
|
+
import { HOOKS } from "../plugin-api/constants.js";
|
|
28
|
+
import type { PluginLogger, StopContext } from "../plugin-api/types.js";
|
|
29
|
+
import {
|
|
30
|
+
NUDGE_TEXT,
|
|
31
|
+
REFUSAL_NUDGE_TEXT,
|
|
32
|
+
} from "../plugins/defaults/empty-response/hooks/stop.js";
|
|
33
|
+
import stop from "../plugins/defaults/empty-response/hooks/stop.js";
|
|
34
|
+
import { defaultEmptyResponsePlugin } from "../plugins/defaults/empty-response/register.js";
|
|
35
|
+
import { runHook } from "../plugins/pipeline.js";
|
|
36
|
+
import {
|
|
37
|
+
registerPlugin,
|
|
38
|
+
resetPluginRegistryForTests,
|
|
39
|
+
} from "../plugins/registry.js";
|
|
40
|
+
import type { ContentBlock, Message } from "../providers/types.js";
|
|
41
|
+
|
|
42
|
+
// ─── Fixtures ────────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
const noopLogger: PluginLogger = {
|
|
45
|
+
info: () => {},
|
|
46
|
+
warn: () => {},
|
|
47
|
+
error: () => {},
|
|
48
|
+
debug: () => {},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const emptyTextBlock: ContentBlock = { type: "text", text: " " };
|
|
52
|
+
|
|
53
|
+
/** A prior assistant turn that issued a tool call but no visible text. */
|
|
54
|
+
const priorToolUseTurn: Message = {
|
|
55
|
+
role: "assistant",
|
|
56
|
+
content: [{ type: "tool_use", id: "tu_1", name: "read_file", input: {} }],
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** A prior assistant turn that delivered visible text to the user. */
|
|
60
|
+
const priorVisibleTextTurn: Message = {
|
|
61
|
+
role: "assistant",
|
|
62
|
+
content: [{ type: "text", text: "here is what I found earlier" }],
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/** A genuine user prompt — the boundary that opens a new response cycle. */
|
|
66
|
+
function userPrompt(text: string): Message {
|
|
67
|
+
return { role: "user", content: [{ type: "text", text }] };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function makeCtx(overrides: Partial<StopContext> = {}): StopContext {
|
|
71
|
+
return {
|
|
72
|
+
conversationId: "conv-stop",
|
|
73
|
+
messages: [],
|
|
74
|
+
responseContent: [],
|
|
75
|
+
stopReason: null,
|
|
76
|
+
decision: "stop",
|
|
77
|
+
logger: noopLogger,
|
|
78
|
+
...overrides,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ─── Default decisions ───────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
describe("empty-response stop hook — default decisions", () => {
|
|
85
|
+
test("empty turn after a prior tool-use turn → continue with canonical nudge", async () => {
|
|
86
|
+
// GIVEN a run that already issued a tool call, then returned an empty
|
|
87
|
+
// (whitespace-only) assistant turn with no visible text.
|
|
88
|
+
const ctx = makeCtx({
|
|
89
|
+
messages: [priorToolUseTurn],
|
|
90
|
+
responseContent: [emptyTextBlock],
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// WHEN the default stop hook runs.
|
|
94
|
+
await stop(ctx);
|
|
95
|
+
|
|
96
|
+
// THEN it asks the loop to continue and appends the canonical nudge.
|
|
97
|
+
expect(ctx.decision).toBe("continue");
|
|
98
|
+
expect(ctx.messages.at(-1)).toEqual({
|
|
99
|
+
role: "user",
|
|
100
|
+
content: [{ type: "text", text: NUDGE_TEXT }],
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("turn contains visible text → stop", async () => {
|
|
105
|
+
// GIVEN a turn that delivered visible text.
|
|
106
|
+
const ctx = makeCtx({
|
|
107
|
+
messages: [priorToolUseTurn],
|
|
108
|
+
responseContent: [{ type: "text", text: "here is a summary" }],
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// WHEN the hook runs.
|
|
112
|
+
await stop(ctx);
|
|
113
|
+
|
|
114
|
+
// THEN the decision stays at stop and nothing is appended.
|
|
115
|
+
expect(ctx.decision).toBe("stop");
|
|
116
|
+
expect(ctx.messages).toEqual([priorToolUseTurn]);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("prior turn already delivered visible text → stop", async () => {
|
|
120
|
+
// GIVEN the model said its piece in an earlier turn this run, then ended
|
|
121
|
+
// with a side-effect tool and returned empty. Nudging would force a
|
|
122
|
+
// verbatim re-send of text the user already saw.
|
|
123
|
+
const ctx = makeCtx({
|
|
124
|
+
messages: [userPrompt("do X"), priorVisibleTextTurn, priorToolUseTurn],
|
|
125
|
+
responseContent: [],
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// WHEN the hook runs.
|
|
129
|
+
await stop(ctx);
|
|
130
|
+
|
|
131
|
+
// THEN the decision stays at stop.
|
|
132
|
+
expect(ctx.decision).toBe("stop");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("visible text before the last user prompt is ignored → continue", async () => {
|
|
136
|
+
// GIVEN the inbound conversation already contains an assistant turn with
|
|
137
|
+
// visible text, but it precedes this run's user prompt. The current cycle
|
|
138
|
+
// (after that prompt) holds only a tool-use turn, so the earlier text
|
|
139
|
+
// belongs to the prior conversation and must not suppress the nudge.
|
|
140
|
+
const ctx = makeCtx({
|
|
141
|
+
messages: [
|
|
142
|
+
priorVisibleTextTurn,
|
|
143
|
+
userPrompt("do the next thing"),
|
|
144
|
+
priorToolUseTurn,
|
|
145
|
+
],
|
|
146
|
+
responseContent: [],
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// WHEN the hook runs.
|
|
150
|
+
await stop(ctx);
|
|
151
|
+
|
|
152
|
+
// THEN it continues — the cycle scope sees only the tool-use turn, not the
|
|
153
|
+
// inbound conversation's visible text.
|
|
154
|
+
expect(ctx.decision).toBe("continue");
|
|
155
|
+
expect(ctx.messages.at(-1)).toEqual({
|
|
156
|
+
role: "user",
|
|
157
|
+
content: [{ type: "text", text: NUDGE_TEXT }],
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("first model call with no prior turn → stop", async () => {
|
|
162
|
+
// GIVEN an empty first assistant response with no prior turn this run and
|
|
163
|
+
// no refusal — not the pattern the organic-empty-turn nudge guards against.
|
|
164
|
+
const ctx = makeCtx({ messages: [], responseContent: [] });
|
|
165
|
+
|
|
166
|
+
// WHEN the hook runs.
|
|
167
|
+
await stop(ctx);
|
|
168
|
+
|
|
169
|
+
// THEN the decision stays at stop.
|
|
170
|
+
expect(ctx.decision).toBe("stop");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// ─── Refusal stop ──────────────────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
test("refusal on the first call with no content → continue with refusal nudge", async () => {
|
|
176
|
+
// GIVEN the canonical failure mode: the provider's safety classifier zeros
|
|
177
|
+
// the response on the very first model call, returning `stopReason:
|
|
178
|
+
// "refusal"` and no visible text.
|
|
179
|
+
const ctx = makeCtx({
|
|
180
|
+
messages: [],
|
|
181
|
+
responseContent: [],
|
|
182
|
+
stopReason: "refusal",
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// WHEN the hook runs.
|
|
186
|
+
await stop(ctx);
|
|
187
|
+
|
|
188
|
+
// THEN it continues with the refusal-specific nudge, even with no prior turn.
|
|
189
|
+
expect(ctx.decision).toBe("continue");
|
|
190
|
+
expect(ctx.messages.at(-1)).toEqual({
|
|
191
|
+
role: "user",
|
|
192
|
+
content: [{ type: "text", text: REFUSAL_NUDGE_TEXT }],
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("refusal with a thinking-only block still continues", async () => {
|
|
197
|
+
// GIVEN a refusal whose only content is a thinking block — the user sees
|
|
198
|
+
// nothing.
|
|
199
|
+
const ctx = makeCtx({
|
|
200
|
+
messages: [],
|
|
201
|
+
responseContent: [
|
|
202
|
+
{ type: "thinking", thinking: "...", signature: "sig" },
|
|
203
|
+
],
|
|
204
|
+
stopReason: "refusal",
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// WHEN the hook runs.
|
|
208
|
+
await stop(ctx);
|
|
209
|
+
|
|
210
|
+
// THEN it continues with the refusal-specific nudge.
|
|
211
|
+
expect(ctx.decision).toBe("continue");
|
|
212
|
+
expect(ctx.messages.at(-1)).toEqual({
|
|
213
|
+
role: "user",
|
|
214
|
+
content: [{ type: "text", text: REFUSAL_NUDGE_TEXT }],
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("refusal but visible text present → stop (model recovered)", async () => {
|
|
219
|
+
// GIVEN a refusal that still delivered some visible text before refusing —
|
|
220
|
+
// the user has something to see.
|
|
221
|
+
const ctx = makeCtx({
|
|
222
|
+
responseContent: [{ type: "text", text: "partial answer" }],
|
|
223
|
+
stopReason: "refusal",
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// WHEN the hook runs.
|
|
227
|
+
await stop(ctx);
|
|
228
|
+
|
|
229
|
+
// THEN the decision stays at stop.
|
|
230
|
+
expect(ctx.decision).toBe("stop");
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("refusal beats the post-tool-empty nudge wording", async () => {
|
|
234
|
+
// GIVEN conditions that would trip both the refusal branch and the
|
|
235
|
+
// post-tool-empty branch.
|
|
236
|
+
const ctx = makeCtx({
|
|
237
|
+
messages: [priorToolUseTurn],
|
|
238
|
+
responseContent: [],
|
|
239
|
+
stopReason: "refusal",
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// WHEN the hook runs.
|
|
243
|
+
await stop(ctx);
|
|
244
|
+
|
|
245
|
+
// THEN refusal wins because its wording is more accurate.
|
|
246
|
+
expect(ctx.decision).toBe("continue");
|
|
247
|
+
expect(ctx.messages.at(-1)).toEqual({
|
|
248
|
+
role: "user",
|
|
249
|
+
content: [{ type: "text", text: REFUSAL_NUDGE_TEXT }],
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// ─── Via runHook + registry ──────────────────────────────────────────────────
|
|
255
|
+
|
|
256
|
+
describe("empty-response stop hook — via runHook", () => {
|
|
257
|
+
beforeEach(() => {
|
|
258
|
+
resetPluginRegistryForTests();
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("registering the default plugin makes the hook continue on empty-after-tools", async () => {
|
|
262
|
+
// GIVEN the default empty-response plugin is registered.
|
|
263
|
+
registerPlugin(defaultEmptyResponsePlugin);
|
|
264
|
+
|
|
265
|
+
// WHEN the stop chain runs over an empty-after-tools context.
|
|
266
|
+
const result = await runHook<StopContext>(
|
|
267
|
+
HOOKS.STOP,
|
|
268
|
+
makeCtx({ messages: [priorToolUseTurn], responseContent: [] }),
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
// THEN the chain settles on continue with the canonical nudge appended.
|
|
272
|
+
expect(result.decision).toBe("continue");
|
|
273
|
+
expect(result.messages.at(-1)).toEqual({
|
|
274
|
+
role: "user",
|
|
275
|
+
content: [{ type: "text", text: NUDGE_TEXT }],
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test("a later-registered user hook can override the default decision", async () => {
|
|
280
|
+
// GIVEN the default plugin registers first, then a user plugin whose hook
|
|
281
|
+
// observes the default's decision and forces a stop.
|
|
282
|
+
let observedDecision: string | null = null;
|
|
283
|
+
registerPlugin(defaultEmptyResponsePlugin);
|
|
284
|
+
registerPlugin({
|
|
285
|
+
manifest: { name: "force-stop", version: "0.0.1" },
|
|
286
|
+
hooks: {
|
|
287
|
+
stop: async (ctx: StopContext) => {
|
|
288
|
+
observedDecision = ctx.decision;
|
|
289
|
+
ctx.decision = "stop";
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// WHEN the chain runs over an empty-after-tools context.
|
|
295
|
+
const result = await runHook<StopContext>(
|
|
296
|
+
HOOKS.STOP,
|
|
297
|
+
makeCtx({ messages: [priorToolUseTurn], responseContent: [] }),
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
// THEN the user hook saw the default's continue, and its override wins.
|
|
301
|
+
expect(observedDecision as string | null).toBe("continue");
|
|
302
|
+
expect(result.decision).toBe("stop");
|
|
303
|
+
});
|
|
304
|
+
});
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
// Mirrors `src/config/feature-flag-cache.ts`. Duplicated by design — see
|
|
20
20
|
// the "No source-module imports" section above.
|
|
21
21
|
type FlagSlot = {
|
|
22
|
-
overrides: Record<string, boolean> | null;
|
|
22
|
+
overrides: Record<string, boolean | string> | null;
|
|
23
23
|
fromGateway: boolean;
|
|
24
24
|
};
|
|
25
25
|
|
|
@@ -45,7 +45,7 @@ function flagSlot(): FlagSlot {
|
|
|
45
45
|
* `clearFeatureFlagOverridesCache()` from `assistant-feature-flags.ts`.
|
|
46
46
|
*/
|
|
47
47
|
export function setOverridesForTesting(
|
|
48
|
-
overrides: Record<string, boolean>,
|
|
48
|
+
overrides: Record<string, boolean | string>,
|
|
49
49
|
): void {
|
|
50
50
|
const s = flagSlot();
|
|
51
51
|
s.overrides = { ...overrides };
|
|
@@ -287,6 +287,19 @@ describe("mapGeminiError", () => {
|
|
|
287
287
|
expect(msg).toContain("Rate limit");
|
|
288
288
|
});
|
|
289
289
|
|
|
290
|
+
test("maps 402 status to a non-retryable out-of-credits message", () => {
|
|
291
|
+
const msg = mapGeminiError(new FakeApiError(402, "payment required"));
|
|
292
|
+
expect(msg).toContain("out of credits");
|
|
293
|
+
expect(msg).not.toContain("Please try again");
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test("maps a managed-proxy 402 failure (plain Error) to out-of-credits message", () => {
|
|
297
|
+
const msg = mapGeminiError(
|
|
298
|
+
new Error("Managed proxy request failed (402): insufficient credits"),
|
|
299
|
+
);
|
|
300
|
+
expect(msg).toContain("out of credits");
|
|
301
|
+
});
|
|
302
|
+
|
|
290
303
|
test("maps 500 status to server error message", () => {
|
|
291
304
|
const msg = mapGeminiError(new FakeApiError(500, "internal"));
|
|
292
305
|
expect(msg).toContain("temporarily unavailable");
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// Type-only import: this binds the mock to the real `Provider` contract so it
|
|
2
|
+
// can be passed to a live `AgentLoop` without casts. It must stay `import type`
|
|
3
|
+
// — a value import would pull `providers/types.ts`'s runtime exports into this
|
|
4
|
+
// shared helper, which the test-machinery isolation rule forbids.
|
|
5
|
+
import type {
|
|
6
|
+
Message,
|
|
7
|
+
Provider,
|
|
8
|
+
ProviderResponse,
|
|
9
|
+
SendMessageOptions,
|
|
10
|
+
ToolDefinition,
|
|
11
|
+
} from "../../providers/types.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Records the arguments of a single `provider.sendMessage` invocation so tests
|
|
15
|
+
* can assert on what the agent loop sent (messages, tools, system prompt, the
|
|
16
|
+
* resolved options bag).
|
|
17
|
+
*/
|
|
18
|
+
export interface RecordedProviderCall {
|
|
19
|
+
messages: Message[];
|
|
20
|
+
tools?: ToolDefinition[];
|
|
21
|
+
systemPrompt?: string;
|
|
22
|
+
options?: SendMessageOptions;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* One scripted provider turn. A `ProviderResponse` is returned normally; an
|
|
27
|
+
* `Error` is thrown to simulate a provider HTTP rejection (e.g. a
|
|
28
|
+
* context-too-large error the orchestrator must recover from).
|
|
29
|
+
*/
|
|
30
|
+
export type ScriptedResponse = ProviderResponse | Error;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A mock provider that returns pre-configured responses in sequence.
|
|
34
|
+
*
|
|
35
|
+
* Drives the real {@link import("../../agent/loop.js").AgentLoop} by mocking
|
|
36
|
+
* only the provider HTTP boundary: each call returns the next scripted
|
|
37
|
+
* `ProviderResponse` (the last response repeats once the script is exhausted)
|
|
38
|
+
* and replays its text blocks as `text_delta` events so the loop streams
|
|
39
|
+
* exactly as it would against a live provider. A scripted `Error` entry is
|
|
40
|
+
* thrown instead of returned, so a rejection can be sequenced before a
|
|
41
|
+
* recovery response.
|
|
42
|
+
*/
|
|
43
|
+
export function createMockProvider(
|
|
44
|
+
responses: ScriptedResponse[],
|
|
45
|
+
name = "mock",
|
|
46
|
+
): {
|
|
47
|
+
provider: Provider;
|
|
48
|
+
calls: RecordedProviderCall[];
|
|
49
|
+
} {
|
|
50
|
+
const calls: RecordedProviderCall[] = [];
|
|
51
|
+
let callIndex = 0;
|
|
52
|
+
|
|
53
|
+
const provider: Provider = {
|
|
54
|
+
name,
|
|
55
|
+
async sendMessage(
|
|
56
|
+
messages: Message[],
|
|
57
|
+
options?: SendMessageOptions,
|
|
58
|
+
): Promise<ProviderResponse> {
|
|
59
|
+
calls.push({
|
|
60
|
+
messages: [...messages],
|
|
61
|
+
tools: options?.tools,
|
|
62
|
+
systemPrompt: options?.systemPrompt,
|
|
63
|
+
options,
|
|
64
|
+
});
|
|
65
|
+
const scripted = responses[callIndex] ?? responses[responses.length - 1];
|
|
66
|
+
callIndex++;
|
|
67
|
+
|
|
68
|
+
if (scripted instanceof Error) {
|
|
69
|
+
throw scripted;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Replay streaming deltas for text blocks, mirroring a live provider.
|
|
73
|
+
if (options?.onEvent) {
|
|
74
|
+
for (const block of scripted.content) {
|
|
75
|
+
if (block.type === "text") {
|
|
76
|
+
options.onEvent({ type: "text_delta", text: block.text });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return scripted;
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
return { provider, calls };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** A scripted assistant turn that ends with a plain text response. */
|
|
89
|
+
export function textResponse(text: string): ProviderResponse {
|
|
90
|
+
return {
|
|
91
|
+
content: [{ type: "text", text }],
|
|
92
|
+
model: "mock-model",
|
|
93
|
+
usage: { inputTokens: 10, outputTokens: 5 },
|
|
94
|
+
stopReason: "end_turn",
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** A scripted assistant turn that invokes a single tool. */
|
|
99
|
+
export function toolUseResponse(
|
|
100
|
+
id: string,
|
|
101
|
+
name: string,
|
|
102
|
+
input: Record<string, unknown>,
|
|
103
|
+
): ProviderResponse {
|
|
104
|
+
return {
|
|
105
|
+
content: [{ type: "tool_use", id, name, input }],
|
|
106
|
+
model: "mock-model",
|
|
107
|
+
usage: { inputTokens: 10, outputTokens: 5 },
|
|
108
|
+
stopReason: "tool_use",
|
|
109
|
+
};
|
|
110
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared test harness for driving the daemon's native `server_tool_complete`
|
|
3
|
+
* web_search handler in isolation (ATL-727).
|
|
4
|
+
*
|
|
5
|
+
* Both `native-web-search.test.ts` and `web-search-backend-failure.test.ts`
|
|
6
|
+
* exercise the same handler the same way: build a set of mocked
|
|
7
|
+
* `EventHandlerDeps` (capturing emitted `ServerMessage`s and `rlog.warn`
|
|
8
|
+
* records), then drive a `server_tool_start` → `server_tool_complete` pair.
|
|
9
|
+
* This module is the single source of truth for that harness so the two suites
|
|
10
|
+
* cannot drift apart.
|
|
11
|
+
*
|
|
12
|
+
* Note: each consuming test file must still install its own
|
|
13
|
+
* `mock.module(...)` stubs for the daemon collaborators the handler imports at
|
|
14
|
+
* load time (config loader, conversation-crud, llm-request-log-store), because
|
|
15
|
+
* Bun's `mock.module()` is scoped to the file that registers it.
|
|
16
|
+
*/
|
|
17
|
+
import type {
|
|
18
|
+
EventHandlerDeps,
|
|
19
|
+
EventHandlerState,
|
|
20
|
+
} from "../../daemon/conversation-agent-loop-handlers.js";
|
|
21
|
+
import { dispatchAgentEvent } from "../../daemon/conversation-agent-loop-handlers.js";
|
|
22
|
+
import type { ServerMessage } from "../../daemon/message-protocol.js";
|
|
23
|
+
|
|
24
|
+
/** A `tool_result` `ServerMessage` emitted by the handler. */
|
|
25
|
+
export type ToolResultEvent = Extract<ServerMessage, { type: "tool_result" }>;
|
|
26
|
+
|
|
27
|
+
/** A captured `rlog.warn(obj, msg)` call. */
|
|
28
|
+
export interface LogRecord {
|
|
29
|
+
obj: Record<string, unknown>;
|
|
30
|
+
msg?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface HandlerHarness {
|
|
34
|
+
deps: EventHandlerDeps;
|
|
35
|
+
/** Every `ServerMessage` the handler emitted via `onEvent`. */
|
|
36
|
+
events: ServerMessage[];
|
|
37
|
+
/** Every `rlog.warn(obj, msg)` call the handler made. */
|
|
38
|
+
warnings: LogRecord[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Build mocked handler deps that capture emitted events and warn logs. */
|
|
42
|
+
export function createHandlerDeps(reqId = "req-web-search"): HandlerHarness {
|
|
43
|
+
const events: ServerMessage[] = [];
|
|
44
|
+
const warnings: LogRecord[] = [];
|
|
45
|
+
const rlog = {
|
|
46
|
+
warn: (obj: Record<string, unknown>, msg?: string) =>
|
|
47
|
+
warnings.push({ obj, msg }),
|
|
48
|
+
info: () => {},
|
|
49
|
+
error: () => {},
|
|
50
|
+
debug: () => {},
|
|
51
|
+
trace: () => {},
|
|
52
|
+
fatal: () => {},
|
|
53
|
+
};
|
|
54
|
+
const deps = {
|
|
55
|
+
ctx: {
|
|
56
|
+
conversationId: "conv-web-search",
|
|
57
|
+
provider: { name: "anthropic" },
|
|
58
|
+
traceEmitter: { emit: () => {} },
|
|
59
|
+
streamThinking: false,
|
|
60
|
+
emitActivityState: () => {},
|
|
61
|
+
markWorkspaceTopLevelDirty: () => {},
|
|
62
|
+
currentTurnSurfaces: [],
|
|
63
|
+
} as unknown as EventHandlerDeps["ctx"],
|
|
64
|
+
onEvent: (msg: ServerMessage) => events.push(msg),
|
|
65
|
+
reqId,
|
|
66
|
+
isFirstMessage: false,
|
|
67
|
+
shouldGenerateTitle: false,
|
|
68
|
+
rlog: rlog as unknown as EventHandlerDeps["rlog"],
|
|
69
|
+
turnChannelContext: {
|
|
70
|
+
userMessageChannel: "vellum",
|
|
71
|
+
assistantMessageChannel: "vellum",
|
|
72
|
+
} as EventHandlerDeps["turnChannelContext"],
|
|
73
|
+
turnInterfaceContext: {
|
|
74
|
+
userMessageInterface: "macos",
|
|
75
|
+
assistantMessageInterface: "macos",
|
|
76
|
+
} as EventHandlerDeps["turnInterfaceContext"],
|
|
77
|
+
applyCompaction: async () => {},
|
|
78
|
+
} as EventHandlerDeps;
|
|
79
|
+
return { deps, events, warnings };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The `server_tool_complete` payload shape a test supplies. */
|
|
83
|
+
export interface WebSearchCompleteEvent {
|
|
84
|
+
isError: boolean;
|
|
85
|
+
errorCode?: string;
|
|
86
|
+
errorMessage?: string;
|
|
87
|
+
content?: unknown[];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Drive one native (Anthropic) web_search start → complete pair. */
|
|
91
|
+
export async function completeNativeWebSearch(
|
|
92
|
+
state: EventHandlerState,
|
|
93
|
+
deps: EventHandlerDeps,
|
|
94
|
+
toolUseId: string,
|
|
95
|
+
event: WebSearchCompleteEvent,
|
|
96
|
+
): Promise<void> {
|
|
97
|
+
await dispatchAgentEvent(state, deps, {
|
|
98
|
+
type: "server_tool_start",
|
|
99
|
+
name: "web_search",
|
|
100
|
+
toolUseId,
|
|
101
|
+
input: { query: "what is the weather" },
|
|
102
|
+
});
|
|
103
|
+
await dispatchAgentEvent(state, deps, {
|
|
104
|
+
type: "server_tool_complete",
|
|
105
|
+
toolUseId,
|
|
106
|
+
isError: event.isError,
|
|
107
|
+
...(event.errorCode ? { errorCode: event.errorCode } : {}),
|
|
108
|
+
...(event.errorMessage ? { errorMessage: event.errorMessage } : {}),
|
|
109
|
+
content: event.content ?? [],
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** All `tool_result` events emitted so far, in order. */
|
|
114
|
+
export function toolResults(events: ServerMessage[]): ToolResultEvent[] {
|
|
115
|
+
return events.filter((e): e is ToolResultEvent => e.type === "tool_result");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The most recent `tool_result` event, if any. */
|
|
119
|
+
export function lastToolResult(
|
|
120
|
+
events: ServerMessage[],
|
|
121
|
+
): ToolResultEvent | undefined {
|
|
122
|
+
const results = toolResults(events);
|
|
123
|
+
return results[results.length - 1];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** The captured `web_search_backend_failure` telemetry warn records. */
|
|
127
|
+
export function backendFailureLogs(warnings: LogRecord[]): LogRecord[] {
|
|
128
|
+
return warnings.filter((w) => w.obj.event === "web_search_backend_failure");
|
|
129
|
+
}
|