@voicelayer/sdk 0.6.1 → 0.6.2
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.js +155 -162
- package/dist/index.d.ts +454 -6
- package/dist/index.js +809 -363
- package/dist/runtime/text-session.d.ts +1 -1
- package/dist/runtime/text-session.js +178 -171
- package/dist/{text-session-C4Z59gG4.d.ts → text-session-B-T33y5P.d.ts} +8 -2
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -750,7 +750,7 @@ var init_consultation = __esm({
|
|
|
750
750
|
});
|
|
751
751
|
}
|
|
752
752
|
});
|
|
753
|
-
var CALL_CONTROL_PUBSUB_CHANNEL, CALL_CONTROL_ACK_CHANNEL, CALL_CONTROL_READY_KEY, CallEventKind, OP_CALL_EVENT_KIND, OpCallEventKind, commandId, SayCommand, HangupCommand, DtmfCommand, InstructCommand, InjectContextCommand, CallControlCommand;
|
|
753
|
+
var CALL_CONTROL_PUBSUB_CHANNEL, CALL_CONTROL_ACK_CHANNEL, CALL_CONTROL_READY_KEY, CallEventKind, OP_CALL_EVENT_KIND, OpCallEventKind, LatencyMs, TURN_LATENCY_MAX_PER_CALL, commandId, SayCommand, HangupCommand, DtmfCommand, InstructCommand, InjectContextCommand, CallControlCommand;
|
|
754
754
|
var init_call_events = __esm({
|
|
755
755
|
"../contracts/src/call-events.ts"() {
|
|
756
756
|
init_consultation();
|
|
@@ -779,7 +779,9 @@ var init_call_events = __esm({
|
|
|
779
779
|
"dtmf.sent",
|
|
780
780
|
// A host's runtime intervention (burn-down G-6): call_say / call_send_guidance / call_inject_context / call_instruct,
|
|
781
781
|
// with masked args and the API key — written by the API only, never through the worker's /events endpoint.
|
|
782
|
-
"mcp.interaction"
|
|
782
|
+
"mcp.interaction",
|
|
783
|
+
// One per caller turn the agent answered (burn-down G-44): where that turn's wait went — see TurnLatencyPayload.
|
|
784
|
+
"turn.latency"
|
|
783
785
|
]);
|
|
784
786
|
OP_CALL_EVENT_KIND = /^(tool|handoff|lookup|record|notify|engine)\.[a-z0-9_]{1,48}(\.[a-z0-9_]{1,48})?$/;
|
|
785
787
|
OpCallEventKind = z.string().regex(OP_CALL_EVENT_KIND);
|
|
@@ -806,6 +808,18 @@ var init_call_events = __esm({
|
|
|
806
808
|
code: z.number().int().min(0).max(15),
|
|
807
809
|
participantId: z.string().optional()
|
|
808
810
|
});
|
|
811
|
+
LatencyMs = z.number().int().min(0).max(6e5).nullable();
|
|
812
|
+
TURN_LATENCY_MAX_PER_CALL = 500;
|
|
813
|
+
z.object({
|
|
814
|
+
turn: z.number().int().min(1),
|
|
815
|
+
atMs: z.number().int().min(0),
|
|
816
|
+
endpointingMs: LatencyMs,
|
|
817
|
+
sttMs: LatencyMs,
|
|
818
|
+
llmMs: LatencyMs,
|
|
819
|
+
ttsMs: LatencyMs,
|
|
820
|
+
endToEndMs: LatencyMs,
|
|
821
|
+
realtime: z.boolean().optional()
|
|
822
|
+
});
|
|
809
823
|
z.object({
|
|
810
824
|
totalUsd: z.string(),
|
|
811
825
|
breakdown: z.object({
|
|
@@ -924,9 +938,68 @@ var init_text_turns = __esm({
|
|
|
924
938
|
]);
|
|
925
939
|
}
|
|
926
940
|
});
|
|
941
|
+
var PLATFORM_MAX_CALL_MINUTES, SIP_LEG_MAX_CALL_DURATION_SECONDS, INBOUND_REFUSED_MESSAGE, Message, CallerRateLimit, CallLimits, CallerChannel, CallDurationLimits, INBOUND_REFUSED_END_REASON_PREFIX, CallAdmissionResponse;
|
|
942
|
+
var init_call_limits = __esm({
|
|
943
|
+
"../contracts/src/call-limits.ts"() {
|
|
944
|
+
PLATFORM_MAX_CALL_MINUTES = 240;
|
|
945
|
+
SIP_LEG_MAX_CALL_DURATION_SECONDS = (PLATFORM_MAX_CALL_MINUTES + 15) * 60;
|
|
946
|
+
INBOUND_REFUSED_MESSAGE = "Sorry, we can't take your call right now. Please try again later. Goodbye.";
|
|
947
|
+
Message = z.string().trim().min(1).max(300);
|
|
948
|
+
CallerRateLimit = z.object({
|
|
949
|
+
enabled: z.boolean().optional(),
|
|
950
|
+
perTenMinutes: z.number().int().min(1).max(100).optional(),
|
|
951
|
+
perDay: z.number().int().min(1).max(1e3).optional(),
|
|
952
|
+
withheldPerTenMinutes: z.number().int().min(1).max(100).optional(),
|
|
953
|
+
withheldPerDay: z.number().int().min(1).max(1e3).optional()
|
|
954
|
+
});
|
|
955
|
+
CallLimits = z.object({
|
|
956
|
+
/** The agent's own ceiling in minutes; clamped to the workspace's plan ceiling. Absent ⇒ DEFAULT_MAX_CALL_MINUTES. */
|
|
957
|
+
maxDurationMinutes: z.number().int().min(1).max(PLATFORM_MAX_CALL_MINUTES).optional(),
|
|
958
|
+
/** Seconds before the limit the wrap-up line is spoken; 0 ⇒ no warning. */
|
|
959
|
+
wrapUpWarningSeconds: z.number().int().min(0).max(300).optional(),
|
|
960
|
+
wrapUpMessage: Message.optional(),
|
|
961
|
+
goodbyeMessage: Message.optional(),
|
|
962
|
+
callerRateLimit: CallerRateLimit.optional(),
|
|
963
|
+
rateLimitedMessage: Message.optional()
|
|
964
|
+
});
|
|
965
|
+
CallerChannel = z.enum(["phone", "web"]);
|
|
966
|
+
z.object({
|
|
967
|
+
roomName: z.string().min(1).max(256),
|
|
968
|
+
/** From the dispatch metadata: lets the API admit (gate) a room whose call row doesn't exist yet. */
|
|
969
|
+
agentId: z.string().max(128).optional(),
|
|
970
|
+
phoneNumberId: z.string().max(128).optional(),
|
|
971
|
+
caller: z.object({
|
|
972
|
+
channel: CallerChannel,
|
|
973
|
+
/** The caller's number as the carrier presented it; null / absent / not E.164 ⇒ the withheld bucket. */
|
|
974
|
+
number: z.string().max(64).nullable().optional()
|
|
975
|
+
})
|
|
976
|
+
});
|
|
977
|
+
CallDurationLimits = z.object({
|
|
978
|
+
/** The agent's own ceiling (already clamped to the plan), or null when it set none. */
|
|
979
|
+
agentMaxSeconds: z.number().int().min(1).nullable(),
|
|
980
|
+
/** The plan ceiling — the platform bound no call outlives. */
|
|
981
|
+
ceilingSeconds: z.number().int().min(1),
|
|
982
|
+
wrapUpWarningSeconds: z.number().int().min(0),
|
|
983
|
+
wrapUpMessage: z.string(),
|
|
984
|
+
goodbyeMessage: z.string()
|
|
985
|
+
});
|
|
986
|
+
INBOUND_REFUSED_END_REASON_PREFIX = "inbound_refused:";
|
|
987
|
+
CallAdmissionResponse = z.discriminatedUnion("admitted", [
|
|
988
|
+
z.object({ admitted: z.literal(true), limits: CallDurationLimits }),
|
|
989
|
+
z.object({
|
|
990
|
+
admitted: z.literal(false),
|
|
991
|
+
/** 'rate_limited:caller' | 'inbound_refused:<code>' — already recorded as the call's end reason. */
|
|
992
|
+
endReason: z.string(),
|
|
993
|
+
/** The fixed line to speak before hanging up. */
|
|
994
|
+
message: z.string()
|
|
995
|
+
})
|
|
996
|
+
]);
|
|
997
|
+
}
|
|
998
|
+
});
|
|
927
999
|
var VoiceConfig, ModelConfig, SttConfig, RealtimeConfig, PipelineMode, ToolBinding, ProcessFieldType, ProcessFieldSpec, ProcessCompleteWhen, ProcessBackendAckSpec, ProcessSpec, TriggerConditionSpec, TriggerActionSpec, TriggerSpec, RequiredInfoField, AgentConfig;
|
|
928
1000
|
var init_agent_config = __esm({
|
|
929
1001
|
"../contracts/src/agent-config.ts"() {
|
|
1002
|
+
init_call_limits();
|
|
930
1003
|
init_consultation();
|
|
931
1004
|
VoiceConfig = z.object({
|
|
932
1005
|
// Finite, vetted list — platform-level enum.
|
|
@@ -1090,6 +1163,9 @@ var init_agent_config = __esm({
|
|
|
1090
1163
|
// LLM slot through the connector instead of a first-party model. Null/absent →
|
|
1091
1164
|
// first-party pipeline (unchanged default). See docs/specs/voice-brain-connector.
|
|
1092
1165
|
brainConnectorId: z.string().uuid().nullable().optional(),
|
|
1166
|
+
// Per-agent call bounds: caller rate limit, max duration and their spoken lines (burn-down G-41, G-43). Absent ⇒
|
|
1167
|
+
// every default (call-limits.ts) — the rate limit is ON by default.
|
|
1168
|
+
callLimits: CallLimits.optional(),
|
|
1093
1169
|
// Provenance: 'sdk' (hand-coded defineAgent worker), 'flow' (canvas deploy),
|
|
1094
1170
|
// 'playbook' (agent-mode deploy), or 'connector' (brain-connector agent).
|
|
1095
1171
|
// DERIVED from the linked agents row's deploy tags (flowId / playbookId /
|
|
@@ -1445,6 +1521,7 @@ var init_agent_versions = __esm({
|
|
|
1445
1521
|
"../contracts/src/agent-versions.ts"() {
|
|
1446
1522
|
init_agent_config();
|
|
1447
1523
|
init_agents();
|
|
1524
|
+
init_call_limits();
|
|
1448
1525
|
init_consultation();
|
|
1449
1526
|
z.object({
|
|
1450
1527
|
systemPrompt: z.string().max(64e3),
|
|
@@ -1459,6 +1536,8 @@ var init_agent_versions = __esm({
|
|
|
1459
1536
|
consultation: ConsultationPolicy,
|
|
1460
1537
|
tags: z.array(z.string().max(64)).max(32).nullable(),
|
|
1461
1538
|
brainConnectorId: z.string().uuid().nullable(),
|
|
1539
|
+
// Versions published before call limits existed carry none: read as null (every default).
|
|
1540
|
+
callLimits: CallLimits.nullable().default(null),
|
|
1462
1541
|
flowVersion: z.number().int().nullable(),
|
|
1463
1542
|
processSchema: ProcessSchemaDTO.nullable()
|
|
1464
1543
|
});
|
|
@@ -1494,6 +1573,7 @@ var init_agent_versions = __esm({
|
|
|
1494
1573
|
"language",
|
|
1495
1574
|
"tools",
|
|
1496
1575
|
"brainConnectorId",
|
|
1576
|
+
"callLimits",
|
|
1497
1577
|
"consultation",
|
|
1498
1578
|
"tags",
|
|
1499
1579
|
"flowVersion",
|
|
@@ -1635,6 +1715,9 @@ var init_model_catalog = __esm({
|
|
|
1635
1715
|
CostEstimateBilledPerMinute = z.object({
|
|
1636
1716
|
platformFee: z.number().nonnegative(),
|
|
1637
1717
|
providerPassthrough: z.number().nonnegative(),
|
|
1718
|
+
// The flat own-key fee per minute (burn-down B-26); 0 when every provider runs on the platform's keys. Optional
|
|
1719
|
+
// only for responses from an API older than the fee.
|
|
1720
|
+
ownKeyFee: z.number().nonnegative().optional(),
|
|
1638
1721
|
telephony: z.number().nonnegative(),
|
|
1639
1722
|
allIn: z.number().nonnegative()
|
|
1640
1723
|
});
|
|
@@ -1900,9 +1983,46 @@ var init_call_review = __esm({
|
|
|
1900
1983
|
});
|
|
1901
1984
|
}
|
|
1902
1985
|
});
|
|
1986
|
+
var CAPTURED_FIELDS_MAX, CAPTURED_STRING_MAX_CHARS, CAPTURED_ARRAY_MAX_ITEMS, CapturedFieldValue, CapturedField, StructuredOutputs;
|
|
1987
|
+
var init_call_outcome = __esm({
|
|
1988
|
+
"../contracts/src/call-outcome.ts"() {
|
|
1989
|
+
init_agent_config();
|
|
1990
|
+
CAPTURED_FIELDS_MAX = 200;
|
|
1991
|
+
CAPTURED_STRING_MAX_CHARS = 8e3;
|
|
1992
|
+
CAPTURED_ARRAY_MAX_ITEMS = 100;
|
|
1993
|
+
CapturedFieldValue = z.union([
|
|
1994
|
+
z.string().max(CAPTURED_STRING_MAX_CHARS),
|
|
1995
|
+
z.number().finite(),
|
|
1996
|
+
z.boolean(),
|
|
1997
|
+
z.array(z.string().max(CAPTURED_STRING_MAX_CHARS)).max(CAPTURED_ARRAY_MAX_ITEMS),
|
|
1998
|
+
z.null()
|
|
1999
|
+
]);
|
|
2000
|
+
CapturedField = z.object({
|
|
2001
|
+
name: z.string().min(1).max(64),
|
|
2002
|
+
type: ProcessFieldType,
|
|
2003
|
+
required: z.boolean(),
|
|
2004
|
+
// false ⇒ the call never captured it; `value` is then null
|
|
2005
|
+
filled: z.boolean(),
|
|
2006
|
+
value: CapturedFieldValue
|
|
2007
|
+
});
|
|
2008
|
+
StructuredOutputs = z.record(CapturedFieldValue);
|
|
2009
|
+
z.object({
|
|
2010
|
+
// Every declared field, in declaration order; empty when the agent declares none.
|
|
2011
|
+
fields: z.array(CapturedField).max(CAPTURED_FIELDS_MAX)
|
|
2012
|
+
}).superRefine((body, ctx) => {
|
|
2013
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2014
|
+
for (const f of body.fields) {
|
|
2015
|
+
if (seen.has(f.name)) ctx.addIssue({ code: z.ZodIssueCode.custom, message: `duplicate field ${f.name}` });
|
|
2016
|
+
seen.add(f.name);
|
|
2017
|
+
if (!f.filled && f.value !== null) ctx.addIssue({ code: z.ZodIssueCode.custom, message: `${f.name}: an unfilled field has no value` });
|
|
2018
|
+
}
|
|
2019
|
+
});
|
|
2020
|
+
}
|
|
2021
|
+
});
|
|
1903
2022
|
var TranscriptTurn, ConsultationAudit, ToolCallAudit, McpInteractionAudit;
|
|
1904
2023
|
var init_call_result = __esm({
|
|
1905
2024
|
"../contracts/src/call-result.ts"() {
|
|
2025
|
+
init_call_outcome();
|
|
1906
2026
|
TranscriptTurn = z.object({
|
|
1907
2027
|
speaker: z.enum(["caller", "agent"]),
|
|
1908
2028
|
text: z.string(),
|
|
@@ -1951,9 +2071,13 @@ var init_call_result = __esm({
|
|
|
1951
2071
|
startedAt: z.string().datetime(),
|
|
1952
2072
|
endedAt: z.string().datetime(),
|
|
1953
2073
|
durationMs: z.number().int().min(0),
|
|
2074
|
+
// Final segments only, in call order.
|
|
1954
2075
|
transcript: z.array(TranscriptTurn),
|
|
1955
|
-
//
|
|
1956
|
-
|
|
2076
|
+
// The filled fields of the agent's process / flow, keyed by name (burn-down G-20). Absent when the agent declares no
|
|
2077
|
+
// fields, or its worker never reported them.
|
|
2078
|
+
structuredOutputs: StructuredOutputs.optional(),
|
|
2079
|
+
// Every declared field with what the call captured and whether it was filled — the same list `call.ended` carries.
|
|
2080
|
+
fields: z.array(CapturedField).optional(),
|
|
1957
2081
|
recordingUrl: z.string().url().optional(),
|
|
1958
2082
|
cost: z.object({
|
|
1959
2083
|
totalUsd: z.string(),
|
|
@@ -1965,9 +2089,11 @@ var init_call_result = __esm({
|
|
|
1965
2089
|
});
|
|
1966
2090
|
}
|
|
1967
2091
|
});
|
|
1968
|
-
var WEBHOOK_EVENT_TYPES, CallWebhookDirection, CallWebhookOutcome, CallWebhookAttributes, Party;
|
|
2092
|
+
var WEBHOOK_EVENT_TYPES, CallWebhookDirection, CallWebhookOutcome, CallWebhookAttributes, Party, CALL_ENDED_PAYLOAD_VERSION, CallEndedTranscript;
|
|
1969
2093
|
var init_webhook_events = __esm({
|
|
1970
2094
|
"../contracts/src/webhook-events.ts"() {
|
|
2095
|
+
init_call_result();
|
|
2096
|
+
init_call_outcome();
|
|
1971
2097
|
WEBHOOK_EVENT_TYPES = ["call.started", "call.ended", "recording.ready"];
|
|
1972
2098
|
z.enum(WEBHOOK_EVENT_TYPES);
|
|
1973
2099
|
CallWebhookDirection = z.enum(["inbound", "outbound", "web"]);
|
|
@@ -1985,7 +2111,18 @@ var init_webhook_events = __esm({
|
|
|
1985
2111
|
// set, it is the same value `call.ended` carries; `call.ended` always has one.
|
|
1986
2112
|
direction: CallWebhookDirection.nullable()
|
|
1987
2113
|
});
|
|
2114
|
+
CALL_ENDED_PAYLOAD_VERSION = 2;
|
|
2115
|
+
CallEndedTranscript = z.object({
|
|
2116
|
+
// Final segments only (what was said, never an interim STT guess), in call order. Empty when `truncated`.
|
|
2117
|
+
turns: z.array(TranscriptTurn),
|
|
2118
|
+
// How many final turns the call has — the length of `turns` unless truncated.
|
|
2119
|
+
turnCount: z.number().int().min(0),
|
|
2120
|
+
truncated: z.boolean(),
|
|
2121
|
+
// Where the whole transcript is: GET /v1/calls/:callId/result (its `transcript`). Always set.
|
|
2122
|
+
fetchPath: z.string().regex(/^\/v1\/calls\/[0-9a-f-]{36}\/result$/)
|
|
2123
|
+
});
|
|
1988
2124
|
z.object({
|
|
2125
|
+
version: z.literal(CALL_ENDED_PAYLOAD_VERSION),
|
|
1989
2126
|
callId: z.string().uuid(),
|
|
1990
2127
|
durationMs: z.number().int().min(0),
|
|
1991
2128
|
endedAt: z.string().datetime(),
|
|
@@ -1996,7 +2133,20 @@ var init_webhook_events = __esm({
|
|
|
1996
2133
|
toE164: Party,
|
|
1997
2134
|
direction: CallWebhookDirection,
|
|
1998
2135
|
outcome: CallWebhookOutcome,
|
|
1999
|
-
attributes: CallWebhookAttributes
|
|
2136
|
+
attributes: CallWebhookAttributes,
|
|
2137
|
+
// Every field the agent declared (its process, or a flow's slots), in declaration order, with what the call captured
|
|
2138
|
+
// and whether it was filled. Empty when the agent declares none.
|
|
2139
|
+
fields: z.array(CapturedField),
|
|
2140
|
+
// The filled fields as one object keyed by name (as GET /v1/calls/:id/result returns them). Absent when the agent
|
|
2141
|
+
// declares no fields.
|
|
2142
|
+
structuredOutputs: StructuredOutputs.optional(),
|
|
2143
|
+
transcript: CallEndedTranscript,
|
|
2144
|
+
// true: the agent's worker confirmed its last transcript segment and captured field had landed before this payload
|
|
2145
|
+
// was built. false: built without that word (a worker that crashed or runs an SDK before 0.6.2, or one that didn't
|
|
2146
|
+
// answer within the hold) — the transcript and fields are what had arrived; GET /v1/calls/:id/result has any later.
|
|
2147
|
+
finalized: z.boolean(),
|
|
2148
|
+
// true: the payload would have passed CALL_ENDED_PAYLOAD_MAX_BYTES, so values were left out (see the cap) — fetch them.
|
|
2149
|
+
valuesOmitted: z.boolean()
|
|
2000
2150
|
});
|
|
2001
2151
|
z.object({
|
|
2002
2152
|
callId: z.string().uuid(),
|
|
@@ -2942,166 +3092,6 @@ var init_telephony = __esm({
|
|
|
2942
3092
|
});
|
|
2943
3093
|
}
|
|
2944
3094
|
});
|
|
2945
|
-
var SHORTENER_HOSTS, PLACEHOLDER_HOSTS, PublicUrl, A2pBusinessType, A2pBusinessInfo, A2pOptInType, A2pUseCase, A2pMessagingProfile, A2pState;
|
|
2946
|
-
var init_a2p = __esm({
|
|
2947
|
-
"../contracts/src/a2p.ts"() {
|
|
2948
|
-
SHORTENER_HOSTS = [
|
|
2949
|
-
"bit.ly",
|
|
2950
|
-
"tinyurl.com",
|
|
2951
|
-
"t.co",
|
|
2952
|
-
"goo.gl",
|
|
2953
|
-
"ow.ly",
|
|
2954
|
-
"is.gd",
|
|
2955
|
-
"buff.ly",
|
|
2956
|
-
"rebrand.ly",
|
|
2957
|
-
"cutt.ly",
|
|
2958
|
-
"shorturl.at",
|
|
2959
|
-
"tiny.cc"
|
|
2960
|
-
];
|
|
2961
|
-
PLACEHOLDER_HOSTS = ["acme.com", "example.com", "example.org", "test.com", "localhost"];
|
|
2962
|
-
PublicUrl = z.string().url().refine((v) => v.startsWith("https://"), "must be https").refine((v) => {
|
|
2963
|
-
try {
|
|
2964
|
-
const host = new URL(v).hostname.toLowerCase().replace(/^www\./, "");
|
|
2965
|
-
return !SHORTENER_HOSTS.includes(host);
|
|
2966
|
-
} catch {
|
|
2967
|
-
return false;
|
|
2968
|
-
}
|
|
2969
|
-
}, "public URL shorteners are rejected by carriers \u2014 use your own domain").refine((v) => {
|
|
2970
|
-
try {
|
|
2971
|
-
const host = new URL(v).hostname.toLowerCase().replace(/^www\./, "");
|
|
2972
|
-
return !PLACEHOLDER_HOSTS.some((p) => host === p || host.endsWith(`.${p}`));
|
|
2973
|
-
} catch {
|
|
2974
|
-
return false;
|
|
2975
|
-
}
|
|
2976
|
-
}, "placeholder domain \u2014 reviewers will follow this link and reject the campaign");
|
|
2977
|
-
A2pBusinessType = z.enum([
|
|
2978
|
-
"Sole Proprietorship",
|
|
2979
|
-
"Partnership",
|
|
2980
|
-
"Corporation",
|
|
2981
|
-
"Co-operative",
|
|
2982
|
-
"Limited Liability Corporation",
|
|
2983
|
-
"Non-profit Corporation"
|
|
2984
|
-
]);
|
|
2985
|
-
A2pBusinessInfo = z.object({
|
|
2986
|
-
legalName: z.string().trim().min(2).max(200),
|
|
2987
|
-
/** EIN (US) or equivalent registration number. */
|
|
2988
|
-
registrationNumber: z.string().trim().min(4).max(50),
|
|
2989
|
-
businessType: A2pBusinessType,
|
|
2990
|
-
/** Publicly reachable production site — not staging, not a 404. */
|
|
2991
|
-
website: PublicUrl,
|
|
2992
|
-
industry: z.string().trim().min(2).max(60),
|
|
2993
|
-
address: z.object({
|
|
2994
|
-
street: z.string().trim().min(2).max(200),
|
|
2995
|
-
city: z.string().trim().min(1).max(100),
|
|
2996
|
-
region: z.string().trim().min(1).max(100),
|
|
2997
|
-
postalCode: z.string().trim().min(2).max(20),
|
|
2998
|
-
isoCountry: z.string().trim().length(2)
|
|
2999
|
-
}),
|
|
3000
|
-
authorizedRep: z.object({
|
|
3001
|
-
firstName: z.string().trim().min(1).max(100),
|
|
3002
|
-
lastName: z.string().trim().min(1).max(100),
|
|
3003
|
-
email: z.string().trim().email(),
|
|
3004
|
-
phone: z.string().trim().regex(/^\+[1-9]\d{6,14}$/, "must be E.164"),
|
|
3005
|
-
jobTitle: z.string().trim().min(2).max(100)
|
|
3006
|
-
})
|
|
3007
|
-
});
|
|
3008
|
-
A2pOptInType = z.enum(["WEB_FORM", "PAPER_FORM", "VERBAL", "VIA_TEXT", "MOBILE_QR_CODE"]);
|
|
3009
|
-
A2pUseCase = z.enum([
|
|
3010
|
-
"MIXED",
|
|
3011
|
-
"CUSTOMER_CARE",
|
|
3012
|
-
"MARKETING",
|
|
3013
|
-
"ACCOUNT_NOTIFICATION",
|
|
3014
|
-
"2FA",
|
|
3015
|
-
"DELIVERY_NOTIFICATION",
|
|
3016
|
-
"HIGHER_EDUCATION",
|
|
3017
|
-
"POLLING_VOTING",
|
|
3018
|
-
"PUBLIC_SERVICE_ANNOUNCEMENT",
|
|
3019
|
-
"LOW_VOLUME"
|
|
3020
|
-
]);
|
|
3021
|
-
A2pMessagingProfile = z.object({
|
|
3022
|
-
useCase: A2pUseCase,
|
|
3023
|
-
/** Specific, not generic. "We send texts" gets rejected; describe the actual
|
|
3024
|
-
* messages and when they are sent. */
|
|
3025
|
-
description: z.string().trim().min(40).max(4096),
|
|
3026
|
-
/**
|
|
3027
|
-
* The single most-rejected field. Must describe HOW people opt in, state the
|
|
3028
|
-
* message frequency, include the "message and data rates may apply"
|
|
3029
|
-
* disclosure, and link to publicly reachable evidence. Twilio's API bounds it
|
|
3030
|
-
* to 40–2049 characters.
|
|
3031
|
-
*/
|
|
3032
|
-
messageFlow: z.string().trim().min(40).max(2049),
|
|
3033
|
-
optInType: A2pOptInType,
|
|
3034
|
-
/** Publicly accessible screenshots/pages showing the opt-in. Reviewers open
|
|
3035
|
-
* these; anything behind a login fails. */
|
|
3036
|
-
optInEvidenceUrls: z.array(PublicUrl).min(1).max(5),
|
|
3037
|
-
/** Real messages the customer will send. Must reflect the declared use case
|
|
3038
|
-
* and carry opt-out language. */
|
|
3039
|
-
messageSamples: z.array(z.string().trim().min(10).max(1024)).min(2).max(5),
|
|
3040
|
-
privacyPolicyUrl: PublicUrl,
|
|
3041
|
-
termsAndConditionsUrl: PublicUrl,
|
|
3042
|
-
hasEmbeddedLinks: z.boolean().default(false),
|
|
3043
|
-
hasEmbeddedPhone: z.boolean().default(false)
|
|
3044
|
-
});
|
|
3045
|
-
z.object({
|
|
3046
|
-
business: A2pBusinessInfo,
|
|
3047
|
-
messaging: A2pMessagingProfile,
|
|
3048
|
-
/** Register against Twilio's mock endpoints — exercises the full pipeline
|
|
3049
|
-
* with no fees and no real carrier submission. Used in CI and staging. */
|
|
3050
|
-
mock: z.boolean().default(false)
|
|
3051
|
-
}).superRefine((v, ctx) => {
|
|
3052
|
-
const flow = v.messaging.messageFlow.toLowerCase();
|
|
3053
|
-
if (!/(msg|message)\s*(&|and)\s*data rates/.test(flow)) {
|
|
3054
|
-
ctx.addIssue({
|
|
3055
|
-
code: z.ZodIssueCode.custom,
|
|
3056
|
-
path: ["messaging", "messageFlow"],
|
|
3057
|
-
message: 'must include a "Message and data rates may apply" disclosure \u2014 carriers reject without it'
|
|
3058
|
-
});
|
|
3059
|
-
}
|
|
3060
|
-
if (!/\d/.test(flow) || !/(msg|message|text)/.test(flow)) {
|
|
3061
|
-
ctx.addIssue({
|
|
3062
|
-
code: z.ZodIssueCode.custom,
|
|
3063
|
-
path: ["messaging", "messageFlow"],
|
|
3064
|
-
message: 'must state message frequency, e.g. "Up to 4 msgs/month"'
|
|
3065
|
-
});
|
|
3066
|
-
}
|
|
3067
|
-
const hasOptOut = v.messaging.messageSamples.some((s) => /stop/i.test(s));
|
|
3068
|
-
if (!hasOptOut) {
|
|
3069
|
-
ctx.addIssue({
|
|
3070
|
-
code: z.ZodIssueCode.custom,
|
|
3071
|
-
path: ["messaging", "messageSamples"],
|
|
3072
|
-
message: 'at least one sample must include opt-out language (e.g. "Reply STOP to opt out")'
|
|
3073
|
-
});
|
|
3074
|
-
}
|
|
3075
|
-
});
|
|
3076
|
-
A2pState = z.enum([
|
|
3077
|
-
"none",
|
|
3078
|
-
"profile_pending",
|
|
3079
|
-
"profile_approved",
|
|
3080
|
-
"profile_failed",
|
|
3081
|
-
"brand_pending",
|
|
3082
|
-
"brand_approved",
|
|
3083
|
-
"brand_failed",
|
|
3084
|
-
"campaign_pending",
|
|
3085
|
-
"messaging_ready",
|
|
3086
|
-
"campaign_failed"
|
|
3087
|
-
]);
|
|
3088
|
-
z.object({
|
|
3089
|
-
state: A2pState,
|
|
3090
|
-
customerProfileSid: z.string().nullable(),
|
|
3091
|
-
trustProductSid: z.string().nullable(),
|
|
3092
|
-
brandSid: z.string().nullable(),
|
|
3093
|
-
messagingServiceSid: z.string().nullable(),
|
|
3094
|
-
campaignSid: z.string().nullable(),
|
|
3095
|
-
mock: z.boolean(),
|
|
3096
|
-
/** Carrier/Twilio rejection details, surfaced verbatim so the customer can
|
|
3097
|
-
* fix the specific field rather than guess. */
|
|
3098
|
-
failures: z.array(z.object({ code: z.number().nullable(), field: z.string().nullable(), message: z.string() })).default([]),
|
|
3099
|
-
/** Plain-language next step for the dashboard. */
|
|
3100
|
-
nextAction: z.string().nullable(),
|
|
3101
|
-
updatedAt: z.string().nullable()
|
|
3102
|
-
});
|
|
3103
|
-
}
|
|
3104
|
-
});
|
|
3105
3095
|
var ConnectorMode, ConnectorStatus, ConnectorNormalize;
|
|
3106
3096
|
var init_connector = __esm({
|
|
3107
3097
|
"../contracts/src/connector.ts"() {
|
|
@@ -3218,7 +3208,7 @@ var init_connector_stream = __esm({
|
|
|
3218
3208
|
]);
|
|
3219
3209
|
}
|
|
3220
3210
|
});
|
|
3221
|
-
var FLOW_BOOT_FAILURES;
|
|
3211
|
+
var FLOW_BOOT_FAILURES, AGENT_END_REASONS;
|
|
3222
3212
|
var init_call_end_reason = __esm({
|
|
3223
3213
|
"../contracts/src/call-end-reason.ts"() {
|
|
3224
3214
|
FLOW_BOOT_FAILURES = [
|
|
@@ -3229,9 +3219,11 @@ var init_call_end_reason = __esm({
|
|
|
3229
3219
|
"worker_identity_refused",
|
|
3230
3220
|
"provider_unavailable"
|
|
3231
3221
|
];
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3222
|
+
AGENT_END_REASONS = [
|
|
3223
|
+
"max_duration",
|
|
3224
|
+
...FLOW_BOOT_FAILURES.map((cause) => `flow_boot:${cause}`)
|
|
3225
|
+
];
|
|
3226
|
+
z.enum(AGENT_END_REASONS);
|
|
3235
3227
|
}
|
|
3236
3228
|
});
|
|
3237
3229
|
var EnvironmentSpec;
|
|
@@ -4427,6 +4419,23 @@ var init_wallet = __esm({
|
|
|
4427
4419
|
});
|
|
4428
4420
|
}
|
|
4429
4421
|
});
|
|
4422
|
+
var PipelineSlotKey;
|
|
4423
|
+
var init_pipeline_keys = __esm({
|
|
4424
|
+
"../contracts/src/pipeline-keys.ts"() {
|
|
4425
|
+
PipelineSlotKey = z.object({
|
|
4426
|
+
/** The provider as the slot reports it (LiveKit's label: `openai`, `api.openai.com`, `Deepgram`, …). */
|
|
4427
|
+
provider: z.string().min(1).max(200),
|
|
4428
|
+
/** true = the workspace's own key built it; false = the platform's. */
|
|
4429
|
+
byok: z.boolean()
|
|
4430
|
+
});
|
|
4431
|
+
z.object({
|
|
4432
|
+
llm: PipelineSlotKey.optional(),
|
|
4433
|
+
stt: PipelineSlotKey.optional(),
|
|
4434
|
+
tts: PipelineSlotKey.optional(),
|
|
4435
|
+
realtime: PipelineSlotKey.optional()
|
|
4436
|
+
});
|
|
4437
|
+
}
|
|
4438
|
+
});
|
|
4430
4439
|
|
|
4431
4440
|
// ../contracts/src/index.ts
|
|
4432
4441
|
var init_src = __esm({
|
|
@@ -4456,6 +4465,7 @@ var init_src = __esm({
|
|
|
4456
4465
|
init_tests();
|
|
4457
4466
|
init_call_review();
|
|
4458
4467
|
init_call_result();
|
|
4468
|
+
init_call_outcome();
|
|
4459
4469
|
init_webhook_events();
|
|
4460
4470
|
init_numbers();
|
|
4461
4471
|
init_project_limits();
|
|
@@ -4471,12 +4481,12 @@ var init_src = __esm({
|
|
|
4471
4481
|
init_connection();
|
|
4472
4482
|
init_secret_headers();
|
|
4473
4483
|
init_telephony();
|
|
4474
|
-
init_a2p();
|
|
4475
4484
|
init_connector();
|
|
4476
4485
|
init_connector_stream();
|
|
4477
4486
|
init_flow_compile();
|
|
4478
4487
|
init_flow_compile_check();
|
|
4479
4488
|
init_call_end_reason();
|
|
4489
|
+
init_call_limits();
|
|
4480
4490
|
init_flow_enrichment();
|
|
4481
4491
|
init_environments();
|
|
4482
4492
|
init_variables();
|
|
@@ -4492,6 +4502,7 @@ var init_src = __esm({
|
|
|
4492
4502
|
init_wallet();
|
|
4493
4503
|
init_network_address();
|
|
4494
4504
|
init_dispatch_metadata();
|
|
4505
|
+
init_pipeline_keys();
|
|
4495
4506
|
}
|
|
4496
4507
|
});
|
|
4497
4508
|
|
|
@@ -11984,6 +11995,9 @@ var MemoryClient = class {
|
|
|
11984
11995
|
);
|
|
11985
11996
|
}
|
|
11986
11997
|
};
|
|
11998
|
+
|
|
11999
|
+
// src/calls.ts
|
|
12000
|
+
init_src();
|
|
11987
12001
|
var CallSchema = z.object({
|
|
11988
12002
|
id: z.string(),
|
|
11989
12003
|
projectId: z.string(),
|
|
@@ -12042,6 +12056,7 @@ z.object({
|
|
|
12042
12056
|
});
|
|
12043
12057
|
var CallStateSyncResponse = z.object({ ok: z.literal(true) }).or(z.object({}).passthrough());
|
|
12044
12058
|
var DtmfEventResponse = z.object({ accepted: z.literal(true) }).or(z.object({}).passthrough());
|
|
12059
|
+
var CallFinalizeResponse = z.object({ ok: z.boolean() }).or(z.object({}).passthrough());
|
|
12045
12060
|
var UsageReportResponse = z.object({ recorded: z.boolean() }).or(z.object({}).passthrough());
|
|
12046
12061
|
var CallEventEnvelopeResponse = z.object({
|
|
12047
12062
|
event: z.object({
|
|
@@ -12080,13 +12095,28 @@ var CallsClient = class {
|
|
|
12080
12095
|
...input.attributes !== void 0 ? { attributes: input.attributes } : {},
|
|
12081
12096
|
...input.participants !== void 0 ? { participants: input.participants } : {},
|
|
12082
12097
|
...input.legs !== void 0 ? { legs: input.legs } : {},
|
|
12083
|
-
...input.startedAt !== void 0 ? { startedAt: input.startedAt } : {}
|
|
12098
|
+
...input.startedAt !== void 0 ? { startedAt: input.startedAt } : {},
|
|
12099
|
+
...input.pipelineKeys !== void 0 ? { pipelineKeys: input.pipelineKeys } : {},
|
|
12100
|
+
...input.finalizes !== void 0 ? { finalizes: input.finalizes } : {}
|
|
12084
12101
|
}
|
|
12085
12102
|
},
|
|
12086
12103
|
CallByRoomResponse
|
|
12087
12104
|
);
|
|
12088
12105
|
return call;
|
|
12089
12106
|
}
|
|
12107
|
+
/**
|
|
12108
|
+
* The call's admission (burn-down G-41/G-42/G-43), asked once right after the caller joins and before the agent
|
|
12109
|
+
* speaks: admitted with its duration limits, or refused with the end reason the platform recorded and the line to
|
|
12110
|
+
* speak before hanging up. Throws on a transport / server error — the runtime retries, then refuses (fail closed).
|
|
12111
|
+
*/
|
|
12112
|
+
async admit(input) {
|
|
12113
|
+
return this.transport.request(
|
|
12114
|
+
// The runtime owns the retries (bounded, then a polite refusal): a server error comes straight back, and one
|
|
12115
|
+
// attempt never holds a caller longer than 2.5 s.
|
|
12116
|
+
{ method: "POST", path: "/v1/calls/admission", body: input, noRetryStatuses: [500, 502, 503, 504], signal: AbortSignal.timeout(2500) },
|
|
12117
|
+
CallAdmissionResponse
|
|
12118
|
+
);
|
|
12119
|
+
}
|
|
12090
12120
|
async syncState(callId, input) {
|
|
12091
12121
|
await this.transport.request(
|
|
12092
12122
|
{
|
|
@@ -12120,11 +12150,12 @@ var CallsClient = class {
|
|
|
12120
12150
|
}
|
|
12121
12151
|
// One-shot cascade usage report at session close — the platform prices it
|
|
12122
12152
|
// into the call's bill (voice_components). Idempotent server-side.
|
|
12123
|
-
async reportUsage(callId, report) {
|
|
12153
|
+
async reportUsage(callId, report, opts = {}) {
|
|
12124
12154
|
await this.transport.request(
|
|
12125
12155
|
{
|
|
12126
12156
|
method: "POST",
|
|
12127
12157
|
path: `/v1/calls/${encodeURIComponent(callId)}/usage-report`,
|
|
12158
|
+
...opts.signal ? { signal: opts.signal } : {},
|
|
12128
12159
|
body: {
|
|
12129
12160
|
durationMs: report.durationMs,
|
|
12130
12161
|
llm: report.llm,
|
|
@@ -12135,6 +12166,22 @@ var CallsClient = class {
|
|
|
12135
12166
|
UsageReportResponse
|
|
12136
12167
|
);
|
|
12137
12168
|
}
|
|
12169
|
+
/**
|
|
12170
|
+
* The worker's last word on a call (burn-down G-20): what the call captured — every declared field — sent at the
|
|
12171
|
+
* session's close after the worker's other writes for the call have landed. The API keeps the fields on the call
|
|
12172
|
+
* and releases the call's held `call.ended` webhook. Idempotent.
|
|
12173
|
+
*/
|
|
12174
|
+
async finalize(callId, input, opts = {}) {
|
|
12175
|
+
await this.transport.request(
|
|
12176
|
+
{
|
|
12177
|
+
method: "POST",
|
|
12178
|
+
path: `/v1/calls/${encodeURIComponent(callId)}/finalize`,
|
|
12179
|
+
...opts.signal ? { signal: opts.signal } : {},
|
|
12180
|
+
body: { fields: input.fields }
|
|
12181
|
+
},
|
|
12182
|
+
CallFinalizeResponse
|
|
12183
|
+
);
|
|
12184
|
+
}
|
|
12138
12185
|
async appendDtmfEvent(callId, input) {
|
|
12139
12186
|
await this.transport.request(
|
|
12140
12187
|
{
|
|
@@ -13304,6 +13351,38 @@ function buildRedisBrainPubSub(redisUrl) {
|
|
|
13304
13351
|
// src/runtime/pipeline-from-config.ts
|
|
13305
13352
|
init_providers2();
|
|
13306
13353
|
init_registry();
|
|
13354
|
+
|
|
13355
|
+
// src/runtime/pipeline-key-owner.ts
|
|
13356
|
+
var vaultKeyed = /* @__PURE__ */ new WeakSet();
|
|
13357
|
+
var isObject = (v) => v !== null && (typeof v === "object" || typeof v === "function");
|
|
13358
|
+
function vaultKeyedFactory(factory) {
|
|
13359
|
+
return async (call) => {
|
|
13360
|
+
const provider = await factory(call);
|
|
13361
|
+
if (isObject(provider)) vaultKeyed.add(provider);
|
|
13362
|
+
return provider;
|
|
13363
|
+
};
|
|
13364
|
+
}
|
|
13365
|
+
function builtWithVaultKey(provider) {
|
|
13366
|
+
return isObject(provider) && vaultKeyed.has(provider);
|
|
13367
|
+
}
|
|
13368
|
+
function providerLabel(provider) {
|
|
13369
|
+
const label = isObject(provider) ? provider.provider : void 0;
|
|
13370
|
+
return typeof label === "string" && label.length > 0 ? label : "unknown";
|
|
13371
|
+
}
|
|
13372
|
+
function pipelineKeys(pipeline, envKeyIsCustomers) {
|
|
13373
|
+
const slot = (provider) => ({
|
|
13374
|
+
provider: providerLabel(provider),
|
|
13375
|
+
byok: builtWithVaultKey(provider) || envKeyIsCustomers
|
|
13376
|
+
});
|
|
13377
|
+
return {
|
|
13378
|
+
...pipeline.llm != null ? { llm: slot(pipeline.llm) } : {},
|
|
13379
|
+
...pipeline.stt != null ? { stt: slot(pipeline.stt) } : {},
|
|
13380
|
+
...pipeline.tts != null ? { tts: slot(pipeline.tts) } : {},
|
|
13381
|
+
...pipeline.realtime != null ? { realtime: slot(pipeline.realtime) } : {}
|
|
13382
|
+
};
|
|
13383
|
+
}
|
|
13384
|
+
|
|
13385
|
+
// src/runtime/pipeline-from-config.ts
|
|
13307
13386
|
function modelsFromWireConfig(p, creds2) {
|
|
13308
13387
|
const language = p.language ?? "en-US";
|
|
13309
13388
|
if (p.mode === "realtime" && p.realtime) {
|
|
@@ -13317,17 +13396,20 @@ function modelsFromWireConfig(p, creds2) {
|
|
|
13317
13396
|
out.tts = ttsFactory(p.voice.provider, p.voice.voiceId, p.voice.model, creds2?.[p.voice.provider]);
|
|
13318
13397
|
return out;
|
|
13319
13398
|
}
|
|
13399
|
+
function owned(factory, creds2) {
|
|
13400
|
+
return creds2 ? vaultKeyedFactory(factory) : factory;
|
|
13401
|
+
}
|
|
13320
13402
|
function realtimeFactory(r, creds2) {
|
|
13321
|
-
return resolveRealtime(r.provider, { model: r.model, ...r.voice ? { voice: r.voice } : {}, ...creds2 ? { creds: creds2 } : {} });
|
|
13403
|
+
return owned(resolveRealtime(r.provider, { model: r.model, ...r.voice ? { voice: r.voice } : {}, ...creds2 ? { creds: creds2 } : {} }), creds2);
|
|
13322
13404
|
}
|
|
13323
13405
|
function llmFactory(provider, model2, creds2) {
|
|
13324
|
-
return resolveLlm(provider, { model: model2, ...creds2 ? { creds: creds2 } : {} });
|
|
13406
|
+
return owned(resolveLlm(provider, { model: model2, ...creds2 ? { creds: creds2 } : {} }), creds2);
|
|
13325
13407
|
}
|
|
13326
13408
|
function sttFactory(provider, model2, language, creds2) {
|
|
13327
|
-
return resolveStt(provider, { ...model2 ? { model: model2 } : {}, language, ...creds2 ? { creds: creds2 } : {} });
|
|
13409
|
+
return owned(resolveStt(provider, { ...model2 ? { model: model2 } : {}, language, ...creds2 ? { creds: creds2 } : {} }), creds2);
|
|
13328
13410
|
}
|
|
13329
13411
|
function ttsFactory(provider, voiceId, model2, creds2) {
|
|
13330
|
-
return resolveTts(provider, { voice: voiceId, ...model2 ? { model: model2 } : {}, ...creds2 ? { creds: creds2 } : {} });
|
|
13412
|
+
return owned(resolveTts(provider, { voice: voiceId, ...model2 ? { model: model2 } : {}, ...creds2 ? { creds: creds2 } : {} }), creds2);
|
|
13331
13413
|
}
|
|
13332
13414
|
|
|
13333
13415
|
// src/agent.ts
|
|
@@ -15513,6 +15595,9 @@ function extractText2(msg) {
|
|
|
15513
15595
|
const trimmed = txt.trim();
|
|
15514
15596
|
return trimmed.length === 0 ? null : trimmed;
|
|
15515
15597
|
}
|
|
15598
|
+
|
|
15599
|
+
// src/runtime/handoff.ts
|
|
15600
|
+
init_src();
|
|
15516
15601
|
function createHandoffRuntime(config, deps) {
|
|
15517
15602
|
return {
|
|
15518
15603
|
async handoff(to, opts) {
|
|
@@ -15661,7 +15746,9 @@ function buildDefaultSipOps(trunkOverride, fromNumber) {
|
|
|
15661
15746
|
participantIdentity: identity,
|
|
15662
15747
|
playDialtone: false,
|
|
15663
15748
|
waitUntilAnswered: true,
|
|
15664
|
-
...fromNumber ? { fromNumber } : {}
|
|
15749
|
+
...fromNumber ? { fromNumber } : {},
|
|
15750
|
+
// LiveKit's own ceiling on the transfer leg (burn-down G-43): it can't outlive the platform's longest call.
|
|
15751
|
+
maxCallDuration: SIP_LEG_MAX_CALL_DURATION_SECONDS
|
|
15665
15752
|
});
|
|
15666
15753
|
return { identity };
|
|
15667
15754
|
}
|
|
@@ -16185,16 +16272,16 @@ async function exchangeWorkerToken(input) {
|
|
|
16185
16272
|
const internalToken = process.env["INTERNAL_SERVICE_TOKEN"];
|
|
16186
16273
|
if (!apiUrl || !internalToken) return null;
|
|
16187
16274
|
const delays = input.retryDelaysMs ?? EXCHANGE_RETRY_DELAYS_MS;
|
|
16188
|
-
const
|
|
16275
|
+
const deadline2 = Date.now() + (input.budgetMs ?? EXCHANGE_BUDGET_MS);
|
|
16189
16276
|
for (let attempt = 0; ; attempt++) {
|
|
16190
|
-
const left =
|
|
16277
|
+
const left = deadline2 - Date.now();
|
|
16191
16278
|
if (left <= 0) return null;
|
|
16192
16279
|
const r = await exchangeOnce(input, apiUrl, internalToken, Math.min(input.attemptTimeoutMs ?? EXCHANGE_ATTEMPT_TIMEOUT_MS, left));
|
|
16193
16280
|
if ("token" in r) return r.token;
|
|
16194
16281
|
if ("stale" in r) throw new StaleRouteError(r.stale);
|
|
16195
16282
|
const base = delays[attempt];
|
|
16196
16283
|
const wait = base === void 0 ? void 0 : Math.max(base, "retryAfterMs" in r && r.retryAfterMs !== void 0 ? r.retryAfterMs : 0);
|
|
16197
|
-
if (r.final || wait === void 0 || Date.now() + wait >=
|
|
16284
|
+
if (r.final || wait === void 0 || Date.now() + wait >= deadline2) return null;
|
|
16198
16285
|
await new Promise((resolve) => setTimeout(resolve, wait));
|
|
16199
16286
|
}
|
|
16200
16287
|
}
|
|
@@ -16393,7 +16480,7 @@ async function answerTextTurn(deps) {
|
|
|
16393
16480
|
);
|
|
16394
16481
|
const brain = pipeline?.brain === "bound";
|
|
16395
16482
|
const label = pipeline?.llm ?? { provider: "openai", model: model2.model };
|
|
16396
|
-
const byok =
|
|
16483
|
+
const byok = builtWithVaultKey(model2);
|
|
16397
16484
|
const usage = brain ? [{ provider: "brain", model: model2.model || "brain", inputTokens: 0, outputTokens: 0, byok: true }] : [];
|
|
16398
16485
|
const onMetrics = (m) => {
|
|
16399
16486
|
if (brain) return;
|
|
@@ -17040,8 +17127,18 @@ function guardIsActive(opts) {
|
|
|
17040
17127
|
return false;
|
|
17041
17128
|
}
|
|
17042
17129
|
|
|
17130
|
+
// src/runtime/close-deadline.ts
|
|
17131
|
+
var CLOSE_REQUEST_TIMEOUT_MS = 5e3;
|
|
17132
|
+
function deadline(ms) {
|
|
17133
|
+
const controller = new AbortController();
|
|
17134
|
+
const timer = setTimeout(() => controller.abort(new Error(`timed out after ${ms} ms`)), ms);
|
|
17135
|
+
timer.unref?.();
|
|
17136
|
+
return { signal: controller.signal, clear: () => clearTimeout(timer) };
|
|
17137
|
+
}
|
|
17138
|
+
|
|
17043
17139
|
// src/runtime/call-sync.ts
|
|
17044
17140
|
var END_REASON_WAIT_MS = 2e3;
|
|
17141
|
+
var FINALIZE_WRITES_WAIT_MS = 3e3;
|
|
17045
17142
|
function makeNoopHandle(callId) {
|
|
17046
17143
|
return {
|
|
17047
17144
|
callId,
|
|
@@ -17053,6 +17150,13 @@ function makeNoopHandle(callId) {
|
|
|
17053
17150
|
publishDtmf() {
|
|
17054
17151
|
},
|
|
17055
17152
|
async recordEndReason() {
|
|
17153
|
+
},
|
|
17154
|
+
publishTurnLatency() {
|
|
17155
|
+
},
|
|
17156
|
+
track(write) {
|
|
17157
|
+
return write;
|
|
17158
|
+
},
|
|
17159
|
+
async finalize() {
|
|
17056
17160
|
}
|
|
17057
17161
|
};
|
|
17058
17162
|
}
|
|
@@ -17065,6 +17169,16 @@ async function createCallSyncHandle(init) {
|
|
|
17065
17169
|
});
|
|
17066
17170
|
return makeNoopHandle(init.call.callId);
|
|
17067
17171
|
}
|
|
17172
|
+
const inFlight = /* @__PURE__ */ new Set();
|
|
17173
|
+
const track = (write) => {
|
|
17174
|
+
const settled = write.then(
|
|
17175
|
+
() => void 0,
|
|
17176
|
+
() => void 0
|
|
17177
|
+
);
|
|
17178
|
+
inFlight.add(settled);
|
|
17179
|
+
void settled.then(() => inFlight.delete(settled));
|
|
17180
|
+
return write;
|
|
17181
|
+
};
|
|
17068
17182
|
return {
|
|
17069
17183
|
callId,
|
|
17070
17184
|
resolved: true,
|
|
@@ -17074,13 +17188,13 @@ async function createCallSyncHandle(init) {
|
|
|
17074
17188
|
startMs,
|
|
17075
17189
|
(input.endAt ?? input.at).getTime() - init.call.startedAt.getTime()
|
|
17076
17190
|
);
|
|
17077
|
-
void init.client.calls.appendTranscriptSegment(callId, {
|
|
17191
|
+
void track(init.client.calls.appendTranscriptSegment(callId, {
|
|
17078
17192
|
speaker: input.speaker,
|
|
17079
17193
|
text: input.text,
|
|
17080
17194
|
startMs,
|
|
17081
17195
|
endMs,
|
|
17082
17196
|
final: input.final
|
|
17083
|
-
}).catch((err) => {
|
|
17197
|
+
})).catch((err) => {
|
|
17084
17198
|
console.warn("[sdk.call-sync] segment publish failed", {
|
|
17085
17199
|
speaker: input.speaker,
|
|
17086
17200
|
err: err instanceof Error ? err.message : err
|
|
@@ -17088,12 +17202,43 @@ async function createCallSyncHandle(init) {
|
|
|
17088
17202
|
});
|
|
17089
17203
|
},
|
|
17090
17204
|
syncState(input) {
|
|
17091
|
-
void init.client.calls.syncState(callId, input).catch((err) => {
|
|
17205
|
+
void track(init.client.calls.syncState(callId, input)).catch((err) => {
|
|
17092
17206
|
console.warn("[sdk.call-sync] state sync failed", {
|
|
17093
17207
|
err: err instanceof Error ? err.message : err
|
|
17094
17208
|
});
|
|
17095
17209
|
});
|
|
17096
17210
|
},
|
|
17211
|
+
publishTurnLatency(payload) {
|
|
17212
|
+
void track(init.client.calls.appendEvent(callId, { kind: "turn.latency", payload: { ...payload } })).catch((err) => {
|
|
17213
|
+
console.warn("[sdk.call-sync] turn latency publish failed", {
|
|
17214
|
+
turn: payload.turn,
|
|
17215
|
+
err: err instanceof Error ? err.message : err
|
|
17216
|
+
});
|
|
17217
|
+
});
|
|
17218
|
+
},
|
|
17219
|
+
track,
|
|
17220
|
+
async finalize(fields) {
|
|
17221
|
+
let timer;
|
|
17222
|
+
try {
|
|
17223
|
+
await Promise.race([
|
|
17224
|
+
Promise.all([...inFlight]),
|
|
17225
|
+
new Promise((resolve) => {
|
|
17226
|
+
timer = setTimeout(resolve, FINALIZE_WRITES_WAIT_MS);
|
|
17227
|
+
})
|
|
17228
|
+
]);
|
|
17229
|
+
clearTimeout(timer);
|
|
17230
|
+
const limit = deadline(CLOSE_REQUEST_TIMEOUT_MS);
|
|
17231
|
+
try {
|
|
17232
|
+
await init.client.calls.finalize(callId, { fields: [...fields] }, { signal: limit.signal });
|
|
17233
|
+
} finally {
|
|
17234
|
+
limit.clear();
|
|
17235
|
+
}
|
|
17236
|
+
} catch (err) {
|
|
17237
|
+
console.warn("[sdk.call-sync] finalize failed", { err: err instanceof Error ? err.message : err });
|
|
17238
|
+
} finally {
|
|
17239
|
+
clearTimeout(timer);
|
|
17240
|
+
}
|
|
17241
|
+
},
|
|
17097
17242
|
async recordEndReason(reason) {
|
|
17098
17243
|
let timer;
|
|
17099
17244
|
try {
|
|
@@ -17113,7 +17258,7 @@ async function createCallSyncHandle(init) {
|
|
|
17113
17258
|
}
|
|
17114
17259
|
},
|
|
17115
17260
|
publishDtmf(input) {
|
|
17116
|
-
void init.client.calls.appendDtmfEvent(callId, input).catch((err) => {
|
|
17261
|
+
void track(init.client.calls.appendDtmfEvent(callId, input)).catch((err) => {
|
|
17117
17262
|
console.warn("[sdk.call-sync] dtmf publish failed", {
|
|
17118
17263
|
direction: input.direction,
|
|
17119
17264
|
digit: input.digit,
|
|
@@ -17138,7 +17283,10 @@ async function resolveCallIdWithRetry(init) {
|
|
|
17138
17283
|
attributes: initialAttributes(init.call),
|
|
17139
17284
|
participants: initialParticipants(init.call),
|
|
17140
17285
|
legs: initialLegs(init.call, init.roomName),
|
|
17141
|
-
startedAt: init.call.startedAt.toISOString()
|
|
17286
|
+
startedAt: init.call.startedAt.toISOString(),
|
|
17287
|
+
...init.pipelineKeys ? { pipelineKeys: init.pipelineKeys } : {},
|
|
17288
|
+
// this worker sends POST /v1/calls/:id/finalize at the close: the API holds call.ended for it (G-20)
|
|
17289
|
+
finalizes: true
|
|
17142
17290
|
});
|
|
17143
17291
|
return summary.id;
|
|
17144
17292
|
} catch {
|
|
@@ -17208,7 +17356,7 @@ function initialLegs(call, roomName) {
|
|
|
17208
17356
|
startedAt
|
|
17209
17357
|
}
|
|
17210
17358
|
];
|
|
17211
|
-
if (destination) {
|
|
17359
|
+
if (destination && call.metadata["direction"] !== "outbound") {
|
|
17212
17360
|
legs.unshift({
|
|
17213
17361
|
legKey: "primary-inbound",
|
|
17214
17362
|
kind: "sip-inbound",
|
|
@@ -17228,28 +17376,29 @@ function looksLikeSipParticipant(value) {
|
|
|
17228
17376
|
|
|
17229
17377
|
// src/runtime/metrics-bridge.ts
|
|
17230
17378
|
init_dist2();
|
|
17379
|
+
var payerKey = (byok) => byok === void 0 ? "" : byok ? "byok" : "platform";
|
|
17231
17380
|
var UsageAccumulator = class {
|
|
17232
17381
|
llm = /* @__PURE__ */ new Map();
|
|
17233
17382
|
tts = /* @__PURE__ */ new Map();
|
|
17234
17383
|
stt = /* @__PURE__ */ new Map();
|
|
17235
|
-
// `byok` keeps a line whose payer is known (the SDK's helper calls) apart from LiveKit's own line on the same model,
|
|
17236
|
-
// which the API prices by the workspace's vault keys.
|
|
17237
17384
|
addLlm(provider, model2, inputTokens, outputTokens, byok) {
|
|
17238
|
-
const key = `${provider}:${model2}:${byok
|
|
17385
|
+
const key = `${provider}:${model2}:${payerKey(byok)}`;
|
|
17239
17386
|
const entry2 = this.llm.get(key) ?? { provider, model: model2, inputTokens: 0, outputTokens: 0, ...byok !== void 0 ? { byok } : {} };
|
|
17240
17387
|
entry2.inputTokens += Math.max(0, inputTokens || 0);
|
|
17241
17388
|
entry2.outputTokens += Math.max(0, outputTokens || 0);
|
|
17242
17389
|
this.llm.set(key, entry2);
|
|
17243
17390
|
}
|
|
17244
|
-
addTts(provider, chars) {
|
|
17245
|
-
const
|
|
17391
|
+
addTts(provider, chars, byok) {
|
|
17392
|
+
const key = `${provider}:${payerKey(byok)}`;
|
|
17393
|
+
const entry2 = this.tts.get(key) ?? { provider, chars: 0, ...byok !== void 0 ? { byok } : {} };
|
|
17246
17394
|
entry2.chars += Math.max(0, chars || 0);
|
|
17247
|
-
this.tts.set(
|
|
17395
|
+
this.tts.set(key, entry2);
|
|
17248
17396
|
}
|
|
17249
|
-
addStt(provider, seconds) {
|
|
17250
|
-
const
|
|
17397
|
+
addStt(provider, seconds, byok) {
|
|
17398
|
+
const key = `${provider}:${payerKey(byok)}`;
|
|
17399
|
+
const entry2 = this.stt.get(key) ?? { provider, seconds: 0, ...byok !== void 0 ? { byok } : {} };
|
|
17251
17400
|
entry2.seconds += Math.max(0, seconds || 0);
|
|
17252
|
-
this.stt.set(
|
|
17401
|
+
this.stt.set(key, entry2);
|
|
17253
17402
|
}
|
|
17254
17403
|
isEmpty() {
|
|
17255
17404
|
return this.llm.size === 0 && this.tts.size === 0 && this.stt.size === 0;
|
|
@@ -17302,10 +17451,13 @@ function attachEndpointingProbe(session, events, ctx, deps) {
|
|
|
17302
17451
|
});
|
|
17303
17452
|
recordTurnLatencySpan({ ...base, stage: "endpointing", latencyMs: delayMs });
|
|
17304
17453
|
recordTurnLatencySpan({ ...base, stage: "stt", latencyMs: delayMs });
|
|
17454
|
+
ctx.turns?.note({ stage: "endpointing", latencyMs: delayMs });
|
|
17455
|
+
ctx.turns?.note({ stage: "stt", latencyMs: delayMs });
|
|
17305
17456
|
};
|
|
17306
17457
|
deps.vad.onSpeechEnd((sample) => {
|
|
17307
17458
|
if (!armed) return;
|
|
17308
17459
|
speechEndAt = sample.speechEndAt;
|
|
17460
|
+
ctx.turns?.noteSpeechEnd(sample.speechEndAt);
|
|
17309
17461
|
tryEmit();
|
|
17310
17462
|
});
|
|
17311
17463
|
session.on(events.userStateChanged, (event) => {
|
|
@@ -17347,7 +17499,8 @@ function dispatch(metrics2, ctx) {
|
|
|
17347
17499
|
provider,
|
|
17348
17500
|
...model2 ? { model: model2 } : {},
|
|
17349
17501
|
inputTokens: m.promptTokens,
|
|
17350
|
-
outputTokens: m.completionTokens
|
|
17502
|
+
outputTokens: m.completionTokens,
|
|
17503
|
+
...ctx.keyOwner?.llm ? { byok: ctx.keyOwner.llm.byok } : {}
|
|
17351
17504
|
});
|
|
17352
17505
|
recordLlmUsage({
|
|
17353
17506
|
...base,
|
|
@@ -17363,6 +17516,7 @@ function dispatch(metrics2, ctx) {
|
|
|
17363
17516
|
} else if (typeof m.ttftMs === "number" && m.ttftMs >= 0) {
|
|
17364
17517
|
recordTurnLatency({ ...base, firstTokenMs: m.ttftMs });
|
|
17365
17518
|
recordTurnLatencySpan({ ...base, stage: "llm", latencyMs: m.ttftMs });
|
|
17519
|
+
ctx.turns?.note({ stage: "llm", latencyMs: m.ttftMs, startedAt: m.timestamp - Math.max(0, m.durationMs) });
|
|
17366
17520
|
}
|
|
17367
17521
|
break;
|
|
17368
17522
|
}
|
|
@@ -17370,7 +17524,7 @@ function dispatch(metrics2, ctx) {
|
|
|
17370
17524
|
const m = metrics2;
|
|
17371
17525
|
const provider = m.metadata?.modelProvider ?? "unknown";
|
|
17372
17526
|
const model2 = m.metadata?.modelName;
|
|
17373
|
-
ctx.usage?.record({ kind: "tts", provider, chars: m.charactersCount });
|
|
17527
|
+
ctx.usage?.record({ kind: "tts", provider, chars: m.charactersCount, ...ctx.keyOwner?.tts ? { byok: ctx.keyOwner.tts.byok } : {} });
|
|
17374
17528
|
recordTtsUsage({
|
|
17375
17529
|
...base,
|
|
17376
17530
|
provider,
|
|
@@ -17380,6 +17534,7 @@ function dispatch(metrics2, ctx) {
|
|
|
17380
17534
|
if (typeof m.ttfbMs === "number" && m.ttfbMs >= 0) {
|
|
17381
17535
|
recordTurnLatency({ ...base, ttsStartMs: m.ttfbMs });
|
|
17382
17536
|
recordTurnLatencySpan({ ...base, stage: "tts", latencyMs: m.ttfbMs });
|
|
17537
|
+
ctx.turns?.note({ stage: "tts", latencyMs: m.ttfbMs, startedAt: m.timestamp - Math.max(0, m.durationMs) });
|
|
17383
17538
|
}
|
|
17384
17539
|
break;
|
|
17385
17540
|
}
|
|
@@ -17387,7 +17542,12 @@ function dispatch(metrics2, ctx) {
|
|
|
17387
17542
|
const m = metrics2;
|
|
17388
17543
|
const provider = m.metadata?.modelProvider ?? "unknown";
|
|
17389
17544
|
const model2 = m.metadata?.modelName;
|
|
17390
|
-
ctx.usage?.record({
|
|
17545
|
+
ctx.usage?.record({
|
|
17546
|
+
kind: "stt",
|
|
17547
|
+
provider,
|
|
17548
|
+
seconds: (m.audioDurationMs ?? 0) / 1e3,
|
|
17549
|
+
...ctx.keyOwner?.stt ? { byok: ctx.keyOwner.stt.byok } : {}
|
|
17550
|
+
});
|
|
17391
17551
|
recordSttUsage({
|
|
17392
17552
|
...base,
|
|
17393
17553
|
provider,
|
|
@@ -17410,6 +17570,7 @@ function dispatch(metrics2, ctx) {
|
|
|
17410
17570
|
if (typeof m.ttftMs === "number" && m.ttftMs >= 0) {
|
|
17411
17571
|
recordTurnLatency({ ...base, firstTokenMs: m.ttftMs });
|
|
17412
17572
|
recordTurnLatencySpan({ ...base, stage: "llm", latencyMs: m.ttftMs, realtime: true });
|
|
17573
|
+
ctx.turns?.note({ stage: "llm", latencyMs: m.ttftMs, startedAt: m.timestamp, realtime: true });
|
|
17413
17574
|
}
|
|
17414
17575
|
if (typeof m.sessionDurationMs === "number" && m.sessionDurationMs > 0) {
|
|
17415
17576
|
recordRealtimeSessionDuration({
|
|
@@ -17431,6 +17592,11 @@ function dispatch(metrics2, ctx) {
|
|
|
17431
17592
|
});
|
|
17432
17593
|
recordTurnLatencySpan({ ...base, stage: "endpointing", latencyMs: m.endOfUtteranceDelayMs });
|
|
17433
17594
|
recordTurnLatencySpan({ ...base, stage: "stt", latencyMs: m.transcriptionDelayMs });
|
|
17595
|
+
if (ctx.turns) {
|
|
17596
|
+
if (m.endOfUtteranceDelayMs > 0) ctx.turns.note({ stage: "endpointing", latencyMs: m.endOfUtteranceDelayMs });
|
|
17597
|
+
if (m.transcriptionDelayMs > 0) ctx.turns.note({ stage: "stt", latencyMs: m.transcriptionDelayMs });
|
|
17598
|
+
if (m.lastSpeakingTimeMs > 0) ctx.turns.noteSpeechEnd(m.lastSpeakingTimeMs);
|
|
17599
|
+
}
|
|
17434
17600
|
break;
|
|
17435
17601
|
}
|
|
17436
17602
|
case "interruption_metrics": {
|
|
@@ -17468,10 +17634,10 @@ function createTelemetrySink(accumulator = new UsageAccumulator()) {
|
|
|
17468
17634
|
);
|
|
17469
17635
|
break;
|
|
17470
17636
|
case "tts":
|
|
17471
|
-
accumulator.addTts(sample.provider, sample.chars ?? 0);
|
|
17637
|
+
accumulator.addTts(sample.provider, sample.chars ?? 0, sample.byok);
|
|
17472
17638
|
break;
|
|
17473
17639
|
case "stt":
|
|
17474
|
-
accumulator.addStt(sample.provider, sample.seconds ?? 0);
|
|
17640
|
+
accumulator.addStt(sample.provider, sample.seconds ?? 0, sample.byok);
|
|
17475
17641
|
break;
|
|
17476
17642
|
}
|
|
17477
17643
|
},
|
|
@@ -17554,7 +17720,12 @@ async function reportUsageAtClose(opts) {
|
|
|
17554
17720
|
try {
|
|
17555
17721
|
await opts.meter.drain(opts.drainMs ?? HANGUP_DRAIN_MS);
|
|
17556
17722
|
if (opts.usage.isEmpty()) return;
|
|
17557
|
-
|
|
17723
|
+
const limit = deadline(CLOSE_REQUEST_TIMEOUT_MS);
|
|
17724
|
+
try {
|
|
17725
|
+
await opts.send(opts.usage.report(opts.durationMs), limit.signal);
|
|
17726
|
+
} finally {
|
|
17727
|
+
limit.clear();
|
|
17728
|
+
}
|
|
17558
17729
|
} catch (err) {
|
|
17559
17730
|
opts.warn?.("[agent] usage report failed", { err: err instanceof Error ? err.message : String(err) });
|
|
17560
17731
|
}
|
|
@@ -17603,6 +17774,7 @@ function attachTurnModelClock(session, events, ctx, deps = {}) {
|
|
|
17603
17774
|
safely2(() => {
|
|
17604
17775
|
recordTurnLatencySpan({ ...base, stage: "llm", latencyMs, allowZero: true });
|
|
17605
17776
|
if (latencyMs > 0) recordTurnLatency({ ...base, firstTokenMs: latencyMs });
|
|
17777
|
+
deps.turns?.note({ stage: "llm", latencyMs, replyAt: t.windowEnd });
|
|
17606
17778
|
});
|
|
17607
17779
|
};
|
|
17608
17780
|
const releaseParked = () => {
|
|
@@ -17719,6 +17891,145 @@ function attachTurnModelClock(session, events, ctx, deps = {}) {
|
|
|
17719
17891
|
};
|
|
17720
17892
|
}
|
|
17721
17893
|
|
|
17894
|
+
// src/runtime/turn-latency.ts
|
|
17895
|
+
init_src();
|
|
17896
|
+
var MAX_TURN_LATENCY_EVENTS = TURN_LATENCY_MAX_PER_CALL;
|
|
17897
|
+
var MAX_STAGE_MS = 6e5;
|
|
17898
|
+
var noStages = () => ({ endpointingMs: null, sttMs: null, llmMs: null, ttsMs: null, realtime: false });
|
|
17899
|
+
function attachTurnLatencyRecorder(session, events, deps) {
|
|
17900
|
+
const now = deps.now ?? Date.now;
|
|
17901
|
+
const maxTurns = deps.maxTurns ?? MAX_TURN_LATENCY_EVENTS;
|
|
17902
|
+
let waiting = null;
|
|
17903
|
+
let open = null;
|
|
17904
|
+
let turns = 0;
|
|
17905
|
+
let lastReplyAt = -Infinity;
|
|
17906
|
+
let lastSpeechEndAt = null;
|
|
17907
|
+
let closed = false;
|
|
17908
|
+
const ms = (v) => v === null || !Number.isFinite(v) || v < 0 || v > MAX_STAGE_MS ? null : Math.round(v);
|
|
17909
|
+
const emit = (t) => {
|
|
17910
|
+
const endpointingMs = ms(t.endpointingMs);
|
|
17911
|
+
const llmMs = ms(t.llmMs);
|
|
17912
|
+
const ttsMs = ms(t.ttsMs);
|
|
17913
|
+
const measured = t.speechEndAt !== null ? ms(t.replyAt - t.speechEndAt) : null;
|
|
17914
|
+
const summed = endpointingMs !== null && llmMs !== null && ttsMs !== null ? ms(endpointingMs + llmMs + ttsMs) : null;
|
|
17915
|
+
try {
|
|
17916
|
+
deps.emit({
|
|
17917
|
+
turn: t.index,
|
|
17918
|
+
atMs: Math.max(0, Math.round(t.replyAt - deps.callStartedAt)),
|
|
17919
|
+
endpointingMs,
|
|
17920
|
+
sttMs: ms(t.sttMs),
|
|
17921
|
+
llmMs,
|
|
17922
|
+
ttsMs,
|
|
17923
|
+
endToEndMs: t.realtime ? llmMs : measured ?? summed,
|
|
17924
|
+
...t.realtime ? { realtime: true } : {}
|
|
17925
|
+
});
|
|
17926
|
+
} catch (err) {
|
|
17927
|
+
console.warn("[agent] turn latency emit failed", { err: err instanceof Error ? err.message : String(err) });
|
|
17928
|
+
}
|
|
17929
|
+
};
|
|
17930
|
+
const at = (event) => {
|
|
17931
|
+
const t = event?.createdAt;
|
|
17932
|
+
return typeof t === "number" ? t : now();
|
|
17933
|
+
};
|
|
17934
|
+
const targetFor = (s) => {
|
|
17935
|
+
if (waiting && (s.startedAt === void 0 || s.startedAt >= waiting.firstFinalAt)) return waiting;
|
|
17936
|
+
return open;
|
|
17937
|
+
};
|
|
17938
|
+
if (!deps.realtime) {
|
|
17939
|
+
session.on(events.userInputTranscribed, (event) => {
|
|
17940
|
+
if (closed || !event?.isFinal) return;
|
|
17941
|
+
waiting ??= { ...noStages(), firstFinalAt: at(event) };
|
|
17942
|
+
});
|
|
17943
|
+
session.on(events.agentStateChanged, (event) => {
|
|
17944
|
+
if (closed || event?.newState !== "speaking" || !waiting) return;
|
|
17945
|
+
const replyAt = at(event);
|
|
17946
|
+
if (open) emit(open);
|
|
17947
|
+
open = null;
|
|
17948
|
+
if (turns >= maxTurns) {
|
|
17949
|
+
waiting = null;
|
|
17950
|
+
return;
|
|
17951
|
+
}
|
|
17952
|
+
turns += 1;
|
|
17953
|
+
const speechEndAt = lastSpeechEndAt !== null && lastSpeechEndAt > lastReplyAt && lastSpeechEndAt <= replyAt ? lastSpeechEndAt : null;
|
|
17954
|
+
const { firstFinalAt: _first, ...stages } = waiting;
|
|
17955
|
+
open = { ...stages, index: turns, replyAt, speechEndAt };
|
|
17956
|
+
waiting = null;
|
|
17957
|
+
lastReplyAt = replyAt;
|
|
17958
|
+
});
|
|
17959
|
+
}
|
|
17960
|
+
return {
|
|
17961
|
+
note(s) {
|
|
17962
|
+
if (closed || !Number.isFinite(s.latencyMs) || s.latencyMs < 0) return;
|
|
17963
|
+
if (deps.realtime) {
|
|
17964
|
+
if (s.stage !== "llm" || turns >= maxTurns) return;
|
|
17965
|
+
turns += 1;
|
|
17966
|
+
emit({ ...noStages(), llmMs: s.latencyMs, realtime: true, index: turns, replyAt: (s.startedAt ?? now()) + s.latencyMs, speechEndAt: null });
|
|
17967
|
+
return;
|
|
17968
|
+
}
|
|
17969
|
+
switch (s.stage) {
|
|
17970
|
+
case "endpointing":
|
|
17971
|
+
case "stt": {
|
|
17972
|
+
const key = s.stage === "endpointing" ? "endpointingMs" : "sttMs";
|
|
17973
|
+
if (waiting) waiting[key] = s.latencyMs;
|
|
17974
|
+
else if (open && open[key] === null) open[key] = s.latencyMs;
|
|
17975
|
+
return;
|
|
17976
|
+
}
|
|
17977
|
+
case "llm": {
|
|
17978
|
+
if (s.replyAt !== void 0) {
|
|
17979
|
+
if (open && open.replyAt === s.replyAt) open.llmMs = (open.llmMs ?? 0) + s.latencyMs;
|
|
17980
|
+
return;
|
|
17981
|
+
}
|
|
17982
|
+
const t = targetFor(s);
|
|
17983
|
+
if (t) t.llmMs = (t.llmMs ?? 0) + s.latencyMs;
|
|
17984
|
+
return;
|
|
17985
|
+
}
|
|
17986
|
+
case "tts": {
|
|
17987
|
+
const t = targetFor(s);
|
|
17988
|
+
if (t && t.ttsMs === null) t.ttsMs = s.latencyMs;
|
|
17989
|
+
return;
|
|
17990
|
+
}
|
|
17991
|
+
}
|
|
17992
|
+
},
|
|
17993
|
+
noteSpeechEnd(at2) {
|
|
17994
|
+
if (Number.isFinite(at2) && at2 > 0) lastSpeechEndAt = at2;
|
|
17995
|
+
},
|
|
17996
|
+
close() {
|
|
17997
|
+
if (closed) return;
|
|
17998
|
+
closed = true;
|
|
17999
|
+
if (open) emit(open);
|
|
18000
|
+
open = null;
|
|
18001
|
+
waiting = null;
|
|
18002
|
+
}
|
|
18003
|
+
};
|
|
18004
|
+
}
|
|
18005
|
+
|
|
18006
|
+
// src/runtime/call-outcome.ts
|
|
18007
|
+
init_src();
|
|
18008
|
+
var cut = (s) => s.length > CAPTURED_STRING_MAX_CHARS ? s.slice(0, CAPTURED_STRING_MAX_CHARS) : s;
|
|
18009
|
+
function wireValue(value) {
|
|
18010
|
+
if (value === void 0 || value === null) return null;
|
|
18011
|
+
if (typeof value === "string") return value.trim().length === 0 ? null : cut(value);
|
|
18012
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : null;
|
|
18013
|
+
if (typeof value === "boolean") return value;
|
|
18014
|
+
if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value.toISOString();
|
|
18015
|
+
if (Array.isArray(value)) {
|
|
18016
|
+
const items = value.filter((v) => v !== void 0 && v !== null).map((v) => cut(typeof v === "string" ? v : JSON.stringify(v))).slice(0, CAPTURED_ARRAY_MAX_ITEMS);
|
|
18017
|
+
return items.length === 0 ? null : items;
|
|
18018
|
+
}
|
|
18019
|
+
try {
|
|
18020
|
+
return cut(JSON.stringify(value));
|
|
18021
|
+
} catch {
|
|
18022
|
+
return null;
|
|
18023
|
+
}
|
|
18024
|
+
}
|
|
18025
|
+
function capturedFields(def, data) {
|
|
18026
|
+
if (!def) return [];
|
|
18027
|
+
return Object.entries(def.collect).filter(([name]) => name.length > 0 && name.length <= 64).slice(0, CAPTURED_FIELDS_MAX).map(([name, field]) => {
|
|
18028
|
+
const value = wireValue(data[name]);
|
|
18029
|
+
return { name, type: field.type, required: field.required === true, filled: value !== null, value };
|
|
18030
|
+
});
|
|
18031
|
+
}
|
|
18032
|
+
|
|
17722
18033
|
// src/agent.ts
|
|
17723
18034
|
init_model_meter();
|
|
17724
18035
|
|
|
@@ -17921,6 +18232,230 @@ function actionSpecToAction(spec) {
|
|
|
17921
18232
|
if (spec.kind === "say") return { say: spec.text };
|
|
17922
18233
|
return void 0;
|
|
17923
18234
|
}
|
|
18235
|
+
|
|
18236
|
+
// src/runtime/call-admission.ts
|
|
18237
|
+
init_src();
|
|
18238
|
+
|
|
18239
|
+
// src/end-call.ts
|
|
18240
|
+
var defaultClock = {
|
|
18241
|
+
now: () => Date.now(),
|
|
18242
|
+
setTimeout: (fn, ms) => setTimeout(fn, ms),
|
|
18243
|
+
clearTimeout: (h) => clearTimeout(h)
|
|
18244
|
+
};
|
|
18245
|
+
var HOST_HANGUP_HOOK_LIMIT_MS = 2e3;
|
|
18246
|
+
var GUARANTEE_RETRY_MS = 1500;
|
|
18247
|
+
var GOODBYE_LIMIT_MS = 1e4;
|
|
18248
|
+
var DEFAULT_MAX_CALL_DURATION_MS = 36e5;
|
|
18249
|
+
function createEndCallController(deps) {
|
|
18250
|
+
const clock = deps.clock ?? defaultClock;
|
|
18251
|
+
const log = deps.log ?? (() => {
|
|
18252
|
+
});
|
|
18253
|
+
let ended = false;
|
|
18254
|
+
let idleTimer = null;
|
|
18255
|
+
let durationTimer = null;
|
|
18256
|
+
let warnTimer = null;
|
|
18257
|
+
const clearIdle = () => {
|
|
18258
|
+
if (idleTimer !== null) {
|
|
18259
|
+
clock.clearTimeout(idleTimer);
|
|
18260
|
+
idleTimer = null;
|
|
18261
|
+
}
|
|
18262
|
+
};
|
|
18263
|
+
const clearDuration = () => {
|
|
18264
|
+
if (durationTimer !== null) {
|
|
18265
|
+
clock.clearTimeout(durationTimer);
|
|
18266
|
+
durationTimer = null;
|
|
18267
|
+
}
|
|
18268
|
+
if (warnTimer !== null) {
|
|
18269
|
+
clock.clearTimeout(warnTimer);
|
|
18270
|
+
warnTimer = null;
|
|
18271
|
+
}
|
|
18272
|
+
};
|
|
18273
|
+
const sleep2 = (ms) => new Promise((resolve) => {
|
|
18274
|
+
clock.setTimeout(resolve, ms);
|
|
18275
|
+
});
|
|
18276
|
+
const guarantee = async (teardown = deps.runTeardown) => {
|
|
18277
|
+
if (deps.guaranteeMs === false) return;
|
|
18278
|
+
const deadline2 = clock.now() + deps.guaranteeMs;
|
|
18279
|
+
while (!deps.isEnded() && clock.now() < deadline2) {
|
|
18280
|
+
await sleep2(Math.min(GUARANTEE_RETRY_MS, Math.max(0, deadline2 - clock.now())));
|
|
18281
|
+
if (deps.isEnded()) return;
|
|
18282
|
+
log("[agent] end-call teardown not confirmed; retrying", { teardown: deps.teardown });
|
|
18283
|
+
try {
|
|
18284
|
+
await teardown();
|
|
18285
|
+
} catch (err) {
|
|
18286
|
+
log("[agent] end-call retry teardown threw", { err: String(err) });
|
|
18287
|
+
}
|
|
18288
|
+
}
|
|
18289
|
+
};
|
|
18290
|
+
let hostHangup = null;
|
|
18291
|
+
const hostHangUp = (reason) => {
|
|
18292
|
+
if (deps.isEnded()) return Promise.resolve();
|
|
18293
|
+
hostHangup ??= (async () => {
|
|
18294
|
+
ended = true;
|
|
18295
|
+
clearIdle();
|
|
18296
|
+
clearDuration();
|
|
18297
|
+
const info = { ...reason !== void 0 ? { reason } : {}, trigger: "host", teardown: "room" };
|
|
18298
|
+
deps.emit?.(info);
|
|
18299
|
+
log("[agent] hang up (host)", { reason });
|
|
18300
|
+
if (deps.onEnd) {
|
|
18301
|
+
let limit;
|
|
18302
|
+
await Promise.race([
|
|
18303
|
+
Promise.resolve().then(() => deps.onEnd(info)).catch((err) => log("[agent] endCall.onEnd threw on a host hangup", { err: String(err) })),
|
|
18304
|
+
new Promise((resolve) => {
|
|
18305
|
+
limit = clock.setTimeout(resolve, HOST_HANGUP_HOOK_LIMIT_MS);
|
|
18306
|
+
})
|
|
18307
|
+
]).finally(() => clock.clearTimeout(limit));
|
|
18308
|
+
}
|
|
18309
|
+
const teardown = deps.runRoomTeardown ?? deps.runTeardown;
|
|
18310
|
+
try {
|
|
18311
|
+
await teardown();
|
|
18312
|
+
} catch (err) {
|
|
18313
|
+
log("[agent] host hangup teardown threw", { err: String(err) });
|
|
18314
|
+
}
|
|
18315
|
+
await guarantee(teardown);
|
|
18316
|
+
if (!deps.isEnded()) hostHangup = null;
|
|
18317
|
+
})();
|
|
18318
|
+
return hostHangup;
|
|
18319
|
+
};
|
|
18320
|
+
const hangUp = async (opts) => {
|
|
18321
|
+
if (opts?.trigger === "host") return hostHangUp(opts.reason);
|
|
18322
|
+
if (ended) return;
|
|
18323
|
+
ended = true;
|
|
18324
|
+
clearIdle();
|
|
18325
|
+
clearDuration();
|
|
18326
|
+
const info = {
|
|
18327
|
+
...opts?.reason !== void 0 ? { reason: opts.reason } : {},
|
|
18328
|
+
trigger: opts?.trigger ?? "user",
|
|
18329
|
+
teardown: deps.teardown
|
|
18330
|
+
};
|
|
18331
|
+
deps.emit?.(info);
|
|
18332
|
+
log("[agent] hang up", { trigger: info.trigger, teardown: info.teardown });
|
|
18333
|
+
let suppress = false;
|
|
18334
|
+
if (deps.onEnd) {
|
|
18335
|
+
try {
|
|
18336
|
+
suppress = await deps.onEnd(info) === false;
|
|
18337
|
+
} catch (err) {
|
|
18338
|
+
log("[agent] endCall.onEnd threw; running default teardown", { err: String(err) });
|
|
18339
|
+
}
|
|
18340
|
+
}
|
|
18341
|
+
if (suppress) {
|
|
18342
|
+
log("[agent] endCall.onEnd suppressed default teardown", { trigger: info.trigger });
|
|
18343
|
+
return;
|
|
18344
|
+
}
|
|
18345
|
+
try {
|
|
18346
|
+
await deps.runTeardown();
|
|
18347
|
+
} catch (err) {
|
|
18348
|
+
log("[agent] end-call teardown threw", { err: String(err) });
|
|
18349
|
+
}
|
|
18350
|
+
await guarantee();
|
|
18351
|
+
};
|
|
18352
|
+
return {
|
|
18353
|
+
hangUp,
|
|
18354
|
+
armIdle() {
|
|
18355
|
+
if (ended || deps.idleHangupMs === false) return;
|
|
18356
|
+
clearIdle();
|
|
18357
|
+
idleTimer = clock.setTimeout(() => {
|
|
18358
|
+
idleTimer = null;
|
|
18359
|
+
void hangUp({ trigger: "idle", reason: "no activity on the line" });
|
|
18360
|
+
}, deps.idleHangupMs);
|
|
18361
|
+
},
|
|
18362
|
+
cancelIdle() {
|
|
18363
|
+
clearIdle();
|
|
18364
|
+
},
|
|
18365
|
+
armDuration() {
|
|
18366
|
+
if (ended || deps.maxCallDurationMs === false) return;
|
|
18367
|
+
clearDuration();
|
|
18368
|
+
const durationMs = deps.maxCallDurationMs ?? DEFAULT_MAX_CALL_DURATION_MS;
|
|
18369
|
+
const wrapUp = deps.wrapUp;
|
|
18370
|
+
if (wrapUp && wrapUp.warnBeforeMs > 0 && durationMs > wrapUp.warnBeforeMs) {
|
|
18371
|
+
warnTimer = clock.setTimeout(() => {
|
|
18372
|
+
warnTimer = null;
|
|
18373
|
+
if (ended) return;
|
|
18374
|
+
try {
|
|
18375
|
+
void Promise.resolve(wrapUp.warn()).catch((err) => log("[agent] wrap-up line failed", { err: String(err) }));
|
|
18376
|
+
} catch (err) {
|
|
18377
|
+
log("[agent] wrap-up line failed", { err: String(err) });
|
|
18378
|
+
}
|
|
18379
|
+
}, durationMs - wrapUp.warnBeforeMs);
|
|
18380
|
+
}
|
|
18381
|
+
durationTimer = clock.setTimeout(() => {
|
|
18382
|
+
durationTimer = null;
|
|
18383
|
+
void (async () => {
|
|
18384
|
+
if (wrapUp && !ended) {
|
|
18385
|
+
let limit;
|
|
18386
|
+
await Promise.race([
|
|
18387
|
+
wrapUp.goodbye().catch((err) => log("[agent] goodbye line failed", { err: String(err) })),
|
|
18388
|
+
new Promise((resolve) => {
|
|
18389
|
+
limit = clock.setTimeout(resolve, GOODBYE_LIMIT_MS);
|
|
18390
|
+
})
|
|
18391
|
+
]).finally(() => clock.clearTimeout(limit));
|
|
18392
|
+
}
|
|
18393
|
+
await hangUp({
|
|
18394
|
+
trigger: "max_duration",
|
|
18395
|
+
reason: `call exceeded the ${durationMs}ms duration ceiling`
|
|
18396
|
+
});
|
|
18397
|
+
})();
|
|
18398
|
+
}, durationMs);
|
|
18399
|
+
},
|
|
18400
|
+
noteEngineClosed() {
|
|
18401
|
+
if (ended) return;
|
|
18402
|
+
log("[agent] realtime engine closed; guaranteeing teardown");
|
|
18403
|
+
void hangUp({ trigger: "engine-closed", reason: "realtime engine closed" });
|
|
18404
|
+
},
|
|
18405
|
+
dispose() {
|
|
18406
|
+
clearIdle();
|
|
18407
|
+
clearDuration();
|
|
18408
|
+
}
|
|
18409
|
+
};
|
|
18410
|
+
}
|
|
18411
|
+
|
|
18412
|
+
// src/runtime/call-admission.ts
|
|
18413
|
+
var CALL_ADMISSION_RETRY_DELAYS_MS = [300, 1e3];
|
|
18414
|
+
var ADMISSION_UNAVAILABLE_END_REASON = `${INBOUND_REFUSED_END_REASON_PREFIX}gate_unavailable`;
|
|
18415
|
+
function notAVerdict(err) {
|
|
18416
|
+
if (err instanceof VoiceLayerAuthError || err instanceof VoiceLayerValidationError) return true;
|
|
18417
|
+
return err instanceof VoiceLayerHttpError && err.status >= 400 && err.status < 500 && err.status !== 408 && err.status !== 429;
|
|
18418
|
+
}
|
|
18419
|
+
async function requestCallAdmission(admit, req, opts = {}) {
|
|
18420
|
+
if (!admit) return { kind: "admitted", limits: null };
|
|
18421
|
+
const delays = opts.delaysMs ?? CALL_ADMISSION_RETRY_DELAYS_MS;
|
|
18422
|
+
const sleep2 = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
18423
|
+
const log = opts.log ?? ((msg, attrs2) => console.warn(msg, attrs2 ?? {}));
|
|
18424
|
+
for (let attempt = 1; ; attempt++) {
|
|
18425
|
+
try {
|
|
18426
|
+
const res = await admit(req);
|
|
18427
|
+
return res.admitted ? { kind: "admitted", limits: res.limits } : { kind: "refused", endReason: res.endReason, message: res.message };
|
|
18428
|
+
} catch (err) {
|
|
18429
|
+
if (notAVerdict(err)) {
|
|
18430
|
+
if (opts.platformWorker) {
|
|
18431
|
+
log("[agent] call admission refused for this room on a platform worker; refusing the call (fail closed)", { err: String(err) });
|
|
18432
|
+
return { kind: "refused", endReason: ADMISSION_UNAVAILABLE_END_REASON, message: INBOUND_REFUSED_MESSAGE };
|
|
18433
|
+
}
|
|
18434
|
+
log("[agent] call admission unavailable for this room; running on the agent settings", { err: String(err) });
|
|
18435
|
+
return { kind: "admitted", limits: null };
|
|
18436
|
+
}
|
|
18437
|
+
if (attempt > delays.length) {
|
|
18438
|
+
log("[agent] call admission failed after retries; refusing the call (fail closed)", { err: String(err), attempts: attempt });
|
|
18439
|
+
return { kind: "refused", endReason: ADMISSION_UNAVAILABLE_END_REASON, message: INBOUND_REFUSED_MESSAGE };
|
|
18440
|
+
}
|
|
18441
|
+
await sleep2(delays[attempt - 1]);
|
|
18442
|
+
}
|
|
18443
|
+
}
|
|
18444
|
+
}
|
|
18445
|
+
function effectiveMaxCallDurationMs(code, limits) {
|
|
18446
|
+
if (!limits) return code === void 0 ? DEFAULT_MAX_CALL_DURATION_MS : code;
|
|
18447
|
+
const ceiling = limits.ceilingSeconds * 1e3;
|
|
18448
|
+
const base = limits.agentMaxSeconds !== null ? limits.agentMaxSeconds * 1e3 : code === void 0 ? DEFAULT_MAX_CALL_DURATION_MS : code;
|
|
18449
|
+
return base === false ? ceiling : Math.min(base, ceiling);
|
|
18450
|
+
}
|
|
18451
|
+
async function playAdmissionRefusal(verdict, deps) {
|
|
18452
|
+
try {
|
|
18453
|
+
await deps.say(verdict.message, { allowInterruptions: false });
|
|
18454
|
+
} catch (err) {
|
|
18455
|
+
(deps.log ?? ((m, a) => console.warn(m, a ?? {})))("[agent] admission refusal line failed to play", { err: String(err) });
|
|
18456
|
+
}
|
|
18457
|
+
await deps.hangUp(verdict.endReason);
|
|
18458
|
+
}
|
|
17924
18459
|
init_src();
|
|
17925
18460
|
var AskHostInput = z.object({
|
|
17926
18461
|
question: z.string().min(1).max(2e3),
|
|
@@ -18471,7 +19006,7 @@ function asText(value) {
|
|
|
18471
19006
|
function masked(value) {
|
|
18472
19007
|
return maskPii(asText(value).slice(0, READ_CAP));
|
|
18473
19008
|
}
|
|
18474
|
-
var
|
|
19009
|
+
var cut2 = (text, cap) => text.length > cap ? `${text.slice(0, cap)}\u2026` : text;
|
|
18475
19010
|
function toolAuditPayload(r) {
|
|
18476
19011
|
const args = masked(r.params);
|
|
18477
19012
|
const result = r.ok ? masked(r.result) : void 0;
|
|
@@ -18480,11 +19015,11 @@ function toolAuditPayload(r) {
|
|
|
18480
19015
|
const payload = {
|
|
18481
19016
|
invocationId: r.invocationId,
|
|
18482
19017
|
toolName: r.toolName,
|
|
18483
|
-
args:
|
|
18484
|
-
...result !== void 0 ? { result:
|
|
19018
|
+
args: cut2(args, cap),
|
|
19019
|
+
...result !== void 0 ? { result: cut2(result, cap) } : {},
|
|
18485
19020
|
ok: r.ok,
|
|
18486
19021
|
blocked: r.blocked,
|
|
18487
|
-
...error !== void 0 ? { errorMessage:
|
|
19022
|
+
...error !== void 0 ? { errorMessage: cut2(error, Math.min(cap, 300)) } : {},
|
|
18488
19023
|
startedAt: r.startedAt.toISOString(),
|
|
18489
19024
|
durationMs: r.durationMs,
|
|
18490
19025
|
// the tool got the caller's values in place of the call's PII tokens (G-33): how many and where — never a value
|
|
@@ -18504,150 +19039,6 @@ function toolAuditObserver(emit) {
|
|
|
18504
19039
|
};
|
|
18505
19040
|
}
|
|
18506
19041
|
|
|
18507
|
-
// src/end-call.ts
|
|
18508
|
-
var defaultClock = {
|
|
18509
|
-
now: () => Date.now(),
|
|
18510
|
-
setTimeout: (fn, ms) => setTimeout(fn, ms),
|
|
18511
|
-
clearTimeout: (h) => clearTimeout(h)
|
|
18512
|
-
};
|
|
18513
|
-
var HOST_HANGUP_HOOK_LIMIT_MS = 2e3;
|
|
18514
|
-
var GUARANTEE_RETRY_MS = 1500;
|
|
18515
|
-
var DEFAULT_MAX_CALL_DURATION_MS = 36e5;
|
|
18516
|
-
function createEndCallController(deps) {
|
|
18517
|
-
const clock = deps.clock ?? defaultClock;
|
|
18518
|
-
const log = deps.log ?? (() => {
|
|
18519
|
-
});
|
|
18520
|
-
let ended = false;
|
|
18521
|
-
let idleTimer = null;
|
|
18522
|
-
let durationTimer = null;
|
|
18523
|
-
const clearIdle = () => {
|
|
18524
|
-
if (idleTimer !== null) {
|
|
18525
|
-
clock.clearTimeout(idleTimer);
|
|
18526
|
-
idleTimer = null;
|
|
18527
|
-
}
|
|
18528
|
-
};
|
|
18529
|
-
const clearDuration = () => {
|
|
18530
|
-
if (durationTimer !== null) {
|
|
18531
|
-
clock.clearTimeout(durationTimer);
|
|
18532
|
-
durationTimer = null;
|
|
18533
|
-
}
|
|
18534
|
-
};
|
|
18535
|
-
const sleep2 = (ms) => new Promise((resolve) => {
|
|
18536
|
-
clock.setTimeout(resolve, ms);
|
|
18537
|
-
});
|
|
18538
|
-
const guarantee = async (teardown = deps.runTeardown) => {
|
|
18539
|
-
if (deps.guaranteeMs === false) return;
|
|
18540
|
-
const deadline = clock.now() + deps.guaranteeMs;
|
|
18541
|
-
while (!deps.isEnded() && clock.now() < deadline) {
|
|
18542
|
-
await sleep2(Math.min(GUARANTEE_RETRY_MS, Math.max(0, deadline - clock.now())));
|
|
18543
|
-
if (deps.isEnded()) return;
|
|
18544
|
-
log("[agent] end-call teardown not confirmed; retrying", { teardown: deps.teardown });
|
|
18545
|
-
try {
|
|
18546
|
-
await teardown();
|
|
18547
|
-
} catch (err) {
|
|
18548
|
-
log("[agent] end-call retry teardown threw", { err: String(err) });
|
|
18549
|
-
}
|
|
18550
|
-
}
|
|
18551
|
-
};
|
|
18552
|
-
let hostHangup = null;
|
|
18553
|
-
const hostHangUp = (reason) => {
|
|
18554
|
-
if (deps.isEnded()) return Promise.resolve();
|
|
18555
|
-
hostHangup ??= (async () => {
|
|
18556
|
-
ended = true;
|
|
18557
|
-
clearIdle();
|
|
18558
|
-
clearDuration();
|
|
18559
|
-
const info = { ...reason !== void 0 ? { reason } : {}, trigger: "host", teardown: "room" };
|
|
18560
|
-
deps.emit?.(info);
|
|
18561
|
-
log("[agent] hang up (host)", { reason });
|
|
18562
|
-
if (deps.onEnd) {
|
|
18563
|
-
let limit;
|
|
18564
|
-
await Promise.race([
|
|
18565
|
-
Promise.resolve().then(() => deps.onEnd(info)).catch((err) => log("[agent] endCall.onEnd threw on a host hangup", { err: String(err) })),
|
|
18566
|
-
new Promise((resolve) => {
|
|
18567
|
-
limit = clock.setTimeout(resolve, HOST_HANGUP_HOOK_LIMIT_MS);
|
|
18568
|
-
})
|
|
18569
|
-
]).finally(() => clock.clearTimeout(limit));
|
|
18570
|
-
}
|
|
18571
|
-
const teardown = deps.runRoomTeardown ?? deps.runTeardown;
|
|
18572
|
-
try {
|
|
18573
|
-
await teardown();
|
|
18574
|
-
} catch (err) {
|
|
18575
|
-
log("[agent] host hangup teardown threw", { err: String(err) });
|
|
18576
|
-
}
|
|
18577
|
-
await guarantee(teardown);
|
|
18578
|
-
if (!deps.isEnded()) hostHangup = null;
|
|
18579
|
-
})();
|
|
18580
|
-
return hostHangup;
|
|
18581
|
-
};
|
|
18582
|
-
const hangUp = async (opts) => {
|
|
18583
|
-
if (opts?.trigger === "host") return hostHangUp(opts.reason);
|
|
18584
|
-
if (ended) return;
|
|
18585
|
-
ended = true;
|
|
18586
|
-
clearIdle();
|
|
18587
|
-
clearDuration();
|
|
18588
|
-
const info = {
|
|
18589
|
-
...opts?.reason !== void 0 ? { reason: opts.reason } : {},
|
|
18590
|
-
trigger: opts?.trigger ?? "user",
|
|
18591
|
-
teardown: deps.teardown
|
|
18592
|
-
};
|
|
18593
|
-
deps.emit?.(info);
|
|
18594
|
-
log("[agent] hang up", { trigger: info.trigger, teardown: info.teardown });
|
|
18595
|
-
let suppress = false;
|
|
18596
|
-
if (deps.onEnd) {
|
|
18597
|
-
try {
|
|
18598
|
-
suppress = await deps.onEnd(info) === false;
|
|
18599
|
-
} catch (err) {
|
|
18600
|
-
log("[agent] endCall.onEnd threw; running default teardown", { err: String(err) });
|
|
18601
|
-
}
|
|
18602
|
-
}
|
|
18603
|
-
if (suppress) {
|
|
18604
|
-
log("[agent] endCall.onEnd suppressed default teardown", { trigger: info.trigger });
|
|
18605
|
-
return;
|
|
18606
|
-
}
|
|
18607
|
-
try {
|
|
18608
|
-
await deps.runTeardown();
|
|
18609
|
-
} catch (err) {
|
|
18610
|
-
log("[agent] end-call teardown threw", { err: String(err) });
|
|
18611
|
-
}
|
|
18612
|
-
await guarantee();
|
|
18613
|
-
};
|
|
18614
|
-
return {
|
|
18615
|
-
hangUp,
|
|
18616
|
-
armIdle() {
|
|
18617
|
-
if (ended || deps.idleHangupMs === false) return;
|
|
18618
|
-
clearIdle();
|
|
18619
|
-
idleTimer = clock.setTimeout(() => {
|
|
18620
|
-
idleTimer = null;
|
|
18621
|
-
void hangUp({ trigger: "idle", reason: "no activity on the line" });
|
|
18622
|
-
}, deps.idleHangupMs);
|
|
18623
|
-
},
|
|
18624
|
-
cancelIdle() {
|
|
18625
|
-
clearIdle();
|
|
18626
|
-
},
|
|
18627
|
-
armDuration() {
|
|
18628
|
-
if (ended || deps.maxCallDurationMs === false) return;
|
|
18629
|
-
clearDuration();
|
|
18630
|
-
const durationMs = deps.maxCallDurationMs ?? DEFAULT_MAX_CALL_DURATION_MS;
|
|
18631
|
-
durationTimer = clock.setTimeout(() => {
|
|
18632
|
-
durationTimer = null;
|
|
18633
|
-
void hangUp({
|
|
18634
|
-
trigger: "max_duration",
|
|
18635
|
-
reason: `call exceeded the ${durationMs}ms duration ceiling`
|
|
18636
|
-
});
|
|
18637
|
-
}, durationMs);
|
|
18638
|
-
},
|
|
18639
|
-
noteEngineClosed() {
|
|
18640
|
-
if (ended) return;
|
|
18641
|
-
log("[agent] realtime engine closed; guaranteeing teardown");
|
|
18642
|
-
void hangUp({ trigger: "engine-closed", reason: "realtime engine closed" });
|
|
18643
|
-
},
|
|
18644
|
-
dispose() {
|
|
18645
|
-
clearIdle();
|
|
18646
|
-
clearDuration();
|
|
18647
|
-
}
|
|
18648
|
-
};
|
|
18649
|
-
}
|
|
18650
|
-
|
|
18651
19042
|
// src/agent.ts
|
|
18652
19043
|
init_helper_models();
|
|
18653
19044
|
var FLOW_HANGUP_GRACE_MS = Number(process.env["VL_FLOW_HANGUP_GRACE_MS"] ?? "1200");
|
|
@@ -18962,6 +19353,7 @@ var Agent = class {
|
|
|
18962
19353
|
const built = await resolveCallPipeline(effectiveConfig.models ?? {}, callInfo, job.proc.userData.vad);
|
|
18963
19354
|
const pipeline = built.pipeline;
|
|
18964
19355
|
if (built.failure && !flowBootFailure) flowBootFailure = built.failure;
|
|
19356
|
+
const keyOwner = pipelineKeys(pipeline, callCredential?.source !== "worker-token");
|
|
18965
19357
|
const vadTap = tapVad(pipeline.vad, { endOfSpeechType: VADEventType.END_OF_SPEECH });
|
|
18966
19358
|
const usingRealtime = pipeline.realtime != null;
|
|
18967
19359
|
let runtimeLlm = usingRealtime ? pipeline.realtime : pipeline.llm;
|
|
@@ -18974,9 +19366,10 @@ var Agent = class {
|
|
|
18974
19366
|
seedCallPrefill(processRt, dispatchMdEarly);
|
|
18975
19367
|
await seedProjectVariables(processRt, callCredential);
|
|
18976
19368
|
let opEventCallId = null;
|
|
19369
|
+
let trackWrite = (write) => write;
|
|
18977
19370
|
const emitOpEvent = (kind, payload) => {
|
|
18978
19371
|
if (!sdkClient || !opEventCallId) return;
|
|
18979
|
-
void sdkClient.calls.appendEvent(opEventCallId, { kind, payload }).catch(() => {
|
|
19372
|
+
void trackWrite(sdkClient.calls.appendEvent(opEventCallId, { kind, payload })).catch(() => {
|
|
18980
19373
|
});
|
|
18981
19374
|
};
|
|
18982
19375
|
const graphEvents = graphMode ? withOpEventFanout(createGraphEvents(processRt), emitOpEvent) : null;
|
|
@@ -19003,8 +19396,23 @@ var Agent = class {
|
|
|
19003
19396
|
const callSync = await createCallSyncHandle({
|
|
19004
19397
|
client: sdkClient,
|
|
19005
19398
|
roomName: job.room.name ?? `room-${job.job?.id ?? "unknown"}`,
|
|
19006
|
-
call: callInfo
|
|
19399
|
+
call: callInfo,
|
|
19400
|
+
pipelineKeys: keyOwner
|
|
19007
19401
|
});
|
|
19402
|
+
trackWrite = callSync.track;
|
|
19403
|
+
const callerIsPhone = isSipParticipant2(participant);
|
|
19404
|
+
const callAdmission = await requestCallAdmission(
|
|
19405
|
+
sdkClient ? (req) => sdkClient.calls.admit(req) : null,
|
|
19406
|
+
{
|
|
19407
|
+
roomName: job.room.name ?? `room-${job.job?.id ?? "unknown"}`,
|
|
19408
|
+
...stringValue(callInfo.metadata["agentId"]) ? { agentId: stringValue(callInfo.metadata["agentId"]) } : {},
|
|
19409
|
+
...stringValue(callInfo.metadata["phoneNumberId"]) ? { phoneNumberId: stringValue(callInfo.metadata["phoneNumberId"]) } : {},
|
|
19410
|
+
caller: { channel: callerIsPhone ? "phone" : "web", number: callerIsPhone ? callInfo.callerId : null }
|
|
19411
|
+
},
|
|
19412
|
+
// our pool's per-call worker token: no "no verdict" — a missing row is a refusal, never an unbilled call
|
|
19413
|
+
{ platformWorker: callCredential?.source === "worker-token" }
|
|
19414
|
+
);
|
|
19415
|
+
const admissionLimits = callAdmission.kind === "admitted" ? callAdmission.limits : null;
|
|
19008
19416
|
const transcriptLines = [];
|
|
19009
19417
|
const transcript = createTranscriptHandle(() => transcriptLines);
|
|
19010
19418
|
const ctxRef = { current: null };
|
|
@@ -19136,7 +19544,8 @@ ${callIntent}`
|
|
|
19136
19544
|
const endCallTeardown = endCallPolicy.teardown ?? "room";
|
|
19137
19545
|
const endCallGuaranteeMs = endCallPolicy.guaranteeMs === void 0 ? 4e3 : endCallPolicy.guaranteeMs;
|
|
19138
19546
|
const endCallIdleMs = endCallPolicy.idleHangupMs === void 0 ? 2e4 : endCallPolicy.idleHangupMs;
|
|
19139
|
-
const endCallMaxDurationMs = endCallPolicy.maxCallDurationMs
|
|
19547
|
+
const endCallMaxDurationMs = effectiveMaxCallDurationMs(endCallPolicy.maxCallDurationMs, admissionLimits);
|
|
19548
|
+
const lineRef = { say: null };
|
|
19140
19549
|
const endCallRoomName = job.room.name ?? `room-${job.job?.id ?? "unknown"}`;
|
|
19141
19550
|
const localDisconnect = async () => {
|
|
19142
19551
|
try {
|
|
@@ -19149,6 +19558,17 @@ ${callIntent}`
|
|
|
19149
19558
|
guaranteeMs: endCallGuaranteeMs,
|
|
19150
19559
|
idleHangupMs: endCallIdleMs,
|
|
19151
19560
|
maxCallDurationMs: endCallMaxDurationMs,
|
|
19561
|
+
...admissionLimits ? {
|
|
19562
|
+
wrapUp: {
|
|
19563
|
+
warnBeforeMs: admissionLimits.wrapUpWarningSeconds * 1e3,
|
|
19564
|
+
warn: () => lineRef.say?.(admissionLimits.wrapUpMessage),
|
|
19565
|
+
goodbye: async () => {
|
|
19566
|
+
const reported = callSync.recordEndReason("max_duration");
|
|
19567
|
+
await lineRef.say?.(admissionLimits.goodbyeMessage);
|
|
19568
|
+
await reported;
|
|
19569
|
+
}
|
|
19570
|
+
}
|
|
19571
|
+
} : {},
|
|
19152
19572
|
// 'room' deletes the whole LK room (drops the SIP/PSTN leg → carrier BYE);
|
|
19153
19573
|
// 'agent' only disconnects the agent participant.
|
|
19154
19574
|
runTeardown: endCallTeardown === "room" ? () => endRoomBestEffort(endCallRoomName, localDisconnect) : localDisconnect,
|
|
@@ -19187,6 +19607,7 @@ ${callIntent}`
|
|
|
19187
19607
|
}
|
|
19188
19608
|
});
|
|
19189
19609
|
const session = adapter.raw.session;
|
|
19610
|
+
lineRef.say = (text) => adapter.say(text, { allowInterruptions: false });
|
|
19190
19611
|
sessionRef.current = session;
|
|
19191
19612
|
const handoffRt = createHandoffRuntime(effectiveConfig.handoff, {
|
|
19192
19613
|
session,
|
|
@@ -19423,6 +19844,7 @@ ${callIntent}`
|
|
|
19423
19844
|
})
|
|
19424
19845
|
});
|
|
19425
19846
|
let usageReported = Promise.resolve();
|
|
19847
|
+
let finalized = Promise.resolve();
|
|
19426
19848
|
session.once(Events.Close, () => {
|
|
19427
19849
|
sessionClosed = true;
|
|
19428
19850
|
endCallController.dispose();
|
|
@@ -19440,7 +19862,7 @@ ${callIntent}`
|
|
|
19440
19862
|
meter: modelMeter,
|
|
19441
19863
|
usage: usageSink,
|
|
19442
19864
|
durationMs: outcome.durationMs,
|
|
19443
|
-
send: (report) => calls.reportUsage(callSync.callId, report),
|
|
19865
|
+
send: (report, signal) => calls.reportUsage(callSync.callId, report, { signal }),
|
|
19444
19866
|
warn: (message, meta) => console.warn(message, { callId: callSync.callId, ...meta })
|
|
19445
19867
|
});
|
|
19446
19868
|
}
|
|
@@ -19580,6 +20002,11 @@ ${callIntent}`
|
|
|
19580
20002
|
callId: callSync.callId,
|
|
19581
20003
|
...callCampaignId ? { campaignId: callCampaignId } : {}
|
|
19582
20004
|
};
|
|
20005
|
+
const turnLatency = attachTurnLatencyRecorder(
|
|
20006
|
+
session,
|
|
20007
|
+
{ userInputTranscribed: Events.UserInputTranscribed, agentStateChanged: Events.AgentStateChanged },
|
|
20008
|
+
{ callStartedAt: callInfo.startedAt.getTime(), emit: (payload) => callSync.publishTurnLatency(payload), realtime: usingRealtime }
|
|
20009
|
+
);
|
|
19583
20010
|
const modelClock = graphMode && !usingRealtime ? attachTurnModelClock(
|
|
19584
20011
|
session,
|
|
19585
20012
|
{
|
|
@@ -19588,13 +20015,26 @@ ${callIntent}`
|
|
|
19588
20015
|
agentStateChanged: Events.AgentStateChanged,
|
|
19589
20016
|
speechCreated: Events.SpeechCreated
|
|
19590
20017
|
},
|
|
19591
|
-
metricsCtx
|
|
20018
|
+
metricsCtx,
|
|
20019
|
+
{ turns: turnLatency }
|
|
19592
20020
|
) : void 0;
|
|
19593
20021
|
if (modelClock) session.once(Events.Close, () => modelClock.close());
|
|
20022
|
+
session.once(Events.Close, () => {
|
|
20023
|
+
turnLatency.close();
|
|
20024
|
+
finalized = (async () => {
|
|
20025
|
+
await modelMeter.drain(HANGUP_DRAIN_MS);
|
|
20026
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
20027
|
+
await callSync.finalize(capturedFields(effectiveConfig.process, processRt.getData()));
|
|
20028
|
+
})().catch((err) => {
|
|
20029
|
+
console.warn("[agent] finalize failed", { callId: callSync.callId, err: err instanceof Error ? err.message : String(err) });
|
|
20030
|
+
});
|
|
20031
|
+
});
|
|
19594
20032
|
modelMeter.bind(metricsCtx, modelClock);
|
|
19595
20033
|
attachMetricsBridge(session, Events.MetricsCollected, {
|
|
19596
20034
|
...metricsCtx,
|
|
19597
20035
|
usage: usageSink,
|
|
20036
|
+
keyOwner,
|
|
20037
|
+
turns: turnLatency,
|
|
19598
20038
|
...modelClock ? { modelClock } : {}
|
|
19599
20039
|
});
|
|
19600
20040
|
if (graphMode) {
|
|
@@ -19604,7 +20044,7 @@ ${callIntent}`
|
|
|
19604
20044
|
userStateChanged: Events.UserStateChanged,
|
|
19605
20045
|
userInputTranscribed: Events.UserInputTranscribed
|
|
19606
20046
|
},
|
|
19607
|
-
metricsCtx,
|
|
20047
|
+
{ ...metricsCtx, turns: turnLatency },
|
|
19608
20048
|
{ vad: vadTap }
|
|
19609
20049
|
);
|
|
19610
20050
|
}
|
|
@@ -19667,11 +20107,11 @@ ${callIntent}`
|
|
|
19667
20107
|
room: job.room.name ?? null,
|
|
19668
20108
|
participant: participant.identity
|
|
19669
20109
|
});
|
|
19670
|
-
endCallController.armDuration();
|
|
20110
|
+
if (callAdmission.kind !== "refused") endCallController.armDuration();
|
|
19671
20111
|
{
|
|
19672
20112
|
const announceMd = parseJobMetadata(job.job?.metadata);
|
|
19673
20113
|
const announcement = stringValue(announceMd["recordingAnnouncement"]);
|
|
19674
|
-
if (announcement) {
|
|
20114
|
+
if (announcement && callAdmission.kind !== "refused") {
|
|
19675
20115
|
try {
|
|
19676
20116
|
await adapter.say(announcement, { allowInterruptions: false });
|
|
19677
20117
|
} catch (err) {
|
|
@@ -19679,7 +20119,13 @@ ${callIntent}`
|
|
|
19679
20119
|
}
|
|
19680
20120
|
}
|
|
19681
20121
|
}
|
|
19682
|
-
if (
|
|
20122
|
+
if (callAdmission.kind === "refused") {
|
|
20123
|
+
console.warn("[agent] call refused at admission", { room: job.room.name ?? null, endReason: callAdmission.endReason });
|
|
20124
|
+
await playAdmissionRefusal(callAdmission, {
|
|
20125
|
+
say: (text, opts) => adapter.say(text, opts),
|
|
20126
|
+
hangUp: (reason) => endCallController.hangUp({ trigger: "admission", reason })
|
|
20127
|
+
});
|
|
20128
|
+
} else if (puppetOwnsOpening(this.config.puppetMode, flowBootFailure)) {
|
|
19683
20129
|
const dispatchMd = parseJobMetadata(job.job?.metadata);
|
|
19684
20130
|
const initialDirective = stringValue(dispatchMd["initialDirective"]);
|
|
19685
20131
|
if (initialDirective) {
|
|
@@ -19771,6 +20217,7 @@ ${callIntent}`
|
|
|
19771
20217
|
}
|
|
19772
20218
|
await waitForSessionClose;
|
|
19773
20219
|
await usageReported;
|
|
20220
|
+
await finalized;
|
|
19774
20221
|
} catch (err) {
|
|
19775
20222
|
console.error("[agent] runJob failed", {
|
|
19776
20223
|
room: job.room.name ?? null,
|
|
@@ -19880,7 +20327,7 @@ function sharedBrainPubSub() {
|
|
|
19880
20327
|
async function applyStoredPipeline(client, agentId, base, codeMode) {
|
|
19881
20328
|
try {
|
|
19882
20329
|
const stored = await client.agents.getConfig(agentId);
|
|
19883
|
-
if (!stored) return { config: base, brain: "none"
|
|
20330
|
+
if (!stored) return { config: base, brain: "none" };
|
|
19884
20331
|
const mode = codeMode ?? stored.mode;
|
|
19885
20332
|
const providers = /* @__PURE__ */ new Set();
|
|
19886
20333
|
if (mode === "realtime" && stored.realtime) {
|
|
@@ -19955,7 +20402,6 @@ async function applyStoredPipeline(client, agentId, base, codeMode) {
|
|
|
19955
20402
|
models: { ...base.models ?? {}, ...mapped, ...brainLlm ? { llm: brainLlm } : {} }
|
|
19956
20403
|
},
|
|
19957
20404
|
brain: brainLlm ? "bound" : brainConnectorId && mode !== "realtime" ? "unreachable" : "none",
|
|
19958
|
-
byokProviders: Object.keys(creds2),
|
|
19959
20405
|
...mode !== "realtime" && stored.model ? { llm: { provider: stored.model.provider, model: stored.model.model } } : {}
|
|
19960
20406
|
};
|
|
19961
20407
|
} catch (err) {
|
|
@@ -19963,7 +20409,7 @@ async function applyStoredPipeline(client, agentId, base, codeMode) {
|
|
|
19963
20409
|
agentId,
|
|
19964
20410
|
err: err instanceof Error ? err.message : String(err)
|
|
19965
20411
|
});
|
|
19966
|
-
return { config: base, brain: "unknown"
|
|
20412
|
+
return { config: base, brain: "unknown" };
|
|
19967
20413
|
}
|
|
19968
20414
|
}
|
|
19969
20415
|
function firstString(...values) {
|