@vellumai/assistant 0.8.8 → 0.8.9-staging.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bun.lock +2 -2
- package/examples/plugins/echo/README.md +61 -60
- package/examples/plugins/echo/hooks/post-tool-use.ts +18 -0
- package/examples/plugins/echo/hooks/stop.ts +16 -0
- package/examples/plugins/echo/hooks/user-prompt-submit.ts +18 -0
- package/examples/plugins/echo/package.json +1 -2
- package/examples/plugins/echo/src/emit.ts +19 -0
- package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +7 -6
- package/openapi.yaml +236 -6
- package/package.json +2 -2
- package/src/__tests__/agent-loop-callsite-precedence.test.ts +69 -14
- package/src/__tests__/agent-loop-exit-reason.test.ts +185 -140
- package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +50 -35
- package/src/__tests__/agent-loop-output-hooks.test.ts +357 -0
- package/src/__tests__/agent-loop-override-profile.test.ts +25 -6
- package/src/__tests__/agent-loop-provider-error-recording.test.ts +41 -21
- package/src/__tests__/agent-loop-thinking.test.ts +36 -20
- package/src/__tests__/agent-loop.test.ts +441 -96
- package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +14 -14
- package/src/__tests__/agent-wake-override-profile.test.ts +17 -21
- package/src/__tests__/anthropic-provider.test.ts +1 -1
- package/src/__tests__/app-control-flow.test.ts +1 -1
- package/src/__tests__/app-dir-path-guard.test.ts +1 -0
- package/src/__tests__/approval-cascade.test.ts +5 -4
- package/src/__tests__/approval-routes-http.test.ts +4 -1
- package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
- package/src/__tests__/channel-approval-routes.test.ts +1 -1
- package/src/__tests__/channel-approvals.test.ts +1 -1
- package/src/__tests__/compaction-circuit.test.ts +258 -0
- package/src/__tests__/compaction-direct.test.ts +132 -0
- package/src/__tests__/compaction-events.test.ts +5 -5
- package/src/__tests__/conversation-abort-tool-results.test.ts +6 -4
- package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +7 -10
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +59 -103
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +94 -189
- package/src/__tests__/conversation-agent-loop.test.ts +249 -490
- package/src/__tests__/conversation-clean-command.test.ts +5 -2
- package/src/__tests__/conversation-confirmation-signals.test.ts +5 -4
- package/src/__tests__/conversation-crud-inference-profile.test.ts +7 -9
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +4 -4
- package/src/__tests__/conversation-process-callsite.test.ts +14 -14
- package/src/__tests__/conversation-provider-retry-repair.test.ts +18 -16
- package/src/__tests__/conversation-queue.test.ts +9 -9
- package/src/__tests__/conversation-runtime-assembly.test.ts +923 -231
- package/src/__tests__/conversation-runtime-workspace.test.ts +115 -20
- package/src/__tests__/conversation-slash-queue.test.ts +6 -4
- package/src/__tests__/conversation-slash-unknown.test.ts +6 -4
- package/src/__tests__/conversation-speed-override.test.ts +10 -9
- package/src/__tests__/conversation-starter-routes.test.ts +14 -6
- package/src/__tests__/conversation-workspace-cache-state.test.ts +23 -20
- package/src/__tests__/conversation-workspace-injection.test.ts +68 -6
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +14 -11
- package/src/__tests__/conversations-import-system-filter.test.ts +101 -0
- package/src/__tests__/credential-security-invariants.test.ts +0 -1
- package/src/__tests__/db-acp-history.test.ts +101 -0
- package/src/__tests__/dynamic-page-surface.test.ts +31 -0
- package/src/__tests__/empty-response-hook.test.ts +1 -1
- package/src/__tests__/file-write-tool.test.ts +63 -0
- package/src/__tests__/gateway-only-guard.test.ts +12 -2
- package/src/__tests__/guardian-grant-minting.test.ts +1 -1
- package/src/__tests__/guardian-routing-invariants.test.ts +2 -4
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +1 -1
- package/src/__tests__/heartbeat-disk-pressure.test.ts +1 -0
- package/src/__tests__/heartbeat-service.test.ts +1 -0
- package/src/__tests__/history-repair-hook.test.ts +1 -1
- package/src/__tests__/host-app-control-routes.test.ts +1 -1
- package/src/__tests__/host-cu-routes-targeted.test.ts +3 -3
- package/src/__tests__/inference-profile-reaper.test.ts +62 -0
- package/src/__tests__/inference-profile-session-handler.test.ts +86 -0
- package/src/__tests__/injector-background-turn.test.ts +13 -23
- package/src/__tests__/injector-chain.test.ts +268 -44
- package/src/__tests__/injector-disk-pressure.test.ts +210 -52
- package/src/__tests__/injector-document-comments.test.ts +97 -114
- package/src/__tests__/injector-pkb-v2-silenced.test.ts +2 -2
- package/src/__tests__/injector-v3-suppression.test.ts +4 -4
- package/src/__tests__/list-messages-client-message-id.test.ts +91 -0
- package/src/__tests__/list-messages-hidden-metadata.test.ts +38 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +73 -6
- package/src/__tests__/memory-v2-static-injector.test.ts +86 -8
- package/src/__tests__/parallel-tool.benchmark.test.ts +35 -8
- package/src/__tests__/plugin-api-shim.test.ts +6 -9
- package/src/__tests__/plugin-bootstrap.test.ts +12 -23
- package/src/__tests__/plugin-registry.test.ts +3 -49
- package/src/__tests__/plugin-types.test.ts +0 -70
- package/src/__tests__/reaction-persistence.test.ts +1 -1
- package/src/__tests__/send-endpoint-busy.test.ts +4 -1
- package/src/__tests__/skill-feature-flags-integration.test.ts +33 -0
- package/src/__tests__/steer-tool-repair.test.ts +1 -1
- package/src/__tests__/subagent-call-site-routing.test.ts +1 -1
- package/src/__tests__/subagent-detail.test.ts +25 -7
- package/src/__tests__/subagent-fork-notifications.test.ts +1 -3
- package/src/__tests__/subagent-fork-spawn.test.ts +1 -1
- package/src/__tests__/subagent-manager-notify.test.ts +1 -3
- package/src/__tests__/subagent-notify-parent.test.ts +1 -3
- package/src/__tests__/subagent-spawn-tool-fork.test.ts +1 -1
- package/src/__tests__/title-generate-hook.test.ts +1 -1
- package/src/__tests__/tool-error-hook.test.ts +1 -1
- package/src/__tests__/tool-result-truncate-hook.test.ts +1 -1
- package/src/__tests__/user-plugin-loader.test.ts +54 -286
- package/src/acp/__tests__/agent-process.test.ts +161 -0
- package/src/acp/__tests__/client-handler.test.ts +40 -0
- package/src/acp/__tests__/helpers/acp-history-db.ts +82 -0
- package/src/acp/__tests__/helpers/exec-file-stub.ts +101 -0
- package/src/acp/__tests__/prepare-agent-env.test.ts +137 -0
- package/src/acp/__tests__/session-manager-persistence.test.ts +95 -28
- package/src/acp/__tests__/session-manager-resume.test.ts +736 -0
- package/src/acp/agent-process.ts +61 -1
- package/src/acp/auto-install.test.ts +196 -0
- package/src/acp/auto-install.ts +177 -0
- package/src/acp/client-handler.ts +31 -0
- package/src/acp/feature-gate.test.ts +48 -0
- package/src/acp/feature-gate.ts +34 -0
- package/src/acp/prepare-agent-env.ts +83 -29
- package/src/acp/resolve-agent.test.ts +320 -7
- package/src/acp/resolve-agent.ts +182 -18
- package/src/acp/resume-hint.ts +25 -0
- package/src/acp/session-manager.ts +495 -73
- package/src/acp/types.ts +8 -0
- package/src/agent/compaction-circuit.ts +60 -102
- package/src/agent/loop.ts +390 -240
- package/src/api/responses/conversation-message.ts +14 -1
- package/src/approvals/guardian-request-resolvers.ts +1 -1
- package/src/background-wake/next-wake.ts +1 -0
- package/src/cli/commands/db/__tests__/repair.test.ts +3 -1
- package/src/cli/commands/plugins.ts +43 -37
- package/src/cli/lib/__tests__/install-from-github.test.ts +429 -111
- package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +196 -0
- package/src/cli/lib/__tests__/plugin-details.test.ts +372 -0
- package/src/cli/lib/__tests__/plugin-marketplace.test.ts +220 -0
- package/src/cli/lib/__tests__/search-plugins.test.ts +226 -32
- package/src/cli/lib/install-from-github.ts +464 -55
- package/src/cli/lib/plugin-catalog-cache.ts +84 -0
- package/src/cli/lib/plugin-details.ts +409 -0
- package/src/cli/lib/plugin-marketplace.ts +197 -0
- package/src/cli/lib/search-plugins.ts +195 -29
- package/src/config/__tests__/feature-flag-registry-guard.test.ts +2 -2
- package/src/config/acp-defaults.test.ts +10 -0
- package/src/config/acp-defaults.ts +6 -0
- package/src/config/bundled-skills/acp/SKILL.md +83 -31
- package/src/config/bundled-skills/acp/TOOLS.json +4 -4
- package/src/config/bundled-skills/app-builder/SKILL.md +224 -381
- package/src/config/bundled-skills/app-builder/TOOLS.json +29 -0
- package/src/config/bundled-skills/app-builder/references/DESIGN_SYSTEM.md +48 -0
- package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +57 -0
- package/src/config/bundled-skills/app-builder/references/SLIDES.md +38 -0
- package/src/config/bundled-skills/app-builder/tools/app-list.ts +62 -0
- package/src/config/bundled-skills/document-editor/SKILL.md +28 -23
- package/src/config/bundled-skills/document-editor/TOOLS.json +1 -1
- package/src/config/bundled-tool-registry.ts +2 -0
- package/src/config/feature-flag-registry.json +14 -5
- package/src/config/schemas/heartbeat.ts +9 -0
- package/src/context/strip-injections.ts +8 -2
- package/src/context/window-manager.ts +27 -13
- package/src/daemon/conversation-agent-loop-handlers.ts +10 -35
- package/src/daemon/conversation-agent-loop.ts +167 -1005
- package/src/daemon/conversation-lifecycle.ts +11 -255
- package/src/daemon/conversation-process.ts +8 -136
- package/src/daemon/conversation-registry.ts +159 -0
- package/src/daemon/conversation-runtime-assembly.ts +293 -392
- package/src/daemon/conversation-store.ts +9 -90
- package/src/daemon/conversation-surfaces.ts +24 -8
- package/src/daemon/conversation-workspace.ts +17 -0
- package/src/daemon/conversation.ts +383 -56
- package/src/daemon/external-plugins-bootstrap.ts +14 -19
- package/src/daemon/handlers/conversations.ts +3 -1
- package/src/daemon/handlers/skills.ts +4 -1
- package/src/daemon/host-proxy-preactivation.ts +1 -3
- package/src/daemon/lifecycle.ts +21 -0
- package/src/daemon/server.ts +2 -0
- package/src/daemon/wake-conversation-ops.ts +269 -0
- package/src/embedded/plugin-api.ts +2 -2
- package/src/export/__tests__/transcript-formatter.test.ts +5 -0
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +3 -0
- package/src/heartbeat/heartbeat-run-store.ts +23 -1
- package/src/heartbeat/heartbeat-service.ts +26 -0
- package/src/ipc/__tests__/browser-ipc.test.ts +1 -1
- package/src/ipc/__tests__/ui-request-route.test.ts +3 -3
- package/src/ipc/skill-routes/__tests__/memory.test.ts +15 -0
- package/src/ipc/skill-routes/memory.ts +4 -2
- package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +87 -0
- package/src/memory/conversation-crud.ts +29 -19
- package/src/memory/conversation-starter-checkpoints.ts +1 -0
- package/src/memory/db-init.ts +2 -0
- package/src/memory/job-handlers/conversation-starters.ts +13 -2
- package/src/memory/jobs/__tests__/embed-concept-page.test.ts +5 -4
- package/src/memory/jobs-worker.ts +25 -1
- package/src/memory/migrations/272-acp-session-history-cwd.ts +36 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/schema/acp.ts +4 -0
- package/src/memory/v2/__tests__/consolidation-job.test.ts +3 -3
- package/src/memory/v2/consolidation-job.ts +13 -4
- package/src/plugin-api/constants.ts +4 -0
- package/src/plugin-api/index.ts +6 -5
- package/src/plugin-api/types.ts +75 -0
- package/src/plugins/defaults/compaction/compact.ts +59 -0
- package/src/plugins/defaults/compaction/manager-store.ts +44 -0
- package/src/plugins/defaults/compaction/package.json +1 -2
- package/src/plugins/defaults/empty-response/package.json +0 -1
- package/src/plugins/defaults/history-repair/package.json +0 -1
- package/src/plugins/defaults/index.ts +135 -26
- package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +103 -38
- package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +167 -56
- package/src/plugins/defaults/memory-retrieval/injector-chain.ts +2 -2
- package/src/plugins/defaults/{injectors/register.ts → memory-retrieval/injectors.ts} +148 -73
- package/src/plugins/defaults/memory-retrieval/unified-turn-context.ts +223 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/assign.test.ts +4 -4
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/live-integration.test.ts +9 -6
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/maintain-job.test.ts +5 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/orchestrate.test.ts +8 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/reconcile.test.ts +2 -2
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/render-injection.test.ts +1 -1
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/router.test.ts +10 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selection-log-store.test.ts +8 -8
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selector.test.ts +5 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/shadow-plugin.test.ts +16 -17
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/types.test.ts +2 -2
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/assign.ts +9 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/capabilities.ts +5 -2
- package/src/plugins/defaults/memory-v3-shadow/hooks/post-compact.ts +14 -0
- package/src/plugins/defaults/memory-v3-shadow/hooks/user-prompt-submit.ts +19 -0
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +75 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/maintain-job.ts +15 -8
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/orchestrate.ts +2 -2
- package/src/plugins/defaults/memory-v3-shadow/package.json +14 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/page-content.ts +2 -2
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/provider-blocks.ts +1 -1
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/reconcile.ts +7 -3
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/render-injection.ts +1 -1
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/router.ts +5 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/selection-log-store.ts +4 -4
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/selector.ts +7 -7
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/shadow-plugin.ts +32 -94
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/tree.ts +1 -1
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/types.ts +1 -1
- package/src/plugins/defaults/title-generate/package.json +0 -1
- package/src/plugins/defaults/tool-error/package.json +0 -1
- package/src/plugins/defaults/tool-result-truncate/package.json +0 -1
- package/src/plugins/pipeline.ts +6 -293
- package/src/plugins/registry.ts +9 -37
- package/src/plugins/types.ts +76 -381
- package/src/plugins/user-loader.ts +30 -127
- package/src/proactive-artifact/aux-message-injector.ts +1 -1
- package/src/proactive-artifact/job.test.ts +1 -1
- package/src/prompts/__tests__/system-prompt.test.ts +6 -0
- package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +35 -3
- package/src/runtime/__tests__/agent-wake.test.ts +555 -691
- package/src/runtime/__tests__/interactive-ui.test.ts +1 -1
- package/src/runtime/agent-wake.ts +108 -209
- package/src/runtime/assistant-event-hub.ts +1 -1
- package/src/runtime/channel-approvals.ts +1 -1
- package/src/runtime/interactive-ui.ts +1 -1
- package/src/runtime/routes/__tests__/acp-routes.test.ts +283 -55
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +1 -1
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +398 -73
- package/src/runtime/routes/__tests__/surface-action-routes.test.ts +5 -4
- package/src/runtime/routes/__tests__/surface-content-routes.test.ts +4 -1
- package/src/runtime/routes/acp-routes.test.ts +89 -25
- package/src/runtime/routes/acp-routes.ts +81 -29
- package/src/runtime/routes/approval-routes.ts +1 -1
- package/src/runtime/routes/browser-routes.ts +1 -1
- package/src/runtime/routes/browser-tabs-routes.ts +6 -10
- package/src/runtime/routes/conversation-cli-routes.ts +1 -1
- package/src/runtime/routes/conversation-list-routes.ts +1 -1
- package/src/runtime/routes/conversation-query-routes.ts +1 -1
- package/src/runtime/routes/conversation-routes.ts +28 -2
- package/src/runtime/routes/conversation-starter-routes.ts +13 -7
- package/src/runtime/routes/conversations-import-routes.ts +24 -7
- package/src/runtime/routes/host-app-control-routes.ts +1 -1
- package/src/runtime/routes/host-cu-routes.ts +1 -1
- package/src/runtime/routes/identity-routes.ts +18 -3
- package/src/runtime/routes/inbound-message-handler.ts +1 -1
- package/src/runtime/routes/inference-profile-session-handler.ts +11 -0
- package/src/runtime/routes/inference-profile-session-reaper.ts +6 -0
- package/src/runtime/routes/memory-v3-routes.ts +16 -6
- package/src/runtime/routes/playground/helpers.ts +1 -1
- package/src/runtime/routes/plugins-routes.ts +336 -35
- package/src/runtime/routes/surface-conversation-resolver.ts +4 -3
- package/src/runtime/routes/work-items-routes.ts +2 -4
- package/src/runtime/services/conversation-serializer.ts +1 -1
- package/src/signals/cancel.ts +2 -4
- package/src/subagent/manager.ts +21 -5
- package/src/tools/acp/context.ts +20 -0
- package/src/tools/acp/list-agents.test.ts +7 -1
- package/src/tools/acp/spawn.test.ts +158 -55
- package/src/tools/acp/spawn.ts +47 -72
- package/src/tools/acp/steer.test.ts +105 -8
- package/src/tools/acp/steer.ts +48 -17
- package/src/tools/apps/executors.ts +13 -8
- package/src/tools/filesystem/write.ts +34 -0
- package/src/tools/subagent/spawn.ts +2 -4
- package/src/tools/ui-surface/definitions.ts +25 -5
- package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +4 -5
- package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +69 -45
- package/docs/plugins.md +0 -832
- package/examples/plugins/echo/register.ts +0 -143
- package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -405
- package/src/__tests__/compaction-pipeline.test.ts +0 -210
- package/src/__tests__/compaction-timeout-recovery.test.ts +0 -251
- package/src/__tests__/overflow-reduce-pipeline.test.ts +0 -667
- package/src/__tests__/pipeline-runner.test.ts +0 -554
- package/src/__tests__/plugin-external-api.test.ts +0 -68
- package/src/daemon/wake-target-adapter.ts +0 -253
- package/src/plugins/defaults/circuit-breaker/middlewares/circuitBreaker.ts +0 -93
- package/src/plugins/defaults/circuit-breaker/package.json +0 -15
- package/src/plugins/defaults/circuit-breaker/register.ts +0 -39
- package/src/plugins/defaults/compaction/middlewares/compaction.ts +0 -25
- package/src/plugins/defaults/compaction/register.ts +0 -35
- package/src/plugins/defaults/compaction/terminal.ts +0 -73
- package/src/plugins/defaults/empty-response/register.ts +0 -23
- package/src/plugins/defaults/history-repair/register.ts +0 -24
- package/src/plugins/defaults/overflow-reduce/middlewares/overflowReduce.ts +0 -126
- package/src/plugins/defaults/overflow-reduce/package.json +0 -15
- package/src/plugins/defaults/overflow-reduce/register.ts +0 -42
- package/src/plugins/defaults/title-generate/register.ts +0 -35
- package/src/plugins/defaults/tool-error/register.ts +0 -23
- package/src/plugins/defaults/tool-result-truncate/register.ts +0 -24
- package/src/plugins/external-api.ts +0 -104
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/capabilities.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/core.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/eval-turns.json +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/live-turns.json +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/health.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/needle.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/provider-blocks.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/snapshot.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/tree.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-eviction.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-skeleton.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/core.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/README.md +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/assignments.json +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/core.json +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-x.md +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-y.md +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-b/topic-z.md +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/health.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/llm-retry.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/needle.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/snapshot.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/working-set.ts +0 -0
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 {
|
|
@@ -8,21 +10,28 @@ import {
|
|
|
8
10
|
estimateToolsTokens,
|
|
9
11
|
getCalibrationProviderKey,
|
|
10
12
|
} from "../context/token-estimator.js";
|
|
11
|
-
import type {
|
|
13
|
+
import type {
|
|
14
|
+
ContextWindowManager,
|
|
15
|
+
ContextWindowResult,
|
|
16
|
+
} from "../context/window-manager.js";
|
|
17
|
+
import type { InboundActorContext } from "../daemon/conversation-runtime-assembly.js";
|
|
12
18
|
import type { ToolActivityMetadata } from "../daemon/message-types/web-activity.js";
|
|
19
|
+
import type { TrustContext } from "../daemon/trust-context.js";
|
|
13
20
|
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
21
|
import type {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
} from "../
|
|
25
|
-
import {
|
|
22
|
+
AssistantMessageContext,
|
|
23
|
+
PostToolUseContext,
|
|
24
|
+
PreModelCallContext,
|
|
25
|
+
StopContext,
|
|
26
|
+
} from "../plugin-api/types.js";
|
|
27
|
+
import {
|
|
28
|
+
DEFAULT_COMPACTION_PLUGIN_NAME,
|
|
29
|
+
defaultCompact,
|
|
30
|
+
} from "../plugins/defaults/compaction/compact.js";
|
|
31
|
+
import postCompact from "../plugins/defaults/memory-retrieval/hooks/post-compact.js";
|
|
32
|
+
import { runHook } from "../plugins/pipeline.js";
|
|
33
|
+
import type { CompactionCircuitEvent } from "../plugins/types.js";
|
|
34
|
+
import { PluginExecutionError } from "../plugins/types.js";
|
|
26
35
|
import { normalizeThinkingConfigForWire } from "../providers/thinking-config.js";
|
|
27
36
|
import type {
|
|
28
37
|
ContentBlock,
|
|
@@ -308,7 +317,7 @@ export type AgentEvent =
|
|
|
308
317
|
* immediately before re-injection — whether or not the pipeline actually
|
|
309
318
|
* compacted. The daemon's event dispatcher always commits `basis` (the
|
|
310
319
|
* stripped pre-compaction history) as the conversation's durable message
|
|
311
|
-
* state, so re-injection ({@link
|
|
320
|
+
* state, so re-injection ({@link postCompact}) re-applies
|
|
312
321
|
* injections onto the stripped base rather than stacking on top of the
|
|
313
322
|
* still-injected messages. When `result.compacted` is set it
|
|
314
323
|
* additionally commits the durable compaction result (DB-record fields,
|
|
@@ -382,34 +391,17 @@ export function isMaxTokensStopReason(
|
|
|
382
391
|
}
|
|
383
392
|
|
|
384
393
|
/**
|
|
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.
|
|
394
|
+
* Concatenate the text of an assistant message's `text` blocks (ignoring
|
|
395
|
+
* `tool_use`, `thinking`, and other non-text blocks). Used to re-emit the
|
|
396
|
+
* finalized text as a single `text_delta` when a turn's live output was
|
|
397
|
+
* deferred by a `pre-model-call` hook.
|
|
396
398
|
*/
|
|
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
|
-
};
|
|
399
|
+
function assistantTextOf(content: ReadonlyArray<ContentBlock>): string {
|
|
400
|
+
let text = "";
|
|
401
|
+
for (const block of content) {
|
|
402
|
+
if (block.type === "text") text += block.text;
|
|
403
|
+
}
|
|
404
|
+
return text;
|
|
413
405
|
}
|
|
414
406
|
|
|
415
407
|
/**
|
|
@@ -454,29 +446,11 @@ export interface ResolvedSystemPrompt {
|
|
|
454
446
|
model?: string;
|
|
455
447
|
}
|
|
456
448
|
|
|
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
449
|
export interface AgentLoopRunOptions {
|
|
450
|
+
/** Input history the run starts from; the loop appends its output onto a copy. */
|
|
451
|
+
messages: Message[];
|
|
452
|
+
/** Sink the loop streams its {@link AgentEvent}s through as the turn runs. */
|
|
453
|
+
onEvent: (event: AgentEvent) => void | Promise<void>;
|
|
480
454
|
signal?: AbortSignal;
|
|
481
455
|
requestId?: string;
|
|
482
456
|
onCheckpoint?: (
|
|
@@ -484,14 +458,24 @@ export interface AgentLoopRunOptions {
|
|
|
484
458
|
) => CheckpointDecision | Promise<CheckpointDecision>;
|
|
485
459
|
callSite?: LLMCallSite;
|
|
486
460
|
/**
|
|
487
|
-
*
|
|
488
|
-
*
|
|
489
|
-
*
|
|
490
|
-
*
|
|
491
|
-
*
|
|
492
|
-
*
|
|
461
|
+
* Trust classification and channel identity for the turn's inbound actor,
|
|
462
|
+
* supplied by the caller as the turn-start snapshot. Read only on the
|
|
463
|
+
* mid-loop in-place compaction path — to scope the compactor's image
|
|
464
|
+
* manifest (guardian-only attachments are excluded for untrusted actors) and
|
|
465
|
+
* forwarded to {@link postCompact}. Callers without a meaningful actor (agent
|
|
466
|
+
* wakes, standalone unit tests) pass an `unknown`-class snapshot so the
|
|
467
|
+
* compactor fail-closes to excluding guardian-only attachments.
|
|
493
468
|
*/
|
|
494
|
-
|
|
469
|
+
trust: TrustContext;
|
|
470
|
+
/**
|
|
471
|
+
* The conversation's {@link ContextWindowManager}, supplied by the
|
|
472
|
+
* orchestrator so mid-loop in-place compaction can run against the real
|
|
473
|
+
* per-conversation manager. Absent for callers without a compaction path
|
|
474
|
+
* (agent wakes, standalone unit tests); the loop only reads it when the
|
|
475
|
+
* mid-loop budget gate trips with {@link AgentLoopRunOptions.compactInPlace}
|
|
476
|
+
* enabled.
|
|
477
|
+
*/
|
|
478
|
+
contextWindowManager?: ContextWindowManager;
|
|
495
479
|
/**
|
|
496
480
|
* Ad-hoc inference-profile override applied to every LLM call the loop
|
|
497
481
|
* issues. When set, each `SendMessageOptions.config` carries
|
|
@@ -514,21 +498,51 @@ export interface AgentLoopRunOptions {
|
|
|
514
498
|
overflowRecovery: { enabled: boolean; safetyMarginRatio: number };
|
|
515
499
|
};
|
|
516
500
|
/**
|
|
517
|
-
*
|
|
518
|
-
*
|
|
519
|
-
*
|
|
520
|
-
*
|
|
501
|
+
* When `true`, the loop owns turn-start and mid-loop compaction. The pre-call
|
|
502
|
+
* budget gate runs before the very first provider call — subsuming the
|
|
503
|
+
* proactive turn-start compaction the orchestrator would otherwise perform
|
|
504
|
+
* inline before `run()` — as well as before each tool-use re-entry. When the
|
|
505
|
+
* gate trips it compacts the running history in place, re-applying runtime
|
|
506
|
+
* injections via the default post-compaction hook ({@link postCompact}), and
|
|
507
|
+
* continues instead of yielding `exitReason = "budget"`.
|
|
508
|
+
*
|
|
509
|
+
* The first-call pass honors the compaction circuit breaker and proceeds with
|
|
510
|
+
* the call whether or not it compacted (preflight-overflow recovery and the
|
|
511
|
+
* convergence loop remain the escalation path), so it never yields on the
|
|
512
|
+
* first call. Reruns without an inline compaction path (agent wakes,
|
|
513
|
+
* convergence/auto-compress reruns) leave it `false`: they skip the
|
|
514
|
+
* first-call gate and keep yielding for budget on mid-loop re-entries.
|
|
515
|
+
* Defaults to `false` when omitted.
|
|
516
|
+
*/
|
|
517
|
+
compactInPlace?: boolean;
|
|
518
|
+
/**
|
|
519
|
+
* Whether the in-flight turn has no human present to answer clarification
|
|
520
|
+
* questions. Resolved once by the orchestrator at turn start and forwarded to
|
|
521
|
+
* {@link postCompact} so post-compaction
|
|
522
|
+
* re-injection uses the turn-start snapshot rather than re-reading mutable
|
|
523
|
+
* client/headless state mid-turn. Defaults to `false` when omitted.
|
|
521
524
|
*/
|
|
522
|
-
|
|
525
|
+
isNonInteractive?: boolean;
|
|
523
526
|
/**
|
|
524
|
-
*
|
|
525
|
-
*
|
|
526
|
-
*
|
|
527
|
-
*
|
|
528
|
-
*
|
|
529
|
-
*
|
|
527
|
+
* The `model_profile:` turn-context label resolved once by the orchestrator
|
|
528
|
+
* at turn start, or `null` when the active inference profile is unchanged
|
|
529
|
+
* since the last notified one. Forwarded to
|
|
530
|
+
* {@link postCompact} so post-compaction re-injection
|
|
531
|
+
* re-emits the turn-start value rather than re-deriving the change-detected
|
|
532
|
+
* label (which flips once the notification is persisted mid-turn). Defaults to
|
|
533
|
+
* `null` when omitted.
|
|
530
534
|
*/
|
|
531
|
-
|
|
535
|
+
modelProfile?: string | null;
|
|
536
|
+
/**
|
|
537
|
+
* Inbound actor identity and trust fields for the unified `<turn_context>`
|
|
538
|
+
* block, or `null` on guardian turns. Resolved once by the orchestrator at
|
|
539
|
+
* turn start via the actor-trust resolver, whose contact/member registry
|
|
540
|
+
* inputs can be mutated mid-turn by contact tools, and forwarded to
|
|
541
|
+
* {@link postCompact} so post-compaction
|
|
542
|
+
* re-injection re-emits the turn-start value rather than re-resolving it.
|
|
543
|
+
* Defaults to `null` when omitted.
|
|
544
|
+
*/
|
|
545
|
+
actorContext?: InboundActorContext | null;
|
|
532
546
|
}
|
|
533
547
|
|
|
534
548
|
/**
|
|
@@ -570,17 +584,21 @@ export type LoopToolExecutor = (
|
|
|
570
584
|
}>;
|
|
571
585
|
|
|
572
586
|
export interface AgentLoopConstructorOptions {
|
|
587
|
+
/** LLM provider the loop issues every call through. */
|
|
588
|
+
provider: Provider;
|
|
589
|
+
/** Base system prompt, before any per-turn dynamic resolution. */
|
|
590
|
+
systemPrompt: string;
|
|
573
591
|
config?: Partial<AgentLoopConfig>;
|
|
574
592
|
tools?: ToolDefinition[];
|
|
575
593
|
toolExecutor?: LoopToolExecutor;
|
|
576
594
|
resolveTools?: (history: Message[]) => ToolDefinition[];
|
|
577
595
|
resolveSystemPrompt?: (history: Message[]) => ResolvedSystemPrompt;
|
|
578
596
|
/**
|
|
579
|
-
* Conversation this loop drives.
|
|
580
|
-
*
|
|
581
|
-
*
|
|
597
|
+
* Conversation this loop drives. Scopes the loop-held compaction circuit
|
|
598
|
+
* breaker and is the source of truth the loop's pipeline contexts and
|
|
599
|
+
* post-compaction re-injection resolve the live conversation through.
|
|
582
600
|
*/
|
|
583
|
-
conversationId
|
|
601
|
+
conversationId: string;
|
|
584
602
|
}
|
|
585
603
|
|
|
586
604
|
export class AgentLoop {
|
|
@@ -594,6 +612,14 @@ export class AgentLoop {
|
|
|
594
612
|
| null;
|
|
595
613
|
private toolExecutor: LoopToolExecutor | null;
|
|
596
614
|
|
|
615
|
+
/**
|
|
616
|
+
* Conversation this loop drives. Source of truth for the `conversationId`
|
|
617
|
+
* the loop's pipeline contexts and post-compaction re-injection resolve the
|
|
618
|
+
* live conversation through, so the loop knows its own identity without a
|
|
619
|
+
* threaded-in turn context.
|
|
620
|
+
*/
|
|
621
|
+
private readonly conversationId: string;
|
|
622
|
+
|
|
597
623
|
/**
|
|
598
624
|
* Loop-held compaction circuit breaker. The loop has a 1:1 lifetime with its
|
|
599
625
|
* conversation, so it is the source of truth for the cross-turn failure
|
|
@@ -603,19 +629,17 @@ export class AgentLoop {
|
|
|
603
629
|
*/
|
|
604
630
|
readonly compactionCircuit: CompactionCircuit;
|
|
605
631
|
|
|
606
|
-
constructor(
|
|
607
|
-
provider: Provider,
|
|
608
|
-
systemPrompt: string,
|
|
609
|
-
options?: AgentLoopConstructorOptions,
|
|
610
|
-
) {
|
|
632
|
+
constructor(options: AgentLoopConstructorOptions) {
|
|
611
633
|
const {
|
|
634
|
+
provider,
|
|
635
|
+
systemPrompt,
|
|
612
636
|
config,
|
|
613
637
|
tools,
|
|
614
638
|
toolExecutor,
|
|
615
639
|
resolveTools,
|
|
616
640
|
resolveSystemPrompt,
|
|
617
641
|
conversationId,
|
|
618
|
-
} = options
|
|
642
|
+
} = options;
|
|
619
643
|
this.provider = provider;
|
|
620
644
|
this.systemPrompt = systemPrompt;
|
|
621
645
|
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
@@ -623,7 +647,8 @@ export class AgentLoop {
|
|
|
623
647
|
this.resolveTools = resolveTools ?? null;
|
|
624
648
|
this.resolveSystemPrompt = resolveSystemPrompt ?? null;
|
|
625
649
|
this.toolExecutor = toolExecutor ?? null;
|
|
626
|
-
this.
|
|
650
|
+
this.conversationId = conversationId;
|
|
651
|
+
this.compactionCircuit = new CompactionCircuit(this.conversationId);
|
|
627
652
|
}
|
|
628
653
|
|
|
629
654
|
/**
|
|
@@ -675,23 +700,15 @@ export class AgentLoop {
|
|
|
675
700
|
* compaction outcome into a user-visible turn failure.
|
|
676
701
|
*/
|
|
677
702
|
private async recordCompactionOutcome(
|
|
678
|
-
|
|
703
|
+
requestId: string | undefined,
|
|
679
704
|
summaryFailed: boolean,
|
|
680
705
|
onEvent: (event: AgentEvent) => void | Promise<void>,
|
|
681
706
|
): Promise<void> {
|
|
682
707
|
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
|
-
);
|
|
708
|
+
await this.compactionCircuit.recordOutcome(summaryFailed, onEvent);
|
|
692
709
|
} catch (recordError) {
|
|
693
710
|
log.error(
|
|
694
|
-
{ err: recordError, requestId
|
|
711
|
+
{ err: recordError, requestId },
|
|
695
712
|
"Recording a compaction outcome against the circuit breaker failed; suppressing to keep the agent loop alive",
|
|
696
713
|
);
|
|
697
714
|
}
|
|
@@ -700,19 +717,22 @@ export class AgentLoop {
|
|
|
700
717
|
/**
|
|
701
718
|
* Compact the running history in place when the mid-loop budget gate trips.
|
|
702
719
|
*
|
|
703
|
-
*
|
|
704
|
-
*
|
|
705
|
-
*
|
|
706
|
-
*
|
|
707
|
-
* `exitReason = "budget"` and the orchestrator escalates.
|
|
720
|
+
* Calls the default compaction plugin on the stripped history, then
|
|
721
|
+
* re-applies injections via the supplied hooks. Returns the history to
|
|
722
|
+
* continue from, or `null` when the compactor exhausted its retry budget so
|
|
723
|
+
* the caller yields `exitReason = "budget"` and the orchestrator escalates.
|
|
708
724
|
*/
|
|
709
725
|
private async compact(
|
|
710
726
|
history: Message[],
|
|
711
|
-
|
|
712
|
-
|
|
727
|
+
requestId: string | undefined,
|
|
728
|
+
trust: TrustContext,
|
|
729
|
+
manager: ContextWindowManager | undefined,
|
|
713
730
|
signal: AbortSignal | undefined,
|
|
714
731
|
onEvent: (event: AgentEvent) => void | Promise<void>,
|
|
715
732
|
overrideProfile: string | null,
|
|
733
|
+
isNonInteractive: boolean,
|
|
734
|
+
modelProfile: string | null,
|
|
735
|
+
actorContext: InboundActorContext | null,
|
|
716
736
|
): Promise<Message[] | null> {
|
|
717
737
|
await onEvent({ type: "context_compacting" });
|
|
718
738
|
// Strip runtime injections so the compactor summarizes the raw persistent
|
|
@@ -721,49 +741,33 @@ export class AgentLoop {
|
|
|
721
741
|
// Record the history-stripped marker right after stripping, before the
|
|
722
742
|
// pipeline runs.
|
|
723
743
|
await onEvent({ type: "history_stripped" });
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
getMiddlewaresFor("compaction"),
|
|
729
|
-
(args) => defaultCompactionTerminal(args, turnContext),
|
|
730
|
-
// The mid-loop budget gate is reached only when this turn decides to
|
|
731
|
-
// compact in place, so `force` the pipeline past its auto-threshold
|
|
732
|
-
// check. `actorTrustClass` comes from the turn context (the actor whose
|
|
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,
|
|
744
|
+
if (manager == null) {
|
|
745
|
+
throw new PluginExecutionError(
|
|
746
|
+
"default-compaction: contextWindowManager run option is missing — orchestrator must supply it before invoking compaction",
|
|
747
|
+
DEFAULT_COMPACTION_PLUGIN_NAME,
|
|
747
748
|
);
|
|
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
749
|
}
|
|
756
|
-
//
|
|
757
|
-
//
|
|
758
|
-
//
|
|
759
|
-
|
|
750
|
+
// The mid-loop budget gate is reached only when this turn decides to
|
|
751
|
+
// compact in place, so `force` past the auto-threshold check.
|
|
752
|
+
// `actorTrustClass` comes from the turn's trust snapshot (the actor whose
|
|
753
|
+
// turn triggered compaction) so the compactor's image manifest excludes
|
|
754
|
+
// guardian-only attachments for untrusted actors. `overrideProfile` is the
|
|
755
|
+
// turn's resolved inference-profile override for the summary call.
|
|
756
|
+
const compactResult = await defaultCompact({
|
|
757
|
+
manager,
|
|
758
|
+
messages: rawHistory,
|
|
759
|
+
signal,
|
|
760
|
+
force: true,
|
|
761
|
+
actorTrustClass: trust.trustClass,
|
|
762
|
+
overrideProfile,
|
|
763
|
+
});
|
|
760
764
|
// `force: true` bypasses the auto-threshold gate, but early returns
|
|
761
765
|
// for "no eligible messages" / "insufficient messages" still leave
|
|
762
766
|
// `summaryFailed` undefined. Only record an outcome when the summary LLM
|
|
763
767
|
// actually ran.
|
|
764
768
|
if (compactResult.summaryFailed !== undefined) {
|
|
765
769
|
await this.recordCompactionOutcome(
|
|
766
|
-
|
|
770
|
+
requestId,
|
|
767
771
|
compactResult.summaryFailed,
|
|
768
772
|
onEvent,
|
|
769
773
|
);
|
|
@@ -783,29 +787,38 @@ export class AgentLoop {
|
|
|
783
787
|
// Re-inject onto the same base the `compaction_completed` dispatch commits:
|
|
784
788
|
// the compacted messages when the pipeline compacted, the stripped
|
|
785
789
|
// pre-compaction history otherwise.
|
|
786
|
-
|
|
790
|
+
const injection = await postCompact({
|
|
787
791
|
history: compactResult.compacted ? compactResult.messages : rawHistory,
|
|
788
|
-
|
|
792
|
+
requestId,
|
|
793
|
+
conversationId: this.conversationId,
|
|
794
|
+
trust,
|
|
795
|
+
isNonInteractive,
|
|
796
|
+
// Mid-loop re-injection always runs at full injection volume.
|
|
797
|
+
mode: "full",
|
|
798
|
+
modelProfile,
|
|
799
|
+
actorContext,
|
|
789
800
|
});
|
|
801
|
+
return injection.messages;
|
|
790
802
|
}
|
|
791
803
|
|
|
792
|
-
async run(
|
|
793
|
-
messages: Message[],
|
|
794
|
-
onEvent: (event: AgentEvent) => void | Promise<void>,
|
|
795
|
-
options?: AgentLoopRunOptions,
|
|
796
|
-
): Promise<AgentLoopRunResult> {
|
|
804
|
+
async run(options: AgentLoopRunOptions): Promise<AgentLoopRunResult> {
|
|
797
805
|
const {
|
|
806
|
+
messages,
|
|
807
|
+
onEvent,
|
|
798
808
|
signal,
|
|
799
809
|
requestId,
|
|
800
810
|
onCheckpoint,
|
|
801
811
|
callSite,
|
|
802
|
-
|
|
812
|
+
trust,
|
|
803
813
|
overrideProfile,
|
|
804
814
|
resolveOverrideProfile,
|
|
805
815
|
resolveContextWindow,
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
816
|
+
compactInPlace = false,
|
|
817
|
+
contextWindowManager,
|
|
818
|
+
isNonInteractive = false,
|
|
819
|
+
modelProfile = null,
|
|
820
|
+
actorContext = null,
|
|
821
|
+
} = options;
|
|
809
822
|
let history = [...messages];
|
|
810
823
|
// Index into `history` where this run's appended output begins. It starts
|
|
811
824
|
// after the input and resets to the compacted base whenever the loop
|
|
@@ -818,6 +831,13 @@ export class AgentLoop {
|
|
|
818
831
|
let lastLlmCallTime = 0;
|
|
819
832
|
let exitReason: ExitReason | null = null;
|
|
820
833
|
let appendedNewMessages = false;
|
|
834
|
+
// Armed at the end of a tool-use iteration so the budget gate runs at the
|
|
835
|
+
// top of the NEXT iteration — before that iteration's provider call —
|
|
836
|
+
// instead of after the current one. Stop-hook re-query continues re-enter
|
|
837
|
+
// without arming, so the gate fires on exactly the same occasions as the
|
|
838
|
+
// prior post-call placement, plus the first call when
|
|
839
|
+
// `compactInPlace` is set (the primary run's turn-start compaction).
|
|
840
|
+
let budgetGateArmed = compactInPlace;
|
|
821
841
|
const rlog = requestId ? log.child({ requestId }) : log;
|
|
822
842
|
|
|
823
843
|
// Resolve the inference-profile override that applies right now. The
|
|
@@ -858,6 +878,105 @@ export class AgentLoop {
|
|
|
858
878
|
let toolUseBlocks: Extract<ContentBlock, { type: "tool_use" }>[] = [];
|
|
859
879
|
|
|
860
880
|
try {
|
|
881
|
+
// ── Pre-call budget gate ─────────────────────────────────────
|
|
882
|
+
// When overflow recovery is enabled, estimate the running context
|
|
883
|
+
// size as it approaches the preflight budget before issuing the
|
|
884
|
+
// provider call. With `compactInPlace` the loop compacts in place and
|
|
885
|
+
// proceeds with the call; otherwise it yields (`exitReason =
|
|
886
|
+
// "budget"`) so the orchestrator can recover before the call risks a
|
|
887
|
+
// hard context-too-large rejection. Keyed off the loop's own
|
|
888
|
+
// `history.length` (the messages actually in context this turn,
|
|
889
|
+
// including tool iterations) rather than the durable conversation
|
|
890
|
+
// count.
|
|
891
|
+
//
|
|
892
|
+
// Armed after each tool-use iteration; stop-hook re-query continues
|
|
893
|
+
// skip it. The first call runs it only when `compactInPlace` is set,
|
|
894
|
+
// where it stands in for the orchestrator's turn-start compaction: it
|
|
895
|
+
// honors the compaction circuit breaker and proceeds with the call
|
|
896
|
+
// rather than yielding, since there is no prior turn output to
|
|
897
|
+
// escalate.
|
|
898
|
+
if (budgetGateArmed) {
|
|
899
|
+
budgetGateArmed = false;
|
|
900
|
+
// The gate only re-arms after a completed tool-use iteration
|
|
901
|
+
// (`toolUseTurns` is incremented first), so reaching it with
|
|
902
|
+
// `toolUseTurns === 0` uniquely identifies the first-call pass: it
|
|
903
|
+
// compacts-or-proceeds (never yields) and honors the compaction
|
|
904
|
+
// circuit breaker, matching the orchestrator's turn-start compaction.
|
|
905
|
+
const isFirstCallGate = toolUseTurns === 0;
|
|
906
|
+
const contextWindow = resolveContextWindow?.();
|
|
907
|
+
if (contextWindow?.overflowRecovery.enabled) {
|
|
908
|
+
const { maxInputTokens, overflowRecovery } = contextWindow;
|
|
909
|
+
const safetyMargin =
|
|
910
|
+
history.length > LONG_HISTORY_MESSAGE_THRESHOLD
|
|
911
|
+
? Math.max(
|
|
912
|
+
overflowRecovery.safetyMarginRatio,
|
|
913
|
+
LONG_HISTORY_SAFETY_MARGIN_FLOOR,
|
|
914
|
+
)
|
|
915
|
+
: overflowRecovery.safetyMarginRatio;
|
|
916
|
+
const preflightBudget = Math.floor(
|
|
917
|
+
maxInputTokens * (1 - safetyMargin),
|
|
918
|
+
);
|
|
919
|
+
const midLoopThreshold =
|
|
920
|
+
preflightBudget * MID_LOOP_YIELD_THRESHOLD_RATIO;
|
|
921
|
+
const estimated = this.estimateTokens(history);
|
|
922
|
+
if (estimated > midLoopThreshold) {
|
|
923
|
+
let compactedInPlace = false;
|
|
924
|
+
// The turn-start pass skips compaction while the circuit breaker
|
|
925
|
+
// is open so a run of failed summaries doesn't keep hammering the
|
|
926
|
+
// summary LLM; mid-loop compaction is force-driven and proceeds
|
|
927
|
+
// regardless (it has already committed to compacting in place).
|
|
928
|
+
const compactionAllowed =
|
|
929
|
+
!isFirstCallGate || !(await this.compactionCircuit.isOpen());
|
|
930
|
+
if (compactInPlace && compactionAllowed) {
|
|
931
|
+
rlog.info(
|
|
932
|
+
{
|
|
933
|
+
turn: toolUseTurns,
|
|
934
|
+
estimated,
|
|
935
|
+
threshold: midLoopThreshold,
|
|
936
|
+
},
|
|
937
|
+
"Token estimate approaching budget — compacting in place",
|
|
938
|
+
);
|
|
939
|
+
const compacted = await this.compact(
|
|
940
|
+
history,
|
|
941
|
+
requestId,
|
|
942
|
+
trust,
|
|
943
|
+
contextWindowManager,
|
|
944
|
+
signal,
|
|
945
|
+
onEvent,
|
|
946
|
+
resolveEffectiveOverrideProfile() ?? null,
|
|
947
|
+
isNonInteractive,
|
|
948
|
+
modelProfile,
|
|
949
|
+
actorContext,
|
|
950
|
+
);
|
|
951
|
+
if (compacted) {
|
|
952
|
+
history = compacted;
|
|
953
|
+
// The compacted, re-injected array is the new base; output
|
|
954
|
+
// produced after this point is what the orchestrator
|
|
955
|
+
// persists.
|
|
956
|
+
newMessagesStart = history.length;
|
|
957
|
+
compactedInPlace = true;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
// The turn-start gate proceeds with the call whether or not it
|
|
961
|
+
// compacted (preflight-overflow recovery and the convergence loop
|
|
962
|
+
// remain the escalation path); only mid-loop re-entries yield to
|
|
963
|
+
// the orchestrator before the call.
|
|
964
|
+
if (!compactedInPlace && !isFirstCallGate) {
|
|
965
|
+
rlog.warn(
|
|
966
|
+
{
|
|
967
|
+
turn: toolUseTurns,
|
|
968
|
+
estimated,
|
|
969
|
+
threshold: midLoopThreshold,
|
|
970
|
+
},
|
|
971
|
+
"Token estimate approaching budget — yielding for compaction",
|
|
972
|
+
);
|
|
973
|
+
exitReason = "budget";
|
|
974
|
+
break;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
861
980
|
// Resolve tools for this turn: use the dynamic resolver if provided,
|
|
862
981
|
// otherwise fall back to the static tool list.
|
|
863
982
|
const currentTools = this.resolveTools
|
|
@@ -921,12 +1040,14 @@ export class AgentLoop {
|
|
|
921
1040
|
providerConfig.cacheTtl = this.config.cacheTtl;
|
|
922
1041
|
}
|
|
923
1042
|
|
|
924
|
-
// Cache-anchor signal for volatile latest-user-message turns
|
|
925
|
-
// memory-v3 injects
|
|
926
|
-
//
|
|
927
|
-
//
|
|
928
|
-
//
|
|
929
|
-
|
|
1043
|
+
// Cache-anchor signal for volatile latest-user-message turns: when
|
|
1044
|
+
// memory-v3 is live it injects a per-turn `<memory>` block into the
|
|
1045
|
+
// latest user message, so the provider must anchor its long-TTL cache
|
|
1046
|
+
// breakpoint on the most recent STABLE user message instead of the
|
|
1047
|
+
// volatile latest one. Read here alongside the rest of the provider
|
|
1048
|
+
// config; only set when true so the wire/config stays byte-identical
|
|
1049
|
+
// when off.
|
|
1050
|
+
if (isAssistantFeatureFlagEnabled("memory-v3-live", getConfig())) {
|
|
930
1051
|
providerConfig.mutableLatestUserMessage = true;
|
|
931
1052
|
}
|
|
932
1053
|
|
|
@@ -941,14 +1062,14 @@ export class AgentLoop {
|
|
|
941
1062
|
providerConfig.callSite = callSite;
|
|
942
1063
|
providerConfig.usageTracking = "manual";
|
|
943
1064
|
// Per-conversation seed for deterministic `mix`-profile expansion.
|
|
944
|
-
// Sourced from the
|
|
945
|
-
//
|
|
946
|
-
//
|
|
947
|
-
//
|
|
948
|
-
//
|
|
949
|
-
//
|
|
950
|
-
if (
|
|
951
|
-
providerConfig.selectionSeed =
|
|
1065
|
+
// Sourced from the loop's own conversation id so every LLM call in a
|
|
1066
|
+
// conversation resolves the same mix arm (stable across turns and
|
|
1067
|
+
// retries, and across daemon restarts since the seed is the durable
|
|
1068
|
+
// conversation id). Absent for standalone `AgentLoop` instances
|
|
1069
|
+
// (unit tests constructed without a conversation id) — those fall
|
|
1070
|
+
// back to per-call random mix selection.
|
|
1071
|
+
if (this.conversationId) {
|
|
1072
|
+
providerConfig.selectionSeed = this.conversationId;
|
|
952
1073
|
}
|
|
953
1074
|
}
|
|
954
1075
|
|
|
@@ -1004,6 +1125,12 @@ export class AgentLoop {
|
|
|
1004
1125
|
stripOldMediaBlocks(history),
|
|
1005
1126
|
);
|
|
1006
1127
|
|
|
1128
|
+
// A `pre-model-call` hook (below) can defer this turn's assistant
|
|
1129
|
+
// output; when set, the live text stream is held so an
|
|
1130
|
+
// `assistant-message` hook can emit the finalized (transformed) text
|
|
1131
|
+
// instead. Reset per model call.
|
|
1132
|
+
let deferAssistantOutput = false;
|
|
1133
|
+
|
|
1007
1134
|
// The `onEvent` wrapping below applies sensitive-output placeholder
|
|
1008
1135
|
// substitution to streamed text while forwarding every other event
|
|
1009
1136
|
// type through unchanged.
|
|
@@ -1013,6 +1140,9 @@ export class AgentLoop {
|
|
|
1013
1140
|
config: providerConfig,
|
|
1014
1141
|
onEvent: (event) => {
|
|
1015
1142
|
if (event.type === "text_delta") {
|
|
1143
|
+
// Held when the turn's output is deferred — the final text is
|
|
1144
|
+
// emitted once, after the `assistant-message` hook runs.
|
|
1145
|
+
if (deferAssistantOutput) return;
|
|
1016
1146
|
// Apply sensitive-output placeholder substitution (chunk-safe)
|
|
1017
1147
|
if (substitutionMap.size > 0) {
|
|
1018
1148
|
const combined = streamingPending + event.text;
|
|
@@ -1068,12 +1198,32 @@ export class AgentLoop {
|
|
|
1068
1198
|
signal,
|
|
1069
1199
|
};
|
|
1070
1200
|
|
|
1071
|
-
//
|
|
1072
|
-
//
|
|
1073
|
-
//
|
|
1074
|
-
//
|
|
1075
|
-
|
|
1076
|
-
|
|
1201
|
+
// Let plugins edit the outbound request and opt this call into deferred
|
|
1202
|
+
// output streaming. Runs for every provider call; hooks self-gate on
|
|
1203
|
+
// call site / conversation. Fail-open: a throwing hook leaves the
|
|
1204
|
+
// request unchanged and streaming live.
|
|
1205
|
+
try {
|
|
1206
|
+
const preModelCtx: PreModelCallContext = {
|
|
1207
|
+
conversationId: this.conversationId,
|
|
1208
|
+
callSite,
|
|
1209
|
+
systemPrompt: providerOptions.systemPrompt,
|
|
1210
|
+
deferAssistantOutput: false,
|
|
1211
|
+
logger: rlog,
|
|
1212
|
+
};
|
|
1213
|
+
const finalPreModelCtx = await runHook(
|
|
1214
|
+
HOOKS.PRE_MODEL_CALL,
|
|
1215
|
+
preModelCtx,
|
|
1216
|
+
);
|
|
1217
|
+
providerOptions.systemPrompt = finalPreModelCtx.systemPrompt;
|
|
1218
|
+
// The hook owns the policy (it sees `callSite`/conversation and
|
|
1219
|
+
// self-gates); the loop honors whatever it decides.
|
|
1220
|
+
deferAssistantOutput = finalPreModelCtx.deferAssistantOutput;
|
|
1221
|
+
} catch (preModelCallError) {
|
|
1222
|
+
rlog.error(
|
|
1223
|
+
{ err: preModelCallError },
|
|
1224
|
+
"pre-model-call hook failed — proceeding with the original request",
|
|
1225
|
+
);
|
|
1226
|
+
}
|
|
1077
1227
|
|
|
1078
1228
|
// Announce the LLM-call boundary so downstream handlers (the
|
|
1079
1229
|
// daemon's persistence pipeline) can reserve an empty assistant row
|
|
@@ -1161,12 +1311,52 @@ export class AgentLoop {
|
|
|
1161
1311
|
streamingPending = "";
|
|
1162
1312
|
}
|
|
1163
1313
|
|
|
1314
|
+
// Run the `assistant-message` hook on a finalized message and, when
|
|
1315
|
+
// output was deferred, emit the finalized text once (with sensitive-output
|
|
1316
|
+
// substitution applied, matching the live stream). Fail-open: the hook
|
|
1317
|
+
// receives a clone, so a throw — even mid in-place mutation — leaves the
|
|
1318
|
+
// original message intact.
|
|
1319
|
+
const finalizeAssistantMessage = async (
|
|
1320
|
+
message: Message,
|
|
1321
|
+
): Promise<Message> => {
|
|
1322
|
+
let finalized = message;
|
|
1323
|
+
try {
|
|
1324
|
+
const ctx: AssistantMessageContext = {
|
|
1325
|
+
conversationId: this.conversationId,
|
|
1326
|
+
callSite,
|
|
1327
|
+
content: structuredClone(message.content),
|
|
1328
|
+
stopReason: response.stopReason,
|
|
1329
|
+
logger: rlog,
|
|
1330
|
+
};
|
|
1331
|
+
const result = await runHook(HOOKS.ASSISTANT_MESSAGE, ctx);
|
|
1332
|
+
finalized = { role: "assistant", content: result.content };
|
|
1333
|
+
} catch (assistantMessageError) {
|
|
1334
|
+
rlog.error(
|
|
1335
|
+
{ err: assistantMessageError },
|
|
1336
|
+
"assistant-message hook failed — keeping the original content",
|
|
1337
|
+
);
|
|
1338
|
+
finalized = message;
|
|
1339
|
+
}
|
|
1340
|
+
if (deferAssistantOutput) {
|
|
1341
|
+
// The persisted message keeps sensitive-output placeholders; the
|
|
1342
|
+
// stream shows real values — substitute before emitting.
|
|
1343
|
+
const finalText = applySubstitutions(
|
|
1344
|
+
assistantTextOf(finalized.content),
|
|
1345
|
+
substitutionMap,
|
|
1346
|
+
);
|
|
1347
|
+
if (finalText.length > 0) {
|
|
1348
|
+
onEvent({ type: "text_delta", text: finalText });
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
return finalized;
|
|
1352
|
+
};
|
|
1353
|
+
|
|
1164
1354
|
// Build the assistant message with placeholder-only text.
|
|
1165
1355
|
// Both provider history and persisted conversation store must retain
|
|
1166
1356
|
// placeholders so the model never sees real sensitive values — neither
|
|
1167
1357
|
// on subsequent loop turns nor on session reload from the database.
|
|
1168
1358
|
// Substitution to real values happens only in streamed text_delta events.
|
|
1169
|
-
|
|
1359
|
+
let assistantMessage: Message = {
|
|
1170
1360
|
role: "assistant",
|
|
1171
1361
|
content: response.content,
|
|
1172
1362
|
};
|
|
@@ -1195,10 +1385,6 @@ export class AgentLoop {
|
|
|
1195
1385
|
block.type !== "server_tool_use" &&
|
|
1196
1386
|
block.type !== "web_search_tool_result",
|
|
1197
1387
|
);
|
|
1198
|
-
const safeAssistantMessage: Message = {
|
|
1199
|
-
role: "assistant",
|
|
1200
|
-
content: safeContent,
|
|
1201
|
-
};
|
|
1202
1388
|
rlog.warn(
|
|
1203
1389
|
{
|
|
1204
1390
|
turn: toolUseTurns,
|
|
@@ -1209,6 +1395,13 @@ export class AgentLoop {
|
|
|
1209
1395
|
},
|
|
1210
1396
|
"LLM response reached output token limit",
|
|
1211
1397
|
);
|
|
1398
|
+
// Run the hook on the truncated reply so output-filter plugins still
|
|
1399
|
+
// see it, and so a deferred turn gets its synthetic final emit (the
|
|
1400
|
+
// live stream was suppressed; without this the client would see nothing).
|
|
1401
|
+
const safeAssistantMessage = await finalizeAssistantMessage({
|
|
1402
|
+
role: "assistant",
|
|
1403
|
+
content: safeContent,
|
|
1404
|
+
});
|
|
1212
1405
|
history.push(safeAssistantMessage);
|
|
1213
1406
|
appendedNewMessages = true;
|
|
1214
1407
|
await onEvent({
|
|
@@ -1241,7 +1434,7 @@ export class AgentLoop {
|
|
|
1241
1434
|
// follow-up turn. It receives the full history and, when it asks to
|
|
1242
1435
|
// continue, appends the follow-up turn itself.
|
|
1243
1436
|
const stopCtx: StopContext = {
|
|
1244
|
-
conversationId:
|
|
1437
|
+
conversationId: this.conversationId,
|
|
1245
1438
|
messages: [...history],
|
|
1246
1439
|
responseContent: response.content,
|
|
1247
1440
|
stopReason: response.stopReason,
|
|
@@ -1280,6 +1473,12 @@ export class AgentLoop {
|
|
|
1280
1473
|
}
|
|
1281
1474
|
}
|
|
1282
1475
|
|
|
1476
|
+
// Run the `assistant-message` hook + emit any deferred final text.
|
|
1477
|
+
// On a no-tool turn this point is reached only after the `stop` hook
|
|
1478
|
+
// resolves to "stop" (a `continue` already re-queried above), so a
|
|
1479
|
+
// re-queried reply is never transformed-then-discarded.
|
|
1480
|
+
assistantMessage = await finalizeAssistantMessage(assistantMessage);
|
|
1481
|
+
|
|
1283
1482
|
history.push(assistantMessage);
|
|
1284
1483
|
appendedNewMessages = true;
|
|
1285
1484
|
|
|
@@ -1430,7 +1629,7 @@ export class AgentLoop {
|
|
|
1430
1629
|
continue;
|
|
1431
1630
|
}
|
|
1432
1631
|
const postToolUseCtx: PostToolUseContext = {
|
|
1433
|
-
conversationId:
|
|
1632
|
+
conversationId: this.conversationId,
|
|
1434
1633
|
toolResponse: block as ToolResultContent,
|
|
1435
1634
|
messages: history,
|
|
1436
1635
|
maxInputTokens: contextWindowTokens,
|
|
@@ -1508,8 +1707,9 @@ export class AgentLoop {
|
|
|
1508
1707
|
history.push({ role: "user", content: resultBlocks });
|
|
1509
1708
|
|
|
1510
1709
|
// Invoke checkpoint callback after tool results are in history.
|
|
1511
|
-
// Handoff
|
|
1512
|
-
//
|
|
1710
|
+
// Handoff takes precedence over the budget gate: a handoff decision
|
|
1711
|
+
// breaks here and leaves `budgetGateArmed` false, so a queued message
|
|
1712
|
+
// is processed before the next iteration's pre-call budget gate.
|
|
1513
1713
|
if (onCheckpoint) {
|
|
1514
1714
|
const decision = await onCheckpoint({
|
|
1515
1715
|
turnIndex: toolUseTurns - 1, // 0-based (toolUseTurns was already incremented)
|
|
@@ -1523,61 +1723,11 @@ export class AgentLoop {
|
|
|
1523
1723
|
}
|
|
1524
1724
|
}
|
|
1525
1725
|
|
|
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
|
-
}
|
|
1726
|
+
// Arm the pre-call budget gate for the next iteration. Placed after
|
|
1727
|
+
// the checkpoint so a handoff yield (which breaks above) leaves it
|
|
1728
|
+
// disarmed; the gate then runs at the top of the next iteration,
|
|
1729
|
+
// before that iteration's provider call.
|
|
1730
|
+
budgetGateArmed = true;
|
|
1581
1731
|
} catch (error) {
|
|
1582
1732
|
// Abort errors are expected when user cancels — synthesize
|
|
1583
1733
|
// cancellation tool_results so the history stays valid for the
|