u-foo 2.5.15 → 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 (40) hide show
  1. package/package.json +1 -1
  2. package/src/code/agent.js +333 -243
  3. package/src/code/commands.js +16 -0
  4. package/src/code/context/assembler.js +18 -13
  5. package/src/code/context/executionSegment.js +97 -119
  6. package/src/code/context/index.js +11 -1
  7. package/src/code/context/planGraph.js +1410 -0
  8. package/src/code/context/planGraphService.js +857 -0
  9. package/src/code/context/planMode.js +398 -0
  10. package/src/code/context/planProjection.js +432 -0
  11. package/src/code/context/promptLayers.js +21 -5
  12. package/src/code/context/stateCommit.js +2 -0
  13. package/src/code/context/toolRuntime.js +172 -0
  14. package/src/code/context/userInteraction.js +457 -0
  15. package/src/code/context/userNudge.js +116 -0
  16. package/src/code/dispatch.js +17 -1
  17. package/src/code/index.js +2 -0
  18. package/src/code/nativeRunner.js +518 -37
  19. package/src/code/repl.js +160 -18
  20. package/src/code/runtime/agentWakeup.js +58 -0
  21. package/src/code/runtime/graphOwner.js +41 -0
  22. package/src/code/runtime/graphYieldRouter.js +42 -0
  23. package/src/code/runtime/index.js +15 -0
  24. package/src/code/runtime/loopMailbox.js +124 -0
  25. package/src/code/runtime/runtimeEvents.js +39 -0
  26. package/src/code/runtime/taskControl.js +565 -0
  27. package/src/code/runtime/taskFocus.js +165 -0
  28. package/src/code/runtime/taskLoop.js +383 -0
  29. package/src/code/runtime/taskRun.js +187 -0
  30. package/src/code/runtime/toolProvenance.js +70 -0
  31. package/src/code/runtime/workspaceLease.js +208 -0
  32. package/src/code/sessionStore.js +0 -10
  33. package/src/code/skills/injection.js +1 -0
  34. package/src/code/taskDecomposer.js +32 -8
  35. package/src/code/tools/askUser.js +11 -0
  36. package/src/code/tools/planGraph.js +29 -0
  37. package/src/ui/format/index.js +25 -1
  38. package/src/ui/format/markdownRenderer.js +224 -2
  39. package/src/ui/ink/UcodeApp.js +285 -22
  40. package/src/code/context/featureFlag.js +0 -13
package/src/code/agent.js CHANGED
@@ -10,9 +10,7 @@ const {
10
10
  saveSessionSnapshot,
11
11
  loadSessionSnapshot,
12
12
  } = require("./sessionStore");
13
- const { buildPromptContext } = require("../agents/prompts/native");
14
13
  const { buildSkillInjections } = require("./skills");
15
- const { isContextV2Enabled } = require("./context/featureFlag");
16
14
  const {
17
15
  assembleModelContext,
18
16
  syncMessagesToTranscript,
@@ -34,9 +32,14 @@ const {
34
32
  } = require("./context/stateCommit");
35
33
  const { applyWorkingSetPlan } = require("./context/workingSet");
36
34
  const {
37
- parseExecutionSegment,
38
- executeExecutionSegment,
39
- } = require("./context/executionSegment");
35
+ normalizePlanGraphCommand,
36
+ runPlanGraphCommand,
37
+ } = require("./context/planGraphService");
38
+ const {
39
+ shouldFrameAsUserReminder,
40
+ buildContinuationUserPrompt,
41
+ clearUserPrompts,
42
+ } = require("./context/userNudge");
40
43
  const {
41
44
  runUbusCommand,
42
45
  parseBusCheckOutput,
@@ -65,6 +68,16 @@ function ensureContextSessionState(state = {}) {
65
68
  const { emptyExecutionState } = require("./context/executionSegment");
66
69
  state.executionState = emptyExecutionState();
67
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);
68
81
  if (!state.contextPolicy || typeof state.contextPolicy !== "object") {
69
82
  const { defaultContextPolicy } = require("./context/assembler");
70
83
  state.contextPolicy = defaultContextPolicy();
@@ -83,79 +96,129 @@ function buildSkillBodyBlocks(skillInjections = {}) {
83
96
  });
84
97
  }
