u-foo 2.5.5 → 2.5.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/ucode.js +9 -0
- package/package.json +1 -1
- package/src/agents/launch/notifier.js +6 -0
- package/src/agents/launch/ptyRunner.js +2 -2
- package/src/agents/launch/ptyWrapper.js +2 -2
- package/src/agents/prompts/native/index.js +2 -2
- package/src/agents/prompts/native/toolDescriptions/bash.js +1 -1
- package/src/agents/prompts/native/toolDescriptions/edit.js +1 -0
- package/src/agents/prompts/native/toolDescriptions/read.js +3 -2
- package/src/code/agent.js +77 -1086
- package/src/code/busConsumer.js +504 -0
- package/src/code/dispatch.js +1 -6
- package/src/code/launcher/ucode.js +8 -254
- package/src/code/launcher/ucodeBootstrap.js +18 -1
- package/src/code/launcher/ucodeBuild.js +0 -3
- package/src/code/launcher/ucodeDoctor.js +26 -8
- package/src/code/launcher/ucodeRuntimeConfig.js +12 -3
- package/src/code/nativeRunner.js +93 -113
- package/src/code/repl.js +610 -0
- package/src/code/sessionStore.js +5 -1
- package/src/code/skills/injection.js +17 -1
- package/src/code/taskDecomposer.js +47 -31
- package/src/code/tools/bash.js +19 -2
- package/src/code/tools/common.js +34 -0
- package/src/code/tools/edit.js +11 -3
- package/src/code/tools/read.js +20 -2
- package/src/coordination/bus/inject.js +52 -7
- package/src/coordination/bus/subscriber.js +33 -6
- package/src/runtime/daemon/deliveryScheduler.js +102 -2
- package/src/runtime/daemon/index.js +8 -1
- package/src/runtime/daemon/ops.js +23 -0
package/src/code/nativeRunner.js
CHANGED
|
@@ -14,6 +14,11 @@ const DEFAULT_ANTHROPIC_BASE_URL = "https://api.anthropic.com/v1";
|
|
|
14
14
|
// for non-trivial tasks while still catching runaway loops. Override via env.
|
|
15
15
|
const DEFAULT_MAX_NATIVE_TOOL_CALLS = 100;
|
|
16
16
|
const DEFAULT_MAX_NATIVE_TOOL_ERRORS = 5;
|
|
17
|
+
// Anthropic Messages rejects max_tokens above the model's real cap (64K on
|
|
18
|
+
// current models), so the transports use different defaults. Override either
|
|
19
|
+
// via UFOO_UCODE_MAX_TOKENS (positive integer).
|
|
20
|
+
const DEFAULT_OPENAI_MAX_TOKENS = 131072;
|
|
21
|
+
const DEFAULT_ANTHROPIC_MAX_TOKENS = 64000;
|
|
17
22
|
|
|
18
23
|
function nowMs() {
|
|
19
24
|
return Date.now();
|
|
@@ -38,6 +43,10 @@ function resolveNativeToolBudget(env = process.env) {
|
|
|
38
43
|
};
|
|
39
44
|
}
|
|
40
45
|
|
|
46
|
+
function resolveMaxTokens(fallback) {
|
|
47
|
+
return normalizePositiveInt(process.env.UFOO_UCODE_MAX_TOKENS, fallback);
|
|
48
|
+
}
|
|
49
|
+
|
|
41
50
|
function enforceNativeToolBudget({
|
|
42
51
|
toolCallsExecuted = 0,
|
|
43
52
|
toolErrors = 0,
|
|
@@ -46,7 +55,7 @@ function enforceNativeToolBudget({
|
|
|
46
55
|
lastTool = "",
|
|
47
56
|
lastError = "",
|
|
48
57
|
} = {}) {
|
|
49
|
-
if (toolCallsExecuted
|
|
58
|
+
if (toolCallsExecuted >= maxToolCalls) {
|
|
50
59
|
throw new Error(`tool call budget exceeded (${maxToolCalls})`);
|
|
51
60
|
}
|
|
52
61
|
if (toolErrors >= maxToolErrors) {
|
|
@@ -93,104 +102,6 @@ function clipText(value = "", maxChars = 6000) {
|
|
|
93
102
|
return `${text.slice(0, maxChars)}\n...[truncated]`;
|
|
94
103
|
}
|
|
95
104
|
|
|
96
|
-
function summarizeFileSnippet(file = "", content = "") {
|
|
97
|
-
const target = String(file || "").trim();
|
|
98
|
-
const body = String(content || "").trim();
|
|
99
|
-
if (!body) return `${target}: empty`;
|
|
100
|
-
|
|
101
|
-
if (target.toLowerCase().endsWith("package.json")) {
|
|
102
|
-
try {
|
|
103
|
-
const parsed = JSON.parse(body);
|
|
104
|
-
const name = String(parsed.name || "").trim() || "(unknown)";
|
|
105
|
-
const version = String(parsed.version || "").trim() || "(unknown)";
|
|
106
|
-
const scripts = parsed.scripts && typeof parsed.scripts === "object"
|
|
107
|
-
? Object.keys(parsed.scripts).slice(0, 4)
|
|
108
|
-
: [];
|
|
109
|
-
const scriptText = scripts.length > 0 ? ` scripts=${scripts.join(",")}` : "";
|
|
110
|
-
return `${target}: name=${name} version=${version}${scriptText}`;
|
|
111
|
-
} catch {
|
|
112
|
-
// fall through
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const lines = body
|
|
117
|
-
.split(/\r?\n/)
|
|
118
|
-
.map((line) => line.trim())
|
|
119
|
-
.filter(Boolean)
|
|
120
|
-
.slice(0, 3)
|
|
121
|
-
.map((line) => (line.length > 120 ? `${line.slice(0, 120)}...` : line));
|
|
122
|
-
|
|
123
|
-
if (lines.length === 0) return `${target}: empty`;
|
|
124
|
-
return `${target}: ${lines.join(" | ")}`;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function extractPreflightEvidence(systemPrompt = "") {
|
|
128
|
-
const source = String(systemPrompt || "");
|
|
129
|
-
if (!source) return [];
|
|
130
|
-
|
|
131
|
-
const results = [];
|
|
132
|
-
const fileRegex = /File:\s*([^\n]+)\n([\s\S]*?)(?=\n---\n(?:File|Command):|$)/g;
|
|
133
|
-
let match = fileRegex.exec(source);
|
|
134
|
-
while (match) {
|
|
135
|
-
const file = String(match[1] || "").trim();
|
|
136
|
-
const content = String(match[2] || "").trim();
|
|
137
|
-
if (file) {
|
|
138
|
-
results.push({ kind: "file", label: file, summary: summarizeFileSnippet(file, content) });
|
|
139
|
-
}
|
|
140
|
-
match = fileRegex.exec(source);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const cmdRegex = /Command:\s*([^\n]+)\n([\s\S]*?)(?=\n---\n(?:File|Command):|$)/g;
|
|
144
|
-
match = cmdRegex.exec(source);
|
|
145
|
-
while (match) {
|
|
146
|
-
const command = String(match[1] || "").trim();
|
|
147
|
-
const output = String(match[2] || "").trim();
|
|
148
|
-
if (command) {
|
|
149
|
-
const clipped = clipText(output, 300).replace(/\s+/g, " ").trim();
|
|
150
|
-
results.push({ kind: "command", label: command, summary: `${command}: ${clipped || "(no output)"}` });
|
|
151
|
-
}
|
|
152
|
-
match = cmdRegex.exec(source);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return results;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function isAnalysisPrompt(text = "") {
|
|
159
|
-
return /(?:analy[sz]e|analysis|review|audit|status|architecture|codebase|repo|project|现状|架构|审查|分析|项目|代码库)/i.test(text);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function parseReadIntent(prompt = "") {
|
|
163
|
-
const text = String(prompt || "");
|
|
164
|
-
const patterns = [
|
|
165
|
-
/(?:\bread\b|\bcat\b|查看|读取)\s+([A-Za-z0-9_./\\-]+(?:\.[A-Za-z0-9._-]+)?)/i,
|
|
166
|
-
/([A-Za-z0-9_./\\-]+\.(?:md|txt|json|js|ts|jsx|tsx|yml|yaml|toml|sh))/i,
|
|
167
|
-
];
|
|
168
|
-
for (const re of patterns) {
|
|
169
|
-
const match = text.match(re);
|
|
170
|
-
if (!match || !match[1]) continue;
|
|
171
|
-
const candidate = String(match[1]).trim().replace(/[),.;:]+$/, "");
|
|
172
|
-
if (!candidate) continue;
|
|
173
|
-
if (candidate.length > 260) continue;
|
|
174
|
-
return candidate;
|
|
175
|
-
}
|
|
176
|
-
return "";
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function parseBashIntent(prompt = "") {
|
|
180
|
-
const text = String(prompt || "").trim();
|
|
181
|
-
if (!text) return "";
|
|
182
|
-
if (
|
|
183
|
-
/\b(ls|dir|tree)\b/i.test(text)
|
|
184
|
-
|| /\b(list|show)\s+(files|dirs|directories|folders)\b/i.test(text)
|
|
185
|
-
|| /列出|目录|文件列表/.test(text)
|
|
186
|
-
) {
|
|
187
|
-
return "ls -la";
|
|
188
|
-
}
|
|
189
|
-
const cmdMatch = text.match(/(?:运行|执行|run|exec(?:ute)?)\s+`([^`]+)`/i);
|
|
190
|
-
if (cmdMatch && cmdMatch[1]) return String(cmdMatch[1]).trim();
|
|
191
|
-
return "";
|
|
192
|
-
}
|
|
193
|
-
|
|
194
105
|
function normalizeProvider(value = "") {
|
|
195
106
|
const text = String(value || "").trim().toLowerCase();
|
|
196
107
|
if (!text) return "";
|
|
@@ -308,6 +219,11 @@ function buildCoreToolSpecs() {
|
|
|
308
219
|
properties: {
|
|
309
220
|
path: { type: "string" },
|
|
310
221
|
content: { type: "string" },
|
|
222
|
+
mode: {
|
|
223
|
+
type: "string",
|
|
224
|
+
enum: ["overwrite", "append"],
|
|
225
|
+
description: 'Write mode: "overwrite" replaces the file (default), "append" adds to its end.',
|
|
226
|
+
},
|
|
311
227
|
append: { type: "boolean" },
|
|
312
228
|
},
|
|
313
229
|
required: ["path", "content"],
|
|
@@ -534,7 +450,7 @@ async function runOpenAiLikeTurn({
|
|
|
534
450
|
} = {}) {
|
|
535
451
|
const payload = {
|
|
536
452
|
model,
|
|
537
|
-
max_tokens:
|
|
453
|
+
max_tokens: resolveMaxTokens(DEFAULT_OPENAI_MAX_TOKENS),
|
|
538
454
|
messages,
|
|
539
455
|
tools: buildCoreToolSpecs(),
|
|
540
456
|
tool_choice: "auto",
|
|
@@ -588,6 +504,9 @@ async function runOpenAiLikeTurn({
|
|
|
588
504
|
let rawBuffer = "";
|
|
589
505
|
let responseText = "";
|
|
590
506
|
const announcedToolNames = new Set();
|
|
507
|
+
let nextSyntheticIndex = 0;
|
|
508
|
+
let lastSyntheticIndex = -1;
|
|
509
|
+
let sawDone = false;
|
|
591
510
|
|
|
592
511
|
while (true) {
|
|
593
512
|
const { done, value } = await reader.read();
|
|
@@ -601,8 +520,11 @@ async function runOpenAiLikeTurn({
|
|
|
601
520
|
const payloadText = parseSseDataBlock(block);
|
|
602
521
|
if (!payloadText) continue;
|
|
603
522
|
if (payloadText === "[DONE]") {
|
|
604
|
-
|
|
605
|
-
|
|
523
|
+
// Stop reading after this batch, but keep the buffered tail and
|
|
524
|
+
// finish the blocks already parsed alongside [DONE] instead of
|
|
525
|
+
// silently dropping them.
|
|
526
|
+
sawDone = true;
|
|
527
|
+
continue;
|
|
606
528
|
}
|
|
607
529
|
|
|
608
530
|
const chunk = parseJsonSafe(payloadText, null);
|
|
@@ -633,7 +555,23 @@ async function runOpenAiLikeTurn({
|
|
|
633
555
|
|
|
634
556
|
if (Array.isArray(delta.tool_calls)) {
|
|
635
557
|
for (const callPart of delta.tool_calls) {
|
|
636
|
-
|
|
558
|
+
let index;
|
|
559
|
+
if (Number.isFinite(callPart.index)) {
|
|
560
|
+
index = callPart.index;
|
|
561
|
+
} else if (typeof callPart.id === "string" && callPart.id) {
|
|
562
|
+
// Provider omitted index: a chunk carrying an id starts a new
|
|
563
|
+
// call, so give it its own synthetic index instead of
|
|
564
|
+
// collapsing every call into slot 0.
|
|
565
|
+
while (toolCallMap.has(nextSyntheticIndex)) nextSyntheticIndex += 1;
|
|
566
|
+
index = nextSyntheticIndex;
|
|
567
|
+
nextSyntheticIndex += 1;
|
|
568
|
+
lastSyntheticIndex = index;
|
|
569
|
+
} else if (lastSyntheticIndex >= 0) {
|
|
570
|
+
// No index and no id: continuation of the latest synthetic call.
|
|
571
|
+
index = lastSyntheticIndex;
|
|
572
|
+
} else {
|
|
573
|
+
index = 0;
|
|
574
|
+
}
|
|
637
575
|
const previous = toolCallMap.get(index) || {
|
|
638
576
|
id: "",
|
|
639
577
|
type: "function",
|
|
@@ -664,6 +602,8 @@ async function runOpenAiLikeTurn({
|
|
|
664
602
|
}
|
|
665
603
|
}
|
|
666
604
|
}
|
|
605
|
+
|
|
606
|
+
if (sawDone) break;
|
|
667
607
|
}
|
|
668
608
|
|
|
669
609
|
if (rawBuffer.trim()) {
|
|
@@ -755,7 +695,7 @@ async function runAnthropicTurn({
|
|
|
755
695
|
} = {}) {
|
|
756
696
|
const payload = {
|
|
757
697
|
model,
|
|
758
|
-
max_tokens:
|
|
698
|
+
max_tokens: resolveMaxTokens(DEFAULT_ANTHROPIC_MAX_TOKENS),
|
|
759
699
|
messages,
|
|
760
700
|
tools: buildAnthropicToolSpecs(),
|
|
761
701
|
stream: true,
|
|
@@ -812,6 +752,9 @@ async function runAnthropicTurn({
|
|
|
812
752
|
const blockMap = new Map();
|
|
813
753
|
let rawBuffer = "";
|
|
814
754
|
let responseText = "";
|
|
755
|
+
let nextSyntheticBlockIndex = 0;
|
|
756
|
+
let lastBlockIndex = -1;
|
|
757
|
+
let sawDone = false;
|
|
815
758
|
|
|
816
759
|
while (true) {
|
|
817
760
|
const { done, value } = await reader.read();
|
|
@@ -823,7 +766,13 @@ async function runAnthropicTurn({
|
|
|
823
766
|
|
|
824
767
|
for (const rawBlock of parsed.blocks) {
|
|
825
768
|
const { event, data } = parseSseEventBlock(rawBlock);
|
|
826
|
-
if (!data
|
|
769
|
+
if (!data) continue;
|
|
770
|
+
if (data === "[DONE]") {
|
|
771
|
+
// Stop reading after this batch instead of waiting for the server
|
|
772
|
+
// to close the connection, mirroring the OpenAI transport.
|
|
773
|
+
sawDone = true;
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
827
776
|
|
|
828
777
|
const payloadChunk = parseJsonSafe(data, null);
|
|
829
778
|
if (!payloadChunk || typeof payloadChunk !== "object") continue;
|
|
@@ -836,7 +785,18 @@ async function runAnthropicTurn({
|
|
|
836
785
|
}
|
|
837
786
|
|
|
838
787
|
if (event === "content_block_start") {
|
|
839
|
-
|
|
788
|
+
let index;
|
|
789
|
+
if (Number.isFinite(payloadChunk.index)) {
|
|
790
|
+
index = payloadChunk.index;
|
|
791
|
+
} else {
|
|
792
|
+
// Provider omitted index: each start opens a new block, so give
|
|
793
|
+
// it its own synthetic index instead of collapsing every block
|
|
794
|
+
// into slot 0.
|
|
795
|
+
while (blockMap.has(nextSyntheticBlockIndex)) nextSyntheticBlockIndex += 1;
|
|
796
|
+
index = nextSyntheticBlockIndex;
|
|
797
|
+
nextSyntheticBlockIndex += 1;
|
|
798
|
+
}
|
|
799
|
+
lastBlockIndex = index;
|
|
840
800
|
const contentBlock = payloadChunk.content_block && typeof payloadChunk.content_block === "object"
|
|
841
801
|
? payloadChunk.content_block
|
|
842
802
|
: {};
|
|
@@ -873,7 +833,15 @@ async function runAnthropicTurn({
|
|
|
873
833
|
}
|
|
874
834
|
|
|
875
835
|
if (event === "content_block_delta") {
|
|
876
|
-
|
|
836
|
+
let index;
|
|
837
|
+
if (Number.isFinite(payloadChunk.index)) {
|
|
838
|
+
index = payloadChunk.index;
|
|
839
|
+
} else if (lastBlockIndex >= 0) {
|
|
840
|
+
// No index: continuation of the most recently started block.
|
|
841
|
+
index = lastBlockIndex;
|
|
842
|
+
} else {
|
|
843
|
+
index = 0;
|
|
844
|
+
}
|
|
877
845
|
const delta = payloadChunk.delta && typeof payloadChunk.delta === "object"
|
|
878
846
|
? payloadChunk.delta
|
|
879
847
|
: {};
|
|
@@ -916,6 +884,8 @@ async function runAnthropicTurn({
|
|
|
916
884
|
}
|
|
917
885
|
}
|
|
918
886
|
}
|
|
887
|
+
|
|
888
|
+
if (sawDone) break;
|
|
919
889
|
}
|
|
920
890
|
|
|
921
891
|
const assistantContent = Array.from(blockMap.values())
|
|
@@ -1276,6 +1246,17 @@ async function runNativeAgentTask({
|
|
|
1276
1246
|
const guards = createGuards({ signal, timeoutMs });
|
|
1277
1247
|
const nextSessionId = String(sessionId || "").trim() || `native-${randomUUID()}`;
|
|
1278
1248
|
const promptText = String(prompt || "").trim();
|
|
1249
|
+
// Track every text delta so the error path can return the partial output
|
|
1250
|
+
// the model already produced instead of discarding it.
|
|
1251
|
+
let partialOutput = "";
|
|
1252
|
+
const trackingStreamDelta = (chunk) => {
|
|
1253
|
+
const text = String(chunk || "");
|
|
1254
|
+
if (!text) return;
|
|
1255
|
+
partialOutput += text;
|
|
1256
|
+
if (typeof onStreamDelta === "function") {
|
|
1257
|
+
onStreamDelta(chunk);
|
|
1258
|
+
}
|
|
1259
|
+
};
|
|
1279
1260
|
|
|
1280
1261
|
try {
|
|
1281
1262
|
guards.ensureActive();
|
|
@@ -1309,7 +1290,7 @@ async function runNativeAgentTask({
|
|
|
1309
1290
|
baseUrl: runtime.baseUrl,
|
|
1310
1291
|
apiKey: runtime.apiKey,
|
|
1311
1292
|
timeoutMs,
|
|
1312
|
-
onStreamDelta,
|
|
1293
|
+
onStreamDelta: trackingStreamDelta,
|
|
1313
1294
|
onThinkingDelta,
|
|
1314
1295
|
onPhase,
|
|
1315
1296
|
onToolEvent,
|
|
@@ -1329,14 +1310,16 @@ async function runNativeAgentTask({
|
|
|
1329
1310
|
output: outputText,
|
|
1330
1311
|
messages: cloneMessageList(runResult.messages),
|
|
1331
1312
|
sessionId: nextSessionId,
|
|
1332
|
-
streamed
|
|
1313
|
+
// The loop marks streamed=true whenever it receives a stream callback;
|
|
1314
|
+
// only report it when the caller actually registered one.
|
|
1315
|
+
streamed: Boolean(runResult.streamed) && typeof onStreamDelta === "function",
|
|
1333
1316
|
};
|
|
1334
1317
|
} catch (err) {
|
|
1335
1318
|
const message = err && err.message ? err.message : "native runner failed";
|
|
1336
1319
|
return {
|
|
1337
1320
|
ok: false,
|
|
1338
1321
|
error: message,
|
|
1339
|
-
output:
|
|
1322
|
+
output: partialOutput.trim(),
|
|
1340
1323
|
sessionId: nextSessionId,
|
|
1341
1324
|
streamed: false,
|
|
1342
1325
|
};
|
|
@@ -1345,9 +1328,6 @@ async function runNativeAgentTask({
|
|
|
1345
1328
|
|
|
1346
1329
|
module.exports = {
|
|
1347
1330
|
runNativeAgentTask,
|
|
1348
|
-
parseReadIntent,
|
|
1349
|
-
parseBashIntent,
|
|
1350
|
-
extractPreflightEvidence,
|
|
1351
1331
|
resolveRuntimeConfig,
|
|
1352
1332
|
resolveCompletionUrl,
|
|
1353
1333
|
resolveAnthropicMessagesUrl,
|