u-foo 1.0.6 → 1.1.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 +44 -4
- package/SKILLS/ufoo/SKILL.md +17 -2
- package/SKILLS/uinit/SKILL.md +8 -3
- package/bin/ucode-core.js +15 -0
- package/bin/ucode.js +125 -0
- package/bin/ufoo-assistant-agent.js +5 -0
- package/bin/ufoo-engine.js +25 -0
- package/bin/ufoo.js +4 -0
- package/modules/AGENTS.template.md +14 -4
- package/modules/bus/README.md +8 -5
- package/modules/bus/SKILLS/ubus/SKILL.md +5 -4
- package/modules/context/SKILLS/uctx/SKILL.md +3 -1
- package/modules/online/SKILLS/ufoo-online/SKILL.md +144 -0
- package/package.json +12 -3
- package/scripts/import-pi-mono.js +124 -0
- package/scripts/postinstall.js +20 -49
- package/scripts/sync-claude-skills.sh +21 -0
- package/src/agent/cliRunner.js +524 -31
- package/src/agent/internalRunner.js +76 -9
- package/src/agent/launcher.js +97 -45
- package/src/agent/normalizeOutput.js +1 -1
- package/src/agent/notifier.js +144 -4
- package/src/agent/ptyRunner.js +480 -10
- package/src/agent/ptyWrapper.js +28 -3
- package/src/agent/readyDetector.js +16 -0
- package/src/agent/ucode.js +443 -0
- package/src/agent/ucodeBootstrap.js +113 -0
- package/src/agent/ucodeBuild.js +67 -0
- package/src/agent/ucodeDoctor.js +184 -0
- package/src/agent/ucodeRuntimeConfig.js +129 -0
- package/src/agent/ufooAgent.js +11 -2
- package/src/assistant/agent.js +260 -0
- package/src/assistant/bridge.js +172 -0
- package/src/assistant/engine.js +252 -0
- package/src/assistant/stdio.js +58 -0
- package/src/assistant/ufooEngineCli.js +306 -0
- package/src/bus/activate.js +27 -11
- package/src/bus/daemon.js +133 -5
- package/src/bus/index.js +137 -80
- package/src/bus/inject.js +47 -17
- package/src/bus/message.js +145 -17
- package/src/bus/nickname.js +3 -1
- package/src/bus/queue.js +6 -1
- package/src/bus/store.js +189 -0
- package/src/bus/subscriber.js +20 -4
- package/src/bus/utils.js +9 -3
- package/src/chat/agentBar.js +117 -0
- package/src/chat/agentDirectory.js +88 -0
- package/src/chat/agentSockets.js +225 -0
- package/src/chat/agentViewController.js +298 -0
- package/src/chat/chatLogController.js +115 -0
- package/src/chat/commandExecutor.js +700 -0
- package/src/chat/commands.js +132 -0
- package/src/chat/completionController.js +414 -0
- package/src/chat/cronScheduler.js +160 -0
- package/src/chat/daemonConnection.js +166 -0
- package/src/chat/daemonCoordinator.js +64 -0
- package/src/chat/daemonMessageRouter.js +257 -0
- package/src/chat/daemonReconnect.js +41 -0
- package/src/chat/daemonTransport.js +36 -0
- package/src/chat/daemonTransportDefaults.js +10 -0
- package/src/chat/dashboardKeyController.js +480 -0
- package/src/chat/dashboardView.js +154 -0
- package/src/chat/index.js +935 -2909
- package/src/chat/inputHistoryController.js +105 -0
- package/src/chat/inputListenerController.js +304 -0
- package/src/chat/inputMath.js +104 -0
- package/src/chat/inputSubmitHandler.js +171 -0
- package/src/chat/layout.js +165 -0
- package/src/chat/pasteController.js +81 -0
- package/src/chat/rawKeyMap.js +42 -0
- package/src/chat/settingsController.js +132 -0
- package/src/chat/statusLineController.js +177 -0
- package/src/chat/streamTracker.js +138 -0
- package/src/chat/text.js +70 -0
- package/src/chat/transport.js +61 -0
- package/src/cli/busCoreCommands.js +59 -0
- package/src/cli/ctxCoreCommands.js +199 -0
- package/src/cli/onlineCoreCommands.js +379 -0
- package/src/cli.js +741 -238
- package/src/code/README.md +29 -0
- package/src/code/UCODE_PROMPT.md +32 -0
- package/src/code/agent.js +1651 -0
- package/src/code/cli.js +158 -0
- package/src/code/config +0 -0
- package/src/code/dispatch.js +42 -0
- package/src/code/index.js +70 -0
- package/src/code/nativeRunner.js +1213 -0
- package/src/code/runtime.js +154 -0
- package/src/code/sessionStore.js +162 -0
- package/src/code/taskDecomposer.js +269 -0
- package/src/code/tools/bash.js +53 -0
- package/src/code/tools/common.js +42 -0
- package/src/code/tools/edit.js +70 -0
- package/src/code/tools/read.js +44 -0
- package/src/code/tools/write.js +35 -0
- package/src/code/tui.js +1580 -0
- package/src/config.js +47 -1
- package/src/context/decisions.js +12 -2
- package/src/context/index.js +18 -1
- package/src/context/sync.js +127 -0
- package/src/daemon/agentProcessManager.js +74 -0
- package/src/daemon/cronOps.js +241 -0
- package/src/daemon/index.js +661 -488
- package/src/daemon/ipcServer.js +99 -0
- package/src/daemon/ops.js +417 -179
- package/src/daemon/promptLoop.js +319 -0
- package/src/daemon/promptRequest.js +101 -0
- package/src/daemon/providerSessions.js +32 -17
- package/src/daemon/reporting.js +90 -0
- package/src/daemon/run.js +2 -5
- package/src/daemon/status.js +24 -1
- package/src/init/index.js +68 -14
- package/src/online/bridge.js +663 -0
- package/src/online/client.js +245 -0
- package/src/online/runner.js +253 -0
- package/src/online/server.js +992 -0
- package/src/online/tokens.js +103 -0
- package/src/report/store.js +331 -0
- package/src/shared/eventContract.js +35 -0
- package/src/shared/ptySocketContract.js +21 -0
- package/src/status/index.js +50 -17
- package/src/terminal/adapterContract.js +87 -0
- package/src/terminal/adapterRouter.js +84 -0
- package/src/terminal/adapters/externalAdapter.js +14 -0
- package/src/terminal/adapters/internalAdapter.js +13 -0
- package/src/terminal/adapters/internalPtyAdapter.js +42 -0
- package/src/terminal/adapters/internalQueueAdapter.js +37 -0
- package/src/terminal/adapters/terminalAdapter.js +31 -0
- package/src/terminal/adapters/tmuxAdapter.js +30 -0
- package/src/ufoo/agentsStore.js +69 -3
- package/src/utils/banner.js +5 -2
- package/scripts/.archived/bash-to-js-migration/README.md +0 -46
- package/scripts/.archived/bash-to-js-migration/banner.sh +0 -89
- package/scripts/.archived/bash-to-js-migration/bus-alert.sh +0 -6
- package/scripts/.archived/bash-to-js-migration/bus-autotrigger.sh +0 -6
- package/scripts/.archived/bash-to-js-migration/bus-daemon.sh +0 -231
- package/scripts/.archived/bash-to-js-migration/bus-inject.sh +0 -176
- package/scripts/.archived/bash-to-js-migration/bus-listen.sh +0 -6
- package/scripts/.archived/bash-to-js-migration/bus.sh +0 -986
- package/scripts/.archived/bash-to-js-migration/context-decisions.sh +0 -167
- package/scripts/.archived/bash-to-js-migration/context-doctor.sh +0 -72
- package/scripts/.archived/bash-to-js-migration/context-lint.sh +0 -110
- package/scripts/.archived/bash-to-js-migration/doctor.sh +0 -22
- package/scripts/.archived/bash-to-js-migration/init.sh +0 -247
- package/scripts/.archived/bash-to-js-migration/skills.sh +0 -113
- package/scripts/.archived/bash-to-js-migration/status.sh +0 -125
- package/scripts/banner.sh +0 -2
- package/src/bus/API_DESIGN.md +0 -204
|
@@ -0,0 +1,1213 @@
|
|
|
1
|
+
const { randomUUID } = require("crypto");
|
|
2
|
+
const { loadConfig } = require("../config");
|
|
3
|
+
const { runToolCall } = require("./dispatch");
|
|
4
|
+
|
|
5
|
+
const CORE_TOOL_NAMES = new Set(["read", "write", "edit", "bash"]);
|
|
6
|
+
const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
|
|
7
|
+
const DEFAULT_ANTHROPIC_BASE_URL = "https://api.anthropic.com/v1";
|
|
8
|
+
|
|
9
|
+
function nowMs() {
|
|
10
|
+
return Date.now();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function normalizeTimeoutMs(value) {
|
|
14
|
+
const parsed = Number(value);
|
|
15
|
+
if (!Number.isFinite(parsed)) return 300000;
|
|
16
|
+
return Math.max(1000, Math.floor(parsed));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function createGuards({ signal = null, timeoutMs = 300000 } = {}) {
|
|
20
|
+
const startedAt = nowMs();
|
|
21
|
+
const budgetMs = normalizeTimeoutMs(timeoutMs);
|
|
22
|
+
|
|
23
|
+
function ensureActive() {
|
|
24
|
+
if (signal && typeof signal === "object" && signal.aborted) {
|
|
25
|
+
const err = new Error("CLI cancelled");
|
|
26
|
+
err.code = "cancelled";
|
|
27
|
+
throw err;
|
|
28
|
+
}
|
|
29
|
+
if (nowMs() - startedAt > budgetMs) {
|
|
30
|
+
const err = new Error(`CLI timeout (${budgetMs}ms)`);
|
|
31
|
+
err.code = "timeout";
|
|
32
|
+
throw err;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
ensureActive,
|
|
38
|
+
budgetMs,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function emitToolEvent(callback, event = {}) {
|
|
43
|
+
if (typeof callback !== "function") return;
|
|
44
|
+
try {
|
|
45
|
+
callback(event);
|
|
46
|
+
} catch {
|
|
47
|
+
// ignore callback failures
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function clipText(value = "", maxChars = 6000) {
|
|
52
|
+
const text = String(value || "");
|
|
53
|
+
if (text.length <= maxChars) return text;
|
|
54
|
+
return `${text.slice(0, maxChars)}\n...[truncated]`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function summarizeFileSnippet(file = "", content = "") {
|
|
58
|
+
const target = String(file || "").trim();
|
|
59
|
+
const body = String(content || "").trim();
|
|
60
|
+
if (!body) return `${target}: empty`;
|
|
61
|
+
|
|
62
|
+
if (target.toLowerCase().endsWith("package.json")) {
|
|
63
|
+
try {
|
|
64
|
+
const parsed = JSON.parse(body);
|
|
65
|
+
const name = String(parsed.name || "").trim() || "(unknown)";
|
|
66
|
+
const version = String(parsed.version || "").trim() || "(unknown)";
|
|
67
|
+
const scripts = parsed.scripts && typeof parsed.scripts === "object"
|
|
68
|
+
? Object.keys(parsed.scripts).slice(0, 4)
|
|
69
|
+
: [];
|
|
70
|
+
const scriptText = scripts.length > 0 ? ` scripts=${scripts.join(",")}` : "";
|
|
71
|
+
return `${target}: name=${name} version=${version}${scriptText}`;
|
|
72
|
+
} catch {
|
|
73
|
+
// fall through
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const lines = body
|
|
78
|
+
.split(/\r?\n/)
|
|
79
|
+
.map((line) => line.trim())
|
|
80
|
+
.filter(Boolean)
|
|
81
|
+
.slice(0, 3)
|
|
82
|
+
.map((line) => (line.length > 120 ? `${line.slice(0, 120)}...` : line));
|
|
83
|
+
|
|
84
|
+
if (lines.length === 0) return `${target}: empty`;
|
|
85
|
+
return `${target}: ${lines.join(" | ")}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function extractPreflightEvidence(systemPrompt = "") {
|
|
89
|
+
const source = String(systemPrompt || "");
|
|
90
|
+
if (!source) return [];
|
|
91
|
+
|
|
92
|
+
const results = [];
|
|
93
|
+
const fileRegex = /File:\s*([^\n]+)\n([\s\S]*?)(?=\n---\n(?:File|Command):|$)/g;
|
|
94
|
+
let match = fileRegex.exec(source);
|
|
95
|
+
while (match) {
|
|
96
|
+
const file = String(match[1] || "").trim();
|
|
97
|
+
const content = String(match[2] || "").trim();
|
|
98
|
+
if (file) {
|
|
99
|
+
results.push({ kind: "file", label: file, summary: summarizeFileSnippet(file, content) });
|
|
100
|
+
}
|
|
101
|
+
match = fileRegex.exec(source);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const cmdRegex = /Command:\s*([^\n]+)\n([\s\S]*?)(?=\n---\n(?:File|Command):|$)/g;
|
|
105
|
+
match = cmdRegex.exec(source);
|
|
106
|
+
while (match) {
|
|
107
|
+
const command = String(match[1] || "").trim();
|
|
108
|
+
const output = String(match[2] || "").trim();
|
|
109
|
+
if (command) {
|
|
110
|
+
const clipped = clipText(output, 300).replace(/\s+/g, " ").trim();
|
|
111
|
+
results.push({ kind: "command", label: command, summary: `${command}: ${clipped || "(no output)"}` });
|
|
112
|
+
}
|
|
113
|
+
match = cmdRegex.exec(source);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return results;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function isAnalysisPrompt(text = "") {
|
|
120
|
+
return /(?:analy[sz]e|analysis|review|audit|status|architecture|codebase|repo|project|现状|架构|审查|分析|项目|代码库)/i.test(text);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function parseReadIntent(prompt = "") {
|
|
124
|
+
const text = String(prompt || "");
|
|
125
|
+
const patterns = [
|
|
126
|
+
/(?:\bread\b|\bcat\b|查看|读取)\s+([A-Za-z0-9_./\\-]+(?:\.[A-Za-z0-9._-]+)?)/i,
|
|
127
|
+
/([A-Za-z0-9_./\\-]+\.(?:md|txt|json|js|ts|jsx|tsx|yml|yaml|toml|sh))/i,
|
|
128
|
+
];
|
|
129
|
+
for (const re of patterns) {
|
|
130
|
+
const match = text.match(re);
|
|
131
|
+
if (!match || !match[1]) continue;
|
|
132
|
+
const candidate = String(match[1]).trim().replace(/[),.;:]+$/, "");
|
|
133
|
+
if (!candidate) continue;
|
|
134
|
+
if (candidate.length > 260) continue;
|
|
135
|
+
return candidate;
|
|
136
|
+
}
|
|
137
|
+
return "";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function parseBashIntent(prompt = "") {
|
|
141
|
+
const text = String(prompt || "").trim();
|
|
142
|
+
if (!text) return "";
|
|
143
|
+
if (
|
|
144
|
+
/\b(ls|dir|tree)\b/i.test(text)
|
|
145
|
+
|| /\b(list|show)\s+(files|dirs|directories|folders)\b/i.test(text)
|
|
146
|
+
|| /列出|目录|文件列表/.test(text)
|
|
147
|
+
) {
|
|
148
|
+
return "ls -la";
|
|
149
|
+
}
|
|
150
|
+
const cmdMatch = text.match(/(?:运行|执行|run|exec(?:ute)?)\s+`([^`]+)`/i);
|
|
151
|
+
if (cmdMatch && cmdMatch[1]) return String(cmdMatch[1]).trim();
|
|
152
|
+
return "";
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function normalizeProvider(value = "") {
|
|
156
|
+
const text = String(value || "").trim().toLowerCase();
|
|
157
|
+
if (!text) return "";
|
|
158
|
+
if (text === "codex" || text === "codex-cli" || text === "codex-code") return "openai";
|
|
159
|
+
if (text === "claude" || text === "claude-cli" || text === "claude-code") return "anthropic";
|
|
160
|
+
if (text === "openai" || text === "anthropic") return text;
|
|
161
|
+
return text;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function resolveTransport({ provider = "", baseUrl = "" } = {}) {
|
|
165
|
+
const normalizedProvider = normalizeProvider(provider);
|
|
166
|
+
const url = String(baseUrl || "").trim().toLowerCase();
|
|
167
|
+
|
|
168
|
+
if (normalizedProvider === "anthropic") return "anthropic-messages";
|
|
169
|
+
if (url.includes("anthropic.com")) return "anthropic-messages";
|
|
170
|
+
if (/\/messages(?:$|[/?#])/.test(url) && !/\/chat\/completions(?:$|[/?#])/.test(url)) {
|
|
171
|
+
return "anthropic-messages";
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return "openai-chat";
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function resolveRuntimeConfig({ workspaceRoot = process.cwd(), provider = "", model = "" } = {}) {
|
|
178
|
+
const config = loadConfig(workspaceRoot);
|
|
179
|
+
const configuredProvider = normalizeProvider(config.ucodeProvider || config.agentProvider || "");
|
|
180
|
+
const selectedProvider = normalizeProvider(
|
|
181
|
+
provider
|
|
182
|
+
|| process.env.UFOO_UCODE_PROVIDER
|
|
183
|
+
|| configuredProvider
|
|
184
|
+
|| "openai"
|
|
185
|
+
) || "openai";
|
|
186
|
+
|
|
187
|
+
const selectedModel = String(
|
|
188
|
+
model
|
|
189
|
+
|| process.env.UFOO_UCODE_MODEL
|
|
190
|
+
|| config.ucodeModel
|
|
191
|
+
|| config.agentModel
|
|
192
|
+
|| ""
|
|
193
|
+
).trim();
|
|
194
|
+
|
|
195
|
+
const defaultBaseUrl = selectedProvider === "anthropic"
|
|
196
|
+
? String(process.env.ANTHROPIC_BASE_URL || DEFAULT_ANTHROPIC_BASE_URL)
|
|
197
|
+
: String(process.env.OPENAI_BASE_URL || DEFAULT_OPENAI_BASE_URL);
|
|
198
|
+
|
|
199
|
+
const baseUrl = String(
|
|
200
|
+
process.env.UFOO_UCODE_BASE_URL
|
|
201
|
+
|| config.ucodeBaseUrl
|
|
202
|
+
|| defaultBaseUrl
|
|
203
|
+
).trim();
|
|
204
|
+
|
|
205
|
+
const apiKey = String(
|
|
206
|
+
process.env.UFOO_UCODE_API_KEY
|
|
207
|
+
|| config.ucodeApiKey
|
|
208
|
+
|| (selectedProvider === "openai" ? process.env.OPENAI_API_KEY : "")
|
|
209
|
+
|| (selectedProvider === "anthropic" ? process.env.ANTHROPIC_API_KEY : "")
|
|
210
|
+
|| ""
|
|
211
|
+
).trim();
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
provider: selectedProvider,
|
|
215
|
+
model: selectedModel,
|
|
216
|
+
baseUrl,
|
|
217
|
+
apiKey,
|
|
218
|
+
transport: resolveTransport({ provider: selectedProvider, baseUrl }),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function resolveCompletionUrl(baseUrl = "") {
|
|
223
|
+
const raw = String(baseUrl || "").trim();
|
|
224
|
+
if (!raw) return "";
|
|
225
|
+
const normalized = raw.replace(/\/+$/, "");
|
|
226
|
+
if (/\/chat\/completions$/i.test(normalized)) return normalized;
|
|
227
|
+
if (/\/v1$/i.test(normalized)) return `${normalized}/chat/completions`;
|
|
228
|
+
if (/\/api$/i.test(normalized)) return `${normalized}/v1/chat/completions`;
|
|
229
|
+
return `${normalized}/chat/completions`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function resolveAnthropicMessagesUrl(baseUrl = "") {
|
|
233
|
+
const raw = String(baseUrl || "").trim() || DEFAULT_ANTHROPIC_BASE_URL;
|
|
234
|
+
const normalized = raw.replace(/\/+$/, "");
|
|
235
|
+
if (/\/messages$/i.test(normalized)) return normalized;
|
|
236
|
+
if (/\/v1$/i.test(normalized)) return `${normalized}/messages`;
|
|
237
|
+
if (/\/api$/i.test(normalized)) return `${normalized}/v1/messages`;
|
|
238
|
+
return `${normalized}/messages`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function buildCoreToolSpecs() {
|
|
242
|
+
return [
|
|
243
|
+
{
|
|
244
|
+
type: "function",
|
|
245
|
+
function: {
|
|
246
|
+
name: "read",
|
|
247
|
+
description: "Read a text file from workspace.",
|
|
248
|
+
parameters: {
|
|
249
|
+
type: "object",
|
|
250
|
+
properties: {
|
|
251
|
+
path: { type: "string" },
|
|
252
|
+
startLine: { type: "integer" },
|
|
253
|
+
endLine: { type: "integer" },
|
|
254
|
+
maxBytes: { type: "integer" },
|
|
255
|
+
},
|
|
256
|
+
required: ["path"],
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
type: "function",
|
|
262
|
+
function: {
|
|
263
|
+
name: "write",
|
|
264
|
+
description: "Write content to a file in workspace.",
|
|
265
|
+
parameters: {
|
|
266
|
+
type: "object",
|
|
267
|
+
properties: {
|
|
268
|
+
path: { type: "string" },
|
|
269
|
+
content: { type: "string" },
|
|
270
|
+
append: { type: "boolean" },
|
|
271
|
+
},
|
|
272
|
+
required: ["path", "content"],
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
type: "function",
|
|
278
|
+
function: {
|
|
279
|
+
name: "edit",
|
|
280
|
+
description: "Replace text in a file in workspace.",
|
|
281
|
+
parameters: {
|
|
282
|
+
type: "object",
|
|
283
|
+
properties: {
|
|
284
|
+
path: { type: "string" },
|
|
285
|
+
find: { type: "string" },
|
|
286
|
+
replace: { type: "string" },
|
|
287
|
+
all: { type: "boolean" },
|
|
288
|
+
},
|
|
289
|
+
required: ["path", "find", "replace"],
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
type: "function",
|
|
295
|
+
function: {
|
|
296
|
+
name: "bash",
|
|
297
|
+
description: "Run one shell command in workspace.",
|
|
298
|
+
parameters: {
|
|
299
|
+
type: "object",
|
|
300
|
+
properties: {
|
|
301
|
+
command: { type: "string" },
|
|
302
|
+
timeoutMs: { type: "integer" },
|
|
303
|
+
},
|
|
304
|
+
required: ["command"],
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
];
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function buildAnthropicToolSpecs() {
|
|
312
|
+
return buildCoreToolSpecs().map((spec) => ({
|
|
313
|
+
name: spec.function.name,
|
|
314
|
+
description: spec.function.description,
|
|
315
|
+
input_schema: spec.function.parameters,
|
|
316
|
+
}));
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function createRequestController({ signal = null, timeoutMs = 300000 } = {}) {
|
|
320
|
+
const controller = new AbortController();
|
|
321
|
+
let timedOut = false;
|
|
322
|
+
|
|
323
|
+
const timer = setTimeout(() => {
|
|
324
|
+
timedOut = true;
|
|
325
|
+
try {
|
|
326
|
+
controller.abort();
|
|
327
|
+
} catch {
|
|
328
|
+
// ignore
|
|
329
|
+
}
|
|
330
|
+
}, normalizeTimeoutMs(timeoutMs));
|
|
331
|
+
|
|
332
|
+
let abortHandler = null;
|
|
333
|
+
if (signal && typeof signal === "object") {
|
|
334
|
+
abortHandler = () => {
|
|
335
|
+
try {
|
|
336
|
+
controller.abort();
|
|
337
|
+
} catch {
|
|
338
|
+
// ignore
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
if (signal.aborted) {
|
|
342
|
+
abortHandler();
|
|
343
|
+
} else if (typeof signal.addEventListener === "function") {
|
|
344
|
+
signal.addEventListener("abort", abortHandler, { once: true });
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
signal: controller.signal,
|
|
350
|
+
timedOut: () => timedOut,
|
|
351
|
+
cleanup: () => {
|
|
352
|
+
clearTimeout(timer);
|
|
353
|
+
if (signal && abortHandler && typeof signal.removeEventListener === "function") {
|
|
354
|
+
signal.removeEventListener("abort", abortHandler);
|
|
355
|
+
}
|
|
356
|
+
},
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function parseJsonSafe(value = "", fallback = null) {
|
|
361
|
+
try {
|
|
362
|
+
return JSON.parse(String(value || ""));
|
|
363
|
+
} catch {
|
|
364
|
+
return fallback;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function cloneMessageList(value = []) {
|
|
369
|
+
const parsed = parseJsonSafe(toJsonString(value), []);
|
|
370
|
+
if (!Array.isArray(parsed)) return [];
|
|
371
|
+
return parsed.filter((entry) => entry && typeof entry === "object" && !Array.isArray(entry));
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function normalizeToolName(value = "") {
|
|
375
|
+
const name = String(value || "").trim().toLowerCase();
|
|
376
|
+
if (!CORE_TOOL_NAMES.has(name)) return "";
|
|
377
|
+
return name;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function toJsonString(value) {
|
|
381
|
+
try {
|
|
382
|
+
return JSON.stringify(value);
|
|
383
|
+
} catch {
|
|
384
|
+
return String(value || "");
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function parseSseBlocks(text = "") {
|
|
389
|
+
const source = String(text || "");
|
|
390
|
+
const blocks = source.split(/\r?\n\r?\n/);
|
|
391
|
+
if (blocks.length <= 1) {
|
|
392
|
+
return { blocks: [], rest: source };
|
|
393
|
+
}
|
|
394
|
+
const rest = blocks.pop() || "";
|
|
395
|
+
return { blocks, rest };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function parseSseEventBlock(block = "") {
|
|
399
|
+
const lines = String(block || "").split(/\r?\n/);
|
|
400
|
+
let event = "message";
|
|
401
|
+
const data = [];
|
|
402
|
+
|
|
403
|
+
for (const line of lines) {
|
|
404
|
+
if (!line) continue;
|
|
405
|
+
if (line.startsWith("event:")) {
|
|
406
|
+
event = line.slice(6).trim() || "message";
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
if (line.startsWith("data:")) {
|
|
410
|
+
data.push(line.slice(5).trimStart());
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return {
|
|
415
|
+
event,
|
|
416
|
+
data: data.join("\n"),
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function parseSseDataBlock(block = "") {
|
|
421
|
+
return parseSseEventBlock(block).data;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function normalizeToolCallArgs(raw = "") {
|
|
425
|
+
const text = String(raw || "").trim();
|
|
426
|
+
if (!text) return {};
|
|
427
|
+
const parsed = parseJsonSafe(text, null);
|
|
428
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
429
|
+
return parsed;
|
|
430
|
+
}
|
|
431
|
+
return {};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function runCoreTool({ tool = "", args = {}, workspaceRoot = process.cwd(), onToolEvent = null } = {}) {
|
|
435
|
+
const normalizedTool = normalizeToolName(tool);
|
|
436
|
+
if (!normalizedTool) {
|
|
437
|
+
emitToolEvent(onToolEvent, {
|
|
438
|
+
tool: String(tool || "unknown"),
|
|
439
|
+
phase: "error",
|
|
440
|
+
args: args && typeof args === "object" ? { ...args } : {},
|
|
441
|
+
error: `unsupported tool: ${tool}`,
|
|
442
|
+
});
|
|
443
|
+
return {
|
|
444
|
+
ok: false,
|
|
445
|
+
error: `unsupported tool: ${tool}`,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const safeArgs = args && typeof args === "object" ? { ...args } : {};
|
|
450
|
+
emitToolEvent(onToolEvent, {
|
|
451
|
+
tool: normalizedTool,
|
|
452
|
+
phase: "start",
|
|
453
|
+
args: safeArgs,
|
|
454
|
+
error: "",
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
const result = runToolCall(
|
|
458
|
+
{ tool: normalizedTool, args: safeArgs },
|
|
459
|
+
{ workspaceRoot, cwd: workspaceRoot }
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
if (!result || result.ok === false) {
|
|
463
|
+
emitToolEvent(onToolEvent, {
|
|
464
|
+
tool: normalizedTool,
|
|
465
|
+
phase: "error",
|
|
466
|
+
args: safeArgs,
|
|
467
|
+
error: String((result && result.error) || `${normalizedTool} failed`),
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return result;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
async function runOpenAiLikeTurn({
|
|
475
|
+
url = "",
|
|
476
|
+
apiKey = "",
|
|
477
|
+
model = "",
|
|
478
|
+
messages = [],
|
|
479
|
+
onTextDelta = null,
|
|
480
|
+
signal = null,
|
|
481
|
+
timeoutMs = 300000,
|
|
482
|
+
} = {}) {
|
|
483
|
+
const payload = {
|
|
484
|
+
model,
|
|
485
|
+
messages,
|
|
486
|
+
tools: buildCoreToolSpecs(),
|
|
487
|
+
tool_choice: "auto",
|
|
488
|
+
stream: true,
|
|
489
|
+
temperature: 0,
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
const headers = {
|
|
493
|
+
"content-type": "application/json",
|
|
494
|
+
};
|
|
495
|
+
if (apiKey) {
|
|
496
|
+
headers.authorization = `Bearer ${apiKey}`;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const request = createRequestController({ signal, timeoutMs });
|
|
500
|
+
|
|
501
|
+
try {
|
|
502
|
+
const response = await fetch(url, {
|
|
503
|
+
method: "POST",
|
|
504
|
+
headers,
|
|
505
|
+
body: JSON.stringify(payload),
|
|
506
|
+
signal: request.signal,
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
if (!response.ok) {
|
|
510
|
+
const body = await response.text().catch(() => "");
|
|
511
|
+
throw new Error(`provider request failed (${response.status}): ${clipText(body, 500)}`);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (!response.body || typeof response.body.getReader !== "function") {
|
|
515
|
+
const data = await response.json();
|
|
516
|
+
const message = data && data.choices && data.choices[0] && data.choices[0].message
|
|
517
|
+
? data.choices[0].message
|
|
518
|
+
: {};
|
|
519
|
+
const text = typeof message.content === "string" ? message.content : "";
|
|
520
|
+
const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
|
|
521
|
+
if (text && typeof onTextDelta === "function") {
|
|
522
|
+
onTextDelta(text);
|
|
523
|
+
}
|
|
524
|
+
return {
|
|
525
|
+
text,
|
|
526
|
+
toolCalls,
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const reader = response.body.getReader();
|
|
531
|
+
const decoder = new TextDecoder();
|
|
532
|
+
const toolCallMap = new Map();
|
|
533
|
+
let rawBuffer = "";
|
|
534
|
+
let responseText = "";
|
|
535
|
+
|
|
536
|
+
while (true) {
|
|
537
|
+
const { done, value } = await reader.read();
|
|
538
|
+
if (done) break;
|
|
539
|
+
|
|
540
|
+
rawBuffer += decoder.decode(value, { stream: true });
|
|
541
|
+
const parsed = parseSseBlocks(rawBuffer);
|
|
542
|
+
rawBuffer = parsed.rest;
|
|
543
|
+
|
|
544
|
+
for (const block of parsed.blocks) {
|
|
545
|
+
const payloadText = parseSseDataBlock(block);
|
|
546
|
+
if (!payloadText) continue;
|
|
547
|
+
if (payloadText === "[DONE]") {
|
|
548
|
+
rawBuffer = "";
|
|
549
|
+
break;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const chunk = parseJsonSafe(payloadText, null);
|
|
553
|
+
if (!chunk || typeof chunk !== "object") continue;
|
|
554
|
+
|
|
555
|
+
const choice = chunk.choices && chunk.choices[0] ? chunk.choices[0] : null;
|
|
556
|
+
if (!choice || typeof choice !== "object") continue;
|
|
557
|
+
|
|
558
|
+
const delta = choice.delta && typeof choice.delta === "object" ? choice.delta : {};
|
|
559
|
+
|
|
560
|
+
if (typeof delta.content === "string" && delta.content) {
|
|
561
|
+
responseText += delta.content;
|
|
562
|
+
if (typeof onTextDelta === "function") {
|
|
563
|
+
onTextDelta(delta.content);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (Array.isArray(delta.tool_calls)) {
|
|
568
|
+
for (const callPart of delta.tool_calls) {
|
|
569
|
+
const index = Number.isFinite(callPart.index) ? callPart.index : 0;
|
|
570
|
+
const previous = toolCallMap.get(index) || {
|
|
571
|
+
id: "",
|
|
572
|
+
type: "function",
|
|
573
|
+
function: {
|
|
574
|
+
name: "",
|
|
575
|
+
arguments: "",
|
|
576
|
+
},
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
if (typeof callPart.id === "string" && callPart.id) previous.id = callPart.id;
|
|
580
|
+
if (callPart.function && typeof callPart.function === "object") {
|
|
581
|
+
if (typeof callPart.function.name === "string" && callPart.function.name) {
|
|
582
|
+
previous.function.name = callPart.function.name;
|
|
583
|
+
}
|
|
584
|
+
if (typeof callPart.function.arguments === "string" && callPart.function.arguments) {
|
|
585
|
+
previous.function.arguments += callPart.function.arguments;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
toolCallMap.set(index, previous);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
if (rawBuffer.trim()) {
|
|
596
|
+
const fallbackBlock = parseSseDataBlock(rawBuffer);
|
|
597
|
+
if (fallbackBlock && fallbackBlock !== "[DONE]") {
|
|
598
|
+
const chunk = parseJsonSafe(fallbackBlock, null);
|
|
599
|
+
const choice = chunk && chunk.choices && chunk.choices[0] ? chunk.choices[0] : null;
|
|
600
|
+
if (choice && choice.delta && typeof choice.delta.content === "string" && choice.delta.content) {
|
|
601
|
+
responseText += choice.delta.content;
|
|
602
|
+
if (typeof onTextDelta === "function") {
|
|
603
|
+
onTextDelta(choice.delta.content);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
return {
|
|
610
|
+
text: responseText,
|
|
611
|
+
toolCalls: Array.from(toolCallMap.entries())
|
|
612
|
+
.sort((a, b) => a[0] - b[0])
|
|
613
|
+
.map((entry) => entry[1]),
|
|
614
|
+
};
|
|
615
|
+
} catch (err) {
|
|
616
|
+
if (request.timedOut()) {
|
|
617
|
+
const timeoutError = new Error(`CLI timeout (${normalizeTimeoutMs(timeoutMs)}ms)`);
|
|
618
|
+
timeoutError.code = "timeout";
|
|
619
|
+
throw timeoutError;
|
|
620
|
+
}
|
|
621
|
+
if (signal && typeof signal === "object" && signal.aborted) {
|
|
622
|
+
const cancelError = new Error("CLI cancelled");
|
|
623
|
+
cancelError.code = "cancelled";
|
|
624
|
+
throw cancelError;
|
|
625
|
+
}
|
|
626
|
+
throw err;
|
|
627
|
+
} finally {
|
|
628
|
+
request.cleanup();
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function normalizeAnthropicMessageContent(raw = []) {
|
|
633
|
+
if (!Array.isArray(raw)) return [];
|
|
634
|
+
return raw
|
|
635
|
+
.map((item) => {
|
|
636
|
+
if (!item || typeof item !== "object") return null;
|
|
637
|
+
if (item.type === "text") {
|
|
638
|
+
return {
|
|
639
|
+
type: "text",
|
|
640
|
+
text: String(item.text || ""),
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
if (item.type === "tool_use") {
|
|
644
|
+
return {
|
|
645
|
+
type: "tool_use",
|
|
646
|
+
id: String(item.id || ""),
|
|
647
|
+
name: String(item.name || ""),
|
|
648
|
+
input: item.input && typeof item.input === "object" && !Array.isArray(item.input)
|
|
649
|
+
? item.input
|
|
650
|
+
: {},
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
return null;
|
|
654
|
+
})
|
|
655
|
+
.filter(Boolean);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function extractAnthropicToolCalls(content = []) {
|
|
659
|
+
return normalizeAnthropicMessageContent(content)
|
|
660
|
+
.filter((item) => item.type === "tool_use")
|
|
661
|
+
.map((item) => ({
|
|
662
|
+
id: String(item.id || `tool_${randomUUID()}`),
|
|
663
|
+
name: String(item.name || ""),
|
|
664
|
+
args: item.input && typeof item.input === "object" && !Array.isArray(item.input)
|
|
665
|
+
? item.input
|
|
666
|
+
: {},
|
|
667
|
+
}));
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
async function runAnthropicTurn({
|
|
671
|
+
url = "",
|
|
672
|
+
apiKey = "",
|
|
673
|
+
model = "",
|
|
674
|
+
systemPrompt = "",
|
|
675
|
+
messages = [],
|
|
676
|
+
onTextDelta = null,
|
|
677
|
+
signal = null,
|
|
678
|
+
timeoutMs = 300000,
|
|
679
|
+
} = {}) {
|
|
680
|
+
const payload = {
|
|
681
|
+
model,
|
|
682
|
+
max_tokens: 4096,
|
|
683
|
+
messages,
|
|
684
|
+
tools: buildAnthropicToolSpecs(),
|
|
685
|
+
stream: true,
|
|
686
|
+
};
|
|
687
|
+
const systemText = String(systemPrompt || "").trim();
|
|
688
|
+
if (systemText) {
|
|
689
|
+
payload.system = systemText;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const headers = {
|
|
693
|
+
"content-type": "application/json",
|
|
694
|
+
"anthropic-version": "2023-06-01",
|
|
695
|
+
};
|
|
696
|
+
if (apiKey) {
|
|
697
|
+
headers["x-api-key"] = apiKey;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
const request = createRequestController({ signal, timeoutMs });
|
|
701
|
+
|
|
702
|
+
try {
|
|
703
|
+
const response = await fetch(url, {
|
|
704
|
+
method: "POST",
|
|
705
|
+
headers,
|
|
706
|
+
body: JSON.stringify(payload),
|
|
707
|
+
signal: request.signal,
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
if (!response.ok) {
|
|
711
|
+
const body = await response.text().catch(() => "");
|
|
712
|
+
throw new Error(`provider request failed (${response.status}): ${clipText(body, 500)}`);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
if (!response.body || typeof response.body.getReader !== "function") {
|
|
716
|
+
const data = await response.json();
|
|
717
|
+
const content = normalizeAnthropicMessageContent(data && data.content);
|
|
718
|
+
const text = content
|
|
719
|
+
.filter((item) => item.type === "text")
|
|
720
|
+
.map((item) => item.text)
|
|
721
|
+
.join("");
|
|
722
|
+
if (text && typeof onTextDelta === "function") {
|
|
723
|
+
onTextDelta(text);
|
|
724
|
+
}
|
|
725
|
+
return {
|
|
726
|
+
text,
|
|
727
|
+
assistantContent: content,
|
|
728
|
+
toolCalls: extractAnthropicToolCalls(content),
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
const reader = response.body.getReader();
|
|
733
|
+
const decoder = new TextDecoder();
|
|
734
|
+
const blockMap = new Map();
|
|
735
|
+
let rawBuffer = "";
|
|
736
|
+
let responseText = "";
|
|
737
|
+
|
|
738
|
+
while (true) {
|
|
739
|
+
const { done, value } = await reader.read();
|
|
740
|
+
if (done) break;
|
|
741
|
+
|
|
742
|
+
rawBuffer += decoder.decode(value, { stream: true });
|
|
743
|
+
const parsed = parseSseBlocks(rawBuffer);
|
|
744
|
+
rawBuffer = parsed.rest;
|
|
745
|
+
|
|
746
|
+
for (const rawBlock of parsed.blocks) {
|
|
747
|
+
const { event, data } = parseSseEventBlock(rawBlock);
|
|
748
|
+
if (!data || data === "[DONE]") continue;
|
|
749
|
+
|
|
750
|
+
const payloadChunk = parseJsonSafe(data, null);
|
|
751
|
+
if (!payloadChunk || typeof payloadChunk !== "object") continue;
|
|
752
|
+
|
|
753
|
+
if (event === "error") {
|
|
754
|
+
const errMsg = payloadChunk.error && payloadChunk.error.message
|
|
755
|
+
? String(payloadChunk.error.message)
|
|
756
|
+
: "anthropic stream error";
|
|
757
|
+
throw new Error(errMsg);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
if (event === "content_block_start") {
|
|
761
|
+
const index = Number.isFinite(payloadChunk.index) ? payloadChunk.index : 0;
|
|
762
|
+
const contentBlock = payloadChunk.content_block && typeof payloadChunk.content_block === "object"
|
|
763
|
+
? payloadChunk.content_block
|
|
764
|
+
: {};
|
|
765
|
+
|
|
766
|
+
if (contentBlock.type === "text") {
|
|
767
|
+
blockMap.set(index, {
|
|
768
|
+
order: index,
|
|
769
|
+
type: "text",
|
|
770
|
+
text: String(contentBlock.text || ""),
|
|
771
|
+
});
|
|
772
|
+
} else if (contentBlock.type === "tool_use") {
|
|
773
|
+
blockMap.set(index, {
|
|
774
|
+
order: index,
|
|
775
|
+
type: "tool_use",
|
|
776
|
+
id: String(contentBlock.id || ""),
|
|
777
|
+
name: String(contentBlock.name || ""),
|
|
778
|
+
input: contentBlock.input && typeof contentBlock.input === "object" && !Array.isArray(contentBlock.input)
|
|
779
|
+
? { ...contentBlock.input }
|
|
780
|
+
: {},
|
|
781
|
+
inputJson: "",
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
if (event === "content_block_delta") {
|
|
788
|
+
const index = Number.isFinite(payloadChunk.index) ? payloadChunk.index : 0;
|
|
789
|
+
const delta = payloadChunk.delta && typeof payloadChunk.delta === "object"
|
|
790
|
+
? payloadChunk.delta
|
|
791
|
+
: {};
|
|
792
|
+
const current = blockMap.get(index) || { order: index, type: "text", text: "" };
|
|
793
|
+
|
|
794
|
+
if (delta.type === "text_delta") {
|
|
795
|
+
const deltaText = String(delta.text || "");
|
|
796
|
+
current.type = "text";
|
|
797
|
+
current.text = `${String(current.text || "")}${deltaText}`;
|
|
798
|
+
blockMap.set(index, current);
|
|
799
|
+
if (deltaText) {
|
|
800
|
+
responseText += deltaText;
|
|
801
|
+
if (typeof onTextDelta === "function") {
|
|
802
|
+
onTextDelta(deltaText);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
continue;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
if (delta.type === "input_json_delta") {
|
|
809
|
+
current.type = "tool_use";
|
|
810
|
+
current.inputJson = `${String(current.inputJson || "")}${String(delta.partial_json || "")}`;
|
|
811
|
+
blockMap.set(index, current);
|
|
812
|
+
continue;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
const assistantContent = Array.from(blockMap.values())
|
|
819
|
+
.sort((a, b) => a.order - b.order)
|
|
820
|
+
.map((item) => {
|
|
821
|
+
if (item.type === "text") {
|
|
822
|
+
return {
|
|
823
|
+
type: "text",
|
|
824
|
+
text: String(item.text || ""),
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
const inputFromDelta = normalizeToolCallArgs(item.inputJson || "");
|
|
829
|
+
const mergedInput = {
|
|
830
|
+
...(item.input && typeof item.input === "object" ? item.input : {}),
|
|
831
|
+
...(inputFromDelta && typeof inputFromDelta === "object" ? inputFromDelta : {}),
|
|
832
|
+
};
|
|
833
|
+
return {
|
|
834
|
+
type: "tool_use",
|
|
835
|
+
id: String(item.id || `tool_${randomUUID()}`),
|
|
836
|
+
name: String(item.name || ""),
|
|
837
|
+
input: mergedInput,
|
|
838
|
+
};
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
if (!responseText) {
|
|
842
|
+
responseText = assistantContent
|
|
843
|
+
.filter((item) => item.type === "text")
|
|
844
|
+
.map((item) => item.text)
|
|
845
|
+
.join("");
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
return {
|
|
849
|
+
text: responseText,
|
|
850
|
+
assistantContent,
|
|
851
|
+
toolCalls: extractAnthropicToolCalls(assistantContent),
|
|
852
|
+
};
|
|
853
|
+
} catch (err) {
|
|
854
|
+
if (request.timedOut()) {
|
|
855
|
+
const timeoutError = new Error(`CLI timeout (${normalizeTimeoutMs(timeoutMs)}ms)`);
|
|
856
|
+
timeoutError.code = "timeout";
|
|
857
|
+
throw timeoutError;
|
|
858
|
+
}
|
|
859
|
+
if (signal && typeof signal === "object" && signal.aborted) {
|
|
860
|
+
const cancelError = new Error("CLI cancelled");
|
|
861
|
+
cancelError.code = "cancelled";
|
|
862
|
+
throw cancelError;
|
|
863
|
+
}
|
|
864
|
+
throw err;
|
|
865
|
+
} finally {
|
|
866
|
+
request.cleanup();
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
async function runNativeLoopOpenAi({
|
|
871
|
+
workspaceRoot = process.cwd(),
|
|
872
|
+
prompt = "",
|
|
873
|
+
systemPrompt = "",
|
|
874
|
+
historyMessages = [],
|
|
875
|
+
model = "",
|
|
876
|
+
baseUrl = "",
|
|
877
|
+
apiKey = "",
|
|
878
|
+
timeoutMs = 300000,
|
|
879
|
+
onStreamDelta = null,
|
|
880
|
+
onToolEvent = null,
|
|
881
|
+
signal = null,
|
|
882
|
+
guards,
|
|
883
|
+
} = {}) {
|
|
884
|
+
const requestModel = String(model || "").trim();
|
|
885
|
+
if (!requestModel) {
|
|
886
|
+
throw new Error("ucode model is not configured");
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
const requestUrl = resolveCompletionUrl(baseUrl);
|
|
890
|
+
if (!requestUrl) {
|
|
891
|
+
throw new Error("ucode baseUrl is not configured");
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
const messages = cloneMessageList(historyMessages);
|
|
895
|
+
const systemText = String(systemPrompt || "").trim();
|
|
896
|
+
const hasSystem = messages.some((entry) => String(entry.role || "").trim() === "system");
|
|
897
|
+
if (systemText && !hasSystem) {
|
|
898
|
+
messages.unshift({ role: "system", content: systemText });
|
|
899
|
+
}
|
|
900
|
+
messages.push({ role: "user", content: String(prompt || "") });
|
|
901
|
+
|
|
902
|
+
let aggregated = "";
|
|
903
|
+
let streamed = false;
|
|
904
|
+
let toolCallsExecuted = 0;
|
|
905
|
+
|
|
906
|
+
while (true) {
|
|
907
|
+
guards.ensureActive();
|
|
908
|
+
|
|
909
|
+
const turnResult = await runOpenAiLikeTurn({
|
|
910
|
+
url: requestUrl,
|
|
911
|
+
apiKey,
|
|
912
|
+
model: requestModel,
|
|
913
|
+
messages,
|
|
914
|
+
signal,
|
|
915
|
+
timeoutMs,
|
|
916
|
+
onTextDelta: (chunk) => {
|
|
917
|
+
const text = String(chunk || "");
|
|
918
|
+
if (!text) return;
|
|
919
|
+
aggregated += text;
|
|
920
|
+
if (typeof onStreamDelta === "function") {
|
|
921
|
+
streamed = true;
|
|
922
|
+
onStreamDelta(text);
|
|
923
|
+
}
|
|
924
|
+
},
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
const toolCalls = Array.isArray(turnResult.toolCalls)
|
|
928
|
+
? turnResult.toolCalls.filter((call) => call && call.function && typeof call.function === "object")
|
|
929
|
+
: [];
|
|
930
|
+
|
|
931
|
+
if (toolCalls.length === 0) {
|
|
932
|
+
const text = String(turnResult.text || "").trim();
|
|
933
|
+
if (text) {
|
|
934
|
+
messages.push({
|
|
935
|
+
role: "assistant",
|
|
936
|
+
content: text,
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
if (!aggregated.trim() && text) {
|
|
940
|
+
aggregated = text;
|
|
941
|
+
}
|
|
942
|
+
return {
|
|
943
|
+
text: aggregated,
|
|
944
|
+
streamed,
|
|
945
|
+
toolCallsExecuted,
|
|
946
|
+
messages,
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
const assistantToolCalls = [];
|
|
951
|
+
for (const call of toolCalls) {
|
|
952
|
+
const callId = String(call.id || `call_${randomUUID()}`);
|
|
953
|
+
const name = normalizeToolName(call.function.name || "");
|
|
954
|
+
const args = normalizeToolCallArgs(call.function.arguments || "");
|
|
955
|
+
|
|
956
|
+
assistantToolCalls.push({
|
|
957
|
+
id: callId,
|
|
958
|
+
type: "function",
|
|
959
|
+
function: {
|
|
960
|
+
name: name || String(call.function.name || ""),
|
|
961
|
+
arguments: toJsonString(args),
|
|
962
|
+
},
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
if (assistantToolCalls.length === 0) {
|
|
967
|
+
return {
|
|
968
|
+
text: aggregated,
|
|
969
|
+
streamed,
|
|
970
|
+
toolCallsExecuted,
|
|
971
|
+
messages,
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
messages.push({
|
|
976
|
+
role: "assistant",
|
|
977
|
+
content: null,
|
|
978
|
+
tool_calls: assistantToolCalls,
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
for (const toolCall of assistantToolCalls) {
|
|
982
|
+
const toolResult = runCoreTool({
|
|
983
|
+
tool: toolCall.function.name,
|
|
984
|
+
args: normalizeToolCallArgs(toolCall.function.arguments),
|
|
985
|
+
workspaceRoot,
|
|
986
|
+
onToolEvent,
|
|
987
|
+
});
|
|
988
|
+
toolCallsExecuted += 1;
|
|
989
|
+
messages.push({
|
|
990
|
+
role: "tool",
|
|
991
|
+
tool_call_id: toolCall.id,
|
|
992
|
+
content: clipText(toJsonString(toolResult), 12000),
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
async function runNativeLoopAnthropic({
|
|
1000
|
+
workspaceRoot = process.cwd(),
|
|
1001
|
+
prompt = "",
|
|
1002
|
+
systemPrompt = "",
|
|
1003
|
+
historyMessages = [],
|
|
1004
|
+
model = "",
|
|
1005
|
+
baseUrl = "",
|
|
1006
|
+
apiKey = "",
|
|
1007
|
+
timeoutMs = 300000,
|
|
1008
|
+
onStreamDelta = null,
|
|
1009
|
+
onToolEvent = null,
|
|
1010
|
+
signal = null,
|
|
1011
|
+
guards,
|
|
1012
|
+
} = {}) {
|
|
1013
|
+
const requestModel = String(model || "").trim();
|
|
1014
|
+
if (!requestModel) {
|
|
1015
|
+
throw new Error("ucode model is not configured");
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
const requestUrl = resolveAnthropicMessagesUrl(baseUrl);
|
|
1019
|
+
if (!requestUrl) {
|
|
1020
|
+
throw new Error("ucode baseUrl is not configured");
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
const messages = cloneMessageList(historyMessages);
|
|
1024
|
+
messages.push({
|
|
1025
|
+
role: "user",
|
|
1026
|
+
content: String(prompt || ""),
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
let aggregated = "";
|
|
1030
|
+
let streamed = false;
|
|
1031
|
+
let toolCallsExecuted = 0;
|
|
1032
|
+
|
|
1033
|
+
while (true) {
|
|
1034
|
+
guards.ensureActive();
|
|
1035
|
+
|
|
1036
|
+
const turnResult = await runAnthropicTurn({
|
|
1037
|
+
url: requestUrl,
|
|
1038
|
+
apiKey,
|
|
1039
|
+
model: requestModel,
|
|
1040
|
+
systemPrompt,
|
|
1041
|
+
messages,
|
|
1042
|
+
signal,
|
|
1043
|
+
timeoutMs,
|
|
1044
|
+
onTextDelta: (chunk) => {
|
|
1045
|
+
const text = String(chunk || "");
|
|
1046
|
+
if (!text) return;
|
|
1047
|
+
aggregated += text;
|
|
1048
|
+
if (typeof onStreamDelta === "function") {
|
|
1049
|
+
streamed = true;
|
|
1050
|
+
onStreamDelta(text);
|
|
1051
|
+
}
|
|
1052
|
+
},
|
|
1053
|
+
});
|
|
1054
|
+
|
|
1055
|
+
const toolCalls = Array.isArray(turnResult.toolCalls) ? turnResult.toolCalls : [];
|
|
1056
|
+
|
|
1057
|
+
if (toolCalls.length === 0) {
|
|
1058
|
+
const assistantContent = Array.isArray(turnResult.assistantContent)
|
|
1059
|
+
? turnResult.assistantContent
|
|
1060
|
+
: [];
|
|
1061
|
+
if (assistantContent.length > 0) {
|
|
1062
|
+
messages.push({
|
|
1063
|
+
role: "assistant",
|
|
1064
|
+
content: assistantContent,
|
|
1065
|
+
});
|
|
1066
|
+
} else if (String(turnResult.text || "").trim()) {
|
|
1067
|
+
messages.push({
|
|
1068
|
+
role: "assistant",
|
|
1069
|
+
content: [
|
|
1070
|
+
{
|
|
1071
|
+
type: "text",
|
|
1072
|
+
text: String(turnResult.text || ""),
|
|
1073
|
+
},
|
|
1074
|
+
],
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
const text = String(turnResult.text || "").trim();
|
|
1078
|
+
if (!aggregated.trim() && text) {
|
|
1079
|
+
aggregated = text;
|
|
1080
|
+
}
|
|
1081
|
+
return {
|
|
1082
|
+
text: aggregated,
|
|
1083
|
+
streamed,
|
|
1084
|
+
toolCallsExecuted,
|
|
1085
|
+
messages,
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
const assistantContent = Array.isArray(turnResult.assistantContent)
|
|
1090
|
+
? turnResult.assistantContent
|
|
1091
|
+
: [];
|
|
1092
|
+
|
|
1093
|
+
messages.push({
|
|
1094
|
+
role: "assistant",
|
|
1095
|
+
content: assistantContent,
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1098
|
+
const toolResults = [];
|
|
1099
|
+
for (const call of toolCalls) {
|
|
1100
|
+
const toolResult = runCoreTool({
|
|
1101
|
+
tool: call.name,
|
|
1102
|
+
args: call.args,
|
|
1103
|
+
workspaceRoot,
|
|
1104
|
+
onToolEvent,
|
|
1105
|
+
});
|
|
1106
|
+
toolCallsExecuted += 1;
|
|
1107
|
+
toolResults.push({
|
|
1108
|
+
type: "tool_result",
|
|
1109
|
+
tool_use_id: String(call.id || ""),
|
|
1110
|
+
content: clipText(toJsonString(toolResult), 12000),
|
|
1111
|
+
is_error: Boolean(!toolResult || toolResult.ok === false),
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
messages.push({
|
|
1116
|
+
role: "user",
|
|
1117
|
+
content: toolResults,
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
async function runNativeAgentTask({
|
|
1124
|
+
workspaceRoot = process.cwd(),
|
|
1125
|
+
prompt = "",
|
|
1126
|
+
systemPrompt = "",
|
|
1127
|
+
provider = "",
|
|
1128
|
+
model = "",
|
|
1129
|
+
messages = [],
|
|
1130
|
+
sessionId = "",
|
|
1131
|
+
timeoutMs = 300000,
|
|
1132
|
+
onStreamDelta = null,
|
|
1133
|
+
onToolEvent = null,
|
|
1134
|
+
signal = null,
|
|
1135
|
+
} = {}) {
|
|
1136
|
+
const guards = createGuards({ signal, timeoutMs });
|
|
1137
|
+
const nextSessionId = String(sessionId || "").trim() || `native-${randomUUID()}`;
|
|
1138
|
+
const promptText = String(prompt || "").trim();
|
|
1139
|
+
|
|
1140
|
+
try {
|
|
1141
|
+
guards.ensureActive();
|
|
1142
|
+
|
|
1143
|
+
if (!promptText) {
|
|
1144
|
+
return {
|
|
1145
|
+
ok: false,
|
|
1146
|
+
error: "empty task",
|
|
1147
|
+
output: "",
|
|
1148
|
+
sessionId: nextSessionId,
|
|
1149
|
+
streamed: false,
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
const runtime = resolveRuntimeConfig({
|
|
1154
|
+
workspaceRoot,
|
|
1155
|
+
provider,
|
|
1156
|
+
model,
|
|
1157
|
+
});
|
|
1158
|
+
|
|
1159
|
+
const loopRunner = runtime.transport === "anthropic-messages"
|
|
1160
|
+
? runNativeLoopAnthropic
|
|
1161
|
+
: runNativeLoopOpenAi;
|
|
1162
|
+
|
|
1163
|
+
const runResult = await loopRunner({
|
|
1164
|
+
workspaceRoot,
|
|
1165
|
+
prompt: promptText,
|
|
1166
|
+
systemPrompt,
|
|
1167
|
+
historyMessages: messages,
|
|
1168
|
+
model: runtime.model,
|
|
1169
|
+
baseUrl: runtime.baseUrl,
|
|
1170
|
+
apiKey: runtime.apiKey,
|
|
1171
|
+
timeoutMs,
|
|
1172
|
+
onStreamDelta,
|
|
1173
|
+
onToolEvent,
|
|
1174
|
+
signal,
|
|
1175
|
+
guards,
|
|
1176
|
+
});
|
|
1177
|
+
|
|
1178
|
+
const outputText = String(runResult.text || "").trim() || (
|
|
1179
|
+
runResult.toolCallsExecuted > 0
|
|
1180
|
+
? `Completed ${runResult.toolCallsExecuted} tool call${runResult.toolCallsExecuted === 1 ? "" : "s"}.`
|
|
1181
|
+
: ""
|
|
1182
|
+
);
|
|
1183
|
+
|
|
1184
|
+
return {
|
|
1185
|
+
ok: true,
|
|
1186
|
+
error: "",
|
|
1187
|
+
output: outputText,
|
|
1188
|
+
messages: cloneMessageList(runResult.messages),
|
|
1189
|
+
sessionId: nextSessionId,
|
|
1190
|
+
streamed: Boolean(runResult.streamed),
|
|
1191
|
+
};
|
|
1192
|
+
} catch (err) {
|
|
1193
|
+
const message = err && err.message ? err.message : "native runner failed";
|
|
1194
|
+
return {
|
|
1195
|
+
ok: false,
|
|
1196
|
+
error: message,
|
|
1197
|
+
output: "",
|
|
1198
|
+
sessionId: nextSessionId,
|
|
1199
|
+
streamed: false,
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
module.exports = {
|
|
1205
|
+
runNativeAgentTask,
|
|
1206
|
+
parseReadIntent,
|
|
1207
|
+
parseBashIntent,
|
|
1208
|
+
extractPreflightEvidence,
|
|
1209
|
+
resolveRuntimeConfig,
|
|
1210
|
+
resolveCompletionUrl,
|
|
1211
|
+
resolveAnthropicMessagesUrl,
|
|
1212
|
+
resolveTransport,
|
|
1213
|
+
};
|