u-foo 2.3.2 → 2.3.4
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/package.json +1 -1
- package/src/agent/loopRuntime.js +18 -22
- package/src/agent/ufooAgent.js +6 -24
- package/src/chat/agentBar.js +3 -3
- package/src/chat/inputMath.js +25 -56
- package/src/chat/inputSubmitHandler.js +8 -6
- package/src/code/agent.js +4 -1
- package/src/code/nativeRunner.js +58 -0
- package/src/code/taskDecomposer.js +4 -3
- package/src/code/tui.js +53 -48
- package/src/controller/launchRouting.js +206 -0
- package/src/controller/routerFinalize.js +222 -0
- package/src/daemon/ops.js +6 -2
- package/src/daemon/promptLoop.js +13 -21
package/package.json
CHANGED
package/src/agent/loopRuntime.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { executeControllerTool } = require("./controllerToolExecutor");
|
|
4
4
|
const { createLoopObserver } = require("./loopObservability");
|
|
5
|
+
const { finalizeRouterPayload } = require("../controller/routerFinalize");
|
|
5
6
|
|
|
6
7
|
const DEFAULT_LOOP_OPTIONS = {
|
|
7
8
|
enabled: false,
|
|
@@ -126,34 +127,23 @@ function buildLoopContinuationPrompt({
|
|
|
126
127
|
async function finalizeLoopRun({
|
|
127
128
|
projectRoot,
|
|
128
129
|
payload,
|
|
130
|
+
prompt = "",
|
|
129
131
|
processManager,
|
|
130
132
|
dispatchMessages,
|
|
131
133
|
handleOps,
|
|
132
134
|
markPending,
|
|
133
135
|
finalizeLocally = true,
|
|
134
136
|
}) {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
if (typeof dispatchMessages === "function") {
|
|
149
|
-
await dispatchMessages(projectRoot, dispatch);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const opsResults = typeof handleOps === "function"
|
|
153
|
-
? await handleOps(projectRoot, ops, processManager || null)
|
|
154
|
-
: [];
|
|
155
|
-
|
|
156
|
-
return { ok: true, payload, opsResults };
|
|
137
|
+
return finalizeRouterPayload({
|
|
138
|
+
projectRoot,
|
|
139
|
+
payload,
|
|
140
|
+
prompt,
|
|
141
|
+
processManager: processManager || null,
|
|
142
|
+
dispatchMessages,
|
|
143
|
+
handleOps,
|
|
144
|
+
markPending,
|
|
145
|
+
finalizeLocally,
|
|
146
|
+
});
|
|
157
147
|
}
|
|
158
148
|
|
|
159
149
|
function buildTerminalPayload(reason, lastPayload, rounds, toolCalls, toolErrors, totals = {}) {
|
|
@@ -248,6 +238,7 @@ async function runPromptWithControllerLoop({
|
|
|
248
238
|
return finalizeLoopRun({
|
|
249
239
|
projectRoot,
|
|
250
240
|
payload,
|
|
241
|
+
prompt: currentPrompt,
|
|
251
242
|
processManager,
|
|
252
243
|
dispatchMessages,
|
|
253
244
|
handleOps,
|
|
@@ -261,6 +252,7 @@ async function runPromptWithControllerLoop({
|
|
|
261
252
|
return finalizeLoopRun({
|
|
262
253
|
projectRoot,
|
|
263
254
|
payload,
|
|
255
|
+
prompt: currentPrompt,
|
|
264
256
|
processManager,
|
|
265
257
|
dispatchMessages,
|
|
266
258
|
handleOps,
|
|
@@ -357,6 +349,7 @@ async function runPromptWithControllerLoop({
|
|
|
357
349
|
return finalizeLoopRun({
|
|
358
350
|
projectRoot,
|
|
359
351
|
payload: finalPayload,
|
|
352
|
+
prompt: currentPrompt,
|
|
360
353
|
processManager,
|
|
361
354
|
dispatchMessages,
|
|
362
355
|
handleOps,
|
|
@@ -370,6 +363,7 @@ async function runPromptWithControllerLoop({
|
|
|
370
363
|
return finalizeLoopRun({
|
|
371
364
|
projectRoot,
|
|
372
365
|
payload: finalPayload,
|
|
366
|
+
prompt: currentPrompt,
|
|
373
367
|
processManager,
|
|
374
368
|
dispatchMessages,
|
|
375
369
|
handleOps,
|
|
@@ -430,6 +424,7 @@ async function runPromptWithControllerLoop({
|
|
|
430
424
|
return finalizeLoopRun({
|
|
431
425
|
projectRoot,
|
|
432
426
|
payload: finalPayload,
|
|
427
|
+
prompt: currentPrompt,
|
|
433
428
|
processManager,
|
|
434
429
|
dispatchMessages,
|
|
435
430
|
handleOps,
|
|
@@ -456,6 +451,7 @@ async function runPromptWithControllerLoop({
|
|
|
456
451
|
return finalizeLoopRun({
|
|
457
452
|
projectRoot,
|
|
458
453
|
payload,
|
|
454
|
+
prompt: currentPrompt,
|
|
459
455
|
processManager,
|
|
460
456
|
dispatchMessages,
|
|
461
457
|
handleOps,
|
package/src/agent/ufooAgent.js
CHANGED
|
@@ -9,6 +9,7 @@ const { normalizeProvider, sendUpstreamPrompt } = require("./upstreamTransport")
|
|
|
9
9
|
const { normalizeAgentTypeAlias } = require("../bus/utils");
|
|
10
10
|
const { buildCachedMemoryPrefix } = require("../memory");
|
|
11
11
|
const { listProjectRuntimes, isGlobalControllerProjectRoot } = require("../projects");
|
|
12
|
+
const { assignMissingLaunchNicknames } = require("../controller/launchRouting");
|
|
12
13
|
const {
|
|
13
14
|
CONTROLLER_MODES,
|
|
14
15
|
resolveControllerMode,
|
|
@@ -462,6 +463,8 @@ function buildSystemPrompt(context, options = {}) {
|
|
|
462
463
|
"- When returning tool_call, set done=false and keep dispatch/ops empty for that round.",
|
|
463
464
|
"- Use dispatch_message for direct bus delivery, ack_bus for controller queue acknowledgement, and launch_agent for bounded worker launches.",
|
|
464
465
|
"- When you have enough information, omit tool_call and return the final reply/dispatch/ops with done=true.",
|
|
466
|
+
"- When launching a new coding agent for a user task, include a short task-specific nickname and include a dispatch to that launched nickname with the task.",
|
|
467
|
+
"- Do not emit launch-only ops for delegated work; a launched worker must receive a task in dispatch.",
|
|
465
468
|
"- Do not emit assistant_call or ops.assistant_call; that legacy helper path has been removed.",
|
|
466
469
|
`- Round budget: maxRounds=${loopRuntime.maxRounds || ""}, remainingToolCalls=${loopRuntime.remainingToolCalls || 0}.`,
|
|
467
470
|
agentGuidance,
|
|
@@ -505,6 +508,8 @@ function buildSystemPrompt(context, options = {}) {
|
|
|
505
508
|
"- For short-lived exploration, prefer a dispatch to an online agent or reply with a clarification.",
|
|
506
509
|
"- Primary routing signal is semantic continuity from agent_prompt_history; prefer the agent that already handled similar prompts.",
|
|
507
510
|
"- Launch a new coding agent when the request is a new topic without clear ownership in existing histories.",
|
|
511
|
+
"- When launching a new coding agent for a user task, include a short task-specific nickname and include a dispatch to that launched nickname with the task.",
|
|
512
|
+
"- Do not emit launch-only ops for delegated work; a launched worker must receive a task in dispatch.",
|
|
508
513
|
"- dispatch.injection_mode defaults to immediate when omitted.",
|
|
509
514
|
"- Use queued only when routing a chat-dialog request that is clearly a new unrelated task for an agent whose recent prompt history shows a different ongoing thread.",
|
|
510
515
|
"- If the new request strongly continues the target agent's recent prompt history, keep injection_mode immediate even when that agent is busy.",
|
|
@@ -588,21 +593,6 @@ function buildRouteAgentSystemPrompt(context, options = {}) {
|
|
|
588
593
|
].join("\n");
|
|
589
594
|
}
|
|
590
595
|
|
|
591
|
-
function extractNickname(prompt) {
|
|
592
|
-
if (!prompt) return "";
|
|
593
|
-
const patterns = [
|
|
594
|
-
/(?:叫|名为|叫做|取名|昵称)\s*([A-Za-z0-9_-]{1,32})/i,
|
|
595
|
-
/(?:named|name)\s+([A-Za-z0-9_-]{1,32})/i,
|
|
596
|
-
];
|
|
597
|
-
for (const re of patterns) {
|
|
598
|
-
const match = prompt.match(re);
|
|
599
|
-
if (match && match[1]) return match[1];
|
|
600
|
-
}
|
|
601
|
-
const quoted = prompt.match(/[“"']([^“"'\\]{1,32})[”"']/);
|
|
602
|
-
if (quoted && quoted[1]) return quoted[1];
|
|
603
|
-
return "";
|
|
604
|
-
}
|
|
605
|
-
|
|
606
596
|
function shouldUseDirectProvider(value = "") {
|
|
607
597
|
const provider = normalizeProvider(value);
|
|
608
598
|
return provider === "ucode" || provider === "codex" || provider === "claude";
|
|
@@ -733,15 +723,7 @@ async function runUfooAgent({
|
|
|
733
723
|
payload = { reply: text, dispatch: [], ops: [] };
|
|
734
724
|
}
|
|
735
725
|
|
|
736
|
-
|
|
737
|
-
if (fallbackNickname && payload && Array.isArray(payload.ops)) {
|
|
738
|
-
for (const op of payload.ops) {
|
|
739
|
-
if (op && (op.action === "launch" || op.action === "rename") && !op.nickname) {
|
|
740
|
-
op.nickname = fallbackNickname;
|
|
741
|
-
break;
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
}
|
|
726
|
+
payload = assignMissingLaunchNicknames(payload, { prompt, context: bus });
|
|
745
727
|
|
|
746
728
|
saveSessionState(projectRoot, {
|
|
747
729
|
provider,
|
package/src/chat/agentBar.js
CHANGED
|
@@ -33,8 +33,8 @@ function computeAgentBar(options = {}) {
|
|
|
33
33
|
let windowItems = Math.max(1, Math.min(maxAgentWindow, activeAgents.length));
|
|
34
34
|
let start = agentListWindowStart;
|
|
35
35
|
const ufooItem = focusMode === "dashboard" && selectedAgentIndex === 0
|
|
36
|
-
? "\x1b[90;
|
|
37
|
-
: "\x1b[
|
|
36
|
+
? "\x1b[90;7mufoo\x1b[0m"
|
|
37
|
+
: "\x1b[36mufoo\x1b[0m";
|
|
38
38
|
const ufooLen = stripAnsi(ufooItem).length;
|
|
39
39
|
|
|
40
40
|
const computeStart = (items) => {
|
|
@@ -78,7 +78,7 @@ function computeAgentBar(options = {}) {
|
|
|
78
78
|
const prefix = indicator
|
|
79
79
|
? `${indicatorColor}${indicator}\x1b[0m`
|
|
80
80
|
: "";
|
|
81
|
-
const idx = s + i + 1; // +1 for
|
|
81
|
+
const idx = s + i + 1; // +1 for ufoo chat at index 0
|
|
82
82
|
if (focusMode === "dashboard" && idx === selectedAgentIndex) {
|
|
83
83
|
return `${prefix}\x1b[90;7m${label}\x1b[0m`;
|
|
84
84
|
}
|
package/src/chat/inputMath.js
CHANGED
|
@@ -76,35 +76,30 @@ function getWrapWidth(input, fallbackWidth) {
|
|
|
76
76
|
|
|
77
77
|
/**
|
|
78
78
|
* Simulate blessed's wrapping for a single logical line (no newlines).
|
|
79
|
-
* Returns
|
|
79
|
+
* Returns the zero-based visual row and column at the end of the line.
|
|
80
80
|
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* to the next line. We replicate this behavior so cursor math matches what
|
|
85
|
-
* blessed actually renders.
|
|
81
|
+
* A cursor at exactly width cells is still at the end of the current visual
|
|
82
|
+
* row; it should not become a phantom row until another character or an
|
|
83
|
+
* explicit newline is processed.
|
|
86
84
|
*/
|
|
87
|
-
function
|
|
85
|
+
function measureLineEnd(line, width, strWidth) {
|
|
88
86
|
const chars = Array.from(String(line || ""));
|
|
87
|
+
let row = 0;
|
|
89
88
|
let col = 0;
|
|
90
|
-
let rows = 1;
|
|
91
89
|
for (const ch of chars) {
|
|
92
90
|
const w = safeStrWidth(strWidth, ch);
|
|
93
91
|
if (w === 0) continue;
|
|
94
|
-
if (col
|
|
95
|
-
|
|
96
|
-
col =
|
|
92
|
+
if (col > 0 && col + w > width) {
|
|
93
|
+
row += 1;
|
|
94
|
+
col = 0;
|
|
97
95
|
}
|
|
98
96
|
col += w;
|
|
99
97
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
col = 0;
|
|
106
|
-
}
|
|
107
|
-
return { rows, lastCol: col };
|
|
98
|
+
return { row, col };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function countLineRows(line, width, strWidth) {
|
|
102
|
+
return measureLineEnd(line, width, strWidth).row + 1;
|
|
108
103
|
}
|
|
109
104
|
|
|
110
105
|
function countLines(text, width, strWidth) {
|
|
@@ -114,8 +109,7 @@ function countLines(text, width, strWidth) {
|
|
|
114
109
|
const lines = expanded.split("\n");
|
|
115
110
|
let total = 0;
|
|
116
111
|
for (const line of lines) {
|
|
117
|
-
|
|
118
|
-
total += (lastCol === 0 && rows > 1) ? rows - 1 : rows;
|
|
112
|
+
total += countLineRows(line, width, strWidth);
|
|
119
113
|
}
|
|
120
114
|
return total;
|
|
121
115
|
}
|
|
@@ -136,28 +130,10 @@ function getCursorRowCol(text, pos, width, strWidth) {
|
|
|
136
130
|
for (let i = 0; i < lines.length; i += 1) {
|
|
137
131
|
const line = lines[i] || "";
|
|
138
132
|
if (i < lines.length - 1) {
|
|
139
|
-
|
|
140
|
-
row += (lastCol === 0 && rows > 1) ? rows - 1 : rows;
|
|
133
|
+
row += countLineRows(line, width, strWidth);
|
|
141
134
|
} else {
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
for (const ch of chars) {
|
|
145
|
-
const w = safeStrWidth(strWidth, ch);
|
|
146
|
-
if (w === 0) continue;
|
|
147
|
-
if (col >= width) {
|
|
148
|
-
row += 1;
|
|
149
|
-
col = col - width;
|
|
150
|
-
}
|
|
151
|
-
col += w;
|
|
152
|
-
}
|
|
153
|
-
if (col > width) {
|
|
154
|
-
row += 1;
|
|
155
|
-
col = col - width;
|
|
156
|
-
} else if (col === width) {
|
|
157
|
-
row += 1;
|
|
158
|
-
col = 0;
|
|
159
|
-
}
|
|
160
|
-
return { row, col };
|
|
135
|
+
const measured = measureLineEnd(line, width, strWidth);
|
|
136
|
+
return { row: row + measured.row, col: measured.col };
|
|
161
137
|
}
|
|
162
138
|
}
|
|
163
139
|
return { row, col: 0 };
|
|
@@ -187,27 +163,20 @@ function getCursorPosForRowCol(text, targetRow, targetCol, width, strWidth) {
|
|
|
187
163
|
lineOffset += ch.length;
|
|
188
164
|
continue;
|
|
189
165
|
}
|
|
190
|
-
if (col
|
|
166
|
+
if (col > 0 && col + w > width) {
|
|
167
|
+
if (row === targetRow && targetCol >= col) {
|
|
168
|
+
return expandedToOriginal(original, expPos + lineOffset);
|
|
169
|
+
}
|
|
191
170
|
row += 1;
|
|
192
|
-
col =
|
|
171
|
+
col = 0;
|
|
193
172
|
}
|
|
194
173
|
if (row === targetRow && col + w > targetCol) {
|
|
195
174
|
return expandedToOriginal(original, expPos + lineOffset);
|
|
196
175
|
}
|
|
197
176
|
col += w;
|
|
198
177
|
lineOffset += ch.length;
|
|
199
|
-
if (col
|
|
200
|
-
|
|
201
|
-
return expandedToOriginal(original, expPos + lineOffset);
|
|
202
|
-
}
|
|
203
|
-
row += 1;
|
|
204
|
-
col = col - width;
|
|
205
|
-
} else if (col === width) {
|
|
206
|
-
if (row === targetRow) {
|
|
207
|
-
return expandedToOriginal(original, expPos + lineOffset);
|
|
208
|
-
}
|
|
209
|
-
row += 1;
|
|
210
|
-
col = 0;
|
|
178
|
+
if (row === targetRow && col === targetCol) {
|
|
179
|
+
return expandedToOriginal(original, expPos + lineOffset);
|
|
211
180
|
}
|
|
212
181
|
}
|
|
213
182
|
|
|
@@ -30,8 +30,10 @@ function createInputSubmitHandler(options = {}) {
|
|
|
30
30
|
throw new Error("createInputSubmitHandler requires a mutable state object");
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
function
|
|
34
|
-
|
|
33
|
+
function userEcho(text, targetLabel = "") {
|
|
34
|
+
const body = escapeBlessed(text);
|
|
35
|
+
if (!targetLabel) return body;
|
|
36
|
+
return `{magenta-fg}@${escapeBlessed(targetLabel)}{/magenta-fg} ${body}`;
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
async function tryActivateTargetAgent(agentId) {
|
|
@@ -92,7 +94,7 @@ function createInputSubmitHandler(options = {}) {
|
|
|
92
94
|
const label = getAgentLabel(state.targetAgent);
|
|
93
95
|
logMessage(
|
|
94
96
|
"user",
|
|
95
|
-
|
|
97
|
+
userEcho(text, label)
|
|
96
98
|
);
|
|
97
99
|
renderScreen(); // Immediately render the user message
|
|
98
100
|
markPendingDelivery(state.targetAgent);
|
|
@@ -126,7 +128,7 @@ function createInputSubmitHandler(options = {}) {
|
|
|
126
128
|
const message = atTarget.message.trim();
|
|
127
129
|
logMessage(
|
|
128
130
|
"user",
|
|
129
|
-
|
|
131
|
+
userEcho(message, atTarget.target)
|
|
130
132
|
);
|
|
131
133
|
renderScreen(); // Immediately render the user message
|
|
132
134
|
markPendingDelivery(resolvedTarget);
|
|
@@ -143,7 +145,7 @@ function createInputSubmitHandler(options = {}) {
|
|
|
143
145
|
|
|
144
146
|
if (text.startsWith("/")) {
|
|
145
147
|
if (shouldEchoCommandInChat(text)) {
|
|
146
|
-
logMessage("user",
|
|
148
|
+
logMessage("user", userEcho(text));
|
|
147
149
|
renderScreen(); // Render slash command immediately
|
|
148
150
|
}
|
|
149
151
|
try {
|
|
@@ -189,7 +191,7 @@ function createInputSubmitHandler(options = {}) {
|
|
|
189
191
|
allow_relevance_queue: true,
|
|
190
192
|
},
|
|
191
193
|
});
|
|
192
|
-
logMessage("user",
|
|
194
|
+
logMessage("user", userEcho(text));
|
|
193
195
|
renderScreen(); // Render plain text message immediately
|
|
194
196
|
}
|
|
195
197
|
|
package/src/code/agent.js
CHANGED
|
@@ -1119,6 +1119,7 @@ async function runUbusCommand(state = {}, options = {}) {
|
|
|
1119
1119
|
// eslint-disable-next-line no-await-in-loop
|
|
1120
1120
|
nlResult = await runNl(message.task, state, {
|
|
1121
1121
|
onProgress: progressReporter,
|
|
1122
|
+
signal: options.signal,
|
|
1122
1123
|
});
|
|
1123
1124
|
} catch (err) {
|
|
1124
1125
|
sendErrors.push(`task from ${message.publisher} failed: ${err && err.message ? err.message : "task failed"}`);
|
|
@@ -1180,7 +1181,9 @@ async function runUbusCommand(state = {}, options = {}) {
|
|
|
1180
1181
|
});
|
|
1181
1182
|
}
|
|
1182
1183
|
|
|
1183
|
-
const nlResult = await runNl(item.task, state
|
|
1184
|
+
const nlResult = await runNl(item.task, state, {
|
|
1185
|
+
signal: options.signal,
|
|
1186
|
+
});
|
|
1184
1187
|
const reply = String(formatNl(nlResult, false) || "").replace(/\s+/g, " ").trim() || "Done.";
|
|
1185
1188
|
const sendRes = shell(`ufoo bus send ${shellQuote(item.publisher)} ${shellQuote(reply.slice(0, 2000))}`);
|
|
1186
1189
|
if (!sendRes.ok) {
|
package/src/code/nativeRunner.js
CHANGED
|
@@ -9,6 +9,8 @@ const { getBashToolDescription } = require("./prompts/toolDescriptions/bash");
|
|
|
9
9
|
const CORE_TOOL_NAMES = new Set(["read", "write", "edit", "bash"]);
|
|
10
10
|
const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
|
|
11
11
|
const DEFAULT_ANTHROPIC_BASE_URL = "https://api.anthropic.com/v1";
|
|
12
|
+
const DEFAULT_MAX_NATIVE_TOOL_CALLS = 12;
|
|
13
|
+
const DEFAULT_MAX_NATIVE_TOOL_ERRORS = 1;
|
|
12
14
|
|
|
13
15
|
function nowMs() {
|
|
14
16
|
return Date.now();
|
|
@@ -20,6 +22,36 @@ function normalizeTimeoutMs(value) {
|
|
|
20
22
|
return Math.max(1000, Math.floor(parsed));
|
|
21
23
|
}
|
|
22
24
|
|
|
25
|
+
function normalizePositiveInt(value, fallback) {
|
|
26
|
+
const parsed = Number.parseInt(String(value || ""), 10);
|
|
27
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
|
|
28
|
+
return Math.floor(parsed);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function resolveNativeToolBudget(env = process.env) {
|
|
32
|
+
return {
|
|
33
|
+
maxToolCalls: normalizePositiveInt(env.UFOO_UCODE_MAX_TOOL_CALLS, DEFAULT_MAX_NATIVE_TOOL_CALLS),
|
|
34
|
+
maxToolErrors: normalizePositiveInt(env.UFOO_UCODE_MAX_TOOL_ERRORS, DEFAULT_MAX_NATIVE_TOOL_ERRORS),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function enforceNativeToolBudget({
|
|
39
|
+
toolCallsExecuted = 0,
|
|
40
|
+
toolErrors = 0,
|
|
41
|
+
maxToolCalls = DEFAULT_MAX_NATIVE_TOOL_CALLS,
|
|
42
|
+
maxToolErrors = DEFAULT_MAX_NATIVE_TOOL_ERRORS,
|
|
43
|
+
lastTool = "",
|
|
44
|
+
lastError = "",
|
|
45
|
+
} = {}) {
|
|
46
|
+
if (toolCallsExecuted > maxToolCalls) {
|
|
47
|
+
throw new Error(`tool call budget exceeded (${maxToolCalls})`);
|
|
48
|
+
}
|
|
49
|
+
if (toolErrors >= maxToolErrors) {
|
|
50
|
+
const detail = [lastTool, lastError].filter(Boolean).join(": ");
|
|
51
|
+
throw new Error(`tool error budget exceeded (${maxToolErrors})${detail ? `: ${detail}` : ""}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
23
55
|
function createGuards({ signal = null, timeoutMs = 300000 } = {}) {
|
|
24
56
|
const startedAt = nowMs();
|
|
25
57
|
const budgetMs = normalizeTimeoutMs(timeoutMs);
|
|
@@ -907,6 +939,8 @@ async function runNativeLoopOpenAi({
|
|
|
907
939
|
let aggregated = "";
|
|
908
940
|
let streamed = false;
|
|
909
941
|
let toolCallsExecuted = 0;
|
|
942
|
+
let toolErrors = 0;
|
|
943
|
+
const toolBudget = resolveNativeToolBudget();
|
|
910
944
|
|
|
911
945
|
while (true) {
|
|
912
946
|
guards.ensureActive();
|
|
@@ -991,6 +1025,17 @@ async function runNativeLoopOpenAi({
|
|
|
991
1025
|
onToolEvent,
|
|
992
1026
|
});
|
|
993
1027
|
toolCallsExecuted += 1;
|
|
1028
|
+
if (!toolResult || toolResult.ok === false) {
|
|
1029
|
+
toolErrors += 1;
|
|
1030
|
+
}
|
|
1031
|
+
enforceNativeToolBudget({
|
|
1032
|
+
toolCallsExecuted,
|
|
1033
|
+
toolErrors,
|
|
1034
|
+
maxToolCalls: toolBudget.maxToolCalls,
|
|
1035
|
+
maxToolErrors: toolBudget.maxToolErrors,
|
|
1036
|
+
lastTool: toolCall.function.name,
|
|
1037
|
+
lastError: toolResult && toolResult.error ? String(toolResult.error) : "",
|
|
1038
|
+
});
|
|
994
1039
|
messages.push({
|
|
995
1040
|
role: "tool",
|
|
996
1041
|
tool_call_id: toolCall.id,
|
|
@@ -1034,6 +1079,8 @@ async function runNativeLoopAnthropic({
|
|
|
1034
1079
|
let aggregated = "";
|
|
1035
1080
|
let streamed = false;
|
|
1036
1081
|
let toolCallsExecuted = 0;
|
|
1082
|
+
let toolErrors = 0;
|
|
1083
|
+
const toolBudget = resolveNativeToolBudget();
|
|
1037
1084
|
|
|
1038
1085
|
while (true) {
|
|
1039
1086
|
guards.ensureActive();
|
|
@@ -1109,6 +1156,17 @@ async function runNativeLoopAnthropic({
|
|
|
1109
1156
|
onToolEvent,
|
|
1110
1157
|
});
|
|
1111
1158
|
toolCallsExecuted += 1;
|
|
1159
|
+
if (!toolResult || toolResult.ok === false) {
|
|
1160
|
+
toolErrors += 1;
|
|
1161
|
+
}
|
|
1162
|
+
enforceNativeToolBudget({
|
|
1163
|
+
toolCallsExecuted,
|
|
1164
|
+
toolErrors,
|
|
1165
|
+
maxToolCalls: toolBudget.maxToolCalls,
|
|
1166
|
+
maxToolErrors: toolBudget.maxToolErrors,
|
|
1167
|
+
lastTool: call.name,
|
|
1168
|
+
lastError: toolResult && toolResult.error ? String(toolResult.error) : "",
|
|
1169
|
+
});
|
|
1112
1170
|
toolResults.push({
|
|
1113
1171
|
type: "tool_result",
|
|
1114
1172
|
tool_use_id: String(call.id || ""),
|
|
@@ -147,8 +147,9 @@ async function runDecomposedTask({
|
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
-
// Stop on
|
|
151
|
-
|
|
150
|
+
// Stop on any step failure. A failed tool/provider call means the
|
|
151
|
+
// current plan is no longer reliable, and continuing can trigger loops.
|
|
152
|
+
if (!stepResult.ok) {
|
|
152
153
|
return {
|
|
153
154
|
ok: false,
|
|
154
155
|
error: `Failed at ${step.name}: ${stepResult.error}`,
|
|
@@ -266,4 +267,4 @@ module.exports = {
|
|
|
266
267
|
runDecomposedTask,
|
|
267
268
|
compileSummary,
|
|
268
269
|
createBusProgressReporter,
|
|
269
|
-
};
|
|
270
|
+
};
|
package/src/code/tui.js
CHANGED
|
@@ -993,6 +993,7 @@ function runUcodeTui({
|
|
|
993
993
|
const ubusResult = await runUbusCommand(state, {
|
|
994
994
|
workspaceRoot,
|
|
995
995
|
subscriberId: autoBusSubscriberId,
|
|
996
|
+
signal: abortController.signal,
|
|
996
997
|
onMessageReceived: (msg) => {
|
|
997
998
|
// Display the incoming message immediately
|
|
998
999
|
const { extractAgentNickname } = require("./agent");
|
|
@@ -1254,59 +1255,63 @@ function runUcodeTui({
|
|
|
1254
1255
|
});
|
|
1255
1256
|
let streamState = null;
|
|
1256
1257
|
let renderedToolLogCount = 0;
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1258
|
+
let nlResult = null;
|
|
1259
|
+
try {
|
|
1260
|
+
nlResult = await runNaturalLanguageTask(result.task, state, {
|
|
1261
|
+
signal: abortController.signal,
|
|
1262
|
+
onDelta: (delta) => {
|
|
1263
|
+
const text = escapeStripper.write(String(delta || ""));
|
|
1264
|
+
if (!text) return;
|
|
1265
|
+
if (!streamState) {
|
|
1266
|
+
streamState = createNlStreamState();
|
|
1267
|
+
}
|
|
1268
|
+
appendNlStreamDelta(streamState, text);
|
|
1269
|
+
},
|
|
1270
|
+
onToolLog: (entry) => {
|
|
1271
|
+
renderedToolLogCount += 1;
|
|
1272
|
+
logToolHint(entry);
|
|
1273
|
+
},
|
|
1274
|
+
});
|
|
1275
|
+
const tail = escapeStripper.flush();
|
|
1276
|
+
if (tail) {
|
|
1262
1277
|
if (!streamState) {
|
|
1263
1278
|
streamState = createNlStreamState();
|
|
1264
1279
|
}
|
|
1265
|
-
appendNlStreamDelta(streamState,
|
|
1266
|
-
},
|
|
1267
|
-
onToolLog: (entry) => {
|
|
1268
|
-
renderedToolLogCount += 1;
|
|
1269
|
-
logToolHint(entry);
|
|
1270
|
-
},
|
|
1271
|
-
});
|
|
1272
|
-
const tail = escapeStripper.flush();
|
|
1273
|
-
if (tail) {
|
|
1274
|
-
if (!streamState) {
|
|
1275
|
-
streamState = createNlStreamState();
|
|
1280
|
+
appendNlStreamDelta(streamState, tail);
|
|
1276
1281
|
}
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
updateStatus("", "none");
|
|
1281
|
-
let finalStreamInfo = { lastChar: "" };
|
|
1282
|
-
if (streamState) {
|
|
1283
|
-
finalStreamInfo = finalizeNlStream(streamState);
|
|
1284
|
-
}
|
|
1285
|
-
if (Array.isArray(nlResult && nlResult.logs) && nlResult.logs.length > renderedToolLogCount) {
|
|
1286
|
-
for (const entry of nlResult.logs.slice(renderedToolLogCount)) {
|
|
1287
|
-
logToolHint(entry);
|
|
1282
|
+
let finalStreamInfo = { lastChar: "" };
|
|
1283
|
+
if (streamState) {
|
|
1284
|
+
finalStreamInfo = finalizeNlStream(streamState);
|
|
1288
1285
|
}
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
&&
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1286
|
+
if (Array.isArray(nlResult && nlResult.logs) && nlResult.logs.length > renderedToolLogCount) {
|
|
1287
|
+
for (const entry of nlResult.logs.slice(renderedToolLogCount)) {
|
|
1288
|
+
logToolHint(entry);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
const streamed = Boolean(nlResult && nlResult.streamed);
|
|
1292
|
+
const hasVisibleStreamText = Boolean(
|
|
1293
|
+
streamState
|
|
1294
|
+
&& typeof streamState.full === "string"
|
|
1295
|
+
&& /[^\s]/.test(streamState.full)
|
|
1296
|
+
);
|
|
1297
|
+
const streamLastChar = nlResult && typeof nlResult.streamLastChar === "string"
|
|
1298
|
+
? nlResult.streamLastChar.slice(-1)
|
|
1299
|
+
: finalStreamInfo.lastChar;
|
|
1300
|
+
if (streamed && hasVisibleStreamText && streamLastChar !== "\n") {
|
|
1301
|
+
logBox.log("");
|
|
1302
|
+
screen.render();
|
|
1303
|
+
}
|
|
1304
|
+
const shouldSkipSummary = Boolean(streamed && nlResult && nlResult.ok && hasVisibleStreamText);
|
|
1305
|
+
if (!shouldSkipSummary) {
|
|
1306
|
+
logText(formatNlResult(nlResult, false));
|
|
1307
|
+
}
|
|
1308
|
+
const persisted = persistSessionState(state);
|
|
1309
|
+
if (!persisted || persisted.ok === false) {
|
|
1310
|
+
logText(`Error: failed to persist session ${state.sessionId}: ${(persisted && persisted.error) || "unknown error"}`);
|
|
1311
|
+
}
|
|
1312
|
+
} finally {
|
|
1313
|
+
pendingTask = null;
|
|
1314
|
+
updateStatus("", "none");
|
|
1310
1315
|
}
|
|
1311
1316
|
}
|
|
1312
1317
|
};
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
function asTrimmedString(value) {
|
|
4
|
+
return typeof value === "string" ? value.trim() : "";
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function normalizeNicknameSegment(value = "", fallback = "task") {
|
|
8
|
+
const normalized = asTrimmedString(value)
|
|
9
|
+
.toLowerCase()
|
|
10
|
+
.replace(/[^a-z0-9_-]+/g, "-")
|
|
11
|
+
.replace(/-+/g, "-")
|
|
12
|
+
.replace(/^-+|-+$/g, "");
|
|
13
|
+
return normalized || fallback;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function stripRoutingPromptMetadata(prompt = "") {
|
|
17
|
+
let text = String(prompt || "");
|
|
18
|
+
const markers = [
|
|
19
|
+
"\nRouting request metadata (JSON):",
|
|
20
|
+
"\nPrivate runtime reports for ufoo-agent (JSON):",
|
|
21
|
+
"\nController loop state (JSON):",
|
|
22
|
+
"\nController tool results so far (JSON):",
|
|
23
|
+
];
|
|
24
|
+
for (const marker of markers) {
|
|
25
|
+
const index = text.indexOf(marker);
|
|
26
|
+
if (index >= 0) {
|
|
27
|
+
text = text.slice(0, index);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return text.trim();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function normalizeLaunchAgentForNickname(agent = "") {
|
|
34
|
+
const raw = asTrimmedString(agent).toLowerCase();
|
|
35
|
+
if (raw === "claude" || raw === "claude-code" || raw === "uclaude") return "claude";
|
|
36
|
+
if (raw === "codex" || raw === "ucodex") return "codex";
|
|
37
|
+
if (raw === "ufoo" || raw === "ucode" || raw === "ufoo-code") return "ucode";
|
|
38
|
+
return "";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function nicknameCapturePattern() {
|
|
42
|
+
return "(?:`([^`]{1,32})`|[\"'“”]([^\"'“”]{1,32})[\"'“”]?|([A-Za-z0-9_-]{1,32}))";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function pickNicknameCapture(match) {
|
|
46
|
+
if (!match) return "";
|
|
47
|
+
for (let i = 1; i < match.length; i += 1) {
|
|
48
|
+
const value = asTrimmedString(match[i]);
|
|
49
|
+
if (value) return normalizeNicknameSegment(value, "");
|
|
50
|
+
}
|
|
51
|
+
return "";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function extractRequestedLaunchNickname(prompt = "") {
|
|
55
|
+
const text = stripRoutingPromptMetadata(prompt);
|
|
56
|
+
if (!text) return "";
|
|
57
|
+
const value = nicknameCapturePattern();
|
|
58
|
+
const patterns = [
|
|
59
|
+
new RegExp(`(?:launch|start|create|spawn|open|new|启动|新建|创建|拉起)[\\s\\S]{0,80}(?:named|name|nickname|叫做|叫|名为|昵称|取名|为)\\s*${value}`, "i"),
|
|
60
|
+
new RegExp(`(?:named|name|nickname|叫做|叫|名为|昵称|取名)\\s*${value}[\\s\\S]{0,80}(?:agent|worker|codex|claude|ucode|ufoo|代理|智能体)`, "i"),
|
|
61
|
+
];
|
|
62
|
+
for (const re of patterns) {
|
|
63
|
+
const nickname = pickNicknameCapture(text.match(re));
|
|
64
|
+
if (nickname) return nickname;
|
|
65
|
+
}
|
|
66
|
+
return "";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function collectKeywordTokens(text = "") {
|
|
70
|
+
const specs = [
|
|
71
|
+
[/处理|修复|解决|排查/g, "fix"],
|
|
72
|
+
[/路由|主路由/g, "route"],
|
|
73
|
+
[/启动|新启动|拉起/g, "launch"],
|
|
74
|
+
[/投递|派发|发送|分发/g, "dispatch"],
|
|
75
|
+
[/任务|工作/g, "task"],
|
|
76
|
+
[/测试|验证/g, "test"],
|
|
77
|
+
[/前端|界面/g, "frontend"],
|
|
78
|
+
[/后端|服务端/g, "backend"],
|
|
79
|
+
[/设计/g, "design"],
|
|
80
|
+
[/审查|评审/g, "review"],
|
|
81
|
+
[/调试/g, "debug"],
|
|
82
|
+
[/重构/g, "refactor"],
|
|
83
|
+
[/发布/g, "release"],
|
|
84
|
+
[/文档/g, "docs"],
|
|
85
|
+
[/性能/g, "perf"],
|
|
86
|
+
];
|
|
87
|
+
const matches = [];
|
|
88
|
+
for (const [re, token] of specs) {
|
|
89
|
+
re.lastIndex = 0;
|
|
90
|
+
let match = re.exec(text);
|
|
91
|
+
while (match) {
|
|
92
|
+
matches.push({ index: match.index, token });
|
|
93
|
+
match = re.exec(text);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return matches
|
|
97
|
+
.sort((a, b) => a.index - b.index)
|
|
98
|
+
.map((item) => item.token);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const ENGLISH_STOPWORDS = new Set([
|
|
102
|
+
"the", "a", "an", "and", "or", "to", "for", "with", "from", "this", "that",
|
|
103
|
+
"please", "pls", "ufoo", "chat", "agent", "agents", "worker", "workers",
|
|
104
|
+
"nickname", "nick", "name", "named", "new", "current", "online", "source",
|
|
105
|
+
"metadata", "json", "request", "user", "feedback", "bug", "issue",
|
|
106
|
+
]);
|
|
107
|
+
|
|
108
|
+
function collectEnglishTokens(text = "") {
|
|
109
|
+
const rawTokens = String(text || "").toLowerCase().match(/[a-z][a-z0-9_-]{1,24}/g) || [];
|
|
110
|
+
return rawTokens
|
|
111
|
+
.map((token) => normalizeNicknameSegment(token, ""))
|
|
112
|
+
.filter((token) => token && !ENGLISH_STOPWORDS.has(token));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function uniqueOrdered(values = []) {
|
|
116
|
+
const seen = new Set();
|
|
117
|
+
const result = [];
|
|
118
|
+
for (const value of values) {
|
|
119
|
+
const token = normalizeNicknameSegment(value, "");
|
|
120
|
+
if (!token || seen.has(token)) continue;
|
|
121
|
+
seen.add(token);
|
|
122
|
+
result.push(token);
|
|
123
|
+
}
|
|
124
|
+
return result;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function toExistingNicknameSet(existingNicknames = []) {
|
|
128
|
+
if (existingNicknames instanceof Set) return new Set(existingNicknames);
|
|
129
|
+
if (Array.isArray(existingNicknames)) return new Set(existingNicknames.map((item) => normalizeNicknameSegment(item, "")));
|
|
130
|
+
if (existingNicknames && typeof existingNicknames === "object") {
|
|
131
|
+
return new Set(Object.keys(existingNicknames).map((item) => normalizeNicknameSegment(item, "")));
|
|
132
|
+
}
|
|
133
|
+
return new Set();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function truncateNickname(value = "", maxLength = 32) {
|
|
137
|
+
const normalized = normalizeNicknameSegment(value, "task");
|
|
138
|
+
if (normalized.length <= maxLength) return normalized;
|
|
139
|
+
return normalized.slice(0, maxLength).replace(/-+$/g, "") || "task";
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function makeUniqueNickname(base, existingNicknames = []) {
|
|
143
|
+
const existing = toExistingNicknameSet(existingNicknames);
|
|
144
|
+
const normalizedBase = truncateNickname(base);
|
|
145
|
+
if (!existing.has(normalizedBase)) return normalizedBase;
|
|
146
|
+
for (let i = 2; i < 100; i += 1) {
|
|
147
|
+
const suffix = `-${i}`;
|
|
148
|
+
const candidate = `${truncateNickname(normalizedBase, 32 - suffix.length)}${suffix}`;
|
|
149
|
+
if (!existing.has(candidate)) return candidate;
|
|
150
|
+
}
|
|
151
|
+
return `${truncateNickname(normalizedBase, 27)}-${Date.now().toString(36).slice(-4)}`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function collectExistingNicknames(context = {}) {
|
|
155
|
+
const values = [];
|
|
156
|
+
if (context && typeof context === "object") {
|
|
157
|
+
if (context.nicknames && typeof context.nicknames === "object") {
|
|
158
|
+
values.push(...Object.keys(context.nicknames));
|
|
159
|
+
}
|
|
160
|
+
if (Array.isArray(context.agents)) {
|
|
161
|
+
for (const agent of context.agents) {
|
|
162
|
+
if (agent && agent.nickname) values.push(agent.nickname);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return values;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function buildLaunchNickname(prompt = "", agent = "", existingNicknames = []) {
|
|
170
|
+
const explicit = extractRequestedLaunchNickname(prompt);
|
|
171
|
+
if (explicit) return makeUniqueNickname(explicit, existingNicknames);
|
|
172
|
+
|
|
173
|
+
const corePrompt = stripRoutingPromptMetadata(prompt);
|
|
174
|
+
const agentPrefix = normalizeLaunchAgentForNickname(agent) || "agent";
|
|
175
|
+
const tokens = uniqueOrdered([
|
|
176
|
+
...collectKeywordTokens(corePrompt),
|
|
177
|
+
...collectEnglishTokens(corePrompt),
|
|
178
|
+
]).filter((token) => token !== agentPrefix);
|
|
179
|
+
const stem = tokens.slice(0, 2).join("-") || "task";
|
|
180
|
+
return makeUniqueNickname(`${agentPrefix}-${stem}`, existingNicknames);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function assignMissingLaunchNicknames(payload = {}, options = {}) {
|
|
184
|
+
if (!payload || typeof payload !== "object" || !Array.isArray(payload.ops)) return payload;
|
|
185
|
+
const existing = new Set(collectExistingNicknames(options.context));
|
|
186
|
+
let changed = false;
|
|
187
|
+
const ops = payload.ops.map((op) => {
|
|
188
|
+
if (!op || op.action !== "launch" || op.nickname) return op;
|
|
189
|
+
const count = Number.parseInt(op.count || 1, 10);
|
|
190
|
+
if (Number.isFinite(count) && count > 1) return op;
|
|
191
|
+
const nickname = buildLaunchNickname(options.prompt || "", op.agent || "", existing);
|
|
192
|
+
existing.add(nickname);
|
|
193
|
+
changed = true;
|
|
194
|
+
return { ...op, nickname };
|
|
195
|
+
});
|
|
196
|
+
return changed ? { ...payload, ops } : payload;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
module.exports = {
|
|
200
|
+
assignMissingLaunchNicknames,
|
|
201
|
+
buildLaunchNickname,
|
|
202
|
+
collectExistingNicknames,
|
|
203
|
+
extractRequestedLaunchNickname,
|
|
204
|
+
normalizeLaunchAgentForNickname,
|
|
205
|
+
stripRoutingPromptMetadata,
|
|
206
|
+
};
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
normalizeLaunchAgentForNickname,
|
|
5
|
+
stripRoutingPromptMetadata,
|
|
6
|
+
} = require("./launchRouting");
|
|
7
|
+
|
|
8
|
+
const LAUNCH_TARGET_PREFIX = "__ufoo_launch_";
|
|
9
|
+
|
|
10
|
+
function normalizePayload(payload) {
|
|
11
|
+
if (!payload || typeof payload !== "object") {
|
|
12
|
+
return { reply: "", dispatch: [], ops: [] };
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
...payload,
|
|
16
|
+
reply: typeof payload.reply === "string" ? payload.reply : "",
|
|
17
|
+
dispatch: Array.isArray(payload.dispatch) ? payload.dispatch : [],
|
|
18
|
+
ops: Array.isArray(payload.ops) ? payload.ops : [],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isLaunchOp(op) {
|
|
23
|
+
return Boolean(op && op.action === "launch");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function hasDispatchMessage(item) {
|
|
27
|
+
return Boolean(item && String(item.target || "").trim() && String(item.message || "").trim());
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function launchPlaceholder(index) {
|
|
31
|
+
return `${LAUNCH_TARGET_PREFIX}${index}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parseLaunchPlaceholder(target = "") {
|
|
35
|
+
const raw = String(target || "").trim();
|
|
36
|
+
if (!raw.startsWith(LAUNCH_TARGET_PREFIX)) return -1;
|
|
37
|
+
const parsed = Number.parseInt(raw.slice(LAUNCH_TARGET_PREFIX.length), 10);
|
|
38
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : -1;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isLaunchDispatchTarget(target = "", op = {}) {
|
|
42
|
+
const raw = String(target || "").trim();
|
|
43
|
+
if (!raw || raw.includes(":")) return false;
|
|
44
|
+
const normalized = raw.toLowerCase();
|
|
45
|
+
const nickname = String(op.nickname || "").trim();
|
|
46
|
+
if (nickname && raw === nickname) return true;
|
|
47
|
+
const placeholders = new Set([
|
|
48
|
+
"new",
|
|
49
|
+
"new-agent",
|
|
50
|
+
"new-worker",
|
|
51
|
+
"worker",
|
|
52
|
+
"agent",
|
|
53
|
+
"launched",
|
|
54
|
+
"launched-agent",
|
|
55
|
+
"launched-worker",
|
|
56
|
+
]);
|
|
57
|
+
if (placeholders.has(normalized)) return true;
|
|
58
|
+
const launchAgent = normalizeLaunchAgentForNickname(op.agent || "");
|
|
59
|
+
return Boolean(launchAgent && normalizeLaunchAgentForNickname(raw) === launchAgent);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function resolveLaunchResultTarget(op = {}, result = {}) {
|
|
63
|
+
if (result && result.agent_id) return String(result.agent_id);
|
|
64
|
+
if (result && Array.isArray(result.subscriber_ids) && result.subscriber_ids[0]) {
|
|
65
|
+
return String(result.subscriber_ids[0]);
|
|
66
|
+
}
|
|
67
|
+
if (result && result.subscriber_id) return String(result.subscriber_id);
|
|
68
|
+
if (result && result.nickname) return String(result.nickname);
|
|
69
|
+
if (op && op.nickname) return String(op.nickname);
|
|
70
|
+
return "";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function resolveDispatchLaunchIndex(dispatchItem = {}, launchOps = []) {
|
|
74
|
+
const target = String(dispatchItem.target || "").trim();
|
|
75
|
+
const placeholderIndex = parseLaunchPlaceholder(target);
|
|
76
|
+
if (placeholderIndex >= 0) return placeholderIndex;
|
|
77
|
+
|
|
78
|
+
for (let i = 0; i < launchOps.length; i += 1) {
|
|
79
|
+
if (isLaunchDispatchTarget(target, launchOps[i])) return i;
|
|
80
|
+
}
|
|
81
|
+
return -1;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function prepareLaunchDispatchPayload(payload, prompt = "") {
|
|
85
|
+
const normalized = normalizePayload(payload);
|
|
86
|
+
const launchOps = [];
|
|
87
|
+
const otherOps = [];
|
|
88
|
+
for (const op of normalized.ops) {
|
|
89
|
+
if (isLaunchOp(op)) launchOps.push(op);
|
|
90
|
+
else if (op) otherOps.push(op);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (launchOps.length === 0) {
|
|
94
|
+
return {
|
|
95
|
+
payload: normalized,
|
|
96
|
+
launchOps,
|
|
97
|
+
otherOps,
|
|
98
|
+
dispatch: normalized.dispatch.filter(hasDispatchMessage),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let dispatch = normalized.dispatch.filter(hasDispatchMessage).map((item) => ({ ...item }));
|
|
103
|
+
const taskMessage = stripRoutingPromptMetadata(prompt);
|
|
104
|
+
if (dispatch.length === 0 && !taskMessage) {
|
|
105
|
+
return {
|
|
106
|
+
payload: { ...normalized, dispatch: [], ops: otherOps },
|
|
107
|
+
launchOps: [],
|
|
108
|
+
otherOps,
|
|
109
|
+
dispatch: [],
|
|
110
|
+
droppedLaunches: launchOps,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (dispatch.length === 0 && taskMessage) {
|
|
115
|
+
dispatch = launchOps.map((op, index) => ({
|
|
116
|
+
target: op.nickname || launchPlaceholder(index),
|
|
117
|
+
message: taskMessage,
|
|
118
|
+
injection_mode: "immediate",
|
|
119
|
+
source: "ufoo-agent",
|
|
120
|
+
}));
|
|
121
|
+
} else if (launchOps.length === 1) {
|
|
122
|
+
const fallbackTarget = launchOps[0].nickname || launchPlaceholder(0);
|
|
123
|
+
dispatch = dispatch.map((item) => (
|
|
124
|
+
isLaunchDispatchTarget(item.target, launchOps[0])
|
|
125
|
+
? { ...item, target: fallbackTarget }
|
|
126
|
+
: item
|
|
127
|
+
));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
payload: { ...normalized, dispatch, ops: [...launchOps, ...otherOps] },
|
|
132
|
+
launchOps,
|
|
133
|
+
otherOps,
|
|
134
|
+
dispatch,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function bindDispatchToLaunchResults(dispatch = [], launchOps = [], launchResults = []) {
|
|
139
|
+
const resultByIndex = launchResults.filter((item) => item && item.action === "launch");
|
|
140
|
+
return dispatch.map((item) => {
|
|
141
|
+
const index = resolveDispatchLaunchIndex(item, launchOps);
|
|
142
|
+
if (index < 0) return item;
|
|
143
|
+
const target = resolveLaunchResultTarget(launchOps[index], resultByIndex[index] || {});
|
|
144
|
+
return target ? { ...item, target } : item;
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function finalizeRouterPayload({
|
|
149
|
+
projectRoot,
|
|
150
|
+
payload,
|
|
151
|
+
prompt = "",
|
|
152
|
+
processManager,
|
|
153
|
+
dispatchMessages,
|
|
154
|
+
handleOps,
|
|
155
|
+
markPending = () => {},
|
|
156
|
+
finalizeLocally = true,
|
|
157
|
+
}) {
|
|
158
|
+
const prepared = prepareLaunchDispatchPayload(payload, prompt);
|
|
159
|
+
if (finalizeLocally === false) {
|
|
160
|
+
return {
|
|
161
|
+
ok: true,
|
|
162
|
+
payload: prepared.payload,
|
|
163
|
+
opsResults: [],
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (prepared.launchOps.length === 0) {
|
|
168
|
+
for (const item of prepared.dispatch) {
|
|
169
|
+
if (item && item.target && item.target !== "broadcast") {
|
|
170
|
+
markPending(item.target);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (typeof dispatchMessages === "function") {
|
|
174
|
+
await dispatchMessages(projectRoot, prepared.dispatch);
|
|
175
|
+
}
|
|
176
|
+
const opsResults = typeof handleOps === "function" && prepared.otherOps.length > 0
|
|
177
|
+
? await handleOps(projectRoot, prepared.otherOps, processManager)
|
|
178
|
+
: [];
|
|
179
|
+
return {
|
|
180
|
+
ok: true,
|
|
181
|
+
payload: { ...prepared.payload, dispatch: prepared.dispatch, ops: prepared.otherOps },
|
|
182
|
+
opsResults,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const launchResults = typeof handleOps === "function"
|
|
187
|
+
? await handleOps(projectRoot, prepared.launchOps, processManager)
|
|
188
|
+
: [];
|
|
189
|
+
const boundDispatch = bindDispatchToLaunchResults(prepared.dispatch, prepared.launchOps, launchResults);
|
|
190
|
+
|
|
191
|
+
for (const item of boundDispatch) {
|
|
192
|
+
if (item && item.target && item.target !== "broadcast") {
|
|
193
|
+
markPending(item.target);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (typeof dispatchMessages === "function") {
|
|
197
|
+
await dispatchMessages(projectRoot, boundDispatch);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const otherResults = typeof handleOps === "function" && prepared.otherOps.length > 0
|
|
201
|
+
? await handleOps(projectRoot, prepared.otherOps, processManager)
|
|
202
|
+
: [];
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
ok: true,
|
|
206
|
+
payload: { ...prepared.payload, dispatch: boundDispatch },
|
|
207
|
+
opsResults: [...launchResults, ...otherResults],
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
module.exports = {
|
|
212
|
+
bindDispatchToLaunchResults,
|
|
213
|
+
finalizeRouterPayload,
|
|
214
|
+
normalizePayload,
|
|
215
|
+
prepareLaunchDispatchPayload,
|
|
216
|
+
__private: {
|
|
217
|
+
isLaunchDispatchTarget,
|
|
218
|
+
launchPlaceholder,
|
|
219
|
+
resolveLaunchResultTarget,
|
|
220
|
+
stripRoutingPromptMetadata,
|
|
221
|
+
},
|
|
222
|
+
};
|
package/src/daemon/ops.js
CHANGED
|
@@ -44,6 +44,10 @@ function toTmuxBinary(agent = "") {
|
|
|
44
44
|
return "";
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
function resolveUfooRunnerPath() {
|
|
48
|
+
return path.resolve(__dirname, "../../bin/ufoo.js");
|
|
49
|
+
}
|
|
50
|
+
|
|
47
51
|
function normalizeLaunchScope(value, fallback = "inplace") {
|
|
48
52
|
const raw = String(value || "").trim().toLowerCase();
|
|
49
53
|
if (!raw) return fallback;
|
|
@@ -546,7 +550,7 @@ async function spawnManagedHostAgent(
|
|
|
546
550
|
if (hasPreRegisteredSubscriber) {
|
|
547
551
|
// Group mode: use ufoo launcher for activity_state monitoring
|
|
548
552
|
// This enables ReadyDetector and bootstrap to work correctly
|
|
549
|
-
const ufooRunner =
|
|
553
|
+
const ufooRunner = resolveUfooRunnerPath();
|
|
550
554
|
const launchCmd = `${shellEscape(process.execPath)} ${shellEscape(ufooRunner)} agent-pty-runner ${shellEscape(normalizedAgent)}${argText}`.trim();
|
|
551
555
|
runCmd = titleCmd
|
|
552
556
|
? `cd ${shellEscape(projectRoot)} && ${titleCmd} && ${launchCmd}`
|
|
@@ -591,7 +595,7 @@ async function spawnManagedHostAgent(
|
|
|
591
595
|
}
|
|
592
596
|
|
|
593
597
|
async function spawnInternalAgent(projectRoot, agent, count = 1, nickname = "", processManager = null, extraEnv = {}) {
|
|
594
|
-
const runner =
|
|
598
|
+
const runner = resolveUfooRunnerPath();
|
|
595
599
|
const logDir = getUfooPaths(projectRoot).runDir;
|
|
596
600
|
fs.mkdirSync(logDir, { recursive: true });
|
|
597
601
|
|
package/src/daemon/promptLoop.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const { finalizeRouterPayload } = require("../controller/routerFinalize");
|
|
2
|
+
|
|
1
3
|
function normalizePayload(payload) {
|
|
2
4
|
if (!payload || typeof payload !== "object") {
|
|
3
5
|
return { reply: "", dispatch: [], ops: [] };
|
|
@@ -36,34 +38,23 @@ function stripAssistantCall(payload) {
|
|
|
36
38
|
async function finalizePromptRun({
|
|
37
39
|
projectRoot,
|
|
38
40
|
payload,
|
|
41
|
+
prompt,
|
|
39
42
|
processManager,
|
|
40
43
|
dispatchMessages,
|
|
41
44
|
handleOps,
|
|
42
45
|
markPending,
|
|
43
46
|
finalizeLocally = true,
|
|
44
47
|
}) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
ok: true,
|
|
48
|
-
payload,
|
|
49
|
-
opsResults: [],
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
for (const item of payload.dispatch || []) {
|
|
54
|
-
if (item && item.target && item.target !== "broadcast") {
|
|
55
|
-
markPending(item.target);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
await dispatchMessages(projectRoot, payload.dispatch || []);
|
|
60
|
-
const opsResults = await handleOps(projectRoot, payload.ops || [], processManager);
|
|
61
|
-
|
|
62
|
-
return {
|
|
63
|
-
ok: true,
|
|
48
|
+
return finalizeRouterPayload({
|
|
49
|
+
projectRoot,
|
|
64
50
|
payload,
|
|
65
|
-
|
|
66
|
-
|
|
51
|
+
prompt,
|
|
52
|
+
processManager,
|
|
53
|
+
dispatchMessages,
|
|
54
|
+
handleOps,
|
|
55
|
+
markPending,
|
|
56
|
+
finalizeLocally,
|
|
57
|
+
});
|
|
67
58
|
}
|
|
68
59
|
|
|
69
60
|
async function runPromptWithAssistant({
|
|
@@ -129,6 +120,7 @@ async function runPromptWithAssistant({
|
|
|
129
120
|
return finalizePromptRun({
|
|
130
121
|
projectRoot,
|
|
131
122
|
payload: firstPayload,
|
|
123
|
+
prompt: prompt || "",
|
|
132
124
|
processManager,
|
|
133
125
|
dispatchMessages,
|
|
134
126
|
handleOps,
|