u-foo 2.5.13 → 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 +187 -31
  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 +408 -55
  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,12 +34,18 @@ 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).
27
42
  const DEFAULT_OPENAI_MAX_TOKENS = 131072;
28
43
  const DEFAULT_ANTHROPIC_MAX_TOKENS = 64000;
44
+ // Extended thinking is on by default for the anthropic transport; the budget
45
+ // stays well below the 64K max_tokens cap as the Messages API requires.
46
+ // UFOO_UCODE_THINKING_BUDGET_TOKENS overrides; 0 or a non-numeric value
47
+ // disables thinking (the payload then omits the field entirely).
48
+ const DEFAULT_ANTHROPIC_THINKING_BUDGET_TOKENS = 10000;
29
49
  // Prompt caching is GA on the current Messages API: cache_control blocks need
30
50
  // no anthropic-beta header. Kept as a constant so the marker shape stays in
31
51
  // one place (system block + last history message, 2 of the 4 allowed
@@ -38,7 +58,7 @@ function nowMs() {
38
58
 
39
59
  function normalizeTimeoutMs(value) {
40
60
  const parsed = Number(value);
41
- if (!Number.isFinite(parsed)) return 300000;
61
+ if (!Number.isFinite(parsed)) return DEFAULT_NATIVE_TIMEOUT_MS;
42
62
  return Math.max(1000, Math.floor(parsed));
43
63
  }
44
64
 
@@ -59,6 +79,16 @@ function resolveMaxTokens(fallback) {
59
79
  return normalizePositiveInt(process.env.UFOO_UCODE_MAX_TOKENS, fallback);
60
80
  }
61
81
 
82
+ function resolveThinkingBudgetTokens() {
83
+ const raw = process.env.UFOO_UCODE_THINKING_BUDGET_TOKENS;
84
+ if (raw === undefined || raw === null || String(raw).trim() === "") {
85
+ return DEFAULT_ANTHROPIC_THINKING_BUDGET_TOKENS;
86
+ }
87
+ const parsed = Number.parseInt(String(raw), 10);
88
+ if (!Number.isFinite(parsed) || parsed <= 0) return 0;
89
+ return Math.floor(parsed);
90
+ }
91
+
62
92
  function toUsageInt(value) {
63
93
  const parsed = Number(value);
64
94
  if (!Number.isFinite(parsed) || parsed <= 0) return 0;
@@ -128,7 +158,7 @@ function enforceNativeToolBudget({
128
158
  }
129
159
  }
130
160
 
131
- function createGuards({ signal = null, timeoutMs = 300000 } = {}) {
161
+ function createGuards({ signal = null, timeoutMs = DEFAULT_NATIVE_TIMEOUT_MS } = {}) {
132
162
  const startedAt = nowMs();
133
163
  const budgetMs = normalizeTimeoutMs(timeoutMs);
134
164
 
@@ -349,6 +379,25 @@ function buildCoreToolSpecs() {
349
379
  },
350
380
  },
351
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
+ },
352
401
  ];
353
402
  }
354
403
 
@@ -360,7 +409,7 @@ function buildAnthropicToolSpecs() {
360
409
  }));
361
410
  }
362
411
 
