u-foo 2.5.15 → 3.0.1

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 (56) hide show
  1. package/package.json +1 -1
  2. package/src/code/agent.js +350 -246
  3. package/src/code/commands.js +16 -0
  4. package/src/code/context/assembler.js +18 -13
  5. package/src/code/context/executionSegment.js +102 -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 +405 -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 +4 -0
  18. package/src/code/nativeRunner.js +589 -172
  19. package/src/code/protocol/controlPlane.js +93 -0
  20. package/src/code/protocol/faultHarness.js +90 -0
  21. package/src/code/protocol/index.js +20 -0
  22. package/src/code/protocol/loopEvents.js +102 -0
  23. package/src/code/protocol/materialize.js +107 -0
  24. package/src/code/protocol/messageFixtures.js +116 -0
  25. package/src/code/protocol/ownership.js +147 -0
  26. package/src/code/protocol/protocolValidator.js +165 -0
  27. package/src/code/protocol/suspension.js +173 -0
  28. package/src/code/protocol/toolCallLedger.js +222 -0
  29. package/src/code/protocol/transitions.js +97 -0
  30. package/src/code/providers/anthropicMessagesTransport.js +93 -0
  31. package/src/code/providers/index.js +7 -0
  32. package/src/code/providers/openaiChatTransport.js +98 -0
  33. package/src/code/providers/transportContract.js +46 -0
  34. package/src/code/repl.js +147 -18
  35. package/src/code/runtime/agentWakeup.js +58 -0
  36. package/src/code/runtime/graphOwner.js +41 -0
  37. package/src/code/runtime/graphYieldRouter.js +42 -0
  38. package/src/code/runtime/index.js +15 -0
  39. package/src/code/runtime/loopMailbox.js +124 -0
  40. package/src/code/runtime/runtimeEvents.js +39 -0
  41. package/src/code/runtime/taskControl.js +565 -0
  42. package/src/code/runtime/taskFocus.js +165 -0
  43. package/src/code/runtime/taskLoop.js +394 -0
  44. package/src/code/runtime/taskRun.js +348 -0
  45. package/src/code/runtime/toolProvenance.js +70 -0
  46. package/src/code/runtime/workspaceLease.js +249 -0
  47. package/src/code/sessionStore.js +1 -10
  48. package/src/code/skills/injection.js +1 -0
  49. package/src/code/taskDecomposer.js +32 -8
  50. package/src/code/taskRoute.js +73 -0
  51. package/src/code/tools/askUser.js +11 -0
  52. package/src/code/tools/planGraph.js +29 -0
  53. package/src/ui/format/index.js +25 -1
  54. package/src/ui/format/markdownRenderer.js +224 -2
  55. package/src/ui/ink/UcodeApp.js +268 -22
  56. 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;
@@ -545,37 +520,41 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
545
520
  : null;
546
521
  const pushToolLog = createToolLogCollector(logs, onToolLog);
547
522
 
548
- // Detect bug fix tasks and use decomposed runner
549
- const isBugFixTask = /\b(?:fix(?:es|ed|ing)?|bugs?|issues?|problems?|errors?|broken)\b|doesn't work|not work/i.test(taskText);
550
- const useDecomposition = isBugFixTask && !options.disableDecomposition;
523
+ // Structural / explicit upgrade to decomposed runner (not keyword "fix").
524
+ const { shouldUpgradeToDecomposition } = require("./taskRoute");
525
+ const routeDecision = shouldUpgradeToDecomposition(taskText, {
526
+ disableDecomposition: options.disableDecomposition,
527
+ forceDecomposition: options.forceDecomposition,
528
+ forceDirect: options.forceDirect,
529
+ failureCount: options.failureCount,
530
+ modelRequestedUpgrade: options.modelRequestedUpgrade,
531
+ hasPlanGraph: Boolean(
532
+ state.executionState
533
+ && state.executionState.planGraph
534
+ && state.executionState.planGraph.graphId
535
+ ),
536
+ });
537
+ const useDecomposition = Boolean(routeDecision.upgrade);
538
+ state.lastRouteDecision = routeDecision;
551
539
  const analysisTask = isProjectAnalysisTask(taskText);
552
540
  const workspaceRoot = String(state.workspaceRoot || process.cwd());
553
- const contextV2 = isContextV2Enabled();
554
- if (contextV2) ensureContextSessionState(state);
541
+ ensureContextSessionState(state);
555
542
 
