feat: add agent todo tool and inline todo snapshot cards in chat
This commit is contained in:
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user