363
- function createRequestController({ signal = null, timeoutMs = 300000 } = {}) {
412
+ function createRequestController({ signal = null, timeoutMs = DEFAULT_NATIVE_TIMEOUT_MS } = {}) {
364
413
  const controller = new AbortController();
365
414
  let timedOut = false;
366
415
 
@@ -422,11 +471,7 @@ function normalizeToolName(value = "") {
422
471
  }
423
472
 
424
473
  function toJsonString(value) {
425
- try {
426
- return JSON.stringify(value);
427
- } catch {
428
- return String(value || "");
429
- }
474
+ return stableStringify(value);
430
475
  }
431
476
 
432
477
  function parseSseBlocks(text = "") {
@@ -475,7 +520,15 @@ function normalizeToolCallArgs(raw = "") {
475
520
  return {};
476
521
  }
477
522
 
478
- 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
+ } = {}) {
479
532
  const normalizedTool = normalizeToolName(tool);
480
533
  if (!normalizedTool) {
481
534
  emitToolEvent(onToolEvent, {
@@ -491,6 +544,9 @@ function runCoreTool({ tool = "", args = {}, workspaceRoot = process.cwd(), onTo
491
544
  }
492
545
 
493
546
  const safeArgs = args && typeof args === "object" ? { ...args } : {};
547
+ if (normalizedTool === "artifact_read" && sessionId && !safeArgs.sessionId) {
548
+ safeArgs.sessionId = sessionId;
549
+ }
494
550
  emitToolEvent(onToolEvent, {
495
551
  tool: normalizedTool,
496
552
  phase: "start",
@@ -498,9 +554,13 @@ function runCoreTool({ tool = "", args = {}, workspaceRoot = process.cwd(), onTo
498
554
  error: "",
499
555
  });
500
556
 
557
+ const toolOptions = { workspaceRoot, cwd: workspaceRoot };
558
+ if (normalizedTool === "artifact_read" && sessionId) {
559
+ toolOptions.sessionId = sessionId;
560
+ }
501
561
  const result = runToolCall(
502
562
  { tool: normalizedTool, args: safeArgs },
503
- { workspaceRoot, cwd: workspaceRoot }
563
+ toolOptions,
504
564
  );
505
565
 
506
566
  if (!result || result.ok === false) {
@@ -510,6 +570,26 @@ function runCoreTool({ tool = "", args = {}, workspaceRoot = process.cwd(), onTo
510
570
  args: safeArgs,
511
571
  error: String((result && result.error) || `${normalizedTool} failed`),
512
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;
513
593
  }
514
594
 
515
595
  return result;
@@ -533,7 +613,7 @@ async function runSseRequest({
533
613
  headers = {},
534
614
  payload = {},
535
615
  signal = null,
536
- timeoutMs = 300000,
616
+ timeoutMs = DEFAULT_NATIVE_TIMEOUT_MS,
537
617
  onPhase = null,
538
618
  onNonStream,
539
619
  onEvent,
@@ -625,7 +705,7 @@ async function runOpenAiLikeTurn({
625
705
  onThinkingDelta = null,
626
706
  onPhase = null,
627
707
  signal = null,
628
- timeoutMs = 300000,
708
+ timeoutMs = DEFAULT_NATIVE_TIMEOUT_MS,
629
709
  } = {}) {
630
710
  const payload = {
631
711
  model,
@@ -794,6 +874,13 @@ function normalizeAnthropicMessageContent(raw = []) {
794
874
  text: String(item.text || ""),
795
875
  };
796
876
  }
877
+ if (item.type === "thinking") {
878
+ return {
879
+ type: "thinking",
880
+ thinking: String(item.thinking || ""),
881
+ signature: String(item.signature || ""),
882
+ };
883
+ }
797
884
  if (item.type === "tool_use") {
798
885
  return {
799
886
  type: "tool_use",
@@ -868,12 +955,13 @@ async function runAnthropicTurn({
868
955
  apiKey = "",
869
956
  model = "",
870
957
  systemPrompt = "",
958
+ systemBlocks = null,
871
959
  messages = [],
872
960
  onTextDelta = null,
873
961
  onThinkingDelta = null,
874
962
  onPhase = null,
875
963
  signal = null,
876
- timeoutMs = 300000,
964
+ timeoutMs = DEFAULT_NATIVE_TIMEOUT_MS,
877
965
  } = {}) {
878
966
  const payload = {
879
967
  model,
@@ -882,17 +970,23 @@ async function runAnthropicTurn({
882
970
  tools: buildAnthropicToolSpecs(),
883
971
  stream: true,
884
972
  };
885
- const systemText = String(systemPrompt || "").trim();
886
- if (systemText) {
887
- // Block form with a cache breakpoint; the system prompt is the most
888
- // stable prefix of every request.
889
- payload.system = [
890
- {
891
- type: "text",
892
- text: systemText,
893
- cache_control: { ...ANTHROPIC_CACHE_CONTROL },
894
- },
895
- ];
973
+ const thinkingBudget = resolveThinkingBudgetTokens();
974
+ if (thinkingBudget > 0) {
975
+ payload.thinking = { type: "enabled", budget_tokens: thinkingBudget };
976
+ }
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
+ }
896
990
  }
897
991
 
898
992
  const headers = {
@@ -994,6 +1088,7 @@ async function runAnthropicTurn({
994
1088
  order: index,
995
1089
  type: "thinking",
996
1090
  text: String(contentBlock.thinking || ""),
1091
+ signature: String(contentBlock.signature || ""),
997
1092
  });
998
1093
  } else if (contentBlock.type === "tool_use") {
999
1094
  blockMap.set(index, {
@@ -1058,6 +1153,16 @@ async function runAnthropicTurn({
1058
1153
  return;
1059
1154
  }
1060
1155
 
1156
+ if (delta.type === "signature_delta") {
1157
+ // Signed thinking blocks must be replayed verbatim on later turns
1158
+ // (tool-use continuation contract), so accumulate the signature
1159
+ // alongside the thinking text.
1160
+ current.type = "thinking";
1161
+ current.signature = `${String(current.signature || "")}${String(delta.signature || "")}`;
1162
+ blockMap.set(index, current);
1163
+ return;
1164
+ }
1165
+
1061
1166
  if (delta.type === "input_json_delta") {
1062
1167
  current.type = "tool_use";
1063
1168
  current.inputJson = `${String(current.inputJson || "")}${String(delta.partial_json || "")}`;
@@ -1069,8 +1174,17 @@ async function runAnthropicTurn({
1069
1174
  buildResult: () => {
1070
1175
  const assistantContent = Array.from(blockMap.values())
1071
1176
  .sort((a, b) => a.order - b.order)
1072
- .filter((item) => item.type !== "thinking")
1073
1177
  .map((item) => {
1178
+ if (item.type === "thinking") {
1179
+ // Kept (with signature) so tool-use continuation turns can
1180
+ // replay the thinking blocks the API requires.
1181
+ return {
1182
+ type: "thinking",
1183
+ thinking: String(item.text || ""),
1184
+ signature: String(item.signature || ""),
1185
+ };
1186
+ }
1187
+
1074
1188
  if (item.type === "text") {
1075
1189
  return {
1076
1190
  type: "text",
@@ -1248,16 +1362,20 @@ async function runNativeLoop({
1248
1362
  workspaceRoot = process.cwd(),
1249
1363
  prompt = "",
1250
1364
  systemPrompt = "",
1365
+ systemBlocks = null,
1251
1366
  historyMessages = [],
1252
1367
  model = "",
1253
1368
  baseUrl = "",
1254
1369
  apiKey = "",
1255
1370
  provider = "",
1256
- timeoutMs = 300000,
1371
+ timeoutMs = DEFAULT_NATIVE_TIMEOUT_MS,
1257
1372
  onStreamDelta = null,
1258
1373
  onThinkingDelta = null,
1259
1374
  onPhase = null,
1260
1375
  onToolEvent = null,
1376
+ onArtifactPersisted = null,
1377
+ sessionId = "",
1378
+ contextV2 = false,
1261
1379
  signal = null,
1262
1380
  guards,
1263
1381
  } = {}) {
@@ -1271,13 +1389,14 @@ async function runNativeLoop({
1271
1389
  throw new Error("ucode baseUrl is not configured");
1272
1390
  }
1273
1391
 
1274
- const messages = cloneMessageList(historyMessages);
1392
+ const messages = sanitizeModelMessages(cloneMessageList(historyMessages));
1275
1393
  transport.prepareMessages({ messages, systemPrompt, prompt });
1276
1394
 
1277
1395
  let aggregated = "";
1278
1396
  let streamed = false;
1279
1397
  let toolCallsExecuted = 0;
1280
1398
  let toolErrors = 0;
1399
+ let executionState = emptyExecutionState();
1281
1400
  const toolBudget = resolveNativeToolBudget();
1282
1401
  const usage = createUsageTotals();
1283
1402
 
@@ -1290,6 +1409,7 @@ async function runNativeLoop({
1290
1409
  model: requestModel,
1291
1410
  provider,
1292
1411
  systemPrompt,
1412
+ systemBlocks,
1293
1413
  messages,
1294
1414
  signal,
1295
1415
  timeoutMs,
@@ -1312,8 +1432,34 @@ async function runNativeLoop({
1312
1432
  const toolCalls = transport.getToolCalls(turnResult);
1313
1433
 
1314
1434
  if (toolCalls.length === 0) {
1315
- transport.appendFinalAssistantMessage({ messages, turnResult });
1316
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 });
1317
1463
  if (!aggregated.trim() && text) {
1318
1464
  aggregated = text;
1319
1465
  }
@@ -1344,6 +1490,9 @@ async function runNativeLoop({
1344
1490
  args: pending.args,
1345
1491
  workspaceRoot,
1346
1492
  onToolEvent,
1493
+ sessionId,
1494
+ contextV2,
1495
+ onArtifactPersisted,
1347
1496
  });
1348
1497
  toolCallsExecuted += 1;
1349
1498
  if (!toolResult || toolResult.ok === false) {
@@ -1375,15 +1524,18 @@ async function runNativeAgentTask({
1375
1524
  workspaceRoot = process.cwd(),
1376
1525
  prompt = "",
1377
1526
  systemPrompt = "",
1527
+ systemBlocks = null,
1378
1528
  provider = "",
1379
1529
  model = "",
1380
1530
  messages = [],
1381
1531
  sessionId = "",
1382
- timeoutMs = 300000,
1532
+ timeoutMs = DEFAULT_NATIVE_TIMEOUT_MS,
1383
1533
  onStreamDelta = null,
1384
1534
  onThinkingDelta = null,
1385
1535
  onPhase = null,
1386
1536
  onToolEvent = null,
1537
+ onArtifactPersisted = null,
1538
+ contextV2 = false,
1387
1539
  signal = null,
1388
1540
  } = {}) {
1389
1541
  const guards = createGuards({ signal, timeoutMs });
@@ -1444,6 +1596,7 @@ async function runNativeAgentTask({
1444
1596
  workspaceRoot,
1445
1597
  prompt: promptText,
1446
1598
  systemPrompt,
1599
+ systemBlocks,
1447
1600
  historyMessages: messages,
1448
1601
  model: runtime.model,
1449
1602
  baseUrl: runtime.baseUrl,
@@ -1454,6 +1607,9 @@ async function runNativeAgentTask({
1454
1607
  onThinkingDelta,
1455
1608
  onPhase,
1456
1609
  onToolEvent,
1610
+ onArtifactPersisted,
1611
+ sessionId: nextSessionId,
1612
+ contextV2: contextV2 || isContextV2Enabled(),
1457
1613
  signal,
1458
1614
  guards,
1459
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
  };