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
|
@@ -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:
|
|
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.
|
|
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
|
};
|
package/src/code/sessionStore.js
CHANGED
|
@@ -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 = [
|
|
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
|
|