@vellumai/assistant 0.8.8 → 0.8.9-staging.2
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/ARCHITECTURE.md +6 -6
- package/bun.lock +2 -2
- package/examples/plugins/echo/README.md +61 -60
- package/examples/plugins/echo/hooks/post-tool-use.ts +18 -0
- package/examples/plugins/echo/hooks/stop.ts +16 -0
- package/examples/plugins/echo/hooks/user-prompt-submit.ts +18 -0
- package/examples/plugins/echo/package.json +1 -2
- package/examples/plugins/echo/src/emit.ts +19 -0
- package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +7 -6
- package/openapi.yaml +235 -6
- package/package.json +2 -2
- package/src/__tests__/agent-loop-callsite-precedence.test.ts +69 -14
- package/src/__tests__/agent-loop-exit-reason.test.ts +204 -144
- package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +50 -35
- package/src/__tests__/agent-loop-output-hooks.test.ts +357 -0
- package/src/__tests__/agent-loop-override-profile.test.ts +25 -6
- package/src/__tests__/agent-loop-provider-error-recording.test.ts +41 -21
- package/src/__tests__/agent-loop-thinking.test.ts +36 -20
- package/src/__tests__/agent-loop.test.ts +441 -96
- package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +14 -14
- package/src/__tests__/agent-wake-override-profile.test.ts +17 -21
- package/src/__tests__/anthropic-provider.test.ts +1 -1
- package/src/__tests__/app-builder-tool-scripts.test.ts +21 -0
- package/src/__tests__/app-control-flow.test.ts +1 -1
- package/src/__tests__/app-dir-path-guard.test.ts +1 -0
- package/src/__tests__/app-executors.test.ts +132 -0
- package/src/__tests__/approval-cascade.test.ts +5 -4
- package/src/__tests__/approval-routes-http.test.ts +4 -1
- package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
- package/src/__tests__/channel-approval-routes.test.ts +1 -1
- package/src/__tests__/channel-approvals.test.ts +1 -1
- package/src/__tests__/compaction-circuit.test.ts +258 -0
- package/src/__tests__/compaction-direct.test.ts +132 -0
- package/src/__tests__/compaction-events.test.ts +5 -5
- package/src/__tests__/compactor-web-search-strip.test.ts +213 -0
- package/src/__tests__/context-overflow-reducer.test.ts +1 -1
- package/src/__tests__/conversation-abort-tool-results.test.ts +6 -4
- package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +7 -10
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +78 -119
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +142 -218
- package/src/__tests__/conversation-agent-loop.test.ts +297 -586
- package/src/__tests__/conversation-clean-command.test.ts +5 -2
- package/src/__tests__/conversation-confirmation-signals.test.ts +5 -4
- package/src/__tests__/conversation-crud-inference-profile.test.ts +7 -9
- package/src/__tests__/conversation-history-web-search.test.ts +1 -1
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +4 -4
- package/src/__tests__/conversation-process-callsite.test.ts +14 -14
- package/src/__tests__/conversation-provider-retry-repair.test.ts +70 -65
- package/src/__tests__/conversation-queue.test.ts +9 -9
- package/src/__tests__/conversation-runtime-assembly.test.ts +923 -231
- package/src/__tests__/conversation-runtime-workspace.test.ts +115 -20
- package/src/__tests__/conversation-slash-queue.test.ts +6 -4
- package/src/__tests__/conversation-slash-unknown.test.ts +6 -4
- package/src/__tests__/conversation-speed-override.test.ts +10 -9
- package/src/__tests__/conversation-starter-routes.test.ts +14 -6
- package/src/__tests__/conversation-workspace-cache-state.test.ts +23 -20
- package/src/__tests__/conversation-workspace-injection.test.ts +68 -6
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +14 -11
- package/src/__tests__/conversations-import-system-filter.test.ts +101 -0
- package/src/__tests__/credential-security-invariants.test.ts +0 -1
- package/src/__tests__/db-acp-history.test.ts +101 -0
- package/src/__tests__/dynamic-page-surface.test.ts +31 -0
- package/src/__tests__/empty-response-hook.test.ts +1 -1
- package/src/__tests__/file-write-tool.test.ts +63 -0
- package/src/__tests__/gateway-only-guard.test.ts +12 -2
- package/src/__tests__/guardian-grant-minting.test.ts +1 -1
- package/src/__tests__/guardian-routing-invariants.test.ts +2 -4
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +1 -1
- package/src/__tests__/heartbeat-disk-pressure.test.ts +1 -0
- package/src/__tests__/heartbeat-service.test.ts +1 -0
- package/src/__tests__/history-repair-hook.test.ts +1 -1
- package/src/__tests__/host-app-control-routes.test.ts +1 -1
- package/src/__tests__/host-cu-routes-targeted.test.ts +3 -3
- package/src/__tests__/inference-profile-reaper.test.ts +62 -0
- package/src/__tests__/inference-profile-session-handler.test.ts +86 -0
- package/src/__tests__/injector-background-turn.test.ts +13 -23
- package/src/__tests__/injector-chain.test.ts +268 -44
- package/src/__tests__/injector-disk-pressure.test.ts +210 -52
- package/src/__tests__/injector-document-comments.test.ts +97 -114
- package/src/__tests__/injector-pkb-v2-silenced.test.ts +2 -2
- package/src/__tests__/injector-v3-suppression.test.ts +4 -4
- package/src/__tests__/list-messages-client-message-id.test.ts +91 -0
- package/src/__tests__/list-messages-hidden-metadata.test.ts +38 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +131 -26
- package/src/__tests__/memory-v2-static-injector.test.ts +86 -8
- package/src/__tests__/parallel-tool.benchmark.test.ts +35 -8
- package/src/__tests__/plugin-api-shim.test.ts +6 -9
- package/src/__tests__/plugin-bootstrap.test.ts +12 -23
- package/src/__tests__/plugin-registry.test.ts +3 -49
- package/src/__tests__/plugin-types.test.ts +0 -70
- package/src/__tests__/pre-model-call-sanitize.test.ts +109 -0
- package/src/__tests__/reaction-persistence.test.ts +1 -1
- package/src/__tests__/send-endpoint-busy.test.ts +4 -1
- package/src/__tests__/skill-feature-flags-integration.test.ts +33 -0
- package/src/__tests__/steer-tool-repair.test.ts +1 -1
- package/src/__tests__/subagent-call-site-routing.test.ts +1 -1
- package/src/__tests__/subagent-detail.test.ts +25 -7
- package/src/__tests__/subagent-fork-notifications.test.ts +1 -3
- package/src/__tests__/subagent-fork-spawn.test.ts +1 -1
- package/src/__tests__/subagent-manager-notify.test.ts +1 -3
- package/src/__tests__/subagent-notify-parent.test.ts +1 -3
- package/src/__tests__/subagent-spawn-tool-fork.test.ts +1 -1
- package/src/__tests__/title-generate-hook.test.ts +1 -1
- package/src/__tests__/tool-error-hook.test.ts +1 -1
- package/src/__tests__/tool-result-truncate-hook.test.ts +1 -1
- package/src/__tests__/user-plugin-loader.test.ts +54 -286
- package/src/acp/__tests__/agent-process.test.ts +161 -0
- package/src/acp/__tests__/client-handler.test.ts +40 -0
- package/src/acp/__tests__/helpers/acp-history-db.ts +82 -0
- package/src/acp/__tests__/helpers/exec-file-stub.ts +106 -0
- package/src/acp/__tests__/prepare-agent-env.test.ts +97 -0
- package/src/acp/__tests__/session-manager-persistence.test.ts +95 -28
- package/src/acp/__tests__/session-manager-resume.test.ts +888 -0
- package/src/acp/agent-process.ts +61 -1
- package/src/acp/auto-install.test.ts +280 -0
- package/src/acp/auto-install.ts +232 -0
- package/src/acp/client-handler.ts +31 -0
- package/src/acp/feature-gate.test.ts +48 -0
- package/src/acp/feature-gate.ts +34 -0
- package/src/acp/prepare-agent-env.ts +80 -27
- package/src/acp/resolve-agent.test.ts +225 -9
- package/src/acp/resolve-agent.ts +122 -17
- package/src/acp/resume-hint.ts +23 -0
- package/src/acp/session-manager.ts +507 -73
- package/src/agent/compaction-circuit.ts +60 -102
- package/src/agent/loop.ts +414 -248
- package/src/api/responses/conversation-message.ts +14 -1
- package/src/approvals/guardian-request-resolvers.ts +1 -1
- package/src/background-wake/next-wake.ts +1 -0
- package/src/cli/commands/db/__tests__/repair.test.ts +3 -1
- package/src/cli/commands/plugins.ts +43 -37
- package/src/cli/lib/__tests__/install-from-github.test.ts +429 -111
- package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +196 -0
- package/src/cli/lib/__tests__/plugin-details.test.ts +372 -0
- package/src/cli/lib/__tests__/plugin-marketplace.test.ts +220 -0
- package/src/cli/lib/__tests__/search-plugins.test.ts +226 -32
- package/src/cli/lib/install-from-github.ts +464 -55
- package/src/cli/lib/plugin-catalog-cache.ts +84 -0
- package/src/cli/lib/plugin-details.ts +409 -0
- package/src/cli/lib/plugin-marketplace.ts +197 -0
- package/src/cli/lib/search-plugins.ts +195 -29
- package/src/config/__tests__/feature-flag-registry-guard.test.ts +2 -2
- package/src/config/acp-defaults.test.ts +10 -0
- package/src/config/acp-defaults.ts +6 -0
- package/src/config/bundled-skills/acp/SKILL.md +85 -33
- package/src/config/bundled-skills/acp/TOOLS.json +4 -4
- package/src/config/bundled-skills/app-builder/SKILL.md +224 -381
- package/src/config/bundled-skills/app-builder/TOOLS.json +72 -2
- package/src/config/bundled-skills/app-builder/references/DESIGN_SYSTEM.md +48 -0
- package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +57 -0
- package/src/config/bundled-skills/app-builder/references/SLIDES.md +38 -0
- package/src/config/bundled-skills/app-builder/tools/app-list.ts +62 -0
- package/src/config/bundled-skills/app-builder/tools/app-update.ts +18 -0
- package/src/config/bundled-skills/document-editor/SKILL.md +28 -23
- package/src/config/bundled-skills/document-editor/TOOLS.json +1 -1
- package/src/config/bundled-tool-registry.ts +4 -0
- package/src/config/call-site-defaults.ts +0 -2
- package/src/config/feature-flag-registry.json +15 -6
- package/src/config/schemas/call-site-catalog.ts +0 -14
- package/src/config/schemas/heartbeat.ts +9 -0
- package/src/config/schemas/llm.ts +0 -2
- package/src/context/compactor.ts +22 -4
- package/src/context/strip-injections.ts +8 -2
- package/src/context/window-manager.ts +27 -13
- package/src/daemon/conversation-agent-loop-handlers.ts +10 -35
- package/src/daemon/conversation-agent-loop.ts +175 -1055
- package/src/daemon/conversation-lifecycle.ts +11 -255
- package/src/daemon/conversation-process.ts +8 -136
- package/src/daemon/conversation-registry.ts +159 -0
- package/src/daemon/conversation-runtime-assembly.ts +293 -392
- package/src/daemon/conversation-store.ts +9 -90
- package/src/daemon/conversation-surfaces.ts +24 -8
- package/src/daemon/conversation-workspace.ts +17 -0
- package/src/daemon/conversation.ts +404 -57
- package/src/daemon/disk-pressure-policy.ts +0 -1
- package/src/daemon/external-plugins-bootstrap.ts +14 -19
- package/src/daemon/handlers/conversations.ts +3 -1
- package/src/daemon/handlers/skills.ts +4 -1
- package/src/daemon/host-proxy-preactivation.ts +1 -3
- package/src/daemon/lifecycle.ts +21 -7
- package/src/daemon/server.ts +2 -0
- package/src/daemon/wake-conversation-ops.ts +269 -0
- package/src/embedded/plugin-api.ts +2 -2
- package/src/export/__tests__/transcript-formatter.test.ts +5 -0
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +3 -0
- package/src/heartbeat/heartbeat-run-store.ts +23 -1
- package/src/heartbeat/heartbeat-service.ts +26 -0
- package/src/ipc/__tests__/browser-ipc.test.ts +1 -1
- package/src/ipc/__tests__/ui-request-route.test.ts +3 -3
- package/src/ipc/skill-routes/__tests__/memory.test.ts +15 -0
- package/src/ipc/skill-routes/memory.ts +4 -2
- package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +87 -0
- package/src/memory/conversation-crud.ts +29 -19
- package/src/memory/conversation-starter-checkpoints.ts +1 -0
- package/src/memory/db-init.ts +2 -0
- package/src/memory/job-handlers/conversation-starters.ts +13 -2
- package/src/memory/jobs/__tests__/embed-concept-page.test.ts +5 -4
- package/src/memory/jobs-worker.ts +25 -1
- package/src/memory/migrations/272-acp-session-history-cwd.ts +36 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/schema/acp.ts +4 -0
- package/src/memory/v2/__tests__/consolidation-job.test.ts +3 -3
- package/src/memory/v2/consolidation-job.ts +13 -4
- package/src/plugin-api/constants.ts +4 -0
- package/src/plugin-api/index.ts +6 -5
- package/src/plugin-api/types.ts +75 -0
- package/src/plugins/defaults/compaction/compact.ts +59 -0
- package/src/{daemon → plugins/defaults/compaction}/context-overflow-reducer.ts +7 -7
- package/src/plugins/defaults/compaction/manager-store.ts +57 -0
- package/src/plugins/defaults/compaction/package.json +1 -2
- package/src/plugins/defaults/empty-response/package.json +0 -1
- package/src/plugins/defaults/history-repair/package.json +0 -1
- package/src/plugins/defaults/index.ts +135 -26
- package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +100 -52
- package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +184 -74
- package/src/plugins/defaults/memory-retrieval/injector-chain.ts +2 -2
- package/src/plugins/defaults/{injectors/register.ts → memory-retrieval/injectors.ts} +148 -73
- package/src/plugins/defaults/memory-retrieval/unified-turn-context.ts +223 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/assign.test.ts +4 -4
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/live-integration.test.ts +9 -6
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/maintain-job.test.ts +5 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/orchestrate.test.ts +8 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/reconcile.test.ts +2 -2
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/render-injection.test.ts +1 -1
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/router.test.ts +10 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selection-log-store.test.ts +8 -8
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selector.test.ts +5 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/shadow-plugin.test.ts +16 -17
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/types.test.ts +2 -2
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/assign.ts +9 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/capabilities.ts +5 -2
- package/src/plugins/defaults/memory-v3-shadow/hooks/post-compact.ts +14 -0
- package/src/plugins/defaults/memory-v3-shadow/hooks/user-prompt-submit.ts +19 -0
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +75 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/maintain-job.ts +15 -8
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/orchestrate.ts +2 -2
- package/src/plugins/defaults/memory-v3-shadow/package.json +14 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/page-content.ts +2 -2
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/provider-blocks.ts +1 -1
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/reconcile.ts +7 -3
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/render-injection.ts +1 -1
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/router.ts +5 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/selection-log-store.ts +4 -4
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/selector.ts +7 -7
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/shadow-plugin.ts +32 -94
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/tree.ts +1 -1
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/types.ts +1 -1
- package/src/plugins/defaults/title-generate/package.json +0 -1
- package/src/plugins/defaults/tool-error/package.json +0 -1
- package/src/plugins/defaults/tool-result-truncate/package.json +0 -1
- package/src/plugins/pipeline.ts +6 -293
- package/src/plugins/registry.ts +9 -37
- package/src/plugins/types.ts +76 -381
- package/src/plugins/user-loader.ts +30 -127
- package/src/prompts/__tests__/system-prompt.test.ts +6 -0
- package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +35 -3
- package/src/runtime/__tests__/agent-wake.test.ts +555 -691
- package/src/runtime/__tests__/interactive-ui.test.ts +1 -1
- package/src/runtime/agent-wake.ts +108 -209
- package/src/runtime/assistant-event-hub.ts +1 -1
- package/src/runtime/channel-approvals.ts +1 -1
- package/src/runtime/interactive-ui.ts +1 -1
- package/src/runtime/routes/__tests__/acp-routes.test.ts +315 -55
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +1 -1
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +423 -73
- package/src/runtime/routes/__tests__/surface-action-routes.test.ts +5 -4
- package/src/runtime/routes/__tests__/surface-content-routes.test.ts +4 -1
- package/src/runtime/routes/acp-routes.test.ts +89 -25
- package/src/runtime/routes/acp-routes.ts +81 -29
- package/src/runtime/routes/approval-routes.ts +1 -1
- package/src/runtime/routes/browser-routes.ts +1 -1
- package/src/runtime/routes/browser-tabs-routes.ts +6 -10
- package/src/runtime/routes/conversation-cli-routes.ts +1 -1
- package/src/runtime/routes/conversation-list-routes.ts +1 -1
- package/src/runtime/routes/conversation-query-routes.ts +1 -1
- package/src/runtime/routes/conversation-routes.ts +28 -2
- package/src/runtime/routes/conversation-starter-routes.ts +13 -7
- package/src/runtime/routes/conversations-import-routes.ts +24 -7
- package/src/runtime/routes/host-app-control-routes.ts +1 -1
- package/src/runtime/routes/host-cu-routes.ts +1 -1
- package/src/runtime/routes/identity-routes.ts +18 -3
- package/src/runtime/routes/inbound-message-handler.ts +1 -1
- package/src/runtime/routes/inference-profile-session-handler.ts +11 -0
- package/src/runtime/routes/inference-profile-session-reaper.ts +6 -0
- package/src/runtime/routes/memory-v3-routes.ts +16 -6
- package/src/runtime/routes/playground/helpers.ts +1 -1
- package/src/runtime/routes/plugins-routes.ts +337 -35
- package/src/runtime/routes/surface-conversation-resolver.ts +4 -3
- package/src/runtime/routes/work-items-routes.ts +2 -4
- package/src/runtime/services/conversation-serializer.ts +1 -1
- package/src/signals/cancel.ts +2 -4
- package/src/subagent/manager.ts +21 -5
- package/src/tools/acp/context.ts +20 -0
- package/src/tools/acp/list-agents.test.ts +8 -2
- package/src/tools/acp/spawn.test.ts +176 -195
- package/src/tools/acp/spawn.ts +37 -172
- package/src/tools/acp/steer.test.ts +105 -8
- package/src/tools/acp/steer.ts +48 -17
- package/src/tools/apps/executors.ts +166 -50
- package/src/tools/filesystem/write.ts +34 -0
- package/src/tools/subagent/spawn.ts +2 -4
- package/src/tools/ui-surface/definitions.ts +25 -5
- package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +4 -5
- package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +69 -45
- package/docs/plugins.md +0 -832
- package/examples/plugins/echo/register.ts +0 -143
- package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -405
- package/src/__tests__/compaction-pipeline.test.ts +0 -210
- package/src/__tests__/compaction-timeout-recovery.test.ts +0 -251
- package/src/__tests__/overflow-reduce-pipeline.test.ts +0 -667
- package/src/__tests__/pipeline-runner.test.ts +0 -554
- package/src/__tests__/plugin-external-api.test.ts +0 -68
- package/src/daemon/wake-target-adapter.ts +0 -253
- package/src/plugins/defaults/circuit-breaker/middlewares/circuitBreaker.ts +0 -93
- package/src/plugins/defaults/circuit-breaker/package.json +0 -15
- package/src/plugins/defaults/circuit-breaker/register.ts +0 -39
- package/src/plugins/defaults/compaction/middlewares/compaction.ts +0 -25
- package/src/plugins/defaults/compaction/register.ts +0 -35
- package/src/plugins/defaults/compaction/terminal.ts +0 -73
- package/src/plugins/defaults/empty-response/register.ts +0 -23
- package/src/plugins/defaults/history-repair/register.ts +0 -24
- package/src/plugins/defaults/overflow-reduce/middlewares/overflowReduce.ts +0 -126
- package/src/plugins/defaults/overflow-reduce/package.json +0 -15
- package/src/plugins/defaults/overflow-reduce/register.ts +0 -42
- package/src/plugins/defaults/title-generate/register.ts +0 -35
- package/src/plugins/defaults/tool-error/register.ts +0 -23
- package/src/plugins/defaults/tool-result-truncate/register.ts +0 -24
- package/src/plugins/external-api.ts +0 -104
- package/src/proactive-artifact/aux-message-injector.ts +0 -97
- package/src/proactive-artifact/decision.test.ts +0 -226
- package/src/proactive-artifact/decision.ts +0 -165
- package/src/proactive-artifact/index.ts +0 -7
- package/src/proactive-artifact/job.test.ts +0 -962
- package/src/proactive-artifact/job.ts +0 -372
- package/src/proactive-artifact/message-copy.ts +0 -58
- package/src/proactive-artifact/trigger-state.test.ts +0 -286
- package/src/proactive-artifact/trigger-state.ts +0 -123
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/capabilities.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/core.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/eval-turns.json +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/live-turns.json +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/health.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/needle.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/provider-blocks.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/snapshot.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/tree.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-eviction.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-skeleton.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/core.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/README.md +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/assignments.json +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/core.json +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-x.md +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-y.md +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-b/topic-z.md +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/health.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/llm-retry.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/needle.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/snapshot.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/working-set.ts +0 -0
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Verifies that the
|
|
3
|
-
* `
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* long-TTL cache breakpoint on the most recent STABLE message instead
|
|
2
|
+
* Verifies that the `memory-v3-live` cache-anchor signal surfaces on every
|
|
3
|
+
* `SendMessageOptions.config` the loop emits. When the flag is on, the latest
|
|
4
|
+
* user message carries a volatile `<memory>` block, so the loop sets
|
|
5
|
+
* `providerConfig.mutableLatestUserMessage` to tell the provider to anchor its
|
|
6
|
+
* long-TTL cache breakpoint on the most recent STABLE message instead.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
* from
|
|
10
|
-
* is
|
|
8
|
+
* The loop reads the flag directly where it assembles `providerConfig`, so the
|
|
9
|
+
* signal is sourced from the flag rather than a run option. When the flag is
|
|
10
|
+
* off the field is omitted (not `false`/`undefined`) so the wire stays
|
|
11
|
+
* byte-identical to today.
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
|
-
import { describe, expect, mock, test } from "bun:test";
|
|
14
|
+
import { afterEach, describe, expect, mock, test } from "bun:test";
|
|
14
15
|
|
|
15
16
|
import { makeMockLogger } from "./helpers/mock-logger.js";
|
|
16
17
|
|
|
@@ -19,6 +20,10 @@ mock.module("../util/logger.js", () => ({
|
|
|
19
20
|
}));
|
|
20
21
|
|
|
21
22
|
import { AgentLoop } from "../agent/loop.js";
|
|
23
|
+
import {
|
|
24
|
+
clearCachedOverrides,
|
|
25
|
+
setCachedOverrides,
|
|
26
|
+
} from "../config/feature-flag-cache.js";
|
|
22
27
|
import type {
|
|
23
28
|
Message,
|
|
24
29
|
Provider,
|
|
@@ -75,13 +80,20 @@ function makeRecordingProvider(responses: ProviderResponse[]): {
|
|
|
75
80
|
return { provider, configs: () => configs };
|
|
76
81
|
}
|
|
77
82
|
|
|
78
|
-
describe("AgentLoop.run — mutableLatestUserMessage
|
|
79
|
-
|
|
83
|
+
describe("AgentLoop.run — mutableLatestUserMessage from memory-v3-live", () => {
|
|
84
|
+
afterEach(() => {
|
|
85
|
+
clearCachedOverrides();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("sets mutableLatestUserMessage on every LLM call when memory-v3-live is on (multi-turn)", async () => {
|
|
89
|
+
// GIVEN memory-v3-live is enabled
|
|
90
|
+
setCachedOverrides({ "memory-v3-live": true }, { fromGateway: true });
|
|
91
|
+
|
|
92
|
+
// AND a provider that records the config of each LLM call across a tool round-trip
|
|
80
93
|
const { provider, configs } = makeRecordingProvider([
|
|
81
94
|
toolUseResponse("t1", "echo", { value: "first" }),
|
|
82
95
|
textResponse("done"),
|
|
83
96
|
]);
|
|
84
|
-
|
|
85
97
|
const dummyTools: ToolDefinition[] = [
|
|
86
98
|
{
|
|
87
99
|
name: "echo",
|
|
@@ -92,50 +104,53 @@ describe("AgentLoop.run — mutableLatestUserMessage plumbing", () => {
|
|
|
92
104
|
},
|
|
93
105
|
},
|
|
94
106
|
];
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
107
|
+
const loop = new AgentLoop({
|
|
108
|
+
provider: provider,
|
|
109
|
+
systemPrompt: "system",
|
|
110
|
+
conversationId: "test-conversation",
|
|
99
111
|
config: { maxTokens: 1024 },
|
|
100
112
|
tools: dummyTools,
|
|
101
|
-
toolExecutor:
|
|
113
|
+
toolExecutor: async () => ({ content: "ok", isError: false }),
|
|
102
114
|
});
|
|
103
115
|
|
|
104
|
-
|
|
116
|
+
// WHEN the loop runs
|
|
117
|
+
await loop.run({
|
|
118
|
+
messages: [userMessage],
|
|
119
|
+
onEvent: () => {},
|
|
120
|
+
trust: { sourceChannel: "vellum", trustClass: "unknown" },
|
|
105
121
|
callSite: "mainAgent",
|
|
106
|
-
mutableLatestUserMessage: true,
|
|
107
122
|
});
|
|
108
123
|
|
|
109
|
-
//
|
|
124
|
+
// THEN every send (initial + tool round-trip) carries the cache-anchor signal
|
|
110
125
|
expect(configs()).toHaveLength(2);
|
|
111
126
|
for (const cfg of configs()) {
|
|
112
127
|
expect(cfg?.mutableLatestUserMessage).toBe(true);
|
|
113
128
|
}
|
|
114
129
|
});
|
|
115
130
|
|
|
116
|
-
test("omits mutableLatestUserMessage when
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
config: { maxTokens: 1024 },
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
await loop.run([userMessage], () => {});
|
|
131
|
+
test("omits mutableLatestUserMessage when memory-v3-live is off (flag-off byte-identity)", async () => {
|
|
132
|
+
// GIVEN memory-v3-live is off (no override; registry default is false)
|
|
133
|
+
clearCachedOverrides();
|
|
123
134
|
|
|
124
|
-
|
|
125
|
-
expect("mutableLatestUserMessage" in (configs()[0] ?? {})).toBe(false);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
test("omits mutableLatestUserMessage when explicitly false (flag-off byte-identity)", async () => {
|
|
135
|
+
// AND a provider that records the config of each LLM call
|
|
129
136
|
const { provider, configs } = makeRecordingProvider([textResponse("hi")]);
|
|
130
|
-
const loop = new AgentLoop(
|
|
137
|
+
const loop = new AgentLoop({
|
|
138
|
+
provider: provider,
|
|
139
|
+
systemPrompt: "system",
|
|
140
|
+
conversationId: "test-conversation",
|
|
131
141
|
config: { maxTokens: 1024 },
|
|
132
142
|
});
|
|
133
143
|
|
|
134
|
-
|
|
144
|
+
// WHEN the loop runs
|
|
145
|
+
await loop.run({
|
|
146
|
+
messages: [userMessage],
|
|
147
|
+
onEvent: () => {},
|
|
148
|
+
trust: { sourceChannel: "vellum", trustClass: "unknown" },
|
|
135
149
|
callSite: "mainAgent",
|
|
136
|
-
mutableLatestUserMessage: false,
|
|
137
150
|
});
|
|
138
151
|
|
|
152
|
+
// THEN the field is omitted entirely, not carried as false/undefined
|
|
153
|
+
expect(configs()).toHaveLength(1);
|
|
139
154
|
expect("mutableLatestUserMessage" in (configs()[0] ?? {})).toBe(false);
|
|
140
155
|
});
|
|
141
156
|
});
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the `pre-model-call` and `assistant-message` plugin hooks: a plugin
|
|
3
|
+
* can edit the outbound request, transform the finalized assistant message
|
|
4
|
+
* (persisted + streamed), and defer the live stream so the transformed text is
|
|
5
|
+
* emitted once. All hooks here use neutral transforms (redaction / uppercasing).
|
|
6
|
+
*/
|
|
7
|
+
import { beforeEach, describe, expect, test } from "bun:test";
|
|
8
|
+
|
|
9
|
+
import type { AgentEvent } from "../agent/loop.js";
|
|
10
|
+
import { AgentLoop } from "../agent/loop.js";
|
|
11
|
+
import type {
|
|
12
|
+
AssistantMessageContext,
|
|
13
|
+
PreModelCallContext,
|
|
14
|
+
} from "../plugin-api/types.js";
|
|
15
|
+
import { resetPluginRegistryAndRegisterDefaults } from "../plugins/defaults/index.js";
|
|
16
|
+
import { registerPlugin } from "../plugins/registry.js";
|
|
17
|
+
import type {
|
|
18
|
+
ContentBlock,
|
|
19
|
+
Message,
|
|
20
|
+
ProviderResponse,
|
|
21
|
+
} from "../providers/types.js";
|
|
22
|
+
import { createMockProvider, textResponse } from "./helpers/mock-provider.js";
|
|
23
|
+
|
|
24
|
+
const userMessage: Message = {
|
|
25
|
+
role: "user",
|
|
26
|
+
content: [{ type: "text", text: "Hello" }],
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function collect(events: AgentEvent[]): (event: AgentEvent) => void {
|
|
30
|
+
return (event) => events.push(event);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function streamedText(events: AgentEvent[]): string {
|
|
34
|
+
return events
|
|
35
|
+
.filter(
|
|
36
|
+
(e): e is Extract<AgentEvent, { type: "text_delta" }> =>
|
|
37
|
+
e.type === "text_delta",
|
|
38
|
+
)
|
|
39
|
+
.map((e) => e.text)
|
|
40
|
+
.join("");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function textOf(content: ReadonlyArray<ContentBlock>): string {
|
|
44
|
+
let out = "";
|
|
45
|
+
for (const block of content) if (block.type === "text") out += block.text;
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function lastAssistant(history: Message[]): Message {
|
|
50
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
51
|
+
if (history[i].role === "assistant") return history[i];
|
|
52
|
+
}
|
|
53
|
+
throw new Error("no assistant message in history");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function registerOutputHookPlugin(hooks: {
|
|
57
|
+
preModelCall?: (ctx: PreModelCallContext) => void;
|
|
58
|
+
assistantMessage?: (ctx: AssistantMessageContext) => void;
|
|
59
|
+
}): void {
|
|
60
|
+
registerPlugin({
|
|
61
|
+
manifest: { name: "test-output-hooks", version: "0.0.0" },
|
|
62
|
+
hooks: {
|
|
63
|
+
...(hooks.preModelCall
|
|
64
|
+
? {
|
|
65
|
+
"pre-model-call": async (ctx: PreModelCallContext) => {
|
|
66
|
+
hooks.preModelCall!(ctx);
|
|
67
|
+
},
|
|
68
|
+
}
|
|
69
|
+
: {}),
|
|
70
|
+
...(hooks.assistantMessage
|
|
71
|
+
? {
|
|
72
|
+
"assistant-message": async (ctx: AssistantMessageContext) => {
|
|
73
|
+
hooks.assistantMessage!(ctx);
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
: {}),
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
describe("agent loop output hooks", () => {
|
|
82
|
+
beforeEach(() => {
|
|
83
|
+
resetPluginRegistryAndRegisterDefaults();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("assistant-message transforms the persisted message content", async () => {
|
|
87
|
+
registerOutputHookPlugin({
|
|
88
|
+
assistantMessage: (ctx) => {
|
|
89
|
+
ctx.content = ctx.content.map((b) =>
|
|
90
|
+
b.type === "text"
|
|
91
|
+
? { type: "text", text: b.text.replace("secret", "[redacted]") }
|
|
92
|
+
: b,
|
|
93
|
+
);
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
const { provider } = createMockProvider([textResponse("my secret value")]);
|
|
97
|
+
const loop = new AgentLoop({
|
|
98
|
+
provider: provider,
|
|
99
|
+
systemPrompt: "system",
|
|
100
|
+
conversationId: "test-conversation",
|
|
101
|
+
});
|
|
102
|
+
const events: AgentEvent[] = [];
|
|
103
|
+
const { history } = await loop.run({
|
|
104
|
+
messages: [userMessage],
|
|
105
|
+
onEvent: collect(events),
|
|
106
|
+
trust: { sourceChannel: "vellum", trustClass: "unknown" },
|
|
107
|
+
});
|
|
108
|
+
expect(textOf(lastAssistant(history).content)).toBe("my [redacted] value");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("deferred output: the real stream is suppressed and the transformed text is emitted once", async () => {
|
|
112
|
+
registerOutputHookPlugin({
|
|
113
|
+
preModelCall: (ctx) => {
|
|
114
|
+
ctx.deferAssistantOutput = true;
|
|
115
|
+
},
|
|
116
|
+
assistantMessage: (ctx) => {
|
|
117
|
+
ctx.content = [{ type: "text", text: "[filtered]" }];
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
const { provider } = createMockProvider([textResponse("my secret value")]);
|
|
121
|
+
const loop = new AgentLoop({
|
|
122
|
+
provider: provider,
|
|
123
|
+
systemPrompt: "system",
|
|
124
|
+
conversationId: "test-conversation",
|
|
125
|
+
});
|
|
126
|
+
const events: AgentEvent[] = [];
|
|
127
|
+
const { history } = await loop.run({
|
|
128
|
+
messages: [userMessage],
|
|
129
|
+
onEvent: collect(events),
|
|
130
|
+
trust: { sourceChannel: "vellum", trustClass: "unknown" },
|
|
131
|
+
});
|
|
132
|
+
// Her real words never streamed; only the transformed text did, once.
|
|
133
|
+
expect(streamedText(events)).not.toContain("secret");
|
|
134
|
+
expect(streamedText(events)).toBe("[filtered]");
|
|
135
|
+
expect(textOf(lastAssistant(history).content)).toBe("[filtered]");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("without defer, the real text still streams while storage is transformed", async () => {
|
|
139
|
+
registerOutputHookPlugin({
|
|
140
|
+
assistantMessage: (ctx) => {
|
|
141
|
+
ctx.content = [{ type: "text", text: "[stored]" }];
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
const { provider } = createMockProvider([textResponse("live text")]);
|
|
145
|
+
const loop = new AgentLoop({
|
|
146
|
+
provider: provider,
|
|
147
|
+
systemPrompt: "system",
|
|
148
|
+
conversationId: "test-conversation",
|
|
149
|
+
});
|
|
150
|
+
const events: AgentEvent[] = [];
|
|
151
|
+
const { history } = await loop.run({
|
|
152
|
+
messages: [userMessage],
|
|
153
|
+
onEvent: collect(events),
|
|
154
|
+
trust: { sourceChannel: "vellum", trustClass: "unknown" },
|
|
155
|
+
});
|
|
156
|
+
expect(streamedText(events)).toBe("live text"); // streamed live, untransformed
|
|
157
|
+
expect(textOf(lastAssistant(history).content)).toBe("[stored]"); // storage transformed
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("transforms text but leaves tool_use blocks intact", async () => {
|
|
161
|
+
const toolAndText: ProviderResponse = {
|
|
162
|
+
content: [
|
|
163
|
+
{ type: "text", text: "calling" },
|
|
164
|
+
{ type: "tool_use", id: "t1", name: "noop", input: {} },
|
|
165
|
+
],
|
|
166
|
+
model: "mock-model",
|
|
167
|
+
usage: { inputTokens: 1, outputTokens: 1 },
|
|
168
|
+
stopReason: "tool_use",
|
|
169
|
+
};
|
|
170
|
+
registerOutputHookPlugin({
|
|
171
|
+
assistantMessage: (ctx) => {
|
|
172
|
+
ctx.content = ctx.content.map((b) =>
|
|
173
|
+
b.type === "text" ? { type: "text", text: b.text.toUpperCase() } : b,
|
|
174
|
+
);
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
const { provider } = createMockProvider([
|
|
178
|
+
toolAndText,
|
|
179
|
+
textResponse("done"),
|
|
180
|
+
]);
|
|
181
|
+
const loop = new AgentLoop({
|
|
182
|
+
provider: provider,
|
|
183
|
+
systemPrompt: "system",
|
|
184
|
+
conversationId: "test-conversation",
|
|
185
|
+
tools: [
|
|
186
|
+
{
|
|
187
|
+
name: "noop",
|
|
188
|
+
description: "",
|
|
189
|
+
input_schema: { type: "object", properties: {} },
|
|
190
|
+
},
|
|
191
|
+
],
|
|
192
|
+
toolExecutor: async () => ({ content: "ok", isError: false }),
|
|
193
|
+
});
|
|
194
|
+
const { history } = await loop.run({
|
|
195
|
+
messages: [userMessage],
|
|
196
|
+
onEvent: collect([]),
|
|
197
|
+
trust: { sourceChannel: "vellum", trustClass: "unknown" },
|
|
198
|
+
});
|
|
199
|
+
const toolTurn = history.find(
|
|
200
|
+
(m) =>
|
|
201
|
+
m.role === "assistant" && m.content.some((b) => b.type === "tool_use"),
|
|
202
|
+
)!;
|
|
203
|
+
expect(toolTurn.content.find((b) => b.type === "text")).toEqual({
|
|
204
|
+
type: "text",
|
|
205
|
+
text: "CALLING",
|
|
206
|
+
});
|
|
207
|
+
expect(toolTurn.content.find((b) => b.type === "tool_use")).toMatchObject({
|
|
208
|
+
type: "tool_use",
|
|
209
|
+
id: "t1",
|
|
210
|
+
name: "noop",
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("pre-model-call can edit the outbound system prompt", async () => {
|
|
215
|
+
registerOutputHookPlugin({
|
|
216
|
+
preModelCall: (ctx) => {
|
|
217
|
+
ctx.systemPrompt = `${ctx.systemPrompt ?? ""} [EDITED]`;
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
const { provider, calls } = createMockProvider([textResponse("hi")]);
|
|
221
|
+
const loop = new AgentLoop({
|
|
222
|
+
provider: provider,
|
|
223
|
+
systemPrompt: "base prompt",
|
|
224
|
+
conversationId: "test-conversation",
|
|
225
|
+
});
|
|
226
|
+
await loop.run({
|
|
227
|
+
messages: [userMessage],
|
|
228
|
+
onEvent: collect([]),
|
|
229
|
+
trust: { sourceChannel: "vellum", trustClass: "unknown" },
|
|
230
|
+
});
|
|
231
|
+
expect(calls[0].systemPrompt).toContain("[EDITED]");
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("fail-open: a hook that mutates in place AND then throws cannot corrupt the persisted content", async () => {
|
|
235
|
+
// The hook mutates the array it receives before throwing. If the loop
|
|
236
|
+
// handed the hook the real `assistantMessage.content` array, the
|
|
237
|
+
// mid-mutation would survive into history. The loop must clone the
|
|
238
|
+
// content before invoking the hook.
|
|
239
|
+
registerOutputHookPlugin({
|
|
240
|
+
assistantMessage: (ctx) => {
|
|
241
|
+
ctx.content.push({ type: "text", text: "[INJECTED]" });
|
|
242
|
+
throw new Error("boom");
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
const { provider } = createMockProvider([textResponse("untouched")]);
|
|
246
|
+
const loop = new AgentLoop({
|
|
247
|
+
provider: provider,
|
|
248
|
+
systemPrompt: "system",
|
|
249
|
+
conversationId: "test-conversation",
|
|
250
|
+
});
|
|
251
|
+
const { history } = await loop.run({
|
|
252
|
+
messages: [userMessage],
|
|
253
|
+
onEvent: collect([]),
|
|
254
|
+
trust: { sourceChannel: "vellum", trustClass: "unknown" },
|
|
255
|
+
});
|
|
256
|
+
const finalContent = lastAssistant(history).content;
|
|
257
|
+
expect(textOf(finalContent)).toBe("untouched");
|
|
258
|
+
expect(
|
|
259
|
+
finalContent.some((b) => b.type === "text" && b.text === "[INJECTED]"),
|
|
260
|
+
).toBe(false);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test("max_tokens turn: the hook fires and the deferred final text is emitted", async () => {
|
|
264
|
+
// Without this, an output-filter plugin misses the truncated reply; with
|
|
265
|
+
// defer set, the live stream was suppressed and the client would see
|
|
266
|
+
// nothing at all.
|
|
267
|
+
const seen: { calls: number } = { calls: 0 };
|
|
268
|
+
registerOutputHookPlugin({
|
|
269
|
+
preModelCall: (ctx) => {
|
|
270
|
+
ctx.deferAssistantOutput = true;
|
|
271
|
+
},
|
|
272
|
+
assistantMessage: (ctx) => {
|
|
273
|
+
seen.calls += 1;
|
|
274
|
+
ctx.content = ctx.content.map((b) =>
|
|
275
|
+
b.type === "text" ? { type: "text", text: b.text.toUpperCase() } : b,
|
|
276
|
+
);
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
const truncated: ProviderResponse = {
|
|
280
|
+
content: [{ type: "text", text: "partial answer" }],
|
|
281
|
+
model: "mock-model",
|
|
282
|
+
usage: { inputTokens: 1, outputTokens: 1 },
|
|
283
|
+
stopReason: "max_tokens",
|
|
284
|
+
};
|
|
285
|
+
const { provider } = createMockProvider([truncated]);
|
|
286
|
+
const loop = new AgentLoop({
|
|
287
|
+
provider: provider,
|
|
288
|
+
systemPrompt: "system",
|
|
289
|
+
conversationId: "test-conversation",
|
|
290
|
+
});
|
|
291
|
+
const events: AgentEvent[] = [];
|
|
292
|
+
const { history } = await loop.run({
|
|
293
|
+
messages: [userMessage],
|
|
294
|
+
onEvent: collect(events),
|
|
295
|
+
trust: { sourceChannel: "vellum", trustClass: "unknown" },
|
|
296
|
+
});
|
|
297
|
+
expect(seen.calls).toBe(1);
|
|
298
|
+
expect(textOf(lastAssistant(history).content)).toBe("PARTIAL ANSWER");
|
|
299
|
+
// Real stream suppressed; transformed final text emitted once.
|
|
300
|
+
expect(streamedText(events)).toBe("PARTIAL ANSWER");
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test("deferred final text passes through sensitive-output substitution", async () => {
|
|
304
|
+
// A tool returns a sensitive binding; the next assistant turn uses the
|
|
305
|
+
// placeholder (the persisted message must keep it — model never sees real
|
|
306
|
+
// values on reload), but the deferred final stream must show the real
|
|
307
|
+
// value, just like the normal live stream would.
|
|
308
|
+
registerOutputHookPlugin({
|
|
309
|
+
preModelCall: (ctx) => {
|
|
310
|
+
ctx.deferAssistantOutput = true;
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
const placeholder = "VELLUM_ASSISTANT_INVITE_CODE_TESTXXXX";
|
|
314
|
+
const real = "real-invite-code-xyz";
|
|
315
|
+
const toolThenText = [
|
|
316
|
+
{
|
|
317
|
+
content: [{ type: "tool_use", id: "t1", name: "issue", input: {} }],
|
|
318
|
+
model: "mock-model",
|
|
319
|
+
usage: { inputTokens: 1, outputTokens: 1 },
|
|
320
|
+
stopReason: "tool_use",
|
|
321
|
+
} as ProviderResponse,
|
|
322
|
+
textResponse(`Your code is ${placeholder}.`),
|
|
323
|
+
];
|
|
324
|
+
const { provider } = createMockProvider(toolThenText);
|
|
325
|
+
const loop = new AgentLoop({
|
|
326
|
+
provider: provider,
|
|
327
|
+
systemPrompt: "system",
|
|
328
|
+
conversationId: "test-conversation",
|
|
329
|
+
tools: [
|
|
330
|
+
{
|
|
331
|
+
name: "issue",
|
|
332
|
+
description: "",
|
|
333
|
+
input_schema: { type: "object", properties: {} },
|
|
334
|
+
},
|
|
335
|
+
],
|
|
336
|
+
toolExecutor: async () => ({
|
|
337
|
+
content: `code=${placeholder}`,
|
|
338
|
+
isError: false,
|
|
339
|
+
sensitiveBindings: [{ kind: "invite_code", placeholder, value: real }],
|
|
340
|
+
}),
|
|
341
|
+
});
|
|
342
|
+
const events: AgentEvent[] = [];
|
|
343
|
+
const { history } = await loop.run({
|
|
344
|
+
messages: [userMessage],
|
|
345
|
+
onEvent: collect(events),
|
|
346
|
+
trust: { sourceChannel: "vellum", trustClass: "unknown" },
|
|
347
|
+
});
|
|
348
|
+
// Persisted message keeps the placeholder — model must never see real
|
|
349
|
+
// values on reload.
|
|
350
|
+
expect(textOf(lastAssistant(history).content)).toContain(placeholder);
|
|
351
|
+
expect(textOf(lastAssistant(history).content)).not.toContain(real);
|
|
352
|
+
// Streamed final text shows the substituted real value, matching the
|
|
353
|
+
// behavior of the normal live stream.
|
|
354
|
+
expect(streamedText(events)).toContain(real);
|
|
355
|
+
expect(streamedText(events)).not.toContain(placeholder);
|
|
356
|
+
});
|
|
357
|
+
});
|
|
@@ -110,13 +110,19 @@ describe("AgentLoop.run — overrideProfile plumbing", () => {
|
|
|
110
110
|
_input: Record<string, unknown>,
|
|
111
111
|
) => ({ content: "ok", isError: false });
|
|
112
112
|
|
|
113
|
-
const loop = new AgentLoop(
|
|
113
|
+
const loop = new AgentLoop({
|
|
114
|
+
provider: provider,
|
|
115
|
+
systemPrompt: "system",
|
|
116
|
+
conversationId: "test-conversation",
|
|
114
117
|
config: { maxTokens: 1024 },
|
|
115
118
|
tools: dummyTools,
|
|
116
119
|
toolExecutor: toolExecutor,
|
|
117
120
|
});
|
|
118
121
|
|
|
119
|
-
await loop.run(
|
|
122
|
+
await loop.run({
|
|
123
|
+
messages: [userMessage],
|
|
124
|
+
onEvent: () => {},
|
|
125
|
+
trust: { sourceChannel: "vellum", trustClass: "unknown" },
|
|
120
126
|
callSite: "mainAgent",
|
|
121
127
|
overrideProfile: "fast",
|
|
122
128
|
});
|
|
@@ -130,11 +136,18 @@ describe("AgentLoop.run — overrideProfile plumbing", () => {
|
|
|
130
136
|
|
|
131
137
|
test("omits overrideProfile from providerConfig when unset (default behavior unchanged)", async () => {
|
|
132
138
|
const { provider, configs } = makeRecordingProvider([textResponse("hi")]);
|
|
133
|
-
const loop = new AgentLoop(
|
|
139
|
+
const loop = new AgentLoop({
|
|
140
|
+
provider: provider,
|
|
141
|
+
systemPrompt: "system",
|
|
142
|
+
conversationId: "test-conversation",
|
|
134
143
|
config: { maxTokens: 1024 },
|
|
135
144
|
});
|
|
136
145
|
|
|
137
|
-
await loop.run(
|
|
146
|
+
await loop.run({
|
|
147
|
+
messages: [userMessage],
|
|
148
|
+
onEvent: () => {},
|
|
149
|
+
trust: { sourceChannel: "vellum", trustClass: "unknown" },
|
|
150
|
+
});
|
|
138
151
|
|
|
139
152
|
// Single send, no overrideProfile field at all.
|
|
140
153
|
expect(configs()).toHaveLength(1);
|
|
@@ -148,11 +161,17 @@ describe("AgentLoop.run — overrideProfile plumbing", () => {
|
|
|
148
161
|
// receives so a non-existent profile silently falls back at the
|
|
149
162
|
// provider layer (covered by provider-send-message-override-profile.test.ts).
|
|
150
163
|
const { provider, configs } = makeRecordingProvider([textResponse("hi")]);
|
|
151
|
-
const loop = new AgentLoop(
|
|
164
|
+
const loop = new AgentLoop({
|
|
165
|
+
provider: provider,
|
|
166
|
+
systemPrompt: "system",
|
|
167
|
+
conversationId: "test-conversation",
|
|
152
168
|
config: { maxTokens: 1024 },
|
|
153
169
|
});
|
|
154
170
|
|
|
155
|
-
await loop.run(
|
|
171
|
+
await loop.run({
|
|
172
|
+
messages: [userMessage],
|
|
173
|
+
onEvent: () => {},
|
|
174
|
+
trust: { sourceChannel: "vellum", trustClass: "unknown" },
|
|
156
175
|
callSite: "mainAgent",
|
|
157
176
|
overrideProfile: "does-not-exist",
|
|
158
177
|
});
|
|
@@ -83,14 +83,19 @@ describe("AgentLoop provider_error event emission", () => {
|
|
|
83
83
|
const { provider, calls } = makeThrowingProvider("anthropic", () => thrown);
|
|
84
84
|
|
|
85
85
|
const events: AgentEvent[] = [];
|
|
86
|
-
const loop = new AgentLoop(
|
|
86
|
+
const loop = new AgentLoop({
|
|
87
|
+
provider: provider,
|
|
88
|
+
systemPrompt: "you are a helpful assistant",
|
|
89
|
+
conversationId: "test-conversation",
|
|
90
|
+
});
|
|
87
91
|
|
|
88
|
-
await loop.run(
|
|
89
|
-
[{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
|
90
|
-
(e) => {
|
|
92
|
+
await loop.run({
|
|
93
|
+
messages: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
|
94
|
+
onEvent: (e) => {
|
|
91
95
|
events.push(e);
|
|
92
96
|
},
|
|
93
|
-
|
|
97
|
+
trust: { sourceChannel: "vellum", trustClass: "unknown" },
|
|
98
|
+
});
|
|
94
99
|
|
|
95
100
|
expect(calls).toHaveLength(1);
|
|
96
101
|
|
|
@@ -122,14 +127,19 @@ describe("AgentLoop provider_error event emission", () => {
|
|
|
122
127
|
const { provider } = makeThrowingProvider("gemini", () => thrown);
|
|
123
128
|
|
|
124
129
|
const events: AgentEvent[] = [];
|
|
125
|
-
const loop = new AgentLoop(
|
|
130
|
+
const loop = new AgentLoop({
|
|
131
|
+
provider: provider,
|
|
132
|
+
systemPrompt: "system",
|
|
133
|
+
conversationId: "test-conversation",
|
|
134
|
+
});
|
|
126
135
|
|
|
127
|
-
await loop.run(
|
|
128
|
-
[{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
|
129
|
-
(e) => {
|
|
136
|
+
await loop.run({
|
|
137
|
+
messages: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
|
138
|
+
onEvent: (e) => {
|
|
130
139
|
events.push(e);
|
|
131
140
|
},
|
|
132
|
-
|
|
141
|
+
trust: { sourceChannel: "vellum", trustClass: "unknown" },
|
|
142
|
+
});
|
|
133
143
|
|
|
134
144
|
const providerErrorIdx = events.findIndex(
|
|
135
145
|
(e) => e.type === "provider_error",
|
|
@@ -148,14 +158,19 @@ describe("AgentLoop provider_error event emission", () => {
|
|
|
148
158
|
const { provider } = makeThrowingProvider("openai", () => thrown);
|
|
149
159
|
|
|
150
160
|
const events: AgentEvent[] = [];
|
|
151
|
-
const loop = new AgentLoop(
|
|
161
|
+
const loop = new AgentLoop({
|
|
162
|
+
provider: provider,
|
|
163
|
+
systemPrompt: "system",
|
|
164
|
+
conversationId: "test-conversation",
|
|
165
|
+
});
|
|
152
166
|
|
|
153
|
-
await loop.run(
|
|
154
|
-
[{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
|
155
|
-
(e) => {
|
|
167
|
+
await loop.run({
|
|
168
|
+
messages: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
|
169
|
+
onEvent: (e) => {
|
|
156
170
|
events.push(e);
|
|
157
171
|
},
|
|
158
|
-
|
|
172
|
+
trust: { sourceChannel: "vellum", trustClass: "unknown" },
|
|
173
|
+
});
|
|
159
174
|
|
|
160
175
|
const providerErrorEvent = events.find((e) => e.type === "provider_error");
|
|
161
176
|
expect(providerErrorEvent).toBeDefined();
|
|
@@ -179,15 +194,20 @@ describe("AgentLoop provider_error event emission", () => {
|
|
|
179
194
|
});
|
|
180
195
|
|
|
181
196
|
const events: AgentEvent[] = [];
|
|
182
|
-
const loop = new AgentLoop(
|
|
197
|
+
const loop = new AgentLoop({
|
|
198
|
+
provider: provider,
|
|
199
|
+
systemPrompt: "system",
|
|
200
|
+
conversationId: "test-conversation",
|
|
201
|
+
});
|
|
183
202
|
|
|
184
|
-
await loop.run(
|
|
185
|
-
[{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
|
186
|
-
(e) => {
|
|
203
|
+
await loop.run({
|
|
204
|
+
messages: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
|
205
|
+
onEvent: (e) => {
|
|
187
206
|
events.push(e);
|
|
188
207
|
},
|
|
189
|
-
{
|
|
190
|
-
|
|
208
|
+
trust: { sourceChannel: "vellum", trustClass: "unknown" },
|
|
209
|
+
signal: controller.signal,
|
|
210
|
+
});
|
|
191
211
|
|
|
192
212
|
const providerErrorEvent = events.find((e) => e.type === "provider_error");
|
|
193
213
|
// Cancellation should never produce a recording row — there's no
|