@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.
@@ -45,35 +45,6 @@ var init_log = __esm({
45
45
  }
46
46
  });
47
47
 
48
- // src/runtime/graph/graph-model.ts
49
- function buildGraphModel(program) {
50
- return {
51
- entry: program.entry,
52
- node: (id) => program.nodes[id],
53
- out: (id) => program.nodes[id]?.out ?? [],
54
- selectByPort: (id, port) => {
55
- const edges = program.nodes[id]?.out ?? [];
56
- const byHandle = edges.find((e) => e.sourceHandle === port);
57
- if (byHandle) return byHandle.to;
58
- const byLabel = edges.find((e) => e.label === port);
59
- return byLabel ? byLabel.to : null;
60
- },
61
- askNodeForSlot: (slot) => {
62
- const want = slot.trim();
63
- for (const node of Object.values(program.nodes)) {
64
- if (node.type !== "ask") continue;
65
- const s = typeof node.config.slot === "string" ? node.config.slot : node.id;
66
- if (s.trim() === want) return node.id;
67
- }
68
- return void 0;
69
- }
70
- };
71
- }
72
- var init_graph_model = __esm({
73
- "src/runtime/graph/graph-model.ts"() {
74
- }
75
- });
76
-
77
48
  // ../contracts/src/effect-stack.ts
78
49
  var EffectStack;
