Skip to main content

Telnyx AI: Agent SDK (Beta) — Full Documentation

Complete page content for Agent SDK (Beta) (AI section) of the Telnyx developer docs (https://developers.telnyx.com). This file: https://developers.telnyx.com/development/llms/ai-agent-sdk-beta-llms-full-txt.md · Root index: https://developers.telnyx.com/llms.txt

Get Started

Overview

Source: https://developers.telnyx.com/docs/agent-sdk.md
The Agent SDK is a TypeScript base class, Agent, that extends StatefulActor with the primitives AI agents need most: a persistent conversation history, durable scheduled tasks, and merge-patch state — with no extra infrastructure to manage. Agent ships as an export of @telnyx/edge-runtime, alongside StatefulActor. There is no separate package to install.
The Agent base class is in Beta. The API surface may change.

What Agent adds to StatefulActor

State is durable and survives restarts — the same persistence guarantee that StatefulActors provide. Everything StatefulActor provides is inherited too, including the embedded SQL database and WebSocket termination.

Bring your own harness

The loop that makes an agent more than a single model call — build the prompt from history, call the model, run the tools it asks for, decide whether to continue or stop — is called a harness. The Agent SDK deliberately doesn’t ship one. It is the substrate a harness runs on: history, state, and timers stay durable underneath whatever loop you run, and this.messages converts history to the format your stack expects. Two ways to wire one in:
  • Roll your own. process() is the harness: it calls inference through the pre-authenticated Telnyx API binding — no API key to manage — and toOpenAI() produces exactly the payload it takes. → Roll Your Own Agent
  • Bring a framework as the harness. LangGraph, LangChain — anything that runs on Node — executes inside the actor, with the SDK as its durable memory and scheduler. → LangGraph Agent
Either way you bring your own LLM: Telnyx Inference is wired in through the binding, and any OpenAI-compatible provider is one baseURL away. See Calling LLMs. And because the substrate is harness-neutral, an opinionated first-party harness can slot in later without changing anything you build now.

Where to go next


Quickstart

Source: https://developers.telnyx.com/docs/agent-sdk/quickstart.md
The simplest agent receives a message, queues a background task to process it, then schedules a follow-up.
Env is a global type generated by telnyx-edge types from your telnyx.toml — it includes SUPPORT: ActorNamespace<SupportAgent> and any other bindings you declare. Run telnyx-edge types before tsc or ship to keep it in sync. telnyx.toml:

Deploy

Generate binding types, then ship:
Your function is live at https://<name>-<id>.telnyxcompute.com — see edge compute configuration for the full reference.

Next steps


Concepts

How Agents Run

Source: https://developers.telnyx.com/docs/agent-sdk/concepts/how-agents-run.md
An Agent is a StatefulActor — understanding four properties of that execution model explains why the SDK looks the way it does.

One actor per conversation

You address an agent by name: env.CONVOS.idFromName("+15550001111") always routes to the same actor instance, anywhere in the fleet. That makes the phone number (or user id, or session id) the unit of isolation — each conversation gets its own history, state, and timers, with no cross-talk and no shared database to partition. See Addressing.

Turns are serialized

An actor runs one method call at a time. While process() is thinking, a second inbound message waits — it never interleaves. That is what makes the read-modify-write patterns in agent code safe without locks: this.messages.add() then this.queue("process") can’t race another webhook for the same customer. See Execution model.

Think in the background

Inbound RPC methods run under a 30-second wall-clock budget — fine for receive(), tight for an LLM round-trip plus tool calls. That’s why the quickstart acks the webhook and defers the thinking:
Queued and scheduled tasks fire inside the actor’s alarm() handler, which has a larger budget — on the order of minutes — and retries on failure. Put LLM calls, tool use, and outbound API work there.

Everything durable survives restarts

History, state, and pending tasks live in the actor’s durable storage, not in memory. If the actor is evicted, crashes, or the pod restarts, a pending task’s timer re-fires after recovery, and the scheduler re-arms to the earliest pending task whenever the actor wakes. A 24-hour follow-up scheduled today fires tomorrow no matter what happens in between. See Lifecycle.

Build

Message History

Source: https://developers.telnyx.com/docs/agent-sdk/message-history.md
this.messages is a persistent, ordered log of AgentMessage objects stored in the actor’s KV. It is per-actor — each idFromName("user-123") gets its own independent history.

Framework adapters

this.messages can convert history to the format expected by popular LLM SDKs:
toAnthropic() omits system messages: the Anthropic Messages API takes the system prompt as a top-level system parameter, not as a conversation message. Pass it on the request yourself. The toOpenAI() payload feeds Telnyx Inference directly — env.TELNYX.ai.openai.chat.createCompletion({ model, messages }) on the pre-authenticated Telnyx API binding, or any OpenAI-compatible endpoint over HTTP. See Calling LLMs for both wiring patterns.

Message shape


Scheduled Tasks

Source: https://developers.telnyx.com/docs/agent-sdk/scheduled-tasks.md
this.queue(), this.schedule(), and this.every() let you defer work durably. Tasks survive pod restarts — the scheduler uses the actor’s built-in alarm mechanism. A task’s name must match a method on your class.

Task dispatch

When a task fires, the platform calls the method by name on your actor instance:

Retries

Tasks retry up to 5 times by default on failure. Configure with maxRetries:

Durable State

Source: https://developers.telnyx.com/docs/agent-sdk/state.md
this.setState() merges a patch into the actor’s current state. Use it for small structured values you want to read quickly — the current conversation state, user preferences, last-seen timestamp.
setState is a recursive merge (RFC 7396) — top-level keys you pass are merged in, nested objects are deep-merged, and null deletes a key. replaceState replaces the whole state object. State is one value, read whole. The moment data turns relational or queryable — orders, events, anything you’d filter or aggregate — put it in the actor’s embedded SQL database instead.

SQL Storage

Source: https://developers.telnyx.com/docs/agent-sdk/sql.md
Agent extends StatefulActor, and every actor carries a private, durable SQLite database at this.ctx.storage.sql. There is nothing to declare in telnyx.toml — the database is created on first use, and it works inside an agent exactly as it does on a plain actor.

Which tier holds the data?

An agent has three durable tiers, all in the same actor: State is read and written as one value; history is read back in order. The moment you want “the last five orders over $10” — a lookup neither tier answers without a scan — put rows in SQL.

Using it from an agent

exec() is synchronous — the database is a local file in the actor’s own process — with positional ? binds and a cursor you drain with toArray():
Everything the actor surface guarantees applies unchanged: the database is private to this one agent instance — per conversation, if you key actors by conversation — reads always reflect your prior writes, and serialized turns mean no other call interleaves mid-write. Wrap related writes in this.ctx.storage.transactionSync(() => { ... }) to commit them atomically. Patterns that come up in agents:
  • Webhook dedup. INSERT the event id into a table with a UNIQUE constraint before queue()-ing work; a thrown constraint violation means you already handled that event.
  • Tool-call ledger. Record each tool invocation and result as a row; answer “what did you do?” or audit questions with a query instead of replaying history.
  • Searchable history. this.messages is an ordered log, not an index. If the agent needs keyword lookup over past conversation, append each message to an SQL table too — in the same method that calls messages.add() — and query it with LIKE plus an index.

Semantics and limits

The full contract — cursor rules, multi-statement batches, transactionSync, binding types, and the limits (1 GB per actor database, 2 MiB per bound value, integer range) — is on the actor SQL guide. It applies verbatim inside an Agent: the SDK reserves alarm() for its scheduler, but all of ctx.storage stays yours. For data that more than one function or caller must query — shared across agents, or read from the CLI and REST API — use a standalone SQL Database instead; the embedded database is strictly per-instance.

WebSockets

Source: https://developers.telnyx.com/docs/agent-sdk/websockets.md
An Agent can terminate WebSockets today. The capability comes from StatefulActor: define one method, webSocket(), and the agent owns the live socket — frames dispatch one at a time, serialized with RPCs and tasks like every other call, with history, state, and timers right there in the handler. Actor WebSocket support is in beta — the platform guide carries the full contract and current caveats.

The shape

A function terminates the handshake — authenticate once, pick the agent, hand off — and the agent handles every frame after that:
The frame handler follows the same rule as the webhook path in the Quickstart: append to history, queue() the LLM turn, return. A message handler runs under the 30-second method budget, and while it runs every other frame, RPC, and task on this agent waits — so respond() does the slow thinking in a task, then pushes the reply to every open socket with this.ctx.broadcast().

Pushing without an inbound frame

Tasks run inside the agent, so a timer can push. Use the scheduler — not raw alarms, which the SDK reserves for the task queue:

What to know before shipping

  • Connections are capped at about five minutes today, measured from the handshake, even on a socket actively exchanging frames — surfacing as an abnormal close (code 1006). The cap will be raised in a future update. Reconnecting is the client’s job: back off and reopen the same name. idFromName routes the new socket to the same agent, where history, state, and pending tasks all survived — only the socket is new.
  • Sends aren’t held for durability. ws.send() and broadcast() are immediate; a handler that sends and then throws leaves the client holding a frame about state that never committed. Frames that carry the message log’s seq let a client detect gaps and re-derive on reconnect.
  • Bound per-frame work. Frames that arrive while a handler runs queue up to 256 events or 1 MiB; overflow closes the socket with 1013. Batch chatty clients into fewer, larger frames.
Close codes, reconnect strategy, and the full contract: actor WebSockets and Connection Lifecycle.

What’s in development

This page is the platform surface, and it stays. In development on top of it: AgentClient, a browser SDK — browser clients connecting straight to an agent, automatic reconnect, and state sync driven by onStateChanged — with onConnect() on the Agent class as the seam it lands on. Until then, webSocket() is the supported path.

Examples

Roll Your Own Agent

Source: https://developers.telnyx.com/docs/agent-sdk/examples/roll-your-own.md
No framework — your process() method is the agent loop. The Agent SDK supplies the durable parts (history, retries, the follow-up timer); the LLM is one call on the pre-authenticated Telnyx API binding, which speaks the this.messages.toOpenAI() payload directly — no API key to manage. Prefer another provider? Any OpenAI-compatible endpoint works over fetch, with its key in a secret. Each customer gets their own Conversation actor, keyed by phone number. src/conversation.ts — the actor:
src/index.ts — the function that routes inbound webhooks to the right actor:
Verify Telnyx webhook signatures before processing — see receiving webhooks. The examples above omit verification for brevity; production code must check the telnyx-signature-ed25519 header. telnyx.toml:
That’s the whole deployment — the [telnyx] binding carries auth, so there is no API key to provision. Prefer a framework running the loop instead? See the LangGraph version of this same agent. Task delivery is at-least-once: a crash after messages.add() or messages.send() succeeds retries the whole process() method. For production, guard outbound side effects — e.g. check state before sending, or use a stable message ID to deduplicate.

LangGraph Agent

Source: https://developers.telnyx.com/docs/agent-sdk/examples/langgraph.md
Here an agent framework does the thinking. LangGraph’s ReAct agent runs inside the actor’s process() method — with tool calling, multi-step reasoning, the works — while the Agent SDK supplies what the framework doesn’t have: durable per-customer history, retries, and the follow-up timer. this.messages.toLangChain() returns plain { role, content } messages, which LangGraph accepts as-is. The LLM is ChatOpenAI pointed at Telnyx Inference — swap baseURL, key, and model to bring any OpenAI-compatible provider. The same pattern fits any agent framework that runs on Node. Install the framework alongside the runtime:
src/conversation.ts — the actor:
Only the framework’s final reply lands in this.messages — intermediate tool calls and tool results stay inside the LangGraph run. If you want them in the durable history too, append() them from out.messages. src/index.ts — the function that routes inbound webhooks to the right actor:
Verify Telnyx webhook signatures before processing — see receiving webhooks. The examples above omit verification for brevity; production code must check the telnyx-signature-ed25519 header. telnyx.toml:
Set the API key as a secret — it reaches the actor as process.env.TELNYX_API_KEY:
Prefer owning the loop yourself? The hand-rolled version of this same agent needs no key at all — it calls inference through the pre-authenticated Telnyx binding. A framework owns its own HTTP stack, so it authenticates like any external client. Task delivery is at-least-once: a crash after messages.add() or messages.send() succeeds retries the whole process() method. For production, guard outbound side effects — e.g. check state before sending, or use a stable message ID to deduplicate.

Reference

Overview

Source: https://developers.telnyx.com/docs/agent-sdk/api-reference.md
Everything the Agent SDK adds lives on the Agent base class, imported from @telnyx/edge-runtime:
The surface splits into four areas: Two rules apply across the whole surface:
  • Every member is protected — this is an inside-the-class API, called from your own methods. It is not part of the RPC surface your subclass exposes to stubs.
  • alarm() is claimed by the SDK. The task scheduler runs on the actor’s single alarm slot. Do not override alarm() in an Agent subclass — schedule a task instead. (On a plain StatefulActor, alarm() remains yours — see Alarms.)
Everything from StatefulActor is still there — this.ctx, this.env, RPC dispatch, fetch() — see the StatefulActor Runtime API.

Agent Class

Source: https://developers.telnyx.com/docs/agent-sdk/api-reference/agent.md
Agent<E, State> extends StatefulActor<E>. Subclass it, declare async methods for your inbound surface, and use the protected members inside them.

Type parameters

Construction

You don’t construct agents yourself — the runtime does. On every activation the base constructor also re-arms the task scheduler to the earliest pending task (inside ctx.blockConcurrencyWhile), which is what makes timers survive crashes and restarts. If you need your own one-shot init, override the constructor and call super(ctx, env) first, exactly as with a StatefulActor.

Properties

Plus everything inherited: this.ctx, this.env.

Methods

State and scheduling methods are listed with full semantics on their own pages:

Override hooks

Reserved: alarm()

The SDK claims the actor’s alarm slot to drive the task scheduler: when the alarm fires, Agent.alarm() drains every due task, dispatches each to the method it names, and re-arms to the next deadline. Overriding alarm() in an Agent subclass breaks queue/schedule/every. If you need timed work, schedule a task.

Message Log

Source: https://developers.telnyx.com/docs/agent-sdk/api-reference/message-log.md
this.messages is the actor’s durable conversation log. Messages are ordered by an assigned monotonic seq (insertion order, not wall-clock), and the log is append-only — there is no delete.

Types

Writing

Reading

all() reads the entire history every time — on a long-lived conversation prefer last(n) for a bounded context window.

Adapters

Each adapter reads the full history and converts it. They differ in how they treat system and tool messages:
If you record tool calls in history and hand it to LangChain, note the drop: persist what the framework needs via append() with plain roles, or rebuild framework state from toOpenAI() output instead.

Scheduling

Source: https://developers.telnyx.com/docs/agent-sdk/api-reference/scheduling.md
Tasks are durable named timers. Each task names a method on your class; when the timer fires, the SDK calls that method with the task’s payload. All of it rides the actor’s single alarm slot, re-armed to the earliest pending deadline.

Creating tasks

All three return Promise<string> — the task id.

ScheduleOptions

With a stable id, scheduling is an upsert — the prior task with that id is replaced and the timer re-armed. Without one, every call creates a new task under a random id.

Dispatch

  • A due task calls this[task.name](task.payload) — one argument, the payload.
  • If no such method exists, the fallback fires instead: onTask(name, payload, { attempt }).
  • Task methods are ordinary methods. A method that should be schedulable but not RPC-callable from a stub can be named with a leading _ — the runtime excludes _-names from RPC, but the scheduler still dispatches to them.

Failure and retries

  • A task that throws is retried with exponential backoff (starting around a second, capped at 5 minutes), up to maxRetries times after the first delivery — 6 runs total by default.
  • A task that exhausts its retries is parked: deleted without firing again, and there is no callback when that happens.
  • A recurring (every) task resets its attempt count after each successful run, and schedules its next fire from the start of the drain turn (the timestamp captured before dispatch), not from when the method returns.

Delivery contract

Delivery is at-least-once: a crash after your method runs but before the task is marked done re-runs it on the next activation. Write task handlers to be idempotent — the same rule as alarm handlers. Payloads are stored in the actor’s durable storage and must be codec-safe — JSON-native values plus Date, Map, Set, ArrayBuffer/TypedArray, BigInt, RegExp. Functions, class instances, or circular references throw a CodecError — see Errors.

Inspecting and cancelling


State

Source: https://developers.telnyx.com/docs/agent-sdk/api-reference/state.md
Agent state is one durable value of your State type. setState merges, which is what you want for the common case — update two fields without re-writing the rest.

Methods

  • The merge is recursive (RFC 7396) — top-level keys are merged in, nested objects are deep-merged, and null deletes a key.
  • Values must be codec-safe, like all actor storage — see Errors for CodecError.

Hooks

initialState(): State

The default for a fresh actor. Called whenever state is read and nothing has been stored yet; the default implementation returns {}. Reading alone doesn’t persist it — the first write does.

onStateChanged(next, prev)

Fires after every setState() resolves, with the merged state and the state before the patch. Override it to react to changes — mirror to an external system, log transitions, invalidate caches. replaceState() does fire this hook — the next argument is the new state and prev is the state before replacement.

Limits

Source: https://developers.telnyx.com/docs/agent-sdk/limits.md

Execution budgets

Do LLM and outbound API work in tasks, not inbound methods — see How Agents Run. Full platform numbers live in the StatefulActor API reference.

Task retries

A task that throws retries up to its maxRetries (default 5) with exponential backoff, then is parked — it stops retrying and no longer fires. every() tasks reset their attempt count after each successful run.

Storage

Message history and state live in the actor’s durable storage and are subject to its key/value size caps — see the storage reference. Message history is append-only — there is no delete or trim API. last(n) reads a bounded window, but writes accumulate forever. For relational or queryable data, use the actor’s embedded SQL database instead — up to 1 GB per agent.

What’s shipped vs. in development

Shipped today:
  • Agent base class with message history, scheduled tasks, and merge-patch state
  • WebSocket termination via webSocket() — see WebSockets
  • BlobStore for blob access within actors
In development:
  • AgentClient browser SDK — browser clients connecting straight to an agent, with automatic reconnect and state sync on top of onStateChanged. onConnect() is the seam it lands on.
The Agent API surface is Beta and may change as pieces land.