u-foo 2.5.14 → 3.0.0
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/environment.js +20 -8
- package/src/code/agent.js +517 -112
- package/src/code/commands.js +77 -0
- package/src/code/context/artifactGc.js +292 -0
- package/src/code/context/artifactIndex.js +161 -0
- package/src/code/context/artifacts.js +183 -0
- package/src/code/context/assembler.js +703 -0
- package/src/code/context/executionSegment.js +292 -0
- package/src/code/context/index.js +28 -0
- package/src/code/context/planGraph.js +1410 -0
- package/src/code/context/planGraphService.js +857 -0
- package/src/code/context/planMode.js +398 -0
- package/src/code/context/planProjection.js +432 -0
- package/src/code/context/projectSnapshot.js +201 -0
- package/src/code/context/promptLayers.js +175 -0
- package/src/code/context/reducers.js +328 -0
- package/src/code/context/stableJson.js +29 -0
- package/src/code/context/stateCommit.js +414 -0
- package/src/code/context/toolRuntime.js +172 -0
- package/src/code/context/transcript.js +182 -0
- package/src/code/context/transcriptSync.js +106 -0
- package/src/code/context/userInteraction.js +457 -0
- package/src/code/context/userNudge.js +116 -0
- package/src/code/context/workingSet.js +323 -0
- package/src/code/dispatch.js +20 -1
- package/src/code/index.js +8 -0
- package/src/code/modelCommand.js +87 -0
- package/src/code/nativeRunner.js +625 -34
- package/src/code/repl.js +196 -50
- package/src/code/runtime/agentWakeup.js +58 -0
- package/src/code/runtime/graphOwner.js +41 -0
- package/src/code/runtime/graphYieldRouter.js +42 -0
- package/src/code/runtime/index.js +15 -0
- package/src/code/runtime/loopMailbox.js +124 -0
- package/src/code/runtime/runtimeEvents.js +39 -0
- package/src/code/runtime/taskControl.js +565 -0
- package/src/code/runtime/taskFocus.js +165 -0
- package/src/code/runtime/taskLoop.js +383 -0
- package/src/code/runtime/taskRun.js +187 -0
- package/src/code/runtime/toolProvenance.js +70 -0
- package/src/code/runtime/workspaceLease.js +208 -0
- package/src/code/sessionStore.js +217 -15
- package/src/code/skills/index.js +10 -0
- package/src/code/skills/injection.js +66 -3
- package/src/code/skills/loader.js +21 -0
- package/src/code/skills/manifest.js +87 -0
- package/src/code/skills/render.js +15 -1
- package/src/code/taskDecomposer.js +56 -2
- package/src/code/tools/artifactRead.js +40 -0
- package/src/code/tools/askUser.js +11 -0
- package/src/code/tools/planGraph.js +29 -0
- package/src/code/tui.js +2 -0
- package/src/code/usageStore.js +15 -0
- package/src/ui/format/index.js +285 -45
- package/src/ui/format/markdownRenderer.js +436 -71
- package/src/ui/ink/ChatApp.js +39 -8
- package/src/ui/ink/UcodeApp.js +592 -43
- package/src/ui/ink/chatLogModel.js +102 -21
package/src/code/agent.js
CHANGED
|
@@ -10,8 +10,36 @@ const {
|
|
|
10
10
|
saveSessionSnapshot,
|
|
11
11
|
loadSessionSnapshot,
|
|
12
12
|
} = require("./sessionStore");
|
|
13
|
-
const { buildPromptContext } = require("../agents/prompts/native");
|
|
14
13
|
const { buildSkillInjections } = require("./skills");
|
|
14
|
+
const {
|
|
15
|
+
assembleModelContext,
|
|
16
|
+
syncMessagesToTranscript,
|
|
17
|
+
applyContextSideEffects,
|
|
18
|
+
ensureProjectSnapshot,
|
|
19
|
+
recordToolCallInSession,
|
|
20
|
+
commitAfterSegmentEnd,
|
|
21
|
+
sanitizeModelMessages,
|
|
22
|
+
} = require("./context/assembler");
|
|
23
|
+
const { buildLayeredSystemPrompt } = require("./context/promptLayers");
|
|
24
|
+
const {
|
|
25
|
+
createProjectPreflightContextV2,
|
|
26
|
+
} = require("./context/projectSnapshot");
|
|
27
|
+
const {
|
|
28
|
+
ensureTaskContract,
|
|
29
|
+
ensureStateEpoch,
|
|
30
|
+
parseStructuredSideEffects,
|
|
31
|
+
patchTaskContractFromUserMessage,
|
|
32
|
+
} = require("./context/stateCommit");
|
|
33
|
+
const { applyWorkingSetPlan } = require("./context/workingSet");
|
|
34
|
+
const {
|
|
35
|
+
normalizePlanGraphCommand,
|
|
36
|
+
runPlanGraphCommand,
|
|
37
|
+
} = require("./context/planGraphService");
|
|
38
|
+
const {
|
|
39
|
+
shouldFrameAsUserReminder,
|
|
40
|
+
buildContinuationUserPrompt,
|
|
41
|
+
clearUserPrompts,
|
|
42
|
+
} = require("./context/userNudge");
|
|
15
43
|
const {
|
|
16
44
|
runUbusCommand,
|
|
17
45
|
parseBusCheckOutput,
|
|
@@ -34,6 +62,164 @@ const {
|
|
|
34
62
|
parseAgentArgs,
|
|
35
63
|
} = require("./repl");
|
|
36
64
|
|
|
65
|
+
function ensureContextSessionState(state = {}) {
|
|
66
|
+
if (!Array.isArray(state.workingSet)) state.workingSet = [];
|
|
67
|
+
if (!state.executionState || typeof state.executionState !== "object") {
|
|
68
|
+
const { emptyExecutionState } = require("./context/executionSegment");
|
|
69
|
+
state.executionState = emptyExecutionState();
|
|
70
|
+
}
|
|
71
|
+
if (typeof state.executionState.planMode !== "boolean") {
|
|
72
|
+
state.executionState.planMode = false;
|
|
73
|
+
}
|
|
74
|
+
if (!Array.isArray(state.executionState.pendingUserPrompts)) {
|
|
75
|
+
state.executionState.pendingUserPrompts = [];
|
|
76
|
+
}
|
|
77
|
+
if (!state.executionState.planGraph || typeof state.executionState.planGraph !== "object") {
|
|
78
|
+
state.executionState.planGraph = require("./context/planGraphService").emptyPlanGraphState();
|
|
79
|
+
}
|
|
80
|
+
require("./context/planProjection").ensurePlanUiState(state.executionState);
|
|
81
|
+
if (!state.contextPolicy || typeof state.contextPolicy !== "object") {
|
|
82
|
+
const { defaultContextPolicy } = require("./context/assembler");
|
|
83
|
+
state.contextPolicy = defaultContextPolicy();
|
|
84
|
+
}
|
|
85
|
+
if (!Number.isFinite(state.toolCallsSinceCommit)) state.toolCallsSinceCommit = 0;
|
|
86
|
+
ensureStateEpoch(state);
|
|
87
|
+
return state;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function buildSkillBodyBlocks(skillInjections = {}) {
|
|
91
|
+
const blocks = Array.isArray(skillInjections.blocks) ? skillInjections.blocks : [];
|
|
92
|
+
return blocks.map((block) => {
|
|
93
|
+
const text = String(block || "");
|
|
94
|
+
if (text.includes("<active_skill>")) return text;
|
|
95
|
+
return text.replace(/^<skill>/, "<active_skill>").replace(/<\/skill>/, "</active_skill>");
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function runPlanGraphSteps({
|
|
100
|
+
command = null,
|
|
101
|
+
segment = null,
|
|
102
|
+
workspaceRoot = process.cwd(),
|
|
103
|
+
sessionId = "",
|
|
104
|
+
state = {},
|
|
105
|
+
pushToolLog = () => null,
|
|
106
|
+
} = {}) {
|
|
107
|
+
if (!state.executionState || typeof state.executionState !== "object") {
|
|
108
|
+
state.executionState = require("./context/executionSegment").emptyExecutionState();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const normalized = command
|
|
112
|
+
|| (segment ? normalizePlanGraphCommand(segment) : null);
|
|
113
|
+
if (!normalized) {
|
|
114
|
+
return { ok: false, error: "missing plan_graph command" };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const result = runPlanGraphCommand(normalized, {
|
|
118
|
+
executionState: state.executionState,
|
|
119
|
+
autoAdvance: true,
|
|
120
|
+
parallel: true,
|
|
121
|
+
runTool: ({ node, args, tool, stepId }) => {
|
|
122
|
+
pushToolLog({
|
|
123
|
+
tool,
|
|
124
|
+
phase: "start",
|
|
125
|
+
args,
|
|
126
|
+
error: "",
|
|
127
|
+
origin: {
|
|
128
|
+
kind: "plan_graph",
|
|
129
|
+
graphRevision: Number(state.executionState.planGraph && state.executionState.planGraph.revision) || 0,
|
|
130
|
+
nodeId: stepId || (node && node.id) || "",
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
const { runToolCall: dispatchToolCall } = require("./dispatch");
|
|
134
|
+
const { persistToolResultToContext } = require("./context/assembler");
|
|
135
|
+
const toolResult = dispatchToolCall(
|
|
136
|
+
{ tool, args },
|
|
137
|
+
{ workspaceRoot, cwd: workspaceRoot, sessionId },
|
|
138
|
+
);
|
|
139
|
+
if (!toolResult || toolResult.ok === false) {
|
|
140
|
+
pushToolLog({
|
|
141
|
+
tool,
|
|
142
|
+
phase: "error",
|
|
143
|
+
args,
|
|
144
|
+
error: String((toolResult && toolResult.error) || "tool failed"),
|
|
145
|
+
origin: {
|
|
146
|
+
kind: "plan_graph",
|
|
147
|
+
nodeId: stepId || (node && node.id) || "",
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
return toolResult;
|
|
151
|
+
}
|
|
152
|
+
const persisted = persistToolResultToContext({
|
|
153
|
+
workspaceRoot,
|
|
154
|
+
sessionId,
|
|
155
|
+
tool,
|
|
156
|
+
args,
|
|
157
|
+
rawResult: toolResult,
|
|
158
|
+
});
|
|
159
|
+
recordToolCallInSession(state, persisted, workspaceRoot);
|
|
160
|
+
const plan = require("./context/workingSet").defaultContextPlanFromToolEvent(
|
|
161
|
+
tool,
|
|
162
|
+
persisted.artifactId || (persisted.modelPayload && persisted.modelPayload.artifactId),
|
|
163
|
+
args,
|
|
164
|
+
);
|
|
165
|
+
if (plan) state.workingSet = applyWorkingSetPlan(state.workingSet, plan, state);
|
|
166
|
+
if ((tool === "write" || tool === "edit") && args && args.path) {
|
|
167
|
+
const filePath = String(args.path);
|
|
168
|
+
const files = Array.isArray(state.executionState.modifiedFiles)
|
|
169
|
+
? state.executionState.modifiedFiles.slice()
|
|
170
|
+
: [];
|
|
171
|
+
if (!files.includes(filePath)) files.push(filePath);
|
|
172
|
+
state.executionState.modifiedFiles = files;
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
...(persisted.modelPayload || toolResult),
|
|
176
|
+
origin: {
|
|
177
|
+
kind: "plan_graph",
|
|
178
|
+
graphRevision: Number(state.executionState.planGraph && state.executionState.planGraph.revision) || 0,
|
|
179
|
+
nodeId: stepId || (node && node.id) || "",
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
state.executionState = result.executionState || state.executionState;
|
|
186
|
+
commitAfterSegmentEnd(state, {
|
|
187
|
+
ok: result.status === "accepted",
|
|
188
|
+
segmentId: result.graphId || "",
|
|
189
|
+
error: result.status === "accepted" ? "" : "plan_graph rejected",
|
|
190
|
+
stoppedAt: result.stoppedAt || "",
|
|
191
|
+
}, workspaceRoot);
|
|
192
|
+
return {
|
|
193
|
+
ok: result.status === "accepted",
|
|
194
|
+
graphId: result.graphId || "",
|
|
195
|
+
error: result.status === "accepted"
|
|
196
|
+
? ""
|
|
197
|
+
: (Array.isArray(result.errors) ? result.errors.map((e) => e.message || e.code).join("; ") : "plan_graph rejected"),
|
|
198
|
+
stoppedAt: result.stoppedAt || "",
|
|
199
|
+
modelPayload: result.modelPayload || result,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function runExecutionSegmentSteps({
|
|
204
|
+
segment = {},
|
|
205
|
+
workspaceRoot = process.cwd(),
|
|
206
|
+
sessionId = "",
|
|
207
|
+
state = {},
|
|
208
|
+
pushToolLog = () => null,
|
|
209
|
+
} = {}) {
|
|
210
|
+
return runPlanGraphSteps({
|
|
211
|
+
command: normalizePlanGraphCommand(segment) || normalizePlanGraphCommand({
|
|
212
|
+
type: "execution_segment",
|
|
213
|
+
...segment,
|
|
214
|
+
}),
|
|
215
|
+
workspaceRoot,
|
|
216
|
+
sessionId,
|
|
217
|
+
state,
|
|
218
|
+
pushToolLog,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
|
|
37
223
|
function readTextOrFile(value = "") {
|
|
38
224
|
const raw = String(value || "").trim();
|
|
39
225
|
if (!raw) return "";
|
|
@@ -137,14 +323,14 @@ function isCliCancelledError(message = "") {
|
|
|
137
323
|
}
|
|
138
324
|
|
|
139
325
|
function computeExtendedTimeout(baseTimeoutMs) {
|
|
140
|
-
const base = Number.isFinite(baseTimeoutMs) ? Math.max(1000, Math.floor(baseTimeoutMs)) :
|
|
141
|
-
return Math.min(
|
|
326
|
+
const base = Number.isFinite(baseTimeoutMs) ? Math.max(1000, Math.floor(baseTimeoutMs)) : 43200000;
|
|
327
|
+
return Math.min(43200000, Math.max(base * 2, base + 120000));
|
|
142
328
|
}
|
|
143
329
|
|
|
144
330
|
// Reasoning models routinely blew the old 10min budget across a multi-turn
|
|
145
|
-
// tool loop. Total per-task budget defaults to
|
|
331
|
+
// tool loop. Total per-task budget defaults to 12h and can be raised per
|
|
146
332
|
// call, via --timeout-ms, or via UFOO_UCODE_TASK_TIMEOUT_MS.
|
|
147
|
-
const DEFAULT_NL_TASK_TIMEOUT_MS =
|
|
333
|
+
const DEFAULT_NL_TASK_TIMEOUT_MS = 43200000;
|
|
148
334
|
|
|
149
335
|
function resolveNlTaskTimeoutMs(value) {
|
|
150
336
|
if (Number.isFinite(value) && value > 0) return Math.max(1000, Math.floor(value));
|
|
@@ -190,7 +376,7 @@ function normalizeToolLogEvent(event = {}) {
|
|
|
190
376
|
if (!event || typeof event !== "object") return null;
|
|
191
377
|
const tool = String(event.tool || event.name || "").trim().toLowerCase();
|
|
192
378
|
if (!tool) return null;
|
|
193
|
-
if (tool !== "read" && tool !== "write" && tool !== "edit" && tool !== "bash") return null;
|
|
379
|
+
if (tool !== "read" && tool !== "write" && tool !== "edit" && tool !== "bash" && tool !== "artifact_read") return null;
|
|
194
380
|
const phase = String(event.phase || "update").trim().toLowerCase();
|
|
195
381
|
const normalizedPhase = phase === "error" ? "error" : (phase === "start" ? "start" : "");
|
|
196
382
|
if (!normalizedPhase) return null;
|
|
@@ -247,7 +433,7 @@ function pushSkillWarning(logs = [], onToolLog = null, warning = "") {
|
|
|
247
433
|
|
|
248
434
|
function stripSkillBlocksFromText(value = "") {
|
|
249
435
|
return String(value || "")
|
|
250
|
-
.replace(/<skill>\s*[\s\S]*?<\/skill>\s*/
|
|
436
|
+
.replace(/<(?:active_)?skill>\s*[\s\S]*?<\/(?:active_)?skill>\s*/gi, "")
|
|
251
437
|
.trim();
|
|
252
438
|
}
|
|
253
439
|
|
|
@@ -286,94 +472,6 @@ function isProjectAnalysisTask(task = "") {
|
|
|
286
472
|
return /(?:analy[sz]e|analysis|review|audit|status|architecture|codebase|repo|project|现状|架构|审查|分析|项目|代码库)/i.test(text);
|
|
287
473
|
}
|
|
288
474
|
|
|
289
|
-
function createProjectPreflightContext({
|
|
290
|
-
workspaceRoot = process.cwd(),
|
|
291
|
-
pushToolLog = () => null,
|
|
292
|
-
} = {}) {
|
|
293
|
-
const root = String(workspaceRoot || process.cwd());
|
|
294
|
-
const readCandidates = [
|
|
295
|
-
"AGENTS.md",
|
|
296
|
-
"README.md",
|
|
297
|
-
"README.zh-CN.md",
|
|
298
|
-
"package.json",
|
|
299
|
-
];
|
|
300
|
-
const blocks = [];
|
|
301
|
-
|
|
302
|
-
for (const relPath of readCandidates) {
|
|
303
|
-
pushToolLog({
|
|
304
|
-
tool: "read",
|
|
305
|
-
phase: "start",
|
|
306
|
-
args: { path: relPath },
|
|
307
|
-
error: "",
|
|
308
|
-
});
|
|
309
|
-
const readRes = runToolCall(
|
|
310
|
-
{
|
|
311
|
-
tool: "read",
|
|
312
|
-
args: { path: relPath, maxBytes: 12000 },
|
|
313
|
-
},
|
|
314
|
-
{
|
|
315
|
-
workspaceRoot: root,
|
|
316
|
-
cwd: root,
|
|
317
|
-
}
|
|
318
|
-
);
|
|
319
|
-
pushToolLog({
|
|
320
|
-
tool: "read",
|
|
321
|
-
phase: readRes && readRes.ok === false ? "error" : "",
|
|
322
|
-
args: { path: relPath },
|
|
323
|
-
error: readRes && readRes.ok === false ? String(readRes.error || "") : "",
|
|
324
|
-
});
|
|
325
|
-
if (!readRes || readRes.ok === false) continue;
|
|
326
|
-
const content = String(readRes.content || "").trim();
|
|
327
|
-
if (!content) continue;
|
|
328
|
-
const clipped = content.length > 2400
|
|
329
|
-
? `${content.slice(0, 2400)}\n...[truncated]`
|
|
330
|
-
: content;
|
|
331
|
-
blocks.push(`File: ${relPath}\n${clipped}`);
|
|
332
|
-
if (blocks.length >= 2) break;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
if (blocks.length === 0) {
|
|
336
|
-
const command = "ls -la";
|
|
337
|
-
pushToolLog({
|
|
338
|
-
tool: "bash",
|
|
339
|
-
phase: "start",
|
|
340
|
-
args: { command },
|
|
341
|
-
error: "",
|
|
342
|
-
});
|
|
343
|
-
const bashRes = runToolCall(
|
|
344
|
-
{
|
|
345
|
-
tool: "bash",
|
|
346
|
-
args: { command, timeoutMs: 4000 },
|
|
347
|
-
},
|
|
348
|
-
{
|
|
349
|
-
workspaceRoot: root,
|
|
350
|
-
cwd: root,
|
|
351
|
-
}
|
|
352
|
-
);
|
|
353
|
-
pushToolLog({
|
|
354
|
-
tool: "bash",
|
|
355
|
-
phase: bashRes && bashRes.ok === false ? "error" : "",
|
|
356
|
-
args: { command },
|
|
357
|
-
error: bashRes && bashRes.ok === false ? String(bashRes.error || "") : "",
|
|
358
|
-
});
|
|
359
|
-
if (bashRes && bashRes.ok !== false) {
|
|
360
|
-
const stdout = String(bashRes.stdout || "").trim();
|
|
361
|
-
const clipped = stdout.length > 1200
|
|
362
|
-
? `${stdout.slice(0, 1200)}\n...[truncated]`
|
|
363
|
-
: stdout;
|
|
364
|
-
if (clipped) {
|
|
365
|
-
blocks.push(`Command: ${command}\n${clipped}`);
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
if (blocks.length === 0) return "";
|
|
371
|
-
return [
|
|
372
|
-
"Preflight snapshot (captured by ucode):",
|
|
373
|
-
...blocks.map((block) => `---\n${block}`),
|
|
374
|
-
].join("\n");
|
|
375
|
-
}
|
|
376
|
-
|
|
377
475
|
function buildNlFallbackSummary(logs = []) {
|
|
378
476
|
const list = Array.isArray(logs) ? logs : [];
|
|
379
477
|
const started = list.filter((entry) => entry && entry.phase === "start").length;
|
|
@@ -427,28 +525,59 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
|
|
|
427
525
|
const useDecomposition = isBugFixTask && !options.disableDecomposition;
|
|
428
526
|
const analysisTask = isProjectAnalysisTask(taskText);
|
|
429
527
|
const workspaceRoot = String(state.workspaceRoot || process.cwd());
|
|
430
|
-
|
|
431
|
-
|
|
528
|
+
ensureContextSessionState(state);
|
|
529
|
+
|
|
530
|
+
let projectSnapshot = state.projectSnapshot || null;
|
|
531
|
+
if (analysisTask) {
|
|
532
|
+
projectSnapshot = createProjectPreflightContextV2({
|
|
432
533
|
workspaceRoot,
|
|
534
|
+
sessionId: String(state.sessionId || ""),
|
|
433
535
|
pushToolLog,
|
|
434
|
-
|
|
435
|
-
|
|
536
|
+
existingSnapshot: state.projectSnapshot,
|
|
537
|
+
});
|
|
538
|
+
state.projectSnapshot = projectSnapshot;
|
|
539
|
+
} else {
|
|
540
|
+
projectSnapshot = ensureProjectSnapshot(state, workspaceRoot);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
ensureTaskContract(state, taskText);
|
|
544
|
+
if (!shouldFrameAsUserReminder(state.executionState)) {
|
|
545
|
+
state.taskContract = patchTaskContractFromUserMessage(state.taskContract, taskText);
|
|
546
|
+
}
|
|
547
|
+
|
|
436
548
|
const taskPrompt = analysisTask
|
|
437
549
|
? `${taskText}\n\nAnalysis requirements:\n- Inspect repository evidence before concluding.\n- Cite concrete file observations.\n- Keep findings concise and actionable.`
|
|
438
550
|
: taskText;
|
|
439
551
|
const skillInjections = buildSkillInjections({
|
|
440
552
|
prompt: taskPrompt,
|
|
441
553
|
workspaceRoot,
|
|
554
|
+
sessionId: String(state.sessionId || ""),
|
|
555
|
+
persistBodies: true,
|
|
556
|
+
useActiveSkillTag: true,
|
|
442
557
|
});
|
|
443
558
|
for (const warning of skillInjections.warnings || []) {
|
|
444
559
|
pushSkillWarning(logs, onToolLog, warning);
|
|
445
560
|
}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
const
|
|
450
|
-
|
|
451
|
-
|
|
561
|
+
if (Array.isArray(skillInjections.activeSkills) && skillInjections.activeSkills.length > 0) {
|
|
562
|
+
state.activeSkills = skillInjections.activeSkills;
|
|
563
|
+
}
|
|
564
|
+
const skillBodyBlocks = buildSkillBodyBlocks(skillInjections);
|
|
565
|
+
// Skill body goes into turnDynamic (system layered prompt) to avoid
|
|
566
|
+
// double injection and mixed system/user privilege semantics.
|
|
567
|
+
let effectiveTaskPrompt = taskPrompt;
|
|
568
|
+
if (shouldFrameAsUserReminder(state.executionState)) {
|
|
569
|
+
effectiveTaskPrompt = buildContinuationUserPrompt(effectiveTaskPrompt, state.executionState);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const assembled = assembleModelContext(state, {
|
|
573
|
+
workspaceRoot,
|
|
574
|
+
model,
|
|
575
|
+
provider,
|
|
576
|
+
turnDynamic: skillBodyBlocks.join("\n\n"),
|
|
577
|
+
latestUserMessage: effectiveTaskPrompt,
|
|
578
|
+
});
|
|
579
|
+
const systemContext = assembled.systemPrompt;
|
|
580
|
+
state.summary = assembled.summary || state.summary;
|
|
452
581
|
|
|
453
582
|
const onStream = onDelta
|
|
454
583
|
? (delta) => {
|
|
@@ -468,20 +597,28 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
|
|
|
468
597
|
: runNativeAgentTask;
|
|
469
598
|
const onPhase = typeof options.onPhase === "function" ? options.onPhase : null;
|
|
470
599
|
const onThinkingDelta = typeof options.onThinkingDelta === "function" ? options.onThinkingDelta : null;
|
|
600
|
+
let lastTranscriptBaseline = 0;
|
|
471
601
|
const invokeNative = (sessionIdValue = "", timeoutOverrideMs = timeoutMs) => {
|
|
472
602
|
toolEventsThisAttempt = 0;
|
|
603
|
+
const historyMessages = assembled.messages;
|
|
604
|
+
// Sanitized length matches what nativeRunner clones before appending this
|
|
605
|
+
// turn's user/tool/assistant messages — used as the transcript sync baseline.
|
|
606
|
+
lastTranscriptBaseline = sanitizeModelMessages(historyMessages).length;
|
|
473
607
|
return runNativeAgentImpl({
|
|
474
608
|
workspaceRoot,
|
|
475
609
|
provider,
|
|
476
610
|
model,
|
|
477
611
|
prompt: effectiveTaskPrompt,
|
|
478
612
|
systemPrompt: systemContext,
|
|
479
|
-
|
|
480
|
-
|
|
613
|
+
systemBlocks: assembled.systemBlocks || null,
|
|
614
|
+
messages: historyMessages,
|
|
615
|
+
sessionId: String(sessionIdValue || state.sessionId || ""),
|
|
481
616
|
timeoutMs: timeoutOverrideMs,
|
|
482
617
|
onStreamDelta: onStream,
|
|
483
618
|
onThinkingDelta,
|
|
484
619
|
onPhase,
|
|
620
|
+
executionState: state.executionState || null,
|
|
621
|
+
onArtifactPersisted: (persisted) => recordToolCallInSession(state, persisted, workspaceRoot),
|
|
485
622
|
onToolEvent: (event) => {
|
|
486
623
|
toolEventsThisAttempt += 1;
|
|
487
624
|
pushToolLog(event);
|
|
@@ -493,6 +630,31 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
|
|
|
493
630
|
try {
|
|
494
631
|
let cliRes;
|
|
495
632
|
|
|
633
|
+
const requestedPlan = normalizePlanGraphCommand(
|
|
634
|
+
options.executionSegment || options.nextSegment || options.planGraph || null,
|
|
635
|
+
);
|
|
636
|
+
if (requestedPlan) {
|
|
637
|
+
const planResult = await runPlanGraphSteps({
|
|
638
|
+
command: requestedPlan,
|
|
639
|
+
workspaceRoot,
|
|
640
|
+
sessionId: String(state.sessionId || ""),
|
|
641
|
+
state,
|
|
642
|
+
pushToolLog,
|
|
643
|
+
});
|
|
644
|
+
if (!planResult.ok) {
|
|
645
|
+
return {
|
|
646
|
+
ok: false,
|
|
647
|
+
summary: "",
|
|
648
|
+
artifacts: [],
|
|
649
|
+
logs: logs.slice(),
|
|
650
|
+
error: planResult.error,
|
|
651
|
+
metrics: {},
|
|
652
|
+
streamed: false,
|
|
653
|
+
streamLastChar: "",
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
496
658
|
// Use decomposed runner for bug fix tasks
|
|
497
659
|
if (useDecomposition) {
|
|
498
660
|
const decomposedResult = await runDecomposedTask({
|
|
@@ -506,6 +668,8 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
|
|
|
506
668
|
systemPrompt: systemContext,
|
|
507
669
|
messages: Array.isArray(state.nlMessages) ? state.nlMessages : [],
|
|
508
670
|
sessionId: String(state.sessionId || ""),
|
|
671
|
+
state,
|
|
672
|
+
systemBlocks: assembled.systemBlocks || null,
|
|
509
673
|
});
|
|
510
674
|
|
|
511
675
|
if (decomposedResult.ok) {
|
|
@@ -538,6 +702,9 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
|
|
|
538
702
|
|
|
539
703
|
if (!cliRes || cliRes.ok === false) {
|
|
540
704
|
const errMsg = String((cliRes && cliRes.error) || "");
|
|
705
|
+
if (isCliCancelledError(errMsg) && state.executionState) {
|
|
706
|
+
clearUserPrompts(state.executionState);
|
|
707
|
+
}
|
|
541
708
|
return {
|
|
542
709
|
ok: false,
|
|
543
710
|
summary: "",
|
|
@@ -553,16 +720,70 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
|
|
|
553
720
|
if (cliRes && typeof cliRes.sessionId === "string" && cliRes.sessionId.trim()) {
|
|
554
721
|
state.sessionId = cliRes.sessionId.trim();
|
|
555
722
|
}
|
|
723
|
+
if (cliRes && cliRes.executionState && typeof cliRes.executionState === "object") {
|
|
724
|
+
// Preserve planMode if the runner returned a fresh empty state without it.
|
|
725
|
+
const priorPlanMode = Boolean(state.executionState && state.executionState.planMode);
|
|
726
|
+
const priorSource = state.executionState && state.executionState.planModeSource
|
|
727
|
+
? String(state.executionState.planModeSource)
|
|
728
|
+
: "";
|
|
729
|
+
state.executionState = cliRes.executionState;
|
|
730
|
+
if (typeof state.executionState.planMode !== "boolean") {
|
|
731
|
+
state.executionState.planMode = priorPlanMode;
|
|
732
|
+
}
|
|
733
|
+
if (!state.executionState.planModeSource && priorSource) {
|
|
734
|
+
state.executionState.planModeSource = priorSource;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
556
737
|
if (cliRes && Array.isArray(cliRes.messages)) {
|
|
557
|
-
|
|
738
|
+
// Sync first so ensureTranscript does not migrate the just-assigned
|
|
739
|
+
// nlMessages and then append the same delta again.
|
|
740
|
+
syncMessagesToTranscript(state, cliRes.messages, workspaceRoot, {
|
|
741
|
+
baselineCount: lastTranscriptBaseline,
|
|
742
|
+
});
|
|
743
|
+
state.nlMessages = stripSkillBlocksFromMessages(
|
|
744
|
+
Array.isArray(state.nlMessages) && state.nlMessages.length > 0
|
|
745
|
+
? state.nlMessages
|
|
746
|
+
: cliRes.messages,
|
|
747
|
+
);
|
|
558
748
|
}
|
|
559
749
|
const normalized = String(cliRes.output || "").trim();
|
|
750
|
+
const sideEffects = parseStructuredSideEffects(normalized);
|
|
751
|
+
if (sideEffects) {
|
|
752
|
+
applyContextSideEffects(state, sideEffects);
|
|
753
|
+
const planCommand = normalizePlanGraphCommand(sideEffects);
|
|
754
|
+
if (planCommand) {
|
|
755
|
+
await runPlanGraphSteps({
|
|
756
|
+
command: planCommand,
|
|
757
|
+
workspaceRoot,
|
|
758
|
+
sessionId: String(state.sessionId || ""),
|
|
759
|
+
state,
|
|
760
|
+
pushToolLog,
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
}
|
|
560
764
|
const summary = extractJsonSummary(normalized);
|
|
561
765
|
const resolvedSummary = String(summary || "").trim() || buildNlFallbackSummary(logs);
|
|
766
|
+
const artifactIds = Array.isArray(state.workingSet)
|
|
767
|
+
? state.workingSet.map((entry) => entry.artifactId).filter(Boolean)
|
|
768
|
+
: [];
|
|
769
|
+
if (cliRes && cliRes.waitingUserInteraction) {
|
|
770
|
+
return {
|
|
771
|
+
ok: true,
|
|
772
|
+
summary: resolvedSummary || "Waiting for your reply",
|
|
773
|
+
artifacts: artifactIds,
|
|
774
|
+
logs: logs.slice(),
|
|
775
|
+
error: "",
|
|
776
|
+
metrics: {},
|
|
777
|
+
streamed: Boolean(streamed || cliRes.streamed),
|
|
778
|
+
streamLastChar,
|
|
779
|
+
waitingUserInteraction: true,
|
|
780
|
+
interactionId: cliRes.interactionId || "",
|
|
781
|
+
};
|
|
782
|
+
}
|
|
562
783
|
return {
|
|
563
784
|
ok: true,
|
|
564
785
|
summary: resolvedSummary,
|
|
565
|
-
artifacts:
|
|
786
|
+
artifacts: artifactIds,
|
|
566
787
|
logs: logs.slice(),
|
|
567
788
|
error: "",
|
|
568
789
|
metrics: {},
|
|
@@ -626,8 +847,7 @@ function buildNlContext({
|
|
|
626
847
|
|| readTextOrFile(process.env.UFOO_UCODE_PROMPT_FILE)
|
|
627
848
|
|| "";
|
|
628
849
|
|
|
629
|
-
|
|
630
|
-
return clampContext(buildPromptContext({
|
|
850
|
+
return clampContext(resolveWireSystemPrompt({
|
|
631
851
|
workspaceRoot: workspaceRoot || process.cwd(),
|
|
632
852
|
model,
|
|
633
853
|
provider,
|
|
@@ -635,6 +855,32 @@ function buildNlContext({
|
|
|
635
855
|
}));
|
|
636
856
|
}
|
|
637
857
|
|
|
858
|
+
/**
|
|
859
|
+
* Single wire entry for system prompt assembly (layered Context Manager).
|
|
860
|
+
*/
|
|
861
|
+
function resolveWireSystemPrompt({
|
|
862
|
+
workspaceRoot = process.cwd(),
|
|
863
|
+
model = "",
|
|
864
|
+
provider = "",
|
|
865
|
+
appendSystemPrompt = "",
|
|
866
|
+
overrideSystemPrompt = "",
|
|
867
|
+
epochDynamic = "",
|
|
868
|
+
turnDynamic = "",
|
|
869
|
+
sessionStableExtras = "",
|
|
870
|
+
} = {}) {
|
|
871
|
+
if (overrideSystemPrompt) return String(overrideSystemPrompt);
|
|
872
|
+
|
|
873
|
+
return buildLayeredSystemPrompt({
|
|
874
|
+
workspaceRoot,
|
|
875
|
+
model,
|
|
876
|
+
provider,
|
|
877
|
+
appendSystemPrompt,
|
|
878
|
+
epochDynamic,
|
|
879
|
+
turnDynamic,
|
|
880
|
+
sessionStableExtras,
|
|
881
|
+
}).flatText;
|
|
882
|
+
}
|
|
883
|
+
|
|
638
884
|
function buildSessionSnapshotFromState(state = {}) {
|
|
639
885
|
const source = state && typeof state === "object" ? state : {};
|
|
640
886
|
return {
|
|
@@ -645,6 +891,27 @@ function buildSessionSnapshotFromState(state = {}) {
|
|
|
645
891
|
context: String(source.context || ""),
|
|
646
892
|
nlMessages: Array.isArray(source.nlMessages) ? source.nlMessages : [],
|
|
647
893
|
createdAt: String(source.sessionCreatedAt || "").trim(),
|
|
894
|
+
summary: String(source.summary || "").trim(),
|
|
895
|
+
projectSnapshot: source.projectSnapshot && typeof source.projectSnapshot === "object"
|
|
896
|
+
? source.projectSnapshot
|
|
897
|
+
: null,
|
|
898
|
+
taskContract: source.taskContract && typeof source.taskContract === "object"
|
|
899
|
+
? source.taskContract
|
|
900
|
+
: null,
|
|
901
|
+
stateEpoch: source.stateEpoch && typeof source.stateEpoch === "object"
|
|
902
|
+
? source.stateEpoch
|
|
903
|
+
: null,
|
|
904
|
+
workingSet: Array.isArray(source.workingSet) ? source.workingSet : [],
|
|
905
|
+
executionState: source.executionState && typeof source.executionState === "object"
|
|
906
|
+
? source.executionState
|
|
907
|
+
: null,
|
|
908
|
+
contextPolicy: source.contextPolicy && typeof source.contextPolicy === "object"
|
|
909
|
+
? source.contextPolicy
|
|
910
|
+
: null,
|
|
911
|
+
toolCallsSinceCommit: Number.isFinite(source.toolCallsSinceCommit)
|
|
912
|
+
? Math.max(0, Math.floor(source.toolCallsSinceCommit))
|
|
913
|
+
: 0,
|
|
914
|
+
activeSkills: Array.isArray(source.activeSkills) ? source.activeSkills : [],
|
|
648
915
|
};
|
|
649
916
|
}
|
|
650
917
|
|
|
@@ -708,12 +975,147 @@ function resumeSessionState(state = {}, sessionId = "", workspaceRoot = process.
|
|
|
708
975
|
state.context = String(snapshot.context || "");
|
|
709
976
|
state.nlMessages = Array.isArray(snapshot.nlMessages) ? snapshot.nlMessages : [];
|
|
710
977
|
state.sessionCreatedAt = String(snapshot.createdAt || "").trim();
|
|
978
|
+
state.summary = String(snapshot.summary || "").trim();
|
|
979
|
+
state.projectSnapshot = snapshot.projectSnapshot || null;
|
|
980
|
+
state.taskContract = snapshot.taskContract || null;
|
|
981
|
+
state.stateEpoch = snapshot.stateEpoch || null;
|
|
982
|
+
state.workingSet = Array.isArray(snapshot.workingSet) ? snapshot.workingSet : [];
|
|
983
|
+
state.executionState = snapshot.executionState || null;
|
|
984
|
+
state.contextPolicy = snapshot.contextPolicy || null;
|
|
985
|
+
state.toolCallsSinceCommit = Number.isFinite(snapshot.toolCallsSinceCommit)
|
|
986
|
+
? snapshot.toolCallsSinceCommit
|
|
987
|
+
: 0;
|
|
988
|
+
state.activeSkills = Array.isArray(snapshot.activeSkills) ? snapshot.activeSkills : [];
|
|
989
|
+
ensureContextSessionState(state);
|
|
990
|
+
const { ensureTranscript } = require("./context/assembler");
|
|
991
|
+
ensureTranscript(state, state.workspaceRoot);
|
|
992
|
+
if (Array.isArray(state.transcriptEvents) && state.transcriptEvents.length > 0) {
|
|
993
|
+
const { transcriptEventsToMessages } = require("./context/transcript");
|
|
994
|
+
state.nlMessages = transcriptEventsToMessages(state.transcriptEvents, { preferArtifact: true });
|
|
995
|
+
}
|
|
711
996
|
|
|
712
997
|
return {
|
|
713
998
|
ok: true,
|
|
714
999
|
error: "",
|
|
715
1000
|
sessionId: state.sessionId,
|
|
716
|
-
restoredMessages: state.nlMessages.length,
|
|
1001
|
+
restoredMessages: Array.isArray(state.nlMessages) ? state.nlMessages.length : 0,
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
/**
|
|
1006
|
+
* Continue after TUI resolves approval/choice/chat.
|
|
1007
|
+
* ask_user: answer is written as the deferred tool_result (contiguous, no question echo).
|
|
1008
|
+
* checkpoint: short answer-only user message referencing interaction/node.
|
|
1009
|
+
*/
|
|
1010
|
+
async function resumeAfterUserInteraction(answerText = "", state = {}, options = {}) {
|
|
1011
|
+
ensureContextSessionState(state);
|
|
1012
|
+
const { resolveUserInteraction } = require("./context/userInteraction");
|
|
1013
|
+
const { appendAnswerToolResult } = require("./nativeRunner");
|
|
1014
|
+
const resolved = resolveUserInteraction(state.executionState, answerText);
|
|
1015
|
+
if (!resolved.ok) {
|
|
1016
|
+
return {
|
|
1017
|
+
ok: false,
|
|
1018
|
+
error: resolved.error || "failed to resolve user interaction",
|
|
1019
|
+
code: resolved.code || "",
|
|
1020
|
+
waitingUserInteraction: true,
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
const logs = [];
|
|
1025
|
+
const pushToolLog = (event) => {
|
|
1026
|
+
try {
|
|
1027
|
+
logs.push(normalizeToolLogEvent(event));
|
|
1028
|
+
} catch { /* ignore */ }
|
|
1029
|
+
};
|
|
1030
|
+
|
|
1031
|
+
let messages = Array.isArray(state.nlMessages) ? state.nlMessages.slice() : [];
|
|
1032
|
+
if (resolved.continueMode === "tool_result" && resolved.resume && resolved.resume.call) {
|
|
1033
|
+
const appended = appendAnswerToolResult(messages, resolved.resume, resolved.answer);
|
|
1034
|
+
if (!appended.ok) {
|
|
1035
|
+
return { ok: false, error: appended.error || "failed to append answer tool_result" };
|
|
1036
|
+
}
|
|
1037
|
+
} else {
|
|
1038
|
+
// Checkpoint / non-tool path: answer-only contiguous user message (no question).
|
|
1039
|
+
messages.push({
|
|
1040
|
+
role: "user",
|
|
1041
|
+
content: JSON.stringify(resolved.answer),
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
state.nlMessages = messages;
|
|
1045
|
+
|
|
1046
|
+
const workspaceRoot = state.workspaceRoot || process.cwd();
|
|
1047
|
+
const assembled = assembleModelContext(state, {
|
|
1048
|
+
workspaceRoot,
|
|
1049
|
+
provider: state.provider,
|
|
1050
|
+
model: state.model,
|
|
1051
|
+
});
|
|
1052
|
+
const systemContext = assembled.systemPrompt || "";
|
|
1053
|
+
|
|
1054
|
+
let streamLastChar = "";
|
|
1055
|
+
const onDelta = typeof options.onDelta === "function" ? options.onDelta : null;
|
|
1056
|
+
const trackingOnDelta = onDelta
|
|
1057
|
+
? (delta) => {
|
|
1058
|
+
const text = String(delta || "");
|
|
1059
|
+
if (text) streamLastChar = text.slice(-1);
|
|
1060
|
+
return onDelta(delta);
|
|
1061
|
+
}
|
|
1062
|
+
: null;
|
|
1063
|
+
|
|
1064
|
+
const cliRes = await runNativeAgentTask({
|
|
1065
|
+
workspaceRoot,
|
|
1066
|
+
provider: state.provider,
|
|
1067
|
+
model: state.model,
|
|
1068
|
+
prompt: "",
|
|
1069
|
+
systemPrompt: systemContext,
|
|
1070
|
+
systemBlocks: assembled.systemBlocks || null,
|
|
1071
|
+
messages,
|
|
1072
|
+
sessionId: String(state.sessionId || ""),
|
|
1073
|
+
onToolEvent: pushToolLog,
|
|
1074
|
+
onStreamDelta: trackingOnDelta,
|
|
1075
|
+
onThinkingDelta: typeof options.onThinkingDelta === "function" ? options.onThinkingDelta : null,
|
|
1076
|
+
onPhase: typeof options.onPhase === "function" ? options.onPhase : null,
|
|
1077
|
+
executionState: state.executionState,
|
|
1078
|
+
signal: options.signal,
|
|
1079
|
+
resume: true,
|
|
1080
|
+
});
|
|
1081
|
+
|
|
1082
|
+
if (cliRes && cliRes.executionState) {
|
|
1083
|
+
state.executionState = cliRes.executionState;
|
|
1084
|
+
}
|
|
1085
|
+
if (cliRes && Array.isArray(cliRes.messages)) {
|
|
1086
|
+
state.nlMessages = stripSkillBlocksFromMessages(cliRes.messages);
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
if (!cliRes || cliRes.ok === false) {
|
|
1090
|
+
return {
|
|
1091
|
+
ok: false,
|
|
1092
|
+
error: (cliRes && cliRes.error) || "resume failed",
|
|
1093
|
+
logs,
|
|
1094
|
+
waitingUserInteraction: false,
|
|
1095
|
+
streamed: false,
|
|
1096
|
+
streamLastChar: "",
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
if (cliRes.waitingUserInteraction) {
|
|
1101
|
+
return {
|
|
1102
|
+
ok: true,
|
|
1103
|
+
summary: "Waiting for your reply",
|
|
1104
|
+
logs,
|
|
1105
|
+
waitingUserInteraction: true,
|
|
1106
|
+
interactionId: cliRes.interactionId || "",
|
|
1107
|
+
streamed: Boolean(cliRes.streamed),
|
|
1108
|
+
streamLastChar,
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
return {
|
|
1113
|
+
ok: true,
|
|
1114
|
+
summary: String(cliRes.output || "").trim() || "continued",
|
|
1115
|
+
logs,
|
|
1116
|
+
waitingUserInteraction: false,
|
|
1117
|
+
streamed: Boolean(cliRes.streamed),
|
|
1118
|
+
streamLastChar,
|
|
717
1119
|
};
|
|
718
1120
|
}
|
|
719
1121
|
|
|
@@ -721,12 +1123,15 @@ module.exports = {
|
|
|
721
1123
|
runUcodeCoreAgent,
|
|
722
1124
|
runSingleCommand,
|
|
723
1125
|
runNaturalLanguageTask,
|
|
1126
|
+
resumeAfterUserInteraction,
|
|
724
1127
|
formatNlResult,
|
|
725
1128
|
normalizeToolLogEvent,
|
|
726
1129
|
isProjectAnalysisTask,
|
|
727
1130
|
buildNlFallbackSummary,
|
|
728
1131
|
buildNlContext,
|
|
1132
|
+
resolveWireSystemPrompt,
|
|
729
1133
|
stripSkillBlocksFromMessages,
|
|
1134
|
+
stripSkillBlocksFromText,
|
|
730
1135
|
resolvePlannerProvider,
|
|
731
1136
|
extractJsonSummary,
|
|
732
1137
|
enrichNativeError,
|