u-foo 3.0.0 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "u-foo",
3
- "version": "3.0.0",
3
+ "version": "3.0.1",
4
4
  "description": "Multi-Agent Workspace Protocol. Just add u. claude → uclaude, codex → ucodex.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "homepage": "https://ufoo.dev",
package/src/code/agent.js CHANGED
@@ -520,9 +520,22 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
520
520
  : null;
521
521
  const pushToolLog = createToolLogCollector(logs, onToolLog);
522
522
 
523
- // Detect bug fix tasks and use decomposed runner
524
- const isBugFixTask = /\b(?:fix(?:es|ed|ing)?|bugs?|issues?|problems?|errors?|broken)\b|doesn't work|not work/i.test(taskText);
525
- 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;
526
539
  const analysisTask = isProjectAnalysisTask(taskText);
527
540
  const workspaceRoot = String(state.workspaceRoot || process.cwd());
528
541
  ensureContextSessionState(state);
@@ -1124,6 +1137,7 @@ module.exports = {
1124
1137
  runSingleCommand,
1125
1138
  runNaturalLanguageTask,
1126
1139
  resumeAfterUserInteraction,
1140
+ submitUserInteractionAnswer: (...args) => require("./protocol/suspension").submitUserInteractionAnswer(...args),
1127
1141
  formatNlResult,
1128
1142
  normalizeToolLogEvent,
1129
1143
  isProjectAnalysisTask,
@@ -8,11 +8,16 @@ const {
8
8
  } = require("./planGraph");
9
9
 
10
10
  function emptyExecutionState() {
11
+ // Durable execution control plane. Field ownership: see
12
+ // src/code/protocol/ownership.js (STATE_OWNERSHIP / DURABLE_FIELDS).
11
13
  return {
12
14
  currentSegmentId: "",
13
15
  mode: "single_action",
14
16
  planMode: false,
15
17
  planModeSource: "",
18
+ // R5 orthognal fields (dual-written with planMode during migration)
19
+ planningPolicy: "direct_allowed",
20
+ executionOwner: { kind: "none", id: "" },
16
21
  steps: {},
17
22
  modifiedFiles: [],
18
23
  lastExitCodes: [],
@@ -63,6 +63,11 @@ function setPlanMode(executionState = null, enabled = true, {
63
63
  const next = Boolean(enabled);
64
64
  const wasOn = state.planMode === true;
65
65
  state.planMode = next;
66
+ // R5 dual-write: Plan Mode UI maps to planningPolicy only.
67
+ state.planningPolicy = next ? "graph_required" : "direct_allowed";
68
+ if (!state.executionOwner || typeof state.executionOwner !== "object") {
69
+ state.executionOwner = { kind: "none", id: "" };
70
+ }
66
71
  if (next) {
67
72
  if (!wasOn) state.planModeEnteredAt = new Date().toISOString();
68
73
  state.planModeReason = String(reason || state.planModeReason || "").trim();
@@ -101,7 +106,9 @@ function planHasNodes(executionState = null) {
101
106
  }
102
107
 
103
108
  function planModeBlocksDirectTool(tool = "", executionState = null) {
104
- if (!isPlanModeEnabled(executionState)) return false;
109
+ const { getPlanningPolicy } = require("../protocol/controlPlane");
110
+ const policy = getPlanningPolicy(executionState);
111
+ if (policy !== "graph_required") return false;
105
112
  const name = String(tool || "").trim().toLowerCase();
106
113
  return name === "write" || name === "edit" || name === "bash";
107
114
  }
package/src/code/index.js CHANGED
@@ -19,6 +19,7 @@ const {
19
19
  runSingleCommand,
20
20
  runNaturalLanguageTask,
21
21
  resumeAfterUserInteraction,
22
+ submitUserInteractionAnswer,
22
23
  formatNlResult,
23
24
  resolvePlannerProvider,
24
25
  parseAgentArgs,
@@ -62,6 +63,7 @@ module.exports = {
62
63
  runSingleCommand,
63
64
  runNaturalLanguageTask,
64
65
  resumeAfterUserInteraction,
66
+ submitUserInteractionAnswer,
65
67
  formatNlResult,
66
68
  resolvePlannerProvider,
67
69
  parseAgentArgs,
@@ -34,6 +34,19 @@ const {
34
34
  getPendingUserInteraction,
35
35
  } = require("./context/userInteraction");
36
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");
37
50
  const { stableStringify } = require("./context/stableJson");
38
51
  const { getReadToolDescription } = require("../agents/prompts/native/toolDescriptions/read");
39
52
  const { getWriteToolDescription } = require("../agents/prompts/native/toolDescriptions/write");
@@ -1460,140 +1473,76 @@ async function runAnthropicTurn({
1460
1473
  });
1461
1474
  }
1462
1475
 
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.
1476
+ const {
1477
+ createOpenAiChatTransport,
1478
+ createAnthropicMessagesTransport,
1479
+ } = require("./providers");
1480
+
1481
+ // Transport descriptors: wire-format only. Plan Mode / leases / policy live in the loop.
1467
1482
  const TRANSPORTS = {
1468
- "openai-chat": {
1483
+ "openai-chat": createOpenAiChatTransport({
1469
1484
  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
1485
  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
- }
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
+ };
1509
1498
 
1510
- 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
+ }
1511
1503
 
1512
- messages.push({
1513
- role: "assistant",
1514
- content: null,
1515
- tool_calls: assistantToolCalls,
1516
- });
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
+ }
1517
1519
 
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
- });
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
+ }
1574
1529
 
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
- };
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
+ }
1597
1546
 
1598
1547
  async function runNativeLoop({
1599
1548
  transport,
@@ -1644,6 +1593,14 @@ async function runNativeLoop({
1644
1593
  ensurePendingUserPrompts(executionState);
1645
1594
  const toolBudget = resolveNativeToolBudget();
1646
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
+ }
1647
1604
 
1648
1605
  function injectPendingUserReminders() {
1649
1606
  const nudges = drainUserPrompts(executionState);
@@ -1661,6 +1618,10 @@ async function runNativeLoop({
1661
1618
 
1662
1619
  injectPendingUserReminders();
1663
1620
 
1621
+ if (activeLedger) {
1622
+ runProviderTurnGate(activeLedger);
1623
+ }
1624
+
1664
1625
  const turnResult = await transport.runTurn({
1665
1626
  url: requestUrl,
1666
1627
  apiKey,
@@ -1730,6 +1691,7 @@ async function runNativeLoop({
1730
1691
  messages,
1731
1692
  usage,
1732
1693
  executionState,
1694
+ protocolLedger: lastProtocolLedger || snapshotLedger(activeLedger),
1733
1695
  };
1734
1696
  }
1735
1697
 
@@ -1742,61 +1704,55 @@ async function runNativeLoop({
1742
1704
  messages,
1743
1705
  usage,
1744
1706
  executionState,
1707
+ protocolLedger: lastProtocolLedger || snapshotLedger(activeLedger),
1745
1708
  };
1746
1709
  }
1747
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
+
1748
1717
  const callNames = pendingCalls.map((call) => String(call.name || "").trim().toLowerCase());
1749
1718
  const hasPlanGraph = callNames.includes("plan_graph");
1750
1719
  const hasAskUser = callNames.includes("ask_user");
1751
1720
  const hasDataTool = callNames.some((name) => EXECUTABLE_GRAPH_TOOLS.has(name));
1752
1721
  if (hasPlanGraph && hasDataTool) {
1753
1722
  // prepareToolCalls already appended the assistant tool_calls / tool_use
1754
- // message; every declared call must get a contiguous tool result.
1755
- const collectedResults = [];
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",
1729
+ };
1756
1730
  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
- });
1731
+ shadowResolvePending(activeLedger, pending, rejected);
1768
1732
  toolCallsExecuted += 1;
1769
1733
  toolErrors += 1;
1770
1734
  }
1771
- if (typeof transport.flushToolResults === "function") {
1772
- transport.flushToolResults({ messages, collected: collectedResults });
1773
- }
1735
+ flushLedgerToolResults(activeLedger, transport, messages, pendingCalls);
1736
+ lastProtocolLedger = snapshotLedger(activeLedger);
1774
1737
  continue;
1775
1738
  }
1776
1739
  if (hasAskUser && pendingCalls.length > 1) {
1777
- const collectedResults = [];
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
+ };
1778
1746
  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
- });
1747
+ shadowResolvePending(activeLedger, pending, rejected);
1790
1748
  toolCallsExecuted += 1;
1791
1749
  toolErrors += 1;
1792
1750
  }
1793
- if (typeof transport.flushToolResults === "function") {
1794
- transport.flushToolResults({ messages, collected: collectedResults });
1795
- }
1751
+ flushLedgerToolResults(activeLedger, transport, messages, pendingCalls);
1752
+ lastProtocolLedger = snapshotLedger(activeLedger);
1796
1753
  continue;
1797
1754
  }
1798
1755
 
1799
- const collectedResults = [];
1800
1756
  let deferredAskUser = null;
1801
1757
  for (const pending of pendingCalls) {
1802
1758
  const pendingName = String(pending.name || "").trim().toLowerCase();
@@ -1814,12 +1770,7 @@ async function runNativeLoop({
1814
1770
  };
1815
1771
  toolCallsExecuted += 1;
1816
1772
  toolErrors += 1;
1817
- transport.appendToolResult({
1818
- messages,
1819
- collected: collectedResults,
1820
- call: pending,
1821
- toolResult: blocked,
1822
- });
1773
+ shadowResolvePending(activeLedger, pending, blocked);
1823
1774
  continue;
1824
1775
  }
1825
1776
  if (planModeBlocksDirectTool(pendingName, executionState)) {
@@ -1833,12 +1784,7 @@ async function runNativeLoop({
1833
1784
  };
1834
1785
  toolCallsExecuted += 1;
1835
1786
  toolErrors += 1;
1836
- transport.appendToolResult({
1837
- messages,
1838
- collected: collectedResults,
1839
- call: pending,
1840
- toolResult: blocked,
1841
- });
1787
+ shadowResolvePending(activeLedger, pending, blocked);
1842
1788
  continue;
1843
1789
  }
1844
1790
  const leaseCheck = checkWriteAllowed(executionState, {
@@ -1858,12 +1804,7 @@ async function runNativeLoop({
1858
1804
  };
1859
1805
  toolCallsExecuted += 1;
1860
1806
  toolErrors += 1;
1861
- transport.appendToolResult({
1862
- messages,
1863
- collected: collectedResults,
1864
- call: pending,
1865
- toolResult: blocked,
1866
- });
1807
+ shadowResolvePending(activeLedger, pending, blocked);
1867
1808
  continue;
1868
1809
  }
1869
1810
 
@@ -1882,6 +1823,7 @@ async function runNativeLoop({
1882
1823
  }
1883
1824
  : null;
1884
1825
 
1826
+ markExecuting(activeLedger, pendingToolCallId(pending));
1885
1827
  const toolResult = runCoreTool({
1886
1828
  tool: pending.name,
1887
1829
  args: pending.args,
@@ -1911,6 +1853,7 @@ async function runNativeLoop({
1911
1853
  transport: provider === "anthropic" ? "anthropic-messages" : "openai-chat",
1912
1854
  };
1913
1855
  }
1856
+ deferCall(activeLedger, pendingToolCallId(pending), { reason: "ask_user" });
1914
1857
  deferredAskUser = { call: pending, interactionId: toolResult.interactionId || "" };
1915
1858
  continue;
1916
1859
  }
@@ -1923,17 +1866,11 @@ async function runNativeLoop({
1923
1866
  lastTool: pending.name,
1924
1867
  lastError: toolResult && toolResult.error ? String(toolResult.error) : "",
1925
1868
  });
1926
- transport.appendToolResult({
1927
- messages,
1928
- collected: collectedResults,
1929
- call: pending,
1930
- toolResult,
1931
- });
1869
+ shadowResolvePending(activeLedger, pending, toolResult);
1932
1870
  }
1933
1871
 
1934
- if (typeof transport.flushToolResults === "function") {
1935
- transport.flushToolResults({ messages, collected: collectedResults });
1936
- }
1872
+ flushLedgerToolResults(activeLedger, transport, messages, pendingCalls);
1873
+ lastProtocolLedger = snapshotLedger(activeLedger);
1937
1874
 
1938
1875
  if (deferredAskUser) {
1939
1876
  return {
@@ -1945,6 +1882,7 @@ async function runNativeLoop({
1945
1882
  executionState,
1946
1883
  waitingUserInteraction: true,
1947
1884
  interactionId: deferredAskUser.interactionId || "",
1885
+ protocolLedger: lastProtocolLedger,
1948
1886
  };
1949
1887
  }
1950
1888
 
@@ -1959,33 +1897,30 @@ async function runNativeLoop({
1959
1897
  executionState,
1960
1898
  waitingUserInteraction: true,
1961
1899
  interactionId: (getPendingUserInteraction(executionState) || {}).id || "",
1900
+ protocolLedger: lastProtocolLedger,
1962
1901
  };
1963
1902
  }
1964
1903
  }
1965
1904
  }
1966
1905
 
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
- });
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
+ });
1921
+ }
1988
1922
  }
1923
+ checkFaultPoint("after_answer_commit");
1989
1924
  return { ok: true };
1990
1925
  }
