u-foo 3.0.27 → 3.0.29

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.
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "u-foo",
3
- "version": "3.0.27",
3
+ "version": "3.0.29",
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",
@@ -24,6 +24,8 @@ function contextTokensFromUsage(usage = null) {
24
24
  const input = toTokenCount(usage.input);
25
25
  const cacheRead = toTokenCount(usage.cacheRead);
26
26
  const cacheCreation = toTokenCount(usage.cacheCreation);
27
+ if (usage.inputIncludesCache === true) return input;
28
+ if (usage.inputIncludesCache === false) return input + cacheRead + cacheCreation;
27
29
  if (cacheCreation > 0) return input + cacheRead + cacheCreation;
28
30
  // Anthropic exclusive split: input can be smaller than cache_read alone.
29
31
  if (cacheRead > 0 && input < cacheRead) return input + cacheRead + cacheCreation;
@@ -96,6 +96,7 @@ const DEFAULT_KIMI_MODEL = "k3";
96
96
  // for non-trivial tasks while still catching runaway loops. Override via env.
97
97
  const DEFAULT_MAX_NATIVE_TOOL_CALLS = 100;
98
98
  const DEFAULT_MAX_NATIVE_TOOL_ERRORS = 20;
99
+ const MAX_EMPTY_TERMINAL_RETRIES = 2;
99
100
  const DEFAULT_NATIVE_TIMEOUT_MS = 43200000; // 12 hours
100
101
  /** Max text-only auto-continues while a plan is waiting on a task (per user submit). */
101
102
  const DEFAULT_MAX_PLAN_AUTO_CONTINUES = 24;
@@ -213,7 +214,8 @@ function readOpenAiUsage(raw) {
213
214
  input: toUsageInt(raw.prompt_tokens),
214
215
  output: toUsageInt(raw.completion_tokens),
215
216
  cacheRead: toUsageInt(details.cached_tokens),
216
- cacheCreation: 0,
217
+ cacheCreation: toUsageInt(details.created_cache_tokens),
218
+ inputIncludesCache: true,
217
219
  };
218
220
  }
219
221
 
@@ -226,6 +228,7 @@ function readAnthropicUsage(raw, { includeOutput = false } = {}) {
226
228
  output: includeOutput ? toUsageInt(raw.output_tokens) : 0,
227
229
  cacheRead: toUsageInt(raw.cache_read_input_tokens),
228
230
  cacheCreation: toUsageInt(raw.cache_creation_input_tokens),
231
+ inputIncludesCache: false,
229
232
  };
230
233
  }
231
234
 
