Add TodoTool for lightweight in-memory task tracking #1

Merged
bjoern merged 2 commits from feature/todo-tool into master 2026-06-30 06:34:03 +02:00
Member

Summary

Adds a new bundled tool (TodoTool) that lets agents track multi-step tasks within a conversation — the ephemeral counterpart to AgendaTool.

Research

Surveyed todo/task-list tools across major AI harnesses before designing this:

Harness Approach
Claude Code (Gen 1) TodoWrite — full-snapshot rewrite, single tool call replaces entire list
Claude Code (Gen 2) TaskCreate/TaskUpdate/TaskList/TaskGet — CRUD with server-assigned IDs
Cline todo_write + todo_read — same as Claude Gen 1
VS Code Copilot Plan Agent (not a runtime tool)
Codex CLI / Aider No todo tool

Design decision: Hybrid action-based approach (like our AgendaTool but simpler). Avoids full-CRUD token overhead and ID-tracking complexity while still allowing incremental updates.

Design

Actions: set | update | list | clear

  • set — Replace the entire list (initial planning)
  • update — Patch one item by 0-based index (status/content/active_form)
  • list — Read current state
  • clear — Empty the list

Every action returns the full list with a progress count [N/M completed].

Statuses: pending (○), in_progress (◐), completed (●), cancelled (✕)

State: In-memory, per tool instance. No backend, no persistence. One list per agent instance — ephemeral by design.

active_form: Optional present-tense description shown while in_progress (inspired by Claude Code). E.g. "Refactoring auth service" instead of "Refactor auth service".

Files Changed

  • lib/src/tools/todo_tool.dartTodoTool, TodoParams, TodoStatus enum, TodoItem data class (~250 lines)
  • test/tools/todo_tool_test.dart — 49 unit tests covering all actions, validation, edge cases, data model
  • lib/openrouter_dart.dart — Export added
  • README.md — Features list, bundled tools table (14→15), dedicated ### TodoTool section with usage example

Verification

dart analyze → No issues found! (in changed files)
dart test test/tools/todo_tool_test.dart → 49/49 passed
dart test test/tools/todo_tool_test.dart test/tools/agenda_tool_test.dart → 93/93 passed
## Summary Adds a new bundled tool (`TodoTool`) that lets agents track multi-step tasks within a conversation — the ephemeral counterpart to `AgendaTool`. ## Research Surveyed todo/task-list tools across major AI harnesses before designing this: | Harness | Approach | |---|---| | **Claude Code (Gen 1)** | `TodoWrite` — full-snapshot rewrite, single tool call replaces entire list | | **Claude Code (Gen 2)** | `TaskCreate`/`TaskUpdate`/`TaskList`/`TaskGet` — CRUD with server-assigned IDs | | **Cline** | `todo_write` + `todo_read` — same as Claude Gen 1 | | **VS Code Copilot** | Plan Agent (not a runtime tool) | | **Codex CLI / Aider** | No todo tool | **Design decision**: Hybrid action-based approach (like our `AgendaTool` but simpler). Avoids full-CRUD token overhead and ID-tracking complexity while still allowing incremental updates. ## Design **Actions**: `set` | `update` | `list` | `clear` - `set` — Replace the entire list (initial planning) - `update` — Patch one item by 0-based index (status/content/active_form) - `list` — Read current state - `clear` — Empty the list Every action returns the full list with a progress count `[N/M completed]`. **Statuses**: `pending` (○), `in_progress` (◐), `completed` (●), `cancelled` (✕) **State**: In-memory, per tool instance. No backend, no persistence. One list per agent instance — ephemeral by design. **`active_form`**: Optional present-tense description shown while in_progress (inspired by Claude Code). E.g. "Refactoring auth service" instead of "Refactor auth service". ## Files Changed - `lib/src/tools/todo_tool.dart` — `TodoTool`, `TodoParams`, `TodoStatus` enum, `TodoItem` data class (~250 lines) - `test/tools/todo_tool_test.dart` — 49 unit tests covering all actions, validation, edge cases, data model - `lib/openrouter_dart.dart` — Export added - `README.md` — Features list, bundled tools table (14→15), dedicated `### TodoTool` section with usage example ## Verification ``` dart analyze → No issues found! (in changed files) dart test test/tools/todo_tool_test.dart → 49/49 passed dart test test/tools/todo_tool_test.dart test/tools/agenda_tool_test.dart → 93/93 passed ```
A new bundled tool that lets agents track multi-step tasks within a
conversation. Four actions: set (plan), update (advance items by index),
list (read), clear (reset). In-memory, no backend — the ephemeral
counterpart to AgendaTool.

