This repository has been archived on 2026-08-16. You can view files and clone it, but cannot push or open issues or pull requests.
ollie-core/doc/PLANNING.md

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

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.