Files
fidoca/PLAN.md
2026-09-25 11:41:25 +02:00

27 KiB

Fido Todo/Plan and Subagent Plan

Status: Proposed
Scope: Add a small coordination layer without turning Fido into a general workflow engine.

Goals

  • Preserve a concise, durable task plan across turns and restarts.
  • Make progress and blockers visible to the user in a persistent, full todo panel.
  • Make the complete current todo state available to the model in the conversation, as a tool response rather than as mutable initial prompt text.
  • Keep the system-prompt prefix stable so plan updates do not unnecessarily bust prompt caching.
  • Allow bounded delegation for genuinely independent investigation or review work, with an explicit subagent identity and a clearly isolated message fork.
  • Keep the advertised tool catalog stable for prompt caching; enforce read-only and no-recursion policy at invocation time rather than changing schemas or tool lists when entering a subagent.
  • Show live subagent progress in a separate, accent-colored console activity bar.
  • Keep the tool surface small: no separate task database UI, queue service, or shell wrappers.
  • Keep the parent agent responsible for integration, safety, and final verification.

Non-goals

  • A general-purpose project-management system.
  • Autonomous recursive agents.
  • Background workers, job queues, or a persistent multi-agent scheduler in the first version.
  • Letting subagents silently modify the user's working tree.
  • Replacing bash_exec; shell remains the general escape hatch for tests, builds, Git, and unusual operations.

Design principles

  1. State is not shell state. A plan is structured session/workspace state and should not be implemented by parsing files or maintaining a second conversation transcript.
  2. One tool per capability. Use one update_plan tool and one optional delegate tool. Do not add get_plan, complete_task, list_agents, or join_subagent initially.
  3. Short-lived, explicit delegation. A delegated task has a goal, scope, budget, and output contract. Results return to the parent before the parent continues.
  4. Read-only by runtime policy. Subagents see the same stable advertised tool catalog as the parent, but invocation guards reject mutating tools and recursive delegation. Write-capable delegation is a later, explicitly approved feature.
  5. Evidence over confidence. A plan item is complete only after the relevant verification is recorded; a subagent finding is a hypothesis until the parent checks it.
  6. No repository litter. Runtime plan and delegation state should use the existing .fido-* ignore convention or the per-workspace state location, not a tracked TODO.md.
  7. Cache-stable system context. The system message contains stable instructions and tool contracts only. The current todo state is delivered through the update_plan tool result (and a restored tool-result event after a restart), so changing a plan does not rewrite the cached system-prompt prefix.
  8. Explicit delegation boundary. A subagent receives an independent snapshot of the parent message array plus a bootstrap result that states its role and responsibilities. The parent receives the subagent's final output, not its private transcript.
  9. Stable tool advertisements. The tool set, schemas, descriptions, and ordering must not change when a main agent spawns a subagent. delegate remains advertised inside the subagent; a runtime identity/depth guard converts a nested call into an explicit failure tool result.
  10. Observable delegation. While a subagent is working, the console displays a second activity bar, distinct from the main status bar and visually marked with an accent color.

1. Todo/plan capability

Tool

Add one model-facing tool:

update_plan({
  reason: string,
  items: [
    {
      id: string,
      title: string,
      status: "pending" | "in_progress" | "blocked" | "done",
      details?: string,
      dependencies?: string[],
      next_action?: string,
      evidence?: string[]
    }
  ]
})

The tool replaces the current plan atomically. The model must include existing IDs when changing status; unknown IDs, invalid dependencies, and more than one in_progress item should produce a clear ToolError.

A single replacement operation is intentional: it is easier for the model to reason about and keeps the tool schema small. On every successful call, the tool result is the complete canonical plan state for the next model request. The result must include the version, update timestamp, reason, and every normalized item, including all optional fields. Do not replace it with a count, a summary, a partial list, or an ellipsis. The model should treat the latest update_plan result—not memory of an earlier call—as the source of truth.

Model-facing tool description

The description for update_plan must explicitly say:

Replace the workspace todo list and return its complete normalized state. The returned tool result is the model-visible source of truth for the current plan; it contains the full version, updatedAt, reason, and every item with its ID, title, status, details, dependencies, next action, and evidence. The plan is delivered in the tool result, not interpolated into the initial system prompt, so updating it does not change the cached system-prompt prefix. Use it at milestones, preserve existing IDs, and keep exactly zero or one in_progress item.

