illuma-agents 1.0.65 → 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 (75) 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/graphs/Graph.cjs +3 -132
  4. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  5. package/dist/cjs/main.cjs +47 -4
  6. package/dist/cjs/main.cjs.map +1 -1
  7. package/dist/cjs/messages/format.cjs +0 -50
  8. package/dist/cjs/messages/format.cjs.map +1 -1
  9. package/dist/cjs/prompts/collab.cjs +11 -0
  10. package/dist/cjs/prompts/collab.cjs.map +1 -0
  11. package/dist/cjs/prompts/taskmanager.cjs +66 -0
  12. package/dist/cjs/prompts/taskmanager.cjs.map +1 -0
  13. package/dist/cjs/prompts/taskplanning.cjs +99 -0
  14. package/dist/cjs/prompts/taskplanning.cjs.map +1 -0
  15. package/dist/cjs/tools/Calculator.cjs +23 -2
  16. package/dist/cjs/tools/Calculator.cjs.map +1 -1
  17. package/dist/cjs/tools/CodeExecutor.cjs +19 -1
  18. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  19. package/dist/cjs/tools/TaskProgress.cjs +172 -0
  20. package/dist/cjs/tools/TaskProgress.cjs.map +1 -0
  21. package/dist/cjs/tools/ToolNode.cjs +2 -1
  22. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  23. package/dist/cjs/tools/search/schema.cjs +45 -0
  24. package/dist/cjs/tools/search/schema.cjs.map +1 -1
  25. package/dist/cjs/tools/search/tool.cjs.map +1 -1
  26. package/dist/esm/common/enum.mjs +2 -0
  27. package/dist/esm/common/enum.mjs.map +1 -1
  28. package/dist/esm/graphs/Graph.mjs +3 -132
  29. package/dist/esm/graphs/Graph.mjs.map +1 -1
  30. package/dist/esm/main.mjs +8 -3
  31. package/dist/esm/main.mjs.map +1 -1
  32. package/dist/esm/messages/format.mjs +2 -51
  33. package/dist/esm/messages/format.mjs.map +1 -1
  34. package/dist/esm/prompts/collab.mjs +9 -0
  35. package/dist/esm/prompts/collab.mjs.map +1 -0
  36. package/dist/esm/prompts/taskmanager.mjs +60 -0
  37. package/dist/esm/prompts/taskmanager.mjs.map +1 -0
  38. package/dist/esm/prompts/taskplanning.mjs +94 -0
  39. package/dist/esm/prompts/taskplanning.mjs.map +1 -0
  40. package/dist/esm/tools/Calculator.mjs +20 -3
  41. package/dist/esm/tools/Calculator.mjs.map +1 -1
  42. package/dist/esm/tools/CodeExecutor.mjs +16 -2
  43. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  44. package/dist/esm/tools/TaskProgress.mjs +163 -0
  45. package/dist/esm/tools/TaskProgress.mjs.map +1 -0
  46. package/dist/esm/tools/ToolNode.mjs +2 -1
  47. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  48. package/dist/esm/tools/search/schema.mjs +42 -1
  49. package/dist/esm/tools/search/schema.mjs.map +1 -1
  50. package/dist/esm/tools/search/tool.mjs.map +1 -1
  51. package/dist/types/common/enum.d.ts +2 -0
  52. package/dist/types/index.d.ts +2 -0
  53. package/dist/types/messages/format.d.ts +0 -9
  54. package/dist/types/prompts/index.d.ts +1 -0
  55. package/dist/types/prompts/taskplanning.d.ts +54 -0
  56. package/dist/types/tools/Calculator.d.ts +26 -0
  57. package/dist/types/tools/CodeExecutor.d.ts +51 -0
  58. package/dist/types/tools/TaskProgress.d.ts +142 -0
  59. package/dist/types/tools/search/index.d.ts +1 -0
  60. package/dist/types/tools/search/schema.d.ts +69 -0
  61. package/package.json +1 -1
  62. package/src/common/enum.ts +2 -0
  63. package/src/graphs/Graph.ts +5 -160
  64. package/src/index.ts +4 -0
  65. package/src/messages/format.ts +0 -60
  66. package/src/prompts/index.ts +2 -1
  67. package/src/prompts/taskplanning.ts +96 -0
  68. package/src/specs/task-progress.test.ts +330 -0
  69. package/src/tools/Calculator.ts +24 -3
  70. package/src/tools/CodeExecutor.ts +19 -2
  71. package/src/tools/TaskProgress.ts +247 -0
  72. package/src/tools/ToolNode.ts +2 -1
  73. package/src/tools/search/index.ts +1 -0
  74. package/src/tools/search/schema.ts +45 -0
  75. package/src/tools/search/tool.ts +3 -3
