Skip to content

title: Architecture description: How letscode is built: the agent loop, the extension model, the contracts that don't drift.


Architecture

This section is for people who want to understand letscode, and especially for those who want to extend it. Each page commits to contracts that should drift less than the code.

  • Agent loop


    Event-driven async generator, parallel tool execution, cancellation, retries, the event sequence per turn.

  • Extension model


    The five extension types, the ten lifecycle hooks, the stable-surface contract, the versioning rule.

  • Events


    The Event and StreamEvent unions, the sequence per turn, RetryEvent, and how frontends subscribe.

  • RPC mode


    letscode --mode rpc: NDJSON over stdio. The Event models are the wire, no translation layer.

Layers

letscode is a three-layer Python application. Each layer is independently usable.

flowchart TD
    subgraph "Frontend layer"
        Basic[Basic TUI<br/>prompt_toolkit + rich]
        Future[Plugin frontends<br/>Textual / web / RPC]
    end
    subgraph "Agent loop layer"
        Loop[agent_loop<br/>AsyncIterator&lt;Event&gt;]
        State[AgentState<br/>messages, tools, queues]
        Hooks[Lifecycle hooks<br/>before/after_tool_call<br/>transform_context<br/>convert_to_llm<br/>render_custom_message<br/>system_prompt<br/>on_event]
    end
    subgraph "LLM client layer"
        Client[LLMClient<br/>openai.AsyncOpenAI wrapper]
    end
    Basic --> Loop
    Future -.->|same contract| Loop
    Loop --> Hooks
    Loop --> State
    Loop --> Client
    Client --> Provider[(OpenAI Chat Completions<br/>endpoint)]
    Plugins[(Pluggy + entry points<br/>tools · commands · skills · frontends · hooks)] -.extend.-> Loop
    Plugins -.extend.-> Basic

The frontend can be the basic terminal one or any plugin-supplied alternative. The agent loop is provider-agnostic by design; the LLM client is the only layer that knows about OpenAI's wire format.

Functional core / imperative shell

Side effects (LLM I/O, file I/O, subprocess) live at the edges. The agent loop is pure-ish: it consumes messages and tools, produces events. State mutation happens in controlled places (the loop's own structure, the steering / follow-up queues).

This shape makes the loop testable end-to-end without ever calling a real LLM. See tests/_helpers.py for the FakeLLMClient that drives the same event stream from canned chunks.

Where the contracts live

  • Type signatures for messages, events, tools, hooks → src/letscode/agent/*.py. 02-design.md is the design rationale.
  • Stable surface for pluginsextension model §4.
  • CLI flags and env varsconfiguration.
  • Session JSONL formatsessions.

If a contract here turns out wrong, update the doc and the code in the same change. That's the rule.