Build your first agent
A forty-line TypeScript script that:
- Polls the agent's inbox.
- Asks an LLM for a reply.
- Sends that reply back out through Postbox.
- 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_readonly stamps rows that aren't already read; a duplicatesendto the same address is a new row, not a re-delivery. - Failure is visible. problem+json bodies carry a
typeanddetail; wraprpcin 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_KEYis scoped toagent-hello. It cannot readagent-triage's mail or writeagent-triage's memory, no matter what it puts in the body.