mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-23 03:24:42 -07:00
feat(core): Add tracker CRUD tools & visualization (#19489)
Co-authored-by: Jerop Kipruto <jerop@google.com>
This commit is contained in:
@@ -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: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user