ollie/doc/PLANNING.md

7.2 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_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 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 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.