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
@@ -4,6 +4,15 @@ const { randomUUID } = require("crypto");
4
4
 
5
5
  /**
6
6
  * TaskRun registry — parent Task node identity vs runnable attempt.
7
+ *
8
+ * Scheduler owner: TaskLoop (`processTaskRun` / `resumePersistedTaskRuns`).
9
+ * Agent Loop may only issue control commands (start/cancel/complete) via CAS.
10
+ *
11
+ * Restart rules:
12
+ * - queued → remain queued (scheduler resumes)
13
+ * - running + phase waiting_model|executing_tools|planning → requeue to queued
14
+ * - cancelling → stay cancelling until cancel completes
15
+ * - terminal → never transition backward
7
16
  */
8
17
 
9
18
  const TASK_RUN_STATUSES = Object.freeze([
@@ -25,6 +34,31 @@ const TASK_RUN_PHASES = Object.freeze([
25
34
 
26
35
  const TERMINAL_TASK_RUN = new Set(["succeeded", "failed", "cancelled"]);
27
36
 
37
+ /** Keep in sync with protocol/transitions.TASK_RUN_TRANSITIONS. */
38
+ const TASK_RUN_TRANSITIONS = Object.freeze({
39
+ queued: Object.freeze(["running", "cancelled"]),
40
+ running: Object.freeze(["succeeded", "failed", "cancelling"]),
41
+ cancelling: Object.freeze(["cancelled", "failed"]),
42
+ succeeded: Object.freeze([]),
43
+ failed: Object.freeze([]),
44
+ cancelled: Object.freeze([]),
45
+ });
46
+
47
+ /** Default write-lease / heartbeat staleness (ms). */
48
+ const DEFAULT_LEASE_STALE_MS = 30 * 60 * 1000;
49
+
50
+ /** Extra recovery edge used only by recoverTaskRunsAfterRestart. */
51
+ const RECOVERY_TRANSITIONS = Object.freeze({
52
+ running: Object.freeze(["queued"]),
53
+ });
54
+
55
+ function isAllowedTaskRunTransition(fromStatus = "", toStatus = "") {
56
+ const from = String(fromStatus || "").trim();
57
+ const to = String(toStatus || "").trim();
58
+ const allowed = TASK_RUN_TRANSITIONS[from];
59
+ if (!allowed) return false;
60
+ return allowed.includes(to);
61
+ }
28
62
  function createTaskRunId() {
29
63
  return `trun_${Date.now().toString(36)}_${randomUUID().slice(0, 6)}`;
30
64
  }
@@ -33,6 +67,7 @@ function emptyTaskRunStore() {
33
67
  return {
34
68
  byId: {},
35
69
  commandLog: {},
70
+ wakeupLog: {},
36
71
  };
37
72
  }
38
73
 
@@ -47,6 +82,9 @@ function ensureTaskRunStore(executionState = null) {
47
82
  if (!state.taskRuns.commandLog || typeof state.taskRuns.commandLog !== "object") {
48
83
  state.taskRuns.commandLog = {};
49
84
  }
85
+ if (!state.taskRuns.wakeupLog || typeof state.taskRuns.wakeupLog !== "object") {
86
+ state.taskRuns.wakeupLog = {};
87
+ }
50
88
  return state.taskRuns;
51
89
  }
52
90
 
@@ -55,13 +93,22 @@ function createTaskRun({
55
93
  parentNodeId = "",
56
94
  childGraphId = "",
57
95
  attempt = 1,
96
+ kind = "",
97
+ objective = "",
98
+ title = "",
58
99
  } = {}) {
59
100
  const now = new Date().toISOString();
101
+ const parentNode = String(parentNodeId || "").trim();
102
+ const resolvedKind = String(kind || "").trim()
103
+ || (parentNode ? "graph_node" : "standalone");
60
104
  return {
61
105
  id: createTaskRunId(),
106
+ kind: resolvedKind,
62
107
  parentGraphId: String(parentGraphId || "").trim(),
63
- parentNodeId: String(parentNodeId || "").trim(),
108
+ parentNodeId: parentNode,
64
109
  childGraphId: String(childGraphId || "").trim(),
110
+ objective: String(objective || "").trim(),
111
+ title: String(title || objective || "").trim(),
65
112
  status: "queued",
66
113
  phase: "initializing",
67
114
  attempt: Number.isFinite(attempt) ? Math.max(1, Math.floor(attempt)) : 1,
@@ -73,6 +120,8 @@ function createTaskRun({
73
120
  createdAt: now,
74
121
  startedAt: "",
75
122
  completedAt: "",
123
+ heartbeatAt: "",
124
+ lastWakeupId: "",
76
125
  };
77
126
  }
78
127
 
@@ -113,8 +162,26 @@ function isTerminalTaskRun(run = null) {
113
162
  return Boolean(run && TERMINAL_TASK_RUN.has(String(run.status || "")));
114
163
  }
115
164
 
165
+ function touchTaskRunHeartbeat(executionState = null, taskRunId = "") {
166
+ const run = getTaskRun(executionState, taskRunId);
167
+ if (!run) return null;
168
+ run.heartbeatAt = new Date().toISOString();
169
+ putTaskRun(executionState, run);
170
+ return run;
171
+ }
172
+
173
+ function isTransitionAllowed(fromStatus, toStatus, { allowRecovery = false } = {}) {
174
+ if (fromStatus === toStatus) return true;
175
+ if (isAllowedTaskRunTransition(fromStatus, toStatus)) return true;
176
+ if (allowRecovery) {
177
+ const extra = RECOVERY_TRANSITIONS[fromStatus] || [];
178
+ return extra.includes(toStatus);
179
+ }
180
+ return false;
181
+ }
182
+
116
183
  /**
117
- * Compare-and-set status transition. Returns { ok, run }.
184
+ * Compare-and-set status transition. Enforces allowed edges; terminal is final.
118
185
  */
119
186
  function casTaskRunStatus(executionState = null, taskRunId = "", {
120
187
  expectedStatus = "",
@@ -123,6 +190,7 @@ function casTaskRunStatus(executionState = null, taskRunId = "", {
123
190
  result = null,
124
191
  error = null,
125
192
  changedFiles = null,
193
+ allowRecovery = false,
126
194
  } = {}) {
127
195
  const run = getTaskRun(executionState, taskRunId);
128
196
  if (!run) return { ok: false, code: "TASK_RUN_NOT_FOUND", run: null };
@@ -135,10 +203,28 @@ function casTaskRunStatus(executionState = null, taskRunId = "", {
135
203
  currentStatus: run.status,
136
204
  };
137
205
  }
206
+ if (TERMINAL_TASK_RUN.has(run.status)) {
207
+ return {
208
+ ok: false,
209
+ code: "TASK_ALREADY_TERMINAL",
210
+ run,
211
+ currentStatus: run.status,
212
+ };
213
+ }
138
214
  const next = String(nextStatus || "").trim();
139
215
  if (!TASK_RUN_STATUSES.includes(next)) {
140
216
  return { ok: false, code: "INVALID_TASK_STATUS", run };
141
217
  }
218
+ if (!isTransitionAllowed(run.status, next, { allowRecovery })) {
219
+ return {
220
+ ok: false,
221
+ code: "TASK_TRANSITION_FORBIDDEN",
222
+ run,
223
+ currentStatus: run.status,
224
+ nextStatus: next,
225
+ allowed: (TASK_RUN_TRANSITIONS[run.status] || []).slice(),
226
+ };
227
+ }
142
228
  run.status = next;
143
229
  if (phase && TASK_RUN_PHASES.includes(phase)) run.phase = phase;
144
230
  if (result !== null) run.result = result;
@@ -150,6 +236,7 @@ function casTaskRunStatus(executionState = null, taskRunId = "", {
150
236
  run.phase = "finalizing";
151
237
  }
152
238
  if (next === "cancelling") run.cancelRequested = true;
239
+ run.heartbeatAt = new Date().toISOString();
153
240
  putTaskRun(executionState, run);
154
241
  return { ok: true, run };
155
242
  }
@@ -168,10 +255,89 @@ function getCachedControlCommand(executionState = null, commandId = "") {
168
255
  return store.commandLog[id] ? JSON.parse(JSON.stringify(store.commandLog[id])) : null;
169
256
  }
170
257
 
258
+ /**
259
+ * Deduplicate wakeups by wakeupId. Second delivery returns cached result.
260
+ */
261
+ function beginWakeup(executionState = null, wakeupId = "", meta = {}) {
262
+ const id = String(wakeupId || "").trim();
263
+ if (!id) return { ok: true, fresh: true };
264
+ const store = ensureTaskRunStore(executionState);
265
+ const existing = store.wakeupLog[id];
266
+ if (existing && existing.status === "completed") {
267
+ return {
268
+ ok: true,
269
+ fresh: false,
270
+ idempotentReplay: true,
271
+ result: existing.result ? JSON.parse(JSON.stringify(existing.result)) : existing,
272
+ };
273
+ }
274
+ if (existing && existing.status === "started") {
275
+ return {
276
+ ok: true,
277
+ fresh: false,
278
+ idempotentReplay: true,
279
+ result: { status: "in_flight", wakeupId: id },
280
+ };
281
+ }
282
+ store.wakeupLog[id] = {
283
+ status: "started",
284
+ startedAt: new Date().toISOString(),
285
+ ...meta,
286
+ };
287
+ return { ok: true, fresh: true };
288
+ }
289
+
290
+ function completeWakeup(executionState = null, wakeupId = "", result = {}) {
291
+ const id = String(wakeupId || "").trim();
292
+ if (!id) return;
293
+ const store = ensureTaskRunStore(executionState);
294
+ store.wakeupLog[id] = {
295
+ ...(store.wakeupLog[id] || {}),
296
+ status: "completed",
297
+ completedAt: new Date().toISOString(),
298
+ result: JSON.parse(JSON.stringify(result || {})),
299
+ };
300
+ }
301
+
302
+ /**
303
+ * After process restart: requeue interrupted running runs; leave cancelling alone.
304
+ * Does not execute tools — caller should invoke processTaskRun separately.
305
+ */
306
+ function recoverTaskRunsAfterRestart(executionState = null) {
307
+ const store = ensureTaskRunStore(executionState);
308
+ const recovered = [];
309
+ for (const run of Object.values(store.byId)) {
310
+ if (!run) continue;
311
+ if (run.status === "running") {
312
+ const phase = String(run.phase || "");
313
+ if (phase === "waiting_model" || phase === "executing_tools" || phase === "planning") {
314
+ const cas = casTaskRunStatus(executionState, run.id, {
315
+ expectedStatus: "running",
316
+ nextStatus: "queued",
317
+ phase: "initializing",
318
+ allowRecovery: true,
319
+ });
320
+ recovered.push({
321
+ taskRunId: run.id,
322
+ action: cas.ok ? "requeued" : "skip",
323
+ code: cas.ok ? "" : cas.code,
324
+ });
325
+ } else {
326
+ recovered.push({ taskRunId: run.id, action: "resume_running" });
327
+ }
328
+ } else if (run.status === "queued" || run.status === "cancelling") {
329
+ recovered.push({ taskRunId: run.id, action: `resume_${run.status}` });
330
+ }
331
+ }
332
+ return recovered;
333
+ }
334
+
171
335
  module.exports = {
172
336
  TASK_RUN_STATUSES,
173
337
  TASK_RUN_PHASES,
174
338
  TERMINAL_TASK_RUN,
339
+ TASK_RUN_TRANSITIONS,
340
+ DEFAULT_LEASE_STALE_MS,
175
341
  createTaskRunId,
176
342
  emptyTaskRunStore,
177
343
  ensureTaskRunStore,
@@ -181,7 +347,11 @@ module.exports = {
181
347
  findActiveTaskRunForNode,
182
348
  listActiveWritingTaskRuns,
183
349
  isTerminalTaskRun,
350
+ touchTaskRunHeartbeat,
184
351
  casTaskRunStatus,
185
352
  cacheControlCommand,
186
353
  getCachedControlCommand,
354
+ beginWakeup,
355
+ completeWakeup,
356
+ recoverTaskRunsAfterRestart,
187
357
  };
@@ -190,6 +190,46 @@ function hasActiveWriteLease(executionState = null) {
190
190
  return countWriteLeases(executionState) > 0;
191
191
  }
192
192
 
193
+ /**
194
+ * Release leases whose TaskRun is missing/terminal, or whose acquiredAt is older
195
+ * than maxAgeMs without an active non-terminal run heartbeat.
196
+ */
197
+ function releaseStaleWriteLeases(executionState = null, {
198
+ maxAgeMs = require("./taskRun").DEFAULT_LEASE_STALE_MS,
199
+ nowMs = Date.now(),
200
+ } = {}) {
201
+ const {
202
+ getTaskRun,
203
+ isTerminalTaskRun,
204
+ } = require("./taskRun");
205
+ const lease = ensureWorkspaceLease(executionState);
206
+ const holders = normalizeHolders(lease);
207
+ const released = [];
208
+ const kept = [];
209
+ const maxAge = Number.isFinite(maxAgeMs) ? Math.max(0, maxAgeMs) : 0;
210
+
211
+ for (const holder of holders) {
212
+ const run = getTaskRun(executionState, holder.taskRunId);
213
+ if (!run || isTerminalTaskRun(run)) {
214
+ released.push({ taskRunId: holder.taskRunId, reason: "terminal_or_missing" });
215
+ continue;
216
+ }
217
+ const acquired = Date.parse(String(holder.acquiredAt || ""));
218
+ if (maxAge > 0 && Number.isFinite(acquired) && (nowMs - acquired) > maxAge) {
219
+ const beat = Date.parse(String(run.heartbeatAt || run.startedAt || ""));
220
+ if (!Number.isFinite(beat) || (nowMs - beat) > maxAge) {
221
+ released.push({ taskRunId: holder.taskRunId, reason: "stale_heartbeat" });
222
+ continue;
223
+ }
224
+ }
225
+ kept.push(holder);
226
+ }
227
+
228
+ lease.holders = kept;
229
+ if (kept.length === 0) lease.acquiredAt = "";
230
+ return { ok: true, released, kept: kept.slice(), lease };
231
+ }
232
+
193
233
  module.exports = {
194
234
  WRITE_TOOLS,
195
235
  MAX_CONCURRENT_WRITE_LEASES,
@@ -205,4 +245,5 @@ module.exports = {
205
245
  clearWorkspaceLease,
206
246
  checkWriteAllowed,
207
247
  hasActiveWriteLease,
248
+ releaseStaleWriteLeases,
208
249
  };
@@ -68,6 +68,7 @@ function normalizeContextPolicy(value = {}) {
68
68
  }
69
69
 
70
70
  function buildSessionSnapshot(input = {}) {
71
+ // Durable session fields vs projections: src/code/protocol/ownership.js
71
72
  const source = input && typeof input === "object" ? input : {};
72
73
  const sessionId = resolveSessionId(source.sessionId);
73
74
  const createdAt = String(source.createdAt || "").trim() || toIsoNow();
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Structural / explicit decomposition upgrade decisions (R7).
5
+ * Not a language-specific bug-keyword classifier.
6
+ */
7
+
8
+ function shouldUpgradeToDecomposition(task = "", options = {}) {
9
+ const text = String(task || "").trim();
10
+ const reasons = [];
11
+
12
+ if (options.forceDirect === true || options.disableDecomposition === true) {
13
+ return {
14
+ upgrade: false,
15
+ reason: "forced_direct",
16
+ reasons: ["forced_direct"],
17
+ };
18
+ }
19
+ if (options.forceDecomposition === true || options.forceDecompose === true) {
20
+ return {
21
+ upgrade: true,
22
+ reason: "forced_decomposition",
23
+ reasons: ["forced_decomposition"],
24
+ };
25
+ }
26
+ if (!text) {
27
+ return { upgrade: false, reason: "empty", reasons: ["empty"] };
28
+ }
29
+
30
+ // Explicit user/model requests (EN + ZH).
31
+ if (/(?:\bdecompos(?:e|ition)\b|\bbreak\s+(?:this|it)\s+down\b|\bmulti[- ]?step\s+plan\b|拆解|分步|分解任务|制定计划)/i.test(text)) {
32
+ reasons.push("explicit_decompose_request");
33
+ }
34
+
35
+ // Multiple independently verifiable goals (numbered / bulleted / conjunctions).
36
+ const numbered = (text.match(/(?:^|\n)\s*(?:\d+[\).]|[-*•])\s+\S+/g) || []).length;
37
+ if (numbered >= 2) reasons.push("multiple_listed_goals");
38
+
39
+ const multiClause = /(?:^|[;;。\n])\s*(?:and\s+also|also|然后|并且|同时|另外|以及)\b/i.test(text)
40
+ || (text.split(/[;;]/).map((s) => s.trim()).filter((s) => s.length > 12).length >= 3);
41
+ if (multiClause) reasons.push("multi_clause_objectives");
42
+
43
+ // High-risk / multi-file structural cues (not "fix" alone).
44
+ if (/\b(?:across\s+(?:files?|modules?|packages?)|multiple\s+files?|refactor\s+the\s+\w+|迁移|跨文件|多文件)\b/i.test(text)) {
45
+ reasons.push("multi_file_or_refactor_scope");
46
+ }
47
+ if (/\b(?:checkpoint|rollback|migration|schema\s+change|生产环境|回滚)\b/i.test(text)) {
48
+ reasons.push("high_risk_change");
49
+ }
50
+
51
+ // Runtime budget / prior failures may request upgrade.
52
+ if (Number(options.failureCount || 0) >= 2) {
53
+ reasons.push("repeated_failures");
54
+ }
55
+ if (options.modelRequestedUpgrade === true) {
56
+ reasons.push("model_route_decision");
57
+ }
58
+ if (options.hasPlanGraph === true) {
59
+ // Already structured — keep direct loop unless other reasons fire.
60
+ // (graph exists is not itself a decompose trigger)
61
+ }
62
+
63
+ const upgrade = reasons.length > 0;
64
+ return {
65
+ upgrade,
66
+ reason: upgrade ? reasons[0] : "direct_default",
67
+ reasons,
68
+ };
69
+ }
70
+
71
+ module.exports = {
72
+ shouldUpgradeToDecomposition,
73
+ };
@@ -0,0 +1,132 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Ucode thinking-intensity levels.
5
+ *
6
+ * Used as the secondary `/model <id> <level>` menu after picking a model.
7
+ * Maps to Anthropic extended-thinking budget_tokens and OpenAI-compatible
8
+ * reasoning_effort where the transport supports it.
9
+ */
10
+
11
+ const THINKING_LEVELS = Object.freeze([
12
+ {
13
+ id: "off",
14
+ desc: "disable extended thinking",
15
+ budgetTokens: 0,
16
+ reasoningEffort: "",
17
+ },
18
+ {
19
+ id: "low",
20
+ desc: "light thinking",
21
+ budgetTokens: 2048,
22
+ reasoningEffort: "low",
23
+ },
24
+ {
25
+ id: "medium",
26
+ desc: "default thinking",
27
+ budgetTokens: 10000,
28
+ reasoningEffort: "medium",
29
+ },
30
+ {
31
+ id: "high",
32
+ desc: "deeper thinking",
33
+ budgetTokens: 32000,
34
+ reasoningEffort: "high",
35
+ },
36
+ {
37
+ id: "max",
38
+ desc: "maximum thinking budget",
39
+ budgetTokens: 48000,
40
+ reasoningEffort: "high",
41
+ },
42
+ ]);
43
+
44
+ const DEFAULT_THINKING_LEVEL = "medium";
45
+ const THINKING_LEVEL_IDS = new Set(THINKING_LEVELS.map((item) => item.id));
46
+
47
+ function normalizeThinkingLevel(value = "") {
48
+ const raw = String(value || "").trim().toLowerCase();
49
+ if (!raw) return "";
50
+ if (raw === "none" || raw === "disable" || raw === "disabled" || raw === "0") return "off";
51
+ if (raw === "med" || raw === "default") return "medium";
52
+ if (raw === "maximum" || raw === "xhigh" || raw === "ultra") return "max";
53
+ if (THINKING_LEVEL_IDS.has(raw)) return raw;
54
+ return "";
55
+ }
56
+
57
+ function getThinkingLevel(id = "") {
58
+ const normalized = normalizeThinkingLevel(id) || DEFAULT_THINKING_LEVEL;
59
+ return THINKING_LEVELS.find((item) => item.id === normalized) || THINKING_LEVELS[2];
60
+ }
61
+
62
+ function suggestThinkingLevels(options = {}) {
63
+ const current = normalizeThinkingLevel(options.current || "") || DEFAULT_THINKING_LEVEL;
64
+ return THINKING_LEVELS.map((item) => ({
65
+ id: item.id,
66
+ desc: item.id === current ? `${item.desc} · current` : item.desc,
67
+ }));
68
+ }
69
+
70
+ function resolveThinkingFromEnvAndConfig({
71
+ env = process.env,
72
+ configLevel = "",
73
+ } = {}) {
74
+ // Explicit numeric budget still wins (advanced override).
75
+ const rawBudget = env && env.UFOO_UCODE_THINKING_BUDGET_TOKENS;
76
+ if (rawBudget !== undefined && rawBudget !== null && String(rawBudget).trim() !== "") {
77
+ const parsed = Number.parseInt(String(rawBudget), 10);
78
+ if (!Number.isFinite(parsed) || parsed <= 0) {
79
+ return {
80
+ level: "off",
81
+ budgetTokens: 0,
82
+ reasoningEffort: "",
83
+ source: "env-budget",
84
+ };
85
+ }
86
+ return {
87
+ level: "",
88
+ budgetTokens: Math.floor(parsed),
89
+ reasoningEffort: "",
90
+ source: "env-budget",
91
+ };
92
+ }
93
+
94
+ const fromEnv = normalizeThinkingLevel(env && env.UFOO_UCODE_THINKING);
95
+ const fromConfig = normalizeThinkingLevel(configLevel);
96
+ const level = fromEnv || fromConfig || DEFAULT_THINKING_LEVEL;
97
+ const spec = getThinkingLevel(level);
98
+ return {
99
+ level: spec.id,
100
+ budgetTokens: spec.budgetTokens,
101
+ reasoningEffort: spec.reasoningEffort,
102
+ source: fromEnv ? "env" : (fromConfig ? "config" : "default"),
103
+ };
104
+ }
105
+
106
+ function applyThinkingLevelToEnv(level = "", env = process.env) {
107
+ const normalized = normalizeThinkingLevel(level);
108
+ if (!normalized) return "";
109
+ const spec = getThinkingLevel(normalized);
110
+ try {
111
+ env.UFOO_UCODE_THINKING = spec.id;
112
+ if (spec.budgetTokens > 0) {
113
+ env.UFOO_UCODE_THINKING_BUDGET_TOKENS = String(spec.budgetTokens);
114
+ } else {
115
+ env.UFOO_UCODE_THINKING_BUDGET_TOKENS = "0";
116
+ }
117
+ } catch {
118
+ // ignore env write failures
119
+ }
120
+ return spec.id;
121
+ }
122
+
123
+ module.exports = {
124
+ THINKING_LEVELS,
125
+ THINKING_LEVEL_IDS,
126
+ DEFAULT_THINKING_LEVEL,
127
+ normalizeThinkingLevel,
128
+ getThinkingLevel,
129
+ suggestThinkingLevels,
130
+ resolveThinkingFromEnvAndConfig,
131
+ applyThinkingLevelToEnv,
132
+ };
@@ -0,0 +1,118 @@
1
+ "use strict";
2
+
3
+ const {
4
+ startStandaloneTask,
5
+ cancelTask,
6
+ failTask,
7
+ completeTaskFromLoop,
8
+ } = require("../runtime/taskControl");
9
+ const { getTaskRun } = require("../runtime/taskRun");
10
+ const { emptyExecutionState } = require("../context/executionSegment");
11
+
12
+ function normalizeTaskRunCommand(args = {}) {
13
+ if (!args || typeof args !== "object") return null;
14
+ const operation = String(args.operation || args.op || "").trim().toLowerCase();
15
+ if (!operation) return null;
16
+ return {
17
+ operation,
18
+ objective: args.objective,
19
+ title: args.title,
20
+ taskRunId: args.taskRunId || args.task_run_id,
21
+ nodeId: args.nodeId || args.node_id,
22
+ reason: args.reason,
23
+ result: args.result,
24
+ commandId: args.commandId || args.command_id,
25
+ };
26
+ }
27
+
28
+ function runTaskRunTool(args = {}, options = {}) {
29
+ const command = normalizeTaskRunCommand(args) || args;
30
+ const operation = String(command.operation || "").trim().toLowerCase();
31
+ const executionState = options.executionState && typeof options.executionState === "object"
32
+ ? options.executionState
33
+ : emptyExecutionState();
34
+ const commandId = String(command.commandId || "").trim();
35
+ const runTool = options.runTool || null;
36
+ const knownTools = options.knownTools || null;
37
+
38
+ let payload;
39
+ if (operation === "start") {
40
+ payload = startStandaloneTask(executionState, {
41
+ objective: command.objective,
42
+ title: command.title,
43
+ commandId,
44
+ runTool,
45
+ knownTools,
46
+ processImmediately: options.processImmediately !== false,
47
+ });
48
+ } else if (operation === "cancel") {
49
+ payload = cancelTask(executionState, {
50
+ taskRunId: command.taskRunId,
51
+ nodeId: command.nodeId,
52
+ reason: command.reason,
53
+ commandId,
54
+ });
55
+ } else if (operation === "fail") {
56
+ payload = failTask(executionState, {
57
+ taskRunId: command.taskRunId,
58
+ nodeId: command.nodeId,
59
+ reason: command.reason,
60
+ commandId,
61
+ });
62
+ } else if (operation === "complete") {
63
+ payload = completeTaskFromLoop(executionState, {
64
+ taskRunId: command.taskRunId,
65
+ result: command.result,
66
+ commandId,
67
+ });
68
+ } else if (operation === "inspect") {
69
+ const run = getTaskRun(executionState, command.taskRunId);
70
+ if (!run) {
71
+ payload = {
72
+ status: "rejected",
73
+ ok: false,
74
+ errors: [{ code: "TASK_RUN_NOT_FOUND", message: "task run missing" }],
75
+ };
76
+ } else {
77
+ payload = {
78
+ status: "accepted",
79
+ ok: true,
80
+ taskRun: {
81
+ id: run.id,
82
+ kind: run.kind || "",
83
+ status: run.status,
84
+ phase: run.phase,
85
+ objective: run.objective || "",
86
+ title: run.title || "",
87
+ parentGraphId: run.parentGraphId || "",
88
+ parentNodeId: run.parentNodeId || "",
89
+ childGraphId: run.childGraphId || "",
90
+ result: run.result,
91
+ error: run.error,
92
+ changedFiles: Array.isArray(run.changedFiles) ? run.changedFiles.slice() : [],
93
+ },
94
+ };
95
+ }
96
+ } else {
97
+ payload = {
98
+ status: "rejected",
99
+ ok: false,
100
+ errors: [{
101
+ code: "UNKNOWN_TASK_RUN_OP",
102
+ message: `unknown task_run operation: ${operation || "(empty)"}`,
103
+ }],
104
+ };
105
+ }
106
+
107
+ const ok = payload && payload.ok !== false && payload.status !== "rejected";
108
+ return {
109
+ ok,
110
+ ...payload,
111
+ executionState,
112
+ };
113
+ }
114
+
115
+ module.exports = {
116
+ normalizeTaskRunCommand,
117
+ runTaskRunTool,
118
+ };
package/src/config.js CHANGED
@@ -2,7 +2,14 @@ const fs = require("fs");
2
2
  const os = require("os");
3
3
  const path = require("path");
4
4
 
5
- const UCODE_FIELDS = ["ucodeProvider", "ucodeModel", "ucodeBaseUrl", "ucodeApiKey", "ucodeAgentDir"];
5
+ const UCODE_FIELDS = [
6
+ "ucodeProvider",
7
+ "ucodeModel",
8
+ "ucodeBaseUrl",
9
+ "ucodeApiKey",
10
+ "ucodeAgentDir",
11
+ "ucodeThinking",
12
+ ];
6
13
 
7
14
  const SETTINGS_MODEL_DEFAULTS = Object.freeze({
8
15
  agent: Object.freeze({
@@ -47,6 +54,7 @@ const DEFAULT_UCODE_CONFIG = {
47
54
  ucodeBaseUrl: "",
48
55
  ucodeApiKey: "",
49
56
  ucodeAgentDir: "",
57
+ ucodeThinking: "",
50
58
  };
51
59
 
52
60
  function normalizeLaunchMode(value) {
@@ -251,6 +259,7 @@ function loadGlobalUcodeConfig() {
251
259
  ucodeBaseUrl: typeof raw.ucodeBaseUrl === "string" ? raw.ucodeBaseUrl : "",
252
260
  ucodeApiKey: typeof raw.ucodeApiKey === "string" ? raw.ucodeApiKey : "",
253
261
  ucodeAgentDir: typeof raw.ucodeAgentDir === "string" ? raw.ucodeAgentDir : "",
262
+ ucodeThinking: typeof raw.ucodeThinking === "string" ? raw.ucodeThinking : "",
254
263
  };
255
264
  }
256
265