@vellumai/assistant 0.11.2-dev.202608051838.a2809c6 → 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 CHANGED
@@ -67,8 +67,10 @@ All HTTP API requests use a single `Authorization: Bearer <jwt>` header for auth
67
67
  | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- |
68
68
  | `actor_client_v1` | `chat.{read,write}`, `approval.{read,write}`, `settings.{read,write}`, `attachments.{read,write}`, `calls.{read,write}`, `feature_flags.{read,write}` | Desktop, CLI clients |
69
69
  | `gateway_ingress_v1` | `ingress.write`, `internal.write` | Gateway channel inbound + webhook forwarding |
70
- | `gateway_service_v1` | `settings.read`, `settings.write`, `internal.write` | Gateway service-to-daemon calls |
71
- | `internal_v1` | `internal.all` | Internal service connections |
70
+ | `gateway_service_v1` | `chat.{read,write}`, `settings.{read,write}`, `attachments.{read,write}`, `internal.write` | Gateway service-to-daemon calls |
71
+ | `local_v1` | `local.all` | Local (loopback) conversation sessions |
72
+ | `speech_relay_v1` | `speech.relay` | Daemon dial of the gateway speech relay only |
73
+ | `ui_page_v1` | `settings.read` | Served UI pages |
72
74
 
73
75
  **Identity lifecycle:**
74
76
 
package/openapi.yaml CHANGED
@@ -9565,6 +9565,28 @@ paths:
9565
9565
  required:
9566
9566
  - ok
9567
9567
  additionalProperties: false
9568
+ /v1/conversations/unread-count:
9569
+ get:
9570
+ operationId: conversations_unreadcount_get
9571
+ summary: Unread conversation count
9572
+ description:
9573
+ "Count of foreground conversations whose latest assistant message is unseen. Matches the sidebar's unread
9574
+ indicators: archived rows and non-surfaced background/scheduled rows are excluded."
9575
+ tags:
9576
+ - conversations
9577
+ responses:
9578
+ "200":
9579
+ description: Successful response
9580
+ content:
9581
+ application/json:
9582
+ schema:
9583
+ type: object
9584
+ properties:
9585
+ count:
9586
+ type: number
9587
+ required:
9588
+ - count
9589
+ additionalProperties: false
9568
9590
  /v1/conversations/wake:
9569
9591
  post:
9570
9592
  operationId: conversations_wake_post
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.11.2-dev.202608051838.a2809c6",
3
+ "version": "0.11.2-dev.202608051933.deffc0f",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,16 +1,17 @@
1
1
  /**
2
2
  * Verifies that the per-turn `overrideProfile` plumbed into `AgentLoop.run()`
3
3
  * surfaces on every `SendMessageOptions.config` the loop emits, and that
4
- * `SubagentManager.spawn()` propagates an inherited `overrideProfile` from
5
- * its `SubagentConfig` into the subagent's `runAgentLoop()` call.
4
+ * `SubagentManager.spawn()` propagates an `overrideProfile` set on its
5
+ * `SubagentConfig` into the subagent's `runAgentLoop()` call.
6
6
  *
7
- * Together these two assertions establish the PR 6 contract: a parent
8
- * conversation that pins an inference profile sees that profile applied to
9
- * every LLM call within its agent-loop turn, and any subagent spawned during
10
- * that turn inherits the same profile automatically.
7
+ * Together these establish where a pinned profile does and does not travel: it
8
+ * applies to every LLM call within the pinning conversation's own turn, and it
9
+ * stops at the spawn boundary. `executeSubagentSpawn` sets `overrideProfile`
10
+ * on the `SubagentConfig` only for a profile named explicitly on the spawn, so
11
+ * the propagation above carries a caller's choice rather than a parent's pin.
11
12
  *
12
- * Default behavior (no `overrideProfile` set) must remain unchanged — the
13
- * field is omitted from `providerConfig` rather than carrying `undefined`.
13
+ * With no `overrideProfile` set, the field is omitted from `providerConfig`
14
+ * rather than carrying `undefined`.
14
15
  */
15
16
 
16
17
  import { describe, expect, mock, test } from "bun:test";
@@ -183,7 +184,7 @@ describe("AgentLoop.run — overrideProfile plumbing", () => {
183
184
  });
184
185
  });
185
186
 
186
- // ── Subagent inheritance ─────────────────────────────────────────────────
187
+ // ── Subagent profile forwarding ──────────────────────────────────────────
187
188
 
188
189
  // Capture the SubagentManager → Conversation handshake so we can verify the
