u-foo 2.5.6 → 2.5.7
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/ucode.js +9 -0
- package/package.json +1 -1
- package/src/agents/launch/notifier.js +6 -0
- package/src/agents/prompts/native/index.js +1 -1
- package/src/agents/prompts/native/toolDescriptions/edit.js +1 -0
- package/src/code/agent.js +53 -1076
- package/src/code/busConsumer.js +504 -0
- package/src/code/dispatch.js +1 -6
- package/src/code/launcher/ucode.js +3 -251
- package/src/code/launcher/ucodeBootstrap.js +18 -1
- package/src/code/launcher/ucodeBuild.js +0 -3
- package/src/code/launcher/ucodeDoctor.js +24 -9
- package/src/code/launcher/ucodeRuntimeConfig.js +12 -3
- package/src/code/nativeRunner.js +62 -109
- package/src/code/repl.js +610 -0
- package/src/code/sessionStore.js +5 -1
- package/src/code/skills/injection.js +17 -1
- package/src/code/taskDecomposer.js +36 -29
- package/src/code/tools/common.js +34 -0
- package/src/code/tools/edit.js +11 -3
- package/src/coordination/bus/inject.js +52 -7
- package/src/coordination/bus/subscriber.js +33 -6
- package/src/runtime/daemon/deliveryScheduler.js +102 -2
- package/src/runtime/daemon/index.js +8 -1
- package/src/runtime/daemon/ops.js +23 -0
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { execSync } = require("child_process");
|
|
4
|
+
const { createBusProgressReporter } = require("./taskDecomposer");
|
|
5
|
+
const { DeliveryQueue } = require("../coordination/bus/deliveryQueue");
|
|
6
|
+
|
|
7
|
+
function shellQuote(value = "") {
|
|
8
|
+
const text = String(value == null ? "" : value);
|
|
9
|
+
return `'${text.replace(/'/g, `'\"'\"'`)}'`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function toText(value = "") {
|
|
13
|
+
if (typeof value === "string") return value;
|
|
14
|
+
if (Buffer.isBuffer(value)) return value.toString("utf8");
|
|
15
|
+
return String(value == null ? "" : value);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function stripAnsi(text = "") {
|
|
19
|
+
const raw = String(text || "");
|
|
20
|
+
if (!raw) return "";
|
|
21
|
+
// CSI + OSC sequences (best-effort).
|
|
22
|
+
return raw
|
|
23
|
+
.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "")
|
|
24
|
+
.replace(/\x1b\][^\x07]*\x07/g, "")
|
|
25
|
+
.replace(/\x1b\][^\x1b]*(?:\x1b\\)/g, "");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Bound every shell capture so a hung `ufoo bus` CLI cannot freeze the
|
|
29
|
+
// agent loop (autoBus re-invokes this every 800ms).
|
|
30
|
+
const SHELL_CAPTURE_TIMEOUT_MS = 15000;
|
|
31
|
+
|
|
32
|
+
function runShellCapture(command = "", workspaceRoot = process.cwd()) {
|
|
33
|
+
try {
|
|
34
|
+
const output = execSync(String(command || ""), {
|
|
35
|
+
cwd: workspaceRoot,
|
|
36
|
+
encoding: "utf8",
|
|
37
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
38
|
+
timeout: SHELL_CAPTURE_TIMEOUT_MS,
|
|
39
|
+
});
|
|
40
|
+
return {
|
|
41
|
+
ok: true,
|
|
42
|
+
output: toText(output),
|
|
43
|
+
error: "",
|
|
44
|
+
};
|
|
45
|
+
} catch (err) {
|
|
46
|
+
const stdout = toText(err && err.stdout);
|
|
47
|
+
const stderr = toText(err && err.stderr);
|
|
48
|
+
const detail = [stdout, stderr].filter(Boolean).join("\n").trim();
|
|
49
|
+
return {
|
|
50
|
+
ok: false,
|
|
51
|
+
output: detail,
|
|
52
|
+
error: detail || (err && err.message ? err.message : "shell command failed"),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function safeSubscriberName(subscriberId = "") {
|
|
58
|
+
return String(subscriberId || "").replace(/:/g, "_");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function resolvePendingQueueFile(workspaceRoot = process.cwd(), subscriberId = "") {
|
|
62
|
+
const root = String(workspaceRoot || process.cwd()).trim() || process.cwd();
|
|
63
|
+
const sub = String(subscriberId || "").trim();
|
|
64
|
+
if (!sub) return "";
|
|
65
|
+
return path.join(root, ".ufoo", "bus", "queues", safeSubscriberName(sub), "pending.jsonl");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function resolveUfooProjectRoot(preferredRoot = "", env = process.env) {
|
|
69
|
+
const candidates = [
|
|
70
|
+
String(preferredRoot || "").trim(),
|
|
71
|
+
String((env && env.UFOO_UCODE_PROJECT_ROOT) || "").trim(),
|
|
72
|
+
String((env && env.UFOO_PROJECT_ROOT) || "").trim(),
|
|
73
|
+
process.cwd(),
|
|
74
|
+
].filter(Boolean);
|
|
75
|
+
|
|
76
|
+
for (const root of candidates) {
|
|
77
|
+
try {
|
|
78
|
+
const busDir = path.join(root, ".ufoo", "bus");
|
|
79
|
+
if (fs.existsSync(busDir)) return root;
|
|
80
|
+
} catch {
|
|
81
|
+
// ignore
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return candidates[0] || process.cwd();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function countPendingQueueLines(filePath = "") {
|
|
89
|
+
const target = String(filePath || "").trim();
|
|
90
|
+
if (!target) return 0;
|
|
91
|
+
try {
|
|
92
|
+
if (!fs.existsSync(target)) return 0;
|
|
93
|
+
const content = String(fs.readFileSync(target, "utf8") || "");
|
|
94
|
+
if (!content.trim()) return 0;
|
|
95
|
+
return content.split(/\r?\n/).filter((line) => line.trim()).length;
|
|
96
|
+
} catch {
|
|
97
|
+
return 0;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function isPidAlive(pid) {
|
|
102
|
+
const p = parseInt(String(pid || "").trim(), 10);
|
|
103
|
+
if (!Number.isFinite(p) || p <= 0) return false;
|
|
104
|
+
try {
|
|
105
|
+
process.kill(p, 0);
|
|
106
|
+
return true;
|
|
107
|
+
} catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function listProcessingFiles(pendingFilePath = "") {
|
|
113
|
+
const pendingFile = String(pendingFilePath || "").trim();
|
|
114
|
+
if (!pendingFile) return [];
|
|
115
|
+
const dir = path.dirname(pendingFile);
|
|
116
|
+
const base = path.basename(pendingFile);
|
|
117
|
+
const prefix = `${base}.processing.`;
|
|
118
|
+
try {
|
|
119
|
+
if (!fs.existsSync(dir)) return [];
|
|
120
|
+
return fs.readdirSync(dir)
|
|
121
|
+
.filter((name) => name && name.startsWith(prefix))
|
|
122
|
+
.map((name) => path.join(dir, name));
|
|
123
|
+
} catch {
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function countRecoverableProcessingFiles(pendingFilePath = "", options = {}) {
|
|
129
|
+
const pendingFile = String(pendingFilePath || "").trim();
|
|
130
|
+
if (!pendingFile) return 0;
|
|
131
|
+
const maxAgeMs = Number.isFinite(options.maxAgeMs) ? options.maxAgeMs : 60000;
|
|
132
|
+
const now = Date.now();
|
|
133
|
+
const files = listProcessingFiles(pendingFile);
|
|
134
|
+
let count = 0;
|
|
135
|
+
|
|
136
|
+
for (const file of files) {
|
|
137
|
+
const name = path.basename(file);
|
|
138
|
+
const m = name.match(/\.processing\.(\d+)\./);
|
|
139
|
+
const pid = m ? parseInt(m[1], 10) : NaN;
|
|
140
|
+
|
|
141
|
+
if (Number.isFinite(pid) && pid > 0 && !isPidAlive(pid)) {
|
|
142
|
+
count += 1;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (!Number.isFinite(maxAgeMs) || maxAgeMs <= 0) continue;
|
|
147
|
+
try {
|
|
148
|
+
const stat = fs.statSync(file);
|
|
149
|
+
if (stat && stat.isFile() && (now - stat.mtimeMs > maxAgeMs)) {
|
|
150
|
+
count += 1;
|
|
151
|
+
}
|
|
152
|
+
} catch {
|
|
153
|
+
// ignore
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return count;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function getPendingBusCount(workspaceRoot = process.cwd(), subscriberId = "") {
|
|
161
|
+
const pendingFile = resolvePendingQueueFile(workspaceRoot, subscriberId);
|
|
162
|
+
const pendingLines = countPendingQueueLines(pendingFile);
|
|
163
|
+
if (!pendingFile) return pendingLines;
|
|
164
|
+
// If a prior crash left `.processing.*` behind, count it so autoBus can self-heal.
|
|
165
|
+
const recoverable = countRecoverableProcessingFiles(pendingFile, { maxAgeMs: 60000 });
|
|
166
|
+
return pendingLines + recoverable;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function drainJsonlFile(filePath = "") {
|
|
170
|
+
const target = String(filePath || "").trim();
|
|
171
|
+
if (!target) return { drained: [], rawLines: [], error: "" };
|
|
172
|
+
const queue = new DeliveryQueue(target);
|
|
173
|
+
const drained = [];
|
|
174
|
+
const rawLines = [];
|
|
175
|
+
const claims = [];
|
|
176
|
+
try {
|
|
177
|
+
queue.recover();
|
|
178
|
+
while (true) {
|
|
179
|
+
const claim = queue.claimNext();
|
|
180
|
+
if (!claim) break;
|
|
181
|
+
claims.push(claim);
|
|
182
|
+
drained.push(claim.event);
|
|
183
|
+
rawLines.push(JSON.stringify(claim.event));
|
|
184
|
+
queue.completeClaim(claim);
|
|
185
|
+
}
|
|
186
|
+
} catch (err) {
|
|
187
|
+
for (const claim of claims) queue.restoreClaim(claim);
|
|
188
|
+
return { drained: [], rawLines: [], error: err && err.message ? err.message : "drain failed" };
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
drained,
|
|
192
|
+
rawLines,
|
|
193
|
+
error: "",
|
|
194
|
+
claims,
|
|
195
|
+
processingFile: claims[0] ? claims[0].processingFile : "",
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function extractTaskFromBusEvent(evt) {
|
|
200
|
+
if (!evt || typeof evt !== "object") return null;
|
|
201
|
+
if (String(evt.event || "").trim().toLowerCase() !== "message") return null;
|
|
202
|
+
let publisher = "";
|
|
203
|
+
if (typeof evt.publisher === "string") {
|
|
204
|
+
publisher = String(evt.publisher || "").trim();
|
|
205
|
+
} else if (evt.publisher && typeof evt.publisher === "object") {
|
|
206
|
+
publisher = String(evt.publisher.subscriber || evt.publisher.nickname || "").trim();
|
|
207
|
+
} else {
|
|
208
|
+
publisher = String(evt.publisher || "").trim();
|
|
209
|
+
}
|
|
210
|
+
if (publisher === "[object Object]") publisher = "";
|
|
211
|
+
if (!publisher) return null;
|
|
212
|
+
const data = evt.data && typeof evt.data === "object" ? evt.data : {};
|
|
213
|
+
const message = typeof data.message === "string"
|
|
214
|
+
? data.message
|
|
215
|
+
: (typeof data.text === "string" ? data.text : "");
|
|
216
|
+
const task = String(message || "").trim();
|
|
217
|
+
if (!task) return null;
|
|
218
|
+
return { publisher, task };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function shouldAutoConsumeBus(subscriberId = "") {
|
|
222
|
+
const id = String(subscriberId || "").trim().toLowerCase();
|
|
223
|
+
if (!id) return false;
|
|
224
|
+
return id.startsWith("ufoo-code:")
|
|
225
|
+
|| id.startsWith("ucode:")
|
|
226
|
+
|| id.startsWith("ufoo:");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function extractBusMessageTask(contentRaw = "") {
|
|
230
|
+
const raw = String(contentRaw || "").trim();
|
|
231
|
+
if (!raw) return "";
|
|
232
|
+
try {
|
|
233
|
+
const parsed = JSON.parse(raw);
|
|
234
|
+
if (parsed && typeof parsed === "object") {
|
|
235
|
+
if (typeof parsed.message === "string" && parsed.message.trim()) return parsed.message.trim();
|
|
236
|
+
if (typeof parsed.text === "string" && parsed.text.trim()) return parsed.text.trim();
|
|
237
|
+
if (typeof parsed.prompt === "string" && parsed.prompt.trim()) return parsed.prompt.trim();
|
|
238
|
+
}
|
|
239
|
+
} catch {
|
|
240
|
+
// treat as plain text below
|
|
241
|
+
}
|
|
242
|
+
return raw;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function busCheckOutputIndicatesPending(raw = "") {
|
|
246
|
+
const text = stripAnsi(String(raw || ""));
|
|
247
|
+
if (!text.trim()) return false;
|
|
248
|
+
if (/no pending messages/i.test(text)) return false;
|
|
249
|
+
if (/you have\s+\d+\s+pending/i.test(text)) return true;
|
|
250
|
+
if (/after handling,\s*run:\s*ufoo bus ack/i.test(text)) return true;
|
|
251
|
+
if (/pending event/i.test(text)) return true;
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function parseBusCheckOutput(raw = "") {
|
|
256
|
+
const text = stripAnsi(String(raw || ""));
|
|
257
|
+
if (!text.trim()) return [];
|
|
258
|
+
if (/no pending messages/i.test(text)) return [];
|
|
259
|
+
|
|
260
|
+
const lines = text.split(/\r?\n/);
|
|
261
|
+
const rows = [];
|
|
262
|
+
let current = null;
|
|
263
|
+
|
|
264
|
+
for (const line of lines) {
|
|
265
|
+
const trimmed = String(line || "").trim();
|
|
266
|
+
if (!trimmed) continue;
|
|
267
|
+
|
|
268
|
+
const header = trimmed.match(/^@.+\s+from\s+([^\s]+)\s*$/i);
|
|
269
|
+
if (header) {
|
|
270
|
+
if (current && current.publisher) rows.push(current);
|
|
271
|
+
current = {
|
|
272
|
+
publisher: String(header[1] || "").trim(),
|
|
273
|
+
content: "",
|
|
274
|
+
};
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (!current) continue;
|
|
279
|
+
|
|
280
|
+
const contentMatch = trimmed.match(/^content:\s*(.*)$/i);
|
|
281
|
+
if (contentMatch) {
|
|
282
|
+
current.content = String(contentMatch[1] || "").trim();
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (
|
|
287
|
+
current.content
|
|
288
|
+
&& !/^(type|event|seq|target|timestamp):\s*/i.test(trimmed)
|
|
289
|
+
&& !trimmed.startsWith("@")
|
|
290
|
+
) {
|
|
291
|
+
current.content = `${current.content}\n${trimmed}`;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (current && current.publisher) rows.push(current);
|
|
296
|
+
|
|
297
|
+
return rows
|
|
298
|
+
.map((entry) => {
|
|
299
|
+
const publisher = String(entry.publisher || "").trim();
|
|
300
|
+
const content = String(entry.content || "").trim();
|
|
301
|
+
const task = extractBusMessageTask(content);
|
|
302
|
+
if (!publisher || !task) return null;
|
|
303
|
+
return {
|
|
304
|
+
publisher,
|
|
305
|
+
content,
|
|
306
|
+
task,
|
|
307
|
+
};
|
|
308
|
+
})
|
|
309
|
+
.filter(Boolean);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async function runUbusCommand(state = {}, options = {}) {
|
|
313
|
+
const runtimeWorkspace = resolveUfooProjectRoot(String(
|
|
314
|
+
options.workspaceRoot
|
|
315
|
+
|| (state && state.workspaceRoot)
|
|
316
|
+
|| ""
|
|
317
|
+
));
|
|
318
|
+
const shell = typeof options.execShell === "function"
|
|
319
|
+
? options.execShell
|
|
320
|
+
: (command) => runShellCapture(command, runtimeWorkspace);
|
|
321
|
+
const runNl = typeof options.runNaturalLanguageTaskImpl === "function"
|
|
322
|
+
? options.runNaturalLanguageTaskImpl
|
|
323
|
+
: require("./agent").runNaturalLanguageTask;
|
|
324
|
+
const formatNl = typeof options.formatNlResultImpl === "function"
|
|
325
|
+
? options.formatNlResultImpl
|
|
326
|
+
: require("./agent").formatNlResult;
|
|
327
|
+
const onMessageReceived = typeof options.onMessageReceived === "function"
|
|
328
|
+
? options.onMessageReceived
|
|
329
|
+
: null;
|
|
330
|
+
|
|
331
|
+
const explicitSubscriber = String(options.subscriberId || "").trim();
|
|
332
|
+
const envSubscriber = String(process.env.UFOO_SUBSCRIBER_ID || "").trim();
|
|
333
|
+
let subscriberId = explicitSubscriber || envSubscriber;
|
|
334
|
+
if (!subscriberId) {
|
|
335
|
+
const whoami = shell("ufoo bus whoami 2>/dev/null || true");
|
|
336
|
+
subscriberId = String((whoami && whoami.output) || "").trim();
|
|
337
|
+
}
|
|
338
|
+
if (!subscriberId) {
|
|
339
|
+
const joined = shell("ufoo bus join | tail -1");
|
|
340
|
+
subscriberId = String((joined && joined.output) || "").trim();
|
|
341
|
+
}
|
|
342
|
+
if (!subscriberId) {
|
|
343
|
+
return {
|
|
344
|
+
ok: false,
|
|
345
|
+
summary: "",
|
|
346
|
+
error: "failed to resolve bus subscriber id",
|
|
347
|
+
handled: 0,
|
|
348
|
+
subscriberId: "",
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Prefer consuming pending.jsonl directly (stable, ANSI/wrapping-proof).
|
|
353
|
+
const pendingFile = resolvePendingQueueFile(runtimeWorkspace, subscriberId);
|
|
354
|
+
const queue = pendingFile ? new DeliveryQueue(pendingFile) : null;
|
|
355
|
+
if (queue) queue.recover();
|
|
356
|
+
const hasPendingFile = Boolean(pendingFile && fs.existsSync(pendingFile));
|
|
357
|
+
let handled = 0;
|
|
358
|
+
const sendErrors = [];
|
|
359
|
+
const messageExchanges = [];
|
|
360
|
+
|
|
361
|
+
if (queue && hasPendingFile) {
|
|
362
|
+
while (fs.existsSync(pendingFile)) {
|
|
363
|
+
const claim = queue.claimNext();
|
|
364
|
+
if (!claim) break;
|
|
365
|
+
const message = extractTaskFromBusEvent(claim.event);
|
|
366
|
+
if (!message) {
|
|
367
|
+
queue.completeClaim(claim);
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
let nlResult;
|
|
371
|
+
|
|
372
|
+
// Notify that we received the message (for immediate display)
|
|
373
|
+
if (onMessageReceived) {
|
|
374
|
+
onMessageReceived({
|
|
375
|
+
from: message.publisher,
|
|
376
|
+
task: message.task,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Create progress reporter for this message
|
|
381
|
+
const progressReporter = createBusProgressReporter(shell, message.publisher);
|
|
382
|
+
|
|
383
|
+
try {
|
|
384
|
+
// Send initial acknowledgment
|
|
385
|
+
shell(`ufoo bus send ${shellQuote(message.publisher)} ${shellQuote("🚀 Starting task...")}`);
|
|
386
|
+
|
|
387
|
+
// eslint-disable-next-line no-await-in-loop
|
|
388
|
+
nlResult = await runNl(message.task, state, {
|
|
389
|
+
onProgress: progressReporter,
|
|
390
|
+
signal: options.signal,
|
|
391
|
+
});
|
|
392
|
+
} catch (err) {
|
|
393
|
+
const errorMessage = err && err.message ? err.message : "task failed";
|
|
394
|
+
sendErrors.push(`task from ${message.publisher} failed: ${errorMessage}`);
|
|
395
|
+
queue.restoreClaim(claim);
|
|
396
|
+
// Send error notification
|
|
397
|
+
shell(`ufoo bus send ${shellQuote(message.publisher)} ${shellQuote(`❌ Error: ${errorMessage}`)}`);
|
|
398
|
+
break;
|
|
399
|
+
}
|
|
400
|
+
const reply = String(formatNl(nlResult, false) || "").replace(/\s+/g, " ").trim() || "Done.";
|
|
401
|
+
const sendRes = shell(`ufoo bus send ${shellQuote(message.publisher)} ${shellQuote(reply.slice(0, 2000))}`);
|
|
402
|
+
if (!sendRes.ok) {
|
|
403
|
+
sendErrors.push(`reply to ${message.publisher} failed: ${sendRes.error || "send failed"}`);
|
|
404
|
+
queue.restoreClaim(claim);
|
|
405
|
+
break;
|
|
406
|
+
}
|
|
407
|
+
handled += 1;
|
|
408
|
+
queue.completeClaim(claim);
|
|
409
|
+
messageExchanges.push({
|
|
410
|
+
from: message.publisher,
|
|
411
|
+
task: message.task,
|
|
412
|
+
reply,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Fallback: if there is no pending file, fall back to CLI `bus check` parsing.
|
|
418
|
+
if (!hasPendingFile) {
|
|
419
|
+
const checked = shell(`ufoo bus check ${shellQuote(subscriberId)}`);
|
|
420
|
+
if (!checked.ok) {
|
|
421
|
+
return {
|
|
422
|
+
ok: false,
|
|
423
|
+
summary: "",
|
|
424
|
+
error: checked.error || "ufoo bus check failed",
|
|
425
|
+
handled: 0,
|
|
426
|
+
subscriberId,
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
const parsed = parseBusCheckOutput(checked.output);
|
|
430
|
+
if (parsed.length === 0 && busCheckOutputIndicatesPending(checked.output)) {
|
|
431
|
+
return {
|
|
432
|
+
ok: false,
|
|
433
|
+
summary: "",
|
|
434
|
+
error: "failed to parse ufoo bus check output (pending events detected).",
|
|
435
|
+
handled: 0,
|
|
436
|
+
subscriberId,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
for (const item of parsed) {
|
|
440
|
+
// Notify that we received the message (for immediate display)
|
|
441
|
+
if (onMessageReceived) {
|
|
442
|
+
onMessageReceived({
|
|
443
|
+
from: item.publisher,
|
|
444
|
+
task: item.task,
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const nlResult = await runNl(item.task, state, {
|
|
449
|
+
signal: options.signal,
|
|
450
|
+
});
|
|
451
|
+
const reply = String(formatNl(nlResult, false) || "").replace(/\s+/g, " ").trim() || "Done.";
|
|
452
|
+
const sendRes = shell(`ufoo bus send ${shellQuote(item.publisher)} ${shellQuote(reply.slice(0, 2000))}`);
|
|
453
|
+
if (!sendRes.ok) {
|
|
454
|
+
sendErrors.push(`reply to ${item.publisher} failed: ${sendRes.error || "send failed"}`);
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
handled += 1;
|
|
458
|
+
messageExchanges.push({
|
|
459
|
+
from: item.publisher,
|
|
460
|
+
task: item.task,
|
|
461
|
+
reply,
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (sendErrors.length > 0) {
|
|
467
|
+
return {
|
|
468
|
+
ok: false,
|
|
469
|
+
summary: "",
|
|
470
|
+
error: sendErrors.join("; "),
|
|
471
|
+
handled,
|
|
472
|
+
subscriberId,
|
|
473
|
+
messageExchanges,
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const summary = handled > 0
|
|
478
|
+
? `ubus: handled ${handled} message${handled === 1 ? "" : "s"} for ${subscriberId}.`
|
|
479
|
+
: `ubus: no pending messages for ${subscriberId}.`;
|
|
480
|
+
return {
|
|
481
|
+
ok: true,
|
|
482
|
+
summary,
|
|
483
|
+
error: "",
|
|
484
|
+
handled,
|
|
485
|
+
subscriberId,
|
|
486
|
+
messageExchanges,
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
module.exports = {
|
|
491
|
+
runUbusCommand,
|
|
492
|
+
parseBusCheckOutput,
|
|
493
|
+
extractBusMessageTask,
|
|
494
|
+
runShellCapture,
|
|
495
|
+
stripAnsi,
|
|
496
|
+
busCheckOutputIndicatesPending,
|
|
497
|
+
resolvePendingQueueFile,
|
|
498
|
+
resolveUfooProjectRoot,
|
|
499
|
+
countPendingQueueLines,
|
|
500
|
+
getPendingBusCount,
|
|
501
|
+
drainJsonlFile,
|
|
502
|
+
extractTaskFromBusEvent,
|
|
503
|
+
shouldAutoConsumeBus,
|
|
504
|
+
};
|
package/src/code/dispatch.js
CHANGED
|
@@ -27,12 +27,7 @@ function runToolCall(input = {}, options = {}) {
|
|
|
27
27
|
if (tool === "read") return runReadTool(args, options);
|
|
28
28
|
if (tool === "write") return runWriteTool(args, options);
|
|
29
29
|
if (tool === "edit") return runEditTool(args, options);
|
|
30
|
-
|
|
31
|
-
return {
|
|
32
|
-
ok: false,
|
|
33
|
-
error: "unknown tool",
|
|
34
|
-
supported_tools: TOOL_NAMES.slice(),
|
|
35
|
-
};
|
|
30
|
+
return runBashTool(args, options);
|
|
36
31
|
}
|
|
37
32
|
|
|
38
33
|
module.exports = {
|