@vellumai/assistant 0.11.2-dev.202608051734.355dff7 → 0.11.2-dev.202608051933.deffc0f
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 +4 -2
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/ingress.ts +10 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/ingress.ts +10 -0
- package/node_modules/@vellumai/service-contracts/src/ingress.ts +10 -0
- package/openapi.yaml +22 -0
- package/package.json +1 -1
- package/src/__tests__/agent-loop-override-profile.test.ts +38 -48
- package/src/__tests__/assistant-feature-flags-integration.test.ts +0 -10
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +1 -0
- package/src/__tests__/conversation-tool-setup-attribution.test.ts +61 -0
- package/src/__tests__/cu-unified-flow.test.ts +2 -6
- package/src/__tests__/file-ops-service.test.ts +75 -4
- package/src/__tests__/host-bash-proxy.test.ts +10 -5
- package/src/__tests__/host-browser-proxy.test.ts +2 -2
- package/src/__tests__/host-cu-proxy.test.ts +3 -7
- package/src/__tests__/host-file-proxy.test.ts +3 -7
- package/src/__tests__/host-file-read-tool.test.ts +5 -1
- package/src/__tests__/host-transfer-proxy-targeted.test.ts +6 -4
- package/src/__tests__/llm-resolver-override-or-default.test.ts +141 -0
- package/src/__tests__/media-stream-server-integration.test.ts +36 -0
- package/src/__tests__/stt-language-catalog-parity.test.ts +38 -0
- package/src/__tests__/subagent-tool-gate-mode.test.ts +1 -0
- package/src/__tests__/subagent-tools.test.ts +48 -92
- package/src/__tests__/tool-result-spool.test.ts +74 -18
- package/src/agent/loop.ts +6 -3
- package/src/calls/media-stream-server.ts +9 -1
- package/src/config/bundled-skills/settings/TOOLS.json +2 -2
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +1 -0
- package/src/config/bundled-skills/subagent/SKILL.md +3 -3
- package/src/config/bundled-skills/subagent/TOOLS.json +1 -1
- package/src/config/feature-flag-registry.json +0 -16
- package/src/config/llm-resolver.ts +41 -12
- package/src/config/schemas/stt.ts +10 -4
- package/src/context/post-turn-tool-result-truncation.ts +32 -16
- package/src/context/tool-result-spool.ts +32 -18
- package/src/daemon/conversation-tool-setup.ts +1 -1
- package/src/persistence/conversation-attention-store.ts +30 -10
- package/src/persistence/conversation-queries.ts +74 -1
- package/src/platform/managed-speech.test.ts +20 -0
- package/src/platform/managed-speech.ts +9 -0
- package/src/providers/speech-to-text/__tests__/resolve.test.ts +164 -6
- package/src/providers/speech-to-text/deepgram.ts +1 -0
- package/src/providers/speech-to-text/resolve.ts +80 -10
- package/src/providers/speech-to-text/vellum-managed.ts +7 -1
- package/src/providers/speech-to-text/vellum-speech-relay-connection.ts +3 -1
- package/src/runtime/auth/__tests__/guard-tests.test.ts +2 -0
- package/src/runtime/auth/__tests__/scopes.test.ts +20 -0
- package/src/runtime/auth/same-actor.ts +38 -7
- package/src/runtime/auth/scopes.ts +12 -0
- package/src/runtime/auth/types.ts +2 -0
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +279 -1
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +14 -10
- package/src/runtime/routes/conversation-list-routes.ts +25 -0
- package/src/runtime/routes/conversation-query-routes.ts +0 -50
- package/src/stt/daemon-batch-transcriber.ts +8 -6
- package/src/tools/filesystem/read.ts +2 -2
- package/src/tools/host-filesystem/read.ts +14 -6
- package/src/tools/shared/filesystem/file-ops-service.ts +36 -3
- package/src/tools/subagent/spawn.ts +26 -78
- package/src/tools/types.ts +5 -3
|
@@ -709,7 +709,7 @@ describe("HostTransferProxy — targetClientId", () => {
|
|
|
709
709
|
);
|
|
710
710
|
|
|
711
711
|
expect(result.isError).toBe(true);
|
|
712
|
-
expect(result.content).toContain("
|
|
712
|
+
expect(result.content).toContain("signed in as a different user");
|
|
713
713
|
expect(sentMessages).toHaveLength(0);
|
|
714
714
|
});
|
|
715
715
|
|
|
@@ -738,7 +738,7 @@ describe("HostTransferProxy — targetClientId", () => {
|
|
|
738
738
|
);
|
|
739
739
|
|
|
740
740
|
expect(result.isError).toBe(true);
|
|
741
|
-
expect(result.content).toContain("
|
|
741
|
+
expect(result.content).toContain("no authenticated actor");
|
|
742
742
|
expect(sentMessages).toHaveLength(0);
|
|
743
743
|
});
|
|
744
744
|
|
|
@@ -767,7 +767,9 @@ describe("HostTransferProxy — targetClientId", () => {
|
|
|
767
767
|
);
|
|
768
768
|
|
|
769
769
|
expect(result.isError).toBe(true);
|
|
770
|
-
expect(result.content).toContain(
|
|
770
|
+
expect(result.content).toContain(
|
|
771
|
+
"registered without an authenticated user",
|
|
772
|
+
);
|
|
771
773
|
expect(sentMessages).toHaveLength(0);
|
|
772
774
|
});
|
|
773
775
|
|
|
@@ -863,7 +865,7 @@ describe("HostTransferProxy — targetClientId", () => {
|
|
|
863
865
|
);
|
|
864
866
|
|
|
865
867
|
expect(result.isError).toBe(true);
|
|
866
|
-
expect(result.content).toContain("
|
|
868
|
+
expect(result.content).toContain("signed in as a different user");
|
|
867
869
|
expect(sentMessages).toHaveLength(0);
|
|
868
870
|
});
|
|
869
871
|
|
|
@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test";
|
|
|
2
2
|
|
|
3
3
|
import { CODE_DEFAULT_PROFILE_ENTRIES } from "../config/default-profile-catalog.js";
|
|
4
4
|
import {
|
|
5
|
+
composeCallSiteTweak,
|
|
5
6
|
type ResolutionFallbackReason,
|
|
6
7
|
resolveCallSiteConfig,
|
|
7
8
|
resolveDefaultProfileKey,
|
|
@@ -178,6 +179,146 @@ describe("fallback and completeness", () => {
|
|
|
178
179
|
});
|
|
179
180
|
});
|
|
180
181
|
|
|
182
|
+
describe("composeCallSiteTweak", () => {
|
|
183
|
+
// `CallSiteDefaultConfig` supports a shipped `model` (and the provider it
|
|
184
|
+
// implies). No entry sets one today, so these pin the invariant the merge
|
|
185
|
+
// depends on rather than waiting for the first entry that would break it.
|
|
186
|
+
test("a workspace entry owns selection: a shipped model/provider is dropped", () => {
|
|
187
|
+
const tweak = composeCallSiteTweak(
|
|
188
|
+
{ model: "shipped-model", provider: "openai", effort: "low" },
|
|
189
|
+
{ profile: "mine" },
|
|
190
|
+
);
|
|
191
|
+
expect("model" in tweak).toBe(false);
|
|
192
|
+
expect("provider" in tweak).toBe(false);
|
|
193
|
+
// The shipped tuning still layers in.
|
|
194
|
+
expect(tweak.effort).toBe("low");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("with no workspace entry the shipped fragment applies whole", () => {
|
|
198
|
+
const tweak = composeCallSiteTweak(
|
|
199
|
+
{ model: "shipped-model", provider: "openai", effort: "low" },
|
|
200
|
+
undefined,
|
|
201
|
+
);
|
|
202
|
+
expect(tweak.model).toBe("shipped-model");
|
|
203
|
+
expect(tweak.provider).toBe("openai");
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("a workspace provider/model still applies", () => {
|
|
207
|
+
const tweak = composeCallSiteTweak(
|
|
208
|
+
{ model: "shipped-model", effort: "low" },
|
|
209
|
+
{ provider: "anthropic", model: "chosen-model" },
|
|
210
|
+
);
|
|
211
|
+
expect(tweak.model).toBe("chosen-model");
|
|
212
|
+
expect(tweak.provider).toBe("anthropic");
|
|
213
|
+
expect(tweak.effort).toBe("low");
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("profile and logitBias never reach the fragment", () => {
|
|
217
|
+
const tweak = composeCallSiteTweak(
|
|
218
|
+
{ effort: "low" },
|
|
219
|
+
{ profile: "mine", logitBias: "suppress-cjk", maxTokens: 10 },
|
|
220
|
+
);
|
|
221
|
+
expect("profile" in tweak).toBe(false);
|
|
222
|
+
expect("logitBias" in tweak).toBe(false);
|
|
223
|
+
expect(tweak.maxTokens).toBe(10);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
describe("shipped call-site tuning", () => {
|
|
228
|
+
// A workspace entry that only repoints the profile must not cost the call
|
|
229
|
+
// site the tuning it ships with. `recall` carries a full set: a token
|
|
230
|
+
// budget, effort, sampling, thinking, and cache posture.
|
|
231
|
+
test("a profile-only entry keeps the shipped tuning and takes the new model", () => {
|
|
232
|
+
const shipped = resolveCallSiteConfig(
|
|
233
|
+
"recall",
|
|
234
|
+
LLMSchema.parse({ profiles: { mine: completeCustom }, ...anthropicDp }),
|
|
235
|
+
);
|
|
236
|
+
const repointed = resolveCallSiteConfig(
|
|
237
|
+
"recall",
|
|
238
|
+
LLMSchema.parse({
|
|
239
|
+
profiles: { mine: completeCustom },
|
|
240
|
+
callSites: { recall: { profile: "mine" } },
|
|
241
|
+
...anthropicDp,
|
|
242
|
+
}),
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
// The pin is what selects the model.
|
|
246
|
+
expect(repointed.model).toBe("gpt-5.5");
|
|
247
|
+
expect(repointed.model).not.toBe(shipped.model);
|
|
248
|
+
|
|
249
|
+
// Everything the call site ships with survives the repoint.
|
|
250
|
+
expect(repointed.maxTokens).toBe(4096);
|
|
251
|
+
expect(repointed.effort).toBe("low");
|
|
252
|
+
expect(repointed.temperature).toBe(0);
|
|
253
|
+
expect(repointed.disableCache).toBe(true);
|
|
254
|
+
expect(repointed.thinking.enabled).toBe(false);
|
|
255
|
+
expect(repointed.thinking.streamThinking).toBe(false);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("a profile-only entry keeps a shipped nested budget", () => {
|
|
259
|
+
const resolved = resolveCallSiteConfig(
|
|
260
|
+
"memoryRouter",
|
|
261
|
+
LLMSchema.parse({
|
|
262
|
+
profiles: { mine: completeCustom },
|
|
263
|
+
callSites: { memoryRouter: { profile: "mine" } },
|
|
264
|
+
...anthropicDp,
|
|
265
|
+
}),
|
|
266
|
+
);
|
|
267
|
+
expect(resolved.contextWindow.maxInputTokens).toBe(1000000);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("an explicit workspace field beats the shipped one, field by field", () => {
|
|
271
|
+
const resolved = resolveCallSiteConfig(
|
|
272
|
+
"recall",
|
|
273
|
+
LLMSchema.parse({
|
|
274
|
+
profiles: { mine: completeCustom },
|
|
275
|
+
callSites: {
|
|
276
|
+
recall: { profile: "mine", maxTokens: 512, effort: "high" },
|
|
277
|
+
},
|
|
278
|
+
...anthropicDp,
|
|
279
|
+
}),
|
|
280
|
+
);
|
|
281
|
+
// The two fields the entry names come from the entry.
|
|
282
|
+
expect(resolved.maxTokens).toBe(512);
|
|
283
|
+
expect(resolved.effort).toBe("high");
|
|
284
|
+
// The ones it does not name still come from the shipped tuning.
|
|
285
|
+
expect(resolved.temperature).toBe(0);
|
|
286
|
+
expect(resolved.disableCache).toBe(true);
|
|
287
|
+
expect(resolved.thinking.enabled).toBe(false);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test("a provider/model entry keeps the shipped tuning too", () => {
|
|
291
|
+
const resolved = resolveCallSiteConfig(
|
|
292
|
+
"recall",
|
|
293
|
+
LLMSchema.parse({
|
|
294
|
+
profiles: { mine: completeCustom },
|
|
295
|
+
callSites: {
|
|
296
|
+
recall: { provider: "openai", model: "gpt-5.5" },
|
|
297
|
+
},
|
|
298
|
+
...anthropicDp,
|
|
299
|
+
}),
|
|
300
|
+
);
|
|
301
|
+
expect(resolved.model).toBe("gpt-5.5");
|
|
302
|
+
expect(resolved.maxTokens).toBe(4096);
|
|
303
|
+
expect(resolved.effort).toBe("low");
|
|
304
|
+
expect(resolved.disableCache).toBe(true);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test("a call site with no shipped tuning is unaffected", () => {
|
|
308
|
+
const resolved = resolveCallSiteConfig(
|
|
309
|
+
"conversationSummarization",
|
|
310
|
+
LLMSchema.parse({
|
|
311
|
+
profiles: { mine: completeCustom },
|
|
312
|
+
callSites: { conversationSummarization: { profile: "mine" } },
|
|
313
|
+
...anthropicDp,
|
|
314
|
+
}),
|
|
315
|
+
);
|
|
316
|
+
expect(resolved.model).toBe("gpt-5.5");
|
|
317
|
+
expect(resolved.maxTokens).toBe(completeCustom.maxTokens);
|
|
318
|
+
expect(resolved.effort).toBe(completeCustom.effort);
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
181
322
|
describe("composition", () => {
|
|
182
323
|
test("base + winner + site-tweak composition (nested tweaks combine leaf-wise)", () => {
|
|
183
324
|
const llm = LLMSchema.parse({
|
|
@@ -406,6 +406,8 @@ mock.module("../calls/access-request-wait.js", () => ({
|
|
|
406
406
|
// Now import the module under test.
|
|
407
407
|
// ---------------------------------------------------------------------------
|
|
408
408
|
|
|
409
|
+
import { GATEWAY_TUNNEL_LOST_WS_CLOSE_CODE } from "@vellumai/service-contracts/ingress";
|
|
410
|
+
|
|
409
411
|
import { revokeScopedApprovalGrantsForContext } from "../approvals/scoped-approval-grants.js";
|
|
410
412
|
import { CallController } from "../calls/call-controller.js";
|
|
411
413
|
import { postPointerMessageSafe } from "../calls/call-pointer-messages.js";
|
|
@@ -746,6 +748,40 @@ describe("MediaStreamCallSession", () => {
|
|
|
746
748
|
);
|
|
747
749
|
});
|
|
748
750
|
|
|
751
|
+
test("tunnel-lost close code decodes to a readable failure detail", () => {
|
|
752
|
+
const mock = createMockWs();
|
|
753
|
+
mockSessions.set("call-1", {
|
|
754
|
+
id: "call-1",
|
|
755
|
+
conversationId: "conv-1",
|
|
756
|
+
status: "in_progress",
|
|
757
|
+
startedAt: Date.now() - 60000,
|
|
758
|
+
toNumber: "+16175550142",
|
|
759
|
+
initiatedFromConversationId: "conv-origin",
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
const session = new MediaStreamCallSession(mock.ws, "call-1");
|
|
763
|
+
// The gateway's close reason is dropped in relay, so only the code
|
|
764
|
+
// arrives; it must decode to something a human can act on.
|
|
765
|
+
session.handleTransportClosed(GATEWAY_TUNNEL_LOST_WS_CLOSE_CODE);
|
|
766
|
+
|
|
767
|
+
expect(updateCallSession).toHaveBeenCalledWith(
|
|
768
|
+
"call-1",
|
|
769
|
+
expect.objectContaining({
|
|
770
|
+
status: "failed",
|
|
771
|
+
lastError:
|
|
772
|
+
"Media stream WebSocket closed unexpectedly: public ingress tunnel disconnected",
|
|
773
|
+
}),
|
|
774
|
+
);
|
|
775
|
+
expect(postPointerMessageSafe).toHaveBeenCalledWith(
|
|
776
|
+
"conv-origin",
|
|
777
|
+
"failed",
|
|
778
|
+
"+16175550142",
|
|
779
|
+
expect.objectContaining({
|
|
780
|
+
reason: "public ingress tunnel disconnected",
|
|
781
|
+
}),
|
|
782
|
+
);
|
|
783
|
+
});
|
|
784
|
+
|
|
749
785
|
test("close on already-terminal session is a no-op", () => {
|
|
750
786
|
const mock = createMockWs();
|
|
751
787
|
mockSessions.set("call-1", {
|
|
@@ -129,3 +129,41 @@ describe("web STT language catalog stays in sync with the daemon roster", () =>
|
|
|
129
129
|
expect(nonExtended).toEqual(expected);
|
|
130
130
|
});
|
|
131
131
|
});
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The settings skill accepts plain language names ("tamil", "hindi") and its
|
|
135
|
+
* tool description advertises that, so a roster code with no English-name
|
|
136
|
+
* alias is reachable only by someone who knows the raw BCP-47 code. Adding a
|
|
137
|
+
* language to the roster without its alias is the easy half-step, so pin the
|
|
138
|
+
* two together the same way the catalog copies are pinned.
|
|
139
|
+
*/
|
|
140
|
+
const SETTINGS_TOOL_SOURCE = readFileSync(
|
|
141
|
+
join(
|
|
142
|
+
REPO_ROOT,
|
|
143
|
+
"assistant/src/config/bundled-skills/settings/tools/voice-config-update.ts",
|
|
144
|
+
),
|
|
145
|
+
"utf-8",
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
describe("settings-skill language aliases", () => {
|
|
149
|
+
test("every roster language is reachable by an English name", () => {
|
|
150
|
+
const aliasBlockStart = SETTINGS_TOOL_SOURCE.indexOf(
|
|
151
|
+
"const STT_LANGUAGE_ALIASES",
|
|
152
|
+
);
|
|
153
|
+
expect(aliasBlockStart).toBeGreaterThan(-1);
|
|
154
|
+
const aliasBlockEnd = SETTINGS_TOOL_SOURCE.indexOf("};", aliasBlockStart);
|
|
155
|
+
const aliasBlock = SETTINGS_TOOL_SOURCE.slice(
|
|
156
|
+
aliasBlockStart,
|
|
157
|
+
aliasBlockEnd,
|
|
158
|
+
);
|
|
159
|
+
// Every value in the alias map, i.e. the codes a plain name can reach.
|
|
160
|
+
const aliasedCodes = new Set(
|
|
161
|
+
[...aliasBlock.matchAll(/:\s*"([a-z-]+)"/g)].map((match) => match[1]),
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const unreachable = DEEPGRAM_NOVA3_MONOLINGUAL_CODES.filter(
|
|
165
|
+
(code) => !aliasedCodes.has(code),
|
|
166
|
+
);
|
|
167
|
+
expect(unreachable).toEqual([]);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
@@ -116,6 +116,7 @@ function makeSetupCtx(overrides: Partial<Conversation> = {}): Conversation {
|
|
|
116
116
|
conversationId: "conv-test",
|
|
117
117
|
currentRequestId: "req-1",
|
|
118
118
|
workingDir: "/tmp/test",
|
|
119
|
+
getTurnActorPrincipalId: () => undefined,
|
|
119
120
|
abortController: null,
|
|
120
121
|
sendToClient: mock(() => {}),
|
|
121
122
|
pendingSurfaceActions: new Map(),
|