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.
- package/package.json +1 -1
- package/src/code/agent.js +333 -243
- package/src/code/commands.js +16 -0
- package/src/code/context/assembler.js +18 -13
- package/src/code/context/executionSegment.js +97 -119
- package/src/code/context/index.js +11 -1
- package/src/code/context/planGraph.js +1410 -0
- package/src/code/context/planGraphService.js +857 -0
- package/src/code/context/planMode.js +398 -0
- package/src/code/context/planProjection.js +432 -0
- package/src/code/context/promptLayers.js +21 -5
- package/src/code/context/stateCommit.js +2 -0
- package/src/code/context/toolRuntime.js +172 -0
- package/src/code/context/userInteraction.js +457 -0
- package/src/code/context/userNudge.js +116 -0
- package/src/code/dispatch.js +17 -1
- package/src/code/index.js +2 -0
- package/src/code/nativeRunner.js +518 -37
- package/src/code/repl.js +160 -18
- package/src/code/runtime/agentWakeup.js +58 -0
- package/src/code/runtime/graphOwner.js +41 -0
- package/src/code/runtime/graphYieldRouter.js +42 -0
- package/src/code/runtime/index.js +15 -0
- package/src/code/runtime/loopMailbox.js +124 -0
- package/src/code/runtime/runtimeEvents.js +39 -0
- package/src/code/runtime/taskControl.js +565 -0
- package/src/code/runtime/taskFocus.js +165 -0
- package/src/code/runtime/taskLoop.js +383 -0
- package/src/code/runtime/taskRun.js +187 -0
- package/src/code/runtime/toolProvenance.js +70 -0
- package/src/code/runtime/workspaceLease.js +208 -0
- package/src/code/sessionStore.js +0 -10
- package/src/code/skills/injection.js +1 -0
- package/src/code/taskDecomposer.js +32 -8
- package/src/code/tools/askUser.js +11 -0
- package/src/code/tools/planGraph.js +29 -0
- package/src/ui/format/index.js +25 -1
- package/src/ui/format/markdownRenderer.js +224 -2
- package/src/ui/ink/UcodeApp.js +285 -22
- package/src/code/context/featureFlag.js +0 -13
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, {
|
|
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,7 @@ async function runUcodeCoreAgent({
|
|
|
259
282
|
resolveNlTaskTimeoutMs,
|
|
260
283
|
resolveUcodeProviderModel,
|
|
261
284
|
runNaturalLanguageTask,
|
|
285
|
+
resumeAfterUserInteraction,
|
|
262
286
|
} = require("./agent");
|
|
263
287
|
const resolvedWorkspaceRoot = resolveUfooProjectRoot(workspaceRoot);
|
|
264
288
|
const resolvedUcode = resolveUcodeProviderModel({
|
|
@@ -315,6 +339,7 @@ async function runUcodeCoreAgent({
|
|
|
315
339
|
model: state.model || "default",
|
|
316
340
|
workspaceRoot: workspaceRoot,
|
|
317
341
|
sessionId: state.sessionId,
|
|
342
|
+
planMode: Boolean(state.executionState && state.executionState.planMode),
|
|
318
343
|
});
|
|
319
344
|
printPrompt(stdout);
|
|
320
345
|
const rl = readline.createInterface({
|
|
@@ -333,6 +358,7 @@ async function runUcodeCoreAgent({
|
|
|
333
358
|
let autoBusQueued = false;
|
|
334
359
|
let autoBusError = "";
|
|
335
360
|
let closing = false;
|
|
361
|
+
let taskInFlight = false;
|
|
336
362
|
|
|
337
363
|
const runAutoBusOnce = async () => {
|
|
338
364
|
if (!autoBusEnabled || closing) return;
|
|
@@ -433,6 +459,10 @@ async function runUcodeCoreAgent({
|
|
|
433
459
|
sessionId: state.sessionId,
|
|
434
460
|
});
|
|
435
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
|
+
}
|
|
436
466
|
}
|
|
437
467
|
if (result.kind === "model") {
|
|
438
468
|
const applied = applyUcodeModelCommand(state, result);
|
|
@@ -441,6 +471,11 @@ async function runUcodeCoreAgent({
|
|
|
441
471
|
persistSessionState(state);
|
|
442
472
|
}
|
|
443
473
|
}
|
|
474
|
+
if (result.kind === "plan") {
|
|
475
|
+
const applied = applyUcodePlanCommand(state, result);
|
|
476
|
+
stdout.write(`${applied.output}\n`);
|
|
477
|
+
if (applied.ok) persistSessionState(state);
|
|
478
|
+
}
|
|
444
479
|
if (result.kind === "ubus") {
|
|
445
480
|
const ubusResult = await runUbusCommand(state, {
|
|
446
481
|
workspaceRoot: runtimeWorkspace,
|
|
@@ -488,23 +523,29 @@ async function runUcodeCoreAgent({
|
|
|
488
523
|
});
|
|
489
524
|
}
|
|
490
525
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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
|
+
}
|
|
508
549
|
|
|
509
550
|
if (!state.jsonOutput) {
|
|
510
551
|
const tail = escapeStripper.flush();
|
|
@@ -547,6 +588,106 @@ async function runUcodeCoreAgent({
|
|
|
547
588
|
};
|
|
548
589
|
|
|
549
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
|
+
}
|
|
550
691
|
chain = chain.then(() => handleLine(line)).catch((err) => {
|
|
551
692
|
stdout.write(`${JSON.stringify({ ok: false, error: err && err.message ? err.message : "agent loop failed" })}\n`);
|
|
552
693
|
printPrompt(stdout);
|
|
@@ -640,5 +781,6 @@ module.exports = {
|
|
|
640
781
|
parseAgentArgs,
|
|
641
782
|
formatSessionUsageStatus,
|
|
642
783
|
applyUcodeModelCommand,
|
|
784
|
+
applyUcodePlanCommand,
|
|
643
785
|
suggestUcodeModels,
|
|
644
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
|
+
};
|