pi-durable-subagents 1.0.12 → 1.0.13
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/README.md +2 -2
- package/dist/orchestrator/executor/index.js +12 -2
- package/dist/orchestrator/executor/observe.js +23 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.13
|
|
4
|
+
|
|
5
|
+
- A subagent resumed after its execution was interrupted (the orchestrator
|
|
6
|
+
restarted, the process died, a failover) is told that the processes its
|
|
7
|
+
tools had started, background ones included, were stopped with it, and not
|
|
8
|
+
to wait for them; an asker that hibernated while waiting is told the same
|
|
9
|
+
with the answer. Before, it was told only to continue, and one slept on a
|
|
10
|
+
test loop that no longer ran.
|
|
11
|
+
- The "no execution activity" alert names the tool command running and how
|
|
12
|
+
long it has run (for example "running bash `make fault-matrix` for 14m"), so
|
|
13
|
+
a long silent command reads differently from a stuck call.
|
|
14
|
+
|
|
3
15
|
## 1.0.12
|
|
4
16
|
|
|
5
17
|
- Slot, memory and model decisions read the orchestrator settings recorded in
|
package/README.md
CHANGED
|
@@ -51,7 +51,7 @@ the npx cache, so `install-service` refuses to run from there.
|
|
|
51
51
|
| A step runs past its `timeoutMs` (even inside a silent tool) | Stops it cleanly as `timeout`. Only time spent working counts; waiting for you does not. |
|
|
52
52
|
| You quit pi (Ctrl+D, `/quit`, closing the terminal) while subagents run | That session's workflows pause: nothing more is spent, nothing is lost. When you come back, pi says so; `resume` (or `r` in the list) continues them in the same sessions. Set `"onQuit": "continue"` to let them run on instead. |
|
|
53
53
|
| pi crashes or is killed (`kill -9`) while subagents run | The work keeps running. When you come back, the session that started the work is told what needs you. |
|
|
54
|
-
| The machine or the orchestrator dies mid-run | The next pi you open resumes the work. Finished results are kept and nothing runs twice. |
|
|
54
|
+
| The machine or the orchestrator dies mid-run | The next pi you open resumes the work. Finished results are kept and nothing runs twice. The resumed subagent is told that processes its tools had started (background ones included) were stopped, so it checks them instead of waiting for them. |
|
|
55
55
|
| You steer a subagent while it is asking you a question | Your message reaches it, in order. Nothing is rejected or lost. |
|
|
56
56
|
| Two steers arrive out of order and the second replaces the first | Only the second one applies. |
|
|
57
57
|
| A step is refused, or a dependency fails | The workflow stops that branch cleanly. Nothing is retried in vain. |
|
|
@@ -174,7 +174,7 @@ The main agent is interrupted only when there is something to decide:
|
|
|
174
174
|
|
|
175
175
|
- a question;
|
|
176
176
|
- a finished workflow;
|
|
177
|
-
- a stalled subagent;
|
|
177
|
+
- a stalled subagent (the alert names the command it is running and for how long, so a long silent command reads differently from a stuck call);
|
|
178
178
|
- an unknown outcome;
|
|
179
179
|
- a reached budget.
|
|
180
180
|
|
|
@@ -88,6 +88,16 @@ async function defaultModel() {
|
|
|
88
88
|
return settings.defaultModel ? `${settings.defaultProvider ? `${settings.defaultProvider}/` : ""}${settings.defaultModel}` : undefined;
|
|
89
89
|
}
|
|
90
90
|
/** P2, P9, P22: Construct the journal-owned execution authority under the engine's OS lock. */
|
|
91
|
+
/** Restart feedback: a resumed child believed the test loop it had started was still running and slept on it. Its
|
|
92
|
+
* previous execution was stopped (fenced), and with it every process its tools started, background ones included. */
|
|
93
|
+
export function continueMessage(dangling) {
|
|
94
|
+
return "Your previous execution was interrupted and this one continues the task. Processes your tools started " +
|
|
95
|
+
"(background ones included) were stopped with it: do not wait for them or their output; check what they left " +
|
|
96
|
+
"and start again what is still needed. Tool calls whose outcomes are unknown: " + (dangling.join(", ") || "none") + ".";
|
|
97
|
+
}
|
|
98
|
+
/** An asker hibernates while it waits (its execution is stopped to free the provider slot), so the answer says so. */
|
|
99
|
+
export const HIBERNATED_NOTE = "While you waited for the answer below your execution was stopped; processes your tools had " +
|
|
100
|
+
"started (background ones included) were stopped with it. Check them before relying on them.";
|
|
91
101
|
export default function createExecutor(ledgers, options = {}) {
|
|
92
102
|
const { home, config, orch } = ledgers;
|
|
93
103
|
const containment = options.containment ?? serialContainment(), effects = options.effects ?? createEffects(ledgers);
|
|
@@ -742,7 +752,7 @@ export default function createExecutor(ledgers, options = {}) {
|
|
|
742
752
|
else if (unresolved && !receipt)
|
|
743
753
|
await sender.send(t.callId, "continue", { message: String(unresolved.message) }, { qid: String(unresolved.qid), rev: Number(unresolved.rev) }, { rid: String(unresolved.rid2) });
|
|
744
754
|
else if (previous)
|
|
745
|
-
await sender.send(t.callId, "continue", { message:
|
|
755
|
+
await sender.send(t.callId, "continue", { message: continueMessage(dangling) }, undefined, { rid: contentHash([exec, "dispatch"]) });
|
|
746
756
|
else
|
|
747
757
|
await sender.send(t.callId, "task", { message: t.opening?.message ?? t.spec.task }, undefined, { rid: contentHash([exec, "dispatch"]) });
|
|
748
758
|
if (interrupted(a)) {
|
|
@@ -858,7 +868,7 @@ export default function createExecutor(ledgers, options = {}) {
|
|
|
858
868
|
return { action: "reject", reason: "stale-rev" };
|
|
859
869
|
const rid2 = forwardRid(req.rid, ctx.widRev, ctx.key, hash);
|
|
860
870
|
const item = attentionEntries(ctx.journal.entries()).find(e => e.item.call === dest && e.item.qid === sleeping.qid && e.item.rev === sleeping.rev)?.item;
|
|
861
|
-
await ctx.journal.append("answer-bound", { call: dest, qid: sleeping.qid, rev: sleeping.rev, rid: req.rid, rid2, hash, message:
|
|
871
|
+
await ctx.journal.append("answer-bound", { call: dest, qid: sleeping.qid, rev: sleeping.rev, rid: req.rid, rid2, hash, message: `${HIBERNATED_NOTE}\n\nQuestion: ${item?.text ?? sleeping.qid}\nAnswer: ${req.body.message ?? ""}` });
|
|
862
872
|
active.get(dest)?.wake();
|
|
863
873
|
wake();
|
|
864
874
|
return { action: "apply" };
|
|
@@ -11,12 +11,21 @@ import { monotoneTime } from "../../kernel/guards.js";
|
|
|
11
11
|
import { readSession } from "./session.js";
|
|
12
12
|
import { ActiveTime, activeTotal } from "./time.js";
|
|
13
13
|
import { observation, reached, sessionUsage, totalUsage } from "./usage.js";
|
|
14
|
+
/** A tool call's command for a status line: bash's command, else its arguments, on one line and clipped. */
|
|
15
|
+
export function toolCommand(args) {
|
|
16
|
+
const a = args && typeof args === "object" ? args : {};
|
|
17
|
+
const text = typeof a.command === "string" ? a.command : Object.keys(a).length ? JSON.stringify(a) : "";
|
|
18
|
+
const line = text.replace(/\s+/g, " ").trim();
|
|
19
|
+
return line.length > 120 ? `${line.slice(0, 119)}…` : line;
|
|
20
|
+
}
|
|
14
21
|
/** P18, P31, P9: Observe slim evidence, decide limits and fence before returning to settlement. */
|
|
15
22
|
export async function observeExecution(d) {
|
|
16
23
|
const { home, config, ticket: t, exec, child, serial } = d;
|
|
17
24
|
const session = callSession(home, t.wid, t.key, t.gen), clock = new ActiveTime(), started = clock.last;
|
|
18
25
|
let progress = started, providerError;
|
|
19
26
|
const tools = new Set();
|
|
27
|
+
// The tool calls running now, for the stall text: a silent long command and a stuck call read differently.
|
|
28
|
+
const running = new Map();
|
|
20
29
|
const prior = activeTotal(t.journal.entries(), t.callId);
|
|
21
30
|
let size = (await fileStat(session).catch(() => ({ size: 0 }))).size, checkpoint = performance.now();
|
|
22
31
|
let signal;
|
|
@@ -48,8 +57,15 @@ export async function observeExecution(d) {
|
|
|
48
57
|
if (open && fresh)
|
|
49
58
|
await t.journal.append(JT.attentionResolved, { id, rev: last.rev, resolution: "activity" });
|
|
50
59
|
else if (!open && !clock.asking && performance.now() - clock.last >= (config.k?.stallMs ?? 600000))
|
|
51
|
-
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
|
|
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 } });
|
|
52
61
|
});
|
|
62
|
+
/** "; running bash `make matrix` for 14m (no output or CPU use seen)": a silent long command, not a stuck model. */
|
|
63
|
+
const runningText = () => {
|
|
64
|
+
const now = performance.now(), open = [...running.values()].filter(r => r.name !== "ask");
|
|
65
|
+
if (!open.length)
|
|
66
|
+
return "";
|
|
67
|
+
return "; running " + open.map(r => `${r.name}${r.command ? ` \`${r.command}\`` : ""} for ${Math.floor((now - r.since) / 60000)}m`).join(", ") + " (no output or CPU use seen)";
|
|
68
|
+
};
|
|
53
69
|
const noProgress = () => serial(async () => {
|
|
54
70
|
const id = `noprogress:${t.callId}`;
|
|
55
71
|
const items = attentionEntries(t.journal.entries(), id), last = items.at(-1)?.item;
|
|
@@ -94,10 +110,14 @@ export async function observeExecution(d) {
|
|
|
94
110
|
event.type === "message_end" && message?.stopReason === "error" ? message.errorMessage : undefined;
|
|
95
111
|
if (typeof error === "string" && error)
|
|
96
112
|
providerError = error;
|
|
97
|
-
if (event.type === "tool_execution_start")
|
|
113
|
+
if (event.type === "tool_execution_start") {
|
|
98
114
|
tools.add(String(event.toolCallId ?? ""));
|
|
99
|
-
|
|
115
|
+
running.set(String(event.toolCallId ?? ""), { name: String(event.toolName ?? "tool"), command: toolCommand(event.args), since: performance.now() });
|
|
116
|
+
}
|
|
117
|
+
if (event.type === "tool_execution_end") {
|
|
100
118
|
tools.delete(String(event.toolCallId ?? ""));
|
|
119
|
+
running.delete(String(event.toolCallId ?? ""));
|
|
120
|
+
}
|
|
101
121
|
// Receipt-time progress is independent of RPC chatter, CPU and session growth; open tools suppress alerts.
|
|
102
122
|
if (event.type === "message_update" || ["tool_execution_start", "tool_execution_update", "tool_execution_end"].includes(String(event.type)) ||
|
|
103
123
|
event.type === "message_end" && message?.stopReason !== "error" && (message?.usage?.output ?? 0) > 0)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-durable-subagents",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.13",
|
|
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",
|