From ad181bf4a4e7f787fda8c557164e2cd96a729aad Mon Sep 17 00:00:00 2001 From: August Date: Thu, 22 Oct 2026 19:33:47 +0000 Subject: [PATCH 01/15] docs(server-sdks): add TypeScript ChatGateway and HandoffRouter reference --- .../ai-chat-client/create-conversation.mdx | 5 +- .../agents/ai-chat-client/index.mdx | 17 +- .../agents/ai-chat-client/raw-post.mdx | 64 ++++ .../typescript/agents/chat-gateway/close.mdx | 37 ++ .../typescript/agents/chat-gateway/index.mdx | 339 ++++++++++++++++++ .../agents/chat-gateway/last-activity.mdx | 44 +++ .../agents/chat-gateway/mint-handle.mdx | 44 +++ .../agents/chat-gateway/prepare.mdx | 95 +++++ .../agents/chat-gateway/read-handle.mdx | 59 +++ .../typescript/agents/chat-gateway/router.mdx | 76 ++++ .../agents/chat-gateway/visible-messages.mdx | 44 +++ .../agents/handoff-router/escalate.mdx | 45 +++ .../agents/handoff-router/index.mdx | 288 +++++++++++++++ .../agents/handoff-router/redeem.mdx | 50 +++ .../agents/handoff-router/register.mdx | 61 ++++ .../agents/handoff-router/router.mdx | 59 +++ .../typescript/agents/handoff-router/say.mdx | 56 +++ .../reference/typescript/agents/overview.mdx | 12 + 18 files changed, 1387 insertions(+), 8 deletions(-) create mode 100644 fern/products/server-sdks/pages/reference/typescript/agents/ai-chat-client/raw-post.mdx create mode 100644 fern/products/server-sdks/pages/reference/typescript/agents/chat-gateway/close.mdx create mode 100644 fern/products/server-sdks/pages/reference/typescript/agents/chat-gateway/index.mdx create mode 100644 fern/products/server-sdks/pages/reference/typescript/agents/chat-gateway/last-activity.mdx create mode 100644 fern/products/server-sdks/pages/reference/typescript/agents/chat-gateway/mint-handle.mdx create mode 100644 fern/products/server-sdks/pages/reference/typescript/agents/chat-gateway/prepare.mdx create mode 100644 fern/products/server-sdks/pages/reference/typescript/agents/chat-gateway/read-handle.mdx create mode 100644 fern/products/server-sdks/pages/reference/typescript/agents/chat-gateway/router.mdx create mode 100644 fern/products/server-sdks/pages/reference/typescript/agents/chat-gateway/visible-messages.mdx create mode 100644 fern/products/server-sdks/pages/reference/typescript/agents/handoff-router/escalate.mdx create mode 100644 fern/products/server-sdks/pages/reference/typescript/agents/handoff-router/index.mdx create mode 100644 fern/products/server-sdks/pages/reference/typescript/agents/handoff-router/redeem.mdx create mode 100644 fern/products/server-sdks/pages/reference/typescript/agents/handoff-router/register.mdx create mode 100644 fern/products/server-sdks/pages/reference/typescript/agents/handoff-router/router.mdx create mode 100644 fern/products/server-sdks/pages/reference/typescript/agents/handoff-router/say.mdx diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/ai-chat-client/create-conversation.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/ai-chat-client/create-conversation.mdx index 67aa23d077..317769a07c 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/ai-chat-client/create-conversation.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/ai-chat-client/create-conversation.mdx @@ -14,7 +14,10 @@ it for an id that already exists returns that conversation untouched. ## **Parameters** - The conversation id. You choose it, and it is scoped to your project. + The conversation id. You choose it, and it is scoped to your project. The service + keeps only letters, digits, `_`, `-`, `.`, and `:`, and silently strips any other + character, so the client logs a warning for an id it would change. Use `.` to + compose ids, such as `root.2` for a second leg of `root`. diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/ai-chat-client/index.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/ai-chat-client/index.mdx index 068d17b4e7..f3cced1096 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/ai-chat-client/index.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/ai-chat-client/index.mdx @@ -42,10 +42,9 @@ other users. - Your space name, `your-space` rather than `your-space.signalwire.com`, used to build - the service URL. Falls back to `SIGNALWIRE_SPACE`. `RestClient` reads that variable - as a full hostname, so a value set for REST builds a wrong URL here; pass `space` or - `url` explicitly in that case. + Your space name (`your-space`) or its hostname (`your-space.signalwire.com`), used + to build the service URL. Falls back to `SIGNALWIRE_SPACE`, so the value set for + `RestClient` works here too. @@ -58,9 +57,10 @@ other users. - Seconds of silence on the connection before a request is abandoned. The - service sends keepalive bytes while a slow turn runs, so this bounds a dead - connection rather than total turn length. `0` disables it. + Seconds without receiving any data, headers or body, before a request is + abandoned. Every chunk restarts it, and the service sends keepalive bytes while a + slow turn runs, so this bounds a dead connection rather than total turn length. + `0` disables it. A configured environment needs no arguments at all. Identity travels in HTTP @@ -135,6 +135,9 @@ response that isn't JSON throws `AIChatError` with the HTTP status as its `code` Complete the client's lifecycle. + + Send one call and return the response with its body unread. + ## **Example** diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/ai-chat-client/raw-post.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/ai-chat-client/raw-post.mdx new file mode 100644 index 0000000000..4b26b8bf1e --- /dev/null +++ b/fern/products/server-sdks/pages/reference/typescript/agents/ai-chat-client/raw-post.mdx @@ -0,0 +1,64 @@ +--- +title: "rawPost" +slug: reference/typescript/agents/ai-chat-client/raw-post +description: Send one JSON-RPC call and get the response with its body unread. +max-toc-depth: 3 +--- + +[ref-aichatclient]: /docs/server-sdks/reference/typescript/agents/ai-chat-client +[chatgateway]: /docs/server-sdks/reference/typescript/agents/chat-gateway +[error-codes]: /docs/apis/error-codes + +Posts one JSON-RPC call and returns the `Response` before the SDK reads its body. Use it in a proxy +that has to stream the body through rather than buffer it. [`ChatGateway`][chatgateway] uses it to +relay a chat reply. + +The service pads a slow response with keepalive whitespace so that intermediaries don't close the +connection mid-turn. A proxy that waits for the whole body holds that padding back and brings back +the timeout it's there to prevent. Pass `response.body` on as it arrives. + +Prefer the typed methods unless you are relaying bytes. + + + You own interpreting the result. A JSON-RPC error arrives under HTTP `200`, so check the status + first, then look for an `error` member in the body and compare its code with + [AI chat errors][error-codes]. Read the body to the end or cancel it, so the connection is + released. The client's `readIdleTimeoutSeconds` applies to each read. + + +## Parameters + + + The JSON-RPC method, for example `chat`. + + +"} required={true} toc={true}> + The method's parameters, sent as they are. The client wraps them in the JSON-RPC envelope and + supplies the request `id`. + + +## Returns + +`Promise` -- the response, with its body unread. + +## Examples + +```typescript +import { serve } from '@hono/node-server'; +import { Hono } from 'hono'; +import { AIChatClient } from '@signalwire/sdk'; + +const client = new AIChatClient({ space: 'your-space' }); +const app = new Hono(); + +app.post('/chat', async (c) => { + const { id, message } = await c.req.json<{ id: string; message: string }>(); + const upstream = await client.rawPost('chat', { id, message }); + return new Response(upstream.body, { + status: upstream.status, + headers: { 'Content-Type': 'application/json' }, + }); +}); + +serve({ fetch: app.fetch, port: 3000 }); +``` diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/chat-gateway/close.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/chat-gateway/close.mdx new file mode 100644 index 0000000000..ad35308da8 --- /dev/null +++ b/fern/products/server-sdks/pages/reference/typescript/agents/chat-gateway/close.mdx @@ -0,0 +1,37 @@ +--- +title: "close" +slug: reference/typescript/agents/chat-gateway/close +description: Close the AI chat client, when the gateway built it. +max-toc-depth: 3 +--- + +[chatgateway]: /docs/server-sdks/reference/typescript/agents/chat-gateway +[aichatclient]: /docs/server-sdks/reference/typescript/agents/ai-chat-client + +Closes the [`AIChatClient`][aichatclient] the gateway built from the environment. A client you +passed to the constructor belongs to you and is left open. Call `close()` once, when the process +shuts down. + +## Parameters + +None. + +## Returns + +`Promise`. + +## Examples + +```typescript +import { ChatGateway } from '@signalwire/sdk'; + +const gateway = new ChatGateway({ + configUrl: 'https://bayview-taxi.example.com/dispatch', + key: '', +}); + +process.on('SIGTERM', async () => { + await gateway.close(); + process.exit(0); +}); +``` diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/chat-gateway/index.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/chat-gateway/index.mdx new file mode 100644 index 0000000000..d9660c8149 --- /dev/null +++ b/fern/products/server-sdks/pages/reference/typescript/agents/chat-gateway/index.mdx @@ -0,0 +1,339 @@ +--- +title: "ChatGateway" +slug: reference/typescript/agents/chat-gateway +description: Browser-facing proxy that lets a page chat with an AI agent without holding a SignalWire API token. +max-toc-depth: 3 +--- + +[aichatclient]: /docs/server-sdks/reference/typescript/agents/ai-chat-client +[rawpost]: /docs/server-sdks/reference/typescript/agents/ai-chat-client/raw-post +[router]: /docs/server-sdks/reference/typescript/agents/chat-gateway/router +[minthandle]: /docs/server-sdks/reference/typescript/agents/chat-gateway/mint-handle +[readhandle]: /docs/server-sdks/reference/typescript/agents/chat-gateway/read-handle +[prepare]: /docs/server-sdks/reference/typescript/agents/chat-gateway/prepare +[visiblemessages]: /docs/server-sdks/reference/typescript/agents/chat-gateway/visible-messages +[lastactivity]: /docs/server-sdks/reference/typescript/agents/chat-gateway/last-activity +[close]: /docs/server-sdks/reference/typescript/agents/chat-gateway/close +[handoffrouter]: /docs/server-sdks/reference/typescript/agents/handoff-router + +A chat widget in a page can't hold a SignalWire API token: the token carries the whole project, and +every visitor could read it. `ChatGateway` mounts inside a web application you already run, holds +the credentials on the server, and forwards on the widget's behalf. + +The browser learns two things, the gateway's URL and a publishable key. It doesn't learn the +project, the Space, the token, or which agent runs, because the gateway sends `config_url` itself +and a key only reaches the one agent it was issued for. + +```typescript +import { AgentBase, ChatGateway } from '@signalwire/sdk'; + +const agent = new AgentBase({ name: 'dispatch', route: '/dispatch' }); +agent.setPromptText('You are Ada, the dispatcher for Bayview Taxi.'); + +const gateway = new ChatGateway({ + configUrl: 'https://bayview-taxi.example.com/dispatch', // never leaves the server + key: '', // safe in the page + allowedOrigins: ['https://bayviewtaxi.example.com'], + secret: process.env.SIGNALWIRE_CHAT_GATEWAY_SECRET, + conversationTimeout: 1800, + maxNewConversations: 60, + maxTurns: 200, +}); + +agent.mount(gateway.router(), { prefix: '/chat' }); + +await agent.serve(); +``` + + + +An agent already serves a Hono app, so `agent.mount()` hosts the gateway with no new +infrastructure. [`router()`][router] returns a standard Hono app, so `app.route()` works on any +other Hono server. + + +The SignalWire address widget also calls `/handoff`, `/escalate`, and `/say` at the same URL to +move a conversation between chat and a phone call. [`HandoffRouter`][handoffrouter] serves those +three routes; mount it at the same prefix as the gateway. + + +## Parameters + +The constructor takes one options object, `ChatGatewayOptions`. It throws an `Error` when +`configUrl` is empty. + + + The agent this key may talk to. Sent on every call and never taken from the request, so whoever + holds a key can't choose which agent runs. + + + + The publishable key the widget carries. Falls back to `SIGNALWIRE_CHAT_GATEWAY_KEY`, then to a + generated `pk_` value, which is useful only to a process that also serves the page and can embed + it. + + + + Origins allowed to use this key, stored without trailing slashes. Localhost is always allowed so + local development works unconfigured; any other origin must be listed. + + + + The [`AIChatClient`][aichatclient] to forward with. Built from the environment when omitted, and + then closed by [`close()`][close]. A client you pass in is yours to close. + + + + HMAC key for signing handles. Falls back to `SIGNALWIRE_CHAT_GATEWAY_SECRET`, then to a random + per-process value. + + + + Seconds a handle stays valid: long enough to outlive a page refresh, short enough that a session + left open overnight expires. + + + + Idle seconds before the service ends a conversation, sent on every create. Omit it for the + service default of 3600. Set it here rather than in the page: the service's responses don't + expose the deadline, so the gateway reports it to the widget on `start` and `log`. + + + + New conversations allowed per `windowSeconds`. The cap that matters most: a leaked key is used + to open many one-turn conversations, each billed for its opening turn. + + + + Turns one conversation may run, in total. + + + + The window `maxNewConversations` counts over, in seconds. + + + + Set `secret` explicitly if you run more than one replica or restart often. A random per-process + secret invalidates outstanding handles on restart, and a handle signed by one replica is refused + by another. + + +## Properties + + + The agent config sent on every call. Read-only. + + + + The publishable key the browser presents, whether you passed one or the gateway generated it. + Read-only. + + + + The listed origins, without trailing slashes. Read-only. + + + + Seconds a handle stays valid. + + + + Idle seconds before the service ends a conversation, or `null` for the service default. + + + + New conversations allowed per window. + + + + Turns allowed per conversation. + + + + Window for `maxNewConversations`, in seconds. + + + + The idle timeout reported to the widget. Never `null`: it falls back to the service default of + 3600, so the widget is never asked to warn about a deadline it wasn't told. Read-only. + + +## Exports + +All of these are importable from `@signalwire/sdk`. + + + A request the gateway refused. Carries `status`, the HTTP status to return (400, 401, 403, 413 or + 429), and `reason`, a short explanation that is safe to show the browser. Reasons name a category + and nothing finer, because anything finer would let a caller map the caps and the allowlist by + probing. + + + + The constructor's options object, described under Parameters. + + + + `65536` (64 KiB). The largest request body the router accepts. A larger body is answered with + `413` before it is parsed. + + + + `8192` (8 KiB of UTF-8). The largest chat message, refused with `413` before a conversation is + created or a turn is counted. [`HandoffRouter`][handoffrouter] applies the same limit to the + text of `/say`. + + + + `8192` (8 KiB). The largest `user_meta_data` object, measured serialized, refused with `413`. + + +## The browser-facing wire + +One endpoint: `POST /` on the router, with the key in `Authorization: Bearer`. Mounted at +`/chat`, the path is `/chat/`. A preflight `OPTIONS /` is answered for allowed origins. The +body is JSON with a `method` of `start`, `chat`, `log`, or `end`; `method` defaults to `chat`. + +| Method | Body fields | Returns | +|---|---|---| +| `start` | `handle?`, `user_meta_data?` | `{ greeting, status, timeout }` | +| `chat` | `handle?`, `message`, `user_meta_data?` | The service's JSON-RPC response, streamed through | +| `log` | `handle` | `{ messages, timeout, last_activity }` | +| `end` | `handle` | `{ status: 'ended' }` | + +A `start` or `chat` with no handle creates a conversation and returns its handle in the +`X-Chat-Handle` response header. `start` opens the conversation with no user message, so the +agent speaks first. `log` and `end` require a handle. Any other `method` is a `400`. + +Rejections return `{ error: reason }` with the status: `400` malformed request or handle, `401` +bad key, `403` origin not allowed or a bad or expired handle, `413` a request, message, or +metadata over its limit, `429` a cap was reached. A failure reaching the chat service before any +reply is sent returns `502` with `{ error: 'chat service error' }`. + +### Page context + +`user_meta_data` is the one field forwarded rather than replaced: the page context a widget +collects about itself, such as URL, title, referrer, locale, and viewport. It reaches the agent's +config request as `params.user_meta_data`, so a chat agent can tailor its greeting. + +The service reads it when it creates a conversation, which is the `start` call or whichever `chat` +auto-creates one. Metadata sent on later turns is accepted and unused. The browser writes it, so +treat it as the visitor's claim about themselves, never as authority; a page can put anything in it, +including text aimed at your prompt. + +## What a leaked key can do + +A publishable key is public by definition, so design for it being taken. + +A key alone can't read a conversation. The full `chat_log` isn't exposed, and handles are signed by +the gateway, so they can't be forged, guessed, or enumerated. `log` returns only the user and +assistant turns. + +It can start and continue conversations, and every turn is billed. `maxNewConversations` and +`maxTurns` bound that: the first limits conversations created per window, the second limits turns +in each. The caps are counted in the serving process. Behind several replicas each keeps its own, so +the effective cap multiplies by the replica count; set them with that in mind, or put a shared +limiter in front. + +The origin allowlist stops a key pasted into someone else's page, because a browser sends that +page's origin and the gateway refuses it. It doesn't stop a script that sends no origin or a false +one. Treat it as leak containment, not access control. + +To rotate a key, construct the gateway with a new `key` and redeploy. Change `secret` at the same +time to invalidate outstanding handles too. + +## Methods + + + + The Hono app to mount on your server. + + + Issue a signed handle server-side. + + + Verify a handle and return its conversation id. + + + Validate a browser request without the router. + + + The transcript a browser may redraw. + + + Epoch seconds of the newest message. + + + Close the client, when the gateway built it. + + + +### checkKey + +Verifies the publishable key a request presented, comparing in constant time. +[`router()`][router] and [`prepare()`][prepare] call it for you. + + + The key from the request, or `null` or `undefined` when absent. + + +Returns `void`. Throws `GatewayRejection` with status `401` and reason `bad key` when the key is +missing or wrong. + +### checkOrigin + +Allows localhost and listed origins, and refuses any other. A missing `Origin` is allowed: +browsers send one on the cross-origin requests this serves, so its absence means a caller that isn't +a browser, and refusing it would break server-side use without stopping an attacker. + + + The request's `Origin` header, or `null` or `undefined` when absent. + + +Returns `void`. Throws `GatewayRejection` with status `403` and reason `origin not allowed` for an +origin that isn't allowed. + +### readUserMetadata + +Validates the `user_meta_data` field of a browser request. Absent, `null`, and empty all yield +`null`. + +"} required={true} toc={true}> + The browser's request body. + + +Returns `Record | null`. Throws `GatewayRejection` with status `400` when the field +isn't a JSON object or can't be serialized, and `413` when it is larger than +`MAX_USER_METADATA_BYTES` serialized. + +## Example + +Checking a request's key and origin by hand, and reading its page context, in a handler you wrote +yourself: + +```typescript +import { ChatGateway, GatewayRejection } from '@signalwire/sdk'; + +const gateway = new ChatGateway({ + configUrl: 'https://bayview-taxi.example.com/dispatch', + key: '', + allowedOrigins: ['https://bayviewtaxi.example.com'], +}); + +export function guard(origin: string | undefined, authorization: string | undefined): void { + const key = authorization?.startsWith('Bearer ') ? authorization.slice(7) : null; + try { + gateway.checkKey(key); + gateway.checkOrigin(origin); + } catch (err) { + if (err instanceof GatewayRejection) { + console.warn(err.status, err.reason); // 401 bad key, 403 origin not allowed + } + throw err; + } +} + +export function pageContext(body: Record) { + return gateway.readUserMetadata(body); // Record | null +} +``` diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/chat-gateway/last-activity.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/chat-gateway/last-activity.mdx new file mode 100644 index 0000000000..bae6fdfc2a --- /dev/null +++ b/fern/products/server-sdks/pages/reference/typescript/agents/chat-gateway/last-activity.mdx @@ -0,0 +1,44 @@ +--- +title: "lastActivity" +slug: reference/typescript/agents/chat-gateway/last-activity +description: Get the epoch time of the newest message in a conversation. +max-toc-depth: 3 +--- + +[chatgateway]: /docs/server-sdks/reference/typescript/agents/chat-gateway +[visiblemessages]: /docs/server-sdks/reference/typescript/agents/chat-gateway/visible-messages +[log]: /docs/server-sdks/reference/typescript/agents/ai-chat-client/log + +A static method that returns when a conversation last moved, so a browser can restore its idle clock +after a reload. Without it a widget restarts the clock at zero: a tab closed for 55 minutes of a +60-minute timeout would wait another hour before warning, while the conversation ends in five. + +The service stamps messages in microseconds; this converts to seconds. Every role counts, not only +the ones [`visibleMessages()`][visiblemessages] keeps, because the service's idle clock moves on any +write. Messages without a positive integer `timestamp` are skipped. + +## Parameters + + + Messages as [`log()`][log] returns them in `messages`. + + +## Returns + +`number | null` -- epoch seconds of the newest dated message, or `null` when none is dated. + +## Examples + +```typescript +import { AIChatClient, ChatGateway } from '@signalwire/sdk'; + +const client = new AIChatClient({ space: 'your-space' }); + +const log = await client.log('chat-8f21'); +const last = ChatGateway.lastActivity(log.messages); + +if (last !== null) { + const idleSeconds = Date.now() / 1000 - last; + console.log(`Idle for ${Math.round(idleSeconds)} seconds`); +} +``` diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/chat-gateway/mint-handle.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/chat-gateway/mint-handle.mdx new file mode 100644 index 0000000000..0b6bf19c32 --- /dev/null +++ b/fern/products/server-sdks/pages/reference/typescript/agents/chat-gateway/mint-handle.mdx @@ -0,0 +1,44 @@ +--- +title: "mintHandle" +slug: reference/typescript/agents/chat-gateway/mint-handle +description: Issue a signed handle that names a conversation. +max-toc-depth: 3 +--- + +[chatgateway]: /docs/server-sdks/reference/typescript/agents/chat-gateway +[readhandle]: /docs/server-sdks/reference/typescript/agents/chat-gateway/read-handle + +Issues a signed handle for a conversation. The browser never names a conversation directly: if it +could, a publishable key and a guessed id would be enough to continue someone else's chat. With +signing, a caller can only present handles this gateway issued. + +A handle is valid for `handleTtl` seconds and is signed with the gateway's `secret`. +[`readHandle()`][readhandle] verifies one and returns the conversation id. + +## Parameters + + + The conversation to name. A new random id, prefixed `chat-`, when omitted or empty. + + +## Returns + +`string` -- the handle. + +## Examples + +```typescript +import { ChatGateway } from '@signalwire/sdk'; + +const gateway = new ChatGateway({ + configUrl: 'https://bayview-taxi.example.com/dispatch', + key: '', + secret: process.env.SIGNALWIRE_CHAT_GATEWAY_SECRET, +}); + +const fresh = gateway.mintHandle(); +const named = gateway.mintHandle('chat-8f21'); + +console.log(gateway.readHandle(fresh)); // chat- +console.log(gateway.readHandle(named)); // chat-8f21 +``` diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/chat-gateway/prepare.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/chat-gateway/prepare.mdx new file mode 100644 index 0000000000..b639258d16 --- /dev/null +++ b/fern/products/server-sdks/pages/reference/typescript/agents/chat-gateway/prepare.mdx @@ -0,0 +1,95 @@ +--- +title: "prepare" +slug: reference/typescript/agents/chat-gateway/prepare +description: Validate a browser request and build the call to the chat service. +max-toc-depth: 3 +--- + +[chatgateway]: /docs/server-sdks/reference/typescript/agents/chat-gateway +[rawpost]: /docs/server-sdks/reference/typescript/agents/ai-chat-client/raw-post +[router]: /docs/server-sdks/reference/typescript/agents/chat-gateway/router +[readhandle]: /docs/server-sdks/reference/typescript/agents/chat-gateway/read-handle + +Validates a browser request and builds the call to forward. [`router()`][router] uses it; call it +yourself to serve the gateway from a framework other than Hono. It checks the key, then the origin. + +Everything the browser could use to widen its own access is refused or replaced here: `method` must +be one of `start`, `chat`, `log`, or `end` (default `chat`), the conversation comes from a signed +handle, and `config_url` is the gateway's. The one field forwarded is `user_meta_data`, after the +checks described under [`ChatGateway`][chatgateway]. A chat message over 8 KiB of UTF-8 is refused +with `413` before a conversation is created or a turn is counted. + +`prepare()` doesn't check the size of the request body. [`router()`][router] limits it to 64 KiB +before parsing; apply a limit of your own when you call `prepare()` directly. + +A `log` or `end` request needs a handle. A `start` or `chat` without one creates a conversation, +which counts against `maxNewConversations`; a `chat` counts against `maxTurns`. + +## Parameters + +"} required={true} toc={true}> + The browser's request body: `{ method?, handle?, message?, user_meta_data? }`. + + + + The request's credentials. + + + + + + The `Origin` header. + + + + The presented publishable key. + + + + +## Returns + +`[string, Record, string | null]` -- a tuple of `[method, params, mintedHandle]`. + +`method` is the service method to call: `create_conversation` for `start`, `chat` for `chat`, +`chat_log` for `log`, and `end_conversation` for `end`. `params` are its parameters. `mintedHandle` +is the new handle when this call created the conversation, and `null` otherwise; return it to the +browser. Send the call with [`rawPost()`][rawpost]. + +## Throws + +`GatewayRejection` when the request is refused: `401` for a bad key, `403` for a disallowed origin +or a bad handle, `400` for a malformed request, `413` for an oversized message or metadata, and +`429` when a cap was reached. + +## Examples + +```typescript +import { AIChatClient, ChatGateway, GatewayRejection } from '@signalwire/sdk'; + +const gateway = new ChatGateway({ + configUrl: 'https://bayview-taxi.example.com/dispatch', + key: '', + allowedOrigins: ['https://bayviewtaxi.example.com'], +}); +const client = new AIChatClient({ space: 'your-space' }); + +export async function relay( + body: Record, + origin: string | null, + key: string | null, +): Promise { + try { + const [method, params, minted] = gateway.prepare(body, { origin, key }); + const upstream = await client.rawPost(method, params); + const headers = new Headers({ 'Content-Type': 'application/json' }); + if (minted) headers.set('X-Chat-Handle', minted); + return new Response(upstream.body, { status: 200, headers }); + } catch (err) { + if (err instanceof GatewayRejection) { + return Response.json({ error: err.reason }, { status: err.status }); + } + throw err; + } +} +``` diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/chat-gateway/read-handle.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/chat-gateway/read-handle.mdx new file mode 100644 index 0000000000..ad3d60697e --- /dev/null +++ b/fern/products/server-sdks/pages/reference/typescript/agents/chat-gateway/read-handle.mdx @@ -0,0 +1,59 @@ +--- +title: "readHandle" +slug: reference/typescript/agents/chat-gateway/read-handle +description: Verify a handle and return the conversation id inside it. +max-toc-depth: 3 +--- + +[chatgateway]: /docs/server-sdks/reference/typescript/agents/chat-gateway +[minthandle]: /docs/server-sdks/reference/typescript/agents/chat-gateway/mint-handle + +Returns the conversation id inside a handle from [`mintHandle()`][minthandle]. The signature is +checked first and the expiry second, both before the id is used. + +## Parameters + + + A handle this gateway issued. + + +## Returns + +`string` -- the conversation id. + +## Throws + +`GatewayRejection`, with a `status` and a coarse `reason`: + +| Status | Reason | Cause | +|---|---|---| +| `400` | `malformed handle` | The handle isn't in the expected shape. | +| `403` | `invalid handle` | The signature doesn't verify. | +| `403` | `expired handle` | The handle is older than `handleTtl`. | + +## Examples + +```typescript +import { ChatGateway, GatewayRejection } from '@signalwire/sdk'; + +const gateway = new ChatGateway({ + configUrl: 'https://bayview-taxi.example.com/dispatch', + key: '', + secret: process.env.SIGNALWIRE_CHAT_GATEWAY_SECRET, +}); + +function conversationFor(handle: string): string | null { + try { + return gateway.readHandle(handle); + } catch (err) { + if (err instanceof GatewayRejection) { + console.warn(`handle refused: ${err.status} ${err.reason}`); + return null; + } + throw err; + } +} + +console.log(conversationFor(gateway.mintHandle('chat-8f21'))); // chat-8f21 +console.log(conversationFor('not-a-handle')); // null +``` diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/chat-gateway/router.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/chat-gateway/router.mdx new file mode 100644 index 0000000000..4c676c0241 --- /dev/null +++ b/fern/products/server-sdks/pages/reference/typescript/agents/chat-gateway/router.mdx @@ -0,0 +1,76 @@ +--- +title: "router" +slug: reference/typescript/agents/chat-gateway/router +description: Build the Hono app that exposes the gateway to a browser. +max-toc-depth: 3 +--- + +[chatgateway]: /docs/server-sdks/reference/typescript/agents/chat-gateway +[handoffrouter]: /docs/server-sdks/reference/typescript/agents/handoff-router +[aichatclient]: /docs/server-sdks/reference/typescript/agents/ai-chat-client + +Returns a Hono app that serves the gateway at `/`: `POST /` for the four browser methods +(`start`, `chat`, `log`, `end`) and `OPTIONS /` for the CORS preflight. Mount it with +`agent.mount(gateway.router(), { prefix: '/chat' })`, or with `app.route()` on any Hono app. + +A `chat` reply is streamed through without buffering. The service pads slow turns with keepalive +whitespace so proxies don't close the connection, and collecting the body here would hold that +padding back. A newly created handle is returned in the `X-Chat-Handle` response header, which is +why it can be sent before the body. + +CORS headers go to origins the gateway allows, and `X-Chat-Handle` is exposed to them. A request +body over 64 KiB is answered with `413` and `{ error: 'request too large' }` without being +parsed. See [`ChatGateway`][chatgateway] for the methods, bodies, and status codes. + +## Parameters + +None. + +## Returns + +`Hono` -- the app. `hono` is installed with `@signalwire/sdk`. + +## Examples + +Mount the gateway on an agent, with the handoff routes at the same prefix: + +```typescript +import { AgentBase, ChatGateway, HandoffRouter } from '@signalwire/sdk'; + +const agent = new AgentBase({ name: 'dispatch', route: '/dispatch' }); +const gateway = new ChatGateway({ + configUrl: 'https://bayview-taxi.example.com/dispatch', + key: '', + allowedOrigins: ['https://bayviewtaxi.example.com'], +}); + +agent.mount(gateway.router(), { prefix: '/chat' }); + +// Optional: let the widget move a conversation to a phone call and back. +const handoff = new HandoffRouter({ gateway }); +agent.mount(handoff.router(), { prefix: '/chat' }); + +await agent.serve(); +``` + +Mount it on a Hono app you already run: + +```typescript +import { serve } from '@hono/node-server'; +import { Hono } from 'hono'; +import { ChatGateway } from '@signalwire/sdk'; + +const gateway = new ChatGateway({ + configUrl: 'https://bayview-taxi.example.com/dispatch', + key: '', + allowedOrigins: ['https://bayviewtaxi.example.com'], +}); + +const app = new Hono(); +app.route('/chat', gateway.router()); + +serve({ fetch: app.fetch, port: 3000 }); +``` + +To let the widget move a conversation to a phone call and back, mount a +[`HandoffRouter`][handoffrouter] at the same prefix. diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/chat-gateway/visible-messages.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/chat-gateway/visible-messages.mdx new file mode 100644 index 0000000000..00365a8040 --- /dev/null +++ b/fern/products/server-sdks/pages/reference/typescript/agents/chat-gateway/visible-messages.mdx @@ -0,0 +1,44 @@ +--- +title: "visibleMessages" +slug: reference/typescript/agents/chat-gateway/visible-messages +description: Reduce a conversation log to the turns a browser may redraw. +max-toc-depth: 3 +--- + +[chatgateway]: /docs/server-sdks/reference/typescript/agents/chat-gateway +[lastactivity]: /docs/server-sdks/reference/typescript/agents/chat-gateway/last-activity +[log]: /docs/server-sdks/reference/typescript/agents/ai-chat-client/log + +A static method that returns the transcript a browser may redraw, and nothing else. The service's +log holds the substituted system prompt and the tool calls and results alongside the dialogue, so +relaying it would publish your prompt to anyone with a handle. + +Only `user` and `assistant` messages with non-blank text are kept. Each is reduced to `role`, +`content` and, when the source message is dated, `timestamp` in epoch seconds, so a restored +transcript isn't shown as sent now. The gateway's `log` method uses this before it answers, and +[`lastActivity()`][lastactivity] reads the same log for the idle clock. + +## Parameters + + + Messages as [`log()`][log] returns them in `messages`. + + +## Returns + +`Array>` -- the visible turns, in order. + +## Examples + +```typescript +import { AIChatClient, ChatGateway } from '@signalwire/sdk'; + +const client = new AIChatClient({ space: 'your-space' }); + +const log = await client.log('chat-8f21'); +const transcript = ChatGateway.visibleMessages(log.messages); + +for (const turn of transcript) { + console.log(`${String(turn.role)}: ${String(turn.content)}`); +} +``` diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/handoff-router/escalate.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/handoff-router/escalate.mdx new file mode 100644 index 0000000000..622b8c840f --- /dev/null +++ b/fern/products/server-sdks/pages/reference/typescript/agents/handoff-router/escalate.mdx @@ -0,0 +1,45 @@ +--- +title: "escalate" +slug: reference/typescript/agents/handoff-router/escalate +description: End a chat leg and wait for its record before a call is placed. +max-toc-depth: 3 +--- + +[handoffrouter]: /docs/server-sdks/reference/typescript/agents/handoff-router +[readhandle]: /docs/server-sdks/reference/typescript/agents/chat-gateway/read-handle + +Ends a chat leg and waits for your `captureLeg` callback to write its record, so a voice leg started +right after finds the text leg already recorded. The browser waits for this before it places the +call. The `/escalate` route calls this method. + +The wait is bounded by `captureTimeout`. If `captureLeg` throws or runs past the timeout, the +failure is logged and `escalate()` still returns `true`. + +## Parameters + + + The chat leg's handle. + + +## Returns + +`Promise` -- `false` when the handle doesn't verify, `true` otherwise. + +## Examples + +```typescript +import { ChatGateway, HandoffRouter } from '@signalwire/sdk'; + +const gateway = new ChatGateway({ configUrl: 'https://bayview-taxi.example.com/dispatch' }); +const handoff = new HandoffRouter({ + gateway, + captureLeg: async (conversationId, medium) => { + console.log(`saving ${medium} leg of ${conversationId}`); + return true; + }, +}); + +const ok = await handoff.escalate(gateway.mintHandle('chat-8f21')); +console.log(ok); // true +console.log(await handoff.escalate('not-a-handle')); // false +``` diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/handoff-router/index.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/handoff-router/index.mdx new file mode 100644 index 0000000000..afd5774836 --- /dev/null +++ b/fern/products/server-sdks/pages/reference/typescript/agents/handoff-router/index.mdx @@ -0,0 +1,288 @@ +--- +title: "HandoffRouter" +slug: reference/typescript/agents/handoff-router +description: The three routes a browser chat widget calls to move a conversation between text and a phone call. +max-toc-depth: 3 +--- + +[chatgateway]: /docs/server-sdks/reference/typescript/agents/chat-gateway +[register]: /docs/server-sdks/reference/typescript/agents/handoff-router/register +[redeem]: /docs/server-sdks/reference/typescript/agents/handoff-router/redeem +[escalate]: /docs/server-sdks/reference/typescript/agents/handoff-router/escalate +[say]: /docs/server-sdks/reference/typescript/agents/handoff-router/say +[router]: /docs/server-sdks/reference/typescript/agents/handoff-router/router +[readhandle]: /docs/server-sdks/reference/typescript/agents/chat-gateway/read-handle +[dynamicconfig]: /docs/server-sdks/reference/typescript/agents/agent-base/add-per-call-config + +[`ChatGateway`][chatgateway] lets a browser hold a text conversation. `HandoffRouter` is the other +half the SignalWire address widget expects: three routes, served at the same URL prefix as the +gateway, that move that conversation to a phone call and back, and let a visitor type into a live +call. Without these routes, a gateway answers the widget's chat requests and returns `404` for the +rest. + +| Route | Body | Effect | +|---|---|---| +| `POST {prefix}/handoff` | `{ nonce }` | End the call, wait for its record, return a chat handle for the next leg | +| `POST {prefix}/escalate` | `{ handle }` | End the chat leg and wait for its record before a call is placed | +| `POST {prefix}/say` | `{ nonce, text }` | Deliver typed text into the live call | + +Every route checks the request origin against the gateway's allowlist. Unknown, expired, and +already-redeemed nonces all answer `404`, so a caller can't probe whether a given call is live. + +This class implements the wire contract: the routes, the nonce, the ordering guarantee, and the +spend limits. What a conversation is belongs to your application, and arrives as callbacks: where a +leg's transcript is written, and how a call is ended or spoken into. + + + +## How the nonce works + +A browser can't be trusted to name a call, since a page-supplied call ID would let anyone inject +speech into a stranger's call. Instead the browser proves which call it is on. Your application puts +a random `handoff_nonce` in the user variables of one dial, [registers][register] it against that +call from a per-call config callback (see [`addPerCallConfig()`][dynamicconfig]), and the +browser presents it later. The same dial carries the `chat_handle`, so the callback recovers the +conversation id with [`readHandle()`][readhandle]. + +Exchanging a nonce for a chat handle works once. Typing is repeatable, up to `maxMessagesPerCall`, +until `/handoff` uses the nonce or `nonceTtl` seconds pass from its registration, even if the call is +still live. + +## The ordering guarantee + +A new medium doesn't start until the one it replaces has ended and its record is written. +`/handoff` ends the call and waits for your `captureLeg` callback to confirm the capture before it +issues a handle. `/escalate` waits for the chat leg's capture before it returns. Without the wait, +the new medium's config fetch can run before the record exists, and the new leg starts with no +history. + + +The registry lives in the serving process, like the gateway's caps. A redemption must reach the +replica that served the dial: run one replica, use sticky routing, or pass a shared `registry`. +Registering, redeeming, and taking a typing slot each read the table and write it back, so across +replicas that share a `registry` a nonce can be redeemed once by each, and the typing cap can be +passed. Use one nonce table, or route each call's requests to one replica. + + +## Parameters + +The constructor takes one options object, `HandoffRouterOptions`. + + + The gateway that owns the conversations. Used to issue and read handles and to check origins, so + all routes on the URL apply the same origin policy. + + + + Ends a leg and writes its record: `captureLeg(conversationId, medium)`, where `medium` is `voice` + or `chat`. Return `true` only once the record is written. Sync or async. When omitted, nothing + waits and the ordering guarantee doesn't hold. + + + + Hangs a call up server-side so its teardown hooks run now: `endCall(callId)`. + + + + Delivers typed text into a live call as if the caller said it: `sendMessage(callId, text)`. When + omitted, typing is off and `/say` returns `404`. + + + + Returns the id for the new leg, from the current one. An ended conversation can't be reopened, so + each leg needs a new id. Defaults to appending `.1`, or incrementing an existing `.N` suffix: `root` + becomes `root.1`, and `root.2` becomes `root.3`. Keep `.` as the separator. + + + + Seconds a nonce stays usable for `/handoff` and `/say`, from registration. + + + + Typed messages allowed per call. Each is a billed turn, so this limits spending as well as abuse. + + + + Seconds to wait for `captureLeg`. An upper limit; a capture normally takes under a second. On + timeout the next medium starts without this leg's record, and a warning is logged. + + + + The nonce table. Supply one backed by shared storage to run more than one replica. It may return + copies of its entries, because the router writes every change back with `set()`. + + +## Properties + +`gateway`, `captureLeg`, `endCall`, `sendMessage`, `nextConversationId`, `nonceTtl`, +`maxMessagesPerCall`, and `captureTimeout` are readable and writable on the instance. `captureLeg`, +`endCall`, and `sendMessage` are `null` when not set. + +## Exports + +All of these are importable from `@signalwire/sdk`. + + + What a registered nonce gives access to. + + + + + + The conversation the call belongs to. + + + + The platform call id, from the request the platform sent. + + + + When it was registered, in monotonic seconds, used for expiry. + + + + Typed messages delivered so far. + + + + `true` once `/handoff` has used the nonce. The entry stays in the table until it would have + expired, so registering the nonce again can't make it usable again. + + + + + + The type of `captureLeg`. + + + + The type of `endCall`. + + + + The type of `sendMessage`. + + + + The constructor's options object, described under Parameters. + + +`MAX_REQUEST_BODY_BYTES` (64 KiB) and `MAX_MESSAGE_BYTES` (8 KiB of UTF-8) apply to these routes as +they do to the gateway. A body over the first, or `/say` text over the second, is answered with +`413`. Both are checked before the nonce is looked up, so the answer says nothing about whether the +nonce is live. + +## Methods + + + + Record what a nonce gives access to. + + + Exchange a nonce for a chat handle. Single use. + + + End a chat leg and wait for its record. + + + Deliver typed text into a live call. + + + The Hono app to mount beside the gateway's. + + + +## Examples + +Register the nonce from the per-call config callback of the dial that carried it, reading the call +id from the platform's request and never from anything the browser sent. + +```typescript +import { AgentBase, ChatGateway, HandoffRouter, userVariables } from '@signalwire/sdk'; + +const agent = new AgentBase({ name: 'dispatch', route: '/dispatch' }); +agent.setPromptText('You are Ada, the dispatcher for Bayview Taxi.'); + +const gateway = new ChatGateway({ + configUrl: 'https://bayview-taxi.example.com/dispatch', + key: '', + allowedOrigins: ['https://bayviewtaxi.example.com'], +}); + +const handoff = new HandoffRouter({ + gateway, + // Write the leg's transcript, then return true once it is saved. + captureLeg: async (conversationId, medium) => { + console.log(`saving ${medium} leg of ${conversationId}`); + return true; + }, + // End the call server-side. + endCall: async (callId) => { + console.log(`hanging up ${callId}`); + }, + // Speak typed text into the call. + sendMessage: async (callId, text) => { + console.log(`sending to ${callId}: ${text}`); + return true; + }, +}); + +agent.addPerCallConfig((_query, body) => { + const variables = userVariables(body); + const nonce = variables['handoff_nonce']; + const chatHandle = variables['chat_handle']; + const call = body['call'] as { call_id?: string } | undefined; + if (typeof nonce === 'string' && typeof chatHandle === 'string') { + handoff.register(nonce, { + conversationId: gateway.readHandle(chatHandle), + callId: call?.call_id ?? null, + }); + } +}); + +agent.mount(gateway.router(), { prefix: '/chat' }); +agent.mount(handoff.router(), { prefix: '/chat' }); + +await agent.serve(); +``` + +A `captureLeg`, `endCall`, or `sendMessage` that throws is logged and treated as a failure. A +`sendMessage` that throws or returns `false` doesn't count against `maxMessagesPerCall`. + +The option types and a shared registry: + +```typescript +import { HandoffRouter, NonceEntry, ChatGateway } from '@signalwire/sdk'; +import type { CaptureLeg, EndCall, SendMessage, HandoffRouterOptions } from '@signalwire/sdk'; + +const gateway = new ChatGateway({ configUrl: 'https://bayview-taxi.example.com/dispatch' }); + +const captureLeg: CaptureLeg = async (conversationId, medium) => { + console.log(conversationId, medium); + return true; +}; +const endCall: EndCall = (callId) => { + console.log(callId); +}; +const sendMessage: SendMessage = (callId, text) => { + console.log(callId, text); + return true; +}; + +// A shared nonce table: any Map works. +const registry = new Map(); + +const options: HandoffRouterOptions = { + gateway, + captureLeg, + endCall, + sendMessage, + nextConversationId: (id) => `${id}.next`, + nonceTtl: 1800, + maxMessagesPerCall: 100, + captureTimeout: 5, + registry, +}; +const handoff = new HandoffRouter(options); +console.log(handoff.nonceTtl, handoff.maxMessagesPerCall, handoff.captureTimeout, new NonceEntry('chat-8f21', 'call-1')); +``` diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/handoff-router/redeem.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/handoff-router/redeem.mdx new file mode 100644 index 0000000000..dbaf858112 --- /dev/null +++ b/fern/products/server-sdks/pages/reference/typescript/agents/handoff-router/redeem.mdx @@ -0,0 +1,50 @@ +--- +title: "redeem" +slug: reference/typescript/agents/handoff-router/redeem +description: Exchange a handoff nonce for a chat handle, once. +max-toc-depth: 3 +--- + +[handoffrouter]: /docs/server-sdks/reference/typescript/agents/handoff-router +[register]: /docs/server-sdks/reference/typescript/agents/handoff-router/register +[minthandle]: /docs/server-sdks/reference/typescript/agents/chat-gateway/mint-handle + +Exchanges a nonce for a chat handle. It ends the call, waits for your `captureLeg` callback to write +the call's record, and only then issues a handle for a new leg of the same conversation, so the chat +leg starts with the call's history. + +The nonce is used up as soon as `redeem()` accepts it, even if what follows fails: a nonce is one +attempt. The `/handoff` route calls this method. A throwing `endCall` or `captureLeg`, or a capture +that outlasts `captureTimeout`, is logged and the handle is still issued. + +## Parameters + + + The nonce the browser presented, registered earlier with [`register()`][register]. + + +## Returns + +`Promise` -- the handle, or `null` for a nonce that is unknown, expired, or already +used. The three can't be told apart. The new leg's conversation id comes from `nextConversationId`. + +## Examples + +```typescript +import { ChatGateway, HandoffRouter } from '@signalwire/sdk'; + +const gateway = new ChatGateway({ configUrl: 'https://bayview-taxi.example.com/dispatch' }); +const handoff = new HandoffRouter({ + gateway, + captureLeg: async () => true, + endCall: async () => undefined, +}); +handoff.register('n_7f3a91c2', { conversationId: 'chat-8f21', callId: 'call-1' }); + +const handle = await handoff.redeem('n_7f3a91c2'); +if (handle === null) { + console.log('Unknown, expired, or already used'); +} else { + console.log(gateway.readHandle(handle)); // chat-8f21.1 +} +``` diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/handoff-router/register.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/handoff-router/register.mdx new file mode 100644 index 0000000000..033d8db82d --- /dev/null +++ b/fern/products/server-sdks/pages/reference/typescript/agents/handoff-router/register.mdx @@ -0,0 +1,61 @@ +--- +title: "register" +slug: reference/typescript/agents/handoff-router/register +description: Record what a handoff nonce gives access to. +max-toc-depth: 3 +--- + +[handoffrouter]: /docs/server-sdks/reference/typescript/agents/handoff-router +[readhandle]: /docs/server-sdks/reference/typescript/agents/chat-gateway/read-handle +[redeem]: /docs/server-sdks/reference/typescript/agents/handoff-router/redeem + +Records which conversation and call a nonce belongs to. Call it from the per-call config callback of +the dial that carried the nonce, with `callId` from the request the platform sent, never from +anything the browser supplied. + +Registering a nonce that's already in the table changes nothing, because the callback runs on every +config request for the call. The first registration's call, time, and message count stand, and a +nonce [`redeem()`][redeem] has used stays used until it would have expired. An empty or non-string +nonce is ignored. + +## Parameters + + + The nonce put in the dial's user variables. + + + + What the nonce gives access to. + + + + + + The conversation the call belongs to. + + + + The platform call id. Without one, `/handoff` has no call to end and `/say` has no call to speak + into. + + + + +## Returns + +`void`. + +## Examples + +```typescript +import { ChatGateway, HandoffRouter } from '@signalwire/sdk'; + +const gateway = new ChatGateway({ configUrl: 'https://bayview-taxi.example.com/dispatch' }); +const handoff = new HandoffRouter({ gateway }); + +// From the per-call config callback of the dial that carried the nonce. +handoff.register('n_7f3a91c2', { + conversationId: 'chat-8f21', + callId: 'b2c0f1e4-5a6d-4f0e-9d1b-3c7e8a9f0b12', +}); +``` diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/handoff-router/router.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/handoff-router/router.mdx new file mode 100644 index 0000000000..0aa8d6eda8 --- /dev/null +++ b/fern/products/server-sdks/pages/reference/typescript/agents/handoff-router/router.mdx @@ -0,0 +1,59 @@ +--- +title: "router" +slug: reference/typescript/agents/handoff-router/router +description: Build the Hono app that serves the handoff routes. +max-toc-depth: 3 +--- + +[handoffrouter]: /docs/server-sdks/reference/typescript/agents/handoff-router +[chatgateway]: /docs/server-sdks/reference/typescript/agents/chat-gateway +[redeem]: /docs/server-sdks/reference/typescript/agents/handoff-router/redeem +[escalate]: /docs/server-sdks/reference/typescript/agents/handoff-router/escalate +[say]: /docs/server-sdks/reference/typescript/agents/handoff-router/say + +Returns a Hono app that serves `POST /handoff`, `/escalate`, and `/say`. Mount it at the same prefix +as the [gateway's router][chatgateway], because the browser derives all three paths from the +gateway's URL. + +| Route | Body | Returns | Calls | +|---|---|---|---| +| `/handoff` | `{ nonce }` | `{ handle }` | [`redeem()`][redeem] | +| `/escalate` | `{ handle }` | `{ ok: true }` | [`escalate()`][escalate] | +| `/say` | `{ nonce, text }` | `{ ok: true }` | [`say()`][say] | + +Each route returns `403` for a refused origin. It returns `404` with `{ error: 'not found' }` when +the nonce or handle doesn't verify, and `/escalate` returns `400` with `{ error: 'bad request' }` +when `handle` is missing. A body over 64 KiB is answered with `413` and +`{ error: 'request too large' }`, and `/say` text over 8 KiB with `413` and +`{ error: 'message too large' }`. Both are checked before the nonce is looked up. + +Each route answers its own CORS preflight, and every answer carries CORS headers for an origin the +gateway allows. + +## Parameters + +None. + +## Returns + +`Hono` -- the app. + +## Examples + +```typescript +import { AgentBase, ChatGateway, HandoffRouter } from '@signalwire/sdk'; + +const agent = new AgentBase({ name: 'dispatch', route: '/dispatch' }); +const gateway = new ChatGateway({ + configUrl: 'https://bayview-taxi.example.com/dispatch', + key: '', + allowedOrigins: ['https://bayviewtaxi.example.com'], +}); +const handoff = new HandoffRouter({ gateway }); + +// Same prefix: the widget derives all three paths from the gateway's URL. +agent.mount(gateway.router(), { prefix: '/chat' }); +agent.mount(handoff.router(), { prefix: '/chat' }); + +await agent.serve(); +``` diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/handoff-router/say.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/handoff-router/say.mdx new file mode 100644 index 0000000000..b427edd486 --- /dev/null +++ b/fern/products/server-sdks/pages/reference/typescript/agents/handoff-router/say.mdx @@ -0,0 +1,56 @@ +--- +title: "say" +slug: reference/typescript/agents/handoff-router/say +description: Deliver typed text into the live call a nonce names. +max-toc-depth: 3 +--- + +[handoffrouter]: /docs/server-sdks/reference/typescript/agents/handoff-router +[register]: /docs/server-sdks/reference/typescript/agents/handoff-router/register +[redeem]: /docs/server-sdks/reference/typescript/agents/handoff-router/redeem + +Delivers typed text into the call the nonce names, through your `sendMessage` callback. It doesn't +use up the nonce: typing works until [`redeem()`][redeem] uses it or `nonceTtl` seconds pass from +its registration, even if the call is still live. The `/say` route calls this method. + +The call is found by nonce, never by a browser-supplied call id, and no other request field is +forwarded. Surrounding whitespace is removed from the text. Each delivered message takes one of the +call's `maxMessagesPerCall` slots; a delivery that throws or returns `false` gives its slot back. + +## Parameters + + + The nonce the browser presented, registered earlier with [`register()`][register]. + + + + The typed text. Text over 8 KiB of UTF-8 (`MAX_MESSAGE_BYTES`), or empty after trimming, isn't + delivered. + + +## Returns + +`Promise` -- `true` when the text was delivered, meaning `sendMessage` neither threw nor +returned `false`. `false` when typing is off (no `sendMessage`), the text is empty or too large, the +nonce is unknown, expired, or used, the registration has no call id, or the call has reached +`maxMessagesPerCall`. + +## Examples + +```typescript +import { ChatGateway, HandoffRouter } from '@signalwire/sdk'; + +const gateway = new ChatGateway({ configUrl: 'https://bayview-taxi.example.com/dispatch' }); +const handoff = new HandoffRouter({ + gateway, + maxMessagesPerCall: 50, + sendMessage: async (callId, text) => { + console.log(`to ${callId}: ${text}`); + return true; + }, +}); +handoff.register('n_7f3a91c2', { conversationId: 'chat-8f21', callId: 'call-1' }); + +const delivered = await handoff.say('n_7f3a91c2', 'My pickup is 123 Gough Street.'); +console.log(delivered ? 'Delivered' : 'Not delivered'); +``` diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/overview.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/overview.mdx index 194fdc921f..bf1f475156 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/overview.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/overview.mdx @@ -148,6 +148,18 @@ await agent.run(); > Hold a text conversation with an AI agent from your own server. + + Let a browser chat widget talk to an agent without holding an API token. + + + Move a conversation between a phone call and chat, and type into a live call. + Date: Thu, 22 Oct 2026 19:33:47 +0000 Subject: [PATCH 02/15] docs(server-sdks): add TypeScript capabilities, post-prompt, and SHA-256 webhook helpers --- .../capabilities/declared-capabilities.mdx | 57 ++++++++ .../core/capabilities/has-capability.mdx | 49 +++++++ .../typescript/core/capabilities/index.mdx | 76 ++++++++++ .../core/capabilities/user-variables.mdx | 46 ++++++ .../core/post-prompt/dialogue-turns.mdx | 77 ++++++++++ .../typescript/core/post-prompt/index.mdx | 135 ++++++++++++++++++ .../post-prompt/normalize-post-prompt.mdx | 64 +++++++++ .../post-prompt/parse-post-prompt-data.mdx | 62 ++++++++ .../core/post-prompt/strip-json-fence.mdx | 42 ++++++ .../typescript/core/security/index.mdx | 18 ++- .../core/security/validate-request.mdx | 5 +- .../validate-webhook-signature-sha256.mdx | 98 +++++++++++++ .../security/validate-webhook-signature.mdx | 7 + .../typescript/core/security/validate.mdx | 10 +- .../webhook-validation-middleware.mdx | 9 +- 15 files changed, 747 insertions(+), 8 deletions(-) create mode 100644 fern/products/server-sdks/pages/reference/typescript/core/capabilities/declared-capabilities.mdx create mode 100644 fern/products/server-sdks/pages/reference/typescript/core/capabilities/has-capability.mdx create mode 100644 fern/products/server-sdks/pages/reference/typescript/core/capabilities/index.mdx create mode 100644 fern/products/server-sdks/pages/reference/typescript/core/capabilities/user-variables.mdx create mode 100644 fern/products/server-sdks/pages/reference/typescript/core/post-prompt/dialogue-turns.mdx create mode 100644 fern/products/server-sdks/pages/reference/typescript/core/post-prompt/index.mdx create mode 100644 fern/products/server-sdks/pages/reference/typescript/core/post-prompt/normalize-post-prompt.mdx create mode 100644 fern/products/server-sdks/pages/reference/typescript/core/post-prompt/parse-post-prompt-data.mdx create mode 100644 fern/products/server-sdks/pages/reference/typescript/core/post-prompt/strip-json-fence.mdx create mode 100644 fern/products/server-sdks/pages/reference/typescript/core/security/validate-webhook-signature-sha256.mdx diff --git a/fern/products/server-sdks/pages/reference/typescript/core/capabilities/declared-capabilities.mdx b/fern/products/server-sdks/pages/reference/typescript/core/capabilities/declared-capabilities.mdx new file mode 100644 index 0000000000..353a213fcc --- /dev/null +++ b/fern/products/server-sdks/pages/reference/typescript/core/capabilities/declared-capabilities.mdx @@ -0,0 +1,57 @@ +--- +title: "declaredCapabilities" +slug: reference/typescript/core/capabilities/declared-capabilities +description: "Return the capability names a client declared as truthy." +max-toc-depth: 3 +--- + +[ref-capabilities]: /docs/server-sdks/reference/typescript/core/capabilities +[user-variables]: /docs/server-sdks/reference/typescript/core/capabilities/user-variables + +Return the names the client declared as truthy under `capabilities`. It accepts either a whole SWML +request body or user variables already extracted with [`userVariables()`][user-variables], so it +works from a dynamic-config callback and from a tool handler alike. + +A capability counts only when its value is truthy. `false`, `0`, an empty string, an empty array, and +an empty object all mean "not declared". + +```typescript {1} +declaredCapabilities(bodyParams: unknown): ReadonlySet +``` + +## Parameters + + + A SWML request body, or a user variables object. + + +## Returns + +`ReadonlySet` -- The declared names. Empty when nothing was declared, the payload was +malformed, or the client isn't a browser at all. Never throws. + +## Examples + +```typescript {11} +import { declaredCapabilities } from '@signalwire/sdk'; + +const body = { + vars: { + userVariables: { + capabilities: { display_content: true, transcript: 1, chat_handoff: false, tags: [] }, + }, + }, +}; + +console.log([...declaredCapabilities(body)]); +// [ 'display_content', 'transcript' ] + +// User variables passed directly give the same result. +console.log([...declaredCapabilities(body.vars.userVariables)]); +// [ 'display_content', 'transcript' ] + +console.log(declaredCapabilities(null).size); +// 0 +``` + +Part of the [client capabilities][ref-capabilities] helpers. diff --git a/fern/products/server-sdks/pages/reference/typescript/core/capabilities/has-capability.mdx b/fern/products/server-sdks/pages/reference/typescript/core/capabilities/has-capability.mdx new file mode 100644 index 0000000000..990fedba1d --- /dev/null +++ b/fern/products/server-sdks/pages/reference/typescript/core/capabilities/has-capability.mdx @@ -0,0 +1,49 @@ +--- +title: "hasCapability" +slug: reference/typescript/core/capabilities/has-capability +description: "Whether a client explicitly declared one named capability." +max-toc-depth: 3 +--- + +[ref-capabilities]: /docs/server-sdks/reference/typescript/core/capabilities +[declared-capabilities]: /docs/server-sdks/reference/typescript/core/capabilities/declared-capabilities + +Whether the client declared `name`. A convenience over +[`declaredCapabilities()`][declared-capabilities] for a single check. + +```typescript {1} +hasCapability(bodyParams: unknown, name: string): boolean +``` + +## Parameters + + + A SWML request body, or a user variables object. + + + + Capability name, such as `'display_content'`. + + +## Returns + +`boolean` -- `true` only when the capability was explicitly declared truthy. + +## Examples + +```typescript {7,9,11} +import { hasCapability } from '@signalwire/sdk'; + +const body = { + vars: { userVariables: { capabilities: { transcript: true, chat_handoff: false } } }, +}; + +console.log(hasCapability(body, 'transcript')); +// true +console.log(hasCapability(body, 'chat_handoff')); +// false +console.log(hasCapability(body, 'display_content')); +// false +``` + +Part of the [client capabilities][ref-capabilities] helpers. diff --git a/fern/products/server-sdks/pages/reference/typescript/core/capabilities/index.mdx b/fern/products/server-sdks/pages/reference/typescript/core/capabilities/index.mdx new file mode 100644 index 0000000000..cf28eac898 --- /dev/null +++ b/fern/products/server-sdks/pages/reference/typescript/core/capabilities/index.mdx @@ -0,0 +1,76 @@ +--- +title: "Client capabilities" +slug: reference/typescript/core/capabilities +description: "Read the rendering capabilities a browser client declares in its user variables at dial time." +max-toc-depth: 3 +--- + +[set-dynamic-config-callback]: /docs/server-sdks/reference/typescript/agents/agent-base/set-dynamic-config-callback + +A browser client such as the SignalWire address widget declares what it can render in the user +variables it sends at dial time. The capabilities helpers read those declarations so an agent can +decide what to offer: whether to push content to a screen, whether to advertise a text-handoff tool. + +```json +{ + "vars": { + "userVariables": { + "capabilities": { + "display_content": true, + "transcript": true, + "chat_handoff": false + } + } + } +} +``` + + +A capability says what the client can render. It is never permission to do anything privileged, +because a caller controls its own user variables. Use these values as hints for what to offer. + + +Absence means no. Every function resolves errors and missing data to "not declared", because +offering a caller something they can't reach is worse than never mentioning it: a phone caller has no +screen. There is no fixed list of capability names; a client can declare a name this SDK has never +seen and your application can act on it. + +```typescript +import { userVariables, declaredCapabilities, hasCapability } from '@signalwire/sdk'; +``` + +## Functions + + + + Return the user variables from a SWML request body. + + + Return the capability names the client declared as truthy. + + + Whether the client declared one named capability. + + + +## Examples + +Offer a screen-only prompt section to callers who can render it. The request body is the third +argument of the [`setDynamicConfigCallback()`][set-dynamic-config-callback] callback: + +```typescript {6-10} +import { AgentBase, hasCapability } from '@signalwire/sdk'; + +const agent = new AgentBase({ name: 'dispatch', route: '/dispatch' }); +agent.setPromptText('You are Ada, the dispatcher for Bayview Taxi.'); + +agent.setDynamicConfigCallback((_queryParams, bodyParams, _headers, agentCopy) => { + if (hasCapability(bodyParams, 'display_content')) { + agentCopy.promptAddSection('Screen', { + body: 'The caller can see a screen. Offer to show the fare breakdown there.', + }); + } +}); + +await agent.serve(); +``` diff --git a/fern/products/server-sdks/pages/reference/typescript/core/capabilities/user-variables.mdx b/fern/products/server-sdks/pages/reference/typescript/core/capabilities/user-variables.mdx new file mode 100644 index 0000000000..258027360f --- /dev/null +++ b/fern/products/server-sdks/pages/reference/typescript/core/capabilities/user-variables.mdx @@ -0,0 +1,46 @@ +--- +title: "userVariables" +slug: reference/typescript/core/capabilities/user-variables +description: "Return the user variables nested inside a SWML request body." +max-toc-depth: 3 +--- + +[ref-capabilities]: /docs/server-sdks/reference/typescript/core/capabilities + +Return the user variables from a SWML request body. They are nested two levels down, at +`vars.userVariables`, which is easy to get wrong silently: a missing level yields an empty object and +every downstream check quietly reports "not declared". + +```typescript {1} +userVariables(bodyParams: unknown): Record +``` + +## Parameters + + + The SWML request body, as passed to a dynamic-config callback or SWAIG handler. + + +## Returns + +`Record` -- The user variables, or `{}` when absent or malformed. Never throws. + +## Examples + +```typescript {7} +import { userVariables } from '@signalwire/sdk'; + +const body = { + vars: { userVariables: { handoff_nonce: 'n-4821', capabilities: { transcript: true } } }, +}; + +const variables = userVariables(body); +console.log(variables['handoff_nonce']); +// n-4821 +console.log(userVariables({ vars: {} })); +// {} +console.log(userVariables('not an object')); +// {} +``` + +Part of the [client capabilities][ref-capabilities] helpers. diff --git a/fern/products/server-sdks/pages/reference/typescript/core/post-prompt/dialogue-turns.mdx b/fern/products/server-sdks/pages/reference/typescript/core/post-prompt/dialogue-turns.mdx new file mode 100644 index 0000000000..309c2457a3 --- /dev/null +++ b/fern/products/server-sdks/pages/reference/typescript/core/post-prompt/dialogue-turns.mdx @@ -0,0 +1,77 @@ +--- +title: "dialogueTurns" +slug: reference/typescript/core/post-prompt/dialogue-turns +description: "Extract the user and assistant turns from a call log, dropping tool traffic and the summary echo." +max-toc-depth: 3 +--- + +[ref-post-prompt]: /docs/server-sdks/reference/typescript/core/post-prompt + +Extract the real dialogue from a call log. Drops everything that is machinery rather than speech: +entries whose role isn't in `roles`, entries carrying `tool_calls`, and entries with empty or +non-string content. The `system`, `system-log`, `tool`, and `assistant-manual` roles are all +excluded by default. + +`dropEcho` handles one engine behavior. The chat engine appends its own post-prompt output to +`call_log` as a bare `role: assistant` entry, indistinguishable by role from real speech. Replayed +into another medium, the agent would narrate a summary of itself. It is identifiable only by +content, being identical to `post_prompt_data.raw`, which is what this option compares against. + +```typescript {1-4} +dialogueTurns( + callLog: unknown, + opts?: { roles?: readonly string[]; dropEcho?: string | null }, +): DialogueTurn[] +``` + +## Parameters + + + The log, as delivered in `call_log`, `raw_call_log`, or `raw_messages`. A non-array value yields + `[]`. + + + + Filtering options. + + + + + Roles to keep. `DIALOGUE_ROLES` is `['user', 'assistant']`. + + + + Exact content to treat as the summary echo and drop. Compared after trimming whitespace. + + + +## Returns + +`DialogueTurn[]` -- `{ role, content }` pairs, in order. + +## Examples + +```typescript {13} +import { dialogueTurns } from '@signalwire/sdk'; + +const rawSummary = '{"intent":"book_ride"}'; +const callLog = [ + { role: 'system', content: 'You are Ada.' }, + { role: 'user', content: 'I need a cab to the ferry building.' }, + { role: 'assistant', content: '', tool_calls: [{ id: 't1' }] }, + { role: 'tool', content: 'ride booked' }, + { role: 'assistant', content: 'Booked. Your driver arrives in 6 minutes.' }, + { role: 'assistant', content: rawSummary }, +]; + +for (const turn of dialogueTurns(callLog, { dropEcho: rawSummary })) { + console.log(turn.role, turn.content); +} +// user I need a cab to the ferry building. +// assistant Booked. Your driver arrives in 6 minutes. + +console.log(dialogueTurns(callLog, { roles: ['user'] })); +// [ { role: 'user', content: 'I need a cab to the ferry building.' } ] +``` + +Part of the [post-prompt normalization][ref-post-prompt] helpers. diff --git a/fern/products/server-sdks/pages/reference/typescript/core/post-prompt/index.mdx b/fern/products/server-sdks/pages/reference/typescript/core/post-prompt/index.mdx new file mode 100644 index 0000000000..83b86cf7f3 --- /dev/null +++ b/fern/products/server-sdks/pages/reference/typescript/core/post-prompt/index.mdx @@ -0,0 +1,135 @@ +--- +title: "Post-prompt normalization" +slug: reference/typescript/core/post-prompt +description: "Read voice and chat post-prompt bodies as one shape, with the summary parsed and the dialogue extracted." +max-toc-depth: 3 +--- + +[on-summary]: /docs/server-sdks/reference/typescript/agents/agent-base/on-summary +[normalize]: /docs/server-sdks/reference/typescript/core/post-prompt/normalize-post-prompt +[dialogue]: /docs/server-sdks/reference/typescript/core/post-prompt/dialogue-turns +[on-call-end]: /docs/server-sdks/reference/typescript/agents/agent-base/on-call-end + +One conversation can run over voice and over text chat, and both produce a post-prompt body, but not +the same shape. The post-prompt helpers absorb that divergence so your application sees one result +regardless of which engine finished the conversation. + +| Field | Voice | Chat | +|---|---|---| +| `app_name` | `"swml app"` | `"ai_chat"` | +| `conversation_id` | absent | present at top level | +| Full log | `raw_call_log` | `raw_messages` | +| Summary arrives as | a `summarize_conversation` tool call | a bare `role: assistant` turn inside `call_log` | +| `post_prompt_data` | parsed object | `{ "raw": "" }` | + +`conversation_type` is a reliable top-level discriminator on both. The voice engine can also +deliver `post_prompt_data` as `{ "parsed": [ {...} ], "raw": "..." }`, an object wrapped in a +list, which passes structural checks and misses every field lookup. The parser unwraps it. + +The helpers don't decide what a summary should contain. The schema is whatever your post-prompt +text asked the model to produce, so parsing is schema-agnostic and returns the object as found. +Nothing here throws: the conversation that produced the body is already over. + +```typescript +import { + normalizePostPrompt, + NormalizedPostPrompt, + parsePostPromptData, + dialogueTurns, + stripJsonFence, + DIALOGUE_ROLES, +} from '@signalwire/sdk'; +import type { DialogueTurn } from '@signalwire/sdk'; +``` + +## Properties + +`NormalizedPostPrompt` is one finished conversation leg in a shape that doesn't vary by engine. Its +properties are `readonly`, and [`normalizePostPrompt()`][normalize] returns a frozen instance. You can +also construct one directly with `new NormalizedPostPrompt(fields)`, where `fields` is a +`Partial`; any property you leave out takes the default shown here. + + + `conversation_type` as reported, such as `'voice'` or `'chat'`. Empty when the engine didn't say. + + + + Present on chat, absent on voice. When `null`, fall back to your own key from global data or + the call ID rather than treating this as authoritative. + + + + The parsed `post_prompt_data`, with whatever keys your post-prompt asked for. `{}` when there was + none or it couldn't be parsed. A model that answered in prose instead of JSON yields + `{ summary: '' }`. + + + + `user` and `assistant` turns only, as `{ role, content }` pairs, with tool calls and the chat + engine's summary echo removed. + + + + The platform call ID, when present. + + + + The complete request body, untouched. + + +## Constants and types + + + The roles that count as dialogue: `['user', 'assistant']`. Frozen. It is the default for the `roles` + option of [`dialogueTurns()`][dialogue]. Everything else in a call log is machinery: `system` is + the prompt, `system-log` is lifecycle and step tracing, `tool` is function output, and + `assistant-manual` is filler speech. + + + + One user or assistant turn: `{ role, content }`. Exported as a type. + + +## Functions + + + + Normalize a post-prompt body from either engine. + + + Return post_prompt_data as a plain object, whichever shape it arrived in. + + + Extract the user and assistant turns from a call log. + + + Unwrap a fenced JSON code block. + + + +## Examples + +Store every finished leg the same way, whether it arrives through +[`onSummary()`][on-summary] or from an [`onCallEnd()`][on-call-end] handler: + +```typescript {11-14} +import { AgentBase, normalizePostPrompt } from '@signalwire/sdk'; + +class DispatchAgent extends AgentBase { + constructor() { + super({ name: 'dispatch', route: '/dispatch' }); + this.setPromptText('You are Ada, the dispatcher for Bayview Taxi.'); + this.setPostPrompt('Summarize the call as JSON with keys intent and resolved.'); + } + + override onSummary(_summary: unknown, rawData: unknown): void { + const leg = normalizePostPrompt(rawData); + if (leg.dialogue.length > 0) { + // Write to your system of record. + console.log(leg.medium, leg.conversationId ?? leg.callId, leg.summary); + } + } +} + +await new DispatchAgent().serve(); +``` diff --git a/fern/products/server-sdks/pages/reference/typescript/core/post-prompt/normalize-post-prompt.mdx b/fern/products/server-sdks/pages/reference/typescript/core/post-prompt/normalize-post-prompt.mdx new file mode 100644 index 0000000000..3f7ff1f5a5 --- /dev/null +++ b/fern/products/server-sdks/pages/reference/typescript/core/post-prompt/normalize-post-prompt.mdx @@ -0,0 +1,64 @@ +--- +title: "normalizePostPrompt" +slug: reference/typescript/core/post-prompt/normalize-post-prompt +description: "Normalize a post-prompt body from the voice or chat engine into one shape." +max-toc-depth: 3 +--- + +[ref-post-prompt]: /docs/server-sdks/reference/typescript/core/post-prompt +[parse]: /docs/server-sdks/reference/typescript/core/post-prompt/parse-post-prompt-data +[dialogue]: /docs/server-sdks/reference/typescript/core/post-prompt/dialogue-turns + +Normalize a post-prompt body from either engine. Parses `post_prompt_data` with +[`parsePostPromptData()`][parse], reads the log from the first non-empty of `call_log`, +`raw_call_log`, or `raw_messages`, and extracts the dialogue with [`dialogueTurns()`][dialogue], +dropping the chat engine's summary echo by comparing against the raw summary string. + +```typescript {1} +normalizePostPrompt(body: unknown): NormalizedPostPrompt +``` + +## Parameters + + + The complete post-prompt request body. + + +## Returns + +[`NormalizedPostPrompt`][ref-post-prompt] -- A frozen instance. Never throws: a body the function +can't make sense of yields one with empty fields. + +## Examples + +A chat conversation, with the summary echoed as the last assistant turn: + +```typescript {16} +import { normalizePostPrompt } from '@signalwire/sdk'; + +const body = { + conversation_type: 'chat', + conversation_id: 'conv-1042', + call_id: 'call-7', + post_prompt_data: { raw: '```json\n{"intent":"book_ride","resolved":true}\n```' }, + call_log: [ + { role: 'system', content: 'You are Ada, the dispatcher for Bayview Taxi.' }, + { role: 'user', content: 'I need a cab to the ferry building.' }, + { role: 'assistant', content: 'Booked. Your driver arrives in 6 minutes.' }, + { role: 'assistant', content: '```json\n{"intent":"book_ride","resolved":true}\n```' }, + ], +}; + +const leg = normalizePostPrompt(body); +console.log(leg.medium, leg.conversationId, leg.callId); +// chat conv-1042 call-7 +console.log(leg.summary); +// { intent: 'book_ride', resolved: true } +console.log(leg.dialogue); +// [ +// { role: 'user', content: 'I need a cab to the ferry building.' }, +// { role: 'assistant', content: 'Booked. Your driver arrives in 6 minutes.' } +// ] +``` + +Part of the [post-prompt normalization][ref-post-prompt] helpers. diff --git a/fern/products/server-sdks/pages/reference/typescript/core/post-prompt/parse-post-prompt-data.mdx b/fern/products/server-sdks/pages/reference/typescript/core/post-prompt/parse-post-prompt-data.mdx new file mode 100644 index 0000000000..b80d76d121 --- /dev/null +++ b/fern/products/server-sdks/pages/reference/typescript/core/post-prompt/parse-post-prompt-data.mdx @@ -0,0 +1,62 @@ +--- +title: "parsePostPromptData" +slug: reference/typescript/core/post-prompt/parse-post-prompt-data +description: "Return post_prompt_data as a plain object, whichever shape it arrived in." +max-toc-depth: 3 +--- + +[ref-post-prompt]: /docs/server-sdks/reference/typescript/core/post-prompt +[strip]: /docs/server-sdks/reference/typescript/core/post-prompt/strip-json-fence + +Return `post_prompt_data` as a plain object, whichever shape it arrived in. A malformed summary +degrades to `{}` rather than failing the request that delivered it. + +The function handles these shapes, in order: + +1. An object, or a list of objects, under `parsed` (`{ parsed: [ {...} ], raw: '...' }`): the object, + or the first non-empty one in the list, is returned. +2. A flat object with real keys already present, ignoring `raw` and `parsed`. +3. A `raw` string: unfenced with [`stripJsonFence()`][strip], then parsed as JSON. A JSON object is + returned as is. Prose, or any other JSON value, is returned as `{ summary: '' }`. + +```typescript {1} +parsePostPromptData(data: unknown): Record +``` + +## Parameters + + + The `post_prompt_data` value from a post-prompt body. + + +## Returns + +`Record` -- The summary object, or `{}` when there's nothing usable. + +## Examples + +```typescript {4} +import { parsePostPromptData } from '@signalwire/sdk'; + +// Voice, already parsed +console.log(parsePostPromptData({ intent: 'book_ride', resolved: true })); +// { intent: 'book_ride', resolved: true } + +// Voice, wrapped in a list +console.log(parsePostPromptData({ parsed: [{ intent: 'book_ride' }], raw: '...' })); +// { intent: 'book_ride' } + +// Chat, fenced JSON +console.log(parsePostPromptData({ raw: '```json\n{"intent":"book_ride"}\n```' })); +// { intent: 'book_ride' } + +// Prose instead of JSON +console.log(parsePostPromptData({ raw: 'Caller booked a ride to the ferry building.' })); +// { summary: 'Caller booked a ride to the ferry building.' } + +// Nothing usable +console.log(parsePostPromptData(undefined)); +// {} +``` + +Part of the [post-prompt normalization][ref-post-prompt] helpers. diff --git a/fern/products/server-sdks/pages/reference/typescript/core/post-prompt/strip-json-fence.mdx b/fern/products/server-sdks/pages/reference/typescript/core/post-prompt/strip-json-fence.mdx new file mode 100644 index 0000000000..05cb2d1c2c --- /dev/null +++ b/fern/products/server-sdks/pages/reference/typescript/core/post-prompt/strip-json-fence.mdx @@ -0,0 +1,42 @@ +--- +title: "stripJsonFence" +slug: reference/typescript/core/post-prompt/strip-json-fence +description: "Unwrap a fenced JSON code block from the chat engine's summary." +max-toc-depth: 3 +--- + +[ref-post-prompt]: /docs/server-sdks/reference/typescript/core/post-prompt + +Unwrap a fenced code block such as ```` ```json ... ``` ````. The chat engine hands the model's +answer back verbatim, fence and all, where the voice engine parses it first. Text without a +fence is returned trimmed. + +```typescript {1} +stripJsonFence(text: string): string +``` + +## Parameters + + + The text to unwrap. + + +## Returns + +`string` -- The text without a surrounding code fence, trimmed. + +## Examples + +```typescript {4} +import { stripJsonFence } from '@signalwire/sdk'; + +const fenced = '```json\n{"intent":"book_ride"}\n```'; +console.log(stripJsonFence(fenced)); +// {"intent":"book_ride"} +console.log(stripJsonFence(' {"intent":"book_ride"} ')); +// {"intent":"book_ride"} +console.log(JSON.parse(stripJsonFence(fenced)).intent); +// book_ride +``` + +Part of the [post-prompt normalization][ref-post-prompt] helpers. diff --git a/fern/products/server-sdks/pages/reference/typescript/core/security/index.mdx b/fern/products/server-sdks/pages/reference/typescript/core/security/index.mdx index 2ee41a5da9..b19c22d94b 100644 --- a/fern/products/server-sdks/pages/reference/typescript/core/security/index.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/core/security/index.mdx @@ -7,6 +7,7 @@ max-toc-depth: 3 [agentbase]: /docs/server-sdks/reference/typescript/agents/agent-base [validate-webhook-signature]: /docs/server-sdks/reference/typescript/core/security/validate-webhook-signature +[validate-webhook-signature-sha256]: /docs/server-sdks/reference/typescript/core/security/validate-webhook-signature-sha256 [validate-request]: /docs/server-sdks/reference/typescript/core/security/validate-request [validate]: /docs/server-sdks/reference/typescript/core/security/validate [webhook-validation-middleware]: /docs/server-sdks/reference/typescript/core/security/webhook-validation-middleware @@ -15,13 +16,20 @@ SignalWire signs the webhook requests it sends to your endpoints. These utilities verify those signatures so you can reject forged or tampered requests. All are imported directly from `@signalwire/sdk`. -```typescript {2-7} +SignalWire sends two signature headers on signed webhooks: `X-SignalWire-Sha256-Signature` (HMAC-SHA256) +and `X-SignalWire-Signature` (SHA-1). [`validate()`][validate] and +[`webhookValidationMiddleware()`][webhook-validation-middleware] check the SHA-256 header first and fall +back to the SHA-1 header, so deployments that only receive the SHA-1 header keep validating. + +```typescript {2-8} import { validateWebhookSignature, + validateWebhookSignatureSha256, validateRequest, validate, webhookValidationMiddleware, SIGNALWIRE_SIGNATURE_HEADER, + SIGNALWIRE_SHA256_SIGNATURE_HEADER, TWILIO_COMPAT_SIGNATURE_HEADER, } from '@signalwire/sdk'; ``` @@ -39,6 +47,9 @@ for you. Verify a signature against a raw request body. + + Verify the SHA-256 signature header against a raw request body. + Verify a signature against a raw body or pre-parsed form params. @@ -56,6 +67,11 @@ for you. The SignalWire signature header name: `'x-signalwire-signature'`. + + The SHA-256 signature header name: `'x-signalwire-sha256-signature'`. When a request carries it, + [`validate()`][validate] and the middleware check it before the SHA-1 header. + + The legacy signature header name, `'x-twilio-signature'`. The validators accept it as an alias of `SIGNALWIRE_SIGNATURE_HEADER`. diff --git a/fern/products/server-sdks/pages/reference/typescript/core/security/validate-request.mdx b/fern/products/server-sdks/pages/reference/typescript/core/security/validate-request.mdx index c36ef080f0..eeb2b73012 100644 --- a/fern/products/server-sdks/pages/reference/typescript/core/security/validate-request.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/core/security/validate-request.mdx @@ -5,6 +5,7 @@ description: "Verify a webhook signature from a raw body or pre-parsed form para max-toc-depth: 3 --- +[validate-webhook-signature-sha256]: /docs/server-sdks/reference/typescript/core/security/validate-webhook-signature-sha256 [validate-webhook-signature]: /docs/server-sdks/reference/typescript/core/security/validate-webhook-signature Verify a SignalWire webhook signature, accepting either a raw body string or @@ -38,7 +39,9 @@ validateRequest(signingKey: string, signature: string, url: string, paramsOrRawB ## **Returns** `boolean` -- `true` when the signature matches; `false` otherwise. The -comparison is constant-time. Throws a `TypeError` when `paramsOrRawBody` is +comparison is constant-time. It checks the SHA-1 signature only; for the +`X-SignalWire-Sha256-Signature` header, use +[`validateWebhookSignatureSha256()`][validate-webhook-signature-sha256]. Throws a `TypeError` when `paramsOrRawBody` is neither a string nor a record, `Map`, or array of tuples. ## **Example** diff --git a/fern/products/server-sdks/pages/reference/typescript/core/security/validate-webhook-signature-sha256.mdx b/fern/products/server-sdks/pages/reference/typescript/core/security/validate-webhook-signature-sha256.mdx new file mode 100644 index 0000000000..577c464123 --- /dev/null +++ b/fern/products/server-sdks/pages/reference/typescript/core/security/validate-webhook-signature-sha256.mdx @@ -0,0 +1,98 @@ +--- +title: "validateWebhookSignatureSha256" +slug: reference/typescript/core/security/validate-webhook-signature-sha256 +description: "Verify a SignalWire webhook against the SHA-256 signature header." +max-toc-depth: 3 +--- + +[validate-webhook-signature]: /docs/server-sdks/reference/typescript/core/security/validate-webhook-signature +[security]: /docs/server-sdks/reference/typescript/core/security + +Verify a webhook against the `X-SignalWire-Sha256-Signature` header. SignalWire sends it alongside +the SHA-1 `X-SignalWire-Signature` header on signed webhooks. The signature is the hex HMAC-SHA256 of +the full URL followed by the raw request body, keyed with your Signing Key. Prefer it when the header +is present. + +Only RELAY, SWML, and JSON webhooks are signed this way. Form-encoded cXML webhooks stay on the SHA-1 +signature, which [`validateWebhookSignature()`][validate-webhook-signature] checks. + +```typescript {1} +validateWebhookSignatureSha256(signingKey: string, signature: string, url: string, rawBody: string): boolean +``` + + +`rawBody` must be the raw request body as a string, before any JSON or form parsing. Passing a +parsed object throws a `TypeError`. + + +## Parameters + + + Your SignalWire Signing Key. An empty or non-string value throws an `Error`. + + + + The `X-SignalWire-Sha256-Signature` header value, a 64-character lowercase hex string. A missing + or empty value returns `false`. + + + + The full public URL SignalWire POSTed to, exactly as the platform saw it when it computed the + signature. + + + + The raw request body, before any parsing. + + +## Returns + +`boolean` -- `true` when the SHA-256 signature matches; `false` otherwise. The comparison is +constant-time. + +## Examples + +Check the SHA-256 header when it is present, and fall back to the SHA-1 header otherwise: + +```typescript {13-15} +import { validateWebhookSignature, validateWebhookSignatureSha256 } from '@signalwire/sdk'; +import { Hono } from 'hono'; + +const app = new Hono(); +const signingKey = process.env['SIGNALWIRE_SIGNING_KEY'] ?? ''; + +app.post('/webhook', async (c) => { + const rawBody = await c.req.text(); + const url = c.req.url; + const sha256 = c.req.header('x-signalwire-sha256-signature'); + const sha1 = c.req.header('x-signalwire-signature') ?? ''; + + const ok = sha256 + ? validateWebhookSignatureSha256(signingKey, sha256, url, rawBody) + : validateWebhookSignature(signingKey, sha1, url, rawBody); + if (!ok) return c.text('Forbidden', 403); + + return c.json({ received: JSON.parse(rawBody) }); +}); +``` + +To see the check in isolation, sign a body yourself: + +```typescript {8} +import { createHmac } from 'node:crypto'; +import { validateWebhookSignatureSha256 } from '@signalwire/sdk'; + +const key = 'PSK_example'; +const url = 'https://dispatch.example.com/webhook'; +const body = '{"event":"call.state","state":"ended"}'; +const header = createHmac('sha256', key).update(url + body).digest('hex'); +console.log(validateWebhookSignatureSha256(key, header, url, body)); +// true +console.log(validateWebhookSignatureSha256(key, header, url, body + ' ')); +// false +console.log(validateWebhookSignatureSha256(key, '', url, body)); +// false +``` + +Part of [webhook signature validation][security]. `SIGNALWIRE_SHA256_SIGNATURE_HEADER` holds the +lowercase header name. diff --git a/fern/products/server-sdks/pages/reference/typescript/core/security/validate-webhook-signature.mdx b/fern/products/server-sdks/pages/reference/typescript/core/security/validate-webhook-signature.mdx index 85b516ae8a..e4787283ef 100644 --- a/fern/products/server-sdks/pages/reference/typescript/core/security/validate-webhook-signature.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/core/security/validate-webhook-signature.mdx @@ -5,6 +5,7 @@ description: "Verify a SignalWire webhook signature against a raw request body." max-toc-depth: 3 --- +[validate-webhook-signature-sha256]: /docs/server-sdks/reference/typescript/core/security/validate-webhook-signature-sha256 [validate-request]: /docs/server-sdks/reference/typescript/core/security/validate-request Verify that an inbound request was genuinely signed by SignalWire. This is the @@ -15,6 +16,12 @@ request body **before** any JSON or form parsing. validateWebhookSignature(signingKey: string, signature: string, url: string, rawBody: string): boolean ``` + +This function checks the SHA-1 `X-SignalWire-Signature` header. When a request also carries +`X-SignalWire-Sha256-Signature`, prefer +[`validateWebhookSignatureSha256()`][validate-webhook-signature-sha256]. + + `rawBody` must be the raw request body as a UTF-8 string. Passing a parsed object throws a `TypeError`. For pre-parsed form params, use diff --git a/fern/products/server-sdks/pages/reference/typescript/core/security/validate.mdx b/fern/products/server-sdks/pages/reference/typescript/core/security/validate.mdx index f63ca0ebb0..2bd0813c85 100644 --- a/fern/products/server-sdks/pages/reference/typescript/core/security/validate.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/core/security/validate.mdx @@ -6,6 +6,7 @@ max-toc-depth: 3 --- [validate-webhook-signature]: /docs/server-sdks/reference/typescript/core/security/validate-webhook-signature +[validate-webhook-signature-sha256]: /docs/server-sdks/reference/typescript/core/security/validate-webhook-signature-sha256 [webhook-validation-middleware]: /docs/server-sdks/reference/typescript/core/security/webhook-validation-middleware Decide whether an inbound webhook is authentic from plain request primitives, @@ -30,8 +31,11 @@ validate(method: string, url: string, headers: Record, body: str "} required={true} toc={true}> - Request headers. The signature header is looked up case-insensitively under - `X-SignalWire-Signature`, or the legacy `X-Twilio-Signature` alias. + Request headers, looked up case-insensitively. When `X-SignalWire-Sha256-Signature` is present, + it is checked first with + [`validateWebhookSignatureSha256()`][validate-webhook-signature-sha256], and a match lets the + request through. Otherwise the decision falls to `X-SignalWire-Signature`, or the legacy + `X-Twilio-Signature` alias. @@ -45,7 +49,7 @@ validate(method: string, url: string, headers: Record, body: str ## **Returns** `WebhookRejection | null` -- `null` when the signature is valid. Otherwise a -`[403, {}, 'Forbidden']` triple, whether the header is missing, empty, or +`[403, {}, 'Forbidden']` triple, whether the headers are missing, empty, or wrong. The body never says which check failed. ## **Example** diff --git a/fern/products/server-sdks/pages/reference/typescript/core/security/webhook-validation-middleware.mdx b/fern/products/server-sdks/pages/reference/typescript/core/security/webhook-validation-middleware.mdx index ec75306829..73c353c778 100644 --- a/fern/products/server-sdks/pages/reference/typescript/core/security/webhook-validation-middleware.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/core/security/webhook-validation-middleware.mdx @@ -6,12 +6,15 @@ max-toc-depth: 3 --- [validate-webhook-signature]: /docs/server-sdks/reference/typescript/core/security/validate-webhook-signature +[validate-webhook-signature-sha256]: /docs/server-sdks/reference/typescript/core/security/validate-webhook-signature-sha256 [agentbase]: /docs/server-sdks/reference/typescript/agents/agent-base Create a [Hono](https://hono.dev) middleware that captures the raw request body, -validates the SignalWire signature via -[`validateWebhookSignature()`][validate-webhook-signature], and rejects requests -that fail. On failure it responds `403 Forbidden` with no body detail (to avoid +validates the SignalWire signature, and rejects requests that fail. It checks the +`X-SignalWire-Sha256-Signature` header first, using +[`validateWebhookSignatureSha256()`][validate-webhook-signature-sha256], and falls back to +`X-SignalWire-Signature` (or `X-Twilio-Signature`), using +[`validateWebhookSignature()`][validate-webhook-signature]. On failure it responds `403 Forbidden` with no body detail (to avoid leaking which check failed); on success it calls `next()`. ```typescript {1} From a6f2199ce8aa7152ba2827c9498cfb059b6cd409 Mon Sep 17 00:00:00 2001 From: August Date: Thu, 22 Oct 2026 19:33:47 +0000 Subject: [PATCH 03/15] docs(server-sdks): sync TypeScript AgentBase reference to 3.7.1 --- .../agents/agent-base/add-language.mdx | 32 +++++--- .../agents/agent-base/add-per-call-config.mdx | 77 +++++++++++++++++++ .../agents/agent-base/as-router.mdx | 15 ++-- .../agents/agent-base/define-contexts.mdx | 17 ++-- .../agents/agent-base/define-tool.mdx | 8 +- .../agents/agent-base/enable-mcp-server.mdx | 4 + .../agent-base/get-basic-auth-credentials.mdx | 7 +- .../typescript/agents/agent-base/index.mdx | 25 +++++- .../typescript/agents/agent-base/mount.mdx | 65 ++++++++++++++++ .../agents/agent-base/on-call-end.mdx | 60 +++++++++++++++ .../agents/agent-base/on-call-start.mdx | 72 +++++++++++++++++ .../agents/agent-base/on-summary.mdx | 9 ++- .../typescript/agents/agent-base/serve.mdx | 5 ++ .../set-dynamic-config-callback.mdx | 38 +++++++-- .../agents/agent-base/validate-basic-auth.mdx | 21 +++-- .../typescript/agents/agent-server/run.mdx | 5 ++ .../agents/agent-server/unregister.mdx | 12 +-- .../agents/swaig-function/index.mdx | 7 +- 18 files changed, 418 insertions(+), 61 deletions(-) create mode 100644 fern/products/server-sdks/pages/reference/typescript/agents/agent-base/add-per-call-config.mdx create mode 100644 fern/products/server-sdks/pages/reference/typescript/agents/agent-base/mount.mdx create mode 100644 fern/products/server-sdks/pages/reference/typescript/agents/agent-base/on-call-end.mdx create mode 100644 fern/products/server-sdks/pages/reference/typescript/agents/agent-base/on-call-start.mdx diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/add-language.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/add-language.mdx index ef683ab7bf..d0b00555ae 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/add-language.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/add-language.mdx @@ -52,16 +52,32 @@ engine that can change. Explicit TTS engine name (e.g., `"elevenlabs"`, `"rime"`). -"} toc={true}> - Filler phrases keyed by category for this language. + + Explicit TTS model identifier (e.g., `"mistv2"`, `"eleven_turbo_v2_5"`). + + + + Phrases the AI can say to fill a pause in speech. Rendered as `speech_fillers`. + + +"} toc={true}> + Filler phrases for both speech and functions, the older single `fillers` list. + Used as `speechFillers` when that isn't set. An object keyed by category is + flattened into one list, with a warning; pass a string array instead. Speech recognition model identifier. ->"} toc={true}> - Per-function filler phrases, keyed by function name then language code. +>"} toc={true}> + Phrases the AI says while a function runs. Rendered as `function_fillers`. An + object keyed by function name then language code is flattened into one list, + with a warning; for fillers per tool, use the tool's own `fillers` option. + + When both speech and function fillers are set, the language renders + `speech_fillers` and `function_fillers`. When only one is set, it renders as the + older `fillers` list. "} toc={true}> @@ -116,12 +132,8 @@ agent.addLanguage({ name: 'English', code: 'en-US', voice: 'rime.spore', - fillers: { - speech: ['Um...', 'Let me think...'], - }, - functionFillers: { - get_weather: { 'en-US': ['Checking the weather...', 'One moment please...'] }, - }, + speechFillers: ['Um...', 'Let me think...'], + functionFillers: ['Checking on that...', 'One moment please...'], }); await agent.serve(); ``` diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/add-per-call-config.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/add-per-call-config.mdx new file mode 100644 index 0000000000..e10684a821 --- /dev/null +++ b/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/add-per-call-config.mdx @@ -0,0 +1,77 @@ +--- +title: "addPerCallConfig" +slug: reference/typescript/agents/agent-base/add-per-call-config +description: Register a per-request configuration callback without replacing callbacks already set. +max-toc-depth: 3 +--- + +[ref-agentbase]: /docs/server-sdks/reference/typescript/agents/agent-base +[set-dynamic-config-callback]: /docs/server-sdks/reference/typescript/agents/agent-base/set-dynamic-config-callback +[add-post-answer-verb]: /docs/server-sdks/reference/typescript/agents/agent-base/add-post-answer-verb +[clear-post-answer-verbs]: /docs/server-sdks/reference/typescript/agents/agent-base/clear-post-answer-verbs + +Register a per-request configuration callback, keeping any already registered. +Same signature and contract as +[`setDynamicConfigCallback()`][set-dynamic-config-callback], except that +callbacks accumulate instead of replacing each other. They run in registration +order on the same per-request copy of the agent, so a later callback sees what +an earlier one configured. + +Prefer this form when composing. A base class and a subclass, or an agent and a +helper, can each register what they own without knowing about the other. +Calling `setDynamicConfigCallback()` afterwards replaces every callback +registered so far. + +Callbacks run for every request of a call: the SWML fetch, each SWAIG function +call, and the summary delivery. Only the SWML fetch carries the caller's data, +such as user variables, so verify the caller there, remember the `call_id`, and +check it on later requests. + +## Rejecting a call + +A callback can't refuse the request, but it can decide what the call does. +Build the agent to fail closed: add a goodbye with +[`addPostAnswerVerb('play', …)`][add-post-answer-verb] and +`addPostAnswerVerb('hangup', {})`, and no tools, so a call hears the message +and hangs up before the `ai` verb runs. For a call the callback accepts, add the +tools to the copy and call [`clearPostAnswerVerbs()`][clear-post-answer-verbs] +last. + +A callback that throws is logged, and the copy is rendered as it stands, so a +failure before that last step leaves the goodbye in place. A throwing callback +also stops the ones after it without restoring verbs an earlier one cleared, so +the callback that clears them must be registered last. + +## Parameters + + + A function with the signature + `(queryParams: Record, bodyParams: SwmlRequestData, headers: Record, agent: AgentBase) => void | Promise`. + `agent` is the per-request copy. Configure that object, never the agent + itself, or the configuration leaks across callers. + + +## Returns + +[`AgentBase`][ref-agentbase] -- Returns `this` for method chaining. + +## Example + +```typescript {6-14} +import { AgentBase } from '@signalwire/sdk'; + +const agent = new AgentBase({ name: 'dispatch', route: '/dispatch' }); +agent.setPromptText('You are Ada, the dispatcher for Bayview Taxi.'); + +agent.addPerCallConfig((queryParams, bodyParams, headers, copy) => { + if (queryParams.lang === 'es') { + copy.addLanguage({ name: 'Spanish', code: 'es-MX', voice: 'rime.marsh' }); + } +}); + +agent.addPerCallConfig((queryParams, bodyParams, headers, copy) => { + copy.setGlobalData({ tenant: queryParams.tenant ?? 'default' }); +}); + +await agent.serve(); +``` diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/as-router.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/as-router.mdx index a455faf6a5..aec81a9dcf 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/as-router.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/as-router.mdx @@ -7,8 +7,14 @@ max-toc-depth: 3 [agentserver]: /docs/server-sdks/reference/typescript/agents/agent-server -Get this agent's Hono application for mounting as a sub-router in a larger Hono -application or an [`AgentServer`][agentserver]. +Get a router that embeds this agent's routes in a host Hono application. The +routes are relative to the agent's root (`/`, `/swaig`, `/post_prompt`, plus any +routing callbacks), so mount it under the agent's route: +`app.route(agent.route, agent.asRouter())`. [`AgentServer`][agentserver] mounts it +the same way. `getApp()` serves the same routes under the agent's route instead. + +The routes also answer with a trailing slash (`/swaig/`, `/post_prompt/`), for a host +app with strict routing. ## **Parameters** @@ -16,9 +22,8 @@ None. ## **Returns** -`HostAppRouter` -- The agent's Hono app, with all agent endpoints (SWML -delivery, SWAIG function handling, post-prompt, debug events, health checks, -etc.). `HostAppRouter` is a type alias for `Hono`, named for the host-app +`HostAppRouter` -- A Hono sub-app carrying the agent's routes, relative to its +root. `HostAppRouter` is a type alias for `Hono`, named for the host-app mounting contract it fulfills. ## **Example** diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/define-contexts.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/define-contexts.mdx index ab98f02598..63141f560b 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/define-contexts.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/define-contexts.mdx @@ -12,9 +12,12 @@ Define contexts and steps for multi-step conversation workflows. Contexts allow agent to guide the caller through a structured sequence of interactions -- such as gathering information, verifying identity, and then performing an action. -Returns a [`ContextBuilder`][contextbuilder] for fluent context definition. If an -existing `ContextBuilder` instance is passed, it is used directly. Otherwise a new -empty `ContextBuilder` is created and returned. +Called with no argument, it returns the agent's [`ContextBuilder`][contextbuilder], +creating it on first use, so calling it again adds to the same workflow. Passing a +`ContextBuilder` replaces the current one and returns it. Passing a plain object uses +that object as the contexts, rendered as it is, and returns the agent. + +The contexts render inside the AI prompt, as `ai.prompt.contexts`. Contexts can coexist with traditional prompts. The only restriction is that POM @@ -24,13 +27,15 @@ sections and raw text cannot be mixed in the main prompt. ## **Parameters** - An existing `ContextBuilder` instance to use directly. If omitted or a plain object - is passed, a new empty `ContextBuilder` is created instead. + A `ContextBuilder` to use in place of the current one, or the contexts as a plain + object. Omit it to get the current builder. Throws a `TypeError` for a value that + is neither. ## **Returns** -[`ContextBuilder`][contextbuilder] -- The active `ContextBuilder` for further configuration. +[`ContextBuilder`][contextbuilder] -- The active `ContextBuilder` for further +configuration, or [`AgentBase`][ref-agentbase] (`this`) when given a plain object. ## **Examples** diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/define-tool.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/define-tool.mdx index 20799b477d..9240eeb8cb 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/define-tool.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/define-tool.mdx @@ -56,8 +56,12 @@ instead. Callback invoked when the AI calls this tool. Receives - `(args, rawData: SwaigRequest)` and returns a [`FunctionResult`][functionresult], - a plain object, or a string. + `(args, rawData: SwaigRequest, agent?: AgentBase)` and returns a + [`FunctionResult`][functionresult], a plain object, or a string. `agent` is the + agent running the call: with a dynamic config callback, the request's configured + copy, so the handler sees what the callback set up. Two-argument handlers work + unchanged. Arguments that don't match the tool's `parameters` schema log a warning + before the handler runs. diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/enable-mcp-server.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/enable-mcp-server.mdx index d14999bf9c..4bcd74e4cf 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/enable-mcp-server.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/enable-mcp-server.mdx @@ -12,6 +12,10 @@ Expose this agent's tools as a Model Context Protocol (MCP) server endpoint. Adds a `/mcp` route that speaks JSON-RPC 2.0, allowing external MCP clients (Claude Desktop, other agents, etc.) to discover and invoke the agent's tools. +The endpoint requires the agent's basic auth credentials, so an MCP client must +send an `Authorization` header. It lists and calls only the tools the agent runs +itself, not DataMap or external webhook tools. + This method exposes your agent's tools **as** an MCP server. To connect your agent **to** an external MCP server as a client, use diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/get-basic-auth-credentials.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/get-basic-auth-credentials.mdx index 3eeba9ae47..ad345bcd0b 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/get-basic-auth-credentials.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/get-basic-auth-credentials.mdx @@ -12,9 +12,10 @@ includes a source label indicating where the credentials came from. When `true`, the returned tuple includes a third element indicating the credential - source: `"provided"` (set explicitly), `"environment"` (from `SWML_BASIC_AUTH_USER` - / `SWML_BASIC_AUTH_PASSWORD` environment variables), or `"generated"` - (generated at startup). + source: `"provided"` (set explicitly), `"config file"` (from the agent's config + file), `"environment"` (from `SWML_BASIC_AUTH_USER` / `SWML_BASIC_AUTH_PASSWORD` + environment variables), or `"generated"` (generated at startup). Credentials + resolve in that order. ## **Returns** diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/index.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/index.mdx index 9003cbad8e..555f53068a 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/index.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/index.mdx @@ -64,8 +64,9 @@ const agent = new AgentBase({ Explicit `[username, password]` tuple for HTTP Basic Auth on all endpoints. If not - set, credentials are read from `SWML_BASIC_AUTH_USER` / `SWML_BASIC_AUTH_PASSWORD` - env vars, or auto-generated on startup. + set, credentials come from the config file, then the `SWML_BASIC_AUTH_USER` / + `SWML_BASIC_AUTH_PASSWORD` env vars, and are otherwise generated on startup. A + password set without a user gets the user `signalwire`. @@ -77,6 +78,14 @@ const agent = new AgentBase({ Expiration time in seconds for SWAIG function authentication tokens. + + Secret that signs the agent's per-call SWAIG function tokens. Falls back to the + `SIGNALWIRE_SWAIG_SECRET` environment variable. Set the same secret on every + replica, and keep it across restarts, so a token issued by one process validates + on another. When neither is set, each process generates its own secret. Treat this + value as a secret. + + Automatically add an `answer` verb before the AI verb in the SWML document. @@ -278,6 +287,9 @@ await agent.run(); Add an external MCP server for tool discovery and invocation. + + Register a per-request configuration callback without replacing the ones already set. + Add a speech recognition hint with pattern matching and replacement. @@ -410,12 +422,21 @@ await agent.run(); List all currently loaded skills. + + Serve another Hono app or fetch handler alongside the agent's routes. + Manually set the proxy URL base for webhook callbacks. Set the list of native platform functions. + + Run handlers with the transcript when the call ends. + + + Run handlers when the call starts, before the AI's first turn. + Lifecycle hook for debug events received at /debug_events. diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/mount.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/mount.mdx new file mode 100644 index 0000000000..e42a5a794a --- /dev/null +++ b/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/mount.mdx @@ -0,0 +1,65 @@ +--- +title: "mount" +slug: reference/typescript/agents/agent-base/mount +description: Serve another Hono app or fetch handler alongside the agent's own routes. +max-toc-depth: 3 +--- + +[ref-agentbase]: /docs/server-sdks/reference/typescript/agents/agent-base +[get-app]: /docs/server-sdks/reference/typescript/agents/agent-base/get-app +[chat-gateway]: /docs/server-sdks/reference/typescript/agents/chat-gateway + +Mount another Hono app, or a fetch handler, alongside this agent's routes. Use +it instead of adding routes to the app from [`getApp()`][get-app] by hand: the +agent rebuilds its app when its routes change (when you register a routing +callback, for example), and a mount is replayed on every build. + +A mounted app answers its own CORS preflights, since the agent's CORS and CSRF +middleware don't apply to it. It also sets its own security headers and isn't +behind the agent's basic auth. Mount before calling `serve()`. + +## Parameters + + + A Hono app, whose routes are added under `prefix`, or a fetch handler, which is + mounted at `prefix` and doesn't see it. + + + + Mount options. + + + + + + Path to mount at, from the host root. A trailing slash is dropped. Defaults to + the root. + + + + Accepted so code written for the Python SDK works. It has no effect. + + + + +## Returns + +[`AgentBase`][ref-agentbase] -- Returns `this` for method chaining. + +## Example + +```typescript {10} +import { AgentBase, AIChatClient, ChatGateway } from '@signalwire/sdk'; + +const agent = new AgentBase({ name: 'dispatch', route: '/dispatch' }); +const gateway = new ChatGateway({ + client: new AIChatClient(), + configUrl: 'https://bayview-taxi.example.com/dispatch', + key: '', +}); + +agent.mount(gateway.router(), { prefix: '/chat' }); +await agent.serve(); +``` + +See [`ChatGateway`][chat-gateway] for what the mounted router serves. diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/on-call-end.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/on-call-end.mdx new file mode 100644 index 0000000000..651410f13d --- /dev/null +++ b/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/on-call-end.mdx @@ -0,0 +1,60 @@ +--- +title: "onCallEnd" +slug: reference/typescript/agents/agent-base/on-call-end +description: Register a handler that receives the conversation transcript when a call ends. +max-toc-depth: 3 +--- + +[on-summary]: /docs/server-sdks/reference/typescript/agents/agent-base/on-summary +[on-call-start]: /docs/server-sdks/reference/typescript/agents/agent-base/on-call-start +[post-prompt]: /docs/server-sdks/reference/typescript/core/post-prompt/normalize-post-prompt + +Register a handler that runs when the call ends, with the transcript. Handlers +run in registration order. + +The SDK registers the platform's reserved `hangup_hook` function for you. It +fires on hangup and is never offered to the model, so it can't be called early +or skipped. Registering the first handler also turns on the +`swaig_post_conversation` parameter. Without it the hook still fires but +carries no transcript, and the handler would receive an empty list with nothing +to say why. If you have explicitly set that parameter to `false`, the SDK leaves +it alone and logs a warning. + +A handler's return value is ignored, since the call is over. An exception is +logged, not raised, so a failing handler doesn't stop the others. For the +post-prompt summary, see [`onSummary()`][on-summary]; for the start of the +call, see [`onCallStart()`][on-call-start]. + +## Parameters + + + Called as `handler(callLog, rawData)`. May be async. + + - `callLog` -- the conversation as the platform recorded it. + - `rawData` -- the complete SWAIG request, including `global_data` and + `call_id`. + + +## Returns + +The handler, unchanged. + +## Example + +```typescript {6-10} +import { AgentBase } from '@signalwire/sdk'; + +const agent = new AgentBase({ name: 'dispatch', route: '/dispatch' }); +agent.setPromptText('You are Ada, the dispatcher for Bayview Taxi.'); + +agent.onCallEnd((callLog, rawData) => { + const conversationId = rawData.global_data?.['conversation_id']; + // Write the transcript to your system of record. + console.log(conversationId, callLog.length, 'turns'); +}); + +await agent.serve(); +``` + +To handle voice and chat transcripts with one shape, pass `rawData` through +[`normalizePostPrompt()`][post-prompt]. diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/on-call-start.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/on-call-start.mdx new file mode 100644 index 0000000000..18c87c3c51 --- /dev/null +++ b/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/on-call-start.mdx @@ -0,0 +1,72 @@ +--- +title: "onCallStart" +slug: reference/typescript/agents/agent-base/on-call-start +description: Register a handler that runs when the call starts, before the AI's first turn. +max-toc-depth: 3 +--- + +[on-call-end]: /docs/server-sdks/reference/typescript/agents/agent-base/on-call-end +[function-result]: /docs/server-sdks/reference/typescript/agents/function-result +[update-global-data]: /docs/server-sdks/reference/typescript/agents/function-result/update-global-data +[ai-message]: /docs/server-sdks/reference/typescript/rest/calling/ai-message + +Register a handler that runs when the call starts, before the AI speaks. It's +the counterpart of [`onCallEnd()`][on-call-end]. Handlers run in registration +order. + +The SDK registers the platform's reserved `startup_hook` function for you. The +platform calls it once, after the call is answered and before the AI's first +turn, and never offers it to the model. + +A handler may return a [`FunctionResult`][function-result], or nothing: + +- The result's actions run at once, in registration order across handlers. Use + [`updateGlobalData()`][update-global-data], for example, to give the call data + before the conversation starts. +- The result's response text is added to the conversation as a system message + the model reads before its first turn. The texts of several handlers are + joined, one per line. Only a plain string response is used, and OpenAI + Realtime agents run the actions without adding the text. + +`postProcess` isn't used. A hook whose handlers return no text still adds a +short placeholder line to the conversation, as any function with an empty reply +does. An exception is logged, not raised, and that handler's result is dropped, +so a failing handler can't stop the call from starting or the other handlers +from running. + + +The platform waits for the hook before the AI starts, up to 120 seconds, and +sends the request again after a timeout or a `5xx`. Keep handlers short and safe +to run twice. Start long work without awaiting it, and send its result into the +call later with [`calling.aiMessage()`][ai-message] on the REST client. + + +## Parameters + + + Called as `handler(rawData)` with the complete SWAIG request: `call_id`, + `global_data`, and the caller's details. May be async. + + +## Returns + +The handler, unchanged. + +## Example + +```typescript {6-12} +import { AgentBase, FunctionResult } from '@signalwire/sdk'; + +const agent = new AgentBase({ name: 'dispatch', route: '/dispatch' }); +agent.setPromptText('You are Ada, the dispatcher for Bayview Taxi.'); + +agent.onCallStart(async (rawData) => { + const caller = String(rawData.caller_id_num ?? ''); + // Look the caller up in your system of record. + const rider = { id: 'rider-1042', name: 'Sam' }; + return new FunctionResult(`The caller ${caller} is ${rider.name}, a returning rider.`) + .updateGlobalData({ rider_id: rider.id }); +}); + +await agent.serve(); +``` diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/on-summary.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/on-summary.mdx index 83a972bb1e..94c14e8468 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/on-summary.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/on-summary.mdx @@ -14,7 +14,9 @@ them to a CRM, triggering follow-up workflows, or logging call outcomes. A post-prompt must be configured via [`setPostPrompt()`][set-post-prompt] -for summaries to be generated. +for summaries to be generated. With a dynamic config callback set, this runs on the +request's configured copy of the agent. A summary delivery must carry the token the +SDK adds to the post-prompt URL for that call, or it is refused with `403`. The default implementation does nothing. You must override it in a subclass or set a @@ -36,7 +38,10 @@ to receive summaries at an external endpoint. ## **Returns** -`void | Promise` +`void | Record | Promise>` -- For a +request whose `action` is `fetch_conversation`, return the conversation (for +example `{ conversation_summary: '...' }`); the post-prompt endpoint sends it back +instead of `{ success: true }`. Otherwise the return value is ignored. ## **Example** diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/serve.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/serve.mdx index 5804237453..a92bb23cf3 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/serve.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/serve.mdx @@ -19,6 +19,11 @@ The server automatically includes: - Security headers middleware - Basic Auth middleware +Set `SWML_SSL_ENABLED`, `SWML_SSL_CERT_PATH`, and `SWML_SSL_KEY_PATH` to serve HTTPS; +webhook URLs then use `https`, on `SWML_SSL_DOMAIN` when it's set. When the certificate +or key file is missing, it warns and serves HTTP. See +[environment variables](/docs/server-sdks/reference/typescript/agents/configuration/environment-variables). + [`run()`][run] is an alias for `serve()`. Both methods are equivalent in the TypeScript SDK. diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/set-dynamic-config-callback.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/set-dynamic-config-callback.mdx index a24244e3db..fe3b1abb54 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/set-dynamic-config-callback.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/set-dynamic-config-callback.mdx @@ -6,25 +6,47 @@ max-toc-depth: 3 --- [ref-agentbase]: /docs/server-sdks/reference/typescript/agents/agent-base +[add-per-call-config]: /docs/server-sdks/reference/typescript/agents/agent-base/add-per-call-config +[define-tool]: /docs/server-sdks/reference/typescript/agents/agent-base/define-tool +[on-summary]: /docs/server-sdks/reference/typescript/agents/agent-base/on-summary -Set a callback that runs on every incoming SWML request, receiving an ephemeral copy -of the agent so you can dynamically configure **any** aspect of it -- prompts, -parameters, languages, tools, global data, etc. -- based on the request's query -parameters, body, or headers. +Set a callback that configures a per-request copy of the agent, so you can +dynamically configure **any** aspect of it -- prompts, parameters, languages, +tools, global data, etc. -- based on the request's query parameters, body, or +headers. + +On each request that renders SWML, runs a SWAIG function, or delivers a summary, +the SDK makes a copy of the agent, calls the callback with the request's query +parameters, body, and headers (credential-bearing headers removed) and the copy, +and handles the request with the copy. Tool handlers receive the copy as their +third argument (see [`defineTool()`][define-tool]), and +[`onSummary()`][on-summary] runs on it. This is the primary mechanism for multi-tenant or per-caller customization. +Setting it replaces every callback registered so far, including those from +[`addPerCallConfig()`][add-per-call-config], which adds a callback instead. -The `agent` argument is an **ephemeral copy** of the base agent — changes made -inside the callback apply only to the current request and do not persist to the -base agent or affect other requests. +The `agent` argument is an **ephemeral copy** of the base agent. It has its own +prompt, tools, skills list, hints, languages, pronunciations, params, global data, +function includes, LLM params, fillers, call-flow verbs, SWAIG query params, native +functions, MCP servers, SIP usernames, routing callbacks, and contexts, so changing +any of them affects only the current request. Anything else is shared with the +base agent and every request in flight, such as a field a subclass adds, a skill +instance, or an object that isn't plain data (a `URL`, a class instance): assigning +a new value on the copy is safe, mutating a shared object in place is not. +A handler that captured the base agent (an arrow function, or a method bound with +`.bind(this)`) still sees the base agent, not the copy; read per-call configuration +from the handler's third argument. A subclass's JavaScript `#private` fields aren't +on the copy, so methods that read them throw there; use TypeScript `private` fields. + ## **Parameters** A function with the signature - `(queryParams: Record, bodyParams: Record, headers: Record, agent: AgentBase) => void | Promise`. + `(queryParams: Record, bodyParams: SwmlRequestData, headers: Record, agent: AgentBase) => void | Promise`. Use the `agent` argument to call any configuration method on the ephemeral copy. diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/validate-basic-auth.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/validate-basic-auth.mdx index 4a0fe37f5c..f526e28f88 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/validate-basic-auth.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/validate-basic-auth.mdx @@ -1,21 +1,17 @@ --- title: "validateBasicAuth" slug: reference/typescript/agents/agent-base/validate-basic-auth -description: Override to add custom basic-auth validation logic. +description: Check a request's basic-auth credentials, or override it to check them another way. max-toc-depth: 3 --- -Lifecycle hook for custom basic-auth validation. The default implementation -returns `true`. Subclasses can override this to add secondary validation logic -(e.g., database lookups, external identity providers). +Check a request's basic-auth credentials. Every route that requires basic auth +calls it. The default compares them with the agent's credentials in constant time +and returns `false` when the agent has no credentials. Override it to check +credentials another way, such as a database lookup or an external identity +provider. - -This hook is defined but **not yet called** by the built-in Hono `basicAuth` -middleware. The middleware currently validates credentials directly against the -stored username/password pair. Overriding this method will have no effect on -request authentication in the current SDK version. It is reserved for future -integration. - +An override replaces the comparison. Call `super.validateBasicAuth()` to keep it. ## **Parameters** @@ -47,7 +43,8 @@ class SecureAgent extends AgentBase { username: string, password: string, ): Promise { - // Custom validation against an external user store + if (await super.validateBasicAuth(username, password)) return true; + // Fall back to an external user store const response = await fetch('https://auth.example.com/verify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/agent-server/run.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/agent-server/run.mdx index 4f93fccc8b..84d4cad4f5 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/agent-server/run.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/agent-server/run.mdx @@ -10,6 +10,11 @@ to serve the Hono application with all registered agents. Handles server mode on for serverless deployments (AWS Lambda, Google Cloud Functions, Azure Functions), use `ServerlessAdapter` instead. +Set `SWML_SSL_ENABLED`, `SWML_SSL_CERT_PATH`, and `SWML_SSL_KEY_PATH` to serve HTTPS; +webhook URLs then use `https`, on `SWML_SSL_DOMAIN` when it's set. When the certificate +or key file is missing, it warns and serves HTTP. See +[environment variables](/docs/server-sdks/reference/typescript/agents/configuration/environment-variables). + ## **Parameters** diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/agent-server/unregister.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/agent-server/unregister.mdx index 5fd6f1479b..a85ee51bfe 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/agent-server/unregister.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/agent-server/unregister.mdx @@ -5,14 +5,10 @@ description: "Remove an agent from the server's registry by route." max-toc-depth: 3 --- -Remove an agent from the server's registry. The agent's routes are no longer tracked, -though Hono does not fully remove mounted routers at runtime. - - -Because Hono does not support dynamic route removal, the agent's HTTP routes may still -respond until the server restarts. This method primarily removes the agent from the -internal registry used by `getAgents()` and the root listing endpoint. - +Remove an agent from the server. The next `getApp()` builds the app without the +agent's routes, and a server started with `run()` serves each request from the current +app, so the routes stop being served. The agent no longer appears in `getAgents()` or +the root listing endpoint. ## **Parameters** diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/swaig-function/index.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/swaig-function/index.mdx index 8d3347be00..2eb0302688 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/swaig-function/index.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/swaig-function/index.mdx @@ -54,9 +54,10 @@ const fn = new SwaigFunction(opts); - The handler invoked when the AI calls this tool. Receives parsed - arguments and optional raw request data. Can return a `FunctionResult`, - a plain object, or a string. May be async. + The handler invoked when the AI calls this tool. Receives + `(args, rawData, agent?)`: the parsed arguments, the raw request data, and the + agent running the call (with a dynamic config callback, the request's configured + copy). Can return a `FunctionResult`, a plain object, or a string. May be async. From 1deaaeffc8475c4c98003860369be9d74368a1a1 Mon Sep 17 00:00:00 2001 From: August Date: Thu, 22 Oct 2026 19:33:47 +0000 Subject: [PATCH 04/15] docs(server-sdks): sync TypeScript FunctionResult and SemanticGate reference to 3.7.1 --- .../pages/ai/guides/semantic-gates.mdx | 4 +- .../agents/function-result/change-voice.mdx | 38 ++++++++++++ .../agents/function-result/hold.mdx | 62 ++++++++++++++++--- .../agents/function-result/index.mdx | 8 ++- .../function-result/join-conference.mdx | 6 +- .../typescript/agents/function-result/pay.mdx | 24 ++++--- .../function-result/rpc-ai-global-data.mdx | 58 +++++++++++++++++ .../agents/function-result/rpc-ai-message.mdx | 44 +++++++++---- .../typescript/agents/function-result/tap.mdx | 7 ++- .../typescript/agents/semantic-gate/index.mdx | 20 +++--- .../agents/semantic-gate/to-dict.mdx | 7 ++- 11 files changed, 229 insertions(+), 49 deletions(-) create mode 100644 fern/products/server-sdks/pages/reference/typescript/agents/function-result/change-voice.mdx create mode 100644 fern/products/server-sdks/pages/reference/typescript/agents/function-result/rpc-ai-global-data.mdx diff --git a/fern/products/platform/pages/ai/guides/semantic-gates.mdx b/fern/products/platform/pages/ai/guides/semantic-gates.mdx index bbf32ed575..335a851856 100644 --- a/fern/products/platform/pages/ai/guides/semantic-gates.mdx +++ b/fern/products/platform/pages/ai/guides/semantic-gates.mdx @@ -461,8 +461,8 @@ default. Set the URL to show your own image instead. -The Python Server SDK accepts `visual_input` on a gate from version 3.7.1. The TypeScript Server SDK -doesn't accept it yet, so define video gates in SWML. +The Server SDKs accept `visual_input` on a gate from version 3.7.1: `visual_input` in Python and the +`visualInput` option in TypeScript. ## Tune or turn off gates for a call diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/function-result/change-voice.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/function-result/change-voice.mdx new file mode 100644 index 0000000000..ee3e9accd1 --- /dev/null +++ b/fern/products/server-sdks/pages/reference/typescript/agents/function-result/change-voice.mdx @@ -0,0 +1,38 @@ +--- +title: "changeVoice" +slug: reference/typescript/agents/function-result/change-voice +description: Change the AI's voice for the rest of the call. +max-toc-depth: 3 +--- + +[functionresult]: /docs/server-sdks/reference/typescript/agents/function-result +[add-language]: /docs/server-sdks/reference/typescript/agents/agent-base/add-language + +Change the AI's voice for the rest of the call. Sends the `change_voice` action. + +The new voice replaces the voice of the language currently in use, and may be on a +different TTS engine. The platform applies it at the next batch of speech, never +mid-utterance, and it then stays for that language for the rest of the call. If the +new voice can't be opened, the platform falls back to its fallback voice. + +## Parameters + + + Voice spec in `engine.voice:model` form, the same form a language's voice takes + in [`addLanguage()`][add-language], such as `"elevenlabs.rachel"`. The `engine.` + prefix and the `:model` suffix are optional. Throws an `Error` when the value is + empty or only whitespace, which the platform would ignore. + + +## Returns + +[`FunctionResult`][functionresult] -- `this`, for chaining. + +## Example + +```typescript {3-4} +import { FunctionResult } from '@signalwire/sdk'; + +const result = new FunctionResult('Tell the caller you are switching to a calmer voice.') + .changeVoice('elevenlabs.rachel'); +``` diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/function-result/hold.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/function-result/hold.mdx index a5acf18abb..bd6eddad49 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/function-result/hold.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/function-result/hold.mdx @@ -1,31 +1,73 @@ --- title: "hold" slug: reference/typescript/agents/function-result/hold -description: Put the call on hold with an optional timeout. +description: Put the call on hold, optionally announcing it and choosing the step the call resumes in. max-toc-depth: 3 --- [functionresult]: /docs/server-sdks/reference/typescript/agents/function-result +[settoolresponse]: /docs/server-sdks/reference/typescript/agents/function-result/set-tool-response +[swmlchangestep]: /docs/server-sdks/reference/typescript/agents/function-result/swml-change-step -Put the call on hold. The caller hears hold music until the hold is released -or the timeout expires. +Put the call on hold. The caller hears hold music until the hold is released or +the timeout expires. -## **Parameters** +During a hold, speech detection is paused and the agent doesn't respond, so +anything the caller needs to hear must be said before the hold takes effect. +Passing `prompt` does that: it becomes the structured response's `tool_prompt` +(with `tool_result: "status: on hold"`, see [`setToolResponse()`][settoolresponse]) +and turns on `postProcess`, so the model takes one more turn, and can tell the +caller, before the hold runs. + +`step` and `timeoutStep` move the call to a step when the hold ends: `step` when +the call is taken off hold, `timeoutStep` when the hold times out. Unlike +[`swmlChangeStep()`][swmlchangestep], which applies at once, they wait for the +hold to end. Omit one, and a hold that ends that way resumes in the current step. +With neither, the action is the bare timeout (`{ hold: 300 }`); with either, it's +`{ hold: { timeout, step, timeout_step } }`. + +## Parameters + + + Instruction for the model to act on before the hold. A number here is taken as + `timeout`, so `hold(120)` sets a 120-second hold. + Maximum hold duration in seconds. Clamped to the range 0--900 (15 minutes max). -## **Returns** + + Step to move to when the call is taken off hold. + + + + Step to move to when the hold times out. + + +## Returns [`FunctionResult`][functionresult] -- `this`, for chaining. -## **Example** +## Examples + +### Default 300-second hold + +```typescript {3} +import { FunctionResult } from '@signalwire/sdk'; + +const result = new FunctionResult('Please hold.').hold(); +``` + +### Announce the hold and route what happens next -```typescript {5} +```typescript {3-8} import { FunctionResult } from '@signalwire/sdk'; -// Default 300-second hold -const result = new FunctionResult('Please hold.') - .hold(); +const result = new FunctionResult().hold( + 'Tell the caller you are checking whether a dispatcher is available.', + 300, + 'back_with_dispatcher', + 'take_a_message', +); ``` diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/function-result/index.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/function-result/index.mdx index 4b51e34b99..e1e89a10a2 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/function-result/index.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/function-result/index.mdx @@ -245,6 +245,9 @@ agent.run(); Stop the currently playing background audio file. + + Change the AI's voice for the rest of the call. + Start recording the call in the background. @@ -403,7 +406,10 @@ agent.run(); Dial out to a phone number with a destination SWML URL via RPC. - Inject a message into the AI agent running on another call. + Send a message, global data, or both to the AI agent running on another call. + + + Merge data into another call's global data without a conversation turn. Release another call from hold via RPC. diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/function-result/join-conference.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/function-result/join-conference.mdx index a053aa2381..41615749d2 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/function-result/join-conference.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/function-result/join-conference.mdx @@ -52,8 +52,10 @@ used internally. Passing any non-default parameter triggers the full object form When omitted, default hold music is used. - - Maximum number of participants. + + Maximum number of participants: an integer of 2 or more, sent whenever it's + given. A numeric string is converted, and a SWML variable reference such as + `'${room_size}'` is passed through. Throws an `Error` for anything else. diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/function-result/pay.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/function-result/pay.mdx index f780558e51..742ff79af4 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/function-result/pay.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/function-result/pay.mdx @@ -40,16 +40,19 @@ voice, then submits to your payment connector endpoint. Payment method type. Currently only `"credit-card"` is supported. - - Seconds to wait for the next DTMF digit before timing out. + + Seconds to wait for the next DTMF digit before timing out. An integer, a numeric + string, or a SWML variable reference. - - Number of retry attempts if payment collection fails. + + Number of retry attempts if payment collection fails. An integer, a numeric + string, or a SWML variable reference. - - Whether to prompt the caller for the card's security code (CVV). + + Whether to prompt the caller for the card's security code (CVV). A boolean, + `"true"` or `"false"`, or a SWML variable reference. @@ -57,8 +60,13 @@ voice, then submits to your payment connector endpoint. to skip, or a string with the actual postal code to use without prompting. - - Minimum number of digits required for the postal code. + + Minimum number of digits required for the postal code. An integer, a numeric + string, or a SWML variable reference. + + `timeout`, `maxAttempts`, `securityCode`, `minPostalCodeLength`, and `postalCode` + are sent as strings, as the platform reads them. Each is checked first, and an + invalid value throws an `Error`. diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/function-result/rpc-ai-global-data.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/function-result/rpc-ai-global-data.mdx new file mode 100644 index 0000000000..1f283f75e5 --- /dev/null +++ b/fern/products/server-sdks/pages/reference/typescript/agents/function-result/rpc-ai-global-data.mdx @@ -0,0 +1,58 @@ +--- +title: "rpcAiGlobalData" +slug: reference/typescript/agents/function-result/rpc-ai-global-data +description: Merge data into another call's global data without adding a conversation turn. +max-toc-depth: 3 +--- + +[functionresult]: /docs/server-sdks/reference/typescript/agents/function-result +[rpcaimessage]: /docs/server-sdks/reference/typescript/agents/function-result/rpc-ai-message + +Merge data into another call's global data without injecting a conversation turn. +A thin wrapper over [`rpcAiMessage()`][rpcaimessage] with only `globalData` set. +Use it when the other call needs a value rather than an instruction; that call's +prompt reads it back with `${global_data.key}`. + +## Parameters + + + Call ID of the target call. + + +"} required={true} toc={true}> + Object merged into that call's global data. + + +## Returns + +[`FunctionResult`][functionresult] -- `this`, for chaining. + +## Example + +```typescript {20-22} +import { AgentBase, FunctionResult } from '@signalwire/sdk'; + +const agent = new AgentBase({ name: 'dispatch', route: '/dispatch' }); +agent.setPromptText('You are Ada, the dispatcher for Bayview Taxi.'); + +agent.defineTool({ + name: 'share_eta', + description: 'Send the ETA to the waiting caller', + parameters: { + type: 'object', + properties: { + waiting_call_id: { type: 'string', description: 'Call ID of the waiting caller' }, + eta_minutes: { type: 'integer', description: 'Minutes until pickup' }, + }, + }, + required: ['waiting_call_id', 'eta_minutes'], + handler: (args) => { + const waitingCallId = String(args.waiting_call_id); + return new FunctionResult('Tell the driver the passenger has been updated.') + .rpcAiGlobalData(waitingCallId, { eta_minutes: args.eta_minutes }) + .rpcAiUnhold(waitingCallId); + }, +}); + +await agent.serve(); +``` diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/function-result/rpc-ai-message.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/function-result/rpc-ai-message.mdx index 89f242eabe..7bbbbeaa84 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/function-result/rpc-ai-message.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/function-result/rpc-ai-message.mdx @@ -1,39 +1,59 @@ --- title: "rpcAiMessage" slug: reference/typescript/agents/function-result/rpc-ai-message -description: Inject a message into the AI agent running on another call. +description: Send a message, global data, or both to the AI agent running on another call. max-toc-depth: 3 --- [functionresult]: /docs/server-sdks/reference/typescript/agents/function-result +[rpcaiglobaldata]: /docs/server-sdks/reference/typescript/agents/function-result/rpc-ai-global-data -Inject a message into the AI agent running on another call. Useful for -cross-call coordination, such as notifying a held caller's agent about a -status change or instructing it to relay information. +Send a message, global data, or both to the AI agent running on another call. +Useful for cross-call coordination, such as notifying a held caller's agent about +a status change. -## **Parameters** +The two payloads behave differently. `messageText` lands as a turn in the other +agent's conversation, where it competes with everything else arriving at that +moment. `globalData` is merged into the other call's global data, where it stays +silent until a prompt expands it with `${global_data.your_key}`, so data is the +better channel for content a later step needs to say. To send data only, use +[`rpcAiGlobalData()`][rpcaiglobaldata]. + +## Parameters Call ID of the target call whose AI agent should receive the message. - + The message text to inject into the target AI's conversation. - Role for the injected message. Typically `"system"` for instructions. + Role for the injected message, sent with the message. Typically `"system"` for + instructions. + + +"} toc={true}> + Object merged into the target call's global data. -## **Returns** +Throws an `Error` when neither `messageText` nor `globalData` is given. + +## Returns [`FunctionResult`][functionresult] -- `this`, for chaining. -## **Example** +## Example -```typescript {4} +```typescript {4-9} import { FunctionResult } from '@signalwire/sdk'; -const result = new FunctionResult() - .rpcAiMessage('call-id-123', 'The caller needs help with billing.'); +const result = new FunctionResult('Tell the driver the passenger has been updated.') + .rpcAiMessage( + '', + 'Your driver is on the way. Tell the caller their pickup is confirmed.', + 'system', + { eta_minutes: 8 }, + ); ``` diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/function-result/tap.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/function-result/tap.mdx index 24ddc9e31c..36306ba4a2 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/function-result/tap.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/function-result/tap.mdx @@ -29,11 +29,12 @@ WebSocket (`wss://`, `ws://`) and RTP (`rtp://`) destinations. specific tap. If omitted, a default ID is generated. - - Audio direction to tap. + + Audio direction to tap. Always sent, since the `tap` verb's own default is + `"speak"`. Throws an `Error` for any other value. - `"speak"` -- what the party says - - `"hear"` -- what the party hears + - `"listen"` -- what the party hears - `"both"` -- both directions diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/semantic-gate/index.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/semantic-gate/index.mdx index db9ad82c7b..d9a1e4fdda 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/semantic-gate/index.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/semantic-gate/index.mdx @@ -24,8 +24,9 @@ failing gate in list order. The decision model reads `conversation` (the recent dialogue), `semantic_state` (the call's `global_data.semantic_state`, or `{}`), and `proposed_function` (the -function's name, description, and the arguments the AI is calling it with). Name -what each question is about with those names in backticks. They label what the +function's name, description, and the arguments the AI is calling it with), plus +`visual_input` (the caller's newest video frames) when a gate sets `visualInput`. +Name what each question is about with those names in backticks. They label what the decision model reads; nothing is substituted. The decision model reads literally and doesn't do arithmetic or compare dates, so put a computed result in the state with [`setSemanticState()`][set-semantic-state] instead of asking for it. @@ -45,12 +46,6 @@ function names: `startup_hook`, `hangup_hook`, `check_for_input`, `end_call`, `get_ideal_strategy`, `pause_conversation`. - -The TypeScript SDK has no `visual_input` option on a gate, and it throws on a gate -object with a `visual_input` key. To gate a function on the caller's video, write -the function in SWML with the [gate object][swml-gates]. - - ## Parameters @@ -92,6 +87,15 @@ the function in SWML with the [gate object][swml-gates]. What "no" means, at most 2 KB. Sent as `criteria.false`. + + How many of the caller's newest video frames the decision model sees with this + gate, sent as `visual_input`: `true` is 1, `false` or `0` is none, or a whole + number up to `MAX_VISUAL_INPUT` (5). Every gate on a function sees as many frames + as the gate that asks for the most. If the call has no video frame to show, the + gate fails without being asked. The gate's provider must accept images. Omitted by + default. Requires SDK 3.7.1 or later. + + ## Methods diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/semantic-gate/to-dict.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/semantic-gate/to-dict.mdx index f492f9018f..1b5694bb8c 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/semantic-gate/to-dict.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/semantic-gate/to-dict.mdx @@ -8,8 +8,8 @@ max-toc-depth: 3 [swml-gates]: /docs/swml/reference/calling/ai/swaig/functions/gates Return the gate as the object sent in the function's `gates` list, in the shape of -the SWML [gate object][swml-gates]. Keys that weren't set (`id`, `criteria`) are -left out. +the SWML [gate object][swml-gates]. Keys that weren't set (`id`, `criteria`, +`visual_input`) are left out. ## Parameters @@ -18,7 +18,8 @@ None. ## Returns `Record` -- The gate: `id` (when set), `question`, `criteria` -(when `trueMeans` or `falseMeans` is set), `threshold`, and `on_fail`. +(when `trueMeans` or `falseMeans` is set), `threshold`, `on_fail`, and +`visual_input` (when `visualInput` is set). ## Examples From 5d4b046049bbca070ddf4c1684953ac19157e54a Mon Sep 17 00:00:00 2001 From: August Date: Thu, 22 Oct 2026 19:33:47 +0000 Subject: [PATCH 05/15] docs(server-sdks): correct TypeScript DataMap body and template scoping --- .../agent-base/register-swaig-function.mdx | 2 +- .../typescript/agents/data-map/body.mdx | 19 +++++++-------- .../agents/data-map/fallback-output.mdx | 2 +- .../agents/data-map/global-error-keys.mdx | 2 +- .../agents/data-map/helper-functions.mdx | 15 +++++++----- .../typescript/agents/data-map/index.mdx | 17 ++++++++++---- .../typescript/agents/data-map/output.mdx | 7 +++--- .../typescript/agents/data-map/params.mdx | 23 ++++++++++--------- .../agents/data-map/register-with-agent.mdx | 2 +- .../agents/data-map/to-swaig-function.mdx | 2 +- .../agents/data-map/webhook-expressions.mdx | 2 +- .../typescript/agents/data-map/webhook.mdx | 6 ++--- .../reference/typescript/agents/helpers.mdx | 2 +- .../swml-service/register-swaig-function.mdx | 2 +- 14 files changed, 57 insertions(+), 46 deletions(-) diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/register-swaig-function.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/register-swaig-function.mdx index 19234eaf3e..050bf76761 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/register-swaig-function.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/agent-base/register-swaig-function.mdx @@ -41,7 +41,7 @@ const plantTool = new DataMap('get_plant_info') .parameter('plantId', 'string', 'Plant ID', { required: true }) .webhook('GET', 'https://api.plants.example.com/plants/${args.plantId}') .output(new FunctionResult( - 'The plant ${args.plantId} needs ${response.water_schedule} watering.', + 'The plant ${input.args.plantId} needs ${water_schedule} watering.', )); // Register the DataMap as a SWAIG function diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/data-map/body.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/data-map/body.mdx index 778f9a8f1e..c03b9bce59 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/data-map/body.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/data-map/body.mdx @@ -1,26 +1,23 @@ --- title: "body" slug: reference/typescript/agents/data-map/body -description: Set the request body for a webhook. +description: Set the JSON request body for a webhook, written as its params. max-toc-depth: 3 --- [ref-datamap]: /docs/server-sdks/reference/typescript/agents/data-map [params]: /docs/server-sdks/reference/typescript/agents/data-map/params -Set the request body for the most recently added webhook. Used for POST/PUT -requests. Writes to the `body` key of the webhook definition. - - -`body()` and [`params()`][params] write to different keys in the webhook -specification: `body` sets the request body, while `params` sets -query/form parameters. - +Set the JSON request body for the most recently added webhook; the same as +[`params()`][params]. The platform reads a webhook's request body from its +`params` field and has no `body` field, so this sets `params`, replacing any +`params` already set, and the request is sent as a POST. ## **Parameters** "} required={true} toc={true}> - Request body data. Supports `${variable}` substitutions. + Request body object. Values can contain templates, which read the call data, so + arguments are `${args.name}`. ## **Returns** @@ -41,5 +38,5 @@ search headers: { Authorization: 'Bearer TOKEN' }, }) .body({ query: '${args.query}', limit: 3 }) - .output(new FunctionResult('Found: ${response.results[0].title}')); + .output(new FunctionResult('Found: ${results[0].title}')); ``` diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/data-map/fallback-output.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/data-map/fallback-output.mdx index 0ff313b858..cfd49ed3fc 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/data-map/fallback-output.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/data-map/fallback-output.mdx @@ -27,6 +27,6 @@ import { DataMap, FunctionResult } from '@signalwire/sdk'; const dm = new DataMap('get_weather'); dm.webhook('GET', 'https://api.weather.com/v1/current?q=${args.city}'); -dm.output(new FunctionResult('Weather: ${response.current.condition.text}')); +dm.output(new FunctionResult('Weather: ${current.condition.text}')); dm.fallbackOutput(new FunctionResult('Weather service is currently unavailable.')); ``` diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/data-map/global-error-keys.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/data-map/global-error-keys.mdx index 7649ccc34c..5edd05faee 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/data-map/global-error-keys.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/data-map/global-error-keys.mdx @@ -29,5 +29,5 @@ import { DataMap, FunctionResult } from '@signalwire/sdk'; const dm = new DataMap('get_weather'); dm.globalErrorKeys(['error', 'fault', 'exception']); dm.webhook('GET', 'https://api.weather.com/v1/current?q=${args.city}'); -dm.output(new FunctionResult('Weather: ${response.current.condition.text}')); +dm.output(new FunctionResult('Weather: ${current.condition.text}')); ``` diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/data-map/helper-functions.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/data-map/helper-functions.mdx index 21da1388db..6102935cf7 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/data-map/helper-functions.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/data-map/helper-functions.mdx @@ -15,7 +15,7 @@ import { createSimpleApiTool } from '@signalwire/sdk'; const weatherTool = createSimpleApiTool({ name: 'get_weather', url: 'https://api.weather.com/v1/current?q=${args.location}', - responseTemplate: 'Weather: ${response.current.condition.text}, ${response.current.temp_f}F', + responseTemplate: 'Weather: ${current.condition.text}, ${current.temp_f}F', parameters: { location: { type: 'string', description: 'City name', required: true }, }, @@ -32,11 +32,13 @@ configuration. - API endpoint URL. + API endpoint URL. Reads the arguments as `${args.name}`. - Template string for formatting the response. + Template string for formatting the response; it becomes the webhook's output. + Reads the response's fields from the root, such as `${current.temp_f}`, and the + arguments as `${input.args.name}`. '} toc={true}> @@ -49,11 +51,12 @@ configuration. "} toc={true}> - HTTP headers. + HTTP headers, sent as written. "} toc={true}> - Request body for POST/PUT. + JSON request body, set as the webhook's `params`, so a tool with a body is sent + as a POST. An empty object is left out. @@ -114,7 +117,7 @@ import { const weather = createSimpleApiTool({ name: 'get_weather', url: 'https://api.weatherapi.com/v1/current.json?key=KEY&q=${enc:args.city}', - responseTemplate: 'Weather in ${args.city}: ${response.current.condition.text}', + responseTemplate: 'Weather in ${input.args.city}: ${current.condition.text}', parameters: { city: { type: 'string', description: 'City name', required: true }, }, diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/data-map/index.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/data-map/index.mdx index 250f37773d..5d8ef10308 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/data-map/index.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/data-map/index.mdx @@ -52,14 +52,23 @@ full specification. ## **Variable Substitution Patterns** +What a template can read depends on where it is. A webhook's `url` and `params`, +the top-level expressions, and the fallback output read the call data, where an +argument is `${args.city}`. A webhook's `output`, `expressions`, and `foreach` read +that webhook's response instead: a JSON object's fields from the root (`${temp}`), +a JSON array under `array` (`${array[0].joke}`), and the call data under `input`, +so an argument there is `${input.args.city}`; `${args.city}` expands to nothing. +Header values are sent as written, with no templates expanded. + | Pattern | Description | |---------|-------------| -| `${args.param}` | Function argument value | +| `${args.param}` | Function argument value, in a webhook's `url` and `params`, top-level expressions, and the fallback output | +| `${input.args.param}` | Function argument value, in a webhook's `output`, `expressions`, and `foreach` | | `${enc:args.param}` | URL-encoded argument (use in webhook URLs) | | `${lc:args.param}` | Lowercase argument value | | `${fmt_ph:args.phone}` | Format as phone number | -| `${response.field}` | API response field | -| `${response.arr[0]}` | Array element in response | +| `${field}` | Field of the webhook's JSON response, read from the root | +| `${array[0].field}` | Element of a webhook response that is a JSON array | | `${global_data.key}` | Global session data | | `${meta_data.key}` | Call metadata | | `${this.field}` | Current item in foreach | @@ -145,7 +154,7 @@ const dm = new DataMap('get_weather'); dm.purpose('Look up current weather for a location'); dm.parameter('city', 'string', 'City name', { required: true }); dm.webhook('GET', 'https://api.weather.com/v1/current?q=${args.city}'); -dm.output(new FunctionResult('Weather in ${args.city}: ${response.current.condition.text}, ${response.current.temp_f}F')); +dm.output(new FunctionResult('Weather in ${input.args.city}: ${current.condition.text}, ${current.temp_f}F')); ``` ### Expression-based control diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/data-map/output.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/data-map/output.mdx index f030464d62..5dd2ba0070 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/data-map/output.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/data-map/output.mdx @@ -21,8 +21,9 @@ to say even when external APIs are unavailable. ## **Parameters** - A FunctionResult defining the response template. Use `${response.field}` to - reference API response fields. + A FunctionResult defining the response template. Read the response's fields + from the root, such as `${current.temp_f}`, and the function's arguments as + `${input.args.name}`. ## **Returns** @@ -37,5 +38,5 @@ import { DataMap, FunctionResult } from '@signalwire/sdk'; const dm = new DataMap('get_weather'); dm.webhook('GET', 'https://api.weather.com/v1/current'); -dm.output(new FunctionResult('The weather is ${response.current.condition.text}.')); +dm.output(new FunctionResult('The weather is ${current.condition.text}.')); ``` diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/data-map/params.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/data-map/params.mdx index e97abdb3d1..dd8a95f6dd 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/data-map/params.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/data-map/params.mdx @@ -1,25 +1,26 @@ --- title: "params" slug: reference/typescript/agents/data-map/params -description: "Set query or form parameters for the most recently added webhook." +description: "Set the JSON request body for the most recently added webhook." max-toc-depth: 3 --- [ref-datamap]: /docs/server-sdks/reference/typescript/agents/data-map +[body]: /docs/server-sdks/reference/typescript/agents/data-map/body -Set request query/form parameters for the most recently added webhook. Writes -to the `params` key of the webhook definition. +Set the JSON request body for the most recently added webhook, written as its +`params`. [`body()`][body] does the same. - -`params()` is **not** an alias for `body()`. They write to different keys in the -webhook specification: `body` sets the request body, while `params` sets -query/form parameters. - +The platform sends `params` as the request's JSON body, not as URL query +parameters, expanding templates in its values first, and a webhook with `params` is +always a POST, whatever its method. Put query-string values for a GET request in the +URL instead. ## **Parameters** "} required={true} toc={true}> - Request parameters. Supports `${variable}` substitutions. + Request body object. Values can contain templates, which read the call data, so + arguments are `${args.name}`. ## **Returns** @@ -35,7 +36,7 @@ import { DataMap, FunctionResult } from '@signalwire/sdk'; const dm = new DataMap('search_docs'); dm.purpose('Search documentation'); dm.parameter('query', 'string', 'Search query', { required: true }); -dm.webhook('GET', 'https://api.docs.example.com/search'); +dm.webhook('POST', 'https://api.docs.example.com/search'); dm.params({ q: '${args.query}', limit: 10 }); -dm.output(new FunctionResult('Found: ${response.results[0].title}')); +dm.output(new FunctionResult('Found: ${results[0].title}')); ``` diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/data-map/register-with-agent.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/data-map/register-with-agent.mdx index eeb1561c71..c87ee81fae 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/data-map/register-with-agent.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/data-map/register-with-agent.mdx @@ -31,7 +31,7 @@ const dm = new DataMap('get_weather'); dm.purpose('Look up current weather'); dm.parameter('city', 'string', 'City name', { required: true }); dm.webhook('GET', 'https://api.weather.com/v1/current?q=${args.city}'); -dm.output(new FunctionResult('Weather: ${response.current.condition.text}')); +dm.output(new FunctionResult('Weather: ${current.condition.text}')); const agent = new AgentBase({ name: 'weather-agent' }); dm.registerWithAgent(agent); diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/data-map/to-swaig-function.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/data-map/to-swaig-function.mdx index cda5be7a68..5f3e1bbc74 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/data-map/to-swaig-function.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/data-map/to-swaig-function.mdx @@ -24,7 +24,7 @@ const dm = new DataMap('get_weather'); dm.purpose('Look up weather'); dm.parameter('city', 'string', 'City name', { required: true }); dm.webhook('GET', 'https://api.weather.com/v1/current?q=${args.city}'); -dm.output(new FunctionResult('Weather: ${response.current.temp_f}F')); +dm.output(new FunctionResult('Weather: ${current.temp_f}F')); const swaigDef = dm.toSwaigFunction(); console.log(JSON.stringify(swaigDef, null, 2)); diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/data-map/webhook-expressions.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/data-map/webhook-expressions.mdx index e924c210aa..e8a0fbafdc 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/data-map/webhook-expressions.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/data-map/webhook-expressions.mdx @@ -30,7 +30,7 @@ const dm = new DataMap('check_status'); dm.webhook('GET', 'https://api.example.com/status'); dm.webhookExpressions([ { - string: '${response.status}', + string: '${status}', pattern: 'active', output: new FunctionResult('The account is active.').toDict(), }, diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/data-map/webhook.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/data-map/webhook.mdx index 90ce24f7ca..9e9f7d0f97 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/data-map/webhook.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/data-map/webhook.mdx @@ -65,7 +65,7 @@ weather .webhook('GET', 'https://api.weatherapi.com/v1/current.json?key=YOUR_API_KEY&q=${enc:args.city}', { headers: { Accept: 'application/json' }, }) - .output(new FunctionResult('Weather: ${response.current.condition.text}')); + .output(new FunctionResult('Weather: ${current.condition.text}')); console.log(weather.toSwaigFunction()); ``` @@ -80,8 +80,8 @@ search .purpose('Search across multiple providers') .parameter('query', 'string', 'Search query', { required: true }) .webhook('GET', 'https://primary-api.example.com/search?q=${enc:args.query}') - .output(new FunctionResult('Primary: ${response.result}')) + .output(new FunctionResult('Primary: ${result}')) .webhook('GET', 'https://backup-api.example.com/search?q=${enc:args.query}') - .output(new FunctionResult('Backup: ${response.result}')) + .output(new FunctionResult('Backup: ${result}')) .fallbackOutput(new FunctionResult('All search providers are unavailable.')); ``` diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/helpers.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/helpers.mdx index a29b797cf1..516ef504da 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/helpers.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/helpers.mdx @@ -145,7 +145,7 @@ const weatherTool = createSimpleApiTool({ name: 'get_weather', url: 'https://api.weather.com/v1/current?key=API_KEY&q=${args.location}', responseTemplate: - 'Weather in ${args.location}: ${response.current.condition.text}, ${response.current.temp_f}F', + 'Weather in ${input.args.location}: ${current.condition.text}, ${current.temp_f}F', parameters: { location: { type: 'string', diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/swml-service/register-swaig-function.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/swml-service/register-swaig-function.mdx index 74ea4c058b..e0fc43396c 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/swml-service/register-swaig-function.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/swml-service/register-swaig-function.mdx @@ -37,7 +37,7 @@ const plantTool = new DataMap('get_plant_info') .parameter('plantId', 'string', 'Plant ID', { required: true }) .webhook('GET', 'https://api.plants.example.com/plants/${args.plantId}') .output(new FunctionResult( - 'The plant ${args.plantId} needs ${response.water_schedule} watering.', + 'The plant ${input.args.plantId} needs ${water_schedule} watering.', )); service.registerSwaigFunction(plantTool.toSwaigFunction()); From 868e71c865d59c9e33e6d191f607895c582561df Mon Sep 17 00:00:00 2001 From: August Date: Thu, 22 Oct 2026 19:33:47 +0000 Subject: [PATCH 06/15] docs(server-sdks): sync TypeScript skills reference to 3.7.1 --- .../agents/skill-base/get-instance-key.mdx | 9 +++--- .../skill-base/get-parameter-schema.mdx | 2 +- .../typescript/agents/skill-base/index.mdx | 7 ++--- .../agents/skill-manager/add-skill.mdx | 6 +++- .../skill-manager/load-skill-by-name.mdx | 3 +- .../agents/skill-manager/load-skill.mdx | 4 +++ .../skill-registry/add-skill-directory.mdx | 2 +- .../agents/skill-registry/clear.mdx | 3 +- .../agents/skill-registry/discover-all.mdx | 11 +++---- .../discover-from-directory.mdx | 4 ++- .../typescript/agents/skill-registry/lock.mdx | 6 ++-- .../agents/skill-registry/unregister.mdx | 5 ++-- .../typescript/agents/skills/ask-claude.mdx | 5 ++-- .../agents/skills/custom-skills.mdx | 2 +- .../typescript/agents/skills/google-maps.mdx | 9 +++--- .../typescript/agents/skills/index.mdx | 8 +++-- .../typescript/agents/skills/mcp-gateway.mdx | 13 ++++---- .../agents/skills/native-vector-search.mdx | 30 ++++++++++++------- .../typescript/agents/skills/spider.mdx | 27 ++++++++++------- .../agents/skills/swml-transfer.mdx | 5 ++++ .../typescript/agents/skills/web-search.mdx | 4 ++- 21 files changed, 103 insertions(+), 62 deletions(-) diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/skill-base/get-instance-key.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/skill-base/get-instance-key.mdx index 2dd35cfdbc..63cf13d93c 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/skill-base/get-instance-key.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/skill-base/get-instance-key.mdx @@ -26,16 +26,17 @@ class MySkill extends SkillBase { Instance key behavior: -```typescript {4} +```typescript {5} import { AgentBase } from '@signalwire/sdk'; const agent = new AgentBase({ name: 'demo' }); -agent.addSkill("notify") +// With MySkill registered as "notify": +await agent.addSkillByName('notify'); // Instance key: "notify" -agent.addSkill("notify", {"tool_name": "email"}) +await agent.addSkillByName('notify', { tool_name: 'email' }); // Instance key: "notify_email" -agent.addSkill("notify", {"tool_name": "sms"}) +await agent.addSkillByName('notify', { tool_name: 'sms' }); // Instance key: "notify_sms" ``` diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/skill-base/get-parameter-schema.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/skill-base/get-parameter-schema.mdx index c85a09b471..9b146b0176 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/skill-base/get-parameter-schema.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/skill-base/get-parameter-schema.mdx @@ -25,7 +25,7 @@ and values describe the parameter. | `default` | any | Default value | | `required` | boolean | Whether the parameter is required | | `hidden` | boolean | Hide in UIs (for secrets like API keys) | -| `env_var` | string | Environment variable that can provide this value | +| `env_var` | string | Environment variable a configuration tool can read this value from. A hint only: the SDK doesn't read it, so the skill gets the value from its params | | `enum` | unknown[] | Allowed values | | `min` / `max` | number | Bounds for numeric types | | `items` | object | Schema for array item types | diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/skill-base/index.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/skill-base/index.mdx index c2b370dd1d..0fa662c652 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/skill-base/index.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/skill-base/index.mdx @@ -243,15 +243,12 @@ class MyWeatherSkill extends SkillBase { ### Using a skill -```typescript {5} +```typescript {6} import { AgentBase } from '@signalwire/sdk'; const agent = new AgentBase({ name: 'weather-agent' }); agent.setPromptText("You are a helpful assistant."); -agent.addSkill("weather"); - -// Or with custom configuration -agent.addSkill("weather", { units: "celsius" }); +await agent.addSkillByName('weather_api', { units: 'metric' }); agent.run(); ``` diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/skill-manager/add-skill.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/skill-manager/add-skill.mdx index 242c1e0533..e8dbf39a7d 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/skill-manager/add-skill.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/skill-manager/add-skill.mdx @@ -6,8 +6,12 @@ description: "Add and initialize a skill instance. Fails closed on invalid confi [ref-skillbase]: /docs/server-sdks/reference/typescript/agents/skill-base [ref-setup]: /docs/server-sdks/reference/typescript/agents/skill-base/setup +[ref-loadskill]: /docs/server-sdks/reference/typescript/agents/skill-manager/load-skill -Add a skill to the manager. The call sequence is: +Add a skill to the manager. This records the skill only: it doesn't register +the skill's tools, prompt sections, hints, or global data on an agent. Use +[`loadSkill`][ref-loadskill] or `AgentBase.addSkill()` for that. The call +sequence is: 1. Deduplicate by instance key (multi-instance skills with the same key are skipped with a warning; single-instance duplicates throw). diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/skill-manager/load-skill-by-name.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/skill-manager/load-skill-by-name.mdx index d503eed5ce..d18045fdd2 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/skill-manager/load-skill-by-name.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/skill-manager/load-skill-by-name.mdx @@ -9,7 +9,8 @@ description: "Look up a skill class in the global registry by name and load it." Look up a registered skill class by name in the global [`SkillRegistry`][ref-registry], construct an instance, and add it via -[`addSkill`][ref-addskill]. Returns `[false, message]` if the name is not +[`addSkill`][ref-addskill]. On a manager that belongs to an agent, the skill +is registered on that agent, as with `loadSkill`. Returns `[false, message]` if the name is not registered, the instantiation fails, or `addSkill` throws. ## **Parameters** diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/skill-manager/load-skill.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/skill-manager/load-skill.mdx index 235989a18e..530edd95bf 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/skill-manager/load-skill.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/skill-manager/load-skill.mdx @@ -12,6 +12,10 @@ Construct a skill instance from the provided class and add it via `[success, errorMessage]` tuple — matching the Python SDK's `load_skill` return contract. +On a manager that belongs to an agent, the skill's tools, prompt sections, +hints, and global data are registered on that agent, as with +`agent.addSkill()`. On a standalone manager the call only records the skill. + ## **Parameters** diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/skill-registry/add-skill-directory.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/skill-registry/add-skill-directory.mdx index b26fec2951..befc61a604 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/skill-registry/add-skill-directory.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/skill-registry/add-skill-directory.mdx @@ -9,7 +9,7 @@ description: "Register an external directory of skills with strict validation." Register an external directory containing skill subdirectories. The path is validated (it must exist and be a directory) and de-duplicated before being -added. This is the recommended, strict entry point for registering third-party +added. `discoverAll()` scans these directories along with the search paths. This is the recommended, strict entry point for registering third-party skill directories. diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/skill-registry/clear.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/skill-registry/clear.mdx index 85144910b8..d0bd87383e 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/skill-registry/clear.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/skill-registry/clear.mdx @@ -4,7 +4,8 @@ slug: reference/typescript/agents/skill-registry/clear description: "Remove all skill registrations." --- -Remove all skill registrations from the registry. +Remove every skill registration from the registry except locked skills, which +stay registered. The built-in skills are locked. ## **Parameters** diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/skill-registry/discover-all.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/skill-registry/discover-all.mdx index f687866a65..7109438d0c 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/skill-registry/discover-all.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/skill-registry/discover-all.mdx @@ -1,12 +1,13 @@ --- title: "discoverAll" slug: reference/typescript/agents/skill-registry/discover-all -description: "Discover and register skills from all configured search paths." +description: "Discover and register skills from all configured directories." --- -Discover and register skills from all directories in the search paths list. -Calls `discoverFromDirectory()` for each path added via `addSearchPath()` or -the `SIGNALWIRE_SKILL_PATHS` environment variable. +Discover and register skills from every configured directory: the search paths +(added via `addSearchPath()` or the `SIGNALWIRE_SKILL_PATHS` environment +variable) and the directories added with `addSkillDirectory()`, each scanned +once. Calls `discoverFromDirectory()` for each. ## **Parameters** @@ -14,4 +15,4 @@ None. ## **Returns** -`Promise` -- Array of all newly discovered skill names across all paths. +`Promise` -- Array of all newly discovered skill names across all directories. diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/skill-registry/discover-from-directory.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/skill-registry/discover-from-directory.mdx index e93830184f..7bd9e444cd 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/skill-registry/discover-from-directory.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/skill-registry/discover-from-directory.mdx @@ -4,7 +4,9 @@ slug: reference/typescript/agents/skill-registry/discover-from-directory description: "Discover and register skills from a directory." --- -Discover and register skills from a directory by importing each file. Requires +Discover and register skills from a directory by importing each file. Imports +every `.ts` and `.js` file in the directory (not `.d.ts`), and `skill.ts` (or +`skill.js` when there's no `skill.ts`) in each subdirectory. Requires `SWML_SKILL_DISCOVERY_ENABLED=true` to be set. ## **Parameters** diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/skill-registry/lock.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/skill-registry/lock.mdx index 35808f17cb..964a07aa29 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/skill-registry/lock.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/skill-registry/lock.mdx @@ -4,8 +4,10 @@ slug: reference/typescript/agents/skill-registry/lock description: "Lock skill names to prevent overwriting." --- -Lock one or more skill names to prevent overwriting. If called with no arguments, -locks all currently registered skills. +Lock one or more skill names, so they can't be overwritten, unregistered, or +cleared. The built-in skills are locked, so a built-in can't be replaced by +removing it and registering another class under its name. If called with no +arguments, locks all currently registered skills. ## **Parameters** diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/skill-registry/unregister.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/skill-registry/unregister.mdx index 813c682d7d..8ba28c266c 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/skill-registry/unregister.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/skill-registry/unregister.mdx @@ -4,7 +4,7 @@ slug: reference/typescript/agents/skill-registry/unregister description: "Remove a skill registration by name." --- -Remove a skill registration by name. +Remove a skill registration by name. A locked skill isn't removed. ## **Parameters** @@ -14,4 +14,5 @@ Remove a skill registration by name. ## **Returns** -`boolean` +`boolean` -- `true` if the skill was found and removed; `false` if it isn't +registered or is locked. diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/skills/ask-claude.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/skills/ask-claude.mdx index 266e2759ed..6bb0e399ac 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/skills/ask-claude.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/skills/ask-claude.mdx @@ -16,8 +16,9 @@ sub-tasks that benefit from a dedicated AI query. **Env vars:** `ANTHROPIC_API_KEY` - Anthropic API key. The handler reads the `ANTHROPIC_API_KEY` environment - variable at request time; a config value is not used as a fallback. + Anthropic API key. Falls back to the `ANTHROPIC_API_KEY` environment + variable when the config value is not supplied. `setup()` fails when + neither is set. diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/skills/custom-skills.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/skills/custom-skills.mdx index 4d964ab5c8..977c711d06 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/skills/custom-skills.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/skills/custom-skills.mdx @@ -22,7 +22,7 @@ without writing skill classes. - `description` (string, required) -- Tool description shown to the AI. - `handler_code` (string, required) -- JavaScript function body. Receives `(args, rawData, FunctionResult)` as arguments. Must return a `FunctionResult`, string, or object. - `parameters` (array, optional) -- Parameter definitions: `[{ name, type, description, required? }]`. - - `required` (string[], optional) -- Array of required parameter names. + - `required` (string[], optional) -- Required parameter names, in addition to any parameter marked `required: true` in `parameters`. - `prompt_description` (string, optional) -- Description to include in the prompt section. - `secure` (boolean, optional) -- Whether to mark the tool as secure. - `fillers` (object, optional) -- Filler phrases keyed by language. diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/skills/google-maps.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/skills/google-maps.mdx index e17f9371b9..0ad9f47e62 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/skills/google-maps.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/skills/google-maps.mdx @@ -14,13 +14,12 @@ APIs. **Tools:** `lookup_address`, `compute_route` (each tool name is configurable). -**Env vars:** `GOOGLE_MAPS_API_KEY` (required — the tool handlers read this -directly and do not fall back to an `api_key` config value). +**Env vars:** `GOOGLE_MAPS_API_KEY` (used when `api_key` is not supplied). - Schema-level declaration for the Google Maps API key. The tool handlers read - the `GOOGLE_MAPS_API_KEY` environment variable at request time; a config - value is not used as a fallback. + Google Maps API key, with the Geocoding and Routes APIs enabled. Falls back + to the `GOOGLE_MAPS_API_KEY` environment variable when the config value is + not supplied. `setup()` fails when neither is set. diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/skills/index.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/skills/index.mdx index b16508a00b..5f26bee9c4 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/skills/index.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/skills/index.mdx @@ -62,8 +62,9 @@ agent.run(); ## Configuration All skills accept configuration via a config object passed to the skill constructor. -Skills with an `env_var` fallback on a parameter read from `process.env` when the -parameter is not provided directly. +A skill that falls back to an environment variable for a parameter says so on its +page. The `env_var` entry in a parameter schema is a hint for tools that read the +schema; the SDK doesn't read it. ```typescript {4} import { WebSearchSkill } from '@signalwire/sdk'; @@ -129,7 +130,8 @@ Every skill inherits these parameters from `SkillBase`: Additional SWAIG fields to merge into each tool definition provided by this skill - (e.g., `fillers`, `secure`). + (e.g., `fillers`, `secure`). A tool's own fields win over these. Setting + `secure: false` lets the skill's tools run without a per-call token. diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/skills/mcp-gateway.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/skills/mcp-gateway.mdx index b897e02079..ba3b59822e 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/skills/mcp-gateway.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/skills/mcp-gateway.mdx @@ -11,15 +11,17 @@ The skill connects to the gateway at load time, enumerates the configured services and tools, and registers each as a SWAIG function on the agent. The gateway URL is validated by an SSRF guard — private, loopback, and -cloud-metadata endpoints are rejected. Requests use retry semantics with -`retry_attempts` and a per-request timeout of `request_timeout` seconds. +cloud-metadata endpoints are rejected. Every later request, and every redirect +it follows, is checked the same way, so a gateway that redirects to a private or +internal address, or a hostname that later resolves to one, is refused. Requests +that fail on a connection error, a timeout, or a refused address are retried up +to `retry_attempts` times, with a per-request timeout of `request_timeout` +seconds. **Class:** `McpGatewaySkill` **Tools:** Dynamically registered from the gateway (prefixed by `tool_prefix`) -**Required packages:** `undici` - **Env vars:** `MCP_GATEWAY_AUTH_TOKEN`, `MCP_GATEWAY_AUTH_USER`, `MCP_GATEWAY_AUTH_PASSWORD` @@ -76,7 +78,8 @@ cloud-metadata endpoints are rejected. Requests use retry semantics with Explicit opt-in that, together with `verify_ssl: false`, disables TLS - certificate verification. + certificate verification. Only the certificate check is skipped: the address + checks still apply. diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/skills/native-vector-search.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/skills/native-vector-search.mdx index 2f55e814d9..afb0b4507f 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/skills/native-vector-search.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/skills/native-vector-search.mdx @@ -1,22 +1,28 @@ --- title: "NativeVectorSearchSkill" slug: reference/typescript/agents/skills/native-vector-search -description: Vector + keyword document search against a local .swsearch index, an in-memory document array, or a remote search server. +description: Keyword document search over an in-memory document array or a remote search server. --- [add-skill]: /docs/server-sdks/reference/typescript/agents/agent-base/add-skill -Search documents using vector similarity and keyword matching. Supports three -modes: +Search documents by keyword scoring or through a remote search server. Supports +two modes: - **In-memory (TF-IDF)** — supply a `documents` array in config; the skill tokenizes and indexes at startup (TypeScript-specific fast path). -- **Local (SQLite)** — point `index_file` at a `.swsearch` index, or set - `build_index: true` and `source_dir` to index at startup. - **Remote (network)** — point `remote_url` at a search server and optionally name the index via `index_name`. Validated by the SSRF guard before each request; health check uses a 5-second timeout. + +The Python SDK's local-index parameters (`index_file`, `build_index`, +`backend`, and the rest) are accepted so a Python configuration still loads, +but this skill doesn't use them. Only `index_file` has an effect, as part of +the instance key. The `search_stats.backend` value the skill writes to global +data is `remote` with `remote_url` and `memory` otherwise. + + **Class:** `NativeVectorSearchSkill` **Tools:** `search_knowledge` (default — `tool_name` override supported) @@ -47,7 +53,10 @@ modes: URL of a remote search server (network mode). Validated by the SSRF guard — - private, loopback, and cloud-metadata endpoints are rejected. + private, loopback, and cloud-metadata endpoints are rejected. Every request, + and every redirect it follows, is checked the same way. Credentials in the + URL (`https://user:password@host`) are sent as basic auth and kept out of + logs. @@ -71,13 +80,14 @@ modes: - Manual keyword weight in the hybrid TF-IDF + keyword score - (range `0.0-1.0`). Overrides the automatic default of `0.3`. + In-memory mode only: how much keyword overlap counts against TF-IDF in the + score (range `0.0-1.0`, default `0.3`). Has no effect with `remote_url`, + where the server ranks results, and logs a warning there. - Embedding model for remote/SQLite modes. Shortcuts: `"mini"` (fastest, - 384 dims), `"base"` or `"large"` (768 dims). Full model names also accepted. + Accepted for compatibility with the Python SDK, and has no effect: in-memory + mode ranks with TF-IDF, and a remote server uses its index's own model. ## Content Parameters diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/skills/spider.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/skills/spider.mdx index 7d2e34e26b..5483fa3309 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/skills/spider.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/skills/spider.mdx @@ -8,7 +8,8 @@ description: Fast web scraping and crawling using cheerio-based extraction with Fast web scraping and crawling. Extracts text, markdown, or structured data from any public URL, optionally following links up to a bounded depth. Uses -`cheerio` for parsing and enforces an SSRF guard on crawl hops. +`cheerio` for parsing and enforces an SSRF guard: the URL you pass and every +redirect it follows must resolve to a public address. **Class:** `SpiderSkill` @@ -33,7 +34,8 @@ testing. - Number of concurrent requests allowed (range `1-20`). + Deprecated, and has no effect: the spider fetches one page at a time. Setting + it logs a warning (range `1-20`). @@ -49,10 +51,10 @@ testing. - Content extraction method. One of `"fast_text"`, `"clean_text"`, - `"full_text"`, `"html"`, `"markdown"`, `"structured"`, `"custom"`. Only - `fast_text`, `markdown`, and `structured` are wired through the handlers - in the TypeScript port; the others fall back to `fast_text`. + Content extraction method. One of `"fast_text"`, `"markdown"`, or + `"structured"`. The values `"clean_text"`, `"full_text"`, `"html"`, and + `"custom"` were never implemented: they still work as `fast_text` and log a + warning. Any other value makes `setup()` fail. @@ -71,9 +73,9 @@ testing. URL patterns to follow when crawling. - - User-Agent header for outbound requests. Defaults to a Chrome-compatible - UA string. + + User-Agent header for outbound requests. Also the agent name matched against + `robots.txt` rules when `follow_robots_txt` is `true`. "} default="{}" toc={true}> @@ -81,8 +83,11 @@ testing. - Whether to respect `robots.txt`. Defaults to `false` to match Python's - runtime behavior. + Skip pages that the site's `robots.txt` disallows for `user_agent`. This + covers each page the tools fetch and each redirect they follow. A + `robots.txt` answered with `401` or `403` disallows every page, and any other + `4xx` allows every page. A server error or a failed request disallows the + page and is retried on the next request. Rules are kept for 24 hours. diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/skills/swml-transfer.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/skills/swml-transfer.mdx index d5d0b13be6..1348320057 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/skills/swml-transfer.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/skills/swml-transfer.mdx @@ -75,6 +75,11 @@ registered when at least one entry is supplied in `patterns`. a transfer. + +The destination list in the AI's prompt shows each URL without the credentials +it carries, and shows each `transfers` key without its regex slashes and flag. + + Regex patterns in `transfers` are matched **case-sensitively**. Use character classes or flags in the source pattern when case-insensitive matching is diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/skills/web-search.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/skills/web-search.mdx index 33e7906827..f4e57c7693 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/skills/web-search.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/skills/web-search.mdx @@ -8,7 +8,9 @@ description: Search the web via Google Custom Search, scrape results, and qualit Search the web using the Google Custom Search JSON API. The skill fetches more results than requested, scrapes each page, scores the extracted -content, and returns only the highest-quality matches to the AI. +content, and returns only the highest-quality matches to the AI. Page fetches +follow the SSRF guard: a redirect to a private or internal address, or a +hostname that resolves to one, is refused. **Class:** `WebSearchSkill` From 063ea8d20329eb15c149d0333f11aef6205c0d29 Mon Sep 17 00:00:00 2001 From: August Date: Thu, 22 Oct 2026 19:33:47 +0000 Subject: [PATCH 07/15] docs(server-sdks): sync TypeScript CLI reference and add sw-tsdocs --- .../reference/typescript/agents/cli/index.mdx | 18 +- .../typescript/agents/cli/sw-tsdocs.mdx | 243 +++++++++++ .../typescript/agents/cli/swaig-test.mdx | 399 +++++++++++++++--- 3 files changed, 595 insertions(+), 65 deletions(-) create mode 100644 fern/products/server-sdks/pages/reference/typescript/agents/cli/sw-tsdocs.mdx diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/cli/index.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/cli/index.mdx index 54241dd158..1fbe2a9353 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/cli/index.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/cli/index.mdx @@ -6,12 +6,14 @@ max-toc-depth: 3 --- [swaig-test]: /docs/server-sdks/reference/typescript/agents/cli/swaig-test +[sw-tsdocs]: /docs/server-sdks/reference/typescript/agents/cli/sw-tsdocs -The TypeScript Server SDK ships one command-line tool, `swaig-test`, for local -development. It loads an agent file, renders its SWML, lists its SWAIG -functions, and executes a function with arguments you supply, all without -deploying or placing a call. The package's `bin` entry makes it available -through `npx` after installation. +The TypeScript Server SDK ships two command-line tools. `swaig-test` loads an +agent file, renders its SWML, lists its SWAIG functions, executes a function +with arguments you supply, and simulates serverless platforms, all without +deploying or placing a call. `sw-tsdocs` prints the SDK's documentation, API +signatures, and examples from the installed package. The package's `bin` entry +makes both available through `npx` after installation. @@ -20,6 +22,7 @@ through `npx` after installation. | Command | Purpose | |---------|---------| | [`swaig-test`][swaig-test] | Test SWAIG functions and generate SWML output locally | +| [`sw-tsdocs`][sw-tsdocs] | Look up the installed SDK's docs, API signatures, and examples | ## Local development workflow @@ -31,5 +34,8 @@ npx swaig-test src/agent.ts --dump-swml npx swaig-test src/agent.ts --list-tools # 3. Test a specific function -npx swaig-test src/agent.ts --exec get_info --arg topic=SignalWire +npx swaig-test src/agent.ts --exec get_info --topic SignalWire + +# 4. Look up an API's signature +npx sw-tsdocs api AgentBase ``` diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/cli/sw-tsdocs.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/cli/sw-tsdocs.mdx new file mode 100644 index 0000000000..1f108182ef --- /dev/null +++ b/fern/products/server-sdks/pages/reference/typescript/agents/cli/sw-tsdocs.mdx @@ -0,0 +1,243 @@ +--- +title: "sw-tsdocs" +slug: reference/typescript/agents/cli/sw-tsdocs +description: "Read the SDK's version-matched documentation, API signatures, and examples from the installed package." +max-toc-depth: 3 +--- + +[ref-swaig-test]: /docs/server-sdks/reference/typescript/agents/cli/swaig-test + +The `sw-tsdocs` command prints the documentation that ships inside the +`@signalwire/sdk` package: concept guides, API signatures read from the +package's type declarations, examples, and tutorials. Because it reads the +installed package, its answers match the SDK version you have. People and +coding agents can both use it to look up an API without leaving the terminal. + +```bash +npx sw-tsdocs [ [name] | ...] +``` + +Run it in a project that depends on `@signalwire/sdk`. With no arguments it +prints an index of what the SDK does and where to start. + +## Commands + +| Command | Purpose | +|---|---| +| `sw-tsdocs` | The index: what the SDK does and where to start | +| `sw-tsdocs ` | One area: concepts, files to read, examples, and API names | +| `sw-tsdocs topics` | Every topic, one line each | +| `sw-tsdocs api ` | Signature, JSDoc, and members of an SDK name | +| `sw-tsdocs examples [filter]` | Examples with descriptions and paths | +| `sw-tsdocs grep ` | Search the installed docs and examples | +| `sw-tsdocs show ` | Print an installed doc | +| `sw-tsdocs path [file]` | Where the docs are, or one file's full path | +| `sw-tsdocs init` | Add a note about `sw-tsdocs` to the project's `AGENTS.md` | +| `sw-tsdocs help` | Print the usage text (`-h` and `--help` work too) | +| `sw-tsdocs --version` | The installed SDK version (`-V` works too) | + +A word that isn't a topic or command is tried as an SDK name, then as a doc +name, and is treated as `api ` or `show ` when it matches. + +## Topics + +`sw-tsdocs ` prints the topic's concepts, the installed docs to read, +related examples, and the API names to look up next. The topics are: + +| Group | Topics | +|---|---| +| Build AI agents | `quickstart`, `agents`, `prompts`, `tools`, `datamap`, `contexts`, `skills`, `prefabs`, `pgi`, `tutorials` | +| Call control and APIs | `swml`, `relay`, `rest` | +| Integrations | `livewire`, `mcp`, `chat`, `bedrock` | +| Run and test | `deploy`, `security`, `config`, `testing`, `cli` | + + + One of the topic names above. + + + + Optional, and read only by the `skills` topic: a built-in skill name, such + as `weather_api`, to print that skill's description and parameters. + + +## Parameters + +### api + +```bash +npx sw-tsdocs api +``` + + + An SDK class, function, or member, such as `AgentBase` or + `FunctionResult.connect`. Prints the source location, the signature, and the + JSDoc. When several names match, the first is printed and the rest are listed + under "Other matches". Exits `1` when no name matches, and suggests similar + names. + + +### examples + +```bash +npx sw-tsdocs examples [filter] +``` + + + Optional text that narrows the list of examples. + + +### grep + +```bash +npx sw-tsdocs grep [--code] [--limit N] +``` + + + A case-insensitive regular expression, searched across the installed docs + and examples. Exits `1` when nothing matches. + + + + Search the SDK's code as well. + + + + The most matches to print. Must be `1` or more. + + +### show + +```bash +npx sw-tsdocs show [--toc | --section ] +``` + + + An installed doc. A partial name works when it matches one file; when it + matches several, the command lists them and exits `1`. + + + + Print the doc's headings with line numbers instead of its text. Cannot be + combined with `--section`. + + + + Print only the section whose heading contains this text. + + +### path + +```bash +npx sw-tsdocs path [file] +``` + + + Optional. Without it, prints the directory the documentation is installed + in. With it, prints the full path of each matching file. + + +### tutorials + +```bash +npx sw-tsdocs tutorials +``` + +The `tutorials` topic lists the three tutorials that ship with the package and +the lesson files to read: a voice agent that answers questions from Wikipedia, +a reservation line built with Programmatically Governed Inference, and a +multi-agent sales tutorial. Print a lesson with `sw-tsdocs show `. + +### init + +```bash +npx sw-tsdocs init [--dir ] [--skill] [--print] +``` + +Adds a note to the project that tells coding agents to use `sw-tsdocs` before +writing SignalWire code. The note covers running `sw-tsdocs`, checking +signatures with `sw-tsdocs api`, reading the `pgi` topic before building an +agent that takes real actions, and testing with [`swaig-test`][ref-swaig-test]. + + + The project directory. It must exist, or the command exits `2`. + + + + Also write an Agent Skills `SKILL.md`. + + + + Print the note to standard output and write nothing. + + +`init` writes these files, each reported as `created`, `updated`, or +`unchanged`: + +| File | When | +|---|---| +| `AGENTS.md` | Always. The note sits between `` and `` markers, so running `init` again updates it in place. A new file starts with a `# Notes for coding agents` heading. | +| `CLAUDE.md` | Only when the file already exists and doesn't import `@AGENTS.md`. | +| `.agents/skills/signalwire-sdk-typescript/SKILL.md` | With `--skill`. | +| `.claude/skills/signalwire-sdk-typescript/SKILL.md` | With `--skill`. | + +## Examples + +```bash +# Index of the SDK +npx sw-tsdocs + +# Installed SDK version +npx sw-tsdocs --version +``` + +```text +3.7.1 +``` + +```bash +# One area, then a built-in skill's parameters +npx sw-tsdocs contexts +npx sw-tsdocs skills weather_api + +# Look up an API +npx sw-tsdocs api FunctionResult.connect +``` + +```text +# method FunctionResult.connect + +Source: /path/to/project/node_modules/@signalwire/sdk/dist/FunctionResult.d.ts:198 + +connect(destination: string, final?: boolean, fromAddr?: string): this + +Connect the call to another destination with an inline SWML `connect` +document, emitted with `transfer: "true"` or `"false"` beside it. +... +``` + +```bash +# Find examples and search the docs +npx sw-tsdocs examples simple +npx sw-tsdocs grep 'swaig-test' --limit 5 + +# Read part of a doc +npx sw-tsdocs show cli-guide --toc +npx sw-tsdocs show cli-guide --section "Running It" + +# Add the note to AGENTS.md and write skill files +npx sw-tsdocs init --skill +``` + +```text +created: /path/to/project/AGENTS.md +created: /path/to/project/.agents/skills/signalwire-sdk-typescript/SKILL.md +created: /path/to/project/.claude/skills/signalwire-sdk-typescript/SKILL.md +``` + +## Exit codes + +| Code | Meaning | +|------|---------| +| `0` | Success | +| `1` | A name, doc, or search pattern wasn't found, or a doc name matched several files | +| `2` | Usage error: an unknown option or word, a missing argument, or a project directory that doesn't exist | diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/cli/swaig-test.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/cli/swaig-test.mdx index 11f0b71d22..0f85ada7d4 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/cli/swaig-test.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/cli/swaig-test.mdx @@ -6,49 +6,72 @@ max-toc-depth: 3 --- [ref-datamap]: /docs/server-sdks/reference/typescript/agents/data-map +[ref-sw-tsdocs]: /docs/server-sdks/reference/typescript/agents/cli/sw-tsdocs The `swaig-test` command loads an agent file and lets you render its SWML, list -its SWAIG functions, execute a function, and simulate serverless environments, -all without deploying. It supports both webhook and [DataMap][ref-datamap] -functions. +its SWAIG functions, call a function, and simulate serverless environments, all +without deploying. It supports webhook functions, [DataMap][ref-datamap] +functions, and functions that point at an external webhook URL. ```bash -npx swaig-test [options] +npx swaig-test [options] ``` +`swaig-test` sends its requests through the agent's own HTTP app, the way +SignalWire would. It fetches the agent's SWML with a simulated call, so basic +auth, signature checks, the dynamic configuration callback, and function tokens +all run as they do on a server. The callback runs once for `--dump-swml`. +`--exec` reads the function from that SWML and calls it at the `web_hook_url` +the SWML gives it. A DataMap function runs in a local DataMap simulator, and a +function whose URL points elsewhere is called at that URL. + +The agent file can be `.ts`, `.js`, `.mjs`, or `.mts`. The command picks the +agent to test in this order: + +1. The export named by `--agent-class`, either an instance or a class to + instantiate. +2. With `--route`, the service whose route matches. +3. The export named `agent`, then the default export, then any exported + instance, then any exported class. +4. An agent the file creates without exporting it, as in the quickstart. + One action flag is required. A target with no action exits with `Error: one of --dump-swml, --list-tools, or --exec is required`. The one exception is `--simulate-serverless`, which renders SWML for the platform on -its own. +its own. Running `swaig-test` with no arguments prints the help text. +Options accept both `--name value` and `--name=value`. For `-h`, `--help`, +`--help-platforms`, `--help-examples`, and `--parse-only`, see +[Help and validation](#help-and-validation). + ## Actions Choose one action per invocation: - List all SWAIG functions registered on the agent, with their parameters. + List the agent's SWAIG functions with their parameters. Each function is + labeled `LOCAL webhook` or `EXTERNAL webhook` (with its URL); a DataMap + function carries no label. With `--raw` or `--format-json`, prints a + JSON array of `name`, `description`, `parameters`, and `type`. - List all agents exported from the file. Useful for multi-agent files. + List the agents and services exported from the file, with each instance's + name and route. Useful for multi-agent files. Exits `1` when the file has + none. With `--raw` or `--format-json`, prints JSON. - Generate and output the full SWML document. Combine with `--raw` for - machine-readable JSON. + Print the SWML document the agent serves, as indented JSON. Combine with + `--raw` for compact JSON that suppresses logs. - Execute a SWAIG function by name. Pass arguments with `--arg key=value`. - - - - Validate the command-line arguments, print `parse OK`, and exit without - loading the agent file, touching the filesystem, or making network requests. - `--dry-run` is an alias. Accepted anywhere on the command line, including - after `--exec`. + Call a SWAIG function by name. Everything after the function name belongs to + the function, so put `swaig-test`'s own options before `--exec`. Pass + arguments as `--name value` (see [Function arguments](#function-arguments)). ## Common options @@ -57,34 +80,64 @@ Choose one action per invocation: Output raw JSON only, suppressing all logs. Designed for piping to `jq`. - - Enable verbose output with debug information. + + Enable verbose output with debug information: the request being sent, the + function's arguments, and, for `--list-tools`, each function's full + configuration. - Format output as indented JSON. + Print the result as indented JSON. - Name of the agent class to use when the file exports several. + Name of the exported agent class or instance to use. Cannot be combined with + `--route`. - Override the agent's route. + Use the service with this route, such as `/sales`. Cannot be combined with + `--agent-class`. +## Function arguments + +After `--exec `, pass each argument as `--name value`. `swaig-test` +types the value by the function's parameter schema and converts dashes in the +name to underscores. + +| Parameter type | How the value is read | +|---|---| +| `string` | Used as given | +| `integer` | Parsed as an integer; anything else is an error | +| `number` | Parsed as a number; anything else is an error | +| `array` | Split on commas | +| `boolean` | `true` or `false` after the name, or the bare flag for `true` | + +`--arg name=value` also works: the value is parsed as JSON, and falls back to a +string. An argument the function doesn't declare is still passed to the +function, with a warning. A `swaig-test` option placed after `--exec`, such as +`--verbose`, is read as a function argument; put it before `--exec`. + ## Function execution options - - A function argument. Repeat for each argument. + + Send the smallest function request: the call ID and the arguments. - - Override a top-level field in the mock call data (e.g., - `--override call_state=answered`). Repeatable. + + Send a request with every key SignalWire may send. This is the default. -## SWML generation options + + A JSON object merged into the function request. For a DataMap function, it + supplies the call data the simulator reads, such as `global_data`. Without a + `global_data` key, the simulator uses the `global_data` in the agent's SWML. + + +## Call and request options + +These apply to `--dump-swml`, `--list-tools`, and `--exec`. Simulated call type: `sip` or `webrtc`. @@ -94,28 +147,73 @@ Choose one action per invocation: Simulated call direction: `inbound` or `outbound`. - - Simulated call state: `active`, `ringing`, or `hold`. + + Call state, set as `call.state`. - Override the call ID in the mock data. + The `call_id` the simulated request carries. + + + + Sets `call.project_id`. + + + + Sets `call.space_id`. - Override the caller's number in the mock data. + Sets `call.from`, the caller's address. - Override the destination extension in the mock data. + Sets `call.to`, the destination. + + + + A JSON object set as `vars.userVariables`. + + + + A JSON object of query parameters for the SWML request. They are also added + to `vars.userVariables`. Use this to test a dynamic configuration callback + that reads the query string. + + + + A request header. Repeatable. + + + + Method of the SWML request: `POST` or `GET`. A `GET` request carries the call + ID and query parameters in the query string. + + + + A JSON object of extra fields merged into the SWML request body. + + + + Set a value in the simulated request by dotted path, such as + `call.state=answered`. The value is read as `null`, `true`, `false`, a + number, JSON, or a string, in that order. Repeatable. A path the simulated + request doesn't contain adds a key and prints a warning. + + + + Like `--override`, but the value must be JSON. Repeatable. -## Serverless simulation +## Environment and serverless simulation - Render SWML as the agent would on a serverless platform. Valid values: - `lambda`, `gcf`, `azure`, and `cgi`. Any other value exits with an error - naming the supported set. + Run with a serverless platform's environment, so the SWML's webhook URLs are + the platform's. Valid values are `lambda`, `cgi`, `gcf`, and `azure`; + `cloud_function` and `azure_function` are accepted as aliases. Any other + value exits with an error naming the supported set. The simulated + environment replaces other platforms' variables set in your shell, and your + shell's variables are restored afterward. @@ -123,59 +221,241 @@ Choose one action per invocation: - Load environment variables from a file (one `KEY=VALUE` per line). + Load environment variables from a file, one `KEY=VALUE` per line. Blank lines + and lines starting with `#` are skipped, and matching quotes around a value + are removed. + + +### AWS Lambda + +Use with `--simulate-serverless lambda`. + + + Function name (`AWS_LAMBDA_FUNCTION_NAME`). + + + + Function URL (`AWS_LAMBDA_FUNCTION_URL`). + + + + Region (`AWS_REGION`). + + + + Serve at an API Gateway URL, `https://ID.execute-api.REGION.amazonaws.com/STAGE`. + Ignored, with a warning, when `--aws-function-url` is set. + + + + API Gateway stage. Applies only with `--aws-api-gateway-id`. + + +### CGI + +Use with `--simulate-serverless cgi`. + + + Server host name. Required with `--simulate-serverless cgi`. + + + + Script path (`SCRIPT_NAME`). + + + + Serve over HTTPS. + + + + `PATH_INFO`. + + +### Google Cloud Functions + +Use with `--simulate-serverless gcf`. + + + Project (`GOOGLE_CLOUD_PROJECT`). + + + + Function URL (`FUNCTION_URL`). + + + + Region (`GOOGLE_CLOUD_REGION`). + + + + Service name (`K_SERVICE`). + + +### Azure Functions + +Use with `--simulate-serverless azure`. + + + Environment (`AZURE_FUNCTIONS_ENVIRONMENT`). + + + + Function URL (`AZURE_FUNCTION_URL`). + + +## Help and validation + + + Validate the command-line arguments, print `parse OK`, and exit without + loading the agent file, touching the filesystem, or making network requests. + `--dry-run` is an alias. Accepted anywhere on the command line, including + after `--exec`. + + + + Print the help text. It ends with a pointer to [`sw-tsdocs`][ref-sw-tsdocs]. + + + + Print the serverless platform options. + + + + Print usage examples. ## Examples -### List tools and generate SWML +The examples below run against this agent: + +```typescript title="agent.ts" +import { AgentBase, FunctionResult } from '@signalwire/sdk'; + +export const agent = new AgentBase({ name: 'ride-desk', route: '/' }); + +agent.setPromptText('You are a dispatcher for a taxi company.'); +agent.addLanguage({ name: 'English', code: 'en-US', voice: 'rachel' }); + +agent.defineTool({ + name: 'book_ride', + description: 'Book a taxi', + parameters: { + type: 'object', + properties: { + pickup: { type: 'string', description: 'Pickup address' }, + passengers: { type: 'integer', description: 'Number of passengers' }, + }, + required: ['pickup'], + }, + handler: (args) => + new FunctionResult(`Booked a ride from ${args.pickup} for ${args.passengers ?? 1}.`), +}); +``` + +### List tools ```bash -# List all registered SWAIG functions -npx swaig-test src/agent.ts --list-tools +npx swaig-test agent.ts --list-tools +``` -# Generate the SWML document +```text +Available SWAIG functions: + book_ride - Book a taxi (LOCAL webhook) + Parameters: + pickup (string) (required): Pickup address + passengers (integer): Number of passengers +``` + +### Generate SWML + +```bash +# Print the SWML document npx swaig-test src/agent.ts --dump-swml -# Generate raw JSON for piping -npx swaig-test src/agent.ts --dump-swml --raw | jq '.' +# Compact JSON for piping +npx swaig-test src/agent.ts --dump-swml --raw | jq '.sections.main' -# Validate the arguments without loading the agent -npx swaig-test src/agent.ts --parse-only +# Render the SWML a dynamic agent serves for a request +npx swaig-test src/agent.ts --dump-swml \ + --query-params '{"tier":"premium"}' \ + --header "X-Customer-ID=12345" ``` ### Execute functions ```bash -# Execute with arguments -npx swaig-test src/agent.ts --exec search --arg query="AI agents" --arg limit=5 +npx swaig-test agent.ts --exec book_ride --pickup "123 Gough Street" --passengers 2 +``` -# Execute with verbose debug output and an overridden caller number -npx swaig-test src/agent.ts --verbose --exec book_ride \ - --arg pickup="123 Gough Street" \ - --override from=+15551234567 +```text +RESULT: +Response: Booked a ride from 123 Gough Street for 2. +``` + +```bash +# Options for swaig-test go before --exec +npx swaig-test src/agent.ts --verbose --exec search --query test --limit 5 + +# Change the simulated call +npx swaig-test src/agent.ts --call-type sip --call-state answered \ + --exec transfer --dest sales + +# Set a value by dotted path +npx swaig-test src/agent.ts --override call.project_id=my-project --dump-swml + +# The older --arg form still works +npx swaig-test src/agent.ts --exec search --arg query="AI agents" ``` ### Multi-agent files ```bash -# Discover agents in the file -npx swaig-test src/agents.ts --list-agents +npx swaig-test multi-agent.ts --list-agents +``` + +```text +Agents found in multi-agent.ts: + sales (instance) + Name: sales + Route: /sales + support (instance) + Name: support + Route: /support +``` -# Target a specific agent by class name +```bash +# Target a service by route +npx swaig-test multi-agent.ts --route /sales --list-tools + +# Or target an export by name npx swaig-test src/agents.ts --agent-class SalesAgent --list-tools ``` ### Serverless simulation ```bash -# AWS Lambda -npx swaig-test src/agent.ts --simulate-serverless lambda +# AWS Lambda in a chosen region +npx swaig-test src/agent.ts --simulate-serverless lambda \ + --aws-region us-west-2 --dump-swml + +# CGI over HTTPS +npx swaig-test src/agent.ts --simulate-serverless cgi \ + --cgi-host example.com --cgi-https --exec my_function # Google Cloud Functions, with environment variables npx swaig-test src/agent.ts --simulate-serverless gcf \ --env SWML_BASIC_AUTH_USER=user \ - --env-file .env + --env-file .env --dump-swml +``` + +### Validate arguments only + +```bash +npx swaig-test src/agent.ts --parse-only +``` + +```text +parse OK ``` ## Exit codes @@ -183,4 +463,5 @@ npx swaig-test src/agent.ts --simulate-serverless gcf \ | Code | Meaning | |------|---------| | `0` | Success | -| `1` | Error (file not found, invalid arguments, execution failure) | +| `1` | Runtime failure: the agent file didn't load, the function wasn't found, an argument didn't fit its type, or the function call returned a non-2xx status | +| `2` | Usage error: an unknown option, a missing value or agent file, conflicting options, or no action given | From 244026af2cfef2cb33aead21c31cd1f109af0909 Mon Sep 17 00:00:00 2001 From: August Date: Thu, 22 Oct 2026 19:33:47 +0000 Subject: [PATCH 08/15] docs(server-sdks): sync TypeScript configuration, Bedrock, LiveWire, prefabs, and Relay reference to 3.7.1 --- .../typescript/agents/bedrock-agent/index.mdx | 32 ++++---- .../bedrock-agent/set-inference-params.mdx | 17 ++-- .../agents/bedrock-agent/set-voice.mdx | 4 +- .../configuration/environment-variables.mdx | 79 ++++++++++++++++--- .../create-azure-handler.mdx | 2 +- .../serverless-adapter/create-gcf-handler.mdx | 2 +- .../create-lambda-handler.mdx | 2 +- .../serverless-adapter/handle-request.mdx | 9 ++- .../agents/livewire/agent-session/index.mdx | 15 ++-- .../agents/livewire/agent/index.mdx | 10 +-- .../agents/prefabs/receptionist-agent.mdx | 3 +- .../agents/prefabs/survey-agent.mdx | 3 +- .../relay/actions/stream-action/index.mdx | 2 +- .../relay/actions/stream-action/stop.mdx | 2 +- .../typescript/relay/call/stream.mdx | 12 +-- .../typescript/relay/call/user-event.mdx | 4 +- 16 files changed, 136 insertions(+), 62 deletions(-) diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/bedrock-agent/index.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/bedrock-agent/index.mdx index d84abd9970..aeeef10d6a 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/bedrock-agent/index.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/bedrock-agent/index.mdx @@ -32,7 +32,7 @@ import { BedrockAgent } from '@signalwire/sdk'; const agent = new BedrockAgent({ systemPrompt: 'You are a helpful voice assistant.', - voiceId: 'joanna', + voiceId: 'tiffany', }); ``` @@ -56,20 +56,22 @@ The `createBedrockAgent(config)` factory function returns the same instance as - Bedrock voice identifier (e.g., `"matthew"`, `"joanna"`). + Bedrock voice identifier (`"tiffany"`, `"matthew"`, `"amy"`, `"lupe"`, or `"carlos"`). - - Generation temperature. Range: 0 to 1. + + Generation temperature. Range: 0 to 1. A numeric string is converted, and a SWML + variable reference such as `'${temperature}'` is kept; anything else throws. - - Nucleus sampling parameter. Range: 0 to 1. + + Nucleus sampling parameter. Range: 0 to 1. A numeric string is converted, and a + SWML variable reference is kept; anything else throws. - - Maximum tokens to generate per response. Accepted for compatibility with the - Python SDK. Not currently applied to the call. + + Maximum tokens to generate per response, an integer or a numeric string. Sent in + the prompt as `max_tokens`, but the platform's Bedrock session doesn't apply it. "} toc={true}> @@ -97,13 +99,15 @@ voice-to-voice model: |--------|----------| | `setLlmModel()` | Logs a warning and does nothing. Bedrock uses a fixed voice-to-voice model. | | `setLlmTemperature()` | Redirects to `setInferenceParams(temperature)`. | -| `setPromptLlmParams()` | Logs a warning. Use `setInferenceParams()` instead. | +| `setPromptLlmParams()` | `temperature`, `top_p`, and `max_tokens` update the inference settings, as `setInferenceParams()` does. Any other setting, such as `confidence`, `presence_penalty`, or `barge_confidence`, is ignored with a warning. | | `setPostPromptLlmParams()` | Logs a warning. The Bedrock post-prompt uses the LLM configured in the platform. | -Parameters specific to text-based LLMs (`barge_confidence`, `presence_penalty`, -`frequency_penalty`) are filtered out during SWML rendering and have no effect -on Bedrock agents. +The `amazon_bedrock` verb carries the prompt, SWAIG functions, params, global data, +and post-prompt settings. Speech hints, languages, pronunciation rules, multilingual +settings, and contexts aren't part of it, so they're left out of the SWML, with one +warning per agent for each. The platform's Bedrock session reads only the prompt's +text, `voice_id`, `temperature`, and `top_p`. Prompt methods (`setPromptText()`, `setPromptPom()`, `promptAddSection()`, and @@ -124,7 +128,7 @@ import { BedrockAgent, FunctionResult } from '@signalwire/sdk'; const agent = new BedrockAgent({ name: 'bedrock-assistant', route: '/assistant', - voiceId: 'joanna', + voiceId: 'tiffany', temperature: 0.5, }); diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/bedrock-agent/set-inference-params.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/bedrock-agent/set-inference-params.mdx index 2c832ce381..cda78ec50e 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/bedrock-agent/set-inference-params.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/bedrock-agent/set-inference-params.mdx @@ -12,17 +12,20 @@ an argument left `undefined` keeps its current value. ## **Parameters** - - Generation temperature. Range: 0 to 1. + + Generation temperature. Range: 0 to 1. A numeric string is converted, and a SWML + variable reference such as `'${temperature}'` is kept; anything else throws and + changes nothing. - - Nucleus sampling parameter. Range: 0 to 1. + + Nucleus sampling parameter. Range: 0 to 1. A numeric string is converted, and a + SWML variable reference is kept; anything else throws and changes nothing. - - Maximum tokens to generate. Accepted for compatibility with the Python SDK. - Not currently applied to the call. + + Maximum tokens to generate, an integer or a numeric string. Sent in the prompt as + `max_tokens`, but the platform's Bedrock session doesn't apply it. ## **Returns** diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/bedrock-agent/set-voice.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/bedrock-agent/set-voice.mdx index 7e80c0b9d6..8dfcb2c70a 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/bedrock-agent/set-voice.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/bedrock-agent/set-voice.mdx @@ -12,7 +12,7 @@ Set the Bedrock voice ID after construction. ## **Parameters** - Bedrock voice identifier (e.g., `"matthew"`, `"joanna"`). + Bedrock voice identifier (`"tiffany"`, `"matthew"`, `"amy"`, `"lupe"`, or `"carlos"`). ## **Returns** @@ -30,5 +30,5 @@ const agent = new BedrockAgent({ systemPrompt: 'You are a helpful assistant.', }); -agent.setVoice('joanna'); +agent.setVoice('tiffany'); ``` diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/configuration/environment-variables.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/configuration/environment-variables.mdx index 9cfbc901f2..690d7390a7 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/configuration/environment-variables.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/configuration/environment-variables.mdx @@ -35,12 +35,22 @@ const agent = new AgentBase({ name: 'my-agent' }); Username for HTTP Basic Authentication on all agent webhook endpoints. - When unset, the username falls back to the agent's `name`. + When a password is set without a user, the user is `signalwire`. When the + password is generated, the user falls back to the agent's `name`. - Password for HTTP Basic Authentication. If not set, a random password is - auto-generated on each startup and printed to the console. + Password for HTTP Basic Authentication. Credentials resolve from the + constructor's `basicAuth`, then the config file, then these variables. If none + is set, a random password is generated on each startup; it exists only in that + process, and the agent logs a warning. + + + + Secret that signs per-call SWAIG function tokens, used when the agent's + `swaigSecret` option isn't set. Set the same value on every replica, and keep + it across restarts, so a token issued by one process validates on another. + Treat it as a secret. @@ -54,7 +64,9 @@ until you update the credentials in your SignalWire dashboard. ## SSL / TLS - Enable HTTPS. Only the string `"true"` is accepted (strict equality check). + Enable HTTPS for `serve()` and `AgentServer.run()`. Accepts `true`, `1`, or + `yes`. Webhook URLs then use `https`, on `SWML_SSL_DOMAIN` when it's set. When + the certificate or key file is missing, the agent warns and serves HTTP. @@ -82,6 +94,14 @@ until you update the credentials in your SignalWire dashboard. Enable proxy request debug logging. Only the string `"true"` is accepted. + + The SDK's outbound URL fetches (the spider and web_search skills, for example) + connect directly, ignoring any environment proxy, so each connection can be + checked for private and internal addresses. When set, a request that Node's + global `fetch` would send through a proxy goes through it instead, where that + check can't apply. Use a proxy that restricts destinations itself. + + Trust `X-Forwarded-For`, `X-Forwarded-Proto`, and `X-Forwarded-Host` headers for client IP detection (used by rate limiting) and auto-detection of the @@ -115,15 +135,15 @@ for webhook URL generation. ## Security - Comma-separated list of allowed `Host` header values. When unset, no host - check is applied. When set, requests with a `Host` header not in the list + Comma-separated list of allowed `Host` header values. When unset, or when the + list includes `*`, no host check is applied. When set, requests with a `Host` header not in the list are rejected with HTTP 403. Set to specific domain(s) in production (e.g., `"agent.example.com,api.example.com"`). - Comma-separated list of allowed CORS origins. Restrict to trusted origins in - production. + Comma-separated list of allowed CORS origins. A list that includes `*` allows + every origin. Restrict to trusted origins in production. @@ -138,9 +158,11 @@ for webhook URL generation. - Rate limit in requests per minute per client IP. When unset, no rate limit + Rate limit in requests per minute per client. When unset, no rate limit is applied. When set to a positive integer, clients exceeding the limit - receive HTTP 429. + receive HTTP 429. Each client is keyed by its connection address, by the + forwarded address when `SWML_TRUST_PROXY_HEADERS` is set, or on a serverless + platform by the address the platform reports. ## Logging @@ -174,6 +196,21 @@ for webhook URL generation. is enabled only when writing to a terminal. +## AI Chat gateway + + + Publishable key a `ChatGateway` accepts from the browser widget, used when the + gateway's `key` option isn't set. Without either, the gateway generates a key, + which is only useful to a process that also serves the page. + + + + Key that signs a `ChatGateway`'s conversation handles, used when the gateway's + `secret` option isn't set. Without either, each process generates its own, and + outstanding handles stop working on restart and across replicas. Treat it as a + secret. + + ## Skills @@ -285,7 +322,13 @@ You typically do not set these manually -- they are provided by the platform run - Knative/Cloud Run service name. + Knative/Cloud Run service name. Cloud Run sets it without `FUNCTION_TARGET`; a + server there isn't taken for a Cloud Function. + + + + The function's URL. When set, webhook URLs use it. Otherwise they are built from + the project, region, and service. ### Azure Functions @@ -298,6 +341,20 @@ You typically do not set these manually -- they are provided by the platform run Azure Functions environment name. + + The function's URL. When set, webhook URLs use it. + + + + Function app name. With `AZURE_FUNCTION_NAME`, builds the webhook URL + `https://.azurewebsites.net/api/` when `AZURE_FUNCTION_URL` + isn't set. + + + + Function name used in the built webhook URL. + + ### CGI diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/configuration/serverless-adapter/create-azure-handler.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/configuration/serverless-adapter/create-azure-handler.mdx index 46e4d747dc..b19cd4ae89 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/configuration/serverless-adapter/create-azure-handler.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/configuration/serverless-adapter/create-azure-handler.mdx @@ -10,7 +10,7 @@ app. The returned function accepts an Azure context and request object. ## **Parameters** - + A Hono-compatible application with a `fetch` method. diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/configuration/serverless-adapter/create-gcf-handler.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/configuration/serverless-adapter/create-gcf-handler.mdx index 1a7e89f8dd..8102a6a9a3 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/configuration/serverless-adapter/create-gcf-handler.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/configuration/serverless-adapter/create-gcf-handler.mdx @@ -10,7 +10,7 @@ Hono app. The returned function accepts GCF request/response objects. ## **Parameters** - + A Hono-compatible application with a `fetch` method. diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/configuration/serverless-adapter/create-lambda-handler.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/configuration/serverless-adapter/create-lambda-handler.mdx index 7a45632372..03c4658c74 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/configuration/serverless-adapter/create-lambda-handler.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/configuration/serverless-adapter/create-lambda-handler.mdx @@ -11,7 +11,7 @@ of a `ServerlessResponse`. ## **Parameters** - + A Hono-compatible application with a `fetch` method. diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/configuration/serverless-adapter/handle-request.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/configuration/serverless-adapter/handle-request.mdx index 360d1c044a..c89c5fa754 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/configuration/serverless-adapter/handle-request.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/configuration/serverless-adapter/handle-request.mdx @@ -10,13 +10,18 @@ app, and return a normalized `ServerlessResponse`. ## **Parameters** - + A Hono-compatible application with a `fetch` method. The incoming serverless event. Properties include `httpMethod`, `method`, - `headers`, `body`, `path`, `rawPath`, and `queryStringParameters`. + `headers`, `body`, `isBase64Encoded`, `path`, `rawPath`, `queryStringParameters`, + `multiValueQueryStringParameters` and `pathParameters` (API Gateway REST), + `rawQueryString` (API Gateway v2 and function URLs), and `requestContext`. + + With a signing key set, the signature is checked over the URL SignalWire signed: + the URL the platform was called on, with its query, and the raw body. ## **Returns** diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/livewire/agent-session/index.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/livewire/agent-session/index.mdx index 485915b2a1..3fb7916ff2 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/livewire/agent-session/index.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/livewire/agent-session/index.mdx @@ -39,7 +39,7 @@ new AgentSession(options?: { llm?: any; vad?: any; turnDetection?: any; - tools?: FunctionTool[]; + tools?: FunctionTool[] | Record; mcpServers?: unknown; userData?: UserData; allowInterruptions?: boolean; @@ -62,8 +62,9 @@ new AgentSession(options?: { - LLM provider or model string. If provided, the model name is extracted (stripping - any provider prefix like `"openai/"`) and set on the underlying SignalWire agent. + A model name, or an LLM plugin object such as `plugins.OpenAILLM` whose `model` + is used. Sets the `model` param on the underlying SignalWire agent, with any + `provider/` prefix (like `"openai/"`) removed. @@ -76,8 +77,9 @@ new AgentSession(options?: { turn detection automatically. - - Optional additional tools merged with the agent's tools when `start()` is called. + + Optional additional tools, as an array or an object keyed by name, merged with the + agent's tools when `start()` is called. @@ -91,7 +93,8 @@ new AgentSession(options?: { - Forwarded to the SignalWire AI params when the session starts. + `false` sets the `enable_barge` AI param to `false` when the session starts, which + turns barge-in off. diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/livewire/agent/index.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/livewire/agent/index.mdx index 4ed40380cf..2b45f8b870 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/livewire/agent/index.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/livewire/agent/index.mdx @@ -36,7 +36,7 @@ const agent = new Agent({ ```typescript {1-13} new Agent(options?: { instructions?: string; - tools?: FunctionTool[]; + tools?: FunctionTool[] | Record; userData?: UserData; chatCtx?: unknown; stt?: unknown; @@ -63,10 +63,10 @@ first time they are supplied. System prompt text. Mapped to the SignalWire prompt when the session starts. - - Array of [`FunctionTool`][functiontool] objects. Stored internally as a - name-keyed record; each tool is registered as a SWAIG function on the - underlying agent. + + [`FunctionTool`][functiontool] objects, as an array or as an object keyed by + name, as LiveKit agents-js takes them. Stored internally as a name-keyed record; + each tool is registered as a SWAIG function on the underlying agent. diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/prefabs/receptionist-agent.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/prefabs/receptionist-agent.mdx index a3b9ea046a..0e123b7c6e 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/prefabs/receptionist-agent.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/prefabs/receptionist-agent.mdx @@ -8,7 +8,8 @@ description: A front-desk agent that greets callers, looks up departments, and t [functionresult]: /docs/server-sdks/reference/typescript/agents/function-result A front-desk agent that greets callers and transfers them to the correct -department by phone number or SIP address. Visitor check-in is opt-in. +department by phone number or SIP address. Visitor check-in is opt-in. The per-call state is kept in memory and dropped when the call's summary arrives, +after an hour idle, or when more than 10,000 calls are held. ```typescript {3} import { ReceptionistAgent } from '@signalwire/sdk'; diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/prefabs/survey-agent.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/prefabs/survey-agent.mdx index 3341db9cd4..731590573c 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/prefabs/survey-agent.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/prefabs/survey-agent.mdx @@ -9,7 +9,8 @@ description: A survey agent that conducts surveys with support for multiple ques Conducts surveys with support for multiple question types, conditional branching based on answers, per-answer scoring, and a completion callback. The agent guides the caller -through each question, validates answers, and tracks progress per call. +through each question, validates answers, and tracks progress per call. The per-call state is kept in memory and dropped when the call's summary arrives, +after an hour idle, or when more than 10,000 calls are held. ```typescript {3} import { SurveyAgent } from '@signalwire/sdk'; diff --git a/fern/products/server-sdks/pages/reference/typescript/relay/actions/stream-action/index.mdx b/fern/products/server-sdks/pages/reference/typescript/relay/actions/stream-action/index.mdx index bbcc158b38..3a262facf6 100644 --- a/fern/products/server-sdks/pages/reference/typescript/relay/actions/stream-action/index.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/relay/actions/stream-action/index.mdx @@ -42,7 +42,7 @@ const client = new RelayClient({ client.onCall(async (call) => { await call.answer(); const action = await call.stream('wss://example.com/audio-stream', { - track: 'inbound', + track: 'inbound_track', }); const event = await action.wait(); diff --git a/fern/products/server-sdks/pages/reference/typescript/relay/actions/stream-action/stop.mdx b/fern/products/server-sdks/pages/reference/typescript/relay/actions/stream-action/stop.mdx index 0eabed3196..7ad038f4f0 100644 --- a/fern/products/server-sdks/pages/reference/typescript/relay/actions/stream-action/stop.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/relay/actions/stream-action/stop.mdx @@ -25,7 +25,7 @@ const client = new RelayClient({ client.onCall(async (call) => { await call.answer(); const action = await call.stream('wss://example.com/audio-stream', { - track: 'inbound', + track: 'inbound_track', }); // Stop the stream when done diff --git a/fern/products/server-sdks/pages/reference/typescript/relay/call/stream.mdx b/fern/products/server-sdks/pages/reference/typescript/relay/call/stream.mdx index bd0980ac78..2a19b0abce 100644 --- a/fern/products/server-sdks/pages/reference/typescript/relay/call/stream.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/relay/call/stream.mdx @@ -35,12 +35,12 @@ This method emits [`calling.call.stream`][calling-call-stream] events. See [Call - `"OPUS"` -- Opus codec (higher quality, variable bitrate) - - Which audio track to stream. + + Which audio to stream. Sent as given. - - `"inbound"` -- audio received from the caller - - `"outbound"` -- audio sent to the caller - - `"both"` -- audio in both directions + - `"inbound_track"` -- audio received from the caller + - `"outbound_track"` -- audio sent to the caller + - `"both_tracks"` -- audio in both directions @@ -92,7 +92,7 @@ client.onCall(async (call) => { // Stream audio to an external service for real-time processing const action = await call.stream( 'wss://example.com/audio-stream', - { track: 'inbound', codec: 'PCMU', customParameters: { session_id: 'abc123' } } + { track: 'inbound_track', codec: 'PCMU', customParameters: { session_id: 'abc123' } } ); // The stream runs until stopped or the call ends diff --git a/fern/products/server-sdks/pages/reference/typescript/relay/call/user-event.mdx b/fern/products/server-sdks/pages/reference/typescript/relay/call/user-event.mdx index 69cb093757..2839fce751 100644 --- a/fern/products/server-sdks/pages/reference/typescript/relay/call/user-event.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/relay/call/user-event.mdx @@ -15,8 +15,8 @@ the same call can receive and react to these events. The event name or identifier. -The `options` type also accepts additional keys (`& Record`), but -only the `event` field is extracted and sent in the RPC call. +The `options` type also accepts additional keys (`& Record`), and +every field is sent in the RPC call. `event` is left out when it isn't set. ## **Returns** From d61c9e42999bb08f32b1639b617533ddc98bce9e Mon Sep 17 00:00:00 2001 From: August Date: Thu, 22 Oct 2026 19:33:47 +0000 Subject: [PATCH 09/15] chore(server-sdks): record TypeScript reference sync to v3.7.1 --- fern/products/server-sdks/sdk-source-sync.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fern/products/server-sdks/sdk-source-sync.json b/fern/products/server-sdks/sdk-source-sync.json index 0086273f16..6cefb132fe 100644 --- a/fern/products/server-sdks/sdk-source-sync.json +++ b/fern/products/server-sdks/sdk-source-sync.json @@ -12,8 +12,8 @@ "repository": "https://github.com/signalwire/signalwire-typescript.git", "local_path": "temp/signalwire-typescript", "status": "active", - "synced_commit": "6a2aa09a3dce2221ba2e0dfc21ee6262dac8e6c2", - "synced_at": "2026-09-04" + "synced_commit": "875e6e225a3a8c00436510465d5375c3ae00976e", + "synced_at": "2026-10-22" }, "go": { "repository": "https://github.com/signalwire/signalwire-go.git", From 6de06a60e358a7496ece056b05a117d2d1c3b9e4 Mon Sep 17 00:00:00 2001 From: August Date: Thu, 22 Oct 2026 19:33:47 +0000 Subject: [PATCH 10/15] docs(calling): bump TypeScript SDK pin to 3.7.1 and fix guide samples --- .../calling/fax/send-and-receive-faxes.mdx | 18 ++++---- .../pages/calling/voice/call-queues.mdx | 33 +++++++------- .../pages/calling/voice/call-streaming.mdx | 32 +++++++------ .../pages/calling/voice/forward-calls.mdx | 19 ++++---- .../pages/calling/voice/machine-detection.mdx | 24 +++++----- .../calling/voice/make-and-receive-calls.mdx | 45 +++++++------------ .../pages/calling/voice/record-calls.mdx | 22 ++++----- .../pages/calling/voice/sip-refer.mdx | 14 +++--- .../pages/calling/whatsapp/receive-calls.mdx | 2 +- 9 files changed, 99 insertions(+), 110 deletions(-) diff --git a/fern/products/platform/pages/calling/fax/send-and-receive-faxes.mdx b/fern/products/platform/pages/calling/fax/send-and-receive-faxes.mdx index 8d45c10897..755197a563 100644 --- a/fern/products/platform/pages/calling/fax/send-and-receive-faxes.mdx +++ b/fern/products/platform/pages/calling/fax/send-and-receive-faxes.mdx @@ -122,7 +122,7 @@ print(call["id"]) ```typescript -// Install: npm install @signalwire/sdk@2.0.5 +// Install: npm install @signalwire/sdk@3.7.1 // This sample also runs as JavaScript: save as send-fax.mjs, // then run: node send-fax.mjs import { RestClient, SwmlBuilder } from "@signalwire/sdk"; @@ -135,12 +135,10 @@ const client = new RestClient({ const swml = new SwmlBuilder() .send_fax({ document: "", header_info: "Bayview Taxi receipt" }) - .build(); + .render(); // JSON text; parse it to pass the document object to dial() -const call = await client.calling.dial({ - from: "", - to: "", - swml, +const call = await client.calling.dial("", "", { + swml: JSON.parse(swml), }); console.log(call.id); ``` @@ -215,7 +213,7 @@ print(script["id"]) ```typescript -// Install: npm install @signalwire/sdk@2.0.5 +// Install: npm install @signalwire/sdk@3.7.1 // This sample also runs as JavaScript: save as receive-fax-script.mjs, // then run: node receive-fax-script.mjs import { RestClient, SwmlBuilder } from "@signalwire/sdk"; @@ -228,7 +226,7 @@ const client = new RestClient({ const swml = new SwmlBuilder() .receive_fax() - .build(); + .render(); // The script's contents are SWML as JSON text const script = await client.fabric.swmlScripts.create({ name: "Receive faxes", @@ -377,7 +375,7 @@ asyncio.run(main()) ```typescript -// Install: npm install @signalwire/sdk@2.0.5 +// Install: npm install @signalwire/sdk@3.7.1 // Save as send-fax-relay.mts and run: npx tsx send-fax-relay.mts import { RelayClient, FaxEvent } from "@signalwire/sdk"; @@ -468,7 +466,7 @@ client.run() ```typescript -// Install: npm install @signalwire/sdk@2.0.5 +// Install: npm install @signalwire/sdk@3.7.1 // Save as receive-fax-relay.mts and run: npx tsx receive-fax-relay.mts import { RelayClient, FaxEvent } from "@signalwire/sdk"; diff --git a/fern/products/platform/pages/calling/voice/call-queues.mdx b/fern/products/platform/pages/calling/voice/call-queues.mdx index 083774dd67..fe67fde5e4 100644 --- a/fern/products/platform/pages/calling/voice/call-queues.mdx +++ b/fern/products/platform/pages/calling/voice/call-queues.mdx @@ -124,7 +124,7 @@ print(queue["id"]) ```typescript -// Install: npm install @signalwire/sdk@2.0.5 +// Install: npm install @signalwire/sdk@3.7.1 // This sample also runs as JavaScript: save as create-queue.mjs, // then run: node create-queue.mjs import { RestClient } from "@signalwire/sdk"; @@ -193,7 +193,7 @@ service.serve() ```typescript -// Install: npm install @signalwire/sdk@2.0.5 +// Install: npm install @signalwire/sdk@3.7.1 // This sample also runs as JavaScript: save as dispatch-queue.mjs, // then run: node dispatch-queue.mjs import { SWMLService } from "@signalwire/sdk"; @@ -203,6 +203,9 @@ const service = new SWMLService({ route: "/swml", port: 3000, basicAuth: ["signalwire", ""], + // @signalwire/sdk 3.7.1 validates enter_queue against an outdated schema + // that rejects every valid document, so validation is off for this service. + schemaValidation: false, }); service .getBuilder() @@ -295,7 +298,7 @@ print(call["id"]) ```typescript -// Install: npm install @signalwire/sdk@2.0.5 +// Install: npm install @signalwire/sdk@3.7.1 // This sample also runs as JavaScript: save as call-agent.mjs, // then run: node call-agent.mjs import { RestClient, SwmlBuilder } from "@signalwire/sdk"; @@ -312,12 +315,10 @@ const swml = new SwmlBuilder() // Runs only if no caller is waiting. .say("No callers are waiting.") .hangup() - .build(); + .render(); // JSON text; parse it to pass the document object to dial() -const call = await client.calling.dial({ - from: "", - to: "", - swml, +const call = await client.calling.dial("", "", { + swml: JSON.parse(swml), }); console.log(call.id); ``` @@ -405,7 +406,7 @@ client.calling.end("") ```typescript -// Install: npm install @signalwire/sdk@2.0.5 +// Install: npm install @signalwire/sdk@3.7.1 // This sample also runs as JavaScript: save as remove-caller.mjs, // then run: node remove-caller.mjs import { RestClient } from "@signalwire/sdk"; @@ -564,7 +565,7 @@ client.run() ```typescript -// Install: npm install @signalwire/sdk@2.0.5 +// Install: npm install @signalwire/sdk@3.7.1 // Save as dispatch-queue-relay.mts and run: npx tsx dispatch-queue-relay.mts import { RelayClient, QueueEvent } from "@signalwire/sdk"; @@ -683,7 +684,7 @@ asyncio.run(main()) ```typescript -// Install: npm install @signalwire/sdk@2.0.5 +// Install: npm install @signalwire/sdk@3.7.1 // Save as call-agent-relay.mts and run: npx tsx call-agent-relay.mts import { RelayClient, ConnectEvent } from "@signalwire/sdk"; @@ -805,7 +806,7 @@ for queue in client.queues.list()["data"]: ```typescript -// Install: npm install @signalwire/sdk@2.0.5 +// Install: npm install @signalwire/sdk@3.7.1 // This sample also runs as JavaScript: save as list-queues.mjs, // then run: node list-queues.mjs import { RestClient } from "@signalwire/sdk"; @@ -817,7 +818,7 @@ const client = new RestClient({ }); const queues = await client.queues.list(); -for (const queue of queues.data) { +for (const queue of queues.data ?? []) { console.log( `${queue.friendly_name} (${queue.id}): ${queue.current_size} waiting, ` + `average wait ${queue.average_wait_time} s`, @@ -878,7 +879,7 @@ for member in sorted(members, key=lambda m: m["position"]): ```typescript -// Install: npm install @signalwire/sdk@2.0.5 +// Install: npm install @signalwire/sdk@3.7.1 // This sample also runs as JavaScript: save as list-waiting.mjs, // then run: node list-waiting.mjs import { RestClient } from "@signalwire/sdk"; @@ -893,7 +894,7 @@ const members = []; let params = {}; while (true) { const page = await client.queues.listMembers("", params); - members.push(...page.data); + members.push(...(page.data ?? [])); const nextUrl = page.links?.next; if (!nextUrl) break; params = Object.fromEntries(new URLSearchParams(nextUrl.split("?")[1])); @@ -943,7 +944,7 @@ print(queue["friendly_name"]) ```typescript -// Install: npm install @signalwire/sdk@2.0.5 +// Install: npm install @signalwire/sdk@3.7.1 // This sample also runs as JavaScript: save as update-queue.mjs, // then run: node update-queue.mjs import { RestClient } from "@signalwire/sdk"; diff --git a/fern/products/platform/pages/calling/voice/call-streaming.mdx b/fern/products/platform/pages/calling/voice/call-streaming.mdx index 8053cc9eee..14db580729 100644 --- a/fern/products/platform/pages/calling/voice/call-streaming.mdx +++ b/fern/products/platform/pages/calling/voice/call-streaming.mdx @@ -358,7 +358,7 @@ uvicorn.run(app, host="0.0.0.0", port=8080) ```javascript -// Install: npm install @signalwire/sdk@2.0.5 @hono/node-server ws +// Install: npm install @signalwire/sdk@3.7.1 @hono/node-server ws // Save as stream-app.mjs and run: node stream-app.mjs import { serve } from "@hono/node-server"; import { SWMLService } from "@signalwire/sdk"; @@ -369,7 +369,8 @@ const service = new SWMLService({ basicAuth: ["signalwire", ""], }); const swml = service.getBuilder(); -swml.addVerb("stream", { url: "" }); +// The SDK's SWML schema has no stream verb, so skip validation for that verb only. +swml.addVerb("stream", { url: "" }, { skipValidation: true }); swml.say("Your call is being streamed. Say a few words.").sleep(20000).say("Goodbye."); // SignalWire fetches the call's SWML from the server's root URL. @@ -493,10 +494,14 @@ swml = SWMLBuilder(service) ```javascript title="JavaScript — SWML builder" const swml = service.getBuilder(); -swml - .say("You are connected to the echo server. Speak, and you will hear yourself.") - .connect({ to: "stream:", codec: "PCMU", realtime: true, name: "echo" }) - .hangup(); +swml.say("You are connected to the echo server. Speak, and you will hear yourself."); +// The SDK's SWML schema lacks connect's stream settings, so skip validation for this verb only. +swml.addVerb( + "connect", + { to: "stream:", codec: "PCMU", realtime: true, name: "echo" }, + { skipValidation: true }, +); +swml.hangup(); ``` ```yaml title="YAML" @@ -560,9 +565,9 @@ swml.say("The stream has stopped. Goodbye.") ``` ```javascript title="JavaScript — SWML builder" {1,3} -swml.addVerb("stream", { url: "", control_id: "my-stream" }); +swml.addVerb("stream", { url: "", control_id: "my-stream" }, { skipValidation: true }); swml.say("Your call is being streamed. Say a few words.").sleep(20000); -swml.addVerb("stop_stream", { control_id: "my-stream" }); +swml.addVerb("stop_stream", { control_id: "my-stream" }, { skipValidation: true }); swml.say("The stream has stopped. Goodbye."); ``` @@ -703,7 +708,7 @@ asyncio.run(main()) ```typescript -// Install: npm install @signalwire/sdk@2.0.5 +// Install: npm install @signalwire/sdk@3.7.1 // This sample also runs as JavaScript: save as stream-call-relay.mjs, // then run: node stream-call-relay.mjs import { RelayClient } from "@signalwire/sdk"; @@ -822,7 +827,7 @@ asyncio.run(main()) ```typescript -// Install: npm install @signalwire/sdk@2.0.5 +// Install: npm install @signalwire/sdk@3.7.1 // This sample also runs as JavaScript: save as echo-call-relay.mjs, // then run: node echo-call-relay.mjs import { RelayClient } from "@signalwire/sdk"; @@ -983,7 +988,7 @@ print("Stream stopped") ```typescript -// Install: npm install @signalwire/sdk@2.0.5 +// Install: npm install @signalwire/sdk@3.7.1 // This sample also runs as JavaScript: save as stream-rest.mjs, // then run: node stream-rest.mjs import { RestClient } from "@signalwire/sdk"; @@ -994,12 +999,11 @@ const client = new RestClient({ host: ".signalwire.com", }); -await client.calling.stream("", { - url: "", +await client.calling.stream("", "", { control_id: "rest-stream", }); await new Promise((resolve) => setTimeout(resolve, 10_000)); -await client.calling.streamStop("", { control_id: "rest-stream" }); +await client.calling.streamStop("", "rest-stream"); console.log("Stream stopped"); ``` diff --git a/fern/products/platform/pages/calling/voice/forward-calls.mdx b/fern/products/platform/pages/calling/voice/forward-calls.mdx index 6b2eb758da..5c2f324d1a 100644 --- a/fern/products/platform/pages/calling/voice/forward-calls.mdx +++ b/fern/products/platform/pages/calling/voice/forward-calls.mdx @@ -140,7 +140,7 @@ service.serve() ```typescript -// Install: npm install @signalwire/sdk@2.0.5 +// Install: npm install @signalwire/sdk@3.7.1 // This sample also runs as JavaScript: save as forward-calls.mjs, // then run: node forward-calls.mjs import { SWMLService } from "@signalwire/sdk"; @@ -311,9 +311,8 @@ client.run() ```typescript -// Install: npm install @signalwire/sdk@2.0.5 -// This sample also runs as JavaScript: save as forward-relay.mjs, -// then run: node forward-relay.mjs +// Install: npm install @signalwire/sdk@3.7.1 +// Save as forward-relay.mts and run: npx tsx forward-relay.mts import { RelayClient } from "@signalwire/sdk"; const client = new RelayClient({ @@ -323,7 +322,9 @@ const client = new RelayClient({ }); client.onCall(async (call) => { - const caller = call.device.params?.from_number; + // An inbound phone call's device carries the caller's number in params.from_number. + const { params } = call.device as { params?: { from_number?: string } }; + const caller = params?.from_number; call.on("calling.call.connect", async (event) => { const state = event.params.connect_state; @@ -350,7 +351,7 @@ client.onCall(async (call) => { timeout: 20, }, }]], - { ringback: [{ type: "ringtone", name: "us" }] }, + { ringback: [{ type: "ringtone", params: { name: "us" } }] }, ); await call.waitForEnded(); }); @@ -441,7 +442,7 @@ print("Forwarding") ```typescript -// Install: npm install @signalwire/sdk@2.0.5 +// Install: npm install @signalwire/sdk@3.7.1 // This sample also runs as JavaScript: save as forward-rest.mjs, // then run: node forward-rest.mjs import { RestClient, SwmlBuilder } from "@signalwire/sdk"; @@ -455,8 +456,8 @@ const client = new RestClient({ const swml = new SwmlBuilder() .connect({ to: "", from: "${call.from}", timeout: 20 }) .say("Sorry, nobody at Bayview Taxi can take your call right now. Please call back later.") - .build(); -await client.calling.transfer("", { dest: swml }); + .render(); // JSON text; parse it to pass the document object to transfer() +await client.calling.transfer("", JSON.parse(swml)); console.log("Forwarding"); ``` diff --git a/fern/products/platform/pages/calling/voice/machine-detection.mdx b/fern/products/platform/pages/calling/voice/machine-detection.mdx index 6fd0a65033..5a50258211 100644 --- a/fern/products/platform/pages/calling/voice/machine-detection.mdx +++ b/fern/products/platform/pages/calling/voice/machine-detection.mdx @@ -153,7 +153,7 @@ print(call["id"]) ```typescript -// Install: npm install @signalwire/sdk@2.0.5 +// Install: npm install @signalwire/sdk@3.7.1 // This sample also runs as JavaScript: save as leave-message.mjs, // then run: node leave-message.mjs import { RestClient, SwmlBuilder } from "@signalwire/sdk"; @@ -185,12 +185,10 @@ const swml = new SwmlBuilder() default: live, }) .hangup() - .build(); + .render(); // JSON text; parse it to pass the document object to dial() -const call = await client.calling.dial({ - from: "", - to: "", - swml, +const call = await client.calling.dial("", "", { + swml: JSON.parse(swml), }); console.log(call.id); ``` @@ -328,7 +326,7 @@ print(call["id"]) ```typescript -// Install: npm install @signalwire/sdk@2.0.5 +// Install: npm install @signalwire/sdk@3.7.1 // This sample also runs as JavaScript: save as detect-fax.mjs, // then run: node detect-fax.mjs import { RestClient, SwmlBuilder } from "@signalwire/sdk"; @@ -360,12 +358,10 @@ const swml = new SwmlBuilder() default: hangUp, }) .hangup() - .build(); + .render(); // JSON text; parse it to pass the document object to dial() -const call = await client.calling.dial({ - from: "", - to: "", - swml, +const call = await client.calling.dial("", "", { + swml: JSON.parse(swml), }); console.log(call.id); ``` @@ -600,7 +596,7 @@ asyncio.run(main()) ```typescript -// Install: npm install @signalwire/sdk@2.0.5 +// Install: npm install @signalwire/sdk@3.7.1 // Save as leave-message-relay.mts and run: npx tsx leave-message-relay.mts import { RelayClient, DetectEvent } from "@signalwire/sdk"; @@ -743,7 +739,7 @@ asyncio.run(main()) ```typescript -// Install: npm install @signalwire/sdk@2.0.5 +// Install: npm install @signalwire/sdk@3.7.1 // Save as detect-fax-relay.mts and run: npx tsx detect-fax-relay.mts import { RelayClient, RelayEvent, FaxEvent } from "@signalwire/sdk"; diff --git a/fern/products/platform/pages/calling/voice/make-and-receive-calls.mdx b/fern/products/platform/pages/calling/voice/make-and-receive-calls.mdx index 6157feb556..24629947ac 100644 --- a/fern/products/platform/pages/calling/voice/make-and-receive-calls.mdx +++ b/fern/products/platform/pages/calling/voice/make-and-receive-calls.mdx @@ -161,7 +161,7 @@ print(call["id"]) ```typescript -// Install: npm install @signalwire/sdk@2.0.5 +// Install: npm install @signalwire/sdk@3.7.1 // This sample also runs as JavaScript: save as outbound-call.mjs, // then run: node outbound-call.mjs import { RestClient, SwmlBuilder } from "@signalwire/sdk"; @@ -174,12 +174,10 @@ const client = new RestClient({ const swml = new SwmlBuilder() .say("Hello, welcome to SignalWire!") - .build(); + .render(); // JSON text; parse it to pass the document object to dial() -const call = await client.calling.dial({ - from: "", - to: "", - swml, +const call = await client.calling.dial("", "", { + swml: JSON.parse(swml), }); console.log(call.id); ``` @@ -208,11 +206,6 @@ curl -X POST "https://.signalwire.com/api/calling/calls" \ - -`@signalwire/sdk@2.0.5` takes a single options object. Newer releases take the positional -`dial(from, to, options)` form. - - The cURL example sends the `dial` command to [Call commands][rest-call-commands]. The REST API returns a call `id` and status `queued`. This confirms that SignalWire accepted the @@ -303,7 +296,7 @@ service.serve() ```typescript -// Install: npm install @signalwire/sdk@2.0.5 +// Install: npm install @signalwire/sdk@3.7.1 // This sample also runs as JavaScript: save as inbound-swml-server.mjs, // then run: node inbound-swml-server.mjs import { SWMLService } from "@signalwire/sdk"; @@ -428,7 +421,7 @@ print(script["id"]) ```typescript -// Install: npm install @signalwire/sdk@2.0.5 +// Install: npm install @signalwire/sdk@3.7.1 // This sample also runs as JavaScript: save as inbound-script.mjs, // then run: node inbound-script.mjs import { RestClient, SwmlBuilder } from "@signalwire/sdk"; @@ -441,7 +434,7 @@ const client = new RestClient({ const swml = new SwmlBuilder() .say("Hello, welcome to SignalWire!") - .build(); + .render(); // The script's contents are SWML as JSON text const script = await client.fabric.swmlScripts.create({ name: "Inbound welcome", @@ -510,7 +503,7 @@ client.phone_numbers.set_swml_webhook( ```typescript -// Install: npm install @signalwire/sdk@2.0.5 +// Install: npm install @signalwire/sdk@3.7.1 // This sample also runs as JavaScript: save as assign-swml-webhook.mjs, // then run: node assign-swml-webhook.mjs import { RestClient } from "@signalwire/sdk"; @@ -592,11 +585,9 @@ call = client.calling.dial( ``` -```typescript {5-6} -const call = await client.calling.dial({ - from: "", - to: "", - swml, +```typescript {3-4} +const call = await client.calling.dial("", "", { + swml: JSON.parse(swml), status_url: "", status_events: ["ringing", "answered", "ended"], }); @@ -753,7 +744,7 @@ asyncio.run(main()) ```typescript -// Install: npm install @signalwire/sdk@2.0.5 +// Install: npm install @signalwire/sdk@3.7.1 // This sample also runs as JavaScript: save as outbound-relay.mjs, // then run: node outbound-relay.mjs import { RelayClient } from "@signalwire/sdk"; @@ -850,7 +841,7 @@ client.run() ```typescript -// Install: npm install @signalwire/sdk@2.0.5 +// Install: npm install @signalwire/sdk@3.7.1 // This sample also runs as JavaScript: save as inbound-relay.mjs, // then run: node inbound-relay.mjs import { RelayClient } from "@signalwire/sdk"; @@ -916,7 +907,7 @@ client.phone_numbers.set_relay_topic("", "inbound-calling" ```typescript -// Install: npm install @signalwire/sdk@2.0.5 +// Install: npm install @signalwire/sdk@3.7.1 // This sample also runs as JavaScript: save as assign-relay-topic.mjs, // then run: node assign-relay-topic.mjs import { RestClient } from "@signalwire/sdk"; @@ -927,7 +918,7 @@ const client = new RestClient({ host: ".signalwire.com", }); -await client.phoneNumbers.setRelayTopic("", { topic: "inbound-calling" }); +await client.phoneNumbers.setRelayTopic("", "inbound-calling"); ``` @@ -1063,7 +1054,7 @@ print(sat["token"]) ```typescript -// Install: npm install @signalwire/sdk@2.0.5 +// Install: npm install @signalwire/sdk@3.7.1 // This sample also runs as JavaScript: save as subscriber-token.mjs, // then run: node subscriber-token.mjs import { RestClient } from "@signalwire/sdk"; @@ -1077,9 +1068,7 @@ const client = new RestClient({ // Create the Subscriber once. Later, run only the token request for a fresh token. await client.fabric.subscribers.create({ email: "" }); -const sat = await client.fabric.tokens.createSubscriberToken({ - reference: "", -}); +const sat = await client.fabric.tokens.createSubscriberToken(""); console.log(sat.token); ``` diff --git a/fern/products/platform/pages/calling/voice/record-calls.mdx b/fern/products/platform/pages/calling/voice/record-calls.mdx index 6f25684bf8..aac304fb55 100644 --- a/fern/products/platform/pages/calling/voice/record-calls.mdx +++ b/fern/products/platform/pages/calling/voice/record-calls.mdx @@ -136,7 +136,7 @@ if __name__ == "__main__": ``` ```typescript title="TypeScript — Agents" {9-11} -// Install: npm install @signalwire/sdk@2.0.5 +// Install: npm install @signalwire/sdk@3.7.1 // This sample also runs as JavaScript: save as record-whole-call.mjs, // then run: node record-whole-call.mjs import { AgentBase } from '@signalwire/sdk'; @@ -249,7 +249,7 @@ client.run() ``` ```typescript title="TypeScript — Relay client" {17} -// Install: npm install @signalwire/sdk@2.0.5 +// Install: npm install @signalwire/sdk@3.7.1 // This sample also runs as JavaScript: save as record-whole-call-relay.mjs, // then run: node record-whole-call-relay.mjs import { RelayClient } from '@signalwire/sdk'; @@ -380,7 +380,7 @@ if __name__ == "__main__": ``` ```typescript title="TypeScript — Agents" {21-26} -// Install: npm install @signalwire/sdk@2.0.5 +// Install: npm install @signalwire/sdk@3.7.1 // This sample also runs as JavaScript: save as recording-consent.mjs, // then run: node recording-consent.mjs import { AgentBase, FunctionResult } from '@signalwire/sdk'; @@ -448,7 +448,7 @@ async def handle_call(call): await call.connect([[{"type": "phone", "params": {"to_number": "", "from_number": "", "timeout": 30}}]]) ``` -```typescript title="TypeScript — Relay client" {12-14} +```typescript title="TypeScript — Relay client" {11-16} // Without an AI agent, gate the recording on a keypress instead of a tool call. client.onCall(async (call) => { await call.answer(); @@ -459,8 +459,10 @@ client.onCall(async (call) => { { digits: { max: 1, digit_timeout: 5, terminators: '#' } } ); const event = await action.wait(); + // A keypress result is { type: 'digit', params: { digits, terminator } }. + const digits = Object(event.params.result).params?.digits; - if (event.params.result?.params?.digits === '1') { + if (digits === '1') { await call.record({ direction: 'both', format: 'mp3', stereo: true }); } @@ -727,7 +729,7 @@ if __name__ == "__main__": ``` ```typescript title="TypeScript — Agents" {13-27} -// Install: npm install @signalwire/sdk@2.0.5 +// Install: npm install @signalwire/sdk@3.7.1 // This sample also runs as JavaScript: save as voicemail-agent.mjs, // then run: node voicemail-agent.mjs import { AgentBase, FunctionResult } from '@signalwire/sdk'; @@ -843,16 +845,16 @@ async def handle_call(call): await call.hangup() ``` -```typescript title="TypeScript — Relay client" {7-9} +```typescript title="TypeScript — Relay client" {7-11} client.onCall(async (call) => { await call.answer(); const prompt = await call.play([{ type: 'tts', text: 'Leave a message after the beep.' }]); await prompt.wait(); // play returns as soon as the command is accepted - const action = await call.record({ - beep: true, end_silence_timeout: 3, terminators: '#', max_length: 120, - }); + // The SDK's record() type doesn't list max_length yet; passing a variable sends it unchanged. + const audio = { beep: true, end_silence_timeout: 3, terminators: '#', max_length: 120 }; + const action = await call.record(audio); const event = await action.wait(); // no_input is terminal too: a caller who never spoke still gets a url, but its media returns 404 diff --git a/fern/products/platform/pages/calling/voice/sip-refer.mdx b/fern/products/platform/pages/calling/voice/sip-refer.mdx index 57d3362ffc..6085e49ca7 100644 --- a/fern/products/platform/pages/calling/voice/sip-refer.mdx +++ b/fern/products/platform/pages/calling/voice/sip-refer.mdx @@ -250,7 +250,7 @@ print(call["id"]) ```typescript -// Install: npm install @signalwire/sdk@2.0.5 +// Install: npm install @signalwire/sdk@3.7.1 // This sample also runs as JavaScript: save as sip-refer.mjs, // then run: node sip-refer.mjs import { RestClient, SwmlBuilder } from "@signalwire/sdk"; @@ -283,12 +283,10 @@ const swml = new SwmlBuilder() default: failed, }) .hangup() - .build(); + .render(); // JSON text; parse it to pass the document object to dial() -const call = await client.calling.dial({ - from: "", - to: "", - swml, +const call = await client.calling.dial("", "", { + swml: JSON.parse(swml), }); console.log(call.id); ``` @@ -458,7 +456,7 @@ asyncio.run(main()) ```typescript -// Install: npm install @signalwire/sdk@2.0.5 +// Install: npm install @signalwire/sdk@3.7.1 // Save as sip-refer-relay.mts and run: npx tsx sip-refer-relay.mts import { RelayClient, ReferEvent } from "@signalwire/sdk"; @@ -562,7 +560,7 @@ client.calling.refer( ```typescript -// Install: npm install @signalwire/sdk@2.0.5 +// Install: npm install @signalwire/sdk@3.7.1 // This sample also runs as JavaScript: save as refer-call.mjs, // then run: node refer-call.mjs import { RestClient } from "@signalwire/sdk"; diff --git a/fern/products/platform/pages/calling/whatsapp/receive-calls.mdx b/fern/products/platform/pages/calling/whatsapp/receive-calls.mdx index bbeea9196f..720998c93e 100644 --- a/fern/products/platform/pages/calling/whatsapp/receive-calls.mdx +++ b/fern/products/platform/pages/calling/whatsapp/receive-calls.mdx @@ -205,7 +205,7 @@ agent.serve() ```typescript -// Install: npm install @signalwire/sdk@2.0.5 +// Install: npm install @signalwire/sdk@3.7.1 // Save as whatsapp-agent.mts and run: npx tsx whatsapp-agent.mts import { AgentBase } from "@signalwire/sdk"; From 2e2c4c07069fb137620b81262945aa0bce1a3741 Mon Sep 17 00:00:00 2001 From: August Date: Thu, 22 Oct 2026 19:33:47 +0000 Subject: [PATCH 11/15] docs(rules): pin TypeScript SDK 3.7.1 and tie guide pins to the reference sync --- .claude/rules/mdx-capability-guides.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.claude/rules/mdx-capability-guides.md b/.claude/rules/mdx-capability-guides.md index e78f406c13..9a4f9ed846 100644 --- a/.claude/rules/mdx-capability-guides.md +++ b/.claude/rules/mdx-capability-guides.md @@ -102,8 +102,9 @@ Canonical names come from the glossary (`/docs/platform/glossary`); the ones thi - **SDK examples first.** For every API operation covered by a supported SDK, lead the explanation with the SDK method and make its example the first visible option. In a shared ``, order Python SDK, then TypeScript SDK, then the direct HTTP alternative (see "Direct HTTP" above). Apply this to setup, resource creation, authentication, call control, and follow-up tasks as well as the first call. In prose, say "the Server SDKs" and link to the landing page (`/docs/server-sdks`), before the underlying REST endpoint; never name a language or link a per-language reference page. Code block titles still name the language. If no supported SDK method exists for the documented version, explain the gap and use direct HTTP for that operation. A hosted-script-only step can show YAML and JSON directly because the reader pastes a document into the Dashboard. - **Complete programs.** Every sample under `` and `## Examples` runs as pasted: imports, client setup, the call, and the wait or disconnect. A block under a task section may show only the changed lines, with line highlights (`{6-8}`), when its paragraph names the sample it extends. -- **Header comments** on every complete sample. Python: `# Install: python -m pip install signalwire-sdk==3.4.1` then `# Save as .py and run: python .py`. TypeScript with no type annotations: `// Install: npm install @signalwire/sdk@2.0.5` then `// This sample also runs as JavaScript: save as .mjs,` / `// then run: node .mjs`. TypeScript that uses types: `// Save as .mts and run: npx tsx .mts`. Browser: `// Install: npm install @signalwire/js@4.0.0-rc.2 rxjs@7.8.2` plus the HTML elements the script expects, as comments. -- **Pinned versions** are literal and identical across every guide: `signalwire-sdk==3.4.1`, `@signalwire/sdk@2.0.5`, `@signalwire/js@4.0.0-rc.2 rxjs@7.8.2`. Bump them in every guide in one PR, never one page at a time. +- **Header comments** on every complete sample. Python: `# Install: python -m pip install signalwire-sdk==3.4.1` then `# Save as .py and run: python .py`. TypeScript with no type annotations: `// Install: npm install @signalwire/sdk@3.7.1` then `// This sample also runs as JavaScript: save as .mjs,` / `// then run: node .mjs`. TypeScript that uses types: `// Save as .mts and run: npx tsx .mts`. Browser: `// Install: npm install @signalwire/js@4.0.0-rc.2 rxjs@7.8.2` plus the HTML elements the script expects, as comments. +- **Pinned versions** are literal and identical across every guide: `signalwire-sdk==3.4.1`, `@signalwire/sdk@3.7.1`, `@signalwire/js@4.0.0-rc.2 rxjs@7.8.2`. Bump them in every guide in one PR, never one page at a time. +- **Pins match the reference.** Each pinned Server SDK version must be the release whose source the reference pages are synced to, as recorded for that SDK in `fern/products/server-sdks/sdk-source-sync.json` (`synced_commit` is that release's tag commit). Bumping a pin means syncing that SDK's reference to the same release, and syncing the reference to a new release means bumping the pin: do both in the same PR, or guide samples and reference pages disagree. - **Placeholders**: ``, ``, ``, ``, ``, ``, ``; new ones follow the same `` shape. Explain them once, in a `| Value | Replace with |` table before the first code block. - **Relay boilerplate**: `contexts=["default"]`, `timeout: 30` on the phone device, Python `async with client:` and `await call.wait_for_ended()`, TypeScript `await client.connect()` / `try … finally { await client.disconnect(); }` and `await call.waitForEnded()`. Hang up in an `on_completed`/`onCompleted` callback guarded by `call.state !== "ended"`. - **Builder gaps**: when the SDK's SWML builder lacks a verb, say so in a comment and use `add_verb`/`addVerb` with validation off for that verb only. Don't silently hand-write JSON where the builder works. From 5c341a4c54ecae75db9f5851c8c7ad4ca7284fd0 Mon Sep 17 00:00:00 2001 From: August Date: Fri, 23 Oct 2026 15:54:30 +0000 Subject: [PATCH 12/15] docs(calling): skip enter_queue validation per verb and fix record-calls highlight --- .../pages/calling/voice/call-queues.mdx | 23 ++++++++++--------- .../pages/calling/voice/record-calls.mdx | 2 +- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/call-queues.mdx b/fern/products/platform/pages/calling/voice/call-queues.mdx index fe67fde5e4..f12af458cc 100644 --- a/fern/products/platform/pages/calling/voice/call-queues.mdx +++ b/fern/products/platform/pages/calling/voice/call-queues.mdx @@ -203,17 +203,14 @@ const service = new SWMLService({ route: "/swml", port: 3000, basicAuth: ["signalwire", ""], - // @signalwire/sdk 3.7.1 validates enter_queue against an outdated schema - // that rejects every valid document, so validation is off for this service. - schemaValidation: false, }); -service - .getBuilder() - .say("Thanks for calling Bayview Taxi. A dispatcher will be with you shortly.") - .enter_queue({ queue_name: "dispatch", wait_time: 120 }) - // Runs only if the caller leaves the queue without reaching an agent. - .say("All our dispatchers are busy. Please call back in a few minutes.") - .hangup(); +const swml = service.getBuilder(); +swml.say("Thanks for calling Bayview Taxi. A dispatcher will be with you shortly."); +// The SDK's SWML schema requires transfer_after_bridge on enter_queue, which the +// platform treats as optional, so skip validation for this verb only. +swml.addVerb("enter_queue", { queue_name: "dispatch", wait_time: 120 }, { skipValidation: true }); +// Runs only if the caller leaves the queue without reaching an agent. +swml.say("All our dispatchers are busy. Please call back in a few minutes.").hangup(); await service.serve(); ``` @@ -443,7 +440,11 @@ setup and local testing. ```typescript - .enter_queue({ queue_name: "dispatch", wait_time: 120, status_url: "" }) +swml.addVerb( + "enter_queue", + { queue_name: "dispatch", wait_time: 120, status_url: "" }, + { skipValidation: true }, +); ``` diff --git a/fern/products/platform/pages/calling/voice/record-calls.mdx b/fern/products/platform/pages/calling/voice/record-calls.mdx index aac304fb55..a721ecdfcf 100644 --- a/fern/products/platform/pages/calling/voice/record-calls.mdx +++ b/fern/products/platform/pages/calling/voice/record-calls.mdx @@ -845,7 +845,7 @@ async def handle_call(call): await call.hangup() ``` -```typescript title="TypeScript — Relay client" {7-11} +```typescript title="TypeScript — Relay client" {7-9} client.onCall(async (call) => { await call.answer(); From af750e451c2cfd9a26e698d383b95f2a56f1db38 Mon Sep 17 00:00:00 2001 From: August Date: Fri, 23 Oct 2026 15:54:36 +0000 Subject: [PATCH 13/15] docs(server-sdks): correct TypeScript DataMap webhook method, chaining, and requireArgs --- .../typescript/agents/data-map/webhook.mdx | 46 +++++++++++-------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/data-map/webhook.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/data-map/webhook.mdx index 9e9f7d0f97..8e11e566c3 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/data-map/webhook.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/data-map/webhook.mdx @@ -6,20 +6,22 @@ max-toc-depth: 3 --- [ref-datamap]: /docs/server-sdks/reference/typescript/agents/data-map +[ref-fallback-output]: /docs/server-sdks/reference/typescript/agents/data-map/fallback-output -Add an API call. Multiple webhooks can be chained -- they execute in order, and -if earlier webhooks fail, later ones act as fallbacks. +Add an API call. Multiple webhooks can be chained. The platform tries them in +order, skipping any whose `requireArgs` are all absent, and the first webhook it +requests decides the result: if that request fails, the +[fallback output][ref-fallback-output] +is used and no later webhook is tried. ## **Parameters** - HTTP method for the request. + HTTP method for the request: `"GET"` or `"POST"`. It is uppercased. - - `"GET"` -- retrieve a resource - - `"POST"` -- create a resource or submit data - - `"PUT"` -- replace a resource - - `"DELETE"` -- remove a resource - - `"PATCH"` -- partially update a resource + The platform sends GET and POST requests only. A webhook is sent as a POST when + `method` is `"POST"` or when it has params, and as a GET otherwise, so `"PUT"`, + `"PATCH"`, and `"DELETE"` are sent as GET. @@ -45,7 +47,8 @@ if earlier webhooks fail, later ones act as fallbacks. - Only execute this webhook if all listed arguments are present. + Skip this webhook, without a request, unless at least one of the listed + arguments is present. @@ -70,18 +73,21 @@ weather console.log(weather.toSwaigFunction()); ``` -Chained webhooks act as fallbacks -- if the first webhook fails, the second is tried: +Chain webhooks with `requireArgs` to pick an endpoint by the arguments the caller +gave. A failed request isn't retried on the next webhook; it gives the fallback +output: -```typescript {7,9} +```typescript {8-11} import { DataMap, FunctionResult } from '@signalwire/sdk'; -const search = new DataMap('search'); -search - .purpose('Search across multiple providers') - .parameter('query', 'string', 'Search query', { required: true }) - .webhook('GET', 'https://primary-api.example.com/search?q=${enc:args.query}') - .output(new FunctionResult('Primary: ${result}')) - .webhook('GET', 'https://backup-api.example.com/search?q=${enc:args.query}') - .output(new FunctionResult('Backup: ${result}')) - .fallbackOutput(new FunctionResult('All search providers are unavailable.')); +const lookup = new DataMap('find_order'); +lookup + .purpose('Find an order by order number or by email') + .parameter('order_id', 'string', 'Order number') + .parameter('email', 'string', 'Email address on the order') + .webhook('GET', 'https://api.example.com/orders/${enc:args.order_id}', { requireArgs: ['order_id'] }) + .output(new FunctionResult('Order ${id} is ${status}.')) + .webhook('GET', 'https://api.example.com/orders?email=${enc:args.email}', { requireArgs: ['email'] }) + .output(new FunctionResult('The latest order for that email is ${status}.')) + .fallbackOutput(new FunctionResult('I could not look up that order.')); ``` From 590dec707513667a77d64f4e0b79cbefe76906ed Mon Sep 17 00:00:00 2001 From: August Date: Fri, 23 Oct 2026 15:54:46 +0000 Subject: [PATCH 14/15] docs(server-sdks): correct TypeScript skill instance keys and skill parameters --- .../agents/skill-base/get-instance-key.mdx | 19 ++--- .../typescript/agents/skill-base/index.mdx | 7 +- .../typescript/agents/skills/mcp-gateway.mdx | 11 +-- .../agents/skills/native-vector-search.mdx | 83 ++++--------------- 4 files changed, 37 insertions(+), 83 deletions(-) diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/skill-base/get-instance-key.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/skill-base/get-instance-key.mdx index 63cf13d93c..1746b43aed 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/skill-base/get-instance-key.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/skill-base/get-instance-key.mdx @@ -9,18 +9,17 @@ Get the unique key used to track this skill instance. ## **Returns** -`string` -- By default, returns `this.skillName`. Subclasses that support multiple -instances typically override this to return a more specific key (e.g., -`"_"` derived from `config`). +`string` -- When the class's `SUPPORTS_MULTIPLE_INSTANCES` is `false` (the +default), returns `this.skillName`. When it's `true`, returns +`"_"`, where `toolName` is `config.tool_name` or, if that's +unset, the skill name. Override it to key instances by something else. ## **Example** ```typescript {2} -class MySkill extends SkillBase { - getInstanceKey(): string { - const toolName = this.getConfig('tool_name'); - return toolName ? `${this.skillName}_${toolName}` : this.skillName; - } +class NotifySkill extends SkillBase { + static SUPPORTS_MULTIPLE_INSTANCES = true; + // ... } ``` @@ -30,9 +29,9 @@ Instance key behavior: import { AgentBase } from '@signalwire/sdk'; const agent = new AgentBase({ name: 'demo' }); -// With MySkill registered as "notify": +// With NotifySkill registered as "notify": await agent.addSkillByName('notify'); -// Instance key: "notify" +// Instance key: "notify_notify" await agent.addSkillByName('notify', { tool_name: 'email' }); // Instance key: "notify_email" diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/skill-base/index.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/skill-base/index.mdx index 0fa662c652..d1f2735826 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/skill-base/index.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/skill-base/index.mdx @@ -243,12 +243,15 @@ class MyWeatherSkill extends SkillBase { ### Using a skill -```typescript {6} +```typescript {6-9} import { AgentBase } from '@signalwire/sdk'; const agent = new AgentBase({ name: 'weather-agent' }); agent.setPromptText("You are a helpful assistant."); -await agent.addSkillByName('weather_api', { units: 'metric' }); +await agent.addSkillByName('weather_api', { + api_key: 'YOUR_API_KEY', // or set WEATHER_API_KEY; setup fails without a key + units: 'metric', +}); agent.run(); ``` diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/skills/mcp-gateway.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/skills/mcp-gateway.mdx index ba3b59822e..36fc9f45b1 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/skills/mcp-gateway.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/skills/mcp-gateway.mdx @@ -13,10 +13,11 @@ services and tools, and registers each as a SWAIG function on the agent. The gateway URL is validated by an SSRF guard — private, loopback, and cloud-metadata endpoints are rejected. Every later request, and every redirect it follows, is checked the same way, so a gateway that redirects to a private or -internal address, or a hostname that later resolves to one, is refused. Requests -that fail on a connection error, a timeout, or a refused address are retried up -to `retry_attempts` times, with a per-request timeout of `request_timeout` -seconds. +internal address, or a hostname that later resolves to one, is refused. A tool +call is tried up to `retry_attempts` times in total, with a per-request timeout +of `request_timeout` seconds. It's tried again after a connection error, a +timeout, a refused address, or an HTTP 5xx response, but not after a 4xx +response or a `200` whose body isn't JSON. **Class:** `McpGatewaySkill` @@ -63,7 +64,7 @@ seconds. - Number of retry attempts for failed requests. + Total attempts per tool call, including the first. diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/skills/native-vector-search.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/skills/native-vector-search.mdx index afb0b4507f..04cf2b56b7 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/skills/native-vector-search.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/skills/native-vector-search.mdx @@ -18,8 +18,8 @@ two modes: The Python SDK's local-index parameters (`index_file`, `build_index`, `backend`, and the rest) are accepted so a Python configuration still loads, -but this skill doesn't use them. Only `index_file` has an effect, as part of -the instance key. The `search_stats.backend` value the skill writes to global +but this skill doesn't use them; see +[Python compatibility parameters](#python-compatibility-parameters). The `search_stats.backend` value the skill writes to global data is `remote` with `remote_url` and `memory` otherwise. @@ -38,19 +38,6 @@ data is `remote` with `remote_url` and `memory` otherwise. instances on the same agent. - - Path to a local `.swsearch` SQLite index file. - - - - Whether to build the index from source files at startup. - - - - Directory containing documents to index. Required when `build_index` is - `true`. - - URL of a remote search server (network mode). Validated by the SSRF guard — private, loopback, and cloud-metadata endpoints are rejected. Every request, @@ -85,11 +72,6 @@ data is `remote` with `remote_url` and `memory` otherwise. where the server ranks results, and logs a warning there. - - Accepted for compatibility with the Python SDK, and has no effect: in-memory - mode ranks with TF-IDF, and a remote server uses its index's own model. - - ## Content Parameters @@ -98,16 +80,8 @@ data is `remote` with `remote_url` and `memory` otherwise. - Tags applied to every document at index-build time. - - - - File extensions to include when building an index from `source_dir`. - - - - Glob patterns excluded from index builds. Defaults include - `**/node_modules/**`, `**/.git/**`, `**/dist/**`, `**/build/**`. + Tags added to every entry in `documents` when the skill indexes them at + startup. @@ -143,48 +117,25 @@ data is `remote` with `remote_url` and `memory` otherwise. Additional speech recognition hints for the tool. -## NLP Parameters - - - NLP backend for query processing: `"basic"`, `"spacy"`, or `"nltk"`. - **Deprecated** — use `query_nlp_backend` and `index_nlp_backend` instead. - - - - NLP backend for query expansion: `"basic"`, `"spacy"`, or `"nltk"`. - - - - NLP backend for indexing: `"basic"`, `"spacy"`, or `"nltk"`. - - -## Backend Parameters - - - Storage backend for local database mode: `"sqlite"` or `"pgvector"`. Ignored - when `remote_url` is set. SQLite and pgvector backends require native - Python-only dependencies; configurations written for the Python SDK remain - valid but the TypeScript SDK only exercises the in-memory (`documents`) and - remote (`remote_url`) paths. - - - - PostgreSQL connection string. Required when `backend="pgvector"`. - - - - PostgreSQL collection name. Required when `backend="pgvector"`. - - ## Other Parameters Enable verbose logging during indexing and search. - - Overwrite existing pgvector collection when building the index. - +## Python compatibility parameters + +The skill accepts these parameters so a configuration written for the Python +SDK still loads, but it doesn't use them. The in-memory mode ranks with TF-IDF, +and a remote server uses its own index and model. + +| Parameter | Effect in TypeScript | +|---|---| +| `index_file` | Part of the instance key only; no index file is read. | +| `build_index`, `source_dir`, `file_types`, `exclude_patterns` | None. No index is built from files. | +| `backend`, `connection_string`, `collection_name`, `overwrite` | None. The SQLite and pgvector backends aren't available. | +| `model_name` | None. | +| `nlp_backend`, `query_nlp_backend`, `index_nlp_backend` | None. | ## Example — in-memory documents From 77ee23dd83af0c2fcb473575f3c5ba1e63a5b44e Mon Sep 17 00:00:00 2001 From: August Date: Fri, 23 Oct 2026 15:54:54 +0000 Subject: [PATCH 15/15] docs(server-sdks): fix TypeScript Bedrock, FunctionResult, and SemanticGate details --- .../pages/reference/typescript/agents/bedrock-agent/index.mdx | 2 +- .../typescript/agents/bedrock-agent/set-inference-params.mdx | 2 +- .../pages/reference/typescript/agents/function-result/pay.mdx | 4 ++-- .../typescript/agents/function-result/rpc-ai-global-data.mdx | 4 ++-- .../pages/reference/typescript/agents/semantic-gate/index.mdx | 3 ++- 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/bedrock-agent/index.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/bedrock-agent/index.mdx index aeeef10d6a..824624c786 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/bedrock-agent/index.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/bedrock-agent/index.mdx @@ -60,7 +60,7 @@ The `createBedrockAgent(config)` factory function returns the same instance as - Generation temperature. Range: 0 to 1. A numeric string is converted, and a SWML + Generation temperature. Range: 0 to 2. A numeric string is converted, and a SWML variable reference such as `'${temperature}'` is kept; anything else throws. diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/bedrock-agent/set-inference-params.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/bedrock-agent/set-inference-params.mdx index cda78ec50e..b6c500d28a 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/bedrock-agent/set-inference-params.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/bedrock-agent/set-inference-params.mdx @@ -13,7 +13,7 @@ an argument left `undefined` keeps its current value. ## **Parameters** - Generation temperature. Range: 0 to 1. A numeric string is converted, and a SWML + Generation temperature. Range: 0 to 2. A numeric string is converted, and a SWML variable reference such as `'${temperature}'` is kept; anything else throws and changes nothing. diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/function-result/pay.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/function-result/pay.mdx index 742ff79af4..13ad544d83 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/function-result/pay.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/function-result/pay.mdx @@ -65,8 +65,8 @@ voice, then submits to your payment connector endpoint. string, or a SWML variable reference. `timeout`, `maxAttempts`, `securityCode`, `minPostalCodeLength`, and `postalCode` - are sent as strings, as the platform reads them. Each is checked first, and an - invalid value throws an `Error`. + are sent as strings, as the platform reads them. All but `postalCode` are checked + first, and an invalid value throws an `Error`. diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/function-result/rpc-ai-global-data.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/function-result/rpc-ai-global-data.mdx index 1f283f75e5..2a57cfb0cf 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/function-result/rpc-ai-global-data.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/function-result/rpc-ai-global-data.mdx @@ -29,7 +29,7 @@ prompt reads it back with `${global_data.key}`. ## Example -```typescript {20-22} +```typescript {20-21} import { AgentBase, FunctionResult } from '@signalwire/sdk'; const agent = new AgentBase({ name: 'dispatch', route: '/dispatch' }); @@ -44,8 +44,8 @@ agent.defineTool({ waiting_call_id: { type: 'string', description: 'Call ID of the waiting caller' }, eta_minutes: { type: 'integer', description: 'Minutes until pickup' }, }, + required: ['waiting_call_id', 'eta_minutes'], }, - required: ['waiting_call_id', 'eta_minutes'], handler: (args) => { const waitingCallId = String(args.waiting_call_id); return new FunctionResult('Tell the driver the passenger has been updated.') diff --git a/fern/products/server-sdks/pages/reference/typescript/agents/semantic-gate/index.mdx b/fern/products/server-sdks/pages/reference/typescript/agents/semantic-gate/index.mdx index d9a1e4fdda..15bb6f61bf 100644 --- a/fern/products/server-sdks/pages/reference/typescript/agents/semantic-gate/index.mdx +++ b/fern/products/server-sdks/pages/reference/typescript/agents/semantic-gate/index.mdx @@ -92,7 +92,8 @@ function names: `startup_hook`, `hangup_hook`, `check_for_input`, `end_call`, gate, sent as `visual_input`: `true` is 1, `false` or `0` is none, or a whole number up to `MAX_VISUAL_INPUT` (5). Every gate on a function sees as many frames as the gate that asks for the most. If the call has no video frame to show, the - gate fails without being asked. The gate's provider must accept images. Omitted by + gate fails without being asked. The gate's provider must accept images: one that + doesn't makes the check an error, and the function isn't called. Omitted by default. Requires SDK 3.7.1 or later.