@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
|
@@ -2,52 +2,165 @@
|
|
|
2
2
|
* Tests for `wakeAgentForOpportunity()` — the generic internal agent-wake
|
|
3
3
|
* mechanism.
|
|
4
4
|
*
|
|
5
|
-
* Exercise strategy: the wake helper takes a `resolveTarget` dependency
|
|
6
|
-
*
|
|
7
|
-
* `
|
|
8
|
-
*
|
|
9
|
-
* `
|
|
5
|
+
* Exercise strategy: the wake helper takes a `resolveTarget` dependency that
|
|
6
|
+
* yields a live `Conversation`. These tests build a lightweight structural
|
|
7
|
+
* double typed as `Conversation` (`makeWakeConversation`) that stubs only the
|
|
8
|
+
* handful of members the wake touches — `getMessages`, `messages.push`,
|
|
9
|
+
* `isProcessing`/`setProcessing`, `setTrustContext`, `setSubagentAllowedTools`,
|
|
10
|
+
* `drainQueue`, and a scripted `agentLoop.run()`.
|
|
10
11
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* `
|
|
12
|
+
* The wake's side effects flow through the daemon boundary, so the
|
|
13
|
+
* instrumentation is captured at that boundary: event emission and the
|
|
14
|
+
* ui_surface card via the `broadcastMessage` module mock, and tail
|
|
15
|
+
* persistence via the `addMessage` module mock. Each double registers itself
|
|
16
|
+
* in `wakeConvRegistry` keyed by `conversationId` so those module mocks can
|
|
17
|
+
* route captured calls back to the originating conversation's probe arrays.
|
|
16
18
|
*/
|
|
17
19
|
|
|
18
20
|
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
19
21
|
|
|
20
22
|
import type { DiskPressureStatus } from "../../daemon/disk-pressure-guard.js";
|
|
21
23
|
|
|
24
|
+
// ── Per-conversation capture registry ────────────────────────────────
|
|
25
|
+
//
|
|
26
|
+
// Module mocks for the daemon boundary (`broadcastMessage`, `addMessage`)
|
|
27
|
+
// are process-global, so each test double registers itself here keyed by
|
|
28
|
+
// `conversationId`. The mocks look up the originating conversation's probe
|
|
29
|
+
// and record the captured frame / persisted message.
|
|
30
|
+
|
|
31
|
+
/** Captured client wire frame (output of the event→wire translator). */
|
|
32
|
+
interface CapturedFrame {
|
|
33
|
+
type: string;
|
|
34
|
+
text?: string;
|
|
35
|
+
[key: string]: unknown;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Instrumentation attached to a `makeWakeConversation` double. */
|
|
39
|
+
interface WakeConversationProbe {
|
|
40
|
+
/** Wire frames emitted to clients (excludes the ui_surface card). */
|
|
41
|
+
emittedEvents: CapturedFrame[];
|
|
42
|
+
/** ui_surface cards broadcast when a wake produces output. */
|
|
43
|
+
surfaceBroadcasts: Array<{ surfaceId?: string; source?: string }>;
|
|
44
|
+
/** Messages appended to live history via `conversation.messages.push`. */
|
|
45
|
+
pushedMessages: Message[];
|
|
46
|
+
/** Recorded `agentLoop.run` invocations, in call order. */
|
|
47
|
+
runCalls: Array<{
|
|
48
|
+
input: Message[];
|
|
49
|
+
requestId?: string;
|
|
50
|
+
trust?: unknown;
|
|
51
|
+
allowedTools?: string[];
|
|
52
|
+
order: number;
|
|
53
|
+
}>;
|
|
54
|
+
/** Every `setProcessing` value, in call order. */
|
|
55
|
+
processingToggles: boolean[];
|
|
56
|
+
/** Tail messages persisted via `addMessage`, in call order. */
|
|
57
|
+
persistedTailCalls: Message[];
|
|
58
|
+
/** Number of times `drainQueue` was invoked. */
|
|
59
|
+
drainQueueCalls: number;
|
|
60
|
+
/**
|
|
61
|
+
* Cross-hook call sequence tag. Each push/persist/drain (and the
|
|
62
|
+
* processing toggles that bracket them) appends an entry so tests can
|
|
63
|
+
* assert end-to-end ordering, not just per-hook counts.
|
|
64
|
+
*/
|
|
65
|
+
callSequence: string[];
|
|
66
|
+
/**
|
|
67
|
+
* Snapshot of the processing flag at the moment `drainQueue` was
|
|
68
|
+
* invoked. Lets tests prove drain ran AFTER setProcessing(false),
|
|
69
|
+
* rather than just inferring it from the order of recorded toggles.
|
|
70
|
+
*/
|
|
71
|
+
processingDuringDrain: boolean[];
|
|
72
|
+
/**
|
|
73
|
+
* Tool allowlist snapshots captured whenever the wake applies/restores a
|
|
74
|
+
* scope. `undefined` means unrestricted.
|
|
75
|
+
*/
|
|
76
|
+
allowedToolSnapshots: Array<string[] | undefined>;
|
|
77
|
+
/** `setTrustContext` calls, with the value and a monotonic order tag. */
|
|
78
|
+
setTrustContextCalls: Array<{ ctx: unknown; order: number }>;
|
|
79
|
+
/** Number of persisted tail messages at the moment each frame emitted. */
|
|
80
|
+
persistedAtEachEmit: number[];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const wakeConvRegistry = new Map<string, WakeConversationProbe>();
|
|
84
|
+
|
|
22
85
|
// Stub the DB-backed override-profile read so unit tests don't need a
|
|
23
86
|
// real SQLite database. The wake helper calls this on every invocation
|
|
24
|
-
// to honor the conversation's pinned inference profile.
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
87
|
+
// to honor the conversation's pinned inference profile. `getConversation`
|
|
88
|
+
// is consumed by `defaultResolveTarget` (existence/archived check) and by
|
|
89
|
+
// `persistWakeTailMessage` (createdAt for the disk-view sync). `addMessage`
|
|
90
|
+
// is the persistence boundary the wake's tail persistence flows through —
|
|
91
|
+
// it records into the originating conversation's probe.
|
|
29
92
|
mock.module("../../memory/conversation-crud.js", () => ({
|
|
30
93
|
getConversationOverrideProfile: () => undefined,
|
|
31
|
-
getConversation: () => ({
|
|
32
|
-
|
|
94
|
+
getConversation: () => ({
|
|
95
|
+
archivedAt: null,
|
|
96
|
+
createdAt: "2026-01-01T00:00:00.000Z",
|
|
97
|
+
}),
|
|
98
|
+
provenanceFromTrustContext: () => ({}),
|
|
99
|
+
addMessage: async (
|
|
100
|
+
conversationId: string,
|
|
101
|
+
role: string,
|
|
102
|
+
contentJson: string,
|
|
103
|
+
) => {
|
|
104
|
+
const probe = wakeConvRegistry.get(conversationId);
|
|
105
|
+
if (probe) {
|
|
106
|
+
let content: unknown = contentJson;
|
|
107
|
+
try {
|
|
108
|
+
content = JSON.parse(contentJson);
|
|
109
|
+
} catch {
|
|
110
|
+
// Leave the raw string if it is not valid JSON.
|
|
111
|
+
}
|
|
112
|
+
probe.persistedTailCalls.push({ role, content } as Message);
|
|
113
|
+
probe.callSequence.push("persist");
|
|
114
|
+
}
|
|
115
|
+
return { id: `msg-${probe ? probe.persistedTailCalls.length : 0}` };
|
|
116
|
+
},
|
|
117
|
+
}));
|
|
118
|
+
|
|
119
|
+
// The wake's tail persistence syncs each row to the disk view. Stub it so
|
|
120
|
+
// unit tests don't touch the filesystem.
|
|
121
|
+
mock.module("../../memory/conversation-disk-view.js", () => ({
|
|
122
|
+
syncMessageToDisk: () => {},
|
|
123
|
+
}));
|
|
124
|
+
|
|
125
|
+
// The daemon event boundary. `emitWakeAgentEvent` translates each agent
|
|
126
|
+
// event to a wire frame and broadcasts it; `broadcastWakeSurface` broadcasts
|
|
127
|
+
// the ui_surface card. Route both back to the originating conversation's
|
|
128
|
+
// probe by the `conversationId` stamped on the frame.
|
|
129
|
+
mock.module("../../runtime/assistant-event-hub.js", () => ({
|
|
130
|
+
broadcastMessage: (frame: CapturedFrame & { conversationId?: string }) => {
|
|
131
|
+
const probe = frame.conversationId
|
|
132
|
+
? wakeConvRegistry.get(frame.conversationId)
|
|
133
|
+
: undefined;
|
|
134
|
+
if (!probe) return;
|
|
135
|
+
if (frame.type === "ui_surface_show") {
|
|
136
|
+
const source = (
|
|
137
|
+
frame.data as
|
|
138
|
+
| { metadata?: Array<{ label?: string; value?: string }> }
|
|
139
|
+
| undefined
|
|
140
|
+
)?.metadata?.find((m) => m.label === "Source")?.value;
|
|
141
|
+
probe.surfaceBroadcasts.push({
|
|
142
|
+
surfaceId: frame.surfaceId as string | undefined,
|
|
143
|
+
source,
|
|
144
|
+
});
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
probe.emittedEvents.push(frame);
|
|
148
|
+
probe.persistedAtEachEmit.push(probe.persistedTailCalls.length);
|
|
149
|
+
},
|
|
33
150
|
}));
|
|
34
151
|
|
|
35
152
|
const mockGetOrCreateConversationCalls: Array<{
|
|
36
153
|
conversationId: string;
|
|
37
154
|
options: unknown;
|
|
38
155
|
}> = [];
|
|
156
|
+
let mockResolverTarget: unknown = null;
|
|
39
157
|
mock.module("../../daemon/conversation-store.js", () => ({
|
|
40
158
|
getOrCreateConversation: (conversationId: string, options?: unknown) => {
|
|
41
159
|
mockGetOrCreateConversationCalls.push({ conversationId, options });
|
|
42
|
-
return Promise.resolve(
|
|
160
|
+
return Promise.resolve(mockResolverTarget);
|
|
43
161
|
},
|
|
44
162
|
}));
|
|
45
163
|
|
|
46
|
-
let mockResolverTarget: unknown = null;
|
|
47
|
-
mock.module("../../daemon/wake-target-adapter.js", () => ({
|
|
48
|
-
conversationToWakeTarget: () => mockResolverTarget,
|
|
49
|
-
}));
|
|
50
|
-
|
|
51
164
|
mock.module("../../config/loader.js", () => ({
|
|
52
165
|
getConfig: () => ({ llm: {} }),
|
|
53
166
|
loadConfig: () => ({ llm: {} }),
|
|
@@ -122,11 +235,11 @@ import type {
|
|
|
122
235
|
AgentLoopRunOptions,
|
|
123
236
|
AgentLoopRunResult,
|
|
124
237
|
} from "../../agent/loop.js";
|
|
238
|
+
import type { Conversation } from "../../daemon/conversation.js";
|
|
125
239
|
import type { Message } from "../../providers/types.js";
|
|
126
240
|
import {
|
|
127
241
|
__resetWakeChainForTests,
|
|
128
242
|
wakeAgentForOpportunity,
|
|
129
|
-
type WakeTarget,
|
|
130
243
|
} from "../agent-wake.js";
|
|
131
244
|
|
|
132
245
|
// ── Test helpers ─────────────────────────────────────────────────────
|
|
@@ -142,40 +255,16 @@ const runResult = (history: Message[]): AgentLoopRunResult => ({
|
|
|
142
255
|
newMessages: [],
|
|
143
256
|
});
|
|
144
257
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
pushedMessages: Message[];
|
|
148
|
-
runCalls: Array<{
|
|
149
|
-
input: Message[];
|
|
150
|
-
requestId?: string;
|
|
151
|
-
turnContext?: unknown;
|
|
152
|
-
allowedTools?: string[];
|
|
153
|
-
}>;
|
|
154
|
-
processingToggles: boolean[];
|
|
155
|
-
/** Tail messages handed to `persistTailMessage`, in call order. */
|
|
156
|
-
persistedTailCalls: Message[];
|
|
157
|
-
/** Number of times `drainQueue` was invoked. */
|
|
158
|
-
drainQueueCalls: number;
|
|
159
|
-
/**
|
|
160
|
-
* Cross-hook call sequence tag. Each push/persist/drain (and the
|
|
161
|
-
* processing toggles that bracket them) appends an entry so tests can
|
|
162
|
-
* assert end-to-end ordering, not just per-hook counts.
|
|
163
|
-
*/
|
|
164
|
-
callSequence: string[];
|
|
165
|
-
/**
|
|
166
|
-
* Snapshot of `processing` at the moment `drainQueue` was invoked.
|
|
167
|
-
* Lets tests prove drain ran AFTER markProcessing(false), rather than
|
|
168
|
-
* just inferring it from the order of recorded toggles.
|
|
169
|
-
*/
|
|
170
|
-
processingDuringDrain: boolean[];
|
|
171
|
-
/**
|
|
172
|
-
* Tool allowlist snapshots captured whenever the wake applies/restores a
|
|
173
|
-
* scope. `undefined` means unrestricted.
|
|
174
|
-
*/
|
|
175
|
-
allowedToolSnapshots: Array<string[] | undefined>;
|
|
176
|
-
}
|
|
258
|
+
/** A structural `Conversation` double plus its captured instrumentation. */
|
|
259
|
+
type WakeConversation = Conversation & WakeConversationProbe;
|
|
177
260
|
|
|
178
|
-
|
|
261
|
+
type ScriptedRun = (
|
|
262
|
+
input: Message[],
|
|
263
|
+
onEvent: (event: AgentEvent) => void | Promise<void>,
|
|
264
|
+
runOptions?: AgentLoopRunOptions,
|
|
265
|
+
) => Promise<AgentLoopRunResult>;
|
|
266
|
+
|
|
267
|
+
function makeWakeConversation(options: {
|
|
179
268
|
conversationId?: string;
|
|
180
269
|
baseline?: Message[];
|
|
181
270
|
scriptedAssistant?: Message | null;
|
|
@@ -186,131 +275,127 @@ function makeTarget(options: {
|
|
|
186
275
|
/** When true, omit `drainQueue` so we can verify the wake handles its absence. */
|
|
187
276
|
omitDrainQueue?: boolean;
|
|
188
277
|
initialAllowedTools?: Set<string>;
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
278
|
+
/** Replaces the default scripted `agentLoop.run` body entirely. */
|
|
279
|
+
runImpl?: ScriptedRun;
|
|
280
|
+
}): WakeConversation {
|
|
281
|
+
const conversationId = options.conversationId ?? "conv-test";
|
|
282
|
+
const probe: WakeConversationProbe = {
|
|
283
|
+
emittedEvents: [],
|
|
284
|
+
surfaceBroadcasts: [],
|
|
285
|
+
pushedMessages: [],
|
|
286
|
+
runCalls: [],
|
|
287
|
+
processingToggles: [],
|
|
288
|
+
persistedTailCalls: [],
|
|
289
|
+
drainQueueCalls: 0,
|
|
290
|
+
callSequence: [],
|
|
291
|
+
processingDuringDrain: [],
|
|
292
|
+
allowedToolSnapshots: [],
|
|
293
|
+
setTrustContextCalls: [],
|
|
294
|
+
persistedAtEachEmit: [],
|
|
295
|
+
};
|
|
296
|
+
wakeConvRegistry.set(conversationId, probe);
|
|
297
|
+
|
|
204
298
|
let processing = options.isProcessing ?? false;
|
|
205
|
-
let
|
|
299
|
+
let order = 0;
|
|
206
300
|
let activeAllowedTools = options.initialAllowedTools;
|
|
207
301
|
const snapshotAllowedTools = (): string[] | undefined =>
|
|
208
302
|
activeAllowedTools ? [...activeAllowedTools].sort() : undefined;
|
|
209
303
|
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
304
|
+
const messages: Message[] = [...(options.baseline ?? [])];
|
|
305
|
+
const nativePush = messages.push.bind(messages);
|
|
306
|
+
messages.push = (...items: Message[]): number => {
|
|
307
|
+
for (const item of items) {
|
|
308
|
+
probe.pushedMessages.push(item);
|
|
309
|
+
probe.callSequence.push("push");
|
|
310
|
+
}
|
|
311
|
+
return nativePush(...items);
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
const defaultRun: ScriptedRun = async (input, onEvent) => {
|
|
315
|
+
for (const ev of options.scriptedEvents ?? []) {
|
|
316
|
+
await onEvent(ev);
|
|
317
|
+
}
|
|
318
|
+
const next = [...input];
|
|
319
|
+
if (options.scriptedAssistant) {
|
|
320
|
+
next.push(options.scriptedAssistant);
|
|
321
|
+
await onEvent({
|
|
322
|
+
type: "message_complete",
|
|
323
|
+
message: options.scriptedAssistant,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
if (options.scriptedTail) {
|
|
327
|
+
for (const tailMsg of options.scriptedTail) {
|
|
328
|
+
next.push(tailMsg);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return runResult(next);
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
const runBody = options.runImpl ?? defaultRun;
|
|
335
|
+
|
|
336
|
+
const drainQueue = options.omitDrainQueue
|
|
337
|
+
? undefined
|
|
338
|
+
: async () => {
|
|
339
|
+
probe.drainQueueCalls += 1;
|
|
340
|
+
// Snapshot the live processing flag *inside* drain, not via the
|
|
341
|
+
// toggle log, so we directly observe the state visible to the
|
|
342
|
+
// dequeued message's enqueueMessage() gate.
|
|
343
|
+
probe.processingDuringDrain.push(processing);
|
|
344
|
+
probe.callSequence.push("drain");
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const conversation = {
|
|
348
|
+
conversationId,
|
|
349
|
+
...probe,
|
|
220
350
|
get drainQueueCalls() {
|
|
221
|
-
return drainQueueCalls;
|
|
351
|
+
return probe.drainQueueCalls;
|
|
352
|
+
},
|
|
353
|
+
get subagentAllowedTools() {
|
|
354
|
+
return activeAllowedTools;
|
|
355
|
+
},
|
|
356
|
+
setSubagentAllowedTools: (tools: Set<string> | undefined) => {
|
|
357
|
+
activeAllowedTools = tools;
|
|
358
|
+
probe.allowedToolSnapshots.push(snapshotAllowedTools());
|
|
359
|
+
probe.callSequence.push(
|
|
360
|
+
`tools:${snapshotAllowedTools()?.join(",") ?? "all"}`,
|
|
361
|
+
);
|
|
222
362
|
},
|
|
223
363
|
agentLoop: {
|
|
224
|
-
run: async (
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
runOptions?: AgentLoopRunOptions,
|
|
228
|
-
) => {
|
|
229
|
-
runCalls.push({
|
|
364
|
+
run: async (options: AgentLoopRunOptions) => {
|
|
365
|
+
const { messages: input, onEvent } = options;
|
|
366
|
+
probe.runCalls.push({
|
|
230
367
|
input: [...input],
|
|
231
|
-
requestId:
|
|
232
|
-
|
|
368
|
+
requestId: options.requestId,
|
|
369
|
+
trust: options.trust,
|
|
233
370
|
allowedTools: snapshotAllowedTools(),
|
|
371
|
+
order: order++,
|
|
234
372
|
});
|
|
235
|
-
|
|
236
|
-
for (const ev of options.scriptedEvents ?? []) {
|
|
237
|
-
await onEvent(ev);
|
|
238
|
-
}
|
|
239
|
-
// Final history = input + optional assistant message + optional tail.
|
|
240
|
-
const next = [...input];
|
|
241
|
-
if (options.scriptedAssistant) {
|
|
242
|
-
next.push(options.scriptedAssistant);
|
|
243
|
-
await onEvent({
|
|
244
|
-
type: "message_complete",
|
|
245
|
-
message: options.scriptedAssistant,
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
if (options.scriptedTail) {
|
|
249
|
-
for (const tailMsg of options.scriptedTail) {
|
|
250
|
-
next.push(tailMsg);
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
return runResult(next);
|
|
373
|
+
return runBody(input, onEvent, options);
|
|
254
374
|
},
|
|
255
375
|
},
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
pushedMessages.push(msg);
|
|
259
|
-
history.push(msg);
|
|
260
|
-
callSequence.push("push");
|
|
261
|
-
},
|
|
262
|
-
emitAgentEvent: (event) => {
|
|
263
|
-
emittedEvents.push(event);
|
|
264
|
-
},
|
|
376
|
+
messages,
|
|
377
|
+
getMessages: () => messages,
|
|
265
378
|
isProcessing: () => processing,
|
|
266
|
-
|
|
379
|
+
setProcessing: (on: boolean) => {
|
|
267
380
|
processing = on;
|
|
268
|
-
processingToggles.push(on);
|
|
269
|
-
callSequence.push(on ? "processing:true" : "processing:false");
|
|
381
|
+
probe.processingToggles.push(on);
|
|
382
|
+
probe.callSequence.push(on ? "processing:true" : "processing:false");
|
|
270
383
|
},
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
callSequence.push("persist");
|
|
274
|
-
},
|
|
275
|
-
setWakeAllowedTools: (tools: ReadonlySet<string>) => {
|
|
276
|
-
const previous = activeAllowedTools;
|
|
277
|
-
activeAllowedTools = new Set(tools);
|
|
278
|
-
allowedToolSnapshots.push(snapshotAllowedTools());
|
|
279
|
-
callSequence.push(`tools:${snapshotAllowedTools()?.join(",") ?? "all"}`);
|
|
280
|
-
return () => {
|
|
281
|
-
activeAllowedTools = previous;
|
|
282
|
-
allowedToolSnapshots.push(snapshotAllowedTools());
|
|
283
|
-
callSequence.push(
|
|
284
|
-
`tools:${snapshotAllowedTools()?.join(",") ?? "all"}`,
|
|
285
|
-
);
|
|
286
|
-
};
|
|
384
|
+
setTrustContext: (ctx: unknown) => {
|
|
385
|
+
probe.setTrustContextCalls.push({ ctx, order: order++ });
|
|
287
386
|
},
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
drainQueueCalls++;
|
|
293
|
-
// Snapshot the live processing flag *inside* drain, not via
|
|
294
|
-
// the toggle log, so we directly observe the state visible
|
|
295
|
-
// to the dequeued message's enqueueMessage() gate.
|
|
296
|
-
processingDuringDrain.push(processing);
|
|
297
|
-
callSequence.push("drain");
|
|
298
|
-
},
|
|
299
|
-
}),
|
|
387
|
+
getTurnChannelContext: () => null,
|
|
388
|
+
getTurnInterfaceContext: () => null,
|
|
389
|
+
trustContext: undefined,
|
|
390
|
+
...(drainQueue ? { drainQueue } : {}),
|
|
300
391
|
};
|
|
301
392
|
|
|
302
|
-
|
|
303
|
-
// simulate an external (non-wake) processing state.
|
|
304
|
-
(target as unknown as { setProcessing: (v: boolean) => void }).setProcessing =
|
|
305
|
-
(v: boolean) => {
|
|
306
|
-
processing = v;
|
|
307
|
-
};
|
|
308
|
-
|
|
309
|
-
return target;
|
|
393
|
+
return conversation as unknown as WakeConversation;
|
|
310
394
|
}
|
|
311
395
|
|
|
312
396
|
beforeEach(() => {
|
|
313
397
|
__resetWakeChainForTests();
|
|
398
|
+
wakeConvRegistry.clear();
|
|
314
399
|
recordRequestLogCalls.length = 0;
|
|
315
400
|
mockGetOrCreateConversationCalls.length = 0;
|
|
316
401
|
mockResolverTarget = null;
|
|
@@ -335,21 +420,21 @@ beforeEach(() => {
|
|
|
335
420
|
|
|
336
421
|
describe("wakeAgentForOpportunity", () => {
|
|
337
422
|
test("disabled disk pressure flag allows background wakes to pass through", async () => {
|
|
338
|
-
const
|
|
423
|
+
const conversation = makeWakeConversation({
|
|
339
424
|
scriptedAssistant: null,
|
|
340
425
|
});
|
|
341
426
|
|
|
342
427
|
const result = await wakeAgentForOpportunity(
|
|
343
428
|
{
|
|
344
|
-
conversationId:
|
|
429
|
+
conversationId: conversation.conversationId,
|
|
345
430
|
hint: "background completion",
|
|
346
431
|
source: "background-tool",
|
|
347
432
|
},
|
|
348
|
-
{ resolveTarget: async () =>
|
|
433
|
+
{ resolveTarget: async () => conversation },
|
|
349
434
|
);
|
|
350
435
|
|
|
351
436
|
expect(result).toEqual({ invoked: true, producedToolCalls: false });
|
|
352
|
-
expect(
|
|
437
|
+
expect(conversation.runCalls).toHaveLength(1);
|
|
353
438
|
});
|
|
354
439
|
|
|
355
440
|
test("blocks background wakes during disk pressure before marking processing", async () => {
|
|
@@ -368,7 +453,7 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
368
453
|
blockedCapabilities: ["agent-turns", "background-work", "remote-ingress"],
|
|
369
454
|
error: null,
|
|
370
455
|
};
|
|
371
|
-
const
|
|
456
|
+
const conversation = makeWakeConversation({
|
|
372
457
|
isProcessing: true,
|
|
373
458
|
scriptedAssistant: {
|
|
374
459
|
role: "assistant",
|
|
@@ -378,12 +463,12 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
378
463
|
|
|
379
464
|
const result = await wakeAgentForOpportunity(
|
|
380
465
|
{
|
|
381
|
-
conversationId:
|
|
466
|
+
conversationId: conversation.conversationId,
|
|
382
467
|
hint: "background shell completed",
|
|
383
468
|
source: "background-tool",
|
|
384
469
|
trustContext: { sourceChannel: "vellum", trustClass: "guardian" },
|
|
385
470
|
},
|
|
386
|
-
{ resolveTarget: async () =>
|
|
471
|
+
{ resolveTarget: async () => conversation },
|
|
387
472
|
);
|
|
388
473
|
|
|
389
474
|
expect(result).toEqual({
|
|
@@ -391,10 +476,10 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
391
476
|
producedToolCalls: false,
|
|
392
477
|
reason: "disk_pressure",
|
|
393
478
|
});
|
|
394
|
-
expect(
|
|
395
|
-
expect(
|
|
396
|
-
expect(
|
|
397
|
-
expect(
|
|
479
|
+
expect(conversation.runCalls).toHaveLength(0);
|
|
480
|
+
expect(conversation.processingToggles).toEqual([]);
|
|
481
|
+
expect(conversation.drainQueueCalls).toBe(0);
|
|
482
|
+
expect(conversation.isProcessing()).toBe(true);
|
|
398
483
|
});
|
|
399
484
|
|
|
400
485
|
test("blocks trusted-contact direct wakes during disk pressure", async () => {
|
|
@@ -413,11 +498,11 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
413
498
|
blockedCapabilities: ["agent-turns", "background-work", "remote-ingress"],
|
|
414
499
|
error: null,
|
|
415
500
|
};
|
|
416
|
-
const
|
|
501
|
+
const conversation = makeWakeConversation({ scriptedAssistant: null });
|
|
417
502
|
|
|
418
503
|
const result = await wakeAgentForOpportunity(
|
|
419
504
|
{
|
|
420
|
-
conversationId:
|
|
505
|
+
conversationId: conversation.conversationId,
|
|
421
506
|
hint: "notify the guardian",
|
|
422
507
|
source: "notification",
|
|
423
508
|
trustContext: {
|
|
@@ -425,14 +510,14 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
425
510
|
trustClass: "trusted_contact",
|
|
426
511
|
},
|
|
427
512
|
},
|
|
428
|
-
{ resolveTarget: async () =>
|
|
513
|
+
{ resolveTarget: async () => conversation },
|
|
429
514
|
);
|
|
430
515
|
|
|
431
516
|
expect(result.reason).toBe("disk_pressure");
|
|
432
|
-
expect(
|
|
517
|
+
expect(conversation.runCalls).toHaveLength(0);
|
|
433
518
|
});
|
|
434
519
|
|
|
435
|
-
test("
|
|
520
|
+
test("forwards a guardian trust snapshot for explicit local-owner cleanup-mode wakes", async () => {
|
|
436
521
|
mockDiskPressureStatus = {
|
|
437
522
|
enabled: true,
|
|
438
523
|
state: "critical",
|
|
@@ -448,31 +533,29 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
448
533
|
blockedCapabilities: ["agent-turns", "background-work", "remote-ingress"],
|
|
449
534
|
error: null,
|
|
450
535
|
};
|
|
451
|
-
const
|
|
536
|
+
const conversation = makeWakeConversation({ scriptedAssistant: null });
|
|
452
537
|
|
|
453
538
|
const result = await wakeAgentForOpportunity(
|
|
454
539
|
{
|
|
455
|
-
conversationId:
|
|
540
|
+
conversationId: conversation.conversationId,
|
|
456
541
|
hint: "clean storage",
|
|
457
542
|
source: "local-cleanup",
|
|
458
543
|
sourceChannel: "vellum",
|
|
459
544
|
sourceInterface: "macos",
|
|
460
545
|
},
|
|
461
|
-
{ resolveTarget: async () =>
|
|
546
|
+
{ resolveTarget: async () => conversation },
|
|
462
547
|
);
|
|
463
548
|
|
|
464
549
|
expect(result).toEqual({ invoked: true, producedToolCalls: false });
|
|
465
|
-
expect(
|
|
466
|
-
expect(
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
diskPressureContext: { cleanupModeActive: true },
|
|
470
|
-
},
|
|
550
|
+
expect(conversation.runCalls).toHaveLength(1);
|
|
551
|
+
expect(conversation.runCalls[0]!.trust).toEqual({
|
|
552
|
+
sourceChannel: "vellum",
|
|
553
|
+
trustClass: "guardian",
|
|
471
554
|
});
|
|
472
555
|
});
|
|
473
556
|
|
|
474
557
|
test("silent no-op when agent produces no tool calls and no text", async () => {
|
|
475
|
-
const
|
|
558
|
+
const conversation = makeWakeConversation({
|
|
476
559
|
baseline: [
|
|
477
560
|
{ role: "user", content: [{ type: "text", text: "hi" }] },
|
|
478
561
|
{ role: "assistant", content: [{ type: "text", text: "hello" }] },
|
|
@@ -486,23 +569,23 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
486
569
|
|
|
487
570
|
const result = await wakeAgentForOpportunity(
|
|
488
571
|
{
|
|
489
|
-
conversationId:
|
|
572
|
+
conversationId: conversation.conversationId,
|
|
490
573
|
hint: "someone asked a question",
|
|
491
574
|
source: "unit-test",
|
|
492
575
|
},
|
|
493
|
-
{ resolveTarget: async () =>
|
|
576
|
+
{ resolveTarget: async () => conversation },
|
|
494
577
|
);
|
|
495
578
|
|
|
496
579
|
expect(result).toEqual({ invoked: true, producedToolCalls: false });
|
|
497
580
|
// Nothing emitted to client.
|
|
498
|
-
expect(
|
|
581
|
+
expect(conversation.emittedEvents).toHaveLength(0);
|
|
499
582
|
// Nothing persisted.
|
|
500
|
-
expect(
|
|
583
|
+
expect(conversation.persistedTailCalls).toHaveLength(0);
|
|
501
584
|
// Nothing pushed into live history.
|
|
502
|
-
expect(
|
|
585
|
+
expect(conversation.pushedMessages).toHaveLength(0);
|
|
503
586
|
// Hint was included in the run input, but baseline is unchanged.
|
|
504
|
-
expect(
|
|
505
|
-
const input =
|
|
587
|
+
expect(conversation.runCalls).toHaveLength(1);
|
|
588
|
+
const input = conversation.runCalls[0]!.input;
|
|
506
589
|
expect(input).toHaveLength(5); // 2 baseline + 3 hint (user + assistant + user)
|
|
507
590
|
expect(input[2]).toEqual({
|
|
508
591
|
role: "user",
|
|
@@ -534,7 +617,7 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
534
617
|
});
|
|
535
618
|
|
|
536
619
|
test("scopes allowed tools during the wake and restores before queued messages drain", async () => {
|
|
537
|
-
const
|
|
620
|
+
const conversation = makeWakeConversation({
|
|
538
621
|
initialAllowedTools: new Set(["bash"]),
|
|
539
622
|
scriptedAssistant: {
|
|
540
623
|
role: "assistant",
|
|
@@ -544,29 +627,29 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
544
627
|
|
|
545
628
|
const result = await wakeAgentForOpportunity(
|
|
546
629
|
{
|
|
547
|
-
conversationId:
|
|
630
|
+
conversationId: conversation.conversationId,
|
|
548
631
|
hint: "review for memories",
|
|
549
632
|
source: "memory-retrospective",
|
|
550
633
|
allowedTools: ["remember"],
|
|
551
634
|
},
|
|
552
|
-
{ resolveTarget: async () =>
|
|
635
|
+
{ resolveTarget: async () => conversation },
|
|
553
636
|
);
|
|
554
637
|
|
|
555
638
|
expect(result).toEqual({ invoked: true, producedToolCalls: false });
|
|
556
|
-
expect(
|
|
557
|
-
expect(
|
|
639
|
+
expect(conversation.runCalls[0]!.allowedTools).toEqual(["remember"]);
|
|
640
|
+
expect(conversation.allowedToolSnapshots).toEqual([["remember"], ["bash"]]);
|
|
558
641
|
|
|
559
|
-
const restoreIndex =
|
|
642
|
+
const restoreIndex = conversation.callSequence.indexOf("tools:bash");
|
|
560
643
|
const processingFalseIndex =
|
|
561
|
-
|
|
562
|
-
const drainIndex =
|
|
644
|
+
conversation.callSequence.indexOf("processing:false");
|
|
645
|
+
const drainIndex = conversation.callSequence.indexOf("drain");
|
|
563
646
|
expect(restoreIndex).toBeGreaterThan(-1);
|
|
564
647
|
expect(restoreIndex).toBeLessThan(processingFalseIndex);
|
|
565
648
|
expect(processingFalseIndex).toBeLessThan(drainIndex);
|
|
566
649
|
});
|
|
567
650
|
|
|
568
651
|
test("restores allowed tools before drain when the wake is a silent no-op", async () => {
|
|
569
|
-
const
|
|
652
|
+
const conversation = makeWakeConversation({
|
|
570
653
|
scriptedAssistant: {
|
|
571
654
|
role: "assistant",
|
|
572
655
|
content: [{ type: "text", text: "" }],
|
|
@@ -575,22 +658,25 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
575
658
|
|
|
576
659
|
const result = await wakeAgentForOpportunity(
|
|
577
660
|
{
|
|
578
|
-
conversationId:
|
|
661
|
+
conversationId: conversation.conversationId,
|
|
579
662
|
hint: "review for memories",
|
|
580
663
|
source: "memory-retrospective",
|
|
581
664
|
allowedTools: ["remember"],
|
|
582
665
|
},
|
|
583
|
-
{ resolveTarget: async () =>
|
|
666
|
+
{ resolveTarget: async () => conversation },
|
|
584
667
|
);
|
|
585
668
|
|
|
586
669
|
expect(result).toEqual({ invoked: true, producedToolCalls: false });
|
|
587
|
-
expect(
|
|
588
|
-
expect(
|
|
670
|
+
expect(conversation.runCalls[0]!.allowedTools).toEqual(["remember"]);
|
|
671
|
+
expect(conversation.allowedToolSnapshots).toEqual([
|
|
672
|
+
["remember"],
|
|
673
|
+
undefined,
|
|
674
|
+
]);
|
|
589
675
|
|
|
590
|
-
const restoreIndex =
|
|
676
|
+
const restoreIndex = conversation.callSequence.indexOf("tools:all");
|
|
591
677
|
const processingFalseIndex =
|
|
592
|
-
|
|
593
|
-
const drainIndex =
|
|
678
|
+
conversation.callSequence.indexOf("processing:false");
|
|
679
|
+
const drainIndex = conversation.callSequence.indexOf("drain");
|
|
594
680
|
expect(restoreIndex).toBeGreaterThan(-1);
|
|
595
681
|
expect(restoreIndex).toBeLessThan(processingFalseIndex);
|
|
596
682
|
expect(processingFalseIndex).toBeLessThan(drainIndex);
|
|
@@ -608,29 +694,28 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
608
694
|
},
|
|
609
695
|
],
|
|
610
696
|
};
|
|
611
|
-
const
|
|
697
|
+
const conversation = makeWakeConversation({
|
|
612
698
|
baseline: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
|
613
699
|
scriptedAssistant: assistantMessage,
|
|
614
700
|
});
|
|
615
701
|
|
|
616
702
|
const result = await wakeAgentForOpportunity(
|
|
617
703
|
{
|
|
618
|
-
conversationId:
|
|
704
|
+
conversationId: conversation.conversationId,
|
|
619
705
|
hint: "question directed at assistant",
|
|
620
706
|
source: "meet-chat-opportunity",
|
|
621
707
|
},
|
|
622
|
-
{ resolveTarget: async () =>
|
|
708
|
+
{ resolveTarget: async () => conversation },
|
|
623
709
|
);
|
|
624
710
|
|
|
625
711
|
expect(result).toEqual({ invoked: true, producedToolCalls: true });
|
|
626
|
-
// Assistant message persisted via the
|
|
627
|
-
expect(
|
|
628
|
-
expect(
|
|
712
|
+
// Assistant message persisted via the daemon boundary (addMessage).
|
|
713
|
+
expect(conversation.persistedTailCalls).toHaveLength(1);
|
|
714
|
+
expect(conversation.persistedTailCalls[0]).toEqual(assistantMessage);
|
|
629
715
|
// Assistant message pushed into live history.
|
|
630
|
-
expect(
|
|
631
|
-
// message_complete
|
|
632
|
-
|
|
633
|
-
const flushed = target.emittedEvents.find(
|
|
716
|
+
expect(conversation.pushedMessages).toContainEqual(assistantMessage);
|
|
717
|
+
// message_complete frame flushed to the client via the translator.
|
|
718
|
+
const flushed = conversation.emittedEvents.find(
|
|
634
719
|
(e) => e.type === "message_complete",
|
|
635
720
|
);
|
|
636
721
|
expect(flushed).toBeDefined();
|
|
@@ -667,7 +752,7 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
667
752
|
content: [{ type: "text", text: "Done." }],
|
|
668
753
|
};
|
|
669
754
|
|
|
670
|
-
const
|
|
755
|
+
const conversation = makeWakeConversation({
|
|
671
756
|
baseline: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
|
672
757
|
scriptedAssistant: firstAssistant,
|
|
673
758
|
scriptedTail: [toolResultUserMsg, followupAssistant],
|
|
@@ -675,30 +760,30 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
675
760
|
|
|
676
761
|
const result = await wakeAgentForOpportunity(
|
|
677
762
|
{
|
|
678
|
-
conversationId:
|
|
763
|
+
conversationId: conversation.conversationId,
|
|
679
764
|
hint: "question directed at assistant",
|
|
680
765
|
source: "meet-chat-opportunity",
|
|
681
766
|
},
|
|
682
|
-
{ resolveTarget: async () =>
|
|
767
|
+
{ resolveTarget: async () => conversation },
|
|
683
768
|
);
|
|
684
769
|
|
|
685
770
|
expect(result).toEqual({ invoked: true, producedToolCalls: true });
|
|
686
771
|
|
|
687
|
-
// All three tail messages persisted in order via the
|
|
688
|
-
expect(
|
|
689
|
-
expect(
|
|
690
|
-
expect(
|
|
691
|
-
expect(
|
|
772
|
+
// All three tail messages persisted in order via the daemon boundary.
|
|
773
|
+
expect(conversation.persistedTailCalls).toHaveLength(3);
|
|
774
|
+
expect(conversation.persistedTailCalls[0]).toEqual(firstAssistant);
|
|
775
|
+
expect(conversation.persistedTailCalls[1]).toEqual(toolResultUserMsg);
|
|
776
|
+
expect(conversation.persistedTailCalls[2]).toEqual(followupAssistant);
|
|
692
777
|
|
|
693
778
|
// All three also pushed into live history so next turn sees them.
|
|
694
|
-
expect(
|
|
695
|
-
expect(
|
|
696
|
-
expect(
|
|
697
|
-
expect(
|
|
779
|
+
expect(conversation.pushedMessages).toHaveLength(3);
|
|
780
|
+
expect(conversation.pushedMessages[0]).toEqual(firstAssistant);
|
|
781
|
+
expect(conversation.pushedMessages[1]).toEqual(toolResultUserMsg);
|
|
782
|
+
expect(conversation.pushedMessages[2]).toEqual(followupAssistant);
|
|
698
783
|
});
|
|
699
784
|
|
|
700
785
|
test("marks processing true during the run and false afterwards", async () => {
|
|
701
|
-
const
|
|
786
|
+
const conversation = makeWakeConversation({
|
|
702
787
|
baseline: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
|
703
788
|
scriptedAssistant: {
|
|
704
789
|
role: "assistant",
|
|
@@ -709,65 +794,47 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
709
794
|
// Snapshot isProcessing() inside the run to prove we actually
|
|
710
795
|
// hold the processing flag while agentLoop.run executes.
|
|
711
796
|
const observedDuringRun: boolean[] = [];
|
|
712
|
-
const originalRun =
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
runOptions?: AgentLoopRunOptions,
|
|
717
|
-
) => {
|
|
718
|
-
observedDuringRun.push(target.isProcessing());
|
|
719
|
-
return originalRun(input, onEvent, runOptions);
|
|
797
|
+
const originalRun = conversation.agentLoop.run;
|
|
798
|
+
conversation.agentLoop.run = async (options: AgentLoopRunOptions) => {
|
|
799
|
+
observedDuringRun.push(conversation.isProcessing());
|
|
800
|
+
return originalRun(options);
|
|
720
801
|
};
|
|
721
802
|
|
|
722
803
|
await wakeAgentForOpportunity(
|
|
723
804
|
{
|
|
724
|
-
conversationId:
|
|
805
|
+
conversationId: conversation.conversationId,
|
|
725
806
|
hint: "x",
|
|
726
807
|
source: "unit-test",
|
|
727
808
|
},
|
|
728
|
-
{ resolveTarget: async () =>
|
|
809
|
+
{ resolveTarget: async () => conversation },
|
|
729
810
|
);
|
|
730
811
|
|
|
731
|
-
//
|
|
732
|
-
expect(
|
|
812
|
+
// setProcessing toggled on then off exactly once.
|
|
813
|
+
expect(conversation.processingToggles).toEqual([true, false]);
|
|
733
814
|
// And the flag was observed as true inside the run body.
|
|
734
815
|
expect(observedDuringRun).toEqual([true]);
|
|
735
816
|
// Back to idle by the time the wake returns.
|
|
736
|
-
expect(
|
|
817
|
+
expect(conversation.isProcessing()).toBe(false);
|
|
737
818
|
});
|
|
738
819
|
|
|
739
820
|
test("marks processing false even when the agent loop throws", async () => {
|
|
740
|
-
const
|
|
741
|
-
const toggles: boolean[] = [];
|
|
742
|
-
let processing = false;
|
|
743
|
-
const target: WakeTarget = {
|
|
821
|
+
const conversation = makeWakeConversation({
|
|
744
822
|
conversationId: "conv-err-guard",
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
throw new Error("LLM exploded");
|
|
748
|
-
},
|
|
749
|
-
},
|
|
750
|
-
getMessages: () => history,
|
|
751
|
-
pushMessage: () => {},
|
|
752
|
-
emitAgentEvent: () => {},
|
|
753
|
-
isProcessing: () => processing,
|
|
754
|
-
markProcessing: (on) => {
|
|
755
|
-
processing = on;
|
|
756
|
-
toggles.push(on);
|
|
823
|
+
runImpl: async () => {
|
|
824
|
+
throw new Error("LLM exploded");
|
|
757
825
|
},
|
|
758
|
-
|
|
759
|
-
};
|
|
826
|
+
});
|
|
760
827
|
|
|
761
828
|
const result = await wakeAgentForOpportunity(
|
|
762
829
|
{ conversationId: "conv-err-guard", hint: "boom", source: "t" },
|
|
763
|
-
{ resolveTarget: async () =>
|
|
830
|
+
{ resolveTarget: async () => conversation },
|
|
764
831
|
);
|
|
765
832
|
|
|
766
833
|
expect(result).toEqual({ invoked: true, producedToolCalls: false });
|
|
767
834
|
// Critical: the finally block must have released the flag despite
|
|
768
835
|
// the thrown error, otherwise the next user turn would hang.
|
|
769
|
-
expect(
|
|
770
|
-
expect(
|
|
836
|
+
expect(conversation.processingToggles).toEqual([true, false]);
|
|
837
|
+
expect(conversation.isProcessing()).toBe(false);
|
|
771
838
|
});
|
|
772
839
|
|
|
773
840
|
test("applies caller-supplied trustContext to the target before the agent loop runs", async () => {
|
|
@@ -775,32 +842,9 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
775
842
|
// guardian trust to clear the side-effect approval gate. The wake must
|
|
776
843
|
// call setTrustContext BEFORE agentLoop.run so the per-turn snapshot
|
|
777
844
|
// captures the elevated trust.
|
|
778
|
-
const
|
|
779
|
-
const runCalls: number[] = [];
|
|
780
|
-
let callOrder = 0;
|
|
781
|
-
|
|
782
|
-
const history: Message[] = [];
|
|
783
|
-
let processing = false;
|
|
784
|
-
const target: WakeTarget = {
|
|
845
|
+
const conversation = makeWakeConversation({
|
|
785
846
|
conversationId: "conv-trust",
|
|
786
|
-
|
|
787
|
-
run: async (input) => {
|
|
788
|
-
runCalls.push(callOrder++);
|
|
789
|
-
return runResult([...input]);
|
|
790
|
-
},
|
|
791
|
-
},
|
|
792
|
-
getMessages: () => history,
|
|
793
|
-
pushMessage: () => {},
|
|
794
|
-
emitAgentEvent: () => {},
|
|
795
|
-
isProcessing: () => processing,
|
|
796
|
-
markProcessing: (on) => {
|
|
797
|
-
processing = on;
|
|
798
|
-
},
|
|
799
|
-
persistTailMessage: async () => {},
|
|
800
|
-
setTrustContext: (ctx) => {
|
|
801
|
-
trustCalls.push({ ctx, before: callOrder++ });
|
|
802
|
-
},
|
|
803
|
-
};
|
|
847
|
+
});
|
|
804
848
|
|
|
805
849
|
await wakeAgentForOpportunity(
|
|
806
850
|
{
|
|
@@ -809,40 +853,25 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
809
853
|
source: "memory_v2_consolidation",
|
|
810
854
|
trustContext: { sourceChannel: "vellum", trustClass: "guardian" },
|
|
811
855
|
},
|
|
812
|
-
{ resolveTarget: async () =>
|
|
856
|
+
{ resolveTarget: async () => conversation },
|
|
813
857
|
);
|
|
814
858
|
|
|
815
|
-
expect(
|
|
816
|
-
expect(
|
|
859
|
+
expect(conversation.setTrustContextCalls).toHaveLength(1);
|
|
860
|
+
expect(conversation.setTrustContextCalls[0]!.ctx).toEqual({
|
|
817
861
|
sourceChannel: "vellum",
|
|
818
862
|
trustClass: "guardian",
|
|
819
863
|
});
|
|
820
864
|
// setTrustContext fired strictly before agentLoop.run.
|
|
821
|
-
expect(runCalls).toHaveLength(1);
|
|
822
|
-
expect(
|
|
865
|
+
expect(conversation.runCalls).toHaveLength(1);
|
|
866
|
+
expect(conversation.setTrustContextCalls[0]!.order).toBeLessThan(
|
|
867
|
+
conversation.runCalls[0]!.order,
|
|
868
|
+
);
|
|
823
869
|
});
|
|
824
870
|
|
|
825
871
|
test("does not call setTrustContext when no trustContext is supplied", async () => {
|
|
826
|
-
const
|
|
827
|
-
const history: Message[] = [];
|
|
828
|
-
let processing = false;
|
|
829
|
-
const target: WakeTarget = {
|
|
872
|
+
const conversation = makeWakeConversation({
|
|
830
873
|
conversationId: "conv-no-trust",
|
|
831
|
-
|
|
832
|
-
run: async (input) => runResult([...input]),
|
|
833
|
-
},
|
|
834
|
-
getMessages: () => history,
|
|
835
|
-
pushMessage: () => {},
|
|
836
|
-
emitAgentEvent: () => {},
|
|
837
|
-
isProcessing: () => processing,
|
|
838
|
-
markProcessing: (on) => {
|
|
839
|
-
processing = on;
|
|
840
|
-
},
|
|
841
|
-
persistTailMessage: async () => {},
|
|
842
|
-
setTrustContext: (ctx) => {
|
|
843
|
-
trustCalls.push(ctx);
|
|
844
|
-
},
|
|
845
|
-
};
|
|
874
|
+
});
|
|
846
875
|
|
|
847
876
|
await wakeAgentForOpportunity(
|
|
848
877
|
{
|
|
@@ -850,13 +879,13 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
850
879
|
hint: "x",
|
|
851
880
|
source: "t",
|
|
852
881
|
},
|
|
853
|
-
{ resolveTarget: async () =>
|
|
882
|
+
{ resolveTarget: async () => conversation },
|
|
854
883
|
);
|
|
855
884
|
|
|
856
885
|
// Inbound-message conversations populate trust via processMessage().
|
|
857
886
|
// Without an explicit opt-in from the caller, the wake must not
|
|
858
887
|
// overwrite whatever the conversation already holds.
|
|
859
|
-
expect(
|
|
888
|
+
expect(conversation.setTrustContextCalls).toHaveLength(0);
|
|
860
889
|
});
|
|
861
890
|
|
|
862
891
|
test("two concurrent wakes on the same conversation are serialized", async () => {
|
|
@@ -867,36 +896,22 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
867
896
|
const runCompleteOrder: number[] = [];
|
|
868
897
|
|
|
869
898
|
let callIndex = 0;
|
|
870
|
-
const
|
|
871
|
-
let processing = false;
|
|
872
|
-
const target: WakeTarget = {
|
|
899
|
+
const conversation = makeWakeConversation({
|
|
873
900
|
conversationId: "conv-serialize",
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
return runResult(input); // no assistant message → silent no-op
|
|
885
|
-
},
|
|
886
|
-
},
|
|
887
|
-
getMessages: () => history,
|
|
888
|
-
pushMessage: (msg) => {
|
|
889
|
-
history.push(msg);
|
|
890
|
-
},
|
|
891
|
-
emitAgentEvent: () => {},
|
|
892
|
-
isProcessing: () => processing,
|
|
893
|
-
markProcessing: (on) => {
|
|
894
|
-
processing = on;
|
|
901
|
+
runImpl: async (input) => {
|
|
902
|
+
const myIndex = ++callIndex;
|
|
903
|
+
runStartOrder.push(myIndex);
|
|
904
|
+
if (myIndex === 1) {
|
|
905
|
+
await gate1.promise;
|
|
906
|
+
} else {
|
|
907
|
+
await gate2.promise;
|
|
908
|
+
}
|
|
909
|
+
runCompleteOrder.push(myIndex);
|
|
910
|
+
return runResult(input); // no assistant message → silent no-op
|
|
895
911
|
},
|
|
896
|
-
|
|
897
|
-
};
|
|
912
|
+
});
|
|
898
913
|
|
|
899
|
-
const deps = { resolveTarget: async () =>
|
|
914
|
+
const deps = { resolveTarget: async () => conversation };
|
|
900
915
|
|
|
901
916
|
const p1 = wakeAgentForOpportunity(
|
|
902
917
|
{ conversationId: "conv-serialize", hint: "first", source: "t1" },
|
|
@@ -924,30 +939,11 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
924
939
|
});
|
|
925
940
|
|
|
926
941
|
test("waits while a concurrent user turn is in flight", async () => {
|
|
927
|
-
const
|
|
928
|
-
let processing = true;
|
|
929
|
-
const target: WakeTarget & { setProcessing: (v: boolean) => void } = {
|
|
942
|
+
const conversation = makeWakeConversation({
|
|
930
943
|
conversationId: "conv-user-turn",
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
getMessages: () => history,
|
|
935
|
-
pushMessage: (msg) => {
|
|
936
|
-
history.push(msg);
|
|
937
|
-
},
|
|
938
|
-
emitAgentEvent: () => {},
|
|
939
|
-
isProcessing: () => processing,
|
|
940
|
-
// The wake's own markProcessing updates track the flag too — the
|
|
941
|
-
// outer "user turn" holds it at true until setProcessing(false)
|
|
942
|
-
// is called below.
|
|
943
|
-
markProcessing: (on) => {
|
|
944
|
-
processing = on;
|
|
945
|
-
},
|
|
946
|
-
persistTailMessage: async () => {},
|
|
947
|
-
setProcessing: (v) => {
|
|
948
|
-
processing = v;
|
|
949
|
-
},
|
|
950
|
-
};
|
|
944
|
+
isProcessing: true,
|
|
945
|
+
runImpl: async (input) => runResult(input),
|
|
946
|
+
});
|
|
951
947
|
|
|
952
948
|
const wakePromise = wakeAgentForOpportunity(
|
|
953
949
|
{
|
|
@@ -955,7 +951,7 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
955
951
|
hint: "opportunity while user typing",
|
|
956
952
|
source: "unit-test",
|
|
957
953
|
},
|
|
958
|
-
{ resolveTarget: async () =>
|
|
954
|
+
{ resolveTarget: async () => conversation },
|
|
959
955
|
);
|
|
960
956
|
|
|
961
957
|
// Wake should be waiting (isProcessing returns true).
|
|
@@ -969,7 +965,7 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
969
965
|
expect(settled).toBe(false);
|
|
970
966
|
|
|
971
967
|
// "User turn" completes — wake now proceeds.
|
|
972
|
-
|
|
968
|
+
conversation.setProcessing(false);
|
|
973
969
|
const result = await wakePromise;
|
|
974
970
|
expect(result.invoked).toBe(true);
|
|
975
971
|
expect(result.producedToolCalls).toBe(false);
|
|
@@ -992,17 +988,11 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
992
988
|
// forward the injected `now` past the 30s deadline so waitUntilIdle
|
|
993
989
|
// returns false. Without the distinct `timeout` reason, callers
|
|
994
990
|
// cannot tell this case apart from "not_found".
|
|
995
|
-
const
|
|
996
|
-
const target: WakeTarget = {
|
|
991
|
+
const conversation = makeWakeConversation({
|
|
997
992
|
conversationId: "conv-busy",
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
emitAgentEvent: () => {},
|
|
1002
|
-
isProcessing: () => true,
|
|
1003
|
-
markProcessing: () => {},
|
|
1004
|
-
persistTailMessage: async () => {},
|
|
1005
|
-
};
|
|
993
|
+
isProcessing: true,
|
|
994
|
+
runImpl: async (input) => runResult(input),
|
|
995
|
+
});
|
|
1006
996
|
let t = 0;
|
|
1007
997
|
const now = () => {
|
|
1008
998
|
// First call establishes the deadline at +30_000. Every subsequent
|
|
@@ -1015,7 +1005,7 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
1015
1005
|
|
|
1016
1006
|
const result = await wakeAgentForOpportunity(
|
|
1017
1007
|
{ conversationId: "conv-busy", hint: "x", source: "y" },
|
|
1018
|
-
{ resolveTarget: async () =>
|
|
1008
|
+
{ resolveTarget: async () => conversation, now },
|
|
1019
1009
|
);
|
|
1020
1010
|
expect(result).toEqual({
|
|
1021
1011
|
invoked: false,
|
|
@@ -1025,35 +1015,20 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
1025
1015
|
});
|
|
1026
1016
|
|
|
1027
1017
|
test("agent loop error is treated as a no-op", async () => {
|
|
1028
|
-
const
|
|
1029
|
-
let processing = false;
|
|
1030
|
-
const persisted: Message[] = [];
|
|
1031
|
-
const target: WakeTarget = {
|
|
1018
|
+
const conversation = makeWakeConversation({
|
|
1032
1019
|
conversationId: "conv-err",
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
throw new Error("LLM exploded");
|
|
1036
|
-
},
|
|
1020
|
+
runImpl: async () => {
|
|
1021
|
+
throw new Error("LLM exploded");
|
|
1037
1022
|
},
|
|
1038
|
-
|
|
1039
|
-
pushMessage: () => {},
|
|
1040
|
-
emitAgentEvent: () => {},
|
|
1041
|
-
isProcessing: () => processing,
|
|
1042
|
-
markProcessing: (on) => {
|
|
1043
|
-
processing = on;
|
|
1044
|
-
},
|
|
1045
|
-
persistTailMessage: async (m) => {
|
|
1046
|
-
persisted.push(m);
|
|
1047
|
-
},
|
|
1048
|
-
};
|
|
1023
|
+
});
|
|
1049
1024
|
|
|
1050
1025
|
const result = await wakeAgentForOpportunity(
|
|
1051
1026
|
{ conversationId: "conv-err", hint: "boom", source: "t" },
|
|
1052
|
-
{ resolveTarget: async () =>
|
|
1027
|
+
{ resolveTarget: async () => conversation },
|
|
1053
1028
|
);
|
|
1054
1029
|
|
|
1055
1030
|
expect(result).toEqual({ invoked: true, producedToolCalls: false });
|
|
1056
|
-
expect(
|
|
1031
|
+
expect(conversation.persistedTailCalls).toHaveLength(0);
|
|
1057
1032
|
});
|
|
1058
1033
|
|
|
1059
1034
|
test("drainQueue is called in finally after a successful run", async () => {
|
|
@@ -1061,7 +1036,7 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
1061
1036
|
// wake set `processing = true`) must be picked up after the wake
|
|
1062
1037
|
// completes. Mirrors the canonical user-turn `finally` path which
|
|
1063
1038
|
// sets `processing = false` then calls `drainQueue`.
|
|
1064
|
-
const
|
|
1039
|
+
const conversation = makeWakeConversation({
|
|
1065
1040
|
baseline: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
|
1066
1041
|
scriptedAssistant: {
|
|
1067
1042
|
role: "assistant",
|
|
@@ -1071,76 +1046,55 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
1071
1046
|
|
|
1072
1047
|
await wakeAgentForOpportunity(
|
|
1073
1048
|
{
|
|
1074
|
-
conversationId:
|
|
1049
|
+
conversationId: conversation.conversationId,
|
|
1075
1050
|
hint: "x",
|
|
1076
1051
|
source: "unit-test",
|
|
1077
1052
|
},
|
|
1078
|
-
{ resolveTarget: async () =>
|
|
1053
|
+
{ resolveTarget: async () => conversation },
|
|
1079
1054
|
);
|
|
1080
1055
|
|
|
1081
|
-
expect(
|
|
1056
|
+
expect(conversation.drainQueueCalls).toBe(1);
|
|
1082
1057
|
// Critical ordering invariant: drain runs after processing=false.
|
|
1083
1058
|
// If drain ran while processing was still true,
|
|
1084
1059
|
// `enqueueMessage`'s `if (!ctx.isProcessing()) return ...` gate would
|
|
1085
1060
|
// see processing=true and the drained item would itself just
|
|
1086
1061
|
// re-enqueue — no progress. Snapshot the live flag *inside* drain
|
|
1087
1062
|
// (rather than inferring from toggle order) so a future regression
|
|
1088
|
-
// that called drain before
|
|
1063
|
+
// that called drain before setProcessing(false) would fail this
|
|
1089
1064
|
// assertion directly.
|
|
1090
|
-
expect(
|
|
1091
|
-
expect(
|
|
1092
|
-
expect(
|
|
1065
|
+
expect(conversation.processingDuringDrain).toEqual([false]);
|
|
1066
|
+
expect(conversation.processingToggles).toEqual([true, false]);
|
|
1067
|
+
expect(conversation.isProcessing()).toBe(false);
|
|
1093
1068
|
});
|
|
1094
1069
|
|
|
1095
1070
|
test("drainQueue is called in finally even when the agent loop throws", async () => {
|
|
1096
1071
|
// Verifies the drain is in the finally block, not just on success.
|
|
1097
1072
|
// A wake that crashes mid-run must still flush queued messages —
|
|
1098
1073
|
// otherwise a transient LLM error strands every concurrent send.
|
|
1099
|
-
const
|
|
1100
|
-
const toggles: boolean[] = [];
|
|
1101
|
-
let processing = false;
|
|
1102
|
-
const target: WakeTarget = {
|
|
1074
|
+
const conversation = makeWakeConversation({
|
|
1103
1075
|
conversationId: "conv-drain-on-throw",
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
throw new Error("LLM exploded mid-wake");
|
|
1107
|
-
},
|
|
1108
|
-
},
|
|
1109
|
-
getMessages: () => [],
|
|
1110
|
-
pushMessage: () => {},
|
|
1111
|
-
emitAgentEvent: () => {},
|
|
1112
|
-
isProcessing: () => processing,
|
|
1113
|
-
markProcessing: (on) => {
|
|
1114
|
-
processing = on;
|
|
1115
|
-
toggles.push(on);
|
|
1076
|
+
runImpl: async () => {
|
|
1077
|
+
throw new Error("LLM exploded mid-wake");
|
|
1116
1078
|
},
|
|
1117
|
-
|
|
1118
|
-
drainQueue: async () => {
|
|
1119
|
-
// Snapshot the live `processing` flag *inside* drain rather
|
|
1120
|
-
// than inferring from toggle order. This directly observes the
|
|
1121
|
-
// state visible to enqueueMessage's gate when a queued message
|
|
1122
|
-
// is dequeued.
|
|
1123
|
-
drainProcessingSnapshots.push(processing);
|
|
1124
|
-
},
|
|
1125
|
-
};
|
|
1079
|
+
});
|
|
1126
1080
|
|
|
1127
1081
|
const result = await wakeAgentForOpportunity(
|
|
1128
1082
|
{ conversationId: "conv-drain-on-throw", hint: "boom", source: "t" },
|
|
1129
|
-
{ resolveTarget: async () =>
|
|
1083
|
+
{ resolveTarget: async () => conversation },
|
|
1130
1084
|
);
|
|
1131
1085
|
|
|
1132
1086
|
expect(result).toEqual({ invoked: true, producedToolCalls: false });
|
|
1133
|
-
// Drain ran AFTER
|
|
1087
|
+
// Drain ran AFTER setProcessing(false), satisfying the
|
|
1134
1088
|
// enqueueMessage gate invariant. Snapshot proves the flag was
|
|
1135
1089
|
// false at the moment drain ran.
|
|
1136
|
-
expect(
|
|
1137
|
-
expect(
|
|
1090
|
+
expect(conversation.processingDuringDrain).toEqual([false]);
|
|
1091
|
+
expect(conversation.processingToggles).toEqual([true, false]);
|
|
1138
1092
|
});
|
|
1139
1093
|
|
|
1140
1094
|
test("missing drainQueue hook is tolerated (no-op fallback)", async () => {
|
|
1141
1095
|
// The hook is intentionally optional so test stubs without a queue
|
|
1142
|
-
// can omit it. Production daemon
|
|
1143
|
-
const
|
|
1096
|
+
// can omit it. Production daemon always wires it.
|
|
1097
|
+
const conversation = makeWakeConversation({
|
|
1144
1098
|
baseline: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
|
1145
1099
|
scriptedAssistant: {
|
|
1146
1100
|
role: "assistant",
|
|
@@ -1151,50 +1105,50 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
1151
1105
|
|
|
1152
1106
|
const result = await wakeAgentForOpportunity(
|
|
1153
1107
|
{
|
|
1154
|
-
conversationId:
|
|
1108
|
+
conversationId: conversation.conversationId,
|
|
1155
1109
|
hint: "x",
|
|
1156
1110
|
source: "unit-test",
|
|
1157
1111
|
},
|
|
1158
|
-
{ resolveTarget: async () =>
|
|
1112
|
+
{ resolveTarget: async () => conversation },
|
|
1159
1113
|
);
|
|
1160
1114
|
|
|
1161
1115
|
expect(result.invoked).toBe(true);
|
|
1162
1116
|
// No throw, no drain attempt recorded.
|
|
1163
|
-
expect(
|
|
1117
|
+
expect(conversation.drainQueueCalls).toBe(0);
|
|
1164
1118
|
});
|
|
1165
1119
|
|
|
1166
1120
|
test("drainQueue rejection does not propagate from the wake", async () => {
|
|
1167
1121
|
// Defense in depth: if the queue drain throws (e.g. a poisoned
|
|
1168
1122
|
// message), the wake itself must still resolve normally — the
|
|
1169
1123
|
// drain failure is logged but never surfaced.
|
|
1170
|
-
const
|
|
1124
|
+
const conversation = makeWakeConversation({
|
|
1171
1125
|
baseline: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
|
1172
1126
|
scriptedAssistant: {
|
|
1173
1127
|
role: "assistant",
|
|
1174
1128
|
content: [{ type: "text", text: "reply" }],
|
|
1175
1129
|
},
|
|
1176
1130
|
});
|
|
1177
|
-
|
|
1131
|
+
conversation.drainQueue = async () => {
|
|
1178
1132
|
throw new Error("drain blew up");
|
|
1179
1133
|
};
|
|
1180
1134
|
|
|
1181
1135
|
const result = await wakeAgentForOpportunity(
|
|
1182
1136
|
{
|
|
1183
|
-
conversationId:
|
|
1137
|
+
conversationId: conversation.conversationId,
|
|
1184
1138
|
hint: "x",
|
|
1185
1139
|
source: "unit-test",
|
|
1186
1140
|
},
|
|
1187
|
-
{ resolveTarget: async () =>
|
|
1141
|
+
{ resolveTarget: async () => conversation },
|
|
1188
1142
|
);
|
|
1189
1143
|
|
|
1190
1144
|
expect(result.invoked).toBe(true);
|
|
1191
1145
|
});
|
|
1192
1146
|
|
|
1193
1147
|
test("persistTailMessage called for each tail message in order", async () => {
|
|
1194
|
-
// Verifies Gap 2 fix: the wake delegates persistence to the
|
|
1195
|
-
// so the
|
|
1196
|
-
// only check the call ordering / arguments here — the
|
|
1197
|
-
//
|
|
1148
|
+
// Verifies Gap 2 fix: the wake delegates persistence to the daemon
|
|
1149
|
+
// boundary (addMessage) so the channel/interface metadata is built
|
|
1150
|
+
// there. We only check the call ordering / arguments here — the
|
|
1151
|
+
// metadata composition is exercised separately.
|
|
1198
1152
|
const firstAssistant: Message = {
|
|
1199
1153
|
role: "assistant",
|
|
1200
1154
|
content: [
|
|
@@ -1214,7 +1168,7 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
1214
1168
|
role: "assistant",
|
|
1215
1169
|
content: [{ type: "text", text: "All set." }],
|
|
1216
1170
|
};
|
|
1217
|
-
const
|
|
1171
|
+
const conversation = makeWakeConversation({
|
|
1218
1172
|
baseline: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
|
1219
1173
|
scriptedAssistant: firstAssistant,
|
|
1220
1174
|
scriptedTail: [toolResultUserMsg, followup],
|
|
@@ -1222,14 +1176,14 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
1222
1176
|
|
|
1223
1177
|
await wakeAgentForOpportunity(
|
|
1224
1178
|
{
|
|
1225
|
-
conversationId:
|
|
1179
|
+
conversationId: conversation.conversationId,
|
|
1226
1180
|
hint: "x",
|
|
1227
1181
|
source: "meet-chat-opportunity",
|
|
1228
1182
|
},
|
|
1229
|
-
{ resolveTarget: async () =>
|
|
1183
|
+
{ resolveTarget: async () => conversation },
|
|
1230
1184
|
);
|
|
1231
1185
|
|
|
1232
|
-
expect(
|
|
1186
|
+
expect(conversation.persistedTailCalls).toEqual([
|
|
1233
1187
|
firstAssistant,
|
|
1234
1188
|
toolResultUserMsg,
|
|
1235
1189
|
followup,
|
|
@@ -1249,7 +1203,7 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
1249
1203
|
// assistant outputs).
|
|
1250
1204
|
//
|
|
1251
1205
|
// Mirrors the canonical user-turn pattern in
|
|
1252
|
-
// conversation-agent-loop.ts:
|
|
1206
|
+
// conversation-agent-loop.ts: messages updated →
|
|
1253
1207
|
// processing=false → drainQueue.
|
|
1254
1208
|
const firstAssistant: Message = {
|
|
1255
1209
|
role: "assistant",
|
|
@@ -1265,7 +1219,7 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
1265
1219
|
role: "assistant",
|
|
1266
1220
|
content: [{ type: "text", text: "All done." }],
|
|
1267
1221
|
};
|
|
1268
|
-
const
|
|
1222
|
+
const conversation = makeWakeConversation({
|
|
1269
1223
|
baseline: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
|
1270
1224
|
scriptedAssistant: firstAssistant,
|
|
1271
1225
|
scriptedTail: [toolResultUserMsg, followup],
|
|
@@ -1273,17 +1227,17 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
1273
1227
|
|
|
1274
1228
|
await wakeAgentForOpportunity(
|
|
1275
1229
|
{
|
|
1276
|
-
conversationId:
|
|
1230
|
+
conversationId: conversation.conversationId,
|
|
1277
1231
|
hint: "x",
|
|
1278
1232
|
source: "meet-chat-opportunity",
|
|
1279
1233
|
},
|
|
1280
|
-
{ resolveTarget: async () =>
|
|
1234
|
+
{ resolveTarget: async () => conversation },
|
|
1281
1235
|
);
|
|
1282
1236
|
|
|
1283
1237
|
// Full call sequence: processing toggled true → 3 pushes →
|
|
1284
1238
|
// 3 persists → processing toggled false → drain. Specifically,
|
|
1285
1239
|
// every push and every persist must precede the single drain.
|
|
1286
|
-
expect(
|
|
1240
|
+
expect(conversation.callSequence).toEqual([
|
|
1287
1241
|
"processing:true",
|
|
1288
1242
|
"push",
|
|
1289
1243
|
"push",
|
|
@@ -1298,14 +1252,14 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
1298
1252
|
// Belt-and-braces: cross-check via index lookups so the failure
|
|
1299
1253
|
// mode (drain before push/persist) shows up clearly even if the
|
|
1300
1254
|
// exact sequence ever picks up additional entries.
|
|
1301
|
-
const drainIdx =
|
|
1302
|
-
const lastPushIdx =
|
|
1303
|
-
const lastPersistIdx =
|
|
1255
|
+
const drainIdx = conversation.callSequence.indexOf("drain");
|
|
1256
|
+
const lastPushIdx = conversation.callSequence.lastIndexOf("push");
|
|
1257
|
+
const lastPersistIdx = conversation.callSequence.lastIndexOf("persist");
|
|
1304
1258
|
expect(drainIdx).toBeGreaterThan(lastPushIdx);
|
|
1305
1259
|
expect(drainIdx).toBeGreaterThan(lastPersistIdx);
|
|
1306
1260
|
|
|
1307
1261
|
// And processing was false when drain ran.
|
|
1308
|
-
expect(
|
|
1262
|
+
expect(conversation.processingDuringDrain).toEqual([false]);
|
|
1309
1263
|
},
|
|
1310
1264
|
);
|
|
1311
1265
|
|
|
@@ -1318,7 +1272,7 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
1318
1272
|
// visible text and no tool calls, so no push/persist/emit should
|
|
1319
1273
|
// happen. drainQueue must still run in the finally block so a
|
|
1320
1274
|
// racy queued message is not stranded.
|
|
1321
|
-
const
|
|
1275
|
+
const conversation = makeWakeConversation({
|
|
1322
1276
|
baseline: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
|
1323
1277
|
scriptedAssistant: {
|
|
1324
1278
|
role: "assistant",
|
|
@@ -1328,26 +1282,26 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
1328
1282
|
|
|
1329
1283
|
await wakeAgentForOpportunity(
|
|
1330
1284
|
{
|
|
1331
|
-
conversationId:
|
|
1285
|
+
conversationId: conversation.conversationId,
|
|
1332
1286
|
hint: "x",
|
|
1333
1287
|
source: "unit-test",
|
|
1334
1288
|
},
|
|
1335
|
-
{ resolveTarget: async () =>
|
|
1289
|
+
{ resolveTarget: async () => conversation },
|
|
1336
1290
|
);
|
|
1337
1291
|
|
|
1338
1292
|
// No push, no persist, no emit.
|
|
1339
|
-
expect(
|
|
1340
|
-
expect(
|
|
1341
|
-
expect(
|
|
1293
|
+
expect(conversation.pushedMessages).toHaveLength(0);
|
|
1294
|
+
expect(conversation.persistedTailCalls).toHaveLength(0);
|
|
1295
|
+
expect(conversation.emittedEvents).toHaveLength(0);
|
|
1342
1296
|
|
|
1343
1297
|
// But drain still ran exactly once, after processing flipped to
|
|
1344
1298
|
// false. Sequence: toggle true → toggle false → drain.
|
|
1345
|
-
expect(
|
|
1299
|
+
expect(conversation.callSequence).toEqual([
|
|
1346
1300
|
"processing:true",
|
|
1347
1301
|
"processing:false",
|
|
1348
1302
|
"drain",
|
|
1349
1303
|
]);
|
|
1350
|
-
expect(
|
|
1304
|
+
expect(conversation.processingDuringDrain).toEqual([false]);
|
|
1351
1305
|
},
|
|
1352
1306
|
);
|
|
1353
1307
|
|
|
@@ -1386,94 +1340,61 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
1386
1340
|
content: [{ type: "text", text: "All done." }],
|
|
1387
1341
|
};
|
|
1388
1342
|
|
|
1389
|
-
const
|
|
1390
|
-
const pushedMessages: Message[] = [];
|
|
1391
|
-
const persistedTailCalls: Message[] = [];
|
|
1392
|
-
// Snapshot of how many tail messages had been persisted at each
|
|
1393
|
-
// point a streaming event reached the target. This is the actual
|
|
1394
|
-
// observability invariant: when a turn-2 streaming event arrives,
|
|
1395
|
-
// turn-1's messages must already be persisted so a fetchHistory
|
|
1396
|
-
// call from a client opening the conversation mid-stream returns
|
|
1397
|
-
// turn-1's content.
|
|
1398
|
-
const persistedAtEachEmit: number[] = [];
|
|
1399
|
-
const baseline: Message[] = [
|
|
1400
|
-
{ role: "user", content: [{ type: "text", text: "hi" }] },
|
|
1401
|
-
];
|
|
1402
|
-
const history: Message[] = [...baseline];
|
|
1403
|
-
let processing = false;
|
|
1404
|
-
|
|
1405
|
-
const target: WakeTarget = {
|
|
1343
|
+
const conversation = makeWakeConversation({
|
|
1406
1344
|
conversationId: "conv-stream",
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
},
|
|
1459
|
-
},
|
|
1460
|
-
getMessages: () => history,
|
|
1461
|
-
pushMessage: (msg) => {
|
|
1462
|
-
pushedMessages.push(msg);
|
|
1463
|
-
history.push(msg);
|
|
1464
|
-
},
|
|
1465
|
-
emitAgentEvent: (event) => {
|
|
1466
|
-
emittedEvents.push(event);
|
|
1467
|
-
persistedAtEachEmit.push(persistedTailCalls.length);
|
|
1468
|
-
},
|
|
1469
|
-
isProcessing: () => processing,
|
|
1470
|
-
markProcessing: (on) => {
|
|
1471
|
-
processing = on;
|
|
1472
|
-
},
|
|
1473
|
-
persistTailMessage: async (msg) => {
|
|
1474
|
-
persistedTailCalls.push(msg);
|
|
1345
|
+
baseline: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
|
1346
|
+
runImpl: async (_input, onEvent, runOptions) => {
|
|
1347
|
+
// Preamble + assistant hint + postamble (mirrors what the
|
|
1348
|
+
// wake injects). The agent-wake helper expects these three
|
|
1349
|
+
// hint messages in the input it hands to run().
|
|
1350
|
+
const runHistory: Message[] = [..._input];
|
|
1351
|
+
|
|
1352
|
+
// Turn 1: stream a text_delta + message_complete, then
|
|
1353
|
+
// fire the checkpoint after the tool_result lands.
|
|
1354
|
+
await onEvent({ type: "text_delta", text: "Working" });
|
|
1355
|
+
runHistory.push(turn1Assistant);
|
|
1356
|
+
await onEvent({
|
|
1357
|
+
type: "message_complete",
|
|
1358
|
+
message: turn1Assistant,
|
|
1359
|
+
});
|
|
1360
|
+
runHistory.push(turn1ToolResult);
|
|
1361
|
+
const dec1 = await runOptions!.onCheckpoint!({
|
|
1362
|
+
turnIndex: 0,
|
|
1363
|
+
toolCount: 1,
|
|
1364
|
+
hasToolUse: true,
|
|
1365
|
+
history: runHistory,
|
|
1366
|
+
});
|
|
1367
|
+
expect(dec1).toBe("continue");
|
|
1368
|
+
|
|
1369
|
+
// Turn 2: another tool turn — must already see the live
|
|
1370
|
+
// streaming because mode flipped after turn 1.
|
|
1371
|
+
await onEvent({ type: "text_delta", text: "Still going" });
|
|
1372
|
+
runHistory.push(turn2Assistant);
|
|
1373
|
+
await onEvent({
|
|
1374
|
+
type: "message_complete",
|
|
1375
|
+
message: turn2Assistant,
|
|
1376
|
+
});
|
|
1377
|
+
runHistory.push(turn2ToolResult);
|
|
1378
|
+
const dec2 = await runOptions!.onCheckpoint!({
|
|
1379
|
+
turnIndex: 1,
|
|
1380
|
+
toolCount: 1,
|
|
1381
|
+
hasToolUse: true,
|
|
1382
|
+
history: runHistory,
|
|
1383
|
+
});
|
|
1384
|
+
expect(dec2).toBe("continue");
|
|
1385
|
+
|
|
1386
|
+
// Final assistant message with no tool calls — loop would
|
|
1387
|
+
// exit. onCheckpoint does NOT fire for the terminal turn,
|
|
1388
|
+
// so the post-run flushPendingTail must catch this one.
|
|
1389
|
+
await onEvent({ type: "text_delta", text: "All done." });
|
|
1390
|
+
runHistory.push(finalAssistant);
|
|
1391
|
+
await onEvent({
|
|
1392
|
+
type: "message_complete",
|
|
1393
|
+
message: finalAssistant,
|
|
1394
|
+
});
|
|
1395
|
+
return runResult(runHistory);
|
|
1475
1396
|
},
|
|
1476
|
-
};
|
|
1397
|
+
});
|
|
1477
1398
|
|
|
1478
1399
|
const result = await wakeAgentForOpportunity(
|
|
1479
1400
|
{
|
|
@@ -1481,31 +1402,35 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
1481
1402
|
hint: "consolidate",
|
|
1482
1403
|
source: "memory_v2_consolidation",
|
|
1483
1404
|
},
|
|
1484
|
-
{ resolveTarget: async () =>
|
|
1405
|
+
{ resolveTarget: async () => conversation },
|
|
1485
1406
|
);
|
|
1486
1407
|
|
|
1487
1408
|
expect(result).toEqual({ invoked: true, producedToolCalls: true });
|
|
1488
1409
|
|
|
1489
1410
|
// All 5 tail messages persisted in order. The first two via
|
|
1490
1411
|
// turn-1 checkpoint, the next two via turn-2 checkpoint, and
|
|
1491
|
-
// `finalAssistant` via the post-run flush.
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
expect(persistedTailCalls
|
|
1495
|
-
expect(persistedTailCalls[
|
|
1496
|
-
expect(persistedTailCalls[
|
|
1497
|
-
expect(persistedTailCalls[
|
|
1412
|
+
// `finalAssistant` via the post-run flush. Persistence flows
|
|
1413
|
+
// through addMessage (serialize → store), so identity is not
|
|
1414
|
+
// preserved — assert structural equality.
|
|
1415
|
+
expect(conversation.persistedTailCalls).toHaveLength(5);
|
|
1416
|
+
expect(conversation.persistedTailCalls[0]).toEqual(turn1Assistant);
|
|
1417
|
+
expect(conversation.persistedTailCalls[1]).toEqual(turn1ToolResult);
|
|
1418
|
+
expect(conversation.persistedTailCalls[2]).toEqual(turn2Assistant);
|
|
1419
|
+
expect(conversation.persistedTailCalls[3]).toEqual(turn2ToolResult);
|
|
1420
|
+
expect(conversation.persistedTailCalls[4]).toEqual(finalAssistant);
|
|
1498
1421
|
|
|
1499
1422
|
// Critical observability invariant: by the time turn-2's
|
|
1500
1423
|
// streaming text_delta reached the client, turn-1's messages
|
|
1501
1424
|
// were already persisted. A client opening the conversation at
|
|
1502
1425
|
// that moment would fetchHistory and see turn-1, plus stream
|
|
1503
1426
|
// turn-2 live — instead of seeing an empty welcome view.
|
|
1504
|
-
const turn2DeltaIdx = emittedEvents.findIndex(
|
|
1505
|
-
(e) => e.type === "
|
|
1427
|
+
const turn2DeltaIdx = conversation.emittedEvents.findIndex(
|
|
1428
|
+
(e) => e.type === "assistant_text_delta" && e.text === "Still going",
|
|
1506
1429
|
);
|
|
1507
1430
|
expect(turn2DeltaIdx).toBeGreaterThan(-1);
|
|
1508
|
-
expect(
|
|
1431
|
+
expect(
|
|
1432
|
+
conversation.persistedAtEachEmit[turn2DeltaIdx],
|
|
1433
|
+
).toBeGreaterThanOrEqual(2);
|
|
1509
1434
|
},
|
|
1510
1435
|
);
|
|
1511
1436
|
|
|
@@ -1530,46 +1455,22 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
1530
1455
|
content: [{ type: "tool_result", tool_use_id: "tu-1", content: "ok" }],
|
|
1531
1456
|
};
|
|
1532
1457
|
|
|
1533
|
-
const
|
|
1534
|
-
const baseline: Message[] = [
|
|
1535
|
-
{ role: "user", content: [{ type: "text", text: "hi" }] },
|
|
1536
|
-
];
|
|
1537
|
-
const history: Message[] = [...baseline];
|
|
1538
|
-
let processing = false;
|
|
1539
|
-
const wakeProducedOutputCalls: string[] = [];
|
|
1540
|
-
|
|
1541
|
-
const target: WakeTarget = {
|
|
1458
|
+
const conversation = makeWakeConversation({
|
|
1542
1459
|
conversationId: "conv-card",
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
},
|
|
1556
|
-
},
|
|
1557
|
-
getMessages: () => history,
|
|
1558
|
-
pushMessage: (msg) => {
|
|
1559
|
-
history.push(msg);
|
|
1560
|
-
},
|
|
1561
|
-
emitAgentEvent: () => {},
|
|
1562
|
-
isProcessing: () => processing,
|
|
1563
|
-
markProcessing: (on) => {
|
|
1564
|
-
processing = on;
|
|
1565
|
-
},
|
|
1566
|
-
persistTailMessage: async (msg) => {
|
|
1567
|
-
persistedTailCalls.push(msg);
|
|
1568
|
-
},
|
|
1569
|
-
onWakeProducedOutput: (_source, _hint, surfaceId) => {
|
|
1570
|
-
wakeProducedOutputCalls.push(surfaceId);
|
|
1460
|
+
baseline: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
|
1461
|
+
runImpl: async (_input, _onEvent, runOptions) => {
|
|
1462
|
+
const runHistory: Message[] = [..._input];
|
|
1463
|
+
runHistory.push(firstAssistant);
|
|
1464
|
+
runHistory.push(toolResult);
|
|
1465
|
+
await runOptions!.onCheckpoint!({
|
|
1466
|
+
turnIndex: 0,
|
|
1467
|
+
toolCount: 1,
|
|
1468
|
+
hasToolUse: true,
|
|
1469
|
+
history: runHistory,
|
|
1470
|
+
});
|
|
1471
|
+
return runResult(runHistory);
|
|
1571
1472
|
},
|
|
1572
|
-
};
|
|
1473
|
+
});
|
|
1573
1474
|
|
|
1574
1475
|
await wakeAgentForOpportunity(
|
|
1575
1476
|
{
|
|
@@ -1577,14 +1478,14 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
1577
1478
|
hint: "do the thing",
|
|
1578
1479
|
source: "memory_v2_consolidation",
|
|
1579
1480
|
},
|
|
1580
|
-
{ resolveTarget: async () =>
|
|
1481
|
+
{ resolveTarget: async () => conversation },
|
|
1581
1482
|
);
|
|
1582
1483
|
|
|
1583
1484
|
// ui_surface fired exactly once (idempotent goLive), and the
|
|
1584
1485
|
// surfaceId matches the block prepended into the first
|
|
1585
1486
|
// assistant message.
|
|
1586
|
-
expect(
|
|
1587
|
-
const persistedFirst = persistedTailCalls[0];
|
|
1487
|
+
expect(conversation.surfaceBroadcasts).toHaveLength(1);
|
|
1488
|
+
const persistedFirst = conversation.persistedTailCalls[0];
|
|
1588
1489
|
expect(persistedFirst).toBeDefined();
|
|
1589
1490
|
const blocks = Array.isArray(persistedFirst!.content)
|
|
1590
1491
|
? persistedFirst!.content
|
|
@@ -1593,7 +1494,9 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
1593
1494
|
(b: { type?: string }) => b.type === "ui_surface",
|
|
1594
1495
|
) as { surfaceId?: string } | undefined;
|
|
1595
1496
|
expect(uiBlock).toBeDefined();
|
|
1596
|
-
expect(uiBlock!.surfaceId).toBe(
|
|
1497
|
+
expect(uiBlock!.surfaceId).toBe(
|
|
1498
|
+
conversation.surfaceBroadcasts[0]!.surfaceId,
|
|
1499
|
+
);
|
|
1597
1500
|
},
|
|
1598
1501
|
);
|
|
1599
1502
|
|
|
@@ -1611,7 +1514,7 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
1611
1514
|
rawRequest: { request: "no-op wake" },
|
|
1612
1515
|
rawResponse: { response: "no output" },
|
|
1613
1516
|
};
|
|
1614
|
-
const
|
|
1517
|
+
const conversation = makeWakeConversation({
|
|
1615
1518
|
baseline: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
|
1616
1519
|
scriptedEvents: [usageEvent],
|
|
1617
1520
|
// Empty assistant text → silent no-op.
|
|
@@ -1623,17 +1526,17 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
1623
1526
|
|
|
1624
1527
|
const result = await wakeAgentForOpportunity(
|
|
1625
1528
|
{
|
|
1626
|
-
conversationId:
|
|
1529
|
+
conversationId: conversation.conversationId,
|
|
1627
1530
|
hint: "consider doing nothing",
|
|
1628
1531
|
source: "unit-test",
|
|
1629
1532
|
},
|
|
1630
|
-
{ resolveTarget: async () =>
|
|
1533
|
+
{ resolveTarget: async () => conversation },
|
|
1631
1534
|
);
|
|
1632
1535
|
|
|
1633
1536
|
expect(result).toEqual({ invoked: true, producedToolCalls: false });
|
|
1634
1537
|
// Nothing emitted, nothing persisted to the conversation.
|
|
1635
|
-
expect(
|
|
1636
|
-
expect(
|
|
1538
|
+
expect(conversation.emittedEvents).toHaveLength(0);
|
|
1539
|
+
expect(conversation.persistedTailCalls).toHaveLength(0);
|
|
1637
1540
|
// Critical: the LLM request log must NOT be inserted with messageId=NULL,
|
|
1638
1541
|
// otherwise the next user turn's backfillMessageIdOnLogs sweep would
|
|
1639
1542
|
// misattach this row to an unrelated future assistant reply.
|
|
@@ -1652,7 +1555,7 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
1652
1555
|
rawRequest: { request: "produced wake" },
|
|
1653
1556
|
rawResponse: { response: "real reply" },
|
|
1654
1557
|
};
|
|
1655
|
-
const
|
|
1558
|
+
const conversation = makeWakeConversation({
|
|
1656
1559
|
baseline: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
|
1657
1560
|
scriptedEvents: [usageEvent],
|
|
1658
1561
|
scriptedAssistant: {
|
|
@@ -1663,17 +1566,17 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
1663
1566
|
|
|
1664
1567
|
const result = await wakeAgentForOpportunity(
|
|
1665
1568
|
{
|
|
1666
|
-
conversationId:
|
|
1569
|
+
conversationId: conversation.conversationId,
|
|
1667
1570
|
hint: "do reply",
|
|
1668
1571
|
source: "unit-test",
|
|
1669
1572
|
},
|
|
1670
|
-
{ resolveTarget: async () =>
|
|
1573
|
+
{ resolveTarget: async () => conversation },
|
|
1671
1574
|
);
|
|
1672
1575
|
|
|
1673
1576
|
expect(result).toEqual({ invoked: true, producedToolCalls: false });
|
|
1674
1577
|
expect(recordRequestLogCalls).toHaveLength(1);
|
|
1675
1578
|
expect(recordRequestLogCalls[0]).toMatchObject({
|
|
1676
|
-
conversationId:
|
|
1579
|
+
conversationId: conversation.conversationId,
|
|
1677
1580
|
provider: "test-provider",
|
|
1678
1581
|
messageId: undefined,
|
|
1679
1582
|
});
|
|
@@ -1696,7 +1599,7 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
1696
1599
|
rawRequest: circular,
|
|
1697
1600
|
rawResponse: { response: "real reply" },
|
|
1698
1601
|
};
|
|
1699
|
-
const
|
|
1602
|
+
const conversation = makeWakeConversation({
|
|
1700
1603
|
baseline: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
|
1701
1604
|
scriptedEvents: [usageEvent],
|
|
1702
1605
|
scriptedAssistant: {
|
|
@@ -1707,16 +1610,16 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
1707
1610
|
|
|
1708
1611
|
const result = await wakeAgentForOpportunity(
|
|
1709
1612
|
{
|
|
1710
|
-
conversationId:
|
|
1613
|
+
conversationId: conversation.conversationId,
|
|
1711
1614
|
hint: "do reply",
|
|
1712
1615
|
source: "unit-test",
|
|
1713
1616
|
},
|
|
1714
|
-
{ resolveTarget: async () =>
|
|
1617
|
+
{ resolveTarget: async () => conversation },
|
|
1715
1618
|
);
|
|
1716
1619
|
|
|
1717
1620
|
expect(result).toEqual({ invoked: true, producedToolCalls: false });
|
|
1718
1621
|
// Wake still produced output even though logging failed.
|
|
1719
|
-
expect(
|
|
1622
|
+
expect(conversation.persistedTailCalls).toHaveLength(1);
|
|
1720
1623
|
// No log row was inserted because JSON.stringify threw.
|
|
1721
1624
|
expect(recordRequestLogCalls).toHaveLength(0);
|
|
1722
1625
|
});
|
|
@@ -1731,22 +1634,13 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
1731
1634
|
// every fork sent `messages: []`. Threading trustContext through
|
|
1732
1635
|
// ensures `setTrustContext` + `ensureActorScopedHistory` run during
|
|
1733
1636
|
// hydration.
|
|
1734
|
-
const makeDefaultResolverTarget = (
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1637
|
+
const makeDefaultResolverTarget = (
|
|
1638
|
+
conversationId: string,
|
|
1639
|
+
): WakeConversation =>
|
|
1640
|
+
makeWakeConversation({
|
|
1738
1641
|
conversationId,
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
pushMessage: () => {},
|
|
1742
|
-
emitAgentEvent: () => {},
|
|
1743
|
-
isProcessing: () => processing,
|
|
1744
|
-
markProcessing: (on) => {
|
|
1745
|
-
processing = on;
|
|
1746
|
-
},
|
|
1747
|
-
persistTailMessage: async () => {},
|
|
1748
|
-
};
|
|
1749
|
-
};
|
|
1642
|
+
runImpl: async (input) => runResult(input),
|
|
1643
|
+
});
|
|
1750
1644
|
|
|
1751
1645
|
test("default resolver threads WakeOptions.trustContext into getOrCreateConversation", async () => {
|
|
1752
1646
|
mockResolverTarget = makeDefaultResolverTarget("conv-thread-trust");
|
|
@@ -1787,11 +1681,7 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
1787
1681
|
});
|
|
1788
1682
|
|
|
1789
1683
|
describe("suppressWakeSurface option", () => {
|
|
1790
|
-
function
|
|
1791
|
-
target: WakeTarget;
|
|
1792
|
-
persistedTailCalls: Message[];
|
|
1793
|
-
wakeProducedOutputCalls: string[];
|
|
1794
|
-
} {
|
|
1684
|
+
function makeCheckpointConversation(): WakeConversation {
|
|
1795
1685
|
const firstAssistant: Message = {
|
|
1796
1686
|
role: "assistant",
|
|
1797
1687
|
content: [
|
|
@@ -1802,55 +1692,30 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
1802
1692
|
role: "user",
|
|
1803
1693
|
content: [{ type: "tool_result", tool_use_id: "tu-1", content: "ok" }],
|
|
1804
1694
|
};
|
|
1805
|
-
const persistedTailCalls: Message[] = [];
|
|
1806
|
-
const baseline: Message[] = [
|
|
1807
|
-
{ role: "user", content: [{ type: "text", text: "hi" }] },
|
|
1808
|
-
];
|
|
1809
|
-
const history: Message[] = [...baseline];
|
|
1810
|
-
let processing = false;
|
|
1811
|
-
const wakeProducedOutputCalls: string[] = [];
|
|
1812
1695
|
|
|
1813
|
-
|
|
1696
|
+
return makeWakeConversation({
|
|
1814
1697
|
conversationId: "conv-suppress-surface",
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
},
|
|
1828
|
-
},
|
|
1829
|
-
getMessages: () => history,
|
|
1830
|
-
pushMessage: (msg) => {
|
|
1831
|
-
history.push(msg);
|
|
1832
|
-
},
|
|
1833
|
-
emitAgentEvent: () => {},
|
|
1834
|
-
isProcessing: () => processing,
|
|
1835
|
-
markProcessing: (on) => {
|
|
1836
|
-
processing = on;
|
|
1837
|
-
},
|
|
1838
|
-
persistTailMessage: async (msg) => {
|
|
1839
|
-
persistedTailCalls.push(msg);
|
|
1840
|
-
},
|
|
1841
|
-
onWakeProducedOutput: (_source, _hint, surfaceId) => {
|
|
1842
|
-
wakeProducedOutputCalls.push(surfaceId);
|
|
1698
|
+
baseline: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
|
1699
|
+
runImpl: async (_input, _onEvent, runOptions) => {
|
|
1700
|
+
const runHistory: Message[] = [..._input];
|
|
1701
|
+
runHistory.push(firstAssistant);
|
|
1702
|
+
runHistory.push(toolResult);
|
|
1703
|
+
await runOptions!.onCheckpoint!({
|
|
1704
|
+
turnIndex: 0,
|
|
1705
|
+
toolCount: 1,
|
|
1706
|
+
hasToolUse: true,
|
|
1707
|
+
history: runHistory,
|
|
1708
|
+
});
|
|
1709
|
+
return runResult(runHistory);
|
|
1843
1710
|
},
|
|
1844
|
-
};
|
|
1845
|
-
return { target, persistedTailCalls, wakeProducedOutputCalls };
|
|
1711
|
+
});
|
|
1846
1712
|
}
|
|
1847
1713
|
|
|
1848
1714
|
test(
|
|
1849
1715
|
"default (suppressWakeSurface omitted) still injects the ui_surface " +
|
|
1850
1716
|
"card and calls onWakeProducedOutput",
|
|
1851
1717
|
async () => {
|
|
1852
|
-
const
|
|
1853
|
-
makeCheckpointTarget();
|
|
1718
|
+
const conversation = makeCheckpointConversation();
|
|
1854
1719
|
|
|
1855
1720
|
await wakeAgentForOpportunity(
|
|
1856
1721
|
{
|
|
@@ -1858,12 +1723,12 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
1858
1723
|
hint: "do the thing",
|
|
1859
1724
|
source: "memory_v2_consolidation",
|
|
1860
1725
|
},
|
|
1861
|
-
{ resolveTarget: async () =>
|
|
1726
|
+
{ resolveTarget: async () => conversation },
|
|
1862
1727
|
);
|
|
1863
1728
|
|
|
1864
1729
|
// Existing behavior: card injected, broadcast fired exactly once.
|
|
1865
|
-
expect(
|
|
1866
|
-
const persistedFirst = persistedTailCalls[0];
|
|
1730
|
+
expect(conversation.surfaceBroadcasts).toHaveLength(1);
|
|
1731
|
+
const persistedFirst = conversation.persistedTailCalls[0];
|
|
1867
1732
|
expect(persistedFirst).toBeDefined();
|
|
1868
1733
|
const blocks = Array.isArray(persistedFirst!.content)
|
|
1869
1734
|
? persistedFirst!.content
|
|
@@ -1879,8 +1744,7 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
1879
1744
|
"suppressWakeSurface: true produces output but skips the ui_surface " +
|
|
1880
1745
|
"card injection and the onWakeProducedOutput broadcast",
|
|
1881
1746
|
async () => {
|
|
1882
|
-
const
|
|
1883
|
-
makeCheckpointTarget();
|
|
1747
|
+
const conversation = makeCheckpointConversation();
|
|
1884
1748
|
|
|
1885
1749
|
await wakeAgentForOpportunity(
|
|
1886
1750
|
{
|
|
@@ -1889,11 +1753,11 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
1889
1753
|
source: "memory_v2_consolidation",
|
|
1890
1754
|
suppressWakeSurface: true,
|
|
1891
1755
|
},
|
|
1892
|
-
{ resolveTarget: async () =>
|
|
1756
|
+
{ resolveTarget: async () => conversation },
|
|
1893
1757
|
);
|
|
1894
1758
|
|
|
1895
1759
|
// Tail still persisted (wake produced real output).
|
|
1896
|
-
const persistedFirst = persistedTailCalls[0];
|
|
1760
|
+
const persistedFirst = conversation.persistedTailCalls[0];
|
|
1897
1761
|
expect(persistedFirst).toBeDefined();
|
|
1898
1762
|
// First assistant tail message should NOT have a ui_surface block
|
|
1899
1763
|
// prepended at the front.
|
|
@@ -1907,7 +1771,7 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
1907
1771
|
);
|
|
1908
1772
|
expect(uiBlock).toBeUndefined();
|
|
1909
1773
|
// Live broadcast was suppressed.
|
|
1910
|
-
expect(
|
|
1774
|
+
expect(conversation.surfaceBroadcasts).toHaveLength(0);
|
|
1911
1775
|
},
|
|
1912
1776
|
);
|
|
1913
1777
|
});
|