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.
- package/package.json +1 -1
- package/src/code/agent.js +350 -246
- package/src/code/commands.js +16 -0
- package/src/code/context/assembler.js +18 -13
- package/src/code/context/executionSegment.js +102 -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 +405 -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 +4 -0
- package/src/code/nativeRunner.js +589 -172
- package/src/code/protocol/controlPlane.js +93 -0
- package/src/code/protocol/faultHarness.js +90 -0
- package/src/code/protocol/index.js +20 -0
- package/src/code/protocol/loopEvents.js +102 -0
- package/src/code/protocol/materialize.js +107 -0
- package/src/code/protocol/messageFixtures.js +116 -0
- package/src/code/protocol/ownership.js +147 -0
- package/src/code/protocol/protocolValidator.js +165 -0
- package/src/code/protocol/suspension.js +173 -0
- package/src/code/protocol/toolCallLedger.js +222 -0
- package/src/code/protocol/transitions.js +97 -0
- package/src/code/providers/anthropicMessagesTransport.js +93 -0
- package/src/code/providers/index.js +7 -0
- package/src/code/providers/openaiChatTransport.js +98 -0
- package/src/code/providers/transportContract.js +46 -0
- package/src/code/repl.js +147 -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 +394 -0
- package/src/code/runtime/taskRun.js +348 -0
- package/src/code/runtime/toolProvenance.js +70 -0
- package/src/code/runtime/workspaceLease.js +249 -0
- package/src/code/sessionStore.js +1 -10
- package/src/code/skills/injection.js +1 -0
- package/src/code/taskDecomposer.js +32 -8
- package/src/code/taskRoute.js +73 -0
- 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 +268 -22
- 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,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, {
|
|
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
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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
|
+
};
|