u-foo 3.0.4 → 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/planProjection.js +199 -42
- package/src/code/nativeRunner.js +60 -13
- package/src/code/runtime/agentWakeup.js +64 -1
- package/src/ui/ink/UcodeApp.js +20 -7
package/package.json
CHANGED
|
@@ -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
|
};
|
package/src/code/nativeRunner.js
CHANGED
|
@@ -30,6 +30,12 @@ const {
|
|
|
30
30
|
shouldAutoContinuePlan,
|
|
31
31
|
buildPlanAutoContinueReminder,
|
|
32
32
|
} = require("./context/userNudge");
|
|
33
|
+
const {
|
|
34
|
+
drainAgentMailboxForTurn,
|
|
35
|
+
shouldAutoContinueForTaskWake,
|
|
36
|
+
buildTaskRunWakeReminder,
|
|
37
|
+
listTaskRunsAwaitingModel,
|
|
38
|
+
} = require("./runtime/agentWakeup");
|
|
33
39
|
const {
|
|
34
40
|
runAskUserTool,
|
|
35
41
|
syncInteractionFromPlanGraph,
|
|
@@ -1782,33 +1788,74 @@ async function runNativeLoop({
|
|
|
1782
1788
|
messages.push({ role: "user", content });
|
|
1783
1789
|
}
|
|
1784
1790
|
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
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() {
|
|
1788
1800
|
const waitingId = String(
|
|
1789
1801
|
(executionState.planGraph && executionState.planGraph.waitingFor
|
|
1790
1802
|
&& executionState.planGraph.waitingFor.id) || ""
|
|
1791
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();
|
|
1792
1815
|
if (
|
|
1793
1816
|
consecutiveEmptyAutoContinues >= 2
|
|
1794
|
-
&&
|
|
1795
|
-
&&
|
|
1817
|
+
&& continueKey
|
|
1818
|
+
&& continueKey === lastAutoContinueWaitingId
|
|
1796
1819
|
) {
|
|
1797
1820
|
return false;
|
|
1798
1821
|
}
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
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;
|
|
1806
1852
|
}
|
|
1807
1853
|
|
|
1808
1854
|
while (true) {
|
|
1809
1855
|
guards.ensureActive();
|
|
1810
1856
|
|
|
1811
1857
|
injectPendingUserReminders();
|
|
1858
|
+
injectRuntimeMailboxEvents();
|
|
1812
1859
|
|
|
1813
1860
|
if (activeLedger) {
|
|
1814
1861
|
runProviderTurnGate(activeLedger);
|
|
@@ -1876,7 +1923,7 @@ async function runNativeLoop({
|
|
|
1876
1923
|
if (!aggregated.trim() && text) {
|
|
1877
1924
|
aggregated = text;
|
|
1878
1925
|
}
|
|
1879
|
-
if (
|
|
1926
|
+
if (tryInjectAgentAutoContinue()) {
|
|
1880
1927
|
continue;
|
|
1881
1928
|
}
|
|
1882
1929
|
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
|