Message placement and cache rule

  • Build the system message once per session from the stable workspace/tool instructions. It must never contain the current plan JSON, a plan timestamp, or a plan-dependent status.
  • After a successful replacement, the normal provider tool message is already part of the model's message history. The next request therefore sees the full state without a second prompt or a get_plan call.
  • If a plan is restored from disk after a restart, load it into the state module and restore it as a provider-compatible tool result (with an internal matching call/event if the provider requires one), not by rewriting the system message. This restore result must also contain the complete plan state.
  • Compaction must preserve the latest plan-state tool result. If older results are elided, carry the latest complete result forward as the newest tool result rather than replacing it with a summary.
  • The plan UI consumes the same normalized object returned by the tool, so the model-visible state and the floating panel cannot silently diverge.

State model

Persist a versioned JSON document, initially at .fido-plan.json (covered by the existing .fido-* ignore rule), with this shape:

{
  "version": 1,
  "updatedAt": "2025-01-01T00:00:00.000Z",
  "reason": "Current task plan",
  "items": []
}

Recommended rules:

  • Keep IDs stable and human-readable (inspect, implement, verify).
  • Allow at most 20 items and keep titles/details short.
  • Permit zero or one in_progress item; represent waiting on a person or external service as blocked.
  • Require next_action for pending, in_progress, and blocked items.
  • Record concise evidence such as npm test or test/ui.test.js:42; do not store full command output in the plan.
  • Never treat a plan update as an undoable source-code edit.

Runtime integration

  1. Add a small src/plan.js state module with loadPlan, getPlan, and setPlan/validation helpers.
  2. Add PLAN_FILE to src/config.js, defaulting to .fido-plan.json.
  3. Load the plan during startup for the UI and local commands, but do not interpolate it into the system prompt. When a persisted plan exists, arrange for the initial model-visible message history to contain a full plan-state tool result (or the equivalent provider-compatible restore event).
  4. Register update_plan in src/tools/index.js; its description and full-result contract must be available to the model.
  5. Add plan to the UI store and call ui.setPlan(...) after startup load, every successful update_plan, and /plan clear. The panel must update atomically from the canonical normalized result.
  6. Add a PlanPanel floating box anchored to the right side of the full-screen Ink layout. It is a persistent overlay/side rail, not a transcript entry, and it must show the full todo list, not only the active item or a count. Render every item and every bounded item field (ID, title, status, details, dependencies, next action, and evidence), with an empty-state message when there is no plan. At narrow widths, use a responsive overlay/fallback, but do not discard the state or silently reduce the model-facing result.
  7. Keep a concise tool-call notification for the transcript, but do not dump the full plan there; the full state belongs in the right-side panel and in the tool result.
  8. Add /plan to display the current plan and /plan clear to start a new plan. Keep these as local commands; they do not require an LLM turn.
  9. Decide explicitly whether /recall restores the plan. Recommended default: recall restores conversation messages but keeps the workspace plan unchanged, because the plan describes the current workspace rather than a historical transcript.

Usage policy

The stable system prompt should tell the model to:

  • create a plan when a request has multiple meaningful steps, spans several files, or is likely to be interrupted;
  • skip a formal plan for a small, obvious change;
  • call update_plan when the current state needs to be established, after a milestone, or when a blocker changes the next action;
  • read the complete state from the latest update_plan tool result instead of assuming that an earlier result or the initial prompt is still current;
  • keep exactly zero or one current item in progress;
  • preserve stable IDs and record concise blockers, next actions, and evidence;
  • update the plan at milestones, not after every tool call; and
  • never place the plan in the system prompt or use a shell command to maintain it.

A full plan snapshot in a tool result is model-visible prompt context, even though it is not the initial system prompt. It must survive normal turn handoff and compaction.

2. Subagent capability

Tool

Add one optional model-facing tool:

delegate({
  task: string,
  context?: string,
  scope?: {
    paths?: string[],
    symbols?: string[]
  },
  mode?: "read_only",
  output_contract?: "findings",
  timeout_ms?: number
})

The first version should run one bounded task synchronously and return a concise result. There should be no background job ID and no separate status/join tool. A later background implementation may add those only after demonstrating a real need.

Required model-facing tool description

The delegate description must not merely say “spawn a helper.” It must tell the parent model all of the following:

