@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
|
@@ -1,34 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tests for the memory-v3-live v2-suppression branch in
|
|
3
|
-
* `applyRuntimeInjections
|
|
3
|
+
* `applyRuntimeInjections`.
|
|
4
4
|
*
|
|
5
|
-
* When `
|
|
5
|
+
* When the `memory-v3-live` flag is on AND the v3 injector (id `memory-v3`,
|
|
6
6
|
* placement `after-memory-prefix`) actually produces a block, runtime
|
|
7
7
|
* assembly strips the v2 `<memory>` prefix from EVERY user message before
|
|
8
8
|
* splicing the v3 block — so v3 becomes the sole `<memory>` source and history
|
|
9
9
|
* is byte-stable for prompt caching.
|
|
10
10
|
*
|
|
11
|
-
* Keyed off whether v3 produced a block, NOT off the
|
|
11
|
+
* Keyed off whether v3 produced a block, NOT off the flag alone: a v3
|
|
12
12
|
* failure (`produce()` → null) leaves v2's block intact (fallback-to-v2).
|
|
13
13
|
*
|
|
14
14
|
* The flag-off path must be byte-for-byte identical to today — that is the
|
|
15
|
-
* load-bearing regression guard.
|
|
15
|
+
* load-bearing regression guard. `applyRuntimeInjections` reads the flag
|
|
16
|
+
* itself, so these tests drive it through the override cache.
|
|
16
17
|
*/
|
|
17
18
|
|
|
18
|
-
import { beforeEach, describe, expect, test } from "bun:test";
|
|
19
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
19
20
|
|
|
20
|
-
import {
|
|
21
|
-
getInjectors,
|
|
22
|
-
registerPlugin,
|
|
23
|
-
resetPluginRegistryForTests,
|
|
24
|
-
} from "../plugins/registry.js";
|
|
25
21
|
import type {
|
|
26
22
|
InjectionBlock,
|
|
27
23
|
Injector,
|
|
28
|
-
Plugin,
|
|
29
24
|
TurnContext,
|
|
30
25
|
} from "../plugins/types.js";
|
|
31
26
|
import type { Message } from "../providers/types.js";
|
|
27
|
+
import { setOverridesForTesting } from "./feature-flag-test-helpers.js";
|
|
28
|
+
|
|
29
|
+
// Drive the suppression branch by controlling the static injector chain that
|
|
30
|
+
// `applyRuntimeInjections` walks. The slot is mutated per-test to stand in for
|
|
31
|
+
// the memory-v3 injector producing (or not producing) a block.
|
|
32
|
+
const injectorChainSlot: Injector[] = [];
|
|
33
|
+
mock.module("../plugins/defaults/memory-retrieval/injector-chain.js", () => ({
|
|
34
|
+
getInjectorChain: () => injectorChainSlot,
|
|
35
|
+
}));
|
|
32
36
|
|
|
33
37
|
const { applyRuntimeInjections } =
|
|
34
38
|
await import("../daemon/conversation-runtime-assembly.js");
|
|
@@ -42,10 +46,6 @@ function makeTurnContext(): TurnContext {
|
|
|
42
46
|
};
|
|
43
47
|
}
|
|
44
48
|
|
|
45
|
-
function wrapInPlugin(name: string, injectors: Injector[]): Plugin {
|
|
46
|
-
return { manifest: { name, version: "0.0.1" }, injectors };
|
|
47
|
-
}
|
|
48
|
-
|
|
49
49
|
/**
|
|
50
50
|
* A fake v3 injector that mirrors the real one's id + placement. The real
|
|
51
51
|
* injector's `renderMemoryBlock` already wraps its content in
|
|
@@ -88,11 +88,15 @@ function tailTexts(messages: Message[]): string[] {
|
|
|
88
88
|
|
|
89
89
|
describe("memory-v3-live v2 suppression", () => {
|
|
90
90
|
beforeEach(() => {
|
|
91
|
-
|
|
91
|
+
injectorChainSlot.length = 0;
|
|
92
|
+
// Clean baseline: no overrides → `memory-v3-live` resolves to its registry
|
|
93
|
+
// default (off). Each test seeds the flag it needs.
|
|
94
|
+
setOverridesForTesting({});
|
|
92
95
|
});
|
|
93
96
|
|
|
94
97
|
test("flag ON + v3 produced a block → v2 stripped from all turns, exactly one <memory> (the v3 block)", async () => {
|
|
95
|
-
|
|
98
|
+
setOverridesForTesting({ "memory-v3-live": true });
|
|
99
|
+
injectorChainSlot.push(v3Injector("v3 working set"));
|
|
96
100
|
|
|
97
101
|
// History: a prior user turn that still carries a v2 block (rehydrated),
|
|
98
102
|
// plus the current tail user turn with its own v2 block.
|
|
@@ -107,7 +111,6 @@ describe("memory-v3-live v2 suppression", () => {
|
|
|
107
111
|
|
|
108
112
|
const result = await applyRuntimeInjections(runMessages, {
|
|
109
113
|
turnContext: makeTurnContext(),
|
|
110
|
-
suppressV2MemoryForV3: true,
|
|
111
114
|
});
|
|
112
115
|
|
|
113
116
|
// Exactly one <memory> source across the WHOLE assembled context.
|
|
@@ -138,7 +141,8 @@ describe("memory-v3-live v2 suppression", () => {
|
|
|
138
141
|
});
|
|
139
142
|
|
|
140
143
|
test("flag ON but v3 produced NOTHING → v2 block left intact (fallback-to-v2)", async () => {
|
|
141
|
-
|
|
144
|
+
setOverridesForTesting({ "memory-v3-live": true });
|
|
145
|
+
injectorChainSlot.push(v3Injector(null));
|
|
142
146
|
|
|
143
147
|
const runMessages: Message[] = [
|
|
144
148
|
userMsgWithV2Memory("fresh recalled fact", "current question"),
|
|
@@ -146,7 +150,6 @@ describe("memory-v3-live v2 suppression", () => {
|
|
|
146
150
|
|
|
147
151
|
const result = await applyRuntimeInjections(runMessages, {
|
|
148
152
|
turnContext: makeTurnContext(),
|
|
149
|
-
suppressV2MemoryForV3: true,
|
|
150
153
|
});
|
|
151
154
|
|
|
152
155
|
// v2's block survives — the turn still ships memory.
|
|
@@ -158,7 +161,7 @@ describe("memory-v3-live v2 suppression", () => {
|
|
|
158
161
|
});
|
|
159
162
|
|
|
160
163
|
test("flag OFF → byte-for-byte identical to today even when v3 would have produced a block", async () => {
|
|
161
|
-
|
|
164
|
+
injectorChainSlot.push(v3Injector("v3 working set"));
|
|
162
165
|
|
|
163
166
|
const runMessages: Message[] = [
|
|
164
167
|
userMsgWithV2Memory("old recalled fact", "earlier question"),
|
|
@@ -169,16 +172,16 @@ describe("memory-v3-live v2 suppression", () => {
|
|
|
169
172
|
userMsgWithV2Memory("fresh recalled fact", "current question"),
|
|
170
173
|
];
|
|
171
174
|
|
|
172
|
-
// With
|
|
173
|
-
// (after-memory-prefix), but NO v2 stripping happens. This
|
|
174
|
-
// exact pre-flag assembly behavior: v2 prefix stays, v3
|
|
175
|
+
// With the flag off (registry default), the v3 injector still runs through
|
|
176
|
+
// the chain (after-memory-prefix), but NO v2 stripping happens. This
|
|
177
|
+
// captures the exact pre-flag assembly behavior: v2 prefix stays, v3
|
|
178
|
+
// splices after it.
|
|
175
179
|
const offResult = await applyRuntimeInjections(runMessages, {
|
|
176
180
|
turnContext: makeTurnContext(),
|
|
177
|
-
suppressV2MemoryForV3: false,
|
|
178
181
|
});
|
|
179
182
|
|
|
180
183
|
// The tail keeps v2's block AND gains v3's (the historical double-injection
|
|
181
|
-
// the suppression
|
|
184
|
+
// the suppression exists to prevent) — proving suppression is the ONLY
|
|
182
185
|
// behavior change and it is fully gated off here.
|
|
183
186
|
const texts = tailTexts(offResult.messages);
|
|
184
187
|
expect(texts[0]).toBe("<memory>\nfresh recalled fact\n</memory>");
|
|
@@ -190,21 +193,13 @@ describe("memory-v3-live v2 suppression", () => {
|
|
|
190
193
|
.filter((b): b is { type: "text"; text: string } => b.type === "text")
|
|
191
194
|
.map((b) => b.text);
|
|
192
195
|
expect(firstUserTexts[0]).toBe("<memory>\nold recalled fact\n</memory>");
|
|
193
|
-
|
|
194
|
-
// Strongest guard: omitting the option entirely yields the SAME result as
|
|
195
|
-
// passing it false — the default path is untouched by this PR.
|
|
196
|
-
resetPluginRegistryForTests();
|
|
197
|
-
registerPlugin(wrapInPlugin("v3", [v3Injector("v3 working set")]));
|
|
198
|
-
const defaultResult = await applyRuntimeInjections(runMessages, {
|
|
199
|
-
turnContext: makeTurnContext(),
|
|
200
|
-
});
|
|
201
|
-
expect(defaultResult.messages).toEqual(offResult.messages);
|
|
202
196
|
});
|
|
203
197
|
|
|
204
198
|
test("no v3 injector registered + flag ON → no stripping, messages untouched", async () => {
|
|
205
199
|
// No injector named memory-v3 at all (e.g. plugin not loaded): the
|
|
206
200
|
// suppression branch keys off the produced block, so nothing is stripped.
|
|
207
|
-
|
|
201
|
+
setOverridesForTesting({ "memory-v3-live": true });
|
|
202
|
+
expect(injectorChainSlot).toHaveLength(0);
|
|
208
203
|
|
|
209
204
|
const runMessages: Message[] = [
|
|
210
205
|
userMsgWithV2Memory("fresh recalled fact", "current question"),
|
|
@@ -212,7 +207,6 @@ describe("memory-v3-live v2 suppression", () => {
|
|
|
212
207
|
|
|
213
208
|
const result = await applyRuntimeInjections(runMessages, {
|
|
214
209
|
turnContext: makeTurnContext(),
|
|
215
|
-
suppressV2MemoryForV3: true,
|
|
216
210
|
});
|
|
217
211
|
|
|
218
212
|
expect(result.messages).toEqual(runMessages);
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
mock.module("../util/logger.js", () => ({
|
|
4
|
+
getLogger: () =>
|
|
5
|
+
new Proxy({} as Record<string, unknown>, {
|
|
6
|
+
get: () => () => {},
|
|
7
|
+
}),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
// Toggle for the collectUsageData opt-out the real store consults. The store
|
|
11
|
+
// module is intentionally NOT mocked here — it has its own DB-backed tests, and
|
|
12
|
+
// Bun's `mock.module` is process-global, so mocking it would leak into those
|
|
13
|
+
// tests when files share an invocation. Exercising the real store keeps every
|
|
14
|
+
// auth-fallback test order-independent.
|
|
15
|
+
let collectUsageData = true;
|
|
16
|
+
|
|
17
|
+
mock.module("../config/loader.js", () => ({
|
|
18
|
+
getConfig: () => ({
|
|
19
|
+
ui: {},
|
|
20
|
+
model: "test",
|
|
21
|
+
provider: "test",
|
|
22
|
+
memory: { enabled: false },
|
|
23
|
+
rateLimit: { maxRequestsPerMinute: 0 },
|
|
24
|
+
secretDetection: { enabled: false },
|
|
25
|
+
collectUsageData,
|
|
26
|
+
}),
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
import { queryUnreportedAuthFallbackEvents } from "../memory/auth-fallback-events-store.js";
|
|
30
|
+
import { getDb } from "../memory/db-connection.js";
|
|
31
|
+
import { initializeDb } from "../memory/db-init.js";
|
|
32
|
+
import { authFallbackEvents } from "../memory/schema.js";
|
|
33
|
+
import { GATEWAY_PRINCIPALS } from "../runtime/auth/route-policy.js";
|
|
34
|
+
import { RouteError } from "../runtime/routes/errors.js";
|
|
35
|
+
import { ROUTES } from "../runtime/routes/internal-telemetry-routes.js";
|
|
36
|
+
import type { RouteHandlerArgs } from "../runtime/routes/types.js";
|
|
37
|
+
|
|
38
|
+
initializeDb();
|
|
39
|
+
|
|
40
|
+
const route = ROUTES.find(
|
|
41
|
+
(r) => r.operationId === "internal_telemetry_auth_fallback",
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
function call(body: unknown) {
|
|
45
|
+
if (!route) throw new Error("route not found");
|
|
46
|
+
return route.handler({ body } as RouteHandlerArgs);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const VALID_BODY = {
|
|
50
|
+
window_start: 1000,
|
|
51
|
+
window_end: 2000,
|
|
52
|
+
counts: [
|
|
53
|
+
{
|
|
54
|
+
guard: "edge",
|
|
55
|
+
path: "/v1/messages",
|
|
56
|
+
failure_kind: "missing_authorization",
|
|
57
|
+
count: 5,
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
describe("internal-telemetry-routes: auth-fallback", () => {
|
|
63
|
+
beforeEach(() => {
|
|
64
|
+
collectUsageData = true;
|
|
65
|
+
getDb().delete(authFallbackEvents).run();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("route is locked to service-token callers (GATEWAY_PRINCIPALS + internal.write)", () => {
|
|
69
|
+
expect(route).toBeDefined();
|
|
70
|
+
expect(route?.endpoint).toBe("internal/telemetry/auth-fallback");
|
|
71
|
+
expect(route?.method).toBe("POST");
|
|
72
|
+
expect(route?.policy?.allowedPrincipalTypes).toEqual(GATEWAY_PRINCIPALS);
|
|
73
|
+
expect(route?.policy?.requiredScopes).toEqual(["internal.write"]);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("valid batch is persisted with snake_case → camelCase mapping", () => {
|
|
77
|
+
const result = call(VALID_BODY);
|
|
78
|
+
expect(result).toEqual({ recorded: 1 });
|
|
79
|
+
|
|
80
|
+
const rows = queryUnreportedAuthFallbackEvents(0, undefined, 100);
|
|
81
|
+
expect(rows.length).toBe(1);
|
|
82
|
+
expect(rows[0]).toMatchObject({
|
|
83
|
+
guard: "edge",
|
|
84
|
+
path: "/v1/messages",
|
|
85
|
+
failureKind: "missing_authorization",
|
|
86
|
+
count: 5,
|
|
87
|
+
windowStart: 1000,
|
|
88
|
+
windowEnd: 2000,
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("returns skipped and persists nothing under the opt-out", () => {
|
|
93
|
+
collectUsageData = false;
|
|
94
|
+
expect(call(VALID_BODY)).toEqual({ skipped: true });
|
|
95
|
+
expect(queryUnreportedAuthFallbackEvents(0, undefined, 100).length).toBe(0);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("rejects a malformed body without persisting", () => {
|
|
99
|
+
expect(() => call({ window_start: 1000 })).toThrow(RouteError);
|
|
100
|
+
expect(() => call({ ...VALID_BODY, counts: [] })).toThrow(RouteError);
|
|
101
|
+
expect(() =>
|
|
102
|
+
call({
|
|
103
|
+
...VALID_BODY,
|
|
104
|
+
counts: [{ guard: "edge", path: "/x", failure_kind: "y", count: 0 }],
|
|
105
|
+
}),
|
|
106
|
+
).toThrow(RouteError);
|
|
107
|
+
expect(queryUnreportedAuthFallbackEvents(0, undefined, 100).length).toBe(0);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
@@ -42,6 +42,10 @@ import { getDb } from "../memory/db-connection.js";
|
|
|
42
42
|
import { initializeDb } from "../memory/db-init.js";
|
|
43
43
|
import { messages } from "../memory/schema.js";
|
|
44
44
|
import { writeSlackMetadata } from "../messaging/providers/slack/message-metadata.js";
|
|
45
|
+
import {
|
|
46
|
+
_resetStreamStateForTesting,
|
|
47
|
+
recordPersistedSeq,
|
|
48
|
+
} from "../runtime/assistant-stream-state.js";
|
|
45
49
|
import { handleListMessages } from "../runtime/routes/conversation-routes.js";
|
|
46
50
|
import { BadRequestError } from "../runtime/routes/errors.js";
|
|
47
51
|
|
|
@@ -107,6 +111,7 @@ interface ListResponse {
|
|
|
107
111
|
hasMore?: boolean;
|
|
108
112
|
oldestTimestamp?: number | null;
|
|
109
113
|
oldestMessageId?: string | null;
|
|
114
|
+
seq?: number | null;
|
|
110
115
|
}
|
|
111
116
|
|
|
112
117
|
function callList(query: Record<string, string>): ListResponse {
|
|
@@ -118,9 +123,64 @@ function callList(query: Record<string, string>): ListResponse {
|
|
|
118
123
|
describe("handleListMessages page=latest", () => {
|
|
119
124
|
beforeEach(() => {
|
|
120
125
|
resetTables();
|
|
126
|
+
_resetStreamStateForTesting();
|
|
121
127
|
mockAssistantName = null;
|
|
122
128
|
});
|
|
123
129
|
|
|
130
|
+
describe("persisted seq", () => {
|
|
131
|
+
test("returns the recorded persisted seq for the conversation", () => {
|
|
132
|
+
/**
|
|
133
|
+
* The snapshot must advertise the `seq` of the last durably-persisted
|
|
134
|
+
* event so a client can align it with the `/events` stream.
|
|
135
|
+
*/
|
|
136
|
+
|
|
137
|
+
// GIVEN a conversation with persisted messages
|
|
138
|
+
const conv = createConversation();
|
|
139
|
+
seedMessages(conv.id, 3);
|
|
140
|
+
// AND the daemon has recorded a persisted seq for it
|
|
141
|
+
recordPersistedSeq(conv.id, 42);
|
|
142
|
+
|
|
143
|
+
// WHEN the snapshot is fetched
|
|
144
|
+
const body = callList({ conversationId: conv.id, page: "latest" });
|
|
145
|
+
|
|
146
|
+
// THEN the response carries that seq
|
|
147
|
+
expect(body.seq).toBe(42);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("returns null seq when nothing has been persisted in-process", () => {
|
|
151
|
+
/**
|
|
152
|
+
* A cold conversation (or one aged out / post-restart) reports no seq,
|
|
153
|
+
* signalling the client to cold-start rather than align to a stale
|
|
154
|
+
* position.
|
|
155
|
+
*/
|
|
156
|
+
|
|
157
|
+
// GIVEN a conversation with no recorded persisted seq
|
|
158
|
+
const conv = createConversation();
|
|
159
|
+
seedMessages(conv.id, 2);
|
|
160
|
+
|
|
161
|
+
// WHEN the snapshot is fetched
|
|
162
|
+
const body = callList({ conversationId: conv.id, page: "latest" });
|
|
163
|
+
|
|
164
|
+
// THEN seq is null
|
|
165
|
+
expect(body.seq).toBeNull();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("the no-pagination path also returns the persisted seq", () => {
|
|
169
|
+
/** `seq` is present on every resolved-conversation response shape. */
|
|
170
|
+
|
|
171
|
+
// GIVEN a conversation with a recorded persisted seq
|
|
172
|
+
const conv = createConversation();
|
|
173
|
+
seedMessages(conv.id, 2);
|
|
174
|
+
recordPersistedSeq(conv.id, 7);
|
|
175
|
+
|
|
176
|
+
// WHEN fetched with no pagination params
|
|
177
|
+
const body = callList({ conversationId: conv.id });
|
|
178
|
+
|
|
179
|
+
// THEN the seq still rides along
|
|
180
|
+
expect(body.seq).toBe(7);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
124
184
|
test("page=latest with no limit returns all messages chronologically", () => {
|
|
125
185
|
const conv = createConversation();
|
|
126
186
|
seedMessages(conv.id, 120);
|
|
@@ -57,6 +57,8 @@ interface MessagePayload {
|
|
|
57
57
|
role: string;
|
|
58
58
|
toolCalls?: ToolCallPayload[];
|
|
59
59
|
textSegments?: string[];
|
|
60
|
+
contentOrder?: string[];
|
|
61
|
+
contentBlocks?: Array<{ type: string; [key: string]: unknown }>;
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
describe("handleListMessages tool_result merging", () => {
|
|
@@ -106,6 +108,24 @@ describe("handleListMessages tool_result merging", () => {
|
|
|
106
108
|
expect(toolCalls).toHaveLength(1);
|
|
107
109
|
expect(toolCalls![0].name).toBe("bash");
|
|
108
110
|
expect(toolCalls![0].result).toBe("file1.txt\nfile2.txt");
|
|
111
|
+
|
|
112
|
+
// The unified contentBlocks projection ships alongside the legacy arrays,
|
|
113
|
+
// in contentOrder order, with the tool_result already paired onto the
|
|
114
|
+
// tool_use block.
|
|
115
|
+
expect(body.messages[1].contentOrder).toEqual(["text:0", "tool:0"]);
|
|
116
|
+
expect(body.messages[1].contentBlocks).toEqual([
|
|
117
|
+
{ type: "text", text: "Running command." },
|
|
118
|
+
{
|
|
119
|
+
type: "tool_use",
|
|
120
|
+
toolCall: {
|
|
121
|
+
id: "tu1",
|
|
122
|
+
name: "bash",
|
|
123
|
+
input: { command: "ls" },
|
|
124
|
+
result: "file1.txt\nfile2.txt",
|
|
125
|
+
isError: false,
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
]);
|
|
109
129
|
});
|
|
110
130
|
|
|
111
131
|
test("merges multiple tool_results into matching tool_uses", async () => {
|
|
@@ -6,7 +6,7 @@ mock.module("../util/logger.js", () => ({
|
|
|
6
6
|
getLogger: () => makeMockLogger(),
|
|
7
7
|
}));
|
|
8
8
|
|
|
9
|
-
import { getDb } from "../memory/db-connection.js";
|
|
9
|
+
import { getDb, getSqlite } from "../memory/db-connection.js";
|
|
10
10
|
import { initializeDb } from "../memory/db-init.js";
|
|
11
11
|
import {
|
|
12
12
|
getUsageDayBuckets,
|
|
@@ -456,6 +456,228 @@ describe("getUsageTotals", () => {
|
|
|
456
456
|
});
|
|
457
457
|
});
|
|
458
458
|
|
|
459
|
+
describe("usage aggregation schedule filters", () => {
|
|
460
|
+
beforeEach(() => {
|
|
461
|
+
const db = getDb();
|
|
462
|
+
db.run(`DELETE FROM llm_usage_events`);
|
|
463
|
+
db.run(`DELETE FROM cron_runs`);
|
|
464
|
+
db.run(`DELETE FROM cron_jobs`);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
function insertScheduleJob(id: string, name: string): void {
|
|
468
|
+
const now = Date.UTC(2026, 0, 1);
|
|
469
|
+
getSqlite().run(
|
|
470
|
+
`INSERT INTO cron_jobs (
|
|
471
|
+
id,
|
|
472
|
+
name,
|
|
473
|
+
cron_expression,
|
|
474
|
+
message,
|
|
475
|
+
next_run_at,
|
|
476
|
+
created_by,
|
|
477
|
+
created_at,
|
|
478
|
+
updated_at
|
|
479
|
+
) VALUES (?, ?, '* * * * *', 'Example scheduled task', ?, 'user', ?, ?)`,
|
|
480
|
+
[id, name, now, now, now],
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function insertScheduleRun({
|
|
485
|
+
id,
|
|
486
|
+
scheduleId,
|
|
487
|
+
conversationId,
|
|
488
|
+
startedAt,
|
|
489
|
+
finishedAt,
|
|
490
|
+
}: {
|
|
491
|
+
id: string;
|
|
492
|
+
scheduleId: string;
|
|
493
|
+
conversationId: string;
|
|
494
|
+
startedAt: number;
|
|
495
|
+
finishedAt: number | null;
|
|
496
|
+
}): void {
|
|
497
|
+
getSqlite().run(
|
|
498
|
+
`INSERT INTO cron_runs (
|
|
499
|
+
id,
|
|
500
|
+
job_id,
|
|
501
|
+
status,
|
|
502
|
+
started_at,
|
|
503
|
+
finished_at,
|
|
504
|
+
conversation_id,
|
|
505
|
+
created_at
|
|
506
|
+
) VALUES (?, ?, 'ok', ?, ?, ?, ?)`,
|
|
507
|
+
[id, scheduleId, startedAt, finishedAt, conversationId, startedAt],
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function seedScheduleUsage(): void {
|
|
512
|
+
insertScheduleJob("schedule-a", "Morning summary");
|
|
513
|
+
insertScheduleJob("schedule-b", "Nightly sync");
|
|
514
|
+
insertScheduleRun({
|
|
515
|
+
id: "run-a-1",
|
|
516
|
+
scheduleId: "schedule-a",
|
|
517
|
+
conversationId: "conv-reused",
|
|
518
|
+
startedAt: 1_000,
|
|
519
|
+
finishedAt: 2_000,
|
|
520
|
+
});
|
|
521
|
+
insertScheduleRun({
|
|
522
|
+
id: "run-b-1",
|
|
523
|
+
scheduleId: "schedule-b",
|
|
524
|
+
conversationId: "conv-reused",
|
|
525
|
+
startedAt: 3_000,
|
|
526
|
+
finishedAt: 3_500,
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
insertEventAt(
|
|
530
|
+
900,
|
|
531
|
+
{ conversationId: "conv-reused", inputTokens: 90 },
|
|
532
|
+
{ estimatedCostUsd: 0.09, pricingStatus: "priced" },
|
|
533
|
+
);
|
|
534
|
+
insertEventAt(
|
|
535
|
+
1_000,
|
|
536
|
+
{
|
|
537
|
+
conversationId: "conv-reused",
|
|
538
|
+
callSite: "mainAgent",
|
|
539
|
+
inputTokens: 100,
|
|
540
|
+
},
|
|
541
|
+
{ estimatedCostUsd: 0.1, pricingStatus: "priced" },
|
|
542
|
+
);
|
|
543
|
+
insertEventAt(
|
|
544
|
+
1_500,
|
|
545
|
+
{
|
|
546
|
+
conversationId: "conv-reused",
|
|
547
|
+
callSite: "mainAgent",
|
|
548
|
+
inputTokens: 200,
|
|
549
|
+
},
|
|
550
|
+
{ estimatedCostUsd: 0.2, pricingStatus: "priced" },
|
|
551
|
+
);
|
|
552
|
+
insertEventAt(
|
|
553
|
+
2_000,
|
|
554
|
+
{
|
|
555
|
+
conversationId: "conv-reused",
|
|
556
|
+
callSite: "mainAgent",
|
|
557
|
+
inputTokens: 300,
|
|
558
|
+
},
|
|
559
|
+
{ estimatedCostUsd: 0.3, pricingStatus: "priced" },
|
|
560
|
+
);
|
|
561
|
+
insertEventAt(
|
|
562
|
+
2_500,
|
|
563
|
+
{ conversationId: "conv-reused", inputTokens: 400 },
|
|
564
|
+
{ estimatedCostUsd: 0.4, pricingStatus: "priced" },
|
|
565
|
+
);
|
|
566
|
+
insertEventAt(
|
|
567
|
+
3_200,
|
|
568
|
+
{
|
|
569
|
+
conversationId: "conv-reused",
|
|
570
|
+
provider: "openai",
|
|
571
|
+
inputTokens: 500,
|
|
572
|
+
},
|
|
573
|
+
{ estimatedCostUsd: 0.5, pricingStatus: "priced" },
|
|
574
|
+
);
|
|
575
|
+
insertEventAt(
|
|
576
|
+
1_500,
|
|
577
|
+
{ conversationId: "conv-other", inputTokens: 800 },
|
|
578
|
+
{ estimatedCostUsd: 0.8, pricingStatus: "priced" },
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
test("filters totals, buckets, breakdowns, and series by cron run windows", () => {
|
|
583
|
+
seedScheduleUsage();
|
|
584
|
+
const range = { from: 0, to: 4_000 };
|
|
585
|
+
const filter = { scheduleId: "schedule-a" };
|
|
586
|
+
|
|
587
|
+
const totals = getUsageTotals(range, filter);
|
|
588
|
+
expect(totals.eventCount).toBe(3);
|
|
589
|
+
expect(totals.totalInputTokens).toBe(600);
|
|
590
|
+
expect(totals.totalEstimatedCostUsd).toBeCloseTo(0.6);
|
|
591
|
+
|
|
592
|
+
const dailyBuckets = getUsageDayBuckets(range, "UTC", {}, filter);
|
|
593
|
+
expect(dailyBuckets).toHaveLength(1);
|
|
594
|
+
expect(dailyBuckets[0].totalInputTokens).toBe(600);
|
|
595
|
+
expect(dailyBuckets[0].eventCount).toBe(3);
|
|
596
|
+
|
|
597
|
+
const hourlyBuckets = getUsageHourBuckets(range, "UTC", {}, filter);
|
|
598
|
+
expect(hourlyBuckets).toHaveLength(1);
|
|
599
|
+
expect(hourlyBuckets[0].totalInputTokens).toBe(600);
|
|
600
|
+
expect(hourlyBuckets[0].eventCount).toBe(3);
|
|
601
|
+
|
|
602
|
+
const breakdown = getUsageGroupBreakdown(range, "provider", filter);
|
|
603
|
+
expect(breakdown).toHaveLength(1);
|
|
604
|
+
expect(breakdown[0].group).toBe("anthropic");
|
|
605
|
+
expect(breakdown[0].totalInputTokens).toBe(600);
|
|
606
|
+
expect(breakdown[0].eventCount).toBe(3);
|
|
607
|
+
|
|
608
|
+
const series = getUsageGroupedSeries(
|
|
609
|
+
range,
|
|
610
|
+
"call_site",
|
|
611
|
+
"daily",
|
|
612
|
+
"UTC",
|
|
613
|
+
{},
|
|
614
|
+
filter,
|
|
615
|
+
);
|
|
616
|
+
expect(series).toHaveLength(1);
|
|
617
|
+
expect(series[0].totalInputTokens).toBe(600);
|
|
618
|
+
expect(series[0].groups["null:call_site"]).toBeUndefined();
|
|
619
|
+
expect(series[0].groups["null:schedule"]).toBeUndefined();
|
|
620
|
+
expect(series[0].groups["value:mainAgent"]).toMatchObject({
|
|
621
|
+
group: "Main Agent",
|
|
622
|
+
totalInputTokens: 600,
|
|
623
|
+
eventCount: 3,
|
|
624
|
+
});
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
test("groups schedule-attributed usage by schedule id with schedule names", () => {
|
|
628
|
+
seedScheduleUsage();
|
|
629
|
+
const range = { from: 0, to: 4_000 };
|
|
630
|
+
|
|
631
|
+
const breakdown = getUsageGroupBreakdown(range, "schedule");
|
|
632
|
+
const scheduleA = breakdown.find((row) => row.groupKey === "schedule-a");
|
|
633
|
+
const scheduleB = breakdown.find((row) => row.groupKey === "schedule-b");
|
|
634
|
+
const other = breakdown.find((row) => row.groupKey === null);
|
|
635
|
+
|
|
636
|
+
expect(scheduleA).toMatchObject({
|
|
637
|
+
group: "Morning summary",
|
|
638
|
+
groupId: "schedule-a",
|
|
639
|
+
groupKey: "schedule-a",
|
|
640
|
+
totalInputTokens: 600,
|
|
641
|
+
eventCount: 3,
|
|
642
|
+
});
|
|
643
|
+
expect(scheduleB).toMatchObject({
|
|
644
|
+
group: "Nightly sync",
|
|
645
|
+
groupId: "schedule-b",
|
|
646
|
+
groupKey: "schedule-b",
|
|
647
|
+
totalInputTokens: 500,
|
|
648
|
+
eventCount: 1,
|
|
649
|
+
});
|
|
650
|
+
expect(other).toMatchObject({
|
|
651
|
+
group: "Other",
|
|
652
|
+
groupId: null,
|
|
653
|
+
groupKey: null,
|
|
654
|
+
totalInputTokens: 1_290,
|
|
655
|
+
eventCount: 3,
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
const series = getUsageGroupedSeries(range, "schedule", "daily", "UTC", {});
|
|
659
|
+
expect(series).toHaveLength(1);
|
|
660
|
+
expect(series[0].groups["value:schedule-a"]).toMatchObject({
|
|
661
|
+
group: "Morning summary",
|
|
662
|
+
groupKey: "schedule-a",
|
|
663
|
+
totalInputTokens: 600,
|
|
664
|
+
eventCount: 3,
|
|
665
|
+
});
|
|
666
|
+
expect(series[0].groups["value:schedule-b"]).toMatchObject({
|
|
667
|
+
group: "Nightly sync",
|
|
668
|
+
groupKey: "schedule-b",
|
|
669
|
+
totalInputTokens: 500,
|
|
670
|
+
eventCount: 1,
|
|
671
|
+
});
|
|
672
|
+
expect(series[0].groups["null:schedule"]).toMatchObject({
|
|
673
|
+
group: "Other",
|
|
674
|
+
groupKey: null,
|
|
675
|
+
totalInputTokens: 1_290,
|
|
676
|
+
eventCount: 3,
|
|
677
|
+
});
|
|
678
|
+
});
|
|
679
|
+
});
|
|
680
|
+
|
|
459
681
|
describe("getUsageDayBuckets", () => {
|
|
460
682
|
beforeEach(() => {
|
|
461
683
|
const db = getDb();
|