85
98
 
86
- async function runExecutionSegmentSteps({
87
- segment = {},
99
+ async function runPlanGraphSteps({
100
+ command = null,
101
+ segment = null,
88
102
  workspaceRoot = process.cwd(),
89
103
  sessionId = "",
90
104
  state = {},
91
105
  pushToolLog = () => null,
92
106
  } = {}) {
93
- const exec = executeExecutionSegment({
94
- segment,
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, {
95
118
  executionState: state.executionState,
96
- onStepStart: ({ tool, args }) => {
97
- pushToolLog({ tool, phase: "start", args, error: "" });
98
- },
99
- onStepComplete: ({ tool, args, result }) => {
119
+ autoAdvance: true,
120
+ parallel: true,
121
+ runTool: ({ node, args, tool, stepId }) => {
100
122
  pushToolLog({
101
123
  tool,
102
- phase: result && result.ok === false ? "error" : "",
124
+ phase: "start",
103
125
  args,
104
- error: result && result.ok === false ? String(result.error || "") : "",
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
+ },
105
132
  });
106
- if (isContextV2Enabled() && result && result.ok !== false) {
107
- const plan = require("./context/workingSet").defaultContextPlanFromToolEvent(
108
- tool,
109
- result.artifactId,
110
- args,
111
- );
112
- if (plan) state.workingSet = applyWorkingSetPlan(state.workingSet, plan, state);
113
- }
114
- if ((tool === "write" || tool === "edit") && args && args.path) {
115
- const filePath = String(args.path);
116
- if (!state.executionState || typeof state.executionState !== "object") {
117
- state.executionState = require("./context/executionSegment").emptyExecutionState();
118
- }
119
- const files = Array.isArray(state.executionState.modifiedFiles)
120
- ? state.executionState.modifiedFiles.slice()
121
- : [];
122
- if (!files.includes(filePath)) files.push(filePath);
123
- state.executionState.modifiedFiles = files;
124
- }
125
- },
126
- runStep: ({ tool, args }) => {
127
133
  const { runToolCall: dispatchToolCall } = require("./dispatch");
128
134
  const { persistToolResultToContext } = require("./context/assembler");
129
- const result = dispatchToolCall(
135
+ const toolResult = dispatchToolCall(
130
136
  { tool, args },
131
137
  { workspaceRoot, cwd: workspaceRoot, sessionId },
132
138
  );
133
- if (!result || result.ok === false || !isContextV2Enabled()) {
134
- return result;
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;
135
151
  }
136
152
  const persisted = persistToolResultToContext({
137
153
  workspaceRoot,
138
154
  sessionId,
139
155
  tool,
140
156
  args,
141
- rawResult: result,
157
+ rawResult: toolResult,
142
158
  });
143
159
  recordToolCallInSession(state, persisted, workspaceRoot);
144
- return persisted.modelPayload || result;
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
+ };
145
182
  },
146
183
  });
147
- state.executionState = exec.executionState;
148
- if (isContextV2Enabled()) {
149
- commitAfterSegmentEnd(state, exec, workspaceRoot);
150
- }
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);
151
192
  return {
152
- ok: exec.ok,
153
- segmentId: exec.segmentId,
154
- error: exec.error,
155
- stoppedAt: exec.stoppedAt,
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,
156
200
  };
157
201
  }
158
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
+
159
222
 
160
223
  function readTextOrFile(value = "") {
161
224
  const raw = String(value || "").trim();
@@ -409,94 +472,6 @@ function isProjectAnalysisTask(task = "") {
409
472
  return /(?:analy[sz]e|analysis|review|audit|status|architecture|codebase|repo|project|现状|架构|审查|分析|项目|代码库)/i.test(text);
410
473
  }
411
474
 
412
- function createProjectPreflightContext({
413
- workspaceRoot = process.cwd(),
414
- pushToolLog = () => null,
415
- } = {}) {
416
- const root = String(workspaceRoot || process.cwd());
417
- const readCandidates = [
418
- "AGENTS.md",
419
- "README.md",
420
- "README.zh-CN.md",
421
- "package.json",
422
- ];
423
- const blocks = [];
424
-
425
- for (const relPath of readCandidates) {
426
- pushToolLog({
427
- tool: "read",
428
- phase: "start",
429
- args: { path: relPath },
430
- error: "",
431
- });
432
- const readRes = runToolCall(
433
- {
434
- tool: "read",
435
- args: { path: relPath, maxBytes: 12000 },
436
- },
437
- {
438
- workspaceRoot: root,
439
- cwd: root,
440
- }
441
- );
442
- pushToolLog({
443
- tool: "read",
444
- phase: readRes && readRes.ok === false ? "error" : "",
445
- args: { path: relPath },
446
- error: readRes && readRes.ok === false ? String(readRes.error || "") : "",
447
- });
448
- if (!readRes || readRes.ok === false) continue;
449
- const content = String(readRes.content || "").trim();
450
- if (!content) continue;
451
- const clipped = content.length > 2400
452
- ? `${content.slice(0, 2400)}\n...[truncated]`
453
- : content;
454
- blocks.push(`File: ${relPath}\n${clipped}`);
455
- if (blocks.length >= 2) break;
456
- }
457
-
458
- if (blocks.length === 0) {
459
- const command = "ls -la";
460
- pushToolLog({
461
- tool: "bash",
462
- phase: "start",
463
- args: { command },
464
- error: "",
465
- });
466
- const bashRes = runToolCall(
467
- {
468
- tool: "bash",
469
- args: { command, timeoutMs: 4000 },
470
- },
471
- {
472
- workspaceRoot: root,
473
- cwd: root,
474
- }
475
- );
476
- pushToolLog({
477
- tool: "bash",
478
- phase: bashRes && bashRes.ok === false ? "error" : "",
479
- args: { command },
480
- error: bashRes && bashRes.ok === false ? String(bashRes.error || "") : "",
481
- });
482
- if (bashRes && bashRes.ok !== false) {
483
- const stdout = String(bashRes.stdout || "").trim();
484
- const clipped = stdout.length > 1200
485
- ? `${stdout.slice(0, 1200)}\n...[truncated]`
486
- : stdout;
487
- if (clipped) {
488
- blocks.push(`Command: ${command}\n${clipped}`);
489
- }
490
- }
491
- }
492
-
493
- if (blocks.length === 0) return "";
494
- return [
495
- "Preflight snapshot (captured by ucode):",
496
- ...blocks.map((block) => `---\n${block}`),
497
- ].join("\n");
498
- }
499
-
500
475
  function buildNlFallbackSummary(logs = []) {
501
476
  const list = Array.isArray(logs) ? logs : [];
502
477
  const started = list.filter((entry) => entry && entry.phase === "start").length;
@@ -550,32 +525,23 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
550
525
  const useDecomposition = isBugFixTask && !options.disableDecomposition;
551
526
  const analysisTask = isProjectAnalysisTask(taskText);
552
527
  const workspaceRoot = String(state.workspaceRoot || process.cwd());
553
- const contextV2 = isContextV2Enabled();
554
- if (contextV2) ensureContextSessionState(state);
528
+ ensureContextSessionState(state);
555
529
 
556
- let preflightContext = "";
557
530
  let projectSnapshot = state.projectSnapshot || null;
558
531
  if (analysisTask) {
559
- if (contextV2) {
560
- projectSnapshot = createProjectPreflightContextV2({
561
- workspaceRoot,
562
- sessionId: String(state.sessionId || ""),
563
- pushToolLog,
564
- existingSnapshot: state.projectSnapshot,
565
- });
566
- state.projectSnapshot = projectSnapshot;
567
- } else {
568
- preflightContext = createProjectPreflightContext({
569
- workspaceRoot,
570
- pushToolLog,
571
- });
572
- }
573
- } else if (contextV2) {
532
+ projectSnapshot = createProjectPreflightContextV2({
533
+ workspaceRoot,
534
+ sessionId: String(state.sessionId || ""),
535
+ pushToolLog,
536
+ existingSnapshot: state.projectSnapshot,
537
+ });
538
+ state.projectSnapshot = projectSnapshot;
539
+ } else {
574
540
  projectSnapshot = ensureProjectSnapshot(state, workspaceRoot);
575
541
  }
576
542
 
577
- if (contextV2) {
578
- ensureTaskContract(state, taskText);
543
+ ensureTaskContract(state, taskText);
544
+ if (!shouldFrameAsUserReminder(state.executionState)) {
579
545
  state.taskContract = patchTaskContractFromUserMessage(state.taskContract, taskText);
580
546
  }
581
547
 
@@ -586,45 +552,33 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
586
552
  prompt: taskPrompt,
587
553
  workspaceRoot,
588
554
  sessionId: String(state.sessionId || ""),
589
- persistBodies: contextV2,
590
- useActiveSkillTag: contextV2,
555
+ persistBodies: true,
556
+ useActiveSkillTag: true,
591
557
  });
592
558
  for (const warning of skillInjections.warnings || []) {
593
559
  pushSkillWarning(logs, onToolLog, warning);
594
560
  }
595
- if (contextV2 && Array.isArray(skillInjections.activeSkills) && skillInjections.activeSkills.length > 0) {
561
+ if (Array.isArray(skillInjections.activeSkills) && skillInjections.activeSkills.length > 0) {
596
562
  state.activeSkills = skillInjections.activeSkills;
597
563
  }
598
- const skillBodyBlocks = contextV2
599
- ? buildSkillBodyBlocks(skillInjections)
600
- : (skillInjections.blocks || []);
601
- // v1: skill body rides in the user prompt.
602
- // v2: skill body goes only into turnDynamic (system layered prompt) to avoid
564
+ const skillBodyBlocks = buildSkillBodyBlocks(skillInjections);
565
+ // Skill body goes into turnDynamic (system layered prompt) to avoid
603
566
  // double injection and mixed system/user privilege semantics.
604
- const effectiveTaskPrompt = contextV2
605
- ? taskPrompt
606
- : (skillBodyBlocks.length > 0
607
- ? `${skillBodyBlocks.join("\n\n")}\n\n${taskPrompt}`
608
- : taskPrompt);
609
-
610
- let assembled = null;
611
- let systemContext = "";
612
- if (contextV2) {
613
- assembled = assembleModelContext(state, {
614
- workspaceRoot,
615
- model,
616
- provider,
617
- turnDynamic: skillBodyBlocks.join("\n\n"),
618
- latestUserMessage: effectiveTaskPrompt,
619
- });
620
- systemContext = assembled.systemPrompt;
621
- state.summary = assembled.summary || state.summary;
622
- } else {
623
- systemContext = [String(state.context || "").trim(), preflightContext]
624
- .filter(Boolean)
625
- .join("\n\n");
567
+ let effectiveTaskPrompt = taskPrompt;
568
+ if (shouldFrameAsUserReminder(state.executionState)) {
569
+ effectiveTaskPrompt = buildContinuationUserPrompt(effectiveTaskPrompt, state.executionState);
626
570
  }
627
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;
581
+
628
582
  const onStream = onDelta
629
583
  ? (delta) => {
630
584
  const text = String(delta || "");
@@ -646,9 +600,7 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
646
600
  let lastTranscriptBaseline = 0;
647
601
  const invokeNative = (sessionIdValue = "", timeoutOverrideMs = timeoutMs) => {
648
602
  toolEventsThisAttempt = 0;
649
- const historyMessages = contextV2 && assembled
650
- ? assembled.messages
651
- : (Array.isArray(state.nlMessages) ? state.nlMessages : []);
603
+ const historyMessages = assembled.messages;
652
604
  // Sanitized length matches what nativeRunner clones before appending this
653
605
  // turn's user/tool/assistant messages — used as the transcript sync baseline.
654
606
  lastTranscriptBaseline = sanitizeModelMessages(historyMessages).length;
@@ -658,17 +610,15 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
658
610
  model,
659
611
  prompt: effectiveTaskPrompt,
660
612
  systemPrompt: systemContext,
661
- systemBlocks: contextV2 && assembled ? assembled.systemBlocks : null,
613
+ systemBlocks: assembled.systemBlocks || null,
662
614
  messages: historyMessages,
663
615
  sessionId: String(sessionIdValue || state.sessionId || ""),
664
616
  timeoutMs: timeoutOverrideMs,
665
617
  onStreamDelta: onStream,
666
618
  onThinkingDelta,
667
619
  onPhase,
668
- contextV2,
669
- onArtifactPersisted: contextV2
670
- ? (persisted) => recordToolCallInSession(state, persisted, workspaceRoot)
671
- : null,
620
+ executionState: state.executionState || null,
621
+ onArtifactPersisted: (persisted) => recordToolCallInSession(state, persisted, workspaceRoot),
672
622
  onToolEvent: (event) => {
673
623
  toolEventsThisAttempt += 1;
674
624
  pushToolLog(event);
@@ -680,22 +630,24 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
680
630
  try {
681
631
  let cliRes;
682
632
 
683
- const requestedSegment = parseExecutionSegment(options.executionSegment || options.nextSegment || null);
684
- if (contextV2 && requestedSegment && requestedSegment.steps && requestedSegment.steps.length > 0) {
685
- const segmentResult = await runExecutionSegmentSteps({
686
- segment: requestedSegment,
633
+ const requestedPlan = normalizePlanGraphCommand(
634
+ options.executionSegment || options.nextSegment || options.planGraph || null,
635
+ );
636
+ if (requestedPlan) {
637
+ const planResult = await runPlanGraphSteps({
638
+ command: requestedPlan,
687
639
  workspaceRoot,
688
640
  sessionId: String(state.sessionId || ""),
689
641
  state,
690
642
  pushToolLog,
691
643
  });
692
- if (!segmentResult.ok) {
644
+ if (!planResult.ok) {
693
645
  return {
694
646
  ok: false,
695
647
  summary: "",
696
648
  artifacts: [],
697
649
  logs: logs.slice(),
698
- error: segmentResult.error,
650
+ error: planResult.error,
699
651
  metrics: {},
700
652
  streamed: false,
701
653
  streamLastChar: "",
@@ -716,9 +668,8 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
716
668
  systemPrompt: systemContext,
717
669
  messages: Array.isArray(state.nlMessages) ? state.nlMessages : [],
718
670
  sessionId: String(state.sessionId || ""),
719
- state: contextV2 ? state : null,
720
- contextV2,
721
- systemBlocks: contextV2 && assembled ? assembled.systemBlocks : null,
671
+ state,
672
+ systemBlocks: assembled.systemBlocks || null,
722
673
  });
723
674
 
724
675
  if (decomposedResult.ok) {
@@ -751,6 +702,9 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
751
702
 
752
703
  if (!cliRes || cliRes.ok === false) {
753
704
  const errMsg = String((cliRes && cliRes.error) || "");
705
+ if (isCliCancelledError(errMsg) && state.executionState) {
706
+ clearUserPrompts(state.executionState);
707
+ }
754
708
  return {
755
709
  ok: false,
756
710
  summary: "",
@@ -766,22 +720,40 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
766
720
  if (cliRes && typeof cliRes.sessionId === "string" && cliRes.sessionId.trim()) {
767
721
  state.sessionId = cliRes.sessionId.trim();
768
722
  }
769
- if (cliRes && Array.isArray(cliRes.messages)) {
770
- state.nlMessages = stripSkillBlocksFromMessages(cliRes.messages);
771
- if (contextV2) {
772
- syncMessagesToTranscript(state, cliRes.messages, workspaceRoot, {
773
- baselineCount: lastTranscriptBaseline,
774
- });
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;
775
732
  }
733
+ if (!state.executionState.planModeSource && priorSource) {
734
+ state.executionState.planModeSource = priorSource;
735
+ }
736
+ }
737
+ if (cliRes && Array.isArray(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
+ );
776
748
  }
777
749
  const normalized = String(cliRes.output || "").trim();
778
- const sideEffects = contextV2 ? parseStructuredSideEffects(normalized) : null;
779
- if (contextV2 && sideEffects) {
750
+ const sideEffects = parseStructuredSideEffects(normalized);
751
+ if (sideEffects) {
780
752
  applyContextSideEffects(state, sideEffects);
781
- const nextSegment = parseExecutionSegment(sideEffects);
782
- if (nextSegment && nextSegment.steps && nextSegment.steps.length > 0) {
783
- await runExecutionSegmentSteps({
784
- segment: nextSegment,
753
+ const planCommand = normalizePlanGraphCommand(sideEffects);
754
+ if (planCommand) {
755
+ await runPlanGraphSteps({
756
+ command: planCommand,
785
757
  workspaceRoot,
786
758
  sessionId: String(state.sessionId || ""),
787
759
  state,
@@ -791,9 +763,23 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
791
763
  }
792
764
  const summary = extractJsonSummary(normalized);
793
765
  const resolvedSummary = String(summary || "").trim() || buildNlFallbackSummary(logs);
794
- const artifactIds = contextV2 && Array.isArray(state.workingSet)
766
+ const artifactIds = Array.isArray(state.workingSet)
795
767
  ? state.workingSet.map((entry) => entry.artifactId).filter(Boolean)
796
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
+ }
797
783
  return {
798
784
  ok: true,
799
785
  summary: resolvedSummary,
@@ -870,9 +856,7 @@ function buildNlContext({
870
856
  }
871
857
 
872
858
  /**
873
- * Single wire entry for system prompt assembly.
874
- * v2 (default): layered Context Manager prompt.
875
- * v1 (explicit off): legacy flat buildPromptContext.
859
+ * Single wire entry for system prompt assembly (layered Context Manager).
876
860
  */
877
861
  function resolveWireSystemPrompt({
878
862
  workspaceRoot = process.cwd(),
@@ -886,25 +870,15 @@ function resolveWireSystemPrompt({
886
870
  } = {}) {
887
871
  if (overrideSystemPrompt) return String(overrideSystemPrompt);
888
872
 
889
- if (isContextV2Enabled()) {
890
- return buildLayeredSystemPrompt({
891
- workspaceRoot,
892
- model,
893
- provider,
894
- appendSystemPrompt,
895
- epochDynamic,
896
- turnDynamic,
897
- sessionStableExtras,
898
- }).flatText;
899
- }
900
-
901
- // Legacy v1 path — kept for UFOO_UCODE_CONTEXT_V2=0 compatibility only.
902
- return buildPromptContext({
873
+ return buildLayeredSystemPrompt({
903
874
  workspaceRoot,
904
875
  model,
905
876
  provider,
906
877
  appendSystemPrompt,
907
- });
878
+ epochDynamic,
879
+ turnDynamic,
880
+ sessionStableExtras,
881
+ }).flatText;
908
882
  }
909
883
 
910
884
  function buildSessionSnapshotFromState(state = {}) {
@@ -1012,14 +986,12 @@ function resumeSessionState(state = {}, sessionId = "", workspaceRoot = process.
1012
986
  ? snapshot.toolCallsSinceCommit
1013
987
  : 0;
1014
988
  state.activeSkills = Array.isArray(snapshot.activeSkills) ? snapshot.activeSkills : [];
1015
- if (isContextV2Enabled()) {
1016
- ensureContextSessionState(state);
1017
- const { ensureTranscript } = require("./context/assembler");
1018
- ensureTranscript(state, state.workspaceRoot);
1019
- if (Array.isArray(state.transcriptEvents) && state.transcriptEvents.length > 0) {
1020
- const { transcriptEventsToMessages } = require("./context/transcript");
1021
- state.nlMessages = transcriptEventsToMessages(state.transcriptEvents, { preferArtifact: true });
1022
- }
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 });
1023
995
  }
1024
996
 
1025
997
  return {
@@ -1030,10 +1002,128 @@ function resumeSessionState(state = {}, sessionId = "", workspaceRoot = process.
1030
1002
  };
1031
1003
  }
1032
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,
1119
+ };
1120
+ }
1121
+
1033
1122
  module.exports = {
1034
1123
  runUcodeCoreAgent,
1035
1124
  runSingleCommand,
1036
1125
  runNaturalLanguageTask,
1126
+ resumeAfterUserInteraction,
1037
1127
  formatNlResult,
1038
1128
  normalizeToolLogEvent,
1039
1129
  isProjectAnalysisTask,