@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
|
@@ -40,12 +40,35 @@ mock.module("../config/loader.js", () => ({
|
|
|
40
40
|
}));
|
|
41
41
|
|
|
42
42
|
// ── Mock conversation-crud (used by handleToolResult/handleMessageComplete) ──
|
|
43
|
+
// Reserve returns a role-distinct id so tests can tell the grouped tool-result
|
|
44
|
+
// `user` row apart from the assistant row, and assert it is reserved exactly
|
|
45
|
+
// once per batch. `updateMessageContent` is a spy so tests can inspect the
|
|
46
|
+
// content written into the row on each arrival.
|
|
47
|
+
// Widen the reservation window so concurrent tool-result handlers provably
|
|
48
|
+
// overlap before the first `reserveMessage` resolves; defaults to no delay.
|
|
49
|
+
let reserveMessageDelayMs = 0;
|
|
50
|
+
const reserveMessageMock = mock(
|
|
51
|
+
async (_conversationId: string, role: string) => {
|
|
52
|
+
if (reserveMessageDelayMs > 0) {
|
|
53
|
+
await new Promise((resolve) =>
|
|
54
|
+
setTimeout(resolve, reserveMessageDelayMs),
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
return { id: role === "user" ? "tool-result-row" : "assistant-row" };
|
|
58
|
+
},
|
|
59
|
+
);
|
|
60
|
+
const updateMessageContentMock = mock((_id: string, _content: string) => {});
|
|
61
|
+
|
|
43
62
|
mock.module("../memory/conversation-crud.js", () => ({
|
|
44
|
-
|
|
63
|
+
getConversation: () => null,
|
|
45
64
|
getMessageById: () => null,
|
|
46
|
-
updateMessageContent:
|
|
65
|
+
updateMessageContent: updateMessageContentMock,
|
|
47
66
|
provenanceFromTrustContext: () => ({}),
|
|
48
|
-
reserveMessage:
|
|
67
|
+
reserveMessage: reserveMessageMock,
|
|
68
|
+
}));
|
|
69
|
+
|
|
70
|
+
mock.module("../memory/conversation-disk-view.js", () => ({
|
|
71
|
+
syncMessageToDisk: () => {},
|
|
49
72
|
}));
|
|
50
73
|
|
|
51
74
|
mock.module("../memory/llm-request-log-store.js", () => ({
|
|
@@ -53,19 +76,37 @@ mock.module("../memory/llm-request-log-store.js", () => ({
|
|
|
53
76
|
backfillMessageIdOnLogs: () => {},
|
|
54
77
|
}));
|
|
55
78
|
|
|
79
|
+
mock.module("../memory/memory-recall-log-store.js", () => ({
|
|
80
|
+
backfillMemoryRecallLogMessageId: () => {},
|
|
81
|
+
}));
|
|
82
|
+
|
|
83
|
+
mock.module("../memory/memory-v2-activation-log-store.js", () => ({
|
|
84
|
+
backfillMemoryV2ActivationMessageId: () => {},
|
|
85
|
+
}));
|
|
86
|
+
|
|
56
87
|
// ── Imports (after mocks) ─────────────────────────────────────────────────────
|
|
88
|
+
import type { AgentEvent } from "../agent/loop.js";
|
|
57
89
|
import type {
|
|
58
90
|
EventHandlerDeps,
|
|
59
91
|
EventHandlerState,
|
|
60
92
|
} from "../daemon/conversation-agent-loop-handlers.js";
|
|
61
93
|
import {
|
|
62
94
|
createEventHandlerState,
|
|
95
|
+
dispatchAgentEvent,
|
|
63
96
|
handleInputJsonDelta,
|
|
97
|
+
handleMessageComplete,
|
|
64
98
|
handleToolResult,
|
|
65
99
|
handleToolUse,
|
|
66
100
|
handleToolUsePreviewStart,
|
|
67
101
|
} from "../daemon/conversation-agent-loop-handlers.js";
|
|
68
102
|
import type { ServerMessage } from "../daemon/message-protocol.js";
|
|
103
|
+
import type { AssistantEvent } from "../runtime/assistant-event.js";
|
|
104
|
+
import {
|
|
105
|
+
_resetStreamStateForTesting,
|
|
106
|
+
getCurrentSeq,
|
|
107
|
+
getPersistedSeq,
|
|
108
|
+
stampAndBuffer,
|
|
109
|
+
} from "../runtime/assistant-stream-state.js";
|
|
69
110
|
|
|
70
111
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
71
112
|
|
|
@@ -243,7 +284,7 @@ describe("tool preview lifecycle", () => {
|
|
|
243
284
|
expect((emitted as any).conversationId).toBe("test-session-id");
|
|
244
285
|
});
|
|
245
286
|
|
|
246
|
-
test("handleToolResult includes toolUseId", () => {
|
|
287
|
+
test("handleToolResult includes toolUseId", async () => {
|
|
247
288
|
const collector = createEventCollector();
|
|
248
289
|
const deps = createMockDeps({
|
|
249
290
|
onEvent: collector.onEvent,
|
|
@@ -260,7 +301,7 @@ describe("tool preview lifecycle", () => {
|
|
|
260
301
|
});
|
|
261
302
|
state.currentTurnToolUseIds.push("toolu_result789");
|
|
262
303
|
|
|
263
|
-
handleToolResult(state, deps, {
|
|
304
|
+
await handleToolResult(state, deps, {
|
|
264
305
|
type: "tool_result",
|
|
265
306
|
toolUseId: "toolu_result789",
|
|
266
307
|
content: "file1.txt\nfile2.txt",
|
|
@@ -278,6 +319,428 @@ describe("tool preview lifecycle", () => {
|
|
|
278
319
|
|
|
279
320
|
// ── Event ordering ────────────────────────────────────────────────────────
|
|
280
321
|
|
|
322
|
+
describe("persisted seq advances on tool_use_start", () => {
|
|
323
|
+
beforeEach(() => {
|
|
324
|
+
_resetStreamStateForTesting();
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test("advances the conversation's persisted seq to the tool_use_start seq", () => {
|
|
328
|
+
/**
|
|
329
|
+
* The assistant row (including tool_use blocks) is persisted at
|
|
330
|
+
* message_complete, which precedes tool events. handleToolUse emits a
|
|
331
|
+
* seq-stamped tool_use_start afterward, so the persisted seq must catch
|
|
332
|
+
* up to that event -- otherwise /messages would advertise a seq below an
|
|
333
|
+
* event it already reflects.
|
|
334
|
+
*/
|
|
335
|
+
// GIVEN an onEvent that stamps conversation-scoped events like the hub
|
|
336
|
+
const collector = createEventCollector();
|
|
337
|
+
const conversationId = "test-session-id";
|
|
338
|
+
const deps = createMockDeps({
|
|
339
|
+
onEvent: (msg: ServerMessage) => {
|
|
340
|
+
collector.events.push(msg);
|
|
341
|
+
stampAndBuffer(msg as unknown as AssistantEvent);
|
|
342
|
+
},
|
|
343
|
+
ctx: {
|
|
344
|
+
...createMockDeps().ctx,
|
|
345
|
+
conversationId,
|
|
346
|
+
emitActivityState: collector.emitActivityState,
|
|
347
|
+
} as unknown as EventHandlerDeps["ctx"],
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// AND prior streamed text deltas have already advanced the global seq
|
|
351
|
+
stampAndBuffer({
|
|
352
|
+
type: "assistant_text_delta",
|
|
353
|
+
text: "hello",
|
|
354
|
+
conversationId,
|
|
355
|
+
} as unknown as AssistantEvent);
|
|
356
|
+
stampAndBuffer({
|
|
357
|
+
type: "assistant_text_delta",
|
|
358
|
+
text: " world",
|
|
359
|
+
conversationId,
|
|
360
|
+
} as unknown as AssistantEvent);
|
|
361
|
+
|
|
362
|
+
// WHEN a tool_use is handled (its block is already durable)
|
|
363
|
+
handleToolUse(state, deps, {
|
|
364
|
+
type: "tool_use",
|
|
365
|
+
id: "toolu_abc123",
|
|
366
|
+
name: "bash",
|
|
367
|
+
input: { command: "ls" },
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// THEN the persisted seq equals the just-stamped tool_use_start seq
|
|
371
|
+
const toolUseStart = collector.events.find(
|
|
372
|
+
(e) => e.type === "tool_use_start",
|
|
373
|
+
);
|
|
374
|
+
expect(toolUseStart).toBeDefined();
|
|
375
|
+
expect(getPersistedSeq(conversationId)).toBe(getCurrentSeq());
|
|
376
|
+
expect(getPersistedSeq(conversationId)).toBe(
|
|
377
|
+
(toolUseStart as unknown as AssistantEvent).seq ?? null,
|
|
378
|
+
);
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
describe("persisted seq advances at the turn boundary for all turn types", () => {
|
|
383
|
+
const conversationId = "test-session-id";
|
|
384
|
+
|
|
385
|
+
beforeEach(() => {
|
|
386
|
+
_resetStreamStateForTesting();
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
/** onEvent that stamps conversation-scoped events like the runtime hub. */
|
|
390
|
+
function makeStampingDeps(
|
|
391
|
+
overrides: Partial<EventHandlerDeps["ctx"]> = {},
|
|
392
|
+
): { deps: EventHandlerDeps; events: ServerMessage[] } {
|
|
393
|
+
const events: ServerMessage[] = [];
|
|
394
|
+
const deps = createMockDeps({
|
|
395
|
+
onEvent: (msg: ServerMessage) => {
|
|
396
|
+
events.push(msg);
|
|
397
|
+
stampAndBuffer(msg as unknown as AssistantEvent);
|
|
398
|
+
},
|
|
399
|
+
ctx: {
|
|
400
|
+
...createMockDeps().ctx,
|
|
401
|
+
conversationId,
|
|
402
|
+
...overrides,
|
|
403
|
+
} as unknown as EventHandlerDeps["ctx"],
|
|
404
|
+
});
|
|
405
|
+
return { deps, events };
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
test("a streamed thinking delta is mirrored for incremental persistence", async () => {
|
|
409
|
+
/**
|
|
410
|
+
* Thinking rides the same mirror-and-flush path as text, so a thinking
|
|
411
|
+
* delta is appended to the running view and bumps the single persisted
|
|
412
|
+
* seq field -- the debounced partial flush then writes it to the row,
|
|
413
|
+
* letting long reasoning streams survive a refresh just like long
|
|
414
|
+
* answers do.
|
|
415
|
+
*/
|
|
416
|
+
// GIVEN a turn that streams thinking
|
|
417
|
+
const { deps, events } = makeStampingDeps({ streamThinking: true });
|
|
418
|
+
state.lastAssistantMessageId = "assistant-msg-1";
|
|
419
|
+
|
|
420
|
+
// WHEN a thinking_delta is dispatched
|
|
421
|
+
await dispatchAgentEvent(state, deps, {
|
|
422
|
+
type: "thinking_delta",
|
|
423
|
+
thinking: "Let me reason about this.",
|
|
424
|
+
} as Extract<AgentEvent, { type: "thinking_delta" }>);
|
|
425
|
+
|
|
426
|
+
// THEN it is mirrored into the running view and the persisted seq field
|
|
427
|
+
// tracks the emitted delta
|
|
428
|
+
const thinkingDelta = events.find(
|
|
429
|
+
(e) => e.type === "assistant_thinking_delta",
|
|
430
|
+
);
|
|
431
|
+
expect(thinkingDelta).toBeDefined();
|
|
432
|
+
expect(state.currentMessageContent).toEqual([
|
|
433
|
+
{
|
|
434
|
+
type: "thinking",
|
|
435
|
+
thinking: "Let me reason about this.",
|
|
436
|
+
signature: "",
|
|
437
|
+
},
|
|
438
|
+
]);
|
|
439
|
+
expect(state.lastPersistedContentSeq).toBe(
|
|
440
|
+
(thinkingDelta as unknown as AssistantEvent).seq ?? undefined,
|
|
441
|
+
);
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
test("a thinking-only turn advances the persisted seq to the thinking delta", async () => {
|
|
445
|
+
/**
|
|
446
|
+
* Reasoning-model turns can emit thinking with no text delta. Because
|
|
447
|
+
* thinking is now mirrored and flushed like text, the persisted seq
|
|
448
|
+
* advances to the streamed thinking_delta -- otherwise /messages would
|
|
449
|
+
* advertise a seq behind content the snapshot already reflects.
|
|
450
|
+
*/
|
|
451
|
+
// GIVEN a turn that streams thinking (no text delta)
|
|
452
|
+
const { deps, events } = makeStampingDeps({ streamThinking: true });
|
|
453
|
+
state.lastAssistantMessageId = "assistant-msg-1";
|
|
454
|
+
|
|
455
|
+
// WHEN a thinking_delta is dispatched, then the turn completes
|
|
456
|
+
await dispatchAgentEvent(state, deps, {
|
|
457
|
+
type: "thinking_delta",
|
|
458
|
+
thinking: "Let me reason about this.",
|
|
459
|
+
} as Extract<AgentEvent, { type: "thinking_delta" }>);
|
|
460
|
+
await handleMessageComplete(state, deps, {
|
|
461
|
+
type: "message_complete",
|
|
462
|
+
message: {
|
|
463
|
+
role: "assistant",
|
|
464
|
+
content: [
|
|
465
|
+
{ type: "thinking", thinking: "Let me reason about this." },
|
|
466
|
+
],
|
|
467
|
+
},
|
|
468
|
+
} as Extract<AgentEvent, { type: "message_complete" }>);
|
|
469
|
+
|
|
470
|
+
// THEN the persisted seq equals the streamed thinking delta's seq
|
|
471
|
+
const thinkingDelta = events.find(
|
|
472
|
+
(e) => e.type === "assistant_thinking_delta",
|
|
473
|
+
);
|
|
474
|
+
expect(thinkingDelta).toBeDefined();
|
|
475
|
+
expect(getPersistedSeq(conversationId)).toBe(getCurrentSeq());
|
|
476
|
+
expect(getPersistedSeq(conversationId)).toBe(
|
|
477
|
+
(thinkingDelta as unknown as AssistantEvent).seq ?? null,
|
|
478
|
+
);
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
test("a tool result advances the persisted seq on arrival", async () => {
|
|
482
|
+
/**
|
|
483
|
+
* Tool results are persisted into their grouped row as they arrive (so a
|
|
484
|
+
* long-running tool's output survives a refresh), advancing the persisted
|
|
485
|
+
* seq to the just-stamped tool_result event rather than deferring to
|
|
486
|
+
* message_complete.
|
|
487
|
+
*/
|
|
488
|
+
// GIVEN a tool whose result is about to arrive
|
|
489
|
+
const { deps, events } = makeStampingDeps({ streamThinking: true });
|
|
490
|
+
state.lastAssistantMessageId = "assistant-msg-1";
|
|
491
|
+
state.toolUseIdToName.set("toolu_result", "bash");
|
|
492
|
+
state.toolCallTimestamps.set("toolu_result", { startedAt: Date.now() });
|
|
493
|
+
state.currentTurnToolUseIds.push("toolu_result");
|
|
494
|
+
|
|
495
|
+
// WHEN the tool result is handled
|
|
496
|
+
await handleToolResult(state, deps, {
|
|
497
|
+
type: "tool_result",
|
|
498
|
+
toolUseId: "toolu_result",
|
|
499
|
+
content: "file1.txt\nfile2.txt",
|
|
500
|
+
isError: false,
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
// THEN the persisted seq equals the just-stamped tool_result seq
|
|
504
|
+
const toolResult = events.find((e) => e.type === "tool_result");
|
|
505
|
+
expect(toolResult).toBeDefined();
|
|
506
|
+
expect(getPersistedSeq(conversationId)).toBe(getCurrentSeq());
|
|
507
|
+
expect(getPersistedSeq(conversationId)).toBe(
|
|
508
|
+
(toolResult as unknown as AssistantEvent).seq ?? null,
|
|
509
|
+
);
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
test("thinking that is not streamed leaves the persisted seq unset", async () => {
|
|
513
|
+
/**
|
|
514
|
+
* When streamThinking is off, no thinking_delta SSE event is emitted, so
|
|
515
|
+
* nothing is mirrored and there is no stamped event to anchor a seq to.
|
|
516
|
+
* The turn must not invent a seq from unrelated global stream position.
|
|
517
|
+
*/
|
|
518
|
+
// GIVEN a turn that does NOT stream thinking
|
|
519
|
+
const { deps, events } = makeStampingDeps({ streamThinking: false });
|
|
520
|
+
state.lastAssistantMessageId = "assistant-msg-1";
|
|
521
|
+
|
|
522
|
+
// WHEN a thinking_delta is dispatched, then the turn completes
|
|
523
|
+
await dispatchAgentEvent(state, deps, {
|
|
524
|
+
type: "thinking_delta",
|
|
525
|
+
thinking: "Internal reasoning.",
|
|
526
|
+
} as Extract<AgentEvent, { type: "thinking_delta" }>);
|
|
527
|
+
await handleMessageComplete(state, deps, {
|
|
528
|
+
type: "message_complete",
|
|
529
|
+
message: {
|
|
530
|
+
role: "assistant",
|
|
531
|
+
content: [{ type: "thinking", thinking: "Internal reasoning." }],
|
|
532
|
+
},
|
|
533
|
+
} as Extract<AgentEvent, { type: "message_complete" }>);
|
|
534
|
+
|
|
535
|
+
// THEN no thinking_delta was emitted and the persisted seq stays unset
|
|
536
|
+
expect(
|
|
537
|
+
events.find((e) => e.type === "assistant_thinking_delta"),
|
|
538
|
+
).toBeUndefined();
|
|
539
|
+
expect(state.lastPersistedContentSeq).toBeUndefined();
|
|
540
|
+
expect(getPersistedSeq(conversationId)).toBeNull();
|
|
541
|
+
});
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
describe("tool results are persisted on arrival into a grouped row", () => {
|
|
545
|
+
const conversationId = "test-session-id";
|
|
546
|
+
|
|
547
|
+
beforeEach(() => {
|
|
548
|
+
_resetStreamStateForTesting();
|
|
549
|
+
reserveMessageMock.mockClear();
|
|
550
|
+
updateMessageContentMock.mockClear();
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
/** onEvent that stamps conversation-scoped events like the runtime hub. */
|
|
554
|
+
function makeStampingDeps(): {
|
|
555
|
+
deps: EventHandlerDeps;
|
|
556
|
+
events: ServerMessage[];
|
|
557
|
+
} {
|
|
558
|
+
const events: ServerMessage[] = [];
|
|
559
|
+
const deps = createMockDeps({
|
|
560
|
+
onEvent: (msg: ServerMessage) => {
|
|
561
|
+
events.push(msg);
|
|
562
|
+
stampAndBuffer(msg as unknown as AssistantEvent);
|
|
563
|
+
},
|
|
564
|
+
ctx: {
|
|
565
|
+
...createMockDeps().ctx,
|
|
566
|
+
conversationId,
|
|
567
|
+
} as unknown as EventHandlerDeps["ctx"],
|
|
568
|
+
});
|
|
569
|
+
return { deps, events };
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/** Register a tool as started so its result can be handled. */
|
|
573
|
+
function registerTool(toolUseId: string): void {
|
|
574
|
+
state.toolUseIdToName.set(toolUseId, "bash");
|
|
575
|
+
state.toolCallTimestamps.set(toolUseId, { startedAt: Date.now() });
|
|
576
|
+
state.currentTurnToolUseIds.push(toolUseId);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/** Parse the content of the most recent updateMessageContent call. */
|
|
580
|
+
function latestWrittenBlocks(): Array<Record<string, unknown>> {
|
|
581
|
+
const calls = updateMessageContentMock.mock.calls;
|
|
582
|
+
const last = calls[calls.length - 1];
|
|
583
|
+
return JSON.parse(last[1] as string);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
test("the first result reserves one user row and writes its block", async () => {
|
|
587
|
+
/**
|
|
588
|
+
* The grouped tool-result row is a `user` message reserved when the first
|
|
589
|
+
* result of a batch arrives, then written via updateContent so the result
|
|
590
|
+
* is durable immediately.
|
|
591
|
+
*/
|
|
592
|
+
// GIVEN a started tool
|
|
593
|
+
const { deps } = makeStampingDeps();
|
|
594
|
+
state.lastAssistantMessageId = "assistant-msg-1";
|
|
595
|
+
registerTool("toolu_a");
|
|
596
|
+
|
|
597
|
+
// WHEN its result arrives
|
|
598
|
+
await handleToolResult(state, deps, {
|
|
599
|
+
type: "tool_result",
|
|
600
|
+
toolUseId: "toolu_a",
|
|
601
|
+
content: "result-a",
|
|
602
|
+
isError: false,
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
// THEN a single user row was reserved and tracked, and its block written
|
|
606
|
+
const userReserves = reserveMessageMock.mock.calls.filter(
|
|
607
|
+
(call) => call[1] === "user",
|
|
608
|
+
);
|
|
609
|
+
expect(userReserves).toHaveLength(1);
|
|
610
|
+
expect(await state.pendingToolResultRowReservation).toBe(
|
|
611
|
+
"tool-result-row",
|
|
612
|
+
);
|
|
613
|
+
const blocks = latestWrittenBlocks();
|
|
614
|
+
expect(blocks).toHaveLength(1);
|
|
615
|
+
expect(blocks[0]).toMatchObject({
|
|
616
|
+
type: "tool_result",
|
|
617
|
+
tool_use_id: "toolu_a",
|
|
618
|
+
content: "result-a",
|
|
619
|
+
is_error: false,
|
|
620
|
+
});
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
test("parallel results share one row, grouped as sibling blocks", async () => {
|
|
624
|
+
/**
|
|
625
|
+
* Results from parallel tool calls in the same turn must land in a single
|
|
626
|
+
* `user` row (the tool_result-in-user-turn shape providers expect), so the
|
|
627
|
+
* row is reserved once and rewritten in place as each result arrives.
|
|
628
|
+
*/
|
|
629
|
+
// GIVEN two started tools
|
|
630
|
+
const { deps } = makeStampingDeps();
|
|
631
|
+
state.lastAssistantMessageId = "assistant-msg-1";
|
|
632
|
+
registerTool("toolu_a");
|
|
633
|
+
registerTool("toolu_b");
|
|
634
|
+
|
|
635
|
+
// WHEN both results arrive
|
|
636
|
+
await handleToolResult(state, deps, {
|
|
637
|
+
type: "tool_result",
|
|
638
|
+
toolUseId: "toolu_a",
|
|
639
|
+
content: "result-a",
|
|
640
|
+
isError: false,
|
|
641
|
+
});
|
|
642
|
+
await handleToolResult(state, deps, {
|
|
643
|
+
type: "tool_result",
|
|
644
|
+
toolUseId: "toolu_b",
|
|
645
|
+
content: "result-b",
|
|
646
|
+
isError: false,
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
// THEN the row was reserved exactly once and now holds both blocks
|
|
650
|
+
const userReserves = reserveMessageMock.mock.calls.filter(
|
|
651
|
+
(call) => call[1] === "user",
|
|
652
|
+
);
|
|
653
|
+
expect(userReserves).toHaveLength(1);
|
|
654
|
+
const blocks = latestWrittenBlocks();
|
|
655
|
+
expect(blocks.map((b) => b.tool_use_id)).toEqual(["toolu_a", "toolu_b"]);
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
test("concurrent results race but reserve exactly one row", async () => {
|
|
659
|
+
/**
|
|
660
|
+
* `agent/loop.ts` dispatches each `tool_result` without awaiting, so two
|
|
661
|
+
* handlers for one parallel batch can enter reservation before the first
|
|
662
|
+
* `reserveMessage` resolves. A shared in-flight reservation promise must
|
|
663
|
+
* collapse them onto a single row rather than reserving one per result.
|
|
664
|
+
*/
|
|
665
|
+
// GIVEN two started tools AND a reservation slow enough to overlap them
|
|
666
|
+
const { deps } = makeStampingDeps();
|
|
667
|
+
state.lastAssistantMessageId = "assistant-msg-1";
|
|
668
|
+
registerTool("toolu_a");
|
|
669
|
+
registerTool("toolu_b");
|
|
670
|
+
reserveMessageDelayMs = 10;
|
|
671
|
+
|
|
672
|
+
// WHEN both results are handled concurrently (neither awaited first)
|
|
673
|
+
try {
|
|
674
|
+
await Promise.all([
|
|
675
|
+
handleToolResult(state, deps, {
|
|
676
|
+
type: "tool_result",
|
|
677
|
+
toolUseId: "toolu_a",
|
|
678
|
+
content: "result-a",
|
|
679
|
+
isError: false,
|
|
680
|
+
}),
|
|
681
|
+
handleToolResult(state, deps, {
|
|
682
|
+
type: "tool_result",
|
|
683
|
+
toolUseId: "toolu_b",
|
|
684
|
+
content: "result-b",
|
|
685
|
+
isError: false,
|
|
686
|
+
}),
|
|
687
|
+
]);
|
|
688
|
+
} finally {
|
|
689
|
+
reserveMessageDelayMs = 0;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// THEN exactly one user row was reserved and it holds both sibling blocks
|
|
693
|
+
const userReserves = reserveMessageMock.mock.calls.filter(
|
|
694
|
+
(call) => call[1] === "user",
|
|
695
|
+
);
|
|
696
|
+
expect(userReserves).toHaveLength(1);
|
|
697
|
+
expect(await state.pendingToolResultRowReservation).toBe(
|
|
698
|
+
"tool-result-row",
|
|
699
|
+
);
|
|
700
|
+
const blocks = latestWrittenBlocks();
|
|
701
|
+
expect(blocks.map((b) => b.tool_use_id).sort()).toEqual([
|
|
702
|
+
"toolu_a",
|
|
703
|
+
"toolu_b",
|
|
704
|
+
]);
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
test("message_complete finalizes the on-arrival row without a second reserve", async () => {
|
|
708
|
+
/**
|
|
709
|
+
* Because the row already exists from the on-arrival write, the
|
|
710
|
+
* message_complete drain finalizes it (rewrite + bookkeeping) instead of
|
|
711
|
+
* inserting a second row, then clears the batch state.
|
|
712
|
+
*/
|
|
713
|
+
// GIVEN a result already persisted on arrival
|
|
714
|
+
const { deps } = makeStampingDeps();
|
|
715
|
+
state.lastAssistantMessageId = "assistant-msg-1";
|
|
716
|
+
registerTool("toolu_a");
|
|
717
|
+
await handleToolResult(state, deps, {
|
|
718
|
+
type: "tool_result",
|
|
719
|
+
toolUseId: "toolu_a",
|
|
720
|
+
content: "result-a",
|
|
721
|
+
isError: false,
|
|
722
|
+
});
|
|
723
|
+
const reservesAfterArrival = reserveMessageMock.mock.calls.filter(
|
|
724
|
+
(call) => call[1] === "user",
|
|
725
|
+
).length;
|
|
726
|
+
|
|
727
|
+
// WHEN the next call completes, draining the buffered result
|
|
728
|
+
await handleMessageComplete(state, deps, {
|
|
729
|
+
type: "message_complete",
|
|
730
|
+
message: { role: "assistant", content: [{ type: "text", text: "ok" }] },
|
|
731
|
+
} as Extract<AgentEvent, { type: "message_complete" }>);
|
|
732
|
+
|
|
733
|
+
// THEN no additional user row was reserved and the batch state is cleared
|
|
734
|
+
const reservesAfterDrain = reserveMessageMock.mock.calls.filter(
|
|
735
|
+
(call) => call[1] === "user",
|
|
736
|
+
).length;
|
|
737
|
+
expect(reservesAfterDrain).toBe(reservesAfterArrival);
|
|
738
|
+
expect(state.pendingToolResults.size).toBe(0);
|
|
739
|
+
expect(state.pendingToolResultRowReservation).toBeUndefined();
|
|
740
|
+
expect(state.persistedToolUseIds.has("toolu_a")).toBe(true);
|
|
741
|
+
});
|
|
742
|
+
});
|
|
743
|
+
|
|
281
744
|
describe("event ordering", () => {
|
|
282
745
|
test("events are emitted in correct order: tool_use_preview_start → tool_input_delta → tool_use", () => {
|
|
283
746
|
const collector = createEventCollector();
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the default `tool-result-truncate` plugin's `post-tool-use` hook.
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - The hook tail-drops an oversized `toolResponse.content` to the budget
|
|
6
|
+
* derived from `maxInputTokens`, matching `truncateToolResult`, and is a
|
|
7
|
+
* no-op for content that already fits.
|
|
8
|
+
* - End-to-end through `runHook` + the registry: registering the default
|
|
9
|
+
* plugin makes the hook fire and truncate the tool response.
|
|
10
|
+
* - Chain ordering: because defaults register first, the default hook runs
|
|
11
|
+
* ahead of a later-registered user hook, which therefore observes an
|
|
12
|
+
* already-truncated response.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { beforeEach, describe, expect, test } from "bun:test";
|
|
16
|
+
|
|
17
|
+
import { HOOKS } from "../plugin-api/constants.js";
|
|
18
|
+
import type { PluginLogger, PostToolUseContext } from "../plugin-api/types.js";
|
|
19
|
+
import postToolUse from "../plugins/defaults/tool-result-truncate/hooks/post-tool-use.js";
|
|
20
|
+
import { defaultToolResultTruncatePlugin } from "../plugins/defaults/tool-result-truncate/register.js";
|
|
21
|
+
import {
|
|
22
|
+
truncateToolResult,
|
|
23
|
+
TRUNCATION_SUFFIX,
|
|
24
|
+
} from "../plugins/defaults/tool-result-truncate/terminal.js";
|
|
25
|
+
import { runHook } from "../plugins/pipeline.js";
|
|
26
|
+
import {
|
|
27
|
+
registerPlugin,
|
|
28
|
+
resetPluginRegistryForTests,
|
|
29
|
+
} from "../plugins/registry.js";
|
|
30
|
+
import type { ToolResultContent } from "../providers/types.js";
|
|
31
|
+
|
|
32
|
+
const noopLogger: PluginLogger = {
|
|
33
|
+
info: () => {},
|
|
34
|
+
warn: () => {},
|
|
35
|
+
error: () => {},
|
|
36
|
+
debug: () => {},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const MAX_INPUT_TOKENS = 10_000;
|
|
40
|
+
|
|
41
|
+
function makeToolResponse(content: string): ToolResultContent {
|
|
42
|
+
return { type: "tool_result", tool_use_id: "tu_1", content };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function makeCtx(content: string): PostToolUseContext {
|
|
46
|
+
return {
|
|
47
|
+
conversationId: "conv-test",
|
|
48
|
+
toolResponse: makeToolResponse(content),
|
|
49
|
+
messages: [],
|
|
50
|
+
maxInputTokens: MAX_INPUT_TOKENS,
|
|
51
|
+
logger: noopLogger,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe("tool-result-truncate post-tool-use hook — direct", () => {
|
|
56
|
+
test("truncates oversized content identically to truncateToolResult", async () => {
|
|
57
|
+
// GIVEN a tool response whose content far exceeds the derived budget.
|
|
58
|
+
const content = "a".repeat(1_000_000);
|
|
59
|
+
const expected = truncateToolResult(content, MAX_INPUT_TOKENS);
|
|
60
|
+
const ctx = makeCtx(content);
|
|
61
|
+
|
|
62
|
+
// WHEN the hook runs over the context.
|
|
63
|
+
await postToolUse(ctx);
|
|
64
|
+
|
|
65
|
+
// THEN the response content matches the canonical truncation output.
|
|
66
|
+
expect(expected.truncated).toBe(true);
|
|
67
|
+
expect(ctx.toolResponse.content).toBe(expected.content);
|
|
68
|
+
expect(ctx.toolResponse.content).toContain(TRUNCATION_SUFFIX);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("is a no-op for content that already fits the budget", async () => {
|
|
72
|
+
// GIVEN a tool response well within the derived budget.
|
|
73
|
+
const content = "small result";
|
|
74
|
+
const ctx = makeCtx(content);
|
|
75
|
+
|
|
76
|
+
// WHEN the hook runs.
|
|
77
|
+
await postToolUse(ctx);
|
|
78
|
+
|
|
79
|
+
// THEN the content is unchanged.
|
|
80
|
+
expect(ctx.toolResponse.content).toBe(content);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("tool-result-truncate post-tool-use hook — via runHook", () => {
|
|
85
|
+
beforeEach(() => {
|
|
86
|
+
resetPluginRegistryForTests();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("registering the default plugin truncates an oversized response", async () => {
|
|
90
|
+
// GIVEN the default tool-result-truncate plugin is registered.
|
|
91
|
+
registerPlugin(defaultToolResultTruncatePlugin);
|
|
92
|
+
const content = "a".repeat(1_000_000);
|
|
93
|
+
const expected = truncateToolResult(content, MAX_INPUT_TOKENS);
|
|
94
|
+
|
|
95
|
+
// WHEN the post-tool-use chain runs.
|
|
96
|
+
const result = await runHook<PostToolUseContext>(
|
|
97
|
+
HOOKS.POST_TOOL_USE,
|
|
98
|
+
makeCtx(content),
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
// THEN the tool response is truncated.
|
|
102
|
+
expect(result.toolResponse.content).toBe(expected.content);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("default hook runs before a later-registered user hook", async () => {
|
|
106
|
+
// GIVEN the default plugin is registered first, then a user plugin whose
|
|
107
|
+
// hook records the response content it observes.
|
|
108
|
+
let observed: string | null = null;
|
|
109
|
+
registerPlugin(defaultToolResultTruncatePlugin);
|
|
110
|
+
registerPlugin({
|
|
111
|
+
manifest: { name: "observer-plugin", version: "0.0.1" },
|
|
112
|
+
hooks: {
|
|
113
|
+
"post-tool-use": async (ctx: PostToolUseContext) => {
|
|
114
|
+
observed = ctx.toolResponse.content;
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
const content = "a".repeat(1_000_000);
|
|
119
|
+
const expected = truncateToolResult(content, MAX_INPUT_TOKENS);
|
|
120
|
+
|
|
121
|
+
// WHEN the chain runs.
|
|
122
|
+
await runHook<PostToolUseContext>(HOOKS.POST_TOOL_USE, makeCtx(content));
|
|
123
|
+
|
|
124
|
+
// THEN the user hook saw the already-truncated content.
|
|
125
|
+
expect(observed as string | null).toBe(expected.content);
|
|
126
|
+
});
|
|
127
|
+
});
|