@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.d.ts +230 -19
- package/dist/index.js +664 -115
- package/package.json +5 -5
- package/dist/index.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import
|
|
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
|
-
|
|
208
|
-
|
|
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) => ({
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
|
2467
|
+
const raw = resolvePath(ctx.slots, nameRaw.trim());
|
|
2468
|
+
const value = String(raw ?? "").toLowerCase();
|
|
2380
2469
|
const target = valRaw.trim().toLowerCase();
|
|
2381
|
-
const
|
|
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
|
|
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
|
-
|
|
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 (
|
|
2691
|
-
const ref = str(cfg.connectionRef);
|
|
3011
|
+
if (auth === "connection") {
|
|
2692
3012
|
const resolver = deps.resolvers?.secrets;
|
|
2693
|
-
const resolved =
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
2820
|
-
|
|
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
|
|
3186
|
+
return fallback();
|
|
2823
3187
|
}
|
|
2824
|
-
deps.events.note("classify.start", { node: node.id, options:
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
3222
|
+
return fallback();
|
|
2857
3223
|
}
|
|
2858
|
-
deps.events.note("router.start", { node: node.id, skills:
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
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:
|
|
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
|
-
//
|
|
3010
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
|
10537
|
+
if (this.config.flowRuntime && !loadedPlan) {
|
|
10056
10538
|
const flowAgentId = stringValue(dispatchMdEarly["agentId"]);
|
|
10057
|
-
if (
|
|
10058
|
-
|
|
10059
|
-
|
|
10060
|
-
|
|
10061
|
-
|
|
10062
|
-
|
|
10063
|
-
|
|
10064
|
-
|
|
10065
|
-
|
|
10066
|
-
|
|
10067
|
-
|
|
10068
|
-
|
|
10069
|
-
|
|
10070
|
-
|
|
10071
|
-
|
|
10072
|
-
|
|
10073
|
-
|
|
10074
|
-
|
|
10075
|
-
|
|
10076
|
-
|
|
10077
|
-
|
|
10078
|
-
|
|
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
|
-
}
|
|
10101
|
-
|
|
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:
|
|
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
|
|
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 };
|