ayjnt
All examples
statehttp

Basic agent

One ChatAgent with state, no UI, no middleware. The smallest possible ayjnt project beyond the blank scaffold — adds persistent messages so you can see how state works on a Durable Object.

What you'll learn
  • How the blank scaffold maps folders to agent URLs
  • How `this.state` / `this.setState` persist on a Durable Object instance
  • How to run ayjnt dev and hit the agent with curl
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

Replace the `alive` agent with a chat agent

Delete the starter agent and add a chat one. Each folder under `agents/` is exactly one Durable Object class — the folder name becomes the URL prefix.

~/my-agent-app
my-app/ (after editing)
agent.ts
03 step

agents/chat/agent.ts

Extend `Agent`, declare your state type, write `onRequest`. `this.state` is the per-instance DO state; `this.setState` persists it and broadcasts the update to any connected UI. `GeneratedEnv` picks up every DO binding ayjnt generated automatically.

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

type State = {
  messages: { role: "user" | "assistant"; text: string }[];
};

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

  override async onRequest(request: Request): Promise<Response> {
    if (request.method === "POST") {
      const { text } = (await request.json()) as { text: string };
      this.setState({
        messages: [...this.state.messages, { role: "user", text }],
      });
      return Response.json({ ok: true, count: this.state.messages.length });
    }
    return Response.json({ instance: this.name, ...this.state });
  }
}
04 step

Run it and hit it with curl

ayjnt dev wraps wrangler dev. Each path segment after `/chat/` is a separate DO — `/chat/room-1` and `/chat/room-2` have independent state, even though they share the same agent class.

~/my-agent-app
05 step

What it looks like

This example is terminal-only — there's no UI. You're verifying state persists across requests to the same instance, and that different instances are isolated.

two instances, independent state result
$ curl localhost:8787/chat/room-1
{
  "instance": "room-1",
  "messages": [
    { "role": "user", "text": "hi" },
    { "role": "user", "text": "again" }
  ]
}

$ curl localhost:8787/chat/room-2
{
  "instance": "room-2",
  "messages": []
}

  room-1                    room-2
  ┌──────────────────┐      ┌──────────────────┐
  │ #messages: 2     │      │ #messages: 0     │
  │  • hi            │      │                  │
  │  • again         │      │  (fresh DO)      │
  └──────────────────┘      └──────────────────┘
        ↑ own DO                  ↑ own DO
        └── state survives worker restarts ──┘
06 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