illuma-agents 1.0.66 → 1.0.67

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/cjs/common/enum.cjs +2 -0
  2. package/dist/cjs/common/enum.cjs.map +1 -1
  3. package/dist/cjs/main.cjs +19 -0
  4. package/dist/cjs/main.cjs.map +1 -1
  5. package/dist/cjs/prompts/collab.cjs +11 -0
  6. package/dist/cjs/prompts/collab.cjs.map +1 -0
  7. package/dist/cjs/prompts/taskmanager.cjs +66 -0
  8. package/dist/cjs/prompts/taskmanager.cjs.map +1 -0
  9. package/dist/cjs/prompts/taskplanning.cjs +99 -0
  10. package/dist/cjs/prompts/taskplanning.cjs.map +1 -0
  11. package/dist/cjs/tools/TaskProgress.cjs +172 -0
  12. package/dist/cjs/tools/TaskProgress.cjs.map +1 -0
  13. package/dist/esm/common/enum.mjs +2 -0
  14. package/dist/esm/common/enum.mjs.map +1 -1
  15. package/dist/esm/main.mjs +4 -0
  16. package/dist/esm/main.mjs.map +1 -1
  17. package/dist/esm/prompts/collab.mjs +9 -0
  18. package/dist/esm/prompts/collab.mjs.map +1 -0
  19. package/dist/esm/prompts/taskmanager.mjs +60 -0
  20. package/dist/esm/prompts/taskmanager.mjs.map +1 -0
  21. package/dist/esm/prompts/taskplanning.mjs +94 -0
  22. package/dist/esm/prompts/taskplanning.mjs.map +1 -0
  23. package/dist/esm/tools/TaskProgress.mjs +163 -0
  24. package/dist/esm/tools/TaskProgress.mjs.map +1 -0
  25. package/dist/types/common/enum.d.ts +2 -0
  26. package/dist/types/index.d.ts +2 -0
  27. package/dist/types/prompts/index.d.ts +1 -0
  28. package/dist/types/prompts/taskplanning.d.ts +54 -0
  29. package/dist/types/tools/TaskProgress.d.ts +142 -0
  30. package/package.json +1 -1
  31. package/src/common/enum.ts +2 -0
  32. package/src/index.ts +4 -0
  33. package/src/prompts/index.ts +2 -1
  34. package/src/prompts/taskplanning.ts +96 -0
  35. package/src/specs/task-progress.test.ts +330 -0
  36. package/src/tools/TaskProgress.ts +247 -0
@@ -1,2 +1,3 @@
1
1
  export * from './collab';
