@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
|
-
//
|
|
1065
|
-
//
|
|
1066
|
-
//
|
|
1067
|
-
//
|
|
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
|
-
|
|
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(
|
|
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
|
|
4426
|
+
var TOKEN2;
|
|
4023
4427
|
var init_interpolate = __esm({
|
|
4024
4428
|
"src/runtime/graph/interpolate.ts"() {
|
|
4025
|
-
|
|
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
|
-
|
|
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) =>
|
|
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
|
|
5839
|
+
var QUESTION_MARK2, POLITE_REQUEST, YES_NO;
|
|
5450
5840
|
var init_turn_text = __esm({
|
|
5451
5841
|
"src/runtime/graph/turn-text.ts"() {
|
|
5452
|
-
|
|
5453
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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),
|
|
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) ||
|
|
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
|
-
|
|
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
|
|
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
|
|
9466
|
+
const norm2 = normalizeState(raw);
|
|
8805
9467
|
const oldState = currentState;
|
|
8806
9468
|
const rawOld = currentRaw;
|
|
8807
|
-
currentState =
|
|
9469
|
+
currentState = norm2;
|
|
8808
9470
|
currentRaw = raw;
|
|
8809
|
-
fire("state", { oldState, newState:
|
|
8810
|
-
if (NON_BUSY_STATES.has(
|
|
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) => {
|