Skip to main content

Build your first agent

A forty-line TypeScript script that:

  1. Polls the agent's inbox.
  2. Asks an LLM for a reply.
  3. Sends that reply back out through Postbox.
  4. Writes a one-line summary to semantic memory.

The code

first-agent.ts
const BASE = process.env.AGENTPACK_URL!;
const KEY = process.env.AGENT_DEVICE_KEY!;
const ID = "agent-hello";

const headers = {
"content-type": "application/json",
"x-agentpack-device-key": KEY,
};

async function rpc<T>(path: string, body: unknown): Promise<T> {
const r = await fetch(`${BASE}${path}`, {
method: "POST",
headers,
body: JSON.stringify(body),
});
if (!r.ok) throw new Error(`${path} -> ${r.status} ${await r.text()}`);
return r.json() as Promise<T>;
}

type Msg = { id: string; from_addr: string; subject: string; body: string };

async function llm(subject: string, body: string): Promise<string> {
// Swap for your model of choice. Keep it boring here.
return `Re: ${subject}\n\nThanks for your note. I saw: "${body.slice(0, 80)}..."`;
}

async function tick() {
const { messages } = await rpc<{ messages: Msg[] }>("/agent-postbox/list", {
agent_id: ID, direction: "in", limit: 5,
});
for (const m of messages) {
const reply = await llm(m.subject, m.body);
await rpc("/agent-postbox/send", {
agent_id: ID, from: `${ID}@example.test`, to: m.from_addr,
subject: `Re: ${m.subject}`, body: reply,
});
await rpc("/agent-postbox/mark_read", { agent_id: ID, ids: [m.id] });
await rpc("/agent-memory/mem/write", {
agent_id: ID, kind: "episode",
text: `Replied to ${m.from_addr} about "${m.subject}"`,
provenance: `inbound-email:${m.id}`, scope: ["self"],
});
}
}

setInterval(tick, 15_000);

Run it

AGENTPACK_URL="https://<ref>.firebaseapp.com/functions/v1" \
AGENT_DEVICE_KEY="ap_dev_..." \
npx tsx first-agent.ts

What's good about this shape

  • Every call is idempotent-ish. mark_read only stamps rows that aren't already read; a duplicate send to the same address is a new row, not a re-delivery.
  • Failure is visible. problem+json bodies carry a type and detail; wrap rpc in a retry with jitter and log both.
  • No database coupling. The agent doesn't know about Cloud Scheduler, Firestore vector search, or RLS. It speaks HTTP.
  • The agent's identity is leaf-level. A leaked AGENT_DEVICE_KEY is scoped to agent-hello. It cannot read agent-triage's mail or write agent-triage's memory, no matter what it puts in the body.

Where to go from here

  • Wire /agent-postbox/webhook/register so you don't have to poll.
  • Turn the llm() call into something real and gate its spend via Governor.
  • Promote recurring work into a Scheduler job so Firestore wakes the agent, not a machine you pay for.