@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();
@@ -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): { created: number; assigned: number; inProgress: number; completed: number; failed: number; cancelled: number; total: number } {
276
- const stats = { created: 0, assigned: 0, inProgress: 0, completed: 0, failed: 0, cancelled: 0, total: 0 };
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
- ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
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
  }