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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "u-foo",
3
- "version": "2.3.2",
3
+ "version": "2.3.3",
4
4
  "description": "Multi-Agent Workspace Protocol. Just add u. claude → uclaude, codex → ucodex.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "homepage": "https://ufoo.dev",
@@ -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
- if (finalizeLocally === false) {
136
- return { ok: true, payload, opsResults: [] };
137
- }
138
-
139
- const dispatch = Array.isArray(payload.dispatch) ? payload.dispatch : [];
140
- const ops = Array.isArray(payload.ops) ? payload.ops : [];
141
-
142
- for (const item of dispatch) {
143
- if (item && item.target && item.target !== "broadcast" && typeof markPending === "function") {
144
- markPending(item.target);
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,
@@ -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
- const fallbackNickname = extractNickname(prompt);
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,
@@ -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 { rows, lastCol }.
79
+ * Returns the zero-based visual row and column at the end of the line.
80
80
  *
81
- * Blessed preprocesses double-width chars by inserting a \x03 marker after
82
- * each one, then wraps at character count = width. This means a double-width
83
- * char at col (width - 1) overflows by 1 cell — only the \x03 marker wraps
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 wrapLine(line, width, strWidth) {
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 >= width) {
95
- rows += 1;
96
- col = col - width;
92
+ if (col > 0 && col + w > width) {
93
+ row += 1;
94
+ col = 0;
97
95
  }
98
96
  col += w;
99
97
  }
100
- if (col > width) {
101
- rows += 1;
102
- col = col - width;
103
- } else if (col === width) {
104
- rows += 1;
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
- const { rows, lastCol } = wrapLine(line, width, strWidth);
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
- const { rows, lastCol } = wrapLine(line, width, strWidth);
140
- row += (lastCol === 0 && rows > 1) ? rows - 1 : rows;
133
+ row += countLineRows(line, width, strWidth);
141
134
  } else {
142
- const chars = Array.from(line);
143
- let col = 0;
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 >= width) {
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 = col - width;
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 > width) {
200
- if (row === targetRow) {
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 userPrefix() {
34
- return "{white-fg}you{/white-fg} {gray-fg}·{/gray-fg}";
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
- `${userPrefix()} {magenta-fg}@${escapeBlessed(label)}{/magenta-fg} ${escapeBlessed(text)}`
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
- `${userPrefix()} {magenta-fg}@${escapeBlessed(atTarget.target)}{/magenta-fg} ${escapeBlessed(message)}`
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", `${userPrefix()} ${escapeBlessed(text)}`);
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", `${userPrefix()} ${escapeBlessed(text)}`);
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
+ };
@@ -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
- if (finalizeLocally === false) {
46
- return {
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
- opsResults,
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,