supafone-labs 0.4.10 → 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;
@@ -144,14 +202,32 @@ export interface LabsCallStage {
144
202
  key?: string;
145
203
  id?: string;
146
204
  name: string;
205
+ /** Plain-English job description used by the hosted call-plan generator. */
206
+ description?: string;
147
207
  goal?: string;
148
208
  instructions?: string;
149
209
  exitCriteria?: string[];
150
210
  exit_criteria?: string[];
151
211
  tools?: string[];
212
+ temperature?: number;
213
+ nextStages?: string[];
214
+ next_stages?: string[];
152
215
  metadata?: Record<string, unknown>;
153
216
  }
154
217
 
218
+ export type LabsStageGeneration = "oracle" | "template" | "off";
219
+
220
+ export interface LabsCallPlan {
221
+ version: "supafone_call_plan_v1" | string;
222
+ summary: string;
223
+ base_system_prompt: string;
224
+ call_stages: LabsCallStage[];
225
+ generated_by: "supafone_hosted_haiku" | "deterministic_template" | "deterministic_fallback" | "developer" | string;
226
+ model: string;
227
+ fallback: boolean;
228
+ warnings?: string[];
229
+ }
230
+
155
231
  export interface LabsVoiceSelection {
156
232
  provider?: string;
157
233
  voiceId?: string;
@@ -159,6 +235,115 @@ export interface LabsVoiceSelection {
159
235
  model?: string;
160
236
  }
161
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
+
162
347
  export interface LabsProviderKeys {
163
348
  /** Agent runtime/platform providers. */
164
349
  ultravox?: string;
@@ -481,6 +666,8 @@ export interface CreateLabsAgentRequest {
481
666
  agentStyle?: LabsAgentStyle;
482
667
  agent_style?: LabsAgentStyle;
483
668
  name: string;
669
+ /** Plain-language job description used by the hosted call-plan generator. */
670
+ description?: string;
484
671
  assistantName?: string;
485
672
  assistant_name?: string;
486
673
  businessName?: string;
@@ -500,18 +687,30 @@ export interface CreateLabsAgentRequest {
500
687
  preset_key?: string;
501
688
  runtimeMode?: LabsRuntimeMode;
502
689
  runtime_mode?: LabsRuntimeMode;
503
- /** Default true. When true, the SDK creates sensible call stages from prompt metadata. */
504
- callStages?: boolean | LabsCallStage[];
505
- call_stages?: boolean | LabsCallStage[];
690
+ /** Defaults to Supafone's hosted planner; model credentials remain server-side. */
691
+ callStages?: boolean | LabsStageGeneration | LabsCallStage[];
692
+ call_stages?: boolean | LabsStageGeneration | LabsCallStage[];
506
693
  stages?: LabsCallStage[];
507
694
  autoCallStages?: boolean;
508
695
  auto_call_stages?: boolean;
696
+ stageGeneration?: LabsStageGeneration;
697
+ stage_generation?: LabsStageGeneration;
698
+ stageCount?: number;
699
+ stage_count?: number;
700
+ stageDetail?: "compact" | "standard" | "detailed";
701
+ stage_detail?: "compact" | "standard" | "detailed";
509
702
  goal?: string;
510
703
  greeting?: string;
511
704
  systemPrompt?: string;
512
705
  system_prompt?: string;
706
+ /** Fixed language for the full call. Does not enable mid-call switching. */
513
707
  language?: string;
708
+ preferredLanguage?: string;
709
+ preferred_language?: string;
514
710
  voice?: LabsVoiceSelection;
711
+ /** Resolve a real current catalog voice from this plain-language preference. */
712
+ voicePreference?: LabsVoicePreference;
713
+ voice_preference?: LabsVoicePreference;
515
714
  providerKeys?: LabsProviderKeys;
516
715
  provider_keys?: LabsProviderKeys;
517
716
  byok?: LabsProviderKeys | LabsByokConfig;
@@ -578,21 +777,125 @@ export interface LabsToolListResponse {
578
777
 
579
778
  export interface LabsVoiceListOptions {
580
779
  provider?: string;
780
+ search?: string;
781
+ /** Native/accent language advertised for the individual voice. */
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;
794
+ cursor?: number;
795
+ limit?: number;
796
+ agencyId?: string;
797
+ agency_id?: string;
581
798
  }
582
799
 
583
800
  export interface LabsVoiceListResponse {
584
- voices: Array<Record<string, unknown>>;
801
+ account_id?: string;
802
+ voices: LabsVoiceCatalogItem[];
585
803
  total: number;
586
- 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
+ }>;
587
813
  provider_accounts?: Record<string, unknown>;
588
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;
881
+ }
882
+
883
+ export interface LabsRuntimeResponse {
884
+ account_id: string;
885
+ provider: "ultravox" | string;
886
+ managed: boolean;
887
+ byok_connected: boolean;
888
+ base_url?: string;
889
+ updated_at?: string;
589
890
  }
590
891
 
591
892
  export interface LabsCallListOptions {
592
893
  agencyId?: string;
894
+ agency_id?: string;
593
895
  agentKey?: string;
594
896
  agent_key?: string;
595
897
  limit?: number;
898
+ offset?: number;
596
899
  }
597
900
 
598
901
  export interface LabsCallArtifact {
@@ -603,7 +906,11 @@ export interface LabsCallArtifact {
603
906
  started_at?: string;
604
907
  duration_seconds?: number;
605
908
  recording_url?: string;
909
+ recording_download_url?: string;
910
+ recording_archived?: boolean;
606
911
  transcript_url?: string;
912
+ watcher_events?: LabsDeveloperActivityEvent[];
913
+ watcher_event_count?: number;
607
914
  [extra: string]: unknown;
608
915
  }
609
916
 
@@ -632,6 +939,40 @@ export interface LabsTranscriptListResponse {
632
939
  [extra: string]: unknown;
633
940
  }
634
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
+
635
976
  export interface LabsPhoneNumberSearchOptions {
636
977
  agencyId?: string;
637
978
  agency_id?: string;
@@ -718,6 +1059,8 @@ export interface LabsPhoneNumberProvisionRequest {
718
1059
  numberPool?: string;
719
1060
  number_pool?: string;
720
1061
  premium?: boolean;
1062
+ billingCheckoutSessionId?: string;
1063
+ billing_checkout_session_id?: string;
721
1064
  style?: LabsAgentStyle;
722
1065
  agentStyle?: LabsAgentStyle;
723
1066
  agent_style?: LabsAgentStyle;
@@ -779,7 +1122,7 @@ export interface CreateLabsAgentWithNumberRequest extends CreateLabsAgentRequest
779
1122
  }
780
1123
 
781
1124
  export interface CreateLabsAgentWithNumberResponse extends CreateLabsAgentResponse {
782
- number?: LabsPhoneNumberProvisionResponse;
1125
+ number?: LabsPhoneNumberProvisionResponse | LabsBillingCheckoutResponse;
783
1126
  }
784
1127
 
785
1128
  export interface LabsAgentResponse {
@@ -805,6 +1148,7 @@ export interface CreateLabsAgentResponse {
805
1148
  snippet?: string;
806
1149
  [extra: string]: unknown;
807
1150
  };
1151
+ call_plan?: LabsCallPlan;
808
1152
  [extra: string]: unknown;
809
1153
  }
810
1154
 
@@ -1094,6 +1438,140 @@ const COACH_SYSTEM =
1094
1438
  "speaks aloud — a correction or nudge, phrased imperatively. If nothing needs " +
1095
1439
  "correcting, return an empty string.";
1096
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
+
1097
1575
  export class SupafoneLabs {
1098
1576
  readonly baseUrl: string;
1099
1577
  readonly supafoneApiBaseUrl: string;
@@ -1153,6 +1631,15 @@ export class SupafoneLabs {
1153
1631
  this.campaigns = new CampaignsNamespace(this);
1154
1632
  }
1155
1633
 
1634
+ /** Build a complete hosted plan with the same Supafone key used to create agents. */
1635
+ generateCallStages(input: CreateLabsAgentRequest & { description?: string }): Promise<LabsCallPlan> {
1636
+ return this.labs.agents.plan(input);
1637
+ }
1638
+
1639
+ generate_call_stages(input: CreateLabsAgentRequest & { description?: string }): Promise<LabsCallPlan> {
1640
+ return this.generateCallStages(input);
1641
+ }
1642
+
1156
1643
  /** True once login() (or a passed sessionToken) is in effect. */
1157
1644
  get isLoggedIn(): boolean {
1158
1645
  return !!this.sessionToken;
@@ -1215,6 +1702,31 @@ export class SupafoneLabs {
1215
1702
  }
1216
1703
  }
1217
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
+
1218
1730
  /**
1219
1731
  * Exchange the account email/password for a product-API JWT (the same login
1220
1732
  * as app.supafone.ai). Called lazily by campaigns/calls — call directly only
@@ -1426,6 +1938,52 @@ export class SupafoneLabs {
1426
1938
  return out.text.trim();
1427
1939
  }
1428
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
+
1429
1987
  /** Hosted TTS — returns raw audio bytes (WAV/PCM per voice). */
1430
1988
  async tts(text: string, voice = "supafone-labs-calm-en"): Promise<Uint8Array> {
1431
1989
  const ctrl = new AbortController();
@@ -2088,23 +2646,31 @@ class CampaignsNamespace {
2088
2646
 
2089
2647
  class LabsNamespace {
2090
2648
  readonly agents: LabsAgentsNamespace;
2649
+ readonly billing: LabsBillingNamespace;
2091
2650
  readonly presets: LabsPresetsNamespace;
2092
2651
  readonly tools: LabsToolsNamespace;
2093
2652
  readonly voices: LabsVoicesNamespace;
2653
+ readonly runtime: LabsRuntimeNamespace;
2094
2654
  readonly phoneNumbers: LabsPhoneNumbersNamespace;
2095
2655
  readonly telephony: LabsTelephonyNamespace;
2096
2656
  readonly calls: LabsCallsNamespace;
2657
+ readonly activity: LabsActivityNamespace;
2658
+ readonly plans: LabsPlansNamespace;
2097
2659
  readonly recordings: LabsRecordingsNamespace;
2098
2660
  readonly transcripts: LabsTranscriptsNamespace;
2099
2661
 
2100
2662
  constructor(private sm: SupafoneLabs) {
2101
2663
  this.agents = new LabsAgentsNamespace(sm);
2664
+ this.billing = new LabsBillingNamespace(sm);
2102
2665
  this.presets = new LabsPresetsNamespace(sm);
2103
2666
  this.tools = new LabsToolsNamespace(sm);
2104
2667
  this.voices = new LabsVoicesNamespace(sm);
2668
+ this.runtime = new LabsRuntimeNamespace(sm);
2105
2669
  this.phoneNumbers = new LabsPhoneNumbersNamespace(sm);
2106
2670
  this.telephony = new LabsTelephonyNamespace(sm);
2107
2671
  this.calls = new LabsCallsNamespace(sm);
2672
+ this.activity = new LabsActivityNamespace(sm);
2673
+ this.plans = new LabsPlansNamespace(sm);
2108
2674
  this.recordings = new LabsRecordingsNamespace(sm);
2109
2675
  this.transcripts = new LabsTranscriptsNamespace(sm);
2110
2676
  }
@@ -2115,6 +2681,73 @@ class LabsNamespace {
2115
2681
  }
2116
2682
  }
2117
2683
 
2684
+ export type LabsBillingCheckoutKind = "plan" | "credits" | "number_addon";
2685
+
2686
+ export interface LabsBillingCheckoutInput {
2687
+ kind?: LabsBillingCheckoutKind;
2688
+ planKey?: "developer" | "growth" | "scale" | string;
2689
+ plan_key?: string;
2690
+ numberStrategy?: "dedicated" | "premium" | string;
2691
+ number_strategy?: string;
2692
+ phoneNumber?: string;
2693
+ phone_number?: string;
2694
+ quantity?: number;
2695
+ successUrl?: string;
2696
+ success_url?: string;
2697
+ cancelUrl?: string;
2698
+ cancel_url?: string;
2699
+ }
2700
+
2701
+ export interface LabsBillingCheckoutResponse {
2702
+ status: "requires_payment" | "pending" | "paid" | string;
2703
+ checkout_session_id: string;
2704
+ checkout_url?: string;
2705
+ kind: LabsBillingCheckoutKind | string;
2706
+ plan_key?: string | null;
2707
+ number_strategy?: string | null;
2708
+ phone_number?: string | null;
2709
+ stripe_subscription_id?: string | null;
2710
+ ready_to_provision?: boolean;
2711
+ next_step?: string;
2712
+ }
2713
+
2714
+ class LabsBillingNamespace {
2715
+ constructor(private sm: SupafoneLabs) {}
2716
+
2717
+ /** Start hosted Stripe Checkout. MCP callers should render checkout_url as a link. */
2718
+ checkout(input: LabsBillingCheckoutInput = {}): Promise<LabsBillingCheckoutResponse> {
2719
+ return this.sm.request("POST", "/v1/billing/checkout", compact({
2720
+ kind: input.kind ?? "plan",
2721
+ plan_key: input.plan_key ?? input.planKey,
2722
+ number_strategy: input.number_strategy ?? input.numberStrategy,
2723
+ phone_number: input.phone_number ?? input.phoneNumber,
2724
+ quantity: input.quantity,
2725
+ success_url: input.success_url ?? input.successUrl,
2726
+ cancel_url: input.cancel_url ?? input.cancelUrl,
2727
+ }));
2728
+ }
2729
+
2730
+ status(checkoutSessionId: string): Promise<LabsBillingCheckoutResponse> {
2731
+ if (!checkoutSessionId?.trim()) throw new SupafoneLabsError("checkoutSessionId is required");
2732
+ return this.sm.request(
2733
+ "GET",
2734
+ `/v1/billing/checkout/${encodeURIComponent(checkoutSessionId)}`,
2735
+ );
2736
+ }
2737
+
2738
+ portal(): Promise<{ url: string }> {
2739
+ return this.sm.request("POST", "/v1/billing/portal", {});
2740
+ }
2741
+
2742
+ createCheckout(input: LabsBillingCheckoutInput = {}): Promise<LabsBillingCheckoutResponse> {
2743
+ return this.checkout(input);
2744
+ }
2745
+
2746
+ getCheckout(checkoutSessionId: string): Promise<LabsBillingCheckoutResponse> {
2747
+ return this.status(checkoutSessionId);
2748
+ }
2749
+ }
2750
+
2118
2751
  class LabsAgentsNamespace {
2119
2752
  constructor(private sm: SupafoneLabs) {}
2120
2753
 
@@ -2127,6 +2760,19 @@ class LabsAgentsNamespace {
2127
2760
  );
2128
2761
  }
2129
2762
 
2763
+ /** Preview the exact validated call-plan contract the Supafone runtime executes. */
2764
+ plan(input: CreateLabsAgentRequest & { description?: string }): Promise<LabsCallPlan> {
2765
+ return this.sm.requestSupafoneApi<LabsCallPlan>(
2766
+ "POST",
2767
+ "/api/v1/labs/agent-plans",
2768
+ stagePlanPayload(input),
2769
+ );
2770
+ }
2771
+
2772
+ generateCallStages(input: CreateLabsAgentRequest & { description?: string }): Promise<LabsCallPlan> {
2773
+ return this.plan(input);
2774
+ }
2775
+
2130
2776
  /** Default the agent onto the client's Voice Watcher setting (live supervision
2131
2777
  * + QA + scoring) unless the caller set it explicitly; mirror into
2132
2778
  * labs.voice_watcher when a labs block exists. Never overwrites a caller value. */
@@ -2266,13 +2912,140 @@ class LabsToolsNamespace {
2266
2912
  class LabsVoicesNamespace {
2267
2913
  constructor(private sm: SupafoneLabs) {}
2268
2914
 
2269
- /** Supafone-managed Cartesia voices. Other TTS engines remain explicit BYOK choices. */
2915
+ /** Live normalized catalog from every connected/managed TTS provider. */
2270
2916
  list(opts: LabsVoiceListOptions = {}): Promise<LabsVoiceListResponse> {
2271
2917
  const q = new URLSearchParams();
2272
2918
  if (opts.provider) q.set("provider", opts.provider);
2919
+ if (opts.search) q.set("search", opts.search);
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));
2931
+ if (opts.cursor !== undefined) q.set("cursor", String(opts.cursor));
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);
2273
2935
  const suffix = q.toString() ? `?${q}` : "";
2274
2936
  return this.sm.requestSupafoneApi<LabsVoiceListResponse>("GET", `/api/v1/labs/voices${suffix}`);
2275
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
+ }
3020
+ }
3021
+
3022
+ class LabsRuntimeNamespace {
3023
+ constructor(private sm: SupafoneLabs) {}
3024
+
3025
+ get(opts: { agencyId?: string; agency_id?: string } = {}): Promise<LabsRuntimeResponse> {
3026
+ const q = new URLSearchParams();
3027
+ const agencyId = opts.agency_id ?? opts.agencyId;
3028
+ if (agencyId) q.set("agency_id", agencyId);
3029
+ const suffix = q.toString() ? `?${q}` : "";
3030
+ return this.sm.requestSupafoneApi<LabsRuntimeResponse>("GET", `/api/v1/labs/runtime${suffix}`);
3031
+ }
3032
+
3033
+ configure(input: {
3034
+ agencyId?: string;
3035
+ agency_id?: string;
3036
+ provider?: "ultravox" | string;
3037
+ credentials?: { apiKey?: string; api_key?: string; baseUrl?: string; base_url?: string };
3038
+ }): Promise<LabsRuntimeResponse> {
3039
+ const credentials = input.credentials ?? {};
3040
+ return this.sm.requestSupafoneApi<LabsRuntimeResponse>("PUT", "/api/v1/labs/runtime", compact({
3041
+ agency_id: input.agency_id ?? input.agencyId,
3042
+ provider: input.provider ?? "ultravox",
3043
+ credentials: compact({
3044
+ api_key: credentials.api_key ?? credentials.apiKey,
3045
+ base_url: credentials.base_url ?? credentials.baseUrl,
3046
+ }),
3047
+ }));
3048
+ }
2276
3049
  }
2277
3050
 
2278
3051
  class LabsCallsNamespace {
@@ -2284,11 +3057,53 @@ class LabsCallsNamespace {
2284
3057
  return this.sm.requestSupafoneApi<LabsCallListResponse>("GET", `/api/v1/labs/calls${suffix}`);
2285
3058
  }
2286
3059
 
2287
- get(callId: string, opts: { agencyId?: string } = {}): Promise<Record<string, unknown>> {
3060
+ get(callId: string, opts: { agencyId?: string; agency_id?: string } = {}): Promise<{ call: LabsCallArtifact }> {
2288
3061
  const q = new URLSearchParams();
2289
- 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);
2290
3064
  const suffix = q.toString() ? `?${q}` : "";
2291
- return this.sm.requestSupafoneApi("GET", `/api/v1/labs/calls/${encodeURIComponent(callId)}${suffix}`);
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);
3072
+ const suffix = q.toString() ? `?${q}` : "";
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
+ });
2292
3107
  }
2293
3108
  }
2294
3109
 
@@ -2362,13 +3177,32 @@ class LabsPhoneNumbersNamespace {
2362
3177
  );
2363
3178
  }
2364
3179
 
2365
- /** Buy a Supafone-managed number. Developers do not need a Twilio account. */
2366
- buy(input: LabsPhoneNumberProvisionRequest): Promise<LabsPhoneNumberProvisionResponse> {
3180
+ /**
3181
+ * Buy a managed number. Paid strategies return a hosted Checkout link first;
3182
+ * call again with billingCheckoutSessionId after Checkout reports paid.
3183
+ */
3184
+ buy(
3185
+ input: LabsPhoneNumberProvisionRequest,
3186
+ ): Promise<LabsPhoneNumberProvisionResponse | LabsBillingCheckoutResponse> {
3187
+ const strategy = input.number_strategy ?? input.numberStrategy ?? (input.premium ? "premium" : "default_pool");
3188
+ const checkoutSessionId = input.billing_checkout_session_id ?? input.billingCheckoutSessionId;
3189
+ if ((strategy === "dedicated" || strategy === "premium") && !checkoutSessionId) {
3190
+ const phoneNumber = input.phone_number ?? input.phoneNumber;
3191
+ if (!phoneNumber) {
3192
+ return Promise.reject(new SupafoneLabsError("phoneNumber is required before starting number Checkout"));
3193
+ }
3194
+ return this.sm.labs.billing.checkout({
3195
+ kind: "number_addon",
3196
+ numberStrategy: strategy,
3197
+ phoneNumber,
3198
+ });
3199
+ }
2367
3200
  return this.sm.requestSupafoneApi<LabsPhoneNumberProvisionResponse>(
2368
3201
  "POST",
2369
3202
  "/api/v1/labs/phone-numbers",
2370
3203
  phoneNumberProvisionPayload({
2371
3204
  ...input,
3205
+ numberStrategy: strategy,
2372
3206
  telephony: input.telephony ?? { mode: "supafone_managed", provider: "supafone" },
2373
3207
  }),
2374
3208
  );
@@ -2423,7 +3257,9 @@ class LabsPhoneNumbersNamespace {
2423
3257
  * Search if needed, buy the first matching Supafone-managed number, and assign
2424
3258
  * it to the supplied agent. This is the zero-Twilio-account happy path.
2425
3259
  */
2426
- async buyAndAssign(input: LabsPhoneNumberBuyAndAssignRequest): Promise<LabsPhoneNumberProvisionResponse> {
3260
+ async buyAndAssign(
3261
+ input: LabsPhoneNumberBuyAndAssignRequest,
3262
+ ): Promise<LabsPhoneNumberProvisionResponse | LabsBillingCheckoutResponse> {
2427
3263
  let phoneNumber = input.phoneNumber ?? input.phone_number ?? "";
2428
3264
  if (!phoneNumber) {
2429
3265
  const found = await this.search({
@@ -2631,12 +3467,14 @@ class OptimizerNamespace {
2631
3467
  }
2632
3468
 
2633
3469
  function labsAgentPayload(input: CreateLabsAgentRequest): Record<string, unknown> {
3470
+ const fixedLanguage = input.preferred_language ?? input.preferredLanguage ?? input.language;
2634
3471
  return compact({
2635
3472
  agency_id: input.agency_id ?? input.agencyId,
2636
3473
  agent_key: input.agent_key ?? input.agentKey,
2637
3474
  agent_type: input.agent_type ?? input.agentType,
2638
3475
  style: input.agent_style ?? input.agentStyle ?? input.style,
2639
3476
  name: input.name,
3477
+ description: input.description,
2640
3478
  assistant_name: input.assistant_name ?? input.assistantName,
2641
3479
  business_name: input.business_name ?? input.businessName,
2642
3480
  industry: input.industry,
@@ -2649,11 +3487,18 @@ function labsAgentPayload(input: CreateLabsAgentRequest): Record<string, unknown
2649
3487
  preset_key: input.preset_key ?? input.presetKey,
2650
3488
  runtime_mode: input.runtime_mode ?? input.runtimeMode,
2651
3489
  call_stages: callStagesPayload(input),
3490
+ stage_generation: input.stage_generation ?? input.stageGeneration,
3491
+ stage_count: input.stage_count ?? input.stageCount,
3492
+ stage_detail: input.stage_detail ?? input.stageDetail,
2652
3493
  goal: input.goal,
2653
3494
  greeting: input.greeting,
2654
3495
  system_prompt: input.system_prompt ?? input.systemPrompt,
2655
- language: input.language,
3496
+ language: fixedLanguage,
2656
3497
  voice: input.voice ? voicePayload(input.voice) : undefined,
3498
+ voice_preference: voicePreferencePayload(
3499
+ input.voice_preference ?? input.voicePreference,
3500
+ fixedLanguage,
3501
+ ),
2657
3502
  provider_keys: input.provider_keys ?? (input.providerKeys ? providerKeysPayload(input.providerKeys) : undefined),
2658
3503
  byok: input.byok ? byokPayload(input.byok) : undefined,
2659
3504
  telephony: input.telephony ? telephonyPayload(input.telephony) : undefined,
@@ -2673,10 +3518,12 @@ function labsAgentPayload(input: CreateLabsAgentRequest): Record<string, unknown
2673
3518
 
2674
3519
  function hostedListQuery(opts: LabsCallListOptions): URLSearchParams {
2675
3520
  const q = new URLSearchParams();
2676
- 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);
2677
3523
  const agentKey = opts.agent_key ?? opts.agentKey;
2678
3524
  if (agentKey) q.set("agent_key", agentKey);
2679
3525
  if (opts.limit !== undefined) q.set("limit", String(opts.limit));
3526
+ if (opts.offset !== undefined) q.set("offset", String(opts.offset));
2680
3527
  return q;
2681
3528
  }
2682
3529
 
@@ -2752,6 +3599,7 @@ function phoneNumberProvisionPayload(input: LabsPhoneNumberProvisionRequest): Re
2752
3599
  number_strategy: input.number_strategy ?? input.numberStrategy,
2753
3600
  number_pool: input.number_pool ?? input.numberPool,
2754
3601
  premium: input.premium,
3602
+ billing_checkout_session_id: input.billing_checkout_session_id ?? input.billingCheckoutSessionId,
2755
3603
  style: input.agent_style ?? input.agentStyle ?? input.style,
2756
3604
  direction: input.direction,
2757
3605
  telephony: input.telephony ? telephonyPayload(input.telephony) : undefined,
@@ -2786,12 +3634,39 @@ function phoneNumberReleasePayload(input: LabsPhoneNumberReleaseRequest): Record
2786
3634
  });
2787
3635
  }
2788
3636
 
2789
- function callStagesPayload(input: CreateLabsAgentRequest): Record<string, unknown>[] | undefined {
3637
+ function callStagesPayload(
3638
+ input: CreateLabsAgentRequest,
3639
+ ): Record<string, unknown>[] | LabsStageGeneration | false | undefined {
2790
3640
  const explicit = input.call_stages ?? input.callStages ?? input.stages;
2791
3641
  const auto = input.auto_call_stages ?? input.autoCallStages;
2792
3642
  if (Array.isArray(explicit)) return explicit.map(callStagePayload);
2793
- if (explicit === false || auto === false) return undefined;
2794
- return generateCallStages(input).map(callStagePayload);
3643
+ if (explicit === false || auto === false) return false;
3644
+ if (explicit === "oracle" || explicit === "template" || explicit === "off") return explicit;
3645
+ // Omitted means the private Supafone API generates and compiles the plan.
3646
+ return undefined;
3647
+ }
3648
+
3649
+ function stagePlanPayload(
3650
+ input: CreateLabsAgentRequest & { description?: string },
3651
+ ): Record<string, unknown> {
3652
+ return compact({
3653
+ agency_id: input.agency_id ?? input.agencyId,
3654
+ description: input.description,
3655
+ name: input.name,
3656
+ assistant_name: input.assistant_name ?? input.assistantName,
3657
+ business_name: input.business_name ?? input.businessName,
3658
+ industry: input.industry,
3659
+ direction: input.direction ?? input.agent_style ?? input.agentStyle ?? input.style,
3660
+ goal: input.goal,
3661
+ system_prompt: input.system_prompt ?? input.systemPrompt,
3662
+ tools: input.tools ? toolsPayload(input.tools) : undefined,
3663
+ call_stages: Array.isArray(input.call_stages ?? input.callStages ?? input.stages)
3664
+ ? (input.call_stages ?? input.callStages ?? input.stages)
3665
+ : undefined,
3666
+ stage_generation: input.stage_generation ?? input.stageGeneration,
3667
+ stage_count: input.stage_count ?? input.stageCount,
3668
+ stage_detail: input.stage_detail ?? input.stageDetail,
3669
+ });
2795
3670
  }
2796
3671
 
2797
3672
  function callStagePayload(input: LabsCallStage): Record<string, unknown> {
@@ -2802,11 +3677,15 @@ function callStagePayload(input: LabsCallStage): Record<string, unknown> {
2802
3677
  instructions: input.instructions,
2803
3678
  exit_criteria: input.exit_criteria ?? input.exitCriteria,
2804
3679
  tools: input.tools,
3680
+ temperature: input.temperature,
3681
+ next_stages: input.next_stages ?? input.nextStages,
2805
3682
  metadata: input.metadata,
2806
3683
  });
2807
3684
  }
2808
3685
 
2809
3686
  export function generateCallStages(input: CreateLabsAgentRequest): LabsCallStage[] {
3687
+ // Offline compatibility template. New code should use
3688
+ // client.generateCallStages(...) for the hosted, executable plan.
2810
3689
  const direction = String(input.direction ?? input.agent_style ?? input.agentStyle ?? input.style ?? "inbound").toLowerCase();
2811
3690
  const haystack = [
2812
3691
  input.name,
@@ -2882,6 +3761,23 @@ function voicePayload(input: LabsVoiceSelection): Record<string, unknown> {
2882
3761
  });
2883
3762
  }
2884
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
+
2885
3781
  function providerKeysPayload(input: LabsProviderKeys): Record<string, unknown> {
2886
3782
  return compact({
2887
3783
  ultravox: input.ultravox,