@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
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { getWorkspaceDir } from "../../util/platform.js";
|
|
5
|
+
import { stripCommentLines } from "../../util/strip-comment-lines.js";
|
|
6
|
+
import { getPkbAutoInjectList } from "./autoinject.js";
|
|
7
|
+
|
|
8
|
+
/** Max buffer.md lines injected into prompts — keeps context bounded even when filing is off. */
|
|
9
|
+
const MAX_BUFFER_LINES = 50;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Read the always-loaded PKB files and append a nudge encouraging the
|
|
13
|
+
* assistant to proactively read topic files and use `remember` aggressively.
|
|
14
|
+
*
|
|
15
|
+
* Which files are loaded is determined by `pkb/_autoinject.md` (one filename
|
|
16
|
+
* per line). Falls back to the built-in defaults when that file is absent.
|
|
17
|
+
*
|
|
18
|
+
* Returns the concatenated content ready for injection, or `null` if all
|
|
19
|
+
* files are missing or empty.
|
|
20
|
+
*/
|
|
21
|
+
export function readPkbContext(): string | null {
|
|
22
|
+
const pkbDir = join(getWorkspaceDir(), "pkb");
|
|
23
|
+
if (!existsSync(pkbDir)) return null;
|
|
24
|
+
|
|
25
|
+
const filesToInject = getPkbAutoInjectList(pkbDir);
|
|
26
|
+
|
|
27
|
+
const parts: string[] = [];
|
|
28
|
+
for (const file of filesToInject) {
|
|
29
|
+
// Path traversal guard: reject entries that escape the pkb directory
|
|
30
|
+
const filePath = resolve(pkbDir, file);
|
|
31
|
+
if (!filePath.startsWith(pkbDir + "/")) continue;
|
|
32
|
+
|
|
33
|
+
if (!existsSync(filePath)) continue;
|
|
34
|
+
try {
|
|
35
|
+
let content = stripCommentLines(readFileSync(filePath, "utf-8")).trim();
|
|
36
|
+
if (file === "buffer.md" && content.length > 0) {
|
|
37
|
+
// Cap buffer entries to prevent unbounded growth when filing is disabled
|
|
38
|
+
const lines = content.split("\n");
|
|
39
|
+
if (lines.length > MAX_BUFFER_LINES) {
|
|
40
|
+
content = lines.slice(-MAX_BUFFER_LINES).join("\n");
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (content.length > 0) parts.push(content);
|
|
44
|
+
} catch {
|
|
45
|
+
// Skip unreadable files
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return parts.length > 0 ? parts.join("\n\n") : null;
|
|
50
|
+
}
|
package/src/memory/pkb/types.ts
CHANGED
|
@@ -5,8 +5,22 @@
|
|
|
5
5
|
* index writer) land in later PRs.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
|
|
10
|
+
import { getWorkspaceDir } from "../../util/platform.js";
|
|
11
|
+
|
|
8
12
|
export const PKB_TARGET_TYPE = "pkb_file" as const;
|
|
9
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Absolute path to the workspace's PKB directory (`<workspace>/pkb`). PKB is
|
|
16
|
+
* a workspace-shared resource with a single on-disk location, so this is the
|
|
17
|
+
* canonical way to resolve its root rather than re-joining the workspace dir
|
|
18
|
+
* at each call site.
|
|
19
|
+
*/
|
|
20
|
+
export function getPkbRoot(): string {
|
|
21
|
+
return join(getWorkspaceDir(), "pkb");
|
|
22
|
+
}
|
|
23
|
+
|
|
10
24
|
/**
|
|
11
25
|
* Sentinel `memory_scope_id` under which ALL PKB points are indexed and
|
|
12
26
|
* searched. PKB files are a workspace-shared resource: one copy on disk is
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
export interface ScheduleAttributionFilter {
|
|
2
|
+
scheduleId?: string;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export type ScheduleAttributionSqlParam = string | number;
|
|
6
|
+
|
|
7
|
+
export interface ScheduleAttributionSqlFragment {
|
|
8
|
+
sql: string;
|
|
9
|
+
params: ScheduleAttributionSqlParam[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function normalizeScheduleAttributionFilter(
|
|
13
|
+
filter?: ScheduleAttributionFilter,
|
|
14
|
+
): ScheduleAttributionFilter {
|
|
15
|
+
const scheduleId = filter?.scheduleId?.trim();
|
|
16
|
+
return scheduleId ? { scheduleId } : {};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function usageColumn(column: string, eventAlias: string): string {
|
|
20
|
+
return `${eventAlias}.${column}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function buildScheduleRunWindowPredicate({
|
|
24
|
+
eventAlias,
|
|
25
|
+
runAlias,
|
|
26
|
+
filter,
|
|
27
|
+
}: {
|
|
28
|
+
eventAlias: string;
|
|
29
|
+
runAlias: string;
|
|
30
|
+
filter?: ScheduleAttributionFilter;
|
|
31
|
+
}): string {
|
|
32
|
+
const normalized = normalizeScheduleAttributionFilter(filter);
|
|
33
|
+
const scheduleClause = normalized.scheduleId
|
|
34
|
+
? `${runAlias}.job_id = ? AND `
|
|
35
|
+
: "";
|
|
36
|
+
return `${scheduleClause}${runAlias}.conversation_id = ${usageColumn(
|
|
37
|
+
"conversation_id",
|
|
38
|
+
eventAlias,
|
|
39
|
+
)}
|
|
40
|
+
AND ${usageColumn("created_at", eventAlias)} >= ${runAlias}.started_at
|
|
41
|
+
AND ${usageColumn("created_at", eventAlias)} <= COALESCE(${runAlias}.finished_at, ?)`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function buildScheduleRunWindowParams(
|
|
45
|
+
filter: ScheduleAttributionFilter | undefined,
|
|
46
|
+
now: number,
|
|
47
|
+
): ScheduleAttributionSqlParam[] {
|
|
48
|
+
const normalized = normalizeScheduleAttributionFilter(filter);
|
|
49
|
+
return normalized.scheduleId ? [normalized.scheduleId, now] : [now];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function buildScheduleRunWindowExists({
|
|
53
|
+
eventAlias,
|
|
54
|
+
filter,
|
|
55
|
+
now,
|
|
56
|
+
runAlias = "schedule_filter_runs",
|
|
57
|
+
}: {
|
|
58
|
+
eventAlias: string;
|
|
59
|
+
filter?: ScheduleAttributionFilter;
|
|
60
|
+
now: number;
|
|
61
|
+
runAlias?: string;
|
|
62
|
+
}): ScheduleAttributionSqlFragment {
|
|
63
|
+
return {
|
|
64
|
+
sql: `EXISTS (
|
|
65
|
+
SELECT 1
|
|
66
|
+
FROM cron_runs ${runAlias}
|
|
67
|
+
WHERE ${buildScheduleRunWindowPredicate({
|
|
68
|
+
eventAlias,
|
|
69
|
+
runAlias,
|
|
70
|
+
filter,
|
|
71
|
+
})}
|
|
72
|
+
)`,
|
|
73
|
+
params: buildScheduleRunWindowParams(filter, now),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function buildScheduleAttributionSubquery({
|
|
78
|
+
eventAlias,
|
|
79
|
+
filter,
|
|
80
|
+
now,
|
|
81
|
+
selectExpression,
|
|
82
|
+
runAlias = "schedule_attr_runs",
|
|
83
|
+
}: {
|
|
84
|
+
eventAlias: string;
|
|
85
|
+
filter?: ScheduleAttributionFilter;
|
|
86
|
+
now: number;
|
|
87
|
+
selectExpression: string;
|
|
88
|
+
runAlias?: string;
|
|
89
|
+
}): ScheduleAttributionSqlFragment {
|
|
90
|
+
return {
|
|
91
|
+
sql: `(
|
|
92
|
+
SELECT ${selectExpression}
|
|
93
|
+
FROM cron_runs ${runAlias}
|
|
94
|
+
WHERE ${buildScheduleRunWindowPredicate({
|
|
95
|
+
eventAlias,
|
|
96
|
+
runAlias,
|
|
97
|
+
filter,
|
|
98
|
+
})}
|
|
99
|
+
ORDER BY ${runAlias}.started_at DESC, ${runAlias}.id DESC
|
|
100
|
+
LIMIT 1
|
|
101
|
+
)`,
|
|
102
|
+
params: buildScheduleRunWindowParams(filter, now),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -22,6 +22,7 @@ export const cronJobs = sqliteTable("cron_jobs", {
|
|
|
22
22
|
maxRetries: integer("max_retries").notNull().default(3),
|
|
23
23
|
retryBackoffMs: integer("retry_backoff_ms").notNull().default(60000),
|
|
24
24
|
timeoutMs: integer("timeout_ms"), // script-mode execution timeout override (ms); null = use default
|
|
25
|
+
createdFromConversationId: text("created_from_conversation_id"),
|
|
25
26
|
createdBy: text("created_by").notNull(), // 'agent' | 'user'
|
|
26
27
|
mode: text("mode").notNull().default("execute"), // 'notify' | 'execute'
|
|
27
28
|
routingIntent: text("routing_intent").notNull().default("all_channels"), // 'single_channel' | 'multi_channel' | 'all_channels'
|
|
@@ -278,6 +279,21 @@ export const onboardingEvents = sqliteTable("onboarding_events", {
|
|
|
278
279
|
abVariant: text("ab_variant"),
|
|
279
280
|
});
|
|
280
281
|
|
|
282
|
+
// Aggregated legacy-loopback auth-fallback counts forwarded by the gateway.
|
|
283
|
+
// One row per (guard, path, failure_kind) per flush window; `count` is how many
|
|
284
|
+
// requests fell back to the loopback exemption in that window. Flushed to the
|
|
285
|
+
// platform telemetry endpoint by the usage telemetry reporter.
|
|
286
|
+
export const authFallbackEvents = sqliteTable("auth_fallback_events", {
|
|
287
|
+
id: text("id").primaryKey(),
|
|
288
|
+
createdAt: integer("created_at").notNull(),
|
|
289
|
+
guard: text("guard").notNull(), // 'edge' | 'edge-scoped' | 'edge-guardian'
|
|
290
|
+
path: text("path").notNull(),
|
|
291
|
+
failureKind: text("failure_kind").notNull(),
|
|
292
|
+
count: integer("count").notNull(),
|
|
293
|
+
windowStart: integer("window_start").notNull(),
|
|
294
|
+
windowEnd: integer("window_end").notNull(),
|
|
295
|
+
});
|
|
296
|
+
|
|
281
297
|
export const traceEvents = sqliteTable(
|
|
282
298
|
"trace_events",
|
|
283
299
|
{
|
|
@@ -27,6 +27,7 @@ export interface UsageGroupedSeriesBucket extends UsageDayBucket {
|
|
|
27
27
|
|
|
28
28
|
export interface UsageGroupedBucketRow extends UsageEventBucketRow {
|
|
29
29
|
group_key: string | null;
|
|
30
|
+
group_label?: string | null;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
const VALUE_GROUP_PREFIX = "value:";
|
|
@@ -42,6 +43,9 @@ export function displayUsageGroup(
|
|
|
42
43
|
if (groupBy === "inference_profile") {
|
|
43
44
|
return groupKey === null ? "Default / Unset" : groupKey;
|
|
44
45
|
}
|
|
46
|
+
if (groupBy === "schedule") {
|
|
47
|
+
return groupKey ?? "Other";
|
|
48
|
+
}
|
|
45
49
|
return groupKey ?? "Other";
|
|
46
50
|
}
|
|
47
51
|
|
|
@@ -102,7 +106,8 @@ export function bucketGroupedUsageEvents(
|
|
|
102
106
|
let group = groupedBucket.groups[seriesKey];
|
|
103
107
|
if (!group) {
|
|
104
108
|
group = {
|
|
105
|
-
group:
|
|
109
|
+
group:
|
|
110
|
+
row.group_label ?? displayUsageGroup(options.groupBy, row.group_key),
|
|
106
111
|
groupKey: row.group_key,
|
|
107
112
|
totalInputTokens: 0,
|
|
108
113
|
totalOutputTokens: 0,
|
|
@@ -279,7 +279,7 @@ describe("memoryV2ConsolidateJob — non-empty buffer", () => {
|
|
|
279
279
|
expect(runnerLastArgs).not.toBeNull();
|
|
280
280
|
expect(runnerLastArgs?.jobName).toBe("memory.consolidate");
|
|
281
281
|
expect(runnerLastArgs?.source).toBe("memory_v2_consolidation");
|
|
282
|
-
expect(runnerLastArgs?.callSite).toBe("
|
|
282
|
+
expect(runnerLastArgs?.callSite).toBe("memoryV2Consolidation");
|
|
283
283
|
expect(runnerLastArgs?.origin).toBe("memory_consolidation");
|
|
284
284
|
// The whole point of this PR: opt out of activity.failed notifications
|
|
285
285
|
// because consolidation runs on tight intervals and transient failures
|
|
@@ -197,7 +197,7 @@ export async function memoryV2ConsolidateJob(
|
|
|
197
197
|
source: MEMORY_V2_CONSOLIDATION_SOURCE,
|
|
198
198
|
prompt,
|
|
199
199
|
trustContext: { sourceChannel: "vellum", trustClass: "guardian" },
|
|
200
|
-
callSite: "
|
|
200
|
+
callSite: "memoryV2Consolidation",
|
|
201
201
|
timeoutMs: CONSOLIDATION_TIMEOUT_MS,
|
|
202
202
|
origin: "memory_consolidation",
|
|
203
203
|
suppressFailureNotifications: true,
|
|
@@ -42,6 +42,22 @@ describe("computeV3Health", () => {
|
|
|
42
42
|
expect(report.unassigned).toEqual(["page-b", "page-c"]);
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
+
test("excludes synthetic capability slugs from unassigned / novel clusters", () => {
|
|
46
|
+
const t = tree({ "domain-a/topic-x": ["page-a"] });
|
|
47
|
+
const report = computeV3Health({
|
|
48
|
+
tree: t,
|
|
49
|
+
allSlugs: ["page-a", "page-b", "cli-commands/example", "skills/example"],
|
|
50
|
+
});
|
|
51
|
+
// Capability slugs are handled by the always-on capabilities leaf (injected
|
|
52
|
+
// into the live lane tree, absent here), not the persisted tree — so they must
|
|
53
|
+
// not be reported as unassigned or grouped into novel clusters. page-b is the
|
|
54
|
+
// one real unassigned concept page.
|
|
55
|
+
expect(report.unassigned).toEqual(["page-b"]);
|
|
56
|
+
expect(report.novelClusters).toEqual([
|
|
57
|
+
{ prefix: "page-b", slugs: ["page-b"], count: 1 },
|
|
58
|
+
]);
|
|
59
|
+
});
|
|
60
|
+
|
|
45
61
|
test("flags dangling page refs pointing at missing leaves", () => {
|
|
46
62
|
const t = tree({ "domain-a/topic-x": ["page-a"] });
|
|
47
63
|
const report = computeV3Health({
|
|
@@ -217,6 +217,43 @@ describe("orchestrate — fixture sequence (carry-forward)", () => {
|
|
|
217
217
|
expect(t2.currentSelections.map((s) => s.slug)).not.toContain("page-a");
|
|
218
218
|
expect(t2.finalInjection).toContain("page-a");
|
|
219
219
|
});
|
|
220
|
+
|
|
221
|
+
test("carry-forward survives a turn whose selections fill the cap", async () => {
|
|
222
|
+
const tree = makeTree();
|
|
223
|
+
// Cap of 1: under a naive record-then-cap order this turn's own selection
|
|
224
|
+
// would evict the carried page before injection. Snapshotting the carry
|
|
225
|
+
// BEFORE recording this turn keeps the earlier page in the injection.
|
|
226
|
+
const workingSet = new WorkingSet(1);
|
|
227
|
+
const needle = fakeNeedle([]);
|
|
228
|
+
const stub = (selectIds: number[]): Provider => ({
|
|
229
|
+
name: "stub",
|
|
230
|
+
sendMessage: async (_messages, options) =>
|
|
231
|
+
options?.tools?.[0]?.name === "open_leaves"
|
|
232
|
+
? toolUseResponse("open_leaves", { ids: [1] })
|
|
233
|
+
: toolUseResponse("select_pages", { ids: selectIds, pinned_ids: [] }),
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
providerStub = stub([1]); // turn 1 → page-a
|
|
237
|
+
await orchestrate(makeTurn(1, "page a"), {
|
|
238
|
+
tree,
|
|
239
|
+
core: new Set(),
|
|
240
|
+
needle,
|
|
241
|
+
workingSet,
|
|
242
|
+
pageSummary: summaryOf,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
providerStub = stub([2]); // turn 2 → page-b, never re-selects page-a
|
|
246
|
+
const t2 = await orchestrate(makeTurn(2, "page b"), {
|
|
247
|
+
tree,
|
|
248
|
+
core: new Set(),
|
|
249
|
+
needle,
|
|
250
|
+
workingSet,
|
|
251
|
+
pageSummary: summaryOf,
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
expect(t2.currentSelections.map((s) => s.slug)).toEqual(["page-b"]);
|
|
255
|
+
expect(t2.finalInjection).toContain("page-a"); // carried despite the cap
|
|
256
|
+
});
|
|
220
257
|
});
|
|
221
258
|
|
|
222
259
|
// ---------------------------------------------------------------------------
|
|
@@ -298,14 +335,16 @@ describe("orchestrate — edge cases", () => {
|
|
|
298
335
|
expect(result.finalInjection).toEqual(["page-a", "page-b"]);
|
|
299
336
|
});
|
|
300
337
|
|
|
301
|
-
test("L1
|
|
338
|
+
test("omitted L1 ids opens only the deterministic lanes, not the whole tree", async () => {
|
|
302
339
|
const tree = makeTree();
|
|
303
|
-
//
|
|
340
|
+
// L1 omits ids → routeL1 opens NO routed leaves; only the needle/core lanes
|
|
341
|
+
// drive the open set, so the whole tree is never fanned out (topic-y, which
|
|
342
|
+
// nothing routes or needles to, stays closed).
|
|
304
343
|
providerStub = {
|
|
305
344
|
name: "stub",
|
|
306
345
|
sendMessage: async (_messages, options) => {
|
|
307
346
|
if (options?.tools?.[0]?.name === "open_leaves") {
|
|
308
|
-
return toolUseResponse("open_leaves", {}); // omitted ids
|
|
347
|
+
return toolUseResponse("open_leaves", {}); // omitted ids → []
|
|
309
348
|
}
|
|
310
349
|
return toolUseResponse("select_pages", {}); // omitted → all members
|
|
311
350
|
},
|
|
@@ -313,15 +352,12 @@ describe("orchestrate — edge cases", () => {
|
|
|
313
352
|
const result = await orchestrate(makeTurn(1, "x"), {
|
|
314
353
|
tree,
|
|
315
354
|
core: new Set(),
|
|
316
|
-
needle: fakeNeedle([]),
|
|
355
|
+
needle: fakeNeedle(["page-a"]), // needle opens domain-a/topic-x only
|
|
317
356
|
workingSet: new WorkingSet(),
|
|
318
357
|
pageSummary: summaryOf,
|
|
319
358
|
});
|
|
320
|
-
expect(result.openedLeaves).toEqual([
|
|
321
|
-
|
|
322
|
-
"domain-a/topic-y",
|
|
323
|
-
]);
|
|
324
|
-
expect(result.finalInjection).toEqual(["page-a", "page-b", "page-c"]);
|
|
359
|
+
expect(result.openedLeaves).toEqual(["domain-a/topic-x"]);
|
|
360
|
+
expect(result.finalInjection).toEqual(["page-a", "page-b"]);
|
|
325
361
|
});
|
|
326
362
|
|
|
327
363
|
test("pinned current-turn selections land in the working set", async () => {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { cachedTextBlock } from "../provider-blocks.js";
|
|
4
|
+
|
|
5
|
+
describe("cachedTextBlock", () => {
|
|
6
|
+
test("stamps an ephemeral cache_control with a 1h TTL", () => {
|
|
7
|
+
const block = cachedTextBlock("stable leaf block");
|
|
8
|
+
expect(block).toMatchObject({ type: "text", text: "stable leaf block" });
|
|
9
|
+
expect(
|
|
10
|
+
(block as unknown as { cache_control?: unknown }).cache_control,
|
|
11
|
+
).toEqual({ type: "ephemeral", ttl: "1h" });
|
|
12
|
+
});
|
|
13
|
+
});
|
|
@@ -3,10 +3,13 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Coverage matrix:
|
|
5
5
|
* - Returned IDs map to the right leaves by 1-based index, in model order.
|
|
6
|
-
* - Omitted `ids` →
|
|
6
|
+
* - Omitted `ids` → no leaves (the router must name leaves explicitly,
|
|
7
|
+
* never open the whole tree).
|
|
7
8
|
* - Explicit `ids: []` → no leaves (deliberate abstention).
|
|
8
9
|
* - Out-of-range / duplicate IDs ignored, no throw.
|
|
9
|
-
* - No provider / missing tool_use / schema mismatch / throw →
|
|
10
|
+
* - No provider / missing tool_use / schema mismatch / throw → no leaves
|
|
11
|
+
* (degrade to the deterministic lanes), the last three after a re-prompt
|
|
12
|
+
* retry; a malformed response that recovers on retry returns its IDs.
|
|
10
13
|
* - The rendered leaf block is byte-identical across two calls with
|
|
11
14
|
* different queries (the cache invariant).
|
|
12
15
|
* - The system prompt mentions "register" (locks the routing commitment).
|
|
@@ -76,6 +79,45 @@ function toolUseResponse(input: Record<string, unknown>): ProviderResponse {
|
|
|
76
79
|
};
|
|
77
80
|
}
|
|
78
81
|
|
|
82
|
+
/** A 200 response that carries no tool_use — the malformed-but-successful case
|
|
83
|
+
* the re-prompt retry exists to recover from. */
|
|
84
|
+
function noToolResponse(): ProviderResponse {
|
|
85
|
+
return {
|
|
86
|
+
model: "stub-model",
|
|
87
|
+
stopReason: "end_turn",
|
|
88
|
+
usage: { inputTokens: 0, outputTokens: 0 },
|
|
89
|
+
content: [{ type: "text", text: "no tool call" }],
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Provider returning a different response per call (the i-th call returns
|
|
94
|
+
* responses[i], or the last entry once exhausted), recording each call so a
|
|
95
|
+
* test can assert how many attempts were made. */
|
|
96
|
+
function makeSequenceProvider(responses: ProviderResponse[]): Provider {
|
|
97
|
+
let i = 0;
|
|
98
|
+
return {
|
|
99
|
+
name: "sequence",
|
|
100
|
+
sendMessage: async (messages, options) => {
|
|
101
|
+
providerCalls.push({ messages, options });
|
|
102
|
+
const response = responses[Math.min(i, responses.length - 1)];
|
|
103
|
+
i += 1;
|
|
104
|
+
return response;
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Provider that records each call and then throws — for the throw-after-retries
|
|
110
|
+
* path (the provider's own RetryProvider has already exhausted its backoff). */
|
|
111
|
+
function makeThrowingProvider(): Provider {
|
|
112
|
+
return {
|
|
113
|
+
name: "throwing",
|
|
114
|
+
sendMessage: async (messages, options) => {
|
|
115
|
+
providerCalls.push({ messages, options });
|
|
116
|
+
throw new Error("boom");
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
79
121
|
function makeLeaf(path: LeafPath, description: string): LeafNode {
|
|
80
122
|
return {
|
|
81
123
|
path,
|
|
@@ -101,8 +143,6 @@ function makeTree(): LeafTree {
|
|
|
101
143
|
};
|
|
102
144
|
}
|
|
103
145
|
|
|
104
|
-
const SORTED_PATHS = ["people/alice", "people/bob", "projects/atlas"];
|
|
105
|
-
|
|
106
146
|
function makeTurn(currentMessage: string): TurnContext {
|
|
107
147
|
return {
|
|
108
148
|
conversationId: "conv-xyz",
|
|
@@ -128,10 +168,10 @@ describe("routeL1 — id mapping", () => {
|
|
|
128
168
|
expect(result).toEqual(["projects/atlas", "people/alice"]);
|
|
129
169
|
});
|
|
130
170
|
|
|
131
|
-
test("omitted ids opens
|
|
171
|
+
test("omitted ids opens no leaves (must name leaves explicitly)", async () => {
|
|
132
172
|
providerStub = makeProvider(toolUseResponse({}));
|
|
133
173
|
const result = await routeL1(makeTurn("anything"), makeTree());
|
|
134
|
-
expect(result).toEqual(
|
|
174
|
+
expect(result).toEqual([]);
|
|
135
175
|
});
|
|
136
176
|
|
|
137
177
|
test("explicit empty ids opens no leaves (abstention)", async () => {
|
|
@@ -157,39 +197,43 @@ describe("routeL1 — id mapping", () => {
|
|
|
157
197
|
});
|
|
158
198
|
});
|
|
159
199
|
|
|
160
|
-
describe("routeL1 —
|
|
161
|
-
test("no provider →
|
|
200
|
+
describe("routeL1 — degradation on failure", () => {
|
|
201
|
+
test("no provider → no leaves, without calling the provider", async () => {
|
|
162
202
|
providerStub = null;
|
|
163
203
|
const result = await routeL1(makeTurn("x"), makeTree());
|
|
164
|
-
expect(result).toEqual(
|
|
204
|
+
expect(result).toEqual([]);
|
|
205
|
+
expect(providerCalls).toHaveLength(0);
|
|
165
206
|
});
|
|
166
207
|
|
|
167
|
-
test("missing tool_use →
|
|
168
|
-
providerStub = makeProvider(
|
|
169
|
-
model: "stub-model",
|
|
170
|
-
stopReason: "end_turn",
|
|
171
|
-
usage: { inputTokens: 0, outputTokens: 0 },
|
|
172
|
-
content: [{ type: "text", text: "no tool call" }],
|
|
173
|
-
});
|
|
208
|
+
test("missing tool_use → no leaves after retrying", async () => {
|
|
209
|
+
providerStub = makeProvider(noToolResponse());
|
|
174
210
|
const result = await routeL1(makeTurn("x"), makeTree());
|
|
175
|
-
expect(result).toEqual(
|
|
211
|
+
expect(result).toEqual([]);
|
|
212
|
+
expect(providerCalls).toHaveLength(3);
|
|
176
213
|
});
|
|
177
214
|
|
|
178
|
-
test("schema mismatch →
|
|
215
|
+
test("schema mismatch → no leaves after retrying", async () => {
|
|
179
216
|
providerStub = makeProvider(toolUseResponse({ ids: "not-an-array" }));
|
|
180
217
|
const result = await routeL1(makeTurn("x"), makeTree());
|
|
181
|
-
expect(result).toEqual(
|
|
218
|
+
expect(result).toEqual([]);
|
|
219
|
+
expect(providerCalls).toHaveLength(3);
|
|
182
220
|
});
|
|
183
221
|
|
|
184
|
-
test("provider throw →
|
|
185
|
-
providerStub =
|
|
186
|
-
name: "throwing",
|
|
187
|
-
sendMessage: async () => {
|
|
188
|
-
throw new Error("boom");
|
|
189
|
-
},
|
|
190
|
-
};
|
|
222
|
+
test("provider throw → no leaves after retrying", async () => {
|
|
223
|
+
providerStub = makeThrowingProvider();
|
|
191
224
|
const result = await routeL1(makeTurn("x"), makeTree());
|
|
192
|
-
expect(result).toEqual(
|
|
225
|
+
expect(result).toEqual([]);
|
|
226
|
+
expect(providerCalls).toHaveLength(3);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("a malformed response that recovers on retry returns its IDs", async () => {
|
|
230
|
+
providerStub = makeSequenceProvider([
|
|
231
|
+
noToolResponse(),
|
|
232
|
+
toolUseResponse({ ids: [2] }),
|
|
233
|
+
]);
|
|
234
|
+
const result = await routeL1(makeTurn("bob?"), makeTree());
|
|
235
|
+
expect(result).toEqual(["people/bob"]);
|
|
236
|
+
expect(providerCalls).toHaveLength(2);
|
|
193
237
|
});
|
|
194
238
|
});
|
|
195
239
|
|
|
@@ -213,11 +257,11 @@ describe("routeL1 — request shape", () => {
|
|
|
213
257
|
const [blockA, blockB] = providerCalls[0].messages[0].content as Array<{
|
|
214
258
|
type: string;
|
|
215
259
|
text: string;
|
|
216
|
-
cache_control?: { type: string };
|
|
260
|
+
cache_control?: { type: string; ttl?: string };
|
|
217
261
|
}>;
|
|
218
262
|
expect(blockA.type).toBe("text");
|
|
219
263
|
expect(blockA.text).toContain("<leaves>");
|
|
220
|
-
expect(blockA.cache_control).toEqual({ type: "ephemeral" });
|
|
264
|
+
expect(blockA.cache_control).toEqual({ type: "ephemeral", ttl: "1h" });
|
|
221
265
|
|
|
222
266
|
expect(blockB.type).toBe("text");
|
|
223
267
|
expect(blockB.text).toContain("<current_message>alice?</current_message>");
|
|
@@ -225,6 +269,34 @@ describe("routeL1 — request shape", () => {
|
|
|
225
269
|
expect(blockB.cache_control).toBeUndefined();
|
|
226
270
|
});
|
|
227
271
|
|
|
272
|
+
test("situational context renders in the per-turn block when present", async () => {
|
|
273
|
+
providerStub = makeProvider(toolUseResponse({ ids: [1] }));
|
|
274
|
+
await routeL1(
|
|
275
|
+
{
|
|
276
|
+
...makeTurn("x"),
|
|
277
|
+
situationalContext: "Today is Saturday. Alice's anniversary is today.",
|
|
278
|
+
},
|
|
279
|
+
makeTree(),
|
|
280
|
+
);
|
|
281
|
+
const blockB = providerCalls[0].messages[0].content[1] as { text: string };
|
|
282
|
+
expect(blockB.text).toContain(
|
|
283
|
+
"<situation>Today is Saturday. Alice's anniversary is today.</situation>",
|
|
284
|
+
);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test("situational context is omitted when the turn has none", async () => {
|
|
288
|
+
providerStub = makeProvider(toolUseResponse({ ids: [1] }));
|
|
289
|
+
await routeL1(makeTurn("x"), makeTree());
|
|
290
|
+
const blockB = providerCalls[0].messages[0].content[1] as { text: string };
|
|
291
|
+
expect(blockB.text).not.toContain("<situation>");
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("system prompt mentions situation (locks the routing commitment)", async () => {
|
|
295
|
+
providerStub = makeProvider(toolUseResponse({ ids: [1] }));
|
|
296
|
+
await routeL1(makeTurn("x"), makeTree());
|
|
297
|
+
expect(providerCalls[0].options?.systemPrompt).toMatch(/[Ss]ituation/);
|
|
298
|
+
});
|
|
299
|
+
|
|
228
300
|
test("system prompt mentions register (locks the routing commitment)", async () => {
|
|
229
301
|
providerStub = makeProvider(toolUseResponse({ ids: [1] }));
|
|
230
302
|
await routeL1(makeTurn("x"), makeTree());
|