@vellumai/assistant 0.11.4 → 0.11.5-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/AGENTS.md +2 -2
- package/ARCHITECTURE.md +29 -4
- package/README.md +1 -1
- package/docs/architecture/memory.md +17 -5
- package/docs/architecture/security.md +96 -152
- package/docs/guardian-request-flow.md +13 -2
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +166 -1
- package/node_modules/@vellumai/gateway-client/src/http-delivery.ts +7 -14
- package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +29 -31
- package/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
- package/openapi.yaml +193 -3
- package/package.json +1 -1
- package/src/__tests__/anthropic-provider.test.ts +10 -3
- package/src/__tests__/approval-interception-trust-gates.test.ts +40 -0
- package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
- package/src/__tests__/channel-approval-routes.test.ts +4 -0
- package/src/__tests__/channel-inbound-disk-pressure.test.ts +5 -6
- package/src/__tests__/channel-reply-delivery.test.ts +26 -13
- package/src/__tests__/checker.test.ts +20 -314
- package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +6 -2
- package/src/__tests__/client-os-metadata-persistence.test.ts +16 -0
- package/src/__tests__/compactor-retained-image-validation.test.ts +142 -0
- package/src/__tests__/container-cpu-sampler.test.ts +153 -0
- package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
- package/src/__tests__/conversation-attachments.test.ts +0 -1
- package/src/__tests__/conversation-error.test.ts +17 -0
- package/src/__tests__/conversation-lifecycle.test.ts +20 -26
- package/src/__tests__/conversation-pairing.test.ts +186 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +21 -2
- package/src/__tests__/credential-broker-server-use.test.ts +24 -5
- package/src/__tests__/credential-routes.test.ts +22 -3
- package/src/__tests__/credential-security-invariants.test.ts +2 -1
- package/src/__tests__/daemon-credential-client.test.ts +88 -0
- package/src/__tests__/default-plugin-names-guard.test.ts +12 -1
- package/src/__tests__/dm-backfill.test.ts +63 -0
- package/src/__tests__/dynamic-skill-background-guard.test.ts +0 -2
- package/src/__tests__/edit-propagation.test.ts +107 -4
- package/src/__tests__/events-client-registration.test.ts +28 -0
- package/src/__tests__/file-write-tool.test.ts +4 -2
- package/src/__tests__/guardian-prompt-notice-privacy.test.ts +133 -0
- package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +143 -97
- package/src/__tests__/helpers/gateway-classify-mock.ts +27 -6
- package/src/__tests__/host-proxy-interface.test.ts +11 -1
- package/src/__tests__/host-shell-tool.test.ts +23 -4
- package/src/__tests__/image-conversion.test.ts +143 -1
- package/src/__tests__/inline-skill-load-permissions.test.ts +47 -36
- package/src/__tests__/input-repairs.test.ts +207 -0
- package/src/__tests__/live-workspace-guard.test.ts +75 -0
- package/src/__tests__/mcp-abort-signal.test.ts +1 -1
- package/src/__tests__/mcp-client-auth.test.ts +1 -1
- package/src/__tests__/mcp-tool-annotations-risk.test.ts +1 -1
- package/src/__tests__/media-resolve-image-validation.test.ts +310 -0
- package/src/__tests__/mtime-cache.test.ts +2 -0
- package/src/__tests__/notification-vellum-adapter.test.ts +124 -1
- package/src/__tests__/openai-provider.test.ts +22 -0
- package/src/__tests__/personal-memory-auth-bypass.test.ts +22 -6
- package/src/__tests__/platform-bash-auto-approve.test.ts +0 -4
- package/src/__tests__/platform.test.ts +16 -1
- package/src/__tests__/plugin-api-resolve-credential.test.ts +22 -13
- package/src/__tests__/plugin-api-shim.test.ts +5 -0
- package/src/__tests__/plugin-api-store-credential.test.ts +268 -0
- package/src/__tests__/plugin-config-data-migration.test.ts +8 -1
- package/src/__tests__/plugin-disabled-state.test.ts +2 -0
- package/src/__tests__/plugin-effective-enabled-set.test.ts +55 -3
- package/src/__tests__/plugin-execution-context.test.ts +73 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -1
- package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +0 -1
- package/src/__tests__/reaction-persistence.test.ts +200 -7
- package/src/__tests__/require-fresh-approval.test.ts +0 -4
- package/src/__tests__/resource-pressure-guard.test.ts +428 -0
- package/src/__tests__/resource-pressure-routes.test.ts +113 -0
- package/src/__tests__/risk-classification-boundary-guard.test.ts +115 -0
- package/src/__tests__/run-conversation-turn-persistence.test.ts +27 -1
- package/src/__tests__/secret-routes-platform-proxy.test.ts +7 -0
- package/src/__tests__/skill-tool-factory.test.ts +161 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +132 -5
- package/src/__tests__/tool-executor.test.ts +63 -38
- package/src/__tests__/tool-policy.test.ts +97 -1
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +7 -4
- package/src/__tests__/user-plugin-loader.test.ts +2 -0
- package/src/__tests__/validate-input.test.ts +243 -7
- package/src/__tests__/verification-control-plane-policy.test.ts +0 -2
- package/src/acp/__tests__/acp-claude-oauth.test.ts +97 -29
- package/src/acp/__tests__/prepare-agent-env.test.ts +376 -153
- package/src/acp/acp-claude-oauth.ts +38 -21
- package/src/acp/prepare-agent-env.ts +146 -55
- package/src/agent/loop-tool-dedup.test.ts +161 -0
- package/src/agent/loop.ts +68 -22
- package/src/api/constants/app-tools.ts +34 -0
- package/src/api/events/resource-pressure-status-changed.ts +53 -0
- package/src/api/index.ts +19 -0
- package/src/api/responses/resource-pressure-status.ts +25 -0
- package/src/approvals/guardian-channel-delivery.ts +70 -6
- package/src/approvals/guardian-request-resolvers.ts +32 -66
- package/src/channels/__tests__/message-audience.test.ts +49 -0
- package/src/channels/__tests__/types.test.ts +17 -3
- package/src/channels/message-audience.ts +40 -0
- package/src/channels/types.ts +16 -12
- package/src/cli/__tests__/catalog-search-help.test.ts +50 -0
- package/src/cli/commands/__tests__/channel-verification-sessions.test.ts +31 -0
- package/src/cli/commands/__tests__/conversations-search.test.ts +289 -0
- package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -0
- package/src/cli/commands/__tests__/inference-providers.test.ts +68 -0
- package/src/cli/commands/__tests__/keys.test.ts +89 -8
- package/src/cli/commands/__tests__/plugins.test.ts +57 -1
- package/src/cli/commands/channel-verification-sessions.help.ts +11 -9
- package/src/cli/commands/channel-verification-sessions.ts +11 -21
- package/src/cli/commands/conversations.help.ts +34 -0
- package/src/cli/commands/conversations.ts +125 -0
- package/src/cli/commands/inference-providers.ts +9 -4
- package/src/cli/commands/inference.help.ts +9 -4
- package/src/cli/commands/keys.help.ts +13 -1
- package/src/cli/commands/keys.ts +26 -15
- package/src/cli/commands/memory/__tests__/memory-v2.test.ts +57 -7
- package/src/cli/commands/memory/index.help.ts +41 -27
- package/src/cli/commands/memory/index.ts +2 -0
- package/src/cli/commands/memory/memory-v2.ts +58 -54
- package/src/cli/commands/memory/memory-validate.ts +18 -0
- package/src/cli/commands/monitoring.ts +1 -0
- package/src/cli/commands/plugins.help.ts +28 -31
- package/src/cli/commands/plugins.ts +10 -0
- package/src/cli/commands/skills.help.ts +13 -16
- package/src/cli/commands/trust.ts +3 -14
- package/src/cli/lib/__tests__/install-from-github.test.ts +38 -0
- package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +34 -0
- package/src/cli/lib/__tests__/plugin-surfaces.test.ts +32 -1
- package/src/cli/lib/__tests__/toggle-plugin.test.ts +2 -1
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +64 -0
- package/src/cli/lib/bundled-marketplace.json +13 -0
- package/src/cli/lib/daemon-credential-client.ts +25 -3
- package/src/cli/lib/install-from-github.ts +35 -24
- package/src/cli/lib/merge-plugin-tree.ts +22 -4
- package/src/cli/lib/plugin-surfaces.ts +27 -0
- package/src/config/feature-flag-registry.json +35 -19
- package/src/config/webhook-routing.ts +8 -0
- package/src/context/compactor.ts +31 -2
- package/src/daemon/__tests__/provider-rejection-log-fields.test.ts +272 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +4 -1
- package/src/daemon/conversation-error.ts +13 -0
- package/src/daemon/conversation-messaging.ts +22 -2
- package/src/daemon/conversation-process.ts +11 -0
- package/src/daemon/conversation-store.ts +10 -0
- package/src/daemon/conversation-surfaces.ts +21 -8
- package/src/daemon/conversation.ts +12 -10
- package/src/daemon/lifecycle.ts +6 -2
- package/src/daemon/message-types/conversations.ts +5 -7
- package/src/daemon/provider-rejection-log-fields.ts +124 -0
- package/src/daemon/resource-pressure-guard-lifecycle.ts +66 -0
- package/src/daemon/resource-pressure-guard.ts +387 -0
- package/src/daemon/shutdown-handlers.ts +2 -0
- package/src/daemon/startup-error.ts +16 -0
- package/src/daemon/trust-context.ts +14 -10
- package/src/daemon/unsendable-image-notice.ts +76 -0
- package/src/hooks/registry.ts +65 -51
- package/src/ipc/__tests__/socket-path.test.ts +15 -7
- package/src/ipc/gateway-client.test.ts +1 -1
- package/src/ipc/gateway-client.ts +14 -21
- package/src/ipc/socket-cleanup.ts +2 -11
- package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +1 -1
- package/src/messaging/provider-message-metadata.ts +128 -0
- package/src/messaging/providers/__tests__/transport-dispatch.test.ts +116 -68
- package/src/messaging/providers/channel-transport.ts +90 -13
- package/src/messaging/providers/discord/send.ts +16 -0
- package/src/messaging/providers/discord/transport.ts +11 -1
- package/src/messaging/providers/index.ts +109 -16
- package/src/messaging/providers/slack/message-metadata.ts +45 -0
- package/src/messaging/providers/slack/render-transcript.ts +1 -4
- package/src/messaging/providers/slack/send.test.ts +6 -9
- package/src/messaging/providers/slack/send.ts +40 -28
- package/src/messaging/providers/slack/transport.ts +22 -26
- package/src/messaging/providers/telegram-bot/send.test.ts +2 -8
- package/src/messaging/providers/telegram-bot/transport.ts +3 -6
- package/src/messaging/read-provider-metadata.test.ts +157 -0
- package/src/messaging/read-provider-metadata.ts +51 -0
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +94 -0
- package/src/notifications/__tests__/edit-notification.test.ts +174 -2
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +536 -4
- package/src/notifications/adapters/macos.ts +55 -0
- package/src/notifications/adapters/slack.ts +10 -5
- package/src/notifications/assistant-reply-producer.ts +37 -4
- package/src/notifications/conversation-pairing.ts +111 -21
- package/src/notifications/edit-notification.ts +38 -8
- package/src/notifications/emit-signal.ts +12 -14
- package/src/notifications/home-feed-side-effect.ts +218 -15
- package/src/notifications/types.ts +5 -0
- package/src/permissions/AGENTS.md +16 -0
- package/src/permissions/checker.test.ts +75 -122
- package/src/permissions/checker.ts +44 -560
- package/src/permissions/confirmation-guardian-request.test.ts +28 -3
- package/src/permissions/confirmation-guardian-request.ts +5 -1
- package/src/persistence/conversation-crud.ts +23 -20
- package/src/persistence/conversation-queries.ts +14 -1
- package/src/persistence/conversation-types.ts +22 -0
- package/src/persistence/delivery-crud.ts +122 -15
- package/src/plugin-api/__tests__/import-graph-partial-mock.test.ts +67 -0
- package/src/plugin-api/conversation-turn.ts +27 -0
- package/src/plugin-api/credential-scope.test.ts +15 -0
- package/src/plugin-api/credential-scope.ts +14 -0
- package/src/plugin-api/index.ts +19 -1
- package/src/plugin-api/resolve-credential.ts +15 -10
- package/src/plugin-api/store-credential.ts +148 -0
- package/src/plugin-api/system-card.ts +36 -0
- package/src/plugin-api/vision-support.test.ts +82 -0
- package/src/plugin-api/vision-support.ts +14 -2
- package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +361 -110
- package/src/plugins/defaults/image-fallback/__tests__/vision-recovery.test.ts +4 -0
- package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +58 -0
- package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +64 -23
- package/src/plugins/defaults/image-recovery/detect.ts +24 -1
- package/src/plugins/defaults/main.ts +15 -0
- package/src/plugins/defaults/memory/AGENTS.md +11 -3
- package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +0 -1
- package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +9 -12
- package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
- package/src/plugins/defaults/memory/src/memory-v2-routes.ts +12 -10
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +114 -0
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +8 -0
- package/src/plugins/defaults/memory/substrate/__tests__/edge-index.test.ts +0 -30
- package/src/plugins/defaults/memory/substrate/__tests__/ingest.test.ts +73 -1
- package/src/plugins/defaults/memory/substrate/__tests__/page-index.test.ts +37 -0
- package/src/plugins/defaults/memory/substrate/__tests__/page-links.test.ts +208 -0
- package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +158 -1
- package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +1 -61
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +68 -6
- package/src/plugins/defaults/memory/substrate/edge-index.ts +0 -31
- package/src/plugins/defaults/memory/substrate/ingest.ts +59 -0
- package/src/plugins/defaults/memory/substrate/page-index.ts +35 -8
- package/src/plugins/defaults/memory/substrate/page-links.ts +133 -0
- package/src/plugins/defaults/memory/substrate/page-store.ts +10 -0
- package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +124 -34
- package/src/plugins/defaults/memory/substrate/static-context.ts +0 -29
- package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +3 -1
- package/src/plugins/defaults/memory/v3/card.ts +9 -9
- package/src/plugins/defaults/memory/v3/core-set.test.ts +7 -0
- package/src/plugins/defaults/memory/v3/core-set.ts +5 -1
- package/src/plugins/defaults/memory/v3/edge.ts +13 -57
- package/src/plugins/mtime-cache.ts +1 -1
- package/src/plugins/pipeline.ts +14 -7
- package/src/plugins/plugin-execution-context.ts +35 -11
- package/src/plugins/plugin-tree-walk.ts +63 -4
- package/src/providers/anthropic/__tests__/pause-turn-continuation.test.ts +170 -0
- package/src/providers/anthropic/client.ts +332 -241
- package/src/providers/connection-resolution.ts +2 -1
- package/src/providers/content-blocks.ts +22 -0
- package/src/providers/gemini/client.ts +5 -2
- package/src/providers/inference/__tests__/adapter-factory-litellm.test.ts +47 -1
- package/src/providers/inference/__tests__/adapter-factory-ollama.test.ts +83 -0
- package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +98 -1
- package/src/providers/inference/__tests__/base-url-route-validation.test.ts +38 -0
- package/src/providers/inference/__tests__/base-url-security.test.ts +10 -0
- package/src/providers/inference/__tests__/connections-ollama.test.ts +90 -0
- package/src/providers/inference/__tests__/missing-credential-guard.test.ts +117 -0
- package/src/providers/inference/adapter-factory.ts +33 -2
- package/src/providers/inference/auth.ts +13 -2
- package/src/providers/inference/credential-usage.ts +37 -0
- package/src/providers/inference/missing-credential-guard.ts +110 -0
- package/src/providers/inference/resolve-auth.ts +7 -7
- package/src/providers/media-resolve.ts +176 -15
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +576 -46
- package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +125 -2
- package/src/providers/openai/chat-completions-provider.ts +322 -42
- package/src/routes/route-host-protocol.ts +7 -0
- package/src/routes/worker.ts +31 -10
- package/src/runtime/AGENTS.md +35 -0
- package/src/runtime/__tests__/runtime-http-port-collision.test.ts +131 -0
- package/src/runtime/__tests__/web-presence.test.ts +234 -0
- package/src/runtime/assistant-event-hub.ts +38 -0
- package/src/runtime/channel-reply-delivery.ts +54 -31
- package/src/runtime/channel-retry-sweep.ts +6 -4
- package/src/runtime/effective-capabilities.test.ts +19 -0
- package/src/runtime/effective-capabilities.ts +25 -0
- package/src/runtime/guardian-reply-router.ts +6 -2
- package/src/runtime/http-errors.ts +1 -0
- package/src/runtime/http-server.ts +20 -5
- package/src/runtime/routes/__tests__/client-routes.test.ts +150 -0
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +133 -0
- package/src/runtime/routes/__tests__/credential-delete-in-use.test.ts +155 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +52 -1
- package/src/runtime/routes/__tests__/monitoring-routes.test.ts +1 -0
- package/src/runtime/routes/__tests__/pending-interactions-route.test.ts +175 -0
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +88 -35
- package/src/runtime/routes/__tests__/surface-action-routes.test.ts +55 -6
- package/src/runtime/routes/__tests__/system-card-turn-termination.test.ts +107 -0
- package/src/runtime/routes/__tests__/user-route-dispatcher-host.test.ts +52 -1
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +75 -4
- package/src/runtime/routes/approval-routes.ts +37 -4
- package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +16 -22
- package/src/runtime/routes/canned-message-complete.ts +68 -29
- package/src/runtime/routes/client-routes.ts +113 -0
- package/src/runtime/routes/conversation-list-routes.ts +22 -7
- package/src/runtime/routes/conversation-management-routes.ts +15 -9
- package/src/runtime/routes/conversation-routes.ts +36 -22
- package/src/runtime/routes/credential-in-use.ts +85 -0
- package/src/runtime/routes/credential-routes.ts +55 -85
- package/src/runtime/routes/guardian-approval-interception.ts +22 -22
- package/src/runtime/routes/guardian-approval-reply-helpers.ts +14 -12
- package/src/runtime/routes/identity-routes.ts +5 -121
- package/src/runtime/routes/inbound-message-handler.ts +71 -27
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -17
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +77 -50
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +61 -97
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +101 -77
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +17 -8
- package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +91 -7
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +72 -70
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/inference-provider-connection-routes.ts +9 -7
- package/src/runtime/routes/monitoring-routes.ts +9 -1
- package/src/runtime/routes/plugins-routes.ts +17 -5
- package/src/runtime/routes/resource-pressure-routes.ts +22 -0
- package/src/runtime/routes/secret-routes.ts +39 -8
- package/src/runtime/routes/settings-routes.ts +6 -6
- package/src/runtime/routes/surface-action-routes.ts +20 -19
- package/src/runtime/routes/user-route-dispatcher.ts +31 -2
- package/src/runtime/routes/user-route-resolution.ts +18 -1
- package/src/runtime/slack-reply-session.test.ts +23 -13
- package/src/runtime/slack-reply-session.ts +38 -55
- package/src/runtime/web-presence.ts +90 -0
- package/src/skills/validate-input.ts +177 -40
- package/src/subagent/__tests__/consult-context-gating.test.ts +6 -10
- package/src/subagent/consult-context.ts +7 -23
- package/src/tools/credentials/broker.ts +24 -79
- package/src/tools/credentials/ref-parse.ts +35 -0
- package/src/tools/credentials/resolve.ts +4 -10
- package/src/tools/credentials/store.ts +168 -0
- package/src/tools/credentials/tool-policy.ts +67 -0
- package/src/tools/executor.ts +53 -15
- package/src/tools/network/__tests__/web-search.test.ts +41 -1
- package/src/tools/network/url-safety.ts +9 -16
- package/src/tools/network/web-search.ts +21 -3
- package/src/tools/permission-checker.ts +43 -52
- package/src/tools/schema-transforms.ts +40 -5
- package/src/tools/shared/input-repairs.ts +160 -0
- package/src/tools/skills/skill-tool-factory.ts +18 -4
- package/src/tools/subagent/spawn.ts +0 -1
- package/src/tools/tool-approval-handler.ts +16 -10
- package/src/tools/tool-types.ts +26 -13
- package/src/tools/types.ts +6 -6
- package/src/util/__tests__/cgroup-memory.test.ts +3 -0
- package/src/util/cgroup-memory.ts +3 -0
- package/src/util/container-cpu-sampler.ts +250 -0
- package/src/util/image-conversion.ts +178 -14
- package/src/util/platform.ts +93 -10
- package/src/permissions/ipc-risk-types.ts +0 -143
- package/src/permissions/risk-types.ts +0 -76
|
@@ -15,10 +15,12 @@ import type {
|
|
|
15
15
|
UserPromptSubmitContext,
|
|
16
16
|
} from "@vellumai/plugin-api";
|
|
17
17
|
|
|
18
|
-
// The current-turn boundary
|
|
19
|
-
// reaches through `@vellumai/plugin-api`; wire
|
|
20
|
-
// tests exercise the shipped
|
|
18
|
+
// The current-turn boundary and the vision-rejection classifier are the real
|
|
19
|
+
// host helpers the plugin reaches through `@vellumai/plugin-api`; wire them
|
|
20
|
+
// into the mock so the tests exercise the shipped behavior rather than a
|
|
21
|
+
// stand-in.
|
|
21
22
|
import { lastToolResultUserMessageIndex } from "../../../../context/outbound-sanitize.js";
|
|
23
|
+
import { isVisionNotSupportedError } from "../../../../util/provider-error-patterns.js";
|
|
22
24
|
|
|
23
25
|
// ─── Mocks ──────────────────────────────────────────────────────────────────
|
|
24
26
|
|
|
@@ -48,18 +50,48 @@ const mockResolveMediaSourceData = (source: ImageContent["source"]) =>
|
|
|
48
50
|
? { data: source.data, media_type: source.media_type }
|
|
49
51
|
: null;
|
|
50
52
|
|
|
53
|
+
// System cards the plugin posts, captured instead of persisted.
|
|
54
|
+
let persistedCards: Array<{
|
|
55
|
+
conversationId: string;
|
|
56
|
+
text: string;
|
|
57
|
+
metadata: Record<string, unknown>;
|
|
58
|
+
}> = [];
|
|
59
|
+
let cardPersistenceFails = false;
|
|
60
|
+
|
|
51
61
|
// Mock @vellumai/plugin-api — only the runtime handles the plugin imports.
|
|
52
62
|
// `extractAllText` stays real (imported from the relative path, not plugin-api).
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
+
// One factory backs every install so a per-test override (e.g. a
|
|
64
|
+
// call-counting provider) cannot silently drop the rest of the surface. The
|
|
65
|
+
// module registry is process-wide, so the surface covers every plugin-api
|
|
66
|
+
// export the plugin's hooks import, including ones only sibling test files
|
|
67
|
+
// exercise.
|
|
68
|
+
function installPluginApiMock(overrides: Record<string, unknown> = {}): void {
|
|
69
|
+
mock.module("@vellumai/plugin-api", () => ({
|
|
70
|
+
doesSupportVision: (arg: ModelProfileInfo | string) =>
|
|
71
|
+
typeof arg === "string"
|
|
72
|
+
? visionModels.has(arg)
|
|
73
|
+
: visionProfiles.has(arg.key),
|
|
74
|
+
getModelProfiles: () => mockProfiles,
|
|
75
|
+
resolveMediaSourceData: mockResolveMediaSourceData,
|
|
76
|
+
getConfiguredProvider: async () => (providerResolves ? fakeProvider : null),
|
|
77
|
+
lastToolResultUserMessageIndex,
|
|
78
|
+
isVisionNotSupportedError,
|
|
79
|
+
persistSystemCard: async (opts: {
|
|
80
|
+
conversationId: string;
|
|
81
|
+
text: string;
|
|
82
|
+
metadata: Record<string, unknown>;
|
|
83
|
+
}) => {
|
|
84
|
+
if (cardPersistenceFails) {
|
|
85
|
+
throw new Error("card write failed");
|
|
86
|
+
}
|
|
87
|
+
persistedCards.push(opts);
|
|
88
|
+
return "card-1";
|
|
89
|
+
},
|
|
90
|
+
...overrides,
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
installPluginApiMock();
|
|
63
95
|
|
|
64
96
|
// Mock the image-persist module to avoid filesystem side effects in tests.
|
|
65
97
|
let mockPersistPath: string | null =
|
|
@@ -77,6 +109,7 @@ const postCompact = (await import("../hooks/post-compact.js")).default;
|
|
|
77
109
|
const conversationDeleted = (await import("../hooks/conversation-deleted.js"))
|
|
78
110
|
.default;
|
|
79
111
|
const { findVisionProfile } = await import("../src/vision-caption.js");
|
|
112
|
+
const { flattenTextOnlyBlocks } = await import("../src/caption-blocks.js");
|
|
80
113
|
const { closeCaptionStore, initCaptionStore, resetCaptionCacheForTests } =
|
|
81
114
|
await import("../src/caption-cache.js");
|
|
82
115
|
|
|
@@ -130,9 +163,16 @@ function textMsg(text: string): Message {
|
|
|
130
163
|
return { role: "user", content: [{ type: "text", text }] };
|
|
131
164
|
}
|
|
132
165
|
|
|
166
|
+
/**
|
|
167
|
+
* A user-prompt-submit context whose `originalMessages` tail is the submitted
|
|
168
|
+
* message, the way the agent loop snapshots the history before the hook chain
|
|
169
|
+
* runs. Pass `originalMessages` explicitly to model a history the turn did not
|
|
170
|
+
* submit anything into.
|
|
171
|
+
*/
|
|
133
172
|
function makeCtx(
|
|
134
173
|
overrides: Partial<UserPromptSubmitContext> = {},
|
|
135
174
|
): UserPromptSubmitContext {
|
|
175
|
+
const latestMessages = overrides.latestMessages ?? [];
|
|
136
176
|
return {
|
|
137
177
|
conversationId: "c1",
|
|
138
178
|
userMessageId: "m1",
|
|
@@ -140,8 +180,8 @@ function makeCtx(
|
|
|
140
180
|
modelProfileKey: "text-only",
|
|
141
181
|
isNonInteractive: false,
|
|
142
182
|
prompt: "What is in this image?",
|
|
143
|
-
|
|
144
|
-
|
|
183
|
+
latestMessages,
|
|
184
|
+
originalMessages: Object.freeze([...latestMessages]),
|
|
145
185
|
logger,
|
|
146
186
|
...overrides,
|
|
147
187
|
} as unknown as UserPromptSubmitContext;
|
|
@@ -201,6 +241,9 @@ beforeEach(() => {
|
|
|
201
241
|
};
|
|
202
242
|
providerResolves = true;
|
|
203
243
|
mockPersistPath = "/workspace/data/attachments/mock-hash.png";
|
|
244
|
+
persistedCards = [];
|
|
245
|
+
cardPersistenceFails = false;
|
|
246
|
+
installPluginApiMock();
|
|
204
247
|
resetCaptionCacheForTests();
|
|
205
248
|
});
|
|
206
249
|
|
|
@@ -276,7 +319,13 @@ describe("image-fallback user-prompt-submit hook", () => {
|
|
|
276
319
|
expect(text).not.toContain("/workspace/data/attachments/");
|
|
277
320
|
});
|
|
278
321
|
|
|
279
|
-
test("preserves
|
|
322
|
+
test("preserves the user's own text around the caption it substitutes", async () => {
|
|
323
|
+
/**
|
|
324
|
+
* Tests that surrounding prose survives the substitution, merged with the
|
|
325
|
+
* caption in submitted order.
|
|
326
|
+
*/
|
|
327
|
+
|
|
328
|
+
// GIVEN a submitted message that wraps an image in the user's own prose
|
|
280
329
|
const messages: Message[] = [
|
|
281
330
|
{
|
|
282
331
|
role: "user",
|
|
@@ -288,16 +337,16 @@ describe("image-fallback user-prompt-submit hook", () => {
|
|
|
288
337
|
},
|
|
289
338
|
];
|
|
290
339
|
const ctx = makeCtx({ latestMessages: messages });
|
|
340
|
+
|
|
341
|
+
// WHEN the turn starts
|
|
291
342
|
await userPromptSubmit(ctx);
|
|
343
|
+
|
|
344
|
+
// THEN the caption replaces the image between the two prose blocks, and
|
|
345
|
+
// the now text-only content is merged into the single block providers
|
|
346
|
+
// serialize as a plain string
|
|
347
|
+
expect(ctx.latestMessages[0].content).toHaveLength(1);
|
|
292
348
|
expect((ctx.latestMessages[0].content[0] as { text: string }).text).toBe(
|
|
293
|
-
"Look at this:",
|
|
294
|
-
);
|
|
295
|
-
expect(ctx.latestMessages[0].content[1].type).toBe("text");
|
|
296
|
-
expect(
|
|
297
|
-
(ctx.latestMessages[0].content[1] as { text: string }).text,
|
|
298
|
-
).toContain("[Image auto-described");
|
|
299
|
-
expect((ctx.latestMessages[0].content[2] as { text: string }).text).toBe(
|
|
300
|
-
"What do you see?",
|
|
349
|
+
"Look at this:\n\n[Image auto-described for text-only model: A red chart showing Q3 revenue.]\n\nWhat do you see?",
|
|
301
350
|
);
|
|
302
351
|
});
|
|
303
352
|
|
|
@@ -333,15 +382,9 @@ describe("image-fallback user-prompt-submit hook", () => {
|
|
|
333
382
|
},
|
|
334
383
|
};
|
|
335
384
|
// Override the mock to track calls.
|
|
336
|
-
|
|
337
|
-
doesSupportVision: (arg: ModelProfileInfo | string) =>
|
|
338
|
-
typeof arg === "string"
|
|
339
|
-
? visionModels.has(arg)
|
|
340
|
-
: visionProfiles.has(arg.key),
|
|
341
|
-
getModelProfiles: () => mockProfiles,
|
|
342
|
-
resolveMediaSourceData: mockResolveMediaSourceData,
|
|
385
|
+
installPluginApiMock({
|
|
343
386
|
getConfiguredProvider: async () => trackingProvider,
|
|
344
|
-
})
|
|
387
|
+
});
|
|
345
388
|
|
|
346
389
|
const messages1 = [imageMsg("same-data")];
|
|
347
390
|
const ctx1 = makeCtx({ latestMessages: messages1 });
|
|
@@ -353,18 +396,6 @@ describe("image-fallback user-prompt-submit hook", () => {
|
|
|
353
396
|
const ctx2 = makeCtx({ latestMessages: messages2 });
|
|
354
397
|
await userPromptSubmit(ctx2);
|
|
355
398
|
expect(callCount).toBe(1); // still 1 — cache hit
|
|
356
|
-
|
|
357
|
-
// Restore the original mock for other tests.
|
|
358
|
-
mock.module("@vellumai/plugin-api", () => ({
|
|
359
|
-
doesSupportVision: (arg: ModelProfileInfo | string) =>
|
|
360
|
-
typeof arg === "string"
|
|
361
|
-
? visionModels.has(arg)
|
|
362
|
-
: visionProfiles.has(arg.key),
|
|
363
|
-
getModelProfiles: () => mockProfiles,
|
|
364
|
-
resolveMediaSourceData: mockResolveMediaSourceData,
|
|
365
|
-
getConfiguredProvider: async () =>
|
|
366
|
-
providerResolves ? fakeProvider : null,
|
|
367
|
-
}));
|
|
368
399
|
});
|
|
369
400
|
|
|
370
401
|
test("captions images nested in a historical tool_result's contentBlocks", async () => {
|
|
@@ -399,13 +430,287 @@ describe("image-fallback user-prompt-submit hook", () => {
|
|
|
399
430
|
expect(
|
|
400
431
|
(ctx.latestMessages[0].content[0] as { text: string }).text,
|
|
401
432
|
).toContain("[Image auto-described");
|
|
402
|
-
expect(ctx.latestMessages[2].content
|
|
433
|
+
expect(ctx.latestMessages[2].content).toHaveLength(1);
|
|
403
434
|
expect(
|
|
404
435
|
(ctx.latestMessages[2].content[0] as { text: string }).text,
|
|
405
436
|
).toContain("[Image auto-described");
|
|
406
|
-
expect(
|
|
407
|
-
|
|
437
|
+
expect(
|
|
438
|
+
(ctx.latestMessages[2].content[0] as { text: string }).text,
|
|
439
|
+
).toEndWith("\n\nboth?");
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
describe("image-fallback dropped-image notice", () => {
|
|
444
|
+
test("posts a transcript card when this turn's image cannot be described", async () => {
|
|
445
|
+
/**
|
|
446
|
+
* Tests that an image the model never receives in any form is reported to
|
|
447
|
+
* the user instead of being dropped silently.
|
|
448
|
+
*/
|
|
449
|
+
|
|
450
|
+
// GIVEN a workspace with no vision-capable profile to caption with
|
|
451
|
+
visionProfiles = new Set<string>();
|
|
452
|
+
|
|
453
|
+
// AND a turn that submits prose plus an image
|
|
454
|
+
const submitted: Message = {
|
|
455
|
+
role: "user",
|
|
456
|
+
content: [{ type: "text", text: "look at this" }, imageBlock("dropped")],
|
|
457
|
+
};
|
|
458
|
+
const ctx = makeCtx({ latestMessages: [submitted] });
|
|
459
|
+
|
|
460
|
+
// WHEN the turn starts
|
|
461
|
+
await userPromptSubmit(ctx);
|
|
462
|
+
|
|
463
|
+
// THEN the model receives the prose and a placeholder, merged into the
|
|
464
|
+
// single text block providers serialize as a plain string
|
|
465
|
+
expect(ctx.latestMessages[0].content).toHaveLength(1);
|
|
466
|
+
expect((ctx.latestMessages[0].content[0] as { text: string }).text).toBe(
|
|
467
|
+
"look at this\n\n[Image: no vision-capable model configured to describe it]",
|
|
408
468
|
);
|
|
469
|
+
|
|
470
|
+
// AND the transcript carries one card telling the user the image was not sent
|
|
471
|
+
expect(persistedCards).toHaveLength(1);
|
|
472
|
+
expect(persistedCards[0].conversationId).toBe("c1");
|
|
473
|
+
expect(persistedCards[0].text).toBe(
|
|
474
|
+
"The image you attached was not sent to the model: no vision-capable model is configured to describe it. Configure one to use images in this conversation.",
|
|
475
|
+
);
|
|
476
|
+
expect(persistedCards[0].metadata).toEqual({
|
|
477
|
+
plugin: "image-fallback",
|
|
478
|
+
droppedImageCount: 1,
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
test("reports every image the turn attached in a single card", async () => {
|
|
483
|
+
/**
|
|
484
|
+
* Tests that one turn attaching several undescribable images produces one
|
|
485
|
+
* card that counts them.
|
|
486
|
+
*/
|
|
487
|
+
|
|
488
|
+
// GIVEN no vision-capable profile
|
|
489
|
+
visionProfiles = new Set<string>();
|
|
490
|
+
|
|
491
|
+
// AND a turn that submits two images
|
|
492
|
+
const submitted: Message = {
|
|
493
|
+
role: "user",
|
|
494
|
+
content: [imageBlock("drop-a"), imageBlock("drop-b")],
|
|
495
|
+
};
|
|
496
|
+
const ctx = makeCtx({ latestMessages: [submitted] });
|
|
497
|
+
|
|
498
|
+
// WHEN the turn starts
|
|
499
|
+
await userPromptSubmit(ctx);
|
|
500
|
+
|
|
501
|
+
// THEN a single card reports both
|
|
502
|
+
expect(persistedCards).toHaveLength(1);
|
|
503
|
+
expect(persistedCards[0].text).toContain("The 2 images you attached");
|
|
504
|
+
expect(persistedCards[0].metadata.droppedImageCount).toBe(2);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
test("posts a card when an injector rebuilt the submitted message", async () => {
|
|
508
|
+
/**
|
|
509
|
+
* Tests that the notice survives an earlier hook replacing the tail
|
|
510
|
+
* message object, which memory and runtime-context injection do when they
|
|
511
|
+
* append their blocks to the submitted message.
|
|
512
|
+
*/
|
|
513
|
+
|
|
514
|
+
// GIVEN no vision-capable profile
|
|
515
|
+
visionProfiles = new Set<string>();
|
|
516
|
+
|
|
517
|
+
// AND a submitted message carrying an image
|
|
518
|
+
const submitted: Message = {
|
|
519
|
+
role: "user",
|
|
520
|
+
content: [imageBlock("dropped")],
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
// AND a working history whose tail is a rebuilt copy of it, carrying an
|
|
524
|
+
// injected context block, rather than the submitted object itself
|
|
525
|
+
const injected: Message = {
|
|
526
|
+
role: "user",
|
|
527
|
+
content: [
|
|
528
|
+
...submitted.content,
|
|
529
|
+
{ type: "text", text: "<runtime_context />" },
|
|
530
|
+
],
|
|
531
|
+
};
|
|
532
|
+
const ctx = makeCtx({
|
|
533
|
+
latestMessages: [injected],
|
|
534
|
+
originalMessages: Object.freeze([submitted]),
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
// WHEN the turn starts
|
|
538
|
+
await userPromptSubmit(ctx);
|
|
539
|
+
|
|
540
|
+
// THEN the user is still told the image was not sent
|
|
541
|
+
expect(persistedCards).toHaveLength(1);
|
|
542
|
+
expect(persistedCards[0].metadata.droppedImageCount).toBe(1);
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
test("posts no card when a vision profile describes the image", async () => {
|
|
546
|
+
/**
|
|
547
|
+
* Tests that a captioned image is not reported as dropped: the model does
|
|
548
|
+
* receive its content.
|
|
549
|
+
*/
|
|
550
|
+
|
|
551
|
+
// GIVEN a vision-capable profile and a submitted image
|
|
552
|
+
const ctx = makeCtx({ latestMessages: [imageMsg("described")] });
|
|
553
|
+
|
|
554
|
+
// WHEN the turn starts
|
|
555
|
+
await userPromptSubmit(ctx);
|
|
556
|
+
|
|
557
|
+
// THEN the image is captioned and nothing is reported to the user
|
|
558
|
+
expect(
|
|
559
|
+
(ctx.latestMessages[0].content[0] as { text: string }).text,
|
|
560
|
+
).toContain("[Image auto-described");
|
|
561
|
+
expect(persistedCards).toHaveLength(0);
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
test("posts no card for raw images the sweep re-encounters from earlier turns", async () => {
|
|
565
|
+
/**
|
|
566
|
+
* Tests that the notice fires once. Persisted rows keep their raw images,
|
|
567
|
+
* so every later turn's sweep drops them again.
|
|
568
|
+
*/
|
|
569
|
+
|
|
570
|
+
// GIVEN no vision-capable profile
|
|
571
|
+
visionProfiles = new Set<string>();
|
|
572
|
+
|
|
573
|
+
// AND a history whose earlier turn holds a raw image, with this turn
|
|
574
|
+
// submitting text only
|
|
575
|
+
const history = [imageMsg("from-an-earlier-turn"), textMsg("follow up")];
|
|
576
|
+
const ctx = makeCtx({ latestMessages: history });
|
|
577
|
+
|
|
578
|
+
// WHEN the turn starts
|
|
579
|
+
await userPromptSubmit(ctx);
|
|
580
|
+
|
|
581
|
+
// THEN the older image is still replaced for the provider call
|
|
582
|
+
expect(
|
|
583
|
+
(ctx.latestMessages[0].content[0] as { text: string }).text,
|
|
584
|
+
).toContain("no vision-capable model");
|
|
585
|
+
|
|
586
|
+
// AND no card repeats a notice the user already saw
|
|
587
|
+
expect(persistedCards).toHaveLength(0);
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
test("posts no card for media a tool returned", async () => {
|
|
591
|
+
/**
|
|
592
|
+
* Tests that tool-returned media stays out of the user-facing tally: the
|
|
593
|
+
* user did not attach it.
|
|
594
|
+
*/
|
|
595
|
+
|
|
596
|
+
// GIVEN no vision-capable profile
|
|
597
|
+
visionProfiles = new Set<string>();
|
|
598
|
+
|
|
599
|
+
// AND a submitted message carrying a tool result with a screenshot
|
|
600
|
+
const ctx = makeCtx({
|
|
601
|
+
latestMessages: [
|
|
602
|
+
{ role: "user", content: [toolResult([imageBlock("screenshot")])] },
|
|
603
|
+
],
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
// WHEN the turn starts
|
|
607
|
+
await userPromptSubmit(ctx);
|
|
608
|
+
|
|
609
|
+
// THEN the nested image is replaced without notifying the user
|
|
610
|
+
const result = ctx.latestMessages[0].content[0] as ToolResultContent;
|
|
611
|
+
expect((result.contentBlocks![0] as { text: string }).text).toContain(
|
|
612
|
+
"no vision-capable model",
|
|
613
|
+
);
|
|
614
|
+
expect(persistedCards).toHaveLength(0);
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
test("still runs the turn when the notice cannot be persisted", async () => {
|
|
618
|
+
/**
|
|
619
|
+
* Tests that the notice is best-effort: losing it must not fail the turn.
|
|
620
|
+
*/
|
|
621
|
+
|
|
622
|
+
// GIVEN no vision-capable profile
|
|
623
|
+
visionProfiles = new Set<string>();
|
|
624
|
+
|
|
625
|
+
// AND a transcript that rejects the card write
|
|
626
|
+
cardPersistenceFails = true;
|
|
627
|
+
const ctx = makeCtx({ latestMessages: [imageMsg("dropped")] });
|
|
628
|
+
|
|
629
|
+
// WHEN the turn starts
|
|
630
|
+
await userPromptSubmit(ctx);
|
|
631
|
+
|
|
632
|
+
// THEN the substitution still reaches the provider-bound history
|
|
633
|
+
expect(
|
|
634
|
+
(ctx.latestMessages[0].content[0] as { text: string }).text,
|
|
635
|
+
).toContain("no vision-capable model");
|
|
636
|
+
});
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
describe("flattenTextOnlyBlocks", () => {
|
|
640
|
+
test("leaves a single text block untouched", () => {
|
|
641
|
+
/**
|
|
642
|
+
* Tests that content already in the shape providers serialize as a string
|
|
643
|
+
* is not rebuilt.
|
|
644
|
+
*/
|
|
645
|
+
|
|
646
|
+
// GIVEN a user message holding one text block
|
|
647
|
+
const block = { type: "text" as const, text: "just text" };
|
|
648
|
+
const messages: Message[] = [{ role: "user", content: [block] }];
|
|
649
|
+
|
|
650
|
+
// WHEN the content is flattened
|
|
651
|
+
const flattened = flattenTextOnlyBlocks(messages);
|
|
652
|
+
|
|
653
|
+
// THEN nothing changes
|
|
654
|
+
expect(flattened).toBe(0);
|
|
655
|
+
expect(messages[0].content).toEqual([block]);
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
test("leaves content holding a non-text block untouched", () => {
|
|
659
|
+
/**
|
|
660
|
+
* Tests that merging never discards a block the provider must serialize on
|
|
661
|
+
* its own, e.g. an attached file.
|
|
662
|
+
*/
|
|
663
|
+
|
|
664
|
+
// GIVEN a user message mixing text with a file block
|
|
665
|
+
const messages: Message[] = [
|
|
666
|
+
{
|
|
667
|
+
role: "user",
|
|
668
|
+
content: [
|
|
669
|
+
{ type: "text", text: "see the attachment" },
|
|
670
|
+
{
|
|
671
|
+
type: "file",
|
|
672
|
+
source: {
|
|
673
|
+
type: "base64",
|
|
674
|
+
media_type: "application/pdf",
|
|
675
|
+
data: "pdfdata",
|
|
676
|
+
},
|
|
677
|
+
},
|
|
678
|
+
],
|
|
679
|
+
},
|
|
680
|
+
];
|
|
681
|
+
|
|
682
|
+
// WHEN the content is flattened
|
|
683
|
+
const flattened = flattenTextOnlyBlocks(messages);
|
|
684
|
+
|
|
685
|
+
// THEN both blocks survive
|
|
686
|
+
expect(flattened).toBe(0);
|
|
687
|
+
expect(messages[0].content).toHaveLength(2);
|
|
688
|
+
expect(messages[0].content[1].type).toBe("file");
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
test("leaves assistant content untouched", () => {
|
|
692
|
+
/**
|
|
693
|
+
* Tests that only user content is merged: assistant block boundaries carry
|
|
694
|
+
* provider-side meaning (streamed parts, tool calls).
|
|
695
|
+
*/
|
|
696
|
+
|
|
697
|
+
// GIVEN an assistant message with two text blocks
|
|
698
|
+
const messages: Message[] = [
|
|
699
|
+
{
|
|
700
|
+
role: "assistant",
|
|
701
|
+
content: [
|
|
702
|
+
{ type: "text", text: "first" },
|
|
703
|
+
{ type: "text", text: "second" },
|
|
704
|
+
],
|
|
705
|
+
},
|
|
706
|
+
];
|
|
707
|
+
|
|
708
|
+
// WHEN the content is flattened
|
|
709
|
+
const flattened = flattenTextOnlyBlocks(messages);
|
|
710
|
+
|
|
711
|
+
// THEN the blocks stay separate
|
|
712
|
+
expect(flattened).toBe(0);
|
|
713
|
+
expect(messages[0].content).toHaveLength(2);
|
|
409
714
|
});
|
|
410
715
|
});
|
|
411
716
|
|
|
@@ -545,9 +850,9 @@ describe("image-fallback post-compact hook", () => {
|
|
|
545
850
|
];
|
|
546
851
|
const ctx = makeCompactCtx({ history });
|
|
547
852
|
await postCompact(ctx);
|
|
548
|
-
expect(ctx.history[0].content
|
|
549
|
-
expect((ctx.history[0].content[
|
|
550
|
-
"[Image auto-described for text-only model: A red chart showing Q3 revenue.]",
|
|
853
|
+
expect(ctx.history[0].content).toHaveLength(1);
|
|
854
|
+
expect((ctx.history[0].content[0] as { text: string }).text).toBe(
|
|
855
|
+
"Images retained from the compacted portion of the conversation:\n\n[Image auto-described for text-only model: A red chart showing Q3 revenue.]",
|
|
551
856
|
);
|
|
552
857
|
});
|
|
553
858
|
|
|
@@ -577,15 +882,9 @@ describe("image-fallback post-compact hook", () => {
|
|
|
577
882
|
return sendMessageResponse;
|
|
578
883
|
},
|
|
579
884
|
};
|
|
580
|
-
|
|
581
|
-
doesSupportVision: (arg: ModelProfileInfo | string) =>
|
|
582
|
-
typeof arg === "string"
|
|
583
|
-
? visionModels.has(arg)
|
|
584
|
-
: visionProfiles.has(arg.key),
|
|
585
|
-
getModelProfiles: () => mockProfiles,
|
|
586
|
-
resolveMediaSourceData: mockResolveMediaSourceData,
|
|
885
|
+
installPluginApiMock({
|
|
587
886
|
getConfiguredProvider: async () => trackingProvider,
|
|
588
|
-
})
|
|
887
|
+
});
|
|
589
888
|
|
|
590
889
|
// The image is captioned once at ingestion (turn-start sweep)...
|
|
591
890
|
const ctx1 = makeCtx({ latestMessages: [imageMsg("compacted-image")] });
|
|
@@ -598,18 +897,6 @@ describe("image-fallback post-compact hook", () => {
|
|
|
598
897
|
await postCompact(ctx2);
|
|
599
898
|
expect(callCount).toBe(1); // still 1 — cache hit
|
|
600
899
|
expect(ctx2.history[0].content[0].type).toBe("text");
|
|
601
|
-
|
|
602
|
-
// Restore the original mock for other tests.
|
|
603
|
-
mock.module("@vellumai/plugin-api", () => ({
|
|
604
|
-
doesSupportVision: (arg: ModelProfileInfo | string) =>
|
|
605
|
-
typeof arg === "string"
|
|
606
|
-
? visionModels.has(arg)
|
|
607
|
-
: visionProfiles.has(arg.key),
|
|
608
|
-
getModelProfiles: () => mockProfiles,
|
|
609
|
-
resolveMediaSourceData: mockResolveMediaSourceData,
|
|
610
|
-
getConfiguredProvider: async () =>
|
|
611
|
-
providerResolves ? fakeProvider : null,
|
|
612
|
-
}));
|
|
613
900
|
});
|
|
614
901
|
|
|
615
902
|
test("uses fail-open placeholder when no vision profile is configured", async () => {
|
|
@@ -640,15 +927,9 @@ describe("image-fallback conversation-deleted hook", () => {
|
|
|
640
927
|
return sendMessageResponse;
|
|
641
928
|
},
|
|
642
929
|
};
|
|
643
|
-
|
|
644
|
-
doesSupportVision: (arg: ModelProfileInfo | string) =>
|
|
645
|
-
typeof arg === "string"
|
|
646
|
-
? visionModels.has(arg)
|
|
647
|
-
: visionProfiles.has(arg.key),
|
|
648
|
-
getModelProfiles: () => mockProfiles,
|
|
649
|
-
resolveMediaSourceData: mockResolveMediaSourceData,
|
|
930
|
+
installPluginApiMock({
|
|
650
931
|
getConfiguredProvider: async () => trackingProvider,
|
|
651
|
-
})
|
|
932
|
+
});
|
|
652
933
|
|
|
653
934
|
// Caption an image in the doomed conversation.
|
|
654
935
|
const ctx1 = makeCtx({
|
|
@@ -668,18 +949,6 @@ describe("image-fallback conversation-deleted hook", () => {
|
|
|
668
949
|
});
|
|
669
950
|
await userPromptSubmit(ctx2);
|
|
670
951
|
expect(callCount).toBe(2);
|
|
671
|
-
|
|
672
|
-
// Restore the original mock for other tests.
|
|
673
|
-
mock.module("@vellumai/plugin-api", () => ({
|
|
674
|
-
doesSupportVision: (arg: ModelProfileInfo | string) =>
|
|
675
|
-
typeof arg === "string"
|
|
676
|
-
? visionModels.has(arg)
|
|
677
|
-
: visionProfiles.has(arg.key),
|
|
678
|
-
getModelProfiles: () => mockProfiles,
|
|
679
|
-
resolveMediaSourceData: mockResolveMediaSourceData,
|
|
680
|
-
getConfiguredProvider: async () =>
|
|
681
|
-
providerResolves ? fakeProvider : null,
|
|
682
|
-
}));
|
|
683
952
|
});
|
|
684
953
|
|
|
685
954
|
test("captions shared with a surviving conversation keep serving hits", async () => {
|
|
@@ -691,15 +960,9 @@ describe("image-fallback conversation-deleted hook", () => {
|
|
|
691
960
|
return sendMessageResponse;
|
|
692
961
|
},
|
|
693
962
|
};
|
|
694
|
-
|
|
695
|
-
doesSupportVision: (arg: ModelProfileInfo | string) =>
|
|
696
|
-
typeof arg === "string"
|
|
697
|
-
? visionModels.has(arg)
|
|
698
|
-
: visionProfiles.has(arg.key),
|
|
699
|
-
getModelProfiles: () => mockProfiles,
|
|
700
|
-
resolveMediaSourceData: mockResolveMediaSourceData,
|
|
963
|
+
installPluginApiMock({
|
|
701
964
|
getConfiguredProvider: async () => trackingProvider,
|
|
702
|
-
})
|
|
965
|
+
});
|
|
703
966
|
|
|
704
967
|
// The same image is captioned in one conversation and cache-hit in a
|
|
705
968
|
// second, which records the second conversation's association.
|
|
@@ -724,17 +987,5 @@ describe("image-fallback conversation-deleted hook", () => {
|
|
|
724
987
|
});
|
|
725
988
|
await userPromptSubmit(ctxB2);
|
|
726
989
|
expect(callCount).toBe(1);
|
|
727
|
-
|
|
728
|
-
// Restore the original mock for other tests.
|
|
729
|
-
mock.module("@vellumai/plugin-api", () => ({
|
|
730
|
-
doesSupportVision: (arg: ModelProfileInfo | string) =>
|
|
731
|
-
typeof arg === "string"
|
|
732
|
-
? visionModels.has(arg)
|
|
733
|
-
: visionProfiles.has(arg.key),
|
|
734
|
-
getModelProfiles: () => mockProfiles,
|
|
735
|
-
resolveMediaSourceData: mockResolveMediaSourceData,
|
|
736
|
-
getConfiguredProvider: async () =>
|
|
737
|
-
providerResolves ? fakeProvider : null,
|
|
738
|
-
}));
|
|
739
990
|
});
|
|
740
991
|
});
|
|
@@ -53,6 +53,10 @@ mock.module("@vellumai/plugin-api", () => ({
|
|
|
53
53
|
getConfiguredProvider: async () => fakeProvider,
|
|
54
54
|
isVisionNotSupportedError,
|
|
55
55
|
lastToolResultUserMessageIndex,
|
|
56
|
+
// The module registry is process-wide, so this surface covers every
|
|
57
|
+
// plugin-api export the plugin's hooks import, including the transcript
|
|
58
|
+
// notice only the sibling test file exercises.
|
|
59
|
+
persistSystemCard: async () => "card-1",
|
|
56
60
|
}));
|
|
57
61
|
|
|
58
62
|
mock.module("../src/image-persist.js", () => ({
|