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
6 changes: 6 additions & 0 deletions .changeset/acp-todolist-plan.md
Original file line number Diff line number Diff line change
@@ -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).
Original file line number Diff line number Diff line change
Expand Up @@ -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) => ({

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 Emit read-mode plans after execution to avoid stale ACP state

When a single assistant response batches TodoList({ todos: ... }) followed by TodoList({}), the executor prepares all calls and publishes each tool.call.started (and therefore each ACP plan) before executeBatch runs any task, while the default ToolAccesses.all() only serializes the later executions. This read-mode fallback snapshots the pre-write list, so the later read's plan update can overwrite the correct write plan with stale entries; the read tool result will see the updated list after the write executes, but ACP never gets a result-time plan refresh. Emit the query-mode plan from the executed/current list or suppress the started-time display for reads.

Useful? React with 👍 / 👎.

title: todo.title,
status: todo.status,
})),
},
approvalRule: this.name,
execute: async () => {
if (args.todos === undefined) {
Expand Down
44 changes: 44 additions & 0 deletions packages/agent-core-v2/test/session/todo/tools/todo-list.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [] });
});
});