ayjnt
All examples
rpcmulti-agent

Inter-agent RPC

Two agents talking over Workers RPC via typed `getAgent<T>()`. An Orders agent calls Inventory to decrement stock — full method autocomplete, exception propagation, oversell protection.

What you'll learn
  • How `getAgent<T>` gives you typed DO stubs with method autocomplete
  • How exceptions propagate across the RPC boundary
  • Why agent RPC args must be structured-cloneable (plain data only)
01 step

Start from the blank scaffold

Every ayjnt example starts here. `bunx ayjnt new` drops a one-agent project with a single `alive` agent that responds "I'm alive" to any request — enough to prove the pipeline works before you replace it with the real thing.

~/my-agent-app
my-app/ (blank scaffold)
agent.ts
package.json
tsconfig.json
.gitignore
README.md
02 step

Two agents, one worker

Each folder becomes its own DO-backed agent with isolated state. Orders is instanced per customer (`/orders/customer-1`, `/orders/customer-2`); Inventory is a single shared instance (`/inventory/main`).

~/my-agent-app
my-app/agents/
agent.ts
agent.ts
03 step

Callee — typed method that throws on failure

`InventoryAgent.decrement` is an ordinary async method. Throwing is fine — the exception crosses the RPC boundary unchanged. Methods declared on the class are callable as DO stubs via `getAgent<T>`.

agents/inventory/agent.ts ts
import { Agent } from "agents";
import type { GeneratedEnv } from "@ayjnt/env";

type State = { stock: Record<string, number> };

export default class InventoryAgent extends Agent<GeneratedEnv, State> {
  override initialState: State = { stock: { widget: 10, gadget: 5 } };

  async decrement(sku: string, qty: number): Promise<number> {
    const current = this.state.stock[sku] ?? 0;
    if (current < qty) {
      throw new Error(`insufficient stock for ${sku}: have ${current}, need ${qty}`);
    }
    const remaining = current - qty;
    this.setState({ stock: { ...this.state.stock, [sku]: remaining } });
    return remaining;
  }

  override async onRequest(): Promise<Response> {
    return Response.json({ instance: this.name, ...this.state });
  }
}
04 step

Caller — typed stub via getAgent<T>

The generic makes `inv.decrement` autocomplete. `INVENTORY_AGENT` is a DO binding ayjnt generates automatically from the `agents/inventory/` folder — you never write wrangler.jsonc. Rename `decrement` in the callee and this file fails to compile.

agents/orders/agent.ts ts
import { Agent } from "agents";
import { getAgent } from "ayjnt/rpc";
import type InventoryAgent from "../inventory/agent.ts";
import type { GeneratedEnv } from "@ayjnt/env";

type State = { orders: { sku: string; qty: number; remaining: number }[] };

export default class OrdersAgent extends Agent<GeneratedEnv, State> {
  override initialState: State = { orders: [] };

  override async onRequest(request: Request): Promise<Response> {
    if (request.method !== "POST") return Response.json(this.state);

    const { sku, qty } = (await request.json()) as { sku: string; qty: number };
    try {
      const inv = await getAgent<InventoryAgent>(this.env.INVENTORY_AGENT, "main");
      const remaining = await inv.decrement(sku, qty);  // throws on oversell
      this.setState({ orders: [...this.state.orders, { sku, qty, remaining }] });
      return Response.json({ ok: true, remaining });
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      return Response.json({ ok: false, error: message }, { status: 409 });
    }
  }
}
05 step

Run it + oversell

Two customers each buy from the same inventory instance. The third customer tries to oversell and gets a 409 — exception thrown in Inventory, caught in Orders, translated to a structured HTTP response.

~/my-agent-app
06 step

What it looks like

Two DO instances, one typed RPC edge. The error message from Inventory bubbles up through the Orders agent to the HTTP client, unmodified.

inter-agent RPC + exception propagation result
client                orders/customer-3            inventory/main
──────                ─────────────────            ──────────────
POST /orders/customer-3
  {"sku":"widget",
   "qty":99}
       ─────────▶    onRequest
                     getAgent<InventoryAgent>(
                        env.INVENTORY_AGENT,
                        "main")
                        ─────────────────────▶    decrement("widget", 99)
                                                  current = 3
                                                  throw new Error(
                                                    "insufficient stock…")
                        ◀─────────────────────
                     catch(err)
                     Response.json(
                       { ok:false, error }, 409)
       ◀─────────
{ ok:false,
  error: "insufficient stock
          for widget:
          have 3, need 99" }
07 step

Deploy

`ayjnt deploy` checks your git tree is clean + synced with origin, regenerates the wrangler config from scratch, then shells out to `wrangler deploy`. The committed migrations.json file is the source of truth for what's in production.

~/my-agent-app