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.
Files changed (47) hide show
  1. package/package.json +1 -1
  2. package/src/agents/prompts/native/tasks.js +4 -1
  3. package/src/app/chat/commandExecutor.js +111 -1
  4. package/src/app/chat/commands.js +2 -1
  5. package/src/app/chat/daemonMessageRouter.js +1 -1
  6. package/src/app/chat/inputSubmitHandler.js +3 -2
  7. package/src/code/agent.js +17 -3
  8. package/src/code/commands.js +3 -3
  9. package/src/code/context/executionSegment.js +5 -0
  10. package/src/code/context/planMode.js +8 -1
  11. package/src/code/context/promptLayers.js +10 -9
  12. package/src/code/dispatch.js +4 -0
  13. package/src/code/index.js +2 -0
  14. package/src/code/modelCommand.js +199 -23
  15. package/src/code/nativeRunner.js +299 -225
  16. package/src/code/protocol/controlPlane.js +93 -0
  17. package/src/code/protocol/faultHarness.js +90 -0
  18. package/src/code/protocol/index.js +20 -0
  19. package/src/code/protocol/loopEvents.js +102 -0
  20. package/src/code/protocol/materialize.js +107 -0
  21. package/src/code/protocol/messageFixtures.js +116 -0
  22. package/src/code/protocol/ownership.js +147 -0
  23. package/src/code/protocol/protocolValidator.js +165 -0
  24. package/src/code/protocol/suspension.js +173 -0
  25. package/src/code/protocol/toolCallLedger.js +222 -0
  26. package/src/code/protocol/transitions.js +97 -0
  27. package/src/code/providers/anthropicMessagesTransport.js +93 -0
  28. package/src/code/providers/index.js +8 -0
  29. package/src/code/providers/modelsCatalog.js +304 -0
  30. package/src/code/providers/openaiChatTransport.js +98 -0
  31. package/src/code/providers/transportContract.js +46 -0
  32. package/src/code/repl.js +45 -29
  33. package/src/code/runtime/taskControl.js +177 -53
  34. package/src/code/runtime/taskFocus.js +30 -10
  35. package/src/code/runtime/taskLoop.js +25 -3
  36. package/src/code/runtime/taskRun.js +172 -2
  37. package/src/code/runtime/workspaceLease.js +41 -0
  38. package/src/code/sessionStore.js +1 -0
  39. package/src/code/taskRoute.js +73 -0
  40. package/src/code/thinkingLevels.js +132 -0
  41. package/src/code/tools/taskRun.js +118 -0
  42. package/src/config.js +10 -1
  43. package/src/ui/format/index.js +48 -3
  44. package/src/ui/ink/ChatApp.js +137 -25
  45. package/src/ui/ink/UcodeApp.js +38 -30
  46. package/src/ui/ink/chatLogModel.js +238 -32
  47. 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,8 @@
1
+ "use strict";
2
+
3
+ module.exports = {
4
+ ...require("./transportContract"),
5
+ ...require("./openaiChatTransport"),
6
+ ...require("./anthropicMessagesTransport"),
7
+ ...require("./modelsCatalog"),
8
+ };
@@ -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 nextModel = String(modelMatch[1] || "").trim();
117
- if (!nextModel) {
116
+ const rest = String(modelMatch[1] || "").trim();
117
+ if (!rest) {
118
118
  return { kind: "model", action: "show" };
119
119
  }
120
- // Reject accidental multi-token garbage; model ids are single tokens.
121
- if (/\s/.test(nextModel)) {
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: nextModel,
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 resumeAfterUserInteraction(trimmed, state, {
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 && resumeResult.waitingUserInteraction) {
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
- const shouldSkipSummary = Boolean(streamed && resumeResult.ok && streamedVisible);
658
- if (!shouldSkipSummary && resumeResult.summary) {
659
- stdout.write(`${resumeResult.summary}\n`);
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
+ };