u-foo 3.0.6 โ 3.0.7
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 +42 -0
- package/src/code/context/planProjection.js +163 -62
- package/src/code/contextWindow.js +117 -0
- package/src/code/nativeRunner.js +116 -29
- package/src/code/runtime/index.js +1 -0
- package/src/code/runtime/taskFocusContext.js +190 -0
- package/src/code/sessionStore.js +9 -0
- package/src/ui/format/index.js +8 -14
- package/src/ui/ink/ChatApp.js +20 -8
- package/src/ui/ink/UcodeApp.js +75 -4
- package/src/ui/ink/chatLogModel.js +4 -2
package/src/code/nativeRunner.js
CHANGED
|
@@ -33,9 +33,13 @@ const {
|
|
|
33
33
|
const {
|
|
34
34
|
drainAgentMailboxForTurn,
|
|
35
35
|
shouldAutoContinueForTaskWake,
|
|
36
|
-
buildTaskRunWakeReminder,
|
|
37
36
|
listTaskRunsAwaitingModel,
|
|
38
37
|
} = require("./runtime/agentWakeup");
|
|
38
|
+
const {
|
|
39
|
+
shouldIsolateTaskFocusTurn,
|
|
40
|
+
buildIsolatedTaskFocusTurn,
|
|
41
|
+
sanitizeToolResultForModel,
|
|
42
|
+
} = require("./runtime/taskFocusContext");
|
|
39
43
|
const {
|
|
40
44
|
runAskUserTool,
|
|
41
45
|
syncInteractionFromPlanGraph,
|
|
@@ -185,6 +189,7 @@ function createUsageTotals() {
|
|
|
185
189
|
output: 0,
|
|
186
190
|
cacheRead: 0,
|
|
187
191
|
cacheCreation: 0,
|
|
192
|
+
lastContextTokens: 0,
|
|
188
193
|
};
|
|
189
194
|
}
|
|
190
195
|
|
|
@@ -889,6 +894,8 @@ function runCoreTool({
|
|
|
889
894
|
return {
|
|
890
895
|
...result.modelPayload,
|
|
891
896
|
ok: result.status === "accepted",
|
|
897
|
+
// Keep executionState for the runner only; sanitizeToolResultForModel
|
|
898
|
+
// strips it before the payload enters provider messages.
|
|
892
899
|
executionState: result.executionState || state,
|
|
893
900
|
};
|
|
894
901
|
}
|
|
@@ -1694,7 +1701,7 @@ function shadowResolvePending(ledger, pending, toolResult) {
|
|
|
1694
1701
|
const callId = pendingToolCallId(pending);
|
|
1695
1702
|
if (!callId) return;
|
|
1696
1703
|
resolveCall(ledger, callId, {
|
|
1697
|
-
result: toolResult,
|
|
1704
|
+
result: sanitizeToolResultForModel(toolResult),
|
|
1698
1705
|
isError: Boolean(!toolResult || toolResult.ok === false),
|
|
1699
1706
|
});
|
|
1700
1707
|
}
|
|
@@ -1733,6 +1740,7 @@ async function runNativeLoop({
|
|
|
1733
1740
|
onPhase = null,
|
|
1734
1741
|
onToolEvent = null,
|
|
1735
1742
|
onArtifactPersisted = null,
|
|
1743
|
+
onContextUsage = null,
|
|
1736
1744
|
sessionId = "",
|
|
1737
1745
|
signal = null,
|
|
1738
1746
|
guards,
|
|
@@ -1743,12 +1751,31 @@ async function runNativeLoop({
|
|
|
1743
1751
|
if (!requestModel) {
|
|
1744
1752
|
throw new Error("ucode model is not configured");
|
|
1745
1753
|
}
|
|
1754
|
+
const {
|
|
1755
|
+
contextTokensFromUsage,
|
|
1756
|
+
buildContextMeter,
|
|
1757
|
+
} = require("./contextWindow");
|
|
1758
|
+
function emitContextUsage(turnUsage = null) {
|
|
1759
|
+
const contextTokens = contextTokensFromUsage(turnUsage);
|
|
1760
|
+
usage.lastContextTokens = contextTokens;
|
|
1761
|
+
if (typeof onContextUsage !== "function") return;
|
|
1762
|
+
try {
|
|
1763
|
+
onContextUsage(buildContextMeter({
|
|
1764
|
+
usedTokens: contextTokens,
|
|
1765
|
+
model: requestModel,
|
|
1766
|
+
}));
|
|
1767
|
+
} catch {
|
|
1768
|
+
// ignore UI callback failures
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1746
1771
|
|
|
1747
1772
|
const requestUrl = transport.resolveUrl(baseUrl);
|
|
1748
1773
|
if (!requestUrl) {
|
|
1749
1774
|
throw new Error("ucode baseUrl is not configured");
|
|
1750
1775
|
}
|
|
1751
1776
|
|
|
1777
|
+
// Durable Agent Loop transcript (returned + synced to session). Provider
|
|
1778
|
+
// turns that serve TaskFocus use a fresh providerMessages list instead.
|
|
1752
1779
|
const messages = sanitizeModelMessages(cloneMessageList(historyMessages));
|
|
1753
1780
|
if (!resume) {
|
|
1754
1781
|
transport.prepareMessages({ messages, systemPrompt, prompt });
|
|
@@ -1772,6 +1799,9 @@ async function runNativeLoop({
|
|
|
1772
1799
|
let planAutoContinues = 0;
|
|
1773
1800
|
let lastAutoContinueWaitingId = "";
|
|
1774
1801
|
let consecutiveEmptyAutoContinues = 0;
|
|
1802
|
+
let providerMessages = messages;
|
|
1803
|
+
let isolatedActive = false;
|
|
1804
|
+
let isolatedPrefixLength = 0;
|
|
1775
1805
|
|
|
1776
1806
|
if (resume) {
|
|
1777
1807
|
await withFaultPoint("before_provider_resume", () => {});
|
|
@@ -1779,20 +1809,55 @@ async function runNativeLoop({
|
|
|
1779
1809
|
|
|
1780
1810
|
function injectPendingUserReminders() {
|
|
1781
1811
|
const nudges = drainUserPrompts(executionState);
|
|
1782
|
-
if (nudges.length === 0) return;
|
|
1812
|
+
if (nudges.length === 0) return "";
|
|
1783
1813
|
const waiting = executionState.planGraph && executionState.planGraph.waitingFor
|
|
1784
1814
|
? executionState.planGraph.waitingFor
|
|
1785
1815
|
: null;
|
|
1786
|
-
|
|
1787
|
-
if (!content) return;
|
|
1788
|
-
messages.push({ role: "user", content });
|
|
1816
|
+
return formatUserReminderMessage(nudges, { waitingFor: waiting }) || "";
|
|
1789
1817
|
}
|
|
1790
1818
|
|
|
1791
1819
|
/** Deliver mid-loop TaskRun runtime events before the next model call. */
|
|
1792
|
-
function injectRuntimeMailboxEvents() {
|
|
1820
|
+
function injectRuntimeMailboxEvents(targetMessages) {
|
|
1793
1821
|
const drained = drainAgentMailboxForTurn(executionState);
|
|
1794
1822
|
if (!drained.text) return false;
|
|
1795
|
-
|
|
1823
|
+
targetMessages.push({ role: "user", content: drained.text });
|
|
1824
|
+
return true;
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
/**
|
|
1828
|
+
* Mirror provider-only TaskFocus deltas onto the durable transcript so
|
|
1829
|
+
* session sync / ask_user resume still see tool_use pairs, without feeding
|
|
1830
|
+
* the full Agent Loop history back into the next isolated provider turn.
|
|
1831
|
+
*/
|
|
1832
|
+
function mirrorIsolatedDeltaToDurable() {
|
|
1833
|
+
if (!isolatedActive || providerMessages === messages) return;
|
|
1834
|
+
const delta = providerMessages.slice(isolatedPrefixLength);
|
|
1835
|
+
for (const entry of delta) {
|
|
1836
|
+
messages.push(cloneMessageList([entry])[0]);
|
|
1837
|
+
}
|
|
1838
|
+
isolatedPrefixLength = providerMessages.length;
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
/**
|
|
1842
|
+
* Serve waiting TaskRun / plan-task turns on a fresh one-message context.
|
|
1843
|
+
* Durable `messages` is left intact for transcript sync.
|
|
1844
|
+
*/
|
|
1845
|
+
function applyIsolatedTaskFocusContext() {
|
|
1846
|
+
if (!shouldIsolateTaskFocusTurn(executionState)) {
|
|
1847
|
+
isolatedActive = false;
|
|
1848
|
+
providerMessages = messages;
|
|
1849
|
+
isolatedPrefixLength = 0;
|
|
1850
|
+
return false;
|
|
1851
|
+
}
|
|
1852
|
+
const drained = drainAgentMailboxForTurn(executionState);
|
|
1853
|
+
const userNudge = injectPendingUserReminders();
|
|
1854
|
+
const isolated = buildIsolatedTaskFocusTurn(executionState, {
|
|
1855
|
+
mailboxEvents: drained.events || [],
|
|
1856
|
+
userNudge,
|
|
1857
|
+
});
|
|
1858
|
+
providerMessages = isolated.messages;
|
|
1859
|
+
isolatedActive = true;
|
|
1860
|
+
isolatedPrefixLength = providerMessages.length;
|
|
1796
1861
|
return true;
|
|
1797
1862
|
}
|
|
1798
1863
|
|
|
@@ -1820,26 +1885,24 @@ async function runNativeLoop({
|
|
|
1820
1885
|
return false;
|
|
1821
1886
|
}
|
|
1822
1887
|
|
|
1823
|
-
//
|
|
1824
|
-
|
|
1888
|
+
// TaskFocus isolation rebuilds providerMessages at the top of the next
|
|
1889
|
+
// loop iteration โ just signal continue when a task/plan still needs service.
|
|
1890
|
+
if (shouldIsolateTaskFocusTurn(executionState) || shouldAutoContinueForTaskWake(executionState)) {
|
|
1825
1891
|
planAutoContinues += 1;
|
|
1826
1892
|
lastAutoContinueWaitingId = continueKey;
|
|
1827
1893
|
consecutiveEmptyAutoContinues += 1;
|
|
1828
1894
|
return true;
|
|
1829
1895
|
}
|
|
1830
1896
|
|
|
1831
|
-
if (
|
|
1832
|
-
const reminder = buildPlanAutoContinueReminder(executionState);
|
|
1833
|
-
if (!reminder) return false;
|
|
1834
|
-
messages.push({ role: "user", content: reminder });
|
|
1897
|
+
if (injectRuntimeMailboxEvents(messages)) {
|
|
1835
1898
|
planAutoContinues += 1;
|
|
1836
1899
|
lastAutoContinueWaitingId = continueKey;
|
|
1837
1900
|
consecutiveEmptyAutoContinues += 1;
|
|
1838
1901
|
return true;
|
|
1839
1902
|
}
|
|
1840
1903
|
|
|
1841
|
-
if (
|
|
1842
|
-
const reminder =
|
|
1904
|
+
if (shouldAutoContinuePlan(executionState)) {
|
|
1905
|
+
const reminder = buildPlanAutoContinueReminder(executionState);
|
|
1843
1906
|
if (!reminder) return false;
|
|
1844
1907
|
messages.push({ role: "user", content: reminder });
|
|
1845
1908
|
planAutoContinues += 1;
|
|
@@ -1854,8 +1917,11 @@ async function runNativeLoop({
|
|
|
1854
1917
|
while (true) {
|
|
1855
1918
|
guards.ensureActive();
|
|
1856
1919
|
|
|
1857
|
-
|
|
1858
|
-
|
|
1920
|
+
if (!applyIsolatedTaskFocusContext()) {
|
|
1921
|
+
const nudge = injectPendingUserReminders();
|
|
1922
|
+
if (nudge) messages.push({ role: "user", content: nudge });
|
|
1923
|
+
injectRuntimeMailboxEvents(messages);
|
|
1924
|
+
}
|
|
1859
1925
|
|
|
1860
1926
|
if (activeLedger) {
|
|
1861
1927
|
runProviderTurnGate(activeLedger);
|
|
@@ -1868,7 +1934,7 @@ async function runNativeLoop({
|
|
|
1868
1934
|
provider,
|
|
1869
1935
|
systemPrompt,
|
|
1870
1936
|
systemBlocks,
|
|
1871
|
-
messages,
|
|
1937
|
+
messages: providerMessages,
|
|
1872
1938
|
signal,
|
|
1873
1939
|
timeoutMs,
|
|
1874
1940
|
onPhase,
|
|
@@ -1886,6 +1952,7 @@ async function runNativeLoop({
|
|
|
1886
1952
|
|
|
1887
1953
|
usage.turns += 1;
|
|
1888
1954
|
addUsageTotals(usage, turnResult && turnResult.usage);
|
|
1955
|
+
emitContextUsage(turnResult && turnResult.usage);
|
|
1889
1956
|
|
|
1890
1957
|
const toolCalls = transport.getToolCalls(turnResult);
|
|
1891
1958
|
|
|
@@ -1894,7 +1961,7 @@ async function runNativeLoop({
|
|
|
1894
1961
|
const sideEffects = parseStructuredSideEffects(text);
|
|
1895
1962
|
const planCommand = sideEffects ? normalizePlanGraphCommand(sideEffects) : null;
|
|
1896
1963
|
if (planCommand) {
|
|
1897
|
-
transport.appendFinalAssistantMessage({ messages, turnResult });
|
|
1964
|
+
transport.appendFinalAssistantMessage({ messages: providerMessages, turnResult });
|
|
1898
1965
|
const planResult = runCoreTool({
|
|
1899
1966
|
tool: "plan_graph",
|
|
1900
1967
|
args: planCommand,
|
|
@@ -1908,18 +1975,22 @@ async function runNativeLoop({
|
|
|
1908
1975
|
if (planResult && planResult.executionState) {
|
|
1909
1976
|
executionState = planResult.executionState;
|
|
1910
1977
|
}
|
|
1911
|
-
|
|
1978
|
+
providerMessages.push({
|
|
1912
1979
|
role: "user",
|
|
1913
1980
|
content: JSON.stringify({
|
|
1914
1981
|
type: "plan_graph_result",
|
|
1915
|
-
...(
|
|
1916
|
-
|
|
1917
|
-
|
|
1982
|
+
...sanitizeToolResultForModel(
|
|
1983
|
+
(planResult && planResult.status)
|
|
1984
|
+
? planResult
|
|
1985
|
+
: { status: "rejected", ok: false, error: "plan_graph failed" },
|
|
1986
|
+
),
|
|
1918
1987
|
}),
|
|
1919
1988
|
});
|
|
1989
|
+
mirrorIsolatedDeltaToDurable();
|
|
1920
1990
|
continue;
|
|
1921
1991
|
}
|
|
1922
|
-
transport.appendFinalAssistantMessage({ messages, turnResult });
|
|
1992
|
+
transport.appendFinalAssistantMessage({ messages: providerMessages, turnResult });
|
|
1993
|
+
mirrorIsolatedDeltaToDurable();
|
|
1923
1994
|
if (!aggregated.trim() && text) {
|
|
1924
1995
|
aggregated = text;
|
|
1925
1996
|
}
|
|
@@ -1940,8 +2011,13 @@ async function runNativeLoop({
|
|
|
1940
2011
|
// A tool-using turn resets the empty auto-continue streak (progress possible).
|
|
1941
2012
|
consecutiveEmptyAutoContinues = 0;
|
|
1942
2013
|
|
|
1943
|
-
const pendingCalls = transport.prepareToolCalls({
|
|
2014
|
+
const pendingCalls = transport.prepareToolCalls({
|
|
2015
|
+
messages: providerMessages,
|
|
2016
|
+
turnResult,
|
|
2017
|
+
toolCalls,
|
|
2018
|
+
});
|
|
1944
2019
|
if (!pendingCalls) {
|
|
2020
|
+
mirrorIsolatedDeltaToDurable();
|
|
1945
2021
|
return {
|
|
1946
2022
|
text: aggregated,
|
|
1947
2023
|
streamed,
|
|
@@ -1977,7 +2053,8 @@ async function runNativeLoop({
|
|
|
1977
2053
|
toolCallsExecuted += 1;
|
|
1978
2054
|
toolErrors += 1;
|
|
1979
2055
|
}
|
|
1980
|
-
flushLedgerToolResults(activeLedger, transport,
|
|
2056
|
+
flushLedgerToolResults(activeLedger, transport, providerMessages, pendingCalls);
|
|
2057
|
+
mirrorIsolatedDeltaToDurable();
|
|
1981
2058
|
lastProtocolLedger = snapshotLedger(activeLedger);
|
|
1982
2059
|
continue;
|
|
1983
2060
|
}
|
|
@@ -1993,7 +2070,8 @@ async function runNativeLoop({
|
|
|
1993
2070
|
toolCallsExecuted += 1;
|
|
1994
2071
|
toolErrors += 1;
|
|
1995
2072
|
}
|
|
1996
|
-
flushLedgerToolResults(activeLedger, transport,
|
|
2073
|
+
flushLedgerToolResults(activeLedger, transport, providerMessages, pendingCalls);
|
|
2074
|
+
mirrorIsolatedDeltaToDurable();
|
|
1997
2075
|
lastProtocolLedger = snapshotLedger(activeLedger);
|
|
1998
2076
|
continue;
|
|
1999
2077
|
}
|
|
@@ -2114,7 +2192,8 @@ async function runNativeLoop({
|
|
|
2114
2192
|
shadowResolvePending(activeLedger, pending, toolResult);
|
|
2115
2193
|
}
|
|
2116
2194
|
|
|
2117
|
-
flushLedgerToolResults(activeLedger, transport,
|
|
2195
|
+
flushLedgerToolResults(activeLedger, transport, providerMessages, pendingCalls);
|
|
2196
|
+
mirrorIsolatedDeltaToDurable();
|
|
2118
2197
|
lastProtocolLedger = snapshotLedger(activeLedger);
|
|
2119
2198
|
|
|
2120
2199
|
if (deferredAskUser) {
|
|
@@ -2184,6 +2263,7 @@ async function runNativeAgentTask({
|
|
|
2184
2263
|
onPhase = null,
|
|
2185
2264
|
onToolEvent = null,
|
|
2186
2265
|
onArtifactPersisted = null,
|
|
2266
|
+
onContextUsage = null,
|
|
2187
2267
|
signal = null,
|
|
2188
2268
|
executionState = null,
|
|
2189
2269
|
resume = false,
|
|
@@ -2258,6 +2338,7 @@ async function runNativeAgentTask({
|
|
|
2258
2338
|
onPhase,
|
|
2259
2339
|
onToolEvent,
|
|
2260
2340
|
onArtifactPersisted,
|
|
2341
|
+
onContextUsage,
|
|
2261
2342
|
sessionId: nextSessionId,
|
|
2262
2343
|
signal,
|
|
2263
2344
|
guards,
|
|
@@ -2274,6 +2355,11 @@ async function runNativeAgentTask({
|
|
|
2274
2355
|
const usage = runResult.usage && typeof runResult.usage === "object"
|
|
2275
2356
|
? runResult.usage
|
|
2276
2357
|
: createUsageTotals();
|
|
2358
|
+
const { buildContextMeter } = require("./contextWindow");
|
|
2359
|
+
const contextMeter = buildContextMeter({
|
|
2360
|
+
usedTokens: usage.lastContextTokens,
|
|
2361
|
+
model: runtime.model,
|
|
2362
|
+
});
|
|
2277
2363
|
appendUsageRecord(workspaceRoot, {
|
|
2278
2364
|
sessionId: nextSessionId,
|
|
2279
2365
|
model: runtime.model,
|
|
@@ -2292,6 +2378,7 @@ async function runNativeAgentTask({
|
|
|
2292
2378
|
messages: cloneMessageList(runResult.messages),
|
|
2293
2379
|
sessionId: nextSessionId,
|
|
2294
2380
|
usage,
|
|
2381
|
+
contextMeter,
|
|
2295
2382
|
executionState: runResult.executionState || executionState || null,
|
|
2296
2383
|
// The loop marks streamed=true whenever it receives a stream callback;
|
|
2297
2384
|
// only report it when the caller actually registered one.
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Isolated TaskFocus turns for Agent Loop.
|
|
5
|
+
*
|
|
6
|
+
* TaskLoop steps must not inherit the full Agent Loop transcript. When a
|
|
7
|
+
* TaskRun is waiting_model (or the parent plan is waiting on a task node),
|
|
8
|
+
* rebuild a fresh one-message turn from TaskFocus + compact graph hints.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const {
|
|
12
|
+
listTaskRunsAwaitingModel,
|
|
13
|
+
buildTaskRunWakeReminder,
|
|
14
|
+
formatAgentRuntimeEvents,
|
|
15
|
+
} = require("./agentWakeup");
|
|
16
|
+
const { buildTaskFocus, renderTaskFocusText, getExecutionKind } = require("./taskFocus");
|
|
17
|
+
const { shouldAutoContinuePlan, buildPlanAutoContinueReminder } = require("../context/userNudge");
|
|
18
|
+
|
|
19
|
+
function getChildGraph(executionState = null, graphId = "") {
|
|
20
|
+
const id = String(graphId || "").trim();
|
|
21
|
+
if (!id || !executionState || typeof executionState !== "object") return null;
|
|
22
|
+
if (executionState.graphs && executionState.graphs[id]) return executionState.graphs[id];
|
|
23
|
+
if (executionState.planGraph && executionState.planGraph.graphId === id) {
|
|
24
|
+
return executionState.planGraph;
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function waitingPlanNode(executionState = null) {
|
|
30
|
+
const pg = executionState && executionState.planGraph;
|
|
31
|
+
const waiting = pg && pg.waitingFor;
|
|
32
|
+
if (!waiting || !waiting.id) return null;
|
|
33
|
+
if (String(waiting.type || "").trim().toLowerCase() !== "task") return null;
|
|
34
|
+
const nodes = Array.isArray(pg.nodes) ? pg.nodes : [];
|
|
35
|
+
return nodes.find((node) => node && node.id === waiting.id) || null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* True when the next model turn should use TaskFocus isolation instead of the
|
|
40
|
+
* accumulating Agent Loop transcript.
|
|
41
|
+
*/
|
|
42
|
+
function shouldIsolateTaskFocusTurn(executionState = null) {
|
|
43
|
+
if (!executionState || typeof executionState !== "object") return false;
|
|
44
|
+
if (executionState.pendingUserInteraction) return false;
|
|
45
|
+
// Active TaskRun waiting_model always gets a fresh TaskFocus turn.
|
|
46
|
+
if (listTaskRunsAwaitingModel(executionState).length > 0) return true;
|
|
47
|
+
// Parent-plan waits without a TaskRun only isolate when the Agent Loop
|
|
48
|
+
// would also auto-continue (skip approval_required / terminal yields).
|
|
49
|
+
if (!shouldAutoContinuePlan(executionState)) return false;
|
|
50
|
+
const node = waitingPlanNode(executionState);
|
|
51
|
+
if (!node) return false;
|
|
52
|
+
const kind = getExecutionKind(node);
|
|
53
|
+
return kind === "task_loop" || kind === "expand" || kind === "inline_llm" || kind === "llm";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function pickPrimaryAwaitingRun(executionState = null) {
|
|
57
|
+
const runs = listTaskRunsAwaitingModel(executionState);
|
|
58
|
+
return runs[0] || null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function rebuildFocusText(executionState = null, run = null) {
|
|
62
|
+
if (run && String(run.lastFocusText || "").trim()) {
|
|
63
|
+
return String(run.lastFocusText).trim();
|
|
64
|
+
}
|
|
65
|
+
const parent = executionState && executionState.planGraph;
|
|
66
|
+
const nodes = parent && Array.isArray(parent.nodes) ? parent.nodes : [];
|
|
67
|
+
const nodeId = (run && run.parentNodeId)
|
|
68
|
+
|| (waitingPlanNode(executionState) && waitingPlanNode(executionState).id)
|
|
69
|
+
|| "";
|
|
70
|
+
const focus = buildTaskFocus({
|
|
71
|
+
nodes,
|
|
72
|
+
currentNodeId: nodeId,
|
|
73
|
+
taskRunsById: (executionState.taskRuns && executionState.taskRuns.byId) || {},
|
|
74
|
+
recentlyChangedFiles: executionState.modifiedFiles || [],
|
|
75
|
+
standaloneTask: run && !run.parentNodeId
|
|
76
|
+
? {
|
|
77
|
+
id: run.id,
|
|
78
|
+
objective: run.objective || run.title || "",
|
|
79
|
+
title: run.title || run.objective || run.id,
|
|
80
|
+
status: run.status,
|
|
81
|
+
}
|
|
82
|
+
: null,
|
|
83
|
+
});
|
|
84
|
+
return renderTaskFocusText(focus);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function compactChildGraphHint(executionState = null, run = null) {
|
|
88
|
+
if (!run || !run.childGraphId) return "";
|
|
89
|
+
const child = getChildGraph(executionState, run.childGraphId);
|
|
90
|
+
if (!child) return `Child graph ${run.childGraphId}: (missing)`;
|
|
91
|
+
const waiting = child.waitingFor;
|
|
92
|
+
const nodes = Array.isArray(child.nodes) ? child.nodes : [];
|
|
93
|
+
const lines = [
|
|
94
|
+
`Child graph ${child.graphId}:`,
|
|
95
|
+
` objective: ${String(child.objective || "").slice(0, 200)}`,
|
|
96
|
+
` nodes: ${nodes.length}`,
|
|
97
|
+
];
|
|
98
|
+
if (waiting && waiting.id) {
|
|
99
|
+
lines.push(` waitingFor: ${waiting.type || "node"}:${waiting.id}`);
|
|
100
|
+
}
|
|
101
|
+
const pending = nodes
|
|
102
|
+
.filter((n) => n && (n.status === "pending" || n.status === "ready" || n.status === "waiting_llm"))
|
|
103
|
+
.slice(0, 6)
|
|
104
|
+
.map((n) => `${n.id}[${n.status || "pending"}]`);
|
|
105
|
+
if (pending.length) lines.push(` open: ${pending.join(", ")}`);
|
|
106
|
+
return lines.join("\n");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function buildIsolationWakeText(executionState = null, mailboxEvents = []) {
|
|
110
|
+
const parts = [];
|
|
111
|
+
const mailText = formatAgentRuntimeEvents(mailboxEvents);
|
|
112
|
+
if (mailText) parts.push(mailText);
|
|
113
|
+
const taskWake = buildTaskRunWakeReminder(executionState);
|
|
114
|
+
if (taskWake) parts.push(taskWake);
|
|
115
|
+
else if (shouldAutoContinuePlan(executionState)) {
|
|
116
|
+
const planWake = buildPlanAutoContinueReminder(executionState);
|
|
117
|
+
if (planWake) parts.push(planWake);
|
|
118
|
+
}
|
|
119
|
+
return parts.join("\n\n").trim();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Build a fresh Agent Loop message list for one TaskFocus-scoped model turn.
|
|
124
|
+
*/
|
|
125
|
+
function buildIsolatedTaskFocusTurn(executionState = null, options = {}) {
|
|
126
|
+
const run = pickPrimaryAwaitingRun(executionState);
|
|
127
|
+
const focusText = rebuildFocusText(executionState, run);
|
|
128
|
+
const childHint = compactChildGraphHint(executionState, run);
|
|
129
|
+
const wakeText = String(options.wakeText || "").trim()
|
|
130
|
+
|| buildIsolationWakeText(executionState, options.mailboxEvents || []);
|
|
131
|
+
const userNudge = String(options.userNudge || "").trim();
|
|
132
|
+
|
|
133
|
+
const content = [
|
|
134
|
+
"Isolated TaskFocus turn (no prior Agent Loop transcript).",
|
|
135
|
+
"Serve only the current task / child graph. Do not re-litigate finished plan nodes.",
|
|
136
|
+
focusText,
|
|
137
|
+
childHint,
|
|
138
|
+
wakeText,
|
|
139
|
+
userNudge,
|
|
140
|
+
].filter(Boolean).join("\n\n");
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
isolated: true,
|
|
144
|
+
taskRunId: run && run.id ? String(run.id) : "",
|
|
145
|
+
childGraphId: run && run.childGraphId ? String(run.childGraphId) : "",
|
|
146
|
+
parentNodeId: run && run.parentNodeId ? String(run.parentNodeId) : "",
|
|
147
|
+
messages: [{ role: "user", content }],
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Drop runner-only fields before a tool result is serialized into model messages.
|
|
153
|
+
* executionState alone can be hundreds of KB and must never enter the prompt.
|
|
154
|
+
*/
|
|
155
|
+
function sanitizeToolResultForModel(toolResult = null) {
|
|
156
|
+
if (!toolResult || typeof toolResult !== "object" || Array.isArray(toolResult)) {
|
|
157
|
+
return toolResult;
|
|
158
|
+
}
|
|
159
|
+
const {
|
|
160
|
+
executionState,
|
|
161
|
+
compile,
|
|
162
|
+
taskLoopResume,
|
|
163
|
+
...rest
|
|
164
|
+
} = toolResult;
|
|
165
|
+
if (rest.modelPayload && typeof rest.modelPayload === "object") {
|
|
166
|
+
const {
|
|
167
|
+
executionState: nestedState,
|
|
168
|
+
compile: nestedCompile,
|
|
169
|
+
...payloadRest
|
|
170
|
+
} = rest.modelPayload;
|
|
171
|
+
rest.modelPayload = payloadRest;
|
|
172
|
+
void nestedState;
|
|
173
|
+
void nestedCompile;
|
|
174
|
+
}
|
|
175
|
+
void executionState;
|
|
176
|
+
void compile;
|
|
177
|
+
void taskLoopResume;
|
|
178
|
+
return rest;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
module.exports = {
|
|
182
|
+
shouldIsolateTaskFocusTurn,
|
|
183
|
+
pickPrimaryAwaitingRun,
|
|
184
|
+
rebuildFocusText,
|
|
185
|
+
compactChildGraphHint,
|
|
186
|
+
buildIsolationWakeText,
|
|
187
|
+
buildIsolatedTaskFocusTurn,
|
|
188
|
+
sanitizeToolResultForModel,
|
|
189
|
+
waitingPlanNode,
|
|
190
|
+
};
|
package/src/code/sessionStore.js
CHANGED
|
@@ -113,6 +113,15 @@ function buildSessionSnapshot(input = {}) {
|
|
|
113
113
|
: 0,
|
|
114
114
|
// In-memory compatibility for callers still reading nlMessages
|
|
115
115
|
nlMessages: cloneMessages(source.nlMessages),
|
|
116
|
+
contextMeter: source.contextMeter && typeof source.contextMeter === "object"
|
|
117
|
+
? {
|
|
118
|
+
usedTokens: Number(source.contextMeter.usedTokens) || 0,
|
|
119
|
+
limitTokens: Number(source.contextMeter.limitTokens) || 0,
|
|
120
|
+
model: String(source.contextMeter.model || source.model || "").trim(),
|
|
121
|
+
label: String(source.contextMeter.label || "").trim(),
|
|
122
|
+
updatedAt: String(source.contextMeter.updatedAt || "").trim(),
|
|
123
|
+
}
|
|
124
|
+
: null,
|
|
116
125
|
};
|
|
117
126
|
}
|
|
118
127
|
|
package/src/ui/format/index.js
CHANGED
|
@@ -16,22 +16,16 @@ const UCODE_BANNER_LINES = [
|
|
|
16
16
|
|
|
17
17
|
const UCODE_VERSION = String((pkg && pkg.version) || "dev");
|
|
18
18
|
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
for (let i = 0; i <= UFO_FIELD_CELLS - 2; i += 1) {
|
|
25
|
-
UFO_FRAMES.push(`${" ".repeat(i)}๐ธ${" ".repeat(UFO_FIELD_CELLS - 2 - i)}`);
|
|
26
|
-
}
|
|
27
|
-
for (let i = UFO_FIELD_CELLS - 3; i > 0; i -= 1) {
|
|
28
|
-
UFO_FRAMES.push(`${" ".repeat(i)}๐ธ${" ".repeat(UFO_FIELD_CELLS - 2 - i)}`);
|
|
29
|
-
}
|
|
19
|
+
// Fixed-width ASCII spinner. The old ๐ธ patrol used emoji + padding that many
|
|
20
|
+
// terminals measure differently from Ink/Yoga, so long status lines left a
|
|
21
|
+
// staircase of ghost glyphs (and dragged vN.N.N into the log area).
|
|
22
|
+
const SPINNER_FRAMES = ["-", "\\", "|", "/"];
|
|
23
|
+
const WAITING_FRAMES = [". ", ".. ", "...", ".. ", ". "];
|
|
30
24
|
|
|
31
25
|
const STATUS_INDICATORS = {
|
|
32
|
-
thinking:
|
|
33
|
-
typing:
|
|
34
|
-
waiting:
|
|
26
|
+
thinking: SPINNER_FRAMES,
|
|
27
|
+
typing: SPINNER_FRAMES,
|
|
28
|
+
waiting: WAITING_FRAMES,
|
|
35
29
|
};
|
|
36
30
|
|
|
37
31
|
// Friendly labels for the tool-call events surfaced in the status line.
|
package/src/ui/ink/ChatApp.js
CHANGED
|
@@ -1187,7 +1187,7 @@ function createChatStatusLine({ React, ink }) {
|
|
|
1187
1187
|
const { useEffect, useState } = React;
|
|
1188
1188
|
const { Box, Text } = ink;
|
|
1189
1189
|
const h = React.createElement;
|
|
1190
|
-
return function ChatStatusLine({ status, version }) {
|
|
1190
|
+
return function ChatStatusLine({ status, version, cols = 80 }) {
|
|
1191
1191
|
const message = String((status && status.message) || "");
|
|
1192
1192
|
const animated = Boolean(message)
|
|
1193
1193
|
&& isAnimatedStatusType(inferStatusType(message, status && status.type));
|
|
@@ -1197,10 +1197,16 @@ function createChatStatusLine({ React, ink }) {
|
|
|
1197
1197
|
const timer = setInterval(() => setTick((t) => t + 1), 100);
|
|
1198
1198
|
return () => clearInterval(timer);
|
|
1199
1199
|
}, [animated]);
|
|
1200
|
+
const versionText = `v${version || ""}`;
|
|
1201
|
+
const raw = computeStatusText(status, animated ? tick : 0);
|
|
1202
|
+
// Keep the version pinned on the right; truncate the live message so a
|
|
1203
|
+
// long bus status cannot collide with it or leave redraw trails.
|
|
1204
|
+
const leftBudget = Math.max(12, (Number(cols) || 80) - fmt.displayCellWidth(versionText) - 2);
|
|
1205
|
+
const left = fitPlainLine(raw, leftBudget);
|
|
1200
1206
|
return h(Box, { marginTop: 1, width: "100%" },
|
|
1201
|
-
h(Text, { color: "gray"
|
|
1207
|
+
h(Text, { color: "gray", wrap: "truncate" }, left),
|
|
1202
1208
|
h(Box, { flexGrow: 1 }),
|
|
1203
|
-
h(Text, { color: "gray" },
|
|
1209
|
+
h(Text, { color: "gray" }, versionText),
|
|
1204
1210
|
);
|
|
1205
1211
|
};
|
|
1206
1212
|
}
|
|
@@ -3605,7 +3611,7 @@ function createChatApp({ React, ink, props, interactive = true }) {
|
|
|
3605
3611
|
}
|
|
3606
3612
|
if (row.kind === "user") {
|
|
3607
3613
|
const userBody = renderUserLogBody(row.bodyText);
|
|
3608
|
-
return h(Box, { key, width: "100%", marginBottom: 1 },
|
|
3614
|
+
return h(Box, { key, width: "100%", marginBottom: 1, alignItems: "flex-start" },
|
|
3609
3615
|
h(Text, { color: "green", bold: true }, row.markerText || "โบ "),
|
|
3610
3616
|
userBody.at
|
|
3611
3617
|
? h(Text, { color: "magenta", bold: true }, `@${userBody.at} `)
|
|
@@ -3621,7 +3627,9 @@ function createChatApp({ React, ink, props, interactive = true }) {
|
|
|
3621
3627
|
wrap: "wrap",
|
|
3622
3628
|
};
|
|
3623
3629
|
if (colors.dim) bodyProps.dimColor = true;
|
|
3624
|
-
|
|
3630
|
+
// Pin the gutter glyph to the first text line; default Yoga stretch/center
|
|
3631
|
+
// floats markers above wrapped speaker ยท body rows.
|
|
3632
|
+
return h(Box, { key, width: "100%", alignItems: "flex-start" },
|
|
3625
3633
|
h(Text, {
|
|
3626
3634
|
color: colors.marker,
|
|
3627
3635
|
bold: row.kind === "error" || row.kind === "assistant",
|
|
@@ -3682,7 +3690,7 @@ function createChatApp({ React, ink, props, interactive = true }) {
|
|
|
3682
3690
|
}
|
|
3683
3691
|
if (row.kind === "user") {
|
|
3684
3692
|
const userBody = renderUserLogBody(row.bodyText);
|
|
3685
|
-
return h(Box, { key, width: "100%", marginTop, marginBottom: 1 },
|
|
3693
|
+
return h(Box, { key, width: "100%", marginTop, marginBottom: 1, alignItems: "flex-start" },
|
|
3686
3694
|
h(Text, { color: "green", bold: true }, row.markerText || "โบ "),
|
|
3687
3695
|
userBody.at
|
|
3688
3696
|
? h(Text, { color: "magenta", bold: true }, `@${userBody.at} `)
|
|
@@ -3698,7 +3706,7 @@ function createChatApp({ React, ink, props, interactive = true }) {
|
|
|
3698
3706
|
wrap: "wrap",
|
|
3699
3707
|
};
|
|
3700
3708
|
if (colors.dim) bodyProps.dimColor = true;
|
|
3701
|
-
return h(Box, { key, width: "100%", marginTop },
|
|
3709
|
+
return h(Box, { key, width: "100%", marginTop, alignItems: "flex-start" },
|
|
3702
3710
|
h(Text, {
|
|
3703
3711
|
color: colors.marker,
|
|
3704
3712
|
bold: row.kind === "error" || row.kind === "assistant",
|
|
@@ -3809,7 +3817,11 @@ function createChatApp({ React, ink, props, interactive = true }) {
|
|
|
3809
3817
|
activeStreamGroups ? h(Box, { flexDirection: "column" },
|
|
3810
3818
|
...activeStreamGroups.map(renderChatLogGroup).filter(Boolean),
|
|
3811
3819
|
) : null,
|
|
3812
|
-
h(ChatStatusLine, {
|
|
3820
|
+
h(ChatStatusLine, {
|
|
3821
|
+
status: state.status,
|
|
3822
|
+
version: fmt.UCODE_VERSION,
|
|
3823
|
+
cols: size.cols || 80,
|
|
3824
|
+
}),
|
|
3813
3825
|
completionsOpen ? (() => {
|
|
3814
3826
|
const start = Math.min(completionWindowStart, Math.max(0, completions.length - POPUP_PAGE_SIZE));
|
|
3815
3827
|
const end = Math.min(completions.length, start + POPUP_PAGE_SIZE);
|