u-foo 2.5.15 → 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 (40) hide show
  1. package/package.json +1 -1
  2. package/src/code/agent.js +333 -243
  3. package/src/code/commands.js +16 -0
  4. package/src/code/context/assembler.js +18 -13
  5. package/src/code/context/executionSegment.js +97 -119
  6. package/src/code/context/index.js +11 -1
  7. package/src/code/context/planGraph.js +1410 -0
  8. package/src/code/context/planGraphService.js +857 -0
  9. package/src/code/context/planMode.js +398 -0
  10. package/src/code/context/planProjection.js +432 -0
  11. package/src/code/context/promptLayers.js +21 -5
  12. package/src/code/context/stateCommit.js +2 -0
  13. package/src/code/context/toolRuntime.js +172 -0
  14. package/src/code/context/userInteraction.js +457 -0
  15. package/src/code/context/userNudge.js +116 -0
  16. package/src/code/dispatch.js +17 -1
  17. package/src/code/index.js +2 -0
  18. package/src/code/nativeRunner.js +518 -37
  19. package/src/code/repl.js +160 -18
  20. package/src/code/runtime/agentWakeup.js +58 -0
  21. package/src/code/runtime/graphOwner.js +41 -0
  22. package/src/code/runtime/graphYieldRouter.js +42 -0
  23. package/src/code/runtime/index.js +15 -0
  24. package/src/code/runtime/loopMailbox.js +124 -0
  25. package/src/code/runtime/runtimeEvents.js +39 -0
  26. package/src/code/runtime/taskControl.js +565 -0
  27. package/src/code/runtime/taskFocus.js +165 -0
  28. package/src/code/runtime/taskLoop.js +383 -0
  29. package/src/code/runtime/taskRun.js +187 -0
  30. package/src/code/runtime/toolProvenance.js +70 -0
  31. package/src/code/runtime/workspaceLease.js +208 -0
  32. package/src/code/sessionStore.js +0 -10
  33. package/src/code/skills/injection.js +1 -0
  34. package/src/code/taskDecomposer.js +32 -8
  35. package/src/code/tools/askUser.js +11 -0
  36. package/src/code/tools/planGraph.js +29 -0
  37. package/src/ui/format/index.js +25 -1
  38. package/src/ui/format/markdownRenderer.js +224 -2
  39. package/src/ui/ink/UcodeApp.js +285 -22
  40. package/src/code/context/featureFlag.js +0 -13