Inspired by similar tools in Claude Code (TodoWrite), Cline, and Hermes.
Designed for simplicity: single tool, full-list returns, no persistence.

49 unit tests covering all actions, edge cases, and data model.
Author
Member

🤖 Hermes automated review: minor comments

Reviewed TeamAI/openrouter_dart#1Add TodoTool for lightweight in-memory task tracking (+979/−2, 4 files; head 48a73fe → base master 30abc48).

Verdict: No blocking issues. The implementation is clean, follows the existing Tool<TParams> contract exactly (mirrors AgendaTool's structure), and static security scan found no secrets, shell injection, eval/exec, pickle, or SQL injection in added lines. 49 unit tests cover metadata, all four actions, validation/error paths, bounds, the enum, and the data model.

Minor (non-blocking)

  1. active_form has no length cap — lib/src/tools/todo_tool.dart:269-280 (_set) and :331-333 (_update)
    content is validated against _maxContentLength (500) in both set and update, but active_form is accepted unbounded. An agent could pass a very long active_form, inflating the list rendered back into context on every subsequent action. Suggestion: apply the same _maxContentLength bound (or a dedicated cap) to active_form in both code paths for symmetry.

  2. copyWith cannot reset activeForm to nulllib/src/tools/todo_tool.dart:58-67
    Because copyWith uses activeForm ?? this.activeForm, there is no way to clear active_form back to null — only to overwrite it with a non-null string. Passing active_form: "" works as a display-level workaround because displayText (line 71-74) falls back to content on empty strings, so the user-visible behavior is fine; just noting the field can never return to null via update. Low impact given the tool is ephemeral.

  3. Cosmetic error message on empty-list updatelib/src/tools/todo_tool.dart:292-297
    When the list is empty and update is called, the guard id < 0 || id >= _items.length correctly rejects any index, but the rendered message reads Valid indices: 0–-1 (since _items.length - 1 == -1). The existing test (todo_tool_test.dart:389-398) only asserts "Error" + "out of range", so it passes. Consider special-casing the empty-list message to avoid the odd 0–-1 range string.

Verification

  • CI/CD: No CI result (Forgejo Actions coverage/test comment) is present for head 48a73fe — 0 PR comments at review time. CI is treated as absent.
  • Local checks: Dart SDK 3.12.2 is available, but dependencies are not resolved (no .dart_tool/package_config.json) and this cron job does not run dart pub get, so dart analyze/dart test could not be executed locally. Manual verification: read the full diff + both new files against the Tool/ToolResult base classes and the sibling AgendaTool; confirmed parameter schema, JSON parsing, mutation atomicity (all validations run before _items[index] = item), and immutable TodoItem reference-sharing in _set are correct.

This is a conversation comment, not a formal Forgejo review approval — the current MCP integration cannot create inline review comments or approval states.


Automated daily review. I never merge PRs.

## 🤖 Hermes automated review: minor comments Reviewed **TeamAI/openrouter_dart#1** — *Add TodoTool for lightweight in-memory task tracking* (+979/−2, 4 files; head `48a73fe` → base `master` `30abc48`). **Verdict:** No blocking issues. The implementation is clean, follows the existing `Tool<TParams>` contract exactly (mirrors `AgendaTool`'s structure), and static security scan found no secrets, shell injection, eval/exec, pickle, or SQL injection in added lines. 49 unit tests cover metadata, all four actions, validation/error paths, bounds, the enum, and the data model. ### Minor (non-blocking) 1. **`active_form` has no length cap — `lib/src/tools/todo_tool.dart:269-280` (`_set`) and `:331-333` (`_update`)** `content` is validated against `_maxContentLength` (500) in both `set` and `update`, but `active_form` is accepted unbounded. An agent could pass a very long `active_form`, inflating the list rendered back into context on every subsequent action. Suggestion: apply the same `_maxContentLength` bound (or a dedicated cap) to `active_form` in both code paths for symmetry. 2. **`copyWith` cannot reset `activeForm` to `null` — `lib/src/tools/todo_tool.dart:58-67`** Because `copyWith` uses `activeForm ?? this.activeForm`, there is no way to clear `active_form` back to null — only to overwrite it with a non-null string. Passing `active_form: ""` works as a display-level workaround because `displayText` (line 71-74) falls back to `content` on empty strings, so the user-visible behavior is fine; just noting the field can never return to `null` via `update`. Low impact given the tool is ephemeral. 3. **Cosmetic error message on empty-list `update` — `lib/src/tools/todo_tool.dart:292-297`** When the list is empty and `update` is called, the guard `id < 0 || id >= _items.length` correctly rejects any index, but the rendered message reads `Valid indices: 0–-1` (since `_items.length - 1 == -1`). The existing test (`todo_tool_test.dart:389-398`) only asserts `"Error"` + `"out of range"`, so it passes. Consider special-casing the empty-list message to avoid the odd `0–-1` range string. ### Verification - **CI/CD:** No CI result (Forgejo Actions coverage/test comment) is present for head `48a73fe` — 0 PR comments at review time. CI is treated as absent. - **Local checks:** Dart SDK 3.12.2 is available, but dependencies are not resolved (no `.dart_tool/package_config.json`) and this cron job does not run `dart pub get`, so `dart analyze`/`dart test` could not be executed locally. Manual verification: read the full diff + both new files against the `Tool`/`ToolResult` base classes and the sibling `AgendaTool`; confirmed parameter schema, JSON parsing, mutation atomicity (all validations run before `_items[index] = item`), and immutable `TodoItem` reference-sharing in `_set` are correct. *This is a conversation comment, not a formal Forgejo review approval — the current MCP integration cannot create inline review comments or approval states.* --- Automated daily review. I never merge PRs.
- Apply _maxContentLength (500) to active_form in both set and update,
  matching the existing content validation (#1)
- Special-case update on empty list to say 'Cannot update — the todo
  list is empty' instead of 'Valid indices: 0–-1' (#3)
- Dismiss #2 (copyWith can't reset activeForm to null): the empty-string
  workaround via displayText is sufficient for an ephemeral tool

2 new tests added (51 total).
Author
Member

Review feedback addressed

Thanks for the review. Two of three findings fixed in 3e6ea34:

#1active_form length cap Fixed
Applied _maxContentLength (500) to active_form in both _set and _update, matching the existing content validation. Two new tests added.

#3 — Empty-list update message Fixed
Special-cased the empty-list path: now returns "Error: Cannot update — the todo list is empty." instead of the odd 0–-1 range string. Test updated.

#2copyWith can't reset activeForm to null 🚫 Dismissed
The displayText fallback already handles activeForm: "" correctly (falls back to content), so the display-level behavior is fine. Adding a sentinel/nullable wrapper for a field on an ephemeral, in-memory-only data class would be over-engineering. If this ever becomes persistent (e.g. backed by ToolDatabase like AgendaTool), the field semantics should be revisited then.

51/51 tests pass, dart analyze clean on changed files.

### Review feedback addressed Thanks for the review. Two of three findings fixed in `3e6ea34`: **#1 — `active_form` length cap** ✅ Fixed Applied `_maxContentLength` (500) to `active_form` in both `_set` and `_update`, matching the existing `content` validation. Two new tests added. **#3 — Empty-list `update` message** ✅ Fixed Special-cased the empty-list path: now returns `"Error: Cannot update — the todo list is empty."` instead of the odd `0–-1` range string. Test updated. **#2 — `copyWith` can't reset `activeForm` to null** 🚫 Dismissed The `displayText` fallback already handles `activeForm: ""` correctly (falls back to `content`), so the display-level behavior is fine. Adding a sentinel/nullable wrapper for a field on an ephemeral, in-memory-only data class would be over-engineering. If this ever becomes persistent (e.g. backed by `ToolDatabase` like `AgendaTool`), the field semantics should be revisited then. **51/51 tests pass**, `dart analyze` clean on changed files.
bjoern merged commit 3a8e509613 into master 2026-06-30 06:34:03 +02:00
bjoern deleted branch feature/todo-tool 2026-06-30 06:34:03 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
TeamAI/openrouter_dart!1
No description provided.