Skip to content

feat(plan): add pluggable Storage interface (parity with todo toolset) - #3239

Merged
Sayt-0 merged 1 commit into
mainfrom
feat/plan-pluggable-storage
Jun 25, 2026
Merged

feat(plan): add pluggable Storage interface (parity with todo toolset)#3239
Sayt-0 merged 1 commit into
mainfrom
feat/plan-pluggable-storage

Conversation

@Sayt-0

@Sayt-0 Sayt-0 commented Jun 25, 2026

Copy link
Copy Markdown
Member

Summary

Brings the plan builtin toolset to parity with todo: storage is now a
pluggable Storage interface with a default filesystem backend, an injection
seam, and per-instance construction instead of a process-wide-only singleton.

Acceptance criteria

# Criterion Status Where
1 plan.Storage interface + default filesystem impl yes Storage interface; FilesystemStorage
2 plan.WithStorage(...) injects a custom backend yes WithStorage(Storage) Option, panics on nil (matches todo)
3 Per-instance constructor; default CreateToolSet() preserved yes New(opts ...Option); CreateToolSet() still returns the sync.OnceValue singleton
4 Revision bump owned by Storage.Upsert yes bump moved into FilesystemStorage.Upsert
5 Tests for filesystem and custom (in-memory) backend through the interface yes TestStorage_Conformance runs both backends over all four methods
6 No behavior change for standalone/CLI usage yes registry entry unchanged; filesystem default keeps atomic temp+rename, name validation, unreadable-file warnings, last-writer-wins bump

Interface

type Storage interface {
    Get(ctx context.Context, name string) (Plan, bool, error)
    Upsert(ctx context.Context, name, content, title, author string) (Plan, error)
    List(ctx context.Context) (plans []Summary, warnings []string, err error)
    Delete(ctx context.Context, name string) (deleted bool, err error)
}

Behavior preservation

  • Handlers (write_plan / read_plan / list_plans / delete_plan) delegate to Storage rather than calling load/save directly.
  • FilesystemStorage retains the original atomic write (temp file + rename), namePattern validation, missing-vs-corrupt distinction, corrupt-plan-still-deletable, filename-as-authoritative-key, and lazy directory creation on first write.
  • The serialization mutex moves from ToolSet to FilesystemStorage; the shared singleton still routes every operation through one backend instance, so in-process collaboration is serialized exactly as before.
  • Describe() reports the backend via fmt.Stringer, so the filesystem case still renders plan(dir=...).

Tests

Area Coverage
Storage interface TestStorage_Conformance over FilesystemStorage and an in-memory backend
Injection custom-backend write/read/delete through WithStorage; WithStorage(nil) panics
Revision ownership a non-bumping backend proves the handler adds no bump of its own
Output contract nil List normalizes to "plans":[]; backend errors surface as IsError on every handler

All four handlers and Describe are at 100% statement coverage. Remaining uncovered lines in FilesystemStorage are OS-error paths (MkdirAll / ReadFile / Remove failures) that require fault injection and were uncovered before this change.

Design choices open to revision

These follow the issue text and the todo pattern, but can be changed if a different shape is preferred:

  • New(dir string) becomes New(opts ...Option), a breaking signature change to an exported constructor (mirrors todo.New). If keeping New(dir string) is preferred, a separate constructor can be added instead.
  • The in-memory backend lives in tests only; todo exports MemoryTodoStorage (its default). An exported plan.MemoryStorage can be added if embedders want an off-the-shelf ephemeral backend.
  • Name validation lives in FilesystemStorage (the issue lists it under the filesystem implementation), so custom backends are free to define their own key rules. If validation should be guaranteed for all backends, it can move up into the handler.

Validation

  • go build ./...: clean
  • go test ./pkg/tools/builtin/plan/... ./pkg/teamloader/...: pass
  • golangci-lint run ./pkg/tools/builtin/plan/...: clean

Closes #3237

Introduce plan.Storage with a default FilesystemStorage, a WithStorage
option, and a per-instance New(opts...) constructor so embedders can
inject their own backend and get isolation instead of the process-wide
singleton.

The revision bump moves into Storage.Upsert so backends can make it
atomic; the filesystem default keeps today's read-modify-write
(last-writer-wins). The handlers delegate to Storage instead of touching
files directly, and Describe() reports the backend via fmt.Stringer.
CreateToolSet() still returns the process-wide singleton, so standalone
and CLI behavior is unchanged: atomic temp+rename writes, name
validation, and unreadable-file warnings all live in FilesystemStorage.

Tests cover the default filesystem backend and a custom in-memory backend
through the interface (conformance suite), plus handler-level coverage for
custom-backend injection, nil-list normalization, backend-error
propagation, and proof that the revision bump is owned by Storage.

Closes #3237
@Sayt-0
Sayt-0 requested a review from a team as a code owner June 25, 2026 15:27

@docker-agent docker-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assessment: 🟢 APPROVE

The PR correctly refactors the plan toolset to match the todo pluggable-storage pattern. The drafter examined the diff for concurrency issues, nil-pointer risks, revision-bump ownership, interface conformance, and behavior regressions. The verifier confirmed all raised concerns are either pre-existing or non-regressions.

Areas checked:

  • ✅ Mutex move from ToolSetFilesystemStorage — all four methods (Get, Upsert, List, Delete) acquire s.mu correctly; no unguarded path introduced
  • ✅ Singleton path (CreateToolSet / sync.OnceValue) — unchanged and still serializes all default-path agents
  • ✅ Multi-instance serialization — independently-created New() instances were never cross-serialized in the old code either; behavior is unchanged
  • ✅ Revision bump moved into FilesystemStorage.Upsert — no double-bump or missing bump
  • WithStorage(nil) panic guard — consistent with todo pattern
  • var _ Storage = (*FilesystemStorage)(nil) compile-time interface assertion present
  • TestStorage_Conformance runs both FilesystemStorage and in-memory backend through all four methods
  • ✅ Error propagation from Storage methods through all four handlers verified

No confirmed bugs introduced by this PR.

@aheritier aheritier added area/agent For work that has to do with the general agent loop/agentic features of the app area/runtime Runtime engine, agent loop execution, tool dispatch, loop detection kind/feat PR adds a new feature (maps to feat:). Use on PRs only. labels Jun 25, 2026
@Sayt-0
Sayt-0 merged commit db365cb into main Jun 25, 2026
12 checks passed
@Sayt-0
Sayt-0 deleted the feat/plan-pluggable-storage branch June 25, 2026 15:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/agent For work that has to do with the general agent loop/agentic features of the app area/runtime Runtime engine, agent loop execution, tool dispatch, loop detection kind/feat PR adds a new feature (maps to feat:). Use on PRs only.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

plan toolset: add pluggable Storage interface (parity with todo toolset)

4 participants