@voicelayer/sdk 0.6.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -14,11 +14,11 @@ import { LoggerProvider, BatchLogRecordProcessor } from '@opentelemetry/sdk-logs
14
14
  import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
15
15
  import { NodeSDK } from '@opentelemetry/sdk-node';
16
16
  import { ATTR_SERVICE_VERSION, ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions';
17
+ import { llm, DEFAULT_API_CONNECT_OPTIONS, intervalForRetry, voice as voice$1, APIStatusError, APITimeoutError, APIConnectionError } from '@livekit/agents';
17
18
  import http from 'http';
18
19
  import https from 'https';
19
20
  import { Readable } from 'stream';
20
21
  import { lookup } from 'dns/promises';
21
- import { voice as voice$1, llm } from '@livekit/agents';
22
22
  import { getQuickJS, shouldInterruptAfterDeadline } from 'quickjs-emscripten';
23
23
  import { randomUUID, createHash } from 'crypto';
24
24
  import { setup, createActor } from 'xstate';
@@ -95,35 +95,6 @@ var init_log = __esm({
95
95
  }
96
96
  });
97
97
 
98
- // src/runtime/graph/graph-model.ts
99
- function buildGraphModel(program) {
100
- return {
101
- entry: program.entry,
102
- node: (id) => program.nodes[id],
103
- out: (id) => program.nodes[id]?.out ?? [],
104
- selectByPort: (id, port) => {
105
- const edges = program.nodes[id]?.out ?? [];
106
- const byHandle = edges.find((e) => e.sourceHandle === port);
107
- if (byHandle) return byHandle.to;
108
- const byLabel = edges.find((e) => e.label === port);
109
- return byLabel ? byLabel.to : null;
110
- },
111
- askNodeForSlot: (slot) => {
112
- const want = slot.trim();
113
- for (const node of Object.values(program.nodes)) {
114
- if (node.type !== "ask") continue;
115
- const s = typeof node.config.slot === "string" ? node.config.slot : node.id;
116
- if (s.trim() === want) return node.id;
117
- }
118
- return void 0;
119
- }
120
- };
121
- }
122
- var init_graph_model = __esm({
123
- "src/runtime/graph/graph-model.ts"() {
124
- }
125
- });
126
-
127
98
  // ../contracts/src/effect-stack.ts
128
99
  var EffectStack;
