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
package/src/code/repl.js CHANGED
@@ -21,18 +21,27 @@ 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");
27
+ const { applyUcodePlanCommand } = require("./context/planMode");
25
28
 
26
29
  function printPrompt(stdout = process.stdout) {
27
30
  stdout.write("> ");
28
31
  }
29
32
 
30
- 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
+ } = {}) {
31
39
  stdout.write(`${buildUcodeBannerLines({
32
40
  model,
33
41
  engine: "ufoo-core",
34
42
  workspaceRoot,
35
43
  sessionId,
44
+ planMode,
36
45
  width: (stdout && stdout.columns) || 0,
37
46
  }).join("\n")}\n`);
38
47
  }
@@ -75,40 +84,14 @@ function extractAgentNickname(agentId = "") {
75
84
  return base;
76
85
  }
77
86
 
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
87
  function runSingleCommand(line = "", workspaceRoot = process.cwd()) {
93
88
  const text = normalizeLine(line);
94
89
  if (!text) return { kind: "empty" };
95
- if (text === "exit" || text === "quit") return { kind: "exit" };
96
- if (text === "help") {
90
+ if (text === "exit" || text === "quit" || text === "/exit" || text === "/quit") return { kind: "exit" };
91
+ if (text === "help" || text === "/help") {
97
92
  return {
98
93
  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"),
94
+ output: listUcodeCommandsForHelp(),
112
95
  };
113
96
  }
114
97
  const legacyUfooMarker = parseLegacyUfooMarkerCommand(text);
@@ -128,6 +111,41 @@ function runSingleCommand(line = "", workspaceRoot = process.cwd()) {
128
111
  kind: "status",
129
112
  };
130
113
  }
114
+ const modelMatch = text.match(/^(?:\/model|model)(?:\s+(.*))?$/i);
115
+ if (modelMatch) {
116
+ const nextModel = String(modelMatch[1] || "").trim();
117
+ if (!nextModel) {
118
+ return { kind: "model", action: "show" };
119
+ }
120
+ // Reject accidental multi-token garbage; model ids are single tokens.
121
+ if (/\s/.test(nextModel)) {
122
+ return {
123
+ kind: "error",
124
+ output: "usage: /model [model-id]",
125
+ };
126
+ }
127
+ return {
128
+ kind: "model",
129
+ action: "set",
130
+ model: nextModel,
131
+ };
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
+ }
131
149
  const skillsMatch = text.match(/^(?:\/skills|skills)(?:\s+(.*))?$/i);
132
150
  if (skillsMatch) {
133
151
  const args = String(skillsMatch[1] || "").trim().split(/\s+/).filter(Boolean);
@@ -187,13 +205,13 @@ function runSingleCommand(line = "", workspaceRoot = process.cwd()) {
187
205
  task,
188
206
  };
189
207
  }
190
- const resumeMatch = text.match(/^resume(?:\s+(.+))?$/i);
208
+ const resumeMatch = text.match(/^(?:\/resume|resume)(?:\s+(.+))?$/i);
191
209
  if (resumeMatch) {
192
210
  const session = String(resumeMatch[1] || "").trim();
193
211
  if (!session) {
194
212
  return {
195
213
  kind: "error",
196
- output: "usage: resume <session-id>",
214
+ output: "usage: /resume <session-id>",
197
215
  };
198
216
  }
199
217
  return {
@@ -264,6 +282,7 @@ async function runUcodeCoreAgent({
264
282
  resolveNlTaskTimeoutMs,
265
283
  resolveUcodeProviderModel,
266
284
  runNaturalLanguageTask,
285
+ resumeAfterUserInteraction,
267
286
  } = require("./agent");
268
287
  const resolvedWorkspaceRoot = resolveUfooProjectRoot(workspaceRoot);
269
288
  const resolvedUcode = resolveUcodeProviderModel({
@@ -320,6 +339,7 @@ async function runUcodeCoreAgent({
320
339
  model: state.model || "default",
321
340
  workspaceRoot: workspaceRoot,
322
341
  sessionId: state.sessionId,
342
+ planMode: Boolean(state.executionState && state.executionState.planMode),
323
343
  });
324
344
  printPrompt(stdout);
325
345
  const rl = readline.createInterface({
@@ -338,6 +358,7 @@ async function runUcodeCoreAgent({
338
358
  let autoBusQueued = false;
339
359
  let autoBusError = "";
340
360
  let closing = false;
361
+ let taskInFlight = false;
341
362
 
342
363
  const runAutoBusOnce = async () => {
343
364
  if (!autoBusEnabled || closing) return;
@@ -438,6 +459,22 @@ async function runUcodeCoreAgent({
438
459
  sessionId: state.sessionId,
439
460
  });
440
461
  stdout.write(`${formatSessionUsageStatus(usageSummary)}\n`);
462
+ const { formatPlanModeStatus } = require("./context/planMode");
463
+ if (state.executionState) {
464
+ stdout.write(`${formatPlanModeStatus(state.executionState).split("\n").slice(0, 5).join("\n")}\n`);
465
+ }
466
+ }
467
+ if (result.kind === "model") {
468
+ const applied = applyUcodeModelCommand(state, result);
469
+ stdout.write(`${applied.output}\n`);
470
+ if (applied.ok && result.action === "set") {
471
+ persistSessionState(state);
472
+ }
473
+ }
474
+ if (result.kind === "plan") {
475
+ const applied = applyUcodePlanCommand(state, result);
476
+ stdout.write(`${applied.output}\n`);
477
+ if (applied.ok) persistSessionState(state);
441
478
  }
442
479
  if (result.kind === "ubus") {
443
480
  const ubusResult = await runUbusCommand(state, {
@@ -486,23 +523,29 @@ async function runUcodeCoreAgent({
486
523
  });
487
524
  }
488
525
 
489
- const nlResult = await runNaturalLanguageTask(result.task, state, {
490
- onDelta: state.jsonOutput
491
- ? null
492
- : async (delta) => {
493
- const text = escapeStripper.write(String(delta || ""));
494
- const safeText = stripBlessedTags(stripLeakedEscapeTags(text));
495
- if (!safeText) return;
496
- if (/[^\s]/.test(safeText)) {
497
- streamedVisible = true;
498
- }
499
- if (streamBuffer) {
500
- await streamBuffer.write(safeText);
501
- } else {
502
- stdout.write(safeText);
503
- }
504
- },
505
- });
526
+ taskInFlight = true;
527
+ let nlResult;
528
+ try {
529
+ nlResult = await runNaturalLanguageTask(result.task, state, {
530
+ onDelta: state.jsonOutput
531
+ ? null
532
+ : async (delta) => {
533
+ const text = escapeStripper.write(String(delta || ""));
534
+ const safeText = stripBlessedTags(stripLeakedEscapeTags(text));
535
+ if (!safeText) return;
536
+ if (/[^\s]/.test(safeText)) {
537
+ streamedVisible = true;
538
+ }
539
+ if (streamBuffer) {
540
+ await streamBuffer.write(safeText);
541
+ } else {
542
+ stdout.write(safeText);
543
+ }
544
+ },
545
+ });
546
+ } finally {
547
+ taskInFlight = false;
548
+ }
506
549
 
507
550
  if (!state.jsonOutput) {
508
551
  const tail = escapeStripper.flush();
@@ -545,6 +588,106 @@ async function runUcodeCoreAgent({
545
588
  };
546
589
 
547
590
  rl.on("line", (line) => {
591
+ const trimmed = normalizeLine(line);
592
+
593
+ // Pending approval/choice/chat takes priority over nudge / new NL.
594
+ try {
595
+ const {
596
+ hasPendingUserInteraction,
597
+ parseUserInteractionInput,
598
+ getPendingUserInteraction,
599
+ } = require("./context/userInteraction");
600
+ if (
601
+ trimmed
602
+ && state.executionState
603
+ && hasPendingUserInteraction(state.executionState)
604
+ ) {
605
+ const pending = getPendingUserInteraction(state.executionState);
606
+ const parsed = parseUserInteractionInput(pending, trimmed);
607
+ if (!parsed.ok) {
608
+ stdout.write(`${parsed.error || "Invalid reply"}\n`);
609
+ printPrompt(stdout);
610
+ return;
611
+ }
612
+ chain = chain.then(async () => {
613
+ let streamBuffer = null;
614
+ let streamedVisible = false;
615
+ const escapeStripper = createEscapeTagStripper();
616
+ if (!state.jsonOutput) {
617
+ streamBuffer = new StreamBuffer(stdout.write.bind(stdout), {
618
+ delay: 10,
619
+ chunkSize: 4,
620
+ });
621
+ }
622
+ taskInFlight = true;
623
+ let resumeResult;
624
+ try {
625
+ resumeResult = await resumeAfterUserInteraction(trimmed, state, {
626
+ onDelta: state.jsonOutput
627
+ ? null
628
+ : async (delta) => {
629
+ const text = escapeStripper.write(String(delta || ""));
630
+ const safeText = stripBlessedTags(stripLeakedEscapeTags(text));
631
+ if (!safeText) return;
632
+ if (/[^\s]/.test(safeText)) {
633
+ streamedVisible = true;
634
+ }
635
+ if (streamBuffer) {
636
+ await streamBuffer.write(safeText);
637
+ } else {
638
+ stdout.write(safeText);
639
+ }
640
+ },
641
+ });
642
+ } finally {
643
+ taskInFlight = false;
644
+ }
645
+ if (streamBuffer) {
646
+ await streamBuffer.finish();
647
+ }
648
+ const streamed = !state.jsonOutput && Boolean(resumeResult && resumeResult.streamed);
649
+ if (streamed && streamedVisible && resumeResult && resumeResult.streamLastChar !== "\n") {
650
+ stdout.write("\n");
651
+ }
652
+ if (resumeResult && resumeResult.waitingUserInteraction) {
653
+ stdout.write("Still waiting for your reply.\n");
654
+ } else if (!resumeResult || resumeResult.ok === false) {
655
+ stdout.write(`Error: ${(resumeResult && resumeResult.error) || "resume failed"}\n`);
656
+ } else {
657
+ const shouldSkipSummary = Boolean(streamed && resumeResult.ok && streamedVisible);
658
+ if (!shouldSkipSummary && resumeResult.summary) {
659
+ stdout.write(`${resumeResult.summary}\n`);
660
+ }
661
+ }
662
+ const persisted = persistSessionState(state);
663
+ if (!state.jsonOutput && (!persisted || persisted.ok === false)) {
664
+ stdout.write(`Warning: failed to persist session ${state.sessionId}: ${(persisted && persisted.error) || "unknown error"}\n`);
665
+ }
666
+ printPrompt(stdout);
667
+ }).catch((err) => {
668
+ stdout.write(`${JSON.stringify({ ok: false, error: err && err.message ? err.message : "resume failed" })}\n`);
669
+ printPrompt(stdout);
670
+ });
671
+ return;
672
+ }
673
+ } catch (err) {
674
+ stdout.write(`Error: ${err && err.message ? err.message : "interaction failed"}\n`);
675
+ printPrompt(stdout);
676
+ return;
677
+ }
678
+
679
+ // Mid-task NL input becomes a pending user reminder for the next LLM turn.
680
+ if (taskInFlight && trimmed && !/^\//.test(trimmed)) {
681
+ const { enqueueUserPrompt } = require("./context/userNudge");
682
+ const { emptyExecutionState } = require("./context/executionSegment");
683
+ if (!state.executionState || typeof state.executionState !== "object") {
684
+ state.executionState = emptyExecutionState();
685
+ }
686
+ enqueueUserPrompt(state.executionState, trimmed);
687
+ stdout.write("Queued user reminder for next model turn.\n");
688
+ printPrompt(stdout);
689
+ return;
690
+ }
548
691
  chain = chain.then(() => handleLine(line)).catch((err) => {
549
692
  stdout.write(`${JSON.stringify({ ok: false, error: err && err.message ? err.message : "agent loop failed" })}\n`);
550
693
  printPrompt(stdout);
@@ -637,4 +780,7 @@ module.exports = {
637
780
  extractAgentNickname,
638
781
  parseAgentArgs,
639
782
  formatSessionUsageStatus,
783
+ applyUcodeModelCommand,
784
+ applyUcodePlanCommand,
785
+ suggestUcodeModels,
640
786
  };
@@ -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
+ };
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Typed mailboxes for Agent Loop vs TaskLoop.
5
+ * Task mailbox schema has no user-event type (runtime isolation).
6
+ */
7
+
8
+ const { isRuntimeEvent } = require("./runtimeEvents");
9
+
10
+ const AGENT_EVENT_KINDS = Object.freeze(["user", "runtime"]);
11
+ const TASK_EVENT_KINDS = Object.freeze([
12
+ "graph_yield",
13
+ "tool_result",
14
+ "control",
15
+ "advance",
16
+ "model_turn",
17
+ ]);
18
+
19
+ function emptyMailbox() {
20
+ return { queue: [], seq: 0 };
21
+ }
22
+
23
+ function ensureMailbox(store = null, key = "mailbox") {
24
+ const target = store && typeof store === "object" ? store : {};
25
+ if (!target[key] || typeof target[key] !== "object") {
26
+ target[key] = emptyMailbox();
27
+ }
28
+ if (!Array.isArray(target[key].queue)) target[key].queue = [];
29
+ if (!Number.isFinite(target[key].seq)) target[key].seq = 0;
30
+ return target[key];
31
+ }
32
+
33
+ function enqueue(mailbox = null, event = {}) {
34
+ const box = mailbox && typeof mailbox === "object" ? mailbox : emptyMailbox();
35
+ if (!Array.isArray(box.queue)) box.queue = [];
36
+ box.seq = (Number(box.seq) || 0) + 1;
37
+ const entry = {
38
+ id: `evt_${box.seq}`,
39
+ enqueuedAt: new Date().toISOString(),
40
+ ...event,
41
+ };
42
+ box.queue.push(entry);
43
+ return entry;
44
+ }
45
+
46
+ function drain(mailbox = null, { max = 0 } = {}) {
47
+ const box = mailbox && typeof mailbox === "object" ? mailbox : emptyMailbox();
48
+ if (!Array.isArray(box.queue) || box.queue.length === 0) return [];
49
+ if (!max || max >= box.queue.length) {
50
+ const all = box.queue.slice();
51
+ box.queue = [];
52
+ return all;
53
+ }
54
+ const taken = box.queue.splice(0, Math.max(1, Math.floor(max)));
55
+ return taken;
56
+ }
57
+
58
+ function peek(mailbox = null) {
59
+ const box = mailbox && typeof mailbox === "object" ? mailbox : emptyMailbox();
60
+ return Array.isArray(box.queue) && box.queue.length > 0 ? box.queue[0] : null;
61
+ }
62
+
63
+ function enqueueAgentUser(executionState = null, text = "") {
64
+ const state = executionState && typeof executionState === "object" ? executionState : {};
65
+ const mailbox = ensureMailbox(state, "agentMailbox");
66
+ return enqueue(mailbox, {
67
+ kind: "user",
68
+ text: String(text || "").trim(),
69
+ });
70
+ }
71
+
72
+ function enqueueAgentRuntime(executionState = null, runtimeEvent = {}) {
73
+ if (!isRuntimeEvent(runtimeEvent)) {
74
+ throw new Error("enqueueAgentRuntime requires a runtime event");
75
+ }
76
+ const state = executionState && typeof executionState === "object" ? executionState : {};
77
+ const mailbox = ensureMailbox(state, "agentMailbox");
78
+ return enqueue(mailbox, {
79
+ kind: "runtime",
80
+ event: runtimeEvent,
81
+ });
82
+ }
83
+
84
+ function enqueueTaskEvent(executionState = null, taskRunId = "", event = {}) {
85
+ const state = executionState && typeof executionState === "object" ? executionState : {};
86
+ if (!state.taskMailboxes || typeof state.taskMailboxes !== "object") {
87
+ state.taskMailboxes = {};
88
+ }
89
+ const id = String(taskRunId || "").trim();
90
+ if (!id) throw new Error("taskRunId required");
91
+ const kind = String(event.kind || "").trim();
92
+ if (!TASK_EVENT_KINDS.includes(kind)) {
93
+ throw new Error(`invalid task mailbox event kind: ${kind}`);
94
+ }
95
+ if (!state.taskMailboxes[id]) state.taskMailboxes[id] = emptyMailbox();
96
+ return enqueue(state.taskMailboxes[id], event);
97
+ }
98
+
99
+ function drainAgentMailbox(executionState = null, options = {}) {
100
+ const state = executionState && typeof executionState === "object" ? executionState : {};
101
+ return drain(ensureMailbox(state, "agentMailbox"), options);
102
+ }
103
+
104
+ function drainTaskMailbox(executionState = null, taskRunId = "", options = {}) {
105
+ const state = executionState && typeof executionState === "object" ? executionState : {};
106
+ const id = String(taskRunId || "").trim();
107
+ if (!state.taskMailboxes || !state.taskMailboxes[id]) return [];
108
+ return drain(state.taskMailboxes[id], options);
109
+ }
110
+
111
+ module.exports = {
112
+ AGENT_EVENT_KINDS,
113
+ TASK_EVENT_KINDS,
114
+ emptyMailbox,
115
+ ensureMailbox,
116
+ enqueue,
117
+ drain,
118
+ peek,
119
+ enqueueAgentUser,
120
+ enqueueAgentRuntime,
121
+ enqueueTaskEvent,
122
+ drainAgentMailbox,
123
+ drainTaskMailbox,
124
+ };
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Runtime event types for Agent Loop wakeup (never user-role messages).
5
+ */
6
+
7
+ const RUNTIME_EVENT_TYPES = Object.freeze([
8
+ "task_started",
9
+ "task_succeeded",
10
+ "task_failed",
11
+ "task_cancelled",
12
+ "parent_graph_ready_changed",
13
+ ]);
14
+
15
+ function createRuntimeEvent(type = "", payload = {}) {
16
+ const eventType = String(type || "").trim();
17
+ if (!RUNTIME_EVENT_TYPES.includes(eventType)) {
18
+ throw new Error(`unknown runtime event type: ${type}`);
19
+ }
20
+ return {
21
+ type: eventType,
22
+ at: new Date().toISOString(),
23
+ ...payload,
24
+ };
25
+ }
26
+
27
+ function isRuntimeEvent(value = null) {
28
+ return Boolean(
29
+ value
30
+ && typeof value === "object"
31
+ && RUNTIME_EVENT_TYPES.includes(String(value.type || "")),
32
+ );
33
+ }
34
+
35
+ module.exports = {
36
+ RUNTIME_EVENT_TYPES,
37
+ createRuntimeEvent,
38
+ isRuntimeEvent,
39
+ };