u-foo 2.5.14 → 2.5.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/package.json +1 -1
  2. package/src/agents/prompts/native/environment.js +20 -8
  3. package/src/code/agent.js +339 -24
  4. package/src/code/commands.js +61 -0
  5. package/src/code/context/artifactGc.js +292 -0
  6. package/src/code/context/artifactIndex.js +161 -0
  7. package/src/code/context/artifacts.js +183 -0
  8. package/src/code/context/assembler.js +698 -0
  9. package/src/code/context/executionSegment.js +314 -0
  10. package/src/code/context/featureFlag.js +13 -0
  11. package/src/code/context/index.js +18 -0
  12. package/src/code/context/projectSnapshot.js +201 -0
  13. package/src/code/context/promptLayers.js +159 -0
  14. package/src/code/context/reducers.js +328 -0
  15. package/src/code/context/stableJson.js +29 -0
  16. package/src/code/context/stateCommit.js +412 -0
  17. package/src/code/context/transcript.js +182 -0
  18. package/src/code/context/transcriptSync.js +106 -0
  19. package/src/code/context/workingSet.js +323 -0
  20. package/src/code/dispatch.js +4 -1
  21. package/src/code/index.js +6 -0
  22. package/src/code/modelCommand.js +87 -0
  23. package/src/code/nativeRunner.js +140 -30
  24. package/src/code/repl.js +36 -32
  25. package/src/code/sessionStore.js +227 -15
  26. package/src/code/skills/index.js +10 -0
  27. package/src/code/skills/injection.js +65 -3
  28. package/src/code/skills/loader.js +21 -0
  29. package/src/code/skills/manifest.js +87 -0
  30. package/src/code/skills/render.js +15 -1
  31. package/src/code/taskDecomposer.js +32 -2
  32. package/src/code/tools/artifactRead.js +40 -0
  33. package/src/code/tui.js +2 -0
  34. package/src/code/usageStore.js +15 -0
  35. package/src/ui/format/index.js +260 -44
  36. package/src/ui/format/markdownRenderer.js +215 -72
  37. package/src/ui/ink/ChatApp.js +39 -8
  38. package/src/ui/ink/UcodeApp.js +313 -27
  39. package/src/ui/ink/chatLogModel.js +102 -21
@@ -6,12 +6,26 @@ const {
6
6
  } = require("../agents/providers/credentials/kimi");
7
7
  const { runToolCall } = require("./dispatch");
8
8
  const { appendUsageRecord } = require("./usageStore");
9
+ const { isContextV2Enabled } = require("./context/featureFlag");
10
+ const {
11
+ persistToolResultToContext,
12
+ sanitizeModelMessages,
13
+ } = require("./context/assembler");
14
+ const { systemBlocksToAnthropicPayload } = require("./context/promptLayers");
15
+ const { parseStructuredSideEffects } = require("./context/stateCommit");
16
+ const {
17
+ parseExecutionSegment,
18
+ executeExecutionSegment,
19
+ formatSegmentResultMessage,
20
+ emptyExecutionState,
21
+ } = require("./context/executionSegment");
22
+ const { stableStringify } = require("./context/stableJson");
9
23
  const { getReadToolDescription } = require("../agents/prompts/native/toolDescriptions/read");
10
24
  const { getWriteToolDescription } = require("../agents/prompts/native/toolDescriptions/write");
11
25
  const { getEditToolDescription } = require("../agents/prompts/native/toolDescriptions/edit");
12
26
  const { getBashToolDescription } = require("../agents/prompts/native/toolDescriptions/bash");
13
27
 
14
- const CORE_TOOL_NAMES = new Set(["read", "write", "edit", "bash"]);
28
+ const CORE_TOOL_NAMES = new Set(["read", "write", "edit", "bash", "artifact_read"]);
15
29
  const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
16
30
  const DEFAULT_ANTHROPIC_BASE_URL = "https://api.anthropic.com/v1";
17
31
  const DEFAULT_KIMI_BASE_URL = "https://api.kimi.com/coding/v1";
