@agenticmail/enterprise 0.5.209 → 0.5.211
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.
|
@@ -49,6 +49,28 @@ export function createTaskQueueRoutes(taskQueue: TaskQueueManager) {
|
|
|
49
49
|
return c.json({ task });
|
|
50
50
|
});
|
|
51
51
|
|
|
52
|
+
// GET /task-pipeline/chain/:chainId — full task chain (delegation flow)
|
|
53
|
+
router.get('/chain/:chainId', (c) => {
|
|
54
|
+
const chain = taskQueue.getTaskChain(c.req.param('chainId'));
|
|
55
|
+
if (!chain.length) return c.json({ error: 'Chain not found' }, 404);
|
|
56
|
+
return c.json({ chain });
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// POST /task-pipeline/:id/delegate — delegate task to another agent
|
|
60
|
+
router.post('/:id/delegate', async (c) => {
|
|
61
|
+
const body = await c.req.json();
|
|
62
|
+
const task = await taskQueue.delegateTask(c.req.param('id'), {
|
|
63
|
+
toAgent: body.toAgent,
|
|
64
|
+
toAgentName: body.toAgentName || body.toAgent,
|
|
65
|
+
delegationType: body.delegationType || 'delegation',
|
|
66
|
+
title: body.title,
|
|
67
|
+
description: body.description,
|
|
68
|
+
priority: body.priority,
|
|
69
|
+
});
|
|
70
|
+
if (!task) return c.json({ error: 'Task not found' }, 404);
|
|
71
|
+
return c.json({ task }, 201);
|
|
72
|
+
});
|
|
73
|
+
|
|
52
74
|
// POST /task-pipeline — create task manually
|
|
53
75
|
router.post('/', async (c) => {
|
|
54
76
|
const body = await c.req.json();
|
package/src/engine/task-queue.ts
CHANGED
|
@@ -58,6 +58,32 @@ export interface TaskRecord {
|
|
|
58
58
|
modelUsed: string | null; // actual model that executed
|
|
59
59
|
tokensUsed: number;
|
|
60
60
|
costUsd: number;
|
|
61
|
+
|
|
62
|
+
// Task chain (multi-agent delegation tracking)
|
|
63
|
+
chainId: string | null; // shared ID across all tasks in a delegation chain
|
|
64
|
+
chainSeq: number; // sequence number within chain (0 = origin)
|
|
65
|
+
delegatedFrom: string | null; // task ID this was delegated from
|
|
66
|
+
delegatedTo: string | null; // task ID this was delegated to
|
|
67
|
+
delegationType: string | null; // 'delegation' | 'review' | 'revision' | 'escalation' | 'return'
|
|
68
|
+
|
|
69
|
+
// Customer context (for support/external-facing tasks)
|
|
70
|
+
customerContext: {
|
|
71
|
+
name: string;
|
|
72
|
+
email: string;
|
|
73
|
+
phone: string;
|
|
74
|
+
company: string;
|
|
75
|
+
channel: string; // 'email' | 'chat' | 'phone' | 'whatsapp' | 'ticket'
|
|
76
|
+
isNew: boolean;
|
|
77
|
+
metadata: Record<string, unknown>;
|
|
78
|
+
} | null;
|
|
79
|
+
|
|
80
|
+
// Activity log (micro-events within a task)
|
|
81
|
+
activityLog: Array<{
|
|
82
|
+
ts: string;
|
|
83
|
+
type: string; // 'created' | 'assigned' | 'started' | 'delegated' | 'returned' | 'progress' | 'completed' | 'failed' | 'note'
|
|
84
|
+
agent: string;
|
|
85
|
+
detail: string;
|
|
86
|
+
}>;
|
|
61
87
|
}
|
|
62
88
|
|
|
63
89
|
type TaskListener = (event: TaskEvent) => void;
|
|
@@ -113,12 +139,29 @@ export class TaskQueueManager {
|
|
|
113
139
|
fallback_model TEXT,
|
|
114
140
|
model_used TEXT,
|
|
115
141
|
tokens_used INTEGER NOT NULL DEFAULT 0,
|
|
116
|
-
cost_usd REAL NOT NULL DEFAULT 0
|
|
142
|
+
cost_usd REAL NOT NULL DEFAULT 0,
|
|
143
|
+
chain_id TEXT,
|
|
144
|
+
chain_seq INTEGER NOT NULL DEFAULT 0,
|
|
145
|
+
delegated_from TEXT,
|
|
146
|
+
delegated_to TEXT,
|
|
147
|
+
delegation_type TEXT,
|
|
148
|
+
customer_context TEXT,
|
|
149
|
+
activity_log TEXT NOT NULL DEFAULT '[]'
|
|
117
150
|
)`);
|
|
118
151
|
await this.db.run(`CREATE INDEX IF NOT EXISTS idx_task_pipeline_org ON task_pipeline(org_id)`);
|
|
119
152
|
await this.db.run(`CREATE INDEX IF NOT EXISTS idx_task_pipeline_agent ON task_pipeline(assigned_to)`);
|
|
120
153
|
await this.db.run(`CREATE INDEX IF NOT EXISTS idx_task_pipeline_status ON task_pipeline(status)`);
|
|
121
154
|
await this.db.run(`CREATE INDEX IF NOT EXISTS idx_task_pipeline_created ON task_pipeline(created_at)`);
|
|
155
|
+
await this.db.run(`CREATE INDEX IF NOT EXISTS idx_task_pipeline_chain ON task_pipeline(chain_id)`);
|
|
156
|
+
|
|
157
|
+
// Add new columns to existing tables (safe — IF NOT EXISTS not available for columns, so catch errors)
|
|
158
|
+
for (const col of [
|
|
159
|
+
['chain_id', 'TEXT'], ['chain_seq', 'INTEGER DEFAULT 0'], ['delegated_from', 'TEXT'],
|
|
160
|
+
['delegated_to', 'TEXT'], ['delegation_type', 'TEXT'], ['customer_context', 'TEXT'],
|
|
161
|
+
['activity_log', 'TEXT DEFAULT \'[]\'']
|
|
162
|
+
]) {
|
|
163
|
+
try { await this.db.run(`ALTER TABLE task_pipeline ADD COLUMN ${col[0]} ${col[1]}`); } catch { /* already exists */ }
|
|
164
|
+
}
|
|
122
165
|
|
|
123
166
|
// Load recent tasks into memory
|
|
124
167
|
const cutoff = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
|
|
@@ -152,6 +195,11 @@ export class TaskQueueManager {
|
|
|
152
195
|
model?: string;
|
|
153
196
|
fallbackModel?: string;
|
|
154
197
|
estimatedDurationMs?: number;
|
|
198
|
+
chainId?: string;
|
|
199
|
+
chainSeq?: number;
|
|
200
|
+
delegatedFrom?: string;
|
|
201
|
+
delegationType?: string;
|
|
202
|
+
customerContext?: TaskRecord['customerContext'];
|
|
155
203
|
}): Promise<TaskRecord> {
|
|
156
204
|
await this.init();
|
|
157
205
|
const now = new Date().toISOString();
|
|
@@ -185,6 +233,13 @@ export class TaskQueueManager {
|
|
|
185
233
|
modelUsed: null,
|
|
186
234
|
tokensUsed: 0,
|
|
187
235
|
costUsd: 0,
|
|
236
|
+
chainId: opts.chainId || randomUUID(),
|
|
237
|
+
chainSeq: opts.chainSeq || 0,
|
|
238
|
+
delegatedFrom: opts.delegatedFrom || null,
|
|
239
|
+
delegatedTo: null,
|
|
240
|
+
delegationType: opts.delegationType || null,
|
|
241
|
+
customerContext: opts.customerContext || null,
|
|
242
|
+
activityLog: [{ ts: now, type: 'created', agent: opts.createdBy || 'system', detail: 'Task created' }],
|
|
188
243
|
};
|
|
189
244
|
this.tasks.set(task.id, task);
|
|
190
245
|
await this.persist(task);
|
|
@@ -192,6 +247,63 @@ export class TaskQueueManager {
|
|
|
192
247
|
return task;
|
|
193
248
|
}
|
|
194
249
|
|
|
250
|
+
/**
|
|
251
|
+
* Delegate a task from one agent to another, creating a new linked task in the chain.
|
|
252
|
+
*/
|
|
253
|
+
async delegateTask(taskId: string, opts: {
|
|
254
|
+
toAgent: string;
|
|
255
|
+
toAgentName: string;
|
|
256
|
+
delegationType?: string;
|
|
257
|
+
title?: string;
|
|
258
|
+
description?: string;
|
|
259
|
+
priority?: TaskPriority;
|
|
260
|
+
}): Promise<TaskRecord | null> {
|
|
261
|
+
const source = this.tasks.get(taskId);
|
|
262
|
+
if (!source) return null;
|
|
263
|
+
|
|
264
|
+
const now = new Date().toISOString();
|
|
265
|
+
source.activityLog.push({ ts: now, type: 'delegated', agent: source.assignedTo, detail: `Delegated to ${opts.toAgentName} (${opts.delegationType || 'delegation'})` });
|
|
266
|
+
|
|
267
|
+
// Create the new delegated task
|
|
268
|
+
const delegated = await this.createTask({
|
|
269
|
+
orgId: source.orgId,
|
|
270
|
+
assignedTo: opts.toAgent,
|
|
271
|
+
assignedToName: opts.toAgentName,
|
|
272
|
+
createdBy: source.assignedTo,
|
|
273
|
+
createdByName: source.assignedToName,
|
|
274
|
+
title: opts.title || source.title,
|
|
275
|
+
description: opts.description || source.description,
|
|
276
|
+
category: source.category,
|
|
277
|
+
tags: [...source.tags],
|
|
278
|
+
priority: opts.priority || source.priority,
|
|
279
|
+
parentTaskId: source.parentTaskId,
|
|
280
|
+
relatedAgentIds: [...new Set([...source.relatedAgentIds, source.assignedTo])],
|
|
281
|
+
chainId: source.chainId || undefined,
|
|
282
|
+
chainSeq: (source.chainSeq || 0) + 1,
|
|
283
|
+
delegatedFrom: source.id,
|
|
284
|
+
delegationType: opts.delegationType || 'delegation',
|
|
285
|
+
customerContext: source.customerContext,
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// Update source to point to delegated task
|
|
289
|
+
source.delegatedTo = delegated.id;
|
|
290
|
+
await this.persist(source);
|
|
291
|
+
this.emit({ type: 'task_updated', task: source, timestamp: now });
|
|
292
|
+
|
|
293
|
+
return delegated;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Get full task chain by chainId — all tasks in a delegation flow.
|
|
298
|
+
*/
|
|
299
|
+
getTaskChain(chainId: string): TaskRecord[] {
|
|
300
|
+
const chain: TaskRecord[] = [];
|
|
301
|
+
for (const t of this.tasks.values()) {
|
|
302
|
+
if (t.chainId === chainId) chain.push(t);
|
|
303
|
+
}
|
|
304
|
+
return chain.sort((a, b) => (a.chainSeq || 0) - (b.chainSeq || 0));
|
|
305
|
+
}
|
|
306
|
+
|
|
195
307
|
async updateTask(taskId: string, updates: Partial<Pick<TaskRecord, 'status' | 'progress' | 'result' | 'error' | 'modelUsed' | 'tokensUsed' | 'costUsd' | 'sessionId' | 'title' | 'description' | 'priority'>>): Promise<TaskRecord | null> {
|
|
196
308
|
await this.init();
|
|
197
309
|
const task = this.tasks.get(taskId);
|
|
@@ -199,8 +311,9 @@ export class TaskQueueManager {
|
|
|
199
311
|
|
|
200
312
|
const now = new Date().toISOString();
|
|
201
313
|
|
|
202
|
-
if (updates.status === 'assigned' && !task.assignedAt) task.assignedAt = now;
|
|
203
|
-
if (updates.status === 'in_progress' && !task.startedAt) task.startedAt = now;
|
|
314
|
+
if (updates.status === 'assigned' && !task.assignedAt) { task.assignedAt = now; task.activityLog.push({ ts: now, type: 'assigned', agent: task.assignedTo, detail: 'Task assigned' }); }
|
|
315
|
+
if (updates.status === 'in_progress' && !task.startedAt) { task.startedAt = now; task.activityLog.push({ ts: now, type: 'started', agent: task.assignedTo, detail: 'Work started' }); }
|
|
316
|
+
if (updates.progress !== undefined && updates.progress !== task.progress) { task.activityLog.push({ ts: now, type: 'progress', agent: task.assignedTo, detail: `Progress: ${updates.progress}%` }); }
|
|
204
317
|
if (updates.status === 'completed' || updates.status === 'failed' || updates.status === 'cancelled') {
|
|
205
318
|
task.completedAt = now;
|
|
206
319
|
if (task.startedAt) task.actualDurationMs = new Date(now).getTime() - new Date(task.startedAt).getTime();
|
|
@@ -272,8 +385,21 @@ export class TaskQueueManager {
|
|
|
272
385
|
return this.getAllTasks(orgId, limit);
|
|
273
386
|
}
|
|
274
387
|
|
|
275
|
-
getPipelineStats(orgId?: string): {
|
|
276
|
-
|
|
388
|
+
getPipelineStats(orgId?: string): {
|
|
389
|
+
created: number; assigned: number; inProgress: number; completed: number; failed: number; cancelled: number; total: number;
|
|
390
|
+
todayCompleted: number; todayFailed: number; todayCreated: number; avgDurationMs: number; totalCost: number; totalTokens: number;
|
|
391
|
+
topAgents: Array<{ agent: string; name: string; completed: number; active: number }>;
|
|
392
|
+
} {
|
|
393
|
+
const stats = {
|
|
394
|
+
created: 0, assigned: 0, inProgress: 0, completed: 0, failed: 0, cancelled: 0, total: 0,
|
|
395
|
+
todayCompleted: 0, todayFailed: 0, todayCreated: 0, avgDurationMs: 0, totalCost: 0, totalTokens: 0,
|
|
396
|
+
topAgents: [] as Array<{ agent: string; name: string; completed: number; active: number }>,
|
|
397
|
+
};
|
|
398
|
+
const todayStart = new Date(); todayStart.setHours(0, 0, 0, 0);
|
|
399
|
+
const todayMs = todayStart.getTime();
|
|
400
|
+
let durationSum = 0; let durationCount = 0;
|
|
401
|
+
const agentMap = new Map<string, { name: string; completed: number; active: number }>();
|
|
402
|
+
|
|
277
403
|
for (const t of this.tasks.values()) {
|
|
278
404
|
if (orgId && t.orgId !== orgId) continue;
|
|
279
405
|
stats.total++;
|
|
@@ -283,7 +409,31 @@ export class TaskQueueManager {
|
|
|
283
409
|
else if (t.status === 'completed') stats.completed++;
|
|
284
410
|
else if (t.status === 'failed') stats.failed++;
|
|
285
411
|
else if (t.status === 'cancelled') stats.cancelled++;
|
|
412
|
+
|
|
413
|
+
// Today metrics
|
|
414
|
+
const createdMs = new Date(t.createdAt).getTime();
|
|
415
|
+
if (createdMs >= todayMs) stats.todayCreated++;
|
|
416
|
+
if (t.completedAt && new Date(t.completedAt).getTime() >= todayMs) {
|
|
417
|
+
if (t.status === 'completed') stats.todayCompleted++;
|
|
418
|
+
if (t.status === 'failed') stats.todayFailed++;
|
|
419
|
+
}
|
|
420
|
+
if (t.actualDurationMs) { durationSum += t.actualDurationMs; durationCount++; }
|
|
421
|
+
stats.totalCost += t.costUsd || 0;
|
|
422
|
+
stats.totalTokens += t.tokensUsed || 0;
|
|
423
|
+
|
|
424
|
+
// Per-agent
|
|
425
|
+
if (t.assignedTo) {
|
|
426
|
+
if (!agentMap.has(t.assignedTo)) agentMap.set(t.assignedTo, { name: t.assignedToName || t.assignedTo, completed: 0, active: 0 });
|
|
427
|
+
const a = agentMap.get(t.assignedTo)!;
|
|
428
|
+
if (t.status === 'completed') a.completed++;
|
|
429
|
+
if (t.status === 'in_progress' || t.status === 'assigned') a.active++;
|
|
430
|
+
}
|
|
286
431
|
}
|
|
432
|
+
stats.avgDurationMs = durationCount > 0 ? Math.round(durationSum / durationCount) : 0;
|
|
433
|
+
stats.topAgents = Array.from(agentMap.entries())
|
|
434
|
+
.map(([agent, d]) => ({ agent, ...d }))
|
|
435
|
+
.sort((a, b) => (b.completed + b.active) - (a.completed + a.active))
|
|
436
|
+
.slice(0, 5);
|
|
287
437
|
return stats;
|
|
288
438
|
}
|
|
289
439
|
|
|
@@ -311,14 +461,17 @@ export class TaskQueueManager {
|
|
|
311
461
|
created_at, assigned_at, started_at, completed_at,
|
|
312
462
|
estimated_duration_ms, actual_duration_ms, result, error,
|
|
313
463
|
parent_task_id, related_agent_ids, session_id,
|
|
314
|
-
model, fallback_model, model_used, tokens_used, cost_usd
|
|
315
|
-
|
|
464
|
+
model, fallback_model, model_used, tokens_used, cost_usd,
|
|
465
|
+
chain_id, chain_seq, delegated_from, delegated_to, delegation_type,
|
|
466
|
+
customer_context, activity_log
|
|
467
|
+
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
316
468
|
ON CONFLICT (id) DO UPDATE SET
|
|
317
469
|
status=EXCLUDED.status, priority=EXCLUDED.priority, progress=EXCLUDED.progress,
|
|
318
470
|
assigned_at=EXCLUDED.assigned_at, started_at=EXCLUDED.started_at, completed_at=EXCLUDED.completed_at,
|
|
319
471
|
actual_duration_ms=EXCLUDED.actual_duration_ms, result=EXCLUDED.result, error=EXCLUDED.error,
|
|
320
472
|
model_used=EXCLUDED.model_used, tokens_used=EXCLUDED.tokens_used, cost_usd=EXCLUDED.cost_usd,
|
|
321
|
-
session_id=EXCLUDED.session_id, title=EXCLUDED.title, description=EXCLUDED.description
|
|
473
|
+
session_id=EXCLUDED.session_id, title=EXCLUDED.title, description=EXCLUDED.description,
|
|
474
|
+
delegated_to=EXCLUDED.delegated_to, activity_log=EXCLUDED.activity_log`, [
|
|
322
475
|
task.id, task.orgId, task.assignedTo, task.assignedToName,
|
|
323
476
|
task.createdBy, task.createdByName,
|
|
324
477
|
task.title, task.description, task.category, JSON.stringify(task.tags),
|
|
@@ -329,6 +482,9 @@ export class TaskQueueManager {
|
|
|
329
482
|
task.error,
|
|
330
483
|
task.parentTaskId, JSON.stringify(task.relatedAgentIds), task.sessionId,
|
|
331
484
|
task.model, task.fallbackModel, task.modelUsed, task.tokensUsed, task.costUsd,
|
|
485
|
+
task.chainId, task.chainSeq, task.delegatedFrom, task.delegatedTo, task.delegationType,
|
|
486
|
+
task.customerContext ? JSON.stringify(task.customerContext) : null,
|
|
487
|
+
JSON.stringify(task.activityLog || []),
|
|
332
488
|
]);
|
|
333
489
|
} catch (e: any) {
|
|
334
490
|
console.error('[TaskQueue] persist error:', e.message);
|
|
@@ -366,6 +522,13 @@ export class TaskQueueManager {
|
|
|
366
522
|
modelUsed: row.model_used || null,
|
|
367
523
|
tokensUsed: row.tokens_used || 0,
|
|
368
524
|
costUsd: row.cost_usd || 0,
|
|
525
|
+
chainId: row.chain_id || null,
|
|
526
|
+
chainSeq: row.chain_seq || 0,
|
|
527
|
+
delegatedFrom: row.delegated_from || null,
|
|
528
|
+
delegatedTo: row.delegated_to || null,
|
|
529
|
+
delegationType: row.delegation_type || null,
|
|
530
|
+
customerContext: safeJson(row.customer_context, null),
|
|
531
|
+
activityLog: safeJson(row.activity_log, []),
|
|
369
532
|
};
|
|
370
533
|
}
|
|
371
534
|
}
|