@vellumai/assistant 0.10.1 → 0.10.2-dev.202606241651.2d2b40d
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/docs/workspace-tools.md +42 -33
- package/eslint-rules/cli-no-daemon-internals.js +6 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-delivery-contract.test.ts +91 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/trust-verdict-contract.test.ts +31 -0
- package/node_modules/@vellumai/gateway-client/src/guardian-delivery-contract.ts +48 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +14 -0
- package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +17 -0
- package/openapi.yaml +74 -1
- package/package.json +1 -1
- package/scripts/test.sh +36 -15
- package/src/__tests__/actor-token-service.test.ts +36 -14
- package/src/__tests__/agent-loop-override-profile.test.ts +1 -0
- package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
- package/src/__tests__/agent-wake-override-profile.test.ts +2 -0
- package/src/__tests__/annotate-activity-metadata.test.ts +2 -0
- package/src/__tests__/annotate-risk-options.test.ts +2 -0
- package/src/__tests__/approval-cascade.test.ts +2 -0
- package/src/__tests__/background-workers-disk-pressure.test.ts +2 -0
- package/src/__tests__/btw-routes.test.ts +2 -0
- package/src/__tests__/build-persisted-content.test.ts +2 -0
- package/src/__tests__/call-controller.test.ts +19 -0
- package/src/__tests__/channel-guardian.test.ts +94 -58
- package/src/__tests__/channel-reply-delivery.test.ts +2 -0
- package/src/__tests__/compaction-events.test.ts +2 -0
- package/src/__tests__/compaction.benchmark.test.ts +2 -0
- package/src/__tests__/compactor-call-site-logging.test.ts +2 -0
- package/src/__tests__/compactor-low-watermark-cut.test.ts +2 -0
- package/src/__tests__/compactor-preserved-tail-count.test.ts +2 -0
- package/src/__tests__/compactor-summary-call-truncation.test.ts +2 -0
- package/src/__tests__/compactor-web-search-strip.test.ts +2 -0
- package/src/__tests__/computer-use-tools.test.ts +13 -0
- package/src/__tests__/config-loader-backfill.test.ts +5 -1
- package/src/__tests__/config-schema.test.ts +1 -0
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +31 -29
- package/src/__tests__/contacts-relay-reads.test.ts +13 -15
- package/src/__tests__/conversation-abort-tool-results.test.ts +2 -0
- package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +2 -0
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +2 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +2 -0
- package/src/__tests__/conversation-agent-loop.test.ts +7 -0
- package/src/__tests__/conversation-analysis-routes.test.ts +2 -0
- package/src/__tests__/conversation-app-control-lifecycle.test.ts +2 -0
- package/src/__tests__/conversation-confirmation-signals.test.ts +2 -0
- package/src/__tests__/conversation-history-web-search.test.ts +2 -0
- package/src/__tests__/conversation-load-history-repair.test.ts +2 -0
- package/src/__tests__/conversation-load-history-stripped.test.ts +2 -0
- package/src/__tests__/conversation-pairing.test.ts +2 -0
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +2 -0
- package/src/__tests__/conversation-process-callsite.test.ts +2 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +2 -0
- package/src/__tests__/conversation-queue.test.ts +91 -0
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +14 -0
- package/src/__tests__/conversation-routes-slash-commands.test.ts +14 -0
- package/src/__tests__/conversation-slash-queue.test.ts +2 -0
- package/src/__tests__/conversation-slash-unknown.test.ts +2 -0
- package/src/__tests__/conversation-speed-override.test.ts +2 -0
- package/src/__tests__/conversation-surfaces-action-delivery.test.ts +65 -0
- package/src/__tests__/conversation-title-service.test.ts +2 -0
- package/src/__tests__/conversation-tool-setup-attribution.test.ts +47 -0
- package/src/__tests__/conversation-usage.test.ts +2 -0
- package/src/__tests__/conversation-workspace-cache-state.test.ts +2 -0
- package/src/__tests__/conversation-workspace-injection.test.ts +2 -0
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +2 -0
- package/src/__tests__/credential-security-invariants.test.ts +0 -1
- package/src/__tests__/db-migration-rollback.test.ts +205 -171
- package/src/__tests__/db-test-helpers.ts +5 -4
- package/src/__tests__/deterministic-verification-control-plane.test.ts +4 -2
- package/src/__tests__/disk-pressure-guard.test.ts +41 -0
- package/src/__tests__/dm-persistence.test.ts +2 -0
- package/src/__tests__/emit-signal-routing-intent.test.ts +10 -5
- package/src/__tests__/events-dev-bypass-actor.test.ts +7 -1
- package/src/__tests__/filing-service.test.ts +2 -0
- package/src/__tests__/guardian-binding-drift-heal.test.ts +75 -10
- package/src/__tests__/guardian-dispatch.test.ts +95 -1
- package/src/__tests__/guardian-outbound-http.test.ts +13 -0
- package/src/__tests__/heartbeat-disk-pressure.test.ts +2 -0
- package/src/__tests__/heartbeat-service.test.ts +2 -0
- package/src/__tests__/helpers/channel-test-adapter.ts +1 -7
- package/src/__tests__/host-app-control-routes.test.ts +24 -30
- package/src/__tests__/host-bash-routes.test.ts +31 -41
- package/src/__tests__/host-browser-routes.test.ts +26 -32
- package/src/__tests__/host-cu-proxy.test.ts +299 -0
- package/src/__tests__/host-cu-routes-targeted.test.ts +25 -33
- package/src/__tests__/host-file-routes-targeted.test.ts +40 -52
- package/src/__tests__/host-transfer-routes-targeted.test.ts +31 -43
- package/src/__tests__/http-user-message-parity.test.ts +167 -8
- package/src/__tests__/inbound-slack-persistence.test.ts +2 -0
- package/src/__tests__/invite-redemption-service.test.ts +43 -0
- package/src/__tests__/llm-context-normalization.test.ts +105 -0
- package/src/__tests__/llm-usage-store.test.ts +25 -0
- package/src/__tests__/media-stream-server-integration.test.ts +127 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +2 -0
- package/src/__tests__/messaging-send-tool.test.ts +2 -0
- package/src/__tests__/migration-import-from-url.test.ts +2 -2
- package/src/__tests__/native-web-search.test.ts +2 -0
- package/src/__tests__/non-member-access-request.test.ts +189 -17
- package/src/__tests__/notification-broadcaster.test.ts +4 -0
- package/src/__tests__/notification-decision-recipient-context.test.ts +33 -32
- package/src/__tests__/notification-deep-link.test.ts +6 -0
- package/src/__tests__/notification-guardian-path.test.ts +19 -0
- package/src/__tests__/outbound-slack-persistence.test.ts +2 -0
- package/src/__tests__/pending-interactions-resolved-event.test.ts +7 -4
- package/src/__tests__/persistence-secret-redaction.test.ts +2 -0
- package/src/__tests__/plugin-bootstrap.test.ts +3 -73
- package/src/__tests__/plugin-route-contribution.test.ts +4 -17
- package/src/__tests__/plugin-tool-contribution.test.ts +3 -18
- package/src/__tests__/plugin-types.test.ts +0 -2
- package/src/__tests__/process-message-background-slack.test.ts +2 -0
- package/src/__tests__/process-message-display-content.test.ts +2 -0
- package/src/__tests__/provider-usage-tracking.test.ts +39 -0
- package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +2 -0
- package/src/__tests__/registry.test.ts +3 -0
- package/src/__tests__/relay-server.test.ts +694 -25
- package/src/__tests__/runtime-attachment-metadata.test.ts +0 -1
- package/src/__tests__/secret-ingress-http.test.ts +14 -0
- package/src/__tests__/send-endpoint-busy.test.ts +30 -8
- package/src/__tests__/skills.test.ts +44 -0
- package/src/__tests__/slack-inbound-verification.test.ts +47 -2
- package/src/__tests__/sse-actor-principal-guardian-source.test.ts +102 -0
- package/src/__tests__/steer-on-enqueue-question.test.ts +181 -0
- package/src/__tests__/stt-hints.test.ts +44 -13
- package/src/__tests__/subagent-detail.test.ts +27 -0
- package/src/__tests__/subagent-disposal.test.ts +65 -0
- package/src/__tests__/subagent-notify-parent.test.ts +2 -0
- package/src/__tests__/subagent-spawn-tool-fork.test.ts +2 -0
- package/src/__tests__/subagent-tools.test.ts +2 -0
- package/src/__tests__/suggestion-routes.test.ts +2 -0
- package/src/__tests__/title-generate-hook.test.ts +2 -0
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +2 -0
- package/src/__tests__/tool-executor.test.ts +16 -11
- package/src/__tests__/tool-preview-lifecycle.test.ts +2 -0
- package/src/__tests__/tool-result-metadata-plumbing.test.ts +2 -0
- package/src/__tests__/tool-start-timestamp.test.ts +2 -0
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +10 -10
- package/src/__tests__/twilio-routes.test.ts +96 -0
- package/src/__tests__/verification-control-plane-policy.test.ts +2 -0
- package/src/__tests__/web-search-backend-failure.test.ts +2 -0
- package/src/__tests__/workspace-tool-loader.test.ts +195 -2
- package/src/agent/loop-exclusive-tool.test.ts +150 -0
- package/src/agent/loop.ts +56 -0
- package/src/api/constants/sse-replay.ts +41 -0
- package/src/api/index.ts +6 -0
- package/src/api/responses/llm-request-log-entry.ts +25 -0
- package/src/api/responses/subagent-detail.ts +17 -0
- package/src/calls/__tests__/relay-setup-router.test.ts +262 -4
- package/src/calls/call-domain.ts +3 -3
- package/src/calls/guardian-dispatch.ts +10 -8
- package/src/calls/inbound-trust-reader.ts +17 -1
- package/src/calls/media-stream-server.ts +21 -0
- package/src/calls/relay-server.ts +167 -50
- package/src/calls/relay-setup-router.ts +37 -7
- package/src/calls/relay-verification.ts +4 -4
- package/src/calls/stt-hints.ts +9 -12
- package/src/calls/twilio-routes.ts +14 -4
- package/src/cli/commands/__tests__/cache.test.ts +8 -1
- package/src/cli/commands/cache.ts +194 -181
- package/src/cli/commands/db/__tests__/repair.test.ts +6 -5
- package/src/cli/commands/db/status.ts +37 -1
- package/src/cli/commands/mcp.ts +252 -218
- package/src/cli/commands/memory/__tests__/worker.test.ts +302 -0
- package/src/cli/commands/memory/index.ts +2 -0
- package/src/cli/commands/memory/worker.ts +175 -0
- package/src/cli/commands/plugins.ts +75 -3
- package/src/cli/lib/__tests__/install-from-github.test.ts +102 -0
- package/src/cli/lib/__tests__/list-installed-plugins.test.ts +160 -1
- package/src/cli/lib/list-installed-plugins.ts +179 -1
- package/src/config/__tests__/loader-callsite-strip-fallback.test.ts +143 -0
- package/src/config/bundled-skills/computer-use/TOOLS.json +6 -1
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +27 -17
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +13 -3
- package/src/config/feature-flag-registry.json +0 -8
- package/src/config/loader.ts +36 -5
- package/src/config/schemas/__tests__/memory-v3.test.ts +1 -0
- package/src/config/schemas/memory-lifecycle.ts +12 -0
- package/src/config/schemas/memory-v3.ts +7 -0
- package/src/config/schemas/memory.ts +4 -0
- package/src/config/schemas/timeouts.ts +8 -0
- package/src/config/seed-inference-profiles.ts +14 -5
- package/src/config/skills.ts +27 -5
- package/src/contacts/__tests__/guardian-delivery-reader.test.ts +312 -0
- package/src/contacts/contacts-write.ts +3 -0
- package/src/contacts/guardian-delivery-reader.ts +223 -0
- package/src/daemon/conversation-agent-loop.ts +9 -0
- package/src/daemon/conversation-process.ts +39 -17
- package/src/daemon/conversation-surfaces.ts +8 -0
- package/src/daemon/conversation-tool-setup.ts +49 -16
- package/src/daemon/conversation.ts +21 -2
- package/src/daemon/disk-pressure-guard.ts +12 -2
- package/src/daemon/event-loop-watchdog.ts +28 -1
- package/src/daemon/external-plugins-bootstrap.ts +4 -34
- package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +25 -0
- package/src/daemon/handlers/__tests__/config-channels.test.ts +225 -0
- package/src/daemon/handlers/config-a2a.ts +6 -14
- package/src/daemon/handlers/config-channels.ts +78 -22
- package/src/daemon/handlers/conversations.ts +77 -0
- package/src/daemon/host-cu-proxy.ts +102 -11
- package/src/daemon/lifecycle.ts +4 -0
- package/src/daemon/memory-v2-startup.test.ts +72 -0
- package/src/daemon/memory-v2-startup.ts +87 -19
- package/src/daemon/server.ts +0 -4
- package/src/daemon/shutdown-handlers.ts +20 -0
- package/src/daemon/tool-setup-types.ts +9 -0
- package/src/ipc/__tests__/clients-list-ipc.test.ts +1 -1
- package/src/ipc/assistant-server.ts +2 -2
- package/src/memory/__tests__/301-create-watchdog-events.test.ts +110 -0
- package/src/memory/__tests__/memory-retrospective-job.test.ts +8 -0
- package/src/memory/__tests__/prompt-override.test.ts +192 -0
- package/src/memory/__tests__/watchdog-events-store.test.ts +161 -0
- package/src/memory/conversation-crud.ts +38 -0
- package/src/memory/db-connection.ts +22 -3
- package/src/memory/db-init.ts +36 -502
- package/src/memory/db-singleton.ts +6 -4
- package/src/memory/jobs-worker.ts +58 -0
- package/src/memory/llm-usage-store.ts +48 -20
- package/src/memory/memory-retrospective-job.ts +9 -8
- package/src/memory/migrations/014-backfill-inbox-thread-state.ts +13 -3
- package/src/memory/migrations/136-drop-assistant-id-columns.ts +52 -27
- package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +130 -56
- package/src/memory/migrations/300-add-processing-started-at.ts +30 -0
- package/src/memory/migrations/301-create-watchdog-events.ts +45 -0
- package/src/memory/migrations/__tests__/014-backfill-inbox-thread-state.test.ts +108 -0
- package/src/memory/migrations/__tests__/136-drop-assistant-id-columns.test.ts +82 -0
- package/src/memory/migrations/__tests__/209-strip-thinking-from-consolidated.test.ts +224 -0
- package/src/memory/migrations/__tests__/run-migrations.test.ts +2 -2
- package/src/memory/migrations/run-migrations.ts +90 -6
- package/src/memory/migrations/schema-introspection.ts +14 -0
- package/src/memory/migrations/validate-migration-state.ts +101 -66
- package/src/memory/prompt-override.ts +129 -0
- package/src/memory/schema/conversations.ts +9 -0
- package/src/memory/schema/infrastructure.ts +20 -0
- package/src/memory/steps.ts +573 -0
- package/src/memory/v2/__tests__/cli-command-store.test.ts +25 -0
- package/src/memory/v2/__tests__/skill-store.test.ts +80 -0
- package/src/memory/v2/cli-command-store.ts +75 -38
- package/src/memory/v2/prompts/consolidation.ts +13 -82
- package/src/memory/v2/prompts/router.ts +21 -93
- package/src/memory/v2/skill-store.ts +68 -31
- package/src/memory/watchdog-events-store.ts +87 -0
- package/src/memory/worker-control.ts +118 -0
- package/src/memory/worker-process.ts +72 -0
- package/src/notifications/__tests__/broadcaster.test.ts +16 -8
- package/src/notifications/__tests__/connected-channels.test.ts +114 -0
- package/src/notifications/__tests__/decision-engine.test.ts +78 -9
- package/src/notifications/__tests__/destination-resolver.test.ts +256 -0
- package/src/notifications/broadcaster.ts +8 -1
- package/src/notifications/decision-engine.ts +15 -7
- package/src/notifications/destination-resolver.ts +68 -24
- package/src/notifications/emit-signal.ts +39 -14
- package/src/onboarding/checkin-event.test.ts +220 -0
- package/src/onboarding/checkin-event.ts +321 -0
- package/src/onboarding/schedule-checkin.ts +190 -0
- package/src/permissions/question-prompter.test.ts +1 -1
- package/src/permissions/question-prompter.ts +7 -4
- package/src/plugin-api/index.ts +6 -6
- package/src/plugin-api/types.ts +3 -5
- package/src/plugin-api/vision-support.test.ts +28 -4
- package/src/plugin-api/vision-support.ts +66 -31
- package/src/plugins/defaults/advisor/__tests__/consult.test.ts +161 -0
- package/src/plugins/defaults/advisor/__tests__/context-pack-gating.test.ts +106 -0
- package/src/plugins/defaults/advisor/__tests__/context-pack.test.ts +60 -0
- package/src/plugins/defaults/advisor/consult.ts +110 -6
- package/src/plugins/defaults/advisor/context-pack.ts +288 -0
- package/src/plugins/defaults/advisor/steering.ts +14 -2
- package/src/plugins/defaults/advisor/tools/advisor.ts +32 -5
- package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +47 -7
- package/src/plugins/defaults/image-fallback/hooks/post-tool-use.ts +10 -11
- package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +12 -20
- package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +42 -11
- package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +11 -2
- package/src/plugins/defaults/memory-v3-shadow/pool-select.test.ts +146 -0
- package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +29 -1
- package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +8 -1
- package/src/plugins/mtime-cache.ts +7 -2
- package/src/plugins/types.ts +0 -2
- package/src/providers/anthropic/client.ts +5 -0
- package/src/providers/call-site-routing.ts +4 -0
- package/src/providers/model-catalog.ts +16 -0
- package/src/providers/openai/responses-provider.ts +5 -0
- package/src/providers/openrouter/client.ts +5 -0
- package/src/providers/provider-send-message.ts +4 -0
- package/src/providers/ratelimit.ts +4 -0
- package/src/providers/retry.ts +4 -0
- package/src/providers/types.ts +9 -0
- package/src/providers/usage-tracking.ts +4 -0
- package/src/runtime/__tests__/channel-verification-service.test.ts +133 -0
- package/src/runtime/__tests__/guardian-vellum-migration.test.ts +181 -0
- package/src/runtime/__tests__/is-guardian-bound-for-channel.test.ts +66 -0
- package/src/runtime/__tests__/local-principal-trust.test.ts +164 -0
- package/src/runtime/__tests__/trust-verdict-consumer.test.ts +335 -3
- package/src/runtime/access-request-helper.ts +19 -39
- package/src/runtime/actor-trust-resolver.ts +2 -2
- package/src/runtime/anchored-guardian.test.ts +156 -0
- package/src/runtime/anchored-guardian.ts +135 -0
- package/src/runtime/assistant-event-hub.ts +1 -1
- package/src/runtime/assistant-stream-state.ts +9 -2
- package/src/runtime/auth/__tests__/require-bound-guardian.test.ts +99 -0
- package/src/runtime/auth/require-bound-guardian.ts +21 -11
- package/src/runtime/channel-verification-service.ts +56 -31
- package/src/runtime/confirmation-request-guardian-bridge.ts +3 -3
- package/src/runtime/guardian-vellum-migration.ts +66 -7
- package/src/runtime/invite-redemption-service.ts +50 -18
- package/src/runtime/local-actor-identity.ts +76 -11
- package/src/runtime/local-principal-trust.ts +52 -0
- package/src/runtime/pending-interactions.ts +11 -1
- package/src/runtime/routes/__tests__/channel-verification-revoke.test.ts +56 -5
- package/src/runtime/routes/__tests__/channel-verification-routes.test.ts +1 -1
- package/src/runtime/routes/__tests__/contact-routes.test.ts +212 -0
- package/src/runtime/routes/__tests__/global-search-routes.test.ts +93 -0
- package/src/runtime/routes/__tests__/surface-action-routes.test.ts +215 -1
- package/src/runtime/routes/browser-routes.ts +1 -1
- package/src/runtime/routes/channel-verification-routes.ts +3 -3
- package/src/runtime/routes/contact-routes.ts +8 -32
- package/src/runtime/routes/conversation-cli-routes.ts +4 -5
- package/src/runtime/routes/conversation-list-routes.ts +4 -7
- package/src/runtime/routes/conversation-routes.ts +74 -81
- package/src/runtime/routes/events-routes.ts +2 -2
- package/src/runtime/routes/global-search-routes.ts +3 -1
- package/src/runtime/routes/guardian-action-routes.ts +4 -5
- package/src/runtime/routes/host-app-control-routes.ts +5 -4
- package/src/runtime/routes/host-bash-routes.ts +5 -4
- package/src/runtime/routes/host-browser-routes.ts +9 -11
- package/src/runtime/routes/host-cu-routes.ts +5 -4
- package/src/runtime/routes/host-file-routes.ts +5 -4
- package/src/runtime/routes/host-transfer-routes.ts +6 -6
- package/src/runtime/routes/http-adapter.ts +1 -1
- package/src/runtime/routes/identity-routes.ts +3 -2
- package/src/runtime/routes/inbound-message-handler.ts +5 -5
- package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +97 -5
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +61 -49
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +16 -4
- package/src/runtime/routes/inbound-stages/escalation-intercept.ts +7 -7
- package/src/runtime/routes/inbound-stages/guardian-activation-intercept.test.ts +21 -8
- package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +14 -3
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/llm-context-normalization.ts +71 -0
- package/src/runtime/routes/mcp-auth-routes.ts +38 -15
- package/src/runtime/routes/migration-rollback-routes.ts +4 -3
- package/src/runtime/routes/migration-routes.ts +4 -1
- package/src/runtime/routes/onboarding-checkin-routes.ts +86 -0
- package/src/runtime/routes/subagents-routes.ts +5 -0
- package/src/runtime/routes/surface-action-routes.ts +51 -55
- package/src/runtime/services/__tests__/conversation-serializer.test.ts +1 -0
- package/src/runtime/services/conversation-serializer.ts +7 -9
- package/src/runtime/tool-grant-request-helper.ts +3 -3
- package/src/runtime/trust-verdict-consumer.ts +85 -9
- package/src/runtime/verification-outbound-actions.ts +18 -18
- package/src/signals/user-message.ts +16 -0
- package/src/subagent/manager.ts +9 -0
- package/src/telemetry/types.ts +34 -1
- package/src/telemetry/usage-telemetry-reporter.test.ts +3 -2
- package/src/telemetry/usage-telemetry-reporter.ts +87 -3
- package/src/tools/ask-question/ask-question-tool.test.ts +29 -0
- package/src/tools/ask-question/ask-question-tool.ts +13 -0
- package/src/tools/computer-use/definitions.ts +8 -2
- package/src/tools/executor.ts +4 -4
- package/src/tools/registry.ts +18 -0
- package/src/tools/tool-approval-handler.ts +1 -1
- package/src/tools/tool-defaults.ts +9 -2
- package/src/tools/types.ts +17 -2
- package/src/tools/workspace-tools/loader.ts +348 -244
- package/src/util/platform.ts +5 -0
- package/src/util/telemetry-db-path.ts +24 -0
- package/src/workspace/migrations/017-seed-persona-dirs.ts +3 -34
- package/src/workspace/migrations/019-scope-journal-to-guardian.ts +3 -24
- package/src/__tests__/workspace-tools-watcher-flag.test.ts +0 -70
- package/src/daemon/workspace-tools-watcher.ts +0 -328
- package/src/memory/migrations/registry.ts +0 -573
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schedules the onboarding "Day 2 Check-in" calendar event server-side.
|
|
3
|
+
*
|
|
4
|
+
* Flow:
|
|
5
|
+
* 1. Resolve the Google OAuth connection, requiring the calendar.events scope.
|
|
6
|
+
* No connection / missing scope → a `scheduled: false` result (never an
|
|
7
|
+
* error): the web caller treats this as a best-effort skip.
|
|
8
|
+
* 2. List tomorrow's timed events in the 8am–8pm window (user's timezone) to
|
|
9
|
+
* derive busy intervals. Uses events.list rather than freeBusy.query
|
|
10
|
+
* because the onboarding grant is the narrow `calendar.events` scope, which
|
|
11
|
+
* authorizes events.list/insert but not freeBusy.query.
|
|
12
|
+
* 3. Choose the first open 15-minute slot (12pm–5pm, widening to 8am–8pm).
|
|
13
|
+
* 4. Create the event with the locked title + HTML description, sendUpdates=all.
|
|
14
|
+
*
|
|
15
|
+
* Authenticates via `resolveOAuthConnection("google")` + `connection.request()`,
|
|
16
|
+
* the same path the calendar watcher uses — no skill subprocess.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { canonicalizeTimeZone } from "../daemon/date-context.js";
|
|
20
|
+
import type { OAuthConnection } from "../oauth/connection.js";
|
|
21
|
+
import { resolveOAuthConnection } from "../oauth/connection-resolver.js";
|
|
22
|
+
import { getLogger } from "../util/logger.js";
|
|
23
|
+
import {
|
|
24
|
+
buildCheckinDescription,
|
|
25
|
+
buildCheckinTitle,
|
|
26
|
+
checkinAvailabilityWindow,
|
|
27
|
+
type CheckinNames,
|
|
28
|
+
chooseCheckinSlot,
|
|
29
|
+
extractBusyFromEvents,
|
|
30
|
+
type GcalEvent,
|
|
31
|
+
} from "./checkin-event.js";
|
|
32
|
+
|
|
33
|
+
const log = getLogger("onboarding:schedule-checkin");
|
|
34
|
+
|
|
35
|
+
const GOOGLE_CALENDAR_BASE_URL = "https://www.googleapis.com/calendar/v3";
|
|
36
|
+
const GOOGLE_CALENDAR_EVENTS_SCOPE =
|
|
37
|
+
"https://www.googleapis.com/auth/calendar.events";
|
|
38
|
+
/** Calendar shares the Google OAuth connection with Gmail. */
|
|
39
|
+
const GOOGLE_PROVIDER = "google";
|
|
40
|
+
const PRIMARY_CALENDAR_ID = "primary";
|
|
41
|
+
|
|
42
|
+
export interface ScheduleCheckinInput extends CheckinNames {
|
|
43
|
+
/** IANA timezone reported by the client (e.g. "America/New_York"). */
|
|
44
|
+
timeZone?: string;
|
|
45
|
+
/** Override "now" for deterministic tests. Defaults to the current time. */
|
|
46
|
+
nowMs?: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type ScheduleCheckinResult =
|
|
50
|
+
| {
|
|
51
|
+
scheduled: true;
|
|
52
|
+
eventId: string;
|
|
53
|
+
htmlLink: string | null;
|
|
54
|
+
/** Event start, ISO 8601 (UTC). */
|
|
55
|
+
start: string;
|
|
56
|
+
/** Event end, ISO 8601 (UTC). */
|
|
57
|
+
end: string;
|
|
58
|
+
timeZone: string;
|
|
59
|
+
}
|
|
60
|
+
| {
|
|
61
|
+
scheduled: false;
|
|
62
|
+
/**
|
|
63
|
+
* Why nothing was booked. `calendar_unavailable` covers both "not
|
|
64
|
+
* connected" and "calendar scope not granted" — the client surfaces a
|
|
65
|
+
* single best-effort skip either way.
|
|
66
|
+
*/
|
|
67
|
+
reason: "calendar_unavailable";
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
interface EventsListResponse {
|
|
71
|
+
items?: GcalEvent[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Subset of the events.insert response we surface back to the caller. */
|
|
75
|
+
interface CreatedEvent {
|
|
76
|
+
id?: string;
|
|
77
|
+
htmlLink?: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Schedule the Day 2 check-in. Resolves to a `scheduled: false` result when no
|
|
82
|
+
* calendar is connected; throws only on unexpected Calendar API failures (the
|
|
83
|
+
* route handler maps those to a 5xx, the web caller swallows them).
|
|
84
|
+
*/
|
|
85
|
+
export async function scheduleOnboardingCheckin(
|
|
86
|
+
input: ScheduleCheckinInput,
|
|
87
|
+
): Promise<ScheduleCheckinResult> {
|
|
88
|
+
const timeZone =
|
|
89
|
+
canonicalizeTimeZone(input.timeZone) ??
|
|
90
|
+
// Fall back to the daemon host timezone when the client didn't report one.
|
|
91
|
+
Intl.DateTimeFormat().resolvedOptions().timeZone ??
|
|
92
|
+
"UTC";
|
|
93
|
+
const nowMs = input.nowMs ?? Date.now();
|
|
94
|
+
|
|
95
|
+
let connection: OAuthConnection;
|
|
96
|
+
try {
|
|
97
|
+
connection = await resolveOAuthConnection(GOOGLE_PROVIDER, {
|
|
98
|
+
requiredScopes: [GOOGLE_CALENDAR_EVENTS_SCOPE],
|
|
99
|
+
});
|
|
100
|
+
} catch (err) {
|
|
101
|
+
// No active connection or the calendar scope wasn't granted — skip quietly.
|
|
102
|
+
log.info(
|
|
103
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
104
|
+
"Check-in skipped: Google Calendar not available",
|
|
105
|
+
);
|
|
106
|
+
return { scheduled: false, reason: "calendar_unavailable" };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const { timeMinMs, timeMaxMs } = checkinAvailabilityWindow(nowMs, timeZone);
|
|
110
|
+
|
|
111
|
+
const eventsResp = await connection.request({
|
|
112
|
+
method: "GET",
|
|
113
|
+
path: `/calendars/${encodeURIComponent(PRIMARY_CALENDAR_ID)}/events`,
|
|
114
|
+
query: {
|
|
115
|
+
timeMin: new Date(timeMinMs).toISOString(),
|
|
116
|
+
timeMax: new Date(timeMaxMs).toISOString(),
|
|
117
|
+
// Expand recurring events into instances so each occurrence is a concrete
|
|
118
|
+
// busy interval in the window.
|
|
119
|
+
singleEvents: "true",
|
|
120
|
+
orderBy: "startTime",
|
|
121
|
+
maxResults: "250",
|
|
122
|
+
},
|
|
123
|
+
baseUrl: GOOGLE_CALENDAR_BASE_URL,
|
|
124
|
+
headers: { "Content-Type": "application/json" },
|
|
125
|
+
});
|
|
126
|
+
if (eventsResp.status < 200 || eventsResp.status >= 300) {
|
|
127
|
+
throw new Error(
|
|
128
|
+
`Calendar events.list ${eventsResp.status}: ${stringifyBody(eventsResp.body)}`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const busy = extractBusyFromEvents(
|
|
133
|
+
(eventsResp.body as EventsListResponse).items ?? [],
|
|
134
|
+
);
|
|
135
|
+
const slot = chooseCheckinSlot(nowMs, timeZone, busy);
|
|
136
|
+
|
|
137
|
+
const uuid = crypto.randomUUID();
|
|
138
|
+
const eventResp = await connection.request({
|
|
139
|
+
method: "POST",
|
|
140
|
+
path: `/calendars/${encodeURIComponent(PRIMARY_CALENDAR_ID)}/events`,
|
|
141
|
+
query: { sendUpdates: "all" },
|
|
142
|
+
baseUrl: GOOGLE_CALENDAR_BASE_URL,
|
|
143
|
+
headers: { "Content-Type": "application/json" },
|
|
144
|
+
body: {
|
|
145
|
+
summary: buildCheckinTitle(input),
|
|
146
|
+
description: buildCheckinDescription(uuid),
|
|
147
|
+
start: {
|
|
148
|
+
dateTime: new Date(slot.startMs).toISOString(),
|
|
149
|
+
timeZone,
|
|
150
|
+
},
|
|
151
|
+
end: {
|
|
152
|
+
dateTime: new Date(slot.endMs).toISOString(),
|
|
153
|
+
timeZone,
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
if (eventResp.status < 200 || eventResp.status >= 300) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
`Calendar event create ${eventResp.status}: ${stringifyBody(eventResp.body)}`,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const event = eventResp.body as CreatedEvent;
|
|
164
|
+
if (!event.id) {
|
|
165
|
+
throw new Error("Calendar event create returned no event id");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
log.info(
|
|
169
|
+
{
|
|
170
|
+
eventId: event.id,
|
|
171
|
+
start: new Date(slot.startMs).toISOString(),
|
|
172
|
+
window: slot.window,
|
|
173
|
+
timeZone,
|
|
174
|
+
},
|
|
175
|
+
"Scheduled onboarding Day 2 check-in",
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
scheduled: true,
|
|
180
|
+
eventId: event.id,
|
|
181
|
+
htmlLink: event.htmlLink ?? null,
|
|
182
|
+
start: new Date(slot.startMs).toISOString(),
|
|
183
|
+
end: new Date(slot.endMs).toISOString(),
|
|
184
|
+
timeZone,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function stringifyBody(body: unknown): string {
|
|
189
|
+
return typeof body === "string" ? body : JSON.stringify(body ?? "");
|
|
190
|
+
}
|
|
@@ -9,7 +9,7 @@ import type {
|
|
|
9
9
|
|
|
10
10
|
// Use a tiny timeout so the setTimeout branch fires quickly in tests
|
|
11
11
|
const mockConfig = {
|
|
12
|
-
timeouts: {
|
|
12
|
+
timeouts: { questionResponseTimeoutSec: 0.05 },
|
|
13
13
|
};
|
|
14
14
|
// Preserve every other export from the real config/loader so other
|
|
15
15
|
// tests in the same `bun test` run (which share module-level mocks)
|
|
@@ -156,9 +156,12 @@ export interface QuestionBatchMetadata {
|
|
|
156
156
|
* the whole batch to `/v1/question-response` when the user is done — no
|
|
157
157
|
* per-question accumulator, no partial state machine.
|
|
158
158
|
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
159
|
+
* The idle timeout is a backstop (`getConfig().timeouts.questionResponseTimeoutSec`,
|
|
160
|
+
* default 30 min), not the primary way a prompt is dismissed. An interactive
|
|
161
|
+
* user who moves on instead of answering enqueues another message, which
|
|
162
|
+
* supersedes the open prompt at the enqueue handler (see conversation-routes.ts);
|
|
163
|
+
* a non-interactive turn resolves immediately at the tool. The backstop only
|
|
164
|
+
* fires when a prompt is left open with no response and no follow-up message.
|
|
162
165
|
*/
|
|
163
166
|
export class QuestionPrompter {
|
|
164
167
|
async prompt(params: QuestionPromptParams): Promise<QuestionPromptResult> {
|
|
@@ -199,7 +202,7 @@ export class QuestionPrompter {
|
|
|
199
202
|
const requestId = uuid();
|
|
200
203
|
|
|
201
204
|
return new Promise<QuestionPromptResult>((resolve, reject) => {
|
|
202
|
-
const timeoutMs = getConfig().timeouts.
|
|
205
|
+
const timeoutMs = getConfig().timeouts.questionResponseTimeoutSec * 1000;
|
|
203
206
|
|
|
204
207
|
// Closure-scoped idempotency guard. Every resolution path (timeout,
|
|
205
208
|
// abort, route resolution via `rpcResolve`/`rpcReject`) routes through
|
package/src/plugin-api/index.ts
CHANGED
|
@@ -130,12 +130,12 @@ export type {
|
|
|
130
130
|
} from "../runtime/assistant-event-hub.js";
|
|
131
131
|
export { assistantEventHub } from "../runtime/assistant-event-hub.js";
|
|
132
132
|
export { getModelProfiles } from "./model-profiles.js";
|
|
133
|
-
// Check whether a
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
//
|
|
133
|
+
// Check whether a model or profile can process image input. Accepts a concrete
|
|
134
|
+
// model id, a profile key, or a `ModelProfileInfo`; a bare string is resolved
|
|
135
|
+
// as a model id first and then as a profile key. Profile resolution merges over
|
|
136
|
+
// the workspace default and infers the provider for model-only profiles, then
|
|
137
|
+
// looks up the model catalog's `supportsVision` flag (mix profiles are
|
|
138
|
+
// vision-capable if any arm is). Returns false when nothing resolves.
|
|
139
139
|
export { doesSupportVision } from "./vision-support.js";
|
|
140
140
|
// Resolve a provider for a call site (optionally overriding the profile) so a
|
|
141
141
|
// plugin can run inference through the workspace's configured profiles and
|
package/src/plugin-api/types.ts
CHANGED
|
@@ -72,15 +72,13 @@ export type PluginHookFn<TCtx = unknown> = (
|
|
|
72
72
|
// ─── Init context ────────────────────────────────────────────────────────────
|
|
73
73
|
|
|
74
74
|
/**
|
|
75
|
-
* Context passed to `Plugin.init()` during bootstrap. Carries resolved
|
|
76
|
-
* config
|
|
77
|
-
*
|
|
75
|
+
* Context passed to `Plugin.init()` during bootstrap. Carries the resolved
|
|
76
|
+
* config, a pino-compatible logger scoped to the plugin, a per-plugin
|
|
77
|
+
* writable data directory, and the assistant's version metadata.
|
|
78
78
|
*/
|
|
79
79
|
export interface PluginInitContext {
|
|
80
80
|
/** Parsed config for this plugin (may be `unknown` until the manifest validates). */
|
|
81
81
|
config: unknown;
|
|
82
|
-
/** Resolved credential values keyed by the entries of `manifest.requiresCredential`. */
|
|
83
|
-
credentials: Record<string, string>;
|
|
84
82
|
/** Pino-compatible child logger bound to `{ plugin: <name> }`. */
|
|
85
83
|
logger: PluginLogger;
|
|
86
84
|
/** Absolute path to `<workspaceDir>/plugins-data/<plugin>/` (created by bootstrap). */
|
|
@@ -84,16 +84,16 @@ describe("doesSupportVision", () => {
|
|
|
84
84
|
expect(doesSupportVision(profile("text-profile"))).toBe(false);
|
|
85
85
|
});
|
|
86
86
|
|
|
87
|
-
test("
|
|
87
|
+
test("returns false for an unknown profile key not in config", () => {
|
|
88
88
|
setMockConfig({});
|
|
89
|
-
expect(doesSupportVision(profile("nonexistent"))).toBe(
|
|
89
|
+
expect(doesSupportVision(profile("nonexistent"))).toBe(false);
|
|
90
90
|
});
|
|
91
91
|
|
|
92
|
-
test("
|
|
92
|
+
test("returns false for an unknown provider/model pair", () => {
|
|
93
93
|
setMockConfig({
|
|
94
94
|
"unknown-model": { provider: "unknown-provider", model: "unknown-model" },
|
|
95
95
|
});
|
|
96
|
-
expect(doesSupportVision(profile("unknown-model"))).toBe(
|
|
96
|
+
expect(doesSupportVision(profile("unknown-model"))).toBe(false);
|
|
97
97
|
});
|
|
98
98
|
|
|
99
99
|
test("inherits provider from llm.default when profile only sets model", () => {
|
|
@@ -147,3 +147,27 @@ describe("doesSupportVision", () => {
|
|
|
147
147
|
expect(doesSupportVision(profile("mix-profile"))).toBe(false);
|
|
148
148
|
});
|
|
149
149
|
});
|
|
150
|
+
|
|
151
|
+
describe("doesSupportVision with a bare string", () => {
|
|
152
|
+
test("returns true for a known vision-capable model id", () => {
|
|
153
|
+
expect(doesSupportVision("claude-opus-4-6")).toBe(true);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("returns false for a known text-only model id", () => {
|
|
157
|
+
expect(doesSupportVision("accounts/fireworks/models/glm-5p2")).toBe(false);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("falls back to resolving the string as a profile key", () => {
|
|
161
|
+
// "vision-profile" is not a catalog model id, so it resolves as a profile
|
|
162
|
+
// key → anthropic/claude-opus-4-6 (vision-capable).
|
|
163
|
+
setMockConfig({
|
|
164
|
+
"vision-profile": { provider: "anthropic", model: "claude-opus-4-6" },
|
|
165
|
+
});
|
|
166
|
+
expect(doesSupportVision("vision-profile")).toBe(true);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("returns false for a string that is neither a model nor a profile", () => {
|
|
170
|
+
setMockConfig({});
|
|
171
|
+
expect(doesSupportVision("some-unknown-string")).toBe(false);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
@@ -3,10 +3,17 @@
|
|
|
3
3
|
*
|
|
4
4
|
* A plugin that gates image processing on vision capability (e.g. an
|
|
5
5
|
* image-to-text fallback for text-only models) calls {@link doesSupportVision}
|
|
6
|
-
* instead of hardcoding model names.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* instead of hardcoding model names. One entry point serves both shapes a
|
|
7
|
+
* caller might hold:
|
|
8
|
+
* - a concrete model id (e.g. the provider-reported model that just ran), and
|
|
9
|
+
* - a profile — either a {@link ModelProfileInfo} or a bare profile key —
|
|
10
|
+
* resolved through `llm.profiles` to an effective `(provider, model)`.
|
|
11
|
+
*
|
|
12
|
+
* A bare string is tried as a model id first and then as a profile key, so the
|
|
13
|
+
* two callers share one function. Resolution returns `false` when nothing
|
|
14
|
+
* resolves (rather than failing open): a consumer gating an image→text
|
|
15
|
+
* fallback wants an unknown model treated as "can't show images" — caption it —
|
|
16
|
+
* over silently shipping a raw image to a provider that may reject it.
|
|
10
17
|
*/
|
|
11
18
|
|
|
12
19
|
import { getConfig } from "../config/loader.js";
|
|
@@ -17,46 +24,74 @@ import {
|
|
|
17
24
|
import type { ModelProfileInfo } from "./types.js";
|
|
18
25
|
|
|
19
26
|
/**
|
|
20
|
-
* Whether
|
|
27
|
+
* Whether the given model or profile can process image input.
|
|
21
28
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
* `modelOrProfile` may be a concrete model id, a profile key, or a
|
|
30
|
+
* {@link ModelProfileInfo}. A bare string is resolved as a model id first and,
|
|
31
|
+
* failing that, as a profile key. Returns `false` when nothing resolves.
|
|
32
|
+
*/
|
|
33
|
+
export function doesSupportVision(
|
|
34
|
+
modelOrProfile: ModelProfileInfo | string,
|
|
35
|
+
): boolean {
|
|
36
|
+
if (typeof modelOrProfile === "string") {
|
|
37
|
+
// Concrete model id first, then fall back to treating it as a profile key.
|
|
38
|
+
return (
|
|
39
|
+
modelVision(modelOrProfile) ?? profileVision(modelOrProfile) ?? false
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
return profileVision(modelOrProfile.key) ?? false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Catalog vision flag for a concrete model id, or `undefined` when the catalog
|
|
47
|
+
* doesn't know the model. The same model id carries the same capability under
|
|
48
|
+
* every provider that offers it, so the first catalog match wins.
|
|
33
49
|
*/
|
|
34
|
-
|
|
50
|
+
function modelVision(model: string): boolean | undefined {
|
|
51
|
+
for (const provider of PROVIDER_CATALOG) {
|
|
52
|
+
const catalogModel = provider.models.find((m) => m.id === model);
|
|
53
|
+
if (catalogModel != null) return catalogModel.supportsVision ?? false;
|
|
54
|
+
}
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Resolve a profile key through `llm.profiles` to its vision capability, or
|
|
60
|
+
* `undefined` when the key is unknown or resolves to a model the catalog
|
|
61
|
+
* doesn't know. A mix profile resolves to `true` if any arm supports vision
|
|
62
|
+
* (the mix can route to it) and `false` only once every arm is a known
|
|
63
|
+
* text-only model.
|
|
64
|
+
*/
|
|
65
|
+
function profileVision(profileKey: string): boolean | undefined {
|
|
35
66
|
const { llm } = getConfig();
|
|
36
|
-
const entry = llm.profiles[
|
|
37
|
-
if (entry == null) return
|
|
67
|
+
const entry = llm.profiles[profileKey];
|
|
68
|
+
if (entry == null) return undefined;
|
|
38
69
|
|
|
39
|
-
// Mix: fail-open if any arm supports vision.
|
|
40
70
|
if (entry.mix != null) {
|
|
41
|
-
|
|
71
|
+
let sawUnknown = false;
|
|
72
|
+
for (const arm of entry.mix) {
|
|
42
73
|
const armEntry = llm.profiles[arm.profile];
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
74
|
+
const armVision =
|
|
75
|
+
armEntry == null ? undefined : resolveEntryVision(armEntry, llm);
|
|
76
|
+
if (armVision === true) return true;
|
|
77
|
+
if (armVision == null) sawUnknown = true;
|
|
78
|
+
}
|
|
79
|
+
return sawUnknown ? undefined : false;
|
|
46
80
|
}
|
|
47
81
|
|
|
48
|
-
return
|
|
82
|
+
return resolveEntryVision(entry, llm);
|
|
49
83
|
}
|
|
50
84
|
|
|
51
85
|
/**
|
|
52
86
|
* Resolve whether a concrete (non-mix) profile entry supports vision by
|
|
53
|
-
* merging its fields over `llm.default` and inferring the provider when
|
|
54
|
-
*
|
|
87
|
+
* merging its fields over `llm.default` and inferring the provider when only
|
|
88
|
+
* the model is set. Returns `undefined` when the effective `(provider, model)`
|
|
89
|
+
* can't be determined or isn't in the catalog.
|
|
55
90
|
*/
|
|
56
|
-
function
|
|
91
|
+
function resolveEntryVision(
|
|
57
92
|
entry: { provider?: string; model?: string },
|
|
58
93
|
llm: { default?: { provider?: string; model?: string } },
|
|
59
|
-
): boolean {
|
|
94
|
+
): boolean | undefined {
|
|
60
95
|
const provider = entry.provider ?? llm.default?.provider;
|
|
61
96
|
const model = entry.model ?? llm.default?.model;
|
|
62
97
|
|
|
@@ -67,12 +102,12 @@ function resolveEntrySupportsVision(
|
|
|
67
102
|
(typeof model === "string" ? getCatalogProviderForModel(model) : undefined);
|
|
68
103
|
|
|
69
104
|
if (typeof effectiveProvider !== "string" || typeof model !== "string") {
|
|
70
|
-
return
|
|
105
|
+
return undefined;
|
|
71
106
|
}
|
|
72
107
|
|
|
73
108
|
const catalogProvider = PROVIDER_CATALOG.find(
|
|
74
109
|
(p) => p.id === effectiveProvider,
|
|
75
110
|
);
|
|
76
111
|
const catalogModel = catalogProvider?.models.find((m) => m.id === model);
|
|
77
|
-
return catalogModel?.supportsVision
|
|
112
|
+
return catalogModel?.supportsVision;
|
|
78
113
|
}
|
|
@@ -8,12 +8,26 @@ let sendMessageArgs: Record<string, unknown> | null = null;
|
|
|
8
8
|
let responseText = "Use a channel-based worker pool; drain on shutdown.";
|
|
9
9
|
let sendMessageError: Error | null = null;
|
|
10
10
|
let providerResolves = true;
|
|
11
|
+
let providerSupportsWeb = false;
|
|
12
|
+
let streamDeltas: string[] = [];
|
|
13
|
+
let streamEvents: Array<Record<string, unknown>> = [];
|
|
11
14
|
|
|
12
15
|
const fakeProvider = {
|
|
13
16
|
name: "mock-advisor-provider",
|
|
17
|
+
get supportsNativeWebSearch() {
|
|
18
|
+
return providerSupportsWeb;
|
|
19
|
+
},
|
|
14
20
|
async sendMessage(messages: unknown, options: unknown) {
|
|
15
21
|
sendMessageArgs = { messages, options } as Record<string, unknown>;
|
|
16
22
|
if (sendMessageError) throw sendMessageError;
|
|
23
|
+
const onEvent = (
|
|
24
|
+
options as { onEvent?: (e: Record<string, unknown>) => void }
|
|
25
|
+
).onEvent;
|
|
26
|
+
if (onEvent) {
|
|
27
|
+
// Activity (search/thinking) streams before the final advice text.
|
|
28
|
+
for (const ev of streamEvents) onEvent(ev);
|
|
29
|
+
for (const text of streamDeltas) onEvent({ type: "text_delta", text });
|
|
30
|
+
}
|
|
17
31
|
return {
|
|
18
32
|
content: [{ type: "text", text: responseText }],
|
|
19
33
|
model: "mock-model",
|
|
@@ -29,6 +43,14 @@ mock.module("../../../../providers/provider-send-message.js", () => ({
|
|
|
29
43
|
getConfiguredProvider: async () => (providerResolves ? fakeProvider : null),
|
|
30
44
|
}));
|
|
31
45
|
|
|
46
|
+
// Keep the tool tests focused on the consult wiring: stub the context pack so
|
|
47
|
+
// they don't reach into the registry / workspace / memory sources (those have
|
|
48
|
+
// their own coverage). The consult itself never imports this module.
|
|
49
|
+
mock.module("../context-pack.js", () => ({
|
|
50
|
+
buildAdvisorContext: async () => null,
|
|
51
|
+
deriveRecallQuery: () => null,
|
|
52
|
+
}));
|
|
53
|
+
|
|
32
54
|
const { consultAdvisor } = await import("../consult.js");
|
|
33
55
|
const advisorTool = (await import("../tools/advisor.js")).default;
|
|
34
56
|
const { recordSystemPrompt, recordMessages, resetAdvisorStateForTests } =
|
|
@@ -49,6 +71,9 @@ beforeEach(() => {
|
|
|
49
71
|
responseText = "Use a channel-based worker pool; drain on shutdown.";
|
|
50
72
|
sendMessageError = null;
|
|
51
73
|
providerResolves = true;
|
|
74
|
+
providerSupportsWeb = false;
|
|
75
|
+
streamDeltas = [];
|
|
76
|
+
streamEvents = [];
|
|
52
77
|
resetAdvisorStateForTests();
|
|
53
78
|
});
|
|
54
79
|
|
|
@@ -100,6 +125,104 @@ describe("consultAdvisor", () => {
|
|
|
100
125
|
expect(options.systemPrompt).toContain("You are a coding agent.");
|
|
101
126
|
});
|
|
102
127
|
|
|
128
|
+
test("stays tool-less when the provider has no native web search", async () => {
|
|
129
|
+
providerSupportsWeb = false;
|
|
130
|
+
await consultAdvisor({ systemPrompt: null, messages: [userMsg("hi")] });
|
|
131
|
+
const options = sendMessageArgs?.options as { tools?: unknown };
|
|
132
|
+
expect(options.tools).toBeUndefined();
|
|
133
|
+
expect(optionConfig().tool_choice).toEqual({ type: "none" });
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("enables native web search when the provider supports it", async () => {
|
|
137
|
+
providerSupportsWeb = true;
|
|
138
|
+
await consultAdvisor({ systemPrompt: null, messages: [userMsg("hi")] });
|
|
139
|
+
|
|
140
|
+
const options = sendMessageArgs?.options as {
|
|
141
|
+
tools?: Array<{ name: string }>;
|
|
142
|
+
};
|
|
143
|
+
expect(options.tools?.map((t) => t.name)).toEqual(["web_search"]);
|
|
144
|
+
// tool_choice must not be `none`, or the provider suppresses its server tool.
|
|
145
|
+
expect(optionConfig().tool_choice).toEqual({ type: "auto" });
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("streams web-search activity to onText, not just the final advice", async () => {
|
|
149
|
+
providerSupportsWeb = true;
|
|
150
|
+
streamEvents = [
|
|
151
|
+
{ type: "server_tool_start", name: "web_search", toolUseId: "s1", input: {} },
|
|
152
|
+
{
|
|
153
|
+
type: "server_tool_complete",
|
|
154
|
+
toolUseId: "s1",
|
|
155
|
+
isError: false,
|
|
156
|
+
resolvedInput: { query: "vellum streaming" },
|
|
157
|
+
},
|
|
158
|
+
];
|
|
159
|
+
streamDeltas = ["Here is ", "the advice."];
|
|
160
|
+
const chunks: string[] = [];
|
|
161
|
+
|
|
162
|
+
await consultAdvisor({
|
|
163
|
+
systemPrompt: null,
|
|
164
|
+
messages: [userMsg("hi")],
|
|
165
|
+
onText: (c) => chunks.push(c),
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const joined = chunks.join("");
|
|
169
|
+
// The drawer isn't silent during the search prefix...
|
|
170
|
+
expect(joined).toContain("Searching the web");
|
|
171
|
+
expect(joined).toContain("Searched: vellum streaming");
|
|
172
|
+
// ...and the advice text still streams.
|
|
173
|
+
expect(joined).toContain("Here is the advice.");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("surfaces a failure note (not 'Searched') when a web search errors", async () => {
|
|
177
|
+
providerSupportsWeb = true;
|
|
178
|
+
streamEvents = [
|
|
179
|
+
{ type: "server_tool_start", name: "web_search", toolUseId: "s1", input: {} },
|
|
180
|
+
{
|
|
181
|
+
type: "server_tool_complete",
|
|
182
|
+
toolUseId: "s1",
|
|
183
|
+
isError: true,
|
|
184
|
+
errorCode: "query_too_long",
|
|
185
|
+
resolvedInput: { query: "an overly long query" },
|
|
186
|
+
},
|
|
187
|
+
];
|
|
188
|
+
streamDeltas = ["Proceeding without search."];
|
|
189
|
+
const chunks: string[] = [];
|
|
190
|
+
|
|
191
|
+
await consultAdvisor({
|
|
192
|
+
systemPrompt: null,
|
|
193
|
+
messages: [userMsg("hi")],
|
|
194
|
+
onText: (c) => chunks.push(c),
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const joined = chunks.join("");
|
|
198
|
+
expect(joined).toContain("Web search failed");
|
|
199
|
+
expect(joined).not.toContain("🔎 Searched:");
|
|
200
|
+
// The consult still continues and streams its guidance.
|
|
201
|
+
expect(joined).toContain("Proceeding without search.");
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("streams the model's reasoning summary to onText", async () => {
|
|
205
|
+
streamEvents = [{ type: "thinking_delta", thinking: "weighing tradeoffs" }];
|
|
206
|
+
const chunks: string[] = [];
|
|
207
|
+
await consultAdvisor({
|
|
208
|
+
systemPrompt: null,
|
|
209
|
+
messages: [userMsg("hi")],
|
|
210
|
+
onText: (c) => chunks.push(c),
|
|
211
|
+
});
|
|
212
|
+
expect(chunks.join("")).toContain("weighing tradeoffs");
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("embeds the runtime context in the advisor system prompt", async () => {
|
|
216
|
+
await consultAdvisor({
|
|
217
|
+
systemPrompt: "You are a coding agent.",
|
|
218
|
+
messages: [userMsg("hi")],
|
|
219
|
+
runtimeContext: "## Available tools\n- bash — run commands",
|
|
220
|
+
});
|
|
221
|
+
const options = sendMessageArgs?.options as { systemPrompt: string };
|
|
222
|
+
expect(options.systemPrompt).toContain("<agent_runtime_context>");
|
|
223
|
+
expect(options.systemPrompt).toContain("- bash — run commands");
|
|
224
|
+
});
|
|
225
|
+
|
|
103
226
|
test("soft-fails when no provider is configured", async () => {
|
|
104
227
|
providerResolves = false;
|
|
105
228
|
const advice = await consultAdvisor({
|
|
@@ -123,6 +246,29 @@ describe("consultAdvisor", () => {
|
|
|
123
246
|
});
|
|
124
247
|
expect(advice).toContain("no guidance");
|
|
125
248
|
});
|
|
249
|
+
|
|
250
|
+
test("streams the model's text deltas to `onText` as it generates", async () => {
|
|
251
|
+
streamDeltas = ["Use a ", "channel-based ", "worker pool."];
|
|
252
|
+
const chunks: string[] = [];
|
|
253
|
+
|
|
254
|
+
const advice = await consultAdvisor({
|
|
255
|
+
systemPrompt: null,
|
|
256
|
+
messages: [userMsg("hi")],
|
|
257
|
+
onText: (c) => chunks.push(c),
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// Each visible delta is forwarded live...
|
|
261
|
+
expect(chunks).toEqual(["Use a ", "channel-based ", "worker pool."]);
|
|
262
|
+
// ...and the complete guidance is still returned.
|
|
263
|
+
expect(advice).toBe(responseText);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("registers no `onEvent` sink when `onText` is absent", async () => {
|
|
267
|
+
streamDeltas = ["x"];
|
|
268
|
+
await consultAdvisor({ systemPrompt: null, messages: [userMsg("hi")] });
|
|
269
|
+
const options = sendMessageArgs?.options as { onEvent?: unknown };
|
|
270
|
+
expect(options.onEvent).toBeUndefined();
|
|
271
|
+
});
|
|
126
272
|
});
|
|
127
273
|
|
|
128
274
|
describe("advisor tool.execute", () => {
|
|
@@ -150,4 +296,19 @@ describe("advisor tool.execute", () => {
|
|
|
150
296
|
expect(result?.content).toContain("advisor unavailable");
|
|
151
297
|
expect(result?.content).toContain("kaboom");
|
|
152
298
|
});
|
|
299
|
+
|
|
300
|
+
test("streams the consult live via `ctx.onOutput`", async () => {
|
|
301
|
+
recordMessages("c3", [userMsg("hi")]);
|
|
302
|
+
streamDeltas = ["plan: ", "do X"];
|
|
303
|
+
const out: string[] = [];
|
|
304
|
+
|
|
305
|
+
const result = await advisorTool.execute?.({}, {
|
|
306
|
+
conversationId: "c3",
|
|
307
|
+
onOutput: (c: string) => out.push(c),
|
|
308
|
+
} as never);
|
|
309
|
+
|
|
310
|
+
expect(out).toEqual(["plan: ", "do X"]);
|
|
311
|
+
expect(result?.isError).toBe(false);
|
|
312
|
+
expect(result?.content).toBe(responseText);
|
|
313
|
+
});
|
|
153
314
|
});
|