@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
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import type { Message, ToolDefinition } from "../../types.js";
|
|
4
|
+
import {
|
|
5
|
+
mapNeutralToolChoice,
|
|
6
|
+
OpenAIChatCompletionsProvider,
|
|
7
|
+
} from "../chat-completions-provider.js";
|
|
8
|
+
import { mapNeutralToolChoiceForResponses } from "../responses-provider.js";
|
|
9
|
+
|
|
10
|
+
const TOOLS: ToolDefinition[] = [
|
|
11
|
+
{
|
|
12
|
+
name: "bash",
|
|
13
|
+
description: "Run a shell command",
|
|
14
|
+
input_schema: { type: "object", properties: {} },
|
|
15
|
+
},
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const USER_MESSAGE: Message[] = [
|
|
19
|
+
{ role: "user", content: [{ type: "text", text: "hi" }] },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Stub the SDK client so we can capture the outgoing chat.completions.create
|
|
24
|
+
* params without hitting the network.
|
|
25
|
+
*/
|
|
26
|
+
function stubChatProvider(): {
|
|
27
|
+
provider: OpenAIChatCompletionsProvider;
|
|
28
|
+
requests: Array<Record<string, unknown>>;
|
|
29
|
+
} {
|
|
30
|
+
const provider = new OpenAIChatCompletionsProvider("test-key", "test-model");
|
|
31
|
+
const requests: Array<Record<string, unknown>> = [];
|
|
32
|
+
(provider as unknown as { client: unknown }).client = {
|
|
33
|
+
chat: {
|
|
34
|
+
completions: {
|
|
35
|
+
create: async (params: Record<string, unknown>) => {
|
|
36
|
+
requests.push(params);
|
|
37
|
+
return {
|
|
38
|
+
async *[Symbol.asyncIterator]() {
|
|
39
|
+
yield {
|
|
40
|
+
choices: [{ delta: {}, finish_reason: "stop" }],
|
|
41
|
+
usage: { prompt_tokens: 1, completion_tokens: 1 },
|
|
42
|
+
};
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
return { provider, requests };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe("mapNeutralToolChoice (chat-completions wire format)", () => {
|
|
53
|
+
// Each neutral tool_choice variant maps to its OpenAI-compatible form.
|
|
54
|
+
test("maps the Anthropic-shaped union to OpenAI's tool_choice values", () => {
|
|
55
|
+
// GIVEN the neutral (Anthropic-shaped) tool_choice union
|
|
56
|
+
// WHEN each variant is mapped to the chat-completions wire format
|
|
57
|
+
// THEN it produces the OpenAI-compatible value
|
|
58
|
+
expect(mapNeutralToolChoice({ type: "auto" })).toBe("auto");
|
|
59
|
+
expect(mapNeutralToolChoice({ type: "any" })).toBe("required");
|
|
60
|
+
expect(mapNeutralToolChoice({ type: "none" })).toBe("none");
|
|
61
|
+
expect(mapNeutralToolChoice({ type: "tool", name: "bash" })).toEqual({
|
|
62
|
+
type: "function",
|
|
63
|
+
function: { name: "bash" },
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Unmappable input yields undefined so the request omits tool_choice.
|
|
68
|
+
test("returns undefined for absent or unrecognized values", () => {
|
|
69
|
+
// GIVEN values that don't describe a valid tool_choice
|
|
70
|
+
// WHEN they are mapped
|
|
71
|
+
// THEN the mapper returns undefined (request falls back to the API default)
|
|
72
|
+
expect(mapNeutralToolChoice(undefined)).toBeUndefined();
|
|
73
|
+
expect(mapNeutralToolChoice(null)).toBeUndefined();
|
|
74
|
+
expect(mapNeutralToolChoice("none")).toBeUndefined();
|
|
75
|
+
expect(mapNeutralToolChoice({ type: "bogus" })).toBeUndefined();
|
|
76
|
+
// AND a forced tool with no name can't be expressed
|
|
77
|
+
expect(mapNeutralToolChoice({ type: "tool" })).toBeUndefined();
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe("mapNeutralToolChoiceForResponses (Responses API wire format)", () => {
|
|
82
|
+
// The Responses API named shape omits the chat-completions function wrapper.
|
|
83
|
+
test("uses the un-nested named-function shape", () => {
|
|
84
|
+
// GIVEN the neutral tool_choice union
|
|
85
|
+
// WHEN mapped to the Responses API format
|
|
86
|
+
// THEN string variants match and the named shape has no `function` wrapper
|
|
87
|
+
expect(mapNeutralToolChoiceForResponses({ type: "auto" })).toBe("auto");
|
|
88
|
+
expect(mapNeutralToolChoiceForResponses({ type: "any" })).toBe("required");
|
|
89
|
+
expect(mapNeutralToolChoiceForResponses({ type: "none" })).toBe("none");
|
|
90
|
+
expect(
|
|
91
|
+
mapNeutralToolChoiceForResponses({ type: "tool", name: "bash" }),
|
|
92
|
+
).toEqual({ type: "function", name: "bash" });
|
|
93
|
+
expect(mapNeutralToolChoiceForResponses({ type: "bogus" })).toBeUndefined();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("OpenAIChatCompletionsProvider tool_choice wiring", () => {
|
|
98
|
+
// A `{ type: "none" }` config forces a text-only answer on the wire.
|
|
99
|
+
test("forwards config.tool_choice on the wire when tools are present", async () => {
|
|
100
|
+
// GIVEN a provider with a stubbed SDK client
|
|
101
|
+
const { provider, requests } = stubChatProvider();
|
|
102
|
+
|
|
103
|
+
// WHEN sendMessage is called with tools and tool_choice `{ type: "none" }`
|
|
104
|
+
await provider.sendMessage(USER_MESSAGE, {
|
|
105
|
+
tools: TOOLS,
|
|
106
|
+
config: { tool_choice: { type: "none" } },
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// THEN the outgoing request carries `tool_choice: "none"`
|
|
110
|
+
expect(requests).toHaveLength(1);
|
|
111
|
+
expect(requests[0].tool_choice).toBe("none");
|
|
112
|
+
expect(requests[0].tools).toBeDefined();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// A `{ type: "tool", name }` config forces that specific tool call.
|
|
116
|
+
test("maps a forced tool choice to the named-function wire shape", async () => {
|
|
117
|
+
// GIVEN a provider with a stubbed SDK client
|
|
118
|
+
const { provider, requests } = stubChatProvider();
|
|
119
|
+
|
|
120
|
+
// WHEN sendMessage is called with a forced tool choice
|
|
121
|
+
await provider.sendMessage(USER_MESSAGE, {
|
|
122
|
+
tools: TOOLS,
|
|
123
|
+
config: { tool_choice: { type: "tool", name: "bash" } },
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// THEN the request carries the OpenAI named-function shape
|
|
127
|
+
expect(requests[0].tool_choice).toEqual({
|
|
128
|
+
type: "function",
|
|
129
|
+
function: { name: "bash" },
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// tool_choice is only meaningful alongside tools, so it is dropped.
|
|
134
|
+
test("omits tool_choice when no tools are supplied", async () => {
|
|
135
|
+
// GIVEN a provider with a stubbed SDK client
|
|
136
|
+
const { provider, requests } = stubChatProvider();
|
|
137
|
+
|
|
138
|
+
// WHEN sendMessage is called with a tool_choice but no tools
|
|
139
|
+
await provider.sendMessage(USER_MESSAGE, {
|
|
140
|
+
config: { tool_choice: { type: "none" } },
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// THEN the request omits both tools and tool_choice
|
|
144
|
+
expect(requests[0].tools).toBeUndefined();
|
|
145
|
+
expect(requests[0].tool_choice).toBeUndefined();
|
|
146
|
+
});
|
|
147
|
+
});
|
|
@@ -237,6 +237,43 @@ export function clampReasoningEffort(
|
|
|
237
237
|
: value;
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
+
/**
|
|
241
|
+
* Translate the neutral (Anthropic-shaped) `tool_choice` carried on the call
|
|
242
|
+
* config into the OpenAI chat-completions wire format. Callers express
|
|
243
|
+
* `tool_choice` once in the Anthropic union — `{ type: "auto" | "any" | "none"
|
|
244
|
+
* | "tool", name? }` — and each provider maps it to its own dialect (the
|
|
245
|
+
* Anthropic client forwards the union verbatim). For OpenAI-compatible APIs:
|
|
246
|
+
* - `{ type: "auto" }` -> `"auto"`
|
|
247
|
+
* - `{ type: "any" }` -> `"required"`
|
|
248
|
+
* - `{ type: "none" }` -> `"none"`
|
|
249
|
+
* - `{ type: "tool", name }` -> `{ type: "function", function: { name } }`
|
|
250
|
+
* Returns `undefined` for an absent or unrecognized value so the request omits
|
|
251
|
+
* `tool_choice` and falls back to the API default.
|
|
252
|
+
*
|
|
253
|
+
* See OpenAI's tool_choice spec:
|
|
254
|
+
* https://platform.openai.com/docs/api-reference/chat/create#chat-create-tool_choice
|
|
255
|
+
*/
|
|
256
|
+
export function mapNeutralToolChoice(
|
|
257
|
+
toolChoice: unknown,
|
|
258
|
+
): OpenAI.Chat.Completions.ChatCompletionToolChoiceOption | undefined {
|
|
259
|
+
if (toolChoice == null || typeof toolChoice !== "object") return undefined;
|
|
260
|
+
const tc = toolChoice as { type?: unknown; name?: unknown };
|
|
261
|
+
switch (tc.type) {
|
|
262
|
+
case "auto":
|
|
263
|
+
return "auto";
|
|
264
|
+
case "any":
|
|
265
|
+
return "required";
|
|
266
|
+
case "none":
|
|
267
|
+
return "none";
|
|
268
|
+
case "tool":
|
|
269
|
+
return typeof tc.name === "string"
|
|
270
|
+
? { type: "function", function: { name: tc.name } }
|
|
271
|
+
: undefined;
|
|
272
|
+
default:
|
|
273
|
+
return undefined;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
240
277
|
const OPENAI_SUPPORTED_IMAGE_TYPES = new Set([
|
|
241
278
|
"image/jpeg",
|
|
242
279
|
"image/png",
|
|
@@ -367,6 +404,15 @@ export class OpenAIChatCompletionsProvider implements Provider {
|
|
|
367
404
|
parameters: t.input_schema as OpenAI.FunctionParameters,
|
|
368
405
|
},
|
|
369
406
|
}));
|
|
407
|
+
|
|
408
|
+
// Honor a caller-supplied tool_choice (e.g. `{ type: "none" }` to force
|
|
409
|
+
// a text-only answer, or `{ type: "tool", name }` for a forced call).
|
|
410
|
+
// Only meaningful when tools are present — OpenAI rejects a named or
|
|
411
|
+
// "required" choice with no tools.
|
|
412
|
+
const toolChoice = mapNeutralToolChoice(configObj?.tool_choice);
|
|
413
|
+
if (toolChoice !== undefined) {
|
|
414
|
+
params.tool_choice = toolChoice;
|
|
415
|
+
}
|
|
370
416
|
}
|
|
371
417
|
|
|
372
418
|
const { signal: timeoutSignal, cleanup: cleanupTimeout } =
|
|
@@ -49,6 +49,41 @@ const EFFORT_TO_REASONING_EFFORT: Record<
|
|
|
49
49
|
/** Values accepted by the Responses API `text.verbosity` parameter. */
|
|
50
50
|
const VALID_VERBOSITIES = new Set<string>(["low", "medium", "high"]);
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Translate the neutral (Anthropic-shaped) `tool_choice` carried on the call
|
|
54
|
+
* config into the OpenAI Responses API wire format. Callers express
|
|
55
|
+
* `tool_choice` once in the Anthropic union — `{ type: "auto" | "any" | "none"
|
|
56
|
+
* | "tool", name? }`. For the Responses API:
|
|
57
|
+
* - `{ type: "auto" }` -> `"auto"`
|
|
58
|
+
* - `{ type: "any" }` -> `"required"`
|
|
59
|
+
* - `{ type: "none" }` -> `"none"`
|
|
60
|
+
* - `{ type: "tool", name }` -> `{ type: "function", name }`
|
|
61
|
+
* Note the named shape differs from chat-completions (no nested `function`
|
|
62
|
+
* wrapper). Returns `undefined` for an absent or unrecognized value.
|
|
63
|
+
*
|
|
64
|
+
* https://platform.openai.com/docs/api-reference/responses/create#responses-create-tool_choice
|
|
65
|
+
*/
|
|
66
|
+
export function mapNeutralToolChoiceForResponses(
|
|
67
|
+
toolChoice: unknown,
|
|
68
|
+
): string | { type: "function"; name: string } | undefined {
|
|
69
|
+
if (toolChoice == null || typeof toolChoice !== "object") return undefined;
|
|
70
|
+
const tc = toolChoice as { type?: unknown; name?: unknown };
|
|
71
|
+
switch (tc.type) {
|
|
72
|
+
case "auto":
|
|
73
|
+
return "auto";
|
|
74
|
+
case "any":
|
|
75
|
+
return "required";
|
|
76
|
+
case "none":
|
|
77
|
+
return "none";
|
|
78
|
+
case "tool":
|
|
79
|
+
return typeof tc.name === "string"
|
|
80
|
+
? { type: "function", name: tc.name }
|
|
81
|
+
: undefined;
|
|
82
|
+
default:
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
52
87
|
/** `text.verbosity` is a GPT-5-series-only parameter. Older models on the
|
|
53
88
|
* Responses API (o-series, etc.) reject unknown wire fields with HTTP 400, so
|
|
54
89
|
* gate forwarding by model name here. The retry layer can't make this call
|
|
@@ -229,6 +264,16 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
229
264
|
strict: null,
|
|
230
265
|
}));
|
|
231
266
|
}
|
|
267
|
+
|
|
268
|
+
// Honor a caller-supplied tool_choice (e.g. `{ type: "none" }` to force
|
|
269
|
+
// a text-only answer, or `{ type: "tool", name }` for a forced call).
|
|
270
|
+
// Only meaningful when tools are present.
|
|
271
|
+
const toolChoice = mapNeutralToolChoiceForResponses(
|
|
272
|
+
configObj?.tool_choice,
|
|
273
|
+
);
|
|
274
|
+
if (toolChoice !== undefined) {
|
|
275
|
+
params.tool_choice = toolChoice;
|
|
276
|
+
}
|
|
232
277
|
}
|
|
233
278
|
|
|
234
279
|
const { signal: timeoutSignal, cleanup: cleanupTimeout } =
|
|
@@ -28,7 +28,6 @@ const log = getLogger("provider-registry");
|
|
|
28
28
|
|
|
29
29
|
const providers = new Map<string, Provider>();
|
|
30
30
|
const routingSources = new Map<string, "user-key" | "managed-proxy">();
|
|
31
|
-
const OPENAI_COMPATIBLE_ENDPOINTS_FLAG = "openai-compatible-endpoints";
|
|
32
31
|
const NATIVE_WEB_SEARCH_PROVIDER_IDS = new Set(["anthropic", "openai"]);
|
|
33
32
|
|
|
34
33
|
/** Per-connection provider cache, keyed by connection name and model. */
|
|
@@ -258,13 +257,6 @@ export async function resolveProviderFromConnection(
|
|
|
258
257
|
config: ProvidersConfig,
|
|
259
258
|
opts: { model?: string } = {},
|
|
260
259
|
): Promise<Provider | null> {
|
|
261
|
-
if (
|
|
262
|
-
connection.provider === "openai-compatible" &&
|
|
263
|
-
!isProviderFeatureFlagEnabled(OPENAI_COMPATIBLE_ENDPOINTS_FLAG, config)
|
|
264
|
-
) {
|
|
265
|
-
return null;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
260
|
const model = opts.model ?? resolveModel(config, connection.provider);
|
|
269
261
|
const cacheKey = getConnectionProviderCacheKey(connection, model);
|
|
270
262
|
const cached = connectionProviders.get(cacheKey);
|
|
@@ -249,7 +249,6 @@ import {
|
|
|
249
249
|
const runResult = (history: Message[]): AgentLoopRunResult => ({
|
|
250
250
|
history,
|
|
251
251
|
exitReason: null,
|
|
252
|
-
appendedNewMessages: true,
|
|
253
252
|
// The wake path slices its own new-message boundary off the returned
|
|
254
253
|
// history (it never destructures `newMessages`), so this is type-only.
|
|
255
254
|
newMessages: [],
|
|
@@ -63,6 +63,10 @@ import {
|
|
|
63
63
|
persistWakeTailMessage,
|
|
64
64
|
scopeWakeAllowedTools,
|
|
65
65
|
} from "../daemon/wake-conversation-ops.js";
|
|
66
|
+
import {
|
|
67
|
+
recordCompactionEndBestEffort,
|
|
68
|
+
recordCompactionStartBestEffort,
|
|
69
|
+
} from "../memory/compaction-log-writer-clickhouse.js";
|
|
66
70
|
import { getConversationOverrideProfile } from "../memory/conversation-crud.js";
|
|
67
71
|
import {
|
|
68
72
|
buildProviderErrorResponsePayload,
|
|
@@ -598,6 +602,15 @@ export async function wakeAgentForOpportunity(
|
|
|
598
602
|
}
|
|
599
603
|
};
|
|
600
604
|
const onEvent = (event: AgentEvent): void => {
|
|
605
|
+
// Compaction logging is observability, not turn state, so it writes
|
|
606
|
+
// immediately even while buffering — a compaction during a silent
|
|
607
|
+
// (never-goes-live) wake still happened and should be recorded.
|
|
608
|
+
if (event.type === "context_compacting") {
|
|
609
|
+
recordCompactionStartBestEffort(conversationId, event);
|
|
610
|
+
}
|
|
611
|
+
if (event.type === "compaction_completed") {
|
|
612
|
+
recordCompactionEndBestEffort(conversationId, event);
|
|
613
|
+
}
|
|
601
614
|
// Replicates the recordRequestLog side-effect in `handleUsage` because
|
|
602
615
|
// wakes own their own onEvent and never reach `dispatchAgentEvent`.
|
|
603
616
|
// Defer persistence while buffering — see `pendingLogs` above.
|
|
@@ -75,6 +75,7 @@ const steerOrResumeMock = mock(
|
|
|
75
75
|
mock.module("../../../acp/index.js", () => ({
|
|
76
76
|
getAcpSessionManager: () => ({
|
|
77
77
|
getStatus: () => fakeInMemorySessions,
|
|
78
|
+
getActiveAndPendingIds: () => fakeInMemorySessions.map((s) => s.id),
|
|
78
79
|
spawn: spawnMock,
|
|
79
80
|
steerOrResume: steerOrResumeMock,
|
|
80
81
|
}),
|
|
@@ -86,6 +87,34 @@ mock.module("../../../acp/prepare-agent-env.js", () => ({
|
|
|
86
87
|
prepareAgentEnv: async (agentConfig: unknown) => agentConfig,
|
|
87
88
|
}));
|
|
88
89
|
|
|
90
|
+
// The spawn route and steer's resume branch gate on a high-risk approval
|
|
91
|
+
// (ATL-822) before starting the host agent. These tests pin the
|
|
92
|
+
// resolve/install/resume flow, so the hub mock auto-resolves the freshly
|
|
93
|
+
// registered confirmation the same way `POST /v1/confirm` would (resolve +
|
|
94
|
+
// directResolve). `approvalBehavior` flips allow/deny; `confirmationRequests`
|
|
95
|
+
// captures the prompts. Other event types are ignored.
|
|
96
|
+
import * as pendingInteractions from "../../../runtime/pending-interactions.js";
|
|
97
|
+
|
|
98
|
+
let approvalBehavior: "allow" | "deny" = "allow";
|
|
99
|
+
const confirmationRequests: Array<Record<string, unknown>> = [];
|
|
100
|
+
const broadcasts: Array<Record<string, unknown>> = [];
|
|
101
|
+
|
|
102
|
+
mock.module("../../../runtime/assistant-event-hub.js", () => ({
|
|
103
|
+
broadcastMessage: (msg: { type?: string; requestId?: string }) => {
|
|
104
|
+
broadcasts.push(msg as Record<string, unknown>);
|
|
105
|
+
if (msg?.type !== "confirmation_request") return;
|
|
106
|
+
confirmationRequests.push(msg as Record<string, unknown>);
|
|
107
|
+
const interaction = pendingInteractions.resolve(
|
|
108
|
+
msg.requestId as string,
|
|
109
|
+
approvalBehavior === "allow" ? "approved" : "rejected",
|
|
110
|
+
);
|
|
111
|
+
interaction?.directResolve?.(approvalBehavior);
|
|
112
|
+
},
|
|
113
|
+
}));
|
|
114
|
+
|
|
115
|
+
/** Drain pending micro/macrotasks so background resume work settles. */
|
|
116
|
+
const flushAsync = () => new Promise((resolve) => setTimeout(resolve, 0));
|
|
117
|
+
|
|
89
118
|
const config = await installAcpConfigStub();
|
|
90
119
|
const which = installWhichStub();
|
|
91
120
|
|
|
@@ -143,6 +172,9 @@ beforeEach(() => {
|
|
|
143
172
|
_resetAdapterInstallCacheForTests();
|
|
144
173
|
config.setConfig({});
|
|
145
174
|
which.setWhich((cmd) => `/usr/local/bin/${cmd}`);
|
|
175
|
+
approvalBehavior = "allow";
|
|
176
|
+
confirmationRequests.length = 0;
|
|
177
|
+
broadcasts.length = 0;
|
|
146
178
|
});
|
|
147
179
|
|
|
148
180
|
describe("GET /v1/acp/sessions — merged in-memory + history", () => {
|
|
@@ -652,3 +684,122 @@ describe("POST /v1/acp/:id/steer: resume fallback", () => {
|
|
|
652
684
|
await expect(promise).rejects.toBeInstanceOf(NotFoundError);
|
|
653
685
|
});
|
|
654
686
|
});
|
|
687
|
+
|
|
688
|
+
// ---------------------------------------------------------------------------
|
|
689
|
+
// POST /v1/acp/:id/steer: resume crosses the host-spawn boundary, so it is
|
|
690
|
+
// gated by the same high-risk guardian approval as spawn (ATL-822). Steering
|
|
691
|
+
// a session still active in memory only redirects an already-approved live
|
|
692
|
+
// process and is NOT prompted.
|
|
693
|
+
// ---------------------------------------------------------------------------
|
|
694
|
+
|
|
695
|
+
describe("POST /v1/acp/:id/steer: resume approval gate", () => {
|
|
696
|
+
test("acks immediately with approvalPending, then resumes once granted", async () => {
|
|
697
|
+
insertHistoryRow({
|
|
698
|
+
id: "gone-1",
|
|
699
|
+
agentId: "claude",
|
|
700
|
+
parentConversationId: "conv-z",
|
|
701
|
+
status: "completed",
|
|
702
|
+
cwd: "/work/repo",
|
|
703
|
+
});
|
|
704
|
+
steerOrResumeImpl = async () => ({ resumed: true });
|
|
705
|
+
|
|
706
|
+
const handler = getSteerHandler();
|
|
707
|
+
// The ack returns before the (async) resume completes, so a slow guardian
|
|
708
|
+
// approval can't trip the client's short ack timeout (ATL-822 / Codex P2).
|
|
709
|
+
const body = await handler({
|
|
710
|
+
pathParams: { id: "gone-1" },
|
|
711
|
+
body: { instruction: "keep going" },
|
|
712
|
+
});
|
|
713
|
+
expect(body).toEqual({
|
|
714
|
+
acpSessionId: "gone-1",
|
|
715
|
+
steered: false,
|
|
716
|
+
approvalPending: true,
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
// The high-risk prompt is surfaced for the resume.
|
|
720
|
+
expect(confirmationRequests).toHaveLength(1);
|
|
721
|
+
const prompt = confirmationRequests[0];
|
|
722
|
+
expect(prompt.toolName).toBe("acp_steer");
|
|
723
|
+
expect(prompt.riskLevel).toBe("high");
|
|
724
|
+
expect(prompt.executionTarget).toBe("host");
|
|
725
|
+
expect(prompt.conversationId).toBe("conv-z");
|
|
726
|
+
expect((prompt.input as { cwd?: string }).cwd).toBe("/work/repo");
|
|
727
|
+
|
|
728
|
+
// After approval settles, the background worker performs the resume.
|
|
729
|
+
await flushAsync();
|
|
730
|
+
expect(steerOrResumeMock).toHaveBeenCalledTimes(1);
|
|
731
|
+
expect(steerOrResumeMock.mock.calls[0][0]).toBe("gone-1");
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
test("denied resume never reaches the session manager and reports an error event", async () => {
|
|
735
|
+
approvalBehavior = "deny";
|
|
736
|
+
insertHistoryRow({
|
|
737
|
+
id: "gone-2",
|
|
738
|
+
acpSessionId: "proto-gone-2",
|
|
739
|
+
parentConversationId: "conv-z",
|
|
740
|
+
status: "completed",
|
|
741
|
+
cwd: "/work/repo",
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
const handler = getSteerHandler();
|
|
745
|
+
const body = await handler({
|
|
746
|
+
pathParams: { id: "gone-2" },
|
|
747
|
+
body: { instruction: "do evil" },
|
|
748
|
+
});
|
|
749
|
+
expect(body).toEqual({
|
|
750
|
+
acpSessionId: "gone-2",
|
|
751
|
+
steered: false,
|
|
752
|
+
approvalPending: true,
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
await flushAsync();
|
|
756
|
+
// Denied before any host re-spawn, and the denial surfaces over SSE.
|
|
757
|
+
expect(steerOrResumeMock).not.toHaveBeenCalled();
|
|
758
|
+
expect(pendingInteractions.getAll()).toHaveLength(0);
|
|
759
|
+
// Keyed by the daemon/route id (what SSE consumers index by), not the
|
|
760
|
+
// persisted protocol id.
|
|
761
|
+
const errEvent = broadcasts.find((m) => m.type === "acp_session_error");
|
|
762
|
+
expect(errEvent?.acpSessionId).toBe("gone-2");
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
test("a legacy row without a persisted cwd is not resumable, so no prompt", async () => {
|
|
766
|
+
insertHistoryRow({ id: "legacy-2", status: "completed", cwd: null });
|
|
767
|
+
steerOrResumeImpl = async (id) => {
|
|
768
|
+
throw new AcpResumeError(
|
|
769
|
+
new Error(`ACP session "${id}" cannot be resumed.`),
|
|
770
|
+
);
|
|
771
|
+
};
|
|
772
|
+
|
|
773
|
+
const handler = getSteerHandler();
|
|
774
|
+
await expect(
|
|
775
|
+
handler({ pathParams: { id: "legacy-2" }, body: { instruction: "go" } }),
|
|
776
|
+
).rejects.toBeInstanceOf(FailedDependencyError);
|
|
777
|
+
// No spawn would occur, so the gate stays out of the way.
|
|
778
|
+
expect(confirmationRequests).toHaveLength(0);
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
test("steering a session active in memory is not prompted", async () => {
|
|
782
|
+
fakeInMemorySessions = [
|
|
783
|
+
{
|
|
784
|
+
id: "live-2",
|
|
785
|
+
agentId: "claude",
|
|
786
|
+
acpSessionId: "proto-live-2",
|
|
787
|
+
parentConversationId: "conv-1",
|
|
788
|
+
status: "running",
|
|
789
|
+
startedAt: 1000,
|
|
790
|
+
},
|
|
791
|
+
];
|
|
792
|
+
// A resumable row also exists, but the in-memory session wins → steer.
|
|
793
|
+
insertHistoryRow({ id: "live-2", status: "completed", cwd: "/work/repo" });
|
|
794
|
+
|
|
795
|
+
const handler = getSteerHandler();
|
|
796
|
+
const body = await handler({
|
|
797
|
+
pathParams: { id: "live-2" },
|
|
798
|
+
body: { instruction: "redirect" },
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
expect(confirmationRequests).toHaveLength(0);
|
|
802
|
+
expect(body).toEqual({ acpSessionId: "live-2", steered: true });
|
|
803
|
+
expect(steerOrResumeMock).toHaveBeenCalledTimes(1);
|
|
804
|
+
});
|
|
805
|
+
});
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* once assistant output exists.
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
-
import { mkdtempSync,
|
|
27
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
28
28
|
import { tmpdir } from "node:os";
|
|
29
29
|
import { join } from "node:path";
|
|
30
30
|
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
@@ -40,7 +40,6 @@ import { invalidateConfigCache } from "../../../config/loader.js";
|
|
|
40
40
|
import { createConversation } from "../../../memory/conversation-crud.js";
|
|
41
41
|
import { getDb } from "../../../memory/db-connection.js";
|
|
42
42
|
import { initializeDb } from "../../../memory/db-init.js";
|
|
43
|
-
import { enqueueMemoryJob } from "../../../memory/jobs-store.js";
|
|
44
43
|
import { recordUsageEvent } from "../../../memory/llm-usage-store.js";
|
|
45
44
|
import { rawAll, rawRun } from "../../../memory/raw-query.js";
|
|
46
45
|
import { ROUTES } from "../consolidation-routes.js";
|
|
@@ -112,10 +111,6 @@ function recordUsageCostAt(
|
|
|
112
111
|
);
|
|
113
112
|
}
|
|
114
113
|
|
|
115
|
-
function readConfig(): Record<string, unknown> {
|
|
116
|
-
return JSON.parse(readFileSync(configPath, "utf-8"));
|
|
117
|
-
}
|
|
118
|
-
|
|
119
114
|
function readMemoryJobRows(): Array<{
|
|
120
115
|
id: string;
|
|
121
116
|
status: string;
|
|
@@ -379,7 +374,7 @@ describe("listConsolidationRuns handler", () => {
|
|
|
379
374
|
});
|
|
380
375
|
});
|
|
381
376
|
|
|
382
|
-
describe("
|
|
377
|
+
describe("consolidation config and run-now handlers", () => {
|
|
383
378
|
beforeEach(() => {
|
|
384
379
|
workspaceDir = mkdtempSync(join(tmpdir(), "vellum-consolidation-routes-"));
|
|
385
380
|
origWorkspaceDir = process.env.VELLUM_WORKSPACE_DIR;
|
|
@@ -399,12 +394,13 @@ describe("updateConsolidationConfig handler", () => {
|
|
|
399
394
|
rmSync(workspaceDir, { recursive: true, force: true });
|
|
400
395
|
});
|
|
401
396
|
|
|
402
|
-
test("
|
|
397
|
+
test("reports consolidation unavailable when global memory is disabled", async () => {
|
|
403
398
|
writeFileSync(
|
|
404
399
|
configPath,
|
|
405
400
|
JSON.stringify(
|
|
406
401
|
{
|
|
407
402
|
memory: {
|
|
403
|
+
enabled: false,
|
|
408
404
|
v2: {
|
|
409
405
|
enabled: true,
|
|
410
406
|
consolidation_interval_hours: 4,
|
|
@@ -416,8 +412,8 @@ describe("updateConsolidationConfig handler", () => {
|
|
|
416
412
|
) + "\n",
|
|
417
413
|
);
|
|
418
414
|
|
|
419
|
-
const handler = findHandler("
|
|
420
|
-
const result = (await handler({
|
|
415
|
+
const handler = findHandler("getConsolidationConfig");
|
|
416
|
+
const result = (await handler({})) as {
|
|
421
417
|
available: boolean;
|
|
422
418
|
enabled: boolean;
|
|
423
419
|
intervalMs: number;
|
|
@@ -425,17 +421,8 @@ describe("updateConsolidationConfig handler", () => {
|
|
|
425
421
|
success: boolean;
|
|
426
422
|
};
|
|
427
423
|
|
|
428
|
-
expect(readConfig()).toEqual({
|
|
429
|
-
memory: {
|
|
430
|
-
v2: {
|
|
431
|
-
enabled: true,
|
|
432
|
-
consolidation_interval_hours: 4,
|
|
433
|
-
consolidation_enabled: false,
|
|
434
|
-
},
|
|
435
|
-
},
|
|
436
|
-
});
|
|
437
424
|
expect(result).toMatchObject({
|
|
438
|
-
available:
|
|
425
|
+
available: false,
|
|
439
426
|
enabled: false,
|
|
440
427
|
intervalMs: 4 * 60 * 60 * 1000,
|
|
441
428
|
nextRunAt: null,
|
|
@@ -443,15 +430,15 @@ describe("updateConsolidationConfig handler", () => {
|
|
|
443
430
|
});
|
|
444
431
|
});
|
|
445
432
|
|
|
446
|
-
test("
|
|
433
|
+
test("run-now is unavailable when global memory is disabled", async () => {
|
|
447
434
|
writeFileSync(
|
|
448
435
|
configPath,
|
|
449
436
|
JSON.stringify(
|
|
450
437
|
{
|
|
451
438
|
memory: {
|
|
439
|
+
enabled: false,
|
|
452
440
|
v2: {
|
|
453
441
|
enabled: true,
|
|
454
|
-
consolidation_enabled: true,
|
|
455
442
|
},
|
|
456
443
|
},
|
|
457
444
|
},
|
|
@@ -459,36 +446,12 @@ describe("updateConsolidationConfig handler", () => {
|
|
|
459
446
|
2,
|
|
460
447
|
) + "\n",
|
|
461
448
|
);
|
|
462
|
-
const automaticJobId = enqueueMemoryJob("memory_v2_consolidate", {
|
|
463
|
-
trigger: "automatic",
|
|
464
|
-
});
|
|
465
|
-
const legacyJobId = enqueueMemoryJob("memory_v2_consolidate", {});
|
|
466
|
-
const manualJobId = enqueueMemoryJob("memory_v2_consolidate", {
|
|
467
|
-
trigger: "manual",
|
|
468
|
-
});
|
|
469
449
|
|
|
470
|
-
const handler = findHandler("
|
|
471
|
-
await handler({
|
|
472
|
-
|
|
473
|
-
const rowsById = new Map(
|
|
474
|
-
readMemoryJobRows().map((row) => [row.id, row] as const),
|
|
475
|
-
);
|
|
476
|
-
expect(rowsById.get(automaticJobId)).toMatchObject({
|
|
477
|
-
status: "failed",
|
|
478
|
-
lastError: "automatic_consolidation_disabled",
|
|
479
|
-
});
|
|
480
|
-
expect(rowsById.get(legacyJobId)).toMatchObject({
|
|
481
|
-
status: "failed",
|
|
482
|
-
lastError: "automatic_consolidation_disabled",
|
|
483
|
-
});
|
|
484
|
-
expect(rowsById.get(manualJobId)).toMatchObject({
|
|
485
|
-
status: "pending",
|
|
486
|
-
lastError: null,
|
|
487
|
-
payload: JSON.stringify({ trigger: "manual" }),
|
|
488
|
-
});
|
|
450
|
+
const handler = findHandler("runConsolidationNow");
|
|
451
|
+
await expect(handler({})).rejects.toThrow("Consolidation is not available");
|
|
489
452
|
});
|
|
490
453
|
|
|
491
|
-
test("run-now remains available when
|
|
454
|
+
test("run-now remains available when memory v2 is enabled", async () => {
|
|
492
455
|
writeFileSync(
|
|
493
456
|
configPath,
|
|
494
457
|
JSON.stringify(
|
|
@@ -496,7 +459,6 @@ describe("updateConsolidationConfig handler", () => {
|
|
|
496
459
|
memory: {
|
|
497
460
|
v2: {
|
|
498
461
|
enabled: true,
|
|
499
|
-
consolidation_enabled: false,
|
|
500
462
|
},
|
|
501
463
|
},
|
|
502
464
|
},
|