diff --git a/core b/core index b194968..52999c8 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit b194968851774fd57a554f002abc9eaf2e7367c4 +Subproject commit 52999c8560d6147d1d153ed9dcc9ed0537add571 diff --git a/doc/ARCHITECTURE.md b/doc/ARCHITECTURE.md new file mode 100644 index 0000000..a07d774 --- /dev/null +++ b/doc/ARCHITECTURE.md @@ -0,0 +1,162 @@ +# 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, tool definitions (builtin.go) + tools/execute/ — execute.Server: execute_code, execute_tool, execute_pipe + tools/reasoning/ — reasoning.Server: reasoning_think, reasoning_plan + +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) +- **task backend (MCP)** — any MCP server that exposes `task_create` is automatically wired as the persistence backend for `reasoning_plan` by `BuildAgentEnv`; no consumer code required. See [9beads-mcp](https://github.com/lneely/9beads-mcp) for the reference implementation and the interface contract. +- **fallback plan backend** — consumers can supply a `tools.PlanBackend` via `agent.WithFallbackPlanBackend` that is used when no `task_create` MCP tool is found. ollie-9p uses this to enqueue plan steps into the session queue. +- **`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(workdir string)` accepts a working directory that is set as `cmd.Dir` for sandboxed commands and used to expand `{CWD}` in the sandbox config. Pass `""` to fall back to `os.Getwd()`. + +**Adding a new tool server:** +1. Implement `tools.Server` in a new package under `pkg/tools/` +2. Export `func Decl(...) func() tools.Server` +3. Add tool definitions to `pkg/tools/builtin.go` (avoids import cycles) +4. 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 + ├── "reasoning" → tools.Server + │ reasoning_think + │ reasoning_plan + │ └── tools.PlanBackend (optional, priority order) + │ 1. dispatchPlanBackend → task_create (MCP) + │ 2. WithFallbackPlanBackend (e.g. queuePlanBackend) + │ 3. nil → in-context plan only + └── "" → tools.Server +``` + +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 + +Five tools are registered across two 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 | +| `reasoning` | `reasoning_think` | Externalizes intermediate reasoning (no-op, recorded in history) | +| `reasoning` | `reasoning_plan` | Decomposes a goal into ordered steps; persists to task backend if available | + +File operations go through `execute_code` using standard shell tools (`cat`, `grep`, `sed`, `ed`, `ssam` if plan9port is available, etc.). + +Tool definitions live in `pkg/tools/builtin.go` to avoid import cycles — the subpackages import `pkg/tools`, so `pkg/tools` cannot import them back. + +`OLLIE_TOOLS_PATH` defaults to `~/.local/share/ollie/tools`. The directory can be a symlink or a mountpoint — `execute_tool` treats it as an ordinary filesystem path. + +## Planning and Task Persistence + +`reasoning_plan` is a meta-cognitive tool for executive planning. It decomposes a goal into a dependency graph of steps before execution begins. If a task backend is available, the plan is committed to persistent storage. + +The task backend is any MCP server that exposes a `task_create` tool. `BuildAgentEnv` scans available tools after connecting MCP servers: if `task_create` is found, it wires a `dispatchPlanBackend` to the reasoning server's `Plan` field via the `tools.PlanBackendSetter` interface. If not found, it falls back to any `PlanBackend` supplied via `WithFallbackPlanBackend`. If neither is present, `reasoning_plan` produces an in-context plan only. + +ollie-9p supplies a `queuePlanBackend` fallback for every session: when `task_create` is absent, plan steps are enqueued to the session's `enqueue` file in topological order and returned as placeholder IDs (`q1`, `q2`, …). This implementation lives in `9p` because it has a hard dependency on the 9P filesystem layout; the extension point itself (`tools.PlanBackend` + `WithFallbackPlanBackend`) remains in core. + +The reference task backend is [9beads-mcp](https://github.com/lneely/9beads-mcp), which wraps the [9beads](https://github.com/lneely/9beads) 9P task server. + +### Task interface contract + +Auto-wiring requires only `task_create`. The minimum viable contract is: + +| Tool | Required | Purpose | +|---|---|---| +| `task_create` | yes | Create a task; must return a plain-text ID | +| `task_delete` | recommended | Remove tasks when a plan is aborted or superseded | +| `task_list` | optional | Orient the agent across sessions | +| `task_read` | optional | Inspect a specific task | +| `task_update` | optional | Claim, complete, fail, defer, label tasks | +| `task_edit` | optional | Revise title, body, or parent of an existing task | +| `task_dep` | optional | Add or remove blocking dependencies between tasks | + +Ollie degrades gracefully: if a tool is absent, the agent falls back to `execute_code` (shell-out) or skips that lifecycle step. The only hard requirement for persistent planning is `task_create` returning an ID in the `{"content": [{"type": "text", "text": ""}]}` format. + +See [PLANNING.md](PLANNING.md) for the full design rationale. + +## Typical Consumer Setup + +```go +newDispatcher := tools.NewDispatcherFunc(map[string]func() tools.Server{ + "execute": execute.Decl(workdir), // "" falls back to os.Getwd() + "reasoning": reasoning.Decl(), +}) + +env := agent.BuildAgentEnv(cfg, newDispatcher(), workdir) // also connects MCP servers from cfg + +core := agent.NewAgentCore(agent.AgentCoreConfig{ + Backend: be, + WorkDir: workdir, + 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. `WorkDir` is preserved across switches. + +After connecting MCP servers, `BuildAgentEnv` scans the tool list for `task_create`. If found, it wires a `dispatchPlanBackend` to the reasoning server's `Plan` field via `tools.PlanBackendSetter`. If not found, it wires any fallback passed via `WithFallbackPlanBackend`. This auto-wiring runs on every agent start and `/agent` switch. + +`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/.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. diff --git a/doc/ARCHITECTURE.md~ b/doc/ARCHITECTURE.md~ new file mode 100644 index 0000000..9a953ba --- /dev/null +++ b/doc/ARCHITECTURE.md~ @@ -0,0 +1,162 @@ +# 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, tool definitions (builtin.go) + tools/execute/ — execute.Server: execute_code, execute_tool, execute_pipe + tools/reasoning/ — reasoning.Server: reasoning_think, reasoning_plan + +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) +- **task backend (MCP)** — any MCP server that exposes `task_create` is automatically wired as the persistence backend for `reasoning_plan` by `BuildAgentEnv`; no consumer code required. See [9beads-mcp](https://github.com/lneely/9beads-mcp) for the reference implementation and the interface contract. +- **fallback plan backend** — consumers can supply a `tools.PlanBackend` via `agent.WithFallbackPlanBackend` that is used when no `task_create` MCP tool is found. ollie-9p uses this to enqueue plan steps into the session queue. +- **`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(workdir string)` accepts a working directory that is set as `cmd.Dir` for sandboxed commands and used to expand `{CWD}` in the sandbox config. Pass `""` to fall back to `os.Getwd()`. + +**Adding a new tool server:** +1. Implement `tools.Server` in a new package under `pkg/tools/` +2. Export `func Decl(...) func() tools.Server` +3. Add tool definitions to `pkg/tools/builtin.go` (avoids import cycles) +4. 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 + ├── "reasoning" → tools.Server + │ reasoning_think + │ reasoning_plan + │ └── tools.PlanBackend (optional, priority order) + │ 1. dispatchPlanBackend → task_create (MCP) + │ 2. WithFallbackPlanBackend (e.g. queuePlanBackend) + │ 3. nil → in-context plan only + └── "" → tools.Server +``` + +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 + +Five tools are registered across two 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 | +| `reasoning` | `reasoning_think` | Externalizes intermediate reasoning (no-op, recorded in history) | +| `reasoning` | `reasoning_plan` | Decomposes a goal into ordered steps; persists to task backend if available | + +File operations go through `execute_code` using standard shell tools (`cat`, `grep`, `sed`, `ed`, `ssam` if plan9port is available, etc.). + +Tool definitions live in `pkg/tools/builtin.go` to avoid import cycles — the subpackages import `pkg/tools`, so `pkg/tools` cannot import them back. + +`OLLIE_TOOLS_PATH` defaults to `~/.local/share/ollie/tools`. The directory can be a symlink or a mountpoint — `execute_tool` treats it as an ordinary filesystem path. + +## Planning and Task Persistence + +`reasoning_plan` is a meta-cognitive tool for executive planning. It decomposes a goal into a dependency graph of steps before execution begins. If a task backend is available, the plan is committed to persistent storage. + +The task backend is any MCP server that exposes a `task_create` tool. `BuildAgentEnv` scans available tools after connecting MCP servers: if `task_create` is found, it wires a `dispatchPlanBackend` to the reasoning server's `Plan` field via the `tools.PlanBackendSetter` interface. If not found, it falls back to any `PlanBackend` supplied via `WithFallbackPlanBackend`. If neither is present, `reasoning_plan` produces an in-context plan only. + +ollie-9p supplies a `queuePlanBackend` fallback for every session: when `task_create` is absent, plan steps are enqueued to the session's `enqueue` file in topological order and returned as placeholder IDs (`q1`, `q2`, …). This implementation lives in `9p` because it has a hard dependency on the 9P filesystem layout; the extension point itself (`tools.PlanBackend` + `WithFallbackPlanBackend`) remains in core. + +The reference task backend is [9beads-mcp](https://github.com/lneely/9beads-mcp), which wraps the [9beads](https://github.com/lneely/9beads) 9P task server. + +### Task interface contract + +Auto-wiring requires only `task_create`. The minimum viable contract is: + +| Tool | Required | Purpose | +|---|---|---| +| `task_create` | yes | Create a task; must return a plain-text ID | +| `task_delete` | recommended | Remove tasks when a plan is aborted or superseded | +| `task_list` | optional | Orient the agent across sessions | +| `task_read` | optional | Inspect a specific task | +| `task_update` | optional | Claim, complete, fail, defer, label tasks | +| `task_edit` | optional | Revise title, body, or parent of an existing task | +| `task_dep` | optional | Add or remove blocking dependencies between tasks | + +Ollie degrades gracefully: if a tool is absent, the agent falls back to `execute_code` (shell-out) or skips that lifecycle step. The only hard requirement for persistent planning is `task_create` returning an ID in the `{"content": [{"type": "text", "text": ""}]}` format. + +See [doc/PLANNING.md](doc/PLANNING.md) for the full design rationale. + +## Typical Consumer Setup + +```go +newDispatcher := tools.NewDispatcherFunc(map[string]func() tools.Server{ + "execute": execute.Decl(workdir), // "" falls back to os.Getwd() + "reasoning": reasoning.Decl(), +}) + +env := agent.BuildAgentEnv(cfg, newDispatcher(), workdir) // also connects MCP servers from cfg + +core := agent.NewAgentCore(agent.AgentCoreConfig{ + Backend: be, + WorkDir: workdir, + 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. `WorkDir` is preserved across switches. + +After connecting MCP servers, `BuildAgentEnv` scans the tool list for `task_create`. If found, it wires a `dispatchPlanBackend` to the reasoning server's `Plan` field via `tools.PlanBackendSetter`. If not found, it wires any fallback passed via `WithFallbackPlanBackend`. This auto-wiring runs on every agent start and `/agent` switch. + +`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/.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. diff --git a/doc/PLANNING.md b/doc/PLANNING.md new file mode 100644 index 0000000..d86c1e3 --- /dev/null +++ b/doc/PLANNING.md @@ -0,0 +1,170 @@ +# Planning and Task Persistence in Ollie + +## Problem + +An agent without planning is less effective but still useful. Planning capability +should be optional — degraded but functional when no task backend is available, +and persistent when one is. + +The challenge: how to integrate a planning tool with an optional external task +backend without hard-coupling the core to any specific implementation. + +## Design Decisions + +### reasoning_plan as a built-in tool + +Planning is executive functioning — breaking a goal into ordered steps before +acting. It belongs in the `reasoning_*` namespace alongside `reasoning_think`, +which handles moment-to-moment reflection. Both are meta-cognitive tools: +`reasoning_think` externalizes intermediate thought; `reasoning_plan` +externalizes the work breakdown. + +Tool schemas are more reliable than free-text instructions over long sessions. +Agents rarely forget how to call `file_read`; they can forget shell conventions. +The planning operation is complex enough (goal + structured steps + dependency +edges) that a typed schema earns its place. + +`reasoning_plan` does not rely on shell-out. It takes structured JSON, formats +the plan as readable text, and optionally persists it. This is the right +minimum for a built-in tool. + +### Loose coupling via PlanBackend interface + +The reasoning server holds an optional `Plan tools.PlanBackend` field (nil by +default). When nil, `reasoning_plan` produces an in-context plan only. When set, +the plan is persisted (or queued) by the backend. + +`tools.PlanBackend` and `tools.PlanBackendSetter` are defined in `pkg/tools`. +The reasoning server implements `PlanBackendSetter`. The agent package implements +`dispatchPlanBackend`, which routes task creation through the dispatcher. No +import cycles: reasoning → tools, agent → tools, agent ↛ reasoning. + +Consumers can supply a fallback backend via `agent.WithFallbackPlanBackend(b)`: + +```go +env := agent.BuildAgentEnv(cfg, d, workdir, agent.WithFallbackPlanBackend(myFallback)) +``` + +The fallback is only used when no `task_create` MCP tool is found. If a task +backend is available it takes priority unconditionally. + +### Task backend as an MCP server + +The task backend is any MCP server that exposes `task_create`. There is no +built-in task server in ollie core — this is an intentional extension point. +The convention (the interface contract) is: + +| Tool | Required | Description | +|---|---|---| +| `task_create` | yes | Create a task; return its ID as plain text | +| `task_delete` | recommended | Remove tasks when a plan is aborted or superseded | +| `task_list` | no | List tasks by status | +| `task_read` | no | Read a task by ID | +| `task_update` | no | Update task status/metadata | +| `task_edit` | no | Revise title, body, or parent of an existing task | +| `task_dep` | no | Add or remove blocking dependencies between tasks | + +`task_create` must return a plain-text ID in the standard MCP content format: +`{"content": [{"type": "text", "text": ""}]}`. + +Any MCP server implementing this convention will be auto-detected and wired. + +### Auto-wiring in BuildAgentEnv + +After connecting MCP servers, `BuildAgentEnv` scans the tool list for +`task_create`. If found, it constructs a `dispatchPlanBackend` and sets it on +the reasoning server via `PlanBackendSetter`. This wiring runs on every agent +start and `/agent` switch, so the task backend is always current. + +If `task_create` is not found, `BuildAgentEnv` checks whether the caller +supplied a fallback via `WithFallbackPlanBackend`. If so, that backend is wired +instead. Priority: task MCP > caller fallback > nil (in-context only). + +No frontend changes are needed to gain planning capability — add a task MCP +server to the agent config and it works. + +### Graceful degradation + +- No task MCP server configured, no fallback → `reasoning_plan` produces an + in-context plan only. +- No task MCP server configured, fallback supplied → steps are handed to the + fallback backend (e.g. queued for sequential execution). See + [Queue-based fallback](#queue-based-fallback) below. +- Task MCP server configured but unreachable → `task_create` fails, + `CreatePlan` returns an error, reasoning server degrades to in-context plan + with a warning. +- Task MCP server running → full persistence, steps get IDs, plan is durable. + +The agent's behavior is identical in all cases: call `reasoning_plan`, get a +formatted plan, proceed. The persistence difference is transparent. + +### Queue-based fallback + +ollie-9p registers a `queuePlanBackend` as the fallback for every session. When +`task_create` is absent, `reasoning_plan` writes each step as a queued prompt to +the session's `enqueue` file in topological order (blockers before dependents). +The implementation lives in `9p` (not core) because it has a hard dependency on +the 9P filesystem layout — the extension point is the interface, not this +specific implementation. + +Steps are returned as placeholder IDs (`q1`, `q2`, …) so the agent can refer to +them in subsequent `reasoning_think` calls. The queue persists independently of +context — unlike an in-context-only plan, steps survive compaction. + +The goal description is prepended to the first enqueued step for context. +Dependency annotations (`after: q1, q2`) are included in each step's prompt so +the agent retains the relationship even if it processes steps across multiple +turns. + +Fan-out parallelism is achievable by spawning sub-sessions: the enqueued step +can instruct the agent to create a child session per parallel branch, nudge each +with its task via `prompt`, and have sub-agents write completion notifications +back to the parent via `enqueue`. + +### Shell-out for everything else + +Queries, comments, bulk operations, event watching — all of these belong in +`execute_code`, not in built-in tools. The `task_*` MCP tools cover the full +planning and execution lifecycle (create, list, read, update, edit, delete, +dependency management). For example, advanced 9beads operations are accessible +via shell: `cat $TASK_DIR/list`, `grep`, etc. + +This keeps the built-in surface minimal and relies on `execute_code` for +flexibility. + +## Reference Implementation: 9beads-mcp + +[9beads-mcp](https://github.com/lneely/9beads-mcp) wraps the 9beads 9P task +server as an MCP server. It: + +- Resolves the project mount from `$PWD` (passed explicitly in the MCP server + env config, since MCP subprocesses run in a minimal environment) +- Auto-mounts the project directory if not already mounted +- Exposes `task_create`, `task_list`, `task_read`, `task_update`, `task_edit`, `task_delete`, `task_dep` + +Agent config example: + +```yaml +mcpServers: + task: + command: 9beads-mcp + env: + PWD: "$PWD" +``` + +`$PWD` is expanded by `os.ExpandEnv` at connect time, giving the MCP server +the agent's working directory. + +## Future Direction: Event-Driven Planning + +9beads exposes `~/mnt/beads/events` as a blocking JSON event stream. A +goroutine in the frontend could watch this and inject events into the agent's +interrupt queue (via `PromptFIFO`) when: + +- A blocked step becomes unblocked (its dependency completed) +- A step is assigned to this agent by an external actor +- An external process marks a step complete + +This would make agents reactive rather than polling — the agent yields after +completing work and wakes up when the event stream fires. Not implemented yet; +documented here as the natural next step.