u-foo 2.5.14 → 3.0.0

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 (59) hide show
  1. package/package.json +1 -1
  2. package/src/agents/prompts/native/environment.js +20 -8
  3. package/src/code/agent.js +517 -112
  4. package/src/code/commands.js +77 -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 +703 -0
  9. package/src/code/context/executionSegment.js +292 -0
  10. package/src/code/context/index.js +28 -0
  11. package/src/code/context/planGraph.js +1410 -0
  12. package/src/code/context/planGraphService.js +857 -0
  13. package/src/code/context/planMode.js +398 -0
  14. package/src/code/context/planProjection.js +432 -0
  15. package/src/code/context/projectSnapshot.js +201 -0
  16. package/src/code/context/promptLayers.js +175 -0
  17. package/src/code/context/reducers.js +328 -0
  18. package/src/code/context/stableJson.js +29 -0
  19. package/src/code/context/stateCommit.js +414 -0
  20. package/src/code/context/toolRuntime.js +172 -0
  21. package/src/code/context/transcript.js +182 -0
  22. package/src/code/context/transcriptSync.js +106 -0
  23. package/src/code/context/userInteraction.js +457 -0
  24. package/src/code/context/userNudge.js +116 -0
  25. package/src/code/context/workingSet.js +323 -0
  26. package/src/code/dispatch.js +20 -1
  27. package/src/code/index.js +8 -0
  28. package/src/code/modelCommand.js +87 -0
  29. package/src/code/nativeRunner.js +625 -34
  30. package/src/code/repl.js +196 -50
  31. package/src/code/runtime/agentWakeup.js +58 -0
  32. package/src/code/runtime/graphOwner.js +41 -0
  33. package/src/code/runtime/graphYieldRouter.js +42 -0
  34. package/src/code/runtime/index.js +15 -0
  35. package/src/code/runtime/loopMailbox.js +124 -0
  36. package/src/code/runtime/runtimeEvents.js +39 -0
  37. package/src/code/runtime/taskControl.js +565 -0
  38. package/src/code/runtime/taskFocus.js +165 -0
  39. package/src/code/runtime/taskLoop.js +383 -0
  40. package/src/code/runtime/taskRun.js +187 -0
  41. package/src/code/runtime/toolProvenance.js +70 -0
  42. package/src/code/runtime/workspaceLease.js +208 -0
  43. package/src/code/sessionStore.js +217 -15
  44. package/src/code/skills/index.js +10 -0
  45. package/src/code/skills/injection.js +66 -3
  46. package/src/code/skills/loader.js +21 -0
  47. package/src/code/skills/manifest.js +87 -0
  48. package/src/code/skills/render.js +15 -1
  49. package/src/code/taskDecomposer.js +56 -2
  50. package/src/code/tools/artifactRead.js +40 -0
  51. package/src/code/tools/askUser.js +11 -0
  52. package/src/code/tools/planGraph.js +29 -0
  53. package/src/code/tui.js +2 -0
  54. package/src/code/usageStore.js +15 -0
  55. package/src/ui/format/index.js +285 -45
  56. package/src/ui/format/markdownRenderer.js +436 -71
  57. package/src/ui/ink/ChatApp.js +39 -8
  58. package/src/ui/ink/UcodeApp.js +592 -43
  59. package/src/ui/ink/chatLogModel.js +102 -21
@@ -6,12 +6,50 @@ const {
6
6
  } = require("../agents/providers/credentials/kimi");
7
7
  const { runToolCall } = require("./dispatch");
8
8
  const { appendUsageRecord } = require("./usageStore");
9
+ const {
10
+ persistToolResultToContext,
11
+ sanitizeModelMessages,
12
+ } = require("./context/assembler");
13
+ const { systemBlocksToAnthropicPayload } = require("./context/promptLayers");
14
+ const { parseStructuredSideEffects } = require("./context/stateCommit");
15
+ const {
16
+ emptyExecutionState,
17
+ } = require("./context/executionSegment");
18
+ const {
19
+ normalizePlanGraphCommand,
20
+ runPlanGraphCommand,
21
+ activePlanRequiresExpansion,
22
+ } = require("./context/planGraphService");
23
+ const { planModeBlocksDirectTool } = require("./context/planMode");
24
+ const {
25
+ drainUserPrompts,
26
+ clearUserPrompts,
27
+ formatUserReminderMessage,
28
+ ensurePendingUserPrompts,
29
+ } = require("./context/userNudge");
30
+ const {
31
+ runAskUserTool,
32
+ syncInteractionFromPlanGraph,
33
+ hasPendingUserInteraction,
34
+ getPendingUserInteraction,
35
+ } = require("./context/userInteraction");
36
+ const { checkWriteAllowed } = require("./runtime/workspaceLease");
37
+ const { stableStringify } = require("./context/stableJson");
9
38
  const { getReadToolDescription } = require("../agents/prompts/native/toolDescriptions/read");
10
39
  const { getWriteToolDescription } = require("../agents/prompts/native/toolDescriptions/write");
11
40
  const { getEditToolDescription } = require("../agents/prompts/native/toolDescriptions/edit");
12
41
  const { getBashToolDescription } = require("../agents/prompts/native/toolDescriptions/bash");
