@vellumai/assistant 0.8.8 → 0.8.9-staging.1
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/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 +236 -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 +185 -140
- 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-control-flow.test.ts +1 -1
- package/src/__tests__/app-dir-path-guard.test.ts +1 -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__/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 +59 -103
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +94 -189
- package/src/__tests__/conversation-agent-loop.test.ts +249 -490
- 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-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 +18 -16
- 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 +73 -6
- 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__/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 +101 -0
- package/src/acp/__tests__/prepare-agent-env.test.ts +137 -0
- package/src/acp/__tests__/session-manager-persistence.test.ts +95 -28
- package/src/acp/__tests__/session-manager-resume.test.ts +736 -0
- package/src/acp/agent-process.ts +61 -1
- package/src/acp/auto-install.test.ts +196 -0
- package/src/acp/auto-install.ts +177 -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 +83 -29
- package/src/acp/resolve-agent.test.ts +320 -7
- package/src/acp/resolve-agent.ts +182 -18
- package/src/acp/resume-hint.ts +25 -0
- package/src/acp/session-manager.ts +495 -73
- package/src/acp/types.ts +8 -0
- package/src/agent/compaction-circuit.ts +60 -102
- package/src/agent/loop.ts +390 -240
- 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 +83 -31
- 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 +29 -0
- 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/document-editor/SKILL.md +28 -23
- package/src/config/bundled-skills/document-editor/TOOLS.json +1 -1
- package/src/config/bundled-tool-registry.ts +2 -0
- package/src/config/feature-flag-registry.json +14 -5
- package/src/config/schemas/heartbeat.ts +9 -0
- 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 +167 -1005
- 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 +383 -56
- 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 -0
- 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/plugins/defaults/compaction/manager-store.ts +44 -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 +103 -38
- package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +167 -56
- 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/proactive-artifact/aux-message-injector.ts +1 -1
- package/src/proactive-artifact/job.test.ts +1 -1
- 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 +283 -55
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +1 -1
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +398 -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 +336 -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 +7 -1
- package/src/tools/acp/spawn.test.ts +158 -55
- package/src/tools/acp/spawn.ts +47 -72
- package/src/tools/acp/steer.test.ts +105 -8
- package/src/tools/acp/steer.ts +48 -17
- package/src/tools/apps/executors.ts +13 -8
- 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/{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
|
@@ -312,16 +312,14 @@ let mockInjectionBlocks: {
|
|
|
312
312
|
pkbSystemReminder?: string;
|
|
313
313
|
unifiedTurnContext?: string;
|
|
314
314
|
} = {};
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
)
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
}),
|
|
324
|
-
);
|
|
315
|
+
const defaultApplyRuntimeInjectionsImpl = async (
|
|
316
|
+
msgs: Message[],
|
|
317
|
+
_options?: unknown,
|
|
318
|
+
) => ({
|
|
319
|
+
messages: msgs,
|
|
320
|
+
blocks: { ...mockInjectionBlocks },
|
|
321
|
+
});
|
|
322
|
+
const applyRuntimeInjectionsMock = mock(defaultApplyRuntimeInjectionsImpl);
|
|
325
323
|
let mockSlackChronologicalContext: {
|
|
326
324
|
renderedMessages: Array<{
|
|
327
325
|
message: Message;
|
|
@@ -358,7 +356,6 @@ const getSlackCompactionWatermarkForPrefixMock = mock(
|
|
|
358
356
|
);
|
|
359
357
|
mock.module("../daemon/conversation-runtime-assembly.js", () => ({
|
|
360
358
|
applyRuntimeInjections: applyRuntimeInjectionsMock,
|
|
361
|
-
buildUnifiedTurnContextBlock: buildUnifiedTurnContextBlockMock,
|
|
362
359
|
stripInjectionsForCompaction: (msgs: Message[]) => msgs,
|
|
363
360
|
isSlackChannelConversation: () => false,
|
|
364
361
|
getSlackCompactionWatermarkForPrefix:
|
|
@@ -537,8 +534,8 @@ mock.module("../proactive-artifact/index.js", () => ({
|
|
|
537
534
|
// ── Imports (after mocks) ────────────────────────────────────────────
|
|
538
535
|
|
|
539
536
|
import { AgentLoop } from "../agent/loop.js";
|
|
537
|
+
import type { Conversation } from "../daemon/conversation.js";
|
|
540
538
|
import {
|
|
541
|
-
type AgentLoopConversationContext,
|
|
542
539
|
applyCompactionResult,
|
|
543
540
|
runAgentLoopImpl,
|
|
544
541
|
} from "../daemon/conversation-agent-loop.js";
|
|
@@ -552,13 +549,13 @@ import {
|
|
|
552
549
|
// ── Test helpers ─────────────────────────────────────────────────────
|
|
553
550
|
|
|
554
551
|
function makeCtx(
|
|
555
|
-
overrides?: Partial<
|
|
552
|
+
overrides?: Partial<Conversation> & {
|
|
556
553
|
providerResponses?: ScriptedResponse[];
|
|
557
554
|
loopProvider?: Provider;
|
|
558
555
|
loopTools?: ToolDefinition[];
|
|
559
556
|
toolExecutor?: LoopToolExecutor;
|
|
560
557
|
},
|
|
561
|
-
):
|
|
558
|
+
): Conversation {
|
|
562
559
|
const {
|
|
563
560
|
providerResponses,
|
|
564
561
|
loopProvider,
|
|
@@ -589,7 +586,9 @@ function makeCtx(
|
|
|
589
586
|
providerResponses ?? [textResponse("response")],
|
|
590
587
|
loopProviderName,
|
|
591
588
|
).provider;
|
|
592
|
-
const agentLoop = new AgentLoop(
|
|
589
|
+
const agentLoop = new AgentLoop({
|
|
590
|
+
provider: provider,
|
|
591
|
+
systemPrompt: "system prompt",
|
|
593
592
|
conversationId,
|
|
594
593
|
tools: loopTools ?? [],
|
|
595
594
|
toolExecutor,
|
|
@@ -616,15 +615,24 @@ function makeCtx(
|
|
|
616
615
|
usage: { inputTokens: 0, outputTokens: 0 },
|
|
617
616
|
stopReason: "end_turn",
|
|
618
617
|
}),
|
|
619
|
-
} as unknown as
|
|
618
|
+
} as unknown as Conversation["provider"],
|
|
620
619
|
systemPrompt: "system prompt",
|
|
621
620
|
|
|
622
621
|
contextWindowManager: {
|
|
622
|
+
updateConfig: () => {},
|
|
623
623
|
shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
|
|
624
624
|
maybeCompact: async () => ({ compacted: false }),
|
|
625
|
-
} as unknown as
|
|
626
|
-
|
|
627
|
-
|
|
625
|
+
} as unknown as Conversation["contextWindowManager"],
|
|
626
|
+
conversationType: mockConversationRow?.conversationType ?? undefined,
|
|
627
|
+
source: mockConversationRow?.source ?? undefined,
|
|
628
|
+
contextSummary: mockConversationRow?.contextSummary ?? null,
|
|
629
|
+
contextCompactedMessageCount:
|
|
630
|
+
mockConversationRow?.contextCompactedMessageCount ?? 0,
|
|
631
|
+
contextCompactedAt: mockConversationRow?.contextCompactedAt ?? null,
|
|
632
|
+
slackContextCompactionWatermarkTs:
|
|
633
|
+
mockConversationRow?.slackContextCompactionWatermarkTs ?? null,
|
|
634
|
+
lastNotifiedInferenceProfile:
|
|
635
|
+
mockConversationRow?.lastNotifiedInferenceProfile ?? null,
|
|
628
636
|
|
|
629
637
|
memoryPolicy: { scopeId: "default", includeDefaultFallback: true },
|
|
630
638
|
|
|
@@ -636,8 +644,6 @@ function makeCtx(
|
|
|
636
644
|
currentTurnSurfaces: [],
|
|
637
645
|
|
|
638
646
|
workingDir: "/tmp",
|
|
639
|
-
workspaceTopLevelContext: null,
|
|
640
|
-
workspaceTopLevelDirty: false,
|
|
641
647
|
channelCapabilities: undefined,
|
|
642
648
|
commandIntent: undefined,
|
|
643
649
|
trustContext: undefined,
|
|
@@ -647,15 +653,15 @@ function makeCtx(
|
|
|
647
653
|
preactivatedSkillIds: undefined,
|
|
648
654
|
skillProjectionState: new Map(),
|
|
649
655
|
skillProjectionCache:
|
|
650
|
-
new Map() as unknown as
|
|
656
|
+
new Map() as unknown as Conversation["skillProjectionCache"],
|
|
651
657
|
|
|
652
658
|
traceEmitter: {
|
|
653
659
|
emit: () => {},
|
|
654
|
-
} as unknown as
|
|
660
|
+
} as unknown as Conversation["traceEmitter"],
|
|
655
661
|
profiler: {
|
|
656
662
|
startRequest: () => {},
|
|
657
663
|
emitSummary: () => {},
|
|
658
|
-
} as unknown as
|
|
664
|
+
} as unknown as Conversation["profiler"],
|
|
659
665
|
usageStats: {
|
|
660
666
|
totalInputTokens: 0,
|
|
661
667
|
totalOutputTokens: 0,
|
|
@@ -668,13 +674,12 @@ function makeCtx(
|
|
|
668
674
|
lastAttachmentWarnings: [],
|
|
669
675
|
|
|
670
676
|
hasNoClient: false,
|
|
671
|
-
prompter: {} as unknown as
|
|
672
|
-
queue: {} as unknown as
|
|
677
|
+
prompter: {} as unknown as Conversation["prompter"],
|
|
678
|
+
queue: {} as unknown as Conversation["queue"],
|
|
673
679
|
|
|
674
680
|
getWorkspaceGitService: () => ({ ensureInitialized: async () => {} }),
|
|
675
681
|
commitTurnChanges: async () => {},
|
|
676
682
|
|
|
677
|
-
refreshWorkspaceTopLevelContextIfNeeded: () => {},
|
|
678
683
|
markWorkspaceTopLevelDirty: () => {},
|
|
679
684
|
emitActivityState: () => {},
|
|
680
685
|
getQueueDepth: () => 0,
|
|
@@ -701,10 +706,32 @@ function makeCtx(
|
|
|
701
706
|
}),
|
|
702
707
|
retrackCachedNodes: () => {},
|
|
703
708
|
recordPkbQueryVectors: () => {},
|
|
704
|
-
} as unknown as
|
|
709
|
+
} as unknown as Conversation["graphMemory"],
|
|
705
710
|
|
|
706
711
|
...ctxOverrides,
|
|
707
|
-
} as
|
|
712
|
+
} as unknown as Conversation;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
type CompactionResult = Parameters<typeof applyCompactionResult>[1];
|
|
716
|
+
|
|
717
|
+
function makeCompactionResult(
|
|
718
|
+
overrides?: Partial<CompactionResult>,
|
|
719
|
+
): CompactionResult {
|
|
720
|
+
return {
|
|
721
|
+
messages: [{ role: "user", content: [{ type: "text", text: "summary" }] }],
|
|
722
|
+
compactedPersistedMessages: 4,
|
|
723
|
+
previousEstimatedInputTokens: 12000,
|
|
724
|
+
estimatedInputTokens: 3000,
|
|
725
|
+
maxInputTokens: 100000,
|
|
726
|
+
thresholdTokens: 80000,
|
|
727
|
+
compactedMessages: 4,
|
|
728
|
+
summaryCalls: 1,
|
|
729
|
+
summaryInputTokens: 100,
|
|
730
|
+
summaryOutputTokens: 20,
|
|
731
|
+
summaryModel: "mock-model",
|
|
732
|
+
summaryText: "summary",
|
|
733
|
+
...overrides,
|
|
734
|
+
};
|
|
708
735
|
}
|
|
709
736
|
|
|
710
737
|
// ── Tests ────────────────────────────────────────────────────────────
|
|
@@ -744,7 +771,9 @@ beforeEach(() => {
|
|
|
744
771
|
setConversationHistoryStrippedAtMock.mockClear();
|
|
745
772
|
setConversationHistoryStrippedAtMock.mockImplementation(() => {});
|
|
746
773
|
applyRuntimeInjectionsMock.mockClear();
|
|
747
|
-
|
|
774
|
+
applyRuntimeInjectionsMock.mockImplementation(
|
|
775
|
+
defaultApplyRuntimeInjectionsImpl,
|
|
776
|
+
);
|
|
748
777
|
resolveTurnTimezoneContextMock.mockClear();
|
|
749
778
|
formatTurnTimestampMock.mockClear();
|
|
750
779
|
mockSlackChronologicalContext = null;
|
|
@@ -757,11 +786,10 @@ beforeEach(() => {
|
|
|
757
786
|
projectAssistantMessageMock.mockClear();
|
|
758
787
|
publishSyncInvalidationMock.mockClear();
|
|
759
788
|
mockMessageById = null;
|
|
760
|
-
//
|
|
761
|
-
//
|
|
762
|
-
//
|
|
763
|
-
//
|
|
764
|
-
// hitting the bare terminals.
|
|
789
|
+
// The compaction pipeline runs through the plugin registry; reset and
|
|
790
|
+
// re-register every default so it dispatches to middleware backed by the
|
|
791
|
+
// mocked collaborators these tests install (`syncMessageToDisk`, etc.)
|
|
792
|
+
// instead of hitting the bare terminal.
|
|
765
793
|
resetPluginRegistryAndRegisterDefaults();
|
|
766
794
|
});
|
|
767
795
|
|
|
@@ -785,7 +813,7 @@ describe("session-agent-loop", () => {
|
|
|
785
813
|
});
|
|
786
814
|
});
|
|
787
815
|
|
|
788
|
-
test("
|
|
816
|
+
test("freezes the client timezone snapshot on the conversation, not the options bag", async () => {
|
|
789
817
|
mockUiConfig = {
|
|
790
818
|
userTimezone: "US/Eastern",
|
|
791
819
|
detectedTimezone: "US/Central",
|
|
@@ -802,17 +830,25 @@ describe("session-agent-loop", () => {
|
|
|
802
830
|
|
|
803
831
|
await runAgentLoopImpl(ctx, "hello", "msg-1", () => {});
|
|
804
832
|
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
expect(turnContextOptions).toMatchObject({
|
|
812
|
-
configuredUserTimezone: "America/New_York",
|
|
833
|
+
// The turn-start client timezone and the long-absence gap are frozen on
|
|
834
|
+
// the conversation as `currentTurnTemporalSnapshot`; the live
|
|
835
|
+
// `clientTimezone` would otherwise be clobbered mid-turn. `current_time`
|
|
836
|
+
// is not frozen — it is computed fresh at each injection inside
|
|
837
|
+
// `applyRuntimeInjections`.
|
|
838
|
+
expect(ctx.currentTurnTemporalSnapshot).toEqual({
|
|
813
839
|
clientTimezone: "America/Los_Angeles",
|
|
814
|
-
|
|
815
|
-
});
|
|
840
|
+
timeSinceLastMessage: null,
|
|
841
|
+
});
|
|
842
|
+
// Neither the timezones nor the long-absence gap are threaded through the
|
|
843
|
+
// options bag — `applyRuntimeInjections` sources the client timezone and
|
|
844
|
+
// gap from the snapshot and the config timezones from config
|
|
845
|
+
// (`ui.userTimezone`, `ui.detectedTimezone`).
|
|
846
|
+
const injectionOptions = applyRuntimeInjectionsMock.mock.calls[0]?.[1];
|
|
847
|
+
expect(injectionOptions).not.toHaveProperty("timestamp");
|
|
848
|
+
expect(injectionOptions).not.toHaveProperty("clientTimezone");
|
|
849
|
+
expect(injectionOptions).not.toHaveProperty("configuredUserTimezone");
|
|
850
|
+
expect(injectionOptions).not.toHaveProperty("detectedTimezone");
|
|
851
|
+
expect(injectionOptions).not.toHaveProperty("timeSinceLastMessage");
|
|
816
852
|
});
|
|
817
853
|
});
|
|
818
854
|
|
|
@@ -880,7 +916,23 @@ describe("session-agent-loop", () => {
|
|
|
880
916
|
});
|
|
881
917
|
|
|
882
918
|
describe("disk pressure injection context", () => {
|
|
883
|
-
|
|
919
|
+
// The loop sets `ctx.diskPressureCleanupModeActive` for the duration of the
|
|
920
|
+
// turn (the disk-pressure-warning injector reads it via the per-conversation
|
|
921
|
+
// registry) and resets it in the turn-end cleanup path. Snapshot the flag at
|
|
922
|
+
// each `applyRuntimeInjections` call so assertions observe its value while
|
|
923
|
+
// injection runs, not the post-turn reset.
|
|
924
|
+
function captureCleanupFlagDuringInjection(ctx: {
|
|
925
|
+
diskPressureCleanupModeActive?: boolean;
|
|
926
|
+
}): () => Array<boolean | undefined> {
|
|
927
|
+
const observed: Array<boolean | undefined> = [];
|
|
928
|
+
applyRuntimeInjectionsMock.mockImplementation(async (msgs: Message[]) => {
|
|
929
|
+
observed.push(ctx.diskPressureCleanupModeActive);
|
|
930
|
+
return { messages: msgs, blocks: { ...mockInjectionBlocks } };
|
|
931
|
+
});
|
|
932
|
+
return () => observed;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
test("sets the cleanup-mode flag on the conversation for cleanup-mode turns", async () => {
|
|
884
936
|
mockDiskPressureDecision = {
|
|
885
937
|
action: "allow-cleanup-mode",
|
|
886
938
|
reason: "guardian",
|
|
@@ -901,8 +953,9 @@ describe("session-agent-loop", () => {
|
|
|
901
953
|
trustContext: {
|
|
902
954
|
sourceChannel: "telegram",
|
|
903
955
|
trustClass: "guardian",
|
|
904
|
-
} as
|
|
956
|
+
} as Conversation["trustContext"],
|
|
905
957
|
});
|
|
958
|
+
const cleanupFlagDuringInjection = captureCleanupFlagDuringInjection(ctx);
|
|
906
959
|
|
|
907
960
|
await runAgentLoopImpl(ctx, "free up space", "msg-1", () => {});
|
|
908
961
|
|
|
@@ -921,21 +974,16 @@ describe("session-agent-loop", () => {
|
|
|
921
974
|
},
|
|
922
975
|
}),
|
|
923
976
|
);
|
|
924
|
-
|
|
925
|
-
.calls[0]![1] as {
|
|
926
|
-
diskPressureContext?: { cleanupModeActive: boolean } | null;
|
|
927
|
-
};
|
|
928
|
-
expect(firstInjectionOptions.diskPressureContext).toEqual({
|
|
929
|
-
cleanupModeActive: true,
|
|
930
|
-
});
|
|
977
|
+
expect(cleanupFlagDuringInjection()).toEqual([true]);
|
|
931
978
|
});
|
|
932
979
|
|
|
933
|
-
test("
|
|
980
|
+
test("sets the cleanup-mode flag on the conversation for local-owner turns", async () => {
|
|
934
981
|
mockDiskPressureDecision = {
|
|
935
982
|
action: "allow-cleanup-mode",
|
|
936
983
|
reason: "local-owner",
|
|
937
984
|
};
|
|
938
985
|
const ctx = makeCtx();
|
|
986
|
+
const cleanupFlagDuringInjection = captureCleanupFlagDuringInjection(ctx);
|
|
939
987
|
|
|
940
988
|
await runAgentLoopImpl(ctx, "free up space", "msg-1", () => {});
|
|
941
989
|
|
|
@@ -947,16 +995,10 @@ describe("session-agent-loop", () => {
|
|
|
947
995
|
trustContext: null,
|
|
948
996
|
}),
|
|
949
997
|
);
|
|
950
|
-
|
|
951
|
-
.calls[0]![1] as {
|
|
952
|
-
diskPressureContext?: { cleanupModeActive: boolean } | null;
|
|
953
|
-
};
|
|
954
|
-
expect(firstInjectionOptions.diskPressureContext).toEqual({
|
|
955
|
-
cleanupModeActive: true,
|
|
956
|
-
});
|
|
998
|
+
expect(cleanupFlagDuringInjection()).toEqual([true]);
|
|
957
999
|
});
|
|
958
1000
|
|
|
959
|
-
test("keeps cleanup
|
|
1001
|
+
test("keeps the cleanup-mode flag set across overflow recovery reinjection", async () => {
|
|
960
1002
|
mockDiskPressureDecision = {
|
|
961
1003
|
action: "allow-cleanup-mode",
|
|
962
1004
|
reason: "guardian",
|
|
@@ -976,20 +1018,16 @@ describe("session-agent-loop", () => {
|
|
|
976
1018
|
trustContext: {
|
|
977
1019
|
sourceChannel: "telegram",
|
|
978
1020
|
trustClass: "guardian",
|
|
979
|
-
} as
|
|
1021
|
+
} as Conversation["trustContext"],
|
|
980
1022
|
});
|
|
1023
|
+
const cleanupFlagDuringInjection = captureCleanupFlagDuringInjection(ctx);
|
|
981
1024
|
|
|
982
1025
|
await runAgentLoopImpl(ctx, "free up space", "msg-1", () => {});
|
|
983
1026
|
|
|
984
1027
|
expect(applyRuntimeInjectionsMock.mock.calls.length).toBeGreaterThan(1);
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
};
|
|
989
|
-
expect(options.diskPressureContext).toEqual({
|
|
990
|
-
cleanupModeActive: true,
|
|
991
|
-
});
|
|
992
|
-
}
|
|
1028
|
+
const flags = cleanupFlagDuringInjection();
|
|
1029
|
+
expect(flags.length).toBeGreaterThan(1);
|
|
1030
|
+
expect(flags.every((flag) => flag === true)).toBe(true);
|
|
993
1031
|
});
|
|
994
1032
|
|
|
995
1033
|
test("blocks policy-denied turns before runtime injection or model execution", async () => {
|
|
@@ -1008,7 +1046,7 @@ describe("session-agent-loop", () => {
|
|
|
1008
1046
|
emit: (...args: unknown[]) => {
|
|
1009
1047
|
traceEvents.push(args);
|
|
1010
1048
|
},
|
|
1011
|
-
} as unknown as
|
|
1049
|
+
} as unknown as Conversation["traceEmitter"],
|
|
1012
1050
|
});
|
|
1013
1051
|
const runSpy = spyOn(ctx.agentLoop, "run");
|
|
1014
1052
|
|
|
@@ -1169,7 +1207,7 @@ describe("session-agent-loop", () => {
|
|
|
1169
1207
|
usage: { inputTokens: 0, outputTokens: 0 },
|
|
1170
1208
|
stopReason: "end_turn",
|
|
1171
1209
|
}),
|
|
1172
|
-
} as unknown as
|
|
1210
|
+
} as unknown as Conversation["provider"],
|
|
1173
1211
|
});
|
|
1174
1212
|
|
|
1175
1213
|
await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
|
|
@@ -1218,7 +1256,7 @@ describe("session-agent-loop", () => {
|
|
|
1218
1256
|
usage: { inputTokens: 0, outputTokens: 0 },
|
|
1219
1257
|
stopReason: "end_turn",
|
|
1220
1258
|
}),
|
|
1221
|
-
} as unknown as
|
|
1259
|
+
} as unknown as Conversation["provider"],
|
|
1222
1260
|
});
|
|
1223
1261
|
|
|
1224
1262
|
await runAgentLoopImpl(ctx, "hello", "msg-1", () => {});
|
|
@@ -1284,7 +1322,7 @@ describe("session-agent-loop", () => {
|
|
|
1284
1322
|
usage: { inputTokens: 0, outputTokens: 0 },
|
|
1285
1323
|
stopReason: "end_turn",
|
|
1286
1324
|
}),
|
|
1287
|
-
} as unknown as
|
|
1325
|
+
} as unknown as Conversation["provider"],
|
|
1288
1326
|
});
|
|
1289
1327
|
|
|
1290
1328
|
await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
|
|
@@ -1335,7 +1373,7 @@ describe("session-agent-loop", () => {
|
|
|
1335
1373
|
usage: { inputTokens: 0, outputTokens: 0 },
|
|
1336
1374
|
stopReason: "end_turn",
|
|
1337
1375
|
}),
|
|
1338
|
-
} as unknown as
|
|
1376
|
+
} as unknown as Conversation["provider"],
|
|
1339
1377
|
traceEmitter: {
|
|
1340
1378
|
emit: (
|
|
1341
1379
|
event: string,
|
|
@@ -1346,7 +1384,7 @@ describe("session-agent-loop", () => {
|
|
|
1346
1384
|
traceEvents.push({ label, attrs: payload.attributes ?? {} });
|
|
1347
1385
|
}
|
|
1348
1386
|
},
|
|
1349
|
-
} as unknown as
|
|
1387
|
+
} as unknown as Conversation["traceEmitter"],
|
|
1350
1388
|
});
|
|
1351
1389
|
|
|
1352
1390
|
await runAgentLoopImpl(ctx, "hello", "msg-1", () => {});
|
|
@@ -1393,7 +1431,7 @@ describe("session-agent-loop", () => {
|
|
|
1393
1431
|
usage: { inputTokens: 0, outputTokens: 0 },
|
|
1394
1432
|
stopReason: "end_turn",
|
|
1395
1433
|
}),
|
|
1396
|
-
} as unknown as
|
|
1434
|
+
} as unknown as Conversation["provider"],
|
|
1397
1435
|
traceEmitter: {
|
|
1398
1436
|
emit: (
|
|
1399
1437
|
event: string,
|
|
@@ -1404,7 +1442,7 @@ describe("session-agent-loop", () => {
|
|
|
1404
1442
|
traceEvents.push({ label, attrs: payload.attributes ?? {} });
|
|
1405
1443
|
}
|
|
1406
1444
|
},
|
|
1407
|
-
} as unknown as
|
|
1445
|
+
} as unknown as Conversation["traceEmitter"],
|
|
1408
1446
|
});
|
|
1409
1447
|
|
|
1410
1448
|
await runAgentLoopImpl(ctx, "hello", "msg-1", () => {});
|
|
@@ -1460,7 +1498,7 @@ describe("session-agent-loop", () => {
|
|
|
1460
1498
|
usage: { inputTokens: 0, outputTokens: 0 },
|
|
1461
1499
|
stopReason: "end_turn",
|
|
1462
1500
|
}),
|
|
1463
|
-
} as unknown as
|
|
1501
|
+
} as unknown as Conversation["provider"],
|
|
1464
1502
|
});
|
|
1465
1503
|
|
|
1466
1504
|
await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
|
|
@@ -1521,9 +1559,21 @@ describe("session-agent-loop", () => {
|
|
|
1521
1559
|
},
|
|
1522
1560
|
});
|
|
1523
1561
|
|
|
1524
|
-
//
|
|
1525
|
-
// the turn
|
|
1526
|
-
|
|
1562
|
+
// The provider rejects the first call as too large; the convergence
|
|
1563
|
+
// reducer compacts and the rerun completes the turn, forwarding the
|
|
1564
|
+
// compaction's cache-aware usage to recordUsage.
|
|
1565
|
+
const { provider } = createMockProvider([
|
|
1566
|
+
new Error("context_length_exceeded"),
|
|
1567
|
+
textResponse("recovered"),
|
|
1568
|
+
]);
|
|
1569
|
+
const ctx = makeCtx({
|
|
1570
|
+
loopProvider: provider,
|
|
1571
|
+
contextWindowManager: {
|
|
1572
|
+
updateConfig: () => {},
|
|
1573
|
+
shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
|
|
1574
|
+
maybeCompact: async () => ({ compacted: false }),
|
|
1575
|
+
} as unknown as Conversation["contextWindowManager"],
|
|
1576
|
+
});
|
|
1527
1577
|
await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
|
|
1528
1578
|
|
|
1529
1579
|
const compactorCall = recordUsageMock.mock.calls.find(
|
|
@@ -1605,9 +1655,10 @@ describe("session-agent-loop", () => {
|
|
|
1605
1655
|
const ctx = makeCtx({
|
|
1606
1656
|
loopProvider: provider,
|
|
1607
1657
|
contextWindowManager: {
|
|
1658
|
+
updateConfig: () => {},
|
|
1608
1659
|
shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
|
|
1609
1660
|
maybeCompact: async () => ({ compacted: false }),
|
|
1610
|
-
} as unknown as
|
|
1661
|
+
} as unknown as Conversation["contextWindowManager"],
|
|
1611
1662
|
});
|
|
1612
1663
|
|
|
1613
1664
|
await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
|
|
@@ -1626,6 +1677,7 @@ describe("session-agent-loop", () => {
|
|
|
1626
1677
|
const ctx = makeCtx({
|
|
1627
1678
|
providerResponses: [new Error("context_length_exceeded")],
|
|
1628
1679
|
contextWindowManager: {
|
|
1680
|
+
updateConfig: () => {},
|
|
1629
1681
|
shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
|
|
1630
1682
|
// Compaction succeeds but context is still too large
|
|
1631
1683
|
maybeCompact: async () => ({
|
|
@@ -1645,7 +1697,7 @@ describe("session-agent-loop", () => {
|
|
|
1645
1697
|
summaryOutputTokens: 200,
|
|
1646
1698
|
summaryModel: "mock-model",
|
|
1647
1699
|
}),
|
|
1648
|
-
} as unknown as
|
|
1700
|
+
} as unknown as Conversation["contextWindowManager"],
|
|
1649
1701
|
});
|
|
1650
1702
|
|
|
1651
1703
|
await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
|
|
@@ -1685,9 +1737,10 @@ describe("session-agent-loop", () => {
|
|
|
1685
1737
|
const ctx = makeCtx({
|
|
1686
1738
|
loopProvider: provider,
|
|
1687
1739
|
contextWindowManager: {
|
|
1740
|
+
updateConfig: () => {},
|
|
1688
1741
|
shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
|
|
1689
1742
|
maybeCompact: async () => ({ compacted: false }),
|
|
1690
|
-
} as unknown as
|
|
1743
|
+
} as unknown as Conversation["contextWindowManager"],
|
|
1691
1744
|
});
|
|
1692
1745
|
|
|
1693
1746
|
await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
|
|
@@ -1734,6 +1787,7 @@ describe("session-agent-loop", () => {
|
|
|
1734
1787
|
],
|
|
1735
1788
|
hasNoClient: true,
|
|
1736
1789
|
contextWindowManager: {
|
|
1790
|
+
updateConfig: () => {},
|
|
1737
1791
|
shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
|
|
1738
1792
|
maybeCompact: async () => ({
|
|
1739
1793
|
compacted: true,
|
|
@@ -1752,7 +1806,7 @@ describe("session-agent-loop", () => {
|
|
|
1752
1806
|
summaryOutputTokens: 100,
|
|
1753
1807
|
summaryModel: "mock-model",
|
|
1754
1808
|
}),
|
|
1755
|
-
} as unknown as
|
|
1809
|
+
} as unknown as Conversation["contextWindowManager"],
|
|
1756
1810
|
});
|
|
1757
1811
|
|
|
1758
1812
|
await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
|
|
@@ -1834,6 +1888,7 @@ describe("session-agent-loop", () => {
|
|
|
1834
1888
|
toolExecutor: async () => ({ content: "data", isError: false }),
|
|
1835
1889
|
hasNoClient: true,
|
|
1836
1890
|
contextWindowManager: {
|
|
1891
|
+
updateConfig: () => {},
|
|
1837
1892
|
shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
|
|
1838
1893
|
maybeCompact: async () => ({
|
|
1839
1894
|
compacted: true,
|
|
@@ -1852,7 +1907,7 @@ describe("session-agent-loop", () => {
|
|
|
1852
1907
|
summaryOutputTokens: 100,
|
|
1853
1908
|
summaryModel: "mock-model",
|
|
1854
1909
|
}),
|
|
1855
|
-
} as unknown as
|
|
1910
|
+
} as unknown as Conversation["contextWindowManager"],
|
|
1856
1911
|
});
|
|
1857
1912
|
|
|
1858
1913
|
await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
|
|
@@ -1919,9 +1974,10 @@ describe("session-agent-loop", () => {
|
|
|
1919
1974
|
const ctx = makeCtx({
|
|
1920
1975
|
providerResponses: [new Error("context_length_exceeded")],
|
|
1921
1976
|
contextWindowManager: {
|
|
1977
|
+
updateConfig: () => {},
|
|
1922
1978
|
shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
|
|
1923
1979
|
maybeCompact: async () => ({ compacted: false }),
|
|
1924
|
-
} as unknown as
|
|
1980
|
+
} as unknown as Conversation["contextWindowManager"],
|
|
1925
1981
|
});
|
|
1926
1982
|
|
|
1927
1983
|
await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
|
|
@@ -1929,48 +1985,6 @@ describe("session-agent-loop", () => {
|
|
|
1929
1985
|
// maxAttempts is 3 — reducer should be called at most 3 times
|
|
1930
1986
|
expect(reducerCalls).toBeLessThanOrEqual(3);
|
|
1931
1987
|
});
|
|
1932
|
-
|
|
1933
|
-
test("preflight budget evaluation invokes reducer before provider call", async () => {
|
|
1934
|
-
const events: ServerMessage[] = [];
|
|
1935
|
-
let reducerCalls = 0;
|
|
1936
|
-
|
|
1937
|
-
// Set token estimate above budget (100000 * 0.95 = 95000)
|
|
1938
|
-
mockEstimateTokens = 96000;
|
|
1939
|
-
|
|
1940
|
-
mockReducerStepFn = (msgs: Message[]) => {
|
|
1941
|
-
reducerCalls++;
|
|
1942
|
-
return {
|
|
1943
|
-
messages: msgs,
|
|
1944
|
-
tier: "forced_compaction",
|
|
1945
|
-
state: {
|
|
1946
|
-
appliedTiers: ["forced_compaction"],
|
|
1947
|
-
injectionMode: "full",
|
|
1948
|
-
exhausted: true,
|
|
1949
|
-
},
|
|
1950
|
-
estimatedTokens: 50000,
|
|
1951
|
-
};
|
|
1952
|
-
};
|
|
1953
|
-
|
|
1954
|
-
// After the preflight reducer brings the estimate under budget, the loop
|
|
1955
|
-
// completes the turn in a single provider call.
|
|
1956
|
-
const { provider, calls } = createMockProvider([textResponse("ok")]);
|
|
1957
|
-
const ctx = makeCtx({
|
|
1958
|
-
loopProvider: provider,
|
|
1959
|
-
contextWindowManager: {
|
|
1960
|
-
shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
|
|
1961
|
-
maybeCompact: async () => ({ compacted: false }),
|
|
1962
|
-
} as unknown as AgentLoopConversationContext["contextWindowManager"],
|
|
1963
|
-
});
|
|
1964
|
-
|
|
1965
|
-
await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
|
|
1966
|
-
|
|
1967
|
-
// Reducer should have been called during preflight
|
|
1968
|
-
expect(reducerCalls).toBeGreaterThanOrEqual(1);
|
|
1969
|
-
// Agent loop should still succeed in a single provider call
|
|
1970
|
-
expect(calls.length).toBe(1);
|
|
1971
|
-
const complete = events.find((e) => e.type === "message_complete");
|
|
1972
|
-
expect(complete).toBeDefined();
|
|
1973
|
-
});
|
|
1974
1988
|
});
|
|
1975
1989
|
|
|
1976
1990
|
describe("provider ordering error retry", () => {
|
|
@@ -2024,7 +2038,7 @@ describe("session-agent-loop", () => {
|
|
|
2024
2038
|
],
|
|
2025
2039
|
toolExecutor: async () => ({ content: "file content", isError: false }),
|
|
2026
2040
|
canHandoffAtCheckpoint: () => true,
|
|
2027
|
-
} as unknown as Partial<
|
|
2041
|
+
} as unknown as Partial<Conversation>);
|
|
2028
2042
|
|
|
2029
2043
|
await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
|
|
2030
2044
|
|
|
@@ -2055,7 +2069,7 @@ describe("session-agent-loop", () => {
|
|
|
2055
2069
|
],
|
|
2056
2070
|
toolExecutor: async () => ({ content: "content", isError: false }),
|
|
2057
2071
|
canHandoffAtCheckpoint: () => false,
|
|
2058
|
-
} as unknown as Partial<
|
|
2072
|
+
} as unknown as Partial<Conversation>);
|
|
2059
2073
|
|
|
2060
2074
|
await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
|
|
2061
2075
|
|
|
@@ -2206,7 +2220,7 @@ describe("session-agent-loop", () => {
|
|
|
2206
2220
|
drainQueue: (reason: string) => {
|
|
2207
2221
|
drainReason = reason;
|
|
2208
2222
|
},
|
|
2209
|
-
} as unknown as Partial<
|
|
2223
|
+
} as unknown as Partial<Conversation>);
|
|
2210
2224
|
|
|
2211
2225
|
// WHEN the orchestrator runs the turn to completion
|
|
2212
2226
|
await runAgentLoopImpl(ctx, "hi", "msg-1", () => {});
|
|
@@ -2648,9 +2662,10 @@ describe("session-agent-loop", () => {
|
|
|
2648
2662
|
textResponse("retry succeeded"),
|
|
2649
2663
|
],
|
|
2650
2664
|
contextWindowManager: {
|
|
2665
|
+
updateConfig: () => {},
|
|
2651
2666
|
shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
|
|
2652
2667
|
maybeCompact: async () => ({ compacted: false }),
|
|
2653
|
-
} as unknown as
|
|
2668
|
+
} as unknown as Conversation["contextWindowManager"],
|
|
2654
2669
|
});
|
|
2655
2670
|
await runAgentLoopImpl(ctx, "hi", "msg-1", () => {});
|
|
2656
2671
|
|
|
@@ -3112,7 +3127,7 @@ describe("session-agent-loop", () => {
|
|
|
3112
3127
|
});
|
|
3113
3128
|
|
|
3114
3129
|
describe("Slack compaction watermarks", () => {
|
|
3115
|
-
test("
|
|
3130
|
+
test("first-call compaction runs in the loop and drops the Slack watermark", async () => {
|
|
3116
3131
|
const renderedSlackMessages: Message[] = [
|
|
3117
3132
|
{
|
|
3118
3133
|
role: "user",
|
|
@@ -3140,9 +3155,12 @@ describe("session-agent-loop", () => {
|
|
|
3140
3155
|
})),
|
|
3141
3156
|
compactableStartIndex: 0,
|
|
3142
3157
|
};
|
|
3143
|
-
const shouldCompactInputs: Message[][] = [];
|
|
3144
3158
|
const maybeCompactInputs: Message[][] = [];
|
|
3145
3159
|
|
|
3160
|
+
// Sits above the loop's first-call gate threshold (~80.75k), so the
|
|
3161
|
+
// loop's first-call gate owns the turn-start compaction.
|
|
3162
|
+
mockEstimateTokens = 90_000;
|
|
3163
|
+
|
|
3146
3164
|
const ctx = makeCtx({
|
|
3147
3165
|
channelCapabilities: {
|
|
3148
3166
|
channel: "slack",
|
|
@@ -3154,18 +3172,19 @@ describe("session-agent-loop", () => {
|
|
|
3154
3172
|
trustContext: {
|
|
3155
3173
|
sourceChannel: "slack",
|
|
3156
3174
|
trustClass: "guardian",
|
|
3157
|
-
} as
|
|
3175
|
+
} as Conversation["trustContext"],
|
|
3158
3176
|
getTurnChannelContext: () => ({
|
|
3159
3177
|
userMessageChannel: "slack" as const,
|
|
3160
3178
|
assistantMessageChannel: "slack" as const,
|
|
3161
3179
|
}),
|
|
3162
3180
|
contextWindowManager: {
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
return { needed: true, estimatedTokens: 95_000 };
|
|
3166
|
-
},
|
|
3181
|
+
updateConfig: () => {},
|
|
3182
|
+
shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
|
|
3167
3183
|
maybeCompact: async (messages: Message[]) => {
|
|
3168
3184
|
maybeCompactInputs.push(messages);
|
|
3185
|
+
// Drop back under budget so the post-compaction provider call
|
|
3186
|
+
// proceeds.
|
|
3187
|
+
mockEstimateTokens = 1000;
|
|
3169
3188
|
return {
|
|
3170
3189
|
compacted: true,
|
|
3171
3190
|
messages: [
|
|
@@ -3173,10 +3192,10 @@ describe("session-agent-loop", () => {
|
|
|
3173
3192
|
role: "user",
|
|
3174
3193
|
content: [{ type: "text", text: "summary" }],
|
|
3175
3194
|
},
|
|
3176
|
-
messages[
|
|
3195
|
+
messages[messages.length - 1]!,
|
|
3177
3196
|
],
|
|
3178
3197
|
compactedPersistedMessages: 2,
|
|
3179
|
-
previousEstimatedInputTokens:
|
|
3198
|
+
previousEstimatedInputTokens: 90_000,
|
|
3180
3199
|
estimatedInputTokens: 5_000,
|
|
3181
3200
|
maxInputTokens: 100_000,
|
|
3182
3201
|
thresholdTokens: 80_000,
|
|
@@ -3189,292 +3208,24 @@ describe("session-agent-loop", () => {
|
|
|
3189
3208
|
summaryFailed: false,
|
|
3190
3209
|
};
|
|
3191
3210
|
},
|
|
3192
|
-
} as unknown as
|
|
3193
|
-
});
|
|
3194
|
-
|
|
3195
|
-
await runAgentLoopImpl(ctx, "next reply", "user-msg-start", () => {});
|
|
3196
|
-
|
|
3197
|
-
expect(shouldCompactInputs[0]).toBe(renderedSlackMessages);
|
|
3198
|
-
expect(maybeCompactInputs[0]).toBe(renderedSlackMessages);
|
|
3199
|
-
expect(getSlackCompactionWatermarkForPrefixMock).toHaveBeenCalledWith(
|
|
3200
|
-
mockSlackChronologicalContext,
|
|
3201
|
-
2,
|
|
3202
|
-
);
|
|
3203
|
-
expect(updateConversationSlackContextWatermarkMock).toHaveBeenCalledWith(
|
|
3204
|
-
"test-conv",
|
|
3205
|
-
"1700000020.000000",
|
|
3206
|
-
expect.any(Number),
|
|
3207
|
-
);
|
|
3208
|
-
const firstInjectionOptions = applyRuntimeInjectionsMock.mock
|
|
3209
|
-
.calls[0]![1] as {
|
|
3210
|
-
slackChronologicalMessages?: Message[] | null;
|
|
3211
|
-
};
|
|
3212
|
-
expect(firstInjectionOptions.slackChronologicalMessages).toBeNull();
|
|
3213
|
-
});
|
|
3214
|
-
|
|
3215
|
-
test("overflow reducer Slack compaction persists watermark from rendered context", async () => {
|
|
3216
|
-
const renderedSlackMessages: Message[] = [
|
|
3217
|
-
{
|
|
3218
|
-
role: "user",
|
|
3219
|
-
content: [{ type: "text", text: "first rendered Slack row" }],
|
|
3220
|
-
},
|
|
3221
|
-
{
|
|
3222
|
-
role: "user",
|
|
3223
|
-
content: [{ type: "text", text: "second rendered Slack row" }],
|
|
3224
|
-
},
|
|
3225
|
-
{
|
|
3226
|
-
role: "user",
|
|
3227
|
-
content: [{ type: "text", text: "retained Slack row" }],
|
|
3228
|
-
},
|
|
3229
|
-
];
|
|
3230
|
-
mockSlackChronologicalContext = {
|
|
3231
|
-
messages: renderedSlackMessages,
|
|
3232
|
-
renderedMessages: renderedSlackMessages.map((message, index) => ({
|
|
3233
|
-
message,
|
|
3234
|
-
sourceChannelTs: [
|
|
3235
|
-
"1700000010.000000",
|
|
3236
|
-
"1700000020.000000",
|
|
3237
|
-
"1700000030.000000",
|
|
3238
|
-
][index]!,
|
|
3239
|
-
tagLineProvenance: "none",
|
|
3240
|
-
})),
|
|
3241
|
-
compactableStartIndex: 0,
|
|
3242
|
-
};
|
|
3243
|
-
mockEstimateTokens = 120_000;
|
|
3244
|
-
mockReducerStepFn = (_msgs: Message[]) => ({
|
|
3245
|
-
messages: [
|
|
3246
|
-
{
|
|
3247
|
-
role: "user",
|
|
3248
|
-
content: [{ type: "text", text: "summary" }],
|
|
3249
|
-
},
|
|
3250
|
-
renderedSlackMessages[2]!,
|
|
3251
|
-
],
|
|
3252
|
-
tier: "forced_compaction",
|
|
3253
|
-
state: {
|
|
3254
|
-
appliedTiers: ["forced_compaction"],
|
|
3255
|
-
injectionMode: "full",
|
|
3256
|
-
exhausted: false,
|
|
3257
|
-
},
|
|
3258
|
-
estimatedTokens: 5_000,
|
|
3259
|
-
compactionResult: {
|
|
3260
|
-
compacted: true,
|
|
3261
|
-
messages: [
|
|
3262
|
-
{
|
|
3263
|
-
role: "user",
|
|
3264
|
-
content: [{ type: "text", text: "summary" }],
|
|
3265
|
-
},
|
|
3266
|
-
renderedSlackMessages[2]!,
|
|
3267
|
-
],
|
|
3268
|
-
compactedPersistedMessages: 2,
|
|
3269
|
-
previousEstimatedInputTokens: 120_000,
|
|
3270
|
-
estimatedInputTokens: 5_000,
|
|
3271
|
-
maxInputTokens: 100_000,
|
|
3272
|
-
thresholdTokens: 80_000,
|
|
3273
|
-
compactedMessages: 2,
|
|
3274
|
-
summaryCalls: 1,
|
|
3275
|
-
summaryInputTokens: 100,
|
|
3276
|
-
summaryOutputTokens: 20,
|
|
3277
|
-
summaryModel: "mock-model",
|
|
3278
|
-
summaryText: "summary",
|
|
3279
|
-
summaryFailed: false,
|
|
3280
|
-
},
|
|
3281
|
-
});
|
|
3282
|
-
|
|
3283
|
-
const ctx = makeCtx({
|
|
3284
|
-
channelCapabilities: {
|
|
3285
|
-
channel: "slack",
|
|
3286
|
-
dashboardCapable: false,
|
|
3287
|
-
supportsDynamicUi: false,
|
|
3288
|
-
supportsVoiceInput: false,
|
|
3289
|
-
chatType: "channel",
|
|
3290
|
-
},
|
|
3291
|
-
trustContext: {
|
|
3292
|
-
sourceChannel: "slack",
|
|
3293
|
-
trustClass: "guardian",
|
|
3294
|
-
} as AgentLoopConversationContext["trustContext"],
|
|
3295
|
-
getTurnChannelContext: () => ({
|
|
3296
|
-
userMessageChannel: "slack" as const,
|
|
3297
|
-
assistantMessageChannel: "slack" as const,
|
|
3298
|
-
}),
|
|
3299
|
-
contextWindowManager: {
|
|
3300
|
-
shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
|
|
3301
|
-
maybeCompact: async () => ({ compacted: false }),
|
|
3302
|
-
} as unknown as AgentLoopConversationContext["contextWindowManager"],
|
|
3211
|
+
} as unknown as Conversation["contextWindowManager"],
|
|
3303
3212
|
});
|
|
3304
3213
|
|
|
3305
|
-
await runAgentLoopImpl(
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
);
|
|
3311
|
-
expect(updateConversationSlackContextWatermarkMock).toHaveBeenCalledWith(
|
|
3312
|
-
"test-conv",
|
|
3313
|
-
"1700000020.000000",
|
|
3314
|
-
expect.any(Number),
|
|
3214
|
+
await runAgentLoopImpl(
|
|
3215
|
+
ctx,
|
|
3216
|
+
"next reply",
|
|
3217
|
+
"user-msg-first-call",
|
|
3218
|
+
() => {},
|
|
3315
3219
|
);
|
|
3316
|
-
const reinjectionOptions = applyRuntimeInjectionsMock.mock.calls.find(
|
|
3317
|
-
(call) => {
|
|
3318
|
-
const options = call[1] as {
|
|
3319
|
-
slackChronologicalMessages?: Message[] | null;
|
|
3320
|
-
};
|
|
3321
|
-
return options.slackChronologicalMessages === null;
|
|
3322
|
-
},
|
|
3323
|
-
)?.[1] as { slackChronologicalMessages?: Message[] | null } | undefined;
|
|
3324
|
-
expect(reinjectionOptions?.slackChronologicalMessages).toBeNull();
|
|
3325
|
-
});
|
|
3326
|
-
|
|
3327
|
-
test("same-turn Slack compaction updates watermark from projected provenance", async () => {
|
|
3328
|
-
const renderedSlackMessages: Message[] = [
|
|
3329
|
-
{
|
|
3330
|
-
role: "user",
|
|
3331
|
-
content: [{ type: "text", text: "first rendered Slack row" }],
|
|
3332
|
-
},
|
|
3333
|
-
{
|
|
3334
|
-
role: "user",
|
|
3335
|
-
content: [{ type: "text", text: "second rendered Slack row" }],
|
|
3336
|
-
},
|
|
3337
|
-
{
|
|
3338
|
-
role: "user",
|
|
3339
|
-
content: [{ type: "text", text: "third rendered Slack row" }],
|
|
3340
|
-
},
|
|
3341
|
-
{
|
|
3342
|
-
role: "user",
|
|
3343
|
-
content: [{ type: "text", text: "retained Slack row" }],
|
|
3344
|
-
},
|
|
3345
|
-
];
|
|
3346
|
-
mockSlackChronologicalContext = {
|
|
3347
|
-
messages: renderedSlackMessages,
|
|
3348
|
-
renderedMessages: renderedSlackMessages.map((message, index) => ({
|
|
3349
|
-
message,
|
|
3350
|
-
sourceChannelTs: [
|
|
3351
|
-
"1700000010.000000",
|
|
3352
|
-
"1700000020.000000",
|
|
3353
|
-
"1700000030.000000",
|
|
3354
|
-
"1700000040.000000",
|
|
3355
|
-
][index]!,
|
|
3356
|
-
tagLineProvenance: "none",
|
|
3357
|
-
})),
|
|
3358
|
-
compactableStartIndex: 0,
|
|
3359
|
-
};
|
|
3360
|
-
|
|
3361
|
-
const firstSummaryMessage: Message = {
|
|
3362
|
-
role: "user",
|
|
3363
|
-
content: [{ type: "text", text: "first summary" }],
|
|
3364
|
-
};
|
|
3365
|
-
const firstCompactedMessages: Message[] = [
|
|
3366
|
-
firstSummaryMessage,
|
|
3367
|
-
renderedSlackMessages[2]!,
|
|
3368
|
-
renderedSlackMessages[3]!,
|
|
3369
|
-
];
|
|
3370
|
-
const secondSummaryMessage: Message = {
|
|
3371
|
-
role: "user",
|
|
3372
|
-
content: [{ type: "text", text: "second summary" }],
|
|
3373
|
-
};
|
|
3374
|
-
const secondCompactedMessages: Message[] = [
|
|
3375
|
-
secondSummaryMessage,
|
|
3376
|
-
renderedSlackMessages[3]!,
|
|
3377
|
-
];
|
|
3378
|
-
const reducerInputs: Message[][] = [];
|
|
3379
|
-
mockEstimateTokens = 120_000;
|
|
3380
|
-
mockReducerStepFn = (msgs: Message[]) => {
|
|
3381
|
-
reducerInputs.push(msgs);
|
|
3382
|
-
mockEstimateTokens = 1000;
|
|
3383
|
-
return {
|
|
3384
|
-
messages: secondCompactedMessages,
|
|
3385
|
-
tier: "forced_compaction",
|
|
3386
|
-
state: {
|
|
3387
|
-
appliedTiers: ["forced_compaction"],
|
|
3388
|
-
injectionMode: "full",
|
|
3389
|
-
exhausted: false,
|
|
3390
|
-
},
|
|
3391
|
-
estimatedTokens: 5_000,
|
|
3392
|
-
compactionResult: {
|
|
3393
|
-
compacted: true,
|
|
3394
|
-
messages: secondCompactedMessages,
|
|
3395
|
-
compactedPersistedMessages: 1,
|
|
3396
|
-
previousEstimatedInputTokens: 120_000,
|
|
3397
|
-
estimatedInputTokens: 5_000,
|
|
3398
|
-
maxInputTokens: 100_000,
|
|
3399
|
-
thresholdTokens: 80_000,
|
|
3400
|
-
compactedMessages: 1,
|
|
3401
|
-
summaryCalls: 1,
|
|
3402
|
-
summaryInputTokens: 100,
|
|
3403
|
-
summaryOutputTokens: 20,
|
|
3404
|
-
summaryModel: "mock-model",
|
|
3405
|
-
summaryText: "second summary",
|
|
3406
|
-
summaryFailed: false,
|
|
3407
|
-
},
|
|
3408
|
-
};
|
|
3409
|
-
};
|
|
3410
3220
|
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
trustContext: {
|
|
3420
|
-
sourceChannel: "slack",
|
|
3421
|
-
trustClass: "guardian",
|
|
3422
|
-
} as AgentLoopConversationContext["trustContext"],
|
|
3423
|
-
getTurnChannelContext: () => ({
|
|
3424
|
-
userMessageChannel: "slack" as const,
|
|
3425
|
-
assistantMessageChannel: "slack" as const,
|
|
3426
|
-
}),
|
|
3427
|
-
contextWindowManager: {
|
|
3428
|
-
shouldCompact: () => ({ needed: true, estimatedTokens: 120_000 }),
|
|
3429
|
-
maybeCompact: async (messages: Message[]) => {
|
|
3430
|
-
expect(messages).toBe(renderedSlackMessages);
|
|
3431
|
-
return {
|
|
3432
|
-
compacted: true,
|
|
3433
|
-
messages: firstCompactedMessages,
|
|
3434
|
-
compactedPersistedMessages: 2,
|
|
3435
|
-
previousEstimatedInputTokens: 120_000,
|
|
3436
|
-
estimatedInputTokens: 60_000,
|
|
3437
|
-
maxInputTokens: 100_000,
|
|
3438
|
-
thresholdTokens: 80_000,
|
|
3439
|
-
compactedMessages: 2,
|
|
3440
|
-
summaryCalls: 1,
|
|
3441
|
-
summaryInputTokens: 100,
|
|
3442
|
-
summaryOutputTokens: 20,
|
|
3443
|
-
summaryModel: "mock-model",
|
|
3444
|
-
summaryText: "first summary",
|
|
3445
|
-
summaryFailed: false,
|
|
3446
|
-
};
|
|
3447
|
-
},
|
|
3448
|
-
} as unknown as AgentLoopConversationContext["contextWindowManager"],
|
|
3449
|
-
});
|
|
3450
|
-
|
|
3451
|
-
await runAgentLoopImpl(ctx, "next reply", "user-msg-repeat", () => {});
|
|
3452
|
-
|
|
3453
|
-
expect(reducerInputs[0]).toBe(firstCompactedMessages);
|
|
3454
|
-
expect(getSlackCompactionWatermarkForPrefixMock.mock.calls).toEqual([
|
|
3455
|
-
[mockSlackChronologicalContext, 2],
|
|
3456
|
-
[
|
|
3457
|
-
{
|
|
3458
|
-
renderedMessages: [
|
|
3459
|
-
{
|
|
3460
|
-
message: firstSummaryMessage,
|
|
3461
|
-
sourceChannelTs: null,
|
|
3462
|
-
tagLineProvenance: "none",
|
|
3463
|
-
},
|
|
3464
|
-
mockSlackChronologicalContext!.renderedMessages[2],
|
|
3465
|
-
mockSlackChronologicalContext!.renderedMessages[3],
|
|
3466
|
-
],
|
|
3467
|
-
messages: firstCompactedMessages,
|
|
3468
|
-
compactableStartIndex: 1,
|
|
3469
|
-
},
|
|
3470
|
-
1,
|
|
3471
|
-
],
|
|
3472
|
-
]);
|
|
3473
|
-
expect(updateConversationSlackContextWatermarkMock.mock.calls).toEqual([
|
|
3474
|
-
["test-conv", "1700000020.000000", expect.any(Number)],
|
|
3475
|
-
["test-conv", "1700000030.000000", expect.any(Number)],
|
|
3476
|
-
]);
|
|
3477
|
-
expect(loadSlackChronologicalContextMock).toHaveBeenCalledTimes(1);
|
|
3221
|
+
// The loop compacts its own stripped working history, never the loaded
|
|
3222
|
+
// Slack transcript array, so provenance cannot be projected and the
|
|
3223
|
+
// watermark is left untouched — matching mid-loop compaction.
|
|
3224
|
+
expect(maybeCompactInputs.length).toBeGreaterThan(0);
|
|
3225
|
+
expect(maybeCompactInputs[0]).not.toBe(renderedSlackMessages);
|
|
3226
|
+
expect(
|
|
3227
|
+
updateConversationSlackContextWatermarkMock,
|
|
3228
|
+
).not.toHaveBeenCalled();
|
|
3478
3229
|
});
|
|
3479
3230
|
|
|
3480
3231
|
test("mid-loop Slack compaction does not persist watermark from mismatched loaded context", async () => {
|
|
@@ -3542,32 +3293,16 @@ describe("session-agent-loop", () => {
|
|
|
3542
3293
|
trustContext: {
|
|
3543
3294
|
sourceChannel: "slack",
|
|
3544
3295
|
trustClass: "guardian",
|
|
3545
|
-
} as
|
|
3296
|
+
} as Conversation["trustContext"],
|
|
3546
3297
|
getTurnChannelContext: () => ({
|
|
3547
3298
|
userMessageChannel: "slack" as const,
|
|
3548
3299
|
assistantMessageChannel: "slack" as const,
|
|
3549
3300
|
}),
|
|
3550
3301
|
contextWindowManager: {
|
|
3302
|
+
updateConfig: () => {},
|
|
3551
3303
|
shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
|
|
3552
3304
|
maybeCompact: async (messages: Message[]) => {
|
|
3553
3305
|
maybeCompactInputs.push(messages);
|
|
3554
|
-
if (messages === renderedSlackMessages) {
|
|
3555
|
-
return {
|
|
3556
|
-
compacted: false,
|
|
3557
|
-
messages,
|
|
3558
|
-
compactedPersistedMessages: 0,
|
|
3559
|
-
previousEstimatedInputTokens: 1000,
|
|
3560
|
-
estimatedInputTokens: 1000,
|
|
3561
|
-
maxInputTokens: 100_000,
|
|
3562
|
-
thresholdTokens: 80_000,
|
|
3563
|
-
compactedMessages: 0,
|
|
3564
|
-
summaryCalls: 0,
|
|
3565
|
-
summaryInputTokens: 0,
|
|
3566
|
-
summaryOutputTokens: 0,
|
|
3567
|
-
summaryModel: "",
|
|
3568
|
-
summaryText: "",
|
|
3569
|
-
};
|
|
3570
|
-
}
|
|
3571
3306
|
// The mid-loop gate compacted its in-loop basis; drop the estimate
|
|
3572
3307
|
// back under budget so the post-compaction provider call proceeds.
|
|
3573
3308
|
mockEstimateTokens = 1000;
|
|
@@ -3593,15 +3328,16 @@ describe("session-agent-loop", () => {
|
|
|
3593
3328
|
summaryFailed: false,
|
|
3594
3329
|
};
|
|
3595
3330
|
},
|
|
3596
|
-
} as unknown as
|
|
3331
|
+
} as unknown as Conversation["contextWindowManager"],
|
|
3597
3332
|
});
|
|
3598
3333
|
|
|
3599
3334
|
await runAgentLoopImpl(ctx, "next reply", "user-msg-mid-loop", () => {});
|
|
3600
3335
|
|
|
3601
|
-
expect(maybeCompactInputs[0]).toBe(renderedSlackMessages);
|
|
3602
3336
|
// The mid-loop gate compacts the loop's own in-loop history, never the
|
|
3603
|
-
// loaded Slack rows — the mismatch this test guards against.
|
|
3604
|
-
|
|
3337
|
+
// loaded Slack rows — the mismatch this test guards against. No
|
|
3338
|
+
// start-of-turn compaction runs, so this is the turn's only compaction.
|
|
3339
|
+
expect(maybeCompactInputs).toHaveLength(1);
|
|
3340
|
+
expect(maybeCompactInputs[0]).not.toBe(renderedSlackMessages);
|
|
3605
3341
|
expect(getSlackCompactionWatermarkForPrefixMock).toHaveBeenCalledWith(
|
|
3606
3342
|
null,
|
|
3607
3343
|
2,
|
|
@@ -3611,7 +3347,7 @@ describe("session-agent-loop", () => {
|
|
|
3611
3347
|
).not.toHaveBeenCalled();
|
|
3612
3348
|
});
|
|
3613
3349
|
|
|
3614
|
-
test("next inbound Slack turn
|
|
3350
|
+
test("next inbound Slack turn loads chronological context using the persisted watermark", async () => {
|
|
3615
3351
|
mockConversationRow = {
|
|
3616
3352
|
...mockConversationRow,
|
|
3617
3353
|
contextSummary: "## Summary\n- compacted Slack context",
|
|
@@ -3671,7 +3407,7 @@ describe("session-agent-loop", () => {
|
|
|
3671
3407
|
trustContext: {
|
|
3672
3408
|
sourceChannel: "slack",
|
|
3673
3409
|
trustClass: "guardian",
|
|
3674
|
-
} as
|
|
3410
|
+
} as Conversation["trustContext"],
|
|
3675
3411
|
getTurnChannelContext: () => ({
|
|
3676
3412
|
userMessageChannel: "slack" as const,
|
|
3677
3413
|
assistantMessageChannel: "slack" as const,
|
|
@@ -3690,26 +3426,9 @@ describe("session-agent-loop", () => {
|
|
|
3690
3426
|
trustClass: "guardian",
|
|
3691
3427
|
}),
|
|
3692
3428
|
);
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
};
|
|
3697
|
-
expect(firstInjectionOptions.slackChronologicalMessages).toBe(
|
|
3698
|
-
mockSlackChronologicalContext.messages,
|
|
3699
|
-
);
|
|
3700
|
-
const rendered = firstInjectionOptions
|
|
3701
|
-
.slackChronologicalMessages!.flatMap((message) => message.content)
|
|
3702
|
-
.filter((block): block is { type: "text"; text: string } => {
|
|
3703
|
-
return block.type === "text";
|
|
3704
|
-
})
|
|
3705
|
-
.map((block) => block.text)
|
|
3706
|
-
.join("\n");
|
|
3707
|
-
expect(rendered).toContain("compacted Slack context");
|
|
3708
|
-
expect(rendered).toContain("after watermark reply");
|
|
3709
|
-
expect(rendered).not.toContain("before watermark");
|
|
3710
|
-
});
|
|
3711
|
-
|
|
3712
|
-
test("subsequent Slack turn keeps long-thread compaction summary and filtered tail", async () => {
|
|
3429
|
+
});
|
|
3430
|
+
|
|
3431
|
+
test("subsequent Slack turn loads chronological context using the persisted long-thread watermark", async () => {
|
|
3713
3432
|
mockConversationRow = {
|
|
3714
3433
|
...mockConversationRow,
|
|
3715
3434
|
contextSummary: "## Summary\n- compacted long Slack thread",
|
|
@@ -3779,7 +3498,7 @@ describe("session-agent-loop", () => {
|
|
|
3779
3498
|
trustContext: {
|
|
3780
3499
|
sourceChannel: "slack",
|
|
3781
3500
|
trustClass: "guardian",
|
|
3782
|
-
} as
|
|
3501
|
+
} as Conversation["trustContext"],
|
|
3783
3502
|
getTurnChannelContext: () => ({
|
|
3784
3503
|
userMessageChannel: "slack" as const,
|
|
3785
3504
|
assistantMessageChannel: "slack" as const,
|
|
@@ -3802,21 +3521,6 @@ describe("session-agent-loop", () => {
|
|
|
3802
3521
|
slackContextCompactionWatermarkTs: "1700000080.000000",
|
|
3803
3522
|
}),
|
|
3804
3523
|
);
|
|
3805
|
-
const firstInjectionOptions = applyRuntimeInjectionsMock.mock
|
|
3806
|
-
.calls[0]![1] as {
|
|
3807
|
-
slackChronologicalMessages?: Message[] | null;
|
|
3808
|
-
};
|
|
3809
|
-
const rendered = firstInjectionOptions
|
|
3810
|
-
.slackChronologicalMessages!.flatMap((message) => message.content)
|
|
3811
|
-
.filter((block): block is { type: "text"; text: string } => {
|
|
3812
|
-
return block.type === "text";
|
|
3813
|
-
})
|
|
3814
|
-
.map((block) => block.text)
|
|
3815
|
-
.join("\n");
|
|
3816
|
-
expect(rendered).toContain("compacted long Slack thread");
|
|
3817
|
-
expect(rendered).toContain("reply after compaction");
|
|
3818
|
-
expect(rendered).not.toContain("pre-compaction");
|
|
3819
|
-
expect(rendered).not.toContain("original root");
|
|
3820
3524
|
});
|
|
3821
3525
|
|
|
3822
3526
|
test("applyCompactionResult records Slack timestamp watermark when provided", async () => {
|
|
@@ -3858,6 +3562,59 @@ describe("session-agent-loop", () => {
|
|
|
3858
3562
|
true,
|
|
3859
3563
|
);
|
|
3860
3564
|
});
|
|
3565
|
+
|
|
3566
|
+
test("applyCompactionResult advances the persisted count from the trusted in-context boundary", async () => {
|
|
3567
|
+
// Trusted views slice past the already-compacted prefix, so a further
|
|
3568
|
+
// compaction advances the persisted count from the mirrored DB boundary.
|
|
3569
|
+
|
|
3570
|
+
// GIVEN a trusted (guardian) conversation that has already compacted 5
|
|
3571
|
+
// persisted messages, so its in-context history starts past that prefix
|
|
3572
|
+
const ctx = makeCtx({
|
|
3573
|
+
contextCompactedMessageCount: 5,
|
|
3574
|
+
trustContext: {
|
|
3575
|
+
trustClass: "guardian",
|
|
3576
|
+
} as Conversation["trustContext"],
|
|
3577
|
+
});
|
|
3578
|
+
|
|
3579
|
+
// WHEN a turn compacts 4 more in-context messages
|
|
3580
|
+
await applyCompactionResult(
|
|
3581
|
+
ctx,
|
|
3582
|
+
makeCompactionResult({ compactedPersistedMessages: 4 }),
|
|
3583
|
+
() => {},
|
|
3584
|
+
"req-1",
|
|
3585
|
+
);
|
|
3586
|
+
|
|
3587
|
+
// THEN the persisted count advances from the prior boundary (5 + 4)
|
|
3588
|
+
expect(ctx.contextCompactedMessageCount).toBe(9);
|
|
3589
|
+
});
|
|
3590
|
+
|
|
3591
|
+
test("applyCompactionResult resets the persisted count to the unsliced boundary for untrusted views", async () => {
|
|
3592
|
+
// Untrusted views render history unsliced (boundary 0), so a compaction
|
|
3593
|
+
// must record only the new summary's prefix instead of adding to the raw
|
|
3594
|
+
// mirror — otherwise future loads slice past unsummarized rows.
|
|
3595
|
+
|
|
3596
|
+
// GIVEN an untrusted view of a conversation whose raw DB count mirrors a
|
|
3597
|
+
// 5-message compacted prefix — but untrusted views render that history
|
|
3598
|
+
// unsliced (boundary 0), so the compactor operates on the full list
|
|
3599
|
+
const ctx = makeCtx({
|
|
3600
|
+
contextCompactedMessageCount: 5,
|
|
3601
|
+
trustContext: {
|
|
3602
|
+
trustClass: "unknown",
|
|
3603
|
+
} as Conversation["trustContext"],
|
|
3604
|
+
});
|
|
3605
|
+
|
|
3606
|
+
// WHEN that turn compacts 4 in-context messages
|
|
3607
|
+
await applyCompactionResult(
|
|
3608
|
+
ctx,
|
|
3609
|
+
makeCompactionResult({ compactedPersistedMessages: 4 }),
|
|
3610
|
+
() => {},
|
|
3611
|
+
"req-1",
|
|
3612
|
+
);
|
|
3613
|
+
|
|
3614
|
+
// THEN the persisted count reflects only the new summary's prefix (0 + 4)
|
|
3615
|
+
// rather than double-counting the raw mirror (which would yield 9)
|
|
3616
|
+
expect(ctx.contextCompactedMessageCount).toBe(4);
|
|
3617
|
+
});
|
|
3861
3618
|
});
|
|
3862
3619
|
|
|
3863
3620
|
describe("compaction-strip marker persistence", () => {
|
|
@@ -3894,9 +3651,10 @@ describe("session-agent-loop", () => {
|
|
|
3894
3651
|
],
|
|
3895
3652
|
toolExecutor: async () => ({ content: "ok", isError: false }),
|
|
3896
3653
|
contextWindowManager: {
|
|
3654
|
+
updateConfig: () => {},
|
|
3897
3655
|
shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
|
|
3898
3656
|
maybeCompact: async () => ({ compacted: false }),
|
|
3899
|
-
} as unknown as
|
|
3657
|
+
} as unknown as Conversation["contextWindowManager"],
|
|
3900
3658
|
});
|
|
3901
3659
|
|
|
3902
3660
|
// WHEN the orchestrator runs the turn to completion
|
|
@@ -3943,9 +3701,10 @@ describe("session-agent-loop", () => {
|
|
|
3943
3701
|
],
|
|
3944
3702
|
toolExecutor: async () => ({ content: "ok", isError: false }),
|
|
3945
3703
|
contextWindowManager: {
|
|
3704
|
+
updateConfig: () => {},
|
|
3946
3705
|
shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
|
|
3947
3706
|
maybeCompact: async () => ({ compacted: false }),
|
|
3948
|
-
} as unknown as
|
|
3707
|
+
} as unknown as Conversation["contextWindowManager"],
|
|
3949
3708
|
});
|
|
3950
3709
|
|
|
3951
3710
|
// Must not throw — the strip-site marker write is wrapped in try/catch.
|