Quick Start · Demo · Tutorial · API · Contributing
A single Python backend ("the harness") owns one engine and exposes it through stable, hot-swappable component slots; the topology of a turn — which roles run, in what order, with what branches and retries — is declared as a small JSON graph over a closed 9-primitive vocabulary, validated at save time and executed by a generic engine. Two frontends sit on top: a chat UI and a ComfyUI-style canvas ("Loop Designer") that draws the graphs directly from the API.
→ Open the live demo — zero setup, zero LLM calls
The Pages site replays a real captured run of the
sample-manual-route graph: the engine's offline preset is
deterministic, so the replay is bit-identical to any run you would get,
and it makes zero LLM calls (the route node's manual decider pins
the branch in the document).
| Surface | Link |
|---|---|
| Landing + replay of a real run | alexscotland.github.io/Guts/ |
| Live (interactive) Loop Designer | alexscotland.github.io/Guts/designer/ |
When a live backend exists the build bakes it into the designer
(GUTS_DEMO=1 — the real app in stranger-safe posture: one env var
activates the offline preset + the showcase graph); the replay fixture
can be refreshed any time with deploy/tools/capture_demo_run.py.
Details in docs/deploy-demo.md.
One-time setup — the SearXNG instance needs a local secret key (the template
is committed as searxng/.env.example; the real .env is gitignored):
cp searxng/.env.example searxng/.env
echo "SEARXNG_SECRET=$(openssl rand -hex 32)" >> searxng/.envThen bring the whole stack up:
docker compose up --build| Surface | URL |
|---|---|
| Loop Designer (canvas) | http://localhost:3020 |
| Chat UI (WIP) | http://localhost:3010 |
| API | http://localhost:8000 |
| SearXNG (web search) | http://localhost:8080 |
The LLM runs on your host via Ollama
(OLLAMA_URL=http://host.docker.internal:11434, default model
Duggles/qwen-3-8-larger-context:latest — see app/inference/config.py).
No model available? Activate the offline preset (passthrough goal,
trivial plan, echo executor, direct loop — zero LLM calls) and the
manual-decider graphs still run:
curl -s -X POST localhost:8000/api/components/activate \
-H 'content-type: application/json' -d '{"preset": "offline"}'Your first graph — tutorial: a step-by-step
walkthrough (boot the stack → read the one discovery door → the smallest
legal graph document → activate + run three curls → read the node-event
stream → build your own graph on the canvas → fail on purpose → the no-LLM
offline preset). Every command runs as-is; every deterministic output is a
real capture.
# 1. make a saved graph active — takes effect on the very next /api/chat
curl -s -X POST localhost:8000/api/components/activate \
-H 'content-type: application/json' \
-d '{"loop": "graph", "graph_id": "sample-critique"}'
# 2. one real turn through the graph engine
curl -s -X POST localhost:8000/api/chat \
-H 'content-type: application/json' \
-d '{"message": "What is this repository, in three bullets?"}'
# 3. read back the node event stream (start/result/done/control/...) —
# while a run is in flight the same URL serves the LIVE record
curl -s localhost:8000/api/graphs/sample-critique/last-run | python3 -m json.toolFake-driven only — real-LLM runs are smoke tests, never assertions
(app/tests/, 270 tests). Third-party deps resolve from a local,
gitignored app/.pydeps/ (the Docker image installs the same
requirements.txt):
cd app
pip install --target .pydeps -r requirements.txt # one-time, then:
PYTHONPATH=.pydeps python3 -m pytest tests/ -qA real-LLM end-to-end smoke run (prints the answer + every node event)
ships at app/graph_smoke.py.
- One engine, hot-swappable slots — six stable slots (goal-resolver, planner, executor, evaluator, memory, loop) you swap at runtime; the new alias is live on the very next turn, no restart.
- Topology as data — the order roles run is a small JSON graph over a closed 9-primitive vocabulary, validated before it can be saved or run; a new flow is a JSON file, not a new loop class.
- Loop Designer — a ComfyUI-style canvas that draws and edits graphs straight from the API; the palette renders the engine, not a copy of it.
- Chat UI — a Next.js chat front on the same stateless backend.
- Runs with or without an LLM — an
offlinepreset (zero LLM calls) andmanual-route graphs exercise the full contract with no model access. - Agents — a capability bundle (model + MCP servers + skills + persona) around exactly one loop; bind it at turn start, roll it back any time.
- Six sample graphs —
app/graphs/sample-*.json, each adding exactly one more contract feature, all active-ready. - Fake-driven test suite — the oracle; real-LLM runs are smoke tests, never assertions.
Guts ships real local tools (shell, Python, files, PDFs, web search) and drives whatever LLM you point Ollama at. Treat it like a powerful local box:
- Keep the API (
:8000) and the chat/designer frontends on localhost or a private network — there is no built-in auth. - Ollama runs on your host; whatever model access it grants is the access the harness gets. Keep it behind your own controls.
- Don't commit
searxng/.env, the localapp/.pydeps/, or any saved graph/agent document that embeds credentials.
Slots (code, swapped at runtime) — every role the loop needs is a slot with a stable contract; implementations register under an alias and are activated at runtime, so any slot changes from the very next turn with no restart:
| Slot | Job | Ships with |
|---|---|---|
goal_resolver |
user input → a resolved Goal |
LLM extractor, passthrough |
planner |
Goal → task list |
LLM planner, trivial single-task |
executor |
runs one task (LLM + tools) | LLM+tools executor, echo |
evaluator |
continue / complete / replan after a task | heuristic, always-complete |
memory |
conversation store, per-agent (concurrency-safe) | full, trimmed |
loop |
the topology of a turn | plan_execute, direct, graph |
Graphs (data, saved as JSON) — the loop slot's graph alias executes
the active graph per turn over a closed primitive vocabulary:
| Primitive | Maps to | Provides | Needs (in) | Notes |
|---|---|---|---|---|
prompt |
— (no slot, no LLM) | context |
goal? |
inject fixed text (config.text) |
research |
executor slot, read-only by contract | context, result |
goal, context? |
|
plan |
planner slot |
plan |
goal, context? |
|
act |
executor slot — the only side-effecter |
result |
plan or context |
rejected inside parallel branches |
critic |
evaluator slot, then LLM-with-schema |
verdict, pass |
result |
|
gate |
— (pure projection, no LLM) | pass |
verdict |
deterministic pass/fail readout |
merge |
— (the only committer of branch outputs) | merged, result, context |
result/merged |
commits in declared edge order |
parallel |
— (asyncio, branch-local blackboards) | merged |
goal, context? |
inline branch graphs; max_parallel cap |
route |
one structured LLM call (or manual pin) | context, result (passthrough) |
goal/context/result |
v1: choice among its outgoing edges |
Edges are either data (typed against provides/needs, must be acyclic) or
control (retries; may leave only critic/gate; must carry
max_passes). Everything is validated before it can be saved
(app/schemas/graph.py — pure, no LLM, no I/O), a document that cannot be
validated cannot be saved, activated, or executed.
Agents (a document, the layer above the loop) — a specialist is a second
document: a capability bundle — an optional model, a set of MCP tool servers, a
set of skills, an optional persona, and settings overrides — around exactly
one loop child (a saved graph id or a built-in alias). Bind the bundle at
turn start and run the existing loop unchanged, so "a different agent = a
different model/MCP/skills/flow" is a validated document + activate, not an
edit-and-restart. Contract and the full surface (chat panel, the designer's
read-only Agents library, and the designer's agent node — add/edit/
save/activate/delete/test-MCP on the canvas over the REST door):
agent-graph.md.
app/graphs/sample-*.json — each file adds exactly one more contract feature
on top of the last. They validate against the real contract
(validate_graph) and are active-ready in the designer's library:
| File | Shows |
|---|---|
sample-hello |
the minimal graph document: prompt → act, one data edge, one budget ceiling |
sample-pipeline |
research → plan → act; per-node budgets; on_failure: "skip" — a flaky lookup degrades, the planner falls back to the goal alone |
sample-critique |
the retry loop: act → critic + a control edge back to act with max_passes: 3 — the only legal cycle in the contract |
sample-fanout |
the full vocabulary: parallel (2 inline read-only branches) → merge (commits in declared order) → act with on_failure: "retry(2)" → critic → gate (terminal, no LLM) |
sample-supervisor |
the route v1 primitive: the classic supervisor — outgoing edges are the choice set, llm decider (choice + mandatory reason), declared fallback, untaken branches skipped as "not routed" |
sample-manual-route |
the same supervisor with the manual decider: pinned + budget: 0 = zero LLM calls on the hop — deterministic debug/offline mode |
Validate any of them yourself (stdlib-only, no dependencies needed):
cd app
python3 -c "from schemas.graph import parse_graph; print(parse_graph('graphs/sample-critique.json').topo)"| Endpoint | Purpose |
|---|---|
POST /api/chat |
one turn; the frontend owns the thread (stateless backend) |
GET /api/components |
live slot/alias/preset state plus the closed primitive vocabulary — the one discovery door the designer renders |
POST /api/components/activate |
swap a whole preset, one slot+alias, or the graph loop (+ graph_id) |
GET/POST /api/graphs |
list / save (validated — invalid ⇒ 400, never saved) graph documents |
GET/DELETE /api/graphs/{id} |
fetch / delete one document |
GET /api/graphs/{id}/last-run |
the canvas seam: finished run record, or the in-flight one (status: "running" + events so far) |
POST /api/agents/{id}/activate · POST /api/agents/deactivate |
bind a saved agent (persona/model/MCP/skills) onto the pipeline from the next turn · roll it back (the document stays — re-activate any time) |
GET /health |
liveness |
Graphs are JSON files (app/graphs/<id>.json + _meta.json for the active
id) — files are the store; restart-safe, git-trackable, diffable.
app/ the harness — FastAPI server + agent core (Python 3.11)
components/ slots, registry, presets, the graph engine + 9 primitives
schemas/ graph.py — the frozen contract + validator (stdlib-only)
graphs/ saved graph documents, incl. the six samples above
tools/ web search, python, files, PDFs, memory, shell, time
tests/ fake-driven suite (the oracle)
chat-ui/ Next.js chat frontend (thread lives in the browser)
designer/ Next.js Loop Designer — canvas over the two doors above
searxng/ private SearXNG instance (search) + Valkey (cache)
docs/ the tutorial + the design docs; the graph contract and its
evolution:
tutorial.md (Your first graph — the walkthrough,
runnable as written)
graph-loop-designer.md (frozen v0 contract)
ai-routing-nodes.md (v1: route — the 9th primitive)
ai-per-node-outputs.md (v2: node outputs on the event seam)
hot-swap-components.md (slots/aliases/presets)
agent-graph.md (layer above: the agent as a
capability-composition doc — model +
MCP + skills + exactly one loop;
implemented (v1): read-only Agents
library + agent-node authoring
surface on the canvas)
- Small closed vocabulary + open topology. Freedom comes from how you
connect, not from node count; adding a primitive is a separate, deliberate
axis — a node is one class (
run(state, config)) + a registration, discovered through one door. - Data is inert until validated. The validator is pure and shared by save, activate, and execute — a document that cannot run cannot even be saved.
- Determinism where it's cheap. Merges commit in declared edge order;
the
manualdecider +budget: 0gives fully offline graphs; tests run against fakes and assert byte-identical conversation order. - Non-determinism is paid for in the output. The
llmdecider must return a choice and a mandatory reason; an LLM fallback is declared, never silent. - The engine is the single source of truth. The designer renders
GET /api/components, not a copy — the two only diverge when a contributor adds a primitive to the backend, and then the palette updates automatically.
- Backend: run
cd app && PYTHONPATH=.pydeps python3 -m pytest tests/ -qbefore pushing; fake-driven tests are the oracle, real-LLM scripts are smoke runs. - New primitive: implement it in
app/components/primitives/, register it in the registry, extendPRIMITIVESinapp/schemas/graph.py, add fakes + tests, and add a sample graph that exercises it. - Docs first: if it changes the contract, it gets a
docs/*.mdentry.
_ _
/ \ / \
{ } { }
{ { } }
\ \ / /
\ Y /
.-"`"`"-.
,` `.
/ \
/ \
{ ;"";, }
{ /";`'`,; }
\{ ;`,'`;. /
{ }`""` } /}
{ } { //
{||} { /
`"' `"'

