feat(core): Add tracker CRUD tools & visualization (#19489)

Co-authored-by: Jerop Kipruto <jerop@google.com>
This commit is contained in:
anj-s
2026-03-03 16:42:48 -08:00
committed by GitHub
parent af424aefa9
commit a63c76522a
12 changed files with 1118 additions and 27 deletions
+49
View File
@@ -70,6 +70,14 @@ import {
StandardFileSystemService,
type FileSystemService,
} from '../services/fileSystemService.js';
import {
TrackerCreateTaskTool,
TrackerUpdateTaskTool,
TrackerGetTaskTool,
TrackerListTasksTool,
TrackerAddDependencyTool,
TrackerVisualizeTool,
} from '../tools/trackerTools.js';
import {
logRipgrepFallback,
logFlashFallback,
@@ -96,6 +104,7 @@ import {
} from '../services/modelConfigService.js';
import { DEFAULT_MODEL_CONFIGS } from './defaultModelConfigs.js';
import { ContextManager } from '../services/contextManager.js';
import { TrackerService } from '../services/trackerService.js';
import type { GenerateContentParameters } from '@google/genai';
// Re-export OAuth config type
@@ -572,6 +581,7 @@ export interface ConfigParameters {
toolOutputMasking?: Partial<ToolOutputMaskingConfig>;
disableLLMCorrection?: boolean;
plan?: boolean;
tracker?: boolean;
planSettings?: PlanSettings;
modelSteering?: boolean;
onModelChange?: (model: string) => void;
@@ -605,6 +615,7 @@ export class Config implements McpContext {
private sessionId: string;
private clientVersion: string;
private fileSystemService: FileSystemService;
private trackerService?: TrackerService;
private contentGeneratorConfig!: ContentGeneratorConfig;
private contentGenerator!: ContentGenerator;
readonly modelConfigService: ModelConfigService;
@@ -783,6 +794,7 @@ export class Config implements McpContext {
private readonly experimentalJitContext: boolean;
private readonly disableLLMCorrection: boolean;
private readonly planEnabled: boolean;
private readonly trackerEnabled: boolean;
private readonly planModeRoutingEnabled: boolean;
private readonly modelSteering: boolean;
private contextManager?: ContextManager;
@@ -873,6 +885,7 @@ export class Config implements McpContext {
this.agents = params.agents ?? {};
this.disableLLMCorrection = params.disableLLMCorrection ?? true;
this.planEnabled = params.plan ?? false;
this.trackerEnabled = params.tracker ?? false;
this.planModeRoutingEnabled = params.planSettings?.modelRouting ?? true;
this.enableEventDrivenScheduler = params.enableEventDrivenScheduler ?? true;
this.skillsSupport = params.skillsSupport ?? true;
@@ -2193,6 +2206,15 @@ export class Config implements McpContext {
return this.bugCommand;
}
getTrackerService(): TrackerService {
if (!this.trackerService) {
this.trackerService = new TrackerService(
this.storage.getProjectTempTrackerDir(),
);
}
return this.trackerService;
}
getFileService(): FileDiscoveryService {
if (!this.fileDiscoveryService) {
this.fileDiscoveryService = new FileDiscoveryService(this.targetDir, {
@@ -2260,6 +2282,10 @@ export class Config implements McpContext {
return this.planEnabled;
}
isTrackerEnabled(): boolean {
return this.trackerEnabled;
}
getApprovedPlanPath(): string | undefined {
return this.approvedPlanPath;
}
@@ -2825,6 +2851,29 @@ export class Config implements McpContext {
);
}
if (this.isTrackerEnabled()) {
maybeRegister(TrackerCreateTaskTool, () =>
registry.registerTool(new TrackerCreateTaskTool(this, this.messageBus)),
);
maybeRegister(TrackerUpdateTaskTool, () =>
registry.registerTool(new TrackerUpdateTaskTool(this, this.messageBus)),
);
maybeRegister(TrackerGetTaskTool, () =>
registry.registerTool(new TrackerGetTaskTool(this, this.messageBus)),
);
maybeRegister(TrackerListTasksTool, () =>
registry.registerTool(new TrackerListTasksTool(this, this.messageBus)),
);
maybeRegister(TrackerAddDependencyTool, () =>
registry.registerTool(
new TrackerAddDependencyTool(this, this.messageBus),
),
);
maybeRegister(TrackerVisualizeTool, () =>
registry.registerTool(new TrackerVisualizeTool(this, this.messageBus)),
);
}
// Register Subagents as Tools
this.registerSubAgentTools(registry);
@@ -0,0 +1,47 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect } from 'vitest';
import { Config } from './config.js';
import { TRACKER_CREATE_TASK_TOOL_NAME } from '../tools/tool-names.js';
import * as os from 'node:os';
describe('Config Tracker Feature Flag', () => {
const baseParams = {
sessionId: 'test-session',
targetDir: os.tmpdir(),
cwd: os.tmpdir(),
model: 'gemini-1.5-pro',
debugMode: false,
};
it('should not register tracker tools by default', async () => {
const config = new Config(baseParams);
await config.initialize();
const registry = config.getToolRegistry();
expect(registry.getTool(TRACKER_CREATE_TASK_TOOL_NAME)).toBeUndefined();
});
it('should register tracker tools when tracker is enabled', async () => {
const config = new Config({
...baseParams,
tracker: true,
});
await config.initialize();
const registry = config.getToolRegistry();
expect(registry.getTool(TRACKER_CREATE_TASK_TOOL_NAME)).toBeDefined();
});
it('should not register tracker tools when tracker is explicitly disabled', async () => {
const config = new Config({
...baseParams,
tracker: false,
});
await config.initialize();
const registry = config.getToolRegistry();
expect(registry.getTool(TRACKER_CREATE_TASK_TOOL_NAME)).toBeUndefined();
});
});
+3
View File
@@ -121,6 +121,8 @@ export * from './services/chatRecordingService.js';
export * from './services/fileSystemService.js';
export * from './services/sessionSummaryUtils.js';
export * from './services/contextManager.js';
export * from './services/trackerService.js';
export * from './services/trackerTypes.js';
export * from './skills/skillManager.js';
export * from './skills/skillLoader.js';
@@ -167,6 +169,7 @@ export * from './tools/read-many-files.js';
export * from './tools/mcp-client.js';
export * from './tools/mcp-tool.js';
export * from './tools/write-todos.js';
export * from './tools/trackerTools.js';
export * from './tools/activate-skill.js';
export * from './tools/ask-user.js';
+57 -27
View File
@@ -50,6 +50,15 @@ export class TrackerService {
id,
};
if (task.parentId) {
const parentList = await this.listTasks();
if (!parentList.find((t) => t.id === task.parentId)) {
throw new Error(`Parent task with ID ${task.parentId} not found.`);
}
}
TrackerTaskSchema.parse(task);
await this.saveTask(task);
return task;
}
@@ -70,7 +79,8 @@ export class TrackerService {
error &&
typeof error === 'object' &&
'code' in error &&
error.code === 'ENOENT'
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
(error as NodeJS.ErrnoException).code === 'ENOENT'
) {
return null;
}
@@ -130,26 +140,48 @@ export class TrackerService {
id: string,
updates: Partial<TrackerTask>,
): Promise<TrackerTask> {
const task = await this.getTask(id);
const isClosing = updates.status === TaskStatus.CLOSED;
const changingDependencies = updates.dependencies !== undefined;
let taskMap: Map<string, TrackerTask> | undefined;
if (isClosing || changingDependencies) {
const allTasks = await this.listTasks();
taskMap = new Map<string, TrackerTask>(allTasks.map((t) => [t.id, t]));
}
const task = taskMap ? taskMap.get(id) : await this.getTask(id);
if (!task) {
throw new Error(`Task with ID ${id} not found.`);
}
const updatedTask = { ...task, ...updates };
const updatedTask = { ...task, ...updates, id: task.id };
// Validate status transition if closing
if (
updatedTask.status === TaskStatus.CLOSED &&
task.status !== TaskStatus.CLOSED
) {
await this.validateCanClose(updatedTask);
if (updatedTask.parentId) {
const parentExists = taskMap
? taskMap.has(updatedTask.parentId)
: !!(await this.getTask(updatedTask.parentId));
if (!parentExists) {
throw new Error(
`Parent task with ID ${updatedTask.parentId} not found.`,
);
}
}
// Validate circular dependencies if dependencies changed
if (updates.dependencies) {
await this.validateNoCircularDependencies(updatedTask);
if (taskMap) {
if (isClosing && task.status !== TaskStatus.CLOSED) {
this.validateCanClose(updatedTask, taskMap);
}
if (changingDependencies) {
taskMap.set(updatedTask.id, updatedTask);
this.validateNoCircularDependencies(updatedTask, taskMap);
}
}
TrackerTaskSchema.parse(updatedTask);
await this.saveTask(updatedTask);
return updatedTask;
}
@@ -165,9 +197,12 @@ export class TrackerService {
/**
* Validates that a task can be closed (all dependencies must be closed).
*/
private async validateCanClose(task: TrackerTask): Promise<void> {
private validateCanClose(
task: TrackerTask,
taskMap: Map<string, TrackerTask>,
): void {
for (const depId of task.dependencies) {
const dep = await this.getTask(depId);
const dep = taskMap.get(depId);
if (!dep) {
throw new Error(`Dependency ${depId} not found for task ${task.id}.`);
}
@@ -182,16 +217,10 @@ export class TrackerService {
/**
* Validates that there are no circular dependencies.
*/
private async validateNoCircularDependencies(
private validateNoCircularDependencies(
task: TrackerTask,
): Promise<void> {
const allTasks = await this.listTasks();
const taskMap = new Map<string, TrackerTask>(
allTasks.map((t) => [t.id, t]),
);
// Ensure the current (possibly unsaved) task state is used
taskMap.set(task.id, task);
taskMap: Map<string, TrackerTask>,
): void {
const visited = new Set<string>();
const stack = new Set<string>();
@@ -209,10 +238,11 @@ export class TrackerService {
stack.add(currentId);
const currentTask = taskMap.get(currentId);
if (currentTask) {
for (const depId of currentTask.dependencies) {
check(depId);
}
if (!currentTask) {
throw new Error(`Dependency ${currentId} not found.`);
}
for (const depId of currentTask.dependencies) {
check(depId);
}
stack.delete(currentId);
@@ -0,0 +1,161 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type { ToolDefinition } from './types.js';
import {
TRACKER_CREATE_TASK_TOOL_NAME,
TRACKER_UPDATE_TASK_TOOL_NAME,
TRACKER_GET_TASK_TOOL_NAME,
TRACKER_LIST_TASKS_TOOL_NAME,
TRACKER_ADD_DEPENDENCY_TOOL_NAME,
TRACKER_VISUALIZE_TOOL_NAME,
} from '../tool-names.js';
export const TRACKER_CREATE_TASK_DEFINITION: ToolDefinition = {
base: {
name: TRACKER_CREATE_TASK_TOOL_NAME,
description: 'Creates a new task in the tracker.',
parametersJsonSchema: {
type: 'object',
properties: {
title: {
type: 'string',
description: 'Short title of the task.',
},
description: {
type: 'string',
description: 'Detailed description of the task.',
},
type: {
type: 'string',
enum: ['epic', 'task', 'bug'],
description: 'Type of the task.',
},
parentId: {
type: 'string',
description: 'Optional ID of the parent task.',
},
dependencies: {
type: 'array',
items: { type: 'string' },
description: 'Optional list of task IDs that this task depends on.',
},
},
required: ['title', 'description', 'type'],
},
},
};
export const TRACKER_UPDATE_TASK_DEFINITION: ToolDefinition = {
base: {
name: TRACKER_UPDATE_TASK_TOOL_NAME,
description: 'Updates an existing task in the tracker.',
parametersJsonSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'The 6-character hex ID of the task to update.',
},
title: {
type: 'string',
description: 'New title for the task.',
},
description: {
type: 'string',
description: 'New description for the task.',
},
status: {
type: 'string',
enum: ['open', 'in_progress', 'blocked', 'closed'],
description: 'New status for the task.',
},
dependencies: {
type: 'array',
items: { type: 'string' },
description: 'New list of dependency IDs.',
},
},
required: ['id'],
},
},
};
export const TRACKER_GET_TASK_DEFINITION: ToolDefinition = {
base: {
name: TRACKER_GET_TASK_TOOL_NAME,
description: 'Retrieves details for a specific task.',
parametersJsonSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'The 6-character hex ID of the task.',
},
},
required: ['id'],
},
},
};
export const TRACKER_LIST_TASKS_DEFINITION: ToolDefinition = {
base: {
name: TRACKER_LIST_TASKS_TOOL_NAME,
description:
'Lists tasks in the tracker, optionally filtered by status, type, or parent.',
parametersJsonSchema: {
type: 'object',
properties: {
status: {
type: 'string',
enum: ['open', 'in_progress', 'blocked', 'closed'],
description: 'Filter by status.',
},
type: {
type: 'string',
enum: ['epic', 'task', 'bug'],
description: 'Filter by type.',
},
parentId: {
type: 'string',
description: 'Filter by parent task ID.',
},
},
},
},
};
export const TRACKER_ADD_DEPENDENCY_DEFINITION: ToolDefinition = {
base: {
name: TRACKER_ADD_DEPENDENCY_TOOL_NAME,
description: 'Adds a dependency between two tasks.',
parametersJsonSchema: {
type: 'object',
properties: {
taskId: {
type: 'string',
description: 'The ID of the task that has a dependency.',
},
dependencyId: {
type: 'string',
description: 'The ID of the task that is being depended upon.',
},
},
required: ['taskId', 'dependencyId'],
},
},
};
export const TRACKER_VISUALIZE_DEFINITION: ToolDefinition = {
base: {
name: TRACKER_VISUALIZE_TOOL_NAME,
description: 'Renders an ASCII tree visualization of the task graph.',
parametersJsonSchema: {
type: 'object',
properties: {},
},
},
};
+28
View File
@@ -154,6 +154,13 @@ export const LS_TOOL_NAME_LEGACY = 'list_directory'; // Just to be safe if anyth
export const EDIT_TOOL_NAMES = new Set([EDIT_TOOL_NAME, WRITE_FILE_TOOL_NAME]);
export const TRACKER_CREATE_TASK_TOOL_NAME = 'tracker_create_task';
export const TRACKER_UPDATE_TASK_TOOL_NAME = 'tracker_update_task';
export const TRACKER_GET_TASK_TOOL_NAME = 'tracker_get_task';
export const TRACKER_LIST_TASKS_TOOL_NAME = 'tracker_list_tasks';
export const TRACKER_ADD_DEPENDENCY_TOOL_NAME = 'tracker_add_dependency';
export const TRACKER_VISUALIZE_TOOL_NAME = 'tracker_visualize';
// Tool Display Names
export const WRITE_FILE_DISPLAY_NAME = 'WriteFile';
export const EDIT_DISPLAY_NAME = 'Edit';
@@ -213,11 +220,32 @@ export const ALL_BUILTIN_TOOL_NAMES = [
MEMORY_TOOL_NAME,
ACTIVATE_SKILL_TOOL_NAME,
ASK_USER_TOOL_NAME,
TRACKER_CREATE_TASK_TOOL_NAME,
TRACKER_UPDATE_TASK_TOOL_NAME,
TRACKER_GET_TASK_TOOL_NAME,
TRACKER_LIST_TASKS_TOOL_NAME,
TRACKER_ADD_DEPENDENCY_TOOL_NAME,
TRACKER_VISUALIZE_TOOL_NAME,
GET_INTERNAL_DOCS_TOOL_NAME,
ENTER_PLAN_MODE_TOOL_NAME,
EXIT_PLAN_MODE_TOOL_NAME,
] as const;
/**
* Read-only tools available in Plan Mode.
* This list is used to dynamically generate the Plan Mode prompt,
* filtered by what tools are actually enabled in the current configuration.
*/
export const PLAN_MODE_TOOLS = [
GLOB_TOOL_NAME,
GREP_TOOL_NAME,
READ_FILE_TOOL_NAME,
LS_TOOL_NAME,
WEB_SEARCH_TOOL_NAME,
ASK_USER_TOOL_NAME,
ACTIVATE_SKILL_TOOL_NAME,
] as const;
/**
* Validates if a tool name is syntactically valid.
* Checks against built-in tools, discovered tools, and MCP naming conventions.
@@ -0,0 +1,145 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { Config } from '../config/config.js';
import { MessageBus } from '../confirmation-bus/message-bus.js';
import type { PolicyEngine } from '../policy/policy-engine.js';
import {
TrackerCreateTaskTool,
TrackerListTasksTool,
TrackerUpdateTaskTool,
TrackerVisualizeTool,
TrackerAddDependencyTool,
} from './trackerTools.js';
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import * as os from 'node:os';
import { TaskStatus, TaskType } from '../services/trackerTypes.js';
describe('Tracker Tools Integration', () => {
let tempDir: string;
let config: Config;
let messageBus: MessageBus;
beforeEach(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'tracker-tools-test-'));
config = new Config({
sessionId: 'test-session',
targetDir: tempDir,
cwd: tempDir,
model: 'gemini-3-flash',
debugMode: false,
});
messageBus = new MessageBus(null as unknown as PolicyEngine, false);
});
afterEach(async () => {
await fs.rm(tempDir, { recursive: true, force: true });
});
const getSignal = () => new AbortController().signal;
it('creates and lists tasks', async () => {
const createTool = new TrackerCreateTaskTool(config, messageBus);
const createResult = await createTool.buildAndExecute(
{
title: 'Test Task',
description: 'Test Description',
type: TaskType.TASK,
},
getSignal(),
);
expect(createResult.llmContent).toContain('Created task');
const listTool = new TrackerListTasksTool(config, messageBus);
const listResult = await listTool.buildAndExecute({}, getSignal());
expect(listResult.llmContent).toContain('Test Task');
expect(listResult.llmContent).toContain(`(${TaskStatus.OPEN})`);
});
it('updates task status', async () => {
const createTool = new TrackerCreateTaskTool(config, messageBus);
await createTool.buildAndExecute(
{
title: 'Update Me',
description: '...',
type: TaskType.TASK,
},
getSignal(),
);
const tasks = await config.getTrackerService().listTasks();
const taskId = tasks[0].id;
const updateTool = new TrackerUpdateTaskTool(config, messageBus);
const updateResult = await updateTool.buildAndExecute(
{
id: taskId,
status: TaskStatus.IN_PROGRESS,
},
getSignal(),
);
expect(updateResult.llmContent).toContain(
`Status: ${TaskStatus.IN_PROGRESS}`,
);
const task = await config.getTrackerService().getTask(taskId);
expect(task?.status).toBe(TaskStatus.IN_PROGRESS);
});
it('adds dependencies and visualizes the graph', async () => {
const createTool = new TrackerCreateTaskTool(config, messageBus);
// Create Parent
await createTool.buildAndExecute(
{
title: 'Parent Task',
description: '...',
type: TaskType.TASK,
},
getSignal(),
);
// Create Child
await createTool.buildAndExecute(
{
title: 'Child Task',
description: '...',
type: TaskType.TASK,
},
getSignal(),
);
const tasks = await config.getTrackerService().listTasks();
const parentId = tasks.find((t) => t.title === 'Parent Task')!.id;
const childId = tasks.find((t) => t.title === 'Child Task')!.id;
// Add Dependency
const addDepTool = new TrackerAddDependencyTool(config, messageBus);
await addDepTool.buildAndExecute(
{
taskId: parentId,
dependencyId: childId,
},
getSignal(),
);
const updatedParent = await config.getTrackerService().getTask(parentId);
expect(updatedParent?.dependencies).toContain(childId);
// Visualize
const vizTool = new TrackerVisualizeTool(config, messageBus);
const vizResult = await vizTool.buildAndExecute({}, getSignal());
expect(vizResult.llmContent).toContain('Parent Task');
expect(vizResult.llmContent).toContain('Child Task');
expect(vizResult.llmContent).toContain(childId);
});
});
+606
View File
@@ -0,0 +1,606 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type { Config } from '../config/config.js';
import type { MessageBus } from '../confirmation-bus/message-bus.js';
import {
TRACKER_ADD_DEPENDENCY_DEFINITION,
TRACKER_CREATE_TASK_DEFINITION,
TRACKER_GET_TASK_DEFINITION,
TRACKER_LIST_TASKS_DEFINITION,
TRACKER_UPDATE_TASK_DEFINITION,
TRACKER_VISUALIZE_DEFINITION,
} from './definitions/trackerTools.js';
import { resolveToolDeclaration } from './definitions/resolver.js';
import {
TRACKER_ADD_DEPENDENCY_TOOL_NAME,
TRACKER_CREATE_TASK_TOOL_NAME,
TRACKER_GET_TASK_TOOL_NAME,
TRACKER_LIST_TASKS_TOOL_NAME,
TRACKER_UPDATE_TASK_TOOL_NAME,
TRACKER_VISUALIZE_TOOL_NAME,
} from './tool-names.js';
import type { ToolResult } from './tools.js';
import { BaseDeclarativeTool, BaseToolInvocation, Kind } from './tools.js';
import { ToolErrorType } from './tool-error.js';
import type { TrackerTask, TaskType } from '../services/trackerTypes.js';
import { TaskStatus } from '../services/trackerTypes.js';
// --- tracker_create_task ---
interface CreateTaskParams {
title: string;
description: string;
type: TaskType;
parentId?: string;
dependencies?: string[];
}
class TrackerCreateTaskInvocation extends BaseToolInvocation<
CreateTaskParams,
ToolResult
> {
constructor(
private readonly config: Config,
params: CreateTaskParams,
messageBus: MessageBus,
toolName: string,
) {
super(params, messageBus, toolName);
}
private get service() {
return this.config.getTrackerService();
}
getDescription(): string {
return `Creating task: ${this.params.title}`;
}
override async execute(_signal: AbortSignal): Promise<ToolResult> {
try {
const task = await this.service.createTask({
title: this.params.title,
description: this.params.description,
type: this.params.type,
status: TaskStatus.OPEN,
parentId: this.params.parentId,
dependencies: this.params.dependencies ?? [],
});
return {
llmContent: `Created task ${task.id}: ${task.title}`,
returnDisplay: `Created task ${task.id}.`,
};
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
return {
llmContent: `Error creating task: ${errorMessage}`,
returnDisplay: 'Failed to create task.',
error: {
message: errorMessage,
type: ToolErrorType.EXECUTION_FAILED,
},
};
}
}
}
export class TrackerCreateTaskTool extends BaseDeclarativeTool<
CreateTaskParams,
ToolResult
> {
static readonly Name = TRACKER_CREATE_TASK_TOOL_NAME;
constructor(
private config: Config,
messageBus: MessageBus,
) {
super(
TrackerCreateTaskTool.Name,
'Create Task',
TRACKER_CREATE_TASK_DEFINITION.base.description!,
Kind.Edit,
TRACKER_CREATE_TASK_DEFINITION.base.parametersJsonSchema,
messageBus,
);
}
protected createInvocation(params: CreateTaskParams, messageBus: MessageBus) {
return new TrackerCreateTaskInvocation(
this.config,
params,
messageBus,
this.name,
);
}
override getSchema(modelId?: string) {
return resolveToolDeclaration(TRACKER_CREATE_TASK_DEFINITION, modelId);
}
}
// --- tracker_update_task ---
interface UpdateTaskParams {
id: string;
title?: string;
description?: string;
status?: TaskStatus;
dependencies?: string[];
}
class TrackerUpdateTaskInvocation extends BaseToolInvocation<
UpdateTaskParams,
ToolResult
> {
constructor(
private readonly config: Config,
params: UpdateTaskParams,
messageBus: MessageBus,
toolName: string,
) {
super(params, messageBus, toolName);
}
private get service() {
return this.config.getTrackerService();
}
getDescription(): string {
return `Updating task ${this.params.id}`;
}
override async execute(_signal: AbortSignal): Promise<ToolResult> {
const { id, ...updates } = this.params;
try {
const task = await this.service.updateTask(id, updates);
return {
llmContent: `Updated task ${task.id}. Status: ${task.status}`,
returnDisplay: `Updated task ${task.id}.`,
};
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
return {
llmContent: `Error updating task: ${errorMessage}`,
returnDisplay: 'Failed to update task.',
error: {
message: errorMessage,
type: ToolErrorType.EXECUTION_FAILED,
},
};
}
}
}
export class TrackerUpdateTaskTool extends BaseDeclarativeTool<
UpdateTaskParams,
ToolResult
> {
static readonly Name = TRACKER_UPDATE_TASK_TOOL_NAME;
constructor(
private config: Config,
messageBus: MessageBus,
) {
super(
TrackerUpdateTaskTool.Name,
'Update Task',
TRACKER_UPDATE_TASK_DEFINITION.base.description!,
Kind.Edit,
TRACKER_UPDATE_TASK_DEFINITION.base.parametersJsonSchema,
messageBus,
);
}
protected createInvocation(params: UpdateTaskParams, messageBus: MessageBus) {
return new TrackerUpdateTaskInvocation(
this.config,
params,
messageBus,
this.name,
);
}
override getSchema(modelId?: string) {
return resolveToolDeclaration(TRACKER_UPDATE_TASK_DEFINITION, modelId);
}
}
// --- tracker_get_task ---
interface GetTaskParams {
id: string;
}
class TrackerGetTaskInvocation extends BaseToolInvocation<
GetTaskParams,
ToolResult
> {
constructor(
private readonly config: Config,
params: GetTaskParams,
messageBus: MessageBus,
toolName: string,
) {
super(params, messageBus, toolName);
}
private get service() {
return this.config.getTrackerService();
}
getDescription(): string {
return `Retrieving task ${this.params.id}`;
}
override async execute(_signal: AbortSignal): Promise<ToolResult> {
const task = await this.service.getTask(this.params.id);
if (!task) {
return {
llmContent: `Task ${this.params.id} not found.`,
returnDisplay: 'Task not found.',
};
}
return {
llmContent: JSON.stringify(task, null, 2),
returnDisplay: `Retrieved task ${task.id}.`,
};
}
}
export class TrackerGetTaskTool extends BaseDeclarativeTool<
GetTaskParams,
ToolResult
> {
static readonly Name = TRACKER_GET_TASK_TOOL_NAME;
constructor(
private config: Config,
messageBus: MessageBus,
) {
super(
TrackerGetTaskTool.Name,
'Get Task',
TRACKER_GET_TASK_DEFINITION.base.description!,
Kind.Read,
TRACKER_GET_TASK_DEFINITION.base.parametersJsonSchema,
messageBus,
);
}
protected createInvocation(params: GetTaskParams, messageBus: MessageBus) {
return new TrackerGetTaskInvocation(
this.config,
params,
messageBus,
this.name,
);
}
override getSchema(modelId?: string) {
return resolveToolDeclaration(TRACKER_GET_TASK_DEFINITION, modelId);
}
}
// --- tracker_list_tasks ---
interface ListTasksParams {
status?: TaskStatus;
type?: TaskType;
parentId?: string;
}
class TrackerListTasksInvocation extends BaseToolInvocation<
ListTasksParams,
ToolResult
> {
constructor(
private readonly config: Config,
params: ListTasksParams,
messageBus: MessageBus,
toolName: string,
) {
super(params, messageBus, toolName);
}
private get service() {
return this.config.getTrackerService();
}
getDescription(): string {
return 'Listing tasks.';
}
override async execute(_signal: AbortSignal): Promise<ToolResult> {
let tasks = await this.service.listTasks();
if (this.params.status) {
tasks = tasks.filter((t) => t.status === this.params.status);
}
if (this.params.type) {
tasks = tasks.filter((t) => t.type === this.params.type);
}
if (this.params.parentId) {
tasks = tasks.filter((t) => t.parentId === this.params.parentId);
}
if (tasks.length === 0) {
return {
llmContent: 'No tasks found matching the criteria.',
returnDisplay: 'No matching tasks.',
};
}
const content = tasks
.map((t) => `- [${t.id}] ${t.title} (${t.status})`)
.join('\n');
return {
llmContent: content,
returnDisplay: `Listed ${tasks.length} tasks.`,
};
}
}
export class TrackerListTasksTool extends BaseDeclarativeTool<
ListTasksParams,
ToolResult
> {
static readonly Name = TRACKER_LIST_TASKS_TOOL_NAME;
constructor(
private config: Config,
messageBus: MessageBus,
) {
super(
TrackerListTasksTool.Name,
'List Tasks',
TRACKER_LIST_TASKS_DEFINITION.base.description!,
Kind.Search,
TRACKER_LIST_TASKS_DEFINITION.base.parametersJsonSchema,
messageBus,
);
}
protected createInvocation(params: ListTasksParams, messageBus: MessageBus) {
return new TrackerListTasksInvocation(
this.config,
params,
messageBus,
this.name,
);
}
override getSchema(modelId?: string) {
return resolveToolDeclaration(TRACKER_LIST_TASKS_DEFINITION, modelId);
}
}
// --- tracker_add_dependency ---
interface AddDependencyParams {
taskId: string;
dependencyId: string;
}
class TrackerAddDependencyInvocation extends BaseToolInvocation<
AddDependencyParams,
ToolResult
> {
constructor(
private readonly config: Config,
params: AddDependencyParams,
messageBus: MessageBus,
toolName: string,
) {
super(params, messageBus, toolName);
}
private get service() {
return this.config.getTrackerService();
}
getDescription(): string {
return `Adding dependency: ${this.params.taskId} depends on ${this.params.dependencyId}`;
}
override async execute(_signal: AbortSignal): Promise<ToolResult> {
if (this.params.taskId === this.params.dependencyId) {
return {
llmContent: `Error: Task ${this.params.taskId} cannot depend on itself.`,
returnDisplay: 'Self-referential dependency rejected.',
error: {
message: 'Task cannot depend on itself',
type: ToolErrorType.EXECUTION_FAILED,
},
};
}
const [task, dep] = await Promise.all([
this.service.getTask(this.params.taskId),
this.service.getTask(this.params.dependencyId),
]);
if (!task) {
return {
llmContent: `Task ${this.params.taskId} not found.`,
returnDisplay: 'Task not found.',
};
}
if (!dep) {
return {
llmContent: `Dependency task ${this.params.dependencyId} not found.`,
returnDisplay: 'Dependency not found.',
};
}
const newDeps = Array.from(
new Set([...task.dependencies, this.params.dependencyId]),
);
try {
await this.service.updateTask(task.id, { dependencies: newDeps });
return {
llmContent: `Linked ${task.id} -> ${dep.id}.`,
returnDisplay: 'Dependency added.',
};
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
return {
llmContent: `Error adding dependency: ${errorMessage}`,
returnDisplay: 'Failed to add dependency.',
error: {
message: errorMessage,
type: ToolErrorType.EXECUTION_FAILED,
},
};
}
}
}
export class TrackerAddDependencyTool extends BaseDeclarativeTool<
AddDependencyParams,
ToolResult
> {
static readonly Name = TRACKER_ADD_DEPENDENCY_TOOL_NAME;
constructor(
private config: Config,
messageBus: MessageBus,
) {
super(
TrackerAddDependencyTool.Name,
'Add Dependency',
TRACKER_ADD_DEPENDENCY_DEFINITION.base.description!,
Kind.Edit,
TRACKER_ADD_DEPENDENCY_DEFINITION.base.parametersJsonSchema,
messageBus,
);
}
protected createInvocation(
params: AddDependencyParams,
messageBus: MessageBus,
) {
return new TrackerAddDependencyInvocation(
this.config,
params,
messageBus,
this.name,
);
}
override getSchema(modelId?: string) {
return resolveToolDeclaration(TRACKER_ADD_DEPENDENCY_DEFINITION, modelId);
}
}
// --- tracker_visualize ---
class TrackerVisualizeInvocation extends BaseToolInvocation<
Record<string, never>,
ToolResult
> {
constructor(
private readonly config: Config,
params: Record<string, never>,
messageBus: MessageBus,
toolName: string,
) {
super(params, messageBus, toolName);
}
private get service() {
return this.config.getTrackerService();
}
getDescription(): string {
return 'Visualizing the task graph.';
}
override async execute(_signal: AbortSignal): Promise<ToolResult> {
const tasks = await this.service.listTasks();
if (tasks.length === 0) {
return {
llmContent: 'No tasks to visualize.',
returnDisplay: 'Empty tracker.',
};
}
const statusEmojis: Record<TaskStatus, string> = {
open: '⭕',
in_progress: '🚧',
blocked: '🚫',
closed: '✅',
};
const typeLabels: Record<TaskType, string> = {
epic: '[EPIC]',
task: '[TASK]',
bug: '[BUG]',
};
const childrenMap = new Map<string, TrackerTask[]>();
const roots: TrackerTask[] = [];
for (const task of tasks) {
if (task.parentId) {
if (!childrenMap.has(task.parentId)) {
childrenMap.set(task.parentId, []);
}
childrenMap.get(task.parentId)!.push(task);
} else {
roots.push(task);
}
}
let output = 'Task Tracker Graph:\n';
const renderTask = (
task: TrackerTask,
depth: number,
visited: Set<string>,
) => {
if (visited.has(task.id)) {
output += `${' '.repeat(depth)}[CYCLE DETECTED: ${task.id}]\n`;
return;
}
visited.add(task.id);
const indent = ' '.repeat(depth);
output += `${indent}${statusEmojis[task.status]} ${task.id} ${typeLabels[task.type]} ${task.title}\n`;
if (task.dependencies.length > 0) {
output += `${indent} └─ Depends on: ${task.dependencies.join(', ')}\n`;
}
const children = childrenMap.get(task.id) ?? [];
for (const child of children) {
renderTask(child, depth + 1, new Set(visited));
}
};
for (const root of roots) {
renderTask(root, 0, new Set());
}
return {
llmContent: output,
returnDisplay: 'Graph rendered.',
};
}
}
export class TrackerVisualizeTool extends BaseDeclarativeTool<
Record<string, never>,
ToolResult
> {
static readonly Name = TRACKER_VISUALIZE_TOOL_NAME;
constructor(
private config: Config,
messageBus: MessageBus,
) {
super(
TrackerVisualizeTool.Name,
'Visualize Tracker',
TRACKER_VISUALIZE_DEFINITION.base.description!,
Kind.Read,
TRACKER_VISUALIZE_DEFINITION.base.parametersJsonSchema,
messageBus,
);
}
protected createInvocation(
params: Record<string, never>,
messageBus: MessageBus,
) {
return new TrackerVisualizeInvocation(
this.config,
params,
messageBus,
this.name,
);
}
override getSchema(modelId?: string) {
return resolveToolDeclaration(TRACKER_VISUALIZE_DEFINITION, modelId);
}
}