Skip to content
Merged
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 src/vs/workbench/contrib/chat/browser/chat.contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,12 @@ configurationRegistry.registerConfiguration({
mode: 'startup'
}
},
'chat.todoListTool.writeOnly': {
type: 'boolean',
default: false,
description: nls.localize('chat.todoListTool.writeOnly', "When enabled, the todo tool operates in write-only mode, requiring the agent to remember todos in context."),
tags: ['experimental']
},
[ChatConfiguration.ShowThinking]: {
type: 'boolean',
default: false,
Expand Down
183 changes: 122 additions & 61 deletions src/vs/workbench/contrib/chat/common/tools/manageTodoListTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,62 +23,76 @@ import { IChatTodo, IChatTodoListService } from '../chatTodoListService.js';
import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';

export const TodoListToolSettingId = 'chat.todoListTool.enabled';
export const TodoListToolWriteOnlySettingId = 'chat.todoListTool.writeOnly';

export const ManageTodoListToolToolId = 'manage_todo_list';

export const ManageTodoListToolData: IToolData = {
id: ManageTodoListToolToolId,
toolReferenceName: 'todos',
when: ContextKeyExpr.equals(`config.${TodoListToolSettingId}`, true),
canBeReferencedInPrompt: true,
icon: ThemeIcon.fromId(Codicon.checklist.id),
displayName: 'Update Todo List',
userDescription: 'Manage and track todo items for task planning',
modelDescription: 'Manage a structured todo list to track progress and plan tasks throughout your coding session. Use this tool VERY frequently to ensure task visibility and proper planning.\n\nWhen to use this tool:\n- Complex multi-step work requiring planning and tracking\n- When user provides multiple tasks or requests (numbered/comma-separated)\n- After receiving new instructions that require multiple steps\n- BEFORE starting work on any todo (mark as in-progress)\n- IMMEDIATELY after completing each todo (mark completed individually)\n- When breaking down larger tasks into smaller actionable steps\n- To give users visibility into your progress and planning\n\nWhen NOT to use:\n- Single, trivial tasks that can be completed in one step\n- Purely conversational/informational requests\n- When just reading files or performing simple searches\n\nCRITICAL workflow:\n1. Plan tasks by writing todo list with specific, actionable items\n2. Mark ONE todo as in-progress before starting work\n3. Complete the work for that specific todo\n4. Mark that todo as completed IMMEDIATELY\n5. Move to next todo and repeat\n\nTodo states:\n- not-started: Todo not yet begun\n- in-progress: Currently working (limit ONE at a time)\n- completed: Finished successfully\n\nIMPORTANT: Mark todos completed as soon as they are done. Do not batch completions.',
source: ToolDataSource.Internal,
inputSchema: {
type: 'object',
properties: {
operation: {
type: 'string',
enum: ['write', 'read'],
description: 'write: Replace entire todo list with new content. read: Retrieve current todo list. ALWAYS provide complete list when writing - partial updates not supported.'
},
todoList: {
type: 'array',
description: 'Complete array of all todo items (required for write operation, ignored for read). Must include ALL items - both existing and new.',
items: {
type: 'object',
properties: {
id: {
type: 'number',
description: 'Unique identifier for the todo. Use sequential numbers starting from 1.'
},
title: {
type: 'string',
description: 'Concise action-oriented todo label (3-7 words). Displayed in UI.'
},
description: {
type: 'string',
description: 'Detailed context, requirements, or implementation notes. Include file paths, specific methods, or acceptance criteria.'
},
status: {
type: 'string',
enum: ['not-started', 'in-progress', 'completed'],
description: 'not-started: Not begun | in-progress: Currently working (max 1) | completed: Fully finished with no blockers'
},
export function createManageTodoListToolData(writeOnly: boolean): IToolData {
const baseProperties: any = {
todoList: {
type: 'array',
description: writeOnly
? 'Complete array of all todo items. Must include ALL items - both existing and new.'
: 'Complete array of all todo items (required for write operation, ignored for read). Must include ALL items - both existing and new.',
items: {
type: 'object',
properties: {
id: {
type: 'number',
description: 'Unique identifier for the todo. Use sequential numbers starting from 1.'
},
required: ['id', 'title', 'description', 'status']
}
title: {
type: 'string',
description: 'Concise action-oriented todo label (3-7 words). Displayed in UI.'
},
description: {
type: 'string',
description: 'Detailed context, requirements, or implementation notes. Include file paths, specific methods, or acceptance criteria.'
},
status: {
type: 'string',
enum: ['not-started', 'in-progress', 'completed'],
description: 'not-started: Not begun | in-progress: Currently working (max 1) | completed: Fully finished with no blockers'
},
},
required: ['id', 'title', 'description', 'status']
}
},
required: ['operation']
}
};

const requiredFields = ['todoList'];

if (!writeOnly) {
baseProperties.operation = {
type: 'string',
enum: ['write', 'read'],
description: 'write: Replace entire todo list with new content. read: Retrieve current todo list. ALWAYS provide complete list when writing - partial updates not supported.'
};
requiredFields.unshift('operation');
}
};

interface IManageTodoListToolInputParams {
return {
id: ManageTodoListToolToolId,
toolReferenceName: 'todos',
when: ContextKeyExpr.equals(`config.${TodoListToolSettingId}`, true),
canBeReferencedInPrompt: true,
icon: ThemeIcon.fromId(Codicon.checklist.id),
displayName: 'Update Todo List',
userDescription: 'Manage and track todo items for task planning',
modelDescription: 'Manage a structured todo list to track progress and plan tasks throughout your coding session. Use this tool VERY frequently to ensure task visibility and proper planning.\n\nWhen to use this tool:\n- Complex multi-step work requiring planning and tracking\n- When user provides multiple tasks or requests (numbered/comma-separated)\n- After receiving new instructions that require multiple steps\n- BEFORE starting work on any todo (mark as in-progress)\n- IMMEDIATELY after completing each todo (mark completed individually)\n- When breaking down larger tasks into smaller actionable steps\n- To give users visibility into your progress and planning\n\nWhen NOT to use:\n- Single, trivial tasks that can be completed in one step\n- Purely conversational/informational requests\n- When just reading files or performing simple searches\n\nCRITICAL workflow:\n1. Plan tasks by writing todo list with specific, actionable items\n2. Mark ONE todo as in-progress before starting work\n3. Complete the work for that specific todo\n4. Mark that todo as completed IMMEDIATELY\n5. Move to next todo and repeat\n\nTodo states:\n- not-started: Todo not yet begun\n- in-progress: Currently working (limit ONE at a time)\n- completed: Finished successfully\n\nIMPORTANT: Mark todos completed as soon as they are done. Do not batch completions.',
source: ToolDataSource.Internal,
inputSchema: {
type: 'object',
properties: baseProperties,
required: requiredFields
}
};
}

export const ManageTodoListToolData: IToolData = createManageTodoListToolData(false);

operation: 'write' | 'read';
interface IManageTodoListToolInputParams {
operation?: 'write' | 'read'; // Optional in write-only mode
todoList: Array<{
id: number;
title: string;
Expand All @@ -91,6 +105,7 @@ interface IManageTodoListToolInputParams {
export class ManageTodoListTool extends Disposable implements IToolImpl {

constructor(
private readonly writeOnly: boolean,
@IChatTodoListService private readonly chatTodoListService: IChatTodoListService,
@ILogService private readonly logService: ILogService,
@ITelemetryService private readonly telemetryService: ITelemetryService
Expand All @@ -108,7 +123,45 @@ export class ManageTodoListTool extends Disposable implements IToolImpl {
this.logService.debug(`ManageTodoListTool: Invoking with options ${JSON.stringify(args)}`);

try {
switch (args.operation) {

// In write-only mode, we always perform a write operation
if (this.writeOnly && !args.chatSessionId) {
if (!args.todoList) {
return {
content: [{
kind: 'text',
value: 'Error: todoList is required for write operation'
}]
};
}

const todoList: IChatTodo[] = args.todoList.map((parsedTodo) => ({
id: parsedTodo.id,
title: parsedTodo.title,
description: parsedTodo.description,
status: parsedTodo.status
}));
this.chatTodoListService.setTodos(chatSessionId, todoList);
return {
content: [{
kind: 'text',
value: 'Successfully wrote todo list'
}]
};
}

// Regular mode: check operation parameter
const operation = args.operation;
if (operation === undefined) {
return {
content: [{
kind: 'text',
value: 'Error: operation parameter is required'
}]
};
}

switch (operation) {
case 'read': {
const todoItems = this.chatTodoListService.getTodos(chatSessionId);
const readResult = this.handleRead(todoItems, chatSessionId);
Expand Down Expand Up @@ -182,19 +235,27 @@ export class ManageTodoListTool extends Disposable implements IToolImpl {
const currentTodoItems = this.chatTodoListService.getTodos(chatSessionId);
let message: string | undefined;

switch (args.operation) {
case 'write': {
if (args.todoList) {
message = this.generatePastTenseMessage(currentTodoItems, args.todoList);
}
break;
// In write-only mode, we always treat it as a write operation
if (this.writeOnly && !args.chatSessionId) {
if (args.todoList) {
message = this.generatePastTenseMessage(currentTodoItems, args.todoList);
}
case 'read': {
message = 'Read todo list';
break;
} else {
// Regular mode: check operation
switch (args.operation) {
case 'write': {
if (args.todoList) {
message = this.generatePastTenseMessage(currentTodoItems, args.todoList);
}
break;
}
case 'read': {
message = 'Read todo list';
break;
}
default:
break;
}
default:
break;
}

const items = args.todoList ?? currentTodoItems;
Expand Down Expand Up @@ -266,7 +327,7 @@ export class ManageTodoListTool extends Disposable implements IToolImpl {
}

const markdownTaskList = this.formatTodoListAsMarkdownTaskList(todoItems);
return `# Task List\n\n${markdownTaskList}`;
return `# Todo List\n\n${markdownTaskList}`;
}

private formatTodoListAsMarkdownTaskList(todoList: IChatTodo[]): string {
Expand Down
14 changes: 9 additions & 5 deletions src/vs/workbench/contrib/chat/common/tools/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
*--------------------------------------------------------------------------------------------*/

import { Disposable } from '../../../../../base/common/lifecycle.js';
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
import { IWorkbenchContribution } from '../../../../common/contributions.js';
import { ILanguageModelToolsService } from '../../common/languageModelToolsService.js';
import { EditTool, EditToolData } from './editFileTool.js';
import { ManageTodoListTool, ManageTodoListToolData } from './manageTodoListTool.js';
import { ManageTodoListTool, createManageTodoListToolData, TodoListToolWriteOnlySettingId } from './manageTodoListTool.js';

export class BuiltinToolsContribution extends Disposable implements IWorkbenchContribution {

Expand All @@ -17,17 +18,20 @@ export class BuiltinToolsContribution extends Disposable implements IWorkbenchCo
constructor(
@ILanguageModelToolsService toolsService: ILanguageModelToolsService,
@IInstantiationService instantiationService: IInstantiationService,
@IConfigurationService private readonly configurationService: IConfigurationService,
) {
super();

const editTool = instantiationService.createInstance(EditTool);
this._register(toolsService.registerToolData(EditToolData));
this._register(toolsService.registerToolImplementation(EditToolData.id, editTool));

const manageTodoListTool = instantiationService.createInstance(ManageTodoListTool);
this._register(manageTodoListTool);
this._register(toolsService.registerToolData(ManageTodoListToolData));
this._register(toolsService.registerToolImplementation(ManageTodoListToolData.id, manageTodoListTool));
// Check if write-only mode is enabled for the todo tool
const writeOnlyMode = this.configurationService.getValue<boolean>(TodoListToolWriteOnlySettingId) === true;
const todoToolData = createManageTodoListToolData(writeOnlyMode);
const manageTodoListTool = instantiationService.createInstance(ManageTodoListTool, writeOnlyMode);
this._register(toolsService.registerToolData(todoToolData));
this._register(toolsService.registerToolImplementation(todoToolData.id, manageTodoListTool));
}
}

Expand Down
Loading