The same system as the two whiteboard diagrams from the previous videos (the generic Harness/Loop/Memory/LLM-Ops one and the Hermes-specific one), now with a file path on every box.
flowchart TB
subgraph GW["Gateway Interface — prometheus/gateway/"]
CLI["cli.py (default)"]
VOICE["voice.py (wake word)"]
TG["telegram.py · discord.py · whatsapp.py (optional)"]
end
subgraph RUN["Ephemeral Agent Run — everything here is rebuilt per turn"]
WM["Working Memory — runtime/session.py<br/>SOUL.md + memory context + chat history"]
subgraph LOOP["The Loop — loop/agent.py"]
LLM["LLM call<br/>(loop/models.py)"]
TOOLS["Tools — tools/<br/>calendar · notes · messages · search · MCP · …"]
LLM -->|tool calls| TOOLS -->|results| LLM
end
WM --> LLM
GUARD["end-loop guardrails:<br/>no-tool-call exit · max iterations"]
end
GW --> WM
LLM -->|reply| GW
subgraph MEM["Memory — prometheus/memory/"]
GATE{{"retrieval_gate.py<br/>'does this turn need memory?'"}}
PROC["procedural/ — SKILL.md<br/>how to act"]
SEM["semantic/ — facts (FTS5,<br/>or Supabase pgvector)"]
EPI["episodic/ — dated events"]
CONS{{"consolidation.py<br/>'only after N new chats'"}}
DB[("state.db — one SQLite file")]
end
WM -.->|every turn| GATE
GATE -->|only if needed| SEM & EPI
PROC -->|on keyword match| WM
GW -->|save messages| DB
CONS -->|distill into facts| SEM
CONS -->|one episode| EPI
SEM & EPI --- DB
subgraph OPS["LLM Ops — prometheus/ops/ + evals/"]
TRACE["tracing.py — 1 trace/run<br/>JSONL always · OTel → Phoenix/Langfuse<br/>every model call · 1 receipt/turn"]
DET["evals/deterministic — 0/1<br/>'did the right tool fire?'"]
JUDGE["evals/judge — scored %<br/>'was the reply good?'"]
RGATE{{"release_gate.py"}}
TRACE --> DET & JUDGE --> RGATE -->|eval passed| SHIP["release: new prompt/<br/>model/config version"]
end
RUN -.->|every event| TRACE
flowchart LR
GW["Gateway<br/>cli · telegram · voice · dashboard"] --> WM["Working memory<br/>SOUL.md + memory + history"]
WM --> LLM
subgraph LOOP["The Loop — loop/agent.py"]
LLM["LLM"] -->|tool call| TOOLS["Tools<br/>create_event · list_events<br/>search_web · save_note · …"]
TOOLS -->|result| LLM
end
LLM -->|reply| REPLY["Reply"] --> GW
GATE{{"Retrieval gate<br/>does this turn need memory?"}} -. only if needed .-> WM
MEM[("Memory — state.db<br/>SQLite + FTS5<br/>semantic · episodic · procedural")] --> GATE
REPLY -. save chat .-> MEM
MEM -->|every N chats| CONS["Consolidate → facts"] --> MEM
REPLY --> OPS["LLM Ops<br/>trace → eval → gate → release"]
OPS -. improved prompt/config .-> WM
WM -.- WATERMARK["prometheus-agent · Sean's AI Stories · @Engine-User"]:::wm
classDef wm fill:none,stroke:none,color:#9aa0aa,font-size:11px;
Architecture of prometheus-agent — built on the series (@Engine-User). Code is MIT; this diagram is licensed CC BY-NC-SA 4.0 — reuse it with credit to the channel, not for commercial resale.
Retrieval finds the top facts for a message and, by default, puts all of them
in the prompt; consolidation keeps every fact the summariser proposes. With
PROMETHEUS_SLOT_GATE=jev and a TYPESAFE_API_KEY, Jev decides both
(prometheus/memory/slot_gate.py, spec 005): one call scores every retrieved fact on
"how much does leaving this one out change the answer?" and keeps those at
0.5 or above, which scored 12 of 12 on lab/jev-system-one's cases; and
each proposed fact is scored on whether a later answer would need it before
it is stored. Any failure keeps today's behaviour, so a slow judge never costs
a memory. The turn card says how many it kept.
Some assistants (e.g. Hermes) keep long-term memory as a single MEMORY.md
markdown file. Prometheus keeps the queryable source in state.db (the facts and
episodes tables, keyword-searchable via FTS5) and regenerates a readable
~/.prometheus/MEMORY.md mirror after every turn — so you get both: a real file you
can open, backed by a sturdy database. The dashboard's Memory tab is the
friendly view; the Data tab shows the raw state.db tables.
Each fact is also written to ~/.prometheus/memory/<id>.md, one file per fact, in
the same pass. That is the layout of Claude Code's memory, which the Prometheus
Memory importer already uploads one memory per file. Episodes, MEMORY.md and
state.db are never written there (spec 003).
When a waku_memory MCP server is connected, app.py gives consolidation a
remember callable, and consolidation sends every fact it keeps to Prometheus Memory
with memory.remember (kind fact, or reference for company research),
right after storing it locally (spec 006). Prometheus's MCP client names itself prometheus-agent, which Prometheus Memory labels
as Origin prometheus. The summariser also keeps research findings
(companies, products, markets, prices, launches) and flags a batch that is
company research. Facts from a flagged batch go to scope
project:Company brain; all other facts go to scope global. The
consolidation event lists the kept facts with their Prometheus Memory ids.
The facts table records each pending send with synced = 0 and the scope.
A failed send is logged and never fails the turn, and the next consolidation
sends pending facts before new ones. Each kept fact in the event carries
sent: true when Prometheus Memory took it, false when it did not, and null without
Prometheus Memory. When sent is false, the chat's card reads "Kept on this agent
only" and says Prometheus Memory did not answer, and the receipt reads "2 kept, 2 on
this agent only", so neither claims a fact Prometheus Memory never stored. Rows from before spec 006 count as sent,
because the capture shim imports that backlog from memory/<id>.md. Hosted
containers consolidate every turn; laptops consolidate every 6 exchanges.
Without a connected server, nothing is sent.
The bundled research-report skill teaches the house language and the
prometheus-report v1 format (spec 007): one Markdown document whose first line is
<!-- prometheus-report v1 -->, with fixed sections and fenced JSON blocks
(prometheus-metrics, prometheus-chart, prometheus-compare, prometheus-timeline,
prometheus-sources) that waku.one renders. The format is frozen; a new component
is a new block name. When a turn's reply has that marker on a line of its
own, prometheus/memory/reports.py sends the report through the same remember
callable as one semantic memory, scope project:Company brain for company
or market research (one small-model question) and global otherwise. The
chat reply becomes the sentences before the marker plus "Report saved", the
turn emits a report event {title, memory_id, scope, summary} before
done, and the chat log keeps the short reply with the card in its meta.
The dashboard's chat draws that card (title, summary bullets, "Open report"
to /memories/<id> on www.waku.one, or on the waku.one site framing it), and
under it the facts a consolidation event says the turn kept (spec 008).
Without Prometheus Memory, or when the send fails, the reply keeps the whole
report and there is no event.
A turn saves at most one report. The model's own memory_remember tool
refuses a body holding the marker line, and if a report was saved by the
model anyway, save sends nothing more and the card points at that memory.
The skill says to name only the "treg cost" and never a total: the receipt
is the one place a turn's total is shown.
A turn is research when the research-report skill matches its message:
research on companies, competitors or markets, or a request to save a brief,
summary or snapshot built from tool results ("save an audience brief to the
Company brain"). With
Prometheus Memory connected, prometheus/memory/brain.py runs memory.search twice before
the model's first call (spec 009): once for the subject (the message without
words like "research" and "the"), in every scope, and once for earlier
reports, kind semantic. The hits go into the system prompt under "What the
company brain already knows", each with its date and id, reports first, and
the skill says to start there, name the earlier report and its date, and
research only what is missing or older than 30 days. Each earlier report
found is read once with memory.get and goes into the prompt as its digest
(title, Summary, key numbers, at most 1,500 characters), and the loop cuts a
whole report the model fetched itself to its digest before each later call
(reports.shrink_read, passed to run_loop as trim). The same trim
step then cuts every other tool result longer than 4,000 characters that the
model has already read to its first 2,000 characters and a note
(prometheus/loop/trim.py); the newest results are always sent whole, and the trace
keeps every output whole. Each search is shown as
a waku_memory_memory_search tool call (waku.one reads its entries as
Used), the done payload and the turn's meta carry them as used, and the
dashboard's chat lists them under "Used from memory". The searches are not
folded into the chat log. A failed search is logged and skipped; the turn
goes on without it.
A turn that answered from memory does not keep that memory again. The answer
repeats what it read, so the summariser proposes it as new facts (2026-10-05:
one recall turn sent five copies of a report's findings to Prometheus Memory).
app.py passes consolidation everything the turn read (what the retrieval
gate found, what research read first, and what the model's own Prometheus Memory
searches, gets and recalls returned), the summariser is shown it, and
consolidation.restates drops a proposed fact that has a number or a date
and whose every name, number and date is already in it. A new fact the person
adds ("Zep raised again in 2026") has something the memory does not, so it is
kept; a fact with no number is never dropped by this check.
A turn that saved a report consolidates with it (spec 009 B): the summariser
is told the findings are in the report, a fact whose subject the report names
is dropped, and at most two facts are kept, as the person's own (scope
global). A laptop batch that holds an earlier report turn reads that report's
title and summary from the chat log's meta.
Each tool card shows cost_usd and the provider when the result carries them
(treg's call, through the hosted relay too). PROMETHEUS_UNAVAILABLE_TOOLS names tools a deployment does
not offer, and the system prompt says not to call them; hosted containers set
it to treg's balance and resources_list, which the relay refuses.
Every reply in the chat ends with one line that says what the turn did and
cost (spec 011), for example claude-sonnet-5 · 12.4k in / 1.9k out · $0.064 est | treg 2 · $0.030 | memory 4 used · 2 kept · report saved | $0.094.
Clicking it opens a table of the same numbers, with each kept fact and the
report linked to its page on waku.one. prometheus/ops/receipt.py builds it once
from the turn's own events; the done payload carries it as receipt, the
chat log keeps it in meta.receipt, and the trace gets one receipt event.
It holds names, counts, dollars and ids, and never a tool's arguments or
output.
Every tool trace line (schema v: 2, spec 012) carries the turn's
turn_id, its source (treg, waku_memory, local or mcp:<server>),
its span kind (tool, retrieval or memory_write), duration_ms, ok
and on a failure error, cost_usd, endpoint_id and provider when the
result names them, the Prometheus Memory query, results, memory_ids and
retrieval_trace_id, and its args redacted and trimmed to 500 characters.
Every llm line carries turn_id and cost_usd. Where an OpenTelemetry
GenAI attribute exists, the OTel export uses it, mapped in one table
(GENAI in prometheus/ops/observability.py). The dashboard's Observability page
(GET /api/observability) reads these lines, and reads a line written
before spec 012 with the same functions, leaving empty what it cannot derive.
Each turn it returns carries scores: the five turn checks of
prometheus/ops/turn_evals.py (source code, value 1, 0 or null for n/a), run on
every read rather than stored, plus any score event written for the turn:
the AI judge's, written once when someone presses "Judge this turn" (spec
015, docs/evals.md). The judge's call carries an X-Prometheus-Turn id of its
own, so the metering proxy answers its exact charge.
Every turn has a turn_id, written on the trace's turn_start and
turn_end, on each usage.jsonl row and in the chat log's meta. Every model
call writes a usage.jsonl row and an llm event: the loop's with kind
loop, and the small-model calls around it with gate, consolidation,
report, triage or quick (metered() in prometheus/ops/tracing.py).
Model dollars are an estimate from prometheus/ops/pricing.py, marked "est". On
the hosted free tier, every model call carries X-Prometheus-Turn, and the
metering proxy answers GET /v1/turns/<turn_id>/charges with the exact
charge and the credits Prometheus Memory took (hosted/README.md); the receipt
shows those instead. A proxy that does not know the turn leaves the estimate.
prometheus/gateway/— how text gets in and out:cli.py,voice.py(wake word),telegram.py,discord.pyandwhatsapp.py, started byrunner.pyandsupervisor.py. Gateways only move text.prometheus/runtime/session.py— working memory for one turn: SOUL.md, memory context and chat history.prometheus/loop/agent.py— the loop.loop/models.py— pluggable providers over two wire formats.prometheus/graph/— the engine, node factories andworkflows/(triage): opt-in structure around the loop. The loop never changes, a graph node can be a loop turn, and every failure fails open to the plain loop.prometheus/tools/— what the agent can call:calendar.py,google_calendar.py,apple.py,notes.py,messages.py,search.py,github.py,workspace.py,memory_admin.py, the MCP client andexperimental.py.registry.pydecides which are on.prometheus/memory/— semantic (FTS5), episodic and procedural (SKILL.md) memory, plusretrieval_gate.py(hero 1: does this turn need memory?) andconsolidation.py(every N exchanges),reports.py(a research report goes to Prometheus Memory whole) andbrain.py(a research turn reads Prometheus Memory first).prometheus/ops/— tracing (JSONL + OTel), the dashboard (localhost:7777),release_gate.py, andcompare_history.py(the Compare arena's own JSONL scoreboard, neverstate.db).prometheus/ops/static/— the dashboard frontend. Read context/design-system.md before changing how anything looks.evals/deterministic/(0/1, pytest) andevals/judge/(DeepEval, scored). The two never mix.evals/hosted_docker/is a third tier forhosted/: 0/1 and offline, but it needs a Docker daemon and its own CI job.examples/— teaching material, not product; one folder per topic.~/.prometheus/— runtime state:state.db,calendar.ics,outbox/,traces/.PROMETHEUS_HOMEmoves it;makesets it to the repo's own.prometheus/, which is gitignored.prometheus/config.pyresolve_home()has the rules..env— a model key is read from the environment, then the nearest.envfrom the working directory upward, then<home>/.env, the first one winning. A dashboard save writes to that nearest.envwhen one exists and to<home>/.envotherwise (prometheus/config.pyenv_write_target()).prometheus/key_locations.pyreports those places to the setup page and toprometheus connections, as paths and yes or no, never a value.
- The gate before retrieval (not retrieval on every turn): a cheap-model judge answers "does this message need the user's memory?" — saves latency and, more importantly, keeps irrelevant memories from biasing answers.
- Consolidation is batched ("after N chats"), asynchronous to the reply path, and loss-safe: if the summarizer fails, the chat log stays unconsolidated.
- Deterministic evals and judge evals never mix. One is a unit test, the other is a scored opinion. The release gate requires 100% of the first and a threshold on the second.
- Every layer has a boring default and a documented upgrade — FTS5 → pgvector, mock calendar → Google Calendar, JSONL → Phoenix/Langfuse. The default is always zero-signup.
- Graphs wrap the loop, never replace it. When a turn needs shape (parallel
steps, explicit routing), an opt-in graph workflow (
prometheus/graph/) arranges nodes around the untouched loop — thefull_agentnode ISrun_loop. Routers are plain code reading state a model wrote; every failure fails open to the plain loop; the dashboard renders the topology from the engine's owndescribe()so the picture can't drift. Seedocs/agent-graphs-design.md.
This tree, prometheus/, is not a framework, not multi-agent, and not production.
(Still not multi-agent even with graph workflows: a graph's agent_node is the
same loop invoked as one step — no peer-to-peer agent messaging, execution
follows the edges deterministically.) It's the readable blueprint — OpenClaw
and Hermes are the products; this is the afternoon read that explains them.
hosted/ runs this same loop as a service instead; it is a deployment of
prometheus, not a second architecture (conventions.md §3). Spec 001 designed it and
it runs at agent.waku.one, one container per person. Each container reaches
that person's Prometheus Memory on its own: the gateway mints their Prometheus Memory key
at their first sign-in, and every start passes it in (spec 004). Every turn
then sends the facts it keeps to that Prometheus Memory (spec 006). Each can call
treg through the metering proxy, which holds the platform's treg token and
charges each call to the person's credits. A laptop reaches treg directly on
the person's own account, once prometheus connect treg has signed them in (spec
007). waku.one reads a person's chat history through the gateway's
/v1/conversations routes, which forward to the container's own
/api/session; the container keeps the one copy (spec 007). waku.one shows
the agent's own chat by framing /embed/chat, the dashboard's chat column
alone: POST /v1/embed gives it a one-time code, the code becomes an embed
session that reaches only the chat's routes, and only the origins in the
gateway's PROMETHEUS_EMBED_ORIGINS may frame the page (spec 008, and
hosted/README.md).