79
50
  var init_effect_stack = __esm({
@@ -1061,11 +1032,10 @@ var init_agent_config = __esm({
1061
1032
  projectId: z.string().min(1),
1062
1033
  // Human-readable label. No `agentType` enum on purpose.
1063
1034
  name: z.string().min(1).max(128),
1064
- // Soft override of the worker's baked-in prompt. Absent → call_start
1065
- // uses the SDK worker's predefined prompt (the source of truth for
1066
- // pre-defined agents like FNOL). Present → it overrides the worker's
1067
- // prompt at call_start as a snapshot decision; mid-call edits are not
1068
- // 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.
1069
1039
  systemPrompt: z.string().max(64e3).optional(),
1070
1040
  // Routing/decision layer (Voiceflow "Instructions"): plain-language guidance the
1071
1041
  // agent reads to decide WHEN to do things (vs systemPrompt = WHO it is). Injected
@@ -1946,12 +1916,143 @@ var init_call_result = __esm({
1946
1916
  });
1947
1917
  }
1948
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
+ });
1949
2049
  var WEBHOOK_EVENT_TYPES, CallWebhookDirection, CallWebhookOutcome, CallWebhookAttributes, Party, CALL_ENDED_PAYLOAD_VERSION, CallEndedTranscript;
1950
2050
  var init_webhook_events = __esm({
1951
2051
  "../contracts/src/webhook-events.ts"() {
1952
2052
  init_call_result();
1953
2053
  init_call_outcome();
1954
- 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"];
1955
2056
  z.enum(WEBHOOK_EVENT_TYPES);
1956
2057
  CallWebhookDirection = z.enum(["inbound", "outbound", "web"]);
1957
2058
  CallWebhookOutcome = z.enum(["completed", "dropped", "failed", "no_answer", "other"]);
@@ -2010,6 +2111,19 @@ var init_webhook_events = __esm({
2010
2111
  recordingUrl: z.string().url(),
2011
2112
  status: z.enum(["starting", "active", "completed", "failed", "aborted"])
2012
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
+ });
2013
2127
  }
2014
2128
  });
2015
2129
  var PhoneNumberStatus, PhoneNumberProvider, PhoneNumberCapabilities, PhoneNumberRecord;
@@ -2084,10 +2198,154 @@ var init_project_limits = __esm({
2084
2198
  });
2085
2199
 
2086
2200
  // ../contracts/src/flow-ports.ts
2087
- var INCOMPLETE_PORT_ID;
2201
+ var INCOMPLETE_PORT_ID, UNFILLED_PORT_ID, MISSING_PORT_ID;
2088
2202
  var init_flow_ports = __esm({
2089
2203
  "../contracts/src/flow-ports.ts"() {
2090
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;
2091
2349
  }
2092
2350
  });
2093
2351
 
@@ -2108,6 +2366,9 @@ var init_flow_compile = __esm({
2108
2366
  "../contracts/src/flow-compile.ts"() {
2109
2367
  init_flow_enrichment();
2110
2368
  init_flow_edges();
2369
+ init_flow_ports();
2370
+ init_flow_ask_lines();
2371
+ init_flow_required_inputs();
2111
2372
  }
2112
2373
  });
2113
2374
 
@@ -2295,6 +2556,8 @@ var init_flow_validate = __esm({
2295
2556
  init_flow_ports();
2296
2557
  init_flow_edges();
2297
2558
  init_flow_compile_check();
2559
+ init_flow_required_inputs();
2560
+ init_flow_ask_lines();
2298
2561
  init_secret_headers();
2299
2562
  init_network_address();
2300
2563
  init_dispatch_metadata();
@@ -3820,6 +4083,7 @@ var init_monitoring = __esm({
3820
4083
  var WalletRefusalCode, WalletMode, WalletInvoiceStatus, WalletInvoice, WalletEntryKind, WalletEntry, TOPUP_MIN_USD, TOPUP_MAX_USD;
3821
4084
  var init_wallet = __esm({
3822
4085
  "../contracts/src/wallet.ts"() {
4086
+ init_budgets();
3823
4087
  WalletRefusalCode = z.enum([
3824
4088
  // Prepaid balance (or an invoiced credit limit) cannot cover the action.
3825
4089
  "insufficient_balance",
@@ -3829,14 +4093,18 @@ var init_wallet = __esm({
3829
4093
  // still answered. Paying the invoice restores the workspace.
3830
4094
  "account_suspended",
3831
4095
  // The action's reservation already closed (e.g. a call ended or expired) — it cannot be re-admitted.
3832
- "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"
3833
4099
  ]);
3834
4100
  z.object({
3835
4101
  error: WalletRefusalCode,
3836
4102
  code: WalletRefusalCode,
3837
4103
  availableUsd: z.string(),
3838
4104
  needUsd: z.string(),
3839
- message: z.string()
4105
+ message: z.string(),
4106
+ // budget_exceeded only: which budget, and when it resets.
4107
+ budget: BudgetRefusalDetail.optional()
3840
4108
  });
3841
4109
  WalletMode = z.enum(["prepaid", "invoiced"]);
3842
4110
  z.object({
@@ -3877,7 +4145,7 @@ var init_wallet = __esm({
3877
4145
  payUrl: z.string().nullable()
3878
4146
  });
3879
4147
  z.object({ invoices: z.array(WalletInvoice) });
3880
- 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"]);
3881
4149
  WalletEntry = z.object({
3882
4150
  id: z.string(),
3883
4151
  kind: WalletEntryKind,
@@ -3921,6 +4189,81 @@ var init_pipeline_keys = __esm({
3921
4189
  });
3922
4190
  }
3923
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
+ });
3924
4267
 
3925
4268
  // ../contracts/src/index.ts
3926
4269
  var init_src = __esm({
@@ -3957,6 +4300,10 @@ var init_src = __esm({
3957
4300
  init_flow();
3958
4301
  init_flow_program();
3959
4302
  init_flow_ports();
4303
+ init_flow_persona();
4304
+ init_flow_ask_lines();
4305
+ init_flow_required_inputs();
4306
+ init_question_text();
3960
4307
  init_flow_validate();
3961
4308
  init_flow_builtins();
3962
4309
  init_compliance();
@@ -3985,16 +4332,73 @@ var init_src = __esm({
3985
4332
  init_channel_connector();
3986
4333
  init_monitoring();
3987
4334
  init_wallet();
4335
+ init_budgets();
3988
4336
  init_network_address();
3989
4337
  init_dispatch_metadata();
3990
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.";
3991
4395
  }
3992
4396
  });
3993
4397
 
3994
4398
  // src/runtime/graph/interpolate.ts
3995
4399
  function interpolate(template, scope2) {
3996
4400
  if (!template || template.indexOf("{{") === -1) return template;
3997
- return template.replace(TOKEN, (_match, expr) => {
4401
+ return template.replace(TOKEN2, (_match, expr) => {
3998
4402
  const value = resolvePath(scope2, expr.trim());
3999
4403
  return value === void 0 || value === null ? "" : String(value);
4000
4404
  });
@@ -4019,10 +4423,10 @@ function hasSlot(slots, name) {
4019
4423
  const v = resolvePath(slots, name.trim());
4020
4424
  return v !== void 0 && v !== null && v !== "";
4021
4425
  }
4022
- var TOKEN;
4426
+ var TOKEN2;
4023
4427
  var init_interpolate = __esm({
4024
4428
  "src/runtime/graph/interpolate.ts"() {
4025
- TOKEN = /\{\{\s*([^}]+?)\s*\}\}/g;
4429
+ TOKEN2 = /\{\{\s*([^}]+?)\s*\}\}/g;
4026
4430
  }
4027
4431
  });
4028
4432
 
@@ -5220,7 +5624,9 @@ function evaluateDeterministic(atom, ctx) {
5220
5624
  if (result === void 0) return false;
5221
5625
  if (!path || path === "ok") {
5222
5626
  const ok = result.ok === true || result.kind === "ok";
5223
- return op === "!=" ? !ok : ok;
5627
+ const want = (rawVal ?? "").trim().toLowerCase();
5628
+ const eq2 = want === "" ? ok : String(ok) === want;
5629
+ return op === "!=" ? !eq2 : eq2;
5224
5630
  }
5225
5631
  const actual = resolvePath(result, path);
5226
5632
  if (!op) return actual !== void 0 && actual !== null && actual !== "";
@@ -5415,28 +5821,12 @@ var init_conditions = __esm({
5415
5821
  });
5416
5822
 
5417
5823
  // src/runtime/graph/turn-text.ts
5418
- function lastSentences(line) {
5419
- const text = line.trim();
5420
- if (!text) return [];
5421
- const sentences = [];
5422
- let last = 0;
5423
- for (const m of text.matchAll(SENTENCE_END)) {
5424
- const end = (m.index ?? 0) + m[0].length;
5425
- sentences.push(text.slice(last, end));
5426
- last = end;
5427
- }
5428
- if (last < text.length) sentences.push(text.slice(last));
5429
- return sentences.slice(-2);
5430
- }
5431
- function isQuestion(line) {
5432
- return lastSentences(line).some((s) => QUESTION_MARK.test(s));
5433
- }
5434
5824
  function wordsOf(field) {
5435
5825
  return field.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
5436
5826
  }
5437
5827
  function asksFor(line, fields) {
5438
5828
  if (!line) return false;
5439
- 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());
5440
5830
  return questions.some((q) => {
5441
5831
  if (YES_NO.test(q) && !POLITE_REQUEST.test(q)) return false;
5442
5832
  const words = new Set(q.match(/[a-z0-9]+/g) ?? []);
@@ -5446,11 +5836,11 @@ function asksFor(line, fields) {
5446
5836
  });
5447
5837
  });
5448
5838
  }
5449
- var QUESTION_MARK, SENTENCE_END, POLITE_REQUEST, YES_NO;
5839
+ var QUESTION_MARK2, POLITE_REQUEST, YES_NO;
5450
5840
  var init_turn_text = __esm({
5451
5841
  "src/runtime/graph/turn-text.ts"() {
5452
- QUESTION_MARK = /[??؟]/;
5453
- SENTENCE_END = /[.!?…。!?؟]+["'”’)\]]*\s*/g;
5842
+ init_src();
5843
+ QUESTION_MARK2 = /[??؟]/;
5454
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/;
5455
5845
  YES_NO = /^(?:would|do|does|did|is|are|was|were|can|could|shall|should|will|have|has|had|may|might|am)\b/;
5456
5846
  }
@@ -6013,7 +6403,8 @@ async function nextTurn(deps, state, reprompt, opts) {
6013
6403
  ...opts?.deferCapture ? { deferCapture: true } : {},
6014
6404
  ...opts?.number ? { number: opts.number } : {},
6015
6405
  ...opts?.fields?.length ? { fields: opts.fields } : {},
6016
- ...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 } : {}
6017
6408
  });
6018
6409
  if (evt.kind === "hangup") return { result: { kind: "end" } };
6019
6410
  if (evt.kind === "control") return { result: evt.result };
@@ -6040,7 +6431,7 @@ async function nextTurn(deps, state, reprompt, opts) {
6040
6431
  state.lastNotInContext = evt.notInContext;
6041
6432
  state.lastAgentText = evt.agentText;
6042
6433
  state.lastDtmf = void 0;
6043
- return { text: evt.text };
6434
+ return { text: evt.text, ...evt.capture ? { capture: evt.capture } : {} };
6044
6435
  }
6045
6436
  }
6046
6437
  function detectYesNo(text) {
@@ -6050,6 +6441,58 @@ function detectYesNo(text) {
6050
6441
  if (no && !yes) return "no";
6051
6442
  return null;
6052
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
+ }
6053
6496
  function formatCollected(deps) {
6054
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");
6055
6498
  }
@@ -6103,6 +6546,27 @@ function positiveMs(raw, fallback) {
6103
6546
  const n = Number(raw);
6104
6547
  return Number.isFinite(n) && n > 0 ? n : fallback;
6105
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
+ }
6106
6570
  function routeByPort(node, deps, port) {
6107
6571
  return deps.model.selectByPort(node.id, port) !== null ? { kind: "take", port } : { kind: "advance" };
6108
6572
  }
@@ -6114,11 +6578,15 @@ function menuDigitOf(b) {
6114
6578
  function menuLabelOf(label) {
6115
6579
  return label.replace(/^\s*[0-9*#]\s*[-—–:.]*\s*/, "").trim() || label.trim();
6116
6580
  }
6117
- 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;
6118
6585
  var init_executors = __esm({
6119
6586
  "src/runtime/graph/executors.ts"() {
6120
6587
  init_log();
6121
6588
  init_src();
6589
+ init_reask_templates();
6122
6590
  init_interpolate();
6123
6591
  init_builtins();
6124
6592
  init_conditions();
@@ -6140,9 +6608,11 @@ var init_executors = __esm({
6140
6608
  NO_WORDS = /\b(no|nope|nah|wrong|incorrect|negative|not right|that'?s not|change|actually)\b/i;
6141
6609
  confirmExecutor = async (node, state, deps) => {
6142
6610
  const text = interpolate(str(node.config.text), scopeFor(state, deps));
6143
- 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;
6144
6614
  for (let attempt = 0; attempt < 2; attempt++) {
6145
- const t = await nextTurn(deps, state, text || "Is that correct?");
6615
+ const t = await nextTurn(deps, state, reprompt);
6146
6616
  if ("result" in t) return t.result;
6147
6617
  let verdict = null;
6148
6618
  if (t.dtmf === "1") verdict = "yes";
@@ -6170,7 +6640,7 @@ var init_executors = __esm({
6170
6640
  deps.events.note("confirm.unresolved", { node: node.id });
6171
6641
  return { kind: "advance" };
6172
6642
  };
6173
- askExecutor = async (node, state, deps) => {
6643
+ legacyAskExecutor = async (node, state, deps) => {
6174
6644
  const slot = str(node.config.slot) || node.id;
6175
6645
  const authoredSlot = slot === node.id ? "" : slot.trim();
6176
6646
  const label = authoredSlot.replace(/[_-]+/g, " ");
@@ -6211,6 +6681,165 @@ var init_executors = __esm({
6211
6681
  deps.events.note("ask.unfilled", { slot: slot.trim() });
6212
6682
  return { kind: "advance" };
6213
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;
6214
6843
  gateExecutor = async (node, _state, deps) => {
6215
6844
  const required = Array.isArray(node.config.requiredFields) ? node.config.requiredFields.filter((x) => typeof x === "string") : [];
6216
6845
  const data = deps.processRt.getData();
@@ -6233,7 +6862,7 @@ var init_executors = __esm({
6233
6862
  }
6234
6863
  const reason = str(cfg.reason) || `flow:${node.id}`;
6235
6864
  const mode = cfg.mode === "warm" || cfg.mode === "cold" ? cfg.mode : void 0;
6236
- 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;
6237
6866
  deps.events.note("handoff.start", { node: node.id, to: target, ...mode ? { mode } : {} });
6238
6867
  try {
6239
6868
  await deps.ctx.handoff(target, {
@@ -6337,9 +6966,17 @@ ${collected}`);
6337
6966
  const toolRef = str(cfg.toolRef);
6338
6967
  const registry = deps.resolvers?.registryTools;
6339
6968
  if (toolRef && registry) {
6340
- deps.events.note("tool.invoked", { node: node.id, name: toolRef, ref: true });
6341
6969
  try {
6342
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 });
6343
6980
  const slots2 = deps.processRt.getData();
6344
6981
  const bag2 = {};
6345
6982
  for (const field of Object.keys(spec?.input ?? {})) {
@@ -6372,6 +7009,8 @@ ${collected}`);
6372
7009
  deps.events.note("tool.unavailable", { node: node.id, name: toolRef });
6373
7010
  return routeByOutcome();
6374
7011
  }
7012
+ const blocked = blockOnMissingInputs(node, state, deps, requiredInputsOf(node, state, deps), true);
7013
+ if (blocked) return blocked;
6375
7014
  const url = str(cfg.url);
6376
7015
  if (!url) {
6377
7016
  state.toolResults[node.id] = { ok: false, status: 0, data: null };
@@ -6597,7 +7236,7 @@ ${collected}`);
6597
7236
  deps.events.note("menu.selected", { node: node.id, choice });
6598
7237
  return { kind: "take", port: choice };
6599
7238
  }
6600
- await speak(prompt, deps);
7239
+ await speak(isEnglishLanguage(deps.askContext?.language) && prompt ? notCaught(prompt) : prompt, deps);
6601
7240
  }
6602
7241
  }
6603
7242
  deps.events.note("menu.unmatched", { node: node.id });
@@ -6870,7 +7509,7 @@ ${collected}`);
6870
7509
  }
6871
7510
  await speak(
6872
7511
  interpolate(
6873
- 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,
6874
7513
  scopeFor(state, deps)
6875
7514
  ),
6876
7515
  deps
@@ -6926,6 +7565,7 @@ ${collected}`);
6926
7565
  ...deps.resolvers ? { resolvers: deps.resolvers } : {},
6927
7566
  ...deps.live ? { live: deps.live } : {},
6928
7567
  ...deps.security ? { security: deps.security } : {},
7568
+ ...deps.askContext ? { askContext: deps.askContext } : {},
6929
7569
  depth: depth + 1
6930
7570
  });
6931
7571
  deps.events.note("subflow.exit", { node: node.id, ref, outcome: outcome.kind });
@@ -6938,6 +7578,8 @@ ${collected}`);
6938
7578
  EMAIL_SEND_TIMEOUT_MS = 1e4;
6939
7579
  emailExecutor = async (node, state, deps) => {
6940
7580
  const cfg = node.config;
7581
+ const blocked = blockOnMissingInputs(node, state, deps, requiredInputsOf(node, state, deps), false);
7582
+ if (blocked) return blocked;
6941
7583
  const scope2 = scopeFor(state, deps);
6942
7584
  const render = (key) => interpolate(str(cfg[key]), scope2).trim();
6943
7585
  const to = render("to");
@@ -7047,6 +7689,12 @@ async function selectNext(model2, from, result, state, deps) {
7047
7689
  ...state.lastDtmf !== void 0 ? { lastDtmf: state.lastDtmf } : {}
7048
7690
  });
7049
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
+ }
7050
7698
  async function runFlowGraph(program, deps) {
7051
7699
  const model2 = buildGraphModel(program);
7052
7700
  const maxNodeVisits = deps.maxNodeVisits ?? DEFAULT_MAX_NODE_VISITS;
@@ -7059,6 +7707,7 @@ async function runFlowGraph(program, deps) {
7059
7707
  while (cursor !== null) {
7060
7708
  if (++steps > maxSteps) {
7061
7709
  deps.events.note("flow.loop_guard_tripped", { reason: "max_steps", steps });
7710
+ await closeAtLoopGuard(state, deps);
7062
7711
  break;
7063
7712
  }
7064
7713
  const node = model2.node(cursor);
@@ -7071,6 +7720,7 @@ async function runFlowGraph(program, deps) {
7071
7720
  const visitCap = node.config.hub === true ? HUB_MAX_NODE_VISITS : maxNodeVisits;
7072
7721
  if (visit > visitCap) {
7073
7722
  deps.events.note("flow.loop_guard_tripped", { reason: "max_node_visits", nodeId: cursor });
7723
+ await closeAtLoopGuard(state, deps);
7074
7724
  break;
7075
7725
  }
7076
7726
  state.cursor = cursor;
@@ -7091,7 +7741,8 @@ async function runFlowGraph(program, deps) {
7091
7741
  ...deps.depth !== void 0 ? { depth: deps.depth } : {},
7092
7742
  ...deps.busy ? { busy: deps.busy } : {},
7093
7743
  ...deps.live ? { live: deps.live } : {},
7094
- ...deps.security ? { security: deps.security } : {}
7744
+ ...deps.security ? { security: deps.security } : {},
7745
+ ...deps.askContext ? { askContext: deps.askContext } : {}
7095
7746
  });
7096
7747
  } catch (err) {
7097
7748
  const message = err instanceof Error ? err.message : String(err);
@@ -7124,6 +7775,7 @@ var init_interpreter = __esm({
7124
7775
  init_log();
7125
7776
  init_graph_model();
7126
7777
  init_executors();
7778
+ init_src();
7127
7779
  init_conditions();
7128
7780
  DEFAULT_MAX_NODE_VISITS = 25;
7129
7781
  DEFAULT_MAX_STEPS = 200;
@@ -7513,6 +8165,13 @@ function createUtteranceStream(session, processRt, ctx, rails, prewarm, wantsDtm
7513
8165
  ...number ? { numberField: number.field } : {},
7514
8166
  ...answer ? { answering: answer.field } : {}
7515
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
+ }
7516
8175
  if (opts?.deferCapture) {
7517
8176
  const run = capture(raw.text, true, step, answer).then(() => {
7518
8177
  if (pending === run) pending = null;
@@ -7686,7 +8345,8 @@ var NOT_SENT = /* @__PURE__ */ new Set([
7686
8345
  "email_provider_error",
7687
8346
  "email_send_failed",
7688
8347
  "insufficient_balance",
7689
- "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"
7690
8350
  ]);
7691
8351
  function dedupeConversationEmails(resolver) {
7692
8352
  const sends = /* @__PURE__ */ new Map();
@@ -7769,7 +8429,8 @@ async function runFlowProgram(program, ctx, opts) {
7769
8429
  ...opts.resolvers ? { resolvers: opts.resolvers } : {},
7770
8430
  ...opts.busy ? { busy: opts.busy } : {},
7771
8431
  ...opts.live ? { live: opts.live } : {},
7772
- ...security ? { security } : {}
8432
+ ...security ? { security } : {},
8433
+ ...opts.askContext ? { askContext: opts.askContext } : {}
7773
8434
  });
7774
8435
  graphLog("outcome", outcome);
7775
8436
  return outcome;
@@ -8256,14 +8917,15 @@ function createProcessRuntime(def) {
8256
8917
  console.error("[process] onField threw", err);
8257
8918
  }
8258
8919
  };
8259
- 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;
8260
8922
  const answerField = answering !== void 0 ? def.collect[answering] : void 0;
8261
8923
  const answerWasOpen = answering !== void 0 && !callerGave.has(answering);
8262
8924
  const status = primitive.getFieldStatus();
8263
8925
  const captured = (name) => status.find((f) => f.name === name)?.captured === true;
8264
8926
  const stepFields = opts?.stepFields;
8265
8927
  const named = (name) => stepFields?.some((f) => f.trim() === name.trim()) === true;
8266
- 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));
8267
8929
  const open = (name) => !captured(name) || named(name) && !callerGave.has(name);
8268
8930
  const remaining = spoken.filter(([name]) => open(name) && (!completed || stepFields === void 0 || named(name)));
8269
8931
  const correctable = spoken.filter(([name]) => captured(name) && named(name) && callerGave.has(name));
@@ -8801,13 +9463,13 @@ function createTextSessionAdapter(transport) {
8801
9463
  };
8802
9464
  function setState(next) {
8803
9465
  const raw = String(next);
8804
- const norm = normalizeState(raw);
9466
+ const norm2 = normalizeState(raw);
8805
9467
  const oldState = currentState;
8806
9468
  const rawOld = currentRaw;
8807
- currentState = norm;
9469
+ currentState = norm2;
8808
9470
  currentRaw = raw;
8809
- fire("state", { oldState, newState: norm, rawOldState: rawOld, rawNewState: raw });
8810
- 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");
8811
9473
  }
8812
9474
  function push(text, opts) {
8813
9475
  if (closed || text.length === 0) return;
@@ -8928,7 +9590,8 @@ function runTextSession(opts) {
8928
9590
  events,
8929
9591
  ...opts.maxSteps !== void 0 ? { maxSteps: opts.maxSteps } : {},
8930
9592
  ...opts.onAwaitInput ? { onAwaitInput: opts.onAwaitInput } : {},
8931
- ...opts.resolvers ? { resolvers: opts.resolvers } : {}
9593
+ ...opts.resolvers ? { resolvers: opts.resolvers } : {},
9594
+ ...opts.askContext ? { askContext: opts.askContext } : {}
8932
9595
  });
8933
9596
  return {
8934
9597
  sendUserMessage: (text) => {