@voicelayer/sdk 0.6.0 → 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.d.ts +2 -2
- package/dist/brain/index.js +155 -162
- package/dist/index.d.ts +707 -257
- package/dist/index.js +1251 -454
- package/dist/runtime/text-session.d.ts +1 -1
- package/dist/runtime/text-session.js +256 -176
- package/dist/{text-session-CoyEcdmg.d.ts → text-session-B-T33y5P.d.ts} +116 -110
- package/package.json +5 -5
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
|
|
|
@@ -4675,10 +4686,86 @@ var init_dist = __esm({
|
|
|
4675
4686
|
init_chat_params();
|
|
4676
4687
|
}
|
|
4677
4688
|
});
|
|
4689
|
+
function runWithModelMeter(listener, fn) {
|
|
4690
|
+
return meterScope.run(listener, fn);
|
|
4691
|
+
}
|
|
4692
|
+
function requestedModel(body) {
|
|
4693
|
+
const model2 = body?.model;
|
|
4694
|
+
return typeof model2 === "string" && model2 ? model2 : void 0;
|
|
4695
|
+
}
|
|
4696
|
+
function safely(fn) {
|
|
4697
|
+
try {
|
|
4698
|
+
fn();
|
|
4699
|
+
} catch (err) {
|
|
4700
|
+
console.warn("[voicelayer] model meter listener failed", { err: err instanceof Error ? err.message : String(err) });
|
|
4701
|
+
}
|
|
4702
|
+
}
|
|
4703
|
+
function metered(target, create, byok) {
|
|
4704
|
+
return async (...args) => {
|
|
4705
|
+
const listener = meterScope.getStore();
|
|
4706
|
+
const startedAt = Date.now();
|
|
4707
|
+
const model2 = requestedModel(args[0]);
|
|
4708
|
+
if (listener) safely(() => listener.started());
|
|
4709
|
+
let res;
|
|
4710
|
+
try {
|
|
4711
|
+
res = await Reflect.apply(create, target, args);
|
|
4712
|
+
return res;
|
|
4713
|
+
} finally {
|
|
4714
|
+
if (listener) {
|
|
4715
|
+
const usage = res?.usage;
|
|
4716
|
+
const record = {
|
|
4717
|
+
provider: "openai",
|
|
4718
|
+
model: model2 ?? res?.model ?? "unknown",
|
|
4719
|
+
...usage ? { usage: { inputTokens: usage.prompt_tokens ?? 0, outputTokens: usage.completion_tokens ?? 0 } } : {},
|
|
4720
|
+
...byok !== void 0 ? { byok } : {},
|
|
4721
|
+
startedAt,
|
|
4722
|
+
endedAt: Date.now()
|
|
4723
|
+
};
|
|
4724
|
+
safely(() => listener.finished(record));
|
|
4725
|
+
}
|
|
4726
|
+
}
|
|
4727
|
+
};
|
|
4728
|
+
}
|
|
4729
|
+
function meteredClient(client, byok) {
|
|
4730
|
+
const hit = wrapped.get(client);
|
|
4731
|
+
if (hit) return hit;
|
|
4732
|
+
const chat = client.chat;
|
|
4733
|
+
const embeddings = client.embeddings;
|
|
4734
|
+
const completionsCreate = chat ? metered(chat.completions, chat.completions.create, byok) : void 0;
|
|
4735
|
+
const embeddingsCreate = embeddings ? metered(embeddings, embeddings.create, byok) : void 0;
|
|
4736
|
+
const completions = chat ? new Proxy(chat.completions, { get: (t, p, r) => p === "create" ? completionsCreate : Reflect.get(t, p, r) }) : void 0;
|
|
4737
|
+
const chatProxy = chat ? new Proxy(chat, { get: (t, p, r) => p === "completions" ? completions : Reflect.get(t, p, r) }) : void 0;
|
|
4738
|
+
const embeddingsProxy = embeddings ? new Proxy(embeddings, { get: (t, p, r) => p === "create" ? embeddingsCreate : Reflect.get(t, p, r) }) : void 0;
|
|
4739
|
+
const proxy = new Proxy(client, {
|
|
4740
|
+
get(target, prop, receiver) {
|
|
4741
|
+
if (prop === "chat" && chatProxy) return chatProxy;
|
|
4742
|
+
if (prop === "embeddings" && embeddingsProxy) return embeddingsProxy;
|
|
4743
|
+
return Reflect.get(target, prop, receiver);
|
|
4744
|
+
}
|
|
4745
|
+
});
|
|
4746
|
+
wrapped.set(client, proxy);
|
|
4747
|
+
wrapped.set(proxy, proxy);
|
|
4748
|
+
return proxy;
|
|
4749
|
+
}
|
|
4750
|
+
var meterScope, wrapped;
|
|
4751
|
+
var init_model_meter = __esm({
|
|
4752
|
+
"src/runtime/model-meter.ts"() {
|
|
4753
|
+
meterScope = new AsyncLocalStorage();
|
|
4754
|
+
wrapped = /* @__PURE__ */ new WeakMap();
|
|
4755
|
+
}
|
|
4756
|
+
});
|
|
4678
4757
|
function configureDefaultOpenAI(cred) {
|
|
4679
4758
|
credential = cred;
|
|
4680
4759
|
cached = null;
|
|
4681
4760
|
}
|
|
4761
|
+
function configureHelperCallCredential(source) {
|
|
4762
|
+
callCredentialSource = source;
|
|
4763
|
+
cached = null;
|
|
4764
|
+
}
|
|
4765
|
+
function helperKeyIsCustomers() {
|
|
4766
|
+
if (credential) return true;
|
|
4767
|
+
return callCredentialSource !== "worker-token";
|
|
4768
|
+
}
|
|
4682
4769
|
function defaultOpenAIOptions() {
|
|
4683
4770
|
if (!credential) return {};
|
|
4684
4771
|
return {
|
|
@@ -4686,27 +4773,31 @@ function defaultOpenAIOptions() {
|
|
|
4686
4773
|
...credential.baseURL ? { baseURL: credential.baseURL } : {}
|
|
4687
4774
|
};
|
|
4688
4775
|
}
|
|
4689
|
-
function internalChatClient(
|
|
4776
|
+
function internalChatClient() {
|
|
4690
4777
|
const cred = defaultOpenAIOptions();
|
|
4691
4778
|
return createChatClient({
|
|
4692
4779
|
...cred.apiKey ? { apiKey: cred.apiKey } : {},
|
|
4693
|
-
...cred.baseURL ? { baseURL: cred.baseURL } : {}
|
|
4694
|
-
...opts?.timeoutMs ? { timeoutMs: opts.timeoutMs } : {}
|
|
4780
|
+
...cred.baseURL ? { baseURL: cred.baseURL } : {}
|
|
4695
4781
|
});
|
|
4696
4782
|
}
|
|
4697
4783
|
function defaultOpenAI() {
|
|
4698
|
-
|
|
4784
|
+
const scoped = scope.getStore();
|
|
4785
|
+
if (scoped) return meteredClient(scoped);
|
|
4786
|
+
if (!cached) cached = meteredClient(internalChatClient(), helperKeyIsCustomers());
|
|
4787
|
+
return cached;
|
|
4699
4788
|
}
|
|
4700
4789
|
function hasOpenAiKey() {
|
|
4701
4790
|
return scope.getStore() !== void 0 || credential !== null || Boolean(process.env["OPENAI_API_KEY"]);
|
|
4702
4791
|
}
|
|
4703
|
-
var credential, cached, scope;
|
|
4792
|
+
var credential, cached, scope, callCredentialSource;
|
|
4704
4793
|
var init_openai_default = __esm({
|
|
4705
4794
|
"src/runtime/openai-default.ts"() {
|
|
4706
4795
|
init_dist();
|
|
4796
|
+
init_model_meter();
|
|
4707
4797
|
credential = null;
|
|
4708
4798
|
cached = null;
|
|
4709
4799
|
scope = new AsyncLocalStorage();
|
|
4800
|
+
callCredentialSource = null;
|
|
4710
4801
|
}
|
|
4711
4802
|
});
|
|
4712
4803
|
function ipv4Parts(ip) {
|
|
@@ -5743,7 +5834,9 @@ var init_metrics = __esm({
|
|
|
5743
5834
|
}
|
|
5744
5835
|
});
|
|
5745
5836
|
function recordTurnLatencySpan(args) {
|
|
5746
|
-
if (!Number.isFinite(args.latencyMs) || args.latencyMs
|
|
5837
|
+
if (!Number.isFinite(args.latencyMs) || args.latencyMs < 0)
|
|
5838
|
+
return;
|
|
5839
|
+
if (args.latencyMs === 0 && !args.allowZero)
|
|
5747
5840
|
return;
|
|
5748
5841
|
const attributes = {
|
|
5749
5842
|
[ATTR.projectId]: args.projectId,
|
|
@@ -9137,19 +9230,22 @@ function parseArgs(raw) {
|
|
|
9137
9230
|
}
|
|
9138
9231
|
function defaultComplete() {
|
|
9139
9232
|
if (!hasOpenAiKey()) return null;
|
|
9140
|
-
const client = internalChatClient({ timeoutMs: RUNNER_TIMEOUT_MS });
|
|
9141
9233
|
return async ({ model: model2, temperature, messages, tools }) => {
|
|
9234
|
+
const client = defaultOpenAI();
|
|
9142
9235
|
const res = await withHelperModelFallback({
|
|
9143
9236
|
client,
|
|
9144
9237
|
model: model2,
|
|
9145
9238
|
fallbackModel: RUNNER_MODEL,
|
|
9146
9239
|
helper: "agent runner",
|
|
9147
|
-
call: (m) => client.chat.completions.create(
|
|
9148
|
-
|
|
9149
|
-
|
|
9150
|
-
|
|
9151
|
-
|
|
9152
|
-
|
|
9240
|
+
call: (m) => client.chat.completions.create(
|
|
9241
|
+
{
|
|
9242
|
+
model: m,
|
|
9243
|
+
...chatCompletionParams(m, { ...temperature !== void 0 ? { temperature } : {}, reasoningEffort: "low" }),
|
|
9244
|
+
messages,
|
|
9245
|
+
...tools.length ? { tools } : {}
|
|
9246
|
+
},
|
|
9247
|
+
{ timeout: RUNNER_TIMEOUT_MS }
|
|
9248
|
+
),
|
|
9153
9249
|
warn: graphWarn
|
|
9154
9250
|
});
|
|
9155
9251
|
const msg = res.choices[0]?.message;
|
|
@@ -11899,6 +11995,9 @@ var MemoryClient = class {
|
|
|
11899
11995
|
);
|
|
11900
11996
|
}
|
|
11901
11997
|
};
|
|
11998
|
+
|
|
11999
|
+
// src/calls.ts
|
|
12000
|
+
init_src();
|
|
11902
12001
|
var CallSchema = z.object({
|
|
11903
12002
|
id: z.string(),
|
|
11904
12003
|
projectId: z.string(),
|
|
@@ -11957,6 +12056,7 @@ z.object({
|
|
|
11957
12056
|
});
|
|
11958
12057
|
var CallStateSyncResponse = z.object({ ok: z.literal(true) }).or(z.object({}).passthrough());
|
|
11959
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());
|
|
11960
12060
|
var UsageReportResponse = z.object({ recorded: z.boolean() }).or(z.object({}).passthrough());
|
|
11961
12061
|
var CallEventEnvelopeResponse = z.object({
|
|
11962
12062
|
event: z.object({
|
|
@@ -11995,13 +12095,28 @@ var CallsClient = class {
|
|
|
11995
12095
|
...input.attributes !== void 0 ? { attributes: input.attributes } : {},
|
|
11996
12096
|
...input.participants !== void 0 ? { participants: input.participants } : {},
|
|
11997
12097
|
...input.legs !== void 0 ? { legs: input.legs } : {},
|
|
11998
|
-
...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 } : {}
|
|
11999
12101
|
}
|
|
12000
12102
|
},
|
|
12001
12103
|
CallByRoomResponse
|
|
12002
12104
|
);
|
|
12003
12105
|
return call;
|
|
12004
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
|
+
}
|
|
12005
12120
|
async syncState(callId, input) {
|
|
12006
12121
|
await this.transport.request(
|
|
12007
12122
|
{
|
|
@@ -12035,11 +12150,12 @@ var CallsClient = class {
|
|
|
12035
12150
|
}
|
|
12036
12151
|
// One-shot cascade usage report at session close — the platform prices it
|
|
12037
12152
|
// into the call's bill (voice_components). Idempotent server-side.
|
|
12038
|
-
async reportUsage(callId, report) {
|
|
12153
|
+
async reportUsage(callId, report, opts = {}) {
|
|
12039
12154
|
await this.transport.request(
|
|
12040
12155
|
{
|
|
12041
12156
|
method: "POST",
|
|
12042
12157
|
path: `/v1/calls/${encodeURIComponent(callId)}/usage-report`,
|
|
12158
|
+
...opts.signal ? { signal: opts.signal } : {},
|
|
12043
12159
|
body: {
|
|
12044
12160
|
durationMs: report.durationMs,
|
|
12045
12161
|
llm: report.llm,
|
|
@@ -12050,6 +12166,22 @@ var CallsClient = class {
|
|
|
12050
12166
|
UsageReportResponse
|
|
12051
12167
|
);
|
|
12052
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
|
+
}
|
|
12053
12185
|
async appendDtmfEvent(callId, input) {
|
|
12054
12186
|
await this.transport.request(
|
|
12055
12187
|
{
|
|
@@ -13219,6 +13351,38 @@ function buildRedisBrainPubSub(redisUrl) {
|
|
|
13219
13351
|
// src/runtime/pipeline-from-config.ts
|
|
13220
13352
|
init_providers2();
|
|
13221
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
|
|
13222
13386
|
function modelsFromWireConfig(p, creds2) {
|
|
13223
13387
|
const language = p.language ?? "en-US";
|
|
13224
13388
|
if (p.mode === "realtime" && p.realtime) {
|
|
@@ -13232,17 +13396,20 @@ function modelsFromWireConfig(p, creds2) {
|
|
|
13232
13396
|
out.tts = ttsFactory(p.voice.provider, p.voice.voiceId, p.voice.model, creds2?.[p.voice.provider]);
|
|
13233
13397
|
return out;
|
|
13234
13398
|
}
|
|
13399
|
+
function owned(factory, creds2) {
|
|
13400
|
+
return creds2 ? vaultKeyedFactory(factory) : factory;
|
|
13401
|
+
}
|
|
13235
13402
|
function realtimeFactory(r, creds2) {
|
|
13236
|
-
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);
|
|
13237
13404
|
}
|
|
13238
13405
|
function llmFactory(provider, model2, creds2) {
|
|
13239
|
-
return resolveLlm(provider, { model: model2, ...creds2 ? { creds: creds2 } : {} });
|
|
13406
|
+
return owned(resolveLlm(provider, { model: model2, ...creds2 ? { creds: creds2 } : {} }), creds2);
|
|
13240
13407
|
}
|
|
13241
13408
|
function sttFactory(provider, model2, language, creds2) {
|
|
13242
|
-
return resolveStt(provider, { ...model2 ? { model: model2 } : {}, language, ...creds2 ? { creds: creds2 } : {} });
|
|
13409
|
+
return owned(resolveStt(provider, { ...model2 ? { model: model2 } : {}, language, ...creds2 ? { creds: creds2 } : {} }), creds2);
|
|
13243
13410
|
}
|
|
13244
13411
|
function ttsFactory(provider, voiceId, model2, creds2) {
|
|
13245
|
-
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);
|
|
13246
13413
|
}
|
|
13247
13414
|
|
|
13248
13415
|
// src/agent.ts
|
|
@@ -15428,6 +15595,9 @@ function extractText2(msg) {
|
|
|
15428
15595
|
const trimmed = txt.trim();
|
|
15429
15596
|
return trimmed.length === 0 ? null : trimmed;
|
|
15430
15597
|
}
|
|
15598
|
+
|
|
15599
|
+
// src/runtime/handoff.ts
|
|
15600
|
+
init_src();
|
|
15431
15601
|
function createHandoffRuntime(config, deps) {
|
|
15432
15602
|
return {
|
|
15433
15603
|
async handoff(to, opts) {
|
|
@@ -15576,7 +15746,9 @@ function buildDefaultSipOps(trunkOverride, fromNumber) {
|
|
|
15576
15746
|
participantIdentity: identity,
|
|
15577
15747
|
playDialtone: false,
|
|
15578
15748
|
waitUntilAnswered: true,
|
|
15579
|
-
...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
|
|
15580
15752
|
});
|
|
15581
15753
|
return { identity };
|
|
15582
15754
|
}
|
|
@@ -16100,16 +16272,16 @@ async function exchangeWorkerToken(input) {
|
|
|
16100
16272
|
const internalToken = process.env["INTERNAL_SERVICE_TOKEN"];
|
|
16101
16273
|
if (!apiUrl || !internalToken) return null;
|
|
16102
16274
|
const delays = input.retryDelaysMs ?? EXCHANGE_RETRY_DELAYS_MS;
|
|
16103
|
-
const
|
|
16275
|
+
const deadline2 = Date.now() + (input.budgetMs ?? EXCHANGE_BUDGET_MS);
|
|
16104
16276
|
for (let attempt = 0; ; attempt++) {
|
|
16105
|
-
const left =
|
|
16277
|
+
const left = deadline2 - Date.now();
|
|
16106
16278
|
if (left <= 0) return null;
|
|
16107
16279
|
const r = await exchangeOnce(input, apiUrl, internalToken, Math.min(input.attemptTimeoutMs ?? EXCHANGE_ATTEMPT_TIMEOUT_MS, left));
|
|
16108
16280
|
if ("token" in r) return r.token;
|
|
16109
16281
|
if ("stale" in r) throw new StaleRouteError(r.stale);
|
|
16110
16282
|
const base = delays[attempt];
|
|
16111
16283
|
const wait = base === void 0 ? void 0 : Math.max(base, "retryAfterMs" in r && r.retryAfterMs !== void 0 ? r.retryAfterMs : 0);
|
|
16112
|
-
if (r.final || wait === void 0 || Date.now() + wait >=
|
|
16284
|
+
if (r.final || wait === void 0 || Date.now() + wait >= deadline2) return null;
|
|
16113
16285
|
await new Promise((resolve) => setTimeout(resolve, wait));
|
|
16114
16286
|
}
|
|
16115
16287
|
}
|
|
@@ -16308,7 +16480,7 @@ async function answerTextTurn(deps) {
|
|
|
16308
16480
|
);
|
|
16309
16481
|
const brain = pipeline?.brain === "bound";
|
|
16310
16482
|
const label = pipeline?.llm ?? { provider: "openai", model: model2.model };
|
|
16311
|
-
const byok =
|
|
16483
|
+
const byok = builtWithVaultKey(model2);
|
|
16312
16484
|
const usage = brain ? [{ provider: "brain", model: model2.model || "brain", inputTokens: 0, outputTokens: 0, byok: true }] : [];
|
|
16313
16485
|
const onMetrics = (m) => {
|
|
16314
16486
|
if (brain) return;
|
|
@@ -16955,8 +17127,18 @@ function guardIsActive(opts) {
|
|
|
16955
17127
|
return false;
|
|
16956
17128
|
}
|
|
16957
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
|
+
|
|
16958
17139
|
// src/runtime/call-sync.ts
|
|
16959
17140
|
var END_REASON_WAIT_MS = 2e3;
|
|
17141
|
+
var FINALIZE_WRITES_WAIT_MS = 3e3;
|
|
16960
17142
|
function makeNoopHandle(callId) {
|
|
16961
17143
|
return {
|
|
16962
17144
|
callId,
|
|
@@ -16968,6 +17150,13 @@ function makeNoopHandle(callId) {
|
|
|
16968
17150
|
publishDtmf() {
|
|
16969
17151
|
},
|
|
16970
17152
|
async recordEndReason() {
|
|
17153
|
+
},
|
|
17154
|
+
publishTurnLatency() {
|
|
17155
|
+
},
|
|
17156
|
+
track(write) {
|
|
17157
|
+
return write;
|
|
17158
|
+
},
|
|
17159
|
+
async finalize() {
|
|
16971
17160
|
}
|
|
16972
17161
|
};
|
|
16973
17162
|
}
|
|
@@ -16980,6 +17169,16 @@ async function createCallSyncHandle(init) {
|
|
|
16980
17169
|
});
|
|
16981
17170
|
return makeNoopHandle(init.call.callId);
|
|
16982
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
|
+
};
|
|
16983
17182
|
return {
|
|
16984
17183
|
callId,
|
|
16985
17184
|
resolved: true,
|
|
@@ -16989,13 +17188,13 @@ async function createCallSyncHandle(init) {
|
|
|
16989
17188
|
startMs,
|
|
16990
17189
|
(input.endAt ?? input.at).getTime() - init.call.startedAt.getTime()
|
|
16991
17190
|
);
|
|
16992
|
-
void init.client.calls.appendTranscriptSegment(callId, {
|
|
17191
|
+
void track(init.client.calls.appendTranscriptSegment(callId, {
|
|
16993
17192
|
speaker: input.speaker,
|
|
16994
17193
|
text: input.text,
|
|
16995
17194
|
startMs,
|
|
16996
17195
|
endMs,
|
|
16997
17196
|
final: input.final
|
|
16998
|
-
}).catch((err) => {
|
|
17197
|
+
})).catch((err) => {
|
|
16999
17198
|
console.warn("[sdk.call-sync] segment publish failed", {
|
|
17000
17199
|
speaker: input.speaker,
|
|
17001
17200
|
err: err instanceof Error ? err.message : err
|
|
@@ -17003,12 +17202,43 @@ async function createCallSyncHandle(init) {
|
|
|
17003
17202
|
});
|
|
17004
17203
|
},
|
|
17005
17204
|
syncState(input) {
|
|
17006
|
-
void init.client.calls.syncState(callId, input).catch((err) => {
|
|
17205
|
+
void track(init.client.calls.syncState(callId, input)).catch((err) => {
|
|
17007
17206
|
console.warn("[sdk.call-sync] state sync failed", {
|
|
17008
17207
|
err: err instanceof Error ? err.message : err
|
|
17009
17208
|
});
|
|
17010
17209
|
});
|
|
17011
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
|
+
},
|
|
17012
17242
|
async recordEndReason(reason) {
|
|
17013
17243
|
let timer;
|
|
17014
17244
|
try {
|
|
@@ -17028,7 +17258,7 @@ async function createCallSyncHandle(init) {
|
|
|
17028
17258
|
}
|
|
17029
17259
|
},
|
|
17030
17260
|
publishDtmf(input) {
|
|
17031
|
-
void init.client.calls.appendDtmfEvent(callId, input).catch((err) => {
|
|
17261
|
+
void track(init.client.calls.appendDtmfEvent(callId, input)).catch((err) => {
|
|
17032
17262
|
console.warn("[sdk.call-sync] dtmf publish failed", {
|
|
17033
17263
|
direction: input.direction,
|
|
17034
17264
|
digit: input.digit,
|
|
@@ -17053,7 +17283,10 @@ async function resolveCallIdWithRetry(init) {
|
|
|
17053
17283
|
attributes: initialAttributes(init.call),
|
|
17054
17284
|
participants: initialParticipants(init.call),
|
|
17055
17285
|
legs: initialLegs(init.call, init.roomName),
|
|
17056
|
-
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
|
|
17057
17290
|
});
|
|
17058
17291
|
return summary.id;
|
|
17059
17292
|
} catch {
|
|
@@ -17123,7 +17356,7 @@ function initialLegs(call, roomName) {
|
|
|
17123
17356
|
startedAt
|
|
17124
17357
|
}
|
|
17125
17358
|
];
|
|
17126
|
-
if (destination) {
|
|
17359
|
+
if (destination && call.metadata["direction"] !== "outbound") {
|
|
17127
17360
|
legs.unshift({
|
|
17128
17361
|
legKey: "primary-inbound",
|
|
17129
17362
|
kind: "sip-inbound",
|
|
@@ -17143,26 +17376,29 @@ function looksLikeSipParticipant(value) {
|
|
|
17143
17376
|
|
|
17144
17377
|
// src/runtime/metrics-bridge.ts
|
|
17145
17378
|
init_dist2();
|
|
17379
|
+
var payerKey = (byok) => byok === void 0 ? "" : byok ? "byok" : "platform";
|
|
17146
17380
|
var UsageAccumulator = class {
|
|
17147
17381
|
llm = /* @__PURE__ */ new Map();
|
|
17148
17382
|
tts = /* @__PURE__ */ new Map();
|
|
17149
17383
|
stt = /* @__PURE__ */ new Map();
|
|
17150
|
-
addLlm(provider, model2, inputTokens, outputTokens) {
|
|
17151
|
-
const key = `${provider}:${model2}`;
|
|
17152
|
-
const entry2 = this.llm.get(key) ?? { provider, model: model2, inputTokens: 0, outputTokens: 0 };
|
|
17384
|
+
addLlm(provider, model2, inputTokens, outputTokens, byok) {
|
|
17385
|
+
const key = `${provider}:${model2}:${payerKey(byok)}`;
|
|
17386
|
+
const entry2 = this.llm.get(key) ?? { provider, model: model2, inputTokens: 0, outputTokens: 0, ...byok !== void 0 ? { byok } : {} };
|
|
17153
17387
|
entry2.inputTokens += Math.max(0, inputTokens || 0);
|
|
17154
17388
|
entry2.outputTokens += Math.max(0, outputTokens || 0);
|
|
17155
17389
|
this.llm.set(key, entry2);
|
|
17156
17390
|
}
|
|
17157
|
-
addTts(provider, chars) {
|
|
17158
|
-
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 } : {} };
|
|
17159
17394
|
entry2.chars += Math.max(0, chars || 0);
|
|
17160
|
-
this.tts.set(
|
|
17395
|
+
this.tts.set(key, entry2);
|
|
17161
17396
|
}
|
|
17162
|
-
addStt(provider, seconds) {
|
|
17163
|
-
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 } : {} };
|
|
17164
17400
|
entry2.seconds += Math.max(0, seconds || 0);
|
|
17165
|
-
this.stt.set(
|
|
17401
|
+
this.stt.set(key, entry2);
|
|
17166
17402
|
}
|
|
17167
17403
|
isEmpty() {
|
|
17168
17404
|
return this.llm.size === 0 && this.tts.size === 0 && this.stt.size === 0;
|
|
@@ -17215,10 +17451,13 @@ function attachEndpointingProbe(session, events, ctx, deps) {
|
|
|
17215
17451
|
});
|
|
17216
17452
|
recordTurnLatencySpan({ ...base, stage: "endpointing", latencyMs: delayMs });
|
|
17217
17453
|
recordTurnLatencySpan({ ...base, stage: "stt", latencyMs: delayMs });
|
|
17454
|
+
ctx.turns?.note({ stage: "endpointing", latencyMs: delayMs });
|
|
17455
|
+
ctx.turns?.note({ stage: "stt", latencyMs: delayMs });
|
|
17218
17456
|
};
|
|
17219
17457
|
deps.vad.onSpeechEnd((sample) => {
|
|
17220
17458
|
if (!armed) return;
|
|
17221
17459
|
speechEndAt = sample.speechEndAt;
|
|
17460
|
+
ctx.turns?.noteSpeechEnd(sample.speechEndAt);
|
|
17222
17461
|
tryEmit();
|
|
17223
17462
|
});
|
|
17224
17463
|
session.on(events.userStateChanged, (event) => {
|
|
@@ -17260,7 +17499,8 @@ function dispatch(metrics2, ctx) {
|
|
|
17260
17499
|
provider,
|
|
17261
17500
|
...model2 ? { model: model2 } : {},
|
|
17262
17501
|
inputTokens: m.promptTokens,
|
|
17263
|
-
outputTokens: m.completionTokens
|
|
17502
|
+
outputTokens: m.completionTokens,
|
|
17503
|
+
...ctx.keyOwner?.llm ? { byok: ctx.keyOwner.llm.byok } : {}
|
|
17264
17504
|
});
|
|
17265
17505
|
recordLlmUsage({
|
|
17266
17506
|
...base,
|
|
@@ -17269,9 +17509,14 @@ function dispatch(metrics2, ctx) {
|
|
|
17269
17509
|
inputTokens: m.promptTokens,
|
|
17270
17510
|
outputTokens: m.completionTokens
|
|
17271
17511
|
});
|
|
17272
|
-
if (
|
|
17512
|
+
if (ctx.modelClock) {
|
|
17513
|
+
const startAt = m.timestamp - Math.max(0, m.durationMs);
|
|
17514
|
+
const waitedMs = typeof m.ttftMs === "number" && m.ttftMs >= 0 ? m.ttftMs : Math.max(0, m.durationMs);
|
|
17515
|
+
ctx.modelClock.noteReplyModelTime({ startAt, endAt: startAt + waitedMs });
|
|
17516
|
+
} else if (typeof m.ttftMs === "number" && m.ttftMs >= 0) {
|
|
17273
17517
|
recordTurnLatency({ ...base, firstTokenMs: m.ttftMs });
|
|
17274
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) });
|
|
17275
17520
|
}
|
|
17276
17521
|
break;
|
|
17277
17522
|
}
|
|
@@ -17279,7 +17524,7 @@ function dispatch(metrics2, ctx) {
|
|
|
17279
17524
|
const m = metrics2;
|
|
17280
17525
|
const provider = m.metadata?.modelProvider ?? "unknown";
|
|
17281
17526
|
const model2 = m.metadata?.modelName;
|
|
17282
|
-
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 } : {} });
|
|
17283
17528
|
recordTtsUsage({
|
|
17284
17529
|
...base,
|
|
17285
17530
|
provider,
|
|
@@ -17289,6 +17534,7 @@ function dispatch(metrics2, ctx) {
|
|
|
17289
17534
|
if (typeof m.ttfbMs === "number" && m.ttfbMs >= 0) {
|
|
17290
17535
|
recordTurnLatency({ ...base, ttsStartMs: m.ttfbMs });
|
|
17291
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) });
|
|
17292
17538
|
}
|
|
17293
17539
|
break;
|
|
17294
17540
|
}
|
|
@@ -17296,7 +17542,12 @@ function dispatch(metrics2, ctx) {
|
|
|
17296
17542
|
const m = metrics2;
|
|
17297
17543
|
const provider = m.metadata?.modelProvider ?? "unknown";
|
|
17298
17544
|
const model2 = m.metadata?.modelName;
|
|
17299
|
-
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
|
+
});
|
|
17300
17551
|
recordSttUsage({
|
|
17301
17552
|
...base,
|
|
17302
17553
|
provider,
|
|
@@ -17319,6 +17570,7 @@ function dispatch(metrics2, ctx) {
|
|
|
17319
17570
|
if (typeof m.ttftMs === "number" && m.ttftMs >= 0) {
|
|
17320
17571
|
recordTurnLatency({ ...base, firstTokenMs: m.ttftMs });
|
|
17321
17572
|
recordTurnLatencySpan({ ...base, stage: "llm", latencyMs: m.ttftMs, realtime: true });
|
|
17573
|
+
ctx.turns?.note({ stage: "llm", latencyMs: m.ttftMs, startedAt: m.timestamp, realtime: true });
|
|
17322
17574
|
}
|
|
17323
17575
|
if (typeof m.sessionDurationMs === "number" && m.sessionDurationMs > 0) {
|
|
17324
17576
|
recordRealtimeSessionDuration({
|
|
@@ -17340,6 +17592,11 @@ function dispatch(metrics2, ctx) {
|
|
|
17340
17592
|
});
|
|
17341
17593
|
recordTurnLatencySpan({ ...base, stage: "endpointing", latencyMs: m.endOfUtteranceDelayMs });
|
|
17342
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
|
+
}
|
|
17343
17600
|
break;
|
|
17344
17601
|
}
|
|
17345
17602
|
case "interruption_metrics": {
|
|
@@ -17372,14 +17629,15 @@ function createTelemetrySink(accumulator = new UsageAccumulator()) {
|
|
|
17372
17629
|
sample.provider,
|
|
17373
17630
|
sample.model ?? "unknown",
|
|
17374
17631
|
sample.inputTokens ?? 0,
|
|
17375
|
-
sample.outputTokens ?? 0
|
|
17632
|
+
sample.outputTokens ?? 0,
|
|
17633
|
+
sample.byok
|
|
17376
17634
|
);
|
|
17377
17635
|
break;
|
|
17378
17636
|
case "tts":
|
|
17379
|
-
accumulator.addTts(sample.provider, sample.chars ?? 0);
|
|
17637
|
+
accumulator.addTts(sample.provider, sample.chars ?? 0, sample.byok);
|
|
17380
17638
|
break;
|
|
17381
17639
|
case "stt":
|
|
17382
|
-
accumulator.addStt(sample.provider, sample.seconds ?? 0);
|
|
17640
|
+
accumulator.addStt(sample.provider, sample.seconds ?? 0, sample.byok);
|
|
17383
17641
|
break;
|
|
17384
17642
|
}
|
|
17385
17643
|
},
|
|
@@ -17392,84 +17650,467 @@ function createTelemetrySink(accumulator = new UsageAccumulator()) {
|
|
|
17392
17650
|
};
|
|
17393
17651
|
}
|
|
17394
17652
|
|
|
17395
|
-
// src/runtime/
|
|
17396
|
-
|
|
17397
|
-
|
|
17398
|
-
|
|
17399
|
-
|
|
17400
|
-
|
|
17401
|
-
|
|
17402
|
-
|
|
17403
|
-
|
|
17404
|
-
const
|
|
17405
|
-
|
|
17406
|
-
|
|
17407
|
-
const originalNext = stream.next.bind(stream);
|
|
17408
|
-
stream.next = async () => {
|
|
17409
|
-
const result = await originalNext();
|
|
17410
|
-
try {
|
|
17411
|
-
const event = result.value;
|
|
17412
|
-
if (!result.done && event && event.type === opts.endOfSpeechType) {
|
|
17413
|
-
const silenceMs = numberOr(event.silenceDuration, 0);
|
|
17414
|
-
const inferenceMs = numberOr(event.inferenceDuration, 0);
|
|
17415
|
-
const sample = {
|
|
17416
|
-
speechEndAt: now() - silenceMs - inferenceMs,
|
|
17417
|
-
silenceMs,
|
|
17418
|
-
inferenceMs
|
|
17419
|
-
};
|
|
17420
|
-
for (const listener of [...listeners]) listener(sample);
|
|
17421
|
-
}
|
|
17422
|
-
} catch (err) {
|
|
17423
|
-
console.warn("[agent] vad tap failed", err);
|
|
17424
|
-
}
|
|
17425
|
-
return result;
|
|
17426
|
-
};
|
|
17427
|
-
return stream;
|
|
17653
|
+
// src/runtime/call-model-usage.ts
|
|
17654
|
+
init_dist2();
|
|
17655
|
+
var MAX_UNTAGGED = 32;
|
|
17656
|
+
function createCallModelMeter(usage) {
|
|
17657
|
+
let base = null;
|
|
17658
|
+
let clock;
|
|
17659
|
+
let untagged = [];
|
|
17660
|
+
let inFlight = 0;
|
|
17661
|
+
let idle = [];
|
|
17662
|
+
const otel = (t) => {
|
|
17663
|
+
if (base) recordLlmUsage({ ...base, ...t });
|
|
17664
|
+
else if (untagged.length < MAX_UNTAGGED) untagged.push(t);
|
|
17428
17665
|
};
|
|
17429
17666
|
return {
|
|
17430
|
-
|
|
17431
|
-
|
|
17432
|
-
|
|
17667
|
+
listener: {
|
|
17668
|
+
started() {
|
|
17669
|
+
inFlight += 1;
|
|
17670
|
+
},
|
|
17671
|
+
finished(call) {
|
|
17672
|
+
inFlight = Math.max(0, inFlight - 1);
|
|
17673
|
+
try {
|
|
17674
|
+
const tokens = call.usage;
|
|
17675
|
+
if (tokens && tokens.inputTokens + tokens.outputTokens > 0) {
|
|
17676
|
+
usage.record({
|
|
17677
|
+
kind: "llm",
|
|
17678
|
+
provider: call.provider,
|
|
17679
|
+
model: call.model,
|
|
17680
|
+
inputTokens: tokens.inputTokens,
|
|
17681
|
+
outputTokens: tokens.outputTokens,
|
|
17682
|
+
...call.byok !== void 0 ? { byok: call.byok } : {}
|
|
17683
|
+
});
|
|
17684
|
+
otel({ provider: call.provider, model: call.model, inputTokens: tokens.inputTokens, outputTokens: tokens.outputTokens });
|
|
17685
|
+
}
|
|
17686
|
+
clock?.noteModelTime({ startAt: call.startedAt, endAt: call.endedAt });
|
|
17687
|
+
} finally {
|
|
17688
|
+
if (inFlight === 0 && idle.length > 0) {
|
|
17689
|
+
const waiting = idle;
|
|
17690
|
+
idle = [];
|
|
17691
|
+
for (const resolve of waiting) resolve();
|
|
17692
|
+
}
|
|
17693
|
+
}
|
|
17694
|
+
}
|
|
17695
|
+
},
|
|
17696
|
+
bind(b, c) {
|
|
17697
|
+
base = b;
|
|
17698
|
+
clock = c;
|
|
17699
|
+
const early = untagged;
|
|
17700
|
+
untagged = [];
|
|
17701
|
+
for (const t of early) recordLlmUsage({ ...b, ...t });
|
|
17702
|
+
},
|
|
17703
|
+
drain(timeoutMs) {
|
|
17704
|
+
if (inFlight === 0) return Promise.resolve();
|
|
17705
|
+
return new Promise((resolve) => {
|
|
17706
|
+
const timer = setTimeout(done, timeoutMs);
|
|
17707
|
+
timer.unref?.();
|
|
17708
|
+
function done() {
|
|
17709
|
+
clearTimeout(timer);
|
|
17710
|
+
idle = idle.filter((r) => r !== done);
|
|
17711
|
+
resolve();
|
|
17712
|
+
}
|
|
17713
|
+
idle.push(done);
|
|
17714
|
+
});
|
|
17433
17715
|
}
|
|
17434
17716
|
};
|
|
17435
17717
|
}
|
|
17436
|
-
|
|
17437
|
-
|
|
17438
|
-
|
|
17439
|
-
|
|
17440
|
-
|
|
17441
|
-
|
|
17718
|
+
var HANGUP_DRAIN_MS = 3e3;
|
|
17719
|
+
async function reportUsageAtClose(opts) {
|
|
17720
|
+
try {
|
|
17721
|
+
await opts.meter.drain(opts.drainMs ?? HANGUP_DRAIN_MS);
|
|
17722
|
+
if (opts.usage.isEmpty()) return;
|
|
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
|
+
}
|
|
17729
|
+
} catch (err) {
|
|
17730
|
+
opts.warn?.("[agent] usage report failed", { err: err instanceof Error ? err.message : String(err) });
|
|
17442
17731
|
}
|
|
17443
|
-
return moduleUrl;
|
|
17444
17732
|
}
|
|
17445
|
-
|
|
17446
|
-
|
|
17447
|
-
|
|
17448
|
-
|
|
17449
|
-
|
|
17450
|
-
|
|
17451
|
-
|
|
17452
|
-
const
|
|
17453
|
-
|
|
17454
|
-
|
|
17455
|
-
|
|
17456
|
-
|
|
17457
|
-
|
|
17458
|
-
|
|
17459
|
-
|
|
17460
|
-
|
|
17461
|
-
if (
|
|
17462
|
-
|
|
17463
|
-
|
|
17464
|
-
|
|
17465
|
-
|
|
17466
|
-
|
|
17467
|
-
|
|
17733
|
+
var MAX_INTERVALS_PER_TURN = 64;
|
|
17734
|
+
var REPLY_METRICS_WAIT_MS = 1500;
|
|
17735
|
+
function modelTimeWithin(intervals, from, to) {
|
|
17736
|
+
const clipped = intervals.map((i) => ({ startAt: Math.max(i.startAt, from), endAt: Math.min(i.endAt, to) })).filter((i) => i.endAt > i.startAt).sort((a, b) => a.startAt - b.startAt);
|
|
17737
|
+
let total = 0;
|
|
17738
|
+
let runStart = -Infinity;
|
|
17739
|
+
let runEnd = -Infinity;
|
|
17740
|
+
for (const i of clipped) {
|
|
17741
|
+
if (i.startAt > runEnd) {
|
|
17742
|
+
if (runEnd > runStart) total += runEnd - runStart;
|
|
17743
|
+
runStart = i.startAt;
|
|
17744
|
+
runEnd = i.endAt;
|
|
17745
|
+
} else if (i.endAt > runEnd) {
|
|
17746
|
+
runEnd = i.endAt;
|
|
17747
|
+
}
|
|
17748
|
+
}
|
|
17749
|
+
if (runEnd > runStart) total += runEnd - runStart;
|
|
17750
|
+
return total;
|
|
17751
|
+
}
|
|
17752
|
+
var newTurn = () => ({ windowStart: 0, windowEnd: 0, intervals: [], modelReply: false, repliesPending: 0 });
|
|
17753
|
+
function attachTurnModelClock(session, events, ctx, deps = {}) {
|
|
17754
|
+
const now = deps.now ?? Date.now;
|
|
17755
|
+
const replyWaitMs = deps.replyWaitMs ?? REPLY_METRICS_WAIT_MS;
|
|
17756
|
+
const base = { projectId: ctx.projectId, callId: ctx.callId, ...ctx.campaignId ? { campaignId: ctx.campaignId } : {} };
|
|
17757
|
+
let phase = "idle";
|
|
17758
|
+
let turn = newTurn();
|
|
17759
|
+
let parked = null;
|
|
17760
|
+
let outsidePending = 0;
|
|
17761
|
+
let abandonedPending = 0;
|
|
17762
|
+
let spokeWhileDeciding = false;
|
|
17763
|
+
const safely2 = (fn) => {
|
|
17764
|
+
try {
|
|
17765
|
+
fn();
|
|
17766
|
+
} catch (err) {
|
|
17767
|
+
console.warn("[agent] turn model time record failed", { err: err instanceof Error ? err.message : String(err) });
|
|
17468
17768
|
}
|
|
17469
|
-
|
|
17470
|
-
|
|
17769
|
+
};
|
|
17770
|
+
const record = (t) => {
|
|
17771
|
+
if (t.repliesPending > 0) return;
|
|
17772
|
+
const latencyMs = modelTimeWithin(t.intervals, t.windowStart, t.windowEnd);
|
|
17773
|
+
if (t.modelReply && latencyMs <= 0) return;
|
|
17774
|
+
safely2(() => {
|
|
17775
|
+
recordTurnLatencySpan({ ...base, stage: "llm", latencyMs, allowZero: true });
|
|
17776
|
+
if (latencyMs > 0) recordTurnLatency({ ...base, firstTokenMs: latencyMs });
|
|
17777
|
+
deps.turns?.note({ stage: "llm", latencyMs, replyAt: t.windowEnd });
|
|
17778
|
+
});
|
|
17779
|
+
};
|
|
17780
|
+
const releaseParked = () => {
|
|
17781
|
+
if (!parked) return;
|
|
17782
|
+
clearTimeout(parked.timer);
|
|
17783
|
+
const t = parked.turn;
|
|
17784
|
+
parked = null;
|
|
17785
|
+
abandonedPending += t.repliesPending;
|
|
17786
|
+
record(t);
|
|
17787
|
+
};
|
|
17788
|
+
const flush = () => {
|
|
17789
|
+
if (phase === "answered") {
|
|
17790
|
+
if (turn.repliesPending > 0) {
|
|
17791
|
+
releaseParked();
|
|
17792
|
+
const t = turn;
|
|
17793
|
+
const timer = setTimeout(releaseParked, replyWaitMs);
|
|
17794
|
+
timer.unref?.();
|
|
17795
|
+
parked = { turn: t, timer };
|
|
17796
|
+
} else {
|
|
17797
|
+
record(turn);
|
|
17798
|
+
}
|
|
17799
|
+
} else {
|
|
17800
|
+
abandonedPending += turn.repliesPending;
|
|
17471
17801
|
}
|
|
17472
|
-
|
|
17802
|
+
phase = "idle";
|
|
17803
|
+
turn = newTurn();
|
|
17804
|
+
spokeWhileDeciding = false;
|
|
17805
|
+
};
|
|
17806
|
+
const at = (event) => {
|
|
17807
|
+
const t = event?.createdAt;
|
|
17808
|
+
return typeof t === "number" ? t : now();
|
|
17809
|
+
};
|
|
17810
|
+
session.on(events.userStateChanged, (event) => {
|
|
17811
|
+
if (event?.newState !== "speaking") return;
|
|
17812
|
+
if (phase === "deciding") {
|
|
17813
|
+
spokeWhileDeciding = true;
|
|
17814
|
+
return;
|
|
17815
|
+
}
|
|
17816
|
+
flush();
|
|
17817
|
+
phase = "listening";
|
|
17818
|
+
});
|
|
17819
|
+
session.on(events.userInputTranscribed, (event) => {
|
|
17820
|
+
if (!event?.isFinal) return;
|
|
17821
|
+
if (phase === "deciding" && spokeWhileDeciding) {
|
|
17822
|
+
turn = { ...newTurn(), modelReply: turn.modelReply, repliesPending: turn.repliesPending, windowStart: at(event) };
|
|
17823
|
+
spokeWhileDeciding = false;
|
|
17824
|
+
return;
|
|
17825
|
+
}
|
|
17826
|
+
if (phase !== "idle" && phase !== "listening") return;
|
|
17827
|
+
turn.windowStart = at(event);
|
|
17828
|
+
phase = "deciding";
|
|
17829
|
+
});
|
|
17830
|
+
session.on(events.agentStateChanged, (event) => {
|
|
17831
|
+
if (event?.newState !== "speaking" || phase !== "deciding") return;
|
|
17832
|
+
turn.windowEnd = Math.max(turn.windowStart, at(event));
|
|
17833
|
+
phase = "answered";
|
|
17834
|
+
spokeWhileDeciding = false;
|
|
17835
|
+
});
|
|
17836
|
+
session.on(events.speechCreated, (event) => {
|
|
17837
|
+
if (event?.source === "say") return;
|
|
17838
|
+
if (phase === "deciding") {
|
|
17839
|
+
turn.modelReply = true;
|
|
17840
|
+
turn.repliesPending += 1;
|
|
17841
|
+
} else if (phase === "answered") {
|
|
17842
|
+
turn.repliesPending += 1;
|
|
17843
|
+
} else {
|
|
17844
|
+
outsidePending += 1;
|
|
17845
|
+
}
|
|
17846
|
+
});
|
|
17847
|
+
const push = (t, interval) => {
|
|
17848
|
+
if (t.intervals.length < MAX_INTERVALS_PER_TURN) t.intervals.push(interval);
|
|
17849
|
+
};
|
|
17850
|
+
return {
|
|
17851
|
+
noteModelTime(interval) {
|
|
17852
|
+
push(turn, interval);
|
|
17853
|
+
},
|
|
17854
|
+
noteReplyModelTime(interval) {
|
|
17855
|
+
if (parked && parked.turn.repliesPending > 0) {
|
|
17856
|
+
push(parked.turn, interval);
|
|
17857
|
+
parked.turn.repliesPending -= 1;
|
|
17858
|
+
if (parked.turn.repliesPending === 0) releaseParked();
|
|
17859
|
+
return;
|
|
17860
|
+
}
|
|
17861
|
+
const inTurn = phase === "deciding" || phase === "answered";
|
|
17862
|
+
if (abandonedPending > 0 && inTurn && interval.startAt < turn.windowStart) {
|
|
17863
|
+
abandonedPending -= 1;
|
|
17864
|
+
return;
|
|
17865
|
+
}
|
|
17866
|
+
if (inTurn && turn.repliesPending > 0) {
|
|
17867
|
+
push(turn, interval);
|
|
17868
|
+
turn.repliesPending -= 1;
|
|
17869
|
+
return;
|
|
17870
|
+
}
|
|
17871
|
+
if (abandonedPending > 0) {
|
|
17872
|
+
abandonedPending -= 1;
|
|
17873
|
+
return;
|
|
17874
|
+
}
|
|
17875
|
+
if (outsidePending > 0 || phase === "idle" || phase === "listening") {
|
|
17876
|
+
outsidePending = Math.max(0, outsidePending - 1);
|
|
17877
|
+
const ttftMs = interval.endAt - interval.startAt;
|
|
17878
|
+
if (ttftMs > 0) {
|
|
17879
|
+
safely2(() => {
|
|
17880
|
+
recordTurnLatency({ ...base, firstTokenMs: ttftMs });
|
|
17881
|
+
recordTurnLatencySpan({ ...base, stage: "llm", latencyMs: ttftMs });
|
|
17882
|
+
});
|
|
17883
|
+
}
|
|
17884
|
+
return;
|
|
17885
|
+
}
|
|
17886
|
+
push(turn, interval);
|
|
17887
|
+
},
|
|
17888
|
+
close() {
|
|
17889
|
+
flush();
|
|
17890
|
+
}
|
|
17891
|
+
};
|
|
17892
|
+
}
|
|
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
|
+
|
|
18033
|
+
// src/agent.ts
|
|
18034
|
+
init_model_meter();
|
|
18035
|
+
|
|
18036
|
+
// src/runtime/vad-tap.ts
|
|
18037
|
+
function tapVad(vad, opts) {
|
|
18038
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
18039
|
+
const now = opts.now ?? Date.now;
|
|
18040
|
+
const target = vad;
|
|
18041
|
+
if (!target || typeof target.stream !== "function") {
|
|
18042
|
+
return { onSpeechEnd: () => () => {
|
|
18043
|
+
} };
|
|
18044
|
+
}
|
|
18045
|
+
const originalStream = target.stream.bind(target);
|
|
18046
|
+
target.stream = () => {
|
|
18047
|
+
const stream = originalStream();
|
|
18048
|
+
const originalNext = stream.next.bind(stream);
|
|
18049
|
+
stream.next = async () => {
|
|
18050
|
+
const result = await originalNext();
|
|
18051
|
+
try {
|
|
18052
|
+
const event = result.value;
|
|
18053
|
+
if (!result.done && event && event.type === opts.endOfSpeechType) {
|
|
18054
|
+
const silenceMs = numberOr(event.silenceDuration, 0);
|
|
18055
|
+
const inferenceMs = numberOr(event.inferenceDuration, 0);
|
|
18056
|
+
const sample = {
|
|
18057
|
+
speechEndAt: now() - silenceMs - inferenceMs,
|
|
18058
|
+
silenceMs,
|
|
18059
|
+
inferenceMs
|
|
18060
|
+
};
|
|
18061
|
+
for (const listener of [...listeners]) listener(sample);
|
|
18062
|
+
}
|
|
18063
|
+
} catch (err) {
|
|
18064
|
+
console.warn("[agent] vad tap failed", err);
|
|
18065
|
+
}
|
|
18066
|
+
return result;
|
|
18067
|
+
};
|
|
18068
|
+
return stream;
|
|
18069
|
+
};
|
|
18070
|
+
return {
|
|
18071
|
+
onSpeechEnd(listener) {
|
|
18072
|
+
listeners.add(listener);
|
|
18073
|
+
return () => listeners.delete(listener);
|
|
18074
|
+
}
|
|
18075
|
+
};
|
|
18076
|
+
}
|
|
18077
|
+
function numberOr(value, fallback) {
|
|
18078
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
18079
|
+
}
|
|
18080
|
+
function resolveAgentModulePath(moduleUrl) {
|
|
18081
|
+
if (moduleUrl.startsWith("file://")) {
|
|
18082
|
+
return fileURLToPath(moduleUrl);
|
|
18083
|
+
}
|
|
18084
|
+
return moduleUrl;
|
|
18085
|
+
}
|
|
18086
|
+
|
|
18087
|
+
// src/agent.ts
|
|
18088
|
+
init_dist2();
|
|
18089
|
+
init_src();
|
|
18090
|
+
|
|
18091
|
+
// src/runtime/plan-merge.ts
|
|
18092
|
+
function applyPlanToConfig(base, plan) {
|
|
18093
|
+
const draft = plan.draftConfig;
|
|
18094
|
+
let prompt = base.prompt;
|
|
18095
|
+
if (typeof draft.systemPrompt === "string" && draft.systemPrompt.length > 0) {
|
|
18096
|
+
prompt = draft.systemPrompt;
|
|
18097
|
+
} else if (plan.objective) {
|
|
18098
|
+
prompt = plan.objective;
|
|
18099
|
+
}
|
|
18100
|
+
const routingInstructions = typeof draft.routingInstructions === "string" && draft.routingInstructions.length > 0 ? draft.routingInstructions : base.routingInstructions;
|
|
18101
|
+
let models = base.models;
|
|
18102
|
+
if (draft.language || draft.model || draft.voice) {
|
|
18103
|
+
const baseModels = base.models ?? {};
|
|
18104
|
+
const next = {
|
|
18105
|
+
...baseModels
|
|
18106
|
+
};
|
|
18107
|
+
if (draft.language && next.language === void 0) {
|
|
18108
|
+
next.language = draft.language;
|
|
18109
|
+
}
|
|
18110
|
+
if (draft.model && typeof baseModels.llm !== "object") {
|
|
18111
|
+
next.llm = `${draft.model.provider}/${draft.model.model}`;
|
|
18112
|
+
}
|
|
18113
|
+
if (draft.voice && typeof baseModels.tts !== "object") {
|
|
17473
18114
|
next.tts = `${draft.voice.provider}/${draft.voice.voiceId}`;
|
|
17474
18115
|
}
|
|
17475
18116
|
models = next;
|
|
@@ -17591,6 +18232,230 @@ function actionSpecToAction(spec) {
|
|
|
17591
18232
|
if (spec.kind === "say") return { say: spec.text };
|
|
17592
18233
|
return void 0;
|
|
17593
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
|
+
}
|
|
17594
18459
|
init_src();
|
|
17595
18460
|
var AskHostInput = z.object({
|
|
17596
18461
|
question: z.string().min(1).max(2e3),
|
|
@@ -18141,7 +19006,7 @@ function asText(value) {
|
|
|
18141
19006
|
function masked(value) {
|
|
18142
19007
|
return maskPii(asText(value).slice(0, READ_CAP));
|
|
18143
19008
|
}
|
|
18144
|
-
var
|
|
19009
|
+
var cut2 = (text, cap) => text.length > cap ? `${text.slice(0, cap)}\u2026` : text;
|
|
18145
19010
|
function toolAuditPayload(r) {
|
|
18146
19011
|
const args = masked(r.params);
|
|
18147
19012
|
const result = r.ok ? masked(r.result) : void 0;
|
|
@@ -18150,11 +19015,11 @@ function toolAuditPayload(r) {
|
|
|
18150
19015
|
const payload = {
|
|
18151
19016
|
invocationId: r.invocationId,
|
|
18152
19017
|
toolName: r.toolName,
|
|
18153
|
-
args:
|
|
18154
|
-
...result !== void 0 ? { result:
|
|
19018
|
+
args: cut2(args, cap),
|
|
19019
|
+
...result !== void 0 ? { result: cut2(result, cap) } : {},
|
|
18155
19020
|
ok: r.ok,
|
|
18156
19021
|
blocked: r.blocked,
|
|
18157
|
-
...error !== void 0 ? { errorMessage:
|
|
19022
|
+
...error !== void 0 ? { errorMessage: cut2(error, Math.min(cap, 300)) } : {},
|
|
18158
19023
|
startedAt: r.startedAt.toISOString(),
|
|
18159
19024
|
durationMs: r.durationMs,
|
|
18160
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
|
|
@@ -18174,150 +19039,6 @@ function toolAuditObserver(emit) {
|
|
|
18174
19039
|
};
|
|
18175
19040
|
}
|
|
18176
19041
|
|
|
18177
|
-
// src/end-call.ts
|
|
18178
|
-
var defaultClock = {
|
|
18179
|
-
now: () => Date.now(),
|
|
18180
|
-
setTimeout: (fn, ms) => setTimeout(fn, ms),
|
|
18181
|
-
clearTimeout: (h) => clearTimeout(h)
|
|
18182
|
-
};
|
|
18183
|
-
var HOST_HANGUP_HOOK_LIMIT_MS = 2e3;
|
|
18184
|
-
var GUARANTEE_RETRY_MS = 1500;
|
|
18185
|
-
var DEFAULT_MAX_CALL_DURATION_MS = 36e5;
|
|
18186
|
-
function createEndCallController(deps) {
|
|
18187
|
-
const clock = deps.clock ?? defaultClock;
|
|
18188
|
-
const log = deps.log ?? (() => {
|
|
18189
|
-
});
|
|
18190
|
-
let ended = false;
|
|
18191
|
-
let idleTimer = null;
|
|
18192
|
-
let durationTimer = null;
|
|
18193
|
-
const clearIdle = () => {
|
|
18194
|
-
if (idleTimer !== null) {
|
|
18195
|
-
clock.clearTimeout(idleTimer);
|
|
18196
|
-
idleTimer = null;
|
|
18197
|
-
}
|
|
18198
|
-
};
|
|
18199
|
-
const clearDuration = () => {
|
|
18200
|
-
if (durationTimer !== null) {
|
|
18201
|
-
clock.clearTimeout(durationTimer);
|
|
18202
|
-
durationTimer = null;
|
|
18203
|
-
}
|
|
18204
|
-
};
|
|
18205
|
-
const sleep2 = (ms) => new Promise((resolve) => {
|
|
18206
|
-
clock.setTimeout(resolve, ms);
|
|
18207
|
-
});
|
|
18208
|
-
const guarantee = async (teardown = deps.runTeardown) => {
|
|
18209
|
-
if (deps.guaranteeMs === false) return;
|
|
18210
|
-
const deadline = clock.now() + deps.guaranteeMs;
|
|
18211
|
-
while (!deps.isEnded() && clock.now() < deadline) {
|
|
18212
|
-
await sleep2(Math.min(GUARANTEE_RETRY_MS, Math.max(0, deadline - clock.now())));
|
|
18213
|
-
if (deps.isEnded()) return;
|
|
18214
|
-
log("[agent] end-call teardown not confirmed; retrying", { teardown: deps.teardown });
|
|
18215
|
-
try {
|
|
18216
|
-
await teardown();
|
|
18217
|
-
} catch (err) {
|
|
18218
|
-
log("[agent] end-call retry teardown threw", { err: String(err) });
|
|
18219
|
-
}
|
|
18220
|
-
}
|
|
18221
|
-
};
|
|
18222
|
-
let hostHangup = null;
|
|
18223
|
-
const hostHangUp = (reason) => {
|
|
18224
|
-
if (deps.isEnded()) return Promise.resolve();
|
|
18225
|
-
hostHangup ??= (async () => {
|
|
18226
|
-
ended = true;
|
|
18227
|
-
clearIdle();
|
|
18228
|
-
clearDuration();
|
|
18229
|
-
const info = { ...reason !== void 0 ? { reason } : {}, trigger: "host", teardown: "room" };
|
|
18230
|
-
deps.emit?.(info);
|
|
18231
|
-
log("[agent] hang up (host)", { reason });
|
|
18232
|
-
if (deps.onEnd) {
|
|
18233
|
-
let limit;
|
|
18234
|
-
await Promise.race([
|
|
18235
|
-
Promise.resolve().then(() => deps.onEnd(info)).catch((err) => log("[agent] endCall.onEnd threw on a host hangup", { err: String(err) })),
|
|
18236
|
-
new Promise((resolve) => {
|
|
18237
|
-
limit = clock.setTimeout(resolve, HOST_HANGUP_HOOK_LIMIT_MS);
|
|
18238
|
-
})
|
|
18239
|
-
]).finally(() => clock.clearTimeout(limit));
|
|
18240
|
-
}
|
|
18241
|
-
const teardown = deps.runRoomTeardown ?? deps.runTeardown;
|
|
18242
|
-
try {
|
|
18243
|
-
await teardown();
|
|
18244
|
-
} catch (err) {
|
|
18245
|
-
log("[agent] host hangup teardown threw", { err: String(err) });
|
|
18246
|
-
}
|
|
18247
|
-
await guarantee(teardown);
|
|
18248
|
-
if (!deps.isEnded()) hostHangup = null;
|
|
18249
|
-
})();
|
|
18250
|
-
return hostHangup;
|
|
18251
|
-
};
|
|
18252
|
-
const hangUp = async (opts) => {
|
|
18253
|
-
if (opts?.trigger === "host") return hostHangUp(opts.reason);
|
|
18254
|
-
if (ended) return;
|
|
18255
|
-
ended = true;
|
|
18256
|
-
clearIdle();
|
|
18257
|
-
clearDuration();
|
|
18258
|
-
const info = {
|
|
18259
|
-
...opts?.reason !== void 0 ? { reason: opts.reason } : {},
|
|
18260
|
-
trigger: opts?.trigger ?? "user",
|
|
18261
|
-
teardown: deps.teardown
|
|
18262
|
-
};
|
|
18263
|
-
deps.emit?.(info);
|
|
18264
|
-
log("[agent] hang up", { trigger: info.trigger, teardown: info.teardown });
|
|
18265
|
-
let suppress = false;
|
|
18266
|
-
if (deps.onEnd) {
|
|
18267
|
-
try {
|
|
18268
|
-
suppress = await deps.onEnd(info) === false;
|
|
18269
|
-
} catch (err) {
|
|
18270
|
-
log("[agent] endCall.onEnd threw; running default teardown", { err: String(err) });
|
|
18271
|
-
}
|
|
18272
|
-
}
|
|
18273
|
-
if (suppress) {
|
|
18274
|
-
log("[agent] endCall.onEnd suppressed default teardown", { trigger: info.trigger });
|
|
18275
|
-
return;
|
|
18276
|
-
}
|
|
18277
|
-
try {
|
|
18278
|
-
await deps.runTeardown();
|
|
18279
|
-
} catch (err) {
|
|
18280
|
-
log("[agent] end-call teardown threw", { err: String(err) });
|
|
18281
|
-
}
|
|
18282
|
-
await guarantee();
|
|
18283
|
-
};
|
|
18284
|
-
return {
|
|
18285
|
-
hangUp,
|
|
18286
|
-
armIdle() {
|
|
18287
|
-
if (ended || deps.idleHangupMs === false) return;
|
|
18288
|
-
clearIdle();
|
|
18289
|
-
idleTimer = clock.setTimeout(() => {
|
|
18290
|
-
idleTimer = null;
|
|
18291
|
-
void hangUp({ trigger: "idle", reason: "no activity on the line" });
|
|
18292
|
-
}, deps.idleHangupMs);
|
|
18293
|
-
},
|
|
18294
|
-
cancelIdle() {
|
|
18295
|
-
clearIdle();
|
|
18296
|
-
},
|
|
18297
|
-
armDuration() {
|
|
18298
|
-
if (ended || deps.maxCallDurationMs === false) return;
|
|
18299
|
-
clearDuration();
|
|
18300
|
-
const durationMs = deps.maxCallDurationMs ?? DEFAULT_MAX_CALL_DURATION_MS;
|
|
18301
|
-
durationTimer = clock.setTimeout(() => {
|
|
18302
|
-
durationTimer = null;
|
|
18303
|
-
void hangUp({
|
|
18304
|
-
trigger: "max_duration",
|
|
18305
|
-
reason: `call exceeded the ${durationMs}ms duration ceiling`
|
|
18306
|
-
});
|
|
18307
|
-
}, durationMs);
|
|
18308
|
-
},
|
|
18309
|
-
noteEngineClosed() {
|
|
18310
|
-
if (ended) return;
|
|
18311
|
-
log("[agent] realtime engine closed; guaranteeing teardown");
|
|
18312
|
-
void hangUp({ trigger: "engine-closed", reason: "realtime engine closed" });
|
|
18313
|
-
},
|
|
18314
|
-
dispose() {
|
|
18315
|
-
clearIdle();
|
|
18316
|
-
clearDuration();
|
|
18317
|
-
}
|
|
18318
|
-
};
|
|
18319
|
-
}
|
|
18320
|
-
|
|
18321
19042
|
// src/agent.ts
|
|
18322
19043
|
init_helper_models();
|
|
18323
19044
|
var FLOW_HANGUP_GRACE_MS = Number(process.env["VL_FLOW_HANGUP_GRACE_MS"] ?? "1200");
|
|
@@ -18417,6 +19138,8 @@ var Agent = class {
|
|
|
18417
19138
|
const agentId = typeof md["agentId"] === "string" ? md["agentId"] : void 0;
|
|
18418
19139
|
const room = job.room.name ?? void 0;
|
|
18419
19140
|
registerTelemetryFlush(job);
|
|
19141
|
+
const usageSink = createTelemetrySink();
|
|
19142
|
+
const modelMeter = createCallModelMeter(usageSink);
|
|
18420
19143
|
try {
|
|
18421
19144
|
return await withCallContext(
|
|
18422
19145
|
{
|
|
@@ -18427,14 +19150,16 @@ var Agent = class {
|
|
|
18427
19150
|
...agentId ? { agentId } : {},
|
|
18428
19151
|
...room ? { room } : {}
|
|
18429
19152
|
},
|
|
18430
|
-
(
|
|
19153
|
+
// Every model call the SDK makes on its own during the call (judge, extractor, agent loop, embeddings) reports
|
|
19154
|
+
// into the call's usage and latency — the meter follows all the async work the call starts (burn-down G-40).
|
|
19155
|
+
() => runWithModelMeter(modelMeter.listener, () => this.runCallSpan(job, usageSink, modelMeter))
|
|
18431
19156
|
);
|
|
18432
19157
|
} finally {
|
|
18433
19158
|
await flushTelemetry().catch(() => {
|
|
18434
19159
|
});
|
|
18435
19160
|
}
|
|
18436
19161
|
}
|
|
18437
|
-
async runCallSpan(job) {
|
|
19162
|
+
async runCallSpan(job, usageSink, modelMeter) {
|
|
18438
19163
|
const { AutoSubscribe, voice: voice3, VADEventType } = await import('@livekit/agents');
|
|
18439
19164
|
const Events = voice3.AgentSessionEventTypes;
|
|
18440
19165
|
await job.connect(void 0, AutoSubscribe.SUBSCRIBE_ALL);
|
|
@@ -18460,6 +19185,7 @@ var Agent = class {
|
|
|
18460
19185
|
const identity = admission.resolution;
|
|
18461
19186
|
const sdkClient = identity.kind === "ok" ? identity.identity.client : null;
|
|
18462
19187
|
const callCredential = identity.kind === "ok" ? identity.identity.credential : null;
|
|
19188
|
+
configureHelperCallCredential(callCredential?.source ?? null);
|
|
18463
19189
|
const identityRefused = identity.kind === "refused" && (!!this.config.flowRuntime || identity.reason === "no_project_in_dispatch");
|
|
18464
19190
|
if (identity.kind === "refused" && !identityRefused) {
|
|
18465
19191
|
console.warn("[agent] running with no workspace credential (worker-token exchange failed)", { reason: identity.reason });
|
|
@@ -18627,6 +19353,7 @@ var Agent = class {
|
|
|
18627
19353
|
const built = await resolveCallPipeline(effectiveConfig.models ?? {}, callInfo, job.proc.userData.vad);
|
|
18628
19354
|
const pipeline = built.pipeline;
|
|
18629
19355
|
if (built.failure && !flowBootFailure) flowBootFailure = built.failure;
|
|
19356
|
+
const keyOwner = pipelineKeys(pipeline, callCredential?.source !== "worker-token");
|
|
18630
19357
|
const vadTap = tapVad(pipeline.vad, { endOfSpeechType: VADEventType.END_OF_SPEECH });
|
|
18631
19358
|
const usingRealtime = pipeline.realtime != null;
|
|
18632
19359
|
let runtimeLlm = usingRealtime ? pipeline.realtime : pipeline.llm;
|
|
@@ -18639,9 +19366,10 @@ var Agent = class {
|
|
|
18639
19366
|
seedCallPrefill(processRt, dispatchMdEarly);
|
|
18640
19367
|
await seedProjectVariables(processRt, callCredential);
|
|
18641
19368
|
let opEventCallId = null;
|
|
19369
|
+
let trackWrite = (write) => write;
|
|
18642
19370
|
const emitOpEvent = (kind, payload) => {
|
|
18643
19371
|
if (!sdkClient || !opEventCallId) return;
|
|
18644
|
-
void sdkClient.calls.appendEvent(opEventCallId, { kind, payload }).catch(() => {
|
|
19372
|
+
void trackWrite(sdkClient.calls.appendEvent(opEventCallId, { kind, payload })).catch(() => {
|
|
18645
19373
|
});
|
|
18646
19374
|
};
|
|
18647
19375
|
const graphEvents = graphMode ? withOpEventFanout(createGraphEvents(processRt), emitOpEvent) : null;
|
|
@@ -18668,8 +19396,23 @@ var Agent = class {
|
|
|
18668
19396
|
const callSync = await createCallSyncHandle({
|
|
18669
19397
|
client: sdkClient,
|
|
18670
19398
|
roomName: job.room.name ?? `room-${job.job?.id ?? "unknown"}`,
|
|
18671
|
-
call: callInfo
|
|
19399
|
+
call: callInfo,
|
|
19400
|
+
pipelineKeys: keyOwner
|
|
18672
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;
|
|
18673
19416
|
const transcriptLines = [];
|
|
18674
19417
|
const transcript = createTranscriptHandle(() => transcriptLines);
|
|
18675
19418
|
const ctxRef = { current: null };
|
|
@@ -18801,7 +19544,8 @@ ${callIntent}`
|
|
|
18801
19544
|
const endCallTeardown = endCallPolicy.teardown ?? "room";
|
|
18802
19545
|
const endCallGuaranteeMs = endCallPolicy.guaranteeMs === void 0 ? 4e3 : endCallPolicy.guaranteeMs;
|
|
18803
19546
|
const endCallIdleMs = endCallPolicy.idleHangupMs === void 0 ? 2e4 : endCallPolicy.idleHangupMs;
|
|
18804
|
-
const endCallMaxDurationMs = endCallPolicy.maxCallDurationMs
|
|
19547
|
+
const endCallMaxDurationMs = effectiveMaxCallDurationMs(endCallPolicy.maxCallDurationMs, admissionLimits);
|
|
19548
|
+
const lineRef = { say: null };
|
|
18805
19549
|
const endCallRoomName = job.room.name ?? `room-${job.job?.id ?? "unknown"}`;
|
|
18806
19550
|
const localDisconnect = async () => {
|
|
18807
19551
|
try {
|
|
@@ -18814,6 +19558,17 @@ ${callIntent}`
|
|
|
18814
19558
|
guaranteeMs: endCallGuaranteeMs,
|
|
18815
19559
|
idleHangupMs: endCallIdleMs,
|
|
18816
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
|
+
} : {},
|
|
18817
19572
|
// 'room' deletes the whole LK room (drops the SIP/PSTN leg → carrier BYE);
|
|
18818
19573
|
// 'agent' only disconnects the agent participant.
|
|
18819
19574
|
runTeardown: endCallTeardown === "room" ? () => endRoomBestEffort(endCallRoomName, localDisconnect) : localDisconnect,
|
|
@@ -18852,6 +19607,7 @@ ${callIntent}`
|
|
|
18852
19607
|
}
|
|
18853
19608
|
});
|
|
18854
19609
|
const session = adapter.raw.session;
|
|
19610
|
+
lineRef.say = (text) => adapter.say(text, { allowInterruptions: false });
|
|
18855
19611
|
sessionRef.current = session;
|
|
18856
19612
|
const handoffRt = createHandoffRuntime(effectiveConfig.handoff, {
|
|
18857
19613
|
session,
|
|
@@ -19082,12 +19838,13 @@ ${callIntent}`
|
|
|
19082
19838
|
}
|
|
19083
19839
|
}
|
|
19084
19840
|
};
|
|
19085
|
-
const usageSink = createTelemetrySink();
|
|
19086
19841
|
const callEffects = new EffectStack({
|
|
19087
19842
|
onDisposeError: (err) => console.warn("[agent] call-effect dispose failed", {
|
|
19088
19843
|
err: err instanceof Error ? err.message : String(err)
|
|
19089
19844
|
})
|
|
19090
19845
|
});
|
|
19846
|
+
let usageReported = Promise.resolve();
|
|
19847
|
+
let finalized = Promise.resolve();
|
|
19091
19848
|
session.once(Events.Close, () => {
|
|
19092
19849
|
sessionClosed = true;
|
|
19093
19850
|
endCallController.dispose();
|
|
@@ -19099,12 +19856,14 @@ ${callIntent}`
|
|
|
19099
19856
|
void offboardParticipant(remaining.id);
|
|
19100
19857
|
}
|
|
19101
19858
|
const outcome = callEndOutcome(startedAt);
|
|
19102
|
-
if (sdkClient
|
|
19103
|
-
|
|
19104
|
-
|
|
19105
|
-
|
|
19106
|
-
|
|
19107
|
-
|
|
19859
|
+
if (sdkClient) {
|
|
19860
|
+
const calls = sdkClient.calls;
|
|
19861
|
+
usageReported = reportUsageAtClose({
|
|
19862
|
+
meter: modelMeter,
|
|
19863
|
+
usage: usageSink,
|
|
19864
|
+
durationMs: outcome.durationMs,
|
|
19865
|
+
send: (report, signal) => calls.reportUsage(callSync.callId, report, { signal }),
|
|
19866
|
+
warn: (message, meta) => console.warn(message, { callId: callSync.callId, ...meta })
|
|
19108
19867
|
});
|
|
19109
19868
|
}
|
|
19110
19869
|
void audioExperience.onCallEnd(audioCtx, outcome);
|
|
@@ -19243,9 +20002,40 @@ ${callIntent}`
|
|
|
19243
20002
|
callId: callSync.callId,
|
|
19244
20003
|
...callCampaignId ? { campaignId: callCampaignId } : {}
|
|
19245
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
|
+
);
|
|
20010
|
+
const modelClock = graphMode && !usingRealtime ? attachTurnModelClock(
|
|
20011
|
+
session,
|
|
20012
|
+
{
|
|
20013
|
+
userStateChanged: Events.UserStateChanged,
|
|
20014
|
+
userInputTranscribed: Events.UserInputTranscribed,
|
|
20015
|
+
agentStateChanged: Events.AgentStateChanged,
|
|
20016
|
+
speechCreated: Events.SpeechCreated
|
|
20017
|
+
},
|
|
20018
|
+
metricsCtx,
|
|
20019
|
+
{ turns: turnLatency }
|
|
20020
|
+
) : void 0;
|
|
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
|
+
});
|
|
20032
|
+
modelMeter.bind(metricsCtx, modelClock);
|
|
19246
20033
|
attachMetricsBridge(session, Events.MetricsCollected, {
|
|
19247
20034
|
...metricsCtx,
|
|
19248
|
-
usage: usageSink
|
|
20035
|
+
usage: usageSink,
|
|
20036
|
+
keyOwner,
|
|
20037
|
+
turns: turnLatency,
|
|
20038
|
+
...modelClock ? { modelClock } : {}
|
|
19249
20039
|
});
|
|
19250
20040
|
if (graphMode) {
|
|
19251
20041
|
attachEndpointingProbe(
|
|
@@ -19254,7 +20044,7 @@ ${callIntent}`
|
|
|
19254
20044
|
userStateChanged: Events.UserStateChanged,
|
|
19255
20045
|
userInputTranscribed: Events.UserInputTranscribed
|
|
19256
20046
|
},
|
|
19257
|
-
metricsCtx,
|
|
20047
|
+
{ ...metricsCtx, turns: turnLatency },
|
|
19258
20048
|
{ vad: vadTap }
|
|
19259
20049
|
);
|
|
19260
20050
|
}
|
|
@@ -19317,11 +20107,11 @@ ${callIntent}`
|
|
|
19317
20107
|
room: job.room.name ?? null,
|
|
19318
20108
|
participant: participant.identity
|
|
19319
20109
|
});
|
|
19320
|
-
endCallController.armDuration();
|
|
20110
|
+
if (callAdmission.kind !== "refused") endCallController.armDuration();
|
|
19321
20111
|
{
|
|
19322
20112
|
const announceMd = parseJobMetadata(job.job?.metadata);
|
|
19323
20113
|
const announcement = stringValue(announceMd["recordingAnnouncement"]);
|
|
19324
|
-
if (announcement) {
|
|
20114
|
+
if (announcement && callAdmission.kind !== "refused") {
|
|
19325
20115
|
try {
|
|
19326
20116
|
await adapter.say(announcement, { allowInterruptions: false });
|
|
19327
20117
|
} catch (err) {
|
|
@@ -19329,7 +20119,13 @@ ${callIntent}`
|
|
|
19329
20119
|
}
|
|
19330
20120
|
}
|
|
19331
20121
|
}
|
|
19332
|
-
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)) {
|
|
19333
20129
|
const dispatchMd = parseJobMetadata(job.job?.metadata);
|
|
19334
20130
|
const initialDirective = stringValue(dispatchMd["initialDirective"]);
|
|
19335
20131
|
if (initialDirective) {
|
|
@@ -19420,6 +20216,8 @@ ${callIntent}`
|
|
|
19420
20216
|
}
|
|
19421
20217
|
}
|
|
19422
20218
|
await waitForSessionClose;
|
|
20219
|
+
await usageReported;
|
|
20220
|
+
await finalized;
|
|
19423
20221
|
} catch (err) {
|
|
19424
20222
|
console.error("[agent] runJob failed", {
|
|
19425
20223
|
room: job.room.name ?? null,
|
|
@@ -19529,7 +20327,7 @@ function sharedBrainPubSub() {
|
|
|
19529
20327
|
async function applyStoredPipeline(client, agentId, base, codeMode) {
|
|
19530
20328
|
try {
|
|
19531
20329
|
const stored = await client.agents.getConfig(agentId);
|
|
19532
|
-
if (!stored) return { config: base, brain: "none"
|
|
20330
|
+
if (!stored) return { config: base, brain: "none" };
|
|
19533
20331
|
const mode = codeMode ?? stored.mode;
|
|
19534
20332
|
const providers = /* @__PURE__ */ new Set();
|
|
19535
20333
|
if (mode === "realtime" && stored.realtime) {
|
|
@@ -19604,7 +20402,6 @@ async function applyStoredPipeline(client, agentId, base, codeMode) {
|
|
|
19604
20402
|
models: { ...base.models ?? {}, ...mapped, ...brainLlm ? { llm: brainLlm } : {} }
|
|
19605
20403
|
},
|
|
19606
20404
|
brain: brainLlm ? "bound" : brainConnectorId && mode !== "realtime" ? "unreachable" : "none",
|
|
19607
|
-
byokProviders: Object.keys(creds2),
|
|
19608
20405
|
...mode !== "realtime" && stored.model ? { llm: { provider: stored.model.provider, model: stored.model.model } } : {}
|
|
19609
20406
|
};
|
|
19610
20407
|
} catch (err) {
|
|
@@ -19612,7 +20409,7 @@ async function applyStoredPipeline(client, agentId, base, codeMode) {
|
|
|
19612
20409
|
agentId,
|
|
19613
20410
|
err: err instanceof Error ? err.message : String(err)
|
|
19614
20411
|
});
|
|
19615
|
-
return { config: base, brain: "unknown"
|
|
20412
|
+
return { config: base, brain: "unknown" };
|
|
19616
20413
|
}
|
|
19617
20414
|
}
|
|
19618
20415
|
function firstString(...values) {
|