2
- export * from './taskmanager';
2
+ export * from './taskmanager';
3
+ export * from './taskplanning';
@@ -0,0 +1,96 @@
1
+ // src/prompts/taskplanning.ts
2
+
3
+ /**
4
+ * System prompt fragment for task planning capabilities.
5
+ *
6
+ * Include this in the agent's system prompt when:
7
+ * - Agent is ephemeral (not a persistent workflow)
8
+ * - Agent handles complex, multi-step research or coding tasks
9
+ * - You want visible progress tracking for the user
10
+ *
11
+ * Do NOT include for:
12
+ * - Simple tool-calling agents
13
+ * - Workflow/builder agents with predefined steps
14
+ * - Agents with fixed execution patterns
15
+ */
16
+ export const taskPlanningPrompt = `## Task Planning & Progress
17
+
18
+ You have access to a \`manage_todo_list\` tool for tracking complex tasks.
19
+
20
+ ### When to Use Task Planning
21
+ - **USE** for complex work requiring 3+ steps (research, multi-file changes, analysis)
22
+ - **SKIP** for simple requests (single file edit, quick lookup, conversational responses)
23
+
24
+ ### Task Workflow
25
+ 1. **Plan First**: Before starting complex work, create a todo list with 3-6 clear, actionable items
26
+ 2. **One at a Time**: Mark ONE task as "in-progress" before starting work on it
27
+ 3. **Multiple Tool Calls OK**: A single task may require multiple tool calls (searches, reads, writes)
28
+ 4. **Complete Immediately**: Mark task "completed" right after finishing, don't batch updates
29
+ 5. **Iterate**: Move to next task, repeat until done
30
+
31
+ ### Task Best Practices
32
+ - Keep titles short and action-oriented (3-7 words): "Research API options", "Implement auth flow"
33
+ - Group related work into single tasks rather than micro-tasks
34
+ - Update status promptly so user sees real-time progress
35
+ - For simple 1-2 step work, just do it directly without creating todos
36
+
37
+ ### Example Task Breakdown
38
+ For "Add user authentication to the app":
39
+ 1. Review existing auth code and dependencies
40
+ 2. Implement login endpoint
41
+ 3. Add session management
42
+ 4. Create protected route middleware
43
+ 5. Test authentication flow
44
+
45
+ Each task above might involve multiple file reads, code writes, and searches - that's expected.
46
+ `;
47
+
48
+ /**
49
+ * Short version of task planning prompt for constrained context
50
+ */
51
+ export const taskPlanningPromptShort = `## Task Tracking
52
+ Use \`manage_todo_list\` for complex multi-step work (3+ steps).
53
+ - Create 3-6 actionable todos before starting
54
+ - Mark one "in-progress" at a time
55
+ - Mark "completed" immediately after finishing
56
+ - Skip for simple 1-2 step tasks
57
+ `;
58
+
59
+ /**
60
+ * Function description for structured output agents
61
+ */
62
+ export const manageTodoListFunctionDescription =
63
+ 'Manage a todo list for tracking progress on complex multi-step tasks. Create todos before starting work, mark in-progress while working, mark completed when done.';
64
+
65
+ /**
66
+ * Function parameters schema (for agents using function calling format)
67
+ */
68
+ export const manageTodoListFunctionParameters = {
69
+ type: 'object',
70
+ properties: {
71
+ todoList: {
72
+ type: 'array',
73
+ description: 'Complete array of all todo items (existing and new)',
74
+ items: {
75
+ type: 'object',
76
+ properties: {
77
+ id: {
78
+ type: 'number',
79
+ description: 'Sequential ID starting from 1',
80
+ },
81
+ title: {
82
+ type: 'string',
83
+ description: 'Short action-oriented title (3-7 words)',
84
+ },
85
+ status: {
86
+ type: 'string',
87
+ enum: ['not-started', 'in-progress', 'completed'],
88
+ description: 'Current task status',
89
+ },
90
+ },
91
+ required: ['id', 'title', 'status'],
92
+ },
93
+ },
94
+ },
95
+ required: ['todoList'],
96
+ };
@@ -0,0 +1,330 @@
1
+ // src/specs/task-progress.test.ts
2
+ import { GraphEvents } from '@/common';
3
+ import {
4
+ createTaskProgressTool,
5
+ TaskProgressToolName,
6
+ TaskProgressToolDescription,
7
+ TaskProgressToolSchema,
8
+ TaskProgressToolDefinition,
9
+ type TaskItem,
10
+ type TaskStatus,
11
+ type TaskProgressPayload,
12
+ } from '@/tools/TaskProgress';
13
+
14
+ // Mock the safeDispatchCustomEvent
15
+ jest.mock('@/utils/events', () => ({
16
+ safeDispatchCustomEvent: jest.fn().mockResolvedValue(undefined),
17
+ }));
18
+
19
+ import { safeDispatchCustomEvent } from '@/utils/events';
20
+ const mockDispatch = safeDispatchCustomEvent as jest.MockedFunction<
21
+ typeof safeDispatchCustomEvent
22
+ >;
23
+
24
+ describe('TaskProgress Tool', () => {
25
+ beforeEach(() => {
26
+ jest.clearAllMocks();
27
+ });
28
+
29
+ describe('Tool Definition', () => {
30
+ it('should have correct tool name', () => {
31
+ expect(TaskProgressToolName).toBe('manage_todo_list');
32
+ });
33
+
34
+ it('should have description with usage guidelines', () => {
35
+ expect(TaskProgressToolDescription).toContain('todo list');
36
+ expect(TaskProgressToolDescription).toContain('not-started');
37
+ expect(TaskProgressToolDescription).toContain('in-progress');
38
+ expect(TaskProgressToolDescription).toContain('completed');
39
+ });
40
+
41
+ it('should have valid JSON schema', () => {
42
+ expect(TaskProgressToolSchema.type).toBe('object');
43
+ expect(TaskProgressToolSchema.required).toContain('todoList');
44
+ expect(TaskProgressToolSchema.properties.todoList.type).toBe('array');
45
+ });
46
+
47
+ it('should export complete tool definition', () => {
48
+ expect(TaskProgressToolDefinition.name).toBe(TaskProgressToolName);
49
+ expect(TaskProgressToolDefinition.description).toBe(
50
+ TaskProgressToolDescription
51
+ );
52
+ expect(TaskProgressToolDefinition.schema).toEqual(TaskProgressToolSchema);
53
+ });
54
+ });
55
+
56
+ describe('createTaskProgressTool', () => {
57
+ it('should create a tool with correct name', () => {
58
+ const tool = createTaskProgressTool();
59
+ expect(tool.name).toBe('manage_todo_list');
60
+ });
61
+
62
+ it('should create a tool with description', () => {
63
+ const tool = createTaskProgressTool();
64
+ expect(tool.description).toBeDefined();
65
+ expect(tool.description.length).toBeGreaterThan(0);
66
+ });
67
+
68
+ it('should accept onTaskUpdate callback parameter', () => {
69
+ const mockCallback = jest.fn();
70
+ const tool = createTaskProgressTool({ onTaskUpdate: mockCallback });
71
+ expect(tool).toBeDefined();
72
+ });
73
+ });
74
+
75
+ describe('Tool Invocation', () => {
76
+ it('should accept valid todo list and return success message', async () => {
77
+ const tool = createTaskProgressTool();
78
+
79
+ const validTodoList: TaskItem[] = [
80
+ { id: 1, title: 'Research API', status: 'not-started' },
81
+ { id: 2, title: 'Implement feature', status: 'not-started' },
82
+ ];
83
+
84
+ const result = await tool.invoke({ todoList: validTodoList });
85
+
86
+ expect(result).toContain('Todo list updated successfully');
87
+ expect(result).toContain('0/2 completed');
88
+ expect(result).toContain('2 remaining');
89
+ });
90
+
91
+ it('should dispatch SSE event with correct event type', async () => {
92
+ const tool = createTaskProgressTool();
93
+
94
+ const todoList: TaskItem[] = [
95
+ { id: 1, title: 'Task 1', status: 'in-progress' },
96
+ ];
97
+
98
+ await tool.invoke({ todoList });
99
+
100
+ expect(mockDispatch).toHaveBeenCalledWith(
101
+ GraphEvents.ON_TASK_PROGRESS,
102
+ expect.objectContaining({
103
+ tasks: todoList,
104
+ timestamp: expect.any(String),
105
+ isComplete: false,
106
+ }),
107
+ expect.anything() // config object is passed
108
+ );
109
+ });
110
+
111
+ it('should include timestamp in dispatched payload', async () => {
112
+ const tool = createTaskProgressTool();
113
+
114
+ const todoList: TaskItem[] = [
115
+ { id: 1, title: 'Task', status: 'not-started' },
116
+ ];
117
+
118
+ await tool.invoke({ todoList });
119
+
120
+ const dispatchedPayload = mockDispatch.mock.calls[0][1] as TaskProgressPayload;
121
+ expect(dispatchedPayload.timestamp).toBeDefined();
122
+ expect(new Date(dispatchedPayload.timestamp).getTime()).toBeGreaterThan(0);
123
+ });
124
+
125
+ it('should call onTaskUpdate callback when provided', async () => {
126
+ const mockCallback = jest.fn();
127
+ const tool = createTaskProgressTool({ onTaskUpdate: mockCallback });
128
+
129
+ const todoList: TaskItem[] = [
130
+ { id: 1, title: 'Task', status: 'completed' },
131
+ ];
132
+
133
+ await tool.invoke({ todoList });
134
+
135
+ expect(mockCallback).toHaveBeenCalledWith(todoList);
136
+ });
137
+
138
+ it('should handle async onTaskUpdate callback', async () => {
139
+ const asyncCallback = jest.fn().mockResolvedValue(undefined);
140
+ const tool = createTaskProgressTool({ onTaskUpdate: asyncCallback });
141
+
142
+ const todoList: TaskItem[] = [
143
+ { id: 1, title: 'Task', status: 'in-progress' },
144
+ ];
145
+
146
+ await tool.invoke({ todoList });
147
+
148
+ expect(asyncCallback).toHaveBeenCalledWith(todoList);
149
+ });
150
+
151
+ it('should not fail if onTaskUpdate throws', async () => {
152
+ const failingCallback = jest.fn().mockRejectedValue(new Error('Callback error'));
153
+ const tool = createTaskProgressTool({ onTaskUpdate: failingCallback });
154
+
155
+ const todoList: TaskItem[] = [
156
+ { id: 1, title: 'Task', status: 'in-progress' },
157
+ ];
158
+
159
+ // Should not throw, just warn
160
+ const result = await tool.invoke({ todoList });
161
+ expect(result).toContain('Todo list updated successfully');
162
+ });
163
+ });
164
+
165
+ describe('Status Tracking', () => {
166
+ it('should correctly count completed tasks', async () => {
167
+ const tool = createTaskProgressTool();
168
+
169
+ const todoList: TaskItem[] = [
170
+ { id: 1, title: 'Done 1', status: 'completed' },
171
+ { id: 2, title: 'Done 2', status: 'completed' },
172
+ { id: 3, title: 'Not done', status: 'not-started' },
173
+ ];
174
+
175
+ const result = await tool.invoke({ todoList });
176
+
177
+ expect(result).toContain('2/3 completed');
178
+ });
179
+
180
+ it('should correctly count in-progress tasks', async () => {
181
+ const tool = createTaskProgressTool();
182
+
183
+ const todoList: TaskItem[] = [
184
+ { id: 1, title: 'Working', status: 'in-progress' },
185
+ { id: 2, title: 'Not started', status: 'not-started' },
186
+ ];
187
+
188
+ const result = await tool.invoke({ todoList });
189
+
190
+ expect(result).toContain('1 in progress');
191
+ });
192
+
193
+ it('should correctly count not-started tasks', async () => {
194
+ const tool = createTaskProgressTool();
195
+
196
+ const todoList: TaskItem[] = [
197
+ { id: 1, title: 'Task 1', status: 'not-started' },
198
+ { id: 2, title: 'Task 2', status: 'not-started' },
199
+ { id: 3, title: 'Task 3', status: 'completed' },
200
+ ];
201
+
202
+ const result = await tool.invoke({ todoList });
203
+
204
+ expect(result).toContain('2 remaining');
205
+ });
206
+
207
+ it('should warn about multiple in-progress tasks', async () => {
208
+ const tool = createTaskProgressTool();
209
+
210
+ const todoList: TaskItem[] = [
211
+ { id: 1, title: 'Task 1', status: 'in-progress' },
212
+ { id: 2, title: 'Task 2', status: 'in-progress' },
213
+ ];
214
+
215
+ const result = await tool.invoke({ todoList });
216
+
217
+ expect(result).toContain('Warning');
218
+ expect(result).toContain('2 tasks marked as in-progress');
219
+ });
220
+
221
+ it('should support failed status', async () => {
222
+ const tool = createTaskProgressTool();
223
+
224
+ const todoList: TaskItem[] = [
225
+ { id: 1, title: 'Failed task', status: 'failed' },
226
+ { id: 2, title: 'Completed', status: 'completed' },
227
+ ];
228
+
229
+ const result = await tool.invoke({ todoList });
230
+
231
+ expect(result).toContain('Todo list updated successfully');
232
+ });
233
+ });
234
+
235
+ describe('Validation', () => {
236
+ it('should throw on non-array todoList (schema validation)', async () => {
237
+ const tool = createTaskProgressTool();
238
+
239
+ // LangChain schema validation catches this before our code runs
240
+ await expect(
241
+ tool.invoke({ todoList: 'not an array' })
242
+ ).rejects.toThrow();
243
+ });
244
+
245
+ it('should throw on tasks with non-numeric id (schema validation)', async () => {
246
+ const tool = createTaskProgressTool();
247
+
248
+ const invalidTodoList = [
249
+ { id: 'abc', title: 'Task', status: 'not-started' },
250
+ ];
251
+
252
+ // LangChain schema validation catches this before our code runs
253
+ await expect(
254
+ tool.invoke({ todoList: invalidTodoList })
255
+ ).rejects.toThrow();
256
+ });
257
+
258
+ it('should reject tasks with empty title', async () => {
259
+ const tool = createTaskProgressTool();
260
+
261
+ const invalidTodoList = [{ id: 1, title: '', status: 'not-started' }];
262
+
263
+ const result = await tool.invoke({ todoList: invalidTodoList });
264
+
265
+ expect(result).toContain('Error');
266
+ expect(result).toContain('title');
267
+ });
268
+
269
+ it('should throw on tasks with invalid status (schema validation)', async () => {
270
+ const tool = createTaskProgressTool();
271
+
272
+ const invalidTodoList = [
273
+ { id: 1, title: 'Task', status: 'invalid-status' },
274
+ ];
275
+
276
+ // LangChain schema validation catches this before our code runs
277
+ await expect(
278
+ tool.invoke({ todoList: invalidTodoList })
279
+ ).rejects.toThrow();
280
+ });
281
+
282
+ it('should accept empty todo list', async () => {
283
+ const tool = createTaskProgressTool();
284
+
285
+ const result = await tool.invoke({ todoList: [] });
286
+
287
+ expect(result).toContain('Todo list updated successfully');
288
+ expect(result).toContain('0/0 completed');
289
+ });
290
+ });
291
+
292
+ describe('Type Exports', () => {
293
+ it('should export TaskStatus type with correct values', () => {
294
+ const validStatuses: TaskStatus[] = [
295
+ 'not-started',
296
+ 'in-progress',
297
+ 'completed',
298
+ 'failed',
299
+ ];
300
+
301
+ validStatuses.forEach((status) => {
302
+ expect(['not-started', 'in-progress', 'completed', 'failed']).toContain(
303
+ status
304
+ );
305
+ });
306
+ });
307
+
308
+ it('should export TaskItem type with required fields', () => {
309
+ const validTask: TaskItem = {
310
+ id: 1,
311
+ title: 'Test task',
312
+ status: 'not-started',
313
+ };
314
+
315
+ expect(validTask.id).toBe(1);
316
+ expect(validTask.title).toBe('Test task');
317
+ expect(validTask.status).toBe('not-started');
318
+ });
319
+
320
+ it('should export TaskProgressPayload type', () => {
321
+ const payload: TaskProgressPayload = {
322
+ tasks: [{ id: 1, title: 'Test', status: 'completed' }],
323
+ timestamp: new Date().toISOString(),
324
+ };
325
+
326
+ expect(payload.tasks).toBeDefined();
327
+ expect(payload.timestamp).toBeDefined();
328
+ });
329
+ });
330
+ });
@@ -0,0 +1,247 @@
1
+ // src/tools/TaskProgress.ts
2
+ import { tool, DynamicStructuredTool } from '@langchain/core/tools';
3
+ import { safeDispatchCustomEvent } from '@/utils/events';
4
+ import { GraphEvents } from '@/common';
5
+
6
+ /**
7
+ * Task status enum matching the UI component expectations
8
+ */
9
+ export type TaskStatus = 'not-started' | 'in-progress' | 'completed' | 'failed';
10
+
11
+ /**
12
+ * Individual task item in the todo list
13
+ */
14
+ export interface TaskItem {
15
+ /** Unique identifier for the task (sequential number starting from 1) */
16
+ id: number;
17
+ /** Short, action-oriented title (3-7 words) */
18
+ title: string;
19
+ /** Current status of the task */
20
+ status: TaskStatus;
21
+ /** Optional message for additional context */
22
+ message?: string;
23
+ }
24
+
25
+ /**
26
+ * Payload sent via SSE event for task progress updates
27
+ * This matches the format expected by the Ranger UI
28
+ */
29
+ export interface TaskProgressPayload {
30
+ /** Complete list of all tasks */
31
+ tasks: TaskItem[];
32
+ /** Timestamp of the update */
33
+ timestamp: string;
34
+ /** Optional title for the task group */
35
+ title?: string;
36
+ /** Whether all tasks are complete */
37
+ isComplete?: boolean;
38
+ }
39
+
40
+ /**
41
+ * Tool name constant - matches what AI calls
42
+ */
43
+ export const TaskProgressToolName = 'manage_todo_list';
44
+
45
+ /**
46
+ * Tool description for the AI
47
+ */
48
+ export const TaskProgressToolDescription = `Manage a structured todo list to track progress and plan tasks throughout your session.
49
+
50
+ **When to use this tool:**
51
+ - Complex multi-step work requiring planning and tracking (3+ steps)
52
+ - When breaking down larger tasks into smaller actionable steps
53
+ - Before starting work on any todo (mark as in-progress)
54
+ - Immediately after completing each todo (mark as completed)
55
+ - To give users visibility into your progress and planning
56
+
57
+ **When NOT to use:**
58
+ - Single, trivial tasks completed in one step
59
+ - Purely conversational/informational requests
60
+ - Simple file reads or searches
61
+
62
+ **Critical workflow:**
63
+ 1. Plan tasks by creating todo list with specific, actionable items
64
+ 2. Mark ONE todo as in-progress before starting work
65
+ 3. Complete the work (may involve multiple tool calls)
66
+ 4. Mark that todo as completed IMMEDIATELY
67
+ 5. Move to next todo and repeat
68
+
69
+ **Todo states:**
70
+ - not-started: Todo not yet begun
71
+ - in-progress: Currently working (limit ONE at a time)
72
+ - completed: Finished successfully
73
+ - failed: Task could not be completed
74
+
75
+ **Important:** Mark todos completed as soon as they are done. Do not batch completions.`;
76
+
77
+ /**
78
+ * JSON Schema for the tool input
79
+ */
80
+ export const TaskProgressToolSchema = {
81
+ type: 'object',
82
+ properties: {
83
+ todoList: {
84
+ type: 'array',
85
+ description:
86
+ 'Complete array of all todo items. Must include ALL items - both existing and new.',
87
+ items: {
88
+ type: 'object',
89
+ properties: {
90
+ id: {
91
+ type: 'number',
92
+ description:
93
+ 'Unique identifier for the todo. Use sequential numbers starting from 1.',
94
+ },
95
+ title: {
96
+ type: 'string',
97
+ description:
98
+ 'Concise action-oriented todo label (3-7 words). Displayed in UI.',
99
+ },
100
+ status: {
101
+ type: 'string',
102
+ enum: ['not-started', 'in-progress', 'completed', 'failed'],
103
+ description:
104
+ 'not-started: Not begun | in-progress: Currently working (max 1) | completed: Fully finished | failed: Could not complete',
105
+ },
106
+ },
107
+ required: ['id', 'title', 'status'],
108
+ },
109
+ },
110
+ },
111
+ required: ['todoList'],
112
+ } as const;
113
+
114
+ /**
115
+ * Tool definition for registration/export
116
+ */
117
+ export const TaskProgressToolDefinition = {
118
+ name: TaskProgressToolName,
119
+ description: TaskProgressToolDescription,
120
+ schema: TaskProgressToolSchema,
121
+ } as const;
122
+
123
+ /**
124
+ * Parameters for creating the TaskProgress tool
125
+ */
126
+ export interface TaskProgressToolParams {
127
+ /**
128
+ * Optional callback to handle task updates externally
129
+ * Called with the task list whenever it's updated
130
+ */
131
+ onTaskUpdate?: (tasks: TaskItem[]) => void | Promise<void>;
132
+ }
133
+
134
+ /**
135
+ * Creates a TaskProgress tool that allows the AI to manage a todo list
136
+ * and emits events for the UI to display progress.
137
+ *
138
+ * This tool is designed for ephemeral agents doing complex, multi-step work.
139
+ * It should NOT be included for:
140
+ * - Persistent workflow agents
141
+ * - Simple tool-calling agents
142
+ * - No-code/low-code builder workflows (unless explicitly enabled)
143
+ *
144
+ * @param params - Optional parameters including external callback
145
+ * @returns DynamicStructuredTool that can be added to agent tools
146
+ *
147
+ * @example
148
+ * ```typescript
149
+ * // Add to agent tools for ephemeral research/planning agents
150
+ * const tools = [
151
+ * createTaskProgressTool(),
152
+ * // ... other tools
153
+ * ];
154
+ * ```
155
+ */
156
+ export function createTaskProgressTool(
157
+ params: TaskProgressToolParams = {}
158
+ ): DynamicStructuredTool {
159
+ const { onTaskUpdate } = params;
160
+
161
+ return tool(
162
+ async (rawInput, config) => {
163
+ const { todoList } = rawInput as { todoList: TaskItem[] };
164
+
165
+ // Validate input
166
+ if (!Array.isArray(todoList)) {
167
+ return 'Error: todoList must be an array of task items.';
168
+ }
169
+
170
+ // Validate each task
171
+ for (const task of todoList) {
172
+ if (typeof task.id !== 'number') {
173
+ return `Error: Task id must be a number. Got: ${typeof task.id}`;
174
+ }
175
+ if (typeof task.title !== 'string' || task.title.trim() === '') {
176
+ return `Error: Task title must be a non-empty string for task ${task.id}`;
177
+ }
178
+ if (
179
+ !['not-started', 'in-progress', 'completed', 'failed'].includes(
180
+ task.status
181
+ )
182
+ ) {
183
+ return `Error: Invalid status "${task.status}" for task ${task.id}. Must be one of: not-started, in-progress, completed, failed`;
184
+ }
185
+ }
186
+
187
+ // Check for multiple in-progress tasks (warning, not error)
188
+ const inProgressCount = todoList.filter(
189
+ (t) => t.status === 'in-progress'
190
+ ).length;
191
+ const warning =
192
+ inProgressCount > 1
193
+ ? `\nWarning: ${inProgressCount} tasks marked as in-progress. Best practice is to work on one task at a time.`
194
+ : '';
195
+
196
+ // Check if all tasks are complete
197
+ const allComplete =
198
+ todoList.length > 0 &&
199
+ todoList.every(
200
+ (t) => t.status === 'completed' || t.status === 'failed'
201
+ );
202
+
203
+ // Create the payload for SSE event
204
+ const payload: TaskProgressPayload = {
205
+ tasks: todoList,
206
+ timestamp: new Date().toISOString(),
207
+ isComplete: allComplete,
208
+ };
209
+
210
+ // Dispatch SSE event for UI to consume
211
+ await safeDispatchCustomEvent(
212
+ GraphEvents.ON_TASK_PROGRESS,
213
+ payload,
214
+ config
215
+ );
216
+
217
+ // Call external callback if provided
218
+ if (onTaskUpdate) {
219
+ try {
220
+ await onTaskUpdate(todoList);
221
+ } catch (e) {
222
+ // Don't fail the tool call if callback fails
223
+ // eslint-disable-next-line no-console
224
+ console.warn('TaskProgress callback error:', e);
225
+ }
226
+ }
227
+
228
+ // Build response message
229
+ const completed = todoList.filter((t) => t.status === 'completed').length;
230
+ const inProgress = todoList.filter(
231
+ (t) => t.status === 'in-progress'
232
+ ).length;
233
+ const notStarted = todoList.filter(
234
+ (t) => t.status === 'not-started'
235
+ ).length;
236
+
237
+ return `Todo list updated successfully. Status: ${completed}/${todoList.length} completed, ${inProgress} in progress, ${notStarted} remaining.${warning}`;
238
+ },
239
+ {
240
+ name: TaskProgressToolName,
241
+ description: TaskProgressToolDescription,
242
+ schema: TaskProgressToolSchema,
243
+ }
244
+ );
245
+ }
246
+
247
+ export default createTaskProgressTool;