@voicelayer/sdk 0.6.1 → 0.6.3

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.
@@ -81,14 +81,14 @@ declare const ConnectorUpFrame: z.ZodDiscriminatedUnion<"kind", [z.ZodObject<{
81
81
  code: z.ZodEnum<["upstream_timeout", "upstream_error", "bad_response", "normalize_failed", "unreachable"]>;
82
82
  message: z.ZodString;
83
83
  }, "strip", z.ZodTypeAny, {
84
+ kind: "brain.error";
84
85
  code: "upstream_timeout" | "upstream_error" | "bad_response" | "normalize_failed" | "unreachable";
85
86
  message: string;
86
- kind: "brain.error";
87
87
  streamId: string;
88
88
  }, {
89
+ kind: "brain.error";
89
90
  code: "upstream_timeout" | "upstream_error" | "bad_response" | "normalize_failed" | "unreachable";
90
91
  message: string;
91
- kind: "brain.error";
92
92
  streamId: string;
93
93
  }>]>;
94
94
  type ConnectorUpFrame = z.infer<typeof ConnectorUpFrame>;
@@ -883,7 +883,9 @@ var CallEventKind = z.enum([
883
883
  "dtmf.sent",
884
884
  // A host's runtime intervention (burn-down G-6): call_say / call_send_guidance / call_inject_context / call_instruct,
885
885
  // with masked args and the API key — written by the API only, never through the worker's /events endpoint.
886
- "mcp.interaction"
886
+ "mcp.interaction",
887
+ // One per caller turn the agent answered (burn-down G-44): where that turn's wait went — see TurnLatencyPayload.
888
+ "turn.latency"
887
889
  ]);
888
890
  var OP_CALL_EVENT_KIND = /^(tool|handoff|lookup|record|notify|engine)\.[a-z0-9_]{1,48}(\.[a-z0-9_]{1,48})?$/;
889
891
  var OpCallEventKind = z.string().regex(OP_CALL_EVENT_KIND);
@@ -910,6 +912,17 @@ z.object({
910
912
  code: z.number().int().min(0).max(15),
911
913
  participantId: z.string().optional()
912
914
  });
