Chatbot
A chatbot is an event-driven Chidori agent: it runs under chidori serve, every incoming message becomes the event, and conversation history is persisted with chidori.memory(). Each turn is its own session — and therefore its own replayable checkpoint.
The whole thing, front to back
agents/chatbot.ts
import type { Chidori } from "chidori";
async function loadHistory(chidori: Chidori, userId: string) {
const stored = await chidori.memory("get", "chat:" + userId);
return stored ?? [];
}
async function saveHistory(chidori: Chidori, userId: string, history: any[]) {
// Keep the last 20 turns to bound the prompt
await chidori.memory("set", "chat:" + userId, history.slice(-20));
}
export async function agent(event, chidori: Chidori) {
if (event.path !== "/chat" || event.method !== "POST") {
return { status: 404, body: { error: "POST /chat" } };
}
const body = event.body;
const userId = body.user_id ?? "anonymous";
const message = body.message ?? "";
const history = await loadHistory(chidori, userId);
history.push({ role: "user", content: message });
const reply = await chidori.prompt(
await chidori.template("prompts/chat.jinja", { history }),
{ model: "claude-sonnet", tools: ["web_search", "calculator"] },
);
history.push({ role: "assistant", content: reply });
await saveHistory(chidori, userId, history);
await chidori.checkpoint("turn", { userId });
return { status: 200, body: { reply, user_id: userId } };
}prompts/chat.jinja
You are a helpful assistant. Keep answers concise unless the user asks for detail.
{% for turn in history %}
{{ turn.role | upper }}: {{ turn.content }}
{% endfor %}
ASSISTANT:Run it
chidori serve agents/chatbot.ts --port 8080Send a message:
curl -X POST http://localhost:8080/sessions \
-H "Content-Type: application/json" \
-d '{"path": "/chat", "method": "POST", "body": {"user_id": "alice", "message": "What\u2019s 15% of 80?"}}'Response:
{ "reply": "15% of 80 is 12.", "user_id": "alice" }The next request from alice will see the first turn in its history thanks to chidori.memory().
Adding tools
Expose tools by listing them in the chidori.prompt(..., { tools: [...] }) options. The runtime drives the provider tool-use loop for you. A tool is a .ts module exporting its metadata and a run function:
tools/calculator.ts
import type { Chidori, ToolDefinition } from "chidori";
export const tool: ToolDefinition = {
name: "calculator",
description: "Evaluate an arithmetic expression. Supports + - * / and parentheses.",
parameters: {
type: "object",
properties: { expression: { type: "string" } },
required: ["expression"],
},
};
export async function run(args: { expression: string }, chidori: Chidori) {
// Delegate to a sandboxed exec so LLM-generated expressions can't escape.
return chidori.execPython("result = " + args.expression + "\nresult", { timeoutMs: 2000 });
}The description becomes the LLM-facing label; the parameters JSON Schema is what the provider sees for function-calling.
Per-turn checkpoints
Every request to /chat is a session with its own checkpoint. That means:
- Debug a weird answer by replaying the exact session from the checkpoint — same history, same retrieved tool results, zero LLM spend.
- A/B new system prompts by replaying old sessions against a new template and diffing the outputs.
- Regression-test tool wiring by checking canonical session checkpoints into git.
# Grab the last session's checkpoint
curl http://localhost:8080/sessions | jq '.[0].id'
curl http://localhost:8080/sessions/<id>/checkpoint > session.jsonStreaming
For token-by-token streaming, label your prompt output with a type and run the server's SSE endpoint. Each chidori.prompt(text, { type: "..." }) call streams its tokens as they arrive from the LLM, tagged with that label so the client can distinguish progress chatter from the final answer:
const reply = await chidori.prompt(
await chidori.template("prompts/chat.jinja", { history }),
{ model: "claude-sonnet", type: "final", tools: ["web_search", "calculator"] },
);Connect a client to POST /sessions/stream for an SSE feed, or use --stream for a one-shot CLI run.
Tips
- Bound your history — unbounded chat history means unbounded prompt size and unbounded token cost.
- Store only raw turns in
chidori.memory()— regenerate any system prompt or summary in the agent. This makes history re-renderable under a new prompt template. - Use
chidori.retry()around thechidori.prompt()call for transient provider errors in production. - Default missing payload fields with
??— incoming event bodies are untrusted JSON, so guard the keys you read everywhere they show up.
