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