Contents

Articraft Harness Repository Summary


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.

1writable agent file
7provider families
5-7tools per provider

This report focuses on control flow and architectural contracts. It intentionally avoids line-by-line code commentary.

Repository Map

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.

Fig. File count by major area from the current checkout.

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"]
          
drag to pan · scroll to zoom
Fig. High-level repository ownership. The runtime writes code, the SDK defines what code can build, storage persists canonical records, and the viewer consumes materialized outputs.

High Level Object Flow

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"]
          
drag to pan · scroll to zoom
Fig. The main path from user prompt to local library record.

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.

# Required generated script shape def build_object_model() -> ArticulatedObject: ... def run_tests() -> TestReport: ... object_model = build_object_model()

Harness Turn Loop

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"]
          
drag to pan · scroll to zoom
Fig. Shared agent turn loop. Provider details are hidden behind a common generate_with_tools response shape.
StageOwnerPurposeLoop Effect
Seedagent.toolsAdd SDK docs context and runtime guidance.Starts the first request with the authoring contract loaded.
PrepareProvider adapterSync provider-side state, maybe compact context, maybe refresh cache.Keeps long runs inside provider context limits.
DecodeMessageCodecExtract text, usage, thinking summary, diagnostics, and tool calls.Normalizes provider responses.
No actionHarnessRecover from empty responses.Injects a final-response or compile-required reminder.
FinishHarnessAccept final text only after latest code compiled.Prevents unvalidated completion.
ToolsTool registryExecute read/edit/probe/compile tools locally.Appends tool result messages and marks mutations.
GuidanceGuidanceInjectorAdd targeted correction messages after known failure patterns.Steers the next model turn without changing code itself.

Click a table header to sort.


Tool Dispatch Loop

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"]
          
drag to pan · scroll to zoom
Fig. Tool execution pipeline. Gemini can run a small safe subset in parallel; most calls are serial to preserve file state.
ToolKindMain RoleMutates
read_fileInfoReads model.py or curated docs from the virtual workspace.No
find_examplesInfoLexically searches curated SDK examples and returns relevant markdown.No
probe_modelInfoLoads the current model and runs a read-only snippet against exact geometry helpers.No
replaceEditPerforms exact string replacement in the bound model.py.Yes
write_fileEditReplaces the entire bound model.py.Yes
apply_patchEditApplies a single-file patch to the bound model.py.Yes
compile_modelValidateRuns the compile/test/export feedback loop and returns structured signals.No
Provider-specific tool exposure

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.


Provider Loops

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
          
drag to pan · scroll to zoom
Fig. Shared provider adapter pattern.

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 Feedback Loop

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"]
          
drag to pan · scroll to zoom
Fig. Compile loop. Freshness is tracked by edit revision; mutating tools dirty the revision, and successful compile marks it fresh.

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.

Why compile feedback matters

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.


Storage And Materialization

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"]
          
drag to pan · scroll to zoom
Fig. Persistence path after a successful run.
AreaCanonicalContainsOwner
Record revisionYesprompt.txt, model.py, provenance, inputs, traces.storage.revisions
Record metadataYesDisplay info, active revision, hashes, lineage, ratings, creator metadata.RecordStore
ManifestNoSearch and browse rows derived from records.library_manifest
MaterializationNomodel.urdf, compile report, meshes, GLB, viewer assets.MaterializationStore
Run cacheNoStaging scripts, trace output, run result rows.RunStore

External Agent Flow

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"]
          
drag to pan · scroll to zoom
Fig. Supported external-agent loop. It avoids fake internal traces and preserves 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.


Viewer Flow

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"]
          
drag to pan · scroll to zoom
Fig. Viewer materialization and rendering loop.

How To Recreate The Control Flow

An engineer rebuilding this architecture should preserve these contracts first; implementation details can vary.

  1. Create a local record store with immutable-enough revisions and a separate derived materialization cache.
  2. Expose one writable artifact to the agent, here model.py, plus a read-only virtual documentation tree.
  3. Require the artifact to export one object graph with a fixed script contract.
  4. Build a small declarative tool registry: read docs, edit artifact, probe artifact, search examples, and compile artifact.
  5. Normalize provider output into one shape: assistant text, tool calls, usage, diagnostics, and optional thinking summary.
  6. Run a bounded turn loop that refuses final success until the latest artifact revision has compiled.
  7. Track edit revisions and compile freshness, so a compile result cannot validate stale code.
  8. Make compile feedback executable and structured: run authored tests, baseline QC, export validation, timeout handling, and signal rendering.
  9. Persist only after success: prompt, model, provenance, traces, URDF, copied referenced assets, compile report, metadata, and manifest row.
  10. Let the viewer lazily rebuild derived outputs when missing or stale, using fingerprints rather than trusting timestamps alone.

Source Map

The most important files for understanding the control flow are below. This is a conceptual map, not a complete index.

ConceptFilesResponsibility
Single runagent/single_run.py, agent/run_context.pyBuild storage context, create agent, run harness, persist success or failure.
Agent loopagent/harness.py, agent/harness_codec.pyBounded turns, tool execution, compile freshness, no-action recovery, cost and trace updates.
Guidanceagent/harness_guidance.py, agent/harness_compile.pyTargeted recovery messages, compile-state tracking, repeated-failure handling.
Providersagent/providers/Convert shared messages and tool schemas into provider-native APIs and back.
Toolsagent/tools/Read, edit, probe, example search, and compile tool definitions.
Compileragent/compiler.pyExecute model script, run tests and baseline QC, export URDF, enforce timeout.
Persistenceagent/record_persistence.py, storage/Write record metadata, revision files, provenance, compile report, assets, and manifest.
SDK coresdk/_core/v0/, sdk/_docs/Object graph, geometry types, assets, tests, docs, and URDF export.
Viewer APIviewer/api/Browse records, serve files, materialize missing assets, expose traces and metadata.
Viewer webviewer/web/src/Parse URDF, build Three.js scene graph, load meshes, expose joint inspection controls.
Repository counts used in the chart
agent: 78 files sdk: 186 files viewer: 123 files tests: 67 files storage: 15 files cli: 6 files