Skip to content

DIN Protocol

DIN (Directed Interaction Notation) is the structured communication protocol between Lethe's LLM and its orchestrator. The LLM reads graph state through DIN-Read (a text projection of the subgraph) and writes operations through DIN-Write (a JSON object with typed operations). This protocol replaces free-form chat with a validated, schema-enforced interaction language.


Why a Structured Protocol

Standard LLM agents communicate through natural language --- the model is asked to "think step by step" and its text output is parsed heuristically. This creates three problems:

  1. Ambiguous intent. "I should look into X" --- is this a task creation, a search query, or a remark? Parsing natural language for structured operations is fragile.

  2. Lost structure. The graph has typed nodes, edges, and constraints. Natural language flattens this structure into prose the orchestrator must reconstruct.

  3. Validation gap. There is no schema to validate against. Malformed output is detected only when downstream code fails.

DIN addresses all three: operations are typed JSON objects validated by Pydantic models, with a repair loop for malformed output. The protocol works in concert with the context composition pipeline (which determines what the LLM sees) and the agent loop (which orchestrates when the LLM is called).

Design constraint: llama.cpp thinking mode

DIN was also motivated by a practical constraint. When llama.cpp runs in thinking mode (extended reasoning), it cannot produce structured output during thinking --- only the final response is controllable. DIN's single-response JSON format works within this constraint.


DIN-Write: LLM to Orchestrator

The LLM produces a JSON object with an operations array:

{
  "operations": [
    {
      "op": "create",
      "id": "t-b2c4",
      "description": "Analyze liquidation mechanism parameters",
      "embed_text": "Aave V3 liquidation health factor threshold parameters",
      "depends": ["t-a3f2"]
    },
    {
      "op": "call",
      "task_id": "t-a3f2",
      "tool": "web_search",
      "params": {"query": "Aave V3 liquidation bonus close factor"}
    },
    {
      "op": "evidence",
      "task_id": "t-f1e8",
      "content": "The governance process requires a two-phase vote...",
      "embed_text": "Aave governance two-phase voting quorum requirements"
    }
  ]
}

The remaining five operation types cover task lifecycle management and evidence consolidation:

{
  "operations": [
    {
      "op": "update",
      "id": "t-a3f2",
      "result": "Liquidation triggers at health factor < 1.0 with a 5% bonus...",
      "embed_text": "Aave V3 liquidation health factor trigger threshold bonus"
    },
    {
      "op": "compose",
      "parent_id": "t-0001",
      "synthesis": "Combining subtask findings: the liquidation mechanism...",
      "embed_text": "Aave V3 liquidation mechanism comprehensive analysis"
    },
    {
      "op": "dead_end",
      "task_id": "t-d4e5",
      "reason": "Flash loan attack vector is mitigated by Chainlink's TWAP",
      "embed_text": "flash loan oracle attack mitigated by TWAP price feed"
    },
    {
      "op": "synthesize",
      "evidence_ids": ["e-7c0b", "e-9a1f", "e-3d4e"],
      "summary": "Three sources confirm: Aave V3 uses per-asset risk parameters...",
      "embed_text": "Aave V3 per-asset risk parameter configuration sources"
    },
    {
      "op": "continue",
      "note": "Need to verify governance timelock on parameter changes"
    }
  ]
}

Operation Types

Operation Required Fields Optional Fields Effect
create id, description, embed_text prompt, antifocus, depends New subtask + SUBTASK_OF edge
update id result, embed_text Update task; providing result marks DONE
evidence task_id, content, embed_text --- New EvidenceNode + PRODUCED edge
call task_id, tool params Execute tool (web_search)
compose parent_id, synthesis embed_text Set parent result from subtask synthesis
dead_end task_id, reason, embed_text --- Mark task FAILED
continue --- note Request another iteration
synthesize evidence_ids (min 2), summary, embed_text --- Compress evidence + SUPERSEDES edges

Discriminated Union

Operations use a discriminated union on the op field, validated by Pydantic:

class LLMResponseSchema(BaseModel):
    operations: list[Operation]

Each variant enforces its own field constraints. CreateTask requires a hex short ID (t-xxxx pattern) and an embed_text for embedding projection. CallTool requires task_id identifying which task the call is for. Synthesize operates on evidence_ids (minimum 2), producing a summary that supersedes the originals.

embed_text Requirement

Every content-producing operation (create, evidence, dead_end, synthesize) requires an embed_text field --- a short semantic projection optimized for the embedding model. This is not optional; the schema rejects operations without it.

The LLM is instructed to write embed_text as a concise declarative summary of the semantic content (minimum 1 character, enforced by schema). This standardized projection ensures consistent behavior in the embedding algebra, regardless of how verbose or conversational the full content is.


DIN-Read: Orchestrator to LLM

The orchestrator serializes the packed context subgraph into an incident format the LLM can parse:

=GOAL "What are the security risks in Aave V3's liquidation mechanism?"
=NOTE Focus on oracle price manipulation vectors.
=WARN "Subtasks:
  t-f1e8 [done] [well-covered] Review access control
    result: "Access control uses role-based permissions..."
  t-c3a1 [pending] [sparse] Analyze oracle dependencies"
