@personaai/runtime 0.16.2 → 0.18.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 CHANGED
@@ -159,7 +159,7 @@ function evictStaleRuns(runs, now, graceMs = DEFAULT_RUN_GRACE_MS, maxTracked =
159
159
  }
160
160
 
161
161
  // src/version.ts
162
- var RUNTIME_VERSION = "0.16.1";
162
+ var RUNTIME_VERSION = "0.18.0";
163
163
 
164
164
  // src/routes/health.ts
165
165
  var alwaysOnCapabilities = {
@@ -997,6 +997,7 @@ var createAgent = async (request, ctx) => {
997
997
  modelName: typeof body.modelName === "string" ? body.modelName : void 0,
998
998
  webSearchEnabled: typeof body.webSearchEnabled === "boolean" ? body.webSearchEnabled : void 0,
999
999
  sandboxEnabled: typeof body.sandboxEnabled === "boolean" ? body.sandboxEnabled : void 0,
1000
+ frontendToolsEnabled: typeof body.frontendToolsEnabled === "boolean" ? body.frontendToolsEnabled : void 0,
1000
1001
  visibility: body.visibility,
1001
1002
  category: body.category,
1002
1003
  skills: Array.isArray(body.skills) ? body.skills : void 0,
@@ -1681,6 +1682,203 @@ var workflowStreamRoute = async (request, ctx) => {
1681
1682
  };
1682
1683
  };
1683
1684
 
1685
+ // src/routes/channels.ts
1686
+ var SIGNATURE_HEADER = "x-twilio-signature";
1687
+ var DEFAULT_VOICE_FALLBACK = "Sorry, this assistant is unavailable right now. Please try again later.";
1688
+ var DEFAULT_MESSAGE_FALLBACK = "Sorry, something went wrong while handling your message. Please try again.";
1689
+ function firstHeader(value) {
1690
+ return value?.split(",")[0]?.trim() || void 0;
1691
+ }
1692
+ function resolveSignedUrl(request, publicUrl) {
1693
+ const search = new URLSearchParams();
1694
+ for (const [key, value] of Object.entries(request.query)) {
1695
+ if (value !== void 0) search.set(key, value);
1696
+ }
1697
+ const query = search.toString();
1698
+ const pathWithQuery = request.path + (query ? `?${query}` : "");
1699
+ if (publicUrl) {
1700
+ return `${publicUrl.replace(/\/+$/, "")}${pathWithQuery}`;
1701
+ }
1702
+ const host = firstHeader(request.headers["x-forwarded-host"]) ?? firstHeader(request.headers.host);
1703
+ const proto = firstHeader(request.headers["x-forwarded-proto"]) ?? "https";
1704
+ if (!host) return null;
1705
+ return `${proto}://${host}${pathWithQuery}`;
1706
+ }
1707
+ async function computeTwilioSignature(authToken, url, params) {
1708
+ const payload = url + Object.keys(params).sort().map((key2) => `${key2}${params[key2]}`).join("");
1709
+ const encoder = new TextEncoder();
1710
+ const key = await crypto.subtle.importKey(
1711
+ "raw",
1712
+ encoder.encode(authToken),
1713
+ { name: "HMAC", hash: "SHA-1" },
1714
+ false,
1715
+ ["sign"]
1716
+ );
1717
+ const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(payload));
1718
+ const bytes = new Uint8Array(signature);
1719
+ let binary = "";
1720
+ for (const byte of bytes) binary += String.fromCharCode(byte);
1721
+ return btoa(binary);
1722
+ }
1723
+ function constantTimeEqual(a, b) {
1724
+ if (a.length !== b.length) return false;
1725
+ let diff = 0;
1726
+ for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
1727
+ return diff === 0;
1728
+ }
1729
+ async function verifyTwilioSignature(request, options) {
1730
+ const provided = request.headers[SIGNATURE_HEADER];
1731
+ if (!provided || !options.authToken) return false;
1732
+ const url = resolveSignedUrl(request, options.publicUrl);
1733
+ if (!url) return false;
1734
+ const expected = await computeTwilioSignature(options.authToken, url, formParams(request));
1735
+ return constantTimeEqual(expected, provided);
1736
+ }
1737
+ function formParams(request) {
1738
+ const params = {};
1739
+ const body = request.body;
1740
+ if (!body || typeof body !== "object" || Array.isArray(body)) return params;
1741
+ for (const [key, value] of Object.entries(body)) {
1742
+ if (typeof value === "string") params[key] = value;
1743
+ else if (typeof value === "number" || typeof value === "boolean") params[key] = String(value);
1744
+ }
1745
+ return params;
1746
+ }
1747
+ function escapeXml(value) {
1748
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
1749
+ }
1750
+ function twiml(body) {
1751
+ return {
1752
+ kind: "buffered",
1753
+ status: 200,
1754
+ headers: { "content-type": "text/xml; charset=utf-8" },
1755
+ body: `<?xml version="1.0" encoding="UTF-8"?>${body}`
1756
+ };
1757
+ }
1758
+ function forbidden(message) {
1759
+ return {
1760
+ kind: "buffered",
1761
+ status: 403,
1762
+ headers: { "content-type": "text/plain; charset=utf-8" },
1763
+ body: message
1764
+ };
1765
+ }
1766
+ function channelKind(from) {
1767
+ return from.trim().toLowerCase().startsWith("whatsapp:") ? "whatsapp" : "sms";
1768
+ }
1769
+ function toUser(result) {
1770
+ if (!result) return null;
1771
+ if (typeof result === "string") return result ? { externalUserId: result } : null;
1772
+ if (typeof result.externalUserId !== "string" || !result.externalUserId) return null;
1773
+ return result;
1774
+ }
1775
+ async function authenticate(request, options, channelRequest) {
1776
+ if (!await verifyTwilioSignature(request, options)) {
1777
+ return { response: forbidden("Invalid Twilio signature") };
1778
+ }
1779
+ let resolved = null;
1780
+ try {
1781
+ resolved = await options.resolveUser(channelRequest);
1782
+ } catch {
1783
+ resolved = null;
1784
+ }
1785
+ const user = toUser(resolved);
1786
+ if (!user) {
1787
+ return {
1788
+ response: channelRequest.channel === "voice" ? twiml("<Response><Reject/></Response>") : twiml("<Response></Response>")
1789
+ };
1790
+ }
1791
+ return { user };
1792
+ }
1793
+ var twilioVoiceChannel = async (request, ctx) => {
1794
+ const options = ctx.channels?.twilio;
1795
+ if (!options) {
1796
+ throw new RuntimeHttpError(404, "NOT_FOUND", "Twilio voice channel is not configured.");
1797
+ }
1798
+ const params = formParams(request);
1799
+ const channelRequest = {
1800
+ channel: "voice",
1801
+ from: params.From ?? "",
1802
+ to: params.To ?? "",
1803
+ ...params.CallSid ? { callSid: params.CallSid } : {}
1804
+ };
1805
+ const auth = await authenticate(request, options, channelRequest);
1806
+ if ("response" in auth) return auth.response;
1807
+ const agentId = auth.user.agentId ?? options.agentId;
1808
+ const fallback = options.voiceFallback ?? DEFAULT_VOICE_FALLBACK;
1809
+ if (!agentId) {
1810
+ ctx.logger.warn("twilio voice channel has no agent configured", {
1811
+ hasDefault: false,
1812
+ resolverProvided: false
1813
+ });
1814
+ return twiml(`<Response><Say>${escapeXml(fallback)}</Say><Hangup/></Response>`);
1815
+ }
1816
+ try {
1817
+ const session = await ctx.client.voice.createTelephonySession({
1818
+ agentId,
1819
+ externalUserId: auth.user.externalUserId,
1820
+ ...auth.user.threadId ? { threadId: auth.user.threadId } : {},
1821
+ ...channelRequest.from ? { from: channelRequest.from } : {},
1822
+ ...channelRequest.to ? { to: channelRequest.to } : {},
1823
+ ...channelRequest.callSid ? { callSid: channelRequest.callSid } : {}
1824
+ });
1825
+ await ctx.hooks?.onVoiceSessionCreate?.({
1826
+ userId: auth.user.externalUserId,
1827
+ agentId
1828
+ });
1829
+ return twiml(
1830
+ `<Response><Connect><Stream url="${escapeXml(session.streamUrl)}"/></Connect></Response>`
1831
+ );
1832
+ } catch (err) {
1833
+ ctx.logger.error("twilio voice channel failed to start a session", {
1834
+ error: err instanceof Error ? err.message : String(err)
1835
+ });
1836
+ return twiml(`<Response><Say>${escapeXml(fallback)}</Say><Hangup/></Response>`);
1837
+ }
1838
+ };
1839
+ var twilioMessageChannel = async (request, ctx) => {
1840
+ const options = ctx.channels?.twilio;
1841
+ if (!options) {
1842
+ throw new RuntimeHttpError(404, "NOT_FOUND", "Twilio messaging channel is not configured.");
1843
+ }
1844
+ const params = formParams(request);
1845
+ const channelRequest = {
1846
+ channel: channelKind(params.From ?? ""),
1847
+ from: params.From ?? "",
1848
+ to: params.To ?? "",
1849
+ ...params.MessageSid ? { messageSid: params.MessageSid } : {},
1850
+ ...params.Body ? { body: params.Body } : {},
1851
+ ...params.ProfileName ? { profileName: params.ProfileName } : {}
1852
+ };
1853
+ const auth = await authenticate(request, options, channelRequest);
1854
+ if ("response" in auth) return auth.response;
1855
+ const agentId = auth.user.agentId ?? options.agentId;
1856
+ const fallback = options.messageFallback === void 0 ? DEFAULT_MESSAGE_FALLBACK : options.messageFallback;
1857
+ if (!agentId) {
1858
+ ctx.logger.warn("twilio message channel has no agent configured");
1859
+ return fallback ? twiml(`<Response><Message>${escapeXml(fallback)}</Message></Response>`) : twiml("<Response></Response>");
1860
+ }
1861
+ const text = channelRequest.body ?? "";
1862
+ if (!text.trim()) {
1863
+ return twiml("<Response></Response>");
1864
+ }
1865
+ try {
1866
+ const result = await ctx.client.messages.generateReply({
1867
+ agentId,
1868
+ externalUserId: auth.user.externalUserId,
1869
+ text,
1870
+ ...auth.user.threadId ? { threadId: auth.user.threadId } : {},
1871
+ ...channelRequest.profileName ? { displayName: channelRequest.profileName } : {}
1872
+ });
1873
+ return twiml(`<Response><Message>${escapeXml(result.reply)}</Message></Response>`);
1874
+ } catch (err) {
1875
+ ctx.logger.error("twilio message channel failed to generate a reply", {
1876
+ error: err instanceof Error ? err.message : String(err)
1877
+ });
1878
+ return fallback ? twiml(`<Response><Message>${escapeXml(fallback)}</Message></Response>`) : twiml("<Response></Response>");
1879
+ }
1880
+ };
1881
+
1684
1882
  // src/runtime.ts
