supafone-labs 0.4.12 → 0.4.13

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/src/index.ts CHANGED
@@ -93,6 +93,64 @@ export interface WhisperOptions {
93
93
  temperature?: number;
94
94
  }
95
95
 
96
+ export type SecondMindDirectiveKind = "empathy" | "tactical" | "guardrail" | "mixed";
97
+
98
+ export interface DirectiveTextControl {
99
+ enabled?: boolean;
100
+ instructions?: string;
101
+ maxChars?: number;
102
+ max_chars?: number;
103
+ }
104
+
105
+ export interface DirectiveListControl {
106
+ enabled?: boolean;
107
+ instructions?: string;
108
+ maxItems?: number;
109
+ max_items?: number;
110
+ itemMaxChars?: number;
111
+ item_max_chars?: number;
112
+ }
113
+
114
+ /** Serializable controls for each field generated by SecondMind. */
115
+ export interface DirectiveContract {
116
+ empathyDirective?: DirectiveTextControl;
117
+ empathy_directive?: DirectiveTextControl;
118
+ tacticalDirective?: DirectiveTextControl;
119
+ tactical_directive?: DirectiveTextControl;
120
+ surfaceFacts?: DirectiveListControl;
121
+ surface_facts?: DirectiveListControl;
122
+ guardrails?: DirectiveListControl;
123
+ languageMode?: "caller" | "model" | "fixed";
124
+ language_mode?: "caller" | "model" | "fixed";
125
+ fixedLanguage?: string;
126
+ fixed_language?: string;
127
+ allowedKinds?: SecondMindDirectiveKind[];
128
+ allowed_kinds?: SecondMindDirectiveKind[];
129
+ confidenceThreshold?: number;
130
+ confidence_threshold?: number;
131
+ operatorGuardrails?: string[];
132
+ operator_guardrails?: string[];
133
+ }
134
+
135
+ export interface SecondMindDirective {
136
+ empathy_directive: string;
137
+ tactical_directive: string;
138
+ surface_facts: string[];
139
+ guardrails: string[];
140
+ language: string;
141
+ confidence: number;
142
+ kind: SecondMindDirectiveKind;
143
+ }
144
+
145
+ export interface StructuredWhisperOptions extends WhisperOptions {
146
+ directiveContract?: DirectiveContract;
147
+ directive_contract?: DirectiveContract;
148
+ /** Local final say: revise or suppress the generated directive before use. */
149
+ transform?: (
150
+ directive: SecondMindDirective,
151
+ ) => SecondMindDirective | null | Promise<SecondMindDirective | null>;
152
+ }
153
+
96
154
  export interface Balance {
97
155
  plan: string;
98
156
  seconds_remaining: number;
@@ -177,6 +235,115 @@ export interface LabsVoiceSelection {
177
235
  model?: string;
178
236
  }
179
237
 
238
+ export type LabsVoiceGender = "female" | "male" | "neutral";
239
+
240
+ export interface LabsVoiceLanguage {
241
+ code: string;
242
+ locale: string;
243
+ name: string;
244
+ native_name: string;
245
+ aliases?: string[];
246
+ routing_supported?: boolean;
247
+ }
248
+
249
+ export interface LabsVoiceLanguageSupport {
250
+ provider: string;
251
+ model?: string | null;
252
+ known: boolean;
253
+ language_codes: string[];
254
+ /** Number of language codes Supafone can enumerate from provider docs. */
255
+ enumerated_language_count: number;
256
+ generally_available_language_codes: string[];
257
+ experimental_language_codes: string[];
258
+ documented_language_count: number;
259
+ documented_language_count_is_minimum: boolean;
260
+ support_tier: string;
261
+ cross_lingual: boolean | null;
262
+ documentation_url?: string | null;
263
+ verified_at: string;
264
+ native_language_codes: string[];
265
+ ultravox_routing_language_codes: string[];
266
+ primary_language_support_tier: "generally_available" | "experimental" | "unknown";
267
+ }
268
+
269
+ export interface LabsVoiceProviderTraits {
270
+ countries: string[];
271
+ accents: string[];
272
+ age_groups: string[];
273
+ categories: string[];
274
+ use_cases: string[];
275
+ descriptors: string[];
276
+ native_locales: string[];
277
+ created_at?: string | number | null;
278
+ is_public?: boolean | null;
279
+ is_owner?: boolean | null;
280
+ }
281
+
282
+ export interface LabsVoiceCatalogItem {
283
+ /** Stable runtime reference, including provider prefix when required. */
284
+ id: string;
285
+ voice_id: string;
286
+ /** Human-readable provider display name. */
287
+ provider: string;
288
+ /** Canonical SDK/runtime provider key. */
289
+ provider_key: string;
290
+ /** Runtime adapter used to place the voice on an Ultravox call. */
291
+ runtime_provider_key: string;
292
+ /** Actual TTS engine behind the voice (can differ for Ultravox built-ins). */
293
+ synthesis_provider_key: string;
294
+ /** Raw identifier used by the provider API. */
295
+ provider_voice_id: string;
296
+ name: string;
297
+ label: string;
298
+ description: string;
299
+ style: string;
300
+ model?: string | null;
301
+ language: string;
302
+ language_code: string;
303
+ language_locale: string;
304
+ language_name: string;
305
+ native_language_name: string;
306
+ languages: LabsVoiceLanguage[];
307
+ native_language_codes: string[];
308
+ model_language_codes: string[];
309
+ runtime_supported_language_codes: string[];
310
+ routing_supported: boolean;
311
+ language_support: LabsVoiceLanguageSupport;
312
+ gender: LabsVoiceGender;
313
+ accent: string;
314
+ age: string;
315
+ provider_traits: LabsVoiceProviderTraits;
316
+ voice_types: string[];
317
+ primary_voice_type: string;
318
+ tags: string[];
319
+ /** Sanitized provider-native fields retained for forward-compatible filtering. */
320
+ provider_metadata: Record<string, unknown>;
321
+ provider_metadata_fields: string[];
322
+ preview_url?: string | null;
323
+ preview_available: boolean;
324
+ source: string;
325
+ ownership?: string | null;
326
+ configured: boolean;
327
+ recommended: boolean;
328
+ premium?: boolean;
329
+ is_custom: boolean;
330
+ runtime: { provider: string; voice_id: string; model?: string };
331
+ [extra: string]: unknown;
332
+ }
333
+
334
+ export interface LabsVoicePreference {
335
+ description: string;
336
+ language?: string;
337
+ provider?: string;
338
+ gender?: LabsVoiceGender;
339
+ voiceType?: string;
340
+ voice_type?: string;
341
+ model?: string;
342
+ configuredOnly?: boolean;
343
+ configured_only?: boolean;
344
+ premium?: boolean;
345
+ }
346
+
180
347
  export interface LabsProviderKeys {
181
348
  /** Agent runtime/platform providers. */
182
349
  ultravox?: string;
@@ -536,8 +703,14 @@ export interface CreateLabsAgentRequest {
536
703
  greeting?: string;
537
704
  systemPrompt?: string;
538
705
  system_prompt?: string;
706
+ /** Fixed language for the full call. Does not enable mid-call switching. */
539
707
  language?: string;
708
+ preferredLanguage?: string;
709
+ preferred_language?: string;
540
710
  voice?: LabsVoiceSelection;
711
+ /** Resolve a real current catalog voice from this plain-language preference. */
712
+ voicePreference?: LabsVoicePreference;
713
+ voice_preference?: LabsVoicePreference;
541
714
  providerKeys?: LabsProviderKeys;
542
715
  provider_keys?: LabsProviderKeys;
543
716
  byok?: LabsProviderKeys | LabsByokConfig;
@@ -605,17 +778,106 @@ export interface LabsToolListResponse {
605
778
  export interface LabsVoiceListOptions {
606
779
  provider?: string;
607
780
  search?: string;
781
+ /** Native/accent language advertised for the individual voice. */
608
782
  language?: string;
783
+ /** Language that the provider model and Ultravox can both run live. */
784
+ compatibleLanguage?: string;
785
+ compatible_language?: string;
786
+ gender?: LabsVoiceGender;
787
+ voiceType?: string;
788
+ voice_type?: string;
789
+ model?: string;
790
+ runtimeProvider?: string;
791
+ runtime_provider?: string;
792
+ configuredOnly?: boolean;
793
+ configured_only?: boolean;
609
794
  cursor?: number;
610
795
  limit?: number;
796
+ agencyId?: string;
797
+ agency_id?: string;
611
798
  }
612
799
 
613
800
  export interface LabsVoiceListResponse {
614
- voices: Array<Record<string, unknown>>;
801
+ account_id?: string;
802
+ voices: LabsVoiceCatalogItem[];
615
803
  total: number;
616
- providers: Array<Record<string, unknown>>;
804
+ cursor?: number;
805
+ next_cursor?: number | null;
806
+ providers: Array<{
807
+ id: string;
808
+ label: string;
809
+ configured: boolean;
810
+ voice_count: number;
811
+ error?: string | null;
812
+ }>;
617
813
  provider_accounts?: Record<string, unknown>;
618
814
  errors?: Record<string, string>;
815
+ catalog?: {
816
+ dynamic: boolean;
817
+ source: string;
818
+ normalization_schema_version: number;
819
+ cache_ttl_seconds: number;
820
+ capabilities_url?: string;
821
+ };
822
+ }
823
+
824
+ export interface LabsVoiceModelCapability {
825
+ provider: string;
826
+ model?: string | null;
827
+ known: boolean;
828
+ language_codes: string[];
829
+ enumerated_language_count: number;
830
+ generally_available_language_codes: string[];
831
+ experimental_language_codes: string[];
832
+ documented_language_count: number;
833
+ documented_language_count_is_minimum: boolean;
834
+ support_tier: string;
835
+ cross_lingual: boolean | null;
836
+ documentation_url?: string | null;
837
+ verified_at: string;
838
+ ultravox_routing_language_codes: string[];
839
+ ultravox_routing_language_count: number;
840
+ }
841
+
842
+ export interface LabsVoiceProviderCapability {
843
+ provider: string;
844
+ label: string;
845
+ ultravox_integration: "native" | "named_external" | string;
846
+ default_model?: string | null;
847
+ models: LabsVoiceModelCapability[];
848
+ }
849
+
850
+ export interface LabsVoiceCapabilitiesResponse {
851
+ runtime: "ultravox" | string;
852
+ runtime_spoken_languages: LabsVoiceLanguage[];
853
+ runtime_spoken_language_count: number;
854
+ providers: LabsVoiceProviderCapability[];
855
+ selection_rule: Record<string, string>;
856
+ normalization_schema_version: number;
857
+ }
858
+
859
+ export interface LabsVoiceRecommendOptions extends LabsVoicePreference {
860
+ limit?: number;
861
+ agencyId?: string;
862
+ agency_id?: string;
863
+ }
864
+
865
+ export interface LabsVoiceRecommendation {
866
+ voice: LabsVoiceCatalogItem;
867
+ score: number;
868
+ reasons: string[];
869
+ }
870
+
871
+ export interface LabsVoiceRecommendResponse {
872
+ account_id?: string;
873
+ description: string;
874
+ matches: LabsVoiceRecommendation[];
875
+ total_considered: number;
876
+ }
877
+
878
+ export interface LabsVoicePreview {
879
+ content: ArrayBuffer;
880
+ mediaType: string;
619
881
  }
620
882
 
621
883
  export interface LabsRuntimeResponse {
@@ -629,9 +891,11 @@ export interface LabsRuntimeResponse {
629
891
 
630
892
  export interface LabsCallListOptions {
631
893
  agencyId?: string;
894
+ agency_id?: string;
632
895
  agentKey?: string;
633
896
  agent_key?: string;
634
897
  limit?: number;
898
+ offset?: number;
635
899
  }
636
900
 
637
901
  export interface LabsCallArtifact {
@@ -642,7 +906,11 @@ export interface LabsCallArtifact {
642
906
  started_at?: string;
643
907
  duration_seconds?: number;
644
908
  recording_url?: string;
909
+ recording_download_url?: string;
910
+ recording_archived?: boolean;
645
911
  transcript_url?: string;
912
+ watcher_events?: LabsDeveloperActivityEvent[];
913
+ watcher_event_count?: number;
646
914
  [extra: string]: unknown;
647
915
  }
648
916
 
@@ -671,6 +939,40 @@ export interface LabsTranscriptListResponse {
671
939
  [extra: string]: unknown;
672
940
  }
673
941
 
942
+ export interface LabsDeveloperActivityEvent {
943
+ id: string;
944
+ account_id: string;
945
+ event_type: string;
946
+ resource_type: string;
947
+ resource_id: string;
948
+ source?: string;
949
+ detail?: Record<string, unknown>;
950
+ created_at: string;
951
+ [extra: string]: unknown;
952
+ }
953
+
954
+ export interface LabsActivityListOptions {
955
+ accountId?: string;
956
+ account_id?: string;
957
+ agencyId?: string;
958
+ agency_id?: string;
959
+ eventType?: string;
960
+ event_type?: string;
961
+ resourceType?: string;
962
+ resource_type?: string;
963
+ resourceId?: string;
964
+ resource_id?: string;
965
+ limit?: number;
966
+ offset?: number;
967
+ }
968
+
969
+ export interface LabsActivityListResponse {
970
+ events: LabsDeveloperActivityEvent[];
971
+ count: number;
972
+ next_offset?: number | null;
973
+ account_id?: string;
974
+ }
975
+
674
976
  export interface LabsPhoneNumberSearchOptions {
675
977
  agencyId?: string;
676
978
  agency_id?: string;
@@ -1136,6 +1438,140 @@ const COACH_SYSTEM =
1136
1438
  "speaks aloud — a correction or nudge, phrased imperatively. If nothing needs " +
1137
1439
  "correcting, return an empty string.";
1138
1440
 
1441
+ const STRUCTURED_COACH_SYSTEM =
1442
+ "You are the coaching core of a second mind for a live voice agent. Read the " +
1443
+ "conversation and return ONLY one JSON object with exactly these keys: " +
1444
+ "empathy_directive (string), tactical_directive (string), surface_facts (string[]), " +
1445
+ "guardrails (string[]), language (short code), confidence (0-1), and kind " +
1446
+ "(empathy, tactical, guardrail, or mixed). Be silent with low confidence when evidence is weak.";
1447
+
1448
+ interface NormalizedDirectiveContract {
1449
+ empathy_directive: Required<Pick<DirectiveTextControl, "enabled" | "instructions">> & { max_chars: number };
1450
+ tactical_directive: Required<Pick<DirectiveTextControl, "enabled" | "instructions">> & { max_chars: number };
1451
+ surface_facts: Required<Pick<DirectiveListControl, "enabled" | "instructions">> & {
1452
+ max_items: number;
1453
+ item_max_chars: number;
1454
+ };
1455
+ guardrails: Required<Pick<DirectiveListControl, "enabled" | "instructions">> & {
1456
+ max_items: number;
1457
+ item_max_chars: number;
1458
+ };
1459
+ language_mode: "caller" | "model" | "fixed";
1460
+ fixed_language: string;
1461
+ allowed_kinds: SecondMindDirectiveKind[];
1462
+ confidence_threshold: number;
1463
+ operator_guardrails: string[];
1464
+ }
1465
+
1466
+ function boundedInteger(value: unknown, fallback: number, minimum: number, maximum: number): number {
1467
+ const parsed = Number(value);
1468
+ return Number.isFinite(parsed)
1469
+ ? Math.max(minimum, Math.min(maximum, Math.trunc(parsed)))
1470
+ : fallback;
1471
+ }
1472
+
1473
+ function textControl(input: DirectiveTextControl | undefined): NormalizedDirectiveContract["empathy_directive"] {
1474
+ return {
1475
+ enabled: input?.enabled ?? true,
1476
+ instructions: String(input?.instructions ?? "").trim(),
1477
+ max_chars: boundedInteger(input?.max_chars ?? input?.maxChars, 240, 1, 2000),
1478
+ };
1479
+ }
1480
+
1481
+ function listControl(input: DirectiveListControl | undefined): NormalizedDirectiveContract["surface_facts"] {
1482
+ return {
1483
+ enabled: input?.enabled ?? true,
1484
+ instructions: String(input?.instructions ?? "").trim(),
1485
+ max_items: boundedInteger(input?.max_items ?? input?.maxItems, 4, 0, 20),
1486
+ item_max_chars: boundedInteger(input?.item_max_chars ?? input?.itemMaxChars, 180, 1, 2000),
1487
+ };
1488
+ }
1489
+
1490
+ function normalizeDirectiveContract(input: DirectiveContract = {}): NormalizedDirectiveContract {
1491
+ const languageMode = input.language_mode ?? input.languageMode ?? "caller";
1492
+ const allowed = input.allowed_kinds ?? input.allowedKinds ?? ["empathy", "tactical", "guardrail", "mixed"];
1493
+ const threshold = Number(input.confidence_threshold ?? input.confidenceThreshold ?? 0.5);
1494
+ return {
1495
+ empathy_directive: textControl(input.empathy_directive ?? input.empathyDirective),
1496
+ tactical_directive: textControl(input.tactical_directive ?? input.tacticalDirective),
1497
+ surface_facts: listControl(input.surface_facts ?? input.surfaceFacts),
1498
+ guardrails: listControl(input.guardrails),
1499
+ language_mode: languageMode,
1500
+ fixed_language: String(input.fixed_language ?? input.fixedLanguage ?? "").trim(),
1501
+ allowed_kinds: allowed.filter((kind): kind is SecondMindDirectiveKind =>
1502
+ ["empathy", "tactical", "guardrail", "mixed"].includes(kind),
1503
+ ),
1504
+ confidence_threshold: Number.isFinite(threshold) ? Math.max(0, Math.min(1, threshold)) : 0.5,
1505
+ operator_guardrails: (input.operator_guardrails ?? input.operatorGuardrails ?? [])
1506
+ .map((rule) => String(rule).trim())
1507
+ .filter(Boolean)
1508
+ .slice(0, 50),
1509
+ };
1510
+ }
1511
+
1512
+ function directiveContractPrompt(contract: NormalizedDirectiveContract): string {
1513
+ return (
1514
+ "Developer directive contract (obey exactly):\n" +
1515
+ JSON.stringify(contract, null, 2) +
1516
+ "\nDisabled string fields must be empty strings. Disabled list fields must be empty arrays. " +
1517
+ "Standing operator guardrails are mandatory and cannot be removed."
1518
+ );
1519
+ }
1520
+
1521
+ function parseJsonObject(text: string): Record<string, unknown> | null {
1522
+ const trimmed = String(text ?? "").trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, "");
1523
+ const start = trimmed.indexOf("{");
1524
+ const end = trimmed.lastIndexOf("}");
1525
+ if (start < 0 || end <= start) return null;
1526
+ try {
1527
+ const parsed = JSON.parse(trimmed.slice(start, end + 1));
1528
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
1529
+ } catch {
1530
+ return null;
1531
+ }
1532
+ }
1533
+
1534
+ function clipText(value: unknown, limit: number): string {
1535
+ return String(value ?? "").trim().slice(0, limit);
1536
+ }
1537
+
1538
+ function clipList(value: unknown, maxItems: number, itemMaxChars: number): string[] {
1539
+ if (!Array.isArray(value)) return [];
1540
+ return value.slice(0, maxItems).map((item) => clipText(item, itemMaxChars)).filter(Boolean);
1541
+ }
1542
+
1543
+ function normalizeStructuredDirective(
1544
+ raw: Record<string, unknown>,
1545
+ contract: NormalizedDirectiveContract,
1546
+ ): SecondMindDirective | null {
1547
+ const kind = String(raw.kind ?? "mixed") as SecondMindDirectiveKind;
1548
+ if (!["empathy", "tactical", "guardrail", "mixed"].includes(kind)) return null;
1549
+ const parsedConfidence = Number(raw.confidence ?? 0);
1550
+ const confidence = Number.isFinite(parsedConfidence) ? Math.max(0, Math.min(1, parsedConfidence)) : 0;
1551
+ const operatorGuardrails = contract.operator_guardrails;
1552
+ const generatedGuardrails = contract.guardrails.enabled
1553
+ ? clipList(raw.guardrails, contract.guardrails.max_items, contract.guardrails.item_max_chars)
1554
+ : [];
1555
+ const language = contract.language_mode === "fixed"
1556
+ ? contract.fixed_language
1557
+ : clipText(raw.language, 32);
1558
+ return {
1559
+ empathy_directive: contract.empathy_directive.enabled
1560
+ ? clipText(raw.empathy_directive, contract.empathy_directive.max_chars)
1561
+ : "",
1562
+ tactical_directive: contract.tactical_directive.enabled
1563
+ ? clipText(raw.tactical_directive, contract.tactical_directive.max_chars)
1564
+ : "",
1565
+ surface_facts: contract.surface_facts.enabled
1566
+ ? clipList(raw.surface_facts, contract.surface_facts.max_items, contract.surface_facts.item_max_chars)
1567
+ : [],
1568
+ guardrails: [...new Set([...generatedGuardrails, ...operatorGuardrails])],
1569
+ language,
1570
+ confidence: contract.allowed_kinds.includes(kind) ? confidence : 0,
1571
+ kind,
1572
+ };
1573
+ }
1574
+
1139
1575
  export class SupafoneLabs {
1140
1576
  readonly baseUrl: string;
1141
1577
  readonly supafoneApiBaseUrl: string;
@@ -1266,6 +1702,31 @@ export class SupafoneLabs {
1266
1702
  }
1267
1703
  }
1268
1704
 
1705
+ /** @internal Authenticated binary request to the Supafone app API. */
1706
+ async requestSupafoneBinary(path: string): Promise<LabsVoicePreview> {
1707
+ const ctrl = new AbortController();
1708
+ const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
1709
+ try {
1710
+ const res = await fetch(this.supafoneApiBaseUrl + path, {
1711
+ method: "GET",
1712
+ signal: ctrl.signal,
1713
+ headers: { Authorization: `Bearer ${this.supafoneApiKey}` },
1714
+ });
1715
+ if (!res.ok) {
1716
+ const text = await res.text();
1717
+ const parsed = text ? safeJson(text) : {};
1718
+ const detail = (parsed as { detail?: string })?.detail ?? text ?? `HTTP ${res.status}`;
1719
+ throw new SupafoneLabsError(`GET ${path}: ${detail}`, res.status, parsed);
1720
+ }
1721
+ return {
1722
+ content: await res.arrayBuffer(),
1723
+ mediaType: res.headers.get("content-type") || "application/octet-stream",
1724
+ };
1725
+ } finally {
1726
+ clearTimeout(timer);
1727
+ }
1728
+ }
1729
+
1269
1730
  /**
1270
1731
  * Exchange the account email/password for a product-API JWT (the same login
1271
1732
  * as app.supafone.ai). Called lazily by campaigns/calls — call directly only
@@ -1477,6 +1938,52 @@ export class SupafoneLabs {
1477
1938
  return out.text.trim();
1478
1939
  }
1479
1940
 
1941
+ /**
1942
+ * Structured SecondMind guidance with developer-controlled field policy.
1943
+ * Returns null when JSON is invalid, evidence misses the confidence gate,
1944
+ * the generated kind is disallowed, or the local transform suppresses it.
1945
+ */
1946
+ async whisperStructured(
1947
+ transcript: string,
1948
+ opts: StructuredWhisperOptions = {},
1949
+ ): Promise<SecondMindDirective | null> {
1950
+ const contract = normalizeDirectiveContract(
1951
+ opts.directive_contract ?? opts.directiveContract ?? {},
1952
+ );
1953
+ const operatorRules = [opts.guardrails, ...contract.operator_guardrails]
1954
+ .map((rule) => String(rule ?? "").trim())
1955
+ .filter(Boolean);
1956
+ const system = [
1957
+ STRUCTURED_COACH_SYSTEM,
1958
+ directiveContractPrompt(contract),
1959
+ operatorRules.length ? `Operator rules:\n${operatorRules.join("\n")}` : "",
1960
+ ].filter(Boolean).join("\n\n");
1961
+ const out = await this.oracle({
1962
+ model: opts.model,
1963
+ maxTokens: opts.maxTokens ?? 320,
1964
+ ...(opts.temperature !== undefined ? { temperature: opts.temperature } : {}),
1965
+ messages: [
1966
+ { role: "system", content: system },
1967
+ { role: "user", content: transcript },
1968
+ ],
1969
+ });
1970
+ const parsed = parseJsonObject(out.text);
1971
+ let directive = parsed ? normalizeStructuredDirective(parsed, contract) : null;
1972
+ if (directive && opts.transform) {
1973
+ directive = await opts.transform(directive);
1974
+ }
1975
+ if (!directive || directive.confidence < contract.confidence_threshold) return null;
1976
+ return directive;
1977
+ }
1978
+
1979
+ /** Alias that reads naturally in applications building their own watcher loop. */
1980
+ directive(
1981
+ transcript: string,
1982
+ opts: StructuredWhisperOptions = {},
1983
+ ): Promise<SecondMindDirective | null> {
1984
+ return this.whisperStructured(transcript, opts);
1985
+ }
1986
+
1480
1987
  /** Hosted TTS — returns raw audio bytes (WAV/PCM per voice). */
1481
1988
  async tts(text: string, voice = "supafone-labs-calm-en"): Promise<Uint8Array> {
1482
1989
  const ctrl = new AbortController();
@@ -2147,6 +2654,8 @@ class LabsNamespace {
2147
2654
  readonly phoneNumbers: LabsPhoneNumbersNamespace;
2148
2655
  readonly telephony: LabsTelephonyNamespace;
2149
2656
  readonly calls: LabsCallsNamespace;
2657
+ readonly activity: LabsActivityNamespace;
2658
+ readonly plans: LabsPlansNamespace;
2150
2659
  readonly recordings: LabsRecordingsNamespace;
2151
2660
  readonly transcripts: LabsTranscriptsNamespace;
2152
2661
 
@@ -2160,6 +2669,8 @@ class LabsNamespace {
2160
2669
  this.phoneNumbers = new LabsPhoneNumbersNamespace(sm);
2161
2670
  this.telephony = new LabsTelephonyNamespace(sm);
2162
2671
  this.calls = new LabsCallsNamespace(sm);
2672
+ this.activity = new LabsActivityNamespace(sm);
2673
+ this.plans = new LabsPlansNamespace(sm);
2163
2674
  this.recordings = new LabsRecordingsNamespace(sm);
2164
2675
  this.transcripts = new LabsTranscriptsNamespace(sm);
2165
2676
  }
@@ -2401,17 +2912,111 @@ class LabsToolsNamespace {
2401
2912
  class LabsVoicesNamespace {
2402
2913
  constructor(private sm: SupafoneLabs) {}
2403
2914
 
2404
- /** Supafone-managed Cartesia voices. Other TTS engines remain explicit BYOK choices. */
2915
+ /** Live normalized catalog from every connected/managed TTS provider. */
2405
2916
  list(opts: LabsVoiceListOptions = {}): Promise<LabsVoiceListResponse> {
2406
2917
  const q = new URLSearchParams();
2407
2918
  if (opts.provider) q.set("provider", opts.provider);
2408
2919
  if (opts.search) q.set("search", opts.search);
2409
2920
  if (opts.language) q.set("language", opts.language);
2921
+ const compatibleLanguage = opts.compatible_language ?? opts.compatibleLanguage;
2922
+ if (compatibleLanguage) q.set("compatible_language", compatibleLanguage);
2923
+ if (opts.gender) q.set("gender", opts.gender);
2924
+ const voiceType = opts.voice_type ?? opts.voiceType;
2925
+ if (voiceType) q.set("voice_type", voiceType);
2926
+ if (opts.model) q.set("model", opts.model);
2927
+ const runtimeProvider = opts.runtime_provider ?? opts.runtimeProvider;
2928
+ if (runtimeProvider) q.set("runtime_provider", runtimeProvider);
2929
+ const configuredOnly = opts.configured_only ?? opts.configuredOnly;
2930
+ if (configuredOnly !== undefined) q.set("configured_only", String(configuredOnly));
2410
2931
  if (opts.cursor !== undefined) q.set("cursor", String(opts.cursor));
2411
2932
  if (opts.limit !== undefined) q.set("limit", String(opts.limit));
2933
+ const agencyId = opts.agency_id ?? opts.agencyId;
2934
+ if (agencyId) q.set("agency_id", agencyId);
2412
2935
  const suffix = q.toString() ? `?${q}` : "";
2413
2936
  return this.sm.requestSupafoneApi<LabsVoiceListResponse>("GET", `/api/v1/labs/voices${suffix}`);
2414
2937
  }
2938
+
2939
+ /** Provider/model language limits and their Ultravox-compatible intersection. */
2940
+ capabilities(): Promise<LabsVoiceCapabilitiesResponse> {
2941
+ return this.sm.requestSupafoneApi<LabsVoiceCapabilitiesResponse>(
2942
+ "GET",
2943
+ "/api/v1/labs/voices/capabilities",
2944
+ );
2945
+ }
2946
+
2947
+ /** Page through the complete normalized catalog. */
2948
+ async listAll(
2949
+ opts: Omit<LabsVoiceListOptions, "cursor" | "limit"> & { pageSize?: number; maxPages?: number } = {},
2950
+ ): Promise<LabsVoiceListResponse> {
2951
+ const pageSize = Math.max(1, Math.min(opts.pageSize ?? 250, 250));
2952
+ const maxPages = Math.max(1, opts.maxPages ?? 100);
2953
+ let cursor: number | null = 0;
2954
+ let pages = 0;
2955
+ let first: LabsVoiceListResponse | undefined;
2956
+ const voices: LabsVoiceCatalogItem[] = [];
2957
+ const seen = new Set<number>();
2958
+ while (cursor !== null && pages < maxPages) {
2959
+ if (seen.has(cursor)) throw new SupafoneLabsError("Voice catalog returned a repeated cursor");
2960
+ seen.add(cursor);
2961
+ const page = await this.list({ ...opts, cursor, limit: pageSize });
2962
+ first ??= page;
2963
+ voices.push(...page.voices);
2964
+ cursor = page.next_cursor ?? null;
2965
+ pages += 1;
2966
+ }
2967
+ if (cursor !== null) {
2968
+ throw new SupafoneLabsError(`Voice catalog exceeded maxPages=${maxPages}`);
2969
+ }
2970
+ return {
2971
+ ...(first ?? { total: 0, providers: [] }),
2972
+ voices,
2973
+ total: first?.total ?? voices.length,
2974
+ cursor: 0,
2975
+ next_cursor: null,
2976
+ };
2977
+ }
2978
+
2979
+ /** Rank real current voices from a plain-language description. */
2980
+ recommend(opts: LabsVoiceRecommendOptions): Promise<LabsVoiceRecommendResponse> {
2981
+ if (!opts.description?.trim()) {
2982
+ throw new SupafoneLabsError("description is required — describe the voice you want");
2983
+ }
2984
+ return this.sm.requestSupafoneApi<LabsVoiceRecommendResponse>(
2985
+ "POST",
2986
+ "/api/v1/labs/voices/recommend",
2987
+ compact({
2988
+ agency_id: opts.agency_id ?? opts.agencyId,
2989
+ description: opts.description.trim(),
2990
+ language: opts.language,
2991
+ provider: opts.provider,
2992
+ gender: opts.gender,
2993
+ voice_type: opts.voice_type ?? opts.voiceType,
2994
+ model: opts.model,
2995
+ configured_only: opts.configured_only ?? opts.configuredOnly,
2996
+ premium: opts.premium,
2997
+ limit: opts.limit,
2998
+ }),
2999
+ );
3000
+ }
3001
+
3002
+ /** Download an authenticated voice preview. */
3003
+ preview(voiceId: string, opts: { agencyId?: string; agency_id?: string } = {}): Promise<LabsVoicePreview> {
3004
+ if (!voiceId.trim()) throw new SupafoneLabsError("voiceId is required");
3005
+ const q = new URLSearchParams({ voice: voiceId.trim() });
3006
+ const agencyId = opts.agency_id ?? opts.agencyId;
3007
+ if (agencyId) q.set("agency_id", agencyId);
3008
+ return this.sm.requestSupafoneBinary(`/api/v1/labs/voices/preview?${q}`);
3009
+ }
3010
+
3011
+ /** Convert a catalog row into the exact Agent Factory voice field. */
3012
+ selection(voice: LabsVoiceCatalogItem): LabsVoiceSelection {
3013
+ if (!voice?.id) throw new SupafoneLabsError("A catalog voice with id is required");
3014
+ return compact({
3015
+ provider: voice.provider_key || voice.source,
3016
+ voiceId: voice.id,
3017
+ model: voice.model || undefined,
3018
+ }) as LabsVoiceSelection;
3019
+ }
2415
3020
  }
2416
3021
 
2417
3022
  class LabsRuntimeNamespace {
@@ -2452,11 +3057,53 @@ class LabsCallsNamespace {
2452
3057
  return this.sm.requestSupafoneApi<LabsCallListResponse>("GET", `/api/v1/labs/calls${suffix}`);
2453
3058
  }
2454
3059
 
2455
- get(callId: string, opts: { agencyId?: string } = {}): Promise<Record<string, unknown>> {
3060
+ get(callId: string, opts: { agencyId?: string; agency_id?: string } = {}): Promise<{ call: LabsCallArtifact }> {
2456
3061
  const q = new URLSearchParams();
2457
- if (opts.agencyId) q.set("agency_id", opts.agencyId);
3062
+ const agencyId = opts.agency_id ?? opts.agencyId;
3063
+ if (agencyId) q.set("agency_id", agencyId);
3064
+ const suffix = q.toString() ? `?${q}` : "";
3065
+ return this.sm.requestSupafoneApi<{ call: LabsCallArtifact }>("GET", `/api/v1/labs/calls/${encodeURIComponent(callId)}${suffix}`);
3066
+ }
3067
+
3068
+ delete(callId: string, opts: { agencyId?: string; agency_id?: string } = {}): Promise<Record<string, unknown>> {
3069
+ const q = new URLSearchParams();
3070
+ const agencyId = opts.agency_id ?? opts.agencyId;
3071
+ if (agencyId) q.set("agency_id", agencyId);
2458
3072
  const suffix = q.toString() ? `?${q}` : "";
2459
- return this.sm.requestSupafoneApi("GET", `/api/v1/labs/calls/${encodeURIComponent(callId)}${suffix}`);
3073
+ return this.sm.requestSupafoneApi("DELETE", `/api/v1/labs/calls/${encodeURIComponent(callId)}${suffix}`);
3074
+ }
3075
+ }
3076
+
3077
+ class LabsActivityNamespace {
3078
+ constructor(private sm: SupafoneLabs) {}
3079
+
3080
+ list(opts: LabsActivityListOptions = {}): Promise<LabsActivityListResponse> {
3081
+ const q = new URLSearchParams();
3082
+ const accountId = opts.account_id ?? opts.accountId ?? opts.agency_id ?? opts.agencyId;
3083
+ if (accountId) q.set("account_id", accountId);
3084
+ const eventType = opts.event_type ?? opts.eventType;
3085
+ if (eventType) q.set("event_type", eventType);
3086
+ const resourceType = opts.resource_type ?? opts.resourceType;
3087
+ if (resourceType) q.set("resource_type", resourceType);
3088
+ const resourceId = opts.resource_id ?? opts.resourceId;
3089
+ if (resourceId) q.set("resource_id", resourceId);
3090
+ if (opts.limit !== undefined) q.set("limit", String(opts.limit));
3091
+ if (opts.offset !== undefined) q.set("offset", String(opts.offset));
3092
+ const suffix = q.toString() ? `?${q}` : "";
3093
+ return this.sm.requestSupafoneApi<LabsActivityListResponse>("GET", `/api/v1/labs/activity${suffix}`);
3094
+ }
3095
+ }
3096
+
3097
+ class LabsPlansNamespace {
3098
+ constructor(private sm: SupafoneLabs) {}
3099
+
3100
+ list(opts: Omit<LabsActivityListOptions, "eventType" | "event_type" | "resourceType" | "resource_type"> = {}): Promise<LabsActivityListResponse> {
3101
+ const activity = new LabsActivityNamespace(this.sm);
3102
+ return activity.list({
3103
+ ...opts,
3104
+ eventType: "studio.plan.created",
3105
+ resourceType: "studio_plan",
3106
+ });
2460
3107
  }
2461
3108
  }
2462
3109
 
@@ -2820,6 +3467,7 @@ class OptimizerNamespace {
2820
3467
  }
2821
3468
 
2822
3469
  function labsAgentPayload(input: CreateLabsAgentRequest): Record<string, unknown> {
3470
+ const fixedLanguage = input.preferred_language ?? input.preferredLanguage ?? input.language;
2823
3471
  return compact({
2824
3472
  agency_id: input.agency_id ?? input.agencyId,
2825
3473
  agent_key: input.agent_key ?? input.agentKey,
@@ -2845,8 +3493,12 @@ function labsAgentPayload(input: CreateLabsAgentRequest): Record<string, unknown
2845
3493
  goal: input.goal,
2846
3494
  greeting: input.greeting,
2847
3495
  system_prompt: input.system_prompt ?? input.systemPrompt,
2848
- language: input.language,
3496
+ language: fixedLanguage,
2849
3497
  voice: input.voice ? voicePayload(input.voice) : undefined,
3498
+ voice_preference: voicePreferencePayload(
3499
+ input.voice_preference ?? input.voicePreference,
3500
+ fixedLanguage,
3501
+ ),
2850
3502
  provider_keys: input.provider_keys ?? (input.providerKeys ? providerKeysPayload(input.providerKeys) : undefined),
2851
3503
  byok: input.byok ? byokPayload(input.byok) : undefined,
2852
3504
  telephony: input.telephony ? telephonyPayload(input.telephony) : undefined,
@@ -2866,10 +3518,12 @@ function labsAgentPayload(input: CreateLabsAgentRequest): Record<string, unknown
2866
3518
 
2867
3519
  function hostedListQuery(opts: LabsCallListOptions): URLSearchParams {
2868
3520
  const q = new URLSearchParams();
2869
- if (opts.agencyId) q.set("agency_id", opts.agencyId);
3521
+ const agencyId = opts.agency_id ?? opts.agencyId;
3522
+ if (agencyId) q.set("agency_id", agencyId);
2870
3523
  const agentKey = opts.agent_key ?? opts.agentKey;
2871
3524
  if (agentKey) q.set("agent_key", agentKey);
2872
3525
  if (opts.limit !== undefined) q.set("limit", String(opts.limit));
3526
+ if (opts.offset !== undefined) q.set("offset", String(opts.offset));
2873
3527
  return q;
2874
3528
  }
2875
3529
 
@@ -3107,6 +3761,23 @@ function voicePayload(input: LabsVoiceSelection): Record<string, unknown> {
3107
3761
  });
3108
3762
  }
3109
3763
 
3764
+ function voicePreferencePayload(
3765
+ input?: LabsVoicePreference,
3766
+ defaultLanguage?: string,
3767
+ ): Record<string, unknown> | undefined {
3768
+ if (!input) return undefined;
3769
+ return compact({
3770
+ description: input.description,
3771
+ language: input.language ?? defaultLanguage,
3772
+ provider: input.provider,
3773
+ gender: input.gender,
3774
+ voice_type: input.voice_type ?? input.voiceType,
3775
+ model: input.model,
3776
+ configured_only: input.configured_only ?? input.configuredOnly,
3777
+ premium: input.premium,
3778
+ });
3779
+ }
3780
+
3110
3781
  function providerKeysPayload(input: LabsProviderKeys): Record<string, unknown> {
3111
3782
  return compact({
3112
3783
  ultravox: input.ultravox,