@voicelayer/sdk 0.1.9 → 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 +498 -50
- package/dist/runtime/text-session.js +223 -2
- 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();
|
|
@@ -3251,11 +3459,12 @@ ${collected}`);
|
|
|
3251
3459
|
if (ok && cfg.outputAs && typeof cfg.outputAs === "object") {
|
|
3252
3460
|
for (const [slot, path] of Object.entries(cfg.outputAs)) {
|
|
3253
3461
|
if (typeof path !== "string" || !path.trim()) continue;
|
|
3254
|
-
const
|
|
3255
|
-
if (
|
|
3462
|
+
const raw = resolvePath(result, path.trim());
|
|
3463
|
+
if (raw === void 0 || raw === null) {
|
|
3256
3464
|
deps.events.note("tool.output_missing", { node: node.id, slot, path });
|
|
3257
3465
|
continue;
|
|
3258
3466
|
}
|
|
3467
|
+
const value = typeof raw === "string" ? raw : typeof raw === "number" || typeof raw === "boolean" || typeof raw === "bigint" ? String(raw) : JSON.stringify(raw);
|
|
3259
3468
|
const captured = deps.processRt.captureField(slot, value);
|
|
3260
3469
|
deps.events.note(captured ? "tool.output_captured" : "tool.output_rejected", {
|
|
3261
3470
|
node: node.id,
|
|
@@ -7513,6 +7722,18 @@ function buildTextContext(adapter, call, processRt) {
|
|
|
7513
7722
|
get process() {
|
|
7514
7723
|
return { data: processRt.getData(), complete: processRt.isComplete() };
|
|
7515
7724
|
},
|
|
7725
|
+
// Voice-only actions, stubbed so a flow containing them can still be dry-run
|
|
7726
|
+
// over text. Without these the handoff node threw "deps.ctx.handoff is not
|
|
7727
|
+
// a function" mid-trace (seen in the builder's dry run); the graph now
|
|
7728
|
+
// reaches its handoff outcome cleanly and the trace says why nothing dialed.
|
|
7729
|
+
handoff: async (to) => {
|
|
7730
|
+
processRt.recordEvent({
|
|
7731
|
+
kind: "handoff.simulated",
|
|
7732
|
+
data: { to, reason: "text dry run \u2014 no telephony leg to transfer" }
|
|
7733
|
+
});
|
|
7734
|
+
},
|
|
7735
|
+
endCall: async () => {
|
|
7736
|
+
},
|
|
7516
7737
|
// Voice-only escape hatch is absent on text; left undefined.
|
|
7517
7738
|
livekit: { session: {}, job: {} }
|
|
7518
7739
|
};
|
|
@@ -7592,12 +7813,12 @@ function definitionFor(opts) {
|
|
|
7592
7813
|
}
|
|
7593
7814
|
async function runTextTranscript(opts) {
|
|
7594
7815
|
const replies = [];
|
|
7595
|
-
const
|
|
7816
|
+
const trace8 = [];
|
|
7596
7817
|
const processRt = opts.processRt ?? createProcessRuntime(definitionFor(opts));
|
|
7597
7818
|
const session = runTextSession({
|
|
7598
7819
|
program: opts.program,
|
|
7599
7820
|
onAgentText: (t) => void replies.push(t),
|
|
7600
|
-
onTrace: (e) => void
|
|
7821
|
+
onTrace: (e) => void trace8.push(e),
|
|
7601
7822
|
...processRt ? { processRt } : {},
|
|
7602
7823
|
...opts.generate ? { generate: opts.generate } : {},
|
|
7603
7824
|
...opts.call ? { call: opts.call } : {},
|
|
@@ -7606,7 +7827,7 @@ async function runTextTranscript(opts) {
|
|
|
7606
7827
|
for (const m of opts.messages) session.sendUserMessage(m);
|
|
7607
7828
|
session.end();
|
|
7608
7829
|
const outcome = await session.result;
|
|
7609
|
-
return { replies, outcome, trace:
|
|
7830
|
+
return { replies, outcome, trace: trace8 };
|
|
7610
7831
|
}
|
|
7611
7832
|
function ensureLkLogger() {
|
|
7612
7833
|
if (loggerOptions() === void 0) {
|
|
@@ -7639,9 +7860,15 @@ async function runAgentTextTranscript(opts) {
|
|
|
7639
7860
|
return { replies };
|
|
7640
7861
|
}
|
|
7641
7862
|
var DEFAULT_OTLP_ENDPOINT = "https://otel.vlayers.ai/v1/otlp";
|
|
7863
|
+
var DEFAULT_METRIC_EXPORT_INTERVAL_MS = 15e3;
|
|
7642
7864
|
var started = false;
|
|
7643
7865
|
var sdk = null;
|
|
7644
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
|
+
}
|
|
7645
7872
|
function start(opts) {
|
|
7646
7873
|
if (started)
|
|
7647
7874
|
return;
|
|
@@ -7666,21 +7893,23 @@ function start(opts) {
|
|
|
7666
7893
|
processors: [new BatchLogRecordProcessor(new OTLPLogExporter())]
|
|
7667
7894
|
});
|
|
7668
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
|
+
});
|
|
7669
7909
|
sdk = new NodeSDK({
|
|
7670
7910
|
resource,
|
|
7671
7911
|
traceExporter: new OTLPTraceExporter(),
|
|
7672
|
-
metricReader
|
|
7673
|
-
// DELTA temporality: each 60s export carries only the increment since the
|
|
7674
|
-
// previous one. The read side (apps/api openobserve-repository) totals a
|
|
7675
|
-
// metric with SUM(value) over the query window, which is correct only for
|
|
7676
|
-
// deltas. The OTLP exporter defaults to CUMULATIVE — it re-reports the
|
|
7677
|
-
// running total every export, so SUM double-counts every call that lives
|
|
7678
|
-
// longer than one export interval (inflating LLM/TTS/STT usage + cost).
|
|
7679
|
-
// Setting DELTA makes the existing SUM reads correct without touching them.
|
|
7680
|
-
exporter: new OTLPMetricExporter({
|
|
7681
|
-
temporalityPreference: AggregationTemporalityPreference.DELTA
|
|
7682
|
-
})
|
|
7683
|
-
}),
|
|
7912
|
+
metricReader,
|
|
7684
7913
|
instrumentations: [
|
|
7685
7914
|
getNodeAutoInstrumentations({
|
|
7686
7915
|
// fs instrumentation is extremely noisy and rarely useful for app traces.
|
|
@@ -7699,6 +7928,19 @@ function start(opts) {
|
|
|
7699
7928
|
process.once("SIGTERM", () => void shutdown().then(() => process.exit(0)));
|
|
7700
7929
|
process.once("SIGINT", () => void shutdown().then(() => process.exit(0)));
|
|
7701
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
|
+
}
|
|
7702
7944
|
function getLogger(name) {
|
|
7703
7945
|
return logs.getLogger(name);
|
|
7704
7946
|
}
|
|
@@ -7770,6 +8012,12 @@ function getCurrentCallContext() {
|
|
|
7770
8012
|
}
|
|
7771
8013
|
var METER_NAME = "voicelayer";
|
|
7772
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
|
+
};
|
|
7773
8021
|
function meter() {
|
|
7774
8022
|
return metrics.getMeter(METER_NAME, METER_VERSION);
|
|
7775
8023
|
}
|
|
@@ -7790,25 +8038,25 @@ var _llmOutputTokens = null;
|
|
|
7790
8038
|
var _ttsChars = null;
|
|
7791
8039
|
var _sttSeconds = null;
|
|
7792
8040
|
function llmInputTokens() {
|
|
7793
|
-
return _llmInputTokens ??= meter().createCounter(
|
|
8041
|
+
return _llmInputTokens ??= meter().createCounter(USAGE_METRIC_NAMES.llmInputTokens, {
|
|
7794
8042
|
description: "Input tokens consumed by an LLM call",
|
|
7795
8043
|
unit: "{tokens}"
|
|
7796
8044
|
});
|
|
7797
8045
|
}
|
|
7798
8046
|
function llmOutputTokens() {
|
|
7799
|
-
return _llmOutputTokens ??= meter().createCounter(
|
|
8047
|
+
return _llmOutputTokens ??= meter().createCounter(USAGE_METRIC_NAMES.llmOutputTokens, {
|
|
7800
8048
|
description: "Output tokens produced by an LLM call",
|
|
7801
8049
|
unit: "{tokens}"
|
|
7802
8050
|
});
|
|
7803
8051
|
}
|
|
7804
8052
|
function ttsChars() {
|
|
7805
|
-
return _ttsChars ??= meter().createCounter(
|
|
8053
|
+
return _ttsChars ??= meter().createCounter(USAGE_METRIC_NAMES.ttsChars, {
|
|
7806
8054
|
description: "Characters synthesized by TTS",
|
|
7807
8055
|
unit: "{chars}"
|
|
7808
8056
|
});
|
|
7809
8057
|
}
|
|
7810
8058
|
function sttSeconds() {
|
|
7811
|
-
return _sttSeconds ??= meter().createCounter(
|
|
8059
|
+
return _sttSeconds ??= meter().createCounter(USAGE_METRIC_NAMES.sttSeconds, {
|
|
7812
8060
|
description: "Audio seconds transcribed by STT",
|
|
7813
8061
|
unit: "s"
|
|
7814
8062
|
});
|
|
@@ -11011,7 +11259,7 @@ async function quiesce(deps) {
|
|
|
11011
11259
|
async function executeHandoff(args, config, deps) {
|
|
11012
11260
|
const briefing = args.briefing ?? await buildBriefing(config, deps) ?? `Handoff to ${args.to}`;
|
|
11013
11261
|
const mode = args.mode ?? config?.mode ?? "warm";
|
|
11014
|
-
const sipOps = deps.sipOps ?? buildDefaultSipOps();
|
|
11262
|
+
const sipOps = deps.sipOps ?? buildDefaultSipOps(deps.outboundTrunkId, ownDidFor(deps.call));
|
|
11015
11263
|
const notify = deps.notify ?? (() => {
|
|
11016
11264
|
});
|
|
11017
11265
|
if (mode === "warm" && sipOps && sipOps.canConference) {
|
|
@@ -11089,14 +11337,19 @@ async function buildBriefing(config, deps, args) {
|
|
|
11089
11337
|
return void 0;
|
|
11090
11338
|
}
|
|
11091
11339
|
}
|
|
11092
|
-
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) {
|
|
11093
11346
|
const url = process.env["LIVEKIT_URL"];
|
|
11094
11347
|
const key = process.env["LIVEKIT_API_KEY"];
|
|
11095
11348
|
const secret = process.env["LIVEKIT_API_SECRET"];
|
|
11096
11349
|
if (!url || !key || !secret) return null;
|
|
11097
11350
|
const host = url.startsWith("wss://") ? `https://${url.slice("wss://".length)}` : url.startsWith("ws://") ? `http://${url.slice("ws://".length)}` : url;
|
|
11098
11351
|
const client = new SipClient(host, key, secret);
|
|
11099
|
-
const trunkId = process.env["LIVEKIT_SIP_OUTBOUND_TRUNK_ID"];
|
|
11352
|
+
const trunkId = trunkOverride ?? process.env["LIVEKIT_SIP_OUTBOUND_TRUNK_ID"];
|
|
11100
11353
|
return {
|
|
11101
11354
|
canConference: Boolean(trunkId),
|
|
11102
11355
|
async refer(roomName, callerIdentity, target) {
|
|
@@ -11108,7 +11361,8 @@ function buildDefaultSipOps() {
|
|
|
11108
11361
|
await client.createSipParticipant(trunkId, target.replace(/^tel:/, ""), roomName, {
|
|
11109
11362
|
participantIdentity: identity,
|
|
11110
11363
|
playDialtone: false,
|
|
11111
|
-
waitUntilAnswered: true
|
|
11364
|
+
waitUntilAnswered: true,
|
|
11365
|
+
...fromNumber ? { fromNumber } : {}
|
|
11112
11366
|
});
|
|
11113
11367
|
return { identity };
|
|
11114
11368
|
}
|
|
@@ -11896,6 +12150,64 @@ var UsageAccumulator = class {
|
|
|
11896
12150
|
};
|
|
11897
12151
|
}
|
|
11898
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
|
+
}
|
|
11899
12211
|
function attachMetricsBridge(session, eventName, ctx) {
|
|
11900
12212
|
session.on(eventName, (event) => {
|
|
11901
12213
|
try {
|
|
@@ -12011,6 +12323,51 @@ function dispatch(metrics2, ctx) {
|
|
|
12011
12323
|
}
|
|
12012
12324
|
}
|
|
12013
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
|
+
}
|
|
12014
12371
|
function resolveAgentModulePath(moduleUrl) {
|
|
12015
12372
|
if (moduleUrl.startsWith("file://")) {
|
|
12016
12373
|
return fileURLToPath(moduleUrl);
|
|
@@ -12166,6 +12523,82 @@ function actionSpecToAction(spec) {
|
|
|
12166
12523
|
return void 0;
|
|
12167
12524
|
}
|
|
12168
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
|
+
|
|
12169
12602
|
// src/runtime/prompt-compose.ts
|
|
12170
12603
|
function composeSystemPrompt(prompt, routingInstructions) {
|
|
12171
12604
|
const routing = routingInstructions?.trim();
|
|
@@ -12690,26 +13123,34 @@ var Agent = class {
|
|
|
12690
13123
|
const projectId = typeof md["projectId"] === "string" ? md["projectId"] : void 0;
|
|
12691
13124
|
const agentId = typeof md["agentId"] === "string" ? md["agentId"] : void 0;
|
|
12692
13125
|
const room = job.room.name ?? void 0;
|
|
12693
|
-
|
|
12694
|
-
|
|
12695
|
-
|
|
12696
|
-
|
|
12697
|
-
|
|
12698
|
-
|
|
12699
|
-
|
|
12700
|
-
|
|
12701
|
-
|
|
12702
|
-
|
|
12703
|
-
|
|
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
|
+
}
|
|
12704
13143
|
}
|
|
12705
13144
|
async runCallSpan(job) {
|
|
12706
|
-
const { AutoSubscribe, voice: voice3 } = await import('@livekit/agents');
|
|
13145
|
+
const { AutoSubscribe, voice: voice3, VADEventType } = await import('@livekit/agents');
|
|
12707
13146
|
const Events = voice3.AgentSessionEventTypes;
|
|
12708
13147
|
await job.connect(void 0, AutoSubscribe.SUBSCRIBE_ALL);
|
|
12709
13148
|
const participant = await job.waitForParticipant();
|
|
12710
13149
|
const callInfo = buildCallInfo(job, participant);
|
|
12711
|
-
const sdkClient = tryCreateClient();
|
|
12712
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;
|
|
12713
13154
|
let effectiveConfig = this.config;
|
|
12714
13155
|
let graphMode = false;
|
|
12715
13156
|
let graphProgram;
|
|
@@ -12868,6 +13309,7 @@ var Agent = class {
|
|
|
12868
13309
|
callInfo,
|
|
12869
13310
|
job.proc.userData.vad
|
|
12870
13311
|
);
|
|
13312
|
+
const vadTap = tapVad(pipeline.vad, { endOfSpeechType: VADEventType.END_OF_SPEECH });
|
|
12871
13313
|
const usingRealtime = pipeline.realtime != null;
|
|
12872
13314
|
let runtimeLlm = usingRealtime ? pipeline.realtime : pipeline.llm;
|
|
12873
13315
|
if (this.config.puppetMode) {
|
|
@@ -13161,7 +13603,8 @@ ${callIntent}`
|
|
|
13161
13603
|
},
|
|
13162
13604
|
getProcessData: () => processRt.getData(),
|
|
13163
13605
|
transcript,
|
|
13164
|
-
notify: (kind, payload) => emitOpEvent(kind, payload)
|
|
13606
|
+
notify: (kind, payload) => emitOpEvent(kind, payload),
|
|
13607
|
+
...workerCallProfile?.outboundTrunkId ? { outboundTrunkId: workerCallProfile.outboundTrunkId } : {}
|
|
13165
13608
|
});
|
|
13166
13609
|
const triggersRt = createTriggersRuntime(effectiveConfig.triggers, handoffRt);
|
|
13167
13610
|
const audioExperience = new AudioExperiencePrimitive();
|
|
@@ -13505,12 +13948,26 @@ ${callIntent}`
|
|
|
13505
13948
|
console.error("[agent] session error", e.error);
|
|
13506
13949
|
if (e.fatalTransport) endCallController.noteEngineClosed();
|
|
13507
13950
|
});
|
|
13508
|
-
|
|
13951
|
+
const metricsCtx = {
|
|
13509
13952
|
projectId: callProjectId,
|
|
13510
13953
|
callId: callSync.callId,
|
|
13511
|
-
...callCampaignId ? { campaignId: callCampaignId } : {}
|
|
13954
|
+
...callCampaignId ? { campaignId: callCampaignId } : {}
|
|
13955
|
+
};
|
|
13956
|
+
attachMetricsBridge(session, Events.MetricsCollected, {
|
|
13957
|
+
...metricsCtx,
|
|
13512
13958
|
usage: usageAccumulator
|
|
13513
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
|
+
}
|
|
13514
13971
|
adapter.on("state", (e) => {
|
|
13515
13972
|
console.log("[agent] state", {
|
|
13516
13973
|
from: e.rawOldState,
|
|
@@ -13913,15 +14370,6 @@ function builtinToolCapabilities() {
|
|
|
13913
14370
|
}
|
|
13914
14371
|
return [...caps];
|
|
13915
14372
|
}
|
|
13916
|
-
function tryCreateClient() {
|
|
13917
|
-
const apiKey = process.env["VOICELAYER_API_KEY"];
|
|
13918
|
-
if (!apiKey) return null;
|
|
13919
|
-
try {
|
|
13920
|
-
return createClient({ apiKey });
|
|
13921
|
-
} catch {
|
|
13922
|
-
return null;
|
|
13923
|
-
}
|
|
13924
|
-
}
|
|
13925
14373
|
function buildFormStreamWatcher(processRt) {
|
|
13926
14374
|
const apiBaseUrl = process.env["VOICELAYER_API_URL"];
|
|
13927
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();
|
|
@@ -2782,11 +2990,12 @@ ${collected}`);
|
|
|
2782
2990
|
if (ok && cfg.outputAs && typeof cfg.outputAs === "object") {
|
|
2783
2991
|
for (const [slot, path] of Object.entries(cfg.outputAs)) {
|
|
2784
2992
|
if (typeof path !== "string" || !path.trim()) continue;
|
|
2785
|
-
const
|
|
2786
|
-
if (
|
|
2993
|
+
const raw = resolvePath(result, path.trim());
|
|
2994
|
+
if (raw === void 0 || raw === null) {
|
|
2787
2995
|
deps.events.note("tool.output_missing", { node: node.id, slot, path });
|
|
2788
2996
|
continue;
|
|
2789
2997
|
}
|
|
2998
|
+
const value = typeof raw === "string" ? raw : typeof raw === "number" || typeof raw === "boolean" || typeof raw === "bigint" ? String(raw) : JSON.stringify(raw);
|
|
2790
2999
|
const captured = deps.processRt.captureField(slot, value);
|
|
2791
3000
|
deps.events.note(captured ? "tool.output_captured" : "tool.output_rejected", {
|
|
2792
3001
|
node: node.id,
|
|
@@ -4756,6 +4965,18 @@ function buildTextContext(adapter, call, processRt) {
|
|
|
4756
4965
|
get process() {
|
|
4757
4966
|
return { data: processRt.getData(), complete: processRt.isComplete() };
|
|
4758
4967
|
},
|
|
4968
|
+
// Voice-only actions, stubbed so a flow containing them can still be dry-run
|
|
4969
|
+
// over text. Without these the handoff node threw "deps.ctx.handoff is not
|
|
4970
|
+
// a function" mid-trace (seen in the builder's dry run); the graph now
|
|
4971
|
+
// reaches its handoff outcome cleanly and the trace says why nothing dialed.
|
|
4972
|
+
handoff: async (to) => {
|
|
4973
|
+
processRt.recordEvent({
|
|
4974
|
+
kind: "handoff.simulated",
|
|
4975
|
+
data: { to, reason: "text dry run \u2014 no telephony leg to transfer" }
|
|
4976
|
+
});
|
|
4977
|
+
},
|
|
4978
|
+
endCall: async () => {
|
|
4979
|
+
},
|
|
4759
4980
|
// Voice-only escape hatch is absent on text; left undefined.
|
|
4760
4981
|
livekit: { session: {}, job: {} }
|
|
4761
4982
|
};
|
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",
|