bosun 0.35.2 → 0.35.3
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/README.md +14 -1
- package/agent-hooks.mjs +7 -1
- package/agent-pool.mjs +16 -0
- package/agent-prompts.mjs +190 -4
- package/agent-sdk.mjs +6 -1
- package/agent-work-analyzer.mjs +48 -9
- package/autofix.mjs +32 -18
- package/bosun.schema.json +1 -1
- package/kanban-adapter.mjs +62 -12
- package/monitor.mjs +25 -6
- package/opencode-shell.mjs +881 -0
- package/package.json +5 -2
- package/primary-agent.mjs +43 -0
- package/setup.mjs +33 -4
- package/task-executor.mjs +43 -14
- package/ui/app.js +10 -7
- package/ui/components/chat-view.js +31 -9
- package/ui/components/session-list.js +20 -4
- package/ui/modules/router.js +2 -0
- package/ui/tabs/agents.js +66 -8
- package/ui-server.mjs +142 -5
- package/workflow-engine.mjs +664 -10
- package/workflow-nodes.mjs +250 -1
- package/workflow-templates/github.mjs +389 -71
- package/workflow-templates/planning.mjs +31 -11
- package/workflow-templates.mjs +3 -0
package/ui-server.mjs
CHANGED
|
@@ -355,6 +355,15 @@ async function getWorkflowEngineModule() {
|
|
|
355
355
|
};
|
|
356
356
|
_wfEngine.getWorkflowEngine({ services });
|
|
357
357
|
_wfServicesReady = true;
|
|
358
|
+
|
|
359
|
+
// Resume any runs that were interrupted by a previous shutdown.
|
|
360
|
+
// This must happen AFTER services are wired so node executors work.
|
|
361
|
+
const engine = _wfEngine.getWorkflowEngine();
|
|
362
|
+
if (typeof engine.resumeInterruptedRuns === "function") {
|
|
363
|
+
engine.resumeInterruptedRuns().catch((err) => {
|
|
364
|
+
console.warn("[workflows] Failed to resume interrupted runs:", err.message);
|
|
365
|
+
});
|
|
366
|
+
}
|
|
358
367
|
} catch (err) {
|
|
359
368
|
console.warn("[workflows] services setup failed (engine still usable):", err.message);
|
|
360
369
|
}
|
|
@@ -1361,6 +1370,28 @@ let uiInstanceLockHeld = false;
|
|
|
1361
1370
|
const logStreamers = new Map();
|
|
1362
1371
|
let uiDeps = {};
|
|
1363
1372
|
|
|
1373
|
+
/**
|
|
1374
|
+
* Resolve the execPrimaryPrompt function. Prefers the injected dependency,
|
|
1375
|
+
* falls back to importing directly from primary-agent.mjs so the chat
|
|
1376
|
+
* agent works even when the UI server starts standalone.
|
|
1377
|
+
*/
|
|
1378
|
+
let _fallbackExecPrimaryPrompt = null;
|
|
1379
|
+
async function resolveExecPrimaryPrompt() {
|
|
1380
|
+
if (typeof uiDeps.execPrimaryPrompt === "function") return uiDeps.execPrimaryPrompt;
|
|
1381
|
+
if (_fallbackExecPrimaryPrompt) return _fallbackExecPrimaryPrompt;
|
|
1382
|
+
try {
|
|
1383
|
+
const mod = await import("./primary-agent.mjs");
|
|
1384
|
+
if (typeof mod.execPrimaryPrompt === "function") {
|
|
1385
|
+
_fallbackExecPrimaryPrompt = mod.execPrimaryPrompt;
|
|
1386
|
+
console.log("[ui-server] loaded execPrimaryPrompt fallback from primary-agent.mjs");
|
|
1387
|
+
return _fallbackExecPrimaryPrompt;
|
|
1388
|
+
}
|
|
1389
|
+
} catch (err) {
|
|
1390
|
+
console.warn("[ui-server] failed to load execPrimaryPrompt fallback:", err.message);
|
|
1391
|
+
}
|
|
1392
|
+
return null;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1364
1395
|
/**
|
|
1365
1396
|
* Resolve the bosun config directory. Falls back through:
|
|
1366
1397
|
* 1. uiDeps.configDir (injected at server start)
|
|
@@ -6302,11 +6333,68 @@ async function handleApi(req, res, url) {
|
|
|
6302
6333
|
const wfMod = await getWorkflowEngine();
|
|
6303
6334
|
if (!wfMod) { jsonResponse(res, 503, { ok: false, error: "Workflow engine not available" }); return; }
|
|
6304
6335
|
const engine = wfMod.getWorkflowEngine();
|
|
6305
|
-
const
|
|
6336
|
+
const subPath = path.replace("/api/workflows/runs/", "");
|
|
6337
|
+
const segments = subPath.split("/").map(decodeURIComponent);
|
|
6338
|
+
const runId = (segments[0] || "").trim();
|
|
6339
|
+
const action = (segments[1] || "").trim();
|
|
6340
|
+
|
|
6306
6341
|
if (!runId) {
|
|
6307
6342
|
jsonResponse(res, 400, { ok: false, error: "runId is required" });
|
|
6308
6343
|
return;
|
|
6309
6344
|
}
|
|
6345
|
+
|
|
6346
|
+
// ── POST /api/workflows/runs/:id/retry ──────────────────────────
|
|
6347
|
+
// Manual retry endpoint. Accepts { mode: "from_failed" | "from_scratch" }.
|
|
6348
|
+
// If mode is omitted, returns available retry options so the UI can
|
|
6349
|
+
// present a choice to the user.
|
|
6350
|
+
if (action === "retry" && req.method === "POST") {
|
|
6351
|
+
const run = engine.getRunDetail ? engine.getRunDetail(runId) : null;
|
|
6352
|
+
if (!run) {
|
|
6353
|
+
jsonResponse(res, 404, { ok: false, error: "Workflow run not found" });
|
|
6354
|
+
return;
|
|
6355
|
+
}
|
|
6356
|
+
if (run.status !== "failed") {
|
|
6357
|
+
jsonResponse(res, 400, { ok: false, error: `Run status is "${run.status}" — only failed runs can be retried` });
|
|
6358
|
+
return;
|
|
6359
|
+
}
|
|
6360
|
+
const body = await readJsonBody(req);
|
|
6361
|
+
const mode = body?.mode;
|
|
6362
|
+
if (!mode) {
|
|
6363
|
+
// No mode specified — return available retry options so the UI can
|
|
6364
|
+
// present a picker (from scratch vs from failed step).
|
|
6365
|
+
const failedNodes = [];
|
|
6366
|
+
const nodeStatuses = run.detail?.nodeStatuses || {};
|
|
6367
|
+
for (const [nodeId, status] of Object.entries(nodeStatuses)) {
|
|
6368
|
+
if (status === "failed") failedNodes.push(nodeId);
|
|
6369
|
+
}
|
|
6370
|
+
jsonResponse(res, 200, {
|
|
6371
|
+
ok: true,
|
|
6372
|
+
runId,
|
|
6373
|
+
status: run.status,
|
|
6374
|
+
options: [
|
|
6375
|
+
{ mode: "from_failed", label: "Retry from last failed step", failedNodes },
|
|
6376
|
+
{ mode: "from_scratch", label: "Retry from scratch" },
|
|
6377
|
+
],
|
|
6378
|
+
});
|
|
6379
|
+
return;
|
|
6380
|
+
}
|
|
6381
|
+
if (mode !== "from_failed" && mode !== "from_scratch") {
|
|
6382
|
+
jsonResponse(res, 400, { ok: false, error: `Invalid mode "${mode}". Use "from_failed" or "from_scratch".` });
|
|
6383
|
+
return;
|
|
6384
|
+
}
|
|
6385
|
+
const result = await engine.retryRun(runId, { mode });
|
|
6386
|
+
const retryStatus = result.ctx?.errors?.length > 0 ? "failed" : "completed";
|
|
6387
|
+
jsonResponse(res, 200, {
|
|
6388
|
+
ok: true,
|
|
6389
|
+
retryRunId: result.retryRunId,
|
|
6390
|
+
originalRunId: result.originalRunId,
|
|
6391
|
+
mode: result.mode,
|
|
6392
|
+
status: retryStatus,
|
|
6393
|
+
});
|
|
6394
|
+
return;
|
|
6395
|
+
}
|
|
6396
|
+
|
|
6397
|
+
// ── GET /api/workflows/runs/:id ─────────────────────────────────
|
|
6310
6398
|
const run = engine.getRunDetail ? engine.getRunDetail(runId) : null;
|
|
6311
6399
|
if (!run) {
|
|
6312
6400
|
jsonResponse(res, 404, { ok: false, error: "Workflow run not found" });
|
|
@@ -7217,7 +7305,26 @@ async function handleApi(req, res, url) {
|
|
|
7217
7305
|
jsonResponse(res, 404, { ok: false, error: "Session not found" });
|
|
7218
7306
|
return;
|
|
7219
7307
|
}
|
|
7220
|
-
|
|
7308
|
+
// Support ?limit=N&offset=N for message pagination
|
|
7309
|
+
const reqUrl = new URL(req.url, `http://${req.headers.host || "localhost"}`);
|
|
7310
|
+
const limitParam = reqUrl.searchParams.get("limit");
|
|
7311
|
+
const offsetParam = reqUrl.searchParams.get("offset");
|
|
7312
|
+
if (limitParam) {
|
|
7313
|
+
const limit = Math.max(1, Math.min(Number(limitParam) || 20, 500));
|
|
7314
|
+
const allMessages = session.messages || [];
|
|
7315
|
+
const total = allMessages.length;
|
|
7316
|
+
const offset = offsetParam != null
|
|
7317
|
+
? Math.max(0, Math.min(Number(offsetParam) || 0, total))
|
|
7318
|
+
: Math.max(0, total - limit);
|
|
7319
|
+
const sliced = allMessages.slice(offset, offset + limit);
|
|
7320
|
+
jsonResponse(res, 200, {
|
|
7321
|
+
ok: true,
|
|
7322
|
+
session: { ...session, messages: sliced },
|
|
7323
|
+
pagination: { total, offset, limit, hasMore: offset > 0 },
|
|
7324
|
+
});
|
|
7325
|
+
} else {
|
|
7326
|
+
jsonResponse(res, 200, { ok: true, session });
|
|
7327
|
+
}
|
|
7221
7328
|
} catch (err) {
|
|
7222
7329
|
jsonResponse(res, 500, { ok: false, error: err.message });
|
|
7223
7330
|
}
|
|
@@ -7306,12 +7413,35 @@ async function handleApi(req, res, url) {
|
|
|
7306
7413
|
const messageModel = body?.model || undefined;
|
|
7307
7414
|
|
|
7308
7415
|
// Forward to primary agent if applicable (exec records user + assistant events)
|
|
7309
|
-
|
|
7416
|
+
let exec = session.type === "primary" ? uiDeps.execPrimaryPrompt : null;
|
|
7417
|
+
// Fallback: resolve execPrimaryPrompt from primary-agent.mjs if not injected
|
|
7418
|
+
if (!exec && session.type === "primary") {
|
|
7419
|
+
exec = await resolveExecPrimaryPrompt();
|
|
7420
|
+
}
|
|
7310
7421
|
if (exec) {
|
|
7311
7422
|
// Don't record user event here — execPrimaryPrompt records it
|
|
7312
7423
|
// Respond immediately so the UI doesn't block on agent execution
|
|
7313
7424
|
jsonResponse(res, 200, { ok: true, messageId });
|
|
7314
7425
|
broadcastUiEvent(["sessions"], "invalidate", { reason: "session-message", sessionId });
|
|
7426
|
+
|
|
7427
|
+
// Build an onEvent callback so intermediate SDK events (thinking,
|
|
7428
|
+
// tool calls, code edits, etc.) are streamed to the UI in real-time
|
|
7429
|
+
// via the existing session-tracker → WebSocket listener pipeline.
|
|
7430
|
+
// Without this, chat/telegram dispatches only show the final
|
|
7431
|
+
// user+assistant pair instead of the full thought stream that Flows
|
|
7432
|
+
// clients see.
|
|
7433
|
+
const streamOnEvent = (err, event) => {
|
|
7434
|
+
// The adapters call onEvent(err, event) or onEvent(event).
|
|
7435
|
+
// Normalise both calling conventions.
|
|
7436
|
+
const ev = event || err;
|
|
7437
|
+
if (!ev) return;
|
|
7438
|
+
try {
|
|
7439
|
+
tracker.recordEvent(sessionId, ev);
|
|
7440
|
+
} catch {
|
|
7441
|
+
/* best-effort — never crash the agent loop */
|
|
7442
|
+
}
|
|
7443
|
+
};
|
|
7444
|
+
|
|
7315
7445
|
// Fire-and-forget: run agent asynchronously so the request handler
|
|
7316
7446
|
// doesn't block and the agent doesn't appear "busy" to subsequent
|
|
7317
7447
|
// messages from chat, telegram, portal, or any other source.
|
|
@@ -7322,6 +7452,7 @@ async function handleApi(req, res, url) {
|
|
|
7322
7452
|
model: messageModel,
|
|
7323
7453
|
attachments,
|
|
7324
7454
|
attachmentsAppended,
|
|
7455
|
+
onEvent: streamOnEvent,
|
|
7325
7456
|
}).then(() => {
|
|
7326
7457
|
broadcastUiEvent(["sessions"], "invalidate", { reason: "agent-response", sessionId });
|
|
7327
7458
|
}).catch((execErr) => {
|
|
@@ -7335,14 +7466,20 @@ async function handleApi(req, res, url) {
|
|
|
7335
7466
|
broadcastUiEvent(["sessions"], "invalidate", { reason: "agent-error", sessionId });
|
|
7336
7467
|
});
|
|
7337
7468
|
} else {
|
|
7338
|
-
// No agent — record user event and
|
|
7469
|
+
// No agent available — record user event and notify user
|
|
7339
7470
|
tracker.recordEvent(sessionId, {
|
|
7340
7471
|
role: "user",
|
|
7341
7472
|
content: messageContent,
|
|
7342
7473
|
attachments,
|
|
7343
7474
|
timestamp: new Date().toISOString(),
|
|
7344
7475
|
});
|
|
7345
|
-
|
|
7476
|
+
tracker.recordEvent(sessionId, {
|
|
7477
|
+
role: "system",
|
|
7478
|
+
type: "error",
|
|
7479
|
+
content: "⚠️ No agent is available to process this message. The primary agent may not be initialized — try restarting bosun or check the Logs tab for details.",
|
|
7480
|
+
timestamp: new Date().toISOString(),
|
|
7481
|
+
});
|
|
7482
|
+
jsonResponse(res, 200, { ok: true, messageId, warning: "no_agent_available" });
|
|
7346
7483
|
broadcastUiEvent(["sessions"], "invalidate", { reason: "session-message", sessionId });
|
|
7347
7484
|
}
|
|
7348
7485
|
} catch (err) {
|