13
42
 
14
- const CORE_TOOL_NAMES = new Set(["read", "write", "edit", "bash"]);
43
+ const CORE_TOOL_NAMES = new Set([
44
+ "read",
45
+ "write",
46
+ "edit",
47
+ "bash",
48
+ "artifact_read",
49
+ "plan_graph",
50
+ "ask_user",
51
+ ]);
52
+ const EXECUTABLE_GRAPH_TOOLS = new Set(["read", "write", "edit", "bash", "artifact_read"]);
15
53
  const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
16
54
  const DEFAULT_ANTHROPIC_BASE_URL = "https://api.anthropic.com/v1";
17
55
  const DEFAULT_KIMI_BASE_URL = "https://api.kimi.com/coding/v1";
@@ -20,7 +58,8 @@ const DEFAULT_KIMI_MODEL = "k3";
20
58
  // to 200 (fork). We count individual tool calls (not turns), so 100 leaves headroom
21
59
  // for non-trivial tasks while still catching runaway loops. Override via env.
22
60
  const DEFAULT_MAX_NATIVE_TOOL_CALLS = 100;
23
- const DEFAULT_MAX_NATIVE_TOOL_ERRORS = 5;
61
+ const DEFAULT_MAX_NATIVE_TOOL_ERRORS = 20;
62
+ const DEFAULT_NATIVE_TIMEOUT_MS = 43200000; // 12 hours
24
63
  // Anthropic Messages rejects max_tokens above the model's real cap (64K on
25
64
  // current models), so the transports use different defaults. Override either
26
65
  // via UFOO_UCODE_MAX_TOKENS (positive integer).
@@ -43,7 +82,7 @@ function nowMs() {
43
82
 
44
83
  function normalizeTimeoutMs(value) {
45
84
  const parsed = Number(value);
46
- if (!Number.isFinite(parsed)) return 300000;
85
+ if (!Number.isFinite(parsed)) return DEFAULT_NATIVE_TIMEOUT_MS;
47
86
  return Math.max(1000, Math.floor(parsed));
48
87
  }
49
88
 
@@ -143,7 +182,7 @@ function enforceNativeToolBudget({
143
182
  }
144
183
  }
145
184
 
146
- function createGuards({ signal = null, timeoutMs = 300000 } = {}) {
185
+ function createGuards({ signal = null, timeoutMs = DEFAULT_NATIVE_TIMEOUT_MS } = {}) {
147
186
  const startedAt = nowMs();
148
187
  const budgetMs = normalizeTimeoutMs(timeoutMs);
149
188
 
@@ -169,7 +208,9 @@ function createGuards({ signal = null, timeoutMs = 300000 } = {}) {
169
208
  function emitToolEvent(callback, event = {}) {
170
209
  if (typeof callback !== "function") return;
171
210
  try {
172
- callback(event);
211
+ const payload = event && typeof event === "object" ? { ...event } : {};
212
+ if (payload.origin == null) delete payload.origin;
213
+ callback(payload);
173
214
  } catch {
174
215
  // ignore callback failures
175
216
  }
@@ -364,6 +405,151 @@ function buildCoreToolSpecs() {
364
405
  },
365
406
  },
366
407
  },
408
+ {
409
+ type: "function",
410
+ function: {
411
+ name: "artifact_read",
412
+ description: [
413
+ "Read previously stored tool output by artifactId.",
414
+ "This does not read workspace files; use `read` for repository paths.",
415
+ "Optionally read a slice with startLine/endLine, maxChars, or tailLines.",
416
+ ].join(" "),
417
+ parameters: {
418
+ type: "object",
419
+ properties: {
420
+ artifactId: { type: "string" },
421
+ sessionId: { type: "string" },
422
+ startLine: { type: "integer" },
423
+ endLine: { type: "integer" },
424
+ maxChars: { type: "integer" },
425
+ tailLines: { type: "integer" },
426
+ },
427
+ required: ["artifactId"],
428
+ },
429
+ },
430
+ },
431
+ {
432
+ type: "function",
433
+ function: {
434
+ name: "plan_graph",
435
+ description: [
436
+ "Manage the persistent Plan Graph and asynchronous TaskRuns.",
437
+ "Use create, patch, inspect, or cancel_graph for graph operations, and control for TaskRun lifecycle.",
438
+ "`control.start_task` starts a `task_loop` asynchronously and returns immediately.",
439
+ "Use `inline_llm` for work handled by the current graph owner,",
440
+ "`expand` for tasks that must be lowered into child nodes,",
441
+ "and `task_loop` for asynchronous work in an independent TaskLoop.",
442
+ "Do not call `plan_graph` together with data-plane tools in the same assistant turn.",
443
+ ].join(" "),
444
+ parameters: {
445
+ type: "object",
446
+ properties: {
447
+ operation: {
448
+ type: "string",
449
+ enum: [
450
+ "create",
451
+ "patch",
452
+ "inspect",
453
+ "clear",
454
+ "cancel_graph",
455
+ "control",
456
+ ],
457
+ description: [
458
+ "create/patch/inspect/cancel_graph mutate or inspect the graph spec;",
459
+ "control runs TaskRun lifecycle and node status actions.",
460
+ ].join(" "),
461
+ },
462
+ graph: {
463
+ type: "object",
464
+ description: "Full graph for create (objective + nodes). group is input sugar only.",
465
+ },
466
+ operations: {
467
+ type: "array",
468
+ description: [
469
+ "Patch ops only: add_node, expand_node, add_dependency, remove_dependency.",
470
+ "Status actions (complete_task, skip_node, cancel_subtree) belong under control.actions.",
471
+ ].join(" "),
472
+ items: { type: "object" },
473
+ },
474
+ actions: {
475
+ type: "array",
476
+ description: [
477
+ "Control actions: start_task, cancel_task, fail_task, complete_task, skip_node, cancel_subtree.",
478
+ "complete_task with taskRunId finishes a TaskLoop TaskRun;",
479
+ "complete_task with nodeId finishes a waiting_llm inline task owned by the graph owner.",
480
+ ].join(" "),
481
+ items: { type: "object" },
482
+ },
483
+ reason: {
484
+ type: "string",
485
+ description: "Optional reason for cancel_graph or fail/cancel task.",
486
+ },
487
+ commandId: {
488
+ type: "string",
489
+ description: [
490
+ "Optional idempotency key for explicit replay.",
491
+ "When omitted, the Runtime should derive one from the tool invocation when available.",
492
+ ].join(" "),
493
+ },
494
+ expectedSpecRevision: {
495
+ type: "integer",
496
+ description: "Optional optimistic concurrency token for patch.",
497
+ },
498
+ graphId: {
499
+ type: "string",
500
+ description: "Optional graph id check for patch/control.",
501
+ },
502
+ },
503
+ required: ["operation"],
504
+ },
505
+ },
506
+ },
507
+ {
508
+ type: "function",
509
+ function: {
510
+ name: "ask_user",
511
+ description: [
512
+ "Ask the user for input and pause the current Agent loop until the reply arrives.",
513
+ "Use only when user input is required to proceed, not for routine updates or decisions the agent can safely make.",
514
+ "`kind=approval` requests yes/no confirmation; `kind=choice` presents the supplied options; `kind=chat` requests free text.",
515
+ "This must be the only tool call in the turn.",
516
+ "The reply is returned only as this tool result, not as a separate user message or pending user prompt.",
517
+ "After the tool returns, continue from the answer and do not ask the same question again.",
518
+ "Running TaskRuns are not paused automatically.",
519
+ ].join(" "),
520
+ parameters: {
521
+ type: "object",
522
+ properties: {
523
+ kind: {
524
+ type: "string",
525
+ enum: ["approval", "choice", "chat"],
526
+ description: "Interaction type.",
527
+ },
528
+ prompt: {
529
+ type: "string",
530
+ description: "Question shown to the user.",
531
+ },
532
+ options: {
533
+ type: "array",
534
+ description: "For choice: option labels (or {key,label} objects). Ignored for chat.",
535
+ items: {
536
+ oneOf: [
537
+ { type: "string" },
538
+ {
539
+ type: "object",
540
+ properties: {
541
+ key: { type: "string" },
542
+ label: { type: "string" },
543
+ },
544
+ },
545
+ ],
546
+ },
547
+ },
548
+ },
549
+ required: ["kind", "prompt"],
550
+ },
551
+ },
552
+ },
367
553
  ];
368
554
  }
369
555
 
@@ -375,7 +561,7 @@ function buildAnthropicToolSpecs() {
375
561
  }));
