@vellumai/assistant 0.9.0-dev.202606162337.b5a0f93 → 0.9.0-dev.202606170116.ccd16ef

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.
Files changed (57) hide show
  1. package/ARCHITECTURE.md +12 -28
  2. package/docs/architecture/security.md +4 -4
  3. package/docs/stt-provider-onboarding.md +3 -5
  4. package/docs/workflows-testing.md +13 -44
  5. package/docs/workflows.md +3 -5
  6. package/node_modules/@vellumai/environments/src/seeds.ts +2 -5
  7. package/package.json +1 -1
  8. package/src/__tests__/assistant-feature-flag-guard.test.ts +1 -4
  9. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +0 -2
  10. package/src/__tests__/dynamic-page-surface.test.ts +101 -1
  11. package/src/__tests__/gateway-only-guard.test.ts +3 -7
  12. package/src/__tests__/llm-callsite-catalog.test.ts +5 -6
  13. package/src/__tests__/llm-resolver.test.ts +49 -24
  14. package/src/__tests__/oauth-provider-seed-logos.test.ts +4 -6
  15. package/src/__tests__/plugin-api-model-profiles.test.ts +178 -0
  16. package/src/__tests__/schedule-routes-workflow-validation.test.ts +1 -10
  17. package/src/__tests__/schedule-routes.test.ts +0 -30
  18. package/src/__tests__/schedule-tools.test.ts +2 -18
  19. package/src/__tests__/skill-execute-input.test.ts +46 -1
  20. package/src/__tests__/skill-runtime-path.test.ts +2 -3
  21. package/src/api/README.md +6 -6
  22. package/src/api/responses/conversation-message.ts +2 -4
  23. package/src/api/responses/home.ts +0 -4
  24. package/src/calls/voice-session-bridge.ts +2 -2
  25. package/src/config/bundled-skills/workflows/SKILL.md +0 -1
  26. package/src/config/call-site-defaults.ts +12 -2
  27. package/src/config/feature-flag-registry.json +0 -16
  28. package/src/config/profile-order.ts +28 -0
  29. package/src/config/schemas/elevenlabs.ts +0 -1
  30. package/src/daemon/conversation-slash.ts +2 -23
  31. package/src/daemon/conversation-tool-setup.ts +10 -2
  32. package/src/home/relationship-state.ts +2 -4
  33. package/src/memory/__tests__/memory-retrospective-job.test.ts +195 -401
  34. package/src/memory/bookmark-crud.ts +1 -2
  35. package/src/memory/memory-retrospective-constants.ts +4 -4
  36. package/src/memory/memory-retrospective-job.ts +19 -227
  37. package/src/oauth/AGENTS.md +5 -24
  38. package/src/plugin-api/index.ts +4 -0
  39. package/src/plugin-api/model-profiles.ts +33 -0
  40. package/src/plugin-api/types.ts +29 -0
  41. package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +54 -2
  42. package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +107 -7
  43. package/src/runtime/routes/conversation-compaction-routes.ts +1 -1
  44. package/src/runtime/routes/conversation-routes.ts +2 -2
  45. package/src/runtime/routes/schedule-routes.ts +0 -22
  46. package/src/runtime/routes/workflow-routes.test.ts +4 -43
  47. package/src/runtime/routes/workflow-routes.ts +0 -28
  48. package/src/tools/network/web-search-error.ts +1 -1
  49. package/src/tools/schedule/create.ts +3 -9
  50. package/src/tools/schedule/update.ts +2 -10
  51. package/src/tools/skills/execute.ts +34 -0
  52. package/src/tools/ui-surface/definitions.ts +39 -1
  53. package/src/tools/workflows/run-workflow.test.ts +8 -18
  54. package/src/util/platform.ts +2 -2
  55. package/src/workflows/capabilities.ts +2 -3
  56. package/src/workflows/run-manager.test.ts +0 -25
  57. package/src/workflows/run-manager.ts +2 -24
package/ARCHITECTURE.md CHANGED
@@ -609,10 +609,9 @@ Audio-to-text conversion occurs in six distinct runtime boundaries, each with it
609
609
  | ---------------------------- | ----------------------------------------------------------------------------- | -------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
610
610
  | **Telephony (hybrid)** | Twilio-native ConversationRelay or daemon media-stream (provider-conditional) | Configured STT provider (via `services.stt`) | `src/calls/telephony-stt-routing.ts` | `src/calls/twilio-routes.ts` |
611
611
  | **Daemon batch** | Daemon process (REST API to provider) | Configured STT provider (via `services.stt`) | `src/stt/daemon-batch-transcriber.ts` | `src/runtime/routes/inbound-stages/transcribe-audio.ts` |
