@agenticmail/enterprise 0.5.200 → 0.5.201
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/agent-heartbeat-EGMBRD3R.js +510 -0
- package/dist/chunk-5C3SCMY5.js +4457 -0
- package/dist/chunk-6OZYUTPL.js +1224 -0
- package/dist/chunk-ZR4Z42HT.js +3679 -0
- package/dist/cli-agent-PLMDHMRR.js +1602 -0
- package/dist/cli-serve-PLBAWN7N.js +114 -0
- package/dist/cli.js +3 -3
- package/dist/dashboard/app.js +3 -0
- package/dist/dashboard/components/icons.js +1 -0
- package/dist/dashboard/pages/task-pipeline.js +455 -0
- package/dist/index.js +3 -3
- package/dist/routes-KHABOHOV.js +13273 -0
- package/dist/runtime-6WFHCG3N.js +45 -0
- package/dist/server-BENJQHTB.js +15 -0
- package/dist/setup-7RQIFV5Y.js +20 -0
- package/package.json +1 -1
- package/src/dashboard/app.js +3 -0
- package/src/dashboard/components/icons.js +1 -0
- package/src/dashboard/pages/task-pipeline.js +455 -0
- package/src/engine/model-fallback.ts +141 -0
- package/src/engine/routes.ts +5 -0
- package/src/engine/task-queue-after-spawn.ts +66 -0
- package/src/engine/task-queue-before-spawn.ts +109 -0
- package/src/engine/task-queue-routes.ts +133 -0
- package/src/engine/task-queue.ts +369 -0
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized Task Queue System
|
|
3
|
+
*
|
|
4
|
+
* Tracks all agent tasks with rich metadata — before spawn, during execution,
|
|
5
|
+
* and after completion. Provides SSE for real-time dashboard updates.
|
|
6
|
+
*
|
|
7
|
+
* Every task flows through: created → assigned → in_progress → completed|failed|cancelled
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { randomUUID } from 'node:crypto';
|
|
11
|
+
|
|
12
|
+
// ─── Types ────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
export type TaskStatus = 'created' | 'assigned' | 'in_progress' | 'completed' | 'failed' | 'cancelled';
|
|
15
|
+
export type TaskPriority = 'low' | 'normal' | 'high' | 'urgent';
|
|
16
|
+
|
|
17
|
+
export interface TaskRecord {
|
|
18
|
+
id: string;
|
|
19
|
+
orgId: string;
|
|
20
|
+
|
|
21
|
+
// Who
|
|
22
|
+
assignedTo: string; // agent ID
|
|
23
|
+
assignedToName: string; // agent display name
|
|
24
|
+
createdBy: string; // 'system' | agent ID | user ID
|
|
25
|
+
createdByName: string;
|
|
26
|
+
|
|
27
|
+
// What
|
|
28
|
+
title: string; // short summary
|
|
29
|
+
description: string; // detailed task description
|
|
30
|
+
category: string; // 'email' | 'research' | 'meeting' | 'workflow' | 'custom'
|
|
31
|
+
tags: string[];
|
|
32
|
+
|
|
33
|
+
// Status
|
|
34
|
+
status: TaskStatus;
|
|
35
|
+
priority: TaskPriority;
|
|
36
|
+
progress: number; // 0-100
|
|
37
|
+
|
|
38
|
+
// Timing
|
|
39
|
+
createdAt: string;
|
|
40
|
+
assignedAt: string | null;
|
|
41
|
+
startedAt: string | null;
|
|
42
|
+
completedAt: string | null;
|
|
43
|
+
estimatedDurationMs: number | null;
|
|
44
|
+
actualDurationMs: number | null;
|
|
45
|
+
|
|
46
|
+
// Result
|
|
47
|
+
result: Record<string, unknown> | null; // outcome metadata
|
|
48
|
+
error: string | null;
|
|
49
|
+
|
|
50
|
+
// Relationships
|
|
51
|
+
parentTaskId: string | null; // for sub-tasks
|
|
52
|
+
relatedAgentIds: string[]; // other agents involved
|
|
53
|
+
sessionId: string | null; // linked session if any
|
|
54
|
+
|
|
55
|
+
// Model info
|
|
56
|
+
model: string | null;
|
|
57
|
+
fallbackModel: string | null;
|
|
58
|
+
modelUsed: string | null; // actual model that executed
|
|
59
|
+
tokensUsed: number;
|
|
60
|
+
costUsd: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
type TaskListener = (event: TaskEvent) => void;
|
|
64
|
+
|
|
65
|
+
export interface TaskEvent {
|
|
66
|
+
type: 'task_created' | 'task_updated' | 'task_completed' | 'task_failed' | 'task_cancelled' | 'task_progress';
|
|
67
|
+
task: TaskRecord;
|
|
68
|
+
timestamp: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ─── Task Queue Manager ───────────────────────────────────
|
|
72
|
+
|
|
73
|
+
export class TaskQueueManager {
|
|
74
|
+
private tasks = new Map<string, TaskRecord>();
|
|
75
|
+
private listeners = new Set<TaskListener>();
|
|
76
|
+
private db: any;
|
|
77
|
+
private initialized = false;
|
|
78
|
+
|
|
79
|
+
constructor(db?: any) {
|
|
80
|
+
this.db = db;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async init(): Promise<void> {
|
|
84
|
+
if (this.initialized) return;
|
|
85
|
+
if (this.db) {
|
|
86
|
+
try {
|
|
87
|
+
await this.db.run(`CREATE TABLE IF NOT EXISTS task_queue (
|
|
88
|
+
id TEXT PRIMARY KEY,
|
|
89
|
+
org_id TEXT NOT NULL,
|
|
90
|
+
assigned_to TEXT NOT NULL,
|
|
91
|
+
assigned_to_name TEXT NOT NULL DEFAULT '',
|
|
92
|
+
created_by TEXT NOT NULL DEFAULT 'system',
|
|
93
|
+
created_by_name TEXT NOT NULL DEFAULT '',
|
|
94
|
+
title TEXT NOT NULL,
|
|
95
|
+
description TEXT NOT NULL DEFAULT '',
|
|
96
|
+
category TEXT NOT NULL DEFAULT 'custom',
|
|
97
|
+
tags TEXT NOT NULL DEFAULT '[]',
|
|
98
|
+
status TEXT NOT NULL DEFAULT 'created',
|
|
99
|
+
priority TEXT NOT NULL DEFAULT 'normal',
|
|
100
|
+
progress INTEGER NOT NULL DEFAULT 0,
|
|
101
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
102
|
+
assigned_at TEXT,
|
|
103
|
+
started_at TEXT,
|
|
104
|
+
completed_at TEXT,
|
|
105
|
+
estimated_duration_ms INTEGER,
|
|
106
|
+
actual_duration_ms INTEGER,
|
|
107
|
+
result TEXT,
|
|
108
|
+
error TEXT,
|
|
109
|
+
parent_task_id TEXT,
|
|
110
|
+
related_agent_ids TEXT NOT NULL DEFAULT '[]',
|
|
111
|
+
session_id TEXT,
|
|
112
|
+
model TEXT,
|
|
113
|
+
fallback_model TEXT,
|
|
114
|
+
model_used TEXT,
|
|
115
|
+
tokens_used INTEGER NOT NULL DEFAULT 0,
|
|
116
|
+
cost_usd REAL NOT NULL DEFAULT 0
|
|
117
|
+
)`);
|
|
118
|
+
await this.db.run(`CREATE INDEX IF NOT EXISTS idx_task_queue_org ON task_queue(org_id)`);
|
|
119
|
+
await this.db.run(`CREATE INDEX IF NOT EXISTS idx_task_queue_agent ON task_queue(assigned_to)`);
|
|
120
|
+
await this.db.run(`CREATE INDEX IF NOT EXISTS idx_task_queue_status ON task_queue(status)`);
|
|
121
|
+
await this.db.run(`CREATE INDEX IF NOT EXISTS idx_task_queue_created ON task_queue(created_at)`);
|
|
122
|
+
|
|
123
|
+
// Load recent tasks into memory
|
|
124
|
+
const rows = await this.db.all(`SELECT * FROM task_queue WHERE status IN ('created','assigned','in_progress') OR created_at > datetime('now', '-24 hours') ORDER BY created_at DESC LIMIT 500`);
|
|
125
|
+
for (const row of rows || []) {
|
|
126
|
+
this.tasks.set(row.id, this.rowToTask(row));
|
|
127
|
+
}
|
|
128
|
+
} catch (e: any) {
|
|
129
|
+
console.error('[TaskQueue] DB init error:', e.message);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
this.initialized = true;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ─── CRUD ─────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
async createTask(opts: {
|
|
138
|
+
orgId: string;
|
|
139
|
+
assignedTo: string;
|
|
140
|
+
assignedToName: string;
|
|
141
|
+
createdBy?: string;
|
|
142
|
+
createdByName?: string;
|
|
143
|
+
title: string;
|
|
144
|
+
description?: string;
|
|
145
|
+
category?: string;
|
|
146
|
+
tags?: string[];
|
|
147
|
+
priority?: TaskPriority;
|
|
148
|
+
parentTaskId?: string;
|
|
149
|
+
relatedAgentIds?: string[];
|
|
150
|
+
sessionId?: string;
|
|
151
|
+
model?: string;
|
|
152
|
+
fallbackModel?: string;
|
|
153
|
+
estimatedDurationMs?: number;
|
|
154
|
+
}): Promise<TaskRecord> {
|
|
155
|
+
await this.init();
|
|
156
|
+
const now = new Date().toISOString();
|
|
157
|
+
const task: TaskRecord = {
|
|
158
|
+
id: randomUUID(),
|
|
159
|
+
orgId: opts.orgId,
|
|
160
|
+
assignedTo: opts.assignedTo,
|
|
161
|
+
assignedToName: opts.assignedToName,
|
|
162
|
+
createdBy: opts.createdBy || 'system',
|
|
163
|
+
createdByName: opts.createdByName || 'System',
|
|
164
|
+
title: opts.title,
|
|
165
|
+
description: opts.description || '',
|
|
166
|
+
category: opts.category || 'custom',
|
|
167
|
+
tags: opts.tags || [],
|
|
168
|
+
status: 'created',
|
|
169
|
+
priority: opts.priority || 'normal',
|
|
170
|
+
progress: 0,
|
|
171
|
+
createdAt: now,
|
|
172
|
+
assignedAt: null,
|
|
173
|
+
startedAt: null,
|
|
174
|
+
completedAt: null,
|
|
175
|
+
estimatedDurationMs: opts.estimatedDurationMs || null,
|
|
176
|
+
actualDurationMs: null,
|
|
177
|
+
result: null,
|
|
178
|
+
error: null,
|
|
179
|
+
parentTaskId: opts.parentTaskId || null,
|
|
180
|
+
relatedAgentIds: opts.relatedAgentIds || [],
|
|
181
|
+
sessionId: opts.sessionId || null,
|
|
182
|
+
model: opts.model || null,
|
|
183
|
+
fallbackModel: opts.fallbackModel || null,
|
|
184
|
+
modelUsed: null,
|
|
185
|
+
tokensUsed: 0,
|
|
186
|
+
costUsd: 0,
|
|
187
|
+
};
|
|
188
|
+
this.tasks.set(task.id, task);
|
|
189
|
+
await this.persist(task);
|
|
190
|
+
this.emit({ type: 'task_created', task, timestamp: now });
|
|
191
|
+
return task;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async updateTask(taskId: string, updates: Partial<Pick<TaskRecord, 'status' | 'progress' | 'result' | 'error' | 'modelUsed' | 'tokensUsed' | 'costUsd' | 'sessionId' | 'title' | 'description' | 'priority'>>): Promise<TaskRecord | null> {
|
|
195
|
+
await this.init();
|
|
196
|
+
const task = this.tasks.get(taskId);
|
|
197
|
+
if (!task) return null;
|
|
198
|
+
|
|
199
|
+
const now = new Date().toISOString();
|
|
200
|
+
|
|
201
|
+
if (updates.status === 'assigned' && !task.assignedAt) task.assignedAt = now;
|
|
202
|
+
if (updates.status === 'in_progress' && !task.startedAt) task.startedAt = now;
|
|
203
|
+
if (updates.status === 'completed' || updates.status === 'failed' || updates.status === 'cancelled') {
|
|
204
|
+
task.completedAt = now;
|
|
205
|
+
if (task.startedAt) task.actualDurationMs = new Date(now).getTime() - new Date(task.startedAt).getTime();
|
|
206
|
+
if (updates.status === 'completed') task.progress = 100;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
Object.assign(task, updates);
|
|
210
|
+
await this.persist(task);
|
|
211
|
+
|
|
212
|
+
const eventType = updates.status === 'completed' ? 'task_completed'
|
|
213
|
+
: updates.status === 'failed' ? 'task_failed'
|
|
214
|
+
: updates.status === 'cancelled' ? 'task_cancelled'
|
|
215
|
+
: updates.progress !== undefined ? 'task_progress'
|
|
216
|
+
: 'task_updated';
|
|
217
|
+
|
|
218
|
+
this.emit({ type: eventType, task, timestamp: now });
|
|
219
|
+
return task;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
getTask(taskId: string): TaskRecord | undefined {
|
|
223
|
+
return this.tasks.get(taskId);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ─── Queries ──────────────────────────────────────────
|
|
227
|
+
|
|
228
|
+
getActiveTasks(orgId?: string): TaskRecord[] {
|
|
229
|
+
const active: TaskRecord[] = [];
|
|
230
|
+
for (const t of this.tasks.values()) {
|
|
231
|
+
if (t.status === 'created' || t.status === 'assigned' || t.status === 'in_progress') {
|
|
232
|
+
if (!orgId || t.orgId === orgId) active.push(t);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return active.sort((a, b) => {
|
|
236
|
+
const pri = { urgent: 0, high: 1, normal: 2, low: 3 };
|
|
237
|
+
return (pri[a.priority] - pri[b.priority]) || (new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
getAllTasks(orgId?: string, limit = 100): TaskRecord[] {
|
|
242
|
+
const all: TaskRecord[] = [];
|
|
243
|
+
for (const t of this.tasks.values()) {
|
|
244
|
+
if (!orgId || t.orgId === orgId) all.push(t);
|
|
245
|
+
}
|
|
246
|
+
return all.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()).slice(0, limit);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
getAgentTasks(agentId: string, includeCompleted = false): TaskRecord[] {
|
|
250
|
+
const res: TaskRecord[] = [];
|
|
251
|
+
for (const t of this.tasks.values()) {
|
|
252
|
+
if (t.assignedTo === agentId) {
|
|
253
|
+
if (includeCompleted || t.status === 'created' || t.status === 'assigned' || t.status === 'in_progress') {
|
|
254
|
+
res.push(t);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return res.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async getTaskHistory(orgId: string, limit = 50, offset = 0): Promise<TaskRecord[]> {
|
|
262
|
+
if (this.db) {
|
|
263
|
+
try {
|
|
264
|
+
const rows = await this.db.all(
|
|
265
|
+
`SELECT * FROM task_queue WHERE org_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ?`,
|
|
266
|
+
[orgId, limit, offset]
|
|
267
|
+
);
|
|
268
|
+
return (rows || []).map((r: any) => this.rowToTask(r));
|
|
269
|
+
} catch { /* fall through */ }
|
|
270
|
+
}
|
|
271
|
+
return this.getAllTasks(orgId, limit);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
getPipelineStats(orgId?: string): { created: number; assigned: number; inProgress: number; completed: number; failed: number; cancelled: number; total: number } {
|
|
275
|
+
const stats = { created: 0, assigned: 0, inProgress: 0, completed: 0, failed: 0, cancelled: 0, total: 0 };
|
|
276
|
+
for (const t of this.tasks.values()) {
|
|
277
|
+
if (orgId && t.orgId !== orgId) continue;
|
|
278
|
+
stats.total++;
|
|
279
|
+
if (t.status === 'created') stats.created++;
|
|
280
|
+
else if (t.status === 'assigned') stats.assigned++;
|
|
281
|
+
else if (t.status === 'in_progress') stats.inProgress++;
|
|
282
|
+
else if (t.status === 'completed') stats.completed++;
|
|
283
|
+
else if (t.status === 'failed') stats.failed++;
|
|
284
|
+
else if (t.status === 'cancelled') stats.cancelled++;
|
|
285
|
+
}
|
|
286
|
+
return stats;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ─── SSE Subscriptions ────────────────────────────────
|
|
290
|
+
|
|
291
|
+
subscribe(listener: TaskListener): () => void {
|
|
292
|
+
this.listeners.add(listener);
|
|
293
|
+
return () => { this.listeners.delete(listener); };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
private emit(event: TaskEvent): void {
|
|
297
|
+
for (const l of this.listeners) {
|
|
298
|
+
try { l(event); } catch { /* ignore */ }
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ─── Persistence ──────────────────────────────────────
|
|
303
|
+
|
|
304
|
+
private async persist(task: TaskRecord): Promise<void> {
|
|
305
|
+
if (!this.db) return;
|
|
306
|
+
try {
|
|
307
|
+
await this.db.run(`INSERT OR REPLACE INTO task_queue (
|
|
308
|
+
id, org_id, assigned_to, assigned_to_name, created_by, created_by_name,
|
|
309
|
+
title, description, category, tags, status, priority, progress,
|
|
310
|
+
created_at, assigned_at, started_at, completed_at,
|
|
311
|
+
estimated_duration_ms, actual_duration_ms, result, error,
|
|
312
|
+
parent_task_id, related_agent_ids, session_id,
|
|
313
|
+
model, fallback_model, model_used, tokens_used, cost_usd
|
|
314
|
+
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`, [
|
|
315
|
+
task.id, task.orgId, task.assignedTo, task.assignedToName,
|
|
316
|
+
task.createdBy, task.createdByName,
|
|
317
|
+
task.title, task.description, task.category, JSON.stringify(task.tags),
|
|
318
|
+
task.status, task.priority, task.progress,
|
|
319
|
+
task.createdAt, task.assignedAt, task.startedAt, task.completedAt,
|
|
320
|
+
task.estimatedDurationMs, task.actualDurationMs,
|
|
321
|
+
task.result ? JSON.stringify(task.result) : null,
|
|
322
|
+
task.error,
|
|
323
|
+
task.parentTaskId, JSON.stringify(task.relatedAgentIds), task.sessionId,
|
|
324
|
+
task.model, task.fallbackModel, task.modelUsed, task.tokensUsed, task.costUsd,
|
|
325
|
+
]);
|
|
326
|
+
} catch (e: any) {
|
|
327
|
+
console.error('[TaskQueue] persist error:', e.message);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
private rowToTask(row: any): TaskRecord {
|
|
332
|
+
return {
|
|
333
|
+
id: row.id,
|
|
334
|
+
orgId: row.org_id,
|
|
335
|
+
assignedTo: row.assigned_to,
|
|
336
|
+
assignedToName: row.assigned_to_name || '',
|
|
337
|
+
createdBy: row.created_by || 'system',
|
|
338
|
+
createdByName: row.created_by_name || '',
|
|
339
|
+
title: row.title,
|
|
340
|
+
description: row.description || '',
|
|
341
|
+
category: row.category || 'custom',
|
|
342
|
+
tags: safeJson(row.tags, []),
|
|
343
|
+
status: row.status as TaskStatus,
|
|
344
|
+
priority: (row.priority || 'normal') as TaskPriority,
|
|
345
|
+
progress: row.progress || 0,
|
|
346
|
+
createdAt: row.created_at,
|
|
347
|
+
assignedAt: row.assigned_at || null,
|
|
348
|
+
startedAt: row.started_at || null,
|
|
349
|
+
completedAt: row.completed_at || null,
|
|
350
|
+
estimatedDurationMs: row.estimated_duration_ms || null,
|
|
351
|
+
actualDurationMs: row.actual_duration_ms || null,
|
|
352
|
+
result: safeJson(row.result, null),
|
|
353
|
+
error: row.error || null,
|
|
354
|
+
parentTaskId: row.parent_task_id || null,
|
|
355
|
+
relatedAgentIds: safeJson(row.related_agent_ids, []),
|
|
356
|
+
sessionId: row.session_id || null,
|
|
357
|
+
model: row.model || null,
|
|
358
|
+
fallbackModel: row.fallback_model || null,
|
|
359
|
+
modelUsed: row.model_used || null,
|
|
360
|
+
tokensUsed: row.tokens_used || 0,
|
|
361
|
+
costUsd: row.cost_usd || 0,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function safeJson(v: any, fallback: any): any {
|
|
367
|
+
if (!v || typeof v !== 'string') return fallback;
|
|
368
|
+
try { return JSON.parse(v); } catch { return fallback; }
|
|
369
|
+
}
|