@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 +217 -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 +217 -2
- package/dist/index.js.map +1 -1
- package/package.json +67 -67
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.
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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", {
|