u-foo 3.0.0 → 3.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/agents/prompts/native/tasks.js +4 -1
- package/src/app/chat/commandExecutor.js +111 -1
- package/src/app/chat/commands.js +2 -1
- package/src/app/chat/daemonMessageRouter.js +1 -1
- package/src/app/chat/inputSubmitHandler.js +3 -2
- package/src/code/agent.js +17 -3
- package/src/code/commands.js +3 -3
- package/src/code/context/executionSegment.js +5 -0
- package/src/code/context/planMode.js +8 -1
- package/src/code/context/promptLayers.js +10 -9
- package/src/code/dispatch.js +4 -0
- package/src/code/index.js +2 -0
- package/src/code/modelCommand.js +199 -23
- package/src/code/nativeRunner.js +299 -225
- package/src/code/protocol/controlPlane.js +93 -0
- package/src/code/protocol/faultHarness.js +90 -0
- package/src/code/protocol/index.js +20 -0
- package/src/code/protocol/loopEvents.js +102 -0
- package/src/code/protocol/materialize.js +107 -0
- package/src/code/protocol/messageFixtures.js +116 -0
- package/src/code/protocol/ownership.js +147 -0
- package/src/code/protocol/protocolValidator.js +165 -0
- package/src/code/protocol/suspension.js +173 -0
- package/src/code/protocol/toolCallLedger.js +222 -0
- package/src/code/protocol/transitions.js +97 -0
- package/src/code/providers/anthropicMessagesTransport.js +93 -0
- package/src/code/providers/index.js +8 -0
- package/src/code/providers/modelsCatalog.js +304 -0
- package/src/code/providers/openaiChatTransport.js +98 -0
- package/src/code/providers/transportContract.js +46 -0
- package/src/code/repl.js +45 -29
- package/src/code/runtime/taskControl.js +177 -53
- package/src/code/runtime/taskFocus.js +30 -10
- package/src/code/runtime/taskLoop.js +25 -3
- package/src/code/runtime/taskRun.js +172 -2
- package/src/code/runtime/workspaceLease.js +41 -0
- package/src/code/sessionStore.js +1 -0
- package/src/code/taskRoute.js +73 -0
- package/src/code/thinkingLevels.js +132 -0
- package/src/code/tools/taskRun.js +118 -0
- package/src/config.js +10 -1
- package/src/ui/format/index.js +48 -3
- package/src/ui/ink/ChatApp.js +137 -25
- package/src/ui/ink/UcodeApp.js +38 -30
- package/src/ui/ink/chatLogModel.js +238 -32
- package/src/ui/ink/chatReducer.js +18 -6
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { assertTransport } = require("./transportContract");
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Anthropic Messages API transport adapter.
|
|
7
|
+
* @param {{
|
|
8
|
+
* resolveUrl: Function,
|
|
9
|
+
* runTurn: Function,
|
|
10
|
+
* toJsonString: Function,
|
|
11
|
+
* clipText: Function,
|
|
12
|
+
* }} deps
|
|
13
|
+
*/
|
|
14
|
+
function createAnthropicMessagesTransport(deps = {}) {
|
|
15
|
+
const {
|
|
16
|
+
resolveUrl,
|
|
17
|
+
runTurn,
|
|
18
|
+
toJsonString,
|
|
19
|
+
clipText,
|
|
20
|
+
} = deps;
|
|
21
|
+
|
|
22
|
+
const transport = {
|
|
23
|
+
name: "anthropic-messages",
|
|
24
|
+
resolveUrl,
|
|
25
|
+
prepareMessages({ messages, prompt }) {
|
|
26
|
+
messages.push({
|
|
27
|
+
role: "user",
|
|
28
|
+
content: String(prompt || ""),
|
|
29
|
+
});
|
|
30
|
+
},
|
|
31
|
+
runTurn,
|
|
32
|
+
getToolCalls(turnResult) {
|
|
33
|
+
return Array.isArray(turnResult.toolCalls) ? turnResult.toolCalls : [];
|
|
34
|
+
},
|
|
35
|
+
appendFinalAssistantMessage({ messages, turnResult }) {
|
|
36
|
+
const assistantContent = Array.isArray(turnResult.assistantContent)
|
|
37
|
+
? turnResult.assistantContent
|
|
38
|
+
: [];
|
|
39
|
+
if (assistantContent.length > 0) {
|
|
40
|
+
messages.push({
|
|
41
|
+
role: "assistant",
|
|
42
|
+
content: assistantContent,
|
|
43
|
+
});
|
|
44
|
+
} else if (String(turnResult.text || "").trim()) {
|
|
45
|
+
messages.push({
|
|
46
|
+
role: "assistant",
|
|
47
|
+
content: [
|
|
48
|
+
{
|
|
49
|
+
type: "text",
|
|
50
|
+
text: String(turnResult.text || ""),
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
prepareToolCalls({ messages, turnResult, toolCalls }) {
|
|
57
|
+
const assistantContent = Array.isArray(turnResult.assistantContent)
|
|
58
|
+
? turnResult.assistantContent
|
|
59
|
+
: [];
|
|
60
|
+
|
|
61
|
+
messages.push({
|
|
62
|
+
role: "assistant",
|
|
63
|
+
content: assistantContent,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return toolCalls.map((call) => ({
|
|
67
|
+
name: call.name,
|
|
68
|
+
args: call.args,
|
|
69
|
+
source: call,
|
|
70
|
+
}));
|
|
71
|
+
},
|
|
72
|
+
appendToolResult({ collected, call, toolResult }) {
|
|
73
|
+
collected.push({
|
|
74
|
+
type: "tool_result",
|
|
75
|
+
tool_use_id: String(call.source.id || ""),
|
|
76
|
+
content: clipText(toJsonString(toolResult), 12000),
|
|
77
|
+
is_error: Boolean(!toolResult || toolResult.ok === false),
|
|
78
|
+
});
|
|
79
|
+
},
|
|
80
|
+
flushToolResults({ messages, collected }) {
|
|
81
|
+
messages.push({
|
|
82
|
+
role: "user",
|
|
83
|
+
content: collected,
|
|
84
|
+
});
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
return assertTransport(transport, "anthropic-messages");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = {
|
|
92
|
+
createAnthropicMessagesTransport,
|
|
93
|
+
};
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Live provider model catalog via the OpenAI-compatible / Anthropic models route.
|
|
5
|
+
*
|
|
6
|
+
* Used by /model suggestions and settings validation so ucode only offers
|
|
7
|
+
* (and preferably accepts) models the configured endpoint actually lists.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const DEFAULT_ANTHROPIC_BASE_URL = "https://api.anthropic.com/v1";
|
|
11
|
+
const DEFAULT_TIMEOUT_MS = 8000;
|
|
12
|
+
const CACHE_TTL_MS = 60_000;
|
|
13
|
+
|
|
14
|
+
/** @type {Map<string, { at: number, result: object }>} */
|
|
15
|
+
const modelsCache = new Map();
|
|
16
|
+
|
|
17
|
+
function clipText(value = "", maxChars = 400) {
|
|
18
|
+
const text = String(value || "");
|
|
19
|
+
if (text.length <= maxChars) return text;
|
|
20
|
+
return `${text.slice(0, maxChars)}…`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function resolveOpenAiModelsUrl(baseUrl = "") {
|
|
24
|
+
const raw = String(baseUrl || "").trim();
|
|
25
|
+
if (!raw) return "";
|
|
26
|
+
const normalized = raw.replace(/\/+$/, "");
|
|
27
|
+
if (/\/models$/i.test(normalized)) return normalized;
|
|
28
|
+
if (/\/chat\/completions$/i.test(normalized)) {
|
|
29
|
+
return normalized.replace(/\/chat\/completions$/i, "/models");
|
|
30
|
+
}
|
|
31
|
+
if (/\/v1$/i.test(normalized)) return `${normalized}/models`;
|
|
32
|
+
if (/\/api$/i.test(normalized)) return `${normalized}/v1/models`;
|
|
33
|
+
return `${normalized}/models`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function resolveAnthropicModelsUrl(baseUrl = "") {
|
|
37
|
+
const raw = String(baseUrl || "").trim() || DEFAULT_ANTHROPIC_BASE_URL;
|
|
38
|
+
const normalized = raw.replace(/\/+$/, "");
|
|
39
|
+
if (/\/models$/i.test(normalized)) return normalized;
|
|
40
|
+
if (/\/messages$/i.test(normalized)) {
|
|
41
|
+
return normalized.replace(/\/messages$/i, "/models");
|
|
42
|
+
}
|
|
43
|
+
if (/\/v1$/i.test(normalized)) return `${normalized}/models`;
|
|
44
|
+
if (/\/api$/i.test(normalized)) return `${normalized}/v1/models`;
|
|
45
|
+
return `${normalized}/models`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function resolveModelsUrl({ transport = "", baseUrl = "" } = {}) {
|
|
49
|
+
if (String(transport || "") === "anthropic-messages") {
|
|
50
|
+
return resolveAnthropicModelsUrl(baseUrl);
|
|
51
|
+
}
|
|
52
|
+
return resolveOpenAiModelsUrl(baseUrl);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function cacheKey({ transport = "", baseUrl = "", apiKey = "", provider = "" } = {}) {
|
|
56
|
+
const keyTail = apiKey ? String(apiKey).slice(-8) : "";
|
|
57
|
+
return `${provider}|${transport}|${baseUrl}|${keyTail}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function extractModelIds(payload) {
|
|
61
|
+
const ids = [];
|
|
62
|
+
const seen = new Set();
|
|
63
|
+
const push = (value) => {
|
|
64
|
+
const id = String(value || "").trim();
|
|
65
|
+
if (!id || seen.has(id)) return;
|
|
66
|
+
seen.add(id);
|
|
67
|
+
ids.push(id);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
if (!payload || typeof payload !== "object") return ids;
|
|
71
|
+
|
|
72
|
+
if (Array.isArray(payload.data)) {
|
|
73
|
+
for (const item of payload.data) {
|
|
74
|
+
if (!item) continue;
|
|
75
|
+
if (typeof item === "string") push(item);
|
|
76
|
+
else push(item.id || item.model || item.name);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (Array.isArray(payload.models)) {
|
|
81
|
+
for (const item of payload.models) {
|
|
82
|
+
if (!item) continue;
|
|
83
|
+
if (typeof item === "string") push(item);
|
|
84
|
+
else push(item.id || item.model || item.name);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return ids;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function buildListHeaders({ transport = "", apiKey = "" } = {}) {
|
|
92
|
+
const headers = {
|
|
93
|
+
Accept: "application/json",
|
|
94
|
+
};
|
|
95
|
+
const key = String(apiKey || "").trim();
|
|
96
|
+
if (!key) return headers;
|
|
97
|
+
|
|
98
|
+
if (String(transport || "") === "anthropic-messages") {
|
|
99
|
+
headers["x-api-key"] = key;
|
|
100
|
+
headers["anthropic-version"] = "2023-06-01";
|
|
101
|
+
} else {
|
|
102
|
+
headers.Authorization = `Bearer ${key}`;
|
|
103
|
+
}
|
|
104
|
+
return headers;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Fetch the provider's models catalog.
|
|
109
|
+
* @returns {Promise<{
|
|
110
|
+
* ok: boolean,
|
|
111
|
+
* models: string[],
|
|
112
|
+
* url: string,
|
|
113
|
+
* error: string,
|
|
114
|
+
* status: number,
|
|
115
|
+
* cached: boolean,
|
|
116
|
+
* }>}
|
|
117
|
+
*/
|
|
118
|
+
async function listProviderModels(options = {}) {
|
|
119
|
+
const transport = String(options.transport || "openai-chat").trim() || "openai-chat";
|
|
120
|
+
const baseUrl = String(options.baseUrl || "").trim();
|
|
121
|
+
const apiKey = String(options.apiKey || "").trim();
|
|
122
|
+
const provider = String(options.provider || "").trim();
|
|
123
|
+
const timeoutMs = Math.max(1000, Number(options.timeoutMs) || DEFAULT_TIMEOUT_MS);
|
|
124
|
+
const fetchImpl = typeof options.fetchImpl === "function" ? options.fetchImpl : fetch;
|
|
125
|
+
const skipCache = options.skipCache === true;
|
|
126
|
+
const url = resolveModelsUrl({ transport, baseUrl });
|
|
127
|
+
|
|
128
|
+
if (!url) {
|
|
129
|
+
return {
|
|
130
|
+
ok: false,
|
|
131
|
+
models: [],
|
|
132
|
+
url: "",
|
|
133
|
+
error: "models url unavailable (set ucode base url)",
|
|
134
|
+
status: 0,
|
|
135
|
+
cached: false,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const key = cacheKey({ transport, baseUrl, apiKey, provider });
|
|
140
|
+
if (!skipCache) {
|
|
141
|
+
const hit = modelsCache.get(key);
|
|
142
|
+
if (hit && (Date.now() - hit.at) < CACHE_TTL_MS) {
|
|
143
|
+
return { ...hit.result, cached: true };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const controller = typeof AbortController === "function" ? new AbortController() : null;
|
|
148
|
+
const timer = controller
|
|
149
|
+
? setTimeout(() => {
|
|
150
|
+
try { controller.abort(); } catch { /* ignore */ }
|
|
151
|
+
}, timeoutMs)
|
|
152
|
+
: null;
|
|
153
|
+
if (timer && typeof timer.unref === "function") timer.unref();
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
const response = await fetchImpl(url, {
|
|
157
|
+
method: "GET",
|
|
158
|
+
headers: buildListHeaders({ transport, apiKey }),
|
|
159
|
+
signal: controller ? controller.signal : undefined,
|
|
160
|
+
});
|
|
161
|
+
const status = Number(response && response.status) || 0;
|
|
162
|
+
const bodyText = await response.text().catch(() => "");
|
|
163
|
+
let payload = null;
|
|
164
|
+
try {
|
|
165
|
+
payload = bodyText ? JSON.parse(bodyText) : null;
|
|
166
|
+
} catch {
|
|
167
|
+
payload = null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (!response.ok) {
|
|
171
|
+
const result = {
|
|
172
|
+
ok: false,
|
|
173
|
+
models: [],
|
|
174
|
+
url,
|
|
175
|
+
error: `models route failed (${status}): ${clipText(bodyText || response.statusText || "unknown")}`,
|
|
176
|
+
status,
|
|
177
|
+
cached: false,
|
|
178
|
+
};
|
|
179
|
+
return result;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const models = extractModelIds(payload);
|
|
183
|
+
const result = {
|
|
184
|
+
ok: true,
|
|
185
|
+
models,
|
|
186
|
+
url,
|
|
187
|
+
error: models.length === 0 ? "models route returned an empty catalog" : "",
|
|
188
|
+
status,
|
|
189
|
+
cached: false,
|
|
190
|
+
};
|
|
191
|
+
// Cache successful responses even when empty — avoids hammering a broken gateway.
|
|
192
|
+
modelsCache.set(key, { at: Date.now(), result: { ...result, cached: false } });
|
|
193
|
+
return result;
|
|
194
|
+
} catch (err) {
|
|
195
|
+
const message = err && err.name === "AbortError"
|
|
196
|
+
? `models route timed out after ${timeoutMs}ms`
|
|
197
|
+
: (err && err.message ? err.message : String(err || "models route failed"));
|
|
198
|
+
return {
|
|
199
|
+
ok: false,
|
|
200
|
+
models: [],
|
|
201
|
+
url,
|
|
202
|
+
error: message,
|
|
203
|
+
status: 0,
|
|
204
|
+
cached: false,
|
|
205
|
+
};
|
|
206
|
+
} finally {
|
|
207
|
+
if (timer) clearTimeout(timer);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function modelSupported(model = "", models = []) {
|
|
212
|
+
const needle = String(model || "").trim();
|
|
213
|
+
if (!needle) return false;
|
|
214
|
+
const list = Array.isArray(models) ? models : [];
|
|
215
|
+
return list.some((id) => String(id || "").trim() === needle);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Validate a model id against the live catalog.
|
|
220
|
+
* Soft mode: when the catalog cannot be fetched, allow with a warning.
|
|
221
|
+
*/
|
|
222
|
+
async function confirmModelSupported(options = {}) {
|
|
223
|
+
const model = String(options.model || "").trim();
|
|
224
|
+
if (!model) {
|
|
225
|
+
return {
|
|
226
|
+
ok: false,
|
|
227
|
+
allowed: false,
|
|
228
|
+
model: "",
|
|
229
|
+
models: [],
|
|
230
|
+
error: "model id is empty",
|
|
231
|
+
warning: "",
|
|
232
|
+
catalog: null,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const catalog = await listProviderModels(options);
|
|
237
|
+
if (!catalog.ok) {
|
|
238
|
+
return {
|
|
239
|
+
ok: false,
|
|
240
|
+
allowed: options.strict === true ? false : true,
|
|
241
|
+
model,
|
|
242
|
+
models: [],
|
|
243
|
+
error: catalog.error || "models route unavailable",
|
|
244
|
+
warning: options.strict === true
|
|
245
|
+
? ""
|
|
246
|
+
: `could not confirm model via models route (${catalog.error || "unavailable"}); accepting ${model}`,
|
|
247
|
+
catalog,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (catalog.models.length === 0) {
|
|
252
|
+
return {
|
|
253
|
+
ok: false,
|
|
254
|
+
allowed: options.strict === true ? false : true,
|
|
255
|
+
model,
|
|
256
|
+
models: [],
|
|
257
|
+
error: catalog.error || "empty models catalog",
|
|
258
|
+
warning: options.strict === true
|
|
259
|
+
? ""
|
|
260
|
+
: `models route returned no models; accepting ${model}`,
|
|
261
|
+
catalog,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (!modelSupported(model, catalog.models)) {
|
|
266
|
+
const sample = catalog.models.slice(0, 8).join(", ");
|
|
267
|
+
const more = catalog.models.length > 8 ? ` (+${catalog.models.length - 8} more)` : "";
|
|
268
|
+
return {
|
|
269
|
+
ok: false,
|
|
270
|
+
allowed: false,
|
|
271
|
+
model,
|
|
272
|
+
models: catalog.models,
|
|
273
|
+
error: `model "${model}" is not in the provider catalog${sample ? ` (available: ${sample}${more})` : ""}`,
|
|
274
|
+
warning: "",
|
|
275
|
+
catalog,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
ok: true,
|
|
281
|
+
allowed: true,
|
|
282
|
+
model,
|
|
283
|
+
models: catalog.models,
|
|
284
|
+
error: "",
|
|
285
|
+
warning: "",
|
|
286
|
+
catalog,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function clearModelsCache() {
|
|
291
|
+
modelsCache.clear();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
module.exports = {
|
|
295
|
+
resolveOpenAiModelsUrl,
|
|
296
|
+
resolveAnthropicModelsUrl,
|
|
297
|
+
resolveModelsUrl,
|
|
298
|
+
listProviderModels,
|
|
299
|
+
confirmModelSupported,
|
|
300
|
+
modelSupported,
|
|
301
|
+
extractModelIds,
|
|
302
|
+
clearModelsCache,
|
|
303
|
+
CACHE_TTL_MS,
|
|
304
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { randomUUID } = require("crypto");
|
|
4
|
+
const { assertTransport } = require("./transportContract");
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* OpenAI-compatible chat-completions transport adapter.
|
|
8
|
+
* @param {{
|
|
9
|
+
* resolveUrl: Function,
|
|
10
|
+
* runTurn: Function,
|
|
11
|
+
* normalizeToolName: Function,
|
|
12
|
+
* normalizeToolCallArgs: Function,
|
|
13
|
+
* toJsonString: Function,
|
|
14
|
+
* clipText: Function,
|
|
15
|
+
* }} deps
|
|
16
|
+
*/
|
|
17
|
+
function createOpenAiChatTransport(deps = {}) {
|
|
18
|
+
const {
|
|
19
|
+
resolveUrl,
|
|
20
|
+
runTurn,
|
|
21
|
+
normalizeToolName,
|
|
22
|
+
normalizeToolCallArgs,
|
|
23
|
+
toJsonString,
|
|
24
|
+
clipText,
|
|
25
|
+
} = deps;
|
|
26
|
+
|
|
27
|
+
const transport = {
|
|
28
|
+
name: "openai-chat",
|
|
29
|
+
resolveUrl,
|
|
30
|
+
prepareMessages({ messages, systemPrompt, prompt }) {
|
|
31
|
+
const systemText = String(systemPrompt || "").trim();
|
|
32
|
+
const hasSystem = messages.some((entry) => String(entry.role || "").trim() === "system");
|
|
33
|
+
if (systemText && !hasSystem) {
|
|
34
|
+
messages.unshift({ role: "system", content: systemText });
|
|
35
|
+
}
|
|
36
|
+
messages.push({ role: "user", content: String(prompt || "") });
|
|
37
|
+
},
|
|
38
|
+
runTurn,
|
|
39
|
+
getToolCalls(turnResult) {
|
|
40
|
+
return Array.isArray(turnResult.toolCalls)
|
|
41
|
+
? turnResult.toolCalls.filter((call) => call && call.function && typeof call.function === "object")
|
|
42
|
+
: [];
|
|
43
|
+
},
|
|
44
|
+
appendFinalAssistantMessage({ messages, turnResult }) {
|
|
45
|
+
const text = String(turnResult.text || "").trim();
|
|
46
|
+
if (text) {
|
|
47
|
+
messages.push({
|
|
48
|
+
role: "assistant",
|
|
49
|
+
content: text,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
prepareToolCalls({ messages, toolCalls }) {
|
|
54
|
+
const assistantToolCalls = [];
|
|
55
|
+
for (const call of toolCalls) {
|
|
56
|
+
const callId = String(call.id || `call_${randomUUID()}`);
|
|
57
|
+
const name = normalizeToolName(call.function.name || "");
|
|
58
|
+
const args = normalizeToolCallArgs(call.function.arguments || "");
|
|
59
|
+
|
|
60
|
+
assistantToolCalls.push({
|
|
61
|
+
id: callId,
|
|
62
|
+
type: "function",
|
|
63
|
+
function: {
|
|
64
|
+
name: name || String(call.function.name || ""),
|
|
65
|
+
arguments: toJsonString(args),
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (assistantToolCalls.length === 0) return null;
|
|
71
|
+
|
|
72
|
+
messages.push({
|
|
73
|
+
role: "assistant",
|
|
74
|
+
content: null,
|
|
75
|
+
tool_calls: assistantToolCalls,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return assistantToolCalls.map((toolCall) => ({
|
|
79
|
+
name: toolCall.function.name,
|
|
80
|
+
args: normalizeToolCallArgs(toolCall.function.arguments),
|
|
81
|
+
source: toolCall,
|
|
82
|
+
}));
|
|
83
|
+
},
|
|
84
|
+
appendToolResult({ messages, call, toolResult }) {
|
|
85
|
+
messages.push({
|
|
86
|
+
role: "tool",
|
|
87
|
+
tool_call_id: call.source.id,
|
|
88
|
+
content: clipText(toJsonString(toolResult), 12000),
|
|
89
|
+
});
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
return assertTransport(transport, "openai-chat");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = {
|
|
97
|
+
createOpenAiChatTransport,
|
|
98
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Transport contract for native Agent Loop Provider adapters.
|
|
5
|
+
*
|
|
6
|
+
* Transports own wire-format conversion and turn execution only.
|
|
7
|
+
* They must not decide Plan Mode, write leases, or tool batch policy.
|
|
8
|
+
*
|
|
9
|
+
* Required methods:
|
|
10
|
+
* - resolveUrl(baseUrl) → string
|
|
11
|
+
* - prepareMessages({ messages, systemPrompt?, prompt })
|
|
12
|
+
* - runTurn(params) → Promise<turnResult>
|
|
13
|
+
* - getToolCalls(turnResult) → array
|
|
14
|
+
* - appendFinalAssistantMessage({ messages, turnResult })
|
|
15
|
+
* - prepareToolCalls({ messages, turnResult?, toolCalls }) → pendingCalls|null
|
|
16
|
+
* - appendToolResult({ messages?, collected?, call, toolResult })
|
|
17
|
+
* - flushToolResults?({ messages, collected }) // Anthropic-style batch
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const TRANSPORT_NAMES = Object.freeze(["openai-chat", "anthropic-messages"]);
|
|
21
|
+
|
|
22
|
+
function assertTransport(transport = null, name = "") {
|
|
23
|
+
if (!transport || typeof transport !== "object") {
|
|
24
|
+
throw new Error(`missing transport${name ? `: ${name}` : ""}`);
|
|
25
|
+
}
|
|
26
|
+
const required = [
|
|
27
|
+
"resolveUrl",
|
|
28
|
+
"prepareMessages",
|
|
29
|
+
"runTurn",
|
|
30
|
+
"getToolCalls",
|
|
31
|
+
"appendFinalAssistantMessage",
|
|
32
|
+
"prepareToolCalls",
|
|
33
|
+
"appendToolResult",
|
|
34
|
+
];
|
|
35
|
+
for (const key of required) {
|
|
36
|
+
if (typeof transport[key] !== "function") {
|
|
37
|
+
throw new Error(`transport ${name || "?"} missing ${key}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return transport;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = {
|
|
44
|
+
TRANSPORT_NAMES,
|
|
45
|
+
assertTransport,
|
|
46
|
+
};
|
package/src/code/repl.js
CHANGED
|
@@ -113,21 +113,24 @@ function runSingleCommand(line = "", workspaceRoot = process.cwd()) {
|
|
|
113
113
|
}
|
|
114
114
|
const modelMatch = text.match(/^(?:\/model|model)(?:\s+(.*))?$/i);
|
|
115
115
|
if (modelMatch) {
|
|
116
|
-
const
|
|
117
|
-
if (!
|
|
116
|
+
const rest = String(modelMatch[1] || "").trim();
|
|
117
|
+
if (!rest) {
|
|
118
118
|
return { kind: "model", action: "show" };
|
|
119
119
|
}
|
|
120
|
-
|
|
121
|
-
|
|
120
|
+
const parts = rest.split(/\s+/).filter(Boolean);
|
|
121
|
+
const modelId = parts[0] || "";
|
|
122
|
+
const thinking = parts[1] || "";
|
|
123
|
+
if (!modelId || parts.length > 2) {
|
|
122
124
|
return {
|
|
123
125
|
kind: "error",
|
|
124
|
-
output: "usage: /model [model-id]",
|
|
126
|
+
output: "usage: /model [model-id] [off|low|medium|high|max]",
|
|
125
127
|
};
|
|
126
128
|
}
|
|
127
129
|
return {
|
|
128
130
|
kind: "model",
|
|
129
131
|
action: "set",
|
|
130
|
-
model:
|
|
132
|
+
model: modelId,
|
|
133
|
+
thinking,
|
|
131
134
|
};
|
|
132
135
|
}
|
|
133
136
|
const planMatch = text.match(/^(?:\/plan|plan)(?:\s+(.*))?$/i);
|
|
@@ -283,6 +286,7 @@ async function runUcodeCoreAgent({
|
|
|
283
286
|
resolveUcodeProviderModel,
|
|
284
287
|
runNaturalLanguageTask,
|
|
285
288
|
resumeAfterUserInteraction,
|
|
289
|
+
submitUserInteractionAnswer,
|
|
286
290
|
} = require("./agent");
|
|
287
291
|
const resolvedWorkspaceRoot = resolveUfooProjectRoot(workspaceRoot);
|
|
288
292
|
const resolvedUcode = resolveUcodeProviderModel({
|
|
@@ -290,10 +294,29 @@ async function runUcodeCoreAgent({
|
|
|
290
294
|
provider,
|
|
291
295
|
model,
|
|
292
296
|
});
|
|
297
|
+
const {
|
|
298
|
+
currentThinkingLevel,
|
|
299
|
+
} = require("./modelCommand");
|
|
300
|
+
const {
|
|
301
|
+
resolveThinkingFromEnvAndConfig,
|
|
302
|
+
applyThinkingLevelToEnv,
|
|
303
|
+
} = require("./thinkingLevels");
|
|
304
|
+
const { loadGlobalUcodeConfig } = require("../config");
|
|
305
|
+
let initialThinking = "";
|
|
306
|
+
try {
|
|
307
|
+
initialThinking = String((loadGlobalUcodeConfig() || {}).ucodeThinking || "").trim();
|
|
308
|
+
} catch {
|
|
309
|
+
initialThinking = "";
|
|
310
|
+
}
|
|
311
|
+
const thinkingResolved = resolveThinkingFromEnvAndConfig({
|
|
312
|
+
env: process.env,
|
|
313
|
+
configLevel: initialThinking,
|
|
314
|
+
});
|
|
293
315
|
const state = {
|
|
294
316
|
workspaceRoot: resolvedWorkspaceRoot,
|
|
295
317
|
provider: resolvedUcode.provider,
|
|
296
318
|
model: resolvedUcode.model,
|
|
319
|
+
thinking: currentThinkingLevel({ thinking: initialThinking }),
|
|
297
320
|
engine: "ufoo-core",
|
|
298
321
|
context: buildNlContext({
|
|
299
322
|
appendSystemPrompt,
|
|
@@ -307,6 +330,10 @@ async function runUcodeCoreAgent({
|
|
|
307
330
|
timeoutMs: resolveNlTaskTimeoutMs(Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : NaN),
|
|
308
331
|
jsonOutput,
|
|
309
332
|
};
|
|
333
|
+
// Named levels sync into env; leave an explicit numeric budget override alone.
|
|
334
|
+
if (thinkingResolved.source !== "env-budget") {
|
|
335
|
+
applyThinkingLevelToEnv(state.thinking, process.env);
|
|
336
|
+
}
|
|
310
337
|
persistSessionState(state);
|
|
311
338
|
|
|
312
339
|
if (shouldUseUcodeTui({
|
|
@@ -465,7 +492,9 @@ async function runUcodeCoreAgent({
|
|
|
465
492
|
}
|
|
466
493
|
}
|
|
467
494
|
if (result.kind === "model") {
|
|
468
|
-
const applied = applyUcodeModelCommand(state, result
|
|
495
|
+
const applied = await applyUcodeModelCommand(state, result, {
|
|
496
|
+
workspaceRoot: runtimeWorkspace,
|
|
497
|
+
});
|
|
469
498
|
stdout.write(`${applied.output}\n`);
|
|
470
499
|
if (applied.ok && result.action === "set") {
|
|
471
500
|
persistSessionState(state);
|
|
@@ -592,23 +621,12 @@ async function runUcodeCoreAgent({
|
|
|
592
621
|
|
|
593
622
|
// Pending approval/choice/chat takes priority over nudge / new NL.
|
|
594
623
|
try {
|
|
595
|
-
const {
|
|
596
|
-
hasPendingUserInteraction,
|
|
597
|
-
parseUserInteractionInput,
|
|
598
|
-
getPendingUserInteraction,
|
|
599
|
-
} = require("./context/userInteraction");
|
|
624
|
+
const { hasPendingUserInteraction } = require("./context/userInteraction");
|
|
600
625
|
if (
|
|
601
626
|
trimmed
|
|
602
627
|
&& state.executionState
|
|
603
628
|
&& hasPendingUserInteraction(state.executionState)
|
|
604
629
|
) {
|
|
605
|
-
const pending = getPendingUserInteraction(state.executionState);
|
|
606
|
-
const parsed = parseUserInteractionInput(pending, trimmed);
|
|
607
|
-
if (!parsed.ok) {
|
|
608
|
-
stdout.write(`${parsed.error || "Invalid reply"}\n`);
|
|
609
|
-
printPrompt(stdout);
|
|
610
|
-
return;
|
|
611
|
-
}
|
|
612
630
|
chain = chain.then(async () => {
|
|
613
631
|
let streamBuffer = null;
|
|
614
632
|
let streamedVisible = false;
|
|
@@ -622,7 +640,7 @@ async function runUcodeCoreAgent({
|
|
|
622
640
|
taskInFlight = true;
|
|
623
641
|
let resumeResult;
|
|
624
642
|
try {
|
|
625
|
-
resumeResult = await
|
|
643
|
+
resumeResult = await submitUserInteractionAnswer(trimmed, state, {
|
|
626
644
|
onDelta: state.jsonOutput
|
|
627
645
|
? null
|
|
628
646
|
: async (delta) => {
|
|
@@ -649,15 +667,12 @@ async function runUcodeCoreAgent({
|
|
|
649
667
|
if (streamed && streamedVisible && resumeResult && resumeResult.streamLastChar !== "\n") {
|
|
650
668
|
stdout.write("\n");
|
|
651
669
|
}
|
|
652
|
-
if (resumeResult
|
|
653
|
-
stdout.write("Still waiting for your reply.\n");
|
|
654
|
-
} else if (!resumeResult || resumeResult.ok === false) {
|
|
670
|
+
if (!resumeResult || resumeResult.ok === false) {
|
|
655
671
|
stdout.write(`Error: ${(resumeResult && resumeResult.error) || "resume failed"}\n`);
|
|
656
|
-
} else {
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
}
|
|
672
|
+
} else if (resumeResult.shouldEchoSummary && resumeResult.echoSummaryText) {
|
|
673
|
+
stdout.write(`${resumeResult.echoSummaryText}\n`);
|
|
674
|
+
} else if (resumeResult.waitingUserInteraction) {
|
|
675
|
+
stdout.write("Still waiting for your reply.\n");
|
|
661
676
|
}
|
|
662
677
|
const persisted = persistSessionState(state);
|
|
663
678
|
if (!state.jsonOutput && (!persisted || persisted.ok === false)) {
|
|
@@ -783,4 +798,5 @@ module.exports = {
|
|
|
783
798
|
applyUcodeModelCommand,
|
|
784
799
|
applyUcodePlanCommand,
|
|
785
800
|
suggestUcodeModels,
|
|
786
|
-
|
|
801
|
+
suggestUcodeThinkingLevels: require("./modelCommand").suggestUcodeThinkingLevels,
|
|
802
|
+
};
|