@voicelayer/sdk 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import OpenAI from 'openai';
2
+ import OpenAI2 from 'openai';
3
3
  import IORedis3 from 'ioredis';
4
4
  import os from 'os';
5
5
  import { Resend } from 'resend';
@@ -113,6 +113,9 @@ var init_flow = __esm({
113
113
  FlowStatus = z.enum(["draft", "published", "archived"]);
114
114
  z.enum(["shared", "dedicated"]);
115
115
  FlowNodeType = z.enum([
116
+ // Explicit entry marker — the interpreter starts here (falls back to the
117
+ // first indegree-0 node for older graphs without one).
118
+ "start",
116
119
  "say",
117
120
  "ask",
118
121
  "confirm",
@@ -203,9 +206,12 @@ var init_flow = __esm({
203
206
  });
204
207
  z.object({
205
208
  name: z.string().trim().min(1).max(200).optional(),
206
- graph: FlowGraph.optional()
207
- }).refine((b) => b.name !== void 0 || b.graph !== void 0, {
208
- message: "provide at least one of `name` or `graph`"
209
+ graph: FlowGraph.optional(),
210
+ // Framework is a reversible default-structure switch (Voiceflow parity):
211
+ // changing kind re-frames the palette/deployability but never touches the graph.
212
+ kind: FlowKind.optional()
213
+ }).refine((b) => b.name !== void 0 || b.graph !== void 0 || b.kind !== void 0, {
214
+ message: "provide at least one of `name`, `graph`, or `kind`"
209
215
  });
210
216
  z.object({
211
217
  note: z.string().max(1e3).optional()
@@ -506,7 +512,12 @@ var init_call_events = __esm({
506
512
  // Emitted by the agent's built-in `ask_host` tool. The dashboard and MCP
507
513
  // host SSE clients filter on these to surface the "let me check" UX.
508
514
  "consultation.requested",
509
- "consultation.resolved"
515
+ "consultation.resolved",
516
+ // DTMF keypad events. `dtmf.received` fires when a remote participant
517
+ // presses a key on their leg; `dtmf.sent` fires when our agent (or a
518
+ // programmatic caller via /v1/calls/:id/dtmf) emits one outbound.
519
+ "dtmf.received",
520
+ "dtmf.sent"
510
521
  ]);
511
522
  z.object({
512
523
  reason: z.string().optional(),
@@ -526,6 +537,11 @@ var init_call_events = __esm({
526
537
  // through" confirmation in the host chat).
527
538
  correlationId: z.string().optional()
528
539
  });
540
+ z.object({
541
+ digit: z.string().regex(/^[0-9*#A-D]$/),
542
+ code: z.number().int().min(0).max(15),
543
+ participantId: z.string().optional()
544
+ });
529
545
  z.object({
530
546
  totalUsd: z.string(),
531
547
  breakdown: z.object({
@@ -1449,6 +1465,36 @@ var init_project_limits = __esm({
1449
1465
  });
1450
1466
  }
1451
1467
  });
1468
+
1469
+ // ../contracts/src/flow-ports.ts
1470
+ var init_flow_ports = __esm({
1471
+ "../contracts/src/flow-ports.ts"() {
1472
+ }
1473
+ });
1474
+ var FlowIssueSeverity, FlowIssue;
1475
+ var init_flow_validate = __esm({
1476
+ "../contracts/src/flow-validate.ts"() {
1477
+ init_flow_ports();
1478
+ FlowIssueSeverity = z.enum(["error", "warning"]);
1479
+ FlowIssue = z.object({
1480
+ code: z.string().max(64),
1481
+ severity: FlowIssueSeverity,
1482
+ message: z.string().max(500),
1483
+ nodeId: z.string().max(128).optional(),
1484
+ edgeId: z.string().max(128).optional()
1485
+ });
1486
+ z.object({
1487
+ valid: z.boolean(),
1488
+ issues: z.array(FlowIssue).max(256)
1489
+ });
1490
+ }
1491
+ });
1492
+
1493
+ // ../contracts/src/flow-builtins.ts
1494
+ var init_flow_builtins = __esm({
1495
+ "../contracts/src/flow-builtins.ts"() {
1496
+ }
1497
+ });
1452
1498
  var ComplianceRule, ComplianceDecision, ComplianceConfig;
1453
1499
  var init_compliance = __esm({
1454
1500
  "../contracts/src/compliance.ts"() {
@@ -1509,6 +1555,21 @@ var init_compliance = __esm({
1509
1555
  ComplianceConfig.partial();
1510
1556
  }
1511
1557
  });
1558
+ var init_call_recording = __esm({
1559
+ "../contracts/src/call-recording.ts"() {
1560
+ z.object({
1561
+ // When false, the call-event sink skips egress entirely — no recording is made.
1562
+ recordingEnabled: z.boolean().default(true),
1563
+ // Optional consent preamble the agent speaks first on every call. null/empty =
1564
+ // no announcement. Capped so it stays a short notice, not a monologue.
1565
+ consentAnnouncement: z.string().max(1e3).nullable().default(null)
1566
+ });
1567
+ z.object({
1568
+ recordingEnabled: z.boolean().optional(),
1569
+ consentAnnouncement: z.string().max(1e3).nullable().optional()
1570
+ });
1571
+ }
1572
+ });
1512
1573
  var OutboundAmdMode;
1513
1574
  var init_outbound = __esm({
1514
1575
  "../contracts/src/outbound.ts"() {
@@ -1644,11 +1705,19 @@ function asAdherence(value) {
1644
1705
  return typeof value === "string" && ADHERENCE_MODES.has(value) ? value : "off";
1645
1706
  }
1646
1707
  function branchesFor(data) {
1647
- return Array.isArray(data.branches) ? data.branches.filter((b) => b && typeof b.id === "string" && typeof b.label === "string").map((b) => ({ id: b.id, label: b.label })) : [];
1708
+ return Array.isArray(data.branches) ? data.branches.filter((b) => b && typeof b.id === "string" && typeof b.label === "string").map((b) => ({
1709
+ id: b.id,
1710
+ label: b.label,
1711
+ ...typeof b.when === "string" && b.when.trim() ? { when: b.when } : {},
1712
+ ...typeof b.digit === "string" && b.digit.trim() ? { digit: b.digit.trim() } : {},
1713
+ ...typeof b.description === "string" && b.description.trim() ? { description: b.description } : {}
1714
+ })) : [];
1648
1715
  }
1649
1716
  function nodeConfigFor(node) {
1650
1717
  const data = node.data;
1651
1718
  switch (node.type) {
1719
+ case "start":
1720
+ return {};
1652
1721
  case "say":
1653
1722
  case "confirm":
1654
1723
  return { text: typeof data.text === "string" ? data.text : "" };
@@ -1824,11 +1893,15 @@ function compileFlowToProgram(graph, id = "flow") {
1824
1893
  id: node.id,
1825
1894
  type: node.type,
1826
1895
  config: nodeConfigFor(node),
1827
- out: outBySource.get(node.id) ?? []
1896
+ // Defensive: the FlowProgram schema caps out[] at 64 — slice rather than
1897
+ // letting an over-wired node fail the read-side parse (which silently
1898
+ // degrades the whole call to the generic base prompt).
1899
+ out: (outBySource.get(node.id) ?? []).slice(0, 64)
1828
1900
  };
1829
1901
  if (UNSUPPORTED_NODE_TYPES.has(node.type)) unsupportedNodes.push(node.id);
1830
1902
  }
1831
- let entry = stepNodes.filter((n) => (indeg.get(n.id) ?? 0) === 0).map((n) => n.id);
1903
+ const startNodes = stepNodes.filter((n) => n.type === "start").map((n) => n.id);
1904
+ let entry = startNodes.length ? startNodes : stepNodes.filter((n) => (indeg.get(n.id) ?? 0) === 0).map((n) => n.id);
1832
1905
  if (entry.length === 0 && stepNodes.length > 0) entry = [stepNodes[0].id];
1833
1906
  const { acyclic } = planEnrichment(graph);
1834
1907
  const { completionGate } = compileConversationFlow(graph, id);
@@ -1838,7 +1911,13 @@ function compileFlowToProgram(graph, id = "flow") {
1838
1911
  entry: entry.slice(0, 64),
1839
1912
  nodes,
1840
1913
  completionGate: { requiredFields: completionGate.requiredFields.slice(0, 64) },
1841
- diagnostics: { acyclic, droppedEdges, unsupportedNodes }
1914
+ // Diagnostics arrays are schema-capped at 256 — slice so a pathological
1915
+ // graph can't fail the read-side parse.
1916
+ diagnostics: {
1917
+ acyclic,
1918
+ droppedEdges: droppedEdges.slice(0, 256),
1919
+ unsupportedNodes: unsupportedNodes.slice(0, 256)
1920
+ }
1842
1921
  };
1843
1922
  }
1844
1923
  var FIELD_TYPES, HTTP_METHODS, HISTORY_POLICIES, ADHERENCE_MODES, UNSUPPORTED_NODE_TYPES, CAPABILITY_NODE_TYPES, ANNOTATION_NODE_TYPES;
@@ -2031,7 +2110,11 @@ var init_src = __esm({
2031
2110
  init_project_limits();
2032
2111
  init_flow();
2033
2112
  init_flow_program();
2113
+ init_flow_ports();
2114
+ init_flow_validate();
2115
+ init_flow_builtins();
2034
2116
  init_compliance();
2117
+ init_call_recording();
2035
2118
  init_outbound();
2036
2119
  init_connection();
2037
2120
  init_flow_compile();
@@ -2178,11 +2261,13 @@ function buildBuiltinScope(deps) {
2178
2261
  const nowIso = (/* @__PURE__ */ new Date()).toISOString();
2179
2262
  const today = nowIso.slice(0, 10);
2180
2263
  const callerId = typeof call?.callerId === "string" ? call.callerId : "";
2264
+ const agentName = deps.ctx.metadata?.["agentName"];
2181
2265
  return {
2182
2266
  caller_id: callerId,
2183
2267
  from: callerId,
2184
2268
  to: typeof call?.to === "string" ? call.to : "",
2185
2269
  call_id: typeof call?.callId === "string" ? call.callId : "",
2270
+ agent_name: typeof agentName === "string" ? agentName : "",
2186
2271
  channel: "voice",
2187
2272
  now: nowIso,
2188
2273
  today,
@@ -2192,6 +2277,7 @@ function buildBuiltinScope(deps) {
2192
2277
  }
2193
2278
  var init_builtins = __esm({
2194
2279
  "src/runtime/graph/builtins.ts"() {
2280
+ init_src();
2195
2281
  }
2196
2282
  });
2197
2283
  function dotProduct(a, b) {
@@ -2222,7 +2308,7 @@ function createOpenAIEmbedder() {
2222
2308
  const controller = new AbortController();
2223
2309
  const timer = setTimeout(() => controller.abort(), EMBED_TIMEOUT_MS);
2224
2310
  try {
2225
- client ??= new OpenAI();
2311
+ client ??= new OpenAI2();
2226
2312
  const res = await client.embeddings.create(
2227
2313
  { model: EMBED_MODEL, input: key },
2228
2314
  { signal: controller.signal }
@@ -2264,7 +2350,7 @@ var init_embedder = __esm({
2264
2350
  async function selectEdge(edges, ctx) {
2265
2351
  if (edges.length === 0) return null;
2266
2352
  const conditional = edges.filter((e) => e.condition && e.condition.trim());
2267
- const fallthrough = edges.find((e) => !e.condition || !e.condition.trim());
2353
+ const fallthrough = edges.find((e) => (!e.condition || !e.condition.trim()) && !e.sourceHandle);
2268
2354
  const nl = [];
2269
2355
  for (const edge of conditional) {
2270
2356
  const det = evaluateDeterministicCondition(edge.condition, ctx);
@@ -2373,12 +2459,28 @@ function evaluateDeterministic(atom, ctx) {
2373
2459
  if (ctx.lastDtmf === void 0) return void 0;
2374
2460
  return ctx.lastDtmf === dtmfMatch[1].trim();
2375
2461
  }
2376
- const opMatch = atom.match(/^(!?)\s*(?:slot:)?(.+?)\s*(==|!=|=|contains)\s*(.+)$/i);
2462
+ const opMatch = atom.match(
2463
+ /^(!?)\s*(?:slot:)?(.+?)\s*(==|!=|=|contains|starts_with|greater_than|less_than|>=|<=|>|<)\s*(.+)$/i
2464
+ );
2377
2465
  if (opMatch) {
2378
2466
  const [, neg, nameRaw, op, valRaw] = opMatch;
2379
- const value = String(resolvePath(ctx.slots, nameRaw.trim()) ?? "").toLowerCase();
2467
+ const raw = resolvePath(ctx.slots, nameRaw.trim());
2468
+ const value = String(raw ?? "").toLowerCase();
2380
2469
  const target = valRaw.trim().toLowerCase();
2381
- const res = op.toLowerCase() === "contains" ? value.includes(target) : op === "!=" ? value !== target : value === target;
2470
+ const opKey = op.toLowerCase();
2471
+ let res;
2472
+ if (opKey === "contains") res = value.includes(target);
2473
+ else if (opKey === "starts_with") res = value.startsWith(target);
2474
+ else if (opKey === "greater_than" || opKey === ">" || opKey === ">=" || opKey === "less_than" || opKey === "<" || opKey === "<=") {
2475
+ const a = Number(String(raw ?? "").replace(/[^0-9.+-]/g, ""));
2476
+ const b = Number(valRaw.trim().replace(/[^0-9.+-]/g, ""));
2477
+ if (Number.isNaN(a) || Number.isNaN(b)) res = false;
2478
+ else if (opKey === "greater_than" || opKey === ">") res = a > b;
2479
+ else if (opKey === ">=") res = a >= b;
2480
+ else if (opKey === "less_than" || opKey === "<") res = a < b;
2481
+ else res = a <= b;
2482
+ } else if (opKey === "!=") res = value !== target;
2483
+ else res = value === target;
2382
2484
  return neg ? !res : res;
2383
2485
  }
2384
2486
  const slotPres = atom.match(/^(!?)\s*slot:(.+?)\s*(present|empty)?$/i);
@@ -2397,6 +2499,48 @@ function evaluateDeterministic(atom, ctx) {
2397
2499
  }
2398
2500
  return void 0;
2399
2501
  }
2502
+ async function pickOption(options, ctx, hint) {
2503
+ if (options.length === 0) return null;
2504
+ const collected = Object.entries(ctx.slots).filter(([, v]) => v !== void 0 && v !== null && v !== "").map(([k, v]) => `${k.trim()}: ${String(v)}`).join("; ");
2505
+ const list = options.map((o, i) => `${i + 1}. ${o.text}`).join("\n");
2506
+ const prompt = [
2507
+ "Pick which outcome best matches the current state of this voice conversation.",
2508
+ ...hint && hint.trim() ? [`What decides the path: ${hint.trim()}`] : [],
2509
+ `Information collected: ${collected || "(none)"}`,
2510
+ `The caller most recently said: "${ctx.lastUtterance ?? ""}"`,
2511
+ "Outcomes:",
2512
+ list,
2513
+ 'Answer with ONLY the outcome number, or "none" if nothing clearly matches.'
2514
+ ].join("\n");
2515
+ const controller = new AbortController();
2516
+ const timer = setTimeout(() => controller.abort(), JUDGE_TIMEOUT_MS);
2517
+ try {
2518
+ openaiClient ??= new OpenAI2();
2519
+ const res = await openaiClient.chat.completions.create(
2520
+ {
2521
+ model: process.env["VOICELAYER_JUDGE_MODEL"] ?? "gpt-4o-mini",
2522
+ temperature: 0,
2523
+ max_tokens: 4,
2524
+ messages: [
2525
+ { role: "system", content: 'Answer with only a number, or "none".' },
2526
+ { role: "user", content: prompt }
2527
+ ]
2528
+ },
2529
+ { signal: controller.signal }
2530
+ );
2531
+ const answer = (res.choices[0]?.message?.content ?? "").trim().toLowerCase();
2532
+ const n = Number.parseInt(answer.replace(/[^0-9]/g, ""), 10);
2533
+ if (!Number.isInteger(n) || n < 1 || n > options.length) return null;
2534
+ return options[n - 1]?.id ?? null;
2535
+ } catch (err) {
2536
+ console.warn("[graph] option pick failed; treating as undecided", {
2537
+ err: err instanceof Error ? err.message : String(err)
2538
+ });
2539
+ return null;
2540
+ } finally {
2541
+ clearTimeout(timer);
2542
+ }
2543
+ }
2400
2544
  async function judge(condition, ctx) {
2401
2545
  const collected = Object.entries(ctx.slots).filter(([, v]) => v !== void 0 && v !== null && v !== "").map(([k, v]) => `${k.trim()}: ${String(v)}`).join("; ");
2402
2546
  const prompt = [
@@ -2409,7 +2553,7 @@ async function judge(condition, ctx) {
2409
2553
  const controller = new AbortController();
2410
2554
  const timer = setTimeout(() => controller.abort(), JUDGE_TIMEOUT_MS);
2411
2555
  try {
2412
- openaiClient ??= new OpenAI();
2556
+ openaiClient ??= new OpenAI2();
2413
2557
  const res = await openaiClient.chat.completions.create(
2414
2558
  {
2415
2559
  model: process.env["VOICELAYER_JUDGE_MODEL"] ?? "gpt-4o-mini",
@@ -2443,6 +2587,65 @@ var init_conditions = __esm({
2443
2587
  }
2444
2588
  });
2445
2589
 
2590
+ // src/runtime/graph/branch-resolve.ts
2591
+ function branchOptionsOf(config) {
2592
+ if (!Array.isArray(config.branches)) return [];
2593
+ return config.branches.filter((b) => b && typeof b.id === "string" && typeof b.label === "string").map((b) => ({
2594
+ id: b.id,
2595
+ label: b.label,
2596
+ ...typeof b.when === "string" && b.when.trim() ? { when: b.when } : {},
2597
+ ...typeof b.digit === "string" && b.digit.trim() ? { digit: b.digit.trim() } : {}
2598
+ }));
2599
+ }
2600
+ async function resolveBranch(branches, ctx, opts) {
2601
+ const candidates = branches.filter((b) => b.id !== ELSE_BRANCH_ID);
2602
+ if (candidates.length === 0) return null;
2603
+ const nl = [];
2604
+ for (const b of candidates) {
2605
+ if (b.when && b.when.trim()) {
2606
+ const det = evaluateDeterministicCondition(b.when, ctx);
2607
+ if (det === true) return b.id;
2608
+ if (det === void 0) nl.push(b);
2609
+ } else {
2610
+ nl.push(b);
2611
+ }
2612
+ }
2613
+ if (nl.length === 0) return null;
2614
+ const utterance = ctx.lastUtterance?.trim();
2615
+ if (utterance) {
2616
+ const embedder = getEmbedder();
2617
+ if (embedder.available) {
2618
+ const uvec = await embedder.embed(utterance);
2619
+ if (uvec) {
2620
+ let best = null;
2621
+ for (const b of nl) {
2622
+ const bvec = await embedder.embed(b.when?.trim() || b.label);
2623
+ if (!bvec) continue;
2624
+ const score = dotProduct(uvec, bvec);
2625
+ if (!best || score > best.score) best = { id: b.id, score };
2626
+ }
2627
+ if (best && best.score >= ROUTE_THRESHOLD) {
2628
+ console.log("[graph] branch: embedding", { id: best.id, score: Number(best.score.toFixed(3)) });
2629
+ return best.id;
2630
+ }
2631
+ }
2632
+ }
2633
+ }
2634
+ return pickOption(
2635
+ nl.map((b) => ({ id: b.id, text: b.when?.trim() || b.label })),
2636
+ ctx,
2637
+ opts?.hint
2638
+ );
2639
+ }
2640
+ var ELSE_BRANCH_ID;
2641
+ var init_branch_resolve = __esm({
2642
+ "src/runtime/graph/branch-resolve.ts"() {
2643
+ init_conditions();
2644
+ init_embedder();
2645
+ ELSE_BRANCH_ID = "else";
2646
+ }
2647
+ });
2648
+
2446
2649
  // src/runtime/graph/drift.ts
2447
2650
  function adherenceOf(node) {
2448
2651
  const a = node.config.adherence;
@@ -2490,6 +2693,22 @@ function scopeFor(state, deps) {
2490
2693
  function str(value) {
2491
2694
  return typeof value === "string" ? value : "";
2492
2695
  }
2696
+ function evalCtxOf(state, deps) {
2697
+ return {
2698
+ slots: deps.processRt.getData(),
2699
+ tools: state.toolResults,
2700
+ nodeResults: state.nodeResults,
2701
+ ...state.lastUtterance !== void 0 ? { lastUtterance: state.lastUtterance } : {},
2702
+ ...state.lastDtmf !== void 0 ? { lastDtmf: state.lastDtmf } : {}
2703
+ };
2704
+ }
2705
+ function deterministicEdgeTarget(edges, ctx) {
2706
+ for (const e of edges) {
2707
+ if (!e.condition || !e.condition.trim()) continue;
2708
+ if (evaluateDeterministicCondition(e.condition, ctx) === true) return e.to;
2709
+ }
2710
+ return null;
2711
+ }
2493
2712
  async function speak(text, deps) {
2494
2713
  if (!text.trim()) return;
2495
2714
  const session = deps.ctx.livekit.session;
@@ -2526,6 +2745,13 @@ async function nextTurn(deps, reprompt) {
2526
2745
  return { text: evt.text };
2527
2746
  }
2528
2747
  }
2748
+ function detectYesNo(text) {
2749
+ const yes = YES_WORDS.test(text);
2750
+ const no = NO_WORDS.test(text);
2751
+ if (yes && !no) return "yes";
2752
+ if (no && !yes) return "no";
2753
+ return null;
2754
+ }
2529
2755
  function formatCollected(slots) {
2530
2756
  return Object.entries(slots).filter(([, v]) => v !== void 0 && v !== null && v !== "").map(([k, v]) => `${k.trim()}: ${String(v)}`).join("\n");
2531
2757
  }
@@ -2564,12 +2790,21 @@ ${intent}`
2564
2790
  function routeByPort(node, deps, port) {
2565
2791
  return deps.model.selectByPort(node.id, port) !== null ? { kind: "take", port } : { kind: "advance" };
2566
2792
  }
2567
- var ASK_MAX_CLARIFY, LLM_MAX_TURNS, HUB_MAX_TURNS, NO_INPUT_MS, sayExecutor, askExecutor, gateExecutor, endExecutor, handoffExecutor, llmExecutor, toolExecutor, ragExecutor, codeExecutor, classifyExecutor, routerExecutor, playbookExecutor, setExecutor, MAX_SUBFLOW_DEPTH, subflowExecutor, passthroughExecutor, EXECUTORS;
2793
+ function menuDigitOf(b) {
2794
+ if (b.digit && b.digit.trim()) return b.digit.trim();
2795
+ const m = b.label.match(/^\s*([0-9*#])\b/);
2796
+ return m ? m[1] : null;
2797
+ }
2798
+ function menuLabelOf(label) {
2799
+ return label.replace(/^\s*[0-9*#]\s*[-—–:.]*\s*/, "").trim() || label.trim();
2800
+ }
2801
+ 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;
2568
2802
  var init_executors = __esm({
2569
2803
  "src/runtime/graph/executors.ts"() {
2570
2804
  init_interpolate();
2571
2805
  init_builtins();
2572
2806
  init_conditions();
2807
+ init_branch_resolve();
2573
2808
  init_drift();
2574
2809
  init_define_tool();
2575
2810
  ASK_MAX_CLARIFY = 4;
@@ -2580,6 +2815,42 @@ var init_executors = __esm({
2580
2815
  await speak(interpolate(str(node.config.text), scopeFor(state, deps)), deps);
2581
2816
  return { kind: "advance" };
2582
2817
  };
2818
+ YES_WORDS = /\b(yes|yeah|yep|yup|correct|right|exactly|sure|affirmative|that'?s right|sounds good|ok(?:ay)?)\b/i;
2819
+ NO_WORDS = /\b(no|nope|nah|wrong|incorrect|negative|not right|that'?s not|change|actually)\b/i;
2820
+ confirmExecutor = async (node, state, deps) => {
2821
+ const text = interpolate(str(node.config.text), scopeFor(state, deps));
2822
+ await speak(text || "Is that correct?", deps);
2823
+ for (let attempt = 0; attempt < 2; attempt++) {
2824
+ const t = await nextTurn(deps, text || "Is that correct?");
2825
+ if ("result" in t) return t.result;
2826
+ state.lastUtterance = t.text;
2827
+ state.lastDtmf = t.dtmf;
2828
+ let verdict = null;
2829
+ if (t.dtmf === "1") verdict = "yes";
2830
+ else if (t.dtmf === "2") verdict = "no";
2831
+ else if (t.text.trim()) verdict = detectYesNo(t.text);
2832
+ if (verdict === null && t.text.trim()) {
2833
+ verdict = await resolveBranch(
2834
+ [
2835
+ { id: "yes", label: "The caller confirms \u2014 the detail is correct." },
2836
+ { id: "no", label: "The caller says it is wrong or wants to change it." }
2837
+ ],
2838
+ evalCtxOf(state, deps)
2839
+ );
2840
+ }
2841
+ if (verdict === "yes") {
2842
+ deps.events.note("confirm.confirmed", { node: node.id });
2843
+ return routeByPort(node, deps, "yes");
2844
+ }
2845
+ if (verdict === "no") {
2846
+ deps.events.note("confirm.denied", { node: node.id });
2847
+ return routeByPort(node, deps, "no");
2848
+ }
2849
+ if (attempt === 0) await speak("Sorry \u2014 is that correct? Yes or no.", deps);
2850
+ }
2851
+ deps.events.note("confirm.unresolved", { node: node.id });
2852
+ return { kind: "advance" };
2853
+ };
2583
2854
  askExecutor = async (node, state, deps) => {
2584
2855
  const slot = str(node.config.slot) || node.id;
2585
2856
  if (hasSlot(deps.processRt.getData(), slot)) {
@@ -2686,11 +2957,60 @@ ${collected}`);
2686
2957
  toolExecutor = async (node, state, deps) => {
2687
2958
  const cfg = node.config;
2688
2959
  const name = str(cfg.name) || node.id;
2960
+ const routeByOutcome = () => {
2961
+ const ok = state.toolResults[node.id]?.ok === true;
2962
+ return routeByPort(node, deps, ok ? "ok" : "fail");
2963
+ };
2964
+ const url = str(cfg.url);
2965
+ if (!url) {
2966
+ state.toolResults[node.id] = { ok: false, status: 0, data: null };
2967
+ deps.events.note("tool.errored", { node: node.id, name, error: "no url" });
2968
+ return routeByOutcome();
2969
+ }
2970
+ const inputSpec = cfg.input && typeof cfg.input === "object" ? cfg.input : {};
2971
+ const slots = deps.processRt.getData();
2972
+ const bag = {};
2973
+ for (const field of Object.keys(inputSpec)) {
2974
+ const v = resolvePath(slots, field);
2975
+ if (v !== void 0 && v !== null && v !== "") bag[field] = v;
2976
+ }
2977
+ const cfgHeaders = cfg.headers && typeof cfg.headers === "object" ? cfg.headers : {};
2978
+ const method = str(cfg.method) || "POST";
2979
+ const auth = cfg.auth === "connection" ? "connection" : void 0;
2980
+ const connectionRef = str(cfg.connectionRef);
2981
+ deps.events.note("tool.invoked", { node: node.id, name });
2982
+ const serverExec = deps.resolvers?.toolExecutor;
2983
+ if (serverExec) {
2984
+ try {
2985
+ const result = await serverExec({
2986
+ name,
2987
+ url,
2988
+ method,
2989
+ ...Object.keys(cfgHeaders).length ? { headers: cfgHeaders } : {},
2990
+ ...Object.keys(bag).length ? { input: bag } : {},
2991
+ ...auth ? { auth } : {},
2992
+ ...auth && connectionRef ? { connectionRef } : {}
2993
+ });
2994
+ state.toolResults[node.id] = result;
2995
+ deps.events.note(result.ok ? "tool.succeeded" : "tool.errored", {
2996
+ node: node.id,
2997
+ name,
2998
+ status: result.status
2999
+ });
3000
+ } catch (err) {
3001
+ state.toolResults[node.id] = {
3002
+ ok: false,
3003
+ status: 0,
3004
+ data: { error: err instanceof Error ? err.message : String(err) }
3005
+ };
3006
+ deps.events.note("tool.errored", { node: node.id, name });
3007
+ }
3008
+ return routeByOutcome();
3009
+ }
2689
3010
  let authHeaders = {};
2690
- if (cfg.auth === "connection") {
2691
- const ref = str(cfg.connectionRef);
3011
+ if (auth === "connection") {
2692
3012
  const resolver = deps.resolvers?.secrets;
2693
- const resolved = ref && resolver ? await resolver(ref) : null;
3013
+ const resolved = connectionRef && resolver ? await resolver(connectionRef) : null;
2694
3014
  if (!resolved || !resolved.ok) {
2695
3015
  state.toolResults[node.id] = {
2696
3016
  ok: false,
@@ -2701,27 +3021,11 @@ ${collected}`);
2701
3021
  }
2702
3022
  };
2703
3023
  deps.events.note("tool.unavailable", { node: node.id, name });
2704
- return { kind: "advance" };
3024
+ return routeByOutcome();
2705
3025
  }
2706
3026
  authHeaders = resolved.headers;
2707
3027
  }
2708
- const url = str(cfg.url);
2709
- if (!url) {
2710
- state.toolResults[node.id] = { ok: false, status: 0, data: null };
2711
- deps.events.note("tool.errored", { node: node.id, name, error: "no url" });
2712
- return { kind: "advance" };
2713
- }
2714
- const inputSpec = cfg.input && typeof cfg.input === "object" ? cfg.input : {};
2715
- const slots = deps.processRt.getData();
2716
- const bag = {};
2717
- for (const field of Object.keys(inputSpec)) {
2718
- const v = resolvePath(slots, field);
2719
- if (v !== void 0 && v !== null && v !== "") bag[field] = v;
2720
- }
2721
- const cfgHeaders = cfg.headers && typeof cfg.headers === "object" ? cfg.headers : {};
2722
3028
  const headers = { ...cfgHeaders, ...authHeaders };
2723
- const method = str(cfg.method) || "POST";
2724
- deps.events.note("tool.invoked", { node: node.id, name });
2725
3029
  try {
2726
3030
  const def = httpTool({
2727
3031
  name,
@@ -2747,7 +3051,7 @@ ${collected}`);
2747
3051
  };
2748
3052
  deps.events.note("tool.errored", { node: node.id, name });
2749
3053
  }
2750
- return { kind: "advance" };
3054
+ return routeByOutcome();
2751
3055
  };
2752
3056
  ragExecutor = async (node, state, deps) => {
2753
3057
  const cfg = node.config;
@@ -2802,7 +3106,7 @@ ${collected}`);
2802
3106
  if (res.ok) {
2803
3107
  state.nodeResults[node.id] = { ok: true, ...res.outputs };
2804
3108
  deps.events.note("code.ok", { node: node.id });
2805
- return res.next ? routeByPort(node, deps, res.next) : { kind: "advance" };
3109
+ return routeByPort(node, deps, res.next ?? "next");
2806
3110
  }
2807
3111
  state.nodeResults[node.id] = { ok: false, error: res.error };
2808
3112
  deps.events.note("code.errored", { node: node.id });
@@ -2813,25 +3117,85 @@ ${collected}`);
2813
3117
  return routeByPort(node, deps, "error");
2814
3118
  }
2815
3119
  };
3120
+ decisionExecutor = async (node, state, deps) => {
3121
+ const ctx = evalCtxOf(state, deps);
3122
+ const det = deterministicEdgeTarget(deps.model.out(node.id), ctx);
3123
+ if (det !== null) {
3124
+ deps.events.note("decision.routed", { node: node.id, to: det });
3125
+ return { kind: "goto", to: det };
3126
+ }
3127
+ const branches = branchOptionsOf(node.config);
3128
+ if (branches.length === 0) return { kind: "advance" };
3129
+ const choice = await resolveBranch(branches, ctx, { hint: str(node.config.condition) });
3130
+ if (choice) {
3131
+ state.nodeResults[node.id] = { choice };
3132
+ deps.events.note("decision.routed", { node: node.id, choice });
3133
+ return { kind: "take", port: choice };
3134
+ }
3135
+ if (branches.some((b) => b.id === ELSE_BRANCH_ID)) {
3136
+ deps.events.note("decision.defaulted", { node: node.id });
3137
+ return { kind: "take", port: ELSE_BRANCH_ID };
3138
+ }
3139
+ deps.events.note("decision.unresolved", { node: node.id });
3140
+ return { kind: "advance" };
3141
+ };
3142
+ MENU_MAX_INPUTS = 3;
3143
+ menuExecutor = async (node, state, deps) => {
3144
+ const branches = branchOptionsOf(node.config);
3145
+ const fallback = () => branches.some((b) => b.id === ELSE_BRANCH_ID) ? { kind: "take", port: ELSE_BRANCH_ID } : { kind: "advance" };
3146
+ const keyed = branches.filter((b) => b.id !== ELSE_BRANCH_ID && menuDigitOf(b));
3147
+ const synthesized = keyed.length ? `Please choose: ${keyed.map((b) => `press ${menuDigitOf(b)} for ${menuLabelOf(b.label)}`).join(", ")}.` : "";
3148
+ const prompt = interpolate(str(node.config.prompt), scopeFor(state, deps)).trim() || synthesized;
3149
+ await speak(prompt, deps);
3150
+ if (branches.length === 0) return { kind: "advance" };
3151
+ for (let attempt = 0; attempt < MENU_MAX_INPUTS; attempt++) {
3152
+ const t = await nextTurn(deps, prompt);
3153
+ if ("result" in t) return t.result;
3154
+ state.lastUtterance = t.text;
3155
+ state.lastDtmf = t.dtmf;
3156
+ if (t.dtmf) {
3157
+ const hit = branches.find((b) => b.id !== ELSE_BRANCH_ID && menuDigitOf(b) === t.dtmf);
3158
+ if (hit) {
3159
+ deps.events.note("menu.selected", { node: node.id, digit: t.dtmf, choice: hit.id });
3160
+ return { kind: "take", port: hit.id };
3161
+ }
3162
+ deps.events.note("menu.invalid", { node: node.id, digit: t.dtmf });
3163
+ await speak("Sorry, that's not one of the options.", deps);
3164
+ continue;
3165
+ }
3166
+ if (t.text.trim()) {
3167
+ const choice = await resolveBranch(branches, evalCtxOf(state, deps));
3168
+ if (choice) {
3169
+ deps.events.note("menu.selected", { node: node.id, choice });
3170
+ return { kind: "take", port: choice };
3171
+ }
3172
+ await speak(prompt, deps);
3173
+ }
3174
+ }
3175
+ deps.events.note("menu.unmatched", { node: node.id });
3176
+ return fallback();
3177
+ };
2816
3178
  classifyExecutor = async (node, state, deps) => {
2817
3179
  const cfg = node.config;
2818
3180
  const resolver = deps.resolvers?.classify;
2819
- const branches = Array.isArray(cfg.branches) ? cfg.branches.filter((b) => b && typeof b.id === "string" && typeof b.label === "string").map((b) => ({ id: b.id, label: b.label })) : [];
2820
- if (!resolver || branches.length === 0) {
3181
+ const branches = branchOptionsOf(cfg);
3182
+ const options = branches.filter((b) => b.id !== ELSE_BRANCH_ID).map((b) => ({ id: b.id, label: b.label }));
3183
+ const fallback = () => branches.some((b) => b.id === ELSE_BRANCH_ID) ? { kind: "take", port: ELSE_BRANCH_ID } : { kind: "advance" };
3184
+ if (!resolver || options.length === 0) {
2821
3185
  deps.events.note("classify.unavailable", { node: node.id });
2822
- return { kind: "advance" };
3186
+ return fallback();
2823
3187
  }
2824
- deps.events.note("classify.start", { node: node.id, options: branches.length });
3188
+ deps.events.note("classify.start", { node: node.id, options: options.length });
2825
3189
  try {
2826
3190
  const choice = await resolver({
2827
3191
  instructions: str(cfg.instructions),
2828
- options: branches,
3192
+ options,
2829
3193
  ...state.lastUtterance !== void 0 ? { lastUtterance: state.lastUtterance } : {},
2830
3194
  slots: deps.processRt.getData()
2831
3195
  });
2832
3196
  if (!choice) {
2833
3197
  deps.events.note("classify.undecided", { node: node.id });
2834
- return { kind: "advance" };
3198
+ return fallback();
2835
3199
  }
2836
3200
  state.nodeResults[node.id] = { choice };
2837
3201
  deps.events.note("classify.chose", { node: node.id, choice });
@@ -2841,7 +3205,7 @@ ${collected}`);
2841
3205
  node: node.id,
2842
3206
  error: err instanceof Error ? err.message : String(err)
2843
3207
  });
2844
- return { kind: "advance" };
3208
+ return fallback();
2845
3209
  }
2846
3210
  };
2847
3211
  routerExecutor = async (node, state, deps) => {
@@ -2851,21 +3215,23 @@ ${collected}`);
2851
3215
  // Match on the rich skill description when present; fall back to the label.
2852
3216
  label: typeof b.description === "string" && b.description.trim() ? b.description : b.label
2853
3217
  })) : [];
2854
- if (!resolver || branches.length === 0) {
3218
+ const options = branches.filter((b) => b.id !== ELSE_BRANCH_ID);
3219
+ const fallback = () => branches.some((b) => b.id === ELSE_BRANCH_ID) ? { kind: "take", port: ELSE_BRANCH_ID } : { kind: "advance" };
3220
+ if (!resolver || options.length === 0) {
2855
3221
  deps.events.note("router.unavailable", { node: node.id });
2856
- return { kind: "advance" };
3222
+ return fallback();
2857
3223
  }
2858
- deps.events.note("router.start", { node: node.id, skills: branches.length });
3224
+ deps.events.note("router.start", { node: node.id, skills: options.length });
2859
3225
  try {
2860
3226
  const choice = await resolver({
2861
3227
  instructions: str(node.config.instructions),
2862
- options: branches,
3228
+ options,
2863
3229
  ...state.lastUtterance !== void 0 ? { lastUtterance: state.lastUtterance } : {},
2864
3230
  slots: deps.processRt.getData()
2865
3231
  });
2866
3232
  if (!choice) {
2867
3233
  deps.events.note("router.undecided", { node: node.id });
2868
- return { kind: "advance" };
3234
+ return fallback();
2869
3235
  }
2870
3236
  state.nodeResults[node.id] = { choice };
2871
3237
  deps.events.note("router.routed", { node: node.id, choice });
@@ -2875,7 +3241,7 @@ ${collected}`);
2875
3241
  node: node.id,
2876
3242
  error: err instanceof Error ? err.message : String(err)
2877
3243
  });
2878
- return { kind: "advance" };
3244
+ return fallback();
2879
3245
  }
2880
3246
  };
2881
3247
  playbookExecutor = async (node, state, deps) => {
@@ -2921,14 +3287,25 @@ ${collected}`);
2921
3287
  deps.events.note("playbook.gated", { node: node.id, missing: stillMissing.map((m) => m.trim()) });
2922
3288
  continue;
2923
3289
  }
2924
- if (conditional.length > 0) {
2925
- const target = await selectEdge(out, {
2926
- slots: deps.processRt.getData(),
2927
- tools: state.toolResults,
2928
- nodeResults: state.nodeResults,
2929
- lastDtmf: state.lastDtmf,
2930
- lastUtterance: state.lastUtterance
2931
- });
3290
+ const exitCtx = evalCtxOf(state, deps);
3291
+ const det = deterministicEdgeTarget(out, exitCtx);
3292
+ if (det !== null) {
3293
+ deps.events.note("playbook.exit", { node: node.id, to: det });
3294
+ return { kind: "goto", to: det };
3295
+ }
3296
+ const branches = branchOptionsOf(cfg);
3297
+ if (branches.length > 0) {
3298
+ const choice = await resolveBranch(branches, exitCtx, { hint: goal || instr });
3299
+ if (choice) {
3300
+ deps.events.note("playbook.exit", { node: node.id, port: choice });
3301
+ return { kind: "take", port: choice };
3302
+ }
3303
+ if (branches.some((b) => b.id === ELSE_BRANCH_ID)) {
3304
+ deps.events.note("playbook.exit", { node: node.id, port: ELSE_BRANCH_ID });
3305
+ return { kind: "take", port: ELSE_BRANCH_ID };
3306
+ }
3307
+ } else if (conditional.length > 0) {
3308
+ const target = await selectEdge(out, exitCtx);
2932
3309
  if (target) {
2933
3310
  deps.events.note("playbook.exit", { node: node.id, to: target });
2934
3311
  return { kind: "goto", to: target };
@@ -2989,7 +3366,7 @@ ${collected}`);
2989
3366
  });
2990
3367
  EXECUTORS = {
2991
3368
  say: sayExecutor,
2992
- confirm: sayExecutor,
3369
+ confirm: confirmExecutor,
2993
3370
  ask: askExecutor,
2994
3371
  gate: gateExecutor,
2995
3372
  end: endExecutor,
@@ -2998,6 +3375,8 @@ ${collected}`);
2998
3375
  tool: toolExecutor,
2999
3376
  set: setExecutor,
3000
3377
  subflow: subflowExecutor,
3378
+ decision: decisionExecutor,
3379
+ menu: menuExecutor,
3001
3380
  // Agentic / data nodes — execute by default (no feature flag; the platform is
3002
3381
  // pre-customer, so these ship on rather than dark). Each degrades to a recorded
3003
3382
  // no-op when its resolver/config is absent, so they never throw.
@@ -3006,8 +3385,8 @@ ${collected}`);
3006
3385
  classify: classifyExecutor,
3007
3386
  router: routerExecutor,
3008
3387
  playbook: playbookExecutor
3009
- // decision/menu/route have no executor — they advance and the interpreter's
3010
- // condition-aware selectEdge picks the matching outgoing edge.
3388
+ // start (explicit entry marker) and route (routing-plane legacy) have no
3389
+ // executor — they pass through and the default edge / selectEdge routes.
3011
3390
  };
3012
3391
  }
3013
3392
  });
@@ -3025,7 +3404,7 @@ async function selectNext(model, from, result, state, deps) {
3025
3404
  if (result.kind === "take") {
3026
3405
  const target = model.selectByPort(from, result.port);
3027
3406
  if (target !== null) return target;
3028
- const def = model.out(from).find((e) => !e.condition || !e.condition.trim());
3407
+ const def = model.out(from).find((e) => (!e.condition || !e.condition.trim()) && !e.sourceHandle);
3029
3408
  return def ? def.to : null;
3030
3409
  }
3031
3410
  return selectEdge(model.out(from), {
@@ -3982,6 +4361,7 @@ z.object({
3982
4361
  updatedAt: z.string()
3983
4362
  });
3984
4363
  var CallStateSyncResponse = z.object({ ok: z.literal(true) }).or(z.object({}).passthrough());
4364
+ var DtmfEventResponse = z.object({ accepted: z.literal(true) }).or(z.object({}).passthrough());
3985
4365
  var CallEventEnvelopeResponse = z.object({
3986
4366
  event: z.object({
3987
4367
  seq: z.number(),
@@ -4056,6 +4436,21 @@ var CallsClient = class {
4056
4436
  TranscriptSegmentResponse
4057
4437
  );
4058
4438
  }
4439
+ async appendDtmfEvent(callId, input) {
4440
+ await this.transport.request(
4441
+ {
4442
+ method: "POST",
4443
+ path: `/v1/calls/${encodeURIComponent(callId)}/dtmf-events`,
4444
+ body: {
4445
+ direction: input.direction,
4446
+ digit: input.digit,
4447
+ code: input.code,
4448
+ ...input.participantId !== void 0 ? { participantId: input.participantId } : {}
4449
+ }
4450
+ },
4451
+ DtmfEventResponse
4452
+ );
4453
+ }
4059
4454
  /**
4060
4455
  * Append a structured call event. Today the API accepts consultation
4061
4456
  * lifecycle kinds only — see apps/api/src/routes/calls.ts AppendEventBody.
@@ -4246,6 +4641,22 @@ var ConnectionsClient = class {
4246
4641
  }
4247
4642
  }
4248
4643
  };
4644
+
4645
+ // src/runtime/session-adapter.ts
4646
+ var LK_AGENT_STATE_MAP = {
4647
+ initializing: "initializing",
4648
+ idle: "idle",
4649
+ listening: "listening",
4650
+ thinking: "thinking",
4651
+ speaking: "speaking"
4652
+ };
4653
+ var NON_BUSY_STATES = /* @__PURE__ */ new Set([
4654
+ "idle",
4655
+ "listening"
4656
+ ]);
4657
+ function normalizeState(raw) {
4658
+ return LK_AGENT_STATE_MAP[raw] ?? "thinking";
4659
+ }
4249
4660
  var DEFAULT_OTLP_ENDPOINT = "https://otel.vlayers.ai/v1/otlp";
4250
4661
  var started = false;
4251
4662
  var sdk = null;
@@ -5305,6 +5716,7 @@ function createRailController(deps) {
5305
5716
 
5306
5717
  // src/runtime/graph/index.ts
5307
5718
  init_conditions();
5719
+ init_branch_resolve();
5308
5720
  init_embedder();
5309
5721
  init_drift();
5310
5722
  init_interpolate();
@@ -5365,6 +5777,36 @@ function createSecretResolver(opts) {
5365
5777
  }
5366
5778
  };
5367
5779
  }
5780
+ var TOOL_EXECUTE_PATH = "/v1/internal/tools/execute";
5781
+ function createToolExecutor(opts) {
5782
+ const fetchImpl = opts.fetchImpl ?? fetch;
5783
+ const url = opts.baseUrl.replace(/\/+$/, "") + TOOL_EXECUTE_PATH;
5784
+ return async (req) => {
5785
+ try {
5786
+ const res = await fetchImpl(url, {
5787
+ method: "POST",
5788
+ headers: {
5789
+ "content-type": "application/json",
5790
+ authorization: `Bearer ${opts.apiKey}`,
5791
+ "x-internal-token": opts.internalToken
5792
+ },
5793
+ body: JSON.stringify(req)
5794
+ });
5795
+ if (!res.ok) {
5796
+ return { ok: false, status: res.status, data: { kind: "executor_error", reason: `tool execute failed (${res.status})` } };
5797
+ }
5798
+ const body = await res.json();
5799
+ const r = body.result;
5800
+ return { ok: r?.ok ?? false, status: r?.status ?? 0, data: r?.data ?? null };
5801
+ } catch (err) {
5802
+ return {
5803
+ ok: false,
5804
+ status: 0,
5805
+ data: { kind: "executor_error", reason: err instanceof Error ? err.message : String(err) }
5806
+ };
5807
+ }
5808
+ };
5809
+ }
5368
5810
  var PLAYBOOKS_PATH = "/v1/playbooks";
5369
5811
  function createPlaybookResolver(opts) {
5370
5812
  const fetchImpl = opts.fetchImpl ?? fetch;
@@ -5446,8 +5888,12 @@ function createCodeResolver(opts) {
5446
5888
  const jsonStr = vm.dump(res.value);
5447
5889
  res.value.dispose();
5448
5890
  const parsed = typeof jsonStr === "string" ? JSON.parse(jsonStr) : null;
5891
+ if (Array.isArray(parsed) && parsed.length === 1 && typeof parsed[0] === "string") {
5892
+ return { ok: true, outputs: {}, next: parsed[0] };
5893
+ }
5449
5894
  const outputs = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : { value: parsed };
5450
- return { ok: true, outputs };
5895
+ const next = typeof outputs["next"] === "string" ? outputs["next"] : void 0;
5896
+ return { ok: true, outputs, ...next ? { next } : {} };
5451
5897
  } finally {
5452
5898
  vm.dispose();
5453
5899
  runtime.dispose();
@@ -5472,6 +5918,7 @@ function buildGraphResolvers() {
5472
5918
  resolvers.flows = createFlowResolver({ baseUrl, apiKey });
5473
5919
  if (internalToken) {
5474
5920
  resolvers.secrets = createSecretResolver({ baseUrl, apiKey, internalToken });
5921
+ resolvers.toolExecutor = createToolExecutor({ baseUrl, apiKey, internalToken });
5475
5922
  }
5476
5923
  }
5477
5924
  return Object.keys(resolvers).length > 0 ? resolvers : void 0;
@@ -8249,7 +8696,7 @@ function createProcessRuntime(def) {
8249
8696
  void primitive.loadSchema(schema);
8250
8697
  let completed = false;
8251
8698
  let openai2 = null;
8252
- const getOpenAi = () => openai2 ??= new OpenAI();
8699
+ const getOpenAi = () => openai2 ??= new OpenAI2();
8253
8700
  return {
8254
8701
  augmentPrompt(basePrompt) {
8255
8702
  const lines = Object.entries(def.collect).map(([name, field]) => {
@@ -9070,6 +9517,8 @@ function makeNoopHandle(callId) {
9070
9517
  publish() {
9071
9518
  },
9072
9519
  syncState() {
9520
+ },
9521
+ publishDtmf() {
9073
9522
  }
9074
9523
  };
9075
9524
  }
@@ -9109,6 +9558,15 @@ async function createCallSyncHandle(init) {
9109
9558
  err: err instanceof Error ? err.message : err
9110
9559
  });
9111
9560
  });
9561
+ },
9562
+ publishDtmf(input) {
9563
+ void init.client.calls.appendDtmfEvent(callId, input).catch((err) => {
9564
+ console.warn("[sdk.call-sync] dtmf publish failed", {
9565
+ direction: input.direction,
9566
+ digit: input.digit,
9567
+ err: err instanceof Error ? err.message : err
9568
+ });
9569
+ });
9112
9570
  }
9113
9571
  };
9114
9572
  }
@@ -9841,18 +10299,26 @@ var defaultClock = {
9841
10299
  clearTimeout: (h) => clearTimeout(h)
9842
10300
  };
9843
10301
  var GUARANTEE_RETRY_MS = 1500;
10302
+ var DEFAULT_MAX_CALL_DURATION_MS = 36e5;
9844
10303
  function createEndCallController(deps) {
9845
10304
  const clock = deps.clock ?? defaultClock;
9846
10305
  const log = deps.log ?? (() => {
9847
10306
  });
9848
10307
  let ended = false;
9849
10308
  let idleTimer = null;
10309
+ let durationTimer = null;
9850
10310
  const clearIdle = () => {
9851
10311
  if (idleTimer !== null) {
9852
10312
  clock.clearTimeout(idleTimer);
9853
10313
  idleTimer = null;
9854
10314
  }
9855
10315
  };
10316
+ const clearDuration = () => {
10317
+ if (durationTimer !== null) {
10318
+ clock.clearTimeout(durationTimer);
10319
+ durationTimer = null;
10320
+ }
10321
+ };
9856
10322
  const sleep2 = (ms) => new Promise((resolve) => {
9857
10323
  clock.setTimeout(resolve, ms);
9858
10324
  });
@@ -9874,6 +10340,7 @@ function createEndCallController(deps) {
9874
10340
  if (ended) return;
9875
10341
  ended = true;
9876
10342
  clearIdle();
10343
+ clearDuration();
9877
10344
  const info = {
9878
10345
  ...opts?.reason !== void 0 ? { reason: opts.reason } : {},
9879
10346
  trigger: opts?.trigger ?? "user",
@@ -9913,6 +10380,18 @@ function createEndCallController(deps) {
9913
10380
  cancelIdle() {
9914
10381
  clearIdle();
9915
10382
  },
10383
+ armDuration() {
10384
+ if (ended || deps.maxCallDurationMs === false) return;
10385
+ clearDuration();
10386
+ const durationMs = deps.maxCallDurationMs ?? DEFAULT_MAX_CALL_DURATION_MS;
10387
+ durationTimer = clock.setTimeout(() => {
10388
+ durationTimer = null;
10389
+ void hangUp({
10390
+ trigger: "max_duration",
10391
+ reason: `call exceeded the ${durationMs}ms duration ceiling`
10392
+ });
10393
+ }, durationMs);
10394
+ },
9916
10395
  noteEngineClosed() {
9917
10396
  if (ended) return;
9918
10397
  log("[agent] realtime engine closed; guaranteeing teardown");
@@ -9920,6 +10399,7 @@ function createEndCallController(deps) {
9920
10399
  },
9921
10400
  dispose() {
9922
10401
  clearIdle();
10402
+ clearDuration();
9923
10403
  }
9924
10404
  };
9925
10405
  }
@@ -10026,6 +10506,8 @@ var Agent = class {
10026
10506
  let effectiveConfig = this.config;
10027
10507
  let graphMode = false;
10028
10508
  let graphProgram;
10509
+ let flowBootFailure = null;
10510
+ let flowAgentName = null;
10029
10511
  let loadedPlan = null;
10030
10512
  const planIdValue = stringValue(dispatchMdEarly["planId"]);
10031
10513
  if (planIdValue && sdkClient) {
@@ -10052,56 +10534,84 @@ var Agent = class {
10052
10534
  });
10053
10535
  }
10054
10536
  }
10055
- if (this.config.flowRuntime && !loadedPlan && sdkClient) {
10537
+ if (this.config.flowRuntime && !loadedPlan) {
10056
10538
  const flowAgentId = stringValue(dispatchMdEarly["agentId"]);
10057
- if (flowAgentId) {
10058
- try {
10059
- const schema = await sdkClient.agents.getProcessSchema(flowAgentId);
10060
- if (schema) {
10061
- const graphOn = process.env["VL_FLOW_GRAPH_RUNTIME"] === "1" && !!schema.program;
10062
- const parts = processSchemaToAgentParts(schema, {
10063
- ...this.config.flowRuntime.basePrompt ? { basePrompt: this.config.flowRuntime.basePrompt } : {},
10064
- ...graphOn && schema.program ? { graphNodeIds: new Set(Object.keys(schema.program.nodes)) } : {}
10065
- });
10066
- effectiveConfig = {
10067
- ...effectiveConfig,
10068
- prompt: parts.prompt,
10069
- process: parts.process,
10070
- ...parts.triggers ? { triggers: parts.triggers } : {},
10071
- ...parts.handoff ? { handoff: parts.handoff } : {},
10072
- ...parts.memory ? { memory: parts.memory } : {},
10073
- ...parts.tools ? { tools: { ...effectiveConfig.tools ?? {}, ...parts.tools } } : {}
10074
- };
10075
- console.log("[agent] booted from flow", {
10076
- agentId: flowAgentId,
10077
- fields: Object.keys(parts.process.collect).length,
10078
- triggers: Object.keys(parts.triggers ?? {}).length
10079
- });
10080
- effectiveConfig = await applyStoredPipeline(
10081
- sdkClient,
10082
- flowAgentId,
10083
- effectiveConfig,
10084
- void 0
10085
- );
10086
- if (graphOn && schema.program) {
10087
- graphProgram = schema.program;
10088
- graphMode = true;
10089
- console.log("[agent] flow graph runtime enabled", {
10090
- agentId: flowAgentId,
10091
- nodes: Object.keys(schema.program.nodes).length,
10092
- entryNodes: schema.program.entry.length
10093
- });
10094
- }
10095
- } else {
10096
- console.warn("[agent] flowRuntime: agent has no process_schema", {
10097
- agentId: flowAgentId
10098
- });
10539
+ if (!sdkClient) {
10540
+ flowBootFailure = "no_api_key";
10541
+ console.error(
10542
+ "[agent] flowRuntime: VOICELAYER_API_KEY is not set \u2014 cannot resolve the flow schema; answering with an apology and ending the call."
10543
+ );
10544
+ } else if (!flowAgentId) {
10545
+ flowBootFailure = "missing_agent_id";
10546
+ console.error(
10547
+ "[agent] flowRuntime: dispatch metadata carries no agentId \u2014 cannot resolve which flow to run.",
10548
+ { room: job.room.name ?? null }
10549
+ );
10550
+ } else {
10551
+ let schema = null;
10552
+ let fetchError = null;
10553
+ for (let attempt = 0; attempt < 2; attempt++) {
10554
+ try {
10555
+ schema = await sdkClient.agents.getProcessSchema(flowAgentId);
10556
+ fetchError = null;
10557
+ break;
10558
+ } catch (err) {
10559
+ fetchError = err;
10560
+ await new Promise((resolve) => setTimeout(resolve, 500));
10099
10561
  }
10100
- } catch (err) {
10101
- console.warn("[agent] flow schema fetch failed; using base config", {
10562
+ }
10563
+ if (fetchError) {
10564
+ flowBootFailure = "schema_fetch_failed";
10565
+ console.error("[agent] flowRuntime: flow schema fetch failed after retry", {
10102
10566
  agentId: flowAgentId,
10103
- err: err instanceof Error ? err.message : String(err)
10567
+ err: fetchError instanceof Error ? fetchError.message : String(fetchError)
10568
+ });
10569
+ } else if (!schema) {
10570
+ flowBootFailure = "no_process_schema";
10571
+ console.error(
10572
+ "[agent] flowRuntime: agent has no process_schema (wrong agentId, or the flow was never deployed)",
10573
+ { agentId: flowAgentId }
10574
+ );
10575
+ } else {
10576
+ const graphOn = process.env["VL_FLOW_GRAPH_RUNTIME"] !== "0" && !!schema.program;
10577
+ const parts = processSchemaToAgentParts(schema, {
10578
+ ...this.config.flowRuntime.basePrompt ? { basePrompt: this.config.flowRuntime.basePrompt } : {},
10579
+ ...graphOn && schema.program ? { graphNodeIds: new Set(Object.keys(schema.program.nodes)) } : {}
10104
10580
  });
10581
+ effectiveConfig = {
10582
+ ...effectiveConfig,
10583
+ prompt: parts.prompt,
10584
+ process: parts.process,
10585
+ ...parts.triggers ? { triggers: parts.triggers } : {},
10586
+ ...parts.handoff ? { handoff: parts.handoff } : {},
10587
+ ...parts.memory ? { memory: parts.memory } : {},
10588
+ ...parts.tools ? { tools: { ...effectiveConfig.tools ?? {}, ...parts.tools } } : {}
10589
+ };
10590
+ console.log("[agent] booted from flow", {
10591
+ agentId: flowAgentId,
10592
+ fields: Object.keys(parts.process.collect).length,
10593
+ triggers: Object.keys(parts.triggers ?? {}).length
10594
+ });
10595
+ effectiveConfig = await applyStoredPipeline(
10596
+ sdkClient,
10597
+ flowAgentId,
10598
+ effectiveConfig,
10599
+ void 0
10600
+ );
10601
+ try {
10602
+ flowAgentName = (await sdkClient.agents.getConfig(flowAgentId))?.name ?? null;
10603
+ } catch {
10604
+ flowAgentName = null;
10605
+ }
10606
+ if (graphOn && schema.program) {
10607
+ graphProgram = schema.program;
10608
+ graphMode = true;
10609
+ console.log("[agent] flow graph runtime enabled", {
10610
+ agentId: flowAgentId,
10611
+ nodes: Object.keys(schema.program.nodes).length,
10612
+ entryNodes: schema.program.entry.length
10613
+ });
10614
+ }
10105
10615
  }
10106
10616
  }
10107
10617
  }
@@ -10348,6 +10858,7 @@ ${callIntent}`
10348
10858
  const endCallTeardown = endCallPolicy.teardown ?? "room";
10349
10859
  const endCallGuaranteeMs = endCallPolicy.guaranteeMs === void 0 ? 4e3 : endCallPolicy.guaranteeMs;
10350
10860
  const endCallIdleMs = endCallPolicy.idleHangupMs === void 0 ? 2e4 : endCallPolicy.idleHangupMs;
10861
+ const endCallMaxDurationMs = endCallPolicy.maxCallDurationMs === void 0 ? DEFAULT_MAX_CALL_DURATION_MS : endCallPolicy.maxCallDurationMs;
10351
10862
  const endCallRoomName = job.room.name ?? `room-${job.job?.id ?? "unknown"}`;
10352
10863
  const localDisconnect = async () => {
10353
10864
  try {
@@ -10359,6 +10870,7 @@ ${callIntent}`
10359
10870
  teardown: endCallTeardown,
10360
10871
  guaranteeMs: endCallGuaranteeMs,
10361
10872
  idleHangupMs: endCallIdleMs,
10873
+ maxCallDurationMs: endCallMaxDurationMs,
10362
10874
  // 'room' deletes the whole LK room (drops the SIP/PSTN leg → carrier BYE);
10363
10875
  // 'agent' only disconnects the agent participant.
10364
10876
  runTeardown: endCallTeardown === "room" ? () => endRoomBestEffort(endCallRoomName, localDisconnect) : localDisconnect,
@@ -10408,6 +10920,7 @@ ${callIntent}`
10408
10920
  throw new Error("publishDtmf not available on this LiveKit runtime");
10409
10921
  }
10410
10922
  await lp.publishDtmf(code, digit);
10923
+ callSync.publishDtmf({ direction: "sent", digit, code });
10411
10924
  };
10412
10925
  const ctx = buildAgentContext({
10413
10926
  job,
@@ -10435,6 +10948,8 @@ ${callIntent}`
10435
10948
  publishDtmf
10436
10949
  });
10437
10950
  ctxRef.current = ctx;
10951
+ if (flowAgentName) ctx.metadata["agentName"] = flowAgentName;
10952
+ else if (this.config.name) ctx.metadata["agentName"] = this.config.name;
10438
10953
  const onboardParticipant = async (p) => {
10439
10954
  let enriched = p;
10440
10955
  if (participantsCfg?.attributes) {
@@ -10511,8 +11026,17 @@ ${callIntent}`
10511
11026
  const onDtmfReceived = (code, digit, participant2) => {
10512
11027
  const identity = participant2?.identity;
10513
11028
  const pid = identity ? liveKitToVoiceLayer.get(identity) ?? ParticipantId(identity) : ParticipantId("unknown");
10514
- const resolvedDigit = digit && digit.length > 0 ? digit : dtmfCodeToDigit(code) ?? "";
11029
+ const canonicalFromCode = dtmfCodeToDigit(code);
11030
+ const resolvedDigit = canonicalFromCode ?? (digit && digit.length > 0 ? digit.toUpperCase() : "");
10515
11031
  audioExperience.recordDtmf(callInfo.callId, pid);
11032
+ if (resolvedDigit && /^[0-9*#A-D]$/.test(resolvedDigit)) {
11033
+ callSync.publishDtmf({
11034
+ direction: "received",
11035
+ digit: resolvedDigit,
11036
+ code,
11037
+ participantId: String(pid)
11038
+ });
11039
+ }
10516
11040
  const event = {
10517
11041
  participantId: pid,
10518
11042
  code,
@@ -10774,6 +11298,21 @@ ${callIntent}`
10774
11298
  room: job.room.name ?? null,
10775
11299
  participant: participant.identity
10776
11300
  });
11301
+ endCallController.armDuration();
11302
+ {
11303
+ const announceMd = parseJobMetadata(job.job?.metadata);
11304
+ const announcement = stringValue(announceMd["recordingAnnouncement"]);
11305
+ if (announcement) {
11306
+ try {
11307
+ const sessionWithSay = session;
11308
+ if (typeof sessionWithSay.say === "function") {
11309
+ await sessionWithSay.say(announcement, { allowInterruptions: false });
11310
+ }
11311
+ } catch (err) {
11312
+ console.warn("[agent] recording announcement say failed", err);
11313
+ }
11314
+ }
11315
+ }
10777
11316
  if (this.config.puppetMode) {
10778
11317
  const dispatchMd = parseJobMetadata(job.job?.metadata);
10779
11318
  const initialDirective = stringValue(dispatchMd["initialDirective"]);
@@ -10787,6 +11326,18 @@ ${callIntent}`
10787
11326
  console.warn("[agent] puppet initialDirective say failed", err);
10788
11327
  }
10789
11328
  }
11329
+ } else if (flowBootFailure) {
11330
+ const apology = this.config.flowRuntime?.bootFailureMessage ?? "Sorry \u2014 we're having a technical issue starting this call. Please try again in a few minutes. Goodbye.";
11331
+ try {
11332
+ await session.generateReply({
11333
+ instructions: `Say the following line to the user, verbatim and in full \u2014 no additions, omissions, or rephrasing:
11334
+
11335
+ ${apology}`
11336
+ }).waitForPlayout();
11337
+ } catch (err) {
11338
+ console.warn("[agent] flow boot apology failed to play", err);
11339
+ }
11340
+ await endCallController.hangUp({ trigger: "flow", reason: `flow_boot:${flowBootFailure}` });
10790
11341
  } else if (this.config.onCall) {
10791
11342
  await this.config.onCall(ctx);
10792
11343
  } else if (graphMode && graphProgram && graphEvents) {
@@ -11172,6 +11723,4 @@ function createClient(options = {}) {
11172
11723
  };
11173
11724
  }
11174
11725
 
11175
- export { Agent, BaseTool, CallsClient, EmailClient, FormsClient, PlansClient, VoiceLayerAuthError, VoiceLayerError, VoiceLayerHttpError, VoiceLayerNetworkError, VoiceLayerValidationError, applyPronunciations, assemblyai, asyncProbe, cartesia, collectDefaultMetadata, createClient, createNoOpLLM, deepgram, defineAgent, defineFlowRuntime, defineProcess, defineTool, dtmfCodeToDigit, dtmfDigitToCode, duck, elevenlabs, envProbe, formTools, google, httpTool, livekitTurn, mergeMetadata, mute, openai, ParticipantId as participantId, passthrough, processSchemaToAgentParts, processSchemaToDefinition, puppetEnvAvailable, resendEmailTool, runProbes, silero, startPuppetControl, transform, urlProbe, webhook, wrap };
11176
- //# sourceMappingURL=index.js.map
11177
- //# sourceMappingURL=index.js.map
11726
+ export { Agent, BaseTool, CallsClient, EmailClient, FormsClient, LK_AGENT_STATE_MAP, NON_BUSY_STATES, PlansClient, VoiceLayerAuthError, VoiceLayerError, VoiceLayerHttpError, VoiceLayerNetworkError, VoiceLayerValidationError, applyPronunciations, assemblyai, asyncProbe, cartesia, collectDefaultMetadata, createClient, createNoOpLLM, deepgram, defineAgent, defineFlowRuntime, defineProcess, defineTool, dtmfCodeToDigit, dtmfDigitToCode, duck, elevenlabs, envProbe, formTools, google, httpTool, livekitTurn, mergeMetadata, mute, normalizeState, openai, ParticipantId as participantId, passthrough, processSchemaToAgentParts, processSchemaToDefinition, puppetEnvAvailable, resendEmailTool, runProbes, silero, startPuppetControl, transform, urlProbe, webhook, wrap };