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 8080

Send 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.json

Streaming

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 the chidori.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.

Was this page helpful?