feat: add agent todo tool and inline todo snapshot cards in chat

This commit is contained in:
Xiaohan-Tian
2026-06-02 21:52:44 -07:00
parent c1d9f4c9fb
commit 5aacf01457
24 changed files with 995 additions and 28 deletions
+6 -1
View File
@@ -16,6 +16,7 @@ export interface ToolParameter {
type: 'string' | 'number' | 'boolean' | 'array' | 'object';
description: string;
required?: boolean;
enum?: string[];
items?: ToolParameter; // For array types
properties?: Record<string, ToolParameter>; // For object types
}
@@ -114,6 +115,10 @@ export abstract class BaseTool {
schema.items = this.convertParamToJsonSchema(param.items);
}
if (param.enum) {
schema.enum = param.enum;
}
if (param.type === 'object' && param.properties) {
const properties: Record<string, unknown> = {};
const required: string[] = [];
@@ -237,4 +242,4 @@ export abstract class BaseTool {
result
};
}
}
}
@@ -0,0 +1,90 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
vi.mock('../../stores/projectStore', () => ({
useProjectStore: {
getState: () => ({
refreshProjectState: vi.fn(),
}),
},
}));
import { AgentCore } from '../core/AgentCore';
import { UpdateTodoListTool } from './UpdateTodoListTool';
describe('UpdateTodoListTool', () => {
beforeEach(() => {
AgentCore.instance().clearConversation();
});
it('accepts a valid full-list replacement and updates agent state', async () => {
const tool = new UpdateTodoListTool();
const result = await tool.execute({
items: [
{ id: '1', text: 'Inspect current region', status: 'completed' },
{ id: '2', text: 'Draft harmony', status: 'in_progress', activeText: 'Drafting harmony' },
],
});
expect(result.success).toBe(true);
expect(result.result).toContain('(1/2 completed)');
expect(AgentCore.instance().getAgentState().getTodos()).toEqual([
expect.objectContaining({ id: '1', text: 'Inspect current region', status: 'completed' }),
expect.objectContaining({ id: '2', text: 'Draft harmony', status: 'in_progress', activeText: 'Drafting harmony' }),
]);
});
it('rejects empty todo text', async () => {
const tool = new UpdateTodoListTool();
const result = await tool.execute({
items: [
{ id: '1', text: ' ', status: 'pending' },
],
});
expect(result.success).toBe(false);
expect(result.result).toContain('text is required');
});
it('rejects invalid statuses', async () => {
const tool = new UpdateTodoListTool();
const result = await tool.execute({
items: [
{ id: '1', text: 'Task', status: 'active' },
],
});
expect(result.success).toBe(false);
expect(result.result).toContain("invalid status 'active'");
});
it('rejects duplicate ids', async () => {
const tool = new UpdateTodoListTool();
const result = await tool.execute({
items: [
{ id: '1', text: 'Task A', status: 'pending' },
{ id: '1', text: 'Task B', status: 'pending' },
],
});
expect(result.success).toBe(false);
expect(result.result).toContain('duplicate id');
});
it('rejects multiple in-progress items', async () => {
const tool = new UpdateTodoListTool();
const result = await tool.execute({
items: [
{ id: '1', text: 'Task A', status: 'in_progress' },
{ id: '2', text: 'Task B', status: 'in_progress' },
],
});
expect(result.success).toBe(false);
expect(result.result).toContain('Only one todo item can be in_progress');
});
});
+60
View File
@@ -0,0 +1,60 @@
import { AgentCore } from '../core/AgentCore';
import { renderTodoList, summarizeTodoCounts, validateAndNormalizeTodos, type TodoInputItem } from '../core/todo';
import { BaseTool } from './BaseTool';
import type { ToolParameter, ToolResult } from './BaseTool';
export class UpdateTodoListTool extends BaseTool {
readonly name = 'update_todo_list';
readonly description = 'Replace the current task checklist for multi-step work and keep progress updated.';
readonly parameters: Record<string, ToolParameter> = {
items: {
type: 'array',
description: 'The full todo list to keep for the current task.',
required: true,
items: {
type: 'object',
description: 'A single todo item.',
properties: {
id: {
type: 'string',
description: 'Stable task id.',
},
text: {
type: 'string',
description: 'User-visible task description.',
required: true,
},
status: {
type: 'string',
description: 'Current task status.',
required: true,
enum: ['pending', 'in_progress', 'completed'],
},
activeText: {
type: 'string',
description: 'Optional present-tense wording to show while the task is in progress.',
},
},
},
},
};
async execute(params: Record<string, unknown>): Promise<ToolResult> {
try {
this.validateParameters(params);
const items = (params.items as TodoInputItem[]) ?? [];
const todos = validateAndNormalizeTodos(items);
AgentCore.instance().getAgentState().setTodos(todos);
const counts = summarizeTodoCounts(todos);
const rendered = renderTodoList(todos);
return this.createSuccessResult(
`${rendered}\n\nTotal: ${counts.total}, in progress: ${counts.inProgress}, pending: ${counts.pending}, completed: ${counts.completed}`,
);
} catch (error) {
return this.createErrorResult(error instanceof Error ? error.message : 'Failed to update todo list');
}
}
}
+3 -1
View File
@@ -7,11 +7,13 @@ import { AddNotesTool } from './AddNotesTool';
import { RemoveNotesTool } from './RemoveNotesTool';
import { ReadMusicTool } from './ReadMusicTool';
import { ReadChordProgressionTool } from './ReadChordProgressionTool';
import { UpdateTodoListTool } from './UpdateTodoListTool';
export { AddNotesTool, RemoveNotesTool, ReadMusicTool, ReadChordProgressionTool };
export { AddNotesTool, RemoveNotesTool, ReadMusicTool, ReadChordProgressionTool, UpdateTodoListTool };
// Tool registry for easy access
export const AVAILABLE_TOOLS = {
update_todo_list: UpdateTodoListTool,
add_notes: AddNotesTool,
remove_notes: RemoveNotesTool,
read_music: ReadMusicTool,