376
562
  }
377
563
 
378
- function createRequestController({ signal = null, timeoutMs = 300000 } = {}) {
564
+ function createRequestController({ signal = null, timeoutMs = DEFAULT_NATIVE_TIMEOUT_MS } = {}) {
379
565
  const controller = new AbortController();
380
566
  let timedOut = false;
381
567
 
@@ -437,11 +623,7 @@ function normalizeToolName(value = "") {
437
623
  }
438
624
 
439
625
  function toJsonString(value) {
440
- try {
441
- return JSON.stringify(value);
442
- } catch {
443
- return String(value || "");
444
- }
626
+ return stableStringify(value);
445
627
  }
446
628
 
447
629
  function parseSseBlocks(text = "") {
@@ -490,7 +672,17 @@ function normalizeToolCallArgs(raw = "") {
490
672
  return {};
491
673
  }
492
674
 
493
- function runCoreTool({ tool = "", args = {}, workspaceRoot = process.cwd(), onToolEvent = null } = {}) {
675
+ function runCoreTool({
676
+ tool = "",
677
+ args = {},
678
+ workspaceRoot = process.cwd(),
679
+ onToolEvent = null,
680
+ sessionId = "",
681
+ onArtifactPersisted = null,
682
+ executionState = null,
683
+ origin = null,
684
+ resume = null,
685
+ } = {}) {
494
686
  const normalizedTool = normalizeToolName(tool);
495
687
  if (!normalizedTool) {
496
688
  emitToolEvent(onToolEvent, {
@@ -498,6 +690,7 @@ function runCoreTool({ tool = "", args = {}, workspaceRoot = process.cwd(), onTo
498
690
  phase: "error",
499
691
  args: args && typeof args === "object" ? { ...args } : {},
500
692
  error: `unsupported tool: ${tool}`,
693
+ origin,
501
694
  });
502
695
  return {
503
696
  ok: false,
@@ -506,16 +699,101 @@ function runCoreTool({ tool = "", args = {}, workspaceRoot = process.cwd(), onTo
506
699
  }
507
700
 
508
701
  const safeArgs = args && typeof args === "object" ? { ...args } : {};
702
+ if (normalizedTool === "artifact_read" && sessionId && !safeArgs.sessionId) {
703
+ safeArgs.sessionId = sessionId;
704
+ }
509
705
  emitToolEvent(onToolEvent, {
510
706
  tool: normalizedTool,
511
707
  phase: "start",
512
708
  args: safeArgs,
513
709
  error: "",
710
+ origin,
514
711
  });
515
712
 
713
+ if (normalizedTool === "plan_graph") {
714
+ const state = executionState && typeof executionState === "object"
715
+ ? executionState
716
+ : emptyExecutionState();
717
+ const result = runPlanGraphCommand(safeArgs, {
718
+ executionState: state,
719
+ autoAdvance: true,
720
+ parallel: true,
721
+ runTool: ({ node, args: nestedArgs, tool: nestedTool, stepId }) => {
722
+ const nested = runCoreTool({
723
+ tool: nestedTool,
724
+ args: nestedArgs,
725
+ workspaceRoot,
726
+ onToolEvent,
727
+ sessionId,
728
+ onArtifactPersisted,
729
+ executionState: state,
730
+ origin: {
731
+ kind: "plan_graph",
732
+ graphId: String(state.planGraph && state.planGraph.graphId || ""),
733
+ graphRevision: Number(state.planGraph && state.planGraph.specRevision) || 0,
734
+ commandRevision: Number(state.planGraph && state.planGraph.specRevision) || 0,
735
+ nodeId: stepId || (node && node.id) || "",
736
+ attempt: Number(node && node.attempt) || 0,
737
+ },
738
+ });
739
+ return nested;
740
+ },
741
+ });
742
+ if (result.ok === false) {
743
+ emitToolEvent(onToolEvent, {
744
+ tool: "plan_graph",
745
+ phase: "error",
746
+ args: safeArgs,
747
+ error: Array.isArray(result.errors)
748
+ ? result.errors.map((e) => e.message || e.code).join("; ")
749
+ : "plan_graph rejected",
750
+ origin,
751
+ });
752
+ } else {
753
+ syncInteractionFromPlanGraph(result.executionState || state);
754
+ }
755
+ return {
756
+ ...result.modelPayload,
757
+ ok: result.status === "accepted",
758
+ executionState: result.executionState || state,
759
+ };
760
+ }
761
+
762
+ if (normalizedTool === "ask_user") {
763
+ const state = executionState && typeof executionState === "object"
764
+ ? executionState
765
+ : emptyExecutionState();
766
+ const result = runAskUserTool(safeArgs, {
767
+ executionState: state,
768
+ resume: resume || null,
769
+ });
770
+ const ok = result.ok !== false && result.status !== "rejected";
771
+ emitToolEvent(onToolEvent, {
772
+ tool: "ask_user",
773
+ phase: ok ? "end" : "error",
774
+ args: safeArgs,
775
+ result: result.modelPayload || result,
776
+ error: ok ? "" : (result.error || "ask_user rejected"),
777
+ origin,
778
+ });
779
+ return {
780
+ ...(result.modelPayload || result),
781
+ ok,
782
+ status: result.status,
783
+ waiting_user: Boolean(result.waiting_user || result.status === "waiting_user"),
784
+ interactionId: result.interactionId || "",
785
+ executionState: result.executionState || state,
786
+ deferToolResult: ok && result.status === "waiting_user",
787
+ };
788
+ }
789
+
790
+ const toolOptions = { workspaceRoot, cwd: workspaceRoot };
791
+ if (normalizedTool === "artifact_read" && sessionId) {
792
+ toolOptions.sessionId = sessionId;
793
+ }
516
794
  const result = runToolCall(
517
795
  { tool: normalizedTool, args: safeArgs },
518
- { workspaceRoot, cwd: workspaceRoot }
796
+ toolOptions,
519
797
  );
520
798
 
521
799
  if (!result || result.ok === false) {
@@ -524,9 +802,34 @@ function runCoreTool({ tool = "", args = {}, workspaceRoot = process.cwd(), onTo
524
802
  phase: "error",
525
803
  args: safeArgs,
526
804
  error: String((result && result.error) || `${normalizedTool} failed`),
805
+ origin,
527
806
  });
807
+ return result;
528
808
  }
529
809
 
810
+ if (normalizedTool !== "artifact_read" && EXECUTABLE_GRAPH_TOOLS.has(normalizedTool)) {
811
+ const persisted = persistToolResultToContext({
812
+ workspaceRoot,
813
+ sessionId,
814
+ tool: normalizedTool,
815
+ args: safeArgs,
816
+ rawResult: result,
817
+ });
818
+ if (typeof onArtifactPersisted === "function") {
819
+ try {
820
+ onArtifactPersisted(persisted);
821
+ } catch {
822
+ // ignore
823
+ }
824
+ }
825
+ const payload = persisted.modelPayload || result;
826
+ if (origin) payload.origin = origin;
827
+ return payload;
828
+ }
829
+
830
+ if (origin && result && typeof result === "object") {
831
+ return { ...result, origin };
832
+ }
530
833
  return result;
531
834
  }
532
835
 
@@ -548,7 +851,7 @@ async function runSseRequest({
548
851
  headers = {},
549
852
  payload = {},
550
853
  signal = null,
551
- timeoutMs = 300000,
854
+ timeoutMs = DEFAULT_NATIVE_TIMEOUT_MS,
552
855
  onPhase = null,
553
856
  onNonStream,
554
857
  onEvent,
@@ -640,7 +943,7 @@ async function runOpenAiLikeTurn({
640
943
  onThinkingDelta = null,
641
944
  onPhase = null,
642
945
  signal = null,
643
- timeoutMs = 300000,
946
+ timeoutMs = DEFAULT_NATIVE_TIMEOUT_MS,
644
947
  } = {}) {
645
948
  const payload = {
646
949
  model,
@@ -890,12 +1193,13 @@ async function runAnthropicTurn({
890
1193
  apiKey = "",
891
1194
  model = "",
892
1195
  systemPrompt = "",
1196
+ systemBlocks = null,
893
1197
  messages = [],
894
1198
  onTextDelta = null,
895
1199
  onThinkingDelta = null,
896
1200
  onPhase = null,
897
1201
  signal = null,
898
- timeoutMs = 300000,
1202
+ timeoutMs = DEFAULT_NATIVE_TIMEOUT_MS,
899
1203
  } = {}) {
900
1204
  const payload = {
901
1205
  model,
@@ -908,17 +1212,19 @@ async function runAnthropicTurn({
908
1212
  if (thinkingBudget > 0) {
909
1213
  payload.thinking = { type: "enabled", budget_tokens: thinkingBudget };
910
1214
  }
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
- ];
1215
+ if (Array.isArray(systemBlocks) && systemBlocks.length > 0) {
1216
+ payload.system = systemBlocksToAnthropicPayload(systemBlocks);
1217
+ } else {
1218
+ const systemText = String(systemPrompt || "").trim();
1219
+ if (systemText) {
1220
+ payload.system = [
1221
+ {
1222
+ type: "text",
1223
+ text: systemText,
1224
+ cache_control: { ...ANTHROPIC_CACHE_CONTROL },
1225
+ },
1226
+ ];
1227
+ }
922
1228
  }
923
1229
 
924
1230
  const headers = {
@@ -1294,18 +1600,23 @@ async function runNativeLoop({
1294
1600
  workspaceRoot = process.cwd(),
1295
1601
  prompt = "",
1296
1602
  systemPrompt = "",
1603
+ systemBlocks = null,
1297
1604
  historyMessages = [],
1298
1605
  model = "",
1299
1606
  baseUrl = "",
1300
1607
  apiKey = "",
1301
1608
  provider = "",
1302
- timeoutMs = 300000,
1609
+ timeoutMs = DEFAULT_NATIVE_TIMEOUT_MS,
1303
1610
  onStreamDelta = null,
1304
1611
  onThinkingDelta = null,
1305
1612
  onPhase = null,
1306
1613
  onToolEvent = null,
1614
+ onArtifactPersisted = null,
1615
+ sessionId = "",
1307
1616
  signal = null,
1308
1617
  guards,
1618
+ executionState: initialExecutionState = null,
1619
+ resume = false,
1309
1620
  } = {}) {
1310
1621
  const requestModel = String(model || "").trim();
1311
1622
  if (!requestModel) {
@@ -1317,25 +1628,46 @@ async function runNativeLoop({
1317
1628
  throw new Error("ucode baseUrl is not configured");
1318
1629
  }
1319
1630
 
1320
- const messages = cloneMessageList(historyMessages);
1321
- transport.prepareMessages({ messages, systemPrompt, prompt });
1631
+ const messages = sanitizeModelMessages(cloneMessageList(historyMessages));
1632
+ if (!resume) {
1633
+ transport.prepareMessages({ messages, systemPrompt, prompt });
1634
+ }
1322
1635
 
1323
1636
  let aggregated = "";
1324
1637
  let streamed = false;
1325
1638
  let toolCallsExecuted = 0;
1326
1639
  let toolErrors = 0;
1640
+ let executionState = initialExecutionState && typeof initialExecutionState === "object"
1641
+ ? initialExecutionState
1642
+ : emptyExecutionState();
1643
+ if (typeof executionState.planMode !== "boolean") executionState.planMode = false;
1644
+ ensurePendingUserPrompts(executionState);
1327
1645
  const toolBudget = resolveNativeToolBudget();
1328
1646
  const usage = createUsageTotals();
1329
1647
 
1648
+ function injectPendingUserReminders() {
1649
+ const nudges = drainUserPrompts(executionState);
1650
+ if (nudges.length === 0) return;
1651
+ const waiting = executionState.planGraph && executionState.planGraph.waitingFor
1652
+ ? executionState.planGraph.waitingFor
1653
+ : null;
1654
+ const content = formatUserReminderMessage(nudges, { waitingFor: waiting });
1655
+ if (!content) return;
1656
+ messages.push({ role: "user", content });
1657
+ }
1658
+
1330
1659
  while (true) {
1331
1660
  guards.ensureActive();
1332
1661
 
1662
+ injectPendingUserReminders();
1663
+
1333
1664
  const turnResult = await transport.runTurn({
1334
1665
  url: requestUrl,
1335
1666
  apiKey,
1336
1667
  model: requestModel,
1337
1668
  provider,
1338
1669
  systemPrompt,
1670
+ systemBlocks,
1339
1671
  messages,
1340
1672
  signal,
1341
1673
  timeoutMs,
@@ -1358,8 +1690,36 @@ async function runNativeLoop({
1358
1690
  const toolCalls = transport.getToolCalls(turnResult);
1359
1691
 
1360
1692
  if (toolCalls.length === 0) {
1361
- transport.appendFinalAssistantMessage({ messages, turnResult });
1362
1693
  const text = String(turnResult.text || "").trim();
1694
+ const sideEffects = parseStructuredSideEffects(text);
1695
+ const planCommand = sideEffects ? normalizePlanGraphCommand(sideEffects) : null;
1696
+ if (planCommand) {
1697
+ transport.appendFinalAssistantMessage({ messages, turnResult });
1698
+ const planResult = runCoreTool({
1699
+ tool: "plan_graph",
1700
+ args: planCommand,
1701
+ workspaceRoot,
1702
+ onToolEvent,
1703
+ sessionId,
1704
+ onArtifactPersisted,
1705
+ executionState,
1706
+ origin: { kind: "legacy_side_effect", source: planCommand.source || "legacy" },
1707
+ });
1708
+ if (planResult && planResult.executionState) {
1709
+ executionState = planResult.executionState;
1710
+ }
1711
+ messages.push({
1712
+ role: "user",
1713
+ content: JSON.stringify({
1714
+ type: "plan_graph_result",
1715
+ ...((planResult && planResult.status)
1716
+ ? planResult
1717
+ : { status: "rejected", ok: false, error: "plan_graph failed" }),
1718
+ }),
1719
+ });
1720
+ continue;
1721
+ }
1722
+ transport.appendFinalAssistantMessage({ messages, turnResult });
1363
1723
  if (!aggregated.trim() && text) {
1364
1724
  aggregated = text;
1365
1725
  }
@@ -1369,6 +1729,7 @@ async function runNativeLoop({
1369
1729
  toolCallsExecuted,
1370
1730
  messages,
1371
1731
  usage,
1732
+ executionState,
1372
1733
  };
1373
1734
  }
1374
1735
 
@@ -1380,21 +1741,180 @@ async function runNativeLoop({
1380
1741
  toolCallsExecuted,
1381
1742
  messages,
1382
1743
  usage,
1744
+ executionState,
1383
1745
  };
1384
1746
  }
1385
1747
 
1748
+ const callNames = pendingCalls.map((call) => String(call.name || "").trim().toLowerCase());
1749
+ const hasPlanGraph = callNames.includes("plan_graph");
1750
+ const hasAskUser = callNames.includes("ask_user");
1751
+ const hasDataTool = callNames.some((name) => EXECUTABLE_GRAPH_TOOLS.has(name));
1752
+ if (hasPlanGraph && hasDataTool) {
1753
+ // prepareToolCalls already appended the assistant tool_calls / tool_use
1754
+ // message; every declared call must get a contiguous tool result.
1755
+ const collectedResults = [];
1756
+ for (const pending of pendingCalls) {
1757
+ transport.appendToolResult({
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
+ });
1768
+ toolCallsExecuted += 1;
1769
+ toolErrors += 1;
1770
+ }
1771
+ if (typeof transport.flushToolResults === "function") {
1772
+ transport.flushToolResults({ messages, collected: collectedResults });
1773
+ }
1774
+ continue;
1775
+ }
1776
+ if (hasAskUser && pendingCalls.length > 1) {
1777
+ const collectedResults = [];
1778
+ for (const pending of pendingCalls) {
1779
+ transport.appendToolResult({
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
+ });
1790
+ toolCallsExecuted += 1;
1791
+ toolErrors += 1;
1792
+ }
1793
+ if (typeof transport.flushToolResults === "function") {
1794
+ transport.flushToolResults({ messages, collected: collectedResults });
1795
+ }
1796
+ continue;
1797
+ }
1798
+
1386
1799
  const collectedResults = [];
1800
+ let deferredAskUser = null;
1387
1801
  for (const pending of pendingCalls) {
1802
+ const pendingName = String(pending.name || "").trim().toLowerCase();
1803
+ if (
1804
+ EXECUTABLE_GRAPH_TOOLS.has(pendingName)
1805
+ && activePlanRequiresExpansion(executionState.planGraph)
1806
+ ) {
1807
+ const blocked = {
1808
+ ok: false,
1809
+ status: "rejected",
1810
+ errors: [{
1811
+ code: "ACTIVE_PLAN_REQUIRES_EXPANSION",
1812
+ message: "Active plan is waiting on a task; use plan_graph expand_node or control.complete_task instead of direct tools",
1813
+ }],
1814
+ };
1815
+ toolCallsExecuted += 1;
1816
+ toolErrors += 1;
1817
+ transport.appendToolResult({
1818
+ messages,
1819
+ collected: collectedResults,
1820
+ call: pending,
1821
+ toolResult: blocked,
1822
+ });
1823
+ continue;
1824
+ }
1825
+ if (planModeBlocksDirectTool(pendingName, executionState)) {
1826
+ const blocked = {
1827
+ ok: false,
1828
+ status: "rejected",
1829
+ errors: [{
1830
+ code: "PLAN_MODE_BLOCKS_SIDE_EFFECT",
1831
+ message: "Plan mode is on; use plan_graph for write/edit/bash, or ask the user to /plan off",
1832
+ }],
1833
+ };
1834
+ toolCallsExecuted += 1;
1835
+ toolErrors += 1;
1836
+ transport.appendToolResult({
1837
+ messages,
1838
+ collected: collectedResults,
1839
+ call: pending,
1840
+ toolResult: blocked,
1841
+ });
1842
+ continue;
1843
+ }
1844
+ const leaseCheck = checkWriteAllowed(executionState, {
1845
+ tool: pendingName,
1846
+ originKind: "agent_loop",
1847
+ });
1848
+ if (!leaseCheck.ok) {
1849
+ const blocked = {
1850
+ ok: false,
1851
+ status: "rejected",
1852
+ errors: [{
1853
+ code: leaseCheck.code || "WORKSPACE_WRITE_LEASE_HELD",
1854
+ message: leaseCheck.message
1855
+ || "Workspace write lease held by an active TaskRun",
1856
+ owner: leaseCheck.owner || null,
1857
+ }],
1858
+ };
1859
+ toolCallsExecuted += 1;
1860
+ toolErrors += 1;
1861
+ transport.appendToolResult({
1862
+ messages,
1863
+ collected: collectedResults,
1864
+ call: pending,
1865
+ toolResult: blocked,
1866
+ });
1867
+ continue;
1868
+ }
1869
+
1870
+ const toolCallId = pending.source && (pending.source.id || (pending.source.function && pending.source.id))
1871
+ ? String(pending.source.id || "")
1872
+ : "";
1873
+ const resumeForAsk = pendingName === "ask_user"
1874
+ ? {
1875
+ toolCallId: toolCallId || String((pending.source && pending.source.id) || `call_${randomUUID()}`),
1876
+ toolName: "ask_user",
1877
+ call: {
1878
+ name: pending.name,
1879
+ args: pending.args,
1880
+ source: pending.source,
1881
+ },
1882
+ }
1883
+ : null;
1884
+
1388
1885
  const toolResult = runCoreTool({
1389
1886
  tool: pending.name,
1390
1887
  args: pending.args,
1391
1888
  workspaceRoot,
1392
1889
  onToolEvent,
1890
+ sessionId,
1891
+ onArtifactPersisted,
1892
+ executionState,
1893
+ resume: resumeForAsk,
1393
1894
  });
1895
+ if (toolResult && toolResult.executionState) {
1896
+ executionState = toolResult.executionState;
1897
+ }
1394
1898
  toolCallsExecuted += 1;
1395
1899
  if (!toolResult || toolResult.ok === false) {
1396
1900
  toolErrors += 1;
1397
1901
  }
1902
+
1903
+ if (pendingName === "ask_user" && toolResult && toolResult.deferToolResult) {
1904
+ // Attach resume metadata onto pending interaction for contiguous tool_result later.
1905
+ const pendingInteraction = getPendingUserInteraction(executionState);
1906
+ if (pendingInteraction) {
1907
+ pendingInteraction.resume = {
1908
+ ...(pendingInteraction.resume || {}),
1909
+ ...(resumeForAsk || {}),
1910
+ mode: "ask_user",
1911
+ transport: provider === "anthropic" ? "anthropic-messages" : "openai-chat",
1912
+ };
1913
+ }
1914
+ deferredAskUser = { call: pending, interactionId: toolResult.interactionId || "" };
1915
+ continue;
1916
+ }
1917
+
1398
1918
  enforceNativeToolBudget({
1399
1919
  toolCallsExecuted,
1400
1920
  toolErrors,
@@ -1414,23 +1934,79 @@ async function runNativeLoop({
1414
1934
  if (typeof transport.flushToolResults === "function") {
1415
1935
  transport.flushToolResults({ messages, collected: collectedResults });
1416
1936
  }
1937
+
1938
+ if (deferredAskUser) {
1939
+ return {
1940
+ text: aggregated,
1941
+ streamed,
1942
+ toolCallsExecuted,
1943
+ messages,
1944
+ usage,
1945
+ executionState,
1946
+ waitingUserInteraction: true,
1947
+ interactionId: deferredAskUser.interactionId || "",
1948
+ };
1949
+ }
1950
+
1951
+ if (hasPendingUserInteraction(executionState)) {
1952
+ // Checkpoint approval synced from plan_graph — pause for TUI.
1953
+ return {
1954
+ text: aggregated,
1955
+ streamed,
1956
+ toolCallsExecuted,
1957
+ messages,
1958
+ usage,
1959
+ executionState,
1960
+ waitingUserInteraction: true,
1961
+ interactionId: (getPendingUserInteraction(executionState) || {}).id || "",
1962
+ };
1963
+ }
1417
1964
  }
1418
1965
  }
1419
1966
 
1967
+ function appendAnswerToolResult(messages = [], resume = null, answer = {}) {
1968
+ const call = resume && resume.call ? resume.call : null;
1969
+ if (!call || !call.source) return { ok: false, error: "missing deferred tool call" };
1970
+ const transportName = String(resume.transport || "openai-chat");
1971
+ const content = clipText(toJsonString(answer), 12000);
1972
+ if (transportName === "anthropic-messages") {
1973
+ messages.push({
1974
+ role: "user",
1975
+ content: [{
1976
+ type: "tool_result",
1977
+ tool_use_id: String(call.source.id || resume.toolCallId || ""),
1978
+ content,
1979
+ is_error: false,
1980
+ }],
1981
+ });
1982
+ } else {
1983
+ messages.push({
1984
+ role: "tool",
1985
+ tool_call_id: String(call.source.id || resume.toolCallId || ""),
1986
+ content,
1987
+ });
1988
+ }
1989
+ return { ok: true };
1990
+ }
1991
+
1420
1992
  async function runNativeAgentTask({
1421
1993
  workspaceRoot = process.cwd(),
1422
1994
  prompt = "",
1423
1995
  systemPrompt = "",
1996
+ systemBlocks = null,
1424
1997
  provider = "",
1425
1998
  model = "",
1426
1999
  messages = [],
1427
2000
  sessionId = "",
1428
- timeoutMs = 300000,
2001
+ timeoutMs = DEFAULT_NATIVE_TIMEOUT_MS,
1429
2002
  onStreamDelta = null,
1430
2003
  onThinkingDelta = null,
1431
2004
  onPhase = null,
1432
2005
  onToolEvent = null,
2006
+ onArtifactPersisted = null,
1433
2007
  signal = null,
2008
+ executionState = null,
2009
+ resume = false,
1434
2010
  } = {}) {
1435
2011
  const guards = createGuards({ signal, timeoutMs });
1436
2012
  const nextSessionId = String(sessionId || "").trim() || `native-${randomUUID()}`;
@@ -1450,7 +2026,7 @@ async function runNativeAgentTask({
1450
2026
  try {
1451
2027
  guards.ensureActive();
1452
2028
 
1453
- if (!promptText) {
2029
+ if (!resume && !promptText) {
1454
2030
  return {
1455
2031
  ok: false,
1456
2032
  error: "empty task",
@@ -1488,8 +2064,9 @@ async function runNativeAgentTask({
1488
2064
  const runResult = await runNativeLoop({
1489
2065
  transport,
1490
2066
  workspaceRoot,
1491
- prompt: promptText,
2067
+ prompt: resume ? "" : promptText,
1492
2068
  systemPrompt,
2069
+ systemBlocks,
1493
2070
  historyMessages: messages,
1494
2071
  model: runtime.model,
1495
2072
  baseUrl: runtime.baseUrl,
@@ -1500,8 +2077,12 @@ async function runNativeAgentTask({
1500
2077
  onThinkingDelta,
1501
2078
  onPhase,
1502
2079
  onToolEvent,
2080
+ onArtifactPersisted,
2081
+ sessionId: nextSessionId,
1503
2082
  signal,
1504
2083
  guards,
2084
+ executionState,
2085
+ resume: Boolean(resume),
1505
2086
  });
1506
2087
 
1507
2088
  const outputText = String(runResult.text || "").trim() || (
@@ -1531,26 +2112,36 @@ async function runNativeAgentTask({
1531
2112
  messages: cloneMessageList(runResult.messages),
1532
2113
  sessionId: nextSessionId,
1533
2114
  usage,
2115
+ executionState: runResult.executionState || executionState || null,
1534
2116
  // The loop marks streamed=true whenever it receives a stream callback;
1535
2117
  // only report it when the caller actually registered one.
1536
2118
  streamed: Boolean(runResult.streamed) && typeof onStreamDelta === "function",
2119
+ waitingUserInteraction: Boolean(runResult.waitingUserInteraction),
2120
+ interactionId: runResult.interactionId || "",
1537
2121
  };
1538
2122
  } catch (err) {
1539
2123
  const message = err && err.message ? err.message : "native runner failed";
2124
+ if (executionState && typeof executionState === "object") {
2125
+ clearUserPrompts(executionState);
2126
+ }
1540
2127
  return {
1541
2128
  ok: false,
1542
2129
  error: message,
1543
2130
  output: partialOutput.trim(),
1544
2131
  sessionId: nextSessionId,
1545
2132
  streamed: false,
2133
+ executionState: executionState || null,
1546
2134
  };
1547
2135
  }
1548
2136
  }
1549
2137
 
1550
2138
  module.exports = {
1551
2139
  runNativeAgentTask,
2140
+ appendAnswerToolResult,
1552
2141
  resolveRuntimeConfig,
1553
2142
  resolveCompletionUrl,
1554
2143
  resolveAnthropicMessagesUrl,
1555
2144
  resolveTransport,
2145
+ buildCoreToolSpecs,
2146
+ buildAnthropicToolSpecs,
1556
2147
  };