@@ -1431,7 +1434,13 @@ async function runAnthropicTurn({
1431
1434
  let responseText = "";
1432
1435
  let nextSyntheticBlockIndex = 0;
1433
1436
  let lastBlockIndex = -1;
1434
- const turnUsage = { input: 0, output: 0, cacheRead: 0, cacheCreation: 0 };
1437
+ const turnUsage = {
1438
+ input: 0,
1439
+ output: 0,
1440
+ cacheRead: 0,
1441
+ cacheCreation: 0,
1442
+ inputIncludesCache: false,
1443
+ };
1435
1444
 
1436
1445
  return runSseRequest({
1437
1446
  url,
@@ -1806,6 +1815,7 @@ async function runNativeLoop({
1806
1815
  let streamed = false;
1807
1816
  let toolCallsExecuted = 0;
1808
1817
  let toolErrors = 0;
1818
+ let emptyTerminalRetries = 0;
1809
1819
  let executionState = initialExecutionState && typeof initialExecutionState === "object"
1810
1820
  ? initialExecutionState
1811
1821
  : emptyExecutionState();
@@ -1979,6 +1989,22 @@ async function runNativeLoop({
1979
1989
 
1980
1990
  if (toolCalls.length === 0) {
1981
1991
  const text = String(turnResult.text || "").trim();
1992
+ if (!text && toolCallsExecuted > 0) {
1993
+ if (emptyTerminalRetries >= MAX_EMPTY_TERMINAL_RETRIES) {
1994
+ throw new Error("model returned an empty response after tool execution");
1995
+ }
1996
+ emptyTerminalRetries += 1;
1997
+ providerMessages.push({
1998
+ role: "user",
1999
+ content: [
2000
+ "Continue the current task after the tool results.",
2001
+ "Call any remaining tools, or provide a concrete final answer to the user.",
2002
+ "Do not end the turn with an empty response.",
2003
+ ].join(" "),
2004
+ });
2005
+ continue;
2006
+ }
2007
+ emptyTerminalRetries = 0;
1982
2008
  const sideEffects = parseStructuredSideEffects(text);
1983
2009
  const planCommand = sideEffects ? normalizePlanGraphCommand(sideEffects) : null;
1984
2010
  if (planCommand) {
@@ -2032,6 +2058,7 @@ async function runNativeLoop({
2032
2058
 
2033
2059
  // A tool-using turn resets the empty auto-continue streak (progress possible).
2034
2060
  consecutiveEmptyAutoContinues = 0;
2061
+ emptyTerminalRetries = 0;
2035
2062
 
2036
2063
  const pendingCalls = transport.prepareToolCalls({
2037
2064
  messages: providerMessages,
@@ -59,9 +59,22 @@ function createUsageSummary() {
59
59
  output: 0,
60
60
  cacheRead: 0,
61
61
  cacheCreation: 0,
62
+ cacheInput: 0,
62
63
  };
63
64
  }
64
65
 
66
+ function promptTokenTotal(row = {}) {
67
+ const input = toUsageCount(row.input);
68
+ const provider = String(row.provider || "").trim().toLowerCase();
69
+ // OpenAI-compatible usage.prompt_tokens already includes cached tokens.
70
+ // Anthropic reports uncached, cache-read, and cache-creation tokens as
71
+ // disjoint counters, so reconstruct the total prompt-token denominator.
72
+ if (provider === "anthropic") {
73
+ return input + toUsageCount(row.cacheRead) + toUsageCount(row.cacheCreation);
74
+ }
75
+ return input;
76
+ }
77
+
65
78
  function summarizeSessionUsage({ workspaceRoot = process.cwd(), sessionId = "" } = {}) {
66
79
  const summary = createUsageSummary();
67
80
  const targetSessionId = String(sessionId || "").trim();
@@ -88,6 +101,7 @@ function summarizeSessionUsage({ workspaceRoot = process.cwd(), sessionId = "" }
88
101
  summary.output += toUsageCount(row.output);
89
102
  summary.cacheRead += toUsageCount(row.cacheRead);
90
103
  summary.cacheCreation += toUsageCount(row.cacheCreation);
104
+ summary.cacheInput += promptTokenTotal(row);
91
105
  }
92
106
  return summary;
93
107
  }
@@ -98,11 +112,14 @@ function formatSessionUsageStatus(summary = {}) {
98
112
  const output = Number(source.output) || 0;
99
113
  const cacheRead = Number(source.cacheRead) || 0;
100
114
  const cacheCreation = Number(source.cacheCreation) || 0;
101
- const denominator = cacheRead + input;
115
+ const reportedCacheInput = Number(source.cacheInput);
116
+ const denominator = Number.isFinite(reportedCacheInput) && reportedCacheInput > 0
117
+ ? reportedCacheInput
118
+ : input;
102
119
  const hitRate = denominator > 0 ? (cacheRead / denominator) * 100 : 0;
103
120
  return [
104
121
  `Session tokens: input=${input} output=${output} cache_read=${cacheRead} cache_creation=${cacheCreation}`,
105
- `Cache hit rate: ${hitRate.toFixed(1)}% (cache_read/(cache_read+input))`,
122
+ `Cache hit rate: ${hitRate.toFixed(1)}% (cache_read/prompt_input)`,
106
123
  ].join("\n");
107
124
  }
108
125
 
@@ -835,7 +835,9 @@ function buildMergedToolExpandedLines(entries = []) {
835
835
  : [];
836
836
  const maxLength = 120;
837
837
  return list.map((item) => {
838
- const base = `${formatToolDisplayName(item.tool)}${item.detail ? ` ${item.detail}` : ""}`;
838
+ const base = `${formatToolDisplayName(item.tool)}${item.detail ? ` ${item.detail}` : ""}`
839
+ .replace(/\s+/g, " ")
840
+ .trim();
839
841
  let line;
840
842
  if (!item.isError) {
841
843
  line = base;
@@ -203,6 +203,31 @@ function createThinkingLogPublisher(publish, thinkingStatus, streamId) {
203
203
  return { id, onThinkingDelta, stop };
204
204
  }
205
205
 
206
+ function createLeadingWhitespaceNormalizer() {
207
+ let hasVisibleText = false;
208
+ return (delta) => {
209
+ const text = String(delta || "");
210
+ if (hasVisibleText || !text) return text;
211
+ const visible = text.replace(/^\s+/u, "");
212
+ if (!visible) return "";
213
+ hasVisibleText = true;
214
+ return visible;
215
+ };
216
+ }
217
+
218
+ function appendNaturalLanguageResult(nlResult, formatNlResult, appendLog) {
219
+ if (typeof formatNlResult !== "function" || typeof appendLog !== "function" || !nlResult) {
220
+ return false;
221
+ }
222
+ // Successful streamed text is already present in the mutable stream entry.
223
+ // Appending summary + formatted summary here duplicated the same answer.
224
+ if (nlResult.ok !== false && nlResult.streamed) return false;
225
+ const formatted = String(formatNlResult(nlResult) || "").trim();
226
+ if (!formatted) return false;
227
+ appendLog(formatted, nlResult.ok === false ? "error" : "assistant");
228
+ return true;
229
+ }
230
+
206
231
  async function runUcodeRust(props = {}) {
207
232
  const plan = resolveTuiLaunchPlan({
208
233
  mode: props.tuiMode || process.env.UFOO_TUI || "rust",
@@ -562,6 +587,7 @@ async function runUcodeRust(props = {}) {
562
587
  thinking.reset();
563
588
  publish("stream.start", { id: streamId });
564
589
  const thinkingLog = createThinkingLogPublisher(publish, thinking, streamId);
590
+ const normalizeStreamDelta = createLeadingWhitespaceNormalizer();
565
591
  let responseStarted = false;
566
592
  const beginResponse = () => {
567
593
  if (responseStarted) return;
@@ -573,8 +599,10 @@ async function runUcodeRust(props = {}) {
573
599
  const result = await submit(answerText, props.state, {
574
600
  signal: abort.signal,
575
601
  onDelta: (delta) => {
602
+ const visibleDelta = normalizeStreamDelta(delta);
603
+ if (!visibleDelta) return;
576
604
  beginResponse();
577
- publish("stream.delta", { id: streamId, text: String(delta || "") });
605
+ publish("stream.delta", { id: streamId, text: visibleDelta });
578
606
  },
579
607
  onThinkingDelta: (delta) => {
580
608
  if (!responseStarted) thinkingLog.onThinkingDelta(delta);
@@ -621,6 +649,7 @@ async function runUcodeRust(props = {}) {
621
649
  thinking.reset();
622
650
  publish("stream.start", { id: streamId });
623
651
  const thinkingLog = createThinkingLogPublisher(publish, thinking, streamId);
652
+ const normalizeStreamDelta = createLeadingWhitespaceNormalizer();
624
653
  let responseStarted = false;
625
654
  const beginResponse = () => {
626
655
  if (responseStarted) return;
@@ -632,8 +661,10 @@ async function runUcodeRust(props = {}) {
632
661
  const nlResult = await props.runNaturalLanguageTask(text, props.state, {
633
662
  signal: abort.signal,
634
663
  onDelta: (delta) => {
664
+ const visibleDelta = normalizeStreamDelta(delta);
665
+ if (!visibleDelta) return;
635
666
  beginResponse();
636
- publish("stream.delta", { id: streamId, text: String(delta || "") });
667
+ publish("stream.delta", { id: streamId, text: visibleDelta });
637
668
  },
638
669
  onThinkingDelta: (delta) => {
639
670
  if (!responseStarted) thinkingLog.onThinkingDelta(delta);
@@ -656,11 +687,7 @@ async function runUcodeRust(props = {}) {
656
687
  tools.flush();
657
688
  thinkingLog.stop();
658
689
  publish("stream.done", { id: streamId });
659
- if (nlResult && nlResult.summary) appendLog(nlResult.summary, "system");
660
- if (typeof props.formatNlResult === "function" && nlResult) {
661
- const formatted = props.formatNlResult(nlResult);
662
- if (formatted) appendLog(formatted, "assistant");
663
- }
690
+ appendNaturalLanguageResult(nlResult, props.formatNlResult, appendLog);
664
691
  persistIfNeeded();
665
692
  publishPlan();
666
693
  publishUsageFromResult(nlResult);
@@ -782,7 +809,7 @@ async function runUcodeRust(props = {}) {
782
809
  if (name === "task.cancel") {
783
810
  controller.cancelTask();
784
811
  publish("status.set", { text: "cancelling…", busy: true });
785
- appendLog(" Cancellation requested. Stopping the current task...", "system");
812
+ appendLog(" Cancellation requested. Stopping the current task...", "system");
786
813
  return { ok: true };
787
814
  }
788
815
  if (name === "completion.request") {
@@ -1070,6 +1097,8 @@ module.exports = {
1070
1097
  normalizeToolLogEntry,
1071
1098
  splitUcodeBannerRow,
1072
1099
  createThinkingLogPublisher,
1100
+ createLeadingWhitespaceNormalizer,
1101
+ appendNaturalLanguageResult,
1073
1102
  buildUcodeAgentsSnapshot,
1074
1103
  buildUcodeCompletionItems,
1075
1104
  };
Binary file
Binary file
Binary file