@voicelayer/sdk 0.1.2 → 0.1.4

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,4119 @@
1
+ import { z } from 'zod';
2
+ import OpenAI from 'openai';
3
+ import 'quickjs-emscripten';
4
+ import { setup } from 'xstate';
5
+ import 'crypto';
6
+
7
+ var __defProp = Object.defineProperty;
8
+ var __getOwnPropNames = Object.getOwnPropertyNames;
9
+ var __esm = (fn, res) => function __init() {
10
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
11
+ };
12
+ var __export = (target, all) => {
13
+ for (var name in all)
14
+ __defProp(target, name, { get: all[name], enumerable: true });
15
+ };
16
+
17
+ // src/runtime/graph/graph-model.ts
18
+ function buildGraphModel(program) {
19
+ return {
20
+ entry: program.entry,
21
+ node: (id) => program.nodes[id],
22
+ out: (id) => program.nodes[id]?.out ?? [],
23
+ selectByPort: (id, port) => {
24
+ const edges = program.nodes[id]?.out ?? [];
25
+ const byHandle = edges.find((e) => e.sourceHandle === port);
26
+ if (byHandle) return byHandle.to;
27
+ const byLabel = edges.find((e) => e.label === port);
28
+ return byLabel ? byLabel.to : null;
29
+ },
30
+ askNodeForSlot: (slot) => {
31
+ const want = slot.trim();
32
+ for (const node of Object.values(program.nodes)) {
33
+ if (node.type !== "ask") continue;
34
+ const s = typeof node.config.slot === "string" ? node.config.slot : node.id;
35
+ if (s.trim() === want) return node.id;
36
+ }
37
+ return void 0;
38
+ }
39
+ };
40
+ }
41
+ var init_graph_model = __esm({
42
+ "src/runtime/graph/graph-model.ts"() {
43
+ }
44
+ });
45
+ var init_primitive = __esm({
46
+ "../contracts/src/primitive.ts"() {
47
+ }
48
+ });
49
+
50
+ // ../contracts/src/events.ts
51
+ var init_events = __esm({
52
+ "../contracts/src/events.ts"() {
53
+ }
54
+ });
55
+
56
+ // ../contracts/src/context.ts
57
+ var init_context = __esm({
58
+ "../contracts/src/context.ts"() {
59
+ }
60
+ });
61
+
62
+ // ../contracts/src/errors.ts
63
+ var init_errors = __esm({
64
+ "../contracts/src/errors.ts"() {
65
+ }
66
+ });
67
+ var init_ids = __esm({
68
+ "../contracts/src/ids.ts"() {
69
+ }
70
+ });
71
+ var FlowKind, FlowStatus, FlowNodeType, FlowNodePosition, FlowNode, FlowEdge, FlowGraph;
72
+ var init_flow = __esm({
73
+ "../contracts/src/flow.ts"() {
74
+ FlowKind = z.enum(["conversation"]);
75
+ FlowStatus = z.enum(["draft", "published", "archived"]);
76
+ z.enum(["shared", "dedicated"]);
77
+ FlowNodeType = z.enum([
78
+ // Explicit entry marker — the interpreter starts here (falls back to the
79
+ // first indegree-0 node for older graphs without one).
80
+ "start",
81
+ "say",
82
+ "ask",
83
+ "confirm",
84
+ "tool",
85
+ "decision",
86
+ "handoff",
87
+ "end",
88
+ "trigger",
89
+ "gate",
90
+ "playbook",
91
+ "menu",
92
+ "code",
93
+ "set",
94
+ // Reusable component: runs another published flow inline.
95
+ "subflow",
96
+ // AI nodes (PRD P5).
97
+ "llm",
98
+ "rag",
99
+ "memory",
100
+ "classify",
101
+ // Agentic router: per-turn, selects 1-of-N skills by their plain-language
102
+ // descriptions and takes that port (the resident routing surface / hub).
103
+ "router",
104
+ // Canvas-only annotation (sticky note); excluded from compilation/runtime.
105
+ "note"
106
+ ]);
107
+ FlowNodePosition = z.object({
108
+ x: z.number(),
109
+ y: z.number()
110
+ });
111
+ FlowNode = z.object({
112
+ id: z.string().min(1).max(128),
113
+ type: FlowNodeType,
114
+ position: FlowNodePosition,
115
+ // Node-specific config. Generic on purpose — the per-type shape is validated
116
+ // by the runtime compiler that targets each plane, not by storage.
117
+ data: z.record(z.unknown()).default({})
118
+ });
119
+ FlowEdge = z.object({
120
+ id: z.string().min(1).max(128),
121
+ // Source / target node ids (must reference FlowNode.id values).
122
+ source: z.string().min(1).max(128),
123
+ target: z.string().min(1).max(128),
124
+ // React Flow handle ids for multi-port nodes (e.g. a decision's branches).
125
+ sourceHandle: z.string().max(128).optional(),
126
+ targetHandle: z.string().max(128).optional(),
127
+ // Optional label / branch condition — e.g. a normalized tool `result.kind`.
128
+ label: z.string().max(128).optional(),
129
+ condition: z.string().max(512).optional()
130
+ });
131
+ FlowGraph = z.object({
132
+ nodes: z.array(FlowNode).max(1e3).default([]),
133
+ edges: z.array(FlowEdge).max(2e3).default([])
134
+ });
135
+ z.object({
136
+ id: z.string().uuid(),
137
+ projectId: z.string().min(1),
138
+ name: z.string().min(1).max(200),
139
+ kind: FlowKind,
140
+ status: FlowStatus,
141
+ // The working/draft graph. Published versions snapshot this.
142
+ graph: FlowGraph,
143
+ // Latest published version number; null until first publish.
144
+ publishedVersion: z.number().int().min(1).nullable(),
145
+ createdAt: z.string().datetime(),
146
+ // Optimistic-lock token (mirrors AgentConfig.updatedAt).
147
+ updatedAt: z.string().datetime()
148
+ });
149
+ z.object({
150
+ id: z.string().uuid(),
151
+ flowId: z.string().uuid(),
152
+ projectId: z.string().min(1),
153
+ version: z.number().int().min(1),
154
+ kind: FlowKind,
155
+ // Frozen graph at publish time.
156
+ graph: FlowGraph,
157
+ // Optional human note describing the publish.
158
+ note: z.string().max(1e3).nullable(),
159
+ publishedByUserId: z.string().nullable(),
160
+ publishedAt: z.string().datetime()
161
+ });
162
+ z.object({
163
+ name: z.string().trim().min(1).max(200),
164
+ kind: FlowKind,
165
+ // Optional initial graph; defaults to empty.
166
+ graph: FlowGraph.optional()
167
+ });
168
+ z.object({
169
+ name: z.string().trim().min(1).max(200).optional(),
170
+ graph: FlowGraph.optional(),
171
+ // Framework is a reversible default-structure switch (Voiceflow parity):
172
+ // changing kind re-frames the palette/deployability but never touches the graph.
173
+ kind: FlowKind.optional()
174
+ }).refine((b) => b.name !== void 0 || b.graph !== void 0 || b.kind !== void 0, {
175
+ message: "provide at least one of `name`, `graph`, or `kind`"
176
+ });
177
+ z.object({
178
+ note: z.string().max(1e3).optional()
179
+ });
180
+ }
181
+ });
182
+ var FlowProgramEdge, FlowProgramNode, FlowProgramDiagnostics, FlowProgram;
183
+ var init_flow_program = __esm({
184
+ "../contracts/src/flow-program.ts"() {
185
+ init_flow();
186
+ FlowProgramEdge = z.object({
187
+ to: z.string().min(1).max(128),
188
+ // Branch condition (e.g. `slot:Intent==billing`, `tool.lookup.ok`). Empty =
189
+ // default/fallthrough edge. Evaluated by the runtime's condition matcher.
190
+ condition: z.string().max(512).optional(),
191
+ // React Flow handle id for multi-branch nodes (decision/menu ports).
192
+ sourceHandle: z.string().max(128).optional(),
193
+ label: z.string().max(128).optional()
194
+ });
195
+ FlowProgramNode = z.object({
196
+ id: z.string().min(1).max(128),
197
+ type: FlowNodeType,
198
+ // Cleaned, per-type config (text / slot / ask / url / requiredFields / ...).
199
+ // Produced by compileFlowToProgram; read defensively by the executors.
200
+ config: z.record(z.unknown()),
201
+ // Outgoing edges in declaration order.
202
+ out: z.array(FlowProgramEdge).max(64)
203
+ });
204
+ FlowProgramDiagnostics = z.object({
205
+ // False if the graph contains a cycle (runtime still runs it, guarded by a
206
+ // per-call step/visit budget).
207
+ acyclic: z.boolean(),
208
+ // Edge ids whose source/target node was missing (skipped at compile).
209
+ droppedEdges: z.array(z.string().max(128)).max(256),
210
+ // Node ids needing external plumbing not wired yet (rag/classify/code) — the
211
+ // interpreter passes through them.
212
+ unsupportedNodes: z.array(z.string().max(128)).max(256)
213
+ });
214
+ FlowProgram = z.object({
215
+ v: z.literal(1),
216
+ id: z.string().min(1).max(64),
217
+ // Entry node id(s) — nodes with no inbound edge (interpreter starts here).
218
+ entry: z.array(z.string().min(1).max(128)).max(64),
219
+ nodes: z.record(FlowProgramNode),
220
+ // Mirrors the flat completion gate so the same gate computation is reused.
221
+ completionGate: z.object({ requiredFields: z.array(z.string()).max(64) }),
222
+ diagnostics: FlowProgramDiagnostics.optional()
223
+ });
224
+ }
225
+ });
226
+ var AgentEnvironment, AgentStatus, AgentName, AgentVersion, AgentMetadata, AgentCapability, ReadinessReport, ProcessFieldDTO, ProcessTriggerDTO, ProcessToolDTO, ProcessSchemaDTO, AgentDTO;
227
+ var init_agents = __esm({
228
+ "../contracts/src/agents.ts"() {
229
+ init_flow_program();
230
+ AgentEnvironment = z.enum(["dev", "stg", "prd"]);
231
+ AgentStatus = z.enum(["online", "offline", "degraded"]);
232
+ AgentName = z.string().min(1).max(128).regex(/^[a-z0-9][a-z0-9-_]*$/, 'lowercase letters, digits, "-" or "_" only');
233
+ AgentVersion = z.string().min(1).max(64);
234
+ AgentMetadata = z.record(z.unknown());
235
+ AgentCapability = z.string().min(1).max(64).regex(/^[a-z0-9][a-z0-9-_:.]*$/, 'lowercase letters, digits, "-_:." only');
236
+ ReadinessReport = z.object({
237
+ probe: z.string().min(1).max(128),
238
+ ok: z.boolean(),
239
+ detail: z.string().max(512).optional()
240
+ });
241
+ ProcessFieldDTO = z.object({
242
+ name: z.string().min(1).max(64),
243
+ required: z.boolean(),
244
+ type: z.enum(["string", "string[]", "number", "boolean", "datetime", "email", "phone"]),
245
+ pattern: z.string().max(1024).optional(),
246
+ min: z.number().optional(),
247
+ max: z.number().optional(),
248
+ enum: z.array(z.string()).max(64).optional(),
249
+ ask: z.string().max(512).optional()
250
+ });
251
+ ProcessTriggerDTO = z.object({
252
+ name: z.string().min(1).max(64),
253
+ on: z.string().min(1).max(512),
254
+ then: z.string().min(1).max(1024),
255
+ onlyWhile: z.string().max(64).optional()
256
+ });
257
+ ProcessToolDTO = z.object({
258
+ name: z.string().min(1).max(64).regex(/^[a-z][a-z0-9_]*$/, "snake_case"),
259
+ description: z.string().min(1).max(2e3),
260
+ // Shorthand input shape: field name → primitive type. The LLM fills these.
261
+ // Required (the compiler always provides it, possibly {}) to keep the schema's
262
+ // input/output types symmetric — a `.default()` here diverges them.
263
+ input: z.record(z.enum(["string", "number", "boolean"])),
264
+ url: z.string().url(),
265
+ method: z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]),
266
+ headers: z.record(z.string().max(1024)).optional(),
267
+ auth: z.enum(["none", "connection"]),
268
+ connectionRef: z.string().max(128).optional()
269
+ });
270
+ ProcessSchemaDTO = z.object({
271
+ id: z.string().min(1).max(64),
272
+ // System-prompt fragments compiled from say/confirm nodes. The SDK bridge
273
+ // composes these into the agent's prompt.
274
+ prompts: z.array(z.object({ id: z.string().min(1).max(128), text: z.string().max(2e3) })).max(64).optional(),
275
+ fields: z.array(ProcessFieldDTO).max(64),
276
+ completionGate: z.object({
277
+ requiredFields: z.array(z.string()).max(64),
278
+ backendAck: z.object({
279
+ url: z.string().url(),
280
+ timeoutMs: z.number().int().positive()
281
+ }).optional()
282
+ }),
283
+ // Deterministic rails compiled from trigger/handoff/end nodes.
284
+ triggers: z.array(ProcessTriggerDTO).max(64).optional(),
285
+ // Default human destination for `then:'handoff'` rails.
286
+ handoff: z.object({
287
+ fallback: z.string().max(128).optional(),
288
+ routes: z.record(z.string().max(128)).optional()
289
+ }).optional(),
290
+ // Memory config compiled from a memory node (maps to AgentConfig.memory).
291
+ memory: z.object({
292
+ scope: z.enum(["call", "caller", "participant", "account", "platform"]).optional(),
293
+ retentionDays: z.number().int().positive().optional(),
294
+ piiFields: z.array(z.string().max(64)).max(64).optional()
295
+ }).optional(),
296
+ // Tool capabilities compiled from tool nodes (maps to AgentConfig.tools).
297
+ tools: z.array(ProcessToolDTO).max(32).optional(),
298
+ // Graph-preserving compiled program. When present and the flow-runtime graph
299
+ // mode is enabled, the worker executes this node-graph deterministically
300
+ // (the interpreter) instead of the flat goals+rails form above. Embedded here
301
+ // (not a new column) so the existing fetch path delivers it with no migration.
302
+ program: FlowProgram.optional()
303
+ });
304
+ z.object({
305
+ name: AgentName,
306
+ version: AgentVersion,
307
+ environment: AgentEnvironment,
308
+ // Initial status. Defaults to 'online'; SDK sends 'degraded' when any
309
+ // readiness probe failed but registration wasn't required-ready.
310
+ status: z.enum(["online", "degraded"]).optional(),
311
+ statusDetail: z.string().max(512).optional(),
312
+ capabilities: z.array(AgentCapability).max(64).optional(),
313
+ readiness: z.array(ReadinessReport).max(64).optional(),
314
+ metadata: AgentMetadata.optional(),
315
+ processSchema: ProcessSchemaDTO.nullable().optional(),
316
+ // Opt into MCP exposure. When true the API upserts a matching agent_config
317
+ // row keyed by (project_id, name, environment) so MCP / dashboard / REST
318
+ // can list and inspect this agent's identity. When false / omitted, the
319
+ // agent stays invisible to MCP; identity is purely in worker code.
320
+ mcpExposed: z.boolean().optional()
321
+ });
322
+ AgentDTO = z.object({
323
+ id: z.string().uuid(),
324
+ projectId: z.string(),
325
+ name: AgentName,
326
+ version: AgentVersion,
327
+ environment: AgentEnvironment,
328
+ status: AgentStatus,
329
+ statusDetail: z.string().nullable(),
330
+ capabilities: z.array(AgentCapability),
331
+ readiness: z.array(ReadinessReport),
332
+ lastSeenAt: z.string().datetime().nullable(),
333
+ metadata: AgentMetadata,
334
+ processSchema: ProcessSchemaDTO.nullable().optional(),
335
+ createdAt: z.string().datetime(),
336
+ updatedAt: z.string().datetime()
337
+ });
338
+ z.object({
339
+ processSchema: ProcessSchemaDTO.nullable()
340
+ });
341
+ z.object({
342
+ processSchema: ProcessSchemaDTO.nullable()
343
+ });
344
+ z.object({
345
+ agent: AgentDTO,
346
+ // Server-controlled cadence so we can tune without an SDK release.
347
+ heartbeatIntervalMs: z.number().int().positive()
348
+ });
349
+ z.object({
350
+ status: z.enum(["online", "degraded"]).optional(),
351
+ statusDetail: z.string().max(512).optional(),
352
+ metadata: AgentMetadata.optional()
353
+ });
354
+ z.object({
355
+ agent: AgentDTO,
356
+ heartbeatIntervalMs: z.number().int().positive()
357
+ });
358
+ z.object({
359
+ environment: AgentEnvironment.optional()
360
+ });
361
+ z.object({
362
+ agents: z.array(AgentDTO)
363
+ });
364
+ z.object({
365
+ ok: z.literal(true)
366
+ });
367
+ }
368
+ });
369
+ var ProcessLifecycleEvent, ProcessSnapshotPayload;
370
+ var init_process_state = __esm({
371
+ "../contracts/src/process-state.ts"() {
372
+ ProcessLifecycleEvent = z.object({
373
+ id: z.string().min(1),
374
+ at: z.string().datetime(),
375
+ kind: z.string().min(1).max(80),
376
+ source: z.enum(["agent", "caller", "form", "platform"]),
377
+ data: z.record(z.string(), z.unknown())
378
+ });
379
+ ProcessSnapshotPayload = z.object({
380
+ schemaId: z.string(),
381
+ currentNode: z.string(),
382
+ fieldsCaptured: z.record(z.string(), z.unknown()),
383
+ backendAckReceived: z.boolean(),
384
+ backendAckReceivedAt: z.string().datetime().optional(),
385
+ // Optional for back-compat with publishers built before this field
386
+ // existed. The cold-path → SSE pipe is otherwise pass-through.
387
+ lifecycle: z.array(ProcessLifecycleEvent).optional()
388
+ });
389
+ z.object({
390
+ callId: z.string().min(1),
391
+ agentId: z.string().min(1).nullable(),
392
+ projectId: z.string().min(1),
393
+ ts: z.string().datetime(),
394
+ snapshot: ProcessSnapshotPayload
395
+ });
396
+ }
397
+ });
398
+ var GuidanceCommand, ConsultationPolicy;
399
+ var init_consultation = __esm({
400
+ "../contracts/src/consultation.ts"() {
401
+ z.enum([
402
+ "consultation.requested",
403
+ "consultation.resolved"
404
+ ]);
405
+ z.object({
406
+ consultationId: z.string().uuid(),
407
+ // Free-form question. Generic on purpose — no vertical-specific fields.
408
+ question: z.string().min(1).max(2e3),
409
+ // Transcript context the agent felt was relevant when asking.
410
+ transcriptSnippet: z.string().max(4e3).optional(),
411
+ // Hint to the orchestrator about how aggressively to interrupt the human.
412
+ urgency: z.enum(["low", "normal", "high"]).default("normal"),
413
+ // Server-clamped to ConsultationPolicy.maxTimeoutMs and ProjectLimits.
414
+ timeoutMs: z.number().int().min(1e3).max(3e5)
415
+ });
416
+ z.object({
417
+ consultationId: z.string().uuid(),
418
+ // 'timeout' = per-consult cap. 'cumulative_cap' = call-wide ceiling.
419
+ // 'cancelled' = agent decided it no longer needed an answer.
420
+ resolution: z.enum(["guidance", "timeout", "cumulative_cap", "cancelled"]),
421
+ guidance: z.string().max(4e3).optional(),
422
+ // Wall-clock from requested-emit → resolved-emit. SLA tracking.
423
+ latencyMs: z.number().int().min(0)
424
+ });
425
+ GuidanceCommand = z.object({
426
+ kind: z.literal("guidance"),
427
+ consultationId: z.string().uuid(),
428
+ guidance: z.string().min(1).max(4e3),
429
+ // Optional bridge phrase the agent may speak before delivering guidance.
430
+ resumePhrase: z.string().max(500).optional()
431
+ });
432
+ ConsultationPolicy = z.object({
433
+ enabled: z.boolean().default(true),
434
+ // Per-consult default. ProjectLimits.perCallMaxTimeoutMs clamps this.
435
+ defaultTimeoutMs: z.number().int().min(1e3).default(3e4),
436
+ maxTimeoutMs: z.number().int().min(1e3).default(3e4),
437
+ // Cumulative cap across the call. ProjectLimits clamps.
438
+ maxCumulativeHoldMs: z.number().int().min(1e3).default(12e4),
439
+ // Spoken when the agent emits consultation.requested.
440
+ holdPhrase: z.string().min(1).max(500).default("Let me check on that."),
441
+ // Spoken when a single consultation times out — the call continues.
442
+ fallbackPhrase: z.string().min(1).max(500).default("Sorry, I don't have that information right now."),
443
+ // Spoken when the cumulative cap fires — the call ends after this.
444
+ callEndFallback: z.string().min(1).max(500).default("I'm going to need someone to call you back. Thank you."),
445
+ // Optional per-urgency timeout override. Lets ops shorten 'high' urgency
446
+ // holds without lowering the default.
447
+ urgencyOverrides: z.object({
448
+ low: z.number().int().min(1e3).optional(),
449
+ normal: z.number().int().min(1e3).optional(),
450
+ high: z.number().int().min(1e3).optional()
451
+ }).optional()
452
+ });
453
+ }
454
+ });
455
+ var CallEventKind, SayCommand, HangupCommand, DtmfCommand, InstructCommand;
456
+ var init_call_events = __esm({
457
+ "../contracts/src/call-events.ts"() {
458
+ init_consultation();
459
+ CallEventKind = z.enum([
460
+ "status.dialing",
461
+ "status.dispatched",
462
+ "status.ringing",
463
+ "status.answered",
464
+ "status.ended",
465
+ "transcript.partial",
466
+ "transcript.final",
467
+ "tts.spoken",
468
+ "cost.update",
469
+ // Consultation lifecycle — see packages/contracts/src/consultation.ts.
470
+ // Emitted by the agent's built-in `ask_host` tool. The dashboard and MCP
471
+ // host SSE clients filter on these to surface the "let me check" UX.
472
+ "consultation.requested",
473
+ "consultation.resolved",
474
+ // DTMF keypad events. `dtmf.received` fires when a remote participant
475
+ // presses a key on their leg; `dtmf.sent` fires when our agent (or a
476
+ // programmatic caller via /v1/calls/:id/dtmf) emits one outbound.
477
+ "dtmf.received",
478
+ "dtmf.sent"
479
+ ]);
480
+ z.object({
481
+ reason: z.string().optional(),
482
+ durationMs: z.number().int().min(0).optional(),
483
+ endedAt: z.string().datetime().optional()
484
+ });
485
+ z.object({
486
+ speaker: z.enum(["caller", "agent"]),
487
+ text: z.string(),
488
+ startMs: z.number().int().min(0),
489
+ endMs: z.number().int().min(0)
490
+ });
491
+ z.object({
492
+ text: z.string(),
493
+ // The MCP client sets this so it can correlate the spoken event to the
494
+ // call_say invocation that triggered it (used for "did my say actually go
495
+ // through" confirmation in the host chat).
496
+ correlationId: z.string().optional()
497
+ });
498
+ z.object({
499
+ digit: z.string().regex(/^[0-9*#A-D]$/),
500
+ code: z.number().int().min(0).max(15),
501
+ participantId: z.string().optional()
502
+ });
503
+ z.object({
504
+ totalUsd: z.string(),
505
+ breakdown: z.object({
506
+ llmUsd: z.string().optional(),
507
+ ttsUsd: z.string().optional(),
508
+ sttUsd: z.string().optional(),
509
+ telephonyUsd: z.string().optional()
510
+ }).optional()
511
+ });
512
+ z.object({
513
+ // Global bigserial assigned at insert. SSE clients use this as Last-Event-ID.
514
+ seq: z.number().int().min(0),
515
+ callId: z.string().uuid(),
516
+ projectId: z.string().min(1),
517
+ kind: CallEventKind,
518
+ payload: z.unknown(),
519
+ ts: z.string().datetime()
520
+ });
521
+ z.enum(["say", "hangup", "dtmf", "instruct", "guidance"]);
522
+ SayCommand = z.object({
523
+ kind: z.literal("say"),
524
+ text: z.string().min(1),
525
+ // 'interrupt' replaces any in-flight TTS; 'queue' waits for current to end.
526
+ mode: z.enum(["interrupt", "queue"]).default("interrupt"),
527
+ correlationId: z.string().optional()
528
+ });
529
+ HangupCommand = z.object({
530
+ kind: z.literal("hangup"),
531
+ reason: z.string().optional()
532
+ });
533
+ DtmfCommand = z.object({
534
+ kind: z.literal("dtmf"),
535
+ digits: z.string().regex(/^[0-9*#]+$/)
536
+ });
537
+ InstructCommand = z.object({
538
+ kind: z.literal("instruct"),
539
+ text: z.string().min(1)
540
+ });
541
+ z.discriminatedUnion("kind", [
542
+ SayCommand,
543
+ HangupCommand,
544
+ DtmfCommand,
545
+ InstructCommand,
546
+ GuidanceCommand
547
+ ]);
548
+ }
549
+ });
550
+ var VoiceConfig, ModelConfig, SttConfig, RealtimeConfig, PipelineMode, ToolBinding, ProcessFieldType, ProcessFieldSpec, ProcessCompleteWhen, ProcessBackendAckSpec, ProcessSpec, TriggerConditionSpec, TriggerActionSpec, TriggerSpec, RequiredInfoField, AgentConfig;
551
+ var init_agent_config = __esm({
552
+ "../contracts/src/agent-config.ts"() {
553
+ init_consultation();
554
+ VoiceConfig = z.object({
555
+ // Finite, vetted list — platform-level enum.
556
+ provider: z.enum(["deepgram", "elevenlabs", "openai", "cartesia"]),
557
+ voiceId: z.string().min(1).max(128),
558
+ // TTS synthesis model (provider-specific, e.g. 'eleven_turbo_v2_5',
559
+ // 'sonic-2'). Optional — providers like Deepgram fold the voice into the
560
+ // model id (aura-2-harmonia-en) and ignore this.
561
+ model: z.string().min(1).max(128).optional(),
562
+ // Speed knob narrow to what every provider supports — keeps it portable.
563
+ rate: z.number().min(0.5).max(2).optional()
564
+ });
565
+ ModelConfig = z.object({
566
+ provider: z.enum(["openai", "anthropic", "gemini"]),
567
+ model: z.string().min(1).max(128),
568
+ temperature: z.number().min(0).max(2).optional(),
569
+ // Per-call ceiling. Distinct from project-level quota in ProjectLimits.
570
+ maxOutputTokens: z.number().int().min(1).max(32768).optional(),
571
+ // Advanced nucleus-sampling knob. Optional — most builders leave it unset
572
+ // and rely on temperature alone.
573
+ topP: z.number().min(0).max(1).optional()
574
+ });
575
+ SttConfig = z.object({
576
+ provider: z.enum(["deepgram", "cartesia", "assemblyai"]),
577
+ model: z.string().min(1).max(128).optional(),
578
+ // BCP-47; falls back to AgentConfig.language when unset.
579
+ language: z.string().min(2).max(16).optional(),
580
+ // End-of-turn confidence (higher = wait for more certainty before the agent
581
+ // takes its turn). The single biggest perceived-latency lever in voice.
582
+ endOfTurnConfidence: z.number().min(0).max(1).optional(),
583
+ // Hard cap (ms) on how long to wait for end-of-turn before responding.
584
+ endOfTurnTimeoutMs: z.number().int().min(100).max(5e3).optional(),
585
+ // Keyword boost list — bias transcription toward these terms.
586
+ keywords: z.array(z.string().min(1).max(64)).max(100).optional()
587
+ });
588
+ RealtimeConfig = z.object({
589
+ provider: z.enum(["openai", "google"]),
590
+ model: z.string().min(1).max(128),
591
+ // Provider-native voice id ('alloy', 'echo' for OpenAI; 'Aoede', 'Puck' for
592
+ // Gemini Live). Optional — provider default applies when unset.
593
+ voice: z.string().min(1).max(128).optional(),
594
+ temperature: z.number().min(0).max(2).optional()
595
+ });
596
+ PipelineMode = z.enum(["cascade", "realtime"]);
597
+ ToolBinding = z.object({
598
+ toolName: z.string().min(1).max(128),
599
+ // Per-agent override of the tool's default args (e.g. an API base URL).
600
+ // Generic Record on purpose — no vertical-specific shape.
601
+ defaultArgs: z.record(z.unknown()).optional()
602
+ });
603
+ ProcessFieldType = z.enum([
604
+ "string",
605
+ "string[]",
606
+ "number",
607
+ "boolean",
608
+ "datetime",
609
+ "email",
610
+ "phone"
611
+ ]);
612
+ ProcessFieldSpec = z.object({
613
+ type: ProcessFieldType,
614
+ required: z.boolean().optional(),
615
+ // Regex source string (not a RegExp object — JSON-safe). Applied to
616
+ // `string` and `string[]` items by the SDK at validation time.
617
+ pattern: z.string().max(500).optional(),
618
+ min: z.number().optional(),
619
+ max: z.number().optional(),
620
+ enum: z.array(z.string().min(1).max(200)).max(50).optional(),
621
+ // Override the auto-generated capture prompt.
622
+ ask: z.string().max(500).optional(),
623
+ // Override the field-extraction hint shown to the LLM.
624
+ extract: z.string().max(500).optional()
625
+ });
626
+ ProcessCompleteWhen = z.enum([
627
+ "all-required-captured",
628
+ "all-captured"
629
+ ]);
630
+ ProcessBackendAckSpec = z.object({
631
+ url: z.string().url(),
632
+ timeoutMs: z.number().int().min(100).max(6e4).optional(),
633
+ headers: z.record(z.string(), z.string()).optional()
634
+ });
635
+ ProcessSpec = z.object({
636
+ // Defaults to {} so LLM-authored plans can emit `process: { completeWhen }`
637
+ // without naming any structured fields. Downstream agent runtime handles
638
+ // empty maps as "no callee-side capture, only the completeWhen criterion."
639
+ collect: z.record(z.string().min(1).max(128), ProcessFieldSpec).default({}),
640
+ completeWhen: ProcessCompleteWhen.default("all-required-captured"),
641
+ backendAck: ProcessBackendAckSpec.optional()
642
+ });
643
+ TriggerConditionSpec = z.discriminatedUnion("kind", [
644
+ z.object({
645
+ kind: z.literal("regex"),
646
+ pattern: z.string().min(1).max(500),
647
+ flags: z.string().max(8).optional()
648
+ }),
649
+ z.object({
650
+ kind: z.literal("signal"),
651
+ name: z.string().min(1).max(128)
652
+ })
653
+ ]);
654
+ TriggerActionSpec = z.discriminatedUnion("kind", [
655
+ z.object({ kind: z.literal("handoff") }),
656
+ z.object({ kind: z.literal("endCall") }),
657
+ z.object({ kind: z.literal("say"), text: z.string().min(1).max(2e3) })
658
+ ]);
659
+ TriggerSpec = z.object({
660
+ on: TriggerConditionSpec,
661
+ then: TriggerActionSpec,
662
+ // If set, the trigger only fires while the named process field is uncaptured.
663
+ onlyWhile: z.string().min(1).max(128).optional()
664
+ });
665
+ RequiredInfoField = z.object({
666
+ name: z.string().min(1).max(64),
667
+ type: z.enum(["string", "string[]", "number", "boolean", "datetime", "email", "phone"]),
668
+ required: z.boolean(),
669
+ // The field's `ask` prompt — what the value represents / how to source it.
670
+ description: z.string().max(512).optional(),
671
+ // Constrained value set, when the field is an enum.
672
+ enum: z.array(z.string()).max(64).optional()
673
+ });
674
+ AgentConfig = z.object({
675
+ id: z.string().uuid(),
676
+ projectId: z.string().min(1),
677
+ // Human-readable label. No `agentType` enum on purpose.
678
+ name: z.string().min(1).max(128),
679
+ // Soft override of the worker's baked-in prompt. Absent → call_start
680
+ // uses the SDK worker's predefined prompt (the source of truth for
681
+ // pre-defined agents like FNOL). Present → it overrides the worker's
682
+ // prompt at call_start as a snapshot decision; mid-call edits are not
683
+ // honoured. Per-call override at call_start time is also allowed.
684
+ systemPrompt: z.string().max(64e3).optional(),
685
+ // Routing/decision layer (Voiceflow "Instructions"): plain-language guidance the
686
+ // agent reads to decide WHEN to do things (vs systemPrompt = WHO it is). Injected
687
+ // into the system prompt below the persona. Interpolated with {{ }} at runtime.
688
+ routingInstructions: z.string().max(32e3).optional(),
689
+ voice: VoiceConfig,
690
+ model: ModelConfig,
691
+ // STT for the cascade pipeline. Absent → SDK default (Deepgram nova-3).
692
+ stt: SttConfig.optional(),
693
+ // Pipeline mode. Defaults to 'cascade' so pre-existing rows (which have no
694
+ // mode column) behave exactly as before.
695
+ mode: PipelineMode.default("cascade"),
696
+ // Realtime / speech-to-speech model. Used when mode === 'realtime'; ignored
697
+ // otherwise. Optional in the schema so cascade-only agents omit it.
698
+ realtime: RealtimeConfig.optional(),
699
+ // BCP-47 string. Free-form so customers add locales without a release.
700
+ language: z.string().min(2).max(16).default("en-US"),
701
+ tools: z.array(ToolBinding).max(64).default([]),
702
+ consultation: ConsultationPolicy,
703
+ // Structured outcome — the data the agent collects on the call.
704
+ // Optional; absent means the call has no structured outcome.
705
+ process: ProcessSpec.optional(),
706
+ // Deterministic routes that fire BEFORE the LLM. Keyed by stable string
707
+ // ids (e.g. 'cancellation_fee', 'asked_for_card') so call_plan revisions
708
+ // can address a specific trigger.
709
+ triggers: z.record(z.string().min(1).max(128), TriggerSpec).optional(),
710
+ // Free-form labels — no semantic enums.
711
+ tags: z.array(z.string().max(64)).max(32).optional(),
712
+ // Bound brain connector (bring-your-own LLM). When set, the worker drives the
713
+ // LLM slot through the connector instead of a first-party model. Null/absent →
714
+ // first-party pipeline (unchanged default). See docs/specs/voice-brain-connector.
715
+ brainConnectorId: z.string().uuid().nullable().optional(),
716
+ // Provenance: 'sdk' (hand-coded defineAgent worker), 'flow' (canvas deploy),
717
+ // 'playbook' (agent-mode deploy), or 'connector' (brain-connector agent).
718
+ // DERIVED from the linked agents row's deploy tags (flowId / playbookId /
719
+ // connectorId in metadata) — not stored on agent_config, never PATCHed.
720
+ source: z.enum(["sdk", "flow", "playbook", "connector"]).optional(),
721
+ // True when the pipeline MODE is locked to code (genuine SDK agents only):
722
+ // the dashboard renders mode read-only and PATCH rejects a mode change.
723
+ // Derived; read-only. Flow/playbook/connector agents → false.
724
+ modeLocked: z.boolean().optional(),
725
+ // Inputs this agent needs to run a call, projected from its process schema.
726
+ // An MCP host reads these (via list_agents) to pre-fill known values as
727
+ // call_start metadata so the agent never re-asks. Empty when the agent
728
+ // collects nothing structured. DERIVED from the agents row; read-only.
729
+ requiredInfo: z.array(RequiredInfoField).max(64).default([]),
730
+ createdAt: z.string().datetime(),
731
+ // Optimistic-lock token. PATCH must echo this in If-Match.
732
+ updatedAt: z.string().datetime()
733
+ });
734
+ }
735
+ });
736
+ var PlanStatus, PlanCallee, PlanQuestion, PlanEscalation, DraftAgentConfig, CallPlan;
737
+ var init_call_plan = __esm({
738
+ "../contracts/src/call-plan.ts"() {
739
+ init_agent_config();
740
+ PlanStatus = z.enum([
741
+ "drafting",
742
+ "clarifying",
743
+ "ready",
744
+ "committed",
745
+ "abandoned"
746
+ ]);
747
+ PlanCallee = z.object({
748
+ e164: z.string().min(2).optional(),
749
+ // may be unknown at intent time
750
+ displayName: z.string().max(200).optional(),
751
+ // What the host knows about who they're calling. e.g. "their dentist's
752
+ // billing office", "a hiring manager named Priya". Seeds tone/register
753
+ // and likely-triggers; not used to gate behaviour.
754
+ description: z.string().max(2e3).optional()
755
+ });
756
+ PlanQuestion = z.object({
757
+ id: z.string().min(1).max(128),
758
+ // stable across rounds; e.g. 'policy_number'
759
+ prompt: z.string().min(1).max(1e3),
760
+ // 'fact' — the planner needs a piece of data (e.g. account #).
761
+ // 'decision' — the planner needs the host to pick an option/policy.
762
+ // 'auth' — the planner is checking the host has authority to act.
763
+ kind: z.enum(["fact", "decision", "auth"]),
764
+ // For 'decision' questions only.
765
+ options: z.array(z.string().min(1).max(200)).max(20).optional(),
766
+ // True if the agent could ask the callee for this mid-call instead of
767
+ // demanding it from the host now. When true and the host can't answer,
768
+ // the planner promotes this to a draftConfig.process.collect field
769
+ // rather than blocking the call.
770
+ deferrableToCallee: z.boolean().default(false)
771
+ });
772
+ PlanEscalation = z.object({
773
+ // 'consult_host' — ask the MCP host (default for plan-first calls).
774
+ // 'ask_callee' — stay on the line and ask the caller.
775
+ // 'fallback_phrase' — read the agent's configured fallback line.
776
+ onUncertainty: z.enum(["consult_host", "ask_callee", "fallback_phrase"]).default("consult_host"),
777
+ // Hard cap on host consultations for this call. Independent of
778
+ // ConsultationPolicy time caps which still apply.
779
+ maxHostConsults: z.number().int().min(0).max(20).optional(),
780
+ // If host doesn't reply within this window, fall through to `secondary`.
781
+ // Server-clamps to ConsultationPolicy.maxTimeoutMs.
782
+ hostTimeoutMs: z.number().int().min(1e3).max(3e5).optional(),
783
+ secondary: z.enum(["ask_callee", "fallback_phrase"]).default("fallback_phrase")
784
+ });
785
+ DraftAgentConfig = AgentConfig.omit({
786
+ id: true,
787
+ projectId: true,
788
+ createdAt: true,
789
+ updatedAt: true
790
+ }).partial();
791
+ CallPlan = z.object({
792
+ planId: z.string().uuid(),
793
+ projectId: z.string().min(1),
794
+ status: PlanStatus,
795
+ // Why we're calling. One sentence, free-form. Kept verbatim from the
796
+ // host's original IntentRequest so we can replay planning. The refined
797
+ // version lives in draftConfig.systemPrompt.
798
+ objective: z.string().min(1).max(2e3),
799
+ callee: PlanCallee,
800
+ // The AgentConfig the planner is building. Grows across clarify rounds.
801
+ // At status='ready' this is a complete (modulo defaulted fields)
802
+ // AgentConfig override; at status='committed' it's the snapshot the
803
+ // call booted with.
804
+ draftConfig: DraftAgentConfig.default({}),
805
+ // Pre-call host-facing clarifications. When non-empty AND
806
+ // status='clarifying', the host must answer via call_clarify. When
807
+ // non-empty AND status='ready', the remaining questions were either
808
+ // deferred to callee (see unresolvedQuestions) or dropped after
809
+ // exhausting clarificationRounds — see changeSummary on each
810
+ // PlanResponse for what happened.
811
+ openQuestions: z.array(PlanQuestion).max(20).default([]),
812
+ // Snapshot of deferrableToCallee questions that the planner couldn't
813
+ // resolve and instead promoted to draftConfig.process.collect at
814
+ // commit time. Kept for audit + so the host can see what the agent
815
+ // will ask the callee.
816
+ unresolvedQuestions: z.array(PlanQuestion).max(20).default([]),
817
+ escalation: PlanEscalation,
818
+ // Round counter. Server enforces PlanLimits.maxClarificationRounds.
819
+ clarificationRounds: z.number().int().min(0).default(0),
820
+ // Set once `call_commit` succeeds. Links the plan to the call it
821
+ // created. Useful for the dashboard plan inspector.
822
+ committedCallId: z.string().uuid().optional(),
823
+ createdAt: z.string().datetime(),
824
+ updatedAt: z.string().datetime()
825
+ });
826
+ z.object({
827
+ objective: z.string().min(1).max(2e3),
828
+ callee: PlanCallee.optional(),
829
+ // Initial facts the host already has. Folded into draftConfig at the
830
+ // planner's discretion — typically inlined into systemPrompt or stored
831
+ // as process.collect default values.
832
+ knowns: z.record(z.string().min(1).max(128), z.string().max(4e3)).optional(),
833
+ // Hard constraints the planner must respect. e.g.
834
+ // ["never disclose the card CVV", "if asked for SSN, refuse politely"].
835
+ // Generate matching triggers + appear in systemPrompt as do-not-disclose
836
+ // rules.
837
+ constraints: z.array(z.string().min(1).max(500)).max(20).optional(),
838
+ // Per-call escalation overrides — merged with project defaults.
839
+ escalation: PlanEscalation.partial().optional(),
840
+ // Override the agent's default voice/model/language for this call. Most
841
+ // hosts won't set this; here for power users.
842
+ configOverrides: DraftAgentConfig.pick({
843
+ voice: true,
844
+ model: true,
845
+ language: true
846
+ }).optional()
847
+ });
848
+ z.object({
849
+ // Map question.id → answer text. For 'decision' questions the value
850
+ // should be one of `options`, but the server doesn't enforce strictly —
851
+ // the planner will re-ask if the answer is unrecognized.
852
+ answers: z.record(z.string().min(1).max(128), z.string().max(4e3)),
853
+ // The host can also push new facts that weren't asked for.
854
+ additionalKnowns: z.record(z.string().min(1).max(128), z.string().max(4e3)).optional()
855
+ });
856
+ z.object({
857
+ to: z.string().min(2),
858
+ // E.164
859
+ phoneNumberId: z.string().uuid(),
860
+ metadata: z.record(z.string().min(1).max(128), z.string().max(4e3)).optional(),
861
+ force: z.boolean().default(false)
862
+ });
863
+ z.object({
864
+ plan: CallPlan,
865
+ // Convenience: questions to answer next. Same as plan.openQuestions
866
+ // when status='clarifying'; empty otherwise.
867
+ questions: z.array(PlanQuestion),
868
+ // Human-readable summary of what changed in this round. Useful for the
869
+ // host's chat surface ("I added 3 facts and resolved 2 questions").
870
+ changeSummary: z.string().max(2e3).optional()
871
+ });
872
+ z.object({
873
+ callId: z.string().uuid(),
874
+ plan: CallPlan
875
+ // status will be 'committed'
876
+ });
877
+ z.object({
878
+ // After this many round-trips, the planner forces status='ready' and
879
+ // any remaining questions are marked deferrableToCallee or dropped.
880
+ maxClarificationRounds: z.number().int().min(1).default(3),
881
+ // How long an uncommitted plan lives before it's auto-abandoned. Lets
882
+ // the host build a plan in one session and commit it in another.
883
+ planTtlMs: z.number().int().min(6e4).default(36e5)
884
+ // 1h
885
+ });
886
+ }
887
+ });
888
+ var MemoryScope, MemoryRecord, RelevantContext, CallerDTO;
889
+ var init_memory = __esm({
890
+ "../contracts/src/memory.ts"() {
891
+ MemoryScope = z.enum(["call", "caller", "account", "platform"]);
892
+ MemoryRecord = z.record(z.unknown());
893
+ RelevantContext = z.object({
894
+ call: MemoryRecord,
895
+ caller: MemoryRecord,
896
+ account: MemoryRecord,
897
+ platform: MemoryRecord
898
+ });
899
+ z.object({
900
+ callerId: z.string().min(1),
901
+ scopes: z.array(MemoryScope).optional()
902
+ });
903
+ z.object({
904
+ context: RelevantContext
905
+ });
906
+ z.object({
907
+ scope: MemoryScope,
908
+ callerId: z.string().min(1),
909
+ delta: MemoryRecord,
910
+ callId: z.string().optional()
911
+ });
912
+ z.object({
913
+ accepted: z.literal(true)
914
+ });
915
+ z.object({
916
+ callId: z.string().uuid(),
917
+ stage: z.string().min(1).max(64),
918
+ snapshot: MemoryRecord
919
+ });
920
+ z.object({
921
+ id: z.string().uuid()
922
+ });
923
+ z.object({
924
+ externalKey: z.string().min(1).max(256),
925
+ displayName: z.string().max(256).optional(),
926
+ identifiers: z.array(
927
+ z.object({
928
+ type: z.string().min(1).max(64),
929
+ value: z.string().min(1).max(256)
930
+ })
931
+ ).optional()
932
+ });
933
+ CallerDTO = z.object({
934
+ id: z.string().uuid(),
935
+ projectId: z.string(),
936
+ externalKey: z.string(),
937
+ displayName: z.string().nullable(),
938
+ metadata: MemoryRecord,
939
+ createdAt: z.string().datetime(),
940
+ updatedAt: z.string().datetime()
941
+ });
942
+ z.object({
943
+ caller: CallerDTO
944
+ });
945
+ z.object({
946
+ ok: z.literal(true),
947
+ scopesPurged: z.array(MemoryScope)
948
+ });
949
+ }
950
+ });
951
+
952
+ // ../contracts/src/participants.ts
953
+ var init_participants = __esm({
954
+ "../contracts/src/participants.ts"() {
955
+ }
956
+ });
957
+
958
+ // ../contracts/src/forms.ts
959
+ var init_forms = __esm({
960
+ "../contracts/src/forms.ts"() {
961
+ }
962
+ });
963
+ var ProviderKind, CatalogVoice, CatalogModel, CatalogProvider, CostEstimateAssumptions, CostEstimatePerMinute, CostEstimateBilledPerMinute;
964
+ var init_model_catalog = __esm({
965
+ "../contracts/src/model-catalog.ts"() {
966
+ init_agent_config();
967
+ ProviderKind = z.enum(["llm", "stt", "tts", "realtime"]);
968
+ CatalogVoice = z.object({
969
+ id: z.string().min(1).max(128),
970
+ label: z.string().min(1).max(128),
971
+ // Optional sample for an in-picker preview button.
972
+ previewUrl: z.string().url().optional(),
973
+ // e.g. 'female', 'narration', 'en-US'.
974
+ tags: z.array(z.string().max(40)).max(12).optional()
975
+ });
976
+ CatalogModel = z.object({
977
+ id: z.string().min(1).max(128),
978
+ label: z.string().min(1).max(128),
979
+ // Pricing hints (USD) shown next to the option. Only the fields relevant to
980
+ // the provider kind are populated.
981
+ inputPerMillion: z.number().nonnegative().optional(),
982
+ // llm / realtime tokens
983
+ outputPerMillion: z.number().nonnegative().optional(),
984
+ perMillionChars: z.number().nonnegative().optional(),
985
+ // tts
986
+ perMinute: z.number().nonnegative().optional(),
987
+ // stt / realtime session
988
+ // Capability hints.
989
+ contextWindow: z.number().int().positive().optional(),
990
+ notes: z.string().max(200).optional()
991
+ });
992
+ CatalogProvider = z.object({
993
+ id: z.string().min(1).max(64),
994
+ // 'openai' | 'anthropic' | 'deepgram' | ...
995
+ label: z.string().min(1).max(64),
996
+ kind: ProviderKind,
997
+ models: z.array(CatalogModel).default([]),
998
+ // Populated for tts / realtime providers.
999
+ voices: z.array(CatalogVoice).optional(),
1000
+ // Pre-selected when the provider is chosen.
1001
+ defaultModel: z.string().max(128).optional()
1002
+ });
1003
+ z.object({
1004
+ llm: z.array(CatalogProvider).default([]),
1005
+ stt: z.array(CatalogProvider).default([]),
1006
+ tts: z.array(CatalogProvider).default([]),
1007
+ realtime: z.array(CatalogProvider).default([])
1008
+ });
1009
+ CostEstimateAssumptions = z.object({
1010
+ callDurationSeconds: z.number().positive().max(7200).optional(),
1011
+ llmInputTokensPerMin: z.number().nonnegative().max(1e6).optional(),
1012
+ llmOutputTokensPerMin: z.number().nonnegative().max(1e6).optional(),
1013
+ ttsCharsPerMin: z.number().nonnegative().max(1e6).optional(),
1014
+ direction: z.enum(["inbound", "outbound"]).optional(),
1015
+ includeTelephony: z.boolean().optional()
1016
+ });
1017
+ z.object({
1018
+ mode: PipelineMode.default("cascade"),
1019
+ model: ModelConfig.optional(),
1020
+ // LLM (cascade)
1021
+ voice: VoiceConfig.optional(),
1022
+ // TTS (cascade)
1023
+ stt: SttConfig.optional(),
1024
+ // STT (cascade)
1025
+ realtime: RealtimeConfig.optional(),
1026
+ // realtime
1027
+ assumptions: CostEstimateAssumptions.optional()
1028
+ });
1029
+ CostEstimatePerMinute = z.object({
1030
+ llm: z.number().nonnegative(),
1031
+ stt: z.number().nonnegative(),
1032
+ tts: z.number().nonnegative(),
1033
+ realtime: z.number().nonnegative(),
1034
+ telephony: z.number().nonnegative(),
1035
+ total: z.number().nonnegative()
1036
+ });
1037
+ CostEstimateBilledPerMinute = z.object({
1038
+ platformFee: z.number().nonnegative(),
1039
+ providerPassthrough: z.number().nonnegative(),
1040
+ telephony: z.number().nonnegative(),
1041
+ allIn: z.number().nonnegative()
1042
+ });
1043
+ z.object({
1044
+ currency: z.literal("USD"),
1045
+ perMinute: CostEstimatePerMinute,
1046
+ // Present when the server could resolve the project's rate card.
1047
+ billedPerMinute: CostEstimateBilledPerMinute.optional(),
1048
+ // Rough voice-to-voice latency estimate for the chosen pipeline.
1049
+ latencyMs: z.object({
1050
+ estimate: z.number().nonnegative(),
1051
+ realtime: z.boolean()
1052
+ }),
1053
+ // The assumptions actually used (after defaults) — drives the UI footnote.
1054
+ assumptions: CostEstimateAssumptions.required()
1055
+ });
1056
+ }
1057
+ });
1058
+ var EvaluationMetric, EvaluationStatus, EvaluationOption, EvaluationInput;
1059
+ var init_evaluations = __esm({
1060
+ "../contracts/src/evaluations.ts"() {
1061
+ EvaluationMetric = z.enum(["rating", "binary", "options", "text"]);
1062
+ EvaluationStatus = z.enum(["pending", "scored", "failed"]);
1063
+ EvaluationOption = z.object({
1064
+ value: z.string().min(1).max(64),
1065
+ label: z.string().min(1).max(120),
1066
+ description: z.string().max(400).optional()
1067
+ });
1068
+ z.object({
1069
+ id: z.string().uuid(),
1070
+ projectId: z.string(),
1071
+ name: z.string().min(1).max(120),
1072
+ criteria: z.string().max(4e3),
1073
+ metric: EvaluationMetric,
1074
+ model: z.string().min(1).max(128),
1075
+ minValue: z.number().int().nullable(),
1076
+ minLabel: z.string().nullable(),
1077
+ maxValue: z.number().int().nullable(),
1078
+ maxLabel: z.string().nullable(),
1079
+ options: z.array(EvaluationOption),
1080
+ enabled: z.boolean(),
1081
+ isDefault: z.boolean(),
1082
+ createdAt: z.string().datetime(),
1083
+ updatedAt: z.string().datetime()
1084
+ });
1085
+ EvaluationInput = z.object({
1086
+ name: z.string().min(1).max(120),
1087
+ criteria: z.string().max(4e3).default(""),
1088
+ metric: EvaluationMetric.default("rating"),
1089
+ model: z.string().min(1).max(128).default("gpt-4o-mini"),
1090
+ minValue: z.number().int().nullable().optional(),
1091
+ minLabel: z.string().max(200).nullable().optional(),
1092
+ maxValue: z.number().int().nullable().optional(),
1093
+ maxLabel: z.string().max(200).nullable().optional(),
1094
+ options: z.array(EvaluationOption).max(12).default([]),
1095
+ enabled: z.boolean().default(true)
1096
+ });
1097
+ EvaluationInput.partial();
1098
+ z.object({
1099
+ id: z.string().uuid(),
1100
+ callId: z.string().uuid(),
1101
+ projectId: z.string(),
1102
+ evaluationId: z.string().uuid(),
1103
+ name: z.string(),
1104
+ metric: EvaluationMetric,
1105
+ status: EvaluationStatus,
1106
+ valueNumber: z.number().nullable(),
1107
+ valueBool: z.boolean().nullable(),
1108
+ valueText: z.string().nullable(),
1109
+ reasoning: z.string().nullable(),
1110
+ model: z.string().nullable(),
1111
+ costUsd: z.number().nullable(),
1112
+ createdAt: z.string().datetime(),
1113
+ updatedAt: z.string().datetime()
1114
+ });
1115
+ z.object({
1116
+ status: EvaluationStatus,
1117
+ valueNumber: z.number().nullable(),
1118
+ valueBool: z.boolean().nullable(),
1119
+ valueText: z.string().nullable(),
1120
+ reasoning: z.string().nullable(),
1121
+ costUsd: z.number().nullable(),
1122
+ error: z.string().nullable()
1123
+ });
1124
+ z.object({
1125
+ evaluationId: z.string().uuid(),
1126
+ name: z.string(),
1127
+ metric: EvaluationMetric,
1128
+ scored: z.number(),
1129
+ average: z.number().nullable(),
1130
+ // rating
1131
+ positiveRate: z.number().nullable(),
1132
+ // binary (fraction true)
1133
+ distribution: z.array(z.object({ value: z.string(), count: z.number() })),
1134
+ // options/text
1135
+ avgCostUsd: z.number().nullable()
1136
+ });
1137
+ }
1138
+ });
1139
+ var TestCheckType, TestRunStatus, TestCheckStatus, TestSuiteInput, TestRun, TestCheck;
1140
+ var init_tests = __esm({
1141
+ "../contracts/src/tests.ts"() {
1142
+ TestCheckType = z.enum(["response-judge", "routing", "tool-call"]);
1143
+ TestRunStatus = z.enum(["pending", "scored", "failed"]);
1144
+ TestCheckStatus = z.enum(["pass", "fail", "skipped"]);
1145
+ z.object({
1146
+ id: z.string().uuid(),
1147
+ projectId: z.string(),
1148
+ name: z.string().min(1).max(120),
1149
+ description: z.string().max(4e3),
1150
+ criteria: z.string().max(4e3),
1151
+ enabled: z.boolean(),
1152
+ isDefault: z.boolean(),
1153
+ createdAt: z.string().datetime(),
1154
+ updatedAt: z.string().datetime()
1155
+ });
1156
+ TestSuiteInput = z.object({
1157
+ name: z.string().min(1).max(120),
1158
+ description: z.string().max(4e3).default(""),
1159
+ criteria: z.string().max(4e3).default(""),
1160
+ enabled: z.boolean().default(true)
1161
+ });
1162
+ TestSuiteInput.partial();
1163
+ TestRun = z.object({
1164
+ id: z.string().uuid(),
1165
+ projectId: z.string(),
1166
+ testSuiteId: z.string().uuid(),
1167
+ callId: z.string().uuid(),
1168
+ name: z.string(),
1169
+ status: TestRunStatus,
1170
+ reasoning: z.string().nullable(),
1171
+ model: z.string().nullable(),
1172
+ costUsd: z.number().nullable(),
1173
+ createdAt: z.string().datetime(),
1174
+ updatedAt: z.string().datetime()
1175
+ });
1176
+ TestCheck = z.object({
1177
+ id: z.string().uuid(),
1178
+ projectId: z.string(),
1179
+ testRunId: z.string().uuid(),
1180
+ checkType: TestCheckType,
1181
+ turnIndex: z.number().int(),
1182
+ status: TestCheckStatus,
1183
+ input: z.unknown().nullable(),
1184
+ output: z.unknown().nullable(),
1185
+ reasoning: z.string().nullable(),
1186
+ createdAt: z.string().datetime(),
1187
+ updatedAt: z.string().datetime()
1188
+ });
1189
+ TestRun.extend({
1190
+ checks: z.array(TestCheck)
1191
+ });
1192
+ z.object({
1193
+ status: TestRunStatus,
1194
+ reasoning: z.string().nullable(),
1195
+ costUsd: z.number().nullable(),
1196
+ checks: z.array(
1197
+ z.object({
1198
+ checkType: TestCheckType,
1199
+ turnIndex: z.number().int(),
1200
+ status: TestCheckStatus,
1201
+ reasoning: z.string().nullable()
1202
+ })
1203
+ ),
1204
+ error: z.string().nullable()
1205
+ });
1206
+ z.object({
1207
+ testSuiteId: z.string().uuid(),
1208
+ name: z.string(),
1209
+ runs: z.number(),
1210
+ passed: z.number(),
1211
+ failed: z.number(),
1212
+ passRate: z.number().nullable(),
1213
+ avgCostUsd: z.number().nullable()
1214
+ });
1215
+ }
1216
+ });
1217
+ var CallThumb;
1218
+ var init_call_review = __esm({
1219
+ "../contracts/src/call-review.ts"() {
1220
+ CallThumb = z.enum(["up", "down"]);
1221
+ z.object({
1222
+ callId: z.string().uuid(),
1223
+ projectId: z.string(),
1224
+ thumb: CallThumb.nullable(),
1225
+ note: z.string().nullable(),
1226
+ reviewed: z.boolean(),
1227
+ reviewedBy: z.string().nullable(),
1228
+ createdAt: z.string().datetime(),
1229
+ updatedAt: z.string().datetime()
1230
+ });
1231
+ z.object({
1232
+ thumb: CallThumb.nullable().optional(),
1233
+ note: z.string().max(2e3).nullable().optional(),
1234
+ reviewed: z.boolean().optional()
1235
+ });
1236
+ }
1237
+ });
1238
+ var ToolExposure;
1239
+ var init_agent_tools = __esm({
1240
+ "../contracts/src/agent-tools.ts"() {
1241
+ ToolExposure = z.enum(["agent", "mcp"]);
1242
+ z.object({
1243
+ name: z.string().min(1).max(64).regex(/^[a-z][a-z0-9_]*$/, "snake_case"),
1244
+ // The string that routes the LLM/orchestrator to invoke this tool.
1245
+ description: z.string().min(1).max(2e3),
1246
+ // Zod serialized to JSON Schema at registration time. Persisted as
1247
+ // JSON Schema; SDK and dashboard rehydrate to Zod when needed.
1248
+ inputSchema: z.record(z.unknown()),
1249
+ // When present, the runtime validates the handler's return value.
1250
+ outputSchema: z.record(z.unknown()).optional(),
1251
+ exposedVia: z.array(ToolExposure).min(1),
1252
+ // ── Tool/Plugin SDK (PRD P0.2). All optional for back-compat. ──
1253
+ // Latency class (PRD §4.6.3). Runtime defaults to 'in_call' when omitted.
1254
+ mode: z.enum(["in_call", "async_only"]).optional(),
1255
+ // Hard timeout for an in-call tool (ms).
1256
+ timeoutMs: z.number().int().min(50).max(6e4).optional(),
1257
+ // 'connection' resolves a credential from the vault by `connectionRef`.
1258
+ auth: z.enum(["none", "connection"]).optional(),
1259
+ connectionRef: z.string().max(128).optional(),
1260
+ // Retry policy for safe/async verbs.
1261
+ retry: z.object({
1262
+ maxAttempts: z.number().int().min(1).max(5),
1263
+ backoffMs: z.number().int().min(0).max(6e4)
1264
+ }).optional(),
1265
+ // Normalized result kinds the flow/decision layer branches on (result.kind).
1266
+ resultKinds: z.array(z.string().min(1).max(64)).max(24).optional(),
1267
+ // Free-form dashboard metadata (icon, group, etc.). No `category` enum.
1268
+ metadata: z.record(z.unknown()).optional()
1269
+ });
1270
+ }
1271
+ });
1272
+ var TranscriptTurn, ConsultationAudit, ToolCallAudit, McpInteractionAudit;
1273
+ var init_call_result = __esm({
1274
+ "../contracts/src/call-result.ts"() {
1275
+ TranscriptTurn = z.object({
1276
+ speaker: z.enum(["caller", "agent"]),
1277
+ text: z.string(),
1278
+ startMs: z.number().int().min(0),
1279
+ endMs: z.number().int().min(0)
1280
+ });
1281
+ ConsultationAudit = z.object({
1282
+ consultationId: z.string().uuid(),
1283
+ question: z.string(),
1284
+ resolution: z.enum(["guidance", "timeout", "cumulative_cap", "cancelled"]),
1285
+ guidance: z.string().optional(),
1286
+ requestedAt: z.string().datetime(),
1287
+ resolvedAt: z.string().datetime(),
1288
+ latencyMs: z.number().int().min(0)
1289
+ });
1290
+ ToolCallAudit = z.object({
1291
+ toolName: z.string(),
1292
+ // Verbatim args the LLM produced. May contain PII — see open question O2
1293
+ // in docs/architecture/contracts-design.md §5.
1294
+ args: z.record(z.unknown()),
1295
+ result: z.unknown(),
1296
+ startedAt: z.string().datetime(),
1297
+ durationMs: z.number().int().min(0),
1298
+ ok: z.boolean(),
1299
+ errorMessage: z.string().optional()
1300
+ });
1301
+ McpInteractionAudit = z.object({
1302
+ // One of the four runtime tools — closed set, no growth allowed.
1303
+ tool: z.enum(["call_say", "call_send_guidance", "call_inject_context", "call_instruct"]),
1304
+ // Args verbatim — see open question O2.
1305
+ args: z.record(z.unknown()),
1306
+ ts: z.string().datetime(),
1307
+ // Attribution. The API key that drove the orchestrator session.
1308
+ apiKeyId: z.string()
1309
+ });
1310
+ z.object({
1311
+ callId: z.string().uuid(),
1312
+ status: z.enum(["completed", "failed"]),
1313
+ startedAt: z.string().datetime(),
1314
+ endedAt: z.string().datetime(),
1315
+ durationMs: z.number().int().min(0),
1316
+ transcript: z.array(TranscriptTurn),
1317
+ // Whatever the agent's process-schema captured. No prescribed shape.
1318
+ structuredOutputs: z.record(z.unknown()).optional(),
1319
+ recordingUrl: z.string().url().optional(),
1320
+ cost: z.object({
1321
+ totalUsd: z.string(),
1322
+ breakdown: z.record(z.string()).optional()
1323
+ }),
1324
+ consultations: z.array(ConsultationAudit),
1325
+ toolCalls: z.array(ToolCallAudit),
1326
+ mcpInteractions: z.array(McpInteractionAudit)
1327
+ });
1328
+ }
1329
+ });
1330
+ var PhoneNumberStatus, PhoneNumberProvider, PhoneNumberCapabilities, PhoneNumberRecord;
1331
+ var init_numbers = __esm({
1332
+ "../contracts/src/numbers.ts"() {
1333
+ PhoneNumberStatus = z.enum([
1334
+ "pending",
1335
+ "active",
1336
+ "releasing",
1337
+ "released",
1338
+ "failed"
1339
+ ]);
1340
+ PhoneNumberProvider = z.enum(["twilio", "telnyx", "plivo"]);
1341
+ PhoneNumberCapabilities = z.object({
1342
+ voice: z.boolean().optional(),
1343
+ sms: z.boolean().optional(),
1344
+ mms: z.boolean().optional(),
1345
+ fax: z.boolean().optional()
1346
+ });
1347
+ PhoneNumberRecord = z.object({
1348
+ id: z.string().uuid(),
1349
+ projectId: z.string(),
1350
+ e164: z.string().regex(/^\+[1-9]\d{1,14}$/),
1351
+ provider: PhoneNumberProvider,
1352
+ // Carrier-side identifiers — sensitive, stripped from PublicPhoneNumber.
1353
+ providerSid: z.string(),
1354
+ providerTrunkSid: z.string().nullable(),
1355
+ voiceRouteId: z.string().nullable(),
1356
+ voiceRouteTrunkId: z.string().nullable(),
1357
+ agentId: z.string().uuid().nullable(),
1358
+ // Environments pin (draft/live). null = no pin → live (today's behaviour).
1359
+ agentConfigEnv: z.enum(["draft", "live"]).nullable(),
1360
+ status: PhoneNumberStatus,
1361
+ friendlyName: z.string().nullable(),
1362
+ region: z.string().nullable(),
1363
+ capabilities: PhoneNumberCapabilities,
1364
+ providerMetadata: z.record(z.unknown()),
1365
+ purchaseIdempotencyKey: z.string().nullable().optional(),
1366
+ purchasedAt: z.string().datetime().nullable(),
1367
+ releasedAt: z.string().datetime().nullable(),
1368
+ createdByUserId: z.string().nullable(),
1369
+ createdAt: z.string().datetime(),
1370
+ updatedAt: z.string().datetime()
1371
+ });
1372
+ PhoneNumberRecord.omit({
1373
+ providerSid: true,
1374
+ providerTrunkSid: true,
1375
+ providerMetadata: true,
1376
+ purchaseIdempotencyKey: true
1377
+ });
1378
+ z.object({
1379
+ phoneNumberId: z.string().uuid(),
1380
+ // null detaches.
1381
+ agentId: z.string().uuid().nullable()
1382
+ });
1383
+ }
1384
+ });
1385
+ var init_project_limits = __esm({
1386
+ "../contracts/src/project-limits.ts"() {
1387
+ z.object({
1388
+ consultation: z.object({
1389
+ // Ceiling for ConsultationPolicy.maxTimeoutMs. Project default 90_000.
1390
+ perCallMaxTimeoutMs: z.number().int().min(1e3).default(9e4),
1391
+ // Ceiling for ConsultationPolicy.maxCumulativeHoldMs. Default 300_000.
1392
+ perCallMaxCumulativeMs: z.number().int().min(1e3).default(3e5)
1393
+ })
1394
+ });
1395
+ }
1396
+ });
1397
+
1398
+ // ../contracts/src/flow-ports.ts
1399
+ var INCOMPLETE_PORT_ID;
1400
+ var init_flow_ports = __esm({
1401
+ "../contracts/src/flow-ports.ts"() {
1402
+ INCOMPLETE_PORT_ID = "incomplete";
1403
+ }
1404
+ });
1405
+ var FlowIssueSeverity, FlowIssue;
1406
+ var init_flow_validate = __esm({
1407
+ "../contracts/src/flow-validate.ts"() {
1408
+ init_flow_ports();
1409
+ FlowIssueSeverity = z.enum(["error", "warning"]);
1410
+ FlowIssue = z.object({
1411
+ code: z.string().max(64),
1412
+ severity: FlowIssueSeverity,
1413
+ message: z.string().max(500),
1414
+ nodeId: z.string().max(128).optional(),
1415
+ edgeId: z.string().max(128).optional()
1416
+ });
1417
+ z.object({
1418
+ valid: z.boolean(),
1419
+ issues: z.array(FlowIssue).max(256)
1420
+ });
1421
+ }
1422
+ });
1423
+
1424
+ // ../contracts/src/flow-builtins.ts
1425
+ var init_flow_builtins = __esm({
1426
+ "../contracts/src/flow-builtins.ts"() {
1427
+ }
1428
+ });
1429
+ var ComplianceRule, ComplianceDecision, ComplianceConfig;
1430
+ var init_compliance = __esm({
1431
+ "../contracts/src/compliance.ts"() {
1432
+ ComplianceRule = z.enum(["dnc", "consent", "quiet_hours", "frequency_cap"]);
1433
+ ComplianceDecision = z.enum(["allowed", "blocked"]);
1434
+ ComplianceConfig = z.object({
1435
+ // Local calling window for the CALLED party — TCPA default 8am–9pm.
1436
+ quietHoursStartHour: z.number().int().min(0).max(23).default(8),
1437
+ quietHoursEndHour: z.number().int().min(1).max(24).default(21),
1438
+ enforceDnc: z.boolean().default(true),
1439
+ enforceQuietHours: z.boolean().default(true),
1440
+ // Max dial attempts to one number per rolling 24h. 0 = unlimited.
1441
+ maxAttemptsPerDay: z.number().int().min(0).default(0),
1442
+ // Require a recorded consent flag before dialing.
1443
+ requireConsent: z.boolean().default(false),
1444
+ // Fallback IANA tz when the number's region can't be resolved.
1445
+ fallbackTimeZone: z.string().min(1).default("America/New_York")
1446
+ });
1447
+ z.object({
1448
+ phoneE164: z.string().regex(/^\+[1-9]\d{1,14}$/, "must be E.164"),
1449
+ // ISO instant the check is evaluated at (defaults to now server-side).
1450
+ now: z.string().datetime().optional(),
1451
+ // Override the resolved timezone (else derived from the number's area code).
1452
+ timeZone: z.string().optional(),
1453
+ // Recent attempt count in the last 24h (for the frequency cap), if known.
1454
+ recentAttempts: z.number().int().min(0).optional(),
1455
+ // Whether consent is on file (for requireConsent).
1456
+ hasConsent: z.boolean().optional()
1457
+ });
1458
+ z.object({
1459
+ decision: ComplianceDecision,
1460
+ // The first rule that blocked, if any.
1461
+ rule: ComplianceRule.nullable(),
1462
+ reason: z.string().nullable()
1463
+ });
1464
+ z.object({
1465
+ id: z.string().uuid(),
1466
+ projectId: z.string().min(1),
1467
+ phoneE164: z.string(),
1468
+ reason: z.string().nullable(),
1469
+ source: z.string().nullable(),
1470
+ createdAt: z.string().datetime()
1471
+ });
1472
+ z.object({
1473
+ id: z.string().uuid(),
1474
+ projectId: z.string().min(1),
1475
+ phoneE164: z.string(),
1476
+ decision: ComplianceDecision,
1477
+ rule: ComplianceRule.nullable(),
1478
+ reason: z.string().nullable(),
1479
+ checkedAt: z.string().datetime()
1480
+ });
1481
+ z.object({
1482
+ phoneE164: z.string().regex(/^\+[1-9]\d{1,14}$/, "must be E.164"),
1483
+ reason: z.string().max(500).optional(),
1484
+ source: z.string().max(120).optional()
1485
+ });
1486
+ ComplianceConfig.partial();
1487
+ }
1488
+ });
1489
+ var init_call_recording = __esm({
1490
+ "../contracts/src/call-recording.ts"() {
1491
+ z.object({
1492
+ // When false, the call-event sink skips egress entirely — no recording is made.
1493
+ recordingEnabled: z.boolean().default(true),
1494
+ // Optional consent preamble the agent speaks first on every call. null/empty =
1495
+ // no announcement. Capped so it stays a short notice, not a monologue.
1496
+ consentAnnouncement: z.string().max(1e3).nullable().default(null)
1497
+ });
1498
+ z.object({
1499
+ recordingEnabled: z.boolean().optional(),
1500
+ consentAnnouncement: z.string().max(1e3).nullable().optional()
1501
+ });
1502
+ }
1503
+ });
1504
+ var OutboundAmdMode;
1505
+ var init_outbound = __esm({
1506
+ "../contracts/src/outbound.ts"() {
1507
+ OutboundAmdMode = z.enum(["off", "detect", "detectMessageEnd"]);
1508
+ z.object({
1509
+ // Callee in E.164 (e.g. +14155551234). call-router re-validates strictly.
1510
+ to: z.string().regex(/^\+[1-9]\d{6,14}$/, "must be E.164, e.g. +14155551234"),
1511
+ // Caller-id phone number (must belong to the calling project).
1512
+ phoneNumberId: z.string().min(1),
1513
+ // Optional agent override; falls back to the number's bound agent.
1514
+ agentId: z.string().min(1).optional(),
1515
+ // Template/context variables forwarded to the agent as dispatch metadata.
1516
+ variables: z.record(z.string(), z.string()).optional(),
1517
+ // Per-call AMD override; takes precedence over the agent's stored outbound.amd.
1518
+ amd: OutboundAmdMode.optional()
1519
+ });
1520
+ }
1521
+ });
1522
+ var ConnectionAuthType, ConnectionStatus;
1523
+ var init_connection = __esm({
1524
+ "../contracts/src/connection.ts"() {
1525
+ ConnectionAuthType = z.enum(["oauth2", "api_key", "bearer", "basic", "none"]);
1526
+ ConnectionStatus = z.enum(["active", "revoked", "error"]);
1527
+ z.object({
1528
+ v: z.number().int(),
1529
+ iv: z.string(),
1530
+ ct: z.string(),
1531
+ tag: z.string()
1532
+ });
1533
+ z.object({
1534
+ id: z.string().uuid(),
1535
+ projectId: z.string().min(1),
1536
+ name: z.string().min(1).max(128),
1537
+ // Open string — customers name providers freely (e.g. 'google_calendar',
1538
+ // 'salesforce', 'n8n', 'http').
1539
+ provider: z.string().min(1).max(120),
1540
+ authType: ConnectionAuthType,
1541
+ status: ConnectionStatus,
1542
+ // Non-secret metadata (scopes, account label, base URL, …).
1543
+ metadata: z.record(z.unknown()),
1544
+ expiresAt: z.string().datetime().nullable(),
1545
+ createdAt: z.string().datetime(),
1546
+ updatedAt: z.string().datetime()
1547
+ });
1548
+ z.object({
1549
+ name: z.string().trim().min(1).max(128),
1550
+ provider: z.string().trim().min(1).max(120),
1551
+ authType: ConnectionAuthType,
1552
+ // The secret material to encrypt at rest. Never stored or returned in plaintext.
1553
+ secret: z.string().min(1).max(8192),
1554
+ metadata: z.record(z.unknown()).optional(),
1555
+ expiresAt: z.string().datetime().optional()
1556
+ });
1557
+ z.object({
1558
+ name: z.string().trim().min(1).max(128).optional(),
1559
+ // New secret material — re-encrypted at rest; the old envelope is replaced.
1560
+ secret: z.string().min(1).max(8192).optional(),
1561
+ // Merged over the existing metadata (top-level keys win).
1562
+ metadata: z.record(z.unknown()).optional()
1563
+ }).refine((v) => v.name !== void 0 || v.secret !== void 0 || v.metadata !== void 0, {
1564
+ message: "nothing to update"
1565
+ });
1566
+ }
1567
+ });
1568
+ var ConnectorMode, ConnectorStatus, ConnectorNormalize;
1569
+ var init_connector = __esm({
1570
+ "../contracts/src/connector.ts"() {
1571
+ ConnectorMode = z.enum(["tunnel", "direct"]);
1572
+ ConnectorStatus = z.enum(["online", "degraded", "offline"]);
1573
+ ConnectorNormalize = z.enum(["auto", "on", "off"]);
1574
+ z.object({
1575
+ id: z.string().uuid(),
1576
+ projectId: z.string().min(1),
1577
+ name: z.string().min(1).max(128),
1578
+ mode: ConnectorMode,
1579
+ // Tunnel mode: a short non-secret preview of the daemon token. Null for direct.
1580
+ tokenPreview: z.string().nullable(),
1581
+ // Direct mode: the connections-vault row holding the URL + key. Null for tunnel.
1582
+ connectionId: z.string().uuid().nullable(),
1583
+ // Upstream model id / alias. Null → the brain's own default.
1584
+ defaultModel: z.string().nullable(),
1585
+ normalize: ConnectorNormalize,
1586
+ enabled: z.boolean(),
1587
+ status: ConnectorStatus,
1588
+ lastSeenAt: z.string().datetime().nullable(),
1589
+ metadata: z.record(z.unknown()),
1590
+ createdAt: z.string().datetime(),
1591
+ updatedAt: z.string().datetime()
1592
+ });
1593
+ z.object({
1594
+ name: z.string().trim().min(1).max(128),
1595
+ mode: ConnectorMode,
1596
+ // Required when mode === 'direct': the vault connection holding URL + key.
1597
+ connectionId: z.string().uuid().optional(),
1598
+ defaultModel: z.string().trim().min(1).max(200).optional(),
1599
+ normalize: ConnectorNormalize.optional(),
1600
+ metadata: z.record(z.unknown()).optional()
1601
+ });
1602
+ z.object({
1603
+ name: z.string().trim().min(1).max(128).optional(),
1604
+ connectionId: z.string().uuid().nullable().optional(),
1605
+ defaultModel: z.string().trim().min(1).max(200).nullable().optional(),
1606
+ normalize: ConnectorNormalize.optional(),
1607
+ enabled: z.boolean().optional()
1608
+ });
1609
+ }
1610
+ });
1611
+ var BrainWireMessage, HelloFrame, PongFrame, RegisteredFrame, PingFrame, ShutdownFrame, BrainRequestFrame, BrainCancelFrame, BrainDeltaFrame, BrainDoneFrame, BrainErrorFrame;
1612
+ var init_connector_stream = __esm({
1613
+ "../contracts/src/connector-stream.ts"() {
1614
+ BrainWireMessage = z.object({
1615
+ role: z.enum(["system", "user", "assistant"]),
1616
+ content: z.string()
1617
+ });
1618
+ HelloFrame = z.object({
1619
+ kind: z.literal("hello"),
1620
+ token: z.string().min(1),
1621
+ replicaId: z.string().min(1),
1622
+ version: z.string().min(1),
1623
+ caps: z.object({ streaming: z.boolean(), tools: z.boolean(), litellm: z.boolean() })
1624
+ });
1625
+ PongFrame = z.object({ kind: z.literal("pong"), ts: z.number().int() });
1626
+ RegisteredFrame = z.object({
1627
+ kind: z.literal("registered"),
1628
+ connectorId: z.string().uuid(),
1629
+ projectId: z.string().min(1)
1630
+ });
1631
+ PingFrame = z.object({ kind: z.literal("ping"), ts: z.number().int() });
1632
+ ShutdownFrame = z.object({ kind: z.literal("shutdown"), reason: z.string().optional() });
1633
+ BrainRequestFrame = z.object({
1634
+ kind: z.literal("brain.request"),
1635
+ streamId: z.string().min(1),
1636
+ model: z.string().optional(),
1637
+ temperature: z.number().optional(),
1638
+ messages: z.array(BrainWireMessage)
1639
+ });
1640
+ BrainCancelFrame = z.object({
1641
+ kind: z.literal("brain.cancel"),
1642
+ streamId: z.string().min(1)
1643
+ });
1644
+ BrainDeltaFrame = z.object({
1645
+ kind: z.literal("brain.delta"),
1646
+ streamId: z.string().min(1),
1647
+ content: z.string()
1648
+ });
1649
+ BrainDoneFrame = z.object({
1650
+ kind: z.literal("brain.done"),
1651
+ streamId: z.string().min(1),
1652
+ finishReason: z.string()
1653
+ });
1654
+ BrainErrorFrame = z.object({
1655
+ kind: z.literal("brain.error"),
1656
+ streamId: z.string().min(1),
1657
+ code: z.enum([
1658
+ "upstream_timeout",
1659
+ "upstream_error",
1660
+ "bad_response",
1661
+ "normalize_failed",
1662
+ "unreachable"
1663
+ ]),
1664
+ message: z.string()
1665
+ });
1666
+ z.discriminatedUnion("kind", [
1667
+ HelloFrame,
1668
+ PongFrame,
1669
+ BrainDeltaFrame,
1670
+ BrainDoneFrame,
1671
+ BrainErrorFrame
1672
+ ]);
1673
+ z.discriminatedUnion("kind", [
1674
+ RegisteredFrame,
1675
+ PingFrame,
1676
+ ShutdownFrame,
1677
+ BrainRequestFrame,
1678
+ BrainCancelFrame
1679
+ ]);
1680
+ }
1681
+ });
1682
+
1683
+ // ../contracts/src/flow-enrichment.ts
1684
+ var init_flow_enrichment = __esm({
1685
+ "../contracts/src/flow-enrichment.ts"() {
1686
+ }
1687
+ });
1688
+
1689
+ // ../contracts/src/flow-compile.ts
1690
+ var init_flow_compile = __esm({
1691
+ "../contracts/src/flow-compile.ts"() {
1692
+ init_flow_enrichment();
1693
+ }
1694
+ });
1695
+ var EnvironmentSpec;
1696
+ var init_environments = __esm({
1697
+ "../contracts/src/environments.ts"() {
1698
+ EnvironmentSpec = z.enum(["draft", "live"]);
1699
+ z.object({
1700
+ agentName: z.string().min(1),
1701
+ draftPercent: z.number().int().min(0).max(100),
1702
+ livePercent: z.number().int().min(0).max(100)
1703
+ });
1704
+ z.object({
1705
+ draftPercent: z.number().int().min(0).max(100),
1706
+ livePercent: z.number().int().min(0).max(100).optional()
1707
+ }).refine(
1708
+ (v) => v.livePercent === void 0 || v.draftPercent + v.livePercent === 100,
1709
+ { message: "draftPercent + livePercent must equal 100" }
1710
+ );
1711
+ z.object({
1712
+ env: EnvironmentSpec.nullable()
1713
+ });
1714
+ }
1715
+ });
1716
+ var VariableType, VariableScope, VariableName;
1717
+ var init_variables = __esm({
1718
+ "../contracts/src/variables.ts"() {
1719
+ VariableType = z.enum(["string", "number", "boolean", "json"]);
1720
+ VariableScope = z.enum(["project", "user"]);
1721
+ VariableName = z.string().min(1).max(64).regex(
1722
+ /^[A-Za-z_][A-Za-z0-9_]*$/,
1723
+ "must be a valid identifier (letters, digits, underscore; no leading digit)"
1724
+ );
1725
+ z.object({
1726
+ id: z.string().uuid(),
1727
+ projectId: z.string(),
1728
+ name: VariableName,
1729
+ type: VariableType,
1730
+ // Stored as text; interpreted per `type`. null = no default (renders '').
1731
+ defaultValue: z.string().nullable(),
1732
+ description: z.string().max(280).nullable(),
1733
+ scope: VariableScope,
1734
+ createdAt: z.string().datetime(),
1735
+ updatedAt: z.string().datetime()
1736
+ });
1737
+ z.object({
1738
+ name: VariableName,
1739
+ type: VariableType.default("string"),
1740
+ defaultValue: z.string().nullable().optional(),
1741
+ description: z.string().max(280).nullable().optional(),
1742
+ scope: VariableScope.default("project")
1743
+ });
1744
+ z.object({
1745
+ type: VariableType.optional(),
1746
+ defaultValue: z.string().nullable().optional(),
1747
+ description: z.string().max(280).nullable().optional(),
1748
+ scope: VariableScope.optional()
1749
+ }).refine((b) => Object.keys(b).length > 0, { message: "no fields to update" });
1750
+ }
1751
+ });
1752
+ var PlaybookExitCondition;
1753
+ var init_playbooks = __esm({
1754
+ "../contracts/src/playbooks.ts"() {
1755
+ PlaybookExitCondition = z.object({
1756
+ name: z.string().min(1).max(64),
1757
+ description: z.string().max(2e3),
1758
+ requiredVariables: z.array(z.string().min(1).max(64)).max(32)
1759
+ });
1760
+ z.object({
1761
+ id: z.string(),
1762
+ projectId: z.string(),
1763
+ name: z.string().min(1).max(120),
1764
+ // Routing signal — "what it does AND when to use it" (read by the agent router).
1765
+ llmDescription: z.string().max(2e3),
1766
+ instructions: z.string().max(2e4),
1767
+ // Model overrides (null = inherit the agent's). Exactly 3 knobs, no top-p/penalties.
1768
+ model: z.string().max(120).nullable(),
1769
+ temperature: z.number().min(0).max(2).nullable(),
1770
+ maxTokens: z.number().int().positive().max(32e3).nullable(),
1771
+ // Registry HTTP tools (by name) this playbook may call. With tools the
1772
+ // playbook runs as an agent loop (LLM-with-tools + structured exit).
1773
+ tools: z.array(z.string().min(1).max(128)).max(64).default([]),
1774
+ exitConditions: z.array(PlaybookExitCondition).max(24),
1775
+ createdAt: z.string(),
1776
+ updatedAt: z.string()
1777
+ });
1778
+ z.object({
1779
+ name: z.string().min(1).max(120),
1780
+ llmDescription: z.string().max(2e3).default(""),
1781
+ instructions: z.string().max(2e4).default(""),
1782
+ model: z.string().max(120).nullable().optional(),
1783
+ temperature: z.number().min(0).max(2).nullable().optional(),
1784
+ maxTokens: z.number().int().positive().max(32e3).nullable().optional(),
1785
+ tools: z.array(z.string().min(1).max(128)).max(64).default([]),
1786
+ exitConditions: z.array(PlaybookExitCondition).max(24).default([])
1787
+ });
1788
+ z.object({
1789
+ name: z.string().min(1).max(120),
1790
+ llmDescription: z.string().max(2e3),
1791
+ instructions: z.string().max(2e4),
1792
+ model: z.string().max(120).nullable(),
1793
+ temperature: z.number().min(0).max(2).nullable(),
1794
+ maxTokens: z.number().int().positive().max(32e3).nullable(),
1795
+ tools: z.array(z.string().min(1).max(128)).max(64),
1796
+ exitConditions: z.array(PlaybookExitCondition).max(24)
1797
+ }).partial().refine((b) => Object.keys(b).length > 0, { message: "at least one field required" });
1798
+ }
1799
+ });
1800
+ var PlaybookSnapshot;
1801
+ var init_playbook_versions = __esm({
1802
+ "../contracts/src/playbook-versions.ts"() {
1803
+ init_playbooks();
1804
+ PlaybookSnapshot = z.object({
1805
+ name: z.string().min(1).max(120),
1806
+ llmDescription: z.string().max(2e3),
1807
+ instructions: z.string().max(2e4),
1808
+ model: z.string().max(120).nullable(),
1809
+ temperature: z.number().min(0).max(2).nullable(),
1810
+ maxTokens: z.number().int().positive().max(32e3).nullable(),
1811
+ // Registry tool names frozen with the snapshot (default keeps old rows valid).
1812
+ tools: z.array(z.string().min(1).max(128)).max(64).default([]),
1813
+ exitConditions: z.array(PlaybookExitCondition).max(24)
1814
+ });
1815
+ z.object({
1816
+ id: z.string(),
1817
+ playbookId: z.string(),
1818
+ projectId: z.string(),
1819
+ version: z.number().int().min(1),
1820
+ // Frozen playbook definition at publish time.
1821
+ definition: PlaybookSnapshot,
1822
+ note: z.string().max(1e3).nullable(),
1823
+ publishedByUserId: z.string().nullable(),
1824
+ publishedAt: z.string()
1825
+ });
1826
+ z.object({
1827
+ note: z.string().max(1e3).optional()
1828
+ });
1829
+ }
1830
+ });
1831
+
1832
+ // ../contracts/src/playbook-compile.ts
1833
+ var init_playbook_compile = __esm({
1834
+ "../contracts/src/playbook-compile.ts"() {
1835
+ }
1836
+ });
1837
+
1838
+ // ../contracts/src/index.ts
1839
+ var init_src = __esm({
1840
+ "../contracts/src/index.ts"() {
1841
+ init_primitive();
1842
+ init_events();
1843
+ init_context();
1844
+ init_errors();
1845
+ init_ids();
1846
+ init_agents();
1847
+ init_process_state();
1848
+ init_call_events();
1849
+ init_call_plan();
1850
+ init_memory();
1851
+ init_participants();
1852
+ init_forms();
1853
+ init_consultation();
1854
+ init_agent_config();
1855
+ init_model_catalog();
1856
+ init_evaluations();
1857
+ init_tests();
1858
+ init_call_review();
1859
+ init_agent_tools();
1860
+ init_call_result();
1861
+ init_numbers();
1862
+ init_project_limits();
1863
+ init_flow();
1864
+ init_flow_program();
1865
+ init_flow_ports();
1866
+ init_flow_validate();
1867
+ init_flow_builtins();
1868
+ init_compliance();
1869
+ init_call_recording();
1870
+ init_outbound();
1871
+ init_connection();
1872
+ init_connector();
1873
+ init_connector_stream();
1874
+ init_flow_compile();
1875
+ init_flow_enrichment();
1876
+ init_environments();
1877
+ init_variables();
1878
+ init_playbooks();
1879
+ init_playbook_versions();
1880
+ init_playbook_compile();
1881
+ }
1882
+ });
1883
+
1884
+ // src/runtime/graph/interpolate.ts
1885
+ function interpolate(template, scope) {
1886
+ if (!template || template.indexOf("{{") === -1) return template;
1887
+ return template.replace(TOKEN, (_match, expr) => {
1888
+ const value = resolvePath(scope, expr.trim());
1889
+ return value === void 0 || value === null ? "" : String(value);
1890
+ });
1891
+ }
1892
+ function resolvePath(scope, path) {
1893
+ const parts = path.split(".").map((p) => p.trim()).filter((p) => p.length > 0);
1894
+ let current = scope;
1895
+ for (const part of parts) {
1896
+ if (current === null || typeof current !== "object") return void 0;
1897
+ const obj = current;
1898
+ if (Object.prototype.hasOwnProperty.call(obj, part)) {
1899
+ current = obj[part];
1900
+ continue;
1901
+ }
1902
+ const tolerant = Object.keys(obj).find((k) => k.trim() === part);
1903
+ if (tolerant === void 0) return void 0;
1904
+ current = obj[tolerant];
1905
+ }
1906
+ return current;
1907
+ }
1908
+ function hasSlot(slots, name) {
1909
+ const v = resolvePath(slots, name.trim());
1910
+ return v !== void 0 && v !== null && v !== "";
1911
+ }
1912
+ var TOKEN;
1913
+ var init_interpolate = __esm({
1914
+ "src/runtime/graph/interpolate.ts"() {
1915
+ TOKEN = /\{\{\s*([^}]+?)\s*\}\}/g;
1916
+ }
1917
+ });
1918
+
1919
+ // src/runtime/graph/builtins.ts
1920
+ function buildBuiltinScope(deps) {
1921
+ const call = deps.ctx.call;
1922
+ const nowIso = (/* @__PURE__ */ new Date()).toISOString();
1923
+ const today = nowIso.slice(0, 10);
1924
+ const callerId = typeof call?.callerId === "string" ? call.callerId : "";
1925
+ const agentName = deps.ctx.metadata?.["agentName"];
1926
+ return {
1927
+ caller_id: callerId,
1928
+ from: callerId,
1929
+ to: typeof call?.to === "string" ? call.to : "",
1930
+ call_id: typeof call?.callId === "string" ? call.callId : "",
1931
+ agent_name: typeof agentName === "string" ? agentName : "",
1932
+ channel: "voice",
1933
+ now: nowIso,
1934
+ today,
1935
+ vf_now: nowIso,
1936
+ vf_today: today
1937
+ };
1938
+ }
1939
+ var init_builtins = __esm({
1940
+ "src/runtime/graph/builtins.ts"() {
1941
+ init_src();
1942
+ }
1943
+ });
1944
+ function defaultOpenAIOptions() {
1945
+ if (!credential) return {};
1946
+ return {
1947
+ apiKey: credential.apiKey,
1948
+ ...credential.baseURL ? { baseURL: credential.baseURL } : {}
1949
+ };
1950
+ }
1951
+ function defaultOpenAI() {
1952
+ return cached ??= new OpenAI(defaultOpenAIOptions());
1953
+ }
1954
+ function hasOpenAiKey() {
1955
+ return credential !== null || Boolean(process.env["OPENAI_API_KEY"]);
1956
+ }
1957
+ var credential, cached;
1958
+ var init_openai_default = __esm({
1959
+ "src/runtime/openai-default.ts"() {
1960
+ credential = null;
1961
+ cached = null;
1962
+ }
1963
+ });
1964
+
1965
+ // src/runtime/graph/embedder.ts
1966
+ function dotProduct(a, b) {
1967
+ const n = Math.min(a.length, b.length);
1968
+ let sum = 0;
1969
+ for (let i = 0; i < n; i++) sum += a[i] * b[i];
1970
+ return sum;
1971
+ }
1972
+ function createOpenAIEmbedder() {
1973
+ const cache = /* @__PURE__ */ new Map();
1974
+ const available = () => hasOpenAiKey();
1975
+ function remember(key, vec) {
1976
+ if (cache.size >= CACHE_MAX) {
1977
+ const oldest = cache.keys().next().value;
1978
+ if (oldest !== void 0) cache.delete(oldest);
1979
+ }
1980
+ cache.set(key, vec);
1981
+ return vec;
1982
+ }
1983
+ return {
1984
+ get available() {
1985
+ return available();
1986
+ },
1987
+ async embed(text) {
1988
+ const key = text.trim();
1989
+ if (!available() || !key) return null;
1990
+ const hit = cache.get(key);
1991
+ if (hit) return hit;
1992
+ const controller = new AbortController();
1993
+ const timer = setTimeout(() => controller.abort(), EMBED_TIMEOUT_MS);
1994
+ try {
1995
+ const res = await defaultOpenAI().embeddings.create(
1996
+ { model: EMBED_MODEL, input: key },
1997
+ { signal: controller.signal }
1998
+ );
1999
+ const vec = res.data[0]?.embedding;
2000
+ if (!Array.isArray(vec)) return null;
2001
+ return remember(key, vec);
2002
+ } catch (err) {
2003
+ console.warn("[graph] embed failed; router will fall back to judge", {
2004
+ err: err instanceof Error ? err.message : String(err)
2005
+ });
2006
+ return null;
2007
+ } finally {
2008
+ clearTimeout(timer);
2009
+ }
2010
+ },
2011
+ prewarm(text) {
2012
+ void this.embed(text).catch(() => {
2013
+ });
2014
+ }
2015
+ };
2016
+ }
2017
+ function getEmbedder() {
2018
+ if (override) return override;
2019
+ singleton ??= createOpenAIEmbedder();
2020
+ return singleton;
2021
+ }
2022
+ var EMBED_MODEL, ROUTE_THRESHOLD, EMBED_TIMEOUT_MS, CACHE_MAX, singleton, override;
2023
+ var init_embedder = __esm({
2024
+ "src/runtime/graph/embedder.ts"() {
2025
+ init_openai_default();
2026
+ EMBED_MODEL = process.env["VOICELAYER_EMBED_MODEL"] ?? "text-embedding-3-small";
2027
+ ROUTE_THRESHOLD = Number(process.env["VL_FLOW_ROUTE_THRESHOLD"] ?? "0.62");
2028
+ EMBED_TIMEOUT_MS = Number(process.env["VL_FLOW_EMBED_TIMEOUT_MS"] ?? "2500");
2029
+ CACHE_MAX = 2e3;
2030
+ singleton = null;
2031
+ override = null;
2032
+ }
2033
+ });
2034
+
2035
+ // src/runtime/graph/conditions.ts
2036
+ async function selectEdge(edges, ctx) {
2037
+ if (edges.length === 0) return null;
2038
+ const conditional = edges.filter((e) => e.condition && e.condition.trim());
2039
+ const fallthrough = edges.find((e) => (!e.condition || !e.condition.trim()) && !e.sourceHandle);
2040
+ const nl = [];
2041
+ for (const edge of conditional) {
2042
+ const det = evaluateDeterministicCondition(edge.condition, ctx);
2043
+ if (det === true) return edge.to;
2044
+ if (det === void 0) nl.push(edge);
2045
+ }
2046
+ if (nl.length === 0) return fallthrough ? fallthrough.to : null;
2047
+ const routed = await routeByEmbedding(nl, ctx);
2048
+ if (routed) return routed;
2049
+ for (const edge of nl) {
2050
+ if (await evaluateCondition(edge.condition, ctx)) return edge.to;
2051
+ }
2052
+ return fallthrough ? fallthrough.to : null;
2053
+ }
2054
+ function evaluateDeterministicCondition(condition, ctx) {
2055
+ const text = condition.trim();
2056
+ if (!text) return true;
2057
+ let anyOrTrue = false;
2058
+ for (const orPart of splitTop(text, "||")) {
2059
+ let allOk = true;
2060
+ for (const atom of splitTop(orPart, "&&")) {
2061
+ const det = evaluateDeterministic(atom.trim(), ctx);
2062
+ if (det === void 0) return void 0;
2063
+ if (!det) allOk = false;
2064
+ }
2065
+ if (allOk) anyOrTrue = true;
2066
+ }
2067
+ return anyOrTrue;
2068
+ }
2069
+ function isNaturalLanguageCondition(condition) {
2070
+ return !!condition.trim() && evaluateDeterministicCondition(condition, { slots: {}, tools: {} }) === void 0;
2071
+ }
2072
+ async function routeByEmbedding(nl, ctx) {
2073
+ const utterance = ctx.lastUtterance?.trim();
2074
+ if (!utterance) return null;
2075
+ const embedder = getEmbedder();
2076
+ if (!embedder.available) return null;
2077
+ const uvec = await embedder.embed(utterance);
2078
+ if (!uvec) return null;
2079
+ let best = null;
2080
+ for (const edge of nl) {
2081
+ const cvec = await embedder.embed(edge.condition);
2082
+ if (!cvec) continue;
2083
+ const score = dotProduct(uvec, cvec);
2084
+ if (!best || score > best.score) best = { to: edge.to, score };
2085
+ }
2086
+ if (best && best.score >= ROUTE_THRESHOLD) {
2087
+ console.log("[graph] route: embedding", { to: best.to, score: Number(best.score.toFixed(3)) });
2088
+ return best.to;
2089
+ }
2090
+ return null;
2091
+ }
2092
+ async function evaluateCondition(condition, ctx) {
2093
+ const text = condition.trim();
2094
+ if (!text) return true;
2095
+ for (const orPart of splitTop(text, "||")) {
2096
+ const ands = splitTop(orPart, "&&");
2097
+ let allOk = true;
2098
+ for (const atom of ands) {
2099
+ const det = evaluateDeterministic(atom.trim(), ctx);
2100
+ const ok = det === void 0 ? await judge(atom.trim(), ctx) : det;
2101
+ if (!ok) {
2102
+ allOk = false;
2103
+ break;
2104
+ }
2105
+ }
2106
+ if (allOk) return true;
2107
+ }
2108
+ return false;
2109
+ }
2110
+ function splitTop(text, op) {
2111
+ return text.split(op);
2112
+ }
2113
+ function evaluateDeterministic(atom, ctx) {
2114
+ const lower = atom.toLowerCase();
2115
+ if (lower === "true" || lower === "always") return true;
2116
+ if (lower === "false" || lower === "never") return false;
2117
+ const toolMatch = atom.match(/^tool[.:]([A-Za-z0-9_-]+)(?:\.([\w.]+))?\s*(==|!=)?\s*(.*)$/);
2118
+ if (toolMatch) {
2119
+ const [, id, path, op, rawVal] = toolMatch;
2120
+ const result = ctx.tools[id];
2121
+ if (result === void 0) return false;
2122
+ if (!path || path === "ok") {
2123
+ const ok = result.ok === true || result.kind === "ok";
2124
+ return op === "!=" ? !ok : ok;
2125
+ }
2126
+ const actual = resolvePath(result, path);
2127
+ if (!op) return actual !== void 0 && actual !== null && actual !== "";
2128
+ const eq = String(actual ?? "").toLowerCase() === (rawVal ?? "").trim().toLowerCase();
2129
+ return op === "!=" ? !eq : eq;
2130
+ }
2131
+ const nodeMatch = atom.match(/^node:([A-Za-z0-9_-]+)(?:\.([\w.]+))?\s*(==|!=)?\s*(.*)$/);
2132
+ if (nodeMatch) {
2133
+ if (!ctx.nodeResults) return void 0;
2134
+ const [, id, path, op, rawVal] = nodeMatch;
2135
+ const result = ctx.nodeResults[id];
2136
+ if (result === void 0) return void 0;
2137
+ if (!path) return result !== null && result !== "" && result !== false;
2138
+ const actual = resolvePath(result, path);
2139
+ if (!op) return actual !== void 0 && actual !== null && actual !== "";
2140
+ const eq = String(actual ?? "").toLowerCase() === (rawVal ?? "").trim().toLowerCase();
2141
+ return op === "!=" ? !eq : eq;
2142
+ }
2143
+ const dtmfMatch = atom.match(/^dtmf:\s*(.+)$/i);
2144
+ if (dtmfMatch) {
2145
+ if (ctx.lastDtmf === void 0) return void 0;
2146
+ return ctx.lastDtmf === dtmfMatch[1].trim();
2147
+ }
2148
+ const opMatch = atom.match(
2149
+ /^(!?)\s*(?:slot:)?(.+?)\s*(==|!=|=|contains|starts_with|greater_than|less_than|>=|<=|>|<)\s*(.+)$/i
2150
+ );
2151
+ if (opMatch) {
2152
+ const [, neg, nameRaw, op, valRaw] = opMatch;
2153
+ const raw = resolvePath(ctx.slots, nameRaw.trim());
2154
+ const value = String(raw ?? "").toLowerCase();
2155
+ const target = valRaw.trim().toLowerCase();
2156
+ const opKey = op.toLowerCase();
2157
+ let res;
2158
+ if (opKey === "contains") res = value.includes(target);
2159
+ else if (opKey === "starts_with") res = value.startsWith(target);
2160
+ else if (opKey === "greater_than" || opKey === ">" || opKey === ">=" || opKey === "less_than" || opKey === "<" || opKey === "<=") {
2161
+ const a = Number(String(raw ?? "").replace(/[^0-9.+-]/g, ""));
2162
+ const b = Number(valRaw.trim().replace(/[^0-9.+-]/g, ""));
2163
+ if (Number.isNaN(a) || Number.isNaN(b)) res = false;
2164
+ else if (opKey === "greater_than" || opKey === ">") res = a > b;
2165
+ else if (opKey === ">=") res = a >= b;
2166
+ else if (opKey === "less_than" || opKey === "<") res = a < b;
2167
+ else res = a <= b;
2168
+ } else if (opKey === "!=") res = value !== target;
2169
+ else res = value === target;
2170
+ return neg ? !res : res;
2171
+ }
2172
+ const slotPres = atom.match(/^(!?)\s*slot:(.+?)\s*(present|empty)?$/i);
2173
+ if (slotPres) {
2174
+ const [, neg, nameRaw, kw] = slotPres;
2175
+ const present = hasSlot(ctx.slots, nameRaw.trim());
2176
+ const res = (kw ?? "").toLowerCase() === "empty" ? !present : present;
2177
+ return neg ? !res : res;
2178
+ }
2179
+ const bare = atom.match(/^(!?)\s*([\w.-]+)\s*(present|empty)?$/i);
2180
+ if (bare) {
2181
+ const [, neg, name, kw] = bare;
2182
+ const present = hasSlot(ctx.slots, name);
2183
+ const res = (kw ?? "").toLowerCase() === "empty" ? !present : present;
2184
+ return neg ? !res : res;
2185
+ }
2186
+ return void 0;
2187
+ }
2188
+ async function pickOption(options, ctx, hint) {
2189
+ if (options.length === 0) return null;
2190
+ const collected = Object.entries(ctx.slots).filter(([, v]) => v !== void 0 && v !== null && v !== "").map(([k, v]) => `${k.trim()}: ${String(v)}`).join("; ");
2191
+ const list = options.map((o, i) => `${i + 1}. ${o.text}`).join("\n");
2192
+ const prompt = [
2193
+ "Pick which outcome best matches the current state of this voice conversation.",
2194
+ ...hint && hint.trim() ? [`What decides the path: ${hint.trim()}`] : [],
2195
+ `Information collected: ${collected || "(none)"}`,
2196
+ `The caller most recently said: "${ctx.lastUtterance ?? ""}"`,
2197
+ "Outcomes:",
2198
+ list,
2199
+ 'Answer with ONLY the outcome number, or "none" if nothing clearly matches.'
2200
+ ].join("\n");
2201
+ const controller = new AbortController();
2202
+ const timer = setTimeout(() => controller.abort(), JUDGE_TIMEOUT_MS);
2203
+ try {
2204
+ const res = await defaultOpenAI().chat.completions.create(
2205
+ {
2206
+ model: process.env["VOICELAYER_JUDGE_MODEL"] ?? "gpt-4o-mini",
2207
+ temperature: 0,
2208
+ max_tokens: 4,
2209
+ messages: [
2210
+ { role: "system", content: 'Answer with only a number, or "none".' },
2211
+ { role: "user", content: prompt }
2212
+ ]
2213
+ },
2214
+ { signal: controller.signal }
2215
+ );
2216
+ const answer = (res.choices[0]?.message?.content ?? "").trim().toLowerCase();
2217
+ const n = Number.parseInt(answer.replace(/[^0-9]/g, ""), 10);
2218
+ if (!Number.isInteger(n) || n < 1 || n > options.length) return null;
2219
+ return options[n - 1]?.id ?? null;
2220
+ } catch (err) {
2221
+ console.warn("[graph] option pick failed; treating as undecided", {
2222
+ err: err instanceof Error ? err.message : String(err)
2223
+ });
2224
+ return null;
2225
+ } finally {
2226
+ clearTimeout(timer);
2227
+ }
2228
+ }
2229
+ async function judge(condition, ctx) {
2230
+ const collected = Object.entries(ctx.slots).filter(([, v]) => v !== void 0 && v !== null && v !== "").map(([k, v]) => `${k.trim()}: ${String(v)}`).join("; ");
2231
+ const prompt = [
2232
+ "You judge whether a condition about an ongoing voice conversation is currently TRUE.",
2233
+ `Information collected: ${collected || "(none)"}`,
2234
+ `The caller most recently said: "${ctx.lastUtterance ?? ""}"`,
2235
+ `Condition: ${condition}`,
2236
+ 'Answer strictly "yes" or "no".'
2237
+ ].join("\n");
2238
+ const controller = new AbortController();
2239
+ const timer = setTimeout(() => controller.abort(), JUDGE_TIMEOUT_MS);
2240
+ try {
2241
+ const res = await defaultOpenAI().chat.completions.create(
2242
+ {
2243
+ model: process.env["VOICELAYER_JUDGE_MODEL"] ?? "gpt-4o-mini",
2244
+ temperature: 0,
2245
+ max_tokens: 2,
2246
+ messages: [
2247
+ { role: "system", content: 'Answer only "yes" or "no".' },
2248
+ { role: "user", content: prompt }
2249
+ ]
2250
+ },
2251
+ { signal: controller.signal }
2252
+ );
2253
+ return (res.choices[0]?.message?.content ?? "").trim().toLowerCase().startsWith("y");
2254
+ } catch (err) {
2255
+ console.warn("[graph] condition judge failed; treating as false", {
2256
+ condition,
2257
+ err: err instanceof Error ? err.message : String(err)
2258
+ });
2259
+ return false;
2260
+ } finally {
2261
+ clearTimeout(timer);
2262
+ }
2263
+ }
2264
+ var JUDGE_TIMEOUT_MS;
2265
+ var init_conditions = __esm({
2266
+ "src/runtime/graph/conditions.ts"() {
2267
+ init_openai_default();
2268
+ init_interpolate();
2269
+ init_embedder();
2270
+ JUDGE_TIMEOUT_MS = 4e3;
2271
+ }
2272
+ });
2273
+
2274
+ // src/runtime/graph/branch-resolve.ts
2275
+ function branchOptionsOf(config) {
2276
+ if (!Array.isArray(config.branches)) return [];
2277
+ return config.branches.filter((b) => b && typeof b.id === "string" && typeof b.label === "string").map((b) => ({
2278
+ id: b.id,
2279
+ label: b.label,
2280
+ ...typeof b.when === "string" && b.when.trim() ? { when: b.when } : {},
2281
+ ...typeof b.digit === "string" && b.digit.trim() ? { digit: b.digit.trim() } : {},
2282
+ ...typeof b.description === "string" && b.description.trim() ? { description: b.description } : {}
2283
+ }));
2284
+ }
2285
+ async function resolveBranch(branches, ctx, opts) {
2286
+ const candidates = branches.filter((b) => b.id !== ELSE_BRANCH_ID);
2287
+ if (candidates.length === 0) return null;
2288
+ const nl = [];
2289
+ for (const b of candidates) {
2290
+ if (b.when && b.when.trim()) {
2291
+ const det = evaluateDeterministicCondition(b.when, ctx);
2292
+ if (det === true) return b.id;
2293
+ if (det === void 0) nl.push(b);
2294
+ } else {
2295
+ nl.push(b);
2296
+ }
2297
+ }
2298
+ if (nl.length === 0) return null;
2299
+ const utterance = ctx.lastUtterance?.trim();
2300
+ if (utterance) {
2301
+ const embedder = getEmbedder();
2302
+ if (embedder.available) {
2303
+ const uvec = await embedder.embed(utterance);
2304
+ if (uvec) {
2305
+ let best = null;
2306
+ for (const b of nl) {
2307
+ const bvec = await embedder.embed(b.when?.trim() || b.label);
2308
+ if (!bvec) continue;
2309
+ const score = dotProduct(uvec, bvec);
2310
+ if (!best || score > best.score) best = { id: b.id, score };
2311
+ }
2312
+ if (best && best.score >= ROUTE_THRESHOLD) {
2313
+ console.log("[graph] branch: embedding", { id: best.id, score: Number(best.score.toFixed(3)) });
2314
+ return best.id;
2315
+ }
2316
+ }
2317
+ }
2318
+ }
2319
+ return pickOption(
2320
+ nl.map((b) => ({ id: b.id, text: b.when?.trim() || b.label })),
2321
+ ctx,
2322
+ opts?.hint
2323
+ );
2324
+ }
2325
+ var ELSE_BRANCH_ID;
2326
+ var init_branch_resolve = __esm({
2327
+ "src/runtime/graph/branch-resolve.ts"() {
2328
+ init_conditions();
2329
+ init_embedder();
2330
+ ELSE_BRANCH_ID = "else";
2331
+ }
2332
+ });
2333
+
2334
+ // src/runtime/graph/drift.ts
2335
+ function adherenceOf(node) {
2336
+ const a = node.config.adherence;
2337
+ return a === "observe" || a === "enforce" ? a : "off";
2338
+ }
2339
+ async function similarity(reply, intent) {
2340
+ const r = reply.trim();
2341
+ const n = intent.trim();
2342
+ if (!r || !n) return null;
2343
+ const embedder = getEmbedder();
2344
+ if (!embedder.available) return null;
2345
+ const [rv, nv] = await Promise.all([embedder.embed(r), embedder.embed(n)]);
2346
+ if (!rv || !nv) return null;
2347
+ return dotProduct(rv, nv);
2348
+ }
2349
+ var DRIFT_THRESHOLD, MAX_DRIFT_RETRIES;
2350
+ var init_drift = __esm({
2351
+ "src/runtime/graph/drift.ts"() {
2352
+ init_embedder();
2353
+ DRIFT_THRESHOLD = Number(process.env["VL_FLOW_DRIFT_THRESHOLD"] ?? "0.25");
2354
+ MAX_DRIFT_RETRIES = Number(process.env["VL_FLOW_DRIFT_RETRIES"] ?? "1");
2355
+ }
2356
+ });
2357
+
2358
+ // src/define-tool.ts
2359
+ function defineTool(spec) {
2360
+ return {
2361
+ [spec.name]: {
2362
+ description: spec.description,
2363
+ input: spec.input,
2364
+ run: spec.run,
2365
+ ...spec.capability ? { capability: spec.capability } : {}
2366
+ }
2367
+ };
2368
+ }
2369
+ function httpTool(spec) {
2370
+ const method = spec.method ?? "POST";
2371
+ const fetchImpl = spec.fetchImpl ?? fetch;
2372
+ return defineTool({
2373
+ name: spec.name,
2374
+ description: spec.description,
2375
+ input: spec.input,
2376
+ ...spec.capability ? { capability: spec.capability } : {},
2377
+ run: async (input) => {
2378
+ const bag = input ?? {};
2379
+ const url = spec.url.replace(
2380
+ /\{(\w+)\}/g,
2381
+ (_m, key) => encodeURIComponent(String(bag[key] ?? ""))
2382
+ );
2383
+ const init = {
2384
+ method,
2385
+ headers: { "content-type": "application/json", ...spec.headers ?? {} },
2386
+ ...method === "GET" ? {} : { body: JSON.stringify(spec.body ? spec.body(bag) : bag) }
2387
+ };
2388
+ const res = await fetchImpl(url, init);
2389
+ const text = await res.text();
2390
+ let data = text;
2391
+ try {
2392
+ data = JSON.parse(text);
2393
+ } catch {
2394
+ }
2395
+ return { ok: res.ok, status: res.status, data };
2396
+ }
2397
+ });
2398
+ }
2399
+ var init_define_tool = __esm({
2400
+ "src/define-tool.ts"() {
2401
+ }
2402
+ });
2403
+
2404
+ // src/runtime/graph/executors.ts
2405
+ function historyPolicyOf(node) {
2406
+ const p = node.config.historyPolicy;
2407
+ return p === "last-turn" || p === "slots-only" ? p : "full";
2408
+ }
2409
+ async function withHistory(deps, policy, gen) {
2410
+ if (deps.history) await deps.history.withPolicy(policy, gen);
2411
+ else await gen();
2412
+ }
2413
+ function scopeFor(state, deps) {
2414
+ const slots = deps.processRt.getData();
2415
+ return {
2416
+ ...buildBuiltinScope(deps),
2417
+ ...slots,
2418
+ slots,
2419
+ tool: state.toolResults,
2420
+ node: state.nodeResults,
2421
+ call: deps.ctx.call
2422
+ };
2423
+ }
2424
+ function str(value) {
2425
+ return typeof value === "string" ? value : "";
2426
+ }
2427
+ function evalCtxOf(state, deps) {
2428
+ return {
2429
+ slots: deps.processRt.getData(),
2430
+ tools: state.toolResults,
2431
+ nodeResults: state.nodeResults,
2432
+ ...state.lastUtterance !== void 0 ? { lastUtterance: state.lastUtterance } : {},
2433
+ ...state.lastDtmf !== void 0 ? { lastDtmf: state.lastDtmf } : {}
2434
+ };
2435
+ }
2436
+ function deterministicEdgeTarget(edges, ctx) {
2437
+ for (const e of edges) {
2438
+ if (!e.condition || !e.condition.trim()) continue;
2439
+ if (evaluateDeterministicCondition(e.condition, ctx) === true) return e.to;
2440
+ }
2441
+ return null;
2442
+ }
2443
+ async function speak(text, deps, opts) {
2444
+ if (!text.trim()) return;
2445
+ const allowInterruptions = opts?.allowInterruptions === true;
2446
+ if (deps.ctx.adapter) {
2447
+ const r = await deps.ctx.adapter.say(text, { allowInterruptions });
2448
+ console.log("[graph] said", { chars: text.length, interrupted: r.interrupted });
2449
+ return;
2450
+ }
2451
+ const session = deps.ctx.livekit.session;
2452
+ if (deps.realtime) {
2453
+ await session.generateReply({
2454
+ instructions: `Say the following line to the user, verbatim and in full \u2014 no additions, omissions, or rephrasing:
2455
+
2456
+ ${text}`
2457
+ }).waitForPlayout();
2458
+ console.log("[graph] said (realtime)", { chars: text.length });
2459
+ return;
2460
+ }
2461
+ const handle = session.say(text, { allowInterruptions });
2462
+ await handle.waitForPlayout();
2463
+ console.log("[graph] said", { chars: text.length, interrupted: handle.interrupted });
2464
+ }
2465
+ async function genReply(deps, instructions) {
2466
+ if (deps.ctx.adapter) {
2467
+ await deps.ctx.adapter.generateReply({ instructions });
2468
+ return;
2469
+ }
2470
+ await deps.ctx.livekit.session.generateReply({ instructions }).waitForPlayout();
2471
+ }
2472
+ async function nextTurn(deps, reprompt) {
2473
+ let reprompted = false;
2474
+ for (; ; ) {
2475
+ const evt = await deps.utterances.next({ timeoutMs: NO_INPUT_MS });
2476
+ if (evt.kind === "hangup") return { result: { kind: "end" } };
2477
+ if (evt.kind === "control") return { result: evt.result };
2478
+ if (evt.kind === "timeout") {
2479
+ if (!reprompted) {
2480
+ reprompted = true;
2481
+ deps.events.note("no_input.reprompt");
2482
+ await speak(reprompt ?? "Sorry, are you still there?", deps);
2483
+ continue;
2484
+ }
2485
+ deps.events.note("no_input.escalate");
2486
+ return { result: { kind: "end" } };
2487
+ }
2488
+ if (evt.kind === "dtmf") return { text: evt.digit, dtmf: evt.digit };
2489
+ return { text: evt.text };
2490
+ }
2491
+ }
2492
+ function detectYesNo(text) {
2493
+ const yes = YES_WORDS.test(text);
2494
+ const no = NO_WORDS.test(text);
2495
+ if (yes && !no) return "yes";
2496
+ if (no && !yes) return "no";
2497
+ return null;
2498
+ }
2499
+ function formatCollected(slots) {
2500
+ return Object.entries(slots).filter(([, v]) => v !== void 0 && v !== null && v !== "").map(([k, v]) => `${k.trim()}: ${String(v)}`).join("\n");
2501
+ }
2502
+ function round3(n) {
2503
+ return Math.round(n * 1e3) / 1e3;
2504
+ }
2505
+ async function guardedGenerate(node, state, deps, instructions, intent) {
2506
+ await genReply(deps, instructions);
2507
+ const mode = adherenceOf(node);
2508
+ if (mode === "off") return;
2509
+ let sim = await similarity(deps.history?.lastAssistantText() ?? "", intent);
2510
+ if (sim === null || sim >= DRIFT_THRESHOLD) return;
2511
+ if (mode === "observe") {
2512
+ deps.events.note("drift.observed", { node: node.id, score: round3(sim) });
2513
+ return;
2514
+ }
2515
+ for (let attempt = 1; attempt <= MAX_DRIFT_RETRIES; attempt++) {
2516
+ deps.events.note("drift.regenerate", { node: node.id, attempt, score: round3(sim) });
2517
+ await genReply(
2518
+ deps,
2519
+ `${instructions}
2520
+
2521
+ IMPORTANT: stay strictly on this task and do not go off-topic:
2522
+ ${intent}`
2523
+ );
2524
+ sim = await similarity(deps.history?.lastAssistantText() ?? "", intent);
2525
+ if (sim === null || sim >= DRIFT_THRESHOLD) return;
2526
+ }
2527
+ const fallback = str(node.config.fallback);
2528
+ if (fallback) {
2529
+ deps.events.note("drift.fallback_verbatim", { node: node.id });
2530
+ await speak(interpolate(fallback, scopeFor(state, deps)), deps);
2531
+ } else {
2532
+ deps.events.note("drift.unresolved", { node: node.id });
2533
+ }
2534
+ }
2535
+ function routeByPort(node, deps, port) {
2536
+ return deps.model.selectByPort(node.id, port) !== null ? { kind: "take", port } : { kind: "advance" };
2537
+ }
2538
+ function menuDigitOf(b) {
2539
+ if (b.digit && b.digit.trim()) return b.digit.trim();
2540
+ const m = b.label.match(/^\s*([0-9*#])\b/);
2541
+ return m ? m[1] : null;
2542
+ }
2543
+ function menuLabelOf(label) {
2544
+ return label.replace(/^\s*[0-9*#]\s*[-—–:.]*\s*/, "").trim() || label.trim();
2545
+ }
2546
+ var ASK_MAX_CLARIFY, LLM_MAX_TURNS, HUB_MAX_TURNS, NO_INPUT_MS, sayExecutor, YES_WORDS, NO_WORDS, confirmExecutor, askExecutor, gateExecutor, endExecutor, handoffExecutor, llmExecutor, toolExecutor, ragExecutor, codeExecutor, decisionExecutor, MENU_MAX_INPUTS, menuExecutor, classifyExecutor, routerExecutor, playbookExecutor, setExecutor, MAX_SUBFLOW_DEPTH, subflowExecutor, passthroughExecutor, EXECUTORS;
2547
+ var init_executors = __esm({
2548
+ "src/runtime/graph/executors.ts"() {
2549
+ init_src();
2550
+ init_interpolate();
2551
+ init_builtins();
2552
+ init_conditions();
2553
+ init_branch_resolve();
2554
+ init_drift();
2555
+ init_define_tool();
2556
+ ASK_MAX_CLARIFY = 4;
2557
+ LLM_MAX_TURNS = 30;
2558
+ HUB_MAX_TURNS = 500;
2559
+ NO_INPUT_MS = Number(process.env["VL_FLOW_NO_INPUT_MS"] ?? "8000");
2560
+ sayExecutor = async (node, state, deps) => {
2561
+ await speak(interpolate(str(node.config.text), scopeFor(state, deps)), deps);
2562
+ return { kind: "advance" };
2563
+ };
2564
+ YES_WORDS = /\b(yes|yeah|yep|yup|correct|right|exactly|sure|affirmative|that'?s right|sounds good|ok(?:ay)?)\b/i;
2565
+ NO_WORDS = /\b(no|nope|nah|wrong|incorrect|negative|not right|that'?s not|change|actually)\b/i;
2566
+ confirmExecutor = async (node, state, deps) => {
2567
+ const text = interpolate(str(node.config.text), scopeFor(state, deps));
2568
+ await speak(text || "Is that correct?", deps);
2569
+ for (let attempt = 0; attempt < 2; attempt++) {
2570
+ const t = await nextTurn(deps, text || "Is that correct?");
2571
+ if ("result" in t) return t.result;
2572
+ state.lastUtterance = t.text;
2573
+ state.lastDtmf = t.dtmf;
2574
+ let verdict = null;
2575
+ if (t.dtmf === "1") verdict = "yes";
2576
+ else if (t.dtmf === "2") verdict = "no";
2577
+ else if (t.text.trim()) verdict = detectYesNo(t.text);
2578
+ if (verdict === null && t.text.trim()) {
2579
+ verdict = await resolveBranch(
2580
+ [
2581
+ { id: "yes", label: "The caller confirms \u2014 the detail is correct." },
2582
+ { id: "no", label: "The caller says it is wrong or wants to change it." }
2583
+ ],
2584
+ evalCtxOf(state, deps)
2585
+ );
2586
+ }
2587
+ if (verdict === "yes") {
2588
+ deps.events.note("confirm.confirmed", { node: node.id });
2589
+ return routeByPort(node, deps, "yes");
2590
+ }
2591
+ if (verdict === "no") {
2592
+ deps.events.note("confirm.denied", { node: node.id });
2593
+ return routeByPort(node, deps, "no");
2594
+ }
2595
+ if (attempt === 0) await speak("Sorry \u2014 is that correct? Yes or no.", deps);
2596
+ }
2597
+ deps.events.note("confirm.unresolved", { node: node.id });
2598
+ return { kind: "advance" };
2599
+ };
2600
+ askExecutor = async (node, state, deps) => {
2601
+ const slot = str(node.config.slot) || node.id;
2602
+ const authoredSlot = slot === node.id ? "" : slot.trim();
2603
+ const label = authoredSlot.replace(/[_-]+/g, " ");
2604
+ if (hasSlot(deps.processRt.getData(), slot)) {
2605
+ console.log("[graph] ask: slot already known, skipping", { slot: slot.trim() });
2606
+ return { kind: "advance" };
2607
+ }
2608
+ const askText = interpolate(
2609
+ str(node.config.ask) || (label ? `Could you tell me your ${label}?` : "Could you tell me a bit more about what you need?"),
2610
+ scopeFor(state, deps)
2611
+ );
2612
+ await speak(askText, deps);
2613
+ for (let turn = 0; turn < ASK_MAX_CLARIFY; turn++) {
2614
+ const t = await nextTurn(deps, askText);
2615
+ if ("result" in t) return t.result;
2616
+ state.lastUtterance = t.text;
2617
+ state.lastDtmf = t.dtmf;
2618
+ if (hasSlot(deps.processRt.getData(), slot)) {
2619
+ console.log("[graph] ask: captured", { slot: slot.trim() });
2620
+ return { kind: "advance" };
2621
+ }
2622
+ console.log("[graph] ask: clarify", { slot: slot.trim(), turn });
2623
+ await withHistory(
2624
+ deps,
2625
+ historyPolicyOf(node),
2626
+ () => genReply(
2627
+ deps,
2628
+ `You are collecting "${label || "the detail just asked about"}" from the caller. They haven't yet given a clear, valid value. Acknowledge what they said, then briefly ask again for just that.`
2629
+ )
2630
+ );
2631
+ }
2632
+ deps.events.note("ask.unfilled", { slot: slot.trim() });
2633
+ return { kind: "advance" };
2634
+ };
2635
+ gateExecutor = async (node, _state, deps) => {
2636
+ const required = Array.isArray(node.config.requiredFields) ? node.config.requiredFields.filter((x) => typeof x === "string") : [];
2637
+ const data = deps.processRt.getData();
2638
+ const missing = required.filter((f) => !hasSlot(data, f));
2639
+ if (missing.length === 0) return { kind: "advance" };
2640
+ deps.events.note("gate.incomplete", { missing: missing.map((m) => m.trim()) });
2641
+ return { kind: "advance" };
2642
+ };
2643
+ endExecutor = async (node, state, deps) => {
2644
+ const msg = str(node.config.endMessage);
2645
+ if (msg) await speak(interpolate(msg, scopeFor(state, deps)), deps);
2646
+ return { kind: "end" };
2647
+ };
2648
+ handoffExecutor = async (node, _state, deps) => {
2649
+ const cfg = node.config;
2650
+ const target = str(cfg.to) || str(cfg.fallback);
2651
+ if (!target) {
2652
+ deps.events.note("handoff.no_target", { node: node.id });
2653
+ return { kind: "advance" };
2654
+ }
2655
+ const reason = str(cfg.reason) || `flow:${node.id}`;
2656
+ const mode = cfg.mode === "warm" || cfg.mode === "cold" ? cfg.mode : void 0;
2657
+ deps.events.note("handoff.start", { node: node.id, to: target, ...mode ? { mode } : {} });
2658
+ try {
2659
+ await deps.ctx.handoff(target, { reason, ...mode ? { mode } : {} });
2660
+ } catch (err) {
2661
+ deps.events.note("handoff.failed", {
2662
+ node: node.id,
2663
+ error: err instanceof Error ? err.message : String(err)
2664
+ });
2665
+ }
2666
+ return { kind: "handoff", to: target };
2667
+ };
2668
+ llmExecutor = async (node, state, deps) => {
2669
+ const out = deps.model.out(node.id);
2670
+ const conditional = out.filter((e) => e.condition && e.condition.trim());
2671
+ const terminal = out.length === 0;
2672
+ const hub = node.config.hub === true;
2673
+ const policy = historyPolicyOf(node);
2674
+ const maxTurns = hub ? HUB_MAX_TURNS : LLM_MAX_TURNS;
2675
+ const base = interpolate(str(node.config.instructions), scopeFor(state, deps)) || "Help the caller using everything collected so far. Keep replies short and natural.";
2676
+ for (let turn = 0; turn < maxTurns; turn++) {
2677
+ const collected = formatCollected(deps.processRt.getData());
2678
+ const parts = [base];
2679
+ if (collected) parts.push(`Information collected so far:
2680
+ ${collected}`);
2681
+ if (state.lastUtterance) parts.push(`The caller just said: "${state.lastUtterance}"`);
2682
+ await withHistory(
2683
+ deps,
2684
+ policy,
2685
+ () => guardedGenerate(node, state, deps, parts.join("\n\n"), base)
2686
+ );
2687
+ if (!hub && !terminal && conditional.length === 0) return { kind: "advance" };
2688
+ const t = await nextTurn(deps);
2689
+ if ("result" in t) return t.result;
2690
+ state.lastUtterance = t.text;
2691
+ state.lastDtmf = t.dtmf;
2692
+ if (conditional.length > 0) {
2693
+ const target = await selectEdge(out, {
2694
+ slots: deps.processRt.getData(),
2695
+ tools: state.toolResults,
2696
+ nodeResults: state.nodeResults,
2697
+ lastDtmf: state.lastDtmf,
2698
+ lastUtterance: state.lastUtterance
2699
+ });
2700
+ if (target) {
2701
+ console.log("[graph] llm: transition", { node: node.id, to: target, hub });
2702
+ return { kind: "goto", to: target };
2703
+ }
2704
+ }
2705
+ }
2706
+ console.log("[graph] llm: turn cap reached", { node: node.id, hub });
2707
+ return terminal ? { kind: "end" } : { kind: "advance" };
2708
+ };
2709
+ toolExecutor = async (node, state, deps) => {
2710
+ const cfg = node.config;
2711
+ const name = str(cfg.name) || node.id;
2712
+ const routeByOutcome = () => {
2713
+ const ok = state.toolResults[node.id]?.ok === true;
2714
+ return routeByPort(node, deps, ok ? "ok" : "fail");
2715
+ };
2716
+ const toolRef = str(cfg.toolRef);
2717
+ const registry = deps.resolvers?.registryTools;
2718
+ if (toolRef && registry) {
2719
+ deps.events.note("tool.invoked", { node: node.id, name: toolRef, ref: true });
2720
+ try {
2721
+ const spec = (await registry.catalog()).find((t) => t.name === toolRef);
2722
+ const slots2 = deps.processRt.getData();
2723
+ const bag2 = {};
2724
+ for (const field of Object.keys(spec?.input ?? {})) {
2725
+ const v = resolvePath(slots2, field);
2726
+ if (v !== void 0 && v !== null && v !== "") bag2[field] = v;
2727
+ }
2728
+ const result = await registry.run(toolRef, bag2);
2729
+ state.toolResults[node.id] = result;
2730
+ deps.events.note(result.ok ? "tool.succeeded" : "tool.errored", {
2731
+ node: node.id,
2732
+ name: toolRef,
2733
+ status: result.status
2734
+ });
2735
+ } catch (err) {
2736
+ state.toolResults[node.id] = {
2737
+ ok: false,
2738
+ status: 0,
2739
+ data: { error: err instanceof Error ? err.message : String(err) }
2740
+ };
2741
+ deps.events.note("tool.errored", { node: node.id, name: toolRef });
2742
+ }
2743
+ return routeByOutcome();
2744
+ }
2745
+ if (toolRef && !registry) {
2746
+ state.toolResults[node.id] = {
2747
+ ok: false,
2748
+ status: 0,
2749
+ data: { kind: "unavailable", reason: "tool registry resolver not wired" }
2750
+ };
2751
+ deps.events.note("tool.unavailable", { node: node.id, name: toolRef });
2752
+ return routeByOutcome();
2753
+ }
2754
+ const url = str(cfg.url);
2755
+ if (!url) {
2756
+ state.toolResults[node.id] = { ok: false, status: 0, data: null };
2757
+ deps.events.note("tool.errored", { node: node.id, name, error: "no url" });
2758
+ return routeByOutcome();
2759
+ }
2760
+ const inputSpec = cfg.input && typeof cfg.input === "object" ? cfg.input : {};
2761
+ const slots = deps.processRt.getData();
2762
+ const bag = {};
2763
+ for (const field of Object.keys(inputSpec)) {
2764
+ const v = resolvePath(slots, field);
2765
+ if (v !== void 0 && v !== null && v !== "") bag[field] = v;
2766
+ }
2767
+ const cfgHeaders = cfg.headers && typeof cfg.headers === "object" ? cfg.headers : {};
2768
+ const method = str(cfg.method) || "POST";
2769
+ const auth = cfg.auth === "connection" ? "connection" : void 0;
2770
+ const connectionRef = str(cfg.connectionRef);
2771
+ deps.events.note("tool.invoked", { node: node.id, name });
2772
+ const serverExec = deps.resolvers?.toolExecutor;
2773
+ if (serverExec) {
2774
+ try {
2775
+ const result = await serverExec({
2776
+ name,
2777
+ url,
2778
+ method,
2779
+ ...Object.keys(cfgHeaders).length ? { headers: cfgHeaders } : {},
2780
+ ...Object.keys(bag).length ? { input: bag } : {},
2781
+ ...auth ? { auth } : {},
2782
+ ...auth && connectionRef ? { connectionRef } : {}
2783
+ });
2784
+ state.toolResults[node.id] = result;
2785
+ deps.events.note(result.ok ? "tool.succeeded" : "tool.errored", {
2786
+ node: node.id,
2787
+ name,
2788
+ status: result.status
2789
+ });
2790
+ } catch (err) {
2791
+ state.toolResults[node.id] = {
2792
+ ok: false,
2793
+ status: 0,
2794
+ data: { error: err instanceof Error ? err.message : String(err) }
2795
+ };
2796
+ deps.events.note("tool.errored", { node: node.id, name });
2797
+ }
2798
+ return routeByOutcome();
2799
+ }
2800
+ let authHeaders = {};
2801
+ if (auth === "connection") {
2802
+ const resolver = deps.resolvers?.secrets;
2803
+ const resolved = connectionRef && resolver ? await resolver(connectionRef) : null;
2804
+ if (!resolved || !resolved.ok) {
2805
+ state.toolResults[node.id] = {
2806
+ ok: false,
2807
+ status: 0,
2808
+ data: {
2809
+ kind: "unavailable",
2810
+ reason: resolved && !resolved.ok ? resolved.reason : "connection-auth (vault resolver) not wired"
2811
+ }
2812
+ };
2813
+ deps.events.note("tool.unavailable", { node: node.id, name });
2814
+ return routeByOutcome();
2815
+ }
2816
+ authHeaders = resolved.headers;
2817
+ }
2818
+ const headers = { ...cfgHeaders, ...authHeaders };
2819
+ try {
2820
+ const def = httpTool({
2821
+ name,
2822
+ description: str(cfg.description) || name,
2823
+ input: inputSpec,
2824
+ url,
2825
+ method,
2826
+ ...Object.keys(headers).length ? { headers } : {}
2827
+ });
2828
+ const run = def[name]?.run;
2829
+ const result = run ? await run(bag, deps.ctx) : { ok: false, status: 0, data: null };
2830
+ state.toolResults[node.id] = result;
2831
+ deps.events.note(result.ok ? "tool.succeeded" : "tool.errored", {
2832
+ node: node.id,
2833
+ name,
2834
+ status: result.status
2835
+ });
2836
+ } catch (err) {
2837
+ state.toolResults[node.id] = {
2838
+ ok: false,
2839
+ status: 0,
2840
+ data: { error: err instanceof Error ? err.message : String(err) }
2841
+ };
2842
+ deps.events.note("tool.errored", { node: node.id, name });
2843
+ }
2844
+ return routeByOutcome();
2845
+ };
2846
+ ragExecutor = async (node, state, deps) => {
2847
+ const cfg = node.config;
2848
+ const resolver = deps.resolvers?.knowledge;
2849
+ if (!resolver) {
2850
+ state.nodeResults[node.id] = { found: false, count: 0, text: "", chunks: [] };
2851
+ deps.events.note("rag.unavailable", { node: node.id });
2852
+ return routeByPort(node, deps, "not_found");
2853
+ }
2854
+ const query = interpolate(str(cfg.query), scopeFor(state, deps)).trim() || (state.lastUtterance ?? "");
2855
+ const limit = typeof cfg.topK === "number" ? cfg.topK : 3;
2856
+ const minScore = typeof cfg.minScore === "number" ? cfg.minScore : void 0;
2857
+ deps.events.note("rag.query", { node: node.id, chars: query.length, limit });
2858
+ try {
2859
+ const res = await resolver({ query, limit, ...minScore !== void 0 ? { minScore } : {} });
2860
+ const text = res.chunks.map((c) => c.content).join("\n\n");
2861
+ state.nodeResults[node.id] = {
2862
+ found: res.found,
2863
+ count: res.chunks.length,
2864
+ text,
2865
+ chunks: res.chunks
2866
+ };
2867
+ deps.events.note(res.found ? "rag.found" : "rag.not_found", {
2868
+ node: node.id,
2869
+ count: res.chunks.length
2870
+ });
2871
+ return routeByPort(node, deps, res.found ? "found" : "not_found");
2872
+ } catch (err) {
2873
+ state.nodeResults[node.id] = {
2874
+ found: false,
2875
+ count: 0,
2876
+ text: "",
2877
+ chunks: [],
2878
+ error: err instanceof Error ? err.message : String(err)
2879
+ };
2880
+ deps.events.note("rag.errored", { node: node.id });
2881
+ return routeByPort(node, deps, "not_found");
2882
+ }
2883
+ };
2884
+ codeExecutor = async (node, state, deps) => {
2885
+ const cfg = node.config;
2886
+ const resolver = deps.resolvers?.code;
2887
+ const expression = str(cfg.expression);
2888
+ if (!resolver || !expression) {
2889
+ state.nodeResults[node.id] = { ok: false, reason: resolver ? "no-expression" : "no-code-resolver" };
2890
+ deps.events.note("code.unavailable", { node: node.id });
2891
+ return { kind: "advance" };
2892
+ }
2893
+ deps.events.note("code.eval", { node: node.id });
2894
+ try {
2895
+ const res = await resolver({ expression, scope: scopeFor(state, deps) });
2896
+ if (res.ok) {
2897
+ state.nodeResults[node.id] = { ok: true, ...res.outputs };
2898
+ deps.events.note("code.ok", { node: node.id });
2899
+ return routeByPort(node, deps, res.next ?? "next");
2900
+ }
2901
+ state.nodeResults[node.id] = { ok: false, error: res.error };
2902
+ deps.events.note("code.errored", { node: node.id });
2903
+ return routeByPort(node, deps, "error");
2904
+ } catch (err) {
2905
+ state.nodeResults[node.id] = { ok: false, error: err instanceof Error ? err.message : String(err) };
2906
+ deps.events.note("code.errored", { node: node.id });
2907
+ return routeByPort(node, deps, "error");
2908
+ }
2909
+ };
2910
+ decisionExecutor = async (node, state, deps) => {
2911
+ const ctx = evalCtxOf(state, deps);
2912
+ const det = deterministicEdgeTarget(deps.model.out(node.id), ctx);
2913
+ if (det !== null) {
2914
+ deps.events.note("decision.routed", { node: node.id, to: det });
2915
+ return { kind: "goto", to: det };
2916
+ }
2917
+ const branches = branchOptionsOf(node.config);
2918
+ if (branches.length === 0) return { kind: "advance" };
2919
+ const choice = await resolveBranch(branches, ctx, { hint: str(node.config.condition) });
2920
+ if (choice) {
2921
+ state.nodeResults[node.id] = { choice };
2922
+ deps.events.note("decision.routed", { node: node.id, choice });
2923
+ return { kind: "take", port: choice };
2924
+ }
2925
+ if (branches.some((b) => b.id === ELSE_BRANCH_ID)) {
2926
+ deps.events.note("decision.defaulted", { node: node.id });
2927
+ return { kind: "take", port: ELSE_BRANCH_ID };
2928
+ }
2929
+ deps.events.note("decision.unresolved", { node: node.id });
2930
+ return { kind: "advance" };
2931
+ };
2932
+ MENU_MAX_INPUTS = 3;
2933
+ menuExecutor = async (node, state, deps) => {
2934
+ const branches = branchOptionsOf(node.config);
2935
+ const fallback = () => branches.some((b) => b.id === ELSE_BRANCH_ID) ? { kind: "take", port: ELSE_BRANCH_ID } : { kind: "advance" };
2936
+ const keyed = branches.filter((b) => b.id !== ELSE_BRANCH_ID && menuDigitOf(b));
2937
+ const synthesized = keyed.length ? `Please choose: ${keyed.map((b) => `press ${menuDigitOf(b)} for ${menuLabelOf(b.label)}`).join(", ")}.` : "";
2938
+ const prompt = interpolate(str(node.config.prompt), scopeFor(state, deps)).trim() || synthesized;
2939
+ await speak(prompt, deps);
2940
+ if (branches.length === 0) return { kind: "advance" };
2941
+ for (let attempt = 0; attempt < MENU_MAX_INPUTS; attempt++) {
2942
+ const t = await nextTurn(deps, prompt);
2943
+ if ("result" in t) return t.result;
2944
+ state.lastUtterance = t.text;
2945
+ state.lastDtmf = t.dtmf;
2946
+ if (t.dtmf) {
2947
+ const hit = branches.find((b) => b.id !== ELSE_BRANCH_ID && menuDigitOf(b) === t.dtmf);
2948
+ if (hit) {
2949
+ deps.events.note("menu.selected", { node: node.id, digit: t.dtmf, choice: hit.id });
2950
+ return { kind: "take", port: hit.id };
2951
+ }
2952
+ deps.events.note("menu.invalid", { node: node.id, digit: t.dtmf });
2953
+ await speak("Sorry, that's not one of the options.", deps);
2954
+ continue;
2955
+ }
2956
+ if (t.text.trim()) {
2957
+ const choice = await resolveBranch(branches, evalCtxOf(state, deps));
2958
+ if (choice) {
2959
+ deps.events.note("menu.selected", { node: node.id, choice });
2960
+ return { kind: "take", port: choice };
2961
+ }
2962
+ await speak(prompt, deps);
2963
+ }
2964
+ }
2965
+ deps.events.note("menu.unmatched", { node: node.id });
2966
+ return fallback();
2967
+ };
2968
+ classifyExecutor = async (node, state, deps) => {
2969
+ const cfg = node.config;
2970
+ const resolver = deps.resolvers?.classify;
2971
+ const branches = branchOptionsOf(cfg);
2972
+ const options = branches.filter((b) => b.id !== ELSE_BRANCH_ID).map((b) => ({ id: b.id, label: b.label }));
2973
+ const fallback = () => branches.some((b) => b.id === ELSE_BRANCH_ID) ? { kind: "take", port: ELSE_BRANCH_ID } : { kind: "advance" };
2974
+ if (!resolver || options.length === 0) {
2975
+ deps.events.note("classify.unavailable", { node: node.id });
2976
+ return fallback();
2977
+ }
2978
+ deps.events.note("classify.start", { node: node.id, options: options.length });
2979
+ try {
2980
+ const choice = await resolver({
2981
+ instructions: str(cfg.instructions),
2982
+ options,
2983
+ ...state.lastUtterance !== void 0 ? { lastUtterance: state.lastUtterance } : {},
2984
+ slots: deps.processRt.getData()
2985
+ });
2986
+ if (!choice) {
2987
+ deps.events.note("classify.undecided", { node: node.id });
2988
+ return fallback();
2989
+ }
2990
+ state.nodeResults[node.id] = { choice };
2991
+ deps.events.note("classify.chose", { node: node.id, choice });
2992
+ return routeByPort(node, deps, choice);
2993
+ } catch (err) {
2994
+ deps.events.note("classify.errored", {
2995
+ node: node.id,
2996
+ error: err instanceof Error ? err.message : String(err)
2997
+ });
2998
+ return fallback();
2999
+ }
3000
+ };
3001
+ routerExecutor = async (node, state, deps) => {
3002
+ const resolver = deps.resolvers?.classify;
3003
+ const branches = Array.isArray(node.config.branches) ? node.config.branches.filter((b) => b && typeof b.id === "string" && typeof b.label === "string").map((b) => ({
3004
+ id: b.id,
3005
+ // Match on the rich skill description when present; fall back to the label.
3006
+ label: typeof b.description === "string" && b.description.trim() ? b.description : b.label
3007
+ })) : [];
3008
+ const options = branches.filter((b) => b.id !== ELSE_BRANCH_ID);
3009
+ const fallback = () => branches.some((b) => b.id === ELSE_BRANCH_ID) ? { kind: "take", port: ELSE_BRANCH_ID } : { kind: "advance" };
3010
+ if (!resolver || options.length === 0) {
3011
+ deps.events.note("router.unavailable", { node: node.id });
3012
+ return fallback();
3013
+ }
3014
+ deps.events.note("router.start", { node: node.id, skills: options.length });
3015
+ try {
3016
+ const choice = await resolver({
3017
+ instructions: str(node.config.instructions),
3018
+ options,
3019
+ ...state.lastUtterance !== void 0 ? { lastUtterance: state.lastUtterance } : {},
3020
+ slots: deps.processRt.getData()
3021
+ });
3022
+ if (!choice) {
3023
+ deps.events.note("router.undecided", { node: node.id });
3024
+ return fallback();
3025
+ }
3026
+ state.nodeResults[node.id] = { choice };
3027
+ deps.events.note("router.routed", { node: node.id, choice });
3028
+ return routeByPort(node, deps, choice);
3029
+ } catch (err) {
3030
+ deps.events.note("router.errored", {
3031
+ node: node.id,
3032
+ error: err instanceof Error ? err.message : String(err)
3033
+ });
3034
+ return fallback();
3035
+ }
3036
+ };
3037
+ playbookExecutor = async (node, state, deps) => {
3038
+ const cfg = node.config;
3039
+ const out = deps.model.out(node.id);
3040
+ const conditional = out.filter((e) => e.condition && e.condition.trim());
3041
+ const terminal = out.length === 0;
3042
+ const policy = historyPolicyOf(node);
3043
+ const required = Array.isArray(cfg.requiredFields) ? cfg.requiredFields.filter((x) => typeof x === "string") : [];
3044
+ const goal = str(cfg.goal);
3045
+ let instr = str(cfg.instructions);
3046
+ const toolNames = Array.isArray(cfg.tools) ? cfg.tools.filter((x) => typeof x === "string" && x.length > 0) : [];
3047
+ let model = str(cfg.model) || void 0;
3048
+ let temperature = typeof cfg.temperature === "number" ? cfg.temperature : void 0;
3049
+ let libraryExits = [];
3050
+ const ref = str(cfg.playbookRef);
3051
+ if (ref && !cfg.pinned && deps.resolvers?.playbooks) {
3052
+ const resolved = await deps.resolvers.playbooks(ref);
3053
+ if (resolved) {
3054
+ if (!instr) instr = resolved.instructions;
3055
+ for (const f of resolved.requiredFields) if (!required.includes(f)) required.push(f);
3056
+ for (const t of resolved.tools ?? []) if (!toolNames.includes(t)) toolNames.push(t);
3057
+ if (!model && resolved.model) model = resolved.model;
3058
+ if (temperature === void 0 && resolved.temperature !== void 0) temperature = resolved.temperature;
3059
+ libraryExits = resolved.exits ?? [];
3060
+ }
3061
+ }
3062
+ const branches = branchOptionsOf(cfg);
3063
+ const base = interpolate(
3064
+ [goal ? `Goal: ${goal}` : "", instr].filter(Boolean).join("\n\n"),
3065
+ scopeFor(state, deps)
3066
+ ) || "Help the caller accomplish their goal. Keep replies short and natural.";
3067
+ const runner = deps.resolvers?.agentRunner;
3068
+ const registry = deps.resolvers?.registryTools;
3069
+ const useRunner = runner !== void 0 && (toolNames.length > 0 || cfg.agentLoop === true);
3070
+ let agentTools = [];
3071
+ if (useRunner && registry && toolNames.length > 0) {
3072
+ try {
3073
+ const catalog = await registry.catalog();
3074
+ agentTools = catalog.filter((t) => toolNames.includes(t.name));
3075
+ const missing = toolNames.filter((n) => !agentTools.some((t) => t.name === n));
3076
+ if (missing.length) deps.events.note("playbook.tools_missing", { node: node.id, missing });
3077
+ } catch {
3078
+ deps.events.note("playbook.tools_missing", { node: node.id, missing: toolNames });
3079
+ }
3080
+ }
3081
+ const exitOptions = branches.filter((b) => b.id !== ELSE_BRANCH_ID && b.id !== INCOMPLETE_PORT_ID).map((b) => ({
3082
+ id: b.id,
3083
+ description: libraryExits.find((e) => e.id === b.id)?.description ?? b.description ?? b.label
3084
+ }));
3085
+ const transcript = [];
3086
+ for (let turn = 0; turn < LLM_MAX_TURNS; turn++) {
3087
+ const collected = formatCollected(deps.processRt.getData());
3088
+ const missingNow = required.filter((f) => !hasSlot(deps.processRt.getData(), f));
3089
+ const parts = [base];
3090
+ if (collected) parts.push(`Information collected so far:
3091
+ ${collected}`);
3092
+ if (missingNow.length) {
3093
+ parts.push(
3094
+ `You still need to collect: ${missingNow.map((m) => m.trim()).join(", ")}. Do not conclude until you have these.`
3095
+ );
3096
+ }
3097
+ if (state.lastUtterance) parts.push(`The caller just said: "${state.lastUtterance}"`);
3098
+ let ranAgentTurn = false;
3099
+ let spokeThisTurn = false;
3100
+ if (useRunner && runner) {
3101
+ deps.busy?.begin();
3102
+ let turnResult;
3103
+ try {
3104
+ turnResult = await runner({
3105
+ instructions: parts.join("\n\n"),
3106
+ transcript,
3107
+ tools: agentTools,
3108
+ exits: exitOptions,
3109
+ captureVars: required,
3110
+ runTool: (name, args) => registry ? registry.run(name, args) : Promise.resolve({ ok: false, status: 0, data: { error: "no tool registry" } }),
3111
+ ...model ? { model } : {},
3112
+ ...temperature !== void 0 ? { temperature } : {}
3113
+ });
3114
+ } finally {
3115
+ deps.busy?.end();
3116
+ }
3117
+ if (turnResult) {
3118
+ ranAgentTurn = true;
3119
+ for (const call of turnResult.toolCalls) {
3120
+ state.toolResults[call.name] = { ok: call.ok, status: call.status, data: call.data };
3121
+ deps.events.note(call.ok ? "tool.succeeded" : "tool.errored", {
3122
+ node: node.id,
3123
+ name: call.name,
3124
+ status: call.status,
3125
+ agentLoop: true
3126
+ });
3127
+ }
3128
+ if (turnResult.say) {
3129
+ await speak(turnResult.say, deps, {
3130
+ allowInterruptions: cfg.interruptible !== false
3131
+ });
3132
+ transcript.push({ role: "assistant", text: turnResult.say });
3133
+ spokeThisTurn = true;
3134
+ }
3135
+ if (turnResult.exit) {
3136
+ for (const [k, v] of Object.entries(turnResult.exit.vars)) {
3137
+ deps.processRt.captureField(k, v);
3138
+ }
3139
+ const stillMissing2 = required.filter((f) => !hasSlot(deps.processRt.getData(), f));
3140
+ if (stillMissing2.length > 0) {
3141
+ deps.events.note("playbook.exit_blocked", {
3142
+ node: node.id,
3143
+ port: turnResult.exit.port,
3144
+ missing: stillMissing2.map((m) => m.trim())
3145
+ });
3146
+ parts.push(
3147
+ `You attempted to conclude, but these are still missing: ${stillMissing2.map((m) => m.trim()).join(", ")}. Continue the conversation and collect them.`
3148
+ );
3149
+ } else if (exitOptions.some((e) => e.id === turnResult.exit?.port) || deps.model.selectByPort(node.id, turnResult.exit.port) !== null) {
3150
+ deps.events.note("playbook.exit", { node: node.id, port: turnResult.exit.port, structured: true });
3151
+ return routeByPort(node, deps, turnResult.exit.port);
3152
+ } else {
3153
+ deps.events.note("playbook.exit_invalid_port", { node: node.id, port: turnResult.exit.port });
3154
+ }
3155
+ }
3156
+ }
3157
+ }
3158
+ if (!ranAgentTurn || !spokeThisTurn) {
3159
+ await withHistory(deps, policy, () => guardedGenerate(node, state, deps, parts.join("\n\n"), base));
3160
+ }
3161
+ const t = await nextTurn(deps);
3162
+ if ("result" in t) return t.result;
3163
+ state.lastUtterance = t.text;
3164
+ state.lastDtmf = t.dtmf;
3165
+ if (t.text.trim()) transcript.push({ role: "user", text: t.text });
3166
+ const stillMissing = required.filter((f) => !hasSlot(deps.processRt.getData(), f));
3167
+ if (stillMissing.length > 0) {
3168
+ deps.events.note("playbook.gated", { node: node.id, missing: stillMissing.map((m) => m.trim()) });
3169
+ continue;
3170
+ }
3171
+ const exitCtx = evalCtxOf(state, deps);
3172
+ const det = deterministicEdgeTarget(out, exitCtx);
3173
+ if (det !== null) {
3174
+ deps.events.note("playbook.exit", { node: node.id, to: det });
3175
+ return { kind: "goto", to: det };
3176
+ }
3177
+ if (useRunner) continue;
3178
+ if (branches.length > 0) {
3179
+ const choice = await resolveBranch(branches, exitCtx, { hint: goal || instr });
3180
+ if (choice) {
3181
+ deps.events.note("playbook.exit", { node: node.id, port: choice });
3182
+ return { kind: "take", port: choice };
3183
+ }
3184
+ if (branches.some((b) => b.id === ELSE_BRANCH_ID)) {
3185
+ deps.events.note("playbook.exit", { node: node.id, port: ELSE_BRANCH_ID });
3186
+ return { kind: "take", port: ELSE_BRANCH_ID };
3187
+ }
3188
+ } else if (conditional.length > 0) {
3189
+ const target = await selectEdge(out, exitCtx);
3190
+ if (target) {
3191
+ deps.events.note("playbook.exit", { node: node.id, to: target });
3192
+ return { kind: "goto", to: target };
3193
+ }
3194
+ }
3195
+ }
3196
+ deps.events.note("playbook.turn_cap", { node: node.id });
3197
+ const missingAtCap = required.filter((f) => !hasSlot(deps.processRt.getData(), f));
3198
+ if (missingAtCap.length > 0) {
3199
+ deps.events.note("playbook.incomplete", {
3200
+ node: node.id,
3201
+ missing: missingAtCap.map((m) => m.trim())
3202
+ });
3203
+ if (deps.model.selectByPort(node.id, INCOMPLETE_PORT_ID) !== null) {
3204
+ return { kind: "take", port: INCOMPLETE_PORT_ID };
3205
+ }
3206
+ await speak(
3207
+ interpolate(
3208
+ str(cfg.incompleteMessage) || "I wasn't able to get everything we need on this call, so let me have someone follow up with you. Thanks for your time.",
3209
+ scopeFor(state, deps)
3210
+ ),
3211
+ deps
3212
+ );
3213
+ return { kind: "end" };
3214
+ }
3215
+ return terminal ? { kind: "end" } : { kind: "advance" };
3216
+ };
3217
+ setExecutor = async (node, state, deps) => {
3218
+ const assignments = Array.isArray(node.config.assignments) ? node.config.assignments : [];
3219
+ let applied = 0;
3220
+ for (const a of assignments) {
3221
+ if (!a || typeof a.name !== "string" || !a.name.trim()) continue;
3222
+ const value = interpolate(typeof a.value === "string" ? a.value : "", scopeFor(state, deps));
3223
+ deps.processRt.captureField(a.name.trim(), value);
3224
+ applied += 1;
3225
+ }
3226
+ deps.events.note("set.applied", { node: node.id, count: applied });
3227
+ return { kind: "advance" };
3228
+ };
3229
+ MAX_SUBFLOW_DEPTH = 3;
3230
+ subflowExecutor = async (node, _state, deps) => {
3231
+ const ref = str(node.config.flowRef);
3232
+ const depth = deps.depth ?? 0;
3233
+ if (!ref || !deps.resolvers?.flows) {
3234
+ deps.events.note("subflow.skipped", { node: node.id, reason: ref ? "no_resolver" : "no_ref" });
3235
+ return { kind: "advance" };
3236
+ }
3237
+ if (depth >= MAX_SUBFLOW_DEPTH) {
3238
+ deps.events.note("subflow.depth_capped", { node: node.id, depth });
3239
+ return { kind: "advance" };
3240
+ }
3241
+ const program = await deps.resolvers.flows(ref);
3242
+ if (!program) {
3243
+ deps.events.note("subflow.unresolved", { node: node.id, ref });
3244
+ return { kind: "advance" };
3245
+ }
3246
+ deps.events.note("subflow.enter", { node: node.id, ref, depth: depth + 1 });
3247
+ const { runFlowGraph: runFlowGraph2 } = await Promise.resolve().then(() => (init_interpreter(), interpreter_exports));
3248
+ const outcome = await runFlowGraph2(program, {
3249
+ ctx: deps.ctx,
3250
+ processRt: deps.processRt,
3251
+ utterances: deps.utterances,
3252
+ events: deps.events,
3253
+ ...deps.history ? { history: deps.history } : {},
3254
+ ...deps.realtime ? { realtime: true } : {},
3255
+ ...deps.resolvers ? { resolvers: deps.resolvers } : {},
3256
+ depth: depth + 1
3257
+ });
3258
+ deps.events.note("subflow.exit", { node: node.id, ref, outcome: outcome.kind });
3259
+ if (outcome.kind === "ended" || outcome.kind === "hangup") return { kind: "end" };
3260
+ if (outcome.kind === "handoff") return { kind: "handoff", to: outcome.to };
3261
+ return { kind: "advance" };
3262
+ };
3263
+ passthroughExecutor = async () => ({
3264
+ kind: "advance"
3265
+ });
3266
+ EXECUTORS = {
3267
+ say: sayExecutor,
3268
+ confirm: confirmExecutor,
3269
+ ask: askExecutor,
3270
+ gate: gateExecutor,
3271
+ end: endExecutor,
3272
+ handoff: handoffExecutor,
3273
+ llm: llmExecutor,
3274
+ tool: toolExecutor,
3275
+ set: setExecutor,
3276
+ subflow: subflowExecutor,
3277
+ decision: decisionExecutor,
3278
+ menu: menuExecutor,
3279
+ // Agentic / data nodes — execute by default (no feature flag; the platform is
3280
+ // pre-customer, so these ship on rather than dark). Each degrades to a recorded
3281
+ // no-op when its resolver/config is absent, so they never throw.
3282
+ rag: ragExecutor,
3283
+ code: codeExecutor,
3284
+ classify: classifyExecutor,
3285
+ router: routerExecutor,
3286
+ playbook: playbookExecutor
3287
+ // start (explicit entry marker) and route (routing-plane legacy) have no
3288
+ // executor — they pass through and the default edge / selectEdge routes.
3289
+ };
3290
+ }
3291
+ });
3292
+
3293
+ // src/runtime/graph/interpreter.ts
3294
+ var interpreter_exports = {};
3295
+ __export(interpreter_exports, {
3296
+ runFlowGraph: () => runFlowGraph
3297
+ });
3298
+ function pickEntry(model) {
3299
+ return model.entry.length > 0 ? model.entry[0] : null;
3300
+ }
3301
+ async function selectNext(model, from, result, state, deps) {
3302
+ if (result.kind === "goto") return result.to;
3303
+ if (result.kind === "take") {
3304
+ const target = model.selectByPort(from, result.port);
3305
+ if (target !== null) return target;
3306
+ const def = model.out(from).find((e) => (!e.condition || !e.condition.trim()) && !e.sourceHandle);
3307
+ return def ? def.to : null;
3308
+ }
3309
+ return selectEdge(model.out(from), {
3310
+ slots: deps.processRt.getData(),
3311
+ tools: state.toolResults,
3312
+ nodeResults: state.nodeResults,
3313
+ ...state.lastUtterance !== void 0 ? { lastUtterance: state.lastUtterance } : {},
3314
+ ...state.lastDtmf !== void 0 ? { lastDtmf: state.lastDtmf } : {}
3315
+ });
3316
+ }
3317
+ async function runFlowGraph(program, deps) {
3318
+ const model = buildGraphModel(program);
3319
+ const maxNodeVisits = deps.maxNodeVisits ?? DEFAULT_MAX_NODE_VISITS;
3320
+ const maxSteps = deps.maxSteps ?? DEFAULT_MAX_STEPS;
3321
+ let cursor = pickEntry(model);
3322
+ if (cursor === null) return { kind: "completed" };
3323
+ const state = { toolResults: {}, nodeResults: {}, cursor };
3324
+ const visits = /* @__PURE__ */ new Map();
3325
+ let steps = 0;
3326
+ while (cursor !== null) {
3327
+ if (++steps > maxSteps) {
3328
+ deps.events.note("flow.loop_guard_tripped", { reason: "max_steps", steps });
3329
+ break;
3330
+ }
3331
+ const node = model.node(cursor);
3332
+ if (!node) {
3333
+ deps.events.note("flow.missing_node", { nodeId: cursor });
3334
+ break;
3335
+ }
3336
+ const visit = (visits.get(cursor) ?? 0) + 1;
3337
+ visits.set(cursor, visit);
3338
+ const visitCap = node.config.hub === true ? HUB_MAX_NODE_VISITS : maxNodeVisits;
3339
+ if (visit > visitCap) {
3340
+ deps.events.note("flow.loop_guard_tripped", { reason: "max_node_visits", nodeId: cursor });
3341
+ break;
3342
+ }
3343
+ state.cursor = cursor;
3344
+ deps.events.enter(node.id, node.type);
3345
+ console.log("[graph] enter", { node: node.id, type: node.type, step: steps });
3346
+ const executor = EXECUTORS[node.type] ?? passthroughExecutor;
3347
+ let result;
3348
+ try {
3349
+ result = await executor(node, state, {
3350
+ ctx: deps.ctx,
3351
+ processRt: deps.processRt,
3352
+ utterances: deps.utterances,
3353
+ events: deps.events,
3354
+ model,
3355
+ ...deps.history ? { history: deps.history } : {},
3356
+ ...deps.realtime ? { realtime: true } : {},
3357
+ ...deps.resolvers ? { resolvers: deps.resolvers } : {},
3358
+ ...deps.depth !== void 0 ? { depth: deps.depth } : {},
3359
+ ...deps.busy ? { busy: deps.busy } : {}
3360
+ });
3361
+ } catch (err) {
3362
+ const message = err instanceof Error ? err.message : String(err);
3363
+ if (/AgentSession is not running|session is clos/i.test(message)) {
3364
+ deps.events.note("node.session_closed", { nodeId: node.id });
3365
+ return { kind: "ended", nodeId: node.id };
3366
+ }
3367
+ deps.events.note("node.errored", { nodeId: node.id, error: message });
3368
+ const recovery = model.selectByPort(node.id, "error");
3369
+ if (recovery !== null) {
3370
+ deps.events.traverse(node.id, recovery);
3371
+ cursor = recovery;
3372
+ continue;
3373
+ }
3374
+ return { kind: "error", nodeId: node.id, message };
3375
+ }
3376
+ if (result.kind === "end") return { kind: "ended", nodeId: node.id };
3377
+ if (result.kind === "handoff") return { kind: "handoff", to: result.to };
3378
+ const next = await selectNext(model, cursor, result, state, deps);
3379
+ if (next !== null) deps.events.traverse(cursor, next);
3380
+ cursor = next;
3381
+ }
3382
+ return { kind: "completed" };
3383
+ }
3384
+ var DEFAULT_MAX_NODE_VISITS, DEFAULT_MAX_STEPS, HUB_MAX_NODE_VISITS;
3385
+ var init_interpreter = __esm({
3386
+ "src/runtime/graph/interpreter.ts"() {
3387
+ init_graph_model();
3388
+ init_executors();
3389
+ init_conditions();
3390
+ DEFAULT_MAX_NODE_VISITS = 25;
3391
+ DEFAULT_MAX_STEPS = 200;
3392
+ HUB_MAX_NODE_VISITS = 200;
3393
+ }
3394
+ });
3395
+
3396
+ // src/runtime/graph/index.ts
3397
+ init_interpreter();
3398
+
3399
+ // src/runtime/graph/utterances.ts
3400
+ var EVENT_UTTERANCE = "user_input_transcribed";
3401
+ var EVENT_CLOSE = "close";
3402
+ function createUtteranceStream(session, processRt, ctx, rails, prewarm, wantsDtmf) {
3403
+ const queue = [];
3404
+ let waiter = null;
3405
+ let hung = false;
3406
+ const deliver = (e) => {
3407
+ if (hung && e.kind !== "hangup") return;
3408
+ if (waiter) {
3409
+ const resolve = waiter;
3410
+ waiter = null;
3411
+ resolve(e);
3412
+ } else {
3413
+ queue.push(e);
3414
+ }
3415
+ };
3416
+ const onClose = () => {
3417
+ hung = true;
3418
+ if (waiter) {
3419
+ const resolve = waiter;
3420
+ waiter = null;
3421
+ resolve({ kind: "hangup" });
3422
+ }
3423
+ };
3424
+ const unsubs = [];
3425
+ if (ctx.adapter) {
3426
+ const a = ctx.adapter;
3427
+ unsubs.push(
3428
+ a.on("user_input", (e) => {
3429
+ if (!e.isFinal || hung) return;
3430
+ deliver({ kind: "utterance", text: e.transcript });
3431
+ })
3432
+ );
3433
+ unsubs.push(a.once("close", onClose));
3434
+ if (wantsDtmf) {
3435
+ if (typeof ctx.onDtmf === "function") {
3436
+ unsubs.push(
3437
+ ctx.onDtmf((ev) => {
3438
+ if (hung) return;
3439
+ const digit = typeof ev?.digit === "string" ? ev.digit : "";
3440
+ if (!digit) return;
3441
+ deliver({ kind: "dtmf", digit });
3442
+ })
3443
+ );
3444
+ } else {
3445
+ unsubs.push(
3446
+ a.on("dtmf", (e) => {
3447
+ if (hung || !e.digit) return;
3448
+ deliver({ kind: "dtmf", digit: e.digit });
3449
+ })
3450
+ );
3451
+ }
3452
+ }
3453
+ } else {
3454
+ const emitter = session;
3455
+ const onUtterance = (payload) => {
3456
+ const event = payload ?? {};
3457
+ if (!event.isFinal || hung) return;
3458
+ deliver({ kind: "utterance", text: typeof event.transcript === "string" ? event.transcript : "" });
3459
+ };
3460
+ emitter.on(EVENT_UTTERANCE, onUtterance);
3461
+ emitter.on(EVENT_CLOSE, onClose);
3462
+ unsubs.push(() => emitter.off(EVENT_UTTERANCE, onUtterance));
3463
+ unsubs.push(() => emitter.off(EVENT_CLOSE, onClose));
3464
+ if (wantsDtmf && typeof ctx.onDtmf === "function") {
3465
+ const unsubDtmf = ctx.onDtmf((ev) => {
3466
+ if (hung) return;
3467
+ const digit = typeof ev?.digit === "string" ? ev.digit : "";
3468
+ if (!digit) return;
3469
+ deliver({ kind: "dtmf", digit });
3470
+ });
3471
+ unsubs.push(unsubDtmf);
3472
+ }
3473
+ }
3474
+ function awaitRaw(timeoutMs) {
3475
+ return new Promise((resolve) => {
3476
+ let settled = false;
3477
+ const timer = timeoutMs && timeoutMs > 0 ? setTimeout(() => {
3478
+ if (settled) return;
3479
+ settled = true;
3480
+ waiter = null;
3481
+ resolve({ kind: "timeout" });
3482
+ }, timeoutMs) : null;
3483
+ waiter = (e) => {
3484
+ if (settled) return;
3485
+ settled = true;
3486
+ if (timer) clearTimeout(timer);
3487
+ resolve(e);
3488
+ };
3489
+ });
3490
+ }
3491
+ return {
3492
+ async next(opts) {
3493
+ for (; ; ) {
3494
+ let raw;
3495
+ if (queue.length > 0) {
3496
+ raw = queue.shift();
3497
+ } else if (hung) {
3498
+ return { kind: "hangup" };
3499
+ } else {
3500
+ raw = await awaitRaw(opts?.timeoutMs);
3501
+ }
3502
+ if (raw.kind === "hangup") return { kind: "hangup" };
3503
+ if (raw.kind === "timeout") return { kind: "timeout" };
3504
+ if (raw.kind === "dtmf") return { kind: "dtmf", digit: raw.digit };
3505
+ if (rails) {
3506
+ const outcome = await rails.intercept(raw.text, ctx);
3507
+ if (outcome.kind === "consumed") continue;
3508
+ if (outcome.kind === "control") return { kind: "control", result: outcome.result };
3509
+ }
3510
+ if (prewarm) prewarm(raw.text);
3511
+ try {
3512
+ await processRt.ingest(raw.text, ctx);
3513
+ } catch {
3514
+ }
3515
+ return { kind: "utterance", text: raw.text };
3516
+ }
3517
+ },
3518
+ close() {
3519
+ hung = true;
3520
+ for (const u of unsubs) u();
3521
+ }
3522
+ };
3523
+ }
3524
+
3525
+ // src/runtime/graph/history.ts
3526
+ function isConversational(it) {
3527
+ return it.type === "message" && (it.role === "user" || it.role === "assistant");
3528
+ }
3529
+ function prune(items, policy) {
3530
+ if (policy === "full") return items;
3531
+ const persona = items.filter((it) => !isConversational(it));
3532
+ if (policy === "slots-only") return persona;
3533
+ for (let i = items.length - 1; i >= 0; i--) {
3534
+ const it = items[i];
3535
+ if (it.type === "message" && it.role === "user") return [...persona, it];
3536
+ }
3537
+ return persona;
3538
+ }
3539
+ function createHistoryController(agent) {
3540
+ return {
3541
+ lastAssistantText() {
3542
+ if (!agent) return "";
3543
+ try {
3544
+ const items = agent.chatCtx.items;
3545
+ for (let i = items.length - 1; i >= 0; i--) {
3546
+ const it = items[i];
3547
+ if (it.type === "message" && it.role === "assistant") return it.textContent ?? "";
3548
+ }
3549
+ } catch {
3550
+ }
3551
+ return "";
3552
+ },
3553
+ async withPolicy(policy, gen) {
3554
+ if (policy === "full" || !agent) {
3555
+ await gen();
3556
+ return;
3557
+ }
3558
+ let saved;
3559
+ try {
3560
+ saved = agent.chatCtx.copy();
3561
+ const next = agent.chatCtx.copy();
3562
+ next.items = prune(next.items, policy);
3563
+ await agent.updateChatCtx(next);
3564
+ } catch (err) {
3565
+ console.warn("[graph] historyPolicy prune failed; using full history", {
3566
+ policy,
3567
+ err: err instanceof Error ? err.message : String(err)
3568
+ });
3569
+ await gen();
3570
+ return;
3571
+ }
3572
+ try {
3573
+ await gen();
3574
+ } finally {
3575
+ try {
3576
+ await agent.updateChatCtx(saved);
3577
+ } catch (err) {
3578
+ console.warn("[graph] historyPolicy restore failed", {
3579
+ err: err instanceof Error ? err.message : String(err)
3580
+ });
3581
+ }
3582
+ }
3583
+ }
3584
+ };
3585
+ }
3586
+
3587
+ // src/runtime/graph/index.ts
3588
+ init_embedder();
3589
+ init_conditions();
3590
+ init_interpreter();
3591
+
3592
+ // src/runtime/graph/events.ts
3593
+ function createGraphEvents(processRt) {
3594
+ let current = null;
3595
+ return {
3596
+ enter(nodeId, type) {
3597
+ current = nodeId;
3598
+ processRt.recordEvent({ kind: "node.entered", data: { nodeId, type } });
3599
+ },
3600
+ traverse(from, to, via) {
3601
+ processRt.recordEvent({
3602
+ kind: "edge.traversed",
3603
+ data: { from, to, ...via ? { via } : {} }
3604
+ });
3605
+ },
3606
+ note(kind, data) {
3607
+ processRt.recordEvent({ kind, ...data ? { data } : {} });
3608
+ },
3609
+ currentNodeId() {
3610
+ return current;
3611
+ }
3612
+ };
3613
+ }
3614
+
3615
+ // src/runtime/graph/index.ts
3616
+ init_conditions();
3617
+ init_branch_resolve();
3618
+ init_embedder();
3619
+
3620
+ // src/runtime/graph/agent-runner.ts
3621
+ init_openai_default();
3622
+ process.env["VOICELAYER_LLM_MODEL"] ?? process.env["OPENAI_LLM_MODEL"] ?? "gpt-4o";
3623
+ Number(process.env["VL_FLOW_AGENT_TURN_TIMEOUT_MS"] ?? "30000");
3624
+ Number(process.env["VL_FLOW_AGENT_MAX_TOOL_CALLS"] ?? "5");
3625
+
3626
+ // src/runtime/graph/index.ts
3627
+ init_drift();
3628
+ init_interpolate();
3629
+ init_graph_model();
3630
+
3631
+ // src/runtime/graph/resolvers.ts
3632
+ init_src();
3633
+ init_embedder();
3634
+ init_openai_default();
3635
+ Number(process.env["VL_FLOW_CODE_TIMEOUT_MS"] ?? "1000");
3636
+ Number(process.env["VL_FLOW_CODE_MEMORY_BYTES"] ?? String(16 * 1024 * 1024));
3637
+
3638
+ // src/runtime/graph/index.ts
3639
+ init_builtins();
3640
+ async function runFlowProgram(program, ctx, opts) {
3641
+ const embedder = getEmbedder();
3642
+ const wantsRouting = embedder.available && Object.values(program.nodes).some(
3643
+ (n) => n.out.some((e) => !!e.condition && isNaturalLanguageCondition(e.condition))
3644
+ );
3645
+ const prewarm = wantsRouting ? (text) => embedder.prewarm(text) : void 0;
3646
+ const wantsDtmf = Object.values(program.nodes).some(
3647
+ (n) => n.type === "menu" || n.out.some((e) => !!e.condition && e.condition.includes("dtmf:"))
3648
+ );
3649
+ const utterances = createUtteranceStream(
3650
+ opts.session,
3651
+ opts.processRt,
3652
+ ctx,
3653
+ opts.rails,
3654
+ prewarm,
3655
+ wantsDtmf
3656
+ );
3657
+ const history = createHistoryController(opts.agent ?? null);
3658
+ try {
3659
+ const outcome = await runFlowGraph(program, {
3660
+ ctx,
3661
+ processRt: opts.processRt,
3662
+ utterances,
3663
+ events: opts.events,
3664
+ history,
3665
+ ...opts.maxSteps !== void 0 ? { maxSteps: opts.maxSteps } : {},
3666
+ ...opts.maxNodeVisits !== void 0 ? { maxNodeVisits: opts.maxNodeVisits } : {},
3667
+ ...opts.realtime ? { realtime: true } : {},
3668
+ ...opts.resolvers ? { resolvers: opts.resolvers } : {},
3669
+ ...opts.busy ? { busy: opts.busy } : {}
3670
+ });
3671
+ console.log("[graph] outcome", outcome);
3672
+ return outcome;
3673
+ } finally {
3674
+ utterances.close();
3675
+ }
3676
+ }
3677
+
3678
+ // src/runtime/process.ts
3679
+ init_openai_default();
3680
+
3681
+ // ../primitives/process-schema/src/index.ts
3682
+ init_src();
3683
+ var PROCESS_LIFECYCLE_MAX_EVENTS = 100;
3684
+ setup({
3685
+ types: {},
3686
+ guards: {
3687
+ allRequiredCaptured: ({ context }) => context.requiredFields.every((n) => context.fieldsCaptured.has(n)),
3688
+ ackPending: ({ context }) => context.requiresAck && !context.backendAckReceived
3689
+ },
3690
+ actions: {
3691
+ applyLoad: ({ context, event }) => {
3692
+ if (event.type !== "LOAD") return;
3693
+ context.requiredFields = event.requiredFields;
3694
+ context.requiresAck = event.requiresAck;
3695
+ context.fieldsCaptured = /* @__PURE__ */ new Map();
3696
+ context.backendAckReceived = false;
3697
+ context.backendAckReceivedAt = void 0;
3698
+ },
3699
+ applyCapture: ({ context, event }) => {
3700
+ if (event.type !== "CAPTURE") return;
3701
+ context.fieldsCaptured.set(event.name, event.value);
3702
+ },
3703
+ applyAck: ({ context, event }) => {
3704
+ if (event.type !== "ACK") return;
3705
+ context.backendAckReceived = true;
3706
+ context.backendAckReceivedAt = event.receivedAt;
3707
+ },
3708
+ applyLifecycle: ({ context, event }) => {
3709
+ if (event.type !== "EMIT_LIFECYCLE") return;
3710
+ context.lifecycle.push(event.event);
3711
+ if (context.lifecycle.length > PROCESS_LIFECYCLE_MAX_EVENTS) {
3712
+ context.lifecycle.splice(
3713
+ 0,
3714
+ context.lifecycle.length - PROCESS_LIFECYCLE_MAX_EVENTS
3715
+ );
3716
+ }
3717
+ },
3718
+ resetRuntime: ({ context }) => {
3719
+ context.fieldsCaptured = /* @__PURE__ */ new Map();
3720
+ context.backendAckReceived = false;
3721
+ context.backendAckReceivedAt = void 0;
3722
+ context.lifecycle = [];
3723
+ }
3724
+ }
3725
+ }).createMachine({
3726
+ id: "process",
3727
+ initial: "unloaded",
3728
+ context: {
3729
+ fieldsCaptured: /* @__PURE__ */ new Map(),
3730
+ backendAckReceived: false,
3731
+ backendAckReceivedAt: void 0,
3732
+ requiredFields: [],
3733
+ requiresAck: false,
3734
+ lifecycle: []
3735
+ },
3736
+ // EMIT_LIFECYCLE is global — agents can record events in any state
3737
+ // (unloaded through completed) without forcing a transition. Defined at the
3738
+ // machine root so every state inherits it.
3739
+ on: {
3740
+ EMIT_LIFECYCLE: { actions: "applyLifecycle" }
3741
+ },
3742
+ states: {
3743
+ unloaded: {
3744
+ on: {
3745
+ LOAD: { target: "collecting", actions: "applyLoad" }
3746
+ }
3747
+ },
3748
+ collecting: {
3749
+ always: [{ guard: "allRequiredCaptured", target: "awaiting_ack" }],
3750
+ on: {
3751
+ CAPTURE: { actions: "applyCapture" },
3752
+ RESET: { actions: "resetRuntime", target: "collecting" },
3753
+ LOAD: { target: "collecting", actions: "applyLoad" }
3754
+ }
3755
+ },
3756
+ awaiting_ack: {
3757
+ always: [{ guard: "ackPending", target: "awaiting_ack" }, { target: "ready_to_close" }],
3758
+ on: {
3759
+ ACK: { actions: "applyAck" },
3760
+ CAPTURE: { actions: "applyCapture", target: "collecting" },
3761
+ RESET: { actions: "resetRuntime", target: "collecting" },
3762
+ LOAD: { target: "collecting", actions: "applyLoad" }
3763
+ }
3764
+ },
3765
+ ready_to_close: {
3766
+ on: {
3767
+ COMPLETE: { target: "completed" },
3768
+ CAPTURE: { actions: "applyCapture", target: "collecting" },
3769
+ ACK: { actions: "applyAck" },
3770
+ RESET: { actions: "resetRuntime", target: "collecting" },
3771
+ LOAD: { target: "collecting", actions: "applyLoad" }
3772
+ }
3773
+ },
3774
+ completed: {
3775
+ type: "final"
3776
+ }
3777
+ }
3778
+ });
3779
+
3780
+ // src/runtime/process.ts
3781
+ function createProcessRuntime(def) {
3782
+ {
3783
+ return {
3784
+ augmentPrompt: (p) => p,
3785
+ ingest: async () => {
3786
+ },
3787
+ getData: () => ({}),
3788
+ isComplete: () => false,
3789
+ captureField: () => false,
3790
+ recordEvent: () => {
3791
+ },
3792
+ getSnapshot: () => null,
3793
+ getSchema: () => null,
3794
+ subscribe: () => () => {
3795
+ }
3796
+ };
3797
+ }
3798
+ }
3799
+
3800
+ // src/runtime/session-adapter.ts
3801
+ var LK_AGENT_STATE_MAP = {
3802
+ initializing: "initializing",
3803
+ idle: "idle",
3804
+ listening: "listening",
3805
+ thinking: "thinking",
3806
+ speaking: "speaking"
3807
+ };
3808
+ var NON_BUSY_STATES = /* @__PURE__ */ new Set([
3809
+ "idle",
3810
+ "listening"
3811
+ ]);
3812
+ function normalizeState(raw) {
3813
+ return LK_AGENT_STATE_MAP[raw] ?? "thinking";
3814
+ }
3815
+
3816
+ // src/runtime/session-adapter-text.ts
3817
+ function createTextSessionAdapter(transport) {
3818
+ const listeners = /* @__PURE__ */ new Map();
3819
+ let userInputListeners = 0;
3820
+ let inboundActive = false;
3821
+ let inboundSink = null;
3822
+ let inboundEnd = null;
3823
+ let currentState = "initializing";
3824
+ let currentRaw = "initializing";
3825
+ let closed = false;
3826
+ const idleWaiters = /* @__PURE__ */ new Set();
3827
+ const closeResolvers = /* @__PURE__ */ new Set();
3828
+ function fire(event, payload) {
3829
+ const set = listeners.get(event);
3830
+ if (!set) return;
3831
+ for (const handler of [...set]) handler(payload);
3832
+ }
3833
+ function releaseIdleWaiters(reason) {
3834
+ for (const w of [...idleWaiters]) w(reason);
3835
+ idleWaiters.clear();
3836
+ }
3837
+ function on(event, handler) {
3838
+ if (event === "user_input" && inboundActive) {
3839
+ throw new Error('on("user_input") is mutually exclusive with an active inbound() iterator');
3840
+ }
3841
+ let set = listeners.get(event);
3842
+ if (!set) {
3843
+ set = /* @__PURE__ */ new Set();
3844
+ listeners.set(event, set);
3845
+ }
3846
+ const erased = handler;
3847
+ set.add(erased);
3848
+ if (event === "user_input") userInputListeners += 1;
3849
+ return () => {
3850
+ if (set.delete(erased) && event === "user_input") userInputListeners -= 1;
3851
+ };
3852
+ }
3853
+ function once(event, handler) {
3854
+ const off = on(event, (payload) => {
3855
+ off();
3856
+ handler(payload);
3857
+ });
3858
+ return off;
3859
+ }
3860
+ function inbound(opts) {
3861
+ if (userInputListeners > 0) {
3862
+ throw new Error('inbound() is mutually exclusive with an on("user_input") listener');
3863
+ }
3864
+ if (inboundActive) throw new Error("inbound() iterator already active");
3865
+ inboundActive = true;
3866
+ const idleTimeoutMs = opts?.idleTimeoutMs;
3867
+ const buffer = [];
3868
+ let pending = null;
3869
+ let finished = false;
3870
+ let idleTimer = null;
3871
+ const clearIdle = () => {
3872
+ if (idleTimer !== null) {
3873
+ clearTimeout(idleTimer);
3874
+ idleTimer = null;
3875
+ }
3876
+ };
3877
+ const armIdle = () => {
3878
+ clearIdle();
3879
+ if (typeof idleTimeoutMs === "number" && idleTimeoutMs > 0 && !finished) {
3880
+ idleTimer = setTimeout(() => deliver({ kind: "timeout" }), idleTimeoutMs);
3881
+ }
3882
+ };
3883
+ const deliver = (e) => {
3884
+ if (finished) return;
3885
+ if (e.kind === "partial") return;
3886
+ if (pending) {
3887
+ const p = pending;
3888
+ pending = null;
3889
+ p({ value: e, done: false });
3890
+ } else {
3891
+ buffer.push(e);
3892
+ }
3893
+ if (e.kind !== "timeout") armIdle();
3894
+ };
3895
+ const end = () => {
3896
+ if (finished) return;
3897
+ finished = true;
3898
+ clearIdle();
3899
+ if (pending) {
3900
+ const p = pending;
3901
+ pending = null;
3902
+ p({ value: void 0, done: true });
3903
+ }
3904
+ };
3905
+ const teardown = () => {
3906
+ finished = true;
3907
+ clearIdle();
3908
+ if (inboundSink === deliver) inboundSink = null;
3909
+ if (inboundEnd === end) inboundEnd = null;
3910
+ inboundActive = false;
3911
+ };
3912
+ inboundSink = deliver;
3913
+ inboundEnd = end;
3914
+ if (closed) deliver({ kind: "hangup" });
3915
+ armIdle();
3916
+ return {
3917
+ next() {
3918
+ if (buffer.length > 0) {
3919
+ const value = buffer.shift();
3920
+ return Promise.resolve({ value, done: false });
3921
+ }
3922
+ if (finished) {
3923
+ teardown();
3924
+ return Promise.resolve({ value: void 0, done: true });
3925
+ }
3926
+ return new Promise((resolve) => {
3927
+ pending = resolve;
3928
+ });
3929
+ },
3930
+ return() {
3931
+ teardown();
3932
+ return Promise.resolve({ value: void 0, done: true });
3933
+ },
3934
+ [Symbol.asyncIterator]() {
3935
+ return this;
3936
+ }
3937
+ };
3938
+ }
3939
+ const capabilities = {
3940
+ channel: "text",
3941
+ usingRealtime: false,
3942
+ hasStreamingTranscripts: false,
3943
+ canBargeIn: false,
3944
+ canReceiveDtmf: false,
3945
+ canPublishDtmf: false,
3946
+ canResolveSpeaker: true
3947
+ };
3948
+ const adapter = {
3949
+ capabilities,
3950
+ on,
3951
+ once,
3952
+ inbound,
3953
+ get state() {
3954
+ return currentState;
3955
+ },
3956
+ waitForIdle(timeoutMs) {
3957
+ if (closed) return Promise.resolve("closed");
3958
+ if (NON_BUSY_STATES.has(currentState)) return Promise.resolve("idle");
3959
+ return new Promise((resolve) => {
3960
+ let settled = false;
3961
+ const w = (reason) => {
3962
+ if (settled) return;
3963
+ settled = true;
3964
+ clearTimeout(t);
3965
+ idleWaiters.delete(w);
3966
+ resolve(reason);
3967
+ };
3968
+ const t = setTimeout(() => {
3969
+ if (settled) return;
3970
+ settled = true;
3971
+ idleWaiters.delete(w);
3972
+ resolve("timeout");
3973
+ }, timeoutMs);
3974
+ idleWaiters.add(w);
3975
+ });
3976
+ },
3977
+ waitForClose() {
3978
+ if (closed) return Promise.resolve();
3979
+ return new Promise((resolve) => closeResolvers.add(resolve));
3980
+ },
3981
+ start() {
3982
+ return Promise.resolve();
3983
+ },
3984
+ async say(text) {
3985
+ fire("agent_turn", { text });
3986
+ await transport.send(text);
3987
+ return { interrupted: false };
3988
+ },
3989
+ async generateReply(opts) {
3990
+ if (transport.generate) {
3991
+ const text = await transport.generate(opts?.instructions);
3992
+ if (text) {
3993
+ fire("agent_turn", { text });
3994
+ await transport.send(text);
3995
+ }
3996
+ }
3997
+ },
3998
+ interrupt() {
3999
+ return Promise.resolve();
4000
+ },
4001
+ sendDtmf() {
4002
+ return Promise.reject(new Error("sendDtmf unsupported on the text channel (canPublishDtmf=false)"));
4003
+ },
4004
+ endCall() {
4005
+ return Promise.resolve({ torn: "agent", stranded: false });
4006
+ },
4007
+ dispose() {
4008
+ if (inboundEnd) inboundEnd();
4009
+ listeners.clear();
4010
+ releaseIdleWaiters("closed");
4011
+ closeResolvers.clear();
4012
+ }
4013
+ };
4014
+ function setState(next) {
4015
+ const raw = String(next);
4016
+ const norm = normalizeState(raw);
4017
+ const oldState = currentState;
4018
+ const rawOld = currentRaw;
4019
+ currentState = norm;
4020
+ currentRaw = raw;
4021
+ fire("state", { oldState, newState: norm, rawOldState: rawOld, rawNewState: raw });
4022
+ if (NON_BUSY_STATES.has(norm)) releaseIdleWaiters("idle");
4023
+ }
4024
+ function push(text, opts) {
4025
+ if (closed || text.length === 0) return;
4026
+ const speakerId = opts?.speakerId ?? null;
4027
+ fire("user_input", { transcript: text, isFinal: true, speakerId });
4028
+ if (inboundSink) inboundSink({ kind: "final", text, speakerId, at: /* @__PURE__ */ new Date() });
4029
+ }
4030
+ function close() {
4031
+ if (closed) return;
4032
+ closed = true;
4033
+ if (inboundSink) inboundSink({ kind: "hangup" });
4034
+ if (inboundEnd) inboundEnd();
4035
+ fire("close", void 0);
4036
+ releaseIdleWaiters("closed");
4037
+ for (const resolve of [...closeResolvers]) resolve();
4038
+ closeResolvers.clear();
4039
+ }
4040
+ return { adapter, push, setState, close };
4041
+ }
4042
+
4043
+ // src/runtime/text-session.ts
4044
+ function buildTextContext(adapter, call, processRt) {
4045
+ return {
4046
+ call: {
4047
+ callId: call?.callId ?? "text-session",
4048
+ callerId: "text",
4049
+ to: null,
4050
+ startedAt: /* @__PURE__ */ new Date(),
4051
+ metadata: call?.metadata ?? {}
4052
+ },
4053
+ adapter,
4054
+ connectors: {},
4055
+ metadata: {},
4056
+ recordEvent: (event) => processRt.recordEvent(event),
4057
+ get process() {
4058
+ return { data: processRt.getData(), complete: processRt.isComplete() };
4059
+ },
4060
+ // Voice-only escape hatch is absent on text; left undefined.
4061
+ livekit: { session: {}, job: {} }
4062
+ };
4063
+ }
4064
+ function runTextSession(opts) {
4065
+ const processRt = opts.processRt ?? createProcessRuntime();
4066
+ const handle = createTextSessionAdapter({
4067
+ send: opts.onAgentText,
4068
+ ...opts.generate ? { generate: opts.generate } : {}
4069
+ });
4070
+ const ctx = buildTextContext(handle.adapter, opts.call, processRt);
4071
+ const base = createGraphEvents(processRt);
4072
+ const onTrace = opts.onTrace;
4073
+ const events = onTrace ? {
4074
+ enter(nodeId, type) {
4075
+ onTrace({ kind: "node.entered", data: { nodeId, type } });
4076
+ base.enter(nodeId, type);
4077
+ },
4078
+ traverse(from, to, via) {
4079
+ onTrace({ kind: "edge.traversed", data: { from, to, ...via ? { via } : {} } });
4080
+ base.traverse(from, to, via);
4081
+ },
4082
+ note(kind, data) {
4083
+ onTrace({ kind, data: data ?? {} });
4084
+ base.note(kind, data);
4085
+ },
4086
+ currentNodeId: () => base.currentNodeId()
4087
+ } : base;
4088
+ const result = runFlowProgram(opts.program, ctx, {
4089
+ processRt,
4090
+ // createUtteranceStream prefers ctx.adapter; the session arg is unused here.
4091
+ session: {},
4092
+ events,
4093
+ ...opts.maxSteps !== void 0 ? { maxSteps: opts.maxSteps } : {}
4094
+ });
4095
+ return {
4096
+ sendUserMessage: (text) => handle.push(text),
4097
+ result,
4098
+ end: () => handle.close()
4099
+ };
4100
+ }
4101
+ async function runTextTranscript(opts) {
4102
+ const replies = [];
4103
+ const trace = [];
4104
+ const session = runTextSession({
4105
+ program: opts.program,
4106
+ onAgentText: (t) => void replies.push(t),
4107
+ onTrace: (e) => void trace.push(e),
4108
+ ...opts.processRt ? { processRt: opts.processRt } : {},
4109
+ ...opts.generate ? { generate: opts.generate } : {},
4110
+ ...opts.call ? { call: opts.call } : {},
4111
+ ...opts.maxSteps !== void 0 ? { maxSteps: opts.maxSteps } : {}
4112
+ });
4113
+ for (const m of opts.messages) session.sendUserMessage(m);
4114
+ session.end();
4115
+ const outcome = await session.result;
4116
+ return { replies, outcome, trace };
4117
+ }
4118
+
4119
+ export { runTextSession, runTextTranscript };