Skip to content

todo: extract storage interface with in-memory implementation#1960

Merged
dgageot merged 2 commits intodocker:mainfrom
rumpl:todo-storage-interface
Mar 6, 2026
Merged

todo: extract storage interface with in-memory implementation#1960
dgageot merged 2 commits intodocker:mainfrom
rumpl:todo-storage-interface

Conversation

@rumpl
Copy link
Copy Markdown
Member

@rumpl rumpl commented Mar 6, 2026

Introduce a TodoStorage interface to decouple the todo toolset from its storage layer.

Changes

  • TodoStorage interface — defines the storage contract with 6 methods: Add, All, Len, FindByID, Update, and Clear.
  • MemoryTodoStorage — in-memory, concurrency-safe implementation backed by concurrent.Slice[Todo].
  • WithStorage functional option — lets callers inject their own storage implementation into NewTodoTool.
  • NewTodoTool now accepts variadic TodoOptions. Defaults to MemoryTodoStorage when no option is provided, so all existing call sites remain unchanged.
  • Tests updated to exercise the new storage option, including a dedicated TestTodoTool_WithStorage test.

Assisted-By: cagent

Introduce TodoStorage interface to decouple the todo toolset from its
storage layer. Add MemoryTodoStorage as the default in-memory
implementation and a WithStorage functional option so callers can
provide their own.

Assisted-By: cagent
@rumpl rumpl requested a review from a team as a code owner March 6, 2026 09:30
Copy link
Copy Markdown

@docker-agent docker-agent bot left a comment

Choose a reason for hiding this comment

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

Review Summary

Assessment: 🟡 NEEDS ATTENTION

This PR introduces a clean abstraction with the TodoStorage interface, successfully decoupling the todo toolset from its storage implementation. However, there are 3 concurrency and safety issues in the newly added code that should be addressed:

  1. Nil storage validationWithStorage can accept nil, causing panics
  2. ID generation race conditions — Both createTodo and createTodos have non-atomic read-modify-write sequences that can produce duplicate IDs in concurrent scenarios

These issues are all in the changed code and should be fixed before merging.


Findings: 3 issues

type TodoOption func(*TodoTool)

