u-foo 3.0.3 → 3.0.5
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/context/planMode.js +1 -0
- package/src/code/context/planProjection.js +199 -42
- package/src/code/context/promptLayers.js +1 -0
- package/src/code/context/userNudge.js +49 -0
- package/src/code/nativeRunner.js +83 -0
- package/src/code/runtime/agentWakeup.js +64 -1
- package/src/ui/ink/UcodeApp.js +20 -7
package/package.json
CHANGED
|
@@ -232,6 +232,7 @@ function renderPlanModeContext(executionState = null) {
|
|
|
232
232
|
" 2) expand_node on the current ready/waiting task when needed",
|
|
233
233
|
" 3) Runtime executes ready tools / TaskLoops; continue from results",
|
|
234
234
|
" 4) control.complete_task (inline) or control.start_task (task_loop)",
|
|
235
|
+
"Do not end the turn with text only while a task is waiting — advance it.",
|
|
235
236
|
"Do not mix plan_graph with data-plane tools in the same turn.",
|
|
236
237
|
);
|
|
237
238
|
|
|
@@ -204,6 +204,168 @@ function buildCompactSummary(rows = [], focusId = "") {
|
|
|
204
204
|
.join(" · ");
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
+
/**
|
|
208
|
+
* Top-level plan tasks from planGraph JSON (no parent, not generated tools).
|
|
209
|
+
*/
|
|
210
|
+
function listTopLevelPlanTasks(planGraph = {}) {
|
|
211
|
+
const nodes = Array.isArray(planGraph.nodes) ? planGraph.nodes : [];
|
|
212
|
+
return nodes.filter((node) => (
|
|
213
|
+
node
|
|
214
|
+
&& node.type === "task"
|
|
215
|
+
&& !String(node.parentTaskId || "").trim()
|
|
216
|
+
&& !node.generated
|
|
217
|
+
));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Parse planGraph JSON into a DAG IR: nodes, edges, dependency waves.
|
|
222
|
+
*/
|
|
223
|
+
function buildPlanDag(planGraph = {}) {
|
|
224
|
+
const tasks = listTopLevelPlanTasks(planGraph);
|
|
225
|
+
const idSet = new Set(tasks.map((node) => String(node.id || "").trim()).filter(Boolean));
|
|
226
|
+
const nodes = tasks.map((task) => {
|
|
227
|
+
const id = String(task.id || "").trim();
|
|
228
|
+
const deps = (Array.isArray(task.dependsOn) ? task.dependsOn : [])
|
|
229
|
+
.map((dep) => String(dep || "").trim())
|
|
230
|
+
.filter((dep) => dep && idSet.has(dep));
|
|
231
|
+
const { mark, kind } = statusToMark(task.status);
|
|
232
|
+
return {
|
|
233
|
+
id,
|
|
234
|
+
title: nodeTitle(task),
|
|
235
|
+
status: String(task.status || "pending"),
|
|
236
|
+
mark,
|
|
237
|
+
kind,
|
|
238
|
+
dependsOn: deps,
|
|
239
|
+
displayOrder: Number(task.displayOrder) || 0,
|
|
240
|
+
};
|
|
241
|
+
}).filter((node) => node.id);
|
|
242
|
+
|
|
243
|
+
const byId = new Map(nodes.map((node) => [node.id, node]));
|
|
244
|
+
const edges = [];
|
|
245
|
+
for (const node of nodes) {
|
|
246
|
+
for (const dep of node.dependsOn) {
|
|
247
|
+
edges.push({ from: dep, to: node.id });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const depthMemo = new Map();
|
|
252
|
+
function depthOf(id, stack = new Set()) {
|
|
253
|
+
if (depthMemo.has(id)) return depthMemo.get(id);
|
|
254
|
+
if (stack.has(id)) return 0;
|
|
255
|
+
stack.add(id);
|
|
256
|
+
const node = byId.get(id);
|
|
257
|
+
let depth = 0;
|
|
258
|
+
if (node) {
|
|
259
|
+
for (const dep of node.dependsOn) {
|
|
260
|
+
depth = Math.max(depth, depthOf(dep, stack) + 1);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
stack.delete(id);
|
|
264
|
+
depthMemo.set(id, depth);
|
|
265
|
+
return depth;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
for (const node of nodes) depthOf(node.id);
|
|
269
|
+
|
|
270
|
+
const maxDepth = nodes.reduce((max, node) => Math.max(max, depthMemo.get(node.id) || 0), 0);
|
|
271
|
+
const buckets = Array.from({ length: maxDepth + 1 }, () => []);
|
|
272
|
+
const ordered = nodes.slice().sort((a, b) => {
|
|
273
|
+
const depthDiff = (depthMemo.get(a.id) || 0) - (depthMemo.get(b.id) || 0);
|
|
274
|
+
if (depthDiff !== 0) return depthDiff;
|
|
275
|
+
if (a.displayOrder !== b.displayOrder) return a.displayOrder - b.displayOrder;
|
|
276
|
+
return a.id.localeCompare(b.id);
|
|
277
|
+
});
|
|
278
|
+
for (const node of ordered) {
|
|
279
|
+
buckets[depthMemo.get(node.id) || 0].push(node);
|
|
280
|
+
}
|
|
281
|
+
const waves = buckets.filter((wave) => wave.length > 0);
|
|
282
|
+
return {
|
|
283
|
+
nodes,
|
|
284
|
+
edges,
|
|
285
|
+
waves,
|
|
286
|
+
linear: waves.length > 0 && waves.every((wave) => wave.length === 1),
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function waveStepLabel(waveIndex = 0, nodeIndex = 0, waveSize = 1) {
|
|
291
|
+
const step = Math.max(1, Math.floor(Number(waveIndex) || 0) + 1);
|
|
292
|
+
if (waveSize <= 1) return String(step);
|
|
293
|
+
const letter = String.fromCharCode(97 + Math.max(0, Math.min(25, Math.floor(Number(nodeIndex) || 0))));
|
|
294
|
+
return `${step}${letter}`;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function formatParallelWaveLines(wave = [], waveIndex = 0, titleMax = 40) {
|
|
298
|
+
const lines = [];
|
|
299
|
+
const size = wave.length;
|
|
300
|
+
wave.forEach((node, nodeIndex) => {
|
|
301
|
+
const label = waveStepLabel(waveIndex, nodeIndex, size);
|
|
302
|
+
const body = `${label} ${node.mark} ${truncate(node.title, titleMax)}`;
|
|
303
|
+
if (nodeIndex === 0) {
|
|
304
|
+
lines.push(` ┌─ ${body}`);
|
|
305
|
+
if (size > 1) lines.push("──┤");
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
if (nodeIndex === size - 1) {
|
|
309
|
+
lines.push(` └─ ${body}`);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
lines.push(` ├─ ${body}`);
|
|
313
|
+
});
|
|
314
|
+
return lines;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Build markdown (linear list) or ASCII flowchart (parallel waves) from planGraph JSON.
|
|
319
|
+
*/
|
|
320
|
+
function buildRoadmapMarkdown(planGraph = {}, {
|
|
321
|
+
cols = 80,
|
|
322
|
+
taskRunLine = "",
|
|
323
|
+
maxRows = 10,
|
|
324
|
+
} = {}) {
|
|
325
|
+
const dag = buildPlanDag(planGraph);
|
|
326
|
+
if (dag.nodes.length === 0) {
|
|
327
|
+
return { markdown: "", lines: [], dag };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const titleMax = Math.max(12, Math.min(48, Math.floor(Number(cols) || 80) - 12));
|
|
331
|
+
const objective = truncate(String(planGraph.objective || "").trim(), titleMax);
|
|
332
|
+
const lines = [objective ? `**Plan** · ${objective}` : "**Plan**"];
|
|
333
|
+
|
|
334
|
+
if (dag.linear) {
|
|
335
|
+
dag.waves.forEach((wave, waveIndex) => {
|
|
336
|
+
const node = wave[0];
|
|
337
|
+
lines.push(`${waveIndex + 1}. ${node.mark} ${truncate(node.title, titleMax)}`);
|
|
338
|
+
});
|
|
339
|
+
} else {
|
|
340
|
+
dag.waves.forEach((wave, waveIndex) => {
|
|
341
|
+
if (wave.length === 1) {
|
|
342
|
+
const node = wave[0];
|
|
343
|
+
lines.push(`${waveStepLabel(waveIndex, 0, 1)} ${node.mark} ${truncate(node.title, titleMax)}`);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
for (const line of formatParallelWaveLines(wave, waveIndex, Math.max(8, titleMax - 4))) {
|
|
347
|
+
lines.push(line);
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const extra = String(taskRunLine || "").trim();
|
|
353
|
+
if (extra) lines.push(extra);
|
|
354
|
+
|
|
355
|
+
const limit = Number.isFinite(maxRows) && maxRows > 0 ? Math.floor(maxRows) : 10;
|
|
356
|
+
let clipped = lines.slice(0, Math.max(1, limit));
|
|
357
|
+
if (lines.length > clipped.length) {
|
|
358
|
+
clipped = clipped.slice(0, Math.max(1, limit - 1));
|
|
359
|
+
clipped.push(`… +${lines.length - clipped.length} more`);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return {
|
|
363
|
+
markdown: clipped.join("\n"),
|
|
364
|
+
lines: clipped,
|
|
365
|
+
dag,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
207
369
|
function buildDebugLines(executionState = null, planGraph = {}) {
|
|
208
370
|
const lines = [];
|
|
209
371
|
const pg = planGraph && typeof planGraph === "object" ? planGraph : {};
|
|
@@ -305,61 +467,52 @@ function buildPlanUiProjection(executionState = null, options = {}) {
|
|
|
305
467
|
const focusTitle = focus ? truncate(focus.title, narrow ? 18 : 28) : "";
|
|
306
468
|
|
|
307
469
|
let bandLines = [];
|
|
470
|
+
let roadmapMarkdown = "";
|
|
308
471
|
let visible = false;
|
|
472
|
+
let planDag = null;
|
|
473
|
+
|
|
474
|
+
const taskRunSuffix = (() => {
|
|
475
|
+
if (!taskRun) return "";
|
|
476
|
+
const leaseBit = leaseHeld ? "writing" : taskRun.phase;
|
|
477
|
+
const files = taskRun.changedFilesHint ? ` · ${taskRun.changedFilesHint}` : "";
|
|
478
|
+
return `TaskLoop ${leaseBit}${files}`;
|
|
479
|
+
})();
|
|
309
480
|
|
|
310
481
|
if (hasPlan && bandMode !== "hidden") {
|
|
311
482
|
visible = true;
|
|
312
483
|
if (bandMode === "debug") {
|
|
313
484
|
bandLines = buildDebugLines(state, pg);
|
|
485
|
+
roadmapMarkdown = "";
|
|
314
486
|
} else if (narrow) {
|
|
315
487
|
const summary = buildCompactSummary(tree, focus && focus.nodeId);
|
|
316
488
|
bandLines = [truncate(
|
|
317
489
|
`Plan${focusTitle ? ` · ${focusTitle}` : ""}${progressLabel ? ` (${progressLabel})` : ""}${summary && !focusTitle ? ` ${summary}` : ""}`,
|
|
318
490
|
Math.max(24, cols - 2)
|
|
319
491
|
)];
|
|
320
|
-
|
|
321
|
-
const summary = buildCompactSummary(tree, focus && focus.nodeId);
|
|
322
|
-
const header = truncate(
|
|
323
|
-
`Plan${pg.objective ? ` · ${pg.objective}` : ""}${summary ? ` ${summary}` : ""}`,
|
|
324
|
-
Math.max(24, cols - 2)
|
|
325
|
-
);
|
|
326
|
-
bandLines = [header];
|
|
327
|
-
if (focus) {
|
|
328
|
-
const focusChildren = view
|
|
329
|
-
.filter((node) => node && node.parentId === focus.nodeId)
|
|
330
|
-
.map((node) => {
|
|
331
|
-
const { mark } = statusToMark(node.status);
|
|
332
|
-
return `${mark} ${nodeTitle(node)}`;
|
|
333
|
-
});
|
|
334
|
-
if (focusChildren.length > 0) {
|
|
335
|
-
bandLines.push(truncate(
|
|
336
|
-
` └ ${focusChildren.join(" · ")}`,
|
|
337
|
-
Math.max(24, cols - 2)
|
|
338
|
-
));
|
|
339
|
-
} else if (focus.title) {
|
|
340
|
-
bandLines.push(truncate(` → ${focus.title}`, Math.max(24, cols - 2)));
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
if (taskRun) {
|
|
344
|
-
const leaseBit = leaseHeld ? "writing" : taskRun.phase;
|
|
345
|
-
const files = taskRun.changedFilesHint ? ` · ${taskRun.changedFilesHint}` : "";
|
|
346
|
-
bandLines.push(truncate(` TaskLoop ${leaseBit}${files}`, Math.max(24, cols - 2)));
|
|
347
|
-
}
|
|
348
|
-
const maxRows = Number.isFinite(options.maxBandRows) ? options.maxBandRows : 3;
|
|
349
|
-
bandLines = bandLines.slice(0, Math.max(1, maxRows));
|
|
492
|
+
roadmapMarkdown = "";
|
|
350
493
|
} else {
|
|
351
|
-
// expanded
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
494
|
+
// auto + expanded: JSON → DAG → roadmap markdown
|
|
495
|
+
const maxRows = Number.isFinite(options.maxBandRows)
|
|
496
|
+
? options.maxBandRows
|
|
497
|
+
: (bandMode === "expanded" ? 16 : 10);
|
|
498
|
+
const roadmap = buildRoadmapMarkdown(pg, {
|
|
499
|
+
cols,
|
|
500
|
+
taskRunLine: taskRunSuffix,
|
|
501
|
+
maxRows,
|
|
502
|
+
});
|
|
503
|
+
planDag = roadmap.dag;
|
|
504
|
+
roadmapMarkdown = roadmap.markdown;
|
|
505
|
+
bandLines = roadmap.lines.slice();
|
|
506
|
+
if (bandMode === "expanded" && tree.length > 0) {
|
|
507
|
+
// Keep tree as fallback detail only when roadmap empty (shouldn't happen).
|
|
508
|
+
if (bandLines.length === 0) {
|
|
509
|
+
const title = pg.objective ? `Plan · ${pg.objective}` : "Plan";
|
|
510
|
+
bandLines = [truncate(title, Math.max(24, cols - 2))];
|
|
511
|
+
for (const row of tree) {
|
|
512
|
+
bandLines.push(truncate(formatTreeLine(row), Math.max(24, cols - 2)));
|
|
513
|
+
}
|
|
514
|
+
}
|
|
360
515
|
}
|
|
361
|
-
const maxRows = Number.isFinite(options.maxBandRows) ? options.maxBandRows : 7;
|
|
362
|
-
bandLines = bandLines.slice(0, Math.max(1, maxRows));
|
|
363
516
|
}
|
|
364
517
|
}
|
|
365
518
|
|
|
@@ -402,7 +555,7 @@ function buildPlanUiProjection(executionState = null, options = {}) {
|
|
|
402
555
|
leaseHeld,
|
|
403
556
|
progressDone: progress.done,
|
|
404
557
|
progressTotal: progress.total,
|
|
405
|
-
bandLines,
|
|
558
|
+
bandLines: roadmapMarkdown ? [roadmapMarkdown] : bandLines,
|
|
406
559
|
});
|
|
407
560
|
|
|
408
561
|
return {
|
|
@@ -413,11 +566,13 @@ function buildPlanUiProjection(executionState = null, options = {}) {
|
|
|
413
566
|
progress,
|
|
414
567
|
focus,
|
|
415
568
|
tree,
|
|
569
|
+
dag: planDag,
|
|
416
570
|
taskRun,
|
|
417
571
|
leaseHeld,
|
|
418
572
|
statusLine,
|
|
419
573
|
idleHint,
|
|
420
574
|
activityStatusLine,
|
|
575
|
+
roadmapMarkdown,
|
|
421
576
|
bandLines,
|
|
422
577
|
hash,
|
|
423
578
|
};
|
|
@@ -428,5 +583,7 @@ module.exports = {
|
|
|
428
583
|
getBandMode,
|
|
429
584
|
setBandMode,
|
|
430
585
|
statusToMark,
|
|
586
|
+
buildPlanDag,
|
|
587
|
+
buildRoadmapMarkdown,
|
|
431
588
|
buildPlanUiProjection,
|
|
432
589
|
};
|
|
@@ -56,6 +56,7 @@ function buildImmutablePrefix() {
|
|
|
56
56
|
"- After an accepted plan_graph create or patch, Runtime automatically advances ready tool nodes. Never invent or request an execute_graph tool.",
|
|
57
57
|
"- Do not call plan_graph or task_run together with read, read_image, write, edit, bash, or artifact_read in the same assistant turn.",
|
|
58
58
|
"- When an active graph is waiting on a task, advance that node through plan_graph instead of bypassing it with direct workspace tools: use patch.expand_node for execution.kind=expand, control.complete_task (nodeId) for execution.kind=inline_llm, or control.start_task for execution.kind=task_loop.",
|
|
59
|
+
"- Do not end a turn with text only while the plan is still waiting on a task; expand, start, or complete that node. Runtime will auto-continue if you stop early, but prefer advancing in the same turn.",
|
|
59
60
|
"- control.complete_task with nodeId completes a waiting_llm inline_llm task for the current Graph owner. control.complete_task with taskRunId (or task_run complete) is reserved for the owning TaskLoop. Do not directly complete expand or aggregate tasks.",
|
|
60
61
|
"- While Plan Mode is ON, workspace mutations must be represented as plan_graph tool nodes or performed inside a running TaskRun/task_loop.",
|
|
61
62
|
"- Treat a User reminder as the latest user instruction. Reconcile it before continuing from tool results. If it is compatible with the active plan, resume the waiting plan node; otherwise patch, cancel, or replan first.",
|
|
@@ -104,6 +104,52 @@ function buildContinuationUserPrompt(userText = "", executionState = null) {
|
|
|
104
104
|
return formatUserReminderMessage([text], { waitingFor: waiting });
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
const PLAN_AUTO_CONTINUE_STOP_REASONS = new Set([
|
|
108
|
+
"approval_required",
|
|
109
|
+
"graph_terminal",
|
|
110
|
+
"scheduler_deadlock",
|
|
111
|
+
]);
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Whether the Agent Loop should keep going after a text-only model turn
|
|
115
|
+
* because the plan graph is still waiting on an agent-actionable task.
|
|
116
|
+
*/
|
|
117
|
+
function shouldAutoContinuePlan(executionState = null) {
|
|
118
|
+
if (!executionState || typeof executionState !== "object") return false;
|
|
119
|
+
if (executionState.pendingUserInteraction) return false;
|
|
120
|
+
const pg = executionState.planGraph && typeof executionState.planGraph === "object"
|
|
121
|
+
? executionState.planGraph
|
|
122
|
+
: null;
|
|
123
|
+
if (!pg) return false;
|
|
124
|
+
const waiting = pg.waitingFor && typeof pg.waitingFor === "object" ? pg.waitingFor : null;
|
|
125
|
+
if (!waiting || !waiting.id) return false;
|
|
126
|
+
if (String(waiting.type || "").trim().toLowerCase() !== "task") return false;
|
|
127
|
+
const yieldReason = String(pg.lastYieldReason || "").trim().toLowerCase();
|
|
128
|
+
if (yieldReason && PLAN_AUTO_CONTINUE_STOP_REASONS.has(yieldReason)) return false;
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Internal reminder injected by runtime when the model ends a turn while the
|
|
134
|
+
* plan is still waiting on a task. Same shape as user nudges.
|
|
135
|
+
*/
|
|
136
|
+
function buildPlanAutoContinueReminder(executionState = null) {
|
|
137
|
+
const waiting = executionState
|
|
138
|
+
&& executionState.planGraph
|
|
139
|
+
&& executionState.planGraph.waitingFor
|
|
140
|
+
? executionState.planGraph.waitingFor
|
|
141
|
+
: null;
|
|
142
|
+
if (!waiting || !waiting.id) return "";
|
|
143
|
+
return formatUserReminderMessage(
|
|
144
|
+
[
|
|
145
|
+
"Continue the active plan. Serve the waiting task now via plan_graph "
|
|
146
|
+
+ "(expand_node, control.start_task, or control.complete_task as appropriate). "
|
|
147
|
+
+ "Do not end the turn with text only while this node is waiting.",
|
|
148
|
+
],
|
|
149
|
+
{ waitingFor: waiting },
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
107
153
|
module.exports = {
|
|
108
154
|
ensurePendingUserPrompts,
|
|
109
155
|
enqueueUserPrompt,
|
|
@@ -113,4 +159,7 @@ module.exports = {
|
|
|
113
159
|
shouldFrameAsUserReminder,
|
|
114
160
|
formatUserReminderMessage,
|
|
115
161
|
buildContinuationUserPrompt,
|
|
162
|
+
shouldAutoContinuePlan,
|
|
163
|
+
buildPlanAutoContinueReminder,
|
|
164
|
+
PLAN_AUTO_CONTINUE_STOP_REASONS,
|
|
116
165
|
};
|
package/src/code/nativeRunner.js
CHANGED
|
@@ -27,7 +27,15 @@ const {
|
|
|
27
27
|
clearUserPrompts,
|
|
28
28
|
formatUserReminderMessage,
|
|
29
29
|
ensurePendingUserPrompts,
|
|
30
|
+
shouldAutoContinuePlan,
|
|
31
|
+
buildPlanAutoContinueReminder,
|
|
30
32
|
} = require("./context/userNudge");
|
|
33
|
+
const {
|
|
34
|
+
drainAgentMailboxForTurn,
|
|
35
|
+
shouldAutoContinueForTaskWake,
|
|
36
|
+
buildTaskRunWakeReminder,
|
|
37
|
+
listTaskRunsAwaitingModel,
|
|
38
|
+
} = require("./runtime/agentWakeup");
|
|
31
39
|
const {
|
|
32
40
|
runAskUserTool,
|
|
33
41
|
syncInteractionFromPlanGraph,
|
|
@@ -85,6 +93,8 @@ const DEFAULT_KIMI_MODEL = "k3";
|
|
|
85
93
|
const DEFAULT_MAX_NATIVE_TOOL_CALLS = 100;
|
|
86
94
|
const DEFAULT_MAX_NATIVE_TOOL_ERRORS = 20;
|
|
87
95
|
const DEFAULT_NATIVE_TIMEOUT_MS = 43200000; // 12 hours
|
|
96
|
+
/** Max text-only auto-continues while a plan is waiting on a task (per user submit). */
|
|
97
|
+
const DEFAULT_MAX_PLAN_AUTO_CONTINUES = 24;
|
|
88
98
|
// Anthropic Messages rejects max_tokens above the model's real cap (64K on
|
|
89
99
|
// current models), so the transports use different defaults. Override either
|
|
90
100
|
// via UFOO_UCODE_MAX_TOKENS (positive integer).
|
|
@@ -1759,6 +1769,9 @@ async function runNativeLoop({
|
|
|
1759
1769
|
// materialize Provider messages yet. STRICT via UFOO_UCODE_PROTOCOL_STRICT=1.
|
|
1760
1770
|
let activeLedger = null;
|
|
1761
1771
|
let lastProtocolLedger = null;
|
|
1772
|
+
let planAutoContinues = 0;
|
|
1773
|
+
let lastAutoContinueWaitingId = "";
|
|
1774
|
+
let consecutiveEmptyAutoContinues = 0;
|
|
1762
1775
|
|
|
1763
1776
|
if (resume) {
|
|
1764
1777
|
await withFaultPoint("before_provider_resume", () => {});
|
|
@@ -1775,10 +1788,74 @@ async function runNativeLoop({
|
|
|
1775
1788
|
messages.push({ role: "user", content });
|
|
1776
1789
|
}
|
|
1777
1790
|
|
|
1791
|
+
/** Deliver mid-loop TaskRun runtime events before the next model call. */
|
|
1792
|
+
function injectRuntimeMailboxEvents() {
|
|
1793
|
+
const drained = drainAgentMailboxForTurn(executionState);
|
|
1794
|
+
if (!drained.text) return false;
|
|
1795
|
+
messages.push({ role: "user", content: drained.text });
|
|
1796
|
+
return true;
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
function nextAutoContinueKey() {
|
|
1800
|
+
const waitingId = String(
|
|
1801
|
+
(executionState.planGraph && executionState.planGraph.waitingFor
|
|
1802
|
+
&& executionState.planGraph.waitingFor.id) || ""
|
|
1803
|
+
).trim();
|
|
1804
|
+
if (waitingId) return `plan:${waitingId}`;
|
|
1805
|
+
const runs = listTaskRunsAwaitingModel(executionState);
|
|
1806
|
+
if (runs.length > 0) {
|
|
1807
|
+
return `task:${runs.map((run) => run.id).sort().join(",")}`;
|
|
1808
|
+
}
|
|
1809
|
+
return "mailbox";
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
function tryInjectAgentAutoContinue() {
|
|
1813
|
+
if (planAutoContinues >= DEFAULT_MAX_PLAN_AUTO_CONTINUES) return false;
|
|
1814
|
+
const continueKey = nextAutoContinueKey();
|
|
1815
|
+
if (
|
|
1816
|
+
consecutiveEmptyAutoContinues >= 2
|
|
1817
|
+
&& continueKey
|
|
1818
|
+
&& continueKey === lastAutoContinueWaitingId
|
|
1819
|
+
) {
|
|
1820
|
+
return false;
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
// Prefer draining fresh runtime mail (task_started, etc.) before nudges.
|
|
1824
|
+
if (injectRuntimeMailboxEvents()) {
|
|
1825
|
+
planAutoContinues += 1;
|
|
1826
|
+
lastAutoContinueWaitingId = continueKey;
|
|
1827
|
+
consecutiveEmptyAutoContinues += 1;
|
|
1828
|
+
return true;
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
if (shouldAutoContinuePlan(executionState)) {
|
|
1832
|
+
const reminder = buildPlanAutoContinueReminder(executionState);
|
|
1833
|
+
if (!reminder) return false;
|
|
1834
|
+
messages.push({ role: "user", content: reminder });
|
|
1835
|
+
planAutoContinues += 1;
|
|
1836
|
+
lastAutoContinueWaitingId = continueKey;
|
|
1837
|
+
consecutiveEmptyAutoContinues += 1;
|
|
1838
|
+
return true;
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
if (shouldAutoContinueForTaskWake(executionState)) {
|
|
1842
|
+
const reminder = buildTaskRunWakeReminder(executionState);
|
|
1843
|
+
if (!reminder) return false;
|
|
1844
|
+
messages.push({ role: "user", content: reminder });
|
|
1845
|
+
planAutoContinues += 1;
|
|
1846
|
+
lastAutoContinueWaitingId = continueKey;
|
|
1847
|
+
consecutiveEmptyAutoContinues += 1;
|
|
1848
|
+
return true;
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
return false;
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1778
1854
|
while (true) {
|
|
1779
1855
|
guards.ensureActive();
|
|
1780
1856
|
|
|
1781
1857
|
injectPendingUserReminders();
|
|
1858
|
+
injectRuntimeMailboxEvents();
|
|
1782
1859
|
|
|
1783
1860
|
if (activeLedger) {
|
|
1784
1861
|
runProviderTurnGate(activeLedger);
|
|
@@ -1846,6 +1923,9 @@ async function runNativeLoop({
|
|
|
1846
1923
|
if (!aggregated.trim() && text) {
|
|
1847
1924
|
aggregated = text;
|
|
1848
1925
|
}
|
|
1926
|
+
if (tryInjectAgentAutoContinue()) {
|
|
1927
|
+
continue;
|
|
1928
|
+
}
|
|
1849
1929
|
return {
|
|
1850
1930
|
text: aggregated,
|
|
1851
1931
|
streamed,
|
|
@@ -1857,6 +1937,9 @@ async function runNativeLoop({
|
|
|
1857
1937
|
};
|
|
1858
1938
|
}
|
|
1859
1939
|
|
|
1940
|
+
// A tool-using turn resets the empty auto-continue streak (progress possible).
|
|
1941
|
+
consecutiveEmptyAutoContinues = 0;
|
|
1942
|
+
|
|
1860
1943
|
const pendingCalls = transport.prepareToolCalls({ messages, turnResult, toolCalls });
|
|
1861
1944
|
if (!pendingCalls) {
|
|
1862
1945
|
return {
|
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Drain Agent Loop mailbox into a turnDynamic block (never as user role).
|
|
5
|
+
* Mid-loop wakeups (nativeRunner) also consume the same mailbox into the
|
|
6
|
+
* conversation so TaskRun waiting_model does not strand the Agent Loop.
|
|
5
7
|
*/
|
|
6
8
|
|
|
7
|
-
const { drainAgentMailbox, ensureMailbox } = require("./loopMailbox");
|
|
9
|
+
const { drainAgentMailbox, ensureMailbox, peek } = require("./loopMailbox");
|
|
10
|
+
const { ensureTaskRunStore } = require("./taskRun");
|
|
8
11
|
|
|
9
12
|
function formatAgentRuntimeEvents(events = []) {
|
|
10
13
|
const list = Array.isArray(events) ? events : [];
|
|
@@ -43,6 +46,11 @@ function peekAgentMailboxText(executionState = null) {
|
|
|
43
46
|
return formatAgentRuntimeEvents(box.queue || []);
|
|
44
47
|
}
|
|
45
48
|
|
|
49
|
+
function hasPendingAgentMailbox(executionState = null) {
|
|
50
|
+
const state = executionState && typeof executionState === "object" ? executionState : {};
|
|
51
|
+
return Boolean(peek(ensureMailbox(state, "agentMailbox")));
|
|
52
|
+
}
|
|
53
|
+
|
|
46
54
|
function drainAgentMailboxForTurn(executionState = null) {
|
|
47
55
|
const events = drainAgentMailbox(executionState);
|
|
48
56
|
return {
|
|
@@ -51,8 +59,63 @@ function drainAgentMailboxForTurn(executionState = null) {
|
|
|
51
59
|
};
|
|
52
60
|
}
|
|
53
61
|
|
|
62
|
+
const AWAITING_MODEL_PHASES = new Set([
|
|
63
|
+
"waiting_model",
|
|
64
|
+
"planning",
|
|
65
|
+
"initializing",
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* TaskRuns that still need the Agent Loop (model turn / planning).
|
|
70
|
+
*/
|
|
71
|
+
function listTaskRunsAwaitingModel(executionState = null) {
|
|
72
|
+
const store = ensureTaskRunStore(executionState);
|
|
73
|
+
return Object.values(store.byId || {}).filter((run) => (
|
|
74
|
+
run
|
|
75
|
+
&& (run.status === "running" || run.status === "queued")
|
|
76
|
+
&& AWAITING_MODEL_PHASES.has(String(run.phase || "").trim().toLowerCase())
|
|
77
|
+
));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function shouldWakeAgentForTaskRuns(executionState = null) {
|
|
81
|
+
return listTaskRunsAwaitingModel(executionState).length > 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Whether the Agent Loop must keep going after a text-only model turn
|
|
86
|
+
* because TaskRuns or unread runtime mail still need service.
|
|
87
|
+
*/
|
|
88
|
+
function shouldAutoContinueForTaskWake(executionState = null) {
|
|
89
|
+
if (!executionState || typeof executionState !== "object") return false;
|
|
90
|
+
if (executionState.pendingUserInteraction) return false;
|
|
91
|
+
return hasPendingAgentMailbox(executionState) || shouldWakeAgentForTaskRuns(executionState);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function buildTaskRunWakeReminder(executionState = null) {
|
|
95
|
+
const runs = listTaskRunsAwaitingModel(executionState);
|
|
96
|
+
if (runs.length === 0 && !hasPendingAgentMailbox(executionState)) return "";
|
|
97
|
+
const lines = [
|
|
98
|
+
"Runtime wake (not a user message): active TaskRun(s) still need the Agent Loop.",
|
|
99
|
+
"Continue serving them now (read/inspect/edit via tools, or plan_graph/task_run control).",
|
|
100
|
+
"Do not end the turn with text only while a TaskRun is waiting_model.",
|
|
101
|
+
];
|
|
102
|
+
for (const run of runs.slice(0, 4)) {
|
|
103
|
+
const label = run.title || run.objective || run.parentNodeId || run.id;
|
|
104
|
+
lines.push(
|
|
105
|
+
`- taskRunId=${run.id} phase=${run.phase || ""} status=${run.status || ""}`
|
|
106
|
+
+ (label ? ` — ${String(label).slice(0, 160)}` : "")
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
return lines.join("\n");
|
|
110
|
+
}
|
|
111
|
+
|
|
54
112
|
module.exports = {
|
|
55
113
|
formatAgentRuntimeEvents,
|
|
56
114
|
peekAgentMailboxText,
|
|
115
|
+
hasPendingAgentMailbox,
|
|
57
116
|
drainAgentMailboxForTurn,
|
|
117
|
+
listTaskRunsAwaitingModel,
|
|
118
|
+
shouldWakeAgentForTaskRuns,
|
|
119
|
+
shouldAutoContinueForTaskWake,
|
|
120
|
+
buildTaskRunWakeReminder,
|
|
58
121
|
};
|
package/src/ui/ink/UcodeApp.js
CHANGED
|
@@ -90,6 +90,7 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
90
90
|
hasPlan: false,
|
|
91
91
|
visible: false,
|
|
92
92
|
bandLines: [],
|
|
93
|
+
roadmapMarkdown: "",
|
|
93
94
|
idleHint: "",
|
|
94
95
|
statusLine: "",
|
|
95
96
|
hash: "",
|
|
@@ -1248,18 +1249,30 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
1248
1249
|
renderMergeText(activeMerge)
|
|
1249
1250
|
),
|
|
1250
1251
|
) : null,
|
|
1251
|
-
planUi.visible && planUi.bandLines.length > 0
|
|
1252
|
+
planUi.visible && (planUi.roadmapMarkdown || (planUi.bandLines && planUi.bandLines.length > 0))
|
|
1252
1253
|
? h(Box, {
|
|
1253
1254
|
flexDirection: "column",
|
|
1254
1255
|
width: "100%",
|
|
1255
1256
|
marginTop: 1,
|
|
1256
1257
|
},
|
|
1257
|
-
...
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1258
|
+
...(() => {
|
|
1259
|
+
let lines = Array.isArray(planUi.bandLines) ? planUi.bandLines.slice() : [];
|
|
1260
|
+
const md = String(planUi.roadmapMarkdown || "").trim();
|
|
1261
|
+
if (md) {
|
|
1262
|
+
try {
|
|
1263
|
+
const rendered = fmt.renderLogLinesWithMarkdownAnsi(md, { inCodeBlock: false });
|
|
1264
|
+
if (Array.isArray(rendered) && rendered.length > 0) lines = rendered;
|
|
1265
|
+
} catch {
|
|
1266
|
+
lines = md.split(/\r?\n/);
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
return lines.map((line, idx) => h(Text, {
|
|
1270
|
+
key: `plan-band-${idx}`,
|
|
1271
|
+
color: md ? undefined : "magenta",
|
|
1272
|
+
dimColor: !md && idx > 0,
|
|
1273
|
+
wrap: "truncate",
|
|
1274
|
+
}, line || " "));
|
|
1275
|
+
})(),
|
|
1263
1276
|
)
|
|
1264
1277
|
: null,
|
|
1265
1278
|
interactionLines.length > 0
|