u-foo 2.3.2 → 2.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/agent/loopRuntime.js +18 -22
- package/src/agent/ufooAgent.js +6 -24
- package/src/chat/inputMath.js +25 -56
- package/src/chat/inputSubmitHandler.js +8 -6
- package/src/controller/launchRouting.js +206 -0
- package/src/controller/routerFinalize.js +222 -0
- 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/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
|
|
|
@@ -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/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,
|