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.
Files changed (59) hide show
  1. package/package.json +1 -1
  2. package/src/agents/prompts/native/environment.js +20 -8
  3. package/src/code/agent.js +517 -112
  4. package/src/code/commands.js +77 -0
  5. package/src/code/context/artifactGc.js +292 -0
  6. package/src/code/context/artifactIndex.js +161 -0
  7. package/src/code/context/artifacts.js +183 -0
  8. package/src/code/context/assembler.js +703 -0
  9. package/src/code/context/executionSegment.js +292 -0
  10. package/src/code/context/index.js +28 -0
  11. package/src/code/context/planGraph.js +1410 -0
  12. package/src/code/context/planGraphService.js +857 -0
  13. package/src/code/context/planMode.js +398 -0
  14. package/src/code/context/planProjection.js +432 -0
  15. package/src/code/context/projectSnapshot.js +201 -0
  16. package/src/code/context/promptLayers.js +175 -0
  17. package/src/code/context/reducers.js +328 -0
  18. package/src/code/context/stableJson.js +29 -0
  19. package/src/code/context/stateCommit.js +414 -0
  20. package/src/code/context/toolRuntime.js +172 -0
  21. package/src/code/context/transcript.js +182 -0
  22. package/src/code/context/transcriptSync.js +106 -0
  23. package/src/code/context/userInteraction.js +457 -0
  24. package/src/code/context/userNudge.js +116 -0
  25. package/src/code/context/workingSet.js +323 -0
  26. package/src/code/dispatch.js +20 -1
  27. package/src/code/index.js +8 -0
  28. package/src/code/modelCommand.js +87 -0
  29. package/src/code/nativeRunner.js +625 -34
  30. package/src/code/repl.js +196 -50
  31. package/src/code/runtime/agentWakeup.js +58 -0
  32. package/src/code/runtime/graphOwner.js +41 -0
  33. package/src/code/runtime/graphYieldRouter.js +42 -0
  34. package/src/code/runtime/index.js +15 -0
  35. package/src/code/runtime/loopMailbox.js +124 -0
  36. package/src/code/runtime/runtimeEvents.js +39 -0
  37. package/src/code/runtime/taskControl.js +565 -0
  38. package/src/code/runtime/taskFocus.js +165 -0
  39. package/src/code/runtime/taskLoop.js +383 -0
  40. package/src/code/runtime/taskRun.js +187 -0
  41. package/src/code/runtime/toolProvenance.js +70 -0
  42. package/src/code/runtime/workspaceLease.js +208 -0
  43. package/src/code/sessionStore.js +217 -15
  44. package/src/code/skills/index.js +10 -0
  45. package/src/code/skills/injection.js +66 -3
  46. package/src/code/skills/loader.js +21 -0
  47. package/src/code/skills/manifest.js +87 -0
  48. package/src/code/skills/render.js +15 -1
  49. package/src/code/taskDecomposer.js +56 -2
  50. package/src/code/tools/artifactRead.js +40 -0
  51. package/src/code/tools/askUser.js +11 -0
  52. package/src/code/tools/planGraph.js +29 -0
  53. package/src/code/tui.js +2 -0
  54. package/src/code/usageStore.js +15 -0
  55. package/src/ui/format/index.js +285 -45
  56. package/src/ui/format/markdownRenderer.js +436 -71
  57. package/src/ui/ink/ChatApp.js +39 -8
  58. package/src/ui/ink/UcodeApp.js +592 -43
  59. 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)) : 300000;
141
- return Math.min(1800000, Math.max(base * 2, base + 120000));
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 30min and can be raised per
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 = 1800000;
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*/g, "")
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
- const preflightContext = analysisTask
431
- ? createProjectPreflightContext({
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
- const effectiveTaskPrompt = Array.isArray(skillInjections.blocks) && skillInjections.blocks.length > 0
447
- ? `${skillInjections.blocks.join("\n\n")}\n\n${taskPrompt}`
448
- : taskPrompt;
449
- const systemContext = [String(state.context || "").trim(), preflightContext]
450
- .filter(Boolean)
451
- .join("\n\n");
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
- messages: Array.isArray(state.nlMessages) ? state.nlMessages : [],
480
- sessionId: String(sessionIdValue || ""),
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
- state.nlMessages = stripSkillBlocksFromMessages(cliRes.messages);
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
- // New modular prompt assembly
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,