@vellumai/assistant 0.8.10 → 0.8.11-staging.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bun.lock +62 -1
- package/docs/workspace-tools.md +196 -0
- package/examples/plugins/echo/README.md +3 -3
- package/knip.json +1 -0
- package/openapi.yaml +460 -128
- package/package.json +2 -1
- package/scripts/build-plugin-api.ts +299 -0
- package/src/__tests__/agent-loop-callsite-precedence.test.ts +7 -0
- package/src/__tests__/agent-loop-compaction-events.test.ts +197 -0
- package/src/__tests__/agent-loop-exit-reason.test.ts +93 -96
- package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +2 -0
- package/src/__tests__/agent-loop-output-hooks.test.ts +274 -1
- package/src/__tests__/agent-loop-override-profile.test.ts +3 -0
- package/src/__tests__/agent-loop-provider-error-recording.test.ts +4 -0
- package/src/__tests__/agent-loop-thinking.test.ts +4 -0
- package/src/__tests__/agent-loop.test.ts +578 -5
- package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +0 -1
- package/src/__tests__/approval-cascade.test.ts +1 -0
- package/src/__tests__/background-workers-disk-pressure.test.ts +0 -2
- package/src/__tests__/btw-routes.test.ts +0 -1
- package/src/__tests__/build-persisted-content.test.ts +75 -1
- package/src/__tests__/catalog-install-normalize.test.ts +141 -0
- package/src/__tests__/ces-startup-timeout.test.ts +60 -0
- package/src/__tests__/compaction-events.test.ts +1 -0
- package/src/__tests__/config-managed-gemini-defaults.test.ts +2 -46
- package/src/__tests__/context-overflow-reducer.test.ts +264 -124
- package/src/__tests__/context-window-manager-overflow-rung.test.ts +351 -0
- package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +13 -5
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +284 -455
- package/src/__tests__/conversation-agent-loop.test.ts +131 -551
- package/src/__tests__/conversation-app-control-instantiation.test.ts +13 -0
- package/src/__tests__/conversation-confirmation-signals.test.ts +1 -0
- package/src/__tests__/conversation-fork-crud.test.ts +259 -0
- package/src/__tests__/conversation-history-web-search.test.ts +1 -1
- package/src/__tests__/conversation-lifecycle.test.ts +257 -1
- package/src/__tests__/conversation-process-callsite.test.ts +1 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +38 -377
- package/src/__tests__/conversation-queue.test.ts +1 -39
- package/src/__tests__/conversation-runtime-assembly.test.ts +119 -8
- package/src/__tests__/conversation-skill-tools.test.ts +491 -5
- package/src/__tests__/conversation-slash-queue.test.ts +1 -1
- package/src/__tests__/conversation-slash-unknown.test.ts +1 -0
- package/src/__tests__/conversation-speed-override.test.ts +1 -0
- package/src/__tests__/conversation-store.test.ts +74 -0
- package/src/__tests__/conversation-surfaces-app-control.test.ts +4 -1
- package/src/__tests__/conversation-tool-setup-attribution.test.ts +323 -0
- package/src/__tests__/conversation-tool-setup-tools-disabled.test.ts +34 -0
- package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -0
- package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -0
- package/src/__tests__/corrected-target.test.ts +93 -0
- package/src/__tests__/credential-execution-feature-gates.test.ts +3 -5
- package/src/__tests__/credential-execution-tools.test.ts +23 -11
- package/src/__tests__/credential-security-invariants.test.ts +6 -1
- package/src/__tests__/db-schedule-syntax-migration.test.ts +80 -0
- package/src/__tests__/device-id.test.ts +70 -1
- package/src/__tests__/embedding-managed-proxy-selection.test.ts +6 -40
- package/src/__tests__/empty-response-hook.test.ts +242 -66
- package/src/__tests__/external-plugin-loader.test.ts +0 -31
- package/src/__tests__/get-skill-detail-audit.test.ts +43 -1
- package/src/__tests__/guardian-routing-invariants.test.ts +91 -0
- package/src/__tests__/history-repair-hook.test.ts +228 -3
- package/src/__tests__/host-app-control-proxy.test.ts +45 -0
- package/src/__tests__/host-browser-proxy.test.ts +254 -9
- package/src/__tests__/identity-routes.test.ts +1 -0
- package/src/__tests__/image-recovery-hook.test.ts +387 -0
- package/src/__tests__/injector-chain.test.ts +5 -4
- package/src/__tests__/injector-v3-suppression.test.ts +373 -47
- package/src/__tests__/intent-routing.test.ts +7 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +117 -15
- package/src/__tests__/notification-decision-strategy.test.ts +3 -3
- package/src/__tests__/oauth-store.test.ts +0 -85
- package/src/__tests__/{context-overflow-policy.test.ts → overflow-policy.test.ts} +1 -1
- package/src/__tests__/parallel-tool.benchmark.test.ts +4 -0
- package/src/__tests__/persist-unsendable-image-downscale.test.ts +29 -9
- package/src/__tests__/persist-unsendable-image.test.ts +4 -4
- package/src/__tests__/persistence-secret-redaction.test.ts +78 -0
- package/src/__tests__/plugin-bootstrap.test.ts +82 -73
- package/src/__tests__/plugin-tool-contribution.test.ts +7 -4
- package/src/__tests__/plugin-types.test.ts +0 -8
- package/src/__tests__/provider-catalog-visibility.test.ts +1 -9
- package/src/__tests__/prune-old-conversations-job.test.ts +99 -0
- package/src/__tests__/registry.test.ts +240 -1
- package/src/__tests__/require-fresh-approval.test.ts +3 -0
- package/src/__tests__/schedule-routes.test.ts +116 -1
- package/src/__tests__/schedule-store.test.ts +28 -0
- package/src/__tests__/schedule-tools.test.ts +94 -1
- package/src/__tests__/server-history-render.test.ts +39 -0
- package/src/__tests__/skill-projection-feature-flag.test.ts +13 -0
- package/src/__tests__/skill-projection.benchmark.test.ts +25 -7
- package/src/__tests__/skills.test.ts +202 -0
- package/src/__tests__/slim-skill-category.test.ts +195 -0
- package/src/__tests__/strip-memory-injections.test.ts +33 -39
- package/src/__tests__/test-support/tool-invocation-seed.ts +79 -0
- package/src/__tests__/title-generate-hook.test.ts +9 -7
- package/src/__tests__/tool-audit-listener.test.ts +264 -1
- package/src/__tests__/tool-error-hook.test.ts +4 -3
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -0
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +273 -0
- package/src/__tests__/tool-result-truncate-hook.test.ts +1 -0
- package/src/__tests__/tool-start-timestamp.test.ts +218 -0
- package/src/__tests__/tools-get-route.test.ts +202 -0
- package/src/__tests__/workspace-tool-loader.test.ts +319 -0
- package/src/__tests__/workspace-tools-watcher-flag.test.ts +70 -0
- package/src/agent/loop.ts +569 -319
- package/src/api/events/tool-result.ts +9 -0
- package/src/api/events/tool-use-start.ts +7 -0
- package/src/api/index.ts +10 -0
- package/src/api/responses/conversation-message.ts +135 -27
- package/src/api/responses/memory-v3-selection-log.ts +4 -4
- package/src/approvals/guardian-request-resolvers.ts +26 -0
- package/src/browser-session/backends/host-bridge.ts +29 -0
- package/src/browser-session/index.ts +1 -0
- package/src/browser-session/types.ts +5 -1
- package/src/cli/commands/__tests__/schedules.test.ts +62 -4
- package/src/cli/commands/__tests__/skills.test.ts +53 -0
- package/src/cli/commands/channel-verification-sessions.ts +6 -6
- package/src/cli/commands/inference-providers.ts +0 -8
- package/src/cli/commands/plugins.ts +2 -2
- package/src/cli/commands/schedules.ts +27 -4
- package/src/cli/commands/skills.ts +187 -146
- package/src/cli/commands/tools.ts +106 -0
- package/src/cli/lib/__tests__/install-from-github.test.ts +256 -328
- package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +6 -2
- package/src/cli/lib/__tests__/plugin-details.test.ts +10 -16
- package/src/cli/lib/__tests__/plugin-marketplace.test.ts +2 -2
- package/src/cli/lib/__tests__/search-plugins.test.ts +145 -240
- package/src/cli/lib/install-from-github.ts +187 -117
- package/src/cli/lib/plugin-catalog-cache.ts +9 -9
- package/src/cli/lib/plugin-details.ts +38 -68
- package/src/cli/lib/plugin-marketplace.ts +42 -14
- package/src/cli/lib/search-plugins.ts +29 -129
- package/src/cli/program.ts +2 -0
- package/src/config/bundled-skills/acp/SKILL.md +1 -0
- package/src/config/bundled-skills/app-builder/SKILL.md +1 -0
- package/src/config/bundled-skills/app-control/SKILL.md +1 -0
- package/src/config/bundled-skills/computer-use/SKILL.md +1 -0
- package/src/config/bundled-skills/contacts/SKILL.md +1 -0
- package/src/config/bundled-skills/document-editor/SKILL.md +1 -0
- package/src/config/bundled-skills/followups/SKILL.md +1 -0
- package/src/config/bundled-skills/image-studio/SKILL.md +1 -0
- package/src/config/bundled-skills/media-processing/SKILL.md +1 -0
- package/src/config/bundled-skills/messaging/SKILL.md +1 -0
- package/src/config/bundled-skills/phone-calls/SKILL.md +1 -0
- package/src/config/bundled-skills/playbooks/SKILL.md +1 -0
- package/src/config/bundled-skills/schedule/SKILL.md +1 -0
- package/src/config/bundled-skills/schedule/TOOLS.json +11 -3
- package/src/config/bundled-skills/sequences/SKILL.md +1 -0
- package/src/config/bundled-skills/settings/SKILL.md +1 -0
- package/src/config/bundled-skills/skill-management/SKILL.md +101 -1
- package/src/config/bundled-skills/subagent/SKILL.md +1 -0
- package/src/config/bundled-skills/transcribe/SKILL.md +1 -0
- package/src/config/env-registry.ts +23 -0
- package/src/config/feature-flag-registry.json +17 -81
- package/src/config/loader.ts +5 -22
- package/src/config/schema.ts +2 -0
- package/src/config/schemas/__tests__/compaction-logs.test.ts +56 -0
- package/src/config/schemas/__tests__/memory-v2.test.ts +0 -1
- package/src/config/schemas/__tests__/memory-v3.test.ts +61 -1
- package/src/config/schemas/compaction-logs.ts +79 -0
- package/src/config/schemas/memory-v2.ts +0 -8
- package/src/config/schemas/memory-v3.ts +104 -33
- package/src/config/seed-inference-profiles.ts +1 -1
- package/src/config/skills.ts +117 -47
- package/src/context/compactor.ts +11 -0
- package/src/context/strip-injections.ts +38 -4
- package/src/credential-execution/feature-gates.ts +0 -21
- package/src/credential-execution/startup-timeout.ts +32 -4
- package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +18 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +140 -91
- package/src/daemon/conversation-agent-loop.ts +123 -658
- package/src/daemon/conversation-error.ts +6 -33
- package/src/daemon/conversation-lifecycle.ts +1 -1
- package/src/daemon/conversation-runtime-assembly.ts +183 -22
- package/src/daemon/conversation-skill-tools.ts +137 -8
- package/src/daemon/conversation-slash.ts +0 -14
- package/src/daemon/conversation-store.ts +2 -19
- package/src/daemon/conversation-tool-setup.ts +87 -1
- package/src/daemon/conversation.ts +119 -50
- package/src/daemon/external-plugins-bootstrap.ts +36 -95
- package/src/daemon/handlers/config-channels.ts +11 -2
- package/src/daemon/handlers/shared.ts +9 -1
- package/src/daemon/handlers/skills.ts +10 -4
- package/src/daemon/host-app-control-proxy.ts +72 -57
- package/src/daemon/host-browser-proxy.ts +117 -22
- package/src/daemon/lifecycle.ts +34 -5
- package/src/daemon/message-protocol.ts +0 -7
- package/src/daemon/message-types/schedules.ts +1 -0
- package/src/daemon/message-types/skills.ts +17 -0
- package/src/daemon/providers-setup.ts +3 -0
- package/src/daemon/server.ts +3 -3
- package/src/daemon/tool-setup-types.ts +9 -3
- package/src/daemon/trust-context.ts +23 -0
- package/src/daemon/workspace-tools-watcher.ts +324 -0
- package/src/events/tool-audit-listener.ts +78 -16
- package/src/events/tool-metrics-listener.ts +2 -5
- package/src/memory/__tests__/compaction-log-writer-clickhouse.test.ts +227 -0
- package/src/memory/__tests__/conversation-queries.test.ts +176 -0
- package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +20 -32
- package/src/memory/compaction-log-writer-clickhouse.ts +418 -0
- package/src/memory/conversation-crud.ts +134 -3
- package/src/memory/conversation-queries.ts +64 -7
- package/src/memory/db-init.ts +14 -0
- package/src/memory/embedding-backend.test.ts +130 -1
- package/src/memory/embedding-backend.ts +79 -106
- package/src/memory/embedding-gemini.ts +5 -0
- package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +12 -0
- package/src/memory/graph/__tests__/handle-remember-v2.test.ts +19 -0
- package/src/memory/graph/conversation-graph-memory.ts +36 -25
- package/src/memory/graph/tool-handlers.ts +3 -0
- package/src/memory/job-handlers/cleanup.ts +3 -1
- package/src/memory/jobs-store.ts +0 -28
- package/src/memory/jobs-worker.ts +10 -22
- package/src/memory/memory-marker.ts +29 -0
- package/src/memory/memory-retrospective-startup-cleanup.ts +1 -1
- package/src/memory/migrations/268-add-memory-v3-selections.ts +6 -0
- package/src/memory/migrations/270-schedule-description.ts +36 -0
- package/src/memory/migrations/275-tool-invocations-add-skill-id.test.ts +81 -0
- package/src/memory/migrations/275-tool-invocations-add-skill-id.ts +20 -0
- package/src/memory/migrations/276-tool-invocations-created-at-id-index.test.ts +68 -0
- package/src/memory/migrations/276-tool-invocations-created-at-id-index.ts +20 -0
- package/src/memory/migrations/277-add-memory-v3-ever-injected.ts +29 -0
- package/src/memory/migrations/278-tool-invocations-telemetry-columns.test.ts +96 -0
- package/src/memory/migrations/278-tool-invocations-telemetry-columns.ts +39 -0
- package/src/memory/migrations/279-create-skill-loaded-events.test.ts +84 -0
- package/src/memory/migrations/279-create-skill-loaded-events.ts +26 -0
- package/src/memory/migrations/280-conversations-surfaced-at.test.ts +88 -0
- package/src/memory/migrations/280-conversations-surfaced-at.ts +24 -0
- package/src/memory/migrations/index.ts +10 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/schema/conversations.ts +16 -0
- package/src/memory/schema/infrastructure.ts +26 -0
- package/src/memory/skill-loaded-events-store.test.ts +160 -0
- package/src/memory/skill-loaded-events-store.ts +95 -0
- package/src/memory/tool-executed-events-store.test.ts +219 -0
- package/src/memory/tool-executed-events-store.ts +102 -0
- package/src/memory/tool-usage-store.ts +15 -3
- package/src/memory/v2/__tests__/consolidation-job.test.ts +117 -12
- package/src/memory/v2/__tests__/consolidation-prompt-flag-gating-guard.test.ts +189 -0
- package/src/memory/v2/__tests__/injected-block-slugs.test.ts +90 -0
- package/src/memory/v2/__tests__/page-store.test.ts +33 -0
- package/src/memory/v2/__tests__/prompts-consolidation.test.ts +88 -15
- package/src/memory/v2/activation-store.ts +50 -1
- package/src/memory/v2/consolidation-job.ts +113 -29
- package/src/memory/v2/injected-block-slugs.ts +79 -0
- package/src/memory/v2/injection.ts +6 -1
- package/src/memory/v2/prompts/consolidation.ts +414 -13
- package/src/memory/v2/router.ts +2 -28
- package/src/memory/v2/static-context.ts +1 -1
- package/src/memory/v2/types.ts +16 -0
- package/src/notifications/__tests__/copy-composer.test.ts +244 -0
- package/src/notifications/access-request-copy.ts +298 -0
- package/src/notifications/adapters/slack.ts +3 -3
- package/src/notifications/adapters/telegram.ts +2 -1
- package/src/notifications/copy-composer.ts +49 -267
- package/src/notifications/decision-engine.ts +16 -35
- package/src/notifications/home-feed-side-effect.ts +1 -6
- package/src/oauth/oauth-store.ts +0 -9
- package/src/permissions/checker.test.ts +83 -1
- package/src/permissions/checker.ts +25 -2
- package/src/permissions/gateway-threshold-reader.test.ts +182 -0
- package/src/permissions/gateway-threshold-reader.ts +80 -0
- package/src/platform/client.ts +1 -3
- package/src/platform/feature-gate.ts +3 -12
- package/src/plugin-api/constants.ts +4 -2
- package/src/plugin-api/index.ts +63 -11
- package/src/plugin-api/types.ts +236 -71
- package/src/plugins/defaults/compaction/compact.ts +66 -2
- package/src/plugins/defaults/compaction/context-overflow-reducer.ts +240 -32
- package/src/plugins/defaults/compaction/corrected-target.ts +53 -0
- package/src/{daemon/context-overflow-policy.ts → plugins/defaults/compaction/overflow-policy.ts} +1 -1
- package/src/plugins/defaults/compaction/window-manager.ts +303 -1
- package/src/plugins/defaults/empty-response/hooks/post-model-call.ts +173 -0
- package/src/plugins/defaults/empty-response/hooks/stop.ts +11 -115
- package/src/plugins/defaults/empty-response/nudge-state-store.ts +46 -0
- package/src/plugins/defaults/history-repair/hooks/post-model-call.ts +50 -0
- package/src/plugins/defaults/history-repair/hooks/stop.ts +22 -0
- package/src/plugins/defaults/history-repair/repair-state-store.ts +51 -0
- package/src/plugins/defaults/history-repair/terminal.ts +39 -2
- package/src/plugins/defaults/image-recovery/detect.ts +25 -0
- package/src/plugins/defaults/image-recovery/hooks/post-model-call.ts +73 -0
- package/src/plugins/defaults/image-recovery/hooks/stop.ts +22 -0
- package/src/plugins/defaults/image-recovery/image-recovery-state-store.ts +48 -0
- package/src/plugins/defaults/image-recovery/package.json +14 -0
- package/src/{daemon/persist-unsendable-image.ts → plugins/defaults/image-recovery/recover.ts} +67 -14
- package/src/plugins/defaults/index.ts +71 -5
- package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +76 -112
- package/src/plugins/defaults/memory-retrieval/hooks/{user-prompt-submit-temp.ts → user-prompt-submit.ts} +83 -74
- package/src/plugins/defaults/memory-retrieval/injector-chain.ts +14 -8
- package/src/plugins/defaults/memory-retrieval/injectors.ts +2 -18
- package/src/plugins/defaults/memory-retrieval/package.json +14 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +1157 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +683 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +161 -140
- package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +160 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +335 -316
- package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +145 -53
- package/src/plugins/defaults/memory-v3-shadow/__tests__/render-injection.test.ts +38 -1
- package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +18 -8
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +112 -71
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +192 -51
- package/src/plugins/defaults/memory-v3-shadow/__tests__/types.test.ts +4 -16
- package/src/plugins/defaults/memory-v3-shadow/card.test.ts +173 -0
- package/src/plugins/defaults/memory-v3-shadow/card.ts +116 -0
- package/src/plugins/defaults/memory-v3-shadow/core-set.test.ts +104 -0
- package/src/plugins/defaults/memory-v3-shadow/core-set.ts +59 -0
- package/src/plugins/defaults/memory-v3-shadow/ever-injected-store.test.ts +305 -0
- package/src/plugins/defaults/memory-v3-shadow/ever-injected-store.ts +278 -0
- package/src/plugins/defaults/memory-v3-shadow/hot-set.test.ts +138 -0
- package/src/plugins/defaults/memory-v3-shadow/hot-set.ts +85 -0
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +331 -24
- package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +119 -13
- package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +169 -114
- package/src/plugins/defaults/memory-v3-shadow/page-content.ts +47 -16
- package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +144 -66
- package/src/plugins/defaults/memory-v3-shadow/prune.test.ts +758 -0
- package/src/plugins/defaults/memory-v3-shadow/prune.ts +471 -0
- package/src/plugins/defaults/memory-v3-shadow/render-injection.ts +68 -16
- package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +19 -12
- package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +96 -43
- package/src/plugins/defaults/memory-v3-shadow/types.ts +34 -17
- package/src/plugins/defaults/title-generate/hooks/stop.ts +9 -11
- package/src/plugins/pipeline.ts +8 -5
- package/src/plugins/types.ts +5 -51
- package/src/providers/cache-control.ts +26 -0
- package/src/providers/inference/__tests__/base-url-route-validation.test.ts +1 -2
- package/src/providers/model-catalog.ts +13 -1
- package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +147 -0
- package/src/providers/openai/chat-completions-provider.ts +46 -0
- package/src/providers/openai/responses-provider.ts +45 -0
- package/src/providers/registry.ts +0 -8
- package/src/runtime/__tests__/agent-wake.test.ts +0 -1
- package/src/runtime/agent-wake.ts +13 -0
- package/src/runtime/routes/__tests__/acp-routes.test.ts +151 -0
- package/src/runtime/routes/__tests__/consolidation-routes.test.ts +12 -50
- package/src/runtime/routes/__tests__/conversation-surface-routes.test.ts +322 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +0 -62
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +31 -11
- package/src/runtime/routes/acp-routes.test.ts +106 -0
- package/src/runtime/routes/acp-routes.ts +248 -2
- package/src/runtime/routes/browser-tabs-routes.ts +1 -1
- package/src/runtime/routes/channel-verification-routes.ts +14 -5
- package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +0 -14
- package/src/runtime/routes/consolidation-routes.ts +6 -82
- package/src/runtime/routes/conversation-list-routes.ts +6 -0
- package/src/runtime/routes/conversation-management-routes.ts +71 -0
- package/src/runtime/routes/identity-routes.ts +8 -0
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +33 -34
- package/src/runtime/routes/inference-provider-connection-routes.ts +0 -45
- package/src/runtime/routes/plugins-routes.ts +34 -45
- package/src/runtime/routes/schedule-routes.ts +43 -5
- package/src/runtime/routes/settings-routes.ts +140 -15
- package/src/runtime/routes/skills-routes.ts +18 -6
- package/src/runtime/services/__tests__/conversation-serializer.test.ts +140 -0
- package/src/runtime/services/conversation-serializer.ts +38 -1
- package/src/runtime/verification-outbound-actions.ts +147 -2
- package/src/runtime/verification-templates.ts +29 -3
- package/src/schedule/schedule-store.ts +19 -0
- package/src/skills/catalog-install.ts +77 -13
- package/src/tasks/task-scheduler.ts +1 -0
- package/src/telemetry/types.ts +66 -1
- package/src/telemetry/usage-telemetry-reporter.test.ts +542 -13
- package/src/telemetry/usage-telemetry-reporter.ts +213 -20
- package/src/tools/browser/__tests__/browser-execution-acquire.test.ts +49 -2
- package/src/tools/browser/__tests__/browser-status.test.ts +29 -5
- package/src/tools/browser/browser-execution.ts +27 -9
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +380 -4
- package/src/tools/browser/cdp-client/__tests__/host-bridge-cdp-client.test.ts +107 -0
- package/src/tools/browser/cdp-client/__tests__/types.test.ts +6 -1
- package/src/tools/browser/cdp-client/factory.ts +217 -17
- package/src/tools/browser/cdp-client/host-bridge-cdp-client.ts +67 -0
- package/src/tools/browser/cdp-client/types.ts +22 -2
- package/src/tools/credential-execution/make-authenticated-request.ts +2 -1
- package/src/tools/credential-execution/manage-secure-command-tool.ts +169 -164
- package/src/tools/credential-execution/run-authenticated-command.ts +2 -1
- package/src/tools/executor.ts +39 -7
- package/src/tools/registry.ts +387 -5
- package/src/tools/schedule/create.ts +16 -0
- package/src/tools/schedule/list.ts +12 -4
- package/src/tools/schedule/update.ts +12 -0
- package/src/tools/skills/load.ts +11 -6
- package/src/tools/terminal/safe-env.ts +2 -0
- package/src/tools/types.ts +65 -9
- package/src/tools/workspace-tools/loader.ts +673 -0
- package/src/usage/attribution.ts +28 -0
- package/src/util/device-id.ts +17 -3
- package/src/util/platform.ts +16 -0
- package/tsconfig.plugin-api.json +13 -0
- package/src/__tests__/plugin-external-api.test.ts +0 -68
- package/src/__tests__/plugin-skill-contribution.test.ts +0 -355
- package/src/daemon/message-types/browser.ts +0 -10
- package/src/notifications/__tests__/emit-signal-home-feed.test.ts +0 -187
- package/src/plugins/defaults/memory-v3-shadow/__tests__/fixtures/eval-turns.json +0 -36
- package/src/plugins/defaults/memory-v3-shadow/__tests__/fixtures/live-turns.json +0 -37
- package/src/plugins/defaults/memory-v3-shadow/__tests__/working-set-eviction.test.ts +0 -106
- package/src/plugins/defaults/memory-v3-shadow/__tests__/working-set-skeleton.test.ts +0 -44
- package/src/plugins/defaults/memory-v3-shadow/working-set.ts +0 -91
- package/src/plugins/external-api.ts +0 -114
- package/src/plugins/plugin-skill-contributions.ts +0 -292
|
@@ -24,7 +24,11 @@ import {
|
|
|
24
24
|
runAssistantDrivenCompaction,
|
|
25
25
|
runEmergencyCompaction,
|
|
26
26
|
} from "../../../context/compactor.js";
|
|
27
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
estimatePromptTokens,
|
|
29
|
+
estimateToolsTokens,
|
|
30
|
+
} from "../../../context/token-estimator.js";
|
|
31
|
+
import type { InjectionMode } from "../../../daemon/conversation-runtime-assembly.js";
|
|
28
32
|
import type {
|
|
29
33
|
ContentBlock,
|
|
30
34
|
Message,
|
|
@@ -33,6 +37,15 @@ import type {
|
|
|
33
37
|
} from "../../../providers/types.js";
|
|
34
38
|
import type { TrustClass } from "../../../runtime/actor-trust-resolver.js";
|
|
35
39
|
import { getLogger } from "../../../util/logger.js";
|
|
40
|
+
import {
|
|
41
|
+
createInitialReducerState,
|
|
42
|
+
reduceContextOverflow,
|
|
43
|
+
type ReducerConfig,
|
|
44
|
+
type ReducerState,
|
|
45
|
+
type ReducerStepResult,
|
|
46
|
+
} from "./context-overflow-reducer.js";
|
|
47
|
+
import { computeCorrectedOverflowTarget } from "./corrected-target.js";
|
|
48
|
+
import { resolveOverflowAction } from "./overflow-policy.js";
|
|
36
49
|
|
|
37
50
|
const log = getLogger("context-window");
|
|
38
51
|
|
|
@@ -85,6 +98,23 @@ export interface ContextWindowResult {
|
|
|
85
98
|
* compactions that did clear the threshold.
|
|
86
99
|
*/
|
|
87
100
|
exhausted?: boolean;
|
|
101
|
+
/**
|
|
102
|
+
* Runtime-injection volume the overflow reduction ladder settled on for the
|
|
103
|
+
* next provider call. The injection-downgrade rung lowers this to
|
|
104
|
+
* `"minimal"`; every other rung leaves it `"full"`. The agent loop forwards
|
|
105
|
+
* it to the post-compaction re-injection so the reduced prompt keeps the
|
|
106
|
+
* volume the ladder chose. Omitted on the ordinary (non-overflow) compaction
|
|
107
|
+
* path, where re-injection always runs at `"full"`.
|
|
108
|
+
*/
|
|
109
|
+
injectionMode?: InjectionMode;
|
|
110
|
+
/**
|
|
111
|
+
* Set when the overflow reduction ladder applied its terminal
|
|
112
|
+
* auto-compress-latest-turn rung. The agent loop reads it to classify the
|
|
113
|
+
* terminal exit when recovery is exhausted: a still-too-large turn after
|
|
114
|
+
* auto-compress ran is a `budget_yield_unrecovered`, without it a
|
|
115
|
+
* `context_too_large`. Omitted on the ordinary compaction path.
|
|
116
|
+
*/
|
|
117
|
+
autoCompressApplied?: boolean;
|
|
88
118
|
}
|
|
89
119
|
|
|
90
120
|
export interface ShouldCompactResult {
|
|
@@ -135,6 +165,52 @@ export interface EmergencyCompactOptions {
|
|
|
135
165
|
overrideProfile?: string | null;
|
|
136
166
|
}
|
|
137
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Turn-specific inputs for {@link ContextWindowManager.reduceOverflowOneRung}.
|
|
170
|
+
* The manager owns everything the reduction ladder derives from its own state
|
|
171
|
+
* (provider, system prompt, token budgets, conversation id, config, and the
|
|
172
|
+
* running reducer state); the caller supplies only what is specific to the
|
|
173
|
+
* overflow that triggered recovery.
|
|
174
|
+
*/
|
|
175
|
+
export interface OverflowRecoveryRungOptions {
|
|
176
|
+
/**
|
|
177
|
+
* Provider-reported token count from the overflow rejection, or `null` when
|
|
178
|
+
* it could not be parsed. Lowers the compaction target in proportion to the
|
|
179
|
+
* estimator's under-count so the reduced history lands under the provider's
|
|
180
|
+
* true ceiling rather than the under-counted estimate.
|
|
181
|
+
*/
|
|
182
|
+
actualTokens: number | null;
|
|
183
|
+
/**
|
|
184
|
+
* Whether the terminal auto-compress-latest-turn rung is permitted. The
|
|
185
|
+
* caller resolves this from the overflow policy; the manager never makes the
|
|
186
|
+
* policy call itself.
|
|
187
|
+
*/
|
|
188
|
+
allowAutoCompressLatestTurn: boolean;
|
|
189
|
+
/** Per-conversation inference-profile override for the summary call. */
|
|
190
|
+
overrideProfile?: string | null;
|
|
191
|
+
/** Trust class of the actor whose turn triggered overflow recovery. */
|
|
192
|
+
actorTrustClass?: TrustClass;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export interface OverflowRecoveryOptions {
|
|
196
|
+
/**
|
|
197
|
+
* Provider-reported token count from the overflow rejection, or `null` when
|
|
198
|
+
* it could not be parsed. Forwarded to the reduction ladder to correct the
|
|
199
|
+
* compaction target against the estimator's under-count.
|
|
200
|
+
*/
|
|
201
|
+
actualTokens: number | null;
|
|
202
|
+
/**
|
|
203
|
+
* Whether a human is present this turn. The manager resolves the
|
|
204
|
+
* auto-compress-latest-turn permission from the overflow policy using this
|
|
205
|
+
* flag, so callers signal interactivity rather than the policy verdict.
|
|
206
|
+
*/
|
|
207
|
+
isInteractive: boolean;
|
|
208
|
+
/** Per-conversation inference-profile override for the summary call. */
|
|
209
|
+
overrideProfile?: string | null;
|
|
210
|
+
/** Trust class of the actor whose turn triggered overflow recovery. */
|
|
211
|
+
actorTrustClass?: TrustClass;
|
|
212
|
+
}
|
|
213
|
+
|
|
138
214
|
export interface ContextWindowManagerOptions {
|
|
139
215
|
provider: Provider;
|
|
140
216
|
systemPrompt: string | (() => string);
|
|
@@ -218,6 +294,26 @@ export class ContextWindowManager {
|
|
|
218
294
|
*/
|
|
219
295
|
private _nonPersistedPrefixCount = 0;
|
|
220
296
|
private _resolvedSystemPrompt: string | undefined;
|
|
297
|
+
/**
|
|
298
|
+
* Reducer state for the in-progress overflow-recovery ladder, held across
|
|
299
|
+
* the successive {@link reduceOverflowOneRung} calls of a single turn so the
|
|
300
|
+
* ladder advances one rung per call. Reset to `undefined` at each turn
|
|
301
|
+
* boundary via {@link resetOverflowRecovery} so a new turn starts the ladder
|
|
302
|
+
* from the emergency rung.
|
|
303
|
+
*/
|
|
304
|
+
private _overflowReducerState: ReducerState | undefined;
|
|
305
|
+
/**
|
|
306
|
+
* The corrected compaction target and the prompt-token estimate it was
|
|
307
|
+
* derived from, computed once against the overflowing prompt on the first
|
|
308
|
+
* rung of a turn and reused across that turn's later rungs. The correction
|
|
309
|
+
* captures the estimator error the provider's actual token count revealed at
|
|
310
|
+
* the moment of overflow; re-deriving it against an already-reduced prompt
|
|
311
|
+
* would divide the original actual-token count by a smaller estimate and
|
|
312
|
+
* drive the target ever lower. Reset with {@link resetOverflowRecovery}.
|
|
313
|
+
*/
|
|
314
|
+
private _overflowTurnTarget:
|
|
315
|
+
| { targetTokens: number; estimatedInputTokens: number }
|
|
316
|
+
| undefined;
|
|
221
317
|
|
|
222
318
|
constructor(options: ContextWindowManagerOptions) {
|
|
223
319
|
this.provider = options.provider;
|
|
@@ -232,6 +328,16 @@ export class ContextWindowManager {
|
|
|
232
328
|
this.config = config;
|
|
233
329
|
}
|
|
234
330
|
|
|
331
|
+
/**
|
|
332
|
+
* Clear the overflow-recovery ladder so the next {@link reduceOverflowOneRung}
|
|
333
|
+
* call starts a fresh ladder from the emergency rung. Called at the turn
|
|
334
|
+
* boundary.
|
|
335
|
+
*/
|
|
336
|
+
resetOverflowRecovery(): void {
|
|
337
|
+
this._overflowReducerState = undefined;
|
|
338
|
+
this._overflowTurnTarget = undefined;
|
|
339
|
+
}
|
|
340
|
+
|
|
235
341
|
/** Leading non-persisted inherited-context messages the compactor preserves. */
|
|
236
342
|
get nonPersistedPrefixCount(): number {
|
|
237
343
|
return this._nonPersistedPrefixCount;
|
|
@@ -325,6 +431,202 @@ export class ContextWindowManager {
|
|
|
325
431
|
}
|
|
326
432
|
}
|
|
327
433
|
|
|
434
|
+
/**
|
|
435
|
+
* Advance the context-overflow reduction ladder by one rung against
|
|
436
|
+
* `messages`, holding the reducer state across the successive calls of a
|
|
437
|
+
* single turn (reset via {@link resetOverflowRecovery}). The compaction
|
|
438
|
+
* target is the manager's overflow preflight budget, lowered in proportion
|
|
439
|
+
* to the estimator error the provider's actual token count reveals, so the
|
|
440
|
+
* reduced history lands under the provider's true ceiling rather than the
|
|
441
|
+
* under-counted estimate.
|
|
442
|
+
*/
|
|
443
|
+
async reduceOverflowOneRung(
|
|
444
|
+
messages: Message[],
|
|
445
|
+
options: OverflowRecoveryRungOptions,
|
|
446
|
+
signal?: AbortSignal,
|
|
447
|
+
): Promise<ReducerStepResult> {
|
|
448
|
+
try {
|
|
449
|
+
return await this._reduceOverflowOneRung(messages, options, signal);
|
|
450
|
+
} finally {
|
|
451
|
+
this.clearSystemPromptCache();
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Drive the context-overflow reduction ladder one rung against `messages`
|
|
457
|
+
* and adapt the rung into a {@link ContextWindowResult} the agent loop's
|
|
458
|
+
* compaction path consumes. Resolves the auto-compress-latest-turn
|
|
459
|
+
* permission from the overflow policy — the manager owns that policy call,
|
|
460
|
+
* the ladder never makes it — and surfaces the rung's injection mode,
|
|
461
|
+
* terminal auto-compress flag, and exhaustion so the loop can re-inject at
|
|
462
|
+
* the chosen volume and classify the terminal exit when recovery runs out.
|
|
463
|
+
*/
|
|
464
|
+
async recoverContextOverflow(
|
|
465
|
+
messages: Message[],
|
|
466
|
+
options: OverflowRecoveryOptions,
|
|
467
|
+
signal?: AbortSignal,
|
|
468
|
+
): Promise<ContextWindowResult> {
|
|
469
|
+
const allowAutoCompressLatestTurn =
|
|
470
|
+
resolveOverflowAction({
|
|
471
|
+
overflowRecovery: this.config.overflowRecovery,
|
|
472
|
+
isInteractive: options.isInteractive,
|
|
473
|
+
}) === "auto_compress_latest_turn";
|
|
474
|
+
const step = await this.reduceOverflowOneRung(
|
|
475
|
+
messages,
|
|
476
|
+
{
|
|
477
|
+
actualTokens: options.actualTokens,
|
|
478
|
+
allowAutoCompressLatestTurn,
|
|
479
|
+
overrideProfile: options.overrideProfile,
|
|
480
|
+
actorTrustClass: options.actorTrustClass,
|
|
481
|
+
},
|
|
482
|
+
signal,
|
|
483
|
+
);
|
|
484
|
+
return this.overflowStepToResult(step, messages);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Adapt a reduction-ladder {@link ReducerStepResult} into the
|
|
489
|
+
* {@link ContextWindowResult} shape the agent loop's compaction path
|
|
490
|
+
* consumes. A summary rung carries a full compaction result (with the
|
|
491
|
+
* durable-commit and circuit-breaker fields); the non-summary rungs
|
|
492
|
+
* (truncation / media stubbing / injection downgrade) only transform the
|
|
493
|
+
* in-memory history, so they map to a no-op result that still propagates the
|
|
494
|
+
* reduced messages. Both forward the ladder's injection mode, exhaustion, and
|
|
495
|
+
* whether the terminal auto-compress rung was applied.
|
|
496
|
+
*/
|
|
497
|
+
private overflowStepToResult(
|
|
498
|
+
step: ReducerStepResult,
|
|
499
|
+
basis: Message[],
|
|
500
|
+
): ContextWindowResult {
|
|
501
|
+
const autoCompressApplied = step.state.appliedTiers.includes(
|
|
502
|
+
"auto_compress_latest_turn",
|
|
503
|
+
);
|
|
504
|
+
const base =
|
|
505
|
+
step.compactionResult ??
|
|
506
|
+
noopResult(step.messages, step.estimatedTokens, {
|
|
507
|
+
maxInputTokens: this.config.maxInputTokens,
|
|
508
|
+
thresholdTokens: Math.floor(
|
|
509
|
+
this.config.maxInputTokens *
|
|
510
|
+
this.resolveCompactionConfig().autoThreshold,
|
|
511
|
+
),
|
|
512
|
+
reason: `overflow recovery: ${step.tier}`,
|
|
513
|
+
});
|
|
514
|
+
return {
|
|
515
|
+
...base,
|
|
516
|
+
messages: step.messages,
|
|
517
|
+
estimatedInputTokens: step.estimatedTokens,
|
|
518
|
+
previousEstimatedInputTokens: this.estimateInputTokens(basis),
|
|
519
|
+
injectionMode: step.state.injectionMode,
|
|
520
|
+
autoCompressApplied,
|
|
521
|
+
exhausted: step.state.exhausted,
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
private async _reduceOverflowOneRung(
|
|
526
|
+
messages: Message[],
|
|
527
|
+
options: OverflowRecoveryRungOptions,
|
|
528
|
+
signal?: AbortSignal,
|
|
529
|
+
): Promise<ReducerStepResult> {
|
|
530
|
+
if (this.conversationId == null) {
|
|
531
|
+
throw new Error(
|
|
532
|
+
"ContextWindowManager has no conversationId — cannot run overflow recovery",
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
if (!this._overflowReducerState) {
|
|
536
|
+
this._overflowReducerState = createInitialReducerState();
|
|
537
|
+
this._overflowTurnTarget = this.deriveOverflowTurnTarget(
|
|
538
|
+
messages,
|
|
539
|
+
options.actualTokens,
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
const { targetTokens, estimatedInputTokens } = this._overflowTurnTarget!;
|
|
543
|
+
|
|
544
|
+
const config: ReducerConfig = {
|
|
545
|
+
providerName: this.estimationProviderName,
|
|
546
|
+
systemPrompt: this.systemPrompt,
|
|
547
|
+
contextWindow: this.config,
|
|
548
|
+
targetTokens,
|
|
549
|
+
toolTokenBudget: this.resolveTurnToolTokenBudget(),
|
|
550
|
+
conversationId: this.conversationId,
|
|
551
|
+
overrideProfile: options.overrideProfile ?? null,
|
|
552
|
+
actorTrustClass: options.actorTrustClass,
|
|
553
|
+
previousEstimatedInputTokens: estimatedInputTokens,
|
|
554
|
+
maxMiddleTierAttempts: this.config.overflowRecovery.maxAttempts,
|
|
555
|
+
allowAutoCompressLatestTurn: options.allowAutoCompressLatestTurn,
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
const step = await reduceContextOverflow(
|
|
559
|
+
messages,
|
|
560
|
+
config,
|
|
561
|
+
this._overflowReducerState,
|
|
562
|
+
signal,
|
|
563
|
+
);
|
|
564
|
+
this._overflowReducerState = step.state;
|
|
565
|
+
return step;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Compute the corrected compaction target for a turn's overflow recovery:
|
|
570
|
+
* the overflow preflight budget lowered in proportion to the estimator error
|
|
571
|
+
* the provider's actual token count reveals, so the reduced history lands
|
|
572
|
+
* under the provider's true ceiling rather than the under-counted estimate.
|
|
573
|
+
*/
|
|
574
|
+
private deriveOverflowTurnTarget(
|
|
575
|
+
messages: Message[],
|
|
576
|
+
actualTokens: number | null,
|
|
577
|
+
): { targetTokens: number; estimatedInputTokens: number } {
|
|
578
|
+
const estimatedInputTokens = estimatePromptTokens(
|
|
579
|
+
messages,
|
|
580
|
+
this.systemPrompt,
|
|
581
|
+
{
|
|
582
|
+
providerName: this.estimationProviderName,
|
|
583
|
+
toolTokenBudget: this.resolveTurnToolTokenBudget(),
|
|
584
|
+
},
|
|
585
|
+
);
|
|
586
|
+
const { targetTokens, estimationErrorRatio } =
|
|
587
|
+
computeCorrectedOverflowTarget({
|
|
588
|
+
preflightBudget: this.resolveOverflowPreflightBudget(messages.length),
|
|
589
|
+
actualTokens,
|
|
590
|
+
estimatedTokens: estimatedInputTokens,
|
|
591
|
+
});
|
|
592
|
+
if (estimationErrorRatio != null) {
|
|
593
|
+
log.warn(
|
|
594
|
+
{
|
|
595
|
+
actualTokens,
|
|
596
|
+
estimatedTokens: estimatedInputTokens,
|
|
597
|
+
estimationErrorRatio: estimationErrorRatio.toFixed(2),
|
|
598
|
+
targetTokens,
|
|
599
|
+
},
|
|
600
|
+
"Adjusting overflow compaction target based on observed estimation error",
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
return { targetTokens, estimatedInputTokens };
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Tool-token budget for the current turn's overflow recovery. Prefers the
|
|
608
|
+
* live tool set resolved for the turn — matching what the loop sends to the
|
|
609
|
+
* provider — and falls back to the constructor-time snapshot when no
|
|
610
|
+
* resolver is wired (legacy test paths, ad-hoc instantiation).
|
|
611
|
+
*/
|
|
612
|
+
private resolveTurnToolTokenBudget(): number {
|
|
613
|
+
const tools = this.resolveTools?.();
|
|
614
|
+
return tools ? estimateToolsTokens(tools) : this.toolTokenBudget;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* The token budget overflow recovery compacts below, derived from the
|
|
619
|
+
* manager's configured max-input cap and overflow-recovery safety margin.
|
|
620
|
+
* Long histories (> 50 messages) get a wider margin so the reduced prompt
|
|
621
|
+
* keeps clearance under the provider's true ceiling.
|
|
622
|
+
*/
|
|
623
|
+
private resolveOverflowPreflightBudget(messageCount: number): number {
|
|
624
|
+
const baseSafetyMargin = this.config.overflowRecovery.safetyMarginRatio;
|
|
625
|
+
const safetyMargin =
|
|
626
|
+
messageCount > 50 ? Math.max(baseSafetyMargin, 0.15) : baseSafetyMargin;
|
|
627
|
+
return Math.floor(this.config.maxInputTokens * (1 - safetyMargin));
|
|
628
|
+
}
|
|
629
|
+
|
|
328
630
|
private async _maybeCompact(
|
|
329
631
|
messages: Message[],
|
|
330
632
|
signal?: AbortSignal,
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default `post-model-call` hook: when the model yields a turn with no tool
|
|
3
|
+
* calls, decide whether to let the turn end, rewrite it for the user, or
|
|
4
|
+
* re-query the model.
|
|
5
|
+
*
|
|
6
|
+
* Two cases warrant intervention:
|
|
7
|
+
*
|
|
8
|
+
* 1. **Refusal stop.** The provider returned `stopReason === "refusal"` with no
|
|
9
|
+
* visible text (Anthropic's safety classifier zeroed the response) and no
|
|
10
|
+
* earlier turn this run already delivered visible text. The hook rewrites
|
|
11
|
+
* the turn into a plain-text apology (`REFUSAL_FALLBACK_TEXT`) by replacing
|
|
12
|
+
* {@link PostModelCallContext.content} and lets the turn end. A retry is
|
|
13
|
+
* deliberately not attempted: a safety-classifier refusal re-fires on a
|
|
14
|
+
* re-query, so the canned message is the intended terminal response.
|
|
15
|
+
* 2. **Empty turn after tool use.** The turn produced no visible text, follows
|
|
16
|
+
* at least one prior assistant turn this run, and no earlier turn this run
|
|
17
|
+
* already delivered visible text. The hook re-queries the model with
|
|
18
|
+
* `NUDGE_TEXT` (a tool trail exists to summarize, so a retry can recover a
|
|
19
|
+
* real answer). The retry is bounded to one pass per run by a one-shot
|
|
20
|
+
* per-conversation mark this hook sets; the sibling `stop` hook (see
|
|
21
|
+
* `./stop.ts`) clears it when the turn terminates, so the next run nudges
|
|
22
|
+
* afresh.
|
|
23
|
+
*
|
|
24
|
+
* Every other case leaves the decision at `"stop"` (the model said its piece,
|
|
25
|
+
* or there is nothing to act on).
|
|
26
|
+
*
|
|
27
|
+
* Both prior-turn signals are derived from the current response cycle — the
|
|
28
|
+
* messages after the last genuine user prompt (a user turn that isn't purely
|
|
29
|
+
* tool results). Scoping this way keeps prior conversation turns from polluting
|
|
30
|
+
* the signals, and deriving the boundary from history content rather than an
|
|
31
|
+
* index means mid-run compaction (which rewrites the array in place) can't
|
|
32
|
+
* invalidate it. A prior assistant turn this cycle implies a completed tool-use
|
|
33
|
+
* iteration (an empty turn nudges-and-continues without pushing an assistant
|
|
34
|
+
* message), so "a prior assistant turn exists" is the equivalent of "this is
|
|
35
|
+
* not the first model call".
|
|
36
|
+
*
|
|
37
|
+
* Defaults register before any user plugin, so this hook runs at the front of
|
|
38
|
+
* the `post-model-call` chain — later hooks see (and may override) its
|
|
39
|
+
* decision.
|
|
40
|
+
*
|
|
41
|
+
* Only a finalized, no-tool reply is actionable. A provider rejection carries
|
|
42
|
+
* no turn content to assess (a recovery hook like history-repair owns that),
|
|
43
|
+
* and a tool-bearing turn continues naturally — the loop runs the tools and
|
|
44
|
+
* ignores the decision — so the hook returns early for both.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
import type { PluginHookFn, PostModelCallContext } from "@vellumai/plugin-api";
|
|
48
|
+
|
|
49
|
+
import type { ContentBlock, Message } from "../../../../providers/types.js";
|
|
50
|
+
import {
|
|
51
|
+
isEmptyResponseNudged,
|
|
52
|
+
markEmptyResponseNudged,
|
|
53
|
+
} from "../nudge-state-store.js";
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Canonical nudge text for an empty turn after tool use. Must stay verbatim so
|
|
57
|
+
* a plugin that wraps the default sees a stable string.
|
|
58
|
+
*
|
|
59
|
+
* Wire-compat note: this is shown to the LLM, not the user. Edits here affect
|
|
60
|
+
* model behavior but not end-user UX directly.
|
|
61
|
+
*/
|
|
62
|
+
export const NUDGE_TEXT =
|
|
63
|
+
"<system_notice>Your previous response was empty. You must respond to the user with a summary of what you found or did. Do not use any tools — just respond with text.</system_notice>";
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* User-facing text a refusal turn is rewritten into. Used when the provider
|
|
67
|
+
* stops with `"refusal"` and no visible text — i.e. the safety classifier
|
|
68
|
+
* zeroed the response. Unlike `NUDGE_TEXT` (shown only to the model), this is
|
|
69
|
+
* the message the user actually reads in place of an empty assistant bubble.
|
|
70
|
+
*/
|
|
71
|
+
export const REFUSAL_FALLBACK_TEXT =
|
|
72
|
+
"Sorry — I wasn't able to generate a response to that. Please try rephrasing or asking in a different way.";
|
|
73
|
+
|
|
74
|
+
function hasVisibleText(content: ReadonlyArray<ContentBlock>): boolean {
|
|
75
|
+
return content.some(
|
|
76
|
+
(block) => block.type === "text" && block.text.trim().length > 0,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function hasToolUse(content: ReadonlyArray<ContentBlock>): boolean {
|
|
81
|
+
return content.some((block) => block.type === "tool_use");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isAssistantTurn(message: Message): boolean {
|
|
85
|
+
return message.role === "assistant";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** A user-role message carrying only tool results, not a fresh prompt. */
|
|
89
|
+
function isToolResultMessage(message: Message): boolean {
|
|
90
|
+
return (
|
|
91
|
+
message.role === "user" &&
|
|
92
|
+
message.content.length > 0 &&
|
|
93
|
+
message.content.every((block) => block.type === "tool_result")
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Messages belonging to the current response cycle: everything after the last
|
|
99
|
+
* genuine user prompt. Falls back to the whole history when none is found.
|
|
100
|
+
*/
|
|
101
|
+
function currentCycleMessages(
|
|
102
|
+
messages: ReadonlyArray<Message>,
|
|
103
|
+
): ReadonlyArray<Message> {
|
|
104
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
105
|
+
const message = messages[i];
|
|
106
|
+
if (message.role === "user" && !isToolResultMessage(message)) {
|
|
107
|
+
return messages.slice(i + 1);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return messages;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const postModelCall: PluginHookFn<PostModelCallContext> = async (ctx) => {
|
|
114
|
+
// A provider rejection carries no turn content to assess (a recovery hook
|
|
115
|
+
// owns the rejection); the sibling `stop` hook clears the mark when the turn
|
|
116
|
+
// terminates.
|
|
117
|
+
if (ctx.error) return;
|
|
118
|
+
// A tool-bearing turn continues mid-run — the loop runs the tools — so leave
|
|
119
|
+
// the mark intact to keep the one-nudge-per-run bound across tool iterations.
|
|
120
|
+
if (hasToolUse(ctx.content)) return;
|
|
121
|
+
|
|
122
|
+
const turnHasVisibleText = hasVisibleText(ctx.content);
|
|
123
|
+
|
|
124
|
+
const cycleMessages = currentCycleMessages(ctx.messages);
|
|
125
|
+
const priorAssistantTurns = cycleMessages.filter(isAssistantTurn);
|
|
126
|
+
const hadPriorAssistantTurn = priorAssistantTurns.length > 0;
|
|
127
|
+
const priorAssistantHadVisibleText = priorAssistantTurns.some((message) =>
|
|
128
|
+
hasVisibleText(message.content),
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
// Refusal stop: rewrite the empty turn into a user-facing apology and let it
|
|
132
|
+
// end. Skipped when an earlier turn this run already replied, so the apology
|
|
133
|
+
// never lands beneath a real answer.
|
|
134
|
+
if (
|
|
135
|
+
ctx.stopReason === "refusal" &&
|
|
136
|
+
!turnHasVisibleText &&
|
|
137
|
+
!priorAssistantHadVisibleText
|
|
138
|
+
) {
|
|
139
|
+
ctx.content = [{ type: "text", text: REFUSAL_FALLBACK_TEXT }];
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const isEmptyTurnAfterTools =
|
|
144
|
+
!turnHasVisibleText &&
|
|
145
|
+
hadPriorAssistantTurn &&
|
|
146
|
+
!priorAssistantHadVisibleText;
|
|
147
|
+
|
|
148
|
+
if (isEmptyTurnAfterTools) {
|
|
149
|
+
// Re-query once to recover a real answer. The one-shot per-conversation
|
|
150
|
+
// mark makes the hook self-limiting: a second empty turn this run finds the
|
|
151
|
+
// mark already set and lets the turn end rather than nudging again.
|
|
152
|
+
if (!isEmptyResponseNudged(ctx.conversationId)) {
|
|
153
|
+
markEmptyResponseNudged(ctx.conversationId);
|
|
154
|
+
ctx.messages.push({
|
|
155
|
+
role: "user",
|
|
156
|
+
content: [{ type: "text", text: NUDGE_TEXT }],
|
|
157
|
+
});
|
|
158
|
+
ctx.decision = "continue";
|
|
159
|
+
ctx.logger.warn(
|
|
160
|
+
{ plugin: "empty-response", conversationId: ctx.conversationId },
|
|
161
|
+
"Model returned empty response after tool results — retrying",
|
|
162
|
+
);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
ctx.logger.error(
|
|
167
|
+
{ plugin: "empty-response", conversationId: ctx.conversationId },
|
|
168
|
+
"Model returned empty response after tool results — retries exhausted",
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
export default postModelCall;
|
|
@@ -1,126 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Default `stop` hook:
|
|
3
|
-
*
|
|
2
|
+
* Default `stop` hook: clears the per-conversation empty-response nudge bound
|
|
3
|
+
* when a turn terminates.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* 2. **Empty turn after tool use.** The turn produced no visible text, follows
|
|
13
|
-
* at least one prior assistant turn this run, and no earlier turn this run
|
|
14
|
-
* already delivered visible text. Uses `NUDGE_TEXT`.
|
|
15
|
-
*
|
|
16
|
-
* Every other case leaves the decision at `"stop"` (the model said its piece,
|
|
17
|
-
* or there is nothing to nudge about). The retry cap is owned by the agent
|
|
18
|
-
* loop: this hook always asks to continue when a nudge is warranted, and the
|
|
19
|
-
* loop stops anyway once the run's nudge budget is spent.
|
|
20
|
-
*
|
|
21
|
-
* Both prior-turn signals are derived from the current response cycle — the
|
|
22
|
-
* messages after the last genuine user prompt (a user turn that isn't purely
|
|
23
|
-
* tool results). Scoping this way keeps prior conversation turns from polluting
|
|
24
|
-
* the signals, and deriving the boundary from history content rather than an
|
|
25
|
-
* index means mid-run compaction (which rewrites the array in place) can't
|
|
26
|
-
* invalidate it. A prior assistant turn this cycle implies a completed tool-use
|
|
27
|
-
* iteration (an empty turn nudges-and-continues without pushing an assistant
|
|
28
|
-
* message), so "a prior assistant turn exists" is the equivalent of "this is
|
|
29
|
-
* not the first model call".
|
|
30
|
-
*
|
|
31
|
-
* Defaults register before any user plugin, so this hook runs at the front of
|
|
32
|
-
* the `stop` chain — later hooks see (and may override) its decision.
|
|
5
|
+
* The `post-model-call` hook (see `./post-model-call.ts`) marks the bound when
|
|
6
|
+
* it re-queries the model after an empty turn. `stop` is the definitive
|
|
7
|
+
* terminal hook — it fires exactly once when the turn is truly ending, after
|
|
8
|
+
* every retry decision has been made — so clearing the bound here
|
|
9
|
+
* unconditionally guarantees the next run nudges afresh, no matter how the turn
|
|
10
|
+
* ended (a finalized reply, an abort, or a retry the loop's per-run backstop
|
|
11
|
+
* refused).
|
|
33
12
|
*/
|
|
34
13
|
|
|
35
14
|
import type { PluginHookFn, StopContext } from "@vellumai/plugin-api";
|
|
36
15
|
|
|
37
|
-
import
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Canonical nudge text for an empty turn after tool use. Must stay verbatim so
|
|
41
|
-
* a plugin that wraps the default sees a stable string.
|
|
42
|
-
*
|
|
43
|
-
* Wire-compat note: this is shown to the LLM, not the user. Edits here affect
|
|
44
|
-
* model behavior but not end-user UX directly.
|
|
45
|
-
*/
|
|
46
|
-
export const NUDGE_TEXT =
|
|
47
|
-
"<system_notice>Your previous response was empty. You must respond to the user with a summary of what you found or did. Do not use any tools — just respond with text.</system_notice>";
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Refusal-specific nudge. Used when the provider stops with `"refusal"` and no
|
|
51
|
-
* visible text — i.e. the safety classifier zeroed the response. Kept distinct
|
|
52
|
-
* from `NUDGE_TEXT` so the model gets context-appropriate guidance (no "summary
|
|
53
|
-
* of what you found or did" — there is no tool trail to summarize on a refusal).
|
|
54
|
-
*
|
|
55
|
-
* Wire-compat note: this is shown to the LLM, not the user. Edits here affect
|
|
56
|
-
* retry behavior but not end-user UX directly.
|
|
57
|
-
*/
|
|
58
|
-
export const REFUSAL_NUDGE_TEXT =
|
|
59
|
-
'<system_notice>Your previous response was empty because the upstream provider returned stop_reason="refusal". Please answer the user\'s last message directly with a plain-text response. Do not use any tools — just respond with text.</system_notice>';
|
|
60
|
-
|
|
61
|
-
function hasVisibleText(content: ReadonlyArray<ContentBlock>): boolean {
|
|
62
|
-
return content.some(
|
|
63
|
-
(block) => block.type === "text" && block.text.trim().length > 0,
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function isAssistantTurn(message: Message): boolean {
|
|
68
|
-
return message.role === "assistant";
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** A user-role message carrying only tool results, not a fresh prompt. */
|
|
72
|
-
function isToolResultMessage(message: Message): boolean {
|
|
73
|
-
return (
|
|
74
|
-
message.role === "user" &&
|
|
75
|
-
message.content.length > 0 &&
|
|
76
|
-
message.content.every((block) => block.type === "tool_result")
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Messages belonging to the current response cycle: everything after the last
|
|
82
|
-
* genuine user prompt. Falls back to the whole history when none is found.
|
|
83
|
-
*/
|
|
84
|
-
function currentCycleMessages(
|
|
85
|
-
messages: ReadonlyArray<Message>,
|
|
86
|
-
): ReadonlyArray<Message> {
|
|
87
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
88
|
-
const message = messages[i];
|
|
89
|
-
if (message.role === "user" && !isToolResultMessage(message)) {
|
|
90
|
-
return messages.slice(i + 1);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
return messages;
|
|
94
|
-
}
|
|
16
|
+
import { clearEmptyResponseNudged } from "../nudge-state-store.js";
|
|
95
17
|
|
|
96
18
|
const stop: PluginHookFn<StopContext> = async (ctx) => {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const appendNudge = (text: string): void => {
|
|
100
|
-
ctx.messages.push({ role: "user", content: [{ type: "text", text }] });
|
|
101
|
-
ctx.decision = "continue";
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
if (ctx.stopReason === "refusal" && !turnHasVisibleText) {
|
|
105
|
-
appendNudge(REFUSAL_NUDGE_TEXT);
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const cycleMessages = currentCycleMessages(ctx.messages);
|
|
110
|
-
const priorAssistantTurns = cycleMessages.filter(isAssistantTurn);
|
|
111
|
-
const hadPriorAssistantTurn = priorAssistantTurns.length > 0;
|
|
112
|
-
const priorAssistantHadVisibleText = priorAssistantTurns.some((message) =>
|
|
113
|
-
hasVisibleText(message.content),
|
|
114
|
-
);
|
|
115
|
-
|
|
116
|
-
const isEmptyTurnAfterTools =
|
|
117
|
-
!turnHasVisibleText &&
|
|
118
|
-
hadPriorAssistantTurn &&
|
|
119
|
-
!priorAssistantHadVisibleText;
|
|
120
|
-
|
|
121
|
-
if (isEmptyTurnAfterTools) {
|
|
122
|
-
appendNudge(NUDGE_TEXT);
|
|
123
|
-
}
|
|
19
|
+
clearEmptyResponseNudged(ctx.conversationId);
|
|
124
20
|
};
|
|
125
21
|
|
|
126
22
|
export default stop;
|