@vellumai/assistant 0.8.8 → 0.8.9-dev.202606091853.fbaa2ae
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/docs/activation-funnel-telemetry.md +310 -0
- 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 +263 -121
- package/package.json +2 -2
- package/src/__tests__/activation-early-marking.test.ts +120 -0
- 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 +23 -8
- 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 +6 -5
- 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 +146 -0
- package/src/__tests__/compaction-events.test.ts +7 -7
- package/src/__tests__/compaction.benchmark.test.ts +1 -1
- package/src/__tests__/compactor-web-search-strip.test.ts +213 -0
- package/src/__tests__/context-overflow-reducer.test.ts +265 -125
- package/src/__tests__/context-window-manager-compact-retry.test.ts +121 -15
- package/src/__tests__/conversation-abort-tool-results.test.ts +7 -5
- 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 +158 -234
- package/src/__tests__/conversation-agent-loop.test.ts +342 -623
- package/src/__tests__/conversation-clean-command.test.ts +5 -2
- package/src/__tests__/conversation-confirmation-signals.test.ts +6 -5
- package/src/__tests__/conversation-crud-inference-profile.test.ts +7 -9
- package/src/__tests__/conversation-error.test.ts +15 -1
- package/src/__tests__/conversation-history-web-search.test.ts +6 -1
- package/src/__tests__/conversation-media-retry.test.ts +1 -1
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +44 -4
- package/src/__tests__/conversation-process-callsite.test.ts +15 -15
- package/src/__tests__/conversation-provider-retry-repair.test.ts +66 -65
- package/src/__tests__/conversation-queue.test.ts +10 -10
- package/src/__tests__/conversation-runtime-assembly.test.ts +1059 -231
- package/src/__tests__/conversation-runtime-workspace.test.ts +115 -20
- package/src/__tests__/conversation-slash-queue.test.ts +7 -5
- package/src/__tests__/conversation-slash-unknown.test.ts +7 -5
- package/src/__tests__/conversation-speed-override.test.ts +11 -10
- package/src/__tests__/conversation-starter-routes.test.ts +14 -6
- package/src/__tests__/conversation-surfaces-activation-emit.test.ts +395 -0
- package/src/__tests__/conversation-surfaces-app-control.test.ts +48 -1
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +73 -4
- package/src/__tests__/conversation-undo.test.ts +2 -2
- package/src/__tests__/conversation-workspace-cache-state.test.ts +24 -21
- package/src/__tests__/conversation-workspace-injection.test.ts +69 -7
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +15 -12
- package/src/__tests__/conversations-import-system-filter.test.ts +101 -0
- package/src/__tests__/corrected-target.test.ts +93 -0
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/cu-unified-flow.test.ts +36 -0
- 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 +17 -2
- package/src/__tests__/guardian-grant-minting.test.ts +1 -1
- package/src/__tests__/guardian-routing-invariants.test.ts +93 -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 +5 -1
- package/src/__tests__/host-app-control-proxy.test.ts +45 -0
- 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__/llm-resolver.test.ts +73 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +156 -38
- package/src/__tests__/memory-v2-static-injector.test.ts +86 -8
- package/src/__tests__/notification-decision-strategy.test.ts +3 -3
- package/src/__tests__/parallel-tool.benchmark.test.ts +35 -8
- package/src/__tests__/persist-unsendable-image-downscale.test.ts +145 -0
- package/src/__tests__/persist-unsendable-image.test.ts +97 -1
- package/src/__tests__/plugin-api-shim.test.ts +6 -9
- package/src/__tests__/plugin-bootstrap.test.ts +91 -81
- package/src/__tests__/plugin-registry.test.ts +3 -49
- package/src/__tests__/plugin-skill-contribution.test.ts +15 -14
- package/src/__tests__/plugin-tool-contribution.test.ts +7 -4
- package/src/__tests__/plugin-types.test.ts +0 -70
- package/src/__tests__/post-turn-tool-result-truncation.test.ts +69 -0
- package/src/__tests__/pre-model-call-sanitize.test.ts +109 -0
- package/src/__tests__/published-app-updater.test.ts +138 -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 +31 -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 +5 -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/__tests__/web-fetch.test.ts +45 -0
- 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-config-stub.ts +0 -2
- 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/prepare-agent-env.ts +80 -27
- package/src/acp/resolve-agent.test.ts +188 -28
- package/src/acp/resolve-agent.ts +119 -42
- 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 +403 -251
- package/src/api/responses/conversation-message.ts +22 -2
- package/src/api/responses/memory-v3-selection-log.ts +19 -10
- package/src/approvals/guardian-request-resolvers.ts +27 -1
- package/src/background-wake/next-wake.ts +1 -0
- package/src/cli/commands/__tests__/memory-v3.test.ts +191 -210
- package/src/cli/commands/channel-verification-sessions.ts +6 -6
- package/src/cli/commands/db/__tests__/repair.test.ts +3 -1
- package/src/cli/commands/memory-v3.ts +57 -199
- package/src/cli/commands/plugins.ts +43 -37
- package/src/cli/lib/__tests__/install-from-github.test.ts +790 -111
- package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +196 -0
- package/src/cli/lib/__tests__/plugin-details.test.ts +381 -0
- package/src/cli/lib/__tests__/plugin-marketplace.test.ts +270 -0
- package/src/cli/lib/__tests__/search-plugins.test.ts +233 -32
- package/src/cli/lib/install-from-github.ts +797 -60
- package/src/cli/lib/plugin-catalog-cache.ts +84 -0
- package/src/cli/lib/plugin-details.ts +416 -0
- package/src/cli/lib/plugin-marketplace.ts +215 -0
- package/src/cli/lib/search-plugins.ts +201 -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 +9 -3
- package/src/config/acp-schema.ts +1 -7
- package/src/config/bundled-skills/acp/SKILL.md +80 -41
- 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 -3
- package/src/config/feature-flag-registry.json +10 -16
- package/src/config/llm-resolver.ts +39 -7
- package/src/config/schemas/__tests__/memory-v3.test.ts +25 -9
- package/src/config/schemas/call-site-catalog.ts +0 -21
- package/src/config/schemas/heartbeat.ts +9 -0
- package/src/config/schemas/llm.ts +17 -3
- package/src/config/schemas/memory-v3.ts +58 -8
- package/src/config/seed-inference-profiles.ts +18 -0
- package/src/context/compactor.ts +22 -4
- package/src/context/post-turn-tool-result-truncation.ts +39 -1
- package/src/context/strip-injections.ts +8 -2
- package/src/daemon/conversation-agent-loop-handlers.ts +18 -36
- package/src/daemon/conversation-agent-loop.ts +308 -1297
- package/src/daemon/conversation-error.ts +31 -4
- package/src/daemon/conversation-history.ts +1 -1
- package/src/daemon/conversation-lifecycle.ts +11 -255
- package/src/daemon/conversation-media-retry.ts +19 -6
- package/src/daemon/conversation-messaging.ts +17 -0
- package/src/daemon/conversation-process.ts +22 -141
- package/src/daemon/conversation-queue-manager.ts +8 -0
- package/src/daemon/conversation-registry.ts +159 -0
- package/src/daemon/conversation-runtime-assembly.ts +361 -393
- package/src/daemon/conversation-store.ts +9 -90
- package/src/daemon/conversation-surfaces.ts +165 -11
- package/src/daemon/conversation-workspace.ts +17 -0
- package/src/daemon/conversation.ts +444 -62
- package/src/daemon/disk-pressure-policy.ts +0 -1
- package/src/daemon/external-plugins-bootstrap.ts +36 -46
- package/src/daemon/handlers/config-channels.ts +11 -2
- package/src/daemon/handlers/conversations.ts +3 -1
- package/src/daemon/handlers/skills.ts +4 -1
- package/src/daemon/host-app-control-proxy.ts +72 -57
- package/src/daemon/host-proxy-preactivation.ts +1 -3
- package/src/daemon/lifecycle.ts +21 -7
- package/src/daemon/persist-unsendable-image.ts +62 -25
- package/src/daemon/process-message.ts +1 -1
- package/src/daemon/server.ts +2 -0
- package/src/daemon/tool-side-effects.ts +15 -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__/activation-session-store.test.ts +41 -0
- package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +87 -0
- package/src/memory/__tests__/onboarding-events-store.test.ts +80 -0
- package/src/memory/activation-session-store.ts +43 -0
- package/src/memory/conversation-crud.ts +29 -19
- package/src/memory/conversation-starter-checkpoints.ts +1 -0
- package/src/memory/db-init.ts +6 -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/273-onboarding-events-funnel-columns.ts +46 -0
- package/src/memory/migrations/274-create-activation-sessions.ts +15 -0
- package/src/memory/migrations/index.ts +3 -0
- package/src/memory/onboarding-events-store.ts +66 -18
- package/src/memory/schema/acp.ts +4 -0
- package/src/memory/schema/infrastructure.ts +13 -0
- package/src/memory/v2/__tests__/consolidation-job.test.ts +4 -97
- package/src/memory/v2/__tests__/page-store.test.ts +22 -0
- package/src/memory/v2/consolidation-job.ts +2 -63
- package/src/memory/v2/types.ts +5 -0
- package/src/messaging/providers/telegram-bot/api.ts +14 -5
- package/src/notifications/__tests__/copy-composer.test.ts +244 -0
- package/src/notifications/access-request-copy.ts +298 -0
- package/src/notifications/adapters/slack.ts +3 -3
- package/src/notifications/adapters/telegram.ts +9 -2
- package/src/notifications/copy-composer.ts +49 -267
- package/src/notifications/decision-engine.ts +16 -35
- package/src/notifications/home-feed-side-effect.ts +1 -6
- package/src/plugin-api/constants.ts +4 -0
- package/src/plugin-api/index.ts +40 -11
- package/src/plugin-api/types.ts +108 -0
- package/src/plugins/defaults/compaction/compact.ts +106 -0
- package/src/{daemon → plugins/defaults/compaction}/context-overflow-reducer.ts +250 -42
- package/src/plugins/defaults/compaction/corrected-target.ts +53 -0
- package/src/plugins/defaults/compaction/manager-store.ts +57 -0
- package/src/plugins/defaults/compaction/package.json +1 -2
- package/src/{context → plugins/defaults/compaction}/window-manager.ts +95 -25
- 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 +156 -26
- package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +100 -52
- package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +285 -0
- 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/package.json +14 -0
- package/src/plugins/defaults/memory-retrieval/unified-turn-context.ts +223 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/capabilities.test.ts +71 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/dense.test.ts +181 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/edge.test.ts +247 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/live-integration.test.ts +147 -134
- package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +456 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +662 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +306 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/render-injection.test.ts +91 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +402 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/section-needle.test.ts +135 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/sections.test.ts +125 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selection-log-store.test.ts +74 -19
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +446 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +592 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/types.test.ts +2 -24
- package/src/plugins/defaults/memory-v3-shadow/capabilities.ts +91 -0
- package/src/plugins/defaults/memory-v3-shadow/dense.ts +97 -0
- package/src/plugins/defaults/memory-v3-shadow/edge.ts +252 -0
- 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 +76 -0
- package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +555 -0
- package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +266 -0
- package/src/plugins/defaults/memory-v3-shadow/package.json +14 -0
- package/src/plugins/defaults/memory-v3-shadow/page-content.ts +71 -0
- package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +204 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/render-injection.ts +16 -8
- package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +236 -0
- package/src/plugins/defaults/memory-v3-shadow/section-needle.ts +200 -0
- package/src/plugins/defaults/memory-v3-shadow/sections.ts +115 -0
- package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +156 -0
- package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +382 -0
- package/src/plugins/defaults/memory-v3-shadow/types.ts +92 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/working-set.ts +4 -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/external-api.ts +12 -2
- 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/system-prompt.ts +61 -10
- package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +71 -4
- package/src/providers/anthropic/client.ts +9 -10
- package/src/providers/inference/kimi-cjk-token-ids.ts +493 -0
- package/src/providers/inference/logit-bias.ts +55 -0
- package/src/providers/model-catalog.ts +34 -0
- package/src/providers/openai/__tests__/vision-not-supported.test.ts +75 -0
- package/src/providers/openai/chat-completions-provider.ts +44 -1
- package/src/providers/retry.ts +22 -0
- package/src/providers/types.ts +6 -0
- 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 +466 -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__/stt-routes.test.ts +112 -0
- 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 +185 -32
- package/src/runtime/routes/acp-routes.ts +328 -30
- package/src/runtime/routes/app-management-routes.ts +3 -0
- 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/channel-verification-routes.ts +14 -5
- 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 +30 -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/inbound-stages/acl-enforcement.ts +33 -34
- 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 +65 -305
- package/src/runtime/routes/playground/__tests__/force-compact.test.ts +1 -1
- package/src/runtime/routes/playground/helpers.ts +1 -1
- package/src/runtime/routes/plugins-routes.ts +337 -35
- package/src/runtime/routes/stt-routes.ts +45 -12
- package/src/runtime/routes/surface-conversation-resolver.ts +4 -3
- package/src/runtime/routes/work-items-routes.ts +2 -4
- package/src/runtime/routes/workspace-routes.ts +50 -15
- package/src/runtime/services/conversation-serializer.ts +1 -1
- package/src/runtime/verification-outbound-actions.ts +147 -2
- package/src/runtime/verification-templates.ts +29 -3
- package/src/services/published-app-updater.ts +30 -8
- package/src/signals/cancel.ts +2 -4
- package/src/subagent/manager.ts +21 -5
- package/src/telemetry/__tests__/activation-funnel.test.ts +95 -0
- package/src/telemetry/activation-funnel.ts +167 -0
- package/src/telemetry/types.ts +13 -0
- package/src/telemetry/usage-telemetry-reporter.test.ts +154 -0
- package/src/telemetry/usage-telemetry-reporter.ts +26 -1
- package/src/tools/acp/context.ts +20 -0
- package/src/tools/acp/list-agents.test.ts +9 -19
- package/src/tools/acp/list-agents.ts +3 -15
- package/src/tools/acp/spawn.test.ts +173 -202
- 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/browser/browser-execution.ts +12 -2
- package/src/tools/filesystem/write.ts +34 -0
- package/src/tools/network/web-fetch.ts +65 -24
- package/src/tools/skills/load.ts +1 -1
- package/src/tools/subagent/spawn.ts +2 -4
- package/src/tools/ui-surface/definitions.ts +32 -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/daemon/wake-target-adapter.ts +0 -253
- package/src/memory/v3/__tests__/assign.test.ts +0 -242
- package/src/memory/v3/__tests__/capabilities.test.ts +0 -118
- package/src/memory/v3/__tests__/core.test.ts +0 -39
- package/src/memory/v3/__tests__/health.test.ts +0 -219
- package/src/memory/v3/__tests__/maintain-job.test.ts +0 -288
- package/src/memory/v3/__tests__/needle.test.ts +0 -107
- package/src/memory/v3/__tests__/orchestrate.test.ts +0 -436
- package/src/memory/v3/__tests__/provider-blocks.test.ts +0 -13
- package/src/memory/v3/__tests__/reconcile.test.ts +0 -274
- package/src/memory/v3/__tests__/render-injection.test.ts +0 -61
- package/src/memory/v3/__tests__/router.test.ts +0 -332
- package/src/memory/v3/__tests__/selector.test.ts +0 -470
- package/src/memory/v3/__tests__/shadow-plugin.test.ts +0 -432
- package/src/memory/v3/__tests__/snapshot.test.ts +0 -168
- package/src/memory/v3/__tests__/tree.test.ts +0 -192
- package/src/memory/v3/assign.ts +0 -268
- package/src/memory/v3/capabilities.ts +0 -124
- package/src/memory/v3/core.ts +0 -26
- package/src/memory/v3/health.ts +0 -0
- package/src/memory/v3/maintain-job.ts +0 -314
- package/src/memory/v3/needle.ts +0 -115
- package/src/memory/v3/orchestrate.ts +0 -126
- package/src/memory/v3/page-content.ts +0 -34
- package/src/memory/v3/provider-blocks.ts +0 -26
- package/src/memory/v3/reconcile.ts +0 -523
- package/src/memory/v3/router.ts +0 -190
- package/src/memory/v3/selection-log-store.ts +0 -84
- package/src/memory/v3/selector.ts +0 -226
- package/src/memory/v3/shadow-plugin.ts +0 -411
- package/src/memory/v3/snapshot.ts +0 -209
- package/src/memory/v3/tree.ts +0 -174
- package/src/memory/v3/types.ts +0 -59
- package/src/notifications/__tests__/emit-signal-home-feed.test.ts +0 -187
- 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/memory-retrieval/hooks/user-prompt-submit-temp.ts +0 -216
- 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/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/util/map-limit.ts +0 -27
- /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__/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}/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}/llm-retry.ts +0 -0
|
@@ -28,6 +28,20 @@ let mockUiConfig: { userTimezone?: string; detectedTimezone?: string } = {};
|
|
|
28
28
|
|
|
29
29
|
// ── Module mocks (must precede imports of the module under test) ─────
|
|
30
30
|
|
|
31
|
+
// The real AgentLoop resolves the per-conversation ContextWindowManager from
|
|
32
|
+
// the compaction store keyed by conversationId. These orchestrator tests build
|
|
33
|
+
// fake conversations whose manager is a canned stub, so register each stub in a
|
|
34
|
+
// map the mocked store reads from.
|
|
35
|
+
const fakeContextWindowManagers = new Map<string, unknown>();
|
|
36
|
+
mock.module("../plugins/defaults/compaction/manager-store.js", () => ({
|
|
37
|
+
createContextWindowManager: () => undefined,
|
|
38
|
+
getContextWindowManager: (conversationId: string) =>
|
|
39
|
+
fakeContextWindowManagers.get(conversationId),
|
|
40
|
+
disposeContextWindowManager: (conversationId: string) => {
|
|
41
|
+
fakeContextWindowManagers.delete(conversationId);
|
|
42
|
+
},
|
|
43
|
+
}));
|
|
44
|
+
|
|
31
45
|
mock.module("../util/logger.js", () => ({
|
|
32
46
|
getLogger: () =>
|
|
33
47
|
new Proxy({} as Record<string, unknown>, { get: () => () => {} }),
|
|
@@ -100,35 +114,38 @@ mock.module("../context/token-estimator.js", () => ({
|
|
|
100
114
|
let mockReducerStepFn:
|
|
101
115
|
| ((msgs: Message[], cfg: unknown, state: unknown) => unknown)
|
|
102
116
|
| null = null;
|
|
103
|
-
mock.module(
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
117
|
+
mock.module(
|
|
118
|
+
"../plugins/defaults/compaction/context-overflow-reducer.js",
|
|
119
|
+
() => ({
|
|
120
|
+
createInitialReducerState: () => ({
|
|
121
|
+
appliedTiers: [],
|
|
122
|
+
injectionMode: "full" as const,
|
|
123
|
+
exhausted: false,
|
|
124
|
+
}),
|
|
125
|
+
reduceContextOverflow: async (
|
|
126
|
+
msgs: Message[],
|
|
127
|
+
cfg: unknown,
|
|
128
|
+
state: unknown,
|
|
129
|
+
) => {
|
|
130
|
+
if (mockReducerStepFn) return mockReducerStepFn(msgs, cfg, state);
|
|
131
|
+
return {
|
|
132
|
+
messages: msgs,
|
|
133
|
+
tier: "forced_compaction",
|
|
134
|
+
state: {
|
|
135
|
+
appliedTiers: [
|
|
136
|
+
"forced_compaction",
|
|
137
|
+
"tool_result_truncation",
|
|
138
|
+
"media_stubbing",
|
|
139
|
+
"injection_downgrade",
|
|
140
|
+
],
|
|
141
|
+
injectionMode: "full",
|
|
142
|
+
exhausted: true,
|
|
143
|
+
},
|
|
144
|
+
estimatedTokens: 1000,
|
|
145
|
+
};
|
|
146
|
+
},
|
|
108
147
|
}),
|
|
109
|
-
|
|
110
|
-
msgs: Message[],
|
|
111
|
-
cfg: unknown,
|
|
112
|
-
state: unknown,
|
|
113
|
-
) => {
|
|
114
|
-
if (mockReducerStepFn) return mockReducerStepFn(msgs, cfg, state);
|
|
115
|
-
return {
|
|
116
|
-
messages: msgs,
|
|
117
|
-
tier: "forced_compaction",
|
|
118
|
-
state: {
|
|
119
|
-
appliedTiers: [
|
|
120
|
-
"forced_compaction",
|
|
121
|
-
"tool_result_truncation",
|
|
122
|
-
"media_stubbing",
|
|
123
|
-
"injection_downgrade",
|
|
124
|
-
],
|
|
125
|
-
injectionMode: "full",
|
|
126
|
-
exhausted: true,
|
|
127
|
-
},
|
|
128
|
-
estimatedTokens: 1000,
|
|
129
|
-
};
|
|
130
|
-
},
|
|
131
|
-
}));
|
|
148
|
+
);
|
|
132
149
|
|
|
133
150
|
// Policy: default to fail_gracefully
|
|
134
151
|
let mockOverflowAction: string = "fail_gracefully";
|
|
@@ -312,16 +329,14 @@ let mockInjectionBlocks: {
|
|
|
312
329
|
pkbSystemReminder?: string;
|
|
313
330
|
unifiedTurnContext?: string;
|
|
314
331
|
} = {};
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
)
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
}),
|
|
324
|
-
);
|
|
332
|
+
const defaultApplyRuntimeInjectionsImpl = async (
|
|
333
|
+
msgs: Message[],
|
|
334
|
+
_options?: unknown,
|
|
335
|
+
) => ({
|
|
336
|
+
messages: msgs,
|
|
337
|
+
blocks: { ...mockInjectionBlocks },
|
|
338
|
+
});
|
|
339
|
+
const applyRuntimeInjectionsMock = mock(defaultApplyRuntimeInjectionsImpl);
|
|
325
340
|
let mockSlackChronologicalContext: {
|
|
326
341
|
renderedMessages: Array<{
|
|
327
342
|
message: Message;
|
|
@@ -358,7 +373,6 @@ const getSlackCompactionWatermarkForPrefixMock = mock(
|
|
|
358
373
|
);
|
|
359
374
|
mock.module("../daemon/conversation-runtime-assembly.js", () => ({
|
|
360
375
|
applyRuntimeInjections: applyRuntimeInjectionsMock,
|
|
361
|
-
buildUnifiedTurnContextBlock: buildUnifiedTurnContextBlockMock,
|
|
362
376
|
stripInjectionsForCompaction: (msgs: Message[]) => msgs,
|
|
363
377
|
isSlackChannelConversation: () => false,
|
|
364
378
|
getSlackCompactionWatermarkForPrefix:
|
|
@@ -523,22 +537,11 @@ mock.module("../memory/llm-request-log-store.js", () => ({
|
|
|
523
537
|
setAgentLoopExitReasonOnLatestLog: setAgentLoopExitReasonOnLatestLogMock,
|
|
524
538
|
}));
|
|
525
539
|
|
|
526
|
-
let mockHasProactiveArtifactCompleted = true;
|
|
527
|
-
let mockTryClaimProactiveArtifactTrigger = false;
|
|
528
|
-
const runProactiveArtifactJobMock = mock(
|
|
529
|
-
async (_params: Record<string, unknown>) => {},
|
|
530
|
-
);
|
|
531
|
-
mock.module("../proactive-artifact/index.js", () => ({
|
|
532
|
-
hasProactiveArtifactCompleted: () => mockHasProactiveArtifactCompleted,
|
|
533
|
-
tryClaimProactiveArtifactTrigger: () => mockTryClaimProactiveArtifactTrigger,
|
|
534
|
-
runProactiveArtifactJob: runProactiveArtifactJobMock,
|
|
535
|
-
}));
|
|
536
|
-
|
|
537
540
|
// ── Imports (after mocks) ────────────────────────────────────────────
|
|
538
541
|
|
|
539
542
|
import { AgentLoop } from "../agent/loop.js";
|
|
543
|
+
import type { Conversation } from "../daemon/conversation.js";
|
|
540
544
|
import {
|
|
541
|
-
type AgentLoopConversationContext,
|
|
542
545
|
applyCompactionResult,
|
|
543
546
|
runAgentLoopImpl,
|
|
544
547
|
} from "../daemon/conversation-agent-loop.js";
|
|
@@ -552,13 +555,13 @@ import {
|
|
|
552
555
|
// ── Test helpers ─────────────────────────────────────────────────────
|
|
553
556
|
|
|
554
557
|
function makeCtx(
|
|
555
|
-
overrides?: Partial<
|
|
558
|
+
overrides?: Partial<Conversation> & {
|
|
556
559
|
providerResponses?: ScriptedResponse[];
|
|
557
560
|
loopProvider?: Provider;
|
|
558
561
|
loopTools?: ToolDefinition[];
|
|
559
562
|
toolExecutor?: LoopToolExecutor;
|
|
560
563
|
},
|
|
561
|
-
):
|
|
564
|
+
): Conversation {
|
|
562
565
|
const {
|
|
563
566
|
providerResponses,
|
|
564
567
|
loopProvider,
|
|
@@ -589,13 +592,15 @@ function makeCtx(
|
|
|
589
592
|
providerResponses ?? [textResponse("response")],
|
|
590
593
|
loopProviderName,
|
|
591
594
|
).provider;
|
|
592
|
-
const agentLoop = new AgentLoop(
|
|
595
|
+
const agentLoop = new AgentLoop({
|
|
596
|
+
provider: provider,
|
|
597
|
+
systemPrompt: "system prompt",
|
|
593
598
|
conversationId,
|
|
594
599
|
tools: loopTools ?? [],
|
|
595
600
|
toolExecutor,
|
|
596
601
|
});
|
|
597
602
|
|
|
598
|
-
|
|
603
|
+
const ctx = {
|
|
599
604
|
conversationId: "test-conv",
|
|
600
605
|
messages: [
|
|
601
606
|
{ role: "user", content: [{ type: "text", text: "Hello" }] },
|
|
@@ -616,15 +621,24 @@ function makeCtx(
|
|
|
616
621
|
usage: { inputTokens: 0, outputTokens: 0 },
|
|
617
622
|
stopReason: "end_turn",
|
|
618
623
|
}),
|
|
619
|
-
} as unknown as
|
|
624
|
+
} as unknown as Conversation["provider"],
|
|
620
625
|
systemPrompt: "system prompt",
|
|
621
626
|
|
|
622
627
|
contextWindowManager: {
|
|
628
|
+
updateConfig: () => {},
|
|
623
629
|
shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
|
|
624
630
|
maybeCompact: async () => ({ compacted: false }),
|
|
625
|
-
} as unknown as
|
|
626
|
-
|
|
627
|
-
|
|
631
|
+
} as unknown as Conversation["contextWindowManager"],
|
|
632
|
+
conversationType: mockConversationRow?.conversationType ?? undefined,
|
|
633
|
+
source: mockConversationRow?.source ?? undefined,
|
|
634
|
+
contextSummary: mockConversationRow?.contextSummary ?? null,
|
|
635
|
+
contextCompactedMessageCount:
|
|
636
|
+
mockConversationRow?.contextCompactedMessageCount ?? 0,
|
|
637
|
+
contextCompactedAt: mockConversationRow?.contextCompactedAt ?? null,
|
|
638
|
+
slackContextCompactionWatermarkTs:
|
|
639
|
+
mockConversationRow?.slackContextCompactionWatermarkTs ?? null,
|
|
640
|
+
lastNotifiedInferenceProfile:
|
|
641
|
+
mockConversationRow?.lastNotifiedInferenceProfile ?? null,
|
|
628
642
|
|
|
629
643
|
memoryPolicy: { scopeId: "default", includeDefaultFallback: true },
|
|
630
644
|
|
|
@@ -636,8 +650,6 @@ function makeCtx(
|
|
|
636
650
|
currentTurnSurfaces: [],
|
|
637
651
|
|
|
638
652
|
workingDir: "/tmp",
|
|
639
|
-
workspaceTopLevelContext: null,
|
|
640
|
-
workspaceTopLevelDirty: false,
|
|
641
653
|
channelCapabilities: undefined,
|
|
642
654
|
commandIntent: undefined,
|
|
643
655
|
trustContext: undefined,
|
|
@@ -647,15 +659,15 @@ function makeCtx(
|
|
|
647
659
|
preactivatedSkillIds: undefined,
|
|
648
660
|
skillProjectionState: new Map(),
|
|
649
661
|
skillProjectionCache:
|
|
650
|
-
new Map() as unknown as
|
|
662
|
+
new Map() as unknown as Conversation["skillProjectionCache"],
|
|
651
663
|
|
|
652
664
|
traceEmitter: {
|
|
653
665
|
emit: () => {},
|
|
654
|
-
} as unknown as
|
|
666
|
+
} as unknown as Conversation["traceEmitter"],
|
|
655
667
|
profiler: {
|
|
656
668
|
startRequest: () => {},
|
|
657
669
|
emitSummary: () => {},
|
|
658
|
-
} as unknown as
|
|
670
|
+
} as unknown as Conversation["profiler"],
|
|
659
671
|
usageStats: {
|
|
660
672
|
totalInputTokens: 0,
|
|
661
673
|
totalOutputTokens: 0,
|
|
@@ -668,13 +680,12 @@ function makeCtx(
|
|
|
668
680
|
lastAttachmentWarnings: [],
|
|
669
681
|
|
|
670
682
|
hasNoClient: false,
|
|
671
|
-
prompter: {} as unknown as
|
|
672
|
-
queue: {} as unknown as
|
|
683
|
+
prompter: {} as unknown as Conversation["prompter"],
|
|
684
|
+
queue: {} as unknown as Conversation["queue"],
|
|
673
685
|
|
|
674
686
|
getWorkspaceGitService: () => ({ ensureInitialized: async () => {} }),
|
|
675
687
|
commitTurnChanges: async () => {},
|
|
676
688
|
|
|
677
|
-
refreshWorkspaceTopLevelContextIfNeeded: () => {},
|
|
678
689
|
markWorkspaceTopLevelDirty: () => {},
|
|
679
690
|
emitActivityState: () => {},
|
|
680
691
|
getQueueDepth: () => 0,
|
|
@@ -701,10 +712,34 @@ function makeCtx(
|
|
|
701
712
|
}),
|
|
702
713
|
retrackCachedNodes: () => {},
|
|
703
714
|
recordPkbQueryVectors: () => {},
|
|
704
|
-
} as unknown as
|
|
715
|
+
} as unknown as Conversation["graphMemory"],
|
|
705
716
|
|
|
706
717
|
...ctxOverrides,
|
|
707
|
-
} as
|
|
718
|
+
} as unknown as Conversation;
|
|
719
|
+
fakeContextWindowManagers.set(conversationId, ctx.contextWindowManager);
|
|
720
|
+
return ctx;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
type CompactionResult = Parameters<typeof applyCompactionResult>[1];
|
|
724
|
+
|
|
725
|
+
function makeCompactionResult(
|
|
726
|
+
overrides?: Partial<CompactionResult>,
|
|
727
|
+
): CompactionResult {
|
|
728
|
+
return {
|
|
729
|
+
messages: [{ role: "user", content: [{ type: "text", text: "summary" }] }],
|
|
730
|
+
compactedPersistedMessages: 4,
|
|
731
|
+
previousEstimatedInputTokens: 12000,
|
|
732
|
+
estimatedInputTokens: 3000,
|
|
733
|
+
maxInputTokens: 100000,
|
|
734
|
+
thresholdTokens: 80000,
|
|
735
|
+
compactedMessages: 4,
|
|
736
|
+
summaryCalls: 1,
|
|
737
|
+
summaryInputTokens: 100,
|
|
738
|
+
summaryOutputTokens: 20,
|
|
739
|
+
summaryModel: "mock-model",
|
|
740
|
+
summaryText: "summary",
|
|
741
|
+
...overrides,
|
|
742
|
+
};
|
|
708
743
|
}
|
|
709
744
|
|
|
710
745
|
// ── Tests ────────────────────────────────────────────────────────────
|
|
@@ -738,13 +773,12 @@ beforeEach(() => {
|
|
|
738
773
|
title: null,
|
|
739
774
|
};
|
|
740
775
|
mockMessageById = null;
|
|
741
|
-
mockHasProactiveArtifactCompleted = true;
|
|
742
|
-
mockTryClaimProactiveArtifactTrigger = false;
|
|
743
|
-
runProactiveArtifactJobMock.mockClear();
|
|
744
776
|
setConversationHistoryStrippedAtMock.mockClear();
|
|
745
777
|
setConversationHistoryStrippedAtMock.mockImplementation(() => {});
|
|
746
778
|
applyRuntimeInjectionsMock.mockClear();
|
|
747
|
-
|
|
779
|
+
applyRuntimeInjectionsMock.mockImplementation(
|
|
780
|
+
defaultApplyRuntimeInjectionsImpl,
|
|
781
|
+
);
|
|
748
782
|
resolveTurnTimezoneContextMock.mockClear();
|
|
749
783
|
formatTurnTimestampMock.mockClear();
|
|
750
784
|
mockSlackChronologicalContext = null;
|
|
@@ -757,11 +791,10 @@ beforeEach(() => {
|
|
|
757
791
|
projectAssistantMessageMock.mockClear();
|
|
758
792
|
publishSyncInvalidationMock.mockClear();
|
|
759
793
|
mockMessageById = null;
|
|
760
|
-
//
|
|
761
|
-
//
|
|
762
|
-
//
|
|
763
|
-
//
|
|
764
|
-
// hitting the bare terminals.
|
|
794
|
+
// The compaction pipeline runs through the plugin registry; reset and
|
|
795
|
+
// re-register every default so it dispatches to middleware backed by the
|
|
796
|
+
// mocked collaborators these tests install (`syncMessageToDisk`, etc.)
|
|
797
|
+
// instead of hitting the bare terminal.
|
|
765
798
|
resetPluginRegistryAndRegisterDefaults();
|
|
766
799
|
});
|
|
767
800
|
|
|
@@ -785,7 +818,7 @@ describe("session-agent-loop", () => {
|
|
|
785
818
|
});
|
|
786
819
|
});
|
|
787
820
|
|
|
788
|
-
test("
|
|
821
|
+
test("freezes the client timezone snapshot on the conversation, not the options bag", async () => {
|
|
789
822
|
mockUiConfig = {
|
|
790
823
|
userTimezone: "US/Eastern",
|
|
791
824
|
detectedTimezone: "US/Central",
|
|
@@ -802,17 +835,25 @@ describe("session-agent-loop", () => {
|
|
|
802
835
|
|
|
803
836
|
await runAgentLoopImpl(ctx, "hello", "msg-1", () => {});
|
|
804
837
|
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
expect(turnContextOptions).toMatchObject({
|
|
812
|
-
configuredUserTimezone: "America/New_York",
|
|
838
|
+
// The turn-start client timezone and the long-absence gap are frozen on
|
|
839
|
+
// the conversation as `currentTurnTemporalSnapshot`; the live
|
|
840
|
+
// `clientTimezone` would otherwise be clobbered mid-turn. `current_time`
|
|
841
|
+
// is not frozen — it is computed fresh at each injection inside
|
|
842
|
+
// `applyRuntimeInjections`.
|
|
843
|
+
expect(ctx.currentTurnTemporalSnapshot).toEqual({
|
|
813
844
|
clientTimezone: "America/Los_Angeles",
|
|
814
|
-
|
|
815
|
-
});
|
|
845
|
+
timeSinceLastMessage: null,
|
|
846
|
+
});
|
|
847
|
+
// Neither the timezones nor the long-absence gap are threaded through the
|
|
848
|
+
// options bag — `applyRuntimeInjections` sources the client timezone and
|
|
849
|
+
// gap from the snapshot and the config timezones from config
|
|
850
|
+
// (`ui.userTimezone`, `ui.detectedTimezone`).
|
|
851
|
+
const injectionOptions = applyRuntimeInjectionsMock.mock.calls[0]?.[1];
|
|
852
|
+
expect(injectionOptions).not.toHaveProperty("timestamp");
|
|
853
|
+
expect(injectionOptions).not.toHaveProperty("clientTimezone");
|
|
854
|
+
expect(injectionOptions).not.toHaveProperty("configuredUserTimezone");
|
|
855
|
+
expect(injectionOptions).not.toHaveProperty("detectedTimezone");
|
|
856
|
+
expect(injectionOptions).not.toHaveProperty("timeSinceLastMessage");
|
|
816
857
|
});
|
|
817
858
|
});
|
|
818
859
|
|
|
@@ -826,61 +867,24 @@ describe("session-agent-loop", () => {
|
|
|
826
867
|
});
|
|
827
868
|
});
|
|
828
869
|
|
|
829
|
-
describe("proactive artifact trigger", () => {
|
|
830
|
-
test("does not start proactive artifact jobs after foreground user turns", async () => {
|
|
831
|
-
mockConversationRow = {
|
|
832
|
-
...mockConversationRow,
|
|
833
|
-
id: "test-conv",
|
|
834
|
-
conversationType: "standard",
|
|
835
|
-
};
|
|
836
|
-
mockMessageById = {
|
|
837
|
-
id: "user-msg-1",
|
|
838
|
-
conversationId: "test-conv",
|
|
839
|
-
createdAt: 1000,
|
|
840
|
-
};
|
|
841
|
-
mockHasProactiveArtifactCompleted = false;
|
|
842
|
-
mockTryClaimProactiveArtifactTrigger = true;
|
|
843
|
-
|
|
844
|
-
// A two-call agent turn: the model invokes `app_create`, then wraps up
|
|
845
|
-
// with a final text reply.
|
|
846
|
-
const ctx = makeCtx({
|
|
847
|
-
conversationId: "test-conv",
|
|
848
|
-
providerResponses: [
|
|
849
|
-
{
|
|
850
|
-
content: [
|
|
851
|
-
{ type: "text", text: "I'll build that app." },
|
|
852
|
-
{
|
|
853
|
-
type: "tool_use",
|
|
854
|
-
id: "tool-1",
|
|
855
|
-
name: "app_create",
|
|
856
|
-
input: { name: "Flow" },
|
|
857
|
-
},
|
|
858
|
-
],
|
|
859
|
-
model: "mock-model",
|
|
860
|
-
usage: { inputTokens: 10, outputTokens: 5 },
|
|
861
|
-
stopReason: "tool_use",
|
|
862
|
-
},
|
|
863
|
-
textResponse("Done."),
|
|
864
|
-
],
|
|
865
|
-
toolExecutor: async () => ({ content: "{}", isError: false }),
|
|
866
|
-
});
|
|
867
|
-
await runAgentLoopImpl(
|
|
868
|
-
ctx,
|
|
869
|
-
"build a kanban app",
|
|
870
|
-
"user-msg-1",
|
|
871
|
-
() => {},
|
|
872
|
-
{
|
|
873
|
-
isUserMessage: true,
|
|
874
|
-
},
|
|
875
|
-
);
|
|
876
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
877
|
-
|
|
878
|
-
expect(runProactiveArtifactJobMock).toHaveBeenCalledTimes(0);
|
|
879
|
-
});
|
|
880
|
-
});
|
|
881
|
-
|
|
882
870
|
describe("disk pressure injection context", () => {
|
|
883
|
-
|
|
871
|
+
// The loop sets `ctx.diskPressureCleanupModeActive` for the duration of the
|
|
872
|
+
// turn (the disk-pressure-warning injector reads it via the per-conversation
|
|
873
|
+
// registry) and resets it in the turn-end cleanup path. Snapshot the flag at
|
|
874
|
+
// each `applyRuntimeInjections` call so assertions observe its value while
|
|
875
|
+
// injection runs, not the post-turn reset.
|
|
876
|
+
function captureCleanupFlagDuringInjection(ctx: {
|
|
877
|
+
diskPressureCleanupModeActive?: boolean;
|
|
878
|
+
}): () => Array<boolean | undefined> {
|
|
879
|
+
const observed: Array<boolean | undefined> = [];
|
|
880
|
+
applyRuntimeInjectionsMock.mockImplementation(async (msgs: Message[]) => {
|
|
881
|
+
observed.push(ctx.diskPressureCleanupModeActive);
|
|
882
|
+
return { messages: msgs, blocks: { ...mockInjectionBlocks } };
|
|
883
|
+
});
|
|
884
|
+
return () => observed;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
test("sets the cleanup-mode flag on the conversation for cleanup-mode turns", async () => {
|
|
884
888
|
mockDiskPressureDecision = {
|
|
885
889
|
action: "allow-cleanup-mode",
|
|
886
890
|
reason: "guardian",
|
|
@@ -901,8 +905,9 @@ describe("session-agent-loop", () => {
|
|
|
901
905
|
trustContext: {
|
|
902
906
|
sourceChannel: "telegram",
|
|
903
907
|
trustClass: "guardian",
|
|
904
|
-
} as
|
|
908
|
+
} as Conversation["trustContext"],
|
|
905
909
|
});
|
|
910
|
+
const cleanupFlagDuringInjection = captureCleanupFlagDuringInjection(ctx);
|
|
906
911
|
|
|
907
912
|
await runAgentLoopImpl(ctx, "free up space", "msg-1", () => {});
|
|
908
913
|
|
|
@@ -921,21 +926,16 @@ describe("session-agent-loop", () => {
|
|
|
921
926
|
},
|
|
922
927
|
}),
|
|
923
928
|
);
|
|
924
|
-
|
|
925
|
-
.calls[0]![1] as {
|
|
926
|
-
diskPressureContext?: { cleanupModeActive: boolean } | null;
|
|
927
|
-
};
|
|
928
|
-
expect(firstInjectionOptions.diskPressureContext).toEqual({
|
|
929
|
-
cleanupModeActive: true,
|
|
930
|
-
});
|
|
929
|
+
expect(cleanupFlagDuringInjection()).toEqual([true]);
|
|
931
930
|
});
|
|
932
931
|
|
|
933
|
-
test("
|
|
932
|
+
test("sets the cleanup-mode flag on the conversation for local-owner turns", async () => {
|
|
934
933
|
mockDiskPressureDecision = {
|
|
935
934
|
action: "allow-cleanup-mode",
|
|
936
935
|
reason: "local-owner",
|
|
937
936
|
};
|
|
938
937
|
const ctx = makeCtx();
|
|
938
|
+
const cleanupFlagDuringInjection = captureCleanupFlagDuringInjection(ctx);
|
|
939
939
|
|
|
940
940
|
await runAgentLoopImpl(ctx, "free up space", "msg-1", () => {});
|
|
941
941
|
|
|
@@ -947,16 +947,10 @@ describe("session-agent-loop", () => {
|
|
|
947
947
|
trustContext: null,
|
|
948
948
|
}),
|
|
949
949
|
);
|
|
950
|
-
|
|
951
|
-
.calls[0]![1] as {
|
|
952
|
-
diskPressureContext?: { cleanupModeActive: boolean } | null;
|
|
953
|
-
};
|
|
954
|
-
expect(firstInjectionOptions.diskPressureContext).toEqual({
|
|
955
|
-
cleanupModeActive: true,
|
|
956
|
-
});
|
|
950
|
+
expect(cleanupFlagDuringInjection()).toEqual([true]);
|
|
957
951
|
});
|
|
958
952
|
|
|
959
|
-
test("keeps cleanup
|
|
953
|
+
test("keeps the cleanup-mode flag set across overflow recovery reinjection", async () => {
|
|
960
954
|
mockDiskPressureDecision = {
|
|
961
955
|
action: "allow-cleanup-mode",
|
|
962
956
|
reason: "guardian",
|
|
@@ -976,20 +970,16 @@ describe("session-agent-loop", () => {
|
|
|
976
970
|
trustContext: {
|
|
977
971
|
sourceChannel: "telegram",
|
|
978
972
|
trustClass: "guardian",
|
|
979
|
-
} as
|
|
973
|
+
} as Conversation["trustContext"],
|
|
980
974
|
});
|
|
975
|
+
const cleanupFlagDuringInjection = captureCleanupFlagDuringInjection(ctx);
|
|
981
976
|
|
|
982
977
|
await runAgentLoopImpl(ctx, "free up space", "msg-1", () => {});
|
|
983
978
|
|
|
984
979
|
expect(applyRuntimeInjectionsMock.mock.calls.length).toBeGreaterThan(1);
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
};
|
|
989
|
-
expect(options.diskPressureContext).toEqual({
|
|
990
|
-
cleanupModeActive: true,
|
|
991
|
-
});
|
|
992
|
-
}
|
|
980
|
+
const flags = cleanupFlagDuringInjection();
|
|
981
|
+
expect(flags.length).toBeGreaterThan(1);
|
|
982
|
+
expect(flags.every((flag) => flag === true)).toBe(true);
|
|
993
983
|
});
|
|
994
984
|
|
|
995
985
|
test("blocks policy-denied turns before runtime injection or model execution", async () => {
|
|
@@ -1008,7 +998,7 @@ describe("session-agent-loop", () => {
|
|
|
1008
998
|
emit: (...args: unknown[]) => {
|
|
1009
999
|
traceEvents.push(args);
|
|
1010
1000
|
},
|
|
1011
|
-
} as unknown as
|
|
1001
|
+
} as unknown as Conversation["traceEmitter"],
|
|
1012
1002
|
});
|
|
1013
1003
|
const runSpy = spyOn(ctx.agentLoop, "run");
|
|
1014
1004
|
|
|
@@ -1169,7 +1159,7 @@ describe("session-agent-loop", () => {
|
|
|
1169
1159
|
usage: { inputTokens: 0, outputTokens: 0 },
|
|
1170
1160
|
stopReason: "end_turn",
|
|
1171
1161
|
}),
|
|
1172
|
-
} as unknown as
|
|
1162
|
+
} as unknown as Conversation["provider"],
|
|
1173
1163
|
});
|
|
1174
1164
|
|
|
1175
1165
|
await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
|
|
@@ -1218,7 +1208,7 @@ describe("session-agent-loop", () => {
|
|
|
1218
1208
|
usage: { inputTokens: 0, outputTokens: 0 },
|
|
1219
1209
|
stopReason: "end_turn",
|
|
1220
1210
|
}),
|
|
1221
|
-
} as unknown as
|
|
1211
|
+
} as unknown as Conversation["provider"],
|
|
1222
1212
|
});
|
|
1223
1213
|
|
|
1224
1214
|
await runAgentLoopImpl(ctx, "hello", "msg-1", () => {});
|
|
@@ -1284,7 +1274,7 @@ describe("session-agent-loop", () => {
|
|
|
1284
1274
|
usage: { inputTokens: 0, outputTokens: 0 },
|
|
1285
1275
|
stopReason: "end_turn",
|
|
1286
1276
|
}),
|
|
1287
|
-
} as unknown as
|
|
1277
|
+
} as unknown as Conversation["provider"],
|
|
1288
1278
|
});
|
|
1289
1279
|
|
|
1290
1280
|
await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
|
|
@@ -1335,7 +1325,7 @@ describe("session-agent-loop", () => {
|
|
|
1335
1325
|
usage: { inputTokens: 0, outputTokens: 0 },
|
|
1336
1326
|
stopReason: "end_turn",
|
|
1337
1327
|
}),
|
|
1338
|
-
} as unknown as
|
|
1328
|
+
} as unknown as Conversation["provider"],
|
|
1339
1329
|
traceEmitter: {
|
|
1340
1330
|
emit: (
|
|
1341
1331
|
event: string,
|
|
@@ -1346,7 +1336,7 @@ describe("session-agent-loop", () => {
|
|
|
1346
1336
|
traceEvents.push({ label, attrs: payload.attributes ?? {} });
|
|
1347
1337
|
}
|
|
1348
1338
|
},
|
|
1349
|
-
} as unknown as
|
|
1339
|
+
} as unknown as Conversation["traceEmitter"],
|
|
1350
1340
|
});
|
|
1351
1341
|
|
|
1352
1342
|
await runAgentLoopImpl(ctx, "hello", "msg-1", () => {});
|
|
@@ -1393,7 +1383,7 @@ describe("session-agent-loop", () => {
|
|
|
1393
1383
|
usage: { inputTokens: 0, outputTokens: 0 },
|
|
1394
1384
|
stopReason: "end_turn",
|
|
1395
1385
|
}),
|
|
1396
|
-
} as unknown as
|
|
1386
|
+
} as unknown as Conversation["provider"],
|
|
1397
1387
|
traceEmitter: {
|
|
1398
1388
|
emit: (
|
|
1399
1389
|
event: string,
|
|
@@ -1404,7 +1394,7 @@ describe("session-agent-loop", () => {
|
|
|
1404
1394
|
traceEvents.push({ label, attrs: payload.attributes ?? {} });
|
|
1405
1395
|
}
|
|
1406
1396
|
},
|
|
1407
|
-
} as unknown as
|
|
1397
|
+
} as unknown as Conversation["traceEmitter"],
|
|
1408
1398
|
});
|
|
1409
1399
|
|
|
1410
1400
|
await runAgentLoopImpl(ctx, "hello", "msg-1", () => {});
|
|
@@ -1460,7 +1450,7 @@ describe("session-agent-loop", () => {
|
|
|
1460
1450
|
usage: { inputTokens: 0, outputTokens: 0 },
|
|
1461
1451
|
stopReason: "end_turn",
|
|
1462
1452
|
}),
|
|
1463
|
-
} as unknown as
|
|
1453
|
+
} as unknown as Conversation["provider"],
|
|
1464
1454
|
});
|
|
1465
1455
|
|
|
1466
1456
|
await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
|
|
@@ -1521,9 +1511,21 @@ describe("session-agent-loop", () => {
|
|
|
1521
1511
|
},
|
|
1522
1512
|
});
|
|
1523
1513
|
|
|
1524
|
-
//
|
|
1525
|
-
// the turn
|
|
1526
|
-
|
|
1514
|
+
// The provider rejects the first call as too large; the convergence
|
|
1515
|
+
// reducer compacts and the rerun completes the turn, forwarding the
|
|
1516
|
+
// compaction's cache-aware usage to recordUsage.
|
|
1517
|
+
const { provider } = createMockProvider([
|
|
1518
|
+
new Error("context_length_exceeded"),
|
|
1519
|
+
textResponse("recovered"),
|
|
1520
|
+
]);
|
|
1521
|
+
const ctx = makeCtx({
|
|
1522
|
+
loopProvider: provider,
|
|
1523
|
+
contextWindowManager: {
|
|
1524
|
+
updateConfig: () => {},
|
|
1525
|
+
shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
|
|
1526
|
+
maybeCompact: async () => ({ compacted: false }),
|
|
1527
|
+
} as unknown as Conversation["contextWindowManager"],
|
|
1528
|
+
});
|
|
1527
1529
|
await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
|
|
1528
1530
|
|
|
1529
1531
|
const compactorCall = recordUsageMock.mock.calls.find(
|
|
@@ -1605,9 +1607,10 @@ describe("session-agent-loop", () => {
|
|
|
1605
1607
|
const ctx = makeCtx({
|
|
1606
1608
|
loopProvider: provider,
|
|
1607
1609
|
contextWindowManager: {
|
|
1610
|
+
updateConfig: () => {},
|
|
1608
1611
|
shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
|
|
1609
1612
|
maybeCompact: async () => ({ compacted: false }),
|
|
1610
|
-
} as unknown as
|
|
1613
|
+
} as unknown as Conversation["contextWindowManager"],
|
|
1611
1614
|
});
|
|
1612
1615
|
|
|
1613
1616
|
await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
|
|
@@ -1626,6 +1629,7 @@ describe("session-agent-loop", () => {
|
|
|
1626
1629
|
const ctx = makeCtx({
|
|
1627
1630
|
providerResponses: [new Error("context_length_exceeded")],
|
|
1628
1631
|
contextWindowManager: {
|
|
1632
|
+
updateConfig: () => {},
|
|
1629
1633
|
shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
|
|
1630
1634
|
// Compaction succeeds but context is still too large
|
|
1631
1635
|
maybeCompact: async () => ({
|
|
@@ -1645,7 +1649,7 @@ describe("session-agent-loop", () => {
|
|
|
1645
1649
|
summaryOutputTokens: 200,
|
|
1646
1650
|
summaryModel: "mock-model",
|
|
1647
1651
|
}),
|
|
1648
|
-
} as unknown as
|
|
1652
|
+
} as unknown as Conversation["contextWindowManager"],
|
|
1649
1653
|
});
|
|
1650
1654
|
|
|
1651
1655
|
await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
|
|
@@ -1685,9 +1689,10 @@ describe("session-agent-loop", () => {
|
|
|
1685
1689
|
const ctx = makeCtx({
|
|
1686
1690
|
loopProvider: provider,
|
|
1687
1691
|
contextWindowManager: {
|
|
1692
|
+
updateConfig: () => {},
|
|
1688
1693
|
shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
|
|
1689
1694
|
maybeCompact: async () => ({ compacted: false }),
|
|
1690
|
-
} as unknown as
|
|
1695
|
+
} as unknown as Conversation["contextWindowManager"],
|
|
1691
1696
|
});
|
|
1692
1697
|
|
|
1693
1698
|
await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
|
|
@@ -1705,27 +1710,31 @@ describe("session-agent-loop", () => {
|
|
|
1705
1710
|
test("non-interactive auto-compress continues without approval prompt", async () => {
|
|
1706
1711
|
const events: ServerMessage[] = [];
|
|
1707
1712
|
|
|
1708
|
-
//
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1713
|
+
// The ladder applies a middle tier first, then escalates to its terminal
|
|
1714
|
+
// auto-compress-latest-turn rung (permitted by the overflow policy) and
|
|
1715
|
+
// reports exhaustion — mirroring the reducer owning every rung.
|
|
1716
|
+
let reducerCalls = 0;
|
|
1717
|
+
mockReducerStepFn = (msgs: Message[]) => {
|
|
1718
|
+
reducerCalls++;
|
|
1719
|
+
const terminal = reducerCalls >= 2;
|
|
1720
|
+
return {
|
|
1721
|
+
messages: msgs,
|
|
1722
|
+
tier: terminal ? "auto_compress_latest_turn" : "forced_compaction",
|
|
1723
|
+
state: {
|
|
1724
|
+
appliedTiers: terminal
|
|
1725
|
+
? ["forced_compaction", "auto_compress_latest_turn"]
|
|
1726
|
+
: ["forced_compaction"],
|
|
1727
|
+
injectionMode: "full",
|
|
1728
|
+
exhausted: terminal,
|
|
1729
|
+
},
|
|
1730
|
+
estimatedTokens: 120000,
|
|
1731
|
+
};
|
|
1732
|
+
};
|
|
1724
1733
|
|
|
1725
1734
|
mockOverflowAction = "auto_compress_latest_turn";
|
|
1726
1735
|
|
|
1727
1736
|
// The provider rejects the first two calls with context-too-large errors,
|
|
1728
|
-
// then succeeds after the
|
|
1737
|
+
// then succeeds after the terminal auto-compress rung runs.
|
|
1729
1738
|
const ctx = makeCtx({
|
|
1730
1739
|
providerResponses: [
|
|
1731
1740
|
new Error("context_length_exceeded"),
|
|
@@ -1734,6 +1743,7 @@ describe("session-agent-loop", () => {
|
|
|
1734
1743
|
],
|
|
1735
1744
|
hasNoClient: true,
|
|
1736
1745
|
contextWindowManager: {
|
|
1746
|
+
updateConfig: () => {},
|
|
1737
1747
|
shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
|
|
1738
1748
|
maybeCompact: async () => ({
|
|
1739
1749
|
compacted: true,
|
|
@@ -1752,7 +1762,7 @@ describe("session-agent-loop", () => {
|
|
|
1752
1762
|
summaryOutputTokens: 100,
|
|
1753
1763
|
summaryModel: "mock-model",
|
|
1754
1764
|
}),
|
|
1755
|
-
} as unknown as
|
|
1765
|
+
} as unknown as Conversation["contextWindowManager"],
|
|
1756
1766
|
});
|
|
1757
1767
|
|
|
1758
1768
|
await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
|
|
@@ -1779,24 +1789,26 @@ describe("session-agent-loop", () => {
|
|
|
1779
1789
|
// attribute the silent stall.
|
|
1780
1790
|
const events: ServerMessage[] = [];
|
|
1781
1791
|
|
|
1782
|
-
//
|
|
1783
|
-
//
|
|
1784
|
-
// the
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1792
|
+
// The ladder applies a middle tier, then runs its terminal
|
|
1793
|
+
// auto-compress-latest-turn rung and reports exhaustion. Its rerun is the
|
|
1794
|
+
// one that still yields at the mid-loop budget checkpoint.
|
|
1795
|
+
let reducerCalls = 0;
|
|
1796
|
+
mockReducerStepFn = (msgs: Message[]) => {
|
|
1797
|
+
reducerCalls++;
|
|
1798
|
+
const terminal = reducerCalls >= 2;
|
|
1799
|
+
return {
|
|
1800
|
+
messages: msgs,
|
|
1801
|
+
tier: terminal ? "auto_compress_latest_turn" : "forced_compaction",
|
|
1802
|
+
state: {
|
|
1803
|
+
appliedTiers: terminal
|
|
1804
|
+
? ["forced_compaction", "auto_compress_latest_turn"]
|
|
1805
|
+
: ["forced_compaction"],
|
|
1806
|
+
injectionMode: "full",
|
|
1807
|
+
exhausted: terminal,
|
|
1808
|
+
},
|
|
1809
|
+
estimatedTokens: 120_000,
|
|
1810
|
+
};
|
|
1811
|
+
};
|
|
1800
1812
|
|
|
1801
1813
|
mockOverflowAction = "auto_compress_latest_turn";
|
|
1802
1814
|
|
|
@@ -1834,6 +1846,7 @@ describe("session-agent-loop", () => {
|
|
|
1834
1846
|
toolExecutor: async () => ({ content: "data", isError: false }),
|
|
1835
1847
|
hasNoClient: true,
|
|
1836
1848
|
contextWindowManager: {
|
|
1849
|
+
updateConfig: () => {},
|
|
1837
1850
|
shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
|
|
1838
1851
|
maybeCompact: async () => ({
|
|
1839
1852
|
compacted: true,
|
|
@@ -1852,7 +1865,7 @@ describe("session-agent-loop", () => {
|
|
|
1852
1865
|
summaryOutputTokens: 100,
|
|
1853
1866
|
summaryModel: "mock-model",
|
|
1854
1867
|
}),
|
|
1855
|
-
} as unknown as
|
|
1868
|
+
} as unknown as Conversation["contextWindowManager"],
|
|
1856
1869
|
});
|
|
1857
1870
|
|
|
1858
1871
|
await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
|
|
@@ -1919,57 +1932,18 @@ describe("session-agent-loop", () => {
|
|
|
1919
1932
|
const ctx = makeCtx({
|
|
1920
1933
|
providerResponses: [new Error("context_length_exceeded")],
|
|
1921
1934
|
contextWindowManager: {
|
|
1935
|
+
updateConfig: () => {},
|
|
1922
1936
|
shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
|
|
1923
1937
|
maybeCompact: async () => ({ compacted: false }),
|
|
1924
|
-
} as unknown as
|
|
1925
|
-
});
|
|
1926
|
-
|
|
1927
|
-
await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
|
|
1928
|
-
|
|
1929
|
-
// maxAttempts is 3 — reducer should be called at most 3 times
|
|
1930
|
-
expect(reducerCalls).toBeLessThanOrEqual(3);
|
|
1931
|
-
});
|
|
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"],
|
|
1938
|
+
} as unknown as Conversation["contextWindowManager"],
|
|
1963
1939
|
});
|
|
1964
1940
|
|
|
1965
1941
|
await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
|
|
1966
1942
|
|
|
1967
|
-
//
|
|
1968
|
-
|
|
1969
|
-
//
|
|
1970
|
-
expect(
|
|
1971
|
-
const complete = events.find((e) => e.type === "message_complete");
|
|
1972
|
-
expect(complete).toBeDefined();
|
|
1943
|
+
// A misbehaving reducer that never reports exhaustion is bounded by the
|
|
1944
|
+
// driver's safety backstop: at most the middle-tier budget plus the
|
|
1945
|
+
// emergency and terminal rungs (maxAttempts + 2 = 5).
|
|
1946
|
+
expect(reducerCalls).toBeLessThanOrEqual(5);
|
|
1973
1947
|
});
|
|
1974
1948
|
});
|
|
1975
1949
|
|
|
@@ -2024,7 +1998,7 @@ describe("session-agent-loop", () => {
|
|
|
2024
1998
|
],
|
|
2025
1999
|
toolExecutor: async () => ({ content: "file content", isError: false }),
|
|
2026
2000
|
canHandoffAtCheckpoint: () => true,
|
|
2027
|
-
} as unknown as Partial<
|
|
2001
|
+
} as unknown as Partial<Conversation>);
|
|
2028
2002
|
|
|
2029
2003
|
await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
|
|
2030
2004
|
|
|
@@ -2055,7 +2029,7 @@ describe("session-agent-loop", () => {
|
|
|
2055
2029
|
],
|
|
2056
2030
|
toolExecutor: async () => ({ content: "content", isError: false }),
|
|
2057
2031
|
canHandoffAtCheckpoint: () => false,
|
|
2058
|
-
} as unknown as Partial<
|
|
2032
|
+
} as unknown as Partial<Conversation>);
|
|
2059
2033
|
|
|
2060
2034
|
await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
|
|
2061
2035
|
|
|
@@ -2206,7 +2180,7 @@ describe("session-agent-loop", () => {
|
|
|
2206
2180
|
drainQueue: (reason: string) => {
|
|
2207
2181
|
drainReason = reason;
|
|
2208
2182
|
},
|
|
2209
|
-
} as unknown as Partial<
|
|
2183
|
+
} as unknown as Partial<Conversation>);
|
|
2210
2184
|
|
|
2211
2185
|
// WHEN the orchestrator runs the turn to completion
|
|
2212
2186
|
await runAgentLoopImpl(ctx, "hi", "msg-1", () => {});
|
|
@@ -2648,9 +2622,10 @@ describe("session-agent-loop", () => {
|
|
|
2648
2622
|
textResponse("retry succeeded"),
|
|
2649
2623
|
],
|
|
2650
2624
|
contextWindowManager: {
|
|
2625
|
+
updateConfig: () => {},
|
|
2651
2626
|
shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
|
|
2652
2627
|
maybeCompact: async () => ({ compacted: false }),
|
|
2653
|
-
} as unknown as
|
|
2628
|
+
} as unknown as Conversation["contextWindowManager"],
|
|
2654
2629
|
});
|
|
2655
2630
|
await runAgentLoopImpl(ctx, "hi", "msg-1", () => {});
|
|
2656
2631
|
|
|
@@ -3112,7 +3087,7 @@ describe("session-agent-loop", () => {
|
|
|
3112
3087
|
});
|
|
3113
3088
|
|
|
3114
3089
|
describe("Slack compaction watermarks", () => {
|
|
3115
|
-
test("
|
|
3090
|
+
test("first-call compaction runs in the loop and drops the Slack watermark", async () => {
|
|
3116
3091
|
const renderedSlackMessages: Message[] = [
|
|
3117
3092
|
{
|
|
3118
3093
|
role: "user",
|
|
@@ -3140,9 +3115,12 @@ describe("session-agent-loop", () => {
|
|
|
3140
3115
|
})),
|
|
3141
3116
|
compactableStartIndex: 0,
|
|
3142
3117
|
};
|
|
3143
|
-
const shouldCompactInputs: Message[][] = [];
|
|
3144
3118
|
const maybeCompactInputs: Message[][] = [];
|
|
3145
3119
|
|
|
3120
|
+
// Sits above the loop's first-call gate threshold (~80.75k), so the
|
|
3121
|
+
// loop's first-call gate owns the turn-start compaction.
|
|
3122
|
+
mockEstimateTokens = 90_000;
|
|
3123
|
+
|
|
3146
3124
|
const ctx = makeCtx({
|
|
3147
3125
|
channelCapabilities: {
|
|
3148
3126
|
channel: "slack",
|
|
@@ -3154,18 +3132,19 @@ describe("session-agent-loop", () => {
|
|
|
3154
3132
|
trustContext: {
|
|
3155
3133
|
sourceChannel: "slack",
|
|
3156
3134
|
trustClass: "guardian",
|
|
3157
|
-
} as
|
|
3135
|
+
} as Conversation["trustContext"],
|
|
3158
3136
|
getTurnChannelContext: () => ({
|
|
3159
3137
|
userMessageChannel: "slack" as const,
|
|
3160
3138
|
assistantMessageChannel: "slack" as const,
|
|
3161
3139
|
}),
|
|
3162
3140
|
contextWindowManager: {
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
return { needed: true, estimatedTokens: 95_000 };
|
|
3166
|
-
},
|
|
3141
|
+
updateConfig: () => {},
|
|
3142
|
+
shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
|
|
3167
3143
|
maybeCompact: async (messages: Message[]) => {
|
|
3168
3144
|
maybeCompactInputs.push(messages);
|
|
3145
|
+
// Drop back under budget so the post-compaction provider call
|
|
3146
|
+
// proceeds.
|
|
3147
|
+
mockEstimateTokens = 1000;
|
|
3169
3148
|
return {
|
|
3170
3149
|
compacted: true,
|
|
3171
3150
|
messages: [
|
|
@@ -3173,10 +3152,10 @@ describe("session-agent-loop", () => {
|
|
|
3173
3152
|
role: "user",
|
|
3174
3153
|
content: [{ type: "text", text: "summary" }],
|
|
3175
3154
|
},
|
|
3176
|
-
messages[
|
|
3155
|
+
messages[messages.length - 1]!,
|
|
3177
3156
|
],
|
|
3178
3157
|
compactedPersistedMessages: 2,
|
|
3179
|
-
previousEstimatedInputTokens:
|
|
3158
|
+
previousEstimatedInputTokens: 90_000,
|
|
3180
3159
|
estimatedInputTokens: 5_000,
|
|
3181
3160
|
maxInputTokens: 100_000,
|
|
3182
3161
|
thresholdTokens: 80_000,
|
|
@@ -3189,292 +3168,24 @@ describe("session-agent-loop", () => {
|
|
|
3189
3168
|
summaryFailed: false,
|
|
3190
3169
|
};
|
|
3191
3170
|
},
|
|
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"],
|
|
3171
|
+
} as unknown as Conversation["contextWindowManager"],
|
|
3303
3172
|
});
|
|
3304
3173
|
|
|
3305
|
-
await runAgentLoopImpl(
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
);
|
|
3311
|
-
expect(updateConversationSlackContextWatermarkMock).toHaveBeenCalledWith(
|
|
3312
|
-
"test-conv",
|
|
3313
|
-
"1700000020.000000",
|
|
3314
|
-
expect.any(Number),
|
|
3174
|
+
await runAgentLoopImpl(
|
|
3175
|
+
ctx,
|
|
3176
|
+
"next reply",
|
|
3177
|
+
"user-msg-first-call",
|
|
3178
|
+
() => {},
|
|
3315
3179
|
);
|
|
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
|
-
|
|
3411
|
-
const ctx = makeCtx({
|
|
3412
|
-
channelCapabilities: {
|
|
3413
|
-
channel: "slack",
|
|
3414
|
-
dashboardCapable: false,
|
|
3415
|
-
supportsDynamicUi: false,
|
|
3416
|
-
supportsVoiceInput: false,
|
|
3417
|
-
chatType: "channel",
|
|
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
3180
|
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
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);
|
|
3181
|
+
// The loop compacts its own stripped working history, never the loaded
|
|
3182
|
+
// Slack transcript array, so provenance cannot be projected and the
|
|
3183
|
+
// watermark is left untouched — matching mid-loop compaction.
|
|
3184
|
+
expect(maybeCompactInputs.length).toBeGreaterThan(0);
|
|
3185
|
+
expect(maybeCompactInputs[0]).not.toBe(renderedSlackMessages);
|
|
3186
|
+
expect(
|
|
3187
|
+
updateConversationSlackContextWatermarkMock,
|
|
3188
|
+
).not.toHaveBeenCalled();
|
|
3478
3189
|
});
|
|
3479
3190
|
|
|
3480
3191
|
test("mid-loop Slack compaction does not persist watermark from mismatched loaded context", async () => {
|
|
@@ -3542,32 +3253,16 @@ describe("session-agent-loop", () => {
|
|
|
3542
3253
|
trustContext: {
|
|
3543
3254
|
sourceChannel: "slack",
|
|
3544
3255
|
trustClass: "guardian",
|
|
3545
|
-
} as
|
|
3256
|
+
} as Conversation["trustContext"],
|
|
3546
3257
|
getTurnChannelContext: () => ({
|
|
3547
3258
|
userMessageChannel: "slack" as const,
|
|
3548
3259
|
assistantMessageChannel: "slack" as const,
|
|
3549
3260
|
}),
|
|
3550
3261
|
contextWindowManager: {
|
|
3262
|
+
updateConfig: () => {},
|
|
3551
3263
|
shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
|
|
3552
3264
|
maybeCompact: async (messages: Message[]) => {
|
|
3553
3265
|
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
3266
|
// The mid-loop gate compacted its in-loop basis; drop the estimate
|
|
3572
3267
|
// back under budget so the post-compaction provider call proceeds.
|
|
3573
3268
|
mockEstimateTokens = 1000;
|
|
@@ -3593,15 +3288,16 @@ describe("session-agent-loop", () => {
|
|
|
3593
3288
|
summaryFailed: false,
|
|
3594
3289
|
};
|
|
3595
3290
|
},
|
|
3596
|
-
} as unknown as
|
|
3291
|
+
} as unknown as Conversation["contextWindowManager"],
|
|
3597
3292
|
});
|
|
3598
3293
|
|
|
3599
3294
|
await runAgentLoopImpl(ctx, "next reply", "user-msg-mid-loop", () => {});
|
|
3600
3295
|
|
|
3601
|
-
expect(maybeCompactInputs[0]).toBe(renderedSlackMessages);
|
|
3602
3296
|
// 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
|
-
|
|
3297
|
+
// loaded Slack rows — the mismatch this test guards against. No
|
|
3298
|
+
// start-of-turn compaction runs, so this is the turn's only compaction.
|
|
3299
|
+
expect(maybeCompactInputs).toHaveLength(1);
|
|
3300
|
+
expect(maybeCompactInputs[0]).not.toBe(renderedSlackMessages);
|
|
3605
3301
|
expect(getSlackCompactionWatermarkForPrefixMock).toHaveBeenCalledWith(
|
|
3606
3302
|
null,
|
|
3607
3303
|
2,
|
|
@@ -3611,7 +3307,7 @@ describe("session-agent-loop", () => {
|
|
|
3611
3307
|
).not.toHaveBeenCalled();
|
|
3612
3308
|
});
|
|
3613
3309
|
|
|
3614
|
-
test("next inbound Slack turn
|
|
3310
|
+
test("next inbound Slack turn loads chronological context using the persisted watermark", async () => {
|
|
3615
3311
|
mockConversationRow = {
|
|
3616
3312
|
...mockConversationRow,
|
|
3617
3313
|
contextSummary: "## Summary\n- compacted Slack context",
|
|
@@ -3671,7 +3367,7 @@ describe("session-agent-loop", () => {
|
|
|
3671
3367
|
trustContext: {
|
|
3672
3368
|
sourceChannel: "slack",
|
|
3673
3369
|
trustClass: "guardian",
|
|
3674
|
-
} as
|
|
3370
|
+
} as Conversation["trustContext"],
|
|
3675
3371
|
getTurnChannelContext: () => ({
|
|
3676
3372
|
userMessageChannel: "slack" as const,
|
|
3677
3373
|
assistantMessageChannel: "slack" as const,
|
|
@@ -3690,26 +3386,9 @@ describe("session-agent-loop", () => {
|
|
|
3690
3386
|
trustClass: "guardian",
|
|
3691
3387
|
}),
|
|
3692
3388
|
);
|
|
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 () => {
|
|
3389
|
+
});
|
|
3390
|
+
|
|
3391
|
+
test("subsequent Slack turn loads chronological context using the persisted long-thread watermark", async () => {
|
|
3713
3392
|
mockConversationRow = {
|
|
3714
3393
|
...mockConversationRow,
|
|
3715
3394
|
contextSummary: "## Summary\n- compacted long Slack thread",
|
|
@@ -3779,7 +3458,7 @@ describe("session-agent-loop", () => {
|
|
|
3779
3458
|
trustContext: {
|
|
3780
3459
|
sourceChannel: "slack",
|
|
3781
3460
|
trustClass: "guardian",
|
|
3782
|
-
} as
|
|
3461
|
+
} as Conversation["trustContext"],
|
|
3783
3462
|
getTurnChannelContext: () => ({
|
|
3784
3463
|
userMessageChannel: "slack" as const,
|
|
3785
3464
|
assistantMessageChannel: "slack" as const,
|
|
@@ -3802,21 +3481,6 @@ describe("session-agent-loop", () => {
|
|
|
3802
3481
|
slackContextCompactionWatermarkTs: "1700000080.000000",
|
|
3803
3482
|
}),
|
|
3804
3483
|
);
|
|
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
3484
|
});
|
|
3821
3485
|
|
|
3822
3486
|
test("applyCompactionResult records Slack timestamp watermark when provided", async () => {
|
|
@@ -3858,6 +3522,59 @@ describe("session-agent-loop", () => {
|
|
|
3858
3522
|
true,
|
|
3859
3523
|
);
|
|
3860
3524
|
});
|
|
3525
|
+
|
|
3526
|
+
test("applyCompactionResult advances the persisted count from the trusted in-context boundary", async () => {
|
|
3527
|
+
// Trusted views slice past the already-compacted prefix, so a further
|
|
3528
|
+
// compaction advances the persisted count from the mirrored DB boundary.
|
|
3529
|
+
|
|
3530
|
+
// GIVEN a trusted (guardian) conversation that has already compacted 5
|
|
3531
|
+
// persisted messages, so its in-context history starts past that prefix
|
|
3532
|
+
const ctx = makeCtx({
|
|
3533
|
+
contextCompactedMessageCount: 5,
|
|
3534
|
+
trustContext: {
|
|
3535
|
+
trustClass: "guardian",
|
|
3536
|
+
} as Conversation["trustContext"],
|
|
3537
|
+
});
|
|
3538
|
+
|
|
3539
|
+
// WHEN a turn compacts 4 more in-context messages
|
|
3540
|
+
await applyCompactionResult(
|
|
3541
|
+
ctx,
|
|
3542
|
+
makeCompactionResult({ compactedPersistedMessages: 4 }),
|
|
3543
|
+
() => {},
|
|
3544
|
+
"req-1",
|
|
3545
|
+
);
|
|
3546
|
+
|
|
3547
|
+
// THEN the persisted count advances from the prior boundary (5 + 4)
|
|
3548
|
+
expect(ctx.contextCompactedMessageCount).toBe(9);
|
|
3549
|
+
});
|
|
3550
|
+
|
|
3551
|
+
test("applyCompactionResult resets the persisted count to the unsliced boundary for untrusted views", async () => {
|
|
3552
|
+
// Untrusted views render history unsliced (boundary 0), so a compaction
|
|
3553
|
+
// must record only the new summary's prefix instead of adding to the raw
|
|
3554
|
+
// mirror — otherwise future loads slice past unsummarized rows.
|
|
3555
|
+
|
|
3556
|
+
// GIVEN an untrusted view of a conversation whose raw DB count mirrors a
|
|
3557
|
+
// 5-message compacted prefix — but untrusted views render that history
|
|
3558
|
+
// unsliced (boundary 0), so the compactor operates on the full list
|
|
3559
|
+
const ctx = makeCtx({
|
|
3560
|
+
contextCompactedMessageCount: 5,
|
|
3561
|
+
trustContext: {
|
|
3562
|
+
trustClass: "unknown",
|
|
3563
|
+
} as Conversation["trustContext"],
|
|
3564
|
+
});
|
|
3565
|
+
|
|
3566
|
+
// WHEN that turn compacts 4 in-context messages
|
|
3567
|
+
await applyCompactionResult(
|
|
3568
|
+
ctx,
|
|
3569
|
+
makeCompactionResult({ compactedPersistedMessages: 4 }),
|
|
3570
|
+
() => {},
|
|
3571
|
+
"req-1",
|
|
3572
|
+
);
|
|
3573
|
+
|
|
3574
|
+
// THEN the persisted count reflects only the new summary's prefix (0 + 4)
|
|
3575
|
+
// rather than double-counting the raw mirror (which would yield 9)
|
|
3576
|
+
expect(ctx.contextCompactedMessageCount).toBe(4);
|
|
3577
|
+
});
|
|
3861
3578
|
});
|
|
3862
3579
|
|
|
3863
3580
|
describe("compaction-strip marker persistence", () => {
|
|
@@ -3894,9 +3611,10 @@ describe("session-agent-loop", () => {
|
|
|
3894
3611
|
],
|
|
3895
3612
|
toolExecutor: async () => ({ content: "ok", isError: false }),
|
|
3896
3613
|
contextWindowManager: {
|
|
3614
|
+
updateConfig: () => {},
|
|
3897
3615
|
shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
|
|
3898
3616
|
maybeCompact: async () => ({ compacted: false }),
|
|
3899
|
-
} as unknown as
|
|
3617
|
+
} as unknown as Conversation["contextWindowManager"],
|
|
3900
3618
|
});
|
|
3901
3619
|
|
|
3902
3620
|
// WHEN the orchestrator runs the turn to completion
|
|
@@ -3943,9 +3661,10 @@ describe("session-agent-loop", () => {
|
|
|
3943
3661
|
],
|
|
3944
3662
|
toolExecutor: async () => ({ content: "ok", isError: false }),
|
|
3945
3663
|
contextWindowManager: {
|
|
3664
|
+
updateConfig: () => {},
|
|
3946
3665
|
shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
|
|
3947
3666
|
maybeCompact: async () => ({ compacted: false }),
|
|
3948
|
-
} as unknown as
|
|
3667
|
+
} as unknown as Conversation["contextWindowManager"],
|
|
3949
3668
|
});
|
|
3950
3669
|
|
|
3951
3670
|
// Must not throw — the strip-site marker write is wrapped in try/catch.
|