@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.
@@ -14,6 +14,7 @@ import '@opentelemetry/sdk-logs';
14
14
  import '@opentelemetry/sdk-metrics';
15
15
  import '@opentelemetry/sdk-node';
16
16
  import '@opentelemetry/semantic-conventions';
17
+ import { llm, DEFAULT_API_CONNECT_OPTIONS, intervalForRetry, APIStatusError, APITimeoutError, APIConnectionError } from '@livekit/agents';
17
18
  import http from 'http';
18
19
  import https from 'https';
19
20
  import { Readable } from 'stream';
@@ -44,35 +45,6 @@ var init_log = __esm({
44
45
  }
45
46
  });
46
47
 
47
- // src/runtime/graph/graph-model.ts
48
- function buildGraphModel(program) {
49
- return {
50
- entry: program.entry,
51
- node: (id) => program.nodes[id],
52
- out: (id) => program.nodes[id]?.out ?? [],
53
- selectByPort: (id, port) => {
54
- const edges = program.nodes[id]?.out ?? [];
55
- const byHandle = edges.find((e) => e.sourceHandle === port);
56
- if (byHandle) return byHandle.to;
57
- const byLabel = edges.find((e) => e.label === port);
58
- return byLabel ? byLabel.to : null;
59
- },
60
- askNodeForSlot: (slot) => {
61
- const want = slot.trim();
62
- for (const node of Object.values(program.nodes)) {
63
- if (node.type !== "ask") continue;
64
- const s = typeof node.config.slot === "string" ? node.config.slot : node.id;
65
- if (s.trim() === want) return node.id;
66
- }
67
- return void 0;
68
- }
69
- };
70
- }
71
- var init_graph_model = __esm({
72
- "src/runtime/graph/graph-model.ts"() {
73
- }
74
- });
75
-
76
48
  // ../contracts/src/effect-stack.ts
77
49
  var EffectStack;
78
50
  var init_effect_stack = __esm({
@@ -1060,11 +1032,10 @@ var init_agent_config = __esm({
1060
1032
  projectId: z.string().min(1),
1061
1033
  // Human-readable label. No `agentType` enum on purpose.
1062
1034
  name: z.string().min(1).max(128),
1063
- // Soft override of the worker's baked-in prompt. Absent → call_start
1064
- // uses the SDK worker's predefined prompt (the source of truth for
1065
- // pre-defined agents like FNOL). Present → it overrides the worker's
1066
- // prompt at call_start as a snapshot decision; mid-call edits are not
1067
- // honoured. Per-call override at call_start time is also allowed.
1035
+ // The agent's persona (the Behaviour page). For a FLOW agent the PUBLISHED value is the flow persona on every
1036
+ // transport (flowPersonaOf — voice boot and text replies alike; absent → DEFAULT_FLOW_PERSONA), read at call start as
1037
+ // a snapshot decision; mid-call edits are not honoured (burn-down G-46). A coded SDK agent's prompt is its code: this
1038
+ // field doesn't reach it. A call plan may still override the prompt per call.
1068
1039
  systemPrompt: z.string().max(64e3).optional(),
1069
1040
  // Routing/decision layer (Voiceflow "Instructions"): plain-language guidance the
1070
1041
  // agent reads to decide WHEN to do things (vs systemPrompt = WHO it is). Injected
@@ -1945,12 +1916,143 @@ var init_call_result = __esm({
1945
1916
  });
1946
1917
  }
1947
1918
  });
1919
+ function supportedZones() {
1920
+ if (!SUPPORTED_ZONES) {
1921
+ const intl = Intl;
1922
+ SUPPORTED_ZONES = new Set(intl.supportedValuesOf?.("timeZone") ?? []);
1923
+ }
1924
+ return SUPPORTED_ZONES;
1925
+ }
1926
+ function isIanaTimezone(tz) {
1927
+ if (tz === "UTC" || supportedZones().has(tz)) return true;
1928
+ if (!IANA_NAME.test(tz)) return false;
1929
+ try {
1930
+ new Intl.DateTimeFormat("en-US", { timeZone: tz });
1931
+ return true;
1932
+ } catch {
1933
+ return false;
1934
+ }
1935
+ }
1936
+ 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;
1937
+ var init_budgets = __esm({
1938
+ "../contracts/src/budgets.ts"() {
1939
+ BUDGET_SCOPE_KINDS = ["workspace", "agent", "api_key", "channel"];
1940
+ BudgetScopeKind = z.enum(BUDGET_SCOPE_KINDS);
1941
+ CREATABLE_BUDGET_SCOPE_KINDS = ["workspace", "api_key", "channel"];
1942
+ BUDGET_CHANNELS = ["voice", "sms", "email", "chat", "discord"];
1943
+ z.enum(BUDGET_CHANNELS);
1944
+ BudgetScope = z.object({
1945
+ kind: BudgetScopeKind,
1946
+ // null for the workspace; the API key's id, or the channel name.
1947
+ id: z.string().min(1).nullable()
1948
+ });
1949
+ BudgetPeriodKind = z.enum(["day", "month", "billing_period"]);
1950
+ BudgetEnforcement = z.enum(["soft", "hard"]);
1951
+ BudgetInbound = z.enum(["refuse", "answer"]);
1952
+ BudgetOwner = z.enum(["customer", "operator"]);
1953
+ DEFAULT_BUDGET_THRESHOLDS = [50, 80, 100];
1954
+ BUDGET_MAX_USD = 1e6;
1955
+ 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));
1956
+ IANA_NAME = /^[A-Za-z]+(?:\/[A-Za-z0-9_+-]+)+$/;
1957
+ SUPPORTED_ZONES = null;
1958
+ Timezone = z.string().min(1).max(64).refine(isIanaTimezone, "use an IANA time zone name, like America/New_York or UTC");
1959
+ BudgetPeriodStatus = z.object({
1960
+ periodStart: z.string(),
1961
+ // When the budget resets.
1962
+ periodEnd: z.string(),
1963
+ // Charged usage this period.
1964
+ settledUsd: z.string(),
1965
+ // Reserved for calls in progress.
1966
+ inProgressUsd: z.string(),
1967
+ // settled + in progress — what the budget compares with its amount.
1968
+ spentUsd: z.string(),
1969
+ // spent ÷ amount, as a whole percent (may pass 100: calls in progress finish).
1970
+ percent: z.number().int().min(0),
1971
+ // A hard budget at its limit: new actions in its scope are refused until periodEnd.
1972
+ reached: z.boolean()
1973
+ });
1974
+ Budget = z.object({
1975
+ id: z.string(),
1976
+ scope: BudgetScope,
1977
+ period: BudgetPeriodKind,
1978
+ timezone: z.string(),
1979
+ amountUsd: z.string(),
1980
+ enforcement: BudgetEnforcement,
1981
+ thresholds: z.array(z.number().int()),
1982
+ notifyEmail: z.boolean(),
1983
+ notifyWebhook: z.boolean(),
1984
+ // Operator budgets are VoiceLayer's own safety limits: shown to the customer, read-only.
1985
+ owner: BudgetOwner,
1986
+ inbound: BudgetInbound,
1987
+ createdAt: z.string(),
1988
+ current: BudgetPeriodStatus
1989
+ });
1990
+ z.object({ budgets: z.array(Budget) });
1991
+ z.object({
1992
+ scope: z.object({ kind: z.enum(CREATABLE_BUDGET_SCOPE_KINDS), id: z.string().min(1).max(200).nullable().optional() }),
1993
+ period: BudgetPeriodKind,
1994
+ timezone: Timezone.default("UTC"),
1995
+ amountUsd: z.number().positive().max(BUDGET_MAX_USD),
1996
+ enforcement: BudgetEnforcement,
1997
+ thresholds: Thresholds.default([...DEFAULT_BUDGET_THRESHOLDS]),
1998
+ notifyEmail: z.boolean().default(true),
1999
+ notifyWebhook: z.boolean().default(true),
2000
+ // Hard budgets only; a customer budget refuses incoming calls unless told to answer them (MON-9).
2001
+ inbound: BudgetInbound.default("refuse")
2002
+ }).superRefine((b, ctx) => {
2003
+ const id = b.scope.id ?? null;
2004
+ if (b.scope.kind === "workspace" && id !== null) ctx.addIssue({ code: "custom", path: ["scope", "id"], message: "a workspace budget names no scope id" });
2005
+ if (b.scope.kind !== "workspace" && id === null) ctx.addIssue({ code: "custom", path: ["scope", "id"], message: "name the API key or channel" });
2006
+ if (b.scope.kind === "channel" && id !== null && !BUDGET_CHANNELS.includes(id)) {
2007
+ ctx.addIssue({ code: "custom", path: ["scope", "id"], message: `channel is one of ${BUDGET_CHANNELS.join(", ")}` });
2008
+ }
2009
+ });
2010
+ z.object({
2011
+ amountUsd: z.number().positive().max(BUDGET_MAX_USD),
2012
+ enforcement: BudgetEnforcement,
2013
+ thresholds: Thresholds,
2014
+ notifyEmail: z.boolean(),
2015
+ notifyWebhook: z.boolean(),
2016
+ inbound: BudgetInbound
2017
+ }).partial().refine((b) => Object.keys(b).length > 0, "nothing to change");
2018
+ BudgetRefusalDetail = z.object({
2019
+ budgetId: z.string(),
2020
+ scope: BudgetScope,
2021
+ period: BudgetPeriodKind,
2022
+ resetsAt: z.string()
2023
+ });
2024
+ BillingNoticeKind = z.enum([
2025
+ "balance_empty",
2026
+ "credit_limit_reached",
2027
+ "auto_topup_off",
2028
+ "wallet_frozen",
2029
+ "invoice_issued",
2030
+ "invoice_overdue",
2031
+ "workspace_paused",
2032
+ "workspace_restored",
2033
+ "budget_threshold",
2034
+ "budget_reached"
2035
+ ]);
2036
+ BillingNotice = z.object({
2037
+ id: z.string(),
2038
+ kind: BillingNoticeKind,
2039
+ // The same words as the email's subject and first paragraph.
2040
+ title: z.string(),
2041
+ body: z.string(),
2042
+ // The budget a budget notice is about.
2043
+ budgetId: z.string().nullable(),
2044
+ createdAt: z.string()
2045
+ });
2046
+ z.object({ notices: z.array(BillingNotice) });
2047
+ }
2048
+ });
1948
2049
  var WEBHOOK_EVENT_TYPES, CallWebhookDirection, CallWebhookOutcome, CallWebhookAttributes, Party, CALL_ENDED_PAYLOAD_VERSION, CallEndedTranscript;
1949
2050
  var init_webhook_events = __esm({
1950
2051
  "../contracts/src/webhook-events.ts"() {
1951
2052
  init_call_result();
1952
2053
  init_call_outcome();
1953
- WEBHOOK_EVENT_TYPES = ["call.started", "call.ended", "recording.ready"];
2054
+ init_budgets();
2055
+ WEBHOOK_EVENT_TYPES = ["call.started", "call.ended", "recording.ready", "billing.budget_threshold", "billing.budget_reached"];
1954
2056
  z.enum(WEBHOOK_EVENT_TYPES);
1955
2057
  CallWebhookDirection = z.enum(["inbound", "outbound", "web"]);
1956
2058
  CallWebhookOutcome = z.enum(["completed", "dropped", "failed", "no_answer", "other"]);
@@ -2009,6 +2111,19 @@ var init_webhook_events = __esm({
2009
2111
  recordingUrl: z.string().url(),
2010
2112
  status: z.enum(["starting", "active", "completed", "failed", "aborted"])
2011
2113
  });
2114
+ z.object({
2115
+ budgetId: z.string(),
2116
+ scope: BudgetScope,
2117
+ period: BudgetPeriodKind,
2118
+ periodStart: z.string().datetime({ offset: true }),
2119
+ periodEnd: z.string().datetime({ offset: true }),
2120
+ amountUsd: z.string(),
2121
+ spentUsd: z.string(),
2122
+ inProgressUsd: z.string(),
2123
+ // The threshold passed (budget_reached: 100).
2124
+ thresholdPct: z.number().int().min(1).max(100),
2125
+ enforcement: BudgetEnforcement
2126
+ });
2012
2127
  }
2013
2128
  });
