@voicelayer/sdk 0.6.3 → 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
@@ -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
 
@@ -5305,15 +5773,22 @@ function start(opts) {
5305
5773
  ]
5306
5774
  });
5307
5775
  sdk.start();
5308
- const shutdown = async () => {
5309
- try {
5310
- await sdk?.shutdown();
5311
- await loggerProvider?.shutdown();
5312
- } catch {
5313
- }
5314
- };
5315
- process.once("SIGTERM", () => void shutdown().then(() => process.exit(0)));
5316
- 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
+ }
5317
5792
  }
5318
5793
  async function flushTelemetry() {
5319
5794
  const flushes = [];
@@ -6493,7 +6968,9 @@ function evaluateDeterministic(atom, ctx) {
6493
6968
  if (result === void 0) return false;
6494
6969
  if (!path || path === "ok") {
6495
6970
  const ok = result.ok === true || result.kind === "ok";
6496
- return op === "!=" ? !ok : ok;
6971
+ const want = (rawVal ?? "").trim().toLowerCase();
6972
+ const eq2 = want === "" ? ok : String(ok) === want;
6973
+ return op === "!=" ? !eq2 : eq2;
6497
6974
  }
6498
6975
  const actual = resolvePath(result, path);
6499
6976
  if (!op) return actual !== void 0 && actual !== null && actual !== "";
@@ -6688,28 +7165,12 @@ var init_conditions = __esm({
6688
7165
  });
6689
7166
 
6690
7167
  // src/runtime/graph/turn-text.ts
6691
- function lastSentences(line) {
6692
- const text = line.trim();
6693
- if (!text) return [];
6694
- const sentences = [];
6695
- let last = 0;
6696
- for (const m of text.matchAll(SENTENCE_END)) {
6697
- const end = (m.index ?? 0) + m[0].length;
6698
- sentences.push(text.slice(last, end));
6699
- last = end;
6700
- }
6701
- if (last < text.length) sentences.push(text.slice(last));
6702
- return sentences.slice(-2);
6703
- }
6704
- function isQuestion(line) {
6705
- return lastSentences(line).some((s) => QUESTION_MARK.test(s));
6706
- }
6707
7168
  function wordsOf(field) {
6708
7169
  return field.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
6709
7170
  }
6710
7171
  function asksFor(line, fields) {
6711
7172
  if (!line) return false;
6712
- 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());
6713
7174
  return questions.some((q) => {
6714
7175
  if (YES_NO.test(q) && !POLITE_REQUEST.test(q)) return false;
6715
7176
  const words = new Set(q.match(/[a-z0-9]+/g) ?? []);
@@ -6719,11 +7180,11 @@ function asksFor(line, fields) {
6719
7180
  });
6720
7181
  });
6721
7182
  }
6722
- var QUESTION_MARK, SENTENCE_END, POLITE_REQUEST, YES_NO;
7183
+ var QUESTION_MARK2, POLITE_REQUEST, YES_NO;
6723
7184
  var init_turn_text = __esm({
6724
7185
  "src/runtime/graph/turn-text.ts"() {
6725
- QUESTION_MARK = /[??؟]/;
6726
- SENTENCE_END = /[.!?…。!?؟]+["'”’)\]]*\s*/g;
7186
+ init_src();
7187
+ QUESTION_MARK2 = /[??؟]/;
6727
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/;
6728
7189
  YES_NO = /^(?:would|do|does|did|is|are|was|were|can|could|shall|should|will|have|has|had|may|might|am)\b/;
6729
7190
  }
@@ -7306,7 +7767,8 @@ async function nextTurn(deps, state, reprompt, opts) {
7306
7767
  ...opts?.deferCapture ? { deferCapture: true } : {},
7307
7768
  ...opts?.number ? { number: opts.number } : {},
7308
7769
  ...opts?.fields?.length ? { fields: opts.fields } : {},
7309
- ...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 } : {}
7310
7772
  });
7311
7773
  if (evt.kind === "hangup") return { result: { kind: "end" } };
7312
7774
  if (evt.kind === "control") return { result: evt.result };
@@ -7333,7 +7795,7 @@ async function nextTurn(deps, state, reprompt, opts) {
7333
7795
  state.lastNotInContext = evt.notInContext;
7334
7796
  state.lastAgentText = evt.agentText;
7335
7797
  state.lastDtmf = void 0;
7336
- return { text: evt.text };
7798
+ return { text: evt.text, ...evt.capture ? { capture: evt.capture } : {} };
7337
7799
  }
7338
7800
  }
