@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 +58 -37
- package/dist/index.js +483 -48
- package/dist/runtime/text-session.js +208 -0
- package/package.json +9 -14
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
|
-
|
|
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.
|
|
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 —
|
|
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.
|
|
148
|
+
if (field === 'claimNumber') await ctx.connectors.crm.lookup(value);
|
|
135
149
|
},
|
|
136
150
|
async onCallEnd(outcome, ctx) {
|
|
137
|
-
await
|
|
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.
|
|
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](
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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
|
-
##
|
|
298
|
+
## Running locally
|
|
270
299
|
|
|
271
|
-
```
|
|
272
|
-
import
|
|
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
|
-
|
|
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
|
-
-
|
|
333
|
-
-
|
|
334
|
-
-
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
12707
|
-
|
|
12708
|
-
|
|
12709
|
-
|
|
12710
|
-
|
|
12711
|
-
|
|
12712
|
-
|
|
12713
|
-
|
|
12714
|
-
|
|
12715
|
-
|
|
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
|
-
|
|
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.
|
|
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://
|
|
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
|
-
"
|
|
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-
|
|
89
|
-
"@voicelayer/
|
|
90
|
-
"@voicelayer/agents-plugin-openai": "1.0.
|
|
91
|
-
"@voicelayer/agents-plugin-silero": "1.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",
|