152 lines
5.1 KiB
Markdown
152 lines
5.1 KiB
Markdown
# agent0 Architecture Outline
|
|
|
|
## Core Overview
|
|
agent0 is an **agentic TUI** built with Babashka (Clojure) that interacts with local LLMs via Ollama. It features an Elm-architecture UI with background agent processing.
|
|
|
|
---
|
|
|
|
## File Structure & Responsibilities
|
|
|
|
| File | Responsibility |
|
|
|------|------|
|
|
| `app.clj` | TUI layer - Elm architecture, event handling, rendering |
|
|
| `core.clj` | Agent brain - LLM calls, tool execution, async loop |
|
|
| `context.clj` | Project context loading, skill parsing/expansion |
|
|
| `markdown.clj` | Markdown → ANSI-styled terminal output |
|
|
| `web_search.clj` | Web research subagent configuration |
|
|
| `clojuredocs.clj` | ClojureDocs integration (not shown, imported) |
|
|
|
|
---
|
|
|
|
## Data Flow
|
|
|
|
```
|
|
User Input → Skill Expansion → Agent Loop → LLM → Tools → Events → UI
|
|
```
|
|
|
|
1. **Input** → Optional skill expansion (`/name args`)
|
|
2. **Conversation** → Added to history
|
|
3. **Agent Loop** (async future) → Calls LLM, executes tools, appends results
|
|
4. **Events** → Pushed to event queue every 100ms
|
|
5. **UI** → Polls queue and re-renders
|
|
|
|
---
|
|
|
|
## Key Functions
|
|
|
|
### app.clj (TUI Layer)
|
|
|
|
| Function | Purpose |
|
|
|----------|---------|
|
|
| `view` | Elm view - renders state as hiccup UI |
|
|
| `update-fn` | Elm update - handles events, user input, agent polling |
|
|
| `process-agent-event` | Pushes LLM responses to messages list |
|
|
| `format-messages` | Converts messages to display lines with ANSI styling |
|
|
| `wrap-line` / `wrap-text` | Word-wrap with ANSI awareness |
|
|
| `-main` | Entry point - parses args, loads context/skills, starts TUI |
|
|
|
|
### core.clj (Agent Brain)
|
|
|
|
| Function | Purpose |
|
|
|----------|---------|
|
|
| `run-agent-loop!` | Main async loop - iterates: call LLM → execute tools → repeat (max 50) |
|
|
| `call-llm*` | HTTP POST to Ollama `/api/chat` |
|
|
| `call-llm` | Wrapper with tool definitions |
|
|
| `execute-tool` | Runs tool function, extracts diff for UI |
|
|
| `tool-call-label` | Human-readable label for tool calls (displayed as "⚙") |
|
|
| `detect-stuck-loop` | Detects infinite loops (exact repeats + research loops) |
|
|
| `run-subagent!` | Subagent execution (e.g., web_search) |
|
|
| `delegate` | Tool to invoke subagents |
|
|
| `skills-tool` | Tool for listing/running skills |
|
|
| Session persistence: `save-session!`, `load-session`, `new-session-id` | |
|
|
| Tools: `read-file`, `list-files`, `edit-file`, `create-file`, `run-shell-command`, `glob-files`, `grep-files` | File & shell operations |
|
|
|
|
### context.clj (Context & Skills)
|
|
|
|
| Function | Purpose |
|
|
|----------|---------|
|
|
| `load-project-context` | Loads `.agent0/context.md`, `CLAUDE.md`, etc. |
|
|
| `load-skills` | Loads skills from `~/.claude/skills/`, `~/.config/agent0/skills.md`, `.agent0/skills.md` |
|
|
| `parse-skills` | Parses `/name <arg>` templates with `{param}` substitution |
|
|
| `expand-skill` | Expands skill invocations like `/test --watch` |
|
|
| `format-skill-list` | Formats skill list for display |
|
|
|
|
### markdown.clj (Markdown Rendering)
|
|
|
|
| Function | Purpose |
|
|
|----------|---------|
|
|
| `render-markdown` | Converts markdown to ANSI-styled lines |
|
|
| `apply-inline` | Bold, italic, strikethrough, links |
|
|
| `protect-code-spans` / `restore-code-spans` | Preserves inline code |
|
|
| `wrap-words` | ANSI-aware word wrapping |
|
|
|
|
---
|
|
|
|
## Event Types
|
|
|
|
```clojure
|
|
{:type :text :content "..."} ; Assistant response
|
|
{:type :tool :label "..."} ; Tool being called (displayed as "⚙")
|
|
{:type :diff :content "..."} ; File edit preview (diff format)
|
|
{:type :error :message "..."} ; Error message
|
|
{:type :done :conversation [...]} ; Loop finished
|
|
```
|
|
|
|
---
|
|
|
|
## Tool Pattern
|
|
|
|
Tools that modify files return `{:message ".." :diff ".."}`:
|
|
- `:message` → sent to LLM as tool result
|
|
- `:diff` → pushed as separate `:diff` event to UI
|
|
|
|
---
|
|
|
|
## Agent Loop Algorithm
|
|
|
|
```
|
|
1. Call LLM with conversation + tool definitions
|
|
2. If tool_calls in response:
|
|
a. Push tool labels to UI
|
|
b. Execute each tool function
|
|
c. Push diffs to UI (if any)
|
|
d. Append tool results to conversation
|
|
e. Repeat (stuck-loop detection at ~3 repeats)
|
|
3. If no tool_calls:
|
|
a. Push final assistant text
|
|
b. Signal :done
|
|
```
|
|
|
|
**Stuck-loop detection:**
|
|
- Exact repeat: same tool calls 3x in a row → hard stop
|
|
- Research loop: `web_search` called 3+ times with varying args → inject system nudge, stop at 6
|
|
|
|
---
|
|
|
|
## State Management
|
|
|
|
**Model state (UI):**
|
|
```clojure
|
|
{:messages [...] ; Display messages
|
|
:input "" ; User input field
|
|
:conversation [...] ; LLM conversation history
|
|
:event-queue (atom []) ; Background-to-UI events
|
|
:session-id ... ; For persistence
|
|
:agent-running? bool ; Spinner state
|
|
:agent-handle ... ; Future + cancel atom
|
|
:skills {...} ; Available skills}
|
|
```
|
|
|
|
**Sessions saved to:** `~/.local/share/agent0/sessions/{uuid}.edn`
|
|
**Logs saved to:** `~/.local/share/agent0/logs/agent-{timestamp}.log`
|
|
|
|
---
|
|
|
|
## Key Patterns
|
|
|
|
1. **Async agent loop** returns `{:future f :cancel! atom}` for interruption
|
|
2. **TUI uses clojure-tui** hiccup-like syntax with ANSI escape codes
|
|
3. **Polling UI** checks event queue every 100ms (drains and processes)
|
|
4. **Dynamic binding** `*log-file*` passes log context to subagents
|
|
5. **Skills** use `{param}` substitution from `/name <arg>` format
|