u
This commit is contained in:
378
PLAN.md
Normal file
378
PLAN.md
Normal file
@@ -0,0 +1,378 @@
|
||||
# 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:
|
||||
|
||||
```text
|
||||
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:
|
||||
|
||||
```json
|
||||
{
|
||||
"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:
|
||||
|
||||
```text
|
||||
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:
|
||||
|
||||
```json
|
||||
{
|
||||
"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:
|
||||
|
||||
```json
|
||||
{
|
||||
"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:
|
||||
|
||||
```json
|
||||
{
|
||||
"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:
|
||||
|
||||
```text
|
||||
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.
|
||||
|
||||
## Recommended first release
|
||||
|
||||
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.
|
||||
320
README.md
320
README.md
@@ -1,79 +1,293 @@
|
||||
**FIDO** a minimal coding assistant.
|
||||
# Fido Project Overview
|
||||
|
||||
Fido is a console REPL assistant that inspects files, lists directories, finds files
|
||||
and directories by name, searches file contents, applies changes, and runs shell
|
||||
commands (with user approval).
|
||||
Fido is a minimal, interactive terminal coding assistant. It connects an OpenRouter-hosted model to a deliberately small set of workspace tools, then presents the conversation and tool activity in a full-screen terminal UI.
|
||||
|
||||
## Install
|
||||
The project is written in modern JavaScript as native ES modules. It runs directly from source with Node.js; there is no transpiler, bundler, or JSX build step.
|
||||
|
||||
Installs into `~/.fido` and adds a `fido` launcher to your `PATH` via `~/.bashrc`:
|
||||
## What Fido Can Do
|
||||
|
||||
```sh
|
||||
git clone <this-repo> ~/src/fidoca
|
||||
cd ~/src/fidoca
|
||||
./install.sh
|
||||
source ~/.bashrc
|
||||
- Inspect and search a project without leaving the terminal.
|
||||
- List directories, find files by name, search file contents, and read files in chunks.
|
||||
- Apply precise file edits, create files and directories, and remove files or directory trees.
|
||||
- Run shell commands after explicit user approval.
|
||||
- Search the web and extract web pages through Tavily when configured.
|
||||
- Ask the user clarifying questions or request help from a human expert.
|
||||
- Stream model reasoning and tool notifications into a scrollable terminal interface.
|
||||
- Navigate project files from the prompt and insert whole-file, single-line, or line-range mentions.
|
||||
- Keep mutation and conversation state isolated per workspace, outside repositories when installed.
|
||||
- Compact large tool payloads between turns to reduce token usage and avoid retaining stale file contents.
|
||||
|
||||
## Runtime Architecture
|
||||
|
||||
```text
|
||||
bin/fido
|
||||
├─ loads Fido configuration
|
||||
├─ selects per-workspace state
|
||||
└─ starts src/fido.js
|
||||
└─ runRepl()
|
||||
├─ builds the system prompt
|
||||
├─ drives LLMOrbiter + OpenRouter
|
||||
├─ executes tools from src/tools/
|
||||
└─ drives the Ink UI through src/ui/
|
||||
|
||||
model tool call
|
||||
└─ tool registry
|
||||
├─ workspace path validation
|
||||
├─ filesystem, shell, or Tavily operation
|
||||
└─ structured result returned to the model and shown in the UI
|
||||
```
|
||||
|
||||
The installer needs the sibling `llmorbiter` checkout (this project depends on it
|
||||
via `file:../llmorbiter`); it is bundled into `~/.fido/llmorbiter`. Override its
|
||||
location with `LLMORBITER_SRC=/path/to/llmorbiter`.
|
||||
### Main Modules
|
||||
|
||||
Then, from any project directory:
|
||||
| Module | Responsibility |
|
||||
| --- | --- |
|
||||
| `bin/fido` | Installed/source launcher; loads user configuration and creates per-workspace state paths. |
|
||||
| `src/fido.js` | Process entry point; checks for the required API key and starts the REPL. |
|
||||
| `src/repl.js` | Owns conversation state, provider turns, cancellation, tool events, and transcript compaction. |
|
||||
| `src/config.js` | Loads `.env` files and resolves the model, workspace root, and state locations. |
|
||||
| `src/system-prompt.js` | Builds the workspace-aware model instructions sent at session start. |
|
||||
| `src/commands.js` | Implements local slash commands without spending a model turn. |
|
||||
| `src/slash-commands.js` | Defines and resolves the built-in command registry. |
|
||||
| `src/tools/index.js` | Registers the model-facing tools and their UI notification renderers. |
|
||||
| `src/tools/*.js` | Implements individual filesystem, shell, web, and interaction tools. |
|
||||
| `src/sandbox.js` | Resolves relative tool paths and rejects absolute paths or lexical traversal outside the workspace. |
|
||||
| `src/dir-listing.js` | Shared `.gitignore`-aware directory walking and pagination. |
|
||||
| `src/glob.js` | Small shared glob dialect used by name and content searches. |
|
||||
| `src/lines.js` | Shared line splitting with editor-compatible trailing-newline behavior. |
|
||||
| `src/compact.js` | Shrinks old patch arguments and invalidates file reads made stale by later patches. |
|
||||
| `src/undo.js` | Persists a newest-first log describing mutations made by tools. |
|
||||
| `src/suspend.js` | Saves and restores manually suspended conversation snapshots. |
|
||||
| `src/transcript.js` | Formats assistant, user, tool, error, command, and recalled-message output. |
|
||||
| `src/file-browser.js` | Provides testable file-browser navigation and mention-formatting helpers. |
|
||||
| `src/ui/index.js` | Exposes an imperative UI singleton to the REPL and tools. |
|
||||
| `src/ui/store.js` | External, subscriber-based state store for prompts, history, status, and usage totals. |
|
||||
| `src/ui/app.js` | Root Ink application and fixed terminal layout. |
|
||||
| `src/ui/main-input.js` | Main input, slash-command completion, and `Ctrl+O` file/line browser. |
|
||||
|
||||
```sh
|
||||
fido # work on the current directory
|
||||
fido /path/to/project # work on a specific directory
|
||||
## Conversation Flow
|
||||
|
||||
1. Configuration resolves the workspace root using the first positional argument, then `ROOT_DIR`, then the current directory.
|
||||
2. The REPL loads undo and suspend state and mounts the Ink UI.
|
||||
3. It builds a system prompt containing the workspace root, a short root listing, tool contracts, and operating rules.
|
||||
4. The user submits a normal message or a local slash command.
|
||||
5. Normal input is sent to `LLMOrbiter`, which uses the OpenRouter provider and advertises the registered tools to the model.
|
||||
6. A provider turn can iterate up to 20 times, allowing the model to call tools and continue reasoning before returning a final response.
|
||||
7. Tool calls and results are rendered in the transcript; the structured result is also returned to the model.
|
||||
8. Between user turns, Fido compacts large patch arguments and replaces pre-patch file reads that are no longer current.
|
||||
9. The resulting message array remains in memory for the session. It can be saved explicitly with `/suspend`.
|
||||
|
||||
## Model-Facing Tools
|
||||
|
||||
All currently registered tools are defined in `src/tools/index.js`.
|
||||
|
||||
| Tool | Purpose and notable behavior |
|
||||
| --- | --- |
|
||||
| `list_files` | Lists one directory, optionally recursively, with `.gitignore` support and pagination. Dot-directories are hidden unless requested. |
|
||||
| `find_files` | Recursively finds files or directories by a simple name glob. Slashless patterns also match basenames. |
|
||||
| `read_file` | Reads a workspace file as line-based content and supports zero-indexed `seek`/`limit` chunking. |
|
||||
| `ripgrep` | Performs a pure-JavaScript regular-expression content search. It skips binary files, files over 2 MiB, ignored paths, and hidden directories by default. Long matching lines are capped. |
|
||||
| `patch` | Applies ordered create/overwrite or exact first-match search/replace operations. Supports `skiplines`, CRLF adaptation, partial-progress reporting, and undo records. |
|
||||
| `mkdir` | Recursively creates a directory and records its creation. |
|
||||
| `rm` | Deletes a file while retaining its contents in the undo log. |
|
||||
| `rmdir` | Recursively deletes a directory. Its undo entry records the path but not the removed contents. |
|
||||
| `bash_exec` | Runs `bash -c` with the workspace as its initial working directory. Requires explicit approval, closes stdin, disables interactive/color behavior, limits output, and kills the process tree at timeout. |
|
||||
| `websearch` | Uses Tavily to return a synthesized answer and ranked web results. It fails when no Tavily key is configured. |
|
||||
| `browse_url` | Uses Tavily to extract one or more web pages, optionally focused by a relevance query. It fails when no Tavily key is configured. |
|
||||
| `ask_user` | Prompts the user for clarification or a decision, with up to three suggested choices and a free-text option. |
|
||||
| `ask_an_expert` | Displays a copyable question for a human expert and waits for the user to paste the answer back. It does not contact an external expert directly. |
|
||||
|
||||
The web tools remain registered even when `TAVILY_API_KEY` is absent; configuration is validated lazily when one is invoked.
|
||||
|
||||
## Built-In Commands
|
||||
|
||||
| Command | Behavior |
|
||||
| --- | --- |
|
||||
| `/paste` | Temporarily hands the terminal to `$EDITOR` for multiline input, then sends the resulting text to the model. |
|
||||
| `/suspend [description]` | Saves the current message array as a timestamped JSON snapshot. A description is requested if omitted. |
|
||||
| `/recall` | Lets the user select a saved snapshot. The old conversation is restored under a newly built system prompt. |
|
||||
| `/undo` | Displays the most recent mutation-log entries. It currently inspects the log; it does not execute a rollback operation. |
|
||||
| `/reasoning [level]` | Shows or sets provider reasoning effort to `minimal`, `low`, `medium`, or `high`. Omitting the level leaves the model default in effect. |
|
||||
| `/quit` | Ends the session. |
|
||||
|
||||
Typing `/` in the main input opens inline command completion. All recognized slash commands are handled locally and do not consume a model turn.
|
||||
|
||||
## Terminal UI
|
||||
|
||||
The UI is a React application rendered by Ink in the terminal's alternate screen.
|
||||
|
||||
- **Header:** model, workspace, reasoning setting, and history-scroll state.
|
||||
- **History:** bottom-anchored transcript with streaming reasoning and line-granular scrolling.
|
||||
- **Command bar:** main input, local command completion, approval prompts, selections, confirmations, and the `Ctrl+O` file browser.
|
||||
- **Footer:** cumulative model calls, input/output/cache tokens, estimated cost, and key hints.
|
||||
- **History controls:** `PgUp`/`PgDn`, mouse wheel, and `End` to return to the live tail.
|
||||
- **Cancellation:** `Ctrl+C` at the main prompt exits; during a model turn it aborts the provider request and restores the draft.
|
||||
|
||||
The store retains the latest 1,000 logical history entries, while the view mounts only the lines that fit in the current terminal. Mouse reporting can be disabled with `FIDO_NO_MOUSE=1`.
|
||||
|
||||
## File Mentions
|
||||
|
||||
Press `Ctrl+O` in the main prompt to open the file browser.
|
||||
|
||||
- Arrow keys move the selection.
|
||||
- `Enter` opens a directory or enters a file's line selector.
|
||||
- `Tab` inserts a whole-file mention from the browser.
|
||||
- `Backspace` moves to the parent directory.
|
||||
- `Esc` leaves an overlay.
|
||||
- In line mode, `v` starts or ends a range, `g`/`G` jump to the first/last line, and `Enter` inserts the mention.
|
||||
|
||||
Mentions are references, not embedded file contents:
|
||||
|
||||
```text
|
||||
@README.md
|
||||
@src/fido.js:12
|
||||
@src/fido.js:12-40
|
||||
```
|
||||
|
||||
Set `OPENROUTER_API_KEY` (and optionally `TAVILY_API_KEY`) in `~/.fido/.env`.
|
||||
The browser hides ignored and dot-directory entries by default. Mentions use forward-slash workspace-relative paths and one-based line numbers; the model maps those numbers to `read_file`'s zero-based `seek`.
|
||||
|
||||
### File mentions
|
||||
## Safety and Trust Boundaries
|
||||
|
||||
While typing a message, press **`Ctrl+O`** to open an in-prompt file browser.
|
||||
It is `.gitignore`-aware and hides dot-directories, just like `list_files`.
|
||||
Fido has useful path and workflow safeguards, but it is not an operating-system sandbox.
|
||||
|
||||
| Key | Action |
|
||||
| ----------- | -------------------------------------------------- |
|
||||
| `↑` / `↓` | Move the highlight |
|
||||
| `Enter` | Descend into a directory, or open the line selector|
|
||||
| `Tab` | Mention the highlighted file (whole file) |
|
||||
| `Backspace` | Go to the parent directory |
|
||||
| `Esc` | Cancel back to the prompt |
|
||||
- Dedicated filesystem tools require relative paths and reject lexical paths that resolve outside `ROOT_DIR`.
|
||||
- Direct filesystem access can still follow filesystem semantics such as symlinks; path checks should not be treated as a hard kernel-enforced isolation boundary.
|
||||
- `bash_exec` pins only the shell's starting directory. The shell can use absolute paths or change directories, so interactive approval is the primary protection around shell execution.
|
||||
- Users can approve one command or all shell commands for the rest of that Fido session.
|
||||
- Network and extracted web content is explicitly treated as untrusted third-party input.
|
||||
- The model is instructed to inspect files before patching and to prefer targeted replacements over whole-file rewrites.
|
||||
- API keys are loaded from environment files that are excluded by `.gitignore`; installed configuration lives under `FIDO_HOME`.
|
||||
- Tool notifications shorten large arguments, but full model tool results remain part of the provider conversation.
|
||||
|
||||
In the line selector, `↑`/`↓` move the cursor, `v` starts/ends a range, `g`/`G`
|
||||
jump to the first/last line, and `Enter` inserts the mention.
|
||||
## State and Persistence
|
||||
|
||||
The mention is plain text spliced in at the cursor; the model reads the file
|
||||
itself via `read_file` (it is a *reference*, not embedded contents):
|
||||
### Installed layout
|
||||
|
||||
| Selection | Inserted text |
|
||||
| ----------- | ----------------------- |
|
||||
| Whole file | `@src/fido.js` |
|
||||
| Line range | `@src/fido.js:120-145` |
|
||||
| Single line | `@src/fido.js:120` |
|
||||
The installer defaults to `FIDO_HOME=~/.fido`:
|
||||
|
||||
Paths are relative to the workspace root with forward slashes; line numbers are
|
||||
1-based (the model maps them to `read_file`'s 0-indexed `seek`).
|
||||
|
||||
### Layout
|
||||
|
||||
```
|
||||
```text
|
||||
~/.fido/
|
||||
app/ the fido app (this repo)
|
||||
llmorbiter/ bundled dependency
|
||||
bin/fido the launcher (added to PATH)
|
||||
.env your config / API keys
|
||||
state/<workspace>/ per-workspace state
|
||||
app/ # this repository
|
||||
llmorbiter/ # bundled local dependency
|
||||
bin/fido # launcher
|
||||
.env # API keys and configuration
|
||||
state/
|
||||
<workspace-slug>-<checksum>/
|
||||
undo.json
|
||||
suspends/
|
||||
<timestamp>-<description>.json
|
||||
```
|
||||
|
||||
Undo and `/suspend` state is stored **per workspace** under `~/.fido/state/`, so it
|
||||
never mixes between projects and never litters your repositories.
|
||||
The workspace slug is derived from the canonical workspace path and augmented with a checksum, keeping state isolated between projects and avoiding repository litter.
|
||||
|
||||
To remove everything: `./install.sh --uninstall`.
|
||||
### Source-checkout defaults
|
||||
|
||||
When run without the installed launcher, the defaults are:
|
||||
|
||||
```text
|
||||
UNDO_FILE=.fido-undo.json
|
||||
SUSPEND_DIR=.fido-suspends
|
||||
```
|
||||
|
||||
These paths are resolved from the selected workspace. They are covered by the repository's `.fido-*` ignore rule.
|
||||
|
||||
Normal conversations are not automatically persisted on exit. `/suspend` and `/recall` provide explicit manual snapshots.
|
||||
|
||||
## Configuration
|
||||
|
||||
Copy `.env.example` to `.env` and set at least `OPENROUTER_API_KEY`.
|
||||
|
||||
| Variable | Required | Default / meaning |
|
||||
| --- | --- | --- |
|
||||
| `OPENROUTER_API_KEY` | Yes | Credential for the OpenRouter provider. |
|
||||
| `MODEL` | No | `xiaomi/mimo-v2.6-flash` |
|
||||
| `TAVILY_API_KEY` | No | Enables `websearch` and `browse_url`. |
|
||||
| `ROOT_DIR` | No | Workspace root when no positional path is supplied. |
|
||||
| `UNDO_FILE` | No | Undo-log file; the installed launcher overrides it per workspace. |
|
||||
| `SUSPEND_DIR` | No | Suspend directory; the installed launcher overrides it per workspace. |
|
||||
| `COMPACT_DEBUG` | No | Set to `1` to report transcript-compaction statistics. |
|
||||
| `FIDO_HOME` | No | Installer/launcher home, default `~/.fido`. |
|
||||
| `LLMORBITER_SRC` | No | Installer override for the sibling `llmorbiter` checkout. |
|
||||
| `BASHRC` | No | Installer shell RC file, default `~/.bashrc`. |
|
||||
| `FIDO_SKIP_DEPS` | No | Set to `1` to let the installer skip `npm install`. |
|
||||
| `FIDO_NO_MOUSE` | No | Set to `1` to disable terminal mouse reporting. |
|
||||
|
||||
For installed use, launcher-exported variables take precedence over workspace `.env` values. The source application also checks the repository's `.env` and then the current working directory's `.env`.
|
||||
|
||||
## Installation and Development
|
||||
|
||||
### Source checkout
|
||||
|
||||
Fido requires the sibling local dependency declared in `package.json`:
|
||||
|
||||
```text
|
||||
<parent-directory>/
|
||||
fido/
|
||||
llmorbiter/
|
||||
```
|
||||
|
||||
From the Fido checkout:
|
||||
|
||||
```sh
|
||||
npm install
|
||||
cp .env.example .env
|
||||
# Edit .env and set OPENROUTER_API_KEY.
|
||||
npm start
|
||||
```
|
||||
|
||||
Run against another workspace with:
|
||||
|
||||
```sh
|
||||
npm start -- /path/to/project
|
||||
```
|
||||
|
||||
### Installed launcher
|
||||
|
||||
```sh
|
||||
./install.sh
|
||||
source ~/.bashrc
|
||||
fido
|
||||
# or
|
||||
fido /path/to/project
|
||||
```
|
||||
|
||||
The installer copies the app and `llmorbiter` into `FIDO_HOME`, installs dependencies, installs the launcher, and adds `$FIDO_HOME/bin` to `PATH`. Reinstalling preserves an existing `$FIDO_HOME/.env`. Use `./install.sh --uninstall` to remove the install and its PATH block.
|
||||
|
||||
If `llmorbiter` is not a sibling checkout:
|
||||
|
||||
```sh
|
||||
LLMORBITER_SRC=/path/to/llmorbiter ./install.sh
|
||||
```
|
||||
|
||||
### Tests
|
||||
|
||||
```sh
|
||||
npm test
|
||||
# or one file
|
||||
node --test test/compact.test.js
|
||||
```
|
||||
|
||||
The suite uses Node's built-in test runner and focuses on pure helper behavior: transcript compaction, file-browser paths and mention formatting, glob translation, line counting, mouse decoding, and UI/store rendering behavior. Unit tests do not make live model or Tavily requests.
|
||||
|
||||
## Extending the Project
|
||||
|
||||
To add a model-facing tool:
|
||||
|
||||
1. Create a module under `src/tools/` using `defineTool` from `llmorbiter`.
|
||||
2. Validate arguments and return structured data or throw `ToolError`.
|
||||
3. Use `resolveSafe` for workspace-relative filesystem paths.
|
||||
4. Export a concise `notifyCall` and, when useful, a tailored `notifyResult` renderer.
|
||||
5. Register the tool and notification renderers in `src/tools/index.js`.
|
||||
6. Add its contract to `src/system-prompt.js` when the model needs extra usage rules.
|
||||
7. Add focused tests that do not require live network or provider calls.
|
||||
|
||||
The non-React REPL and tool modules should interact with rendering through the `ui` facade in `src/ui/index.js`; direct console writes would bypass the full-screen UI state.
|
||||
|
||||
## Current Status and Planned Work
|
||||
|
||||
The current implementation does **not** include persistent todo plans or subagent delegation. `PLAN.md` is a proposed design for those features, including an `update_plan` tool, a right-side plan panel, a bounded read-only `delegate` tool, and separate subagent activity/status UI. Those capabilities should not be treated as available until their modules, registry entries, persistence, and tests are implemented.
|
||||
|
||||
Other practical limitations today include manual-only conversation snapshots, a view-only `/undo` command, no kernel-level sandbox, and no live provider integration tests in the standard suite.
|
||||
|
||||
## License
|
||||
|
||||
[0BSD](https://opensource.org/license/0bsd)
|
||||
Fido is released under the [0BSD License](README.md#license).
|
||||
|
||||
Reference in New Issue
Block a user