129
100
  var init_effect_stack = __esm({
@@ -1126,11 +1097,10 @@ var init_agent_config = __esm({
1126
1097
  projectId: z.string().min(1),
1127
1098
  // Human-readable label. No `agentType` enum on purpose.
1128
1099
  name: z.string().min(1).max(128),
1129
- // Soft override of the worker's baked-in prompt. Absent → call_start
1130
- // uses the SDK worker's predefined prompt (the source of truth for
1131
- // pre-defined agents like FNOL). Present → it overrides the worker's
1132
- // prompt at call_start as a snapshot decision; mid-call edits are not
1133
- // honoured. Per-call override at call_start time is also allowed.
1100
+ // The agent's persona (the Behaviour page). For a FLOW agent the PUBLISHED value is the flow persona on every
1101
+ // transport (flowPersonaOf — voice boot and text replies alike; absent → DEFAULT_FLOW_PERSONA), read at call start as
1102
+ // a snapshot decision; mid-call edits are not honoured (burn-down G-46). A coded SDK agent's prompt is its code: this
1103
+ // field doesn't reach it. A call plan may still override the prompt per call.
1134
1104
  systemPrompt: z.string().max(64e3).optional(),
1135
1105
  // Routing/decision layer (Voiceflow "Instructions"): plain-language guidance the
1136
1106
  // agent reads to decide WHEN to do things (vs systemPrompt = WHO it is). Injected
@@ -2089,12 +2059,143 @@ var init_call_result = __esm({
2089
2059
  });
2090
2060
  }
2091
2061
  });
2062
+ function supportedZones() {
2063
+ if (!SUPPORTED_ZONES) {
2064
+ const intl = Intl;
2065
+ SUPPORTED_ZONES = new Set(intl.supportedValuesOf?.("timeZone") ?? []);
2066
+ }
2067
+ return SUPPORTED_ZONES;
2068
+ }
2069
+ function isIanaTimezone(tz) {
2070
+ if (tz === "UTC" || supportedZones().has(tz)) return true;
2071
+ if (!IANA_NAME.test(tz)) return false;
2072
+ try {
2073
+ new Intl.DateTimeFormat("en-US", { timeZone: tz });
2074
+ return true;
2075
+ } catch {
2076
+ return false;
2077
+ }
2078
+ }
2079
+ var BUDGET_SCOPE_KINDS, BudgetScopeKind, CREATABLE_BUDGET_SCOPE_KINDS, BUDGET_CHANNELS, BudgetScope, BudgetPeriodKind, BudgetEnforcement, BudgetInbound, BudgetOwner, DEFAULT_BUDGET_THRESHOLDS, BUDGET_MAX_USD, Thresholds, IANA_NAME, SUPPORTED_ZONES, Timezone, BudgetPeriodStatus, Budget, BudgetRefusalDetail, BillingNoticeKind, BillingNotice;
2080
+ var init_budgets = __esm({
2081
+ "../contracts/src/budgets.ts"() {
2082
+ BUDGET_SCOPE_KINDS = ["workspace", "agent", "api_key", "channel"];
2083
+ BudgetScopeKind = z.enum(BUDGET_SCOPE_KINDS);
2084
+ CREATABLE_BUDGET_SCOPE_KINDS = ["workspace", "api_key", "channel"];
2085
+ BUDGET_CHANNELS = ["voice", "sms", "email", "chat", "discord"];
2086
+ z.enum(BUDGET_CHANNELS);
2087
+ BudgetScope = z.object({
2088
+ kind: BudgetScopeKind,
2089
+ // null for the workspace; the API key's id, or the channel name.
2090
+ id: z.string().min(1).nullable()
2091
+ });
2092
+ BudgetPeriodKind = z.enum(["day", "month", "billing_period"]);
2093
+ BudgetEnforcement = z.enum(["soft", "hard"]);
2094
+ BudgetInbound = z.enum(["refuse", "answer"]);
2095
+ BudgetOwner = z.enum(["customer", "operator"]);
2096
+ DEFAULT_BUDGET_THRESHOLDS = [50, 80, 100];
2097
+ BUDGET_MAX_USD = 1e6;
2098
+ Thresholds = z.array(z.number().int().min(1).max(100)).max(5).refine((t) => new Set(t).size === t.length, "thresholds must be distinct").transform((t) => [...t].sort((a, b) => a - b));
2099
+ IANA_NAME = /^[A-Za-z]+(?:\/[A-Za-z0-9_+-]+)+$/;
2100
+ SUPPORTED_ZONES = null;
2101
+ Timezone = z.string().min(1).max(64).refine(isIanaTimezone, "use an IANA time zone name, like America/New_York or UTC");
2102
+ BudgetPeriodStatus = z.object({
2103
+ periodStart: z.string(),
2104
+ // When the budget resets.
2105
+ periodEnd: z.string(),
2106
+ // Charged usage this period.
2107
+ settledUsd: z.string(),
2108
+ // Reserved for calls in progress.
2109
+ inProgressUsd: z.string(),
2110
+ // settled + in progress — what the budget compares with its amount.
2111
+ spentUsd: z.string(),
2112
+ // spent ÷ amount, as a whole percent (may pass 100: calls in progress finish).
2113
+ percent: z.number().int().min(0),
2114
+ // A hard budget at its limit: new actions in its scope are refused until periodEnd.
2115
+ reached: z.boolean()
2116
+ });
2117
+ Budget = z.object({
2118
+ id: z.string(),
2119
+ scope: BudgetScope,
2120
+ period: BudgetPeriodKind,
2121
+ timezone: z.string(),
2122
+ amountUsd: z.string(),
2123
+ enforcement: BudgetEnforcement,
2124
+ thresholds: z.array(z.number().int()),
2125
+ notifyEmail: z.boolean(),
2126
+ notifyWebhook: z.boolean(),
2127
+ // Operator budgets are VoiceLayer's own safety limits: shown to the customer, read-only.
2128
+ owner: BudgetOwner,
2129
+ inbound: BudgetInbound,
2130
+ createdAt: z.string(),
2131
+ current: BudgetPeriodStatus
2132
+ });
2133
+ z.object({ budgets: z.array(Budget) });
2134
+ z.object({
2135
+ scope: z.object({ kind: z.enum(CREATABLE_BUDGET_SCOPE_KINDS), id: z.string().min(1).max(200).nullable().optional() }),
2136
+ period: BudgetPeriodKind,
2137
+ timezone: Timezone.default("UTC"),
2138
+ amountUsd: z.number().positive().max(BUDGET_MAX_USD),
2139
+ enforcement: BudgetEnforcement,
2140
+ thresholds: Thresholds.default([...DEFAULT_BUDGET_THRESHOLDS]),
2141
+ notifyEmail: z.boolean().default(true),
2142
+ notifyWebhook: z.boolean().default(true),
2143
+ // Hard budgets only; a customer budget refuses incoming calls unless told to answer them (MON-9).
2144
+ inbound: BudgetInbound.default("refuse")
2145
+ }).superRefine((b, ctx) => {
2146
+ const id = b.scope.id ?? null;
2147
+ if (b.scope.kind === "workspace" && id !== null) ctx.addIssue({ code: "custom", path: ["scope", "id"], message: "a workspace budget names no scope id" });
2148
+ if (b.scope.kind !== "workspace" && id === null) ctx.addIssue({ code: "custom", path: ["scope", "id"], message: "name the API key or channel" });
2149
+ if (b.scope.kind === "channel" && id !== null && !BUDGET_CHANNELS.includes(id)) {
2150
+ ctx.addIssue({ code: "custom", path: ["scope", "id"], message: `channel is one of ${BUDGET_CHANNELS.join(", ")}` });
2151
+ }
2152
+ });
2153
+ z.object({
2154
+ amountUsd: z.number().positive().max(BUDGET_MAX_USD),
2155
+ enforcement: BudgetEnforcement,
2156
+ thresholds: Thresholds,
2157
+ notifyEmail: z.boolean(),
2158
+ notifyWebhook: z.boolean(),
2159
+ inbound: BudgetInbound
2160
+ }).partial().refine((b) => Object.keys(b).length > 0, "nothing to change");
2161
+ BudgetRefusalDetail = z.object({
2162
+ budgetId: z.string(),
2163
+ scope: BudgetScope,
2164
+ period: BudgetPeriodKind,
2165
+ resetsAt: z.string()
2166
+ });
2167
+ BillingNoticeKind = z.enum([
2168
+ "balance_empty",
2169
+ "credit_limit_reached",
2170
+ "auto_topup_off",
2171
+ "wallet_frozen",
2172
+ "invoice_issued",
2173
+ "invoice_overdue",
2174
+ "workspace_paused",
2175
+ "workspace_restored",
2176
+ "budget_threshold",
2177
+ "budget_reached"
2178
+ ]);
2179
+ BillingNotice = z.object({
2180
+ id: z.string(),
2181
+ kind: BillingNoticeKind,
2182
+ // The same words as the email's subject and first paragraph.
2183
+ title: z.string(),
2184
+ body: z.string(),
2185
+ // The budget a budget notice is about.
2186
+ budgetId: z.string().nullable(),
2187
+ createdAt: z.string()
2188
+ });
2189
+ z.object({ notices: z.array(BillingNotice) });
2190
+ }
2191
+ });
2092
2192
  var WEBHOOK_EVENT_TYPES, CallWebhookDirection, CallWebhookOutcome, CallWebhookAttributes, Party, CALL_ENDED_PAYLOAD_VERSION, CallEndedTranscript;
2093
2193
  var init_webhook_events = __esm({
2094
2194
  "../contracts/src/webhook-events.ts"() {
2095
2195
  init_call_result();
2096
2196
  init_call_outcome();
2097
- WEBHOOK_EVENT_TYPES = ["call.started", "call.ended", "recording.ready"];
2197
+ init_budgets();
2198
+ WEBHOOK_EVENT_TYPES = ["call.started", "call.ended", "recording.ready", "billing.budget_threshold", "billing.budget_reached"];
2098
2199
  z.enum(WEBHOOK_EVENT_TYPES);
2099
2200
  CallWebhookDirection = z.enum(["inbound", "outbound", "web"]);
2100
2201
  CallWebhookOutcome = z.enum(["completed", "dropped", "failed", "no_answer", "other"]);
@@ -2153,6 +2254,19 @@ var init_webhook_events = __esm({
2153
2254
  recordingUrl: z.string().url(),
2154
2255
  status: z.enum(["starting", "active", "completed", "failed", "aborted"])
2155
2256
  });
2257
+ z.object({
2258
+ budgetId: z.string(),
2259
+ scope: BudgetScope,
2260
+ period: BudgetPeriodKind,
2261
+ periodStart: z.string().datetime({ offset: true }),
2262
+ periodEnd: z.string().datetime({ offset: true }),
2263
+ amountUsd: z.string(),
2264
+ spentUsd: z.string(),
2265
+ inProgressUsd: z.string(),
2266
+ // The threshold passed (budget_reached: 100).
2267
+ thresholdPct: z.number().int().min(1).max(100),
2268
+ enforcement: BudgetEnforcement
2269
+ });
2156
2270
  }
2157
2271
  });
2158
2272
  var PhoneNumberStatus, PhoneNumberProvider, PhoneNumberCapabilities, PhoneNumberRecord;
@@ -2227,10 +2341,172 @@ var init_project_limits = __esm({
2227
2341
  });
2228
2342
 
2229
2343
  // ../contracts/src/flow-ports.ts
2230
- var INCOMPLETE_PORT_ID;
2344
+ var INCOMPLETE_PORT_ID, ASK_NEXT_PORT_ID, UNFILLED_PORT_ID, MISSING_PORT_ID;
2231
2345
  var init_flow_ports = __esm({
2232
2346
  "../contracts/src/flow-ports.ts"() {
2233
2347
  INCOMPLETE_PORT_ID = "incomplete";
2348
+ ASK_NEXT_PORT_ID = "next";
2349
+ UNFILLED_PORT_ID = "unfilled";
2350
+ MISSING_PORT_ID = "missing";
2351
+ }
2352
+ });
2353
+
2354
+ // ../contracts/src/flow-persona.ts
2355
+ function flowPersonaOf(config) {
2356
+ const prompt = config?.systemPrompt;
2357
+ return typeof prompt === "string" && prompt.trim() ? prompt : DEFAULT_FLOW_PERSONA;
2358
+ }
2359
+ function composeSystemPrompt(prompt, routingInstructions) {
2360
+ const routing = routingInstructions?.trim();
2361
+ return routing ? `${prompt}
2362
+
2363
+ # Routing
2364
+ ${routing}` : prompt;
2365
+ }
2366
+ var DEFAULT_FLOW_PERSONA;
2367
+ var init_flow_persona = __esm({
2368
+ "../contracts/src/flow-persona.ts"() {
2369
+ DEFAULT_FLOW_PERSONA = "You are a helpful assistant. Keep replies short and natural, and follow the steps of the flow you have been given.";
2370
+ }
2371
+ });
2372
+
2373
+ // ../contracts/src/question-text.ts
2374
+ function lastSentences(line) {
2375
+ const text = line.trim();
2376
+ if (!text) return [];
2377
+ const sentences = [];
2378
+ let last = 0;
2379
+ for (const m of text.matchAll(SENTENCE_END)) {
2380
+ const end = (m.index ?? 0) + m[0].length;
2381
+ sentences.push(text.slice(last, end));
2382
+ last = end;
2383
+ }
2384
+ if (last < text.length) sentences.push(text.slice(last));
2385
+ return sentences.slice(-2);
2386
+ }
2387
+ function isQuestion(line) {
2388
+ return lastSentences(line).some((s) => QUESTION_MARK.test(s));
2389
+ }
2390
+ var QUESTION_MARK, SENTENCE_END;
2391
+ var init_question_text = __esm({
2392
+ "../contracts/src/question-text.ts"() {
2393
+ QUESTION_MARK = /[??؟]/;
2394
+ SENTENCE_END = /[.!?…。!?؟]+["'”’)\]]*\s*/g;
2395
+ }
2396
+ });
2397
+
2398
+ // ../contracts/src/flow-ask-lines.ts
2399
+ function norm(value) {
2400
+ return typeof value === "string" ? value.trim() : "";
2401
+ }
2402
+ function fnv1a64(text) {
2403
+ let hash = FNV_OFFSET;
2404
+ for (const byte of new TextEncoder().encode(text)) {
2405
+ hash ^= BigInt(byte);
2406
+ hash = BigInt.asUintN(64, hash * FNV_PRIME);
2407
+ }
2408
+ return hash.toString(16).padStart(16, "0");
2409
+ }
2410
+ function askLinesBasis(input) {
2411
+ const canonical = JSON.stringify([
2412
+ ["persona", norm(input.persona)],
2413
+ ["routing", norm(input.routing)],
2414
+ ["language", norm(input.language).toLowerCase()],
2415
+ ["ask", norm(input.ask)],
2416
+ ["instructions", norm(input.instructions)]
2417
+ ]);
2418
+ return fnv1a64(canonical);
2419
+ }
2420
+ function isEnglishLanguage(language) {
2421
+ return norm(language).toLowerCase().startsWith("en");
2422
+ }
2423
+ function readAskGeneratedLines(value) {
2424
+ if (!value || typeof value !== "object") return null;
2425
+ const v = value;
2426
+ if (typeof v.basis !== "string" || !v.basis) return null;
2427
+ const reasks = Array.isArray(v.reasks) ? v.reasks.filter((x) => typeof x === "string" && x.trim() !== "") : [];
2428
+ return {
2429
+ reasks,
2430
+ basis: v.basis,
2431
+ ...typeof v.skipOffer === "string" && v.skipOffer.trim() ? { skipOffer: v.skipOffer } : {},
2432
+ ...typeof v.voicedAsk === "string" && v.voicedAsk.trim() ? { voicedAsk: v.voicedAsk } : {}
2433
+ };
2434
+ }
2435
+ function authorReasksOf(value) {
2436
+ return Array.isArray(value) ? value.filter((x) => typeof x === "string" && x.trim() !== "").map((x) => x.trim().slice(0, ASK_LINE_MAX_CHARS)).slice(0, ASK_REASKS_MAX) : [];
2437
+ }
2438
+ function defaultAskText(slot, nodeId) {
2439
+ const authored = slot === nodeId ? "" : slot.trim();
2440
+ const label = authored.replace(/[_-]+/g, " ");
2441
+ return label ? `Could you tell me your ${label}?` : "Could you tell me a bit more about what you need?";
2442
+ }
2443
+ var ASK_LINE_MAX_CHARS, ASK_INSTRUCTIONS_MAX_CHARS, ASK_REASKS_MAX, FNV_OFFSET, FNV_PRIME, DEFAULT_ASK_EXHAUSTED_MESSAGE;
2444
+ var init_flow_ask_lines = __esm({
2445
+ "../contracts/src/flow-ask-lines.ts"() {
2446
+ init_question_text();
2447
+ ASK_LINE_MAX_CHARS = 300;
2448
+ ASK_INSTRUCTIONS_MAX_CHARS = 1e3;
2449
+ ASK_REASKS_MAX = 3;
2450
+ FNV_OFFSET = BigInt("0xcbf29ce484222325");
2451
+ FNV_PRIME = BigInt("0x100000001b3");
2452
+ DEFAULT_ASK_EXHAUSTED_MESSAGE = "I wasn't able to get everything we need on this call, so let me have someone follow up with you. Thanks for your time.";
2453
+ }
2454
+ });
2455
+
2456
+ // ../contracts/src/flow-required-inputs.ts
2457
+ function templateFieldRefs(text) {
2458
+ if (typeof text !== "string" || text.indexOf("{{") === -1) return [];
2459
+ const out = [];
2460
+ for (const m of text.matchAll(TOKEN)) {
2461
+ const expr = (m[1] ?? "").trim();
2462
+ const name = expr.startsWith("slots.") ? expr.slice("slots.".length).trim() : expr;
2463
+ if (name && !out.includes(name)) out.push(name);
2464
+ }
2465
+ return out;
2466
+ }
2467
+ function stepInputNames(type, config) {
2468
+ const out = [];
2469
+ const add = (names) => {
2470
+ for (const n of names) if (!out.includes(n)) out.push(n);
2471
+ };
2472
+ if (type === "email") {
2473
+ for (const key of EMAIL_TEMPLATE_KEYS) add(templateFieldRefs(config[key]));
2474
+ return out;
2475
+ }
2476
+ if (type === "tool") {
2477
+ if (typeof config.toolRef === "string" && config.toolRef.trim()) return out;
2478
+ if (config.input && typeof config.input === "object") add(Object.keys(config.input));
2479
+ if (config.headers && typeof config.headers === "object") {
2480
+ for (const v of Object.values(config.headers)) add(templateFieldRefs(v));
2481
+ }
2482
+ }
2483
+ return out;
2484
+ }
2485
+ function requiredInputsFor(type, config, isRequired) {
2486
+ return stepInputNames(type, config).filter((n) => isRequired(n));
2487
+ }
2488
+ function requiredFieldNamesOf(nodes) {
2489
+ const out = /* @__PURE__ */ new Set();
2490
+ for (const node of nodes) {
2491
+ const config = node.config;
2492
+ if (node.type === "ask" && config.required !== false) {
2493
+ out.add((typeof config.slot === "string" && config.slot.trim() ? config.slot : node.id).trim());
2494
+ }
2495
+ if ((node.type === "playbook" || node.type === "gate") && Array.isArray(config.requiredFields)) {
2496
+ for (const f of config.requiredFields) if (typeof f === "string" && f.trim()) out.add(f.trim());
2497
+ }
2498
+ }
2499
+ return out;
2500
+ }
2501
+ function requiredFieldNames(graph) {
2502
+ return requiredFieldNamesOf(graph.nodes.map((n) => ({ id: n.id, type: n.type, config: n.data })));
2503
+ }
2504
+ var EMAIL_TEMPLATE_KEYS, TOKEN;
2505
+ var init_flow_required_inputs = __esm({
2506
+ "../contracts/src/flow-required-inputs.ts"() {
2507
+ init_flow_ports();
2508
+ EMAIL_TEMPLATE_KEYS = ["to", "subject", "body", "from", "replyTo", "fromName"];
2509
+ TOKEN = /\{\{\s*([^}]+?)\s*\}\}/g;
2234
2510
  }
2235
2511
  });
2236
2512
 
@@ -2333,6 +2609,15 @@ function asAdherence(value) {
2333
2609
  function asStepEntry(value) {
2334
2610
  return typeof value === "string" && STEP_ENTRIES.has(value) ? value : "auto";
2335
2611
  }
2612
+ function asMaxAttempts(value) {
2613
+ return typeof value === "number" && Number.isFinite(value) ? Math.min(6, Math.max(1, Math.round(value))) : ASK_DEFAULT_MAX_ATTEMPTS;
2614
+ }
2615
+ function asExhaustionPolicy(value) {
2616
+ return value === "fallback" || value === "end" ? value : "offer_skip";
2617
+ }
2618
+ function asRephrase(value) {
2619
+ return value === "all" || value === "never" ? value : "reasks";
2620
+ }
2336
2621
  function branchesFor(data) {
2337
2622
  return Array.isArray(data.branches) ? data.branches.filter((b) => b && typeof b.id === "string" && typeof b.label === "string").map((b) => ({
2338
2623
  id: b.id,
@@ -2350,18 +2635,32 @@ function nodeConfigFor(node) {
2350
2635
  case "say":
2351
2636
  case "confirm":
2352
2637
  return { text: typeof data.text === "string" ? data.text : "" };
2353
- case "ask":
2638
+ case "ask": {
2639
+ const required = data.required !== false;
2640
+ const instructions = typeof data.instructions === "string" ? data.instructions.trim() : "";
2641
+ const reasks = authorReasksOf(data.reasks);
2642
+ const exhaustedMessage = typeof data.exhaustedMessage === "string" ? data.exhaustedMessage.trim() : "";
2354
2643
  return {
2355
2644
  slot: typeof data.slot === "string" ? data.slot : node.id,
2356
- required: data.required !== false,
2645
+ required,
2357
2646
  type: asFieldType(data.type),
2358
2647
  historyPolicy: asHistoryPolicy(data.historyPolicy),
2359
2648
  ...typeof data.ask === "string" ? { ask: data.ask } : {},
2360
2649
  ...typeof data.pattern === "string" ? { pattern: data.pattern } : {},
2361
2650
  ...typeof data.min === "number" ? { min: data.min } : {},
2362
2651
  ...typeof data.max === "number" ? { max: data.max } : {},
2363
- ...Array.isArray(data.enum) ? { enum: data.enum.filter((x) => typeof x === "string") } : {}
2652
+ ...Array.isArray(data.enum) ? { enum: data.enum.filter((x) => typeof x === "string") } : {},
2653
+ // burn-down G-46/47/48. `maxAttempts` is ALWAYS emitted: its absence marks a program compiled before the
2654
+ // bounded ask, which the runtime keeps on today's loop until it is re-deployed.
2655
+ maxAttempts: asMaxAttempts(data.maxAttempts),
2656
+ ...required ? { onExhausted: asExhaustionPolicy(data.onExhausted) } : {},
2657
+ ...required && exhaustedMessage ? { exhaustedMessage } : {},
2658
+ rephrase: asRephrase(data.rephrase),
2659
+ ...instructions ? { instructions: instructions.slice(0, ASK_INSTRUCTIONS_MAX_CHARS) } : {},
2660
+ // the author's own re-ask wording wins and is never regenerated
2661
+ ...reasks.length ? { reasks } : {}
2364
2662
  };
2663
+ }
2365
2664
  case "llm":
2366
2665
  return {
2367
2666
  instructions: typeof data.instructions === "string" ? data.instructions : typeof data.text === "string" ? data.text : "",
@@ -2539,6 +2838,13 @@ function nodeConfigFor(node) {
2539
2838
  return typeof data.label === "string" ? { label: data.label } : {};
2540
2839
  }
2541
2840
  }
2841
+ function portless(edge) {
2842
+ return {
2843
+ to: edge.to,
2844
+ ...edge.condition !== void 0 ? { condition: edge.condition } : {},
2845
+ ...edge.label !== void 0 ? { label: edge.label } : {}
2846
+ };
2847
+ }
2542
2848
  function compileFlowToProgram(graph, id = "flow") {
2543
2849
  const stepNodes = graph.nodes.filter(
2544
2850
  (n) => !CAPABILITY_NODE_TYPES.has(n.type) && !ANNOTATION_NODE_TYPES.has(n.type)
@@ -2565,13 +2871,28 @@ function compileFlowToProgram(graph, id = "flow") {
2565
2871
  outBySource.set(edge.source, list);
2566
2872
  indeg.set(edge.target, (indeg.get(edge.target) ?? 0) + 1);
2567
2873
  }
2874
+ for (const node of stepNodes) {
2875
+ if (node.type !== "ask") continue;
2876
+ const list = outBySource.get(node.id);
2877
+ if (!list || !list.some((e) => e.sourceHandle === ASK_NEXT_PORT_ID)) continue;
2878
+ outBySource.set(
2879
+ node.id,
2880
+ list.filter((e) => e.sourceHandle !== void 0).map((e) => e.sourceHandle === ASK_NEXT_PORT_ID ? portless(e) : e)
2881
+ );
2882
+ }
2883
+ const requiredFields = requiredFieldNames(graph);
2884
+ const isRequired = (f) => requiredFields.has(f.trim());
2568
2885
  const nodes = {};
2569
2886
  const unsupportedNodes = [];
2570
2887
  for (const node of stepNodes) {
2888
+ const config = nodeConfigFor(node);
2889
+ if (node.type === "email" || node.type === "tool" && !config.toolRef) {
2890
+ config.requiredInputs = requiredInputsFor(node.type, config, isRequired);
2891
+ }
2571
2892
  nodes[node.id] = {
2572
2893
  id: node.id,
2573
2894
  type: node.type,
2574
- config: nodeConfigFor(node),
2895
+ config,
2575
2896
  // Defensive: the FlowProgram schema caps out[] at 64 — slice rather than
2576
2897
  // letting an over-wired node fail the read-side parse (which silently
2577
2898
  // degrades the whole call to the generic base prompt).
@@ -2600,11 +2921,14 @@ function compileFlowToProgram(graph, id = "flow") {
2600
2921
  }
2601
2922
  };
2602
2923
  }
2603
- var FIELD_TYPES, HTTP_METHODS, HISTORY_POLICIES, ADHERENCE_MODES, STEP_ENTRIES, UNSUPPORTED_NODE_TYPES, CAPABILITY_NODE_TYPES, ANNOTATION_NODE_TYPES;
2924
+ var FIELD_TYPES, HTTP_METHODS, HISTORY_POLICIES, ADHERENCE_MODES, STEP_ENTRIES, ASK_DEFAULT_MAX_ATTEMPTS, UNSUPPORTED_NODE_TYPES, CAPABILITY_NODE_TYPES, ANNOTATION_NODE_TYPES;
2604
2925
  var init_flow_compile = __esm({
2605
2926
  "../contracts/src/flow-compile.ts"() {
2606
2927
  init_flow_enrichment();
2607
2928
  init_flow_edges();
2929
+ init_flow_ports();
2930
+ init_flow_ask_lines();
2931
+ init_flow_required_inputs();
2608
2932
  FIELD_TYPES = [
2609
2933
  "string",
2610
2934
  "string[]",
@@ -2618,6 +2942,7 @@ var init_flow_compile = __esm({
2618
2942
  HISTORY_POLICIES = /* @__PURE__ */ new Set(["full", "last-turn", "slots-only"]);
2619
2943
  ADHERENCE_MODES = /* @__PURE__ */ new Set(["off", "observe", "enforce"]);
2620
2944
  STEP_ENTRIES = /* @__PURE__ */ new Set(["auto", "speak", "listen"]);
2945
+ ASK_DEFAULT_MAX_ATTEMPTS = 3;
2621
2946
  UNSUPPORTED_NODE_TYPES = /* @__PURE__ */ new Set([]);
2622
2947
  CAPABILITY_NODE_TYPES = /* @__PURE__ */ new Set(["memory", "trigger", "speech"]);
2623
2948
  ANNOTATION_NODE_TYPES = /* @__PURE__ */ new Set(["note"]);
@@ -2808,6 +3133,8 @@ var init_flow_validate = __esm({
2808
3133
  init_flow_ports();
2809
3134
  init_flow_edges();
2810
3135
  init_flow_compile_check();
3136
+ init_flow_required_inputs();
3137
+ init_flow_ask_lines();
2811
3138
  init_secret_headers();
2812
3139
  init_network_address();
2813
3140
  init_dispatch_metadata();
@@ -4335,6 +4662,7 @@ var init_monitoring = __esm({
4335
4662
  var WalletRefusalCode, WalletMode, WalletInvoiceStatus, WalletInvoice, WalletEntryKind, WalletEntry, TOPUP_MIN_USD, TOPUP_MAX_USD;
4336
4663
  var init_wallet = __esm({
4337
4664
  "../contracts/src/wallet.ts"() {
4665
+ init_budgets();
4338
4666
  WalletRefusalCode = z.enum([
4339
4667
  // Prepaid balance (or an invoiced credit limit) cannot cover the action.
4340
4668
  "insufficient_balance",
@@ -4344,14 +4672,18 @@ var init_wallet = __esm({
4344
4672
  // still answered. Paying the invoice restores the workspace.
4345
4673
  "account_suspended",
4346
4674
  // The action's reservation already closed (e.g. a call ended or expired) — it cannot be re-admitted.
4347
- "hold_closed"
4675
+ "hold_closed",
4676
+ // A hard budget (docs/specs/monetization LLD §8a) would be passed: new actions in its scope wait for its reset.
4677
+ "budget_exceeded"
4348
4678
  ]);
4349
4679
  z.object({
4350
4680
  error: WalletRefusalCode,
4351
4681
  code: WalletRefusalCode,
4352
4682
  availableUsd: z.string(),
4353
4683
  needUsd: z.string(),
4354
- message: z.string()
4684
+ message: z.string(),
4685
+ // budget_exceeded only: which budget, and when it resets.
4686
+ budget: BudgetRefusalDetail.optional()
4355
4687
  });
4356
4688
  WalletMode = z.enum(["prepaid", "invoiced"]);
4357
4689
  z.object({
@@ -4392,7 +4724,7 @@ var init_wallet = __esm({
4392
4724
  payUrl: z.string().nullable()
4393
4725
  });
4394
4726
  z.object({ invoices: z.array(WalletInvoice) });
4395
- WalletEntryKind = z.enum(["grant", "topup", "usage", "refund", "refund_reversal", "dispute", "dispute_reinstated", "adjustment", "invoice_payment"]);
4727
+ WalletEntryKind = z.enum(["grant", "topup", "usage", "usage_correction", "refund", "refund_reversal", "dispute", "dispute_reinstated", "adjustment", "invoice_payment"]);
4396
4728
  WalletEntry = z.object({
4397
4729
  id: z.string(),
4398
4730
  kind: WalletEntryKind,
@@ -4436,6 +4768,81 @@ var init_pipeline_keys = __esm({
4436
4768
  });
4437
4769
  }
4438
4770
  });
4771
+ var METERING_SCHEMA_VERSION, METER_IDS, MeterId, KEY_OWNERS, READING_SOURCES, SUBJECT_KINDS, SubjectKind, MeteringSubject, DimValue, IdentityDims, Attributes, SERIES_PATTERN, SeriesName, Quantity, MeterReadingInput;
4772
+ var init_metering = __esm({
4773
+ "../contracts/src/metering.ts"() {
4774
+ METERING_SCHEMA_VERSION = 2;
4775
+ METER_IDS = [
4776
+ // A model's prompt tokens as the provider counts them (cached ones included); `cached_input_tokens` is the cached
4777
+ // subset and `reasoning_tokens` the reasoning subset of `output_tokens`. Rating prices the totals until it prices the
4778
+ // subsets on their own (monetization MON-14).
4779
+ "llm.input_tokens",
4780
+ "llm.cached_input_tokens",
4781
+ "llm.output_tokens",
4782
+ "llm.reasoning_tokens",
4783
+ "embedding.input_tokens",
4784
+ "realtime.audio_input_tokens",
4785
+ "realtime.audio_output_tokens",
4786
+ "realtime.text_input_tokens",
4787
+ "realtime.cached_input_tokens",
4788
+ "realtime.text_output_tokens",
4789
+ "stt.billed_seconds",
4790
+ "tts.characters",
4791
+ "call.connected_seconds",
4792
+ "telephony.carrier_seconds",
4793
+ "media.participant_seconds",
4794
+ "media.sip_seconds",
4795
+ "recording.seconds",
4796
+ "storage.byte_days",
4797
+ "message.segments",
4798
+ "email.messages",
4799
+ // One per billable text action — a text turn, or a single model action the API takes for a workspace (a QA review,
4800
+ // an evaluation, a planner call, a knowledge ingest): the per-message fee's basis.
4801
+ "text.turns",
4802
+ // Informational: number rental is a recurring fee on charge_cycles (🔒 B-21).
4803
+ "number.held_days",
4804
+ "provider_fee.micros"
4805
+ ];
4806
+ MeterId = z.enum(METER_IDS);
4807
+ KEY_OWNERS = ["platform", "workspace_key", "workspace_plan", "workspace_brain", "self_hosted"];
4808
+ z.enum(KEY_OWNERS);
4809
+ READING_SOURCES = ["server_observed", "platform_wire", "platform_estimate", "customer_report"];
4810
+ z.enum(READING_SOURCES);
4811
+ SUBJECT_KINDS = ["call", "turn", "job", "message", "email", "number"];
4812
+ SubjectKind = z.enum(SUBJECT_KINDS);
4813
+ MeteringSubject = z.object({ kind: SubjectKind, id: z.string().min(1).max(200) });
4814
+ DimValue = z.string().max(200).nullable();
4815
+ IdentityDims = z.record(z.string(), DimValue);
4816
+ Attributes = z.object({
4817
+ served_model: z.string().max(200).nullable(),
4818
+ channel: z.string().max(64).nullable(),
4819
+ direction: z.string().max(32).nullable(),
4820
+ leg_kind: z.string().max(64).nullable(),
4821
+ country: z.string().max(8).nullable(),
4822
+ worker_owner: z.enum(["platform", "self_hosted", "unknown"]).nullable()
4823
+ });
4824
+ SERIES_PATTERN = /^(req|ws|srv):[A-Za-z0-9._:-]{1,60}$/;
4825
+ SeriesName = z.string().max(64).regex(SERIES_PATTERN);
4826
+ Quantity = z.string().regex(/^\d{1,14}(\.\d{1,6})?$/);
4827
+ MeterReadingInput = z.object({
4828
+ schemaVersion: z.literal(METERING_SCHEMA_VERSION),
4829
+ subject: MeteringSubject,
4830
+ meter: MeterId,
4831
+ identity: IdentityDims,
4832
+ series: SeriesName,
4833
+ quantity: Quantity,
4834
+ final: z.boolean(),
4835
+ void: z.literal(true).optional(),
4836
+ // The pre-send input estimate of a request that may never answer (LLD §4.1). A client never names a source: the
4837
+ // server derives it from the principal (§4.4), and this flag is the one thing only the producer knows. Honoured
4838
+ // only from the call's own worker token; it ranks below the request's wire count, which supersedes it.
4839
+ estimate: z.literal(true).optional(),
4840
+ occurredAt: z.string().datetime({ offset: true }),
4841
+ attributes: Attributes.partial().optional()
4842
+ }).strict();
4843
+ z.object({ readings: z.array(MeterReadingInput).min(1).max(200) }).strict();
4844
+ }
4845
+ });
4439
4846
 
4440
4847
  // ../contracts/src/index.ts
4441
4848
  var init_src = __esm({
@@ -4472,6 +4879,10 @@ var init_src = __esm({
4472
4879
  init_flow();
4473
4880
  init_flow_program();
4474
4881
  init_flow_ports();
4882
+ init_flow_persona();
4883
+ init_flow_ask_lines();
4884
+ init_flow_required_inputs();
4885
+ init_question_text();
4475
4886
  init_flow_validate();
4476
4887
  init_flow_builtins();
4477
4888
  init_compliance();
@@ -4500,16 +4911,73 @@ var init_src = __esm({
4500
4911
  init_channel_connector();
4501
4912
  init_monitoring();
4502
4913
  init_wallet();
4914
+ init_budgets();
4503
4915
  init_network_address();
4504
4916
  init_dispatch_metadata();
4505
4917
  init_pipeline_keys();
4918
+ init_metering();
4919
+ }
4920
+ });
4921
+
4922
+ // src/runtime/graph/graph-model.ts
4923
+ function buildGraphModel(program) {
4924
+ return {
4925
+ // the program's completion gate is the compiler's own subset of these (a gate's fields, else the required asks') —
4926
+ // read too, so a program carries one answer however it was produced
4927
+ requiredFields: /* @__PURE__ */ new Set([
4928
+ ...requiredFieldNamesOf(Object.values(program.nodes)),
4929
+ ...program.completionGate.requiredFields.map((f) => f.trim())
4930
+ ]),
4931
+ entry: program.entry,
4932
+ node: (id) => program.nodes[id],
4933
+ out: (id) => program.nodes[id]?.out ?? [],
4934
+ selectByPort: (id, port) => {
4935
+ const edges = program.nodes[id]?.out ?? [];
4936
+ const byHandle = edges.find((e) => e.sourceHandle === port);
4937
+ if (byHandle) return byHandle.to;
4938
+ const byLabel = edges.find((e) => e.label === port);
4939
+ return byLabel ? byLabel.to : null;
4940
+ },
4941
+ askNodeForSlot: (slot) => {
4942
+ const want = slot.trim();
4943
+ for (const node of Object.values(program.nodes)) {
4944
+ if (node.type !== "ask") continue;
4945
+ const s = typeof node.config.slot === "string" ? node.config.slot : node.id;
4946
+ if (s.trim() === want) return node.id;
4947
+ }
4948
+ return void 0;
4949
+ }
4950
+ };
4951
+ }
4952
+ var init_graph_model = __esm({
4953
+ "src/runtime/graph/graph-model.ts"() {
4954
+ init_src();
4955
+ }
4956
+ });
4957
+
4958
+ // src/runtime/graph/reask-templates.ts
4959
+ function templateReasks(ask) {
4960
+ const q = ask.trim();
4961
+ return [`${NOT_CAUGHT} ${q}`, `Let me ask that another way \u2014 ${q}`, `Just so I get it right: ${q}`];
4962
+ }
4963
+ function notCaught(line) {
4964
+ return `${NOT_CAUGHT} ${line.trim()}`;
4965
+ }
4966
+ function skipOfferTemplate(label) {
4967
+ const what = label.trim() ? `your ${label.trim()}` : "that detail";
4968
+ return `No problem if you don't have ${what} handy \u2014 shall we skip it for now?`;
4969
+ }
4970
+ var NOT_CAUGHT;
4971
+ var init_reask_templates = __esm({
4972
+ "src/runtime/graph/reask-templates.ts"() {
4973
+ NOT_CAUGHT = "Sorry, I didn't catch that.";
4506
4974
  }
4507
4975
  });
4508
4976
 
4509
4977
  // src/runtime/graph/interpolate.ts
4510
4978
  function interpolate(template, scope2) {
4511
4979
  if (!template || template.indexOf("{{") === -1) return template;
4512
- return template.replace(TOKEN, (_match, expr) => {
4980
+ return template.replace(TOKEN2, (_match, expr) => {
4513
4981
  const value = resolvePath(scope2, expr.trim());
4514
4982
  return value === void 0 || value === null ? "" : String(value);
4515
4983
  });
@@ -4534,10 +5002,10 @@ function hasSlot(slots, name) {
4534
5002
  const v = resolvePath(slots, name.trim());
4535
5003
  return v !== void 0 && v !== null && v !== "";
4536
5004
  }
4537
- var TOKEN;
5005
+ var TOKEN2;
4538
5006
  var init_interpolate = __esm({
4539
5007
  "src/runtime/graph/interpolate.ts"() {
4540
- TOKEN = /\{\{\s*([^}]+?)\s*\}\}/g;
5008
+ TOKEN2 = /\{\{\s*([^}]+?)\s*\}\}/g;
4541
5009
  }
4542
5010
  });
4543
5011
 
@@ -4596,32 +5064,116 @@ function acceptsReasoningEffort(model2) {
4596
5064
  const id = baseModelId(model2);
4597
5065
  return isReasoningModel(model2) && !/^o1-(mini|preview)/.test(id) && !/-chat(-|$)/.test(id);
4598
5066
  }
5067
+ function acceptsNoReasoningEffort(model2) {
5068
+ if (!acceptsReasoningEffort(model2))
5069
+ return false;
5070
+ const id = baseModelId(model2);
5071
+ if (/-pro\b/.test(id))
5072
+ return false;
5073
+ const gpt = /^gpt-(\d+)(?:\.(\d+))?/.exec(id);
5074
+ if (!gpt)
5075
+ return false;
5076
+ const major = Number(gpt[1]);
5077
+ const minor = gpt[2] !== void 0 ? Number(gpt[2]) : 0;
5078
+ return major > 5 || major === 5 && minor >= 1;
5079
+ }
5080
+ function forgetLearnedReasoningEfforts() {
5081
+ learnedEfforts.clear();
5082
+ }
5083
+ function chatReasoningEffort(model2, opts) {
5084
+ const learned = learnedEfforts.get(learnedKey(model2, opts.tools === true));
5085
+ if (learned !== void 0)
5086
+ return learned === "omit" ? void 0 : learned;
5087
+ if (!acceptsReasoningEffort(model2))
5088
+ return void 0;
5089
+ if (opts.tools === true && acceptsNoReasoningEffort(model2))
5090
+ return "none";
5091
+ return opts.requested;
5092
+ }
4599
5093
  function chatCompletionParams(model2, input) {
5094
+ const effort = chatReasoningEffort(model2, {
5095
+ ...input.tools !== void 0 ? { tools: input.tools } : {},
5096
+ ...input.reasoningEffort !== void 0 ? { requested: input.reasoningEffort } : {}
5097
+ });
4600
5098
  if (isReasoningModel(model2)) {
4601
5099
  return {
4602
5100
  ...input.maxTokens !== void 0 ? { max_completion_tokens: Math.max(input.maxTokens, REASONING_MIN_COMPLETION_TOKENS) } : {},
4603
- ...input.reasoningEffort !== void 0 && acceptsReasoningEffort(model2) ? { reasoning_effort: input.reasoningEffort } : {}
5101
+ ...effort !== void 0 ? { reasoning_effort: effort } : {}
4604
5102
  };
4605
5103
  }
4606
5104
  return {
5105
+ // only when a provider taught the process that this model (one we don't know as a reasoning model) takes an effort
5106
+ ...effort !== void 0 ? { reasoning_effort: effort } : {},
4607
5107
  ...input.maxTokens !== void 0 ? { max_tokens: input.maxTokens } : {},
4608
5108
  ...input.temperature !== void 0 ? { temperature: input.temperature } : {},
4609
5109
  ...input.topP !== void 0 ? { top_p: input.topP } : {}
4610
5110
  };
4611
5111
  }
4612
- function modelRejectionOf(err) {
5112
+ function acceptsSamplingParams(model2) {
5113
+ return !isReasoningModel(model2);
5114
+ }
5115
+ function providerErrorOf(err) {
5116
+ if (err === null || typeof err !== "object")
5117
+ return null;
4613
5118
  const e = err;
4614
- const status = typeof e?.status === "number" ? e.status : null;
4615
- if (status !== 400 && status !== 403 && status !== 404)
5119
+ const body = e["body"] !== null && typeof e["body"] === "object" ? e["body"] : {};
5120
+ const pick = (k) => e[k] !== void 0 && e[k] !== null ? e[k] : body[k];
5121
+ const statusRaw = typeof e["status"] === "number" ? e["status"] : e["statusCode"];
5122
+ const status = typeof statusRaw === "number" && statusRaw > 0 ? statusRaw : null;
5123
+ const code = pick("code");
5124
+ const type = pick("type");
5125
+ const param = pick("param");
5126
+ const message = typeof body["message"] === "string" ? body["message"] : typeof e["message"] === "string" ? e["message"] : "";
5127
+ return {
5128
+ status,
5129
+ code: typeof code === "string" ? code : typeof type === "string" ? type : "",
5130
+ param: typeof param === "string" ? param : null,
5131
+ message
5132
+ };
5133
+ }
5134
+ function reasoningEffortRejectionOf(err) {
5135
+ const f = providerErrorOf(err);
5136
+ if (!f || f.status !== 400)
5137
+ return null;
5138
+ if (f.param !== "reasoning_effort" && !/reasoning[_ ]effort/i.test(f.message))
5139
+ return null;
5140
+ return { status: f.status, message: f.message, suggestsNone: /reasoning[_ ]effort[^.]*\bnone\b/i.test(f.message) };
5141
+ }
5142
+ function learnReasoningEffort(model2, shape, err) {
5143
+ const rejection = reasoningEffortRejectionOf(err);
5144
+ if (!rejection)
5145
+ return false;
5146
+ const next = shape.sent !== "none" && (rejection.suggestsNone || shape.sent === void 0) ? "none" : "omit";
5147
+ if ((next === "omit" ? void 0 : next) === shape.sent)
5148
+ return false;
5149
+ learnedEfforts.set(learnedKey(model2, shape.tools), next);
5150
+ return true;
5151
+ }
5152
+ async function withReasoningEffortRetry(shape, call) {
5153
+ const sent = chatReasoningEffort(shape.model, {
5154
+ tools: shape.tools,
5155
+ ...shape.requested !== void 0 ? { requested: shape.requested } : {}
5156
+ });
5157
+ try {
5158
+ return await call();
5159
+ } catch (err) {
5160
+ if (!learnReasoningEffort(shape.model, { tools: shape.tools, sent }, err))
5161
+ throw err;
5162
+ return call();
5163
+ }
5164
+ }
5165
+ function modelRejectionOf(err) {
5166
+ const f = providerErrorOf(err);
5167
+ const status = f?.status ?? null;
5168
+ if (!f || status === null || status !== 400 && status !== 403 && status !== 404)
4616
5169
  return null;
4617
- const code = typeof e?.code === "string" ? e.code : typeof e?.type === "string" ? e.type : "";
4618
- const param = typeof e?.param === "string" ? e.param : null;
4619
- const rejected = REJECTION_CODES.has(code) || param !== null && MODEL_PARAMS.has(param);
5170
+ const { code, param } = f;
5171
+ const rejected = REJECTION_CODES.has(code) || param !== null && MODEL_PARAMS.has(param) || reasoningEffortRejectionOf(err) !== null;
4620
5172
  if (!rejected)
4621
5173
  return null;
4622
5174
  if (status === 403 && code !== "model_not_found")
4623
5175
  return null;
4624
- return { status, code: code || "invalid_request_error", message: typeof e?.message === "string" ? e.message : "" };
5176
+ return { status, code: code || "invalid_request_error", message: f.message };
4625
5177
  }
4626
5178
  async function withModelFallback(opts) {
4627
5179
  const fallbackModel = opts.fallbackModel ?? PLATFORM_DEFAULT_CHAT_MODEL;
@@ -4642,11 +5194,13 @@ async function withModelFallback(opts) {
4642
5194
  return opts.call(fallbackModel);
4643
5195
  }
4644
5196
  }
4645
- var PLATFORM_DEFAULT_CHAT_MODEL, REASONING_MIN_COMPLETION_TOKENS, REJECTION_CODES, MODEL_PARAMS, MODEL_REJECTION_TTL_MS, ModelRejectionCache;
5197
+ var PLATFORM_DEFAULT_CHAT_MODEL, REASONING_MIN_COMPLETION_TOKENS, learnedEfforts, learnedKey, REJECTION_CODES, MODEL_PARAMS, MODEL_REJECTION_TTL_MS, ModelRejectionCache;
4646
5198
  var init_chat_params = __esm({
4647
5199
  "../llm-client/dist/chat-params.js"() {
4648
5200
  PLATFORM_DEFAULT_CHAT_MODEL = "gpt-4o-mini";
4649
5201
  REASONING_MIN_COMPLETION_TOKENS = 2048;
5202
+ learnedEfforts = /* @__PURE__ */ new Map();
5203
+ learnedKey = (model2, tools) => `${baseModelId(model2)}|${tools ? "tools" : "plain"}`;
4650
5204
  REJECTION_CODES = /* @__PURE__ */ new Set(["unsupported_parameter", "unsupported_value", "model_not_found"]);
4651
5205
  MODEL_PARAMS = /* @__PURE__ */ new Set(["model", "max_tokens", "max_completion_tokens", "temperature", "top_p", "reasoning_effort"]);
4652
5206
  MODEL_REJECTION_TTL_MS = 5 * 60 * 1e3;
@@ -4674,6 +5228,30 @@ var init_chat_params = __esm({
4674
5228
  };
4675
5229
  }
4676
5230
  });
5231
+
5232
+ // ../llm-client/dist/index.js
5233
+ var dist_exports = {};
5234
+ __export(dist_exports, {
5235
+ MODEL_REJECTION_TTL_MS: () => MODEL_REJECTION_TTL_MS,
5236
+ ModelRejectionCache: () => ModelRejectionCache,
5237
+ PLATFORM_DEFAULT_CHAT_MODEL: () => PLATFORM_DEFAULT_CHAT_MODEL,
5238
+ REASONING_MIN_COMPLETION_TOKENS: () => REASONING_MIN_COMPLETION_TOKENS,
5239
+ acceptsNoReasoningEffort: () => acceptsNoReasoningEffort,
5240
+ acceptsReasoningEffort: () => acceptsReasoningEffort,
5241
+ acceptsSamplingParams: () => acceptsSamplingParams,
5242
+ chatCompletionParams: () => chatCompletionParams,
5243
+ chatReasoningEffort: () => chatReasoningEffort,
5244
+ createChatClient: () => createChatClient,
5245
+ forgetLearnedReasoningEfforts: () => forgetLearnedReasoningEfforts,
5246
+ hasChatKey: () => hasChatKey,
5247
+ isReasoningModel: () => isReasoningModel,
5248
+ learnReasoningEffort: () => learnReasoningEffort,
5249
+ modelRejectionOf: () => modelRejectionOf,
5250
+ providerErrorOf: () => providerErrorOf,
5251
+ reasoningEffortRejectionOf: () => reasoningEffortRejectionOf,
5252
+ withModelFallback: () => withModelFallback,
5253
+ withReasoningEffortRetry: () => withReasoningEffortRetry
5254
+ });
4677
5255
  function createChatClient(config = {}) {
4678
5256
  return new OpenAI({
4679
5257
  ...config.apiKey ? { apiKey: config.apiKey } : {},
@@ -4681,6 +5259,9 @@ function createChatClient(config = {}) {
4681
5259
  ...config.timeoutMs ? { timeout: config.timeoutMs } : {}
4682
5260
  });
4683
5261
  }
5262
+ function hasChatKey(config = {}) {
5263
+ return Boolean(config.apiKey || process.env["OPENAI_API_KEY"]);
5264
+ }
4684
5265
  var init_dist = __esm({
4685
5266
  "../llm-client/dist/index.js"() {
4686
5267
  init_chat_params();
@@ -5139,308 +5720,9 @@ var init_wrap = __esm({
5139
5720
  "src/providers/wrap.ts"() {
5140
5721
  }
5141
5722
  });
5142
-
5143
- // src/providers/index.ts
5144
- async function importOptional(spec, hint) {
5145
- try {
5146
- return await import(spec);
5147
- } catch {
5148
- throw new Error(`${hint}: "${spec}" is not installed. Add it with: pnpm add ${spec}`);
5149
- }
5150
- }
5151
- function withCreds(opts, creds2) {
5152
- if (creds2?.apiKey) opts["apiKey"] = creds2.apiKey;
5153
- if (creds2?.baseURL) opts["baseURL"] = creds2.baseURL;
5154
- return opts;
5155
- }
5156
- var deepgram, openai, anthropic, cartesia, elevenlabs, assemblyai, google, silero, livekitTurn, connector;
5157
- var init_providers2 = __esm({
5158
- "src/providers/index.ts"() {
5159
- init_ssrf();
5160
- init_transport_callback();
5161
- init_connector_llm();
5162
- init_wrap();
5163
- deepgram = {
5164
- stt(options = {}) {
5165
- return async () => {
5166
- const dg = await importOptional("@voicelayer/agents-plugin-deepgram", "deepgram.stt");
5167
- return new dg.STT(
5168
- withCreds(
5169
- { model: options.model ?? "nova-3", language: options.language ?? "en-US" },
5170
- options
5171
- )
5172
- );
5173
- };
5174
- },
5175
- tts(options = {}) {
5176
- return async () => {
5177
- const dg = await importOptional("@voicelayer/agents-plugin-deepgram", "deepgram.tts");
5178
- return new dg.TTS(
5179
- withCreds({ model: options.model ?? "aura-2-harmonia-en" }, options)
5180
- );
5181
- };
5182
- }
5183
- };
5184
- openai = {
5185
- llm(options = {}) {
5186
- return async () => {
5187
- const oa = await importOptional("@voicelayer/agents-plugin-openai", "openai.llm");
5188
- return new oa.LLM(
5189
- withCreds({ model: options.model ?? "gpt-4o-mini" }, options)
5190
- );
5191
- };
5192
- },
5193
- tts(options = {}) {
5194
- return async () => {
5195
- const oa = await importOptional("@voicelayer/agents-plugin-openai", "openai.tts");
5196
- const opts = { model: options.model ?? "tts-1" };
5197
- if (options.voice) opts["voice"] = options.voice;
5198
- if (options.instructions) opts["instructions"] = options.instructions;
5199
- return new oa.TTS(withCreds(opts, options));
5200
- };
5201
- },
5202
- // Speech-to-speech via the OpenAI Realtime API. Returns a RealtimeProvider
5203
- // that the SDK uses in place of the stt/llm/tts pipeline.
5204
- realtime(options = {}) {
5205
- return async () => {
5206
- const oa = await importOptional("@voicelayer/agents-plugin-openai", "openai.realtime");
5207
- const opts = {};
5208
- if (options.model) opts["model"] = options.model;
5209
- if (options.voice) opts["voice"] = options.voice;
5210
- return new oa.realtime.RealtimeModel(withCreds(opts, options));
5211
- };
5212
- }
5213
- };
5214
- anthropic = {
5215
- llm(options = {}) {
5216
- return async () => {
5217
- const an = await importOptional("@livekit/agents-plugin-anthropic", "anthropic.llm");
5218
- return new an.LLM(
5219
- withCreds({ model: options.model ?? "claude-sonnet-5-5" }, options)
5220
- );
5221
- };
5222
- }
5223
- };
5224
- cartesia = {
5225
- tts(options = {}) {
5226
- return async () => {
5227
- const ct = await importOptional("@voicelayer/agents-plugin-cartesia", "cartesia.tts");
5228
- return new ct.TTS(options);
5229
- };
5230
- },
5231
- stt(options = {}) {
5232
- return async () => {
5233
- const ct = await importOptional("@voicelayer/agents-plugin-cartesia", "cartesia.stt");
5234
- const opts = {};
5235
- if (options.model) opts["model"] = options.model;
5236
- if (options.language) opts["language"] = options.language;
5237
- return new ct.STT(withCreds(opts, options));
5238
- };
5239
- }
5240
- };
5241
- elevenlabs = {
5242
- tts(options = {}) {
5243
- return async () => {
5244
- const el = await importOptional("@voicelayer/agents-plugin-elevenlabs", "elevenlabs.tts");
5245
- const opts = {};
5246
- if (options.voice) opts["voiceId"] = options.voice;
5247
- if (options.model) opts["model"] = options.model;
5248
- return new el.TTS(withCreds(opts, options));
5249
- };
5250
- }
5251
- };
5252
- assemblyai = {
5253
- stt(options = {}) {
5254
- return async () => {
5255
- const aai = await importOptional("@voicelayer/agents-plugin-assemblyai", "assemblyai.stt");
5256
- const opts = {};
5257
- if (options.language) opts["language"] = options.language;
5258
- return new aai.STT(withCreds(opts, options));
5259
- };
5260
- }
5261
- };
5262
- google = {
5263
- llm(options = {}) {
5264
- return async () => {
5265
- const g = await importOptional("@voicelayer/agents-plugin-google", "google.llm");
5266
- return new g.LLM(
5267
- withCreds({ model: options.model ?? "gemini-3.8-flash" }, options)
5268
- );
5269
- };
5270
- },
5271
- realtime(options = {}) {
5272
- return async () => {
5273
- const g = await importOptional("@voicelayer/agents-plugin-google", "google.realtime");
5274
- const opts = {};
5275
- if (options.model) opts["model"] = options.model;
5276
- if (options.voice) opts["voice"] = options.voice;
5277
- return new g.beta.realtime.RealtimeModel(withCreds(opts, options));
5278
- };
5279
- }
5280
- };
5281
- silero = {
5282
- vad() {
5283
- return async () => {
5284
- const sil = await importOptional("@voicelayer/agents-plugin-silero", "silero.vad");
5285
- return await sil.VAD.load();
5286
- };
5287
- }
5288
- };
5289
- livekitTurn = {
5290
- english() {
5291
- return async () => {
5292
- const lk = await importOptional("@voicelayer/agents-plugin-livekit", "livekitTurn.english");
5293
- return new lk.turnDetector.EnglishModel();
5294
- };
5295
- },
5296
- multilingual() {
5297
- return async () => {
5298
- const lk = await importOptional("@voicelayer/agents-plugin-livekit", "livekitTurn.multilingual");
5299
- return new lk.turnDetector.MultilingualModel();
5300
- };
5301
- }
5302
- };
5303
- connector = {
5304
- llm(config = {}) {
5305
- return async (call) => {
5306
- if (config.url) {
5307
- await assertPublicHttpsUrl(
5308
- config.url,
5309
- config.allowHosts ? { allowHosts: config.allowHosts } : {}
5310
- );
5311
- return openai.llm({
5312
- ...config.model ? { model: config.model } : {},
5313
- ...config.apiKey ? { apiKey: config.apiKey } : {},
5314
- baseURL: config.url
5315
- })(call);
5316
- }
5317
- const transport = config.transport ?? (config.onQuery ? callbackTransport(config.onQuery) : void 0);
5318
- if (!transport) {
5319
- throw new Error("connector.llm requires one of: url, onQuery, or transport");
5320
- }
5321
- const projectId = typeof call.metadata["projectId"] === "string" ? call.metadata["projectId"] : void 0;
5322
- const llmOptions = {
5323
- ...config.model ? { model: config.model } : {},
5324
- ...config.temperature !== void 0 ? { temperature: config.temperature } : {},
5325
- ...config.fallbackText ? { fallbackText: config.fallbackText } : {},
5326
- ...call.callId ? { callId: call.callId } : {},
5327
- ...projectId ? { projectId } : {}
5328
- };
5329
- return await createConnectorLLM(transport, llmOptions);
5330
- };
5331
- }
5332
- };
5333
- }
5334
- });
5335
-
5336
- // src/providers/registry.ts
5337
- function entry(id, bits) {
5338
- const identity = providerIdentity(id);
5339
- if (!identity) throw new Error(`PROVIDER_REGISTRY: no provider identity in contracts for '${id}'`);
5340
- return { name: identity.id, capabilities: identity.capabilities, ...bits };
5341
- }
5342
- function providerEntry(name) {
5343
- if (!name) return void 0;
5344
- return PROVIDER_REGISTRY[PROVIDER_ALIASES[name] ?? name];
5345
- }
5346
- function resolveStt(name, o) {
5347
- return (providerEntry(name)?.stt ?? PROVIDER_REGISTRY["deepgram"].stt)(o);
5348
- }
5349
- function resolveTts(name, o) {
5350
- return (providerEntry(name)?.tts ?? PROVIDER_REGISTRY["deepgram"].tts)(o);
5351
- }
5352
- function resolveLlm(name, o) {
5353
- return (providerEntry(name)?.llm ?? PROVIDER_REGISTRY["openai"].llm)(withCurrentModel(name, o));
5354
- }
5355
- function resolveRealtime(name, o) {
5356
- return (providerEntry(name)?.realtime ?? PROVIDER_REGISTRY["openai"].realtime)(withCurrentModel(name, o));
5357
- }
5358
- function withCurrentModel(name, o) {
5359
- if (!name || !o.model) return o;
5360
- const current = currentModelFor(name, o.model);
5361
- if (!current.retired) return o;
5362
- console.warn("[agent] the configured model is retired; running its replacement", {
5363
- provider: name,
5364
- model: current.retired,
5365
- replacement: current.model
5366
- });
5367
- return { ...o, model: current.model };
5368
- }
5369
- var model, lang, voice, creds, PROVIDER_REGISTRY;
5370
- var init_registry = __esm({
5371
- "src/providers/registry.ts"() {
5372
- init_providers2();
5373
- init_src();
5374
- model = (o) => o.model ? { model: o.model } : {};
5375
- lang = (o) => o.language ? { language: o.language } : {};
5376
- voice = (o) => o.voice ? { voice: o.voice } : {};
5377
- creds = (o) => o.creds ?? {};
5378
- PROVIDER_REGISTRY = {
5379
- deepgram: entry("deepgram", {
5380
- failureClass: "vendor-api",
5381
- engines: ["livekit"],
5382
- stt: (o) => deepgram.stt({ ...model(o), ...lang(o), ...creds(o) }),
5383
- // Deepgram folds the voice into the model id (aura-2-<name>-en): wire config
5384
- // passes a voice, code config passes a model — both land as `model`.
5385
- tts: (o) => {
5386
- const m = o.voice ?? o.model;
5387
- return deepgram.tts({ ...m ? { model: m } : {}, ...creds(o) });
5388
- }
5389
- }),
5390
- openai: entry("openai", {
5391
- failureClass: "vendor-api",
5392
- engines: ["livekit", "text"],
5393
- llm: (o) => openai.llm({ ...model(o), ...creds(o) }),
5394
- tts: (o) => openai.tts({ ...voice(o), ...model(o), ...creds(o) }),
5395
- realtime: (o) => openai.realtime({ ...model(o), ...voice(o), ...creds(o) })
5396
- }),
5397
- anthropic: entry("anthropic", {
5398
- failureClass: "vendor-api",
5399
- engines: ["livekit", "text"],
5400
- llm: (o) => anthropic.llm({ ...model(o), ...creds(o) })
5401
- }),
5402
- google: entry("google", {
5403
- failureClass: "vendor-api",
5404
- engines: ["livekit", "text"],
5405
- llm: (o) => google.llm({ ...model(o), ...creds(o) }),
5406
- realtime: (o) => google.realtime({ ...model(o), ...voice(o), ...creds(o) })
5407
- }),
5408
- cartesia: entry("cartesia", {
5409
- failureClass: "vendor-api",
5410
- engines: ["livekit"],
5411
- stt: (o) => cartesia.stt({ ...model(o), ...lang(o), ...creds(o) }),
5412
- tts: (o) => cartesia.tts({ ...voice(o), ...model(o), ...creds(o) })
5413
- }),
5414
- elevenlabs: entry("elevenlabs", {
5415
- failureClass: "vendor-api",
5416
- engines: ["livekit"],
5417
- tts: (o) => elevenlabs.tts({ ...voice(o), ...model(o), ...creds(o) })
5418
- }),
5419
- assemblyai: entry("assemblyai", {
5420
- failureClass: "vendor-api",
5421
- engines: ["livekit"],
5422
- stt: (o) => assemblyai.stt({ ...lang(o), ...creds(o) })
5423
- // no model knob
5424
- }),
5425
- silero: entry("silero", {
5426
- failureClass: "local",
5427
- engines: ["livekit"],
5428
- vad: () => silero.vad()
5429
- })
5430
- };
5431
- }
5432
- });
5433
-
5434
- // src/providers/llm.ts
5435
- var init_llm = __esm({
5436
- "src/providers/llm.ts"() {
5437
- init_openai_default();
5438
- init_registry();
5439
- }
5440
- });
5441
- function metricExportIntervalMs() {
5442
- const raw = Number(process.env.OTEL_METRIC_EXPORT_INTERVAL);
5443
- return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_METRIC_EXPORT_INTERVAL_MS;
5723
+ function metricExportIntervalMs() {
5724
+ const raw = Number(process.env.OTEL_METRIC_EXPORT_INTERVAL);
5725
+ return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_METRIC_EXPORT_INTERVAL_MS;
5444
5726
  }
5445
5727
  function start(opts) {
5446
5728
  if (started)
@@ -5491,15 +5773,22 @@ function start(opts) {
5491
5773
  ]
5492
5774
  });
5493
5775
  sdk.start();
5494
- const shutdown = async () => {
5495
- try {
5496
- await sdk?.shutdown();
5497
- await loggerProvider?.shutdown();
5498
- } catch {
5499
- }
5500
- };
5501
- process.once("SIGTERM", () => void shutdown().then(() => process.exit(0)));
5502
- process.once("SIGINT", () => void shutdown().then(() => process.exit(0)));
5776
+ if (opts.signals === false)
5777
+ return;
5778
+ process.once("SIGTERM", () => void shutdownTelemetry().then(() => process.exit(0)));
5779
+ process.once("SIGINT", () => void shutdownTelemetry().then(() => process.exit(0)));
5780
+ }
5781
+ async function shutdownTelemetry() {
5782
+ const running = sdk;
5783
+ const logger = loggerProvider;
5784
+ sdk = null;
5785
+ loggerProvider = null;
5786
+ metricReader = null;
5787
+ try {
5788
+ await running?.shutdown();
5789
+ await logger?.shutdown();
5790
+ } catch {
5791
+ }
5503
5792
  }
5504
5793
  async function flushTelemetry() {
5505
5794
  const flushes = [];
@@ -5815,61 +6104,614 @@ var init_metrics = __esm({
5815
6104
  ttsChars: "vl.tts.chars",
5816
6105
  sttSeconds: "vl.stt.seconds"
5817
6106
  };
5818
- _llmInputTokens = null;
5819
- _llmOutputTokens = null;
5820
- _ttsChars = null;
5821
- _sttSeconds = null;
5822
- _firstTokenMs = null;
5823
- _ttsStartMs = null;
5824
- _eouDelayMs = null;
5825
- _eouTranscriptionDelayMs = null;
5826
- _eouOnTurnCompletedDelayMs = null;
5827
- _interruptionTotalMs = null;
5828
- _interruptionPredictionMs = null;
5829
- _interruptionDetectionDelayMs = null;
5830
- _interruptionCount = null;
5831
- _backchannelCount = null;
5832
- _realtimeSessionDurationMs = null;
5833
- _modelFallbacks = null;
6107
+ _llmInputTokens = null;
6108
+ _llmOutputTokens = null;
6109
+ _ttsChars = null;
6110
+ _sttSeconds = null;
6111
+ _firstTokenMs = null;
6112
+ _ttsStartMs = null;
6113
+ _eouDelayMs = null;
6114
+ _eouTranscriptionDelayMs = null;
6115
+ _eouOnTurnCompletedDelayMs = null;
6116
+ _interruptionTotalMs = null;
6117
+ _interruptionPredictionMs = null;
6118
+ _interruptionDetectionDelayMs = null;
6119
+ _interruptionCount = null;
6120
+ _backchannelCount = null;
6121
+ _realtimeSessionDurationMs = null;
6122
+ _modelFallbacks = null;
6123
+ }
6124
+ });
6125
+ function recordTurnLatencySpan(args) {
6126
+ if (!Number.isFinite(args.latencyMs) || args.latencyMs < 0)
6127
+ return;
6128
+ if (args.latencyMs === 0 && !args.allowZero)
6129
+ return;
6130
+ const attributes = {
6131
+ [ATTR.projectId]: args.projectId,
6132
+ [ATTR.callId]: args.callId,
6133
+ [LATENCY_STAGE_ATTR]: args.stage,
6134
+ [LATENCY_MS_ATTR]: args.latencyMs,
6135
+ [LATENCY_REALTIME_ATTR]: args.realtime ? 1 : 0
6136
+ };
6137
+ if (args.campaignId)
6138
+ attributes[ATTR.campaignId] = args.campaignId;
6139
+ trace.getTracer(TRACER_NAME2).startSpan(LATENCY_SPAN_NAME, { attributes }).end();
6140
+ }
6141
+ var TRACER_NAME2, LATENCY_SPAN_NAME, LATENCY_STAGE_ATTR, LATENCY_MS_ATTR, LATENCY_REALTIME_ATTR;
6142
+ var init_latency_span = __esm({
6143
+ "../observability/dist/latency-span.js"() {
6144
+ init_attributes();
6145
+ TRACER_NAME2 = "@voicelayer/observability";
6146
+ LATENCY_SPAN_NAME = "vl.turn_latency";
6147
+ LATENCY_STAGE_ATTR = "vl.stage";
6148
+ LATENCY_MS_ATTR = "vl.latency_ms";
6149
+ LATENCY_REALTIME_ATTR = "vl.realtime";
6150
+ }
6151
+ });
6152
+
6153
+ // ../observability/dist/index.js
6154
+ var init_dist2 = __esm({
6155
+ "../observability/dist/index.js"() {
6156
+ init_start();
6157
+ init_call_context();
6158
+ init_trace_propagation();
6159
+ init_attributes();
6160
+ init_metrics();
6161
+ init_latency_span();
6162
+ }
6163
+ });
6164
+
6165
+ // src/providers/resilient-llm.ts
6166
+ var resilient_llm_exports = {};
6167
+ __export(resilient_llm_exports, {
6168
+ LLM_APOLOGY: () => LLM_APOLOGY,
6169
+ ResilientLLM: () => ResilientLLM,
6170
+ isResilientLLM: () => isResilientLLM
6171
+ });
6172
+ function isResilientLLM(value) {
6173
+ return typeof value === "object" && value !== null && value[RESILIENT] === true;
6174
+ }
6175
+ function transient(error) {
6176
+ if (error instanceof APIStatusError) {
6177
+ const s = error.statusCode;
6178
+ return s === 408 || s === 429 || s < 0 || s >= 500;
6179
+ }
6180
+ return error instanceof APITimeoutError || error instanceof APIConnectionError;
6181
+ }
6182
+ var LLM_APOLOGY, RESILIENT, ResilientLLM, sameModel, ServedLLM, ResilientLLMStream;
6183
+ var init_resilient_llm = __esm({
6184
+ "src/providers/resilient-llm.ts"() {
6185
+ init_dist();
6186
+ init_dist2();
6187
+ LLM_APOLOGY = "Sorry, I'm having trouble right now. Could you give me a moment and say that again?";
6188
+ RESILIENT = /* @__PURE__ */ Symbol.for("voicelayer.resilientLLM");
6189
+ ResilientLLM = class extends llm.LLM {
6190
+ [RESILIENT] = true;
6191
+ #opts;
6192
+ #fallback = null;
6193
+ #listeners = /* @__PURE__ */ new Set();
6194
+ /** Models the provider rejected on this call (a call's pipeline is built per call): later turns go straight to the fallback. */
6195
+ rejections = new ModelRejectionCache();
6196
+ constructor(opts) {
6197
+ super();
6198
+ this.#opts = opts;
6199
+ }
6200
+ label() {
6201
+ return this.#opts.primary.label();
6202
+ }
6203
+ get model() {
6204
+ return this.#opts.primary.model;
6205
+ }
6206
+ get provider() {
6207
+ return this.#opts.primary.provider;
6208
+ }
6209
+ get apology() {
6210
+ return this.#opts.apology ?? LLM_APOLOGY;
6211
+ }
6212
+ get reasoningParams() {
6213
+ return this.#opts.reasoningParams === true;
6214
+ }
6215
+ /** The fallback LLM, built on first need; null when there is none or it is the agent's own model. */
6216
+ fallbackLLM() {
6217
+ if (!this.#opts.fallback) return null;
6218
+ this.#fallback ??= this.#opts.fallback();
6219
+ return sameModel(this.#fallback.model, this.model) ? null : this.#fallback;
6220
+ }
6221
+ /** Hear about every recovery (and every apology). Returns the unsubscribe. */
6222
+ onIncident(listener) {
6223
+ this.#listeners.add(listener);
6224
+ return () => this.#listeners.delete(listener);
6225
+ }
6226
+ /** @internal */
6227
+ report(incident) {
6228
+ const log = incident.kind === "apology" ? console.error : console.warn;
6229
+ log(`[voicelayer] llm ${incident.kind.replace(/_/g, " ")}`, incident);
6230
+ if (incident.kind === "model_fallback") {
6231
+ const call = getCurrentCallContext();
6232
+ recordModelFallback({
6233
+ model: incident.model,
6234
+ fallbackModel: incident.fallbackModel,
6235
+ code: incident.code,
6236
+ surface: "voice",
6237
+ ...call?.projectId ? { projectId: call.projectId } : {},
6238
+ ...call?.agentId ? { agentId: call.agentId } : {}
6239
+ });
6240
+ }
6241
+ for (const listener of this.#listeners) {
6242
+ try {
6243
+ listener(incident);
6244
+ } catch {
6245
+ }
6246
+ }
6247
+ }
6248
+ chat(args) {
6249
+ return new ResilientLLMStream(this, args);
6250
+ }
6251
+ prewarm() {
6252
+ this.#opts.primary.prewarm();
6253
+ }
6254
+ async aclose() {
6255
+ await Promise.all([this.#opts.primary.aclose(), this.#fallback?.aclose()]);
6256
+ }
6257
+ /** @internal */
6258
+ get primary() {
6259
+ return this.#opts.primary;
6260
+ }
6261
+ };
6262
+ sameModel = (a, b) => a.trim().toLowerCase() === b.trim().toLowerCase();
6263
+ ServedLLM = class extends llm.LLM {
6264
+ constructor(owner, served) {
6265
+ super();
6266
+ this.owner = owner;
6267
+ this.served = served;
6268
+ }
6269
+ owner;
6270
+ served;
6271
+ label() {
6272
+ return this.owner.label();
6273
+ }
6274
+ get model() {
6275
+ return this.served();
6276
+ }
6277
+ get provider() {
6278
+ return this.owner.provider;
6279
+ }
6280
+ chat(args) {
6281
+ return this.owner.chat(args);
6282
+ }
6283
+ emit(event, ...args) {
6284
+ return this.owner.emit(event, ...args);
6285
+ }
6286
+ };
6287
+ ResilientLLMStream = class extends llm.LLMStream {
6288
+ #owner;
6289
+ #args;
6290
+ #conn;
6291
+ /** The model answering this stream — what its metrics report. */
6292
+ #served;
6293
+ #current = null;
6294
+ constructor(owner, args) {
6295
+ const conn = args.connOptions ?? DEFAULT_API_CONNECT_OPTIONS;
6296
+ const served = { model: owner.model };
6297
+ super(new ServedLLM(owner, () => served.model), {
6298
+ chatCtx: args.chatCtx,
6299
+ ...args.toolCtx ? { toolCtx: args.toolCtx } : {},
6300
+ connOptions: { ...conn, maxRetry: 0 }
6301
+ });
6302
+ this.#owner = owner;
6303
+ this.#args = args;
6304
+ this.#conn = conn;
6305
+ this.#served = served;
6306
+ this.abortController.signal.addEventListener("abort", () => this.#current?.close());
6307
+ }
6308
+ get #hasTools() {
6309
+ return this.#args.toolCtx !== void 0 && Object.keys(this.#args.toolCtx).length > 0;
6310
+ }
6311
+ #extraKwargs(model2) {
6312
+ const base = this.#args.extraKwargs;
6313
+ if (!this.#owner.reasoningParams) return base;
6314
+ const effort = chatReasoningEffort(model2, { tools: this.#hasTools });
6315
+ if (effort === void 0) {
6316
+ if (!base || !("reasoning_effort" in base)) return base;
6317
+ const { reasoning_effort: _dropped, ...rest } = base;
6318
+ return rest;
6319
+ }
6320
+ return { ...base, reasoning_effort: effort };
6321
+ }
6322
+ /** One request on `target`, forwarding its chunks. Its failure is the inner LLM's 'error' event. */
6323
+ async #attempt(target) {
6324
+ let failure2 = null;
6325
+ const onError = (ev) => {
6326
+ failure2 ??= ev.error;
6327
+ };
6328
+ target.on("error", onError);
6329
+ let started2 = false;
6330
+ try {
6331
+ const extraKwargs = this.#extraKwargs(target.model);
6332
+ const stream = target.chat({
6333
+ ...this.#args,
6334
+ connOptions: { ...this.#conn, maxRetry: 0 },
6335
+ ...extraKwargs !== void 0 ? { extraKwargs } : {}
6336
+ });
6337
+ this.#current = stream;
6338
+ for await (const chunk of stream) {
6339
+ if (this.abortController.signal.aborted) break;
6340
+ started2 = true;
6341
+ this.queue.put(chunk);
6342
+ }
6343
+ } catch (err) {
6344
+ failure2 ??= err instanceof Error ? err : new Error(String(err));
6345
+ } finally {
6346
+ target.off("error", onError);
6347
+ this.#current = null;
6348
+ }
6349
+ return failure2 ? { ok: false, error: failure2, started: started2 } : { ok: true };
6350
+ }
6351
+ /** Run `target` until it answers, or a failure that retrying it won't fix. */
6352
+ async #run(target) {
6353
+ let effortRetried = false;
6354
+ for (let retries = 0; ; ) {
6355
+ const sent = this.#owner.reasoningParams ? chatReasoningEffort(target.model, { tools: this.#hasTools }) : void 0;
6356
+ const result = await this.#attempt(target);
6357
+ if (result.ok || result.started || this.abortController.signal.aborted) return result;
6358
+ if (this.#owner.reasoningParams && !effortRetried && learnReasoningEffort(target.model, { tools: this.#hasTools, sent }, result.error)) {
6359
+ effortRetried = true;
6360
+ this.#owner.report({
6361
+ kind: "reasoning_effort_adapted",
6362
+ model: target.model,
6363
+ message: providerErrorOf(result.error)?.message ?? result.error.message
6364
+ });
6365
+ continue;
6366
+ }
6367
+ if (!transient(result.error) || retries >= this.#conn.maxRetry) return result;
6368
+ const wait = intervalForRetry(this.#conn, retries);
6369
+ retries += 1;
6370
+ if (wait > 0) await new Promise((r) => setTimeout(r, wait));
6371
+ if (this.abortController.signal.aborted) return result;
6372
+ }
6373
+ }
6374
+ async run() {
6375
+ const owner = this.#owner;
6376
+ const model2 = owner.model;
6377
+ const fallback = owner.fallbackLLM();
6378
+ const known = fallback ? owner.rejections.get(model2) : null;
6379
+ let failure2;
6380
+ if (known && fallback) {
6381
+ owner.report({ kind: "model_fallback", model: model2, fallbackModel: fallback.model, code: known.code, message: known.message, cached: true });
6382
+ failure2 = new Error(known.message);
6383
+ } else {
6384
+ const first = await this.#run(owner.primary);
6385
+ if (first.ok || first.started || this.abortController.signal.aborted) return;
6386
+ failure2 = first.error;
6387
+ const rejection = modelRejectionOf(first.error);
6388
+ if (rejection) owner.rejections.set(model2, rejection);
6389
+ if (fallback) {
6390
+ const f = providerErrorOf(first.error);
6391
+ owner.report({
6392
+ kind: "model_fallback",
6393
+ model: model2,
6394
+ fallbackModel: fallback.model,
6395
+ code: rejection?.code ?? (f?.status ? String(f.status) : "error"),
6396
+ message: f?.message || first.error.message,
6397
+ cached: false
6398
+ });
6399
+ }
6400
+ }
6401
+ if (fallback) {
6402
+ this.#served.model = fallback.model;
6403
+ const second = await this.#run(fallback);
6404
+ if (second.ok || second.started || this.abortController.signal.aborted) return;
6405
+ failure2 = second.error;
6406
+ }
6407
+ owner.report({ kind: "apology", model: this.#served.model, message: providerErrorOf(failure2)?.message || failure2.message });
6408
+ this.queue.put({ id: `vl-apology-${Date.now()}`, delta: { role: "assistant", content: owner.apology } });
6409
+ }
6410
+ };
6411
+ }
6412
+ });
6413
+
6414
+ // src/providers/index.ts
6415
+ async function importOptional(spec, hint) {
6416
+ try {
6417
+ return await import(spec);
6418
+ } catch {
6419
+ throw new Error(`${hint}: "${spec}" is not installed. Add it with: pnpm add ${spec}`);
6420
+ }
6421
+ }
6422
+ function withCreds(opts, creds2) {
6423
+ if (creds2?.apiKey) opts["apiKey"] = creds2.apiKey;
6424
+ if (creds2?.baseURL) opts["baseURL"] = creds2.baseURL;
6425
+ return opts;
6426
+ }
6427
+ var deepgram, openai, anthropic, cartesia, elevenlabs, assemblyai, google, silero, livekitTurn, connector;
6428
+ var init_providers2 = __esm({
6429
+ "src/providers/index.ts"() {
6430
+ init_ssrf();
6431
+ init_transport_callback();
6432
+ init_connector_llm();
6433
+ init_wrap();
6434
+ deepgram = {
6435
+ stt(options = {}) {
6436
+ return async () => {
6437
+ const dg = await importOptional("@voicelayer/agents-plugin-deepgram", "deepgram.stt");
6438
+ return new dg.STT(
6439
+ withCreds(
6440
+ { model: options.model ?? "nova-3", language: options.language ?? "en-US" },
6441
+ options
6442
+ )
6443
+ );
6444
+ };
6445
+ },
6446
+ tts(options = {}) {
6447
+ return async () => {
6448
+ const dg = await importOptional("@voicelayer/agents-plugin-deepgram", "deepgram.tts");
6449
+ return new dg.TTS(
6450
+ withCreds({ model: options.model ?? "aura-2-harmonia-en" }, options)
6451
+ );
6452
+ };
6453
+ }
6454
+ };
6455
+ openai = {
6456
+ llm(options = {}) {
6457
+ return async () => {
6458
+ const oa = await importOptional("@voicelayer/agents-plugin-openai", "openai.llm");
6459
+ const { ResilientLLM: ResilientLLM2 } = await Promise.resolve().then(() => (init_resilient_llm(), resilient_llm_exports));
6460
+ const { PLATFORM_DEFAULT_CHAT_MODEL: PLATFORM_DEFAULT_CHAT_MODEL2 } = await Promise.resolve().then(() => (init_dist(), dist_exports));
6461
+ return new ResilientLLM2({
6462
+ primary: new oa.LLM(withCreds({ model: options.model ?? "gpt-4o-mini" }, options)),
6463
+ fallback: () => new oa.LLM(withCreds({ model: PLATFORM_DEFAULT_CHAT_MODEL2 }, options)),
6464
+ reasoningParams: true
6465
+ });
6466
+ };
6467
+ },
6468
+ tts(options = {}) {
6469
+ return async () => {
6470
+ const oa = await importOptional("@voicelayer/agents-plugin-openai", "openai.tts");
6471
+ const opts = { model: options.model ?? "tts-1" };
6472
+ if (options.voice) opts["voice"] = options.voice;
6473
+ if (options.instructions) opts["instructions"] = options.instructions;
6474
+ return new oa.TTS(withCreds(opts, options));
6475
+ };
6476
+ },
6477
+ // Speech-to-speech via the OpenAI Realtime API. Returns a RealtimeProvider
6478
+ // that the SDK uses in place of the stt/llm/tts pipeline.
6479
+ realtime(options = {}) {
6480
+ return async () => {
6481
+ const oa = await importOptional("@voicelayer/agents-plugin-openai", "openai.realtime");
6482
+ const opts = {};
6483
+ if (options.model) opts["model"] = options.model;
6484
+ if (options.voice) opts["voice"] = options.voice;
6485
+ return new oa.realtime.RealtimeModel(withCreds(opts, options));
6486
+ };
6487
+ }
6488
+ };
6489
+ anthropic = {
6490
+ llm(options = {}) {
6491
+ return async () => {
6492
+ const an = await importOptional("@livekit/agents-plugin-anthropic", "anthropic.llm");
6493
+ return new an.LLM(
6494
+ withCreds({ model: options.model ?? "claude-sonnet-5-5" }, options)
6495
+ );
6496
+ };
6497
+ }
6498
+ };
6499
+ cartesia = {
6500
+ tts(options = {}) {
6501
+ return async () => {
6502
+ const ct = await importOptional("@voicelayer/agents-plugin-cartesia", "cartesia.tts");
6503
+ return new ct.TTS(options);
6504
+ };
6505
+ },
6506
+ stt(options = {}) {
6507
+ return async () => {
6508
+ const ct = await importOptional("@voicelayer/agents-plugin-cartesia", "cartesia.stt");
6509
+ const opts = {};
6510
+ if (options.model) opts["model"] = options.model;
6511
+ if (options.language) opts["language"] = options.language;
6512
+ return new ct.STT(withCreds(opts, options));
6513
+ };
6514
+ }
6515
+ };
6516
+ elevenlabs = {
6517
+ tts(options = {}) {
6518
+ return async () => {
6519
+ const el = await importOptional("@voicelayer/agents-plugin-elevenlabs", "elevenlabs.tts");
6520
+ const opts = {};
6521
+ if (options.voice) opts["voiceId"] = options.voice;
6522
+ if (options.model) opts["model"] = options.model;
6523
+ return new el.TTS(withCreds(opts, options));
6524
+ };
6525
+ }
6526
+ };
6527
+ assemblyai = {
6528
+ stt(options = {}) {
6529
+ return async () => {
6530
+ const aai = await importOptional("@voicelayer/agents-plugin-assemblyai", "assemblyai.stt");
6531
+ const opts = {};
6532
+ if (options.language) opts["language"] = options.language;
6533
+ return new aai.STT(withCreds(opts, options));
6534
+ };
6535
+ }
6536
+ };
6537
+ google = {
6538
+ llm(options = {}) {
6539
+ return async () => {
6540
+ const g = await importOptional("@voicelayer/agents-plugin-google", "google.llm");
6541
+ const { ResilientLLM: ResilientLLM2 } = await Promise.resolve().then(() => (init_resilient_llm(), resilient_llm_exports));
6542
+ return new ResilientLLM2({
6543
+ primary: new g.LLM(withCreds({ model: options.model ?? "gemini-3.8-flash" }, options))
6544
+ });
6545
+ };
6546
+ },
6547
+ realtime(options = {}) {
6548
+ return async () => {
6549
+ const g = await importOptional("@voicelayer/agents-plugin-google", "google.realtime");
6550
+ const opts = {};
6551
+ if (options.model) opts["model"] = options.model;
6552
+ if (options.voice) opts["voice"] = options.voice;
6553
+ return new g.beta.realtime.RealtimeModel(withCreds(opts, options));
6554
+ };
6555
+ }
6556
+ };
6557
+ silero = {
6558
+ vad() {
6559
+ return async () => {
6560
+ const sil = await importOptional("@voicelayer/agents-plugin-silero", "silero.vad");
6561
+ return await sil.VAD.load();
6562
+ };
6563
+ }
6564
+ };
6565
+ livekitTurn = {
6566
+ english() {
6567
+ return async () => {
6568
+ const lk = await importOptional("@voicelayer/agents-plugin-livekit", "livekitTurn.english");
6569
+ return new lk.turnDetector.EnglishModel();
6570
+ };
6571
+ },
6572
+ multilingual() {
6573
+ return async () => {
6574
+ const lk = await importOptional("@voicelayer/agents-plugin-livekit", "livekitTurn.multilingual");
6575
+ return new lk.turnDetector.MultilingualModel();
6576
+ };
6577
+ }
6578
+ };
6579
+ connector = {
6580
+ llm(config = {}) {
6581
+ return async (call) => {
6582
+ if (config.url) {
6583
+ await assertPublicHttpsUrl(
6584
+ config.url,
6585
+ config.allowHosts ? { allowHosts: config.allowHosts } : {}
6586
+ );
6587
+ return openai.llm({
6588
+ ...config.model ? { model: config.model } : {},
6589
+ ...config.apiKey ? { apiKey: config.apiKey } : {},
6590
+ baseURL: config.url
6591
+ })(call);
6592
+ }
6593
+ const transport = config.transport ?? (config.onQuery ? callbackTransport(config.onQuery) : void 0);
6594
+ if (!transport) {
6595
+ throw new Error("connector.llm requires one of: url, onQuery, or transport");
6596
+ }
6597
+ const projectId = typeof call.metadata["projectId"] === "string" ? call.metadata["projectId"] : void 0;
6598
+ const llmOptions = {
6599
+ ...config.model ? { model: config.model } : {},
6600
+ ...config.temperature !== void 0 ? { temperature: config.temperature } : {},
6601
+ ...config.fallbackText ? { fallbackText: config.fallbackText } : {},
6602
+ ...call.callId ? { callId: call.callId } : {},
6603
+ ...projectId ? { projectId } : {}
6604
+ };
6605
+ return await createConnectorLLM(transport, llmOptions);
6606
+ };
6607
+ }
6608
+ };
5834
6609
  }
5835
6610
  });
5836
- function recordTurnLatencySpan(args) {
5837
- if (!Number.isFinite(args.latencyMs) || args.latencyMs < 0)
5838
- return;
5839
- if (args.latencyMs === 0 && !args.allowZero)
5840
- return;
5841
- const attributes = {
5842
- [ATTR.projectId]: args.projectId,
5843
- [ATTR.callId]: args.callId,
5844
- [LATENCY_STAGE_ATTR]: args.stage,
5845
- [LATENCY_MS_ATTR]: args.latencyMs,
5846
- [LATENCY_REALTIME_ATTR]: args.realtime ? 1 : 0
5847
- };
5848
- if (args.campaignId)
5849
- attributes[ATTR.campaignId] = args.campaignId;
5850
- trace.getTracer(TRACER_NAME2).startSpan(LATENCY_SPAN_NAME, { attributes }).end();
6611
+
6612
+ // src/providers/registry.ts
6613
+ function entry(id, bits) {
6614
+ const identity = providerIdentity(id);
6615
+ if (!identity) throw new Error(`PROVIDER_REGISTRY: no provider identity in contracts for '${id}'`);
6616
+ return { name: identity.id, capabilities: identity.capabilities, ...bits };
5851
6617
  }
5852
- var TRACER_NAME2, LATENCY_SPAN_NAME, LATENCY_STAGE_ATTR, LATENCY_MS_ATTR, LATENCY_REALTIME_ATTR;
5853
- var init_latency_span = __esm({
5854
- "../observability/dist/latency-span.js"() {
5855
- init_attributes();
5856
- TRACER_NAME2 = "@voicelayer/observability";
5857
- LATENCY_SPAN_NAME = "vl.turn_latency";
5858
- LATENCY_STAGE_ATTR = "vl.stage";
5859
- LATENCY_MS_ATTR = "vl.latency_ms";
5860
- LATENCY_REALTIME_ATTR = "vl.realtime";
6618
+ function providerEntry(name) {
6619
+ if (!name) return void 0;
6620
+ return PROVIDER_REGISTRY[PROVIDER_ALIASES[name] ?? name];
6621
+ }
6622
+ function resolveStt(name, o) {
6623
+ return (providerEntry(name)?.stt ?? PROVIDER_REGISTRY["deepgram"].stt)(o);
6624
+ }
6625
+ function resolveTts(name, o) {
6626
+ return (providerEntry(name)?.tts ?? PROVIDER_REGISTRY["deepgram"].tts)(o);
6627
+ }
6628
+ function resolveLlm(name, o) {
6629
+ return (providerEntry(name)?.llm ?? PROVIDER_REGISTRY["openai"].llm)(withCurrentModel(name, o));
6630
+ }
6631
+ function resolveRealtime(name, o) {
6632
+ return (providerEntry(name)?.realtime ?? PROVIDER_REGISTRY["openai"].realtime)(withCurrentModel(name, o));
6633
+ }
6634
+ function withCurrentModel(name, o) {
6635
+ if (!name || !o.model) return o;
6636
+ const current = currentModelFor(name, o.model);
6637
+ if (!current.retired) return o;
6638
+ console.warn("[agent] the configured model is retired; running its replacement", {
6639
+ provider: name,
6640
+ model: current.retired,
6641
+ replacement: current.model
6642
+ });
6643
+ return { ...o, model: current.model };
6644
+ }
6645
+ var model, lang, voice, creds, PROVIDER_REGISTRY;
6646
+ var init_registry = __esm({
6647
+ "src/providers/registry.ts"() {
6648
+ init_providers2();
6649
+ init_src();
6650
+ model = (o) => o.model ? { model: o.model } : {};
6651
+ lang = (o) => o.language ? { language: o.language } : {};
6652
+ voice = (o) => o.voice ? { voice: o.voice } : {};
6653
+ creds = (o) => o.creds ?? {};
6654
+ PROVIDER_REGISTRY = {
6655
+ deepgram: entry("deepgram", {
6656
+ failureClass: "vendor-api",
6657
+ engines: ["livekit"],
6658
+ stt: (o) => deepgram.stt({ ...model(o), ...lang(o), ...creds(o) }),
6659
+ // Deepgram folds the voice into the model id (aura-2-<name>-en): wire config
6660
+ // passes a voice, code config passes a model — both land as `model`.
6661
+ tts: (o) => {
6662
+ const m = o.voice ?? o.model;
6663
+ return deepgram.tts({ ...m ? { model: m } : {}, ...creds(o) });
6664
+ }
6665
+ }),
6666
+ openai: entry("openai", {
6667
+ failureClass: "vendor-api",
6668
+ engines: ["livekit", "text"],
6669
+ llm: (o) => openai.llm({ ...model(o), ...creds(o) }),
6670
+ tts: (o) => openai.tts({ ...voice(o), ...model(o), ...creds(o) }),
6671
+ realtime: (o) => openai.realtime({ ...model(o), ...voice(o), ...creds(o) })
6672
+ }),
6673
+ anthropic: entry("anthropic", {
6674
+ failureClass: "vendor-api",
6675
+ engines: ["livekit", "text"],
6676
+ llm: (o) => anthropic.llm({ ...model(o), ...creds(o) })
6677
+ }),
6678
+ google: entry("google", {
6679
+ failureClass: "vendor-api",
6680
+ engines: ["livekit", "text"],
6681
+ llm: (o) => google.llm({ ...model(o), ...creds(o) }),
6682
+ realtime: (o) => google.realtime({ ...model(o), ...voice(o), ...creds(o) })
6683
+ }),
6684
+ cartesia: entry("cartesia", {
6685
+ failureClass: "vendor-api",
6686
+ engines: ["livekit"],
6687
+ stt: (o) => cartesia.stt({ ...model(o), ...lang(o), ...creds(o) }),
6688
+ tts: (o) => cartesia.tts({ ...voice(o), ...model(o), ...creds(o) })
6689
+ }),
6690
+ elevenlabs: entry("elevenlabs", {
6691
+ failureClass: "vendor-api",
6692
+ engines: ["livekit"],
6693
+ tts: (o) => elevenlabs.tts({ ...voice(o), ...model(o), ...creds(o) })
6694
+ }),
6695
+ assemblyai: entry("assemblyai", {
6696
+ failureClass: "vendor-api",
6697
+ engines: ["livekit"],
6698
+ stt: (o) => assemblyai.stt({ ...lang(o), ...creds(o) })
6699
+ // no model knob
6700
+ }),
6701
+ silero: entry("silero", {
6702
+ failureClass: "local",
6703
+ engines: ["livekit"],
6704
+ vad: () => silero.vad()
6705
+ })
6706
+ };
5861
6707
  }
5862
6708
  });
5863
6709
 
5864
- // ../observability/dist/index.js
5865
- var init_dist2 = __esm({
5866
- "../observability/dist/index.js"() {
5867
- init_start();
5868
- init_call_context();
5869
- init_trace_propagation();
5870
- init_attributes();
5871
- init_metrics();
5872
- init_latency_span();
6710
+ // src/providers/llm.ts
6711
+ var init_llm = __esm({
6712
+ "src/providers/llm.ts"() {
6713
+ init_openai_default();
6714
+ init_registry();
5873
6715
  }
5874
6716
  });
5875
6717
 
@@ -6126,7 +6968,9 @@ function evaluateDeterministic(atom, ctx) {
6126
6968
  if (result === void 0) return false;
6127
6969
  if (!path || path === "ok") {
6128
6970
  const ok = result.ok === true || result.kind === "ok";
6129
- return op === "!=" ? !ok : ok;
6971
+ const want = (rawVal ?? "").trim().toLowerCase();
6972
+ const eq2 = want === "" ? ok : String(ok) === want;
6973
+ return op === "!=" ? !eq2 : eq2;
6130
6974
  }
6131
6975
  const actual = resolvePath(result, path);
6132
6976
  if (!op) return actual !== void 0 && actual !== null && actual !== "";
@@ -6321,28 +7165,12 @@ var init_conditions = __esm({
6321
7165
  });
6322
7166
 
6323
7167
  // src/runtime/graph/turn-text.ts
6324
- function lastSentences(line) {
6325
- const text = line.trim();
6326
- if (!text) return [];
6327
- const sentences = [];
6328
- let last = 0;
6329
- for (const m of text.matchAll(SENTENCE_END)) {
6330
- const end = (m.index ?? 0) + m[0].length;
6331
- sentences.push(text.slice(last, end));
6332
- last = end;
6333
- }
6334
- if (last < text.length) sentences.push(text.slice(last));
6335
- return sentences.slice(-2);
6336
- }
6337
- function isQuestion(line) {
6338
- return lastSentences(line).some((s) => QUESTION_MARK.test(s));
6339
- }
6340
7168
  function wordsOf(field) {
6341
7169
  return field.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
6342
7170
  }
6343
7171
  function asksFor(line, fields) {
6344
7172
  if (!line) return false;
6345
- const questions = lastSentences(line).filter((s) => QUESTION_MARK.test(s)).map((s) => s.toLowerCase().replace(/^[\s"'“(]*(?:and|so|ok|okay|great|thanks|sure|alright|now)[,\s]+/, "").trim());
7173
+ const questions = lastSentences(line).filter((s) => QUESTION_MARK2.test(s)).map((s) => s.toLowerCase().replace(/^[\s"'“(]*(?:and|so|ok|okay|great|thanks|sure|alright|now)[,\s]+/, "").trim());
6346
7174
  return questions.some((q) => {
6347
7175
  if (YES_NO.test(q) && !POLITE_REQUEST.test(q)) return false;
6348
7176
  const words = new Set(q.match(/[a-z0-9]+/g) ?? []);
@@ -6352,11 +7180,11 @@ function asksFor(line, fields) {
6352
7180
  });
6353
7181
  });
6354
7182
  }
6355
- var QUESTION_MARK, SENTENCE_END, POLITE_REQUEST, YES_NO;
7183
+ var QUESTION_MARK2, POLITE_REQUEST, YES_NO;
6356
7184
  var init_turn_text = __esm({
6357
7185
  "src/runtime/graph/turn-text.ts"() {
6358
- QUESTION_MARK = /[??؟]/;
6359
- SENTENCE_END = /[.!?…。!?؟]+["'”’)\]]*\s*/g;
7186
+ init_src();
7187
+ QUESTION_MARK2 = /[??؟]/;
6360
7188
  POLITE_REQUEST = /^(?:(?:could|can|would|will) you (?:please )?(?:tell|give|share|say|spell|repeat|read|provide|confirm|let me know)|(?:could|can|may) (?:i|we) (?:please )?(?:get|have|take|grab))\b/;
6361
7189
  YES_NO = /^(?:would|do|does|did|is|are|was|were|can|could|shall|should|will|have|has|had|may|might|am)\b/;
6362
7190
  }
@@ -6939,7 +7767,8 @@ async function nextTurn(deps, state, reprompt, opts) {
6939
7767
  ...opts?.deferCapture ? { deferCapture: true } : {},
6940
7768
  ...opts?.number ? { number: opts.number } : {},
6941
7769
  ...opts?.fields?.length ? { fields: opts.fields } : {},
6942
- ...opts?.answering && opts.answering.fields.length > 0 ? { answering: opts.answering } : {}
7770
+ ...opts?.answering && opts.answering.fields.length > 0 ? { answering: opts.answering } : {},
7771
+ ...opts?.captureAfterRead ? { captureAfterRead: true } : {}
6943
7772
  });
6944
7773
  if (evt.kind === "hangup") return { result: { kind: "end" } };
6945
7774
  if (evt.kind === "control") return { result: evt.result };
@@ -6966,7 +7795,7 @@ async function nextTurn(deps, state, reprompt, opts) {
6966
7795
  state.lastNotInContext = evt.notInContext;
6967
7796
  state.lastAgentText = evt.agentText;
6968
7797
  state.lastDtmf = void 0;
6969
- return { text: evt.text };
7798
+ return { text: evt.text, ...evt.capture ? { capture: evt.capture } : {} };
6970
7799
  }
6971
7800
  }
6972
7801
  function detectYesNo(text) {
@@ -6976,6 +7805,58 @@ function detectYesNo(text) {
6976
7805
  if (no && !yes) return "no";
6977
7806
  return null;
6978
7807
  }
7808
+ function renderLine(template, state, deps) {
7809
+ return { spoken: interpolate(template, scopeFor(state, deps)), model: interpolate(template, modelScopeFor(state, deps)) };
7810
+ }
7811
+ function askLinesFor(node, state, deps, askText, label, rephrase) {
7812
+ const render = (line) => renderLine(line, state, deps);
7813
+ const ctx = deps.askContext;
7814
+ const english = isEnglishLanguage(ctx?.language);
7815
+ const generated = readAskGeneratedLines(node.config.generated);
7816
+ const current = generated !== null && ctx !== void 0 && generated.basis === askLinesBasis({
7817
+ persona: ctx.persona,
7818
+ routing: ctx.routing ?? null,
7819
+ language: ctx.language ?? null,
7820
+ ask: str(node.config.ask),
7821
+ instructions: str(node.config.instructions)
7822
+ });
7823
+ if (generated && !current) deps.events.note("ask.reask_lines_stale", { node: node.id });
7824
+ const usable = current && rephrase !== "never" ? generated : null;
7825
+ const author = authorReasksOf(node.config.reasks).map(render);
7826
+ const pool = author.length ? author : usable && usable.reasks.length ? usable.reasks.map(render) : english ? templateReasks(askText.spoken).map((spoken, i) => ({ spoken, model: templateReasks(askText.model)[i] })) : rephrase === "never" ? [askText] : [];
7827
+ const offerTemplate = english ? skipOfferTemplate(label) : void 0;
7828
+ const skipOffer = usable?.skipOffer ? render(usable.skipOffer) : offerTemplate ? { spoken: offerTemplate, model: offerTemplate } : void 0;
7829
+ const voicedAsk = rephrase === "all" && usable?.voicedAsk ? render(usable.voicedAsk) : void 0;
7830
+ return { pool, ...skipOffer ? { skipOffer } : {}, ...voicedAsk ? { voicedAsk } : {} };
7831
+ }
7832
+ function skipOfferVerdict(text, dtmf) {
7833
+ if (dtmf === "1") return "yes";
7834
+ if (dtmf === "2") return "no";
7835
+ if (!text.trim()) return null;
7836
+ if (KEEP_ASKING.test(text)) return "no";
7837
+ if (SKIP_INTENT.test(text)) return OFFERS_IT.test(text) ? null : "yes";
7838
+ return detectYesNo(text);
7839
+ }
7840
+ async function readYesNo(t, state, deps) {
7841
+ const words = skipOfferVerdict(t.text, t.dtmf);
7842
+ if (words) return words;
7843
+ if (!t.text.trim()) return null;
7844
+ return await resolveBranch(
7845
+ [
7846
+ { id: "yes", label: "The caller agrees to skip it for now." },
7847
+ { id: "no", label: "The caller does not want to skip it \u2014 they will give it." }
7848
+ ],
7849
+ evalCtxOf(state, deps)
7850
+ );
7851
+ }
7852
+ function briefingScope(template, state, deps) {
7853
+ const scope2 = scopeFor(state, deps);
7854
+ const data = deps.processRt.getData();
7855
+ const notProvided = templateFieldRefs(template).filter((f) => isRequiredField(deps, f) && !hasSlot(data, f));
7856
+ if (notProvided.length === 0) return scope2;
7857
+ const filled = Object.fromEntries(notProvided.map((f) => [f, "not provided"]));
7858
+ return { ...scope2, ...filled, slots: { ...scope2.slots, ...filled } };
7859
+ }
6979
7860
  function formatCollected(deps) {
6980
7861
  return Object.entries(deps.processRt.getData()).filter(([, v]) => v !== void 0 && v !== null && v !== "").map(([k, v]) => `${k.trim()}: ${String(forModel(deps, k, v))}`).join("\n");
6981
7862
  }
@@ -7029,6 +7910,27 @@ function positiveMs(raw, fallback) {
7029
7910
  const n = Number(raw);
7030
7911
  return Number.isFinite(n) && n > 0 ? n : fallback;
7031
7912
  }
7913
+ function isRequiredField(deps, name) {
7914
+ return deps.model.requiredFields.has(name.trim());
7915
+ }
7916
+ function requiredInputsOf(node, state, deps) {
7917
+ const baked = node.config.requiredInputs;
7918
+ if (Array.isArray(baked)) return baked.filter((x) => typeof x === "string");
7919
+ const cached2 = state.derivedInputs?.get(node.id);
7920
+ if (cached2) return cached2;
7921
+ const derived = requiredInputsFor(node.type, node.config, (f) => isRequiredField(deps, f));
7922
+ (state.derivedInputs ??= /* @__PURE__ */ new Map()).set(node.id, derived);
7923
+ return derived;
7924
+ }
7925
+ function blockOnMissingInputs(node, state, deps, inputs, tool) {
7926
+ const data = deps.processRt.getData();
7927
+ const missing = inputs.filter((f) => !hasSlot(data, f)).map((f) => f.trim());
7928
+ if (missing.length === 0) return null;
7929
+ deps.events.note("step.missing_inputs", { node: node.id, missing });
7930
+ state.nodeResults[node.id] = { ok: false, error: "missing_required", missing };
7931
+ if (tool) state.toolResults[node.id] = { ok: false, status: 0, data: { kind: "blocked", reason: "missing_required", missing } };
7932
+ return deps.model.selectByPort(node.id, MISSING_PORT_ID) !== null ? { kind: "take", port: MISSING_PORT_ID } : routeByPort(node, deps, "fail");
7933
+ }
7032
7934
  function routeByPort(node, deps, port) {
7033
7935
  return deps.model.selectByPort(node.id, port) !== null ? { kind: "take", port } : { kind: "advance" };
7034
7936
  }
@@ -7040,11 +7942,15 @@ function menuDigitOf(b) {
7040
7942
  function menuLabelOf(label) {
7041
7943
  return label.replace(/^\s*[0-9*#]\s*[-—–:.]*\s*/, "").trim() || label.trim();
7042
7944
  }
7043
- var ASK_MAX_CLARIFY, COLLECT_MAX_STALLED_TURNS, LLM_MAX_TURNS, HUB_MAX_TURNS, NO_INPUT_MS, sayExecutor, YES_WORDS, NO_WORDS, confirmExecutor, askExecutor, gateExecutor, endExecutor, handoffExecutor, OPENING_SILENCE_MS, llmExecutor, toolExecutor, ragExecutor, codeExecutor, decisionExecutor, MENU_MAX_INPUTS, menuExecutor, classifyExecutor, routerExecutor, playbookExecutor, setExecutor, MAX_SUBFLOW_DEPTH, subflowExecutor, EmailStepTimeout, EMAIL_SEND_TIMEOUT_MS, emailExecutor, passthroughExecutor, EXECUTORS;
7945
+ async function speakLine(text, deps) {
7946
+ await speak(text, deps);
7947
+ }
7948
+ var ASK_MAX_CLARIFY, COLLECT_MAX_STALLED_TURNS, LLM_MAX_TURNS, HUB_MAX_TURNS, NO_INPUT_MS, sayExecutor, YES_WORDS, NO_WORDS, confirmExecutor, legacyAskExecutor, ASK_POLICIES, askExecutor, KEEP_ASKING, SKIP_INTENT, OFFERS_IT, gateExecutor, endExecutor, handoffExecutor, OPENING_SILENCE_MS, llmExecutor, toolExecutor, ragExecutor, codeExecutor, decisionExecutor, MENU_MAX_INPUTS, menuExecutor, classifyExecutor, routerExecutor, playbookExecutor, setExecutor, MAX_SUBFLOW_DEPTH, subflowExecutor, EmailStepTimeout, EMAIL_SEND_TIMEOUT_MS, emailExecutor, passthroughExecutor, EXECUTORS;
7044
7949
  var init_executors = __esm({
7045
7950
  "src/runtime/graph/executors.ts"() {
7046
7951
  init_log();
7047
7952
  init_src();
7953
+ init_reask_templates();
7048
7954
  init_interpolate();
7049
7955
  init_builtins();
7050
7956
  init_conditions();
@@ -7066,9 +7972,11 @@ var init_executors = __esm({
7066
7972
  NO_WORDS = /\b(no|nope|nah|wrong|incorrect|negative|not right|that'?s not|change|actually)\b/i;
7067
7973
  confirmExecutor = async (node, state, deps) => {
7068
7974
  const text = interpolate(str(node.config.text), scopeFor(state, deps));
7069
- await speak(text || "Is that correct?", deps);
7975
+ const line = text || "Is that correct?";
7976
+ await speak(line, deps);
7977
+ const reprompt = isEnglishLanguage(deps.askContext?.language) ? notCaught(line) : line;
7070
7978
  for (let attempt = 0; attempt < 2; attempt++) {
7071
- const t = await nextTurn(deps, state, text || "Is that correct?");
7979
+ const t = await nextTurn(deps, state, reprompt);
7072
7980
  if ("result" in t) return t.result;
7073
7981
  let verdict = null;
7074
7982
  if (t.dtmf === "1") verdict = "yes";
@@ -7096,7 +8004,7 @@ var init_executors = __esm({
7096
8004
  deps.events.note("confirm.unresolved", { node: node.id });
7097
8005
  return { kind: "advance" };
7098
8006
  };
7099
- askExecutor = async (node, state, deps) => {
8007
+ legacyAskExecutor = async (node, state, deps) => {
7100
8008
  const slot = str(node.config.slot) || node.id;
7101
8009
  const authoredSlot = slot === node.id ? "" : slot.trim();
7102
8010
  const label = authoredSlot.replace(/[_-]+/g, " ");
@@ -7137,6 +8045,165 @@ var init_executors = __esm({
7137
8045
  deps.events.note("ask.unfilled", { slot: slot.trim() });
7138
8046
  return { kind: "advance" };
7139
8047
  };
8048
+ ASK_POLICIES = /* @__PURE__ */ new Set(["offer_skip", "fallback", "end"]);
8049
+ askExecutor = async (node, state, deps) => {
8050
+ const cfg = node.config;
8051
+ if (typeof cfg.maxAttempts !== "number") return legacyAskExecutor(node, state, deps);
8052
+ const slot = str(cfg.slot) || node.id;
8053
+ const field = slot.trim();
8054
+ const label = (slot === node.id ? "" : field).replace(/[_-]+/g, " ");
8055
+ const required = cfg.required !== false;
8056
+ const maxAttempts = Math.min(6, Math.max(1, Math.round(cfg.maxAttempts)));
8057
+ const policy = ASK_POLICIES.has(str(cfg.onExhausted)) ? str(cfg.onExhausted) : "offer_skip";
8058
+ const rephrase = cfg.rephrase === "all" || cfg.rephrase === "never" ? cfg.rephrase : "reasks";
8059
+ const unfilledWired = deps.model.selectByPort(node.id, UNFILLED_PORT_ID) !== null;
8060
+ const captured = () => hasSlot(deps.processRt.getData(), slot);
8061
+ const exitNext = { kind: "advance" };
8062
+ if (captured()) {
8063
+ deps.events.note("ask.skipped", { slot: field, reason: "already known" });
8064
+ return exitNext;
8065
+ }
8066
+ const askText = renderLine(str(cfg.ask) || defaultAskText(slot, node.id), state, deps);
8067
+ const lines = askLinesFor(node, state, deps, askText, label, rephrase);
8068
+ const closing = () => interpolate(str(cfg.exhaustedMessage) || DEFAULT_ASK_EXHAUSTED_MESSAGE, scopeFor(state, deps));
8069
+ const giveUp = async () => {
8070
+ state.closingLine = closing();
8071
+ if (unfilledWired) return { kind: "take", port: UNFILLED_PORT_ID };
8072
+ await speak(state.closingLine, deps);
8073
+ return { kind: "end" };
8074
+ };
8075
+ let lastLine = askText;
8076
+ let rotation = 0;
8077
+ const nextLine = () => {
8078
+ const pool = lines.pool;
8079
+ if (pool.length === 0) return askText;
8080
+ let line = pool[rotation++ % pool.length];
8081
+ if (line.spoken === lastLine.spoken && pool.length > 1) line = pool[rotation++ % pool.length];
8082
+ return line;
8083
+ };
8084
+ const sayLine = async (line) => {
8085
+ lastLine = line;
8086
+ await speak(line.spoken, deps);
8087
+ };
8088
+ const kind = cfg.type === "phone" || cfg.type === "number" ? cfg.type : void 0;
8089
+ const turnOpts = (since) => ({
8090
+ fields: [slot],
8091
+ // the ask's question is about its slot by definition: every turn here answers it (a free-text slot takes the turn)
8092
+ answering: { fields: answerable(deps, [slot]), step: node.id, since },
8093
+ ...kind ? { number: { field: slot, kind } } : {}
8094
+ });
8095
+ let askedAt = Date.now();
8096
+ if (required && state.skipped?.[field]) {
8097
+ const revisits = (state.skipRevisits?.[field] ?? 0) + 1;
8098
+ state.skipRevisits = { ...state.skipRevisits, [field]: revisits };
8099
+ if (revisits >= 2) {
8100
+ deps.events.note("ask.revisit_exhausted", { slot: field });
8101
+ return giveUp();
8102
+ }
8103
+ deps.events.note("ask.revisit", { slot: field });
8104
+ await sayLine(askText);
8105
+ const t = await nextTurn(deps, state, () => sayLine(nextLine()), turnOpts(askedAt));
8106
+ if ("result" in t) return t.result;
8107
+ if (captured()) {
8108
+ deps.events.note("ask.captured", { slot: field });
8109
+ return exitNext;
8110
+ }
8111
+ deps.events.note("ask.revisit_unfilled", { slot: field });
8112
+ return giveUp();
8113
+ }
8114
+ const clarify = async (question) => {
8115
+ const endOn = lines.pool.length > 0 ? nextLine() : null;
8116
+ const what = label || "the detail just asked about";
8117
+ const parts = [
8118
+ `You are collecting "${what}" from the caller. They haven't given a clear, valid value yet.`,
8119
+ question ? "The caller asked a question: answer it briefly and honestly first." : "Acknowledge what they said in a few words.",
8120
+ // the model-safe copies only: caller-given values fenced and tokenised, never raw (§B.9)
8121
+ endOn ? `Then ask for just that again, ending on this question: "${endOn.model}"` : "Then briefly ask again for just that.",
8122
+ `Don't repeat this line word for word: "${lastLine.model}"`
8123
+ ];
8124
+ const instructions = interpolate(str(cfg.instructions), modelScopeFor(state, deps));
8125
+ if (instructions) parts.push(`About this question (from the flow's author): ${instructions}`);
8126
+ if (endOn) lastLine = endOn;
8127
+ await withHistory(deps, historyPolicyOf(node), () => genReply(deps, parts.join("\n")));
8128
+ };
8129
+ const round = async (attempts) => {
8130
+ let tries = 0;
8131
+ for (let turn = 1; ; turn++) {
8132
+ const t = await nextTurn(deps, state, () => sayLine(nextLine()), turnOpts(askedAt));
8133
+ if ("result" in t) return t.result;
8134
+ if (captured()) return "captured";
8135
+ const text = t.text.trim();
8136
+ const filler = isFiller(text);
8137
+ const question = !filler && isQuestion(text);
8138
+ if (!question) tries += 1;
8139
+ if (tries >= attempts) return "exhausted";
8140
+ if (turn >= 2 * attempts) {
8141
+ deps.events.note("ask.turn_cap", { slot: field, turns: turn });
8142
+ return "exhausted";
8143
+ }
8144
+ deps.events.note("ask.clarify", { slot: field, turn, kind: filler ? "filler" : question ? "question" : "answer" });
8145
+ if (filler && lines.pool.length > 0 || rephrase === "never") await sayLine(nextLine());
8146
+ else await clarify(question);
8147
+ }
8148
+ };
8149
+ await sayLine(lines.voicedAsk ?? askText);
8150
+ let outcome = await round(maxAttempts);
8151
+ if (outcome === "captured") {
8152
+ deps.events.note("ask.captured", { slot: field });
8153
+ return exitNext;
8154
+ }
8155
+ if (outcome !== "exhausted") return outcome;
8156
+ if (!required) {
8157
+ deps.events.note("ask.unfilled", { slot: field });
8158
+ return exitNext;
8159
+ }
8160
+ deps.events.note("ask.exhausted", { slot: field, policy });
8161
+ if (policy === "end" || policy === "fallback") return giveUp();
8162
+ const offer = lines.skipOffer;
8163
+ if (!offer) return giveUp();
8164
+ state.closingLine = closing();
8165
+ const skip = (reason) => {
8166
+ state.skipped = { ...state.skipped, [field]: reason };
8167
+ deps.events.note("ask.skipped_by_caller", { slot: field, reason });
8168
+ return unfilledWired ? { kind: "take", port: UNFILLED_PORT_ID } : exitNext;
8169
+ };
8170
+ let unclear = 0;
8171
+ await sayLine(offer);
8172
+ for (; ; ) {
8173
+ const t = await nextTurn(deps, state, offer.spoken, { fields: [slot], captureAfterRead: true });
8174
+ if ("result" in t) return t.result;
8175
+ const verdict = await readYesNo(t, state, deps);
8176
+ if (verdict === "yes") {
8177
+ await t.capture?.({ exclude: [slot] });
8178
+ return skip("caller");
8179
+ }
8180
+ if (verdict === "no") {
8181
+ await t.capture?.({ exclude: [slot] });
8182
+ deps.events.note("ask.skip_declined", { slot: field });
8183
+ askedAt = Date.now();
8184
+ await sayLine(nextLine());
8185
+ outcome = await round(maxAttempts);
8186
+ if (outcome === "captured") {
8187
+ deps.events.note("ask.captured", { slot: field });
8188
+ return exitNext;
8189
+ }
8190
+ if (outcome !== "exhausted") return outcome;
8191
+ deps.events.note("ask.exhausted", { slot: field, policy, round: 2 });
8192
+ return giveUp();
8193
+ }
8194
+ await t.capture?.();
8195
+ if (captured()) {
8196
+ deps.events.note("ask.captured", { slot: field });
8197
+ return exitNext;
8198
+ }
8199
+ unclear += 1;
8200
+ if (unclear >= 2) return skip("unclear");
8201
+ await sayLine(offer);
8202
+ }
8203
+ };
8204
+ KEEP_ASKING = /\b(?:don'?t|do not|not|never|no need to)(?:\s+\w+){0,3}\s+skip\b|\bi(?:'ve| have)(?: got)? it\b/i;
8205
+ SKIP_INTENT = /\b(?:skip|don'?t have|do not have|haven'?t got|move on|pass on (?:it|that|this))\b/i;
8206
+ OFFERS_IT = /\b(?:give|here|hold on|let me|it'?s|my number)\b/i;
7140
8207
  gateExecutor = async (node, _state, deps) => {
7141
8208
  const required = Array.isArray(node.config.requiredFields) ? node.config.requiredFields.filter((x) => typeof x === "string") : [];
7142
8209
  const data = deps.processRt.getData();
@@ -7159,7 +8226,7 @@ var init_executors = __esm({
7159
8226
  }
7160
8227
  const reason = str(cfg.reason) || `flow:${node.id}`;
7161
8228
  const mode = cfg.mode === "warm" || cfg.mode === "cold" ? cfg.mode : void 0;
7162
- const briefing = str(cfg.briefing) ? interpolate(str(cfg.briefing), scopeFor(state, deps)) : void 0;
8229
+ const briefing = str(cfg.briefing) ? interpolate(str(cfg.briefing), briefingScope(str(cfg.briefing), state, deps)) : void 0;
7163
8230
  deps.events.note("handoff.start", { node: node.id, to: target, ...mode ? { mode } : {} });
7164
8231
  try {
7165
8232
  await deps.ctx.handoff(target, {
@@ -7263,9 +8330,17 @@ ${collected}`);
7263
8330
  const toolRef = str(cfg.toolRef);
7264
8331
  const registry = deps.resolvers?.registryTools;
7265
8332
  if (toolRef && registry) {
7266
- deps.events.note("tool.invoked", { node: node.id, name: toolRef, ref: true });
7267
8333
  try {
7268
8334
  const spec = (await registry.catalog()).find((t) => t.name === toolRef);
8335
+ const blocked2 = blockOnMissingInputs(
8336
+ node,
8337
+ state,
8338
+ deps,
8339
+ Object.keys(spec?.input ?? {}).filter((f) => isRequiredField(deps, f)),
8340
+ true
8341
+ );
8342
+ if (blocked2) return blocked2;
8343
+ deps.events.note("tool.invoked", { node: node.id, name: toolRef, ref: true });
7269
8344
  const slots2 = deps.processRt.getData();
7270
8345
  const bag2 = {};
7271
8346
  for (const field of Object.keys(spec?.input ?? {})) {
@@ -7298,6 +8373,8 @@ ${collected}`);
7298
8373
  deps.events.note("tool.unavailable", { node: node.id, name: toolRef });
7299
8374
  return routeByOutcome();
7300
8375
  }
8376
+ const blocked = blockOnMissingInputs(node, state, deps, requiredInputsOf(node, state, deps), true);
8377
+ if (blocked) return blocked;
7301
8378
  const url = str(cfg.url);
7302
8379
  if (!url) {
7303
8380
  state.toolResults[node.id] = { ok: false, status: 0, data: null };
@@ -7523,7 +8600,7 @@ ${collected}`);
7523
8600
  deps.events.note("menu.selected", { node: node.id, choice });
7524
8601
  return { kind: "take", port: choice };
7525
8602
  }
7526
- await speak(prompt, deps);
8603
+ await speak(isEnglishLanguage(deps.askContext?.language) && prompt ? notCaught(prompt) : prompt, deps);
7527
8604
  }
7528
8605
  }
7529
8606
  deps.events.note("menu.unmatched", { node: node.id });
@@ -7796,7 +8873,7 @@ ${collected}`);
7796
8873
  }
7797
8874
  await speak(
7798
8875
  interpolate(
7799
- str(cfg.incompleteMessage) || "I wasn't able to get everything we need on this call, so let me have someone follow up with you. Thanks for your time.",
8876
+ str(cfg.incompleteMessage) || DEFAULT_ASK_EXHAUSTED_MESSAGE,
7800
8877
  scopeFor(state, deps)
7801
8878
  ),
7802
8879
  deps
@@ -7852,6 +8929,7 @@ ${collected}`);
7852
8929
  ...deps.resolvers ? { resolvers: deps.resolvers } : {},
7853
8930
  ...deps.live ? { live: deps.live } : {},
7854
8931
  ...deps.security ? { security: deps.security } : {},
8932
+ ...deps.askContext ? { askContext: deps.askContext } : {},
7855
8933
  depth: depth + 1
7856
8934
  });
7857
8935
  deps.events.note("subflow.exit", { node: node.id, ref, outcome: outcome.kind });
@@ -7864,6 +8942,8 @@ ${collected}`);
7864
8942
  EMAIL_SEND_TIMEOUT_MS = 1e4;
7865
8943
  emailExecutor = async (node, state, deps) => {
7866
8944
  const cfg = node.config;
8945
+ const blocked = blockOnMissingInputs(node, state, deps, requiredInputsOf(node, state, deps), false);
8946
+ if (blocked) return blocked;
7867
8947
  const scope2 = scopeFor(state, deps);
7868
8948
  const render = (key) => interpolate(str(cfg[key]), scope2).trim();
7869
8949
  const to = render("to");
@@ -7973,6 +9053,12 @@ async function selectNext(model2, from, result, state, deps) {
7973
9053
  ...state.lastDtmf !== void 0 ? { lastDtmf: state.lastDtmf } : {}
7974
9054
  });
7975
9055
  }
9056
+ async function closeAtLoopGuard(state, deps) {
9057
+ try {
9058
+ await speakLine(state.closingLine ?? DEFAULT_ASK_EXHAUSTED_MESSAGE, { ctx: deps.ctx, ...deps.realtime ? { realtime: true } : {} });
9059
+ } catch {
9060
+ }
9061
+ }
7976
9062
  async function runFlowGraph(program, deps) {
7977
9063
  const model2 = buildGraphModel(program);
7978
9064
  const maxNodeVisits = deps.maxNodeVisits ?? DEFAULT_MAX_NODE_VISITS;
@@ -7985,6 +9071,7 @@ async function runFlowGraph(program, deps) {
7985
9071
  while (cursor !== null) {
7986
9072
  if (++steps > maxSteps) {
7987
9073
  deps.events.note("flow.loop_guard_tripped", { reason: "max_steps", steps });
9074
+ await closeAtLoopGuard(state, deps);
7988
9075
  break;
7989
9076
  }
7990
9077
  const node = model2.node(cursor);
@@ -7997,6 +9084,7 @@ async function runFlowGraph(program, deps) {
7997
9084
  const visitCap = node.config.hub === true ? HUB_MAX_NODE_VISITS : maxNodeVisits;
7998
9085
  if (visit > visitCap) {
7999
9086
  deps.events.note("flow.loop_guard_tripped", { reason: "max_node_visits", nodeId: cursor });
9087
+ await closeAtLoopGuard(state, deps);
8000
9088
  break;
8001
9089
  }
8002
9090
  state.cursor = cursor;
@@ -8017,7 +9105,8 @@ async function runFlowGraph(program, deps) {
8017
9105
  ...deps.depth !== void 0 ? { depth: deps.depth } : {},
8018
9106
  ...deps.busy ? { busy: deps.busy } : {},
8019
9107
  ...deps.live ? { live: deps.live } : {},
8020
- ...deps.security ? { security: deps.security } : {}
9108
+ ...deps.security ? { security: deps.security } : {},
9109
+ ...deps.askContext ? { askContext: deps.askContext } : {}
8021
9110
  });
8022
9111
  } catch (err) {
8023
9112
  const message = err instanceof Error ? err.message : String(err);
@@ -8050,6 +9139,7 @@ var init_interpreter = __esm({
8050
9139
  init_log();
8051
9140
  init_graph_model();
8052
9141
  init_executors();
9142
+ init_src();
8053
9143
  init_conditions();
8054
9144
  DEFAULT_MAX_NODE_VISITS = 25;
8055
9145
  DEFAULT_MAX_STEPS = 200;
@@ -9010,6 +10100,13 @@ function createUtteranceStream(session, processRt, ctx, rails, prewarm, wantsDtm
9010
10100
  ...number ? { numberField: number.field } : {},
9011
10101
  ...answer ? { answering: answer.field } : {}
9012
10102
  };
10103
+ if (opts?.captureAfterRead) {
10104
+ const text = raw.text;
10105
+ return {
10106
+ ...turn,
10107
+ capture: (o) => capture(text, false, { ...step, ...o?.exclude?.length ? { exclude: o.exclude } : {} }, answer)
10108
+ };
10109
+ }
9013
10110
  if (opts?.deferCapture) {
9014
10111
  const run = capture(raw.text, true, step, answer).then(() => {
9015
10112
  if (pending === run) pending = null;
@@ -9237,14 +10334,23 @@ function defaultComplete() {
9237
10334
  model: model2,
9238
10335
  fallbackModel: RUNNER_MODEL,
9239
10336
  helper: "agent runner",
9240
- call: (m) => client.chat.completions.create(
9241
- {
9242
- model: m,
9243
- ...chatCompletionParams(m, { ...temperature !== void 0 ? { temperature } : {}, reasoningEffort: "low" }),
9244
- messages,
9245
- ...tools.length ? { tools } : {}
9246
- },
9247
- { timeout: RUNNER_TIMEOUT_MS }
10337
+ // With tools, a model that takes effort 'none' gets 'none' (OpenAI refuses tools at any other effort on chat
10338
+ // completions for GPT-5.2 and later), and a reasoning_effort 400 is learned and retried once (burn-down G-45).
10339
+ call: (m) => withReasoningEffortRetry(
10340
+ { model: m, tools: tools.length > 0, requested: "low" },
10341
+ () => client.chat.completions.create(
10342
+ {
10343
+ model: m,
10344
+ ...chatCompletionParams(m, {
10345
+ ...temperature !== void 0 ? { temperature } : {},
10346
+ reasoningEffort: "low",
10347
+ tools: tools.length > 0
10348
+ }),
10349
+ messages,
10350
+ ...tools.length ? { tools } : {}
10351
+ },
10352
+ { timeout: RUNNER_TIMEOUT_MS }
10353
+ )
9248
10354
  ),
9249
10355
  warn: graphWarn
9250
10356
  });
@@ -9755,7 +10861,8 @@ var NOT_SENT = /* @__PURE__ */ new Set([
9755
10861
  "email_provider_error",
9756
10862
  "email_send_failed",
9757
10863
  "insufficient_balance",
9758
- "spend_cap_exceeded"
10864
+ // a hard budget refused it (the key's monthly budget replaced the old per-key spend cap — monetization MON2)
10865
+ "budget_exceeded"
9759
10866
  ]);
9760
10867
  function dedupeConversationEmails(resolver) {
9761
10868
  const sends = /* @__PURE__ */ new Map();
@@ -10053,7 +11160,8 @@ async function runFlowProgram(program, ctx, opts) {
10053
11160
  ...opts.resolvers ? { resolvers: opts.resolvers } : {},
10054
11161
  ...opts.busy ? { busy: opts.busy } : {},
10055
11162
  ...opts.live ? { live: opts.live } : {},
10056
- ...security ? { security } : {}
11163
+ ...security ? { security } : {},
11164
+ ...opts.askContext ? { askContext: opts.askContext } : {}
10057
11165
  });
10058
11166
  graphLog("outcome", outcome);
10059
11167
  return outcome;
@@ -10569,14 +11677,15 @@ function createProcessRuntime(def) {
10569
11677
  console.error("[process] onField threw", err);
10570
11678
  }
10571
11679
  };
10572
- const answering = opts?.answering;
11680
+ const excluded = (name) => opts?.exclude?.some((f) => f.trim() === name.trim()) === true;
11681
+ const answering = opts?.answering !== void 0 && !excluded(opts.answering) ? opts.answering : void 0;
10573
11682
  const answerField = answering !== void 0 ? def.collect[answering] : void 0;
10574
11683
  const answerWasOpen = answering !== void 0 && !callerGave.has(answering);
10575
11684
  const status = primitive.getFieldStatus();
10576
11685
  const captured = (name) => status.find((f) => f.name === name)?.captured === true;
10577
11686
  const stepFields = opts?.stepFields;
10578
11687
  const named = (name) => stepFields?.some((f) => f.trim() === name.trim()) === true;
10579
- const spoken = Object.entries(def.collect).filter(([, f]) => f.fromCaller !== false);
11688
+ const spoken = Object.entries(def.collect).filter(([name, f]) => f.fromCaller !== false && !excluded(name));
10580
11689
  const open = (name) => !captured(name) || named(name) && !callerGave.has(name);
10581
11690
  const remaining = spoken.filter(([name]) => open(name) && (!completed || stepFields === void 0 || named(name)));
10582
11691
  const correctable = spoken.filter(([name]) => captured(name) && named(name) && callerGave.has(name));
@@ -11099,13 +12208,13 @@ function createTextSessionAdapter(transport) {
11099
12208
  };
11100
12209
  function setState(next) {
11101
12210
  const raw = String(next);
11102
- const norm = normalizeState(raw);
12211
+ const norm2 = normalizeState(raw);
11103
12212
  const oldState = currentState;
11104
12213
  const rawOld = currentRaw;
11105
- currentState = norm;
12214
+ currentState = norm2;
11106
12215
  currentRaw = raw;
11107
- fire("state", { oldState, newState: norm, rawOldState: rawOld, rawNewState: raw });
11108
- if (NON_BUSY_STATES.has(norm)) releaseIdleWaiters("idle");
12216
+ fire("state", { oldState, newState: norm2, rawOldState: rawOld, rawNewState: raw });
12217
+ if (NON_BUSY_STATES.has(norm2)) releaseIdleWaiters("idle");
11109
12218
  }
11110
12219
  function push(text, opts) {
11111
12220
  if (closed || text.length === 0) return;
@@ -11165,7 +12274,6 @@ function processSchemaToDefinition(schema) {
11165
12274
  } : {}
11166
12275
  };
11167
12276
  }
11168
- var DEFAULT_FLOW_PROMPT = "You are a helpful voice assistant. Keep replies short and natural.";
11169
12277
  function toTriggerCondition(on) {
11170
12278
  return on.startsWith("signal:") ? on : new RegExp(on, "i");
11171
12279
  }
@@ -11185,7 +12293,7 @@ function processSchemaToAgentParts(schema, opts = {}) {
11185
12293
  if (p.text.trim()) promptParts.push(p.text.trim());
11186
12294
  }
11187
12295
  }
11188
- const prompt = promptParts.length > 0 ? promptParts.join("\n\n") : DEFAULT_FLOW_PROMPT;
12296
+ const prompt = promptParts.length > 0 ? promptParts.join("\n\n") : DEFAULT_FLOW_PERSONA;
11189
12297
  let triggers;
11190
12298
  if (schema.triggers && schema.triggers.length > 0) {
11191
12299
  triggers = {};
@@ -11362,7 +12470,8 @@ function runTextSession(opts) {
11362
12470
  events,
11363
12471
  ...opts.maxSteps !== void 0 ? { maxSteps: opts.maxSteps } : {},
11364
12472
  ...opts.onAwaitInput ? { onAwaitInput: opts.onAwaitInput } : {},
11365
- ...opts.resolvers ? { resolvers: opts.resolvers } : {}
12473
+ ...opts.resolvers ? { resolvers: opts.resolvers } : {},
12474
+ ...opts.askContext ? { askContext: opts.askContext } : {}
11366
12475
  });
11367
12476
  return {
11368
12477
  sendUserMessage: (text) => {
@@ -12820,6 +13929,40 @@ function computeEmptyRequired(view) {
12820
13929
  return empty;
12821
13930
  }
12822
13931
 
13932
+ // src/flow-persona-boot.ts
13933
+ init_src();
13934
+ var VOICE_FLOW_CUE = "You are on a voice call: speak as in a natural spoken conversation.";
13935
+ async function resolveFlowPersona(agentId, fetchConfig, emit) {
13936
+ let stored = null;
13937
+ let unavailable = false;
13938
+ try {
13939
+ stored = await fetchConfig();
13940
+ } catch {
13941
+ unavailable = true;
13942
+ emit("flow.persona_unavailable", { agentId });
13943
+ }
13944
+ const persona = unavailable ? DEFAULT_FLOW_PERSONA : flowPersonaOf(stored);
13945
+ return {
13946
+ stored,
13947
+ unavailable,
13948
+ persona,
13949
+ basePrompt: `${persona}
13950
+
13951
+ ${VOICE_FLOW_CUE}`,
13952
+ askContext: {
13953
+ persona,
13954
+ routing: stored?.routingInstructions ?? null,
13955
+ language: stored?.language ?? null
13956
+ }
13957
+ };
13958
+ }
13959
+ function recordBootNotes(notes, graph, process2) {
13960
+ for (const n of notes) {
13961
+ if (graph) graph.note(n.kind, n.data);
13962
+ else process2.recordEvent({ kind: n.kind, data: n.data });
13963
+ }
13964
+ }
13965
+
12823
13966
  // src/agent-defaults.ts
12824
13967
  init_providers2();
12825
13968
  init_registry();
@@ -12897,7 +14040,7 @@ async function resolvePipeline(cfg, call, preloadedVad) {
12897
14040
  () => language.startsWith("en") ? livekitTurn.english() : livekitTurn.multilingual(),
12898
14041
  () => language.startsWith("en") ? livekitTurn.english() : livekitTurn.multilingual()
12899
14042
  ) : null;
12900
- const [stt, llm3, tts, vad] = await Promise.all([
14043
+ const [stt, llm4, tts, vad] = await Promise.all([
12901
14044
  sttFactory2(call),
12902
14045
  llmFactory2(call),
12903
14046
  ttsFactory2(call),
@@ -12913,7 +14056,7 @@ async function resolvePipeline(cfg, call, preloadedVad) {
12913
14056
  });
12914
14057
  }
12915
14058
  }
12916
- return { stt, llm: llm3, tts, vad, turnDetector };
14059
+ return { stt, llm: llm4, tts, vad, turnDetector };
12917
14060
  }
12918
14061
  var DEFAULT_REQUIRED_ENV = [
12919
14062
  "LIVEKIT_URL",
@@ -16392,13 +17535,7 @@ function isSipParticipant2(p) {
16392
17535
  }
16393
17536
 
16394
17537
  // src/runtime/prompt-compose.ts
16395
- function composeSystemPrompt(prompt, routingInstructions) {
16396
- const routing = routingInstructions?.trim();
16397
- return routing ? `${prompt}
16398
-
16399
- # Routing
16400
- ${routing}` : prompt;
16401
- }
17538
+ init_src();
16402
17539
  function composeAgentInstructions(input) {
16403
17540
  const composed = composeSystemPrompt(input.prompt, input.routingInstructions);
16404
17541
  return input.graph ? composed : input.augment(composed);
@@ -16417,7 +17554,7 @@ async function answerTextTurn(deps) {
16417
17554
  const { turn, config } = deps;
16418
17555
  const agentId = turn.agentId;
16419
17556
  const now = deps.now ?? Date.now;
16420
- const { voice: voice3, llm: llm3, initializeLogger, loggerOptions } = await import('@livekit/agents');
17557
+ const { voice: voice3, llm: llm4, initializeLogger, loggerOptions } = await import('@livekit/agents');
16421
17558
  if (loggerOptions() === void 0) initializeLogger({ pretty: false, level: "warn" });
16422
17559
  let effective = config;
16423
17560
  let pipeline = null;
@@ -16459,7 +17596,7 @@ async function answerTextTurn(deps) {
16459
17596
  getCtx
16460
17597
  });
16461
17598
  const instructions = processRt.augmentPrompt(composeSystemPrompt(effective.prompt, effective.routingInstructions));
16462
- const chatCtx = llm3.ChatContext.empty();
17599
+ const chatCtx = llm4.ChatContext.empty();
16463
17600
  let skipReply = false;
16464
17601
  for (const h of turn.history) {
16465
17602
  if (h.role === "user" && security && security.inputGuard.check(h.content).action === "block") {
@@ -16979,14 +18116,14 @@ function normalize(text) {
16979
18116
  return text.toLowerCase().replace(/\s+/g, " ").trim();
16980
18117
  }
16981
18118
  function detectVoicemailGreeting(text) {
16982
- const norm = normalize(text);
16983
- if (!norm) return false;
16984
- return VOICEMAIL_PATTERNS.some((re) => re.test(norm));
18119
+ const norm2 = normalize(text);
18120
+ if (!norm2) return false;
18121
+ return VOICEMAIL_PATTERNS.some((re) => re.test(norm2));
16985
18122
  }
16986
18123
  function detectBeepCue(text) {
16987
- const norm = normalize(text);
16988
- if (!norm) return false;
16989
- return BEEP_CUE_PATTERNS.some((re) => re.test(norm));
18124
+ const norm2 = normalize(text);
18125
+ if (!norm2) return false;
18126
+ return BEEP_CUE_PATTERNS.some((re) => re.test(norm2));
16990
18127
  }
16991
18128
  var DisabledAmdRuntime = class {
16992
18129
  enabled = false;
@@ -19040,6 +20177,7 @@ function toolAuditObserver(emit) {
19040
20177
  }
19041
20178
 
19042
20179
  // src/agent.ts
20180
+ init_resilient_llm();
19043
20181
  init_helper_models();
19044
20182
  var FLOW_HANGUP_GRACE_MS = Number(process.env["VL_FLOW_HANGUP_GRACE_MS"] ?? "1200");
19045
20183
  function isLiveKitChildProcess() {
@@ -19201,6 +20339,8 @@ var Agent = class {
19201
20339
  flowBootFailure = "worker_identity_refused";
19202
20340
  }
19203
20341
  let flowAgentName = null;
20342
+ let flowAskContext;
20343
+ const bootNotes = [];
19204
20344
  let loadedPlan = null;
19205
20345
  const planIdValue = stringValue(dispatchMdEarly["planId"]);
19206
20346
  if (planIdValue && sdkClient) {
@@ -19289,9 +20429,14 @@ var Agent = class {
19289
20429
  } else {
19290
20430
  const graphOn = process.env["VL_FLOW_GRAPH_RUNTIME"] !== "0" && !!schema.program;
19291
20431
  const schemaToolExecutor = buildGraphResolvers(callCredential)?.toolExecutor;
20432
+ const personaBoot = await resolveFlowPersona(flowAgentId, () => sdkClient.agents.getConfig(flowAgentId), (kind, data) => {
20433
+ console.warn(`[agent] ${kind}: the flow's published config couldn't be read; the default persona runs`, data);
20434
+ bootNotes.push({ kind, data: { ...data } });
20435
+ });
20436
+ flowAskContext = personaBoot.askContext;
19292
20437
  const parts = processSchemaToAgentParts(schema, {
19293
20438
  ...schemaToolExecutor ? { toolExecutor: schemaToolExecutor } : {},
19294
- ...this.config.flowRuntime.basePrompt ? { basePrompt: this.config.flowRuntime.basePrompt } : {},
20439
+ basePrompt: personaBoot.basePrompt,
19295
20440
  ...graphOn && schema.program ? { graphNodeIds: new Set(Object.keys(schema.program.nodes)) } : {}
19296
20441
  });
19297
20442
  effectiveConfig = {
@@ -19316,11 +20461,12 @@ var Agent = class {
19316
20461
  sdkClient,
19317
20462
  flowAgentId,
19318
20463
  effectiveConfig,
19319
- void 0
20464
+ void 0,
20465
+ personaBoot.unavailable ? void 0 : { stored: personaBoot.stored }
19320
20466
  )).config;
19321
20467
  flowAgentName = await flowAgentDisplayName(
19322
20468
  schema,
19323
- async () => (await sdkClient.agents.getConfig(flowAgentId))?.name ?? null
20469
+ async () => personaBoot.unavailable ? (await sdkClient.agents.getConfig(flowAgentId))?.name ?? null : personaBoot.stored?.name ?? null
19324
20470
  );
19325
20471
  if (graphOn && schema.program) {
19326
20472
  graphProgram = schema.program;
@@ -19372,7 +20518,11 @@ var Agent = class {
19372
20518
  void trackWrite(sdkClient.calls.appendEvent(opEventCallId, { kind, payload })).catch(() => {
19373
20519
  });
19374
20520
  };
20521
+ if (isResilientLLM(pipeline.llm)) {
20522
+ pipeline.llm.onIncident((incident) => emitOpEvent(`engine.llm.${incident.kind}`, { ...incident }));
20523
+ }
19375
20524
  const graphEvents = graphMode ? withOpEventFanout(createGraphEvents(processRt), emitOpEvent) : null;
20525
+ recordBootNotes(bootNotes, graphEvents, processRt);
19376
20526
  const isOutbound = stringValue(dispatchMdEarly["direction"]) === "outbound";
19377
20527
  const perCallAmd = stringValue(dispatchMdEarly["amd"]);
19378
20528
  const amdMode = perCallAmd === "off" || perCallAmd === "detect" || perCallAmd === "detectMessageEnd" ? perCallAmd : effectiveConfig.outbound?.amd ?? "off";
@@ -20187,7 +21337,9 @@ ${callIntent}`
20187
21337
  // Manual turn detection never runs SecureAgent.onUserTurnCompleted: each caller turn is guarded and sealed
20188
21338
  // as the stream admits it, before it joins the model's conversation (burn-down G-25, security §B.7). The cast:
20189
21339
  // lkAgent IS the SecureAgent when lkAgentIsSecure, whose factory type is deliberately loose (secure-agent.ts).
20190
- ...security && securityCallContext && lkAgentIsSecure ? { security: flowTurnSecurity(lkAgent, security, securityCallContext) } : {}
21340
+ ...security && securityCallContext && lkAgentIsSecure ? { security: flowTurnSecurity(lkAgent, security, securityCallContext) } : {},
21341
+ // the published persona / routing / language the ask steps' pre-built lines are checked against (G-48)
21342
+ ...flowAskContext ? { askContext: flowAskContext } : {}
20191
21343
  });
20192
21344
  if (outcome.kind === "error") {
20193
21345
  console.error("[agent] flow graph run errored", {
@@ -20324,9 +21476,9 @@ function sharedBrainPubSub() {
20324
21476
  }
20325
21477
  return _brainPubSub;
20326
21478
  }
20327
- async function applyStoredPipeline(client, agentId, base, codeMode) {
21479
+ async function applyStoredPipeline(client, agentId, base, codeMode, prefetched) {
20328
21480
  try {
20329
- const stored = await client.agents.getConfig(agentId);
21481
+ const stored = prefetched ? prefetched.stored : await client.agents.getConfig(agentId);
20330
21482
  if (!stored) return { config: base, brain: "none" };
20331
21483
  const mode = codeMode ?? stored.mode;
20332
21484
  const providers = /* @__PURE__ */ new Set();