@stackbone/cli 0.7.1 → 0.9.0

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.
@@ -6,66 +6,114 @@ import {
6
6
  defineAgent,
7
7
  inference,
8
8
  telemetry as telemetry2,
9
- voice as voice4
9
+ voice as voice5
10
10
  } from "@livekit/agents";
11
11
  import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
12
12
 
13
- // libs/runtime/voice-worker/src/thinking-sound.ts
14
- import { voice } from "@livekit/agents";
13
+ // libs/runtime/voice-worker/src/background-audio.ts
14
+ import { audioFramesFromFile, voice } from "@livekit/agents";
15
15
  import { AudioFrame } from "@livekit/rtc-node";
16
16
  var SAMPLE_RATE = 48e3;
17
17
  var FRAME_MS = 20;
18
18
  var SAMPLES_PER_FRAME = SAMPLE_RATE * FRAME_MS / 1e3;
19
- var VOLUME = 0.12;
19
+ var CHIME_LEVEL = 0.12;
20
20
  var PATTERN = [
21
21
  [660, 140],
22
22
  [0, 60],
23
23
  [880, 160],
24
24
  [0, 900]
25
25
  ];
26
- var renderPattern = () => {
27
- const total = PATTERN.reduce((n, [, ms]) => n + SAMPLE_RATE * ms / 1e3, 0);
26
+ var renderChime = () => {
27
+ const total = PATTERN.reduce((n, [, ms2]) => n + SAMPLE_RATE * ms2 / 1e3, 0);
28
28
  const pcm = new Int16Array(total);
29
29
  let offset = 0;
30
- for (const [hz, ms] of PATTERN) {
31
- const length = SAMPLE_RATE * ms / 1e3;
30
+ for (const [hz, ms2] of PATTERN) {
31
+ const length = SAMPLE_RATE * ms2 / 1e3;
32
32
  for (let i = 0; hz > 0 && i < length; i++) {
33
33
  const envelope = 0.5 - 0.5 * Math.cos(2 * Math.PI * i / length);
34
- const sample = Math.sin(2 * Math.PI * hz * i / SAMPLE_RATE) * envelope * VOLUME;
34
+ const sample = Math.sin(2 * Math.PI * hz * i / SAMPLE_RATE) * envelope * CHIME_LEVEL;
35
35
  pcm[offset + i] = Math.round(sample * 32767);
36
36
  }
37
37
  offset += length;
38
38
  }
39
- return pcm;
40
- };
41
- var LOOP = renderPattern();
42
- async function* thinkingCue() {
43
- for (; ; ) {
44
- for (let start = 0; start + SAMPLES_PER_FRAME <= LOOP.length; start += SAMPLES_PER_FRAME) {
45
- yield new AudioFrame(
46
- LOOP.slice(start, start + SAMPLES_PER_FRAME),
39
+ const frames = [];
40
+ for (let start = 0; start + SAMPLES_PER_FRAME <= total; start += SAMPLES_PER_FRAME) {
41
+ frames.push(
42
+ new AudioFrame(
43
+ pcm.slice(start, start + SAMPLES_PER_FRAME),
47
44
  SAMPLE_RATE,
48
45
  1,
49
46
  SAMPLES_PER_FRAME
50
- );
51
- }
47
+ )
48
+ );
52
49
  }
53
- }
54
- var startThinkingCue = async ({
50
+ return frames;
51
+ };
52
+ var looped = (frames) => ({
53
+ async *[Symbol.asyncIterator]() {
54
+ const loop = await frames();
55
+ if (loop.length === 0) return;
56
+ for (; ; ) yield* loop;
57
+ }
58
+ });
59
+ var CHIME_FRAMES = renderChime();
60
+ var CHIME = looped(async () => CHIME_FRAMES);
61
+ var LIVEKIT_CLIPS = {
62
+ "office-ambience": voice.BuiltinAudioClip.OFFICE_AMBIENCE,
63
+ "keyboard-typing": voice.BuiltinAudioClip.KEYBOARD_TYPING,
64
+ "keyboard-typing2": voice.BuiltinAudioClip.KEYBOARD_TYPING2,
65
+ "hold-music": voice.BuiltinAudioClip.HOLD_MUSIC
66
+ };
67
+ var decoded = /* @__PURE__ */ new Map();
68
+ var clipFrames = (clip) => {
69
+ let frames = decoded.get(clip);
70
+ if (!frames) {
71
+ frames = (async () => {
72
+ const all = [];
73
+ const path = voice.getBuiltinAudioPath(LIVEKIT_CLIPS[clip]);
74
+ for await (const frame of audioFramesFromFile(path, { sampleRate: SAMPLE_RATE })) {
75
+ all.push(frame);
76
+ }
77
+ return all;
78
+ })();
79
+ frames.catch(() => decoded.delete(clip));
80
+ decoded.set(clip, frames);
81
+ }
82
+ return frames;
83
+ };
84
+ var CLIPS = new Map(
85
+ Object.keys(LIVEKIT_CLIPS).map((clip) => [
86
+ clip,
87
+ looped(() => clipFrames(clip))
88
+ ])
89
+ );
90
+ var backgroundSource = (sound, volume) => {
91
+ if (sound === "none" || volume <= 0) return void 0;
92
+ const source = sound === "chime" ? CHIME : CLIPS.get(sound);
93
+ return source && { source, volume };
94
+ };
95
+ var SILENT = Object.freeze({ start: () => void 0, stop: () => void 0 });
96
+ var startBackgroundAudio = async ({
55
97
  room,
56
- session
98
+ session,
99
+ audio
57
100
  }) => {
58
- const player = new voice.BackgroundAudioPlayer();
59
- await player.start({ room, agentSession: session });
101
+ const ambientSound = backgroundSource(audio.ambientSound, audio.ambientVolume);
102
+ const thinking = backgroundSource(audio.thinkingSound, audio.thinkingVolume);
103
+ if (!ambientSound && !thinking) return SILENT;
104
+ const player = new voice.BackgroundAudioPlayer(ambientSound ? { ambientSound } : {});
105
+ const clip = audio.thinkingSound;
106
+ await Promise.all([
107
+ player.start({ room, agentSession: session }),
108
+ thinking && clip !== "chime" && clip !== "none" ? clipFrames(clip) : void 0
109
+ ]);
110
+ if (!thinking) return SILENT;
60
111
  let playing;
61
112
  return {
62
113
  start: () => {
63
- playing ??= player.play(thinkingCue());
114
+ playing = player.play(thinking);
64
115
  },
65
- stop: () => {
66
- playing?.stop();
67
- playing = void 0;
68
- }
116
+ stop: () => playing?.stop()
69
117
  };
70
118
  };
71
119
 
@@ -101,37 +149,176 @@ var loadVad = async () => {
101
149
  };
102
150
 
103
151
  // libs/runtime/voice-worker/src/worker.ts
104
- import { log as log4, voice as voice3 } from "@livekit/agents";
105
- import { BackgroundVoiceCancellation } from "@livekit/noise-cancellation-node";
152
+ import { log as log4, voice as voice4 } from "@livekit/agents";
153
+ import {
154
+ BackgroundVoiceCancellation,
155
+ TelephonyBackgroundVoiceCancellation
156
+ } from "@livekit/noise-cancellation-node";
106
157
  import { ParticipantKind } from "@livekit/rtc-node";
107
158
 
108
159
  // libs/shared/validators/src/lib/common/json-object.ts
109
160
  import { z } from "zod";
110
161
  var jsonObject = () => z.looseObject({});
111
162
 
112
- // libs/shared/validators/src/lib/common/page.ts
163
+ // libs/shared/validators/src/lib/studio/studio-model-provider.ts
113
164
  import { z as z2 } from "zod";
165
+ var modelProviderSlotSchema = z2.enum(["baseUrl", "apiKey"]);
166
+ var MODEL_PROVIDER_SLOTS = {
167
+ baseUrl: ["MODEL_PROVIDER_BASE_URL", "OPENROUTER_BASE_URL"],
168
+ apiKey: ["MODEL_PROVIDER_API_KEY", "OPENROUTER_API_KEY"]
169
+ };
170
+ var SLOT_NAMES = Object.keys(MODEL_PROVIDER_SLOTS);
171
+ var MODEL_PROVIDER_DEPRECATED_ENV_NAMES = SLOT_NAMES.map(
172
+ (slot) => MODEL_PROVIDER_SLOTS[slot][1]
173
+ );
174
+ var modelProviderSourceSchema = z2.enum(["environment", "stored", "none"]);
175
+ var isModelProviderValuePresent = (value) => typeof value === "string" && value.trim() !== "";
176
+ var storedOverridesSchema = z2.strictObject({
177
+ baseUrl: z2.boolean().optional(),
178
+ apiKey: z2.boolean().optional()
179
+ });
180
+ var modelProviderKindSchema = z2.enum(["openrouter", "ollama", "lmstudio", "generic"]);
181
+ var modelProviderGetResponseSchema = z2.object({
182
+ configured: z2.boolean(),
183
+ base_url: z2.string().nullable(),
184
+ api_key_masked: z2.string().nullable(),
185
+ source: modelProviderSourceSchema.optional(),
186
+ env_override: z2.array(z2.string()).optional(),
187
+ effective_base_url: z2.string().nullable().optional(),
188
+ // The gateway kind of the endpoint IN EFFECT (the one `effective_base_url`
189
+ // names), resolved by the box; `null` when nothing is configured. Optional for
190
+ // the same version-skew reason as `source`: a box that predates the field.
191
+ kind: modelProviderKindSchema.nullable().optional(),
192
+ // The slots where a Studio override beats an exported value (PLA-1825, ADR
193
+ // D8), by SLOT name, never the variable's (D4). Such a slot drops out of
194
+ // `env_override`, which keeps naming only the variables that actually win.
195
+ // A box that knows overrides always sends the list, empty or not; an absent
196
+ // list is a box that predates them, and would drop an `override` it cannot
197
+ // read, so Studio offers no Override against it.
198
+ overridden: z2.array(modelProviderSlotSchema).optional()
199
+ });
200
+ var modelProviderPutRequestSchema = z2.object({
201
+ base_url: z2.url(),
202
+ api_key: z2.string().optional().nullable(),
203
+ kind: modelProviderKindSchema.optional(),
204
+ override: z2.object({ baseUrl: z2.boolean().optional(), apiKey: z2.boolean().optional() }).optional()
205
+ }).refine((body) => body.override?.apiKey !== true || isModelProviderValuePresent(body.api_key), {
206
+ message: "Overriding the API key needs the key to use.",
207
+ path: ["api_key"]
208
+ });
209
+ var modelProviderAgentsOutcomeSchema = z2.object({
210
+ /** Agents that were degraded and are now serving. */
211
+ ready: z2.number().int().nonnegative(),
212
+ /** Agents that are still degraded (their reason was refreshed by the retry). */
213
+ degraded: z2.number().int().nonnegative()
214
+ });
215
+ var modelProviderPutResponseSchema = modelProviderGetResponseSchema.extend({
216
+ agents: modelProviderAgentsOutcomeSchema.optional()
217
+ });
218
+ var modelProviderFirstRunResponseSchema = z2.object({
219
+ completed: z2.boolean()
220
+ });
221
+ var modelProviderKeyCheckSchema = z2.enum(["verified", "unverified"]);
222
+ var modelProviderTestResponseSchema = z2.object({
223
+ ok: z2.boolean(),
224
+ model_count: z2.number().int().nonnegative().optional(),
225
+ error: z2.string().optional(),
226
+ key_checked: modelProviderKeyCheckSchema.optional()
227
+ });
228
+ var modelProviderModelTypeSchema = z2.enum(["chat", "image", "embedding", "other"]);
229
+ var modelProviderModelSchema = z2.object({
230
+ id: z2.string().min(1),
231
+ name: z2.string().optional(),
232
+ context_length: z2.number().int().positive().optional(),
233
+ pricing: z2.object({
234
+ prompt: z2.string().optional(),
235
+ completion: z2.string().optional()
236
+ }).optional(),
237
+ owned_by: z2.string().optional(),
238
+ type: modelProviderModelTypeSchema.optional(),
239
+ // What the gateway says the model can do (PLA-1605): `true`, `false`, or
240
+ // `null` for "this gateway does not say". The three values are distinct on
241
+ // purpose — Studio hides a model that reports no tool support and only WARNS
242
+ // about one that reports nothing, so collapsing `null` into `false` would hide
243
+ // every model on a custom OpenAI-compatible endpoint.
244
+ //
245
+ // Nullable AND optional: a box on an older runtime sends neither field, and
246
+ // that must keep validating, exactly as `type` already does.
247
+ tools: z2.boolean().nullable().optional(),
248
+ reasoning: z2.boolean().nullable().optional(),
249
+ // STA3-17: on a box with several providers, the one that serves this entry.
250
+ // `responding: false` marks an entry remembered from a provider that is down.
251
+ // Declared so the parse keeps it: an object schema strips what it does not name.
252
+ provider: z2.object({ id: z2.string(), name: z2.string(), responding: z2.boolean() }).optional()
253
+ });
254
+ var modelProviderModelsRequestSchema = z2.object({
255
+ base_url: z2.url().optional().nullable(),
256
+ api_key: z2.string().optional().nullable()
257
+ });
258
+ var modelProviderModelsResponseSchema = z2.object({
259
+ ok: z2.boolean(),
260
+ models: z2.array(modelProviderModelSchema).optional(),
261
+ error: z2.string().optional(),
262
+ warning: z2.string().optional()
263
+ });
264
+ var MODEL_PROVIDER_DEFAULT_ID = "default";
265
+ var MODEL_PROVIDER_ID_PATTERN = /^[a-z][a-z0-9-]{0,30}[a-z0-9]$/;
266
+ var modelProviderIdSchema = z2.string().regex(MODEL_PROVIDER_ID_PATTERN, {
267
+ message: "Lowercase letters, digits and dashes, 2 to 32, starting with a letter."
268
+ }).refine((id) => id !== MODEL_PROVIDER_DEFAULT_ID, {
269
+ message: `'${MODEL_PROVIDER_DEFAULT_ID}' is provider 1's id.`
270
+ });
271
+ var pickedModelProviderIdSchema = z2.string().regex(MODEL_PROVIDER_ID_PATTERN);
272
+ var modelProviderListItemSchema = z2.object({
273
+ id: z2.string(),
274
+ name: z2.string(),
275
+ kind: modelProviderKindSchema,
276
+ baseUrl: z2.string(),
277
+ apiKey: z2.string().nullable(),
278
+ source: z2.enum(["env", "local"]),
279
+ removable: z2.boolean(),
280
+ responding: z2.boolean().nullable(),
281
+ error: z2.string().optional()
282
+ });
283
+ var modelProviderListResponseSchema = z2.object({
284
+ providers: z2.array(modelProviderListItemSchema),
285
+ duplicatedIds: z2.array(z2.object({ modelId: z2.string(), providerIds: z2.array(z2.string()) }))
286
+ });
287
+ var providerFields = {
288
+ name: z2.string().trim().min(1),
289
+ kind: modelProviderKindSchema,
290
+ baseUrl: z2.url(),
291
+ apiKey: z2.string().optional()
292
+ };
293
+ var modelProviderCreateRequestSchema = z2.strictObject({
294
+ id: modelProviderIdSchema,
295
+ ...providerFields
296
+ });
297
+ var modelProviderUpdateRequestSchema = z2.strictObject(providerFields);
298
+
299
+ // libs/shared/validators/src/lib/common/page.ts
300
+ import { z as z3 } from "zod";
114
301
  var PAGE_LIMIT_MAX = 100;
115
- var cursorParamSchema = z2.string().min(1);
116
- var limitParamSchema = (max = PAGE_LIMIT_MAX) => z2.coerce.number().int().min(1).max(max);
117
- var orderParamSchema = z2.enum(["asc", "desc"]);
118
- var pageQuerySchema = z2.object({
302
+ var cursorParamSchema = z3.string().min(1);
303
+ var limitParamSchema = (max = PAGE_LIMIT_MAX) => z3.coerce.number().int().min(1).max(max);
304
+ var orderParamSchema = z3.enum(["asc", "desc"]);
305
+ var pageQuerySchema = z3.object({
119
306
  cursor: cursorParamSchema.optional(),
120
307
  limit: limitParamSchema().optional(),
121
308
  order: orderParamSchema.optional()
122
309
  });
123
310
  var forwardPageQuerySchema = pageQuerySchema.omit({ order: true });
124
- var pageCursorSchema = z2.string().nullable();
125
- var pageResponseSchema = (item) => z2.object({ items: z2.array(item), nextCursor: pageCursorSchema, prevCursor: pageCursorSchema });
126
- var forwardPageResponseSchema = (item) => z2.object({ items: z2.array(item), nextCursor: pageCursorSchema });
311
+ var pageCursorSchema = z3.string().nullable();
312
+ var pageResponseSchema = (item) => z3.object({ items: z3.array(item), nextCursor: pageCursorSchema, prevCursor: pageCursorSchema });
313
+ var forwardPageResponseSchema = (item) => z3.object({ items: z3.array(item), nextCursor: pageCursorSchema });
127
314
 
128
315
  // libs/shared/validators/src/lib/agents/runs.ts
129
- import { z as z4 } from "zod";
316
+ import { z as z5 } from "zod";
130
317
 
131
318
  // libs/shared/validators/src/lib/common/system-entries.ts
132
- import { z as z3 } from "zod";
133
- var systemEntriesParamSchema = z3.enum(["include", "exclude"]);
134
- var systemEntriesQuerySchema = z3.object({
319
+ import { z as z4 } from "zod";
320
+ var systemEntriesParamSchema = z4.enum(["include", "exclude"]);
321
+ var systemEntriesQuerySchema = z4.object({
135
322
  system: systemEntriesParamSchema.optional()
136
323
  });
137
324
 
@@ -144,7 +331,7 @@ var RUN_STATUSES = [
144
331
  "waiting",
145
332
  "cancelled"
146
333
  ];
147
- var runStatusSchema = z4.enum(RUN_STATUSES);
334
+ var runStatusSchema = z5.enum(RUN_STATUSES);
148
335
  var RUN_TRIGGERS = [
149
336
  "manual",
150
337
  "webhook",
@@ -157,40 +344,40 @@ var RUN_TRIGGERS = [
157
344
  "platform",
158
345
  "unknown"
159
346
  ];
160
- var runTriggerSchema = z4.enum(RUN_TRIGGERS);
161
- var runEntityKindSchema = z4.enum(["agent", "workflow"]);
162
- var runTriggerLinkSchema = z4.object({
347
+ var runTriggerSchema = z5.enum(RUN_TRIGGERS);
348
+ var runEntityKindSchema = z5.enum(["agent", "workflow"]);
349
+ var runTriggerLinkSchema = z5.object({
163
350
  /** The `trigger_subscriptions.id`: the link the Studio trigger page routes to. */
164
- subscriptionId: z4.string(),
351
+ subscriptionId: z5.string(),
165
352
  /** The connector id, e.g. `gmail`. */
166
- connector: z4.string(),
353
+ connector: z5.string(),
167
354
  /** The trigger id within the connector, e.g. `gmail_new_email_received`. */
168
- trigger: z4.string(),
169
- outcome: z4.enum(["started", "resumed"]).optional(),
355
+ trigger: z5.string(),
356
+ outcome: z5.enum(["started", "resumed"]).optional(),
170
357
  /** The connector's brand name, e.g. `Gmail`. */
171
- connectorDisplayName: z4.string().optional(),
358
+ connectorDisplayName: z5.string().optional(),
172
359
  /** The trigger's name, e.g. `New Email`. */
173
- triggerDisplayName: z4.string().optional(),
174
- connectorLogoUrl: z4.string().optional()
360
+ triggerDisplayName: z5.string().optional(),
361
+ connectorLogoUrl: z5.string().optional()
175
362
  });
176
- var runSchema = z4.object({
177
- id: z4.uuid(),
363
+ var runSchema = z5.object({
364
+ id: z5.uuid(),
178
365
  status: runStatusSchema,
179
366
  trigger: runTriggerSchema,
180
- createdAt: z4.iso.datetime(),
181
- startedAt: z4.iso.datetime().nullable(),
182
- finishedAt: z4.iso.datetime().nullable(),
183
- durationMs: z4.number().int().nullable(),
184
- traceId: z4.string().nullable(),
367
+ createdAt: z5.iso.datetime(),
368
+ startedAt: z5.iso.datetime().nullable(),
369
+ finishedAt: z5.iso.datetime().nullable(),
370
+ durationMs: z5.number().int().nullable(),
371
+ traceId: z5.string().nullable(),
185
372
  // True when the run was triggered from the Studio Playground (header
186
373
  // `STACKBONE_PLAYGROUND_HEADER` on `POST /api/invoke`). Billing and the Trust
187
374
  // Layer's verified metrics filter on this column to exclude stress-tests.
188
- isPlayground: z4.boolean(),
375
+ isPlayground: z5.boolean(),
189
376
  // Name of the agent or workflow this run belongs to (migration 0020). Stamped
190
377
  // at run creation: the agent name for `chat` runs, the workflow name for
191
378
  // `workflow` runs. Combined with `trigger` the UI knows the kind. Null for
192
379
  // classic/manual cloud runs (deep-agent chat is dev-only), so the Runs screen renders `—`.
193
- entityName: z4.string().nullable(),
380
+ entityName: z5.string().nullable(),
194
381
  // Which KIND of entity `entityName` names (STA-426, migration 0037). Stamped at
195
382
  // run creation next to the name, so an agent and a workflow that share a name stop
196
383
  // sharing one row set. Null for a run written before the column existed (no
@@ -205,22 +392,22 @@ var runSchema = z4.object({
205
392
  // `trigger_subscriptions.id`, stamped at start for `trigger: 'event'` and null
206
393
  // for every other gate. Optional as well as nullable for the reason `entityKind`
207
394
  // is: an older box does not send the field.
208
- triggerRef: z4.string().nullable().optional(),
395
+ triggerRef: z5.string().nullable().optional(),
209
396
  // The Connect link that started or resumed the run, with its names (PLA-1898).
210
397
  // Null for a run no link started; optional for the reason `entityKind` is.
211
398
  triggerLink: runTriggerLinkSchema.nullable().optional(),
212
399
  input: jsonObject().nullable(),
213
400
  output: jsonObject().nullable(),
214
- error: z4.object({ message: z4.string(), stack: z4.string().optional() }).nullable(),
401
+ error: z5.object({ message: z5.string(), stack: z5.string().optional() }).nullable(),
215
402
  // Per-turn token usage for deep-agent CHAT runs (feature 68, A5): the chat
216
403
  // recorder folds the turn's usage and stamps the four token counters onto the run.
217
404
  // Null when the run had no model call; `totalTokens` is derived at read
218
405
  // (`input + output`), never stored. No $ cost (D5).
219
- inputTokens: z4.number().int().nullable(),
220
- outputTokens: z4.number().int().nullable(),
221
- cacheReadTokens: z4.number().int().nullable(),
222
- cacheWriteTokens: z4.number().int().nullable(),
223
- totalTokens: z4.number().int().nullable(),
406
+ inputTokens: z5.number().int().nullable(),
407
+ outputTokens: z5.number().int().nullable(),
408
+ cacheReadTokens: z5.number().int().nullable(),
409
+ cacheWriteTokens: z5.number().int().nullable(),
410
+ totalTokens: z5.number().int().nullable(),
224
411
  /**
225
412
  * The run's own `metadata` jsonb, verbatim (PLA-1272 slice 4).
226
413
  *
@@ -241,37 +428,37 @@ var runSchema = z4.object({
241
428
  */
242
429
  metadata: jsonObject().nullable().optional()
243
430
  });
244
- var schemaIssueSchema = z4.object({
431
+ var schemaIssueSchema = z5.object({
245
432
  /** Path segments to the offending field (`[]` for the root value). */
246
- path: z4.array(z4.union([z4.string(), z4.number()])),
247
- message: z4.string()
433
+ path: z5.array(z5.union([z5.string(), z5.number()])),
434
+ message: z5.string()
248
435
  });
249
- var runOutputViolationSchema = z4.object({
436
+ var runOutputViolationSchema = z5.object({
250
437
  /** The workflow whose declared `outputSchema` the return value broke. */
251
- workflowName: z4.string(),
252
- issues: z4.array(schemaIssueSchema),
438
+ workflowName: z5.string(),
439
+ issues: z5.array(schemaIssueSchema),
253
440
  /**
254
441
  * When the projector observed it (ISO). Not the run's `finishedAt`: the run
255
442
  * closed at that instant whether or not anything validated it.
256
443
  */
257
- recordedAt: z4.string()
444
+ recordedAt: z5.string()
258
445
  });
259
- var runIdParamSchema = z4.object({ runId: z4.uuid() });
260
- var runTagKeySchema = z4.string().regex(/^[a-z][a-z0-9_.-]{0,63}$/);
446
+ var runIdParamSchema = z5.object({ runId: z5.uuid() });
447
+ var runTagKeySchema = z5.string().regex(/^[a-z][a-z0-9_.-]{0,63}$/);
261
448
  var RUN_TAG_VALUE_MAX = 256;
262
449
  var RUN_TAGS_MAX = 16;
263
450
  var UNSTORABLE_TAG_CHAR = (
264
451
  // eslint-disable-next-line no-control-regex
265
452
  /[\u0000-\u001f\u007f]|[\ud800-\udbff](?![\udc00-\udfff])|(?<![\ud800-\udbff])[\udc00-\udfff]/
266
453
  );
267
- var runTagValueSchema = z4.string().min(1).max(RUN_TAG_VALUE_MAX).refine((value) => !UNSTORABLE_TAG_CHAR.test(value), {
454
+ var runTagValueSchema = z5.string().min(1).max(RUN_TAG_VALUE_MAX).refine((value) => !UNSTORABLE_TAG_CHAR.test(value), {
268
455
  message: "A tag value holds no control character and no lone surrogate."
269
456
  });
270
457
  var splitRunTagFilter = (tag) => {
271
458
  const colon = tag.indexOf(":");
272
459
  return colon < 0 ? null : { key: tag.slice(0, colon), value: tag.slice(colon + 1) };
273
460
  };
274
- var runTagFilterSchema = z4.string().superRefine((tag, ctx) => {
461
+ var runTagFilterSchema = z5.string().superRefine((tag, ctx) => {
275
462
  const filter = splitRunTagFilter(tag);
276
463
  if (!filter) {
277
464
  ctx.addIssue({ code: "custom", message: "A tag filter is `key:value`." });
@@ -293,17 +480,17 @@ var runTagFilterSchema = z4.string().superRefine((tag, ctx) => {
293
480
  var listRunsQuerySchema = pageQuerySchema.extend({
294
481
  status: runStatusSchema.optional(),
295
482
  trigger: runTriggerSchema.optional(),
296
- from: z4.iso.datetime().optional(),
297
- to: z4.iso.datetime().optional(),
483
+ from: z5.iso.datetime().optional(),
484
+ to: z5.iso.datetime().optional(),
298
485
  // Feature 69 — the Sessions detail screen reads a conversation's turns with
299
486
  // `GET /runs?session_id=<id>&order=asc`. `session_id` is the real
300
487
  // `runs.session_id` join column (the deterministic UUIDv5 of the deep-agent
301
488
  // session key), so it is exact-match on the indexed column.
302
- session_id: z4.uuid().optional(),
489
+ session_id: z5.uuid().optional(),
303
490
  // Migration 0020 — the Runs screen filters to one agent/workflow by its
304
491
  // exact name (the value picked from the name dropdown, or deep-linked from
305
492
  // the agent/workflow detail's "View runs"). Exact-match on `runs.entity_name`.
306
- name: z4.string().min(1).optional(),
493
+ name: z5.string().min(1).optional(),
307
494
  // STA-426 — narrow the name filter to ONE kind of entity, so "the runs of the
308
495
  // workflow named sync" excludes the agent that shares the name. A row that
309
496
  // DECLARES the other kind is always excluded; a row whose kind was never stamped
@@ -317,8 +504,8 @@ var listRunsQuerySchema = pageQuerySchema.extend({
317
504
  // or boolean matches its JSON spelling (`3`, `true`). The ONE filter here that is
318
505
  // not exact-match: it serves a search box, and a search box that needs the whole
319
506
  // address typed without a typo is not one. Nested paths are out of scope.
320
- input_key: z4.string().min(1).optional(),
321
- input_value: z4.string().min(1).optional(),
507
+ input_key: z5.string().min(1).optional(),
508
+ input_value: z5.string().min(1).optional(),
322
509
  // PLA-1785 — `exclude` leaves out the runs of the platform's own entries
323
510
  // (`studio-copilot`, `mapping-suggest`, `rag-ingest`), decided by the box's
324
511
  // `isSystemEntry` and applied before the cursor and the LIMIT, so a page stays
@@ -338,179 +525,43 @@ var listRunsQuerySchema = pageQuerySchema.extend({
338
525
  path: ["input_key"]
339
526
  });
340
527
  var runListResponseSchema = pageResponseSchema(runSchema);
341
- var runEntityEntrySchema = z4.object({
342
- name: z4.string(),
528
+ var runEntityEntrySchema = z5.object({
529
+ name: z5.string(),
343
530
  kind: runEntityKindSchema.nullable()
344
531
  });
345
- var runEntityNamesResponseSchema = z4.object({
346
- names: z4.array(z4.string()),
347
- entries: z4.array(runEntityEntrySchema).optional()
532
+ var runEntityNamesResponseSchema = z5.object({
533
+ names: z5.array(z5.string()),
534
+ entries: z5.array(runEntityEntrySchema).optional()
348
535
  });
349
- var cancelRunResponseSchema = z4.object({
536
+ var cancelRunResponseSchema = z5.object({
350
537
  run: runSchema
351
538
  });
352
539
  var STACKBONE_SESSION_HEADER = "x-stackbone-session";
353
540
  var RUN_TAGS_HEADER = "x-stackbone-run-tags";
354
- var fixtureSchema = z4.object({
355
- id: z4.string(),
356
- name: z4.string(),
541
+ var fixtureSchema = z5.object({
542
+ id: z5.string(),
543
+ name: z5.string(),
357
544
  input: jsonObject().nullable(),
358
- description: z4.string().nullable(),
359
- createdAt: z4.iso.datetime()
545
+ description: z5.string().nullable(),
546
+ createdAt: z5.iso.datetime()
360
547
  });
361
- var createFixtureRequestSchema = z4.object({
362
- name: z4.string().min(1).max(120),
548
+ var createFixtureRequestSchema = z5.object({
549
+ name: z5.string().min(1).max(120),
363
550
  input: jsonObject(),
364
- description: z4.string().max(2e3).optional().nullable()
551
+ description: z5.string().max(2e3).optional().nullable()
365
552
  });
366
553
  var fixtureListResponseSchema = pageResponseSchema(fixtureSchema);
367
- var fixtureIdParamSchema = z4.object({ fxtId: z4.uuid() });
368
- var handlerSchemaSchema = z4.object({
554
+ var fixtureIdParamSchema = z5.object({ fxtId: z5.uuid() });
555
+ var handlerSchemaSchema = z5.object({
369
556
  input: jsonObject().nullable(),
370
557
  output: jsonObject().nullable()
371
558
  });
372
- var schemaResponseSchema = z4.object({
559
+ var schemaResponseSchema = z5.object({
373
560
  input: jsonObject().nullable(),
374
561
  output: jsonObject().nullable(),
375
562
  handlers: jsonObject().nullable().optional()
376
563
  });
377
564
 
378
- // libs/shared/validators/src/lib/studio/studio-model-provider.ts
379
- import { z as z5 } from "zod";
380
- var modelProviderSlotSchema = z5.enum(["baseUrl", "apiKey"]);
381
- var MODEL_PROVIDER_SLOTS = {
382
- baseUrl: ["MODEL_PROVIDER_BASE_URL", "OPENROUTER_BASE_URL"],
383
- apiKey: ["MODEL_PROVIDER_API_KEY", "OPENROUTER_API_KEY"]
384
- };
385
- var SLOT_NAMES = Object.keys(MODEL_PROVIDER_SLOTS);
386
- var MODEL_PROVIDER_DEPRECATED_ENV_NAMES = SLOT_NAMES.map(
387
- (slot) => MODEL_PROVIDER_SLOTS[slot][1]
388
- );
389
- var modelProviderSourceSchema = z5.enum(["environment", "stored", "none"]);
390
- var isModelProviderValuePresent = (value) => typeof value === "string" && value.trim() !== "";
391
- var storedOverridesSchema = z5.strictObject({
392
- baseUrl: z5.boolean().optional(),
393
- apiKey: z5.boolean().optional()
394
- });
395
- var modelProviderKindSchema = z5.enum(["openrouter", "ollama", "lmstudio", "generic"]);
396
- var modelProviderGetResponseSchema = z5.object({
397
- configured: z5.boolean(),
398
- base_url: z5.string().nullable(),
399
- api_key_masked: z5.string().nullable(),
400
- source: modelProviderSourceSchema.optional(),
401
- env_override: z5.array(z5.string()).optional(),
402
- effective_base_url: z5.string().nullable().optional(),
403
- // The gateway kind of the endpoint IN EFFECT (the one `effective_base_url`
404
- // names), resolved by the box; `null` when nothing is configured. Optional for
405
- // the same version-skew reason as `source`: a box that predates the field.
406
- kind: modelProviderKindSchema.nullable().optional(),
407
- // The slots where a Studio override beats an exported value (PLA-1825, ADR
408
- // D8), by SLOT name, never the variable's (D4). Such a slot drops out of
409
- // `env_override`, which keeps naming only the variables that actually win.
410
- // A box that knows overrides always sends the list, empty or not; an absent
411
- // list is a box that predates them, and would drop an `override` it cannot
412
- // read, so Studio offers no Override against it.
413
- overridden: z5.array(modelProviderSlotSchema).optional()
414
- });
415
- var modelProviderPutRequestSchema = z5.object({
416
- base_url: z5.url(),
417
- api_key: z5.string().optional().nullable(),
418
- kind: modelProviderKindSchema.optional(),
419
- override: z5.object({ baseUrl: z5.boolean().optional(), apiKey: z5.boolean().optional() }).optional()
420
- }).refine((body) => body.override?.apiKey !== true || isModelProviderValuePresent(body.api_key), {
421
- message: "Overriding the API key needs the key to use.",
422
- path: ["api_key"]
423
- });
424
- var modelProviderAgentsOutcomeSchema = z5.object({
425
- /** Agents that were degraded and are now serving. */
426
- ready: z5.number().int().nonnegative(),
427
- /** Agents that are still degraded (their reason was refreshed by the retry). */
428
- degraded: z5.number().int().nonnegative()
429
- });
430
- var modelProviderPutResponseSchema = modelProviderGetResponseSchema.extend({
431
- agents: modelProviderAgentsOutcomeSchema.optional()
432
- });
433
- var modelProviderFirstRunResponseSchema = z5.object({
434
- completed: z5.boolean()
435
- });
436
- var modelProviderKeyCheckSchema = z5.enum(["verified", "unverified"]);
437
- var modelProviderTestResponseSchema = z5.object({
438
- ok: z5.boolean(),
439
- model_count: z5.number().int().nonnegative().optional(),
440
- error: z5.string().optional(),
441
- key_checked: modelProviderKeyCheckSchema.optional()
442
- });
443
- var modelProviderModelTypeSchema = z5.enum(["chat", "image", "embedding", "other"]);
444
- var modelProviderModelSchema = z5.object({
445
- id: z5.string().min(1),
446
- name: z5.string().optional(),
447
- context_length: z5.number().int().positive().optional(),
448
- pricing: z5.object({
449
- prompt: z5.string().optional(),
450
- completion: z5.string().optional()
451
- }).optional(),
452
- owned_by: z5.string().optional(),
453
- type: modelProviderModelTypeSchema.optional(),
454
- // What the gateway says the model can do (PLA-1605): `true`, `false`, or
455
- // `null` for "this gateway does not say". The three values are distinct on
456
- // purpose — Studio hides a model that reports no tool support and only WARNS
457
- // about one that reports nothing, so collapsing `null` into `false` would hide
458
- // every model on a custom OpenAI-compatible endpoint.
459
- //
460
- // Nullable AND optional: a box on an older runtime sends neither field, and
461
- // that must keep validating, exactly as `type` already does.
462
- tools: z5.boolean().nullable().optional(),
463
- reasoning: z5.boolean().nullable().optional(),
464
- // STA3-17: on a box with several providers, the one that serves this entry.
465
- // `responding: false` marks an entry remembered from a provider that is down.
466
- // Declared so the parse keeps it: an object schema strips what it does not name.
467
- provider: z5.object({ id: z5.string(), name: z5.string(), responding: z5.boolean() }).optional()
468
- });
469
- var modelProviderModelsRequestSchema = z5.object({
470
- base_url: z5.url().optional().nullable(),
471
- api_key: z5.string().optional().nullable()
472
- });
473
- var modelProviderModelsResponseSchema = z5.object({
474
- ok: z5.boolean(),
475
- models: z5.array(modelProviderModelSchema).optional(),
476
- error: z5.string().optional(),
477
- warning: z5.string().optional()
478
- });
479
- var MODEL_PROVIDER_DEFAULT_ID = "default";
480
- var MODEL_PROVIDER_ID_PATTERN = /^[a-z][a-z0-9-]{0,30}[a-z0-9]$/;
481
- var modelProviderIdSchema = z5.string().regex(MODEL_PROVIDER_ID_PATTERN, {
482
- message: "Lowercase letters, digits and dashes, 2 to 32, starting with a letter."
483
- }).refine((id) => id !== MODEL_PROVIDER_DEFAULT_ID, {
484
- message: `'${MODEL_PROVIDER_DEFAULT_ID}' is provider 1's id.`
485
- });
486
- var pickedModelProviderIdSchema = z5.string().regex(MODEL_PROVIDER_ID_PATTERN);
487
- var modelProviderListItemSchema = z5.object({
488
- id: z5.string(),
489
- name: z5.string(),
490
- kind: modelProviderKindSchema,
491
- baseUrl: z5.string(),
492
- apiKey: z5.string().nullable(),
493
- source: z5.enum(["env", "local"]),
494
- removable: z5.boolean(),
495
- responding: z5.boolean().nullable(),
496
- error: z5.string().optional()
497
- });
498
- var modelProviderListResponseSchema = z5.object({
499
- providers: z5.array(modelProviderListItemSchema),
500
- duplicatedIds: z5.array(z5.object({ modelId: z5.string(), providerIds: z5.array(z5.string()) }))
501
- });
502
- var providerFields = {
503
- name: z5.string().trim().min(1),
504
- kind: modelProviderKindSchema,
505
- baseUrl: z5.url(),
506
- apiKey: z5.string().optional()
507
- };
508
- var modelProviderCreateRequestSchema = z5.strictObject({
509
- id: modelProviderIdSchema,
510
- ...providerFields
511
- });
512
- var modelProviderUpdateRequestSchema = z5.strictObject(providerFields);
513
-
514
565
  // libs/shared/validators/src/lib/studio/studio-voice.ts
515
566
  import { z as z8 } from "zod";
516
567
 
@@ -558,9 +609,22 @@ var ragCollectionNameParamSchema = z7.string().min(1).max(128).regex(STUDIO_RAG_
558
609
  var ragDistanceSchema = z7.enum(["cosine", "l2", "ip"]);
559
610
  var ragIndexKindSchema = z7.enum(["hnsw", "exact"]);
560
611
  var ragNumberFormatSchema = z7.enum(["eu", "us"]);
612
+ var ragRangeFieldSchema = z7.string().min(1).refine((field) => !field.includes("."), {
613
+ message: "field must be a literal top-level metadata key without dots"
614
+ });
615
+ var ragExtractFieldSchema = z7.object({
616
+ name: z7.string().trim().pipe(ragRangeFieldSchema),
617
+ type: z7.enum(["number", "date", "text"]),
618
+ removed: z7.boolean().optional()
619
+ });
620
+ var uniqueFieldNames = (fields) => new Set(fields.map((field) => field.name)).size === fields.length;
621
+ var ragExtractFieldsRequestSchema = z7.array(ragExtractFieldSchema).refine(uniqueFieldNames, {
622
+ message: "Field names must be unique"
623
+ });
561
624
  var ragCollectionSchema = z7.object({
562
625
  name: z7.string(),
563
626
  dimensions: z7.number().int().min(1),
627
+ embedding_state: z7.enum(["ready", "rebuilding", "failed"]).optional(),
564
628
  // Optional for boxes that predate collection-owned embeddings.
565
629
  embedding_provider: z7.string().min(1).optional(),
566
630
  embedding_model: z7.string().min(1).optional(),
@@ -574,7 +638,9 @@ var ragCollectionSchema = z7.object({
574
638
  last_ingested_at: z7.iso.datetime().nullable(),
575
639
  // STA3-57. Optional so a list from a box older than hybrid search still parses.
576
640
  hybrid_enabled: z7.boolean().optional(),
577
- number_format: ragNumberFormatSchema.optional()
641
+ number_format: ragNumberFormatSchema.optional(),
642
+ extract_enabled: z7.boolean().optional(),
643
+ extract_fields: z7.array(ragExtractFieldSchema).optional()
578
644
  });
579
645
  var ragCollectionListResponseSchema = z7.object({
580
646
  items: z7.array(ragCollectionSchema)
@@ -621,7 +687,7 @@ var ragDocumentSchema = z7.object({
621
687
  pending_version: z7.number().int().min(1).nullable().optional()
622
688
  });
623
689
  var ragFilterSchema = jsonObject();
624
- var ragDateBoundSchema = z7.string().length(10).regex(/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/).refine((value) => {
690
+ var ragDateValueSchema = z7.string().length(10).regex(/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/).refine((value) => {
625
691
  const year = Number(value.slice(0, 4));
626
692
  const month = Number(value.slice(5, 7));
627
693
  const day = Number(value.slice(8, 10));
@@ -629,9 +695,6 @@ var ragDateBoundSchema = z7.string().length(10).regex(/^[0-9]{4}-[0-9]{2}-[0-9]{
629
695
  date.setUTCFullYear(year, month - 1, day);
630
696
  return year >= 1 && year <= 9999 && date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day;
631
697
  }, "date must be a real YYYY-MM-DD day in years 0001\u20139999");
632
- var ragRangeFieldSchema = z7.string().min(1).refine((field) => !field.includes("."), {
633
- message: "field must be a literal top-level metadata key without dots"
634
- });
635
698
  var ragRangeSchema = z7.discriminatedUnion("type", [
636
699
  z7.strictObject({
637
700
  field: ragRangeFieldSchema,
@@ -644,10 +707,10 @@ var ragRangeSchema = z7.discriminatedUnion("type", [
644
707
  z7.strictObject({
645
708
  field: ragRangeFieldSchema,
646
709
  type: z7.literal("date"),
647
- gt: ragDateBoundSchema.optional(),
648
- gte: ragDateBoundSchema.optional(),
649
- lt: ragDateBoundSchema.optional(),
650
- lte: ragDateBoundSchema.optional()
710
+ gt: ragDateValueSchema.optional(),
711
+ gte: ragDateValueSchema.optional(),
712
+ lt: ragDateValueSchema.optional(),
713
+ lte: ragDateValueSchema.optional()
651
714
  })
652
715
  ]).superRefine((range, ctx) => {
653
716
  const lower = range.gt ?? range.gte;
@@ -690,6 +753,32 @@ var ragDocumentVersionListResponseSchema = z7.object({
690
753
  items: z7.array(ragDocumentVersionSchema)
691
754
  });
692
755
  var ragDocumentVersionNumberSchema = z7.number().int().min(1).max(2147483647);
756
+ var updateRagDocumentValuesRequestSchema = z7.strictObject({
757
+ version: ragDocumentVersionNumberSchema.optional(),
758
+ values: jsonObject(),
759
+ add_fields: z7.array(ragExtractFieldSchema.omit({ removed: true })).refine(uniqueFieldNames, {
760
+ message: "Field names must be unique"
761
+ }).optional()
762
+ });
763
+ var updateRagDocumentValuesResponseSchema = z7.object({
764
+ id: z7.string(),
765
+ collection: z7.string(),
766
+ version: ragDocumentVersionNumberSchema,
767
+ metadata: jsonObject()
768
+ });
769
+ var extractRagDocumentRequestSchema = z7.strictObject({
770
+ version: ragDocumentVersionNumberSchema.optional()
771
+ });
772
+ var extractRagDocumentResponseSchema = z7.object({
773
+ proposal: z7.array(
774
+ ragExtractFieldSchema.omit({ removed: true }).extend({
775
+ value: z7.union([z7.number().finite(), z7.string()]),
776
+ new: z7.boolean()
777
+ })
778
+ ),
779
+ failed: z7.boolean(),
780
+ truncated: z7.boolean()
781
+ });
693
782
  var ragDocumentVersionParamSchema = z7.string().regex(/^\d+$/, { message: "version must be a positive integer" }).transform((value) => Number.parseInt(value, 10)).pipe(ragDocumentVersionNumberSchema);
694
783
  var activateRagDocumentVersionResponseSchema = z7.object({
695
784
  id: z7.string(),
@@ -721,6 +810,8 @@ var ragQueryHitSchema = z7.object({
721
810
  // The reciprocal rank fusion score a hybrid search ordered by (STA3-57 D10).
722
811
  // Absent when only the search by meaning ran, and from a box older than it.
723
812
  mixed_score: z7.number().optional(),
813
+ dense_rank: z7.number().int().positive().optional(),
814
+ lexical_rank: z7.number().int().positive().optional(),
724
815
  content: z7.string(),
725
816
  metadata: jsonObject()
726
817
  });
@@ -767,7 +858,9 @@ var createRagCollectionRequestSchema = z7.object({
767
858
  embedding_model: z7.string().trim().min(1).optional(),
768
859
  // Omitted, the box creates the collection with hybrid search off and `eu`.
769
860
  hybrid_enabled: z7.boolean().optional(),
770
- number_format: ragNumberFormatSchema.optional()
861
+ number_format: ragNumberFormatSchema.optional(),
862
+ extract_enabled: z7.boolean().optional(),
863
+ extract_fields: ragExtractFieldsRequestSchema.optional()
771
864
  }).refine(
772
865
  (value) => value.embedding_provider === void 0 === (value.embedding_model === void 0),
773
866
  {
@@ -777,14 +870,19 @@ var createRagCollectionRequestSchema = z7.object({
777
870
  );
778
871
  var updateRagCollectionRequestSchema = z7.strictObject({
779
872
  hybrid_enabled: z7.boolean().optional(),
780
- number_format: ragNumberFormatSchema.optional()
781
- }).refine((value) => value.hybrid_enabled !== void 0 || value.number_format !== void 0, {
782
- message: "Send hybrid_enabled, number_format or both"
873
+ number_format: ragNumberFormatSchema.optional(),
874
+ extract_enabled: z7.boolean().optional(),
875
+ extract_fields: ragExtractFieldsRequestSchema.optional()
876
+ }).refine((value) => Object.values(value).some((setting) => setting !== void 0), {
877
+ message: "Send at least one collection setting"
783
878
  });
784
879
  var updateRagCollectionResponseSchema = z7.object({
785
880
  name: z7.string(),
786
881
  hybrid_enabled: z7.boolean(),
787
- number_format: ragNumberFormatSchema
882
+ number_format: ragNumberFormatSchema,
883
+ extract_enabled: z7.boolean().optional(),
884
+ extract_fields: z7.array(ragExtractFieldSchema).optional(),
885
+ unconverted: z7.number().int().nonnegative().optional()
788
886
  });
789
887
  var deleteRagCollectionResponseSchema = z7.object({
790
888
  name: z7.string(),
@@ -857,6 +955,14 @@ var VOICE_TTS_MODELS = [
857
955
  "gradium/default"
858
956
  ];
859
957
  var isCartesiaTtsModel = (model) => model.startsWith("cartesia/") && VOICE_TTS_MODELS.includes(model);
958
+ var VOICE_BACKGROUND_CLIPS = [
959
+ "office-ambience",
960
+ "keyboard-typing",
961
+ "keyboard-typing2",
962
+ "hold-music"
963
+ ];
964
+ var VOICE_AMBIENT_SOUNDS = ["none", ...VOICE_BACKGROUND_CLIPS];
965
+ var VOICE_THINKING_SOUNDS = ["none", "chime", ...VOICE_BACKGROUND_CLIPS];
860
966
  var thresholdSchema = z8.number().min(0).max(1);
861
967
  var millisecondsSchema = z8.int().min(0).max(1e4);
862
968
  var keytermsSchema = z8.array(z8.string().trim().min(1).max(64)).max(50);
@@ -881,7 +987,21 @@ var ragSchema = z8.strictObject({
881
987
  minScore: z8.number().min(0).max(1),
882
988
  minWords: z8.int().min(0).max(20),
883
989
  topK: z8.int().min(1).max(10),
884
- timeoutMs: z8.int().min(200).max(1e4)
990
+ timeoutMs: z8.int().min(200).max(1e4),
991
+ previousExchanges: z8.int().min(0).max(5)
992
+ });
993
+ var volumeSchema = z8.number().min(0).max(1);
994
+ var backgroundAudioSchema = z8.strictObject({
995
+ ambientSound: z8.enum(VOICE_AMBIENT_SOUNDS),
996
+ ambientVolume: volumeSchema,
997
+ thinkingSound: z8.enum(VOICE_THINKING_SOUNDS),
998
+ thinkingVolume: volumeSchema
999
+ });
1000
+ var VOICE_BACKGROUND_AUDIO_DEFAULTS = Object.freeze({
1001
+ ambientSound: "office-ambience",
1002
+ ambientVolume: 0.7,
1003
+ thinkingSound: "keyboard-typing",
1004
+ thinkingVolume: 0.6
885
1005
  });
886
1006
  var effectiveVoiceSettingsObjectSchema = z8.strictObject({
887
1007
  /** BCP-47-ish tag the STT is hinted with and the TTS speaks (`es`, `en-US`), or `multi`. */
@@ -909,26 +1029,18 @@ var effectiveVoiceSettingsObjectSchema = z8.strictObject({
909
1029
  phoneLine: z8.boolean(),
910
1030
  /** LiveKit expressive mode: the turn gets Cartesia's markup guide (D5). Cartesia only. */
911
1031
  expressive: z8.boolean(),
1032
+ backgroundAudio: backgroundAudioSchema,
912
1033
  /**
913
1034
  * Whether the box records the call to its bucket (D30). Off, no egress starts: a
914
1035
  * box may not be allowed to keep a caller's audio. The box alone reads it, so the
915
1036
  * dispatch to the worker leaves it out (`callVoiceSettingsSchema`).
916
1037
  */
917
1038
  recording: z8.boolean(),
918
- /**
919
- * Ask OpenRouter to route this agent's model calls to the provider with the
920
- * highest throughput (`provider: { sort: 'throughput' }`) instead of its default
921
- * price routing. Gemma 4 31B on price routing landed on a 13 tok/s provider (5 s
922
- * a turn); throughput routing answered in about 1 s. A request field, not a
923
- * `:nitro` model suffix, so the model id the box checks and prices stays the
924
- * one written. The box strips it off any other gateway. The SDK applies it when
925
- * it builds the graph, so the worker never reads it.
926
- */
927
- fastModelRouting: z8.boolean(),
928
1039
  /** The box runs the search before the model's turn, so the worker never reads it. */
929
1040
  rag: ragSchema,
930
1041
  /**
931
- * What the agent says when an input guardrail stops the caller's sentence (FDE-66):
1042
+ * What the agent says when a guardrail stops the caller's sentence (FDE-66) or the
1043
+ * agent's reply (STA3-59):
932
1044
  * a policy message written for a chat reader would leak the rule if a TTS read it.
933
1045
  * The box answers the turn itself, so the worker never reads it. Not nullable: a
934
1046
  * blocked turn always says something.
@@ -963,9 +1075,10 @@ var checkCrossFieldRules = (value, ctx) => {
963
1075
  var effectiveVoiceSettingsSchema = effectiveVoiceSettingsObjectSchema.superRefine(checkCrossFieldRules);
964
1076
  var callVoiceSettingsObjectSchema = effectiveVoiceSettingsObjectSchema.omit({
965
1077
  recording: true,
966
- fastModelRouting: true,
967
1078
  rag: true,
968
1079
  refusalPhrase: true
1080
+ }).extend({
1081
+ backgroundAudio: backgroundAudioSchema.default(() => ({ ...VOICE_BACKGROUND_AUDIO_DEFAULTS }))
969
1082
  });
970
1083
  var callVoiceSettingsSchema = callVoiceSettingsObjectSchema.superRefine(checkCrossFieldRules);
971
1084
  var VOICE_SETTINGS_LEAF_PATHS = [
@@ -988,13 +1101,17 @@ var VOICE_SETTINGS_LEAF_PATHS = [
988
1101
  "greeting",
989
1102
  "phoneLine",
990
1103
  "expressive",
1104
+ "backgroundAudio.ambientSound",
1105
+ "backgroundAudio.ambientVolume",
1106
+ "backgroundAudio.thinkingSound",
1107
+ "backgroundAudio.thinkingVolume",
991
1108
  "recording",
992
- "fastModelRouting",
993
1109
  "rag.collection",
994
1110
  "rag.minScore",
995
1111
  "rag.minWords",
996
1112
  "rag.topK",
997
1113
  "rag.timeoutMs",
1114
+ "rag.previousExchanges",
998
1115
  "refusalPhrase"
999
1116
  ];
1000
1117
  var voiceSettingOriginSchema = z8.enum(["studio", "code", "default"]);
@@ -1020,7 +1137,8 @@ var voiceSettingsSchema = effectiveVoiceSettingsObjectSchema.extend({
1020
1137
  vad: vadSchema.partial(),
1021
1138
  turn: turnSchema.partial(),
1022
1139
  interruptions: interruptionsSchema.partial(),
1023
- rag: ragSchema.partial()
1140
+ rag: ragSchema.partial(),
1141
+ backgroundAudio: backgroundAudioSchema.partial()
1024
1142
  }).partial().superRefine(checkCrossFieldRules).superRefine(checkTtsPair);
1025
1143
  var checkStudioTtsPair = (value, ctx) => {
1026
1144
  if (value.ttsVoice !== void 0 && value.ttsModel === void 0) {
@@ -1058,6 +1176,7 @@ var VOICE_TRACES_PATH = "/voice/v1/traces";
1058
1176
  var VOICE_WORKER_AGENT_NAME = "stackbone-voice";
1059
1177
  var VOICE_WORKER_AGENT_NAME_ENV = "STACKBONE_VOICE_AGENT_NAME";
1060
1178
  var VOICE_WORKER_PORT_ENV = "STACKBONE_VOICE_WORKER_PORT";
1179
+ var VOICE_TURN_METRICS_MESSAGE = "voice turn metrics";
1061
1180
  var VOICE_CALL_TOKEN_MAX = 2048;
1062
1181
  var voiceDispatchSchema = z8.strictObject({
1063
1182
  agent: deepAgentNameSchema,
@@ -1122,15 +1241,34 @@ var voiceCallTranscriptSchema = z8.array(
1122
1241
  ).max(1e3).refine((items) => items.reduce((size, item) => size + item.text.length, 0) <= 2e5, {
1123
1242
  message: "A call transcript may contain at most 200000 characters."
1124
1243
  });
1244
+ var VOICE_CALL_TURNS_MAX = 1e3;
1245
+ var AGUI_RUN_ID_MAX = 200;
1246
+ var VOICE_TURN_LATENCY_MAX_MS = 36e5;
1247
+ var turnLatencyMs = z8.number().int().min(0).max(VOICE_TURN_LATENCY_MAX_MS).optional();
1248
+ var voiceCallTurnMetricsSchema = z8.strictObject({
1249
+ /** The AG-UI `runId` of the box run that answered; absent on a reply the box did not run. */
1250
+ runId: z8.string().min(1).max(AGUI_RUN_ID_MAX).optional(),
1251
+ endOfTurnMs: turnLatencyMs,
1252
+ transcriptionMs: turnLatencyMs,
1253
+ llmTtftMs: turnLatencyMs,
1254
+ ttsTtfbMs: turnLatencyMs,
1255
+ e2eMs: turnLatencyMs
1256
+ });
1257
+ var voiceCallTurnsSchema = z8.array(voiceCallTurnMetricsSchema).max(VOICE_CALL_TURNS_MAX);
1125
1258
  var voiceCallCompleteSchema = z8.strictObject({
1126
1259
  transcript: voiceCallTranscriptSchema,
1127
- outcome: z8.enum(["completed", "disconnected", "failed"])
1260
+ outcome: z8.enum(["completed", "disconnected", "failed"]),
1261
+ // The transcript, the memory and the `onEnd` start this body carries must not be
1262
+ // refused over a bad latency, so `turns` is any value here and read apart through
1263
+ // `readVoiceCallTurns`. Not `.catch()` on the array: this schema is a documented
1264
+ // route's body, and the OpenAPI generator cannot describe a catch (STA3-56).
1265
+ turns: z8.unknown().optional()
1128
1266
  });
1129
1267
  var voiceCallLifecycleResponseSchema = z8.strictObject({ ok: z8.literal(true) });
1130
1268
 
1131
1269
  // libs/runtime/voice-worker/src/box-llm.ts
1132
1270
  import { randomUUID } from "node:crypto";
1133
- import { DEFAULT_API_CONNECT_OPTIONS, llm, log } from "@livekit/agents";
1271
+ import { DEFAULT_API_CONNECT_OPTIONS, llm as llm2, log } from "@livekit/agents";
1134
1272
 
1135
1273
  // libs/shared/llm-wire/src/lib/sse/read-sse-data.ts
1136
1274
  async function* readSseData(body) {
@@ -1175,8 +1313,9 @@ var BoxTurnError = class extends Error {
1175
1313
  }
1176
1314
  };
1177
1315
  var boxEndpoint = (boxUrl, path) => `${boxUrl.replace(/\/+$/, "")}${path}`;
1316
+ var mintAguiRunId = () => `run-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
1178
1317
  async function* aguiTurn(opts) {
1179
- const runId = `run-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
1318
+ const runId = opts.runId ?? mintAguiRunId();
1180
1319
  const heard = "toolResult" in opts ? void 0 : opts.heard;
1181
1320
  const message = "toolResult" in opts ? {
1182
1321
  id: `tool-${opts.toolResult.toolCallId}`,
@@ -1456,8 +1595,45 @@ var lastNewAssistantMessage = (chatCtx, seen) => {
1456
1595
  return void 0;
1457
1596
  };
1458
1597
 
1598
+ // libs/runtime/voice-worker/src/turn-metrics.ts
1599
+ import { llm, voice as voice2 } from "@livekit/agents";
1600
+ var AGUI_RUN_ID_EXTRA = "stackboneAguiRunId";
1601
+ var ms = (seconds) => {
1602
+ if (typeof seconds !== "number" || !Number.isFinite(seconds)) return void 0;
1603
+ const value = Math.round(seconds * 1e3);
1604
+ return value >= 0 && value <= VOICE_TURN_LATENCY_MAX_MS ? value : void 0;
1605
+ };
1606
+ var defined = (metrics) => Object.fromEntries(Object.entries(metrics).filter(([, value]) => value !== void 0));
1607
+ var watchTurnMetrics = (session, write, options = {}) => {
1608
+ let caller;
1609
+ let delivered;
1610
+ session.on(voice2.AgentSessionEventTypes.ConversationItemAdded, ({ item }) => {
1611
+ if (!(item instanceof llm.ChatMessage)) return;
1612
+ if (item.role === "user") {
1613
+ caller = item.metrics;
1614
+ delivered = options.endOfTurnMs?.();
1615
+ return;
1616
+ }
1617
+ if (item.role !== "assistant") return;
1618
+ const reply = item.metrics;
1619
+ const runId = item.extra[AGUI_RUN_ID_EXTRA];
1620
+ const e2eMs = ms(reply.e2eLatency);
1621
+ const metrics = defined({
1622
+ endOfTurnMs: delivered ?? ms(caller?.endOfTurnDelay),
1623
+ transcriptionMs: options.endOfTurnMs ? void 0 : ms(caller?.transcriptionDelay),
1624
+ llmTtftMs: ms(reply.llmNodeTtft),
1625
+ ttsTtfbMs: ms(reply.ttsNodeTtfb),
1626
+ e2eMs: e2eMs === void 0 || delivered === void 0 ? e2eMs : e2eMs + delivered
1627
+ });
1628
+ caller = void 0;
1629
+ delivered = void 0;
1630
+ if (Object.keys(metrics).length === 0) return;
1631
+ write(typeof runId === "string" ? { ...metrics, runId } : metrics);
1632
+ });
1633
+ };
1634
+
1459
1635
  // libs/runtime/voice-worker/src/box-llm.ts
1460
- var BoxLLM = class extends llm.LLM {
1636
+ var BoxLLM = class extends llm2.LLM {
1461
1637
  #opts;
1462
1638
  #replies = new HeardTracker();
1463
1639
  #pending = /* @__PURE__ */ new Set();
@@ -1554,7 +1730,7 @@ var lastUserText = (chatCtx) => {
1554
1730
  }
1555
1731
  return "";
1556
1732
  };
1557
- var BoxLLMStream = class extends llm.LLMStream {
1733
+ var BoxLLMStream = class extends llm2.LLMStream {
1558
1734
  #opts;
1559
1735
  #replies;
1560
1736
  #settle;
@@ -1584,6 +1760,7 @@ var BoxLLMStream = class extends llm.LLMStream {
1584
1760
  ...turn
1585
1761
  } = this.#opts;
1586
1762
  const id = randomUUID();
1763
+ const runId = mintAguiRunId();
1587
1764
  const { signal } = this.abortController;
1588
1765
  await pendingResume?.();
1589
1766
  if (signal.aborted || isEnding?.()) return;
@@ -1608,7 +1785,8 @@ var BoxLLMStream = class extends llm.LLMStream {
1608
1785
  tools: clientTools,
1609
1786
  text: lastUserText(this.chatCtx),
1610
1787
  ...heard ? { heard } : {},
1611
- signal: turnSignal
1788
+ signal: turnSignal,
1789
+ runId
1612
1790
  })) {
1613
1791
  restartIdle();
1614
1792
  reply.observe(ev);
@@ -1619,7 +1797,7 @@ var BoxLLMStream = class extends llm.LLMStream {
1619
1797
  delta: {
1620
1798
  role: "assistant",
1621
1799
  content: ev.text,
1622
- extra: { [VOICE_RESPONSE_MARKER]: id }
1800
+ extra: { [VOICE_RESPONSE_MARKER]: id, [AGUI_RUN_ID_EXTRA]: runId }
1623
1801
  }
1624
1802
  });
1625
1803
  break;
@@ -1666,6 +1844,49 @@ var BoxLLMStream = class extends llm.LLMStream {
1666
1844
  }
1667
1845
  };
1668
1846
 
1847
+ // libs/runtime/voice-worker/src/stt-delivery.ts
1848
+ import { stt } from "@livekit/agents";
1849
+ import { TransformStream } from "node:stream/web";
1850
+ var finalDeliveryMs = (event, audioSentSec) => {
1851
+ const alt = event.alternatives?.[0];
1852
+ if (alt === void 0 || !Number.isFinite(alt.endTime) || alt.endTime <= 0) return void 0;
1853
+ const segmentEndSec = alt.endTime - (alt.words?.[0]?.startTimeOffset ?? 0);
1854
+ const ms2 = Math.round((audioSentSec - segmentEndSec) * 1e3);
1855
+ return ms2 >= 0 ? ms2 : void 0;
1856
+ };
1857
+ var createSttDeliveryMeter = () => {
1858
+ let last;
1859
+ return {
1860
+ wrap: async (audio, inner) => {
1861
+ let audioSentSec = 0;
1862
+ async function* counted() {
1863
+ for await (const frame of audio) {
1864
+ audioSentSec += frame.samplesPerChannel / frame.sampleRate;
1865
+ yield frame;
1866
+ }
1867
+ }
1868
+ const events = await inner(counted());
1869
+ if (events === null) return null;
1870
+ return events.pipeThrough(
1871
+ new TransformStream({
1872
+ transform(event, controller) {
1873
+ if (typeof event !== "string" && event.type === stt.SpeechEventType.FINAL_TRANSCRIPT) {
1874
+ last = finalDeliveryMs(event, audioSentSec) ?? last;
1875
+ }
1876
+ controller.enqueue(event);
1877
+ }
1878
+ })
1879
+ );
1880
+ },
1881
+ take: () => {
1882
+ const taken = last;
1883
+ last = void 0;
1884
+ return taken;
1885
+ }
1886
+ };
1887
+ };
1888
+ var deliversItsOwnEndOfTurn = (settings) => settings.turn.mode === "stt" && settings.sttModel.startsWith("deepgram/flux");
1889
+
1669
1890
  // libs/runtime/voice-worker/src/dispatch.ts
1670
1891
  var readDispatch = (metadata) => {
1671
1892
  if (!metadata) return null;
@@ -1742,17 +1963,17 @@ var createHangUp = ({
1742
1963
  };
1743
1964
 
1744
1965
  // libs/runtime/voice-worker/src/session-options.ts
1745
- import { tts, voice as voice2 } from "@livekit/agents";
1966
+ import { tts, voice as voice3 } from "@livekit/agents";
1746
1967
 
1747
1968
  // libs/runtime/voice-worker/src/number-text.ts
1748
- import { TransformStream } from "node:stream/web";
1969
+ import { TransformStream as TransformStream2 } from "node:stream/web";
1749
1970
  var THOUSANDS_COMMA = /(\d),(?=\d{3}(?!\d))/g;
1750
1971
  var TRAILING_NUMBER = /[\d.,]+$/;
1751
1972
  var rewrite = (text) => text.replace(THOUSANDS_COMMA, "$1");
1752
1973
  var dropThousandsComma = (input) => {
1753
1974
  let pending = "";
1754
1975
  return input.pipeThrough(
1755
- new TransformStream({
1976
+ new TransformStream2({
1756
1977
  transform(chunk, controller) {
1757
1978
  const text = pending + chunk;
1758
1979
  const tail = TRAILING_NUMBER.exec(text)?.[0] ?? "";
@@ -1769,7 +1990,7 @@ var dropThousandsComma = (input) => {
1769
1990
 
1770
1991
  // libs/runtime/voice-worker/src/phone-voice.ts
1771
1992
  import { AudioFrame as AudioFrame2 } from "@livekit/rtc-node";
1772
- import { TransformStream as TransformStream2 } from "node:stream/web";
1993
+ import { TransformStream as TransformStream3 } from "node:stream/web";
1773
1994
  var LOW_CUT_HZ = 300;
1774
1995
  var HIGH_CUT_HZ = 3400;
1775
1996
  var Q = Math.SQRT1_2;
@@ -1805,7 +2026,7 @@ var phoneLine = (audio) => {
1805
2026
  let chainRate = 0;
1806
2027
  const norm = Math.tanh(DRIVE);
1807
2028
  return audio.pipeThrough(
1808
- new TransformStream2({
2029
+ new TransformStream3({
1809
2030
  transform(frame, controller) {
1810
2031
  if (!chain || chainRate !== frame.sampleRate) {
1811
2032
  chainRate = frame.sampleRate;
@@ -1858,7 +2079,16 @@ var markupContext = (expressive, speech) => {
1858
2079
  });
1859
2080
  return guide === void 0 ? [] : [{ description: VOICE_TTS_MARKUP_CONTEXT, value: guide }];
1860
2081
  };
1861
- var PhoneLineAgent = class extends voice2.Agent {
2082
+ var VoiceCallAgent = class extends voice3.Agent {
2083
+ constructor() {
2084
+ super(...arguments);
2085
+ this.sttDelivery = createSttDeliveryMeter();
2086
+ }
2087
+ async sttNode(audio, modelSettings) {
2088
+ return this.sttDelivery.wrap(audio, (counted) => super.sttNode(counted, modelSettings));
2089
+ }
2090
+ };
2091
+ var PhoneLineAgent = class extends VoiceCallAgent {
1862
2092
  async ttsNode(text, modelSettings) {
1863
2093
  const audio = await super.ttsNode(text, modelSettings);
1864
2094
  return audio && phoneLine(audio);
@@ -1912,7 +2142,7 @@ var voiceSessionOptions = (settings, pieces) => {
1912
2142
  }
1913
2143
  },
1914
2144
  // The box holds the prompt: LiveKit's own chat context stays prompt-less.
1915
- agent: settings.phoneLine ? new PhoneLineAgent({ instructions: "" }) : new voice2.Agent({ instructions: "" }),
2145
+ agent: settings.phoneLine ? new PhoneLineAgent({ instructions: "" }) : new VoiceCallAgent({ instructions: "" }),
1916
2146
  boxContext: markupContext(settings.expressive, speech)
1917
2147
  };
1918
2148
  };
@@ -1990,30 +2220,39 @@ var CALL_CLOSING_PHRASE = Object.freeze({
1990
2220
  });
1991
2221
  var MAX_FAILED_TURNS_IN_A_ROW = 3;
1992
2222
  var inCallLanguage = (phrase, language) => language.split("-")[0]?.toLowerCase() === "es" ? phrase.es : phrase.en;
1993
- var toolCallCue = () => {
2223
+ var thinkingCue = () => {
1994
2224
  let cue;
1995
2225
  let open = 0;
2226
+ let thinking = false;
2227
+ let playing = false;
2228
+ const sync = () => {
2229
+ const play = thinking || open > 0;
2230
+ if (play === playing || !cue) return;
2231
+ playing = play;
2232
+ if (play) cue.start();
2233
+ else cue.stop();
2234
+ };
1996
2235
  return {
1997
- // A call can open before the cue's track is up.
2236
+ // A turn can start before the track is up.
1998
2237
  attach: (ready) => {
1999
2238
  cue = ready;
2000
- if (open > 0) cue.start();
2239
+ sync();
2240
+ },
2241
+ onAgentState: (state) => {
2242
+ thinking = state === "thinking";
2243
+ sync();
2001
2244
  },
2002
2245
  onToolCall: (phase) => {
2003
- if (phase === "start") {
2004
- open += 1;
2005
- if (open === 1) cue?.start();
2006
- } else if (open > 0) {
2007
- open -= 1;
2008
- if (open === 0) cue?.stop();
2009
- }
2246
+ if (phase === "start") open += 1;
2247
+ else if (open > 0) open -= 1;
2248
+ sync();
2010
2249
  }
2011
2250
  };
2012
2251
  };
2013
2252
  var messageOf = (err) => err instanceof Error ? err.message : String(err);
2014
- var callerNoiseCancellation = () => {
2253
+ var callerNoiseCancellation = (caller) => {
2015
2254
  try {
2016
- return BackgroundVoiceCancellation();
2255
+ return caller.kind === ParticipantKind.SIP ? TelephonyBackgroundVoiceCancellation() : BackgroundVoiceCancellation();
2017
2256
  } catch (err) {
2018
2257
  log4().warn({ error: messageOf(err) }, "voice worker: no noise cancellation on this call");
2019
2258
  return void 0;
@@ -2051,7 +2290,7 @@ var runVoiceJob = async (ctx, deps) => {
2051
2290
  deps
2052
2291
  );
2053
2292
  const settings = dispatch.voiceSettings;
2054
- const cue = toolCallCue();
2293
+ const cue = thinkingCue();
2055
2294
  const fallback = inCallLanguage(TURN_FALLBACK_PHRASE, settings.language);
2056
2295
  let failedInARow = 0;
2057
2296
  let ending = false;
@@ -2147,6 +2386,7 @@ var runVoiceJob = async (ctx, deps) => {
2147
2386
  onRunFinished
2148
2387
  });
2149
2388
  const session = deps.createSession({ ...plan.session, llm: brain });
2389
+ const turns = [];
2150
2390
  let completion;
2151
2391
  const finishCall = (outcome = "disconnected") => {
2152
2392
  if (completion) return completion;
@@ -2155,14 +2395,20 @@ var runVoiceJob = async (ctx, deps) => {
2155
2395
  await hangUp.pendingResume();
2156
2396
  await session.close();
2157
2397
  await brain.settled();
2158
- await voiceCallRequest(
2159
- wire,
2160
- "complete",
2161
- voiceCallCompleteSchema.parse({
2162
- transcript: brain.heardTranscript(session.history),
2163
- outcome
2164
- })
2165
- );
2398
+ const transcript = brain.heardTranscript(session.history);
2399
+ const complete = (body) => voiceCallRequest(wire, "complete", voiceCallCompleteSchema.parse(body));
2400
+ try {
2401
+ await complete({ transcript, outcome, ...turns.length > 0 ? { turns } : {} });
2402
+ } catch (error) {
2403
+ if (!(turns.length > 0 && error instanceof BoxTurnError && error.status === 400)) {
2404
+ throw error;
2405
+ }
2406
+ log4().warn(
2407
+ { agent: dispatch.agent, sessionKey: dispatch.sessionKey, status: error.status },
2408
+ "voice worker: the box refused the turn metrics, completing without them"
2409
+ );
2410
+ await complete({ transcript, outcome });
2411
+ }
2166
2412
  }).catch((error) => {
2167
2413
  log4().warn(
2168
2414
  { agent: dispatch.agent, sessionKey: dispatch.sessionKey, reason: "completion_failed" },
@@ -2173,29 +2419,44 @@ var runVoiceJob = async (ctx, deps) => {
2173
2419
  return completion;
2174
2420
  };
2175
2421
  ctx.addShutdownCallback(() => finishCall());
2176
- session.on(voice3.AgentSessionEventTypes.Close, () => {
2422
+ session.on(
2423
+ voice4.AgentSessionEventTypes.AgentStateChanged,
2424
+ ({ newState }) => cue.onAgentState(newState)
2425
+ );
2426
+ session.on(voice4.AgentSessionEventTypes.Close, () => {
2177
2427
  if (completion || ending) return;
2178
2428
  void finishCall().catch(() => void 0).finally(() => ctx.shutdown("caller left"));
2179
2429
  });
2180
- const noiseCancellation = callerNoiseCancellation();
2430
+ watchTurnMetrics(
2431
+ session,
2432
+ (metrics) => {
2433
+ if (turns.length < VOICE_CALL_TURNS_MAX) turns.push(metrics);
2434
+ log4().info(
2435
+ { agent: dispatch.agent, sessionKey: dispatch.sessionKey, room: ctx.room.name, ...metrics },
2436
+ VOICE_TURN_METRICS_MESSAGE
2437
+ );
2438
+ },
2439
+ deliversItsOwnEndOfTurn(settings) ? { endOfTurnMs: () => plan.agent.sttDelivery.take() } : {}
2440
+ );
2441
+ const noiseCancellation = callerNoiseCancellation(participant);
2181
2442
  await session.start({
2182
2443
  agent: plan.agent,
2183
2444
  room: ctx.room,
2184
2445
  inputOptions: noiseCancellation ? { noiseCancellation } : {}
2185
2446
  });
2186
2447
  if (settings.greeting !== null) sayIfOpen(settings.greeting, "greeting");
2187
- await deps.createCue({ room: ctx.room, session }).then(cue.attach, (err) => {
2188
- log4().warn({ error: messageOf(err) }, "voice worker: the thinking cue did not start");
2448
+ await deps.createCue({ room: ctx.room, session, audio: settings.backgroundAudio }).then(cue.attach, (err) => {
2449
+ log4().warn({ error: messageOf(err) }, "voice worker: the background audio did not start");
2189
2450
  });
2190
2451
  };
2191
2452
 
2192
2453
  // libs/runtime/voice-worker/src/main.ts
2193
2454
  var liveDeps = () => ({
2194
- createSession: (options) => new voice4.AgentSession(options),
2455
+ createSession: (options) => new voice5.AgentSession(options),
2195
2456
  createStt: (options) => new inference.STT(options),
2196
2457
  createTts: (options) => new inference.TTS(options),
2197
2458
  createTurnDetector: (options) => new inference.TurnDetector(options),
2198
- createCue: startThinkingCue,
2459
+ createCue: startBackgroundAudio,
2199
2460
  // OTLP/HTTP JSON, uncompressed: the encoding the box relay takes and forwards as is.
2200
2461
  // `compression` is set so an `OTEL_EXPORTER_OTLP_COMPRESSION=gzip` in the worker's
2201
2462
  // environment cannot turn every export into a body the relay refuses.