Run one bounded, synchronous, read-only subagent and return only its final normalized result. At invocation, Fido creates a private fork (an independent snapshot) of the parent's current messages array; the subagent works from that fork, not from a live reference to the parent array. The subagent's forked messages include a bootstrap tool result that explicitly gives it its current identity (role: "subagent", a unique agent_id, and parent_role: "main") and explains that its final output is for the parent. When the subagent finishes, its final output becomes the delegate tool result in the parent's messages. The parent does not receive the subagent's private tool calls or full transcript. The tool catalog remains unchanged in the subagent: if a subagent calls delegate again, that invocation must immediately return a normal status: "failed" tool result explaining that nested delegation is forbidden. No further subagent is created. The subagent is read-only, and the parent remains responsible for verification and integration.

The same identity/fork/return-path facts must be enforced in the tool result payload and covered by tests, so the model is not relying only on prose in the tool description.

Subagent prompt and tools

Create a separate LLM context by forking the parent's current messages array at the instant delegate is called. The fork must be isolated: later parent messages and mutations must not appear in or mutate the subagent context, and a subagent must not mutate the parent's array.

The forked array is augmented with a bootstrap tool result before it is sent to the subagent. That result, not an unstated system-prompt convention, tells the subagent what identity it now has and how to return work. At minimum it has this shape:

{
  "type": "subagent_bootstrap",
  "identity": {
    "role": "subagent",
    "agent_id": "subagent-123",
    "parent_role": "main"
  },
  "context_mode": "forked_parent_messages",
  "forked_message_count": 42,
  "task": "the delegated task",
  "responsibility": "Inspect only; return the final result to the parent. You are not the main agent, you may not delegate, and the parent will verify your claims."
}

The bootstrap result is a role: "tool" message in the messages array the subagent takes with it. If the provider requires a matching assistant tool call for every tool result, create an internal, non-model-facing matching call/event; do not add another model-facing coordination tool.

In addition to that fork/bootstrap result, the separate subagent system/developer instructions must include:

  • the parent task and why delegation was chosen;
  • the relevant workspace-relative paths and symbols;
  • a strict read-only instruction;
  • a request to inspect rather than implement; and
  • the required final output format below.

The scope fields constrain investigation, but they do not replace or truncate the message-array fork. Pass the fork directly to the isolated runner rather than re-encoding the entire parent transcript as one prompt string.

Stable advertised catalog and runtime guards

Do not construct a reduced subagent tool array. Parent and subagent runs must receive the same immutable tool objects, names, JSON schemas, descriptions, and ordering. In particular, do not remove delegate from the subagent's advertised tools: doing so changes the cached tool prefix and leaves the attempted nested call without a normal tool-result turn.

Instead, pass an invocation context containing the current identity and delegation_depth. Enforce policy in the tool invocation guard:

  • Allowed read-only operations may include list_files, read_file, ripgrep, and explicitly approved browse_url/websearch calls.
  • patch, mkdir, rm, rmdir, unrestricted bash_exec, ask_user, and other parent-only or mutating tools return a normal permission_denied tool result for a subagent. They must not execute their side effects.
  • A subagent call to delegate returns an immediate status: "failed" tool result with a stable error code. It does not start a provider request or create another fork.

The nested-call result must be returned through the normal provider tool-result path, not thrown as a runner exception, so the subagent receives a valid response for the tool call it made. Its minimum shape is:

{
  "type": "delegate_result",
  "identity": {
    "role": "subagent",
    "agent_id": "subagent-123",
    "parent_role": "main"
  },
  "status": "failed",
  "summary": "Nested delegation is not allowed.",
  "findings": [],
  "recommended_next_steps": [
    "Continue the assigned task without calling delegate."
  ],
  "tests_to_run": [],
  "uncertainties": [],
  "error": {
    "code": "nested_delegation_forbidden",
    "message": "A subagent cannot call delegate; no nested subagent was created."
  }
}

The advertised delegate description must tell the model that this failure result is the expected runtime behavior and that the tool remains visible solely to preserve a stable cacheable tool catalog. The same guard remains in place even if the bootstrap result is ignored or the subagent attempts to claim the main-agent identity.

Console subagent activity bar

