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
@@ -6,7 +6,6 @@ const {
6
6
  } = require("../agents/providers/credentials/kimi");
7
7
  const { runToolCall } = require("./dispatch");
8
8
  const { appendUsageRecord } = require("./usageStore");
9
- const { isContextV2Enabled } = require("./context/featureFlag");
10
9
  const {
11
10
  persistToolResultToContext,
12
11
  sanitizeModelMessages,
@@ -14,18 +13,56 @@ const {
14
13
  const { systemBlocksToAnthropicPayload } = require("./context/promptLayers");
15
14
  const { parseStructuredSideEffects } = require("./context/stateCommit");
16
15
  const {
17
- parseExecutionSegment,
18
- executeExecutionSegment,
19
- formatSegmentResultMessage,
20
16
  emptyExecutionState,
21
17
  } = require("./context/executionSegment");
18
+ const {
19
+ normalizePlanGraphCommand,
20
+ runPlanGraphCommand,
21
+ activePlanRequiresExpansion,
22
+ } = require("./context/planGraphService");
23
+ const { planModeBlocksDirectTool } = require("./context/planMode");
24
+ const {
25
+ drainUserPrompts,
26
+ clearUserPrompts,
27
+ formatUserReminderMessage,
28
+ ensurePendingUserPrompts,
29
+ } = require("./context/userNudge");
30
+ const {
31
+ runAskUserTool,
32
+ syncInteractionFromPlanGraph,
33
+ hasPendingUserInteraction,
34
+ getPendingUserInteraction,
35
+ } = require("./context/userInteraction");
36
+ const { checkWriteAllowed } = require("./runtime/workspaceLease");
37
+ const {
38
+ createToolCallLedger,
39
+ declareCalls,
40
+ markExecuting,
41
+ deferCall,
42
+ resolveCall,
43
+ snapshotLedger,
44
+ runProviderTurnGate,
45
+ withFaultPoint,
46
+ checkFaultPoint,
47
+ materializeResolvedToolResults,
48
+ materializeAnswerToolResult,
49
+ } = require("./protocol");
22
50
  const { stableStringify } = require("./context/stableJson");
23
51
  const { getReadToolDescription } = require("../agents/prompts/native/toolDescriptions/read");
24
52
  const { getWriteToolDescription } = require("../agents/prompts/native/toolDescriptions/write");
25
53
  const { getEditToolDescription } = require("../agents/prompts/native/toolDescriptions/edit");
26
54
  const { getBashToolDescription } = require("../agents/prompts/native/toolDescriptions/bash");
27
55
 
28
- const CORE_TOOL_NAMES = new Set(["read", "write", "edit", "bash", "artifact_read"]);
56
+ const CORE_TOOL_NAMES = new Set([
57
+ "read",
58
+ "write",
59
+ "edit",
60
+ "bash",
61
+ "artifact_read",
62
+ "plan_graph",
63
+ "ask_user",
64
+ ]);
65
+ const EXECUTABLE_GRAPH_TOOLS = new Set(["read", "write", "edit", "bash", "artifact_read"]);
29
66
  const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
30
67
  const DEFAULT_ANTHROPIC_BASE_URL = "https://api.anthropic.com/v1";
31
68
  const DEFAULT_KIMI_BASE_URL = "https://api.kimi.com/coding/v1";
@@ -184,7 +221,9 @@ function createGuards({ signal = null, timeoutMs = DEFAULT_NATIVE_TIMEOUT_MS } =
184
221
  function emitToolEvent(callback, event = {}) {
185
222
  if (typeof callback !== "function") return;
186
223
  try {
187
- callback(event);
224
+ const payload = event && typeof event === "object" ? { ...event } : {};
225
+ if (payload.origin == null) delete payload.origin;
226
+ callback(payload);
188
227
  } catch {
189
228
  // ignore callback failures
190
229
  }
@@ -383,7 +422,11 @@ function buildCoreToolSpecs() {
383
422
  type: "function",
384
423
  function: {
385
424
  name: "artifact_read",
386
- description: "Load a stored artifact by artifactId. Use selectors startLine/endLine, maxChars, or tailLines to read a slice.",
425
+ description: [
426
+ "Read previously stored tool output by artifactId.",
427
+ "This does not read workspace files; use `read` for repository paths.",
428
+ "Optionally read a slice with startLine/endLine, maxChars, or tailLines.",
429
+ ].join(" "),
387
430
  parameters: {
388
431
  type: "object",
389
432
  properties: {
@@ -398,6 +441,128 @@ function buildCoreToolSpecs() {
398
441
  },
399
442
  },
400
443
  },
444
+ {
445
+ type: "function",
446
+ function: {
447
+ name: "plan_graph",
448
+ description: [
449
+ "Manage the persistent Plan Graph and asynchronous TaskRuns.",
450
+ "Use create, patch, inspect, or cancel_graph for graph operations, and control for TaskRun lifecycle.",
451
+ "`control.start_task` starts a `task_loop` asynchronously and returns immediately.",
452
+ "Use `inline_llm` for work handled by the current graph owner,",
453
+ "`expand` for tasks that must be lowered into child nodes,",
454
+ "and `task_loop` for asynchronous work in an independent TaskLoop.",
455
+ "Do not call `plan_graph` together with data-plane tools in the same assistant turn.",
456
+ ].join(" "),
457
+ parameters: {
458
+ type: "object",
459
+ properties: {
460
+ operation: {
461
+ type: "string",
462
+ enum: [
463
+ "create",
464
+ "patch",
465
+ "inspect",
466
+ "clear",
467
+ "cancel_graph",
468
+ "control",
469
+ ],
470
+ description: [
471
+ "create/patch/inspect/cancel_graph mutate or inspect the graph spec;",
472
+ "control runs TaskRun lifecycle and node status actions.",
473
+ ].join(" "),
474
+ },
475
+ graph: {
476
+ type: "object",
477
+ description: "Full graph for create (objective + nodes). group is input sugar only.",
478
+ },
479
+ operations: {
480
+ type: "array",
481
+ description: [
482
+ "Patch ops only: add_node, expand_node, add_dependency, remove_dependency.",
483
+ "Status actions (complete_task, skip_node, cancel_subtree) belong under control.actions.",
484
+ ].join(" "),
485
+ items: { type: "object" },
486
+ },
487
+ actions: {
488
+ type: "array",
489
+ description: [
490
+ "Control actions: start_task, cancel_task, fail_task, complete_task, skip_node, cancel_subtree.",
491
+ "complete_task with taskRunId finishes a TaskLoop TaskRun;",
492
+ "complete_task with nodeId finishes a waiting_llm inline task owned by the graph owner.",
493
+ ].join(" "),
494
+ items: { type: "object" },
495
+ },
496
+ reason: {
497
+ type: "string",
498
+ description: "Optional reason for cancel_graph or fail/cancel task.",
499
+ },
500
+ commandId: {
501
+ type: "string",
502
+ description: [
503
+ "Optional idempotency key for explicit replay.",
504
+ "When omitted, the Runtime should derive one from the tool invocation when available.",
505
+ ].join(" "),
506
+ },
507
+ expectedSpecRevision: {
508
+ type: "integer",
509
+ description: "Optional optimistic concurrency token for patch.",
510
+ },
511
+ graphId: {
512
+ type: "string",
513
+ description: "Optional graph id check for patch/control.",
514
+ },
515
+ },
516
+ required: ["operation"],
517
+ },
518
+ },
519
+ },
520
+ {
521
+ type: "function",
522
+ function: {
523
+ name: "ask_user",
524
+ description: [
525
+ "Ask the user for input and pause the current Agent loop until the reply arrives.",
526
+ "Use only when user input is required to proceed, not for routine updates or decisions the agent can safely make.",
527
+ "`kind=approval` requests yes/no confirmation; `kind=choice` presents the supplied options; `kind=chat` requests free text.",
528
+ "This must be the only tool call in the turn.",
529
+ "The reply is returned only as this tool result, not as a separate user message or pending user prompt.",
530
+ "After the tool returns, continue from the answer and do not ask the same question again.",
531
+ "Running TaskRuns are not paused automatically.",
532
+ ].join(" "),
533
+ parameters: {
534
+ type: "object",
535
+ properties: {
536
+ kind: {
537
+ type: "string",
538
+ enum: ["approval", "choice", "chat"],
539
+ description: "Interaction type.",
540
+ },
541
+ prompt: {
542
+ type: "string",
543
+ description: "Question shown to the user.",
544
+ },
545
+ options: {
546
+ type: "array",
547
+ description: "For choice: option labels (or {key,label} objects). Ignored for chat.",
548
+ items: {
549
+ oneOf: [
550
+ { type: "string" },
551
+ {
552
+ type: "object",
553
+ properties: {
554
+ key: { type: "string" },
555
+ label: { type: "string" },
556
+ },
557
+ },
558
+ ],
559
+ },
560
+ },
561
+ },
562
+ required: ["kind", "prompt"],
563
+ },
564
+ },
565
+ },
401
566
  ];
402
567
  }
403
568
 
@@ -526,8 +691,10 @@ function runCoreTool({
526
691
  workspaceRoot = process.cwd(),
527
692
  onToolEvent = null,
528
693
  sessionId = "",
529
- contextV2 = false,
530
694
  onArtifactPersisted = null,
695
+ executionState = null,
696
+ origin = null,
697
+ resume = null,
531
698
  } = {}) {
532
699
  const normalizedTool = normalizeToolName(tool);
533
700
  if (!normalizedTool) {
@@ -536,6 +703,7 @@ function runCoreTool({
536
703
  phase: "error",
537
704
  args: args && typeof args === "object" ? { ...args } : {},
538
705
  error: `unsupported tool: ${tool}`,
706
+ origin,
539
707
  });
540
708
  return {
541
709
  ok: false,
@@ -552,8 +720,86 @@ function runCoreTool({
552
720
  phase: "start",
553
721
  args: safeArgs,
554
722
  error: "",
723
+ origin,
555
724
  });
556
725
 
726
+ if (normalizedTool === "plan_graph") {
727
+ const state = executionState && typeof executionState === "object"
728
+ ? executionState
729
+ : emptyExecutionState();
730
+ const result = runPlanGraphCommand(safeArgs, {
731
+ executionState: state,
732
+ autoAdvance: true,
733
+ parallel: true,
734
+ runTool: ({ node, args: nestedArgs, tool: nestedTool, stepId }) => {
735
+ const nested = runCoreTool({
736
+ tool: nestedTool,
737
+ args: nestedArgs,
738
+ workspaceRoot,
739
+ onToolEvent,
740
+ sessionId,
741
+ onArtifactPersisted,
742
+ executionState: state,
743
+ origin: {
744
+ kind: "plan_graph",
745
+ graphId: String(state.planGraph && state.planGraph.graphId || ""),
746
+ graphRevision: Number(state.planGraph && state.planGraph.specRevision) || 0,
747
+ commandRevision: Number(state.planGraph && state.planGraph.specRevision) || 0,
748
+ nodeId: stepId || (node && node.id) || "",
749
+ attempt: Number(node && node.attempt) || 0,
750
+ },
751
+ });
752
+ return nested;
753
+ },
754
+ });
755
+ if (result.ok === false) {
756
+ emitToolEvent(onToolEvent, {
757
+ tool: "plan_graph",
758
+ phase: "error",
759
+ args: safeArgs,
760
+ error: Array.isArray(result.errors)
761
+ ? result.errors.map((e) => e.message || e.code).join("; ")
762
+ : "plan_graph rejected",
763
+ origin,
764
+ });
765
+ } else {
766
+ syncInteractionFromPlanGraph(result.executionState || state);
767
+ }
768
+ return {
769
+ ...result.modelPayload,
770
+ ok: result.status === "accepted",
771
+ executionState: result.executionState || state,
772
+ };
773
+ }
774
+
775
+ if (normalizedTool === "ask_user") {
776
+ const state = executionState && typeof executionState === "object"
777
+ ? executionState
778
+ : emptyExecutionState();
779
+ const result = runAskUserTool(safeArgs, {
780
+ executionState: state,
781
+ resume: resume || null,
782
+ });
783
+ const ok = result.ok !== false && result.status !== "rejected";
784
+ emitToolEvent(onToolEvent, {
785
+ tool: "ask_user",
786
+ phase: ok ? "end" : "error",
787
+ args: safeArgs,
788
+ result: result.modelPayload || result,
789
+ error: ok ? "" : (result.error || "ask_user rejected"),
790
+ origin,
791
+ });
792
+ return {
793
+ ...(result.modelPayload || result),
794
+ ok,
795
+ status: result.status,
796
+ waiting_user: Boolean(result.waiting_user || result.status === "waiting_user"),
797
+ interactionId: result.interactionId || "",
798
+ executionState: result.executionState || state,
799
+ deferToolResult: ok && result.status === "waiting_user",
800
+ };
801
+ }
802
+
557
803
  const toolOptions = { workspaceRoot, cwd: workspaceRoot };
558
804
  if (normalizedTool === "artifact_read" && sessionId) {
559
805
  toolOptions.sessionId = sessionId;
@@ -569,12 +815,12 @@ function runCoreTool({
569
815
  phase: "error",
570
816
  args: safeArgs,
571
817
  error: String((result && result.error) || `${normalizedTool} failed`),
818
+ origin,
572
819
  });
573
820
  return result;
574
821
  }
575
822
 
576
- const useContextV2 = contextV2 || isContextV2Enabled();
577
- if (useContextV2 && normalizedTool !== "artifact_read") {
823
+ if (normalizedTool !== "artifact_read" && EXECUTABLE_GRAPH_TOOLS.has(normalizedTool)) {
578
824
  const persisted = persistToolResultToContext({
579
825
  workspaceRoot,
580
826
  sessionId,
@@ -589,9 +835,14 @@ function runCoreTool({
589
835
  // ignore
590
836
  }
591
837
  }
592
- return persisted.modelPayload || result;
838
+ const payload = persisted.modelPayload || result;
839
+ if (origin) payload.origin = origin;
840
+ return payload;
593
841
  }
594
842
 
843
+ if (origin && result && typeof result === "object") {
844
+ return { ...result, origin };
845
+ }
595
846
  return result;
596
847
  }
597
848
 
@@ -1222,140 +1473,76 @@ async function runAnthropicTurn({
1222
1473
  });
1223
1474
  }
1224
1475
 
1225
- // Transport descriptors: everything the shared native loop needs that differs
1226
- // between the OpenAI chat-completions and Anthropic messages protocols —
1227
- // request URL resolution, initial message shaping, turn execution, and
1228
- // assistant/tool-result message formatting.
1476
+ const {
1477
+ createOpenAiChatTransport,
1478
+ createAnthropicMessagesTransport,
1479
+ } = require("./providers");
1480
+
1481
+ // Transport descriptors: wire-format only. Plan Mode / leases / policy live in the loop.
1229
1482
  const TRANSPORTS = {
1230
- "openai-chat": {
1483
+ "openai-chat": createOpenAiChatTransport({
1231
1484
  resolveUrl: resolveCompletionUrl,
1232
- prepareMessages({ messages, systemPrompt, prompt }) {
1233
- const systemText = String(systemPrompt || "").trim();
1234
- const hasSystem = messages.some((entry) => String(entry.role || "").trim() === "system");
1235
- if (systemText && !hasSystem) {
1236
- messages.unshift({ role: "system", content: systemText });
1237
- }
1238
- messages.push({ role: "user", content: String(prompt || "") });
1239
- },
1240
1485
  runTurn: runOpenAiLikeTurn,
1241
- getToolCalls(turnResult) {
1242
- return Array.isArray(turnResult.toolCalls)
1243
- ? turnResult.toolCalls.filter((call) => call && call.function && typeof call.function === "object")
1244
- : [];
1245
- },
1246
- appendFinalAssistantMessage({ messages, turnResult }) {
1247
- const text = String(turnResult.text || "").trim();
1248
- if (text) {
1249
- messages.push({
1250
- role: "assistant",
1251
- content: text,
1252
- });
1253
- }
1254
- },
1255
- prepareToolCalls({ messages, toolCalls }) {
1256
- const assistantToolCalls = [];
1257
- for (const call of toolCalls) {
1258
- const callId = String(call.id || `call_${randomUUID()}`);
1259
- const name = normalizeToolName(call.function.name || "");
1260
- const args = normalizeToolCallArgs(call.function.arguments || "");
1261
-
1262
- assistantToolCalls.push({
1263
- id: callId,
1264
- type: "function",
1265
- function: {
1266
- name: name || String(call.function.name || ""),
1267
- arguments: toJsonString(args),
1268
- },
1269
- });
1270
- }
1486
+ normalizeToolName,
1487
+ normalizeToolCallArgs,
1488
+ toJsonString,
1489
+ clipText,
1490
+ }),
1491
+ "anthropic-messages": createAnthropicMessagesTransport({
1492
+ resolveUrl: resolveAnthropicMessagesUrl,
1493
+ runTurn: runAnthropicTurn,
1494
+ toJsonString,
1495
+ clipText,
1496
+ }),
1497
+ };
1271
1498
 
1272
- if (assistantToolCalls.length === 0) return null;
1499
+ function pendingToolCallId(pending = null) {
1500
+ if (!pending || !pending.source) return "";
1501
+ return String(pending.source.id || "").trim();
1502
+ }
1273
1503
 
1274
- messages.push({
1275
- role: "assistant",
1276
- content: null,
1277
- tool_calls: assistantToolCalls,
1278
- });
1504
+ function shadowDeclarePendingCalls(ledger, pendingCalls = []) {
1505
+ const entries = (Array.isArray(pendingCalls) ? pendingCalls : []).map((pending) => ({
1506
+ callId: pendingToolCallId(pending) || `call_${randomUUID()}`,
1507
+ name: String(pending && pending.name || "").trim().toLowerCase(),
1508
+ args: pending && pending.args != null ? pending.args : {},
1509
+ }));
1510
+ // Keep source ids aligned when we had to synthesize.
1511
+ for (let i = 0; i < entries.length; i += 1) {
1512
+ const pending = pendingCalls[i];
1513
+ if (pending && pending.source && !pending.source.id) {
1514
+ pending.source.id = entries[i].callId;
1515
+ }
1516
+ }
1517
+ return declareCalls(ledger, entries);
1518
+ }
1279
1519
 
1280
- return assistantToolCalls.map((toolCall) => ({
1281
- name: toolCall.function.name,
1282
- args: normalizeToolCallArgs(toolCall.function.arguments),
1283
- source: toolCall,
1284
- }));
1285
- },
1286
- appendToolResult({ messages, call, toolResult }) {
1287
- messages.push({
1288
- role: "tool",
1289
- tool_call_id: call.source.id,
1290
- content: clipText(toJsonString(toolResult), 12000),
1291
- });
1292
- },
1293
- },
1294
- "anthropic-messages": {
1295
- resolveUrl: resolveAnthropicMessagesUrl,
1296
- prepareMessages({ messages, prompt }) {
1297
- messages.push({
1298
- role: "user",
1299
- content: String(prompt || ""),
1300
- });
1301
- },
1302
- runTurn: runAnthropicTurn,
1303
- getToolCalls(turnResult) {
1304
- return Array.isArray(turnResult.toolCalls) ? turnResult.toolCalls : [];
1305
- },
1306
- appendFinalAssistantMessage({ messages, turnResult }) {
1307
- const assistantContent = Array.isArray(turnResult.assistantContent)
1308
- ? turnResult.assistantContent
1309
- : [];
1310
- if (assistantContent.length > 0) {
1311
- messages.push({
1312
- role: "assistant",
1313
- content: assistantContent,
1314
- });
1315
- } else if (String(turnResult.text || "").trim()) {
1316
- messages.push({
1317
- role: "assistant",
1318
- content: [
1319
- {
1320
- type: "text",
1321
- text: String(turnResult.text || ""),
1322
- },
1323
- ],
1324
- });
1325
- }
1326
- },
1327
- prepareToolCalls({ messages, turnResult, toolCalls }) {
1328
- const assistantContent = Array.isArray(turnResult.assistantContent)
1329
- ? turnResult.assistantContent
1330
- : [];
1331
-
1332
- messages.push({
1333
- role: "assistant",
1334
- content: assistantContent,
1335
- });
1520
+ function shadowResolvePending(ledger, pending, toolResult) {
1521
+ if (!ledger) return;
1522
+ const callId = pendingToolCallId(pending);
1523
+ if (!callId) return;
1524
+ resolveCall(ledger, callId, {
1525
+ result: toolResult,
1526
+ isError: Boolean(!toolResult || toolResult.ok === false),
1527
+ });
1528
+ }
1336
1529
 
1337
- return toolCalls.map((call) => ({
1338
- name: call.name,
1339
- args: call.args,
1340
- source: call,
1341
- }));
1342
- },
1343
- appendToolResult({ collected, call, toolResult }) {
1344
- collected.push({
1345
- type: "tool_result",
1346
- tool_use_id: String(call.source.id || ""),
1347
- content: clipText(toJsonString(toolResult), 12000),
1348
- is_error: Boolean(!toolResult || toolResult.ok === false),
1349
- });
1350
- },
1351
- flushToolResults({ messages, collected }) {
1352
- messages.push({
1353
- role: "user",
1354
- content: collected,
1355
- });
1356
- },
1357
- },
1358
- };
1530
+ function pendingByIdMap(pendingCalls = []) {
1531
+ const map = Object.create(null);
1532
+ for (const pending of pendingCalls) {
1533
+ const id = pendingToolCallId(pending);
1534
+ if (id) map[id] = pending;
1535
+ }
1536
+ return map;
1537
+ }
1538
+
1539
+ function flushLedgerToolResults(ledger, transport, messages, pendingCalls) {
1540
+ return materializeResolvedToolResults(ledger, {
1541
+ transport,
1542
+ messages,
1543
+ pendingById: pendingByIdMap(pendingCalls),
1544
+ });
1545
+ }
1359
1546
 
1360
1547
  async function runNativeLoop({
1361
1548
  transport,
@@ -1375,9 +1562,10 @@ async function runNativeLoop({
1375
1562
  onToolEvent = null,
1376
1563
  onArtifactPersisted = null,
1377
1564
  sessionId = "",
1378
- contextV2 = false,
1379
1565
  signal = null,
1380
1566
  guards,
1567
+ executionState: initialExecutionState = null,
1568
+ resume = false,
1381
1569
  } = {}) {
1382
1570
  const requestModel = String(model || "").trim();
1383
1571
  if (!requestModel) {
@@ -1390,19 +1578,50 @@ async function runNativeLoop({
1390
1578
  }
1391
1579
 
1392
1580
  const messages = sanitizeModelMessages(cloneMessageList(historyMessages));
1393
- transport.prepareMessages({ messages, systemPrompt, prompt });
1581
+ if (!resume) {
1582
+ transport.prepareMessages({ messages, systemPrompt, prompt });
1583
+ }
1394
1584
 
1395
1585
  let aggregated = "";
1396
1586
  let streamed = false;
1397
1587
  let toolCallsExecuted = 0;
1398
1588
  let toolErrors = 0;
1399
- let executionState = emptyExecutionState();
1589
+ let executionState = initialExecutionState && typeof initialExecutionState === "object"
1590
+ ? initialExecutionState
1591
+ : emptyExecutionState();
1592
+ if (typeof executionState.planMode !== "boolean") executionState.planMode = false;
1593
+ ensurePendingUserPrompts(executionState);
1400
1594
  const toolBudget = resolveNativeToolBudget();
1401
1595
  const usage = createUsageTotals();
1596
+ // Shadow Tool Call Ledger (R1). Observes declare/defer/resolve; does not
1597
+ // materialize Provider messages yet. STRICT via UFOO_UCODE_PROTOCOL_STRICT=1.
1598
+ let activeLedger = null;
1599
+ let lastProtocolLedger = null;
1600
+
1601
+ if (resume) {
1602
+ await withFaultPoint("before_provider_resume", () => {});
1603
+ }
1604
+
1605
+ function injectPendingUserReminders() {
1606
+ const nudges = drainUserPrompts(executionState);
1607
+ if (nudges.length === 0) return;
1608
+ const waiting = executionState.planGraph && executionState.planGraph.waitingFor
1609
+ ? executionState.planGraph.waitingFor
1610
+ : null;
1611
+ const content = formatUserReminderMessage(nudges, { waitingFor: waiting });
1612
+ if (!content) return;
1613
+ messages.push({ role: "user", content });
1614
+ }
1402
1615
 
1403
1616
  while (true) {
1404
1617
  guards.ensureActive();
1405
1618
 
1619
+ injectPendingUserReminders();
1620
+
1621
+ if (activeLedger) {
1622
+ runProviderTurnGate(activeLedger);
1623
+ }
1624
+
1406
1625
  const turnResult = await transport.runTurn({
1407
1626
  url: requestUrl,
1408
1627
  apiKey,
@@ -1433,31 +1652,33 @@ async function runNativeLoop({
1433
1652
 
1434
1653
  if (toolCalls.length === 0) {
1435
1654
  const text = String(turnResult.text || "").trim();
1436
- if (contextV2) {
1437
- const sideEffects = parseStructuredSideEffects(text);
1438
- const segment = parseExecutionSegment(sideEffects);
1439
- if (segment && segment.steps && segment.steps.length > 0) {
1655
+ const sideEffects = parseStructuredSideEffects(text);
1656
+ const planCommand = sideEffects ? normalizePlanGraphCommand(sideEffects) : null;
1657
+ if (planCommand) {
1440
1658
  transport.appendFinalAssistantMessage({ messages, turnResult });
1441
- const exec = executeExecutionSegment({
1442
- segment,
1659
+ const planResult = runCoreTool({
1660
+ tool: "plan_graph",
1661
+ args: planCommand,
1662
+ workspaceRoot,
1663
+ onToolEvent,
1664
+ sessionId,
1665
+ onArtifactPersisted,
1443
1666
  executionState,
1444
- runStep: ({ tool, args }) => runCoreTool({
1445
- tool,
1446
- args,
1447
- workspaceRoot,
1448
- onToolEvent,
1449
- sessionId,
1450
- contextV2,
1451
- onArtifactPersisted,
1452
- }),
1667
+ origin: { kind: "legacy_side_effect", source: planCommand.source || "legacy" },
1453
1668
  });
1454
- executionState = exec.executionState;
1669
+ if (planResult && planResult.executionState) {
1670
+ executionState = planResult.executionState;
1671
+ }
1455
1672
  messages.push({
1456
1673
  role: "user",
1457
- content: formatSegmentResultMessage(exec),
1674
+ content: JSON.stringify({
1675
+ type: "plan_graph_result",
1676
+ ...((planResult && planResult.status)
1677
+ ? planResult
1678
+ : { status: "rejected", ok: false, error: "plan_graph failed" }),
1679
+ }),
1458
1680
  });
1459
1681
  continue;
1460
- }
1461
1682
  }
1462
1683
  transport.appendFinalAssistantMessage({ messages, turnResult });
1463
1684
  if (!aggregated.trim() && text) {
@@ -1469,6 +1690,8 @@ async function runNativeLoop({
1469
1690
  toolCallsExecuted,
1470
1691
  messages,
1471
1692
  usage,
1693
+ executionState,
1694
+ protocolLedger: lastProtocolLedger || snapshotLedger(activeLedger),
1472
1695
  };
1473
1696
  }
1474
1697
 
@@ -1480,24 +1703,161 @@ async function runNativeLoop({
1480
1703
  toolCallsExecuted,
1481
1704
  messages,
1482
1705
  usage,
1706
+ executionState,
1707
+ protocolLedger: lastProtocolLedger || snapshotLedger(activeLedger),
1708
+ };
1709
+ }
1710
+
1711
+ activeLedger = createToolCallLedger({ provider, sessionId });
1712
+ shadowDeclarePendingCalls(activeLedger, pendingCalls);
1713
+ lastProtocolLedger = snapshotLedger(activeLedger);
1714
+ await withFaultPoint("after_prepare_tool_calls", () => {});
1715
+ await withFaultPoint("before_tool_exec", () => {});
1716
+
1717
+ const callNames = pendingCalls.map((call) => String(call.name || "").trim().toLowerCase());
1718
+ const hasPlanGraph = callNames.includes("plan_graph");
1719
+ const hasAskUser = callNames.includes("ask_user");
1720
+ const hasDataTool = callNames.some((name) => EXECUTABLE_GRAPH_TOOLS.has(name));
1721
+ if (hasPlanGraph && hasDataTool) {
1722
+ // prepareToolCalls already appended the assistant tool_calls / tool_use
1723
+ // message; every declared call must get a contiguous tool result via ledger.
1724
+ const rejected = {
1725
+ ok: false,
1726
+ status: "rejected",
1727
+ error: "Do not mix plan_graph with data-plane tools in the same turn",
1728
+ code: "MIXED_PLAN_AND_DATA_TOOLS",
1483
1729
  };
1730
+ for (const pending of pendingCalls) {
1731
+ shadowResolvePending(activeLedger, pending, rejected);
1732
+ toolCallsExecuted += 1;
1733
+ toolErrors += 1;
1734
+ }
1735
+ flushLedgerToolResults(activeLedger, transport, messages, pendingCalls);
1736
+ lastProtocolLedger = snapshotLedger(activeLedger);
1737
+ continue;
1738
+ }
1739
+ if (hasAskUser && pendingCalls.length > 1) {
1740
+ const rejected = {
1741
+ ok: false,
1742
+ status: "rejected",
1743
+ error: "ask_user must be the only tool call in the turn",
1744
+ code: "ASK_USER_MUST_BE_ALONE",
1745
+ };
1746
+ for (const pending of pendingCalls) {
1747
+ shadowResolvePending(activeLedger, pending, rejected);
1748
+ toolCallsExecuted += 1;
1749
+ toolErrors += 1;
1750
+ }
1751
+ flushLedgerToolResults(activeLedger, transport, messages, pendingCalls);
1752
+ lastProtocolLedger = snapshotLedger(activeLedger);
1753
+ continue;
1484
1754
  }
1485
1755
 
1486
- const collectedResults = [];
1756
+ let deferredAskUser = null;
1487
1757
  for (const pending of pendingCalls) {
1758
+ const pendingName = String(pending.name || "").trim().toLowerCase();
1759
+ if (
1760
+ EXECUTABLE_GRAPH_TOOLS.has(pendingName)
1761
+ && activePlanRequiresExpansion(executionState.planGraph)
1762
+ ) {
1763
+ const blocked = {
1764
+ ok: false,
1765
+ status: "rejected",
1766
+ errors: [{
1767
+ code: "ACTIVE_PLAN_REQUIRES_EXPANSION",
1768
+ message: "Active plan is waiting on a task; use plan_graph expand_node or control.complete_task instead of direct tools",
1769
+ }],
1770
+ };
1771
+ toolCallsExecuted += 1;
1772
+ toolErrors += 1;
1773
+ shadowResolvePending(activeLedger, pending, blocked);
1774
+ continue;
1775
+ }
1776
+ if (planModeBlocksDirectTool(pendingName, executionState)) {
1777
+ const blocked = {
1778
+ ok: false,
1779
+ status: "rejected",
1780
+ errors: [{
1781
+ code: "PLAN_MODE_BLOCKS_SIDE_EFFECT",
1782
+ message: "Plan mode is on; use plan_graph for write/edit/bash, or ask the user to /plan off",
1783
+ }],
1784
+ };
1785
+ toolCallsExecuted += 1;
1786
+ toolErrors += 1;
1787
+ shadowResolvePending(activeLedger, pending, blocked);
1788
+ continue;
1789
+ }
1790
+ const leaseCheck = checkWriteAllowed(executionState, {
1791
+ tool: pendingName,
1792
+ originKind: "agent_loop",
1793
+ });
1794
+ if (!leaseCheck.ok) {
1795
+ const blocked = {
1796
+ ok: false,
1797
+ status: "rejected",
1798
+ errors: [{
1799
+ code: leaseCheck.code || "WORKSPACE_WRITE_LEASE_HELD",
1800
+ message: leaseCheck.message
1801
+ || "Workspace write lease held by an active TaskRun",
1802
+ owner: leaseCheck.owner || null,
1803
+ }],
1804
+ };
1805
+ toolCallsExecuted += 1;
1806
+ toolErrors += 1;
1807
+ shadowResolvePending(activeLedger, pending, blocked);
1808
+ continue;
1809
+ }
1810
+
1811
+ const toolCallId = pending.source && (pending.source.id || (pending.source.function && pending.source.id))
1812
+ ? String(pending.source.id || "")
1813
+ : "";
1814
+ const resumeForAsk = pendingName === "ask_user"
1815
+ ? {
1816
+ toolCallId: toolCallId || String((pending.source && pending.source.id) || `call_${randomUUID()}`),
1817
+ toolName: "ask_user",
1818
+ call: {
1819
+ name: pending.name,
1820
+ args: pending.args,
1821
+ source: pending.source,
1822
+ },
1823
+ }
1824
+ : null;
1825
+
1826
+ markExecuting(activeLedger, pendingToolCallId(pending));
1488
1827
  const toolResult = runCoreTool({
1489
1828
  tool: pending.name,
1490
1829
  args: pending.args,
1491
1830
  workspaceRoot,
1492
1831
  onToolEvent,
1493
1832
  sessionId,
1494
- contextV2,
1495
1833
  onArtifactPersisted,
1834
+ executionState,
1835
+ resume: resumeForAsk,
1496
1836
  });
1837
+ if (toolResult && toolResult.executionState) {
1838
+ executionState = toolResult.executionState;
1839
+ }
1497
1840
  toolCallsExecuted += 1;
1498
1841
  if (!toolResult || toolResult.ok === false) {
1499
1842
  toolErrors += 1;
1500
1843
  }
1844
+
1845
+ if (pendingName === "ask_user" && toolResult && toolResult.deferToolResult) {
1846
+ // Attach resume metadata onto pending interaction for contiguous tool_result later.
1847
+ const pendingInteraction = getPendingUserInteraction(executionState);
1848
+ if (pendingInteraction) {
1849
+ pendingInteraction.resume = {
1850
+ ...(pendingInteraction.resume || {}),
1851
+ ...(resumeForAsk || {}),
1852
+ mode: "ask_user",
1853
+ transport: provider === "anthropic" ? "anthropic-messages" : "openai-chat",
1854
+ };
1855
+ }
1856
+ deferCall(activeLedger, pendingToolCallId(pending), { reason: "ask_user" });
1857
+ deferredAskUser = { call: pending, interactionId: toolResult.interactionId || "" };
1858
+ continue;
1859
+ }
1860
+
1501
1861
  enforceNativeToolBudget({
1502
1862
  toolCallsExecuted,
1503
1863
  toolErrors,
@@ -1506,18 +1866,62 @@ async function runNativeLoop({
1506
1866
  lastTool: pending.name,
1507
1867
  lastError: toolResult && toolResult.error ? String(toolResult.error) : "",
1508
1868
  });
1509
- transport.appendToolResult({
1869
+ shadowResolvePending(activeLedger, pending, toolResult);
1870
+ }
1871
+
1872
+ flushLedgerToolResults(activeLedger, transport, messages, pendingCalls);
1873
+ lastProtocolLedger = snapshotLedger(activeLedger);
1874
+
1875
+ if (deferredAskUser) {
1876
+ return {
1877
+ text: aggregated,
1878
+ streamed,
1879
+ toolCallsExecuted,
1510
1880
  messages,
1511
- collected: collectedResults,
1512
- call: pending,
1513
- toolResult,
1514
- });
1881
+ usage,
1882
+ executionState,
1883
+ waitingUserInteraction: true,
1884
+ interactionId: deferredAskUser.interactionId || "",
1885
+ protocolLedger: lastProtocolLedger,
1886
+ };
1887
+ }
1888
+
1889
+ if (hasPendingUserInteraction(executionState)) {
1890
+ // Checkpoint approval synced from plan_graph — pause for TUI.
1891
+ return {
1892
+ text: aggregated,
1893
+ streamed,
1894
+ toolCallsExecuted,
1895
+ messages,
1896
+ usage,
1897
+ executionState,
1898
+ waitingUserInteraction: true,
1899
+ interactionId: (getPendingUserInteraction(executionState) || {}).id || "",
1900
+ protocolLedger: lastProtocolLedger,
1901
+ };
1515
1902
  }
1903
+ }
1904
+ }
1516
1905
 
1517
- if (typeof transport.flushToolResults === "function") {
1518
- transport.flushToolResults({ messages, collected: collectedResults });
1906
+ function appendAnswerToolResult(messages = [], resume = null, answer = {}, options = {}) {
1907
+ const materialized = materializeAnswerToolResult(messages, resume, answer);
1908
+ if (!materialized.ok) return materialized;
1909
+ const ledger = options && options.ledger ? options.ledger : null;
1910
+ if (ledger) {
1911
+ const call = resume && resume.call ? resume.call : null;
1912
+ const callId = String(
1913
+ (call && call.source && call.source.id) || (resume && resume.toolCallId) || ""
1914
+ ).trim();
1915
+ if (callId) {
1916
+ resolveCall(ledger, callId, {
1917
+ result: answer,
1918
+ isError: false,
1919
+ allowFromDeferred: true,
1920
+ });
1519
1921
  }
1520
1922
  }
1923
+ checkFaultPoint("after_answer_commit");
1924
+ return { ok: true };
1521
1925
  }
1522
1926
 
1523
1927
  async function runNativeAgentTask({
@@ -1535,8 +1939,9 @@ async function runNativeAgentTask({
1535
1939
  onPhase = null,
1536
1940
  onToolEvent = null,
1537
1941
  onArtifactPersisted = null,
1538
- contextV2 = false,
1539
1942
  signal = null,
1943
+ executionState = null,
1944
+ resume = false,
1540
1945
  } = {}) {
1541
1946
  const guards = createGuards({ signal, timeoutMs });
1542
1947
  const nextSessionId = String(sessionId || "").trim() || `native-${randomUUID()}`;
@@ -1556,7 +1961,7 @@ async function runNativeAgentTask({
1556
1961
  try {
1557
1962
  guards.ensureActive();
1558
1963
 
1559
- if (!promptText) {
1964
+ if (!resume && !promptText) {
1560
1965
  return {
1561
1966
  ok: false,
1562
1967
  error: "empty task",
@@ -1594,7 +1999,7 @@ async function runNativeAgentTask({
1594
1999
  const runResult = await runNativeLoop({
1595
2000
  transport,
1596
2001
  workspaceRoot,
1597
- prompt: promptText,
2002
+ prompt: resume ? "" : promptText,
1598
2003
  systemPrompt,
1599
2004
  systemBlocks,
1600
2005
  historyMessages: messages,
@@ -1609,9 +2014,10 @@ async function runNativeAgentTask({
1609
2014
  onToolEvent,
1610
2015
  onArtifactPersisted,
1611
2016
  sessionId: nextSessionId,
1612
- contextV2: contextV2 || isContextV2Enabled(),
1613
2017
  signal,
1614
2018
  guards,
2019
+ executionState,
2020
+ resume: Boolean(resume),
1615
2021
  });
1616
2022
 
1617
2023
  const outputText = String(runResult.text || "").trim() || (
@@ -1641,26 +2047,37 @@ async function runNativeAgentTask({
1641
2047
  messages: cloneMessageList(runResult.messages),
1642
2048
  sessionId: nextSessionId,
1643
2049
  usage,
2050
+ executionState: runResult.executionState || executionState || null,
1644
2051
  // The loop marks streamed=true whenever it receives a stream callback;
1645
2052
  // only report it when the caller actually registered one.
1646
2053
  streamed: Boolean(runResult.streamed) && typeof onStreamDelta === "function",
2054
+ waitingUserInteraction: Boolean(runResult.waitingUserInteraction),
2055
+ interactionId: runResult.interactionId || "",
2056
+ protocolLedger: runResult.protocolLedger || null,
1647
2057
  };
1648
2058
  } catch (err) {
1649
2059
  const message = err && err.message ? err.message : "native runner failed";
2060
+ if (executionState && typeof executionState === "object") {
2061
+ clearUserPrompts(executionState);
2062
+ }
1650
2063
  return {
1651
2064
  ok: false,
1652
2065
  error: message,
1653
2066
  output: partialOutput.trim(),
1654
2067
  sessionId: nextSessionId,
1655
2068
  streamed: false,
2069
+ executionState: executionState || null,
1656
2070
  };
1657
2071
  }
1658
2072
  }
1659
2073
 
1660
2074
  module.exports = {
1661
2075
  runNativeAgentTask,
2076
+ appendAnswerToolResult,
1662
2077
  resolveRuntimeConfig,
1663
2078
  resolveCompletionUrl,
1664
2079
  resolveAnthropicMessagesUrl,
1665
2080
  resolveTransport,
2081
+ buildCoreToolSpecs,
2082
+ buildAnthropicToolSpecs,
1666
2083
  };