Contents
Articraft is a local-first agent harness that turns a prompt into one bounded Python artifact, compiles that artifact into an articulated URDF plus managed assets, then persists the result as a browsable local library record.
This report focuses on control flow and architectural contracts. It intentionally avoids line-by-line code commentary.
The repo is organized around one idea: keep generation, data persistence, SDK authoring, and visual inspection separate enough that each loop has a clear owner.
agent/ owns the LLM loop, provider adapters, tool registry, prompt loading, compile feedback, cost tracking, traces, and single-run lifecycle.
sdk/ owns the authored object model: parts, visuals, materials, articulations, tests, geometry helpers, managed mesh assets, and URDF export.
cli/ is a thin command layer that resolves defaults and routes into the shared runtime or storage operations.
storage/ owns the local data root. Canonical records live under records/<id>/revisions/<rev>/; derived materialization lives under the cache tree.
records_manifest.jsonl is a browse index, not the source of truth. Record folders and revision metadata remain canonical.
viewer/api/ exposes FastAPI endpoints for records, files, runs, status, and lazy materialization.
viewer/web/ is a React, TypeScript, and Three.js application that fetches URDF plus assets, builds a scene graph, and exposes joint controls.
flowchart LR
CLI["CLI"] --> Run["run setup"]
Viewer["viewer"] --> Store["viewer store"]
Run --> Agent["agent harness"]
Agent --> SDK["SDK script"]
Agent --> Storage["records"]
SDK --> URDF["URDF"]
Store --> Cache["materialization"]
Cache --> Web["Three viewer"]
A normal generation run is a record-producing workflow, not just an LLM chat. The CLI resolves provider/model/options, creates a staging context, runs the harness, compiles a valid model, and only then writes a canonical record.
flowchart TD
Prompt["prompt"] --> CLI["generate command"]
CLI --> Context["run context"]
Context --> Scaffold["model scaffold"]
Scaffold --> Harness["agent harness"]
Harness -->|"tool loop"| Model["model.py"]
Model --> Compile["compile_model"]
Compile -->|"passes"| Persist["write record"]
Compile -->|"fails"| Harness
Persist --> Manifest["manifest upsert"]
Persist --> Cache["materialization"]
Cache --> Viewer["viewer"]
Input Contract
The prompt may include text and one image. Provider-specific image validation happens before the agent is invoked.
Authoring Contract
The agent edits one bound model.py. The virtual docs tree is read-only and curated.
Output Contract
Success requires a fresh successful compile for the latest code revision. A plain assistant conclusion before that triggers a compile reminder.
The central loop is in ArticraftAgent.run. It is bounded by max_turns, accumulates usage/cost, records traces, and alternates between provider requests and local tool execution.
flowchart TD
Start["start run"] --> Seed["seed messages"]
Seed --> Turn["next turn"]
Turn --> Prep["prepare request"]
Prep --> LLM["call provider"]
LLM --> Decode["decode response"]
Decode --> Empty{"no action?"}
Empty -->|"yes"| Recover["inject reminder"]
Recover --> Turn
Empty -->|"no"| Finish{"text only?"}
Finish -->|"yes"| Fresh{"fresh compile?"}
Fresh -->|"yes"| Done["success"]
Fresh -->|"no"| NeedCompile["compile reminder"]
NeedCompile --> Turn
Finish -->|"no"| Tools["execute tools"]
Tools --> Guidance["inject guidance"]
Guidance --> Limit{"turn limit?"}
Limit -->|"no"| Turn
Limit -->|"yes"| Failed["max turns"]
generate_with_tools response shape.| Stage | Owner | Purpose | Loop Effect |
|---|---|---|---|
| Seed | agent.tools | Add SDK docs context and runtime guidance. | Starts the first request with the authoring contract loaded. |
| Prepare | Provider adapter | Sync provider-side state, maybe compact context, maybe refresh cache. | Keeps long runs inside provider context limits. |
| Decode | MessageCodec | Extract text, usage, thinking summary, diagnostics, and tool calls. | Normalizes provider responses. |
| No action | Harness | Recover from empty responses. | Injects a final-response or compile-required reminder. |
| Finish | Harness | Accept final text only after latest code compiled. | Prevents unvalidated completion. |
| Tools | Tool registry | Execute read/edit/probe/compile tools locally. | Appends tool result messages and marks mutations. |
| Guidance | GuidanceInjector | Add targeted correction messages after known failure patterns. | Steers the next model turn without changing code itself. |
Click a table header to sort.
The harness receives provider-specific tool calls, turns them into local invocations, executes them, then converts results back into provider-visible tool messages. compile_model is special because it must update compile freshness state.
flowchart TD
Calls["tool calls"] --> Batch{"parallel safe?"}
Batch -->|"yes"| Parallel["parallel batch"]
Batch -->|"no"| Serial["serial batch"]
Parallel --> One["single tool"]
Serial --> One
One --> Args["parse args"]
Args --> Special{"compile_model?"}
Special -->|"yes"| Compile["compile loop"]
Special -->|"no"| Registry["tool registry"]
Registry --> Bind["bind workspace"]
Bind --> Invoke["execute tool"]
Compile --> Result["tool result"]
Invoke --> Result
Result --> Mutate{"mutating?"}
Mutate -->|"yes"| Dirty["revision dirty"]
Mutate -->|"no"| Message["tool message"]
Dirty --> Message
Message --> Conversation["conversation"]
| Tool | Kind | Main Role | Mutates |
|---|---|---|---|
read_file | Info | Reads model.py or curated docs from the virtual workspace. | No |
find_examples | Info | Lexically searches curated SDK examples and returns relevant markdown. | No |
probe_model | Info | Loads the current model and runs a read-only snippet against exact geometry helpers. | No |
replace | Edit | Performs exact string replacement in the bound model.py. | Yes |
write_file | Edit | Replaces the entire bound model.py. | Yes |
apply_patch | Edit | Applies a single-file patch to the bound model.py. | Yes |
compile_model | Validate | Runs the compile/test/export feedback loop and returns structured signals. | No |
OpenAI gets read_file, freeform apply_patch, compile_model, probe_model, and find_examples. Codex CLI gets JSON apply_patch plus replace and write_file. Other tool-calling providers get read_file, replace, write_file, compile_model, probe_model, and find_examples.
Providers are adapters, not alternate harnesses. They convert a shared conversation plus shared tool schema into provider-native requests, then convert native responses back into the harness shape.
flowchart LR
Shared["shared state"] --> Convert["convert request"]
Convert --> Native["native API"]
Native --> Response["native response"]
Response --> Normalize["normalize turn"]
Normalize --> Harness["harness loop"]
Harness --> Shared
The OpenAI adapter keeps canonical Responses API input items, supports incremental requests with previous_response_id, can fall back to full context, tracks prompt cache keys, and compacts old interaction history when context pressure or repeated compile failures make that worthwhile.
Tool calls arrive as normalized function or custom calls. Responses API output is serialized back into the adapter state so the next request can be incremental.
The Gemini adapter keeps a contents list, manages prefix cache lifecycle, uses a shared compaction decision policy, and converts function calls and function responses through gemini_codec.
Gemini is the one path where multiple tool calls may execute in parallel, but only for safe read-only tools: read_file, find_examples, and probe_model.
The Codex CLI adapter deliberately treats Codex as a JSON-turn generator. It invokes codex exec with an output schema, forbids direct file edits by prompt instruction, and feeds returned JSON tool calls back into the normal Articraft tool dispatcher.
It has a character-count compaction loop that summarizes older messages with a separate schema when prompts get large or repeated compile failures plateau.
compile_model is the core sensor. It executes the current script in a managed asset session, validates the authored object, runs tests, exports URDF, and converts success or failure into feedback the model can act on.
flowchart TD
Tool["compile_model"] --> Fresh{"fresh result?"}
Fresh -->|"yes"| Reuse["reuse report"]
Fresh -->|"no"| Worker["compile worker"]
Worker --> Load["run model.py"]
Load --> Tests["run tests"]
Tests --> Export["export URDF"]
Export --> OK{"success?"}
OK -->|"yes"| Checkpoint["checkpoint URDF"]
OK -->|"no"| Signals["failure signals"]
Checkpoint --> Result["tool output"]
Signals --> Result
Reuse --> Result
Result --> Agent["next turn"]
Authored Checks
run_tests() should hold prompt-specific claims: hinge contact, clearances, pose-specific behavior, visible feature placement, and intentional allowances.
Baseline QC
The compiler owns general checks: model validity, one root, mesh assets, isolated parts, disconnected visual islands, and current-pose overlaps.
Signal Bundle
Failures and warnings are rendered as compact signals, including repeated-failure detection and non-blocking geometry warnings when configured.
The LLM is not trusted to decide validity from its own text. It must call local tools, write real Python, and get feedback from the compiler. This turns the model into an author inside a closed-loop program synthesis workflow, where the authoritative judge is executable SDK code plus exact geometry tests.
Record persistence separates canonical authored artifacts from derived viewer assets. This keeps the repo small and allows viewer/API materialization to be refreshed without rewriting the user-authored revision.
flowchart TD
Success["fresh compile"] --> Write["write record"]
Write --> Revision["revision files"]
Write --> Provenance["provenance"]
Write --> Report["compile report"]
Write --> Assets["copy assets"]
Revision --> Record["record.json"]
Report --> Cache["cache tree"]
Assets --> Cache
Record --> Manifest["manifest row"]
| Area | Canonical | Contains | Owner |
|---|---|---|---|
| Record revision | Yes | prompt.txt, model.py, provenance, inputs, traces. | storage.revisions |
| Record metadata | Yes | Display info, active revision, hashes, lineage, ratings, creator metadata. | RecordStore |
| Manifest | No | Search and browse rows derived from records. | library_manifest |
| Materialization | No | model.urdf, compile report, meshes, GLB, viewer assets. | MaterializationStore |
| Run cache | No | Staging scripts, trace output, run result rows. | RunStore |
The external workflow exists for agents outside the internal tool loop. It still uses Articraft-managed record creation and finalization, but the outside harness edits the active revision directly and must compile before finalize.
flowchart TD
User["user prompt"] --> Init["external init"]
Init --> Draft["draft record"]
Draft --> Outside["external agent"]
Outside --> Edit["edit revision"]
Edit --> Compile["articraft compile"]
Compile -->|"fails"| Edit
Compile -->|"passes"| Finalize["external finalize"]
Finalize --> Manifest["manifest upsert"]
creator.mode=external_agent.Do not manually create record directories for external runs. The CLI creates the record skeleton, prompt, revision id, provenance shape, and creator metadata.
The viewer is not part of generation, but it closes the inspection loop. It reads records, serves text/assets, lazily materializes missing derived files, and renders the URDF graph in Three.js.
flowchart TD
Browser["browser"] --> API["viewer API"]
API --> Resolve["file resolver"]
Resolve --> Exists{"file exists?"}
Exists -->|"yes"| Serve["serve file"]
Exists -->|"no"| Mat["materialize"]
Mat --> Compile["compile visual"]
Compile --> Serve
Serve --> Parse["parse URDF"]
Parse --> Scene["scene graph"]
Scene --> Controls["joint controls"]
An engineer rebuilding this architecture should preserve these contracts first; implementation details can vary.
model.py, plus a read-only virtual documentation tree.The most important files for understanding the control flow are below. This is a conceptual map, not a complete index.
| Concept | Files | Responsibility |
|---|---|---|
| Single run | agent/single_run.py, agent/run_context.py | Build storage context, create agent, run harness, persist success or failure. |
| Agent loop | agent/harness.py, agent/harness_codec.py | Bounded turns, tool execution, compile freshness, no-action recovery, cost and trace updates. |
| Guidance | agent/harness_guidance.py, agent/harness_compile.py | Targeted recovery messages, compile-state tracking, repeated-failure handling. |
| Providers | agent/providers/ | Convert shared messages and tool schemas into provider-native APIs and back. |
| Tools | agent/tools/ | Read, edit, probe, example search, and compile tool definitions. |
| Compiler | agent/compiler.py | Execute model script, run tests and baseline QC, export URDF, enforce timeout. |
| Persistence | agent/record_persistence.py, storage/ | Write record metadata, revision files, provenance, compile report, assets, and manifest. |
| SDK core | sdk/_core/v0/, sdk/_docs/ | Object graph, geometry types, assets, tests, docs, and URDF export. |
| Viewer API | viewer/api/ | Browse records, serve files, materialize missing assets, expose traces and metadata. |
| Viewer web | viewer/web/src/ | Parse URDF, build Three.js scene graph, load meshes, expose joint inspection controls. |