@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.
- package/CHANGELOG.md +152 -0
- package/main.js +3593 -1135
- package/migrations/0117_rag_extract_fields.sql +3 -0
- package/migrations/0118_voice_outbound_calls.sql +29 -0
- package/migrations/0119_voice_call_metrics.sql +20 -0
- package/migrations/0120_run_steps_voice.sql +19 -0
- package/migrations/0121_model_routing.sql +28 -0
- package/package.json +1 -1
- package/stackbone-cli-0.9.0.tgz +0 -0
- package/voice-worker/main.js +571 -310
- package/voice-worker/main.js.map +4 -4
- package/stackbone-cli-0.7.1.tgz +0 -0
package/voice-worker/main.js
CHANGED
|
@@ -6,66 +6,114 @@ import {
|
|
|
6
6
|
defineAgent,
|
|
7
7
|
inference,
|
|
8
8
|
telemetry as telemetry2,
|
|
9
|
-
voice as
|
|
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/
|
|
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
|
|
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
|
|
27
|
-
const total = PATTERN.reduce((n, [,
|
|
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,
|
|
31
|
-
const length = SAMPLE_RATE *
|
|
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 *
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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
|
|
59
|
-
|
|
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
|
|
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
|
|
105
|
-
import {
|
|
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/
|
|
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 =
|
|
116
|
-
var limitParamSchema = (max = PAGE_LIMIT_MAX) =>
|
|
117
|
-
var orderParamSchema =
|
|
118
|
-
var pageQuerySchema =
|
|
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 =
|
|
125
|
-
var pageResponseSchema = (item) =>
|
|
126
|
-
var forwardPageResponseSchema = (item) =>
|
|
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
|
|
316
|
+
import { z as z5 } from "zod";
|
|
130
317
|
|
|
131
318
|
// libs/shared/validators/src/lib/common/system-entries.ts
|
|
132
|
-
import { z as
|
|
133
|
-
var systemEntriesParamSchema =
|
|
134
|
-
var systemEntriesQuerySchema =
|
|
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 =
|
|
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 =
|
|
161
|
-
var runEntityKindSchema =
|
|
162
|
-
var runTriggerLinkSchema =
|
|
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:
|
|
351
|
+
subscriptionId: z5.string(),
|
|
165
352
|
/** The connector id, e.g. `gmail`. */
|
|
166
|
-
connector:
|
|
353
|
+
connector: z5.string(),
|
|
167
354
|
/** The trigger id within the connector, e.g. `gmail_new_email_received`. */
|
|
168
|
-
trigger:
|
|
169
|
-
outcome:
|
|
355
|
+
trigger: z5.string(),
|
|
356
|
+
outcome: z5.enum(["started", "resumed"]).optional(),
|
|
170
357
|
/** The connector's brand name, e.g. `Gmail`. */
|
|
171
|
-
connectorDisplayName:
|
|
358
|
+
connectorDisplayName: z5.string().optional(),
|
|
172
359
|
/** The trigger's name, e.g. `New Email`. */
|
|
173
|
-
triggerDisplayName:
|
|
174
|
-
connectorLogoUrl:
|
|
360
|
+
triggerDisplayName: z5.string().optional(),
|
|
361
|
+
connectorLogoUrl: z5.string().optional()
|
|
175
362
|
});
|
|
176
|
-
var runSchema =
|
|
177
|
-
id:
|
|
363
|
+
var runSchema = z5.object({
|
|
364
|
+
id: z5.uuid(),
|
|
178
365
|
status: runStatusSchema,
|
|
179
366
|
trigger: runTriggerSchema,
|
|
180
|
-
createdAt:
|
|
181
|
-
startedAt:
|
|
182
|
-
finishedAt:
|
|
183
|
-
durationMs:
|
|
184
|
-
traceId:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
220
|
-
outputTokens:
|
|
221
|
-
cacheReadTokens:
|
|
222
|
-
cacheWriteTokens:
|
|
223
|
-
totalTokens:
|
|
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 =
|
|
431
|
+
var schemaIssueSchema = z5.object({
|
|
245
432
|
/** Path segments to the offending field (`[]` for the root value). */
|
|
246
|
-
path:
|
|
247
|
-
message:
|
|
433
|
+
path: z5.array(z5.union([z5.string(), z5.number()])),
|
|
434
|
+
message: z5.string()
|
|
248
435
|
});
|
|
249
|
-
var runOutputViolationSchema =
|
|
436
|
+
var runOutputViolationSchema = z5.object({
|
|
250
437
|
/** The workflow whose declared `outputSchema` the return value broke. */
|
|
251
|
-
workflowName:
|
|
252
|
-
issues:
|
|
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:
|
|
444
|
+
recordedAt: z5.string()
|
|
258
445
|
});
|
|
259
|
-
var runIdParamSchema =
|
|
260
|
-
var runTagKeySchema =
|
|
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 =
|
|
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 =
|
|
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:
|
|
297
|
-
to:
|
|
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:
|
|
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:
|
|
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:
|
|
321
|
-
input_value:
|
|
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 =
|
|
342
|
-
name:
|
|
528
|
+
var runEntityEntrySchema = z5.object({
|
|
529
|
+
name: z5.string(),
|
|
343
530
|
kind: runEntityKindSchema.nullable()
|
|
344
531
|
});
|
|
345
|
-
var runEntityNamesResponseSchema =
|
|
346
|
-
names:
|
|
347
|
-
entries:
|
|
532
|
+
var runEntityNamesResponseSchema = z5.object({
|
|
533
|
+
names: z5.array(z5.string()),
|
|
534
|
+
entries: z5.array(runEntityEntrySchema).optional()
|
|
348
535
|
});
|
|
349
|
-
var cancelRunResponseSchema =
|
|
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 =
|
|
355
|
-
id:
|
|
356
|
-
name:
|
|
541
|
+
var fixtureSchema = z5.object({
|
|
542
|
+
id: z5.string(),
|
|
543
|
+
name: z5.string(),
|
|
357
544
|
input: jsonObject().nullable(),
|
|
358
|
-
description:
|
|
359
|
-
createdAt:
|
|
545
|
+
description: z5.string().nullable(),
|
|
546
|
+
createdAt: z5.iso.datetime()
|
|
360
547
|
});
|
|
361
|
-
var createFixtureRequestSchema =
|
|
362
|
-
name:
|
|
548
|
+
var createFixtureRequestSchema = z5.object({
|
|
549
|
+
name: z5.string().min(1).max(120),
|
|
363
550
|
input: jsonObject(),
|
|
364
|
-
description:
|
|
551
|
+
description: z5.string().max(2e3).optional().nullable()
|
|
365
552
|
});
|
|
366
553
|
var fixtureListResponseSchema = pageResponseSchema(fixtureSchema);
|
|
367
|
-
var fixtureIdParamSchema =
|
|
368
|
-
var handlerSchemaSchema =
|
|
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 =
|
|
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
|
|
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:
|
|
648
|
-
gte:
|
|
649
|
-
lt:
|
|
650
|
-
lte:
|
|
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
|
-
|
|
782
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2236
|
+
// A turn can start before the track is up.
|
|
1998
2237
|
attach: (ready) => {
|
|
1999
2238
|
cue = ready;
|
|
2000
|
-
|
|
2239
|
+
sync();
|
|
2240
|
+
},
|
|
2241
|
+
onAgentState: (state) => {
|
|
2242
|
+
thinking = state === "thinking";
|
|
2243
|
+
sync();
|
|
2001
2244
|
},
|
|
2002
2245
|
onToolCall: (phase) => {
|
|
2003
|
-
if (phase === "start")
|
|
2004
|
-
|
|
2005
|
-
|
|
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 =
|
|
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
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
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:
|
|
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.
|