@personaai/runtime 0.16.2 → 0.17.0
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/dist/index.cjs +216 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +72 -2
- package/dist/index.d.ts +72 -2
- package/dist/index.js +216 -2
- package/dist/index.js.map +1 -1
- package/package.json +67 -67
package/dist/index.d.cts
CHANGED
|
@@ -265,6 +265,70 @@ interface RcpManifestOptions {
|
|
|
265
265
|
*/
|
|
266
266
|
authToken?: string;
|
|
267
267
|
}
|
|
268
|
+
/**
|
|
269
|
+
* A Twilio webhook request that reached this runtime, after its signature
|
|
270
|
+
* was verified. `from` is the end user's number (WhatsApp numbers keep the
|
|
271
|
+
* `whatsapp:` prefix), `body` is the message text on the message channel.
|
|
272
|
+
*/
|
|
273
|
+
interface TwilioChannelRequest {
|
|
274
|
+
channel: 'voice' | 'whatsapp' | 'sms';
|
|
275
|
+
from: string;
|
|
276
|
+
to: string;
|
|
277
|
+
/** Present on the voice channel. */
|
|
278
|
+
callSid?: string;
|
|
279
|
+
/** Present on the message channel. */
|
|
280
|
+
messageSid?: string;
|
|
281
|
+
/** Present on the message channel — the inbound text. */
|
|
282
|
+
body?: string;
|
|
283
|
+
/** WhatsApp profile name, when Twilio supplies one. */
|
|
284
|
+
profileName?: string;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Return value of {@link TwilioChannelOptions.resolveUser}: the developer's
|
|
288
|
+
* own user id (which becomes the Persona external user id), optionally with
|
|
289
|
+
* a per-request Agent override. `null` means "not one of my users" — the
|
|
290
|
+
* runtime then rejects the call / sends no reply.
|
|
291
|
+
*/
|
|
292
|
+
interface TwilioChannelUser {
|
|
293
|
+
externalUserId: string;
|
|
294
|
+
agentId?: string;
|
|
295
|
+
/** Continue an existing thread instead of the per-user default. */
|
|
296
|
+
threadId?: string;
|
|
297
|
+
}
|
|
298
|
+
type ResolveTwilioChannelUser = (request: TwilioChannelRequest) => TwilioChannelUser | string | null | Promise<TwilioChannelUser | string | null>;
|
|
299
|
+
/**
|
|
300
|
+
* Bring-your-own-Twilio-number webhooks, served by this runtime:
|
|
301
|
+
* `POST /channels/twilio/voice` and `POST /channels/twilio/message`.
|
|
302
|
+
*
|
|
303
|
+
* Both routes verify Twilio's `X-Twilio-Signature` with YOUR auth token,
|
|
304
|
+
* resolve the sender to one of your users, and proxy to Persona. Voice
|
|
305
|
+
* returns TwiML that points Twilio's media stream straight at Persona (your
|
|
306
|
+
* server never relays audio); messaging returns the Agent's reply as TwiML,
|
|
307
|
+
* so no message is sent from Persona's own account.
|
|
308
|
+
*/
|
|
309
|
+
interface TwilioChannelOptions {
|
|
310
|
+
/** Your Twilio Auth Token — used only to verify inbound webhook signatures. */
|
|
311
|
+
authToken: string;
|
|
312
|
+
/**
|
|
313
|
+
* The exact public origin Twilio calls, e.g. `https://app.example.com`.
|
|
314
|
+
* Twilio signs the full URL, so a proxy/rewrite that changes it breaks
|
|
315
|
+
* verification — set this when the request's own host headers aren't the
|
|
316
|
+
* public URL. Omit to derive it from `x-forwarded-proto` / `host`.
|
|
317
|
+
*/
|
|
318
|
+
publicUrl?: string;
|
|
319
|
+
/** Maps a Twilio sender to one of your users. Return `null` to ignore the message/call. */
|
|
320
|
+
resolveUser: ResolveTwilioChannelUser;
|
|
321
|
+
/** Default Agent for both channels. `resolveUser` may override it per request. */
|
|
322
|
+
agentId?: string;
|
|
323
|
+
/** Spoken instead of the Agent's voice when the session cannot start. */
|
|
324
|
+
voiceFallback?: string;
|
|
325
|
+
/** Texted instead of the Agent's reply when the turn fails. `''` disables it. */
|
|
326
|
+
messageFallback?: string;
|
|
327
|
+
}
|
|
328
|
+
interface ChannelsOptions {
|
|
329
|
+
/** Enables the two Twilio channel routes. Unset by default. */
|
|
330
|
+
twilio?: TwilioChannelOptions;
|
|
331
|
+
}
|
|
268
332
|
interface CreateRuntimeOptions {
|
|
269
333
|
/** Base URL of the Persona Developer Platform API, e.g. "https://api.persona.hasanraiyan.me". */
|
|
270
334
|
baseUrl: string;
|
|
@@ -311,6 +375,12 @@ interface CreateRuntimeOptions {
|
|
|
311
375
|
restToolsManifest?: RestToolsManifestOptions;
|
|
312
376
|
/** Serves a conformant RCP manifest for any RCP client to discover. See {@link RcpManifestOptions}. Unset by default — the route only exists when this is provided. */
|
|
313
377
|
rcpManifest?: RcpManifestOptions;
|
|
378
|
+
/**
|
|
379
|
+
* Bring-your-own-Twilio-number webhook routes (`/channels/twilio/*`). See
|
|
380
|
+
* {@link ChannelsOptions}. Unset by default — the routes only exist when
|
|
381
|
+
* this is provided.
|
|
382
|
+
*/
|
|
383
|
+
channels?: ChannelsOptions;
|
|
314
384
|
/** Log level for the runtime — off by default. Overrides the global level set via `setLogLevel()` from `@personaai/sdk`. */
|
|
315
385
|
logLevel?: LogLevel;
|
|
316
386
|
/** Custom logger instance — when provided, `logLevel` is ignored. */
|
|
@@ -337,6 +407,6 @@ declare class RuntimeHttpError extends Error {
|
|
|
337
407
|
constructor(status: number, code: string, message: string);
|
|
338
408
|
}
|
|
339
409
|
|
|
340
|
-
declare const RUNTIME_VERSION = "0.
|
|
410
|
+
declare const RUNTIME_VERSION = "0.17.0";
|
|
341
411
|
|
|
342
|
-
export { type CreateRuntimeOptions, type ErrorContext, type FileUploadContext, type MemoryWriteContext, RUNTIME_VERSION, type RcpManifestOptions, type ResolveUser, type RestToolManifestEntry, type RestToolsManifestOptions, type RunContext, type RunResult, type Runtime, type RuntimeBinaryResponse, type RuntimeBufferedResponse, type RuntimeCapabilities, type RuntimeHooks, RuntimeHttpError, type RuntimeMethod, type RuntimeRequest, type RuntimeResponse, type RuntimeStreamResponse, type RuntimeUploadedFile, type ThreadCreateContext, type ToolCallContext, type VoiceSessionCreateContext, type WorkflowNodeContext, createRuntime };
|
|
412
|
+
export { type ChannelsOptions, type CreateRuntimeOptions, type ErrorContext, type FileUploadContext, type MemoryWriteContext, RUNTIME_VERSION, type RcpManifestOptions, type ResolveTwilioChannelUser, type ResolveUser, type RestToolManifestEntry, type RestToolsManifestOptions, type RunContext, type RunResult, type Runtime, type RuntimeBinaryResponse, type RuntimeBufferedResponse, type RuntimeCapabilities, type RuntimeHooks, RuntimeHttpError, type RuntimeMethod, type RuntimeRequest, type RuntimeResponse, type RuntimeStreamResponse, type RuntimeUploadedFile, type ThreadCreateContext, type ToolCallContext, type TwilioChannelOptions, type TwilioChannelRequest, type TwilioChannelUser, type VoiceSessionCreateContext, type WorkflowNodeContext, createRuntime };
|
package/dist/index.d.ts
CHANGED
|
@@ -265,6 +265,70 @@ interface RcpManifestOptions {
|
|
|
265
265
|
*/
|
|
266
266
|
authToken?: string;
|
|
267
267
|
}
|
|
268
|
+
/**
|
|
269
|
+
* A Twilio webhook request that reached this runtime, after its signature
|
|
270
|
+
* was verified. `from` is the end user's number (WhatsApp numbers keep the
|
|
271
|
+
* `whatsapp:` prefix), `body` is the message text on the message channel.
|
|
272
|
+
*/
|
|
273
|
+
interface TwilioChannelRequest {
|
|
274
|
+
channel: 'voice' | 'whatsapp' | 'sms';
|
|
275
|
+
from: string;
|
|
276
|
+
to: string;
|
|
277
|
+
/** Present on the voice channel. */
|
|
278
|
+
callSid?: string;
|
|
279
|
+
/** Present on the message channel. */
|
|
280
|
+
messageSid?: string;
|
|
281
|
+
/** Present on the message channel — the inbound text. */
|
|
282
|
+
body?: string;
|
|
283
|
+
/** WhatsApp profile name, when Twilio supplies one. */
|
|
284
|
+
profileName?: string;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Return value of {@link TwilioChannelOptions.resolveUser}: the developer's
|
|
288
|
+
* own user id (which becomes the Persona external user id), optionally with
|
|
289
|
+
* a per-request Agent override. `null` means "not one of my users" — the
|
|
290
|
+
* runtime then rejects the call / sends no reply.
|
|
291
|
+
*/
|
|
292
|
+
interface TwilioChannelUser {
|
|
293
|
+
externalUserId: string;
|
|
294
|
+
agentId?: string;
|
|
295
|
+
/** Continue an existing thread instead of the per-user default. */
|
|
296
|
+
threadId?: string;
|
|
297
|
+
}
|
|
298
|
+
type ResolveTwilioChannelUser = (request: TwilioChannelRequest) => TwilioChannelUser | string | null | Promise<TwilioChannelUser | string | null>;
|
|
299
|
+
/**
|
|
300
|
+
* Bring-your-own-Twilio-number webhooks, served by this runtime:
|
|
301
|
+
* `POST /channels/twilio/voice` and `POST /channels/twilio/message`.
|
|
302
|
+
*
|
|
303
|
+
* Both routes verify Twilio's `X-Twilio-Signature` with YOUR auth token,
|
|
304
|
+
* resolve the sender to one of your users, and proxy to Persona. Voice
|
|
305
|
+
* returns TwiML that points Twilio's media stream straight at Persona (your
|
|
306
|
+
* server never relays audio); messaging returns the Agent's reply as TwiML,
|
|
307
|
+
* so no message is sent from Persona's own account.
|
|
308
|
+
*/
|
|
309
|
+
interface TwilioChannelOptions {
|
|
310
|
+
/** Your Twilio Auth Token — used only to verify inbound webhook signatures. */
|
|
311
|
+
authToken: string;
|
|
312
|
+
/**
|
|
313
|
+
* The exact public origin Twilio calls, e.g. `https://app.example.com`.
|
|
314
|
+
* Twilio signs the full URL, so a proxy/rewrite that changes it breaks
|
|
315
|
+
* verification — set this when the request's own host headers aren't the
|
|
316
|
+
* public URL. Omit to derive it from `x-forwarded-proto` / `host`.
|
|
317
|
+
*/
|
|
318
|
+
publicUrl?: string;
|
|
319
|
+
/** Maps a Twilio sender to one of your users. Return `null` to ignore the message/call. */
|
|
320
|
+
resolveUser: ResolveTwilioChannelUser;
|
|
321
|
+
/** Default Agent for both channels. `resolveUser` may override it per request. */
|
|
322
|
+
agentId?: string;
|
|
323
|
+
/** Spoken instead of the Agent's voice when the session cannot start. */
|
|
324
|
+
voiceFallback?: string;
|
|
325
|
+
/** Texted instead of the Agent's reply when the turn fails. `''` disables it. */
|
|
326
|
+
messageFallback?: string;
|
|
327
|
+
}
|
|
328
|
+
interface ChannelsOptions {
|
|
329
|
+
/** Enables the two Twilio channel routes. Unset by default. */
|
|
330
|
+
twilio?: TwilioChannelOptions;
|
|
331
|
+
}
|
|
268
332
|
interface CreateRuntimeOptions {
|
|
269
333
|
/** Base URL of the Persona Developer Platform API, e.g. "https://api.persona.hasanraiyan.me". */
|
|
270
334
|
baseUrl: string;
|
|
@@ -311,6 +375,12 @@ interface CreateRuntimeOptions {
|
|
|
311
375
|
restToolsManifest?: RestToolsManifestOptions;
|
|
312
376
|
/** Serves a conformant RCP manifest for any RCP client to discover. See {@link RcpManifestOptions}. Unset by default — the route only exists when this is provided. */
|
|
313
377
|
rcpManifest?: RcpManifestOptions;
|
|
378
|
+
/**
|
|
379
|
+
* Bring-your-own-Twilio-number webhook routes (`/channels/twilio/*`). See
|
|
380
|
+
* {@link ChannelsOptions}. Unset by default — the routes only exist when
|
|
381
|
+
* this is provided.
|
|
382
|
+
*/
|
|
383
|
+
channels?: ChannelsOptions;
|
|
314
384
|
/** Log level for the runtime — off by default. Overrides the global level set via `setLogLevel()` from `@personaai/sdk`. */
|
|
315
385
|
logLevel?: LogLevel;
|
|
316
386
|
/** Custom logger instance — when provided, `logLevel` is ignored. */
|
|
@@ -337,6 +407,6 @@ declare class RuntimeHttpError extends Error {
|
|
|
337
407
|
constructor(status: number, code: string, message: string);
|
|
338
408
|
}
|
|
339
409
|
|
|
340
|
-
declare const RUNTIME_VERSION = "0.
|
|
410
|
+
declare const RUNTIME_VERSION = "0.17.0";
|
|
341
411
|
|
|
342
|
-
export { type CreateRuntimeOptions, type ErrorContext, type FileUploadContext, type MemoryWriteContext, RUNTIME_VERSION, type RcpManifestOptions, type ResolveUser, type RestToolManifestEntry, type RestToolsManifestOptions, type RunContext, type RunResult, type Runtime, type RuntimeBinaryResponse, type RuntimeBufferedResponse, type RuntimeCapabilities, type RuntimeHooks, RuntimeHttpError, type RuntimeMethod, type RuntimeRequest, type RuntimeResponse, type RuntimeStreamResponse, type RuntimeUploadedFile, type ThreadCreateContext, type ToolCallContext, type VoiceSessionCreateContext, type WorkflowNodeContext, createRuntime };
|
|
412
|
+
export { type ChannelsOptions, type CreateRuntimeOptions, type ErrorContext, type FileUploadContext, type MemoryWriteContext, RUNTIME_VERSION, type RcpManifestOptions, type ResolveTwilioChannelUser, type ResolveUser, type RestToolManifestEntry, type RestToolsManifestOptions, type RunContext, type RunResult, type Runtime, type RuntimeBinaryResponse, type RuntimeBufferedResponse, type RuntimeCapabilities, type RuntimeHooks, RuntimeHttpError, type RuntimeMethod, type RuntimeRequest, type RuntimeResponse, type RuntimeStreamResponse, type RuntimeUploadedFile, type ThreadCreateContext, type ToolCallContext, type TwilioChannelOptions, type TwilioChannelRequest, type TwilioChannelUser, type VoiceSessionCreateContext, type WorkflowNodeContext, createRuntime };
|
package/dist/index.js
CHANGED
|
@@ -131,7 +131,7 @@ function evictStaleRuns(runs, now, graceMs = DEFAULT_RUN_GRACE_MS, maxTracked =
|
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
// src/version.ts
|
|
134
|
-
var RUNTIME_VERSION = "0.
|
|
134
|
+
var RUNTIME_VERSION = "0.17.0";
|
|
135
135
|
|
|
136
136
|
// src/routes/health.ts
|
|
137
137
|
var alwaysOnCapabilities = {
|
|
@@ -1653,6 +1653,203 @@ var workflowStreamRoute = async (request, ctx) => {
|
|
|
1653
1653
|
};
|
|
1654
1654
|
};
|
|
1655
1655
|
|
|
1656
|
+
// src/routes/channels.ts
|
|
1657
|
+
var SIGNATURE_HEADER = "x-twilio-signature";
|
|
1658
|
+
var DEFAULT_VOICE_FALLBACK = "Sorry, this assistant is unavailable right now. Please try again later.";
|
|
1659
|
+
var DEFAULT_MESSAGE_FALLBACK = "Sorry, something went wrong while handling your message. Please try again.";
|
|
1660
|
+
function firstHeader(value) {
|
|
1661
|
+
return value?.split(",")[0]?.trim() || void 0;
|
|
1662
|
+
}
|
|
1663
|
+
function resolveSignedUrl(request, publicUrl) {
|
|
1664
|
+
const search = new URLSearchParams();
|
|
1665
|
+
for (const [key, value] of Object.entries(request.query)) {
|
|
1666
|
+
if (value !== void 0) search.set(key, value);
|
|
1667
|
+
}
|
|
1668
|
+
const query = search.toString();
|
|
1669
|
+
const pathWithQuery = request.path + (query ? `?${query}` : "");
|
|
1670
|
+
if (publicUrl) {
|
|
1671
|
+
return `${publicUrl.replace(/\/+$/, "")}${pathWithQuery}`;
|
|
1672
|
+
}
|
|
1673
|
+
const host = firstHeader(request.headers["x-forwarded-host"]) ?? firstHeader(request.headers.host);
|
|
1674
|
+
const proto = firstHeader(request.headers["x-forwarded-proto"]) ?? "https";
|
|
1675
|
+
if (!host) return null;
|
|
1676
|
+
return `${proto}://${host}${pathWithQuery}`;
|
|
1677
|
+
}
|
|
1678
|
+
async function computeTwilioSignature(authToken, url, params) {
|
|
1679
|
+
const payload = url + Object.keys(params).sort().map((key2) => `${key2}${params[key2]}`).join("");
|
|
1680
|
+
const encoder = new TextEncoder();
|
|
1681
|
+
const key = await crypto.subtle.importKey(
|
|
1682
|
+
"raw",
|
|
1683
|
+
encoder.encode(authToken),
|
|
1684
|
+
{ name: "HMAC", hash: "SHA-1" },
|
|
1685
|
+
false,
|
|
1686
|
+
["sign"]
|
|
1687
|
+
);
|
|
1688
|
+
const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(payload));
|
|
1689
|
+
const bytes = new Uint8Array(signature);
|
|
1690
|
+
let binary = "";
|
|
1691
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
1692
|
+
return btoa(binary);
|
|
1693
|
+
}
|
|
1694
|
+
function constantTimeEqual(a, b) {
|
|
1695
|
+
if (a.length !== b.length) return false;
|
|
1696
|
+
let diff = 0;
|
|
1697
|
+
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
1698
|
+
return diff === 0;
|
|
1699
|
+
}
|
|
1700
|
+
async function verifyTwilioSignature(request, options) {
|
|
1701
|
+
const provided = request.headers[SIGNATURE_HEADER];
|
|
1702
|
+
if (!provided || !options.authToken) return false;
|
|
1703
|
+
const url = resolveSignedUrl(request, options.publicUrl);
|
|
1704
|
+
if (!url) return false;
|
|
1705
|
+
const expected = await computeTwilioSignature(options.authToken, url, formParams(request));
|
|
1706
|
+
return constantTimeEqual(expected, provided);
|
|
1707
|
+
}
|
|
1708
|
+
function formParams(request) {
|
|
1709
|
+
const params = {};
|
|
1710
|
+
const body = request.body;
|
|
1711
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return params;
|
|
1712
|
+
for (const [key, value] of Object.entries(body)) {
|
|
1713
|
+
if (typeof value === "string") params[key] = value;
|
|
1714
|
+
else if (typeof value === "number" || typeof value === "boolean") params[key] = String(value);
|
|
1715
|
+
}
|
|
1716
|
+
return params;
|
|
1717
|
+
}
|
|
1718
|
+
function escapeXml(value) {
|
|
1719
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1720
|
+
}
|
|
1721
|
+
function twiml(body) {
|
|
1722
|
+
return {
|
|
1723
|
+
kind: "buffered",
|
|
1724
|
+
status: 200,
|
|
1725
|
+
headers: { "content-type": "text/xml; charset=utf-8" },
|
|
1726
|
+
body: `<?xml version="1.0" encoding="UTF-8"?>${body}`
|
|
1727
|
+
};
|
|
1728
|
+
}
|
|
1729
|
+
function forbidden(message) {
|
|
1730
|
+
return {
|
|
1731
|
+
kind: "buffered",
|
|
1732
|
+
status: 403,
|
|
1733
|
+
headers: { "content-type": "text/plain; charset=utf-8" },
|
|
1734
|
+
body: message
|
|
1735
|
+
};
|
|
1736
|
+
}
|
|
1737
|
+
function channelKind(from) {
|
|
1738
|
+
return from.trim().toLowerCase().startsWith("whatsapp:") ? "whatsapp" : "sms";
|
|
1739
|
+
}
|
|
1740
|
+
function toUser(result) {
|
|
1741
|
+
if (!result) return null;
|
|
1742
|
+
if (typeof result === "string") return result ? { externalUserId: result } : null;
|
|
1743
|
+
if (typeof result.externalUserId !== "string" || !result.externalUserId) return null;
|
|
1744
|
+
return result;
|
|
1745
|
+
}
|
|
1746
|
+
async function authenticate(request, options, channelRequest) {
|
|
1747
|
+
if (!await verifyTwilioSignature(request, options)) {
|
|
1748
|
+
return { response: forbidden("Invalid Twilio signature") };
|
|
1749
|
+
}
|
|
1750
|
+
let resolved = null;
|
|
1751
|
+
try {
|
|
1752
|
+
resolved = await options.resolveUser(channelRequest);
|
|
1753
|
+
} catch {
|
|
1754
|
+
resolved = null;
|
|
1755
|
+
}
|
|
1756
|
+
const user = toUser(resolved);
|
|
1757
|
+
if (!user) {
|
|
1758
|
+
return {
|
|
1759
|
+
response: channelRequest.channel === "voice" ? twiml("<Response><Reject/></Response>") : twiml("<Response></Response>")
|
|
1760
|
+
};
|
|
1761
|
+
}
|
|
1762
|
+
return { user };
|
|
1763
|
+
}
|
|
1764
|
+
var twilioVoiceChannel = async (request, ctx) => {
|
|
1765
|
+
const options = ctx.channels?.twilio;
|
|
1766
|
+
if (!options) {
|
|
1767
|
+
throw new RuntimeHttpError(404, "NOT_FOUND", "Twilio voice channel is not configured.");
|
|
1768
|
+
}
|
|
1769
|
+
const params = formParams(request);
|
|
1770
|
+
const channelRequest = {
|
|
1771
|
+
channel: "voice",
|
|
1772
|
+
from: params.From ?? "",
|
|
1773
|
+
to: params.To ?? "",
|
|
1774
|
+
...params.CallSid ? { callSid: params.CallSid } : {}
|
|
1775
|
+
};
|
|
1776
|
+
const auth = await authenticate(request, options, channelRequest);
|
|
1777
|
+
if ("response" in auth) return auth.response;
|
|
1778
|
+
const agentId = auth.user.agentId ?? options.agentId;
|
|
1779
|
+
const fallback = options.voiceFallback ?? DEFAULT_VOICE_FALLBACK;
|
|
1780
|
+
if (!agentId) {
|
|
1781
|
+
ctx.logger.warn("twilio voice channel has no agent configured", {
|
|
1782
|
+
hasDefault: false,
|
|
1783
|
+
resolverProvided: false
|
|
1784
|
+
});
|
|
1785
|
+
return twiml(`<Response><Say>${escapeXml(fallback)}</Say><Hangup/></Response>`);
|
|
1786
|
+
}
|
|
1787
|
+
try {
|
|
1788
|
+
const session = await ctx.client.voice.createTelephonySession({
|
|
1789
|
+
agentId,
|
|
1790
|
+
externalUserId: auth.user.externalUserId,
|
|
1791
|
+
...auth.user.threadId ? { threadId: auth.user.threadId } : {},
|
|
1792
|
+
...channelRequest.from ? { from: channelRequest.from } : {},
|
|
1793
|
+
...channelRequest.to ? { to: channelRequest.to } : {},
|
|
1794
|
+
...channelRequest.callSid ? { callSid: channelRequest.callSid } : {}
|
|
1795
|
+
});
|
|
1796
|
+
await ctx.hooks?.onVoiceSessionCreate?.({
|
|
1797
|
+
userId: auth.user.externalUserId,
|
|
1798
|
+
agentId
|
|
1799
|
+
});
|
|
1800
|
+
return twiml(
|
|
1801
|
+
`<Response><Connect><Stream url="${escapeXml(session.streamUrl)}"/></Connect></Response>`
|
|
1802
|
+
);
|
|
1803
|
+
} catch (err) {
|
|
1804
|
+
ctx.logger.error("twilio voice channel failed to start a session", {
|
|
1805
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1806
|
+
});
|
|
1807
|
+
return twiml(`<Response><Say>${escapeXml(fallback)}</Say><Hangup/></Response>`);
|
|
1808
|
+
}
|
|
1809
|
+
};
|
|
1810
|
+
var twilioMessageChannel = async (request, ctx) => {
|
|
1811
|
+
const options = ctx.channels?.twilio;
|
|
1812
|
+
if (!options) {
|
|
1813
|
+
throw new RuntimeHttpError(404, "NOT_FOUND", "Twilio messaging channel is not configured.");
|
|
1814
|
+
}
|
|
1815
|
+
const params = formParams(request);
|
|
1816
|
+
const channelRequest = {
|
|
1817
|
+
channel: channelKind(params.From ?? ""),
|
|
1818
|
+
from: params.From ?? "",
|
|
1819
|
+
to: params.To ?? "",
|
|
1820
|
+
...params.MessageSid ? { messageSid: params.MessageSid } : {},
|
|
1821
|
+
...params.Body ? { body: params.Body } : {},
|
|
1822
|
+
...params.ProfileName ? { profileName: params.ProfileName } : {}
|
|
1823
|
+
};
|
|
1824
|
+
const auth = await authenticate(request, options, channelRequest);
|
|
1825
|
+
if ("response" in auth) return auth.response;
|
|
1826
|
+
const agentId = auth.user.agentId ?? options.agentId;
|
|
1827
|
+
const fallback = options.messageFallback === void 0 ? DEFAULT_MESSAGE_FALLBACK : options.messageFallback;
|
|
1828
|
+
if (!agentId) {
|
|
1829
|
+
ctx.logger.warn("twilio message channel has no agent configured");
|
|
1830
|
+
return fallback ? twiml(`<Response><Message>${escapeXml(fallback)}</Message></Response>`) : twiml("<Response></Response>");
|
|
1831
|
+
}
|
|
1832
|
+
const text = channelRequest.body ?? "";
|
|
1833
|
+
if (!text.trim()) {
|
|
1834
|
+
return twiml("<Response></Response>");
|
|
1835
|
+
}
|
|
1836
|
+
try {
|
|
1837
|
+
const result = await ctx.client.messages.generateReply({
|
|
1838
|
+
agentId,
|
|
1839
|
+
externalUserId: auth.user.externalUserId,
|
|
1840
|
+
text,
|
|
1841
|
+
...auth.user.threadId ? { threadId: auth.user.threadId } : {},
|
|
1842
|
+
...channelRequest.profileName ? { displayName: channelRequest.profileName } : {}
|
|
1843
|
+
});
|
|
1844
|
+
return twiml(`<Response><Message>${escapeXml(result.reply)}</Message></Response>`);
|
|
1845
|
+
} catch (err) {
|
|
1846
|
+
ctx.logger.error("twilio message channel failed to generate a reply", {
|
|
1847
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1848
|
+
});
|
|
1849
|
+
return fallback ? twiml(`<Response><Message>${escapeXml(fallback)}</Message></Response>`) : twiml("<Response></Response>");
|
|
1850
|
+
}
|
|
1851
|
+
};
|
|
1852
|
+
|
|
1656
1853
|
// src/runtime.ts
|
|
1657
1854
|
function resolveCapabilities(capabilities) {
|
|
1658
1855
|
return {
|
|
@@ -1924,6 +2121,22 @@ function createRuntime(options) {
|
|
|
1924
2121
|
requiresAuth: false
|
|
1925
2122
|
});
|
|
1926
2123
|
}
|
|
2124
|
+
if (options.channels?.twilio) {
|
|
2125
|
+
routes.push(
|
|
2126
|
+
{
|
|
2127
|
+
method: "POST",
|
|
2128
|
+
pattern: ["channels", "twilio", "voice"],
|
|
2129
|
+
handler: twilioVoiceChannel,
|
|
2130
|
+
requiresAuth: false
|
|
2131
|
+
},
|
|
2132
|
+
{
|
|
2133
|
+
method: "POST",
|
|
2134
|
+
pattern: ["channels", "twilio", "message"],
|
|
2135
|
+
handler: twilioMessageChannel,
|
|
2136
|
+
requiresAuth: false
|
|
2137
|
+
}
|
|
2138
|
+
);
|
|
2139
|
+
}
|
|
1927
2140
|
const mode = resolveMode(options);
|
|
1928
2141
|
const heartbeatIntervalMs = options.heartbeatIntervalMs ?? 15e3;
|
|
1929
2142
|
const runGraceMs = options.runGraceMs ?? DEFAULT_RUN_GRACE_MS;
|
|
@@ -2091,7 +2304,8 @@ function createRuntime(options) {
|
|
|
2091
2304
|
capabilities,
|
|
2092
2305
|
logger: routeLogger,
|
|
2093
2306
|
restToolsManifest: options.restToolsManifest,
|
|
2094
|
-
rcpManifest: options.rcpManifest
|
|
2307
|
+
rcpManifest: options.rcpManifest,
|
|
2308
|
+
channels: options.channels
|
|
2095
2309
|
});
|
|
2096
2310
|
const durationMs = Date.now() - startMs;
|
|
2097
2311
|
logger.info("handle succeeded", {
|