@@ -6,7 +6,6 @@ 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
9
  const {
11
10
  persistToolResultToContext,
12
11
  sanitizeModelMessages,
@@ -14,18 +13,43 @@ const {
14
13
  const { systemBlocksToAnthropicPayload } = require("./context/promptLayers");
15
14
  const { parseStructuredSideEffects } = require("./context/stateCommit");
16
15
  const {
17
- parseExecutionSegment,
18
- executeExecutionSegment,
19
- formatSegmentResultMessage,
20
16
  emptyExecutionState,
21
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");
22
37
  const { stableStringify } = require("./context/stableJson");
23
38
  const { getReadToolDescription } = require("../agents/prompts/native/toolDescriptions/read");
24
39
  const { getWriteToolDescription } = require("../agents/prompts/native/toolDescriptions/write");
25
40
  const { getEditToolDescription } = require("../agents/prompts/native/toolDescriptions/edit");
26
41
  const { getBashToolDescription } = require("../agents/prompts/native/toolDescriptions/bash");
27
42
 
28
- const CORE_TOOL_NAMES = new Set(["read", "write", "edit", "bash", "artifact_read"]);
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"]);
29
53
  const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
30
54
  const DEFAULT_ANTHROPIC_BASE_URL = "https://api.anthropic.com/v1";
31
55
  const DEFAULT_KIMI_BASE_URL = "https://api.kimi.com/coding/v1";
@@ -184,7 +208,9 @@ function createGuards({ signal = null, timeoutMs = DEFAULT_NATIVE_TIMEOUT_MS } =
184
208
  function emitToolEvent(callback, event = {}) {
185
209
  if (typeof callback !== "function") return;
186
210
  try {
187
- callback(event);
211
+ const payload = event && typeof event === "object" ? { ...event } : {};
212
+ if (payload.origin == null) delete payload.origin;
213
+ callback(payload);
188
214
  } catch {
189
215
  // ignore callback failures
190
216
  }
@@ -383,7 +409,11 @@ function buildCoreToolSpecs() {
383
409
  type: "function",
384
410
  function: {
385
411
  name: "artifact_read",
386
- description: "Load a stored artifact by artifactId. Use selectors startLine/endLine, maxChars, or tailLines to read a slice.",
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(" "),
387
417
  parameters: {
388
418
  type: "object",
389
419
  properties: {
@@ -398,6 +428,128 @@ function buildCoreToolSpecs() {
398
428
  },
399
429
  },
400
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
+ },
401
553
  ];
402
554
  }
403
555
 
@@ -526,8 +678,10 @@ function runCoreTool({
526
678
  workspaceRoot = process.cwd(),
527
679
  onToolEvent = null,
528
680
  sessionId = "",
529
- contextV2 = false,
530
681
  onArtifactPersisted = null,
682
+ executionState = null,
683
+ origin = null,
684
+ resume = null,
531
685
  } = {}) {
532
686
  const normalizedTool = normalizeToolName(tool);
533
687
  if (!normalizedTool) {
@@ -536,6 +690,7 @@ function runCoreTool({
536
690
  phase: "error",
537
691
  args: args && typeof args === "object" ? { ...args } : {},
538
692
  error: `unsupported tool: ${tool}`,
693
+ origin,
539
694
  });
540
695
  return {
541
696
  ok: false,
@@ -552,8 +707,86 @@ function runCoreTool({
552
707
  phase: "start",
553
708
  args: safeArgs,
554
709
  error: "",
710
+ origin,
555
711
  });
556
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
+
557
790
  const toolOptions = { workspaceRoot, cwd: workspaceRoot };
558
791
  if (normalizedTool === "artifact_read" && sessionId) {
559
792
  toolOptions.sessionId = sessionId;
@@ -569,12 +802,12 @@ function runCoreTool({
569
802
  phase: "error",
570
803
  args: safeArgs,
571
804
  error: String((result && result.error) || `${normalizedTool} failed`),
805
+ origin,
572
806
  });
573
807
  return result;
574
808
  }
575
809
 
576
- const useContextV2 = contextV2 || isContextV2Enabled();
577
- if (useContextV2 && normalizedTool !== "artifact_read") {
810
+ if (normalizedTool !== "artifact_read" && EXECUTABLE_GRAPH_TOOLS.has(normalizedTool)) {
578
811
  const persisted = persistToolResultToContext({
579
812
  workspaceRoot,
580
813
  sessionId,
@@ -589,9 +822,14 @@ function runCoreTool({
589
822
  // ignore
590
823
  }
591
824
  }
592
- return persisted.modelPayload || result;
825
+ const payload = persisted.modelPayload || result;
826
+ if (origin) payload.origin = origin;
827
+ return payload;
593
828
  }
594
829
 
830
+ if (origin && result && typeof result === "object") {
831
+ return { ...result, origin };
832
+ }
595
833
  return result;
596
834
  }
597
835
 
@@ -1375,9 +1613,10 @@ async function runNativeLoop({
1375
1613
  onToolEvent = null,
1376
1614
  onArtifactPersisted = null,
1377
1615
  sessionId = "",
1378
- contextV2 = false,
1379
1616
  signal = null,
1380
1617
  guards,
1618
+ executionState: initialExecutionState = null,
1619
+ resume = false,
1381
1620
  } = {}) {
1382
1621
  const requestModel = String(model || "").trim();
1383
1622
  if (!requestModel) {
@@ -1390,19 +1629,38 @@ async function runNativeLoop({
1390
1629
  }
1391
1630
 
1392
1631
  const messages = sanitizeModelMessages(cloneMessageList(historyMessages));
1393
- transport.prepareMessages({ messages, systemPrompt, prompt });
1632
+ if (!resume) {
1633
+ transport.prepareMessages({ messages, systemPrompt, prompt });
1634
+ }
1394
1635
 
1395
1636
  let aggregated = "";
1396
1637
  let streamed = false;
1397
1638
  let toolCallsExecuted = 0;
1398
1639
  let toolErrors = 0;
1399
- let executionState = emptyExecutionState();
1640
+ let executionState = initialExecutionState && typeof initialExecutionState === "object"
1641
+ ? initialExecutionState
1642
+ : emptyExecutionState();
1643
+ if (typeof executionState.planMode !== "boolean") executionState.planMode = false;
1644
+ ensurePendingUserPrompts(executionState);
1400
1645
  const toolBudget = resolveNativeToolBudget();
1401
1646
  const usage = createUsageTotals();
1402
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
+
1403
1659
  while (true) {
1404
1660
  guards.ensureActive();
1405
1661
 
1662
+ injectPendingUserReminders();
1663
+
1406
1664
  const turnResult = await transport.runTurn({
1407
1665
  url: requestUrl,
1408
1666
  apiKey,
@@ -1433,31 +1691,33 @@ async function runNativeLoop({
1433
1691
 
1434
1692
  if (toolCalls.length === 0) {
1435
1693
  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) {
1694
+ const sideEffects = parseStructuredSideEffects(text);
1695
+ const planCommand = sideEffects ? normalizePlanGraphCommand(sideEffects) : null;
1696
+ if (planCommand) {
1440
1697
  transport.appendFinalAssistantMessage({ messages, turnResult });
1441
- const exec = executeExecutionSegment({
1442
- segment,
1698
+ const planResult = runCoreTool({
1699
+ tool: "plan_graph",
1700
+ args: planCommand,
1701
+ workspaceRoot,
1702
+ onToolEvent,
1703
+ sessionId,
1704
+ onArtifactPersisted,
1443
1705
  executionState,
1444
- runStep: ({ tool, args }) => runCoreTool({
1445
- tool,
1446
- args,
1447
- workspaceRoot,
1448
- onToolEvent,
1449
- sessionId,
1450
- contextV2,
1451
- onArtifactPersisted,
1452
- }),
1706
+ origin: { kind: "legacy_side_effect", source: planCommand.source || "legacy" },
1453
1707
  });
1454
- executionState = exec.executionState;
1708
+ if (planResult && planResult.executionState) {
1709
+ executionState = planResult.executionState;
1710
+ }
1455
1711
  messages.push({
1456
1712
  role: "user",
1457
- content: formatSegmentResultMessage(exec),
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
+ }),
1458
1719
  });
1459
1720
  continue;
1460
- }
1461
1721
  }
1462
1722
  transport.appendFinalAssistantMessage({ messages, turnResult });
1463
1723
  if (!aggregated.trim() && text) {
@@ -1469,6 +1729,7 @@ async function runNativeLoop({
1469
1729
  toolCallsExecuted,
1470
1730
  messages,
1471
1731
  usage,
1732
+ executionState,
1472
1733
  };
1473
1734
  }
1474
1735
 
@@ -1480,24 +1741,180 @@ async function runNativeLoop({
1480
1741
  toolCallsExecuted,
1481
1742
  messages,
1482
1743
  usage,
1744
+ executionState,
1483
1745
  };
1484
1746
  }
1485
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
+
1486
1799
  const collectedResults = [];
1800
+ let deferredAskUser = null;
1487
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
+
1488
1885
  const toolResult = runCoreTool({
1489
1886
  tool: pending.name,
1490
1887
  args: pending.args,
1491
1888
  workspaceRoot,
1492
1889
  onToolEvent,
1493
1890
  sessionId,
1494
- contextV2,
1495
1891
  onArtifactPersisted,
1892
+ executionState,
1893
+ resume: resumeForAsk,
1496
1894
  });
1895
+ if (toolResult && toolResult.executionState) {
1896
+ executionState = toolResult.executionState;
1897
+ }
1497
1898
  toolCallsExecuted += 1;
1498
1899
  if (!toolResult || toolResult.ok === false) {
1499
1900
  toolErrors += 1;
1500
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
+
1501
1918
  enforceNativeToolBudget({
1502
1919
  toolCallsExecuted,
1503
1920
  toolErrors,
@@ -1517,9 +1934,61 @@ async function runNativeLoop({
1517
1934
  if (typeof transport.flushToolResults === "function") {
1518
1935
  transport.flushToolResults({ messages, collected: collectedResults });
1519
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
+ }
1520
1964
  }
1521
1965
  }
1522
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
+
1523
1992
  async function runNativeAgentTask({
1524
1993
  workspaceRoot = process.cwd(),
1525
1994
  prompt = "",
@@ -1535,8 +2004,9 @@ async function runNativeAgentTask({
1535
2004
  onPhase = null,
1536
2005
  onToolEvent = null,
1537
2006
  onArtifactPersisted = null,
1538
- contextV2 = false,
1539
2007
  signal = null,
2008
+ executionState = null,
2009
+ resume = false,
1540
2010
  } = {}) {
1541
2011
  const guards = createGuards({ signal, timeoutMs });
1542
2012
  const nextSessionId = String(sessionId || "").trim() || `native-${randomUUID()}`;
@@ -1556,7 +2026,7 @@ async function runNativeAgentTask({
1556
2026
  try {
1557
2027
  guards.ensureActive();
1558
2028
 
1559
- if (!promptText) {
2029
+ if (!resume && !promptText) {
1560
2030
  return {
1561
2031
  ok: false,
1562
2032
  error: "empty task",
@@ -1594,7 +2064,7 @@ async function runNativeAgentTask({
1594
2064
  const runResult = await runNativeLoop({
1595
2065
  transport,
1596
2066
  workspaceRoot,
1597
- prompt: promptText,
2067
+ prompt: resume ? "" : promptText,
1598
2068
  systemPrompt,
1599
2069
  systemBlocks,
1600
2070
  historyMessages: messages,
@@ -1609,9 +2079,10 @@ async function runNativeAgentTask({
1609
2079
  onToolEvent,
1610
2080
  onArtifactPersisted,
1611
2081
  sessionId: nextSessionId,
1612
- contextV2: contextV2 || isContextV2Enabled(),
1613
2082
  signal,
1614
2083
  guards,
2084
+ executionState,
2085
+ resume: Boolean(resume),
1615
2086
  });
1616
2087
 
1617
2088
  const outputText = String(runResult.text || "").trim() || (
@@ -1641,26 +2112,36 @@ async function runNativeAgentTask({
1641
2112
  messages: cloneMessageList(runResult.messages),
1642
2113
  sessionId: nextSessionId,
1643
2114
  usage,
2115
+ executionState: runResult.executionState || executionState || null,
1644
2116
  // The loop marks streamed=true whenever it receives a stream callback;
1645
2117
  // only report it when the caller actually registered one.
1646
2118
  streamed: Boolean(runResult.streamed) && typeof onStreamDelta === "function",
2119
+ waitingUserInteraction: Boolean(runResult.waitingUserInteraction),
2120
+ interactionId: runResult.interactionId || "",
1647
2121
  };
1648
2122
  } catch (err) {
1649
2123
  const message = err && err.message ? err.message : "native runner failed";
2124
+ if (executionState && typeof executionState === "object") {
2125
+ clearUserPrompts(executionState);
2126
+ }
1650
2127
  return {
1651
2128
  ok: false,
1652
2129
  error: message,
1653
2130
  output: partialOutput.trim(),
1654
2131
  sessionId: nextSessionId,
1655
2132
  streamed: false,
2133
+ executionState: executionState || null,
1656
2134
  };
1657
2135
  }
1658
2136
  }
1659
2137
 
1660
2138
  module.exports = {
1661
2139
  runNativeAgentTask,
2140
+ appendAnswerToolResult,
1662
2141
  resolveRuntimeConfig,
1663
2142
  resolveCompletionUrl,
1664
2143
  resolveAnthropicMessagesUrl,
1665
2144
  resolveTransport,
2145
+ buildCoreToolSpecs,
2146
+ buildAnthropicToolSpecs,
1666
2147
  };