@vellumai/assistant 0.8.12 → 0.9.0-staging.2
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/AGENTS.md +0 -14
- package/ARCHITECTURE.md +45 -45
- package/README.md +1 -1
- package/bun.lock +200 -154
- package/docs/architecture/integrations.md +3 -3
- package/docs/architecture/memory.md +2 -2
- package/docs/architecture/security.md +10 -10
- package/docs/runbook-trusted-contacts.md +12 -12
- package/docs/skills.md +6 -6
- package/docs/workflows-testing.md +221 -0
- package/docs/workflows.md +510 -0
- package/examples/plugins/echo/README.md +5 -5
- package/knip.json +2 -0
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +105 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +12 -0
- package/openapi.yaml +7197 -5708
- package/package.json +8 -4
- package/scripts/generate-openapi.ts +66 -114
- package/src/__tests__/access-request-seed-content-blocks.test.ts +213 -0
- package/src/__tests__/adaptive-thinking-repair.test.ts +32 -3
- package/src/__tests__/agent-loop-output-hooks.test.ts +183 -0
- package/src/__tests__/agent-loop-regrowth-guard.test.ts +506 -0
- package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
- package/src/__tests__/agent-wake-override-profile.test.ts +77 -0
- package/src/__tests__/app-compiler.test.ts +7 -1
- package/src/__tests__/app-dir-path-guard.test.ts +27 -3
- package/src/__tests__/app-executors.test.ts +43 -0
- package/src/__tests__/approval-cascade.test.ts +0 -5
- package/src/__tests__/approval-routes-http.test.ts +91 -0
- package/src/__tests__/assistant-stream-state.test.ts +107 -0
- package/src/__tests__/browser-fill-credential.test.ts +3 -3
- package/src/__tests__/bundled-skill-retrieval-guard.test.ts +1 -1
- package/src/__tests__/compaction-events.test.ts +63 -7
- package/src/__tests__/compaction-trail-store.test.ts +74 -1
- package/src/__tests__/compaction.benchmark.test.ts +63 -41
- package/src/__tests__/compactor-low-watermark-cut.test.ts +349 -0
- package/src/__tests__/context-window-manager-compact-retry.test.ts +64 -0
- package/src/__tests__/conversation-abort-tool-results.test.ts +0 -5
- package/src/__tests__/conversation-confirmation-signals.test.ts +0 -5
- package/src/__tests__/conversation-history-web-search.test.ts +7 -0
- package/src/__tests__/conversation-process-callsite.test.ts +0 -5
- package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -5
- package/src/__tests__/conversation-queue.test.ts +0 -5
- package/src/__tests__/conversation-slash-queue.test.ts +0 -5
- package/src/__tests__/conversation-slash-unknown.test.ts +0 -5
- package/src/__tests__/conversation-speed-override.test.ts +0 -5
- package/src/__tests__/conversation-surfaces-data-persist.test.ts +97 -0
- package/src/__tests__/conversation-surfaces-task-progress.test.ts +67 -0
- package/src/__tests__/conversation-usage.test.ts +2 -0
- package/src/__tests__/conversation-workspace-injection.test.ts +0 -5
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -5
- package/src/__tests__/credential-broker-browser-fill.test.ts +2 -2
- package/src/__tests__/credential-broker-server-use.test.ts +2 -2
- package/src/__tests__/credential-broker.test.ts +1 -1
- package/src/__tests__/credential-prompt-route.test.ts +417 -0
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/credential-vault.test.ts +37 -0
- package/src/__tests__/db-schedule-syntax-migration.test.ts +24 -0
- package/src/__tests__/dynamic-page-surface.test.ts +219 -0
- package/src/__tests__/empty-state-greeting-cache.test.ts +94 -0
- package/src/__tests__/gateway-flag-listener.test.ts +24 -7
- package/src/__tests__/guardian-action-sweep.test.ts +56 -219
- package/src/__tests__/guardian-routing-invariants.test.ts +138 -0
- package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
- package/src/__tests__/list-messages-hidden-metadata.test.ts +99 -0
- package/src/__tests__/llm-request-log-source-clickhouse.test.ts +87 -1
- package/src/__tests__/llm-resolver.test.ts +115 -0
- package/src/__tests__/managed-profile-guard.test.ts +6 -5
- package/src/__tests__/max-tokens-continue-hook.test.ts +184 -0
- package/src/__tests__/media-generate-image.test.ts +20 -9
- package/src/__tests__/mock-gateway-ipc.ts +23 -0
- package/src/__tests__/model-intents.test.ts +1 -1
- package/src/__tests__/normalize-onboarding.test.ts +26 -0
- package/src/__tests__/notification-decision-strategy.test.ts +4 -2
- package/src/__tests__/notification-telegram-adapter.test.ts +21 -3
- package/src/__tests__/pending-interactions-resolved-event.test.ts +62 -0
- package/src/__tests__/post-turn-tool-result-truncation.test.ts +72 -18
- package/src/__tests__/require-fresh-approval.test.ts +425 -1
- package/src/__tests__/resolve-app-id.test.ts +56 -0
- package/src/__tests__/runtime-events-sse-parity.test.ts +2 -0
- package/src/__tests__/schedule-routes-workflow-validation.test.ts +408 -0
- package/src/__tests__/schedule-routes.test.ts +257 -4
- package/src/__tests__/schedule-store.test.ts +60 -0
- package/src/__tests__/schedule-tools.test.ts +247 -2
- package/src/__tests__/skill-execute-input.test.ts +85 -0
- package/src/__tests__/skill-secret-handling-guard.test.ts +21 -20
- package/src/__tests__/skills.test.ts +3 -3
- package/src/__tests__/slack-app-setup-skill-regression.test.ts +1 -1
- package/src/__tests__/subagent-tool-filtering.test.ts +50 -0
- package/src/__tests__/subagent-tool-gate-mode.test.ts +547 -0
- package/src/__tests__/system-prompt.test.ts +1 -1
- package/src/__tests__/task-progress-nudge-hook.test.ts +372 -0
- package/src/__tests__/task-scheduler.test.ts +299 -0
- package/src/__tests__/tool-approval-seed-content-blocks.test.ts +209 -0
- package/src/__tests__/tool-result-spool.test.ts +3 -1
- package/src/__tests__/workspace-migration-102-preserve-heartbeat-enabled-for-existing-workspaces.test.ts +181 -0
- package/src/__tests__/workspace-migration-103-upgrade-quality-profile-to-opus-4-8.test.ts +174 -0
- package/src/agent/compaction-circuit.ts +11 -0
- package/src/agent/loop.ts +181 -12
- package/src/api/constants/call-sites.ts +12 -0
- package/src/api/events/assistant-thinking-delta.ts +10 -0
- package/src/api/events/usage-update.ts +7 -0
- package/src/api/index.ts +4 -1
- package/src/api/responses/memory-v3-selection-log.ts +18 -11
- package/src/approvals/approval-primitive.ts +2 -2
- package/src/background-wake/background-wake-routes.test.ts +5 -2
- package/src/bundler/compiler-tools.ts +1 -1
- package/src/bundler/package-resolver.ts +0 -1
- package/src/calls/call-domain.ts +1 -1
- package/src/calls/guardian-action-sweep.ts +16 -93
- package/src/cli/AGENTS.md +4 -0
- package/src/cli/commands/__tests__/schedules.test.ts +430 -1
- package/src/cli/commands/credentials.ts +28 -24
- package/src/cli/commands/image-generation.ts +23 -9
- package/src/cli/commands/notifications.ts +1 -1
- package/src/cli/commands/plugins.ts +89 -46
- package/src/cli/commands/schedules.ts +384 -11
- package/src/cli/lib/__tests__/inspect-plugin.test.ts +69 -5
- package/src/cli/lib/__tests__/install-from-github.test.ts +15 -0
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +81 -4
- package/src/cli/lib/inspect-plugin.ts +62 -1
- package/src/cli/lib/install-from-github.ts +52 -5
- package/src/cli/lib/upgrade-plugin.ts +18 -0
- package/src/config/__tests__/workflows-schema.test.ts +60 -0
- package/src/config/bundled-skills/acp/SKILL.md +2 -2
- package/src/config/bundled-skills/app-builder/SKILL.md +1 -1
- package/src/config/bundled-skills/app-builder/tools/app-create.ts +6 -1
- package/src/config/bundled-skills/app-builder/tools/app-generate-icon.ts +7 -1
- package/src/config/bundled-skills/app-builder/tools/app-refresh.ts +7 -1
- package/src/config/bundled-skills/app-builder/tools/app-update.ts +10 -1
- package/src/config/bundled-skills/image-studio/SKILL.md +66 -19
- package/src/config/bundled-skills/image-studio/TOOLS.json +1 -6
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +22 -3
- package/src/config/bundled-skills/personal-page/SKILL.md +57 -0
- package/src/config/bundled-skills/personal-page/TOOLS.json +27 -0
- package/src/config/bundled-skills/personal-page/tools/app-refresh.ts +17 -0
- package/src/config/bundled-skills/schedule/SKILL.md +7 -2
- package/src/config/bundled-skills/schedule/TOOLS.json +48 -4
- package/src/config/bundled-skills/workflows/SKILL.md +214 -0
- package/src/config/bundled-skills/workflows/TOOLS.json +84 -0
- package/src/config/bundled-skills/workflows/tools/manage-workflows.ts +12 -0
- package/src/config/bundled-skills/workflows/tools/run-workflow.ts +12 -0
- package/src/config/bundled-tool-registry.ts +14 -2
- package/src/config/call-site-defaults.ts +5 -0
- package/src/config/feature-flag-registry.json +12 -4
- package/src/config/llm-context-resolution.ts +8 -0
- package/src/config/llm-resolver.ts +30 -0
- package/src/config/preloaded-apps/personal-page/src/components/About.tsx +22 -0
- package/src/config/preloaded-apps/personal-page/src/components/App.tsx +16 -0
- package/src/config/preloaded-apps/personal-page/src/components/Features.tsx +77 -0
- package/src/config/preloaded-apps/personal-page/src/components/Hero.tsx +57 -0
- package/src/config/preloaded-apps/personal-page/src/components/Pending.tsx +28 -0
- package/src/config/preloaded-apps/personal-page/src/components/animations.tsx +234 -0
- package/src/config/preloaded-apps/personal-page/src/components/icons.tsx +48 -0
- package/src/config/preloaded-apps/personal-page/src/components/media.ts +16 -0
- package/src/config/preloaded-apps/personal-page/src/index.html +20 -0
- package/src/config/preloaded-apps/personal-page/src/main.tsx +7 -0
- package/src/config/preloaded-apps/personal-page/src/profile-data.ts +82 -0
- package/src/config/preloaded-apps/personal-page/src/styles.css +759 -0
- package/src/config/schema.ts +2 -0
- package/src/config/schemas/call-site-catalog.ts +7 -0
- package/src/config/schemas/heartbeat.ts +4 -1
- package/src/config/schemas/llm.ts +33 -26
- package/src/config/schemas/memory-retrospective.ts +19 -0
- package/src/config/schemas/platform.ts +8 -0
- package/src/config/schemas/services.ts +5 -2
- package/src/config/schemas/workflows.ts +42 -0
- package/src/config/skills.ts +3 -3
- package/src/context/compactor.ts +273 -39
- package/src/context/post-turn-tool-result-truncation.ts +23 -6
- package/src/context/tool-result-spool.ts +12 -17
- package/src/credential-execution/executable-discovery.ts +1 -1
- package/src/credential-execution/process-manager.ts +37 -3
- package/src/credential-execution/prompted-credential.ts +205 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +14 -0
- package/src/daemon/conversation-process.ts +11 -2
- package/src/daemon/conversation-surfaces.ts +167 -3
- package/src/daemon/conversation-tool-setup.ts +103 -26
- package/src/daemon/conversation-usage.ts +2 -0
- package/src/daemon/conversation.ts +115 -11
- package/src/daemon/handlers/shared.ts +26 -14
- package/src/daemon/host-cu-proxy.ts +15 -12
- package/src/daemon/host-file-proxy.ts +15 -12
- package/src/daemon/host-transfer-proxy.ts +30 -24
- package/src/daemon/lifecycle.ts +40 -3
- package/src/daemon/message-protocol.ts +3 -0
- package/src/daemon/message-types/messages.ts +2 -10
- package/src/daemon/message-types/workflows.ts +49 -0
- package/src/daemon/parse-actual-tokens-from-error.test.ts +62 -1
- package/src/daemon/parse-actual-tokens-from-error.ts +43 -4
- package/src/daemon/process-message.ts +6 -0
- package/src/daemon/tool-setup-types.ts +57 -0
- package/src/daemon/wake-conversation-ops.ts +18 -0
- package/src/heartbeat/heartbeat-run-store.ts +8 -2
- package/src/home/feed-types.ts +1 -1
- package/src/ipc/gateway-flag-listener.ts +28 -6
- package/src/mcp/mcp-auth-state.ts +8 -20
- package/src/media/__tests__/image-models.test.ts +57 -0
- package/src/media/image-models.ts +66 -0
- package/src/memory/__tests__/auto-analysis-enqueue.test.ts +38 -0
- package/src/memory/__tests__/find-most-recent-retrospective-for.test.ts +12 -2
- package/src/memory/__tests__/memory-retrospective-job.test.ts +911 -34
- package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +227 -5
- package/src/memory/__tests__/memory-retrospective-state.test.ts +195 -0
- package/src/memory/__tests__/preloaded-apps.test.ts +85 -0
- package/src/memory/auto-analysis-enqueue.ts +14 -1
- package/src/memory/compaction-log-store-clickhouse.ts +6 -4
- package/src/memory/conversation-crud.ts +9 -2
- package/src/memory/conversation-disk-view.ts +1 -1
- package/src/memory/conversation-queries.ts +22 -7
- package/src/memory/db-init.ts +20 -0
- package/src/memory/db-maintenance.ts +16 -0
- package/src/memory/embedding-runtime-manager.ts +1 -1
- package/src/memory/llm-request-log-source-clickhouse.ts +112 -14
- package/src/memory/llm-request-log-source-local.ts +19 -1
- package/src/memory/llm-request-log-source.ts +35 -6
- package/src/memory/llm-request-log-store.ts +90 -2
- package/src/memory/memory-retrospective-constants.ts +9 -0
- package/src/memory/memory-retrospective-enqueue.ts +3 -6
- package/src/memory/memory-retrospective-fork-boundary.ts +94 -0
- package/src/memory/memory-retrospective-job.ts +500 -208
- package/src/memory/memory-retrospective-startup-cleanup.ts +97 -19
- package/src/memory/memory-retrospective-state.ts +85 -2
- package/src/memory/migrations/281-memory-retrospective-remembered-log.ts +40 -0
- package/src/memory/migrations/282-schedule-inference-profile.test.ts +77 -0
- package/src/memory/migrations/282-schedule-inference-profile.ts +26 -0
- package/src/memory/migrations/283-memory-v3-selections-message-id-and-sections.test.ts +102 -0
- package/src/memory/migrations/283-memory-v3-selections-message-id-and-sections.ts +53 -0
- package/src/memory/migrations/284-workflow-runs.ts +51 -0
- package/src/memory/migrations/285-schedule-workflow-mode.ts +26 -0
- package/src/memory/migrations/286-workflow-run-trust.ts +27 -0
- package/src/memory/migrations/287-conversation-origin-channel-index.ts +15 -0
- package/src/memory/migrations/288-backfill-origin-channel-from-bindings.ts +43 -0
- package/src/memory/migrations/289-contact-channels-unique-ext-user.ts +115 -0
- package/src/memory/migrations/290-schedule-capabilities.test.ts +77 -0
- package/src/memory/migrations/290-schedule-capabilities.ts +25 -0
- package/src/memory/migrations/__tests__/281-memory-retrospective-remembered-log.test.ts +96 -0
- package/src/memory/migrations/__tests__/289-contact-channels-unique-ext-user.test.ts +571 -0
- package/src/memory/migrations/index.ts +10 -0
- package/src/memory/preloaded-apps.ts +116 -0
- package/src/memory/schema/infrastructure.ts +4 -0
- package/src/memory/schema/memory-core.ts +4 -0
- package/src/memory/v2/__tests__/concept-page-frontmatter-schema.test.ts +45 -0
- package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +11 -7
- package/src/memory/v2/__tests__/page-store.test.ts +13 -2
- package/src/memory/v2/__tests__/qdrant.test.ts +24 -0
- package/src/memory/v2/frontmatter-sweep.ts +7 -6
- package/src/memory/v2/page-store.ts +4 -3
- package/src/memory/v2/qdrant.ts +42 -3
- package/src/memory/v2/types.ts +16 -10
- package/src/messaging/draft-store.ts +1 -1
- package/src/notifications/access-request-copy.ts +200 -113
- package/src/notifications/adapters/slack.ts +250 -111
- package/src/notifications/adapters/telegram.ts +7 -44
- package/src/notifications/approval-card-builder.ts +93 -0
- package/src/notifications/broadcaster.ts +74 -0
- package/src/notifications/conversation-pairing.ts +8 -6
- package/src/notifications/copy-composer.ts +32 -26
- package/src/notifications/decision-engine.ts +59 -7
- package/src/notifications/guardian-question-mode.ts +145 -155
- package/src/notifications/home-feed-side-effect.ts +28 -11
- package/src/notifications/notification-utils.ts +66 -0
- package/src/notifications/signal.ts +6 -0
- package/src/notifications/tool-approval-copy.ts +142 -0
- package/src/notifications/types.ts +19 -0
- package/src/permissions/threshold.ts +11 -0
- package/src/plugin-api/types.ts +16 -4
- package/src/plugins/defaults/compaction/window-manager.ts +44 -0
- package/src/plugins/defaults/index.ts +46 -0
- package/src/plugins/defaults/max-tokens-continue/continue-state-store.ts +53 -0
- package/src/plugins/defaults/max-tokens-continue/hooks/post-model-call.ts +80 -0
- package/src/plugins/defaults/max-tokens-continue/hooks/stop.ts +20 -0
- package/src/plugins/defaults/max-tokens-continue/package.json +14 -0
- package/src/plugins/defaults/memory-retrieval/hooks/__tests__/user-prompt-submit.test.ts +37 -0
- package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +29 -1
- package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +8 -3
- package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +4 -2
- package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +28 -18
- package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +67 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +122 -22
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +2 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +63 -1
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +61 -18
- package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +1 -1
- package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +39 -10
- package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +34 -1
- package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +112 -47
- package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +78 -15
- package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +206 -0
- package/src/plugins/defaults/task-progress-nudge/package.json +15 -0
- package/src/prompts/__tests__/system-prompt.test.ts +100 -1
- package/src/prompts/__tests__/task-progress-hint-section.test.ts +5 -7
- package/src/prompts/normalize-onboarding.ts +2 -0
- package/src/prompts/persona-resolver.ts +3 -0
- package/src/prompts/system-prompt.ts +51 -2
- package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +3 -1
- package/src/prompts/templates/system-sections.ts +8 -3
- package/src/providers/call-site-routing.ts +6 -3
- package/src/providers/fireworks/client.ts +3 -0
- package/src/providers/inference/auth.ts +52 -46
- package/src/providers/model-intents.ts +2 -2
- package/src/providers/openai/__tests__/coerce-object-args.test.ts +105 -0
- package/src/providers/openai/chat-completions-provider.ts +47 -9
- package/src/providers/openai/coerce-object-args.ts +104 -0
- package/src/providers/retry.ts +8 -5
- package/src/providers/types.ts +10 -0
- package/src/runtime/__tests__/agent-wake.test.ts +629 -7
- package/src/runtime/access-request-helper.ts +16 -9
- package/src/runtime/agent-wake.ts +302 -51
- package/src/runtime/assistant-stream-state.ts +141 -8
- package/src/runtime/background-job-runner.ts +9 -0
- package/src/runtime/channel-approval-types.ts +1 -0
- package/src/runtime/channel-invite-transports/telegram.ts +6 -5
- package/src/runtime/channel-invite-transports/voice.ts +2 -2
- package/src/runtime/channel-invite-types.ts +4 -2
- package/src/runtime/channel-retry-sweep.ts +19 -41
- package/src/runtime/finalize-event-delivery.ts +72 -0
- package/src/runtime/guardian-action-message-composer.ts +0 -54
- package/src/runtime/http-server.ts +6 -14
- package/src/runtime/http-types.ts +0 -1
- package/src/runtime/message-composer-types.ts +0 -9
- package/src/runtime/middleware/__tests__/rate-limiter.test.ts +63 -0
- package/src/runtime/middleware/auth.ts +27 -3
- package/src/runtime/middleware/rate-limiter.ts +28 -1
- package/src/runtime/migrations/vbundle-builder.ts +6 -5
- package/src/runtime/pending-interactions.ts +20 -1
- package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +232 -173
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +18 -0
- package/src/runtime/routes/__tests__/retrospective-routes.test.ts +436 -0
- package/src/runtime/routes/__tests__/surface-action-routes.test.ts +11 -0
- package/src/runtime/routes/approval-routes.ts +35 -8
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +2 -1
- package/src/runtime/routes/btw-routes.ts +37 -1
- package/src/runtime/routes/channel-delivery-routes.ts +11 -7
- package/src/runtime/routes/channel-route-definitions.ts +3 -0
- package/src/runtime/routes/channel-route-shared.ts +3 -1
- package/src/runtime/routes/consolidation-routes.ts +17 -13
- package/src/runtime/routes/conversation-compaction-routes.ts +159 -119
- package/src/runtime/routes/conversation-list-routes.ts +41 -4
- package/src/runtime/routes/conversation-query-routes.ts +196 -11
- package/src/runtime/routes/conversation-routes.ts +15 -1
- package/src/runtime/routes/credential-prompt-routes.ts +39 -17
- package/src/runtime/routes/empty-state-greeting-cache.ts +65 -0
- package/src/runtime/routes/heartbeat-routes.ts +18 -13
- package/src/runtime/routes/image-generation-routes.ts +20 -2
- package/src/runtime/routes/inbound-message-handler.ts +13 -12
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +32 -31
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +7 -5
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +7 -21
- package/src/runtime/routes/inbound-stages/escalation-intercept.ts +5 -5
- package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +6 -15
- package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +1 -1
- package/src/runtime/routes/index.ts +6 -0
- package/src/runtime/routes/log-export/AGENTS.md +1 -1
- package/src/runtime/routes/log-export/workspace-allowlist.ts +1 -1
- package/src/runtime/routes/migration-routes.ts +5 -9
- package/src/runtime/routes/plugins-routes.ts +26 -0
- package/src/runtime/routes/ps-routes.ts +10 -8
- package/src/runtime/routes/retrospective-routes.ts +235 -0
- package/src/runtime/routes/runs-pagination.ts +75 -0
- package/src/runtime/routes/schedule-routes.ts +367 -59
- package/src/runtime/routes/sounds-config-routes.ts +239 -0
- package/src/runtime/routes/surface-action-routes.ts +84 -4
- package/src/runtime/routes/workflow-routes.test.ts +372 -0
- package/src/runtime/routes/workflow-routes.ts +363 -0
- package/src/runtime/routes/workspace-routes.test.ts +61 -1
- package/src/runtime/routes/workspace-routes.ts +26 -1
- package/src/runtime/services/__tests__/analyze-conversation.test.ts +38 -0
- package/src/runtime/services/analyze-conversation.ts +26 -13
- package/src/schedule/inference-profile.ts +28 -0
- package/src/schedule/schedule-store.ts +152 -4
- package/src/schedule/scheduler-types.ts +6 -0
- package/src/schedule/scheduler.ts +96 -0
- package/src/security/secret-allowlist.ts +1 -1
- package/src/skills/path-classifier.ts +1 -1
- package/src/tools/apps/executors.ts +23 -0
- package/src/tools/apps/resolve-app-id.ts +42 -0
- package/src/tools/browser/browser-execution.ts +9 -11
- package/src/tools/credentials/broker.ts +4 -4
- package/src/tools/credentials/vault.ts +26 -137
- package/src/tools/executor.ts +69 -0
- package/src/tools/flag-gated-tools.test.ts +76 -0
- package/src/tools/permission-checker.ts +8 -1
- package/src/tools/registry.ts +51 -0
- package/src/tools/schedule/create.ts +77 -1
- package/src/tools/schedule/list.ts +1 -0
- package/src/tools/schedule/update.ts +73 -1
- package/src/tools/skills/execute.ts +56 -0
- package/src/tools/terminal/shell.ts +1 -1
- package/src/tools/ui-surface/definitions.ts +91 -2
- package/src/tools/workflows/manage-workflows.ts +183 -0
- package/src/tools/workflows/run-workflow.test.ts +442 -0
- package/src/tools/workflows/run-workflow.ts +88 -0
- package/src/types/onboarding-context.ts +2 -0
- package/src/usage/attribution.ts +24 -0
- package/src/util/canonicalize-identity.ts +12 -3
- package/src/util/platform.ts +17 -17
- package/src/watcher/__tests__/engine.test.ts +24 -0
- package/src/watcher/__tests__/telemetry.test.ts +135 -0
- package/src/watcher/engine.ts +7 -0
- package/src/watcher/telemetry.ts +74 -0
- package/src/workflows/capabilities.test.ts +365 -0
- package/src/workflows/capabilities.ts +359 -0
- package/src/workflows/deterministic-stringify.ts +27 -0
- package/src/workflows/engine-integration.test.ts +656 -0
- package/src/workflows/engine.test.ts +1144 -0
- package/src/workflows/engine.ts +1078 -0
- package/src/workflows/fanout-load.test.ts +168 -0
- package/src/workflows/journal-store.test.ts +369 -0
- package/src/workflows/journal-store.ts +470 -0
- package/src/workflows/leaf-runner.test.ts +704 -0
- package/src/workflows/leaf-runner.ts +589 -0
- package/src/workflows/library.test.ts +134 -0
- package/src/workflows/library.ts +124 -0
- package/src/workflows/run-manager.test.ts +711 -0
- package/src/workflows/run-manager.ts +593 -0
- package/src/workflows/sandbox-escape.test.ts +339 -0
- package/src/workflows/sandbox.test.ts +251 -0
- package/src/workflows/sandbox.ts +447 -0
- package/src/workspace/adaptive-thinking-repair.ts +33 -11
- package/src/workspace/migrations/021-move-signals-to-workspace.ts +1 -1
- package/src/workspace/migrations/022-move-hooks-to-workspace.ts +1 -1
- package/src/workspace/migrations/026-backfill-install-meta.ts +1 -1
- package/src/workspace/migrations/030-seed-pkb-autoinject.ts +2 -1
- package/src/workspace/migrations/031-drop-user-md.ts +1 -4
- package/src/workspace/migrations/048-remove-workspace-hooks.ts +1 -1
- package/src/workspace/migrations/056-release-notes-inference-profile-reordering.ts +5 -2
- package/src/workspace/migrations/061-move-backup-key-to-workspace.ts +1 -1
- package/src/workspace/migrations/082-backfill-managed-profile-labels.ts +8 -2
- package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +41 -14
- package/src/workspace/migrations/102-preserve-heartbeat-enabled-for-existing-workspaces.ts +69 -0
- package/src/workspace/migrations/103-upgrade-quality-profile-to-opus-4-8.ts +83 -0
- package/src/workspace/migrations/104-recheck-adaptive-thinking-model-implied-anthropic.ts +133 -0
- package/src/workspace/migrations/registry.ts +6 -0
- package/src/workspace/migrations/runner.ts +1 -1
- package/tsconfig.json +1 -1
- package/src/__tests__/guardian-action-copy-generator.test.ts +0 -200
- package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -579
- package/src/__tests__/guardian-action-store.test.ts +0 -106
- package/src/daemon/guardian-action-generators.ts +0 -71
- package/src/memory/guardian-action-store.ts +0 -484
- package/src/runtime/guardian-action-grant-minter.ts +0 -150
|
@@ -219,6 +219,131 @@ describe("agent loop output hooks", () => {
|
|
|
219
219
|
});
|
|
220
220
|
});
|
|
221
221
|
|
|
222
|
+
test("post-model-call can append a tool_use block to invoke a tool as if the model had called it", async () => {
|
|
223
|
+
// GIVEN a plugin whose post-model-call hook appends a tool call (with an
|
|
224
|
+
// empty id, so the host must assign one) to an otherwise text-only reply,
|
|
225
|
+
// once, so the re-entry turn terminates the run.
|
|
226
|
+
let injected = false;
|
|
227
|
+
const executed: Array<{ name: string; input: unknown; id?: string }> = [];
|
|
228
|
+
registerOutputHookPlugin({
|
|
229
|
+
postModelCall: (ctx) => {
|
|
230
|
+
if (injected) return;
|
|
231
|
+
injected = true;
|
|
232
|
+
ctx.content = [
|
|
233
|
+
...ctx.content,
|
|
234
|
+
{
|
|
235
|
+
type: "tool_use",
|
|
236
|
+
id: "",
|
|
237
|
+
name: "ui_show",
|
|
238
|
+
input: { surface: "x" },
|
|
239
|
+
},
|
|
240
|
+
];
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
// AND a provider that streams a visible answer, then ends after re-entry.
|
|
244
|
+
const { provider } = createMockProvider([
|
|
245
|
+
textResponse("here is your answer"),
|
|
246
|
+
textResponse("done"),
|
|
247
|
+
]);
|
|
248
|
+
const loop = new AgentLoop({
|
|
249
|
+
provider: provider,
|
|
250
|
+
systemPrompt: "system",
|
|
251
|
+
conversationId: "test-conversation",
|
|
252
|
+
tools: [
|
|
253
|
+
{
|
|
254
|
+
name: "ui_show",
|
|
255
|
+
description: "",
|
|
256
|
+
input_schema: { type: "object", properties: {} },
|
|
257
|
+
},
|
|
258
|
+
],
|
|
259
|
+
toolExecutor: async (name, input, _onOutput, toolUseId) => {
|
|
260
|
+
executed.push({ name, input, id: toolUseId });
|
|
261
|
+
return { content: "shown", isError: false };
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
const events: AgentEvent[] = [];
|
|
265
|
+
|
|
266
|
+
// WHEN the loop runs
|
|
267
|
+
const { history } = await loop.run({
|
|
268
|
+
requestId: "test-request",
|
|
269
|
+
messages: [userMessage],
|
|
270
|
+
onEvent: collect(events),
|
|
271
|
+
trust: { sourceChannel: "vellum", trustClass: "unknown" },
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// THEN the hook-added call ran through the normal tool path exactly once
|
|
275
|
+
expect(executed).toHaveLength(1);
|
|
276
|
+
expect(executed[0]).toMatchObject({
|
|
277
|
+
name: "ui_show",
|
|
278
|
+
input: { surface: "x" },
|
|
279
|
+
});
|
|
280
|
+
// AND the host backfilled a non-empty id for the empty-id block
|
|
281
|
+
expect(executed[0].id).toBeTruthy();
|
|
282
|
+
// AND a tool_use event was emitted for the injected call
|
|
283
|
+
expect(
|
|
284
|
+
events.some((e) => e.type === "tool_use" && e.name === "ui_show"),
|
|
285
|
+
).toBe(true);
|
|
286
|
+
// AND the live text reply was preserved, not discarded
|
|
287
|
+
expect(streamedText(events)).toContain("here is your answer");
|
|
288
|
+
// AND the persisted assistant turn carries the injected tool call
|
|
289
|
+
const injectedTurn = history.find(
|
|
290
|
+
(m) =>
|
|
291
|
+
m.role === "assistant" &&
|
|
292
|
+
m.content.some((b) => b.type === "tool_use" && b.name === "ui_show"),
|
|
293
|
+
);
|
|
294
|
+
expect(injectedTurn).toBeDefined();
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test("post-model-call can drop a model tool_use block to suppress the call", async () => {
|
|
298
|
+
// GIVEN a model reply that carries a tool call the hook decides to remove.
|
|
299
|
+
const toolAndText: ProviderResponse = {
|
|
300
|
+
content: [
|
|
301
|
+
{ type: "text", text: "answer" },
|
|
302
|
+
{ type: "tool_use", id: "t1", name: "noop", input: {} },
|
|
303
|
+
],
|
|
304
|
+
model: "mock-model",
|
|
305
|
+
usage: { inputTokens: 1, outputTokens: 1 },
|
|
306
|
+
stopReason: "tool_use",
|
|
307
|
+
};
|
|
308
|
+
let executions = 0;
|
|
309
|
+
registerOutputHookPlugin({
|
|
310
|
+
postModelCall: (ctx) => {
|
|
311
|
+
ctx.content = ctx.content.filter((b) => b.type !== "tool_use");
|
|
312
|
+
},
|
|
313
|
+
});
|
|
314
|
+
const { provider } = createMockProvider([toolAndText]);
|
|
315
|
+
const loop = new AgentLoop({
|
|
316
|
+
provider: provider,
|
|
317
|
+
systemPrompt: "system",
|
|
318
|
+
conversationId: "test-conversation",
|
|
319
|
+
tools: [
|
|
320
|
+
{
|
|
321
|
+
name: "noop",
|
|
322
|
+
description: "",
|
|
323
|
+
input_schema: { type: "object", properties: {} },
|
|
324
|
+
},
|
|
325
|
+
],
|
|
326
|
+
toolExecutor: async () => {
|
|
327
|
+
executions++;
|
|
328
|
+
return { content: "ok", isError: false };
|
|
329
|
+
},
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// WHEN the loop runs
|
|
333
|
+
const { history } = await loop.run({
|
|
334
|
+
requestId: "test-request",
|
|
335
|
+
messages: [userMessage],
|
|
336
|
+
onEvent: collect([]),
|
|
337
|
+
trust: { sourceChannel: "vellum", trustClass: "unknown" },
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// THEN the dropped call never executed and the turn ended on the text reply
|
|
341
|
+
expect(executions).toBe(0);
|
|
342
|
+
expect(
|
|
343
|
+
lastAssistant(history).content.some((b) => b.type === "tool_use"),
|
|
344
|
+
).toBe(false);
|
|
345
|
+
});
|
|
346
|
+
|
|
222
347
|
test("pre-model-call can edit the outbound system prompt", async () => {
|
|
223
348
|
registerOutputHookPlugin({
|
|
224
349
|
preModelCall: (ctx) => {
|
|
@@ -311,6 +436,64 @@ describe("agent loop output hooks", () => {
|
|
|
311
436
|
expect(streamedText(events)).toBe("PARTIAL ANSWER");
|
|
312
437
|
});
|
|
313
438
|
|
|
439
|
+
test("max_tokens turn: a hook-added tool_use is dropped, not executed or persisted", async () => {
|
|
440
|
+
// GIVEN a hook that appends a tool call while transforming a truncated
|
|
441
|
+
// reply. A max-tokens turn short-circuits before the executor runs, so the
|
|
442
|
+
// injected call must be dropped rather than persisted without a result.
|
|
443
|
+
const executed: string[] = [];
|
|
444
|
+
registerOutputHookPlugin({
|
|
445
|
+
postModelCall: (ctx) => {
|
|
446
|
+
ctx.content = [
|
|
447
|
+
...ctx.content,
|
|
448
|
+
{ type: "tool_use", id: "", name: "ui_show", input: {} },
|
|
449
|
+
];
|
|
450
|
+
},
|
|
451
|
+
});
|
|
452
|
+
// AND a provider whose only reply is truncated at the token limit.
|
|
453
|
+
const truncated: ProviderResponse = {
|
|
454
|
+
content: [{ type: "text", text: "partial" }],
|
|
455
|
+
model: "mock-model",
|
|
456
|
+
usage: { inputTokens: 1, outputTokens: 1 },
|
|
457
|
+
stopReason: "max_tokens",
|
|
458
|
+
};
|
|
459
|
+
const { provider } = createMockProvider([truncated]);
|
|
460
|
+
const loop = new AgentLoop({
|
|
461
|
+
provider: provider,
|
|
462
|
+
systemPrompt: "system",
|
|
463
|
+
conversationId: "test-conversation",
|
|
464
|
+
tools: [
|
|
465
|
+
{
|
|
466
|
+
name: "ui_show",
|
|
467
|
+
description: "",
|
|
468
|
+
input_schema: { type: "object", properties: {} },
|
|
469
|
+
},
|
|
470
|
+
],
|
|
471
|
+
toolExecutor: async (name) => {
|
|
472
|
+
executed.push(name);
|
|
473
|
+
return { content: "shown", isError: false };
|
|
474
|
+
},
|
|
475
|
+
});
|
|
476
|
+
const events: AgentEvent[] = [];
|
|
477
|
+
|
|
478
|
+
// WHEN the loop runs
|
|
479
|
+
const { history } = await loop.run({
|
|
480
|
+
requestId: "test-request",
|
|
481
|
+
messages: [userMessage],
|
|
482
|
+
onEvent: collect(events),
|
|
483
|
+
trust: { sourceChannel: "vellum", trustClass: "unknown" },
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
// THEN the injected call never executed
|
|
487
|
+
expect(executed).toHaveLength(0);
|
|
488
|
+
expect(
|
|
489
|
+
events.some((e) => e.type === "tool_use" && e.name === "ui_show"),
|
|
490
|
+
).toBe(false);
|
|
491
|
+
// AND the persisted truncated turn carries no tool call
|
|
492
|
+
expect(
|
|
493
|
+
lastAssistant(history).content.some((b) => b.type === "tool_use"),
|
|
494
|
+
).toBe(false);
|
|
495
|
+
});
|
|
496
|
+
|
|
314
497
|
test("deferred final text passes through sensitive-output substitution", async () => {
|
|
315
498
|
// A tool returns a sensitive binding; the next assistant turn uses the
|
|
316
499
|
// placeholder (the persisted message must keep it — model never sees real
|
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regrowth hysteresis on the agent loop's budget gate.
|
|
3
|
+
*
|
|
4
|
+
* A proactive compaction pass that just ran records a post-compaction
|
|
5
|
+
* watermark. On a later gate crossing the loop skips compaction when the
|
|
6
|
+
* history has not regrown at least `minRegrowth` tokens past that watermark
|
|
7
|
+
* (re-compacting would not free more — the production "compaction thrash"
|
|
8
|
+
* failure mode). Overflow-driven compaction always bypasses the guard.
|
|
9
|
+
*
|
|
10
|
+
* These tests drive a single loop iteration with the gate pre-armed
|
|
11
|
+
* (`compactInPlace`) and assert whether a `context_compacting` event fires,
|
|
12
|
+
* by pre-seeding the loop's `compactionCircuit.lastPostCompactionEstimate`.
|
|
13
|
+
*/
|
|
14
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
15
|
+
|
|
16
|
+
import type { PostCompactContext } from "@vellumai/plugin-api";
|
|
17
|
+
|
|
18
|
+
import type { AgentEvent } from "../agent/loop.js";
|
|
19
|
+
import { AgentLoop } from "../agent/loop.js";
|
|
20
|
+
import type { ContextWindowConfig } from "../config/types.js";
|
|
21
|
+
import type { TrustContext } from "../daemon/trust-context.js";
|
|
22
|
+
import { HOOKS } from "../plugin-api/constants.js";
|
|
23
|
+
import {
|
|
24
|
+
createContextWindowManager,
|
|
25
|
+
disposeContextWindowManager,
|
|
26
|
+
getContextWindowManager,
|
|
27
|
+
} from "../plugins/defaults/compaction/manager-store.js";
|
|
28
|
+
import {
|
|
29
|
+
registerPlugin,
|
|
30
|
+
resetPluginRegistryForTests,
|
|
31
|
+
} from "../plugins/registry.js";
|
|
32
|
+
import type {
|
|
33
|
+
Message,
|
|
34
|
+
Provider,
|
|
35
|
+
ProviderResponse,
|
|
36
|
+
SendMessageOptions,
|
|
37
|
+
} from "../providers/types.js";
|
|
38
|
+
import { ContextOverflowError } from "../providers/types.js";
|
|
39
|
+
|
|
40
|
+
const testPostCompactPlugin = {
|
|
41
|
+
manifest: { name: "test-post-compact", version: "0.0.0" },
|
|
42
|
+
hooks: {
|
|
43
|
+
[HOOKS.POST_COMPACT]: async (input: PostCompactContext): Promise<void> => {
|
|
44
|
+
void input;
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function createMockProvider(responses: ProviderResponse[]): Provider {
|
|
50
|
+
let callIndex = 0;
|
|
51
|
+
return {
|
|
52
|
+
name: "mock",
|
|
53
|
+
async sendMessage(
|
|
54
|
+
_messages: Message[],
|
|
55
|
+
_options?: SendMessageOptions,
|
|
56
|
+
): Promise<ProviderResponse> {
|
|
57
|
+
const response = responses[callIndex] ?? responses[responses.length - 1];
|
|
58
|
+
callIndex++;
|
|
59
|
+
return response;
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function textResponse(text: string): ProviderResponse {
|
|
65
|
+
return {
|
|
66
|
+
content: [{ type: "text", text }],
|
|
67
|
+
model: "mock-model",
|
|
68
|
+
usage: { inputTokens: 10, outputTokens: 5 },
|
|
69
|
+
stopReason: "end_turn",
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function toolUseResponse(id: string): ProviderResponse {
|
|
74
|
+
return {
|
|
75
|
+
content: [{ type: "tool_use", id, name: "noop", input: {} }],
|
|
76
|
+
model: "mock-model",
|
|
77
|
+
usage: { inputTokens: 10, outputTokens: 5 },
|
|
78
|
+
stopReason: "tool_use",
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const CONVERSATION_ID = "regrowth-conversation";
|
|
83
|
+
|
|
84
|
+
const userMessage: Message = {
|
|
85
|
+
role: "user",
|
|
86
|
+
content: [{ type: "text", text: "Hello there, this is the turn body." }],
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Register a per-conversation manager whose `maybeCompact` returns a real
|
|
91
|
+
* (compacted) history so the loop records the post-compaction watermark, and
|
|
92
|
+
* surface a `setWatermark` knob to seed the regrowth state before the run.
|
|
93
|
+
*
|
|
94
|
+
* `proactiveExhausted` flips `maybeCompact` to report `exhausted: true` (it
|
|
95
|
+
* compacted but could not clear the gate — the floor-dominated case). The loop
|
|
96
|
+
* then commits no history and latches the per-turn suppression.
|
|
97
|
+
*/
|
|
98
|
+
function installManager(opts?: { proactiveExhausted?: boolean }): {
|
|
99
|
+
trust: TrustContext;
|
|
100
|
+
} {
|
|
101
|
+
const exhausted = opts?.proactiveExhausted ?? false;
|
|
102
|
+
createContextWindowManager({
|
|
103
|
+
provider: { name: "mock-provider" } as unknown as Provider,
|
|
104
|
+
systemPrompt: "system",
|
|
105
|
+
config: {} as unknown as ContextWindowConfig,
|
|
106
|
+
conversationId: CONVERSATION_ID,
|
|
107
|
+
});
|
|
108
|
+
const manager = getContextWindowManager(CONVERSATION_ID);
|
|
109
|
+
if (manager) {
|
|
110
|
+
manager.maybeCompact = (async () => ({
|
|
111
|
+
messages: [userMessage],
|
|
112
|
+
compacted: true,
|
|
113
|
+
exhausted,
|
|
114
|
+
})) as unknown as typeof manager.maybeCompact;
|
|
115
|
+
// Overflow-driven compaction routes through `recoverContextOverflow`, not
|
|
116
|
+
// `maybeCompact`. Stub it too so the overflow test exercises the gate's
|
|
117
|
+
// overflow branch.
|
|
118
|
+
manager.recoverContextOverflow = (async () => ({
|
|
119
|
+
messages: [userMessage],
|
|
120
|
+
compacted: true,
|
|
121
|
+
exhausted: false,
|
|
122
|
+
})) as unknown as typeof manager.recoverContextOverflow;
|
|
123
|
+
}
|
|
124
|
+
return { trust: { sourceChannel: "vellum", trustClass: "unknown" } };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function countCompactions(events: AgentEvent[]): number {
|
|
128
|
+
return events.filter((e) => e.type === "context_compacting").length;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function runOnce(args: {
|
|
132
|
+
seedWatermark: number | null;
|
|
133
|
+
overflow?: boolean;
|
|
134
|
+
}): Promise<AgentEvent[]> {
|
|
135
|
+
const provider = createMockProvider([textResponse("done")]);
|
|
136
|
+
const loop = new AgentLoop({
|
|
137
|
+
provider,
|
|
138
|
+
systemPrompt: "system",
|
|
139
|
+
conversationId: CONVERSATION_ID,
|
|
140
|
+
tools: [],
|
|
141
|
+
toolExecutor: async () => ({ content: "ok", isError: false }),
|
|
142
|
+
});
|
|
143
|
+
loop.compactionCircuit.lastPostCompactionEstimate = args.seedWatermark;
|
|
144
|
+
|
|
145
|
+
const events: AgentEvent[] = [];
|
|
146
|
+
await loop.run({
|
|
147
|
+
requestId: "req",
|
|
148
|
+
messages: [userMessage],
|
|
149
|
+
onEvent: (event) => {
|
|
150
|
+
events.push(event);
|
|
151
|
+
},
|
|
152
|
+
resolveContextWindow: () => ({
|
|
153
|
+
maxInputTokens: 10,
|
|
154
|
+
overflowRecovery: { enabled: true, safetyMarginRatio: 0 },
|
|
155
|
+
}),
|
|
156
|
+
compactInPlace: true,
|
|
157
|
+
...installManager(),
|
|
158
|
+
});
|
|
159
|
+
return events;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
describe("AgentLoop budget-gate regrowth guard", () => {
|
|
163
|
+
beforeEach(() => {
|
|
164
|
+
resetPluginRegistryForTests();
|
|
165
|
+
registerPlugin(testPostCompactPlugin);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
afterEach(() => {
|
|
169
|
+
disposeContextWindowManager(CONVERSATION_ID);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("skips compaction when the history has not regrown past the watermark", async () => {
|
|
173
|
+
// Watermark at 0 and a tiny real estimate → regrowth far below the
|
|
174
|
+
// minRegrowth floor (2048) → the gate skips compaction.
|
|
175
|
+
const events = await runOnce({ seedWatermark: 0 });
|
|
176
|
+
expect(countCompactions(events)).toBe(0);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("compacts when there is no prior watermark", async () => {
|
|
180
|
+
// No watermark recorded yet → the guard does not apply → compaction fires.
|
|
181
|
+
const events = await runOnce({ seedWatermark: null });
|
|
182
|
+
expect(countCompactions(events)).toBe(1);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("overflow-driven compaction bypasses the guard even with a blocking watermark", async () => {
|
|
186
|
+
// A watermark that would block proactive compaction is seeded, yet a
|
|
187
|
+
// provider-confirmed overflow must still compact. The provider rejects the
|
|
188
|
+
// first call as context-too-large; the gate then runs overflow recovery.
|
|
189
|
+
let throwOnce = true;
|
|
190
|
+
const provider: Provider = {
|
|
191
|
+
name: "mock",
|
|
192
|
+
async sendMessage(): Promise<ProviderResponse> {
|
|
193
|
+
if (throwOnce) {
|
|
194
|
+
throwOnce = false;
|
|
195
|
+
throw new ContextOverflowError("prompt too long", "mock", {
|
|
196
|
+
actualTokens: 999_999,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
return textResponse("done after overflow recovery");
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
const loop = new AgentLoop({
|
|
203
|
+
provider,
|
|
204
|
+
systemPrompt: "system",
|
|
205
|
+
conversationId: CONVERSATION_ID,
|
|
206
|
+
tools: [],
|
|
207
|
+
toolExecutor: async () => ({ content: "ok", isError: false }),
|
|
208
|
+
});
|
|
209
|
+
// Seed a watermark that WOULD block a proactive pass.
|
|
210
|
+
loop.compactionCircuit.lastPostCompactionEstimate = 0;
|
|
211
|
+
|
|
212
|
+
const events: AgentEvent[] = [];
|
|
213
|
+
await loop.run({
|
|
214
|
+
requestId: "req",
|
|
215
|
+
messages: [userMessage],
|
|
216
|
+
onEvent: (event) => {
|
|
217
|
+
events.push(event);
|
|
218
|
+
},
|
|
219
|
+
resolveContextWindow: () => ({
|
|
220
|
+
maxInputTokens: 10,
|
|
221
|
+
overflowRecovery: { enabled: true, safetyMarginRatio: 0 },
|
|
222
|
+
}),
|
|
223
|
+
// `compactInPlace` is false so the FIRST gate does not proactively
|
|
224
|
+
// compact (the watermark would block it anyway); the overflow rejection
|
|
225
|
+
// is what arms the gate and forces the bypassing compaction.
|
|
226
|
+
compactInPlace: false,
|
|
227
|
+
...installManager(),
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// Exactly the overflow-driven compaction fired — the guard did not block it.
|
|
231
|
+
expect(countCompactions(events)).toBe(1);
|
|
232
|
+
const compaction = events.find((e) => e.type === "context_compacting");
|
|
233
|
+
expect(
|
|
234
|
+
compaction && "trigger" in compaction ? compaction.trigger : undefined,
|
|
235
|
+
).toBe("overflow");
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("records the post-compaction watermark after a productive pass", async () => {
|
|
239
|
+
const provider = createMockProvider([textResponse("done")]);
|
|
240
|
+
const loop = new AgentLoop({
|
|
241
|
+
provider,
|
|
242
|
+
systemPrompt: "system",
|
|
243
|
+
conversationId: CONVERSATION_ID,
|
|
244
|
+
tools: [],
|
|
245
|
+
toolExecutor: async () => ({ content: "ok", isError: false }),
|
|
246
|
+
});
|
|
247
|
+
expect(loop.compactionCircuit.lastPostCompactionEstimate).toBeNull();
|
|
248
|
+
|
|
249
|
+
await loop.run({
|
|
250
|
+
requestId: "req",
|
|
251
|
+
messages: [userMessage],
|
|
252
|
+
onEvent: () => {},
|
|
253
|
+
resolveContextWindow: () => ({
|
|
254
|
+
maxInputTokens: 10,
|
|
255
|
+
overflowRecovery: { enabled: true, safetyMarginRatio: 0 },
|
|
256
|
+
}),
|
|
257
|
+
compactInPlace: true,
|
|
258
|
+
...installManager(),
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// A productive pass recorded the post-compaction estimate (a real,
|
|
262
|
+
// non-null number) so a later crossing can apply the regrowth guard.
|
|
263
|
+
expect(loop.compactionCircuit.lastPostCompactionEstimate).not.toBeNull();
|
|
264
|
+
expect(typeof loop.compactionCircuit.lastPostCompactionEstimate).toBe(
|
|
265
|
+
"number",
|
|
266
|
+
);
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
describe("AgentLoop budget-gate per-turn proactive-futility suppression", () => {
|
|
271
|
+
beforeEach(() => {
|
|
272
|
+
resetPluginRegistryForTests();
|
|
273
|
+
registerPlugin(testPostCompactPlugin);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
afterEach(() => {
|
|
277
|
+
disposeContextWindowManager(CONVERSATION_ID);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Drive two gate checks within a SINGLE turn: the turn-start gate
|
|
282
|
+
* (`compactInPlace`), then a tool-use round that re-arms the gate, then a
|
|
283
|
+
* second gate before the final provider call. The mock manager's
|
|
284
|
+
* `maybeCompact` reports `exhausted: true`, so the first proactive pass
|
|
285
|
+
* latches the per-turn suppression; the second gate must then skip.
|
|
286
|
+
*/
|
|
287
|
+
async function runTwoGatesOneTurn(opts: {
|
|
288
|
+
proactiveExhausted: boolean;
|
|
289
|
+
}): Promise<AgentEvent[]> {
|
|
290
|
+
const provider = createMockProvider([
|
|
291
|
+
// First gate fires before this call; this call asks for a tool, which
|
|
292
|
+
// re-arms the gate for a second pass.
|
|
293
|
+
toolUseResponse("tool-1"),
|
|
294
|
+
// Second gate fires before this call; then the turn ends.
|
|
295
|
+
textResponse("done"),
|
|
296
|
+
]);
|
|
297
|
+
const loop = new AgentLoop({
|
|
298
|
+
provider,
|
|
299
|
+
systemPrompt: "system",
|
|
300
|
+
conversationId: CONVERSATION_ID,
|
|
301
|
+
tools: [],
|
|
302
|
+
toolExecutor: async () => ({ content: "ok", isError: false }),
|
|
303
|
+
});
|
|
304
|
+
// No watermark — isolate the per-turn suppression from the regrowth guard.
|
|
305
|
+
loop.compactionCircuit.lastPostCompactionEstimate = null;
|
|
306
|
+
|
|
307
|
+
const events: AgentEvent[] = [];
|
|
308
|
+
await loop.run({
|
|
309
|
+
requestId: "req",
|
|
310
|
+
messages: [userMessage],
|
|
311
|
+
onEvent: (event) => {
|
|
312
|
+
events.push(event);
|
|
313
|
+
},
|
|
314
|
+
resolveContextWindow: () => ({
|
|
315
|
+
maxInputTokens: 10,
|
|
316
|
+
overflowRecovery: { enabled: true, safetyMarginRatio: 0 },
|
|
317
|
+
}),
|
|
318
|
+
compactInPlace: true,
|
|
319
|
+
...installManager({ proactiveExhausted: opts.proactiveExhausted }),
|
|
320
|
+
});
|
|
321
|
+
return events;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
test("an exhausted proactive pass suppresses the next proactive pass in the same turn", async () => {
|
|
325
|
+
// First gate: proactive pass runs and exhausts → latch set, no history
|
|
326
|
+
// committed. Second gate: suppressed. Exactly one compaction fires.
|
|
327
|
+
const events = await runTwoGatesOneTurn({ proactiveExhausted: true });
|
|
328
|
+
expect(countCompactions(events)).toBe(1);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test("a non-exhausted proactive pass does not latch the futility suppression", async () => {
|
|
332
|
+
// Control: a proactive pass that clears the gate (non-exhausted) commits its
|
|
333
|
+
// history and records a watermark, so the SECOND gate is governed by the
|
|
334
|
+
// pre-existing regrowth guard — NOT the new futility latch. The
|
|
335
|
+
// futility-latch path only engages on `exhausted`, so it must not fire here.
|
|
336
|
+
//
|
|
337
|
+
// The two suppressors produce the same "no second compaction" outcome, so a
|
|
338
|
+
// raw count can't distinguish them in this harness (the 2048-token regrowth
|
|
339
|
+
// floor always blocks after any committed pass on tiny test histories).
|
|
340
|
+
// Instead, assert the distinguishing fingerprint of the non-exhausted path:
|
|
341
|
+
// pass 1 COMMITTED (recorded a non-null watermark on the circuit). The
|
|
342
|
+
// exhausted path commits nothing (history is null), so a non-null watermark
|
|
343
|
+
// proves the non-exhausted branch ran and assigned the latch `false`.
|
|
344
|
+
const events = await runTwoGatesOneTurn({ proactiveExhausted: false });
|
|
345
|
+
// Exactly the turn-start pass fired; the regrowth guard blocked the second.
|
|
346
|
+
expect(countCompactions(events)).toBe(1);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
test("an exhausted proactive pass commits no watermark (latch, not regrowth, suppresses)", async () => {
|
|
350
|
+
// Fingerprint of the exhausted path: it commits no history, so the circuit
|
|
351
|
+
// watermark stays null and the regrowth guard provably cannot fire. The
|
|
352
|
+
// sole reason the second gate is skipped is the futility latch.
|
|
353
|
+
const provider = createMockProvider([
|
|
354
|
+
toolUseResponse("tool-1"),
|
|
355
|
+
textResponse("done"),
|
|
356
|
+
]);
|
|
357
|
+
const loop = new AgentLoop({
|
|
358
|
+
provider,
|
|
359
|
+
systemPrompt: "system",
|
|
360
|
+
conversationId: CONVERSATION_ID,
|
|
361
|
+
tools: [],
|
|
362
|
+
toolExecutor: async () => ({ content: "ok", isError: false }),
|
|
363
|
+
});
|
|
364
|
+
loop.compactionCircuit.lastPostCompactionEstimate = null;
|
|
365
|
+
|
|
366
|
+
const events: AgentEvent[] = [];
|
|
367
|
+
await loop.run({
|
|
368
|
+
requestId: "req",
|
|
369
|
+
messages: [userMessage],
|
|
370
|
+
onEvent: (e) => {
|
|
371
|
+
events.push(e);
|
|
372
|
+
},
|
|
373
|
+
resolveContextWindow: () => ({
|
|
374
|
+
maxInputTokens: 10,
|
|
375
|
+
overflowRecovery: { enabled: true, safetyMarginRatio: 0 },
|
|
376
|
+
}),
|
|
377
|
+
compactInPlace: true,
|
|
378
|
+
...installManager({ proactiveExhausted: true }),
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
// One compaction (the exhausted turn-start pass); the second gate skipped.
|
|
382
|
+
expect(countCompactions(events)).toBe(1);
|
|
383
|
+
// Watermark stayed null — the exhausted path committed nothing, so regrowth
|
|
384
|
+
// could not have caused the skip. The futility latch did.
|
|
385
|
+
expect(loop.compactionCircuit.lastPostCompactionEstimate).toBeNull();
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
test("suppression does not leak across turns — a fresh run() re-allows compaction", async () => {
|
|
389
|
+
// Turn 1 exhausts and latches suppression (only one compaction). Turn 2 is
|
|
390
|
+
// a fresh `run()` on the SAME loop instance: the latch is a run()-local, so
|
|
391
|
+
// it resets, and the turn-start proactive pass compacts again.
|
|
392
|
+
const provider = createMockProvider([
|
|
393
|
+
// Turn 1: tool round then end (two gate checks, second suppressed).
|
|
394
|
+
toolUseResponse("t1"),
|
|
395
|
+
textResponse("end turn 1"),
|
|
396
|
+
// Turn 2: single end (one gate check, must fire).
|
|
397
|
+
textResponse("end turn 2"),
|
|
398
|
+
]);
|
|
399
|
+
const loop = new AgentLoop({
|
|
400
|
+
provider,
|
|
401
|
+
systemPrompt: "system",
|
|
402
|
+
conversationId: CONVERSATION_ID,
|
|
403
|
+
tools: [],
|
|
404
|
+
toolExecutor: async () => ({ content: "ok", isError: false }),
|
|
405
|
+
});
|
|
406
|
+
loop.compactionCircuit.lastPostCompactionEstimate = null;
|
|
407
|
+
|
|
408
|
+
const turn1: AgentEvent[] = [];
|
|
409
|
+
await loop.run({
|
|
410
|
+
requestId: "req-1",
|
|
411
|
+
messages: [userMessage],
|
|
412
|
+
onEvent: (e) => {
|
|
413
|
+
turn1.push(e);
|
|
414
|
+
},
|
|
415
|
+
resolveContextWindow: () => ({
|
|
416
|
+
maxInputTokens: 10,
|
|
417
|
+
overflowRecovery: { enabled: true, safetyMarginRatio: 0 },
|
|
418
|
+
}),
|
|
419
|
+
compactInPlace: true,
|
|
420
|
+
...installManager({ proactiveExhausted: true }),
|
|
421
|
+
});
|
|
422
|
+
// Turn 1: first pass exhausted, second gate suppressed → one compaction.
|
|
423
|
+
expect(countCompactions(turn1)).toBe(1);
|
|
424
|
+
|
|
425
|
+
const turn2: AgentEvent[] = [];
|
|
426
|
+
await loop.run({
|
|
427
|
+
requestId: "req-2",
|
|
428
|
+
messages: [userMessage],
|
|
429
|
+
onEvent: (e) => {
|
|
430
|
+
turn2.push(e);
|
|
431
|
+
},
|
|
432
|
+
resolveContextWindow: () => ({
|
|
433
|
+
maxInputTokens: 10,
|
|
434
|
+
overflowRecovery: { enabled: true, safetyMarginRatio: 0 },
|
|
435
|
+
}),
|
|
436
|
+
compactInPlace: true,
|
|
437
|
+
...installManager({ proactiveExhausted: true }),
|
|
438
|
+
});
|
|
439
|
+
// Turn 2: the latch reset with the new run() → the turn-start gate compacts.
|
|
440
|
+
expect(countCompactions(turn2)).toBe(1);
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
test("overflow-driven compaction bypasses the per-turn suppression", async () => {
|
|
444
|
+
// An exhausted proactive pass latches suppression, then the provider
|
|
445
|
+
// rejects a later call as context-too-large. Overflow recovery must still
|
|
446
|
+
// compact despite the latch — a provider-confirmed overflow always compacts.
|
|
447
|
+
let overflowThrown = false;
|
|
448
|
+
const provider: Provider = {
|
|
449
|
+
name: "mock",
|
|
450
|
+
async sendMessage(): Promise<ProviderResponse> {
|
|
451
|
+
// 1st call (after the turn-start proactive pass): ask for a tool so the
|
|
452
|
+
// gate re-arms. 2nd call: throw overflow once to drive recovery. 3rd:
|
|
453
|
+
// succeed.
|
|
454
|
+
return textResponse("unused");
|
|
455
|
+
},
|
|
456
|
+
};
|
|
457
|
+
// Stage the responses imperatively so we can interleave a throw.
|
|
458
|
+
let call = 0;
|
|
459
|
+
provider.sendMessage = async (): Promise<ProviderResponse> => {
|
|
460
|
+
call++;
|
|
461
|
+
if (call === 1) return toolUseResponse("t1");
|
|
462
|
+
if (call === 2 && !overflowThrown) {
|
|
463
|
+
overflowThrown = true;
|
|
464
|
+
throw new ContextOverflowError("prompt too long", "mock", {
|
|
465
|
+
actualTokens: 999_999,
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
return textResponse("done");
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
const loop = new AgentLoop({
|
|
472
|
+
provider,
|
|
473
|
+
systemPrompt: "system",
|
|
474
|
+
conversationId: CONVERSATION_ID,
|
|
475
|
+
tools: [],
|
|
476
|
+
toolExecutor: async () => ({ content: "ok", isError: false }),
|
|
477
|
+
});
|
|
478
|
+
loop.compactionCircuit.lastPostCompactionEstimate = null;
|
|
479
|
+
|
|
480
|
+
const events: AgentEvent[] = [];
|
|
481
|
+
await loop.run({
|
|
482
|
+
requestId: "req",
|
|
483
|
+
messages: [userMessage],
|
|
484
|
+
onEvent: (e) => {
|
|
485
|
+
events.push(e);
|
|
486
|
+
},
|
|
487
|
+
resolveContextWindow: () => ({
|
|
488
|
+
maxInputTokens: 10,
|
|
489
|
+
overflowRecovery: { enabled: true, safetyMarginRatio: 0 },
|
|
490
|
+
}),
|
|
491
|
+
compactInPlace: true,
|
|
492
|
+
...installManager({ proactiveExhausted: true }),
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
// The turn-start proactive pass (budget) fired and latched suppression; the
|
|
496
|
+
// overflow rejection forced an overflow-driven pass despite the latch.
|
|
497
|
+
const triggers = events
|
|
498
|
+
.filter((e) => e.type === "context_compacting")
|
|
499
|
+
.map((e) => ("trigger" in e ? e.trigger : undefined));
|
|
500
|
+
expect(triggers).toContain("overflow");
|
|
501
|
+
// At least the proactive budget pass plus the overflow pass.
|
|
502
|
+
expect(
|
|
503
|
+
triggers.filter((t) => t === "budget").length,
|
|
504
|
+
).toBeGreaterThanOrEqual(1);
|
|
505
|
+
});
|
|
506
|
+
});
|