@vellumai/assistant 0.8.7 → 0.8.8-dev.202606052332.17fc8ea
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/Dockerfile +20 -4
- package/bun.lock +2 -2
- package/docker-entrypoint.sh +4 -2
- package/docker-init-apt-root.sh +3 -1
- package/docker-kata-apt-env.sh +3 -1
- package/docker-kata-runtime-family.sh +12 -0
- package/docs/architecture/memory.md +1 -1
- package/examples/plugins/echo/README.md +61 -66
- 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/server-message.ts +3 -3
- package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +7 -6
- package/openapi.yaml +3378 -335
- package/package.json +2 -2
- package/scripts/generate-openapi.ts +68 -41
- package/src/__tests__/agent-loop-exit-reason.test.ts +35 -93
- package/src/__tests__/agent-loop-provider-error-recording.test.ts +1 -1
- package/src/__tests__/agent-loop.test.ts +37 -87
- package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
- package/src/__tests__/annotate-activity-metadata.test.ts +262 -0
- package/src/__tests__/annotate-risk-options.test.ts +2 -3
- package/src/__tests__/anthropic-provider.test.ts +95 -2
- 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-routes-http.test.ts +4 -1
- package/src/__tests__/assistant-event-hub.test.ts +25 -0
- package/src/__tests__/assistant-events-sse-shed.test.ts +8 -0
- package/src/__tests__/{conversation-stream-state.test.ts → assistant-stream-state.test.ts} +252 -91
- package/src/__tests__/auth-fallback-events-store.test.ts +116 -0
- package/src/__tests__/background-workers-disk-pressure.test.ts +6 -0
- package/src/__tests__/btw-routes.test.ts +62 -3
- package/src/__tests__/build-persisted-content.test.ts +184 -0
- package/src/__tests__/catalog-files.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__/clawhub-files.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.benchmark.test.ts +0 -30
- package/src/__tests__/config-watcher.test.ts +1 -1
- package/src/__tests__/conversation-abort-tool-results.test.ts +57 -19
- package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +6 -5
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +10 -7
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +316 -1143
- package/src/__tests__/conversation-agent-loop.test.ts +638 -1655
- package/src/__tests__/conversation-analysis-routes.test.ts +6 -0
- package/src/__tests__/conversation-clean-command.test.ts +5 -2
- package/src/__tests__/conversation-history-web-search.test.ts +11 -1
- package/src/__tests__/conversation-pairing.test.ts +4 -31
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +6 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +30 -10
- package/src/__tests__/conversation-queue.test.ts +2 -0
- package/src/__tests__/conversation-routes-disk-view.test.ts +3 -0
- package/src/__tests__/conversation-routes-slash-commands.test.ts +6 -5
- package/src/__tests__/conversation-runtime-assembly.test.ts +310 -300
- package/src/__tests__/conversation-runtime-workspace.test.ts +105 -45
- package/src/__tests__/conversation-slash-commands.test.ts +8 -42
- package/src/__tests__/conversation-slash-queue.test.ts +6 -1
- package/src/__tests__/conversation-starter-routes.test.ts +14 -6
- package/src/__tests__/conversation-surfaces-action-delivery.test.ts +84 -0
- package/src/__tests__/conversation-sync-tags.test.ts +27 -15
- package/src/__tests__/conversation-title-service.test.ts +135 -2
- package/src/__tests__/conversation-workspace-cache-state.test.ts +17 -16
- package/src/__tests__/conversation-workspace-injection.test.ts +67 -2
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +7 -6
- package/src/__tests__/conversations-import-system-filter.test.ts +101 -0
- package/src/__tests__/cross-provider-web-search.test.ts +214 -1
- package/src/__tests__/db-acp-history.test.ts +101 -0
- package/src/__tests__/db-schedule-syntax-migration.test.ts +5 -0
- package/src/__tests__/dm-persistence.test.ts +5 -1
- package/src/__tests__/dynamic-page-surface.test.ts +31 -0
- package/src/__tests__/empty-response-hook.test.ts +304 -0
- package/src/__tests__/feature-flag-test-helpers.ts +2 -2
- package/src/__tests__/file-write-tool.test.ts +63 -0
- package/src/__tests__/gateway-only-guard.test.ts +12 -2
- package/src/__tests__/gemini-image-service.test.ts +13 -0
- 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__/helpers/mock-provider.ts +110 -0
- package/src/__tests__/helpers/native-web-search-harness.ts +129 -0
- package/src/__tests__/history-repair-hook.test.ts +1 -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__/identity-intro-cache.test.ts +12 -100
- package/src/__tests__/identity-routes.test.ts +248 -7
- package/src/__tests__/inbound-slack-persistence.test.ts +5 -1
- package/src/__tests__/injector-background-turn.test.ts +3 -9
- package/src/__tests__/injector-chain.test.ts +139 -275
- package/src/__tests__/injector-disk-pressure.test.ts +75 -41
- package/src/__tests__/injector-document-comments.test.ts +3 -3
- package/src/__tests__/injector-pkb-v2-silenced.test.ts +30 -22
- package/src/__tests__/injector-v3-suppression.test.ts +31 -37
- package/src/__tests__/internal-telemetry-routes.test.ts +109 -0
- package/src/__tests__/list-messages-hidden-metadata.test.ts +38 -0
- package/src/__tests__/list-messages-page-latest.test.ts +60 -0
- package/src/__tests__/list-messages-tool-merge.test.ts +20 -0
- package/src/__tests__/llm-usage-store.test.ts +223 -1
- package/src/__tests__/memory-retrieval-hook.test.ts +297 -0
- package/src/__tests__/memory-v2-static-injector.test.ts +103 -35
- package/src/__tests__/native-web-search.test.ts +191 -0
- package/src/__tests__/onboarding-template-contract.test.ts +2 -0
- package/src/__tests__/openai-image-service.test.ts +17 -0
- package/src/__tests__/openai-provider.test.ts +31 -1
- package/src/__tests__/{overflow-reduce-pipeline.test.ts → overflow-reduction-loop.test.ts} +64 -284
- package/src/__tests__/persist-unsendable-image.test.ts +215 -0
- package/src/__tests__/persistence-secret-redaction.test.ts +1 -0
- package/src/__tests__/pkb-autoinject.test.ts +2 -5
- package/src/__tests__/plugin-api-shim.test.ts +3 -6
- package/src/__tests__/plugin-bootstrap.test.ts +14 -40
- package/src/__tests__/plugin-registry.test.ts +3 -76
- package/src/__tests__/plugin-types.test.ts +0 -193
- package/src/__tests__/process-message-display-content.test.ts +6 -2
- package/src/__tests__/reaction-persistence.test.ts +1 -1
- package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +5 -1
- package/src/__tests__/resolve-trust-class.test.ts +4 -4
- package/src/__tests__/runtime-events-sse-reconnect.test.ts +60 -23
- package/src/__tests__/schedule-routes.test.ts +603 -2
- package/src/__tests__/schedule-store.test.ts +41 -0
- package/src/__tests__/schedule-tools.test.ts +35 -0
- package/src/__tests__/send-endpoint-busy.test.ts +4 -1
- package/src/__tests__/server-history-render.test.ts +314 -1
- package/src/__tests__/skill-feature-flags-integration.test.ts +33 -0
- package/src/__tests__/skillssh-files.test.ts +1 -1
- package/src/__tests__/subagent-call-site-routing.test.ts +1 -1
- 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__/system-prompt.test.ts +20 -0
- package/src/__tests__/task-scheduler.test.ts +162 -1
- package/src/__tests__/terminal-tools.test.ts +6 -1
- package/src/__tests__/title-generate-hook.test.ts +319 -0
- package/src/__tests__/tool-error-hook.test.ts +278 -0
- package/src/__tests__/tool-preview-lifecycle.test.ts +468 -5
- package/src/__tests__/tool-result-metadata-plumbing.test.ts +1 -0
- package/src/__tests__/tool-result-truncate-hook.test.ts +127 -0
- package/src/__tests__/tool-result-truncation.test.ts +0 -2
- package/src/__tests__/ui-choice-copy-surfaces.test.ts +254 -0
- package/src/__tests__/ui-work-result-surface.test.ts +159 -0
- package/src/__tests__/usage-routes.test.ts +285 -1
- package/src/__tests__/user-plugin-loader.test.ts +54 -286
- package/src/__tests__/voice-session-bridge.test.ts +6 -3
- package/src/__tests__/web-search-backend-failure.test.ts +166 -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-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 +362 -485
- package/src/api/events/assistant-thinking-delta.ts +33 -0
- package/src/api/events/tool-output-chunk.ts +45 -0
- package/src/api/events/tool-use-preview-start.ts +32 -0
- package/src/api/events/trace-event.ts +69 -0
- package/src/api/index.ts +48 -13
- package/src/api/responses/conversation-message.ts +374 -0
- package/src/approvals/guardian-request-resolvers.ts +1 -1
- package/src/avatar/__tests__/avatar-store.test.ts +34 -29
- package/src/background-wake/next-wake.ts +1 -0
- package/src/cli/commands/__tests__/notifications.test.ts +58 -14
- package/src/cli/commands/notifications.ts +112 -60
- 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/assistant-feature-flags.ts +22 -11
- 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 -398
- 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/references/examples/README.md +17 -0
- package/src/config/bundled-skills/app-builder/references/examples/expense-tracker.md +515 -0
- package/src/config/bundled-skills/app-builder/references/examples/focus-timer.md +342 -0
- package/src/config/bundled-skills/app-builder/references/examples/habit-tracker.md +490 -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-skills/messaging/SKILL.md +0 -7
- package/src/config/bundled-tool-registry.ts +2 -0
- package/src/config/feature-flag-cache.ts +3 -3
- package/src/config/feature-flag-registry.json +48 -7
- package/src/config/schemas/__tests__/memory-v2.test.ts +1 -0
- package/src/config/schemas/__tests__/memory-v3.test.ts +25 -0
- package/src/config/schemas/heartbeat.ts +9 -0
- package/src/config/schemas/llm.ts +1 -0
- package/src/config/schemas/memory-v2.ts +8 -0
- package/src/config/schemas/memory-v3.ts +8 -0
- package/src/config/schemas/platform.ts +8 -0
- package/src/config/seed-inference-profiles.ts +2 -2
- package/src/config/skills.ts +13 -0
- package/src/context/compactor.ts +1 -1
- package/src/context/strip-injections.ts +128 -0
- package/src/context/token-estimator.ts +23 -0
- package/src/context/tool-result-truncation.ts +0 -23
- package/src/context/window-manager.ts +5 -7
- package/src/credential-execution/executable-discovery.ts +16 -0
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +6 -0
- package/src/daemon/__tests__/inference-profile-notification.test.ts +153 -0
- package/src/daemon/__tests__/native-web-search-metadata.test.ts +10 -8
- package/src/daemon/assistant-attachments.ts +1 -1
- package/src/daemon/config-watcher.ts +2 -2
- package/src/daemon/context-overflow-reducer.ts +0 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +594 -153
- package/src/daemon/conversation-agent-loop.ts +301 -997
- package/src/daemon/conversation-history.ts +5 -4
- package/src/daemon/conversation-lifecycle.ts +3 -4
- package/src/daemon/conversation-messaging.ts +7 -6
- package/src/daemon/conversation-process.ts +11 -16
- package/src/daemon/conversation-registry.ts +159 -0
- package/src/daemon/conversation-runtime-assembly.ts +218 -398
- package/src/daemon/conversation-slash.ts +6 -25
- package/src/daemon/conversation-store.ts +9 -90
- package/src/daemon/conversation-surfaces.ts +222 -4
- package/src/daemon/conversation-tool-setup.ts +2 -29
- package/src/daemon/conversation-workspace.ts +17 -0
- package/src/daemon/conversation.ts +32 -20
- package/src/daemon/external-plugins-bootstrap.ts +17 -18
- package/src/daemon/handlers/config-a2a.ts +51 -36
- package/src/daemon/handlers/config-slack-channel.ts +20 -14
- package/src/daemon/handlers/config-telegram.ts +16 -2
- package/src/daemon/handlers/conversations.ts +3 -1
- package/src/daemon/handlers/shared.ts +156 -84
- package/src/daemon/handlers/skills.ts +42 -10
- package/src/daemon/lifecycle.ts +25 -0
- package/src/daemon/message-types/apps.ts +1 -29
- package/src/daemon/message-types/messages.ts +9 -57
- package/src/daemon/message-types/skills.ts +2 -0
- package/src/daemon/message-types/surfaces.ts +136 -3
- package/src/daemon/now-scratchpad.ts +21 -0
- package/src/daemon/orphan-reaper.test.ts +210 -0
- package/src/daemon/orphan-reaper.ts +240 -0
- package/src/daemon/overflow-reduction-loop.ts +230 -0
- package/src/daemon/persist-unsendable-image.ts +117 -0
- package/src/daemon/process-message.ts +1 -3
- package/src/daemon/server.ts +2 -0
- package/src/daemon/trace-emitter.ts +6 -4
- package/src/daemon/trust-context.ts +19 -0
- package/src/daemon/wake-target-adapter.ts +3 -1
- 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/home/home-greeting-cache.ts +24 -1
- package/src/ipc/__tests__/browser-ipc.test.ts +1 -1
- package/src/ipc/__tests__/ui-request-route.test.ts +3 -3
- package/src/ipc/gateway-client.test.ts +2 -2
- package/src/ipc/gateway-client.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/media/gemini-image-service.ts +15 -0
- package/src/media/openai-image-service.ts +14 -0
- package/src/media/types.ts +34 -0
- package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +56 -0
- package/src/memory/auth-fallback-events-store.ts +94 -0
- package/src/memory/conversation-starter-checkpoints.ts +1 -0
- package/src/memory/conversation-title-service.ts +65 -41
- package/src/memory/db-init.ts +6 -0
- package/src/memory/graph/__tests__/conversation-graph-memory-registry.test.ts +119 -0
- package/src/memory/graph/conversation-graph-memory.ts +65 -0
- package/src/memory/job-handlers/conversation-starters.ts +13 -2
- package/src/memory/jobs-store.ts +33 -0
- package/src/memory/jobs-worker.ts +32 -5
- package/src/memory/llm-usage-store.ts +224 -50
- package/src/memory/migrations/222-strip-placeholder-sentinels-from-messages.ts +6 -5
- package/src/memory/migrations/270-schedule-source-conversation.ts +13 -0
- package/src/memory/migrations/271-create-auth-fallback-events.ts +21 -0
- package/src/memory/migrations/272-acp-session-history-cwd.ts +36 -0
- package/src/memory/migrations/index.ts +3 -0
- package/src/memory/pkb/autoinject.ts +61 -0
- package/src/memory/pkb/context.ts +50 -0
- package/src/memory/pkb/types.ts +14 -0
- package/src/memory/schedule-attribution-sql.ts +104 -0
- package/src/memory/schema/acp.ts +4 -0
- package/src/memory/schema/infrastructure.ts +16 -0
- package/src/memory/usage-grouped-buckets.ts +6 -1
- package/src/memory/v2/__tests__/consolidation-job.test.ts +4 -4
- package/src/memory/v2/consolidation-job.ts +14 -5
- package/src/notifications/conversation-pairing.ts +8 -15
- package/src/notifications/decision-engine.ts +6 -3
- package/src/notifications/home-feed-side-effect.ts +12 -1
- package/src/permissions/prompter.ts +4 -0
- package/src/plugin-api/constants.ts +4 -0
- package/src/plugin-api/index.ts +7 -5
- package/src/plugin-api/types.ts +151 -1
- package/src/plugins/defaults/compaction/compact.ts +59 -0
- package/src/plugins/defaults/compaction/package.json +1 -1
- package/src/plugins/defaults/compaction/register.ts +8 -19
- package/src/plugins/defaults/empty-response/hooks/stop.ts +126 -0
- package/src/plugins/defaults/empty-response/register.ts +8 -13
- package/src/plugins/defaults/index.ts +2 -18
- package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +95 -0
- package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +216 -0
- package/src/plugins/defaults/memory-retrieval/injector-chain.ts +35 -0
- package/src/plugins/defaults/{injectors/register.ts → memory-retrieval/injectors.ts} +288 -81
- 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__/health.test.ts +16 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/live-integration.test.ts +4 -4
- 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 +48 -12
- package/src/plugins/defaults/memory-v3-shadow/__tests__/provider-blocks.test.ts +13 -0
- 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 +104 -32
- 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 +96 -30
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/shadow-plugin.test.ts +34 -16
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/assign.ts +5 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/capabilities.ts +2 -2
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/health.ts +0 -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 +75 -0
- package/src/plugins/defaults/memory-v3-shadow/llm-retry.ts +32 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/maintain-job.ts +8 -8
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/orchestrate.ts +26 -14
- package/src/plugins/defaults/{llm-call → memory-v3-shadow}/package.json +2 -2
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/page-content.ts +2 -2
- package/src/plugins/defaults/memory-v3-shadow/provider-blocks.ts +26 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/reconcile.ts +3 -3
- package/src/plugins/defaults/memory-v3-shadow/register.ts +26 -0
- 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 +51 -45
- 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 +61 -46
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/shadow-plugin.ts +69 -99
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/tree.ts +1 -1
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/types.ts +8 -0
- package/src/plugins/defaults/title-generate/hooks/stop.ts +75 -0
- package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +35 -0
- package/src/plugins/defaults/title-generate/package.json +1 -1
- package/src/plugins/defaults/title-generate/register.ts +18 -18
- package/src/plugins/defaults/tool-error/hooks/post-tool-use.ts +118 -0
- package/src/plugins/defaults/tool-error/package.json +1 -1
- package/src/plugins/defaults/tool-error/register.ts +9 -21
- package/src/plugins/defaults/tool-result-truncate/hooks/post-tool-use.ts +32 -0
- package/src/plugins/defaults/tool-result-truncate/register.ts +10 -21
- package/src/plugins/defaults/tool-result-truncate/terminal.ts +37 -18
- package/src/plugins/external-api.ts +2 -2
- package/src/plugins/pipeline.ts +6 -305
- package/src/plugins/registry.ts +10 -55
- package/src/plugins/types.ts +62 -797
- package/src/plugins/user-loader.ts +30 -127
- package/src/proactive-artifact/aux-message-injector.ts +4 -4
- package/src/proactive-artifact/job.test.ts +8 -13
- package/src/prompts/__tests__/system-prompt.test.ts +42 -0
- package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +64 -0
- package/src/prompts/templates/BOOTSTRAP.md +2 -2
- package/src/prompts/templates/system-sections.ts +15 -0
- package/src/providers/anthropic/client.ts +37 -29
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +112 -0
- package/src/providers/openai/chat-completions-provider.ts +44 -0
- package/src/providers/openrouter/client.ts +1 -0
- package/src/providers/placeholder-sentinels.ts +35 -0
- package/src/runtime/__tests__/agent-wake.test.ts +10 -6
- package/src/runtime/__tests__/interactive-ui.test.ts +1 -1
- package/src/runtime/agent-wake.ts +2 -5
- package/src/runtime/assistant-event-hub.ts +37 -7
- package/src/runtime/{conversation-stream-state.ts → assistant-stream-state.ts} +132 -58
- package/src/runtime/channel-approvals.ts +1 -1
- package/src/runtime/http-router.ts +16 -21
- package/src/runtime/http-types.ts +16 -70
- package/src/runtime/interactive-ui.ts +1 -1
- package/src/runtime/pending-interactions.ts +1 -0
- package/src/runtime/routes/__tests__/acp-routes.test.ts +283 -55
- package/src/runtime/routes/__tests__/consolidation-routes.test.ts +265 -2
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +1 -1
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +31 -1
- package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +6 -2
- 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/__tests__/tts-routes.test.ts +6 -2
- package/src/runtime/routes/acp-routes.test.ts +89 -25
- package/src/runtime/routes/acp-routes.ts +81 -29
- package/src/runtime/routes/app-management-routes.ts +6 -117
- package/src/runtime/routes/app-routes.ts +13 -15
- package/src/runtime/routes/approval-routes.ts +1 -1
- package/src/runtime/routes/attachment-routes.ts +26 -15
- package/src/runtime/routes/avatar-routes.ts +26 -0
- package/src/runtime/routes/browser-routes.ts +1 -1
- package/src/runtime/routes/browser-tabs-routes.ts +6 -10
- package/src/runtime/routes/btw-routes.ts +29 -23
- package/src/runtime/routes/consolidation-routes.ts +120 -20
- 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 +3 -1
- package/src/runtime/routes/conversation-routes.ts +372 -185
- package/src/runtime/routes/conversation-starter-routes.ts +13 -7
- package/src/runtime/routes/conversations-import-routes.ts +24 -7
- package/src/runtime/routes/documents-routes.ts +4 -0
- package/src/runtime/routes/domain-routes.ts +51 -37
- package/src/runtime/routes/epoch-millis-range.ts +34 -0
- package/src/runtime/routes/events-routes.ts +28 -34
- package/src/runtime/routes/gateway-log-routes.ts +26 -4
- package/src/runtime/routes/heartbeat-routes.ts +32 -12
- 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-intro-cache.ts +11 -34
- package/src/runtime/routes/identity-routes.ts +224 -18
- package/src/runtime/routes/image-generation-routes.ts +40 -2
- package/src/runtime/routes/inbound-message-handler.ts +1 -1
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/integrations/a2a.ts +12 -10
- package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +16 -0
- package/src/runtime/routes/integrations/slack/channel.ts +4 -0
- package/src/runtime/routes/integrations/slack/share.ts +27 -6
- package/src/runtime/routes/integrations/telegram.ts +6 -0
- package/src/runtime/routes/integrations/twilio.ts +42 -0
- package/src/runtime/routes/internal-telemetry-routes.ts +88 -0
- package/src/runtime/routes/log-export-routes.ts +8 -0
- package/src/runtime/routes/memory-v2-routes.ts +15 -8
- package/src/runtime/routes/memory-v3-routes.ts +66 -34
- package/src/runtime/routes/oauth-apps.ts +66 -12
- package/src/runtime/routes/oauth-providers.ts +44 -5
- package/src/runtime/routes/platform-routes.ts +81 -5
- package/src/runtime/routes/playground/__tests__/force-compact.test.ts +6 -4
- package/src/runtime/routes/playground/force-compact.ts +1 -1
- package/src/runtime/routes/playground/helpers.ts +1 -1
- package/src/runtime/routes/rename-conversation-routes.ts +5 -0
- package/src/runtime/routes/schedule-routes.ts +152 -42
- package/src/runtime/routes/secret-routes.ts +14 -2
- package/src/runtime/routes/skills-routes.ts +43 -14
- package/src/runtime/routes/surface-conversation-resolver.ts +4 -3
- package/src/runtime/routes/tool-call-confirmation-enrichment.test.ts +161 -0
- package/src/runtime/routes/tool-call-confirmation-enrichment.ts +107 -0
- package/src/runtime/routes/trust-rules-routes.ts +26 -2
- package/src/runtime/routes/tts-routes.ts +35 -0
- package/src/runtime/routes/types.ts +66 -8
- package/src/runtime/routes/usage-routes.ts +47 -39
- package/src/runtime/routes/webhook-routes.ts +41 -2
- package/src/runtime/routes/work-items-routes.ts +2 -4
- package/src/runtime/routes/workspace-routes.ts +4 -0
- package/src/runtime/services/__tests__/analyze-conversation.test.ts +6 -0
- package/src/runtime/services/analyze-conversation.ts +2 -2
- package/src/runtime/services/conversation-serializer.ts +1 -1
- package/src/schedule/schedule-store.ts +20 -1
- package/src/schedule/schedule-usage-store.ts +83 -0
- package/src/schedule/scheduler.ts +12 -5
- package/src/signals/cancel.ts +2 -4
- package/src/skills/catalog-files.ts +2 -2
- package/src/skills/catalog-install.ts +3 -0
- package/src/skills/categories-cache.ts +118 -0
- package/src/skills/clawhub-files.ts +1 -2
- package/src/skills/skillssh-files.ts +1 -2
- package/src/subagent/manager.ts +17 -5
- package/src/telemetry/types.ts +29 -1
- package/src/telemetry/usage-telemetry-reporter.test.ts +112 -3
- package/src/telemetry/usage-telemetry-reporter.ts +57 -2
- 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/executor.ts +1 -53
- package/src/tools/filesystem/write.ts +34 -0
- package/src/tools/network/__tests__/web-search-metadata.test.ts +7 -1
- package/src/tools/network/__tests__/web-search.test.ts +11 -3
- package/src/tools/network/web-search-error.test.ts +248 -0
- package/src/tools/network/web-search-error.ts +267 -0
- package/src/tools/network/web-search.ts +207 -48
- package/src/tools/schedule/create.ts +2 -0
- package/src/tools/subagent/spawn.ts +2 -4
- package/src/tools/terminal/safe-env.ts +10 -1
- package/src/tools/ui-surface/definitions.ts +34 -5
- package/src/tts/__tests__/provider-catalog-consistency.test.ts +85 -1
- package/src/tts/provider-catalog.ts +76 -1
- package/src/util/mutex.ts +47 -0
- package/src/workspace/git-service.ts +1 -42
- package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +4 -5
- package/src/workspace/migrations/095-bump-heartbeat-interval-30m-to-60m.ts +51 -0
- package/src/workspace/migrations/096-reduce-quality-profile-effort.ts +72 -0
- package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +117 -0
- package/src/workspace/migrations/registry.ts +6 -0
- package/docs/plugins.md +0 -836
- package/examples/plugins/echo/register.ts +0 -184
- package/src/__tests__/bootstrap-turn-cleanup.test.ts +0 -44
- 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__/empty-response-pipeline.test.ts +0 -423
- package/src/__tests__/llm-call-pipeline.test.ts +0 -287
- package/src/__tests__/memory-retrieval-pipeline.test.ts +0 -418
- package/src/__tests__/persistence-pipeline.test.ts +0 -503
- package/src/__tests__/pipeline-runner.test.ts +0 -564
- package/src/__tests__/title-generate-pipeline.test.ts +0 -211
- package/src/__tests__/token-estimate-pipeline.test.ts +0 -479
- package/src/__tests__/tool-error-pipeline.test.ts +0 -241
- package/src/__tests__/tool-execute-pipeline.test.ts +0 -417
- package/src/__tests__/tool-result-truncate-pipeline.test.ts +0 -341
- package/src/daemon/bootstrap-turn-cleanup.ts +0 -45
- package/src/gallery/default-gallery.ts +0 -1359
- package/src/gallery/gallery-manifest.ts +0 -28
- package/src/home/feature-gate.ts +0 -22
- package/src/memory/v3/provider-blocks.ts +0 -16
- 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/terminal.ts +0 -73
- package/src/plugins/defaults/empty-response/middlewares/emptyResponse.ts +0 -22
- package/src/plugins/defaults/empty-response/terminal.ts +0 -106
- package/src/plugins/defaults/injectors/package.json +0 -15
- package/src/plugins/defaults/llm-call/middlewares/llmCall.ts +0 -17
- package/src/plugins/defaults/llm-call/register.ts +0 -45
- package/src/plugins/defaults/memory-retrieval/middlewares/memoryRetrieval.ts +0 -17
- package/src/plugins/defaults/memory-retrieval/package.json +0 -15
- package/src/plugins/defaults/memory-retrieval/register.ts +0 -181
- 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/persistence/middlewares/persistence.ts +0 -19
- package/src/plugins/defaults/persistence/package.json +0 -15
- package/src/plugins/defaults/persistence/register.ts +0 -38
- package/src/plugins/defaults/persistence/terminal.ts +0 -83
- package/src/plugins/defaults/title-generate/terminal.ts +0 -31
- package/src/plugins/defaults/token-estimate/middlewares/tokenEstimate.ts +0 -23
- package/src/plugins/defaults/token-estimate/package.json +0 -15
- package/src/plugins/defaults/token-estimate/register.ts +0 -34
- package/src/plugins/defaults/token-estimate/terminal.ts +0 -40
- package/src/plugins/defaults/tool-error/middlewares/toolError.ts +0 -21
- package/src/plugins/defaults/tool-error/terminal.ts +0 -47
- package/src/plugins/defaults/tool-execute/middlewares/toolExecute.ts +0 -23
- package/src/plugins/defaults/tool-execute/package.json +0 -15
- package/src/plugins/defaults/tool-execute/register.ts +0 -49
- package/src/plugins/defaults/tool-result-truncate/middlewares/toolResultTruncate.ts +0 -23
- package/src/plugins/defaults/tool-result-truncate/types.ts +0 -22
- package/src/skills/category-inference.ts +0 -111
- /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__/needle.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__/types.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}/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
|
@@ -17,6 +17,7 @@ import type {
|
|
|
17
17
|
import { getConfig } from "../config/loader.js";
|
|
18
18
|
import { recordEstimate } from "../context/estimator-calibration.js";
|
|
19
19
|
import { getCalibrationProviderKey } from "../context/token-estimator.js";
|
|
20
|
+
import type { ContextWindowResult } from "../context/window-manager.js";
|
|
20
21
|
import { projectAssistantMessage } from "../memory/conversation-attention-store.js";
|
|
21
22
|
import {
|
|
22
23
|
deleteMessageById,
|
|
@@ -24,8 +25,12 @@ import {
|
|
|
24
25
|
getMessageById,
|
|
25
26
|
messageMetadataSchema,
|
|
26
27
|
provenanceFromTrustContext,
|
|
28
|
+
reserveMessage,
|
|
29
|
+
setConversationHistoryStrippedAt,
|
|
30
|
+
setLastNotifiedInferenceProfile,
|
|
27
31
|
updateMessageContent,
|
|
28
32
|
} from "../memory/conversation-crud.js";
|
|
33
|
+
import { syncMessageToDisk } from "../memory/conversation-disk-view.js";
|
|
29
34
|
import { indexMessageNow } from "../memory/indexer.js";
|
|
30
35
|
import {
|
|
31
36
|
backfillMessageIdOnLogs,
|
|
@@ -41,20 +46,24 @@ import {
|
|
|
41
46
|
type SlackMessageMetadata,
|
|
42
47
|
writeSlackMetadata,
|
|
43
48
|
} from "../messaging/providers/slack/message-metadata.js";
|
|
44
|
-
import { defaultPersistenceTerminal } from "../plugins/defaults/persistence/terminal.js";
|
|
45
|
-
import { DEFAULT_TIMEOUTS, runPipeline } from "../plugins/pipeline.js";
|
|
46
|
-
import { getMiddlewaresFor } from "../plugins/registry.js";
|
|
47
49
|
import type {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
} from "../plugins/types.js";
|
|
53
|
-
import type { ContentBlock, ImageContent } from "../providers/types.js";
|
|
50
|
+
ContentBlock,
|
|
51
|
+
ImageContent,
|
|
52
|
+
Message,
|
|
53
|
+
} from "../providers/types.js";
|
|
54
54
|
import { isContextOverflowError } from "../providers/types.js";
|
|
55
|
+
import {
|
|
56
|
+
getCurrentSeq,
|
|
57
|
+
recordPersistedSeq,
|
|
58
|
+
} from "../runtime/assistant-stream-state.js";
|
|
55
59
|
import { publishSyncInvalidation } from "../runtime/sync/sync-publisher.js";
|
|
56
60
|
import { redactSecrets } from "../security/secret-scanner.js";
|
|
57
61
|
import { extractDomain } from "../tools/network/domain-normalize.js";
|
|
62
|
+
import {
|
|
63
|
+
classifyWebSearchFailure,
|
|
64
|
+
logWebSearchBackendFailure,
|
|
65
|
+
WEB_SEARCH_BACKEND_FAILURE_MESSAGE,
|
|
66
|
+
} from "../tools/network/web-search-error.js";
|
|
58
67
|
import {
|
|
59
68
|
buildPricingUsage,
|
|
60
69
|
resolveStructuredPricing,
|
|
@@ -87,45 +96,36 @@ import type {
|
|
|
87
96
|
} from "./message-protocol.js";
|
|
88
97
|
import { conversationMetadataSyncTag } from "./message-types/sync.js";
|
|
89
98
|
import type {
|
|
99
|
+
ToolActivityMetadata,
|
|
90
100
|
WebSearchMetadata,
|
|
91
101
|
WebSearchResultItem,
|
|
92
102
|
} from "./message-types/web-activity.js";
|
|
93
|
-
import { FALLBACK_TURN_TRUST } from "./trust-context.js";
|
|
94
103
|
|
|
95
104
|
const log = getLogger("agent-loop-handlers");
|
|
96
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Persist the history-stripped marker after the loop strips runtime injections
|
|
108
|
+
* for compaction / overflow recovery. The marker is a durability hint, not
|
|
109
|
+
* turn-critical state — a transient SQLite write failure (SQLITE_BUSY,
|
|
110
|
+
* disk-full, read-only FS) must not abort the turn, so failures log a warning
|
|
111
|
+
* and continue.
|
|
112
|
+
*/
|
|
113
|
+
export function markHistoryStrippedBestEffort(conversationId: string): void {
|
|
114
|
+
try {
|
|
115
|
+
setConversationHistoryStrippedAt(conversationId, Date.now());
|
|
116
|
+
} catch (err) {
|
|
117
|
+
log.warn(
|
|
118
|
+
{ err, conversationId },
|
|
119
|
+
"Failed to persist history-stripped marker after compaction strip (non-fatal)",
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
97
124
|
// ── Partial-persistence tunables ─────────────────────────────────────
|
|
98
125
|
// Debounce for mid-turn `updateContent` writes from text deltas.
|
|
99
126
|
// Indexer + projector still fire ONLY at `handleMessageComplete`.
|
|
100
127
|
const PARTIAL_PERSIST_DEBOUNCE_MS = 1000;
|
|
101
128
|
|
|
102
|
-
/**
|
|
103
|
-
* Build a {@link TurnContext} from the handler's deps for pipeline logging
|
|
104
|
-
* and plugin attribution.
|
|
105
|
-
*
|
|
106
|
-
* Reads `turnIndex` from `deps.ctx.turnCount` — the orchestrator-owned
|
|
107
|
-
* per-turn counter that is stable for the entire duration of a single
|
|
108
|
-
* `runAgentLoopImpl` invocation. The handlers fire after the orchestrator
|
|
109
|
-
* has completed its in-turn pipeline work but before `ctx.turnCount++` runs
|
|
110
|
-
* in the outer `finally` block, so this value always reflects the turn the
|
|
111
|
-
* handler's event belongs to. Trust pulls from the per-turn snapshot first,
|
|
112
|
-
* then the conversation-level context, then the canonical `unknown`
|
|
113
|
-
* fallback so the required field stays populated for edge cases (fresh
|
|
114
|
-
* conversations before the trust resolver runs, heartbeat turns that never
|
|
115
|
-
* bind an actor).
|
|
116
|
-
*/
|
|
117
|
-
function buildHandlerTurnContext(deps: EventHandlerDeps): TurnContext {
|
|
118
|
-
return {
|
|
119
|
-
requestId: deps.reqId,
|
|
120
|
-
conversationId: deps.ctx.conversationId,
|
|
121
|
-
turnIndex: deps.ctx.turnCount,
|
|
122
|
-
trust:
|
|
123
|
-
deps.ctx.currentTurnTrustContext ??
|
|
124
|
-
deps.ctx.trustContext ??
|
|
125
|
-
FALLBACK_TURN_TRUST,
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
|
|
129
129
|
// ── Types ────────────────────────────────────────────────────────────
|
|
130
130
|
|
|
131
131
|
export interface PendingToolResult {
|
|
@@ -137,6 +137,16 @@ export interface PendingToolResult {
|
|
|
137
137
|
/** Mutable state shared across event handlers within a single agent loop run. */
|
|
138
138
|
export interface EventHandlerState {
|
|
139
139
|
llmCallStartedEmitted: boolean;
|
|
140
|
+
/**
|
|
141
|
+
* Profile key whose `model_profile` notice has been assembled into the turn
|
|
142
|
+
* context but not yet marked notified. Set when the turn injects the notice,
|
|
143
|
+
* and consumed the first time the model actually receives that context — i.e.
|
|
144
|
+
* on the first `message_complete`. Persisting on delivery (rather than inline
|
|
145
|
+
* before the provider call) means a cancelled or failed turn re-sends the
|
|
146
|
+
* notice next turn instead of silently marking the profile notified without
|
|
147
|
+
* the model ever having seen it.
|
|
148
|
+
*/
|
|
149
|
+
pendingNotifiedInferenceProfile: string | null;
|
|
140
150
|
pendingDirectiveDisplayBuffer: string;
|
|
141
151
|
firstAssistantText: string;
|
|
142
152
|
/** Most recent resolved provider for the current exchange's usage accounting. */
|
|
@@ -186,6 +196,15 @@ export interface EventHandlerState {
|
|
|
186
196
|
*/
|
|
187
197
|
assistantRowAwaitingFinalization: boolean;
|
|
188
198
|
readonly pendingToolResults: Map<string, PendingToolResult>;
|
|
199
|
+
/**
|
|
200
|
+
* Reservation of the grouped `user` tool-result row for the current batch,
|
|
201
|
+
* resolving to the row id. Shared across the concurrent `handleToolResult`
|
|
202
|
+
* calls of one parallel-tool batch so they reserve exactly one row and write
|
|
203
|
+
* into it as sibling results land. `undefined` until the first result of a
|
|
204
|
+
* batch triggers a reservation (reset on a failed reservation so the next
|
|
205
|
+
* arrival can retry) and again after the batch is finalized.
|
|
206
|
+
*/
|
|
207
|
+
pendingToolResultRowReservation: Promise<string> | undefined;
|
|
189
208
|
readonly persistedToolUseIds: Set<string>;
|
|
190
209
|
readonly accumulatedDirectives: DirectiveRequest[];
|
|
191
210
|
readonly accumulatedToolContentBlocks: ContentBlock[];
|
|
@@ -238,6 +257,13 @@ export interface EventHandlerState {
|
|
|
238
257
|
riskDirectoryScopeOptions?: Array<{ scope: string; label: string }>;
|
|
239
258
|
}
|
|
240
259
|
>;
|
|
260
|
+
/**
|
|
261
|
+
* Structured tool activity (web_search / web_fetch) keyed by tool_use_id,
|
|
262
|
+
* captured when a result lands so it can be persisted on the tool's content
|
|
263
|
+
* block and survive a history reopen. Populated for both external provider
|
|
264
|
+
* tools (in handleToolResult) and native server tools (server_tool_complete).
|
|
265
|
+
*/
|
|
266
|
+
readonly toolActivityMetadata: Map<string, ToolActivityMetadata>;
|
|
241
267
|
/** tool_use_ids emitted in the current turn (populated in handleToolUse, cleared after annotation). */
|
|
242
268
|
currentTurnToolUseIds: string[];
|
|
243
269
|
/** Wall-clock time (ms since epoch) when the agent loop turn started, used as the display timestamp for assistant messages. */
|
|
@@ -246,12 +272,38 @@ export interface EventHandlerState {
|
|
|
246
272
|
readonly serverToolStartedAt: Map<string, number>;
|
|
247
273
|
/** Original input from server_tool_start, keyed by tool_use_id, so the complete handler can read the query. */
|
|
248
274
|
readonly serverToolInputs: Map<string, Record<string, unknown>>;
|
|
275
|
+
/** Request ids for which a user-facing web_search backend-failure notice was already surfaced this turn (dedup noisy repeats). Keyed by request id; each turn has a fresh request id, so this grows at most one entry per turn. */
|
|
276
|
+
readonly webSearchBackendFailureNotified: Set<string>;
|
|
249
277
|
/** Active debounce timer for partial persistence; `undefined` when idle. */
|
|
250
278
|
pendingPartialFlushTimer: ReturnType<typeof setTimeout> | undefined;
|
|
251
279
|
/** In-flight partial flush write awaited at finalize to avoid overwrite races. */
|
|
252
280
|
pendingPartialFlushPromise: Promise<void> | undefined;
|
|
253
|
-
/**
|
|
281
|
+
/**
|
|
282
|
+
* Running mirror of the in-flight assistant message's streamed content
|
|
283
|
+
* (text and thinking), flushed to the assistant row on the partial-persist
|
|
284
|
+
* debounce so a mid-turn snapshot reflects what the user is watching live.
|
|
285
|
+
*/
|
|
254
286
|
currentMessageContent: ContentBlock[];
|
|
287
|
+
/**
|
|
288
|
+
* `seq` of the most recent streamed content delta mirrored into
|
|
289
|
+
* `currentMessageContent`. Recorded as the conversation's persisted `seq`
|
|
290
|
+
* after each flush commits (the debounced partial flushes and the
|
|
291
|
+
* `message_complete` finalize), so the snapshot's advertised `seq` tracks
|
|
292
|
+
* exactly the streamed content the durable row holds. `undefined` until the
|
|
293
|
+
* first content delta of the in-flight message. Because every streamed
|
|
294
|
+
* content type rides the same mirror-and-flush path, this single field
|
|
295
|
+
* never claims content a flush has not yet written.
|
|
296
|
+
*/
|
|
297
|
+
lastPersistedContentSeq: number | undefined;
|
|
298
|
+
/**
|
|
299
|
+
* Whether the reducer has compacted `ctx.messages`, gating the Slack
|
|
300
|
+
* chronological-transcript override on re-injection. The captured
|
|
301
|
+
* transcript is the full persisted history; blindly replaying it after
|
|
302
|
+
* compaction would overwrite the reduced messages and undo compaction, so
|
|
303
|
+
* once this is `true` the override falls back to the reduced
|
|
304
|
+
* `ctx.messages`.
|
|
305
|
+
*/
|
|
306
|
+
reducerCompacted: boolean;
|
|
255
307
|
}
|
|
256
308
|
|
|
257
309
|
/** Immutable context shared across event handlers within a single agent loop run. */
|
|
@@ -265,6 +317,18 @@ export interface EventHandlerDeps {
|
|
|
265
317
|
readonly rlog: pino.Logger;
|
|
266
318
|
readonly turnChannelContext: TurnChannelContext;
|
|
267
319
|
readonly turnInterfaceContext: TurnInterfaceContext;
|
|
320
|
+
/**
|
|
321
|
+
* Commit a successful inline compaction to durable state. Invoked from the
|
|
322
|
+
* `compaction_completed` dispatch case (when `result.compacted`) with the
|
|
323
|
+
* loop's compaction result and the stripped pre-compaction `basis`. Supplied
|
|
324
|
+
* by the orchestrator because the body writes Conversation DB-record fields,
|
|
325
|
+
* projects Slack provenance, and emits transport the loop is intentionally
|
|
326
|
+
* blind to.
|
|
327
|
+
*/
|
|
328
|
+
readonly applyCompaction: (
|
|
329
|
+
result: ContextWindowResult,
|
|
330
|
+
basis: Message[],
|
|
331
|
+
) => Promise<void>;
|
|
268
332
|
}
|
|
269
333
|
|
|
270
334
|
// ── Factory ──────────────────────────────────────────────────────────
|
|
@@ -272,6 +336,7 @@ export interface EventHandlerDeps {
|
|
|
272
336
|
export function createEventHandlerState(): EventHandlerState {
|
|
273
337
|
return {
|
|
274
338
|
llmCallStartedEmitted: false,
|
|
339
|
+
pendingNotifiedInferenceProfile: null,
|
|
275
340
|
pendingDirectiveDisplayBuffer: "",
|
|
276
341
|
firstAssistantText: "",
|
|
277
342
|
exchangeProviderName: undefined,
|
|
@@ -292,6 +357,7 @@ export function createEventHandlerState(): EventHandlerState {
|
|
|
292
357
|
lastAssistantMessageId: undefined,
|
|
293
358
|
assistantRowAwaitingFinalization: false,
|
|
294
359
|
pendingToolResults: new Map(),
|
|
360
|
+
pendingToolResultRowReservation: undefined,
|
|
295
361
|
persistedToolUseIds: new Set(),
|
|
296
362
|
accumulatedDirectives: [],
|
|
297
363
|
accumulatedToolContentBlocks: [],
|
|
@@ -307,22 +373,27 @@ export function createEventHandlerState(): EventHandlerState {
|
|
|
307
373
|
requestIdToToolUseId: new Map(),
|
|
308
374
|
toolConfirmationOutcomes: new Map(),
|
|
309
375
|
toolRiskOutcomes: new Map(),
|
|
376
|
+
toolActivityMetadata: new Map(),
|
|
310
377
|
currentTurnToolUseIds: [],
|
|
311
378
|
turnStartedAt: Date.now(),
|
|
312
379
|
serverToolStartedAt: new Map(),
|
|
313
380
|
serverToolInputs: new Map(),
|
|
381
|
+
webSearchBackendFailureNotified: new Set(),
|
|
314
382
|
pendingPartialFlushTimer: undefined,
|
|
315
383
|
pendingPartialFlushPromise: undefined,
|
|
316
384
|
currentMessageContent: [],
|
|
385
|
+
lastPersistedContentSeq: undefined,
|
|
386
|
+
reducerCompacted: false,
|
|
317
387
|
};
|
|
318
388
|
}
|
|
319
389
|
|
|
320
390
|
// ── Partial-persistence helpers ──────────────────────────────────────
|
|
321
391
|
|
|
322
392
|
/** Canonical persisted-content build: clean → append surfaces → redact. */
|
|
323
|
-
function buildPersistedAssistantContent(
|
|
393
|
+
export function buildPersistedAssistantContent(
|
|
324
394
|
rawBlocks: readonly ContentBlock[],
|
|
325
395
|
surfaces: readonly AssistantSurface[],
|
|
396
|
+
activityByToolUseId?: ReadonlyMap<string, ToolActivityMetadata>,
|
|
326
397
|
): ContentBlock[] {
|
|
327
398
|
const { cleanedContent } = cleanAssistantContent(rawBlocks);
|
|
328
399
|
const cleaned = cleanedContent as ContentBlock[];
|
|
@@ -345,6 +416,18 @@ function buildPersistedAssistantContent(
|
|
|
345
416
|
const tb = block as Extract<ContentBlock, { type: "text" }>;
|
|
346
417
|
return { ...tb, text: redactSecrets(tb.text) };
|
|
347
418
|
}
|
|
419
|
+
// Native server tools (Anthropic web_search) resolve mid-stream — their
|
|
420
|
+
// `server_tool_complete` fires before `message_complete` — so the captured
|
|
421
|
+
// activity is available at persist time. Stamp it on the server_tool_use
|
|
422
|
+
// block here so the web-search card survives a history reopen. External
|
|
423
|
+
// tool_use activity arrives only with the later tool_result, so it is
|
|
424
|
+
// stamped in `annotatePersistedAssistantMessage` instead.
|
|
425
|
+
if (block.type === "server_tool_use" && activityByToolUseId) {
|
|
426
|
+
const activity = activityByToolUseId.get(block.id);
|
|
427
|
+
if (activity) {
|
|
428
|
+
return { ...block, _activityMetadata: activity } as ContentBlock;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
348
431
|
return block;
|
|
349
432
|
});
|
|
350
433
|
}
|
|
@@ -363,6 +446,30 @@ function appendTextToCurrentMessage(
|
|
|
363
446
|
}
|
|
364
447
|
}
|
|
365
448
|
|
|
449
|
+
/**
|
|
450
|
+
* Append a streamed thinking chunk to `state.currentMessageContent`, fusing
|
|
451
|
+
* into the tail thinking block. The streamed delta carries no provider
|
|
452
|
+
* `signature` (that arrives only when the block closes), so the mirrored block
|
|
453
|
+
* holds an empty one; `message_complete` overwrites the row with the
|
|
454
|
+
* authoritative signed content before it is ever sent back to a provider.
|
|
455
|
+
*/
|
|
456
|
+
function appendThinkingToCurrentMessage(
|
|
457
|
+
state: EventHandlerState,
|
|
458
|
+
thinking: string,
|
|
459
|
+
): void {
|
|
460
|
+
if (thinking.length === 0) return;
|
|
461
|
+
const tail = state.currentMessageContent.at(-1);
|
|
462
|
+
if (tail && tail.type === "thinking") {
|
|
463
|
+
tail.thinking = tail.thinking + thinking;
|
|
464
|
+
} else {
|
|
465
|
+
state.currentMessageContent.push({
|
|
466
|
+
type: "thinking",
|
|
467
|
+
thinking,
|
|
468
|
+
signature: "",
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
366
473
|
/** Reset partial-persist accumulator and any pending flush state. Idempotent. */
|
|
367
474
|
function resetPartialPersistAccumulator(state: EventHandlerState): void {
|
|
368
475
|
if (state.pendingPartialFlushTimer !== undefined) {
|
|
@@ -370,10 +477,11 @@ function resetPartialPersistAccumulator(state: EventHandlerState): void {
|
|
|
370
477
|
state.pendingPartialFlushTimer = undefined;
|
|
371
478
|
}
|
|
372
479
|
state.currentMessageContent = [];
|
|
480
|
+
state.lastPersistedContentSeq = undefined;
|
|
373
481
|
state.pendingPartialFlushPromise = undefined;
|
|
374
482
|
}
|
|
375
483
|
|
|
376
|
-
/** Flush `state.currentMessageContent` to the
|
|
484
|
+
/** Flush `state.currentMessageContent` to the persisted assistant row. */
|
|
377
485
|
async function flushAccumulatedContent(
|
|
378
486
|
state: EventHandlerState,
|
|
379
487
|
deps: EventHandlerDeps,
|
|
@@ -382,22 +490,24 @@ async function flushAccumulatedContent(
|
|
|
382
490
|
if (messageId === undefined) return;
|
|
383
491
|
if (state.currentMessageContent.length === 0) return;
|
|
384
492
|
|
|
385
|
-
const built = buildPersistedAssistantContent(
|
|
493
|
+
const built = buildPersistedAssistantContent(
|
|
494
|
+
state.currentMessageContent,
|
|
495
|
+
[],
|
|
496
|
+
state.toolActivityMetadata,
|
|
497
|
+
);
|
|
386
498
|
const contentJson = JSON.stringify(built);
|
|
499
|
+
// Pair the seq with the exact content snapshot taken above: deltas that
|
|
500
|
+
// arrive while the write is in flight bump `lastPersistedContentSeq`
|
|
501
|
+
// again, but they are not part of this write.
|
|
502
|
+
const flushedSeq = state.lastPersistedContentSeq;
|
|
387
503
|
|
|
388
504
|
try {
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
messageId,
|
|
396
|
-
content: contentJson,
|
|
397
|
-
},
|
|
398
|
-
buildHandlerTurnContext(deps),
|
|
399
|
-
DEFAULT_TIMEOUTS.persistence,
|
|
400
|
-
);
|
|
505
|
+
updateMessageContent(messageId, contentJson);
|
|
506
|
+
// Record only after the write commits, so the snapshot seq never
|
|
507
|
+
// claims content that is not yet durable.
|
|
508
|
+
if (flushedSeq != null) {
|
|
509
|
+
recordPersistedSeq(deps.ctx.conversationId, flushedSeq);
|
|
510
|
+
}
|
|
401
511
|
} catch (err) {
|
|
402
512
|
deps.rlog.warn(
|
|
403
513
|
{ err, messageId },
|
|
@@ -633,12 +743,6 @@ export async function handleLlmCallStarted(
|
|
|
633
743
|
// the `assistantRowAwaitingFinalization` flag — `handleMessageComplete`
|
|
634
744
|
// clears it after the successful `updateContent`, so the previous call's
|
|
635
745
|
// committed row is never touched here.
|
|
636
|
-
//
|
|
637
|
-
// Direct `deleteMessageById` (not via the `persistence` pipeline) is
|
|
638
|
-
// intentional: a never-finalized reservation has no segments, no
|
|
639
|
-
// attachments, and no observable history — undoing it isn't a real
|
|
640
|
-
// persistence event for plugins to react to, so routing through the
|
|
641
|
-
// pipeline would only widen the mock surface for no observability win.
|
|
642
746
|
if (state.assistantRowAwaitingFinalization && state.lastAssistantMessageId) {
|
|
643
747
|
try {
|
|
644
748
|
deleteMessageById(state.lastAssistantMessageId);
|
|
@@ -652,20 +756,12 @@ export async function handleLlmCallStarted(
|
|
|
652
756
|
}
|
|
653
757
|
|
|
654
758
|
const metadata = buildAssistantChannelMetadata(state, deps);
|
|
655
|
-
const
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
conversationId: deps.ctx.conversationId,
|
|
662
|
-
role: "assistant",
|
|
663
|
-
metadata,
|
|
664
|
-
},
|
|
665
|
-
buildHandlerTurnContext(deps),
|
|
666
|
-
DEFAULT_TIMEOUTS.persistence,
|
|
667
|
-
)) as PersistReserveResult;
|
|
668
|
-
state.lastAssistantMessageId = reserveResult.message.id;
|
|
759
|
+
const reservedRow = await reserveMessage(
|
|
760
|
+
deps.ctx.conversationId,
|
|
761
|
+
"assistant",
|
|
762
|
+
metadata,
|
|
763
|
+
);
|
|
764
|
+
state.lastAssistantMessageId = reservedRow.id;
|
|
669
765
|
state.assistantRowAwaitingFinalization = true;
|
|
670
766
|
// Fresh row → fresh accumulator. If an earlier (failed) LLM call
|
|
671
767
|
// within the same run left partial state behind, the
|
|
@@ -675,7 +771,7 @@ export async function handleLlmCallStarted(
|
|
|
675
771
|
resetPartialPersistAccumulator(state);
|
|
676
772
|
deps.onEvent({
|
|
677
773
|
type: "assistant_turn_start",
|
|
678
|
-
messageId:
|
|
774
|
+
messageId: reservedRow.id,
|
|
679
775
|
conversationId: deps.ctx.conversationId,
|
|
680
776
|
});
|
|
681
777
|
}
|
|
@@ -711,6 +807,11 @@ function handleTextDelta(
|
|
|
711
807
|
// Mirror the drained delta into state.currentMessageContent so partial
|
|
712
808
|
// flushes mid-turn see the same content the user is watching live.
|
|
713
809
|
appendTextToCurrentMessage(state, drained.emitText);
|
|
810
|
+
// The hub stamps `seq` synchronously on the delta emitted above, so
|
|
811
|
+
// `getCurrentSeq()` here is that delta's seq -- the position the
|
|
812
|
+
// mirrored content now reflects. A partial flush snapshots this to
|
|
813
|
+
// record how far the durable rows track the live stream.
|
|
814
|
+
state.lastPersistedContentSeq = getCurrentSeq();
|
|
714
815
|
schedulePartialFlush(state, deps);
|
|
715
816
|
}
|
|
716
817
|
}
|
|
@@ -746,6 +847,14 @@ function handleThinkingDelta(
|
|
|
746
847
|
conversationId: deps.ctx.conversationId,
|
|
747
848
|
messageId: state.lastAssistantMessageId,
|
|
748
849
|
});
|
|
850
|
+
// Mirror thinking into the same running view as text so the debounced
|
|
851
|
+
// partial flush persists it mid-turn -- long reasoning streams survive a
|
|
852
|
+
// refresh that outlives the SSE replay window, exactly as long answers do.
|
|
853
|
+
appendThinkingToCurrentMessage(state, event.thinking);
|
|
854
|
+
// The hub stamps `seq` synchronously on the delta emitted above, so
|
|
855
|
+
// `getCurrentSeq()` is that delta's position in the mirrored content.
|
|
856
|
+
state.lastPersistedContentSeq = getCurrentSeq();
|
|
857
|
+
schedulePartialFlush(state, deps);
|
|
749
858
|
}
|
|
750
859
|
|
|
751
860
|
export function handleToolUse(
|
|
@@ -773,6 +882,14 @@ export function handleToolUse(
|
|
|
773
882
|
toolUseId: event.id,
|
|
774
883
|
messageId: state.lastAssistantMessageId,
|
|
775
884
|
});
|
|
885
|
+
// `message_complete` always precedes tool events (see handleMessageComplete),
|
|
886
|
+
// so this tool_use block is already durable in the assistant row. The
|
|
887
|
+
// `tool_use_start` emitted just above is therefore the newest stamped event
|
|
888
|
+
// whose content the `/messages` snapshot already reflects -- advance the
|
|
889
|
+
// persisted seq to it. Without this the snapshot would advertise a seq below
|
|
890
|
+
// an event it already incorporates, and a client applying `seq > snapshot.seq`
|
|
891
|
+
// would replay this tool start.
|
|
892
|
+
recordPersistedSeq(deps.ctx.conversationId, getCurrentSeq());
|
|
776
893
|
}
|
|
777
894
|
|
|
778
895
|
export function handleToolUsePreviewStart(
|
|
@@ -890,11 +1007,244 @@ export function handleInputJsonDelta(
|
|
|
890
1007
|
});
|
|
891
1008
|
}
|
|
892
1009
|
|
|
893
|
-
|
|
1010
|
+
/**
|
|
1011
|
+
* Build the persisted `tool_result` content blocks for the buffered results,
|
|
1012
|
+
* redacting secrets from both the flat content and any structured blocks. All
|
|
1013
|
+
* results of one assistant turn share a single `user` row (the shape providers
|
|
1014
|
+
* expect for tool_result-in-user-turn).
|
|
1015
|
+
*/
|
|
1016
|
+
function buildToolResultBlocks(
|
|
1017
|
+
pending: ReadonlyMap<string, PendingToolResult>,
|
|
1018
|
+
) {
|
|
1019
|
+
return Array.from(pending.entries()).map(([toolUseId, result]) => ({
|
|
1020
|
+
type: "tool_result",
|
|
1021
|
+
tool_use_id: toolUseId,
|
|
1022
|
+
content: redactSecrets(result.content),
|
|
1023
|
+
is_error: result.isError,
|
|
1024
|
+
...(result.contentBlocks
|
|
1025
|
+
? {
|
|
1026
|
+
contentBlocks: result.contentBlocks.map((block) =>
|
|
1027
|
+
block.type === "text"
|
|
1028
|
+
? { ...block, text: redactSecrets(block.text) }
|
|
1029
|
+
: block,
|
|
1030
|
+
),
|
|
1031
|
+
}
|
|
1032
|
+
: {}),
|
|
1033
|
+
}));
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
/**
|
|
1037
|
+
* Channel/interface provenance metadata for the grouped tool-result row,
|
|
1038
|
+
* stamped from the turn context so the row carries the same provenance the
|
|
1039
|
+
* snapshot reflects from the moment it lands in SQLite.
|
|
1040
|
+
*/
|
|
1041
|
+
function buildToolResultMetadata(
|
|
1042
|
+
deps: EventHandlerDeps,
|
|
1043
|
+
): Record<string, unknown> {
|
|
1044
|
+
return {
|
|
1045
|
+
...provenanceFromTrustContext(deps.ctx.trustContext),
|
|
1046
|
+
userMessageChannel: deps.turnChannelContext.userMessageChannel,
|
|
1047
|
+
assistantMessageChannel: deps.turnChannelContext.assistantMessageChannel,
|
|
1048
|
+
userMessageInterface: deps.turnInterfaceContext.userMessageInterface,
|
|
1049
|
+
assistantMessageInterface:
|
|
1050
|
+
deps.turnInterfaceContext.assistantMessageInterface,
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
/**
|
|
1055
|
+
* Reserve the grouped `user` tool-result row for the current batch exactly
|
|
1056
|
+
* once. Parallel tool results are dispatched without awaiting (`agent/loop.ts`
|
|
1057
|
+
* emits each `tool_result` synchronously), so concurrent `handleToolResult`
|
|
1058
|
+
* calls can reach this before the first reservation resolves; sharing one
|
|
1059
|
+
* in-flight reservation promise keeps the whole batch in a single row. A
|
|
1060
|
+
* failed reservation resets the promise so the next caller can retry rather
|
|
1061
|
+
* than inheriting a settled rejection.
|
|
1062
|
+
*/
|
|
1063
|
+
function ensureToolResultRowReserved(
|
|
1064
|
+
state: EventHandlerState,
|
|
1065
|
+
conversationId: string,
|
|
1066
|
+
metadata: Record<string, unknown>,
|
|
1067
|
+
): Promise<string> {
|
|
1068
|
+
if (state.pendingToolResultRowReservation === undefined) {
|
|
1069
|
+
state.pendingToolResultRowReservation = reserveMessage(
|
|
1070
|
+
conversationId,
|
|
1071
|
+
"user",
|
|
1072
|
+
metadata,
|
|
1073
|
+
)
|
|
1074
|
+
.then((reserved) => reserved.id)
|
|
1075
|
+
.catch((err) => {
|
|
1076
|
+
state.pendingToolResultRowReservation = undefined;
|
|
1077
|
+
throw err;
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
return state.pendingToolResultRowReservation;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
/**
|
|
1084
|
+
* Persist the buffered tool results into their grouped `user` row as each
|
|
1085
|
+
* result arrives, so a long-running tool's output survives a refresh that
|
|
1086
|
+
* outlives the SSE replay window. The row is reserved once per batch and
|
|
1087
|
+
* rewritten in place as sibling parallel results land, keeping all
|
|
1088
|
+
* `tool_result` blocks of one turn in a single message. `seq` is the position
|
|
1089
|
+
* stamped on the triggering `tool_result` event, captured by the caller before
|
|
1090
|
+
* any await so it reflects exactly the content now durable in the row.
|
|
1091
|
+
* Indexing and the buffer drain are deferred to `finalizePendingToolResultRow`.
|
|
1092
|
+
*/
|
|
1093
|
+
async function persistPendingToolResultRow(
|
|
1094
|
+
state: EventHandlerState,
|
|
1095
|
+
deps: EventHandlerDeps,
|
|
1096
|
+
seq: number,
|
|
1097
|
+
): Promise<void> {
|
|
1098
|
+
if (state.pendingToolResults.size === 0) return;
|
|
1099
|
+
const rowId = await ensureToolResultRowReserved(
|
|
1100
|
+
state,
|
|
1101
|
+
deps.ctx.conversationId,
|
|
1102
|
+
buildToolResultMetadata(deps),
|
|
1103
|
+
);
|
|
1104
|
+
// Serialize the content after the reservation resolves so the last of the
|
|
1105
|
+
// concurrent writers reflects the fullest batch.
|
|
1106
|
+
updateMessageContent(
|
|
1107
|
+
rowId,
|
|
1108
|
+
JSON.stringify(buildToolResultBlocks(state.pendingToolResults)),
|
|
1109
|
+
);
|
|
1110
|
+
recordPersistedSeq(deps.ctx.conversationId, seq);
|
|
1111
|
+
const conv = getConversation(deps.ctx.conversationId);
|
|
1112
|
+
if (conv != null) {
|
|
1113
|
+
syncMessageToDisk(deps.ctx.conversationId, rowId, conv.createdAt);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
/**
|
|
1118
|
+
* Finalize the grouped tool-result row at a turn/loop boundary: ensure the row
|
|
1119
|
+
* is reserved (a fallback for the case where every on-arrival write failed),
|
|
1120
|
+
* rewrite it to the full batch, sync it to disk, index it for memory recall,
|
|
1121
|
+
* and clear the batch state. Shared by `message_complete` and the orchestrator
|
|
1122
|
+
* loop-exit flush so an aborted or yielded turn finalizes the same reserved row
|
|
1123
|
+
* instead of writing a duplicate.
|
|
1124
|
+
*/
|
|
1125
|
+
export async function finalizePendingToolResultRow(
|
|
1126
|
+
state: EventHandlerState,
|
|
1127
|
+
conversationId: string,
|
|
1128
|
+
metadata: Record<string, unknown>,
|
|
1129
|
+
rlog: pino.Logger,
|
|
1130
|
+
): Promise<void> {
|
|
1131
|
+
if (state.pendingToolResults.size === 0) return;
|
|
1132
|
+
const rowId = await ensureToolResultRowReserved(
|
|
1133
|
+
state,
|
|
1134
|
+
conversationId,
|
|
1135
|
+
metadata,
|
|
1136
|
+
);
|
|
1137
|
+
const contentJson = JSON.stringify(
|
|
1138
|
+
buildToolResultBlocks(state.pendingToolResults),
|
|
1139
|
+
);
|
|
1140
|
+
updateMessageContent(rowId, contentJson);
|
|
1141
|
+
// Sync the row to the JSONL disk view so it stays in lockstep with the DB.
|
|
1142
|
+
// `getConversation` returns `ConversationRow | null`, so `!= null` gates on a
|
|
1143
|
+
// real row (skipping the sync when the conversation was not found rather than
|
|
1144
|
+
// asking the disk-view to resolve a missing id).
|
|
1145
|
+
const conv = getConversation(conversationId);
|
|
1146
|
+
if (conv != null) {
|
|
1147
|
+
syncMessageToDisk(conversationId, rowId, conv.createdAt);
|
|
1148
|
+
}
|
|
1149
|
+
// `reserveMessage` + `updateMessageContent` are CRUD-only, so index the
|
|
1150
|
+
// finalized tool-result content explicitly here (mirroring the assistant-row
|
|
1151
|
+
// finalize) once it is durable. Non-fatal: a memory hiccup must not escalate
|
|
1152
|
+
// a successful turn into a throw.
|
|
1153
|
+
const row = getMessageById(rowId, conversationId);
|
|
1154
|
+
if (row) {
|
|
1155
|
+
let provenanceTrustClass:
|
|
1156
|
+
| "guardian"
|
|
1157
|
+
| "trusted_contact"
|
|
1158
|
+
| "unknown"
|
|
1159
|
+
| undefined;
|
|
1160
|
+
let automated: boolean | undefined;
|
|
1161
|
+
if (row.metadata) {
|
|
1162
|
+
try {
|
|
1163
|
+
const parsedMeta = messageMetadataSchema.safeParse(
|
|
1164
|
+
JSON.parse(row.metadata),
|
|
1165
|
+
);
|
|
1166
|
+
if (parsedMeta.success) {
|
|
1167
|
+
provenanceTrustClass = parsedMeta.data.provenanceTrustClass;
|
|
1168
|
+
automated = parsedMeta.data.automated;
|
|
1169
|
+
}
|
|
1170
|
+
} catch {
|
|
1171
|
+
// Malformed metadata JSON — index with undefined provenance fields.
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
try {
|
|
1175
|
+
await indexMessageNow(
|
|
1176
|
+
{
|
|
1177
|
+
messageId: rowId,
|
|
1178
|
+
conversationId,
|
|
1179
|
+
role: "user",
|
|
1180
|
+
content: contentJson,
|
|
1181
|
+
createdAt: row.createdAt,
|
|
1182
|
+
scopeId: "default",
|
|
1183
|
+
provenanceTrustClass,
|
|
1184
|
+
automated,
|
|
1185
|
+
},
|
|
1186
|
+
getConfig().memory,
|
|
1187
|
+
);
|
|
1188
|
+
} catch (err) {
|
|
1189
|
+
rlog.warn(
|
|
1190
|
+
{ err, conversationId, messageId: rowId },
|
|
1191
|
+
"Failed to index tool-result message for memory (non-fatal)",
|
|
1192
|
+
);
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
for (const id of state.pendingToolResults.keys()) {
|
|
1196
|
+
state.persistedToolUseIds.add(id);
|
|
1197
|
+
}
|
|
1198
|
+
state.pendingToolResults.clear();
|
|
1199
|
+
state.pendingToolResultRowReservation = undefined;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
export async function handleToolResult(
|
|
894
1203
|
state: EventHandlerState,
|
|
895
1204
|
deps: EventHandlerDeps,
|
|
896
1205
|
event: Extract<AgentEvent, { type: "tool_result" }>,
|
|
897
|
-
): void {
|
|
1206
|
+
): Promise<void> {
|
|
1207
|
+
// A synthesized cancellation (the tool never executed) is captured for
|
|
1208
|
+
// persistence and forwarded to the client like any result, but skips every
|
|
1209
|
+
// side effect that assumes the tool ran. A real result already captured or
|
|
1210
|
+
// persisted for the same tool wins, so only fill genuine gaps.
|
|
1211
|
+
if (event.cancelled) {
|
|
1212
|
+
if (
|
|
1213
|
+
state.pendingToolResults.has(event.toolUseId) ||
|
|
1214
|
+
state.persistedToolUseIds.has(event.toolUseId)
|
|
1215
|
+
) {
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
state.pendingToolResults.set(event.toolUseId, {
|
|
1219
|
+
content: event.content,
|
|
1220
|
+
isError: event.isError,
|
|
1221
|
+
});
|
|
1222
|
+
state.currentToolUseId = undefined;
|
|
1223
|
+
deps.onEvent({
|
|
1224
|
+
type: "tool_result",
|
|
1225
|
+
toolName: "",
|
|
1226
|
+
result: event.content,
|
|
1227
|
+
isError: event.isError,
|
|
1228
|
+
conversationId: deps.ctx.conversationId,
|
|
1229
|
+
messageId: state.lastAssistantMessageId,
|
|
1230
|
+
toolUseId: event.toolUseId,
|
|
1231
|
+
});
|
|
1232
|
+
// Capture the seq synchronously (before the persist await) so it reflects
|
|
1233
|
+
// the just-stamped tool_result event, then persist on arrival. A failure
|
|
1234
|
+
// here is non-fatal: the buffered result is still drained at
|
|
1235
|
+
// `message_complete`.
|
|
1236
|
+
const cancelledSeq = getCurrentSeq();
|
|
1237
|
+
try {
|
|
1238
|
+
await persistPendingToolResultRow(state, deps, cancelledSeq);
|
|
1239
|
+
} catch (err) {
|
|
1240
|
+
log.warn(
|
|
1241
|
+
{ err, conversationId: deps.ctx.conversationId },
|
|
1242
|
+
"Failed to persist cancelled tool result on arrival (non-fatal; retried at message_complete)",
|
|
1243
|
+
);
|
|
1244
|
+
}
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
898
1248
|
const imageBlocks = event.contentBlocks?.filter(
|
|
899
1249
|
(b): b is ImageContent => b.type === "image",
|
|
900
1250
|
);
|
|
@@ -945,6 +1295,13 @@ export function handleToolResult(
|
|
|
945
1295
|
});
|
|
946
1296
|
}
|
|
947
1297
|
|
|
1298
|
+
// Capture tool activity (web_search / web_fetch) so it can be persisted on
|
|
1299
|
+
// the tool_use block and the activity card survives a history reopen,
|
|
1300
|
+
// matching the live tool_result event's activityMetadata.
|
|
1301
|
+
if (event.activityMetadata) {
|
|
1302
|
+
state.toolActivityMetadata.set(event.toolUseId, event.activityMetadata);
|
|
1303
|
+
}
|
|
1304
|
+
|
|
948
1305
|
const toolName = state.toolUseIdToName.get(event.toolUseId);
|
|
949
1306
|
if (toolName === "file_write" || toolName === "bash") {
|
|
950
1307
|
deps.ctx.markWorkspaceTopLevelDirty();
|
|
@@ -1026,6 +1383,20 @@ export function handleToolResult(
|
|
|
1026
1383
|
riskThreshold: event.riskThreshold,
|
|
1027
1384
|
activityMetadata: event.activityMetadata,
|
|
1028
1385
|
});
|
|
1386
|
+
|
|
1387
|
+
// Capture the seq synchronously (before the persist await) so it reflects the
|
|
1388
|
+
// just-stamped tool_result event, then persist the grouped row on arrival. A
|
|
1389
|
+
// failure here is non-fatal: the buffered result is still drained at
|
|
1390
|
+
// `message_complete`.
|
|
1391
|
+
const resultSeq = getCurrentSeq();
|
|
1392
|
+
try {
|
|
1393
|
+
await persistPendingToolResultRow(state, deps, resultSeq);
|
|
1394
|
+
} catch (err) {
|
|
1395
|
+
log.warn(
|
|
1396
|
+
{ err, conversationId: deps.ctx.conversationId },
|
|
1397
|
+
"Failed to persist tool result on arrival (non-fatal; retried at message_complete)",
|
|
1398
|
+
);
|
|
1399
|
+
}
|
|
1029
1400
|
}
|
|
1030
1401
|
|
|
1031
1402
|
/**
|
|
@@ -1097,6 +1468,16 @@ function annotatePersistedAssistantMessage(
|
|
|
1097
1468
|
rec._riskDirectoryScopeOptions = risk.riskDirectoryScopeOptions;
|
|
1098
1469
|
modified = true;
|
|
1099
1470
|
}
|
|
1471
|
+
// External provider tools (brave/perplexity/tavily) + web_fetch produce
|
|
1472
|
+
// their activity only when the tool_result lands, after message_complete
|
|
1473
|
+
// has already persisted this block — so it is stamped here. Native
|
|
1474
|
+
// server_tool_use activity is stamped earlier, at persist time, in
|
|
1475
|
+
// `buildPersistedAssistantContent`.
|
|
1476
|
+
const activity = state.toolActivityMetadata.get(id);
|
|
1477
|
+
if (activity) {
|
|
1478
|
+
rec._activityMetadata = activity;
|
|
1479
|
+
modified = true;
|
|
1480
|
+
}
|
|
1100
1481
|
}
|
|
1101
1482
|
}
|
|
1102
1483
|
|
|
@@ -1259,6 +1640,18 @@ export async function handleMessageComplete(
|
|
|
1259
1640
|
deps: EventHandlerDeps,
|
|
1260
1641
|
event: Extract<AgentEvent, { type: "message_complete" }>,
|
|
1261
1642
|
): Promise<void> {
|
|
1643
|
+
// The model has now received the turn context, so persist any pending
|
|
1644
|
+
// inference-profile-change notification. Guarded by the pending slot so it
|
|
1645
|
+
// fires once per turn; a turn that fails before reaching delivery leaves the
|
|
1646
|
+
// slot unconsumed and re-sends the notice next turn.
|
|
1647
|
+
if (state.pendingNotifiedInferenceProfile != null) {
|
|
1648
|
+
setLastNotifiedInferenceProfile(
|
|
1649
|
+
deps.ctx.conversationId,
|
|
1650
|
+
state.pendingNotifiedInferenceProfile,
|
|
1651
|
+
);
|
|
1652
|
+
state.pendingNotifiedInferenceProfile = null;
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1262
1655
|
// Reset per-turn tool tracking for the new turn.
|
|
1263
1656
|
state.currentTurnToolUseIds = [];
|
|
1264
1657
|
|
|
@@ -1298,62 +1691,16 @@ export async function handleMessageComplete(
|
|
|
1298
1691
|
state.pendingDirectiveDisplayBuffer = "";
|
|
1299
1692
|
}
|
|
1300
1693
|
|
|
1301
|
-
//
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
contentBlocks: result.contentBlocks.map((block) =>
|
|
1312
|
-
block.type === "text"
|
|
1313
|
-
? { ...block, text: redactSecrets(block.text) }
|
|
1314
|
-
: block,
|
|
1315
|
-
),
|
|
1316
|
-
}
|
|
1317
|
-
: {}),
|
|
1318
|
-
}),
|
|
1319
|
-
);
|
|
1320
|
-
const toolResultMetadata = {
|
|
1321
|
-
...provenanceFromTrustContext(deps.ctx.trustContext),
|
|
1322
|
-
userMessageChannel: deps.turnChannelContext.userMessageChannel,
|
|
1323
|
-
assistantMessageChannel: deps.turnChannelContext.assistantMessageChannel,
|
|
1324
|
-
userMessageInterface: deps.turnInterfaceContext.userMessageInterface,
|
|
1325
|
-
assistantMessageInterface:
|
|
1326
|
-
deps.turnInterfaceContext.assistantMessageInterface,
|
|
1327
|
-
};
|
|
1328
|
-
// Route the add + disk-view sync through the `persistence` pipeline so
|
|
1329
|
-
// plugins can observe or override both operations together. The default
|
|
1330
|
-
// plugin's terminal performs the add and, when `syncToDisk` is true,
|
|
1331
|
-
// immediately calls `syncMessageToDisk` against the just-persisted row.
|
|
1332
|
-
// `getConversation` returns `ConversationRow | null`, so `!= null`
|
|
1333
|
-
// gates on a real row (skipping the sync when the conversation was
|
|
1334
|
-
// not found rather than asking the disk-view to resolve a missing id).
|
|
1335
|
-
const convForToolResult = getConversation(deps.ctx.conversationId);
|
|
1336
|
-
await runPipeline<PersistArgs, PersistResult>(
|
|
1337
|
-
"persistence",
|
|
1338
|
-
getMiddlewaresFor("persistence"),
|
|
1339
|
-
defaultPersistenceTerminal,
|
|
1340
|
-
{
|
|
1341
|
-
op: "add",
|
|
1342
|
-
conversationId: deps.ctx.conversationId,
|
|
1343
|
-
role: "user",
|
|
1344
|
-
content: JSON.stringify(toolResultBlocks),
|
|
1345
|
-
metadata: toolResultMetadata,
|
|
1346
|
-
syncToDisk: convForToolResult != null,
|
|
1347
|
-
createdAtMs: convForToolResult?.createdAt,
|
|
1348
|
-
},
|
|
1349
|
-
buildHandlerTurnContext(deps),
|
|
1350
|
-
DEFAULT_TIMEOUTS.persistence,
|
|
1351
|
-
);
|
|
1352
|
-
for (const id of state.pendingToolResults.keys()) {
|
|
1353
|
-
state.persistedToolUseIds.add(id);
|
|
1354
|
-
}
|
|
1355
|
-
state.pendingToolResults.clear();
|
|
1356
|
-
}
|
|
1694
|
+
// Finalize the grouped tool-result row. Each result was persisted into this
|
|
1695
|
+
// row as it arrived (`persistPendingToolResultRow`); this rewrites it to the
|
|
1696
|
+
// full batch (covering the case where a mid-arrival write failed), indexes it
|
|
1697
|
+
// for memory recall, and clears the batch state.
|
|
1698
|
+
await finalizePendingToolResultRow(
|
|
1699
|
+
state,
|
|
1700
|
+
deps.ctx.conversationId,
|
|
1701
|
+
buildToolResultMetadata(deps),
|
|
1702
|
+
deps.rlog,
|
|
1703
|
+
);
|
|
1357
1704
|
|
|
1358
1705
|
// Accumulate directives + warnings from the assistant content for
|
|
1359
1706
|
// downstream attachment processing. `cleanAssistantContent` is also
|
|
@@ -1391,6 +1738,7 @@ export async function handleMessageComplete(
|
|
|
1391
1738
|
const contentForPersistence = buildPersistedAssistantContent(
|
|
1392
1739
|
event.message.content as ContentBlock[],
|
|
1393
1740
|
deps.ctx.currentTurnSurfaces,
|
|
1741
|
+
state.toolActivityMetadata,
|
|
1394
1742
|
);
|
|
1395
1743
|
|
|
1396
1744
|
// The row was reserved at `llm_call_started` (with channel metadata
|
|
@@ -1407,28 +1755,30 @@ export async function handleMessageComplete(
|
|
|
1407
1755
|
);
|
|
1408
1756
|
}
|
|
1409
1757
|
const contentJson = JSON.stringify(contentForPersistence);
|
|
1410
|
-
|
|
1411
|
-
"persistence",
|
|
1412
|
-
getMiddlewaresFor("persistence"),
|
|
1413
|
-
defaultPersistenceTerminal,
|
|
1414
|
-
{
|
|
1415
|
-
op: "updateContent",
|
|
1416
|
-
messageId: assistantMessageId,
|
|
1417
|
-
content: contentJson,
|
|
1418
|
-
},
|
|
1419
|
-
buildHandlerTurnContext(deps),
|
|
1420
|
-
DEFAULT_TIMEOUTS.persistence,
|
|
1421
|
-
);
|
|
1758
|
+
updateMessageContent(assistantMessageId, contentJson);
|
|
1422
1759
|
state.assistantRowAwaitingFinalization = false;
|
|
1760
|
+
// The assistant row now holds the authoritative content (text + thinking +
|
|
1761
|
+
// tool_use blocks from `event.message`), and any drained tool-result rows
|
|
1762
|
+
// are durable. `lastPersistedContentSeq` is the last streamed text/thinking
|
|
1763
|
+
// delta's seq -- the highest stamped content event this row reflects -- so
|
|
1764
|
+
// recording it is honest. A drained tool result was stamped earlier in the
|
|
1765
|
+
// turn, so this seq already covers it; a call that streams no content (a
|
|
1766
|
+
// pure tool call) advances instead via `tool_use_start`. `recordPersistedSeq`
|
|
1767
|
+
// clamps monotonically, so a lower value here never regresses the seq.
|
|
1768
|
+
if (state.lastPersistedContentSeq != null) {
|
|
1769
|
+
recordPersistedSeq(deps.ctx.conversationId, state.lastPersistedContentSeq);
|
|
1770
|
+
}
|
|
1423
1771
|
// Reset the partial-persist mirror so subsequent calls in this turn
|
|
1424
1772
|
// start with an empty running view.
|
|
1425
1773
|
state.currentMessageContent = [];
|
|
1774
|
+
state.lastPersistedContentSeq = undefined;
|
|
1426
1775
|
|
|
1427
|
-
// ── Indexing + attention projection
|
|
1776
|
+
// ── Indexing + attention projection ──
|
|
1428
1777
|
// `reserveMessage` + `updateMessageContent` are CRUD-only: they don't run
|
|
1429
|
-
// the memory indexer or the attention-cursor projector
|
|
1430
|
-
//
|
|
1431
|
-
//
|
|
1778
|
+
// the memory indexer or the attention-cursor projector (unlike `addMessage`,
|
|
1779
|
+
// which runs both as side-effects of the insert). Because the assistant row
|
|
1780
|
+
// is reserved empty and finalized via `updateMessageContent`, both must be
|
|
1781
|
+
// invoked explicitly here to keep the assistant row's external state
|
|
1432
1782
|
// (Qdrant segments, conversation attention cursor) in lockstep with the
|
|
1433
1783
|
// finalized content. Both are non-fatal — a memory hiccup must not
|
|
1434
1784
|
// escalate a successful generation into a turn-level throw. Indexing
|
|
@@ -1759,7 +2109,7 @@ export async function dispatchAgentEvent(
|
|
|
1759
2109
|
handleInputJsonDelta(state, deps, event);
|
|
1760
2110
|
break;
|
|
1761
2111
|
case "tool_result":
|
|
1762
|
-
handleToolResult(state, deps, event);
|
|
2112
|
+
await handleToolResult(state, deps, event);
|
|
1763
2113
|
break;
|
|
1764
2114
|
case "server_tool_start": {
|
|
1765
2115
|
const query =
|
|
@@ -1828,9 +2178,65 @@ export async function dispatchAgentEvent(
|
|
|
1828
2178
|
// for them would mis-label the provider and ship empty results.
|
|
1829
2179
|
const isAnthropicNative = deps.ctx.provider.name === "anthropic";
|
|
1830
2180
|
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
2181
|
+
// Classify provider failures through the shared normalizer so the same
|
|
2182
|
+
// friendly copy propagates to every client via WebSearchMetadata, while
|
|
2183
|
+
// the raw provider detail stays in telemetry only (ATL-727).
|
|
2184
|
+
const classification = classifyWebSearchFailure({
|
|
2185
|
+
errorCode: event.errorCode,
|
|
2186
|
+
error: event.errorMessage,
|
|
2187
|
+
isError: event.isError,
|
|
2188
|
+
hasResults: results.length > 0,
|
|
2189
|
+
});
|
|
2190
|
+
|
|
2191
|
+
let errorMessage: string | undefined;
|
|
2192
|
+
let fallbackShown = false;
|
|
2193
|
+
if (event.isError) {
|
|
2194
|
+
// A genuine backend failure OR an unclassifiable, message-less native
|
|
2195
|
+
// failure (e.g. `isError:true` with no `error_code`) both surface the
|
|
2196
|
+
// friendly backend copy: a terse "Search failed" placeholder is the
|
|
2197
|
+
// confusing copy this normalization exists to eliminate (ATL-727).
|
|
2198
|
+
// Recoverable categories that carry a real user message
|
|
2199
|
+
// (query_too_long, max_uses_exceeded) keep their own copy.
|
|
2200
|
+
const useBackendCopy =
|
|
2201
|
+
classification.isBackendFailure || !classification.userMessage;
|
|
2202
|
+
if (useBackendCopy) {
|
|
2203
|
+
// Dedup the user-facing friendly notice per turn (request id) so a
|
|
2204
|
+
// burst of failures surfaces at most one full notice. The raw
|
|
2205
|
+
// provider error is preserved on every failure via telemetry below.
|
|
2206
|
+
const alreadyNotified = state.webSearchBackendFailureNotified.has(
|
|
2207
|
+
deps.reqId,
|
|
2208
|
+
);
|
|
2209
|
+
if (alreadyNotified) {
|
|
2210
|
+
errorMessage = "Search is still having trouble.";
|
|
2211
|
+
} else {
|
|
2212
|
+
state.webSearchBackendFailureNotified.add(deps.reqId);
|
|
2213
|
+
errorMessage = WEB_SEARCH_BACKEND_FAILURE_MESSAGE;
|
|
2214
|
+
fallbackShown = true;
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
// Backend-failure telemetry (provider outages / rate limits) must
|
|
2218
|
+
// fire only for genuine backend classifications so it does not
|
|
2219
|
+
// count recoverable input/quota errors — or a message-less unknown
|
|
2220
|
+
// failure that merely borrows the friendly copy — as provider
|
|
2221
|
+
// outages.
|
|
2222
|
+
if (classification.isBackendFailure) {
|
|
2223
|
+
logWebSearchBackendFailure(deps.rlog, {
|
|
2224
|
+
provider: isAnthropicNative
|
|
2225
|
+
? "anthropic-native"
|
|
2226
|
+
: deps.ctx.provider.name,
|
|
2227
|
+
requestId: deps.reqId,
|
|
2228
|
+
errorCategory: classification.category,
|
|
2229
|
+
rawDetail: classification.rawDetail,
|
|
2230
|
+
fallbackShown,
|
|
2231
|
+
queryLength: query.length,
|
|
2232
|
+
});
|
|
2233
|
+
}
|
|
2234
|
+
} else {
|
|
2235
|
+
// Recoverable, non-backend categories with their own user-facing
|
|
2236
|
+
// copy (query_too_long, max_uses_exceeded) keep that message.
|
|
2237
|
+
errorMessage = classification.userMessage;
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
1834
2240
|
|
|
1835
2241
|
const metadata: WebSearchMetadata | undefined = isAnthropicNative
|
|
1836
2242
|
? {
|
|
@@ -1847,6 +2253,14 @@ export async function dispatchAgentEvent(
|
|
|
1847
2253
|
.map((r) => `${r.title}\n${r.url}`)
|
|
1848
2254
|
.join("\n\n");
|
|
1849
2255
|
|
|
2256
|
+
// Capture activity so it persists on the server_tool_use block and the
|
|
2257
|
+
// web-search card survives a history reopen, matching the live event.
|
|
2258
|
+
if (metadata) {
|
|
2259
|
+
state.toolActivityMetadata.set(event.toolUseId, {
|
|
2260
|
+
webSearch: metadata,
|
|
2261
|
+
});
|
|
2262
|
+
}
|
|
2263
|
+
|
|
1850
2264
|
deps.onEvent({
|
|
1851
2265
|
type: "tool_result",
|
|
1852
2266
|
toolName: "web_search",
|
|
@@ -1873,6 +2287,30 @@ export async function dispatchAgentEvent(
|
|
|
1873
2287
|
// banner.
|
|
1874
2288
|
deps.onEvent(event);
|
|
1875
2289
|
break;
|
|
2290
|
+
case "compaction_completed":
|
|
2291
|
+
// Always commit the loop-stripped `basis` as the durable message base
|
|
2292
|
+
// so re-injection re-applies onto the stripped history even when the
|
|
2293
|
+
// pipeline ran but did not compact. When it did compact, commit the
|
|
2294
|
+
// durable result (DB-record fields, Slack provenance, SSE) — which
|
|
2295
|
+
// overwrites `ctx.messages` with the compacted history — and flip the
|
|
2296
|
+
// per-turn re-injection guards the orchestrator reads. This runs
|
|
2297
|
+
// before the loop's `reinject` hook (the loop awaits this dispatch),
|
|
2298
|
+
// so the guards are set in time. A failed durable commit re-throws
|
|
2299
|
+
// below to abort the turn rather than re-injecting against
|
|
2300
|
+
// half-applied state.
|
|
2301
|
+
deps.ctx.messages = event.basis;
|
|
2302
|
+
if (event.result.compacted) {
|
|
2303
|
+
await deps.applyCompaction(event.result, event.basis);
|
|
2304
|
+
state.reducerCompacted = true;
|
|
2305
|
+
}
|
|
2306
|
+
break;
|
|
2307
|
+
case "history_stripped":
|
|
2308
|
+
// Record the history-stripped DB marker right after the loop strips
|
|
2309
|
+
// injections (before the pipeline). Best-effort: a transient marker
|
|
2310
|
+
// write must not abort the turn, so unlike `compaction_completed` this
|
|
2311
|
+
// is not on the re-throw allowlist below.
|
|
2312
|
+
markHistoryStrippedBestEffort(deps.ctx.conversationId);
|
|
2313
|
+
break;
|
|
1876
2314
|
case "error":
|
|
1877
2315
|
handleError(state, deps, event);
|
|
1878
2316
|
break;
|
|
@@ -1925,10 +2363,13 @@ export async function dispatchAgentEvent(
|
|
|
1925
2363
|
// - message_complete: persists assistant message to DB, sets state flags
|
|
1926
2364
|
// - error: sets recovery flags (contextTooLargeDetected, orderingErrorDetected)
|
|
1927
2365
|
// - usage: records token accounting
|
|
2366
|
+
// - compaction_completed: durable compaction commit; aborting the turn is
|
|
2367
|
+
// safer than re-injecting against a half-applied compaction
|
|
1928
2368
|
if (
|
|
1929
2369
|
event.type === "message_complete" ||
|
|
1930
2370
|
event.type === "error" ||
|
|
1931
|
-
event.type === "usage"
|
|
2371
|
+
event.type === "usage" ||
|
|
2372
|
+
event.type === "compaction_completed"
|
|
1932
2373
|
) {
|
|
1933
2374
|
throw err;
|
|
1934
2375
|
}
|