// WithStorage sets a custom storage implementation for the TodoTool.
func WithStorage(storage TodoStorage) TodoOption {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

MEDIUM: WithStorage option can set nil storage causing nil pointer dereference

The WithStorage function accepts a TodoStorage parameter and assigns it directly to t.handler.storage without nil validation. If called as WithStorage(nil), subsequent method calls like h.storage.Len() or h.storage.Add() will panic with nil pointer dereferences.

Recommendation: Add validation:

func WithStorage(storage TodoStorage) TodoOption {
    return func(t *TodoTool) {
        if storage == nil {
            panic("storage cannot be nil")
        }
        t.handler.storage = storage
    }
}

Or document that nil is invalid and will cause panics.

Address PR review feedback:
- WithStorage now panics early if given nil storage
- Replace length-based ID generation with atomic counter to prevent
  duplicate IDs under concurrent access

Assisted-By: cagent
@rumpl
Copy link
Copy Markdown
Member Author

rumpl commented Mar 6, 2026

/review

Copy link
Copy Markdown

@docker-agent docker-agent bot left a comment

Choose a reason for hiding this comment

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

Review Summary

Assessment: 🟢 APPROVE

This PR successfully introduces a clean storage abstraction for the todo toolset. The implementation is well-designed:

Interface Design: TodoStorage provides a clear contract with 6 well-defined methods
Thread Safety: MemoryTodoStorage properly uses concurrent.Slice for concurrency-safe operations
Backward Compatibility: Default storage behavior preserved via functional options pattern
ID Generation: Atomic counter ensures globally unique IDs across concurrent operations
Test Coverage: Comprehensive tests including storage injection and nil-safety

The code was reviewed for potential race conditions and concurrency issues. All operations are properly protected by the underlying concurrent data structures. The atomic ID counter ensures unique IDs even under concurrent load, and the storage abstraction allows for future implementations (e.g., persistent storage) without changing the tool interface.

No blocking issues found. The changed code works correctly as designed.

@dgageot dgageot merged commit 9510550 into docker:main Mar 6, 2026
5 checks passed
rumpl added a commit to rumpl/cagent that referenced this pull request Mar 6, 2026
Follow the same pattern used for todos in PR docker#1960:
- Define a TaskStorage interface with Load/Save methods
- Extract file I/O into FileTaskStorage implementation
- Add TaskOption functional options with WithTaskStorage
- Move mutex from TasksTool into FileTaskStorage
- Update NewTasksTool to accept variadic options
- Add tests for WithTaskStorage and nil panic

No in-memory storage implementation is needed for tasks.

Assisted-By: cagent
rumpl added a commit to rumpl/cagent that referenced this pull request Mar 6, 2026
Follow the same pattern used for todos in PR docker#1960:
- Define a TaskStorage interface with domain-level methods
  (All, Get, Put, Delete) that map to what the toolset does
- Extract file I/O into FileTaskStorage implementation
- Add TaskOption functional options with WithTaskStorage
- Move mutex from TasksTool into FileTaskStorage
- Refactor all handlers to use the interface directly
- Update NewTasksTool to accept variadic options
- Add tests for WithTaskStorage and nil panic

No in-memory storage implementation is needed for tasks.

Assisted-By: cagent
rumpl added a commit to rumpl/cagent that referenced this pull request Mar 6, 2026
Follow the same pattern used for todos in PR docker#1960, with a
domain-level interface whose methods map to what the toolset does:

    type TaskStorage interface {
        Create(task Task) error
        Get(id string) (TaskWithStatus, error)
        List() ([]TaskWithStatus, error)
        Update(task Task) error
        Delete(id string) error
    }

The storage owns all data-integrity concerns:
- Create/Update validate that dependencies exist and are acyclic
- Delete cascades removal from other tasks' dependency lists
- Get/List compute effective status (blocked when a dep is not done)

This keeps the tool handlers thin: they deal with argument parsing,
description resolution, priority/status validation, and JSON
formatting — then delegate to the storage for persistence and
graph logic.

Other changes:
- Add FileTaskStorage (file-backed implementation)
- Add TaskOption / WithTaskStorage functional option
- Export TaskWithStatus (was taskWithEffective)
- Add sentinel errors: ErrTaskNotFound, ErrDependencyNotFound,
  ErrDependencyCycle, ErrDuplicateDependency
- Add FileTaskStorage unit tests for dep validation, cycle
  detection, cascade delete, and effective status
- Update NewTasksTool to accept variadic options

No in-memory storage implementation is needed for tasks.

Assisted-By: cagent
rumpl added a commit to rumpl/cagent that referenced this pull request Mar 6, 2026
Follow the same pattern used for todos in PR docker#1960, with a
domain-level interface whose methods map to what the toolset does:

    type TaskStorage interface {
        Create(task Task) error
        Get(id string) (TaskWithStatus, error)
        List() ([]TaskWithStatus, error)
        Update(task Task) error
        Delete(id string) error
    }

The storage owns all data-integrity concerns:
- Create/Update validate that dependencies exist and are acyclic
- Delete cascades removal from other tasks' dependency lists
- Get/List compute effective status (blocked when a dep is not done)

This keeps the tool handlers thin: they deal with argument parsing,
description resolution, priority/status validation, and JSON
formatting — then delegate to the storage for persistence and
graph logic.

Other changes:
- Add FileTaskStorage (file-backed implementation)
- Add TaskOption / WithTaskStorage functional option
  (nil is silently ignored, keeping the default storage)
- Export TaskWithStatus (was taskWithEffective)
- Add sentinel errors: ErrTaskNotFound, ErrDependencyNotFound,
  ErrDependencyCycle, ErrDuplicateDependency
- Add FileTaskStorage unit tests for dep validation, cycle
  detection, cascade delete, and effective status
- Update NewTasksTool to accept variadic options

No in-memory storage implementation is needed for tasks.

Assisted-By: cagent
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants