@voicelayer/sdk 0.1.10 → 0.1.11

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/README.md CHANGED
@@ -44,7 +44,23 @@ That's what this SDK does. One declaration:
44
44
 
45
45
  ```ts
46
46
  import { defineAgent, defineProcess } from '@voicelayer/sdk';
47
- import { salesforce } from '@voicelayer/connector-salesforce';
47
+
48
+ // A connector is any object with this shape — write one for whatever system
49
+ // you already use. Its methods land on `ctx.connectors.<name>`.
50
+ const crm = {
51
+ __connector: true as const,
52
+ name: 'crm',
53
+ api: {
54
+ createClaim: async (data: Record<string, unknown>) => {
55
+ const res = await fetch('https://your-crm.example.com/claims', {
56
+ method: 'POST',
57
+ headers: { 'content-type': 'application/json' },
58
+ body: JSON.stringify(data),
59
+ });
60
+ return (await res.json()) as { id: string };
61
+ },
62
+ },
63
+ };
48
64
 
49
65
  export default await defineAgent({
50
66
  name: 'fnol',
@@ -62,7 +78,7 @@ export default await defineAgent({
62
78
  },
63
79
  completeWhen: 'all-required-captured',
64
80
  onComplete: async (data, ctx) => {
65
- const claim = await ctx.connectors.salesforce.createClaim(data);
81
+ const claim = await ctx.connectors.crm.createClaim(data);
66
82
  await ctx.say(`Your claim number is ${claim.id}. You will receive a text shortly.`);
67
83
  },
68
84
  }),
@@ -87,10 +103,8 @@ export default await defineAgent({
87
103
  piiFields: ['ssn', 'dob'],
88
104
  },
89
105
 
90
- // 5. Connectors — auto-wired tools, available to the LLM and in ctx.
91
- connectors: {
92
- salesforce: salesforce({ objects: ['Claim', 'Account'] }),
93
- },
106
+ // 5. Connectors — available on ctx, and as LLM tools when `expose: true`.
107
+ connectors: { crm },
94
108
 
95
109
  // 6. Compliance — opt-in posture, defaults are safe.
96
110
  compliance: ['tcpa'],
@@ -131,10 +145,13 @@ defineAgent({
131
145
  if (text.includes('refund')) ctx.metadata.flagged = true;
132
146
  },
133
147
  async onFieldCaptured(field, value, ctx) {
134
- if (field === 'claimNumber') await ctx.connectors.salesforce.lookup(value);
148
+ if (field === 'claimNumber') await ctx.connectors.crm.lookup(value);
135
149
  },
136
150
  async onCallEnd(outcome, ctx) {
137
- await ctx.connectors.slack.send(`#claims`, `Call ${ctx.call.id} ended: ${outcome.reason}`);
151
+ await fetch('https://your-api.example.com/call-ended', {
152
+ method: 'POST',
153
+ body: JSON.stringify({ callId: ctx.call.id, reason: outcome.reason }),
154
+ });
138
155
  },
139
156
  });
140
157
  ```
@@ -148,7 +165,7 @@ defineAgent({
148
165
  // You own the entire conversation loop.
149
166
  await ctx.say('Hi! What is your policy number?');
150
167
  const policy = await ctx.ask();
151
- const account = await ctx.connectors.salesforce.lookupPolicy(policy);
168
+ const account = await ctx.connectors.crm.lookupPolicy(policy);
152
169
  // ... do whatever ...
153
170
  },
154
171
  });
@@ -240,50 +257,53 @@ export default await defineAgent({
240
257
 
241
258
  Works with OpenAI, Azure, Together, Groq, vLLM, or a [LiteLLM](https://docs.litellm.ai) proxy in front of anything. The URL is `https`-only and SSRF-guarded; under the hood it reuses the proven LiveKit OpenAI client.
242
259
 
243
- **3. A private endpoint (tunnel)** — keep your brain behind your firewall. Run the one-container `vl-brain` daemon — it dials *out* to us (no inbound ports) and LiteLLM is baked in to normalize any provider. The connector then shows up as a voice agent in the dashboard (its LLM is your brain; STT/TTS still swappable) — test it in the Playground, attach a number. No SDK code required. See the [`vl-brain` daemon guide](../../apps/connector-daemon/README.md).
260
+ **3. A private endpoint (tunnel)** — keep your brain behind your firewall. Run the one-container `vl-brain` daemon — it dials *out* to us (no inbound ports) and LiteLLM is baked in to normalize any provider. The connector then shows up as a voice agent in the dashboard (its LLM is your brain; STT/TTS still swappable) — test it in the Playground, attach a number. No SDK code required. See the [`vl-brain` daemon guide](https://vlayers.ai/docs/brain-connectors).
244
261
 
245
262
  ---
246
263
 
247
264
  ## Connectors
248
265
 
249
- Connectors are **typed integrations** with external systems. They show up on `ctx.connectors` and (optionally) as LLM tools.
266
+ Connectors are **typed integrations** with external systems. They show up on `ctx.connectors` and, when `expose: true`, every method also becomes an LLM-callable tool automatically.
267
+
268
+ There is no connector registry to install from — a connector is a plain object, so you write one against whatever API you already have:
250
269
 
251
270
  ```ts
252
- import { salesforce, twilio, slack, zendesk, webhook } from '@voicelayer/connectors';
271
+ import { defineAgent, type ConnectorInstance } from '@voicelayer/sdk';
272
+
273
+ const crm = {
274
+ __connector: true as const,
275
+ name: 'crm',
276
+ // With `expose: true`, the LLM can call `crm_lookupPolicy` on its own.
277
+ expose: true,
278
+ api: {
279
+ lookupPolicy: async ({ policyNumber }: { policyNumber: string }) => {
280
+ const res = await fetch(`https://your-crm.example.com/policies/${policyNumber}`, {
281
+ headers: { authorization: `Bearer ${process.env.CRM_TOKEN}` },
282
+ });
283
+ return res.json();
284
+ },
285
+ },
286
+ } satisfies ConnectorInstance;
253
287
 
254
288
  defineAgent({
255
289
  // ...
256
- connectors: {
257
- crm: salesforce({ objects: ['Claim'] }),
258
- sms: twilio(),
259
- alerts: slack({ channel: '#claims' }),
260
- custom: webhook({ url: 'https://your-api.example.com/voicelayer' }),
261
- },
290
+ connectors: { crm },
262
291
  });
263
292
  ```
264
293
 
265
- Available connectors: `salesforce`, `twilio`, `slack`, `zendesk`, `webhook`, `epic` (HIPAA-scoped), `servicenow`. More on the roadmap.
294
+ Each method receives `(args, ctx)`, so you can reach call state from inside a connector. When a `ToolRouter` is active, exposed connector methods are dispatched through Action Guard and audited like any other tool.
266
295
 
267
296
  ---
268
297
 
269
- ## Testing locally
298
+ ## Running locally
270
299
 
271
- ```ts
272
- import { testAgent } from '@voicelayer/sdk/testing';
273
- import agent from './agent.js';
274
-
275
- test('captures all required fields', async () => {
276
- const result = await testAgent(agent)
277
- .say('I was in a wreck yesterday with my Honda Civic, no injuries.')
278
- .say('Claim number is AB-12345678.')
279
- .end();
280
-
281
- expect(result.process.complete).toBe(true);
282
- expect(result.process.data.vehiclesInvolved).toEqual(['Honda Civic']);
283
- });
300
+ ```bash
301
+ node --import tsx agent.ts dev
284
302
  ```
