@vellumai/assistant 0.8.8 → 0.8.9-staging.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +6 -6
- package/bun.lock +2 -2
- package/examples/plugins/echo/README.md +61 -60
- package/examples/plugins/echo/hooks/post-tool-use.ts +18 -0
- package/examples/plugins/echo/hooks/stop.ts +16 -0
- package/examples/plugins/echo/hooks/user-prompt-submit.ts +18 -0
- package/examples/plugins/echo/package.json +1 -2
- package/examples/plugins/echo/src/emit.ts +19 -0
- package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +7 -6
- package/openapi.yaml +235 -6
- package/package.json +2 -2
- package/src/__tests__/agent-loop-callsite-precedence.test.ts +69 -14
- package/src/__tests__/agent-loop-exit-reason.test.ts +204 -144
- package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +50 -35
- package/src/__tests__/agent-loop-output-hooks.test.ts +357 -0
- package/src/__tests__/agent-loop-override-profile.test.ts +25 -6
- package/src/__tests__/agent-loop-provider-error-recording.test.ts +41 -21
- package/src/__tests__/agent-loop-thinking.test.ts +36 -20
- package/src/__tests__/agent-loop.test.ts +441 -96
- package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +14 -14
- package/src/__tests__/agent-wake-override-profile.test.ts +17 -21
- package/src/__tests__/anthropic-provider.test.ts +1 -1
- package/src/__tests__/app-builder-tool-scripts.test.ts +21 -0
- package/src/__tests__/app-control-flow.test.ts +1 -1
- package/src/__tests__/app-dir-path-guard.test.ts +1 -0
- package/src/__tests__/app-executors.test.ts +132 -0
- package/src/__tests__/approval-cascade.test.ts +5 -4
- package/src/__tests__/approval-routes-http.test.ts +4 -1
- package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
- package/src/__tests__/channel-approval-routes.test.ts +1 -1
- package/src/__tests__/channel-approvals.test.ts +1 -1
- package/src/__tests__/compaction-circuit.test.ts +258 -0
- package/src/__tests__/compaction-direct.test.ts +132 -0
- package/src/__tests__/compaction-events.test.ts +5 -5
- package/src/__tests__/compactor-web-search-strip.test.ts +213 -0
- package/src/__tests__/context-overflow-reducer.test.ts +1 -1
- package/src/__tests__/conversation-abort-tool-results.test.ts +6 -4
- package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +7 -10
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +78 -119
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +142 -218
- package/src/__tests__/conversation-agent-loop.test.ts +297 -586
- package/src/__tests__/conversation-clean-command.test.ts +5 -2
- package/src/__tests__/conversation-confirmation-signals.test.ts +5 -4
- package/src/__tests__/conversation-crud-inference-profile.test.ts +7 -9
- package/src/__tests__/conversation-history-web-search.test.ts +1 -1
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +4 -4
- package/src/__tests__/conversation-process-callsite.test.ts +14 -14
- package/src/__tests__/conversation-provider-retry-repair.test.ts +70 -65
- package/src/__tests__/conversation-queue.test.ts +9 -9
- package/src/__tests__/conversation-runtime-assembly.test.ts +923 -231
- package/src/__tests__/conversation-runtime-workspace.test.ts +115 -20
- package/src/__tests__/conversation-slash-queue.test.ts +6 -4
- package/src/__tests__/conversation-slash-unknown.test.ts +6 -4
- package/src/__tests__/conversation-speed-override.test.ts +10 -9
- package/src/__tests__/conversation-starter-routes.test.ts +14 -6
- package/src/__tests__/conversation-workspace-cache-state.test.ts +23 -20
- package/src/__tests__/conversation-workspace-injection.test.ts +68 -6
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +14 -11
- package/src/__tests__/conversations-import-system-filter.test.ts +101 -0
- package/src/__tests__/credential-security-invariants.test.ts +0 -1
- package/src/__tests__/db-acp-history.test.ts +101 -0
- package/src/__tests__/dynamic-page-surface.test.ts +31 -0
- package/src/__tests__/empty-response-hook.test.ts +1 -1
- package/src/__tests__/file-write-tool.test.ts +63 -0
- package/src/__tests__/gateway-only-guard.test.ts +12 -2
- package/src/__tests__/guardian-grant-minting.test.ts +1 -1
- package/src/__tests__/guardian-routing-invariants.test.ts +2 -4
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +1 -1
- package/src/__tests__/heartbeat-disk-pressure.test.ts +1 -0
- package/src/__tests__/heartbeat-service.test.ts +1 -0
- package/src/__tests__/history-repair-hook.test.ts +1 -1
- package/src/__tests__/host-app-control-routes.test.ts +1 -1
- package/src/__tests__/host-cu-routes-targeted.test.ts +3 -3
- package/src/__tests__/inference-profile-reaper.test.ts +62 -0
- package/src/__tests__/inference-profile-session-handler.test.ts +86 -0
- package/src/__tests__/injector-background-turn.test.ts +13 -23
- package/src/__tests__/injector-chain.test.ts +268 -44
- package/src/__tests__/injector-disk-pressure.test.ts +210 -52
- package/src/__tests__/injector-document-comments.test.ts +97 -114
- package/src/__tests__/injector-pkb-v2-silenced.test.ts +2 -2
- package/src/__tests__/injector-v3-suppression.test.ts +4 -4
- package/src/__tests__/list-messages-client-message-id.test.ts +91 -0
- package/src/__tests__/list-messages-hidden-metadata.test.ts +38 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +131 -26
- package/src/__tests__/memory-v2-static-injector.test.ts +86 -8
- package/src/__tests__/parallel-tool.benchmark.test.ts +35 -8
- package/src/__tests__/plugin-api-shim.test.ts +6 -9
- package/src/__tests__/plugin-bootstrap.test.ts +12 -23
- package/src/__tests__/plugin-registry.test.ts +3 -49
- package/src/__tests__/plugin-types.test.ts +0 -70
- package/src/__tests__/pre-model-call-sanitize.test.ts +109 -0
- package/src/__tests__/reaction-persistence.test.ts +1 -1
- package/src/__tests__/send-endpoint-busy.test.ts +4 -1
- package/src/__tests__/skill-feature-flags-integration.test.ts +33 -0
- package/src/__tests__/steer-tool-repair.test.ts +1 -1
- package/src/__tests__/subagent-call-site-routing.test.ts +1 -1
- package/src/__tests__/subagent-detail.test.ts +25 -7
- package/src/__tests__/subagent-fork-notifications.test.ts +1 -3
- package/src/__tests__/subagent-fork-spawn.test.ts +1 -1
- package/src/__tests__/subagent-manager-notify.test.ts +1 -3
- package/src/__tests__/subagent-notify-parent.test.ts +1 -3
- package/src/__tests__/subagent-spawn-tool-fork.test.ts +1 -1
- package/src/__tests__/title-generate-hook.test.ts +1 -1
- package/src/__tests__/tool-error-hook.test.ts +1 -1
- package/src/__tests__/tool-result-truncate-hook.test.ts +1 -1
- package/src/__tests__/user-plugin-loader.test.ts +54 -286
- package/src/acp/__tests__/agent-process.test.ts +161 -0
- package/src/acp/__tests__/client-handler.test.ts +40 -0
- package/src/acp/__tests__/helpers/acp-history-db.ts +82 -0
- package/src/acp/__tests__/helpers/exec-file-stub.ts +106 -0
- package/src/acp/__tests__/prepare-agent-env.test.ts +97 -0
- package/src/acp/__tests__/session-manager-persistence.test.ts +95 -28
- package/src/acp/__tests__/session-manager-resume.test.ts +888 -0
- package/src/acp/agent-process.ts +61 -1
- package/src/acp/auto-install.test.ts +280 -0
- package/src/acp/auto-install.ts +232 -0
- package/src/acp/client-handler.ts +31 -0
- package/src/acp/feature-gate.test.ts +48 -0
- package/src/acp/feature-gate.ts +34 -0
- package/src/acp/prepare-agent-env.ts +80 -27
- package/src/acp/resolve-agent.test.ts +225 -9
- package/src/acp/resolve-agent.ts +122 -17
- package/src/acp/resume-hint.ts +23 -0
- package/src/acp/session-manager.ts +507 -73
- package/src/agent/compaction-circuit.ts +60 -102
- package/src/agent/loop.ts +414 -248
- package/src/api/responses/conversation-message.ts +14 -1
- package/src/approvals/guardian-request-resolvers.ts +1 -1
- package/src/background-wake/next-wake.ts +1 -0
- package/src/cli/commands/db/__tests__/repair.test.ts +3 -1
- package/src/cli/commands/plugins.ts +43 -37
- package/src/cli/lib/__tests__/install-from-github.test.ts +429 -111
- package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +196 -0
- package/src/cli/lib/__tests__/plugin-details.test.ts +372 -0
- package/src/cli/lib/__tests__/plugin-marketplace.test.ts +220 -0
- package/src/cli/lib/__tests__/search-plugins.test.ts +226 -32
- package/src/cli/lib/install-from-github.ts +464 -55
- package/src/cli/lib/plugin-catalog-cache.ts +84 -0
- package/src/cli/lib/plugin-details.ts +409 -0
- package/src/cli/lib/plugin-marketplace.ts +197 -0
- package/src/cli/lib/search-plugins.ts +195 -29
- package/src/config/__tests__/feature-flag-registry-guard.test.ts +2 -2
- package/src/config/acp-defaults.test.ts +10 -0
- package/src/config/acp-defaults.ts +6 -0
- package/src/config/bundled-skills/acp/SKILL.md +85 -33
- package/src/config/bundled-skills/acp/TOOLS.json +4 -4
- package/src/config/bundled-skills/app-builder/SKILL.md +224 -381
- package/src/config/bundled-skills/app-builder/TOOLS.json +72 -2
- package/src/config/bundled-skills/app-builder/references/DESIGN_SYSTEM.md +48 -0
- package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +57 -0
- package/src/config/bundled-skills/app-builder/references/SLIDES.md +38 -0
- package/src/config/bundled-skills/app-builder/tools/app-list.ts +62 -0
- package/src/config/bundled-skills/app-builder/tools/app-update.ts +18 -0
- package/src/config/bundled-skills/document-editor/SKILL.md +28 -23
- package/src/config/bundled-skills/document-editor/TOOLS.json +1 -1
- package/src/config/bundled-tool-registry.ts +4 -0
- package/src/config/call-site-defaults.ts +0 -2
- package/src/config/feature-flag-registry.json +15 -6
- package/src/config/schemas/call-site-catalog.ts +0 -14
- package/src/config/schemas/heartbeat.ts +9 -0
- package/src/config/schemas/llm.ts +0 -2
- package/src/context/compactor.ts +22 -4
- package/src/context/strip-injections.ts +8 -2
- package/src/context/window-manager.ts +27 -13
- package/src/daemon/conversation-agent-loop-handlers.ts +10 -35
- package/src/daemon/conversation-agent-loop.ts +175 -1055
- package/src/daemon/conversation-lifecycle.ts +11 -255
- package/src/daemon/conversation-process.ts +8 -136
- package/src/daemon/conversation-registry.ts +159 -0
- package/src/daemon/conversation-runtime-assembly.ts +293 -392
- package/src/daemon/conversation-store.ts +9 -90
- package/src/daemon/conversation-surfaces.ts +24 -8
- package/src/daemon/conversation-workspace.ts +17 -0
- package/src/daemon/conversation.ts +404 -57
- package/src/daemon/disk-pressure-policy.ts +0 -1
- package/src/daemon/external-plugins-bootstrap.ts +14 -19
- package/src/daemon/handlers/conversations.ts +3 -1
- package/src/daemon/handlers/skills.ts +4 -1
- package/src/daemon/host-proxy-preactivation.ts +1 -3
- package/src/daemon/lifecycle.ts +21 -7
- package/src/daemon/server.ts +2 -0
- package/src/daemon/wake-conversation-ops.ts +269 -0
- package/src/embedded/plugin-api.ts +2 -2
- package/src/export/__tests__/transcript-formatter.test.ts +5 -0
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +3 -0
- package/src/heartbeat/heartbeat-run-store.ts +23 -1
- package/src/heartbeat/heartbeat-service.ts +26 -0
- package/src/ipc/__tests__/browser-ipc.test.ts +1 -1
- package/src/ipc/__tests__/ui-request-route.test.ts +3 -3
- package/src/ipc/skill-routes/__tests__/memory.test.ts +15 -0
- package/src/ipc/skill-routes/memory.ts +4 -2
- package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +87 -0
- package/src/memory/conversation-crud.ts +29 -19
- package/src/memory/conversation-starter-checkpoints.ts +1 -0
- package/src/memory/db-init.ts +2 -0
- package/src/memory/job-handlers/conversation-starters.ts +13 -2
- package/src/memory/jobs/__tests__/embed-concept-page.test.ts +5 -4
- package/src/memory/jobs-worker.ts +25 -1
- package/src/memory/migrations/272-acp-session-history-cwd.ts +36 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/schema/acp.ts +4 -0
- package/src/memory/v2/__tests__/consolidation-job.test.ts +3 -3
- package/src/memory/v2/consolidation-job.ts +13 -4
- package/src/plugin-api/constants.ts +4 -0
- package/src/plugin-api/index.ts +6 -5
- package/src/plugin-api/types.ts +75 -0
- package/src/plugins/defaults/compaction/compact.ts +59 -0
- package/src/{daemon → plugins/defaults/compaction}/context-overflow-reducer.ts +7 -7
- package/src/plugins/defaults/compaction/manager-store.ts +57 -0
- package/src/plugins/defaults/compaction/package.json +1 -2
- package/src/plugins/defaults/empty-response/package.json +0 -1
- package/src/plugins/defaults/history-repair/package.json +0 -1
- package/src/plugins/defaults/index.ts +135 -26
- package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +100 -52
- package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +184 -74
- package/src/plugins/defaults/memory-retrieval/injector-chain.ts +2 -2
- package/src/plugins/defaults/{injectors/register.ts → memory-retrieval/injectors.ts} +148 -73
- package/src/plugins/defaults/memory-retrieval/unified-turn-context.ts +223 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/assign.test.ts +4 -4
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/live-integration.test.ts +9 -6
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/maintain-job.test.ts +5 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/orchestrate.test.ts +8 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/reconcile.test.ts +2 -2
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/render-injection.test.ts +1 -1
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/router.test.ts +10 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selection-log-store.test.ts +8 -8
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selector.test.ts +5 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/shadow-plugin.test.ts +16 -17
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/types.test.ts +2 -2
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/assign.ts +9 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/capabilities.ts +5 -2
- package/src/plugins/defaults/memory-v3-shadow/hooks/post-compact.ts +14 -0
- package/src/plugins/defaults/memory-v3-shadow/hooks/user-prompt-submit.ts +19 -0
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +75 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/maintain-job.ts +15 -8
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/orchestrate.ts +2 -2
- package/src/plugins/defaults/memory-v3-shadow/package.json +14 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/page-content.ts +2 -2
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/provider-blocks.ts +1 -1
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/reconcile.ts +7 -3
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/render-injection.ts +1 -1
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/router.ts +5 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/selection-log-store.ts +4 -4
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/selector.ts +7 -7
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/shadow-plugin.ts +32 -94
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/tree.ts +1 -1
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/types.ts +1 -1
- package/src/plugins/defaults/title-generate/package.json +0 -1
- package/src/plugins/defaults/tool-error/package.json +0 -1
- package/src/plugins/defaults/tool-result-truncate/package.json +0 -1
- package/src/plugins/pipeline.ts +6 -293
- package/src/plugins/registry.ts +9 -37
- package/src/plugins/types.ts +76 -381
- package/src/plugins/user-loader.ts +30 -127
- package/src/prompts/__tests__/system-prompt.test.ts +6 -0
- package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +35 -3
- package/src/runtime/__tests__/agent-wake.test.ts +555 -691
- package/src/runtime/__tests__/interactive-ui.test.ts +1 -1
- package/src/runtime/agent-wake.ts +108 -209
- package/src/runtime/assistant-event-hub.ts +1 -1
- package/src/runtime/channel-approvals.ts +1 -1
- package/src/runtime/interactive-ui.ts +1 -1
- package/src/runtime/routes/__tests__/acp-routes.test.ts +315 -55
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +1 -1
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +423 -73
- package/src/runtime/routes/__tests__/surface-action-routes.test.ts +5 -4
- package/src/runtime/routes/__tests__/surface-content-routes.test.ts +4 -1
- package/src/runtime/routes/acp-routes.test.ts +89 -25
- package/src/runtime/routes/acp-routes.ts +81 -29
- package/src/runtime/routes/approval-routes.ts +1 -1
- package/src/runtime/routes/browser-routes.ts +1 -1
- package/src/runtime/routes/browser-tabs-routes.ts +6 -10
- package/src/runtime/routes/conversation-cli-routes.ts +1 -1
- package/src/runtime/routes/conversation-list-routes.ts +1 -1
- package/src/runtime/routes/conversation-query-routes.ts +1 -1
- package/src/runtime/routes/conversation-routes.ts +28 -2
- package/src/runtime/routes/conversation-starter-routes.ts +13 -7
- package/src/runtime/routes/conversations-import-routes.ts +24 -7
- package/src/runtime/routes/host-app-control-routes.ts +1 -1
- package/src/runtime/routes/host-cu-routes.ts +1 -1
- package/src/runtime/routes/identity-routes.ts +18 -3
- package/src/runtime/routes/inbound-message-handler.ts +1 -1
- package/src/runtime/routes/inference-profile-session-handler.ts +11 -0
- package/src/runtime/routes/inference-profile-session-reaper.ts +6 -0
- package/src/runtime/routes/memory-v3-routes.ts +16 -6
- package/src/runtime/routes/playground/helpers.ts +1 -1
- package/src/runtime/routes/plugins-routes.ts +337 -35
- package/src/runtime/routes/surface-conversation-resolver.ts +4 -3
- package/src/runtime/routes/work-items-routes.ts +2 -4
- package/src/runtime/services/conversation-serializer.ts +1 -1
- package/src/signals/cancel.ts +2 -4
- package/src/subagent/manager.ts +21 -5
- package/src/tools/acp/context.ts +20 -0
- package/src/tools/acp/list-agents.test.ts +8 -2
- package/src/tools/acp/spawn.test.ts +176 -195
- package/src/tools/acp/spawn.ts +37 -172
- package/src/tools/acp/steer.test.ts +105 -8
- package/src/tools/acp/steer.ts +48 -17
- package/src/tools/apps/executors.ts +166 -50
- package/src/tools/filesystem/write.ts +34 -0
- package/src/tools/subagent/spawn.ts +2 -4
- package/src/tools/ui-surface/definitions.ts +25 -5
- package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +4 -5
- package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +69 -45
- package/docs/plugins.md +0 -832
- package/examples/plugins/echo/register.ts +0 -143
- package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -405
- package/src/__tests__/compaction-pipeline.test.ts +0 -210
- package/src/__tests__/compaction-timeout-recovery.test.ts +0 -251
- package/src/__tests__/overflow-reduce-pipeline.test.ts +0 -667
- package/src/__tests__/pipeline-runner.test.ts +0 -554
- package/src/__tests__/plugin-external-api.test.ts +0 -68
- package/src/daemon/wake-target-adapter.ts +0 -253
- package/src/plugins/defaults/circuit-breaker/middlewares/circuitBreaker.ts +0 -93
- package/src/plugins/defaults/circuit-breaker/package.json +0 -15
- package/src/plugins/defaults/circuit-breaker/register.ts +0 -39
- package/src/plugins/defaults/compaction/middlewares/compaction.ts +0 -25
- package/src/plugins/defaults/compaction/register.ts +0 -35
- package/src/plugins/defaults/compaction/terminal.ts +0 -73
- package/src/plugins/defaults/empty-response/register.ts +0 -23
- package/src/plugins/defaults/history-repair/register.ts +0 -24
- package/src/plugins/defaults/overflow-reduce/middlewares/overflowReduce.ts +0 -126
- package/src/plugins/defaults/overflow-reduce/package.json +0 -15
- package/src/plugins/defaults/overflow-reduce/register.ts +0 -42
- package/src/plugins/defaults/title-generate/register.ts +0 -35
- package/src/plugins/defaults/tool-error/register.ts +0 -23
- package/src/plugins/defaults/tool-result-truncate/register.ts +0 -24
- package/src/plugins/external-api.ts +0 -104
- package/src/proactive-artifact/aux-message-injector.ts +0 -97
- package/src/proactive-artifact/decision.test.ts +0 -226
- package/src/proactive-artifact/decision.ts +0 -165
- package/src/proactive-artifact/index.ts +0 -7
- package/src/proactive-artifact/job.test.ts +0 -962
- package/src/proactive-artifact/job.ts +0 -372
- package/src/proactive-artifact/message-copy.ts +0 -58
- package/src/proactive-artifact/trigger-state.test.ts +0 -286
- package/src/proactive-artifact/trigger-state.ts +0 -123
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/capabilities.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/core.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/eval-turns.json +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/live-turns.json +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/health.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/needle.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/provider-blocks.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/snapshot.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/tree.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-eviction.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-skeleton.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/core.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/README.md +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/assignments.json +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/core.json +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-x.md +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-y.md +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-b/topic-z.md +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/health.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/llm-retry.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/needle.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/snapshot.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/working-set.ts +0 -0
package/src/agent/loop.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import * as Sentry from "@sentry/node";
|
|
2
2
|
|
|
3
|
+
import { isAssistantFeatureFlagEnabled } from "../config/assistant-feature-flags.js";
|
|
4
|
+
import { getConfig } from "../config/loader.js";
|
|
3
5
|
import type { LLMCallSite } from "../config/schemas/llm.js";
|
|
4
6
|
import { stripInjectionsForCompaction } from "../context/strip-injections.js";
|
|
5
7
|
import {
|
|
@@ -9,20 +11,26 @@ import {
|
|
|
9
11
|
getCalibrationProviderKey,
|
|
10
12
|
} from "../context/token-estimator.js";
|
|
11
13
|
import type { ContextWindowResult } from "../context/window-manager.js";
|
|
14
|
+
import type { InboundActorContext } from "../daemon/conversation-runtime-assembly.js";
|
|
12
15
|
import type { ToolActivityMetadata } from "../daemon/message-types/web-activity.js";
|
|
16
|
+
import type { TrustContext } from "../daemon/trust-context.js";
|
|
17
|
+
import { stripHistoricalWebSearchResults } from "../daemon/web-search-history.js";
|
|
13
18
|
import { HOOKS } from "../plugin-api/constants.js";
|
|
14
|
-
import type { PostToolUseContext, StopContext } from "../plugin-api/types.js";
|
|
15
|
-
import { defaultCompactionTerminal } from "../plugins/defaults/compaction/terminal.js";
|
|
16
|
-
import type { PostCompactionHookInput } from "../plugins/defaults/memory-retrieval/hooks/post-compact.js";
|
|
17
|
-
import { DEFAULT_TIMEOUTS, runHook, runPipeline } from "../plugins/pipeline.js";
|
|
18
|
-
import { getMiddlewaresFor } from "../plugins/registry.js";
|
|
19
19
|
import type {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
} from "../
|
|
25
|
-
import {
|
|
20
|
+
AssistantMessageContext,
|
|
21
|
+
PostToolUseContext,
|
|
22
|
+
PreModelCallContext,
|
|
23
|
+
StopContext,
|
|
24
|
+
} from "../plugin-api/types.js";
|
|
25
|
+
import {
|
|
26
|
+
DEFAULT_COMPACTION_PLUGIN_NAME,
|
|
27
|
+
defaultCompact,
|
|
28
|
+
} from "../plugins/defaults/compaction/compact.js";
|
|
29
|
+
import { getContextWindowManager } from "../plugins/defaults/compaction/manager-store.js";
|
|
30
|
+
import postCompact from "../plugins/defaults/memory-retrieval/hooks/post-compact.js";
|
|
31
|
+
import { runHook } from "../plugins/pipeline.js";
|
|
32
|
+
import type { CompactionCircuitEvent } from "../plugins/types.js";
|
|
33
|
+
import { PluginExecutionError } from "../plugins/types.js";
|
|
26
34
|
import { normalizeThinkingConfigForWire } from "../providers/thinking-config.js";
|
|
27
35
|
import type {
|
|
28
36
|
ContentBlock,
|
|
@@ -308,7 +316,7 @@ export type AgentEvent =
|
|
|
308
316
|
* immediately before re-injection — whether or not the pipeline actually
|
|
309
317
|
* compacted. The daemon's event dispatcher always commits `basis` (the
|
|
310
318
|
* stripped pre-compaction history) as the conversation's durable message
|
|
311
|
-
* state, so re-injection ({@link
|
|
319
|
+
* state, so re-injection ({@link postCompact}) re-applies
|
|
312
320
|
* injections onto the stripped base rather than stacking on top of the
|
|
313
321
|
* still-injected messages. When `result.compacted` is set it
|
|
314
322
|
* additionally commits the durable compaction result (DB-record fields,
|
|
@@ -382,34 +390,17 @@ export function isMaxTokensStopReason(
|
|
|
382
390
|
}
|
|
383
391
|
|
|
384
392
|
/**
|
|
385
|
-
*
|
|
386
|
-
*
|
|
387
|
-
*
|
|
388
|
-
*
|
|
389
|
-
* tests that construct `AgentLoop` directly without an orchestrator.
|
|
390
|
-
*
|
|
391
|
-
* When the orchestrator-supplied context is present it is used directly so the
|
|
392
|
-
* pipeline sees the real `conversationId`, trust, and `contextWindowManager`.
|
|
393
|
-
* In the fallback path the returned context is still useful for pipeline
|
|
394
|
-
* logging: `requestId` surfaces in every structured record, and `turnIndex`
|
|
395
|
-
* reflects the current tool-use iteration.
|
|
393
|
+
* Concatenate the text of an assistant message's `text` blocks (ignoring
|
|
394
|
+
* `tool_use`, `thinking`, and other non-text blocks). Used to re-emit the
|
|
395
|
+
* finalized text as a single `text_delta` when a turn's live output was
|
|
396
|
+
* deferred by a `pre-model-call` hook.
|
|
396
397
|
*/
|
|
397
|
-
function
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
)
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
// Loop-scoped pipelines do not currently carry a conversation ID; the
|
|
404
|
-
// outer orchestrator owns that dimension. Use a fixed sentinel so log
|
|
405
|
-
// consumers can filter loop-origin records out of conversation queries.
|
|
406
|
-
conversationId: "agent-loop",
|
|
407
|
-
turnIndex,
|
|
408
|
-
trust: {
|
|
409
|
-
sourceChannel: "vellum",
|
|
410
|
-
trustClass: "unknown",
|
|
411
|
-
},
|
|
412
|
-
};
|
|
398
|
+
function assistantTextOf(content: ReadonlyArray<ContentBlock>): string {
|
|
399
|
+
let text = "";
|
|
400
|
+
for (const block of content) {
|
|
401
|
+
if (block.type === "text") text += block.text;
|
|
402
|
+
}
|
|
403
|
+
return text;
|
|
413
404
|
}
|
|
414
405
|
|
|
415
406
|
/**
|
|
@@ -454,29 +445,11 @@ export interface ResolvedSystemPrompt {
|
|
|
454
445
|
model?: string;
|
|
455
446
|
}
|
|
456
447
|
|
|
457
|
-
/**
|
|
458
|
-
* Orchestrator-supplied hook the loop invokes when the mid-loop budget gate
|
|
459
|
-
* trips and inline compaction runs. The loop owns the trigger, the
|
|
460
|
-
* `compaction` pipeline call, the result interpretation (circuit-breaker
|
|
461
|
-
* bookkeeping + the exhaustion decision), and the inline continue; this hook
|
|
462
|
-
* bridges the injection state the loop is intentionally blind to. Durable
|
|
463
|
-
* persistence is signalled out-of-band via the `history_stripped` (marker)
|
|
464
|
-
* and `compaction_completed` (basis commit + successful summary) {@link
|
|
465
|
-
* AgentEvent}s; the {@link MidLoopCompaction.postCompactionHook} is
|
|
466
|
-
* orchestrator-supplied, and its inputs migrate loop-ward as the loop
|
|
467
|
-
* subsumes the re-injection ceremony.
|
|
468
|
-
*/
|
|
469
|
-
export interface MidLoopCompaction {
|
|
470
|
-
/**
|
|
471
|
-
* Re-apply runtime injections onto the post-compaction history and return
|
|
472
|
-
* the history to continue from. The loop supplies its own working state via
|
|
473
|
-
* {@link PostCompactionHookInput} so the hook re-injects from that rather
|
|
474
|
-
* than reading it back from orchestrator state.
|
|
475
|
-
*/
|
|
476
|
-
postCompactionHook: (input: PostCompactionHookInput) => Promise<Message[]>;
|
|
477
|
-
}
|
|
478
|
-
|
|
479
448
|
export interface AgentLoopRunOptions {
|
|
449
|
+
/** Input history the run starts from; the loop appends its output onto a copy. */
|
|
450
|
+
messages: Message[];
|
|
451
|
+
/** Sink the loop streams its {@link AgentEvent}s through as the turn runs. */
|
|
452
|
+
onEvent: (event: AgentEvent) => void | Promise<void>;
|
|
480
453
|
signal?: AbortSignal;
|
|
481
454
|
requestId?: string;
|
|
482
455
|
onCheckpoint?: (
|
|
@@ -484,14 +457,15 @@ export interface AgentLoopRunOptions {
|
|
|
484
457
|
) => CheckpointDecision | Promise<CheckpointDecision>;
|
|
485
458
|
callSite?: LLMCallSite;
|
|
486
459
|
/**
|
|
487
|
-
*
|
|
488
|
-
*
|
|
489
|
-
*
|
|
490
|
-
*
|
|
491
|
-
*
|
|
492
|
-
*
|
|
460
|
+
* Trust classification and channel identity for the turn's inbound actor,
|
|
461
|
+
* supplied by the caller as the turn-start snapshot. Read only on the
|
|
462
|
+
* mid-loop in-place compaction path — to scope the compactor's image
|
|
463
|
+
* manifest (guardian-only attachments are excluded for untrusted actors) and
|
|
464
|
+
* forwarded to {@link postCompact}. Callers without a meaningful actor (agent
|
|
465
|
+
* wakes, standalone unit tests) pass an `unknown`-class snapshot so the
|
|
466
|
+
* compactor fail-closes to excluding guardian-only attachments.
|
|
493
467
|
*/
|
|
494
|
-
|
|
468
|
+
trust: TrustContext;
|
|
495
469
|
/**
|
|
496
470
|
* Ad-hoc inference-profile override applied to every LLM call the loop
|
|
497
471
|
* issues. When set, each `SendMessageOptions.config` carries
|
|
@@ -514,21 +488,51 @@ export interface AgentLoopRunOptions {
|
|
|
514
488
|
overflowRecovery: { enabled: boolean; safetyMarginRatio: number };
|
|
515
489
|
};
|
|
516
490
|
/**
|
|
517
|
-
*
|
|
518
|
-
*
|
|
519
|
-
*
|
|
520
|
-
*
|
|
491
|
+
* When `true`, the loop owns turn-start and mid-loop compaction. The pre-call
|
|
492
|
+
* budget gate runs before the very first provider call — subsuming the
|
|
493
|
+
* proactive turn-start compaction the orchestrator would otherwise perform
|
|
494
|
+
* inline before `run()` — as well as before each tool-use re-entry. When the
|
|
495
|
+
* gate trips it compacts the running history in place, re-applying runtime
|
|
496
|
+
* injections via the default post-compaction hook ({@link postCompact}), and
|
|
497
|
+
* continues instead of yielding `exitReason = "budget"`.
|
|
498
|
+
*
|
|
499
|
+
* The first-call pass honors the compaction circuit breaker and proceeds with
|
|
500
|
+
* the call whether or not it compacted (preflight-overflow recovery and the
|
|
501
|
+
* convergence loop remain the escalation path), so it never yields on the
|
|
502
|
+
* first call. Reruns without an inline compaction path (agent wakes,
|
|
503
|
+
* convergence/auto-compress reruns) leave it `false`: they skip the
|
|
504
|
+
* first-call gate and keep yielding for budget on mid-loop re-entries.
|
|
505
|
+
* Defaults to `false` when omitted.
|
|
521
506
|
*/
|
|
522
|
-
|
|
507
|
+
compactInPlace?: boolean;
|
|
523
508
|
/**
|
|
524
|
-
*
|
|
525
|
-
*
|
|
526
|
-
*
|
|
527
|
-
*
|
|
528
|
-
*
|
|
529
|
-
* prefix reusable. Default unset → existing behavior.
|
|
509
|
+
* Whether the in-flight turn has no human present to answer clarification
|
|
510
|
+
* questions. Resolved once by the orchestrator at turn start and forwarded to
|
|
511
|
+
* {@link postCompact} so post-compaction
|
|
512
|
+
* re-injection uses the turn-start snapshot rather than re-reading mutable
|
|
513
|
+
* client/headless state mid-turn. Defaults to `false` when omitted.
|
|
530
514
|
*/
|
|
531
|
-
|
|
515
|
+
isNonInteractive?: boolean;
|
|
516
|
+
/**
|
|
517
|
+
* The `model_profile:` turn-context label resolved once by the orchestrator
|
|
518
|
+
* at turn start, or `null` when the active inference profile is unchanged
|
|
519
|
+
* since the last notified one. Forwarded to
|
|
520
|
+
* {@link postCompact} so post-compaction re-injection
|
|
521
|
+
* re-emits the turn-start value rather than re-deriving the change-detected
|
|
522
|
+
* label (which flips once the notification is persisted mid-turn). Defaults to
|
|
523
|
+
* `null` when omitted.
|
|
524
|
+
*/
|
|
525
|
+
modelProfile?: string | null;
|
|
526
|
+
/**
|
|
527
|
+
* Inbound actor identity and trust fields for the unified `<turn_context>`
|
|
528
|
+
* block, or `null` on guardian turns. Resolved once by the orchestrator at
|
|
529
|
+
* turn start via the actor-trust resolver, whose contact/member registry
|
|
530
|
+
* inputs can be mutated mid-turn by contact tools, and forwarded to
|
|
531
|
+
* {@link postCompact} so post-compaction
|
|
532
|
+
* re-injection re-emits the turn-start value rather than re-resolving it.
|
|
533
|
+
* Defaults to `null` when omitted.
|
|
534
|
+
*/
|
|
535
|
+
actorContext?: InboundActorContext | null;
|
|
532
536
|
}
|
|
533
537
|
|
|
534
538
|
/**
|
|
@@ -570,17 +574,21 @@ export type LoopToolExecutor = (
|
|
|
570
574
|
}>;
|
|
571
575
|
|
|
572
576
|
export interface AgentLoopConstructorOptions {
|
|
577
|
+
/** LLM provider the loop issues every call through. */
|
|
578
|
+
provider: Provider;
|
|
579
|
+
/** Base system prompt, before any per-turn dynamic resolution. */
|
|
580
|
+
systemPrompt: string;
|
|
573
581
|
config?: Partial<AgentLoopConfig>;
|
|
574
582
|
tools?: ToolDefinition[];
|
|
575
583
|
toolExecutor?: LoopToolExecutor;
|
|
576
584
|
resolveTools?: (history: Message[]) => ToolDefinition[];
|
|
577
585
|
resolveSystemPrompt?: (history: Message[]) => ResolvedSystemPrompt;
|
|
578
586
|
/**
|
|
579
|
-
* Conversation this loop drives.
|
|
580
|
-
*
|
|
581
|
-
*
|
|
587
|
+
* Conversation this loop drives. Scopes the loop-held compaction circuit
|
|
588
|
+
* breaker and is the source of truth the loop's pipeline contexts and
|
|
589
|
+
* post-compaction re-injection resolve the live conversation through.
|
|
582
590
|
*/
|
|
583
|
-
conversationId
|
|
591
|
+
conversationId: string;
|
|
584
592
|
}
|
|
585
593
|
|
|
586
594
|
export class AgentLoop {
|
|
@@ -594,6 +602,14 @@ export class AgentLoop {
|
|
|
594
602
|
| null;
|
|
595
603
|
private toolExecutor: LoopToolExecutor | null;
|
|
596
604
|
|
|
605
|
+
/**
|
|
606
|
+
* Conversation this loop drives. Source of truth for the `conversationId`
|
|
607
|
+
* the loop's pipeline contexts and post-compaction re-injection resolve the
|
|
608
|
+
* live conversation through, so the loop knows its own identity without a
|
|
609
|
+
* threaded-in turn context.
|
|
610
|
+
*/
|
|
611
|
+
private readonly conversationId: string;
|
|
612
|
+
|
|
597
613
|
/**
|
|
598
614
|
* Loop-held compaction circuit breaker. The loop has a 1:1 lifetime with its
|
|
599
615
|
* conversation, so it is the source of truth for the cross-turn failure
|
|
@@ -603,19 +619,17 @@ export class AgentLoop {
|
|
|
603
619
|
*/
|
|
604
620
|
readonly compactionCircuit: CompactionCircuit;
|
|
605
621
|
|
|
606
|
-
constructor(
|
|
607
|
-
provider: Provider,
|
|
608
|
-
systemPrompt: string,
|
|
609
|
-
options?: AgentLoopConstructorOptions,
|
|
610
|
-
) {
|
|
622
|
+
constructor(options: AgentLoopConstructorOptions) {
|
|
611
623
|
const {
|
|
624
|
+
provider,
|
|
625
|
+
systemPrompt,
|
|
612
626
|
config,
|
|
613
627
|
tools,
|
|
614
628
|
toolExecutor,
|
|
615
629
|
resolveTools,
|
|
616
630
|
resolveSystemPrompt,
|
|
617
631
|
conversationId,
|
|
618
|
-
} = options
|
|
632
|
+
} = options;
|
|
619
633
|
this.provider = provider;
|
|
620
634
|
this.systemPrompt = systemPrompt;
|
|
621
635
|
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
@@ -623,7 +637,8 @@ export class AgentLoop {
|
|
|
623
637
|
this.resolveTools = resolveTools ?? null;
|
|
624
638
|
this.resolveSystemPrompt = resolveSystemPrompt ?? null;
|
|
625
639
|
this.toolExecutor = toolExecutor ?? null;
|
|
626
|
-
this.
|
|
640
|
+
this.conversationId = conversationId;
|
|
641
|
+
this.compactionCircuit = new CompactionCircuit(this.conversationId);
|
|
627
642
|
}
|
|
628
643
|
|
|
629
644
|
/**
|
|
@@ -675,23 +690,15 @@ export class AgentLoop {
|
|
|
675
690
|
* compaction outcome into a user-visible turn failure.
|
|
676
691
|
*/
|
|
677
692
|
private async recordCompactionOutcome(
|
|
678
|
-
|
|
693
|
+
requestId: string | undefined,
|
|
679
694
|
summaryFailed: boolean,
|
|
680
695
|
onEvent: (event: AgentEvent) => void | Promise<void>,
|
|
681
696
|
): Promise<void> {
|
|
682
697
|
try {
|
|
683
|
-
await this.compactionCircuit.recordOutcome(
|
|
684
|
-
{
|
|
685
|
-
currentRequestId: turnContext.requestId,
|
|
686
|
-
currentTurnTrustContext: turnContext.trust,
|
|
687
|
-
turnCount: turnContext.turnIndex,
|
|
688
|
-
},
|
|
689
|
-
summaryFailed,
|
|
690
|
-
onEvent,
|
|
691
|
-
);
|
|
698
|
+
await this.compactionCircuit.recordOutcome(summaryFailed, onEvent);
|
|
692
699
|
} catch (recordError) {
|
|
693
700
|
log.error(
|
|
694
|
-
{ err: recordError, requestId
|
|
701
|
+
{ err: recordError, requestId },
|
|
695
702
|
"Recording a compaction outcome against the circuit breaker failed; suppressing to keep the agent loop alive",
|
|
696
703
|
);
|
|
697
704
|
}
|
|
@@ -700,19 +707,21 @@ export class AgentLoop {
|
|
|
700
707
|
/**
|
|
701
708
|
* Compact the running history in place when the mid-loop budget gate trips.
|
|
702
709
|
*
|
|
703
|
-
*
|
|
704
|
-
*
|
|
705
|
-
*
|
|
706
|
-
*
|
|
707
|
-
* `exitReason = "budget"` and the orchestrator escalates.
|
|
710
|
+
* Calls the default compaction plugin on the stripped history, then
|
|
711
|
+
* re-applies injections via the supplied hooks. Returns the history to
|
|
712
|
+
* continue from, or `null` when the compactor exhausted its retry budget so
|
|
713
|
+
* the caller yields `exitReason = "budget"` and the orchestrator escalates.
|
|
708
714
|
*/
|
|
709
715
|
private async compact(
|
|
710
716
|
history: Message[],
|
|
711
|
-
|
|
712
|
-
|
|
717
|
+
requestId: string | undefined,
|
|
718
|
+
trust: TrustContext,
|
|
713
719
|
signal: AbortSignal | undefined,
|
|
714
720
|
onEvent: (event: AgentEvent) => void | Promise<void>,
|
|
715
721
|
overrideProfile: string | null,
|
|
722
|
+
isNonInteractive: boolean,
|
|
723
|
+
modelProfile: string | null,
|
|
724
|
+
actorContext: InboundActorContext | null,
|
|
716
725
|
): Promise<Message[] | null> {
|
|
717
726
|
await onEvent({ type: "context_compacting" });
|
|
718
727
|
// Strip runtime injections so the compactor summarizes the raw persistent
|
|
@@ -721,49 +730,38 @@ export class AgentLoop {
|
|
|
721
730
|
// Record the history-stripped marker right after stripping, before the
|
|
722
731
|
// pipeline runs.
|
|
723
732
|
await onEvent({ type: "history_stripped" });
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
// turn triggered compaction) so the compactor's image manifest excludes
|
|
734
|
-
// guardian-only attachments for untrusted actors. `overrideProfile` is
|
|
735
|
-
// the turn's resolved inference-profile override for the summary call.
|
|
736
|
-
{
|
|
737
|
-
messages: rawHistory,
|
|
738
|
-
signal,
|
|
739
|
-
options: {
|
|
740
|
-
force: true,
|
|
741
|
-
actorTrustClass: turnContext.trust.trustClass,
|
|
742
|
-
overrideProfile,
|
|
743
|
-
},
|
|
744
|
-
},
|
|
745
|
-
turnContext,
|
|
746
|
-
DEFAULT_TIMEOUTS.compaction,
|
|
733
|
+
// The compaction module owns the per-conversation manager; resolve it from
|
|
734
|
+
// the store rather than holding a handle on the loop. Absent for callers
|
|
735
|
+
// without a compaction path (agent wakes, standalone unit tests), which
|
|
736
|
+
// never reach this gate.
|
|
737
|
+
const manager = getContextWindowManager(this.conversationId);
|
|
738
|
+
if (manager == null) {
|
|
739
|
+
throw new PluginExecutionError(
|
|
740
|
+
`default-compaction: no ContextWindowManager registered for conversation ${this.conversationId} — the compaction store must construct one before compaction runs`,
|
|
741
|
+
DEFAULT_COMPACTION_PLUGIN_NAME,
|
|
747
742
|
);
|
|
748
|
-
} catch (error) {
|
|
749
|
-
if (error instanceof PluginTimeoutError) {
|
|
750
|
-
// A timeout counts as a compaction failure against the circuit breaker.
|
|
751
|
-
await this.recordCompactionOutcome(turnContext, true, onEvent);
|
|
752
|
-
return null;
|
|
753
|
-
}
|
|
754
|
-
throw error;
|
|
755
743
|
}
|
|
756
|
-
//
|
|
757
|
-
//
|
|
758
|
-
//
|
|
759
|
-
|
|
744
|
+
// The mid-loop budget gate is reached only when this turn decides to
|
|
745
|
+
// compact in place, so `force` past the auto-threshold check.
|
|
746
|
+
// `actorTrustClass` comes from the turn's trust snapshot (the actor whose
|
|
747
|
+
// turn triggered compaction) so the compactor's image manifest excludes
|
|
748
|
+
// guardian-only attachments for untrusted actors. `overrideProfile` is the
|
|
749
|
+
// turn's resolved inference-profile override for the summary call.
|
|
750
|
+
const compactResult = await defaultCompact({
|
|
751
|
+
manager,
|
|
752
|
+
messages: rawHistory,
|
|
753
|
+
signal,
|
|
754
|
+
force: true,
|
|
755
|
+
actorTrustClass: trust.trustClass,
|
|
756
|
+
overrideProfile,
|
|
757
|
+
});
|
|
760
758
|
// `force: true` bypasses the auto-threshold gate, but early returns
|
|
761
759
|
// for "no eligible messages" / "insufficient messages" still leave
|
|
762
760
|
// `summaryFailed` undefined. Only record an outcome when the summary LLM
|
|
763
761
|
// actually ran.
|
|
764
762
|
if (compactResult.summaryFailed !== undefined) {
|
|
765
763
|
await this.recordCompactionOutcome(
|
|
766
|
-
|
|
764
|
+
requestId,
|
|
767
765
|
compactResult.summaryFailed,
|
|
768
766
|
onEvent,
|
|
769
767
|
);
|
|
@@ -783,29 +781,37 @@ export class AgentLoop {
|
|
|
783
781
|
// Re-inject onto the same base the `compaction_completed` dispatch commits:
|
|
784
782
|
// the compacted messages when the pipeline compacted, the stripped
|
|
785
783
|
// pre-compaction history otherwise.
|
|
786
|
-
|
|
784
|
+
const injection = await postCompact({
|
|
787
785
|
history: compactResult.compacted ? compactResult.messages : rawHistory,
|
|
788
|
-
|
|
786
|
+
requestId,
|
|
787
|
+
conversationId: this.conversationId,
|
|
788
|
+
trust,
|
|
789
|
+
isNonInteractive,
|
|
790
|
+
// Mid-loop re-injection always runs at full injection volume.
|
|
791
|
+
mode: "full",
|
|
792
|
+
modelProfile,
|
|
793
|
+
actorContext,
|
|
789
794
|
});
|
|
795
|
+
return injection.messages;
|
|
790
796
|
}
|
|
791
797
|
|
|
792
|
-
async run(
|
|
793
|
-
messages: Message[],
|
|
794
|
-
onEvent: (event: AgentEvent) => void | Promise<void>,
|
|
795
|
-
options?: AgentLoopRunOptions,
|
|
796
|
-
): Promise<AgentLoopRunResult> {
|
|
798
|
+
async run(options: AgentLoopRunOptions): Promise<AgentLoopRunResult> {
|
|
797
799
|
const {
|
|
800
|
+
messages,
|
|
801
|
+
onEvent,
|
|
798
802
|
signal,
|
|
799
803
|
requestId,
|
|
800
804
|
onCheckpoint,
|
|
801
805
|
callSite,
|
|
802
|
-
|
|
806
|
+
trust,
|
|
803
807
|
overrideProfile,
|
|
804
808
|
resolveOverrideProfile,
|
|
805
809
|
resolveContextWindow,
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
810
|
+
compactInPlace = false,
|
|
811
|
+
isNonInteractive = false,
|
|
812
|
+
modelProfile = null,
|
|
813
|
+
actorContext = null,
|
|
814
|
+
} = options;
|
|
809
815
|
let history = [...messages];
|
|
810
816
|
// Index into `history` where this run's appended output begins. It starts
|
|
811
817
|
// after the input and resets to the compacted base whenever the loop
|
|
@@ -818,6 +824,13 @@ export class AgentLoop {
|
|
|
818
824
|
let lastLlmCallTime = 0;
|
|
819
825
|
let exitReason: ExitReason | null = null;
|
|
820
826
|
let appendedNewMessages = false;
|
|
827
|
+
// Armed at the end of a tool-use iteration so the budget gate runs at the
|
|
828
|
+
// top of the NEXT iteration — before that iteration's provider call —
|
|
829
|
+
// instead of after the current one. Stop-hook re-query continues re-enter
|
|
830
|
+
// without arming, so the gate fires on exactly the same occasions as the
|
|
831
|
+
// prior post-call placement, plus the first call when
|
|
832
|
+
// `compactInPlace` is set (the primary run's turn-start compaction).
|
|
833
|
+
let budgetGateArmed = compactInPlace;
|
|
821
834
|
const rlog = requestId ? log.child({ requestId }) : log;
|
|
822
835
|
|
|
823
836
|
// Resolve the inference-profile override that applies right now. The
|
|
@@ -858,6 +871,104 @@ export class AgentLoop {
|
|
|
858
871
|
let toolUseBlocks: Extract<ContentBlock, { type: "tool_use" }>[] = [];
|
|
859
872
|
|
|
860
873
|
try {
|
|
874
|
+
// ── Pre-call budget gate ─────────────────────────────────────
|
|
875
|
+
// When overflow recovery is enabled, estimate the running context
|
|
876
|
+
// size as it approaches the preflight budget before issuing the
|
|
877
|
+
// provider call. With `compactInPlace` the loop compacts in place and
|
|
878
|
+
// proceeds with the call; otherwise it yields (`exitReason =
|
|
879
|
+
// "budget"`) so the orchestrator can recover before the call risks a
|
|
880
|
+
// hard context-too-large rejection. Keyed off the loop's own
|
|
881
|
+
// `history.length` (the messages actually in context this turn,
|
|
882
|
+
// including tool iterations) rather than the durable conversation
|
|
883
|
+
// count.
|
|
884
|
+
//
|
|
885
|
+
// Armed after each tool-use iteration; stop-hook re-query continues
|
|
886
|
+
// skip it. The first call runs it only when `compactInPlace` is set,
|
|
887
|
+
// where it stands in for the orchestrator's turn-start compaction: it
|
|
888
|
+
// honors the compaction circuit breaker and proceeds with the call
|
|
889
|
+
// rather than yielding, since there is no prior turn output to
|
|
890
|
+
// escalate.
|
|
891
|
+
if (budgetGateArmed) {
|
|
892
|
+
budgetGateArmed = false;
|
|
893
|
+
// The gate only re-arms after a completed tool-use iteration
|
|
894
|
+
// (`toolUseTurns` is incremented first), so reaching it with
|
|
895
|
+
// `toolUseTurns === 0` uniquely identifies the first-call pass: it
|
|
896
|
+
// compacts-or-proceeds (never yields) and honors the compaction
|
|
897
|
+
// circuit breaker, matching the orchestrator's turn-start compaction.
|
|
898
|
+
const isFirstCallGate = toolUseTurns === 0;
|
|
899
|
+
const contextWindow = resolveContextWindow?.();
|
|
900
|
+
if (contextWindow?.overflowRecovery.enabled) {
|
|
901
|
+
const { maxInputTokens, overflowRecovery } = contextWindow;
|
|
902
|
+
const safetyMargin =
|
|
903
|
+
history.length > LONG_HISTORY_MESSAGE_THRESHOLD
|
|
904
|
+
? Math.max(
|
|
905
|
+
overflowRecovery.safetyMarginRatio,
|
|
906
|
+
LONG_HISTORY_SAFETY_MARGIN_FLOOR,
|
|
907
|
+
)
|
|
908
|
+
: overflowRecovery.safetyMarginRatio;
|
|
909
|
+
const preflightBudget = Math.floor(
|
|
910
|
+
maxInputTokens * (1 - safetyMargin),
|
|
911
|
+
);
|
|
912
|
+
const midLoopThreshold =
|
|
913
|
+
preflightBudget * MID_LOOP_YIELD_THRESHOLD_RATIO;
|
|
914
|
+
const estimated = this.estimateTokens(history);
|
|
915
|
+
if (estimated > midLoopThreshold) {
|
|
916
|
+
let compactedInPlace = false;
|
|
917
|
+
// The turn-start pass skips compaction while the circuit breaker
|
|
918
|
+
// is open so a run of failed summaries doesn't keep hammering the
|
|
919
|
+
// summary LLM; mid-loop compaction is force-driven and proceeds
|
|
920
|
+
// regardless (it has already committed to compacting in place).
|
|
921
|
+
const compactionAllowed =
|
|
922
|
+
!isFirstCallGate || !(await this.compactionCircuit.isOpen());
|
|
923
|
+
if (compactInPlace && compactionAllowed) {
|
|
924
|
+
rlog.info(
|
|
925
|
+
{
|
|
926
|
+
turn: toolUseTurns,
|
|
927
|
+
estimated,
|
|
928
|
+
threshold: midLoopThreshold,
|
|
929
|
+
},
|
|
930
|
+
"Token estimate approaching budget — compacting in place",
|
|
931
|
+
);
|
|
932
|
+
const compacted = await this.compact(
|
|
933
|
+
history,
|
|
934
|
+
requestId,
|
|
935
|
+
trust,
|
|
936
|
+
signal,
|
|
937
|
+
onEvent,
|
|
938
|
+
resolveEffectiveOverrideProfile() ?? null,
|
|
939
|
+
isNonInteractive,
|
|
940
|
+
modelProfile,
|
|
941
|
+
actorContext,
|
|
942
|
+
);
|
|
943
|
+
if (compacted) {
|
|
944
|
+
history = compacted;
|
|
945
|
+
// The compacted, re-injected array is the new base; output
|
|
946
|
+
// produced after this point is what the orchestrator
|
|
947
|
+
// persists.
|
|
948
|
+
newMessagesStart = history.length;
|
|
949
|
+
compactedInPlace = true;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
// The turn-start gate proceeds with the call whether or not it
|
|
953
|
+
// compacted (preflight-overflow recovery and the convergence loop
|
|
954
|
+
// remain the escalation path); only mid-loop re-entries yield to
|
|
955
|
+
// the orchestrator before the call.
|
|
956
|
+
if (!compactedInPlace && !isFirstCallGate) {
|
|
957
|
+
rlog.warn(
|
|
958
|
+
{
|
|
959
|
+
turn: toolUseTurns,
|
|
960
|
+
estimated,
|
|
961
|
+
threshold: midLoopThreshold,
|
|
962
|
+
},
|
|
963
|
+
"Token estimate approaching budget — yielding for compaction",
|
|
964
|
+
);
|
|
965
|
+
exitReason = "budget";
|
|
966
|
+
break;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
861
972
|
// Resolve tools for this turn: use the dynamic resolver if provided,
|
|
862
973
|
// otherwise fall back to the static tool list.
|
|
863
974
|
const currentTools = this.resolveTools
|
|
@@ -921,12 +1032,14 @@ export class AgentLoop {
|
|
|
921
1032
|
providerConfig.cacheTtl = this.config.cacheTtl;
|
|
922
1033
|
}
|
|
923
1034
|
|
|
924
|
-
// Cache-anchor signal for volatile latest-user-message turns
|
|
925
|
-
// memory-v3 injects
|
|
926
|
-
//
|
|
927
|
-
//
|
|
928
|
-
//
|
|
929
|
-
|
|
1035
|
+
// Cache-anchor signal for volatile latest-user-message turns: when
|
|
1036
|
+
// memory-v3 is live it injects a per-turn `<memory>` block into the
|
|
1037
|
+
// latest user message, so the provider must anchor its long-TTL cache
|
|
1038
|
+
// breakpoint on the most recent STABLE user message instead of the
|
|
1039
|
+
// volatile latest one. Read here alongside the rest of the provider
|
|
1040
|
+
// config; only set when true so the wire/config stays byte-identical
|
|
1041
|
+
// when off.
|
|
1042
|
+
if (isAssistantFeatureFlagEnabled("memory-v3-live", getConfig())) {
|
|
930
1043
|
providerConfig.mutableLatestUserMessage = true;
|
|
931
1044
|
}
|
|
932
1045
|
|
|
@@ -941,14 +1054,14 @@ export class AgentLoop {
|
|
|
941
1054
|
providerConfig.callSite = callSite;
|
|
942
1055
|
providerConfig.usageTracking = "manual";
|
|
943
1056
|
// Per-conversation seed for deterministic `mix`-profile expansion.
|
|
944
|
-
// Sourced from the
|
|
945
|
-
//
|
|
946
|
-
//
|
|
947
|
-
//
|
|
948
|
-
//
|
|
949
|
-
//
|
|
950
|
-
if (
|
|
951
|
-
providerConfig.selectionSeed =
|
|
1057
|
+
// Sourced from the loop's own conversation id so every LLM call in a
|
|
1058
|
+
// conversation resolves the same mix arm (stable across turns and
|
|
1059
|
+
// retries, and across daemon restarts since the seed is the durable
|
|
1060
|
+
// conversation id). Absent for standalone `AgentLoop` instances
|
|
1061
|
+
// (unit tests constructed without a conversation id) — those fall
|
|
1062
|
+
// back to per-call random mix selection.
|
|
1063
|
+
if (this.conversationId) {
|
|
1064
|
+
providerConfig.selectionSeed = this.conversationId;
|
|
952
1065
|
}
|
|
953
1066
|
}
|
|
954
1067
|
|
|
@@ -994,15 +1107,16 @@ export class AgentLoop {
|
|
|
994
1107
|
);
|
|
995
1108
|
rlog.info({ turn: toolUseTurns }, "LLM call start");
|
|
996
1109
|
|
|
997
|
-
//
|
|
998
|
-
//
|
|
999
|
-
//
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
//
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1110
|
+
// Sanitize the outbound history right before sending: drop accumulated
|
|
1111
|
+
// media, collapse old AX-tree snapshots, and convert historical
|
|
1112
|
+
// web-search results to text. See {@link preModelCallSanitize}.
|
|
1113
|
+
const providerHistory = preModelCallSanitize(history);
|
|
1114
|
+
|
|
1115
|
+
// A `pre-model-call` hook (below) can defer this turn's assistant
|
|
1116
|
+
// output; when set, the live text stream is held so an
|
|
1117
|
+
// `assistant-message` hook can emit the finalized (transformed) text
|
|
1118
|
+
// instead. Reset per model call.
|
|
1119
|
+
let deferAssistantOutput = false;
|
|
1006
1120
|
|
|
1007
1121
|
// The `onEvent` wrapping below applies sensitive-output placeholder
|
|
1008
1122
|
// substitution to streamed text while forwarding every other event
|
|
@@ -1013,6 +1127,9 @@ export class AgentLoop {
|
|
|
1013
1127
|
config: providerConfig,
|
|
1014
1128
|
onEvent: (event) => {
|
|
1015
1129
|
if (event.type === "text_delta") {
|
|
1130
|
+
// Held when the turn's output is deferred — the final text is
|
|
1131
|
+
// emitted once, after the `assistant-message` hook runs.
|
|
1132
|
+
if (deferAssistantOutput) return;
|
|
1016
1133
|
// Apply sensitive-output placeholder substitution (chunk-safe)
|
|
1017
1134
|
if (substitutionMap.size > 0) {
|
|
1018
1135
|
const combined = streamingPending + event.text;
|
|
@@ -1068,12 +1185,32 @@ export class AgentLoop {
|
|
|
1068
1185
|
signal,
|
|
1069
1186
|
};
|
|
1070
1187
|
|
|
1071
|
-
//
|
|
1072
|
-
//
|
|
1073
|
-
//
|
|
1074
|
-
//
|
|
1075
|
-
|
|
1076
|
-
|
|
1188
|
+
// Let plugins edit the outbound request and opt this call into deferred
|
|
1189
|
+
// output streaming. Runs for every provider call; hooks self-gate on
|
|
1190
|
+
// call site / conversation. Fail-open: a throwing hook leaves the
|
|
1191
|
+
// request unchanged and streaming live.
|
|
1192
|
+
try {
|
|
1193
|
+
const preModelCtx: PreModelCallContext = {
|
|
1194
|
+
conversationId: this.conversationId,
|
|
1195
|
+
callSite,
|
|
1196
|
+
systemPrompt: providerOptions.systemPrompt,
|
|
1197
|
+
deferAssistantOutput: false,
|
|
1198
|
+
logger: rlog,
|
|
1199
|
+
};
|
|
1200
|
+
const finalPreModelCtx = await runHook(
|
|
1201
|
+
HOOKS.PRE_MODEL_CALL,
|
|
1202
|
+
preModelCtx,
|
|
1203
|
+
);
|
|
1204
|
+
providerOptions.systemPrompt = finalPreModelCtx.systemPrompt;
|
|
1205
|
+
// The hook owns the policy (it sees `callSite`/conversation and
|
|
1206
|
+
// self-gates); the loop honors whatever it decides.
|
|
1207
|
+
deferAssistantOutput = finalPreModelCtx.deferAssistantOutput;
|
|
1208
|
+
} catch (preModelCallError) {
|
|
1209
|
+
rlog.error(
|
|
1210
|
+
{ err: preModelCallError },
|
|
1211
|
+
"pre-model-call hook failed — proceeding with the original request",
|
|
1212
|
+
);
|
|
1213
|
+
}
|
|
1077
1214
|
|
|
1078
1215
|
// Announce the LLM-call boundary so downstream handlers (the
|
|
1079
1216
|
// daemon's persistence pipeline) can reserve an empty assistant row
|
|
@@ -1161,12 +1298,52 @@ export class AgentLoop {
|
|
|
1161
1298
|
streamingPending = "";
|
|
1162
1299
|
}
|
|
1163
1300
|
|
|
1301
|
+
// Run the `assistant-message` hook on a finalized message and, when
|
|
1302
|
+
// output was deferred, emit the finalized text once (with sensitive-output
|
|
1303
|
+
// substitution applied, matching the live stream). Fail-open: the hook
|
|
1304
|
+
// receives a clone, so a throw — even mid in-place mutation — leaves the
|
|
1305
|
+
// original message intact.
|
|
1306
|
+
const finalizeAssistantMessage = async (
|
|
1307
|
+
message: Message,
|
|
1308
|
+
): Promise<Message> => {
|
|
1309
|
+
let finalized = message;
|
|
1310
|
+
try {
|
|
1311
|
+
const ctx: AssistantMessageContext = {
|
|
1312
|
+
conversationId: this.conversationId,
|
|
1313
|
+
callSite,
|
|
1314
|
+
content: structuredClone(message.content),
|
|
1315
|
+
stopReason: response.stopReason,
|
|
1316
|
+
logger: rlog,
|
|
1317
|
+
};
|
|
1318
|
+
const result = await runHook(HOOKS.ASSISTANT_MESSAGE, ctx);
|
|
1319
|
+
finalized = { role: "assistant", content: result.content };
|
|
1320
|
+
} catch (assistantMessageError) {
|
|
1321
|
+
rlog.error(
|
|
1322
|
+
{ err: assistantMessageError },
|
|
1323
|
+
"assistant-message hook failed — keeping the original content",
|
|
1324
|
+
);
|
|
1325
|
+
finalized = message;
|
|
1326
|
+
}
|
|
1327
|
+
if (deferAssistantOutput) {
|
|
1328
|
+
// The persisted message keeps sensitive-output placeholders; the
|
|
1329
|
+
// stream shows real values — substitute before emitting.
|
|
1330
|
+
const finalText = applySubstitutions(
|
|
1331
|
+
assistantTextOf(finalized.content),
|
|
1332
|
+
substitutionMap,
|
|
1333
|
+
);
|
|
1334
|
+
if (finalText.length > 0) {
|
|
1335
|
+
onEvent({ type: "text_delta", text: finalText });
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
return finalized;
|
|
1339
|
+
};
|
|
1340
|
+
|
|
1164
1341
|
// Build the assistant message with placeholder-only text.
|
|
1165
1342
|
// Both provider history and persisted conversation store must retain
|
|
1166
1343
|
// placeholders so the model never sees real sensitive values — neither
|
|
1167
1344
|
// on subsequent loop turns nor on session reload from the database.
|
|
1168
1345
|
// Substitution to real values happens only in streamed text_delta events.
|
|
1169
|
-
|
|
1346
|
+
let assistantMessage: Message = {
|
|
1170
1347
|
role: "assistant",
|
|
1171
1348
|
content: response.content,
|
|
1172
1349
|
};
|
|
@@ -1195,10 +1372,6 @@ export class AgentLoop {
|
|
|
1195
1372
|
block.type !== "server_tool_use" &&
|
|
1196
1373
|
block.type !== "web_search_tool_result",
|
|
1197
1374
|
);
|
|
1198
|
-
const safeAssistantMessage: Message = {
|
|
1199
|
-
role: "assistant",
|
|
1200
|
-
content: safeContent,
|
|
1201
|
-
};
|
|
1202
1375
|
rlog.warn(
|
|
1203
1376
|
{
|
|
1204
1377
|
turn: toolUseTurns,
|
|
@@ -1209,6 +1382,13 @@ export class AgentLoop {
|
|
|
1209
1382
|
},
|
|
1210
1383
|
"LLM response reached output token limit",
|
|
1211
1384
|
);
|
|
1385
|
+
// Run the hook on the truncated reply so output-filter plugins still
|
|
1386
|
+
// see it, and so a deferred turn gets its synthetic final emit (the
|
|
1387
|
+
// live stream was suppressed; without this the client would see nothing).
|
|
1388
|
+
const safeAssistantMessage = await finalizeAssistantMessage({
|
|
1389
|
+
role: "assistant",
|
|
1390
|
+
content: safeContent,
|
|
1391
|
+
});
|
|
1212
1392
|
history.push(safeAssistantMessage);
|
|
1213
1393
|
appendedNewMessages = true;
|
|
1214
1394
|
await onEvent({
|
|
@@ -1241,7 +1421,7 @@ export class AgentLoop {
|
|
|
1241
1421
|
// follow-up turn. It receives the full history and, when it asks to
|
|
1242
1422
|
// continue, appends the follow-up turn itself.
|
|
1243
1423
|
const stopCtx: StopContext = {
|
|
1244
|
-
conversationId:
|
|
1424
|
+
conversationId: this.conversationId,
|
|
1245
1425
|
messages: [...history],
|
|
1246
1426
|
responseContent: response.content,
|
|
1247
1427
|
stopReason: response.stopReason,
|
|
@@ -1280,6 +1460,12 @@ export class AgentLoop {
|
|
|
1280
1460
|
}
|
|
1281
1461
|
}
|
|
1282
1462
|
|
|
1463
|
+
// Run the `assistant-message` hook + emit any deferred final text.
|
|
1464
|
+
// On a no-tool turn this point is reached only after the `stop` hook
|
|
1465
|
+
// resolves to "stop" (a `continue` already re-queried above), so a
|
|
1466
|
+
// re-queried reply is never transformed-then-discarded.
|
|
1467
|
+
assistantMessage = await finalizeAssistantMessage(assistantMessage);
|
|
1468
|
+
|
|
1283
1469
|
history.push(assistantMessage);
|
|
1284
1470
|
appendedNewMessages = true;
|
|
1285
1471
|
|
|
@@ -1430,7 +1616,7 @@ export class AgentLoop {
|
|
|
1430
1616
|
continue;
|
|
1431
1617
|
}
|
|
1432
1618
|
const postToolUseCtx: PostToolUseContext = {
|
|
1433
|
-
conversationId:
|
|
1619
|
+
conversationId: this.conversationId,
|
|
1434
1620
|
toolResponse: block as ToolResultContent,
|
|
1435
1621
|
messages: history,
|
|
1436
1622
|
maxInputTokens: contextWindowTokens,
|
|
@@ -1508,8 +1694,9 @@ export class AgentLoop {
|
|
|
1508
1694
|
history.push({ role: "user", content: resultBlocks });
|
|
1509
1695
|
|
|
1510
1696
|
// Invoke checkpoint callback after tool results are in history.
|
|
1511
|
-
// Handoff
|
|
1512
|
-
//
|
|
1697
|
+
// Handoff takes precedence over the budget gate: a handoff decision
|
|
1698
|
+
// breaks here and leaves `budgetGateArmed` false, so a queued message
|
|
1699
|
+
// is processed before the next iteration's pre-call budget gate.
|
|
1513
1700
|
if (onCheckpoint) {
|
|
1514
1701
|
const decision = await onCheckpoint({
|
|
1515
1702
|
turnIndex: toolUseTurns - 1, // 0-based (toolUseTurns was already incremented)
|
|
@@ -1523,61 +1710,11 @@ export class AgentLoop {
|
|
|
1523
1710
|
}
|
|
1524
1711
|
}
|
|
1525
1712
|
|
|
1526
|
-
//
|
|
1527
|
-
// the
|
|
1528
|
-
//
|
|
1529
|
-
//
|
|
1530
|
-
|
|
1531
|
-
// context-too-large rejection. Keyed off the loop's own
|
|
1532
|
-
// `history.length` (the messages actually in context this turn,
|
|
1533
|
-
// including tool iterations) rather than the durable conversation
|
|
1534
|
-
// count.
|
|
1535
|
-
const contextWindow = resolveContextWindow?.();
|
|
1536
|
-
if (contextWindow?.overflowRecovery.enabled) {
|
|
1537
|
-
const { maxInputTokens, overflowRecovery } = contextWindow;
|
|
1538
|
-
const safetyMargin =
|
|
1539
|
-
history.length > LONG_HISTORY_MESSAGE_THRESHOLD
|
|
1540
|
-
? Math.max(
|
|
1541
|
-
overflowRecovery.safetyMarginRatio,
|
|
1542
|
-
LONG_HISTORY_SAFETY_MARGIN_FLOOR,
|
|
1543
|
-
)
|
|
1544
|
-
: overflowRecovery.safetyMarginRatio;
|
|
1545
|
-
const preflightBudget = Math.floor(
|
|
1546
|
-
maxInputTokens * (1 - safetyMargin),
|
|
1547
|
-
);
|
|
1548
|
-
const midLoopThreshold =
|
|
1549
|
-
preflightBudget * MID_LOOP_YIELD_THRESHOLD_RATIO;
|
|
1550
|
-
const estimated = this.estimateTokens(history);
|
|
1551
|
-
if (estimated > midLoopThreshold) {
|
|
1552
|
-
if (compaction) {
|
|
1553
|
-
rlog.info(
|
|
1554
|
-
{ phase: "mid-loop", estimated, threshold: midLoopThreshold },
|
|
1555
|
-
"Token estimate approaching budget — compacting in place",
|
|
1556
|
-
);
|
|
1557
|
-
const compacted = await this.compact(
|
|
1558
|
-
history,
|
|
1559
|
-
turnCtx,
|
|
1560
|
-
compaction,
|
|
1561
|
-
signal,
|
|
1562
|
-
onEvent,
|
|
1563
|
-
resolveEffectiveOverrideProfile() ?? null,
|
|
1564
|
-
);
|
|
1565
|
-
if (compacted) {
|
|
1566
|
-
history = compacted;
|
|
1567
|
-
// The compacted, re-injected array is the new base; output
|
|
1568
|
-
// produced after this point is what the orchestrator persists.
|
|
1569
|
-
newMessagesStart = history.length;
|
|
1570
|
-
continue;
|
|
1571
|
-
}
|
|
1572
|
-
}
|
|
1573
|
-
rlog.warn(
|
|
1574
|
-
{ phase: "mid-loop", estimated, threshold: midLoopThreshold },
|
|
1575
|
-
"Token estimate approaching budget — yielding for compaction",
|
|
1576
|
-
);
|
|
1577
|
-
exitReason = "budget";
|
|
1578
|
-
break;
|
|
1579
|
-
}
|
|
1580
|
-
}
|
|
1713
|
+
// Arm the pre-call budget gate for the next iteration. Placed after
|
|
1714
|
+
// the checkpoint so a handoff yield (which breaks above) leaves it
|
|
1715
|
+
// disarmed; the gate then runs at the top of the next iteration,
|
|
1716
|
+
// before that iteration's provider call.
|
|
1717
|
+
budgetGateArmed = true;
|
|
1581
1718
|
} catch (error) {
|
|
1582
1719
|
// Abort errors are expected when user cancels — synthesize
|
|
1583
1720
|
// cancellation tool_results so the history stays valid for the
|
|
@@ -1786,3 +1923,32 @@ function stripOldMediaBlocks(history: Message[]): Message[] {
|
|
|
1786
1923
|
};
|
|
1787
1924
|
});
|
|
1788
1925
|
}
|
|
1926
|
+
|
|
1927
|
+
/**
|
|
1928
|
+
* Sanitize the outbound history immediately before a provider call, bundling
|
|
1929
|
+
* the pre-send transforms the loop applies to every request:
|
|
1930
|
+
* - {@link stripOldMediaBlocks} drops accumulated screenshot/audio bytes from
|
|
1931
|
+
* older tool results — the model saw the media on the turn it was captured.
|
|
1932
|
+
* - {@link compactAxTreeHistory} collapses all but the most recent few
|
|
1933
|
+
* `<ax-tree>` snapshots so TTFT does not grow linearly with step count.
|
|
1934
|
+
* - {@link stripHistoricalWebSearchResults} converts historical
|
|
1935
|
+
* `web_search_tool_result` blocks to text summaries; Anthropic's opaque
|
|
1936
|
+
* `encrypted_content` tokens expire / are route-scoped, and replaying a stale
|
|
1937
|
+
* one is rejected with `Invalid encrypted_content in search_result block`.
|
|
1938
|
+
*
|
|
1939
|
+
* Transforms the outbound copy only — the durable history keeps the rich
|
|
1940
|
+
* originals and each send re-derives the sanitized projection (every transform
|
|
1941
|
+
* is idempotent). Because it runs unconditionally before every provider call,
|
|
1942
|
+
* it is the single place where oversized media and expired web-search tokens
|
|
1943
|
+
* are guaranteed to be removed from a request.
|
|
1944
|
+
*
|
|
1945
|
+
* This is outbound-request preparation and should eventually move to a default
|
|
1946
|
+
* `pre-model-call` plugin hook ({@link HOOKS.PRE_MODEL_CALL}) once that hook's
|
|
1947
|
+
* context carries the outbound message list; for now it lives inline next to
|
|
1948
|
+
* the provider call it guards.
|
|
1949
|
+
*/
|
|
1950
|
+
export function preModelCallSanitize(history: Message[]): Message[] {
|
|
1951
|
+
const mediaStripped = stripOldMediaBlocks(history);
|
|
1952
|
+
const axCompacted = compactAxTreeHistory(mediaStripped);
|
|
1953
|
+
return stripHistoricalWebSearchResults(axCompacted).messages;
|
|
1954
|
+
}
|