diff --git a/.changeset/clean-todos-display.md b/.changeset/clean-todos-display.md new file mode 100644 index 00000000000..ca712d58891 --- /dev/null +++ b/.changeset/clean-todos-display.md @@ -0,0 +1,5 @@ +--- +'@moonshot-ai/kimi-code-sdk': patch +--- + +Restore structured TodoList display metadata for v2 sessions so SDK consumers can render todo items and statuses. diff --git a/packages/agent-core-v2/src/agent/toolExecutor/toolExecutorService.ts b/packages/agent-core-v2/src/agent/toolExecutor/toolExecutorService.ts index 47fc56a48cf..8c05d9cded3 100644 --- a/packages/agent-core-v2/src/agent/toolExecutor/toolExecutorService.ts +++ b/packages/agent-core-v2/src/agent/toolExecutor/toolExecutorService.ts @@ -882,6 +882,7 @@ function normalizeToolResult(result: ExecutableToolResult): ToolResult { } const base: { output: ToolResult['output']; + display?: ToolInputDisplay; stopTurn?: boolean; truncated?: true; note?: string; @@ -889,6 +890,7 @@ function normalizeToolResult(result: ExecutableToolResult): ToolResult { spillExempt?: true; } = { output, + display: result.display, stopTurn: result.stopTurn, spill: result.spill, spillExempt: result.spillExempt, diff --git a/packages/agent-core-v2/src/features/todo/tools/todo-list/todoListTool.ts b/packages/agent-core-v2/src/features/todo/tools/todo-list/todoListTool.ts index 03a2bc1af9d..37a7c837049 100644 --- a/packages/agent-core-v2/src/features/todo/tools/todo-list/todoListTool.ts +++ b/packages/agent-core-v2/src/features/todo/tools/todo-list/todoListTool.ts @@ -42,10 +42,22 @@ export class TodoListTool implements ITodoListTool { : 'Updating todo list'; return { description, + display: + args.todos === undefined + ? undefined + : { + kind: 'todo_list', + items: args.todos.map((todo) => ({ ...todo })), + }, approvalRule: this.name, execute: async () => { if (args.todos === undefined) { - return { isError: false, output: renderTodoList(this.todo.get()) }; + const current = this.todo.get().map((todo) => ({ ...todo })); + return { + isError: false, + output: renderTodoList(current), + display: { kind: 'todo_list' as const, items: current }, + }; } const next: readonly TodoItem[] = args.todos.map((todo) => ({ diff --git a/packages/agent-core-v2/src/tool/toolContract.ts b/packages/agent-core-v2/src/tool/toolContract.ts index b368b2aa3b4..c2f00d4cc1a 100644 --- a/packages/agent-core-v2/src/tool/toolContract.ts +++ b/packages/agent-core-v2/src/tool/toolContract.ts @@ -31,6 +31,7 @@ export interface ToolDelivery { export interface ExecutableToolSuccessResult { readonly output: ExecutableToolOutput; + readonly display?: ToolInputDisplay; readonly isError?: false | undefined; readonly stopTurn?: boolean | undefined; readonly truncated?: boolean | undefined; @@ -43,6 +44,7 @@ export interface ExecutableToolSuccessResult { export interface ExecutableToolErrorResult { readonly output: ExecutableToolOutput; readonly isError: true; + readonly display?: ToolInputDisplay; readonly stopTurn?: boolean | undefined; readonly truncated?: boolean | undefined; readonly note?: string; diff --git a/packages/agent-core-v2/test/agent/toolExecutor/toolExecutor.test.ts b/packages/agent-core-v2/test/agent/toolExecutor/toolExecutor.test.ts index 6fd95df3191..6f7a01f295f 100644 --- a/packages/agent-core-v2/test/agent/toolExecutor/toolExecutor.test.ts +++ b/packages/agent-core-v2/test/agent/toolExecutor/toolExecutor.test.ts @@ -589,6 +589,28 @@ describe('AgentToolExecutorService', () => { }); }); + it('preserves a display produced at execution time in the final result', async () => { + const tool = new TestTool('dynamic-display', { + result: { + output: 'current state', + display: { + kind: 'generic', + summary: 'Resolved during execution', + detail: { value: 2 }, + }, + }, + }); + registry.register(tool); + + const [result] = await execute([toolCall('call_dynamic_display', 'dynamic-display', {})]); + + expect(result?.display).toEqual({ + kind: 'generic', + summary: 'Resolved during execution', + detail: { value: 2 }, + }); + }); + it('captures tool execution failures as error results', async () => { const tool = new TestTool('fail', { execute: async () => { diff --git a/packages/agent-core-v2/test/features/todo/tools/todo-list.test.ts b/packages/agent-core-v2/test/features/todo/tools/todo-list.test.ts index d53c310343e..9d194ff72ff 100644 --- a/packages/agent-core-v2/test/features/todo/tools/todo-list.test.ts +++ b/packages/agent-core-v2/test/features/todo/tools/todo-list.test.ts @@ -152,4 +152,57 @@ describe('TodoListTool', () => { expect(clearExecution.description).toBe('Clearing todo list'); expect(updateExecution.description).toBe('Updating todo list'); }); + + it('defers query display until execution while writes expose defensive input displays', async () => { + const current: TodoItem[] = [{ title: 'existing', status: 'in_progress' }]; + const { tool } = makeTool(current); + const next: TodoItem[] = [{ title: 'next', status: 'pending' }]; + + const readExecution = tool.resolveExecution({}); + const updateExecution = tool.resolveExecution({ todos: next }); + const clearExecution = tool.resolveExecution({ todos: [] }); + + if ( + readExecution.isError === true || + updateExecution.isError === true || + clearExecution.isError === true + ) { + throw new TypeError('expected runnable executions'); + } + + expect(readExecution.display).toBeUndefined(); + expect(updateExecution.display).toEqual({ + kind: 'todo_list', + items: [{ title: 'next', status: 'pending' }], + }); + expect(clearExecution.display).toEqual({ kind: 'todo_list', items: [] }); + + next[0] = { title: 'mutated next', status: 'done' }; + expect(updateExecution.display).toEqual({ + kind: 'todo_list', + items: [{ title: 'next', status: 'pending' }], + }); + + next[0] = { title: 'next', status: 'pending' }; + await updateExecution.execute({ + turnId: 1, + toolCallId: 'update', + signal, + }); + const readResult = await readExecution.execute({ + turnId: 1, + toolCallId: 'read', + signal, + }); + expect(readResult.display).toEqual({ + kind: 'todo_list', + items: [{ title: 'next', status: 'pending' }], + }); + + current[0] = { title: 'mutated current', status: 'done' }; + expect(readResult.display).toEqual({ + kind: 'todo_list', + items: [{ title: 'next', status: 'pending' }], + }); + }); });