285
303
 
286
- No LiveKit, no audio, no API keys. Pure unit-test speed.
304
+ This boots the worker against your LiveKit project and registers the agent with the VoiceLayer control plane, so you can call your number and iterate on the same file you will deploy. Set the environment variables listed under [Production](#production) first.
305
+
306
+ > A headless harness for asserting on process capture without audio is not part of the public API yet. Today, iterate by calling the agent, or drive the flow from the Playground in the dashboard.
287
307
 
288
308
  ---
289
309
 
@@ -329,6 +349,7 @@ At `1.0` we'll commit to semver and a stable public surface. Until then, expect
329
349
 
330
350
  ## Reference
331
351
 
332
- - API: [`docs/API.md`](../../docs/API.md)
333
- - Architecture (what's underneath): [`docs/architecture.md`](../../docs/architecture.md)
334
- - Migration from 0.x → 0.next: [`CHANGELOG.md`](./CHANGELOG.md)
352
+ - [SDK documentation](https://vlayers.ai/docs/sdk)
353
+ - [Dashboard, flow builder, and brain connectors](https://vlayers.ai/docs)
354
+ - [Pricing](https://vlayers.ai/pricing) — $0.05/min flat, providers billed at cost
355
+ - [github.com/vlayers](https://github.com/vlayers)
package/dist/index.js CHANGED
@@ -1669,6 +1669,212 @@ var init_connection = __esm({
1669
1669
  });
1670
1670
  }
1671
1671
  });
1672
+ var E164, RoomPrefix, RecordingPrefix, Cidr, TelephonySettings, ProfileSource, EffectiveCallProfile;
1673
+ var init_telephony = __esm({
1674
+ "../contracts/src/telephony.ts"() {
1675
+ E164 = z.string().regex(/^\+[1-9]\d{6,14}$/, "must be E.164 (+15551234567)");
1676
+ RoomPrefix = z.string().regex(/^[a-z0-9][a-z0-9-]{0,30}[a-z0-9-]?$/, "lowercase letters, digits, dashes; max 32");
1677
+ RecordingPrefix = z.string().min(1).max(128).refine((v) => !v.startsWith("/") && !v.includes(".."), "relative key prefix only");
1678
+ Cidr = z.string().regex(/^\d{1,3}(\.\d{1,3}){3}\/\d{1,2}$/, "must be IPv4 CIDR (a.b.c.d/nn)");
1679
+ TelephonySettings = z.object({
1680
+ outboundTrunkId: z.string().min(1).max(128).nullable().default(null),
1681
+ defaultFromE164: E164.nullable().default(null),
1682
+ roomPrefix: RoomPrefix.nullable().default(null),
1683
+ sipAllowedCidrs: z.array(Cidr).max(32).nullable().default(null),
1684
+ recordingPrefix: RecordingPrefix.nullable().default(null),
1685
+ strictByok: z.boolean().default(false)
1686
+ });
1687
+ z.object({
1688
+ outboundTrunkId: z.string().min(1).max(128).nullable().optional(),
1689
+ defaultFromE164: E164.nullable().optional(),
1690
+ roomPrefix: RoomPrefix.nullable().optional(),
1691
+ sipAllowedCidrs: z.array(Cidr).max(32).nullable().optional(),
1692
+ recordingPrefix: RecordingPrefix.nullable().optional(),
1693
+ strictByok: z.boolean().optional()
1694
+ }).refine((v) => Object.keys(v).length > 0, { message: "nothing to update" });
1695
+ ProfileSource = z.enum(["org", "platform", "none"]);
1696
+ EffectiveCallProfile = z.object({
1697
+ projectId: z.string(),
1698
+ outboundTrunkId: z.string().nullable(),
1699
+ defaultFromE164: z.string().nullable(),
1700
+ roomPrefix: z.string(),
1701
+ sipAllowedCidrs: z.array(z.string()),
1702
+ recordingPrefix: z.string(),
1703
+ strictByok: z.boolean(),
1704
+ sources: z.object({
1705
+ outboundTrunkId: ProfileSource,
1706
+ defaultFromE164: ProfileSource,
1707
+ roomPrefix: ProfileSource,
1708
+ sipAllowedCidrs: ProfileSource,
1709
+ recordingPrefix: ProfileSource
1710
+ })
1711
+ });
1712
+ z.object({
1713
+ settings: TelephonySettings,
1714
+ profile: EffectiveCallProfile
1715
+ });
1716
+ }
1717
+ });
1718
+ var SHORTENER_HOSTS, PLACEHOLDER_HOSTS, PublicUrl, A2pBusinessType, A2pBusinessInfo, A2pOptInType, A2pUseCase, A2pMessagingProfile, A2pState;
1719
+ var init_a2p = __esm({
1720
+ "../contracts/src/a2p.ts"() {
1721
+ SHORTENER_HOSTS = [
1722
+ "bit.ly",
1723
+ "tinyurl.com",
1724
+ "t.co",
1725
+ "goo.gl",
1726
+ "ow.ly",
1727
+ "is.gd",
1728
+ "buff.ly",
1729
+ "rebrand.ly",
1730
+ "cutt.ly",
1731
+ "shorturl.at",
1732
+ "tiny.cc"
1733
+ ];
1734
+ PLACEHOLDER_HOSTS = ["acme.com", "example.com", "example.org", "test.com", "localhost"];
1735
+ PublicUrl = z.string().url().refine((v) => v.startsWith("https://"), "must be https").refine((v) => {
1736
+ try {
1737
+ const host = new URL(v).hostname.toLowerCase().replace(/^www\./, "");
1738
+ return !SHORTENER_HOSTS.includes(host);
1739
+ } catch {
1740
+ return false;
1741
+ }
1742
+ }, "public URL shorteners are rejected by carriers \u2014 use your own domain").refine((v) => {
1743
+ try {
1744
+ const host = new URL(v).hostname.toLowerCase().replace(/^www\./, "");
1745
+ return !PLACEHOLDER_HOSTS.some((p) => host === p || host.endsWith(`.${p}`));
1746
+ } catch {
1747
+ return false;
1748
+ }
1749
+ }, "placeholder domain \u2014 reviewers will follow this link and reject the campaign");
1750
+ A2pBusinessType = z.enum([
1751
+ "Sole Proprietorship",
1752
+ "Partnership",
1753
+ "Corporation",
1754
+ "Co-operative",
1755
+ "Limited Liability Corporation",
1756
+ "Non-profit Corporation"
1757
+ ]);
1758
+ A2pBusinessInfo = z.object({
1759
+ legalName: z.string().trim().min(2).max(200),
1760
+ /** EIN (US) or equivalent registration number. */
1761
+ registrationNumber: z.string().trim().min(4).max(50),
1762
+ businessType: A2pBusinessType,
1763
+ /** Publicly reachable production site — not staging, not a 404. */
1764
+ website: PublicUrl,
1765
+ industry: z.string().trim().min(2).max(60),
1766
+ address: z.object({
1767
+ street: z.string().trim().min(2).max(200),
1768
+ city: z.string().trim().min(1).max(100),
1769
+ region: z.string().trim().min(1).max(100),
1770
+ postalCode: z.string().trim().min(2).max(20),
1771
+ isoCountry: z.string().trim().length(2)
1772
+ }),
1773
+ authorizedRep: z.object({
1774
+ firstName: z.string().trim().min(1).max(100),
1775
+ lastName: z.string().trim().min(1).max(100),
1776
+ email: z.string().trim().email(),
1777
+ phone: z.string().trim().regex(/^\+[1-9]\d{6,14}$/, "must be E.164"),
1778
+ jobTitle: z.string().trim().min(2).max(100)
1779
+ })
1780
+ });
1781
+ A2pOptInType = z.enum(["WEB_FORM", "PAPER_FORM", "VERBAL", "VIA_TEXT", "MOBILE_QR_CODE"]);
1782
+ A2pUseCase = z.enum([
1783
+ "MIXED",
1784
+ "CUSTOMER_CARE",
1785
+ "MARKETING",
1786
+ "ACCOUNT_NOTIFICATION",
1787
+ "2FA",
1788
+ "DELIVERY_NOTIFICATION",
1789
+ "HIGHER_EDUCATION",
1790
+ "POLLING_VOTING",
1791
+ "PUBLIC_SERVICE_ANNOUNCEMENT",
1792
+ "LOW_VOLUME"
1793
+ ]);
1794
+ A2pMessagingProfile = z.object({
1795
+ useCase: A2pUseCase,
1796
+ /** Specific, not generic. "We send texts" gets rejected; describe the actual
1797
+ * messages and when they are sent. */
1798
+ description: z.string().trim().min(40).max(4096),
1799
+ /**
1800
+ * The single most-rejected field. Must describe HOW people opt in, state the
1801
+ * message frequency, include the "message and data rates may apply"
1802
+ * disclosure, and link to publicly reachable evidence. Twilio's API bounds it
1803
+ * to 40–2049 characters.
1804
+ */
1805
+ messageFlow: z.string().trim().min(40).max(2049),
1806
+ optInType: A2pOptInType,
1807
+ /** Publicly accessible screenshots/pages showing the opt-in. Reviewers open
1808
+ * these; anything behind a login fails. */
1809
+ optInEvidenceUrls: z.array(PublicUrl).min(1).max(5),
1810
+ /** Real messages the customer will send. Must reflect the declared use case
1811
+ * and carry opt-out language. */
1812
+ messageSamples: z.array(z.string().trim().min(10).max(1024)).min(2).max(5),
1813
+ privacyPolicyUrl: PublicUrl,
1814
+ termsAndConditionsUrl: PublicUrl,
1815
+ hasEmbeddedLinks: z.boolean().default(false),
1816
+ hasEmbeddedPhone: z.boolean().default(false)
1817
+ });
1818
+ z.object({
1819
+ business: A2pBusinessInfo,
1820
+ messaging: A2pMessagingProfile,
1821
+ /** Register against Twilio's mock endpoints — exercises the full pipeline
1822
+ * with no fees and no real carrier submission. Used in CI and staging. */
1823
+ mock: z.boolean().default(false)
1824
+ }).superRefine((v, ctx) => {
1825
+ const flow = v.messaging.messageFlow.toLowerCase();
1826
+ if (!/(msg|message)\s*(&|and)\s*data rates/.test(flow)) {
1827
+ ctx.addIssue({
1828
+ code: z.ZodIssueCode.custom,
1829
+ path: ["messaging", "messageFlow"],
1830
+ message: 'must include a "Message and data rates may apply" disclosure \u2014 carriers reject without it'
1831
+ });
1832
+ }
1833
+ if (!/\d/.test(flow) || !/(msg|message|text)/.test(flow)) {
1834
+ ctx.addIssue({
1835
+ code: z.ZodIssueCode.custom,
1836
+ path: ["messaging", "messageFlow"],
1837
+ message: 'must state message frequency, e.g. "Up to 4 msgs/month"'
1838
+ });
1839
+ }
1840
+ const hasOptOut = v.messaging.messageSamples.some((s) => /stop/i.test(s));
1841
+ if (!hasOptOut) {
1842
+ ctx.addIssue({
1843
+ code: z.ZodIssueCode.custom,
1844
+ path: ["messaging", "messageSamples"],
1845
+ message: 'at least one sample must include opt-out language (e.g. "Reply STOP to opt out")'
1846
+ });
1847
+ }
1848
+ });
1849
+ A2pState = z.enum([
1850
+ "none",
1851
+ "profile_pending",
1852
+ "profile_approved",
1853
+ "profile_failed",
1854
+ "brand_pending",
1855
+ "brand_approved",
1856
+ "brand_failed",
1857
+ "campaign_pending",
1858
+ "messaging_ready",
1859
+ "campaign_failed"
1860
+ ]);
1861
+ z.object({
1862
+ state: A2pState,
1863
+ customerProfileSid: z.string().nullable(),
1864
+ trustProductSid: z.string().nullable(),
1865
+ brandSid: z.string().nullable(),
1866
+ messagingServiceSid: z.string().nullable(),
1867
+ campaignSid: z.string().nullable(),
1868
+ mock: z.boolean(),
1869
+ /** Carrier/Twilio rejection details, surfaced verbatim so the customer can
1870
+ * fix the specific field rather than guess. */
1871
+ failures: z.array(z.object({ code: z.number().nullable(), field: z.string().nullable(), message: z.string() })).default([]),
1872
+ /** Plain-language next step for the dashboard. */
1873
+ nextAction: z.string().nullable(),
1874
+ updatedAt: z.string().nullable()
1875
+ });
1876
+ }
1877
+ });
1672
1878
  var ConnectorMode, ConnectorStatus, ConnectorNormalize;
1673
1879
  var init_connector = __esm({
1674
1880
  "../contracts/src/connector.ts"() {
@@ -2307,6 +2513,8 @@ var init_src = __esm({
2307
2513
  init_call_recording();
2308
2514
  init_outbound();
2309
2515
  init_connection();
2516
+ init_telephony();
2517
+ init_a2p();
2310
2518
  init_connector();
2311
2519
  init_connector_stream();
2312
2520
  init_flow_compile();
@@ -7605,12 +7813,12 @@ function definitionFor(opts) {
7605
7813
  }
7606
7814
  async function runTextTranscript(opts) {
7607
7815
  const replies = [];
7608
- const trace7 = [];
7816
+ const trace8 = [];
7609
7817
  const processRt = opts.processRt ?? createProcessRuntime(definitionFor(opts));
7610
7818
  const session = runTextSession({
7611
7819
  program: opts.program,
7612
7820
  onAgentText: (t) => void replies.push(t),
7613
- onTrace: (e) => void trace7.push(e),
7821
+ onTrace: (e) => void trace8.push(e),
7614
7822
  ...processRt ? { processRt } : {},
7615
7823
  ...opts.generate ? { generate: opts.generate } : {},
7616
7824
  ...opts.call ? { call: opts.call } : {},
@@ -7619,7 +7827,7 @@ async function runTextTranscript(opts) {
7619
7827
  for (const m of opts.messages) session.sendUserMessage(m);
7620
7828
  session.end();
7621
7829
  const outcome = await session.result;
7622
- return { replies, outcome, trace: trace7 };
7830
+ return { replies, outcome, trace: trace8 };
7623
7831
  }
7624
7832
  function ensureLkLogger() {
7625
7833
  if (loggerOptions() === void 0) {
@@ -7652,9 +7860,15 @@ async function runAgentTextTranscript(opts) {
7652
7860
  return { replies };
7653
7861
  }
7654
7862
  var DEFAULT_OTLP_ENDPOINT = "https://otel.vlayers.ai/v1/otlp";
7863
+ var DEFAULT_METRIC_EXPORT_INTERVAL_MS = 15e3;
7655
7864
  var started = false;
7656
7865
  var sdk = null;
7657
7866
  var loggerProvider = null;
7867
+ var metricReader = null;
7868
+ function metricExportIntervalMs() {
7869
+ const raw = Number(process.env.OTEL_METRIC_EXPORT_INTERVAL);
7870
+ return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_METRIC_EXPORT_INTERVAL_MS;
7871
+ }
7658
7872
  function start(opts) {
7659
7873
  if (started)
7660
7874
  return;
@@ -7679,21 +7893,23 @@ function start(opts) {
7679
7893
  processors: [new BatchLogRecordProcessor(new OTLPLogExporter())]
7680
7894
  });
7681
7895
  logs.setGlobalLoggerProvider(loggerProvider);
7896
+ metricReader = new PeriodicExportingMetricReader({
7897
+ exportIntervalMillis: metricExportIntervalMs(),
7898
+ // DELTA temporality: each export carries only the increment since the
7899
+ // previous one. The read side (apps/api openobserve-repository) totals a
7900
+ // metric with SUM(value) over the query window, which is correct only for
7901
+ // deltas. The OTLP exporter defaults to CUMULATIVE — it re-reports the
7902
+ // running total every export, so SUM double-counts every call that lives
7903
+ // longer than one export interval (inflating LLM/TTS/STT usage + cost).
7904
+ // Setting DELTA makes the existing SUM reads correct without touching them.
7905
+ exporter: new OTLPMetricExporter({
7906
+ temporalityPreference: AggregationTemporalityPreference.DELTA
7907
+ })
7908
+ });
7682
7909
  sdk = new NodeSDK({
7683
7910
  resource,
7684
7911
  traceExporter: new OTLPTraceExporter(),
7685
- metricReader: new PeriodicExportingMetricReader({
7686
- // DELTA temporality: each 60s export carries only the increment since the
7687
- // previous one. The read side (apps/api openobserve-repository) totals a
7688
- // metric with SUM(value) over the query window, which is correct only for
7689
- // deltas. The OTLP exporter defaults to CUMULATIVE — it re-reports the
7690
- // running total every export, so SUM double-counts every call that lives
7691
- // longer than one export interval (inflating LLM/TTS/STT usage + cost).
7692
- // Setting DELTA makes the existing SUM reads correct without touching them.
7693
- exporter: new OTLPMetricExporter({
7694
- temporalityPreference: AggregationTemporalityPreference.DELTA
7695
- })
7696
- }),
7912
+ metricReader,
7697
7913
  instrumentations: [
7698
7914
  getNodeAutoInstrumentations({
7699
7915
  // fs instrumentation is extremely noisy and rarely useful for app traces.
@@ -7712,6 +7928,19 @@ function start(opts) {
7712
7928
  process.once("SIGTERM", () => void shutdown().then(() => process.exit(0)));
7713
7929
  process.once("SIGINT", () => void shutdown().then(() => process.exit(0)));
7714
7930
  }
7931
+ async function flushTelemetry() {
7932
+ const flushes = [];
7933
+ if (metricReader)
7934
+ flushes.push(metricReader.forceFlush());
7935
+ if (loggerProvider)
7936
+ flushes.push(loggerProvider.forceFlush());
7937
+ const proxied = trace.getTracerProvider();
7938
+ const tracerProvider = proxied.getDelegate?.() ?? proxied;
7939
+ if (typeof tracerProvider.forceFlush === "function") {
7940
+ flushes.push(tracerProvider.forceFlush());
7941
+ }
7942
+ await Promise.allSettled(flushes);
7943
+ }
7715
7944
  function getLogger(name) {
7716
7945
  return logs.getLogger(name);
7717
7946
  }
@@ -7783,6 +8012,12 @@ function getCurrentCallContext() {
7783
8012
  }
7784
8013
  var METER_NAME = "voicelayer";
7785
8014
  var METER_VERSION = "0.1.0";
8015
+ var USAGE_METRIC_NAMES = {
8016
+ llmInputTokens: "vl.llm.input_tokens",
8017
+ llmOutputTokens: "vl.llm.output_tokens",
8018
+ ttsChars: "vl.tts.chars",
8019
+ sttSeconds: "vl.stt.seconds"
8020
+ };
7786
8021
  function meter() {
7787
8022
  return metrics.getMeter(METER_NAME, METER_VERSION);
7788
8023
  }
@@ -7803,25 +8038,25 @@ var _llmOutputTokens = null;
7803
8038
  var _ttsChars = null;
7804
8039
  var _sttSeconds = null;
7805
8040
  function llmInputTokens() {
7806
- return _llmInputTokens ??= meter().createCounter("vl.llm.input_tokens", {
8041
+ return _llmInputTokens ??= meter().createCounter(USAGE_METRIC_NAMES.llmInputTokens, {
7807
8042
  description: "Input tokens consumed by an LLM call",
7808
8043
  unit: "{tokens}"
7809
8044
  });
7810
8045
  }
7811
8046
  function llmOutputTokens() {
7812
- return _llmOutputTokens ??= meter().createCounter("vl.llm.output_tokens", {
8047
+ return _llmOutputTokens ??= meter().createCounter(USAGE_METRIC_NAMES.llmOutputTokens, {
7813
8048
  description: "Output tokens produced by an LLM call",
7814
8049
  unit: "{tokens}"
7815
8050
  });
7816
8051
  }
7817
8052
  function ttsChars() {
7818
- return _ttsChars ??= meter().createCounter("vl.tts.chars", {
8053
+ return _ttsChars ??= meter().createCounter(USAGE_METRIC_NAMES.ttsChars, {
7819
8054
  description: "Characters synthesized by TTS",
7820
8055
  unit: "{chars}"
7821
8056
  });
7822
8057
  }
7823
8058
  function sttSeconds() {
7824
- return _sttSeconds ??= meter().createCounter("vl.stt.seconds", {
8059
+ return _sttSeconds ??= meter().createCounter(USAGE_METRIC_NAMES.sttSeconds, {
7825
8060
  description: "Audio seconds transcribed by STT",
7826
8061
  unit: "s"
7827
8062
  });
@@ -11024,7 +11259,7 @@ async function quiesce(deps) {
11024
11259
  async function executeHandoff(args, config, deps) {
11025
11260
  const briefing = args.briefing ?? await buildBriefing(config, deps) ?? `Handoff to ${args.to}`;
11026
11261
  const mode = args.mode ?? config?.mode ?? "warm";
11027
- const sipOps = deps.sipOps ?? buildDefaultSipOps();
11262
+ const sipOps = deps.sipOps ?? buildDefaultSipOps(deps.outboundTrunkId, ownDidFor(deps.call));
11028
11263
  const notify = deps.notify ?? (() => {
11029
11264
  });
11030
11265
  if (mode === "warm" && sipOps && sipOps.canConference) {
@@ -11102,14 +11337,19 @@ async function buildBriefing(config, deps, args) {
11102
11337
  return void 0;
11103
11338
  }
11104
11339
  }
11105
- function buildDefaultSipOps() {
11340
+ function ownDidFor(call) {
11341
+ const direction = typeof call.metadata["direction"] === "string" ? call.metadata["direction"] : "inbound";
11342
+ const candidate = direction === "outbound" ? call.callerId : call.to;
11343
+ return candidate && /^\+[1-9]\d{6,14}$/.test(candidate) ? candidate : void 0;
11344
+ }
11345
+ function buildDefaultSipOps(trunkOverride, fromNumber) {
11106
11346
  const url = process.env["LIVEKIT_URL"];
11107
11347
  const key = process.env["LIVEKIT_API_KEY"];
11108
11348
  const secret = process.env["LIVEKIT_API_SECRET"];
11109
11349
  if (!url || !key || !secret) return null;
11110
11350
  const host = url.startsWith("wss://") ? `https://${url.slice("wss://".length)}` : url.startsWith("ws://") ? `http://${url.slice("ws://".length)}` : url;
11111
11351
  const client = new SipClient(host, key, secret);
11112
- const trunkId = process.env["LIVEKIT_SIP_OUTBOUND_TRUNK_ID"];
11352
+ const trunkId = trunkOverride ?? process.env["LIVEKIT_SIP_OUTBOUND_TRUNK_ID"];
11113
11353
  return {
11114
11354
  canConference: Boolean(trunkId),
11115
11355
  async refer(roomName, callerIdentity, target) {
@@ -11121,7 +11361,8 @@ function buildDefaultSipOps() {
11121
11361
  await client.createSipParticipant(trunkId, target.replace(/^tel:/, ""), roomName, {
11122
11362
  participantIdentity: identity,
11123
11363
  playDialtone: false,
11124
- waitUntilAnswered: true
11364
+ waitUntilAnswered: true,
11365
+ ...fromNumber ? { fromNumber } : {}
11125
11366
  });
11126
11367
  return { identity };
11127
11368
  }
@@ -11909,6 +12150,64 @@ var UsageAccumulator = class {
11909
12150
  };
11910
12151
  }
11911
12152
  };
12153
+ function registerTelemetryFlush(job, flush = flushTelemetry) {
12154
+ const ctx = job;
12155
+ if (typeof ctx?.addShutdownCallback !== "function") return;
12156
+ ctx.addShutdownCallback(async () => {
12157
+ try {
12158
+ await flush();
12159
+ } catch (err) {
12160
+ console.warn("[agent] telemetry flush failed", err);
12161
+ }
12162
+ });
12163
+ }
12164
+ function attachEndpointingProbe(session, events, ctx, deps) {
12165
+ const base = {
12166
+ projectId: ctx.projectId,
12167
+ callId: ctx.callId,
12168
+ ...ctx.campaignId ? { campaignId: ctx.campaignId } : {}
12169
+ };
12170
+ const now = deps.now ?? Date.now;
12171
+ let armed = false;
12172
+ let speechEndAt = null;
12173
+ let finalTranscriptAt = null;
12174
+ const resetTurn = () => {
12175
+ speechEndAt = null;
12176
+ finalTranscriptAt = null;
12177
+ };
12178
+ const tryEmit = () => {
12179
+ if (!armed || speechEndAt == null || finalTranscriptAt == null) return;
12180
+ const delayMs = finalTranscriptAt - speechEndAt;
12181
+ armed = false;
12182
+ resetTurn();
12183
+ if (!(delayMs > 0)) return;
12184
+ recordEouLatency({
12185
+ ...base,
12186
+ endOfUtteranceDelayMs: delayMs,
12187
+ transcriptionDelayMs: delayMs
12188
+ });
12189
+ recordTurnLatencySpan({ ...base, stage: "endpointing", latencyMs: delayMs });
12190
+ recordTurnLatencySpan({ ...base, stage: "stt", latencyMs: delayMs });
12191
+ };
12192
+ deps.vad.onSpeechEnd((sample) => {
12193
+ if (!armed) return;
12194
+ speechEndAt = sample.speechEndAt;
12195
+ tryEmit();
12196
+ });
12197
+ session.on(events.userStateChanged, (event) => {
12198
+ const e = event;
12199
+ if (e?.newState === "speaking") {
12200
+ armed = true;
12201
+ resetTurn();
12202
+ }
12203
+ });
12204
+ session.on(events.userInputTranscribed, (event) => {
12205
+ const e = event;
12206
+ if (!armed || !e?.isFinal) return;
12207
+ finalTranscriptAt = typeof e.createdAt === "number" ? e.createdAt : now();
12208
+ tryEmit();
12209
+ });
12210
+ }
11912
12211
  function attachMetricsBridge(session, eventName, ctx) {
11913
12212
  session.on(eventName, (event) => {
11914
12213
  try {
@@ -12024,6 +12323,51 @@ function dispatch(metrics2, ctx) {
12024
12323
  }
12025
12324
  }
12026
12325
  }
12326
+
12327
+ // src/runtime/vad-tap.ts
12328
+ function tapVad(vad, opts) {
12329
+ const listeners = /* @__PURE__ */ new Set();
12330
+ const now = opts.now ?? Date.now;
12331
+ const target = vad;
12332
+ if (!target || typeof target.stream !== "function") {
12333
+ return { onSpeechEnd: () => () => {
12334
+ } };
12335
+ }
12336
+ const originalStream = target.stream.bind(target);
12337
+ target.stream = () => {
12338
+ const stream = originalStream();
12339
+ const originalNext = stream.next.bind(stream);
12340
+ stream.next = async () => {
12341
+ const result = await originalNext();
12342
+ try {
12343
+ const event = result.value;
12344
+ if (!result.done && event && event.type === opts.endOfSpeechType) {
12345
+ const silenceMs = numberOr(event.silenceDuration, 0);
12346
+ const inferenceMs = numberOr(event.inferenceDuration, 0);
12347
+ const sample = {
12348
+ speechEndAt: now() - silenceMs - inferenceMs,
12349
+ silenceMs,
12350
+ inferenceMs
12351
+ };
12352
+ for (const listener of [...listeners]) listener(sample);
12353
+ }
12354
+ } catch (err) {
12355
+ console.warn("[agent] vad tap failed", err);
12356
+ }
12357
+ return result;
12358
+ };
12359
+ return stream;
12360
+ };
12361
+ return {
12362
+ onSpeechEnd(listener) {
12363
+ listeners.add(listener);
12364
+ return () => listeners.delete(listener);
12365
+ }
12366
+ };
12367
+ }
12368
+ function numberOr(value, fallback) {
12369
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
12370
+ }
12027
12371
  function resolveAgentModulePath(moduleUrl) {
12028
12372
  if (moduleUrl.startsWith("file://")) {
12029
12373
  return fileURLToPath(moduleUrl);
@@ -12179,6 +12523,82 @@ function actionSpecToAction(spec) {
12179
12523
  return void 0;
12180
12524
  }
12181
12525
 
12526
+ // src/worker-identity.ts
12527
+ async function exchangeWorkerToken(input) {
12528
+ const apiUrl = process.env["VOICELAYER_API_URL"];
12529
+ const internalToken = process.env["INTERNAL_SERVICE_TOKEN"];
12530
+ if (!apiUrl || !internalToken) return null;
12531
+ try {
12532
+ const res = await (input.fetchImpl ?? fetch)(
12533
+ `${apiUrl.replace(/\/+$/, "")}/v1/internal/worker-token`,
12534
+ {
12535
+ method: "POST",
12536
+ headers: {
12537
+ "content-type": "application/json",
12538
+ "x-internal-token": internalToken
12539
+ },
12540
+ body: JSON.stringify({
12541
+ projectId: input.projectId,
12542
+ ...input.callId ? { callId: input.callId } : {}
12543
+ })
12544
+ }
12545
+ );
12546
+ if (!res.ok) return null;
12547
+ const body = await res.json();
12548
+ return typeof body.token === "string" && body.token.length > 0 ? body.token : null;
12549
+ } catch {
12550
+ return null;
12551
+ }
12552
+ }
12553
+ async function fetchCallProfile(projectId, fetchImpl) {
12554
+ const apiUrl = process.env["VOICELAYER_API_URL"];
12555
+ const internalToken = process.env["INTERNAL_SERVICE_TOKEN"];
12556
+ if (!apiUrl || !internalToken) return null;
12557
+ try {
12558
+ const res = await (fetchImpl ?? fetch)(
12559
+ `${apiUrl.replace(/\/+$/, "")}/v1/internal/call-profile/${encodeURIComponent(projectId)}`,
12560
+ { headers: { "x-internal-token": internalToken } }
12561
+ );
12562
+ if (!res.ok) return null;
12563
+ const body = await res.json();
12564
+ if (!body.profile) return null;
12565
+ return {
12566
+ outboundTrunkId: typeof body.profile.outboundTrunkId === "string" ? body.profile.outboundTrunkId : null,
12567
+ strictByok: body.profile.strictByok === true
12568
+ };
12569
+ } catch {
12570
+ return null;
12571
+ }
12572
+ }
12573
+ async function resolveWorkerClient(md, fetchImpl) {
12574
+ const projectId = typeof md["projectId"] === "string" ? md["projectId"] : void 0;
12575
+ const callId = typeof md["callId"] === "string" ? md["callId"] : void 0;
12576
+ if (projectId) {
12577
+ const token = await exchangeWorkerToken({
12578
+ projectId,
12579
+ ...callId ? { callId } : {},
12580
+ ...{}
12581
+ });
12582
+ if (token) {
12583
+ try {
12584
+ return createClient({ apiKey: token });
12585
+ } catch {
12586
+ }
12587
+ } else if (process.env["INTERNAL_SERVICE_TOKEN"] && process.env["VOICELAYER_API_URL"]) {
12588
+ console.warn("[sdk.worker-identity] worker-token exchange failed; falling back to env key", {
12589
+ projectId
12590
+ });
12591
+ }
12592
+ }
12593
+ const apiKey = process.env["VOICELAYER_API_KEY"];
12594
+ if (!apiKey) return null;
12595
+ try {
12596
+ return createClient({ apiKey });
12597
+ } catch {
12598
+ return null;
12599
+ }
12600
+ }
12601
+
12182
12602
  // src/runtime/prompt-compose.ts
12183
12603
  function composeSystemPrompt(prompt, routingInstructions) {
12184
12604
  const routing = routingInstructions?.trim();
@@ -12703,26 +13123,34 @@ var Agent = class {
12703
13123
  const projectId = typeof md["projectId"] === "string" ? md["projectId"] : void 0;
12704
13124
  const agentId = typeof md["agentId"] === "string" ? md["agentId"] : void 0;
12705
13125
  const room = job.room.name ?? void 0;
12706
- return withCallContext(
12707
- {
12708
- kind: "agent.call",
12709
- source: this.config.name,
12710
- callId,
12711
- ...projectId ? { projectId } : {},
12712
- ...agentId ? { agentId } : {},
12713
- ...room ? { room } : {}
12714
- },
12715
- () => this.runCallSpan(job)
12716
- );
13126
+ registerTelemetryFlush(job);
13127
+ try {
13128
+ return await withCallContext(
13129
+ {
13130
+ kind: "agent.call",
13131
+ source: this.config.name,
13132
+ callId,
13133
+ ...projectId ? { projectId } : {},
13134
+ ...agentId ? { agentId } : {},
13135
+ ...room ? { room } : {}
13136
+ },
13137
+ () => this.runCallSpan(job)
13138
+ );
13139
+ } finally {
13140
+ await flushTelemetry().catch(() => {
13141
+ });
13142
+ }
12717
13143
  }
12718
13144
  async runCallSpan(job) {
12719
- const { AutoSubscribe, voice: voice3 } = await import('@livekit/agents');
13145
+ const { AutoSubscribe, voice: voice3, VADEventType } = await import('@livekit/agents');
12720
13146
  const Events = voice3.AgentSessionEventTypes;
12721
13147
  await job.connect(void 0, AutoSubscribe.SUBSCRIBE_ALL);
12722
13148
  const participant = await job.waitForParticipant();
12723
13149
  const callInfo = buildCallInfo(job, participant);
12724
- const sdkClient = tryCreateClient();
12725
13150
  const dispatchMdEarly = parseJobMetadata(job.job?.metadata);
13151
+ const sdkClient = await resolveWorkerClient(dispatchMdEarly);
13152
+ const earlyProjectId = stringValue(dispatchMdEarly["projectId"]);
13153
+ const workerCallProfile = earlyProjectId ? await fetchCallProfile(earlyProjectId) : null;
12726
13154
  let effectiveConfig = this.config;
12727
13155
  let graphMode = false;
12728
13156
  let graphProgram;
@@ -12881,6 +13309,7 @@ var Agent = class {
12881
13309
  callInfo,
12882
13310
  job.proc.userData.vad
12883
13311
  );
13312
+ const vadTap = tapVad(pipeline.vad, { endOfSpeechType: VADEventType.END_OF_SPEECH });
12884
13313
  const usingRealtime = pipeline.realtime != null;
12885
13314
  let runtimeLlm = usingRealtime ? pipeline.realtime : pipeline.llm;
12886
13315
  if (this.config.puppetMode) {
@@ -13174,7 +13603,8 @@ ${callIntent}`
13174
13603
  },
13175
13604
  getProcessData: () => processRt.getData(),
13176
13605
  transcript,
13177
- notify: (kind, payload) => emitOpEvent(kind, payload)
13606
+ notify: (kind, payload) => emitOpEvent(kind, payload),
13607
+ ...workerCallProfile?.outboundTrunkId ? { outboundTrunkId: workerCallProfile.outboundTrunkId } : {}
13178
13608
  });
13179
13609
  const triggersRt = createTriggersRuntime(effectiveConfig.triggers, handoffRt);
13180
13610
  const audioExperience = new AudioExperiencePrimitive();
@@ -13518,12 +13948,26 @@ ${callIntent}`
13518
13948
  console.error("[agent] session error", e.error);
13519
13949
  if (e.fatalTransport) endCallController.noteEngineClosed();
13520
13950
  });
13521
- attachMetricsBridge(session, Events.MetricsCollected, {
13951
+ const metricsCtx = {
13522
13952
  projectId: callProjectId,
13523
13953
  callId: callSync.callId,
13524
- ...callCampaignId ? { campaignId: callCampaignId } : {},
13954
+ ...callCampaignId ? { campaignId: callCampaignId } : {}
13955
+ };
13956
+ attachMetricsBridge(session, Events.MetricsCollected, {
13957
+ ...metricsCtx,
13525
13958
  usage: usageAccumulator
13526
13959
  });
13960
+ if (graphMode) {
13961
+ attachEndpointingProbe(
13962
+ session,
13963
+ {
13964
+ userStateChanged: Events.UserStateChanged,
13965
+ userInputTranscribed: Events.UserInputTranscribed
13966
+ },
13967
+ metricsCtx,
13968
+ { vad: vadTap }
13969
+ );
13970
+ }
13527
13971
  adapter.on("state", (e) => {
13528
13972
  console.log("[agent] state", {
13529
13973
  from: e.rawOldState,
@@ -13926,15 +14370,6 @@ function builtinToolCapabilities() {
13926
14370
  }
13927
14371
  return [...caps];
13928
14372
  }
13929
- function tryCreateClient() {
13930
- const apiKey = process.env["VOICELAYER_API_KEY"];
13931
- if (!apiKey) return null;
13932
- try {
13933
- return createClient({ apiKey });
13934
- } catch {
13935
- return null;
13936
- }
13937
- }
13938
14373
  function buildFormStreamWatcher(processRt) {
13939
14374
  const apiBaseUrl = process.env["VOICELAYER_API_URL"];
13940
14375
  if (!apiBaseUrl) {
@@ -1627,6 +1627,212 @@ var init_connection = __esm({
1627
1627
  });
1628
1628
  }
1629
1629
  });
1630
+ var E164, RoomPrefix, RecordingPrefix, Cidr, TelephonySettings, ProfileSource, EffectiveCallProfile;
1631
+ var init_telephony = __esm({
1632
+ "../contracts/src/telephony.ts"() {
1633
+ E164 = z.string().regex(/^\+[1-9]\d{6,14}$/, "must be E.164 (+15551234567)");
1634
+ RoomPrefix = z.string().regex(/^[a-z0-9][a-z0-9-]{0,30}[a-z0-9-]?$/, "lowercase letters, digits, dashes; max 32");
1635
+ RecordingPrefix = z.string().min(1).max(128).refine((v) => !v.startsWith("/") && !v.includes(".."), "relative key prefix only");
1636
+ Cidr = z.string().regex(/^\d{1,3}(\.\d{1,3}){3}\/\d{1,2}$/, "must be IPv4 CIDR (a.b.c.d/nn)");
1637
+ TelephonySettings = z.object({
1638
+ outboundTrunkId: z.string().min(1).max(128).nullable().default(null),
1639
+ defaultFromE164: E164.nullable().default(null),
1640
+ roomPrefix: RoomPrefix.nullable().default(null),
1641
+ sipAllowedCidrs: z.array(Cidr).max(32).nullable().default(null),
1642
+ recordingPrefix: RecordingPrefix.nullable().default(null),
1643
+ strictByok: z.boolean().default(false)
1644
+ });
1645
+ z.object({
1646
+ outboundTrunkId: z.string().min(1).max(128).nullable().optional(),
1647
+ defaultFromE164: E164.nullable().optional(),
1648
+ roomPrefix: RoomPrefix.nullable().optional(),
1649
+ sipAllowedCidrs: z.array(Cidr).max(32).nullable().optional(),
1650
+ recordingPrefix: RecordingPrefix.nullable().optional(),
1651
+ strictByok: z.boolean().optional()
1652
+ }).refine((v) => Object.keys(v).length > 0, { message: "nothing to update" });
1653
+ ProfileSource = z.enum(["org", "platform", "none"]);
1654
+ EffectiveCallProfile = z.object({
1655
+ projectId: z.string(),
1656
+ outboundTrunkId: z.string().nullable(),
1657
+ defaultFromE164: z.string().nullable(),
1658
+ roomPrefix: z.string(),
1659
+ sipAllowedCidrs: z.array(z.string()),
1660
+ recordingPrefix: z.string(),
1661
+ strictByok: z.boolean(),
1662
+ sources: z.object({
1663
+ outboundTrunkId: ProfileSource,
1664
+ defaultFromE164: ProfileSource,
1665
+ roomPrefix: ProfileSource,
1666
+ sipAllowedCidrs: ProfileSource,
1667
+ recordingPrefix: ProfileSource
1668
+ })
1669
+ });
1670
+ z.object({
1671
+ settings: TelephonySettings,
1672
+ profile: EffectiveCallProfile
1673
+ });
1674
+ }
1675
+ });
1676
+ var SHORTENER_HOSTS, PLACEHOLDER_HOSTS, PublicUrl, A2pBusinessType, A2pBusinessInfo, A2pOptInType, A2pUseCase, A2pMessagingProfile, A2pState;
1677
+ var init_a2p = __esm({
1678
+ "../contracts/src/a2p.ts"() {
1679
+ SHORTENER_HOSTS = [
1680
+ "bit.ly",
1681
+ "tinyurl.com",
1682
+ "t.co",
1683
+ "goo.gl",
1684
+ "ow.ly",
1685
+ "is.gd",
1686
+ "buff.ly",
1687
+ "rebrand.ly",
1688
+ "cutt.ly",
1689
+ "shorturl.at",
1690
+ "tiny.cc"
1691
+ ];
1692
+ PLACEHOLDER_HOSTS = ["acme.com", "example.com", "example.org", "test.com", "localhost"];
1693
+ PublicUrl = z.string().url().refine((v) => v.startsWith("https://"), "must be https").refine((v) => {
1694
+ try {
1695
+ const host = new URL(v).hostname.toLowerCase().replace(/^www\./, "");
1696
+ return !SHORTENER_HOSTS.includes(host);
1697
+ } catch {
1698
+ return false;
1699
+ }
1700
+ }, "public URL shorteners are rejected by carriers \u2014 use your own domain").refine((v) => {
1701
+ try {
1702
+ const host = new URL(v).hostname.toLowerCase().replace(/^www\./, "");
1703
+ return !PLACEHOLDER_HOSTS.some((p) => host === p || host.endsWith(`.${p}`));
1704
+ } catch {
1705
+ return false;
1706
+ }
1707
+ }, "placeholder domain \u2014 reviewers will follow this link and reject the campaign");
1708
+ A2pBusinessType = z.enum([
1709
+ "Sole Proprietorship",
1710
+ "Partnership",
1711
+ "Corporation",
1712
+ "Co-operative",
1713
+ "Limited Liability Corporation",
1714
+ "Non-profit Corporation"
1715
+ ]);
1716
+ A2pBusinessInfo = z.object({
1717
+ legalName: z.string().trim().min(2).max(200),
1718
+ /** EIN (US) or equivalent registration number. */
1719
+ registrationNumber: z.string().trim().min(4).max(50),
1720
+ businessType: A2pBusinessType,
1721
+ /** Publicly reachable production site — not staging, not a 404. */
1722
+ website: PublicUrl,
1723
+ industry: z.string().trim().min(2).max(60),
1724
+ address: z.object({
1725
+ street: z.string().trim().min(2).max(200),
1726
+ city: z.string().trim().min(1).max(100),
1727
+ region: z.string().trim().min(1).max(100),
1728
+ postalCode: z.string().trim().min(2).max(20),
1729
+ isoCountry: z.string().trim().length(2)
1730
+ }),
1731
+ authorizedRep: z.object({
1732
+ firstName: z.string().trim().min(1).max(100),
1733
+ lastName: z.string().trim().min(1).max(100),
1734
+ email: z.string().trim().email(),
1735
+ phone: z.string().trim().regex(/^\+[1-9]\d{6,14}$/, "must be E.164"),
1736
+ jobTitle: z.string().trim().min(2).max(100)
1737
+ })
1738
+ });
1739
+ A2pOptInType = z.enum(["WEB_FORM", "PAPER_FORM", "VERBAL", "VIA_TEXT", "MOBILE_QR_CODE"]);
1740
+ A2pUseCase = z.enum([
1741
+ "MIXED",
1742
+ "CUSTOMER_CARE",
1743
+ "MARKETING",
1744
+ "ACCOUNT_NOTIFICATION",
1745
+ "2FA",
1746
+ "DELIVERY_NOTIFICATION",
1747
+ "HIGHER_EDUCATION",
1748
+ "POLLING_VOTING",
1749
+ "PUBLIC_SERVICE_ANNOUNCEMENT",
1750
+ "LOW_VOLUME"
1751
+ ]);
1752
+ A2pMessagingProfile = z.object({
1753
+ useCase: A2pUseCase,
1754
+ /** Specific, not generic. "We send texts" gets rejected; describe the actual
1755
+ * messages and when they are sent. */
1756
+ description: z.string().trim().min(40).max(4096),
1757
+ /**
1758
+ * The single most-rejected field. Must describe HOW people opt in, state the
1759
+ * message frequency, include the "message and data rates may apply"
1760
+ * disclosure, and link to publicly reachable evidence. Twilio's API bounds it
1761
+ * to 40–2049 characters.
1762
+ */
1763
+ messageFlow: z.string().trim().min(40).max(2049),
1764
+ optInType: A2pOptInType,
1765
+ /** Publicly accessible screenshots/pages showing the opt-in. Reviewers open
1766
+ * these; anything behind a login fails. */
1767
+ optInEvidenceUrls: z.array(PublicUrl).min(1).max(5),
1768
+ /** Real messages the customer will send. Must reflect the declared use case
1769
+ * and carry opt-out language. */
1770
+ messageSamples: z.array(z.string().trim().min(10).max(1024)).min(2).max(5),
1771
+ privacyPolicyUrl: PublicUrl,
1772
+ termsAndConditionsUrl: PublicUrl,
1773
+ hasEmbeddedLinks: z.boolean().default(false),
1774
+ hasEmbeddedPhone: z.boolean().default(false)
1775
+ });
1776
+ z.object({
1777
+ business: A2pBusinessInfo,
1778
+ messaging: A2pMessagingProfile,
1779
+ /** Register against Twilio's mock endpoints — exercises the full pipeline
1780
+ * with no fees and no real carrier submission. Used in CI and staging. */
1781
+ mock: z.boolean().default(false)
1782
+ }).superRefine((v, ctx) => {
1783
+ const flow = v.messaging.messageFlow.toLowerCase();
1784
+ if (!/(msg|message)\s*(&|and)\s*data rates/.test(flow)) {
1785
+ ctx.addIssue({
1786
+ code: z.ZodIssueCode.custom,
1787
+ path: ["messaging", "messageFlow"],
1788
+ message: 'must include a "Message and data rates may apply" disclosure \u2014 carriers reject without it'
1789
+ });
1790
+ }
1791
+ if (!/\d/.test(flow) || !/(msg|message|text)/.test(flow)) {
1792
+ ctx.addIssue({
1793
+ code: z.ZodIssueCode.custom,
1794
+ path: ["messaging", "messageFlow"],
1795
+ message: 'must state message frequency, e.g. "Up to 4 msgs/month"'
1796
+ });
1797
+ }
1798
+ const hasOptOut = v.messaging.messageSamples.some((s) => /stop/i.test(s));
1799
+ if (!hasOptOut) {
1800
+ ctx.addIssue({
1801
+ code: z.ZodIssueCode.custom,
1802
+ path: ["messaging", "messageSamples"],
1803
+ message: 'at least one sample must include opt-out language (e.g. "Reply STOP to opt out")'
1804
+ });
1805
+ }
1806
+ });
1807
+ A2pState = z.enum([
1808
+ "none",
1809
+ "profile_pending",
1810
+ "profile_approved",
1811
+ "profile_failed",
1812
+ "brand_pending",
1813
+ "brand_approved",
1814
+ "brand_failed",
1815
+ "campaign_pending",
1816
+ "messaging_ready",
1817
+ "campaign_failed"
1818
+ ]);
1819
+ z.object({
1820
+ state: A2pState,
1821
+ customerProfileSid: z.string().nullable(),
1822
+ trustProductSid: z.string().nullable(),
1823
+ brandSid: z.string().nullable(),
1824
+ messagingServiceSid: z.string().nullable(),
1825
+ campaignSid: z.string().nullable(),
1826
+ mock: z.boolean(),
1827
+ /** Carrier/Twilio rejection details, surfaced verbatim so the customer can
1828
+ * fix the specific field rather than guess. */
1829
+ failures: z.array(z.object({ code: z.number().nullable(), field: z.string().nullable(), message: z.string() })).default([]),
1830
+ /** Plain-language next step for the dashboard. */
1831
+ nextAction: z.string().nullable(),
1832
+ updatedAt: z.string().nullable()
1833
+ });
1834
+ }
1835
+ });
1630
1836
  var ConnectorMode, ConnectorStatus, ConnectorNormalize;
1631
1837
  var init_connector = __esm({
1632
1838
  "../contracts/src/connector.ts"() {
@@ -1931,6 +2137,8 @@ var init_src = __esm({
1931
2137
  init_call_recording();
1932
2138
  init_outbound();
1933
2139
  init_connection();
2140
+ init_telephony();
2141
+ init_a2p();
1934
2142
  init_connector();
1935
2143
  init_connector_stream();
1936
2144
  init_flow_compile();
package/package.json CHANGED
@@ -1,17 +1,12 @@
1
1
  {
2
2
  "name": "@voicelayer/sdk",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "description": "Official SDK for VoiceLayer — define, run, and orchestrate voice AI agents on the VoiceLayer control plane.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "VoiceLayer",
7
- "homepage": "https://github.com/paradox1612/voicelayer/tree/main/packages/sdk#readme",
8
- "repository": {
9
- "type": "git",
10
- "url": "git+https://github.com/paradox1612/voicelayer.git",
11
- "directory": "packages/sdk"
12
- },
7
+ "homepage": "https://vlayers.ai/docs/sdk",
13
8
  "bugs": {
14
- "url": "https://github.com/paradox1612/voicelayer/issues"
9
+ "email": "customer@vlayers.ai"
15
10
  },
16
11
  "keywords": [
17
12
  "voicelayer",
@@ -81,14 +76,14 @@
81
76
  "vitest": "^4.1.5",
82
77
  "@voicelayer/contracts": "0.1.0",
83
78
  "@voicelayer/observability": "0.1.0",
84
- "@voicelayer/primitive-audio-experience": "0.1.0",
85
79
  "@voicelayer/primitive-audio-routing": "0.1.0",
86
- "@voicelayer/primitive-security": "0.1.0",
87
80
  "@voicelayer/primitive-process-schema": "0.1.0",
88
- "@voicelayer/agents-plugin-deepgram": "1.0.0",
89
- "@voicelayer/agents-plugin-livekit": "1.0.0",
90
- "@voicelayer/agents-plugin-openai": "1.0.0",
91
- "@voicelayer/agents-plugin-silero": "1.0.0"
81
+ "@voicelayer/agents-plugin-livekit": "1.0.1",
82
+ "@voicelayer/primitive-security": "0.1.0",
83
+ "@voicelayer/agents-plugin-openai": "1.0.1",
84
+ "@voicelayer/agents-plugin-silero": "1.0.1",
85
+ "@voicelayer/primitive-audio-experience": "0.1.0",
86
+ "@voicelayer/agents-plugin-deepgram": "1.0.1"
92
87
  },
93
88
  "scripts": {
94
89
  "typecheck": "tsc -b --pretty false",