From 8d4dd8bed3f360c6b2f60790996f352f0509c9c2 Mon Sep 17 00:00:00 2001 From: qer Date: Tue, 30 Jun 2026 18:22:20 +0800 Subject: [PATCH] fix(todo-list): accept 'completed' status and normalize to 'done' Closes #1016 The LLM sometimes passes 'completed' as the status for TodoList items, but the schema only accepted 'pending' | 'in_progress' | 'done'. This produced two problems: 1. Validation failed when the model used 'completed'. 2. Even if validation passed, statusMarker() had no case for 'completed' and fell through to the unreachable default branch. Changes: - Extend TodoStatus union to include 'completed' so it is accepted at the type level. - Map 'completed' -> 'done' in setTodos() so persisted state stays clean. - Handle 'completed' in statusMarker() so it renders as '[done]'. - Update the markdown description to explicitly warn against using 'completed'. - Add a test confirming 'completed' is accepted and mapped to 'done'. --- .../src/tools/builtin/state/todo-list.md | 3 ++- .../src/tools/builtin/state/todo-list.ts | 12 +++++++++--- .../agent-core/test/tools/todo-list.test.ts | 17 +++++++++++++++++ 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/packages/agent-core/src/tools/builtin/state/todo-list.md b/packages/agent-core/src/tools/builtin/state/todo-list.md index 3dc3c08dc..aeec3986d 100644 --- a/packages/agent-core/src/tools/builtin/state/todo-list.md +++ b/packages/agent-core/src/tools/builtin/state/todo-list.md @@ -19,9 +19,10 @@ Use this tool to maintain a structured TODO list as you work through a multi-ste - If no available tool can move any task forward, tell the user where you are stuck instead of repeatedly re-ordering the same todos. **How to use:** -- Call with `todos: [...]` to replace the full list. Statuses: pending / in_progress / done. +- Call with `todos: [...]` to replace the full list. Statuses: `pending` / `in_progress` / `done`. - Call with no `todos` argument to retrieve the current list without changing it. - Call with `todos: []` to clear the list. +- **Important:** the status must be exactly `done`, not `completed` or `finished`. - Keep titles short and actionable (e.g. "Read session-control.ts", "Add planMode flag to TurnManager"). - Update statuses as you make progress. - When work is underway, keep exactly one task `in_progress`. diff --git a/packages/agent-core/src/tools/builtin/state/todo-list.ts b/packages/agent-core/src/tools/builtin/state/todo-list.ts index 852042e19..9a5ffdb20 100644 --- a/packages/agent-core/src/tools/builtin/state/todo-list.ts +++ b/packages/agent-core/src/tools/builtin/state/todo-list.ts @@ -28,7 +28,7 @@ export const TODO_STORE_KEY = 'todo'; const TODO_LIST_WRITE_REMINDER = 'Ensure that you continue to use the todo list to track progress. Mark tasks done immediately after finishing them, and keep exactly one task in_progress when work is underway.'; -export type TodoStatus = 'pending' | 'in_progress' | 'done'; +export type TodoStatus = 'pending' | 'in_progress' | 'done' | 'completed'; export interface TodoItem { readonly title: string; @@ -45,7 +45,9 @@ declare module '../../store' { const TodoItemSchema = z.object({ title: z.string().min(1).describe('Short, actionable title for the todo.'), - status: z.enum(['pending', 'in_progress', 'done']).describe('Current status of the todo.'), + status: z + .preprocess((val) => (val === 'completed' ? 'done' : val), z.enum(['pending', 'in_progress', 'done'])) + .describe('Current status of the todo. Must be exactly one of: pending, in_progress, done. Do NOT use completed or finished.'), }); export interface TodoListInput { @@ -81,6 +83,7 @@ function statusMarker(status: TodoStatus): string { case 'in_progress': return '[in_progress]'; case 'done': + case 'completed': return '[done]'; default: { const _exhaustive: never = status; @@ -133,7 +136,10 @@ export class TodoListTool implements BuiltinTool { private setTodos(todos: readonly TodoItem[]): void { this.store.set( TODO_STORE_KEY, - todos.map((todo) => ({ title: todo.title, status: todo.status })), + todos.map((todo) => ({ + title: todo.title, + status: todo.status === 'completed' ? 'done' : todo.status, + })), ); } } diff --git a/packages/agent-core/test/tools/todo-list.test.ts b/packages/agent-core/test/tools/todo-list.test.ts index 003e14a2e..3d8f4aad3 100644 --- a/packages/agent-core/test/tools/todo-list.test.ts +++ b/packages/agent-core/test/tools/todo-list.test.ts @@ -142,6 +142,23 @@ describe('TodoListTool', () => { ]); }); + it('accepts "completed" as a status and maps it to "done"', async () => { + const { tool, getTodos } = makeTool(); + + const result = await executeTool(tool, { + turnId: 't1', + toolCallId: 'call_1', + args: { + todos: [{ title: 'done task', status: 'completed' }], + }, + signal, + }); + + expect(result).toMatchObject({ isError: false }); + expect(result.output).toContain('[done] done task'); + expect(getTodos()).toEqual([{ title: 'done task', status: 'done' }]); + }); + it('renders a done todo with a marker matching the status enum value', async () => { const { tool } = makeTool([{ title: 'shipped', status: 'done' }]);