7.1 KiB
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):
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": "<id>"}]}.
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_planproduces 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 below.
- Task MCP server configured but unreachable →
task_createfails,CreatePlanreturns 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).
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 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:
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.