diff --git a/.changeset/acp-todolist-plan.md b/.changeset/acp-todolist-plan.md new file mode 100644 index 0000000000..d43a082813 --- /dev/null +++ b/.changeset/acp-todolist-plan.md @@ -0,0 +1,6 @@ +--- +"@moonshot-ai/agent-core-v2": patch +"@moonshot-ai/kimi-code": patch +--- + +Fix ACP `plan` updates never being emitted for TodoList calls on the v2 engine: the v2 TodoList tool now attaches the `todo_list` input-display block to its execution, so `kimi acp` sends a `plan` session update whenever the agent reads or updates its todo list (previously the display was never attached, so the plan mapping in the ACP adapter could never fire). diff --git a/packages/agent-core-v2/src/agent/tools/todo-list/todoListTool.ts b/packages/agent-core-v2/src/agent/tools/todo-list/todoListTool.ts index 6d0b0e36c6..236e30526b 100644 --- a/packages/agent-core-v2/src/agent/tools/todo-list/todoListTool.ts +++ b/packages/agent-core-v2/src/agent/tools/todo-list/todoListTool.ts @@ -50,6 +50,13 @@ export class TodoListTool implements ITodoListTool { : 'Updating todo list'; return { description, + display: { + kind: 'todo_list', + items: (args.todos ?? this.todo.getTodos()).map((todo) => ({ + title: todo.title, + status: todo.status, + })), + }, approvalRule: this.name, execute: async () => { if (args.todos === undefined) { diff --git a/packages/agent-core-v2/test/session/todo/tools/todo-list.test.ts b/packages/agent-core-v2/test/session/todo/tools/todo-list.test.ts index 66766d70ff..f1c1bbf60d 100644 --- a/packages/agent-core-v2/test/session/todo/tools/todo-list.test.ts +++ b/packages/agent-core-v2/test/session/todo/tools/todo-list.test.ts @@ -171,4 +171,48 @@ describe('TodoListTool', () => { expect(clearExecution.description).toBe('Clearing todo list'); expect(updateExecution.description).toBe('Updating todo list'); }); + + it('resolveExecution attaches a todo_list display block so ACP can emit a plan update', () => { + const { tool } = makeTool([{ title: 'existing', status: 'in_progress' }]); + + const updateExecution = tool.resolveExecution({ + todos: [ + { title: 'first', status: 'pending' }, + { title: 'second', status: 'done' }, + ], + }); + if (updateExecution.isError === true) { + throw new TypeError('expected a runnable execution'); + } + expect(updateExecution.display).toEqual({ + kind: 'todo_list', + items: [ + { title: 'first', status: 'pending' }, + { title: 'second', status: 'done' }, + ], + }); + }); + + it('query mode display reflects the current stored list', () => { + const { tool } = makeTool([{ title: 'existing', status: 'in_progress' }]); + + const readExecution = tool.resolveExecution({}); + if (readExecution.isError === true) { + throw new TypeError('expected a runnable execution'); + } + expect(readExecution.display).toEqual({ + kind: 'todo_list', + items: [{ title: 'existing', status: 'in_progress' }], + }); + }); + + it('clear mode display carries an empty items array', () => { + const { tool } = makeTool([{ title: 'existing', status: 'pending' }]); + + const clearExecution = tool.resolveExecution({ todos: [] }); + if (clearExecution.isError === true) { + throw new TypeError('expected a runnable execution'); + } + expect(clearExecution.display).toEqual({ kind: 'todo_list', items: [] }); + }); });