> ## Documentation Index
> Fetch the complete documentation index at: https://developers.telnyx.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Agent SDK (Beta) llms-full.txt

> Complete machine-readable documentation content for Agent SDK (Beta) (AI) for AI agents and LLMs

# 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](https://developers.telnyx.com)).
> This file: [https://developers.telnyx.com/development/llms/ai-agent-sdk-beta-llms-full-txt.md](https://developers.telnyx.com/development/llms/ai-agent-sdk-beta-llms-full-txt.md) · Root index: [https://developers.telnyx.com/llms.txt](https://developers.telnyx.com/llms.txt)

## Get Started

### Overview

> Source: [https://developers.telnyx.com/docs/agent-sdk.md](https://developers.telnyx.com/docs/agent-sdk.md)

The **Agent SDK** is a TypeScript base class, `Agent`, that extends
[`StatefulActor`](/docs/edge-compute/stateful-actors) 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.

```ts theme={null}
import { Agent } from "@telnyx/edge-runtime";
```

The `Agent` base class is in **Beta**. The API surface may change.

## What Agent adds to StatefulActor

| Primitive                                              | API                                               | What it does                                |
| ------------------------------------------------------ | ------------------------------------------------- | ------------------------------------------- |
| [**Message History**](/docs/agent-sdk/message-history) | `this.messages`                                   | Ordered, durable conversation log per actor |
| [**Scheduled Tasks**](/docs/agent-sdk/scheduled-tasks) | `this.schedule()`, `this.queue()`, `this.every()` | Named timers that survive restarts          |
| [**Durable State**](/docs/agent-sdk/state)             | `this.setState()`, `this.getState()`              | Merge-patch state on a single KV key        |

State is durable and survives restarts — the same persistence guarantee that
StatefulActors provide. Everything `StatefulActor` provides is inherited too,
including the embedded [SQL database](/docs/agent-sdk/sql) and
[WebSocket termination](/docs/agent-sdk/websockets).

## 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](/docs/edge-compute/telnyx-api) — no API key to
  manage — and `toOpenAI()` produces exactly the payload it takes.
  → [Roll Your Own Agent](/docs/agent-sdk/examples/roll-your-own)
* **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](/docs/agent-sdk/examples/langgraph)

Either way you bring your own LLM: [Telnyx Inference](/docs/inference/getting-started)
is wired in through the binding, and any OpenAI-compatible provider is one `baseURL`
away. See [Calling LLMs](/docs/agent-sdk/concepts/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](/docs/agent-sdk/quickstart) — a working agent in one file
* [How Agents Run](/docs/agent-sdk/concepts/how-agents-run) — the actor execution model in four properties
* [Calling LLMs](/docs/agent-sdk/concepts/calling-llms) — both wiring patterns, side by side
* [API reference](/docs/agent-sdk/api-reference) — every method and override hook

***

### Quickstart

> Source: [https://developers.telnyx.com/docs/agent-sdk/quickstart.md](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.

```ts theme={null}
import { Agent } from "@telnyx/edge-runtime";

export class SupportAgent extends Agent<Env, { from: string; lastReply: string; at: number }> {
  // Called when a new message arrives (from your function's fetch handler)
  async receive(text: string, from: string): Promise<void> {
    await this.setState({ from });
    await this.messages.add("user", text);  // persisted to history
    await this.queue("process");             // durable background task
  }

  // Runs asynchronously — webhook acks immediately, LLM runs in the background
  async process(): Promise<void> {
    const history = await this.messages.toLangChain();  // or toOpenAI(), toAnthropic()
    // ... call your LLM here ...
    const reply = "Hello! How can I help?";
    await this.messages.add("assistant", reply);
    await this.setState({ lastReply: reply, at: Date.now() });
    await this.schedule(86_400, "nudge", null, { id: "nudge" }); // follow up in 24h
  }

  async nudge(): Promise<void> {
    const last = await this.messages.last();
    if (last?.role !== "assistant") return; // customer replied — skip
    // customer hasn't replied — send a follow-up
  }
}

// The function that routes inbound webhooks to the right actor
export default {
  async fetch(req: Request, env: Env): Promise<Response> {
    const { text, from } = await req.json() as { text: string; from: string };
    await env.SUPPORT.idFromName(from).receive(text, from);
    return new Response("ok");
  },
};
```

`Env` is a global type generated by `telnyx-edge types` from your `telnyx.toml` — it
includes `SUPPORT: ActorNamespace&lt;SupportAgent>` and any other bindings you declare.
Run `telnyx-edge types` before `tsc` or `ship` to keep it in sync.

**`telnyx.toml`:**

```toml theme={null}
name = "support-agent"
main = "src/index.ts"
compatibility_date = "2026-05-01"

[[actors]]
binding = "SUPPORT"
type    = "SupportAgent"
```

## Deploy

Generate binding types, then ship:

```bash theme={null}
telnyx-edge types   # generates telnyx-env.d.ts from your telnyx.toml
telnyx-edge ship     # bundles, uploads, and deploys to the edge
```

Your function is live at `https://&lt;name>-&lt;id>.telnyxcompute.com` — see
[edge compute configuration](/docs/edge-compute/configuration) for the full reference.

## Next steps

* [Calling LLMs](/docs/agent-sdk/concepts/calling-llms) — wire in a real model, with or without a framework
* [Message History](/docs/agent-sdk/message-history) — the `this.messages` API and framework adapters
* [Scheduled Tasks](/docs/agent-sdk/scheduled-tasks) — `queue`, `schedule`, `every`, and retries
* [Durable State](/docs/agent-sdk/state) — `setState`, `getState`, and typed state
* [Roll Your Own Agent](/docs/agent-sdk/examples/roll-your-own) / [LangGraph Agent](/docs/agent-sdk/examples/langgraph) — two complete examples

***

## Concepts

### How Agents Run

> Source: [https://developers.telnyx.com/docs/agent-sdk/concepts/how-agents-run.md](https://developers.telnyx.com/docs/agent-sdk/concepts/how-agents-run.md)

An `Agent` is a [StatefulActor](/docs/edge-compute/stateful-actors) — 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](/docs/edge-compute/stateful-actors/concepts/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](/docs/edge-compute/stateful-actors/concepts/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:

```ts theme={null}
async receive(text: string, from: string): Promise<void> {
  await this.messages.add("user", text);
  await this.queue("process"); // returns immediately
}
```

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](/docs/agent-sdk/scheduled-tasks). 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](/docs/edge-compute/stateful-actors/concepts/lifecycle).

***

## Build

### Message History

> Source: [https://developers.telnyx.com/docs/agent-sdk/message-history.md](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.

```ts theme={null}
// Append a message
await this.messages.add("user", "What's my account balance?");
await this.messages.add("assistant", "Your balance is $42.");

// Read history
const all = await this.messages.all();       // all messages, oldest first
const last = await this.messages.last();     // most recent message
const recent = await this.messages.last(10); // last 10 messages
```

## Framework adapters

`this.messages` can convert history to the format expected by popular LLM SDKs:

```ts theme={null}
// LangChain / LangGraph
const msgs = await this.messages.toLangChain();
await agent.invoke({ messages: msgs });

// OpenAI Chat Completions
const msgs = await this.messages.toOpenAI();
await openai.chat.completions.create({ model: "gpt-4o", messages: msgs });

// Anthropic Messages
const msgs = await this.messages.toAnthropic();
await anthropic.messages.create({ model: "claude-opus-4-5", max_tokens: 1024, messages: msgs });
```

`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(&#123; model, messages &#125;)` on the
pre-authenticated [Telnyx API binding](/docs/edge-compute/telnyx-api), or any
OpenAI-compatible endpoint over HTTP. See
[Calling LLMs](/docs/agent-sdk/concepts/calling-llms) for both wiring patterns.

## Message shape

```ts theme={null}
interface AgentMessage {
  role: "system" | "user" | "assistant" | "tool";
  content: string;
  name?: string;          // tool name for role:"tool", speaker label otherwise
  toolCalls?: ToolCall[]; // present on assistant turns that request tools
  toolCallId?: string;    // links a tool result back to a ToolCall
}
```

***

### Scheduled Tasks

> Source: [https://developers.telnyx.com/docs/agent-sdk/scheduled-tasks.md](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.

```ts theme={null}
// Run immediately (next alarm tick)
await this.queue("process");

// Run after a delay (in seconds)
await this.schedule(3600, "sendReminder");

// Run repeatedly (interval in seconds)
await this.every(300, "checkStatus");

// With a stable id — re-scheduling replaces the prior task (dedup)
await this.schedule(86_400, "nudge", null, { id: "daily-nudge" });

// Cancel a named task
await this.cancelSchedule("daily-nudge");

// List pending tasks
const tasks = await this.listSchedules();
```

## Task dispatch

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

```ts theme={null}
export class MyAgent extends Agent {
  async sendReminder(): Promise<void> {
    // runs when the scheduled timer fires
  }

  // Fallback for tasks whose method name doesn't exist on the class
  protected override async onTask(name: string, payload: unknown): Promise<void> {
    console.error(`Unknown task: ${name}`);
  }
}
```

## Retries

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

```ts theme={null}
await this.queue("process", data, { maxRetries: 3 });
```

***

### Durable State

> Source: [https://developers.telnyx.com/docs/agent-sdk/state.md](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.

```ts theme={null}
export interface MyState extends Record<string, unknown> {
  status: "idle" | "processing" | "waiting";
  lastReply: string;
  at: number;
}

export class MyAgent extends Agent<RuntimeEnv, MyState> {
  // Default state for a new actor
  protected override initialState(): MyState {
    return { status: "idle", lastReply: "", at: 0 };
  }

  async process(): Promise<void> {
    await this.setState({ status: "processing" });
    // ... do work ...
    await this.setState({ status: "idle", lastReply: "Done.", at: Date.now() });
  }

  async report(): Promise<MyState> {
    return this.getState();
  }
}
```

`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](/docs/agent-sdk/sql) instead.

***

### SQL Storage

> Source: [https://developers.telnyx.com/docs/agent-sdk/sql.md](https://developers.telnyx.com/docs/agent-sdk/sql.md)

`Agent` extends [`StatefulActor`](/docs/edge-compute/stateful-actors), 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:

| Tier                                               | API                         | Shape                          | Reach for it when                                                           |
| -------------------------------------------------- | --------------------------- | ------------------------------ | --------------------------------------------------------------------------- |
| [State](/docs/agent-sdk/state)                     | `getState()` / `setState()` | one small value, merge-patched | current status, preferences, last-seen — read as a whole every turn         |
| [Message history](/docs/agent-sdk/message-history) | `this.messages`             | append-only conversation log   | what was said, in order, feeding the LLM                                    |
| **SQL**                                            | `this.ctx.storage.sql`      | tables, indexes, aggregates    | anything you'd `WHERE`, `GROUP BY`, or index — orders, events, tool results |

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()`:

```ts theme={null}
import { Agent } from "@telnyx/edge-runtime";

export class Support extends Agent {
  async recordOrder(sku: string, cents: number): Promise<void> {
    this.ctx.storage.sql.exec(
      `CREATE TABLE IF NOT EXISTS orders(sku TEXT, cents INTEGER, at INTEGER)`,
    );
    this.ctx.storage.sql.exec(
      `INSERT INTO orders(sku, cents, at) VALUES (?, ?, ?)`,
      sku,
      cents,
      Date.now(),
    );
  }

  // An answer the LLM can use as tool output — no scan, no external database
  async topSkus(): Promise<Array<{ sku: string; total: number }>> {
    return this.ctx.storage.sql
      .exec<{ sku: string; total: number }>(
        `SELECT sku, SUM(cents) AS total FROM orders
         GROUP BY sku ORDER BY total DESC LIMIT 5`,
      )
      .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](/docs/agent-sdk/concepts/how-agents-run) mean no other call interleaves
mid-write. Wrap related writes in `this.ctx.storage.transactionSync(() => &#123; ... &#125;)` 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](/docs/edge-compute/stateful-actors/guides/storage/sql). It
applies verbatim inside an `Agent`: the SDK reserves
[`alarm()`](/docs/agent-sdk/api-reference/agent) 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](/docs/edge-compute/sqldb) instead; the embedded database is strictly
per-instance.

***

### WebSockets

> Source: [https://developers.telnyx.com/docs/agent-sdk/websockets.md](https://developers.telnyx.com/docs/agent-sdk/websockets.md)

An `Agent` can terminate WebSockets **today**. The capability comes from
[`StatefulActor`](/docs/edge-compute/stateful-actors/websockets): 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:

```ts theme={null}
import { Agent } from "@telnyx/edge-runtime";
import type { ActorNamespace } from "@telnyx/edge-runtime";
import type { WebSocket } from "ws"; // transitive dependency of @telnyx/edge-runtime

export class Assistant extends Agent {
  // req carries whatever headers your front door stamped at the handshake
  async webSocket(ws: WebSocket, req: Request): Promise<void> {
    ws.on("message", async (data, isBinary) => {
      if (isBinary) return;
      await this.messages.add("user", String(data));
      await this.queue("respond"); // think in the background
    });
  }

  async respond(): Promise<void> {
    const history = await this.messages.toOpenAI();
    // ... call your LLM here — see Calling LLMs ...
    const reply = "On it.";
    await this.messages.add("assistant", reply);
    // Fan out to every socket open on this agent
    this.ctx.broadcast(JSON.stringify({ role: "assistant", text: reply }));
  }
}

interface Env {
  ASSISTANT: ActorNamespace;
}

export default {
  async fetch(req: Request, env: Env): Promise<Response> {
    if (req.headers.get("Upgrade")?.toLowerCase() !== "websocket") {
      return new Response("expected websocket", { status: 426 });
    }
    // Authenticate here — this runs once per connection. Stamp what you learn
    // as headers for the actor; the actor WebSockets guide shows the pattern.
    // ⚠️ Derive the actor name from a verified credential (JWT, session cookie),
    // not a caller-controlled query param. This example uses ?user= for brevity.
    const user = new URL(req.url).searchParams.get("user");
    if (!user) return new Response(null, { status: 401 });
    return env.ASSISTANT.idFromName(user).fetch(req);
  },
};
```

The frame handler follows the same rule as the webhook path in the
[Quickstart](/docs/agent-sdk/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](/docs/agent-sdk/api-reference/agent) for the task
queue:

```ts theme={null}
async webSocket(ws: WebSocket, req: Request): Promise<void> {
  // First socket arms the loop; the stable id makes re-arming an upsert
  await this.every(30, "tick", null, { id: "tick" });
}

async tick(): Promise<void> {
  if (this.ctx.count() === 0) {
    await this.cancelSchedule("tick"); // no listeners — stand down
    return;
  }
  this.ctx.broadcast(JSON.stringify({ type: "status", ...(await this.getState()) }));
}
```

## 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](/docs/edge-compute/stateful-actors/websockets) and
[Connection Lifecycle](/docs/edge-compute/stateful-actors/websockets/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`](/docs/agent-sdk/api-reference/state) — 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](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](/docs/edge-compute/telnyx-api), 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](/docs/edge-compute/configuration/secrets).

Each customer gets their own `Conversation` actor, keyed by phone number.

**`src/conversation.ts`** — the actor:

```ts theme={null}
import { Agent } from "@telnyx/edge-runtime";

const SYSTEM_PROMPT =
  "You are a terse, helpful SMS support agent. Answer in one or two sentences.";

// Minimal hand-typed slice of the binding — `telnyx-edge types` generates the full types
interface ConvEnv {
  TELNYX: {
    messages: {
      send(m: { from: string; to: string; text: string }): Promise<unknown>;
    };
    ai: {
      openai: {
        chat: {
          createCompletion(req: {
            model: string;
            messages: Array<{ role: string; content: string }>;
          }): Promise<{ choices: Array<{ message: { content: string } }> }>;
        };
      };
    };
  };
}

interface ConvState extends Record<string, unknown> {
  from: string;
  to: string;
  at: number;
}

export class Conversation extends Agent<ConvEnv, ConvState> {
  protected override initialState(): ConvState {
    return { from: "", to: "", at: 0 };
  }

  async receive({ text, from, to }: { text: string; from: string; to: string }): Promise<void> {
    await this.setState({ from, to });
    await this.messages.add("user", text);
    await this.queue("process"); // ack the webhook now, think in the background
  }

  async process(): Promise<void> {
    // toOpenAI() emits Chat Completions format — the binding takes it directly
    const history = await this.messages.toOpenAI();

    // Pre-authenticated Telnyx client: no base URL, no key. An API error
    // rejects, which marks the task failed — the scheduler retries with backoff.
    const res = await this.env.TELNYX.ai.openai.chat.createCompletion({
      model: "zai-org/GLM-5.2",
      messages: [{ role: "system", content: SYSTEM_PROMPT }, ...history],
    });
    const reply = res.choices[0].message.content;

    await this.messages.add("assistant", reply);
    const { from, to } = await this.getState();
    await this.env.TELNYX.messages.send({ from: to, to: from, text: reply });
    await this.setState({ at: Date.now() });

    // Follow up in 24 hours if the customer goes quiet
    await this.schedule(86_400, "nudge", null, { id: "nudge" });
  }

  async nudge(): Promise<void> {
    const last = await this.messages.last();
    if (last?.role !== "assistant") return; // customer replied — skip
    const { from, to } = await this.getState();
    await this.env.TELNYX.messages.send({
      from: to,
      to: from,
      text: "Just checking in — did that sort things out?",
    });
  }
}
```

**`src/index.ts`** — the function that routes inbound webhooks to the right actor:

```ts theme={null}
import type { ActorNamespace } from "@telnyx/edge-runtime";
import { Conversation } from "./conversation.js";

export { Conversation };

interface Env {
  CONVOS: ActorNamespace<Conversation>;
}

export default {
  async fetch(req: Request, env: Env): Promise<Response> {
    if (new URL(req.url).pathname.startsWith("/health")) return new Response("ok");
    const hook = await req.json() as {
      data: { event_type: string; payload: { from: { phone_number: string }; to: Array<{ phone_number: string }>; text: string } };
    };
    if (hook.data.event_type !== "message.received") return new Response("ignored");
    const { from, to, text } = {
      from: hook.data.payload.from.phone_number,
      to: hook.data.payload.to[0]?.phone_number ?? "",
      text: hook.data.payload.text,
    };
    await env.CONVOS.idFromName(from).receive({ text, from, to });
    return new Response("ok");
  },
};
```

Verify Telnyx webhook signatures before processing — see
[receiving webhooks](/development/api-fundamentals/webhooks/receiving-webhooks).
The examples above omit verification for brevity; production code must check the
`telnyx-signature-ed25519` header.

**`telnyx.toml`:**

```toml theme={null}
name = "support-agent"
main = "src/index.ts"
compatibility_date = "2026-05-01"

[[actors]]
binding = "CONVOS"
type    = "Conversation"

[telnyx]
binding = "TELNYX"
```

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](/docs/agent-sdk/examples/langgraph) of this same agent.

Task delivery is [at-least-once](/docs/agent-sdk/api-reference/scheduling): 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](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 `&#123; role, content &#125;` 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:

```bash theme={null}
npm install @langchain/openai @langchain/langgraph @langchain/core zod
```

**`src/conversation.ts`** — the actor:

```ts theme={null}
import { Agent } from "@telnyx/edge-runtime";
import { ChatOpenAI } from "@langchain/openai";
import { createReactAgent } from "@langchain/langgraph/prebuilt";
import { tool } from "@langchain/core/tools";
import { z } from "zod";

const SYSTEM_PROMPT =
  "You are a terse, helpful SMS support agent. Answer in one or two sentences.";

const llm = new ChatOpenAI({
  model: "zai-org/GLM-5.2",
  apiKey: process.env.TELNYX_API_KEY,
  configuration: { baseURL: "https://api.telnyx.com/v2/ai/openai" },
});

const lookupOrder = tool(
  async ({ orderId }) => {
    // ...query your order system...
    return JSON.stringify({ orderId, status: "shipped", eta: "Friday" });
  },
  {
    name: "lookup_order",
    description: "Look up the status of a customer order by id.",
    schema: z.object({ orderId: z.string() }),
  },
);

const supportAgent = createReactAgent({ llm, tools: [lookupOrder] });

interface ConvEnv {
  TELNYX: {
    messages: {
      send(m: { from: string; to: string; text: string }): Promise<unknown>;
    };
  };
}

interface ConvState extends Record<string, unknown> {
  from: string;
  to: string;
  at: number;
}

export class Conversation extends Agent<ConvEnv, ConvState> {
  protected override initialState(): ConvState {
    return { from: "", to: "", at: 0 };
  }

  async receive({ text, from, to }: { text: string; from: string; to: string }): Promise<void> {
    await this.setState({ from, to });
    await this.messages.add("user", text);
    await this.queue("process"); // ack the webhook now, think in the background
  }

  async process(): Promise<void> {
    // toLangChain() emits plain {role, content} — LangGraph takes it as-is
    const history = await this.messages.toLangChain();

    const out = await supportAgent.invoke({
      messages: [{ role: "system", content: SYSTEM_PROMPT }, ...history],
    });
    const reply = String(out.messages.at(-1)?.content ?? "");
    // Throwing before this point marks the task failed — the scheduler retries

    await this.messages.add("assistant", reply);
    const { from, to } = await this.getState();
    await this.env.TELNYX.messages.send({ from: to, to: from, text: reply });
    await this.setState({ at: Date.now() });

    // Follow up in 24 hours if the customer goes quiet
    await this.schedule(86_400, "nudge", null, { id: "nudge" });
  }

  async nudge(): Promise<void> {
    const last = await this.messages.last();
    if (last?.role !== "assistant") return; // customer replied — skip
    const { from, to } = await this.getState();
    await this.env.TELNYX.messages.send({
      from: to,
      to: from,
      text: "Just checking in — did that sort things out?",
    });
  }
}
```

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:

```ts theme={null}
import type { ActorNamespace } from "@telnyx/edge-runtime";
import { Conversation } from "./conversation.js";

export { Conversation };

interface Env {
  CONVOS: ActorNamespace<Conversation>;
}

export default {
  async fetch(req: Request, env: Env): Promise<Response> {
    if (new URL(req.url).pathname.startsWith("/health")) return new Response("ok");
    const hook = await req.json() as {
      data: { event_type: string; payload: { from: { phone_number: string }; to: Array<{ phone_number: string }>; text: string } };
    };
    if (hook.data.event_type !== "message.received") return new Response("ignored");
    const { from, to, text } = {
      from: hook.data.payload.from.phone_number,
      to: hook.data.payload.to[0]?.phone_number ?? "",
      text: hook.data.payload.text,
    };
    await env.CONVOS.idFromName(from).receive({ text, from, to });
    return new Response("ok");
  },
};
```

Verify Telnyx webhook signatures before processing — see
[receiving webhooks](/development/api-fundamentals/webhooks/receiving-webhooks).
The examples above omit verification for brevity; production code must check the
`telnyx-signature-ed25519` header.

**`telnyx.toml`:**

```toml theme={null}
name = "support-agent"
main = "src/index.ts"
compatibility_date = "2026-05-01"

[[actors]]
binding = "CONVOS"
type    = "Conversation"

[telnyx]
binding = "TELNYX"
```

**Set the API key** as a [secret](/docs/edge-compute/configuration/secrets) — it reaches
the actor as `process.env.TELNYX_API_KEY`:

```bash theme={null}
telnyx-edge secrets add TELNYX_API_KEY "KEY..."
```

Prefer owning the loop yourself? The
[hand-rolled version](/docs/agent-sdk/examples/roll-your-own) 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](/docs/agent-sdk/api-reference/scheduling): 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](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`:

```ts theme={null}
import { Agent } from "@telnyx/edge-runtime";
```

The surface splits into four areas:

| Area                  | Entry points                                                                                       | Reference                                                |
| --------------------- | -------------------------------------------------------------------------------------------------- | -------------------------------------------------------- |
| The base class itself | type parameters, hooks, reserved methods                                                           | [Agent Class](/docs/agent-sdk/api-reference/agent)       |
| Conversation history  | `this.messages.*`                                                                                  | [Message Log](/docs/agent-sdk/api-reference/message-log) |
| Durable timers        | `this.queue()`, `this.schedule()`, `this.every()`, `this.cancelSchedule()`, `this.listSchedules()` | [Scheduling](/docs/agent-sdk/api-reference/scheduling)   |
| Durable state         | `this.getState()`, `this.setState()`, `this.replaceState()`                                        | [State](/docs/agent-sdk/api-reference/state)             |

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](/docs/agent-sdk/api-reference/scheduling) instead. (On a plain
  `StatefulActor`, `alarm()` remains yours — see
  [Alarms](/docs/edge-compute/stateful-actors/alarms).)

Everything from `StatefulActor` is still there — `this.ctx`, `this.env`, RPC dispatch,
`fetch()` — see the
[StatefulActor Runtime API](/docs/edge-compute/stateful-actors/api-reference).

***

### Agent Class

> Source: [https://developers.telnyx.com/docs/agent-sdk/api-reference/agent.md](https://developers.telnyx.com/docs/agent-sdk/api-reference/agent.md)

`Agent&lt;E, State>` extends `StatefulActor&lt;E>`. Subclass it, declare async methods for
your inbound surface, and use the protected members inside them.

```ts theme={null}
import { Agent } from "@telnyx/edge-runtime";

interface MyState extends Record<string, unknown> {
  status: "idle" | "processing";
}

export class SupportAgent extends Agent<MyEnv, MyState> {
  protected override initialState(): MyState {
    return { status: "idle" };
  }

  async receive(text: string): Promise<void> {
    await this.messages.add("user", text);
    await this.queue("process");
  }
}
```

## Type parameters

| Parameter | Constraint                           | Default                      | Meaning                                             |
| --------- | ------------------------------------ | ---------------------------- | --------------------------------------------------- |
| `E`       | `extends Env`                        | `Env`                        | Your environment/bindings type — becomes `this.env` |
| `State`   | `extends Record&lt;string, unknown>` | `Record&lt;string, unknown>` | Your durable state shape                            |

## 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](/docs/edge-compute/stateful-actors/api-reference/base).

## Properties

| Property        | Type         | Description                                                                             |
| --------------- | ------------ | --------------------------------------------------------------------------------------- |
| `this.messages` | `MessageLog` | The conversation history — see [Message Log](/docs/agent-sdk/api-reference/message-log) |

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

## Methods

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

| Method                                       | Returns                    | Reference                                              |
| -------------------------------------------- | -------------------------- | ------------------------------------------------------ |
| `getState()`                                 | `Promise&lt;State>`        | [State](/docs/agent-sdk/api-reference/state)           |
| `setState(patch)`                            | `Promise&lt;State>`        | [State](/docs/agent-sdk/api-reference/state)           |
| `replaceState(next)`                         | `Promise&lt;State>`        | [State](/docs/agent-sdk/api-reference/state)           |
| `queue(method, payload?, opts?)`             | `Promise&lt;string>`       | [Scheduling](/docs/agent-sdk/api-reference/scheduling) |
| `schedule(seconds, method, payload?, opts?)` | `Promise&lt;string>`       | [Scheduling](/docs/agent-sdk/api-reference/scheduling) |
| `every(seconds, method, payload?, opts?)`    | `Promise&lt;string>`       | [Scheduling](/docs/agent-sdk/api-reference/scheduling) |
| `cancelSchedule(id)`                         | `Promise&lt;boolean>`      | [Scheduling](/docs/agent-sdk/api-reference/scheduling) |
| `listSchedules()`                            | `Promise&lt;TaskRecord[]>` | [Scheduling](/docs/agent-sdk/api-reference/scheduling) |

## Override hooks

| Hook                         | Fires                                                                                                          |
| ---------------------------- | -------------------------------------------------------------------------------------------------------------- |
| `initialState(): State`      | When state is read and nothing has been stored yet — return the default for a fresh actor                      |
| `onStateChanged(next, prev)` | After every `setState()` **and `replaceState()`** resolves, with the new state and the state before the change |
| `onTask(name, payload, ctx)` | When a due task's `name` matches no method on the class — `ctx.attempt` carries the delivery attempt           |
| `onConnect(conn)`            | When a client connects *(the connection layer is coming next — see [Limits](/docs/agent-sdk/limits))*          |
| `now(): number`              | Time source for the scheduler (defaults to `Date.now()`) — override in tests for a deterministic clock         |

## 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](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

```ts theme={null}
interface AgentMessage {
  role: "system" | "user" | "assistant" | "tool";
  content: string;
  name?: string;          // tool name for role:"tool", speaker label otherwise
  toolCalls?: ToolCall[]; // present on assistant turns that request tools
  toolCallId?: string;    // links a tool result back to a ToolCall.id
}

interface ToolCall {
  id: string;
  name: string;
  args: unknown;
}

// As persisted — what reads return:
interface StoredMessage extends AgentMessage {
  seq: number; // monotonic, assigned on append
  at: Date;    // wall-clock stamp, informational only
}
```

## Writing

| Method               | Returns              | Semantics                                                            |
| -------------------- | -------------------- | -------------------------------------------------------------------- |
| `add(role, content)` | `Promise&lt;number>` | Shorthand for `append(&#123; role, content &#125;)`                  |
| `append(msg)`        | `Promise&lt;number>` | Appends one `AgentMessage`, atomically assigns and returns its `seq` |
| `appendMany(msgs)`   | `Promise&lt;number>` | Appends a batch atomically, returns the last assigned `seq`          |

## Reading

| Method    | Returns                                  | Semantics                                                         |
| --------- | ---------------------------------------- | ----------------------------------------------------------------- |
| `all()`   | `Promise&lt;StoredMessage[]>`            | Full history, chronological — paginates internally, no length cap |
| `last()`  | `Promise&lt;StoredMessage \| undefined>` | The single most-recent message                                    |
| `last(n)` | `Promise&lt;StoredMessage[]>`            | The last `n` messages, in chronological order                     |
| `count()` | `Promise&lt;number>`                     | Total messages ever appended                                      |

`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:

| Adapter         | Output                              | `system`                                                             | `tool` / `toolCalls`                                                                                         |
| --------------- | ----------------------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `toLangChain()` | plain `&#123; role, content &#125;` | included                                                             | **dropped** — only `user`/`assistant`/`system` survive                                                       |
| `toOpenAI()`    | Chat Completions messages           | included                                                             | mapped to `tool_calls` / `tool_call_id`, `args` JSON-stringified                                             |
| `toAnthropic()` | Messages-API turns                  | **dropped** — pass the system prompt as the top-level `system` param | assistant `toolCalls` become `tool_use` blocks; `tool` results become `user` turns with `tool_result` blocks |

```ts theme={null}
const history = await this.messages.toOpenAI();
// → ready for env.TELNYX.ai.openai.chat.createCompletion({ model, messages: history })
//   (the pre-authenticated binding) or any OpenAI-compatible client
```

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](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](/docs/edge-compute/stateful-actors/alarms) slot, re-armed to the
earliest pending deadline.

## Creating tasks

| Method                                       | Semantics                                                               |
| -------------------------------------------- | ----------------------------------------------------------------------- |
| `queue(method, payload?, opts?)`             | Run as soon as possible — identical to `schedule(0, ...)`               |
| `schedule(seconds, method, payload?, opts?)` | Run once, `seconds` from now                                            |
| `every(seconds, method, payload?, opts?)`    | Run repeatedly at a fixed interval; throws if `seconds` is not positive |

All three return `Promise&lt;string>` — the task id.

```ts theme={null}
await this.queue("process", { attempt: "first" });
await this.schedule(3600, "sendReminder");
await this.every(300, "checkStatus");
```

### `ScheduleOptions`

```ts theme={null}
interface ScheduleOptions {
  id?: string;         // stable id: re-scheduling REPLACES the prior task (dedup)
  maxRetries?: number; // retries after the first delivery; default 5
}
```

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, &#123; attempt &#125;)`.
* 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](/docs/edge-compute/stateful-actors/alarms).

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](/docs/edge-compute/stateful-actors/api-reference/errors).

## Inspecting and cancelling

| Method               | Returns                    | Semantics                                    |
| -------------------- | -------------------------- | -------------------------------------------- |
| `cancelSchedule(id)` | `Promise&lt;boolean>`      | Remove a pending task; `false` if no such id |
| `listSchedules()`    | `Promise&lt;TaskRecord[]>` | Every pending task record                    |

```ts theme={null}
interface TaskRecord {
  id: string;
  name: string;        // the method it will call
  payload?: unknown;
  due: number;         // epoch ms of the next fire
  everyMs?: number;    // recurrence interval, if repeating
  attempts: number;    // consecutive failed deliveries so far
  maxRetries: number;
  createdAt: Date;
}
```

***

### State

> Source: [https://developers.telnyx.com/docs/agent-sdk/api-reference/state.md](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.

```ts theme={null}
interface ConvState extends Record<string, unknown> {
  status: "idle" | "processing";
  lastReply: string;
  at: number;
}

export class Conversation extends Agent<Env, ConvState> {
  protected override initialState(): ConvState {
    return { status: "idle", lastReply: "", at: 0 };
  }

  async process(): Promise<void> {
    await this.setState({ status: "processing" });          // merge: other keys untouched
    // ...
    await this.setState({ status: "idle", at: Date.now() });
  }
}
```

## Methods

| Method               | Returns             | Semantics                                                                      |
| -------------------- | ------------------- | ------------------------------------------------------------------------------ |
| `getState()`         | `Promise&lt;State>` | The stored state, or `initialState()` if nothing has been written yet          |
| `setState(patch)`    | `Promise&lt;State>` | Recursive-merges `patch` (RFC 7396) in a transaction; returns the merged state |
| `replaceState(next)` | `Promise&lt;State>` | Writes `next` wholesale; returns it                                            |

* 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](/docs/edge-compute/stateful-actors/api-reference/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 `&#123;&#125;`. 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](https://developers.telnyx.com/docs/agent-sdk/limits.md)

## Execution budgets

| Context                                     | Budget                           | On overrun                                |
| ------------------------------------------- | -------------------------------- | ----------------------------------------- |
| Inbound RPC method (e.g. `receive()`)       | 30s wall-clock (default)         | Call fails with `ActorMethodTimeoutError` |
| Queued / scheduled task (runs in `alarm()`) | Larger — on the order of minutes | Run counts as failed; task retries        |

Do LLM and outbound API work in tasks, not inbound methods — see
[How Agents Run](/docs/agent-sdk/concepts/how-agents-run). Full platform numbers live in
the [StatefulActor API reference](/docs/edge-compute/stateful-actors/api-reference/base).

## 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](/docs/edge-compute/stateful-actors/api-reference/storage). 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](/docs/agent-sdk/sql) 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](/docs/agent-sdk/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.

***