1685
1883
  function resolveCapabilities(capabilities) {
1686
1884
  return {
@@ -1952,6 +2150,22 @@ function createRuntime(options) {
1952
2150
  requiresAuth: false
1953
2151
  });
1954
2152
  }
2153
+ if (options.channels?.twilio) {
2154
+ routes.push(
2155
+ {
2156
+ method: "POST",
2157
+ pattern: ["channels", "twilio", "voice"],
2158
+ handler: twilioVoiceChannel,
2159
+ requiresAuth: false
2160
+ },
2161
+ {
2162
+ method: "POST",
2163
+ pattern: ["channels", "twilio", "message"],
2164
+ handler: twilioMessageChannel,
2165
+ requiresAuth: false
2166
+ }
2167
+ );
2168
+ }
1955
2169
  const mode = resolveMode(options);
1956
2170
  const heartbeatIntervalMs = options.heartbeatIntervalMs ?? 15e3;
1957
2171
  const runGraceMs = options.runGraceMs ?? DEFAULT_RUN_GRACE_MS;
@@ -2119,7 +2333,8 @@ function createRuntime(options) {
2119
2333
  capabilities,
2120
2334
  logger: routeLogger,
2121
2335
  restToolsManifest: options.restToolsManifest,
2122
- rcpManifest: options.rcpManifest
2336
+ rcpManifest: options.rcpManifest,
2337
+ channels: options.channels
2123
2338
  });
2124
2339
  const durationMs = Date.now() - startMs;
2125
2340
  logger.info("handle succeeded", {