@vellumai/assistant 0.8.8 → 0.8.9-dev.202606091853.fbaa2ae
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +6 -6
- package/bun.lock +2 -2
- package/docs/activation-funnel-telemetry.md +310 -0
- package/examples/plugins/echo/README.md +61 -60
- package/examples/plugins/echo/hooks/post-tool-use.ts +18 -0
- package/examples/plugins/echo/hooks/stop.ts +16 -0
- package/examples/plugins/echo/hooks/user-prompt-submit.ts +18 -0
- package/examples/plugins/echo/package.json +1 -2
- package/examples/plugins/echo/src/emit.ts +19 -0
- package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +7 -6
- package/openapi.yaml +263 -121
- package/package.json +2 -2
- package/src/__tests__/activation-early-marking.test.ts +120 -0
- package/src/__tests__/agent-loop-callsite-precedence.test.ts +69 -14
- package/src/__tests__/agent-loop-exit-reason.test.ts +204 -144
- package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +50 -35
- package/src/__tests__/agent-loop-output-hooks.test.ts +357 -0
- package/src/__tests__/agent-loop-override-profile.test.ts +25 -6
- package/src/__tests__/agent-loop-provider-error-recording.test.ts +41 -21
- package/src/__tests__/agent-loop-thinking.test.ts +36 -20
- package/src/__tests__/agent-loop.test.ts +441 -96
- package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +14 -14
- package/src/__tests__/agent-wake-override-profile.test.ts +17 -21
- package/src/__tests__/anthropic-provider.test.ts +23 -8
- package/src/__tests__/app-builder-tool-scripts.test.ts +21 -0
- package/src/__tests__/app-control-flow.test.ts +1 -1
- package/src/__tests__/app-dir-path-guard.test.ts +1 -0
- package/src/__tests__/app-executors.test.ts +132 -0
- package/src/__tests__/approval-cascade.test.ts +6 -5
- package/src/__tests__/approval-routes-http.test.ts +4 -1
- package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
- package/src/__tests__/channel-approval-routes.test.ts +1 -1
- package/src/__tests__/channel-approvals.test.ts +1 -1
- package/src/__tests__/compaction-circuit.test.ts +258 -0
- package/src/__tests__/compaction-direct.test.ts +146 -0
- package/src/__tests__/compaction-events.test.ts +7 -7
- package/src/__tests__/compaction.benchmark.test.ts +1 -1
- package/src/__tests__/compactor-web-search-strip.test.ts +213 -0
- package/src/__tests__/context-overflow-reducer.test.ts +265 -125
- package/src/__tests__/context-window-manager-compact-retry.test.ts +121 -15
- package/src/__tests__/conversation-abort-tool-results.test.ts +7 -5
- package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +7 -10
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +78 -119
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +158 -234
- package/src/__tests__/conversation-agent-loop.test.ts +342 -623
- package/src/__tests__/conversation-clean-command.test.ts +5 -2
- package/src/__tests__/conversation-confirmation-signals.test.ts +6 -5
- package/src/__tests__/conversation-crud-inference-profile.test.ts +7 -9
- package/src/__tests__/conversation-error.test.ts +15 -1
- package/src/__tests__/conversation-history-web-search.test.ts +6 -1
- package/src/__tests__/conversation-media-retry.test.ts +1 -1
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +44 -4
- package/src/__tests__/conversation-process-callsite.test.ts +15 -15
- package/src/__tests__/conversation-provider-retry-repair.test.ts +66 -65
- package/src/__tests__/conversation-queue.test.ts +10 -10
- package/src/__tests__/conversation-runtime-assembly.test.ts +1059 -231
- package/src/__tests__/conversation-runtime-workspace.test.ts +115 -20
- package/src/__tests__/conversation-slash-queue.test.ts +7 -5
- package/src/__tests__/conversation-slash-unknown.test.ts +7 -5
- package/src/__tests__/conversation-speed-override.test.ts +11 -10
- package/src/__tests__/conversation-starter-routes.test.ts +14 -6
- package/src/__tests__/conversation-surfaces-activation-emit.test.ts +395 -0
- package/src/__tests__/conversation-surfaces-app-control.test.ts +48 -1
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +73 -4
- package/src/__tests__/conversation-undo.test.ts +2 -2
- package/src/__tests__/conversation-workspace-cache-state.test.ts +24 -21
- package/src/__tests__/conversation-workspace-injection.test.ts +69 -7
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +15 -12
- package/src/__tests__/conversations-import-system-filter.test.ts +101 -0
- package/src/__tests__/corrected-target.test.ts +93 -0
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/cu-unified-flow.test.ts +36 -0
- package/src/__tests__/db-acp-history.test.ts +101 -0
- package/src/__tests__/dynamic-page-surface.test.ts +31 -0
- package/src/__tests__/empty-response-hook.test.ts +1 -1
- package/src/__tests__/file-write-tool.test.ts +63 -0
- package/src/__tests__/gateway-only-guard.test.ts +17 -2
- package/src/__tests__/guardian-grant-minting.test.ts +1 -1
- package/src/__tests__/guardian-routing-invariants.test.ts +93 -4
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +1 -1
- package/src/__tests__/heartbeat-disk-pressure.test.ts +1 -0
- package/src/__tests__/heartbeat-service.test.ts +1 -0
- package/src/__tests__/history-repair-hook.test.ts +5 -1
- package/src/__tests__/host-app-control-proxy.test.ts +45 -0
- package/src/__tests__/host-app-control-routes.test.ts +1 -1
- package/src/__tests__/host-cu-routes-targeted.test.ts +3 -3
- package/src/__tests__/inference-profile-reaper.test.ts +62 -0
- package/src/__tests__/inference-profile-session-handler.test.ts +86 -0
- package/src/__tests__/injector-background-turn.test.ts +13 -23
- package/src/__tests__/injector-chain.test.ts +268 -44
- package/src/__tests__/injector-disk-pressure.test.ts +210 -52
- package/src/__tests__/injector-document-comments.test.ts +97 -114
- package/src/__tests__/injector-pkb-v2-silenced.test.ts +2 -2
- package/src/__tests__/injector-v3-suppression.test.ts +4 -4
- package/src/__tests__/list-messages-client-message-id.test.ts +91 -0
- package/src/__tests__/list-messages-hidden-metadata.test.ts +38 -0
- package/src/__tests__/llm-resolver.test.ts +73 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +156 -38
- package/src/__tests__/memory-v2-static-injector.test.ts +86 -8
- package/src/__tests__/notification-decision-strategy.test.ts +3 -3
- package/src/__tests__/parallel-tool.benchmark.test.ts +35 -8
- package/src/__tests__/persist-unsendable-image-downscale.test.ts +145 -0
- package/src/__tests__/persist-unsendable-image.test.ts +97 -1
- package/src/__tests__/plugin-api-shim.test.ts +6 -9
- package/src/__tests__/plugin-bootstrap.test.ts +91 -81
- package/src/__tests__/plugin-registry.test.ts +3 -49
- package/src/__tests__/plugin-skill-contribution.test.ts +15 -14
- package/src/__tests__/plugin-tool-contribution.test.ts +7 -4
- package/src/__tests__/plugin-types.test.ts +0 -70
- package/src/__tests__/post-turn-tool-result-truncation.test.ts +69 -0
- package/src/__tests__/pre-model-call-sanitize.test.ts +109 -0
- package/src/__tests__/published-app-updater.test.ts +138 -0
- package/src/__tests__/reaction-persistence.test.ts +1 -1
- package/src/__tests__/send-endpoint-busy.test.ts +4 -1
- package/src/__tests__/skill-feature-flags-integration.test.ts +31 -0
- package/src/__tests__/steer-tool-repair.test.ts +1 -1
- package/src/__tests__/subagent-call-site-routing.test.ts +1 -1
- package/src/__tests__/subagent-detail.test.ts +25 -7
- package/src/__tests__/subagent-fork-notifications.test.ts +1 -3
- package/src/__tests__/subagent-fork-spawn.test.ts +1 -1
- package/src/__tests__/subagent-manager-notify.test.ts +1 -3
- package/src/__tests__/subagent-notify-parent.test.ts +1 -3
- package/src/__tests__/subagent-spawn-tool-fork.test.ts +1 -1
- package/src/__tests__/title-generate-hook.test.ts +5 -1
- package/src/__tests__/tool-error-hook.test.ts +1 -1
- package/src/__tests__/tool-result-truncate-hook.test.ts +1 -1
- package/src/__tests__/user-plugin-loader.test.ts +54 -286
- package/src/__tests__/web-fetch.test.ts +45 -0
- package/src/acp/__tests__/agent-process.test.ts +161 -0
- package/src/acp/__tests__/client-handler.test.ts +40 -0
- package/src/acp/__tests__/helpers/acp-config-stub.ts +0 -2
- package/src/acp/__tests__/helpers/acp-history-db.ts +82 -0
- package/src/acp/__tests__/helpers/exec-file-stub.ts +106 -0
- package/src/acp/__tests__/prepare-agent-env.test.ts +97 -0
- package/src/acp/__tests__/session-manager-persistence.test.ts +95 -28
- package/src/acp/__tests__/session-manager-resume.test.ts +888 -0
- package/src/acp/agent-process.ts +61 -1
- package/src/acp/auto-install.test.ts +280 -0
- package/src/acp/auto-install.ts +232 -0
- package/src/acp/client-handler.ts +31 -0
- package/src/acp/prepare-agent-env.ts +80 -27
- package/src/acp/resolve-agent.test.ts +188 -28
- package/src/acp/resolve-agent.ts +119 -42
- package/src/acp/resume-hint.ts +23 -0
- package/src/acp/session-manager.ts +507 -73
- package/src/agent/compaction-circuit.ts +60 -102
- package/src/agent/loop.ts +403 -251
- package/src/api/responses/conversation-message.ts +22 -2
- package/src/api/responses/memory-v3-selection-log.ts +19 -10
- package/src/approvals/guardian-request-resolvers.ts +27 -1
- package/src/background-wake/next-wake.ts +1 -0
- package/src/cli/commands/__tests__/memory-v3.test.ts +191 -210
- package/src/cli/commands/channel-verification-sessions.ts +6 -6
- package/src/cli/commands/db/__tests__/repair.test.ts +3 -1
- package/src/cli/commands/memory-v3.ts +57 -199
- package/src/cli/commands/plugins.ts +43 -37
- package/src/cli/lib/__tests__/install-from-github.test.ts +790 -111
- package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +196 -0
- package/src/cli/lib/__tests__/plugin-details.test.ts +381 -0
- package/src/cli/lib/__tests__/plugin-marketplace.test.ts +270 -0
- package/src/cli/lib/__tests__/search-plugins.test.ts +233 -32
- package/src/cli/lib/install-from-github.ts +797 -60
- package/src/cli/lib/plugin-catalog-cache.ts +84 -0
- package/src/cli/lib/plugin-details.ts +416 -0
- package/src/cli/lib/plugin-marketplace.ts +215 -0
- package/src/cli/lib/search-plugins.ts +201 -29
- package/src/config/__tests__/feature-flag-registry-guard.test.ts +2 -2
- package/src/config/acp-defaults.test.ts +10 -0
- package/src/config/acp-defaults.ts +9 -3
- package/src/config/acp-schema.ts +1 -7
- package/src/config/bundled-skills/acp/SKILL.md +80 -41
- package/src/config/bundled-skills/acp/TOOLS.json +4 -4
- package/src/config/bundled-skills/app-builder/SKILL.md +224 -381
- package/src/config/bundled-skills/app-builder/TOOLS.json +72 -2
- package/src/config/bundled-skills/app-builder/references/DESIGN_SYSTEM.md +48 -0
- package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +57 -0
- package/src/config/bundled-skills/app-builder/references/SLIDES.md +38 -0
- package/src/config/bundled-skills/app-builder/tools/app-list.ts +62 -0
- package/src/config/bundled-skills/app-builder/tools/app-update.ts +18 -0
- package/src/config/bundled-skills/document-editor/SKILL.md +28 -23
- package/src/config/bundled-skills/document-editor/TOOLS.json +1 -1
- package/src/config/bundled-tool-registry.ts +4 -0
- package/src/config/call-site-defaults.ts +0 -3
- package/src/config/feature-flag-registry.json +10 -16
- package/src/config/llm-resolver.ts +39 -7
- package/src/config/schemas/__tests__/memory-v3.test.ts +25 -9
- package/src/config/schemas/call-site-catalog.ts +0 -21
- package/src/config/schemas/heartbeat.ts +9 -0
- package/src/config/schemas/llm.ts +17 -3
- package/src/config/schemas/memory-v3.ts +58 -8
- package/src/config/seed-inference-profiles.ts +18 -0
- package/src/context/compactor.ts +22 -4
- package/src/context/post-turn-tool-result-truncation.ts +39 -1
- package/src/context/strip-injections.ts +8 -2
- package/src/daemon/conversation-agent-loop-handlers.ts +18 -36
- package/src/daemon/conversation-agent-loop.ts +308 -1297
- package/src/daemon/conversation-error.ts +31 -4
- package/src/daemon/conversation-history.ts +1 -1
- package/src/daemon/conversation-lifecycle.ts +11 -255
- package/src/daemon/conversation-media-retry.ts +19 -6
- package/src/daemon/conversation-messaging.ts +17 -0
- package/src/daemon/conversation-process.ts +22 -141
- package/src/daemon/conversation-queue-manager.ts +8 -0
- package/src/daemon/conversation-registry.ts +159 -0
- package/src/daemon/conversation-runtime-assembly.ts +361 -393
- package/src/daemon/conversation-store.ts +9 -90
- package/src/daemon/conversation-surfaces.ts +165 -11
- package/src/daemon/conversation-workspace.ts +17 -0
- package/src/daemon/conversation.ts +444 -62
- package/src/daemon/disk-pressure-policy.ts +0 -1
- package/src/daemon/external-plugins-bootstrap.ts +36 -46
- package/src/daemon/handlers/config-channels.ts +11 -2
- package/src/daemon/handlers/conversations.ts +3 -1
- package/src/daemon/handlers/skills.ts +4 -1
- package/src/daemon/host-app-control-proxy.ts +72 -57
- package/src/daemon/host-proxy-preactivation.ts +1 -3
- package/src/daemon/lifecycle.ts +21 -7
- package/src/daemon/persist-unsendable-image.ts +62 -25
- package/src/daemon/process-message.ts +1 -1
- package/src/daemon/server.ts +2 -0
- package/src/daemon/tool-side-effects.ts +15 -0
- package/src/daemon/wake-conversation-ops.ts +269 -0
- package/src/embedded/plugin-api.ts +2 -2
- package/src/export/__tests__/transcript-formatter.test.ts +5 -0
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +3 -0
- package/src/heartbeat/heartbeat-run-store.ts +23 -1
- package/src/heartbeat/heartbeat-service.ts +26 -0
- package/src/ipc/__tests__/browser-ipc.test.ts +1 -1
- package/src/ipc/__tests__/ui-request-route.test.ts +3 -3
- package/src/ipc/skill-routes/__tests__/memory.test.ts +15 -0
- package/src/ipc/skill-routes/memory.ts +4 -2
- package/src/memory/__tests__/activation-session-store.test.ts +41 -0
- package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +87 -0
- package/src/memory/__tests__/onboarding-events-store.test.ts +80 -0
- package/src/memory/activation-session-store.ts +43 -0
- package/src/memory/conversation-crud.ts +29 -19
- package/src/memory/conversation-starter-checkpoints.ts +1 -0
- package/src/memory/db-init.ts +6 -0
- package/src/memory/job-handlers/conversation-starters.ts +13 -2
- package/src/memory/jobs/__tests__/embed-concept-page.test.ts +5 -4
- package/src/memory/jobs-worker.ts +25 -1
- package/src/memory/migrations/272-acp-session-history-cwd.ts +36 -0
- package/src/memory/migrations/273-onboarding-events-funnel-columns.ts +46 -0
- package/src/memory/migrations/274-create-activation-sessions.ts +15 -0
- package/src/memory/migrations/index.ts +3 -0
- package/src/memory/onboarding-events-store.ts +66 -18
- package/src/memory/schema/acp.ts +4 -0
- package/src/memory/schema/infrastructure.ts +13 -0
- package/src/memory/v2/__tests__/consolidation-job.test.ts +4 -97
- package/src/memory/v2/__tests__/page-store.test.ts +22 -0
- package/src/memory/v2/consolidation-job.ts +2 -63
- package/src/memory/v2/types.ts +5 -0
- package/src/messaging/providers/telegram-bot/api.ts +14 -5
- package/src/notifications/__tests__/copy-composer.test.ts +244 -0
- package/src/notifications/access-request-copy.ts +298 -0
- package/src/notifications/adapters/slack.ts +3 -3
- package/src/notifications/adapters/telegram.ts +9 -2
- package/src/notifications/copy-composer.ts +49 -267
- package/src/notifications/decision-engine.ts +16 -35
- package/src/notifications/home-feed-side-effect.ts +1 -6
- package/src/plugin-api/constants.ts +4 -0
- package/src/plugin-api/index.ts +40 -11
- package/src/plugin-api/types.ts +108 -0
- package/src/plugins/defaults/compaction/compact.ts +106 -0
- package/src/{daemon → plugins/defaults/compaction}/context-overflow-reducer.ts +250 -42
- package/src/plugins/defaults/compaction/corrected-target.ts +53 -0
- package/src/plugins/defaults/compaction/manager-store.ts +57 -0
- package/src/plugins/defaults/compaction/package.json +1 -2
- package/src/{context → plugins/defaults/compaction}/window-manager.ts +95 -25
- package/src/plugins/defaults/empty-response/package.json +0 -1
- package/src/plugins/defaults/history-repair/package.json +0 -1
- package/src/plugins/defaults/index.ts +156 -26
- package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +100 -52
- package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +285 -0
- package/src/plugins/defaults/memory-retrieval/injector-chain.ts +2 -2
- package/src/plugins/defaults/{injectors/register.ts → memory-retrieval/injectors.ts} +148 -73
- package/src/plugins/defaults/memory-retrieval/package.json +14 -0
- package/src/plugins/defaults/memory-retrieval/unified-turn-context.ts +223 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/capabilities.test.ts +71 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/dense.test.ts +181 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/edge.test.ts +247 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/live-integration.test.ts +147 -134
- package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +456 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +662 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +306 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/render-injection.test.ts +91 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +402 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/section-needle.test.ts +135 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/sections.test.ts +125 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selection-log-store.test.ts +74 -19
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +446 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +592 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/types.test.ts +2 -24
- package/src/plugins/defaults/memory-v3-shadow/capabilities.ts +91 -0
- package/src/plugins/defaults/memory-v3-shadow/dense.ts +97 -0
- package/src/plugins/defaults/memory-v3-shadow/edge.ts +252 -0
- package/src/plugins/defaults/memory-v3-shadow/hooks/post-compact.ts +14 -0
- package/src/plugins/defaults/memory-v3-shadow/hooks/user-prompt-submit.ts +19 -0
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +76 -0
- package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +555 -0
- package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +266 -0
- package/src/plugins/defaults/memory-v3-shadow/package.json +14 -0
- package/src/plugins/defaults/memory-v3-shadow/page-content.ts +71 -0
- package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +204 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/render-injection.ts +16 -8
- package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +236 -0
- package/src/plugins/defaults/memory-v3-shadow/section-needle.ts +200 -0
- package/src/plugins/defaults/memory-v3-shadow/sections.ts +115 -0
- package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +156 -0
- package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +382 -0
- package/src/plugins/defaults/memory-v3-shadow/types.ts +92 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/working-set.ts +4 -1
- package/src/plugins/defaults/title-generate/package.json +0 -1
- package/src/plugins/defaults/tool-error/package.json +0 -1
- package/src/plugins/defaults/tool-result-truncate/package.json +0 -1
- package/src/plugins/external-api.ts +12 -2
- package/src/plugins/pipeline.ts +6 -293
- package/src/plugins/registry.ts +9 -37
- package/src/plugins/types.ts +76 -381
- package/src/plugins/user-loader.ts +30 -127
- package/src/prompts/__tests__/system-prompt.test.ts +6 -0
- package/src/prompts/system-prompt.ts +61 -10
- package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +71 -4
- package/src/providers/anthropic/client.ts +9 -10
- package/src/providers/inference/kimi-cjk-token-ids.ts +493 -0
- package/src/providers/inference/logit-bias.ts +55 -0
- package/src/providers/model-catalog.ts +34 -0
- package/src/providers/openai/__tests__/vision-not-supported.test.ts +75 -0
- package/src/providers/openai/chat-completions-provider.ts +44 -1
- package/src/providers/retry.ts +22 -0
- package/src/providers/types.ts +6 -0
- package/src/runtime/__tests__/agent-wake.test.ts +555 -691
- package/src/runtime/__tests__/interactive-ui.test.ts +1 -1
- package/src/runtime/agent-wake.ts +108 -209
- package/src/runtime/assistant-event-hub.ts +1 -1
- package/src/runtime/channel-approvals.ts +1 -1
- package/src/runtime/interactive-ui.ts +1 -1
- package/src/runtime/routes/__tests__/acp-routes.test.ts +466 -55
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +1 -1
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +423 -73
- package/src/runtime/routes/__tests__/stt-routes.test.ts +112 -0
- package/src/runtime/routes/__tests__/surface-action-routes.test.ts +5 -4
- package/src/runtime/routes/__tests__/surface-content-routes.test.ts +4 -1
- package/src/runtime/routes/acp-routes.test.ts +185 -32
- package/src/runtime/routes/acp-routes.ts +328 -30
- package/src/runtime/routes/app-management-routes.ts +3 -0
- package/src/runtime/routes/approval-routes.ts +1 -1
- package/src/runtime/routes/browser-routes.ts +1 -1
- package/src/runtime/routes/browser-tabs-routes.ts +6 -10
- package/src/runtime/routes/channel-verification-routes.ts +14 -5
- package/src/runtime/routes/conversation-cli-routes.ts +1 -1
- package/src/runtime/routes/conversation-list-routes.ts +1 -1
- package/src/runtime/routes/conversation-query-routes.ts +1 -1
- package/src/runtime/routes/conversation-routes.ts +30 -2
- package/src/runtime/routes/conversation-starter-routes.ts +13 -7
- package/src/runtime/routes/conversations-import-routes.ts +24 -7
- package/src/runtime/routes/host-app-control-routes.ts +1 -1
- package/src/runtime/routes/host-cu-routes.ts +1 -1
- package/src/runtime/routes/identity-routes.ts +18 -3
- package/src/runtime/routes/inbound-message-handler.ts +1 -1
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +33 -34
- package/src/runtime/routes/inference-profile-session-handler.ts +11 -0
- package/src/runtime/routes/inference-profile-session-reaper.ts +6 -0
- package/src/runtime/routes/memory-v3-routes.ts +65 -305
- package/src/runtime/routes/playground/__tests__/force-compact.test.ts +1 -1
- package/src/runtime/routes/playground/helpers.ts +1 -1
- package/src/runtime/routes/plugins-routes.ts +337 -35
- package/src/runtime/routes/stt-routes.ts +45 -12
- package/src/runtime/routes/surface-conversation-resolver.ts +4 -3
- package/src/runtime/routes/work-items-routes.ts +2 -4
- package/src/runtime/routes/workspace-routes.ts +50 -15
- package/src/runtime/services/conversation-serializer.ts +1 -1
- package/src/runtime/verification-outbound-actions.ts +147 -2
- package/src/runtime/verification-templates.ts +29 -3
- package/src/services/published-app-updater.ts +30 -8
- package/src/signals/cancel.ts +2 -4
- package/src/subagent/manager.ts +21 -5
- package/src/telemetry/__tests__/activation-funnel.test.ts +95 -0
- package/src/telemetry/activation-funnel.ts +167 -0
- package/src/telemetry/types.ts +13 -0
- package/src/telemetry/usage-telemetry-reporter.test.ts +154 -0
- package/src/telemetry/usage-telemetry-reporter.ts +26 -1
- package/src/tools/acp/context.ts +20 -0
- package/src/tools/acp/list-agents.test.ts +9 -19
- package/src/tools/acp/list-agents.ts +3 -15
- package/src/tools/acp/spawn.test.ts +173 -202
- package/src/tools/acp/spawn.ts +37 -172
- package/src/tools/acp/steer.test.ts +105 -8
- package/src/tools/acp/steer.ts +48 -17
- package/src/tools/apps/executors.ts +166 -50
- package/src/tools/browser/browser-execution.ts +12 -2
- package/src/tools/filesystem/write.ts +34 -0
- package/src/tools/network/web-fetch.ts +65 -24
- package/src/tools/skills/load.ts +1 -1
- package/src/tools/subagent/spawn.ts +2 -4
- package/src/tools/ui-surface/definitions.ts +32 -5
- package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +4 -5
- package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +69 -45
- package/docs/plugins.md +0 -832
- package/examples/plugins/echo/register.ts +0 -143
- package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -405
- package/src/__tests__/compaction-pipeline.test.ts +0 -210
- package/src/__tests__/compaction-timeout-recovery.test.ts +0 -251
- package/src/__tests__/overflow-reduce-pipeline.test.ts +0 -667
- package/src/__tests__/pipeline-runner.test.ts +0 -554
- package/src/daemon/wake-target-adapter.ts +0 -253
- package/src/memory/v3/__tests__/assign.test.ts +0 -242
- package/src/memory/v3/__tests__/capabilities.test.ts +0 -118
- package/src/memory/v3/__tests__/core.test.ts +0 -39
- package/src/memory/v3/__tests__/health.test.ts +0 -219
- package/src/memory/v3/__tests__/maintain-job.test.ts +0 -288
- package/src/memory/v3/__tests__/needle.test.ts +0 -107
- package/src/memory/v3/__tests__/orchestrate.test.ts +0 -436
- package/src/memory/v3/__tests__/provider-blocks.test.ts +0 -13
- package/src/memory/v3/__tests__/reconcile.test.ts +0 -274
- package/src/memory/v3/__tests__/render-injection.test.ts +0 -61
- package/src/memory/v3/__tests__/router.test.ts +0 -332
- package/src/memory/v3/__tests__/selector.test.ts +0 -470
- package/src/memory/v3/__tests__/shadow-plugin.test.ts +0 -432
- package/src/memory/v3/__tests__/snapshot.test.ts +0 -168
- package/src/memory/v3/__tests__/tree.test.ts +0 -192
- package/src/memory/v3/assign.ts +0 -268
- package/src/memory/v3/capabilities.ts +0 -124
- package/src/memory/v3/core.ts +0 -26
- package/src/memory/v3/health.ts +0 -0
- package/src/memory/v3/maintain-job.ts +0 -314
- package/src/memory/v3/needle.ts +0 -115
- package/src/memory/v3/orchestrate.ts +0 -126
- package/src/memory/v3/page-content.ts +0 -34
- package/src/memory/v3/provider-blocks.ts +0 -26
- package/src/memory/v3/reconcile.ts +0 -523
- package/src/memory/v3/router.ts +0 -190
- package/src/memory/v3/selection-log-store.ts +0 -84
- package/src/memory/v3/selector.ts +0 -226
- package/src/memory/v3/shadow-plugin.ts +0 -411
- package/src/memory/v3/snapshot.ts +0 -209
- package/src/memory/v3/tree.ts +0 -174
- package/src/memory/v3/types.ts +0 -59
- package/src/notifications/__tests__/emit-signal-home-feed.test.ts +0 -187
- package/src/plugins/defaults/circuit-breaker/middlewares/circuitBreaker.ts +0 -93
- package/src/plugins/defaults/circuit-breaker/package.json +0 -15
- package/src/plugins/defaults/circuit-breaker/register.ts +0 -39
- package/src/plugins/defaults/compaction/middlewares/compaction.ts +0 -25
- package/src/plugins/defaults/compaction/register.ts +0 -35
- package/src/plugins/defaults/compaction/terminal.ts +0 -73
- package/src/plugins/defaults/empty-response/register.ts +0 -23
- package/src/plugins/defaults/history-repair/register.ts +0 -24
- package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +0 -216
- package/src/plugins/defaults/overflow-reduce/middlewares/overflowReduce.ts +0 -126
- package/src/plugins/defaults/overflow-reduce/package.json +0 -15
- package/src/plugins/defaults/overflow-reduce/register.ts +0 -42
- package/src/plugins/defaults/title-generate/register.ts +0 -35
- package/src/plugins/defaults/tool-error/register.ts +0 -23
- package/src/plugins/defaults/tool-result-truncate/register.ts +0 -24
- package/src/proactive-artifact/aux-message-injector.ts +0 -97
- package/src/proactive-artifact/decision.test.ts +0 -226
- package/src/proactive-artifact/decision.ts +0 -165
- package/src/proactive-artifact/index.ts +0 -7
- package/src/proactive-artifact/job.test.ts +0 -962
- package/src/proactive-artifact/job.ts +0 -372
- package/src/proactive-artifact/message-copy.ts +0 -58
- package/src/proactive-artifact/trigger-state.test.ts +0 -286
- package/src/proactive-artifact/trigger-state.ts +0 -123
- package/src/util/map-limit.ts +0 -27
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/eval-turns.json +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/live-turns.json +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-eviction.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-skeleton.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/README.md +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/assignments.json +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/core.json +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-x.md +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-y.md +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-b/topic-z.md +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/llm-retry.ts +0 -0
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,22 @@ import {
|
|
|
8
10
|
estimateToolsTokens,
|
|
9
11
|
getCalibrationProviderKey,
|
|
10
12
|
} from "../context/token-estimator.js";
|
|
11
|
-
import type {
|
|
13
|
+
import type { InboundActorContext } from "../daemon/conversation-runtime-assembly.js";
|
|
12
14
|
import type { ToolActivityMetadata } from "../daemon/message-types/web-activity.js";
|
|
15
|
+
import type { TrustContext } from "../daemon/trust-context.js";
|
|
16
|
+
import { stripHistoricalWebSearchResults } from "../daemon/web-search-history.js";
|
|
13
17
|
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
18
|
import type {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
} from "../
|
|
25
|
-
import {
|
|
19
|
+
PostModelCallContext,
|
|
20
|
+
PostToolUseContext,
|
|
21
|
+
PreModelCallContext,
|
|
22
|
+
StopContext,
|
|
23
|
+
} from "../plugin-api/types.js";
|
|
24
|
+
import { defaultCompact } from "../plugins/defaults/compaction/compact.js";
|
|
25
|
+
import type { ContextWindowResult } from "../plugins/defaults/compaction/window-manager.js";
|
|
26
|
+
import postCompact from "../plugins/defaults/memory-retrieval/hooks/post-compact.js";
|
|
27
|
+
import { runHook } from "../plugins/pipeline.js";
|
|
28
|
+
import type { CompactionCircuitEvent } from "../plugins/types.js";
|
|
26
29
|
import { normalizeThinkingConfigForWire } from "../providers/thinking-config.js";
|
|
27
30
|
import type {
|
|
28
31
|
ContentBlock,
|
|
@@ -308,7 +311,7 @@ export type AgentEvent =
|
|
|
308
311
|
* immediately before re-injection — whether or not the pipeline actually
|
|
309
312
|
* compacted. The daemon's event dispatcher always commits `basis` (the
|
|
310
313
|
* stripped pre-compaction history) as the conversation's durable message
|
|
311
|
-
* state, so re-injection ({@link
|
|
314
|
+
* state, so re-injection ({@link postCompact}) re-applies
|
|
312
315
|
* injections onto the stripped base rather than stacking on top of the
|
|
313
316
|
* still-injected messages. When `result.compacted` is set it
|
|
314
317
|
* additionally commits the durable compaction result (DB-record fields,
|
|
@@ -382,34 +385,17 @@ export function isMaxTokensStopReason(
|
|
|
382
385
|
}
|
|
383
386
|
|
|
384
387
|
/**
|
|
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.
|
|
388
|
+
* Concatenate the text of an assistant message's `text` blocks (ignoring
|
|
389
|
+
* `tool_use`, `thinking`, and other non-text blocks). Used to re-emit the
|
|
390
|
+
* finalized text as a single `text_delta` when a turn's live output was
|
|
391
|
+
* deferred by a `pre-model-call` hook.
|
|
396
392
|
*/
|
|
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
|
-
};
|
|
393
|
+
function assistantTextOf(content: ReadonlyArray<ContentBlock>): string {
|
|
394
|
+
let text = "";
|
|
395
|
+
for (const block of content) {
|
|
396
|
+
if (block.type === "text") text += block.text;
|
|
397
|
+
}
|
|
398
|
+
return text;
|
|
413
399
|
}
|
|
414
400
|
|
|
415
401
|
/**
|
|
@@ -454,29 +440,11 @@ export interface ResolvedSystemPrompt {
|
|
|
454
440
|
model?: string;
|
|
455
441
|
}
|
|
456
442
|
|
|
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
443
|
export interface AgentLoopRunOptions {
|
|
444
|
+
/** Input history the run starts from; the loop appends its output onto a copy. */
|
|
445
|
+
messages: Message[];
|
|
446
|
+
/** Sink the loop streams its {@link AgentEvent}s through as the turn runs. */
|
|
447
|
+
onEvent: (event: AgentEvent) => void | Promise<void>;
|
|
480
448
|
signal?: AbortSignal;
|
|
481
449
|
requestId?: string;
|
|
482
450
|
onCheckpoint?: (
|
|
@@ -484,14 +452,15 @@ export interface AgentLoopRunOptions {
|
|
|
484
452
|
) => CheckpointDecision | Promise<CheckpointDecision>;
|
|
485
453
|
callSite?: LLMCallSite;
|
|
486
454
|
/**
|
|
487
|
-
*
|
|
488
|
-
*
|
|
489
|
-
*
|
|
490
|
-
*
|
|
491
|
-
*
|
|
492
|
-
*
|
|
455
|
+
* Trust classification and channel identity for the turn's inbound actor,
|
|
456
|
+
* supplied by the caller as the turn-start snapshot. Read only on the
|
|
457
|
+
* mid-loop in-place compaction path — to scope the compactor's image
|
|
458
|
+
* manifest (guardian-only attachments are excluded for untrusted actors) and
|
|
459
|
+
* forwarded to {@link postCompact}. Callers without a meaningful actor (agent
|
|
460
|
+
* wakes, standalone unit tests) pass an `unknown`-class snapshot so the
|
|
461
|
+
* compactor fail-closes to excluding guardian-only attachments.
|
|
493
462
|
*/
|
|
494
|
-
|
|
463
|
+
trust: TrustContext;
|
|
495
464
|
/**
|
|
496
465
|
* Ad-hoc inference-profile override applied to every LLM call the loop
|
|
497
466
|
* issues. When set, each `SendMessageOptions.config` carries
|
|
@@ -514,21 +483,51 @@ export interface AgentLoopRunOptions {
|
|
|
514
483
|
overflowRecovery: { enabled: boolean; safetyMarginRatio: number };
|
|
515
484
|
};
|
|
516
485
|
/**
|
|
517
|
-
*
|
|
518
|
-
*
|
|
519
|
-
*
|
|
520
|
-
*
|
|
486
|
+
* When `true`, the loop owns turn-start and mid-loop compaction. The pre-call
|
|
487
|
+
* budget gate runs before the very first provider call — subsuming the
|
|
488
|
+
* proactive turn-start compaction the orchestrator would otherwise perform
|
|
489
|
+
* inline before `run()` — as well as before each tool-use re-entry. When the
|
|
490
|
+
* gate trips it compacts the running history in place, re-applying runtime
|
|
491
|
+
* injections via the default post-compaction hook ({@link postCompact}), and
|
|
492
|
+
* continues instead of yielding `exitReason = "budget"`.
|
|
493
|
+
*
|
|
494
|
+
* The first-call pass honors the compaction circuit breaker and proceeds with
|
|
495
|
+
* the call whether or not it compacted (preflight-overflow recovery and the
|
|
496
|
+
* convergence loop remain the escalation path), so it never yields on the
|
|
497
|
+
* first call. Reruns without an inline compaction path (agent wakes,
|
|
498
|
+
* convergence/auto-compress reruns) leave it `false`: they skip the
|
|
499
|
+
* first-call gate and keep yielding for budget on mid-loop re-entries.
|
|
500
|
+
* Defaults to `false` when omitted.
|
|
501
|
+
*/
|
|
502
|
+
compactInPlace?: boolean;
|
|
503
|
+
/**
|
|
504
|
+
* Whether the in-flight turn has no human present to answer clarification
|
|
505
|
+
* questions. Resolved once by the orchestrator at turn start and forwarded to
|
|
506
|
+
* {@link postCompact} so post-compaction
|
|
507
|
+
* re-injection uses the turn-start snapshot rather than re-reading mutable
|
|
508
|
+
* client/headless state mid-turn. Defaults to `false` when omitted.
|
|
521
509
|
*/
|
|
522
|
-
|
|
510
|
+
isNonInteractive?: boolean;
|
|
523
511
|
/**
|
|
524
|
-
*
|
|
525
|
-
*
|
|
526
|
-
*
|
|
527
|
-
*
|
|
528
|
-
*
|
|
529
|
-
*
|
|
512
|
+
* The `model_profile:` turn-context label resolved once by the orchestrator
|
|
513
|
+
* at turn start, or `null` when the active inference profile is unchanged
|
|
514
|
+
* since the last notified one. Forwarded to
|
|
515
|
+
* {@link postCompact} so post-compaction re-injection
|
|
516
|
+
* re-emits the turn-start value rather than re-deriving the change-detected
|
|
517
|
+
* label (which flips once the notification is persisted mid-turn). Defaults to
|
|
518
|
+
* `null` when omitted.
|
|
530
519
|
*/
|
|
531
|
-
|
|
520
|
+
modelProfile?: string | null;
|
|
521
|
+
/**
|
|
522
|
+
* Inbound actor identity and trust fields for the unified `<turn_context>`
|
|
523
|
+
* block, or `null` on guardian turns. Resolved once by the orchestrator at
|
|
524
|
+
* turn start via the actor-trust resolver, whose contact/member registry
|
|
525
|
+
* inputs can be mutated mid-turn by contact tools, and forwarded to
|
|
526
|
+
* {@link postCompact} so post-compaction
|
|
527
|
+
* re-injection re-emits the turn-start value rather than re-resolving it.
|
|
528
|
+
* Defaults to `null` when omitted.
|
|
529
|
+
*/
|
|
530
|
+
actorContext?: InboundActorContext | null;
|
|
532
531
|
}
|
|
533
532
|
|
|
534
533
|
/**
|
|
@@ -570,17 +569,21 @@ export type LoopToolExecutor = (
|
|
|
570
569
|
}>;
|
|
571
570
|
|
|
572
571
|
export interface AgentLoopConstructorOptions {
|
|
572
|
+
/** LLM provider the loop issues every call through. */
|
|
573
|
+
provider: Provider;
|
|
574
|
+
/** Base system prompt, before any per-turn dynamic resolution. */
|
|
575
|
+
systemPrompt: string;
|
|
573
576
|
config?: Partial<AgentLoopConfig>;
|
|
574
577
|
tools?: ToolDefinition[];
|
|
575
578
|
toolExecutor?: LoopToolExecutor;
|
|
576
579
|
resolveTools?: (history: Message[]) => ToolDefinition[];
|
|
577
580
|
resolveSystemPrompt?: (history: Message[]) => ResolvedSystemPrompt;
|
|
578
581
|
/**
|
|
579
|
-
* Conversation this loop drives.
|
|
580
|
-
*
|
|
581
|
-
*
|
|
582
|
+
* Conversation this loop drives. Scopes the loop-held compaction circuit
|
|
583
|
+
* breaker and is the source of truth the loop's pipeline contexts and
|
|
584
|
+
* post-compaction re-injection resolve the live conversation through.
|
|
582
585
|
*/
|
|
583
|
-
conversationId
|
|
586
|
+
conversationId: string;
|
|
584
587
|
}
|
|
585
588
|
|
|
586
589
|
export class AgentLoop {
|
|
@@ -594,6 +597,14 @@ export class AgentLoop {
|
|
|
594
597
|
| null;
|
|
595
598
|
private toolExecutor: LoopToolExecutor | null;
|
|
596
599
|
|
|
600
|
+
/**
|
|
601
|
+
* Conversation this loop drives. Source of truth for the `conversationId`
|
|
602
|
+
* the loop's pipeline contexts and post-compaction re-injection resolve the
|
|
603
|
+
* live conversation through, so the loop knows its own identity without a
|
|
604
|
+
* threaded-in turn context.
|
|
605
|
+
*/
|
|
606
|
+
private readonly conversationId: string;
|
|
607
|
+
|
|
597
608
|
/**
|
|
598
609
|
* Loop-held compaction circuit breaker. The loop has a 1:1 lifetime with its
|
|
599
610
|
* conversation, so it is the source of truth for the cross-turn failure
|
|
@@ -603,19 +614,17 @@ export class AgentLoop {
|
|
|
603
614
|
*/
|
|
604
615
|
readonly compactionCircuit: CompactionCircuit;
|
|
605
616
|
|
|
606
|
-
constructor(
|
|
607
|
-
provider: Provider,
|
|
608
|
-
systemPrompt: string,
|
|
609
|
-
options?: AgentLoopConstructorOptions,
|
|
610
|
-
) {
|
|
617
|
+
constructor(options: AgentLoopConstructorOptions) {
|
|
611
618
|
const {
|
|
619
|
+
provider,
|
|
620
|
+
systemPrompt,
|
|
612
621
|
config,
|
|
613
622
|
tools,
|
|
614
623
|
toolExecutor,
|
|
615
624
|
resolveTools,
|
|
616
625
|
resolveSystemPrompt,
|
|
617
626
|
conversationId,
|
|
618
|
-
} = options
|
|
627
|
+
} = options;
|
|
619
628
|
this.provider = provider;
|
|
620
629
|
this.systemPrompt = systemPrompt;
|
|
621
630
|
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
@@ -623,7 +632,8 @@ export class AgentLoop {
|
|
|
623
632
|
this.resolveTools = resolveTools ?? null;
|
|
624
633
|
this.resolveSystemPrompt = resolveSystemPrompt ?? null;
|
|
625
634
|
this.toolExecutor = toolExecutor ?? null;
|
|
626
|
-
this.
|
|
635
|
+
this.conversationId = conversationId;
|
|
636
|
+
this.compactionCircuit = new CompactionCircuit(this.conversationId);
|
|
627
637
|
}
|
|
628
638
|
|
|
629
639
|
/**
|
|
@@ -675,23 +685,15 @@ export class AgentLoop {
|
|
|
675
685
|
* compaction outcome into a user-visible turn failure.
|
|
676
686
|
*/
|
|
677
687
|
private async recordCompactionOutcome(
|
|
678
|
-
|
|
688
|
+
requestId: string | undefined,
|
|
679
689
|
summaryFailed: boolean,
|
|
680
690
|
onEvent: (event: AgentEvent) => void | Promise<void>,
|
|
681
691
|
): Promise<void> {
|
|
682
692
|
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
|
-
);
|
|
693
|
+
await this.compactionCircuit.recordOutcome(summaryFailed, onEvent);
|
|
692
694
|
} catch (recordError) {
|
|
693
695
|
log.error(
|
|
694
|
-
{ err: recordError, requestId
|
|
696
|
+
{ err: recordError, requestId },
|
|
695
697
|
"Recording a compaction outcome against the circuit breaker failed; suppressing to keep the agent loop alive",
|
|
696
698
|
);
|
|
697
699
|
}
|
|
@@ -700,19 +702,21 @@ export class AgentLoop {
|
|
|
700
702
|
/**
|
|
701
703
|
* Compact the running history in place when the mid-loop budget gate trips.
|
|
702
704
|
*
|
|
703
|
-
*
|
|
704
|
-
*
|
|
705
|
-
*
|
|
706
|
-
*
|
|
707
|
-
* `exitReason = "budget"` and the orchestrator escalates.
|
|
705
|
+
* Calls the default compaction plugin on the stripped history, then
|
|
706
|
+
* re-applies injections via the supplied hooks. Returns the history to
|
|
707
|
+
* continue from, or `null` when the compactor exhausted its retry budget so
|
|
708
|
+
* the caller yields `exitReason = "budget"` and the orchestrator escalates.
|
|
708
709
|
*/
|
|
709
710
|
private async compact(
|
|
710
711
|
history: Message[],
|
|
711
|
-
|
|
712
|
-
|
|
712
|
+
requestId: string | undefined,
|
|
713
|
+
trust: TrustContext,
|
|
713
714
|
signal: AbortSignal | undefined,
|
|
714
715
|
onEvent: (event: AgentEvent) => void | Promise<void>,
|
|
715
716
|
overrideProfile: string | null,
|
|
717
|
+
isNonInteractive: boolean,
|
|
718
|
+
modelProfile: string | null,
|
|
719
|
+
actorContext: InboundActorContext | null,
|
|
716
720
|
): Promise<Message[] | null> {
|
|
717
721
|
await onEvent({ type: "context_compacting" });
|
|
718
722
|
// Strip runtime injections so the compactor summarizes the raw persistent
|
|
@@ -721,49 +725,29 @@ export class AgentLoop {
|
|
|
721
725
|
// Record the history-stripped marker right after stripping, before the
|
|
722
726
|
// pipeline runs.
|
|
723
727
|
await onEvent({ type: "history_stripped" });
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
force: true,
|
|
741
|
-
actorTrustClass: turnContext.trust.trustClass,
|
|
742
|
-
overrideProfile,
|
|
743
|
-
},
|
|
744
|
-
},
|
|
745
|
-
turnContext,
|
|
746
|
-
DEFAULT_TIMEOUTS.compaction,
|
|
747
|
-
);
|
|
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
|
-
}
|
|
756
|
-
// `CompactionResult` is intentionally `unknown` at the plugin boundary so
|
|
757
|
-
// plugin consumers don't import the window manager; the loop ran the
|
|
758
|
-
// pipeline, so it interprets the concrete result here.
|
|
759
|
-
const compactResult = result as ContextWindowResult;
|
|
728
|
+
// The compaction module owns the per-conversation manager; pass the
|
|
729
|
+
// conversation id and let `defaultCompact` resolve it from the store.
|
|
730
|
+
// The mid-loop budget gate is reached only when this turn decides to
|
|
731
|
+
// compact in place, so `force` past the auto-threshold check.
|
|
732
|
+
// `actorTrustClass` comes from the turn's trust snapshot (the actor whose
|
|
733
|
+
// turn triggered compaction) so the compactor's image manifest excludes
|
|
734
|
+
// guardian-only attachments for untrusted actors. `overrideProfile` is the
|
|
735
|
+
// turn's resolved inference-profile override for the summary call.
|
|
736
|
+
const compactResult = await defaultCompact({
|
|
737
|
+
conversationId: this.conversationId,
|
|
738
|
+
messages: rawHistory,
|
|
739
|
+
signal,
|
|
740
|
+
force: true,
|
|
741
|
+
actorTrustClass: trust.trustClass,
|
|
742
|
+
overrideProfile,
|
|
743
|
+
});
|
|
760
744
|
// `force: true` bypasses the auto-threshold gate, but early returns
|
|
761
745
|
// for "no eligible messages" / "insufficient messages" still leave
|
|
762
746
|
// `summaryFailed` undefined. Only record an outcome when the summary LLM
|
|
763
747
|
// actually ran.
|
|
764
748
|
if (compactResult.summaryFailed !== undefined) {
|
|
765
749
|
await this.recordCompactionOutcome(
|
|
766
|
-
|
|
750
|
+
requestId,
|
|
767
751
|
compactResult.summaryFailed,
|
|
768
752
|
onEvent,
|
|
769
753
|
);
|
|
@@ -783,29 +767,37 @@ export class AgentLoop {
|
|
|
783
767
|
// Re-inject onto the same base the `compaction_completed` dispatch commits:
|
|
784
768
|
// the compacted messages when the pipeline compacted, the stripped
|
|
785
769
|
// pre-compaction history otherwise.
|
|
786
|
-
|
|
770
|
+
const injection = await postCompact({
|
|
787
771
|
history: compactResult.compacted ? compactResult.messages : rawHistory,
|
|
788
|
-
|
|
772
|
+
requestId,
|
|
773
|
+
conversationId: this.conversationId,
|
|
774
|
+
trust,
|
|
775
|
+
isNonInteractive,
|
|
776
|
+
// Mid-loop re-injection always runs at full injection volume.
|
|
777
|
+
mode: "full",
|
|
778
|
+
modelProfile,
|
|
779
|
+
actorContext,
|
|
789
780
|
});
|
|
781
|
+
return injection.messages;
|
|
790
782
|
}
|
|
791
783
|
|
|
792
|
-
async run(
|
|
793
|
-
messages: Message[],
|
|
794
|
-
onEvent: (event: AgentEvent) => void | Promise<void>,
|
|
795
|
-
options?: AgentLoopRunOptions,
|
|
796
|
-
): Promise<AgentLoopRunResult> {
|
|
784
|
+
async run(options: AgentLoopRunOptions): Promise<AgentLoopRunResult> {
|
|
797
785
|
const {
|
|
786
|
+
messages,
|
|
787
|
+
onEvent,
|
|
798
788
|
signal,
|
|
799
789
|
requestId,
|
|
800
790
|
onCheckpoint,
|
|
801
791
|
callSite,
|
|
802
|
-
|
|
792
|
+
trust,
|
|
803
793
|
overrideProfile,
|
|
804
794
|
resolveOverrideProfile,
|
|
805
795
|
resolveContextWindow,
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
796
|
+
compactInPlace = false,
|
|
797
|
+
isNonInteractive = false,
|
|
798
|
+
modelProfile = null,
|
|
799
|
+
actorContext = null,
|
|
800
|
+
} = options;
|
|
809
801
|
let history = [...messages];
|
|
810
802
|
// Index into `history` where this run's appended output begins. It starts
|
|
811
803
|
// after the input and resets to the compacted base whenever the loop
|
|
@@ -818,6 +810,13 @@ export class AgentLoop {
|
|
|
818
810
|
let lastLlmCallTime = 0;
|
|
819
811
|
let exitReason: ExitReason | null = null;
|
|
820
812
|
let appendedNewMessages = false;
|
|
813
|
+
// Armed at the end of a tool-use iteration so the budget gate runs at the
|
|
814
|
+
// top of the NEXT iteration — before that iteration's provider call —
|
|
815
|
+
// instead of after the current one. Stop-hook re-query continues re-enter
|
|
816
|
+
// without arming, so the gate fires on exactly the same occasions as the
|
|
817
|
+
// prior post-call placement, plus the first call when
|
|
818
|
+
// `compactInPlace` is set (the primary run's turn-start compaction).
|
|
819
|
+
let budgetGateArmed = compactInPlace;
|
|
821
820
|
const rlog = requestId ? log.child({ requestId }) : log;
|
|
822
821
|
|
|
823
822
|
// Resolve the inference-profile override that applies right now. The
|
|
@@ -858,6 +857,104 @@ export class AgentLoop {
|
|
|
858
857
|
let toolUseBlocks: Extract<ContentBlock, { type: "tool_use" }>[] = [];
|
|
859
858
|
|
|
860
859
|
try {
|
|
860
|
+
// ── Pre-call budget gate ─────────────────────────────────────
|
|
861
|
+
// When overflow recovery is enabled, estimate the running context
|
|
862
|
+
// size as it approaches the preflight budget before issuing the
|
|
863
|
+
// provider call. With `compactInPlace` the loop compacts in place and
|
|
864
|
+
// proceeds with the call; otherwise it yields (`exitReason =
|
|
865
|
+
// "budget"`) so the orchestrator can recover before the call risks a
|
|
866
|
+
// hard context-too-large rejection. Keyed off the loop's own
|
|
867
|
+
// `history.length` (the messages actually in context this turn,
|
|
868
|
+
// including tool iterations) rather than the durable conversation
|
|
869
|
+
// count.
|
|
870
|
+
//
|
|
871
|
+
// Armed after each tool-use iteration; stop-hook re-query continues
|
|
872
|
+
// skip it. The first call runs it only when `compactInPlace` is set,
|
|
873
|
+
// where it stands in for the orchestrator's turn-start compaction: it
|
|
874
|
+
// honors the compaction circuit breaker and proceeds with the call
|
|
875
|
+
// rather than yielding, since there is no prior turn output to
|
|
876
|
+
// escalate.
|
|
877
|
+
if (budgetGateArmed) {
|
|
878
|
+
budgetGateArmed = false;
|
|
879
|
+
// The gate only re-arms after a completed tool-use iteration
|
|
880
|
+
// (`toolUseTurns` is incremented first), so reaching it with
|
|
881
|
+
// `toolUseTurns === 0` uniquely identifies the first-call pass: it
|
|
882
|
+
// compacts-or-proceeds (never yields) and honors the compaction
|
|
883
|
+
// circuit breaker, matching the orchestrator's turn-start compaction.
|
|
884
|
+
const isFirstCallGate = toolUseTurns === 0;
|
|
885
|
+
const contextWindow = resolveContextWindow?.();
|
|
886
|
+
if (contextWindow?.overflowRecovery.enabled) {
|
|
887
|
+
const { maxInputTokens, overflowRecovery } = contextWindow;
|
|
888
|
+
const safetyMargin =
|
|
889
|
+
history.length > LONG_HISTORY_MESSAGE_THRESHOLD
|
|
890
|
+
? Math.max(
|
|
891
|
+
overflowRecovery.safetyMarginRatio,
|
|
892
|
+
LONG_HISTORY_SAFETY_MARGIN_FLOOR,
|
|
893
|
+
)
|
|
894
|
+
: overflowRecovery.safetyMarginRatio;
|
|
895
|
+
const preflightBudget = Math.floor(
|
|
896
|
+
maxInputTokens * (1 - safetyMargin),
|
|
897
|
+
);
|
|
898
|
+
const midLoopThreshold =
|
|
899
|
+
preflightBudget * MID_LOOP_YIELD_THRESHOLD_RATIO;
|
|
900
|
+
const estimated = this.estimateTokens(history);
|
|
901
|
+
if (estimated > midLoopThreshold) {
|
|
902
|
+
let compactedInPlace = false;
|
|
903
|
+
// The turn-start pass skips compaction while the circuit breaker
|
|
904
|
+
// is open so a run of failed summaries doesn't keep hammering the
|
|
905
|
+
// summary LLM; mid-loop compaction is force-driven and proceeds
|
|
906
|
+
// regardless (it has already committed to compacting in place).
|
|
907
|
+
const compactionAllowed =
|
|
908
|
+
!isFirstCallGate || !(await this.compactionCircuit.isOpen());
|
|
909
|
+
if (compactInPlace && compactionAllowed) {
|
|
910
|
+
rlog.info(
|
|
911
|
+
{
|
|
912
|
+
turn: toolUseTurns,
|
|
913
|
+
estimated,
|
|
914
|
+
threshold: midLoopThreshold,
|
|
915
|
+
},
|
|
916
|
+
"Token estimate approaching budget — compacting in place",
|
|
917
|
+
);
|
|
918
|
+
const compacted = await this.compact(
|
|
919
|
+
history,
|
|
920
|
+
requestId,
|
|
921
|
+
trust,
|
|
922
|
+
signal,
|
|
923
|
+
onEvent,
|
|
924
|
+
resolveEffectiveOverrideProfile() ?? null,
|
|
925
|
+
isNonInteractive,
|
|
926
|
+
modelProfile,
|
|
927
|
+
actorContext,
|
|
928
|
+
);
|
|
929
|
+
if (compacted) {
|
|
930
|
+
history = compacted;
|
|
931
|
+
// The compacted, re-injected array is the new base; output
|
|
932
|
+
// produced after this point is what the orchestrator
|
|
933
|
+
// persists.
|
|
934
|
+
newMessagesStart = history.length;
|
|
935
|
+
compactedInPlace = true;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
// The turn-start gate proceeds with the call whether or not it
|
|
939
|
+
// compacted (preflight-overflow recovery and the convergence loop
|
|
940
|
+
// remain the escalation path); only mid-loop re-entries yield to
|
|
941
|
+
// the orchestrator before the call.
|
|
942
|
+
if (!compactedInPlace && !isFirstCallGate) {
|
|
943
|
+
rlog.warn(
|
|
944
|
+
{
|
|
945
|
+
turn: toolUseTurns,
|
|
946
|
+
estimated,
|
|
947
|
+
threshold: midLoopThreshold,
|
|
948
|
+
},
|
|
949
|
+
"Token estimate approaching budget — yielding for compaction",
|
|
950
|
+
);
|
|
951
|
+
exitReason = "budget";
|
|
952
|
+
break;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
861
958
|
// Resolve tools for this turn: use the dynamic resolver if provided,
|
|
862
959
|
// otherwise fall back to the static tool list.
|
|
863
960
|
const currentTools = this.resolveTools
|
|
@@ -921,12 +1018,14 @@ export class AgentLoop {
|
|
|
921
1018
|
providerConfig.cacheTtl = this.config.cacheTtl;
|
|
922
1019
|
}
|
|
923
1020
|
|
|
924
|
-
// Cache-anchor signal for volatile latest-user-message turns
|
|
925
|
-
// memory-v3 injects
|
|
926
|
-
//
|
|
927
|
-
//
|
|
928
|
-
//
|
|
929
|
-
|
|
1021
|
+
// Cache-anchor signal for volatile latest-user-message turns: when
|
|
1022
|
+
// memory-v3 is live it injects a per-turn `<memory>` block into the
|
|
1023
|
+
// latest user message, so the provider must anchor its long-TTL cache
|
|
1024
|
+
// breakpoint on the most recent STABLE user message instead of the
|
|
1025
|
+
// volatile latest one. Read here alongside the rest of the provider
|
|
1026
|
+
// config; only set when true so the wire/config stays byte-identical
|
|
1027
|
+
// when off.
|
|
1028
|
+
if (isAssistantFeatureFlagEnabled("memory-v3-live", getConfig())) {
|
|
930
1029
|
providerConfig.mutableLatestUserMessage = true;
|
|
931
1030
|
}
|
|
932
1031
|
|
|
@@ -941,14 +1040,14 @@ export class AgentLoop {
|
|
|
941
1040
|
providerConfig.callSite = callSite;
|
|
942
1041
|
providerConfig.usageTracking = "manual";
|
|
943
1042
|
// Per-conversation seed for deterministic `mix`-profile expansion.
|
|
944
|
-
// Sourced from the
|
|
945
|
-
//
|
|
946
|
-
//
|
|
947
|
-
//
|
|
948
|
-
//
|
|
949
|
-
//
|
|
950
|
-
if (
|
|
951
|
-
providerConfig.selectionSeed =
|
|
1043
|
+
// Sourced from the loop's own conversation id so every LLM call in a
|
|
1044
|
+
// conversation resolves the same mix arm (stable across turns and
|
|
1045
|
+
// retries, and across daemon restarts since the seed is the durable
|
|
1046
|
+
// conversation id). Absent for standalone `AgentLoop` instances
|
|
1047
|
+
// (unit tests constructed without a conversation id) — those fall
|
|
1048
|
+
// back to per-call random mix selection.
|
|
1049
|
+
if (this.conversationId) {
|
|
1050
|
+
providerConfig.selectionSeed = this.conversationId;
|
|
952
1051
|
}
|
|
953
1052
|
}
|
|
954
1053
|
|
|
@@ -994,15 +1093,16 @@ export class AgentLoop {
|
|
|
994
1093
|
);
|
|
995
1094
|
rlog.info({ turn: toolUseTurns }, "LLM call start");
|
|
996
1095
|
|
|
997
|
-
//
|
|
998
|
-
//
|
|
999
|
-
//
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
//
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1096
|
+
// Sanitize the outbound history right before sending: drop accumulated
|
|
1097
|
+
// media, collapse old AX-tree snapshots, and convert historical
|
|
1098
|
+
// web-search results to text. See {@link preModelCallSanitize}.
|
|
1099
|
+
const providerHistory = preModelCallSanitize(history);
|
|
1100
|
+
|
|
1101
|
+
// A `pre-model-call` hook (below) can defer this turn's assistant
|
|
1102
|
+
// output; when set, the live text stream is held so an
|
|
1103
|
+
// `post-model-call` hook can emit the finalized (transformed) text
|
|
1104
|
+
// instead. Reset per model call.
|
|
1105
|
+
let deferAssistantOutput = false;
|
|
1006
1106
|
|
|
1007
1107
|
// The `onEvent` wrapping below applies sensitive-output placeholder
|
|
1008
1108
|
// substitution to streamed text while forwarding every other event
|
|
@@ -1013,6 +1113,9 @@ export class AgentLoop {
|
|
|
1013
1113
|
config: providerConfig,
|
|
1014
1114
|
onEvent: (event) => {
|
|
1015
1115
|
if (event.type === "text_delta") {
|
|
1116
|
+
// Held when the turn's output is deferred — the final text is
|
|
1117
|
+
// emitted once, after the `post-model-call` hook runs.
|
|
1118
|
+
if (deferAssistantOutput) return;
|
|
1016
1119
|
// Apply sensitive-output placeholder substitution (chunk-safe)
|
|
1017
1120
|
if (substitutionMap.size > 0) {
|
|
1018
1121
|
const combined = streamingPending + event.text;
|
|
@@ -1068,12 +1171,32 @@ export class AgentLoop {
|
|
|
1068
1171
|
signal,
|
|
1069
1172
|
};
|
|
1070
1173
|
|
|
1071
|
-
//
|
|
1072
|
-
//
|
|
1073
|
-
//
|
|
1074
|
-
//
|
|
1075
|
-
|
|
1076
|
-
|
|
1174
|
+
// Let plugins edit the outbound request and opt this call into deferred
|
|
1175
|
+
// output streaming. Runs for every provider call; hooks self-gate on
|
|
1176
|
+
// call site / conversation. Fail-open: a throwing hook leaves the
|
|
1177
|
+
// request unchanged and streaming live.
|
|
1178
|
+
try {
|
|
1179
|
+
const preModelCtx: PreModelCallContext = {
|
|
1180
|
+
conversationId: this.conversationId,
|
|
1181
|
+
callSite,
|
|
1182
|
+
systemPrompt: providerOptions.systemPrompt,
|
|
1183
|
+
deferAssistantOutput: false,
|
|
1184
|
+
logger: rlog,
|
|
1185
|
+
};
|
|
1186
|
+
const finalPreModelCtx = await runHook(
|
|
1187
|
+
HOOKS.PRE_MODEL_CALL,
|
|
1188
|
+
preModelCtx,
|
|
1189
|
+
);
|
|
1190
|
+
providerOptions.systemPrompt = finalPreModelCtx.systemPrompt;
|
|
1191
|
+
// The hook owns the policy (it sees `callSite`/conversation and
|
|
1192
|
+
// self-gates); the loop honors whatever it decides.
|
|
1193
|
+
deferAssistantOutput = finalPreModelCtx.deferAssistantOutput;
|
|
1194
|
+
} catch (preModelCallError) {
|
|
1195
|
+
rlog.error(
|
|
1196
|
+
{ err: preModelCallError },
|
|
1197
|
+
"pre-model-call hook failed — proceeding with the original request",
|
|
1198
|
+
);
|
|
1199
|
+
}
|
|
1077
1200
|
|
|
1078
1201
|
// Announce the LLM-call boundary so downstream handlers (the
|
|
1079
1202
|
// daemon's persistence pipeline) can reserve an empty assistant row
|
|
@@ -1161,12 +1284,52 @@ export class AgentLoop {
|
|
|
1161
1284
|
streamingPending = "";
|
|
1162
1285
|
}
|
|
1163
1286
|
|
|
1287
|
+
// Run the `post-model-call` hook on a finalized message and, when
|
|
1288
|
+
// output was deferred, emit the finalized text once (with sensitive-output
|
|
1289
|
+
// substitution applied, matching the live stream). Fail-open: the hook
|
|
1290
|
+
// receives a clone, so a throw — even mid in-place mutation — leaves the
|
|
1291
|
+
// original message intact.
|
|
1292
|
+
const finalizeAssistantMessage = async (
|
|
1293
|
+
message: Message,
|
|
1294
|
+
): Promise<Message> => {
|
|
1295
|
+
let finalized = message;
|
|
1296
|
+
try {
|
|
1297
|
+
const ctx: PostModelCallContext = {
|
|
1298
|
+
conversationId: this.conversationId,
|
|
1299
|
+
callSite,
|
|
1300
|
+
content: structuredClone(message.content),
|
|
1301
|
+
stopReason: response.stopReason,
|
|
1302
|
+
logger: rlog,
|
|
1303
|
+
};
|
|
1304
|
+
const result = await runHook(HOOKS.POST_MODEL_CALL, ctx);
|
|
1305
|
+
finalized = { role: "assistant", content: result.content };
|
|
1306
|
+
} catch (assistantMessageError) {
|
|
1307
|
+
rlog.error(
|
|
1308
|
+
{ err: assistantMessageError },
|
|
1309
|
+
"post-model-call hook failed — keeping the original content",
|
|
1310
|
+
);
|
|
1311
|
+
finalized = message;
|
|
1312
|
+
}
|
|
1313
|
+
if (deferAssistantOutput) {
|
|
1314
|
+
// The persisted message keeps sensitive-output placeholders; the
|
|
1315
|
+
// stream shows real values — substitute before emitting.
|
|
1316
|
+
const finalText = applySubstitutions(
|
|
1317
|
+
assistantTextOf(finalized.content),
|
|
1318
|
+
substitutionMap,
|
|
1319
|
+
);
|
|
1320
|
+
if (finalText.length > 0) {
|
|
1321
|
+
onEvent({ type: "text_delta", text: finalText });
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
return finalized;
|
|
1325
|
+
};
|
|
1326
|
+
|
|
1164
1327
|
// Build the assistant message with placeholder-only text.
|
|
1165
1328
|
// Both provider history and persisted conversation store must retain
|
|
1166
1329
|
// placeholders so the model never sees real sensitive values — neither
|
|
1167
1330
|
// on subsequent loop turns nor on session reload from the database.
|
|
1168
1331
|
// Substitution to real values happens only in streamed text_delta events.
|
|
1169
|
-
|
|
1332
|
+
let assistantMessage: Message = {
|
|
1170
1333
|
role: "assistant",
|
|
1171
1334
|
content: response.content,
|
|
1172
1335
|
};
|
|
@@ -1195,10 +1358,6 @@ export class AgentLoop {
|
|
|
1195
1358
|
block.type !== "server_tool_use" &&
|
|
1196
1359
|
block.type !== "web_search_tool_result",
|
|
1197
1360
|
);
|
|
1198
|
-
const safeAssistantMessage: Message = {
|
|
1199
|
-
role: "assistant",
|
|
1200
|
-
content: safeContent,
|
|
1201
|
-
};
|
|
1202
1361
|
rlog.warn(
|
|
1203
1362
|
{
|
|
1204
1363
|
turn: toolUseTurns,
|
|
@@ -1209,6 +1368,13 @@ export class AgentLoop {
|
|
|
1209
1368
|
},
|
|
1210
1369
|
"LLM response reached output token limit",
|
|
1211
1370
|
);
|
|
1371
|
+
// Run the hook on the truncated reply so output-filter plugins still
|
|
1372
|
+
// see it, and so a deferred turn gets its synthetic final emit (the
|
|
1373
|
+
// live stream was suppressed; without this the client would see nothing).
|
|
1374
|
+
const safeAssistantMessage = await finalizeAssistantMessage({
|
|
1375
|
+
role: "assistant",
|
|
1376
|
+
content: safeContent,
|
|
1377
|
+
});
|
|
1212
1378
|
history.push(safeAssistantMessage);
|
|
1213
1379
|
appendedNewMessages = true;
|
|
1214
1380
|
await onEvent({
|
|
@@ -1241,7 +1407,7 @@ export class AgentLoop {
|
|
|
1241
1407
|
// follow-up turn. It receives the full history and, when it asks to
|
|
1242
1408
|
// continue, appends the follow-up turn itself.
|
|
1243
1409
|
const stopCtx: StopContext = {
|
|
1244
|
-
conversationId:
|
|
1410
|
+
conversationId: this.conversationId,
|
|
1245
1411
|
messages: [...history],
|
|
1246
1412
|
responseContent: response.content,
|
|
1247
1413
|
stopReason: response.stopReason,
|
|
@@ -1280,6 +1446,12 @@ export class AgentLoop {
|
|
|
1280
1446
|
}
|
|
1281
1447
|
}
|
|
1282
1448
|
|
|
1449
|
+
// Run the `post-model-call` hook + emit any deferred final text.
|
|
1450
|
+
// On a no-tool turn this point is reached only after the `stop` hook
|
|
1451
|
+
// resolves to "stop" (a `continue` already re-queried above), so a
|
|
1452
|
+
// re-queried reply is never transformed-then-discarded.
|
|
1453
|
+
assistantMessage = await finalizeAssistantMessage(assistantMessage);
|
|
1454
|
+
|
|
1283
1455
|
history.push(assistantMessage);
|
|
1284
1456
|
appendedNewMessages = true;
|
|
1285
1457
|
|
|
@@ -1430,7 +1602,7 @@ export class AgentLoop {
|
|
|
1430
1602
|
continue;
|
|
1431
1603
|
}
|
|
1432
1604
|
const postToolUseCtx: PostToolUseContext = {
|
|
1433
|
-
conversationId:
|
|
1605
|
+
conversationId: this.conversationId,
|
|
1434
1606
|
toolResponse: block as ToolResultContent,
|
|
1435
1607
|
messages: history,
|
|
1436
1608
|
maxInputTokens: contextWindowTokens,
|
|
@@ -1508,8 +1680,9 @@ export class AgentLoop {
|
|
|
1508
1680
|
history.push({ role: "user", content: resultBlocks });
|
|
1509
1681
|
|
|
1510
1682
|
// Invoke checkpoint callback after tool results are in history.
|
|
1511
|
-
// Handoff
|
|
1512
|
-
//
|
|
1683
|
+
// Handoff takes precedence over the budget gate: a handoff decision
|
|
1684
|
+
// breaks here and leaves `budgetGateArmed` false, so a queued message
|
|
1685
|
+
// is processed before the next iteration's pre-call budget gate.
|
|
1513
1686
|
if (onCheckpoint) {
|
|
1514
1687
|
const decision = await onCheckpoint({
|
|
1515
1688
|
turnIndex: toolUseTurns - 1, // 0-based (toolUseTurns was already incremented)
|
|
@@ -1523,61 +1696,11 @@ export class AgentLoop {
|
|
|
1523
1696
|
}
|
|
1524
1697
|
}
|
|
1525
1698
|
|
|
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
|
-
}
|
|
1699
|
+
// Arm the pre-call budget gate for the next iteration. Placed after
|
|
1700
|
+
// the checkpoint so a handoff yield (which breaks above) leaves it
|
|
1701
|
+
// disarmed; the gate then runs at the top of the next iteration,
|
|
1702
|
+
// before that iteration's provider call.
|
|
1703
|
+
budgetGateArmed = true;
|
|
1581
1704
|
} catch (error) {
|
|
1582
1705
|
// Abort errors are expected when user cancels — synthesize
|
|
1583
1706
|
// cancellation tool_results so the history stays valid for the
|
|
@@ -1786,3 +1909,32 @@ function stripOldMediaBlocks(history: Message[]): Message[] {
|
|
|
1786
1909
|
};
|
|
1787
1910
|
});
|
|
1788
1911
|
}
|
|
1912
|
+
|
|
1913
|
+
/**
|
|
1914
|
+
* Sanitize the outbound history immediately before a provider call, bundling
|
|
1915
|
+
* the pre-send transforms the loop applies to every request:
|
|
1916
|
+
* - {@link stripOldMediaBlocks} drops accumulated screenshot/audio bytes from
|
|
1917
|
+
* older tool results — the model saw the media on the turn it was captured.
|
|
1918
|
+
* - {@link compactAxTreeHistory} collapses all but the most recent few
|
|
1919
|
+
* `<ax-tree>` snapshots so TTFT does not grow linearly with step count.
|
|
1920
|
+
* - {@link stripHistoricalWebSearchResults} converts historical
|
|
1921
|
+
* `web_search_tool_result` blocks to text summaries; Anthropic's opaque
|
|
1922
|
+
* `encrypted_content` tokens expire / are route-scoped, and replaying a stale
|
|
1923
|
+
* one is rejected with `Invalid encrypted_content in search_result block`.
|
|
1924
|
+
*
|
|
1925
|
+
* Transforms the outbound copy only — the durable history keeps the rich
|
|
1926
|
+
* originals and each send re-derives the sanitized projection (every transform
|
|
1927
|
+
* is idempotent). Because it runs unconditionally before every provider call,
|
|
1928
|
+
* it is the single place where oversized media and expired web-search tokens
|
|
1929
|
+
* are guaranteed to be removed from a request.
|
|
1930
|
+
*
|
|
1931
|
+
* This is outbound-request preparation and should eventually move to a default
|
|
1932
|
+
* `pre-model-call` plugin hook ({@link HOOKS.PRE_MODEL_CALL}) once that hook's
|
|
1933
|
+
* context carries the outbound message list; for now it lives inline next to
|
|
1934
|
+
* the provider call it guards.
|
|
1935
|
+
*/
|
|
1936
|
+
export function preModelCallSanitize(history: Message[]): Message[] {
|
|
1937
|
+
const mediaStripped = stripOldMediaBlocks(history);
|
|
1938
|
+
const axCompacted = compactAxTreeHistory(mediaStripped);
|
|
1939
|
+
return stripHistoricalWebSearchResults(axCompacted).messages;
|
|
1940
|
+
}
|