@@ -20,7 +34,8 @@ const DEFAULT_KIMI_MODEL = "k3";
20
34
  // to 200 (fork). We count individual tool calls (not turns), so 100 leaves headroom
21
35
  // for non-trivial tasks while still catching runaway loops. Override via env.
22
36
  const DEFAULT_MAX_NATIVE_TOOL_CALLS = 100;
23
- const DEFAULT_MAX_NATIVE_TOOL_ERRORS = 5;
37
+ const DEFAULT_MAX_NATIVE_TOOL_ERRORS = 20;
38
+ const DEFAULT_NATIVE_TIMEOUT_MS = 43200000; // 12 hours
24
39
  // Anthropic Messages rejects max_tokens above the model's real cap (64K on
25
40
  // current models), so the transports use different defaults. Override either
26
41
  // via UFOO_UCODE_MAX_TOKENS (positive integer).
@@ -43,7 +58,7 @@ function nowMs() {
43
58
 
44
59
  function normalizeTimeoutMs(value) {
45
60
  const parsed = Number(value);
46
- if (!Number.isFinite(parsed)) return 300000;
61
+ if (!Number.isFinite(parsed)) return DEFAULT_NATIVE_TIMEOUT_MS;
47
62
  return Math.max(1000, Math.floor(parsed));
48
63
  }
49
64
 
@@ -143,7 +158,7 @@ function enforceNativeToolBudget({
143
158
  }
144
159
  }
145
160
 
146
- function createGuards({ signal = null, timeoutMs = 300000 } = {}) {
161
+ function createGuards({ signal = null, timeoutMs = DEFAULT_NATIVE_TIMEOUT_MS } = {}) {
147
162
  const startedAt = nowMs();
148
163
  const budgetMs = normalizeTimeoutMs(timeoutMs);
149
164
 
@@ -364,6 +379,25 @@ function buildCoreToolSpecs() {
364
379
  },
365
380
  },
366
381
  },
382
+ {
383
+ type: "function",
384
+ function: {
385
+ name: "artifact_read",
386
+ description: "Load a stored artifact by artifactId. Use selectors startLine/endLine, maxChars, or tailLines to read a slice.",
387
+ parameters: {
388
+ type: "object",
389
+ properties: {
390
+ artifactId: { type: "string" },
391
+ sessionId: { type: "string" },
392
+ startLine: { type: "integer" },
393
+ endLine: { type: "integer" },
394
+ maxChars: { type: "integer" },
395
+ tailLines: { type: "integer" },
396
+ },
397
+ required: ["artifactId"],
398
+ },
399
+ },
400
+ },
367
401
  ];
368
402
  }
369
403
 
@@ -375,7 +409,7 @@ function buildAnthropicToolSpecs() {
375
409
  }));
376
410
  }
377
411
 