612
- | **Conversation streaming** | Daemon process (WebSocket-based) | Configured STT provider (via `services.stt`) | `src/stt/stt-stream-session.ts`, `src/providers/speech-to-text/deepgram-realtime.ts`, `src/providers/speech-to-text/google-gemini-live-stream.ts`, `src/providers/speech-to-text/openai-whisper-stream.ts`, `src/providers/speech-to-text/xai-realtime.ts` | `VoiceInputManager` (macOS conversation) via gateway WS proxy |
613
- | **Live voice channel** | Assistant process (gateway-authenticated WebSocket) | Configured STT provider (via `services.stt`) | `src/runtime/http-server.ts`, `src/live-voice/live-voice-session-manager.ts`, `src/live-voice/live-voice-session.ts`, `src/providers/speech-to-text/resolve.ts`, streaming provider adapters | `LiveVoiceChannelManager` (macOS voice mode) via `/v1/live-voice` |
614
- | **Client service-first** | macOS via gateway → daemon | Configured STT provider (via `services.stt`) | `src/runtime/routes/stt-routes.ts`, `clients/shared/Network/STTClient.swift` | `VoiceInputManager` (macOS dictation), `OpenAIVoiceService` (macOS voice mode) |
615
- | **Client-native (fallback)** | macOS on-device | Apple Speech (`SFSpeechRecognizer`) | `clients/macos/.../SpeechRecognizerAdapter.swift` | Fallback when STT service is unconfigured or fails |
612
+ | **Conversation streaming** | Daemon process (WebSocket-based) | Configured STT provider (via `services.stt`) | `src/stt/stt-stream-session.ts`, `src/providers/speech-to-text/deepgram-realtime.ts`, `src/providers/speech-to-text/google-gemini-live-stream.ts`, `src/providers/speech-to-text/openai-whisper-stream.ts`, `src/providers/speech-to-text/xai-realtime.ts` | Web/Electron dictation client via gateway WS proxy |
613
+ | **Live voice channel** | Assistant process (gateway-authenticated WebSocket) | Configured STT provider (via `services.stt`) | `src/runtime/http-server.ts`, `src/live-voice/live-voice-session-manager.ts`, `src/live-voice/live-voice-session.ts`, `src/providers/speech-to-text/resolve.ts`, streaming provider adapters | Web/Electron live voice client via `/v1/live-voice` |
614
+ | **Client service-first** | Web/Electron via gateway → daemon | Configured STT provider (via `services.stt`) | `src/runtime/routes/stt-routes.ts` | Web/Electron dictation and voice clients |
616
615
 
617
616
  **Telephony boundary (hybrid routing):**
618
617
 