1991
1926
 
@@ -2118,6 +2053,7 @@ async function runNativeAgentTask({
2118
2053
  streamed: Boolean(runResult.streamed) && typeof onStreamDelta === "function",
2119
2054
  waitingUserInteraction: Boolean(runResult.waitingUserInteraction),
2120
2055
  interactionId: runResult.interactionId || "",
2056
+ protocolLedger: runResult.protocolLedger || null,
2121
2057
  };
2122
2058
  } catch (err) {
2123
2059
  const message = err && err.message ? err.message : "native runner failed";
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Orthogonal control-plane fields (R5).
5
+ *
6
+ * planningPolicy: whether direct side-effect tools are allowed without a graph.
7
+ * executionOwner: who currently owns workspace / loop advancement.
8
+ *
9
+ * Dual-write with legacy planMode boolean during migration.
10
+ */
11
+
12
+ const PLANNING_POLICIES = Object.freeze(["direct_allowed", "graph_required"]);
13
+
14
+ function emptyExecutionOwner() {
15
+ return { kind: "none", id: "" };
16
+ }
17
+
18
+ function normalizePlanningPolicy(value = "") {
19
+ const raw = String(value || "").trim().toLowerCase();
20
+ if (PLANNING_POLICIES.includes(raw)) return raw;
21
+ return "";
22
+ }
23
+
24
+ function normalizeExecutionOwner(owner = null) {
25
+ if (!owner || typeof owner !== "object") return emptyExecutionOwner();
26
+ const kind = String(owner.kind || "none").trim().toLowerCase();
27
+ if (kind === "agent_loop" || kind === "task_run") {
28
+ return {
29
+ kind,
30
+ id: String(owner.id || owner.taskRunId || owner.agentLoopId || "").trim(),
31
+ };
32
+ }
33
+ return emptyExecutionOwner();
34
+ }
35
+
36
+ /**
37
+ * Sync planningPolicy ↔ planMode. Prefer explicit planningPolicy when present.
38
+ */
39
+ function syncPlanningFields(executionState = null) {
40
+ const state = executionState && typeof executionState === "object" ? executionState : {};
41
+ const explicit = normalizePlanningPolicy(state.planningPolicy);
42
+ if (explicit) {
43
+ state.planningPolicy = explicit;
44
+ state.planMode = explicit === "graph_required";
45
+ } else if (state.planMode === true) {
46
+ state.planningPolicy = "graph_required";
47
+ } else {
48
+ state.planningPolicy = "direct_allowed";
49
+ state.planMode = false;
50
+ }
51
+ state.executionOwner = normalizeExecutionOwner(state.executionOwner);
52
+ return state;
53
+ }
54
+
55
+ function setPlanningPolicy(executionState = null, policy = "direct_allowed", {
56
+ reason = "",
57
+ source = "",
58
+ } = {}) {
59
+ const state = syncPlanningFields(executionState);
60
+ const next = normalizePlanningPolicy(policy) || "direct_allowed";
61
+ state.planningPolicy = next;
62
+ // Dual-write legacy bool
63
+ const { setPlanMode } = require("../context/planMode");
64
+ setPlanMode(state, next === "graph_required", { reason, source });
65
+ state.planningPolicy = next;
66
+ return state;
67
+ }
68
+
69
+ function getPlanningPolicy(executionState = null) {
70
+ return syncPlanningFields(executionState).planningPolicy;
71
+ }
72
+
73
+ function setExecutionOwner(executionState = null, owner = null) {
74
+ const state = syncPlanningFields(executionState);
75
+ state.executionOwner = normalizeExecutionOwner(owner);
76
+ return state.executionOwner;
77
+ }
78
+
79
+ function getExecutionOwner(executionState = null) {
80
+ return syncPlanningFields(executionState).executionOwner;
81
+ }
82
+
83
+ module.exports = {
84
+ PLANNING_POLICIES,
85
+ emptyExecutionOwner,
86
+ normalizePlanningPolicy,
87
+ normalizeExecutionOwner,
88
+ syncPlanningFields,
89
+ setPlanningPolicy,
90
+ getPlanningPolicy,
91
+ setExecutionOwner,
92
+ getExecutionOwner,
93
+ };