Zeta is a local-first substrate for durable agents — the one where you can replay the exact prompt behind any action. An agent is a Markdown file. Every event it receives, every prompt the model saw, and every tool call it made is stored in project-local SQLite, so when an agent does something you did not expect, you replay what actually happened instead of guessing.
A single agents/<slug>.md file declares everything an agent is:
- the durable events it accepts and may return,
- the tools it can use and the shared skills it opts into,
- and the prompt that runs when a matching event arrives.
The runtime does the rest. It stores events, queue state, run attempts, tool calls, model calls, and prompt traces locally, so nothing about a run is hidden — you can list it, diff it, and replay the exact prompt the model saw.
First, point Zeta at a model — it drives every agent. Any OpenAI-compatible
chat completions endpoint works; Zeta looks for one at
http://127.0.0.1:8080/v1/chat/completions by default. See
docs/concepts.md for model profiles and the
Codex backend.
Install Zeta (Python 3.11+) and make a folder for it to watch:
uv tool install zeta-os
mkdir -p ~/zeta-inboxEnable the bundled filesystem connector, so a new file becomes a durable event.
agents/connectors.yaml:
event_connectors:
- filesystemNow write the agent. It is one Markdown file, agents/note-reader.md:
---
name: Note Reader
description: Summarizes files dropped into the inbox.
resumable: true
accepts:
- file.created
ingress:
- event: file.created
filter:
dir: ~/zeta-inbox
idempotency_key: "file:{path}"
tools:
- read
---
A file was just created: {{ event.payload.path }}.
Read it and reply with a one-sentence summary of what it contains.Start the worker and drop a file into the folder:
zeta serve & # polls the inbox
echo "Buy milk. Email the accountant about Q3." > ~/zeta-inbox/todo.txtWithin a couple of seconds the connector emits file.created, note-reader
runs, and its whole timeline — every prompt and tool call — is on disk under the
session agent/note-reader:
zeta trace --session agent/note-reader loga1b2c3d4 assistant_message The note is a short to-do list: buy milk and email the accountant about Q3.
7f8e9d0c prompt 6 components · ~712 tok
e5f6a7b8 assistant_message → read
9c0d1e2f prompt 4 components · ~486 tok
Stop the worker with kill %1 when you are done. Those prompt lines are the
interesting part — the next section is what you do with them.
zeta agent new <slug>scaffolds a starting skeleton if you would rather not write the file by hand.zeta rundrives agents once and exits;zeta serveruns continuously and is what polls connectors like the filesystem watcher.
Agents fail in ways you cannot reproduce, because the input that caused the failure is gone by the time you see the output. In Zeta it is not gone: every prompt is a stored, hash-verified object you can inspect, resend, and diff without re-running anything upstream — no file, no connector, no queue.
Say note-reader returned a lazy summary. Pull up the exact prompt behind it:
zeta trace --session agent/note-reader show 9c0d1e2fResend that same prompt — byte for byte — to a stronger model and diff the two answers, without touching the inbox or replaying the pipeline:
zeta trace --session agent/note-reader replay 9c0d1e2f --model deep --diffChanged a skill or the prompt and want to know exactly what moved? Diff the two prompt versions, component by component:
zeta trace --session agent/note-reader diff 9c0d1e2f 3a4b5c6d --statreplay verifies the rebuilt prompt against the recorded hash before sending, so
you are always comparing against what actually ran — not an approximation of it.
This is what Zeta is built to do: turn "why did it do that?" from a guess into
a command.
Events are the seam between agents: one agent's returns is another agent's
accepts. That is how you build a pipeline instead of one monolithic prompt.
Here a scheduled agent produces a weekly digest and hands it to a second agent
that posts it. agents/release-digest.md runs on a cron and returns an event:
---
name: Release Digest
description: Summarizes the pull requests merged this week.
schedules:
- cron: "0 9 * * 1"
timezone: Europe/Paris
returns:
- release.summary.ready
tools:
- bash
---
Summarize the pull requests merged in the last week as release notes.agents/announcer.md waits for that event and acts on it:
---
name: Announcer
description: Posts release notes for the team.
accepts:
- release.summary.ready
tools:
- write
---
Post these release notes:
{{ event.payload.summary }}The shared event needs a schema, agents/events/release.summary.ready.json:
{
"type": "object",
"required": ["summary"],
"properties": { "summary": { "type": "string" } },
"additionalProperties": false
}A schedules: block turns cron into a trigger event,
agent.release-digest.scheduled. Because zeta run fires due schedules and then
drains the queue, one command drives the whole chain:
zeta run
# fires the schedule -> release-digest runs -> publishes release.summary.ready
# -> announcer runs on that event -> queue empty, exitThe hand-off is an ordinary durable event — inspect it with
zeta events --type-prefix release..
Events are durable records — a type, a source, an object payload, and
optional idempotency and causality metadata. They are the only way work enters
the system. Project event schemas live under agents/events/.
Agents are the Markdown files in agents/. When an event matches an agent's
accepts, the runtime runs the assistant/tool loop against the rendered prompt.
That event may come from a connector, a schedules: cron trigger, or another
agent's returns — which is how agents compose. If the agent declares
returns, Zeta performs one final structured generation and publishes the
validated result as a new durable event.
Tools and skills extend an agent. Tools (read, grep, bash, edit,
write, …) are capabilities granted to the model; skills are shared Markdown
procedures under agents/skills/ that agents opt into.
Connectors bind agents to the outside world. They contribute event schemas and handle ingress (external events in) and egress (returned events out) for services such as Slack or the local filesystem.
Durability and replay is the point of the whole thing. Runtime events answer "what happened?"; prompt traces answer "what exactly did the model see?" — and any stored prompt can be resent (see Replay any decision).
Each of these has a full reference in docs/concepts.md: the frontmatter fields, event and returned-event mechanics, the tool table, connector ingress/egress, running the worker and scheduler, observability, and the JSON-RPC interface.
uv sync --group dev
uv run pre-commit run --all-files
uv run pytestApache-2.0. See LICENSE.