@noobdemon/noob-cli 1.12.10 → 1.12.11
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/CHANGELOG.md +5 -0
- package/package.json +1 -1
- package/src/i18n.js +3 -0
- package/src/repl/agent-dispatch.js +41 -0
- package/src/repl/workflow-commands.js +2 -0
- package/src/repl.js +76 -2
- package/src/subagent.js +3 -0
- package/src/tools.js +5 -1
- package/src/workflow-bg.js +172 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
Tất cả thay đổi đáng kể của `@noobdemon/noob-cli` được ghi vào file này.
|
|
4
4
|
|
|
5
|
+
## [1.12.11] - 2026-06-13
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **Tool `workflow` — orchestration chạy NỀN (background), parity với Claude Code** (`src/workflow-bg.js` mới + `src/tools.js` + `src/repl/agent-dispatch.js` + `src/repl.js`): model gọi `workflow {prompt, name?}` → kick off headless `runAgent` chạy nền với own `AbortController` + own `TokenMeter`, trả về NGAY (không block turn). Khác `spawn_agents` (block lượt tới khi mọi sub-agent xong): `workflow` detach hẳn — user tiếp tục làm việc khác, kết quả consolidated tự chèn vào lượt sau qua `pending` queue. Registry session-scope (`createBgRegistry`), cap 3 run nền song song (vượt → tool trả ERROR), không lồng (bg dispatcher KHÔNG có bgRegistry → workflow nền không tự spawn workflow nền). Main loop drain completion giữa các lượt → notify + auto-inject. Exit CLI (Ctrl+C×2 hoặc /quit) → `sweepOnExit` mark mọi run active = interrupted (journal resume-able qua `/workflow resume <id>`). Lệnh mới: `/workflow active` (list run đang chạy), `/workflow stop <id>|all` (huỷ, resume được). Smoke `scripts/smoke-workflow-bg.mjs` 6/6 pass (start-returns-immediately, cap-3, drain-yields-result, stopBg-interrupted, sweepOnExit, empty-prompt-reject).
|
|
9
|
+
|
|
5
10
|
## [1.12.10] - 2026-06-13
|
|
6
11
|
|
|
7
12
|
### Fixed
|
package/package.json
CHANGED
package/src/i18n.js
CHANGED
|
@@ -205,6 +205,9 @@ export const t = {
|
|
|
205
205
|
workflowAgentAskPrompt:
|
|
206
206
|
' bật agent mode và chạy workflow? [y] có, bật & chạy / [n] huỷ (gõ /agent rồi chạy lại nếu muốn) › ',
|
|
207
207
|
workflowAgentEnabled: 'đã bật agent mode cho workflow này.',
|
|
208
|
+
bgWorkflowDone: (label) => `workflow nền ${label} đã xong`,
|
|
209
|
+
bgWorkflowFailed: (label, err) => `workflow nền ${label} lỗi: ${err}`,
|
|
210
|
+
bgWorkflowSweep: (n) => `${n} workflow nền bị ngắt — resume bằng /workflow resume <id>`,
|
|
208
211
|
workflowAgentDenied:
|
|
209
212
|
'đã huỷ /workflow — agent mode vẫn TẮT. Gõ /agent rồi chạy lại lệnh nếu muốn.',
|
|
210
213
|
// saved workflows (CRUD)
|
|
@@ -33,6 +33,9 @@ import { t } from '../i18n.js';
|
|
|
33
33
|
*/
|
|
34
34
|
export function createAgentDispatcher(deps) {
|
|
35
35
|
const { state, abort, tokenMeter, stopSpin, startSpin, execTool, tui, c } = deps;
|
|
36
|
+
// bgRegistry: tuỳ chọn — chỉ wire ở dispatcher của TURN (repl.js), KHÔNG ở bg
|
|
37
|
+
// dispatcher (tránh đệ quy: workflow nền spawn workflow nền). Thiếu → tool báo lỗi.
|
|
38
|
+
const bgRegistry = deps.bgRegistry || null;
|
|
36
39
|
// Test injection points: production luôn dùng default; smoke test pass mock.
|
|
37
40
|
const runSubAgent = deps.runSubAgent || defaultRunSubAgent;
|
|
38
41
|
const findModel = deps.findModel || defaultFindModel;
|
|
@@ -95,6 +98,44 @@ export function createAgentDispatcher(deps) {
|
|
|
95
98
|
};
|
|
96
99
|
}
|
|
97
100
|
|
|
101
|
+
// workflow: kick off orchestration NỀN, trả về NGAY. Cần agentMode (workflow
|
|
102
|
+
// spawn sub-agent) + bgRegistry (chỉ wire ở turn dispatcher, KHÔNG ở bg → chống
|
|
103
|
+
// workflow nền tự spawn workflow nền).
|
|
104
|
+
if (name === 'workflow') {
|
|
105
|
+
if (!bgRegistry)
|
|
106
|
+
return {
|
|
107
|
+
allow: true,
|
|
108
|
+
result: 'ERROR: workflow nền không khả dụng ở ngữ cảnh này (vd đang trong 1 workflow nền khác — không lồng).',
|
|
109
|
+
};
|
|
110
|
+
if (!state.agentMode)
|
|
111
|
+
return {
|
|
112
|
+
allow: true,
|
|
113
|
+
result: 'ERROR: agent mode đang TẮT — gõ /agent on trước khi chạy workflow nền.',
|
|
114
|
+
};
|
|
115
|
+
const prompt = input?.prompt || '';
|
|
116
|
+
const wfName = input?.name || null;
|
|
117
|
+
const r = bgRegistry.startBgWorkflow({ prompt, name: wfName });
|
|
118
|
+
if (!r.ok) {
|
|
119
|
+
if (r.error === 'cap')
|
|
120
|
+
return {
|
|
121
|
+
allow: true,
|
|
122
|
+
result: `ERROR: đã đạt ${bgRegistry.MAX_BG} workflow nền song song — chờ một run xong hoặc /workflow stop <id>.`,
|
|
123
|
+
};
|
|
124
|
+
if (r.error === 'empty_prompt')
|
|
125
|
+
return { allow: true, result: 'ERROR: workflow cần field "prompt" (yêu cầu cho workflow).' };
|
|
126
|
+
return { allow: true, result: 'ERROR khởi tạo workflow nền: ' + r.error };
|
|
127
|
+
}
|
|
128
|
+
stopSpin();
|
|
129
|
+
console.log(
|
|
130
|
+
chalk.hex('#8b5cf6')(` 🎼 workflow nền bắt đầu — run ${r.id}`)
|
|
131
|
+
);
|
|
132
|
+
startSpin(t.thinking);
|
|
133
|
+
return {
|
|
134
|
+
allow: true,
|
|
135
|
+
result: `workflow đã chạy NỀN (run ${r.id}). Trả về ngay — KHÔNG chờ. Kết quả consolidated sẽ tự chèn vào lượt sau khi xong. Huỷ: /workflow stop ${r.id}. Tiếp tục việc khác hoặc kết thúc reply.`,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
98
139
|
// spawn_agent / spawn_agents chỉ được phép khi agentMode bật; depth giới hạn
|
|
99
140
|
// bởi MAX_SUBAGENT_DEPTH để tránh đệ quy nổ.
|
|
100
141
|
if (name === 'spawn_agent' || name === 'spawn_agents') {
|
|
@@ -56,6 +56,8 @@ export function workflowHelp({ c, t }) {
|
|
|
56
56
|
console.log(' /workflow runs list run journal đã chạy (resume-able)');
|
|
57
57
|
console.log(' /workflow log <id> xem chi tiết 1 run (sub-agent task + kết quả)');
|
|
58
58
|
console.log(' /workflow resume <id> chạy lại run, skip task đã done → tiết kiệm token');
|
|
59
|
+
console.log(' /workflow active workflow nền đang chạy');
|
|
60
|
+
console.log(' /workflow stop <id> | all huỷ workflow nền (resume được)');
|
|
59
61
|
console.log(' /workflow delete|rm <name> xoá workflow đã lưu');
|
|
60
62
|
console.log('');
|
|
61
63
|
console.log(c.accent(' Nhanh nhất để thử:'));
|
package/src/repl.js
CHANGED
|
@@ -82,6 +82,7 @@ import {
|
|
|
82
82
|
shortCwd, shortPath, relTime, firstLine, truncate, fmtTime, fmtK, preview,
|
|
83
83
|
} from './repl/utils.js';
|
|
84
84
|
import { createAgentDispatcher } from './repl/agent-dispatch.js';
|
|
85
|
+
import { createBgRegistry } from './workflow-bg.js';
|
|
85
86
|
export async function startRepl(opts = {}) {
|
|
86
87
|
const state = createState(opts, config);
|
|
87
88
|
const tokenMeter = new TokenMeter();
|
|
@@ -252,6 +253,10 @@ export async function startRepl(opts = {}) {
|
|
|
252
253
|
// lần 2 trong cửa sổ 1.5s → thoát
|
|
253
254
|
if (sigintTimer) clearTimeout(sigintTimer);
|
|
254
255
|
exiting = true;
|
|
256
|
+
// Bg workflow đang chạy → abort + mark interrupted (journal resume-able).
|
|
257
|
+
const swept = bgRegistry.sweepOnExit();
|
|
258
|
+
if (swept > 0)
|
|
259
|
+
console.log(c.dim(' ' + (t.bgWorkflowSweep ? t.bgWorkflowSweep(swept) : `${swept} workflow nền bị ngắt — resume bằng /workflow resume <id>`)));
|
|
255
260
|
persist();
|
|
256
261
|
console.log(c.dim('\n ' + t.bye));
|
|
257
262
|
tui.close(); // khôi phục terminal (raw mode/paste/stdout) trước khi thoát
|
|
@@ -453,6 +458,8 @@ Thực thi: đọc/tạo file cần thiết bằng tool, viết code production-
|
|
|
453
458
|
if (sub === 'runs') return workflowRuns();
|
|
454
459
|
if (sub === 'log') return workflowLog(rest);
|
|
455
460
|
if (sub === 'resume') return workflowResume(rest);
|
|
461
|
+
if (sub === 'stop') return workflowStop(rest);
|
|
462
|
+
if (sub === 'active') return workflowActive();
|
|
456
463
|
}
|
|
457
464
|
// Default: ad-hoc workflow (giữ behavior cũ — model design workflow từ request).
|
|
458
465
|
await workflowExecute(trimmed);
|
|
@@ -594,6 +601,42 @@ Thực thi THEO ĐÚNG THỨ TỰ (BẮT BUỘC):
|
|
|
594
601
|
return _workflowList({ c, t });
|
|
595
602
|
}
|
|
596
603
|
|
|
604
|
+
// Huỷ workflow nền theo id (hoặc `all`). Journal mark interrupted → resume được.
|
|
605
|
+
function workflowStop(arg) {
|
|
606
|
+
const id = (arg || '').trim();
|
|
607
|
+
if (!id) {
|
|
608
|
+
const active = bgRegistry.activeRuns();
|
|
609
|
+
if (!active.length) return console.log(c.dim(' không có workflow nền nào đang chạy.'));
|
|
610
|
+
console.log(c.err(' cần id: /workflow stop <id> | all'));
|
|
611
|
+
for (const r of active) console.log(' ' + c.accent(r.id) + (r.name ? c.dim(' · ' + r.name) : ''));
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
if (id === 'all') {
|
|
615
|
+
const n = bgRegistry.sweepOnExit();
|
|
616
|
+
return console.log(
|
|
617
|
+
c.tool(' 🎼 ' + (n ? `đã huỷ ${n} workflow nền — resume bằng /workflow resume <id>` : 'không có workflow nền nào đang chạy.'))
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
const ok = bgRegistry.stopBg(id);
|
|
621
|
+
console.log(
|
|
622
|
+
ok
|
|
623
|
+
? c.tool(' 🎼 đã huỷ workflow nền ' + c.accent(id) + c.dim(' — resume bằng /workflow resume ' + id))
|
|
624
|
+
: c.err(' không tìm thấy workflow nền đang chạy với id: ' + id)
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// List workflow nền đang chạy.
|
|
629
|
+
function workflowActive() {
|
|
630
|
+
const active = bgRegistry.activeRuns();
|
|
631
|
+
if (!active.length) return console.log(c.dim(' không có workflow nền nào đang chạy.'));
|
|
632
|
+
console.log(c.tool(` 🎼 workflow nền đang chạy (${active.length}/${bgRegistry.MAX_BG}):`));
|
|
633
|
+
const now = Date.now();
|
|
634
|
+
for (const r of active) {
|
|
635
|
+
const secs = Math.round((now - r.startedAt) / 1000);
|
|
636
|
+
console.log(' ' + c.accent(r.id) + (r.name ? c.dim(' · ' + r.name) : '') + c.dim(` · ${secs}s`));
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
597
640
|
function workflowLoad(name) {
|
|
598
641
|
return _workflowLoad(name, { c, t });
|
|
599
642
|
}
|
|
@@ -1292,6 +1335,21 @@ NGUYÊN TẮC:
|
|
|
1292
1335
|
|
|
1293
1336
|
// Main loop — runs until /exit, double Ctrl+C, or EOF. Never exits after a task.
|
|
1294
1337
|
while (true) {
|
|
1338
|
+
// Drain bg workflow đã xong → notify + auto-inject kết quả vào pending (gửi
|
|
1339
|
+
// như lượt mới, model tổng hợp cho user). Làm TRƯỚC khi đọc input để kết quả
|
|
1340
|
+
// nền không kẹt sau lượt user đang gõ.
|
|
1341
|
+
const fin = bgRegistry.drainCompletions();
|
|
1342
|
+
for (const f of fin) {
|
|
1343
|
+
const label = f.name ? `${f.name} (${f.id})` : f.id;
|
|
1344
|
+
if (f.status === 'done') {
|
|
1345
|
+
console.log(c.tool(' 🎼 ' + (t.bgWorkflowDone ? t.bgWorkflowDone(label) : `workflow nền ${label} đã xong`)));
|
|
1346
|
+
pending.push(
|
|
1347
|
+
`[Kết quả workflow nền "${label}"]\n${f.result}\n\nTổng hợp kết quả trên cho user (ngắn gọn, nêu điểm chính). Đây là output từ workflow chạy nền — KHÔNG chạy lại.`
|
|
1348
|
+
);
|
|
1349
|
+
} else {
|
|
1350
|
+
console.log(c.err(' 🎼 ' + (t.bgWorkflowFailed ? t.bgWorkflowFailed(label, f.error) : `workflow nền ${label} lỗi: ${f.error}`)));
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1295
1353
|
let input;
|
|
1296
1354
|
if (pending.length) {
|
|
1297
1355
|
// Có tin xếp hàng → tự gửi câu kế tiếp (không chờ gõ).
|
|
@@ -1419,7 +1477,7 @@ NGUYÊN TẮC:
|
|
|
1419
1477
|
// src/repl/agent-dispatch.js (v1.12.x). Factory được gọi MỖI turn vì abort
|
|
1420
1478
|
// được rebind trong handle() — không cache.
|
|
1421
1479
|
const dispatchTool = createAgentDispatcher({
|
|
1422
|
-
state, abort, tokenMeter, stopSpin, startSpin, execTool, tui, c,
|
|
1480
|
+
state, abort, tokenMeter, stopSpin, startSpin, execTool, tui, c, bgRegistry,
|
|
1423
1481
|
});
|
|
1424
1482
|
|
|
1425
1483
|
const answer = await runAgent({
|
|
@@ -1596,6 +1654,18 @@ NGUYÊN TẮC:
|
|
|
1596
1654
|
return await execToolCore(name, input, { retried: false });
|
|
1597
1655
|
}
|
|
1598
1656
|
|
|
1657
|
+
// Background workflow registry — session-scope, sống ngoài turn. Tool `workflow`
|
|
1658
|
+
// (qua turn dispatcher) gọi startBgWorkflow → headless runAgent nền. Drain ở main
|
|
1659
|
+
// loop giữa lượt → notify + auto-inject. bgRegistry KHÔNG wire vào bg dispatcher
|
|
1660
|
+
// (chống lồng workflow nền). state + execTool + spawnAgentToolsDoc đã sẵn ở scope.
|
|
1661
|
+
const bgRegistry = createBgRegistry({
|
|
1662
|
+
createAgentDispatcher,
|
|
1663
|
+
state,
|
|
1664
|
+
execTool,
|
|
1665
|
+
spawnAgentToolsDoc,
|
|
1666
|
+
cwd: process.cwd(),
|
|
1667
|
+
});
|
|
1668
|
+
|
|
1599
1669
|
// Phần thân tool (tách riêng để retry khi user vừa approve thêm extra root).
|
|
1600
1670
|
// Flow OutOfScopeError: tool ném → repl hỏi user "add folder X? [y/n/a]" → nếu
|
|
1601
1671
|
// y/a: addRoot + persist (đã làm trong addRoot) + state.extraRoots sync + chạy
|
|
@@ -1998,11 +2068,15 @@ NGUYÊN TẮC:
|
|
|
1998
2068
|
break;
|
|
1999
2069
|
case 'exit':
|
|
2000
2070
|
case 'quit':
|
|
2001
|
-
case 'q':
|
|
2071
|
+
case 'q': {
|
|
2072
|
+
const swept = bgRegistry.sweepOnExit();
|
|
2073
|
+
if (swept > 0)
|
|
2074
|
+
console.log(c.dim(' ' + (t.bgWorkflowSweep ? t.bgWorkflowSweep(swept) : `${swept} workflow nền bị ngắt — resume bằng /workflow resume <id>`)));
|
|
2002
2075
|
persist();
|
|
2003
2076
|
exiting = true;
|
|
2004
2077
|
console.log(c.dim(' ' + t.bye));
|
|
2005
2078
|
return true;
|
|
2079
|
+
}
|
|
2006
2080
|
default:
|
|
2007
2081
|
console.log(c.err(' ' + t.unknownCmd(cmd)) + c.dim(' ' + t.tryHelp));
|
|
2008
2082
|
}
|
package/src/subagent.js
CHANGED
|
@@ -87,6 +87,9 @@ Khi nhiệm vụ phức tạp / chia được thành nhiều phần độc lập
|
|
|
87
87
|
Chạy 1 sub-agent độc lập, trả về kết quả dạng string (text cuối của sub-agent).
|
|
88
88
|
- spawn_agents {"agents": [{"task": str, "context"?: str, "model"?: str}, ...]}
|
|
89
89
|
Chạy NHIỀU sub-agent SONG SONG (Promise.all). Trả về mảng kết quả ghép theo thứ tự, mỗi phần có header "── sub-agent #N ──".
|
|
90
|
+
- workflow {"prompt": str, "name"?: str}
|
|
91
|
+
Chạy orchestration NỀN (background) — trả về NGAY, KHÔNG chờ. Dùng khi job dài, nhiều sub-agent, user muốn làm việc khác trong lúc chờ. Kết quả consolidated tự chèn vào lượt sau khi xong. Cap 3 workflow nền song song. Huỷ: /workflow stop <id>.
|
|
92
|
+
KHÁC spawn_agents: spawn_agents BLOCK lượt hiện tại tới khi mọi sub-agent xong (dùng khi cần kết quả NGAY trong lượt này để tổng hợp). workflow detach hẳn (dùng cho job dài user không muốn ngồi chờ). KHÔNG lồng — workflow nền không tự gọi workflow nền.
|
|
90
93
|
|
|
91
94
|
## Rules
|
|
92
95
|
1. TASK PHẢI CỤ THỂ: nêu rõ goal + output mong đợi (vd "đọc src/api.js, liệt kê tất cả endpoint + method, trả về dạng bảng markdown"). Đừng giao task mơ hồ kiểu "phân tích code".
|
package/src/tools.js
CHANGED
|
@@ -213,7 +213,7 @@ export const DESTRUCTIVE = new Set(['write_file', 'edit_file', 'run_command']);
|
|
|
213
213
|
|
|
214
214
|
/**
|
|
215
215
|
* Tên các tool model có thể gọi.
|
|
216
|
-
* @typedef {'read_file'|'web_fetch'|'write_file'|'edit_file'|'list_dir'|'glob'|'grep'|'run_command'|'bg_output'|'kill_bg'} ToolName
|
|
216
|
+
* @typedef {'read_file'|'web_fetch'|'write_file'|'edit_file'|'list_dir'|'glob'|'grep'|'run_command'|'bg_output'|'kill_bg'|'workflow'} ToolName
|
|
217
217
|
*/
|
|
218
218
|
|
|
219
219
|
/**
|
|
@@ -234,6 +234,8 @@ export const DESTRUCTIVE = new Set(['write_file', 'edit_file', 'run_command']);
|
|
|
234
234
|
* @property {number} [id] bg_output/kill_bg
|
|
235
235
|
* @property {string} [url] web_fetch: URL http/https cần tải
|
|
236
236
|
* @property {boolean} [raw] web_fetch: true = trả HTML thô, không strip
|
|
237
|
+
* @property {string} [prompt] workflow: yêu cầu workflow chạy nền
|
|
238
|
+
* @property {string} [name] workflow: tên tuỳ chọn cho run
|
|
237
239
|
*/
|
|
238
240
|
|
|
239
241
|
/**
|
|
@@ -774,6 +776,8 @@ export function describe(name, input) {
|
|
|
774
776
|
return `↳ sub-agent: ${String(input.task || '').slice(0, 80)}`;
|
|
775
777
|
case 'spawn_agents':
|
|
776
778
|
return `↳ ${(input.tasks || []).length} sub-agent song song`;
|
|
779
|
+
case 'workflow':
|
|
780
|
+
return `🎼 workflow nền: ${String(input.prompt || '').slice(0, 60)}`;
|
|
777
781
|
case 'write_todos': {
|
|
778
782
|
const items = Array.isArray(input?.todos) ? input.todos : [];
|
|
779
783
|
const done = items.filter((it) => it && it.done).length;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// Background workflow registry — chạy orchestration NỀN, không block turn.
|
|
2
|
+
//
|
|
3
|
+
// Khác /workflow blocking (repl.js:521-569): tool `workflow` model gọi → kick
|
|
4
|
+
// off headless runAgent với own AbortController + own TokenMeter, trả về NGAY
|
|
5
|
+
// {run_id}. Turn không chờ. Kết quả consolidated được drain ở main loop giữa
|
|
6
|
+
// các lượt → push vào `pending` → auto-inject lượt sau.
|
|
7
|
+
//
|
|
8
|
+
// Lifecycle: session-scope (sống ngoài turn spawn nó). Cap MAX_BG=3 song song.
|
|
9
|
+
// Exit CLI → sweepOnExit() mark mọi run active = interrupted (journal resume-able).
|
|
10
|
+
//
|
|
11
|
+
// KHÔNG closure coupling — plain map + functions, deps inject để test (noob.md rule).
|
|
12
|
+
|
|
13
|
+
import { runAgent } from './agent.js';
|
|
14
|
+
import { TokenMeter } from './tokens.js';
|
|
15
|
+
import { createRun, closeRun } from './workflow-runs.js';
|
|
16
|
+
|
|
17
|
+
export const MAX_BG = 3;
|
|
18
|
+
|
|
19
|
+
// Tạo registry. Deps inject (production wire ở repl.js; test pass mock).
|
|
20
|
+
// deps.createAgentDispatcher — factory dựng dispatchTool cho 1 bg run
|
|
21
|
+
// deps.state — state object (agentMode, model…)
|
|
22
|
+
// deps.execTool — async (name, input) → {allow, result}
|
|
23
|
+
// deps.spawnAgentToolsDoc — (depth) => string doc sub-agent tool
|
|
24
|
+
// deps.cwd — process.cwd() cho journal path
|
|
25
|
+
// deps.runAgent / deps.createRun / deps.closeRun — test override
|
|
26
|
+
export function createBgRegistry(deps = {}) {
|
|
27
|
+
const {
|
|
28
|
+
createAgentDispatcher,
|
|
29
|
+
state,
|
|
30
|
+
execTool,
|
|
31
|
+
spawnAgentToolsDoc,
|
|
32
|
+
cwd = process.cwd(),
|
|
33
|
+
} = deps;
|
|
34
|
+
const _runAgent = deps.runAgent || runAgent;
|
|
35
|
+
const _createRun = deps.createRun || createRun;
|
|
36
|
+
const _closeRun = deps.closeRun || closeRun;
|
|
37
|
+
|
|
38
|
+
// runId → { id, name, controller, meter, status, startedAt, result, error }
|
|
39
|
+
const runs = new Map();
|
|
40
|
+
// Hàng đợi run đã settle, chờ main loop drain.
|
|
41
|
+
const completions = [];
|
|
42
|
+
|
|
43
|
+
const activeCount = () =>
|
|
44
|
+
[...runs.values()].filter((r) => r.status === 'running').length;
|
|
45
|
+
|
|
46
|
+
// Kick off 1 bg workflow. Trả về { ok, id } hoặc { ok:false, error }.
|
|
47
|
+
function startBgWorkflow({ prompt, name = null } = {}) {
|
|
48
|
+
if (!prompt || typeof prompt !== 'string' || !prompt.trim()) {
|
|
49
|
+
return { ok: false, error: 'empty_prompt' };
|
|
50
|
+
}
|
|
51
|
+
if (activeCount() >= MAX_BG) {
|
|
52
|
+
return { ok: false, error: 'cap' };
|
|
53
|
+
}
|
|
54
|
+
let run;
|
|
55
|
+
try {
|
|
56
|
+
run = _createRun({ name: name || 'bg', workflowPrompt: prompt, cwd });
|
|
57
|
+
} catch (e) {
|
|
58
|
+
return { ok: false, error: 'journal:' + (e?.message || e) };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const controller = new AbortController();
|
|
62
|
+
const meter = new TokenMeter();
|
|
63
|
+
const entry = {
|
|
64
|
+
id: run.id,
|
|
65
|
+
name,
|
|
66
|
+
controller,
|
|
67
|
+
meter,
|
|
68
|
+
status: 'running',
|
|
69
|
+
startedAt: Date.now(),
|
|
70
|
+
result: null,
|
|
71
|
+
error: null,
|
|
72
|
+
};
|
|
73
|
+
runs.set(run.id, entry);
|
|
74
|
+
|
|
75
|
+
// workflowRun riêng cho bg dispatcher: journal cache dùng run.data này, KHÔNG
|
|
76
|
+
// đụng state.workflowRun của turn (turn có thể đang chạy /workflow blocking).
|
|
77
|
+
const bgWorkflowRun = { id: run.id, data: run.data, path: run.path };
|
|
78
|
+
|
|
79
|
+
// Headless dispatcher: own controller + meter, stub spinner/tui (bg không có UI).
|
|
80
|
+
const noop = () => {};
|
|
81
|
+
const dispatchTool = createAgentDispatcher({
|
|
82
|
+
state: { ...state, workflowRun: bgWorkflowRun },
|
|
83
|
+
abort: controller,
|
|
84
|
+
tokenMeter: meter,
|
|
85
|
+
stopSpin: noop,
|
|
86
|
+
startSpin: noop,
|
|
87
|
+
execTool,
|
|
88
|
+
tui: null,
|
|
89
|
+
c: null,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Chạy headless. runAgent options TUI-bound (onStatus/onDelta/onSteer) bỏ —
|
|
93
|
+
// đều optional, ?.-guard trong agent.js.
|
|
94
|
+
const history = [{ role: 'user', content: prompt }];
|
|
95
|
+
const promise = _runAgent({
|
|
96
|
+
history,
|
|
97
|
+
model: state.model.id,
|
|
98
|
+
signal: controller.signal,
|
|
99
|
+
tokenMeter: meter,
|
|
100
|
+
goal: prompt,
|
|
101
|
+
extraToolsDoc: spawnAgentToolsDoc ? spawnAgentToolsDoc(0) : '',
|
|
102
|
+
onTool: (n, inp) => dispatchTool(n, inp, 0),
|
|
103
|
+
})
|
|
104
|
+
.then((answer) => {
|
|
105
|
+
entry.status = 'done';
|
|
106
|
+
entry.result = typeof answer === 'string' ? answer : String(answer ?? '');
|
|
107
|
+
try { _closeRun(run.data, 'done', cwd); } catch {}
|
|
108
|
+
completions.push({ id: run.id, name, status: 'done', result: entry.result });
|
|
109
|
+
})
|
|
110
|
+
.catch((err) => {
|
|
111
|
+
const isAbort = err?.message === 'aborted' || err?.name === 'AbortError';
|
|
112
|
+
entry.status = isAbort ? 'interrupted' : 'failed';
|
|
113
|
+
entry.error = err?.message || String(err);
|
|
114
|
+
try { _closeRun(run.data, entry.status, cwd); } catch {}
|
|
115
|
+
// Abort do stopBg/sweepOnExit → KHÔNG enqueue completion (user chủ động huỷ).
|
|
116
|
+
if (!isAbort) {
|
|
117
|
+
completions.push({ id: run.id, name, status: entry.status, error: entry.error });
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
entry.promise = promise;
|
|
121
|
+
|
|
122
|
+
return { ok: true, id: run.id };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Lấy + xoá các run đã settle. Main loop gọi giữa lượt → notify + auto-inject.
|
|
126
|
+
function drainCompletions() {
|
|
127
|
+
if (!completions.length) return [];
|
|
128
|
+
const out = completions.splice(0);
|
|
129
|
+
for (const c of out) runs.delete(c.id);
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Huỷ 1 bg run. controller.abort() → runAgent throw 'aborted' → catch mark
|
|
134
|
+
// interrupted + closeRun. Trả về true nếu có run để huỷ.
|
|
135
|
+
function stopBg(id) {
|
|
136
|
+
const entry = runs.get(id);
|
|
137
|
+
if (!entry || entry.status !== 'running') return false;
|
|
138
|
+
entry.controller.abort();
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Exit CLI: abort + mark interrupted mọi run active. Journal đầy đủ để resume.
|
|
143
|
+
// Đồng bộ (không await) — exit path không chờ promise settle.
|
|
144
|
+
function sweepOnExit() {
|
|
145
|
+
let n = 0;
|
|
146
|
+
for (const entry of runs.values()) {
|
|
147
|
+
if (entry.status === 'running') {
|
|
148
|
+
entry.controller.abort();
|
|
149
|
+
entry.status = 'interrupted';
|
|
150
|
+
n++;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return n;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// List run đang chạy — cho /workflow active + cảnh báo exit.
|
|
157
|
+
function activeRuns() {
|
|
158
|
+
return [...runs.values()]
|
|
159
|
+
.filter((r) => r.status === 'running')
|
|
160
|
+
.map((r) => ({ id: r.id, name: r.name, startedAt: r.startedAt }));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
startBgWorkflow,
|
|
165
|
+
drainCompletions,
|
|
166
|
+
stopBg,
|
|
167
|
+
sweepOnExit,
|
|
168
|
+
activeRuns,
|
|
169
|
+
activeCount,
|
|
170
|
+
MAX_BG,
|
|
171
|
+
};
|
|
172
|
+
}
|