@probelabs/probe 0.6.0-rc203 → 0.6.0-rc205
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.
- package/bin/binaries/probe-v0.6.0-rc205-aarch64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc205-aarch64-unknown-linux-musl.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc205-x86_64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc205-x86_64-pc-windows-msvc.zip +0 -0
- package/bin/binaries/probe-v0.6.0-rc205-x86_64-unknown-linux-musl.tar.gz +0 -0
- package/build/agent/ProbeAgent.d.ts +2 -0
- package/build/agent/ProbeAgent.js +233 -40
- package/build/agent/index.js +1566 -84
- package/build/agent/simpleTelemetry.js +12 -0
- package/build/agent/tasks/TaskManager.js +604 -0
- package/build/agent/tasks/index.js +15 -0
- package/build/agent/tasks/taskTool.js +476 -0
- package/build/agent/tools.js +11 -0
- package/build/delegate.js +7 -2
- package/build/index.js +14 -1
- package/build/search.js +19 -5
- package/build/tools/common.js +67 -0
- package/build/tools/vercel.js +28 -12
- package/build/utils/error-types.js +303 -0
- package/build/utils/path-validation.js +21 -3
- package/cjs/agent/ProbeAgent.cjs +8940 -6393
- package/cjs/agent/simpleTelemetry.cjs +10 -0
- package/cjs/index.cjs +8960 -6393
- package/package.json +2 -2
- package/src/agent/ProbeAgent.d.ts +2 -0
- package/src/agent/ProbeAgent.js +233 -40
- package/src/agent/index.js +14 -2
- package/src/agent/simpleTelemetry.js +12 -0
- package/src/agent/tasks/TaskManager.js +604 -0
- package/src/agent/tasks/index.js +15 -0
- package/src/agent/tasks/taskTool.js +476 -0
- package/src/agent/tools.js +11 -0
- package/src/delegate.js +7 -2
- package/src/index.js +14 -1
- package/src/search.js +19 -5
- package/src/tools/common.js +67 -0
- package/src/tools/vercel.js +28 -12
- package/src/utils/error-types.js +303 -0
- package/src/utils/path-validation.js +21 -3
- package/bin/binaries/probe-v0.6.0-rc203-aarch64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc203-aarch64-unknown-linux-musl.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc203-x86_64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc203-x86_64-pc-windows-msvc.zip +0 -0
- package/bin/binaries/probe-v0.6.0-rc203-x86_64-unknown-linux-musl.tar.gz +0 -0
|
@@ -219,6 +219,18 @@ export class SimpleAppTracer {
|
|
|
219
219
|
});
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
+
/**
|
|
223
|
+
* Record task management events
|
|
224
|
+
*/
|
|
225
|
+
recordTaskEvent(eventType, data = {}) {
|
|
226
|
+
if (!this.isEnabled()) return;
|
|
227
|
+
|
|
228
|
+
this.addEvent(`task.${eventType}`, {
|
|
229
|
+
'session.id': this.sessionId,
|
|
230
|
+
...data
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
222
234
|
setAttributes(attributes) {
|
|
223
235
|
// For simplicity, just log attributes when no active span
|
|
224
236
|
if (this.telemetry && this.telemetry.enableConsole) {
|
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TaskManager - Manages tasks for tracking agent progress
|
|
3
|
+
* @module agent/tasks/TaskManager
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {Object} Task
|
|
8
|
+
* @property {string} id - Unique task identifier (e.g., "task-1")
|
|
9
|
+
* @property {string} title - Short task description
|
|
10
|
+
* @property {string} [description] - Detailed task description
|
|
11
|
+
* @property {'pending'|'in_progress'|'completed'|'cancelled'} status - Current task status
|
|
12
|
+
* @property {'low'|'medium'|'high'|'critical'} [priority] - Task priority
|
|
13
|
+
* @property {string[]} dependencies - Array of task IDs that must complete first
|
|
14
|
+
* @property {string} createdAt - ISO timestamp of creation
|
|
15
|
+
* @property {string} updatedAt - ISO timestamp of last update
|
|
16
|
+
* @property {string} [completedAt] - ISO timestamp when completed
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* TaskManager class for managing tasks within a ProbeAgent session
|
|
21
|
+
*/
|
|
22
|
+
export class TaskManager {
|
|
23
|
+
/**
|
|
24
|
+
* Create a new TaskManager instance
|
|
25
|
+
* @param {Object} [options] - Configuration options
|
|
26
|
+
* @param {boolean} [options.debug=false] - Enable debug logging
|
|
27
|
+
*/
|
|
28
|
+
constructor(options = {}) {
|
|
29
|
+
/** @type {Map<string, Task>} */
|
|
30
|
+
this.tasks = new Map();
|
|
31
|
+
this.taskCounter = 0;
|
|
32
|
+
this.debug = options.debug || false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Generate the next task ID
|
|
37
|
+
* @returns {string} New task ID
|
|
38
|
+
* @private
|
|
39
|
+
*/
|
|
40
|
+
_generateId() {
|
|
41
|
+
this.taskCounter++;
|
|
42
|
+
return `task-${this.taskCounter}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get current timestamp in ISO format
|
|
47
|
+
* @returns {string} ISO timestamp
|
|
48
|
+
* @private
|
|
49
|
+
*/
|
|
50
|
+
_now() {
|
|
51
|
+
return new Date().toISOString();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Create a single task
|
|
56
|
+
* @param {Object} taskData - Task data
|
|
57
|
+
* @param {string} taskData.title - Task title
|
|
58
|
+
* @param {string} [taskData.description] - Task description
|
|
59
|
+
* @param {'low'|'medium'|'high'|'critical'} [taskData.priority] - Task priority
|
|
60
|
+
* @param {string[]} [taskData.dependencies] - Task IDs this task depends on
|
|
61
|
+
* @param {string} [taskData.after] - Task ID to insert this task after (for ordering)
|
|
62
|
+
* @returns {Task} Created task
|
|
63
|
+
* @throws {Error} If dependencies are invalid or create a cycle
|
|
64
|
+
*/
|
|
65
|
+
createTask(taskData) {
|
|
66
|
+
const id = this._generateId();
|
|
67
|
+
const now = this._now();
|
|
68
|
+
|
|
69
|
+
// Validate dependencies exist
|
|
70
|
+
const dependencies = taskData.dependencies || [];
|
|
71
|
+
for (const depId of dependencies) {
|
|
72
|
+
if (!this.tasks.has(depId)) {
|
|
73
|
+
throw new Error(`Dependency "${depId}" does not exist. Available tasks: ${this._getAvailableTaskIds()}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Validate 'after' task exists if specified
|
|
78
|
+
const afterTaskId = taskData.after;
|
|
79
|
+
if (afterTaskId && !this.tasks.has(afterTaskId)) {
|
|
80
|
+
throw new Error(`Task "${afterTaskId}" does not exist. Cannot insert after non-existent task. Available tasks: ${this._getAvailableTaskIds()}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Check for circular dependencies
|
|
84
|
+
if (dependencies.length > 0 && !this._validateNoCycle(id, dependencies)) {
|
|
85
|
+
throw new Error(`Adding dependencies [${dependencies.join(', ')}] to "${id}" would create a circular dependency`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const task = {
|
|
89
|
+
id,
|
|
90
|
+
title: taskData.title,
|
|
91
|
+
description: taskData.description || null,
|
|
92
|
+
status: 'pending',
|
|
93
|
+
priority: taskData.priority || null,
|
|
94
|
+
dependencies,
|
|
95
|
+
createdAt: now,
|
|
96
|
+
updatedAt: now,
|
|
97
|
+
completedAt: null
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Insert task at the correct position
|
|
101
|
+
if (afterTaskId) {
|
|
102
|
+
this._insertAfter(afterTaskId, id, task);
|
|
103
|
+
} else {
|
|
104
|
+
this.tasks.set(id, task);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (this.debug) {
|
|
108
|
+
console.log(`[TaskManager] Created task: ${id} - ${task.title}${afterTaskId ? ` (after ${afterTaskId})` : ''}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return task;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Insert a task after a specific task in the Map order
|
|
116
|
+
* @param {string} afterId - Task ID to insert after
|
|
117
|
+
* @param {string} newId - New task ID
|
|
118
|
+
* @param {Task} newTask - New task object
|
|
119
|
+
* @private
|
|
120
|
+
*/
|
|
121
|
+
_insertAfter(afterId, newId, newTask) {
|
|
122
|
+
const newTasks = new Map();
|
|
123
|
+
|
|
124
|
+
for (const [id, task] of this.tasks) {
|
|
125
|
+
newTasks.set(id, task);
|
|
126
|
+
if (id === afterId) {
|
|
127
|
+
newTasks.set(newId, newTask);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
this.tasks = newTasks;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Create multiple tasks in batch
|
|
136
|
+
* @param {Object[]} tasksData - Array of task data objects
|
|
137
|
+
* @returns {Task[]} Created tasks
|
|
138
|
+
*/
|
|
139
|
+
createTasks(tasksData) {
|
|
140
|
+
const createdTasks = [];
|
|
141
|
+
|
|
142
|
+
for (const taskData of tasksData) {
|
|
143
|
+
const task = this.createTask(taskData);
|
|
144
|
+
createdTasks.push(task);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return createdTasks;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Get a task by ID
|
|
152
|
+
* @param {string} id - Task ID
|
|
153
|
+
* @returns {Task|null} Task or null if not found
|
|
154
|
+
*/
|
|
155
|
+
getTask(id) {
|
|
156
|
+
return this.tasks.get(id) || null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Valid status values for tasks
|
|
161
|
+
* @type {string[]}
|
|
162
|
+
* @private
|
|
163
|
+
*/
|
|
164
|
+
static VALID_STATUSES = ['pending', 'in_progress', 'completed', 'cancelled'];
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Valid priority values for tasks
|
|
168
|
+
* @type {string[]}
|
|
169
|
+
* @private
|
|
170
|
+
*/
|
|
171
|
+
static VALID_PRIORITIES = ['low', 'medium', 'high', 'critical'];
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Update a task
|
|
175
|
+
* @param {string} id - Task ID
|
|
176
|
+
* @param {Object} updates - Fields to update
|
|
177
|
+
* @returns {Task} Updated task
|
|
178
|
+
* @throws {Error} If task not found or update is invalid
|
|
179
|
+
*/
|
|
180
|
+
updateTask(id, updates) {
|
|
181
|
+
const task = this.tasks.get(id);
|
|
182
|
+
if (!task) {
|
|
183
|
+
throw new Error(`Task "${id}" not found. Available tasks: ${this._getAvailableTaskIds()}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Validate status if provided
|
|
187
|
+
if (updates.status !== undefined) {
|
|
188
|
+
if (!TaskManager.VALID_STATUSES.includes(updates.status)) {
|
|
189
|
+
throw new Error(`Invalid status "${updates.status}". Valid statuses: ${TaskManager.VALID_STATUSES.join(', ')}`);
|
|
190
|
+
}
|
|
191
|
+
if (updates.status === 'completed' && !task.completedAt) {
|
|
192
|
+
updates.completedAt = this._now();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Validate priority if provided
|
|
197
|
+
if (updates.priority !== undefined && updates.priority !== null) {
|
|
198
|
+
if (!TaskManager.VALID_PRIORITIES.includes(updates.priority)) {
|
|
199
|
+
throw new Error(`Invalid priority "${updates.priority}". Valid priorities: ${TaskManager.VALID_PRIORITIES.join(', ')}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Handle dependency updates
|
|
204
|
+
if (updates.dependencies !== undefined) {
|
|
205
|
+
// Validate dependencies is an array
|
|
206
|
+
if (!Array.isArray(updates.dependencies)) {
|
|
207
|
+
throw new Error('Dependencies must be an array');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Validate new dependencies exist
|
|
211
|
+
for (const depId of updates.dependencies) {
|
|
212
|
+
if (!this.tasks.has(depId)) {
|
|
213
|
+
throw new Error(`Dependency "${depId}" does not exist. Available tasks: ${this._getAvailableTaskIds()}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Check for circular dependencies
|
|
218
|
+
if (!this._validateNoCycle(id, updates.dependencies)) {
|
|
219
|
+
throw new Error(`Adding dependencies [${updates.dependencies.join(', ')}] to "${id}" would create a circular dependency`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Apply updates
|
|
224
|
+
const updatedTask = {
|
|
225
|
+
...task,
|
|
226
|
+
...updates,
|
|
227
|
+
id, // Ensure ID cannot be changed
|
|
228
|
+
createdAt: task.createdAt, // Ensure createdAt cannot be changed
|
|
229
|
+
updatedAt: this._now()
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
this.tasks.set(id, updatedTask);
|
|
233
|
+
|
|
234
|
+
if (this.debug) {
|
|
235
|
+
console.log(`[TaskManager] Updated task: ${id}`, updates);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return updatedTask;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Update multiple tasks in batch
|
|
243
|
+
* @param {Object[]} updates - Array of {id, ...updates} objects
|
|
244
|
+
* @returns {Task[]} Updated tasks
|
|
245
|
+
*/
|
|
246
|
+
updateTasks(updates) {
|
|
247
|
+
const updatedTasks = [];
|
|
248
|
+
|
|
249
|
+
for (const update of updates) {
|
|
250
|
+
const { id, ...taskUpdates } = update;
|
|
251
|
+
const task = this.updateTask(id, taskUpdates);
|
|
252
|
+
updatedTasks.push(task);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return updatedTasks;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Delete a task
|
|
260
|
+
* @param {string} id - Task ID
|
|
261
|
+
* @returns {boolean} True if deleted
|
|
262
|
+
* @throws {Error} If task has dependents
|
|
263
|
+
*/
|
|
264
|
+
deleteTask(id) {
|
|
265
|
+
const task = this.tasks.get(id);
|
|
266
|
+
if (!task) {
|
|
267
|
+
throw new Error(`Task "${id}" not found. Available tasks: ${this._getAvailableTaskIds()}`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Check if other tasks depend on this one
|
|
271
|
+
const dependents = this._getDependents(id);
|
|
272
|
+
if (dependents.length > 0) {
|
|
273
|
+
throw new Error(`Cannot delete "${id}" - other tasks depend on it: ${dependents.join(', ')}`);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
this.tasks.delete(id);
|
|
277
|
+
|
|
278
|
+
if (this.debug) {
|
|
279
|
+
console.log(`[TaskManager] Deleted task: ${id}`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Delete multiple tasks in batch
|
|
287
|
+
* @param {string[]} ids - Task IDs to delete
|
|
288
|
+
* @returns {string[]} Deleted task IDs
|
|
289
|
+
*/
|
|
290
|
+
deleteTasks(ids) {
|
|
291
|
+
const deletedIds = [];
|
|
292
|
+
|
|
293
|
+
for (const id of ids) {
|
|
294
|
+
this.deleteTask(id);
|
|
295
|
+
deletedIds.push(id);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return deletedIds;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Mark a task as completed
|
|
303
|
+
* @param {string} id - Task ID
|
|
304
|
+
* @returns {Task} Updated task
|
|
305
|
+
*/
|
|
306
|
+
completeTask(id) {
|
|
307
|
+
return this.updateTask(id, { status: 'completed' });
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Mark multiple tasks as completed
|
|
312
|
+
* @param {string[]} ids - Task IDs
|
|
313
|
+
* @returns {Task[]} Updated tasks
|
|
314
|
+
*/
|
|
315
|
+
completeTasks(ids) {
|
|
316
|
+
return ids.map(id => this.completeTask(id));
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* List all tasks
|
|
321
|
+
* @param {Object} [filter] - Optional filter
|
|
322
|
+
* @param {'pending'|'in_progress'|'completed'|'cancelled'} [filter.status] - Filter by status
|
|
323
|
+
* @returns {Task[]} Array of tasks
|
|
324
|
+
*/
|
|
325
|
+
listTasks(filter = {}) {
|
|
326
|
+
let tasks = Array.from(this.tasks.values());
|
|
327
|
+
|
|
328
|
+
if (filter.status) {
|
|
329
|
+
tasks = tasks.filter(t => t.status === filter.status);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return tasks;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Check if there are any incomplete tasks (pending or in_progress)
|
|
337
|
+
* @returns {boolean} True if there are incomplete tasks
|
|
338
|
+
*/
|
|
339
|
+
hasIncompleteTasks() {
|
|
340
|
+
for (const task of this.tasks.values()) {
|
|
341
|
+
if (task.status === 'pending' || task.status === 'in_progress') {
|
|
342
|
+
return true;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Get incomplete tasks (pending or in_progress)
|
|
350
|
+
* @returns {Task[]} Array of incomplete tasks
|
|
351
|
+
*/
|
|
352
|
+
getIncompleteTasks() {
|
|
353
|
+
return Array.from(this.tasks.values()).filter(
|
|
354
|
+
t => t.status === 'pending' || t.status === 'in_progress'
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Check if a dependency is resolved (completed or cancelled)
|
|
360
|
+
* @param {string} depId - Dependency task ID
|
|
361
|
+
* @returns {boolean} True if dependency is resolved or doesn't exist
|
|
362
|
+
* @private
|
|
363
|
+
*/
|
|
364
|
+
_isDependencyResolved(depId) {
|
|
365
|
+
const dep = this.tasks.get(depId);
|
|
366
|
+
// If dependency doesn't exist, treat as resolved (shouldn't happen with validation)
|
|
367
|
+
if (!dep) return true;
|
|
368
|
+
return dep.status === 'completed' || dep.status === 'cancelled';
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Get tasks that are ready to start (all dependencies completed)
|
|
373
|
+
* @returns {Task[]} Array of ready tasks
|
|
374
|
+
*/
|
|
375
|
+
getReadyTasks() {
|
|
376
|
+
return Array.from(this.tasks.values()).filter(task => {
|
|
377
|
+
if (task.status !== 'pending') return false;
|
|
378
|
+
return task.dependencies.every(depId => this._isDependencyResolved(depId));
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Get blocked tasks (have incomplete dependencies)
|
|
384
|
+
* @returns {Task[]} Array of blocked tasks
|
|
385
|
+
*/
|
|
386
|
+
getBlockedTasks() {
|
|
387
|
+
return Array.from(this.tasks.values()).filter(task => {
|
|
388
|
+
if (task.status !== 'pending') return false;
|
|
389
|
+
return task.dependencies.some(depId => !this._isDependencyResolved(depId));
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Get human-readable task summary for checkpoint messages
|
|
395
|
+
* @returns {string} Formatted task summary
|
|
396
|
+
*/
|
|
397
|
+
getTaskSummary() {
|
|
398
|
+
const tasks = this.listTasks();
|
|
399
|
+
if (tasks.length === 0) {
|
|
400
|
+
return 'No tasks created.';
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const lines = ['Tasks:'];
|
|
404
|
+
for (const task of tasks) {
|
|
405
|
+
let line = `- [${task.status}] ${task.id}: ${task.title}`;
|
|
406
|
+
|
|
407
|
+
// Add blocking info for pending tasks
|
|
408
|
+
if (task.status === 'pending' && task.dependencies.length > 0) {
|
|
409
|
+
const blockers = task.dependencies.filter(depId => !this._isDependencyResolved(depId));
|
|
410
|
+
if (blockers.length > 0) {
|
|
411
|
+
line += ` (blocked by: ${blockers.join(', ')})`;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
lines.push(line);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return lines.join('\n');
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Escape XML special characters to prevent injection
|
|
423
|
+
* @param {string} str - String to escape
|
|
424
|
+
* @returns {string} Escaped string
|
|
425
|
+
* @private
|
|
426
|
+
*/
|
|
427
|
+
_escapeXml(str) {
|
|
428
|
+
if (typeof str !== 'string') return String(str);
|
|
429
|
+
return str.replace(/[<>&'"]/g, c => ({
|
|
430
|
+
'<': '<',
|
|
431
|
+
'>': '>',
|
|
432
|
+
'&': '&',
|
|
433
|
+
"'": ''',
|
|
434
|
+
'"': '"'
|
|
435
|
+
}[c]));
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Format tasks for inclusion in AI prompts
|
|
440
|
+
* @returns {string} XML-formatted task list
|
|
441
|
+
*/
|
|
442
|
+
formatTasksForPrompt() {
|
|
443
|
+
const tasks = this.listTasks();
|
|
444
|
+
if (tasks.length === 0) {
|
|
445
|
+
return '<task_status>No tasks created.</task_status>';
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const taskLines = tasks.map(task => {
|
|
449
|
+
const blockers = task.dependencies.filter(depId => !this._isDependencyResolved(depId));
|
|
450
|
+
|
|
451
|
+
let line = ` <task id="${this._escapeXml(task.id)}" status="${this._escapeXml(task.status)}"`;
|
|
452
|
+
if (task.priority) line += ` priority="${this._escapeXml(task.priority)}"`;
|
|
453
|
+
if (blockers.length > 0) line += ` blocked_by="${this._escapeXml(blockers.join(','))}"`;
|
|
454
|
+
line += `>${this._escapeXml(task.title)}</task>`;
|
|
455
|
+
|
|
456
|
+
return line;
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
return `<task_status>\n${taskLines.join('\n')}\n</task_status>`;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Clear all tasks
|
|
464
|
+
*/
|
|
465
|
+
clear() {
|
|
466
|
+
this.tasks.clear();
|
|
467
|
+
this.taskCounter = 0;
|
|
468
|
+
|
|
469
|
+
if (this.debug) {
|
|
470
|
+
console.log('[TaskManager] Cleared all tasks');
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Export tasks for persistence
|
|
476
|
+
* @returns {Object} Serializable task data
|
|
477
|
+
*/
|
|
478
|
+
export() {
|
|
479
|
+
return {
|
|
480
|
+
tasks: Array.from(this.tasks.entries()),
|
|
481
|
+
taskCounter: this.taskCounter
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Import tasks from exported data
|
|
487
|
+
* @param {Object} data - Exported task data
|
|
488
|
+
* @throws {Error} If data is invalid or malformed
|
|
489
|
+
*/
|
|
490
|
+
import(data) {
|
|
491
|
+
// Validate data structure to prevent prototype pollution
|
|
492
|
+
if (!data || typeof data !== 'object') {
|
|
493
|
+
throw new Error('Invalid import data: must be an object');
|
|
494
|
+
}
|
|
495
|
+
if (Object.prototype.hasOwnProperty.call(data, '__proto__') ||
|
|
496
|
+
Object.prototype.hasOwnProperty.call(data, 'constructor')) {
|
|
497
|
+
throw new Error('Invalid import data: prototype pollution attempt detected');
|
|
498
|
+
}
|
|
499
|
+
if (!Array.isArray(data.tasks)) {
|
|
500
|
+
throw new Error('Invalid import data: tasks must be an array');
|
|
501
|
+
}
|
|
502
|
+
if (typeof data.taskCounter !== 'number' || !Number.isInteger(data.taskCounter) || data.taskCounter < 0) {
|
|
503
|
+
throw new Error('Invalid import data: taskCounter must be a non-negative integer');
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Validate each task entry
|
|
507
|
+
for (const entry of data.tasks) {
|
|
508
|
+
if (!Array.isArray(entry) || entry.length !== 2) {
|
|
509
|
+
throw new Error('Invalid import data: each task entry must be a [id, task] tuple');
|
|
510
|
+
}
|
|
511
|
+
const [id, task] = entry;
|
|
512
|
+
if (typeof id !== 'string') {
|
|
513
|
+
throw new Error('Invalid import data: task id must be a string');
|
|
514
|
+
}
|
|
515
|
+
if (!task || typeof task !== 'object') {
|
|
516
|
+
throw new Error('Invalid import data: task must be an object');
|
|
517
|
+
}
|
|
518
|
+
if (Object.prototype.hasOwnProperty.call(task, '__proto__') ||
|
|
519
|
+
Object.prototype.hasOwnProperty.call(task, 'constructor')) {
|
|
520
|
+
throw new Error('Invalid import data: prototype pollution attempt detected in task');
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
this.tasks = new Map(data.tasks);
|
|
525
|
+
this.taskCounter = data.taskCounter;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Get list of available task IDs for error messages
|
|
530
|
+
* @returns {string} Comma-separated list of task IDs
|
|
531
|
+
* @private
|
|
532
|
+
*/
|
|
533
|
+
_getAvailableTaskIds() {
|
|
534
|
+
const ids = Array.from(this.tasks.keys());
|
|
535
|
+
return ids.length > 0 ? ids.join(', ') : '(none)';
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Get tasks that depend on a given task
|
|
540
|
+
* @param {string} taskId - Task ID
|
|
541
|
+
* @returns {string[]} Array of dependent task IDs
|
|
542
|
+
* @private
|
|
543
|
+
*/
|
|
544
|
+
_getDependents(taskId) {
|
|
545
|
+
const dependents = [];
|
|
546
|
+
for (const [id, task] of this.tasks) {
|
|
547
|
+
if (task.dependencies.includes(taskId)) {
|
|
548
|
+
dependents.push(id);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return dependents;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Validate that adding dependencies won't create a cycle
|
|
556
|
+
* Uses DFS to detect cycles
|
|
557
|
+
* @param {string} taskId - Task being updated
|
|
558
|
+
* @param {string[]} newDependencies - New dependencies to add
|
|
559
|
+
* @returns {boolean} True if no cycle would be created
|
|
560
|
+
* @private
|
|
561
|
+
*/
|
|
562
|
+
_validateNoCycle(taskId, newDependencies) {
|
|
563
|
+
// Build a temporary dependency graph including the new dependencies
|
|
564
|
+
const graph = new Map();
|
|
565
|
+
|
|
566
|
+
for (const [id, task] of this.tasks) {
|
|
567
|
+
graph.set(id, [...task.dependencies]);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Add or update the target task's dependencies
|
|
571
|
+
graph.set(taskId, newDependencies);
|
|
572
|
+
|
|
573
|
+
// DFS to detect cycles
|
|
574
|
+
const visited = new Set();
|
|
575
|
+
const recursionStack = new Set();
|
|
576
|
+
|
|
577
|
+
const hasCycle = (nodeId) => {
|
|
578
|
+
if (recursionStack.has(nodeId)) {
|
|
579
|
+
return true; // Found a cycle
|
|
580
|
+
}
|
|
581
|
+
if (visited.has(nodeId)) {
|
|
582
|
+
return false; // Already fully explored
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
visited.add(nodeId);
|
|
586
|
+
recursionStack.add(nodeId);
|
|
587
|
+
|
|
588
|
+
const deps = graph.get(nodeId) || [];
|
|
589
|
+
for (const depId of deps) {
|
|
590
|
+
if (hasCycle(depId)) {
|
|
591
|
+
return true;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
recursionStack.delete(nodeId);
|
|
596
|
+
return false;
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
// Check from the task being modified
|
|
600
|
+
return !hasCycle(taskId);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
export default TaskManager;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task Management Module
|
|
3
|
+
* @module agent/tasks
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { TaskManager, default as TaskManagerDefault } from './TaskManager.js';
|
|
7
|
+
export {
|
|
8
|
+
taskSchema,
|
|
9
|
+
taskToolDefinition,
|
|
10
|
+
taskSystemPrompt,
|
|
11
|
+
taskGuidancePrompt,
|
|
12
|
+
createTaskCompletionBlockedMessage,
|
|
13
|
+
createTaskTool,
|
|
14
|
+
default as createTaskToolDefault
|
|
15
|
+
} from './taskTool.js';
|