378
- function createRequestController({ signal = null, timeoutMs = 300000 } = {}) {
412
+ function createRequestController({ signal = null, timeoutMs = DEFAULT_NATIVE_TIMEOUT_MS } = {}) {
379
413
  const controller = new AbortController();
380
414
  let timedOut = false;
381
415
 
@@ -437,11 +471,7 @@ function normalizeToolName(value = "") {
437
471
  }
438
472
 
439
473
  function toJsonString(value) {
440
- try {
441
- return JSON.stringify(value);
442
- } catch {
443
- return String(value || "");
444
- }
474
+ return stableStringify(value);
445
475
  }
446
476
 
447
477
  function parseSseBlocks(text = "") {
@@ -490,7 +520,15 @@ function normalizeToolCallArgs(raw = "") {
490
520
  return {};
491
521
  }
492
522
 
493
- function runCoreTool({ tool = "", args = {}, workspaceRoot = process.cwd(), onToolEvent = null } = {}) {
523
+ function runCoreTool({
524
+ tool = "",
525
+ args = {},
526
+ workspaceRoot = process.cwd(),
527
+ onToolEvent = null,
528
+ sessionId = "",
529
+ contextV2 = false,
530
+ onArtifactPersisted = null,
531
+ } = {}) {
494
532
  const normalizedTool = normalizeToolName(tool);
495
533
  if (!normalizedTool) {
496
534
  emitToolEvent(onToolEvent, {
@@ -506,6 +544,9 @@ function runCoreTool({ tool = "", args = {}, workspaceRoot = process.cwd(), onTo
506
544
  }
507
545
 
508
546
  const safeArgs = args && typeof args === "object" ? { ...args } : {};
547
+ if (normalizedTool === "artifact_read" && sessionId && !safeArgs.sessionId) {
548
+ safeArgs.sessionId = sessionId;
549
+ }
509
550
  emitToolEvent(onToolEvent, {
510
551
  tool: normalizedTool,
511
552
  phase: "start",
@@ -513,9 +554,13 @@ function runCoreTool({ tool = "", args = {}, workspaceRoot = process.cwd(), onTo
513
554
  error: "",
514
555
  });
515
556
 
557
+ const toolOptions = { workspaceRoot, cwd: workspaceRoot };
558
+ if (normalizedTool === "artifact_read" && sessionId) {
559
+ toolOptions.sessionId = sessionId;
560
+ }
516
561
  const result = runToolCall(
517
562
  { tool: normalizedTool, args: safeArgs },
518
- { workspaceRoot, cwd: workspaceRoot }
563
+ toolOptions,
519
564
  );
520
565
 
521
566
  if (!result || result.ok === false) {
@@ -525,6 +570,26 @@ function runCoreTool({ tool = "", args = {}, workspaceRoot = process.cwd(), onTo
525
570
  args: safeArgs,
526
571
  error: String((result && result.error) || `${normalizedTool} failed`),
527
572
  });
573
+ return result;
574
+ }
575
+
576
+ const useContextV2 = contextV2 || isContextV2Enabled();
577
+ if (useContextV2 && normalizedTool !== "artifact_read") {
578
+ const persisted = persistToolResultToContext({
579
+ workspaceRoot,
580
+ sessionId,
581
+ tool: normalizedTool,
582
+ args: safeArgs,
583
+ rawResult: result,
584
+ });
585
+ if (typeof onArtifactPersisted === "function") {
586
+ try {
587
+ onArtifactPersisted(persisted);
588
+ } catch {
589
+ // ignore
590
+ }
591
+ }
592
+ return persisted.modelPayload || result;
528
593
  }
529
594
 
530
595
  return result;
@@ -548,7 +613,7 @@ async function runSseRequest({
548
613
  headers = {},
549
614
  payload = {},
550
615
  signal = null,
551
- timeoutMs = 300000,
616
+ timeoutMs = DEFAULT_NATIVE_TIMEOUT_MS,
552
617
  onPhase = null,
553
618
  onNonStream,
554
619
  onEvent,
@@ -640,7 +705,7 @@ async function runOpenAiLikeTurn({
640
705
  onThinkingDelta = null,
641
706
  onPhase = null,
642
707
  signal = null,
643
- timeoutMs = 300000,
708
+ timeoutMs = DEFAULT_NATIVE_TIMEOUT_MS,
644
709
  } = {}) {
645
710
  const payload = {
646
711
  model,
@@ -890,12 +955,13 @@ async function runAnthropicTurn({
890
955
  apiKey = "",
891
956
  model = "",
892
957
  systemPrompt = "",
958
+ systemBlocks = null,
893
959
  messages = [],
894
960
  onTextDelta = null,
895
961
  onThinkingDelta = null,
896
962
  onPhase = null,
897
963
  signal = null,
898
- timeoutMs = 300000,
964
+ timeoutMs = DEFAULT_NATIVE_TIMEOUT_MS,
899
965
  } = {}) {
900
966
  const payload = {
901
967
  model,
@@ -908,17 +974,19 @@ async function runAnthropicTurn({
908
974
  if (thinkingBudget > 0) {
909
975
  payload.thinking = { type: "enabled", budget_tokens: thinkingBudget };
910
976
  }
911
- const systemText = String(systemPrompt || "").trim();
912
- if (systemText) {
913
- // Block form with a cache breakpoint; the system prompt is the most
914
- // stable prefix of every request.
915
- payload.system = [
916
- {
917
- type: "text",
918
- text: systemText,
919
- cache_control: { ...ANTHROPIC_CACHE_CONTROL },
920
- },
921
- ];
977
+ if (Array.isArray(systemBlocks) && systemBlocks.length > 0) {
978
+ payload.system = systemBlocksToAnthropicPayload(systemBlocks);
979
+ } else {
980
+ const systemText = String(systemPrompt || "").trim();
981
+ if (systemText) {
982
+ payload.system = [
983
+ {
984
+ type: "text",
985
+ text: systemText,
986
+ cache_control: { ...ANTHROPIC_CACHE_CONTROL },
987
+ },
988
+ ];
989
+ }
922
990
  }
923
991
 
924
992
  const headers = {
@@ -1294,16 +1362,20 @@ async function runNativeLoop({
1294
1362
  workspaceRoot = process.cwd(),
1295
1363
  prompt = "",
1296
1364
  systemPrompt = "",
1365
+ systemBlocks = null,
1297
1366
  historyMessages = [],
1298
1367
  model = "",
1299
1368
  baseUrl = "",
1300
1369
  apiKey = "",
1301
1370
  provider = "",
1302
- timeoutMs = 300000,
1371
+ timeoutMs = DEFAULT_NATIVE_TIMEOUT_MS,
1303
1372
  onStreamDelta = null,
1304
1373
  onThinkingDelta = null,
1305
1374
  onPhase = null,
1306
1375
  onToolEvent = null,
1376
+ onArtifactPersisted = null,
1377
+ sessionId = "",
1378
+ contextV2 = false,
1307
1379
  signal = null,
1308
1380
  guards,
1309
1381
  } = {}) {
@@ -1317,13 +1389,14 @@ async function runNativeLoop({
1317
1389
  throw new Error("ucode baseUrl is not configured");
1318
1390
  }
1319
1391
 
1320
- const messages = cloneMessageList(historyMessages);
1392
+ const messages = sanitizeModelMessages(cloneMessageList(historyMessages));
1321
1393
  transport.prepareMessages({ messages, systemPrompt, prompt });
1322
1394
 
1323
1395
  let aggregated = "";
1324
1396
  let streamed = false;
1325
1397
  let toolCallsExecuted = 0;
1326
1398
  let toolErrors = 0;
1399
+ let executionState = emptyExecutionState();
1327
1400
  const toolBudget = resolveNativeToolBudget();
1328
1401
  const usage = createUsageTotals();
1329
1402
 
@@ -1336,6 +1409,7 @@ async function runNativeLoop({
1336
1409
  model: requestModel,
1337
1410
  provider,
1338
1411
  systemPrompt,
1412
+ systemBlocks,
1339
1413
  messages,
1340
1414
  signal,
1341
1415
  timeoutMs,
@@ -1358,8 +1432,34 @@ async function runNativeLoop({
1358
1432
  const toolCalls = transport.getToolCalls(turnResult);
1359
1433
 
1360
1434
  if (toolCalls.length === 0) {
1361
- transport.appendFinalAssistantMessage({ messages, turnResult });
1362
1435
  const text = String(turnResult.text || "").trim();
1436
+ if (contextV2) {
1437
+ const sideEffects = parseStructuredSideEffects(text);
1438
+ const segment = parseExecutionSegment(sideEffects);
1439
+ if (segment && segment.steps && segment.steps.length > 0) {
1440
+ transport.appendFinalAssistantMessage({ messages, turnResult });
1441
+ const exec = executeExecutionSegment({
1442
+ segment,
1443
+ executionState,
1444
+ runStep: ({ tool, args }) => runCoreTool({
1445
+ tool,
1446
+ args,
1447
+ workspaceRoot,
1448
+ onToolEvent,
1449
+ sessionId,
1450
+ contextV2,
1451
+ onArtifactPersisted,
1452
+ }),
1453
+ });
1454
+ executionState = exec.executionState;
1455
+ messages.push({
1456
+ role: "user",
1457
+ content: formatSegmentResultMessage(exec),
1458
+ });
1459
+ continue;
1460
+ }
1461
+ }
1462
+ transport.appendFinalAssistantMessage({ messages, turnResult });
1363
1463
  if (!aggregated.trim() && text) {
1364
1464
  aggregated = text;
1365
1465
  }
@@ -1390,6 +1490,9 @@ async function runNativeLoop({
1390
1490
  args: pending.args,
1391
1491
  workspaceRoot,
1392
1492
  onToolEvent,
1493
+ sessionId,
1494
+ contextV2,
1495
+ onArtifactPersisted,
1393
1496
  });
1394
1497
  toolCallsExecuted += 1;
1395
1498
  if (!toolResult || toolResult.ok === false) {
@@ -1421,15 +1524,18 @@ async function runNativeAgentTask({
1421
1524
  workspaceRoot = process.cwd(),
1422
1525
  prompt = "",
1423
1526
  systemPrompt = "",
1527
+ systemBlocks = null,
1424
1528
  provider = "",
1425
1529
  model = "",
1426
1530
  messages = [],
1427
1531
  sessionId = "",
1428
- timeoutMs = 300000,
1532
+ timeoutMs = DEFAULT_NATIVE_TIMEOUT_MS,
1429
1533
  onStreamDelta = null,
1430
1534
  onThinkingDelta = null,
1431
1535
  onPhase = null,
1432
1536
  onToolEvent = null,
1537
+ onArtifactPersisted = null,
1538
+ contextV2 = false,
1433
1539
  signal = null,
1434
1540
  } = {}) {
1435
1541
  const guards = createGuards({ signal, timeoutMs });
@@ -1490,6 +1596,7 @@ async function runNativeAgentTask({
1490
1596
  workspaceRoot,
1491
1597
  prompt: promptText,
1492
1598
  systemPrompt,
1599
+ systemBlocks,
1493
1600
  historyMessages: messages,
1494
1601
  model: runtime.model,
1495
1602
  baseUrl: runtime.baseUrl,
@@ -1500,6 +1607,9 @@ async function runNativeAgentTask({
1500
1607
  onThinkingDelta,
1501
1608
  onPhase,
1502
1609
  onToolEvent,
1610
+ onArtifactPersisted,
1611
+ sessionId: nextSessionId,
1612
+ contextV2: contextV2 || isContextV2Enabled(),
1503
1613
  signal,
1504
1614
  guards,
1505
1615
  });
package/src/code/repl.js CHANGED
@@ -21,7 +21,9 @@ const {
21
21
  getPendingBusCount,
22
22
  shouldAutoConsumeBus,
23
23
  } = require("./busConsumer");
24
- const { summarizeSessionUsage } = require("./usageStore");
24
+ const { summarizeSessionUsage, formatSessionUsageStatus } = require("./usageStore");
25
+ const { listUcodeCommandsForHelp } = require("./commands");
26
+ const { applyUcodeModelCommand, suggestUcodeModels } = require("./modelCommand");
25
27
 
26
28
  function printPrompt(stdout = process.stdout) {
27
29
  stdout.write("> ");
@@ -75,40 +77,14 @@ function extractAgentNickname(agentId = "") {
75
77
  return base;
76
78
  }
77
79
 
78
- function formatSessionUsageStatus(summary = {}) {
79
- const source = summary && typeof summary === "object" ? summary : {};
80
- const input = Number(source.input) || 0;
81
- const output = Number(source.output) || 0;
82
- const cacheRead = Number(source.cacheRead) || 0;
83
- const cacheCreation = Number(source.cacheCreation) || 0;
84
- const denominator = cacheRead + input;
85
- const hitRate = denominator > 0 ? (cacheRead / denominator) * 100 : 0;
86
- return [
87
- `Session tokens: input=${input} output=${output} cache_read=${cacheRead} cache_creation=${cacheCreation}`,
88
- `Cache hit rate: ${hitRate.toFixed(1)}% (cache_read/(cache_read+input))`,
89
- ].join("\n");
90
- }
91
-
92
80
  function runSingleCommand(line = "", workspaceRoot = process.cwd()) {
93
81
  const text = normalizeLine(line);
94
82
  if (!text) return { kind: "empty" };
95
- if (text === "exit" || text === "quit") return { kind: "exit" };
96
- if (text === "help") {
83
+ if (text === "exit" || text === "quit" || text === "/exit" || text === "/quit") return { kind: "exit" };
84
+ if (text === "help" || text === "/help") {
97
85
  return {
98
86
  kind: "help",
99
- output: [
100
- "Commands:",
101
- " help",
102
- " exit|quit",
103
- " ubus|/ubus",
104
- " status|/status",
105
- " skills [list]",
106
- " skills show <name>",
107
- " bg|/bg <task>",
108
- " resume <session-id>",
109
- " tool <read|write|edit|bash> <args-json>",
110
- " run <read|write|edit|bash> <args-json>",
111
- ].join("\n"),
87
+ output: listUcodeCommandsForHelp(),
112
88
  };
113
89
  }
114
90
  const legacyUfooMarker = parseLegacyUfooMarkerCommand(text);
@@ -128,6 +104,25 @@ function runSingleCommand(line = "", workspaceRoot = process.cwd()) {
128
104
  kind: "status",
129
105
  };
130
106
  }
107
+ const modelMatch = text.match(/^(?:\/model|model)(?:\s+(.*))?$/i);
108
+ if (modelMatch) {
109
+ const nextModel = String(modelMatch[1] || "").trim();
110
+ if (!nextModel) {
111
+ return { kind: "model", action: "show" };
112
+ }
113
+ // Reject accidental multi-token garbage; model ids are single tokens.
114
+ if (/\s/.test(nextModel)) {
115
+ return {
116
+ kind: "error",
117
+ output: "usage: /model [model-id]",
118
+ };
119
+ }
120
+ return {
121
+ kind: "model",
122
+ action: "set",
123
+ model: nextModel,
124
+ };
125
+ }
131
126
  const skillsMatch = text.match(/^(?:\/skills|skills)(?:\s+(.*))?$/i);
132
127
  if (skillsMatch) {
133
128
  const args = String(skillsMatch[1] || "").trim().split(/\s+/).filter(Boolean);
@@ -187,13 +182,13 @@ function runSingleCommand(line = "", workspaceRoot = process.cwd()) {
187
182
  task,
188
183
  };
189
184
  }
190
- const resumeMatch = text.match(/^resume(?:\s+(.+))?$/i);
185
+ const resumeMatch = text.match(/^(?:\/resume|resume)(?:\s+(.+))?$/i);
191
186
  if (resumeMatch) {
192
187
  const session = String(resumeMatch[1] || "").trim();
193
188
  if (!session) {
194
189
  return {
195
190
  kind: "error",
196
- output: "usage: resume <session-id>",
191
+ output: "usage: /resume <session-id>",
197
192
  };
198
193
  }
199
194
  return {
@@ -439,6 +434,13 @@ async function runUcodeCoreAgent({
439
434
  });
440
435
  stdout.write(`${formatSessionUsageStatus(usageSummary)}\n`);
441
436
  }
437
+ if (result.kind === "model") {
438
+ const applied = applyUcodeModelCommand(state, result);
439
+ stdout.write(`${applied.output}\n`);
440
+ if (applied.ok && result.action === "set") {
441
+ persistSessionState(state);
442
+ }
443
+ }
442
444
  if (result.kind === "ubus") {
443
445
  const ubusResult = await runUbusCommand(state, {
444
446
  workspaceRoot: runtimeWorkspace,
@@ -637,4 +639,6 @@ module.exports = {
637
639
  extractAgentNickname,
638
640
  parseAgentArgs,
639
641
  formatSessionUsageStatus,
642
+ applyUcodeModelCommand,
643
+ suggestUcodeModels,
640
644
  };