@liriraid/agentflow-ai 1.0.14 → 1.0.15
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/agentflow.mjs +102 -2
- package/orchestrator.js +321 -74
- package/package.json +1 -1
- package/scripts/auto-trigger.js +114 -0
- package/scripts/monitor-check.js +172 -0
- package/src/ink/app.mjs +22 -14
- package/src/ink/index.mjs +21 -2
- package/templates/en/ORCHESTRATOR.md +53 -28
- package/templates/es/ORCHESTRATOR.md +61 -22
package/orchestrator.js
CHANGED
|
@@ -129,6 +129,36 @@ const PROJECT_NAME = config.projectName || "Orchestrator Multi-Agents";
|
|
|
129
129
|
const WORKSPACE_LANGUAGE = ["en", "es"].includes(config.workspaceLanguage)
|
|
130
130
|
? config.workspaceLanguage
|
|
131
131
|
: "es";
|
|
132
|
+
|
|
133
|
+
// OpenAI model pricing ($ per 1M tokens) — update when prices change
|
|
134
|
+
const OPENAI_MODEL_PRICING = {
|
|
135
|
+
"gpt-4.1": { input: 2.0, output: 8.0 },
|
|
136
|
+
"gpt-4.1-mini": { input: 0.4, output: 1.6 },
|
|
137
|
+
"gpt-4.1-nano": { input: 0.1, output: 0.4 },
|
|
138
|
+
"gpt-4o": { input: 2.5, output: 10.0 },
|
|
139
|
+
"gpt-4o-mini": { input: 0.15, output: 0.6 },
|
|
140
|
+
"o4-mini": { input: 1.1, output: 4.4 },
|
|
141
|
+
"o3": { input: 10.0, output: 40.0 },
|
|
142
|
+
"o3-mini": { input: 1.1, output: 4.4 },
|
|
143
|
+
"o1": { input: 15.0, output: 60.0 },
|
|
144
|
+
"o1-mini": { input: 3.0, output: 12.0 },
|
|
145
|
+
"gpt-5": { input: 5.0, output: 20.0 },
|
|
146
|
+
"gpt-5.5": { input: 5.0, output: 20.0 },
|
|
147
|
+
"codex-mini-latest": { input: 1.5, output: 6.0 },
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
function calcOpenAICost(model, usage) {
|
|
151
|
+
if (!model || !usage) return null;
|
|
152
|
+
const modelLower = String(model).toLowerCase();
|
|
153
|
+
const key = Object.keys(OPENAI_MODEL_PRICING).find(k =>
|
|
154
|
+
modelLower === k || modelLower.startsWith(k) || modelLower.includes(k)
|
|
155
|
+
);
|
|
156
|
+
if (!key) return null;
|
|
157
|
+
const price = OPENAI_MODEL_PRICING[key];
|
|
158
|
+
const inputTokens = usage.input_tokens || usage.prompt_tokens || 0;
|
|
159
|
+
const outputTokens = usage.output_tokens || usage.completion_tokens || 0;
|
|
160
|
+
return (inputTokens * price.input + outputTokens * price.output) / 1_000_000;
|
|
161
|
+
}
|
|
132
162
|
const TEXT = {
|
|
133
163
|
es: {
|
|
134
164
|
configExists:
|
|
@@ -160,6 +190,8 @@ const TEXT = {
|
|
|
160
190
|
running: "EJECUTANDO",
|
|
161
191
|
busy: "OCUPADO",
|
|
162
192
|
idle: "EN ESPERA",
|
|
193
|
+
failed: "FALLÓ",
|
|
194
|
+
retrying: "REINTENTANDO",
|
|
163
195
|
queueReloaded: (count) => `Cola recargada: ${count} tareas`,
|
|
164
196
|
quitRequested: "Cierre solicitado desde Ink",
|
|
165
197
|
starting: (name) => `${name} iniciando`,
|
|
@@ -173,6 +205,65 @@ const TEXT = {
|
|
|
173
205
|
retryAt: (time, remaining) => `reintenta a las ${time} (${remaining} min)`,
|
|
174
206
|
log: "REGISTRO",
|
|
175
207
|
controls: "Seguir Pausa Recargar Quitar",
|
|
208
|
+
// QUEUE.md section headers
|
|
209
|
+
sectionPending: "## Pendientes",
|
|
210
|
+
sectionInProgress: "## En progreso",
|
|
211
|
+
sectionCompleted: "## Completadas",
|
|
212
|
+
// appendToAgent messages
|
|
213
|
+
agentTaskHeader: (id, title) => `=== ${id}: ${title} ===`,
|
|
214
|
+
agentCwd: (dir) => `CWD: ${dir}`,
|
|
215
|
+
agentCompleted: (elapsed, cost) => `=== COMPLETADA en ${elapsed}${cost} ===`,
|
|
216
|
+
agentFailed: (code, attempt) => `=== FALLÓ (salida ${code}, intento ${attempt}) ===`,
|
|
217
|
+
agentRateLimit: (resetStr) => `=== LÍMITE DE CUOTA (${resetStr}) ===`,
|
|
218
|
+
agentTimeout: "=== TIMEOUT ===",
|
|
219
|
+
agentDied: "=== PROCESS DIED ===",
|
|
220
|
+
agentReassigned: (agent, reason) => `=== REASIGNADA A ${agent} (${reason}) ===`,
|
|
221
|
+
// ag.lastLine state machine prefixes (used for status detection)
|
|
222
|
+
lastCompleted: (id) => `Última: ${id} completada`,
|
|
223
|
+
lastRetry: (id) => `REINTENTO: ${id}`,
|
|
224
|
+
lastLimit: (id, time) => `LÍMITE: ${id} (reintento a las ${time})`,
|
|
225
|
+
lastFailed: (id) => `FALLÓ: ${id}`,
|
|
226
|
+
// Fallback reasons
|
|
227
|
+
reasonQuota: "cuota o límite agotado",
|
|
228
|
+
reasonProvider: "proveedor o sesión no disponibles",
|
|
229
|
+
reasonNoWork: "el agente no trabajó nada (sin cambios)",
|
|
230
|
+
reasonPersistent: "fallo persistente",
|
|
231
|
+
// Log messages
|
|
232
|
+
logRateLimit: (agent, id, resetStr) => `${agent} alcanzó el límite en ${id} (${resetStr})`,
|
|
233
|
+
logFail: (agent, id, code, retries, max) => `${agent} falló ${id} (salida ${code}, ${retries}/${max})`,
|
|
234
|
+
logDone: (agent, id, elapsed, cost) => `${agent} completó ${id} en ${elapsed}${cost}`,
|
|
235
|
+
logFallback: (id, from, to, reason) => `${id} reasignada de ${from} a ${to} (${reason})`,
|
|
236
|
+
logReassignWarn: (id, agent) => `${id} reasignada a ${agent}, pero QUEUE.md no pudo actualizarse`,
|
|
237
|
+
logTimeout: (agent, id) => `${agent} timed out on ${id}`,
|
|
238
|
+
logUnknownAgent: (id, agent) => `${id} skipped — agente "${agent}" no definido en orchestrator.config.json`,
|
|
239
|
+
logPermanentFail: (id, retries) => `${id} falló definitivamente tras ${retries} intentos`,
|
|
240
|
+
logDied: (agent, id) => `${agent} terminó silenciosamente en ${id}`,
|
|
241
|
+
// STATUS.md
|
|
242
|
+
statusTitle: (ts) => `# Estado del Orquestador - ${ts}`,
|
|
243
|
+
statusProject: "**Proyecto:**",
|
|
244
|
+
statusState: "**Estado:**",
|
|
245
|
+
statusRunning: "EJECUTANDO",
|
|
246
|
+
statusPausedLabel: "PAUSADO",
|
|
247
|
+
statusActiveTime: "**Activo:**",
|
|
248
|
+
statusSectionAgents: "## Agentes",
|
|
249
|
+
statusSectionQueue: "## Cola",
|
|
250
|
+
statusPendingLabel: "Pendientes:",
|
|
251
|
+
statusCompletedLabel: "Completadas:",
|
|
252
|
+
statusInProgressLabel: "En progreso:",
|
|
253
|
+
statusInProgressHeader: "### En progreso",
|
|
254
|
+
statusAgentBusy: "🟡 OCUPADO",
|
|
255
|
+
statusAgentIdle: "⚪ EN ESPERA",
|
|
256
|
+
statusNoTask: "Sin tarea",
|
|
257
|
+
// INBOX.md
|
|
258
|
+
inboxDone: (ts, id, agent) => `## [${ts}] ${id} completada — ${agent}`,
|
|
259
|
+
inboxTaskLabel: "- **Tarea:**",
|
|
260
|
+
inboxDurationLabel: "- **Duración:**",
|
|
261
|
+
inboxReportLabel: "- **Reporte:**",
|
|
262
|
+
inboxActionLabel: (file) => `- **Acción:** Lee \`${file}\` y crea las siguientes TASKs si corresponde.`,
|
|
263
|
+
inboxFailed: (ts, id, from, to) => `## [${ts}] ${id} falló — ${from} → reasignada a ${to}`,
|
|
264
|
+
inboxReasonLabel: "- **Motivo:**",
|
|
265
|
+
inboxNewAgentLabel: "- **Nuevo agente:**",
|
|
266
|
+
inboxFailAction: "- **Acción:** La TUI reasignó automáticamente. Verifica en QUEUE.md o espera la siguiente notificación de completada.",
|
|
176
267
|
},
|
|
177
268
|
en: {
|
|
178
269
|
configExists:
|
|
@@ -204,6 +295,8 @@ const TEXT = {
|
|
|
204
295
|
running: "RUNNING",
|
|
205
296
|
busy: "BUSY",
|
|
206
297
|
idle: "IDLE",
|
|
298
|
+
failed: "FAILED",
|
|
299
|
+
retrying: "RETRYING",
|
|
207
300
|
queueReloaded: (count) => `Queue reloaded: ${count} tasks`,
|
|
208
301
|
quitRequested: "Quit requested from Ink",
|
|
209
302
|
starting: (name) => `${name} starting`,
|
|
@@ -216,6 +309,65 @@ const TEXT = {
|
|
|
216
309
|
retryAt: (time, remaining) => `retry at ${time} (${remaining} min)`,
|
|
217
310
|
log: "LOG",
|
|
218
311
|
controls: "Start Pause Reload Quit",
|
|
312
|
+
// QUEUE.md section headers
|
|
313
|
+
sectionPending: "## Pending",
|
|
314
|
+
sectionInProgress: "## In Progress",
|
|
315
|
+
sectionCompleted: "## Completed",
|
|
316
|
+
// appendToAgent messages
|
|
317
|
+
agentTaskHeader: (id, title) => `=== ${id}: ${title} ===`,
|
|
318
|
+
agentCwd: (dir) => `CWD: ${dir}`,
|
|
319
|
+
agentCompleted: (elapsed, cost) => `=== COMPLETED in ${elapsed}${cost} ===`,
|
|
320
|
+
agentFailed: (code, attempt) => `=== FAILED (exit ${code}, attempt ${attempt}) ===`,
|
|
321
|
+
agentRateLimit: (resetStr) => `=== QUOTA LIMIT (${resetStr}) ===`,
|
|
322
|
+
agentTimeout: "=== TIMEOUT ===",
|
|
323
|
+
agentDied: "=== PROCESS DIED ===",
|
|
324
|
+
agentReassigned: (agent, reason) => `=== REASSIGNED TO ${agent} (${reason}) ===`,
|
|
325
|
+
// ag.lastLine state machine prefixes
|
|
326
|
+
lastCompleted: (id) => `Last: ${id} completed`,
|
|
327
|
+
lastRetry: (id) => `RETRY: ${id}`,
|
|
328
|
+
lastLimit: (id, time) => `LIMIT: ${id} (retry at ${time})`,
|
|
329
|
+
lastFailed: (id) => `FAILED: ${id}`,
|
|
330
|
+
// Fallback reasons
|
|
331
|
+
reasonQuota: "quota or rate limit exhausted",
|
|
332
|
+
reasonProvider: "provider or session unavailable",
|
|
333
|
+
reasonNoWork: "agent did no real work (no changes)",
|
|
334
|
+
reasonPersistent: "persistent failure",
|
|
335
|
+
// Log messages
|
|
336
|
+
logRateLimit: (agent, id, resetStr) => `${agent} hit rate limit on ${id} (${resetStr})`,
|
|
337
|
+
logFail: (agent, id, code, retries, max) => `${agent} failed ${id} (exit ${code}, ${retries}/${max})`,
|
|
338
|
+
logDone: (agent, id, elapsed, cost) => `${agent} completed ${id} in ${elapsed}${cost}`,
|
|
339
|
+
logFallback: (id, from, to, reason) => `${id} reassigned from ${from} to ${to} (${reason})`,
|
|
340
|
+
logReassignWarn: (id, agent) => `${id} reassigned to ${agent}, but QUEUE.md could not be updated`,
|
|
341
|
+
logTimeout: (agent, id) => `${agent} timed out on ${id}`,
|
|
342
|
+
logUnknownAgent: (id, agent) => `${id} skipped — agent "${agent}" not in orchestrator.config.json`,
|
|
343
|
+
logPermanentFail: (id, retries) => `${id} permanently failed after ${retries} attempts`,
|
|
344
|
+
logDied: (agent, id) => `${agent} died silently on ${id}`,
|
|
345
|
+
// STATUS.md
|
|
346
|
+
statusTitle: (ts) => `# Orchestrator Status - ${ts}`,
|
|
347
|
+
statusProject: "**Project:**",
|
|
348
|
+
statusState: "**State:**",
|
|
349
|
+
statusRunning: "RUNNING",
|
|
350
|
+
statusPausedLabel: "PAUSED",
|
|
351
|
+
statusActiveTime: "**Active:**",
|
|
352
|
+
statusSectionAgents: "## Agents",
|
|
353
|
+
statusSectionQueue: "## Queue",
|
|
354
|
+
statusPendingLabel: "Pending:",
|
|
355
|
+
statusCompletedLabel: "Completed:",
|
|
356
|
+
statusInProgressLabel: "In progress:",
|
|
357
|
+
statusInProgressHeader: "### In progress",
|
|
358
|
+
statusAgentBusy: "🟡 BUSY",
|
|
359
|
+
statusAgentIdle: "⚪ IDLE",
|
|
360
|
+
statusNoTask: "No task",
|
|
361
|
+
// INBOX.md
|
|
362
|
+
inboxDone: (ts, id, agent) => `## [${ts}] ${id} completed — ${agent}`,
|
|
363
|
+
inboxTaskLabel: "- **Task:**",
|
|
364
|
+
inboxDurationLabel: "- **Duration:**",
|
|
365
|
+
inboxReportLabel: "- **Report:**",
|
|
366
|
+
inboxActionLabel: (file) => `- **Action:** Read \`${file}\` and create next TASKs if applicable.`,
|
|
367
|
+
inboxFailed: (ts, id, from, to) => `## [${ts}] ${id} failed — ${from} → reassigned to ${to}`,
|
|
368
|
+
inboxReasonLabel: "- **Reason:**",
|
|
369
|
+
inboxNewAgentLabel: "- **New agent:**",
|
|
370
|
+
inboxFailAction: "- **Action:** TUI reassigned automatically. Check QUEUE.md or wait for the next completion notification.",
|
|
219
371
|
},
|
|
220
372
|
};
|
|
221
373
|
const L = TEXT[WORKSPACE_LANGUAGE];
|
|
@@ -279,6 +431,38 @@ if (!fs.existsSync(LOG_DIR)) fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
|
279
431
|
const LOCK_FILE = path.join(LOG_DIR, "orchestrator.lock");
|
|
280
432
|
const STATE_FILE = path.join(LOG_DIR, "orchestrator-state.json");
|
|
281
433
|
const CONTROL_FILE = path.join(LOG_DIR, "orchestrator-control.json");
|
|
434
|
+
const STATUS_FILE = path.join(WORKSPACE, "STATUS.md");
|
|
435
|
+
|
|
436
|
+
function updateStatusFile() {
|
|
437
|
+
const statusLines = [
|
|
438
|
+
L.statusTitle(timestamp()),
|
|
439
|
+
``,
|
|
440
|
+
`${L.statusProject} ${PROJECT_NAME}`,
|
|
441
|
+
`${L.statusState} ${state.paused ? L.statusPausedLabel : L.statusRunning}`,
|
|
442
|
+
`${L.statusActiveTime} ${formatDuration(Math.round((Date.now() - state.startTime) / 1000))}`,
|
|
443
|
+
``,
|
|
444
|
+
L.statusSectionAgents,
|
|
445
|
+
``,
|
|
446
|
+
];
|
|
447
|
+
for (const [name, ag] of Object.entries(state.agents)) {
|
|
448
|
+
const status = ag.status === "busy" ? L.statusAgentBusy : L.statusAgentIdle;
|
|
449
|
+
const task = ag.task ? `${ag.task.id}: ${ag.task.title}` : L.statusNoTask;
|
|
450
|
+
statusLines.push(`- **${name}**: ${status} - ${task}`);
|
|
451
|
+
}
|
|
452
|
+
statusLines.push("", L.statusSectionQueue);
|
|
453
|
+
statusLines.push("", `${L.statusPendingLabel} ${state.queue.length}`);
|
|
454
|
+
statusLines.push("", `${L.statusCompletedLabel} ${state.completed.length}`);
|
|
455
|
+
statusLines.push("", `${L.statusInProgressLabel} ${state.inProgress.length}`);
|
|
456
|
+
if (state.inProgress.length > 0) {
|
|
457
|
+
statusLines.push("", L.statusInProgressHeader);
|
|
458
|
+
for (const t of state.inProgress) {
|
|
459
|
+
statusLines.push(`- ${t.id}: ${t.title} (${t.agent})`);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
try {
|
|
463
|
+
fs.writeFileSync(STATUS_FILE, statusLines.join("\n"), "utf-8");
|
|
464
|
+
} catch {}
|
|
465
|
+
}
|
|
282
466
|
|
|
283
467
|
// Limpiar control.json orphan al iniciar (si el proceso anterior fechou mal)
|
|
284
468
|
if (fs.existsSync(CONTROL_FILE)) {
|
|
@@ -373,6 +557,7 @@ for (const name of Object.keys(AGENTS)) {
|
|
|
373
557
|
lastLine: "",
|
|
374
558
|
exitCode: null,
|
|
375
559
|
cost: null,
|
|
560
|
+
totalCost: 0,
|
|
376
561
|
turns: 0,
|
|
377
562
|
};
|
|
378
563
|
}
|
|
@@ -486,6 +671,7 @@ function persistState() {
|
|
|
486
671
|
lastLine: ag.lastLine,
|
|
487
672
|
exitCode: ag.exitCode,
|
|
488
673
|
cost: ag.cost,
|
|
674
|
+
totalCost: ag.totalCost || 0,
|
|
489
675
|
turns: ag.turns,
|
|
490
676
|
},
|
|
491
677
|
]),
|
|
@@ -611,18 +797,28 @@ function renderDashboard() {
|
|
|
611
797
|
lines.push("");
|
|
612
798
|
|
|
613
799
|
for (const [name, ag] of Object.entries(state.agents)) {
|
|
614
|
-
const
|
|
615
|
-
|
|
800
|
+
const lastLine = ag.lastLine || "";
|
|
801
|
+
const isFailed = lastLine.startsWith("FALLÓ:") || lastLine.startsWith("FAILED:");
|
|
802
|
+
const isRetrying = lastLine.startsWith("REINTENTO:") || lastLine.startsWith("LÍMITE:") || lastLine.startsWith("RETRY:") || lastLine.startsWith("LIMIT:");
|
|
803
|
+
let status, detail, dot;
|
|
616
804
|
if (ag.status === "busy") {
|
|
617
805
|
status = `{yellow-fg}${L.busy}{/yellow-fg}`;
|
|
618
806
|
detail = `${ag.task?.id || "?"} ${(ag.task?.title || "").slice(0, 35)} (${elapsedSince(ag.startTime)})`;
|
|
807
|
+
dot = "{green-fg}●{/green-fg}";
|
|
808
|
+
} else if (isFailed) {
|
|
809
|
+
status = `{red-fg}${L.failed}{/red-fg}`;
|
|
810
|
+
detail = lastLine;
|
|
811
|
+
dot = "{red-fg}✕{/red-fg}";
|
|
812
|
+
} else if (isRetrying) {
|
|
813
|
+
status = `{yellow-fg}${L.retrying}{/yellow-fg}`;
|
|
814
|
+
detail = lastLine;
|
|
815
|
+
dot = "{yellow-fg}⟳{/yellow-fg}";
|
|
619
816
|
} else {
|
|
620
817
|
status = `{gray-fg}${L.idle}{/gray-fg}`;
|
|
621
|
-
detail =
|
|
818
|
+
detail = lastLine;
|
|
819
|
+
dot = "{gray-fg}○{/gray-fg}";
|
|
622
820
|
}
|
|
623
|
-
|
|
624
|
-
ag.status === "busy" ? "{green-fg}●{/green-fg}" : "{gray-fg}○{/gray-fg}";
|
|
625
|
-
lines.push(` ${dot} {bold}${name}{/bold} ${status} ${detail}`);
|
|
821
|
+
lines.push(` ${dot} {bold}${name}{/bold} ${status} ${escBl(detail)}`);
|
|
626
822
|
}
|
|
627
823
|
lines.push("");
|
|
628
824
|
|
|
@@ -691,15 +887,25 @@ function renderDashboard() {
|
|
|
691
887
|
|
|
692
888
|
for (const [name, ag] of Object.entries(state.agents)) {
|
|
693
889
|
const box = agentBoxes[name];
|
|
890
|
+
const lastLine = ag.lastLine || "";
|
|
891
|
+
const isFailed = lastLine.startsWith("FALLÓ:") || lastLine.startsWith("FAILED:");
|
|
892
|
+
const isRetrying = lastLine.startsWith("REINTENTO:") || lastLine.startsWith("LÍMITE:") || lastLine.startsWith("RETRY:") || lastLine.startsWith("LIMIT:");
|
|
694
893
|
if (ag.status === "busy") {
|
|
695
894
|
box.style.border.fg = "yellow";
|
|
696
895
|
box.setLabel(
|
|
697
|
-
` {bold}${escBl(name)}{/bold} {yellow-fg}
|
|
896
|
+
` {bold}${escBl(name)}{/bold} {yellow-fg}${L.busy}{/yellow-fg} ${escBl(ag.task?.id || "")} `,
|
|
698
897
|
);
|
|
898
|
+
} else if (isFailed) {
|
|
899
|
+
box.style.border.fg = "red";
|
|
900
|
+
box.setLabel(` {bold}${escBl(name)}{/bold} {red-fg}${L.failed}{/red-fg} `);
|
|
901
|
+
} else if (isRetrying) {
|
|
902
|
+
box.style.border.fg = "yellow";
|
|
903
|
+
box.setLabel(` {bold}${escBl(name)}{/bold} {yellow-fg}${L.retrying}{/yellow-fg} `);
|
|
699
904
|
} else {
|
|
700
905
|
box.style.border.fg = "gray";
|
|
906
|
+
const agCostLabel = ag.totalCost > 0 ? ` {gray-fg}$${ag.totalCost.toFixed(2)}{/gray-fg}` : "";
|
|
701
907
|
box.setLabel(
|
|
702
|
-
` {bold}${escBl(name)}{/bold} {gray-fg}
|
|
908
|
+
` {bold}${escBl(name)}{/bold} {gray-fg}${L.idle}{/gray-fg}${agCostLabel} `,
|
|
703
909
|
);
|
|
704
910
|
}
|
|
705
911
|
}
|
|
@@ -828,12 +1034,12 @@ function writeInboxNotification(task, agentName, elapsed) {
|
|
|
828
1034
|
const progressFile = `progress/PROGRESS-${agentName}.md`;
|
|
829
1035
|
const entry = [
|
|
830
1036
|
``,
|
|
831
|
-
|
|
1037
|
+
L.inboxDone(timestamp(), task.id, agentName),
|
|
832
1038
|
``,
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
1039
|
+
`${L.inboxTaskLabel} ${task.title}`,
|
|
1040
|
+
`${L.inboxDurationLabel} ${formatDuration(elapsed)}`,
|
|
1041
|
+
`${L.inboxReportLabel} ${progressFile}`,
|
|
1042
|
+
L.inboxActionLabel(progressFile),
|
|
837
1043
|
``,
|
|
838
1044
|
].join("\n");
|
|
839
1045
|
try {
|
|
@@ -845,12 +1051,12 @@ function writeInboxFailureNotification(task, failedAgent, newAgent, reason) {
|
|
|
845
1051
|
const inboxFile = path.join(WORKSPACE, "INBOX.md");
|
|
846
1052
|
const entry = [
|
|
847
1053
|
``,
|
|
848
|
-
|
|
1054
|
+
L.inboxFailed(timestamp(), task.id, failedAgent, newAgent),
|
|
849
1055
|
``,
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
1056
|
+
`${L.inboxTaskLabel} ${task.title}`,
|
|
1057
|
+
`${L.inboxReasonLabel} ${reason}`,
|
|
1058
|
+
`${L.inboxNewAgentLabel} ${newAgent}`,
|
|
1059
|
+
L.inboxFailAction,
|
|
854
1060
|
``,
|
|
855
1061
|
].join("\n");
|
|
856
1062
|
try {
|
|
@@ -858,6 +1064,35 @@ function writeInboxFailureNotification(task, failedAgent, newAgent, reason) {
|
|
|
858
1064
|
} catch {}
|
|
859
1065
|
}
|
|
860
1066
|
|
|
1067
|
+
// GAP 2 — Move a task line from ## Pending to ## In Progress when it starts
|
|
1068
|
+
function moveTaskToInProgress(task) {
|
|
1069
|
+
if (!fs.existsSync(QUEUE_FILE)) return;
|
|
1070
|
+
try {
|
|
1071
|
+
const lines = fs.readFileSync(QUEUE_FILE, "utf-8").split("\n");
|
|
1072
|
+
const idMatcher = new RegExp(
|
|
1073
|
+
`^${task.id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(\\s|$|\\|)`,
|
|
1074
|
+
);
|
|
1075
|
+
// Remove from Pending (wherever it is) — keep line for insertion
|
|
1076
|
+
let taskLine = null;
|
|
1077
|
+
const without = lines.filter((l) => {
|
|
1078
|
+
if (idMatcher.test(l.trim())) {
|
|
1079
|
+
taskLine = l;
|
|
1080
|
+
return false;
|
|
1081
|
+
}
|
|
1082
|
+
return true;
|
|
1083
|
+
});
|
|
1084
|
+
if (!taskLine) return; // already moved or not found
|
|
1085
|
+
// Find In Progress section and insert after its header
|
|
1086
|
+
const idx = without.findIndex(
|
|
1087
|
+
(l) =>
|
|
1088
|
+
l.trim().startsWith("## In Progress") ||
|
|
1089
|
+
l.trim().startsWith("## En progreso"),
|
|
1090
|
+
);
|
|
1091
|
+
if (idx >= 0) without.splice(idx + 1, 0, taskLine);
|
|
1092
|
+
fs.writeFileSync(QUEUE_FILE, without.join("\n"), "utf-8");
|
|
1093
|
+
} catch {}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
861
1096
|
// ============================================================================
|
|
862
1097
|
// BRIEF GENERATOR
|
|
863
1098
|
// ============================================================================
|
|
@@ -1069,10 +1304,7 @@ function launchAgent(task) {
|
|
|
1069
1304
|
const agentCfg = AGENTS[agentName];
|
|
1070
1305
|
|
|
1071
1306
|
if (!ag || !agentCfg) {
|
|
1072
|
-
log(
|
|
1073
|
-
"ERROR",
|
|
1074
|
-
`Agente desconocido en QUEUE: "${agentName}" — no está definido en orchestrator.config.json`,
|
|
1075
|
-
);
|
|
1307
|
+
log("ERROR", L.logUnknownAgent(task.id, agentName));
|
|
1076
1308
|
failedTasks.set(task.id, MAX_RETRIES); // don't retry — config bug, not transient
|
|
1077
1309
|
return false;
|
|
1078
1310
|
}
|
|
@@ -1094,10 +1326,10 @@ function launchAgent(task) {
|
|
|
1094
1326
|
log("START", `${agentName} (${cliCmd}) → ${task.id}: ${task.title}`);
|
|
1095
1327
|
appendToAgent(
|
|
1096
1328
|
agentName,
|
|
1097
|
-
`{cyan-fg}
|
|
1329
|
+
`{cyan-fg}${escBl(L.agentTaskHeader(task.id, task.title))}{/cyan-fg}`,
|
|
1098
1330
|
true,
|
|
1099
1331
|
);
|
|
1100
|
-
appendToAgent(agentName, `{gray-fg}
|
|
1332
|
+
appendToAgent(agentName, `{gray-fg}${escBl(L.agentCwd(repoDir))}{/gray-fg}`, true);
|
|
1101
1333
|
appendToAgent(agentName, "", true);
|
|
1102
1334
|
|
|
1103
1335
|
try {
|
|
@@ -1112,8 +1344,8 @@ function launchAgent(task) {
|
|
|
1112
1344
|
proc.stdin.end();
|
|
1113
1345
|
|
|
1114
1346
|
const timeout = setTimeout(() => {
|
|
1115
|
-
log("WARN",
|
|
1116
|
-
appendToAgent(agentName,
|
|
1347
|
+
log("WARN", L.logTimeout(agentName, task.id));
|
|
1348
|
+
appendToAgent(agentName, `{red-fg}${escBl(L.agentTimeout)}{/red-fg}`, true);
|
|
1117
1349
|
try {
|
|
1118
1350
|
proc.kill("SIGTERM");
|
|
1119
1351
|
} catch {}
|
|
@@ -1159,8 +1391,22 @@ function launchAgent(task) {
|
|
|
1159
1391
|
}
|
|
1160
1392
|
}
|
|
1161
1393
|
}
|
|
1394
|
+
// Claude: result event with total_cost_usd
|
|
1162
1395
|
if (event.type === "result" && event.total_cost_usd)
|
|
1163
1396
|
ag.cost = event.total_cost_usd;
|
|
1397
|
+
// Codex: direct cost_usd field (any event)
|
|
1398
|
+
if (event.cost_usd != null && ag.cost == null)
|
|
1399
|
+
ag.cost = event.cost_usd;
|
|
1400
|
+
if (event.usage?.cost_usd != null && ag.cost == null)
|
|
1401
|
+
ag.cost = event.usage.cost_usd;
|
|
1402
|
+
// Codex: token-based cost calculation from usage event
|
|
1403
|
+
if (ag.cost == null && event.usage &&
|
|
1404
|
+
(event.type === "usage" || event.type === "result" || event.type === "response.completed")) {
|
|
1405
|
+
const usageObj = event.usage.usage || event.usage;
|
|
1406
|
+
const model = agentCfg.model || "";
|
|
1407
|
+
const calc = calcOpenAICost(model, usageObj);
|
|
1408
|
+
if (calc != null) ag.cost = calc;
|
|
1409
|
+
}
|
|
1164
1410
|
// OpenCode events
|
|
1165
1411
|
if (event.type === "text" && event.part?.text)
|
|
1166
1412
|
appendToAgent(agentName, event.part.text.slice(0, 120));
|
|
@@ -1230,6 +1476,7 @@ function launchAgent(task) {
|
|
|
1230
1476
|
ag.turns = 0;
|
|
1231
1477
|
task.status = "running";
|
|
1232
1478
|
state.inProgress.push(task);
|
|
1479
|
+
moveTaskToInProgress(task); // GAP 2: reflect in QUEUE.md
|
|
1233
1480
|
renderDashboard();
|
|
1234
1481
|
return true;
|
|
1235
1482
|
} catch (err) {
|
|
@@ -1251,7 +1498,10 @@ function completeTask(task, agentName) {
|
|
|
1251
1498
|
const elapsed = ag.startTime
|
|
1252
1499
|
? Math.round((Date.now() - ag.startTime) / 1000)
|
|
1253
1500
|
: 0;
|
|
1254
|
-
if (ag.cost)
|
|
1501
|
+
if (ag.cost) {
|
|
1502
|
+
state.totalCost += ag.cost;
|
|
1503
|
+
ag.totalCost = (ag.totalCost || 0) + ag.cost;
|
|
1504
|
+
}
|
|
1255
1505
|
task.status = "completed";
|
|
1256
1506
|
task.completedAt = timestamp();
|
|
1257
1507
|
task.elapsed = elapsed;
|
|
@@ -1259,21 +1509,18 @@ function completeTask(task, agentName) {
|
|
|
1259
1509
|
state.inProgress = state.inProgress.filter((t) => t.id !== task.id);
|
|
1260
1510
|
state.completed.push(task);
|
|
1261
1511
|
const costStr = ag.cost ? ` ($${ag.cost.toFixed(2)})` : "";
|
|
1262
|
-
log(
|
|
1263
|
-
"DONE",
|
|
1264
|
-
`${agentName} completó ${task.id} en ${formatDuration(elapsed)}${costStr}`,
|
|
1265
|
-
);
|
|
1512
|
+
log("DONE", L.logDone(agentName, task.id, formatDuration(elapsed), costStr));
|
|
1266
1513
|
appendToAgent(agentName, "", true);
|
|
1267
1514
|
appendToAgent(
|
|
1268
1515
|
agentName,
|
|
1269
|
-
`{green-fg}
|
|
1516
|
+
`{green-fg}${escBl(L.agentCompleted(formatDuration(elapsed), costStr))}{/green-fg}`,
|
|
1270
1517
|
true,
|
|
1271
1518
|
);
|
|
1272
1519
|
ag.status = "idle";
|
|
1273
1520
|
ag.task = null;
|
|
1274
1521
|
ag.process = null;
|
|
1275
1522
|
ag.startTime = null;
|
|
1276
|
-
ag.lastLine =
|
|
1523
|
+
ag.lastLine = L.lastCompleted(task.id);
|
|
1277
1524
|
updateQueueFile(task);
|
|
1278
1525
|
writeInboxNotification(task, agentName, elapsed);
|
|
1279
1526
|
scheduleNext();
|
|
@@ -1337,21 +1584,18 @@ function failTask(task, agentName, code) {
|
|
|
1337
1584
|
if (rl.isRateLimit) {
|
|
1338
1585
|
const resetStr = rl.resetsAt
|
|
1339
1586
|
? `resets ${rl.resetsAt.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", hour12: true })}`
|
|
1340
|
-
: "
|
|
1341
|
-
log("RATE",
|
|
1587
|
+
: "retry in 10 min";
|
|
1588
|
+
log("RATE", L.logRateLimit(agentName, task.id, resetStr));
|
|
1342
1589
|
appendToAgent(
|
|
1343
1590
|
agentName,
|
|
1344
|
-
`{yellow-fg}
|
|
1591
|
+
`{yellow-fg}${escBl(L.agentRateLimit(resetStr))}{/yellow-fg}`,
|
|
1345
1592
|
true,
|
|
1346
1593
|
);
|
|
1347
1594
|
} else {
|
|
1348
|
-
log(
|
|
1349
|
-
"FAIL",
|
|
1350
|
-
`${agentName} falló ${task.id} (salida ${code}, ${retries}/${maxRetries})`,
|
|
1351
|
-
);
|
|
1595
|
+
log("FAIL", L.logFail(agentName, task.id, code, retries, maxRetries));
|
|
1352
1596
|
appendToAgent(
|
|
1353
1597
|
agentName,
|
|
1354
|
-
`{red-fg}
|
|
1598
|
+
`{red-fg}${escBl(L.agentFailed(code, retries))}{/red-fg}`,
|
|
1355
1599
|
true,
|
|
1356
1600
|
);
|
|
1357
1601
|
}
|
|
@@ -1365,14 +1609,17 @@ function failTask(task, agentName, code) {
|
|
|
1365
1609
|
["Codex", "OpenCode"].includes(agentName) &&
|
|
1366
1610
|
(failureFlags.exhaustedQuota ||
|
|
1367
1611
|
failureFlags.providerUnavailable ||
|
|
1612
|
+
failureFlags.noRealWork ||
|
|
1368
1613
|
retries >= maxRetries);
|
|
1369
1614
|
|
|
1370
1615
|
if (shouldFallback) {
|
|
1371
1616
|
const reason = failureFlags.exhaustedQuota
|
|
1372
|
-
?
|
|
1617
|
+
? L.reasonQuota
|
|
1373
1618
|
: failureFlags.providerUnavailable
|
|
1374
|
-
?
|
|
1375
|
-
:
|
|
1619
|
+
? L.reasonProvider
|
|
1620
|
+
: failureFlags.noRealWork
|
|
1621
|
+
? L.reasonNoWork
|
|
1622
|
+
: L.reasonPersistent;
|
|
1376
1623
|
if (tryFallbackToAlternative(task, agentName, reason)) {
|
|
1377
1624
|
writeInboxFailureNotification(task, agentName, task.agent, reason);
|
|
1378
1625
|
setTimeout(() => {
|
|
@@ -1385,8 +1632,8 @@ function failTask(task, agentName, code) {
|
|
|
1385
1632
|
|
|
1386
1633
|
if (retries >= maxRetries) {
|
|
1387
1634
|
task.status = "failed";
|
|
1388
|
-
ag.lastLine =
|
|
1389
|
-
log("ERROR",
|
|
1635
|
+
ag.lastLine = L.lastFailed(task.id);
|
|
1636
|
+
log("ERROR", L.logPermanentFail(task.id, retries));
|
|
1390
1637
|
} else {
|
|
1391
1638
|
task.status = "pending";
|
|
1392
1639
|
let retryDelay = rl.isRateLimit
|
|
@@ -1402,8 +1649,8 @@ function failTask(task, agentName, code) {
|
|
|
1402
1649
|
hour12: true,
|
|
1403
1650
|
});
|
|
1404
1651
|
ag.lastLine = rl.isRateLimit
|
|
1405
|
-
?
|
|
1406
|
-
:
|
|
1652
|
+
? L.lastLimit(task.id, retryTime)
|
|
1653
|
+
: L.lastRetry(task.id);
|
|
1407
1654
|
if (rl.isRateLimit) rateLimitedAgents.set(agentName, task._retryAfter);
|
|
1408
1655
|
}
|
|
1409
1656
|
if (rl.isRateLimit && task._retryAfter) {
|
|
@@ -1459,11 +1706,13 @@ function scheduleNext() {
|
|
|
1459
1706
|
function updateQueueFile(completedTask) {
|
|
1460
1707
|
if (!fs.existsSync(QUEUE_FILE)) return;
|
|
1461
1708
|
const lines = fs.readFileSync(QUEUE_FILE, "utf-8").split("\n");
|
|
1462
|
-
//
|
|
1709
|
+
// Word-boundary match so TASK-1 does NOT also remove TASK-10, TASK-11, etc.
|
|
1463
1710
|
const idMatcher = new RegExp(
|
|
1464
1711
|
`^${completedTask.id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(\\s|$|\\|)`,
|
|
1465
1712
|
);
|
|
1713
|
+
// Remove task from both Pending and In Progress sections
|
|
1466
1714
|
const filtered = lines.filter((l) => !idMatcher.test(l.trim()));
|
|
1715
|
+
// Find Completed section and insert entry
|
|
1467
1716
|
const idx = filtered.findIndex(
|
|
1468
1717
|
(l) =>
|
|
1469
1718
|
l.trim().startsWith("## Completed") ||
|
|
@@ -1512,6 +1761,14 @@ function detectSupportAgentFailure(agentName) {
|
|
|
1512
1761
|
}
|
|
1513
1762
|
|
|
1514
1763
|
const lower = content.toLowerCase();
|
|
1764
|
+
|
|
1765
|
+
const hasToolUses = content.includes("Write") || content.includes("Read") ||
|
|
1766
|
+
content.includes("Bash") || content.includes("Edit") || content.includes("ToolUse");
|
|
1767
|
+
const hasFilesModified = content.includes("files_modified") &&
|
|
1768
|
+
!content.includes("files_modified: list") && !content.includes("files_modified: none");
|
|
1769
|
+
const outputTooShort = ag.output && ag.output.length < 200;
|
|
1770
|
+
const noRealWork = outputTooShort && !hasToolUses && !hasFilesModified;
|
|
1771
|
+
|
|
1515
1772
|
return {
|
|
1516
1773
|
exhaustedQuota:
|
|
1517
1774
|
lower.includes("out of extra usage") ||
|
|
@@ -1519,7 +1776,9 @@ function detectSupportAgentFailure(agentName) {
|
|
|
1519
1776
|
lower.includes("quota") ||
|
|
1520
1777
|
lower.includes("rate_limit") ||
|
|
1521
1778
|
lower.includes("ratelimitexceeded") ||
|
|
1522
|
-
lower.includes("429")
|
|
1779
|
+
lower.includes("429") ||
|
|
1780
|
+
lower.includes("insufficient credits") ||
|
|
1781
|
+
lower.includes("no credits"),
|
|
1523
1782
|
providerUnavailable:
|
|
1524
1783
|
lower.includes("session expired") ||
|
|
1525
1784
|
lower.includes("authentication") ||
|
|
@@ -1532,6 +1791,7 @@ function detectSupportAgentFailure(agentName) {
|
|
|
1532
1791
|
lower.includes("timed out") ||
|
|
1533
1792
|
lower.includes("timeout") ||
|
|
1534
1793
|
lower.includes("network error"),
|
|
1794
|
+
noRealWork: noRealWork || lower.includes("no files") || lower.includes("nothing to"),
|
|
1535
1795
|
};
|
|
1536
1796
|
}
|
|
1537
1797
|
|
|
@@ -1586,20 +1846,14 @@ function tryFallbackToAlternative(task, failedAgentName, reason) {
|
|
|
1586
1846
|
failedTasks.set(task.id, 0);
|
|
1587
1847
|
state.queue.push(task);
|
|
1588
1848
|
|
|
1589
|
-
log(
|
|
1590
|
-
"FALLBACK",
|
|
1591
|
-
`${task.id} reasignada de ${failedAgentName} a ${targetAgent} (${reason})`,
|
|
1592
|
-
);
|
|
1849
|
+
log("FALLBACK", L.logFallback(task.id, failedAgentName, targetAgent, reason));
|
|
1593
1850
|
appendToAgent(
|
|
1594
1851
|
failedAgentName,
|
|
1595
|
-
`{yellow-fg}
|
|
1852
|
+
`{yellow-fg}${escBl(L.agentReassigned(targetAgent, reason))}{/yellow-fg}`,
|
|
1596
1853
|
true,
|
|
1597
1854
|
);
|
|
1598
1855
|
if (!queueUpdated) {
|
|
1599
|
-
log(
|
|
1600
|
-
"WARN",
|
|
1601
|
-
`${task.id} reasignada a ${targetAgent}, pero QUEUE.md no pudo actualizarse`,
|
|
1602
|
-
);
|
|
1856
|
+
log("WARN", L.logReassignWarn(task.id, targetAgent));
|
|
1603
1857
|
}
|
|
1604
1858
|
return true;
|
|
1605
1859
|
}
|
|
@@ -1612,23 +1866,11 @@ if (!CLI.headless && screen) {
|
|
|
1612
1866
|
exitWithSummary();
|
|
1613
1867
|
});
|
|
1614
1868
|
|
|
1615
|
-
screen.key("s", () => {
|
|
1616
|
-
if (state.paused) {
|
|
1617
|
-
state.paused = false;
|
|
1618
|
-
log("INFO", "Reanudado");
|
|
1619
|
-
}
|
|
1620
|
-
scheduleNext();
|
|
1621
|
-
safeRenderDashboard();
|
|
1622
|
-
});
|
|
1623
1869
|
screen.key("p", () => {
|
|
1624
1870
|
state.paused = !state.paused;
|
|
1625
1871
|
log("INFO", state.paused ? L.paused : L.resumed);
|
|
1626
1872
|
safeRenderDashboard();
|
|
1627
|
-
|
|
1628
|
-
screen.key("r", () => {
|
|
1629
|
-
reloadQueue();
|
|
1630
|
-
log("INFO", L.queueReloaded(state.queue.length));
|
|
1631
|
-
safeRenderDashboard();
|
|
1873
|
+
updateStatusFile();
|
|
1632
1874
|
});
|
|
1633
1875
|
}
|
|
1634
1876
|
|
|
@@ -1641,6 +1883,7 @@ log("INFO", L.loadedCompleted(state.completed.length));
|
|
|
1641
1883
|
reloadQueue();
|
|
1642
1884
|
log("INFO", `${L.queue}: ${state.queue.length} ${L.tasks}`);
|
|
1643
1885
|
renderDashboard();
|
|
1886
|
+
updateStatusFile();
|
|
1644
1887
|
if (!state.paused) {
|
|
1645
1888
|
scheduleNext();
|
|
1646
1889
|
renderDashboard();
|
|
@@ -1656,14 +1899,18 @@ setInterval(() => {
|
|
|
1656
1899
|
if (!state.paused) scheduleNext();
|
|
1657
1900
|
renderDashboard();
|
|
1658
1901
|
}, POLL_INTERVAL_MS);
|
|
1902
|
+
|
|
1903
|
+
setInterval(() => {
|
|
1904
|
+
updateStatusFile();
|
|
1905
|
+
}, 60000); // Update STATUS.md cada 60 segundos
|
|
1659
1906
|
setInterval(() => {
|
|
1660
1907
|
for (const [name, ag] of Object.entries(state.agents)) {
|
|
1661
1908
|
if (ag.status !== "busy" || !ag.process) continue;
|
|
1662
1909
|
try {
|
|
1663
1910
|
process.kill(ag.process.pid, 0);
|
|
1664
1911
|
} catch {
|
|
1665
|
-
log("WARN",
|
|
1666
|
-
appendToAgent(name,
|
|
1912
|
+
log("WARN", L.logDied(name, ag.task?.id));
|
|
1913
|
+
appendToAgent(name, `{red-fg}${escBl(L.agentDied)}{/red-fg}`, true);
|
|
1667
1914
|
ag.process = null;
|
|
1668
1915
|
failTask(ag.task, name, -1);
|
|
1669
1916
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@liriraid/agentflow-ai",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.15",
|
|
4
4
|
"description": "Multi-agent workspace orchestrator with TUI. Coordinates AI coding agents over your real frontend and backend projects.",
|
|
5
5
|
"author": "LiriRaid",
|
|
6
6
|
"homepage": "https://github.com/LiriRaid/agentflow-ai#readme",
|