915
+ var LatencyMs = z.number().int().min(0).max(6e5).nullable();
916
+ z.object({
917
+ turn: z.number().int().min(1),
918
+ atMs: z.number().int().min(0),
919
+ endpointingMs: LatencyMs,
920
+ sttMs: LatencyMs,
921
+ llmMs: LatencyMs,
922
+ ttsMs: LatencyMs,
923
+ endToEndMs: LatencyMs,
924
+ realtime: z.boolean().optional()
925
+ });
913
926
  z.object({
914
927
  totalUsd: z.string(),
915
928
  breakdown: z.object({
@@ -1019,6 +1032,58 @@ z.union([
1019
1032
  error: z.object({ code: TextTurnErrorCode, detail: z.string().max(500).optional() })
1020
1033
  })
1021
1034
  ]);
1035
+ var PLATFORM_MAX_CALL_MINUTES = 240;
1036
+ var Message = z.string().trim().min(1).max(300);
1037
+ var CallerRateLimit = z.object({
1038
+ enabled: z.boolean().optional(),
1039
+ perTenMinutes: z.number().int().min(1).max(100).optional(),
1040
+ perDay: z.number().int().min(1).max(1e3).optional(),
1041
+ withheldPerTenMinutes: z.number().int().min(1).max(100).optional(),
1042
+ withheldPerDay: z.number().int().min(1).max(1e3).optional()
1043
+ });
1044
+ var CallLimits = z.object({
1045
+ /** The agent's own ceiling in minutes; clamped to the workspace's plan ceiling. Absent ⇒ DEFAULT_MAX_CALL_MINUTES. */
1046
+ maxDurationMinutes: z.number().int().min(1).max(PLATFORM_MAX_CALL_MINUTES).optional(),
1047
+ /** Seconds before the limit the wrap-up line is spoken; 0 ⇒ no warning. */
1048
+ wrapUpWarningSeconds: z.number().int().min(0).max(300).optional(),
1049
+ wrapUpMessage: Message.optional(),
1050
+ goodbyeMessage: Message.optional(),
1051
+ callerRateLimit: CallerRateLimit.optional(),
1052
+ rateLimitedMessage: Message.optional()
1053
+ });
1054
+ var CallerChannel = z.enum(["phone", "web"]);
1055
+ z.object({
1056
+ roomName: z.string().min(1).max(256),
1057
+ /** From the dispatch metadata: lets the API admit (gate) a room whose call row doesn't exist yet. */
1058
+ agentId: z.string().max(128).optional(),
1059
+ phoneNumberId: z.string().max(128).optional(),
1060
+ caller: z.object({
1061
+ channel: CallerChannel,
1062
+ /** The caller's number as the carrier presented it; null / absent / not E.164 ⇒ the withheld bucket. */
1063
+ number: z.string().max(64).nullable().optional()
1064
+ })
1065
+ });
1066
+ var CallDurationLimits = z.object({
1067
+ /** The agent's own ceiling (already clamped to the plan), or null when it set none. */
1068
+ agentMaxSeconds: z.number().int().min(1).nullable(),
1069
+ /** The plan ceiling — the platform bound no call outlives. */
1070
+ ceilingSeconds: z.number().int().min(1),
1071
+ wrapUpWarningSeconds: z.number().int().min(0),
1072
+ wrapUpMessage: z.string(),
1073
+ goodbyeMessage: z.string()
1074
+ });
1075
+ z.discriminatedUnion("admitted", [
1076
+ z.object({ admitted: z.literal(true), limits: CallDurationLimits }),
1077
+ z.object({
1078
+ admitted: z.literal(false),
1079
+ /** 'rate_limited:caller' | 'inbound_refused:<code>' — already recorded as the call's end reason. */
1080
+ endReason: z.string(),
1081
+ /** The fixed line to speak before hanging up. */
1082
+ message: z.string()
1083
+ })
1084
+ ]);
1085
+
1086
+ // ../contracts/src/agent-config.ts
1022
1087
  var VoiceConfig = z.object({
1023
1088
  // Finite, vetted list — platform-level enum.
1024
1089
  provider: z.enum(["deepgram", "elevenlabs", "openai", "cartesia"]),
@@ -1181,6 +1246,9 @@ var AgentConfig = z.object({
1181
1246
  // LLM slot through the connector instead of a first-party model. Null/absent →
1182
1247
  // first-party pipeline (unchanged default). See docs/specs/voice-brain-connector.
1183
1248
  brainConnectorId: z.string().uuid().nullable().optional(),
1249
+ // Per-agent call bounds: caller rate limit, max duration and their spoken lines (burn-down G-41, G-43). Absent ⇒
1250
+ // every default (call-limits.ts) — the rate limit is ON by default.
1251
+ callLimits: CallLimits.optional(),
1184
1252
  // Provenance: 'sdk' (hand-coded defineAgent worker), 'flow' (canvas deploy),
1185
1253
  // 'playbook' (agent-mode deploy), or 'connector' (brain-connector agent).
1186
1254
  // DERIVED from the linked agents row's deploy tags (flowId / playbookId /
@@ -1474,6 +1542,8 @@ z.object({
1474
1542
  consultation: ConsultationPolicy,
1475
1543
  tags: z.array(z.string().max(64)).max(32).nullable(),
1476
1544
  brainConnectorId: z.string().uuid().nullable(),
1545
+ // Versions published before call limits existed carry none: read as null (every default).
1546
+ callLimits: CallLimits.nullable().default(null),
1477
1547
  flowVersion: z.number().int().nullable(),
1478
1548
  processSchema: ProcessSchemaDTO.nullable()
1479
1549
  });
@@ -1509,6 +1579,7 @@ var AgentConfigDiffField = z.enum([
1509
1579
  "language",
1510
1580
  "tools",
1511
1581
  "brainConnectorId",
1582
+ "callLimits",
1512
1583
  "consultation",
1513
1584
  "tags",
1514
1585
  "flowVersion",
@@ -1634,6 +1705,9 @@ var CostEstimatePerMinute = z.object({
1634
1705
  var CostEstimateBilledPerMinute = z.object({
1635
1706
  platformFee: z.number().nonnegative(),
1636
1707
  providerPassthrough: z.number().nonnegative(),
1708
+ // The flat own-key fee per minute (burn-down B-26); 0 when every provider runs on the platform's keys. Optional
1709
+ // only for responses from an API older than the fee.
1710
+ ownKeyFee: z.number().nonnegative().optional(),
1637
1711
  telephony: z.number().nonnegative(),
1638
1712
  allIn: z.number().nonnegative()
1639
1713
  });
@@ -1834,6 +1908,38 @@ z.object({
1834
1908
  note: z.string().max(2e3).nullable().optional(),
1835
1909
  reviewed: z.boolean().optional()
1836
1910
  });
1911
+ var CAPTURED_FIELDS_MAX = 200;
1912
+ var CAPTURED_STRING_MAX_CHARS = 8e3;
1913
+ var CAPTURED_ARRAY_MAX_ITEMS = 100;
1914
+ var CapturedFieldValue = z.union([
1915
+ z.string().max(CAPTURED_STRING_MAX_CHARS),
1916
+ z.number().finite(),
1917
+ z.boolean(),
1918
+ z.array(z.string().max(CAPTURED_STRING_MAX_CHARS)).max(CAPTURED_ARRAY_MAX_ITEMS),
1919
+ z.null()
1920
+ ]);
1921
+ var CapturedField = z.object({
1922
+ name: z.string().min(1).max(64),
1923
+ type: ProcessFieldType,
1924
+ required: z.boolean(),
1925
+ // false ⇒ the call never captured it; `value` is then null
1926
+ filled: z.boolean(),
1927
+ value: CapturedFieldValue
1928
+ });
1929
+ var StructuredOutputs = z.record(CapturedFieldValue);
1930
+ z.object({
1931
+ // Every declared field, in declaration order; empty when the agent declares none.
1932
+ fields: z.array(CapturedField).max(CAPTURED_FIELDS_MAX)
1933
+ }).superRefine((body, ctx) => {
1934
+ const seen = /* @__PURE__ */ new Set();
1935
+ for (const f of body.fields) {
1936
+ if (seen.has(f.name)) ctx.addIssue({ code: z.ZodIssueCode.custom, message: `duplicate field ${f.name}` });
1937
+ seen.add(f.name);
1938
+ if (!f.filled && f.value !== null) ctx.addIssue({ code: z.ZodIssueCode.custom, message: `${f.name}: an unfilled field has no value` });
1939
+ }
1940
+ });
1941
+
1942
+ // ../contracts/src/call-result.ts
1837
1943
  var TranscriptTurn = z.object({
1838
1944
  speaker: z.enum(["caller", "agent"]),
1839
1945
  text: z.string(),
@@ -1882,9 +1988,13 @@ z.object({
1882
1988
  startedAt: z.string().datetime(),
1883
1989
  endedAt: z.string().datetime(),
1884
1990
  durationMs: z.number().int().min(0),
1991
+ // Final segments only, in call order.
1885
1992
  transcript: z.array(TranscriptTurn),
1886
- // Whatever the agent's process-schema captured. No prescribed shape.
1887
- structuredOutputs: z.record(z.unknown()).optional(),
1993
+ // The filled fields of the agent's process / flow, keyed by name (burn-down G-20). Absent when the agent declares no
1994
+ // fields, or its worker never reported them.
1995
+ structuredOutputs: StructuredOutputs.optional(),
1996
+ // Every declared field with what the call captured and whether it was filled — the same list `call.ended` carries.
1997
+ fields: z.array(CapturedField).optional(),
1888
1998
  recordingUrl: z.string().url().optional(),
1889
1999
  cost: z.object({
1890
2000
  totalUsd: z.string(),
@@ -1911,7 +2021,18 @@ z.object({
1911
2021
  // set, it is the same value `call.ended` carries; `call.ended` always has one.
1912
2022
  direction: CallWebhookDirection.nullable()
1913
2023
  });
2024
+ var CALL_ENDED_PAYLOAD_VERSION = 2;
2025
+ var CallEndedTranscript = z.object({
2026
+ // Final segments only (what was said, never an interim STT guess), in call order. Empty when `truncated`.
2027
+ turns: z.array(TranscriptTurn),
2028
+ // How many final turns the call has — the length of `turns` unless truncated.
2029
+ turnCount: z.number().int().min(0),
2030
+ truncated: z.boolean(),
2031
+ // Where the whole transcript is: GET /v1/calls/:callId/result (its `transcript`). Always set.
2032
+ fetchPath: z.string().regex(/^\/v1\/calls\/[0-9a-f-]{36}\/result$/)
2033
+ });
1914
2034
  z.object({
2035
+ version: z.literal(CALL_ENDED_PAYLOAD_VERSION),
1915
2036
  callId: z.string().uuid(),
1916
2037
  durationMs: z.number().int().min(0),
1917
2038
  endedAt: z.string().datetime(),
@@ -1922,7 +2043,20 @@ z.object({
1922
2043
  toE164: Party,
1923
2044
  direction: CallWebhookDirection,
1924
2045
  outcome: CallWebhookOutcome,
1925
- attributes: CallWebhookAttributes
2046
+ attributes: CallWebhookAttributes,
2047
+ // Every field the agent declared (its process, or a flow's slots), in declaration order, with what the call captured
2048
+ // and whether it was filled. Empty when the agent declares none.
2049
+ fields: z.array(CapturedField),
2050
+ // The filled fields as one object keyed by name (as GET /v1/calls/:id/result returns them). Absent when the agent
2051
+ // declares no fields.
2052
+ structuredOutputs: StructuredOutputs.optional(),
2053
+ transcript: CallEndedTranscript,
2054
+ // true: the agent's worker confirmed its last transcript segment and captured field had landed before this payload
2055
+ // was built. false: built without that word (a worker that crashed or runs an SDK before 0.6.2, or one that didn't
2056
+ // answer within the hold) — the transcript and fields are what had arrived; GET /v1/calls/:id/result has any later.
2057
+ finalized: z.boolean(),
2058
+ // true: the payload would have passed CALL_ENDED_PAYLOAD_MAX_BYTES, so values were left out (see the cap) — fetch them.
2059
+ valuesOmitted: z.boolean()
1926
2060
  });
1927
2061
  z.object({
1928
2062
  callId: z.string().uuid(),
@@ -2267,161 +2401,6 @@ z.object({
2267
2401
  settings: TelephonySettings,
2268
2402
  profile: EffectiveCallProfile
2269
2403
  });
2270
- var SHORTENER_HOSTS = [
2271
- "bit.ly",
2272
- "tinyurl.com",
2273
- "t.co",
2274
- "goo.gl",
2275
- "ow.ly",
2276
- "is.gd",
2277
- "buff.ly",
2278
- "rebrand.ly",
2279
- "cutt.ly",
2280
- "shorturl.at",
2281
- "tiny.cc"
2282
- ];
2283
- var PLACEHOLDER_HOSTS = ["acme.com", "example.com", "example.org", "test.com", "localhost"];
2284
- var PublicUrl = z.string().url().refine((v) => v.startsWith("https://"), "must be https").refine((v) => {
2285
- try {
2286
- const host = new URL(v).hostname.toLowerCase().replace(/^www\./, "");
2287
- return !SHORTENER_HOSTS.includes(host);
2288
- } catch {
2289
- return false;
2290
- }
2291
- }, "public URL shorteners are rejected by carriers \u2014 use your own domain").refine((v) => {
2292
- try {
2293
- const host = new URL(v).hostname.toLowerCase().replace(/^www\./, "");
2294
- return !PLACEHOLDER_HOSTS.some((p) => host === p || host.endsWith(`.${p}`));
2295
- } catch {
2296
- return false;
2297
- }
2298
- }, "placeholder domain \u2014 reviewers will follow this link and reject the campaign");
2299
- var A2pBusinessType = z.enum([
2300
- "Sole Proprietorship",
2301
- "Partnership",
2302
- "Corporation",
2303
- "Co-operative",
2304
- "Limited Liability Corporation",
2305
- "Non-profit Corporation"
2306
- ]);
2307
- var A2pBusinessInfo = z.object({
2308
- legalName: z.string().trim().min(2).max(200),
2309
- /** EIN (US) or equivalent registration number. */
2310
- registrationNumber: z.string().trim().min(4).max(50),
2311
- businessType: A2pBusinessType,
2312
- /** Publicly reachable production site — not staging, not a 404. */
2313
- website: PublicUrl,
2314
- industry: z.string().trim().min(2).max(60),
2315
- address: z.object({
2316
- street: z.string().trim().min(2).max(200),
2317
- city: z.string().trim().min(1).max(100),
2318
- region: z.string().trim().min(1).max(100),
2319
- postalCode: z.string().trim().min(2).max(20),
2320
- isoCountry: z.string().trim().length(2)
2321
- }),
2322
- authorizedRep: z.object({
2323
- firstName: z.string().trim().min(1).max(100),
2324
- lastName: z.string().trim().min(1).max(100),
2325
- email: z.string().trim().email(),
2326
- phone: z.string().trim().regex(/^\+[1-9]\d{6,14}$/, "must be E.164"),
2327
- jobTitle: z.string().trim().min(2).max(100)
2328
- })
2329
- });
2330
- var A2pOptInType = z.enum(["WEB_FORM", "PAPER_FORM", "VERBAL", "VIA_TEXT", "MOBILE_QR_CODE"]);
2331
- var A2pUseCase = z.enum([
2332
- "MIXED",
2333
- "CUSTOMER_CARE",
2334
- "MARKETING",
2335
- "ACCOUNT_NOTIFICATION",
2336
- "2FA",
2337
- "DELIVERY_NOTIFICATION",
2338
- "HIGHER_EDUCATION",
2339
- "POLLING_VOTING",
2340
- "PUBLIC_SERVICE_ANNOUNCEMENT",
2341
- "LOW_VOLUME"
2342
- ]);
2343
- var A2pMessagingProfile = z.object({
2344
- useCase: A2pUseCase,
2345
- /** Specific, not generic. "We send texts" gets rejected; describe the actual
2346
- * messages and when they are sent. */
2347
- description: z.string().trim().min(40).max(4096),
2348
- /**
2349
- * The single most-rejected field. Must describe HOW people opt in, state the
2350
- * message frequency, include the "message and data rates may apply"
2351
- * disclosure, and link to publicly reachable evidence. Twilio's API bounds it
2352
- * to 40–2049 characters.
2353
- */
2354
- messageFlow: z.string().trim().min(40).max(2049),
2355
- optInType: A2pOptInType,
2356
- /** Publicly accessible screenshots/pages showing the opt-in. Reviewers open
2357
- * these; anything behind a login fails. */
2358
- optInEvidenceUrls: z.array(PublicUrl).min(1).max(5),
2359
- /** Real messages the customer will send. Must reflect the declared use case
2360
- * and carry opt-out language. */
2361
- messageSamples: z.array(z.string().trim().min(10).max(1024)).min(2).max(5),
2362
- privacyPolicyUrl: PublicUrl,
2363
- termsAndConditionsUrl: PublicUrl,
2364
- hasEmbeddedLinks: z.boolean().default(false),
2365
- hasEmbeddedPhone: z.boolean().default(false)
2366
- });
2367
- z.object({
2368
- business: A2pBusinessInfo,
2369
- messaging: A2pMessagingProfile,
2370
- /** Register against Twilio's mock endpoints — exercises the full pipeline
2371
- * with no fees and no real carrier submission. Used in CI and staging. */
2372
- mock: z.boolean().default(false)
2373
- }).superRefine((v, ctx) => {
2374
- const flow = v.messaging.messageFlow.toLowerCase();
2375
- if (!/(msg|message)\s*(&|and)\s*data rates/.test(flow)) {
2376
- ctx.addIssue({
2377
- code: z.ZodIssueCode.custom,
2378
- path: ["messaging", "messageFlow"],
2379
- message: 'must include a "Message and data rates may apply" disclosure \u2014 carriers reject without it'
2380
- });
2381
- }
2382
- if (!/\d/.test(flow) || !/(msg|message|text)/.test(flow)) {
2383
- ctx.addIssue({
2384
- code: z.ZodIssueCode.custom,
2385
- path: ["messaging", "messageFlow"],
2386
- message: 'must state message frequency, e.g. "Up to 4 msgs/month"'
2387
- });
2388
- }
2389
- const hasOptOut = v.messaging.messageSamples.some((s) => /stop/i.test(s));
2390
- if (!hasOptOut) {
2391
- ctx.addIssue({
2392
- code: z.ZodIssueCode.custom,
2393
- path: ["messaging", "messageSamples"],
2394
- message: 'at least one sample must include opt-out language (e.g. "Reply STOP to opt out")'
2395
- });
2396
- }
2397
- });
2398
- var A2pState = z.enum([
2399
- "none",
2400
- "profile_pending",
2401
- "profile_approved",
2402
- "profile_failed",
2403
- "brand_pending",
2404
- "brand_approved",
2405
- "brand_failed",
2406
- "campaign_pending",
2407
- "messaging_ready",
2408
- "campaign_failed"
2409
- ]);
2410
- z.object({
2411
- state: A2pState,
2412
- customerProfileSid: z.string().nullable(),
2413
- trustProductSid: z.string().nullable(),
2414
- brandSid: z.string().nullable(),
2415
- messagingServiceSid: z.string().nullable(),
2416
- campaignSid: z.string().nullable(),
2417
- mock: z.boolean(),
2418
- /** Carrier/Twilio rejection details, surfaced verbatim so the customer can
2419
- * fix the specific field rather than guess. */
2420
- failures: z.array(z.object({ code: z.number().nullable(), field: z.string().nullable(), message: z.string() })).default([]),
2421
- /** Plain-language next step for the dashboard. */
2422
- nextAction: z.string().nullable(),
2423
- updatedAt: z.string().nullable()
2424
- });
2425
2404
  var ConnectorMode = z.enum(["tunnel", "direct"]);
2426
2405
  var ConnectorStatus = z.enum(["online", "degraded", "offline"]);
2427
2406
  var ConnectorNormalize = z.enum(["auto", "on", "off"]);
@@ -2536,9 +2515,11 @@ var FLOW_BOOT_FAILURES = [
2536
2515
  "worker_identity_refused",
2537
2516
  "provider_unavailable"
2538
2517
  ];
2539
- z.enum(
2540
- FLOW_BOOT_FAILURES.map((cause) => `flow_boot:${cause}`)
2541
- );
2518
+ var AGENT_END_REASONS = [
2519
+ "max_duration",
2520
+ ...FLOW_BOOT_FAILURES.map((cause) => `flow_boot:${cause}`)
2521
+ ];
2522
+ z.enum(AGENT_END_REASONS);
2542
2523
  var EnvironmentSpec = z.enum(["draft", "live"]);
2543
2524
  z.object({
2544
2525
  agentName: z.string().min(1),
@@ -3603,6 +3584,18 @@ z.object({
3603
3584
  thresholdUsd: z.number().min(1).max(TOPUP_MAX_USD),
3604
3585
  amountUsd: z.number().int().min(TOPUP_MIN_USD).max(TOPUP_MAX_USD)
3605
3586
  });
3587
+ var PipelineSlotKey = z.object({
3588
+ /** The provider as the slot reports it (LiveKit's label: `openai`, `api.openai.com`, `Deepgram`, …). */
3589
+ provider: z.string().min(1).max(200),
3590
+ /** true = the workspace's own key built it; false = the platform's. */
3591
+ byok: z.boolean()
3592
+ });
3593
+ z.object({
3594
+ llm: PipelineSlotKey.optional(),
3595
+ stt: PipelineSlotKey.optional(),
3596
+ tts: PipelineSlotKey.optional(),
3597
+ realtime: PipelineSlotKey.optional()
3598
+ });
3606
3599
 
3607
3600
  // src/brain/transport-tunnel.ts
3608
3601
  var AsyncQueue = class {