u-foo 3.0.0 → 3.0.2

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 (47) hide show
  1. package/package.json +1 -1
  2. package/src/agents/prompts/native/tasks.js +4 -1
  3. package/src/app/chat/commandExecutor.js +111 -1
  4. package/src/app/chat/commands.js +2 -1
  5. package/src/app/chat/daemonMessageRouter.js +1 -1
  6. package/src/app/chat/inputSubmitHandler.js +3 -2
  7. package/src/code/agent.js +17 -3
  8. package/src/code/commands.js +3 -3
  9. package/src/code/context/executionSegment.js +5 -0
  10. package/src/code/context/planMode.js +8 -1
  11. package/src/code/context/promptLayers.js +10 -9
  12. package/src/code/dispatch.js +4 -0
  13. package/src/code/index.js +2 -0
  14. package/src/code/modelCommand.js +199 -23
  15. package/src/code/nativeRunner.js +299 -225
  16. package/src/code/protocol/controlPlane.js +93 -0
  17. package/src/code/protocol/faultHarness.js +90 -0
  18. package/src/code/protocol/index.js +20 -0
  19. package/src/code/protocol/loopEvents.js +102 -0
  20. package/src/code/protocol/materialize.js +107 -0
  21. package/src/code/protocol/messageFixtures.js +116 -0
  22. package/src/code/protocol/ownership.js +147 -0
  23. package/src/code/protocol/protocolValidator.js +165 -0
  24. package/src/code/protocol/suspension.js +173 -0
  25. package/src/code/protocol/toolCallLedger.js +222 -0
  26. package/src/code/protocol/transitions.js +97 -0
  27. package/src/code/providers/anthropicMessagesTransport.js +93 -0
  28. package/src/code/providers/index.js +8 -0
  29. package/src/code/providers/modelsCatalog.js +304 -0
  30. package/src/code/providers/openaiChatTransport.js +98 -0
  31. package/src/code/providers/transportContract.js +46 -0
  32. package/src/code/repl.js +45 -29
  33. package/src/code/runtime/taskControl.js +177 -53
  34. package/src/code/runtime/taskFocus.js +30 -10
  35. package/src/code/runtime/taskLoop.js +25 -3
  36. package/src/code/runtime/taskRun.js +172 -2
  37. package/src/code/runtime/workspaceLease.js +41 -0
  38. package/src/code/sessionStore.js +1 -0
  39. package/src/code/taskRoute.js +73 -0
  40. package/src/code/thinkingLevels.js +132 -0
  41. package/src/code/tools/taskRun.js +118 -0
  42. package/src/config.js +10 -1
  43. package/src/ui/format/index.js +48 -3
  44. package/src/ui/ink/ChatApp.js +137 -25
  45. package/src/ui/ink/UcodeApp.js +38 -30
  46. package/src/ui/ink/chatLogModel.js +238 -32
  47. package/src/ui/ink/chatReducer.js +18 -6
@@ -5,6 +5,7 @@ const {
5
5
  resolveKimiUpstreamCredentials,
6
6
  } = require("../agents/providers/credentials/kimi");
7
7
  const { runToolCall } = require("./dispatch");
8
+ const { runTaskRunTool } = require("./tools/taskRun");
8
9
  const { appendUsageRecord } = require("./usageStore");