189
190
  // `overrideProfile` from `SubagentConfig` is forwarded into the spawned
@@ -329,7 +330,7 @@ seedLlmConfig();
329
330
 
330
331
  import { SubagentManager } from "../subagent/manager.js";
331
332
 
332
- describe("SubagentManager.spawn — overrideProfile inheritance", () => {
333
+ describe("SubagentManager.spawn: forwards a configured overrideProfile", () => {
333
334
  test("forwards overrideProfile from SubagentConfig into runAgentLoop", async () => {
334
335
  capturedRunAgentLoopOptions.length = 0;
335
336
 
@@ -344,9 +345,9 @@ describe("SubagentManager.spawn — overrideProfile inheritance", () => {
344
345
  () => {},
345
346
  );
346
347
 
347
- // The spawned subagent's runAgentLoop must receive both the
348
- // `subagentSpawn` callSite (existing behavior) and the inherited
349
- // `overrideProfile` (new behavior).
348
+ // The spawned subagent's runAgentLoop receives the `subagentSpawn`
349
+ // callSite and whatever `overrideProfile` the config carries, which
350
+ // `executeSubagentSpawn` sets only for an explicitly named profile.
350
351
  expect(capturedRunAgentLoopOptions).toHaveLength(1);
351
352
  const captured = capturedRunAgentLoopOptions[0];
352
353
  expect(captured.callSite).toBe("subagentSpawn");
@@ -399,21 +400,18 @@ describe("SubagentManager.spawn — overrideProfile inheritance", () => {
399
400
  });
400
401
  });
401
402
 
402
- // ── Nested subagent spawn — context.overrideProfile preferred ────────────
403
+ // ── Nested subagent spawn: no profile crosses the boundary ───────────────
403
404
 
404
- // Verify the third-level inheritance contract: when a subagent's agent loop
405
- // is running with `currentTurnOverrideProfile`, the executor closure plumbs
406
- // that value into `ToolContext.overrideProfile`. `executeSubagentSpawn` must
407
- // then prefer `context.overrideProfile` over a row read against the in-flight
408
- // subagent's own conversationId — that row never has `inferenceProfile` set,
409
- // and `getConversationOverrideProfile` short-circuits for background
410
- // conversations regardless. Without preferring the in-memory context, the
411
- // inheritance chain breaks at the second nesting level.
405
+ // When a subagent's agent loop is running under a turn profile, the executor
406
+ // closure plumbs that value into `ToolContext.overrideProfile`. That value
407
+ // describes the spawning turn, so `executeSubagentSpawn` leaves it behind: a
408
+ // child resolves the `subagentSpawn` call site on its own, and a forwarded
409
+ // override would both re-price the child and file its spend as a pin nobody
410
+ // set. These tests cover the nesting levels where a profile could otherwise
411
+ // compound down a chain of children.
412
412
 
413
413
  mock.module("../persistence/conversation-crud.js", () => ({
414
414
  setConversationProcessingStartedAt: () => {},
415
- // Always return undefined for the row read so the test fails fast unless
416
- // executeSubagentSpawn reads from context.overrideProfile first.
417
415
  getConversationOverrideProfile: () => undefined,
418
416
  reserveMessage: mock(async () => ({ id: "msg-reserve" })),
419
417
  }));
@@ -421,8 +419,8 @@ mock.module("../persistence/conversation-crud.js", () => ({
421
419
  import { getSubagentManager } from "../subagent/index.js";
422
420
  import { executeSubagentSpawn } from "../tools/subagent/spawn.js";
423
421
 
424
- describe("executeSubagentSpawn — nested inheritance via context.overrideProfile", () => {
425
- test("forwards context.overrideProfile to SubagentConfig (third-level inheritance)", async () => {
422
+ describe("executeSubagentSpawn: the spawn boundary carries no profile", () => {
423
+ test("a subagent spawning a subagent does not pass its own turn profile down", async () => {
426
424
  const manager = getSubagentManager();
427
425
  const originalSpawn = manager.spawn.bind(manager);
428
426
  let capturedConfig: Record<string, unknown> | undefined;
@@ -432,11 +430,11 @@ describe("executeSubagentSpawn — nested inheritance via context.overrideProfil
432
430
  };
433
431
 
434
432
  try {
435
- // Simulate the second-level spawn: tool invocation occurs inside the
436
- // first subagent's tool context, where `overrideProfile` was populated
437
- // by `runAgentLoopImpl` from its `currentTurnOverrideProfile` snapshot.
438
- // The first subagent's own conversation row has no `inferenceProfile`,
439
- // so the row-read fallback would otherwise return undefined.
433
+ // The second-level spawn: this tool call happens inside the first
434
+ // subagent's tool context, where `overrideProfile` was populated by
435
+ // `runAgentLoopImpl` from its own turn snapshot. That snapshot describes
436
+ // the parent's turn, so it stops here rather than compounding down a
437
+ // chain of nested children.
440
438
  const result = await executeSubagentSpawn(
441
439
  { label: "nested", objective: "do nested work" },
442
440
  {
@@ -450,16 +448,13 @@ describe("executeSubagentSpawn — nested inheritance via context.overrideProfil
450
448
 
451
449
  expect(result.isError).toBe(false);
452
450
  expect(capturedConfig).toBeDefined();
453
- // The forwarded SubagentConfig must carry the in-memory override so
454
- // SubagentManager.spawn forwards it into the nested subagent's
455
- // runAgentLoop options — preserving inheritance across the chain.
456
- expect(capturedConfig!.overrideProfile).toBe("fast");
451
+ expect("overrideProfile" in capturedConfig!).toBe(false);
457
452
  } finally {
458
453
  manager.spawn = originalSpawn;
459
454
  }
460
455
  });
461
456
 
462
- test("forwards the invoker's resolved default profile when neither context nor row carries an override", async () => {
457
+ test("no override anywhere leaves the child on the subagentSpawn default", async () => {
463
458
  const manager = getSubagentManager();
464
459
  const originalSpawn = manager.spawn.bind(manager);
465
460
  let capturedConfig: Record<string, unknown> | undefined;
@@ -481,22 +476,17 @@ describe("executeSubagentSpawn — nested inheritance via context.overrideProfil
481
476
  );
482
477
 
483
478
  expect(capturedConfig).toBeDefined();
484
- // With no explicit override anywhere, the invoker-default tier forwards
485
- // the invoking call site's resolved default profile — here `mainAgent`
486
- // with no active profile or pin, which resolves the catalog `balanced`
487
- // default — so the subagent matches its invoker's model selection.
488
- expect(capturedConfig!.overrideProfile).toBe("balanced");
479
+ expect("overrideProfile" in capturedConfig!).toBe(false);
489
480
  } finally {
490
481
  manager.spawn = originalSpawn;
491
482
  }
492
483
  });
493
484
 
494
- test("an explicit subagentSpawn call-site pin suppresses the invoker-default inheritance", async () => {
495
- // With an active profile the invoker-default tier resolves ("fast"), so
496
- // absent a pin it is forwarded as the inherited override; with an
497
- // explicit llm.callSites.subagentSpawn profile the heuristic must yield —
498
- // a forwarded override would outrank the pin under single-winner
499
- // resolution, overriding the user's explicit call-site choice.
485
+ test("a workspace active profile does not reach the child, pinned or not", async () => {
486
+ // The child resolves `llm.callSites.subagentSpawn` on its own, so the
487
+ // spawn must forward nothing either way: a forwarded override would
488
+ // outrank an explicit call-site pin under single-winner resolution and
489
+ // overturn the operator's choice.
500
490
  const manager = getSubagentManager();
501
491
  const originalSpawn = manager.spawn.bind(manager);
502
492
  let capturedConfig: Record<string, unknown> | undefined;
@@ -517,7 +507,7 @@ describe("executeSubagentSpawn — nested inheritance via context.overrideProfil
517
507
  { label: "nested", objective: "do nested work" },
518
508
  baseContext,
519
509
  );
520
- expect(capturedConfig!.overrideProfile).toBe("fast");
510
+ expect("overrideProfile" in capturedConfig!).toBe(false);
521
511
 
522
512
  seedLlmConfig({
523
513
  activeProfile: "fast",
@@ -80,16 +80,6 @@ describe("isAssistantFeatureFlagEnabled", () => {
80
80
  });
81
81
  });
82
82
 
83
- describe("subagent flags default to disabled", () => {
84
- for (const flagKey of ["subagent-profile-isolation", "subagent-loop-guard"]) {
85
- test(`${flagKey} resolves to false with no override`, () => {
86
- const config = {} as any;
87
-
88
- expect(isAssistantFeatureFlagEnabled(flagKey, config)).toBe(false);
89
- });
90
- }
91
- });
92
-
93
83
  describe("isAssistantFeatureFlagEnabled with skillFlagKey", () => {
94
84
  test("resolves skill flag via canonical path", () => {
95
85
  setOverridesForTesting({ [DECLARED_FLAG_KEY]: false });
@@ -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({