Contents
OpenTopos turns a natural-language prompt into a fully-articulated 3D object — geometry code, URDF joints, textures, rendered views — without any manual Blender interaction.
The harness works through a plan-driven DAG. A plan.json declares tasks with explicit dependency edges. An orchestrator (Runner) executes them: LLM agents for creative work, deterministic Python tools for Blender renders, exports, and rubric evaluation. When the judge scores too low, a fix loop synthesizes targeted repair agents from judge feedback and re-runs only the affected tasks.
Code is truth — procedural Python is authoritative; all meshes, URDF, GLB, and renders are derivative artifacts, freely deletable and reproducible.
Inputs
Agents Write
design.json — part contractsrc/parts/*.py — geometrysrc/build.py — assemblysrc/joints.yaml — articulationTools Produce
artifacts/object.glbartifacts/object.urdfartifacts/view_*.png (8 views)run_report.jsonSeven layers; each depends only on the ones below. New backends, tools, rubrics, and domain workflows extend leaf layers without touching the core orchestrator.
flowchart TD
L7["CLI"]
L6["Domain Workflows"]
L5["Orchestrator"]
L4["Critic"]
L3["Knowledge / Skills"]
L2["Tools"]
L1["Agent Backends"]
L0["Substrate"]
L7 --> L6 --> L5
L5 --> L4
L5 --> L3
L5 --> L2
L5 --> L1
L1 --> L0
L2 --> L0
| Layer | Name | Role | Extend by |
|---|---|---|---|
| L7 | CLI | topos make / run / doctor / cost / skill | New CLI subcommand |
| L6 | Domain Workflows | plan.json template + rubric per object type | New domain dir + plan template |
| L5 | Orchestrator | DAG execution, fix-loop, subgraph expansion | Closed — core |
| L4 | Critic | Rubric evaluation via vision LLM | New Critic backend class |
| L3 | Knowledge / Skills | SKILL.md bundles injected as agent soft-hints | New topos_*/SKILL.md file |
| L2 | Tools | Deterministic Python functions via @tool | New @tool function |
| L1 | Agent Backends | LLM CLI subprocess wrappers | New AgentBackend subclass |
| L0 | Substrate | Workspace, Blender subprocess, config | Config overrides |
topos make End-to-End FlowThe CLI bootstraps a workspace, generates a plan, and hands off to the Runner. topos run <slug> skips everything before Runner.
flowchart LR
USER["user prompt\n+ images"] --> MAKE["cli.py\nmake()"]
MAKE -->|"create"| WS["workspace\noutputs/slug/"]
MAKE -->|"generate"| PLAN["plan.json"]
PLAN --> RUN["Runner.run()"]
RUN -->|"agent tasks"| SRC["src/\nPython source"]
RUN -->|"tool tasks"| ART["artifacts/\nglb · urdf · png"]
RUN --> REPORT["run_report.json"]
topos make top-level flow. The Runner is the only component that executes tasks; the CLI just wires the inputs.The generated plan.json for an articulated object uses this sequence. The subgraph fan-out is resolved at runtime after the design agent writes design.json.
The Runner topo-sorts the task graph, dispatches ready tasks in parallel, and checks early-stop conditions after each complete iteration.
flowchart TD
LOAD["load plan.json\ntopo-sort tasks"]
READY["find ready tasks\n(deps all done)"]
RUN["execute in parallel\nThreadPoolExecutor"]
RECORD["record TaskResult\n(success, cost, output)"]
SKIP["cascade-skip\ndownstreams"]
MORE{"more tasks?"}
CHECK{"early-stop?"}
FIX["build fix agents\nfrom judge feedback"]
REPORT["RunReport"]
LOAD --> READY --> RUN --> RECORD
RECORD -->|"task failed"| SKIP --> MORE
RECORD -->|"task ok"| MORE
MORE -->|"yes"| READY
MORE -->|"no"| CHECK
CHECK -->|"judge pass"| REPORT
CHECK -->|"max iters"| REPORT
CHECK -->|"regression"| REPORT
CHECK -->|"saturation"| REPORT
CHECK -->|"continue"| FIX --> READY
| Trigger | Condition | Default |
|---|---|---|
judge_pass | All judges report passed | active |
max_global_iters | Hit iteration budget ceiling | 3 |
| regression | Assembly score dropped vs previous iter | always on |
| saturation | No judge improved ≥ min_improvement | 0.05 |
Deterministic tools (export, verify) are skipped on fix iters when inputs haven't changed. Stochastic tools (judge, texture gen) always re-run.
Every DAG node is one of three kinds. The Runner dispatches each kind differently.
AgentTask — "kind": "agent"
Spawns an LLM agent (claude/gemini/codex CLI) as a subprocess. The agent receives a goal prompt, skills, and write access to src/. It reads and writes files autonomously using its built-in file tools.
topos:articulated/designer.md.j2)ToolTask — "kind": "tool"
Calls a registered Python function from the @tool registry. No LLM — pure deterministic or stochastic computation. Blender renders, exports, and rubric evaluation all run as ToolTasks.
@tool decoratorSubgraphTask — "kind": "subgraph"
Runtime fan-out. After deps complete, reads a file and calls an expander strategy that emits child tasks with namespaced IDs (02__10_agent_part_frame). The subgraph completes when all children succeed. Part count is not known until design.json is read.
The design agent decides how many parts exist. The subgraph expander reads that decision and materializes per-part agents dynamically. Independent part agents run in parallel and never communicate — they share only design.json.
flowchart TD
DESIGN["01 design agent\nsrc/design.json"]
SG["02 subgraph\nexpand from design.json"]
P1["part agent — Frame\nsrc/parts/Frame.py"]
P2["part agent — DrawerFront\nsrc/parts/DrawerFront.py"]
P3["part agent — Handle\nsrc/parts/Handle.py"]
J1["judge — Frame"]
J2["judge — DrawerFront"]
J3["judge — Handle"]
VER["verify_parts\nBlender import check"]
BUILD["03 build agent\nsrc/build.py"]
JOINTS["04 joints agent\nsrc/joints.yaml"]
DESIGN --> SG
SG --> P1 & P2 & P3
P1 --> J1
P2 --> J2
P3 --> J3
J1 & J2 & J3 --> VER
VER --> BUILD
DESIGN -.->|"parallel"| JOINTS
The design agent writes this file. All subsequent agents read it; none write to it. The bbox field is enforced by build.py at a 5 mm tolerance — independent agents stay aligned without inter-agent communication.
When the assembly judge scores below threshold, the fix loop synthesizes targeted repair agents from judge feedback. Only failing parts and downstream tasks re-run — not the whole pipeline.
flowchart LR
JUDGE["judge\n(assembly)"]
PASS{"passed?"}
DONE["stop\njudge_pass ✓"]
FIX["fix agent\nfeed judge feedback\nas goal prompt"]
BUILD["build agent\nre-stitch assembly"]
RENDER["render\n8 new views"]
HALT{"regression or\nsaturation or\nmax iters?"}
STOP2["halt"]
JUDGE --> PASS
PASS -->|"yes"| DONE
PASS -->|"no"| FIX
FIX --> BUILD --> RENDER --> JUDGE
JUDGE --> HALT
HALT -->|"yes"| STOP2
HALT -->|"no"| PASS
The fix agent receives a rendered Jinja2 prompt (fix_loop.md.j2) with the judge's exact feedback and actionable steps:
AgentTasks run as CLI subprocesses. The backend manages subprocess lifecycle, stream-json parsing, watchdog timeouts, and retry logic. The agent never has API access — it only sees src/ via file tools.
flowchart LR
TASK["AgentTask\ndispatched by Runner"]
PROMPT["build prompt\nsystem base\n+ goal\n+ skills"]
SPAWN["spawn subprocess\nclaude -p prompt\n--output-format stream-json\n--add-dir src/"]
PARSE["parse stream-json\nassistant · tool_use\ntool_result · result"]
WD{"idle >\ngrace_s?"}
KILL["kill\ntimeout"]
DONE["exit 0\ncompleted"]
RETRY["retry?\nquota → backoff\ntimeout → once"]
RESULT["AgentRunResult\nsuccess · cost · files_modified"]
TASK --> PROMPT --> SPAWN --> PARSE
PARSE --> WD
WD -->|"no"| PARSE
WD -->|"yes"| KILL --> RETRY --> SPAWN
PARSE -->|"result event"| DONE --> RESULT
rate_limit_event is excluded from the watchdog — quota waiting does not reset the idle clock.1 — System base
prompts/system/coding_agent_base.md
Framework rules for every agent: write only in src/, no from topos... imports, bbox contract rules.
2 — Task goal
From plan.json goal / goal_file / goal_template
Task-specific instruction. Designer gets designer.md.j2; fix agents get fix_loop.md.j2 with judge feedback injected.
3 — Skills (soft)
topos/skills/topos_*/SKILL.md
Capability bundles. Agent reads only those whose when_to_use matches the current task.
Tools invoke Blender as a subprocess. Agents never call Blender directly — they only write Python source files that Blender later executes.
The judge ToolTask loads a rubric YAML, picks a pluggable Critic backend, and scores rendered images. The score and per-criterion feedback drive the fix loop.
flowchart LR
TOOL["judge tool\nimage_pattern + rubric"]
RUBRIC["load rubric YAML\ncriteria · weights\npass_threshold"]
IMAGES["collect renders\nview_000…007.png"]
CRITIC["critic backend\nclaude_vision\ngemini · openai"]
SCORE["score per criterion\n0–1 + feedback text\n+ suggested_fixes"]
AGG["weighted average\ncheck min_floors"]
RESULT["CriticResult\npassed · overall_score\nsuggested_fixes"]
TOOL --> RUBRIC & IMAGES
RUBRIC & IMAGES --> CRITIC --> SCORE --> AGG --> RESULT
Every agent and tool communicates only through files. No direct agent-to-agent calls. The fix loop closes the cycle by feeding scores back to source files.
flowchart LR
PROMPT["user prompt\n+ references"]
DESIGN["src/design.json"]
PARTS["src/parts/*.py"]
BUILD["src/build.py"]
JOINTS["src/joints.yaml"]
RENDERS["artifacts/view_*.png"]
GLB["artifacts/object.glb"]
URDF["artifacts/object.urdf"]
SCORES["judge scores\n+ suggested_fixes"]
PROMPT -->|"01 design agent"| DESIGN
DESIGN -->|"02 part agents"| PARTS
DESIGN & PARTS -->|"03 build agent"| BUILD
DESIGN -->|"04 joints agent"| JOINTS
BUILD -->|"05 render"| RENDERS
BUILD -->|"06 export"| GLB
BUILD & JOINTS -->|"07 export"| URDF
RENDERS -->|"08 judge"| SCORES
SCORES -->|"fix loop"| PARTS
SCORES -->|"fix loop"| BUILD
Every run writes into one slug-named directory under outputs/. No dated subdirs, no extra top-level folders.
| Path ▲ | Role | Extend by |
|---|---|---|
topos/cli.py | Entry point — topos make/run/doctor/cost/skill | New CLI subcommand |
topos/orchestrator/runner.py | DAG execution, fix-loop dispatch, early-stop | Closed — core |
topos/orchestrator/plan_schema.py | plan.json Pydantic validation + topo-sort | Closed — core |
topos/orchestrator/plan_generator.py | Generates articulated plan.json from prompt | Add domain generator |
topos/orchestrator/expand.py | Subgraph expander strategies (ADR-0008) | Register new expander |
topos/orchestrator/fix_loop.py | Builds FIX agent tasks from judge feedback | Closed — core |
topos/backends/claude_cli.py | ClaudeCLI subprocess, stream-json, watchdog, retry | New AgentBackend subclass |
topos/tools/registry.py | @tool decorator + ToolSpec lookup | Add @tool function |
topos/tools/judge.py | Judge ToolTask: loads rubric, invokes Critic | New rubric YAML |
topos/tools/_blender_subprocess.py | Stateless blender --background runner | Closed — substrate |
topos/agents/visual_critic/ | Critic protocol + backends (claude, gemini, openai) | New Critic subclass |
topos/prompts/system/ | Base system prompts — all agents | Edit with care |
topos/prompts/articulated/ | Domain Jinja2 goal templates | Add domain prompt dir |
topos/skills/topos_*/ | SKILL.md capability bundles — agent soft-hints | New SKILL.md dir |
topos/rubrics/ | Rubric YAML (criteria, weights, thresholds) | Add rubric YAML |
examples/ | Example workspaces with plan.json + per-example prompts | Add example dir |
Click any column header to sort.
| ADR | Decision |
|---|---|
| 0001 | Code-as-truth — Python source authoritative; 3D artifacts are derivative |
| 0002 | Stateless Blender — blender --background per build; hot pool is cache only |
| 0003 | Claude CLI backend — agents run as subprocesses, not API calls |
| 0004 | Recipe injection — domain knowledge via SKILL.md, not hard-coded prompts |
| 0005 | Modeling vs rendering — agents write geometry code; tools invoke Blender |
| 0006 | design.json bbox contract — independent agents coordinate via shared spec file |
| 0007 | Three-layer prompts — system / domain / per-example layering |
| 0008 | Subgraph runtime expansion — part count resolved from design.json at runtime |