9
10
  const {
10
11
  persistToolResultToContext,
@@ -34,6 +35,19 @@ const {
34
35
  getPendingUserInteraction,
35
36
  } = require("./context/userInteraction");
36
37
  const { checkWriteAllowed } = require("./runtime/workspaceLease");
38
+ const {
39
+ createToolCallLedger,
40
+ declareCalls,
41
+ markExecuting,
42
+ deferCall,
43
+ resolveCall,
44
+ snapshotLedger,
45
+ runProviderTurnGate,
46
+ withFaultPoint,
47
+ checkFaultPoint,
48
+ materializeResolvedToolResults,
49
+ materializeAnswerToolResult,
50
+ } = require("./protocol");
37
51
  const { stableStringify } = require("./context/stableJson");
38
52
  const { getReadToolDescription } = require("../agents/prompts/native/toolDescriptions/read");
39
53
  const { getWriteToolDescription } = require("../agents/prompts/native/toolDescriptions/write");
@@ -47,9 +61,11 @@ const CORE_TOOL_NAMES = new Set([
47
61
  "bash",
48
62
  "artifact_read",
49
63
  "plan_graph",
64
+ "task_run",
50
65
  "ask_user",
51
66
  ]);
52
67
  const EXECUTABLE_GRAPH_TOOLS = new Set(["read", "write", "edit", "bash", "artifact_read"]);
68
+ const CONTROL_PLANE_TOOLS = new Set(["plan_graph", "task_run"]);
53
69
  const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
54
70
  const DEFAULT_ANTHROPIC_BASE_URL = "https://api.anthropic.com/v1";
55
71
  const DEFAULT_KIMI_BASE_URL = "https://api.kimi.com/coding/v1";
@@ -65,11 +81,9 @@ const DEFAULT_NATIVE_TIMEOUT_MS = 43200000; // 12 hours
65
81
  // via UFOO_UCODE_MAX_TOKENS (positive integer).
66
82
  const DEFAULT_OPENAI_MAX_TOKENS = 131072;
67
83
  const DEFAULT_ANTHROPIC_MAX_TOKENS = 64000;
68
- // Extended thinking is on by default for the anthropic transport; the budget
69
- // stays well below the 64K max_tokens cap as the Messages API requires.
70
- // UFOO_UCODE_THINKING_BUDGET_TOKENS overrides; 0 or a non-numeric value
71
- // disables thinking (the payload then omits the field entirely).
72
- const DEFAULT_ANTHROPIC_THINKING_BUDGET_TOKENS = 10000;
84
+ // Extended thinking defaults live in thinkingLevels.js (medium = 10k).
85
+ // UFOO_UCODE_THINKING=off|low|medium|high|max selects a preset; numeric
86
+ // UFOO_UCODE_THINKING_BUDGET_TOKENS still overrides. 0 disables thinking.
73
87
  // Prompt caching is GA on the current Messages API: cache_control blocks need
74
88
  // no anthropic-beta header. Kept as a constant so the marker shape stays in
75
89
  // one place (system block + last history message, 2 of the 4 allowed
@@ -103,14 +117,40 @@ function resolveMaxTokens(fallback) {
103
117
  return normalizePositiveInt(process.env.UFOO_UCODE_MAX_TOKENS, fallback);
104
118
  }
105
119
 
106
- function resolveThinkingBudgetTokens() {
107
- const raw = process.env.UFOO_UCODE_THINKING_BUDGET_TOKENS;
108
- if (raw === undefined || raw === null || String(raw).trim() === "") {
109
- return DEFAULT_ANTHROPIC_THINKING_BUDGET_TOKENS;
120
+ function resolveThinkingBudgetTokens(options = {}) {
121
+ const { resolveThinkingFromEnvAndConfig } = require("./thinkingLevels");
122
+ const { loadGlobalUcodeConfig } = require("../config");
123
+ let configLevel = String(options.configLevel || "").trim();
124
+ if (!configLevel) {
125
+ try {
126
+ configLevel = String((loadGlobalUcodeConfig() || {}).ucodeThinking || "").trim();
127
+ } catch {
128
+ configLevel = "";
129
+ }
110
130
  }
111
- const parsed = Number.parseInt(String(raw), 10);
112
- if (!Number.isFinite(parsed) || parsed <= 0) return 0;
113
- return Math.floor(parsed);
131
+ const resolved = resolveThinkingFromEnvAndConfig({
132
+ env: options.env || process.env,
133
+ configLevel,
134
+ });
135
+ return resolved.budgetTokens;
136
+ }
137
+
138
+ function resolveReasoningEffort(options = {}) {
139
+ const { resolveThinkingFromEnvAndConfig } = require("./thinkingLevels");
140
+ const { loadGlobalUcodeConfig } = require("../config");
141
+ let configLevel = String(options.configLevel || "").trim();
142
+ if (!configLevel) {
143
+ try {
144
+ configLevel = String((loadGlobalUcodeConfig() || {}).ucodeThinking || "").trim();
145
+ } catch {
146
+ configLevel = "";
147
+ }
148
+ }
149
+ const resolved = resolveThinkingFromEnvAndConfig({
150
+ env: options.env || process.env,
151
+ configLevel,
152
+ });
153
+ return resolved.reasoningEffort || "";
114
154
  }
115
155
 
116
156
  function toUsageInt(value) {
@@ -433,12 +473,13 @@ function buildCoreToolSpecs() {
433
473
  function: {
434
474
  name: "plan_graph",
435
475
  description: [
436
- "Manage the persistent Plan Graph and asynchronous TaskRuns.",
437
- "Use create, patch, inspect, or cancel_graph for graph operations, and control for TaskRun lifecycle.",
438
- "`control.start_task` starts a `task_loop` asynchronously and returns immediately.",
476
+ "Manage the persistent Plan Graph and graph-bound TaskRuns.",
477
+ "TaskRuns are orthogonal to Plan Mode; for a standalone TaskRun without a plan, use `task_run` instead.",
478
+ "Use create, patch, inspect, or cancel_graph for graph operations, and control for graph-bound TaskRun lifecycle.",
479
+ "`control.start_task` starts a graph `task_loop` asynchronously and returns immediately.",
439
480
  "Use `inline_llm` for work handled by the current graph owner,",
440
481
  "`expand` for tasks that must be lowered into child nodes,",
441
- "and `task_loop` for asynchronous work in an independent TaskLoop.",
482
+ "and `task_loop` for asynchronous work in an independent TaskLoop attached to a plan node.",
442
483
  "Do not call `plan_graph` together with data-plane tools in the same assistant turn.",
443
484
  ].join(" "),
444
485
  parameters: {
@@ -504,6 +545,63 @@ function buildCoreToolSpecs() {
504
545
  },
505
546
  },
506
547
  },
548
+ {
549
+ type: "function",
550
+ function: {
551
+ name: "task_run",
552
+ description: [
553
+ "Start, inspect, cancel, fail, or complete a TaskRun.",
554
+ "TaskRuns are orthogonal to Plan Mode and do not require a plan_graph.",
555
+ "Use operation=start with an objective for a standalone single-point TaskRun; it returns immediately.",
556
+ "On complex multi-goal work, decompose into concrete objectives and start one or more TaskRuns.",
557
+ "Use plan_graph control.start_task only when the TaskRun is attached to a plan_graph task_loop node.",
558
+ "Do not call `task_run` together with data-plane tools in the same assistant turn.",
559
+ ].join(" "),
560
+ parameters: {
561
+ type: "object",
562
+ properties: {
563
+ operation: {
564
+ type: "string",
565
+ enum: ["start", "cancel", "fail", "complete", "inspect"],
566
+ description: [
567
+ "start creates a standalone TaskRun from objective;",
568
+ "cancel/fail/complete/inspect address an existing taskRunId",
569
+ "(cancel/fail may also use nodeId for graph-bound runs).",
570
+ ].join(" "),
571
+ },
572
+ objective: {
573
+ type: "string",
574
+ description: "Required for start: concrete TaskRun objective.",
575
+ },
576
+ title: {
577
+ type: "string",
578
+ description: "Optional short title for start.",
579
+ },
580
+ taskRunId: {
581
+ type: "string",
582
+ description: "TaskRun id for cancel, fail, complete, or inspect.",
583
+ },
584
+ nodeId: {
585
+ type: "string",
586
+ description: "Optional graph node id for cancel/fail of a graph-bound TaskRun.",
587
+ },
588
+ reason: {
589
+ type: "string",
590
+ description: "Optional reason for cancel or fail.",
591
+ },
592
+ result: {
593
+ type: "object",
594
+ description: "Optional result payload for complete (TaskLoop owner).",
595
+ },
596
+ commandId: {
597
+ type: "string",
598
+ description: "Optional idempotency key for explicit replay.",
599
+ },
600
+ },
601
+ required: ["operation"],
602
+ },
603
+ },
604
+ },
507
605
  {
508
606
  type: "function",
509
607
  function: {
@@ -759,6 +857,51 @@ function runCoreTool({
759
857
  };
760
858
  }
761
859
 
860
+ if (normalizedTool === "task_run") {
861
+ const state = executionState && typeof executionState === "object"
862
+ ? executionState
863
+ : emptyExecutionState();
864
+ const result = runTaskRunTool(safeArgs, {
865
+ executionState: state,
866
+ runTool: ({ node, args: nestedArgs, tool: nestedTool, stepId }) => {
867
+ const nested = runCoreTool({
868
+ tool: nestedTool,
869
+ args: nestedArgs,
870
+ workspaceRoot,
871
+ onToolEvent,
872
+ sessionId,
873
+ onArtifactPersisted,
874
+ executionState: state,
875
+ origin: {
876
+ kind: "task_run",
877
+ taskRunId: String(safeArgs.taskRunId || ""),
878
+ nodeId: stepId || (node && node.id) || "",
879
+ attempt: Number(node && node.attempt) || 0,
880
+ },
881
+ });
882
+ return nested;
883
+ },
884
+ });
885
+ const ok = result.ok !== false && result.status !== "rejected";
886
+ emitToolEvent(onToolEvent, {
887
+ tool: "task_run",
888
+ phase: ok ? "end" : "error",
889
+ args: safeArgs,
890
+ result,
891
+ error: ok
892
+ ? ""
893
+ : (Array.isArray(result.errors)
894
+ ? result.errors.map((e) => e.message || e.code).join("; ")
895
+ : (result.error || "task_run rejected")),
896
+ origin,
897
+ });
898
+ return {
899
+ ...result,
900
+ ok,
901
+ executionState: result.executionState || state,
902
+ };
903
+ }
904
+
762
905
  if (normalizedTool === "ask_user") {
763
906
  const state = executionState && typeof executionState === "object"
764
907
  ? executionState
@@ -957,6 +1100,12 @@ async function runOpenAiLikeTurn({
957
1100
  // Kimi k3 rejects any temperature other than 1.
958
1101
  temperature: normalizeProvider(provider) === "kimi" ? 1 : 0,
959
1102
  };
1103
+ const reasoningEffort = resolveReasoningEffort();
1104
+ if (reasoningEffort) {
1105
+ // OpenAI-compatible gateways that support reasoning models accept this;
1106
+ // unknown fields are typically ignored by plain chat models.
1107
+ payload.reasoning_effort = reasoningEffort;
1108
+ }
960
1109
 
961
1110
  const headers = {
962
1111
  "content-type": "application/json",
@@ -1460,140 +1609,76 @@ async function runAnthropicTurn({
1460
1609
  });
1461
1610
  }
1462
1611
 
1463
- // Transport descriptors: everything the shared native loop needs that differs
1464
- // between the OpenAI chat-completions and Anthropic messages protocols —
1465
- // request URL resolution, initial message shaping, turn execution, and
1466
- // assistant/tool-result message formatting.
1612
+ const {
1613
+ createOpenAiChatTransport,
1614
+ createAnthropicMessagesTransport,
1615
+ } = require("./providers");
1616
+
1617
+ // Transport descriptors: wire-format only. Plan Mode / leases / policy live in the loop.
1467
1618
  const TRANSPORTS = {
1468
- "openai-chat": {
1619
+ "openai-chat": createOpenAiChatTransport({
1469
1620
  resolveUrl: resolveCompletionUrl,
1470
- prepareMessages({ messages, systemPrompt, prompt }) {
1471
- const systemText = String(systemPrompt || "").trim();
1472
- const hasSystem = messages.some((entry) => String(entry.role || "").trim() === "system");
1473
- if (systemText && !hasSystem) {
1474
- messages.unshift({ role: "system", content: systemText });
1475
- }
1476
- messages.push({ role: "user", content: String(prompt || "") });
1477
- },
1478
1621
  runTurn: runOpenAiLikeTurn,
1479
- getToolCalls(turnResult) {
1480
- return Array.isArray(turnResult.toolCalls)
1481
- ? turnResult.toolCalls.filter((call) => call && call.function && typeof call.function === "object")
1482
- : [];
1483
- },
1484
- appendFinalAssistantMessage({ messages, turnResult }) {
1485
- const text = String(turnResult.text || "").trim();
1486
- if (text) {
1487
- messages.push({
1488
- role: "assistant",
1489
- content: text,
1490
- });
1491
- }
1492
- },
1493
- prepareToolCalls({ messages, toolCalls }) {
1494
- const assistantToolCalls = [];
1495
- for (const call of toolCalls) {
1496
- const callId = String(call.id || `call_${randomUUID()}`);
1497
- const name = normalizeToolName(call.function.name || "");
1498
- const args = normalizeToolCallArgs(call.function.arguments || "");
1499
-
1500
- assistantToolCalls.push({
1501
- id: callId,
1502
- type: "function",
1503
- function: {
1504
- name: name || String(call.function.name || ""),
1505
- arguments: toJsonString(args),
1506
- },
1507
- });
1508
- }
1622
+ normalizeToolName,
1623
+ normalizeToolCallArgs,
1624
+ toJsonString,
1625
+ clipText,
1626
+ }),
1627
+ "anthropic-messages": createAnthropicMessagesTransport({
1628
+ resolveUrl: resolveAnthropicMessagesUrl,
1629
+ runTurn: runAnthropicTurn,
1630
+ toJsonString,
1631
+ clipText,
1632
+ }),
1633
+ };
1509
1634
 
1510
- if (assistantToolCalls.length === 0) return null;
1635
+ function pendingToolCallId(pending = null) {
1636
+ if (!pending || !pending.source) return "";
1637
+ return String(pending.source.id || "").trim();
1638
+ }
1511
1639
 
1512
- messages.push({
1513
- role: "assistant",
1514
- content: null,
1515
- tool_calls: assistantToolCalls,
1516
- });
1640
+ function shadowDeclarePendingCalls(ledger, pendingCalls = []) {
1641
+ const entries = (Array.isArray(pendingCalls) ? pendingCalls : []).map((pending) => ({
1642
+ callId: pendingToolCallId(pending) || `call_${randomUUID()}`,
1643
+ name: String(pending && pending.name || "").trim().toLowerCase(),
1644
+ args: pending && pending.args != null ? pending.args : {},
1645
+ }));
1646
+ // Keep source ids aligned when we had to synthesize.
1647
+ for (let i = 0; i < entries.length; i += 1) {
1648
+ const pending = pendingCalls[i];
1649
+ if (pending && pending.source && !pending.source.id) {
1650
+ pending.source.id = entries[i].callId;
1651
+ }
1652
+ }
1653
+ return declareCalls(ledger, entries);
1654
+ }
1517
1655
 
1518
- return assistantToolCalls.map((toolCall) => ({
1519
- name: toolCall.function.name,
1520
- args: normalizeToolCallArgs(toolCall.function.arguments),
1521
- source: toolCall,
1522
- }));
1523
- },
1524
- appendToolResult({ messages, call, toolResult }) {
1525
- messages.push({
1526
- role: "tool",
1527
- tool_call_id: call.source.id,
1528
- content: clipText(toJsonString(toolResult), 12000),
1529
- });
1530
- },
1531
- },
1532
- "anthropic-messages": {
1533
- resolveUrl: resolveAnthropicMessagesUrl,
1534
- prepareMessages({ messages, prompt }) {
1535
- messages.push({
1536
- role: "user",
1537
- content: String(prompt || ""),
1538
- });
1539
- },
1540
- runTurn: runAnthropicTurn,
1541
- getToolCalls(turnResult) {
1542
- return Array.isArray(turnResult.toolCalls) ? turnResult.toolCalls : [];
1543
- },
1544
- appendFinalAssistantMessage({ messages, turnResult }) {
1545
- const assistantContent = Array.isArray(turnResult.assistantContent)
1546
- ? turnResult.assistantContent
1547
- : [];
1548
- if (assistantContent.length > 0) {
1549
- messages.push({
1550
- role: "assistant",
1551
- content: assistantContent,
1552
- });
1553
- } else if (String(turnResult.text || "").trim()) {
1554
- messages.push({
1555
- role: "assistant",
1556
- content: [
1557
- {
1558
- type: "text",
1559
- text: String(turnResult.text || ""),
1560
- },
1561
- ],
1562
- });
1563
- }
1564
- },
1565
- prepareToolCalls({ messages, turnResult, toolCalls }) {
1566
- const assistantContent = Array.isArray(turnResult.assistantContent)
1567
- ? turnResult.assistantContent
1568
- : [];
1569
-
1570
- messages.push({
1571
- role: "assistant",
1572
- content: assistantContent,
1573
- });
1656
+ function shadowResolvePending(ledger, pending, toolResult) {
1657
+ if (!ledger) return;
1658
+ const callId = pendingToolCallId(pending);
1659
+ if (!callId) return;
1660
+ resolveCall(ledger, callId, {
1661
+ result: toolResult,
1662
+ isError: Boolean(!toolResult || toolResult.ok === false),
1663
+ });
1664
+ }
1574
1665
 
1575
- return toolCalls.map((call) => ({
1576
- name: call.name,
1577
- args: call.args,
1578
- source: call,
1579
- }));
1580
- },
1581
- appendToolResult({ collected, call, toolResult }) {
1582
- collected.push({
1583
- type: "tool_result",
1584
- tool_use_id: String(call.source.id || ""),
1585
- content: clipText(toJsonString(toolResult), 12000),
1586
- is_error: Boolean(!toolResult || toolResult.ok === false),
1587
- });
1588
- },
1589
- flushToolResults({ messages, collected }) {
1590
- messages.push({
1591
- role: "user",
1592
- content: collected,
1593
- });
1594
- },
1595
- },
1596
- };
1666
+ function pendingByIdMap(pendingCalls = []) {
1667
+ const map = Object.create(null);
1668
+ for (const pending of pendingCalls) {
1669
+ const id = pendingToolCallId(pending);
1670
+ if (id) map[id] = pending;
1671
+ }
1672
+ return map;
1673
+ }
1674
+
1675
+ function flushLedgerToolResults(ledger, transport, messages, pendingCalls) {
1676
+ return materializeResolvedToolResults(ledger, {
1677
+ transport,
1678
+ messages,
1679
+ pendingById: pendingByIdMap(pendingCalls),
1680
+ });
1681
+ }
1597
1682
 
1598
1683
  async function runNativeLoop({
1599
1684
  transport,
@@ -1644,6 +1729,14 @@ async function runNativeLoop({
1644
1729
  ensurePendingUserPrompts(executionState);
1645
1730
  const toolBudget = resolveNativeToolBudget();
1646
1731
  const usage = createUsageTotals();
1732
+ // Shadow Tool Call Ledger (R1). Observes declare/defer/resolve; does not
1733
+ // materialize Provider messages yet. STRICT via UFOO_UCODE_PROTOCOL_STRICT=1.
1734
+ let activeLedger = null;
1735
+ let lastProtocolLedger = null;
1736
+
1737
+ if (resume) {
1738
+ await withFaultPoint("before_provider_resume", () => {});
1739
+ }
1647
1740
 
1648
1741
  function injectPendingUserReminders() {
1649
1742
  const nudges = drainUserPrompts(executionState);
@@ -1661,6 +1754,10 @@ async function runNativeLoop({
1661
1754
 
1662
1755
  injectPendingUserReminders();
1663
1756
 
1757
+ if (activeLedger) {
1758
+ runProviderTurnGate(activeLedger);
1759
+ }
1760
+
1664
1761
  const turnResult = await transport.runTurn({
1665
1762
  url: requestUrl,
1666
1763
  apiKey,
@@ -1730,6 +1827,7 @@ async function runNativeLoop({
1730
1827
  messages,
1731
1828
  usage,
1732
1829
  executionState,
1830
+ protocolLedger: lastProtocolLedger || snapshotLedger(activeLedger),
1733
1831
  };
1734
1832
  }
1735
1833
 
@@ -1742,61 +1840,55 @@ async function runNativeLoop({
1742
1840
  messages,
1743
1841
  usage,
1744
1842
  executionState,
1843
+ protocolLedger: lastProtocolLedger || snapshotLedger(activeLedger),
1745
1844
  };
1746
1845
  }
1747
1846
 
1847
+ activeLedger = createToolCallLedger({ provider, sessionId });
1848
+ shadowDeclarePendingCalls(activeLedger, pendingCalls);
1849
+ lastProtocolLedger = snapshotLedger(activeLedger);
1850
+ await withFaultPoint("after_prepare_tool_calls", () => {});
1851
+ await withFaultPoint("before_tool_exec", () => {});
1852
+
1748
1853
  const callNames = pendingCalls.map((call) => String(call.name || "").trim().toLowerCase());
1749
- const hasPlanGraph = callNames.includes("plan_graph");
1854
+ const hasControlPlane = callNames.some((name) => CONTROL_PLANE_TOOLS.has(name));
1750
1855
  const hasAskUser = callNames.includes("ask_user");
1751
1856
  const hasDataTool = callNames.some((name) => EXECUTABLE_GRAPH_TOOLS.has(name));
1752
- if (hasPlanGraph && hasDataTool) {
1857
+ if (hasControlPlane && hasDataTool) {
1753
1858
  // prepareToolCalls already appended the assistant tool_calls / tool_use
1754
- // message; every declared call must get a contiguous tool result.
1755
- const collectedResults = [];
1859
+ // message; every declared call must get a contiguous tool result via ledger.
1860
+ const rejected = {
1861
+ ok: false,
1862
+ status: "rejected",
1863
+ error: "Do not mix plan_graph/task_run with data-plane tools in the same turn",
1864
+ code: "MIXED_PLAN_AND_DATA_TOOLS",
1865
+ };
1756
1866
  for (const pending of pendingCalls) {
1757
- transport.appendToolResult({
1758
- messages,
1759
- collected: collectedResults,
1760
- call: pending,
1761
- toolResult: {
1762
- ok: false,
1763
- status: "rejected",
1764
- error: "Do not mix plan_graph with data-plane tools in the same turn",
1765
- code: "MIXED_PLAN_AND_DATA_TOOLS",
1766
- },
1767
- });
1867
+ shadowResolvePending(activeLedger, pending, rejected);
1768
1868
  toolCallsExecuted += 1;
1769
1869
  toolErrors += 1;
1770
1870
  }
1771
- if (typeof transport.flushToolResults === "function") {
1772
- transport.flushToolResults({ messages, collected: collectedResults });
1773
- }
1871
+ flushLedgerToolResults(activeLedger, transport, messages, pendingCalls);
1872
+ lastProtocolLedger = snapshotLedger(activeLedger);
1774
1873
  continue;
1775
1874
  }
1776
1875
  if (hasAskUser && pendingCalls.length > 1) {
1777
- const collectedResults = [];
1876
+ const rejected = {
1877
+ ok: false,
1878
+ status: "rejected",
1879
+ error: "ask_user must be the only tool call in the turn",
1880
+ code: "ASK_USER_MUST_BE_ALONE",
1881
+ };
1778
1882
  for (const pending of pendingCalls) {
1779
- transport.appendToolResult({
1780
- messages,
1781
- collected: collectedResults,
1782
- call: pending,
1783
- toolResult: {
1784
- ok: false,
1785
- status: "rejected",
1786
- error: "ask_user must be the only tool call in the turn",
1787
- code: "ASK_USER_MUST_BE_ALONE",
1788
- },
1789
- });
1883
+ shadowResolvePending(activeLedger, pending, rejected);
1790
1884
  toolCallsExecuted += 1;
1791
1885
  toolErrors += 1;
1792
1886
  }
1793
- if (typeof transport.flushToolResults === "function") {
1794
- transport.flushToolResults({ messages, collected: collectedResults });
1795
- }
1887
+ flushLedgerToolResults(activeLedger, transport, messages, pendingCalls);
1888
+ lastProtocolLedger = snapshotLedger(activeLedger);
1796
1889
  continue;
1797
1890
  }
1798
1891
 
1799
- const collectedResults = [];
1800
1892
  let deferredAskUser = null;
1801
1893
  for (const pending of pendingCalls) {
1802
1894
  const pendingName = String(pending.name || "").trim().toLowerCase();
@@ -1814,12 +1906,7 @@ async function runNativeLoop({
1814
1906
  };
1815
1907
  toolCallsExecuted += 1;
1816
1908
  toolErrors += 1;
1817
- transport.appendToolResult({
1818
- messages,
1819
- collected: collectedResults,
1820
- call: pending,
1821
- toolResult: blocked,
1822
- });
1909
+ shadowResolvePending(activeLedger, pending, blocked);
1823
1910
  continue;
1824
1911
  }
1825
1912
  if (planModeBlocksDirectTool(pendingName, executionState)) {
@@ -1833,12 +1920,7 @@ async function runNativeLoop({
1833
1920
  };
1834
1921
  toolCallsExecuted += 1;
1835
1922
  toolErrors += 1;
1836
- transport.appendToolResult({
1837
- messages,
1838
- collected: collectedResults,
1839
- call: pending,
1840
- toolResult: blocked,
1841
- });
1923
+ shadowResolvePending(activeLedger, pending, blocked);
1842
1924
  continue;
1843
1925
  }
1844
1926
  const leaseCheck = checkWriteAllowed(executionState, {
@@ -1858,12 +1940,7 @@ async function runNativeLoop({
1858
1940
  };
1859
1941
  toolCallsExecuted += 1;
1860
1942
  toolErrors += 1;
1861
- transport.appendToolResult({
1862
- messages,
1863
- collected: collectedResults,
1864
- call: pending,
1865
- toolResult: blocked,
1866
- });
1943
+ shadowResolvePending(activeLedger, pending, blocked);
1867
1944
  continue;
1868
1945
  }
1869
1946
 
@@ -1882,6 +1959,7 @@ async function runNativeLoop({
1882
1959
  }
1883
1960
  : null;
1884
1961
 
1962
+ markExecuting(activeLedger, pendingToolCallId(pending));
1885
1963
  const toolResult = runCoreTool({
1886
1964
  tool: pending.name,
1887
1965
  args: pending.args,
@@ -1911,6 +1989,7 @@ async function runNativeLoop({
1911
1989
  transport: provider === "anthropic" ? "anthropic-messages" : "openai-chat",
1912
1990
  };
1913
1991
  }
1992
+ deferCall(activeLedger, pendingToolCallId(pending), { reason: "ask_user" });
1914
1993
  deferredAskUser = { call: pending, interactionId: toolResult.interactionId || "" };
1915
1994
  continue;
1916
1995
  }
@@ -1923,17 +2002,11 @@ async function runNativeLoop({
1923
2002
  lastTool: pending.name,
1924
2003
  lastError: toolResult && toolResult.error ? String(toolResult.error) : "",
1925
2004
  });
1926
- transport.appendToolResult({
1927
- messages,
1928
- collected: collectedResults,
1929
- call: pending,
1930
- toolResult,
1931
- });
2005
+ shadowResolvePending(activeLedger, pending, toolResult);
1932
2006
  }
1933
2007
 
1934
- if (typeof transport.flushToolResults === "function") {
1935
- transport.flushToolResults({ messages, collected: collectedResults });
1936
- }
2008
+ flushLedgerToolResults(activeLedger, transport, messages, pendingCalls);
2009
+ lastProtocolLedger = snapshotLedger(activeLedger);
1937
2010
 
1938
2011
  if (deferredAskUser) {
1939
2012
  return {
@@ -1945,6 +2018,7 @@ async function runNativeLoop({
1945
2018
  executionState,
1946
2019
  waitingUserInteraction: true,
1947
2020
  interactionId: deferredAskUser.interactionId || "",
2021
+ protocolLedger: lastProtocolLedger,
1948
2022
  };
1949
2023
  }
1950
2024
 
@@ -1959,33 +2033,30 @@ async function runNativeLoop({
1959
2033
  executionState,
1960
2034
  waitingUserInteraction: true,
1961
2035
  interactionId: (getPendingUserInteraction(executionState) || {}).id || "",
2036
+ protocolLedger: lastProtocolLedger,
1962
2037
  };
1963
2038
  }
1964
2039
  }
1965
2040
  }
1966
2041
 
1967
- function appendAnswerToolResult(messages = [], resume = null, answer = {}) {
1968
- const call = resume && resume.call ? resume.call : null;
1969
- if (!call || !call.source) return { ok: false, error: "missing deferred tool call" };
1970
- const transportName = String(resume.transport || "openai-chat");
1971
- const content = clipText(toJsonString(answer), 12000);
1972
- if (transportName === "anthropic-messages") {
1973
- messages.push({
1974
- role: "user",
1975
- content: [{
1976
- type: "tool_result",
1977
- tool_use_id: String(call.source.id || resume.toolCallId || ""),
1978
- content,
1979
- is_error: false,
1980
- }],
1981
- });
1982
- } else {
1983
- messages.push({
1984
- role: "tool",
1985
- tool_call_id: String(call.source.id || resume.toolCallId || ""),
1986
- content,
1987
- });
2042
+ function appendAnswerToolResult(messages = [], resume = null, answer = {}, options = {}) {
2043
+ const materialized = materializeAnswerToolResult(messages, resume, answer);
2044
+ if (!materialized.ok) return materialized;
2045
+ const ledger = options && options.ledger ? options.ledger : null;
2046
+ if (ledger) {
2047
+ const call = resume && resume.call ? resume.call : null;
2048
+ const callId = String(
2049
+ (call && call.source && call.source.id) || (resume && resume.toolCallId) || ""
2050
+ ).trim();
2051
+ if (callId) {
2052
+ resolveCall(ledger, callId, {
2053
+ result: answer,
2054
+ isError: false,
2055
+ allowFromDeferred: true,
2056
+ });
2057
+ }
1988
2058
  }
2059
+ checkFaultPoint("after_answer_commit");
1989
2060
  return { ok: true };
1990
2061
  }
1991
2062
 
@@ -2118,6 +2189,7 @@ async function runNativeAgentTask({
2118
2189
  streamed: Boolean(runResult.streamed) && typeof onStreamDelta === "function",
2119
2190
  waitingUserInteraction: Boolean(runResult.waitingUserInteraction),
2120
2191
  interactionId: runResult.interactionId || "",
2192
+ protocolLedger: runResult.protocolLedger || null,
2121
2193
  };
2122
2194
  } catch (err) {
2123
2195
  const message = err && err.message ? err.message : "native runner failed";
@@ -2142,6 +2214,8 @@ module.exports = {
2142
2214
  resolveCompletionUrl,
2143
2215
  resolveAnthropicMessagesUrl,
2144
2216
  resolveTransport,
2217
+ resolveThinkingBudgetTokens,
2218
+ resolveReasoningEffort,
2145
2219
  buildCoreToolSpecs,
2146
2220
  buildAnthropicToolSpecs,
2147
2221
  };