Renderer Architecture
Advanced topic. This page documents Smithers internals. You don’t need to understand this to use Smithers effectively.
SmithersNode trees instead of DOM elements. These trees represent AI agent orchestration plans that can be serialized to XML and executed.
JSX to Execution Pipeline
Copy
Ask AI
┌─────────────────────────────────────────────────────────────────────────────────┐
│ YOUR JSX CODE │
│ │
│ function MyAgent() { │
│ const { ralphCount } = useSmithers() │
│ return ( │
│ <Orchestration> │
│ <Phase name="build"> │
│ <Claude> │
│ Fix the bug in auth.ts (iteration {ralphCount}) │
│ </Claude> │
│ </Phase> │
│ </Orchestration> │
│ ) │
│ } │
└─────────────────────────────────────────────────────────────────────────────────┘
│
│ Babel/TypeScript transforms JSX
▼
┌─────────────────────────────────────────────────────────────────────────────────┐
│ JSX RUNTIME (jsx-runtime.ts) │
│ │
│ // CUSTOM wrapper around React's JSX runtime that exposes React's key: │
│ import { jsx as reactJsx } from 'react/jsx-runtime' │
│ │
│ function withSmithersKey(props, key) { │
│ if (key == null) return props │
│ return { ...props, __smithersKey: key } // Inject key as prop │
│ } │
│ │
│ export function jsx(type, props, key) { │
│ return reactJsx(type, withSmithersKey(props, key), key) │
│ } │
│ │
│ // This allows SmithersNode.key to be populated from React's key │
│ // for plan serialization (key="0" appears in XML output). │
└─────────────────────────────────────────────────────────────────────────────────┘
│
│ React processes component tree
▼
┌─────────────────────────────────────────────────────────────────────────────────┐
│ REACT INTERNALS (Fiber Tree) │
│ WE DON'T OWN THIS │
│ │
│ React builds an internal "fiber" tree that tracks: │
│ - Component instances and their state (useState, useEffect, etc.) │
│ - Which nodes changed and need updates │
│ - Parent/child/sibling relationships │
│ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ FiberNode (MyAgent) │ │
│ │ ├─ hooks: [{state: 0, setState: fn}] ← useState lives here │ │
│ │ └─ child ─→ FiberNode (Ralph) │ │
│ │ ├─ key: 0 │ │
│ │ └─ child ─→ FiberNode (Phase) │ │
│ │ └─ child ─→ FiberNode (Claude) │ │
│ │ └─ hooks: [{state:'pending'}] │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
│ │
│ When state changes (setCount), React: │
│ 1. Marks affected fibers as "needs update" │
│ 2. Re-renders components to get new elements │
│ 3. Diffs old vs new to find what changed │
│ 4. Calls our HOST CONFIG methods to apply changes │
└─────────────────────────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────┼─────────────────────────────┐
│ │ │
▼ ▼ ▼
createInstance() appendChild() commitUpdate()
createTextInstance() removeChild() commitTextUpdate()
insertBefore()
│ │ │
└─────────────────────────────┼─────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────────┐
│ HOST CONFIG (host-config.ts) │
│ WE OWN THIS │
│ │
│ This is the "bridge" between React and our SmithersNode tree. │
│ React calls these methods; we implement them. │
│ │
│ const hostConfig = { │
│ createInstance(type, props) { │
│ // React says "make a <phase>" → we create SmithersNode │
│ return rendererMethods.createElement(type) │
│ }, │
│ appendChild(parent, child) { │
│ // React says "put child under parent" │
│ rendererMethods.insertNode(parent, child) │
│ }, │
│ commitUpdate(instance, payload) { │
│ // React says "props changed on this node" │
│ Object.assign(instance.props, payload) │
│ }, │
│ } │
└─────────────────────────────────────────────────────────────────────────────────┘
│
│ delegates to
▼
┌─────────────────────────────────────────────────────────────────────────────────┐
│ RENDERER METHODS (methods.ts) │
│ WE OWN THIS │
│ │
│ Low-level operations on SmithersNode trees. │
│ No React dependency - pure data structure manipulation. │
│ │
│ rendererMethods = { │
│ createElement(type) → SmithersNode │
│ createTextNode(text) → SmithersNode │
│ setProperty(node, key, value) │
│ insertNode(parent, child, anchor?) │
│ removeNode(parent, child) │
│ } │
└─────────────────────────────────────────────────────────────────────────────────┘
│
│ creates/manipulates
▼
┌─────────────────────────────────────────────────────────────────────────────────┐
│ SMITHERS NODE TREE (types.ts) │
│ WE OWN THIS │
│ │
│ interface SmithersNode { ┌───────────────────────────────────┐ │
│ type: string │ ROOT │ │
│ props: Record<string, unknown> │ └─ ralph {key:0} │ │
│ children: SmithersNode[] │ └─ phase {name:"build"} │ │
│ parent: SmithersNode | null │ └─ claude {status:"..."} │ │
│ key?: string | number │ └─ TEXT "Fix the bug" │ │
│ } └───────────────────────────────────┘ │
│ │
│ This is our "virtual DOM" - a plain JS object tree │
│ that represents the current state of the agent plan. │
└─────────────────────────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────┴─────────────────────────────┐
│ │
▼ ▼
┌─────────────────────────────────┐ ┌─────────────────────────────┐
│ SERIALIZER (serialize.ts) │ │ COMPONENT EXECUTION │
│ │ │ │
│ Converts tree to XML for │ │ Claude, Ralph, Phase │
│ display and approval: │ │ components use useMount │
│ │ │ to execute on render: │
│ <ralph key="0"> │ │ │
│ <phase name="build"> │ │ useMount(() => { │
│ <claude status="pending"> │ │ // Call Claude API │
│ Fix the bug │ │ // Update state │
│ </claude> │ │ // Trigger re-render │
│ </phase> │ │ }) │
│ </ralph> │ │ │
└─────────────────────────────────┘ └─────────────────────────────┘
File Structure
Copy
Ask AI
src/reconciler/
├── index.ts # Public exports
├── jsx-runtime.ts # JSX transform (jsx, jsxs, Fragment)
├── types.ts # SmithersNode, ExecutionState
├── host-config.ts # React reconciler host config
├── methods.ts # Low-level node operations
├── root.ts # createSmithersRoot(), mount()
├── serialize.ts # Tree → XML serialization
└── hooks.ts # useMount, useUnmount, useMountedState
Key Concepts
JSX Runtime Key Injection
React’skey prop is used for reconciliation but not exposed to components. Smithers injects __smithersKey to preserve keys for XML serialization:
Copy
Ask AI
// When you write:
<MyComponent key={0}>...</MyComponent>
// jsx-runtime transforms to include __smithersKey:
jsx(MyComponent, { __smithersKey: 0, children: ... }, 0)
// SmithersNode.key is set for serialization:
// <mycomponent key="0">...</mycomponent>
__smithersKey is internal. Use explicit props like iteration if you need access inside components.Key Propagation Flow
Copy
Ask AI
JSX: <phase key="build"> jsx-runtime.ts host-config.ts
────────────────────> { __smithersKey: "build" } ───────────────> node.key = "build"
│
serialize.ts │
<phase key="build"> <────────────────────┘
Host Config
The host config bridges React and SmithersNode trees:| React Method | Smithers Action |
|---|---|
createInstance() | Create SmithersNode |
appendChild() | Insert child node |
commitUpdate() | Update node props |
SmithersNode vs DOM
| React DOM | Smithers Reconciler |
|---|---|
<div> → DOM Element | <phase> → SmithersNode |
element.appendChild() | rendererMethods.insertNode() |
element.style.color = 'red' | node.props.status = 'running' |
| Browser renders pixels | Serializer outputs XML |
| User clicks trigger events | Claude responses trigger state changes |
The Ralph Wiggum Loop
Copy
Ask AI
┌──────────────────────────────────────────────────────────────────────────────┐
│ THE RALPH WIGGUM LOOP │
│ │
│ 1. Initial render (managed by SmithersProvider) │
│ ┌─────────────────────┐ │
│ │ <ralph iteration={0}>│ ← intrinsic element (serialized to XML) │
│ │ <Claude /> │ ← React component │
│ └─────────────────────┘ │
│ │ │
│ ▼ │
│ 2. Claude mounts, executes, signals completion │
│ Completion triggers: ralphCount++ in SmithersProvider │
│ │ │
│ ▼ │
│ 3. State change triggers re-render with new iteration │
│ ┌─────────────────────┐ │
│ │ <ralph iteration={1}>│ ← iteration incremented │
│ │ <Claude /> │ │
│ └─────────────────────┘ │
│ │ │
│ ▼ │
│ 4. Components can react to iteration changes and re-execute │
│ Loop continues until max iterations or explicit completion │
│ │
│ Note: React's `key` prop can force remounts but is not passed as │
│ props.key or to host config. Smithers injects __smithersKey for │
│ serialization; treat it as internal and prefer explicit props │
│ (like `iteration`) inside components. │
└──────────────────────────────────────────────────────────────────────────────┘
Key Files Explained
jsx-runtime.ts
Custom JSX runtime that wraps React’s runtime to expose thekey prop. Injects __smithersKey into props so it reaches our reconciler and can be stored on SmithersNode.key for plan serialization.
host-config.ts
Implements React’s reconciler interface. React callscreateInstance(), appendChild(), etc. and we create/modify SmithersNodes.
methods.ts
Pure functions for manipulating SmithersNode trees. No React dependency - useful for testing.root.ts
Creates the root container and providesmount() to render a React tree into it.
serialize.ts
Converts SmithersNode tree to XML string. Handles escaping, indentation, and filters out non-serializable props like callbacks.hooks.ts
Vendored lifecycle hooks from react-use.useMount and useUnmount are clearer than raw useEffect.
Why Not Just Build Objects Directly?
You could build SmithersNode trees manually, but React gives you:- Hooks -
useState,useEffect,useContextfor state management - Reconciliation - Efficient diffing when state changes
- Component model - Reusable, composable pieces
- Lifecycle - Mount/unmount/update hooks for execution timing
- Context - Dependency injection without prop drilling
- Familiar API - Leverage existing React knowledge
Usage
Copy
Ask AI
import { createSmithersRoot } from 'smithers-orchestrator'
const root = createSmithersRoot()
await root.mount(() => <MyAgent />)
const xml = root.toXML()
root.dispose()