feat: Add intelligent auto-router and enhanced integrations
- Add intelligent-router.sh hook for automatic agent routing - Add AUTO-TRIGGER-SUMMARY.md documentation - Add FINAL-INTEGRATION-SUMMARY.md documentation - Complete Prometheus integration (6 commands + 4 tools) - Complete Dexto integration (12 commands + 5 tools) - Enhanced Ralph with access to all agents - Fix /clawd command (removed disable-model-invocation) - Update hooks.json to v5 with intelligent routing - 291 total skills now available - All 21 commands with automatic routing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
15
dexto/packages/tools-todo/src/error-codes.ts
Normal file
15
dexto/packages/tools-todo/src/error-codes.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Todo Service Error Codes
|
||||
*/
|
||||
|
||||
export enum TodoErrorCode {
|
||||
// Service lifecycle errors
|
||||
SERVICE_NOT_INITIALIZED = 'TODO_SERVICE_NOT_INITIALIZED',
|
||||
|
||||
// Todo management errors
|
||||
TODO_LIMIT_EXCEEDED = 'TODO_LIMIT_EXCEEDED',
|
||||
INVALID_TODO_STATUS = 'TODO_INVALID_TODO_STATUS',
|
||||
|
||||
// Database errors
|
||||
DATABASE_ERROR = 'TODO_DATABASE_ERROR',
|
||||
}
|
||||
77
dexto/packages/tools-todo/src/errors.ts
Normal file
77
dexto/packages/tools-todo/src/errors.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Todo Service Errors
|
||||
*
|
||||
* Error factory for todo list management operations
|
||||
*/
|
||||
|
||||
import { DextoRuntimeError, ErrorScope, ErrorType } from '@dexto/core';
|
||||
import { TodoErrorCode } from './error-codes.js';
|
||||
|
||||
/**
|
||||
* Error scope for todo-related errors.
|
||||
* Uses a custom string since todo is a custom tool, not part of core.
|
||||
*/
|
||||
const TODO_ERROR_SCOPE = 'todo';
|
||||
|
||||
/**
|
||||
* Factory class for creating Todo-related errors
|
||||
*/
|
||||
export class TodoError {
|
||||
private constructor() {
|
||||
// Private constructor prevents instantiation
|
||||
}
|
||||
|
||||
/**
|
||||
* Service not initialized error
|
||||
*/
|
||||
static notInitialized(): DextoRuntimeError {
|
||||
return new DextoRuntimeError(
|
||||
TodoErrorCode.SERVICE_NOT_INITIALIZED,
|
||||
TODO_ERROR_SCOPE,
|
||||
ErrorType.SYSTEM,
|
||||
'TodoService has not been initialized',
|
||||
{},
|
||||
'Initialize the TodoService before using it'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Todo limit exceeded error
|
||||
*/
|
||||
static todoLimitExceeded(current: number, max: number): DextoRuntimeError {
|
||||
return new DextoRuntimeError(
|
||||
TodoErrorCode.TODO_LIMIT_EXCEEDED,
|
||||
TODO_ERROR_SCOPE,
|
||||
ErrorType.USER,
|
||||
`Todo limit exceeded: ${current} todos. Maximum allowed: ${max}`,
|
||||
{ current, max },
|
||||
'Complete or delete existing todos before adding new ones'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalid todo status error
|
||||
*/
|
||||
static invalidStatus(status: string): DextoRuntimeError {
|
||||
return new DextoRuntimeError(
|
||||
TodoErrorCode.INVALID_TODO_STATUS,
|
||||
TODO_ERROR_SCOPE,
|
||||
ErrorType.USER,
|
||||
`Invalid todo status: ${status}. Must be 'pending', 'in_progress', or 'completed'`,
|
||||
{ status }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Database error
|
||||
*/
|
||||
static databaseError(operation: string, cause: string): DextoRuntimeError {
|
||||
return new DextoRuntimeError(
|
||||
TodoErrorCode.DATABASE_ERROR,
|
||||
TODO_ERROR_SCOPE,
|
||||
ErrorType.SYSTEM,
|
||||
`Database error during ${operation}: ${cause}`,
|
||||
{ operation, cause }
|
||||
);
|
||||
}
|
||||
}
|
||||
21
dexto/packages/tools-todo/src/index.ts
Normal file
21
dexto/packages/tools-todo/src/index.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* @dexto/tools-todo
|
||||
*
|
||||
* Todo/task tracking tools provider for Dexto agents.
|
||||
* Provides the todo_write tool for managing task lists.
|
||||
*/
|
||||
|
||||
// Main provider export
|
||||
export { todoToolsProvider } from './tool-provider.js';
|
||||
|
||||
// Service and utilities (for advanced use cases)
|
||||
export { TodoService } from './todo-service.js';
|
||||
export { TodoError } from './errors.js';
|
||||
export { TodoErrorCode } from './error-codes.js';
|
||||
|
||||
// Types
|
||||
export type { Todo, TodoInput, TodoStatus, TodoUpdateResult, TodoConfig } from './types.js';
|
||||
export { TODO_STATUS_VALUES } from './types.js';
|
||||
|
||||
// Tool implementations (for custom integrations)
|
||||
export { createTodoWriteTool } from './todo-write-tool.js';
|
||||
226
dexto/packages/tools-todo/src/todo-service.test.ts
Normal file
226
dexto/packages/tools-todo/src/todo-service.test.ts
Normal file
@@ -0,0 +1,226 @@
|
||||
/**
|
||||
* TodoService Unit Tests
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { TodoService } from './todo-service.js';
|
||||
import type { Database, AgentEventBus, IDextoLogger } from '@dexto/core';
|
||||
import type { TodoInput } from './types.js';
|
||||
|
||||
// Mock database
|
||||
function createMockDatabase(): Database {
|
||||
const store = new Map<string, unknown>();
|
||||
return {
|
||||
get: vi.fn().mockImplementation(async (key: string) => store.get(key)),
|
||||
set: vi.fn().mockImplementation(async (key: string, value: unknown) => {
|
||||
store.set(key, value);
|
||||
}),
|
||||
delete: vi.fn().mockImplementation(async (key: string) => {
|
||||
store.delete(key);
|
||||
}),
|
||||
list: vi.fn().mockResolvedValue([]),
|
||||
append: vi.fn().mockResolvedValue(undefined),
|
||||
getRange: vi.fn().mockResolvedValue([]),
|
||||
connect: vi.fn().mockResolvedValue(undefined),
|
||||
disconnect: vi.fn().mockResolvedValue(undefined),
|
||||
isConnected: vi.fn().mockReturnValue(true),
|
||||
getStoreType: vi.fn().mockReturnValue('mock'),
|
||||
} as Database;
|
||||
}
|
||||
|
||||
// Mock event bus
|
||||
function createMockEventBus(): AgentEventBus {
|
||||
return {
|
||||
emit: vi.fn(),
|
||||
on: vi.fn().mockReturnThis(),
|
||||
once: vi.fn().mockReturnThis(),
|
||||
off: vi.fn().mockReturnThis(),
|
||||
} as unknown as AgentEventBus;
|
||||
}
|
||||
|
||||
// Mock logger
|
||||
function createMockLogger(): IDextoLogger {
|
||||
return {
|
||||
debug: vi.fn(),
|
||||
silly: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
trackException: vi.fn(),
|
||||
createChild: vi.fn(() => createMockLogger()),
|
||||
setLevel: vi.fn(),
|
||||
getLevel: vi.fn().mockReturnValue('info'),
|
||||
getLogFilePath: vi.fn().mockReturnValue(null),
|
||||
destroy: vi.fn().mockResolvedValue(undefined),
|
||||
} as unknown as IDextoLogger;
|
||||
}
|
||||
|
||||
describe('TodoService', () => {
|
||||
let service: TodoService;
|
||||
let mockDb: Database;
|
||||
let mockEventBus: AgentEventBus;
|
||||
let mockLogger: IDextoLogger;
|
||||
|
||||
beforeEach(async () => {
|
||||
mockDb = createMockDatabase();
|
||||
mockEventBus = createMockEventBus();
|
||||
mockLogger = createMockLogger();
|
||||
|
||||
service = new TodoService(mockDb, mockEventBus, mockLogger);
|
||||
await service.initialize();
|
||||
});
|
||||
|
||||
describe('initialize', () => {
|
||||
it('should initialize successfully', async () => {
|
||||
const newService = new TodoService(mockDb, mockEventBus, mockLogger);
|
||||
await expect(newService.initialize()).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it('should be idempotent', async () => {
|
||||
await expect(service.initialize()).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateTodos', () => {
|
||||
const sessionId = 'test-session';
|
||||
|
||||
it('should create new todos', async () => {
|
||||
const todoInputs: TodoInput[] = [
|
||||
{ content: 'Task 1', activeForm: 'Working on Task 1', status: 'pending' },
|
||||
{ content: 'Task 2', activeForm: 'Working on Task 2', status: 'in_progress' },
|
||||
];
|
||||
|
||||
const result = await service.updateTodos(sessionId, todoInputs);
|
||||
|
||||
expect(result.todos).toHaveLength(2);
|
||||
expect(result.created).toBe(2);
|
||||
expect(result.updated).toBe(0);
|
||||
expect(result.deleted).toBe(0);
|
||||
expect(result.todos[0]?.content).toBe('Task 1');
|
||||
expect(result.todos[1]?.content).toBe('Task 2');
|
||||
});
|
||||
|
||||
it('should preserve existing todo IDs when updating', async () => {
|
||||
// Create initial todos
|
||||
const initialInputs: TodoInput[] = [
|
||||
{ content: 'Task 1', activeForm: 'Working on Task 1', status: 'pending' },
|
||||
];
|
||||
const initialResult = await service.updateTodos(sessionId, initialInputs);
|
||||
const originalId = initialResult.todos[0]?.id;
|
||||
|
||||
// Update with same content but different status
|
||||
const updatedInputs: TodoInput[] = [
|
||||
{ content: 'Task 1', activeForm: 'Working on Task 1', status: 'completed' },
|
||||
];
|
||||
const updatedResult = await service.updateTodos(sessionId, updatedInputs);
|
||||
|
||||
expect(updatedResult.todos[0]?.id).toBe(originalId);
|
||||
expect(updatedResult.todos[0]?.status).toBe('completed');
|
||||
expect(updatedResult.updated).toBe(1);
|
||||
expect(updatedResult.created).toBe(0);
|
||||
});
|
||||
|
||||
it('should track deleted todos', async () => {
|
||||
// Create initial todos
|
||||
const initialInputs: TodoInput[] = [
|
||||
{ content: 'Task 1', activeForm: 'Working on Task 1', status: 'pending' },
|
||||
{ content: 'Task 2', activeForm: 'Working on Task 2', status: 'pending' },
|
||||
];
|
||||
await service.updateTodos(sessionId, initialInputs);
|
||||
|
||||
// Update with only one todo
|
||||
const updatedInputs: TodoInput[] = [
|
||||
{ content: 'Task 1', activeForm: 'Working on Task 1', status: 'completed' },
|
||||
];
|
||||
const result = await service.updateTodos(sessionId, updatedInputs);
|
||||
|
||||
expect(result.todos).toHaveLength(1);
|
||||
expect(result.deleted).toBe(1);
|
||||
});
|
||||
|
||||
it('should emit service:event when todos are updated', async () => {
|
||||
const todoInputs: TodoInput[] = [
|
||||
{ content: 'Task 1', activeForm: 'Working on Task 1', status: 'pending' },
|
||||
];
|
||||
|
||||
await service.updateTodos(sessionId, todoInputs);
|
||||
|
||||
expect(mockEventBus.emit).toHaveBeenCalledWith('service:event', {
|
||||
service: 'todo',
|
||||
event: 'updated',
|
||||
sessionId,
|
||||
data: expect.objectContaining({
|
||||
todos: expect.any(Array),
|
||||
stats: expect.objectContaining({
|
||||
created: 1,
|
||||
updated: 0,
|
||||
deleted: 0,
|
||||
}),
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect maxTodosPerSession limit', async () => {
|
||||
const limitedService = new TodoService(mockDb, mockEventBus, mockLogger, {
|
||||
maxTodosPerSession: 2,
|
||||
});
|
||||
await limitedService.initialize();
|
||||
|
||||
const todoInputs: TodoInput[] = [
|
||||
{ content: 'Task 1', activeForm: 'Working on Task 1', status: 'pending' },
|
||||
{ content: 'Task 2', activeForm: 'Working on Task 2', status: 'pending' },
|
||||
{ content: 'Task 3', activeForm: 'Working on Task 3', status: 'pending' },
|
||||
];
|
||||
|
||||
await expect(limitedService.updateTodos(sessionId, todoInputs)).rejects.toThrow(
|
||||
/Todo limit exceeded/
|
||||
);
|
||||
});
|
||||
|
||||
it('should not emit events when enableEvents is false', async () => {
|
||||
const silentService = new TodoService(mockDb, mockEventBus, mockLogger, {
|
||||
enableEvents: false,
|
||||
});
|
||||
await silentService.initialize();
|
||||
|
||||
const todoInputs: TodoInput[] = [
|
||||
{ content: 'Task 1', activeForm: 'Working on Task 1', status: 'pending' },
|
||||
];
|
||||
|
||||
await silentService.updateTodos(sessionId, todoInputs);
|
||||
|
||||
expect(mockEventBus.emit).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTodos', () => {
|
||||
const sessionId = 'test-session';
|
||||
|
||||
it('should return empty array for new session', async () => {
|
||||
const todos = await service.getTodos(sessionId);
|
||||
expect(todos).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return todos after update', async () => {
|
||||
const todoInputs: TodoInput[] = [
|
||||
{ content: 'Task 1', activeForm: 'Working on Task 1', status: 'pending' },
|
||||
];
|
||||
await service.updateTodos(sessionId, todoInputs);
|
||||
|
||||
const todos = await service.getTodos(sessionId);
|
||||
expect(todos).toHaveLength(1);
|
||||
expect(todos[0]?.content).toBe('Task 1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
it('should throw if not initialized', async () => {
|
||||
const uninitService = new TodoService(mockDb, mockEventBus, mockLogger);
|
||||
|
||||
await expect(uninitService.getTodos('session')).rejects.toThrow(/not been initialized/);
|
||||
await expect(uninitService.updateTodos('session', [])).rejects.toThrow(
|
||||
/not been initialized/
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
209
dexto/packages/tools-todo/src/todo-service.ts
Normal file
209
dexto/packages/tools-todo/src/todo-service.ts
Normal file
@@ -0,0 +1,209 @@
|
||||
/**
|
||||
* Todo Service
|
||||
*
|
||||
* Manages todo lists for tracking agent workflow and task progress.
|
||||
* Emits events through the AgentEventBus using the service:event pattern.
|
||||
*/
|
||||
|
||||
import { nanoid } from 'nanoid';
|
||||
import type { Database, AgentEventBus, IDextoLogger } from '@dexto/core';
|
||||
import { DextoRuntimeError } from '@dexto/core';
|
||||
import { TodoError } from './errors.js';
|
||||
import type { Todo, TodoInput, TodoUpdateResult, TodoConfig, TodoStatus } from './types.js';
|
||||
import { TODO_STATUS_VALUES } from './types.js';
|
||||
|
||||
const DEFAULT_MAX_TODOS = 100;
|
||||
const TODOS_KEY_PREFIX = 'todos:';
|
||||
|
||||
/**
|
||||
* TodoService - Manages todo lists for agent workflow tracking
|
||||
*/
|
||||
export class TodoService {
|
||||
private database: Database;
|
||||
private eventBus: AgentEventBus;
|
||||
private logger: IDextoLogger;
|
||||
private config: Required<TodoConfig>;
|
||||
private initialized: boolean = false;
|
||||
|
||||
constructor(
|
||||
database: Database,
|
||||
eventBus: AgentEventBus,
|
||||
logger: IDextoLogger,
|
||||
config: TodoConfig = {}
|
||||
) {
|
||||
this.database = database;
|
||||
this.eventBus = eventBus;
|
||||
this.logger = logger;
|
||||
this.config = {
|
||||
maxTodosPerSession: config.maxTodosPerSession ?? DEFAULT_MAX_TODOS,
|
||||
enableEvents: config.enableEvents ?? true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the service
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
if (this.initialized) {
|
||||
this.logger.debug('TodoService already initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
this.initialized = true;
|
||||
this.logger.info('TodoService initialized successfully');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update todos for a session (replaces entire list)
|
||||
*/
|
||||
async updateTodos(sessionId: string, todoInputs: TodoInput[]): Promise<TodoUpdateResult> {
|
||||
if (!this.initialized) {
|
||||
throw TodoError.notInitialized();
|
||||
}
|
||||
|
||||
// Validate todo count
|
||||
if (todoInputs.length > this.config.maxTodosPerSession) {
|
||||
throw TodoError.todoLimitExceeded(todoInputs.length, this.config.maxTodosPerSession);
|
||||
}
|
||||
|
||||
try {
|
||||
// Get existing todos
|
||||
const existing = await this.getTodos(sessionId);
|
||||
const existingMap = new Map(existing.map((t) => [this.getTodoKey(t), t]));
|
||||
|
||||
// Create new todos with IDs
|
||||
const now = new Date();
|
||||
const newTodos: Todo[] = [];
|
||||
const stats = { created: 0, updated: 0, deleted: 0 };
|
||||
|
||||
for (let i = 0; i < todoInputs.length; i++) {
|
||||
const input = todoInputs[i]!;
|
||||
this.validateTodoStatus(input.status);
|
||||
|
||||
// Generate consistent key for matching
|
||||
const todoKey = this.getTodoKeyFromInput(input);
|
||||
const existingTodo = existingMap.get(todoKey);
|
||||
|
||||
if (existingTodo) {
|
||||
// Update existing todo
|
||||
const updated: Todo = {
|
||||
...existingTodo,
|
||||
status: input.status,
|
||||
updatedAt: now,
|
||||
position: i,
|
||||
};
|
||||
newTodos.push(updated);
|
||||
stats.updated++;
|
||||
existingMap.delete(todoKey);
|
||||
} else {
|
||||
// Create new todo
|
||||
const created: Todo = {
|
||||
id: nanoid(),
|
||||
sessionId,
|
||||
content: input.content,
|
||||
activeForm: input.activeForm,
|
||||
status: input.status,
|
||||
position: i,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
newTodos.push(created);
|
||||
stats.created++;
|
||||
}
|
||||
}
|
||||
|
||||
// Remaining items in existingMap are deleted
|
||||
stats.deleted = existingMap.size;
|
||||
|
||||
// Save to database
|
||||
const key = this.getTodosDatabaseKey(sessionId);
|
||||
await this.database.set(key, newTodos);
|
||||
|
||||
// Emit event using the service:event pattern
|
||||
if (this.config.enableEvents) {
|
||||
this.eventBus.emit('service:event', {
|
||||
service: 'todo',
|
||||
event: 'updated',
|
||||
sessionId,
|
||||
data: {
|
||||
todos: newTodos,
|
||||
stats,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
this.logger.debug(
|
||||
`Updated todos for session ${sessionId}: ${stats.created} created, ${stats.updated} updated, ${stats.deleted} deleted`
|
||||
);
|
||||
|
||||
return {
|
||||
todos: newTodos,
|
||||
sessionId,
|
||||
...stats,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof DextoRuntimeError) {
|
||||
throw error;
|
||||
}
|
||||
throw TodoError.databaseError(
|
||||
'updateTodos',
|
||||
error instanceof Error ? error.message : String(error)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get todos for a session
|
||||
*/
|
||||
async getTodos(sessionId: string): Promise<Todo[]> {
|
||||
if (!this.initialized) {
|
||||
throw TodoError.notInitialized();
|
||||
}
|
||||
|
||||
try {
|
||||
const key = this.getTodosDatabaseKey(sessionId);
|
||||
const todos = await this.database.get<Todo[]>(key);
|
||||
return todos || [];
|
||||
} catch (error) {
|
||||
if (error instanceof DextoRuntimeError) {
|
||||
throw error;
|
||||
}
|
||||
throw TodoError.databaseError(
|
||||
'getTodos',
|
||||
error instanceof Error ? error.message : String(error)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate database key for session todos
|
||||
*/
|
||||
private getTodosDatabaseKey(sessionId: string): string {
|
||||
return `${TODOS_KEY_PREFIX}${sessionId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate consistent key for todo matching (content + activeForm)
|
||||
* Uses JSON encoding to prevent collisions when fields contain delimiters
|
||||
*/
|
||||
private getTodoKey(todo: Todo | TodoInput): string {
|
||||
return JSON.stringify([todo.content, todo.activeForm]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate key from TodoInput
|
||||
* Uses JSON encoding to prevent collisions when fields contain delimiters
|
||||
*/
|
||||
private getTodoKeyFromInput(input: TodoInput): string {
|
||||
return JSON.stringify([input.content, input.activeForm]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate todo status
|
||||
*/
|
||||
private validateTodoStatus(status: TodoStatus): void {
|
||||
if (!TODO_STATUS_VALUES.includes(status)) {
|
||||
throw TodoError.invalidStatus(status);
|
||||
}
|
||||
}
|
||||
}
|
||||
95
dexto/packages/tools-todo/src/todo-write-tool.ts
Normal file
95
dexto/packages/tools-todo/src/todo-write-tool.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* Todo Write Tool
|
||||
*
|
||||
* Manages task lists for tracking agent progress and workflow organization
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import type { InternalTool, ToolExecutionContext } from '@dexto/core';
|
||||
import type { TodoService } from './todo-service.js';
|
||||
import { TODO_STATUS_VALUES } from './types.js';
|
||||
|
||||
/**
|
||||
* Zod schema for todo item input
|
||||
*/
|
||||
const TodoItemSchema = z
|
||||
.object({
|
||||
content: z
|
||||
.string()
|
||||
.min(1)
|
||||
.describe('Task description in imperative form (e.g., "Fix authentication bug")'),
|
||||
activeForm: z
|
||||
.string()
|
||||
.min(1)
|
||||
.describe(
|
||||
'Present continuous form shown during execution (e.g., "Fixing authentication bug")'
|
||||
),
|
||||
status: z
|
||||
.enum(TODO_STATUS_VALUES)
|
||||
.describe(
|
||||
'Task status: pending (not started), in_progress (currently working), completed (finished)'
|
||||
),
|
||||
})
|
||||
.strict();
|
||||
|
||||
/**
|
||||
* Zod schema for todo_write tool input
|
||||
*/
|
||||
const TodoWriteInputSchema = z
|
||||
.object({
|
||||
todos: z
|
||||
.array(TodoItemSchema)
|
||||
.min(1)
|
||||
.describe('Array of todo items representing the complete task list for the session'),
|
||||
})
|
||||
.strict()
|
||||
.superRefine((value, ctx) => {
|
||||
const inProgressCount = value.todos.filter((todo) => todo.status === 'in_progress').length;
|
||||
if (inProgressCount > 1) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Only one todo may be in_progress at a time.',
|
||||
path: ['todos'],
|
||||
});
|
||||
}
|
||||
})
|
||||
.describe(
|
||||
'Manage task list for current session. Replaces the entire todo list with the provided tasks.'
|
||||
);
|
||||
|
||||
/**
|
||||
* Create todo_write internal tool
|
||||
*/
|
||||
export function createTodoWriteTool(todoService: TodoService): InternalTool {
|
||||
return {
|
||||
id: 'todo_write',
|
||||
description: `Track progress on multi-step tasks. Use for:
|
||||
- Implementation tasks with 3+ steps (features, refactors, bug fixes)
|
||||
- Tasks where the user asks for a plan or breakdown
|
||||
- Complex workflows where progress visibility helps
|
||||
|
||||
Do NOT use for simple single-file edits, quick questions, or explanations.
|
||||
|
||||
IMPORTANT: This replaces the entire todo list. Always include ALL tasks (pending, in_progress, completed). Only ONE task should be in_progress at a time. Update status as you work: pending → in_progress → completed.`,
|
||||
inputSchema: TodoWriteInputSchema,
|
||||
|
||||
execute: async (input: unknown, context?: ToolExecutionContext): Promise<unknown> => {
|
||||
// Validate input against schema
|
||||
const validatedInput = TodoWriteInputSchema.parse(input);
|
||||
|
||||
// Use session_id from context, otherwise default
|
||||
const sessionId = context?.sessionId ?? 'default';
|
||||
|
||||
// Update todos in todo service
|
||||
const result = await todoService.updateTodos(sessionId, validatedInput.todos);
|
||||
|
||||
// Count by status for summary
|
||||
const completed = result.todos.filter((t) => t.status === 'completed').length;
|
||||
const inProgress = result.todos.filter((t) => t.status === 'in_progress').length;
|
||||
const pending = result.todos.filter((t) => t.status === 'pending').length;
|
||||
|
||||
// Return simple summary - TodoPanel shows full state
|
||||
return `Updated tasks: ${completed}/${result.todos.length} completed${inProgress > 0 ? `, 1 in progress` : ''}${pending > 0 ? `, ${pending} pending` : ''}`;
|
||||
},
|
||||
};
|
||||
}
|
||||
92
dexto/packages/tools-todo/src/tool-provider.ts
Normal file
92
dexto/packages/tools-todo/src/tool-provider.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Todo Tools Provider
|
||||
*
|
||||
* Provides task tracking tools by wrapping TodoService.
|
||||
* When registered, the provider initializes TodoService and creates the
|
||||
* todo_write tool for managing task lists.
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import type { CustomToolProvider, ToolCreationContext, InternalTool } from '@dexto/core';
|
||||
import { TodoService } from './todo-service.js';
|
||||
import { createTodoWriteTool } from './todo-write-tool.js';
|
||||
|
||||
/**
|
||||
* Default configuration constants for Todo tools.
|
||||
*/
|
||||
const DEFAULT_MAX_TODOS_PER_SESSION = 100;
|
||||
const DEFAULT_ENABLE_EVENTS = true;
|
||||
|
||||
/**
|
||||
* Configuration schema for Todo tools provider.
|
||||
*/
|
||||
const TodoToolsConfigSchema = z
|
||||
.object({
|
||||
type: z.literal('todo-tools'),
|
||||
maxTodosPerSession: z
|
||||
.number()
|
||||
.int()
|
||||
.positive()
|
||||
.default(DEFAULT_MAX_TODOS_PER_SESSION)
|
||||
.describe(`Maximum todos per session (default: ${DEFAULT_MAX_TODOS_PER_SESSION})`),
|
||||
enableEvents: z
|
||||
.boolean()
|
||||
.default(DEFAULT_ENABLE_EVENTS)
|
||||
.describe('Enable real-time events for todo updates'),
|
||||
})
|
||||
.strict();
|
||||
|
||||
type TodoToolsConfig = z.output<typeof TodoToolsConfigSchema>;
|
||||
|
||||
/**
|
||||
* Todo tools provider.
|
||||
*
|
||||
* Wraps TodoService and provides the todo_write tool for managing task lists.
|
||||
*
|
||||
* When registered via customToolRegistry, TodoService is automatically
|
||||
* initialized and the todo_write tool becomes available to the agent.
|
||||
*/
|
||||
export const todoToolsProvider: CustomToolProvider<'todo-tools', TodoToolsConfig> = {
|
||||
type: 'todo-tools',
|
||||
configSchema: TodoToolsConfigSchema,
|
||||
|
||||
create: (config: TodoToolsConfig, context: ToolCreationContext): InternalTool[] => {
|
||||
const { logger, agent, services } = context;
|
||||
|
||||
logger.debug('Creating TodoService for todo tools');
|
||||
|
||||
// Get required services from context
|
||||
const storageManager = services?.storageManager;
|
||||
if (!storageManager) {
|
||||
throw new Error(
|
||||
'TodoService requires storageManager service. Ensure it is available in ToolCreationContext.'
|
||||
);
|
||||
}
|
||||
|
||||
const database = storageManager.getDatabase();
|
||||
const eventBus = agent.agentEventBus;
|
||||
|
||||
// Create TodoService with validated config
|
||||
const todoService = new TodoService(database, eventBus, logger, {
|
||||
maxTodosPerSession: config.maxTodosPerSession,
|
||||
enableEvents: config.enableEvents,
|
||||
});
|
||||
|
||||
// Start initialization in background
|
||||
todoService.initialize().catch((error) => {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
logger.error(`TodoToolsProvider.create: Failed to initialize TodoService: ${message}`);
|
||||
});
|
||||
|
||||
logger.debug('TodoService created - initialization will complete on first tool use');
|
||||
|
||||
// Create and return the todo_write tool
|
||||
return [createTodoWriteTool(todoService)];
|
||||
},
|
||||
|
||||
metadata: {
|
||||
displayName: 'Todo Tools',
|
||||
description: 'Task tracking and workflow management (todo_write)',
|
||||
category: 'workflow',
|
||||
},
|
||||
};
|
||||
60
dexto/packages/tools-todo/src/types.ts
Normal file
60
dexto/packages/tools-todo/src/types.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Todo Service Types
|
||||
*
|
||||
* Types for todo list management and workflow tracking
|
||||
*/
|
||||
|
||||
/**
|
||||
* Valid todo status values
|
||||
* Centralized constant to prevent duplication across domains
|
||||
*/
|
||||
export const TODO_STATUS_VALUES = ['pending', 'in_progress', 'completed'] as const;
|
||||
|
||||
/**
|
||||
* Todo item status
|
||||
*/
|
||||
export type TodoStatus = (typeof TODO_STATUS_VALUES)[number];
|
||||
|
||||
/**
|
||||
* Todo item with system metadata
|
||||
*/
|
||||
export interface Todo {
|
||||
id: string;
|
||||
sessionId: string;
|
||||
content: string;
|
||||
activeForm: string;
|
||||
status: TodoStatus;
|
||||
position: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Todo input from tool (without system metadata)
|
||||
*/
|
||||
export interface TodoInput {
|
||||
content: string;
|
||||
activeForm: string;
|
||||
status: TodoStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Todo list update result
|
||||
*/
|
||||
export interface TodoUpdateResult {
|
||||
todos: Todo[];
|
||||
sessionId: string;
|
||||
created: number;
|
||||
updated: number;
|
||||
deleted: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for TodoService
|
||||
*/
|
||||
export interface TodoConfig {
|
||||
/** Maximum todos per session */
|
||||
maxTodosPerSession?: number;
|
||||
/** Enable real-time events */
|
||||
enableEvents?: boolean;
|
||||
}
|
||||
Reference in New Issue
Block a user