@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.
- package/ARCHITECTURE.md +12 -28
- package/docs/architecture/security.md +4 -4
- package/docs/stt-provider-onboarding.md +3 -5
- package/docs/workflows-testing.md +13 -44
- package/docs/workflows.md +3 -5
- package/node_modules/@vellumai/environments/src/seeds.ts +2 -5
- package/package.json +1 -1
- package/src/__tests__/assistant-feature-flag-guard.test.ts +1 -4
- package/src/__tests__/assistant-feature-flag-guardrails.test.ts +0 -2
- package/src/__tests__/dynamic-page-surface.test.ts +101 -1
- package/src/__tests__/gateway-only-guard.test.ts +3 -7
- package/src/__tests__/llm-callsite-catalog.test.ts +5 -6
- package/src/__tests__/llm-resolver.test.ts +49 -24
- package/src/__tests__/oauth-provider-seed-logos.test.ts +4 -6
- package/src/__tests__/plugin-api-model-profiles.test.ts +178 -0
- package/src/__tests__/schedule-routes-workflow-validation.test.ts +1 -10
- package/src/__tests__/schedule-routes.test.ts +0 -30
- package/src/__tests__/schedule-tools.test.ts +2 -18
- package/src/__tests__/skill-execute-input.test.ts +46 -1
- package/src/__tests__/skill-runtime-path.test.ts +2 -3
- package/src/api/README.md +6 -6
- package/src/api/responses/conversation-message.ts +2 -4
- package/src/api/responses/home.ts +0 -4
- package/src/calls/voice-session-bridge.ts +2 -2
- package/src/config/bundled-skills/workflows/SKILL.md +0 -1
- package/src/config/call-site-defaults.ts +12 -2
- package/src/config/feature-flag-registry.json +0 -16
- package/src/config/profile-order.ts +28 -0
- package/src/config/schemas/elevenlabs.ts +0 -1
- package/src/daemon/conversation-slash.ts +2 -23
- package/src/daemon/conversation-tool-setup.ts +10 -2
- package/src/home/relationship-state.ts +2 -4
- package/src/memory/__tests__/memory-retrospective-job.test.ts +195 -401
- package/src/memory/bookmark-crud.ts +1 -2
- package/src/memory/memory-retrospective-constants.ts +4 -4
- package/src/memory/memory-retrospective-job.ts +19 -227
- package/src/oauth/AGENTS.md +5 -24
- package/src/plugin-api/index.ts +4 -0
- package/src/plugin-api/model-profiles.ts +33 -0
- package/src/plugin-api/types.ts +29 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +54 -2
- package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +107 -7
- package/src/runtime/routes/conversation-compaction-routes.ts +1 -1
- package/src/runtime/routes/conversation-routes.ts +2 -2
- package/src/runtime/routes/schedule-routes.ts +0 -22
- package/src/runtime/routes/workflow-routes.test.ts +4 -43
- package/src/runtime/routes/workflow-routes.ts +0 -28
- package/src/tools/network/web-search-error.ts +1 -1
- package/src/tools/schedule/create.ts +3 -9
- package/src/tools/schedule/update.ts +2 -10
- package/src/tools/skills/execute.ts +34 -0
- package/src/tools/ui-surface/definitions.ts +39 -1
- package/src/tools/workflows/run-workflow.test.ts +8 -18
- package/src/util/platform.ts +2 -2
- package/src/workflows/capabilities.ts +2 -3
- package/src/workflows/run-manager.test.ts +0 -25
- 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` |
|
|
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 |
|
|
614
|
-
| **Client service-first** |
|
|
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
|
-
|
|
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 `
|
|
691
|
-
2. **Connection failure** (network error, gateway down, auth failure): The
|
|
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/
|
|
717
|
-
| `clients/
|
|
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
|
|
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
|
-
-
|
|
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
|
-
|
|
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 "
|
|
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
|
|
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
|
|
237
|
-
UI->>UI: User enters value in
|
|
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/
|
|
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.
|
|
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
|
-
###
|
|
77
|
+
### Client settings key behavior
|
|
78
78
|
|
|
79
|
-
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
47
|
-
should return an (empty) table
|
|
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.
|
|
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–
|
|
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
|
-
##
|
|
176
|
+
## 7. Production / persona instance — LAST
|
|
207
177
|
|
|
208
|
-
|
|
209
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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`.
|
|
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
|
|
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
|
|
29
|
-
* set of known environment names.
|
|
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
|
@@ -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/
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
7
|
-
* ``
|
|
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).
|
|
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/",
|