@@ -679,16 +678,14 @@ Two provider adapters are supported, each implementing the `StreamingTranscriber
679
678
 
680
679
  **Session lifecycle (client side):**
681
680
 
682
- - `STTStreamingClient` (`clients/shared/Network/STTStreamingClient.swift`) manages the WebSocket session using `URLSessionWebSocketTask`. It builds the gateway WebSocket URL via `GatewayHTTPClient.buildWebSocketRequest(path: "stt/stream", params:)`.
683
- - `STTProviderRegistry` (`clients/shared/Utilities/STTProviderRegistry.swift`) exposes `isStreamingAvailable` (checks the configured provider's `conversationStreamingMode` from the `GET /v1/stt/providers` API) and `isServiceConfigured` (checks whether any STT provider is set).
684
- - macOS: `VoiceInputManager.startStreamingSession()` creates a fresh `STTStreamingClient` per recording session. Streaming partials take priority over `SFSpeechRecognizer` partials while the stream is active and healthy. When recording stops, if the stream delivered at least one `final` event (`streamingReceivedFinal`) and has not failed (`streamingFailed`), the streaming final text is used directly. Otherwise, the batch STT path (`STTClient.transcribe()`) provides the fallback.
681
+ The web client streaming dictation client lives at `clients/web/src/domains/chat/voice/dictation-stream.ts`; it opens the gateway WebSocket to `/v1/stt/stream`, parses `partial`/`final` frames, and reports failures so the caller can fall back to batch transcription. Before opening a session the client checks the configured provider's `conversationStreamingMode` from the `GET /v1/stt/providers` API. When the stream delivers at least one `final` event and has not failed, the streaming final text is used directly; otherwise the batch STT path (`clients/web/src/domains/chat/voice/stt-api.ts`) provides the fallback.
685
682
 
686
683
  **Fallback semantics:**
687
684
 
688
685
  The conversation streaming path degrades gracefully to the existing batch STT path:
689
686
 
690
- 1. **Unsupported provider** (a hypothetical provider with `conversationStreamingMode: "none"`): The client checks `STTProviderRegistry.isStreamingAvailable` before attempting a streaming session. When `false`, recording proceeds with the batch-only flow (no WebSocket is opened). On the daemon side, if a streaming session is somehow opened for an unsupported provider, the session sends an `error` event followed by `closed` and closes the socket with code 1000.
691
- 2. **Connection failure** (network error, gateway down, auth failure): The `STTStreamingClient` reports an `STTStreamFailure` to the client's `onFailure` callback. macOS sets `streamingFailed = true` and falls through to batch STT resolution when recording stops.
687
+ 1. **Unsupported provider** (a hypothetical provider with `conversationStreamingMode: "none"`): The client checks streaming availability (the configured provider's `conversationStreamingMode` from `GET /v1/stt/providers`) before attempting a streaming session. When unavailable, recording proceeds with the batch-only flow (no WebSocket is opened). On the daemon side, if a streaming session is somehow opened for an unsupported provider, the session sends an `error` event followed by `closed` and closes the socket with code 1000.
688
+ 2. **Connection failure** (network error, gateway down, auth failure): The streaming client reports the failure to its caller, which marks the stream as failed and falls through to batch STT resolution when recording stops.
692
689
  3. **Mid-session provider error** (provider WebSocket disconnect, timeout, rate limit): The daemon session emits an `error` event (with a normalized `SttErrorCategory`) followed by `closed`. The client marks the stream as failed and defers to batch STT.
693
690
  4. **Missing credentials**: `resolveStreamingTranscriber()` returns `null` when the API key is not configured. The session sends an `error`+`closed` pair and the client falls back to batch.
694
691
 
@@ -713,9 +710,8 @@ The conversation streaming path degrades gracefully to the existing batch STT pa
713
710
  | `src/providers/speech-to-text/resolve.ts` | `resolveStreamingTranscriber()`: credential-aware factory for streaming adapters; `resolveConversationStreamingSttCapability()`: capability validator |
714
711
  | `src/runtime/http-server.ts` | Runtime WebSocket upgrade handler for `/v1/stt/stream`, session registry (`activeSttStreamSessions`), graceful shutdown |
715
712
  | `gateway/src/http/routes/stt-stream-websocket.ts` | Gateway WebSocket proxy: authenticates client, opens upstream WS to daemon with service token |
716
- | `clients/shared/Network/STTStreamingClient.swift` | Shared Swift WebSocket client: `URLSessionWebSocketTask`-based, event parsing, failure reporting |
717
- | `clients/shared/Utilities/STTProviderRegistry.swift` | Client-side provider catalog: `isStreamingAvailable`, `conversationStreamingMode` per provider |
718
- | `clients/macos/.../VoiceInputManager.swift` | macOS integration: `startStreamingSession()`, streaming/batch priority, fallback on failure |
713
+ | `clients/web/src/domains/chat/voice/dictation-stream.ts` | Web streaming dictation client: WebSocket session, event parsing, failure reporting |
714
+ | `clients/web/src/domains/chat/voice/voice-recording-store.ts` | Web voice recording state: streaming/batch priority, fallback on failure |
719
715
 
720
716
  **Live voice channel boundary:**
721
717
 
@@ -742,32 +738,20 @@ V1 is local/gateway-scoped. Managed/cloud WebSocket proxy support, cross-region
742
738
 
743
739
  **Client service-first boundary:**
744
740
 
745
- All product-facing dictation and voice-streaming paths on macOS use a service-first STT strategy. Clients record audio, encode it to WAV via `AudioWavEncoder` (shared utility in `clients/shared/Utilities/AudioWavEncoder.swift`), and POST it through the gateway to the daemon's `POST /v1/stt/transcribe` endpoint via `STTClient` (`clients/shared/Network/STTClient.swift`). The daemon resolves the configured STT provider through `resolveBatchTranscriber()` and returns the transcribed text.
741
+ All product-facing dictation and voice-streaming paths use a service-first STT strategy. Clients record audio, encode it to WAV, and POST it through the gateway to the daemon's `POST /v1/stt/transcribe` endpoint. The daemon resolves the configured STT provider through `resolveBatchTranscriber()` and returns the transcribed text.
746
742
 
747
- - `STTClient` conforms to `STTClientProtocol` and returns a typed `STTResult` enum (`success`, `notConfigured`, `serviceUnavailable`, `error`). Callers pattern-match on the result to deterministically trigger native fallback.
743
+ - The client receives a typed result distinguishing success from `notConfigured`, `serviceUnavailable`, and `error`, so callers can deterministically trigger their fallback path.
748
744
  - The gateway proxies the request via assistant-scoped path rewriting: `/v1/assistants/:id/stt/transcribe` is rewritten to `/v1/stt/transcribe` on the daemon.
749
745
  - `stt-routes.ts` (`src/runtime/routes/stt-routes.ts`) defines the HTTP endpoint, validates the audio payload, and delegates to `resolveBatchTranscriber()`.
750
746
 
751
- Product-facing flows using service-first STT:
752
-
753
- | Flow | Client | Entry point |
754
- | ----------------------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
755
- | **Push-to-talk dictation** | macOS | `VoiceInputManager.resolveTranscription()` — encodes accumulated PCM buffers to WAV, calls `sttClient.transcribe()`, falls back to native text on failure |
756
- | **Conversation chat capture** | macOS | `VoiceInputManager.handleFinalTranscription()` — prefers streaming final when available; falls back to batch `sttClient.transcribe()` when streaming was not used, failed, or produced no finals |
757
- | **Voice mode (streaming)** | macOS | `OpenAIVoiceService.stopRecordingAndGetTranscription()` — encodes per-turn PCM to WAV, calls `sttClient.transcribe()` for turn-final transcript resolution, falls back to SFSpeechRecognizer result |
758
-
759
- **Client-native fallback boundary:**
760
-
761
- Apple-native on-device recognition via `SFSpeechRecognizer` serves two roles in all three product-facing flows above: (1) it provides low-latency partial transcriptions for real-time display during recording, and (2) it provides the fallback final transcription when the STT service is unconfigured (HTTP 503), temporarily unavailable (HTTP 5xx), or returns an empty result. The `SpeechRecognizerAdapter` protocols on each platform abstract Apple Speech for **testability and dependency injection**.
762
-
763
- The macOS `SpeechRecognizerAdapter` protocol in `clients/macos/vellum-assistant/Features/Voice/SpeechRecognizerAdapter.swift` abstracts `SFSpeechRecognizer` static APIs and instance creation. `AppleSpeechRecognizerAdapter` is the production implementation. `OpenAIVoiceService` and `VoiceInputManager` consume the adapter via dependency injection. **Note:** The protocol leaks Apple Speech types through its surface — `authorizationStatus()` returns `SFSpeechRecognizerAuthorizationStatus` and `makeRecognizer(locale:)` returns `SFSpeechRecognizer?` directly. This means callers depend on the Speech framework at compile time.
747
+ The web client implements these flows in `clients/web/src/domains/chat/voice/`: `stt-api.ts` (batch transcribe), `dictation-stream.ts` (streaming dictation), and `voice-recording-store.ts` (recording state, streaming/batch priority, fallback). Push-to-talk dictation and conversation chat capture prefer a streaming final when available and fall back to batch transcription when streaming was not used, failed, or produced no finals.
764
748
 
765
749
  **Cross-boundary notes:**
766
750
 
767
751
  - The `services.stt` config block is the single source of truth for STT provider selection across the daemon batch boundary, the conversation streaming boundary, the client service-first boundary, and the telephony boundary. The batch and streaming resolvers (`resolveBatchTranscriber()`, `resolveStreamingTranscriber()`) both read from `services.stt.provider` and resolve credentials through the same catalog; the telephony boundary uses `resolveTelephonySttRouting()` to determine the Twilio integration strategy. The daemon provider catalog (`src/providers/speech-to-text/provider-catalog.ts`) is the authoritative registry of supported providers. Native clients fetch display metadata via `GET /v1/stt/providers`.
768
752
  - Conversation streaming does not replace the client service-first batch path. When streaming is available, it runs concurrently during recording and provides real-time partials and finals. The batch path remains the fallback for providers that do not support streaming, when streaming fails mid-session, or when streaming produces no final transcript.
769
753
  - Credential mapping is catalog-driven: `provider-secret-catalog.ts` derives STT API-key provider names from the daemon catalog via `listCredentialProviderNames()`, deduplicating against the LLM/search provider list. Adding a provider to the catalog automatically includes its credential name in `API_KEY_PROVIDERS`.
770
- - Terminology: "STT" and "transcription" refer to the same operation (converting audio to text). "Speech recognition" is used in client-native contexts where Apple's Speech framework terminology is canonical. All three terms map to the same conceptual operation.
754
+ - Terminology: "STT", "transcription", and "speech recognition" all refer to the same operation (converting audio to text).
771
755
  - **Onboarding**: For a step-by-step guide to adding a new STT provider, see `docs/stt-provider-onboarding.md`.
772
756
 
773
757
  ### On-Demand Home Content Generation
@@ -227,14 +227,14 @@ sequenceDiagram
227
227
  participant Route as credentials prompt route
228
228
  participant Prompter as SecretPrompter
229
229
  participant HTTP as HTTP Transport
230
- participant UI as SecretPromptManager (Swift)
230
+ participant UI as Secret prompt UI (client)
231
231
  participant Store as Credential Store (CES / encrypted file)
232
232
 
233
233
  Model->>Route: assistant credentials prompt --service --field --label
234
234
  Route->>Prompter: requestSecretStandalone(service, field, label, ...)
235
235
  Prompter->>HTTP: secret_request {requestId, service, field, label, allowOneTimeSend}
236
- HTTP->>UI: Show SecretPromptView (floating panel)
237
- UI->>UI: User enters value in SecureField
236
+ HTTP->>UI: Show secret prompt
237
+ UI->>UI: User enters value in a masked field
238
238
  alt Store (default)
239
239
  UI->>HTTP: secret_response {requestId, value, delivery: "store"}
240
240
  HTTP->>Prompter: resolve(value, "store")
@@ -298,7 +298,7 @@ The `allowOneTimeSend` config gate (default: `false`) enables a secondary "Send
298
298
  | `assistant/src/security/secret-scanner.ts` | Prefix + shape-based secret regex detection (used by display-time `redactSecrets`) |
299
299
  | `assistant/src/security/secret-ingress.ts` | Prefix-only ingress check on user messages |
300
300
  | `assistant/src/util/log-redact.ts` | Pino log serializers — prefix-based redaction for logs |
301
- | `clients/macos/.../SecretPromptManager.swift` | Floating panel UI for secure credential entry |
301
+ | `clients/web/src/domains/chat/components/secret-prompt-card.tsx` | UI for secure credential entry |
302
302
 
303
303
  ---
304
304
 
@@ -72,13 +72,11 @@ Native clients fetch this metadata at launch via `GET /v1/stt/providers`. No sep
72
72
  | `google-gemini` | `gemini` | shared |
73
73
  | `xai` | `xai` | exclusive |
74
74
 
75
- When the provider ID differs from the credential provider name (e.g. `google-gemini` maps to `gemini`), the key is **shared** with other services that use the same credential. The `sttKeyIsExclusive` / `sttKeyIsShared` helpers in the macOS settings layer derive this automatically from the catalog.
75
+ When the provider ID differs from the credential provider name (e.g. `google-gemini` maps to `gemini`), the key is **shared** with other services that use the same credential.
76
76
 
77
- ### macOS settings key behavior
77
+ ### Client settings key behavior
78
78
 
79
- **File:** `clients/macos/vellum-assistant/Features/Settings/SettingsStore.swift`
80
-
81
- The `sttKeyIsExclusive(for:)` / `sttKeyIsShared(for:)` helpers derive shared-vs-exclusive key behavior from the catalog automatically: if `apiKeyProviderName == id`, the key is exclusive; otherwise it is shared. No new conditionals are needed unless the provider has a non-standard key-ownership model.
79
+ Clients derive shared-vs-exclusive key behavior from the catalog automatically: if `apiKeyProviderName == id`, the key is exclusive; otherwise it is shared. No new conditionals are needed unless the provider has a non-standard key-ownership model. The web settings UI lives in `clients/web/src/domains/settings/ai/speech-to-text-card.tsx`.
82
80
 
83
81
  ## 7. Verify unified STT architecture
84
82
 
@@ -2,14 +2,13 @@
2
2
 
3
3
  This runbook covers the parts of the workflow engine that automated tests do not:
4
4
  real provider calls, real journaled resume across a restart, capability
5
- containment under a live model, flag-off inertness, and persona-leaf voice. Unit
5
+ containment under a live model, and persona-leaf voice. Unit
6
6
  and integration coverage lives under `assistant/src/workflows/*.test.ts` and
7
7
  `assistant/src/__tests__/`; this is the human-in-the-loop pass that runs **last**,
8
8
  on throwaway instances first, and only touches a real inbox or a production
9
9
  instance at the very end.
10
10
 
11
- Work through the steps in order. The `workflows` flag is **off by default**, so
12
- every step that exercises the engine first enables it on a throwaway instance.
11
+ Work through the steps in order, on throwaway instances first.
13
12
 
14
13
  > **Safety rule:** Nothing in this runbook touches a real inbox, a real workspace,
15
14
  > or a production instance until every throwaway-instance step has passed. The
@@ -17,7 +16,7 @@ every step that exercises the engine first enables it on a throwaway instance.
17
16
 
18
17
  ---
19
18
 
20
- ## 1. Hatch a throwaway instance and enable the flag
19
+ ## 1. Hatch a throwaway instance
21
20
 
22
21
  Hatch a disposable Docker instance built from local source:
23
22
 
@@ -26,25 +25,14 @@ vellum hatch --remote docker --source .
26
25
  ```
27
26
 
28
27
  Note the instance name it prints (e.g. `vellum-<adjective>-<animal>`). Use it as
29
- `--assistant <name>` for everything below, or set it active.
30
-
31
- Enable the `workflows` flag via that instance's feature-flag override file. The
32
- override lives in the instance's `protected/feature-flags.json` (overrides here
33
- win over the registry default — see the feature-flag-overrides gotcha):
34
-
35
- ```jsonc
36
- // .../<instance>/protected/feature-flags.json
37
- { "workflows": true }
38
- ```
39
-
40
- Restart the instance so it re-reads the override, then confirm it is up:
28
+ `--assistant <name>` for everything below, or set it active. Confirm it is up:
41
29
 
42
30
  ```
43
31
  vellum ps
44
32
  ```
45
33
 
46
- Sanity-check that the surface is now live: `vellum workflows runs --assistant <name>`
47
- should return an (empty) table rather than a 404.
34
+ Sanity-check that the workflow surface is live: `vellum workflows runs --assistant <name>`
35
+ should return an (empty) table.
48
36
 
49
37
  ---
50
38
 
@@ -168,27 +156,9 @@ or a send tool, with `capabilities.tools` left empty). Run it and confirm:
168
156
 
169
157
  ---
170
158
 
171
- ## 6. Flag-off inertness
172
-
173
- On a **default** instance (no `workflows` override, or set back to `false` and
174
- restart), confirm the whole surface is gone:
175
-
176
- - `run_workflow` and `manage_workflows` are **absent** from the tool set (the
177
- assistant cannot call them).
178
- - The routes 404:
179
-
180
- ```
181
- vellum workflows runs --assistant <default-instance> # request fails (404)
182
- ```
183
-
184
- - A scheduler `workflow`-mode job is **rejected** (the engine gate throws before
185
- any run is launched).
186
-
187
- ---
188
-
189
- ## 7. Real-data smoke (throwaway TEST account)
159
+ ## 6. Real-data smoke (throwaway TEST account)
190
160
 
191
- Only after steps 1–6 pass: run the full path against a **throwaway TEST Google or
161
+ Only after steps 1–5 pass: run the full path against a **throwaway TEST Google or
192
162
  Slack account** with about a dozen messages — never a real account. Drive the
193
163
  end-to-end flow:
194
164
 
@@ -203,17 +173,16 @@ real content, and every side effect was one the manifest declared.
203
173
 
204
174
  ---
205
175
 
206
- ## 8. Production / persona instance — LAST
176
+ ## 7. Production / persona instance — LAST
207
177
 
208
- Flip the `workflows` flag on a real (e.g. persona) instance **only after** the
209
- throwaway instances above pass, and only after the instance is on current code:
178
+ Run on a real (e.g. persona) instance **only after** the throwaway instances
179
+ above pass, and only after the instance is on current code:
210
180
 
211
181
  1. `git pull` and **restart** the instance so it runs the merged code (a restart
212
182
  alone re-runs stale code — see the deploy gotcha).
213
- 2. Enable the flag and restart.
214
- 3. Start with the **smallest real slice** — a few items, dry-run actions where the
183
+ 2. Start with the **smallest real slice** — a few items, dry-run actions where the
215
184
  tool supports it — before any larger or side-effecting run.
216
- 4. **Human-eval the persona-leaf voice**: read a persona leaf's output and confirm
185
+ 3. **Human-eval the persona-leaf voice**: read a persona leaf's output and confirm
217
186
  it reads as the assistant, not as a generic worker.
218
187
 
219
188
  Stop and reassess if any run's `agentsSpawned` approaches `maxAgentsPerRun`, if
package/docs/workflows.md CHANGED
@@ -8,11 +8,9 @@ from each of a hundred documents, draft-then-verify a batch — and you want the
8
8
  results orchestrated deterministically and reported back when the whole run
9
9
  finishes.
10
10
 
11
- Workflows are gated behind the `workflows` feature flag (default **off**). The
12
- `run_workflow` / `manage_workflows` tools are served by the flag-gated `workflows`
11
+ The `run_workflow` / `manage_workflows` tools are served by the `workflows`
13
12
  bundled skill rather than as always-on tools — load it with `skill_load` and invoke
14
- its tools via `skill_execute`. When the flag is off, the skill is absent, the
15
- management routes 404, and the scheduler rejects `workflow`-mode jobs.
13
+ its tools via `skill_execute`.
16
14
 
17
15
  - Engine code: `assistant/src/workflows/`
18
16
  - Skill (tool surface): `assistant/src/config/bundled-skills/workflows/`
@@ -437,7 +435,7 @@ Reached via the skill (`skill_load` then `skill_execute`), not as always-on tool
437
435
  `list_profiles` returns `{ profiles, activeProfile }` — the defined LLM profile
438
436
  names plus the workspace active profile, used to pick a valid leaf `profile`.
439
437
 
440
- ### Routes (read/abort/resume, all 404 when the flag is off)
438
+ ### Routes (read/abort/resume)
441
439
 
442
440
  | Method | Path | Purpose |
443
441
  | ------ | ------------------------------- | ---------------------------------------------------------------------------------------------------- |
@@ -25,11 +25,8 @@ function portBlock(base: number): PortMap {
25
25
  }
26
26
 
27
27
  /**
28
- * Built-in environment definitions and the TS-side source of truth for the
29
- * set of known environment names. The Swift client mirrors this list in
30
- * `clients/macos/vellum-assistant/App/VellumEnvironment.swift`; since Swift
31
- * can't import TypeScript, drift between the two is caught at test time by
32
- * `cli/src/__tests__/env-drift.test.ts`.
28
+ * Built-in environment definitions and the source of truth for the
29
+ * set of known environment names.
33
30
  *
34
31
  * Custom environments via a user config file are a future phase — see the
35
32
  * "Coexisting environments" design doc. Until then, a call site that needs a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.9.0-dev.202606162337.b5a0f93",
3
+ "version": "0.9.0-dev.202606170116.ccd16ef",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -60,10 +60,7 @@ const CANONICAL_KEY_RE = /^[a-z0-9][a-z0-9-]*$/;
60
60
  * Keep this list minimal — only files that genuinely need to reference
61
61
  * the legacy format for backward compatibility.
62
62
  */