2014
2129
  var PhoneNumberStatus, PhoneNumberProvider, PhoneNumberCapabilities, PhoneNumberRecord;
@@ -2083,10 +2198,154 @@ var init_project_limits = __esm({
2083
2198
  });
2084
2199
 
2085
2200
  // ../contracts/src/flow-ports.ts
2086
- var INCOMPLETE_PORT_ID;
2201
+ var INCOMPLETE_PORT_ID, UNFILLED_PORT_ID, MISSING_PORT_ID;
2087
2202
  var init_flow_ports = __esm({
2088
2203
  "../contracts/src/flow-ports.ts"() {
2089
2204
  INCOMPLETE_PORT_ID = "incomplete";
2205
+ UNFILLED_PORT_ID = "unfilled";
2206
+ MISSING_PORT_ID = "missing";
2207
+ }
2208
+ });
2209
+
2210
+ // ../contracts/src/flow-persona.ts
2211
+ var init_flow_persona = __esm({
2212
+ "../contracts/src/flow-persona.ts"() {
2213
+ }
2214
+ });
2215
+
2216
+ // ../contracts/src/question-text.ts
2217
+ function lastSentences(line) {
2218
+ const text = line.trim();
2219
+ if (!text) return [];
2220
+ const sentences = [];
2221
+ let last = 0;
2222
+ for (const m of text.matchAll(SENTENCE_END)) {
2223
+ const end = (m.index ?? 0) + m[0].length;
2224
+ sentences.push(text.slice(last, end));
2225
+ last = end;
2226
+ }
2227
+ if (last < text.length) sentences.push(text.slice(last));
2228
+ return sentences.slice(-2);
2229
+ }
2230
+ function isQuestion(line) {
2231
+ return lastSentences(line).some((s) => QUESTION_MARK.test(s));
2232
+ }
2233
+ var QUESTION_MARK, SENTENCE_END;
2234
+ var init_question_text = __esm({
2235
+ "../contracts/src/question-text.ts"() {
2236
+ QUESTION_MARK = /[??؟]/;
2237
+ SENTENCE_END = /[.!?…。!?؟]+["'”’)\]]*\s*/g;
2238
+ }
2239
+ });
2240
+
2241
+ // ../contracts/src/flow-ask-lines.ts
2242
+ function norm(value) {
2243
+ return typeof value === "string" ? value.trim() : "";
2244
+ }
2245
+ function fnv1a64(text) {
2246
+ let hash = FNV_OFFSET;
2247
+ for (const byte of new TextEncoder().encode(text)) {
2248
+ hash ^= BigInt(byte);
2249
+ hash = BigInt.asUintN(64, hash * FNV_PRIME);
2250
+ }
2251
+ return hash.toString(16).padStart(16, "0");
2252
+ }
2253
+ function askLinesBasis(input) {
2254
+ const canonical = JSON.stringify([
2255
+ ["persona", norm(input.persona)],
2256
+ ["routing", norm(input.routing)],
2257
+ ["language", norm(input.language).toLowerCase()],
2258
+ ["ask", norm(input.ask)],
2259
+ ["instructions", norm(input.instructions)]
2260
+ ]);
2261
+ return fnv1a64(canonical);
2262
+ }
2263
+ function isEnglishLanguage(language) {
2264
+ return norm(language).toLowerCase().startsWith("en");
2265
+ }
2266
+ function readAskGeneratedLines(value) {
2267
+ if (!value || typeof value !== "object") return null;
2268
+ const v = value;
2269
+ if (typeof v.basis !== "string" || !v.basis) return null;
2270
+ const reasks = Array.isArray(v.reasks) ? v.reasks.filter((x) => typeof x === "string" && x.trim() !== "") : [];
2271
+ return {
2272
+ reasks,
2273
+ basis: v.basis,
2274
+ ...typeof v.skipOffer === "string" && v.skipOffer.trim() ? { skipOffer: v.skipOffer } : {},
2275
+ ...typeof v.voicedAsk === "string" && v.voicedAsk.trim() ? { voicedAsk: v.voicedAsk } : {}
2276
+ };
2277
+ }
2278
+ function authorReasksOf(value) {
2279
+ 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) : [];
2280
+ }
2281
+ function defaultAskText(slot, nodeId) {
2282
+ const authored = slot === nodeId ? "" : slot.trim();
2283
+ const label = authored.replace(/[_-]+/g, " ");
2284
+ return label ? `Could you tell me your ${label}?` : "Could you tell me a bit more about what you need?";
2285
+ }
2286
+ var ASK_LINE_MAX_CHARS, ASK_REASKS_MAX, FNV_OFFSET, FNV_PRIME, DEFAULT_ASK_EXHAUSTED_MESSAGE;
2287
+ var init_flow_ask_lines = __esm({
2288
+ "../contracts/src/flow-ask-lines.ts"() {
2289
+ init_question_text();
2290
+ ASK_LINE_MAX_CHARS = 300;
2291
+ ASK_REASKS_MAX = 3;
2292
+ FNV_OFFSET = BigInt("0xcbf29ce484222325");
2293
+ FNV_PRIME = BigInt("0x100000001b3");
2294
+ 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.";
2295
+ }
2296
+ });
2297
+
2298
+ // ../contracts/src/flow-required-inputs.ts
2299
+ function templateFieldRefs(text) {
2300
+ if (typeof text !== "string" || text.indexOf("{{") === -1) return [];
2301
+ const out = [];
2302
+ for (const m of text.matchAll(TOKEN)) {
2303
+ const expr = (m[1] ?? "").trim();
2304
+ const name = expr.startsWith("slots.") ? expr.slice("slots.".length).trim() : expr;
2305
+ if (name && !out.includes(name)) out.push(name);
2306
+ }
2307
+ return out;
2308
+ }
2309
+ function stepInputNames(type, config) {
2310
+ const out = [];
2311
+ const add = (names) => {
2312
+ for (const n of names) if (!out.includes(n)) out.push(n);
2313
+ };
2314
+ if (type === "email") {
2315
+ for (const key of EMAIL_TEMPLATE_KEYS) add(templateFieldRefs(config[key]));
2316
+ return out;
2317
+ }
2318
+ if (type === "tool") {
2319
+ if (typeof config.toolRef === "string" && config.toolRef.trim()) return out;
2320
+ if (config.input && typeof config.input === "object") add(Object.keys(config.input));
2321
+ if (config.headers && typeof config.headers === "object") {
2322
+ for (const v of Object.values(config.headers)) add(templateFieldRefs(v));
2323
+ }
2324
+ }
2325
+ return out;
2326
+ }
2327
+ function requiredInputsFor(type, config, isRequired) {
2328
+ return stepInputNames(type, config).filter((n) => isRequired(n));
2329
+ }
2330
+ function requiredFieldNamesOf(nodes) {
2331
+ const out = /* @__PURE__ */ new Set();
2332
+ for (const node of nodes) {
2333
+ const config = node.config;
2334
+ if (node.type === "ask" && config.required !== false) {
2335
+ out.add((typeof config.slot === "string" && config.slot.trim() ? config.slot : node.id).trim());
2336
+ }
2337
+ if ((node.type === "playbook" || node.type === "gate") && Array.isArray(config.requiredFields)) {
2338
+ for (const f of config.requiredFields) if (typeof f === "string" && f.trim()) out.add(f.trim());
2339
+ }
2340
+ }
2341
+ return out;
2342
+ }
2343
+ var EMAIL_TEMPLATE_KEYS, TOKEN;
2344
+ var init_flow_required_inputs = __esm({
2345
+ "../contracts/src/flow-required-inputs.ts"() {
2346
+ init_flow_ports();
2347
+ EMAIL_TEMPLATE_KEYS = ["to", "subject", "body", "from", "replyTo", "fromName"];
2348
+ TOKEN = /\{\{\s*([^}]+?)\s*\}\}/g;
2090
2349
  }
2091
2350
  });
2092
2351
 
@@ -2107,6 +2366,9 @@ var init_flow_compile = __esm({
2107
2366
  "../contracts/src/flow-compile.ts"() {
2108
2367
  init_flow_enrichment();
2109
2368
  init_flow_edges();
2369
+ init_flow_ports();
2370
+ init_flow_ask_lines();
2371
+ init_flow_required_inputs();
2110
2372
  }
2111
2373
  });
2112
2374
 