=WARN "Parent goal: Comprehensive Aave V3 security assessment"
=WARN "Failed:
  t-d5b2 [failed] Static analysis of Solidity bytecode"
=TASK t-a3f2 "Analyze liquidation mechanism"
  >PROMPT "Focus on health factor calculation and close factor bounds"
  >ANTI "Do not re-examine access control"
  >SUB t-f1e8
  >DEP t-c3a1
>EVI e-7b1c "The health factor threshold triggers liquidation at HF < 1.0.
             Each asset has a liquidation bonus (5-15%) and close factor."
>EVI e-2d9e "Flash loan attacks in 2023 exploited price oracle latency..."
=TASK t-f1e8 "Review access control" st=done
  >RESULT "Access control uses role-based permissions with a 2-day timelock..."
=TOOLS
  web_search

Key serialization rules:

  • Nodes are serialized flat in U-shape position order, not nested
  • Status tags (st=done, st=failed) appear only on terminal tasks; active/pending tasks have no status suffix
  • >SUB and >DEP relationship lines are injected under parent tasks from the edge set
  • >PROMPT and >ANTI appear on VERBATIM tasks that carry these fields
  • =TOOLS lists tool names on indented lines (descriptions are in the system prompt)
  • =WARN lines carry structured context: subtask status with qualitative coverage tags, parent goal, dead-end siblings, and zero-yield hints

Format Elements

Prefix Content
=GOAL User's goal or active task with root goal context
=NOTE Continuation note from the prior iteration's continue operation
=WARN Structured context: subtask status + qualitative coverage tags, parent goal, siblings, zero-yield
=TASK Task with short ID and description; st= suffix only on done/failed tasks
>PROMPT Task's prepared prompt (VERBATIM verbosity only)
>ANTI Task's antifocus text (VERBATIM verbosity only)
>RESULT Terminal task's result text (full for VERBATIM, first sentence for GIST)
>EVI Evidence content (full for VERBATIM, first sentence for GIST)
>SUB Subtask short ID (injected from SUBTASK_OF edges)
>DEP Dependency short ID (injected from DEPENDS_ON edges)
=TOOLS Available tool names on indented lines (descriptions in system prompt)

What DIN-Read Excludes

DIN-Read deliberately omits:

  • Numeric scores --- No goal alignment values, convergence scores, or budget percentages. The LLM should reason about content, not numbers.
  • Embedding distances --- No similarity metrics between nodes.
  • Iteration counts --- No "you are on iteration 7 of 50."
  • Pressure signals --- Budget pressure affects which nodes appear in context (via knapsack scoring), not the text the LLM sees.

This exclusion is the architectural manifestation of the three-layer separation: quantitative control happens in the embedding algebra, not in the prompt.


Parse and Repair

din/parse_json.py implements a two-stage validation pipeline:

flowchart TD
    Raw["LLM Response Text"] --> Extract["Extract JSON<br/><small>find { ... } block</small>"]
    Extract --> Pydantic["Pydantic Validation<br/><small>discriminated union</small>"]

    Pydantic -->|"all valid"| Ops["ParseResult<br/><small>operations + diagnostics</small>"]

    Pydantic -->|"errors"| Diag["Generate Diagnostics<br/><small>per-operation errors</small>"]
    Diag --> Format["format_errors_for_retry()<br/><small>schema + error context</small>"]
    Format --> Retry["LLM Retry<br/><small>halved thinking budget</small>"]
    Retry --> Extract

Diagnostic Types

Diagnostic Meaning
bad_syntax JSON parse failure or structural error
validation Pydantic field constraint violation
unknown_ref References a node ID not in the current graph snapshot

Repair Strategy

When validation fails, format_errors_for_retry() constructs a follow-up prompt containing:

  1. The full JSON schema (so the model has a fresh reference)
  2. The specific errors encountered (field-level, with context)
  3. Instructions to emit the complete corrected JSON

The retry call sends only the system prompt and the error-formatted text as the user message --- the original DIN-Read context is not re-sent, since the model has already done the substantive reasoning and just needs to fix the format. Thinking budget is halved on each retry with exponential backoff.


Internal Operation Types

After Pydantic validation, operations are converted to frozen dataclasses for the apply_ops stage:

@dataclass(frozen=True, slots=True)
class CreateOp:
    description: str
    embed_text: str
    depends_on: tuple[str, ...]

@dataclass(frozen=True, slots=True)
class EvidenceOp:
    task_id: str
    content: str
    embed_text: str

# CreateOp, UpdateOp, EvidenceOp, DeadEndOp,
# CallOp, ComposeOp, ContinueOp, SynthesizeOp

The separation between Pydantic models (API boundary validation) and frozen dataclasses (internal processing) follows the project's typing discipline: Pydantic at boundaries, frozen dataclasses internally. The frozen dataclasses are what apply_ops in the agent loop consumes to mutate the graph.


Schema for Prompt

The JSON schema is generated programmatically from the Pydantic models and included in the LLM's system prompt via the SCHEMA_JSON module constant. This ensures the schema the LLM sees always matches the validation rules. If a new operation type is added to the Pydantic models, the prompt schema updates automatically. The generated schema is embedded in the config/system_prompt template alongside tool descriptions and protocol rules.


Further Reading