The full-screen console must provide a second activity bar dedicated to subagents; do not overload the main command/status bar and do not inject subagent chatter into the normal transcript. Place the bar directly below the header (or above the command bar) as a distinct fixed row, separate from the right-side todo panel.

  • Use a dedicated accent color, specified by the UI design (for example violet/blue such as #a78bfa) with contrasting text, rather than the cyan main-chrome or default transcript colors.
  • Show SUBAGENT, the short agent_id, and the current phase/action such as starting, reasoning, read_file, reviewing, completed, partial, failed, or cancelled.
  • Show elapsed time and a concise, sanitized activity detail. Never print the private message array, raw reasoning, or full subagent transcript.
  • Update the bar for subagent provider status, tool-call, tool-result, and completion callbacks. A nested delegate attempt should briefly show nested delegation rejected and then remain failed/rejected rather than creating a child.
  • The bar is visible while the subagent is active and remains available for the final outcome until the parent turn advances. At narrow widths it may become full-width and wrap/truncate details, but it must still identify the active subagent and status.
  • UI state should be driven by the same store/bridge used by the main app, so nested activity does not steal the input prompt or corrupt the main status line.

The parent retains its own full messages array. The subagent gets a private fork of that array as specified above; “minimum relevant context” applies to task, context, scope, and external data, not to silently replacing the required messages-array fork with an empty context. Neither the parent UI nor the returned tool result may expose the subagent's private transcript.

Result contract

Normalize the subagent's final output to one small object before placing it in the parent conversation as the delegate tool response:

{
  "type": "delegate_result",
  "identity": {
    "role": "subagent",
    "agent_id": "subagent-123",
    "parent_role": "main"
  },
  "status": "completed" | "partial" | "failed",
  "summary": "one-paragraph result",
  "findings": [
    {
      "file": "src/example.js",
      "line": 42,
      "claim": "what was observed",
      "confidence": "high" | "medium" | "low"
    }
  ],
  "recommended_next_steps": ["..."],
  "tests_to_run": ["..."],
  "uncertainties": ["..."]
}

There are two normal identity-bearing result paths, plus the nested-call failure path:

  1. The subagent_bootstrap tool result tells the subagent that its current identity is the named, read-only fork shown above and that it must finish with a result for main.
  2. The delegate_result tool response tells the parent which subagent identity produced the final output. It contains the normalized final output and provenance, not the subagent's intermediate messages.
  3. If that subagent calls delegate, the nested invocation returns the nested_delegation_forbidden status: "failed" tool result to the subagent. It is a valid tool response, not a second successful delegation and not a private transcript sent to the parent.

The parent must verify claims against current files and own integration and the final user-facing response. A subagent must never report a change as complete when it was only asked to investigate.

Safety and lifecycle

  • Default to read-only operation.
  • Deep-fork (or otherwise structurally isolate) the parent message array at delegation time; never share a mutable live message reference between parent and subagent.
  • Generate a unique subagent identity in the runner, include it in both the bootstrap tool result and the final parent-facing result, and do not accept identity text supplied by the model as authoritative.
  • Limit concurrency to one for the first implementation; add parallelism only after rate limits and UI behavior are tested.
  • Enforce a timeout and a result-size limit.
  • Propagate Ctrl+C/cancellation to the subagent.
  • Prevent recursive delegation without changing the advertised tool catalog: keep delegate available, enforce a maximum depth of one in the invocation guard, and return a normal failed tool result when a subagent attempts it.
  • Return only the normalized final output to the parent. Never append the subagent's private message/tool transcript to the parent array or render it as ordinary parent history.
  • Treat subagent output as untrusted input: it may be wrong, stale, or influenced by repository content.
  • Do not expose provider keys, private subagent messages, or full parent transcripts in UI messages; the activity bar may expose only sanitized lifecycle metadata.
  • If write-capable agents are added later, require explicit user approval, use isolated Git worktrees, and require the parent to review and test the diff before integration.

3. Minimal tool surface

After this work, the coordination-related surface should be only:

update_plan   # durable task state
delegate      # optional, bounded read-only delegation

Existing filesystem and execution tools remain unchanged. bash_exec already covers tests, builds, Git, and other commands; do not add run_tests, git_status, or similar wrappers.

4. Implementation order

Phase 1: Plan state and todo UI

  • Add plan configuration, validation, and atomic state helpers.
  • Add update_plan and its unit tests; assert that every successful tool response contains the complete normalized plan with no truncation or summary-only payload.
  • Keep the system prompt stable and add a post-system plan-state tool-result restore path for persisted plans; test that updates do not rewrite messages[0].
  • Add plan state to the UI store and implement the persistent right-side floating full-plan panel with responsive narrow-terminal behavior.
  • Add /plan and /plan clear local UI commands.
  • Verify restart persistence, compaction preservation of the latest full plan result, and malformed-input errors.

Phase 2: Read-only delegation MVP

  • Extract a subagent runner that deep-forks the parent's current messages array into an isolated model context.
  • Add the identity-bearing subagent_bootstrap tool result to that fork and make it an explicit part of the delegate tool contract.
  • Add the stable advertised tool catalog and identity/depth-aware invocation guards; do not remove delegate from the subagent's tool list.
  • Add delegate with timeout, cancellation, and final result normalization; place only the final delegate_result in the parent array.
  • Add the second accent-colored subagent activity bar and wire it to sanitized nested-runner lifecycle events.
  • Add fake-runner tests; do not require a live provider for unit tests.
  • Assert that the subagent receives a fork plus its identity, the parent receives the named subagent's final output as a tool response, neither side shares a live mutable messages reference, and the parent receives no full subagent transcript.
  • Assert that a subagent's delegate call returns a normal status: "failed" tool result with error.code: "nested_delegation_forbidden", starts no nested provider run, and does not mutate the advertised tools or tool schemas.
  • Assert that the console renders a separate accent-colored activity bar during a subagent run and shows the final outcome without exposing the private transcript.
  • Verify the parent remains responsible for final tests, file inspection, and integration.

Phase 3: UX and safety hardening

  • Add status/cancellation rendering for delegation.
  • Add usage/cost reporting for subagent calls.
  • Test provider errors, timeouts, oversized results, and partial findings.
  • Document when to delegate and when not to.

Phase 4: Optional write-capable delegation

Only begin this phase if read-only delegation proves insufficient.

  • Define explicit approval and worktree isolation.
  • Define patch review/merge behavior.
  • Prevent two agents from editing the same files.
  • Add integration tests for applying and reverting agent changes.

Acceptance criteria

Plan

  • A multi-step request can create, update, reload, and clear a plan.
  • The current plan is available after restarting Fido, and the restored snapshot is delivered to the model as a full tool result rather than system-prompt text.
  • Every successful update_plan response contains the complete normalized state (version, timestamp, reason, all items, and all present optional fields) with no truncation, ellipsis, or count-only replacement.
  • Updating the plan does not mutate the system message or force a rebuilt cached system-prompt prefix; a test can snapshot messages[0] before and after many updates and prove it is identical.
  • The latest full plan-state tool result remains available after turn handoff and compaction.
  • Invalid IDs, dependencies, and status combinations fail without corrupting saved state.
  • A plan update does not modify source files or appear in the undo log.
  • The UI renders every current todo item in a persistent floating box anchored to the right side of the screen; it does not merely append a compact plan to transcript history.

Subagent

  • A bounded read-only task returns the documented structured result.
  • At call time the subagent receives an isolated fork of the parent messages array, not a shared live reference.
  • The fork contains a tool result that tells the subagent its current identity (role: "subagent", unique agent_id, parent_role: "main") and return responsibility.
  • The parent receives only the subagent's normalized final output, as a delegate tool response carrying the producing subagent's identity; it does not receive the subagent's private tool/message transcript.
  • The delegate tool description explicitly states the fork, identity, read-only restrictions, stable advertised catalog, nested-call failure behavior, return path, and parent ownership responsibilities.
  • The same tool names, schemas, descriptions, and ordering are advertised to parent and subagent; policy is enforced at invocation time rather than by mutating the tool list.
  • A subagent that calls delegate receives a normal failed tool result with nested_delegation_forbidden; no second subagent is created and no nested provider request is made.
  • The console displays a separate accent-colored subagent activity bar with agent ID, sanitized activity, and final status while delegating.
  • Timeout, cancellation, provider failure, and malformed output are handled gracefully.
  • The parent can continue normally after a partial or failed delegation.
  • Unit tests pass without making a live model/API request.

Regression

  • Existing npm test tests remain green.
  • Existing /undo, /suspend, /recall, and tool notification behavior remain intact.
  • No new tool duplicates an existing shell operation unless it provides a clear safety or state-management benefit.

Ship the plan capability first. Add read-only delegate only after the plan state is stable. This gives the agent a reliable way to decompose work without introducing concurrency, isolation, and model-context bugs all at once.