u-foo 2.5.6 → 2.5.8
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/prompts/native/index.js +1 -1
- package/src/agents/prompts/native/toolDescriptions/edit.js +1 -0
- package/src/code/agent.js +53 -1076
- package/src/code/busConsumer.js +504 -0
- package/src/code/dispatch.js +1 -6
- package/src/code/launcher/ucode.js +3 -251
- package/src/code/launcher/ucodeBootstrap.js +18 -1
- package/src/code/launcher/ucodeBuild.js +0 -3
- package/src/code/launcher/ucodeDoctor.js +24 -9
- package/src/code/launcher/ucodeRuntimeConfig.js +12 -3
- package/src/code/nativeRunner.js +490 -580
- 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 +36 -29
- package/src/code/tools/common.js +34 -0
- package/src/code/tools/edit.js +11 -3
- 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
|
@@ -102,104 +102,6 @@ function clipText(value = "", maxChars = 6000) {
|
|
|
102
102
|
return `${text.slice(0, maxChars)}\n...[truncated]`;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
function summarizeFileSnippet(file = "", content = "") {
|
|
106
|
-
const target = String(file || "").trim();
|
|
107
|
-
const body = String(content || "").trim();
|
|
108
|
-
if (!body) return `${target}: empty`;
|
|
109
|
-
|
|
110
|
-
if (target.toLowerCase().endsWith("package.json")) {
|
|
111
|
-
try {
|
|
112
|
-
const parsed = JSON.parse(body);
|
|
113
|
-
const name = String(parsed.name || "").trim() || "(unknown)";
|
|
114
|
-
const version = String(parsed.version || "").trim() || "(unknown)";
|
|
115
|
-
const scripts = parsed.scripts && typeof parsed.scripts === "object"
|
|
116
|
-
? Object.keys(parsed.scripts).slice(0, 4)
|
|
117
|
-
: [];
|
|
118
|
-
const scriptText = scripts.length > 0 ? ` scripts=${scripts.join(",")}` : "";
|
|
119
|
-
return `${target}: name=${name} version=${version}${scriptText}`;
|
|
120
|
-
} catch {
|
|
121
|
-
// fall through
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const lines = body
|
|
126
|
-
.split(/\r?\n/)
|
|
127
|
-
.map((line) => line.trim())
|
|
128
|
-
.filter(Boolean)
|
|
129
|
-
.slice(0, 3)
|
|
130
|
-
.map((line) => (line.length > 120 ? `${line.slice(0, 120)}...` : line));
|
|
131
|
-
|
|
132
|
-
if (lines.length === 0) return `${target}: empty`;
|
|
133
|
-
return `${target}: ${lines.join(" | ")}`;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function extractPreflightEvidence(systemPrompt = "") {
|
|
137
|
-
const source = String(systemPrompt || "");
|
|
138
|
-
if (!source) return [];
|
|
139
|
-
|
|
140
|
-
const results = [];
|
|
141
|
-
const fileRegex = /File:\s*([^\n]+)\n([\s\S]*?)(?=\n---\n(?:File|Command):|$)/g;
|
|
142
|
-
let match = fileRegex.exec(source);
|
|
143
|
-
while (match) {
|
|
144
|
-
const file = String(match[1] || "").trim();
|
|
145
|
-
const content = String(match[2] || "").trim();
|
|
146
|
-
if (file) {
|
|
147
|
-
results.push({ kind: "file", label: file, summary: summarizeFileSnippet(file, content) });
|
|
148
|
-
}
|
|
149
|
-
match = fileRegex.exec(source);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const cmdRegex = /Command:\s*([^\n]+)\n([\s\S]*?)(?=\n---\n(?:File|Command):|$)/g;
|
|
153
|
-
match = cmdRegex.exec(source);
|
|
154
|
-
while (match) {
|
|
155
|
-
const command = String(match[1] || "").trim();
|
|
156
|
-
const output = String(match[2] || "").trim();
|
|
157
|
-
if (command) {
|
|
158
|
-
const clipped = clipText(output, 300).replace(/\s+/g, " ").trim();
|
|
159
|
-
results.push({ kind: "command", label: command, summary: `${command}: ${clipped || "(no output)"}` });
|
|
160
|
-
}
|
|
161
|
-
match = cmdRegex.exec(source);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
return results;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function isAnalysisPrompt(text = "") {
|
|
168
|
-
return /(?:analy[sz]e|analysis|review|audit|status|architecture|codebase|repo|project|现状|架构|审查|分析|项目|代码库)/i.test(text);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function parseReadIntent(prompt = "") {
|
|
172
|
-
const text = String(prompt || "");
|
|
173
|
-
const patterns = [
|
|
174
|
-
/(?:\bread\b|\bcat\b|查看|读取)\s+([A-Za-z0-9_./\\-]+(?:\.[A-Za-z0-9._-]+)?)/i,
|
|
175
|
-
/([A-Za-z0-9_./\\-]+\.(?:md|txt|json|js|ts|jsx|tsx|yml|yaml|toml|sh))/i,
|
|
176
|
-
];
|
|
177
|
-
for (const re of patterns) {
|
|
178
|
-
const match = text.match(re);
|
|
179
|
-
if (!match || !match[1]) continue;
|
|
180
|
-
const candidate = String(match[1]).trim().replace(/[),.;:]+$/, "");
|
|
181
|
-
if (!candidate) continue;
|
|
182
|
-
if (candidate.length > 260) continue;
|
|
183
|
-
return candidate;
|
|
184
|
-
}
|
|
185
|
-
return "";
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function parseBashIntent(prompt = "") {
|
|
189
|
-
const text = String(prompt || "").trim();
|
|
190
|
-
if (!text) return "";
|
|
191
|
-
if (
|
|
192
|
-
/\b(ls|dir|tree)\b/i.test(text)
|
|
193
|
-
|| /\b(list|show)\s+(files|dirs|directories|folders)\b/i.test(text)
|
|
194
|
-
|| /列出|目录|文件列表/.test(text)
|
|
195
|
-
) {
|
|
196
|
-
return "ls -la";
|
|
197
|
-
}
|
|
198
|
-
const cmdMatch = text.match(/(?:运行|执行|run|exec(?:ute)?)\s+`([^`]+)`/i);
|
|
199
|
-
if (cmdMatch && cmdMatch[1]) return String(cmdMatch[1]).trim();
|
|
200
|
-
return "";
|
|
201
|
-
}
|
|
202
|
-
|
|
203
105
|
function normalizeProvider(value = "") {
|
|
204
106
|
const text = String(value || "").trim().toLowerCase();
|
|
205
107
|
if (!text) return "";
|
|
@@ -317,6 +219,11 @@ function buildCoreToolSpecs() {
|
|
|
317
219
|
properties: {
|
|
318
220
|
path: { type: "string" },
|
|
319
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
|
+
},
|
|
320
227
|
append: { type: "boolean" },
|
|
321
228
|
},
|
|
322
229
|
required: ["path", "content"],
|
|
@@ -530,34 +437,22 @@ function emitPhase(callback, event = {}) {
|
|
|
530
437
|
}
|
|
531
438
|
}
|
|
532
439
|
|
|
533
|
-
|
|
440
|
+
// Shared SSE transport skeleton: POST the payload, then read the stream as
|
|
441
|
+
// SSE blocks, dispatch each non-[DONE] block to onEvent, and stop after the
|
|
442
|
+
// batch that carried [DONE]. Timeout/cancel translation and request cleanup
|
|
443
|
+
// live here so each protocol turn only declares its event handling.
|
|
444
|
+
async function runSseRequest({
|
|
534
445
|
url = "",
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
messages = [],
|
|
538
|
-
onTextDelta = null,
|
|
539
|
-
onThinkingDelta = null,
|
|
540
|
-
onPhase = null,
|
|
446
|
+
headers = {},
|
|
447
|
+
payload = {},
|
|
541
448
|
signal = null,
|
|
542
449
|
timeoutMs = 300000,
|
|
450
|
+
onPhase = null,
|
|
451
|
+
onNonStream,
|
|
452
|
+
onEvent,
|
|
453
|
+
onTail = null,
|
|
454
|
+
buildResult,
|
|
543
455
|
} = {}) {
|
|
544
|
-
const payload = {
|
|
545
|
-
model,
|
|
546
|
-
max_tokens: resolveMaxTokens(DEFAULT_OPENAI_MAX_TOKENS),
|
|
547
|
-
messages,
|
|
548
|
-
tools: buildCoreToolSpecs(),
|
|
549
|
-
tool_choice: "auto",
|
|
550
|
-
stream: true,
|
|
551
|
-
temperature: 0,
|
|
552
|
-
};
|
|
553
|
-
|
|
554
|
-
const headers = {
|
|
555
|
-
"content-type": "application/json",
|
|
556
|
-
};
|
|
557
|
-
if (apiKey) {
|
|
558
|
-
headers.authorization = `Bearer ${apiKey}`;
|
|
559
|
-
}
|
|
560
|
-
|
|
561
456
|
const request = createRequestController({ signal, timeoutMs });
|
|
562
457
|
|
|
563
458
|
emitPhase(onPhase, { type: "request_start" });
|
|
@@ -577,28 +472,13 @@ async function runOpenAiLikeTurn({
|
|
|
577
472
|
|
|
578
473
|
if (!response.body || typeof response.body.getReader !== "function") {
|
|
579
474
|
const data = await response.json();
|
|
580
|
-
|
|
581
|
-
? data.choices[0].message
|
|
582
|
-
: {};
|
|
583
|
-
const text = typeof message.content === "string" ? message.content : "";
|
|
584
|
-
const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
|
|
585
|
-
if (text && typeof onTextDelta === "function") {
|
|
586
|
-
onTextDelta(text);
|
|
587
|
-
}
|
|
588
|
-
return {
|
|
589
|
-
text,
|
|
590
|
-
toolCalls,
|
|
591
|
-
};
|
|
475
|
+
return onNonStream(data);
|
|
592
476
|
}
|
|
593
477
|
|
|
594
478
|
const reader = response.body.getReader();
|
|
595
479
|
const decoder = new TextDecoder();
|
|
596
|
-
const toolCallMap = new Map();
|
|
597
480
|
let rawBuffer = "";
|
|
598
|
-
let
|
|
599
|
-
const announcedToolNames = new Set();
|
|
600
|
-
let nextSyntheticIndex = 0;
|
|
601
|
-
let lastSyntheticIndex = -1;
|
|
481
|
+
let sawDone = false;
|
|
602
482
|
|
|
603
483
|
while (true) {
|
|
604
484
|
const { done, value } = await reader.read();
|
|
@@ -609,91 +489,178 @@ async function runOpenAiLikeTurn({
|
|
|
609
489
|
rawBuffer = parsed.rest;
|
|
610
490
|
|
|
611
491
|
for (const block of parsed.blocks) {
|
|
612
|
-
const
|
|
613
|
-
if (!
|
|
614
|
-
if (
|
|
615
|
-
|
|
616
|
-
|
|
492
|
+
const { event, data } = parseSseEventBlock(block);
|
|
493
|
+
if (!data) continue;
|
|
494
|
+
if (data === "[DONE]") {
|
|
495
|
+
// Stop reading after this batch instead of waiting for the server
|
|
496
|
+
// to close the connection, but keep the buffered tail and finish
|
|
497
|
+
// the blocks already parsed alongside [DONE] instead of silently
|
|
498
|
+
// dropping them.
|
|
499
|
+
sawDone = true;
|
|
500
|
+
continue;
|
|
617
501
|
}
|
|
618
502
|
|
|
619
|
-
|
|
620
|
-
|
|
503
|
+
onEvent({ event, data });
|
|
504
|
+
}
|
|
621
505
|
|
|
622
|
-
|
|
623
|
-
|
|
506
|
+
if (sawDone) break;
|
|
507
|
+
}
|
|
624
508
|
|
|
625
|
-
|
|
509
|
+
if (typeof onTail === "function") {
|
|
510
|
+
onTail(rawBuffer);
|
|
511
|
+
}
|
|
626
512
|
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
513
|
+
return buildResult();
|
|
514
|
+
} catch (err) {
|
|
515
|
+
if (request.timedOut()) {
|
|
516
|
+
const timeoutError = new Error(`CLI timeout (${normalizeTimeoutMs(timeoutMs)}ms)`);
|
|
517
|
+
timeoutError.code = "timeout";
|
|
518
|
+
throw timeoutError;
|
|
519
|
+
}
|
|
520
|
+
if (signal && typeof signal === "object" && signal.aborted) {
|
|
521
|
+
const cancelError = new Error("CLI cancelled");
|
|
522
|
+
cancelError.code = "cancelled";
|
|
523
|
+
throw cancelError;
|
|
524
|
+
}
|
|
525
|
+
throw err;
|
|
526
|
+
} finally {
|
|
527
|
+
request.cleanup();
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
async function runOpenAiLikeTurn({
|
|
532
|
+
url = "",
|
|
533
|
+
apiKey = "",
|
|
534
|
+
model = "",
|
|
535
|
+
messages = [],
|
|
536
|
+
onTextDelta = null,
|
|
537
|
+
onThinkingDelta = null,
|
|
538
|
+
onPhase = null,
|
|
539
|
+
signal = null,
|
|
540
|
+
timeoutMs = 300000,
|
|
541
|
+
} = {}) {
|
|
542
|
+
const payload = {
|
|
543
|
+
model,
|
|
544
|
+
max_tokens: resolveMaxTokens(DEFAULT_OPENAI_MAX_TOKENS),
|
|
545
|
+
messages,
|
|
546
|
+
tools: buildCoreToolSpecs(),
|
|
547
|
+
tool_choice: "auto",
|
|
548
|
+
stream: true,
|
|
549
|
+
temperature: 0,
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
const headers = {
|
|
553
|
+
"content-type": "application/json",
|
|
554
|
+
};
|
|
555
|
+
if (apiKey) {
|
|
556
|
+
headers.authorization = `Bearer ${apiKey}`;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const toolCallMap = new Map();
|
|
560
|
+
const announcedToolNames = new Set();
|
|
561
|
+
let responseText = "";
|
|
562
|
+
let nextSyntheticIndex = 0;
|
|
563
|
+
let lastSyntheticIndex = -1;
|
|
564
|
+
|
|
565
|
+
return runSseRequest({
|
|
566
|
+
url,
|
|
567
|
+
headers,
|
|
568
|
+
payload,
|
|
569
|
+
signal,
|
|
570
|
+
timeoutMs,
|
|
571
|
+
onPhase,
|
|
572
|
+
onNonStream: (data) => {
|
|
573
|
+
const message = data && data.choices && data.choices[0] && data.choices[0].message
|
|
574
|
+
? data.choices[0].message
|
|
575
|
+
: {};
|
|
576
|
+
const text = typeof message.content === "string" ? message.content : "";
|
|
577
|
+
const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
|
|
578
|
+
if (text && typeof onTextDelta === "function") {
|
|
579
|
+
onTextDelta(text);
|
|
580
|
+
}
|
|
581
|
+
return {
|
|
582
|
+
text,
|
|
583
|
+
toolCalls,
|
|
584
|
+
};
|
|
585
|
+
},
|
|
586
|
+
onEvent: ({ data }) => {
|
|
587
|
+
const chunk = parseJsonSafe(data, null);
|
|
588
|
+
if (!chunk || typeof chunk !== "object") return;
|
|
589
|
+
|
|
590
|
+
const choice = chunk.choices && chunk.choices[0] ? chunk.choices[0] : null;
|
|
591
|
+
if (!choice || typeof choice !== "object") return;
|
|
592
|
+
|
|
593
|
+
const delta = choice.delta && typeof choice.delta === "object" ? choice.delta : {};
|
|
594
|
+
|
|
595
|
+
const reasoningChunk = typeof delta.reasoning_content === "string"
|
|
596
|
+
? delta.reasoning_content
|
|
597
|
+
: (typeof delta.reasoning === "string" ? delta.reasoning : "");
|
|
598
|
+
if (reasoningChunk) {
|
|
599
|
+
emitPhase(onPhase, { type: "thinking_delta", text: reasoningChunk });
|
|
600
|
+
if (typeof onThinkingDelta === "function") {
|
|
601
|
+
onThinkingDelta(reasoningChunk);
|
|
635
602
|
}
|
|
603
|
+
}
|
|
636
604
|
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
}
|
|
605
|
+
if (typeof delta.content === "string" && delta.content) {
|
|
606
|
+
responseText += delta.content;
|
|
607
|
+
emitPhase(onPhase, { type: "text_delta", text: delta.content });
|
|
608
|
+
if (typeof onTextDelta === "function") {
|
|
609
|
+
onTextDelta(delta.content);
|
|
643
610
|
}
|
|
611
|
+
}
|
|
644
612
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
613
|
+
if (Array.isArray(delta.tool_calls)) {
|
|
614
|
+
for (const callPart of delta.tool_calls) {
|
|
615
|
+
let index;
|
|
616
|
+
if (Number.isFinite(callPart.index)) {
|
|
617
|
+
index = callPart.index;
|
|
618
|
+
} else if (typeof callPart.id === "string" && callPart.id) {
|
|
619
|
+
// Provider omitted index: a chunk carrying an id starts a new
|
|
620
|
+
// call, so give it its own synthetic index instead of
|
|
621
|
+
// collapsing every call into slot 0.
|
|
622
|
+
while (toolCallMap.has(nextSyntheticIndex)) nextSyntheticIndex += 1;
|
|
623
|
+
index = nextSyntheticIndex;
|
|
624
|
+
nextSyntheticIndex += 1;
|
|
625
|
+
lastSyntheticIndex = index;
|
|
626
|
+
} else if (lastSyntheticIndex >= 0) {
|
|
627
|
+
// No index and no id: continuation of the latest synthetic call.
|
|
628
|
+
index = lastSyntheticIndex;
|
|
629
|
+
} else {
|
|
630
|
+
index = 0;
|
|
631
|
+
}
|
|
632
|
+
const previous = toolCallMap.get(index) || {
|
|
633
|
+
id: "",
|
|
634
|
+
type: "function",
|
|
635
|
+
function: {
|
|
636
|
+
name: "",
|
|
637
|
+
arguments: "",
|
|
638
|
+
},
|
|
639
|
+
};
|
|
672
640
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
}
|
|
641
|
+
if (typeof callPart.id === "string" && callPart.id) previous.id = callPart.id;
|
|
642
|
+
if (callPart.function && typeof callPart.function === "object") {
|
|
643
|
+
if (typeof callPart.function.name === "string" && callPart.function.name) {
|
|
644
|
+
previous.function.name = callPart.function.name;
|
|
645
|
+
}
|
|
646
|
+
if (typeof callPart.function.arguments === "string" && callPart.function.arguments) {
|
|
647
|
+
previous.function.arguments += callPart.function.arguments;
|
|
681
648
|
}
|
|
649
|
+
}
|
|
682
650
|
|
|
683
|
-
|
|
651
|
+
toolCallMap.set(index, previous);
|
|
684
652
|
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
}
|
|
653
|
+
const toolName = previous.function.name;
|
|
654
|
+
const announceKey = `${index}:${toolName}`;
|
|
655
|
+
if (toolName && !announcedToolNames.has(announceKey)) {
|
|
656
|
+
announcedToolNames.add(announceKey);
|
|
657
|
+
emitPhase(onPhase, { type: "tool_request", name: toolName });
|
|
691
658
|
}
|
|
692
659
|
}
|
|
693
660
|
}
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
|
|
661
|
+
},
|
|
662
|
+
onTail: (rawBuffer) => {
|
|
663
|
+
if (!rawBuffer.trim()) return;
|
|
697
664
|
const fallbackBlock = parseSseDataBlock(rawBuffer);
|
|
698
665
|
if (fallbackBlock && fallbackBlock !== "[DONE]") {
|
|
699
666
|
const chunk = parseJsonSafe(fallbackBlock, null);
|
|
@@ -705,29 +672,14 @@ async function runOpenAiLikeTurn({
|
|
|
705
672
|
}
|
|
706
673
|
}
|
|
707
674
|
}
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
return {
|
|
675
|
+
},
|
|
676
|
+
buildResult: () => ({
|
|
711
677
|
text: responseText,
|
|
712
678
|
toolCalls: Array.from(toolCallMap.entries())
|
|
713
679
|
.sort((a, b) => a[0] - b[0])
|
|
714
680
|
.map((entry) => entry[1]),
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
if (request.timedOut()) {
|
|
718
|
-
const timeoutError = new Error(`CLI timeout (${normalizeTimeoutMs(timeoutMs)}ms)`);
|
|
719
|
-
timeoutError.code = "timeout";
|
|
720
|
-
throw timeoutError;
|
|
721
|
-
}
|
|
722
|
-
if (signal && typeof signal === "object" && signal.aborted) {
|
|
723
|
-
const cancelError = new Error("CLI cancelled");
|
|
724
|
-
cancelError.code = "cancelled";
|
|
725
|
-
throw cancelError;
|
|
726
|
-
}
|
|
727
|
-
throw err;
|
|
728
|
-
} finally {
|
|
729
|
-
request.cleanup();
|
|
730
|
-
}
|
|
681
|
+
}),
|
|
682
|
+
});
|
|
731
683
|
}
|
|
732
684
|
|
|
733
685
|
function normalizeAnthropicMessageContent(raw = []) {
|
|
@@ -800,25 +752,19 @@ async function runAnthropicTurn({
|
|
|
800
752
|
headers["x-api-key"] = apiKey;
|
|
801
753
|
}
|
|
802
754
|
|
|
803
|
-
const
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
const body = await response.text().catch(() => "");
|
|
817
|
-
throw new Error(`provider request failed (${response.status}): ${clipText(body, 500)}`);
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
if (!response.body || typeof response.body.getReader !== "function") {
|
|
821
|
-
const data = await response.json();
|
|
755
|
+
const blockMap = new Map();
|
|
756
|
+
let responseText = "";
|
|
757
|
+
let nextSyntheticBlockIndex = 0;
|
|
758
|
+
let lastBlockIndex = -1;
|
|
759
|
+
|
|
760
|
+
return runSseRequest({
|
|
761
|
+
url,
|
|
762
|
+
headers,
|
|
763
|
+
payload,
|
|
764
|
+
signal,
|
|
765
|
+
timeoutMs,
|
|
766
|
+
onPhase,
|
|
767
|
+
onNonStream: (data) => {
|
|
822
768
|
const content = normalizeAnthropicMessageContent(data && data.content);
|
|
823
769
|
const text = content
|
|
824
770
|
.filter((item) => item.type === "text")
|
|
@@ -832,240 +778,181 @@ async function runAnthropicTurn({
|
|
|
832
778
|
assistantContent: content,
|
|
833
779
|
toolCalls: extractAnthropicToolCalls(content),
|
|
834
780
|
};
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
rawBuffer += decoder.decode(value, { stream: true });
|
|
848
|
-
const parsed = parseSseBlocks(rawBuffer);
|
|
849
|
-
rawBuffer = parsed.rest;
|
|
850
|
-
|
|
851
|
-
for (const rawBlock of parsed.blocks) {
|
|
852
|
-
const { event, data } = parseSseEventBlock(rawBlock);
|
|
853
|
-
if (!data || data === "[DONE]") continue;
|
|
854
|
-
|
|
855
|
-
const payloadChunk = parseJsonSafe(data, null);
|
|
856
|
-
if (!payloadChunk || typeof payloadChunk !== "object") continue;
|
|
781
|
+
},
|
|
782
|
+
onEvent: ({ event, data }) => {
|
|
783
|
+
const payloadChunk = parseJsonSafe(data, null);
|
|
784
|
+
if (!payloadChunk || typeof payloadChunk !== "object") return;
|
|
785
|
+
|
|
786
|
+
if (event === "error") {
|
|
787
|
+
const errMsg = payloadChunk.error && payloadChunk.error.message
|
|
788
|
+
? String(payloadChunk.error.message)
|
|
789
|
+
: "anthropic stream error";
|
|
790
|
+
throw new Error(errMsg);
|
|
791
|
+
}
|
|
857
792
|
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
793
|
+
if (event === "content_block_start") {
|
|
794
|
+
let index;
|
|
795
|
+
if (Number.isFinite(payloadChunk.index)) {
|
|
796
|
+
index = payloadChunk.index;
|
|
797
|
+
} else {
|
|
798
|
+
// Provider omitted index: each start opens a new block, so give
|
|
799
|
+
// it its own synthetic index instead of collapsing every block
|
|
800
|
+
// into slot 0.
|
|
801
|
+
while (blockMap.has(nextSyntheticBlockIndex)) nextSyntheticBlockIndex += 1;
|
|
802
|
+
index = nextSyntheticBlockIndex;
|
|
803
|
+
nextSyntheticBlockIndex += 1;
|
|
863
804
|
}
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
if (toolName) {
|
|
896
|
-
emitPhase(onPhase, { type: "tool_request", name: toolName });
|
|
897
|
-
}
|
|
805
|
+
lastBlockIndex = index;
|
|
806
|
+
const contentBlock = payloadChunk.content_block && typeof payloadChunk.content_block === "object"
|
|
807
|
+
? payloadChunk.content_block
|
|
808
|
+
: {};
|
|
809
|
+
|
|
810
|
+
if (contentBlock.type === "text") {
|
|
811
|
+
blockMap.set(index, {
|
|
812
|
+
order: index,
|
|
813
|
+
type: "text",
|
|
814
|
+
text: String(contentBlock.text || ""),
|
|
815
|
+
});
|
|
816
|
+
} else if (contentBlock.type === "thinking") {
|
|
817
|
+
blockMap.set(index, {
|
|
818
|
+
order: index,
|
|
819
|
+
type: "thinking",
|
|
820
|
+
text: String(contentBlock.thinking || ""),
|
|
821
|
+
});
|
|
822
|
+
} else if (contentBlock.type === "tool_use") {
|
|
823
|
+
blockMap.set(index, {
|
|
824
|
+
order: index,
|
|
825
|
+
type: "tool_use",
|
|
826
|
+
id: String(contentBlock.id || ""),
|
|
827
|
+
name: String(contentBlock.name || ""),
|
|
828
|
+
input: contentBlock.input && typeof contentBlock.input === "object" && !Array.isArray(contentBlock.input)
|
|
829
|
+
? { ...contentBlock.input }
|
|
830
|
+
: {},
|
|
831
|
+
inputJson: "",
|
|
832
|
+
});
|
|
833
|
+
const toolName = String(contentBlock.name || "");
|
|
834
|
+
if (toolName) {
|
|
835
|
+
emitPhase(onPhase, { type: "tool_request", name: toolName });
|
|
898
836
|
}
|
|
899
|
-
continue;
|
|
900
837
|
}
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
901
840
|
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
841
|
+
if (event === "content_block_delta") {
|
|
842
|
+
let index;
|
|
843
|
+
if (Number.isFinite(payloadChunk.index)) {
|
|
844
|
+
index = payloadChunk.index;
|
|
845
|
+
} else if (lastBlockIndex >= 0) {
|
|
846
|
+
// No index: continuation of the most recently started block.
|
|
847
|
+
index = lastBlockIndex;
|
|
848
|
+
} else {
|
|
849
|
+
index = 0;
|
|
850
|
+
}
|
|
851
|
+
const delta = payloadChunk.delta && typeof payloadChunk.delta === "object"
|
|
852
|
+
? payloadChunk.delta
|
|
853
|
+
: {};
|
|
854
|
+
const current = blockMap.get(index) || { order: index, type: "text", text: "" };
|
|
855
|
+
|
|
856
|
+
if (delta.type === "text_delta") {
|
|
857
|
+
const deltaText = String(delta.text || "");
|
|
858
|
+
current.type = "text";
|
|
859
|
+
current.text = `${String(current.text || "")}${deltaText}`;
|
|
860
|
+
blockMap.set(index, current);
|
|
861
|
+
if (deltaText) {
|
|
862
|
+
responseText += deltaText;
|
|
863
|
+
emitPhase(onPhase, { type: "text_delta", text: deltaText });
|
|
864
|
+
if (typeof onTextDelta === "function") {
|
|
865
|
+
onTextDelta(deltaText);
|
|
920
866
|
}
|
|
921
|
-
continue;
|
|
922
867
|
}
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
923
870
|
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
}
|
|
871
|
+
if (delta.type === "thinking_delta") {
|
|
872
|
+
const deltaText = String(delta.thinking || "");
|
|
873
|
+
current.type = "thinking";
|
|
874
|
+
current.text = `${String(current.text || "")}${deltaText}`;
|
|
875
|
+
blockMap.set(index, current);
|
|
876
|
+
if (deltaText) {
|
|
877
|
+
emitPhase(onPhase, { type: "thinking_delta", text: deltaText });
|
|
878
|
+
if (typeof onThinkingDelta === "function") {
|
|
879
|
+
onThinkingDelta(deltaText);
|
|
934
880
|
}
|
|
935
|
-
continue;
|
|
936
881
|
}
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
937
884
|
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
}
|
|
885
|
+
if (delta.type === "input_json_delta") {
|
|
886
|
+
current.type = "tool_use";
|
|
887
|
+
current.inputJson = `${String(current.inputJson || "")}${String(delta.partial_json || "")}`;
|
|
888
|
+
blockMap.set(index, current);
|
|
889
|
+
return;
|
|
944
890
|
}
|
|
945
891
|
}
|
|
946
|
-
}
|
|
892
|
+
},
|
|
893
|
+
buildResult: () => {
|
|
894
|
+
const assistantContent = Array.from(blockMap.values())
|
|
895
|
+
.sort((a, b) => a.order - b.order)
|
|
896
|
+
.filter((item) => item.type !== "thinking")
|
|
897
|
+
.map((item) => {
|
|
898
|
+
if (item.type === "text") {
|
|
899
|
+
return {
|
|
900
|
+
type: "text",
|
|
901
|
+
text: String(item.text || ""),
|
|
902
|
+
};
|
|
903
|
+
}
|
|
947
904
|
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
905
|
+
const inputFromDelta = normalizeToolCallArgs(item.inputJson || "");
|
|
906
|
+
const mergedInput = {
|
|
907
|
+
...(item.input && typeof item.input === "object" ? item.input : {}),
|
|
908
|
+
...(inputFromDelta && typeof inputFromDelta === "object" ? inputFromDelta : {}),
|
|
909
|
+
};
|
|
953
910
|
return {
|
|
954
|
-
type: "
|
|
955
|
-
|
|
911
|
+
type: "tool_use",
|
|
912
|
+
id: String(item.id || `tool_${randomUUID()}`),
|
|
913
|
+
name: String(item.name || ""),
|
|
914
|
+
input: mergedInput,
|
|
956
915
|
};
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
const inputFromDelta = normalizeToolCallArgs(item.inputJson || "");
|
|
960
|
-
const mergedInput = {
|
|
961
|
-
...(item.input && typeof item.input === "object" ? item.input : {}),
|
|
962
|
-
...(inputFromDelta && typeof inputFromDelta === "object" ? inputFromDelta : {}),
|
|
963
|
-
};
|
|
964
|
-
return {
|
|
965
|
-
type: "tool_use",
|
|
966
|
-
id: String(item.id || `tool_${randomUUID()}`),
|
|
967
|
-
name: String(item.name || ""),
|
|
968
|
-
input: mergedInput,
|
|
969
|
-
};
|
|
970
|
-
});
|
|
916
|
+
});
|
|
971
917
|
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
918
|
+
if (!responseText) {
|
|
919
|
+
responseText = assistantContent
|
|
920
|
+
.filter((item) => item.type === "text")
|
|
921
|
+
.map((item) => item.text)
|
|
922
|
+
.join("");
|
|
923
|
+
}
|
|
978
924
|
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
const timeoutError = new Error(`CLI timeout (${normalizeTimeoutMs(timeoutMs)}ms)`);
|
|
987
|
-
timeoutError.code = "timeout";
|
|
988
|
-
throw timeoutError;
|
|
989
|
-
}
|
|
990
|
-
if (signal && typeof signal === "object" && signal.aborted) {
|
|
991
|
-
const cancelError = new Error("CLI cancelled");
|
|
992
|
-
cancelError.code = "cancelled";
|
|
993
|
-
throw cancelError;
|
|
994
|
-
}
|
|
995
|
-
throw err;
|
|
996
|
-
} finally {
|
|
997
|
-
request.cleanup();
|
|
998
|
-
}
|
|
925
|
+
return {
|
|
926
|
+
text: responseText,
|
|
927
|
+
assistantContent,
|
|
928
|
+
toolCalls: extractAnthropicToolCalls(assistantContent),
|
|
929
|
+
};
|
|
930
|
+
},
|
|
931
|
+
});
|
|
999
932
|
}
|
|
1000
933
|
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
if (!requestUrl) {
|
|
1024
|
-
throw new Error("ucode baseUrl is not configured");
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
const messages = cloneMessageList(historyMessages);
|
|
1028
|
-
const systemText = String(systemPrompt || "").trim();
|
|
1029
|
-
const hasSystem = messages.some((entry) => String(entry.role || "").trim() === "system");
|
|
1030
|
-
if (systemText && !hasSystem) {
|
|
1031
|
-
messages.unshift({ role: "system", content: systemText });
|
|
1032
|
-
}
|
|
1033
|
-
messages.push({ role: "user", content: String(prompt || "") });
|
|
1034
|
-
|
|
1035
|
-
let aggregated = "";
|
|
1036
|
-
let streamed = false;
|
|
1037
|
-
let toolCallsExecuted = 0;
|
|
1038
|
-
let toolErrors = 0;
|
|
1039
|
-
const toolBudget = resolveNativeToolBudget();
|
|
1040
|
-
|
|
1041
|
-
while (true) {
|
|
1042
|
-
guards.ensureActive();
|
|
1043
|
-
|
|
1044
|
-
const turnResult = await runOpenAiLikeTurn({
|
|
1045
|
-
url: requestUrl,
|
|
1046
|
-
apiKey,
|
|
1047
|
-
model: requestModel,
|
|
1048
|
-
messages,
|
|
1049
|
-
signal,
|
|
1050
|
-
timeoutMs,
|
|
1051
|
-
onPhase,
|
|
1052
|
-
onThinkingDelta,
|
|
1053
|
-
onTextDelta: (chunk) => {
|
|
1054
|
-
const text = String(chunk || "");
|
|
1055
|
-
if (!text) return;
|
|
1056
|
-
aggregated += text;
|
|
1057
|
-
if (typeof onStreamDelta === "function") {
|
|
1058
|
-
streamed = true;
|
|
1059
|
-
onStreamDelta(text);
|
|
1060
|
-
}
|
|
1061
|
-
},
|
|
1062
|
-
});
|
|
1063
|
-
|
|
1064
|
-
const toolCalls = Array.isArray(turnResult.toolCalls)
|
|
1065
|
-
? turnResult.toolCalls.filter((call) => call && call.function && typeof call.function === "object")
|
|
1066
|
-
: [];
|
|
1067
|
-
|
|
1068
|
-
if (toolCalls.length === 0) {
|
|
934
|
+
// Transport descriptors: everything the shared native loop needs that differs
|
|
935
|
+
// between the OpenAI chat-completions and Anthropic messages protocols —
|
|
936
|
+
// request URL resolution, initial message shaping, turn execution, and
|
|
937
|
+
// assistant/tool-result message formatting.
|
|
938
|
+
const TRANSPORTS = {
|
|
939
|
+
"openai-chat": {
|
|
940
|
+
resolveUrl: resolveCompletionUrl,
|
|
941
|
+
prepareMessages({ messages, systemPrompt, prompt }) {
|
|
942
|
+
const systemText = String(systemPrompt || "").trim();
|
|
943
|
+
const hasSystem = messages.some((entry) => String(entry.role || "").trim() === "system");
|
|
944
|
+
if (systemText && !hasSystem) {
|
|
945
|
+
messages.unshift({ role: "system", content: systemText });
|
|
946
|
+
}
|
|
947
|
+
messages.push({ role: "user", content: String(prompt || "") });
|
|
948
|
+
},
|
|
949
|
+
runTurn: runOpenAiLikeTurn,
|
|
950
|
+
getToolCalls(turnResult) {
|
|
951
|
+
return Array.isArray(turnResult.toolCalls)
|
|
952
|
+
? turnResult.toolCalls.filter((call) => call && call.function && typeof call.function === "object")
|
|
953
|
+
: [];
|
|
954
|
+
},
|
|
955
|
+
appendFinalAssistantMessage({ messages, turnResult }) {
|
|
1069
956
|
const text = String(turnResult.text || "").trim();
|
|
1070
957
|
if (text) {
|
|
1071
958
|
messages.push({
|
|
@@ -1073,78 +960,114 @@ async function runNativeLoopOpenAi({
|
|
|
1073
960
|
content: text,
|
|
1074
961
|
});
|
|
1075
962
|
}
|
|
1076
|
-
|
|
1077
|
-
|
|
963
|
+
},
|
|
964
|
+
prepareToolCalls({ messages, toolCalls }) {
|
|
965
|
+
const assistantToolCalls = [];
|
|
966
|
+
for (const call of toolCalls) {
|
|
967
|
+
const callId = String(call.id || `call_${randomUUID()}`);
|
|
968
|
+
const name = normalizeToolName(call.function.name || "");
|
|
969
|
+
const args = normalizeToolCallArgs(call.function.arguments || "");
|
|
970
|
+
|
|
971
|
+
assistantToolCalls.push({
|
|
972
|
+
id: callId,
|
|
973
|
+
type: "function",
|
|
974
|
+
function: {
|
|
975
|
+
name: name || String(call.function.name || ""),
|
|
976
|
+
arguments: toJsonString(args),
|
|
977
|
+
},
|
|
978
|
+
});
|
|
1078
979
|
}
|
|
1079
|
-
return {
|
|
1080
|
-
text: aggregated,
|
|
1081
|
-
streamed,
|
|
1082
|
-
toolCallsExecuted,
|
|
1083
|
-
messages,
|
|
1084
|
-
};
|
|
1085
|
-
}
|
|
1086
980
|
|
|
1087
|
-
|
|
1088
|
-
for (const call of toolCalls) {
|
|
1089
|
-
const callId = String(call.id || `call_${randomUUID()}`);
|
|
1090
|
-
const name = normalizeToolName(call.function.name || "");
|
|
1091
|
-
const args = normalizeToolCallArgs(call.function.arguments || "");
|
|
1092
|
-
|
|
1093
|
-
assistantToolCalls.push({
|
|
1094
|
-
id: callId,
|
|
1095
|
-
type: "function",
|
|
1096
|
-
function: {
|
|
1097
|
-
name: name || String(call.function.name || ""),
|
|
1098
|
-
arguments: toJsonString(args),
|
|
1099
|
-
},
|
|
1100
|
-
});
|
|
1101
|
-
}
|
|
981
|
+
if (assistantToolCalls.length === 0) return null;
|
|
1102
982
|
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
messages,
|
|
1109
|
-
};
|
|
1110
|
-
}
|
|
1111
|
-
|
|
1112
|
-
messages.push({
|
|
1113
|
-
role: "assistant",
|
|
1114
|
-
content: null,
|
|
1115
|
-
tool_calls: assistantToolCalls,
|
|
1116
|
-
});
|
|
983
|
+
messages.push({
|
|
984
|
+
role: "assistant",
|
|
985
|
+
content: null,
|
|
986
|
+
tool_calls: assistantToolCalls,
|
|
987
|
+
});
|
|
1117
988
|
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
tool: toolCall.function.name,
|
|
989
|
+
return assistantToolCalls.map((toolCall) => ({
|
|
990
|
+
name: toolCall.function.name,
|
|
1121
991
|
args: normalizeToolCallArgs(toolCall.function.arguments),
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
if (!toolResult || toolResult.ok === false) {
|
|
1127
|
-
toolErrors += 1;
|
|
1128
|
-
}
|
|
1129
|
-
enforceNativeToolBudget({
|
|
1130
|
-
toolCallsExecuted,
|
|
1131
|
-
toolErrors,
|
|
1132
|
-
maxToolCalls: toolBudget.maxToolCalls,
|
|
1133
|
-
maxToolErrors: toolBudget.maxToolErrors,
|
|
1134
|
-
lastTool: toolCall.function.name,
|
|
1135
|
-
lastError: toolResult && toolResult.error ? String(toolResult.error) : "",
|
|
1136
|
-
});
|
|
992
|
+
source: toolCall,
|
|
993
|
+
}));
|
|
994
|
+
},
|
|
995
|
+
appendToolResult({ messages, call, toolResult }) {
|
|
1137
996
|
messages.push({
|
|
1138
997
|
role: "tool",
|
|
1139
|
-
tool_call_id:
|
|
998
|
+
tool_call_id: call.source.id,
|
|
1140
999
|
content: clipText(toJsonString(toolResult), 12000),
|
|
1141
1000
|
});
|
|
1142
|
-
}
|
|
1143
|
-
}
|
|
1001
|
+
},
|
|
1002
|
+
},
|
|
1003
|
+
"anthropic-messages": {
|
|
1004
|
+
resolveUrl: resolveAnthropicMessagesUrl,
|
|
1005
|
+
prepareMessages({ messages, prompt }) {
|
|
1006
|
+
messages.push({
|
|
1007
|
+
role: "user",
|
|
1008
|
+
content: String(prompt || ""),
|
|
1009
|
+
});
|
|
1010
|
+
},
|
|
1011
|
+
runTurn: runAnthropicTurn,
|
|
1012
|
+
getToolCalls(turnResult) {
|
|
1013
|
+
return Array.isArray(turnResult.toolCalls) ? turnResult.toolCalls : [];
|
|
1014
|
+
},
|
|
1015
|
+
appendFinalAssistantMessage({ messages, turnResult }) {
|
|
1016
|
+
const assistantContent = Array.isArray(turnResult.assistantContent)
|
|
1017
|
+
? turnResult.assistantContent
|
|
1018
|
+
: [];
|
|
1019
|
+
if (assistantContent.length > 0) {
|
|
1020
|
+
messages.push({
|
|
1021
|
+
role: "assistant",
|
|
1022
|
+
content: assistantContent,
|
|
1023
|
+
});
|
|
1024
|
+
} else if (String(turnResult.text || "").trim()) {
|
|
1025
|
+
messages.push({
|
|
1026
|
+
role: "assistant",
|
|
1027
|
+
content: [
|
|
1028
|
+
{
|
|
1029
|
+
type: "text",
|
|
1030
|
+
text: String(turnResult.text || ""),
|
|
1031
|
+
},
|
|
1032
|
+
],
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
},
|
|
1036
|
+
prepareToolCalls({ messages, turnResult, toolCalls }) {
|
|
1037
|
+
const assistantContent = Array.isArray(turnResult.assistantContent)
|
|
1038
|
+
? turnResult.assistantContent
|
|
1039
|
+
: [];
|
|
1144
1040
|
|
|
1145
|
-
|
|
1041
|
+
messages.push({
|
|
1042
|
+
role: "assistant",
|
|
1043
|
+
content: assistantContent,
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
return toolCalls.map((call) => ({
|
|
1047
|
+
name: call.name,
|
|
1048
|
+
args: call.args,
|
|
1049
|
+
source: call,
|
|
1050
|
+
}));
|
|
1051
|
+
},
|
|
1052
|
+
appendToolResult({ collected, call, toolResult }) {
|
|
1053
|
+
collected.push({
|
|
1054
|
+
type: "tool_result",
|
|
1055
|
+
tool_use_id: String(call.source.id || ""),
|
|
1056
|
+
content: clipText(toJsonString(toolResult), 12000),
|
|
1057
|
+
is_error: Boolean(!toolResult || toolResult.ok === false),
|
|
1058
|
+
});
|
|
1059
|
+
},
|
|
1060
|
+
flushToolResults({ messages, collected }) {
|
|
1061
|
+
messages.push({
|
|
1062
|
+
role: "user",
|
|
1063
|
+
content: collected,
|
|
1064
|
+
});
|
|
1065
|
+
},
|
|
1066
|
+
},
|
|
1067
|
+
};
|
|
1146
1068
|
|
|
1147
|
-
async function
|
|
1069
|
+
async function runNativeLoop({
|
|
1070
|
+
transport,
|
|
1148
1071
|
workspaceRoot = process.cwd(),
|
|
1149
1072
|
prompt = "",
|
|
1150
1073
|
systemPrompt = "",
|
|
@@ -1165,16 +1088,13 @@ async function runNativeLoopAnthropic({
|
|
|
1165
1088
|
throw new Error("ucode model is not configured");
|
|
1166
1089
|
}
|
|
1167
1090
|
|
|
1168
|
-
const requestUrl =
|
|
1091
|
+
const requestUrl = transport.resolveUrl(baseUrl);
|
|
1169
1092
|
if (!requestUrl) {
|
|
1170
1093
|
throw new Error("ucode baseUrl is not configured");
|
|
1171
1094
|
}
|
|
1172
1095
|
|
|
1173
1096
|
const messages = cloneMessageList(historyMessages);
|
|
1174
|
-
|
|
1175
|
-
role: "user",
|
|
1176
|
-
content: String(prompt || ""),
|
|
1177
|
-
});
|
|
1097
|
+
transport.prepareMessages({ messages, systemPrompt, prompt });
|
|
1178
1098
|
|
|
1179
1099
|
let aggregated = "";
|
|
1180
1100
|
let streamed = false;
|
|
@@ -1185,7 +1105,7 @@ async function runNativeLoopAnthropic({
|
|
|
1185
1105
|
while (true) {
|
|
1186
1106
|
guards.ensureActive();
|
|
1187
1107
|
|
|
1188
|
-
const turnResult = await
|
|
1108
|
+
const turnResult = await transport.runTurn({
|
|
1189
1109
|
url: requestUrl,
|
|
1190
1110
|
apiKey,
|
|
1191
1111
|
model: requestModel,
|
|
@@ -1206,28 +1126,10 @@ async function runNativeLoopAnthropic({
|
|
|
1206
1126
|
},
|
|
1207
1127
|
});
|
|
1208
1128
|
|
|
1209
|
-
const toolCalls =
|
|
1129
|
+
const toolCalls = transport.getToolCalls(turnResult);
|
|
1210
1130
|
|
|
1211
1131
|
if (toolCalls.length === 0) {
|
|
1212
|
-
|
|
1213
|
-
? turnResult.assistantContent
|
|
1214
|
-
: [];
|
|
1215
|
-
if (assistantContent.length > 0) {
|
|
1216
|
-
messages.push({
|
|
1217
|
-
role: "assistant",
|
|
1218
|
-
content: assistantContent,
|
|
1219
|
-
});
|
|
1220
|
-
} else if (String(turnResult.text || "").trim()) {
|
|
1221
|
-
messages.push({
|
|
1222
|
-
role: "assistant",
|
|
1223
|
-
content: [
|
|
1224
|
-
{
|
|
1225
|
-
type: "text",
|
|
1226
|
-
text: String(turnResult.text || ""),
|
|
1227
|
-
},
|
|
1228
|
-
],
|
|
1229
|
-
});
|
|
1230
|
-
}
|
|
1132
|
+
transport.appendFinalAssistantMessage({ messages, turnResult });
|
|
1231
1133
|
const text = String(turnResult.text || "").trim();
|
|
1232
1134
|
if (!aggregated.trim() && text) {
|
|
1233
1135
|
aggregated = text;
|
|
@@ -1240,20 +1142,21 @@ async function runNativeLoopAnthropic({
|
|
|
1240
1142
|
};
|
|
1241
1143
|
}
|
|
1242
1144
|
|
|
1243
|
-
const
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1145
|
+
const pendingCalls = transport.prepareToolCalls({ messages, turnResult, toolCalls });
|
|
1146
|
+
if (!pendingCalls) {
|
|
1147
|
+
return {
|
|
1148
|
+
text: aggregated,
|
|
1149
|
+
streamed,
|
|
1150
|
+
toolCallsExecuted,
|
|
1151
|
+
messages,
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1251
1154
|
|
|
1252
|
-
const
|
|
1253
|
-
for (const
|
|
1155
|
+
const collectedResults = [];
|
|
1156
|
+
for (const pending of pendingCalls) {
|
|
1254
1157
|
const toolResult = runCoreTool({
|
|
1255
|
-
tool:
|
|
1256
|
-
args:
|
|
1158
|
+
tool: pending.name,
|
|
1159
|
+
args: pending.args,
|
|
1257
1160
|
workspaceRoot,
|
|
1258
1161
|
onToolEvent,
|
|
1259
1162
|
});
|
|
@@ -1266,23 +1169,21 @@ async function runNativeLoopAnthropic({
|
|
|
1266
1169
|
toolErrors,
|
|
1267
1170
|
maxToolCalls: toolBudget.maxToolCalls,
|
|
1268
1171
|
maxToolErrors: toolBudget.maxToolErrors,
|
|
1269
|
-
lastTool:
|
|
1172
|
+
lastTool: pending.name,
|
|
1270
1173
|
lastError: toolResult && toolResult.error ? String(toolResult.error) : "",
|
|
1271
1174
|
});
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1175
|
+
transport.appendToolResult({
|
|
1176
|
+
messages,
|
|
1177
|
+
collected: collectedResults,
|
|
1178
|
+
call: pending,
|
|
1179
|
+
toolResult,
|
|
1277
1180
|
});
|
|
1278
1181
|
}
|
|
1279
1182
|
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
});
|
|
1183
|
+
if (typeof transport.flushToolResults === "function") {
|
|
1184
|
+
transport.flushToolResults({ messages, collected: collectedResults });
|
|
1185
|
+
}
|
|
1284
1186
|
}
|
|
1285
|
-
|
|
1286
1187
|
}
|
|
1287
1188
|
|
|
1288
1189
|
async function runNativeAgentTask({
|
|
@@ -1303,6 +1204,17 @@ async function runNativeAgentTask({
|
|
|
1303
1204
|
const guards = createGuards({ signal, timeoutMs });
|
|
1304
1205
|
const nextSessionId = String(sessionId || "").trim() || `native-${randomUUID()}`;
|
|
1305
1206
|
const promptText = String(prompt || "").trim();
|
|
1207
|
+
// Track every text delta so the error path can return the partial output
|
|
1208
|
+
// the model already produced instead of discarding it.
|
|
1209
|
+
let partialOutput = "";
|
|
1210
|
+
const trackingStreamDelta = (chunk) => {
|
|
1211
|
+
const text = String(chunk || "");
|
|
1212
|
+
if (!text) return;
|
|
1213
|
+
partialOutput += text;
|
|
1214
|
+
if (typeof onStreamDelta === "function") {
|
|
1215
|
+
onStreamDelta(chunk);
|
|
1216
|
+
}
|
|
1217
|
+
};
|
|
1306
1218
|
|
|
1307
1219
|
try {
|
|
1308
1220
|
guards.ensureActive();
|
|
@@ -1323,11 +1235,10 @@ async function runNativeAgentTask({
|
|
|
1323
1235
|
model,
|
|
1324
1236
|
});
|
|
1325
1237
|
|
|
1326
|
-
const
|
|
1327
|
-
? runNativeLoopAnthropic
|
|
1328
|
-
: runNativeLoopOpenAi;
|
|
1238
|
+
const transport = TRANSPORTS[runtime.transport] || TRANSPORTS["openai-chat"];
|
|
1329
1239
|
|
|
1330
|
-
const runResult = await
|
|
1240
|
+
const runResult = await runNativeLoop({
|
|
1241
|
+
transport,
|
|
1331
1242
|
workspaceRoot,
|
|
1332
1243
|
prompt: promptText,
|
|
1333
1244
|
systemPrompt,
|
|
@@ -1336,7 +1247,7 @@ async function runNativeAgentTask({
|
|
|
1336
1247
|
baseUrl: runtime.baseUrl,
|
|
1337
1248
|
apiKey: runtime.apiKey,
|
|
1338
1249
|
timeoutMs,
|
|
1339
|
-
onStreamDelta,
|
|
1250
|
+
onStreamDelta: trackingStreamDelta,
|
|
1340
1251
|
onThinkingDelta,
|
|
1341
1252
|
onPhase,
|
|
1342
1253
|
onToolEvent,
|
|
@@ -1356,14 +1267,16 @@ async function runNativeAgentTask({
|
|
|
1356
1267
|
output: outputText,
|
|
1357
1268
|
messages: cloneMessageList(runResult.messages),
|
|
1358
1269
|
sessionId: nextSessionId,
|
|
1359
|
-
streamed
|
|
1270
|
+
// The loop marks streamed=true whenever it receives a stream callback;
|
|
1271
|
+
// only report it when the caller actually registered one.
|
|
1272
|
+
streamed: Boolean(runResult.streamed) && typeof onStreamDelta === "function",
|
|
1360
1273
|
};
|
|
1361
1274
|
} catch (err) {
|
|
1362
1275
|
const message = err && err.message ? err.message : "native runner failed";
|
|
1363
1276
|
return {
|
|
1364
1277
|
ok: false,
|
|
1365
1278
|
error: message,
|
|
1366
|
-
output:
|
|
1279
|
+
output: partialOutput.trim(),
|
|
1367
1280
|
sessionId: nextSessionId,
|
|
1368
1281
|
streamed: false,
|
|
1369
1282
|
};
|
|
@@ -1372,9 +1285,6 @@ async function runNativeAgentTask({
|
|
|
1372
1285
|
|
|
1373
1286
|
module.exports = {
|
|
1374
1287
|
runNativeAgentTask,
|
|
1375
|
-
parseReadIntent,
|
|
1376
|
-
parseBashIntent,
|
|
1377
|
-
extractPreflightEvidence,
|
|
1378
1288
|
resolveRuntimeConfig,
|
|
1379
1289
|
resolveCompletionUrl,
|
|
1380
1290
|
resolveAnthropicMessagesUrl,
|