Skip to content

Commit 385ffab

Browse files
committed
feat(graph): add session todo list tool and documentation
Add the `SessionTodoTool` as a simpler model-facing interface over the existing todo store, alongside documentation explaining its usage and differences from the kanban board. The session list uses a single-call write model keyed by thread ID, with all argument problems returned as recoverable tool errors rather than fatal run errors. Auto-committed-on: macbook
1 parent 98e7948 commit 385ffab

2 files changed

Lines changed: 56 additions & 7 deletions

File tree

‎crates/tinyagents-graph/src/todos/test.rs‎

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -664,7 +664,11 @@ mod session_list_tests {
664664
}
665665
}
666666

667-
async fn run(tool: &SessionTodoTool, thread: Option<&str>, args: serde_json::Value) -> ToolResult {
667+
async fn run(
668+
tool: &SessionTodoTool,
669+
thread: Option<&str>,
670+
args: serde_json::Value,
671+
) -> ToolResult {
668672
let context = ThreadContext(thread.map(str::to_owned));
669673
tool.execute_with_context(args, Default::default(), Some(&context))
670674
.await
@@ -711,7 +715,11 @@ mod session_list_tests {
711715
)
712716
.await;
713717
let p = raw(&rewritten);
714-
assert_eq!(p["todos"].as_array().unwrap().len(), 1, "a write is the whole list");
718+
assert_eq!(
719+
p["todos"].as_array().unwrap().len(),
720+
1,
721+
"a write is the whole list"
722+
);
715723
assert_eq!(p["todos"][0]["status"], "completed");
716724

717725
let cleared = run(&tool, Some("t"), json!({ "todos": [] })).await;
@@ -721,8 +729,12 @@ mod session_list_tests {
721729
#[tokio::test]
722730
async fn lists_are_keyed_by_thread() {
723731
let tool = SessionTodoTool::new(store());
724-
run(&tool, Some("a"), json!({ "todos": [{ "content": "only a", "status": "pending" }] }))
725-
.await;
732+
run(
733+
&tool,
734+
Some("a"),
735+
json!({ "todos": [{ "content": "only a", "status": "pending" }] }),
736+
)
737+
.await;
726738
let b = run(&tool, Some("b"), json!({})).await;
727739
assert!(raw(&b)["todos"].as_array().unwrap().is_empty());
728740
}
@@ -734,10 +746,19 @@ mod session_list_tests {
734746
async fn bad_input_is_a_tool_error_not_an_err() {
735747
let tool = SessionTodoTool::new(store());
736748
for (args, expect) in [
737-
(json!({ "todos": [{ "content": " ", "status": "pending" }] }), "content"),
738-
(json!({ "todos": [{ "content": "x", "status": "someday" }] }), "invalid status"),
749+
(
750+
json!({ "todos": [{ "content": " ", "status": "pending" }] }),
751+
"content",
752+
),
753+
(
754+
json!({ "todos": [{ "content": "x", "status": "someday" }] }),
755+
"invalid status",
756+
),
739757
(json!({ "todos": "not a list" }), "invalid `todos`"),
740-
(json!({ "cards": [{ "content": "x", "status": "todo" }] }), "pass `todos`"),
758+
(
759+
json!({ "cards": [{ "content": "x", "status": "todo" }] }),
760+
"pass `todos`",
761+
),
741762
(
742763
json!({ "todos": [
743764
{ "content": "a", "status": "in_progress" },

‎docs/modules/graph/todos.md‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,31 @@ Unit tests in `crates/tinyagents-graph/src/todos/test.rs` (types, store invarian
8585
end-to-end model-driven tool run in `tests/e2e_graph_todos.rs`; feature coverage
8686
for the run lifecycle in `tests/feature_graph_task_runs.rs`; and the full
8787
dispatch loop in `tests/e2e_graph_task_dispatch.rs`.
88+
89+
## Session todo list (`todos::session_list`)
90+
91+
Not every host wants a dispatchable kanban board. `SessionTodoTool` is the
92+
other model-facing shape over the same store: the session todo list Claude
93+
Code and Codex use.
94+
95+
- One call writes the whole list: `{"todos": [{"content": "...", "status":
96+
"pending" | "in_progress" | "completed"}]}`. An empty list clears it; a call
97+
with no arguments reads it back. There is no `op`, no ids, no approval
98+
gate, evidence, plan or blocker.
99+
- It shares `store::replace` / `store::list`, so ordering, the
100+
single-`in_progress` invariant and the markdown rendering are the board's.
101+
Board-only states fold on the way out (`ready`/`awaiting_approval`/
102+
`blocked` → `pending`, `rejected` → `completed`).
103+
- The list is keyed by `ToolRunContext::thread_id`. A host that scopes lists
104+
by something else (an agent session id, say) calls
105+
`session_list::call(store, key, &args)` or `write` / `read` with its own
106+
key instead of the `Tool` entry point.
107+
- Every argument problem — wrong key (`cards`), a non-array, empty content,
108+
an unknown status, two `in_progress` items — is a `ToolResult::error` the
109+
model can correct. It is never an `Err`: an `Err` out of a tool dispatch is
110+
fatal to the run, and a host lost a turn exactly that way when a model sent
111+
the retired `{"cards": …}` shape to a host-side copy of this tool.
112+
113+
Register with `register_session_todo_tool(&mut registry, store)` in place of
114+
`register_todo_tools`; the two share the name `todo`, so a registry holds one
115+
or the other.

0 commit comments

Comments
 (0)