@voicelayer/sdk 0.1.11 → 0.3.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.
@@ -0,0 +1,3550 @@
1
+ import { randomUUID } from 'crypto';
2
+ import { promises } from 'dns';
3
+ import { isIP } from 'net';
4
+ import { z } from 'zod';
5
+ import IORedis from 'ioredis';
6
+
7
+ // src/brain/serialize.ts
8
+ function normalizeRole(role) {
9
+ switch (role) {
10
+ case "system":
11
+ case "developer":
12
+ return "system";
13
+ case "user":
14
+ return "user";
15
+ case "assistant":
16
+ return "assistant";
17
+ default:
18
+ return null;
19
+ }
20
+ }
21
+ function extractText(m) {
22
+ if (typeof m.textContent === "string") return m.textContent;
23
+ if (typeof m.content === "string") return m.content;
24
+ if (Array.isArray(m.content)) {
25
+ const parts = m.content.filter((c) => typeof c === "string");
26
+ if (parts.length > 0) return parts.join("\n");
27
+ }
28
+ return null;
29
+ }
30
+ function chatContextToMessages(chatCtx) {
31
+ const items = chatCtx?.items;
32
+ if (!Array.isArray(items)) return [];
33
+ const out = [];
34
+ for (const item of items) {
35
+ const m = item;
36
+ if (m == null || typeof m !== "object") continue;
37
+ if (m.type !== void 0 && m.type !== "message") continue;
38
+ const role = normalizeRole(m.role);
39
+ if (role === null) continue;
40
+ const content = extractText(m);
41
+ if (content === null) continue;
42
+ out.push({ role, content });
43
+ }
44
+ return out;
45
+ }
46
+ function lastUserText(messages) {
47
+ for (let i = messages.length - 1; i >= 0; i--) {
48
+ const m = messages[i];
49
+ if (m && m.role === "user") return m.content;
50
+ }
51
+ return "";
52
+ }
53
+
54
+ // src/brain/transport-callback.ts
55
+ function callbackTransport(onQuery) {
56
+ async function* stream(req, signal) {
57
+ const text = lastUserText(req.messages);
58
+ const result = await onQuery(text, { messages: req.messages, signal });
59
+ if (typeof result === "string") {
60
+ if (result.length > 0) yield { content: result };
61
+ return;
62
+ }
63
+ for await (const piece of result) {
64
+ if (signal.aborted) return;
65
+ if (piece.length > 0) yield { content: piece };
66
+ }
67
+ }
68
+ async function complete(req, signal) {
69
+ let acc = "";
70
+ for await (const chunk of stream(req, signal)) {
71
+ acc += chunk.content ?? "";
72
+ }
73
+ return acc;
74
+ }
75
+ return { kind: "callback", stream, complete };
76
+ }
77
+
78
+ // src/brain/connector-llm.ts
79
+ function buildMetadata(options) {
80
+ return {
81
+ channel: "voice",
82
+ ...options.callId ? { callId: options.callId } : {},
83
+ ...options.projectId ? { projectId: options.projectId } : {}
84
+ };
85
+ }
86
+ async function* chatChunkStream(transport, messages, options, signal) {
87
+ let index = 0;
88
+ let yielded = false;
89
+ try {
90
+ const upstream = transport.stream(
91
+ {
92
+ messages,
93
+ ...options.model ? { model: options.model } : {},
94
+ ...options.temperature !== void 0 ? { temperature: options.temperature } : {},
95
+ metadata: buildMetadata(options)
96
+ },
97
+ signal
98
+ );
99
+ for await (const chunk of upstream) {
100
+ if (signal.aborted) return;
101
+ const content = chunk.content;
102
+ if (content) {
103
+ yielded = true;
104
+ yield { id: `vlc_${index++}`, delta: { role: "assistant", content } };
105
+ }
106
+ }
107
+ } catch (err) {
108
+ if (!yielded && options.fallbackText) {
109
+ yield { id: `vlc_${index++}`, delta: { role: "assistant", content: options.fallbackText } };
110
+ return;
111
+ }
112
+ if (!yielded) throw err;
113
+ }
114
+ }
115
+ async function createConnectorLLM(transport, options = {}) {
116
+ const lk = await import('@livekit/agents');
117
+ const Base = lk.llm?.LLM;
118
+ if (!Base) {
119
+ throw new Error(
120
+ "@livekit/agents does not export llm.LLM; cannot build ConnectorLLM. Check your @livekit/agents version."
121
+ );
122
+ }
123
+ class ConnectorLLM extends Base {
124
+ label() {
125
+ return "voicelayer.ConnectorLLM";
126
+ }
127
+ // LK calls chat({chatCtx,...}) and iterates the result, calling close() on
128
+ // barge-in. We return a plain async-iterable + close(), matching NoOpLLM.
129
+ chat(args = {}) {
130
+ const messages = chatContextToMessages(args.chatCtx);
131
+ const ac = new AbortController();
132
+ return {
133
+ [Symbol.asyncIterator]() {
134
+ return chatChunkStream(transport, messages, options, ac.signal);
135
+ },
136
+ async close() {
137
+ ac.abort();
138
+ }
139
+ };
140
+ }
141
+ }
142
+ return new ConnectorLLM();
143
+ }
144
+
145
+ // src/brain/connector-chat-model.ts
146
+ var ConnectorChatModel = class {
147
+ constructor(transport, options = {}) {
148
+ this.transport = transport;
149
+ this.options = options;
150
+ }
151
+ transport;
152
+ options;
153
+ /** `signal` cancels the brain request (the tunnel sends `brain.cancel`); a deadline is the caller's to set. */
154
+ async complete(input, signal) {
155
+ const ac = new AbortController();
156
+ if (signal) {
157
+ if (signal.aborted) ac.abort(signal.reason);
158
+ else signal.addEventListener("abort", () => ac.abort(signal.reason), { once: true });
159
+ }
160
+ const messages = input.messages.map((m) => ({
161
+ role: m.role,
162
+ content: m.content
163
+ }));
164
+ const model = input.model ?? this.options.model;
165
+ return this.transport.complete(
166
+ {
167
+ messages,
168
+ ...model ? { model } : {},
169
+ ...input.temperature !== void 0 ? { temperature: input.temperature } : {},
170
+ metadata: { channel: "text" }
171
+ },
172
+ ac.signal
173
+ );
174
+ }
175
+ };
176
+
177
+ // src/brain/openai-sse.ts
178
+ async function* toAsyncIterable(source) {
179
+ if (Symbol.asyncIterator in Object(source)) {
180
+ yield* source;
181
+ return;
182
+ }
183
+ const reader = source.getReader();
184
+ try {
185
+ for (; ; ) {
186
+ const { done, value } = await reader.read();
187
+ if (done) break;
188
+ if (value) yield value;
189
+ }
190
+ } finally {
191
+ reader.releaseLock();
192
+ }
193
+ }
194
+ async function* parseChatCompletionSse(source) {
195
+ const decoder = new TextDecoder();
196
+ let buf = "";
197
+ for await (const chunk of toAsyncIterable(source)) {
198
+ buf += decoder.decode(chunk, { stream: true });
199
+ let sep;
200
+ while ((sep = buf.indexOf("\n\n")) >= 0) {
201
+ const event = buf.slice(0, sep);
202
+ buf = buf.slice(sep + 2);
203
+ for (const line of event.split("\n")) {
204
+ const trimmed = line.replace(/\r$/, "").trimStart();
205
+ if (!trimmed.startsWith("data:")) continue;
206
+ const data = trimmed.slice(5).trim();
207
+ if (data === "[DONE]") return;
208
+ if (data.length === 0) continue;
209
+ let json;
210
+ try {
211
+ json = JSON.parse(data);
212
+ } catch {
213
+ continue;
214
+ }
215
+ const choice = json.choices?.[0];
216
+ if (!choice) continue;
217
+ const rawContent = choice.delta?.content;
218
+ const content = typeof rawContent === "string" ? rawContent : void 0;
219
+ const finishReason = typeof choice.finish_reason === "string" ? choice.finish_reason : void 0;
220
+ if (content !== void 0 || finishReason !== void 0) {
221
+ yield {
222
+ ...content !== void 0 ? { content } : {},
223
+ ...finishReason !== void 0 ? { finishReason } : {}
224
+ };
225
+ }
226
+ }
227
+ }
228
+ }
229
+ }
230
+
231
+ // src/brain/daemon-core.ts
232
+ function errMessage(err) {
233
+ return err instanceof Error ? err.message : String(err);
234
+ }
235
+ async function* runBrainRequest(req, cfg, signal) {
236
+ const doFetch = cfg.fetchImpl ?? fetch;
237
+ const url = `${cfg.brainUrl.replace(/\/+$/, "")}/chat/completions`;
238
+ const body = JSON.stringify({
239
+ model: req.model ?? cfg.defaultModel ?? "gpt-4o-mini",
240
+ messages: req.messages.map((m) => ({ role: m.role, content: m.content })),
241
+ stream: true,
242
+ ...req.temperature !== void 0 ? { temperature: req.temperature } : {}
243
+ });
244
+ let res;
245
+ try {
246
+ res = await doFetch(url, {
247
+ method: "POST",
248
+ signal,
249
+ ...cfg.refuseRedirects ? { redirect: "error" } : {},
250
+ headers: {
251
+ "content-type": "application/json",
252
+ ...cfg.apiKey ? { authorization: `Bearer ${cfg.apiKey}` } : {}
253
+ },
254
+ body
255
+ });
256
+ } catch (err) {
257
+ yield { kind: "brain.error", streamId: req.streamId, code: "unreachable", message: errMessage(err) };
258
+ return;
259
+ }
260
+ if (!res.ok) {
261
+ yield {
262
+ kind: "brain.error",
263
+ streamId: req.streamId,
264
+ code: "upstream_error",
265
+ message: `HTTP ${res.status}`
266
+ };
267
+ return;
268
+ }
269
+ if (!res.body) {
270
+ yield { kind: "brain.error", streamId: req.streamId, code: "bad_response", message: "empty body" };
271
+ return;
272
+ }
273
+ try {
274
+ for await (const delta of parseChatCompletionSse(res.body)) {
275
+ if (signal.aborted) return;
276
+ if (delta.content) {
277
+ yield { kind: "brain.delta", streamId: req.streamId, content: delta.content };
278
+ }
279
+ }
280
+ yield { kind: "brain.done", streamId: req.streamId, finishReason: "stop" };
281
+ } catch (err) {
282
+ if (signal.aborted) return;
283
+ yield { kind: "brain.error", streamId: req.streamId, code: "bad_response", message: errMessage(err) };
284
+ }
285
+ }
286
+ var BrainConfigError = class extends Error {
287
+ constructor(message) {
288
+ super(message);
289
+ this.name = "BrainConfigError";
290
+ }
291
+ };
292
+ function ipv4Parts(ip) {
293
+ const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(ip);
294
+ if (!m) return null;
295
+ const parts = [Number(m[1]), Number(m[2]), Number(m[3]), Number(m[4])];
296
+ if (parts.some((n) => n > 255)) return null;
297
+ return parts;
298
+ }
299
+ function isDisallowedV4(ip) {
300
+ const p = ipv4Parts(ip);
301
+ if (!p) return false;
302
+ const [a, b] = p;
303
+ if (a === 0) return true;
304
+ if (a === 10) return true;
305
+ if (a === 127) return true;
306
+ if (a === 169 && b === 254) return true;
307
+ if (a === 172 && b >= 16 && b <= 31) return true;
308
+ if (a === 192 && b === 168) return true;
309
+ if (a === 100 && b >= 64 && b <= 127) return true;
310
+ return false;
311
+ }
312
+ function isDisallowedIp(ip) {
313
+ const raw = ip.startsWith("[") && ip.endsWith("]") ? ip.slice(1, -1) : ip;
314
+ const kind = isIP(raw);
315
+ if (kind === 4) return isDisallowedV4(raw);
316
+ if (kind === 6) {
317
+ const lower = raw.toLowerCase();
318
+ if (lower === "::1" || lower === "::") return true;
319
+ if (lower.startsWith("fe80")) return true;
320
+ if (lower.startsWith("fc") || lower.startsWith("fd")) return true;
321
+ const embedded = /^::(?:ffff:)?(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/.exec(lower);
322
+ if (embedded?.[1]) return isDisallowedV4(embedded[1]);
323
+ const hex = /^::(?:ffff:)?([0-9a-f]{1,4}):([0-9a-f]{1,4})$/.exec(lower);
324
+ if (hex?.[1] && hex[2]) {
325
+ const hi = parseInt(hex[1], 16);
326
+ const lo = parseInt(hex[2], 16);
327
+ return isDisallowedV4(`${hi >> 8}.${hi & 255}.${lo >> 8}.${lo & 255}`);
328
+ }
329
+ return false;
330
+ }
331
+ return false;
332
+ }
333
+ async function assertPublicHttpsUrl(raw, opts = {}) {
334
+ let url;
335
+ try {
336
+ url = new URL(raw);
337
+ } catch {
338
+ throw new BrainConfigError(`brain URL is not a valid URL: ${raw}`);
339
+ }
340
+ if (url.protocol !== "https:") {
341
+ throw new BrainConfigError(`brain URL must use https:// (got "${url.protocol}//")`);
342
+ }
343
+ const host = url.hostname;
344
+ const bare = host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
345
+ if (opts.allowHosts?.includes(host) || opts.allowHosts?.includes(bare)) {
346
+ return url;
347
+ }
348
+ if (isIP(bare)) {
349
+ if (isDisallowedIp(bare)) {
350
+ throw new BrainConfigError(`brain URL host is a disallowed address: ${bare}`);
351
+ }
352
+ return url;
353
+ }
354
+ const lowerHost = bare.toLowerCase();
355
+ if (lowerHost === "localhost" || lowerHost.endsWith(".localhost")) {
356
+ throw new BrainConfigError(`brain URL host "${bare}" is not allowed`);
357
+ }
358
+ const lookup = opts.lookup ?? (async (h) => (await promises.lookup(h, { all: true })).map((a) => a.address));
359
+ let addresses;
360
+ try {
361
+ addresses = await lookup(bare);
362
+ } catch {
363
+ throw new BrainConfigError(`brain URL host could not be resolved: ${bare}`);
364
+ }
365
+ if (addresses.length === 0) {
366
+ throw new BrainConfigError(`brain URL host did not resolve to any address: ${bare}`);
367
+ }
368
+ for (const addr of addresses) {
369
+ if (isDisallowedIp(addr)) {
370
+ throw new BrainConfigError(
371
+ `brain URL host "${bare}" resolves to a disallowed address: ${addr}`
372
+ );
373
+ }
374
+ }
375
+ return url;
376
+ }
377
+
378
+ // src/brain/errors.ts
379
+ var BrainRequestError = class extends Error {
380
+ constructor(code, message) {
381
+ super(message);
382
+ this.code = code;
383
+ this.name = "BrainRequestError";
384
+ }
385
+ code;
386
+ };
387
+
388
+ // src/brain/transport-http.ts
389
+ function httpBrainTransport(opts) {
390
+ const stream = async function* (req, signal) {
391
+ await assertPublicHttpsUrl(opts.baseUrl, {
392
+ ...opts.allowHosts ? { allowHosts: opts.allowHosts } : {},
393
+ ...opts.lookup ? { lookup: opts.lookup } : {}
394
+ });
395
+ const frames = runBrainRequest(
396
+ {
397
+ streamId: randomUUID(),
398
+ messages: req.messages,
399
+ ...req.model ? { model: req.model } : {},
400
+ ...req.temperature !== void 0 ? { temperature: req.temperature } : {}
401
+ },
402
+ {
403
+ brainUrl: opts.baseUrl,
404
+ refuseRedirects: true,
405
+ ...opts.apiKey ? { apiKey: opts.apiKey } : {},
406
+ ...opts.defaultModel ? { defaultModel: opts.defaultModel } : {},
407
+ ...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
408
+ },
409
+ signal
410
+ );
411
+ for await (const frame of frames) {
412
+ if (frame.kind === "brain.delta") yield { content: frame.content };
413
+ else if (frame.kind === "brain.error") throw new BrainRequestError(frame.code, frame.message);
414
+ }
415
+ };
416
+ return {
417
+ kind: "http",
418
+ stream,
419
+ async complete(req, signal) {
420
+ let text = "";
421
+ for await (const chunk of stream(req, signal)) text += chunk.content ?? "";
422
+ return text;
423
+ }
424
+ };
425
+ }
426
+ var FlowKind = z.enum(["conversation"]);
427
+ var FlowStatus = z.enum(["draft", "published", "archived"]);
428
+ z.enum(["shared", "dedicated"]);
429
+ var FlowNodeType = z.enum([
430
+ // Explicit entry marker — the interpreter starts here (falls back to the
431
+ // first indegree-0 node for older graphs without one).
432
+ "start",
433
+ "say",
434
+ "ask",
435
+ "confirm",
436
+ "tool",
437
+ "decision",
438
+ "handoff",
439
+ "end",
440
+ "trigger",
441
+ "gate",
442
+ "playbook",
443
+ "menu",
444
+ "code",
445
+ "set",
446
+ // Reusable component: runs another published flow inline.
447
+ "subflow",
448
+ // AI nodes (PRD P5).
449
+ "llm",
450
+ "rag",
451
+ "memory",
452
+ // Call-wide voice/turn-taking capability (barge-in + endpointing tuning);
453
+ // managed in the Capabilities panel like `memory`, never a graph step.
454
+ "speech",
455
+ "classify",
456
+ // Agentic router: per-turn, selects 1-of-N skills by their plain-language
457
+ // descriptions and takes that port (the resident routing surface / hub).
458
+ "router",
459
+ // Canvas-only annotation (sticky note); excluded from compilation/runtime.
460
+ "note"
461
+ ]);
462
+ var FlowNodePosition = z.object({
463
+ x: z.number(),
464
+ y: z.number()
465
+ });
466
+ var FlowNode = z.object({
467
+ id: z.string().min(1).max(128),
468
+ type: FlowNodeType,
469
+ position: FlowNodePosition,
470
+ // Node-specific config. Generic on purpose — the per-type shape is validated
471
+ // by the runtime compiler that targets each plane, not by storage.
472
+ data: z.record(z.unknown()).default({})
473
+ });
474
+ var FlowEdge = z.object({
475
+ id: z.string().min(1).max(128),
476
+ // Source / target node ids (must reference FlowNode.id values).
477
+ source: z.string().min(1).max(128),
478
+ target: z.string().min(1).max(128),
479
+ // React Flow handle ids for multi-port nodes (e.g. a decision's branches).
480
+ sourceHandle: z.string().max(128).optional(),
481
+ targetHandle: z.string().max(128).optional(),
482
+ // Optional label / branch condition — e.g. a normalized tool `result.kind`.
483
+ label: z.string().max(128).optional(),
484
+ condition: z.string().max(512).optional()
485
+ });
486
+ var FlowGraph = z.object({
487
+ nodes: z.array(FlowNode).max(1e3).default([]),
488
+ edges: z.array(FlowEdge).max(2e3).default([])
489
+ });
490
+ z.object({
491
+ id: z.string().uuid(),
492
+ projectId: z.string().min(1),
493
+ name: z.string().min(1).max(200),
494
+ kind: FlowKind,
495
+ status: FlowStatus,
496
+ // The working/draft graph. Published versions snapshot this.
497
+ graph: FlowGraph,
498
+ // Latest published version number; null until first publish.
499
+ publishedVersion: z.number().int().min(1).nullable(),
500
+ createdAt: z.string().datetime(),
501
+ // Optimistic-lock token (mirrors AgentConfig.updatedAt).
502
+ updatedAt: z.string().datetime()
503
+ });
504
+ z.object({
505
+ id: z.string().uuid(),
506
+ flowId: z.string().uuid(),
507
+ projectId: z.string().min(1),
508
+ version: z.number().int().min(1),
509
+ kind: FlowKind,
510
+ // Frozen graph at publish time.
511
+ graph: FlowGraph,
512
+ // Optional human note describing the publish.
513
+ note: z.string().max(1e3).nullable(),
514
+ publishedByUserId: z.string().nullable(),
515
+ publishedAt: z.string().datetime()
516
+ });
517
+ z.object({
518
+ name: z.string().trim().min(1).max(200),
519
+ kind: FlowKind,
520
+ // Optional initial graph; defaults to empty.
521
+ graph: FlowGraph.optional()
522
+ });
523
+ z.object({
524
+ name: z.string().trim().min(1).max(200).optional(),
525
+ graph: FlowGraph.optional(),
526
+ // Framework is a reversible default-structure switch (Voiceflow parity):
527
+ // changing kind re-frames the palette/deployability but never touches the graph.
528
+ kind: FlowKind.optional()
529
+ }).refine((b) => b.name !== void 0 || b.graph !== void 0 || b.kind !== void 0, {
530
+ message: "provide at least one of `name`, `graph`, or `kind`"
531
+ });
532
+ z.object({
533
+ note: z.string().max(1e3).optional()
534
+ });
535
+
536
+ // ../contracts/src/flow-program.ts
537
+ var FlowProgramEdge = z.object({
538
+ to: z.string().min(1).max(128),
539
+ // Branch condition (e.g. `slot:Intent==billing`, `tool.lookup.ok`). Empty =
540
+ // default/fallthrough edge. Evaluated by the runtime's condition matcher.
541
+ condition: z.string().max(512).optional(),
542
+ // React Flow handle id for multi-branch nodes (decision/menu ports).
543
+ sourceHandle: z.string().max(128).optional(),
544
+ label: z.string().max(128).optional()
545
+ });
546
+ var FlowProgramNode = z.object({
547
+ id: z.string().min(1).max(128),
548
+ type: FlowNodeType,
549
+ // Cleaned, per-type config (text / slot / ask / url / requiredFields / ...).
550
+ // Produced by compileFlowToProgram; read defensively by the executors.
551
+ config: z.record(z.unknown()),
552
+ // Outgoing edges in declaration order.
553
+ out: z.array(FlowProgramEdge).max(64)
554
+ });
555
+ var FlowProgramDiagnostics = z.object({
556
+ // False if the graph contains a cycle (runtime still runs it, guarded by a
557
+ // per-call step/visit budget).
558
+ acyclic: z.boolean(),
559
+ // Edge ids whose source/target node was missing (skipped at compile).
560
+ droppedEdges: z.array(z.string().max(128)).max(256),
561
+ // Node ids needing external plumbing not wired yet (rag/classify/code) — the
562
+ // interpreter passes through them.
563
+ unsupportedNodes: z.array(z.string().max(128)).max(256),
564
+ // Port-less edges out of a conversational node (llm/ask/playbook) that carry
565
+ // a display `label` but no `condition` — compiled as the UNCONDITIONAL
566
+ // default (the label never routes). Mirrors validateFlowGraph's
567
+ // `edge_label_not_condition` warning. Optional: programs stored before this
568
+ // field existed must still pass the read-side parse.
569
+ labelOnlyEdges: z.array(z.string().max(128)).max(256).optional()
570
+ });
571
+ var FlowProgram = z.object({
572
+ v: z.literal(1),
573
+ id: z.string().min(1).max(64),
574
+ // Entry node id(s) — nodes with no inbound edge (interpreter starts here).
575
+ entry: z.array(z.string().min(1).max(128)).max(64),
576
+ nodes: z.record(FlowProgramNode),
577
+ // Mirrors the flat completion gate so the same gate computation is reused.
578
+ completionGate: z.object({ requiredFields: z.array(z.string()).max(64) }),
579
+ diagnostics: FlowProgramDiagnostics.optional()
580
+ });
581
+
582
+ // ../contracts/src/agents.ts
583
+ var AgentEnvironment = z.enum(["dev", "stg", "prd"]);
584
+ var AgentStatus = z.enum(["online", "offline", "degraded"]);
585
+ var AgentName = z.string().min(1).max(128).regex(/^[a-z0-9][a-z0-9-_]*$/, 'lowercase letters, digits, "-" or "_" only');
586
+ var AgentVersion = z.string().min(1).max(64);
587
+ var AgentMetadata = z.record(z.unknown());
588
+ var AgentCapability = z.string().min(1).max(64).regex(/^[a-z0-9][a-z0-9-_:.]*$/, 'lowercase letters, digits, "-_:." only');
589
+ var ReadinessReport = z.object({
590
+ probe: z.string().min(1).max(128),
591
+ ok: z.boolean(),
592
+ detail: z.string().max(512).optional()
593
+ });
594
+ var ProcessFieldDTO = z.object({
595
+ name: z.string().min(1).max(64),
596
+ required: z.boolean(),
597
+ type: z.enum(["string", "string[]", "number", "boolean", "datetime", "email", "phone"]),
598
+ pattern: z.string().max(1024).optional(),
599
+ min: z.number().optional(),
600
+ max: z.number().optional(),
601
+ enum: z.array(z.string()).max(64).optional(),
602
+ ask: z.string().max(512).optional()
603
+ });
604
+ var ProcessTriggerDTO = z.object({
605
+ name: z.string().min(1).max(64),
606
+ on: z.string().min(1).max(512),
607
+ then: z.string().min(1).max(1024),
608
+ onlyWhile: z.string().max(64).optional()
609
+ });
610
+ var ProcessToolDTO = z.object({
611
+ name: z.string().min(1).max(64).regex(/^[a-z][a-z0-9_]*$/, "snake_case"),
612
+ description: z.string().min(1).max(2e3),
613
+ // Shorthand input shape: field name → primitive type. The LLM fills these.
614
+ // Required (the compiler always provides it, possibly {}) to keep the schema's
615
+ // input/output types symmetric — a `.default()` here diverges them.
616
+ input: z.record(z.enum(["string", "number", "boolean"])),
617
+ url: z.string().url(),
618
+ method: z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]),
619
+ headers: z.record(z.string().max(1024)).optional(),
620
+ auth: z.enum(["none", "connection"]),
621
+ connectionRef: z.string().max(128).optional()
622
+ });
623
+ var ProcessSchemaDTO = z.object({
624
+ id: z.string().min(1).max(64),
625
+ // System-prompt fragments compiled from say/confirm nodes. The SDK bridge
626
+ // composes these into the agent's prompt.
627
+ prompts: z.array(z.object({ id: z.string().min(1).max(128), text: z.string().max(2e3) })).max(64).optional(),
628
+ fields: z.array(ProcessFieldDTO).max(64),
629
+ completionGate: z.object({
630
+ requiredFields: z.array(z.string()).max(64),
631
+ backendAck: z.object({
632
+ url: z.string().url(),
633
+ timeoutMs: z.number().int().positive()
634
+ }).optional()
635
+ }),
636
+ // Deterministic rails compiled from trigger/handoff/end nodes.
637
+ triggers: z.array(ProcessTriggerDTO).max(64).optional(),
638
+ // Default human destination for `then:'handoff'` rails.
639
+ handoff: z.object({
640
+ fallback: z.string().max(128).optional(),
641
+ routes: z.record(z.string().max(128)).optional()
642
+ }).optional(),
643
+ // Memory config compiled from a memory node (maps to AgentConfig.memory).
644
+ memory: z.object({
645
+ scope: z.enum(["call", "caller", "participant", "account", "platform"]).optional(),
646
+ retentionDays: z.number().int().positive().optional(),
647
+ piiFields: z.array(z.string().max(64)).max(64).optional()
648
+ }).optional(),
649
+ // Tool capabilities compiled from tool nodes (maps to AgentConfig.tools).
650
+ tools: z.array(ProcessToolDTO).max(32).optional(),
651
+ // Voice turn-taking compiled from the flow's speech capability node (maps to
652
+ // AgentConfig.speech → applySpeechTurnHandling). Absent = platform defaults.
653
+ speech: z.object({
654
+ interruption: z.object({
655
+ enabled: z.boolean().optional(),
656
+ minWords: z.number().int().min(0).max(10).optional(),
657
+ minDuration: z.number().int().min(0).max(5e3).optional()
658
+ }).optional(),
659
+ endpointing: z.object({
660
+ minDelay: z.number().int().min(0).max(5e3).optional(),
661
+ maxDelay: z.number().int().min(0).max(15e3).optional()
662
+ }).optional()
663
+ }).optional(),
664
+ // Graph-preserving compiled program. When present and the flow-runtime graph
665
+ // mode is enabled, the worker executes this node-graph deterministically
666
+ // (the interpreter) instead of the flat goals+rails form above. Embedded here
667
+ // (not a new column) so the existing fetch path delivers it with no migration.
668
+ program: FlowProgram.optional()
669
+ });
670
+ z.object({
671
+ name: AgentName,
672
+ version: AgentVersion,
673
+ environment: AgentEnvironment,
674
+ // Initial status. Defaults to 'online'; SDK sends 'degraded' when any
675
+ // readiness probe failed but registration wasn't required-ready.
676
+ status: z.enum(["online", "degraded"]).optional(),
677
+ statusDetail: z.string().max(512).optional(),
678
+ capabilities: z.array(AgentCapability).max(64).optional(),
679
+ readiness: z.array(ReadinessReport).max(64).optional(),
680
+ metadata: AgentMetadata.optional(),
681
+ processSchema: ProcessSchemaDTO.nullable().optional(),
682
+ // A code-first agent's base prompt (its instructions live in code, not the DB).
683
+ // Synced here so a generic worker can run it over text (P2). Omit for flow agents.
684
+ // Cap matches AgentConfig.systemPrompt (64k) so a large-but-valid prompt doesn't
685
+ // fail registration outright (which agent.ts swallows → agent runs but never
686
+ // registers/heartbeats).
687
+ prompt: z.string().max(64e3).nullable().optional(),
688
+ // The text-turn protocol this worker speaks (burn-down G-4): it registers `<name>::text` and runs text turns as the
689
+ // agent. Absent = it can't (an older SDK, a puppet, a flow pool) — stored as null, so a downgrade stops text turns.
690
+ // Any version is accepted (a newer SDK never fails registration); the API decides which versions it dispatches to.
691
+ textTurns: z.number().int().min(1).max(1e3).optional(),
692
+ // Opt into MCP exposure. When true the API upserts a matching agent_config
693
+ // row keyed by (project_id, name, environment) so MCP / dashboard / REST
694
+ // can list and inspect this agent's identity. When false / omitted, the
695
+ // agent stays invisible to MCP; identity is purely in worker code.
696
+ mcpExposed: z.boolean().optional()
697
+ });
698
+ var AgentDTO = z.object({
699
+ id: z.string().uuid(),
700
+ projectId: z.string(),
701
+ name: AgentName,
702
+ version: AgentVersion,
703
+ environment: AgentEnvironment,
704
+ status: AgentStatus,
705
+ statusDetail: z.string().nullable(),
706
+ capabilities: z.array(AgentCapability),
707
+ readiness: z.array(ReadinessReport),
708
+ lastSeenAt: z.string().datetime().nullable(),
709
+ metadata: AgentMetadata,
710
+ processSchema: ProcessSchemaDTO.nullable().optional(),
711
+ // Whether this agent can run through the engine over text: a flow agent (its
712
+ // compiled graph rides in processSchema.program), a brain-connector agent, OR a
713
+ // code-first agent whose worker takes text turns (G-4). Drives the dashboard's chat gate so it
714
+ // no longer hard-codes "flow only". Computed server-side; not stored. OPTIONAL
715
+ // on the wire so a new worker validating a response from an OLD api pod during
716
+ // a rolling deploy doesn't fail register/heartbeat on a missing field.
717
+ textRunnable: z.boolean().optional(),
718
+ createdAt: z.string().datetime(),
719
+ updatedAt: z.string().datetime()
720
+ });
721
+ z.object({
722
+ processSchema: ProcessSchemaDTO.nullable()
723
+ });
724
+ z.object({
725
+ processSchema: ProcessSchemaDTO.nullable()
726
+ });
727
+ z.object({
728
+ prompt: z.string().nullable()
729
+ });
730
+ z.object({
731
+ agent: AgentDTO,
732
+ // Server-controlled cadence so we can tune without an SDK release.
733
+ heartbeatIntervalMs: z.number().int().positive()
734
+ });
735
+ z.object({
736
+ status: z.enum(["online", "degraded"]).optional(),
737
+ statusDetail: z.string().max(512).optional(),
738
+ metadata: AgentMetadata.optional()
739
+ });
740
+ z.object({
741
+ agent: AgentDTO,
742
+ heartbeatIntervalMs: z.number().int().positive()
743
+ });
744
+ z.object({
745
+ environment: AgentEnvironment.optional()
746
+ });
747
+ z.object({
748
+ agents: z.array(AgentDTO)
749
+ });
750
+ z.object({
751
+ ok: z.literal(true)
752
+ });
753
+ var ProcessLifecycleEvent = z.object({
754
+ id: z.string().min(1),
755
+ at: z.string().datetime(),
756
+ kind: z.string().min(1).max(80),
757
+ source: z.enum(["agent", "caller", "form", "platform"]),
758
+ data: z.record(z.string(), z.unknown())
759
+ });
760
+ var ProcessSnapshotPayload = z.object({
761
+ schemaId: z.string(),
762
+ currentNode: z.string(),
763
+ fieldsCaptured: z.record(z.string(), z.unknown()),
764
+ backendAckReceived: z.boolean(),
765
+ backendAckReceivedAt: z.string().datetime().optional(),
766
+ // Optional for back-compat with publishers built before this field
767
+ // existed. The cold-path → SSE pipe is otherwise pass-through.
768
+ lifecycle: z.array(ProcessLifecycleEvent).optional()
769
+ });
770
+ z.object({
771
+ callId: z.string().min(1),
772
+ agentId: z.string().min(1).nullable(),
773
+ projectId: z.string().min(1),
774
+ ts: z.string().datetime(),
775
+ snapshot: ProcessSnapshotPayload
776
+ });
777
+ z.enum([
778
+ "consultation.requested",
779
+ "consultation.resolved"
780
+ ]);
781
+ z.object({
782
+ consultationId: z.string().uuid(),
783
+ // Free-form question. Generic on purpose — no vertical-specific fields.
784
+ question: z.string().min(1).max(2e3),
785
+ // Transcript context the agent felt was relevant when asking.
786
+ transcriptSnippet: z.string().max(4e3).optional(),
787
+ // Hint to the orchestrator about how aggressively to interrupt the human.
788
+ urgency: z.enum(["low", "normal", "high"]).default("normal"),
789
+ // Server-clamped to ConsultationPolicy.maxTimeoutMs and ProjectLimits.
790
+ timeoutMs: z.number().int().min(1e3).max(3e5)
791
+ });
792
+ z.object({
793
+ consultationId: z.string().uuid(),
794
+ // 'timeout' = per-consult cap. 'cumulative_cap' = call-wide ceiling.
795
+ // 'cancelled' = agent decided it no longer needed an answer.
796
+ resolution: z.enum(["guidance", "timeout", "cumulative_cap", "cancelled"]),
797
+ guidance: z.string().max(4e3).optional(),
798
+ // Wall-clock from requested-emit → resolved-emit. SLA tracking.
799
+ latencyMs: z.number().int().min(0)
800
+ });
801
+ var GuidanceCommand = z.object({
802
+ kind: z.literal("guidance"),
803
+ consultationId: z.string().uuid(),
804
+ guidance: z.string().min(1).max(4e3),
805
+ // Optional bridge phrase the agent may speak before delivering guidance.
806
+ resumePhrase: z.string().max(500).optional(),
807
+ // G-5: the API's id for this command; the agent confirms it (CallControlAck). Optional so a command from an older
808
+ // API is still acted on (just not confirmed).
809
+ commandId: z.string().uuid().optional()
810
+ });
811
+ var ConsultationPolicy = z.object({
812
+ enabled: z.boolean().default(true),
813
+ // Per-consult default. ProjectLimits.perCallMaxTimeoutMs clamps this.
814
+ defaultTimeoutMs: z.number().int().min(1e3).default(3e4),
815
+ maxTimeoutMs: z.number().int().min(1e3).default(3e4),
816
+ // Cumulative cap across the call. ProjectLimits clamps.
817
+ maxCumulativeHoldMs: z.number().int().min(1e3).default(12e4),
818
+ // Spoken when the agent emits consultation.requested.
819
+ holdPhrase: z.string().min(1).max(500).default("Let me check on that."),
820
+ // Spoken when a single consultation times out — the call continues.
821
+ fallbackPhrase: z.string().min(1).max(500).default("Sorry, I don't have that information right now."),
822
+ // Spoken when the cumulative cap fires — the call ends after this.
823
+ callEndFallback: z.string().min(1).max(500).default("I'm going to need someone to call you back. Thank you."),
824
+ // Optional per-urgency timeout override. Lets ops shorten 'high' urgency
825
+ // holds without lowering the default.
826
+ urgencyOverrides: z.object({
827
+ low: z.number().int().min(1e3).optional(),
828
+ normal: z.number().int().min(1e3).optional(),
829
+ high: z.number().int().min(1e3).optional()
830
+ }).optional()
831
+ });
832
+
833
+ // ../contracts/src/call-events.ts
834
+ var CallEventKind = z.enum([
835
+ "status.dialing",
836
+ "status.dispatched",
837
+ "status.ringing",
838
+ "status.answered",
839
+ "status.ended",
840
+ "transcript.partial",
841
+ "transcript.final",
842
+ "tts.spoken",
843
+ "cost.update",
844
+ // Consultation lifecycle — see packages/contracts/src/consultation.ts.
845
+ // Emitted by the agent's built-in `ask_host` tool. The dashboard and MCP
846
+ // host SSE clients filter on these to surface the "let me check" UX.
847
+ "consultation.requested",
848
+ "consultation.resolved",
849
+ // DTMF keypad events. `dtmf.received` fires when a remote participant
850
+ // presses a key on their leg; `dtmf.sent` fires when our agent (or a
851
+ // programmatic caller via /v1/calls/:id/dtmf) emits one outbound.
852
+ "dtmf.received",
853
+ "dtmf.sent"
854
+ ]);
855
+ var OP_CALL_EVENT_KIND = /^(tool|handoff|lookup|record|notify|engine)\.[a-z0-9_]{1,48}(\.[a-z0-9_]{1,48})?$/;
856
+ var OpCallEventKind = z.string().regex(OP_CALL_EVENT_KIND);
857
+ z.object({
858
+ reason: z.string().optional(),
859
+ durationMs: z.number().int().min(0).optional(),
860
+ endedAt: z.string().datetime().optional()
861
+ });
862
+ z.object({
863
+ speaker: z.enum(["caller", "agent"]),
864
+ text: z.string(),
865
+ startMs: z.number().int().min(0),
866
+ endMs: z.number().int().min(0)
867
+ });
868
+ z.object({
869
+ text: z.string(),
870
+ // The MCP client sets this so it can correlate the spoken event to the
871
+ // call_say invocation that triggered it (used for "did my say actually go
872
+ // through" confirmation in the host chat).
873
+ correlationId: z.string().optional()
874
+ });
875
+ z.object({
876
+ digit: z.string().regex(/^[0-9*#A-D]$/),
877
+ code: z.number().int().min(0).max(15),
878
+ participantId: z.string().optional()
879
+ });
880
+ z.object({
881
+ totalUsd: z.string(),
882
+ breakdown: z.object({
883
+ llmUsd: z.string().optional(),
884
+ ttsUsd: z.string().optional(),
885
+ sttUsd: z.string().optional(),
886
+ telephonyUsd: z.string().optional()
887
+ }).optional()
888
+ });
889
+ z.object({
890
+ // Global bigserial assigned at insert. SSE clients use this as Last-Event-ID.
891
+ seq: z.number().int().min(0),
892
+ callId: z.string().uuid(),
893
+ projectId: z.string().min(1),
894
+ // Closed lifecycle kinds OR the namespaced operational vocabulary.
895
+ kind: z.union([CallEventKind, OpCallEventKind]),
896
+ payload: z.unknown(),
897
+ ts: z.string().datetime()
898
+ });
899
+ z.enum(["say", "hangup", "dtmf", "instruct", "guidance"]);
900
+ var commandId = z.string().uuid().optional();
901
+ var SayCommand = z.object({
902
+ kind: z.literal("say"),
903
+ commandId,
904
+ text: z.string().min(1),
905
+ // 'interrupt' replaces any in-flight TTS; 'queue' waits for current to end.
906
+ mode: z.enum(["interrupt", "queue"]).default("interrupt"),
907
+ correlationId: z.string().optional()
908
+ });
909
+ var HangupCommand = z.object({
910
+ kind: z.literal("hangup"),
911
+ commandId,
912
+ reason: z.string().optional()
913
+ });
914
+ var DtmfCommand = z.object({
915
+ kind: z.literal("dtmf"),
916
+ commandId,
917
+ digits: z.string().regex(/^[0-9*#]+$/)
918
+ });
919
+ var InstructCommand = z.object({
920
+ kind: z.literal("instruct"),
921
+ commandId,
922
+ text: z.string().min(1)
923
+ });
924
+ z.discriminatedUnion("kind", [
925
+ SayCommand,
926
+ HangupCommand,
927
+ DtmfCommand,
928
+ InstructCommand,
929
+ GuidanceCommand
930
+ ]);
931
+ z.object({
932
+ commandId: z.string().uuid(),
933
+ outcome: z.enum(["applied", "refused", "failed"]),
934
+ // machine code: output_blocked · not_supported · unknown_consultation · no_consultation · invalid_command ·
935
+ // say_unavailable · dtmf_unavailable
936
+ code: z.string().max(64).optional(),
937
+ detail: z.string().max(500).optional()
938
+ });
939
+ z.object({
940
+ mode: z.literal("text"),
941
+ turnId: z.string().uuid(),
942
+ projectId: z.string().min(1),
943
+ agentId: z.string().uuid()
944
+ });
945
+ var TextTurnHistoryEntry = z.object({
946
+ role: z.enum(["user", "assistant"]),
947
+ content: z.string().max(16e3)
948
+ });
949
+ z.object({
950
+ turnId: z.string().uuid(),
951
+ agentId: z.string().uuid(),
952
+ /** The Line the message came in on (`sms`, `discord`, `web`, `playground`, …). */
953
+ channel: z.string().min(1).max(40),
954
+ /** The sender on that Line (phone number, Discord user, widget visitor) — the agent's caller-scoped memory key. */
955
+ from: z.string().min(1).max(200),
956
+ /** The conversation this turn belongs to (stable across turns) — the tool context's call id. */
957
+ conversationKey: z.string().min(1).max(300),
958
+ history: z.array(TextTurnHistoryEntry).max(80),
959
+ text: z.string().min(1).max(8e3),
960
+ /** When the API stops waiting; the worker stops the run before it. */
961
+ deadlineAt: z.string().datetime()
962
+ });
963
+ var TextTurnUsage = z.object({
964
+ provider: z.string().min(1).max(60),
965
+ model: z.string().min(1).max(200),
966
+ inputTokens: z.number().int().nonnegative(),
967
+ outputTokens: z.number().int().nonnegative(),
968
+ /** Only a first-party worker's label is used; a customer's worker runs on its own key (the API decides). */
969
+ byok: z.boolean()
970
+ });
971
+ var TextTurnErrorCode = z.enum(["agent_unavailable", "agent_timeout"]);
972
+ z.union([
973
+ z.object({
974
+ agentId: z.string().uuid(),
975
+ replies: z.array(z.string().max(16e3)).max(20),
976
+ usage: z.array(TextTurnUsage).max(50).default([])
977
+ }),
978
+ z.object({
979
+ agentId: z.string().uuid(),
980
+ error: z.object({ code: TextTurnErrorCode, detail: z.string().max(500).optional() })
981
+ })
982
+ ]);
983
+ var VoiceConfig = z.object({
984
+ // Finite, vetted list — platform-level enum.
985
+ provider: z.enum(["deepgram", "elevenlabs", "openai", "cartesia"]),
986
+ voiceId: z.string().min(1).max(128),
987
+ // TTS synthesis model (provider-specific, e.g. 'eleven_turbo_v2_5',
988
+ // 'sonic-2'). Optional — providers like Deepgram fold the voice into the
989
+ // model id (aura-2-harmonia-en) and ignore this.
990
+ model: z.string().min(1).max(128).optional(),
991
+ // Speed knob narrow to what every provider supports — keeps it portable.
992
+ rate: z.number().min(0.5).max(2).optional()
993
+ });
994
+ var ModelConfig = z.object({
995
+ provider: z.enum(["openai", "anthropic", "gemini"]),
996
+ model: z.string().min(1).max(128),
997
+ temperature: z.number().min(0).max(2).optional(),
998
+ // Per-call ceiling. Distinct from project-level quota in ProjectLimits.
999
+ maxOutputTokens: z.number().int().min(1).max(32768).optional(),
1000
+ // Advanced nucleus-sampling knob. Optional — most builders leave it unset
1001
+ // and rely on temperature alone.
1002
+ topP: z.number().min(0).max(1).optional()
1003
+ });
1004
+ var SttConfig = z.object({
1005
+ provider: z.enum(["deepgram", "cartesia", "assemblyai"]),
1006
+ model: z.string().min(1).max(128).optional(),
1007
+ // BCP-47; falls back to AgentConfig.language when unset.
1008
+ language: z.string().min(2).max(16).optional(),
1009
+ // End-of-turn confidence (higher = wait for more certainty before the agent
1010
+ // takes its turn). The single biggest perceived-latency lever in voice.
1011
+ endOfTurnConfidence: z.number().min(0).max(1).optional(),
1012
+ // Hard cap (ms) on how long to wait for end-of-turn before responding.
1013
+ endOfTurnTimeoutMs: z.number().int().min(100).max(5e3).optional(),
1014
+ // Keyword boost list — bias transcription toward these terms.
1015
+ keywords: z.array(z.string().min(1).max(64)).max(100).optional()
1016
+ });
1017
+ var RealtimeConfig = z.object({
1018
+ provider: z.enum(["openai", "google"]),
1019
+ model: z.string().min(1).max(128),
1020
+ // Provider-native voice id ('alloy', 'echo' for OpenAI; 'Aoede', 'Puck' for
1021
+ // Gemini Live). Optional — provider default applies when unset.
1022
+ voice: z.string().min(1).max(128).optional(),
1023
+ temperature: z.number().min(0).max(2).optional()
1024
+ });
1025
+ var PipelineMode = z.enum(["cascade", "realtime"]);
1026
+ var ToolBinding = z.object({
1027
+ toolName: z.string().min(1).max(128),
1028
+ // Per-agent override of the tool's default args (e.g. an API base URL).
1029
+ // Generic Record on purpose — no vertical-specific shape.
1030
+ defaultArgs: z.record(z.unknown()).optional()
1031
+ });
1032
+ var ProcessFieldType = z.enum([
1033
+ "string",
1034
+ "string[]",
1035
+ "number",
1036
+ "boolean",
1037
+ "datetime",
1038
+ "email",
1039
+ "phone"
1040
+ ]);
1041
+ var ProcessFieldSpec = z.object({
1042
+ type: ProcessFieldType,
1043
+ required: z.boolean().optional(),
1044
+ // Regex source string (not a RegExp object — JSON-safe). Applied to
1045
+ // `string` and `string[]` items by the SDK at validation time.
1046
+ pattern: z.string().max(500).optional(),
1047
+ min: z.number().optional(),
1048
+ max: z.number().optional(),
1049
+ enum: z.array(z.string().min(1).max(200)).max(50).optional(),
1050
+ // Override the auto-generated capture prompt.
1051
+ ask: z.string().max(500).optional(),
1052
+ // Override the field-extraction hint shown to the LLM.
1053
+ extract: z.string().max(500).optional()
1054
+ });
1055
+ var ProcessCompleteWhen = z.enum([
1056
+ "all-required-captured",
1057
+ "all-captured"
1058
+ ]);
1059
+ var ProcessBackendAckSpec = z.object({
1060
+ url: z.string().url(),
1061
+ timeoutMs: z.number().int().min(100).max(6e4).optional(),
1062
+ headers: z.record(z.string(), z.string()).optional()
1063
+ });
1064
+ var ProcessSpec = z.object({
1065
+ // Defaults to {} so LLM-authored plans can emit `process: { completeWhen }`
1066
+ // without naming any structured fields. Downstream agent runtime handles
1067
+ // empty maps as "no callee-side capture, only the completeWhen criterion."
1068
+ collect: z.record(z.string().min(1).max(128), ProcessFieldSpec).default({}),
1069
+ completeWhen: ProcessCompleteWhen.default("all-required-captured"),
1070
+ backendAck: ProcessBackendAckSpec.optional()
1071
+ });
1072
+ var TriggerConditionSpec = z.discriminatedUnion("kind", [
1073
+ z.object({
1074
+ kind: z.literal("regex"),
1075
+ pattern: z.string().min(1).max(500),
1076
+ flags: z.string().max(8).optional()
1077
+ }),
1078
+ z.object({
1079
+ kind: z.literal("signal"),
1080
+ name: z.string().min(1).max(128)
1081
+ })
1082
+ ]);
1083
+ var TriggerActionSpec = z.discriminatedUnion("kind", [
1084
+ z.object({ kind: z.literal("handoff") }),
1085
+ z.object({ kind: z.literal("endCall") }),
1086
+ z.object({ kind: z.literal("say"), text: z.string().min(1).max(2e3) })
1087
+ ]);
1088
+ var TriggerSpec = z.object({
1089
+ on: TriggerConditionSpec,
1090
+ then: TriggerActionSpec,
1091
+ // If set, the trigger only fires while the named process field is uncaptured.
1092
+ onlyWhile: z.string().min(1).max(128).optional()
1093
+ });
1094
+ var RequiredInfoField = z.object({
1095
+ name: z.string().min(1).max(64),
1096
+ type: z.enum(["string", "string[]", "number", "boolean", "datetime", "email", "phone"]),
1097
+ required: z.boolean(),
1098
+ // The field's `ask` prompt — what the value represents / how to source it.
1099
+ description: z.string().max(512).optional(),
1100
+ // Constrained value set, when the field is an enum.
1101
+ enum: z.array(z.string()).max(64).optional()
1102
+ });
1103
+ var AgentConfig = z.object({
1104
+ id: z.string().uuid(),
1105
+ projectId: z.string().min(1),
1106
+ // Human-readable label. No `agentType` enum on purpose.
1107
+ name: z.string().min(1).max(128),
1108
+ // Soft override of the worker's baked-in prompt. Absent → call_start
1109
+ // uses the SDK worker's predefined prompt (the source of truth for
1110
+ // pre-defined agents like FNOL). Present → it overrides the worker's
1111
+ // prompt at call_start as a snapshot decision; mid-call edits are not
1112
+ // honoured. Per-call override at call_start time is also allowed.
1113
+ systemPrompt: z.string().max(64e3).optional(),
1114
+ // Routing/decision layer (Voiceflow "Instructions"): plain-language guidance the
1115
+ // agent reads to decide WHEN to do things (vs systemPrompt = WHO it is). Injected
1116
+ // into the system prompt below the persona. Interpolated with {{ }} at runtime.
1117
+ routingInstructions: z.string().max(32e3).optional(),
1118
+ voice: VoiceConfig,
1119
+ model: ModelConfig,
1120
+ // STT for the cascade pipeline. Absent → SDK default (Deepgram nova-3).
1121
+ stt: SttConfig.optional(),
1122
+ // Pipeline mode. Defaults to 'cascade' so pre-existing rows (which have no
1123
+ // mode column) behave exactly as before.
1124
+ mode: PipelineMode.default("cascade"),
1125
+ // Realtime / speech-to-speech model. Used when mode === 'realtime'; ignored
1126
+ // otherwise. Optional in the schema so cascade-only agents omit it.
1127
+ realtime: RealtimeConfig.optional(),
1128
+ // BCP-47 string. Free-form so customers add locales without a release.
1129
+ language: z.string().min(2).max(16).default("en-US"),
1130
+ tools: z.array(ToolBinding).max(64).default([]),
1131
+ consultation: ConsultationPolicy,
1132
+ // Structured outcome — the data the agent collects on the call.
1133
+ // Optional; absent means the call has no structured outcome.
1134
+ process: ProcessSpec.optional(),
1135
+ // Deterministic routes that fire BEFORE the LLM. Keyed by stable string
1136
+ // ids (e.g. 'cancellation_fee', 'asked_for_card') so call_plan revisions
1137
+ // can address a specific trigger.
1138
+ triggers: z.record(z.string().min(1).max(128), TriggerSpec).optional(),
1139
+ // Free-form labels — no semantic enums.
1140
+ tags: z.array(z.string().max(64)).max(32).optional(),
1141
+ // Bound brain connector (bring-your-own LLM). When set, the worker drives the
1142
+ // LLM slot through the connector instead of a first-party model. Null/absent →
1143
+ // first-party pipeline (unchanged default). See docs/specs/voice-brain-connector.
1144
+ brainConnectorId: z.string().uuid().nullable().optional(),
1145
+ // Provenance: 'sdk' (hand-coded defineAgent worker), 'flow' (canvas deploy),
1146
+ // 'playbook' (agent-mode deploy), or 'connector' (brain-connector agent).
1147
+ // DERIVED from the linked agents row's deploy tags (flowId / playbookId /
1148
+ // connectorId in metadata) — not stored on agent_config, never PATCHed.
1149
+ source: z.enum(["sdk", "flow", "playbook", "connector"]).optional(),
1150
+ // True when the pipeline MODE is locked to code (genuine SDK agents only):
1151
+ // the dashboard renders mode read-only and PATCH rejects a mode change.
1152
+ // Derived; read-only. Flow/playbook/connector agents → false.
1153
+ modeLocked: z.boolean().optional(),
1154
+ // Inputs this agent needs to run a call, projected from its process schema.
1155
+ // An MCP host reads these (via list_agents) to pre-fill known values as
1156
+ // call_start metadata so the agent never re-asks. Empty when the agent
1157
+ // collects nothing structured. DERIVED from the agents row; read-only.
1158
+ requiredInfo: z.array(RequiredInfoField).max(64).default([]),
1159
+ createdAt: z.string().datetime(),
1160
+ // Optimistic-lock token. PATCH must echo this in If-Match.
1161
+ updatedAt: z.string().datetime()
1162
+ });
1163
+
1164
+ // ../contracts/src/call-plan.ts
1165
+ var PlanStatus = z.enum([
1166
+ "drafting",
1167
+ "clarifying",
1168
+ "ready",
1169
+ "committed",
1170
+ "abandoned"
1171
+ ]);
1172
+ var PlanCallee = z.object({
1173
+ e164: z.string().min(2).optional(),
1174
+ // may be unknown at intent time
1175
+ displayName: z.string().max(200).optional(),
1176
+ // What the host knows about who they're calling. e.g. "their dentist's
1177
+ // billing office", "a hiring manager named Priya". Seeds tone/register
1178
+ // and likely-triggers; not used to gate behaviour.
1179
+ description: z.string().max(2e3).optional()
1180
+ });
1181
+ var PlanQuestion = z.object({
1182
+ id: z.string().min(1).max(128),
1183
+ // stable across rounds; e.g. 'policy_number'
1184
+ prompt: z.string().min(1).max(1e3),
1185
+ // 'fact' — the planner needs a piece of data (e.g. account #).
1186
+ // 'decision' — the planner needs the host to pick an option/policy.
1187
+ // 'auth' — the planner is checking the host has authority to act.
1188
+ kind: z.enum(["fact", "decision", "auth"]),
1189
+ // For 'decision' questions only.
1190
+ options: z.array(z.string().min(1).max(200)).max(20).optional(),
1191
+ // True if the agent could ask the callee for this mid-call instead of
1192
+ // demanding it from the host now. When true and the host can't answer,
1193
+ // the planner promotes this to a draftConfig.process.collect field
1194
+ // rather than blocking the call.
1195
+ deferrableToCallee: z.boolean().default(false)
1196
+ });
1197
+ var PlanEscalation = z.object({
1198
+ // 'consult_host' — ask the MCP host (default for plan-first calls).
1199
+ // 'ask_callee' — stay on the line and ask the caller.
1200
+ // 'fallback_phrase' — read the agent's configured fallback line.
1201
+ onUncertainty: z.enum(["consult_host", "ask_callee", "fallback_phrase"]).default("consult_host"),
1202
+ // Hard cap on host consultations for this call. Independent of
1203
+ // ConsultationPolicy time caps which still apply.
1204
+ maxHostConsults: z.number().int().min(0).max(20).optional(),
1205
+ // If host doesn't reply within this window, fall through to `secondary`.
1206
+ // Server-clamps to ConsultationPolicy.maxTimeoutMs.
1207
+ hostTimeoutMs: z.number().int().min(1e3).max(3e5).optional(),
1208
+ secondary: z.enum(["ask_callee", "fallback_phrase"]).default("fallback_phrase")
1209
+ });
1210
+ var DraftAgentConfig = AgentConfig.omit({
1211
+ id: true,
1212
+ projectId: true,
1213
+ createdAt: true,
1214
+ updatedAt: true
1215
+ }).partial();
1216
+ var CallPlan = z.object({
1217
+ planId: z.string().uuid(),
1218
+ projectId: z.string().min(1),
1219
+ status: PlanStatus,
1220
+ // Why we're calling. One sentence, free-form. Kept verbatim from the
1221
+ // host's original IntentRequest so we can replay planning. The refined
1222
+ // version lives in draftConfig.systemPrompt.
1223
+ objective: z.string().min(1).max(2e3),
1224
+ callee: PlanCallee,
1225
+ // The AgentConfig the planner is building. Grows across clarify rounds.
1226
+ // At status='ready' this is a complete (modulo defaulted fields)
1227
+ // AgentConfig override; at status='committed' it's the snapshot the
1228
+ // call booted with.
1229
+ draftConfig: DraftAgentConfig.default({}),
1230
+ // Pre-call host-facing clarifications. When non-empty AND
1231
+ // status='clarifying', the host must answer via call_clarify. When
1232
+ // non-empty AND status='ready', the remaining questions were either
1233
+ // deferred to callee (see unresolvedQuestions) or dropped after
1234
+ // exhausting clarificationRounds — see changeSummary on each
1235
+ // PlanResponse for what happened.
1236
+ openQuestions: z.array(PlanQuestion).max(20).default([]),
1237
+ // Snapshot of deferrableToCallee questions that the planner couldn't
1238
+ // resolve and instead promoted to draftConfig.process.collect at
1239
+ // commit time. Kept for audit + so the host can see what the agent
1240
+ // will ask the callee.
1241
+ unresolvedQuestions: z.array(PlanQuestion).max(20).default([]),
1242
+ escalation: PlanEscalation,
1243
+ // Round counter. Server enforces PlanLimits.maxClarificationRounds.
1244
+ clarificationRounds: z.number().int().min(0).default(0),
1245
+ // Set once `call_commit` succeeds. Links the plan to the call it
1246
+ // created. Useful for the dashboard plan inspector.
1247
+ committedCallId: z.string().uuid().optional(),
1248
+ createdAt: z.string().datetime(),
1249
+ updatedAt: z.string().datetime()
1250
+ });
1251
+ z.object({
1252
+ objective: z.string().min(1).max(2e3),
1253
+ callee: PlanCallee.optional(),
1254
+ // Initial facts the host already has. Folded into draftConfig at the
1255
+ // planner's discretion — typically inlined into systemPrompt or stored
1256
+ // as process.collect default values.
1257
+ knowns: z.record(z.string().min(1).max(128), z.string().max(4e3)).optional(),
1258
+ // Hard constraints the planner must respect. e.g.
1259
+ // ["never disclose the card CVV", "if asked for SSN, refuse politely"].
1260
+ // Generate matching triggers + appear in systemPrompt as do-not-disclose
1261
+ // rules.
1262
+ constraints: z.array(z.string().min(1).max(500)).max(20).optional(),
1263
+ // Per-call escalation overrides — merged with project defaults.
1264
+ escalation: PlanEscalation.partial().optional(),
1265
+ // Override the agent's default voice/model/language for this call. Most
1266
+ // hosts won't set this; here for power users.
1267
+ configOverrides: DraftAgentConfig.pick({
1268
+ voice: true,
1269
+ model: true,
1270
+ language: true
1271
+ }).optional()
1272
+ });
1273
+ z.object({
1274
+ // Map question.id → answer text. For 'decision' questions the value
1275
+ // should be one of `options`, but the server doesn't enforce strictly —
1276
+ // the planner will re-ask if the answer is unrecognized.
1277
+ answers: z.record(z.string().min(1).max(128), z.string().max(4e3)),
1278
+ // The host can also push new facts that weren't asked for.
1279
+ additionalKnowns: z.record(z.string().min(1).max(128), z.string().max(4e3)).optional()
1280
+ });
1281
+ z.object({
1282
+ to: z.string().min(2),
1283
+ // E.164
1284
+ phoneNumberId: z.string().uuid(),
1285
+ metadata: z.record(z.string().min(1).max(128), z.string().max(4e3)).optional(),
1286
+ force: z.boolean().default(false)
1287
+ });
1288
+ z.object({
1289
+ plan: CallPlan,
1290
+ // Convenience: questions to answer next. Same as plan.openQuestions
1291
+ // when status='clarifying'; empty otherwise.
1292
+ questions: z.array(PlanQuestion),
1293
+ // Human-readable summary of what changed in this round. Useful for the
1294
+ // host's chat surface ("I added 3 facts and resolved 2 questions").
1295
+ changeSummary: z.string().max(2e3).optional()
1296
+ });
1297
+ z.object({
1298
+ callId: z.string().uuid(),
1299
+ plan: CallPlan
1300
+ // status will be 'committed'
1301
+ });
1302
+ z.object({
1303
+ // After this many round-trips, the planner forces status='ready' and
1304
+ // any remaining questions are marked deferrableToCallee or dropped.
1305
+ maxClarificationRounds: z.number().int().min(1).default(3),
1306
+ // How long an uncommitted plan lives before it's auto-abandoned. Lets
1307
+ // the host build a plan in one session and commit it in another.
1308
+ planTtlMs: z.number().int().min(6e4).default(36e5)
1309
+ // 1h
1310
+ });
1311
+ var MemoryScope = z.enum(["call", "caller", "account", "platform"]);
1312
+ var MemoryRecord = z.record(z.unknown());
1313
+ var RelevantContext = z.object({
1314
+ call: MemoryRecord,
1315
+ caller: MemoryRecord,
1316
+ account: MemoryRecord,
1317
+ platform: MemoryRecord
1318
+ });
1319
+ z.object({
1320
+ callerId: z.string().min(1),
1321
+ scopes: z.array(MemoryScope).optional()
1322
+ });
1323
+ z.object({
1324
+ context: RelevantContext
1325
+ });
1326
+ z.object({
1327
+ scope: MemoryScope,
1328
+ callerId: z.string().min(1),
1329
+ delta: MemoryRecord,
1330
+ callId: z.string().optional()
1331
+ });
1332
+ z.object({
1333
+ accepted: z.literal(true)
1334
+ });
1335
+ z.object({
1336
+ callId: z.string().uuid(),
1337
+ stage: z.string().min(1).max(64),
1338
+ snapshot: MemoryRecord
1339
+ });
1340
+ z.object({
1341
+ id: z.string().uuid()
1342
+ });
1343
+ z.object({
1344
+ externalKey: z.string().min(1).max(256),
1345
+ displayName: z.string().max(256).optional(),
1346
+ identifiers: z.array(
1347
+ z.object({
1348
+ type: z.string().min(1).max(64),
1349
+ value: z.string().min(1).max(256)
1350
+ })
1351
+ ).optional()
1352
+ });
1353
+ var CallerDTO = z.object({
1354
+ id: z.string().uuid(),
1355
+ projectId: z.string(),
1356
+ externalKey: z.string(),
1357
+ displayName: z.string().nullable(),
1358
+ metadata: MemoryRecord,
1359
+ createdAt: z.string().datetime(),
1360
+ updatedAt: z.string().datetime()
1361
+ });
1362
+ z.object({
1363
+ caller: CallerDTO
1364
+ });
1365
+ z.object({
1366
+ ok: z.literal(true),
1367
+ scopesPurged: z.array(MemoryScope)
1368
+ });
1369
+ z.object({
1370
+ systemPrompt: z.string().max(64e3),
1371
+ routingInstructions: z.string().max(32e3).nullable(),
1372
+ voice: VoiceConfig,
1373
+ model: ModelConfig,
1374
+ stt: SttConfig.nullable(),
1375
+ mode: PipelineMode,
1376
+ realtime: RealtimeConfig.nullable(),
1377
+ language: z.string().min(2).max(16),
1378
+ tools: z.array(ToolBinding).max(64),
1379
+ consultation: ConsultationPolicy,
1380
+ tags: z.array(z.string().max(64)).max(32).nullable(),
1381
+ brainConnectorId: z.string().uuid().nullable(),
1382
+ flowVersion: z.number().int().nullable(),
1383
+ processSchema: ProcessSchemaDTO.nullable()
1384
+ });
1385
+ z.enum(["published", "draft"]);
1386
+ var AgentConfigVersionDTO = z.object({
1387
+ id: z.string().uuid(),
1388
+ version: z.number().int().min(1),
1389
+ createdAt: z.string().datetime(),
1390
+ // 'user:<clerkId>' · 'api_key:<id>' · 'system:backfill' · 'system:deploy' · 'system:register'
1391
+ createdBy: z.string().nullable(),
1392
+ note: z.string().nullable(),
1393
+ flowVersion: z.number().int().nullable(),
1394
+ isPublished: z.boolean()
1395
+ });
1396
+ z.object({
1397
+ versions: z.array(AgentConfigVersionDTO),
1398
+ publishedVersion: z.number().int().min(1)
1399
+ });
1400
+ z.object({
1401
+ note: z.string().trim().max(500).optional()
1402
+ });
1403
+ z.object({
1404
+ version: AgentConfigVersionDTO
1405
+ });
1406
+ var AgentConfigDiffField = z.enum([
1407
+ "systemPrompt",
1408
+ "routingInstructions",
1409
+ "mode",
1410
+ "model",
1411
+ "voice",
1412
+ "stt",
1413
+ "realtime",
1414
+ "language",
1415
+ "tools",
1416
+ "brainConnectorId",
1417
+ "consultation",
1418
+ "tags",
1419
+ "flowVersion",
1420
+ "processSchema"
1421
+ ]);
1422
+ var AgentConfigChange = z.object({
1423
+ field: AgentConfigDiffField,
1424
+ label: z.string(),
1425
+ /** The published value in plain words. */
1426
+ from: z.string(),
1427
+ /** The draft value in plain words. */
1428
+ to: z.string(),
1429
+ /** True for long text (prompts) — the sheet renders it in a scrollable block. */
1430
+ longText: z.boolean()
1431
+ });
1432
+ z.object({
1433
+ published: z.object({
1434
+ version: z.number().int().min(1),
1435
+ createdAt: z.string().datetime(),
1436
+ note: z.string().nullable()
1437
+ }),
1438
+ hasChanges: z.boolean(),
1439
+ changes: z.array(AgentConfigChange)
1440
+ });
1441
+
1442
+ // ../contracts/src/providers.ts
1443
+ var PROVIDERS = {
1444
+ openai: { id: "openai", label: "OpenAI", capabilities: ["llm", "tts", "realtime"], byok: true, supportsBaseUrl: true, keyPlaceholder: "sk-..." },
1445
+ anthropic: { id: "anthropic", label: "Anthropic", capabilities: ["llm"], byok: true, supportsBaseUrl: true, keyPlaceholder: "sk-ant-..." },
1446
+ deepgram: { id: "deepgram", label: "Deepgram", capabilities: ["stt", "tts"], byok: true, supportsBaseUrl: false, keyPlaceholder: "Deepgram API key" },
1447
+ elevenlabs: { id: "elevenlabs", label: "ElevenLabs", capabilities: ["tts"], byok: true, supportsBaseUrl: false, keyPlaceholder: "ElevenLabs API key" },
1448
+ cartesia: { id: "cartesia", label: "Cartesia", capabilities: ["stt", "tts"], byok: true, supportsBaseUrl: false, keyPlaceholder: "Cartesia API key" },
1449
+ assemblyai: { id: "assemblyai", label: "AssemblyAI", capabilities: ["stt"], byok: true, supportsBaseUrl: false, keyPlaceholder: "AssemblyAI API key" },
1450
+ google: { id: "google", label: "Google (Gemini)", capabilities: ["llm", "realtime"], byok: true, supportsBaseUrl: false, keyPlaceholder: "AIza..." },
1451
+ groq: { id: "groq", label: "Groq", capabilities: ["llm"], byok: true, supportsBaseUrl: true, keyPlaceholder: "gsk_..." },
1452
+ // Runtime-only providers (no external key): local VAD.
1453
+ silero: { id: "silero", label: "Silero", capabilities: ["vad"], byok: false, supportsBaseUrl: false }
1454
+ };
1455
+ function byokProviders() {
1456
+ return Object.values(PROVIDERS).filter((p) => p.byok);
1457
+ }
1458
+ var ProviderKind = z.enum(["llm", "stt", "tts", "realtime"]);
1459
+ var CatalogVoice = z.object({
1460
+ id: z.string().min(1).max(128),
1461
+ label: z.string().min(1).max(128),
1462
+ // Optional sample for an in-picker preview button.
1463
+ previewUrl: z.string().url().optional(),
1464
+ // e.g. 'female', 'narration', 'en-US'.
1465
+ tags: z.array(z.string().max(40)).max(12).optional()
1466
+ });
1467
+ var CatalogModel = z.object({
1468
+ id: z.string().min(1).max(128),
1469
+ label: z.string().min(1).max(128),
1470
+ // Pricing hints (USD) shown next to the option. Only the fields relevant to
1471
+ // the provider kind are populated.
1472
+ inputPerMillion: z.number().nonnegative().optional(),
1473
+ // llm / realtime tokens
1474
+ outputPerMillion: z.number().nonnegative().optional(),
1475
+ perMillionChars: z.number().nonnegative().optional(),
1476
+ // tts
1477
+ perMinute: z.number().nonnegative().optional(),
1478
+ // stt / realtime session
1479
+ // Capability hints.
1480
+ contextWindow: z.number().int().positive().optional(),
1481
+ notes: z.string().max(200).optional()
1482
+ });
1483
+ var CatalogProvider = z.object({
1484
+ id: z.string().min(1).max(64),
1485
+ // 'openai' | 'anthropic' | 'deepgram' | ...
1486
+ label: z.string().min(1).max(64),
1487
+ kind: ProviderKind,
1488
+ models: z.array(CatalogModel).default([]),
1489
+ // Populated for tts / realtime providers.
1490
+ voices: z.array(CatalogVoice).optional(),
1491
+ // Pre-selected when the provider is chosen.
1492
+ defaultModel: z.string().max(128).optional()
1493
+ });
1494
+ z.object({
1495
+ llm: z.array(CatalogProvider).default([]),
1496
+ stt: z.array(CatalogProvider).default([]),
1497
+ tts: z.array(CatalogProvider).default([]),
1498
+ realtime: z.array(CatalogProvider).default([])
1499
+ });
1500
+ var CostEstimateAssumptions = z.object({
1501
+ callDurationSeconds: z.number().positive().max(7200).optional(),
1502
+ llmInputTokensPerMin: z.number().nonnegative().max(1e6).optional(),
1503
+ llmOutputTokensPerMin: z.number().nonnegative().max(1e6).optional(),
1504
+ ttsCharsPerMin: z.number().nonnegative().max(1e6).optional(),
1505
+ direction: z.enum(["inbound", "outbound"]).optional(),
1506
+ includeTelephony: z.boolean().optional()
1507
+ });
1508
+ z.object({
1509
+ mode: PipelineMode.default("cascade"),
1510
+ model: ModelConfig.optional(),
1511
+ // LLM (cascade)
1512
+ voice: VoiceConfig.optional(),
1513
+ // TTS (cascade)
1514
+ stt: SttConfig.optional(),
1515
+ // STT (cascade)
1516
+ realtime: RealtimeConfig.optional(),
1517
+ // realtime
1518
+ assumptions: CostEstimateAssumptions.optional()
1519
+ });
1520
+ var CostEstimatePerMinute = z.object({
1521
+ llm: z.number().nonnegative(),
1522
+ stt: z.number().nonnegative(),
1523
+ tts: z.number().nonnegative(),
1524
+ realtime: z.number().nonnegative(),
1525
+ telephony: z.number().nonnegative(),
1526
+ total: z.number().nonnegative()
1527
+ });
1528
+ var CostEstimateBilledPerMinute = z.object({
1529
+ platformFee: z.number().nonnegative(),
1530
+ providerPassthrough: z.number().nonnegative(),
1531
+ telephony: z.number().nonnegative(),
1532
+ allIn: z.number().nonnegative()
1533
+ });
1534
+ z.object({
1535
+ currency: z.literal("USD"),
1536
+ perMinute: CostEstimatePerMinute,
1537
+ // Present when the server could resolve the project's rate card.
1538
+ billedPerMinute: CostEstimateBilledPerMinute.optional(),
1539
+ // Rough voice-to-voice latency estimate for the chosen pipeline.
1540
+ latencyMs: z.object({
1541
+ estimate: z.number().nonnegative(),
1542
+ realtime: z.boolean()
1543
+ }),
1544
+ // The assumptions actually used (after defaults) — drives the UI footnote.
1545
+ assumptions: CostEstimateAssumptions.required()
1546
+ });
1547
+ var EvaluationMetric = z.enum(["rating", "binary", "options", "text"]);
1548
+ var EvaluationStatus = z.enum(["pending", "scored", "failed"]);
1549
+ var EvaluationOption = z.object({
1550
+ value: z.string().min(1).max(64),
1551
+ label: z.string().min(1).max(120),
1552
+ description: z.string().max(400).optional()
1553
+ });
1554
+ z.object({
1555
+ id: z.string().uuid(),
1556
+ projectId: z.string(),
1557
+ name: z.string().min(1).max(120),
1558
+ criteria: z.string().max(4e3),
1559
+ metric: EvaluationMetric,
1560
+ model: z.string().min(1).max(128),
1561
+ minValue: z.number().int().nullable(),
1562
+ minLabel: z.string().nullable(),
1563
+ maxValue: z.number().int().nullable(),
1564
+ maxLabel: z.string().nullable(),
1565
+ options: z.array(EvaluationOption),
1566
+ enabled: z.boolean(),
1567
+ isDefault: z.boolean(),
1568
+ createdAt: z.string().datetime(),
1569
+ updatedAt: z.string().datetime()
1570
+ });
1571
+ var EvaluationInput = z.object({
1572
+ name: z.string().min(1).max(120),
1573
+ criteria: z.string().max(4e3).default(""),
1574
+ metric: EvaluationMetric.default("rating"),
1575
+ model: z.string().min(1).max(128).default("gpt-4o-mini"),
1576
+ minValue: z.number().int().nullable().optional(),
1577
+ minLabel: z.string().max(200).nullable().optional(),
1578
+ maxValue: z.number().int().nullable().optional(),
1579
+ maxLabel: z.string().max(200).nullable().optional(),
1580
+ options: z.array(EvaluationOption).max(12).default([]),
1581
+ enabled: z.boolean().default(true)
1582
+ });
1583
+ EvaluationInput.partial();
1584
+ z.object({
1585
+ id: z.string().uuid(),
1586
+ callId: z.string().uuid(),
1587
+ projectId: z.string(),
1588
+ evaluationId: z.string().uuid(),
1589
+ name: z.string(),
1590
+ metric: EvaluationMetric,
1591
+ status: EvaluationStatus,
1592
+ valueNumber: z.number().nullable(),
1593
+ valueBool: z.boolean().nullable(),
1594
+ valueText: z.string().nullable(),
1595
+ reasoning: z.string().nullable(),
1596
+ model: z.string().nullable(),
1597
+ costUsd: z.number().nullable(),
1598
+ createdAt: z.string().datetime(),
1599
+ updatedAt: z.string().datetime()
1600
+ });
1601
+ z.object({
1602
+ status: EvaluationStatus,
1603
+ valueNumber: z.number().nullable(),
1604
+ valueBool: z.boolean().nullable(),
1605
+ valueText: z.string().nullable(),
1606
+ reasoning: z.string().nullable(),
1607
+ costUsd: z.number().nullable(),
1608
+ error: z.string().nullable()
1609
+ });
1610
+ z.object({
1611
+ evaluationId: z.string().uuid(),
1612
+ name: z.string(),
1613
+ metric: EvaluationMetric,
1614
+ scored: z.number(),
1615
+ average: z.number().nullable(),
1616
+ // rating
1617
+ positiveRate: z.number().nullable(),
1618
+ // binary (fraction true)
1619
+ distribution: z.array(z.object({ value: z.string(), count: z.number() })),
1620
+ // options/text
1621
+ avgCostUsd: z.number().nullable()
1622
+ });
1623
+ var TestCheckType = z.enum(["response-judge", "routing", "tool-call"]);
1624
+ var TestRunStatus = z.enum(["pending", "scored", "failed"]);
1625
+ var TestCheckStatus = z.enum(["pass", "fail", "skipped"]);
1626
+ z.object({
1627
+ id: z.string().uuid(),
1628
+ projectId: z.string(),
1629
+ name: z.string().min(1).max(120),
1630
+ description: z.string().max(4e3),
1631
+ criteria: z.string().max(4e3),
1632
+ enabled: z.boolean(),
1633
+ isDefault: z.boolean(),
1634
+ createdAt: z.string().datetime(),
1635
+ updatedAt: z.string().datetime()
1636
+ });
1637
+ var TestSuiteInput = z.object({
1638
+ name: z.string().min(1).max(120),
1639
+ description: z.string().max(4e3).default(""),
1640
+ criteria: z.string().max(4e3).default(""),
1641
+ enabled: z.boolean().default(true)
1642
+ });
1643
+ TestSuiteInput.partial();
1644
+ var TestRun = z.object({
1645
+ id: z.string().uuid(),
1646
+ projectId: z.string(),
1647
+ testSuiteId: z.string().uuid(),
1648
+ callId: z.string().uuid(),
1649
+ name: z.string(),
1650
+ status: TestRunStatus,
1651
+ reasoning: z.string().nullable(),
1652
+ model: z.string().nullable(),
1653
+ costUsd: z.number().nullable(),
1654
+ createdAt: z.string().datetime(),
1655
+ updatedAt: z.string().datetime()
1656
+ });
1657
+ var TestCheck = z.object({
1658
+ id: z.string().uuid(),
1659
+ projectId: z.string(),
1660
+ testRunId: z.string().uuid(),
1661
+ checkType: TestCheckType,
1662
+ turnIndex: z.number().int(),
1663
+ status: TestCheckStatus,
1664
+ input: z.unknown().nullable(),
1665
+ output: z.unknown().nullable(),
1666
+ reasoning: z.string().nullable(),
1667
+ createdAt: z.string().datetime(),
1668
+ updatedAt: z.string().datetime()
1669
+ });
1670
+ TestRun.extend({
1671
+ checks: z.array(TestCheck)
1672
+ });
1673
+ z.object({
1674
+ status: TestRunStatus,
1675
+ reasoning: z.string().nullable(),
1676
+ costUsd: z.number().nullable(),
1677
+ checks: z.array(
1678
+ z.object({
1679
+ checkType: TestCheckType,
1680
+ turnIndex: z.number().int(),
1681
+ status: TestCheckStatus,
1682
+ reasoning: z.string().nullable()
1683
+ })
1684
+ ),
1685
+ error: z.string().nullable()
1686
+ });
1687
+ z.object({
1688
+ testSuiteId: z.string().uuid(),
1689
+ name: z.string(),
1690
+ runs: z.number(),
1691
+ passed: z.number(),
1692
+ failed: z.number(),
1693
+ passRate: z.number().nullable(),
1694
+ avgCostUsd: z.number().nullable()
1695
+ });
1696
+ var CallThumb = z.enum(["up", "down"]);
1697
+ z.object({
1698
+ callId: z.string().uuid(),
1699
+ projectId: z.string(),
1700
+ thumb: CallThumb.nullable(),
1701
+ note: z.string().nullable(),
1702
+ reviewed: z.boolean(),
1703
+ reviewedBy: z.string().nullable(),
1704
+ createdAt: z.string().datetime(),
1705
+ updatedAt: z.string().datetime()
1706
+ });
1707
+ z.object({
1708
+ thumb: CallThumb.nullable().optional(),
1709
+ note: z.string().max(2e3).nullable().optional(),
1710
+ reviewed: z.boolean().optional()
1711
+ });
1712
+ var ToolExposure = z.enum(["agent", "mcp"]);
1713
+ z.object({
1714
+ name: z.string().min(1).max(64).regex(/^[a-z][a-z0-9_]*$/, "snake_case"),
1715
+ // The string that routes the LLM/orchestrator to invoke this tool.
1716
+ description: z.string().min(1).max(2e3),
1717
+ // Zod serialized to JSON Schema at registration time. Persisted as
1718
+ // JSON Schema; SDK and dashboard rehydrate to Zod when needed.
1719
+ inputSchema: z.record(z.unknown()),
1720
+ // When present, the runtime validates the handler's return value.
1721
+ outputSchema: z.record(z.unknown()).optional(),
1722
+ exposedVia: z.array(ToolExposure).min(1),
1723
+ // ── Tool/Plugin SDK (PRD P0.2). All optional for back-compat. ──
1724
+ // Latency class (PRD §4.6.3). Runtime defaults to 'in_call' when omitted.
1725
+ mode: z.enum(["in_call", "async_only"]).optional(),
1726
+ // Hard timeout for an in-call tool (ms).
1727
+ timeoutMs: z.number().int().min(50).max(6e4).optional(),
1728
+ // 'connection' resolves a credential from the vault by `connectionRef`.
1729
+ auth: z.enum(["none", "connection"]).optional(),
1730
+ connectionRef: z.string().max(128).optional(),
1731
+ // Retry policy for safe/async verbs.
1732
+ retry: z.object({
1733
+ maxAttempts: z.number().int().min(1).max(5),
1734
+ backoffMs: z.number().int().min(0).max(6e4)
1735
+ }).optional(),
1736
+ // Normalized result kinds the flow/decision layer branches on (result.kind).
1737
+ resultKinds: z.array(z.string().min(1).max(64)).max(24).optional(),
1738
+ // Free-form dashboard metadata (icon, group, etc.). No `category` enum.
1739
+ metadata: z.record(z.unknown()).optional()
1740
+ });
1741
+ var TranscriptTurn = z.object({
1742
+ speaker: z.enum(["caller", "agent"]),
1743
+ text: z.string(),
1744
+ startMs: z.number().int().min(0),
1745
+ endMs: z.number().int().min(0)
1746
+ });
1747
+ var ConsultationAudit = z.object({
1748
+ consultationId: z.string().uuid(),
1749
+ question: z.string(),
1750
+ resolution: z.enum(["guidance", "timeout", "cumulative_cap", "cancelled"]),
1751
+ guidance: z.string().optional(),
1752
+ requestedAt: z.string().datetime(),
1753
+ resolvedAt: z.string().datetime(),
1754
+ latencyMs: z.number().int().min(0)
1755
+ });
1756
+ var ToolCallAudit = z.object({
1757
+ toolName: z.string(),
1758
+ // Verbatim args the LLM produced. May contain PII — see open question O2
1759
+ // in docs/architecture/contracts-design.md §5.
1760
+ args: z.record(z.unknown()),
1761
+ result: z.unknown(),
1762
+ startedAt: z.string().datetime(),
1763
+ durationMs: z.number().int().min(0),
1764
+ ok: z.boolean(),
1765
+ errorMessage: z.string().optional()
1766
+ });
1767
+ var McpInteractionAudit = z.object({
1768
+ // One of the four runtime tools — closed set, no growth allowed.
1769
+ tool: z.enum(["call_say", "call_send_guidance", "call_inject_context", "call_instruct"]),
1770
+ // Args verbatim — see open question O2.
1771
+ args: z.record(z.unknown()),
1772
+ ts: z.string().datetime(),
1773
+ // Attribution. The API key that drove the orchestrator session.
1774
+ apiKeyId: z.string()
1775
+ });
1776
+ z.object({
1777
+ callId: z.string().uuid(),
1778
+ status: z.enum(["completed", "failed"]),
1779
+ startedAt: z.string().datetime(),
1780
+ endedAt: z.string().datetime(),
1781
+ durationMs: z.number().int().min(0),
1782
+ transcript: z.array(TranscriptTurn),
1783
+ // Whatever the agent's process-schema captured. No prescribed shape.
1784
+ structuredOutputs: z.record(z.unknown()).optional(),
1785
+ recordingUrl: z.string().url().optional(),
1786
+ cost: z.object({
1787
+ totalUsd: z.string(),
1788
+ breakdown: z.record(z.string()).optional()
1789
+ }),
1790
+ consultations: z.array(ConsultationAudit),
1791
+ toolCalls: z.array(ToolCallAudit),
1792
+ mcpInteractions: z.array(McpInteractionAudit)
1793
+ });
1794
+ var PhoneNumberStatus = z.enum([
1795
+ "pending",
1796
+ "active",
1797
+ "releasing",
1798
+ "released",
1799
+ "failed"
1800
+ ]);
1801
+ var PhoneNumberProvider = z.enum(["twilio", "telnyx", "plivo"]);
1802
+ var PhoneNumberCapabilities = z.object({
1803
+ voice: z.boolean().optional(),
1804
+ sms: z.boolean().optional(),
1805
+ mms: z.boolean().optional(),
1806
+ fax: z.boolean().optional()
1807
+ });
1808
+ var PhoneNumberRecord = z.object({
1809
+ id: z.string().uuid(),
1810
+ projectId: z.string(),
1811
+ e164: z.string().regex(/^\+[1-9]\d{1,14}$/),
1812
+ provider: PhoneNumberProvider,
1813
+ // Carrier-side identifiers — sensitive, stripped from PublicPhoneNumber.
1814
+ providerSid: z.string(),
1815
+ providerTrunkSid: z.string().nullable(),
1816
+ voiceRouteId: z.string().nullable(),
1817
+ voiceRouteTrunkId: z.string().nullable(),
1818
+ agentId: z.string().uuid().nullable(),
1819
+ // Environments pin (draft/live). null = no pin → live (today's behaviour).
1820
+ agentConfigEnv: z.enum(["draft", "live"]).nullable(),
1821
+ // "Agent answers texts": the bound agent replies to inbound SMS (burn-down G-1). Off by default.
1822
+ smsAgentReplies: z.boolean(),
1823
+ status: PhoneNumberStatus,
1824
+ friendlyName: z.string().nullable(),
1825
+ region: z.string().nullable(),
1826
+ capabilities: PhoneNumberCapabilities,
1827
+ providerMetadata: z.record(z.unknown()),
1828
+ purchaseIdempotencyKey: z.string().nullable().optional(),
1829
+ purchasedAt: z.string().datetime().nullable(),
1830
+ releasedAt: z.string().datetime().nullable(),
1831
+ createdByUserId: z.string().nullable(),
1832
+ createdAt: z.string().datetime(),
1833
+ updatedAt: z.string().datetime()
1834
+ });
1835
+ PhoneNumberRecord.omit({
1836
+ providerSid: true,
1837
+ providerTrunkSid: true,
1838
+ providerMetadata: true,
1839
+ purchaseIdempotencyKey: true
1840
+ });
1841
+ z.object({
1842
+ phoneNumberId: z.string().uuid(),
1843
+ // null detaches.
1844
+ agentId: z.string().uuid().nullable()
1845
+ });
1846
+ z.object({ enabled: z.boolean() });
1847
+ z.object({
1848
+ consultation: z.object({
1849
+ // Ceiling for ConsultationPolicy.maxTimeoutMs. Project default 90_000.
1850
+ perCallMaxTimeoutMs: z.number().int().min(1e3).default(9e4),
1851
+ // Ceiling for ConsultationPolicy.maxCumulativeHoldMs. Default 300_000.
1852
+ perCallMaxCumulativeMs: z.number().int().min(1e3).default(3e5)
1853
+ })
1854
+ });
1855
+ var FlowIssueSeverity = z.enum(["error", "warning"]);
1856
+ var FlowIssue = z.object({
1857
+ code: z.string().max(64),
1858
+ severity: FlowIssueSeverity,
1859
+ message: z.string().max(500),
1860
+ nodeId: z.string().max(128).optional(),
1861
+ edgeId: z.string().max(128).optional()
1862
+ });
1863
+ z.object({
1864
+ valid: z.boolean(),
1865
+ issues: z.array(FlowIssue).max(256)
1866
+ });
1867
+ var ComplianceRule = z.enum(["dnc", "consent", "quiet_hours", "frequency_cap"]);
1868
+ var ComplianceDecision = z.enum(["allowed", "blocked"]);
1869
+ var ComplianceConfig = z.object({
1870
+ // Local calling window for the CALLED party — TCPA default 8am–9pm.
1871
+ quietHoursStartHour: z.number().int().min(0).max(23).default(8),
1872
+ quietHoursEndHour: z.number().int().min(1).max(24).default(21),
1873
+ enforceDnc: z.boolean().default(true),
1874
+ enforceQuietHours: z.boolean().default(true),
1875
+ // Max dial attempts to one number per rolling 24h. 0 = unlimited.
1876
+ maxAttemptsPerDay: z.number().int().min(0).default(0),
1877
+ // Require a recorded consent flag before dialing.
1878
+ requireConsent: z.boolean().default(false),
1879
+ // Fallback IANA tz when the number's region can't be resolved.
1880
+ fallbackTimeZone: z.string().min(1).default("America/New_York")
1881
+ });
1882
+ z.object({
1883
+ phoneE164: z.string().regex(/^\+[1-9]\d{1,14}$/, "must be E.164"),
1884
+ // ISO instant the check is evaluated at (defaults to now server-side).
1885
+ now: z.string().datetime().optional(),
1886
+ // Override the resolved timezone (else derived from the number's area code).
1887
+ timeZone: z.string().optional(),
1888
+ // Recent attempt count in the last 24h (for the frequency cap), if known.
1889
+ recentAttempts: z.number().int().min(0).optional(),
1890
+ // Whether consent is on file (for requireConsent).
1891
+ hasConsent: z.boolean().optional()
1892
+ });
1893
+ z.object({
1894
+ decision: ComplianceDecision,
1895
+ // The first rule that blocked, if any.
1896
+ rule: ComplianceRule.nullable(),
1897
+ reason: z.string().nullable()
1898
+ });
1899
+ z.object({
1900
+ id: z.string().uuid(),
1901
+ projectId: z.string().min(1),
1902
+ phoneE164: z.string(),
1903
+ reason: z.string().nullable(),
1904
+ source: z.string().nullable(),
1905
+ createdAt: z.string().datetime()
1906
+ });
1907
+ z.object({
1908
+ id: z.string().uuid(),
1909
+ projectId: z.string().min(1),
1910
+ phoneE164: z.string(),
1911
+ decision: ComplianceDecision,
1912
+ rule: ComplianceRule.nullable(),
1913
+ reason: z.string().nullable(),
1914
+ checkedAt: z.string().datetime()
1915
+ });
1916
+ z.object({
1917
+ phoneE164: z.string().regex(/^\+[1-9]\d{1,14}$/, "must be E.164"),
1918
+ reason: z.string().max(500).optional(),
1919
+ source: z.string().max(120).optional()
1920
+ });
1921
+ ComplianceConfig.partial();
1922
+ z.object({
1923
+ // When false, the call-event sink skips egress entirely — no recording is made.
1924
+ recordingEnabled: z.boolean().default(true),
1925
+ // Optional consent preamble the agent speaks first on every call. null/empty =
1926
+ // no announcement. Capped so it stays a short notice, not a monologue.
1927
+ consentAnnouncement: z.string().max(1e3).nullable().default(null)
1928
+ });
1929
+ z.object({
1930
+ recordingEnabled: z.boolean().optional(),
1931
+ consentAnnouncement: z.string().max(1e3).nullable().optional()
1932
+ });
1933
+ var OutboundAmdMode = z.enum(["off", "detect", "detectMessageEnd"]);
1934
+ z.object({
1935
+ // Callee in E.164 (e.g. +14155551234). call-router re-validates strictly.
1936
+ to: z.string().regex(/^\+[1-9]\d{6,14}$/, "must be E.164, e.g. +14155551234"),
1937
+ // Caller-id phone number (must belong to the calling project).
1938
+ phoneNumberId: z.string().min(1),
1939
+ // Optional agent override; falls back to the number's bound agent.
1940
+ agentId: z.string().min(1).optional(),
1941
+ // Template/context variables forwarded to the agent as dispatch metadata.
1942
+ variables: z.record(z.string(), z.string()).optional(),
1943
+ // Per-call AMD override; takes precedence over the agent's stored outbound.amd.
1944
+ amd: OutboundAmdMode.optional()
1945
+ });
1946
+ var ConnectionAuthType = z.enum(["oauth2", "api_key", "bearer", "basic", "none"]);
1947
+ var ConnectionStatus = z.enum(["active", "revoked", "error"]);
1948
+ z.object({
1949
+ v: z.number().int(),
1950
+ // Key id (short fingerprint) of the key that encrypted this envelope — lets the
1951
+ // cipher pick the right key from its ring so CREDENTIAL_ENCRYPTION_KEY can rotate
1952
+ // in-band (Tier-1 item 4). Optional/absent on legacy v1 envelopes, which decrypt
1953
+ // with the designated legacy key.
1954
+ kid: z.string().optional(),
1955
+ iv: z.string(),
1956
+ ct: z.string(),
1957
+ tag: z.string()
1958
+ });
1959
+ z.object({
1960
+ id: z.string().uuid(),
1961
+ projectId: z.string().min(1),
1962
+ name: z.string().min(1).max(128),
1963
+ // Open string — customers name providers freely (e.g. 'google_calendar',
1964
+ // 'salesforce', 'n8n', 'http').
1965
+ provider: z.string().min(1).max(120),
1966
+ authType: ConnectionAuthType,
1967
+ status: ConnectionStatus,
1968
+ // Non-secret metadata (scopes, account label, base URL, …).
1969
+ metadata: z.record(z.unknown()),
1970
+ expiresAt: z.string().datetime().nullable(),
1971
+ createdAt: z.string().datetime(),
1972
+ updatedAt: z.string().datetime()
1973
+ });
1974
+ z.object({
1975
+ name: z.string().trim().min(1).max(128),
1976
+ provider: z.string().trim().min(1).max(120),
1977
+ authType: ConnectionAuthType,
1978
+ // The secret material to encrypt at rest. Never stored or returned in plaintext.
1979
+ secret: z.string().min(1).max(8192),
1980
+ metadata: z.record(z.unknown()).optional(),
1981
+ expiresAt: z.string().datetime().optional()
1982
+ });
1983
+ z.object({
1984
+ name: z.string().trim().min(1).max(128).optional(),
1985
+ // New secret material — re-encrypted at rest; the old envelope is replaced.
1986
+ secret: z.string().min(1).max(8192).optional(),
1987
+ // Merged over the existing metadata (top-level keys win).
1988
+ metadata: z.record(z.unknown()).optional()
1989
+ }).refine((v) => v.name !== void 0 || v.secret !== void 0 || v.metadata !== void 0, {
1990
+ message: "nothing to update"
1991
+ });
1992
+ byokProviders().map((p) => ({
1993
+ id: p.id,
1994
+ label: p.label,
1995
+ keyPlaceholder: p.keyPlaceholder ?? "",
1996
+ supportsBaseUrl: p.supportsBaseUrl
1997
+ }));
1998
+ var E164 = z.string().regex(/^\+[1-9]\d{6,14}$/, "must be E.164 (+15551234567)");
1999
+ var RoomPrefix = z.string().regex(/^[a-z0-9][a-z0-9-]{0,30}[a-z0-9-]?$/, "lowercase letters, digits, dashes; max 32");
2000
+ var RecordingPrefix = z.string().min(1).max(128).refine((v) => !v.startsWith("/") && !v.includes(".."), "relative key prefix only");
2001
+ var Cidr = z.string().regex(/^\d{1,3}(\.\d{1,3}){3}\/\d{1,2}$/, "must be IPv4 CIDR (a.b.c.d/nn)");
2002
+ var TelephonySettings = z.object({
2003
+ outboundTrunkId: z.string().min(1).max(128).nullable().default(null),
2004
+ defaultFromE164: E164.nullable().default(null),
2005
+ roomPrefix: RoomPrefix.nullable().default(null),
2006
+ sipAllowedCidrs: z.array(Cidr).max(32).nullable().default(null),
2007
+ recordingPrefix: RecordingPrefix.nullable().default(null),
2008
+ strictByok: z.boolean().default(false)
2009
+ });
2010
+ z.object({
2011
+ outboundTrunkId: z.string().min(1).max(128).nullable().optional(),
2012
+ defaultFromE164: E164.nullable().optional(),
2013
+ roomPrefix: RoomPrefix.nullable().optional(),
2014
+ sipAllowedCidrs: z.array(Cidr).max(32).nullable().optional(),
2015
+ recordingPrefix: RecordingPrefix.nullable().optional(),
2016
+ strictByok: z.boolean().optional()
2017
+ }).refine((v) => Object.keys(v).length > 0, { message: "nothing to update" });
2018
+ var ProfileSource = z.enum(["org", "platform", "none"]);
2019
+ var EffectiveCallProfile = z.object({
2020
+ projectId: z.string(),
2021
+ outboundTrunkId: z.string().nullable(),
2022
+ defaultFromE164: z.string().nullable(),
2023
+ roomPrefix: z.string(),
2024
+ sipAllowedCidrs: z.array(z.string()),
2025
+ recordingPrefix: z.string(),
2026
+ strictByok: z.boolean(),
2027
+ sources: z.object({
2028
+ outboundTrunkId: ProfileSource,
2029
+ defaultFromE164: ProfileSource,
2030
+ roomPrefix: ProfileSource,
2031
+ sipAllowedCidrs: ProfileSource,
2032
+ recordingPrefix: ProfileSource
2033
+ })
2034
+ });
2035
+ z.object({
2036
+ settings: TelephonySettings,
2037
+ profile: EffectiveCallProfile
2038
+ });
2039
+ var SHORTENER_HOSTS = [
2040
+ "bit.ly",
2041
+ "tinyurl.com",
2042
+ "t.co",
2043
+ "goo.gl",
2044
+ "ow.ly",
2045
+ "is.gd",
2046
+ "buff.ly",
2047
+ "rebrand.ly",
2048
+ "cutt.ly",
2049
+ "shorturl.at",
2050
+ "tiny.cc"
2051
+ ];
2052
+ var PLACEHOLDER_HOSTS = ["acme.com", "example.com", "example.org", "test.com", "localhost"];
2053
+ var PublicUrl = z.string().url().refine((v) => v.startsWith("https://"), "must be https").refine((v) => {
2054
+ try {
2055
+ const host = new URL(v).hostname.toLowerCase().replace(/^www\./, "");
2056
+ return !SHORTENER_HOSTS.includes(host);
2057
+ } catch {
2058
+ return false;
2059
+ }
2060
+ }, "public URL shorteners are rejected by carriers \u2014 use your own domain").refine((v) => {
2061
+ try {
2062
+ const host = new URL(v).hostname.toLowerCase().replace(/^www\./, "");
2063
+ return !PLACEHOLDER_HOSTS.some((p) => host === p || host.endsWith(`.${p}`));
2064
+ } catch {
2065
+ return false;
2066
+ }
2067
+ }, "placeholder domain \u2014 reviewers will follow this link and reject the campaign");
2068
+ var A2pBusinessType = z.enum([
2069
+ "Sole Proprietorship",
2070
+ "Partnership",
2071
+ "Corporation",
2072
+ "Co-operative",
2073
+ "Limited Liability Corporation",
2074
+ "Non-profit Corporation"
2075
+ ]);
2076
+ var A2pBusinessInfo = z.object({
2077
+ legalName: z.string().trim().min(2).max(200),
2078
+ /** EIN (US) or equivalent registration number. */
2079
+ registrationNumber: z.string().trim().min(4).max(50),
2080
+ businessType: A2pBusinessType,
2081
+ /** Publicly reachable production site — not staging, not a 404. */
2082
+ website: PublicUrl,
2083
+ industry: z.string().trim().min(2).max(60),
2084
+ address: z.object({
2085
+ street: z.string().trim().min(2).max(200),
2086
+ city: z.string().trim().min(1).max(100),
2087
+ region: z.string().trim().min(1).max(100),
2088
+ postalCode: z.string().trim().min(2).max(20),
2089
+ isoCountry: z.string().trim().length(2)
2090
+ }),
2091
+ authorizedRep: z.object({
2092
+ firstName: z.string().trim().min(1).max(100),
2093
+ lastName: z.string().trim().min(1).max(100),
2094
+ email: z.string().trim().email(),
2095
+ phone: z.string().trim().regex(/^\+[1-9]\d{6,14}$/, "must be E.164"),
2096
+ jobTitle: z.string().trim().min(2).max(100)
2097
+ })
2098
+ });
2099
+ var A2pOptInType = z.enum(["WEB_FORM", "PAPER_FORM", "VERBAL", "VIA_TEXT", "MOBILE_QR_CODE"]);
2100
+ var A2pUseCase = z.enum([
2101
+ "MIXED",
2102
+ "CUSTOMER_CARE",
2103
+ "MARKETING",
2104
+ "ACCOUNT_NOTIFICATION",
2105
+ "2FA",
2106
+ "DELIVERY_NOTIFICATION",
2107
+ "HIGHER_EDUCATION",
2108
+ "POLLING_VOTING",
2109
+ "PUBLIC_SERVICE_ANNOUNCEMENT",
2110
+ "LOW_VOLUME"
2111
+ ]);
2112
+ var A2pMessagingProfile = z.object({
2113
+ useCase: A2pUseCase,
2114
+ /** Specific, not generic. "We send texts" gets rejected; describe the actual
2115
+ * messages and when they are sent. */
2116
+ description: z.string().trim().min(40).max(4096),
2117
+ /**
2118
+ * The single most-rejected field. Must describe HOW people opt in, state the
2119
+ * message frequency, include the "message and data rates may apply"
2120
+ * disclosure, and link to publicly reachable evidence. Twilio's API bounds it
2121
+ * to 40–2049 characters.
2122
+ */
2123
+ messageFlow: z.string().trim().min(40).max(2049),
2124
+ optInType: A2pOptInType,
2125
+ /** Publicly accessible screenshots/pages showing the opt-in. Reviewers open
2126
+ * these; anything behind a login fails. */
2127
+ optInEvidenceUrls: z.array(PublicUrl).min(1).max(5),
2128
+ /** Real messages the customer will send. Must reflect the declared use case
2129
+ * and carry opt-out language. */
2130
+ messageSamples: z.array(z.string().trim().min(10).max(1024)).min(2).max(5),
2131
+ privacyPolicyUrl: PublicUrl,
2132
+ termsAndConditionsUrl: PublicUrl,
2133
+ hasEmbeddedLinks: z.boolean().default(false),
2134
+ hasEmbeddedPhone: z.boolean().default(false)
2135
+ });
2136
+ z.object({
2137
+ business: A2pBusinessInfo,
2138
+ messaging: A2pMessagingProfile,
2139
+ /** Register against Twilio's mock endpoints — exercises the full pipeline
2140
+ * with no fees and no real carrier submission. Used in CI and staging. */
2141
+ mock: z.boolean().default(false)
2142
+ }).superRefine((v, ctx) => {
2143
+ const flow = v.messaging.messageFlow.toLowerCase();
2144
+ if (!/(msg|message)\s*(&|and)\s*data rates/.test(flow)) {
2145
+ ctx.addIssue({
2146
+ code: z.ZodIssueCode.custom,
2147
+ path: ["messaging", "messageFlow"],
2148
+ message: 'must include a "Message and data rates may apply" disclosure \u2014 carriers reject without it'
2149
+ });
2150
+ }
2151
+ if (!/\d/.test(flow) || !/(msg|message|text)/.test(flow)) {
2152
+ ctx.addIssue({
2153
+ code: z.ZodIssueCode.custom,
2154
+ path: ["messaging", "messageFlow"],
2155
+ message: 'must state message frequency, e.g. "Up to 4 msgs/month"'
2156
+ });
2157
+ }
2158
+ const hasOptOut = v.messaging.messageSamples.some((s) => /stop/i.test(s));
2159
+ if (!hasOptOut) {
2160
+ ctx.addIssue({
2161
+ code: z.ZodIssueCode.custom,
2162
+ path: ["messaging", "messageSamples"],
2163
+ message: 'at least one sample must include opt-out language (e.g. "Reply STOP to opt out")'
2164
+ });
2165
+ }
2166
+ });
2167
+ var A2pState = z.enum([
2168
+ "none",
2169
+ "profile_pending",
2170
+ "profile_approved",
2171
+ "profile_failed",
2172
+ "brand_pending",
2173
+ "brand_approved",
2174
+ "brand_failed",
2175
+ "campaign_pending",
2176
+ "messaging_ready",
2177
+ "campaign_failed"
2178
+ ]);
2179
+ z.object({
2180
+ state: A2pState,
2181
+ customerProfileSid: z.string().nullable(),
2182
+ trustProductSid: z.string().nullable(),
2183
+ brandSid: z.string().nullable(),
2184
+ messagingServiceSid: z.string().nullable(),
2185
+ campaignSid: z.string().nullable(),
2186
+ mock: z.boolean(),
2187
+ /** Carrier/Twilio rejection details, surfaced verbatim so the customer can
2188
+ * fix the specific field rather than guess. */
2189
+ failures: z.array(z.object({ code: z.number().nullable(), field: z.string().nullable(), message: z.string() })).default([]),
2190
+ /** Plain-language next step for the dashboard. */
2191
+ nextAction: z.string().nullable(),
2192
+ updatedAt: z.string().nullable()
2193
+ });
2194
+ var ConnectorMode = z.enum(["tunnel", "direct"]);
2195
+ var ConnectorStatus = z.enum(["online", "degraded", "offline"]);
2196
+ var ConnectorNormalize = z.enum(["auto", "on", "off"]);
2197
+ z.object({
2198
+ id: z.string().uuid(),
2199
+ projectId: z.string().min(1),
2200
+ name: z.string().min(1).max(128),
2201
+ mode: ConnectorMode,
2202
+ // Tunnel mode: a short non-secret preview of the daemon token. Null for direct.
2203
+ tokenPreview: z.string().nullable(),
2204
+ // Direct mode: the connections-vault row holding the URL + key. Null for tunnel.
2205
+ connectionId: z.string().uuid().nullable(),
2206
+ // Upstream model id / alias. Null → the brain's own default.
2207
+ defaultModel: z.string().nullable(),
2208
+ normalize: ConnectorNormalize,
2209
+ enabled: z.boolean(),
2210
+ status: ConnectorStatus,
2211
+ lastSeenAt: z.string().datetime().nullable(),
2212
+ metadata: z.record(z.unknown()),
2213
+ createdAt: z.string().datetime(),
2214
+ updatedAt: z.string().datetime()
2215
+ });
2216
+ z.object({
2217
+ name: z.string().trim().min(1).max(128),
2218
+ mode: ConnectorMode,
2219
+ // Required when mode === 'direct': the vault connection holding URL + key.
2220
+ connectionId: z.string().uuid().optional(),
2221
+ defaultModel: z.string().trim().min(1).max(200).optional(),
2222
+ normalize: ConnectorNormalize.optional(),
2223
+ metadata: z.record(z.unknown()).optional()
2224
+ });
2225
+ z.object({
2226
+ name: z.string().trim().min(1).max(128).optional(),
2227
+ connectionId: z.string().uuid().nullable().optional(),
2228
+ defaultModel: z.string().trim().min(1).max(200).nullable().optional(),
2229
+ normalize: ConnectorNormalize.optional(),
2230
+ enabled: z.boolean().optional()
2231
+ });
2232
+ var CONNECTOR_REQUEST_CHANNEL = (connectorId) => `brain-req:${connectorId}`;
2233
+ var CONNECTOR_REPLY_CHANNEL = (streamId) => `brain-reply:${streamId}`;
2234
+ var BrainWireMessage = z.object({
2235
+ role: z.enum(["system", "user", "assistant"]),
2236
+ content: z.string()
2237
+ });
2238
+ var HelloFrame = z.object({
2239
+ kind: z.literal("hello"),
2240
+ token: z.string().min(1),
2241
+ replicaId: z.string().min(1),
2242
+ version: z.string().min(1),
2243
+ caps: z.object({ streaming: z.boolean(), tools: z.boolean(), litellm: z.boolean() })
2244
+ });
2245
+ var PongFrame = z.object({ kind: z.literal("pong"), ts: z.number().int() });
2246
+ var RegisteredFrame = z.object({
2247
+ kind: z.literal("registered"),
2248
+ connectorId: z.string().uuid(),
2249
+ projectId: z.string().min(1)
2250
+ });
2251
+ var PingFrame = z.object({ kind: z.literal("ping"), ts: z.number().int() });
2252
+ var ShutdownFrame = z.object({ kind: z.literal("shutdown"), reason: z.string().optional() });
2253
+ var BrainRequestFrame = z.object({
2254
+ kind: z.literal("brain.request"),
2255
+ streamId: z.string().min(1),
2256
+ model: z.string().optional(),
2257
+ temperature: z.number().optional(),
2258
+ messages: z.array(BrainWireMessage)
2259
+ });
2260
+ var BrainCancelFrame = z.object({
2261
+ kind: z.literal("brain.cancel"),
2262
+ streamId: z.string().min(1)
2263
+ });
2264
+ var BrainDeltaFrame = z.object({
2265
+ kind: z.literal("brain.delta"),
2266
+ streamId: z.string().min(1),
2267
+ content: z.string()
2268
+ });
2269
+ var BrainDoneFrame = z.object({
2270
+ kind: z.literal("brain.done"),
2271
+ streamId: z.string().min(1),
2272
+ finishReason: z.string()
2273
+ });
2274
+ var BrainErrorFrame = z.object({
2275
+ kind: z.literal("brain.error"),
2276
+ streamId: z.string().min(1),
2277
+ code: z.enum([
2278
+ "upstream_timeout",
2279
+ "upstream_error",
2280
+ "bad_response",
2281
+ "normalize_failed",
2282
+ "unreachable"
2283
+ ]),
2284
+ message: z.string()
2285
+ });
2286
+ var ConnectorUpFrame = z.discriminatedUnion("kind", [
2287
+ HelloFrame,
2288
+ PongFrame,
2289
+ BrainDeltaFrame,
2290
+ BrainDoneFrame,
2291
+ BrainErrorFrame
2292
+ ]);
2293
+ z.discriminatedUnion("kind", [
2294
+ RegisteredFrame,
2295
+ PingFrame,
2296
+ ShutdownFrame,
2297
+ BrainRequestFrame,
2298
+ BrainCancelFrame
2299
+ ]);
2300
+ var EnvironmentSpec = z.enum(["draft", "live"]);
2301
+ z.object({
2302
+ agentName: z.string().min(1),
2303
+ draftPercent: z.number().int().min(0).max(100),
2304
+ livePercent: z.number().int().min(0).max(100)
2305
+ });
2306
+ z.object({
2307
+ draftPercent: z.number().int().min(0).max(100),
2308
+ livePercent: z.number().int().min(0).max(100).optional()
2309
+ }).refine(
2310
+ (v) => v.livePercent === void 0 || v.draftPercent + v.livePercent === 100,
2311
+ { message: "draftPercent + livePercent must equal 100" }
2312
+ );
2313
+ z.object({
2314
+ env: EnvironmentSpec.nullable()
2315
+ });
2316
+ var VariableType = z.enum(["string", "number", "boolean", "json"]);
2317
+ var VariableScope = z.enum(["project", "user"]);
2318
+ var VariableName = z.string().min(1).max(64).regex(
2319
+ /^[A-Za-z_][A-Za-z0-9_]*$/,
2320
+ "must be a valid identifier (letters, digits, underscore; no leading digit)"
2321
+ );
2322
+ z.object({
2323
+ id: z.string().uuid(),
2324
+ projectId: z.string(),
2325
+ name: VariableName,
2326
+ type: VariableType,
2327
+ // Stored as text; interpreted per `type`. null = no default (renders '').
2328
+ defaultValue: z.string().nullable(),
2329
+ description: z.string().max(280).nullable(),
2330
+ scope: VariableScope,
2331
+ createdAt: z.string().datetime(),
2332
+ updatedAt: z.string().datetime()
2333
+ });
2334
+ z.object({
2335
+ name: VariableName,
2336
+ type: VariableType.default("string"),
2337
+ defaultValue: z.string().nullable().optional(),
2338
+ description: z.string().max(280).nullable().optional(),
2339
+ scope: VariableScope.default("project")
2340
+ });
2341
+ z.object({
2342
+ type: VariableType.optional(),
2343
+ defaultValue: z.string().nullable().optional(),
2344
+ description: z.string().max(280).nullable().optional(),
2345
+ scope: VariableScope.optional()
2346
+ }).refine((b) => Object.keys(b).length > 0, { message: "no fields to update" });
2347
+ var PlaybookExitCondition = z.object({
2348
+ name: z.string().min(1).max(64),
2349
+ description: z.string().max(2e3),
2350
+ requiredVariables: z.array(z.string().min(1).max(64)).max(32)
2351
+ });
2352
+ z.object({
2353
+ id: z.string(),
2354
+ projectId: z.string(),
2355
+ name: z.string().min(1).max(120),
2356
+ // Routing signal — "what it does AND when to use it" (read by the agent router).
2357
+ llmDescription: z.string().max(2e3),
2358
+ instructions: z.string().max(2e4),
2359
+ // Model overrides (null = inherit the agent's). Exactly 3 knobs, no top-p/penalties.
2360
+ model: z.string().max(120).nullable(),
2361
+ temperature: z.number().min(0).max(2).nullable(),
2362
+ maxTokens: z.number().int().positive().max(32e3).nullable(),
2363
+ // Registry HTTP tools (by name) this playbook may call. With tools the
2364
+ // playbook runs as an agent loop (LLM-with-tools + structured exit).
2365
+ tools: z.array(z.string().min(1).max(128)).max(64).default([]),
2366
+ exitConditions: z.array(PlaybookExitCondition).max(24),
2367
+ createdAt: z.string(),
2368
+ updatedAt: z.string()
2369
+ });
2370
+ z.object({
2371
+ name: z.string().min(1).max(120),
2372
+ llmDescription: z.string().max(2e3).default(""),
2373
+ instructions: z.string().max(2e4).default(""),
2374
+ model: z.string().max(120).nullable().optional(),
2375
+ temperature: z.number().min(0).max(2).nullable().optional(),
2376
+ maxTokens: z.number().int().positive().max(32e3).nullable().optional(),
2377
+ tools: z.array(z.string().min(1).max(128)).max(64).default([]),
2378
+ exitConditions: z.array(PlaybookExitCondition).max(24).default([])
2379
+ });
2380
+ z.object({
2381
+ name: z.string().min(1).max(120),
2382
+ llmDescription: z.string().max(2e3),
2383
+ instructions: z.string().max(2e4),
2384
+ model: z.string().max(120).nullable(),
2385
+ temperature: z.number().min(0).max(2).nullable(),
2386
+ maxTokens: z.number().int().positive().max(32e3).nullable(),
2387
+ tools: z.array(z.string().min(1).max(128)).max(64),
2388
+ exitConditions: z.array(PlaybookExitCondition).max(24)
2389
+ }).partial().refine((b) => Object.keys(b).length > 0, { message: "at least one field required" });
2390
+ var PlaybookSnapshot = z.object({
2391
+ name: z.string().min(1).max(120),
2392
+ llmDescription: z.string().max(2e3),
2393
+ instructions: z.string().max(2e4),
2394
+ model: z.string().max(120).nullable(),
2395
+ temperature: z.number().min(0).max(2).nullable(),
2396
+ maxTokens: z.number().int().positive().max(32e3).nullable(),
2397
+ // Registry tool names frozen with the snapshot (default keeps old rows valid).
2398
+ tools: z.array(z.string().min(1).max(128)).max(64).default([]),
2399
+ exitConditions: z.array(PlaybookExitCondition).max(24)
2400
+ });
2401
+ z.object({
2402
+ id: z.string(),
2403
+ playbookId: z.string(),
2404
+ projectId: z.string(),
2405
+ version: z.number().int().min(1),
2406
+ // Frozen playbook definition at publish time.
2407
+ definition: PlaybookSnapshot,
2408
+ note: z.string().max(1e3).nullable(),
2409
+ publishedByUserId: z.string().nullable(),
2410
+ publishedAt: z.string()
2411
+ });
2412
+ z.object({
2413
+ note: z.string().max(1e3).optional()
2414
+ });
2415
+ var EngineSeamBinding = z.object({
2416
+ key: z.string(),
2417
+ bound: z.boolean()
2418
+ });
2419
+ var EnginePluginDTO = z.object({
2420
+ name: z.string(),
2421
+ provides: z.array(EngineSeamBinding),
2422
+ injects: z.array(z.string())
2423
+ });
2424
+ z.object({
2425
+ plugins: z.array(EnginePluginDTO)
2426
+ });
2427
+
2428
+ // ../contracts/src/channel-connector/capability-state.ts
2429
+ var CONDITION_CLASSES = ["authority", "operational", "observability"];
2430
+ var CONDITION_TYPES = [
2431
+ { id: "CredentialsValid", defaultClass: "authority", summary: "credential lease resolves and is not expired/revoked" },
2432
+ { id: "ProviderAccountGood", defaultClass: "authority", summary: "provider account / sender standing is not suspended or restricted" },
2433
+ { id: "RegistrationApproved", defaultClass: "authority", summary: "required registration / approval is current" },
2434
+ { id: "ResourceOwnershipConfirmed", defaultClass: "authority", summary: "the native object is confirmed to belong to this principal" },
2435
+ { id: "InstallationPresent", defaultClass: "authority", summary: "the container installation exists and is not revoked" },
2436
+ { id: "PermissionsHeld", defaultClass: "authority", summary: "required provider permissions/scopes/intents are held" },
2437
+ { id: "ConsentResolved", defaultClass: "authority", summary: "consent verdict available (ch. 02; not_evaluated until it ships)" },
2438
+ { id: "WindowOpen", defaultClass: "authority", summary: "the reply window for the peer is open" },
2439
+ { id: "TemplateAvailable", defaultClass: "authority", summary: "an approved template exists for the operation" },
2440
+ { id: "PeerReachable", defaultClass: "authority", summary: "the peer can be reached on this channel (e.g. RCS capability, not blocked)" },
2441
+ { id: "WebhookRegistered", defaultClass: "operational", summary: "the provider has our ingress registered" },
2442
+ { id: "InboundVerificationReady", defaultClass: "authority", summary: "the material the declared inbound-auth strategy needs (for the selected authorization method) resolves \u2014 REST auth alone never satisfies it (#306 L4)" },
2443
+ { id: "WebhookHealthy", defaultClass: "operational", summary: "recent verified inbound / delivery health" },
2444
+ { id: "TransportConnected", defaultClass: "operational", summary: "a transport lease is live (gateway / long-poll / pub-sub)" },
2445
+ { id: "ReconcileFresh", defaultClass: "operational", summary: "the last reconcile is within its window" },
2446
+ { id: "ProviderLatency", defaultClass: "observability", summary: "provider latency / error-rate telemetry" }
2447
+ ];
2448
+ new Set(CONDITION_TYPES.map((c) => c.id));
2449
+ var SURFACES = ["direct", "group", "channel", "thread", "broadcast"];
2450
+ var TRANSPORT_KINDS = ["webhook", "long_poll", "gateway", "pubsub", "http_interactions"];
2451
+ var AGENT_INITIATION = ["always", "installed_only", "opt_in_required", "template_only", "never"];
2452
+ var TEMPLATE_REQUIREMENT = ["never", "outside_window", "always"];
2453
+ var WINDOW_OUTSIDE = ["tagged_only", "template_only", "blocked"];
2454
+ var PEER_ID_SCOPES = ["global", "app_scoped", "workspace_scoped", "installation_scoped"];
2455
+ var RECEIPT_KINDS = ["sent", "delivered", "read", "failed"];
2456
+
2457
+ // ../contracts/src/channel-connector/channels.ts
2458
+ var TEXT_ONLY = ["text"];
2459
+ var RICH_MESSAGING = ["text", "image", "audio", "video", "document", "location", "contact", "sticker", "reaction", "interactive", "template"];
2460
+ function baseline(channel, partial) {
2461
+ return {
2462
+ channel,
2463
+ connector: null,
2464
+ version: "1.0",
2465
+ requiredPermissions: [],
2466
+ registration: [],
2467
+ readinessGates: [
2468
+ { condition: "CredentialsValid", class: "authority", blocks: "*", ttlSec: 300 },
2469
+ { condition: "ProviderAccountGood", class: "authority", blocks: "*", ttlSec: 3600 },
2470
+ { condition: "WebhookHealthy", class: "operational", blocks: ["receive.message", "call.receive"], ttlSec: 900, evidence: "runtime" }
2471
+ ],
2472
+ ...partial
2473
+ };
2474
+ }
2475
+ var MESSAGE_CORE = ["receive.message", "send.text", "send.media", "initiate.conversation", "reply.in_window"];
2476
+ var CONTAINER_GATES = [
2477
+ { condition: "CredentialsValid", class: "authority", blocks: "*", ttlSec: 300 },
2478
+ { condition: "InstallationPresent", class: "authority", blocks: "*", ttlSec: 3600 },
2479
+ { condition: "PermissionsHeld", class: "authority", blocks: ["send.text", "send.media", "channel.post", "thread.reply", "group.post"], ttlSec: 3600 },
2480
+ { condition: "WebhookHealthy", class: "operational", blocks: ["receive.message"], ttlSec: 900, evidence: "runtime" }
2481
+ ];
2482
+ var CHANNELS = [
2483
+ {
2484
+ id: "sms",
2485
+ displayName: "SMS",
2486
+ addressSpace: "phone",
2487
+ surfaces: ["direct"],
2488
+ bindingCapabilities: ["sms"],
2489
+ baseline: baseline("sms", {
2490
+ operations: [...MESSAGE_CORE, "read.receipt"],
2491
+ direction: { inbound: true, outbound: true },
2492
+ initiation: { agentInitiated: "always", userInitiated: true, firstContactRule: "agent-initiated with registration (10DLC / TFV per country); STOP/HELP keywords mandatory" },
2493
+ windows: [],
2494
+ templates: { required: "never", reviewed: false },
2495
+ surfaces: ["direct"],
2496
+ messageTypes: TEXT_ONLY,
2497
+ // GSM-7 160/153, UCS-2 70/67 per segment (doc 01 §3); no body cap is a channel fact.
2498
+ limits: { segments: true, note: "GSM-7 160/153 \xB7 UCS-2 70/67 per segment" },
2499
+ receipts: ["sent", "delivered", "failed"],
2500
+ edits: { edit: false, delete: false, reactions: false, commands: false },
2501
+ transport: ["webhook"],
2502
+ identity: { peerIdKind: "phone", peerIdScope: "global" },
2503
+ registration: [{ kind: "campaign_vetting", blocks: ["send.text", "send.media", "initiate.conversation"] }]
2504
+ })
2505
+ },
2506
+ {
2507
+ id: "mms",
2508
+ displayName: "MMS",
2509
+ addressSpace: "phone",
2510
+ surfaces: ["direct", "group"],
2511
+ bindingCapabilities: ["mms"],
2512
+ baseline: baseline("mms", {
2513
+ operations: [...MESSAGE_CORE, "group.post"],
2514
+ direction: { inbound: true, outbound: true },
2515
+ initiation: { agentInitiated: "always", userInitiated: true, firstContactRule: "as SMS; carrier MMS size caps apply" },
2516
+ windows: [],
2517
+ templates: { required: "never", reviewed: false },
2518
+ surfaces: ["direct", "group"],
2519
+ messageTypes: ["text", "image", "audio", "video", "document"],
2520
+ limits: { mediaBytes: 1e6, note: "carrier MMS \u22480.6\u20131 MB (doc 01 \xA73)" },
2521
+ receipts: ["sent", "delivered", "failed"],
2522
+ edits: { edit: false, delete: false, reactions: false, commands: false },
2523
+ transport: ["webhook"],
2524
+ identity: { peerIdKind: "phone", peerIdScope: "global" },
2525
+ registration: [{ kind: "campaign_vetting", blocks: ["send.text", "send.media", "initiate.conversation"] }]
2526
+ })
2527
+ },
2528
+ {
2529
+ id: "voice",
2530
+ displayName: "Voice (PSTN)",
2531
+ addressSpace: "phone",
2532
+ surfaces: ["direct"],
2533
+ bindingCapabilities: ["voice"],
2534
+ baseline: baseline("voice", {
2535
+ operations: ["call.receive", "call.place"],
2536
+ direction: { inbound: true, outbound: true },
2537
+ initiation: { agentInitiated: "always", userInitiated: true, firstContactRule: "agent-initiated; STIR/SHAKEN attestation; emergency addressing; trunk config" },
2538
+ windows: [],
2539
+ templates: { required: "never", reviewed: false },
2540
+ surfaces: ["direct"],
2541
+ messageTypes: [],
2542
+ limits: {},
2543
+ receipts: [],
2544
+ edits: { edit: false, delete: false, reactions: false, commands: false },
2545
+ transport: ["webhook"],
2546
+ identity: { peerIdKind: "phone", peerIdScope: "global" }
2547
+ })
2548
+ },
2549
+ {
2550
+ id: "whatsapp",
2551
+ displayName: "WhatsApp",
2552
+ addressSpace: "whatsapp",
2553
+ surfaces: ["direct", "group"],
2554
+ bindingCapabilities: ["whatsapp.message"],
2555
+ baseline: baseline("whatsapp", {
2556
+ operations: [...MESSAGE_CORE, "send.template", "send.interactive", "send.reaction", "read.receipt", "typing.indicate"],
2557
+ direction: { inbound: true, outbound: true },
2558
+ initiation: { agentInitiated: "template_only", userInitiated: true, firstContactRule: "user-initiated or a reviewed template outside the 24-h window; opt-in mandatory" },
2559
+ windows: [{ kind: "reply", durationSec: 86400, opensOn: "inbound_message", outside: "template_only" }],
2560
+ templates: { required: "outside_window", reviewed: true },
2561
+ surfaces: ["direct", "group"],
2562
+ messageTypes: RICH_MESSAGING,
2563
+ limits: {
2564
+ textChars: 4096,
2565
+ mediaBytes: 1e8,
2566
+ perSecond: 80,
2567
+ formats: { audio: { mime: ["audio/ogg", "audio/mpeg", "audio/mp4", "audio/aac", "audio/amr"], variants: ["voice_note", "file"] } }
2568
+ },
2569
+ receipts: ["sent", "delivered", "read", "failed"],
2570
+ edits: { edit: false, delete: false, reactions: true, commands: false },
2571
+ transport: ["webhook"],
2572
+ identity: { peerIdKind: "whatsapp", peerIdScope: "global" },
2573
+ registration: [{ kind: "business_verification", blocks: ["initiate.conversation", "send.template"] }]
2574
+ })
2575
+ },
2576
+ {
2577
+ id: "instagram",
2578
+ displayName: "Instagram DM",
2579
+ addressSpace: "app",
2580
+ surfaces: ["direct"],
2581
+ bindingCapabilities: ["instagram.message"],
2582
+ baseline: baseline("instagram", {
2583
+ operations: ["receive.message", "send.text", "send.media", "send.reaction", "reply.in_window", "delete.message", "read.receipt"],
2584
+ direction: { inbound: true, outbound: true },
2585
+ initiation: { agentInitiated: "never", userInitiated: true, firstContactRule: "user-initiated only \u2014 the peer identifier exists only after the user messages or clicks" },
2586
+ windows: [{ kind: "reply", durationSec: 86400, opensOn: "inbound_message", outside: "tagged_only" }],
2587
+ templates: { required: "never", reviewed: false },
2588
+ surfaces: ["direct"],
2589
+ messageTypes: ["text", "image", "audio", "video", "document", "sticker", "reaction", "interactive", "template"],
2590
+ limits: { textBytes: 1e3, mediaBytes: 25e6 },
2591
+ receipts: ["read"],
2592
+ edits: { edit: false, delete: true, reactions: true, commands: false },
2593
+ transport: ["webhook"],
2594
+ // doc 07 §3: per person × professional account, stable across apps — the
2595
+ // account scope is embedded in the normalized value, so the id is global.
2596
+ identity: { peerIdKind: "meta_account_user", peerIdScope: "global" },
2597
+ registration: [{ kind: "app_review", blocks: ["send.text", "send.media"] }]
2598
+ })
2599
+ },
2600
+ {
2601
+ id: "messenger",
2602
+ displayName: "Messenger",
2603
+ addressSpace: "app",
2604
+ surfaces: ["direct"],
2605
+ bindingCapabilities: ["messenger.message"],
2606
+ baseline: baseline("messenger", {
2607
+ operations: ["receive.message", "send.text", "send.media", "send.reaction", "reply.in_window", "edit.message", "read.receipt", "typing.indicate"],
2608
+ direction: { inbound: true, outbound: true },
2609
+ initiation: { agentInitiated: "never", userInitiated: true, firstContactRule: "as Instagram plus Get Started / link / ads entry points" },
2610
+ windows: [{ kind: "reply", durationSec: 86400, opensOn: "inbound_message", outside: "tagged_only" }],
2611
+ templates: { required: "never", reviewed: false },
2612
+ surfaces: ["direct"],
2613
+ messageTypes: RICH_MESSAGING,
2614
+ limits: { mediaBytes: 25e6 },
2615
+ receipts: ["delivered", "read"],
2616
+ edits: { edit: true, delete: false, reactions: true, commands: false },
2617
+ transport: ["webhook"],
2618
+ identity: { peerIdKind: "meta_account_user", peerIdScope: "global" },
2619
+ registration: [{ kind: "app_review", blocks: ["send.text", "send.media"] }]
2620
+ })
2621
+ },
2622
+ {
2623
+ id: "telegram",
2624
+ displayName: "Telegram",
2625
+ addressSpace: "app",
2626
+ surfaces: ["direct", "group", "channel", "thread"],
2627
+ bindingCapabilities: ["telegram.message"],
2628
+ baseline: baseline("telegram", {
2629
+ operations: [...MESSAGE_CORE, "send.interactive", "send.reaction", "edit.message", "delete.message", "command.register", "group.post", "channel.post", "thread.reply"],
2630
+ direction: { inbound: true, outbound: true },
2631
+ // Plain bot: the user must start; no reply window. The business-account
2632
+ // 24-h rule is an installation-level narrowing, not the channel ceiling.
2633
+ initiation: { agentInitiated: "never", userInitiated: true, firstContactRule: "the user must start the bot (/start, deep link, group add) before it may message them" },
2634
+ windows: [],
2635
+ templates: { required: "never", reviewed: false },
2636
+ surfaces: ["direct", "group", "channel", "thread"],
2637
+ messageTypes: ["text", "image", "audio", "video", "document", "location", "contact", "sticker", "reaction", "interactive", "poll"],
2638
+ limits: { textChars: 4096, mediaBytes: 5e7, formats: { audio: { mime: ["audio/ogg", "audio/mpeg"], variants: ["voice_note", "file"] } } },
2639
+ receipts: [],
2640
+ edits: { edit: true, delete: true, reactions: true, commands: true },
2641
+ transport: ["webhook", "long_poll"],
2642
+ identity: { peerIdKind: "telegram_user", peerIdScope: "global" }
2643
+ })
2644
+ },
2645
+ {
2646
+ id: "discord",
2647
+ displayName: "Discord",
2648
+ addressSpace: "app",
2649
+ surfaces: ["direct", "group", "channel", "thread"],
2650
+ bindingCapabilities: ["discord.message", "discord.command"],
2651
+ baseline: baseline("discord", {
2652
+ // The channel exposes BOTH modes (doc 04 §1): text via `send.*`, realtime
2653
+ // audio via `call.*`. Mode is per OPERATION — the channel id never implies it.
2654
+ operations: [...MESSAGE_CORE, "send.interactive", "send.reaction", "edit.message", "delete.message", "command.register", "channel.post", "thread.reply", "call.receive", "call.place"],
2655
+ direction: { inbound: true, outbound: true },
2656
+ initiation: { agentInitiated: "installed_only", userInitiated: true, firstContactRule: "guild channels by permission; DMs need a mutual guild or a user install" },
2657
+ windows: [],
2658
+ templates: { required: "never", reviewed: false },
2659
+ surfaces: ["direct", "group", "channel", "thread"],
2660
+ messageTypes: ["text", "image", "audio", "video", "document", "reaction", "interactive", "card", "poll"],
2661
+ limits: { textChars: 2e3, mediaBytes: 10485760 },
2662
+ receipts: [],
2663
+ edits: { edit: true, delete: true, reactions: true, commands: true },
2664
+ transport: ["gateway", "http_interactions"],
2665
+ identity: { peerIdKind: "discord_user", peerIdScope: "global" },
2666
+ readinessGates: [
2667
+ { condition: "CredentialsValid", class: "authority", blocks: "*", ttlSec: 300 },
2668
+ { condition: "InstallationPresent", class: "authority", blocks: "*", ttlSec: 3600 },
2669
+ { condition: "PermissionsHeld", class: "authority", blocks: ["send.text", "send.media", "channel.post", "thread.reply", "call.receive", "call.place"], ttlSec: 3600 },
2670
+ { condition: "TransportConnected", class: "operational", blocks: ["receive.message", "call.receive"], ttlSec: 120 }
2671
+ ]
2672
+ })
2673
+ },
2674
+ {
2675
+ // G-3: the embeddable website chat widget. The widget key IS the identity and its evidence (a `web`-space Binding
2676
+ // at `widget:<keyId>`, no provider connection), so no provider readiness gate applies. The agent only ever answers
2677
+ // inside the visitor's own request — it never starts or pushes a message.
2678
+ id: "web",
2679
+ displayName: "Website chat",
2680
+ addressSpace: "web",
2681
+ surfaces: ["direct"],
2682
+ bindingCapabilities: ["web.message"],
2683
+ baseline: baseline("web", {
2684
+ operations: ["receive.message", "reply.in_window"],
2685
+ direction: { inbound: true, outbound: true },
2686
+ initiation: { agentInitiated: "never", userInitiated: true, firstContactRule: "the visitor opens the widget and writes first" },
2687
+ windows: [],
2688
+ templates: { required: "never", reviewed: false },
2689
+ surfaces: ["direct"],
2690
+ messageTypes: TEXT_ONLY,
2691
+ limits: { textChars: 8e3 },
2692
+ receipts: [],
2693
+ edits: { edit: false, delete: false, reactions: false, commands: false },
2694
+ transport: ["http_interactions"],
2695
+ identity: { peerIdKind: "web_visitor", peerIdScope: "app_scoped" },
2696
+ readinessGates: []
2697
+ })
2698
+ },
2699
+ {
2700
+ id: "slack",
2701
+ displayName: "Slack",
2702
+ addressSpace: "app",
2703
+ surfaces: ["direct", "group", "channel", "thread"],
2704
+ bindingCapabilities: ["slack.message"],
2705
+ baseline: baseline("slack", {
2706
+ operations: [...MESSAGE_CORE, "send.interactive", "send.reaction", "edit.message", "delete.message", "command.register", "channel.post", "thread.reply"],
2707
+ direction: { inbound: true, outbound: true },
2708
+ initiation: { agentInitiated: "installed_only", userInitiated: true, firstContactRule: "any channel the bot is in; DMs" },
2709
+ windows: [],
2710
+ templates: { required: "never", reviewed: false },
2711
+ surfaces: ["direct", "group", "channel", "thread"],
2712
+ messageTypes: ["text", "image", "audio", "video", "document", "reaction", "interactive", "card"],
2713
+ limits: { textChars: 4e3, note: "truncated at 40,000 (doc 01 \xA73)" },
2714
+ receipts: [],
2715
+ edits: { edit: true, delete: true, reactions: true, commands: true },
2716
+ transport: ["webhook", "gateway"],
2717
+ identity: { peerIdKind: "slack_user", peerIdScope: "workspace_scoped" },
2718
+ readinessGates: CONTAINER_GATES
2719
+ })
2720
+ },
2721
+ {
2722
+ id: "teams",
2723
+ displayName: "Microsoft Teams",
2724
+ addressSpace: "app",
2725
+ surfaces: ["direct", "group", "channel", "thread"],
2726
+ bindingCapabilities: ["teams.message"],
2727
+ baseline: baseline("teams", {
2728
+ operations: [...MESSAGE_CORE, "send.interactive", "edit.message", "delete.message", "channel.post", "thread.reply", "read.receipt"],
2729
+ direction: { inbound: true, outbound: true },
2730
+ initiation: { agentInitiated: "installed_only", userInitiated: true, firstContactRule: "proactive send needs an installation in scope plus a conversation reference" },
2731
+ windows: [],
2732
+ templates: { required: "never", reviewed: false },
2733
+ surfaces: ["direct", "group", "channel", "thread"],
2734
+ messageTypes: ["text", "image", "document", "card", "interactive", "reaction"],
2735
+ limits: { textBytes: 102400, note: "message payload \u2264100 KB (doc 01 \xA73)" },
2736
+ receipts: ["read"],
2737
+ edits: { edit: true, delete: true, reactions: false, commands: false },
2738
+ transport: ["webhook"],
2739
+ identity: { peerIdKind: "teams_user", peerIdScope: "installation_scoped" },
2740
+ readinessGates: CONTAINER_GATES
2741
+ })
2742
+ },
2743
+ {
2744
+ id: "rcs",
2745
+ displayName: "RCS",
2746
+ addressSpace: "rcs",
2747
+ surfaces: ["direct"],
2748
+ bindingCapabilities: ["rcs.message"],
2749
+ baseline: baseline("rcs", {
2750
+ operations: [...MESSAGE_CORE, "send.interactive", "read.receipt", "typing.indicate", "delete.message"],
2751
+ direction: { inbound: true, outbound: true },
2752
+ initiation: { agentInitiated: "opt_in_required", userInitiated: true, firstContactRule: "agent-initiated only with consent per AUP; STOP / UNSUBSCRIBE brand-wide" },
2753
+ windows: [],
2754
+ templates: { required: "never", reviewed: false },
2755
+ surfaces: ["direct"],
2756
+ messageTypes: ["text", "image", "audio", "video", "document", "card", "carousel", "interactive"],
2757
+ limits: { textChars: 3072, mediaBytes: 104857600 },
2758
+ receipts: ["delivered", "read"],
2759
+ edits: { edit: false, delete: true, reactions: false, commands: false },
2760
+ transport: ["webhook"],
2761
+ identity: { peerIdKind: "phone", peerIdScope: "global" },
2762
+ registration: [{ kind: "agent_launch", blocks: ["send.text", "send.media", "send.interactive", "initiate.conversation"] }],
2763
+ readinessGates: [
2764
+ { condition: "CredentialsValid", class: "authority", blocks: "*", ttlSec: 300 },
2765
+ { condition: "RegistrationApproved", class: "authority", blocks: ["send.text", "send.media", "send.interactive", "initiate.conversation"], ttlSec: 86400 },
2766
+ { condition: "PeerReachable", class: "authority", blocks: ["send.text", "send.media", "send.interactive", "initiate.conversation"], ttlSec: 86400 },
2767
+ { condition: "WebhookHealthy", class: "operational", blocks: ["receive.message"], ttlSec: 900, evidence: "runtime" }
2768
+ ]
2769
+ })
2770
+ }
2771
+ ];
2772
+ new Map(CHANNELS.map((c) => [c.id, c]));
2773
+ var CHANNEL_IDS = new Set(CHANNELS.map((c) => c.id));
2774
+
2775
+ // ../contracts/src/channel-connector/operations.ts
2776
+ var OPERATION_MODES = ["message", "realtime"];
2777
+ var OPERATIONS = [
2778
+ { id: "receive.message", mode: "message", summary: "accept an inbound message" },
2779
+ { id: "send.text", mode: "message", summary: "send a text message" },
2780
+ { id: "send.media", mode: "message", summary: "send a message carrying a content item (image, audio, video, document)" },
2781
+ { id: "send.template", mode: "message", summary: "send a reviewed template" },
2782
+ { id: "send.interactive", mode: "message", summary: "send structured/interactive content" },
2783
+ { id: "send.reaction", mode: "message", summary: "react to a message" },
2784
+ { id: "edit.message", mode: "message", summary: "edit a sent message" },
2785
+ { id: "delete.message", mode: "message", summary: "delete a sent message" },
2786
+ { id: "initiate.conversation", mode: "message", summary: "first contact / outside any window" },
2787
+ { id: "reply.in_window", mode: "message", summary: "reply inside an open window" },
2788
+ { id: "call.receive", mode: "realtime", summary: "accept an inbound realtime session" },
2789
+ { id: "call.place", mode: "realtime", summary: "place an outbound realtime session" },
2790
+ { id: "read.receipt", mode: "message", summary: "mark a message as read" },
2791
+ { id: "typing.indicate", mode: "message", summary: "typing indicator" },
2792
+ { id: "command.register", mode: "message", summary: "register slash/bot commands" },
2793
+ { id: "thread.reply", mode: "message", summary: "reply inside a thread" },
2794
+ { id: "group.post", mode: "message", summary: "post to a group surface" },
2795
+ { id: "channel.post", mode: "message", summary: "post to a channel surface" }
2796
+ ];
2797
+ new Map(
2798
+ OPERATIONS.map((op) => [op.id, op])
2799
+ );
2800
+ var OPERATION_IDS = new Set(OPERATIONS.map((op) => op.id));
2801
+
2802
+ // ../contracts/src/channel-connector/ids.ts
2803
+ var A2_SHIPPED_ROOTS = [
2804
+ "llm",
2805
+ "conversation",
2806
+ "channels",
2807
+ "tools",
2808
+ "guard",
2809
+ "store",
2810
+ "compliance",
2811
+ "carriers",
2812
+ "agents",
2813
+ "agentRun",
2814
+ "telemetry"
2815
+ ];
2816
+ var A2_PLATFORM_ROOTS = [
2817
+ ...A2_SHIPPED_ROOTS,
2818
+ "policy",
2819
+ "identity",
2820
+ "binding",
2821
+ "provider",
2822
+ "workflow",
2823
+ "session",
2824
+ "engine",
2825
+ "message",
2826
+ "connector",
2827
+ "communication",
2828
+ "event",
2829
+ "reach",
2830
+ "handler",
2831
+ "plugin",
2832
+ "scope",
2833
+ "snapshot"
2834
+ ];
2835
+ /* @__PURE__ */ new Set([
2836
+ ...A2_PLATFORM_ROOTS,
2837
+ ...[...OPERATION_IDS].map((id) => id.split(".")[0]),
2838
+ ...CHANNEL_IDS
2839
+ ]);
2840
+ var NAMESPACE_RE = /^[a-z0-9]+\.[a-z0-9]+$/;
2841
+ var SEMVER_RE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$/;
2842
+ var MODALITY_KINDS = ["text", "image", "audio", "video", "document"];
2843
+ var STRUCTURED_KINDS = [
2844
+ "location",
2845
+ "contact",
2846
+ "sticker",
2847
+ "reaction",
2848
+ "interactive",
2849
+ "card",
2850
+ "carousel",
2851
+ "poll",
2852
+ "template"
2853
+ ];
2854
+ var CONTENT_KINDS = [...MODALITY_KINDS, ...STRUCTURED_KINDS];
2855
+ new Set(CONTENT_KINDS);
2856
+ var LOSSINESS = ["lossless", "lossy", "semantic"];
2857
+ var CONTENT_REF_RE = /^content:([A-Za-z0-9_-]+)\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/;
2858
+ function parseContentRef(ref) {
2859
+ const m = CONTENT_REF_RE.exec(ref);
2860
+ return m ? { principalId: m[1], contentId: m[2] } : null;
2861
+ }
2862
+ function isContentRef(value) {
2863
+ return typeof value === "string" && CONTENT_REF_RE.test(value);
2864
+ }
2865
+ function looksLikeProviderLocator(value) {
2866
+ return /^(https?:\/\/|s3:\/\/|gs:\/\/)/i.test(value) || value.includes("?") || value.includes("://");
2867
+ }
2868
+ var MessageContentProvenance = z.object({
2869
+ /** The connector that produced the item (A7 id). */
2870
+ connector: z.string(),
2871
+ /** The provider-native identifier — reconciliation only; never agent-facing. */
2872
+ providerRef: z.string().optional(),
2873
+ receivedAt: z.string().datetime().optional()
2874
+ }).strict();
2875
+ var noLocator = (label) => z.string().refine((v) => !looksLikeProviderLocator(v), { message: `${label} must not carry a provider locator \u2014 provider ids live in provenance.providerRef only` });
2876
+ var MessageContentMetadata = z.object({
2877
+ bytes: z.number().int().nonnegative().optional(),
2878
+ durationMs: z.number().int().nonnegative().optional(),
2879
+ width: z.number().int().positive().optional(),
2880
+ height: z.number().int().positive().optional(),
2881
+ filename: noLocator("metadata.filename").optional(),
2882
+ caption: noLocator("metadata.caption").optional(),
2883
+ /** e.g. `voice_note` for audio. */
2884
+ variant: z.string().regex(/^[a-z0-9_]+$/).optional(),
2885
+ language: z.string().max(16).optional()
2886
+ }).strict();
2887
+ var contentRefSchema = z.string().refine(isContentRef, {
2888
+ message: "ref must be an opaque platform content reference `content:<principal>/<uuid>`"
2889
+ });
2890
+ var OriginalMessageContent = z.object({
2891
+ kind: z.enum(CONTENT_KINDS),
2892
+ /** MIME type or registry format id. */
2893
+ format: z.string().min(1),
2894
+ ref: contentRefSchema,
2895
+ metadata: MessageContentMetadata.default({}),
2896
+ provenance: MessageContentProvenance,
2897
+ /** Inline text for `text` kind only (the ref still names the stored item). */
2898
+ text: z.string().optional()
2899
+ }).strict().refine((item) => item.text === void 0 || item.kind === "text", { message: "inline text is only valid on kind=text" });
2900
+ var DerivedMessageContent = OriginalMessageContent.innerType().extend({
2901
+ derivedFrom: contentRefSchema,
2902
+ /** The transform capability id (registry or extension id). */
2903
+ transform: z.string().min(1),
2904
+ /** A7 actor ref of the producer. */
2905
+ producer: z.string().min(1),
2906
+ lossiness: z.enum(LOSSINESS)
2907
+ }).strict().refine((item) => item.text === void 0 || item.kind === "text", { message: "inline text is only valid on kind=text" }).refine((item) => parseContentRef(item.derivedFrom)?.principalId === parseContentRef(item.ref)?.principalId, {
2908
+ message: "a derived item must derive from content of the same principal"
2909
+ });
2910
+ z.union([DerivedMessageContent, OriginalMessageContent]);
2911
+ var STEP_KINDS = [
2912
+ "FORM",
2913
+ "SECRET",
2914
+ "OAUTH",
2915
+ "APP_INSTALL",
2916
+ "RESOURCE_SELECT",
2917
+ "RESOURCE_CREATE",
2918
+ "REGISTER",
2919
+ "SUBMIT_APPROVAL",
2920
+ "WAIT",
2921
+ "ACTION_REQUIRED",
2922
+ "GATEWAY_CONNECT",
2923
+ "PROBE",
2924
+ "ACTIVATE"
2925
+ ];
2926
+ var OWNERSHIP_MODES = ["platform_managed", "customer_owned"];
2927
+ new Set(OWNERSHIP_MODES);
2928
+ new Set(STEP_KINDS);
2929
+ var WorkflowStep = z.object({
2930
+ id: z.string().min(1),
2931
+ kind: z.enum(STEP_KINDS),
2932
+ title: z.string().min(1),
2933
+ /** JSON Schema for FORM/SECRET inputs — schema-driven, no connector UI code. */
2934
+ schema: z.record(z.unknown()).optional(),
2935
+ /** Credential role for SECRET/OAUTH outputs. */
2936
+ role: z.string().optional(),
2937
+ approvalKind: z.string().optional(),
2938
+ resourceKind: z.string().optional(),
2939
+ mode: z.string().optional(),
2940
+ dependsOn: z.array(z.string()).default([]),
2941
+ optional: z.boolean().default(false),
2942
+ /**
2943
+ * Ownership modes this step applies to (absent = all). A REQUIRED step out of the run's mode is ownership-skipped
2944
+ * (`OUT_OF_MODE`), which is distinct from `optional` — the engine must skip it regardless of `optional` (#306 §9).
2945
+ */
2946
+ ownership: z.array(z.enum(OWNERSHIP_MODES)).optional(),
2947
+ repeatable: z.boolean().default(false),
2948
+ producesChannels: z.array(z.string()).default([])
2949
+ }).strict();
2950
+ var ConnectorWorkflow = z.object({
2951
+ version: z.string().min(1),
2952
+ steps: z.array(WorkflowStep).min(1),
2953
+ /** What ACTIVATE requires (condition type ids). */
2954
+ activationGates: z.array(z.string()),
2955
+ /** The single step that re-establishes credentials. */
2956
+ reauth: z.object({ stepId: z.string() }).strict(),
2957
+ /** Steps that adopt existing resources on transfer / reconnect. */
2958
+ relink: z.object({ stepIds: z.array(z.string()) }).strict()
2959
+ }).strict();
2960
+ var EXCLUSIVITY = ["exclusive", "per_connection", "shared"];
2961
+ var TIMEOUT_EFFECTS = ["UNKNOWN", "NO_EFFECT", "PROVIDER_ACCEPTS", "PROVIDER_REJECTS"];
2962
+ var RECONCILE_BY = ["provider_ref", "correlation_tag", "listing", "none"];
2963
+ var ABSENCE_PROOF_METHODS = ["provider_ref", "correlation_tag", "listing"];
2964
+ var DUPLICATE_COSTS = ["low", "high"];
2965
+ var RESOURCE_KIND_RE = /^[a-z0-9]+(\.[a-z0-9_]+)+$/;
2966
+ var ResourceKindDeclaration = z.object({
2967
+ kind: z.string().regex(RESOURCE_KIND_RE, "resource kind must be a namespaced dotted id"),
2968
+ /** Customer-facing name of the kind (presentation only; the connector owns its provider vocabulary). */
2969
+ label: z.string().min(1).optional(),
2970
+ parentKinds: z.array(z.string()).default([]),
2971
+ /** Channels this kind enables (registry ids), if any. */
2972
+ channels: z.array(z.string()).default([]),
2973
+ /** An installation target (guild, workspace, tenant, business account). */
2974
+ isContainer: z.boolean().default(false),
2975
+ /** A channel identity (sender). */
2976
+ isSender: z.boolean().default(false),
2977
+ isRegulatory: z.boolean().default(false),
2978
+ exclusivity: z.enum(EXCLUSIVITY),
2979
+ /** Provider-side sharing cap, manifest DATA never a core constant (e.g. Meta WABA = 2). */
2980
+ maxHolders: z.number().int().positive().optional(),
2981
+ /** doc 06 `reconcile { by, absenceProof?, duplicateCost }`. */
2982
+ reconcile: z.object({
2983
+ by: z.enum(RECONCILE_BY),
2984
+ absenceProof: z.object({ minAgeSec: z.number().int().nonnegative(), method: z.enum(ABSENCE_PROOF_METHODS) }).strict().optional(),
2985
+ duplicateCost: z.enum(DUPLICATE_COSTS)
2986
+ }).strict(),
2987
+ /** doc 06 `release { destructive }`. */
2988
+ release: z.object({ destructive: z.boolean() }).strict().default({ destructive: false }),
2989
+ /** Timeout semantics for the effect that creates this kind (D-23). */
2990
+ createTimeoutEffect: z.enum(TIMEOUT_EFFECTS).default("UNKNOWN")
2991
+ }).strict().superRefine((decl, ctx) => {
2992
+ if (decl.isContainer && decl.exclusivity === "exclusive") {
2993
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: `container kind ${decl.kind} cannot be exclusive` });
2994
+ }
2995
+ if (decl.maxHolders !== void 0 && decl.exclusivity === "exclusive" && decl.maxHolders !== 1) {
2996
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: `exclusive kind ${decl.kind} cannot declare maxHolders > 1` });
2997
+ }
2998
+ if (decl.exclusivity === "shared" && (decl.isSender || decl.isContainer)) {
2999
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: `shared kind ${decl.kind} is referenced by value and cannot be a sender or container` });
3000
+ }
3001
+ if (decl.reconcile.absenceProof && decl.reconcile.by === "none") {
3002
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: `kind ${decl.kind}: absenceProof needs a reconcile method other than none` });
3003
+ }
3004
+ });
3005
+
3006
+ // ../contracts/src/channel-connector/approvals.ts
3007
+ var APPROVAL_KINDS = [
3008
+ "business_verification",
3009
+ "campaign_vetting",
3010
+ "toll_free_verification",
3011
+ "app_review",
3012
+ "privileged_permission_review",
3013
+ "admin_consent",
3014
+ "display_name_review",
3015
+ "template_review",
3016
+ "agent_launch",
3017
+ "brand_verification"
3018
+ ];
3019
+ new Set(APPROVAL_KINDS);
3020
+ var DEPLOYMENT_MODELS = ["per_tenant_app", "platform_app", "both"];
3021
+ var CREDENTIAL_STRENGTHS = ["ephemeral", "brokered", "exported_static"];
3022
+ var CREDENTIAL_ROLES = ["primary", "signing", "webhook", "refresh", "app_signing", "oauth_client", "partner"];
3023
+ var INSTALLATION_KINDS = ["guild", "user", "workspace", "org", "tenant", "team", "business_account", "chat"];
3024
+ new Set(INSTALLATION_KINDS);
3025
+ z.string().regex(/^cred:[a-z0-9_-]+\/[^\s]+$/, "A5 credential ref `cred:<store>/<path>`");
3026
+ var AUTHORIZATION_METHODS = ["oauth", "api_key", "api_secret", "account_token", "bot_token", "delegated_app", "installation_token", "certificate", "other"];
3027
+ var AUTHORIZATION_CAPABILITIES = ["discover", "provision", "send", "receive", "manage_resources", "manage_compliance", "manage_webhooks", "manage_installations", "manage_accounts"];
3028
+ var AuthorizationMethodDeclaration = z.object({
3029
+ id: z.string().min(1),
3030
+ method: z.enum(AUTHORIZATION_METHODS),
3031
+ credentialRoles: z.array(z.enum(CREDENTIAL_ROLES)).min(1),
3032
+ capabilities: z.array(z.enum(AUTHORIZATION_CAPABILITIES)).default([]),
3033
+ scope: z.enum(["account", "container"]).optional()
3034
+ }).strict();
3035
+ var INBOUND_AUTH_AVAILABILITY = ["ga", "beta", "private_beta", "unavailable"];
3036
+ var InboundAuthMethodDeclaration = z.object({
3037
+ id: z.string().min(1),
3038
+ verifier: z.string().min(1),
3039
+ credentialRole: z.enum(CREDENTIAL_ROLES),
3040
+ compatibleWith: z.array(z.object({ authMethodId: z.string().min(1), reusesAuthCredential: z.boolean() }).strict()).default([]),
3041
+ availability: z.enum(INBOUND_AUTH_AVAILABILITY).default("ga")
3042
+ }).strict();
3043
+ var MutationApprovalDeclaration = z.object({
3044
+ effect: z.string().min(1),
3045
+ approval: z.enum(["explicit", "implicit"]),
3046
+ capturePriorState: z.boolean().default(false)
3047
+ }).strict();
3048
+ var ReadinessGateSchema = z.object({
3049
+ condition: z.string().min(1),
3050
+ class: z.enum(CONDITION_CLASSES),
3051
+ blocks: z.union([z.literal("*"), z.array(z.string())]),
3052
+ ttlSec: z.number().int().nonnegative(),
3053
+ evidence: z.enum(["configuration", "runtime"]).optional()
3054
+ }).strict();
3055
+ var FormatsSchema = z.record(
3056
+ z.enum(CONTENT_KINDS),
3057
+ z.object({ mime: z.array(z.string()).optional(), variants: z.array(z.string()).optional() }).strict()
3058
+ );
3059
+ var LimitsSchema = z.object({
3060
+ textChars: z.number().int().positive().optional(),
3061
+ textBytes: z.number().int().positive().optional(),
3062
+ mediaBytes: z.number().int().positive().optional(),
3063
+ segments: z.boolean().optional(),
3064
+ perSecond: z.number().positive().optional(),
3065
+ perRecipientPerSecond: z.number().positive().optional(),
3066
+ formats: FormatsSchema.optional(),
3067
+ note: z.string().optional()
3068
+ }).strict();
3069
+ var DescriptorOverride = z.object({
3070
+ version: z.string().min(1).optional(),
3071
+ operations: z.array(z.string()).optional(),
3072
+ direction: z.object({ inbound: z.boolean(), outbound: z.boolean() }).strict().optional(),
3073
+ initiation: z.object({ agentInitiated: z.enum(AGENT_INITIATION), userInitiated: z.boolean(), firstContactRule: z.string() }).strict().optional(),
3074
+ windows: z.array(
3075
+ z.object({
3076
+ kind: z.literal("reply"),
3077
+ durationSec: z.number().int().positive(),
3078
+ opensOn: z.enum(["inbound_message", "user_action"]),
3079
+ outside: z.enum(WINDOW_OUTSIDE)
3080
+ }).strict()
3081
+ ).optional(),
3082
+ templates: z.object({ required: z.enum(TEMPLATE_REQUIREMENT), reviewed: z.boolean(), categories: z.array(z.string()).optional() }).strict().optional(),
3083
+ surfaces: z.array(z.enum(SURFACES)).optional(),
3084
+ messageTypes: z.array(z.enum(CONTENT_KINDS)).optional(),
3085
+ limits: LimitsSchema.optional(),
3086
+ receipts: z.array(z.enum(RECEIPT_KINDS)).optional(),
3087
+ edits: z.object({ edit: z.boolean(), delete: z.boolean(), reactions: z.boolean(), commands: z.boolean() }).strict().optional(),
3088
+ transport: z.array(z.enum(TRANSPORT_KINDS)).optional(),
3089
+ identity: z.object({ peerIdKind: z.string(), peerIdScope: z.enum(PEER_ID_SCOPES) }).strict().optional(),
3090
+ requiredPermissions: z.array(z.string()).optional(),
3091
+ registration: z.array(z.object({ kind: z.string(), blocks: z.array(z.string()) }).strict()).optional(),
3092
+ readinessGates: z.array(ReadinessGateSchema).optional()
3093
+ }).strict();
3094
+ var DecisionRequestDeclaration = z.object({
3095
+ kind: z.string().min(1),
3096
+ responseDeadlineSec: z.number().int().positive(),
3097
+ /** The provider's law when we stay silent. */
3098
+ defaultOutcome: z.enum(TIMEOUT_EFFECTS)
3099
+ }).strict();
3100
+ z.object({
3101
+ /** A7 `<publisher>/<name>`. */
3102
+ id: z.string().min(3),
3103
+ /** A7: semver. */
3104
+ version: z.string().regex(SEMVER_RE, "A7: plugin versions are semver"),
3105
+ displayName: z.string().min(1),
3106
+ publisher: z.string().min(1),
3107
+ /** A STABLE https URL of the provider's own console (the `provider_console` remediation carries it); absent ⇒ remediation has no link. */
3108
+ consoleUrl: z.string().url().optional(),
3109
+ /** The publisher's reverse-domain namespace (`com.acme`) under which every contributed extension id must sit; verified in ch. 05 (D-9). Absent for first-party connectors, which use registry ids only. */
3110
+ namespace: z.string().regex(NAMESPACE_RE, "namespace must be <root>.<label>").optional(),
3111
+ deployment: z.enum(DEPLOYMENT_MODELS),
3112
+ credentialStrength: z.enum(CREDENTIAL_STRENGTHS),
3113
+ /** Roles this connector's connections store refs under. */
3114
+ credentialRoles: z.array(z.enum(CREDENTIAL_ROLES)).default(["primary"]),
3115
+ /** Channels served, each with its descriptor diff over the baseline. */
3116
+ channels: z.array(z.object({ channel: z.string().min(1), descriptor: DescriptorOverride.default({}) }).strict()).min(1),
3117
+ /** Operations the connector implements (registry ids or its own extension ops). */
3118
+ operations: z.array(z.string()).min(1),
3119
+ /** Extension operations this connector defines — each MUST declare a mode. */
3120
+ extensionOperations: z.array(z.object({ id: z.string(), mode: z.enum(OPERATION_MODES), summary: z.string() }).strict()).default([]),
3121
+ onboarding: ConnectorWorkflow,
3122
+ resourceKinds: z.array(ResourceKindDeclaration).default([]),
3123
+ installationKinds: z.array(z.string()).default([]),
3124
+ transports: z.array(z.enum(TRANSPORT_KINDS)).min(1),
3125
+ /** doc 05 §2: transports the provider makes mutually exclusive (e.g. webhook vs long_poll). */
3126
+ transportsExclusive: z.array(z.array(z.enum(TRANSPORT_KINDS)).min(2)).default([]),
3127
+ /** Verifier id the ingress runs before parsing; required with the `webhook` transport. */
3128
+ webhookVerifier: z.string().min(1).optional(),
3129
+ /** Reconcile handler ids by resource kind (doc 02). */
3130
+ reconcilers: z.array(z.object({ resourceKind: z.string(), handler: z.string().min(1) }).strict()).default([]),
3131
+ /** Inbound decision requests with provider-defined defaults (D-23). */
3132
+ decisionRequests: z.array(DecisionRequestDeclaration).default([]),
3133
+ /** Identifier kinds contributed to the ch. 00 B-1 vocabulary. */
3134
+ identifierKinds: z.array(z.string()).default([]),
3135
+ /** Approval kinds used (registry or extension ids). */
3136
+ approvalKinds: z.array(z.string()).default([]),
3137
+ /** Attribution key names used under `platform_app` (doc 08 §4a step 3). */
3138
+ attributionKeys: z.array(z.string()).default([]),
3139
+ /** Quarantine window for PENDING_ATTRIBUTION (D-21), seconds. */
3140
+ pendingAttributionTtlSec: z.number().int().positive().default(7 * 86400),
3141
+ metadata: z.record(z.unknown()).default({}),
3142
+ /** Which relationship ownerships this connector serves (#306 §A/C1). A managed-capable connector MUST declare it explicitly; the default serves only the common OAuth-BYO connector. */
3143
+ ownershipModes: z.array(z.enum(OWNERSHIP_MODES)).min(1).default(["customer_owned"]),
3144
+ /** Authorization methods the connector supports (#306 §4/C3). */
3145
+ authorizationMethods: z.array(AuthorizationMethodDeclaration).default([]),
3146
+ /** Inbound-event authentication strategies (#306 §4/C4); `webhookVerifier` is the single-strategy shorthand, normalized by `resolveInboundAuthMethods`. */
3147
+ inboundAuthMethods: z.array(InboundAuthMethodDeclaration).default([]),
3148
+ /** Provider effects that require an explicit decision, keyed on stable effect ids (#306 L5/C5). */
3149
+ mutationApprovals: z.array(MutationApprovalDeclaration).default([])
3150
+ }).strict();
3151
+ var Label = z.string().trim().min(1).max(60);
3152
+ var Short = z.string().max(300);
3153
+ var Body = z.string().max(4e3);
3154
+ var Iso = z.string().datetime();
3155
+ var MonitoringAuthor = z.enum(["peer", "agent", "system"]);
3156
+ var MonitoringTone = z.enum(["ok", "warn", "bad", "info", "neutral"]);
3157
+ var UNSAFE_CHARS = /[\u0000-\u001f\u007f-\u009f\u00ad\u061c\u180e\u200b-\u200f\u202a-\u202e\u2060-\u2064\u2066-\u2069\ufeff\u{e0000}-\u{e007f}]/u;
3158
+ function isSafeMonitoringHref(href) {
3159
+ if (href !== href.trim() || UNSAFE_CHARS.test(href) || href.includes("\\") || /%2e|%2f|%5c|%25/i.test(href)) return false;
3160
+ if (href.startsWith("/")) {
3161
+ if (href.startsWith("//")) return false;
3162
+ try {
3163
+ const u = new URL(href, "https://dashboard.invalid");
3164
+ return u.origin === "https://dashboard.invalid" && u.pathname.startsWith("/dashboard/") && u.pathname + u.search + u.hash === href;
3165
+ } catch {
3166
+ return false;
3167
+ }
3168
+ }
3169
+ if (!href.startsWith("https://")) return false;
3170
+ try {
3171
+ const u = new URL(href);
3172
+ return u.protocol === "https:" && !u.username && !u.password && Boolean(u.hostname);
3173
+ } catch {
3174
+ return false;
3175
+ }
3176
+ }
3177
+ var MonitoringBlock = z.discriminatedUnion("type", [
3178
+ /** One line of the conversation: what was said, by whom. */
3179
+ z.object({ type: z.literal("message"), author: MonitoringAuthor, text: Body, at: Iso.optional() }).strict(),
3180
+ /** Channel facts worth seeing at a glance (Discord server/channel, SMS from/to, call duration). */
3181
+ z.object({ type: z.literal("facts"), items: z.array(z.object({ label: Label, value: Short }).strict()).min(1).max(12) }).strict(),
3182
+ /** The communication's state in words, with a tone. */
3183
+ z.object({ type: z.literal("status"), label: Label, tone: MonitoringTone }).strict(),
3184
+ /** Somewhere to go: the provider's own view of it (https) or a deeper dashboard page. */
3185
+ z.object({ type: z.literal("link"), label: Label, href: z.string().max(2e3).refine(isSafeMonitoringHref, "href must be https or a /dashboard/ path") }).strict(),
3186
+ /** A dated lifecycle fact (sent, delivered, answered, ended). */
3187
+ z.object({ type: z.literal("event"), label: z.string().trim().min(1).max(120), at: Iso }).strict(),
3188
+ /** A spoken exchange, turn by turn. */
3189
+ z.object({
3190
+ type: z.literal("transcript"),
3191
+ turns: z.array(z.object({ author: MonitoringAuthor, text: Body, offsetMs: z.number().int().nonnegative().optional() }).strict()).max(500)
3192
+ }).strict()
3193
+ ]);
3194
+ var PresentedCommunication = z.object({
3195
+ /** One line for lists ("Asked: What is 2 + 2?"). */
3196
+ summary: z.string().max(140).optional(),
3197
+ /**
3198
+ * Something new starts at this communication — e.g. the agent began a fresh session with no memory of what came
3199
+ * before. The page draws a labelled divider before it. Additive to v1: a reader that ignores it loses nothing else.
3200
+ */
3201
+ boundary: z.object({ label: z.string().trim().min(1).max(80) }).strict().optional(),
3202
+ blocks: z.array(MonitoringBlock).max(50)
3203
+ }).strict();
3204
+ var MonitoringChannelId = z.string().regex(/^[a-z][a-z0-9_.-]{0,39}$/);
3205
+ var MonitoringChannelRef = z.object({ id: z.string(), label: z.string() }).strict();
3206
+ var MonitoredPeer = z.object({
3207
+ kind: z.string().nullable(),
3208
+ address: z.string().nullable(),
3209
+ /** From the channel's presenter (`describePeer`); null when it has no better name than the address. */
3210
+ displayName: z.string().nullable()
3211
+ }).strict();
3212
+ var MonitoredConversationSummary = z.object({
3213
+ id: z.string().uuid(),
3214
+ status: z.string(),
3215
+ openedAt: z.string(),
3216
+ lastActivityAt: z.string(),
3217
+ agent: z.object({ id: z.string(), name: z.string() }).strict().nullable(),
3218
+ peer: MonitoredPeer,
3219
+ channels: z.array(MonitoringChannelRef),
3220
+ communications: z.number().int().nonnegative(),
3221
+ /** The latest INBOUND communication's one-liner (else the latest), as its channel presents it. */
3222
+ summary: z.string().nullable(),
3223
+ /** Something in it failed (a message not delivered, a call that failed). */
3224
+ failed: z.boolean(),
3225
+ /** A call in it is still in progress. */
3226
+ live: z.boolean()
3227
+ }).strict();
3228
+ var MonitoredCommunication = z.object({
3229
+ id: z.string().uuid(),
3230
+ type: z.string(),
3231
+ channel: MonitoringChannelRef,
3232
+ direction: z.string(),
3233
+ status: z.string(),
3234
+ startedAt: z.string(),
3235
+ endedAt: z.string().nullable(),
3236
+ /** Which presenter shaped it (`vl/discord`, `vl/sms`, `generic`), for support. */
3237
+ presenter: z.string(),
3238
+ presented: PresentedCommunication
3239
+ }).strict();
3240
+ var MonitoringUnlinkedHistory = z.object({ calls: z.boolean(), messages: z.boolean() }).strict();
3241
+ z.object({
3242
+ blocksVersion: z.number().int(),
3243
+ scope: z.string(),
3244
+ conversations: z.array(MonitoredConversationSummary),
3245
+ /** Opaque; pass back as `cursor` for the next (older) page. Null on the last page. */
3246
+ nextCursor: z.string().nullable(),
3247
+ /** History from before a channel was linked to the ledger (not shown here, 🔒 control-plane F8/F12). */
3248
+ unlinkedHistory: MonitoringUnlinkedHistory
3249
+ }).strict();
3250
+ z.object({
3251
+ blocksVersion: z.number().int(),
3252
+ scope: z.string(),
3253
+ conversation: MonitoredConversationSummary,
3254
+ /** Oldest first. The page holds the LATEST communications; `earlierCursor` fetches the ones before them. */
3255
+ communications: z.array(MonitoredCommunication),
3256
+ earlierCursor: z.string().nullable()
3257
+ }).strict();
3258
+ z.object({
3259
+ scope: z.string(),
3260
+ /** Channels a presenter currently shows, named by that presenter. */
3261
+ channels: z.array(MonitoringChannelRef),
3262
+ /** The Lines (numbers, bots) conversations in this scope arrived on. */
3263
+ lines: z.array(z.object({ id: z.string().uuid(), label: z.string() }).strict())
3264
+ }).strict();
3265
+ var MONITORING_DIRECTIONS = ["inbound", "outbound"];
3266
+ var MONITORING_STATES = ["live", "failed", "ok"];
3267
+ var MONITORING_MAX_ID_FILTERS = 50;
3268
+ var MONITORING_MAX_CHANNEL_FILTERS = 20;
3269
+ z.object({
3270
+ scope: z.enum(["env:dev", "env:stg", "env:prd"]).optional(),
3271
+ limit: z.coerce.number().int().min(1).max(100).optional(),
3272
+ cursor: z.string().max(200).optional(),
3273
+ from: z.string().datetime().optional(),
3274
+ to: z.string().datetime().optional(),
3275
+ channel: z.array(MonitoringChannelId).max(MONITORING_MAX_CHANNEL_FILTERS).optional(),
3276
+ direction: z.array(z.enum(MONITORING_DIRECTIONS)).max(2).optional(),
3277
+ line: z.array(z.string().uuid()).max(MONITORING_MAX_ID_FILTERS).optional(),
3278
+ agent: z.array(z.string().uuid()).max(MONITORING_MAX_ID_FILTERS).optional(),
3279
+ status: z.array(z.enum(MONITORING_STATES)).max(3).optional(),
3280
+ customer: z.string().trim().max(100).optional()
3281
+ }).strict();
3282
+ var WalletRefusalCode = z.enum([
3283
+ // Prepaid balance (or an invoiced credit limit) cannot cover the action.
3284
+ "insufficient_balance",
3285
+ // The wallet is frozen after a card dispute, until an operator unfreezes it.
3286
+ "account_frozen",
3287
+ // An invoice is unpaid past its 7 days' grace (B-19): new calls, messages and answers are refused; inbound calls are
3288
+ // still answered. Paying the invoice restores the workspace.
3289
+ "account_suspended",
3290
+ // The action's reservation already closed (e.g. a call ended or expired) — it cannot be re-admitted.
3291
+ "hold_closed"
3292
+ ]);
3293
+ z.object({
3294
+ error: WalletRefusalCode,
3295
+ code: WalletRefusalCode,
3296
+ availableUsd: z.string(),
3297
+ needUsd: z.string(),
3298
+ message: z.string()
3299
+ });
3300
+ var WalletMode = z.enum(["prepaid", "invoiced"]);
3301
+ z.object({
3302
+ mode: WalletMode,
3303
+ status: z.enum(["active", "frozen"]),
3304
+ // Decimal strings, six places (the ledger's micro-dollar precision).
3305
+ balanceUsd: z.string(),
3306
+ // Reserved for calls in progress.
3307
+ heldUsd: z.string(),
3308
+ // balance − held (+ credit limit when invoiced); null = unlimited.
3309
+ availableUsd: z.string().nullable(),
3310
+ creditLimitUsd: z.string().nullable(),
3311
+ // A card saved by the first top-up — what auto top-up charges.
3312
+ cardOnFile: z.boolean(),
3313
+ autoTopup: z.object({ enabled: z.boolean(), thresholdUsd: z.string(), amountUsd: z.string() }),
3314
+ // B-19: 'past_due' = an invoice is overdue (a pay-now banner); 'suspended' = paused for it (or by an operator).
3315
+ standing: z.enum(["good", "past_due", "suspended"]),
3316
+ // Why it is suspended: an invoice unpaid past grace (paying restores it) or an operator (only support lifts it).
3317
+ suspension: z.enum(["unpaid_invoice", "operator"]).nullable(),
3318
+ // The oldest overdue invoice, when there is one.
3319
+ overdueInvoice: z.object({ amountUsd: z.string(), dueAt: z.string(), payUrl: z.string().nullable() }).nullable(),
3320
+ // Invoiced wallets: what the next invoice holds so far (charges since the last invoice); null when prepaid.
3321
+ uninvoicedUsd: z.string().nullable()
3322
+ });
3323
+ var WalletInvoiceStatus = z.enum(["draft", "open", "paid", "void", "uncollectible"]);
3324
+ var WalletInvoice = z.object({
3325
+ id: z.string(),
3326
+ // 'final' = issued when the workspace moved to prepaid.
3327
+ kind: z.enum(["monthly", "final"]),
3328
+ periodStart: z.string(),
3329
+ periodEnd: z.string(),
3330
+ amountUsd: z.string(),
3331
+ status: WalletInvoiceStatus,
3332
+ issuedAt: z.string().nullable(),
3333
+ dueAt: z.string().nullable(),
3334
+ paidAt: z.string().nullable(),
3335
+ // Stripe's hosted page to view and pay it (null until issued).
3336
+ payUrl: z.string().nullable()
3337
+ });
3338
+ z.object({ invoices: z.array(WalletInvoice) });
3339
+ var WalletEntryKind = z.enum(["grant", "topup", "usage", "refund", "refund_reversal", "dispute", "dispute_reinstated", "adjustment", "invoice_payment"]);
3340
+ var WalletEntry = z.object({
3341
+ id: z.string(),
3342
+ kind: WalletEntryKind,
3343
+ // Signed: a credit is positive, a debit negative.
3344
+ amountUsd: z.string(),
3345
+ balanceAfterUsd: z.string(),
3346
+ // The usage row a debit charged (usage entries only).
3347
+ usageEventId: z.string().nullable(),
3348
+ note: z.string().nullable(),
3349
+ createdAt: z.string()
3350
+ });
3351
+ z.object({
3352
+ entries: z.array(WalletEntry),
3353
+ // Pass as `before` for the next (older) page; null when there is none.
3354
+ nextBefore: z.string().nullable()
3355
+ });
3356
+ var TOPUP_MIN_USD = 10;
3357
+ var TOPUP_MAX_USD = 500;
3358
+ z.object({ amountUsd: z.number().int().min(TOPUP_MIN_USD).max(TOPUP_MAX_USD) });
3359
+ z.object({
3360
+ enabled: z.boolean(),
3361
+ thresholdUsd: z.number().min(1).max(TOPUP_MAX_USD),
3362
+ amountUsd: z.number().int().min(TOPUP_MIN_USD).max(TOPUP_MAX_USD)
3363
+ });
3364
+
3365
+ // src/brain/transport-tunnel.ts
3366
+ var AsyncQueue = class {
3367
+ values = [];
3368
+ waiting = [];
3369
+ ended = false;
3370
+ error = null;
3371
+ push(value) {
3372
+ if (this.ended) return;
3373
+ const w = this.waiting.shift();
3374
+ if (w) w.resolve({ value, done: false });
3375
+ else this.values.push(value);
3376
+ }
3377
+ end() {
3378
+ if (this.ended) return;
3379
+ this.ended = true;
3380
+ for (const w of this.waiting) w.resolve({ value: void 0, done: true });
3381
+ this.waiting = [];
3382
+ }
3383
+ fail(err) {
3384
+ if (this.ended) return;
3385
+ this.error = err;
3386
+ this.ended = true;
3387
+ for (const w of this.waiting) w.reject(err);
3388
+ this.waiting = [];
3389
+ }
3390
+ async *drain() {
3391
+ for (; ; ) {
3392
+ if (this.values.length > 0) {
3393
+ yield this.values.shift();
3394
+ continue;
3395
+ }
3396
+ if (this.ended) {
3397
+ if (this.error) throw this.error;
3398
+ return;
3399
+ }
3400
+ const result = await new Promise((resolve, reject) => {
3401
+ this.waiting.push({ resolve, reject });
3402
+ });
3403
+ if (result.done) {
3404
+ if (this.error) throw this.error;
3405
+ return;
3406
+ }
3407
+ yield result.value;
3408
+ }
3409
+ }
3410
+ };
3411
+ function tunnelBrainTransport(options) {
3412
+ const requestChannel = CONNECTOR_REQUEST_CHANNEL(options.connectorId);
3413
+ const timeoutMs = options.firstChunkTimeoutMs ?? 2e4;
3414
+ async function* stream(req, signal) {
3415
+ const streamId = randomUUID();
3416
+ const replyChannel = CONNECTOR_REPLY_CHANNEL(streamId);
3417
+ const queue = new AsyncQueue();
3418
+ let unsubscribe = null;
3419
+ let timer = null;
3420
+ const clearTimer = () => {
3421
+ if (timer) {
3422
+ clearTimeout(timer);
3423
+ timer = null;
3424
+ }
3425
+ };
3426
+ const cleanup = async () => {
3427
+ clearTimer();
3428
+ const u = unsubscribe;
3429
+ unsubscribe = null;
3430
+ if (u) await u().catch(() => {
3431
+ });
3432
+ };
3433
+ const onAbort = () => {
3434
+ void options.pubsub.publish(requestChannel, JSON.stringify({ kind: "brain.cancel", streamId })).catch(() => {
3435
+ });
3436
+ queue.end();
3437
+ };
3438
+ unsubscribe = await options.pubsub.subscribe(replyChannel, (raw) => {
3439
+ let parsed;
3440
+ try {
3441
+ parsed = JSON.parse(raw);
3442
+ } catch {
3443
+ return;
3444
+ }
3445
+ const frame = ConnectorUpFrame.safeParse(parsed);
3446
+ if (!frame.success) return;
3447
+ const f = frame.data;
3448
+ if (!("streamId" in f) || f.streamId !== streamId) return;
3449
+ clearTimer();
3450
+ if (f.kind === "brain.delta") {
3451
+ if (f.content) queue.push({ content: f.content });
3452
+ } else if (f.kind === "brain.done") {
3453
+ queue.end();
3454
+ } else if (f.kind === "brain.error") {
3455
+ queue.fail(new BrainRequestError(f.code, `brain ${f.code}: ${f.message}`));
3456
+ }
3457
+ });
3458
+ if (signal.aborted) {
3459
+ await cleanup();
3460
+ return;
3461
+ }
3462
+ signal.addEventListener("abort", onAbort, { once: true });
3463
+ timer = setTimeout(() => queue.fail(new BrainRequestError("no_response", "brain unreachable: no response from connector")), timeoutMs);
3464
+ await options.pubsub.publish(
3465
+ requestChannel,
3466
+ JSON.stringify({
3467
+ kind: "brain.request",
3468
+ streamId,
3469
+ ...req.model ? { model: req.model } : {},
3470
+ ...req.temperature !== void 0 ? { temperature: req.temperature } : {},
3471
+ messages: req.messages.map((m) => ({ role: m.role, content: m.content }))
3472
+ })
3473
+ );
3474
+ try {
3475
+ yield* queue.drain();
3476
+ } finally {
3477
+ signal.removeEventListener("abort", onAbort);
3478
+ await cleanup();
3479
+ }
3480
+ }
3481
+ async function complete(req, signal) {
3482
+ let acc = "";
3483
+ for await (const chunk of stream(req, signal)) acc += chunk.content ?? "";
3484
+ return acc;
3485
+ }
3486
+ return { kind: "tunnel", stream, complete };
3487
+ }
3488
+ var RedisBrainPubSub = class {
3489
+ constructor(sub, pub) {
3490
+ this.sub = sub;
3491
+ this.pub = pub;
3492
+ sub.on("message", (channel, raw) => {
3493
+ const ls = this.listeners.get(channel);
3494
+ if (!ls) return;
3495
+ for (const fn of ls) {
3496
+ try {
3497
+ fn(raw);
3498
+ } catch {
3499
+ }
3500
+ }
3501
+ });
3502
+ }
3503
+ sub;
3504
+ pub;
3505
+ listeners = /* @__PURE__ */ new Map();
3506
+ async publish(channel, message) {
3507
+ await this.pub.publish(channel, message);
3508
+ }
3509
+ async subscribe(channel, handler) {
3510
+ const set = this.listeners.get(channel) ?? /* @__PURE__ */ new Set();
3511
+ const first = set.size === 0;
3512
+ set.add(handler);
3513
+ this.listeners.set(channel, set);
3514
+ if (first) await this.sub.subscribe(channel);
3515
+ return async () => {
3516
+ const ls = this.listeners.get(channel);
3517
+ ls?.delete(handler);
3518
+ if (ls && ls.size === 0) {
3519
+ this.listeners.delete(channel);
3520
+ try {
3521
+ await this.sub.unsubscribe(channel);
3522
+ } catch {
3523
+ }
3524
+ }
3525
+ };
3526
+ }
3527
+ async close() {
3528
+ try {
3529
+ this.sub.disconnect();
3530
+ } catch {
3531
+ }
3532
+ try {
3533
+ this.pub.disconnect();
3534
+ } catch {
3535
+ }
3536
+ }
3537
+ };
3538
+ function buildRedisBrainPubSub(redisUrl) {
3539
+ if (!redisUrl) return null;
3540
+ try {
3541
+ const sub = new IORedis(redisUrl, { maxRetriesPerRequest: null, enableReadyCheck: false });
3542
+ const pub = new IORedis(redisUrl, { maxRetriesPerRequest: null, enableReadyCheck: false });
3543
+ return new RedisBrainPubSub(sub, pub);
3544
+ } catch (err) {
3545
+ console.warn("[brain pubsub] init failed", err);
3546
+ return null;
3547
+ }
3548
+ }
3549
+
3550
+ export { BrainConfigError, BrainRequestError, ConnectorChatModel, RedisBrainPubSub, assertPublicHttpsUrl, buildRedisBrainPubSub, callbackTransport, chatChunkStream, chatContextToMessages, createConnectorLLM, httpBrainTransport, isDisallowedIp, lastUserText, parseChatCompletionSse, runBrainRequest, tunnelBrainTransport };