7339
7801
  function detectYesNo(text) {
@@ -7343,6 +7805,58 @@ function detectYesNo(text) {
7343
7805
  if (no && !yes) return "no";
7344
7806
  return null;
7345
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
+ }
7346
7860
  function formatCollected(deps) {
7347
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");
7348
7862
  }
@@ -7396,6 +7910,27 @@ function positiveMs(raw, fallback) {
7396
7910
  const n = Number(raw);
7397
7911
  return Number.isFinite(n) && n > 0 ? n : fallback;
7398
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
+ }
7399
7934
  function routeByPort(node, deps, port) {
7400
7935
  return deps.model.selectByPort(node.id, port) !== null ? { kind: "take", port } : { kind: "advance" };
7401
7936
  }
@@ -7407,11 +7942,15 @@ function menuDigitOf(b) {
7407
7942
  function menuLabelOf(label) {
7408
7943
  return label.replace(/^\s*[0-9*#]\s*[-—–:.]*\s*/, "").trim() || label.trim();
7409
7944
  }
7410
- 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;
7411
7949
  var init_executors = __esm({
7412
7950
  "src/runtime/graph/executors.ts"() {
7413
7951
  init_log();
7414
7952
  init_src();
7953
+ init_reask_templates();
7415
7954
  init_interpolate();
7416
7955
  init_builtins();
7417
7956
  init_conditions();
@@ -7433,9 +7972,11 @@ var init_executors = __esm({
7433
7972
  NO_WORDS = /\b(no|nope|nah|wrong|incorrect|negative|not right|that'?s not|change|actually)\b/i;
7434
7973
  confirmExecutor = async (node, state, deps) => {
7435
7974
  const text = interpolate(str(node.config.text), scopeFor(state, deps));
7436
- 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;
7437
7978
  for (let attempt = 0; attempt < 2; attempt++) {
7438
- const t = await nextTurn(deps, state, text || "Is that correct?");
7979
+ const t = await nextTurn(deps, state, reprompt);
7439
7980
  if ("result" in t) return t.result;
7440
7981
  let verdict = null;
7441
7982
  if (t.dtmf === "1") verdict = "yes";
@@ -7463,7 +8004,7 @@ var init_executors = __esm({
7463
8004
  deps.events.note("confirm.unresolved", { node: node.id });
7464
8005
  return { kind: "advance" };
7465
8006
  };
7466
- askExecutor = async (node, state, deps) => {
8007
+ legacyAskExecutor = async (node, state, deps) => {
7467
8008
  const slot = str(node.config.slot) || node.id;
7468
8009
  const authoredSlot = slot === node.id ? "" : slot.trim();
7469
8010
  const label = authoredSlot.replace(/[_-]+/g, " ");
@@ -7504,6 +8045,165 @@ var init_executors = __esm({
7504
8045
  deps.events.note("ask.unfilled", { slot: slot.trim() });
7505
8046
  return { kind: "advance" };
7506
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;
7507
8207
  gateExecutor = async (node, _state, deps) => {
7508
8208
  const required = Array.isArray(node.config.requiredFields) ? node.config.requiredFields.filter((x) => typeof x === "string") : [];
7509
8209
  const data = deps.processRt.getData();
@@ -7526,7 +8226,7 @@ var init_executors = __esm({
7526
8226
  }
7527
8227
  const reason = str(cfg.reason) || `flow:${node.id}`;
7528
8228
  const mode = cfg.mode === "warm" || cfg.mode === "cold" ? cfg.mode : void 0;
7529
- 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;
7530
8230
  deps.events.note("handoff.start", { node: node.id, to: target, ...mode ? { mode } : {} });
7531
8231
  try {
7532
8232
  await deps.ctx.handoff(target, {
@@ -7630,9 +8330,17 @@ ${collected}`);
7630
8330
  const toolRef = str(cfg.toolRef);
7631
8331
  const registry = deps.resolvers?.registryTools;
7632
8332
  if (toolRef && registry) {
7633
- deps.events.note("tool.invoked", { node: node.id, name: toolRef, ref: true });
7634
8333
  try {
7635
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 });
7636
8344
  const slots2 = deps.processRt.getData();
7637
8345
  const bag2 = {};
7638
8346
  for (const field of Object.keys(spec?.input ?? {})) {
@@ -7665,6 +8373,8 @@ ${collected}`);
7665
8373
  deps.events.note("tool.unavailable", { node: node.id, name: toolRef });
7666
8374
  return routeByOutcome();
7667
8375
  }
8376
+ const blocked = blockOnMissingInputs(node, state, deps, requiredInputsOf(node, state, deps), true);
8377
+ if (blocked) return blocked;
7668
8378
  const url = str(cfg.url);
7669
8379
  if (!url) {
7670
8380
  state.toolResults[node.id] = { ok: false, status: 0, data: null };
@@ -7890,7 +8600,7 @@ ${collected}`);
7890
8600
  deps.events.note("menu.selected", { node: node.id, choice });
7891
8601
  return { kind: "take", port: choice };
7892
8602
  }
7893
- await speak(prompt, deps);
8603
+ await speak(isEnglishLanguage(deps.askContext?.language) && prompt ? notCaught(prompt) : prompt, deps);
7894
8604
  }
7895
8605
  }
7896
8606
  deps.events.note("menu.unmatched", { node: node.id });
@@ -8163,7 +8873,7 @@ ${collected}`);
8163
8873
  }
8164
8874
  await speak(
8165
8875
  interpolate(
8166
- 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,
8167
8877
  scopeFor(state, deps)
8168
8878
  ),
8169
8879
  deps
@@ -8219,6 +8929,7 @@ ${collected}`);
8219
8929
  ...deps.resolvers ? { resolvers: deps.resolvers } : {},
8220
8930
  ...deps.live ? { live: deps.live } : {},
8221
8931
  ...deps.security ? { security: deps.security } : {},
8932
+ ...deps.askContext ? { askContext: deps.askContext } : {},
8222
8933
  depth: depth + 1
8223
8934
  });
8224
8935
  deps.events.note("subflow.exit", { node: node.id, ref, outcome: outcome.kind });
@@ -8231,6 +8942,8 @@ ${collected}`);
8231
8942
  EMAIL_SEND_TIMEOUT_MS = 1e4;
8232
8943
  emailExecutor = async (node, state, deps) => {
8233
8944
  const cfg = node.config;
8945
+ const blocked = blockOnMissingInputs(node, state, deps, requiredInputsOf(node, state, deps), false);
8946
+ if (blocked) return blocked;
8234
8947
  const scope2 = scopeFor(state, deps);
8235
8948
  const render = (key) => interpolate(str(cfg[key]), scope2).trim();
8236
8949
  const to = render("to");
@@ -8340,6 +9053,12 @@ async function selectNext(model2, from, result, state, deps) {
8340
9053
  ...state.lastDtmf !== void 0 ? { lastDtmf: state.lastDtmf } : {}
8341
9054
  });
8342
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
+ }
8343
9062
  async function runFlowGraph(program, deps) {
8344
9063
  const model2 = buildGraphModel(program);
8345
9064
  const maxNodeVisits = deps.maxNodeVisits ?? DEFAULT_MAX_NODE_VISITS;
@@ -8352,6 +9071,7 @@ async function runFlowGraph(program, deps) {
8352
9071
  while (cursor !== null) {
8353
9072
  if (++steps > maxSteps) {
8354
9073
  deps.events.note("flow.loop_guard_tripped", { reason: "max_steps", steps });
9074
+ await closeAtLoopGuard(state, deps);
8355
9075
  break;
8356
9076
  }
8357
9077
  const node = model2.node(cursor);
@@ -8364,6 +9084,7 @@ async function runFlowGraph(program, deps) {
8364
9084
  const visitCap = node.config.hub === true ? HUB_MAX_NODE_VISITS : maxNodeVisits;
8365
9085
  if (visit > visitCap) {
8366
9086
  deps.events.note("flow.loop_guard_tripped", { reason: "max_node_visits", nodeId: cursor });
9087
+ await closeAtLoopGuard(state, deps);
8367
9088
  break;
8368
9089
  }
8369
9090
  state.cursor = cursor;
@@ -8384,7 +9105,8 @@ async function runFlowGraph(program, deps) {
8384
9105
  ...deps.depth !== void 0 ? { depth: deps.depth } : {},
8385
9106
  ...deps.busy ? { busy: deps.busy } : {},
8386
9107
  ...deps.live ? { live: deps.live } : {},
8387
- ...deps.security ? { security: deps.security } : {}
9108
+ ...deps.security ? { security: deps.security } : {},
9109
+ ...deps.askContext ? { askContext: deps.askContext } : {}
8388
9110
  });
8389
9111
  } catch (err) {
8390
9112
  const message = err instanceof Error ? err.message : String(err);
@@ -8417,6 +9139,7 @@ var init_interpreter = __esm({
8417
9139
  init_log();
8418
9140
  init_graph_model();
8419
9141
  init_executors();
9142
+ init_src();
8420
9143
  init_conditions();
8421
9144
  DEFAULT_MAX_NODE_VISITS = 25;
8422
9145
  DEFAULT_MAX_STEPS = 200;
@@ -9377,6 +10100,13 @@ function createUtteranceStream(session, processRt, ctx, rails, prewarm, wantsDtm
9377
10100
  ...number ? { numberField: number.field } : {},
9378
10101
  ...answer ? { answering: answer.field } : {}
9379
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
+ }
9380
10110
  if (opts?.deferCapture) {
9381
10111
  const run = capture(raw.text, true, step, answer).then(() => {
9382
10112
  if (pending === run) pending = null;
@@ -10131,7 +10861,8 @@ var NOT_SENT = /* @__PURE__ */ new Set([
10131
10861
  "email_provider_error",
10132
10862
  "email_send_failed",
10133
10863
  "insufficient_balance",
10134
- "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"
10135
10866
  ]);
10136
10867
  function dedupeConversationEmails(resolver) {
10137
10868
  const sends = /* @__PURE__ */ new Map();
@@ -10429,7 +11160,8 @@ async function runFlowProgram(program, ctx, opts) {
10429
11160
  ...opts.resolvers ? { resolvers: opts.resolvers } : {},
10430
11161
  ...opts.busy ? { busy: opts.busy } : {},
10431
11162
  ...opts.live ? { live: opts.live } : {},
10432
- ...security ? { security } : {}
11163
+ ...security ? { security } : {},
11164
+ ...opts.askContext ? { askContext: opts.askContext } : {}
10433
11165
  });
10434
11166
  graphLog("outcome", outcome);
10435
11167
  return outcome;
@@ -10945,14 +11677,15 @@ function createProcessRuntime(def) {
10945
11677
  console.error("[process] onField threw", err);
10946
11678
  }
10947
11679
  };
10948
- 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;
10949
11682
  const answerField = answering !== void 0 ? def.collect[answering] : void 0;
10950
11683
  const answerWasOpen = answering !== void 0 && !callerGave.has(answering);
10951
11684
  const status = primitive.getFieldStatus();
10952
11685
  const captured = (name) => status.find((f) => f.name === name)?.captured === true;
10953
11686
  const stepFields = opts?.stepFields;
10954
11687
  const named = (name) => stepFields?.some((f) => f.trim() === name.trim()) === true;
10955
- 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));
10956
11689
  const open = (name) => !captured(name) || named(name) && !callerGave.has(name);
10957
11690
  const remaining = spoken.filter(([name]) => open(name) && (!completed || stepFields === void 0 || named(name)));
10958
11691
  const correctable = spoken.filter(([name]) => captured(name) && named(name) && callerGave.has(name));
@@ -11475,13 +12208,13 @@ function createTextSessionAdapter(transport) {
11475
12208
  };
11476
12209
  function setState(next) {
11477
12210
  const raw = String(next);
11478
- const norm = normalizeState(raw);
12211
+ const norm2 = normalizeState(raw);
11479
12212
  const oldState = currentState;
11480
12213
  const rawOld = currentRaw;
11481
- currentState = norm;
12214
+ currentState = norm2;
11482
12215
  currentRaw = raw;
11483
- fire("state", { oldState, newState: norm, rawOldState: rawOld, rawNewState: raw });
11484
- 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");
11485
12218
  }
11486
12219
  function push(text, opts) {
11487
12220
  if (closed || text.length === 0) return;
@@ -11541,7 +12274,6 @@ function processSchemaToDefinition(schema) {
11541
12274
  } : {}
11542
12275
  };
11543
12276
  }
11544
- var DEFAULT_FLOW_PROMPT = "You are a helpful voice assistant. Keep replies short and natural.";
11545
12277
  function toTriggerCondition(on) {
11546
12278
  return on.startsWith("signal:") ? on : new RegExp(on, "i");
11547
12279
  }
@@ -11561,7 +12293,7 @@ function processSchemaToAgentParts(schema, opts = {}) {
11561
12293
  if (p.text.trim()) promptParts.push(p.text.trim());
11562
12294
  }
11563
12295
  }
11564
- 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;
11565
12297
  let triggers;
11566
12298
  if (schema.triggers && schema.triggers.length > 0) {
11567
12299
  triggers = {};
@@ -11738,7 +12470,8 @@ function runTextSession(opts) {
11738
12470
  events,
11739
12471
  ...opts.maxSteps !== void 0 ? { maxSteps: opts.maxSteps } : {},
11740
12472
  ...opts.onAwaitInput ? { onAwaitInput: opts.onAwaitInput } : {},
11741
- ...opts.resolvers ? { resolvers: opts.resolvers } : {}
12473
+ ...opts.resolvers ? { resolvers: opts.resolvers } : {},
12474
+ ...opts.askContext ? { askContext: opts.askContext } : {}
11742
12475
  });
11743
12476
  return {
11744
12477
  sendUserMessage: (text) => {
@@ -13196,6 +13929,40 @@ function computeEmptyRequired(view) {
13196
13929
  return empty;
13197
13930
  }
13198
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
+
13199
13966
  // src/agent-defaults.ts
13200
13967
  init_providers2();
13201
13968
  init_registry();
@@ -16768,13 +17535,7 @@ function isSipParticipant2(p) {
16768
17535
  }
16769
17536
 
16770
17537
  // src/runtime/prompt-compose.ts
16771
- function composeSystemPrompt(prompt, routingInstructions) {
16772
- const routing = routingInstructions?.trim();
16773
- return routing ? `${prompt}
16774
-
16775
- # Routing
16776
- ${routing}` : prompt;
16777
- }
17538
+ init_src();
16778
17539
  function composeAgentInstructions(input) {
16779
17540
  const composed = composeSystemPrompt(input.prompt, input.routingInstructions);
16780
17541
  return input.graph ? composed : input.augment(composed);
@@ -17355,14 +18116,14 @@ function normalize(text) {
17355
18116
  return text.toLowerCase().replace(/\s+/g, " ").trim();
17356
18117
  }
17357
18118
  function detectVoicemailGreeting(text) {
17358
- const norm = normalize(text);
17359
- if (!norm) return false;
17360
- 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));
17361
18122
  }
17362
18123
  function detectBeepCue(text) {
17363
- const norm = normalize(text);
17364
- if (!norm) return false;
17365
- 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));
17366
18127
  }
17367
18128
  var DisabledAmdRuntime = class {
17368
18129
  enabled = false;
@@ -19578,6 +20339,8 @@ var Agent = class {
19578
20339
  flowBootFailure = "worker_identity_refused";
19579
20340
  }
19580
20341
  let flowAgentName = null;
20342
+ let flowAskContext;
20343
+ const bootNotes = [];
19581
20344
  let loadedPlan = null;
19582
20345
  const planIdValue = stringValue(dispatchMdEarly["planId"]);
19583
20346
  if (planIdValue && sdkClient) {
@@ -19666,9 +20429,14 @@ var Agent = class {
19666
20429
  } else {
19667
20430
  const graphOn = process.env["VL_FLOW_GRAPH_RUNTIME"] !== "0" && !!schema.program;
19668
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;
19669
20437
  const parts = processSchemaToAgentParts(schema, {
19670
20438
  ...schemaToolExecutor ? { toolExecutor: schemaToolExecutor } : {},
19671
- ...this.config.flowRuntime.basePrompt ? { basePrompt: this.config.flowRuntime.basePrompt } : {},
20439
+ basePrompt: personaBoot.basePrompt,
19672
20440
  ...graphOn && schema.program ? { graphNodeIds: new Set(Object.keys(schema.program.nodes)) } : {}
19673
20441
  });
19674
20442
  effectiveConfig = {
@@ -19693,11 +20461,12 @@ var Agent = class {
19693
20461
  sdkClient,
19694
20462
  flowAgentId,
19695
20463
  effectiveConfig,
19696
- void 0
20464
+ void 0,
20465
+ personaBoot.unavailable ? void 0 : { stored: personaBoot.stored }
19697
20466
  )).config;
19698
20467
  flowAgentName = await flowAgentDisplayName(
19699
20468
  schema,
19700
- async () => (await sdkClient.agents.getConfig(flowAgentId))?.name ?? null
20469
+ async () => personaBoot.unavailable ? (await sdkClient.agents.getConfig(flowAgentId))?.name ?? null : personaBoot.stored?.name ?? null
19701
20470
  );
19702
20471
  if (graphOn && schema.program) {
19703
20472
  graphProgram = schema.program;
@@ -19753,6 +20522,7 @@ var Agent = class {
19753
20522
  pipeline.llm.onIncident((incident) => emitOpEvent(`engine.llm.${incident.kind}`, { ...incident }));
19754
20523
  }
19755
20524
  const graphEvents = graphMode ? withOpEventFanout(createGraphEvents(processRt), emitOpEvent) : null;
20525
+ recordBootNotes(bootNotes, graphEvents, processRt);
19756
20526
  const isOutbound = stringValue(dispatchMdEarly["direction"]) === "outbound";
19757
20527
  const perCallAmd = stringValue(dispatchMdEarly["amd"]);
19758
20528
  const amdMode = perCallAmd === "off" || perCallAmd === "detect" || perCallAmd === "detectMessageEnd" ? perCallAmd : effectiveConfig.outbound?.amd ?? "off";
@@ -20567,7 +21337,9 @@ ${callIntent}`
20567
21337
  // Manual turn detection never runs SecureAgent.onUserTurnCompleted: each caller turn is guarded and sealed
20568
21338
  // as the stream admits it, before it joins the model's conversation (burn-down G-25, security §B.7). The cast:
20569
21339
  // lkAgent IS the SecureAgent when lkAgentIsSecure, whose factory type is deliberately loose (secure-agent.ts).
20570
- ...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 } : {}
20571
21343
  });
20572
21344
  if (outcome.kind === "error") {
20573
21345
  console.error("[agent] flow graph run errored", {
@@ -20704,9 +21476,9 @@ function sharedBrainPubSub() {
20704
21476
  }
20705
21477
  return _brainPubSub;
20706
21478
  }
20707
- async function applyStoredPipeline(client, agentId, base, codeMode) {
21479
+ async function applyStoredPipeline(client, agentId, base, codeMode, prefetched) {
20708
21480
  try {
20709
- const stored = await client.agents.getConfig(agentId);
21481
+ const stored = prefetched ? prefetched.stored : await client.agents.getConfig(agentId);
20710
21482
  if (!stored) return { config: base, brain: "none" };
20711
21483
  const mode = codeMode ?? stored.mode;
20712
21484
  const providers = /* @__PURE__ */ new Set();