u-foo 2.5.7 → 2.5.9
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/README.md +2 -0
- package/README.zh-CN.md +3 -1
- package/bin/ufoo.js +6 -1
- package/bin/ukimi.js +70 -0
- package/package.json +2 -1
- package/src/agents/activity/activityDetector.js +7 -0
- package/src/agents/launch/launcher.js +6 -5
- package/src/agents/launch/readyDetector.js +21 -0
- package/src/agents/prompts/defaultBootstrap.js +20 -3
- package/src/agents/providers/credentials/index.js +7 -0
- package/src/agents/providers/credentials/kimi.js +342 -0
- package/src/agents/providers/directAuthStatus.js +94 -0
- package/src/app/chat/commandExecutor.js +10 -5
- package/src/app/chat/commands.js +1 -0
- package/src/app/chat/dashboardView.js +1 -0
- package/src/app/chat/multiWindow/index.js +88 -46
- package/src/app/chat/multiWindow/renderer.js +57 -8
- package/src/app/cli/run.js +5 -3
- package/src/code/nativeRunner.js +522 -514
- package/src/config.js +8 -0
- package/src/orchestration/groups/validateTemplate.js +1 -1
- package/src/runtime/daemon/groupOrchestrator.js +6 -0
- package/src/runtime/daemon/index.js +12 -3
- package/src/runtime/daemon/ops.js +7 -1
- package/src/runtime/daemon/providerSessions.js +41 -2
- package/src/tools/schemaFixtures.js +1 -1
- package/src/ui/format/index.js +14 -2
- package/src/ui/ink/ChatApp.js +321 -64
- package/src/ui/ink/chatReducer.js +32 -8
- package/src/ui/runInk.js +18 -9
package/src/code/nativeRunner.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
const { randomUUID } = require("crypto");
|
|
2
2
|
const { loadConfig, defaultAgentModelForProvider, sameModelProvider } = require("../config");
|
|
3
|
+
const {
|
|
4
|
+
readKimiAccessToken,
|
|
5
|
+
resolveKimiUpstreamCredentials,
|
|
6
|
+
} = require("../agents/providers/credentials/kimi");
|
|
3
7
|
const { runToolCall } = require("./dispatch");
|
|
4
8
|
const { getReadToolDescription } = require("../agents/prompts/native/toolDescriptions/read");
|
|
5
9
|
const { getWriteToolDescription } = require("../agents/prompts/native/toolDescriptions/write");
|
|
@@ -9,6 +13,8 @@ const { getBashToolDescription } = require("../agents/prompts/native/toolDescrip
|
|
|
9
13
|
const CORE_TOOL_NAMES = new Set(["read", "write", "edit", "bash"]);
|
|
10
14
|
const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
|
|
11
15
|
const DEFAULT_ANTHROPIC_BASE_URL = "https://api.anthropic.com/v1";
|
|
16
|
+
const DEFAULT_KIMI_BASE_URL = "https://api.kimi.com/coding/v1";
|
|
17
|
+
const DEFAULT_KIMI_MODEL = "k3";
|
|
12
18
|
// Claude Code SDK defaults to no turn limit; built-in agents cap at 30 (DreamTask)
|
|
13
19
|
// to 200 (fork). We count individual tool calls (not turns), so 100 leaves headroom
|
|
14
20
|
// for non-trivial tasks while still catching runaway loops. Override via env.
|
|
@@ -107,6 +113,7 @@ function normalizeProvider(value = "") {
|
|
|
107
113
|
if (!text) return "";
|
|
108
114
|
if (text === "codex" || text === "codex-cli" || text === "codex-code") return "openai";
|
|
109
115
|
if (text === "claude" || text === "claude-cli" || text === "claude-code") return "anthropic";
|
|
116
|
+
if (text === "kimi" || text === "kimi-code" || text === "moonshot") return "kimi";
|
|
110
117
|
if (text === "openai" || text === "anthropic") return text;
|
|
111
118
|
return text;
|
|
112
119
|
}
|
|
@@ -116,6 +123,7 @@ function resolveTransport({ provider = "", baseUrl = "" } = {}) {
|
|
|
116
123
|
const url = String(baseUrl || "").trim().toLowerCase();
|
|
117
124
|
|
|
118
125
|
if (normalizedProvider === "anthropic") return "anthropic-messages";
|
|
126
|
+
if (normalizedProvider === "kimi") return "openai-chat";
|
|
119
127
|
if (url.includes("anthropic.com")) return "anthropic-messages";
|
|
120
128
|
if (/\/messages(?:$|[/?#])/.test(url) && !/\/chat\/completions(?:$|[/?#])/.test(url)) {
|
|
121
129
|
return "anthropic-messages";
|
|
@@ -141,12 +149,14 @@ function resolveRuntimeConfig({ workspaceRoot = process.cwd(), provider = "", mo
|
|
|
141
149
|
model
|
|
142
150
|
|| process.env.UFOO_UCODE_MODEL
|
|
143
151
|
|| configuredModel
|
|
144
|
-
|| defaultAgentModelForProvider(selectedProvider)
|
|
152
|
+
|| (selectedProvider === "kimi" ? DEFAULT_KIMI_MODEL : defaultAgentModelForProvider(selectedProvider))
|
|
145
153
|
).trim();
|
|
146
154
|
|
|
147
155
|
const defaultBaseUrl = selectedProvider === "anthropic"
|
|
148
156
|
? String(process.env.ANTHROPIC_BASE_URL || DEFAULT_ANTHROPIC_BASE_URL)
|
|
149
|
-
:
|
|
157
|
+
: selectedProvider === "kimi"
|
|
158
|
+
? DEFAULT_KIMI_BASE_URL
|
|
159
|
+
: String(process.env.OPENAI_BASE_URL || DEFAULT_OPENAI_BASE_URL);
|
|
150
160
|
|
|
151
161
|
const baseUrl = String(
|
|
152
162
|
process.env.UFOO_UCODE_BASE_URL
|
|
@@ -154,19 +164,38 @@ function resolveRuntimeConfig({ workspaceRoot = process.cwd(), provider = "", mo
|
|
|
154
164
|
|| defaultBaseUrl
|
|
155
165
|
).trim();
|
|
156
166
|
|
|
157
|
-
const
|
|
167
|
+
const explicitApiKey = String(
|
|
158
168
|
process.env.UFOO_UCODE_API_KEY
|
|
159
169
|
|| config.ucodeApiKey
|
|
160
|
-
|| (selectedProvider === "openai" ? process.env.OPENAI_API_KEY : "")
|
|
161
|
-
|| (selectedProvider === "anthropic" ? process.env.ANTHROPIC_API_KEY : "")
|
|
162
170
|
|| ""
|
|
163
171
|
).trim();
|
|
172
|
+
let apiKey = explicitApiKey;
|
|
173
|
+
let apiKeySource = explicitApiKey ? "explicit" : "";
|
|
174
|
+
let kimiCredentialState = "";
|
|
175
|
+
if (!apiKey && selectedProvider === "kimi") {
|
|
176
|
+
const credential = readKimiAccessToken({ env: process.env });
|
|
177
|
+
if (credential && credential.accessToken) {
|
|
178
|
+
apiKey = String(credential.accessToken).trim();
|
|
179
|
+
apiKeySource = "kimi-credential";
|
|
180
|
+
kimiCredentialState = String(credential.state || "");
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (!apiKey) {
|
|
184
|
+
apiKey = String(
|
|
185
|
+
(selectedProvider === "openai" ? process.env.OPENAI_API_KEY : "")
|
|
186
|
+
|| (selectedProvider === "anthropic" ? process.env.ANTHROPIC_API_KEY : "")
|
|
187
|
+
|| ""
|
|
188
|
+
).trim();
|
|
189
|
+
if (apiKey) apiKeySource = "env";
|
|
190
|
+
}
|
|
164
191
|
|
|
165
192
|
return {
|
|
166
193
|
provider: selectedProvider,
|
|
167
194
|
model: selectedModel,
|
|
168
195
|
baseUrl,
|
|
169
196
|
apiKey,
|
|
197
|
+
apiKeySource,
|
|
198
|
+
kimiCredentialState,
|
|
170
199
|
transport: resolveTransport({ provider: selectedProvider, baseUrl }),
|
|
171
200
|
};
|
|
172
201
|
}
|
|
@@ -437,34 +466,22 @@ function emitPhase(callback, event = {}) {
|
|
|
437
466
|
}
|
|
438
467
|
}
|
|
439
468
|
|
|
440
|
-
|
|
469
|
+
// Shared SSE transport skeleton: POST the payload, then read the stream as
|
|
470
|
+
// SSE blocks, dispatch each non-[DONE] block to onEvent, and stop after the
|
|
471
|
+
// batch that carried [DONE]. Timeout/cancel translation and request cleanup
|
|
472
|
+
// live here so each protocol turn only declares its event handling.
|
|
473
|
+
async function runSseRequest({
|
|
441
474
|
url = "",
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
messages = [],
|
|
445
|
-
onTextDelta = null,
|
|
446
|
-
onThinkingDelta = null,
|
|
447
|
-
onPhase = null,
|
|
475
|
+
headers = {},
|
|
476
|
+
payload = {},
|
|
448
477
|
signal = null,
|
|
449
478
|
timeoutMs = 300000,
|
|
479
|
+
onPhase = null,
|
|
480
|
+
onNonStream,
|
|
481
|
+
onEvent,
|
|
482
|
+
onTail = null,
|
|
483
|
+
buildResult,
|
|
450
484
|
} = {}) {
|
|
451
|
-
const payload = {
|
|
452
|
-
model,
|
|
453
|
-
max_tokens: resolveMaxTokens(DEFAULT_OPENAI_MAX_TOKENS),
|
|
454
|
-
messages,
|
|
455
|
-
tools: buildCoreToolSpecs(),
|
|
456
|
-
tool_choice: "auto",
|
|
457
|
-
stream: true,
|
|
458
|
-
temperature: 0,
|
|
459
|
-
};
|
|
460
|
-
|
|
461
|
-
const headers = {
|
|
462
|
-
"content-type": "application/json",
|
|
463
|
-
};
|
|
464
|
-
if (apiKey) {
|
|
465
|
-
headers.authorization = `Bearer ${apiKey}`;
|
|
466
|
-
}
|
|
467
|
-
|
|
468
485
|
const request = createRequestController({ signal, timeoutMs });
|
|
469
486
|
|
|
470
487
|
emitPhase(onPhase, { type: "request_start" });
|
|
@@ -484,28 +501,12 @@ async function runOpenAiLikeTurn({
|
|
|
484
501
|
|
|
485
502
|
if (!response.body || typeof response.body.getReader !== "function") {
|
|
486
503
|
const data = await response.json();
|
|
487
|
-
|
|
488
|
-
? data.choices[0].message
|
|
489
|
-
: {};
|
|
490
|
-
const text = typeof message.content === "string" ? message.content : "";
|
|
491
|
-
const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
|
|
492
|
-
if (text && typeof onTextDelta === "function") {
|
|
493
|
-
onTextDelta(text);
|
|
494
|
-
}
|
|
495
|
-
return {
|
|
496
|
-
text,
|
|
497
|
-
toolCalls,
|
|
498
|
-
};
|
|
504
|
+
return onNonStream(data);
|
|
499
505
|
}
|
|
500
506
|
|
|
501
507
|
const reader = response.body.getReader();
|
|
502
508
|
const decoder = new TextDecoder();
|
|
503
|
-
const toolCallMap = new Map();
|
|
504
509
|
let rawBuffer = "";
|
|
505
|
-
let responseText = "";
|
|
506
|
-
const announcedToolNames = new Set();
|
|
507
|
-
let nextSyntheticIndex = 0;
|
|
508
|
-
let lastSyntheticIndex = -1;
|
|
509
510
|
let sawDone = false;
|
|
510
511
|
|
|
511
512
|
while (true) {
|
|
@@ -517,96 +518,180 @@ async function runOpenAiLikeTurn({
|
|
|
517
518
|
rawBuffer = parsed.rest;
|
|
518
519
|
|
|
519
520
|
for (const block of parsed.blocks) {
|
|
520
|
-
const
|
|
521
|
-
if (!
|
|
522
|
-
if (
|
|
523
|
-
// Stop reading after this batch
|
|
524
|
-
//
|
|
525
|
-
//
|
|
521
|
+
const { event, data } = parseSseEventBlock(block);
|
|
522
|
+
if (!data) continue;
|
|
523
|
+
if (data === "[DONE]") {
|
|
524
|
+
// Stop reading after this batch instead of waiting for the server
|
|
525
|
+
// to close the connection, but keep the buffered tail and finish
|
|
526
|
+
// the blocks already parsed alongside [DONE] instead of silently
|
|
527
|
+
// dropping them.
|
|
526
528
|
sawDone = true;
|
|
527
529
|
continue;
|
|
528
530
|
}
|
|
529
531
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
+
onEvent({ event, data });
|
|
533
|
+
}
|
|
532
534
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
+
if (sawDone) break;
|
|
536
|
+
}
|
|
535
537
|
|
|
536
|
-
|
|
538
|
+
if (typeof onTail === "function") {
|
|
539
|
+
onTail(rawBuffer);
|
|
540
|
+
}
|
|
537
541
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
542
|
+
return buildResult();
|
|
543
|
+
} catch (err) {
|
|
544
|
+
if (request.timedOut()) {
|
|
545
|
+
const timeoutError = new Error(`CLI timeout (${normalizeTimeoutMs(timeoutMs)}ms)`);
|
|
546
|
+
timeoutError.code = "timeout";
|
|
547
|
+
throw timeoutError;
|
|
548
|
+
}
|
|
549
|
+
if (signal && typeof signal === "object" && signal.aborted) {
|
|
550
|
+
const cancelError = new Error("CLI cancelled");
|
|
551
|
+
cancelError.code = "cancelled";
|
|
552
|
+
throw cancelError;
|
|
553
|
+
}
|
|
554
|
+
throw err;
|
|
555
|
+
} finally {
|
|
556
|
+
request.cleanup();
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
async function runOpenAiLikeTurn({
|
|
561
|
+
url = "",
|
|
562
|
+
apiKey = "",
|
|
563
|
+
model = "",
|
|
564
|
+
provider = "",
|
|
565
|
+
messages = [],
|
|
566
|
+
onTextDelta = null,
|
|
567
|
+
onThinkingDelta = null,
|
|
568
|
+
onPhase = null,
|
|
569
|
+
signal = null,
|
|
570
|
+
timeoutMs = 300000,
|
|
571
|
+
} = {}) {
|
|
572
|
+
const payload = {
|
|
573
|
+
model,
|
|
574
|
+
max_tokens: resolveMaxTokens(DEFAULT_OPENAI_MAX_TOKENS),
|
|
575
|
+
messages,
|
|
576
|
+
tools: buildCoreToolSpecs(),
|
|
577
|
+
tool_choice: "auto",
|
|
578
|
+
stream: true,
|
|
579
|
+
// Kimi k3 rejects any temperature other than 1.
|
|
580
|
+
temperature: normalizeProvider(provider) === "kimi" ? 1 : 0,
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
const headers = {
|
|
584
|
+
"content-type": "application/json",
|
|
585
|
+
};
|
|
586
|
+
if (apiKey) {
|
|
587
|
+
headers.authorization = `Bearer ${apiKey}`;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const toolCallMap = new Map();
|
|
591
|
+
const announcedToolNames = new Set();
|
|
592
|
+
let responseText = "";
|
|
593
|
+
let nextSyntheticIndex = 0;
|
|
594
|
+
let lastSyntheticIndex = -1;
|
|
595
|
+
|
|
596
|
+
return runSseRequest({
|
|
597
|
+
url,
|
|
598
|
+
headers,
|
|
599
|
+
payload,
|
|
600
|
+
signal,
|
|
601
|
+
timeoutMs,
|
|
602
|
+
onPhase,
|
|
603
|
+
onNonStream: (data) => {
|
|
604
|
+
const message = data && data.choices && data.choices[0] && data.choices[0].message
|
|
605
|
+
? data.choices[0].message
|
|
606
|
+
: {};
|
|
607
|
+
const text = typeof message.content === "string" ? message.content : "";
|
|
608
|
+
const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
|
|
609
|
+
if (text && typeof onTextDelta === "function") {
|
|
610
|
+
onTextDelta(text);
|
|
611
|
+
}
|
|
612
|
+
return {
|
|
613
|
+
text,
|
|
614
|
+
toolCalls,
|
|
615
|
+
};
|
|
616
|
+
},
|
|
617
|
+
onEvent: ({ data }) => {
|
|
618
|
+
const chunk = parseJsonSafe(data, null);
|
|
619
|
+
if (!chunk || typeof chunk !== "object") return;
|
|
620
|
+
|
|
621
|
+
const choice = chunk.choices && chunk.choices[0] ? chunk.choices[0] : null;
|
|
622
|
+
if (!choice || typeof choice !== "object") return;
|
|
623
|
+
|
|
624
|
+
const delta = choice.delta && typeof choice.delta === "object" ? choice.delta : {};
|
|
625
|
+
|
|
626
|
+
const reasoningChunk = typeof delta.reasoning_content === "string"
|
|
627
|
+
? delta.reasoning_content
|
|
628
|
+
: (typeof delta.reasoning === "string" ? delta.reasoning : "");
|
|
629
|
+
if (reasoningChunk) {
|
|
630
|
+
emitPhase(onPhase, { type: "thinking_delta", text: reasoningChunk });
|
|
631
|
+
if (typeof onThinkingDelta === "function") {
|
|
632
|
+
onThinkingDelta(reasoningChunk);
|
|
546
633
|
}
|
|
634
|
+
}
|
|
547
635
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
}
|
|
636
|
+
if (typeof delta.content === "string" && delta.content) {
|
|
637
|
+
responseText += delta.content;
|
|
638
|
+
emitPhase(onPhase, { type: "text_delta", text: delta.content });
|
|
639
|
+
if (typeof onTextDelta === "function") {
|
|
640
|
+
onTextDelta(delta.content);
|
|
554
641
|
}
|
|
642
|
+
}
|
|
555
643
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
644
|
+
if (Array.isArray(delta.tool_calls)) {
|
|
645
|
+
for (const callPart of delta.tool_calls) {
|
|
646
|
+
let index;
|
|
647
|
+
if (Number.isFinite(callPart.index)) {
|
|
648
|
+
index = callPart.index;
|
|
649
|
+
} else if (typeof callPart.id === "string" && callPart.id) {
|
|
650
|
+
// Provider omitted index: a chunk carrying an id starts a new
|
|
651
|
+
// call, so give it its own synthetic index instead of
|
|
652
|
+
// collapsing every call into slot 0.
|
|
653
|
+
while (toolCallMap.has(nextSyntheticIndex)) nextSyntheticIndex += 1;
|
|
654
|
+
index = nextSyntheticIndex;
|
|
655
|
+
nextSyntheticIndex += 1;
|
|
656
|
+
lastSyntheticIndex = index;
|
|
657
|
+
} else if (lastSyntheticIndex >= 0) {
|
|
658
|
+
// No index and no id: continuation of the latest synthetic call.
|
|
659
|
+
index = lastSyntheticIndex;
|
|
660
|
+
} else {
|
|
661
|
+
index = 0;
|
|
662
|
+
}
|
|
663
|
+
const previous = toolCallMap.get(index) || {
|
|
664
|
+
id: "",
|
|
665
|
+
type: "function",
|
|
666
|
+
function: {
|
|
667
|
+
name: "",
|
|
668
|
+
arguments: "",
|
|
669
|
+
},
|
|
670
|
+
};
|
|
583
671
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
}
|
|
672
|
+
if (typeof callPart.id === "string" && callPart.id) previous.id = callPart.id;
|
|
673
|
+
if (callPart.function && typeof callPart.function === "object") {
|
|
674
|
+
if (typeof callPart.function.name === "string" && callPart.function.name) {
|
|
675
|
+
previous.function.name = callPart.function.name;
|
|
676
|
+
}
|
|
677
|
+
if (typeof callPart.function.arguments === "string" && callPart.function.arguments) {
|
|
678
|
+
previous.function.arguments += callPart.function.arguments;
|
|
592
679
|
}
|
|
680
|
+
}
|
|
593
681
|
|
|
594
|
-
|
|
682
|
+
toolCallMap.set(index, previous);
|
|
595
683
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
}
|
|
684
|
+
const toolName = previous.function.name;
|
|
685
|
+
const announceKey = `${index}:${toolName}`;
|
|
686
|
+
if (toolName && !announcedToolNames.has(announceKey)) {
|
|
687
|
+
announcedToolNames.add(announceKey);
|
|
688
|
+
emitPhase(onPhase, { type: "tool_request", name: toolName });
|
|
602
689
|
}
|
|
603
690
|
}
|
|
604
691
|
}
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
if (rawBuffer.trim()) {
|
|
692
|
+
},
|
|
693
|
+
onTail: (rawBuffer) => {
|
|
694
|
+
if (!rawBuffer.trim()) return;
|
|
610
695
|
const fallbackBlock = parseSseDataBlock(rawBuffer);
|
|
611
696
|
if (fallbackBlock && fallbackBlock !== "[DONE]") {
|
|
612
697
|
const chunk = parseJsonSafe(fallbackBlock, null);
|
|
@@ -618,29 +703,14 @@ async function runOpenAiLikeTurn({
|
|
|
618
703
|
}
|
|
619
704
|
}
|
|
620
705
|
}
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
return {
|
|
706
|
+
},
|
|
707
|
+
buildResult: () => ({
|
|
624
708
|
text: responseText,
|
|
625
709
|
toolCalls: Array.from(toolCallMap.entries())
|
|
626
710
|
.sort((a, b) => a[0] - b[0])
|
|
627
711
|
.map((entry) => entry[1]),
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
if (request.timedOut()) {
|
|
631
|
-
const timeoutError = new Error(`CLI timeout (${normalizeTimeoutMs(timeoutMs)}ms)`);
|
|
632
|
-
timeoutError.code = "timeout";
|
|
633
|
-
throw timeoutError;
|
|
634
|
-
}
|
|
635
|
-
if (signal && typeof signal === "object" && signal.aborted) {
|
|
636
|
-
const cancelError = new Error("CLI cancelled");
|
|
637
|
-
cancelError.code = "cancelled";
|
|
638
|
-
throw cancelError;
|
|
639
|
-
}
|
|
640
|
-
throw err;
|
|
641
|
-
} finally {
|
|
642
|
-
request.cleanup();
|
|
643
|
-
}
|
|
712
|
+
}),
|
|
713
|
+
});
|
|
644
714
|
}
|
|
645
715
|
|
|
646
716
|
function normalizeAnthropicMessageContent(raw = []) {
|
|
@@ -713,25 +783,19 @@ async function runAnthropicTurn({
|
|
|
713
783
|
headers["x-api-key"] = apiKey;
|
|
714
784
|
}
|
|
715
785
|
|
|
716
|
-
const
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
const body = await response.text().catch(() => "");
|
|
730
|
-
throw new Error(`provider request failed (${response.status}): ${clipText(body, 500)}`);
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
if (!response.body || typeof response.body.getReader !== "function") {
|
|
734
|
-
const data = await response.json();
|
|
786
|
+
const blockMap = new Map();
|
|
787
|
+
let responseText = "";
|
|
788
|
+
let nextSyntheticBlockIndex = 0;
|
|
789
|
+
let lastBlockIndex = -1;
|
|
790
|
+
|
|
791
|
+
return runSseRequest({
|
|
792
|
+
url,
|
|
793
|
+
headers,
|
|
794
|
+
payload,
|
|
795
|
+
signal,
|
|
796
|
+
timeoutMs,
|
|
797
|
+
onPhase,
|
|
798
|
+
onNonStream: (data) => {
|
|
735
799
|
const content = normalizeAnthropicMessageContent(data && data.content);
|
|
736
800
|
const text = content
|
|
737
801
|
.filter((item) => item.type === "text")
|
|
@@ -745,270 +809,181 @@ async function runAnthropicTurn({
|
|
|
745
809
|
assistantContent: content,
|
|
746
810
|
toolCalls: extractAnthropicToolCalls(content),
|
|
747
811
|
};
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
while (true) {
|
|
760
|
-
const { done, value } = await reader.read();
|
|
761
|
-
if (done) break;
|
|
762
|
-
|
|
763
|
-
rawBuffer += decoder.decode(value, { stream: true });
|
|
764
|
-
const parsed = parseSseBlocks(rawBuffer);
|
|
765
|
-
rawBuffer = parsed.rest;
|
|
766
|
-
|
|
767
|
-
for (const rawBlock of parsed.blocks) {
|
|
768
|
-
const { event, data } = parseSseEventBlock(rawBlock);
|
|
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
|
-
}
|
|
776
|
-
|
|
777
|
-
const payloadChunk = parseJsonSafe(data, null);
|
|
778
|
-
if (!payloadChunk || typeof payloadChunk !== "object") continue;
|
|
812
|
+
},
|
|
813
|
+
onEvent: ({ event, data }) => {
|
|
814
|
+
const payloadChunk = parseJsonSafe(data, null);
|
|
815
|
+
if (!payloadChunk || typeof payloadChunk !== "object") return;
|
|
816
|
+
|
|
817
|
+
if (event === "error") {
|
|
818
|
+
const errMsg = payloadChunk.error && payloadChunk.error.message
|
|
819
|
+
? String(payloadChunk.error.message)
|
|
820
|
+
: "anthropic stream error";
|
|
821
|
+
throw new Error(errMsg);
|
|
822
|
+
}
|
|
779
823
|
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
824
|
+
if (event === "content_block_start") {
|
|
825
|
+
let index;
|
|
826
|
+
if (Number.isFinite(payloadChunk.index)) {
|
|
827
|
+
index = payloadChunk.index;
|
|
828
|
+
} else {
|
|
829
|
+
// Provider omitted index: each start opens a new block, so give
|
|
830
|
+
// it its own synthetic index instead of collapsing every block
|
|
831
|
+
// into slot 0.
|
|
832
|
+
while (blockMap.has(nextSyntheticBlockIndex)) nextSyntheticBlockIndex += 1;
|
|
833
|
+
index = nextSyntheticBlockIndex;
|
|
834
|
+
nextSyntheticBlockIndex += 1;
|
|
785
835
|
}
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
blockMap.set(index, {
|
|
818
|
-
order: index,
|
|
819
|
-
type: "tool_use",
|
|
820
|
-
id: String(contentBlock.id || ""),
|
|
821
|
-
name: String(contentBlock.name || ""),
|
|
822
|
-
input: contentBlock.input && typeof contentBlock.input === "object" && !Array.isArray(contentBlock.input)
|
|
823
|
-
? { ...contentBlock.input }
|
|
824
|
-
: {},
|
|
825
|
-
inputJson: "",
|
|
826
|
-
});
|
|
827
|
-
const toolName = String(contentBlock.name || "");
|
|
828
|
-
if (toolName) {
|
|
829
|
-
emitPhase(onPhase, { type: "tool_request", name: toolName });
|
|
830
|
-
}
|
|
836
|
+
lastBlockIndex = index;
|
|
837
|
+
const contentBlock = payloadChunk.content_block && typeof payloadChunk.content_block === "object"
|
|
838
|
+
? payloadChunk.content_block
|
|
839
|
+
: {};
|
|
840
|
+
|
|
841
|
+
if (contentBlock.type === "text") {
|
|
842
|
+
blockMap.set(index, {
|
|
843
|
+
order: index,
|
|
844
|
+
type: "text",
|
|
845
|
+
text: String(contentBlock.text || ""),
|
|
846
|
+
});
|
|
847
|
+
} else if (contentBlock.type === "thinking") {
|
|
848
|
+
blockMap.set(index, {
|
|
849
|
+
order: index,
|
|
850
|
+
type: "thinking",
|
|
851
|
+
text: String(contentBlock.thinking || ""),
|
|
852
|
+
});
|
|
853
|
+
} else if (contentBlock.type === "tool_use") {
|
|
854
|
+
blockMap.set(index, {
|
|
855
|
+
order: index,
|
|
856
|
+
type: "tool_use",
|
|
857
|
+
id: String(contentBlock.id || ""),
|
|
858
|
+
name: String(contentBlock.name || ""),
|
|
859
|
+
input: contentBlock.input && typeof contentBlock.input === "object" && !Array.isArray(contentBlock.input)
|
|
860
|
+
? { ...contentBlock.input }
|
|
861
|
+
: {},
|
|
862
|
+
inputJson: "",
|
|
863
|
+
});
|
|
864
|
+
const toolName = String(contentBlock.name || "");
|
|
865
|
+
if (toolName) {
|
|
866
|
+
emitPhase(onPhase, { type: "tool_request", name: toolName });
|
|
831
867
|
}
|
|
832
|
-
continue;
|
|
833
868
|
}
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
834
871
|
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
}
|
|
872
|
+
if (event === "content_block_delta") {
|
|
873
|
+
let index;
|
|
874
|
+
if (Number.isFinite(payloadChunk.index)) {
|
|
875
|
+
index = payloadChunk.index;
|
|
876
|
+
} else if (lastBlockIndex >= 0) {
|
|
877
|
+
// No index: continuation of the most recently started block.
|
|
878
|
+
index = lastBlockIndex;
|
|
879
|
+
} else {
|
|
880
|
+
index = 0;
|
|
881
|
+
}
|
|
882
|
+
const delta = payloadChunk.delta && typeof payloadChunk.delta === "object"
|
|
883
|
+
? payloadChunk.delta
|
|
884
|
+
: {};
|
|
885
|
+
const current = blockMap.get(index) || { order: index, type: "text", text: "" };
|
|
886
|
+
|
|
887
|
+
if (delta.type === "text_delta") {
|
|
888
|
+
const deltaText = String(delta.text || "");
|
|
889
|
+
current.type = "text";
|
|
890
|
+
current.text = `${String(current.text || "")}${deltaText}`;
|
|
891
|
+
blockMap.set(index, current);
|
|
892
|
+
if (deltaText) {
|
|
893
|
+
responseText += deltaText;
|
|
894
|
+
emitPhase(onPhase, { type: "text_delta", text: deltaText });
|
|
895
|
+
if (typeof onTextDelta === "function") {
|
|
896
|
+
onTextDelta(deltaText);
|
|
861
897
|
}
|
|
862
|
-
continue;
|
|
863
898
|
}
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
864
901
|
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
}
|
|
902
|
+
if (delta.type === "thinking_delta") {
|
|
903
|
+
const deltaText = String(delta.thinking || "");
|
|
904
|
+
current.type = "thinking";
|
|
905
|
+
current.text = `${String(current.text || "")}${deltaText}`;
|
|
906
|
+
blockMap.set(index, current);
|
|
907
|
+
if (deltaText) {
|
|
908
|
+
emitPhase(onPhase, { type: "thinking_delta", text: deltaText });
|
|
909
|
+
if (typeof onThinkingDelta === "function") {
|
|
910
|
+
onThinkingDelta(deltaText);
|
|
875
911
|
}
|
|
876
|
-
continue;
|
|
877
912
|
}
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
878
915
|
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
}
|
|
916
|
+
if (delta.type === "input_json_delta") {
|
|
917
|
+
current.type = "tool_use";
|
|
918
|
+
current.inputJson = `${String(current.inputJson || "")}${String(delta.partial_json || "")}`;
|
|
919
|
+
blockMap.set(index, current);
|
|
920
|
+
return;
|
|
885
921
|
}
|
|
886
922
|
}
|
|
923
|
+
},
|
|
924
|
+
buildResult: () => {
|
|
925
|
+
const assistantContent = Array.from(blockMap.values())
|
|
926
|
+
.sort((a, b) => a.order - b.order)
|
|
927
|
+
.filter((item) => item.type !== "thinking")
|
|
928
|
+
.map((item) => {
|
|
929
|
+
if (item.type === "text") {
|
|
930
|
+
return {
|
|
931
|
+
type: "text",
|
|
932
|
+
text: String(item.text || ""),
|
|
933
|
+
};
|
|
934
|
+
}
|
|
887
935
|
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
.filter((item) => item.type !== "thinking")
|
|
894
|
-
.map((item) => {
|
|
895
|
-
if (item.type === "text") {
|
|
936
|
+
const inputFromDelta = normalizeToolCallArgs(item.inputJson || "");
|
|
937
|
+
const mergedInput = {
|
|
938
|
+
...(item.input && typeof item.input === "object" ? item.input : {}),
|
|
939
|
+
...(inputFromDelta && typeof inputFromDelta === "object" ? inputFromDelta : {}),
|
|
940
|
+
};
|
|
896
941
|
return {
|
|
897
|
-
type: "
|
|
898
|
-
|
|
942
|
+
type: "tool_use",
|
|
943
|
+
id: String(item.id || `tool_${randomUUID()}`),
|
|
944
|
+
name: String(item.name || ""),
|
|
945
|
+
input: mergedInput,
|
|
899
946
|
};
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
const inputFromDelta = normalizeToolCallArgs(item.inputJson || "");
|
|
903
|
-
const mergedInput = {
|
|
904
|
-
...(item.input && typeof item.input === "object" ? item.input : {}),
|
|
905
|
-
...(inputFromDelta && typeof inputFromDelta === "object" ? inputFromDelta : {}),
|
|
906
|
-
};
|
|
907
|
-
return {
|
|
908
|
-
type: "tool_use",
|
|
909
|
-
id: String(item.id || `tool_${randomUUID()}`),
|
|
910
|
-
name: String(item.name || ""),
|
|
911
|
-
input: mergedInput,
|
|
912
|
-
};
|
|
913
|
-
});
|
|
947
|
+
});
|
|
914
948
|
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
949
|
+
if (!responseText) {
|
|
950
|
+
responseText = assistantContent
|
|
951
|
+
.filter((item) => item.type === "text")
|
|
952
|
+
.map((item) => item.text)
|
|
953
|
+
.join("");
|
|
954
|
+
}
|
|
921
955
|
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
const timeoutError = new Error(`CLI timeout (${normalizeTimeoutMs(timeoutMs)}ms)`);
|
|
930
|
-
timeoutError.code = "timeout";
|
|
931
|
-
throw timeoutError;
|
|
932
|
-
}
|
|
933
|
-
if (signal && typeof signal === "object" && signal.aborted) {
|
|
934
|
-
const cancelError = new Error("CLI cancelled");
|
|
935
|
-
cancelError.code = "cancelled";
|
|
936
|
-
throw cancelError;
|
|
937
|
-
}
|
|
938
|
-
throw err;
|
|
939
|
-
} finally {
|
|
940
|
-
request.cleanup();
|
|
941
|
-
}
|
|
956
|
+
return {
|
|
957
|
+
text: responseText,
|
|
958
|
+
assistantContent,
|
|
959
|
+
toolCalls: extractAnthropicToolCalls(assistantContent),
|
|
960
|
+
};
|
|
961
|
+
},
|
|
962
|
+
});
|
|
942
963
|
}
|
|
943
964
|
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
if (!requestUrl) {
|
|
967
|
-
throw new Error("ucode baseUrl is not configured");
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
const messages = cloneMessageList(historyMessages);
|
|
971
|
-
const systemText = String(systemPrompt || "").trim();
|
|
972
|
-
const hasSystem = messages.some((entry) => String(entry.role || "").trim() === "system");
|
|
973
|
-
if (systemText && !hasSystem) {
|
|
974
|
-
messages.unshift({ role: "system", content: systemText });
|
|
975
|
-
}
|
|
976
|
-
messages.push({ role: "user", content: String(prompt || "") });
|
|
977
|
-
|
|
978
|
-
let aggregated = "";
|
|
979
|
-
let streamed = false;
|
|
980
|
-
let toolCallsExecuted = 0;
|
|
981
|
-
let toolErrors = 0;
|
|
982
|
-
const toolBudget = resolveNativeToolBudget();
|
|
983
|
-
|
|
984
|
-
while (true) {
|
|
985
|
-
guards.ensureActive();
|
|
986
|
-
|
|
987
|
-
const turnResult = await runOpenAiLikeTurn({
|
|
988
|
-
url: requestUrl,
|
|
989
|
-
apiKey,
|
|
990
|
-
model: requestModel,
|
|
991
|
-
messages,
|
|
992
|
-
signal,
|
|
993
|
-
timeoutMs,
|
|
994
|
-
onPhase,
|
|
995
|
-
onThinkingDelta,
|
|
996
|
-
onTextDelta: (chunk) => {
|
|
997
|
-
const text = String(chunk || "");
|
|
998
|
-
if (!text) return;
|
|
999
|
-
aggregated += text;
|
|
1000
|
-
if (typeof onStreamDelta === "function") {
|
|
1001
|
-
streamed = true;
|
|
1002
|
-
onStreamDelta(text);
|
|
1003
|
-
}
|
|
1004
|
-
},
|
|
1005
|
-
});
|
|
1006
|
-
|
|
1007
|
-
const toolCalls = Array.isArray(turnResult.toolCalls)
|
|
1008
|
-
? turnResult.toolCalls.filter((call) => call && call.function && typeof call.function === "object")
|
|
1009
|
-
: [];
|
|
1010
|
-
|
|
1011
|
-
if (toolCalls.length === 0) {
|
|
965
|
+
// Transport descriptors: everything the shared native loop needs that differs
|
|
966
|
+
// between the OpenAI chat-completions and Anthropic messages protocols —
|
|
967
|
+
// request URL resolution, initial message shaping, turn execution, and
|
|
968
|
+
// assistant/tool-result message formatting.
|
|
969
|
+
const TRANSPORTS = {
|
|
970
|
+
"openai-chat": {
|
|
971
|
+
resolveUrl: resolveCompletionUrl,
|
|
972
|
+
prepareMessages({ messages, systemPrompt, prompt }) {
|
|
973
|
+
const systemText = String(systemPrompt || "").trim();
|
|
974
|
+
const hasSystem = messages.some((entry) => String(entry.role || "").trim() === "system");
|
|
975
|
+
if (systemText && !hasSystem) {
|
|
976
|
+
messages.unshift({ role: "system", content: systemText });
|
|
977
|
+
}
|
|
978
|
+
messages.push({ role: "user", content: String(prompt || "") });
|
|
979
|
+
},
|
|
980
|
+
runTurn: runOpenAiLikeTurn,
|
|
981
|
+
getToolCalls(turnResult) {
|
|
982
|
+
return Array.isArray(turnResult.toolCalls)
|
|
983
|
+
? turnResult.toolCalls.filter((call) => call && call.function && typeof call.function === "object")
|
|
984
|
+
: [];
|
|
985
|
+
},
|
|
986
|
+
appendFinalAssistantMessage({ messages, turnResult }) {
|
|
1012
987
|
const text = String(turnResult.text || "").trim();
|
|
1013
988
|
if (text) {
|
|
1014
989
|
messages.push({
|
|
@@ -1016,78 +991,114 @@ async function runNativeLoopOpenAi({
|
|
|
1016
991
|
content: text,
|
|
1017
992
|
});
|
|
1018
993
|
}
|
|
1019
|
-
|
|
1020
|
-
|
|
994
|
+
},
|
|
995
|
+
prepareToolCalls({ messages, toolCalls }) {
|
|
996
|
+
const assistantToolCalls = [];
|
|
997
|
+
for (const call of toolCalls) {
|
|
998
|
+
const callId = String(call.id || `call_${randomUUID()}`);
|
|
999
|
+
const name = normalizeToolName(call.function.name || "");
|
|
1000
|
+
const args = normalizeToolCallArgs(call.function.arguments || "");
|
|
1001
|
+
|
|
1002
|
+
assistantToolCalls.push({
|
|
1003
|
+
id: callId,
|
|
1004
|
+
type: "function",
|
|
1005
|
+
function: {
|
|
1006
|
+
name: name || String(call.function.name || ""),
|
|
1007
|
+
arguments: toJsonString(args),
|
|
1008
|
+
},
|
|
1009
|
+
});
|
|
1021
1010
|
}
|
|
1022
|
-
return {
|
|
1023
|
-
text: aggregated,
|
|
1024
|
-
streamed,
|
|
1025
|
-
toolCallsExecuted,
|
|
1026
|
-
messages,
|
|
1027
|
-
};
|
|
1028
|
-
}
|
|
1029
1011
|
|
|
1030
|
-
|
|
1031
|
-
for (const call of toolCalls) {
|
|
1032
|
-
const callId = String(call.id || `call_${randomUUID()}`);
|
|
1033
|
-
const name = normalizeToolName(call.function.name || "");
|
|
1034
|
-
const args = normalizeToolCallArgs(call.function.arguments || "");
|
|
1035
|
-
|
|
1036
|
-
assistantToolCalls.push({
|
|
1037
|
-
id: callId,
|
|
1038
|
-
type: "function",
|
|
1039
|
-
function: {
|
|
1040
|
-
name: name || String(call.function.name || ""),
|
|
1041
|
-
arguments: toJsonString(args),
|
|
1042
|
-
},
|
|
1043
|
-
});
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
if (assistantToolCalls.length === 0) {
|
|
1047
|
-
return {
|
|
1048
|
-
text: aggregated,
|
|
1049
|
-
streamed,
|
|
1050
|
-
toolCallsExecuted,
|
|
1051
|
-
messages,
|
|
1052
|
-
};
|
|
1053
|
-
}
|
|
1012
|
+
if (assistantToolCalls.length === 0) return null;
|
|
1054
1013
|
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1014
|
+
messages.push({
|
|
1015
|
+
role: "assistant",
|
|
1016
|
+
content: null,
|
|
1017
|
+
tool_calls: assistantToolCalls,
|
|
1018
|
+
});
|
|
1060
1019
|
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
tool: toolCall.function.name,
|
|
1020
|
+
return assistantToolCalls.map((toolCall) => ({
|
|
1021
|
+
name: toolCall.function.name,
|
|
1064
1022
|
args: normalizeToolCallArgs(toolCall.function.arguments),
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
if (!toolResult || toolResult.ok === false) {
|
|
1070
|
-
toolErrors += 1;
|
|
1071
|
-
}
|
|
1072
|
-
enforceNativeToolBudget({
|
|
1073
|
-
toolCallsExecuted,
|
|
1074
|
-
toolErrors,
|
|
1075
|
-
maxToolCalls: toolBudget.maxToolCalls,
|
|
1076
|
-
maxToolErrors: toolBudget.maxToolErrors,
|
|
1077
|
-
lastTool: toolCall.function.name,
|
|
1078
|
-
lastError: toolResult && toolResult.error ? String(toolResult.error) : "",
|
|
1079
|
-
});
|
|
1023
|
+
source: toolCall,
|
|
1024
|
+
}));
|
|
1025
|
+
},
|
|
1026
|
+
appendToolResult({ messages, call, toolResult }) {
|
|
1080
1027
|
messages.push({
|
|
1081
1028
|
role: "tool",
|
|
1082
|
-
tool_call_id:
|
|
1029
|
+
tool_call_id: call.source.id,
|
|
1083
1030
|
content: clipText(toJsonString(toolResult), 12000),
|
|
1084
1031
|
});
|
|
1085
|
-
}
|
|
1086
|
-
}
|
|
1032
|
+
},
|
|
1033
|
+
},
|
|
1034
|
+
"anthropic-messages": {
|
|
1035
|
+
resolveUrl: resolveAnthropicMessagesUrl,
|
|
1036
|
+
prepareMessages({ messages, prompt }) {
|
|
1037
|
+
messages.push({
|
|
1038
|
+
role: "user",
|
|
1039
|
+
content: String(prompt || ""),
|
|
1040
|
+
});
|
|
1041
|
+
},
|
|
1042
|
+
runTurn: runAnthropicTurn,
|
|
1043
|
+
getToolCalls(turnResult) {
|
|
1044
|
+
return Array.isArray(turnResult.toolCalls) ? turnResult.toolCalls : [];
|
|
1045
|
+
},
|
|
1046
|
+
appendFinalAssistantMessage({ messages, turnResult }) {
|
|
1047
|
+
const assistantContent = Array.isArray(turnResult.assistantContent)
|
|
1048
|
+
? turnResult.assistantContent
|
|
1049
|
+
: [];
|
|
1050
|
+
if (assistantContent.length > 0) {
|
|
1051
|
+
messages.push({
|
|
1052
|
+
role: "assistant",
|
|
1053
|
+
content: assistantContent,
|
|
1054
|
+
});
|
|
1055
|
+
} else if (String(turnResult.text || "").trim()) {
|
|
1056
|
+
messages.push({
|
|
1057
|
+
role: "assistant",
|
|
1058
|
+
content: [
|
|
1059
|
+
{
|
|
1060
|
+
type: "text",
|
|
1061
|
+
text: String(turnResult.text || ""),
|
|
1062
|
+
},
|
|
1063
|
+
],
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
},
|
|
1067
|
+
prepareToolCalls({ messages, turnResult, toolCalls }) {
|
|
1068
|
+
const assistantContent = Array.isArray(turnResult.assistantContent)
|
|
1069
|
+
? turnResult.assistantContent
|
|
1070
|
+
: [];
|
|
1087
1071
|
|
|
1088
|
-
|
|
1072
|
+
messages.push({
|
|
1073
|
+
role: "assistant",
|
|
1074
|
+
content: assistantContent,
|
|
1075
|
+
});
|
|
1089
1076
|
|
|
1090
|
-
|
|
1077
|
+
return toolCalls.map((call) => ({
|
|
1078
|
+
name: call.name,
|
|
1079
|
+
args: call.args,
|
|
1080
|
+
source: call,
|
|
1081
|
+
}));
|
|
1082
|
+
},
|
|
1083
|
+
appendToolResult({ collected, call, toolResult }) {
|
|
1084
|
+
collected.push({
|
|
1085
|
+
type: "tool_result",
|
|
1086
|
+
tool_use_id: String(call.source.id || ""),
|
|
1087
|
+
content: clipText(toJsonString(toolResult), 12000),
|
|
1088
|
+
is_error: Boolean(!toolResult || toolResult.ok === false),
|
|
1089
|
+
});
|
|
1090
|
+
},
|
|
1091
|
+
flushToolResults({ messages, collected }) {
|
|
1092
|
+
messages.push({
|
|
1093
|
+
role: "user",
|
|
1094
|
+
content: collected,
|
|
1095
|
+
});
|
|
1096
|
+
},
|
|
1097
|
+
},
|
|
1098
|
+
};
|
|
1099
|
+
|
|
1100
|
+
async function runNativeLoop({
|
|
1101
|
+
transport,
|
|
1091
1102
|
workspaceRoot = process.cwd(),
|
|
1092
1103
|
prompt = "",
|
|
1093
1104
|
systemPrompt = "",
|
|
@@ -1095,6 +1106,7 @@ async function runNativeLoopAnthropic({
|
|
|
1095
1106
|
model = "",
|
|
1096
1107
|
baseUrl = "",
|
|
1097
1108
|
apiKey = "",
|
|
1109
|
+
provider = "",
|
|
1098
1110
|
timeoutMs = 300000,
|
|
1099
1111
|
onStreamDelta = null,
|
|
1100
1112
|
onThinkingDelta = null,
|
|
@@ -1108,16 +1120,13 @@ async function runNativeLoopAnthropic({
|
|
|
1108
1120
|
throw new Error("ucode model is not configured");
|
|
1109
1121
|
}
|
|
1110
1122
|
|
|
1111
|
-
const requestUrl =
|
|
1123
|
+
const requestUrl = transport.resolveUrl(baseUrl);
|
|
1112
1124
|
if (!requestUrl) {
|
|
1113
1125
|
throw new Error("ucode baseUrl is not configured");
|
|
1114
1126
|
}
|
|
1115
1127
|
|
|
1116
1128
|
const messages = cloneMessageList(historyMessages);
|
|
1117
|
-
|
|
1118
|
-
role: "user",
|
|
1119
|
-
content: String(prompt || ""),
|
|
1120
|
-
});
|
|
1129
|
+
transport.prepareMessages({ messages, systemPrompt, prompt });
|
|
1121
1130
|
|
|
1122
1131
|
let aggregated = "";
|
|
1123
1132
|
let streamed = false;
|
|
@@ -1128,10 +1137,11 @@ async function runNativeLoopAnthropic({
|
|
|
1128
1137
|
while (true) {
|
|
1129
1138
|
guards.ensureActive();
|
|
1130
1139
|
|
|
1131
|
-
const turnResult = await
|
|
1140
|
+
const turnResult = await transport.runTurn({
|
|
1132
1141
|
url: requestUrl,
|
|
1133
1142
|
apiKey,
|
|
1134
1143
|
model: requestModel,
|
|
1144
|
+
provider,
|
|
1135
1145
|
systemPrompt,
|
|
1136
1146
|
messages,
|
|
1137
1147
|
signal,
|
|
@@ -1149,28 +1159,10 @@ async function runNativeLoopAnthropic({
|
|
|
1149
1159
|
},
|
|
1150
1160
|
});
|
|
1151
1161
|
|
|
1152
|
-
const toolCalls =
|
|
1162
|
+
const toolCalls = transport.getToolCalls(turnResult);
|
|
1153
1163
|
|
|
1154
1164
|
if (toolCalls.length === 0) {
|
|
1155
|
-
|
|
1156
|
-
? turnResult.assistantContent
|
|
1157
|
-
: [];
|
|
1158
|
-
if (assistantContent.length > 0) {
|
|
1159
|
-
messages.push({
|
|
1160
|
-
role: "assistant",
|
|
1161
|
-
content: assistantContent,
|
|
1162
|
-
});
|
|
1163
|
-
} else if (String(turnResult.text || "").trim()) {
|
|
1164
|
-
messages.push({
|
|
1165
|
-
role: "assistant",
|
|
1166
|
-
content: [
|
|
1167
|
-
{
|
|
1168
|
-
type: "text",
|
|
1169
|
-
text: String(turnResult.text || ""),
|
|
1170
|
-
},
|
|
1171
|
-
],
|
|
1172
|
-
});
|
|
1173
|
-
}
|
|
1165
|
+
transport.appendFinalAssistantMessage({ messages, turnResult });
|
|
1174
1166
|
const text = String(turnResult.text || "").trim();
|
|
1175
1167
|
if (!aggregated.trim() && text) {
|
|
1176
1168
|
aggregated = text;
|
|
@@ -1183,20 +1175,21 @@ async function runNativeLoopAnthropic({
|
|
|
1183
1175
|
};
|
|
1184
1176
|
}
|
|
1185
1177
|
|
|
1186
|
-
const
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1178
|
+
const pendingCalls = transport.prepareToolCalls({ messages, turnResult, toolCalls });
|
|
1179
|
+
if (!pendingCalls) {
|
|
1180
|
+
return {
|
|
1181
|
+
text: aggregated,
|
|
1182
|
+
streamed,
|
|
1183
|
+
toolCallsExecuted,
|
|
1184
|
+
messages,
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1194
1187
|
|
|
1195
|
-
const
|
|
1196
|
-
for (const
|
|
1188
|
+
const collectedResults = [];
|
|
1189
|
+
for (const pending of pendingCalls) {
|
|
1197
1190
|
const toolResult = runCoreTool({
|
|
1198
|
-
tool:
|
|
1199
|
-
args:
|
|
1191
|
+
tool: pending.name,
|
|
1192
|
+
args: pending.args,
|
|
1200
1193
|
workspaceRoot,
|
|
1201
1194
|
onToolEvent,
|
|
1202
1195
|
});
|
|
@@ -1209,23 +1202,21 @@ async function runNativeLoopAnthropic({
|
|
|
1209
1202
|
toolErrors,
|
|
1210
1203
|
maxToolCalls: toolBudget.maxToolCalls,
|
|
1211
1204
|
maxToolErrors: toolBudget.maxToolErrors,
|
|
1212
|
-
lastTool:
|
|
1205
|
+
lastTool: pending.name,
|
|
1213
1206
|
lastError: toolResult && toolResult.error ? String(toolResult.error) : "",
|
|
1214
1207
|
});
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1208
|
+
transport.appendToolResult({
|
|
1209
|
+
messages,
|
|
1210
|
+
collected: collectedResults,
|
|
1211
|
+
call: pending,
|
|
1212
|
+
toolResult,
|
|
1220
1213
|
});
|
|
1221
1214
|
}
|
|
1222
1215
|
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
});
|
|
1216
|
+
if (typeof transport.flushToolResults === "function") {
|
|
1217
|
+
transport.flushToolResults({ messages, collected: collectedResults });
|
|
1218
|
+
}
|
|
1227
1219
|
}
|
|
1228
|
-
|
|
1229
1220
|
}
|
|
1230
1221
|
|
|
1231
1222
|
async function runNativeAgentTask({
|
|
@@ -1277,11 +1268,27 @@ async function runNativeAgentTask({
|
|
|
1277
1268
|
model,
|
|
1278
1269
|
});
|
|
1279
1270
|
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1271
|
+
// Kimi tokens expire; resolveRuntimeConfig reads the credential file
|
|
1272
|
+
// synchronously, so refresh it here (async) when the key came from that
|
|
1273
|
+
// file and the token is outside the fresh window.
|
|
1274
|
+
if (
|
|
1275
|
+
runtime.provider === "kimi"
|
|
1276
|
+
&& runtime.apiKeySource === "kimi-credential"
|
|
1277
|
+
&& runtime.kimiCredentialState !== "fresh"
|
|
1278
|
+
) {
|
|
1279
|
+
try {
|
|
1280
|
+
const credential = await resolveKimiUpstreamCredentials({ env: process.env });
|
|
1281
|
+
const token = String(credential && credential.accessToken || "").trim();
|
|
1282
|
+
if (token) runtime.apiKey = token;
|
|
1283
|
+
} catch {
|
|
1284
|
+
// Keep the file token; the request itself will surface auth failures.
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
const transport = TRANSPORTS[runtime.transport] || TRANSPORTS["openai-chat"];
|
|
1283
1289
|
|
|
1284
|
-
const runResult = await
|
|
1290
|
+
const runResult = await runNativeLoop({
|
|
1291
|
+
transport,
|
|
1285
1292
|
workspaceRoot,
|
|
1286
1293
|
prompt: promptText,
|
|
1287
1294
|
systemPrompt,
|
|
@@ -1289,6 +1296,7 @@ async function runNativeAgentTask({
|
|
|
1289
1296
|
model: runtime.model,
|
|
1290
1297
|
baseUrl: runtime.baseUrl,
|
|
1291
1298
|
apiKey: runtime.apiKey,
|
|
1299
|
+
provider: runtime.provider,
|
|
1292
1300
|
timeoutMs,
|
|
1293
1301
|
onStreamDelta: trackingStreamDelta,
|
|
1294
1302
|
onThinkingDelta,
|