@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
|
@@ -4,18 +4,15 @@
|
|
|
4
4
|
* This module contains the core agent loop orchestration: pre-flight
|
|
5
5
|
* setup, event handling, retry logic, history reconstruction, and
|
|
6
6
|
* completion event emission. The Conversation class delegates its
|
|
7
|
-
* runAgentLoop method here
|
|
7
|
+
* runAgentLoop method here, passing itself as the loop context.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { v4 as uuid } from "uuid";
|
|
11
11
|
|
|
12
|
-
import { optimizeImageForTransport } from "../agent/image-optimize.js";
|
|
13
12
|
import type {
|
|
14
13
|
AgentEvent,
|
|
15
|
-
AgentLoop,
|
|
16
14
|
AgentLoopExitReason,
|
|
17
15
|
CheckpointDecision,
|
|
18
|
-
MidLoopCompaction,
|
|
19
16
|
} from "../agent/loop.js";
|
|
20
17
|
import { createAssistantMessage } from "../agent/message-types.js";
|
|
21
18
|
import type {
|
|
@@ -24,7 +21,6 @@ import type {
|
|
|
24
21
|
TurnChannelContext,
|
|
25
22
|
TurnInterfaceContext,
|
|
26
23
|
} from "../channels/types.js";
|
|
27
|
-
import { isAssistantFeatureFlagEnabled } from "../config/assistant-feature-flags.js";
|
|
28
24
|
import {
|
|
29
25
|
contextWindowConfigFromEffective,
|
|
30
26
|
type EffectiveContextWindow,
|
|
@@ -37,18 +33,14 @@ import {
|
|
|
37
33
|
import { getConfig } from "../config/loader.js";
|
|
38
34
|
import type { LLMCallSite } from "../config/schemas/llm.js";
|
|
39
35
|
import type { ContextWindowConfig } from "../config/types.js";
|
|
40
|
-
import { runEmergencyCompaction } from "../context/compactor.js";
|
|
41
36
|
import {
|
|
42
37
|
derefToolResultReReads,
|
|
43
38
|
postTurnTruncateToolResults,
|
|
44
39
|
} from "../context/post-turn-tool-result-truncation.js";
|
|
45
40
|
import {
|
|
46
41
|
estimatePromptTokens,
|
|
47
|
-
estimatePromptTokensWithTools,
|
|
48
42
|
getCalibrationProviderKey,
|
|
49
43
|
} from "../context/token-estimator.js";
|
|
50
|
-
import type { ContextWindowManager } from "../context/window-manager.js";
|
|
51
|
-
import type { ToolProfiler } from "../events/tool-profiling-listener.js";
|
|
52
44
|
import { writeRelationshipState } from "../home/relationship-state-writer.js";
|
|
53
45
|
import {
|
|
54
46
|
clearSentryConversationContext,
|
|
@@ -62,66 +54,48 @@ import {
|
|
|
62
54
|
getConversation,
|
|
63
55
|
getConversationOriginChannel,
|
|
64
56
|
getConversationOriginInterface,
|
|
65
|
-
getConversationOverrideProfileFromRow,
|
|
66
57
|
getLastUserTimestampBefore,
|
|
67
58
|
getMessageById,
|
|
68
59
|
provenanceFromTrustContext,
|
|
60
|
+
resolveOverrideProfile,
|
|
69
61
|
updateConversationContextWindow,
|
|
70
62
|
updateConversationSlackContextWatermark,
|
|
71
|
-
updateMessageMetadata,
|
|
72
63
|
} from "../memory/conversation-crud.js";
|
|
73
64
|
import { getResolvedConversationDirPath } from "../memory/conversation-directories.js";
|
|
74
65
|
import { syncMessageToDisk } from "../memory/conversation-disk-view.js";
|
|
75
66
|
import { isReplaceableTitle } from "../memory/conversation-title-service.js";
|
|
76
|
-
import { isBackgroundConversationType } from "../memory/conversation-types.js";
|
|
77
67
|
import type { ConversationGraphMemory } from "../memory/graph/conversation-graph-memory.js";
|
|
78
68
|
import {
|
|
79
69
|
backfillMessageIdOnLogs,
|
|
80
70
|
recordSyntheticAgentErrorMessageLog,
|
|
81
71
|
} from "../memory/llm-request-log-store.js";
|
|
82
72
|
import { enqueueMemoryRetrospectiveOnCompaction } from "../memory/memory-retrospective-enqueue.js";
|
|
83
|
-
import type { PermissionPrompter } from "../permissions/prompter.js";
|
|
84
73
|
import { HOOKS } from "../plugin-api/constants.js";
|
|
85
74
|
import type { UserPromptSubmitContext } from "../plugin-api/types.js";
|
|
86
|
-
import {
|
|
75
|
+
import {
|
|
76
|
+
createInitialReducerState,
|
|
77
|
+
reduceContextOverflow,
|
|
78
|
+
type ReducerState,
|
|
79
|
+
} from "../plugins/defaults/compaction/context-overflow-reducer.js";
|
|
80
|
+
import { computeCorrectedOverflowTarget } from "../plugins/defaults/compaction/corrected-target.js";
|
|
87
81
|
import { deepRepairHistory } from "../plugins/defaults/history-repair/terminal.js";
|
|
88
|
-
import
|
|
89
|
-
import userPromptSubmitMemoryRetrieval, {
|
|
90
|
-
type MemoryRetrievalHookContext,
|
|
91
|
-
} from "../plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.js";
|
|
92
|
-
import { DEFAULT_TIMEOUTS, runHook, runPipeline } from "../plugins/pipeline.js";
|
|
93
|
-
import { getMiddlewaresFor } from "../plugins/registry.js";
|
|
94
|
-
import type {
|
|
95
|
-
CompactionArgs,
|
|
96
|
-
CompactionResult,
|
|
97
|
-
OverflowReduceArgs,
|
|
98
|
-
OverflowReduceResult,
|
|
99
|
-
TurnContext as PluginTurnContext,
|
|
100
|
-
} from "../plugins/types.js";
|
|
101
|
-
import { PluginExecutionError, PluginTimeoutError } from "../plugins/types.js";
|
|
82
|
+
import { runHook } from "../plugins/pipeline.js";
|
|
102
83
|
import type { ContentBlock, Message } from "../providers/types.js";
|
|
103
84
|
import type { Provider } from "../providers/types.js";
|
|
104
|
-
import {
|
|
85
|
+
import { isUntrustedTrustClass } from "../runtime/actor-trust-resolver.js";
|
|
105
86
|
import { broadcastMessage } from "../runtime/assistant-event-hub.js";
|
|
106
87
|
import { DAEMON_INTERNAL_ASSISTANT_ID } from "../runtime/assistant-scope.js";
|
|
107
88
|
import { publishConversationMessagesChanged } from "../runtime/sync/resource-sync-events.js";
|
|
108
|
-
import {
|
|
89
|
+
import type { ActivationMomentParam } from "../telemetry/activation-funnel.js";
|
|
109
90
|
import type { UsageActor } from "../usage/actors.js";
|
|
110
91
|
import { getLogger } from "../util/logger.js";
|
|
111
92
|
import { timeAgo } from "../util/time.js";
|
|
112
93
|
import { truncate } from "../util/truncate.js";
|
|
113
94
|
import { getWorkspaceGitService } from "../workspace/git-service.js";
|
|
114
95
|
import { commitTurnChanges } from "../workspace/turn-commit.js";
|
|
115
|
-
import {
|
|
116
|
-
type AssistantAttachmentDraft,
|
|
117
|
-
cleanAssistantContent,
|
|
118
|
-
} from "./assistant-attachments.js";
|
|
96
|
+
import { cleanAssistantContent } from "./assistant-attachments.js";
|
|
119
97
|
import { resolveOverflowAction } from "./context-overflow-policy.js";
|
|
120
|
-
import {
|
|
121
|
-
createInitialReducerState,
|
|
122
|
-
reduceContextOverflow,
|
|
123
|
-
type ReducerState,
|
|
124
|
-
} from "./context-overflow-reducer.js";
|
|
98
|
+
import type { Conversation } from "./conversation.js";
|
|
125
99
|
import {
|
|
126
100
|
createEventHandlerState,
|
|
127
101
|
dispatchAgentEvent,
|
|
@@ -140,35 +114,19 @@ import {
|
|
|
140
114
|
isUserCancellation,
|
|
141
115
|
} from "./conversation-error.js";
|
|
142
116
|
import { raceWithTimeout } from "./conversation-media-retry.js";
|
|
143
|
-
import type {
|
|
144
|
-
import type { QueueDrainReason } from "./conversation-queue-manager.js";
|
|
145
|
-
import type {
|
|
146
|
-
ChannelCapabilities,
|
|
147
|
-
InboundActorContext,
|
|
148
|
-
InjectionMode,
|
|
149
|
-
} from "./conversation-runtime-assembly.js";
|
|
117
|
+
import type { InjectionMode } from "./conversation-runtime-assembly.js";
|
|
150
118
|
import {
|
|
151
119
|
applyRuntimeInjections,
|
|
152
|
-
buildActiveDocuments,
|
|
153
|
-
buildActiveSurfaceContext,
|
|
154
|
-
buildSubagentStatusBlock,
|
|
155
|
-
buildUnifiedTurnContextBlock,
|
|
156
|
-
buildWorkspaceTopLevelContext,
|
|
157
120
|
getSlackCompactionWatermarkForPrefix,
|
|
158
|
-
inboundActorContextFromTrust,
|
|
159
|
-
inboundActorContextFromTrustContext,
|
|
160
|
-
loadSlackActiveThreadFocusBlock,
|
|
161
121
|
loadSlackChronologicalContext,
|
|
122
|
+
resolveTurnInboundActorContext,
|
|
123
|
+
resolveTurnModelProfileLabel,
|
|
162
124
|
type SlackChronologicalContext,
|
|
163
125
|
stripInjectionsForCompaction,
|
|
164
126
|
} from "./conversation-runtime-assembly.js";
|
|
165
|
-
import type { SkillProjectionCache } from "./conversation-skill-tools.js";
|
|
166
127
|
import { markSurfaceCompleted } from "./conversation-surfaces.js";
|
|
167
128
|
import { recordUsage } from "./conversation-usage.js";
|
|
168
|
-
import {
|
|
169
|
-
formatTurnTimestamp,
|
|
170
|
-
resolveTurnTimezoneContext,
|
|
171
|
-
} from "./date-context.js";
|
|
129
|
+
import { resolveTurnTimezoneContext } from "./date-context.js";
|
|
172
130
|
import { getDiskPressureStatus } from "./disk-pressure-guard.js";
|
|
173
131
|
import { classifyDiskPressureTurnPolicy } from "./disk-pressure-policy.js";
|
|
174
132
|
import type {
|
|
@@ -177,15 +135,12 @@ import type {
|
|
|
177
135
|
SurfaceType,
|
|
178
136
|
UsageStats,
|
|
179
137
|
} from "./message-protocol.js";
|
|
180
|
-
import type { ConfirmationStateChanged } from "./message-types/messages.js";
|
|
181
138
|
import { parseActualTokensFromError } from "./parse-actual-tokens-from-error.js";
|
|
182
139
|
import {
|
|
140
|
+
oversizedImageReplacement,
|
|
183
141
|
persistUnsendableImageDowngrades,
|
|
184
|
-
UNSENDABLE_IMAGE_NOTE,
|
|
185
142
|
} from "./persist-unsendable-image.js";
|
|
186
|
-
import type { TraceEmitter } from "./trace-emitter.js";
|
|
187
143
|
import { resolveTrustClass, type TrustContext } from "./trust-context.js";
|
|
188
|
-
import { stripHistoricalWebSearchResults } from "./web-search-history.js";
|
|
189
144
|
|
|
190
145
|
const log = getLogger("conversation-agent-loop");
|
|
191
146
|
|
|
@@ -206,23 +161,42 @@ const TOOL_FRIENDLY_LABEL: Record<string, string> = {
|
|
|
206
161
|
skill_execute: "Run Skill Tool",
|
|
207
162
|
};
|
|
208
163
|
|
|
209
|
-
type GitServiceInitializer = {
|
|
210
|
-
ensureInitialized(): Promise<void>;
|
|
211
|
-
};
|
|
212
|
-
|
|
213
164
|
function formatDiskPressureBlockedMessage(): string {
|
|
214
165
|
return "Storage is critically low, so background processes are paused and remote messages are ignored until the guardian frees enough space. Remote senders should try again later.";
|
|
215
166
|
}
|
|
216
167
|
|
|
168
|
+
// ── Image-recovery helpers ───────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* True when a message's content holds an image the provider may have rejected
|
|
172
|
+
* for being oversized — either a top-level image block (user upload) or one
|
|
173
|
+
* nested inside a tool_result's contentBlocks (e.g. a browser screenshot).
|
|
174
|
+
*/
|
|
175
|
+
function messageHasImageBlock(content: ContentBlock[]): boolean {
|
|
176
|
+
return content.some(
|
|
177
|
+
(b) =>
|
|
178
|
+
b.type === "image" ||
|
|
179
|
+
(b.type === "tool_result" &&
|
|
180
|
+
(b.contentBlocks?.some((cb) => cb.type === "image") ?? false)),
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Replace an oversized image with its downscaled form or an unsendable note,
|
|
186
|
+
* leaving still-sendable images untouched. Delegates to the shared
|
|
187
|
+
* {@link oversizedImageReplacement} so the in-memory recovery and the durable
|
|
188
|
+
* persist pass apply the identical provider-cap gate.
|
|
189
|
+
*/
|
|
190
|
+
function recoverImageBlock(
|
|
191
|
+
block: Extract<ContentBlock, { type: "image" }>,
|
|
192
|
+
): ContentBlock {
|
|
193
|
+
return oversizedImageReplacement(block) ?? block;
|
|
194
|
+
}
|
|
195
|
+
|
|
217
196
|
// ── Plugin pipeline helpers ──────────────────────────────────────────
|
|
218
|
-
//
|
|
219
|
-
// Canonical {@link PluginTurnContext} builder threaded into every
|
|
220
|
-
// `runPipeline` call inside `runAgentLoopImpl`. The orchestrator composes
|
|
221
|
-
// the context on demand at each call site from ambient state rather than
|
|
222
|
-
// carrying a persistent `TurnContext` instance across the turn.
|
|
223
197
|
|
|
224
198
|
/**
|
|
225
|
-
* Synthetic fallback trust context used when the orchestrator fires a
|
|
199
|
+
* Synthetic fallback trust context used when the orchestrator fires a hook
|
|
226
200
|
* before the per-turn trust snapshot has been captured (e.g. invocations that
|
|
227
201
|
* bypass `processMessage` / `drainQueue`). We bias to `unknown` rather than
|
|
228
202
|
* `guardian` so a missing snapshot cannot accidentally grant elevated trust
|
|
@@ -233,65 +207,23 @@ const FALLBACK_TURN_TRUST: TrustContext = {
|
|
|
233
207
|
trustClass: "unknown",
|
|
234
208
|
};
|
|
235
209
|
|
|
236
|
-
/**
|
|
237
|
-
* Build the {@link TurnContext} passed to {@link runPipeline}.
|
|
238
|
-
*
|
|
239
|
-
* Canonical source of truth for every pipeline call site inside the agent
|
|
240
|
-
* loop. Every `runPipeline` invocation in `runAgentLoopImpl` (and in the
|
|
241
|
-
* handlers that share its ambient state) must route through this helper
|
|
242
|
-
* rather than constructing a `TurnContext` literal inline — this keeps
|
|
243
|
-
* `turnIndex`, trust resolution, and the `contextWindowManager` attachment
|
|
244
|
-
* consistent across pipeline slots, which in turn keeps structured logs
|
|
245
|
-
* filtered by `conversationId`/`turnIndex` coherent across slots.
|
|
246
|
-
*
|
|
247
|
-
* Behavior:
|
|
248
|
-
* - `turnIndex` is always `ctx.turnCount` — the orchestrator-owned
|
|
249
|
-
* 0-based turn counter. Reading from a single source avoids the
|
|
250
|
-
* earlier inconsistency (`ctx.turnCount`, `ctx.messages.length - 1`,
|
|
251
|
-
* `ctx.messages.length`, and `0` were all used for the same turn).
|
|
252
|
-
* - Trust pulls from the per-turn snapshot first, then the conversation-
|
|
253
|
-
* level context, then {@link FALLBACK_TURN_TRUST}. The cascade matches
|
|
254
|
-
* the one inside the orchestrator's inline injection assembly so
|
|
255
|
-
* middleware reads the same trust class the runtime sees.
|
|
256
|
-
* - `contextWindowManager` is attached unconditionally. Pipelines that
|
|
257
|
-
* don't need it can ignore it; the default compaction plugin reads it
|
|
258
|
-
* via the typed optional field on `TurnContext`.
|
|
259
|
-
*/
|
|
260
|
-
function buildPluginTurnContext(
|
|
261
|
-
ctx: AgentLoopConversationContext,
|
|
262
|
-
requestId: string,
|
|
263
|
-
): PluginTurnContext {
|
|
264
|
-
const trust =
|
|
265
|
-
ctx.currentTurnTrustContext ?? ctx.trustContext ?? FALLBACK_TURN_TRUST;
|
|
266
|
-
return {
|
|
267
|
-
requestId,
|
|
268
|
-
conversationId: ctx.conversationId,
|
|
269
|
-
turnIndex: ctx.turnCount,
|
|
270
|
-
trust,
|
|
271
|
-
contextWindowManager: ctx.contextWindowManager,
|
|
272
|
-
callSite: ctx.currentCallSite,
|
|
273
|
-
};
|
|
274
|
-
}
|
|
275
|
-
|
|
276
210
|
/**
|
|
277
211
|
* Trust class of the actor whose turn is in progress, for the compactor's
|
|
278
212
|
* image manifest filter. Prefers the turn-start snapshot
|
|
279
|
-
* ({@link
|
|
213
|
+
* ({@link Conversation.currentTurnTrustContext}) over the live
|
|
280
214
|
* trust context so compaction running in a later tool iteration can't pick up
|
|
281
215
|
* a concurrent request's actor.
|
|
282
216
|
*/
|
|
283
217
|
function resolveTurnActorTrustClass(
|
|
284
|
-
ctx:
|
|
218
|
+
ctx: Conversation,
|
|
285
219
|
): TrustContext["trustClass"] | undefined {
|
|
286
220
|
return (ctx.currentTurnTrustContext ?? ctx.trustContext)?.trustClass;
|
|
287
221
|
}
|
|
288
222
|
|
|
289
|
-
// ── Context Interface ────────────────────────────────────────────────
|
|
290
|
-
|
|
291
223
|
/**
|
|
292
224
|
* Per-surface entry tracked on the current turn. Inline shape kept stable so
|
|
293
225
|
* routes and persistence helpers can consume it via a named import instead of
|
|
294
|
-
* `infer`-extracting from {@link
|
|
226
|
+
* `infer`-extracting from {@link Conversation}.
|
|
295
227
|
*/
|
|
296
228
|
export interface AssistantSurface {
|
|
297
229
|
surfaceId: string;
|
|
@@ -308,182 +240,20 @@ export interface AssistantSurface {
|
|
|
308
240
|
persistent?: boolean;
|
|
309
241
|
/** Id of the tool call that produced this surface (the `ui_show` proxy tool). Persisted so app previews can gate on the tool result's arrival rather than whole-turn streaming state. */
|
|
310
242
|
toolCallId?: string;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
export interface AgentLoopConversationContext {
|
|
314
|
-
readonly conversationId: string;
|
|
315
|
-
messages: Message[];
|
|
316
|
-
isProcessing(): boolean;
|
|
317
|
-
setProcessing(value: boolean): void;
|
|
318
|
-
abortController: AbortController | null;
|
|
319
|
-
currentRequestId?: string;
|
|
320
|
-
/**
|
|
321
|
-
* The {@link LLMCallSite} of the in-flight turn, set at turn start from
|
|
322
|
-
* `options?.callSite ?? "mainAgent"`. Read by {@link buildPluginTurnContext}
|
|
323
|
-
* so pipeline/injector plugins can tell the main reply apart from
|
|
324
|
-
* background agent-loop work (compaction, subagents, …) on this same
|
|
325
|
-
* conversation. Per-turn mutable, mirroring {@link currentRequestId}.
|
|
326
|
-
*/
|
|
327
|
-
currentCallSite?: LLMCallSite;
|
|
328
|
-
|
|
329
|
-
readonly agentLoop: AgentLoop;
|
|
330
|
-
readonly provider: Provider;
|
|
331
|
-
readonly systemPrompt: string;
|
|
332
|
-
|
|
333
|
-
readonly contextWindowManager: ContextWindowManager;
|
|
334
|
-
contextCompactedMessageCount: number;
|
|
335
|
-
contextCompactedAt: number | null;
|
|
336
|
-
/**
|
|
337
|
-
* Set by `applyCompactionResult` when compaction strips runtime injections
|
|
338
|
-
* from the preserved tail. The next agent loop turn promotes this into a
|
|
339
|
-
* `compactedThisTurn` signal so NOW.md, PKB, and the v2 static block are
|
|
340
|
-
* re-injected on the first turn following `/compact` (which runs outside
|
|
341
|
-
* the agent loop and so has no other way to surface that compaction
|
|
342
|
-
* happened just before this turn).
|
|
343
|
-
*/
|
|
344
|
-
pendingPostCompactReinject: boolean;
|
|
345
|
-
|
|
346
|
-
readonly graphMemory: ConversationGraphMemory;
|
|
347
|
-
|
|
348
|
-
currentActiveSurfaceId?: string;
|
|
349
|
-
currentPage?: string;
|
|
350
|
-
readonly surfaceState: Map<
|
|
351
|
-
string,
|
|
352
|
-
{
|
|
353
|
-
surfaceType: SurfaceType;
|
|
354
|
-
data: SurfaceData;
|
|
355
|
-
title?: string;
|
|
356
|
-
actions?: Array<{
|
|
357
|
-
id: string;
|
|
358
|
-
label: string;
|
|
359
|
-
style?: string;
|
|
360
|
-
data?: Record<string, unknown>;
|
|
361
|
-
}>;
|
|
362
|
-
}
|
|
363
|
-
>;
|
|
364
|
-
pendingSurfaceActions: Map<string, { surfaceType: SurfaceType }>;
|
|
365
|
-
surfaceActionRequestIds: Set<string>;
|
|
366
|
-
approvedViaPromptThisTurn?: boolean;
|
|
367
|
-
currentTurnSurfaces: AssistantSurface[];
|
|
368
|
-
|
|
369
|
-
workingDir: string;
|
|
370
|
-
workspaceTopLevelContext: string | null;
|
|
371
|
-
workspaceTopLevelDirty: boolean;
|
|
372
|
-
channelCapabilities?: ChannelCapabilities;
|
|
373
|
-
/** Per-turn snapshot of trustContext, frozen at message-processing start. */
|
|
374
|
-
currentTurnTrustContext?: TrustContext;
|
|
375
|
-
/** Per-turn snapshot of channelCapabilities, frozen at message-processing start. */
|
|
376
|
-
currentTurnChannelCapabilities?: ChannelCapabilities;
|
|
377
|
-
/**
|
|
378
|
-
* Current inference-profile override for this turn. Read by
|
|
379
|
-
* `createToolExecutor` so `ToolContext.overrideProfile` carries the same
|
|
380
|
-
* profile the agent loop is sending to the provider. Refreshed between
|
|
381
|
-
* model calls so an explicitly confirmed profile session opened mid-turn
|
|
382
|
-
* is inherited by later tool executions and nested subagents.
|
|
383
|
-
*/
|
|
384
|
-
currentTurnOverrideProfile?: string;
|
|
385
243
|
/**
|
|
386
|
-
*
|
|
387
|
-
*
|
|
388
|
-
*
|
|
389
|
-
*
|
|
244
|
+
* Commit-timing activation-rail tag (daemon-only). Persisted into the
|
|
245
|
+
* server-side `ui_surface` history block — NOT the client `ui_surface_show`
|
|
246
|
+
* message — so `restoreSurfaceStateFromHistory` can rehydrate the tag and a
|
|
247
|
+
* post-reload commit still records its funnel milestone. Show-timing moments
|
|
248
|
+
* record at render and are never stored here.
|
|
390
249
|
*/
|
|
391
|
-
|
|
392
|
-
commandIntent?: { type: string; payload?: string; languageCode?: string };
|
|
393
|
-
trustContext?: TrustContext;
|
|
394
|
-
/** Task-run scope for the current turn. Cleared at turn end so queued/drained turns don't inherit it. */
|
|
395
|
-
taskRunId?: string;
|
|
396
|
-
assistantId?: string;
|
|
397
|
-
voiceCallControlPrompt?: string;
|
|
398
|
-
transportHints?: string[];
|
|
399
|
-
clientTimezone?: string;
|
|
400
|
-
|
|
401
|
-
readonly coreToolNames: Set<string>;
|
|
402
|
-
allowedToolNames?: Set<string>;
|
|
403
|
-
diskPressureCleanupModeActive?: boolean;
|
|
404
|
-
toolsDisabledDepth: number;
|
|
405
|
-
preactivatedSkillIds?: string[];
|
|
406
|
-
readonly skillProjectionState: Map<string, string>;
|
|
407
|
-
readonly skillProjectionCache: SkillProjectionCache;
|
|
408
|
-
|
|
409
|
-
readonly traceEmitter: TraceEmitter;
|
|
410
|
-
readonly profiler: ToolProfiler;
|
|
411
|
-
usageStats: UsageStats;
|
|
412
|
-
turnCount: number;
|
|
413
|
-
|
|
414
|
-
lastAssistantAttachments: AssistantAttachmentDraft[];
|
|
415
|
-
lastAttachmentWarnings: string[];
|
|
416
|
-
|
|
417
|
-
hasNoClient: boolean;
|
|
418
|
-
/** True when this conversation is itself a subagent (suppresses subagent status injection). */
|
|
419
|
-
isSubagent?: boolean;
|
|
420
|
-
headlessLock?: boolean;
|
|
421
|
-
readonly streamThinking: boolean;
|
|
422
|
-
readonly prompter: PermissionPrompter;
|
|
423
|
-
readonly queue: MessageQueue;
|
|
424
|
-
|
|
425
|
-
emitActivityState(
|
|
426
|
-
phase:
|
|
427
|
-
| "idle"
|
|
428
|
-
| "thinking"
|
|
429
|
-
| "streaming"
|
|
430
|
-
| "tool_running"
|
|
431
|
-
| "awaiting_confirmation",
|
|
432
|
-
reason:
|
|
433
|
-
| "message_dequeued"
|
|
434
|
-
| "thinking_delta"
|
|
435
|
-
| "first_text_delta"
|
|
436
|
-
| "tool_use_start"
|
|
437
|
-
| "preview_start"
|
|
438
|
-
| "tool_result_received"
|
|
439
|
-
| "confirmation_requested"
|
|
440
|
-
| "confirmation_resolved"
|
|
441
|
-
| "context_compacting"
|
|
442
|
-
| "message_complete"
|
|
443
|
-
| "generation_cancelled"
|
|
444
|
-
| "error_terminal",
|
|
445
|
-
options?: {
|
|
446
|
-
anchor?: "assistant_turn" | "user_turn" | "global";
|
|
447
|
-
requestId?: string;
|
|
448
|
-
statusText?: string;
|
|
449
|
-
},
|
|
450
|
-
): void;
|
|
451
|
-
emitConfirmationStateChanged(
|
|
452
|
-
params: ConfirmationStateChanged extends {
|
|
453
|
-
type: infer _;
|
|
454
|
-
}
|
|
455
|
-
? Omit<ConfirmationStateChanged, "type">
|
|
456
|
-
: never,
|
|
457
|
-
): void;
|
|
458
|
-
|
|
459
|
-
/**
|
|
460
|
-
* Optional callback invoked by the Conversation when a confirmation state changes.
|
|
461
|
-
* The agent loop registers this to track requestId → toolUseId mappings
|
|
462
|
-
* and record confirmation outcomes for persistence.
|
|
463
|
-
*/
|
|
464
|
-
onConfirmationOutcome?: (
|
|
465
|
-
requestId: string,
|
|
466
|
-
state: string,
|
|
467
|
-
toolUseId?: string,
|
|
468
|
-
) => void;
|
|
469
|
-
|
|
470
|
-
getWorkspaceGitService?: (workspaceDir: string) => GitServiceInitializer;
|
|
471
|
-
commitTurnChanges?: typeof commitTurnChanges;
|
|
472
|
-
|
|
473
|
-
refreshWorkspaceTopLevelContextIfNeeded(): void;
|
|
474
|
-
markWorkspaceTopLevelDirty(): void;
|
|
475
|
-
getQueueDepth(): number;
|
|
476
|
-
hasQueuedMessages(): boolean;
|
|
477
|
-
canHandoffAtCheckpoint(): boolean;
|
|
478
|
-
drainQueue(reason: QueueDrainReason): Promise<void>;
|
|
479
|
-
getTurnChannelContext(): TurnChannelContext | null;
|
|
480
|
-
getTurnInterfaceContext(): TurnInterfaceContext | null;
|
|
250
|
+
activationMoment?: ActivationMomentParam;
|
|
481
251
|
}
|
|
482
252
|
|
|
483
253
|
// ── runAgentLoop ─────────────────────────────────────────────────────
|
|
484
254
|
|
|
485
255
|
export async function runAgentLoopImpl(
|
|
486
|
-
ctx:
|
|
256
|
+
ctx: Conversation,
|
|
487
257
|
content: string,
|
|
488
258
|
userMessageId: string,
|
|
489
259
|
onEvent: (msg: ServerMessage) => void,
|
|
@@ -556,26 +326,22 @@ export async function runAgentLoopImpl(
|
|
|
556
326
|
// `resolveCallSiteConfig`, picking up any user overrides under
|
|
557
327
|
// `llm.callSites.mainAgent` (falling back to `llm.default` when absent).
|
|
558
328
|
const turnCallSite: LLMCallSite = options?.callSite ?? "mainAgent";
|
|
559
|
-
// Expose the turn's call site
|
|
560
|
-
//
|
|
329
|
+
// Expose the turn's call site on the live conversation so the runtime
|
|
330
|
+
// injection assembly self-resolves it for the turn's plugin contexts.
|
|
561
331
|
ctx.currentCallSite = turnCallSite;
|
|
562
332
|
|
|
563
|
-
// Read the conversation row once for both the override-profile derivation
|
|
564
|
-
// below and the title-replaceability check at turn start. Later reads in
|
|
565
|
-
// this function (post-turn truncation, disk sync, home-feed emission)
|
|
566
|
-
// intentionally re-read because state can change during the turn.
|
|
567
|
-
const turnStartConversation = getConversation(ctx.conversationId);
|
|
568
|
-
|
|
569
333
|
// Optional per-turn inference-profile override. Plumbed through to every
|
|
570
334
|
// LLM call the loop emits and inherited by any subagents spawned during
|
|
571
335
|
// this turn. Caller-supplied `options.overrideProfile` (e.g.
|
|
572
336
|
// SubagentManager forwarding the parent's pinned profile into the
|
|
573
|
-
// spawned subagent's background conversation) wins over the
|
|
574
|
-
// so the agent loop's
|
|
575
|
-
// explicitly inherited override.
|
|
337
|
+
// spawned subagent's background conversation) wins over the conversation's
|
|
338
|
+
// own override so the agent loop's background-skip rule doesn't zero out an
|
|
339
|
+
// explicitly inherited override. The override state is mirrored onto the
|
|
340
|
+
// live conversation (hydrated on load, kept current by the HTTP setters and
|
|
341
|
+
// the expiry reaper), so the derivation reads `ctx` rather than re-fetching
|
|
342
|
+
// the row.
|
|
576
343
|
const userExplicitOverride =
|
|
577
|
-
options?.overrideProfile ??
|
|
578
|
-
getConversationOverrideProfileFromRow(turnStartConversation);
|
|
344
|
+
options?.overrideProfile ?? resolveOverrideProfile(ctx);
|
|
579
345
|
|
|
580
346
|
const config = getConfig();
|
|
581
347
|
|
|
@@ -588,9 +354,7 @@ export async function runAgentLoopImpl(
|
|
|
588
354
|
|
|
589
355
|
const readCurrentOverrideProfile = (): string | undefined =>
|
|
590
356
|
options?.overrideProfile ??
|
|
591
|
-
|
|
592
|
-
getConversation(ctx.conversationId),
|
|
593
|
-
) ??
|
|
357
|
+
resolveOverrideProfile(ctx) ??
|
|
594
358
|
ctx.toolRoutedProfile;
|
|
595
359
|
|
|
596
360
|
const effectiveContextWindow = resolveEffectiveContextWindow({
|
|
@@ -608,11 +372,7 @@ export async function runAgentLoopImpl(
|
|
|
608
372
|
}).contextWindow,
|
|
609
373
|
currentEffectiveContextWindow,
|
|
610
374
|
);
|
|
611
|
-
|
|
612
|
-
ctx.contextWindowManager as ContextWindowManager & {
|
|
613
|
-
updateConfig?: (config: ContextWindowConfig) => void;
|
|
614
|
-
};
|
|
615
|
-
contextWindowManager.updateConfig?.(currentContextWindowConfig);
|
|
375
|
+
ctx.contextWindowManager.updateConfig(currentContextWindowConfig);
|
|
616
376
|
|
|
617
377
|
let appliedOverrideProfile = turnOverrideProfile;
|
|
618
378
|
let emittedToolRoutedProfile: string | undefined;
|
|
@@ -632,7 +392,7 @@ export async function runAgentLoopImpl(
|
|
|
632
392
|
}).contextWindow,
|
|
633
393
|
currentEffectiveContextWindow,
|
|
634
394
|
);
|
|
635
|
-
contextWindowManager.updateConfig
|
|
395
|
+
ctx.contextWindowManager.updateConfig(currentContextWindowConfig);
|
|
636
396
|
appliedOverrideProfile = currentOverrideProfile;
|
|
637
397
|
rlog.info(
|
|
638
398
|
{ overrideProfile: currentOverrideProfile ?? null },
|
|
@@ -751,11 +511,17 @@ export async function runAgentLoopImpl(
|
|
|
751
511
|
|
|
752
512
|
const isInteractiveResolved =
|
|
753
513
|
options?.isInteractive ?? (!ctx.hasNoClient && !ctx.headlessLock);
|
|
514
|
+
// Whether the in-flight turn has no human present to answer clarification
|
|
515
|
+
// questions. Derived from the loop's `isInteractive` option (which can fall
|
|
516
|
+
// back to mutable client/headless state that flips mid-turn), so it is
|
|
517
|
+
// resolved once here and threaded into every re-injection — including the
|
|
518
|
+
// post-compaction hook — rather than re-read per assembly call.
|
|
519
|
+
const isNonInteractive = !isInteractiveResolved;
|
|
754
520
|
const diskPressureDecision = classifyDiskPressureTurnPolicy(
|
|
755
521
|
getDiskPressureStatus(),
|
|
756
522
|
{
|
|
757
|
-
conversationType:
|
|
758
|
-
conversationSource:
|
|
523
|
+
conversationType: ctx.conversationType ?? null,
|
|
524
|
+
conversationSource: ctx.source ?? null,
|
|
759
525
|
callSite: turnCallSite,
|
|
760
526
|
isInteractive: isInteractiveResolved,
|
|
761
527
|
sourceChannel:
|
|
@@ -772,10 +538,6 @@ export async function runAgentLoopImpl(
|
|
|
772
538
|
: null,
|
|
773
539
|
},
|
|
774
540
|
);
|
|
775
|
-
const diskPressureContext =
|
|
776
|
-
diskPressureDecision.action === "allow-cleanup-mode"
|
|
777
|
-
? { cleanupModeActive: true }
|
|
778
|
-
: null;
|
|
779
541
|
ctx.diskPressureCleanupModeActive =
|
|
780
542
|
diskPressureDecision.action === "allow-cleanup-mode";
|
|
781
543
|
|
|
@@ -879,22 +641,7 @@ export async function runAgentLoopImpl(
|
|
|
879
641
|
}
|
|
880
642
|
|
|
881
643
|
const isFirstMessage = ctx.messages.length === 1;
|
|
882
|
-
// Promote a pending post-compaction re-inject signal (e.g. from `/compact`)
|
|
883
|
-
// into `compactedThisTurn` so NOW.md / PKB / v2 static blocks land on this
|
|
884
|
-
// turn even when no mid-turn compaction fires. Clear the flag immediately
|
|
885
|
-
// so this fires exactly once per `/compact` event.
|
|
886
|
-
const consumedPostCompactReinject = ctx.pendingPostCompactReinject;
|
|
887
|
-
ctx.pendingPostCompactReinject = false;
|
|
888
|
-
state.shouldInjectWorkspace = isFirstMessage || consumedPostCompactReinject;
|
|
889
|
-
let compactedThisTurn = consumedPostCompactReinject;
|
|
890
|
-
let slackCompactedThisTurn = false;
|
|
891
644
|
const isSlackConversation = ctx.channelCapabilities?.channel === "slack";
|
|
892
|
-
let currentSlackContextSummary =
|
|
893
|
-
turnStartConversation?.contextSummary ?? null;
|
|
894
|
-
let currentSlackContextCompactedMessageCount =
|
|
895
|
-
turnStartConversation?.contextCompactedMessageCount ?? 0;
|
|
896
|
-
let currentSlackContextCompactionWatermarkTs =
|
|
897
|
-
turnStartConversation?.slackContextCompactionWatermarkTs ?? null;
|
|
898
645
|
const loadCurrentSlackChronologicalContext =
|
|
899
646
|
(): SlackChronologicalContext | null => {
|
|
900
647
|
if (!isSlackConversation) return null;
|
|
@@ -903,18 +650,15 @@ export async function runAgentLoopImpl(
|
|
|
903
650
|
ctx.channelCapabilities!,
|
|
904
651
|
{
|
|
905
652
|
trustClass: ctx.trustContext?.trustClass,
|
|
906
|
-
contextSummary:
|
|
907
|
-
contextCompactedMessageCount:
|
|
908
|
-
currentSlackContextCompactedMessageCount,
|
|
653
|
+
contextSummary: ctx.contextSummary,
|
|
654
|
+
contextCompactedMessageCount: ctx.contextCompactedMessageCount,
|
|
909
655
|
slackContextCompactionWatermarkTs:
|
|
910
|
-
|
|
656
|
+
ctx.slackContextCompactionWatermarkTs,
|
|
911
657
|
},
|
|
912
658
|
);
|
|
913
659
|
};
|
|
914
660
|
let slackChronologicalContext: SlackChronologicalContext | null =
|
|
915
661
|
loadCurrentSlackChronologicalContext();
|
|
916
|
-
const messagesForStartOfTurnCompaction =
|
|
917
|
-
slackChronologicalContext?.messages ?? ctx.messages;
|
|
918
662
|
const getSlackProvenanceContextForCompactionBasis = (
|
|
919
663
|
messages: Message[],
|
|
920
664
|
compactedMessages: number,
|
|
@@ -1001,15 +745,6 @@ export async function runAgentLoopImpl(
|
|
|
1001
745
|
await applyCompactionResult(ctx, result, onEvent, reqId, {
|
|
1002
746
|
slackContextCompactionWatermarkTs: slackWatermarkTs,
|
|
1003
747
|
});
|
|
1004
|
-
currentSlackContextSummary = result.summaryText;
|
|
1005
|
-
currentSlackContextCompactedMessageCount =
|
|
1006
|
-
ctx.contextCompactedMessageCount;
|
|
1007
|
-
if (slackWatermarkTs) {
|
|
1008
|
-
currentSlackContextCompactionWatermarkTs = slackWatermarkTs;
|
|
1009
|
-
}
|
|
1010
|
-
if (isSlackConversation) {
|
|
1011
|
-
slackCompactedThisTurn = true;
|
|
1012
|
-
}
|
|
1013
748
|
slackChronologicalContext = projectSlackProvenanceAfterCompaction(
|
|
1014
749
|
provenanceContext,
|
|
1015
750
|
compactedBasis,
|
|
@@ -1017,88 +752,6 @@ export async function runAgentLoopImpl(
|
|
|
1017
752
|
);
|
|
1018
753
|
};
|
|
1019
754
|
|
|
1020
|
-
const compactCheck = ctx.contextWindowManager.shouldCompact(
|
|
1021
|
-
messagesForStartOfTurnCompaction,
|
|
1022
|
-
);
|
|
1023
|
-
// Skip auto-compaction while the circuit breaker is open. Force paths
|
|
1024
|
-
// and user-initiated /compact bypass this check.
|
|
1025
|
-
const autoCompactAllowed =
|
|
1026
|
-
!(await ctx.agentLoop.compactionCircuit.isOpen(ctx));
|
|
1027
|
-
if (compactCheck.needed && autoCompactAllowed) {
|
|
1028
|
-
ctx.emitActivityState("thinking", "context_compacting", {
|
|
1029
|
-
requestId: reqId,
|
|
1030
|
-
});
|
|
1031
|
-
}
|
|
1032
|
-
const compactionOptions = {
|
|
1033
|
-
precomputedEstimate: compactCheck.estimatedTokens,
|
|
1034
|
-
overrideProfile: resolveCurrentOverrideProfile() ?? null,
|
|
1035
|
-
actorTrustClass: resolveTurnActorTrustClass(ctx),
|
|
1036
|
-
};
|
|
1037
|
-
let compacted: Awaited<
|
|
1038
|
-
ReturnType<typeof ctx.contextWindowManager.maybeCompact>
|
|
1039
|
-
> | null = null;
|
|
1040
|
-
if (autoCompactAllowed) {
|
|
1041
|
-
try {
|
|
1042
|
-
compacted = (await runPipeline<CompactionArgs, CompactionResult>(
|
|
1043
|
-
"compaction",
|
|
1044
|
-
getMiddlewaresFor("compaction"),
|
|
1045
|
-
(args) =>
|
|
1046
|
-
defaultCompactionTerminal(args, buildPluginTurnContext(ctx, reqId)),
|
|
1047
|
-
{
|
|
1048
|
-
messages: messagesForStartOfTurnCompaction,
|
|
1049
|
-
signal: abortController.signal,
|
|
1050
|
-
options: compactionOptions,
|
|
1051
|
-
},
|
|
1052
|
-
buildPluginTurnContext(ctx, reqId),
|
|
1053
|
-
DEFAULT_TIMEOUTS.compaction,
|
|
1054
|
-
)) as Awaited<ReturnType<typeof ctx.contextWindowManager.maybeCompact>>;
|
|
1055
|
-
} catch (err) {
|
|
1056
|
-
if (err instanceof PluginTimeoutError) {
|
|
1057
|
-
// Pipeline exceeded its budget. Record the failure so the circuit
|
|
1058
|
-
// breaker tracks consecutive timeouts (it trips after three),
|
|
1059
|
-
// then degrade gracefully by skipping compaction this turn —
|
|
1060
|
-
// the turn proceeds with the un-compacted history rather than
|
|
1061
|
-
// hard-failing. The inner summary call has been aborted by the
|
|
1062
|
-
// runner's signal-linking, so updateSummary's local fallback
|
|
1063
|
-
// also ran before this catch block is reached.
|
|
1064
|
-
rlog.warn(
|
|
1065
|
-
{ err, phase: "start-of-turn-compaction" },
|
|
1066
|
-
"Compaction pipeline timed out — skipping compaction this turn",
|
|
1067
|
-
);
|
|
1068
|
-
await ctx.agentLoop.compactionCircuit.recordOutcome(
|
|
1069
|
-
ctx,
|
|
1070
|
-
true,
|
|
1071
|
-
onEvent,
|
|
1072
|
-
);
|
|
1073
|
-
compacted = null;
|
|
1074
|
-
} else {
|
|
1075
|
-
throw err;
|
|
1076
|
-
}
|
|
1077
|
-
}
|
|
1078
|
-
}
|
|
1079
|
-
// Only track circuit-breaker state when a summary LLM call actually ran.
|
|
1080
|
-
// `summaryFailed` is `undefined` on early returns (compaction disabled,
|
|
1081
|
-
// below threshold, no eligible messages, truncation-only
|
|
1082
|
-
// path) — treating those as "successful" compactions would silently reset
|
|
1083
|
-
// the 3-strike counter and break the invariant.
|
|
1084
|
-
if (compacted && compacted.summaryFailed !== undefined) {
|
|
1085
|
-
await ctx.agentLoop.compactionCircuit.recordOutcome(
|
|
1086
|
-
ctx,
|
|
1087
|
-
compacted.summaryFailed,
|
|
1088
|
-
onEvent,
|
|
1089
|
-
);
|
|
1090
|
-
}
|
|
1091
|
-
if (compacted?.compacted) {
|
|
1092
|
-
await applySuccessfulCompaction(
|
|
1093
|
-
compacted,
|
|
1094
|
-
messagesForStartOfTurnCompaction,
|
|
1095
|
-
);
|
|
1096
|
-
state.shouldInjectWorkspace = true;
|
|
1097
|
-
if (compacted.compactedPersistedMessages > 0) {
|
|
1098
|
-
compactedThisTurn = true;
|
|
1099
|
-
}
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
755
|
// Register confirmation outcome tracker so the agent loop can link
|
|
1103
756
|
// confirmation decisions to tool_use_ids for persistence.
|
|
1104
757
|
ctx.onConfirmationOutcome = (requestId, confirmationState, toolUseId) => {
|
|
@@ -1143,39 +796,16 @@ export async function runAgentLoopImpl(
|
|
|
1143
796
|
hostTimeZone,
|
|
1144
797
|
});
|
|
1145
798
|
|
|
1146
|
-
//
|
|
1147
|
-
//
|
|
1148
|
-
//
|
|
1149
|
-
//
|
|
1150
|
-
//
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
assistantId: ctx.assistantId ?? DAEMON_INTERNAL_ASSISTANT_ID,
|
|
1157
|
-
sourceChannel: gc.sourceChannel,
|
|
1158
|
-
conversationExternalId: gc.requesterChatId,
|
|
1159
|
-
actorExternalId: gc.requesterExternalUserId,
|
|
1160
|
-
actorDisplayName: gc.requesterSenderDisplayName,
|
|
1161
|
-
});
|
|
1162
|
-
resolvedInboundActorContext = inboundActorContextFromTrust(actorTrust);
|
|
1163
|
-
} else {
|
|
1164
|
-
resolvedInboundActorContext = inboundActorContextFromTrustContext(gc);
|
|
1165
|
-
}
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
// Resolve the channel/interface labels and the guardian flag for this
|
|
1169
|
-
// turn. These derive only from the captured turn context and the resolved
|
|
1170
|
-
// actor trust class — never from retrieval — so they settle before context
|
|
1171
|
-
// assembly.
|
|
1172
|
-
const interfaceName =
|
|
1173
|
-
capturedTurnInterfaceContext.userMessageInterface ?? undefined;
|
|
1174
|
-
const channelName =
|
|
1175
|
-
capturedTurnChannelContext?.userMessageChannel ?? undefined;
|
|
1176
|
-
const isGuardian =
|
|
1177
|
-
resolvedInboundActorContext?.trustClass === "guardian" ||
|
|
1178
|
-
!resolvedInboundActorContext;
|
|
799
|
+
// Unified `<turn_context>` actor input for this turn (model-facing grounding
|
|
800
|
+
// metadata; the conversation runtime context remains the source for policy
|
|
801
|
+
// gating). Resolved once at turn start and threaded per call site (like
|
|
802
|
+
// `modelProfile`) so post-compaction re-injection receives it as an explicit
|
|
803
|
+
// hook input rather than re-deriving it from live state that can flip
|
|
804
|
+
// mid-turn.
|
|
805
|
+
const actorContext = resolveTurnInboundActorContext(
|
|
806
|
+
ctx.trustContext,
|
|
807
|
+
ctx.assistantId,
|
|
808
|
+
);
|
|
1179
809
|
|
|
1180
810
|
// Surface long gaps between user messages so the model can acknowledge
|
|
1181
811
|
// the absence naturally. Gated at >12h to avoid noisy injection during
|
|
@@ -1196,6 +826,19 @@ export async function runAgentLoopImpl(
|
|
|
1196
826
|
}
|
|
1197
827
|
}
|
|
1198
828
|
|
|
829
|
+
// Freeze the turn-start client timezone and long-absence gap on the
|
|
830
|
+
// conversation so `applyRuntimeInjections` sources them from live state —
|
|
831
|
+
// like the channel/voice/transport hints. Frozen here (rather than read
|
|
832
|
+
// live in assembly) because the live `ctx.clientTimezone` is overwritten
|
|
833
|
+
// when a newer message for the same conversation arrives mid-turn, which
|
|
834
|
+
// would otherwise leak a queued message's timezone into the in-flight turn.
|
|
835
|
+
// The `current_time` value is computed fresh at each injection point, so
|
|
836
|
+
// it is not part of this snapshot.
|
|
837
|
+
ctx.currentTurnTemporalSnapshot = {
|
|
838
|
+
clientTimezone: timezoneContext.clientTimezone,
|
|
839
|
+
timeSinceLastMessage,
|
|
840
|
+
};
|
|
841
|
+
|
|
1199
842
|
// Resolve the effective profile key for this turn and detect changes.
|
|
1200
843
|
// Only inject model_profile into the turn context when the profile
|
|
1201
844
|
// changed since the last turn (or on the first turn of a conversation)
|
|
@@ -1204,519 +847,73 @@ export async function runAgentLoopImpl(
|
|
|
1204
847
|
turnOverrideProfile ??
|
|
1205
848
|
config.llm.activeProfile ??
|
|
1206
849
|
resolveDefaultProfileKey("mainAgent", config.llm);
|
|
1207
|
-
const lastNotified =
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
850
|
+
const lastNotified = ctx.lastNotifiedInferenceProfile;
|
|
851
|
+
const modelProfileKey =
|
|
852
|
+
effectiveProfileKey != null && effectiveProfileKey !== lastNotified
|
|
853
|
+
? effectiveProfileKey
|
|
854
|
+
: null;
|
|
855
|
+
// The key is threaded to the memory hook as plain turn data; the rendered
|
|
856
|
+
// `Label (model)` line for the mid-loop re-injection sites is resolved from
|
|
857
|
+
// it here so both paths share one derivation.
|
|
858
|
+
const modelProfileStr = resolveTurnModelProfileLabel(
|
|
859
|
+
modelProfileKey,
|
|
860
|
+
turnCallSite,
|
|
861
|
+
config.llm,
|
|
862
|
+
);
|
|
863
|
+
if (modelProfileKey != null) {
|
|
1216
864
|
// Record the notification for persistence on delivery rather than here:
|
|
1217
865
|
// the model only "learns" the profile once it receives this turn
|
|
1218
866
|
// context, signalled by the first `message_complete`. Persisting inline
|
|
1219
867
|
// would mark the profile notified even if the turn is cancelled or fails
|
|
1220
868
|
// before the model ever sees the notice.
|
|
1221
|
-
state.pendingNotifiedInferenceProfile =
|
|
869
|
+
state.pendingNotifiedInferenceProfile = modelProfileKey;
|
|
1222
870
|
}
|
|
1223
871
|
|
|
1224
|
-
//
|
|
1225
|
-
//
|
|
1226
|
-
//
|
|
1227
|
-
// submitted, before context assembly" moment because its outputs feed the
|
|
1228
|
-
// injection and overflow-reduction transforms below. It is shaped as the
|
|
1229
|
-
// `user-prompt-submit-temp` hook handler but invoked directly for now: it
|
|
1230
|
-
// must run early, while the canonical late `user-prompt-submit` hook
|
|
1231
|
-
// (history repair, title) runs after those transforms, so the two cannot
|
|
1232
|
-
// share a fire site until compaction is cleared from the gap between them.
|
|
872
|
+
// Trust class and injection mode, resolved once at turn start for the
|
|
873
|
+
// post-compaction re-injection sites further down, which re-apply the
|
|
874
|
+
// runtime injections (gated on trust) onto the compacted history.
|
|
1233
875
|
const isTrustedActor = resolveTrustClass(ctx.trustContext) === "guardian";
|
|
1234
|
-
const memoryCtx: MemoryRetrievalHookContext = {
|
|
1235
|
-
graphMemory: ctx.graphMemory,
|
|
1236
|
-
config: getConfig(),
|
|
1237
|
-
onEvent,
|
|
1238
|
-
isTrustedActor,
|
|
1239
|
-
conversationId: ctx.conversationId,
|
|
1240
|
-
userMessageId,
|
|
1241
|
-
logger: rlog,
|
|
1242
|
-
// An external cancel aborts `prepareMemory` instead of letting it run
|
|
1243
|
-
// to completion after the turn has already been torn down.
|
|
1244
|
-
signal: abortController.signal,
|
|
1245
|
-
latestMessages: ctx.messages,
|
|
1246
|
-
};
|
|
1247
|
-
await userPromptSubmitMemoryRetrieval(memoryCtx);
|
|
1248
|
-
|
|
1249
|
-
// The retriever owns its side effects (injected-block metadata, recall
|
|
1250
|
-
// log, `memory_recalled` event) and records the dense/sparse PKB query
|
|
1251
|
-
// pair on the graph handle for the PKB-reminder injector to read back; the
|
|
1252
|
-
// loop only reuses the injected message list downstream.
|
|
1253
|
-
let runMessages = memoryCtx.latestMessages;
|
|
1254
|
-
|
|
1255
|
-
// Capture wall-clock "now" at its point of use, after the blocking memory
|
|
1256
|
-
// retrieval, so the injected `<turn_context>` timestamp reflects current
|
|
1257
|
-
// time rather than the moment the turn began.
|
|
1258
|
-
const timestamp = formatTurnTimestamp({
|
|
1259
|
-
timeZone: timezoneContext.effectiveTimezone,
|
|
1260
|
-
});
|
|
1261
|
-
|
|
1262
|
-
// Build unified turn context block that replaces the separate temporal,
|
|
1263
|
-
// channel, interface, and actor context blocks.
|
|
1264
|
-
const baseTurnContext = {
|
|
1265
|
-
timestamp,
|
|
1266
|
-
interfaceName,
|
|
1267
|
-
channelName,
|
|
1268
|
-
configuredUserTimezone: timezoneContext.configuredUserTimezone,
|
|
1269
|
-
clientTimezone: timezoneContext.clientTimezone,
|
|
1270
|
-
detectedTimezone: timezoneContext.detectedTimezone,
|
|
1271
|
-
timeSinceLastMessage,
|
|
1272
|
-
modelProfile: modelProfileStr,
|
|
1273
|
-
};
|
|
1274
|
-
const unifiedTurnContextStr = buildUnifiedTurnContextBlock(
|
|
1275
|
-
isGuardian
|
|
1276
|
-
? baseTurnContext
|
|
1277
|
-
: {
|
|
1278
|
-
...baseTurnContext,
|
|
1279
|
-
actorContext: resolvedInboundActorContext,
|
|
1280
|
-
},
|
|
1281
|
-
);
|
|
1282
|
-
|
|
1283
|
-
// The `remember` tool handles scratchpad-style memory writes directly to the graph.
|
|
1284
|
-
|
|
1285
|
-
// Subagent status injection — gives the parent LLM visibility into active/completed children.
|
|
1286
|
-
// Skipped when this conversation IS a subagent (no nesting) or has no children.
|
|
1287
|
-
const subagentStatusBlock = ctx.isSubagent
|
|
1288
|
-
? null
|
|
1289
|
-
: buildSubagentStatusBlock(
|
|
1290
|
-
getSubagentManager().getChildrenOf(ctx.conversationId),
|
|
1291
|
-
);
|
|
1292
|
-
|
|
1293
|
-
// For any Slack conversation (channels and DMs alike), build a
|
|
1294
|
-
// chronological transcript from the persisted message rows so the
|
|
1295
|
-
// model sees one channel-wide view instead of the gateway's per-turn
|
|
1296
|
-
// hints. DMs render as a flat sequence (no thread tags), channels
|
|
1297
|
-
// include sibling threads.
|
|
1298
|
-
const slackConversationForInjection = isSlackConversation
|
|
1299
|
-
? (getConversation(ctx.conversationId) ?? turnStartConversation)
|
|
1300
|
-
: turnStartConversation;
|
|
1301
|
-
if (isSlackConversation && !slackCompactedThisTurn) {
|
|
1302
|
-
slackChronologicalContext ??= loadSlackChronologicalContext(
|
|
1303
|
-
ctx.conversationId,
|
|
1304
|
-
ctx.channelCapabilities!,
|
|
1305
|
-
{
|
|
1306
|
-
trustClass: ctx.trustContext?.trustClass,
|
|
1307
|
-
contextSummary: slackConversationForInjection?.contextSummary,
|
|
1308
|
-
contextCompactedMessageCount:
|
|
1309
|
-
slackConversationForInjection?.contextCompactedMessageCount,
|
|
1310
|
-
slackContextCompactionWatermarkTs:
|
|
1311
|
-
slackConversationForInjection?.slackContextCompactionWatermarkTs,
|
|
1312
|
-
},
|
|
1313
|
-
);
|
|
1314
|
-
}
|
|
1315
|
-
const slackChronologicalMessages =
|
|
1316
|
-
slackChronologicalContext?.messages ?? null;
|
|
1317
|
-
|
|
1318
|
-
// Active-thread focus block: when the inbound user message belongs to
|
|
1319
|
-
// a Slack thread, append a non-persisted `<active_thread>` tail block
|
|
1320
|
-
// to the final user turn listing the thread's parent + replies. Helps
|
|
1321
|
-
// the model orient when the channel transcript is long and
|
|
1322
|
-
// interleaved. Replays strip the block via RUNTIME_INJECTION_PREFIXES.
|
|
1323
|
-
// DMs short-circuit to null inside `loadSlackActiveThreadFocusBlock`
|
|
1324
|
-
// since DMs do not have threads.
|
|
1325
|
-
const slackActiveThreadFocusBlock = isSlackConversation
|
|
1326
|
-
? loadSlackActiveThreadFocusBlock(
|
|
1327
|
-
ctx.conversationId,
|
|
1328
|
-
ctx.channelCapabilities!,
|
|
1329
|
-
{
|
|
1330
|
-
trustClass: ctx.trustContext?.trustClass,
|
|
1331
|
-
contextCompactedMessageCount:
|
|
1332
|
-
slackConversationForInjection?.contextCompactedMessageCount,
|
|
1333
|
-
slackContextCompactionWatermarkTs:
|
|
1334
|
-
slackConversationForInjection?.slackContextCompactionWatermarkTs,
|
|
1335
|
-
},
|
|
1336
|
-
)
|
|
1337
|
-
: null;
|
|
1338
|
-
|
|
1339
|
-
state.reducerCompacted = compactedThisTurn;
|
|
1340
|
-
|
|
1341
|
-
// memory-v3-live: when on, the provider anchors its long-TTL cache
|
|
1342
|
-
// breakpoint on the most recent STABLE user message, since the latest user
|
|
1343
|
-
// message now carries the volatile per-turn `<memory>` block the v3
|
|
1344
|
-
// injector emits. The matching v2-suppression strip is owned by
|
|
1345
|
-
// `applyRuntimeInjections`, which reads the same flag itself. Flag off →
|
|
1346
|
-
// bit-for-bit identical to today's v2 path.
|
|
1347
|
-
const memoryV3Live = isAssistantFeatureFlagEnabled(
|
|
1348
|
-
"memory-v3-live",
|
|
1349
|
-
getConfig(),
|
|
1350
|
-
);
|
|
1351
|
-
|
|
1352
|
-
// Shared injection options — reused whenever we need to re-inject after reduction.
|
|
1353
|
-
const injectionOpts = {
|
|
1354
|
-
diskPressureContext,
|
|
1355
|
-
// Resolved from the conversation's surface state here, where the
|
|
1356
|
-
// runtime injector is the only consumer of the active-surface block.
|
|
1357
|
-
activeSurface: buildActiveSurfaceContext({
|
|
1358
|
-
currentActiveSurfaceId: ctx.currentActiveSurfaceId,
|
|
1359
|
-
currentPage: ctx.currentPage,
|
|
1360
|
-
surfaceState: ctx.surfaceState,
|
|
1361
|
-
}),
|
|
1362
|
-
// Resolved here, where the runtime injector is the only consumer of the
|
|
1363
|
-
// active-documents block.
|
|
1364
|
-
activeDocuments: buildActiveDocuments(ctx.conversationId),
|
|
1365
|
-
workspaceTopLevelContext: buildWorkspaceTopLevelContext(
|
|
1366
|
-
ctx,
|
|
1367
|
-
state.shouldInjectWorkspace,
|
|
1368
|
-
),
|
|
1369
|
-
channelCapabilities: ctx.channelCapabilities ?? null,
|
|
1370
|
-
channelCommandContext: ctx.commandIntent ?? null,
|
|
1371
|
-
unifiedTurnContext: unifiedTurnContextStr,
|
|
1372
|
-
voiceCallControlPrompt: ctx.voiceCallControlPrompt ?? null,
|
|
1373
|
-
transportHints: ctx.transportHints ?? null,
|
|
1374
|
-
isNonInteractive: !isInteractiveResolved,
|
|
1375
|
-
isBackgroundConversation: isBackgroundConversationType(
|
|
1376
|
-
turnStartConversation?.conversationType,
|
|
1377
|
-
),
|
|
1378
|
-
subagentStatusBlock,
|
|
1379
|
-
slackChronologicalMessages,
|
|
1380
|
-
slackActiveThreadFocusBlock,
|
|
1381
|
-
} as const;
|
|
1382
|
-
|
|
1383
876
|
let currentInjectionMode: InjectionMode = "full";
|
|
1384
877
|
|
|
1385
|
-
//
|
|
1386
|
-
//
|
|
1387
|
-
//
|
|
1388
|
-
//
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
});
|
|
1399
|
-
runMessages = injection.messages;
|
|
1400
|
-
|
|
1401
|
-
// Persist injected blocks in message metadata so they survive conversation
|
|
1402
|
-
// reloads (eviction, restart, fork). loadFromDb re-injects from metadata.
|
|
1403
|
-
// Only the first call site persists — the overflow-recovery re-entry sites
|
|
1404
|
-
// send identical bytes and the tail row may not correspond to
|
|
1405
|
-
// `userMessageId`. All blocks are written in a single call to avoid
|
|
1406
|
-
// doubling SQLite SELECT+UPDATE work on every turn.
|
|
1407
|
-
if (
|
|
1408
|
-
injection.blocks.unifiedTurnContext ||
|
|
1409
|
-
injection.blocks.pkbSystemReminder ||
|
|
1410
|
-
injection.blocks.workspaceBlock ||
|
|
1411
|
-
injection.blocks.nowScratchpadBlock ||
|
|
1412
|
-
injection.blocks.pkbContextBlock ||
|
|
1413
|
-
injection.blocks.memoryV2StaticBlock
|
|
1414
|
-
) {
|
|
1415
|
-
try {
|
|
1416
|
-
const metadataUpdates: Record<string, unknown> = {};
|
|
1417
|
-
if (injection.blocks.unifiedTurnContext) {
|
|
1418
|
-
metadataUpdates.turnContextBlock =
|
|
1419
|
-
injection.blocks.unifiedTurnContext;
|
|
1420
|
-
}
|
|
1421
|
-
if (injection.blocks.pkbSystemReminder) {
|
|
1422
|
-
metadataUpdates.pkbSystemReminderBlock =
|
|
1423
|
-
injection.blocks.pkbSystemReminder;
|
|
1424
|
-
}
|
|
1425
|
-
if (injection.blocks.workspaceBlock) {
|
|
1426
|
-
metadataUpdates.workspaceBlock = injection.blocks.workspaceBlock;
|
|
1427
|
-
}
|
|
1428
|
-
if (injection.blocks.nowScratchpadBlock) {
|
|
1429
|
-
metadataUpdates.nowScratchpadBlock =
|
|
1430
|
-
injection.blocks.nowScratchpadBlock;
|
|
1431
|
-
}
|
|
1432
|
-
if (injection.blocks.pkbContextBlock) {
|
|
1433
|
-
metadataUpdates.pkbContextBlock = injection.blocks.pkbContextBlock;
|
|
1434
|
-
}
|
|
1435
|
-
if (injection.blocks.memoryV2StaticBlock) {
|
|
1436
|
-
metadataUpdates.memoryV2StaticBlock =
|
|
1437
|
-
injection.blocks.memoryV2StaticBlock;
|
|
1438
|
-
}
|
|
1439
|
-
updateMessageMetadata(userMessageId, metadataUpdates);
|
|
1440
|
-
} catch (err) {
|
|
1441
|
-
rlog.warn({ err }, "Failed to persist injection metadata (non-fatal)");
|
|
1442
|
-
}
|
|
1443
|
-
}
|
|
1444
|
-
|
|
1445
|
-
// ── Preflight budget evaluation ──────────────────────────────
|
|
1446
|
-
// After runtime injections are applied, estimate the prompt token count
|
|
1447
|
-
// and proactively invoke the reducer if already above budget. This avoids
|
|
1448
|
-
// a wasted provider round-trip that would just fail with context_too_large.
|
|
1449
|
-
const initialContextBudget = resolveCurrentContextBudget();
|
|
1450
|
-
const overflowRecovery = initialContextBudget.overflowRecovery;
|
|
1451
|
-
const preflightBudget = initialContextBudget.preflightBudget;
|
|
1452
|
-
let reducerState: ReducerState | undefined;
|
|
1453
|
-
|
|
1454
|
-
const toolTokenBudget = ctx.agentLoop.getToolTokenBudget(runMessages);
|
|
1455
|
-
// Canonical calibration key — used by the preflight estimate, the
|
|
1456
|
-
// overflow reducer config, and the convergence-path `estimatePromptTokens`
|
|
1457
|
-
// call. Matches the key recorded by `handleUsage` for wrapper providers
|
|
1458
|
-
// (OpenRouter routing to Anthropic → key is `"anthropic"`).
|
|
1459
|
-
const estimationProviderName = getCalibrationProviderKey(ctx.provider);
|
|
1460
|
-
|
|
1461
|
-
const preflightTokens = estimatePromptTokensWithTools(
|
|
1462
|
-
runMessages,
|
|
1463
|
-
ctx.systemPrompt,
|
|
1464
|
-
ctx.agentLoop.getResolvedTools(runMessages),
|
|
1465
|
-
estimationProviderName,
|
|
1466
|
-
);
|
|
1467
|
-
|
|
1468
|
-
if (overflowRecovery.enabled && preflightTokens > preflightBudget) {
|
|
1469
|
-
rlog.warn(
|
|
1470
|
-
{
|
|
1471
|
-
phase: "preflight",
|
|
1472
|
-
estimatedTokens: preflightTokens,
|
|
1473
|
-
budget: preflightBudget,
|
|
1474
|
-
},
|
|
1475
|
-
"Preflight budget exceeded — running overflow reducer before provider call",
|
|
1476
|
-
);
|
|
1477
|
-
|
|
1478
|
-
// Overflow reduction runs through the plugin pipeline. The default
|
|
1479
|
-
// middleware (`default-overflow-reduce`, registered at bootstrap)
|
|
1480
|
-
// contains the historical tier loop — forced compaction → tool-result
|
|
1481
|
-
// truncation → media stubbing → injection downgrade — plus the
|
|
1482
|
-
// re-inject/re-estimate convergence check. The callbacks below are
|
|
1483
|
-
// the orchestrator-specific side effects that the plugin coordinates
|
|
1484
|
-
// per iteration (activity emission, compaction application, runtime
|
|
1485
|
-
// injection reassembly, token re-estimation). Registered plugins that
|
|
1486
|
-
// wrap the `overflowReduce` slot see each iteration through their own
|
|
1487
|
-
// middleware `next` callback.
|
|
1488
|
-
const messagesForPreflightOverflowReduction =
|
|
1489
|
-
slackChronologicalContext?.messages ?? ctx.messages;
|
|
1490
|
-
const overflowArgs: OverflowReduceArgs = {
|
|
1491
|
-
messages: messagesForPreflightOverflowReduction,
|
|
1492
|
-
runMessages,
|
|
1493
|
-
systemPrompt: ctx.systemPrompt,
|
|
1494
|
-
providerName: estimationProviderName,
|
|
1495
|
-
contextWindow: resolveCurrentContextWindowConfig(),
|
|
1496
|
-
preflightBudget,
|
|
1497
|
-
toolTokenBudget,
|
|
1498
|
-
maxAttempts: resolveCurrentContextBudget().overflowRecovery.maxAttempts,
|
|
1499
|
-
abortSignal: abortController.signal,
|
|
1500
|
-
compactFn: async (msgs, signal, opts) => {
|
|
1501
|
-
// Route the reducer's forced-compaction tier through the
|
|
1502
|
-
// `compaction` pipeline so registered plugins observe these
|
|
1503
|
-
// invocations. Without this, custom compaction middleware only
|
|
1504
|
-
// sees the three orchestrator-owned call sites and misses the
|
|
1505
|
-
// reducer-initiated forced compactions entirely.
|
|
1506
|
-
//
|
|
1507
|
-
// Pipeline timeouts must be caught locally — a `PluginTimeoutError`
|
|
1508
|
-
// bubbling out of here would abort the overflow-reducer tier loop
|
|
1509
|
-
// entirely, skipping fallback tiers (tool-result truncation, media
|
|
1510
|
-
// stubbing, injection downgrade) and bypassing circuit-breaker
|
|
1511
|
-
// bookkeeping. On timeout, record the failure and return a
|
|
1512
|
-
// `compacted: false` result so the reducer falls through to the
|
|
1513
|
-
// next tier.
|
|
1514
|
-
try {
|
|
1515
|
-
return (await runPipeline<CompactionArgs, CompactionResult>(
|
|
1516
|
-
"compaction",
|
|
1517
|
-
getMiddlewaresFor("compaction"),
|
|
1518
|
-
(args) =>
|
|
1519
|
-
defaultCompactionTerminal(
|
|
1520
|
-
args,
|
|
1521
|
-
buildPluginTurnContext(ctx, reqId),
|
|
1522
|
-
),
|
|
1523
|
-
{
|
|
1524
|
-
messages: msgs,
|
|
1525
|
-
signal,
|
|
1526
|
-
options: {
|
|
1527
|
-
...(opts ?? {}),
|
|
1528
|
-
overrideProfile: resolveCurrentOverrideProfile() ?? null,
|
|
1529
|
-
actorTrustClass: resolveTurnActorTrustClass(ctx),
|
|
1530
|
-
},
|
|
1531
|
-
},
|
|
1532
|
-
buildPluginTurnContext(ctx, reqId),
|
|
1533
|
-
DEFAULT_TIMEOUTS.compaction,
|
|
1534
|
-
)) as Awaited<
|
|
1535
|
-
ReturnType<typeof ctx.contextWindowManager.maybeCompact>
|
|
1536
|
-
>;
|
|
1537
|
-
} catch (err) {
|
|
1538
|
-
if (err instanceof PluginTimeoutError) {
|
|
1539
|
-
rlog.warn(
|
|
1540
|
-
{ err, phase: "overflow-reducer-forced-compaction" },
|
|
1541
|
-
"Compaction pipeline timed out — falling through to next reducer tier",
|
|
1542
|
-
);
|
|
1543
|
-
await ctx.agentLoop.compactionCircuit.recordOutcome(
|
|
1544
|
-
ctx,
|
|
1545
|
-
true,
|
|
1546
|
-
onEvent,
|
|
1547
|
-
);
|
|
1548
|
-
return {
|
|
1549
|
-
messages: msgs,
|
|
1550
|
-
compacted: false,
|
|
1551
|
-
previousEstimatedInputTokens: 0,
|
|
1552
|
-
estimatedInputTokens: 0,
|
|
1553
|
-
maxInputTokens: 0,
|
|
1554
|
-
thresholdTokens: 0,
|
|
1555
|
-
compactedMessages: 0,
|
|
1556
|
-
compactedPersistedMessages: 0,
|
|
1557
|
-
summaryCalls: 0,
|
|
1558
|
-
summaryInputTokens: 0,
|
|
1559
|
-
summaryOutputTokens: 0,
|
|
1560
|
-
summaryModel: "",
|
|
1561
|
-
summaryText: "",
|
|
1562
|
-
reason: "compaction pipeline timed out",
|
|
1563
|
-
};
|
|
1564
|
-
}
|
|
1565
|
-
throw err;
|
|
1566
|
-
}
|
|
1567
|
-
},
|
|
1568
|
-
emitActivityState: () => {
|
|
1569
|
-
ctx.emitActivityState("thinking", "context_compacting", {
|
|
1570
|
-
requestId: reqId,
|
|
1571
|
-
});
|
|
1572
|
-
},
|
|
1573
|
-
onCompactionResult: async (result, compactedBasis) => {
|
|
1574
|
-
// Track circuit-breaker state whenever the reducer invoked
|
|
1575
|
-
// compaction. The reducer's forced_compaction tier uses
|
|
1576
|
-
// force:true, so it bypasses the open-circuit check, but we
|
|
1577
|
-
// still want failure tracking to detect a run of broken
|
|
1578
|
-
// summaries and clear the counter on success. Only track when
|
|
1579
|
-
// the summary LLM actually ran — `summaryFailed === undefined`
|
|
1580
|
-
// indicates an early return (no eligible messages,
|
|
1581
|
-
// truncation-only path, etc.) that shouldn't influence the
|
|
1582
|
-
// breaker.
|
|
1583
|
-
if (result.summaryFailed !== undefined) {
|
|
1584
|
-
await ctx.agentLoop.compactionCircuit.recordOutcome(
|
|
1585
|
-
ctx,
|
|
1586
|
-
result.summaryFailed,
|
|
1587
|
-
onEvent,
|
|
1588
|
-
);
|
|
1589
|
-
}
|
|
1590
|
-
if (result.compacted) {
|
|
1591
|
-
await applySuccessfulCompaction(result, compactedBasis);
|
|
1592
|
-
state.shouldInjectWorkspace = true;
|
|
1593
|
-
}
|
|
1594
|
-
},
|
|
1595
|
-
reinjectForMode: async (
|
|
1596
|
-
reducedMessages,
|
|
1597
|
-
mode,
|
|
1598
|
-
stepCompacted,
|
|
1599
|
-
accumulatedCompacted,
|
|
1600
|
-
) => {
|
|
1601
|
-
// Mirror the pre-PR-23 behavior: `ctx.messages` must track the
|
|
1602
|
-
// reducer's latest output before re-injection runs, because other
|
|
1603
|
-
// sites consulted through `injectionOpts` (`workspaceTopLevelContext`,
|
|
1604
|
-
// slack history, etc.) depend on it and `applyCompactionResult`
|
|
1605
|
-
// only updates `ctx.messages` on a compaction tier. Assigning here
|
|
1606
|
-
// keeps non-compaction tiers (tool-result truncation, media
|
|
1607
|
-
// stubbing, injection downgrade) observable to downstream
|
|
1608
|
-
// injection assembly on the same turn.
|
|
1609
|
-
ctx.messages = reducedMessages;
|
|
1610
|
-
|
|
1611
|
-
// When THIS iteration compacted, it stripped the existing
|
|
1612
|
-
// memory-static block — so we re-inject current content. A later
|
|
1613
|
-
// iteration that only truncates or downgrades must NOT re-force it,
|
|
1614
|
-
// or each round would grow the token count.
|
|
1615
|
-
// Gate: only the iteration that actually compacted re-injects.
|
|
1616
|
-
// (The `<knowledge_base>`, NOW.md, and v2 static `<info>` blocks
|
|
1617
|
-
// self-gate inside their injectors on whether they are already
|
|
1618
|
-
// present in `reducedMessages`.)
|
|
1619
|
-
const injection = await applyRuntimeInjections(reducedMessages, {
|
|
1620
|
-
...injectionOpts,
|
|
1621
|
-
workspaceTopLevelContext: buildWorkspaceTopLevelContext(
|
|
1622
|
-
ctx,
|
|
1623
|
-
state.shouldInjectWorkspace,
|
|
1624
|
-
),
|
|
1625
|
-
// Once ANY iteration has compacted `ctx.messages`, the captured
|
|
1626
|
-
// `slackChronologicalMessages` snapshot (built from the full
|
|
1627
|
-
// persisted transcript) would overwrite the compacted history
|
|
1628
|
-
// and undo compaction. Suppress the override from here on —
|
|
1629
|
-
// sticky across subsequent non-compacting iterations.
|
|
1630
|
-
slackChronologicalMessages: accumulatedCompacted
|
|
1631
|
-
? null
|
|
1632
|
-
: injectionOpts.slackChronologicalMessages,
|
|
1633
|
-
mode,
|
|
1634
|
-
turnContext: buildPluginTurnContext(ctx, reqId),
|
|
1635
|
-
});
|
|
1636
|
-
let next = injection.messages;
|
|
1637
|
-
if (isTrustedActor && mode !== "minimal") {
|
|
1638
|
-
const memResult = ctx.graphMemory.reinjectCachedMemory(next);
|
|
1639
|
-
next = memResult.runMessages;
|
|
1640
|
-
}
|
|
1641
|
-
return next;
|
|
1642
|
-
},
|
|
1643
|
-
estimatePostInjection: (runMsgs) =>
|
|
1644
|
-
estimatePromptTokens(runMsgs, ctx.systemPrompt, {
|
|
1645
|
-
providerName: estimationProviderName,
|
|
1646
|
-
toolTokenBudget,
|
|
1647
|
-
}),
|
|
1648
|
-
};
|
|
1649
|
-
|
|
1650
|
-
const overflowResult = await runPipeline<
|
|
1651
|
-
OverflowReduceArgs,
|
|
1652
|
-
OverflowReduceResult
|
|
1653
|
-
>(
|
|
1654
|
-
"overflowReduce",
|
|
1655
|
-
getMiddlewaresFor("overflowReduce"),
|
|
1656
|
-
// Terminal — only reached when every registered middleware calls
|
|
1657
|
-
// `next` and delegates past the innermost layer. The default plugin
|
|
1658
|
-
// is a terminal itself (it doesn't call `next`), so in practice
|
|
1659
|
-
// this fallback fires only when the default has been explicitly
|
|
1660
|
-
// deregistered (tests) and no user plugin replaces it. Strict-fail
|
|
1661
|
-
// semantics: throw so the missing terminal surfaces as a visible
|
|
1662
|
-
// error instead of silently returning the history untouched.
|
|
1663
|
-
async () => {
|
|
1664
|
-
throw new PluginExecutionError(
|
|
1665
|
-
"overflowReduce pipeline has no terminal handler — every reducer middleware called next() without providing a replacement",
|
|
1666
|
-
"overflowReduce",
|
|
1667
|
-
);
|
|
1668
|
-
},
|
|
1669
|
-
overflowArgs,
|
|
1670
|
-
buildPluginTurnContext(ctx, reqId),
|
|
1671
|
-
DEFAULT_TIMEOUTS.overflowReduce,
|
|
1672
|
-
);
|
|
1673
|
-
|
|
1674
|
-
ctx.messages = overflowResult.messages;
|
|
1675
|
-
runMessages = overflowResult.runMessages;
|
|
1676
|
-
currentInjectionMode = overflowResult.injectionMode;
|
|
1677
|
-
reducerState = overflowResult.reducerState;
|
|
1678
|
-
if (overflowResult.reducerCompacted) {
|
|
1679
|
-
state.reducerCompacted = true;
|
|
1680
|
-
}
|
|
1681
|
-
}
|
|
1682
|
-
|
|
1683
|
-
// Replace historical web_search_tool_result blocks with text summaries.
|
|
1684
|
-
// The opaque `encrypted_content` tokens Anthropic attaches to each result
|
|
1685
|
-
// expire / are route-scoped; replaying a stale token is rejected with
|
|
1686
|
-
// `Invalid encrypted_content in search_result block`. Titles + URLs
|
|
1687
|
-
// preserve enough context for the model on follow-up turns.
|
|
1688
|
-
const webSearchStrip = stripHistoricalWebSearchResults(runMessages);
|
|
1689
|
-
if (webSearchStrip.stats.blocksStripped > 0) {
|
|
1690
|
-
rlog.info(
|
|
1691
|
-
{ phase: "pre_run", ...webSearchStrip.stats },
|
|
1692
|
-
"Converted historical web_search_tool_result blocks to text summaries",
|
|
1693
|
-
);
|
|
1694
|
-
runMessages = webSearchStrip.messages;
|
|
1695
|
-
}
|
|
1696
|
-
|
|
1697
|
-
// user-prompt-submit hook: plugins may transform `runMessages` right
|
|
1698
|
-
// before the agent loop receives them. Fires once per user turn at the
|
|
1699
|
-
// primary `agentLoop.run` only — the re-entry / retry calls further down
|
|
1700
|
-
// in this function do not refire it (they're not new user submissions).
|
|
1701
|
-
// Plugins may mutate `ctx.latestMessages` in place OR return a new
|
|
1702
|
-
// context with a fresh array; `runHook` forwards whichever the chain
|
|
1703
|
-
// settles on. Order is plugin registration order.
|
|
1704
|
-
//
|
|
1705
|
-
// Fires BEFORE the agent loop runs so the hook-emitted messages are part
|
|
1706
|
-
// of the loop's input; the loop then reports its own appended output via
|
|
1707
|
-
// `AgentLoopRunResult.newMessages`, which is what persistence consumes.
|
|
878
|
+
// user-prompt-submit hook chain. Fires once per user turn at the primary
|
|
879
|
+
// `agentLoop.run` (the re-entry / retry calls further down do not refire it
|
|
880
|
+
// — they're not new user submissions), before the loop runs so the
|
|
881
|
+
// hook-assembled messages are part of its input. Memory retrieval runs
|
|
882
|
+
// first — fetching PKB / NOW.md / memory-graph outputs, persisting its own
|
|
883
|
+
// side effects (injected-block metadata, recall log, `memory_recalled`
|
|
884
|
+
// event), and assembling the turn's runtime-injection blocks onto the
|
|
885
|
+
// history — followed by history repair and title generation, which see the
|
|
886
|
+
// fully injected history. Plugins may mutate `ctx.latestMessages` in place
|
|
887
|
+
// OR return a new context with a fresh array; `runHook` forwards whichever
|
|
888
|
+
// the chain settles on, in plugin registration order. The loop then reports
|
|
889
|
+
// its own appended output via `AgentLoopRunResult.newMessages`, which
|
|
890
|
+
// persistence consumes.
|
|
1708
891
|
const userPromptCtx: UserPromptSubmitContext = {
|
|
1709
892
|
conversationId: ctx.conversationId,
|
|
893
|
+
userMessageId,
|
|
894
|
+
requestId: reqId,
|
|
1710
895
|
prompt: options?.titleText ?? content,
|
|
1711
896
|
originalMessages: ctx.messages,
|
|
1712
|
-
latestMessages:
|
|
897
|
+
latestMessages: ctx.messages,
|
|
1713
898
|
logger: rlog,
|
|
899
|
+
modelProfileKey,
|
|
900
|
+
isNonInteractive,
|
|
1714
901
|
};
|
|
1715
902
|
const finalUserPromptCtx = await runHook(
|
|
1716
903
|
HOOKS.USER_PROMPT_SUBMIT,
|
|
1717
904
|
userPromptCtx,
|
|
1718
905
|
);
|
|
1719
|
-
runMessages = finalUserPromptCtx.latestMessages;
|
|
906
|
+
let runMessages = finalUserPromptCtx.latestMessages;
|
|
907
|
+
|
|
908
|
+
// Reducer state, tool-token budget, and calibration provider key consumed
|
|
909
|
+
// by the post-rejection convergence loop further down. The tool-token
|
|
910
|
+
// budget is resolved once per turn (the resolved tool set is stable across
|
|
911
|
+
// the turn); the calibration key matches the key recorded by `handleUsage`
|
|
912
|
+
// for wrapper providers (OpenRouter routing to Anthropic → key is
|
|
913
|
+
// `"anthropic"`).
|
|
914
|
+
let reducerState: ReducerState | undefined;
|
|
915
|
+
const toolTokenBudget = ctx.agentLoop.getToolTokenBudget(runMessages);
|
|
916
|
+
const estimationProviderName = getCalibrationProviderKey(ctx.provider);
|
|
1720
917
|
|
|
1721
918
|
const shouldGenerateTitle = isReplaceableTitle(
|
|
1722
919
|
getConversation(ctx.conversationId)?.title ?? null,
|
|
@@ -1750,75 +947,45 @@ export async function runAgentLoopImpl(
|
|
|
1750
947
|
|
|
1751
948
|
rlog.info({ callSite: turnCallSite }, "Starting agent loop run");
|
|
1752
949
|
|
|
1753
|
-
//
|
|
1754
|
-
//
|
|
1755
|
-
//
|
|
1756
|
-
// the
|
|
1757
|
-
//
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
// trigger (its budget gate), the `compaction` pipeline call, the result
|
|
1762
|
-
// interpretation (circuit-breaker bookkeeping + the exhaustion decision),
|
|
1763
|
-
// and the inline continue; this callback bridges the injection state the
|
|
1764
|
-
// loop is intentionally blind to. Durable persistence is signalled via
|
|
1765
|
-
// events; re-injection stays orchestrator-supplied for now.
|
|
1766
|
-
const midLoopCompaction: MidLoopCompaction = {
|
|
1767
|
-
postCompactionHook: async ({ history, turnContext }) => {
|
|
1768
|
-
// stripInjectionsForCompaction() unconditionally removed the existing
|
|
1769
|
-
// memory-static block, so re-inject the current content regardless of
|
|
1770
|
-
// whether compaction actually ran. The `<knowledge_base>`, NOW.md, and
|
|
1771
|
-
// v2 static `<info>` blocks self-gate inside their injectors on block
|
|
1772
|
-
// presence.
|
|
1773
|
-
const injection = await postCompactReinject({
|
|
1774
|
-
...injectionOpts,
|
|
1775
|
-
workspaceTopLevelContext: buildWorkspaceTopLevelContext(
|
|
1776
|
-
ctx,
|
|
1777
|
-
state.shouldInjectWorkspace,
|
|
1778
|
-
),
|
|
1779
|
-
// Suppress the chronological-transcript snapshot once the reducer
|
|
1780
|
-
// has collapsed `ctx.messages`; the captured snapshot reflects the
|
|
1781
|
-
// full persisted transcript and would overwrite compaction.
|
|
1782
|
-
slackChronologicalMessages: state.reducerCompacted
|
|
1783
|
-
? null
|
|
1784
|
-
: injectionOpts.slackChronologicalMessages,
|
|
1785
|
-
mode: currentInjectionMode,
|
|
1786
|
-
turnContext,
|
|
1787
|
-
history,
|
|
1788
|
-
isTrustedActor,
|
|
1789
|
-
logger: rlog,
|
|
1790
|
-
});
|
|
1791
|
-
return injection.messages;
|
|
1792
|
-
},
|
|
1793
|
-
};
|
|
950
|
+
// Trust snapshot the loop forwards to its mid-loop in-place compaction
|
|
951
|
+
// (scoping the compactor's image manifest) and the post-compaction
|
|
952
|
+
// re-injection. Prefers the per-turn snapshot, then the conversation-level
|
|
953
|
+
// context, then the fallback — matching the trust the runtime injection
|
|
954
|
+
// assembly resolves for the same turn. The loop's other turn-identity
|
|
955
|
+
// fields self-resolve from its own conversation id.
|
|
956
|
+
const loopTrust =
|
|
957
|
+
ctx.currentTurnTrustContext ?? ctx.trustContext ?? FALLBACK_TURN_TRUST;
|
|
1794
958
|
|
|
1795
959
|
/**
|
|
1796
960
|
* Shared closure: runs the agent loop with the orchestrator's turn
|
|
1797
961
|
* context and maps the loop's returned checkpoint pause-reason into the
|
|
1798
962
|
* orchestrator's yield bookkeeping. Returns the updated history so call
|
|
1799
|
-
* sites consume it exactly as before. Pass `
|
|
1800
|
-
* primary run
|
|
1801
|
-
*
|
|
963
|
+
* sites consume it exactly as before. Pass `compactInPlace` only for the
|
|
964
|
+
* primary run: the loop then runs its budget gate before the first call
|
|
965
|
+
* (subsuming the proactive turn-start compaction) and compacts in place
|
|
966
|
+
* whenever the gate trips. Reruns omit it, skip the first-call gate, and
|
|
967
|
+
* keep yielding for budget.
|
|
1802
968
|
*/
|
|
1803
969
|
const runAgentLoop = async (
|
|
1804
970
|
msgs: Message[],
|
|
1805
|
-
|
|
971
|
+
compactInPlace = false,
|
|
1806
972
|
): Promise<Message[]> => {
|
|
1807
973
|
const { history, exitReason, appendedNewMessages, newMessages } =
|
|
1808
|
-
await ctx.agentLoop.run(
|
|
974
|
+
await ctx.agentLoop.run({
|
|
975
|
+
messages: msgs,
|
|
976
|
+
onEvent: eventHandler,
|
|
1809
977
|
signal: abortController.signal,
|
|
1810
978
|
requestId: reqId,
|
|
1811
979
|
onCheckpoint,
|
|
1812
980
|
callSite: turnCallSite,
|
|
1813
|
-
|
|
981
|
+
trust: loopTrust,
|
|
1814
982
|
overrideProfile: turnOverrideProfile,
|
|
1815
983
|
resolveOverrideProfile: resolveCurrentOverrideProfile,
|
|
1816
984
|
resolveContextWindow,
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
mutableLatestUserMessage: memoryV3Live,
|
|
985
|
+
compactInPlace,
|
|
986
|
+
isNonInteractive,
|
|
987
|
+
modelProfile: modelProfileStr,
|
|
988
|
+
actorContext,
|
|
1822
989
|
});
|
|
1823
990
|
lastRunAppendedNewMessages = appendedNewMessages;
|
|
1824
991
|
lastRunNewMessages = newMessages;
|
|
@@ -1832,7 +999,7 @@ export async function runAgentLoopImpl(
|
|
|
1832
999
|
return history;
|
|
1833
1000
|
};
|
|
1834
1001
|
|
|
1835
|
-
let updatedHistory = await runAgentLoop(runMessages,
|
|
1002
|
+
let updatedHistory = await runAgentLoop(runMessages, true);
|
|
1836
1003
|
|
|
1837
1004
|
rlog.info(
|
|
1838
1005
|
{ resultMessageCount: updatedHistory.length },
|
|
@@ -1875,8 +1042,6 @@ export async function runAgentLoopImpl(
|
|
|
1875
1042
|
// intentionally deferred until there's a concrete plugin-level use case.
|
|
1876
1043
|
const retryRepair = deepRepairHistory(updatedHistory);
|
|
1877
1044
|
runMessages = retryRepair.messages;
|
|
1878
|
-
const retryStrip = stripHistoricalWebSearchResults(runMessages);
|
|
1879
|
-
runMessages = retryStrip.messages;
|
|
1880
1045
|
state.orderingErrorDetected = false;
|
|
1881
1046
|
state.deferredOrderingError = null;
|
|
1882
1047
|
|
|
@@ -1892,44 +1057,41 @@ export async function runAgentLoopImpl(
|
|
|
1892
1057
|
|
|
1893
1058
|
// ── Image-dimension overflow recovery ──────────────────────────
|
|
1894
1059
|
// When the provider rejects because an image block exceeds its pixel
|
|
1895
|
-
// cap,
|
|
1896
|
-
//
|
|
1897
|
-
//
|
|
1898
|
-
//
|
|
1899
|
-
//
|
|
1060
|
+
// or payload cap, recover every oversized image in ctx.messages and
|
|
1061
|
+
// retry once. recoverImageBlock downscales an oversized image, or swaps
|
|
1062
|
+
// it for a text note when resize is a no-op (e.g. sips unavailable
|
|
1063
|
+
// off macOS), while leaving still-sendable images untouched. This covers
|
|
1064
|
+
// both top-level image blocks (user uploads) and images nested inside a
|
|
1065
|
+
// tool_result's contentBlocks (e.g. a browser screenshot), which is where
|
|
1066
|
+
// the rejected block usually lives.
|
|
1900
1067
|
if (state.imageTooLargeDetected) {
|
|
1901
1068
|
state.imageTooLargeDetected = false;
|
|
1902
1069
|
rlog.warn(
|
|
1903
1070
|
{ phase: "image-recovery" },
|
|
1904
|
-
"Image too large —
|
|
1071
|
+
"Image too large — recovering oversized image blocks and retrying",
|
|
1905
1072
|
);
|
|
1906
1073
|
ctx.messages = ctx.messages.map((msg) => {
|
|
1907
1074
|
if (!Array.isArray(msg.content)) return msg;
|
|
1908
|
-
if (!msg.content
|
|
1075
|
+
if (!messageHasImageBlock(msg.content)) return msg;
|
|
1909
1076
|
return {
|
|
1910
1077
|
...msg,
|
|
1911
1078
|
content: msg.content.flatMap((b): ContentBlock[] => {
|
|
1912
|
-
if (b.type
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
if (
|
|
1918
|
-
// sips managed to downscale — use the smaller version
|
|
1079
|
+
if (b.type === "image") return [recoverImageBlock(b)];
|
|
1080
|
+
// Images returned by a tool (e.g. browser_screenshot) live in
|
|
1081
|
+
// the tool_result's contentBlocks, not as top-level blocks.
|
|
1082
|
+
// Recover them in place so the tool_use/tool_result pairing
|
|
1083
|
+
// stays intact rather than dropping the whole tool_result.
|
|
1084
|
+
if (b.type === "tool_result" && b.contentBlocks?.length) {
|
|
1919
1085
|
return [
|
|
1920
1086
|
{
|
|
1921
1087
|
...b,
|
|
1922
|
-
|
|
1923
|
-
type
|
|
1924
|
-
|
|
1925
|
-
data: resized.data,
|
|
1926
|
-
},
|
|
1088
|
+
contentBlocks: b.contentBlocks.map((cb) =>
|
|
1089
|
+
cb.type === "image" ? recoverImageBlock(cb) : cb,
|
|
1090
|
+
),
|
|
1927
1091
|
},
|
|
1928
1092
|
];
|
|
1929
1093
|
}
|
|
1930
|
-
|
|
1931
|
-
// can explain the situation rather than silently dropping context
|
|
1932
|
-
return [{ type: "text" as const, text: UNSENDABLE_IMAGE_NOTE }];
|
|
1094
|
+
return [b];
|
|
1933
1095
|
}),
|
|
1934
1096
|
};
|
|
1935
1097
|
});
|
|
@@ -2007,101 +1169,76 @@ export async function runAgentLoopImpl(
|
|
|
2007
1169
|
},
|
|
2008
1170
|
);
|
|
2009
1171
|
const convergenceBudget = resolveCurrentContextBudget();
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
}
|
|
1172
|
+
const { targetTokens: correctedTarget, estimationErrorRatio } =
|
|
1173
|
+
computeCorrectedOverflowTarget({
|
|
1174
|
+
preflightBudget: convergenceBudget.preflightBudget,
|
|
1175
|
+
actualTokens,
|
|
1176
|
+
estimatedTokens: estimatedTokensAtOverflow,
|
|
1177
|
+
});
|
|
1178
|
+
if (estimationErrorRatio != null) {
|
|
1179
|
+
rlog.warn(
|
|
1180
|
+
{
|
|
1181
|
+
phase: "convergence",
|
|
1182
|
+
actualTokens,
|
|
1183
|
+
estimatedTokens: estimatedTokensAtOverflow,
|
|
1184
|
+
estimationErrorRatio: estimationErrorRatio.toFixed(2),
|
|
1185
|
+
preflightBudget: convergenceBudget.preflightBudget,
|
|
1186
|
+
correctedTarget,
|
|
1187
|
+
},
|
|
1188
|
+
"Adjusting compaction target based on observed estimation error",
|
|
1189
|
+
);
|
|
2029
1190
|
}
|
|
2030
1191
|
|
|
2031
|
-
// ──
|
|
2032
|
-
//
|
|
2033
|
-
// emergency
|
|
2034
|
-
//
|
|
2035
|
-
//
|
|
2036
|
-
//
|
|
2037
|
-
//
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
{
|
|
2059
|
-
phase: "convergence",
|
|
2060
|
-
compactedMessages: emergencyResult.compactedMessages,
|
|
2061
|
-
summaryChars: emergencyResult.summaryText.length,
|
|
2062
|
-
},
|
|
2063
|
-
"Emergency mid-turn compaction succeeded — bypassing reducer tiers",
|
|
2064
|
-
);
|
|
2065
|
-
if (emergencyResult.summaryFailed !== undefined) {
|
|
2066
|
-
await ctx.agentLoop.compactionCircuit.recordOutcome(
|
|
2067
|
-
ctx,
|
|
2068
|
-
emergencyResult.summaryFailed,
|
|
2069
|
-
onEvent,
|
|
2070
|
-
);
|
|
2071
|
-
}
|
|
2072
|
-
if (emergencyResult.compacted) {
|
|
2073
|
-
await applySuccessfulCompaction(emergencyResult, ctx.messages);
|
|
2074
|
-
state.shouldInjectWorkspace = true;
|
|
2075
|
-
}
|
|
2076
|
-
// Clear the overflow flag and re-run the agent loop with
|
|
2077
|
-
// the compacted context.
|
|
2078
|
-
state.contextTooLargeDetected = false;
|
|
2079
|
-
}
|
|
2080
|
-
} catch (err) {
|
|
2081
|
-
rlog.warn(
|
|
2082
|
-
{ phase: "convergence", err },
|
|
2083
|
-
"Emergency mid-turn compaction failed; continuing to reducer tiers",
|
|
2084
|
-
);
|
|
2085
|
-
}
|
|
2086
|
-
// If emergency compaction failed, fall through to reducer tiers.
|
|
2087
|
-
}
|
|
1192
|
+
// ── Bounded context-overflow convergence driver ─────────────────
|
|
1193
|
+
// The compaction plugin's reduction ladder owns every rung: the
|
|
1194
|
+
// emergency summarize-around-last-tool-pair, the middle tiers (forced
|
|
1195
|
+
// compaction, tool-result truncation, media stubbing, injection
|
|
1196
|
+
// downgrade), and the terminal auto-compress of the latest turn. This
|
|
1197
|
+
// driver applies one rung per iteration, re-injects, and re-runs the
|
|
1198
|
+
// agent loop until the context fits or the ladder is exhausted. The
|
|
1199
|
+
// overflow policy decides whether the terminal auto-compress rung is
|
|
1200
|
+
// permitted; the ladder never makes that policy call itself.
|
|
1201
|
+
const overflowAction = resolveOverflowAction({
|
|
1202
|
+
overflowRecovery: convergenceBudget.overflowRecovery,
|
|
1203
|
+
isInteractive: isInteractiveResolved,
|
|
1204
|
+
});
|
|
1205
|
+
const reducerConfig = {
|
|
1206
|
+
providerName: estimationProviderName,
|
|
1207
|
+
systemPrompt: ctx.systemPrompt,
|
|
1208
|
+
contextWindow: resolveCurrentContextWindowConfig(),
|
|
1209
|
+
targetTokens: correctedTarget,
|
|
1210
|
+
toolTokenBudget,
|
|
1211
|
+
conversationId: ctx.conversationId,
|
|
1212
|
+
overrideProfile: resolveCurrentOverrideProfile() ?? null,
|
|
1213
|
+
actorTrustClass: resolveTurnActorTrustClass(ctx),
|
|
1214
|
+
previousEstimatedInputTokens: estimatedTokensAtOverflow,
|
|
1215
|
+
maxMiddleTierAttempts: convergenceBudget.overflowRecovery.maxAttempts,
|
|
1216
|
+
allowAutoCompressLatestTurn:
|
|
1217
|
+
overflowAction === "auto_compress_latest_turn",
|
|
1218
|
+
};
|
|
2088
1219
|
|
|
2089
|
-
|
|
2090
|
-
|
|
1220
|
+
// Safety backstop against a reduction ladder that never reports
|
|
1221
|
+
// exhaustion: the ladder can apply at most the emergency rung, the
|
|
1222
|
+
// middle-tier attempt budget, and the terminal auto-compress rung, so a
|
|
1223
|
+
// well-behaved reducer always sets `exhausted` within this many steps.
|
|
1224
|
+
// The cap only guards against a misbehaving reducer; it never truncates a
|
|
1225
|
+
// legitimate escalation.
|
|
1226
|
+
const maxConvergenceRungs = reducerConfig.maxMiddleTierAttempts + 2;
|
|
1227
|
+
let convergenceRungs = 0;
|
|
2091
1228
|
|
|
2092
1229
|
while (
|
|
2093
1230
|
state.contextTooLargeDetected &&
|
|
2094
|
-
|
|
2095
|
-
|
|
1231
|
+
!reducerState.exhausted &&
|
|
1232
|
+
convergenceRungs < maxConvergenceRungs
|
|
2096
1233
|
) {
|
|
2097
|
-
|
|
1234
|
+
convergenceRungs++;
|
|
2098
1235
|
rlog.warn(
|
|
2099
1236
|
{
|
|
2100
1237
|
phase: "convergence",
|
|
2101
|
-
|
|
1238
|
+
rung: convergenceRungs,
|
|
2102
1239
|
appliedTiers: reducerState.appliedTiers,
|
|
2103
1240
|
},
|
|
2104
|
-
"Context too large — applying next
|
|
1241
|
+
"Context too large — applying next reduction rung",
|
|
2105
1242
|
);
|
|
2106
1243
|
|
|
2107
1244
|
ctx.emitActivityState("thinking", "context_compacting", {
|
|
@@ -2110,20 +1247,8 @@ export async function runAgentLoopImpl(
|
|
|
2110
1247
|
const convergenceCompactionBasis = ctx.messages;
|
|
2111
1248
|
const step = await reduceContextOverflow(
|
|
2112
1249
|
convergenceCompactionBasis,
|
|
2113
|
-
|
|
2114
|
-
providerName: estimationProviderName,
|
|
2115
|
-
systemPrompt: ctx.systemPrompt,
|
|
2116
|
-
contextWindow: resolveCurrentContextWindowConfig(),
|
|
2117
|
-
targetTokens: correctedTarget,
|
|
2118
|
-
toolTokenBudget,
|
|
2119
|
-
},
|
|
1250
|
+
reducerConfig,
|
|
2120
1251
|
reducerState,
|
|
2121
|
-
(msgs, signal, opts) =>
|
|
2122
|
-
ctx.contextWindowManager.maybeCompact(msgs, signal!, {
|
|
2123
|
-
...(opts ?? {}),
|
|
2124
|
-
overrideProfile: resolveCurrentOverrideProfile() ?? null,
|
|
2125
|
-
actorTrustClass: resolveTurnActorTrustClass(ctx),
|
|
2126
|
-
}),
|
|
2127
1252
|
abortController.signal,
|
|
2128
1253
|
);
|
|
2129
1254
|
|
|
@@ -2131,16 +1256,14 @@ export async function runAgentLoopImpl(
|
|
|
2131
1256
|
ctx.messages = step.messages;
|
|
2132
1257
|
currentInjectionMode = step.state.injectionMode;
|
|
2133
1258
|
|
|
2134
|
-
//
|
|
2135
|
-
//
|
|
2136
|
-
//
|
|
2137
|
-
// without calling the summary LLM.
|
|
1259
|
+
// Only track when the summary LLM actually ran — `summaryFailed
|
|
1260
|
+
// === undefined` indicates a forced-compaction early-return path that
|
|
1261
|
+
// never called the summary LLM.
|
|
2138
1262
|
if (
|
|
2139
1263
|
step.compactionResult &&
|
|
2140
1264
|
step.compactionResult.summaryFailed !== undefined
|
|
2141
1265
|
) {
|
|
2142
1266
|
await ctx.agentLoop.compactionCircuit.recordOutcome(
|
|
2143
|
-
ctx,
|
|
2144
1267
|
step.compactionResult.summaryFailed,
|
|
2145
1268
|
onEvent,
|
|
2146
1269
|
);
|
|
@@ -2151,8 +1274,6 @@ export async function runAgentLoopImpl(
|
|
|
2151
1274
|
step.compactionResult,
|
|
2152
1275
|
convergenceCompactionBasis,
|
|
2153
1276
|
);
|
|
2154
|
-
state.shouldInjectWorkspace = true;
|
|
2155
|
-
state.reducerCompacted = true;
|
|
2156
1277
|
}
|
|
2157
1278
|
|
|
2158
1279
|
// Only re-inject the memory-static block when ctx.messages was
|
|
@@ -2161,174 +1282,55 @@ export async function runAgentLoopImpl(
|
|
|
2161
1282
|
// blocks self-gate inside their injectors on whether they are already
|
|
2162
1283
|
// present in `ctx.messages`.)
|
|
2163
1284
|
const injection = await applyRuntimeInjections(ctx.messages, {
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
state.shouldInjectWorkspace,
|
|
2168
|
-
),
|
|
2169
|
-
slackChronologicalMessages: state.reducerCompacted
|
|
2170
|
-
? null
|
|
2171
|
-
: injectionOpts.slackChronologicalMessages,
|
|
1285
|
+
isNonInteractive,
|
|
1286
|
+
modelProfile: modelProfileStr,
|
|
1287
|
+
actorContext,
|
|
2172
1288
|
mode: currentInjectionMode,
|
|
2173
|
-
|
|
1289
|
+
requestId: reqId,
|
|
1290
|
+
conversationId: ctx.conversationId,
|
|
2174
1291
|
});
|
|
2175
1292
|
runMessages = injection.messages;
|
|
2176
1293
|
if (isTrustedActor && currentInjectionMode !== "minimal") {
|
|
2177
1294
|
ctx.graphMemory.retrackCachedNodes();
|
|
2178
1295
|
}
|
|
2179
|
-
const convergenceStrip = stripHistoricalWebSearchResults(runMessages);
|
|
2180
|
-
if (convergenceStrip.stats.blocksStripped > 0) {
|
|
2181
|
-
rlog.info(
|
|
2182
|
-
{ phase: "convergence", ...convergenceStrip.stats },
|
|
2183
|
-
"Converted historical web_search_tool_result blocks to text summaries",
|
|
2184
|
-
);
|
|
2185
|
-
runMessages = convergenceStrip.messages;
|
|
2186
|
-
}
|
|
2187
1296
|
state.contextTooLargeDetected = false;
|
|
2188
1297
|
yieldedForBudget = false;
|
|
2189
1298
|
|
|
2190
1299
|
updatedHistory = await runAgentLoop(runMessages);
|
|
2191
1300
|
|
|
2192
|
-
// If the rerun still yields at checkpoint, the turn is still
|
|
2193
|
-
// incomplete — continue reducing through the remaining tiers
|
|
2194
|
-
// instead of silently dropping the incomplete state.
|
|
2195
1301
|
if (yieldedForBudget && !abortController.signal.aborted) {
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
},
|
|
2202
|
-
"Post-convergence rerun still yielded at checkpoint — continuing reduction",
|
|
2203
|
-
);
|
|
2204
|
-
state.contextTooLargeDetected = true;
|
|
2205
|
-
|
|
2206
|
-
// Fold rerun progress into ctx.messages so the next reducer
|
|
2207
|
-
// tier operates on up-to-date history instead of stale
|
|
2208
|
-
// pre-rerun messages.
|
|
2209
|
-
if (lastRunAppendedNewMessages) {
|
|
2210
|
-
ctx.messages = stripInjectionsForCompaction(updatedHistory);
|
|
2211
|
-
markHistoryStrippedBestEffort(ctx.conversationId);
|
|
2212
|
-
}
|
|
2213
|
-
}
|
|
2214
|
-
}
|
|
2215
|
-
|
|
2216
|
-
// All reducer tiers exhausted but provider still rejects —
|
|
2217
|
-
// consult the overflow policy for latest-turn compression.
|
|
2218
|
-
// The policy either auto-compresses the latest turn or falls
|
|
2219
|
-
// through to the final graceful-error fallback below.
|
|
2220
|
-
if (state.contextTooLargeDetected) {
|
|
2221
|
-
const action = resolveOverflowAction({
|
|
2222
|
-
overflowRecovery,
|
|
2223
|
-
isInteractive: isInteractiveResolved,
|
|
2224
|
-
});
|
|
2225
|
-
|
|
2226
|
-
if (action === "auto_compress_latest_turn") {
|
|
2227
|
-
// Auto-compress without asking — users opt out via the "drop" policy.
|
|
2228
|
-
ctx.emitActivityState("thinking", "context_compacting", {
|
|
2229
|
-
requestId: reqId,
|
|
2230
|
-
});
|
|
2231
|
-
let emergencyCompact: Awaited<
|
|
2232
|
-
ReturnType<typeof ctx.contextWindowManager.maybeCompact>
|
|
2233
|
-
> | null = null;
|
|
2234
|
-
try {
|
|
2235
|
-
emergencyCompact = (await runPipeline<
|
|
2236
|
-
CompactionArgs,
|
|
2237
|
-
CompactionResult
|
|
2238
|
-
>(
|
|
2239
|
-
"compaction",
|
|
2240
|
-
getMiddlewaresFor("compaction"),
|
|
2241
|
-
(args) =>
|
|
2242
|
-
defaultCompactionTerminal(
|
|
2243
|
-
args,
|
|
2244
|
-
buildPluginTurnContext(ctx, reqId),
|
|
2245
|
-
),
|
|
1302
|
+
if (!reducerState.exhausted) {
|
|
1303
|
+
// The rerun still yields and the ladder has a rung left — the turn
|
|
1304
|
+
// is incomplete, so continue reducing instead of silently dropping
|
|
1305
|
+
// the incomplete state.
|
|
1306
|
+
rlog.warn(
|
|
2246
1307
|
{
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
options: {
|
|
2250
|
-
force: true,
|
|
2251
|
-
minKeepRecentUserTurns: 0,
|
|
2252
|
-
overrideProfile: resolveCurrentOverrideProfile() ?? null,
|
|
2253
|
-
},
|
|
1308
|
+
phase: "convergence",
|
|
1309
|
+
appliedTiers: reducerState.appliedTiers,
|
|
2254
1310
|
},
|
|
2255
|
-
|
|
2256
|
-
DEFAULT_TIMEOUTS.compaction,
|
|
2257
|
-
)) as Awaited<
|
|
2258
|
-
ReturnType<typeof ctx.contextWindowManager.maybeCompact>
|
|
2259
|
-
>;
|
|
2260
|
-
} catch (err) {
|
|
2261
|
-
if (err instanceof PluginTimeoutError) {
|
|
2262
|
-
// Emergency compaction timed out. Record the circuit-breaker
|
|
2263
|
-
// failure and fall through to the graceful-error path below
|
|
2264
|
-
// (the unsuccessful-compaction fallback) rather than hard-
|
|
2265
|
-
// failing the turn.
|
|
2266
|
-
rlog.warn(
|
|
2267
|
-
{ err, phase: "emergency-compaction" },
|
|
2268
|
-
"Emergency compaction pipeline timed out — continuing with overflow fallback",
|
|
2269
|
-
);
|
|
2270
|
-
await ctx.agentLoop.compactionCircuit.recordOutcome(
|
|
2271
|
-
ctx,
|
|
2272
|
-
true,
|
|
2273
|
-
onEvent,
|
|
2274
|
-
);
|
|
2275
|
-
emergencyCompact = null;
|
|
2276
|
-
} else {
|
|
2277
|
-
throw err;
|
|
2278
|
-
}
|
|
2279
|
-
}
|
|
2280
|
-
// Only track when the summary LLM actually ran; `force: true`
|
|
2281
|
-
// bypasses the auto-threshold gate but not the early-return paths.
|
|
2282
|
-
if (
|
|
2283
|
-
emergencyCompact &&
|
|
2284
|
-
emergencyCompact.summaryFailed !== undefined
|
|
2285
|
-
) {
|
|
2286
|
-
await ctx.agentLoop.compactionCircuit.recordOutcome(
|
|
2287
|
-
ctx,
|
|
2288
|
-
emergencyCompact.summaryFailed,
|
|
2289
|
-
onEvent,
|
|
1311
|
+
"Post-convergence rerun still yielded at checkpoint — continuing reduction",
|
|
2290
1312
|
);
|
|
2291
|
-
|
|
2292
|
-
if (emergencyCompact?.compacted) {
|
|
2293
|
-
await applySuccessfulCompaction(emergencyCompact, ctx.messages);
|
|
2294
|
-
state.reducerCompacted = true;
|
|
2295
|
-
state.shouldInjectWorkspace = true;
|
|
2296
|
-
}
|
|
1313
|
+
state.contextTooLargeDetected = true;
|
|
2297
1314
|
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
)
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
runMessages = injection.messages;
|
|
2316
|
-
if (isTrustedActor && currentInjectionMode !== "minimal") {
|
|
2317
|
-
ctx.graphMemory.retrackCachedNodes();
|
|
2318
|
-
}
|
|
2319
|
-
const fallbackStrip = stripHistoricalWebSearchResults(runMessages);
|
|
2320
|
-
if (fallbackStrip.stats.blocksStripped > 0) {
|
|
2321
|
-
rlog.info(
|
|
2322
|
-
{ phase: "fail_gracefully_compact", ...fallbackStrip.stats },
|
|
2323
|
-
"Converted historical web_search_tool_result blocks to text summaries",
|
|
2324
|
-
);
|
|
2325
|
-
runMessages = fallbackStrip.messages;
|
|
1315
|
+
// Fold rerun progress into ctx.messages so the next rung operates
|
|
1316
|
+
// on up-to-date history instead of stale pre-rerun messages.
|
|
1317
|
+
if (lastRunAppendedNewMessages) {
|
|
1318
|
+
ctx.messages = stripInjectionsForCompaction(updatedHistory);
|
|
1319
|
+
markHistoryStrippedBestEffort(ctx.conversationId);
|
|
1320
|
+
}
|
|
1321
|
+
} else if (
|
|
1322
|
+
!reducerState.appliedTiers.includes("auto_compress_latest_turn")
|
|
1323
|
+
) {
|
|
1324
|
+
// The ladder is exhausted without the terminal auto-compress rung
|
|
1325
|
+
// (the overflow policy disallowed it, or the middle tiers ran out)
|
|
1326
|
+
// and the turn is still over budget. This is a genuine
|
|
1327
|
+
// unrecoverable overflow — surface `context_too_large` via the
|
|
1328
|
+
// fallback below. The terminal auto-compress rung instead leaves
|
|
1329
|
+
// this false so its post-rerun yield reads as
|
|
1330
|
+
// `budget_yield_unrecovered`.
|
|
1331
|
+
state.contextTooLargeDetected = true;
|
|
2326
1332
|
}
|
|
2327
|
-
state.contextTooLargeDetected = false;
|
|
2328
|
-
|
|
2329
|
-
updatedHistory = await runAgentLoop(runMessages);
|
|
2330
1333
|
}
|
|
2331
|
-
// action === "fail_gracefully" falls through to the final error below
|
|
2332
1334
|
}
|
|
2333
1335
|
|
|
2334
1336
|
// Final fallback: all recovery paths exhausted
|
|
@@ -2906,10 +1908,7 @@ export async function runAgentLoopImpl(
|
|
|
2906
1908
|
// ── Helper ───────────────────────────────────────────────────────────
|
|
2907
1909
|
|
|
2908
1910
|
function emitUsage(
|
|
2909
|
-
ctx: Pick<
|
|
2910
|
-
AgentLoopConversationContext,
|
|
2911
|
-
"conversationId" | "provider" | "usageStats"
|
|
2912
|
-
>,
|
|
1911
|
+
ctx: Pick<Conversation, "conversationId" | "provider" | "usageStats">,
|
|
2913
1912
|
inputTokens: number,
|
|
2914
1913
|
outputTokens: number,
|
|
2915
1914
|
model: string,
|
|
@@ -2949,17 +1948,18 @@ function emitUsage(
|
|
|
2949
1948
|
}
|
|
2950
1949
|
|
|
2951
1950
|
/**
|
|
2952
|
-
* Minimal context shape consumed by `applyCompactionResult
|
|
2953
|
-
* `
|
|
2954
|
-
*
|
|
2955
|
-
*
|
|
1951
|
+
* Minimal context shape consumed by `applyCompactionResult`, satisfied by
|
|
1952
|
+
* `Conversation` via structural typing, so the helper can back both the 5
|
|
1953
|
+
* agent-loop auto-compaction sites and the single `forceCompact`
|
|
1954
|
+
* user-initiated site.
|
|
2956
1955
|
*/
|
|
2957
1956
|
export interface CompactionApplyContext {
|
|
2958
1957
|
readonly conversationId: string;
|
|
2959
1958
|
messages: Message[];
|
|
2960
1959
|
contextCompactedMessageCount: number;
|
|
2961
1960
|
contextCompactedAt: number | null;
|
|
2962
|
-
|
|
1961
|
+
contextSummary: string | null;
|
|
1962
|
+
slackContextCompactionWatermarkTs: string | null;
|
|
2963
1963
|
readonly graphMemory: ConversationGraphMemory;
|
|
2964
1964
|
readonly provider: Provider;
|
|
2965
1965
|
usageStats: UsageStats;
|
|
@@ -3006,13 +2006,22 @@ export async function applyCompactionResult(
|
|
|
3006
2006
|
} = {},
|
|
3007
2007
|
): Promise<void> {
|
|
3008
2008
|
ctx.messages = result.messages;
|
|
3009
|
-
|
|
2009
|
+
// Compaction operates on the in-context history. Untrusted actor views
|
|
2010
|
+
// render that history unsliced (boundary 0); trusted views start past the
|
|
2011
|
+
// already-compacted prefix (the mirrored DB count). Advance from that
|
|
2012
|
+
// in-context boundary rather than the raw mirror so the persisted count
|
|
2013
|
+
// stays consistent with what the new summary represents and never
|
|
2014
|
+
// double-counts an unsliced untrusted view.
|
|
2015
|
+
const inContextCompactedCount = isUntrustedTrustClass(
|
|
2016
|
+
ctx.trustContext?.trustClass,
|
|
2017
|
+
)
|
|
2018
|
+
? 0
|
|
2019
|
+
: ctx.contextCompactedMessageCount;
|
|
2020
|
+
ctx.contextCompactedMessageCount =
|
|
2021
|
+
inContextCompactedCount + result.compactedPersistedMessages;
|
|
2022
|
+
ctx.contextSummary = result.summaryText;
|
|
3010
2023
|
const compactedAt = Date.now();
|
|
3011
2024
|
ctx.contextCompactedAt = compactedAt;
|
|
3012
|
-
// Signal to the next agent loop turn that NOW.md / PKB / v2 static blocks
|
|
3013
|
-
// were stripped from the tail and need fresh re-injection. Consumed and
|
|
3014
|
-
// cleared at the top of the next `runAgentLoopImpl` run.
|
|
3015
|
-
ctx.pendingPostCompactReinject = true;
|
|
3016
2025
|
await ctx.graphMemory.onCompacted(result.compactedPersistedMessages);
|
|
3017
2026
|
updateConversationContextWindow(
|
|
3018
2027
|
ctx.conversationId,
|
|
@@ -3026,6 +2035,8 @@ export async function applyCompactionResult(
|
|
|
3026
2035
|
options.slackContextCompactionWatermarkTs,
|
|
3027
2036
|
compactedAt,
|
|
3028
2037
|
);
|
|
2038
|
+
ctx.slackContextCompactionWatermarkTs =
|
|
2039
|
+
options.slackContextCompactionWatermarkTs;
|
|
3029
2040
|
}
|
|
3030
2041
|
enqueueAutoAnalysisOnCompaction(
|
|
3031
2042
|
ctx.conversationId,
|