@@ -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
+ });
@@ -1,6 +1,28 @@
1
1
  import { Tool } from '@langchain/core/tools';
2
2
  import * as math from 'mathjs';
3
3
 
4
+ export const CalculatorToolName = 'calculator';
5
+
6
+ export const CalculatorToolDescription =
7
+ 'Useful for getting the result of a math expression. The input to this tool should be a valid mathematical expression that could be executed by a simple calculator.';
8
+
9
+ export const CalculatorSchema = {
10
+ type: 'object',
11
+ properties: {
12
+ input: {
13
+ type: 'string',
14
+ description: 'A valid mathematical expression to evaluate',
15
+ },
16
+ },
17
+ required: ['input'],
18
+ } as const;
19
+
20
+ export const CalculatorToolDefinition = {
21
+ name: CalculatorToolName,
22
+ description: CalculatorToolDescription,
23
+ schema: CalculatorSchema,
24
+ } as const;
25
+
4
26
  export class Calculator extends Tool {
5
27
  static lc_name(): string {
6
28
  return 'Calculator';
@@ -10,7 +32,7 @@ export class Calculator extends Tool {
10
32
  return [...super.lc_namespace, 'calculator'];
11
33
  }
12
34
 
13
- name = 'calculator';
35
+ name = CalculatorToolName;
14
36
 
15
37
  async _call(input: string): Promise<string> {
16
38
  try {
@@ -20,6 +42,5 @@ export class Calculator extends Tool {
20
42
  }
21
43
  }
22
44
 
23
- description =
24
- 'Useful for getting the result of a math expression. The input to this tool should be a valid mathematical expression that could be executed by a simple calculator.';
45
+ description = CalculatorToolDescription;
25
46
  }
@@ -35,7 +35,7 @@ const SUPPORTED_LANGUAGES = [
35
35
  'r',
36
36
  ] as const;
37
37
 
38
- const CodeExecutionToolSchema = {
38
+ export const CodeExecutionToolSchema = {
39
39
  type: 'object',
40
40
  properties: {
41
41
  lang: {
@@ -74,6 +74,23 @@ const EXEC_ENDPOINT = `${baseEndpoint}/exec`;
74
74
 
75
75
  type SupportedLanguage = (typeof SUPPORTED_LANGUAGES)[number];
76
76
 
77
+ export const CodeExecutionToolDescription = `
78
+ Runs code and returns stdout/stderr output from a stateless execution environment, similar to running scripts in a command-line interface. Each execution is isolated and independent.
79
+
80
+ Usage:
81
+ - No network access available.
82
+ - Generated files are automatically delivered; **DO NOT** provide download links.
83
+ - NEVER use this tool to execute malicious code.
84
+ `.trim();
85
+
86
+ export const CodeExecutionToolName = Constants.EXECUTE_CODE;
87
+
88
+ export const CodeExecutionToolDefinition = {
89
+ name: CodeExecutionToolName,
90
+ description: CodeExecutionToolDescription,
91
+ schema: CodeExecutionToolSchema,
92
+ } as const;
93
+
77
94
  function createCodeExecutionTool(
78
95
  params: t.CodeExecutionToolParams = {}
79
96
  ): DynamicStructuredTool {
@@ -242,7 +259,7 @@ Rules:
242
259
  }
243
260
  },
244
261
  {
245
- name: Constants.EXECUTE_CODE,
262
+ name: CodeExecutionToolName,
246
263
  description,
247
264
  schema: CodeExecutionToolSchema,
248
265
  responseFormat: Constants.CONTENT_AND_ARTIFACT,
@@ -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;
@@ -392,8 +392,9 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
392
392
  : JSON.stringify(result.content);
393
393
  toolMessage = new ToolMessage({
394
394
  status: 'success',
395
- content: contentString,
396
395
  name: toolName,
396
+ content: contentString,
397
+ artifact: result.artifact,
397
398
  tool_call_id: result.toolCallId,
398
399
  });
399
400
  }
@@ -1,2 +1,3 @@
1
1
  export * from './tool';
2
+ export * from './schema';
2
3
  export type * from './types';