158 lines
8.3 KiB
Markdown
158 lines
8.3 KiB
Markdown
# ollie Architecture
|
|
|
|
## Philosophy
|
|
|
|
ollie is a small, extensible core library for building agentic systems. It has no binary of its own — frontends and consumers live in separate repos and import ollie as a dependency. The internal surface area is kept minimal; everything a consumer needs is exported under `pkg/`.
|
|
|
|
## Package Layout
|
|
|
|
```
|
|
pkg/
|
|
agent/ — Core interface, agent loop, session and context management
|
|
backend/ — Backend interface + implementations
|
|
config/ — Agent config struct and loader
|
|
mcp/ — MCP client (concrete)
|
|
tools/ — Server and Dispatcher interfaces
|
|
tools/execute/ — execute.Server: execute_code, execute_tool, execute_pipe
|
|
tools/file/ — file.Server: file_read, file_write, file_edit, file_glob, file_grep
|
|
tools/memory/ — memory.Server: memory_remember, memory_recall
|
|
tools/reasoning/ — reasoning.Server: reasoning_think
|
|
tools/task/ — task.Server: task_plan, task_complete
|
|
|
|
internal/
|
|
sandbox/ — landrun sandbox config and command wrapper
|
|
```
|
|
|
|
### Why internal/sandbox?
|
|
|
|
The sandbox package is an implementation detail of `pkg/tools/execute`. It has no stable public API and no reason to be imported by consumers directly. Keeping it internal prevents accidental coupling.
|
|
|
|
### Why pkg/mcp is concrete (not interfaced)
|
|
|
|
`pkg/mcp` exposes a concrete `Client` rather than an interface. MCP is a well-defined protocol; the client is a thin transport layer and there is currently no need for consumers to swap implementations. This may be revisited.
|
|
|
|
## Extension Points
|
|
|
|
Consumers extend ollie by implementing or composing its interfaces:
|
|
|
|
- **`backend.Backend`** — swap or add LLM backends
|
|
- **`tools.Server`** — add a new tool server (built-in or MCP-backed); all servers are equal
|
|
- **`tools.Dispatcher`** — replace the tool router entirely (e.g. remote dispatcher, mock)
|
|
- **`agent.Core`** — the agent's public API; frontends drive it without knowing internals
|
|
|
|
All tool servers implement the same `tools.Server` interface regardless of whether they are built-in or backed by MCP. There is no special "builtin" concept — `execute.Server` and `file.Server` are registered by name the same way MCP servers are, and are torn down and recreated on `/agent` switches just like MCP connections.
|
|
|
|
Each built-in server package exports a `Decl` function (`func Decl(...) func() tools.Server`) — a parameterized factory that produces a fresh server instance. `tools.NewDispatcherFunc` takes a map of name→Decl result and returns a `func() tools.Dispatcher` suitable for `agent.AgentCoreConfig.NewDispatcher`. `tools.NewServer(client)` wraps an `mcp.Client` as a `tools.Server`.
|
|
|
|
`execute.Decl(cwd string)` and `file.Decl(cwd string)` accept a working directory. Pass `""` to fall back to `os.Getwd()`.
|
|
|
|
`task.Decl(planDir string)` accepts the directory where plan files are written.
|
|
|
|
**Adding a new tool server:**
|
|
1. Implement `tools.Server` in a new package under `pkg/tools/`
|
|
2. Export `func Decl(...) func() tools.Server`
|
|
3. Register the Decl result by name in `tools.NewDispatcherFunc` — no frontend changes needed
|
|
|
|
## Data Flow
|
|
|
|
```
|
|
Frontend
|
|
└── agent.Core.Submit(prompt)
|
|
└── loop.run()
|
|
├── backend.ChatStream() — LLM call
|
|
└── tools.Dispatcher.Dispatch(...) — tool dispatch
|
|
├── "execute" → tools.Server
|
|
│ execute_code / execute_tool / execute_pipe
|
|
├── "file" → tools.Server
|
|
│ file_read / file_write / file_edit / file_glob / file_grep
|
|
├── "memory" → tools.Server
|
|
│ memory_remember / memory_recall
|
|
├── "reasoning" → tools.Server
|
|
│ reasoning_think
|
|
├── "task" → tools.Server
|
|
│ task_plan / task_complete
|
|
└── "<servername>" → tools.Server (MCP)
|
|
```
|
|
|
|
All tool calls go through one `tools.Dispatcher.Dispatch` path. Every registered server is a `tools.Server`; the dispatcher routes by name and is agnostic to how any server is implemented.
|
|
|
|
## Built-in Tools
|
|
|
|
Twelve tools across five servers:
|
|
|
|
| Server | Tool | What it does |
|
|
|---|---|---|
|
|
| `execute` | `execute_code` | Runs inline bash in a landrun sandbox |
|
|
| `execute` | `execute_tool` | Reads a named script from `OLLIE_TOOLS_PATH` and runs it sandboxed |
|
|
| `execute` | `execute_pipe` | Chains steps, piping stdout of each into stdin of the next |
|
|
| `file` | `file_read` | Reads a file from the filesystem |
|
|
| `file` | `file_write` | Writes a file to the filesystem |
|
|
| `file` | `file_edit` | Edits a file with precise string replacement |
|
|
| `file` | `file_glob` | Finds files by glob pattern |
|
|
| `file` | `file_grep` | Searches file contents by regex |
|
|
| `memory` | `memory_remember` | Saves a persistent memory to a flat markdown file |
|
|
| `memory` | `memory_recall` | Searches saved memories by keyword |
|
|
| `reasoning` | `reasoning_think` | Externalizes intermediate reasoning (no-op, recorded in history) |
|
|
| `task` | `task_plan` | Decomposes a goal into ordered steps; saves a markdown checklist to pl/ |
|
|
| `task` | `task_complete` | Marks a plan step complete; returns remaining steps |
|
|
|
|
`OLLIE_TOOLS_PATH` defaults to `~/.config/ollie/tools`. The directory can be a symlink or a mountpoint — `execute_tool` treats it as an ordinary filesystem path.
|
|
|
|
`OLLIE_MEMORY_PATH` defaults to `~/.local/share/ollie/memory/`. Memory files follow a denote-like naming convention: `YYYYMMDDTHHmmss--title-slug__tag1_tag2.md`.
|
|
|
|
## Planning
|
|
|
|
`task_plan` saves a markdown checklist to the plan directory (`pl/` in ollie-9p) as a `__todo.md` file. The agent then:
|
|
|
|
1. Renames `__todo` → `__wip` when work begins (`file_edit` or shell)
|
|
2. Calls `task_complete` after each step finishes — marks `[ ]` → `[x]`, returns remaining steps
|
|
3. Uses `file_*` tools to add, remove, or revise steps as the plan evolves
|
|
4. Renames `__wip` → `__done` when the goal is realized
|
|
|
|
`task_complete` takes the plan filename and a step title (substring match, case-insensitive). It errors if no matching unchecked step is found, so silent skips are caught.
|
|
|
|
Everything else — listing plans, inspecting a specific step, bulk operations — goes through `file_*` or `execute_code`. The task server handles only creation and step completion.
|
|
|
|
## Typical Consumer Setup
|
|
|
|
```go
|
|
newDispatcher := tools.NewDispatcherFunc(map[string]func() tools.Server{
|
|
"execute": execute.Decl(cwd), // "" falls back to os.Getwd()
|
|
"file": file.Decl(cwd),
|
|
"memory": memory.Decl(),
|
|
"reasoning": reasoning.Decl(),
|
|
"task": task.Decl(planDir),
|
|
})
|
|
|
|
env := agent.BuildAgentEnv(cfg, newDispatcher(), cwd) // also connects MCP servers from cfg
|
|
|
|
core := agent.NewAgentCore(agent.AgentCoreConfig{
|
|
Backend: be,
|
|
CWD: cwd,
|
|
Env: env,
|
|
NewDispatcher: newDispatcher,
|
|
// ...
|
|
})
|
|
```
|
|
|
|
`BuildAgentEnv` adds MCP servers from the config on top of the pre-registered servers. On `/agent` switches, `NewDispatcher` is called to produce a fresh dispatcher — all servers (built-in and MCP) are torn down and recreated for the new agent config. `CWD` is preserved across switches.
|
|
|
|
`Core.ListServers()` returns all registered tool servers and their tools, grouped by server name. Accessible via the `/mcp` command or `ollie/s/{sid}/mcp` in ollie-9p.
|
|
|
|
## Session and Context
|
|
|
|
`agent.Session` owns the message history. A `contextBuilder` enforces a rolling window to prevent unbounded prompt growth. When the window fills, older messages are either evicted (with a compaction notice injected) or proactively summarized via `/compact`.
|
|
|
|
## Sandboxing
|
|
|
|
`internal/sandbox` wraps commands with [landrun](https://github.com/landlock-lsm/landrun) (Landlock LSM). Configuration is layered:
|
|
|
|
1. Global defaults (`~/.config/ollie/sandbox.yaml`)
|
|
2. Named sandbox overlay (`~/.config/ollie/sandbox/<name>.yaml`)
|
|
|
|
If `superpowerd` is running, commands are additionally wrapped with `superpowers run-session` for privilege scoping (optional, detected at runtime).
|
|
|
|
## Frontends
|
|
|
|
The reference frontend is [ollie-tui](https://github.com/lneely/ollie-tui): a readline-based terminal UI that imports ollie as a library. It is the only consumer of `agent.Core` today, but the interface is intentionally frontend-agnostic.
|