Skip to content

🧠 Brain and 🤚 Hand

Trails separates reasoning (what to do) from execution (doing it) across every major subsystem. This isn't accidental — it's the framework's dominant architectural idiom.

Notation

Throughout the guides, look for these markers:

  • 🧠 Brain — the planning/reasoning component
  • 🤚 Hand — the execution/action component

Why This Matters

The split buys three things:

  1. Testability — mock the hand, unit-test the brain. Verify planning logic without touching a database or calling an LLM.
  2. Pluggability — swap the hand without rewriting the brain. Different storage backends, LLM providers, or vector stores slot in without changing planning logic.
  3. Observability — track planning cost (LLM tokens, CPU time for query building) separately from execution cost (SPARQL queries, API calls). The CostScope nesting in ADR-0012a is designed around this split.

Where It Shows Up

Agent Planners

The most visible instance. Three strategies, one split.

graph LR
    subgraph brain["🧠 Brain"]
        PLAN["LLM produces<br/>{thought, action, action_input}<br/>or full step list"]
    end

    subgraph hand["🤚 Hand"]
        EXEC["trails.invoke()<br/>dispatches to<br/>capability handlers"]
    end

    brain -->|"action + input"| hand
    hand -->|"observation"| brain

    style brain fill:#7c4dff20,stroke:#7c4dff
    style hand fill:#ff910020,stroke:#ff9100
Strategy 🧠 Brain 🤚 Hand
ReAct One LLM call per step: {thought, action, action_input} invoke() dispatch, observation fed back
Plan-and-Execute One LLM call produces ordered step list with rationales Execute each step, replan on failure
Reflexion Critic LLM evaluates answer quality Re-execute with revised strategy

All three reuse discover_tools() for the brain's catalog and the same invoke() dispatcher for the hand. The strategies differ only in planning style.


Schema Transformation

graph LR
    subgraph brain["🧠 plan_transform()"]
        M1["Exact name match"]
        M2["Fuzzy match"]
        M3["Type coercion"]
        M4["LLM suggestion"]
        M1 --> M2 --> M3 --> M4
    end

    TP["TransformPlan<br/>(serializable JSON)"]

    subgraph hand["🤚 execute_transform()"]
        SPARQL["Run SPARQL CONSTRUCT"]
        APPLY["Apply to KG store"]
        SPARQL --> APPLY
    end

    brain --> TP --> hand

    style brain fill:#7c4dff20,stroke:#7c4dff
    style hand fill:#ff910020,stroke:#ff9100

The TransformPlan object is serializable — you can log it, cache it, replay it, or send it to another instance for execution. The brain and hand can run on different machines.


ORM QueryBuilder

# 🧠 Brain: build the query plan
notes = (
    Note.where(tag="urgent")
        .annotate(comment_count=Count("comments"))
        .order_by("-created_at")
        .limit(10)
)

# 🤚 Hand: materialize and execute
results = await notes.afetch(ctx)

The QueryBuilder accumulates filters, projections, and sort orders as a plan object. Nothing touches the store until .fetch(ctx) or .afetch(ctx) materializes the plan into SPARQL and executes it.


Ingestion Pipeline

Phase Role What happens
🧠 Selection MIME detection, hash-based dedup, strategy choice Decides how to process each file
🤚 Extraction Extractors, chunkers, load_document() Actually reads the file and creates KG nodes

When an rml_mapping is supplied, the brain delegates to the RML engine — which is itself a brain/hand system (generate mapping → run mapping).


Vector Retrieval

graph TD
    subgraph brain["🧠 retrieve() dispatcher"]
        DEC{mode?}
        DEC -->|graph| G["SPARQL filter plan"]
        DEC -->|vector| V["Embedding plan"]
        DEC -->|hybrid| H["Filter-then-rank<br/>strategy"]
    end

    subgraph hand["🤚 Execution"]
        GE["_retrieve_graph()<br/>Execute SPARQL"]
        VE["_retrieve_vector()<br/>Embed + search"]
        HE["_retrieve_hybrid()<br/>SPARQL → vector rerank"]
    end

    G --> GE
    V --> VE
    H --> HE

    style brain fill:#7c4dff20,stroke:#7c4dff
    style hand fill:#ff910020,stroke:#ff9100

The hybrid mode's filter-then-rank is a brain decision documented in the retrieval docstring. Three different hands execute the three modes.


Runtime Dispatch

Every invoke() call passes through the brain before reaching the hand:

sequenceDiagram
    participant C as Caller
    participant B as 🧠 Brain
    participant H as 🤚 Hand

    C->>B: invoke("notes.create", title="...")
    B->>B: Lookup capability handler
    B->>B: Validate arguments
    B->>B: Cedar policy check
    B->>B: Open cost envelope
    B->>H: Call handler function
    H-->>B: Result + cost
    B-->>C: Response + provenance

More Brain/Hand Splits

Subsystem 🧠 Brain 🤚 Hand
RML Mapping generate_mapping() + validate_mapping() — infer types, produce RML Turtle run_mapping() — delegate to Morph-KGC, load with PROV
Enrichment Discover unfilled placeholders, match against @enrichment registry Execute enrichment functions, persist results
Federation SPARQL validation, query form detection, Cedar policy ctx.kg.query() + result serialization
Auto-Ontology onto_infer analyzes data patterns, onto_generate uses LLM Apply inferred types to KG

Design Consequences

Plans Are Serializable

TransformPlan, PlanResult, QueryBuilder state — these are all data structures you can log, cache, or replay. This enables:

  • Audit trails — provenance records what was planned, not just what ran
  • Dry runs — generate the plan, inspect it, approve it, then execute
  • Distributed execution — brain on one machine, hand on another

Hands Are Pluggable

The same brain logic works with different backends:

Layer Pluggable backends
KG store Oxigraph, Fuseki, Qlever
LLM Anthropic, OpenAI, Ollama, Mock
Vector SqliteVec, Qdrant
RML engine Morph-KGC

Switching backends is a config change, not a code change. The brain never knows which hand it's talking to.

Cost Tracking Lives in the Brain

The brain opens a CostScope before delegating to the hand. Planning tokens (LLM reasoning calls) and execution tokens (capability-internal LLM calls) are tracked in nested scopes with dedup via call_id / parent_call_id (ADR-0012a).


Opportunities for Sharper Separation

Some subsystems still couple the brain and hand tighter than necessary:

Area Current state Possible improvement
SHACL validation Inline at ctx.kg.add() time Explicit validation phase before write
Vector indexing Embedding prep and store insertion not cleanly split Explicit IndexPlan object
Policy + dispatch Interleaved in runtime.py Separate "can this principal act?" from "execute if yes"

These are not bugs — they work fine. But making the plan/execute split explicit everywhere would make the framework more consistent and the architecture more legible.