@finityno/claude-code-acp 0.14.0 → 0.16.0
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/dist/acp-agent.d.ts +11 -0
- package/dist/acp-agent.d.ts.map +1 -1
- package/dist/acp-agent.js +131 -6
- package/dist/lib.d.ts +5 -1
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js +7 -0
- package/dist/mcp-server.d.ts +2 -1
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +14 -1
- package/dist/subagent-tracker.d.ts +102 -2
- package/dist/subagent-tracker.d.ts.map +1 -1
- package/dist/subagent-tracker.js +189 -22
- package/dist/task-manager.d.ts +106 -0
- package/dist/task-manager.d.ts.map +1 -0
- package/dist/task-manager.js +231 -0
- package/dist/task-mcp-tools.d.ts +16 -0
- package/dist/task-mcp-tools.d.ts.map +1 -0
- package/dist/task-mcp-tools.js +256 -0
- package/dist/task-store.d.ts +123 -0
- package/dist/task-store.d.ts.map +1 -0
- package/dist/task-store.js +292 -0
- package/dist/work-item-mcp-tools.d.ts +12 -0
- package/dist/work-item-mcp-tools.d.ts.map +1 -0
- package/dist/work-item-mcp-tools.js +296 -0
- package/package.json +2 -2
package/dist/subagent-tracker.js
CHANGED
|
@@ -7,6 +7,8 @@ export class SubagentTracker {
|
|
|
7
7
|
this.subagents = new Map();
|
|
8
8
|
/** Map of session ID to subagent IDs in that session */
|
|
9
9
|
this.sessionSubagents = new Map();
|
|
10
|
+
/** Map of agent ID to subagent ID (for resume lookups) */
|
|
11
|
+
this.agentIdToSubagent = new Map();
|
|
10
12
|
/** Event listeners for subagent lifecycle events */
|
|
11
13
|
this.listeners = new Map();
|
|
12
14
|
this.client = client;
|
|
@@ -16,6 +18,11 @@ export class SubagentTracker {
|
|
|
16
18
|
* Track a new subagent when Task tool is called
|
|
17
19
|
*/
|
|
18
20
|
trackSubagent(toolUseId, sessionId, input, parentToolUseId) {
|
|
21
|
+
// Check if this is a resume operation
|
|
22
|
+
const isResumed = !!input.resume;
|
|
23
|
+
const originalTaskId = input.resume
|
|
24
|
+
? this.agentIdToSubagent.get(input.resume)
|
|
25
|
+
: undefined;
|
|
19
26
|
const subagent = {
|
|
20
27
|
id: toolUseId,
|
|
21
28
|
parentSessionId: sessionId,
|
|
@@ -28,6 +35,12 @@ export class SubagentTracker {
|
|
|
28
35
|
createdAt: Date.now(),
|
|
29
36
|
runInBackground: input.run_in_background ?? false,
|
|
30
37
|
maxTurns: input.max_turns,
|
|
38
|
+
// SDK 0.2.17 fields
|
|
39
|
+
agentName: input.name,
|
|
40
|
+
teamName: input.team_name,
|
|
41
|
+
permissionMode: input.mode,
|
|
42
|
+
isResumed,
|
|
43
|
+
originalTaskId,
|
|
31
44
|
};
|
|
32
45
|
this.subagents.set(toolUseId, subagent);
|
|
33
46
|
// Track by session
|
|
@@ -35,7 +48,7 @@ export class SubagentTracker {
|
|
|
35
48
|
this.sessionSubagents.set(sessionId, new Set());
|
|
36
49
|
}
|
|
37
50
|
this.sessionSubagents.get(sessionId).add(toolUseId);
|
|
38
|
-
this.logger.log(`[SubagentTracker] Tracked new subagent: ${toolUseId} (${input.subagent_type}: ${input.description})`);
|
|
51
|
+
this.logger.log(`[SubagentTracker] Tracked new subagent: ${toolUseId} (${input.subagent_type}: ${input.description})${isResumed ? " [RESUMED]" : ""}`);
|
|
39
52
|
return subagent;
|
|
40
53
|
}
|
|
41
54
|
/**
|
|
@@ -66,10 +79,12 @@ export class SubagentTracker {
|
|
|
66
79
|
subagent.result = result;
|
|
67
80
|
if (agentId) {
|
|
68
81
|
subagent.agentId = agentId;
|
|
82
|
+
// Track agent ID for resume lookups
|
|
83
|
+
this.agentIdToSubagent.set(agentId, toolUseId);
|
|
69
84
|
}
|
|
70
85
|
await this.emitEvent("subagent_completed", subagent);
|
|
71
86
|
await this.sendSubagentNotification(subagent, "subagent_completed");
|
|
72
|
-
this.logger.log(`[SubagentTracker] Subagent completed: ${toolUseId} (duration: ${this.getDuration(subagent)}ms)`);
|
|
87
|
+
this.logger.log(`[SubagentTracker] Subagent completed: ${toolUseId} (duration: ${this.getDuration(subagent)}ms)${agentId ? ` [agentId: ${agentId}]` : ""}`);
|
|
73
88
|
}
|
|
74
89
|
/**
|
|
75
90
|
* Mark a subagent as failed
|
|
@@ -101,6 +116,40 @@ export class SubagentTracker {
|
|
|
101
116
|
await this.sendSubagentNotification(subagent, "subagent_cancelled");
|
|
102
117
|
this.logger.log(`[SubagentTracker] Subagent cancelled: ${toolUseId}`);
|
|
103
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Handle SDKTaskNotificationMessage from the Claude Agent SDK.
|
|
121
|
+
* This is called when a background task completes/fails/stops.
|
|
122
|
+
*/
|
|
123
|
+
async handleTaskNotification(notification) {
|
|
124
|
+
const subagent = this.subagents.get(notification.task_id);
|
|
125
|
+
if (!subagent) {
|
|
126
|
+
this.logger.error(`[SubagentTracker] Received task notification for unknown subagent: ${notification.task_id}`);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
// Update subagent with notification data
|
|
130
|
+
subagent.outputFile = notification.output_file;
|
|
131
|
+
subagent.summary = notification.summary;
|
|
132
|
+
subagent.completedAt = Date.now();
|
|
133
|
+
// Map SDK status to our status
|
|
134
|
+
switch (notification.status) {
|
|
135
|
+
case "completed":
|
|
136
|
+
subagent.status = "completed";
|
|
137
|
+
await this.emitEvent("subagent_completed", subagent);
|
|
138
|
+
await this.sendSubagentNotification(subagent, "subagent_completed", notification);
|
|
139
|
+
break;
|
|
140
|
+
case "failed":
|
|
141
|
+
subagent.status = "failed";
|
|
142
|
+
await this.emitEvent("subagent_failed", subagent);
|
|
143
|
+
await this.sendSubagentNotification(subagent, "subagent_failed", notification);
|
|
144
|
+
break;
|
|
145
|
+
case "stopped":
|
|
146
|
+
subagent.status = "stopped";
|
|
147
|
+
await this.emitEvent("subagent_stopped", subagent);
|
|
148
|
+
await this.sendSubagentNotification(subagent, "subagent_stopped", notification);
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
this.logger.log(`[SubagentTracker] Task notification: ${notification.task_id} -> ${notification.status}`);
|
|
152
|
+
}
|
|
104
153
|
/**
|
|
105
154
|
* Send progress update for a running subagent
|
|
106
155
|
*/
|
|
@@ -162,6 +211,22 @@ export class SubagentTracker {
|
|
|
162
211
|
getAllSubagents() {
|
|
163
212
|
return Array.from(this.subagents.values());
|
|
164
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* Find a subagent by its agent ID (for resume operations)
|
|
216
|
+
*/
|
|
217
|
+
findByAgentId(agentId) {
|
|
218
|
+
const subagentId = this.agentIdToSubagent.get(agentId);
|
|
219
|
+
if (!subagentId)
|
|
220
|
+
return undefined;
|
|
221
|
+
return this.subagents.get(subagentId);
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Get tasks that can be resumed (have agentId and are completed/failed/stopped)
|
|
225
|
+
*/
|
|
226
|
+
getResumableTasks() {
|
|
227
|
+
return Array.from(this.subagents.values()).filter((s) => s.agentId &&
|
|
228
|
+
(s.status === "completed" || s.status === "failed" || s.status === "stopped"));
|
|
229
|
+
}
|
|
165
230
|
/**
|
|
166
231
|
* Check if a tool use ID is a Task tool (subagent)
|
|
167
232
|
*/
|
|
@@ -192,16 +257,102 @@ export class SubagentTracker {
|
|
|
192
257
|
for (const [id, subagent] of this.subagents) {
|
|
193
258
|
if ((subagent.status === "completed" ||
|
|
194
259
|
subagent.status === "failed" ||
|
|
195
|
-
subagent.status === "cancelled"
|
|
260
|
+
subagent.status === "cancelled" ||
|
|
261
|
+
subagent.status === "stopped") &&
|
|
196
262
|
subagent.completedAt &&
|
|
197
263
|
now - subagent.completedAt > maxAgeMs) {
|
|
198
264
|
this.subagents.delete(id);
|
|
199
265
|
this.sessionSubagents.get(subagent.parentSessionId)?.delete(id);
|
|
266
|
+
if (subagent.agentId) {
|
|
267
|
+
this.agentIdToSubagent.delete(subagent.agentId);
|
|
268
|
+
}
|
|
200
269
|
cleanedCount++;
|
|
201
270
|
}
|
|
202
271
|
}
|
|
203
272
|
return cleanedCount;
|
|
204
273
|
}
|
|
274
|
+
/**
|
|
275
|
+
* Export current state for persistence
|
|
276
|
+
*/
|
|
277
|
+
exportState() {
|
|
278
|
+
const tasks = Array.from(this.subagents.values()).map((s) => ({
|
|
279
|
+
id: s.id,
|
|
280
|
+
parentSessionId: s.parentSessionId,
|
|
281
|
+
parentToolUseId: s.parentToolUseId,
|
|
282
|
+
subagentType: s.subagentType,
|
|
283
|
+
description: s.description,
|
|
284
|
+
prompt: s.prompt,
|
|
285
|
+
model: s.model,
|
|
286
|
+
status: s.status,
|
|
287
|
+
createdAt: s.createdAt,
|
|
288
|
+
startedAt: s.startedAt,
|
|
289
|
+
completedAt: s.completedAt,
|
|
290
|
+
result: s.result,
|
|
291
|
+
error: s.error,
|
|
292
|
+
runInBackground: s.runInBackground,
|
|
293
|
+
maxTurns: s.maxTurns,
|
|
294
|
+
agentId: s.agentId,
|
|
295
|
+
outputFile: s.outputFile,
|
|
296
|
+
summary: s.summary,
|
|
297
|
+
agentName: s.agentName,
|
|
298
|
+
teamName: s.teamName,
|
|
299
|
+
permissionMode: s.permissionMode,
|
|
300
|
+
isResumed: s.isResumed,
|
|
301
|
+
originalTaskId: s.originalTaskId,
|
|
302
|
+
}));
|
|
303
|
+
return {
|
|
304
|
+
version: 1,
|
|
305
|
+
tasks,
|
|
306
|
+
lastUpdated: Date.now(),
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Import state from persistence
|
|
311
|
+
*/
|
|
312
|
+
importState(state) {
|
|
313
|
+
if (state.version !== 1) {
|
|
314
|
+
this.logger.error(`[SubagentTracker] Unknown state version: ${state.version}`);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
for (const task of state.tasks) {
|
|
318
|
+
const subagent = {
|
|
319
|
+
id: task.id,
|
|
320
|
+
parentSessionId: task.parentSessionId,
|
|
321
|
+
parentToolUseId: task.parentToolUseId,
|
|
322
|
+
subagentType: task.subagentType,
|
|
323
|
+
description: task.description,
|
|
324
|
+
prompt: task.prompt,
|
|
325
|
+
model: task.model,
|
|
326
|
+
status: task.status,
|
|
327
|
+
createdAt: task.createdAt,
|
|
328
|
+
startedAt: task.startedAt,
|
|
329
|
+
completedAt: task.completedAt,
|
|
330
|
+
result: task.result,
|
|
331
|
+
error: task.error,
|
|
332
|
+
runInBackground: task.runInBackground,
|
|
333
|
+
maxTurns: task.maxTurns,
|
|
334
|
+
agentId: task.agentId,
|
|
335
|
+
outputFile: task.outputFile,
|
|
336
|
+
summary: task.summary,
|
|
337
|
+
agentName: task.agentName,
|
|
338
|
+
teamName: task.teamName,
|
|
339
|
+
permissionMode: task.permissionMode,
|
|
340
|
+
isResumed: task.isResumed,
|
|
341
|
+
originalTaskId: task.originalTaskId,
|
|
342
|
+
};
|
|
343
|
+
this.subagents.set(task.id, subagent);
|
|
344
|
+
// Rebuild session index
|
|
345
|
+
if (!this.sessionSubagents.has(task.parentSessionId)) {
|
|
346
|
+
this.sessionSubagents.set(task.parentSessionId, new Set());
|
|
347
|
+
}
|
|
348
|
+
this.sessionSubagents.get(task.parentSessionId).add(task.id);
|
|
349
|
+
// Rebuild agent ID index
|
|
350
|
+
if (task.agentId) {
|
|
351
|
+
this.agentIdToSubagent.set(task.agentId, task.id);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
this.logger.log(`[SubagentTracker] Imported ${state.tasks.length} tasks from persistence`);
|
|
355
|
+
}
|
|
205
356
|
/**
|
|
206
357
|
* Get statistics about subagents
|
|
207
358
|
*/
|
|
@@ -214,6 +365,7 @@ export class SubagentTracker {
|
|
|
214
365
|
completed: subagents.filter((s) => s.status === "completed").length,
|
|
215
366
|
failed: subagents.filter((s) => s.status === "failed").length,
|
|
216
367
|
cancelled: subagents.filter((s) => s.status === "cancelled").length,
|
|
368
|
+
stopped: subagents.filter((s) => s.status === "stopped").length,
|
|
217
369
|
byType: this.countByType(subagents),
|
|
218
370
|
averageDurationMs: this.calculateAverageDuration(subagents),
|
|
219
371
|
};
|
|
@@ -224,6 +376,7 @@ export class SubagentTracker {
|
|
|
224
376
|
clear() {
|
|
225
377
|
this.subagents.clear();
|
|
226
378
|
this.sessionSubagents.clear();
|
|
379
|
+
this.agentIdToSubagent.clear();
|
|
227
380
|
}
|
|
228
381
|
// Private helper methods
|
|
229
382
|
async emitEvent(event, subagent, data) {
|
|
@@ -239,31 +392,43 @@ export class SubagentTracker {
|
|
|
239
392
|
}
|
|
240
393
|
}
|
|
241
394
|
}
|
|
242
|
-
async sendSubagentNotification(subagent, eventType) {
|
|
395
|
+
async sendSubagentNotification(subagent, eventType, taskNotification) {
|
|
243
396
|
if (!this.client)
|
|
244
397
|
return;
|
|
398
|
+
const meta = {
|
|
399
|
+
claudeCode: {
|
|
400
|
+
subagent: {
|
|
401
|
+
id: subagent.id,
|
|
402
|
+
eventType,
|
|
403
|
+
subagentType: subagent.subagentType,
|
|
404
|
+
description: subagent.description,
|
|
405
|
+
status: subagent.status,
|
|
406
|
+
parentSessionId: subagent.parentSessionId,
|
|
407
|
+
parentToolUseId: subagent.parentToolUseId,
|
|
408
|
+
model: subagent.model,
|
|
409
|
+
runInBackground: subagent.runInBackground,
|
|
410
|
+
agentId: subagent.agentId,
|
|
411
|
+
durationMs: this.getDuration(subagent),
|
|
412
|
+
outputFile: subagent.outputFile,
|
|
413
|
+
summary: subagent.summary,
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
};
|
|
417
|
+
// Include task notification data if available
|
|
418
|
+
if (taskNotification) {
|
|
419
|
+
meta.claudeCode.taskNotification = {
|
|
420
|
+
taskId: taskNotification.task_id,
|
|
421
|
+
status: taskNotification.status,
|
|
422
|
+
outputFile: taskNotification.output_file,
|
|
423
|
+
summary: taskNotification.summary,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
245
426
|
const notification = {
|
|
246
427
|
sessionId: subagent.parentSessionId,
|
|
247
428
|
update: {
|
|
248
429
|
sessionUpdate: "tool_call_update",
|
|
249
430
|
toolCallId: subagent.id,
|
|
250
|
-
_meta:
|
|
251
|
-
claudeCode: {
|
|
252
|
-
subagent: {
|
|
253
|
-
id: subagent.id,
|
|
254
|
-
eventType,
|
|
255
|
-
subagentType: subagent.subagentType,
|
|
256
|
-
description: subagent.description,
|
|
257
|
-
status: subagent.status,
|
|
258
|
-
parentSessionId: subagent.parentSessionId,
|
|
259
|
-
parentToolUseId: subagent.parentToolUseId,
|
|
260
|
-
model: subagent.model,
|
|
261
|
-
runInBackground: subagent.runInBackground,
|
|
262
|
-
agentId: subagent.agentId,
|
|
263
|
-
durationMs: this.getDuration(subagent),
|
|
264
|
-
},
|
|
265
|
-
},
|
|
266
|
-
},
|
|
431
|
+
_meta: meta,
|
|
267
432
|
},
|
|
268
433
|
};
|
|
269
434
|
await this.client.sessionUpdate(notification);
|
|
@@ -282,7 +447,9 @@ export class SubagentTracker {
|
|
|
282
447
|
return counts;
|
|
283
448
|
}
|
|
284
449
|
calculateAverageDuration(subagents) {
|
|
285
|
-
const completed = subagents.filter((s) => (s.status === "completed" || s.status === "failed"
|
|
450
|
+
const completed = subagents.filter((s) => (s.status === "completed" || s.status === "failed" || s.status === "stopped") &&
|
|
451
|
+
s.startedAt &&
|
|
452
|
+
s.completedAt);
|
|
286
453
|
if (completed.length === 0)
|
|
287
454
|
return undefined;
|
|
288
455
|
const totalDuration = completed.reduce((sum, s) => sum + (s.completedAt - s.startedAt), 0);
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { TrackedSubagent, SubagentTracker, SubagentStatus } from "./subagent-tracker.js";
|
|
2
|
+
import { Logger } from "./acp-agent.js";
|
|
3
|
+
export interface TaskManagerOptions {
|
|
4
|
+
/** Path to persistence file. Defaults to ~/.claude/acp-task-state.json */
|
|
5
|
+
persistencePath?: string;
|
|
6
|
+
/** Logger for output. Defaults to console */
|
|
7
|
+
logger?: Logger;
|
|
8
|
+
/** Enable auto-save. Defaults to true */
|
|
9
|
+
autoSave?: boolean;
|
|
10
|
+
/** Auto-save interval in milliseconds. Defaults to 30000 (30 seconds) */
|
|
11
|
+
autoSaveIntervalMs?: number;
|
|
12
|
+
/** Maximum age of tasks to keep in milliseconds. Defaults to 24 hours */
|
|
13
|
+
maxTaskAgeMs?: number;
|
|
14
|
+
}
|
|
15
|
+
export interface TaskFilter {
|
|
16
|
+
/** Filter by task status */
|
|
17
|
+
status?: SubagentStatus[];
|
|
18
|
+
/** Filter by session ID */
|
|
19
|
+
sessionId?: string;
|
|
20
|
+
/** Filter by background execution */
|
|
21
|
+
runInBackground?: boolean;
|
|
22
|
+
/** Filter by subagent type */
|
|
23
|
+
subagentType?: string;
|
|
24
|
+
/** Only include tasks newer than this timestamp */
|
|
25
|
+
newerThan?: number;
|
|
26
|
+
/** Only include tasks older than this timestamp */
|
|
27
|
+
olderThan?: number;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* TaskManager handles cross-session task persistence and management.
|
|
31
|
+
*
|
|
32
|
+
* It wraps SubagentTracker and provides:
|
|
33
|
+
* - Persistence to disk for background tasks
|
|
34
|
+
* - Cross-session task querying
|
|
35
|
+
* - Resume capability tracking
|
|
36
|
+
* - Task output file reading
|
|
37
|
+
*/
|
|
38
|
+
export declare class TaskManager {
|
|
39
|
+
private tracker;
|
|
40
|
+
private persistencePath;
|
|
41
|
+
private logger;
|
|
42
|
+
private autoSaveInterval?;
|
|
43
|
+
private maxTaskAgeMs;
|
|
44
|
+
private isDirty;
|
|
45
|
+
constructor(tracker: SubagentTracker, options?: TaskManagerOptions);
|
|
46
|
+
/**
|
|
47
|
+
* Load persisted task state from disk
|
|
48
|
+
*/
|
|
49
|
+
loadState(): Promise<void>;
|
|
50
|
+
/**
|
|
51
|
+
* Save current task state to disk
|
|
52
|
+
*/
|
|
53
|
+
saveState(): Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* Force save state immediately
|
|
56
|
+
*/
|
|
57
|
+
forceSave(): Promise<void>;
|
|
58
|
+
/**
|
|
59
|
+
* Get all tasks across all sessions with optional filtering
|
|
60
|
+
*/
|
|
61
|
+
getAllTasks(filter?: TaskFilter): TrackedSubagent[];
|
|
62
|
+
/**
|
|
63
|
+
* Get tasks that can be resumed
|
|
64
|
+
*/
|
|
65
|
+
getResumableTasks(): TrackedSubagent[];
|
|
66
|
+
/**
|
|
67
|
+
* Get a task by ID
|
|
68
|
+
*/
|
|
69
|
+
getTask(taskId: string): TrackedSubagent | undefined;
|
|
70
|
+
/**
|
|
71
|
+
* Get a task by agent ID (for resume operations)
|
|
72
|
+
*/
|
|
73
|
+
getTaskByAgentId(agentId: string): TrackedSubagent | undefined;
|
|
74
|
+
/**
|
|
75
|
+
* Read output file content for a background task
|
|
76
|
+
*/
|
|
77
|
+
getTaskOutput(taskId: string): Promise<string | null>;
|
|
78
|
+
/**
|
|
79
|
+
* Read last N lines of output file for a background task
|
|
80
|
+
*/
|
|
81
|
+
getTaskOutputTail(taskId: string, lines: number): Promise<string | null>;
|
|
82
|
+
/**
|
|
83
|
+
* Cancel a running task
|
|
84
|
+
*/
|
|
85
|
+
cancelTask(taskId: string): Promise<boolean>;
|
|
86
|
+
/**
|
|
87
|
+
* Get statistics about all tasks
|
|
88
|
+
*/
|
|
89
|
+
getStats(): import("./subagent-tracker.js").SubagentStats;
|
|
90
|
+
/**
|
|
91
|
+
* Get the underlying tracker
|
|
92
|
+
*/
|
|
93
|
+
getTracker(): SubagentTracker;
|
|
94
|
+
/**
|
|
95
|
+
* Clean up old completed tasks
|
|
96
|
+
*/
|
|
97
|
+
cleanup(): Promise<number>;
|
|
98
|
+
/**
|
|
99
|
+
* Dispose of the task manager
|
|
100
|
+
*/
|
|
101
|
+
dispose(): void;
|
|
102
|
+
private setupEventListeners;
|
|
103
|
+
private startAutoSave;
|
|
104
|
+
}
|
|
105
|
+
export type { SerializedTrackerState } from "./subagent-tracker.js";
|
|
106
|
+
//# sourceMappingURL=task-manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"task-manager.d.ts","sourceRoot":"","sources":["../src/task-manager.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,eAAe,EACf,eAAe,EACf,cAAc,EAEf,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAKxC,MAAM,WAAW,kBAAkB;IACjC,0EAA0E;IAC1E,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,6CAA6C;IAC7C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,yCAAyC;IACzC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,yEAAyE;IACzE,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,yEAAyE;IACzE,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,UAAU;IACzB,4BAA4B;IAC5B,MAAM,CAAC,EAAE,cAAc,EAAE,CAAC;IAC1B,2BAA2B;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qCAAqC;IACrC,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,8BAA8B;IAC9B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mDAAmD;IACnD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mDAAmD;IACnD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;GAQG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,OAAO,CAAkB;IACjC,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,gBAAgB,CAAC,CAAiB;IAC1C,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,OAAO,CAAkB;gBAErB,OAAO,EAAE,eAAe,EAAE,OAAO,CAAC,EAAE,kBAAkB;IAgBlE;;OAEG;IACG,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;IAyChC;;OAEG;IACG,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;IAwBhC;;OAEG;IACG,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;IAKhC;;OAEG;IACH,WAAW,CAAC,MAAM,CAAC,EAAE,UAAU,GAAG,eAAe,EAAE;IA0BnD;;OAEG;IACH,iBAAiB,IAAI,eAAe,EAAE;IAItC;;OAEG;IACH,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS;IAIpD;;OAEG;IACH,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS;IAI9D;;OAEG;IACG,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAiB3D;;OAEG;IACG,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAU9E;;OAEG;IACG,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAYlD;;OAEG;IACH,QAAQ;IAIR;;OAEG;IACH,UAAU,IAAI,eAAe;IAI7B;;OAEG;IACG,OAAO,IAAI,OAAO,CAAC,MAAM,CAAC;IAShC;;OAEG;IACH,OAAO,IAAI,IAAI;IAYf,OAAO,CAAC,mBAAmB;IAa3B,OAAO,CAAC,aAAa;CAYtB;AAGD,YAAY,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC"}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
/**
|
|
5
|
+
* TaskManager handles cross-session task persistence and management.
|
|
6
|
+
*
|
|
7
|
+
* It wraps SubagentTracker and provides:
|
|
8
|
+
* - Persistence to disk for background tasks
|
|
9
|
+
* - Cross-session task querying
|
|
10
|
+
* - Resume capability tracking
|
|
11
|
+
* - Task output file reading
|
|
12
|
+
*/
|
|
13
|
+
export class TaskManager {
|
|
14
|
+
constructor(tracker, options) {
|
|
15
|
+
this.isDirty = false;
|
|
16
|
+
this.tracker = tracker;
|
|
17
|
+
this.persistencePath =
|
|
18
|
+
options?.persistencePath ??
|
|
19
|
+
path.join(os.homedir(), ".claude", "acp-task-state.json");
|
|
20
|
+
this.logger = options?.logger ?? console;
|
|
21
|
+
this.maxTaskAgeMs = options?.maxTaskAgeMs ?? 24 * 60 * 60 * 1000; // 24 hours
|
|
22
|
+
// Set up event listeners to mark state as dirty
|
|
23
|
+
this.setupEventListeners();
|
|
24
|
+
if (options?.autoSave !== false) {
|
|
25
|
+
this.startAutoSave(options?.autoSaveIntervalMs ?? 30000);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Load persisted task state from disk
|
|
30
|
+
*/
|
|
31
|
+
async loadState() {
|
|
32
|
+
try {
|
|
33
|
+
if (fs.existsSync(this.persistencePath)) {
|
|
34
|
+
const content = await fs.promises.readFile(this.persistencePath, "utf-8");
|
|
35
|
+
const state = JSON.parse(content);
|
|
36
|
+
// Validate state version
|
|
37
|
+
if (state.version !== 1) {
|
|
38
|
+
this.logger.error(`[TaskManager] Unknown state version: ${state.version}, skipping load`);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
// Filter out tasks older than maxTaskAgeMs
|
|
42
|
+
const cutoff = Date.now() - this.maxTaskAgeMs;
|
|
43
|
+
const filteredTasks = state.tasks.filter((task) => task.createdAt > cutoff || task.status === "running");
|
|
44
|
+
if (filteredTasks.length !== state.tasks.length) {
|
|
45
|
+
this.logger.log(`[TaskManager] Filtered out ${state.tasks.length - filteredTasks.length} old tasks`);
|
|
46
|
+
}
|
|
47
|
+
// Import filtered state
|
|
48
|
+
this.tracker.importState({
|
|
49
|
+
...state,
|
|
50
|
+
tasks: filteredTasks,
|
|
51
|
+
});
|
|
52
|
+
this.logger.log(`[TaskManager] Loaded ${filteredTasks.length} tasks from persistence`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
this.logger.error("[TaskManager] Failed to load persisted state:", error);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Save current task state to disk
|
|
61
|
+
*/
|
|
62
|
+
async saveState() {
|
|
63
|
+
if (!this.isDirty) {
|
|
64
|
+
return; // No changes to save
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
const state = this.tracker.exportState();
|
|
68
|
+
// Ensure directory exists
|
|
69
|
+
const dir = path.dirname(this.persistencePath);
|
|
70
|
+
await fs.promises.mkdir(dir, { recursive: true });
|
|
71
|
+
// Write atomically using temp file
|
|
72
|
+
const tempPath = `${this.persistencePath}.tmp`;
|
|
73
|
+
await fs.promises.writeFile(tempPath, JSON.stringify(state, null, 2), "utf-8");
|
|
74
|
+
await fs.promises.rename(tempPath, this.persistencePath);
|
|
75
|
+
this.isDirty = false;
|
|
76
|
+
this.logger.log(`[TaskManager] Saved ${state.tasks.length} tasks to persistence`);
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
this.logger.error("[TaskManager] Failed to save state:", error);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Force save state immediately
|
|
84
|
+
*/
|
|
85
|
+
async forceSave() {
|
|
86
|
+
this.isDirty = true;
|
|
87
|
+
await this.saveState();
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Get all tasks across all sessions with optional filtering
|
|
91
|
+
*/
|
|
92
|
+
getAllTasks(filter) {
|
|
93
|
+
let tasks = this.tracker.getAllSubagents();
|
|
94
|
+
if (filter?.status) {
|
|
95
|
+
tasks = tasks.filter((t) => filter.status.includes(t.status));
|
|
96
|
+
}
|
|
97
|
+
if (filter?.sessionId) {
|
|
98
|
+
tasks = tasks.filter((t) => t.parentSessionId === filter.sessionId);
|
|
99
|
+
}
|
|
100
|
+
if (filter?.runInBackground !== undefined) {
|
|
101
|
+
tasks = tasks.filter((t) => t.runInBackground === filter.runInBackground);
|
|
102
|
+
}
|
|
103
|
+
if (filter?.subagentType) {
|
|
104
|
+
tasks = tasks.filter((t) => t.subagentType === filter.subagentType);
|
|
105
|
+
}
|
|
106
|
+
if (filter?.newerThan !== undefined) {
|
|
107
|
+
tasks = tasks.filter((t) => t.createdAt > filter.newerThan);
|
|
108
|
+
}
|
|
109
|
+
if (filter?.olderThan !== undefined) {
|
|
110
|
+
tasks = tasks.filter((t) => t.createdAt < filter.olderThan);
|
|
111
|
+
}
|
|
112
|
+
// Sort by creation time, newest first
|
|
113
|
+
return tasks.sort((a, b) => b.createdAt - a.createdAt);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Get tasks that can be resumed
|
|
117
|
+
*/
|
|
118
|
+
getResumableTasks() {
|
|
119
|
+
return this.tracker.getResumableTasks();
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Get a task by ID
|
|
123
|
+
*/
|
|
124
|
+
getTask(taskId) {
|
|
125
|
+
return this.tracker.getSubagent(taskId);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Get a task by agent ID (for resume operations)
|
|
129
|
+
*/
|
|
130
|
+
getTaskByAgentId(agentId) {
|
|
131
|
+
return this.tracker.findByAgentId(agentId);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Read output file content for a background task
|
|
135
|
+
*/
|
|
136
|
+
async getTaskOutput(taskId) {
|
|
137
|
+
const task = this.tracker.getSubagent(taskId);
|
|
138
|
+
if (!task?.outputFile) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
return await fs.promises.readFile(task.outputFile, "utf-8");
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
this.logger.error(`[TaskManager] Failed to read output file for task ${taskId}:`, error);
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Read last N lines of output file for a background task
|
|
151
|
+
*/
|
|
152
|
+
async getTaskOutputTail(taskId, lines) {
|
|
153
|
+
const content = await this.getTaskOutput(taskId);
|
|
154
|
+
if (content === null) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
const allLines = content.split("\n");
|
|
158
|
+
return allLines.slice(-lines).join("\n");
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Cancel a running task
|
|
162
|
+
*/
|
|
163
|
+
async cancelTask(taskId) {
|
|
164
|
+
const task = this.tracker.getSubagent(taskId);
|
|
165
|
+
if (!task || task.status !== "running") {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
await this.tracker.cancelSubagent(taskId);
|
|
169
|
+
this.isDirty = true;
|
|
170
|
+
await this.saveState();
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Get statistics about all tasks
|
|
175
|
+
*/
|
|
176
|
+
getStats() {
|
|
177
|
+
return this.tracker.getStats();
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Get the underlying tracker
|
|
181
|
+
*/
|
|
182
|
+
getTracker() {
|
|
183
|
+
return this.tracker;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Clean up old completed tasks
|
|
187
|
+
*/
|
|
188
|
+
async cleanup() {
|
|
189
|
+
const cleaned = this.tracker.cleanup(this.maxTaskAgeMs);
|
|
190
|
+
if (cleaned > 0) {
|
|
191
|
+
this.isDirty = true;
|
|
192
|
+
await this.saveState();
|
|
193
|
+
}
|
|
194
|
+
return cleaned;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Dispose of the task manager
|
|
198
|
+
*/
|
|
199
|
+
dispose() {
|
|
200
|
+
if (this.autoSaveInterval) {
|
|
201
|
+
clearInterval(this.autoSaveInterval);
|
|
202
|
+
this.autoSaveInterval = undefined;
|
|
203
|
+
}
|
|
204
|
+
// Final save on dispose
|
|
205
|
+
this.saveState().catch((error) => {
|
|
206
|
+
this.logger.error("[TaskManager] Failed to save on dispose:", error);
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
setupEventListeners() {
|
|
210
|
+
// Mark state as dirty when subagent events occur
|
|
211
|
+
const markDirty = () => {
|
|
212
|
+
this.isDirty = true;
|
|
213
|
+
};
|
|
214
|
+
this.tracker.addEventListener("subagent_started", markDirty);
|
|
215
|
+
this.tracker.addEventListener("subagent_completed", markDirty);
|
|
216
|
+
this.tracker.addEventListener("subagent_failed", markDirty);
|
|
217
|
+
this.tracker.addEventListener("subagent_cancelled", markDirty);
|
|
218
|
+
this.tracker.addEventListener("subagent_stopped", markDirty);
|
|
219
|
+
}
|
|
220
|
+
startAutoSave(intervalMs) {
|
|
221
|
+
this.autoSaveInterval = setInterval(() => {
|
|
222
|
+
this.saveState().catch((error) => {
|
|
223
|
+
this.logger.error("[TaskManager] Auto-save failed:", error);
|
|
224
|
+
});
|
|
225
|
+
}, intervalMs);
|
|
226
|
+
// Don't prevent process exit
|
|
227
|
+
if (this.autoSaveInterval.unref) {
|
|
228
|
+
this.autoSaveInterval.unref();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { TaskManager } from "./task-manager.js";
|
|
3
|
+
import { SubagentTracker } from "./subagent-tracker.js";
|
|
4
|
+
export interface TaskMcpToolsOptions {
|
|
5
|
+
/** The SubagentTracker instance */
|
|
6
|
+
tracker: SubagentTracker;
|
|
7
|
+
/** Optional TaskManager for persistence features */
|
|
8
|
+
taskManager?: TaskManager;
|
|
9
|
+
/** Session ID for context */
|
|
10
|
+
sessionId: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Register MCP tools for task management
|
|
14
|
+
*/
|
|
15
|
+
export declare function registerTaskMcpTools(server: McpServer, options: TaskMcpToolsOptions): void;
|
|
16
|
+
//# sourceMappingURL=task-mcp-tools.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"task-mcp-tools.d.ts","sourceRoot":"","sources":["../src/task-mcp-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAkB,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAExE,MAAM,WAAW,mBAAmB;IAClC,mCAAmC;IACnC,OAAO,EAAE,eAAe,CAAC;IACzB,oDAAoD;IACpD,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,6BAA6B;IAC7B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,SAAS,EACjB,OAAO,EAAE,mBAAmB,GAC3B,IAAI,CA0RN"}
|