556
- let preflightContext = "";
557
543
  let projectSnapshot = state.projectSnapshot || null;
558
544
  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) {
545
+ projectSnapshot = createProjectPreflightContextV2({
546
+ workspaceRoot,
547
+ sessionId: String(state.sessionId || ""),
548
+ pushToolLog,
549
+ existingSnapshot: state.projectSnapshot,
550
+ });
551
+ state.projectSnapshot = projectSnapshot;
552
+ } else {
574
553
  projectSnapshot = ensureProjectSnapshot(state, workspaceRoot);
575
554
  }
576
555
 
577
- if (contextV2) {
578
- ensureTaskContract(state, taskText);
556
+ ensureTaskContract(state, taskText);
557
+ if (!shouldFrameAsUserReminder(state.executionState)) {
579
558
  state.taskContract = patchTaskContractFromUserMessage(state.taskContract, taskText);
580
559
  }
581
560
 
@@ -586,45 +565,33 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
586
565
  prompt: taskPrompt,
587
566
  workspaceRoot,
588
567
  sessionId: String(state.sessionId || ""),
589
- persistBodies: contextV2,
590
- useActiveSkillTag: contextV2,
568
+ persistBodies: true,
569
+ useActiveSkillTag: true,
591
570
  });
592
571
  for (const warning of skillInjections.warnings || []) {
593
572
  pushSkillWarning(logs, onToolLog, warning);
594
573
  }
