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.
-
Event-driven async generator, parallel tool execution, cancellation, retries, the event sequence per turn.
-
The five extension types, the ten lifecycle hooks, the stable-surface contract, the versioning rule.
-
The
EventandStreamEventunions, the sequence per turn,RetryEvent, and how frontends subscribe. -
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<Event>]
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.mdis the design rationale. - Stable surface for plugins → extension model §4.
- CLI flags and env vars → configuration.
- Session JSONL format → sessions.
If a contract here turns out wrong, update the doc and the code in the same change. That's the rule.