@@ -2294,6 +2556,8 @@ var init_flow_validate = __esm({
2294
2556
  init_flow_ports();
2295
2557
  init_flow_edges();
2296
2558
  init_flow_compile_check();
2559
+ init_flow_required_inputs();
2560
+ init_flow_ask_lines();
2297
2561
  init_secret_headers();
2298
2562
  init_network_address();
2299
2563
  init_dispatch_metadata();
@@ -3819,6 +4083,7 @@ var init_monitoring = __esm({
3819
4083
  var WalletRefusalCode, WalletMode, WalletInvoiceStatus, WalletInvoice, WalletEntryKind, WalletEntry, TOPUP_MIN_USD, TOPUP_MAX_USD;
3820
4084
  var init_wallet = __esm({
3821
4085
  "../contracts/src/wallet.ts"() {
4086
+ init_budgets();
3822
4087
  WalletRefusalCode = z.enum([
3823
4088
  // Prepaid balance (or an invoiced credit limit) cannot cover the action.
3824
4089
  "insufficient_balance",
@@ -3828,14 +4093,18 @@ var init_wallet = __esm({
3828
4093
  // still answered. Paying the invoice restores the workspace.
3829
4094
  "account_suspended",
3830
4095
  // The action's reservation already closed (e.g. a call ended or expired) — it cannot be re-admitted.
3831
- "hold_closed"
4096
+ "hold_closed",
4097
+ // A hard budget (docs/specs/monetization LLD §8a) would be passed: new actions in its scope wait for its reset.
4098
+ "budget_exceeded"
3832
4099
  ]);
3833
4100
  z.object({
3834
4101
  error: WalletRefusalCode,
3835
4102
  code: WalletRefusalCode,
3836
4103
  availableUsd: z.string(),
3837
4104
  needUsd: z.string(),
3838
- message: z.string()
4105
+ message: z.string(),
4106
+ // budget_exceeded only: which budget, and when it resets.
4107
+ budget: BudgetRefusalDetail.optional()
3839
4108
  });
3840
4109
  WalletMode = z.enum(["prepaid", "invoiced"]);
3841
4110
  z.object({
@@ -3876,7 +4145,7 @@ var init_wallet = __esm({
3876
4145
  payUrl: z.string().nullable()
3877
4146
  });
3878
4147
  z.object({ invoices: z.array(WalletInvoice) });
3879
- WalletEntryKind = z.enum(["grant", "topup", "usage", "refund", "refund_reversal", "dispute", "dispute_reinstated", "adjustment", "invoice_payment"]);
4148
+ WalletEntryKind = z.enum(["grant", "topup", "usage", "usage_correction", "refund", "refund_reversal", "dispute", "dispute_reinstated", "adjustment", "invoice_payment"]);
3880
4149
  WalletEntry = z.object({
3881
4150
  id: z.string(),
3882
4151
  kind: WalletEntryKind,
@@ -3920,6 +4189,81 @@ var init_pipeline_keys = __esm({
3920
4189
  });
3921
4190
  }
3922
4191
  });
4192
+ var METERING_SCHEMA_VERSION, METER_IDS, MeterId, KEY_OWNERS, READING_SOURCES, SUBJECT_KINDS, SubjectKind, MeteringSubject, DimValue, IdentityDims, Attributes, SERIES_PATTERN, SeriesName, Quantity, MeterReadingInput;
4193
+ var init_metering = __esm({
4194
+ "../contracts/src/metering.ts"() {
4195
+ METERING_SCHEMA_VERSION = 2;
4196
+ METER_IDS = [
4197
+ // A model's prompt tokens as the provider counts them (cached ones included); `cached_input_tokens` is the cached
4198
+ // subset and `reasoning_tokens` the reasoning subset of `output_tokens`. Rating prices the totals until it prices the
4199
+ // subsets on their own (monetization MON-14).
4200
+ "llm.input_tokens",
4201
+ "llm.cached_input_tokens",
4202
+ "llm.output_tokens",
4203
+ "llm.reasoning_tokens",
4204
+ "embedding.input_tokens",
4205
+ "realtime.audio_input_tokens",
4206
+ "realtime.audio_output_tokens",
4207
+ "realtime.text_input_tokens",
4208
+ "realtime.cached_input_tokens",
4209
+ "realtime.text_output_tokens",
4210
+ "stt.billed_seconds",
4211
+ "tts.characters",
4212
+ "call.connected_seconds",
4213
+ "telephony.carrier_seconds",
4214
+ "media.participant_seconds",
4215
+ "media.sip_seconds",
4216
+ "recording.seconds",
4217
+ "storage.byte_days",
4218
+ "message.segments",
4219
+ "email.messages",
4220
+ // One per billable text action — a text turn, or a single model action the API takes for a workspace (a QA review,
4221
+ // an evaluation, a planner call, a knowledge ingest): the per-message fee's basis.
4222
+ "text.turns",
4223
+ // Informational: number rental is a recurring fee on charge_cycles (🔒 B-21).
4224
+ "number.held_days",
4225
+ "provider_fee.micros"
4226
+ ];
4227
+ MeterId = z.enum(METER_IDS);
4228
+ KEY_OWNERS = ["platform", "workspace_key", "workspace_plan", "workspace_brain", "self_hosted"];
4229
+ z.enum(KEY_OWNERS);
4230
+ READING_SOURCES = ["server_observed", "platform_wire", "platform_estimate", "customer_report"];
4231
+ z.enum(READING_SOURCES);
4232
+ SUBJECT_KINDS = ["call", "turn", "job", "message", "email", "number"];
4233
+ SubjectKind = z.enum(SUBJECT_KINDS);
4234
+ MeteringSubject = z.object({ kind: SubjectKind, id: z.string().min(1).max(200) });
4235
+ DimValue = z.string().max(200).nullable();
4236
+ IdentityDims = z.record(z.string(), DimValue);
4237
+ Attributes = z.object({
4238
+ served_model: z.string().max(200).nullable(),
4239
+ channel: z.string().max(64).nullable(),
4240
+ direction: z.string().max(32).nullable(),
4241
+ leg_kind: z.string().max(64).nullable(),
4242
+ country: z.string().max(8).nullable(),
4243
+ worker_owner: z.enum(["platform", "self_hosted", "unknown"]).nullable()
4244
+ });
4245
+ SERIES_PATTERN = /^(req|ws|srv):[A-Za-z0-9._:-]{1,60}$/;
4246
+ SeriesName = z.string().max(64).regex(SERIES_PATTERN);
4247
+ Quantity = z.string().regex(/^\d{1,14}(\.\d{1,6})?$/);
4248
+ MeterReadingInput = z.object({
4249
+ schemaVersion: z.literal(METERING_SCHEMA_VERSION),
4250
+ subject: MeteringSubject,
4251
+ meter: MeterId,
4252
+ identity: IdentityDims,
4253
+ series: SeriesName,
4254
+ quantity: Quantity,
4255
+ final: z.boolean(),
4256
+ void: z.literal(true).optional(),
4257
+ // The pre-send input estimate of a request that may never answer (LLD §4.1). A client never names a source: the
4258
+ // server derives it from the principal (§4.4), and this flag is the one thing only the producer knows. Honoured
4259
+ // only from the call's own worker token; it ranks below the request's wire count, which supersedes it.
4260
+ estimate: z.literal(true).optional(),
4261
+ occurredAt: z.string().datetime({ offset: true }),
4262
+ attributes: Attributes.partial().optional()
4263
+ }).strict();
4264
+ z.object({ readings: z.array(MeterReadingInput).min(1).max(200) }).strict();
4265
+ }
4266
+ });
3923
4267
 
3924
4268
  // ../contracts/src/index.ts
3925
4269
  var init_src = __esm({
@@ -3956,6 +4300,10 @@ var init_src = __esm({
3956
4300
  init_flow();
3957
4301
  init_flow_program();
3958
4302
  init_flow_ports();
4303
+ init_flow_persona();
4304
+ init_flow_ask_lines();
4305
+ init_flow_required_inputs();
4306
+ init_question_text();
3959
4307
  init_flow_validate();
3960
4308
  init_flow_builtins();
3961
4309
  init_compliance();
@@ -3984,16 +4332,73 @@ var init_src = __esm({
3984
4332
  init_channel_connector();
3985
4333
  init_monitoring();
3986
4334
  init_wallet();
4335
+ init_budgets();
3987
4336
  init_network_address();
3988
4337
  init_dispatch_metadata();
3989
4338
  init_pipeline_keys();
4339
+ init_metering();
4340
+ }
4341
+ });
4342
+
4343
+ // src/runtime/graph/graph-model.ts
4344
+ function buildGraphModel(program) {
4345
+ return {
4346
+ // the program's completion gate is the compiler's own subset of these (a gate's fields, else the required asks') —
4347
+ // read too, so a program carries one answer however it was produced
4348
+ requiredFields: /* @__PURE__ */ new Set([
4349
+ ...requiredFieldNamesOf(Object.values(program.nodes)),
4350
+ ...program.completionGate.requiredFields.map((f) => f.trim())
4351
+ ]),
4352
+ entry: program.entry,
4353
+ node: (id) => program.nodes[id],
4354
+ out: (id) => program.nodes[id]?.out ?? [],
4355
+ selectByPort: (id, port) => {
4356
+ const edges = program.nodes[id]?.out ?? [];
4357
+ const byHandle = edges.find((e) => e.sourceHandle === port);
4358
+ if (byHandle) return byHandle.to;
4359
+ const byLabel = edges.find((e) => e.label === port);
4360
+ return byLabel ? byLabel.to : null;
4361
+ },
4362
+ askNodeForSlot: (slot) => {
4363
+ const want = slot.trim();
4364
+ for (const node of Object.values(program.nodes)) {
4365
+ if (node.type !== "ask") continue;
4366
+ const s = typeof node.config.slot === "string" ? node.config.slot : node.id;
4367
+ if (s.trim() === want) return node.id;
4368
+ }
4369
+ return void 0;
4370
+ }
4371
+ };
4372
+ }
4373
+ var init_graph_model = __esm({
4374
+ "src/runtime/graph/graph-model.ts"() {
4375
+ init_src();
4376
+ }
4377
+ });
4378
+
4379
+ // src/runtime/graph/reask-templates.ts
4380
+ function templateReasks(ask) {
4381
+ const q = ask.trim();
4382
+ return [`${NOT_CAUGHT} ${q}`, `Let me ask that another way \u2014 ${q}`, `Just so I get it right: ${q}`];
4383
+ }
4384
+ function notCaught(line) {
4385
+ return `${NOT_CAUGHT} ${line.trim()}`;
4386
+ }
4387
+ function skipOfferTemplate(label) {
4388
+ const what = label.trim() ? `your ${label.trim()}` : "that detail";
4389
+ return `No problem if you don't have ${what} handy \u2014 shall we skip it for now?`;
4390
+ }
4391
+ var NOT_CAUGHT;
4392
+ var init_reask_templates = __esm({
4393
+ "src/runtime/graph/reask-templates.ts"() {
4394
+ NOT_CAUGHT = "Sorry, I didn't catch that.";
3990
4395
  }
3991
4396
  });
3992
4397
 
3993
4398
  // src/runtime/graph/interpolate.ts
3994
4399
  function interpolate(template, scope2) {
3995
4400
  if (!template || template.indexOf("{{") === -1) return template;
3996
- return template.replace(TOKEN, (_match, expr) => {
4401
+ return template.replace(TOKEN2, (_match, expr) => {
3997
4402
  const value = resolvePath(scope2, expr.trim());
3998
4403
  return value === void 0 || value === null ? "" : String(value);
3999
4404
  });
@@ -4018,10 +4423,10 @@ function hasSlot(slots, name) {
4018
4423
  const v = resolvePath(slots, name.trim());
4019
4424
  return v !== void 0 && v !== null && v !== "";
4020
4425
  }
4021
- var TOKEN;
4426
+ var TOKEN2;
4022
4427
  var init_interpolate = __esm({
4023
4428
  "src/runtime/graph/interpolate.ts"() {
4024
- TOKEN = /\{\{\s*([^}]+?)\s*\}\}/g;
4429
+ TOKEN2 = /\{\{\s*([^}]+?)\s*\}\}/g;
4025
4430
  }
4026
4431
  });
4027
4432
 
@@ -4076,32 +4481,116 @@ function acceptsReasoningEffort(model2) {
4076
4481
  const id = baseModelId(model2);
4077
4482
  return isReasoningModel(model2) && !/^o1-(mini|preview)/.test(id) && !/-chat(-|$)/.test(id);
4078
4483
  }
4484
+ function acceptsNoReasoningEffort(model2) {
4485
+ if (!acceptsReasoningEffort(model2))
4486
+ return false;
4487
+ const id = baseModelId(model2);
4488
+ if (/-pro\b/.test(id))
4489
+ return false;
4490
+ const gpt = /^gpt-(\d+)(?:\.(\d+))?/.exec(id);
4491
+ if (!gpt)
4492
+ return false;
4493
+ const major = Number(gpt[1]);
4494
+ const minor = gpt[2] !== void 0 ? Number(gpt[2]) : 0;
4495
+ return major > 5 || major === 5 && minor >= 1;
4496
+ }
4497
+ function forgetLearnedReasoningEfforts() {
4498
+ learnedEfforts.clear();
4499
+ }
4500
+ function chatReasoningEffort(model2, opts) {
4501
+ const learned = learnedEfforts.get(learnedKey(model2, opts.tools === true));
4502
+ if (learned !== void 0)
4503
+ return learned === "omit" ? void 0 : learned;
4504
+ if (!acceptsReasoningEffort(model2))
4505
+ return void 0;
4506
+ if (opts.tools === true && acceptsNoReasoningEffort(model2))
4507
+ return "none";
4508
+ return opts.requested;
4509
+ }
4079
4510
  function chatCompletionParams(model2, input) {
4511
+ const effort = chatReasoningEffort(model2, {
4512
+ ...input.tools !== void 0 ? { tools: input.tools } : {},
4513
+ ...input.reasoningEffort !== void 0 ? { requested: input.reasoningEffort } : {}
4514
+ });
4080
4515
  if (isReasoningModel(model2)) {
4081
4516
  return {
4082
4517
  ...input.maxTokens !== void 0 ? { max_completion_tokens: Math.max(input.maxTokens, REASONING_MIN_COMPLETION_TOKENS) } : {},
4083
- ...input.reasoningEffort !== void 0 && acceptsReasoningEffort(model2) ? { reasoning_effort: input.reasoningEffort } : {}
4518
+ ...effort !== void 0 ? { reasoning_effort: effort } : {}
4084
4519
  };
4085
4520
  }
4086
4521
  return {
4522
+ // only when a provider taught the process that this model (one we don't know as a reasoning model) takes an effort
4523
+ ...effort !== void 0 ? { reasoning_effort: effort } : {},
4087
4524
  ...input.maxTokens !== void 0 ? { max_tokens: input.maxTokens } : {},
4088
4525
  ...input.temperature !== void 0 ? { temperature: input.temperature } : {},
4089
4526
  ...input.topP !== void 0 ? { top_p: input.topP } : {}
4090
4527
  };
4091
4528
  }
4092
- function modelRejectionOf(err) {
4529
+ function acceptsSamplingParams(model2) {
4530
+ return !isReasoningModel(model2);
4531
+ }
4532
+ function providerErrorOf(err) {
4533
+ if (err === null || typeof err !== "object")
4534
+ return null;
4093
4535
  const e = err;
4094
- const status = typeof e?.status === "number" ? e.status : null;
4095
- if (status !== 400 && status !== 403 && status !== 404)
4536
+ const body = e["body"] !== null && typeof e["body"] === "object" ? e["body"] : {};
4537
+ const pick = (k) => e[k] !== void 0 && e[k] !== null ? e[k] : body[k];
4538
+ const statusRaw = typeof e["status"] === "number" ? e["status"] : e["statusCode"];
4539
+ const status = typeof statusRaw === "number" && statusRaw > 0 ? statusRaw : null;
4540
+ const code = pick("code");
4541
+ const type = pick("type");
4542
+ const param = pick("param");
4543
+ const message = typeof body["message"] === "string" ? body["message"] : typeof e["message"] === "string" ? e["message"] : "";
4544
+ return {
4545
+ status,
4546
+ code: typeof code === "string" ? code : typeof type === "string" ? type : "",
4547
+ param: typeof param === "string" ? param : null,
4548
+ message
4549
+ };
4550
+ }
4551
+ function reasoningEffortRejectionOf(err) {
4552
+ const f = providerErrorOf(err);
4553
+ if (!f || f.status !== 400)
4096
4554
  return null;
4097
- const code = typeof e?.code === "string" ? e.code : typeof e?.type === "string" ? e.type : "";
4098
- const param = typeof e?.param === "string" ? e.param : null;
4099
- const rejected = REJECTION_CODES.has(code) || param !== null && MODEL_PARAMS.has(param);
4555
+ if (f.param !== "reasoning_effort" && !/reasoning[_ ]effort/i.test(f.message))
4556
+ return null;
4557
+ return { status: f.status, message: f.message, suggestsNone: /reasoning[_ ]effort[^.]*\bnone\b/i.test(f.message) };
4558
+ }
4559
+ function learnReasoningEffort(model2, shape, err) {
4560
+ const rejection = reasoningEffortRejectionOf(err);
4561
+ if (!rejection)
4562
+ return false;
4563
+ const next = shape.sent !== "none" && (rejection.suggestsNone || shape.sent === void 0) ? "none" : "omit";
4564
+ if ((next === "omit" ? void 0 : next) === shape.sent)
4565
+ return false;
4566
+ learnedEfforts.set(learnedKey(model2, shape.tools), next);
4567
+ return true;
4568
+ }
4569
+ async function withReasoningEffortRetry(shape, call) {
4570
+ const sent = chatReasoningEffort(shape.model, {
4571
+ tools: shape.tools,
4572
+ ...shape.requested !== void 0 ? { requested: shape.requested } : {}
4573
+ });
4574
+ try {
4575
+ return await call();
4576
+ } catch (err) {
4577
+ if (!learnReasoningEffort(shape.model, { tools: shape.tools, sent }, err))
4578
+ throw err;
4579
+ return call();
4580
+ }
4581
+ }
4582
+ function modelRejectionOf(err) {
4583
+ const f = providerErrorOf(err);
4584
+ const status = f?.status ?? null;
4585
+ if (!f || status === null || status !== 400 && status !== 403 && status !== 404)
4586
+ return null;
4587
+ const { code, param } = f;
4588
+ const rejected = REJECTION_CODES.has(code) || param !== null && MODEL_PARAMS.has(param) || reasoningEffortRejectionOf(err) !== null;
4100
4589
  if (!rejected)
4101
4590
  return null;
4102
4591
  if (status === 403 && code !== "model_not_found")
4103
4592
  return null;
4104
- return { status, code: code || "invalid_request_error", message: typeof e?.message === "string" ? e.message : "" };
4593
+ return { status, code: code || "invalid_request_error", message: f.message };
4105
4594
  }
4106
4595
  async function withModelFallback(opts) {
4107
4596
  const fallbackModel = opts.fallbackModel ?? PLATFORM_DEFAULT_CHAT_MODEL;
@@ -4122,11 +4611,13 @@ async function withModelFallback(opts) {
4122
4611
  return opts.call(fallbackModel);
4123
4612
  }
4124
4613
  }
4125
- var PLATFORM_DEFAULT_CHAT_MODEL, REASONING_MIN_COMPLETION_TOKENS, REJECTION_CODES, MODEL_PARAMS, MODEL_REJECTION_TTL_MS, ModelRejectionCache;
4614
+ var PLATFORM_DEFAULT_CHAT_MODEL, REASONING_MIN_COMPLETION_TOKENS, learnedEfforts, learnedKey, REJECTION_CODES, MODEL_PARAMS, MODEL_REJECTION_TTL_MS, ModelRejectionCache;
4126
4615
  var init_chat_params = __esm({
4127
4616
  "../llm-client/dist/chat-params.js"() {
4128
4617
  PLATFORM_DEFAULT_CHAT_MODEL = "gpt-4o-mini";
4129
4618
  REASONING_MIN_COMPLETION_TOKENS = 2048;
4619
+ learnedEfforts = /* @__PURE__ */ new Map();
4620
+ learnedKey = (model2, tools) => `${baseModelId(model2)}|${tools ? "tools" : "plain"}`;
4130
4621
  REJECTION_CODES = /* @__PURE__ */ new Set(["unsupported_parameter", "unsupported_value", "model_not_found"]);
4131
4622
  MODEL_PARAMS = /* @__PURE__ */ new Set(["model", "max_tokens", "max_completion_tokens", "temperature", "top_p", "reasoning_effort"]);
4132
4623
  MODEL_REJECTION_TTL_MS = 5 * 60 * 1e3;
@@ -4154,6 +4645,30 @@ var init_chat_params = __esm({
4154
4645
  };
4155
4646
  }
4156
4647
  });
4648
+
4649
+ // ../llm-client/dist/index.js
4650
+ var dist_exports = {};
4651
+ __export(dist_exports, {
4652
+ MODEL_REJECTION_TTL_MS: () => MODEL_REJECTION_TTL_MS,
4653
+ ModelRejectionCache: () => ModelRejectionCache,
4654
+ PLATFORM_DEFAULT_CHAT_MODEL: () => PLATFORM_DEFAULT_CHAT_MODEL,
4655
+ REASONING_MIN_COMPLETION_TOKENS: () => REASONING_MIN_COMPLETION_TOKENS,
4656
+ acceptsNoReasoningEffort: () => acceptsNoReasoningEffort,
4657
+ acceptsReasoningEffort: () => acceptsReasoningEffort,
4658
+ acceptsSamplingParams: () => acceptsSamplingParams,
4659
+ chatCompletionParams: () => chatCompletionParams,
4660
+ chatReasoningEffort: () => chatReasoningEffort,
4661
+ createChatClient: () => createChatClient,
4662
+ forgetLearnedReasoningEfforts: () => forgetLearnedReasoningEfforts,
4663
+ hasChatKey: () => hasChatKey,
4664
+ isReasoningModel: () => isReasoningModel,
4665
+ learnReasoningEffort: () => learnReasoningEffort,
4666
+ modelRejectionOf: () => modelRejectionOf,
4667
+ providerErrorOf: () => providerErrorOf,
4668
+ reasoningEffortRejectionOf: () => reasoningEffortRejectionOf,
4669
+ withModelFallback: () => withModelFallback,
4670
+ withReasoningEffortRetry: () => withReasoningEffortRetry
4671
+ });
4157
4672
  function createChatClient(config = {}) {
4158
4673
  return new OpenAI({
4159
4674
  ...config.apiKey ? { apiKey: config.apiKey } : {},
@@ -4161,6 +4676,9 @@ function createChatClient(config = {}) {
4161
4676
  ...config.timeoutMs ? { timeout: config.timeoutMs } : {}
4162
4677
  });
4163
4678
  }
4679
+ function hasChatKey(config = {}) {
4680
+ return Boolean(config.apiKey || process.env["OPENAI_API_KEY"]);
4681
+ }
4164
4682
  var init_dist = __esm({
4165
4683
  "../llm-client/dist/index.js"() {
4166
4684
  init_chat_params();
@@ -4300,6 +4818,340 @@ var init_wrap = __esm({
4300
4818
  "src/providers/wrap.ts"() {
4301
4819
  }
4302
4820
  });
4821
+ var init_start = __esm({
4822
+ "../observability/dist/start.js"() {
4823
+ }
4824
+ });
4825
+
4826
+ // ../observability/dist/attributes.js
4827
+ var ATTR;
4828
+ var init_attributes = __esm({
4829
+ "../observability/dist/attributes.js"() {
4830
+ ATTR = {
4831
+ projectId: "vl.project_id",
4832
+ callId: "vl.call_id",
4833
+ campaignId: "vl.campaign_id",
4834
+ room: "vl.room",
4835
+ agentId: "vl.agent_id",
4836
+ phoneNumberId: "vl.phone_number_id",
4837
+ bindingId: "vl.binding_id",
4838
+ source: "vl.source",
4839
+ kind: "vl.kind"
4840
+ };
4841
+ }
4842
+ });
4843
+ function getCurrentCallContext() {
4844
+ return callContextStore.getStore();
4845
+ }
4846
+ var callContextStore;
4847
+ var init_call_context = __esm({
4848
+ "../observability/dist/call-context.js"() {
4849
+ init_attributes();
4850
+ callContextStore = new AsyncLocalStorage();
4851
+ }
4852
+ });
4853
+ var init_trace_propagation = __esm({
4854
+ "../observability/dist/trace-propagation.js"() {
4855
+ }
4856
+ });
4857
+ function meter() {
4858
+ return metrics.getMeter(METER_NAME, METER_VERSION);
4859
+ }
4860
+ function modelFallbacks() {
4861
+ return _modelFallbacks ??= meter().createCounter("vl.llm.model_fallback", {
4862
+ description: "Model calls the provider rejected that fell back to the platform default model",
4863
+ unit: "{fallbacks}"
4864
+ });
4865
+ }
4866
+ function recordModelFallback(args) {
4867
+ const out = {
4868
+ "vl.model": args.model,
4869
+ "vl.fallback_model": args.fallbackModel,
4870
+ "vl.error_code": args.code
4871
+ };
4872
+ if (args.projectId)
4873
+ out[ATTR.projectId] = args.projectId;
4874
+ if (args.agentId)
4875
+ out[ATTR.agentId] = args.agentId;
4876
+ if (args.surface)
4877
+ out["vl.surface"] = args.surface;
4878
+ modelFallbacks().add(1, out);
4879
+ }
4880
+ var METER_NAME, METER_VERSION, _modelFallbacks;
4881
+ var init_metrics = __esm({
4882
+ "../observability/dist/metrics.js"() {
4883
+ init_attributes();
4884
+ METER_NAME = "voicelayer";
4885
+ METER_VERSION = "0.1.0";
4886
+ _modelFallbacks = null;
4887
+ }
4888
+ });
4889
+ var init_latency_span = __esm({
4890
+ "../observability/dist/latency-span.js"() {
4891
+ init_attributes();
4892
+ }
4893
+ });
4894
+
4895
+ // ../observability/dist/index.js
4896
+ var init_dist2 = __esm({
4897
+ "../observability/dist/index.js"() {
4898
+ init_start();
4899
+ init_call_context();
4900
+ init_trace_propagation();
4901
+ init_attributes();
4902
+ init_metrics();
4903
+ init_latency_span();
4904
+ }
4905
+ });
4906
+
4907
+ // src/providers/resilient-llm.ts
4908
+ var resilient_llm_exports = {};
4909
+ __export(resilient_llm_exports, {
4910
+ LLM_APOLOGY: () => LLM_APOLOGY,
4911
+ ResilientLLM: () => ResilientLLM,
4912
+ isResilientLLM: () => isResilientLLM
4913
+ });
4914
+ function isResilientLLM(value) {
4915
+ return typeof value === "object" && value !== null && value[RESILIENT] === true;
4916
+ }
4917
+ function transient(error) {
4918
+ if (error instanceof APIStatusError) {
4919
+ const s = error.statusCode;
4920
+ return s === 408 || s === 429 || s < 0 || s >= 500;
4921
+ }
4922
+ return error instanceof APITimeoutError || error instanceof APIConnectionError;
4923
+ }
4924
+ var LLM_APOLOGY, RESILIENT, ResilientLLM, sameModel, ServedLLM, ResilientLLMStream;
4925
+ var init_resilient_llm = __esm({
4926
+ "src/providers/resilient-llm.ts"() {
4927
+ init_dist();
4928
+ init_dist2();
4929
+ LLM_APOLOGY = "Sorry, I'm having trouble right now. Could you give me a moment and say that again?";
4930
+ RESILIENT = /* @__PURE__ */ Symbol.for("voicelayer.resilientLLM");
4931
+ ResilientLLM = class extends llm.LLM {
4932
+ [RESILIENT] = true;
4933
+ #opts;
4934
+ #fallback = null;
4935
+ #listeners = /* @__PURE__ */ new Set();
4936
+ /** Models the provider rejected on this call (a call's pipeline is built per call): later turns go straight to the fallback. */
4937
+ rejections = new ModelRejectionCache();
4938
+ constructor(opts) {
4939
+ super();
4940
+ this.#opts = opts;
4941
+ }
4942
+ label() {
4943
+ return this.#opts.primary.label();
4944
+ }
4945
+ get model() {
4946
+ return this.#opts.primary.model;
4947
+ }
4948
+ get provider() {
4949
+ return this.#opts.primary.provider;
4950
+ }
4951
+ get apology() {
4952
+ return this.#opts.apology ?? LLM_APOLOGY;
4953
+ }
4954
+ get reasoningParams() {
4955
+ return this.#opts.reasoningParams === true;
4956
+ }
4957
+ /** The fallback LLM, built on first need; null when there is none or it is the agent's own model. */
4958
+ fallbackLLM() {
4959
+ if (!this.#opts.fallback) return null;
4960
+ this.#fallback ??= this.#opts.fallback();
4961
+ return sameModel(this.#fallback.model, this.model) ? null : this.#fallback;
4962
+ }
4963
+ /** Hear about every recovery (and every apology). Returns the unsubscribe. */
4964
+ onIncident(listener) {
4965
+ this.#listeners.add(listener);
4966
+ return () => this.#listeners.delete(listener);
4967
+ }
4968
+ /** @internal */
4969
+ report(incident) {
4970
+ const log = incident.kind === "apology" ? console.error : console.warn;
4971
+ log(`[voicelayer] llm ${incident.kind.replace(/_/g, " ")}`, incident);
4972
+ if (incident.kind === "model_fallback") {
4973
+ const call = getCurrentCallContext();
4974
+ recordModelFallback({
4975
+ model: incident.model,
4976
+ fallbackModel: incident.fallbackModel,
4977
+ code: incident.code,
4978
+ surface: "voice",
4979
+ ...call?.projectId ? { projectId: call.projectId } : {},
4980
+ ...call?.agentId ? { agentId: call.agentId } : {}
4981
+ });
4982
+ }
4983
+ for (const listener of this.#listeners) {
4984
+ try {
4985
+ listener(incident);
4986
+ } catch {
4987
+ }
4988
+ }
4989
+ }
4990
+ chat(args) {
4991
+ return new ResilientLLMStream(this, args);
4992
+ }
4993
+ prewarm() {
4994
+ this.#opts.primary.prewarm();
4995
+ }
4996
+ async aclose() {
4997
+ await Promise.all([this.#opts.primary.aclose(), this.#fallback?.aclose()]);
4998
+ }
4999
+ /** @internal */
5000
+ get primary() {
5001
+ return this.#opts.primary;
5002
+ }
5003
+ };
5004
+ sameModel = (a, b) => a.trim().toLowerCase() === b.trim().toLowerCase();
5005
+ ServedLLM = class extends llm.LLM {
5006
+ constructor(owner, served) {
5007
+ super();
5008
+ this.owner = owner;
5009
+ this.served = served;
5010
+ }
5011
+ owner;
5012
+ served;
5013
+ label() {
5014
+ return this.owner.label();
5015
+ }
5016
+ get model() {
5017
+ return this.served();
5018
+ }
5019
+ get provider() {
5020
+ return this.owner.provider;
5021
+ }
5022
+ chat(args) {
5023
+ return this.owner.chat(args);
5024
+ }
5025
+ emit(event, ...args) {
5026
+ return this.owner.emit(event, ...args);
5027
+ }
5028
+ };
5029
+ ResilientLLMStream = class extends llm.LLMStream {
5030
+ #owner;
5031
+ #args;
5032
+ #conn;
5033
+ /** The model answering this stream — what its metrics report. */
5034
+ #served;
5035
+ #current = null;
5036
+ constructor(owner, args) {
5037
+ const conn = args.connOptions ?? DEFAULT_API_CONNECT_OPTIONS;
5038
+ const served = { model: owner.model };
5039
+ super(new ServedLLM(owner, () => served.model), {
5040
+ chatCtx: args.chatCtx,
5041
+ ...args.toolCtx ? { toolCtx: args.toolCtx } : {},
5042
+ connOptions: { ...conn, maxRetry: 0 }
5043
+ });
5044
+ this.#owner = owner;
5045
+ this.#args = args;
5046
+ this.#conn = conn;
5047
+ this.#served = served;
5048
+ this.abortController.signal.addEventListener("abort", () => this.#current?.close());
5049
+ }
5050
+ get #hasTools() {
5051
+ return this.#args.toolCtx !== void 0 && Object.keys(this.#args.toolCtx).length > 0;
5052
+ }
5053
+ #extraKwargs(model2) {
5054
+ const base = this.#args.extraKwargs;
5055
+ if (!this.#owner.reasoningParams) return base;
5056
+ const effort = chatReasoningEffort(model2, { tools: this.#hasTools });
5057
+ if (effort === void 0) {
5058
+ if (!base || !("reasoning_effort" in base)) return base;
5059
+ const { reasoning_effort: _dropped, ...rest } = base;
5060
+ return rest;
5061
+ }
5062
+ return { ...base, reasoning_effort: effort };
5063
+ }
5064
+ /** One request on `target`, forwarding its chunks. Its failure is the inner LLM's 'error' event. */
5065
+ async #attempt(target) {
5066
+ let failure = null;
5067
+ const onError = (ev) => {
5068
+ failure ??= ev.error;
5069
+ };
5070
+ target.on("error", onError);
5071
+ let started = false;
5072
+ try {
5073
+ const extraKwargs = this.#extraKwargs(target.model);
5074
+ const stream = target.chat({
5075
+ ...this.#args,
5076
+ connOptions: { ...this.#conn, maxRetry: 0 },
5077
+ ...extraKwargs !== void 0 ? { extraKwargs } : {}
5078
+ });
5079
+ this.#current = stream;
5080
+ for await (const chunk of stream) {
5081
+ if (this.abortController.signal.aborted) break;
5082
+ started = true;
5083
+ this.queue.put(chunk);
5084
+ }
5085
+ } catch (err) {
5086
+ failure ??= err instanceof Error ? err : new Error(String(err));
5087
+ } finally {
5088
+ target.off("error", onError);
5089
+ this.#current = null;
5090
+ }
5091
+ return failure ? { ok: false, error: failure, started } : { ok: true };
5092
+ }
5093
+ /** Run `target` until it answers, or a failure that retrying it won't fix. */
5094
+ async #run(target) {
5095
+ let effortRetried = false;
5096
+ for (let retries = 0; ; ) {
5097
+ const sent = this.#owner.reasoningParams ? chatReasoningEffort(target.model, { tools: this.#hasTools }) : void 0;
5098
+ const result = await this.#attempt(target);
5099
+ if (result.ok || result.started || this.abortController.signal.aborted) return result;
5100
+ if (this.#owner.reasoningParams && !effortRetried && learnReasoningEffort(target.model, { tools: this.#hasTools, sent }, result.error)) {
5101
+ effortRetried = true;
5102
+ this.#owner.report({
5103
+ kind: "reasoning_effort_adapted",
5104
+ model: target.model,
5105
+ message: providerErrorOf(result.error)?.message ?? result.error.message
5106
+ });
5107
+ continue;
5108
+ }
5109
+ if (!transient(result.error) || retries >= this.#conn.maxRetry) return result;
5110
+ const wait = intervalForRetry(this.#conn, retries);
5111
+ retries += 1;
5112
+ if (wait > 0) await new Promise((r) => setTimeout(r, wait));
5113
+ if (this.abortController.signal.aborted) return result;
5114
+ }
5115
+ }
5116
+ async run() {
5117
+ const owner = this.#owner;
5118
+ const model2 = owner.model;
5119
+ const fallback = owner.fallbackLLM();
5120
+ const known = fallback ? owner.rejections.get(model2) : null;
5121
+ let failure;
5122
+ if (known && fallback) {
5123
+ owner.report({ kind: "model_fallback", model: model2, fallbackModel: fallback.model, code: known.code, message: known.message, cached: true });
5124
+ failure = new Error(known.message);
5125
+ } else {
5126
+ const first = await this.#run(owner.primary);
5127
+ if (first.ok || first.started || this.abortController.signal.aborted) return;
5128
+ failure = first.error;
5129
+ const rejection = modelRejectionOf(first.error);
5130
+ if (rejection) owner.rejections.set(model2, rejection);
5131
+ if (fallback) {
5132
+ const f = providerErrorOf(first.error);
5133
+ owner.report({
5134
+ kind: "model_fallback",
5135
+ model: model2,
5136
+ fallbackModel: fallback.model,
5137
+ code: rejection?.code ?? (f?.status ? String(f.status) : "error"),
5138
+ message: f?.message || first.error.message,
5139
+ cached: false
5140
+ });
5141
+ }
5142
+ }
5143
+ if (fallback) {
5144
+ this.#served.model = fallback.model;
5145
+ const second = await this.#run(fallback);
5146
+ if (second.ok || second.started || this.abortController.signal.aborted) return;
5147
+ failure = second.error;
5148
+ }
5149
+ owner.report({ kind: "apology", model: this.#served.model, message: providerErrorOf(failure)?.message || failure.message });
5150
+ this.queue.put({ id: `vl-apology-${Date.now()}`, delta: { role: "assistant", content: owner.apology } });
5151
+ }
5152
+ };
5153
+ }
5154
+ });
4303
5155
 
4304
5156
  // src/providers/index.ts
4305
5157
  async function importOptional(spec, hint) {
@@ -4346,9 +5198,13 @@ var init_providers2 = __esm({
4346
5198
  llm(options = {}) {
4347
5199
  return async () => {
4348
5200
  const oa = await importOptional("@voicelayer/agents-plugin-openai", "openai.llm");
4349
- return new oa.LLM(
4350
- withCreds({ model: options.model ?? "gpt-4o-mini" }, options)
4351
- );
5201
+ const { ResilientLLM: ResilientLLM2 } = await Promise.resolve().then(() => (init_resilient_llm(), resilient_llm_exports));
5202
+ const { PLATFORM_DEFAULT_CHAT_MODEL: PLATFORM_DEFAULT_CHAT_MODEL2 } = await Promise.resolve().then(() => (init_dist(), dist_exports));
5203
+ return new ResilientLLM2({
5204
+ primary: new oa.LLM(withCreds({ model: options.model ?? "gpt-4o-mini" }, options)),
5205
+ fallback: () => new oa.LLM(withCreds({ model: PLATFORM_DEFAULT_CHAT_MODEL2 }, options)),
5206
+ reasoningParams: true
5207
+ });
4352
5208
  };
4353
5209
  },
4354
5210
  tts(options = {}) {
@@ -4424,9 +5280,10 @@ var init_providers2 = __esm({
4424
5280
  llm(options = {}) {
4425
5281
  return async () => {
4426
5282
  const g = await importOptional("@voicelayer/agents-plugin-google", "google.llm");
4427
- return new g.LLM(
4428
- withCreds({ model: options.model ?? "gemini-3.8-flash" }, options)
4429
- );
5283
+ const { ResilientLLM: ResilientLLM2 } = await Promise.resolve().then(() => (init_resilient_llm(), resilient_llm_exports));
5284
+ return new ResilientLLM2({
5285
+ primary: new g.LLM(withCreds({ model: options.model ?? "gemini-3.8-flash" }, options))
5286
+ });
4430
5287
  };
4431
5288
  },
4432
5289
  realtime(options = {}) {
@@ -4528,91 +5385,6 @@ var init_llm = __esm({
4528
5385
  init_registry();
4529
5386
  }
4530
5387
  });
4531
- var init_start = __esm({
4532
- "../observability/dist/start.js"() {
4533
- }
4534
- });
4535
-
4536
- // ../observability/dist/attributes.js
4537
- var ATTR;
4538
- var init_attributes = __esm({
4539
- "../observability/dist/attributes.js"() {
4540
- ATTR = {
4541
- projectId: "vl.project_id",
4542
- callId: "vl.call_id",
4543
- campaignId: "vl.campaign_id",
4544
- room: "vl.room",
4545
- agentId: "vl.agent_id",
4546
- phoneNumberId: "vl.phone_number_id",
4547
- bindingId: "vl.binding_id",
4548
- source: "vl.source",
4549
- kind: "vl.kind"
4550
- };
4551
- }
4552
- });
4553
- function getCurrentCallContext() {
4554
- return callContextStore.getStore();
4555
- }
4556
- var callContextStore;
4557
- var init_call_context = __esm({
4558
- "../observability/dist/call-context.js"() {
4559
- init_attributes();
4560
- callContextStore = new AsyncLocalStorage();
4561
- }
4562
- });
4563
- var init_trace_propagation = __esm({
4564
- "../observability/dist/trace-propagation.js"() {
4565
- }
4566
- });
4567
- function meter() {
4568
- return metrics.getMeter(METER_NAME, METER_VERSION);
4569
- }
4570
- function modelFallbacks() {
4571
- return _modelFallbacks ??= meter().createCounter("vl.llm.model_fallback", {
4572
- description: "Model calls the provider rejected that fell back to the platform default model",
4573
- unit: "{fallbacks}"
4574
- });
4575
- }
4576
- function recordModelFallback(args) {
4577
- const out = {
4578
- "vl.model": args.model,
4579
- "vl.fallback_model": args.fallbackModel,
4580
- "vl.error_code": args.code
4581
- };
4582
- if (args.projectId)
4583
- out[ATTR.projectId] = args.projectId;
4584
- if (args.agentId)
4585
- out[ATTR.agentId] = args.agentId;
4586
- if (args.surface)
4587
- out["vl.surface"] = args.surface;
4588
- modelFallbacks().add(1, out);
4589
- }
4590
- var METER_NAME, METER_VERSION, _modelFallbacks;
4591
- var init_metrics = __esm({
4592
- "../observability/dist/metrics.js"() {
4593
- init_attributes();
4594
- METER_NAME = "voicelayer";
4595
- METER_VERSION = "0.1.0";
4596
- _modelFallbacks = null;
4597
- }
4598
- });
4599
- var init_latency_span = __esm({
4600
- "../observability/dist/latency-span.js"() {
4601
- init_attributes();
4602
- }
4603
- });
4604
-
4605
- // ../observability/dist/index.js
4606
- var init_dist2 = __esm({
4607
- "../observability/dist/index.js"() {
4608
- init_start();
4609
- init_call_context();
4610
- init_trace_propagation();
4611
- init_attributes();
4612
- init_metrics();
4613
- init_latency_span();
4614
- }
4615
- });
4616
5388
 
4617
5389
  // src/runtime/helper-models.ts
4618
5390
  function rejectionsFor(client) {
@@ -4852,7 +5624,9 @@ function evaluateDeterministic(atom, ctx) {
4852
5624
  if (result === void 0) return false;
4853
5625
  if (!path || path === "ok") {
4854
5626
  const ok = result.ok === true || result.kind === "ok";
4855
- return op === "!=" ? !ok : ok;
5627
+ const want = (rawVal ?? "").trim().toLowerCase();
5628
+ const eq2 = want === "" ? ok : String(ok) === want;
5629
+ return op === "!=" ? !eq2 : eq2;
4856
5630
  }
4857
5631
  const actual = resolvePath(result, path);
4858
5632
  if (!op) return actual !== void 0 && actual !== null && actual !== "";
@@ -5047,28 +5821,12 @@ var init_conditions = __esm({
5047
5821
  });
5048
5822
 
5049
5823
  // src/runtime/graph/turn-text.ts
5050
- function lastSentences(line) {
5051
- const text = line.trim();
5052
- if (!text) return [];
5053
- const sentences = [];
5054
- let last = 0;
5055
- for (const m of text.matchAll(SENTENCE_END)) {
5056
- const end = (m.index ?? 0) + m[0].length;
5057
- sentences.push(text.slice(last, end));
5058
- last = end;
5059
- }
5060
- if (last < text.length) sentences.push(text.slice(last));
5061
- return sentences.slice(-2);
5062
- }
5063
- function isQuestion(line) {
5064
- return lastSentences(line).some((s) => QUESTION_MARK.test(s));
5065
- }
5066
5824
  function wordsOf(field) {
5067
5825
  return field.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
5068
5826
  }
5069
5827
  function asksFor(line, fields) {
5070
5828
  if (!line) return false;
5071
- 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());
5829
+ 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());
5072
5830
  return questions.some((q) => {
5073
5831
  if (YES_NO.test(q) && !POLITE_REQUEST.test(q)) return false;
5074
5832
  const words = new Set(q.match(/[a-z0-9]+/g) ?? []);
@@ -5078,11 +5836,11 @@ function asksFor(line, fields) {
5078
5836
  });
5079
5837
  });
5080
5838
  }
5081
- var QUESTION_MARK, SENTENCE_END, POLITE_REQUEST, YES_NO;
5839
+ var QUESTION_MARK2, POLITE_REQUEST, YES_NO;
5082
5840
  var init_turn_text = __esm({
5083
5841
  "src/runtime/graph/turn-text.ts"() {
5084
- QUESTION_MARK = /[??؟]/;
5085
- SENTENCE_END = /[.!?…。!?؟]+["'”’)\]]*\s*/g;
5842
+ init_src();
5843
+ QUESTION_MARK2 = /[??؟]/;
5086
5844
  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/;
5087
5845
  YES_NO = /^(?:would|do|does|did|is|are|was|were|can|could|shall|should|will|have|has|had|may|might|am)\b/;
5088
5846
  }
@@ -5645,7 +6403,8 @@ async function nextTurn(deps, state, reprompt, opts) {
5645
6403
  ...opts?.deferCapture ? { deferCapture: true } : {},
5646
6404
  ...opts?.number ? { number: opts.number } : {},
5647
6405
  ...opts?.fields?.length ? { fields: opts.fields } : {},
5648
- ...opts?.answering && opts.answering.fields.length > 0 ? { answering: opts.answering } : {}
6406
+ ...opts?.answering && opts.answering.fields.length > 0 ? { answering: opts.answering } : {},
6407
+ ...opts?.captureAfterRead ? { captureAfterRead: true } : {}
5649
6408
  });
5650
6409
  if (evt.kind === "hangup") return { result: { kind: "end" } };
5651
6410
  if (evt.kind === "control") return { result: evt.result };
@@ -5672,7 +6431,7 @@ async function nextTurn(deps, state, reprompt, opts) {
5672
6431
  state.lastNotInContext = evt.notInContext;
5673
6432
  state.lastAgentText = evt.agentText;
5674
6433
  state.lastDtmf = void 0;
5675
- return { text: evt.text };
6434
+ return { text: evt.text, ...evt.capture ? { capture: evt.capture } : {} };
5676
6435
  }
5677
6436
  }
5678
6437
  function detectYesNo(text) {
@@ -5682,6 +6441,58 @@ function detectYesNo(text) {
5682
6441
  if (no && !yes) return "no";
5683
6442
  return null;
5684
6443
  }
6444
+ function renderLine(template, state, deps) {
6445
+ return { spoken: interpolate(template, scopeFor(state, deps)), model: interpolate(template, modelScopeFor(state, deps)) };
6446
+ }
6447
+ function askLinesFor(node, state, deps, askText, label, rephrase) {
6448
+ const render = (line) => renderLine(line, state, deps);
6449
+ const ctx = deps.askContext;
6450
+ const english = isEnglishLanguage(ctx?.language);
6451
+ const generated = readAskGeneratedLines(node.config.generated);
6452
+ const current = generated !== null && ctx !== void 0 && generated.basis === askLinesBasis({
6453
+ persona: ctx.persona,
6454
+ routing: ctx.routing ?? null,
6455
+ language: ctx.language ?? null,
6456
+ ask: str(node.config.ask),
6457
+ instructions: str(node.config.instructions)
6458
+ });
6459
+ if (generated && !current) deps.events.note("ask.reask_lines_stale", { node: node.id });
6460
+ const usable = current && rephrase !== "never" ? generated : null;
6461
+ const author = authorReasksOf(node.config.reasks).map(render);
6462
+ 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] : [];
6463
+ const offerTemplate = english ? skipOfferTemplate(label) : void 0;
6464
+ const skipOffer = usable?.skipOffer ? render(usable.skipOffer) : offerTemplate ? { spoken: offerTemplate, model: offerTemplate } : void 0;
6465
+ const voicedAsk = rephrase === "all" && usable?.voicedAsk ? render(usable.voicedAsk) : void 0;
6466
+ return { pool, ...skipOffer ? { skipOffer } : {}, ...voicedAsk ? { voicedAsk } : {} };
6467
+ }
6468
+ function skipOfferVerdict(text, dtmf) {
6469
+ if (dtmf === "1") return "yes";
6470
+ if (dtmf === "2") return "no";
6471
+ if (!text.trim()) return null;
6472
+ if (KEEP_ASKING.test(text)) return "no";
6473
+ if (SKIP_INTENT.test(text)) return OFFERS_IT.test(text) ? null : "yes";
6474
+ return detectYesNo(text);
6475
+ }
6476
+ async function readYesNo(t, state, deps) {
6477
+ const words = skipOfferVerdict(t.text, t.dtmf);
6478
+ if (words) return words;
6479
+ if (!t.text.trim()) return null;
6480
+ return await resolveBranch(
6481
+ [
6482
+ { id: "yes", label: "The caller agrees to skip it for now." },
6483
+ { id: "no", label: "The caller does not want to skip it \u2014 they will give it." }
6484
+ ],
6485
+ evalCtxOf(state, deps)
6486
+ );
6487
+ }
6488
+ function briefingScope(template, state, deps) {
6489
+ const scope2 = scopeFor(state, deps);
6490
+ const data = deps.processRt.getData();
6491
+ const notProvided = templateFieldRefs(template).filter((f) => isRequiredField(deps, f) && !hasSlot(data, f));
6492
+ if (notProvided.length === 0) return scope2;
6493
+ const filled = Object.fromEntries(notProvided.map((f) => [f, "not provided"]));
6494
+ return { ...scope2, ...filled, slots: { ...scope2.slots, ...filled } };
6495
+ }
5685
6496
  function formatCollected(deps) {
5686
6497
  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");
5687
6498
  }
@@ -5735,6 +6546,27 @@ function positiveMs(raw, fallback) {
5735
6546
  const n = Number(raw);
5736
6547
  return Number.isFinite(n) && n > 0 ? n : fallback;
5737
6548
  }
6549
+ function isRequiredField(deps, name) {
6550
+ return deps.model.requiredFields.has(name.trim());
6551
+ }
6552
+ function requiredInputsOf(node, state, deps) {
6553
+ const baked = node.config.requiredInputs;
6554
+ if (Array.isArray(baked)) return baked.filter((x) => typeof x === "string");
6555
+ const cached2 = state.derivedInputs?.get(node.id);
6556
+ if (cached2) return cached2;
6557
+ const derived = requiredInputsFor(node.type, node.config, (f) => isRequiredField(deps, f));
6558
+ (state.derivedInputs ??= /* @__PURE__ */ new Map()).set(node.id, derived);
6559
+ return derived;
6560
+ }
6561
+ function blockOnMissingInputs(node, state, deps, inputs, tool) {
6562
+ const data = deps.processRt.getData();
6563
+ const missing = inputs.filter((f) => !hasSlot(data, f)).map((f) => f.trim());
6564
+ if (missing.length === 0) return null;
6565
+ deps.events.note("step.missing_inputs", { node: node.id, missing });
6566
+ state.nodeResults[node.id] = { ok: false, error: "missing_required", missing };
6567
+ if (tool) state.toolResults[node.id] = { ok: false, status: 0, data: { kind: "blocked", reason: "missing_required", missing } };
6568
+ return deps.model.selectByPort(node.id, MISSING_PORT_ID) !== null ? { kind: "take", port: MISSING_PORT_ID } : routeByPort(node, deps, "fail");
6569
+ }
5738
6570
  function routeByPort(node, deps, port) {
5739
6571
  return deps.model.selectByPort(node.id, port) !== null ? { kind: "take", port } : { kind: "advance" };
5740
6572
  }
@@ -5746,11 +6578,15 @@ function menuDigitOf(b) {
5746
6578
  function menuLabelOf(label) {
5747
6579
  return label.replace(/^\s*[0-9*#]\s*[-—–:.]*\s*/, "").trim() || label.trim();
5748
6580
  }
5749
- 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;
6581
+ async function speakLine(text, deps) {
6582
+ await speak(text, deps);
6583
+ }
6584
+ 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;
5750
6585
  var init_executors = __esm({
5751
6586
  "src/runtime/graph/executors.ts"() {
5752
6587
  init_log();
5753
6588
  init_src();
6589
+ init_reask_templates();
5754
6590
  init_interpolate();
5755
6591
  init_builtins();
5756
6592
  init_conditions();
@@ -5772,9 +6608,11 @@ var init_executors = __esm({
5772
6608
  NO_WORDS = /\b(no|nope|nah|wrong|incorrect|negative|not right|that'?s not|change|actually)\b/i;
5773
6609
  confirmExecutor = async (node, state, deps) => {
5774
6610
  const text = interpolate(str(node.config.text), scopeFor(state, deps));
5775
- await speak(text || "Is that correct?", deps);
6611
+ const line = text || "Is that correct?";
6612
+ await speak(line, deps);
6613
+ const reprompt = isEnglishLanguage(deps.askContext?.language) ? notCaught(line) : line;
5776
6614
  for (let attempt = 0; attempt < 2; attempt++) {
5777
- const t = await nextTurn(deps, state, text || "Is that correct?");
6615
+ const t = await nextTurn(deps, state, reprompt);
5778
6616
  if ("result" in t) return t.result;
5779
6617
  let verdict = null;
5780
6618
  if (t.dtmf === "1") verdict = "yes";
@@ -5802,7 +6640,7 @@ var init_executors = __esm({
5802
6640
  deps.events.note("confirm.unresolved", { node: node.id });
5803
6641
  return { kind: "advance" };
5804
6642
  };
5805
- askExecutor = async (node, state, deps) => {
6643
+ legacyAskExecutor = async (node, state, deps) => {
5806
6644
  const slot = str(node.config.slot) || node.id;
5807
6645
  const authoredSlot = slot === node.id ? "" : slot.trim();
5808
6646
  const label = authoredSlot.replace(/[_-]+/g, " ");
@@ -5843,6 +6681,165 @@ var init_executors = __esm({
5843
6681
  deps.events.note("ask.unfilled", { slot: slot.trim() });
5844
6682
  return { kind: "advance" };
5845
6683
  };
6684
+ ASK_POLICIES = /* @__PURE__ */ new Set(["offer_skip", "fallback", "end"]);
6685
+ askExecutor = async (node, state, deps) => {
6686
+ const cfg = node.config;
6687
+ if (typeof cfg.maxAttempts !== "number") return legacyAskExecutor(node, state, deps);
6688
+ const slot = str(cfg.slot) || node.id;
6689
+ const field = slot.trim();
6690
+ const label = (slot === node.id ? "" : field).replace(/[_-]+/g, " ");
6691
+ const required = cfg.required !== false;
6692
+ const maxAttempts = Math.min(6, Math.max(1, Math.round(cfg.maxAttempts)));
6693
+ const policy = ASK_POLICIES.has(str(cfg.onExhausted)) ? str(cfg.onExhausted) : "offer_skip";
6694
+ const rephrase = cfg.rephrase === "all" || cfg.rephrase === "never" ? cfg.rephrase : "reasks";
6695
+ const unfilledWired = deps.model.selectByPort(node.id, UNFILLED_PORT_ID) !== null;
6696
+ const captured = () => hasSlot(deps.processRt.getData(), slot);
6697
+ const exitNext = { kind: "advance" };
6698
+ if (captured()) {
6699
+ deps.events.note("ask.skipped", { slot: field, reason: "already known" });
6700
+ return exitNext;
6701
+ }
6702
+ const askText = renderLine(str(cfg.ask) || defaultAskText(slot, node.id), state, deps);
6703
+ const lines = askLinesFor(node, state, deps, askText, label, rephrase);
6704
+ const closing = () => interpolate(str(cfg.exhaustedMessage) || DEFAULT_ASK_EXHAUSTED_MESSAGE, scopeFor(state, deps));
6705
+ const giveUp = async () => {
6706
+ state.closingLine = closing();
6707
+ if (unfilledWired) return { kind: "take", port: UNFILLED_PORT_ID };
6708
+ await speak(state.closingLine, deps);
6709
+ return { kind: "end" };
6710
+ };
6711
+ let lastLine = askText;
6712
+ let rotation = 0;
6713
+ const nextLine = () => {
6714
+ const pool = lines.pool;
6715
+ if (pool.length === 0) return askText;
6716
+ let line = pool[rotation++ % pool.length];
6717
+ if (line.spoken === lastLine.spoken && pool.length > 1) line = pool[rotation++ % pool.length];
6718
+ return line;
6719
+ };
6720
+ const sayLine = async (line) => {
6721
+ lastLine = line;
6722
+ await speak(line.spoken, deps);
6723
+ };
6724
+ const kind = cfg.type === "phone" || cfg.type === "number" ? cfg.type : void 0;
6725
+ const turnOpts = (since) => ({
6726
+ fields: [slot],
6727
+ // the ask's question is about its slot by definition: every turn here answers it (a free-text slot takes the turn)
6728
+ answering: { fields: answerable(deps, [slot]), step: node.id, since },
6729
+ ...kind ? { number: { field: slot, kind } } : {}
6730
+ });
6731
+ let askedAt = Date.now();
6732
+ if (required && state.skipped?.[field]) {
6733
+ const revisits = (state.skipRevisits?.[field] ?? 0) + 1;
6734
+ state.skipRevisits = { ...state.skipRevisits, [field]: revisits };
6735
+ if (revisits >= 2) {
6736
+ deps.events.note("ask.revisit_exhausted", { slot: field });
6737
+ return giveUp();
6738
+ }
6739
+ deps.events.note("ask.revisit", { slot: field });
6740
+ await sayLine(askText);
6741
+ const t = await nextTurn(deps, state, () => sayLine(nextLine()), turnOpts(askedAt));
6742
+ if ("result" in t) return t.result;
6743
+ if (captured()) {
6744
+ deps.events.note("ask.captured", { slot: field });
6745
+ return exitNext;
6746
+ }
6747
+ deps.events.note("ask.revisit_unfilled", { slot: field });
6748
+ return giveUp();
6749
+ }
6750
+ const clarify = async (question) => {
6751
+ const endOn = lines.pool.length > 0 ? nextLine() : null;
6752
+ const what = label || "the detail just asked about";
6753
+ const parts = [
6754
+ `You are collecting "${what}" from the caller. They haven't given a clear, valid value yet.`,
6755
+ question ? "The caller asked a question: answer it briefly and honestly first." : "Acknowledge what they said in a few words.",
6756
+ // the model-safe copies only: caller-given values fenced and tokenised, never raw (§B.9)
6757
+ endOn ? `Then ask for just that again, ending on this question: "${endOn.model}"` : "Then briefly ask again for just that.",
6758
+ `Don't repeat this line word for word: "${lastLine.model}"`
6759
+ ];
6760
+ const instructions = interpolate(str(cfg.instructions), modelScopeFor(state, deps));
6761
+ if (instructions) parts.push(`About this question (from the flow's author): ${instructions}`);
6762
+ if (endOn) lastLine = endOn;
6763
+ await withHistory(deps, historyPolicyOf(node), () => genReply(deps, parts.join("\n")));
6764
+ };
6765
+ const round = async (attempts) => {
6766
+ let tries = 0;
6767
+ for (let turn = 1; ; turn++) {
6768
+ const t = await nextTurn(deps, state, () => sayLine(nextLine()), turnOpts(askedAt));
6769
+ if ("result" in t) return t.result;
6770
+ if (captured()) return "captured";
6771
+ const text = t.text.trim();
6772
+ const filler = isFiller(text);
6773
+ const question = !filler && isQuestion(text);
6774
+ if (!question) tries += 1;
6775
+ if (tries >= attempts) return "exhausted";
6776
+ if (turn >= 2 * attempts) {
6777
+ deps.events.note("ask.turn_cap", { slot: field, turns: turn });
6778
+ return "exhausted";
6779
+ }
6780
+ deps.events.note("ask.clarify", { slot: field, turn, kind: filler ? "filler" : question ? "question" : "answer" });
6781
+ if (filler && lines.pool.length > 0 || rephrase === "never") await sayLine(nextLine());
6782
+ else await clarify(question);
6783
+ }
6784
+ };
6785
+ await sayLine(lines.voicedAsk ?? askText);
6786
+ let outcome = await round(maxAttempts);
6787
+ if (outcome === "captured") {
6788
+ deps.events.note("ask.captured", { slot: field });
6789
+ return exitNext;
6790
+ }
6791
+ if (outcome !== "exhausted") return outcome;
6792
+ if (!required) {
6793
+ deps.events.note("ask.unfilled", { slot: field });
6794
+ return exitNext;
6795
+ }
6796
+ deps.events.note("ask.exhausted", { slot: field, policy });
6797
+ if (policy === "end" || policy === "fallback") return giveUp();
6798
+ const offer = lines.skipOffer;
6799
+ if (!offer) return giveUp();
6800
+ state.closingLine = closing();
6801
+ const skip = (reason) => {
6802
+ state.skipped = { ...state.skipped, [field]: reason };
6803
+ deps.events.note("ask.skipped_by_caller", { slot: field, reason });
6804
+ return unfilledWired ? { kind: "take", port: UNFILLED_PORT_ID } : exitNext;
6805
+ };
6806
+ let unclear = 0;
6807
+ await sayLine(offer);
6808
+ for (; ; ) {
6809
+ const t = await nextTurn(deps, state, offer.spoken, { fields: [slot], captureAfterRead: true });
6810
+ if ("result" in t) return t.result;
6811
+ const verdict = await readYesNo(t, state, deps);
6812
+ if (verdict === "yes") {
6813
+ await t.capture?.({ exclude: [slot] });
6814
+ return skip("caller");
6815
+ }
6816
+ if (verdict === "no") {
6817
+ await t.capture?.({ exclude: [slot] });
6818
+ deps.events.note("ask.skip_declined", { slot: field });
6819
+ askedAt = Date.now();
6820
+ await sayLine(nextLine());
6821
+ outcome = await round(maxAttempts);
6822
+ if (outcome === "captured") {
6823
+ deps.events.note("ask.captured", { slot: field });
6824
+ return exitNext;
6825
+ }
6826
+ if (outcome !== "exhausted") return outcome;
6827
+ deps.events.note("ask.exhausted", { slot: field, policy, round: 2 });
6828
+ return giveUp();
6829
+ }
6830
+ await t.capture?.();
6831
+ if (captured()) {
6832
+ deps.events.note("ask.captured", { slot: field });
6833
+ return exitNext;
6834
+ }
6835
+ unclear += 1;
6836
+ if (unclear >= 2) return skip("unclear");
6837
+ await sayLine(offer);
6838
+ }
6839
+ };
6840
+ 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;
6841
+ SKIP_INTENT = /\b(?:skip|don'?t have|do not have|haven'?t got|move on|pass on (?:it|that|this))\b/i;
6842
+ OFFERS_IT = /\b(?:give|here|hold on|let me|it'?s|my number)\b/i;
5846
6843
  gateExecutor = async (node, _state, deps) => {
5847
6844
  const required = Array.isArray(node.config.requiredFields) ? node.config.requiredFields.filter((x) => typeof x === "string") : [];
5848
6845
  const data = deps.processRt.getData();
@@ -5865,7 +6862,7 @@ var init_executors = __esm({
5865
6862
  }
5866
6863
  const reason = str(cfg.reason) || `flow:${node.id}`;
5867
6864
  const mode = cfg.mode === "warm" || cfg.mode === "cold" ? cfg.mode : void 0;
5868
- const briefing = str(cfg.briefing) ? interpolate(str(cfg.briefing), scopeFor(state, deps)) : void 0;
6865
+ const briefing = str(cfg.briefing) ? interpolate(str(cfg.briefing), briefingScope(str(cfg.briefing), state, deps)) : void 0;
5869
6866
  deps.events.note("handoff.start", { node: node.id, to: target, ...mode ? { mode } : {} });
5870
6867
  try {
5871
6868
  await deps.ctx.handoff(target, {
@@ -5969,9 +6966,17 @@ ${collected}`);
5969
6966
  const toolRef = str(cfg.toolRef);
5970
6967
  const registry = deps.resolvers?.registryTools;
5971
6968
  if (toolRef && registry) {
5972
- deps.events.note("tool.invoked", { node: node.id, name: toolRef, ref: true });
5973
6969
  try {
5974
6970
  const spec = (await registry.catalog()).find((t) => t.name === toolRef);
6971
+ const blocked2 = blockOnMissingInputs(
6972
+ node,
6973
+ state,
6974
+ deps,
6975
+ Object.keys(spec?.input ?? {}).filter((f) => isRequiredField(deps, f)),
6976
+ true
6977
+ );
6978
+ if (blocked2) return blocked2;
6979
+ deps.events.note("tool.invoked", { node: node.id, name: toolRef, ref: true });
5975
6980
  const slots2 = deps.processRt.getData();
5976
6981
  const bag2 = {};
5977
6982
  for (const field of Object.keys(spec?.input ?? {})) {
@@ -6004,6 +7009,8 @@ ${collected}`);
6004
7009
  deps.events.note("tool.unavailable", { node: node.id, name: toolRef });
6005
7010
  return routeByOutcome();
6006
7011
  }
7012
+ const blocked = blockOnMissingInputs(node, state, deps, requiredInputsOf(node, state, deps), true);
7013
+ if (blocked) return blocked;
6007
7014
  const url = str(cfg.url);
6008
7015
  if (!url) {
6009
7016
  state.toolResults[node.id] = { ok: false, status: 0, data: null };
@@ -6229,7 +7236,7 @@ ${collected}`);
6229
7236
  deps.events.note("menu.selected", { node: node.id, choice });
6230
7237
  return { kind: "take", port: choice };
6231
7238
  }
6232
- await speak(prompt, deps);
7239
+ await speak(isEnglishLanguage(deps.askContext?.language) && prompt ? notCaught(prompt) : prompt, deps);
6233
7240
  }
6234
7241
  }
6235
7242
  deps.events.note("menu.unmatched", { node: node.id });
@@ -6502,7 +7509,7 @@ ${collected}`);
6502
7509
  }
6503
7510
  await speak(
6504
7511
  interpolate(
6505
- 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.",
7512
+ str(cfg.incompleteMessage) || DEFAULT_ASK_EXHAUSTED_MESSAGE,
6506
7513
  scopeFor(state, deps)
6507
7514
  ),
6508
7515
  deps
@@ -6558,6 +7565,7 @@ ${collected}`);
6558
7565
  ...deps.resolvers ? { resolvers: deps.resolvers } : {},
6559
7566
  ...deps.live ? { live: deps.live } : {},
6560
7567
  ...deps.security ? { security: deps.security } : {},
7568
+ ...deps.askContext ? { askContext: deps.askContext } : {},
6561
7569
  depth: depth + 1
6562
7570
  });
6563
7571
  deps.events.note("subflow.exit", { node: node.id, ref, outcome: outcome.kind });
@@ -6570,6 +7578,8 @@ ${collected}`);
6570
7578
  EMAIL_SEND_TIMEOUT_MS = 1e4;
6571
7579
  emailExecutor = async (node, state, deps) => {
6572
7580
  const cfg = node.config;
7581
+ const blocked = blockOnMissingInputs(node, state, deps, requiredInputsOf(node, state, deps), false);
7582
+ if (blocked) return blocked;
6573
7583
  const scope2 = scopeFor(state, deps);
6574
7584
  const render = (key) => interpolate(str(cfg[key]), scope2).trim();
6575
7585
  const to = render("to");
@@ -6679,6 +7689,12 @@ async function selectNext(model2, from, result, state, deps) {
6679
7689
  ...state.lastDtmf !== void 0 ? { lastDtmf: state.lastDtmf } : {}
6680
7690
  });
6681
7691
  }
7692
+ async function closeAtLoopGuard(state, deps) {
7693
+ try {
7694
+ await speakLine(state.closingLine ?? DEFAULT_ASK_EXHAUSTED_MESSAGE, { ctx: deps.ctx, ...deps.realtime ? { realtime: true } : {} });
7695
+ } catch {
7696
+ }
7697
+ }
6682
7698
  async function runFlowGraph(program, deps) {
6683
7699
  const model2 = buildGraphModel(program);
6684
7700
  const maxNodeVisits = deps.maxNodeVisits ?? DEFAULT_MAX_NODE_VISITS;
@@ -6691,6 +7707,7 @@ async function runFlowGraph(program, deps) {
6691
7707
  while (cursor !== null) {
6692
7708
  if (++steps > maxSteps) {
6693
7709
  deps.events.note("flow.loop_guard_tripped", { reason: "max_steps", steps });
7710
+ await closeAtLoopGuard(state, deps);
6694
7711
  break;
6695
7712
  }
6696
7713
  const node = model2.node(cursor);
@@ -6703,6 +7720,7 @@ async function runFlowGraph(program, deps) {
6703
7720
  const visitCap = node.config.hub === true ? HUB_MAX_NODE_VISITS : maxNodeVisits;
6704
7721
  if (visit > visitCap) {
6705
7722
  deps.events.note("flow.loop_guard_tripped", { reason: "max_node_visits", nodeId: cursor });
7723
+ await closeAtLoopGuard(state, deps);
6706
7724
  break;
6707
7725
  }
6708
7726
  state.cursor = cursor;
@@ -6723,7 +7741,8 @@ async function runFlowGraph(program, deps) {
6723
7741
  ...deps.depth !== void 0 ? { depth: deps.depth } : {},
6724
7742
  ...deps.busy ? { busy: deps.busy } : {},
6725
7743
  ...deps.live ? { live: deps.live } : {},
6726
- ...deps.security ? { security: deps.security } : {}
7744
+ ...deps.security ? { security: deps.security } : {},
7745
+ ...deps.askContext ? { askContext: deps.askContext } : {}
6727
7746
  });
6728
7747
  } catch (err) {
6729
7748
  const message = err instanceof Error ? err.message : String(err);
@@ -6756,6 +7775,7 @@ var init_interpreter = __esm({
6756
7775
  init_log();
6757
7776
  init_graph_model();
6758
7777
  init_executors();
7778
+ init_src();
6759
7779
  init_conditions();
6760
7780
  DEFAULT_MAX_NODE_VISITS = 25;
6761
7781
  DEFAULT_MAX_STEPS = 200;
@@ -7145,6 +8165,13 @@ function createUtteranceStream(session, processRt, ctx, rails, prewarm, wantsDtm
7145
8165
  ...number ? { numberField: number.field } : {},
7146
8166
  ...answer ? { answering: answer.field } : {}
7147
8167
  };
8168
+ if (opts?.captureAfterRead) {
8169
+ const text = raw.text;
8170
+ return {
8171
+ ...turn,
8172
+ capture: (o) => capture(text, false, { ...step, ...o?.exclude?.length ? { exclude: o.exclude } : {} }, answer)
8173
+ };
8174
+ }
7148
8175
  if (opts?.deferCapture) {
7149
8176
  const run = capture(raw.text, true, step, answer).then(() => {
7150
8177
  if (pending === run) pending = null;
@@ -7318,7 +8345,8 @@ var NOT_SENT = /* @__PURE__ */ new Set([
7318
8345
  "email_provider_error",
7319
8346
  "email_send_failed",
7320
8347
  "insufficient_balance",
7321
- "spend_cap_exceeded"
8348
+ // a hard budget refused it (the key's monthly budget replaced the old per-key spend cap — monetization MON2)
8349
+ "budget_exceeded"
7322
8350
  ]);
7323
8351
  function dedupeConversationEmails(resolver) {
7324
8352
  const sends = /* @__PURE__ */ new Map();
@@ -7401,7 +8429,8 @@ async function runFlowProgram(program, ctx, opts) {
7401
8429
  ...opts.resolvers ? { resolvers: opts.resolvers } : {},
7402
8430
  ...opts.busy ? { busy: opts.busy } : {},
7403
8431
  ...opts.live ? { live: opts.live } : {},
7404
- ...security ? { security } : {}
8432
+ ...security ? { security } : {},
8433
+ ...opts.askContext ? { askContext: opts.askContext } : {}
7405
8434
  });
7406
8435
  graphLog("outcome", outcome);
7407
8436
  return outcome;
@@ -7888,14 +8917,15 @@ function createProcessRuntime(def) {
7888
8917
  console.error("[process] onField threw", err);
7889
8918
  }
7890
8919
  };
7891
- const answering = opts?.answering;
8920
+ const excluded = (name) => opts?.exclude?.some((f) => f.trim() === name.trim()) === true;
8921
+ const answering = opts?.answering !== void 0 && !excluded(opts.answering) ? opts.answering : void 0;
7892
8922
  const answerField = answering !== void 0 ? def.collect[answering] : void 0;
7893
8923
  const answerWasOpen = answering !== void 0 && !callerGave.has(answering);
7894
8924
  const status = primitive.getFieldStatus();
7895
8925
  const captured = (name) => status.find((f) => f.name === name)?.captured === true;
7896
8926
  const stepFields = opts?.stepFields;
7897
8927
  const named = (name) => stepFields?.some((f) => f.trim() === name.trim()) === true;
7898
- const spoken = Object.entries(def.collect).filter(([, f]) => f.fromCaller !== false);
8928
+ const spoken = Object.entries(def.collect).filter(([name, f]) => f.fromCaller !== false && !excluded(name));
7899
8929
  const open = (name) => !captured(name) || named(name) && !callerGave.has(name);
7900
8930
  const remaining = spoken.filter(([name]) => open(name) && (!completed || stepFields === void 0 || named(name)));
7901
8931
  const correctable = spoken.filter(([name]) => captured(name) && named(name) && callerGave.has(name));
@@ -8433,13 +9463,13 @@ function createTextSessionAdapter(transport) {
8433
9463
  };
8434
9464
  function setState(next) {
8435
9465
  const raw = String(next);
8436
- const norm = normalizeState(raw);
9466
+ const norm2 = normalizeState(raw);
8437
9467
  const oldState = currentState;
8438
9468
  const rawOld = currentRaw;
8439
- currentState = norm;
9469
+ currentState = norm2;
8440
9470
  currentRaw = raw;
8441
- fire("state", { oldState, newState: norm, rawOldState: rawOld, rawNewState: raw });
8442
- if (NON_BUSY_STATES.has(norm)) releaseIdleWaiters("idle");
9471
+ fire("state", { oldState, newState: norm2, rawOldState: rawOld, rawNewState: raw });
9472
+ if (NON_BUSY_STATES.has(norm2)) releaseIdleWaiters("idle");
8443
9473
  }
8444
9474
  function push(text, opts) {
8445
9475
  if (closed || text.length === 0) return;
@@ -8560,7 +9590,8 @@ function runTextSession(opts) {
8560
9590
  events,
8561
9591
  ...opts.maxSteps !== void 0 ? { maxSteps: opts.maxSteps } : {},
8562
9592
  ...opts.onAwaitInput ? { onAwaitInput: opts.onAwaitInput } : {},
8563
- ...opts.resolvers ? { resolvers: opts.resolvers } : {}
9593
+ ...opts.resolvers ? { resolvers: opts.resolvers } : {},
9594
+ ...opts.askContext ? { askContext: opts.askContext } : {}
8564
9595
  });
8565
9596
  return {
8566
9597
  sendUserMessage: (text) => {