595
- if (contextV2 && Array.isArray(skillInjections.activeSkills) && skillInjections.activeSkills.length > 0) {
574
+ if (Array.isArray(skillInjections.activeSkills) && skillInjections.activeSkills.length > 0) {
596
575
  state.activeSkills = skillInjections.activeSkills;
597
576
  }
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
577
+ const skillBodyBlocks = buildSkillBodyBlocks(skillInjections);
578
+ // Skill body goes into turnDynamic (system layered prompt) to avoid
603
579
  // 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");
580
+ let effectiveTaskPrompt = taskPrompt;
581
+ if (shouldFrameAsUserReminder(state.executionState)) {
582
+ effectiveTaskPrompt = buildContinuationUserPrompt(effectiveTaskPrompt, state.executionState);
626
583
  }
627
584
 
585
+ const assembled = assembleModelContext(state, {
586
+ workspaceRoot,
587
+ model,
588
+ provider,
589
+ turnDynamic: skillBodyBlocks.join("\n\n"),
590
+ latestUserMessage: effectiveTaskPrompt,
591
+ });
592
+ const systemContext = assembled.systemPrompt;
593
+ state.summary = assembled.summary || state.summary;
594
+
628
595
  const onStream = onDelta
629
596
  ? (delta) => {
630
597
  const text = String(delta || "");
@@ -646,9 +613,7 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
646
613
  let lastTranscriptBaseline = 0;
647
614
  const invokeNative = (sessionIdValue = "", timeoutOverrideMs = timeoutMs) => {
648
615
  toolEventsThisAttempt = 0;
649
- const historyMessages = contextV2 && assembled
650
- ? assembled.messages
651
- : (Array.isArray(state.nlMessages) ? state.nlMessages : []);
616
+ const historyMessages = assembled.messages;
652
617
  // Sanitized length matches what nativeRunner clones before appending this
653
618
  // turn's user/tool/assistant messages — used as the transcript sync baseline.
654
619
  lastTranscriptBaseline = sanitizeModelMessages(historyMessages).length;
@@ -658,17 +623,15 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
658
623
  model,
659
624
  prompt: effectiveTaskPrompt,
660
625
  systemPrompt: systemContext,
661
- systemBlocks: contextV2 && assembled ? assembled.systemBlocks : null,
626
+ systemBlocks: assembled.systemBlocks || null,
662
627
  messages: historyMessages,
663
628
  sessionId: String(sessionIdValue || state.sessionId || ""),
664
629
  timeoutMs: timeoutOverrideMs,
665
630
  onStreamDelta: onStream,
666
631
  onThinkingDelta,
667
632
  onPhase,
668
- contextV2,
669
- onArtifactPersisted: contextV2
670
- ? (persisted) => recordToolCallInSession(state, persisted, workspaceRoot)
671
- : null,
633
+ executionState: state.executionState || null,
634
+ onArtifactPersisted: (persisted) => recordToolCallInSession(state, persisted, workspaceRoot),
672
635
  onToolEvent: (event) => {
673
636
  toolEventsThisAttempt += 1;
674
637
  pushToolLog(event);
@@ -680,22 +643,24 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
680
643
  try {
681
644
  let cliRes;
682
645
 
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,
646
+ const requestedPlan = normalizePlanGraphCommand(
647
+ options.executionSegment || options.nextSegment || options.planGraph || null,
648
+ );
649
+ if (requestedPlan) {
650
+ const planResult = await runPlanGraphSteps({
651
+ command: requestedPlan,
687
652
  workspaceRoot,
688
653
  sessionId: String(state.sessionId || ""),
689
654
  state,
690
655
  pushToolLog,
691
656
  });
692
- if (!segmentResult.ok) {
657
+ if (!planResult.ok) {
693
658
  return {
694
659
  ok: false,
695
660
  summary: "",
696
661
  artifacts: [],
697
662
  logs: logs.slice(),
698
- error: segmentResult.error,
663
+ error: planResult.error,
699
664
  metrics: {},
700
665
  streamed: false,
701
666
  streamLastChar: "",
@@ -716,9 +681,8 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
716
681
  systemPrompt: systemContext,
717
682
  messages: Array.isArray(state.nlMessages) ? state.nlMessages : [],
718
683
  sessionId: String(state.sessionId || ""),
719
- state: contextV2 ? state : null,
720
- contextV2,
721
- systemBlocks: contextV2 && assembled ? assembled.systemBlocks : null,
684
+ state,
685
+ systemBlocks: assembled.systemBlocks || null,
722
686
  });
723
687
 
724
688
  if (decomposedResult.ok) {
@@ -751,6 +715,9 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
751
715
 
752
716
  if (!cliRes || cliRes.ok === false) {
753
717
  const errMsg = String((cliRes && cliRes.error) || "");
718
+ if (isCliCancelledError(errMsg) && state.executionState) {
719
+ clearUserPrompts(state.executionState);
720
+ }
754
721
  return {
755
722
  ok: false,
756
723
  summary: "",
@@ -766,22 +733,40 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
766
733
  if (cliRes && typeof cliRes.sessionId === "string" && cliRes.sessionId.trim()) {
767
734
  state.sessionId = cliRes.sessionId.trim();
768
735
  }
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
- });
736
+ if (cliRes && cliRes.executionState && typeof cliRes.executionState === "object") {
737
+ // Preserve planMode if the runner returned a fresh empty state without it.
738
+ const priorPlanMode = Boolean(state.executionState && state.executionState.planMode);
739
+ const priorSource = state.executionState && state.executionState.planModeSource
740
+ ? String(state.executionState.planModeSource)
741
+ : "";
742
+ state.executionState = cliRes.executionState;
743
+ if (typeof state.executionState.planMode !== "boolean") {
744
+ state.executionState.planMode = priorPlanMode;
745
+ }
746
+ if (!state.executionState.planModeSource && priorSource) {
747
+ state.executionState.planModeSource = priorSource;
775
748
  }
776
749
  }
750
+ if (cliRes && Array.isArray(cliRes.messages)) {
751
+ // Sync first so ensureTranscript does not migrate the just-assigned
752
+ // nlMessages and then append the same delta again.
753
+ syncMessagesToTranscript(state, cliRes.messages, workspaceRoot, {
754
+ baselineCount: lastTranscriptBaseline,
755
+ });
756
+ state.nlMessages = stripSkillBlocksFromMessages(
757
+ Array.isArray(state.nlMessages) && state.nlMessages.length > 0
758
+ ? state.nlMessages
759
+ : cliRes.messages,
760
+ );
761
+ }
777
762
  const normalized = String(cliRes.output || "").trim();
778
- const sideEffects = contextV2 ? parseStructuredSideEffects(normalized) : null;
779
- if (contextV2 && sideEffects) {
763
+ const sideEffects = parseStructuredSideEffects(normalized);
764
+ if (sideEffects) {
780
765
  applyContextSideEffects(state, sideEffects);
781
- const nextSegment = parseExecutionSegment(sideEffects);
782
- if (nextSegment && nextSegment.steps && nextSegment.steps.length > 0) {
783
- await runExecutionSegmentSteps({
784
- segment: nextSegment,
766
+ const planCommand = normalizePlanGraphCommand(sideEffects);
767
+ if (planCommand) {
768
+ await runPlanGraphSteps({
769
+ command: planCommand,
785
770
  workspaceRoot,
786
771
  sessionId: String(state.sessionId || ""),
787
772
  state,
@@ -791,9 +776,23 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
791
776
  }
792
777
  const summary = extractJsonSummary(normalized);
793
778
  const resolvedSummary = String(summary || "").trim() || buildNlFallbackSummary(logs);
794
- const artifactIds = contextV2 && Array.isArray(state.workingSet)
779
+ const artifactIds = Array.isArray(state.workingSet)
795
780
  ? state.workingSet.map((entry) => entry.artifactId).filter(Boolean)
796
781
  : [];
782
+ if (cliRes && cliRes.waitingUserInteraction) {
783
+ return {
784
+ ok: true,
785
+ summary: resolvedSummary || "Waiting for your reply",
786
+ artifacts: artifactIds,
787
+ logs: logs.slice(),
788
+ error: "",
789
+ metrics: {},
790
+ streamed: Boolean(streamed || cliRes.streamed),
791
+ streamLastChar,
792
+ waitingUserInteraction: true,
793
+ interactionId: cliRes.interactionId || "",
794
+ };
795
+ }
797
796
  return {
798
797
  ok: true,
799
798
  summary: resolvedSummary,
@@ -870,9 +869,7 @@ function buildNlContext({
870
869
  }
871
870
 
872
871
  /**
873
- * Single wire entry for system prompt assembly.
874
- * v2 (default): layered Context Manager prompt.
875
- * v1 (explicit off): legacy flat buildPromptContext.
872
+ * Single wire entry for system prompt assembly (layered Context Manager).
876
873
  */
877
874
  function resolveWireSystemPrompt({
878
875
  workspaceRoot = process.cwd(),
@@ -886,25 +883,15 @@ function resolveWireSystemPrompt({
886
883
  } = {}) {
887
884
  if (overrideSystemPrompt) return String(overrideSystemPrompt);
888
885
 
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({
886
+ return buildLayeredSystemPrompt({
903
887
  workspaceRoot,
904
888
  model,
905
889
  provider,
906
890
  appendSystemPrompt,
907
- });
891
+ epochDynamic,
892
+ turnDynamic,
893
+ sessionStableExtras,
894
+ }).flatText;
908
895
  }
909
896
 
910
897
  function buildSessionSnapshotFromState(state = {}) {
@@ -1012,14 +999,12 @@ function resumeSessionState(state = {}, sessionId = "", workspaceRoot = process.
1012
999
  ? snapshot.toolCallsSinceCommit
1013
1000
  : 0;
1014
1001
  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
- }
1002
+ ensureContextSessionState(state);
1003
+ const { ensureTranscript } = require("./context/assembler");
1004
+ ensureTranscript(state, state.workspaceRoot);
1005
+ if (Array.isArray(state.transcriptEvents) && state.transcriptEvents.length > 0) {
1006
+ const { transcriptEventsToMessages } = require("./context/transcript");
1007
+ state.nlMessages = transcriptEventsToMessages(state.transcriptEvents, { preferArtifact: true });
1023
1008
  }
1024
1009
 
1025
1010
  return {
@@ -1030,10 +1015,129 @@ function resumeSessionState(state = {}, sessionId = "", workspaceRoot = process.
1030
1015
  };
1031
1016
  }
1032
1017
 
1018
+ /**
1019
+ * Continue after TUI resolves approval/choice/chat.
1020
+ * ask_user: answer is written as the deferred tool_result (contiguous, no question echo).
1021
+ * checkpoint: short answer-only user message referencing interaction/node.
1022
+ */
1023
+ async function resumeAfterUserInteraction(answerText = "", state = {}, options = {}) {
1024
+ ensureContextSessionState(state);
1025
+ const { resolveUserInteraction } = require("./context/userInteraction");
1026
+ const { appendAnswerToolResult } = require("./nativeRunner");
1027
+ const resolved = resolveUserInteraction(state.executionState, answerText);
1028
+ if (!resolved.ok) {
1029
+ return {
1030
+ ok: false,
1031
+ error: resolved.error || "failed to resolve user interaction",
1032
+ code: resolved.code || "",
1033
+ waitingUserInteraction: true,
1034
+ };
1035
+ }
1036
+
1037
+ const logs = [];
1038
+ const pushToolLog = (event) => {
1039
+ try {
1040
+ logs.push(normalizeToolLogEvent(event));
1041
+ } catch { /* ignore */ }
1042
+ };
1043
+
1044
+ let messages = Array.isArray(state.nlMessages) ? state.nlMessages.slice() : [];
1045
+ if (resolved.continueMode === "tool_result" && resolved.resume && resolved.resume.call) {
1046
+ const appended = appendAnswerToolResult(messages, resolved.resume, resolved.answer);
1047
+ if (!appended.ok) {
1048
+ return { ok: false, error: appended.error || "failed to append answer tool_result" };
1049
+ }
1050
+ } else {
1051
+ // Checkpoint / non-tool path: answer-only contiguous user message (no question).
1052
+ messages.push({
1053
+ role: "user",
1054
+ content: JSON.stringify(resolved.answer),
1055
+ });
1056
+ }
1057
+ state.nlMessages = messages;
1058
+
1059
+ const workspaceRoot = state.workspaceRoot || process.cwd();
1060
+ const assembled = assembleModelContext(state, {
1061
+ workspaceRoot,
1062
+ provider: state.provider,
1063
+ model: state.model,
1064
+ });
1065
+ const systemContext = assembled.systemPrompt || "";
1066
+
1067
+ let streamLastChar = "";
1068
+ const onDelta = typeof options.onDelta === "function" ? options.onDelta : null;
1069
+ const trackingOnDelta = onDelta
1070
+ ? (delta) => {
1071
+ const text = String(delta || "");
1072
+ if (text) streamLastChar = text.slice(-1);
1073
+ return onDelta(delta);
1074
+ }
1075
+ : null;
1076
+
1077
+ const cliRes = await runNativeAgentTask({
1078
+ workspaceRoot,
1079
+ provider: state.provider,
1080
+ model: state.model,
1081
+ prompt: "",
1082
+ systemPrompt: systemContext,
1083
+ systemBlocks: assembled.systemBlocks || null,
1084
+ messages,
1085
+ sessionId: String(state.sessionId || ""),
1086
+ onToolEvent: pushToolLog,
1087
+ onStreamDelta: trackingOnDelta,
1088
+ onThinkingDelta: typeof options.onThinkingDelta === "function" ? options.onThinkingDelta : null,
1089
+ onPhase: typeof options.onPhase === "function" ? options.onPhase : null,
1090
+ executionState: state.executionState,
1091
+ signal: options.signal,
1092
+ resume: true,
1093
+ });
1094
+
1095
+ if (cliRes && cliRes.executionState) {
1096
+ state.executionState = cliRes.executionState;
1097
+ }
1098
+ if (cliRes && Array.isArray(cliRes.messages)) {
1099
+ state.nlMessages = stripSkillBlocksFromMessages(cliRes.messages);
1100
+ }
1101
+
1102
+ if (!cliRes || cliRes.ok === false) {
1103
+ return {
1104
+ ok: false,
1105
+ error: (cliRes && cliRes.error) || "resume failed",
1106
+ logs,
1107
+ waitingUserInteraction: false,
1108
+ streamed: false,
1109
+ streamLastChar: "",
1110
+ };
1111
+ }
1112
+
1113
+ if (cliRes.waitingUserInteraction) {
1114
+ return {
1115
+ ok: true,
1116
+ summary: "Waiting for your reply",
1117
+ logs,
1118
+ waitingUserInteraction: true,
1119
+ interactionId: cliRes.interactionId || "",
1120
+ streamed: Boolean(cliRes.streamed),
1121
+ streamLastChar,
1122
+ };
1123
+ }
1124
+
1125
+ return {
1126
+ ok: true,
1127
+ summary: String(cliRes.output || "").trim() || "continued",
1128
+ logs,
1129
+ waitingUserInteraction: false,
1130
+ streamed: Boolean(cliRes.streamed),
1131
+ streamLastChar,
1132
+ };
1133
+ }
1134
+
1033
1135
  module.exports = {
1034
1136
  runUcodeCoreAgent,
1035
1137
  runSingleCommand,
1036
1138
  runNaturalLanguageTask,
1139
+ resumeAfterUserInteraction,
1140
+ submitUserInteractionAnswer: (...args) => require("./protocol/suspension").submitUserInteractionAnswer(...args),
1037
1141
  formatNlResult,
1038
1142
  normalizeToolLogEvent,
1039
1143
  isProjectAnalysisTask,