u-foo 2.5.15 → 3.0.1

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 (56) hide show
  1. package/package.json +1 -1
  2. package/src/code/agent.js +350 -246
  3. package/src/code/commands.js +16 -0
  4. package/src/code/context/assembler.js +18 -13
  5. package/src/code/context/executionSegment.js +102 -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 +405 -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 +4 -0
  18. package/src/code/nativeRunner.js +589 -172
  19. package/src/code/protocol/controlPlane.js +93 -0
  20. package/src/code/protocol/faultHarness.js +90 -0
  21. package/src/code/protocol/index.js +20 -0
  22. package/src/code/protocol/loopEvents.js +102 -0
  23. package/src/code/protocol/materialize.js +107 -0
  24. package/src/code/protocol/messageFixtures.js +116 -0
  25. package/src/code/protocol/ownership.js +147 -0
  26. package/src/code/protocol/protocolValidator.js +165 -0
  27. package/src/code/protocol/suspension.js +173 -0
  28. package/src/code/protocol/toolCallLedger.js +222 -0
  29. package/src/code/protocol/transitions.js +97 -0
  30. package/src/code/providers/anthropicMessagesTransport.js +93 -0
  31. package/src/code/providers/index.js +7 -0
  32. package/src/code/providers/openaiChatTransport.js +98 -0
  33. package/src/code/providers/transportContract.js +46 -0
  34. package/src/code/repl.js +147 -18
  35. package/src/code/runtime/agentWakeup.js +58 -0
  36. package/src/code/runtime/graphOwner.js +41 -0
  37. package/src/code/runtime/graphYieldRouter.js +42 -0
  38. package/src/code/runtime/index.js +15 -0
  39. package/src/code/runtime/loopMailbox.js +124 -0
  40. package/src/code/runtime/runtimeEvents.js +39 -0
  41. package/src/code/runtime/taskControl.js +565 -0
  42. package/src/code/runtime/taskFocus.js +165 -0
  43. package/src/code/runtime/taskLoop.js +394 -0
  44. package/src/code/runtime/taskRun.js +348 -0
  45. package/src/code/runtime/toolProvenance.js +70 -0
  46. package/src/code/runtime/workspaceLease.js +249 -0
  47. package/src/code/sessionStore.js +1 -10
  48. package/src/code/skills/injection.js +1 -0
  49. package/src/code/taskDecomposer.js +32 -8
  50. package/src/code/taskRoute.js +73 -0
  51. package/src/code/tools/askUser.js +11 -0
  52. package/src/code/tools/planGraph.js +29 -0
  53. package/src/ui/format/index.js +25 -1
  54. package/src/ui/format/markdownRenderer.js +224 -2
  55. package/src/ui/ink/UcodeApp.js +268 -22
  56. package/src/code/context/featureFlag.js +0 -13
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+
3
+ const { assertTransport } = require("./transportContract");
4
+
5
+ /**
6
+ * Anthropic Messages API transport adapter.
7
+ * @param {{
8
+ * resolveUrl: Function,
9
+ * runTurn: Function,
10
+ * toJsonString: Function,
11
+ * clipText: Function,
12
+ * }} deps
13
+ */
14
+ function createAnthropicMessagesTransport(deps = {}) {
15
+ const {
16
+ resolveUrl,
17
+ runTurn,
18
+ toJsonString,
19
+ clipText,
20
+ } = deps;
21
+
22
+ const transport = {
23
+ name: "anthropic-messages",
24
+ resolveUrl,
25
+ prepareMessages({ messages, prompt }) {
26
+ messages.push({
27
+ role: "user",
28
+ content: String(prompt || ""),
29
+ });
30
+ },
31
+ runTurn,
32
+ getToolCalls(turnResult) {
33
+ return Array.isArray(turnResult.toolCalls) ? turnResult.toolCalls : [];
34
+ },
35
+ appendFinalAssistantMessage({ messages, turnResult }) {
36
+ const assistantContent = Array.isArray(turnResult.assistantContent)
37
+ ? turnResult.assistantContent
38
+ : [];
39
+ if (assistantContent.length > 0) {
40
+ messages.push({
41
+ role: "assistant",
42
+ content: assistantContent,
43
+ });
44
+ } else if (String(turnResult.text || "").trim()) {
45
+ messages.push({
46
+ role: "assistant",
47
+ content: [
48
+ {
49
+ type: "text",
50
+ text: String(turnResult.text || ""),
51
+ },
52
+ ],
53
+ });
54
+ }
55
+ },
56
+ prepareToolCalls({ messages, turnResult, toolCalls }) {
57
+ const assistantContent = Array.isArray(turnResult.assistantContent)
58
+ ? turnResult.assistantContent
59
+ : [];
60
+
61
+ messages.push({
62
+ role: "assistant",
63
+ content: assistantContent,
64
+ });
65
+
66
+ return toolCalls.map((call) => ({
67
+ name: call.name,
68
+ args: call.args,
69
+ source: call,
70
+ }));
71
+ },
72
+ appendToolResult({ collected, call, toolResult }) {
73
+ collected.push({
74
+ type: "tool_result",
75
+ tool_use_id: String(call.source.id || ""),
76
+ content: clipText(toJsonString(toolResult), 12000),
77
+ is_error: Boolean(!toolResult || toolResult.ok === false),
78
+ });
79
+ },
80
+ flushToolResults({ messages, collected }) {
81
+ messages.push({
82
+ role: "user",
83
+ content: collected,
84
+ });
85
+ },
86
+ };
87
+
88
+ return assertTransport(transport, "anthropic-messages");
89
+ }
90
+
91
+ module.exports = {
92
+ createAnthropicMessagesTransport,
93
+ };
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+
3
+ module.exports = {
4
+ ...require("./transportContract"),
5
+ ...require("./openaiChatTransport"),
6
+ ...require("./anthropicMessagesTransport"),
7
+ };
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+
3
+ const { randomUUID } = require("crypto");
4
+ const { assertTransport } = require("./transportContract");
5
+
6
+ /**
7
+ * OpenAI-compatible chat-completions transport adapter.
8
+ * @param {{
9
+ * resolveUrl: Function,
10
+ * runTurn: Function,
11
+ * normalizeToolName: Function,
12
+ * normalizeToolCallArgs: Function,
13
+ * toJsonString: Function,
14
+ * clipText: Function,
15
+ * }} deps
16
+ */
17
+ function createOpenAiChatTransport(deps = {}) {
18
+ const {
19
+ resolveUrl,
20
+ runTurn,
21
+ normalizeToolName,
22
+ normalizeToolCallArgs,
23
+ toJsonString,
24
+ clipText,
25
+ } = deps;
26
+
27
+ const transport = {
28
+ name: "openai-chat",
29
+ resolveUrl,
30
+ prepareMessages({ messages, systemPrompt, prompt }) {
31
+ const systemText = String(systemPrompt || "").trim();
32
+ const hasSystem = messages.some((entry) => String(entry.role || "").trim() === "system");
33
+ if (systemText && !hasSystem) {
34
+ messages.unshift({ role: "system", content: systemText });
35
+ }
36
+ messages.push({ role: "user", content: String(prompt || "") });
37
+ },
38
+ runTurn,
39
+ getToolCalls(turnResult) {
40
+ return Array.isArray(turnResult.toolCalls)
41
+ ? turnResult.toolCalls.filter((call) => call && call.function && typeof call.function === "object")
42
+ : [];
43
+ },
44
+ appendFinalAssistantMessage({ messages, turnResult }) {
45
+ const text = String(turnResult.text || "").trim();
46
+ if (text) {
47
+ messages.push({
48
+ role: "assistant",
49
+ content: text,
50
+ });
51
+ }
52
+ },
53
+ prepareToolCalls({ messages, toolCalls }) {
54
+ const assistantToolCalls = [];
55
+ for (const call of toolCalls) {
56
+ const callId = String(call.id || `call_${randomUUID()}`);
57
+ const name = normalizeToolName(call.function.name || "");
58
+ const args = normalizeToolCallArgs(call.function.arguments || "");
59
+
60
+ assistantToolCalls.push({
61
+ id: callId,
62
+ type: "function",
63
+ function: {
64
+ name: name || String(call.function.name || ""),
65
+ arguments: toJsonString(args),
66
+ },
67
+ });
68
+ }
69
+
70
+ if (assistantToolCalls.length === 0) return null;
71
+
72
+ messages.push({
73
+ role: "assistant",
74
+ content: null,
75
+ tool_calls: assistantToolCalls,
76
+ });
77
+
78
+ return assistantToolCalls.map((toolCall) => ({
79
+ name: toolCall.function.name,
80
+ args: normalizeToolCallArgs(toolCall.function.arguments),
81
+ source: toolCall,
82
+ }));
83
+ },
84
+ appendToolResult({ messages, call, toolResult }) {
85
+ messages.push({
86
+ role: "tool",
87
+ tool_call_id: call.source.id,
88
+ content: clipText(toJsonString(toolResult), 12000),
89
+ });
90
+ },
91
+ };
92
+
93
+ return assertTransport(transport, "openai-chat");
94
+ }
95
+
96
+ module.exports = {
97
+ createOpenAiChatTransport,
98
+ };
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Transport contract for native Agent Loop Provider adapters.
5
+ *
6
+ * Transports own wire-format conversion and turn execution only.
7
+ * They must not decide Plan Mode, write leases, or tool batch policy.
8
+ *
9
+ * Required methods:
10
+ * - resolveUrl(baseUrl) → string
11
+ * - prepareMessages({ messages, systemPrompt?, prompt })
12
+ * - runTurn(params) → Promise<turnResult>
13
+ * - getToolCalls(turnResult) → array
14
+ * - appendFinalAssistantMessage({ messages, turnResult })
15
+ * - prepareToolCalls({ messages, turnResult?, toolCalls }) → pendingCalls|null
16
+ * - appendToolResult({ messages?, collected?, call, toolResult })
17
+ * - flushToolResults?({ messages, collected }) // Anthropic-style batch
18
+ */
19
+
20
+ const TRANSPORT_NAMES = Object.freeze(["openai-chat", "anthropic-messages"]);
21
+
22
+ function assertTransport(transport = null, name = "") {
23
+ if (!transport || typeof transport !== "object") {
24
+ throw new Error(`missing transport${name ? `: ${name}` : ""}`);
25
+ }
26
+ const required = [
27
+ "resolveUrl",
28
+ "prepareMessages",
29
+ "runTurn",
30
+ "getToolCalls",
31
+ "appendFinalAssistantMessage",
32
+ "prepareToolCalls",
33
+ "appendToolResult",
34
+ ];
35
+ for (const key of required) {
36
+ if (typeof transport[key] !== "function") {
37
+ throw new Error(`transport ${name || "?"} missing ${key}`);
38
+ }
39
+ }
40
+ return transport;
41
+ }
42
+
43
+ module.exports = {
44
+ TRANSPORT_NAMES,
45
+ assertTransport,
46
+ };
package/src/code/repl.js CHANGED
@@ -24,17 +24,24 @@ const {
24
24
  const { summarizeSessionUsage, formatSessionUsageStatus } = require("./usageStore");
25
25
  const { listUcodeCommandsForHelp } = require("./commands");
26
26
  const { applyUcodeModelCommand, suggestUcodeModels } = require("./modelCommand");
27
+ const { applyUcodePlanCommand } = require("./context/planMode");
27
28
 
28
29
  function printPrompt(stdout = process.stdout) {
29
30
  stdout.write("> ");
30
31
  }
31
32
 
32
- function printUcodeBanner(stdout = process.stdout, { model = "", workspaceRoot = process.cwd(), sessionId = "" } = {}) {
33
+ function printUcodeBanner(stdout = process.stdout, {
34
+ model = "",
35
+ workspaceRoot = process.cwd(),
36
+ sessionId = "",
37
+ planMode = false,
38
+ } = {}) {
33
39
  stdout.write(`${buildUcodeBannerLines({
34
40
  model,
35
41
  engine: "ufoo-core",
36
42
  workspaceRoot,
37
43
  sessionId,
44
+ planMode,
38
45
  width: (stdout && stdout.columns) || 0,
39
46
  }).join("\n")}\n`);
40
47
  }
@@ -123,6 +130,22 @@ function runSingleCommand(line = "", workspaceRoot = process.cwd()) {
123
130
  model: nextModel,
124
131
  };
125
132
  }
133
+ const planMatch = text.match(/^(?:\/plan|plan)(?:\s+(.*))?$/i);
134
+ if (planMatch) {
135
+ const arg = String(planMatch[1] || "").trim().toLowerCase();
136
+ if (!arg || arg === "show" || arg === "status") return { kind: "plan", action: "show" };
137
+ if (arg === "on" || arg === "enable") return { kind: "plan", action: "on" };
138
+ if (arg === "off" || arg === "disable") return { kind: "plan", action: "off" };
139
+ if (arg === "clear") return { kind: "plan", action: "clear" };
140
+ if (arg === "hide") return { kind: "plan", action: "hide" };
141
+ if (arg === "focus") return { kind: "plan", action: "focus" };
142
+ if (arg === "debug") return { kind: "plan", action: "debug" };
143
+ if (arg === "toggle") return { kind: "plan", action: "toggle" };
144
+ return {
145
+ kind: "error",
146
+ output: "usage: /plan [on|off|show|hide|focus|debug|clear]",
147
+ };
148
+ }
126
149
  const skillsMatch = text.match(/^(?:\/skills|skills)(?:\s+(.*))?$/i);
127
150
  if (skillsMatch) {
128
151
  const args = String(skillsMatch[1] || "").trim().split(/\s+/).filter(Boolean);
@@ -259,6 +282,8 @@ async function runUcodeCoreAgent({
259
282
  resolveNlTaskTimeoutMs,
260
283
  resolveUcodeProviderModel,
261
284
  runNaturalLanguageTask,
285
+ resumeAfterUserInteraction,
286
+ submitUserInteractionAnswer,
262
287
  } = require("./agent");
263
288
  const resolvedWorkspaceRoot = resolveUfooProjectRoot(workspaceRoot);
264
289
  const resolvedUcode = resolveUcodeProviderModel({
@@ -315,6 +340,7 @@ async function runUcodeCoreAgent({
315
340
  model: state.model || "default",
316
341
  workspaceRoot: workspaceRoot,
317
342
  sessionId: state.sessionId,
343
+ planMode: Boolean(state.executionState && state.executionState.planMode),
318
344
  });
319
345
  printPrompt(stdout);
320
346
  const rl = readline.createInterface({
@@ -333,6 +359,7 @@ async function runUcodeCoreAgent({
333
359
  let autoBusQueued = false;
334
360
  let autoBusError = "";
335
361
  let closing = false;
362
+ let taskInFlight = false;
336
363
 
337
364
  const runAutoBusOnce = async () => {
338
365
  if (!autoBusEnabled || closing) return;
@@ -433,6 +460,10 @@ async function runUcodeCoreAgent({
433
460
  sessionId: state.sessionId,
434
461
  });
435
462
  stdout.write(`${formatSessionUsageStatus(usageSummary)}\n`);
463
+ const { formatPlanModeStatus } = require("./context/planMode");
464
+ if (state.executionState) {
465
+ stdout.write(`${formatPlanModeStatus(state.executionState).split("\n").slice(0, 5).join("\n")}\n`);
466
+ }
436
467
  }
437
468
  if (result.kind === "model") {
438
469
  const applied = applyUcodeModelCommand(state, result);
@@ -441,6 +472,11 @@ async function runUcodeCoreAgent({
441
472
  persistSessionState(state);
442
473
  }
443
474
  }
475
+ if (result.kind === "plan") {
476
+ const applied = applyUcodePlanCommand(state, result);
477
+ stdout.write(`${applied.output}\n`);
478
+ if (applied.ok) persistSessionState(state);
479
+ }
444
480
  if (result.kind === "ubus") {
445
481
  const ubusResult = await runUbusCommand(state, {
446
482
  workspaceRoot: runtimeWorkspace,
@@ -488,23 +524,29 @@ async function runUcodeCoreAgent({
488
524
  });
489
525
  }
490
526
 
491
- const nlResult = await runNaturalLanguageTask(result.task, state, {
492
- onDelta: state.jsonOutput
493
- ? null
494
- : async (delta) => {
495
- const text = escapeStripper.write(String(delta || ""));
496
- const safeText = stripBlessedTags(stripLeakedEscapeTags(text));
497
- if (!safeText) return;
498
- if (/[^\s]/.test(safeText)) {
499
- streamedVisible = true;
500
- }
501
- if (streamBuffer) {
502
- await streamBuffer.write(safeText);
503
- } else {
504
- stdout.write(safeText);
505
- }
506
- },
507
- });
527
+ taskInFlight = true;
528
+ let nlResult;
529
+ try {
530
+ nlResult = await runNaturalLanguageTask(result.task, state, {
531
+ onDelta: state.jsonOutput
532
+ ? null
533
+ : async (delta) => {
534
+ const text = escapeStripper.write(String(delta || ""));
535
+ const safeText = stripBlessedTags(stripLeakedEscapeTags(text));
536
+ if (!safeText) return;
537
+ if (/[^\s]/.test(safeText)) {
538
+ streamedVisible = true;
539
+ }
540
+ if (streamBuffer) {
541
+ await streamBuffer.write(safeText);
542
+ } else {
543
+ stdout.write(safeText);
544
+ }
545
+ },
546
+ });
547
+ } finally {
548
+ taskInFlight = false;
549
+ }
508
550
 
509
551
  if (!state.jsonOutput) {
510
552
  const tail = escapeStripper.flush();
@@ -547,6 +589,92 @@ async function runUcodeCoreAgent({
547
589
  };
548
590
 
549
591
  rl.on("line", (line) => {
592
+ const trimmed = normalizeLine(line);
593
+
594
+ // Pending approval/choice/chat takes priority over nudge / new NL.
595
+ try {
596
+ const { hasPendingUserInteraction } = require("./context/userInteraction");
597
+ if (
598
+ trimmed
599
+ && state.executionState
600
+ && hasPendingUserInteraction(state.executionState)
601
+ ) {
602
+ chain = chain.then(async () => {
603
+ let streamBuffer = null;
604
+ let streamedVisible = false;
605
+ const escapeStripper = createEscapeTagStripper();
606
+ if (!state.jsonOutput) {
607
+ streamBuffer = new StreamBuffer(stdout.write.bind(stdout), {
608
+ delay: 10,
609
+ chunkSize: 4,
610
+ });
611
+ }
612
+ taskInFlight = true;
613
+ let resumeResult;
614
+ try {
615
+ resumeResult = await submitUserInteractionAnswer(trimmed, state, {
616
+ onDelta: state.jsonOutput
617
+ ? null
618
+ : async (delta) => {
619
+ const text = escapeStripper.write(String(delta || ""));
620
+ const safeText = stripBlessedTags(stripLeakedEscapeTags(text));
621
+ if (!safeText) return;
622
+ if (/[^\s]/.test(safeText)) {
623
+ streamedVisible = true;
624
+ }
625
+ if (streamBuffer) {
626
+ await streamBuffer.write(safeText);
627
+ } else {
628
+ stdout.write(safeText);
629
+ }
630
+ },
631
+ });
632
+ } finally {
633
+ taskInFlight = false;
634
+ }
635
+ if (streamBuffer) {
636
+ await streamBuffer.finish();
637
+ }
638
+ const streamed = !state.jsonOutput && Boolean(resumeResult && resumeResult.streamed);
639
+ if (streamed && streamedVisible && resumeResult && resumeResult.streamLastChar !== "\n") {
640
+ stdout.write("\n");
641
+ }
642
+ if (!resumeResult || resumeResult.ok === false) {
643
+ stdout.write(`Error: ${(resumeResult && resumeResult.error) || "resume failed"}\n`);
644
+ } else if (resumeResult.shouldEchoSummary && resumeResult.echoSummaryText) {
645
+ stdout.write(`${resumeResult.echoSummaryText}\n`);
646
+ } else if (resumeResult.waitingUserInteraction) {
647
+ stdout.write("Still waiting for your reply.\n");
648
+ }
649
+ const persisted = persistSessionState(state);
650
+ if (!state.jsonOutput && (!persisted || persisted.ok === false)) {
651
+ stdout.write(`Warning: failed to persist session ${state.sessionId}: ${(persisted && persisted.error) || "unknown error"}\n`);
652
+ }
653
+ printPrompt(stdout);
654
+ }).catch((err) => {
655
+ stdout.write(`${JSON.stringify({ ok: false, error: err && err.message ? err.message : "resume failed" })}\n`);
656
+ printPrompt(stdout);
657
+ });
658
+ return;
659
+ }
660
+ } catch (err) {
661
+ stdout.write(`Error: ${err && err.message ? err.message : "interaction failed"}\n`);
662
+ printPrompt(stdout);
663
+ return;
664
+ }
665
+
666
+ // Mid-task NL input becomes a pending user reminder for the next LLM turn.
667
+ if (taskInFlight && trimmed && !/^\//.test(trimmed)) {
668
+ const { enqueueUserPrompt } = require("./context/userNudge");
669
+ const { emptyExecutionState } = require("./context/executionSegment");
670
+ if (!state.executionState || typeof state.executionState !== "object") {
671
+ state.executionState = emptyExecutionState();
672
+ }
673
+ enqueueUserPrompt(state.executionState, trimmed);
674
+ stdout.write("Queued user reminder for next model turn.\n");
675
+ printPrompt(stdout);
676
+ return;
677
+ }
550
678
  chain = chain.then(() => handleLine(line)).catch((err) => {
551
679
  stdout.write(`${JSON.stringify({ ok: false, error: err && err.message ? err.message : "agent loop failed" })}\n`);
552
680
  printPrompt(stdout);
@@ -640,5 +768,6 @@ module.exports = {
640
768
  parseAgentArgs,
641
769
  formatSessionUsageStatus,
642
770
  applyUcodeModelCommand,
771
+ applyUcodePlanCommand,
643
772
  suggestUcodeModels,
644
773
  };
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Drain Agent Loop mailbox into a turnDynamic block (never as user role).
5
+ */
6
+
7
+ const { drainAgentMailbox, ensureMailbox } = require("./loopMailbox");
8
+
9
+ function formatAgentRuntimeEvents(events = []) {
10
+ const list = Array.isArray(events) ? events : [];
11
+ if (list.length === 0) return "";
12
+ const lines = ["Runtime events (Agent Loop mailbox; not user messages):"];
13
+ for (const entry of list) {
14
+ if (!entry) continue;
15
+ if (entry.kind === "user") {
16
+ lines.push(`- user_nudge: ${String(entry.text || "").slice(0, 400)}`);
17
+ continue;
18
+ }
19
+ if (entry.kind === "runtime" && entry.event) {
20
+ const ev = entry.event;
21
+ const bits = [`type=${ev.type}`];
22
+ if (ev.taskId) bits.push(`taskId=${ev.taskId}`);
23
+ if (ev.taskRunId) bits.push(`taskRunId=${ev.taskRunId}`);
24
+ if (ev.result && ev.result.summary) bits.push(`summary=${String(ev.result.summary).slice(0, 200)}`);
25
+ if (ev.error && (ev.error.message || ev.error)) {
26
+ bits.push(`error=${String(ev.error.message || ev.error).slice(0, 200)}`);
27
+ }
28
+ if (Array.isArray(ev.readyNodes) && ev.readyNodes.length) {
29
+ bits.push(`readyNodes=[${ev.readyNodes.join(",")}]`);
30
+ }
31
+ lines.push(`- ${bits.join(" ")}`);
32
+ }
33
+ }
34
+ return lines.join("\n");
35
+ }
36
+
37
+ /**
38
+ * Peek without drain — for inspect. Prefer drainForAgentTurn for consumption.
39
+ */
40
+ function peekAgentMailboxText(executionState = null) {
41
+ const state = executionState && typeof executionState === "object" ? executionState : {};
42
+ const box = ensureMailbox(state, "agentMailbox");
43
+ return formatAgentRuntimeEvents(box.queue || []);
44
+ }
45
+
46
+ function drainAgentMailboxForTurn(executionState = null) {
47
+ const events = drainAgentMailbox(executionState);
48
+ return {
49
+ events,
50
+ text: formatAgentRuntimeEvents(events),
51
+ };
52
+ }
53
+
54
+ module.exports = {
55
+ formatAgentRuntimeEvents,
56
+ peekAgentMailboxText,
57
+ drainAgentMailboxForTurn,
58
+ };
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Graph ownership: which loop consumes waiting_llm / yields.
5
+ */
6
+
7
+ function agentLoopOwner(agentLoopId = "agent") {
8
+ return {
9
+ kind: "agent_loop",
10
+ agentLoopId: String(agentLoopId || "agent").trim() || "agent",
11
+ };
12
+ }
13
+
14
+ function taskLoopOwner(taskRunId = "") {
15
+ return {
16
+ kind: "task_loop",
17
+ taskRunId: String(taskRunId || "").trim(),
18
+ };
19
+ }
20
+
21
+ function normalizeGraphOwner(source = null) {
22
+ if (!source || typeof source !== "object") return agentLoopOwner();
23
+ const kind = String(source.kind || "").trim();
24
+ if (kind === "task_loop") {
25
+ const taskRunId = String(source.taskRunId || "").trim();
26
+ if (!taskRunId) return agentLoopOwner();
27
+ return taskLoopOwner(taskRunId);
28
+ }
29
+ return agentLoopOwner(source.agentLoopId);
30
+ }
31
+
32
+ function isTaskLoopOwner(owner = null) {
33
+ return Boolean(owner && owner.kind === "task_loop" && owner.taskRunId);
34
+ }
35
+
36
+ module.exports = {
37
+ agentLoopOwner,
38
+ taskLoopOwner,
39
+ normalizeGraphOwner,
40
+ isTaskLoopOwner,
41
+ };
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Route graph yields to the owning loop mailbox.
5
+ */
6
+
7
+ const { isTaskLoopOwner } = require("./graphOwner");
8
+ const { enqueueAgentRuntime, enqueueTaskEvent } = require("./loopMailbox");
9
+ const { createRuntimeEvent } = require("./runtimeEvents");
10
+
11
+ function routeGraphYield(executionState = null, {
12
+ graph = null,
13
+ reason = "",
14
+ waitingFor = null,
15
+ } = {}) {
16
+ const owner = graph && graph.owner ? graph.owner : null;
17
+ const graphId = graph && graph.graphId ? graph.graphId : "";
18
+ const payload = {
19
+ graphId,
20
+ reason: String(reason || "").trim() || "llm_required",
21
+ waitingFor: waitingFor || null,
22
+ };
23
+
24
+ if (isTaskLoopOwner(owner)) {
25
+ return enqueueTaskEvent(executionState, owner.taskRunId, {
26
+ kind: "graph_yield",
27
+ ...payload,
28
+ });
29
+ }
30
+
31
+ // Parent / agent-owned graph: surface as runtime event (not user message).
32
+ return enqueueAgentRuntime(executionState, createRuntimeEvent("parent_graph_ready_changed", {
33
+ readyNodes: waitingFor && waitingFor.id ? [waitingFor.id] : [],
34
+ graphId,
35
+ yieldReason: payload.reason,
36
+ waitingFor,
37
+ }));
38
+ }
39
+
40
+ module.exports = {
41
+ routeGraphYield,
42
+ };
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+
3
+ module.exports = {
4
+ ...require("./runtimeEvents"),
5
+ ...require("./graphOwner"),
6
+ ...require("./loopMailbox"),
7
+ ...require("./taskRun"),
8
+ ...require("./workspaceLease"),
9
+ ...require("./taskFocus"),
10
+ ...require("./taskLoop"),
11
+ ...require("./taskControl"),
12
+ ...require("./toolProvenance"),
13
+ ...require("./graphYieldRouter"),
14
+ ...require("./agentWakeup"),
15
+ };