pi-durable-subagents 1.0.16 → 1.0.17
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/CHANGELOG.md +12 -0
- package/dist/agent/main/snapshots.js +6 -2
- package/dist/orchestrator/executor/observe.js +17 -6
- package/dist/ui/cards.js +35 -17
- package/dist/ui/index.js +5 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.17
|
|
4
|
+
|
|
5
|
+
- A silent subagent shows one warning instead of separate activity and
|
|
6
|
+
progress warnings. The message now says that no new output or tool
|
|
7
|
+
progress has been received and that the model may still be processing;
|
|
8
|
+
the ten-minute thresholds are unchanged.
|
|
9
|
+
- Warning cards update in place to `recovered` when activity resumes, or
|
|
10
|
+
`ended` when the execution ends. They refresh even when the status line
|
|
11
|
+
does not change or the dock is turned off.
|
|
12
|
+
- Execution checkpoints and warnings record the latest received stream
|
|
13
|
+
update's time and type for diagnosis, without recording its content.
|
|
14
|
+
|
|
3
15
|
## 1.0.16
|
|
4
16
|
|
|
5
17
|
- A used-up usage window is found while pi is still retrying: at the second
|
|
@@ -107,11 +107,15 @@ const resolutions = new WeakMap();
|
|
|
107
107
|
function resolvedIn(entries) {
|
|
108
108
|
let done = resolutions.get(entries);
|
|
109
109
|
if (!done) {
|
|
110
|
-
done = new
|
|
110
|
+
done = new Map(entries.filter(e => e.type === JT.attentionResolved).map(e => [JSON.stringify([e.id, e.rev]), typeof e.resolution === "string" ? e.resolution : "resolved"]));
|
|
111
111
|
resolutions.set(entries, done);
|
|
112
112
|
}
|
|
113
113
|
return done;
|
|
114
114
|
}
|
|
115
|
+
/** Preserve the reason so presentation distinguishes resumed output from a terminated execution. */
|
|
116
|
+
export function attentionResolution(home, item) {
|
|
117
|
+
return resolvedIn(readJournalSnapshot(journalPath(home, item.wid))).get(JSON.stringify([item.id, item.rev]));
|
|
118
|
+
}
|
|
115
119
|
const asks = new Map(), ASK_SCANS = 64, MARK = 64;
|
|
116
120
|
function ask(scan, line) {
|
|
117
121
|
if (!line.includes('"ask"'))
|
|
@@ -189,7 +193,7 @@ function answeredAsks(path) {
|
|
|
189
193
|
}
|
|
190
194
|
/** P15: Refresh a question against durable workflow and child receipts at request time. */
|
|
191
195
|
export function resolved(home, item) {
|
|
192
|
-
if (
|
|
196
|
+
if (attentionResolution(home, item) !== undefined)
|
|
193
197
|
return true;
|
|
194
198
|
if (item.kind !== "question" || !item.session || !item.qid)
|
|
195
199
|
return false;
|
|
@@ -23,6 +23,8 @@ export async function observeExecution(d) {
|
|
|
23
23
|
const { home, config, ticket: t, exec, child, serial } = d;
|
|
24
24
|
const session = callSession(home, t.wid, t.key, t.gen), clock = new ActiveTime(), started = clock.last;
|
|
25
25
|
let progress = started, providerError;
|
|
26
|
+
// Diagnostic metadata only: checkpoint the latest received stream update, never its content.
|
|
27
|
+
let lastStream;
|
|
26
28
|
const tools = new Set();
|
|
27
29
|
// The tool calls running now, for the stall text: a silent long command and a stuck call read differently.
|
|
28
30
|
const running = new Map();
|
|
@@ -38,7 +40,7 @@ export async function observeExecution(d) {
|
|
|
38
40
|
const previous = t.journal.entries().findLast(e => e.type === "time" && e.exec === exec);
|
|
39
41
|
if (!monotoneTime({ active: Number(previous?.active ?? 0) }, { active: clock.active }))
|
|
40
42
|
throw new Error("Nonmonotone active time");
|
|
41
|
-
await t.journal.append("time", { exec, active: clock.active });
|
|
43
|
+
await t.journal.append("time", { exec, active: clock.active, ...(lastStream ? { lastStream } : {}) });
|
|
42
44
|
});
|
|
43
45
|
const limits = async () => {
|
|
44
46
|
if (t.spec.timeoutMs !== undefined && prior + clock.active >= t.spec.timeoutMs) {
|
|
@@ -49,6 +51,11 @@ export async function observeExecution(d) {
|
|
|
49
51
|
if (reached(totalUsage(t.journal.entries(), t.callId), t.spec.budget))
|
|
50
52
|
signal();
|
|
51
53
|
};
|
|
54
|
+
const openAlert = (id) => {
|
|
55
|
+
const last = attentionEntries(t.journal.entries(), id).at(-1)?.item;
|
|
56
|
+
return last && !t.journal.entries().some(e => e.type === JT.attentionResolved && e.id === id && e.rev === last.rev);
|
|
57
|
+
};
|
|
58
|
+
const progressDue = () => !clock.asking && !tools.size && performance.now() - progress >= (config.k?.progressMs ?? 600000);
|
|
52
59
|
const stall = () => serial(async () => {
|
|
53
60
|
const id = `stall:${t.callId}`;
|
|
54
61
|
const items = attentionEntries(t.journal.entries(), id), last = items.at(-1)?.item;
|
|
@@ -56,8 +63,8 @@ export async function observeExecution(d) {
|
|
|
56
63
|
const fresh = items.at(-1)?.exec === exec ? clock.last > Number(items.at(-1)?.horizon) : clock.last > started;
|
|
57
64
|
if (open && fresh)
|
|
58
65
|
await t.journal.append(JT.attentionResolved, { id, rev: last.rev, resolution: "activity" });
|
|
59
|
-
else if (!open && !clock.asking && performance.now() - clock.last >= (config.k?.stallMs ?? 600000))
|
|
60
|
-
await t.journal.append(JT.attention, { exec, horizon: clock.last, item: { id, rev: (last?.rev ?? 0) + 1, kind: "stall", text: `${t.wid}/${t.key}: no execution activity for ${Math.floor((performance.now() - clock.last) / 60000)}m` + runningText(), wid: t.wid, call: t.callId } });
|
|
66
|
+
else if (!open && !clock.asking && !progressDue() && !openAlert(`noprogress:${t.callId}`) && performance.now() - clock.last >= (config.k?.stallMs ?? 600000))
|
|
67
|
+
await t.journal.append(JT.attention, { exec, horizon: clock.last, ...(lastStream ? { lastStream } : {}), item: { id, rev: (last?.rev ?? 0) + 1, kind: "stall", text: `${t.wid}/${t.key}: no execution activity observed for ${Math.floor((performance.now() - clock.last) / 60000)}m` + runningText(), wid: t.wid, call: t.callId } });
|
|
61
68
|
});
|
|
62
69
|
/** "; running bash `make matrix` for 14m (no output or CPU use seen)": a silent long command, not a stuck model. */
|
|
63
70
|
const runningText = () => {
|
|
@@ -73,10 +80,10 @@ export async function observeExecution(d) {
|
|
|
73
80
|
const fresh = items.at(-1)?.exec === exec ? progress > Number(items.at(-1)?.horizon) : progress > started;
|
|
74
81
|
if (open && fresh)
|
|
75
82
|
await t.journal.append(JT.attentionResolved, { id, rev: last.rev, resolution: "progress" });
|
|
76
|
-
else if (!open &&
|
|
77
|
-
const text = `${t.wid}/${t.key}:
|
|
83
|
+
else if (!open && progressDue() && !openAlert(`stall:${t.callId}`)) {
|
|
84
|
+
const text = `${t.wid}/${t.key}: no new output or tool progress received for ${Math.floor((performance.now() - progress) / 60000)}m; the model may still be processing` +
|
|
78
85
|
(providerError ? `; last provider error: ${providerError.slice(0, 200)}` : "");
|
|
79
|
-
await t.journal.append(JT.attention, { exec, horizon: progress, item: { id, rev: (last?.rev ?? 0) + 1, kind: "stall", text, wid: t.wid, call: t.callId } });
|
|
86
|
+
await t.journal.append(JT.attention, { exec, horizon: progress, ...(lastStream ? { lastStream } : {}), item: { id, rev: (last?.rev ?? 0) + 1, kind: "stall", text, wid: t.wid, call: t.callId } });
|
|
80
87
|
}
|
|
81
88
|
});
|
|
82
89
|
child.stdin.on("error", () => { });
|
|
@@ -105,6 +112,10 @@ export async function observeExecution(d) {
|
|
|
105
112
|
// P18: Apply RPC boundaries at receipt, before any in-flight scan can resume.
|
|
106
113
|
// Durable observations remain queued; clock transitions never wait on I/O.
|
|
107
114
|
clock.event(event, performance.now());
|
|
115
|
+
if (event.type === "message_update") {
|
|
116
|
+
const type = event.assistantMessageEvent?.type;
|
|
117
|
+
lastStream = { type: typeof type === "string" && /^(thinking|text|toolcall)_(start|delta|end)$/.test(type) ? type : "message_update", receivedAt: Date.now() };
|
|
118
|
+
}
|
|
108
119
|
const message = event.message;
|
|
109
120
|
const error = event.type === "auto_retry_start" ? event.errorMessage : event.type === "auto_retry_end" ? event.finalError :
|
|
110
121
|
event.type === "message_end" && message?.stopReason === "error" ? message.errorMessage : undefined;
|
package/dist/ui/cards.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
2
2
|
import { CT } from "../types.js";
|
|
3
|
-
import { resolved } from "../agent/main/snapshots.js";
|
|
3
|
+
import { attentionResolution, resolved } from "../agent/main/snapshots.js";
|
|
4
4
|
const HEAD = {
|
|
5
5
|
question: { icon: "?", title: "asks the main agent", tone: "accent" },
|
|
6
6
|
finished: { icon: "✓", title: "finished", tone: "success" },
|
|
@@ -33,37 +33,48 @@ export function card(theme, tone, heading, body, width, expanded = false) {
|
|
|
33
33
|
}
|
|
34
34
|
/** P15, P16: Register renderers for attention presentations and watch-view notes in the main session. */
|
|
35
35
|
export function registerCards(pi, home) {
|
|
36
|
-
const
|
|
37
|
-
|
|
36
|
+
const pending = new Map();
|
|
37
|
+
const done = new Map(); // resolution is monotone: once resolved, never re-read
|
|
38
|
+
// pi renders every visible message on each frame; open cards re-read receipts at most once a second.
|
|
38
39
|
const checked = new Map();
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
const resolution = (item) => {
|
|
41
|
+
if (item.kind !== "question" && item.kind !== "stall")
|
|
42
|
+
return undefined;
|
|
43
|
+
const id = `${item.wid}:${item.id}@${item.rev}`;
|
|
44
|
+
if (done.has(id)) {
|
|
45
|
+
pending.delete(id);
|
|
46
|
+
return done.get(id);
|
|
47
|
+
}
|
|
43
48
|
const now = Date.now();
|
|
44
49
|
if (now - (checked.get(id) ?? -Infinity) < 1000)
|
|
45
|
-
return
|
|
50
|
+
return undefined;
|
|
46
51
|
checked.set(id, now);
|
|
47
52
|
try {
|
|
48
|
-
|
|
49
|
-
|
|
53
|
+
const reason = item.kind === "question" ? (resolved(home, item) ? "answered" : undefined) : attentionResolution(home, item);
|
|
54
|
+
if (reason) {
|
|
55
|
+
done.set(id, reason);
|
|
50
56
|
checked.delete(id);
|
|
51
|
-
|
|
57
|
+
pending.delete(id);
|
|
58
|
+
return reason;
|
|
52
59
|
}
|
|
53
60
|
}
|
|
54
61
|
catch { /* display only */ }
|
|
55
|
-
return
|
|
62
|
+
return undefined;
|
|
56
63
|
};
|
|
57
64
|
pi.registerMessageRenderer(CT.attention, (message, options, theme) => {
|
|
58
65
|
const items = message.details?.items;
|
|
59
66
|
if (!items?.length)
|
|
60
67
|
return undefined;
|
|
61
|
-
|
|
68
|
+
for (const item of items)
|
|
69
|
+
if (item.kind === "stall" || item.kind === "question")
|
|
70
|
+
pending.set(`${item.wid}:${item.id}@${item.rev}`, item);
|
|
71
|
+
// Reuse lines until width, theme, expansion or an attention resolution changes.
|
|
62
72
|
let last;
|
|
63
73
|
const draw = (width) => items.flatMap(item => {
|
|
64
|
-
const h = HEAD[item.kind] ?? HEAD.unknown,
|
|
65
|
-
const title = item.kind === "stall" && item.id.startsWith("noprogress:") ? "
|
|
66
|
-
const
|
|
74
|
+
const h = HEAD[item.kind] ?? HEAD.unknown, reason = resolution(item), closed = !!reason;
|
|
75
|
+
const title = item.kind === "stall" && item.id.startsWith("noprogress:") ? "awaiting progress" : h.title;
|
|
76
|
+
const status = item.kind === "question" ? "answered" : reason === "activity" || reason === "progress" ? "recovered" : reason === "ended" || reason === "retired" ? "ended" : "resolved";
|
|
77
|
+
const heading = `${closed ? "✓" : h.icon} ${item.kind === "finished" && !item.call ? "Workflow" : `Subagent ${keyOf(item)}`} ${closed ? `— ${status}` : title}`;
|
|
67
78
|
// v12 §3: a finished digest is first line + dim per-agent lines, clipped; old single-line items read exactly as before.
|
|
68
79
|
const inner = Math.max(1, Math.max(3, Math.floor(width)) - 4);
|
|
69
80
|
const body = item.kind === "finished" && !closed
|
|
@@ -72,7 +83,7 @@ export function registerCards(pi, home) {
|
|
|
72
83
|
return card(theme, closed ? "muted" : h.tone, heading, body, width, options.expanded);
|
|
73
84
|
});
|
|
74
85
|
return { invalidate() { last = undefined; }, render: (width) => {
|
|
75
|
-
const key = `${width}|${items.map(item => item
|
|
86
|
+
const key = `${width}|${items.map(item => resolution(item) ?? "").join("|")}`;
|
|
76
87
|
if (last?.key !== key)
|
|
77
88
|
last = { key, lines: draw(width) };
|
|
78
89
|
return last.lines;
|
|
@@ -87,4 +98,11 @@ export function registerCards(pi, home) {
|
|
|
87
98
|
return last.lines;
|
|
88
99
|
} };
|
|
89
100
|
});
|
|
101
|
+
// The existing UI refresh loop repaints receipts even when dock text did not change.
|
|
102
|
+
return () => {
|
|
103
|
+
const before = done.size;
|
|
104
|
+
for (const item of pending.values())
|
|
105
|
+
resolution(item);
|
|
106
|
+
return done.size !== before;
|
|
107
|
+
};
|
|
90
108
|
}
|
package/dist/ui/index.js
CHANGED
|
@@ -7,8 +7,7 @@ import { registerCards } from "./cards.js";
|
|
|
7
7
|
import { toolRenderers } from "./tool.js";
|
|
8
8
|
/** UI §1–3, P16, P21: Register journal-backed list/watch surfaces only in interactive pi. */
|
|
9
9
|
export function registerUi(pi, deps) {
|
|
10
|
-
|
|
11
|
-
registerCards(pi, deps.home); // P21: cards are optional
|
|
10
|
+
const refreshCards = typeof pi.registerMessageRenderer === "function" ? registerCards(pi, deps.home) : () => false; // P21: cards are optional
|
|
12
11
|
// UI §5: compact tool calls and results (optional surface; without it pi shows the raw JSON).
|
|
13
12
|
if (typeof pi.registerToolRenderer === "function")
|
|
14
13
|
pi.registerToolRenderer((name, next) => {
|
|
@@ -38,7 +37,7 @@ export function registerUi(pi, deps) {
|
|
|
38
37
|
const state = { folded: new Set(), done: new Map(), viewed: new Set(), finished: false };
|
|
39
38
|
let screen, opening = false, stopped = false, closeScreen;
|
|
40
39
|
let unsubscribe, timer;
|
|
41
|
-
let dockRows = () => [], dockAt = null,
|
|
40
|
+
let dockRows = () => [], dockAt = null, lastDock = "";
|
|
42
41
|
// ↓ opens the list only from pi's own input editor. Another surface — /model's selector, a dialog, another
|
|
43
42
|
// extension's overlay — has the focus instead, and its ↓ belongs to it. pi's editor (and any editor built on
|
|
44
43
|
// CustomEditor) carries the app action map; the TUI comes from the dock widget, so without a dock the check
|
|
@@ -86,11 +85,9 @@ export function registerUi(pi, deps) {
|
|
|
86
85
|
const at = dock === "off" ? undefined : data.dockAt;
|
|
87
86
|
if (at !== dockAt) {
|
|
88
87
|
dockAt = at;
|
|
89
|
-
dockTui = undefined;
|
|
90
88
|
// With the dock off, an empty widget below the editor still lends the TUI to the ↓ focus check; pi adds no
|
|
91
89
|
// spacer for it, so it takes no line.
|
|
92
90
|
ctx.ui.setWidget("durable-subagents", !at ? tui => { keysTui = tui; return { invalidate() { }, render: () => [] }; } : (tui, theme) => {
|
|
93
|
-
dockTui = tui;
|
|
94
91
|
keysTui = tui;
|
|
95
92
|
return {
|
|
96
93
|
invalidate() { },
|
|
@@ -106,9 +103,10 @@ export function registerUi(pi, deps) {
|
|
|
106
103
|
}, { placement: at === "above" ? "aboveEditor" : "belowEditor" });
|
|
107
104
|
}
|
|
108
105
|
const shown = dockRows(200).join("\n");
|
|
109
|
-
|
|
106
|
+
const cardsChanged = refreshCards();
|
|
107
|
+
if (shown !== lastDock || cardsChanged) {
|
|
110
108
|
lastDock = shown;
|
|
111
|
-
|
|
109
|
+
keysTui?.requestRender?.();
|
|
112
110
|
}
|
|
113
111
|
screen?.refresh();
|
|
114
112
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-durable-subagents",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.17",
|
|
4
4
|
"description": "Subagents for pi that never lose work and never do it twice. Crash-safe workflows, automatic recovery, and a live view just like the main agent.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|