u-foo 2.3.1 → 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/bus/subscriber.js +6 -4
- package/src/chat/commandExecutor.js +17 -12
- package/src/chat/daemonMessageRouter.js +14 -10
- package/src/chat/index.js +2 -0
- package/src/chat/inputMath.js +25 -56
- package/src/chat/inputSubmitHandler.js +11 -8
- package/src/chat/projectCloseController.js +7 -3
- package/src/chat/settingsController.js +4 -2
- 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/bus/subscriber.js
CHANGED
|
@@ -122,10 +122,11 @@ class SubscriberManager {
|
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
async cleanupDuplicateTty(currentSubscriber, ttyPath) {
|
|
125
|
+
async cleanupDuplicateTty(currentSubscriber, ttyPath, options = {}) {
|
|
126
126
|
if (!ttyPath) return null;
|
|
127
127
|
if (!this.busData.agents) return null;
|
|
128
128
|
|
|
129
|
+
const currentAgentType = String(options.agentType || "").trim();
|
|
129
130
|
let inheritedNickname = null;
|
|
130
131
|
const entries = Object.entries(this.busData.agents);
|
|
131
132
|
for (const [id, meta] of entries) {
|
|
@@ -136,8 +137,9 @@ class SubscriberManager {
|
|
|
136
137
|
: (await this.queueManager.readTty(id));
|
|
137
138
|
if (!metaTty) continue;
|
|
138
139
|
if (metaTty === ttyPath) {
|
|
139
|
-
|
|
140
|
-
|
|
140
|
+
const sameAgentType = !currentAgentType || meta?.agent_type === currentAgentType;
|
|
141
|
+
// Inherit user-set nickname only when replacing the same agent type.
|
|
142
|
+
if (sameAgentType && meta.nickname && !inheritedNickname) {
|
|
141
143
|
inheritedNickname = meta.nickname;
|
|
142
144
|
}
|
|
143
145
|
// Remove stale subscriber using same tty
|
|
@@ -227,7 +229,7 @@ class SubscriberManager {
|
|
|
227
229
|
// 清理同一 tty 的旧订阅者(避免重复启动污染)
|
|
228
230
|
// Inherit nickname from displaced entry when this is a new subscriber
|
|
229
231
|
// with no explicit nickname (e.g. session restart on same TTY)
|
|
230
|
-
const inheritedNickname = await this.cleanupDuplicateTty(subscriber, finalTty);
|
|
232
|
+
const inheritedNickname = await this.cleanupDuplicateTty(subscriber, finalTty, { agentType });
|
|
231
233
|
if (inheritedNickname && !nickname && !existingMeta) {
|
|
232
234
|
finalNickname = inheritedNickname;
|
|
233
235
|
if (!finalScopedNickname) finalScopedNickname = inheritedNickname;
|
|
@@ -117,6 +117,7 @@ function createCommandExecutor(options = {}) {
|
|
|
117
117
|
parseCommand = () => null,
|
|
118
118
|
escapeBlessed = (value) => String(value || ""),
|
|
119
119
|
logMessage = () => {},
|
|
120
|
+
resolveStatusLine = null,
|
|
120
121
|
renderScreen = () => {},
|
|
121
122
|
getActiveAgents = () => [],
|
|
122
123
|
getActiveAgentMetaMap = () => new Map(),
|
|
@@ -157,6 +158,10 @@ function createCommandExecutor(options = {}) {
|
|
|
157
158
|
throw new Error("createCommandExecutor requires projectRoot");
|
|
158
159
|
}
|
|
159
160
|
|
|
161
|
+
const statusMsg = typeof resolveStatusLine === "function"
|
|
162
|
+
? resolveStatusLine
|
|
163
|
+
: (text) => logMessage("status", text);
|
|
164
|
+
|
|
160
165
|
async function handleDoctorCommand() {
|
|
161
166
|
logMessage("system", "{white-fg}⚙{/white-fg} Running health check...");
|
|
162
167
|
|
|
@@ -221,51 +226,51 @@ function createCommandExecutor(options = {}) {
|
|
|
221
226
|
|
|
222
227
|
if (subcommand === "start") {
|
|
223
228
|
if (isDaemonRunning(targetRoot)) {
|
|
224
|
-
|
|
229
|
+
statusMsg("{gray-fg}⚠{/gray-fg} Daemon already running");
|
|
225
230
|
} else {
|
|
226
|
-
|
|
231
|
+
statusMsg("{gray-fg}⚙{/gray-fg} Starting daemon...");
|
|
227
232
|
startDaemon(targetRoot);
|
|
228
233
|
await sleep(1000);
|
|
229
234
|
if (isDaemonRunning(targetRoot)) {
|
|
230
|
-
|
|
235
|
+
statusMsg("{gray-fg}✓{/gray-fg} Daemon started");
|
|
231
236
|
} else {
|
|
232
|
-
|
|
237
|
+
statusMsg("{gray-fg}✗{/gray-fg} Failed to start daemon");
|
|
233
238
|
}
|
|
234
239
|
}
|
|
235
240
|
return;
|
|
236
241
|
}
|
|
237
242
|
|
|
238
243
|
if (subcommand === "stop") {
|
|
239
|
-
|
|
244
|
+
statusMsg("{gray-fg}⚙{/gray-fg} Stopping daemon...");
|
|
240
245
|
stopDaemon(targetRoot);
|
|
241
246
|
await sleep(1000);
|
|
242
247
|
if (!isDaemonRunning(targetRoot)) {
|
|
243
|
-
|
|
248
|
+
statusMsg("{gray-fg}✓{/gray-fg} Daemon stopped");
|
|
244
249
|
} else {
|
|
245
|
-
|
|
250
|
+
statusMsg("{gray-fg}✗{/gray-fg} Failed to stop daemon");
|
|
246
251
|
}
|
|
247
252
|
return;
|
|
248
253
|
}
|
|
249
254
|
|
|
250
255
|
if (subcommand === "restart") {
|
|
251
|
-
|
|
256
|
+
statusMsg("{gray-fg}⚙{/gray-fg} Restarting daemon...");
|
|
252
257
|
stopDaemon(targetRoot);
|
|
253
258
|
await sleep(500);
|
|
254
259
|
startDaemon(targetRoot);
|
|
255
260
|
await sleep(1000);
|
|
256
261
|
if (isDaemonRunning(targetRoot)) {
|
|
257
|
-
|
|
262
|
+
statusMsg("{gray-fg}✓{/gray-fg} Daemon restarted");
|
|
258
263
|
} else {
|
|
259
|
-
|
|
264
|
+
statusMsg("{gray-fg}✗{/gray-fg} Failed to restart daemon");
|
|
260
265
|
}
|
|
261
266
|
return;
|
|
262
267
|
}
|
|
263
268
|
|
|
264
269
|
if (subcommand === "status") {
|
|
265
270
|
if (isDaemonRunning(targetRoot)) {
|
|
266
|
-
|
|
271
|
+
statusMsg("{gray-fg}✓{/gray-fg} Daemon is running");
|
|
267
272
|
} else {
|
|
268
|
-
|
|
273
|
+
statusMsg("{gray-fg}✗{/gray-fg} Daemon is not running");
|
|
269
274
|
}
|
|
270
275
|
return;
|
|
271
276
|
}
|
|
@@ -37,6 +37,14 @@ function createDaemonMessageRouter(options = {}) {
|
|
|
37
37
|
return text.includes(":") && !text.includes(" ");
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
function speakerPrefix(label, color = "cyan") {
|
|
41
|
+
const escapedLabel = escapeBlessed(label);
|
|
42
|
+
if (color === "white") {
|
|
43
|
+
return `{white-fg}${escapedLabel}{/white-fg} {gray-fg}·{/gray-fg} `;
|
|
44
|
+
}
|
|
45
|
+
return `{cyan-fg}${escapedLabel}{/cyan-fg} {gray-fg}·{/gray-fg} `;
|
|
46
|
+
}
|
|
47
|
+
|
|
40
48
|
function normalizeDisplayMessage(raw) {
|
|
41
49
|
let displayMessage = raw || "";
|
|
42
50
|
let streamPayload = null;
|
|
@@ -77,9 +85,9 @@ function createDaemonMessageRouter(options = {}) {
|
|
|
77
85
|
resolveBusStatus(item);
|
|
78
86
|
if (text) {
|
|
79
87
|
const prefix = data.phase === BUS_STATUS_PHASES.ERROR
|
|
80
|
-
? "{
|
|
81
|
-
: "{
|
|
82
|
-
|
|
88
|
+
? "{gray-fg}✗{/gray-fg}"
|
|
89
|
+
: "{gray-fg}✓{/gray-fg}";
|
|
90
|
+
resolveStatusLine(`${prefix} ${escapeBlessed(text)}`, data);
|
|
83
91
|
}
|
|
84
92
|
} else {
|
|
85
93
|
enqueueBusStatus(item);
|
|
@@ -214,7 +222,7 @@ function createDaemonMessageRouter(options = {}) {
|
|
|
214
222
|
);
|
|
215
223
|
// Suppress lifecycle confirmations from chat history — status line plus structured payload is enough.
|
|
216
224
|
if (!isLifecycleStatusOnly && !isGroupStartedConfirmation) {
|
|
217
|
-
logMessage("reply",
|
|
225
|
+
logMessage("reply", `${speakerPrefix("ufoo", "white")}${escapeBlessed(replyText)}`);
|
|
218
226
|
}
|
|
219
227
|
}
|
|
220
228
|
|
|
@@ -352,15 +360,11 @@ function createDaemonMessageRouter(options = {}) {
|
|
|
352
360
|
const publisher = report.agent_id || data.publisher || "ufoo-agent";
|
|
353
361
|
const displayName = resolveAgentDisplayName(publisher);
|
|
354
362
|
const detail = report.summary || report.message || data.message || report.task_id || "report";
|
|
355
|
-
logMessage(
|
|
356
|
-
"system",
|
|
357
|
-
`{gray-fg}↥{/gray-fg} {cyan-fg}${escapeBlessed(displayName)}{/cyan-fg} {gray-fg}→ ufoo-agent{/gray-fg} ${escapeBlessed(detail)}`
|
|
358
|
-
);
|
|
363
|
+
logMessage("bus", `${speakerPrefix(displayName)}${escapeBlessed(detail)}`);
|
|
359
364
|
requestStatus();
|
|
360
365
|
renderScreen();
|
|
361
366
|
return true;
|
|
362
367
|
}
|
|
363
|
-
const prefix = data.event === "broadcast" ? "{gray-fg}⇢{/gray-fg}" : "{gray-fg}↔{/gray-fg}";
|
|
364
368
|
const publisher = data.publisher && data.publisher !== "unknown"
|
|
365
369
|
? data.publisher
|
|
366
370
|
: (data.event === "broadcast" ? "broadcast" : "bus");
|
|
@@ -399,7 +403,7 @@ function createDaemonMessageRouter(options = {}) {
|
|
|
399
403
|
}
|
|
400
404
|
|
|
401
405
|
const pendingBeforeMessage = getPendingState(publisher, displayName);
|
|
402
|
-
const prefixLabel =
|
|
406
|
+
const prefixLabel = speakerPrefix(displayName);
|
|
403
407
|
const continuationPrefix = " ".repeat(stripBlessedTags(prefixLabel).length);
|
|
404
408
|
|
|
405
409
|
if (streamPayload) {
|
package/src/chat/index.js
CHANGED
|
@@ -1905,6 +1905,7 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1905
1905
|
renderDashboard,
|
|
1906
1906
|
renderScreen: () => screen.render(),
|
|
1907
1907
|
logMessage,
|
|
1908
|
+
resolveStatusLine,
|
|
1908
1909
|
escapeBlessed,
|
|
1909
1910
|
});
|
|
1910
1911
|
|
|
@@ -1919,6 +1920,7 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1919
1920
|
parseCommand,
|
|
1920
1921
|
escapeBlessed,
|
|
1921
1922
|
logMessage,
|
|
1923
|
+
resolveStatusLine,
|
|
1922
1924
|
renderScreen: () => screen.render(),
|
|
1923
1925
|
getActiveAgents: () => activeAgents,
|
|
1924
1926
|
getActiveAgentMetaMap: () => activeAgentMetaMap,
|
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,6 +30,12 @@ function createInputSubmitHandler(options = {}) {
|
|
|
30
30
|
throw new Error("createInputSubmitHandler requires a mutable state object");
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
function userEcho(text, targetLabel = "") {
|
|
34
|
+
const body = escapeBlessed(text);
|
|
35
|
+
if (!targetLabel) return body;
|
|
36
|
+
return `{magenta-fg}@${escapeBlessed(targetLabel)}{/magenta-fg} ${body}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
33
39
|
async function tryActivateTargetAgent(agentId) {
|
|
34
40
|
const adapter = getAgentAdapter(agentId);
|
|
35
41
|
const capabilities = adapter && adapter.capabilities ? adapter.capabilities : null;
|
|
@@ -88,7 +94,7 @@ function createInputSubmitHandler(options = {}) {
|
|
|
88
94
|
const label = getAgentLabel(state.targetAgent);
|
|
89
95
|
logMessage(
|
|
90
96
|
"user",
|
|
91
|
-
|
|
97
|
+
userEcho(text, label)
|
|
92
98
|
);
|
|
93
99
|
renderScreen(); // Immediately render the user message
|
|
94
100
|
markPendingDelivery(state.targetAgent);
|
|
@@ -114,10 +120,7 @@ function createInputSubmitHandler(options = {}) {
|
|
|
114
120
|
return;
|
|
115
121
|
}
|
|
116
122
|
setTargetAgent(resolvedTarget);
|
|
117
|
-
|
|
118
|
-
"status",
|
|
119
|
-
`{white-fg}⚙{/white-fg} Target selected: @${escapeBlessed(atTarget.target)}`
|
|
120
|
-
);
|
|
123
|
+
queueStatusLine(`Target selected: @${escapeBlessed(atTarget.target)}`);
|
|
121
124
|
focusInput();
|
|
122
125
|
return;
|
|
123
126
|
}
|
|
@@ -125,7 +128,7 @@ function createInputSubmitHandler(options = {}) {
|
|
|
125
128
|
const message = atTarget.message.trim();
|
|
126
129
|
logMessage(
|
|
127
130
|
"user",
|
|
128
|
-
|
|
131
|
+
userEcho(message, atTarget.target)
|
|
129
132
|
);
|
|
130
133
|
renderScreen(); // Immediately render the user message
|
|
131
134
|
markPendingDelivery(resolvedTarget);
|
|
@@ -142,7 +145,7 @@ function createInputSubmitHandler(options = {}) {
|
|
|
142
145
|
|
|
143
146
|
if (text.startsWith("/")) {
|
|
144
147
|
if (shouldEchoCommandInChat(text)) {
|
|
145
|
-
logMessage("user",
|
|
148
|
+
logMessage("user", userEcho(text));
|
|
146
149
|
renderScreen(); // Render slash command immediately
|
|
147
150
|
}
|
|
148
151
|
try {
|
|
@@ -188,7 +191,7 @@ function createInputSubmitHandler(options = {}) {
|
|
|
188
191
|
allow_relevance_queue: true,
|
|
189
192
|
},
|
|
190
193
|
});
|
|
191
|
-
logMessage("user",
|
|
194
|
+
logMessage("user", userEcho(text));
|
|
192
195
|
renderScreen(); // Render plain text message immediately
|
|
193
196
|
}
|
|
194
197
|
|
|
@@ -23,10 +23,14 @@ function createProjectCloseController(options = {}) {
|
|
|
23
23
|
renderDashboard = () => {},
|
|
24
24
|
renderScreen = () => {},
|
|
25
25
|
logMessage = () => {},
|
|
26
|
+
resolveStatusLine = null,
|
|
26
27
|
escapeBlessed = (value) => String(value || ""),
|
|
27
28
|
} = options;
|
|
28
29
|
|
|
29
30
|
let closingProject = false;
|
|
31
|
+
const statusMsg = typeof resolveStatusLine === "function"
|
|
32
|
+
? resolveStatusLine
|
|
33
|
+
: (text) => logMessage("status", text);
|
|
30
34
|
|
|
31
35
|
function pickFallbackProjectRoot(targetProjectRoot) {
|
|
32
36
|
const rows = Array.isArray(getProjects()) ? getProjects() : [];
|
|
@@ -61,7 +65,7 @@ function createProjectCloseController(options = {}) {
|
|
|
61
65
|
|
|
62
66
|
closingProject = true;
|
|
63
67
|
try {
|
|
64
|
-
|
|
68
|
+
statusMsg(`{gray-fg}⚙{/gray-fg} Closing project ${escapedName} daemon and agents...`);
|
|
65
69
|
|
|
66
70
|
let switchedTo = "";
|
|
67
71
|
if (activeProjectRoot === projectRoot) {
|
|
@@ -89,9 +93,9 @@ function createProjectCloseController(options = {}) {
|
|
|
89
93
|
renderScreen();
|
|
90
94
|
|
|
91
95
|
if (wasRunning) {
|
|
92
|
-
|
|
96
|
+
statusMsg(`{gray-fg}✓{/gray-fg} Closed project ${escapedName} daemon and agents`);
|
|
93
97
|
} else {
|
|
94
|
-
|
|
98
|
+
statusMsg(`{gray-fg}✓{/gray-fg} Project ${escapedName} daemon already stopped`);
|
|
95
99
|
}
|
|
96
100
|
|
|
97
101
|
return {
|
|
@@ -75,7 +75,8 @@ function createSettingsController(options = {}) {
|
|
|
75
75
|
setSelectedProviderIndex(Math.max(0, providerOptions.findIndex((opt) => opt.value === next)));
|
|
76
76
|
saveConfig(projectRoot, { agentProvider: next });
|
|
77
77
|
clearUfooAgentIdentity();
|
|
78
|
-
logMessage("status",
|
|
78
|
+
const statusMsg = resolveStatusLine || ((text) => logMessage("status", text));
|
|
79
|
+
statusMsg(`{gray-fg}⚙{/gray-fg} ufoo-agent: ${providerLabel(next)}`);
|
|
79
80
|
renderDashboard();
|
|
80
81
|
renderScreen();
|
|
81
82
|
void restartDaemon();
|
|
@@ -89,7 +90,8 @@ function createSettingsController(options = {}) {
|
|
|
89
90
|
setSelectedResumeIndex(next ? 0 : 1);
|
|
90
91
|
saveConfig(projectRoot, { autoResume: next });
|
|
91
92
|
const label = next ? "Resume previous session" : "Start new session";
|
|
92
|
-
logMessage("status",
|
|
93
|
+
const statusMsg = resolveStatusLine || ((text) => logMessage("status", text));
|
|
94
|
+
statusMsg(`{gray-fg}⚙{/gray-fg} Resume mode: ${label}`);
|
|
93
95
|
renderDashboard();
|
|
94
96
|
renderScreen();
|
|
95
97
|
return true;
|
|
@@ -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,
|