/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import { BaseDeclarativeTool, BaseToolInvocation, Kind, type ToolInvocation, type Todo, type ToolResult, } from './tools.js'; import type { MessageBus } from '../confirmation-bus/message-bus.js'; import { WRITE_TODOS_TOOL_NAME } from './tool-names.js'; import { WRITE_TODOS_DEFINITION } from './definitions/coreTools.js'; import { resolveToolDeclaration } from './definitions/resolver.js'; const TODO_STATUSES = [ 'pending', 'in_progress', 'completed', 'cancelled', ] as const; export interface WriteTodosToolParams { /** * The full list of todos. This will overwrite any existing list. */ todos: Todo[]; } class WriteTodosToolInvocation extends BaseToolInvocation< WriteTodosToolParams, ToolResult > { constructor( params: WriteTodosToolParams, messageBus: MessageBus, _toolName?: string, _toolDisplayName?: string, ) { super(params, messageBus, _toolName, _toolDisplayName); } getDescription(): string { const count = this.params.todos?.length ?? 0; if (count === 0) { return 'Cleared todo list'; } return `Set ${count} todo(s)`; } async execute( _signal: AbortSignal, _updateOutput?: (output: string) => void, ): Promise { const todos = this.params.todos ?? []; const todoListString = todos .map( (todo, index) => `${index + 1}. [${todo.status}] ${todo.description}`, ) .join('\n'); const llmContent = todos.length > 0 ? `Successfully updated the todo list. The current list is now:\n${todoListString}` : 'Successfully cleared the todo list.'; return { llmContent, returnDisplay: { todos }, }; } } export class WriteTodosTool extends BaseDeclarativeTool< WriteTodosToolParams, ToolResult > { static readonly Name = WRITE_TODOS_TOOL_NAME; constructor(messageBus: MessageBus) { super( WriteTodosTool.Name, 'WriteTodos', WRITE_TODOS_DEFINITION.base.description!, Kind.Other, WRITE_TODOS_DEFINITION.base.parametersJsonSchema, messageBus, true, // isOutputMarkdown false, // canUpdateOutput ); } override getSchema(modelId?: string) { return resolveToolDeclaration(WRITE_TODOS_DEFINITION, modelId); } protected override validateToolParamValues( params: WriteTodosToolParams, ): string | null { const todos = params?.todos; if (!params || !Array.isArray(todos)) { return '`todos` parameter must be an array'; } for (const todo of todos) { if (typeof todo !== 'object' || todo === null) { return 'Each todo item must be an object'; } if (typeof todo.description !== 'string' || !todo.description.trim()) { return 'Each todo must have a non-empty description string'; } if (!TODO_STATUSES.includes(todo.status)) { return `Each todo must have a valid status (${TODO_STATUSES.join(', ')})`; } } const inProgressCount = todos.filter( (todo: Todo) => todo.status === 'in_progress', ).length; if (inProgressCount > 1) { return 'Invalid parameters: Only one task can be "in_progress" at a time.'; } return null; } protected createInvocation( params: WriteTodosToolParams, messageBus: MessageBus, _toolName?: string, _displayName?: string, ): ToolInvocation { return new WriteTodosToolInvocation( params, messageBus, _toolName, _displayName, ); } }