63
- const LEGACY_KEY_ALLOWLIST = new Set([
64
- // macOS client: fallback reads from legacy config section
65
- "clients/macos/vellum-assistant/Features/Settings/SettingsAccountTab.swift",
66
- ]);
63
+ const LEGACY_KEY_ALLOWLIST = new Set<string>([]);
67
64
 
68
65
  function isTestFile(filePath: string): boolean {
69
66
  return (
@@ -58,8 +58,6 @@ function loadRegistry(): Registry {
58
58
  const LEGACY_KEY_ALLOWLIST = new Set([
59
59
  // Type definitions documenting the legacy format
60
60
  "assistant/src/config/types.ts",
61
- // macOS client: fallback reads from legacy config section
62
- "clients/macos/vellum-assistant/Features/Settings/SettingsAccountTab.swift",
63
61
  ]);
64
62
 
65
63
  function isTestFile(filePath: string): boolean {
@@ -5,7 +5,7 @@ import type {
5
5
  UiSurfaceShowDynamicPage,
6
6
  } from "../daemon/message-protocol.js";
7
7
  import { INTERACTIVE_SURFACE_TYPES } from "../daemon/message-protocol.js";
8
- import { uiShowTool } from "../tools/ui-surface/definitions.js";
8
+ import { uiShowTool, uiUpdateTool } from "../tools/ui-surface/definitions.js";
9
9
 
10
10
  // ---------------------------------------------------------------------------
11
11
  // DynamicPageSurfaceData shape
@@ -300,6 +300,106 @@ describe("ui_show empty card guard", () => {
300
300
  });
301
301
  });
302
302
 
303
+ describe("ui_update empty payload guard", () => {
304
+ function makeCtx(onProxy: () => void) {
305
+ return {
306
+ conversationId: "conversation-123",
307
+ workingDir: "/tmp",
308
+ trustClass: "guardian" as const,
309
+ proxyToolResolver: async () => {
310
+ onProxy();
311
+ return { content: "Surface updated", isError: false };
312
+ },
313
+ };
314
+ }
315
+
316
+ test("rejects an empty data object and does not proxy", async () => {
317
+ let proxied = false;
318
+ const result = await uiUpdateTool.execute(
319
+ { surface_id: "surf-1", data: {} },
320
+ makeCtx(() => {
321
+ proxied = true;
322
+ }),
323
+ );
324
+
325
+ expect(result.isError).toBe(true);
326
+ expect(result.content).toContain("empty `data` payload");
327
+ expect(proxied).toBe(false);
328
+ });
329
+
330
+ test("rejects missing data and does not proxy", async () => {
331
+ let proxied = false;
332
+ const result = await uiUpdateTool.execute(
333
+ { surface_id: "surf-1" },
334
+ makeCtx(() => {
335
+ proxied = true;
336
+ }),
337
+ );
338
+
339
+ expect(result.isError).toBe(true);
340
+ expect(proxied).toBe(false);
341
+ });
342
+
343
+ test("rejects data containing only nested empty objects", async () => {
344
+ let proxied = false;
345
+ const result = await uiUpdateTool.execute(
346
+ { surface_id: "surf-1", data: { templateData: {} } },
347
+ makeCtx(() => {
348
+ proxied = true;
349
+ }),
350
+ );
351
+
352
+ expect(result.isError).toBe(true);
353
+ expect(proxied).toBe(false);
354
+ });
355
+
356
+ test("rejects a task_progress update with an empty steps array", async () => {
357
+ let proxied = false;
358
+ const result = await uiUpdateTool.execute(
359
+ { surface_id: "surf-1", data: { templateData: { steps: [] } } },
360
+ makeCtx(() => {
361
+ proxied = true;
362
+ }),
363
+ );
364
+
365
+ expect(result.isError).toBe(true);
366
+ expect(proxied).toBe(false);
367
+ });
368
+
369
+ test("allows a task_progress step update", async () => {
370
+ let proxied = false;
371
+ const result = await uiUpdateTool.execute(
372
+ {
373
+ surface_id: "surf-1",
374
+ data: {
375
+ templateData: {
376
+ steps: [{ label: "Read memo", status: "completed" }],
377
+ },
378
+ },
379
+ },
380
+ makeCtx(() => {
381
+ proxied = true;
382
+ }),
383
+ );
384
+
385
+ expect(result.isError).toBe(false);
386
+ expect(proxied).toBe(true);
387
+ });
388
+
389
+ test("allows a status-only update without resending steps", async () => {
390
+ let proxied = false;
391
+ const result = await uiUpdateTool.execute(
392
+ { surface_id: "surf-1", data: { templateData: { status: "completed" } } },
393
+ makeCtx(() => {
394
+ proxied = true;
395
+ }),
396
+ );
397
+
398
+ expect(result.isError).toBe(false);
399
+ expect(proxied).toBe(true);
400
+ });
401
+ });
402
+
303
403
  // ---------------------------------------------------------------------------
304
404
  // task_progress ui_show appends the update hint to its return value
305
405
  // ---------------------------------------------------------------------------
@@ -23,11 +23,7 @@ const ALLOWLIST = new Set([
23
23
 
24
24
  // --- Intentional local daemon-control paths ---
25
25
  "assistant/src/cli/commands/conversations.ts", // CLI wipe talks to runtime directly
26
- "clients/shared/Network/DaemonClient.swift",
27
- "clients/shared/App/Auth/PlatformOAuthService.swift", // comment explaining runtimeUrl vs platformUrl
28
- "clients/macos/vellum-assistant/App/AppDelegate.swift",
29
- "clients/macos/vellum-assistant/Features/Settings/SettingsConnectTab.swift",
30
- "apps/macos/src/main/bundle-flow.ts", // Electron main calls the local gateway (gatewayPort) with a Guardian token to scan bundles
26
+ "clients/macos/src/main/bundle-flow.ts", // Electron main calls the local gateway (gatewayPort) with a Guardian token to scan bundles
31
27
  ".claude/skills/update/SKILL.md", // daemon health check script
32
28
 
33
29
  // --- Test fixtures that poll the daemon directly (gateway may require auth) ---
@@ -87,12 +83,12 @@ function isGatewayInternal(filePath: string): boolean {
87
83
 
88
84
  /** Additional files allowed for the interpolated-port check only (use gateway port, not runtime). */
89
85
  const INTERPOLATED_PORT_ALLOWLIST = new Set([
90
- "apps/macos/src/main/bundle-flow.ts",
86
+ "clients/macos/src/main/bundle-flow.ts",
91
87
  // Electron main bridges the host proxy to a local assistant's gateway
92
88
  // (gatewayPort) over loopback with a Guardian-minted gateway token — same
93
89
  // class as bundle-flow.ts. Added with #34049 (host proxy for cloud/managed
94
90
  // assistants); the allowlist entry was missed there.
95
- "apps/macos/src/main/host-proxy-router.ts",
91
+ "clients/macos/src/main/host-proxy-router.ts",
96
92
  ]);
97
93
 
98
94
  /** Shared violation filter: exempt test files, gateway internals, and allowlisted paths. */
@@ -54,9 +54,7 @@ describe("LLM call-site catalog", () => {
54
54
 
55
55
  test("CALL_SITE_DEFAULTS covers every LLMCallSite enum value", () => {
56
56
  const defaultIds = new Set(Object.keys(CALL_SITE_DEFAULTS));
57
- const missing = LLMCallSiteEnum.options.filter(
58
- (id) => !defaultIds.has(id),
59
- );
57
+ const missing = LLMCallSiteEnum.options.filter((id) => !defaultIds.has(id));
60
58
  expect(missing).toEqual([]);
61
59
  });
62
60
 
@@ -68,11 +66,12 @@ describe("LLM call-site catalog", () => {
68
66
  expect(extra).toEqual([]);
69
67
  });
70
68
 
71
- test("every CALL_SITE_DEFAULTS entry has a profile field", () => {
69
+ test("every CALL_SITE_DEFAULTS profile, when present, is a non-empty string", () => {
72
70
  for (const [, config] of Object.entries(CALL_SITE_DEFAULTS)) {
73
- expect(config.profile).toBeDefined();
71
+ // A call site may omit `profile` to inherit the workspace default config.
72
+ if (config.profile === undefined) continue;
74
73
  expect(typeof config.profile).toBe("string");
75
- expect(config.profile!.length).toBeGreaterThan(0);
74
+ expect(config.profile.length).toBeGreaterThan(0);
76
75
  }
77
76
  });
78
77
  });
@@ -670,30 +670,6 @@ describe("resolveCallSiteConfig", () => {
670
670
  expect(resolved.effort).toBe("medium");
671
671
  });
672
672
 
673
- test("workflowLeaf defaults to the cost-optimized profile", () => {
674
- const llm = LLMSchema.parse({
675
- default: fullDefault,
676
- profiles: {
677
- balanced: {
678
- model: "claude-sonnet-4-7",
679
- effort: "medium",
680
- },
681
- "cost-optimized": {
682
- model: "claude-haiku-4-5-20251001",
683
- effort: "low",
684
- },
685
- },
686
- });
687
-
688
- expect(resolveDefaultProfileKey("workflowLeaf", llm)).toBe(
689
- "cost-optimized",
690
- );
691
- const resolved = resolveCallSiteConfig("workflowLeaf", llm);
692
- expect(resolved.model).toBe("claude-haiku-4-5-20251001");
693
- expect(resolved.effort).toBe("low");
694
- expect(resolved.thinking.enabled).toBe(false);
695
- });
696
-
697
673
  test("explicit callSites config overrides CALL_SITE_DEFAULTS", () => {
698
674
  const llm = LLMSchema.parse({
699
675
  default: fullDefault,
@@ -1437,3 +1413,52 @@ describe("resolveCallSiteConfig logitBias provenance", () => {
1437
1413
  expect(resolveCallSiteConfig("mainAgent", llm).logitBias).toBeUndefined();
1438
1414
  });
1439
1415
  });
1416
+
1417
+ describe("resolveCallSiteConfig — workflowLeaf default", () => {
1418
+ test("inherits the workspace default config rather than pinning cost-optimized", () => {
1419
+ const llm = LLMSchema.parse({
1420
+ default: fullDefault,
1421
+ profiles: {
1422
+ "cost-optimized": {
1423
+ provider: "anthropic",
1424
+ model: "claude-haiku-4-5-20251001",
1425
+ },
1426
+ },
1427
+ });
1428
+ const resolved = resolveCallSiteConfig("workflowLeaf", llm);
1429
+ // No pinned profile → the model comes from llm.default, NOT the
1430
+ // `cost-optimized` profile (which is uncredentialed on a BYOK install).
1431
+ expect(resolved.model).toBe("claude-opus-4-7");
1432
+ // Call-site tuning still applies.
1433
+ expect(resolved.effort).toBe("low");
1434
+ expect(resolved.thinking?.enabled).toBe(false);
1435
+ // The call site has no implicit default profile key.
1436
+ expect(resolveDefaultProfileKey("workflowLeaf", llm)).toBeUndefined();
1437
+ });
1438
+
1439
+ test("honors an explicit workflowLeaf call-site override", () => {
1440
+ const llm = LLMSchema.parse({
1441
+ default: fullDefault,
1442
+ profiles: {
1443
+ cheap: { provider: "anthropic", model: "claude-haiku-4-5-20251001" },
1444
+ },
1445
+ callSites: { workflowLeaf: { profile: "cheap" } },
1446
+ });
1447
+ expect(resolveCallSiteConfig("workflowLeaf", llm).model).toBe(
1448
+ "claude-haiku-4-5-20251001",
1449
+ );
1450
+ });
1451
+
1452
+ test("honors a per-call override profile (an explicit per-leaf profile)", () => {
1453
+ const llm = LLMSchema.parse({
1454
+ default: fullDefault,
1455
+ profiles: {
1456
+ fancy: { provider: "anthropic", model: "claude-sonnet-4-7" },
1457
+ },
1458
+ });
1459
+ expect(
1460
+ resolveCallSiteConfig("workflowLeaf", llm, { overrideProfile: "fancy" })
1461
+ .model,
1462
+ ).toBe("claude-sonnet-4-7");
1463
+ });
1464
+ });
@@ -3,17 +3,15 @@ import { describe, expect, test } from "bun:test";
3
3
  import { PROVIDER_SEED_DATA } from "../oauth/seed-providers.js";
4
4
 
5
5
  /**
6
- * Allowed CDN prefixes for ``logoUrl``. Mirrors the source registry in
7
- * ``clients/shared/Resources/integration-logos-manifest.json``:
6
+ * Allowed CDN prefixes for the ``logoUrl`` field on ``PROVIDER_SEED_DATA``
7
+ * (``assistant/src/oauth/seed-providers.ts``):
8
8
  *
9
9
  * - Simple Icons (CC0) is the default for most providers.
10
10
  * - thesvg via jsDelivr is the documented fallback for brands Simple Icons
11
11
  * doesn't host (e.g. Salesforce, which Simple Icons removed for
12
- * trademark reasons). Same source is already used for the bundled PDFs
13
- * of figma/github/gmail/linear/notion/outlook/slack.
12
+ * trademark reasons).
14
13
  *
15
- * Adding another CDN should be a deliberate choice — extend this list
16
- * and update the manifest in tandem.
14
+ * Adding another CDN should be a deliberate choice — extend this list.
17
15
  */
18
16
  const ALLOWED_LOGO_URL_PREFIXES = [
19
17
  "https://cdn.simpleicons.org/",