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 +1 -1
- package/src/code/agent.js +17 -3
- package/src/code/context/executionSegment.js +5 -0
- package/src/code/context/planMode.js +8 -1
- package/src/code/index.js +2 -0
- package/src/code/nativeRunner.js +143 -207
- package/src/code/protocol/controlPlane.js +93 -0
- package/src/code/protocol/faultHarness.js +90 -0
- package/src/code/protocol/index.js +20 -0
- package/src/code/protocol/loopEvents.js +102 -0
- package/src/code/protocol/materialize.js +107 -0
- package/src/code/protocol/messageFixtures.js +116 -0
- package/src/code/protocol/ownership.js +147 -0
- package/src/code/protocol/protocolValidator.js +165 -0
- package/src/code/protocol/suspension.js +173 -0
- package/src/code/protocol/toolCallLedger.js +222 -0
- package/src/code/protocol/transitions.js +97 -0
- package/src/code/providers/anthropicMessagesTransport.js +93 -0
- package/src/code/providers/index.js +7 -0
- package/src/code/providers/openaiChatTransport.js +98 -0
- package/src/code/providers/transportContract.js +46 -0
- package/src/code/repl.js +8 -21
- package/src/code/runtime/taskLoop.js +13 -2
- package/src/code/runtime/taskRun.js +162 -1
- package/src/code/runtime/workspaceLease.js +41 -0
- package/src/code/sessionStore.js +1 -0
- package/src/code/taskRoute.js +73 -0
- package/src/ui/ink/UcodeApp.js +10 -27
package/package.json
CHANGED
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
|
-
//
|
|
524
|
-
const
|
|
525
|
-
const
|
|
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
|
-
|
|
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,
|
package/src/code/nativeRunner.js
CHANGED
|
@@ -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
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
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
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
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
|
-
|
|
1499
|
+
function pendingToolCallId(pending = null) {
|
|
1500
|
+
if (!pending || !pending.source) return "";
|
|
1501
|
+
return String(pending.source.id || "").trim();
|
|
1502
|
+
}
|
|
1511
1503
|
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
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
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
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
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1772
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1794
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1927
|
-
messages,
|
|
1928
|
-
collected: collectedResults,
|
|
1929
|
-
call: pending,
|
|
1930
|
-
toolResult,
|
|
1931
|
-
});
|
|
1869
|
+
shadowResolvePending(activeLedger, pending, toolResult);
|
|
1932
1870
|
}
|
|
1933
1871
|
|
|
1934
|
-
|
|
1935
|
-
|
|
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
|
|
1969
|
-
if (!
|
|
1970
|
-
const
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
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
|
+
};
|