Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/agent-core/src/tools/builtin/state/todo-list.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
12 changes: 9 additions & 3 deletions packages/agent-core/src/tools/builtin/state/todo-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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']))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep invalid todo statuses rejected

In the live tool-call path, tool-call.ts only compiles tool.parameters with AJV and passes the original parsedArgs.data into resolveExecution, so this zod preprocess is not run during execution. Because toInputJsonSchema renders the input side of a preprocess as an unconstrained transform input, the generated JSON Schema drops the pending | in_progress | done enum for status; calls such as { todos: [{ title: 'x', status: 'wip' }] } can now validate and be persisted/rendered instead of being rejected. The new test calls executeTool directly, so it misses this validator path.

Useful? React with 👍 / 👎.

.describe('Current status of the todo. Must be exactly one of: pending, in_progress, done. Do NOT use completed or finished.'),
});

export interface TodoListInput {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -133,7 +136,10 @@ export class TodoListTool implements BuiltinTool<TodoListInput> {
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,
})),
);
}
}
17 changes: 17 additions & 0 deletions packages/agent-core/test/tools/todo-list.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' }]);

Expand Down
Loading