@fitchmultz/pi-subagents 0.50.0 → 0.52.0
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 +63 -2
- package/README.md +36 -5
- package/dist/agent/child-runtime.js +80 -0
- package/dist/agent/contracts.js +15 -24
- package/dist/agent/host.js +3 -16
- package/dist/agent/index.js +212 -192
- package/dist/agent/journal-tail.js +115 -156
- package/dist/agent/launch.js +20 -8
- package/dist/agent/legacy.js +65 -23
- package/dist/agent/notifications.js +138 -47
- package/dist/agent/observation-state.js +246 -0
- package/dist/agent/observation-view.js +258 -0
- package/dist/agent/observation.js +350 -0
- package/dist/agent/observe-card.js +243 -0
- package/dist/agent/observe-chrome.js +85 -0
- package/dist/agent/observe-client.js +285 -0
- package/dist/agent/observe-layout.js +65 -0
- package/dist/agent/observe-reading.js +84 -0
- package/dist/agent/observe-screen.js +400 -96
- package/dist/agent/observe-transcript.js +382 -0
- package/dist/agent/observe.js +250 -43
- package/dist/agent/parking.js +253 -0
- package/dist/agent/process.js +228 -28
- package/dist/agent/receipts.js +92 -20
- package/dist/agent/runner.js +229 -126
- package/dist/agent/shell.js +2 -1
- package/dist/agent/storage.js +47 -12
- package/dist/agent/tool-schemas.js +20 -0
- package/dist/agent/typebox.js +10 -5
- package/dist/agent/usage.js +0 -115
- package/dist/agent/view.js +46 -16
- package/dist/agent/wake.js +99 -0
- package/dist/build-info.js +1 -1
- package/dist/intercom-client.d.ts +26 -0
- package/dist/intercom-client.js +45 -0
- package/dist/pi-intercom/agent-dir.d.ts +1 -0
- package/dist/pi-intercom/bridge-session.js +2 -1
- package/dist/pi-intercom/broker/broker.js +62 -43
- package/dist/pi-intercom/broker/client-last.d.ts +24 -0
- package/dist/pi-intercom/broker/client-last.js +42 -5
- package/dist/pi-intercom/broker/client-park.d.ts +9 -0
- package/dist/pi-intercom/broker/client-park.js +21 -0
- package/dist/pi-intercom/broker/client.d.ts +59 -0
- package/dist/pi-intercom/broker/client.js +55 -29
- package/dist/pi-intercom/broker/connection.d.ts +29 -0
- package/dist/pi-intercom/broker/framing.d.ts +17 -0
- package/dist/pi-intercom/broker/framing.js +10 -7
- package/dist/pi-intercom/broker/guard.js +23 -0
- package/dist/pi-intercom/broker/last.d.ts +30 -0
- package/dist/pi-intercom/broker/last.js +87 -40
- package/dist/pi-intercom/broker/parked.js +107 -0
- package/dist/pi-intercom/broker/paths.d.ts +13 -0
- package/dist/pi-intercom/broker/paths.js +35 -12
- package/dist/pi-intercom/broker/pid.js +5 -1
- package/dist/pi-intercom/broker/server-connection.js +20 -1
- package/dist/pi-intercom/broker/snapshot-reader.d.ts +9 -0
- package/dist/pi-intercom/broker/snapshots.d.ts +29 -0
- package/dist/pi-intercom/broker/targets.d.ts +9 -0
- package/dist/pi-intercom/connection.js +71 -20
- package/dist/pi-intercom/identity.js +6 -5
- package/dist/pi-intercom/inbound-delivery.js +16 -60
- package/dist/pi-intercom/inbound-journal.js +83 -6
- package/dist/pi-intercom/inbound-record.js +18 -1
- package/dist/pi-intercom/inbound.js +22 -37
- package/dist/pi-intercom/index.js +41 -0
- package/dist/pi-intercom/last-message.js +4 -16
- package/dist/pi-intercom/lifecycle.js +88 -3
- package/dist/pi-intercom/outbound.js +5 -15
- package/dist/pi-intercom/reply-tracker.js +7 -6
- package/dist/pi-intercom/reply-wait.js +2 -2
- package/dist/pi-intercom/session-targets.d.ts +24 -0
- package/dist/pi-intercom/tool-registration.js +4 -1
- package/dist/pi-intercom/tool-results.js +5 -7
- package/dist/pi-intercom/tool-schemas.js +12 -26
- package/dist/pi-intercom/types.d.ts +212 -0
- package/dist/pi-intercom/types.js +54 -3
- package/dist/pi-intercom/validation.d.ts +12 -0
- package/dist/pi-intercom/validation.js +3 -0
- package/dist/shared/bounded-json.js +151 -0
- package/dist/shared/compact-cards.js +66 -0
- package/dist/shared/journal-frames.js +3 -6
- package/dist/shared/json-projection.js +267 -129
- package/dist/shared/native-journal.js +123 -56
- package/dist/shared/parking.js +12 -0
- package/dist/shared/post-exit-stdio-guard.js +120 -7
- package/dist/shared/tui-action.js +29 -0
- package/dist/shared/unknown.d.ts +9 -0
- package/docs/code-quality.md +5 -3
- package/docs/development.md +42 -3
- package/docs/intercom-bridge.md +9 -4
- package/docs/intercom.md +228 -7
- package/docs/performance.md +118 -14
- package/docs/reference.md +168 -20
- package/docs/retired-history-maintenance.md +252 -0
- package/package.json +28 -15
- package/scripts/archive-retired-history.mjs +464 -0
- package/scripts/build.mjs +48 -28
- package/scripts/measure-size.mjs +1 -1
- package/scripts/native-package-smoke.mjs +153 -12
- package/scripts/package-smoke.mjs +80 -11
- package/scripts/prepare.mjs +2 -2
- package/scripts/real-pi-live.mjs +377 -0
- package/scripts/real-pi-processes.mjs +6 -1
- package/scripts/real-pi-smoke.mjs +6 -282
- package/scripts/run-tests.mjs +4 -2
- package/scripts/typescript.mjs +35 -0
- package/skills/pi-intercom/SKILL.md +2 -1
- package/skills/pi-subagents/SKILL.md +23 -5
- package/src/agent/child-runtime.ts +105 -0
- package/src/agent/contracts.ts +18 -34
- package/src/agent/host.ts +39 -20
- package/src/agent/index.ts +234 -207
- package/src/agent/journal-tail.ts +141 -153
- package/src/agent/launch.ts +33 -9
- package/src/agent/legacy.ts +85 -31
- package/src/agent/notifications.ts +192 -67
- package/src/agent/observation-state.ts +277 -0
- package/src/agent/observation-view.ts +267 -0
- package/src/agent/observation.ts +409 -0
- package/src/agent/observe-card.ts +304 -0
- package/src/agent/observe-chrome.ts +170 -0
- package/src/agent/observe-client.ts +318 -0
- package/src/agent/observe-layout.ts +79 -0
- package/src/agent/observe-reading.ts +97 -0
- package/src/agent/observe-screen.ts +443 -129
- package/src/agent/observe-transcript.ts +434 -0
- package/src/agent/observe.ts +304 -71
- package/src/agent/parking.ts +341 -0
- package/src/agent/process.ts +280 -37
- package/src/agent/receipts.ts +114 -24
- package/src/agent/runner.ts +227 -149
- package/src/agent/shell.ts +2 -1
- package/src/agent/storage.ts +44 -11
- package/src/agent/tool-schemas.ts +33 -0
- package/src/agent/typebox.ts +12 -7
- package/src/agent/usage.ts +0 -130
- package/src/agent/view.ts +70 -20
- package/src/agent/wake.ts +107 -0
- package/src/intercom-client.ts +108 -0
- package/src/pi-intercom/bridge-session.ts +2 -2
- package/src/pi-intercom/broker/broker.ts +64 -53
- package/src/pi-intercom/broker/client-last.ts +65 -12
- package/src/pi-intercom/broker/client-park.ts +23 -0
- package/src/pi-intercom/broker/client.ts +63 -45
- package/src/pi-intercom/broker/framing.ts +11 -8
- package/src/pi-intercom/broker/guard.ts +33 -0
- package/src/pi-intercom/broker/last.ts +131 -43
- package/src/pi-intercom/broker/parked.ts +130 -0
- package/src/pi-intercom/broker/paths.ts +39 -12
- package/src/pi-intercom/broker/pid.ts +6 -1
- package/src/pi-intercom/broker/server-connection.ts +27 -2
- package/src/pi-intercom/connection.ts +103 -39
- package/src/pi-intercom/identity.ts +8 -5
- package/src/pi-intercom/inbound-delivery.ts +20 -66
- package/src/pi-intercom/inbound-journal.ts +103 -7
- package/src/pi-intercom/inbound-record.ts +21 -4
- package/src/pi-intercom/inbound.ts +32 -49
- package/src/pi-intercom/index.ts +44 -0
- package/src/pi-intercom/last-message.ts +11 -20
- package/src/pi-intercom/lifecycle.ts +117 -14
- package/src/pi-intercom/outbound.ts +7 -16
- package/src/pi-intercom/reply-tracker.ts +8 -6
- package/src/pi-intercom/reply-wait.ts +3 -2
- package/src/pi-intercom/runtime-types.ts +0 -1
- package/src/pi-intercom/tool-registration.ts +4 -1
- package/src/pi-intercom/tool-results.ts +10 -9
- package/src/pi-intercom/tool-schemas.ts +12 -44
- package/src/pi-intercom/types.ts +115 -4
- package/src/pi-intercom/validation.ts +4 -0
- package/src/shared/bounded-json.ts +162 -0
- package/src/shared/compact-cards.ts +118 -0
- package/src/shared/journal-frames.ts +3 -19
- package/src/shared/json-projection.ts +267 -147
- package/src/shared/native-journal.ts +157 -75
- package/src/shared/parking.ts +22 -0
- package/src/shared/post-exit-stdio-guard.ts +165 -7
- package/src/shared/tui-action.ts +39 -0
- package/tsconfig.client.json +8 -0
- package/tsconfig.json +1 -0
- package/src/shared/stream-json.d.ts +0 -10
package/dist/agent/index.js
CHANGED
|
@@ -1,25 +1,64 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
|
-
import {
|
|
5
|
-
import { actor, alive, ensureActor, ownIdentity, signalProcess, verifyProcess, shells, } from "./process.js";
|
|
4
|
+
import { actor, alive, ensureActor, ownIdentity, parkFailure, requestStop, signalProcess, stopChildren, } from "./process.js";
|
|
6
5
|
import { isChild, errorText } from "./host.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
6
|
+
import { serveParked } from "./parking.js";
|
|
7
|
+
import { resultSchema, attachmentSchema, admissionSchema, commandSchema, } from "./contracts.js";
|
|
8
|
+
import { delegateSchema, runsSchema } from "./tool-schemas.js";
|
|
9
|
+
import { enqueue, latest, launchAt, list, read, runDirectory, ownerDirectory, conversation, successor, write, } from "./storage.js";
|
|
10
|
+
import { notifications, deliver, settle } from "./notifications.js";
|
|
10
11
|
import { prepareLaunch } from "./launch.js";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
12
|
+
import { renderReceipt, renderInventory, renderInspection } from "./view.js";
|
|
13
|
+
import { legacyDirectory, legacyNotifications } from "./legacy.js";
|
|
14
|
+
import { DirectoryWatch, pathStamp } from "./wake.js";
|
|
14
15
|
import { savedContinuation } from "./saved.js";
|
|
15
16
|
import { executionCwd } from "./cwd.js";
|
|
16
17
|
import { getPiAgentDir } from "../pi-intercom/agent-dir.js";
|
|
18
|
+
import { registerCompactMessages, renderCompactResult } from "../shared/compact-cards.js";
|
|
19
|
+
/** `handoff` and `provisional` mean another worker under the same launcher may still own these children. */
|
|
20
|
+
function retainsChildren(event) {
|
|
21
|
+
const disposition = Reflect.get(event, "disposition");
|
|
22
|
+
return disposition === "handoff" || disposition === "provisional";
|
|
23
|
+
}
|
|
24
|
+
function managedHost(context) {
|
|
25
|
+
const host = Reflect.get(context, "managedHost");
|
|
26
|
+
if (typeof host !== "object" || host === null) {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
const owner = Reflect.get(host, "owner");
|
|
30
|
+
const pid = typeof owner === "object" && owner !== null ? Reflect.get(owner, "pid") : undefined;
|
|
31
|
+
const ready = Reflect.get(host, "ready");
|
|
32
|
+
return typeof pid === "number" && Number.isSafeInteger(pid) && ready instanceof Promise
|
|
33
|
+
? { owner: { pid }, ready: ready.then((accepted) => accepted === true) }
|
|
34
|
+
: undefined;
|
|
35
|
+
}
|
|
36
|
+
/** Legacy results are optional; their reconciliation reports an invalid configured root. */
|
|
37
|
+
function legacyInput() {
|
|
38
|
+
try {
|
|
39
|
+
return [legacyDirectory()];
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
17
45
|
class AgentRuntime {
|
|
18
46
|
ctx;
|
|
47
|
+
// A managed launcher owns children across worker replacement; otherwise this process does.
|
|
48
|
+
host;
|
|
49
|
+
accepted = false;
|
|
19
50
|
base = "";
|
|
20
51
|
owner = "";
|
|
21
52
|
generation = "";
|
|
53
|
+
// Polling remains only while change hints are unavailable or a reconciliation failed.
|
|
22
54
|
timer;
|
|
55
|
+
woken;
|
|
56
|
+
// Change hints for the owner directory, legacy results and every awaited canonical file.
|
|
57
|
+
watches = new Map();
|
|
58
|
+
awaited = new Set();
|
|
59
|
+
// Input stamps the last reconciliation saw; a one-second probe recovers dropped hints.
|
|
60
|
+
observed = new Map();
|
|
61
|
+
probe;
|
|
23
62
|
closing = false;
|
|
24
63
|
reset = Promise.resolve();
|
|
25
64
|
shutdown = new AbortController();
|
|
@@ -37,24 +76,120 @@ class AgentRuntime {
|
|
|
37
76
|
throw new Error("Agent runtime is not attached.");
|
|
38
77
|
}
|
|
39
78
|
}
|
|
79
|
+
wake = () => {
|
|
80
|
+
this.woken ??= setImmediate(() => {
|
|
81
|
+
this.woken = undefined;
|
|
82
|
+
this.scan();
|
|
83
|
+
}).unref();
|
|
84
|
+
};
|
|
85
|
+
recover = () => {
|
|
86
|
+
for (const [file, seen] of this.observed) {
|
|
87
|
+
if (pathStamp(file) !== seen) {
|
|
88
|
+
this.wake();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
};
|
|
40
93
|
scan = () => {
|
|
94
|
+
clearTimeout(this.timer);
|
|
41
95
|
if (this.closing || !this.ctx || this.child) {
|
|
42
96
|
return;
|
|
43
97
|
}
|
|
98
|
+
const inputs = () => [this.base, ...legacyInput(), ...this.awaited];
|
|
99
|
+
const before = new Map(inputs().map((file) => [file, pathStamp(file)]));
|
|
100
|
+
let reconciled = true;
|
|
44
101
|
try {
|
|
45
|
-
|
|
46
|
-
|
|
102
|
+
const { notices, awaited } = notifications(this.ctx, this.base, this.owner);
|
|
103
|
+
this.awaited = awaited;
|
|
104
|
+
try {
|
|
105
|
+
notices.unshift(...legacyNotifications(this.ctx));
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
console.error(`Legacy completion reconciliation: ${errorText(error)}`);
|
|
109
|
+
}
|
|
110
|
+
deliver(this.pi, this.ctx, notices);
|
|
47
111
|
}
|
|
48
112
|
catch (error) {
|
|
113
|
+
reconciled = false;
|
|
49
114
|
console.error(`Agent completion reconciliation: ${errorText(error)}`);
|
|
50
115
|
}
|
|
116
|
+
// Newly awaited files were absent when read, or are rechecked once by the probe.
|
|
117
|
+
this.observed = new Map(inputs().map((file) => [file, before.get(file) ?? ""]));
|
|
118
|
+
const next = this.watch();
|
|
119
|
+
if (!reconciled || next === "poll") {
|
|
120
|
+
this.timer = setTimeout(this.scan, 200).unref();
|
|
121
|
+
}
|
|
122
|
+
else if (next === "rescan") {
|
|
123
|
+
// Publications before arming had no hint; reconcile once more after it.
|
|
124
|
+
this.wake();
|
|
125
|
+
}
|
|
51
126
|
};
|
|
127
|
+
/** Watches every directory that can publish an awaited input; legacy results may never exist. */
|
|
128
|
+
watch() {
|
|
129
|
+
const legacy = legacyInput();
|
|
130
|
+
const directories = [
|
|
131
|
+
...new Set([this.base, ...legacy, ...[...this.awaited].map((file) => path.dirname(file))]),
|
|
132
|
+
];
|
|
133
|
+
for (const [directory, watch] of this.watches) {
|
|
134
|
+
if (!directories.includes(directory)) {
|
|
135
|
+
watch.close();
|
|
136
|
+
this.watches.delete(directory);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const states = directories.map((directory) => {
|
|
140
|
+
let watch = this.watches.get(directory);
|
|
141
|
+
if (watch === undefined) {
|
|
142
|
+
const every = directory === this.base || legacy.includes(directory);
|
|
143
|
+
watch = new DirectoryWatch(directory, (file) => every || file === null || this.awaited.has(path.join(directory, file)), this.wake);
|
|
144
|
+
this.watches.set(directory, watch);
|
|
145
|
+
}
|
|
146
|
+
return [directory, watch.ensure()];
|
|
147
|
+
});
|
|
148
|
+
if (states.some(([directory, state]) => state === "unwatched" && !legacy.includes(directory))) {
|
|
149
|
+
return "poll";
|
|
150
|
+
}
|
|
151
|
+
return states.some(([, state]) => state === "armed") ? "rescan" : "hints";
|
|
152
|
+
}
|
|
153
|
+
unwatch() {
|
|
154
|
+
clearTimeout(this.timer);
|
|
155
|
+
clearInterval(this.probe);
|
|
156
|
+
this.observed.clear();
|
|
157
|
+
clearImmediate(this.woken);
|
|
158
|
+
this.woken = undefined;
|
|
159
|
+
for (const watch of this.watches.values()) {
|
|
160
|
+
watch.close();
|
|
161
|
+
}
|
|
162
|
+
this.watches.clear();
|
|
163
|
+
this.awaited = new Set();
|
|
164
|
+
}
|
|
52
165
|
start(context) {
|
|
53
|
-
this.
|
|
54
|
-
|
|
166
|
+
this.host = managedHost(context);
|
|
167
|
+
if (this.host === undefined || this.accepted || this.child) {
|
|
168
|
+
this.reset = this.reset.then(() => this.attach(context));
|
|
169
|
+
return this.reset;
|
|
170
|
+
}
|
|
171
|
+
// Attach and publish only after the launcher retires rollback: an unready replacement
|
|
172
|
+
// must neither adopt children nor append completions to a journal it may abandon.
|
|
173
|
+
// Readiness follows session_start, so never await it here.
|
|
174
|
+
this.owner = context.sessionManager.getSessionId();
|
|
175
|
+
this.base = ownerDirectory(this.agentDir, this.owner);
|
|
176
|
+
this.host.ready
|
|
177
|
+
.then(async (accepted) => {
|
|
178
|
+
if (accepted && !this.closing) {
|
|
179
|
+
this.accepted = true;
|
|
180
|
+
await this.start(context);
|
|
181
|
+
}
|
|
182
|
+
})
|
|
183
|
+
.catch((error) => {
|
|
184
|
+
console.error(`Agent runtime attachment: ${errorText(error)}`);
|
|
185
|
+
});
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
ownerIdentity() {
|
|
189
|
+
return ownIdentity(this.host?.owner.pid);
|
|
55
190
|
}
|
|
56
191
|
async attach(context) {
|
|
57
|
-
|
|
192
|
+
this.unwatch();
|
|
58
193
|
this.ctx = context;
|
|
59
194
|
if (this.child) {
|
|
60
195
|
return;
|
|
@@ -64,54 +199,52 @@ class AgentRuntime {
|
|
|
64
199
|
this.generation = randomUUID();
|
|
65
200
|
await fs.promises.mkdir(this.base, { recursive: true, mode: 0o700 });
|
|
66
201
|
write(path.join(this.base, "attachment.json"), {
|
|
67
|
-
...
|
|
202
|
+
...this.ownerIdentity(),
|
|
68
203
|
generation: this.generation,
|
|
69
204
|
attached: true,
|
|
70
205
|
});
|
|
71
206
|
if (this.closing) {
|
|
72
207
|
return;
|
|
73
208
|
}
|
|
74
|
-
|
|
75
|
-
this.timer.unref();
|
|
76
|
-
this.
|
|
209
|
+
// Recovery and watch setup stay off the awaited startup path.
|
|
210
|
+
this.timer = setTimeout(this.scan, 200).unref();
|
|
211
|
+
this.probe = setInterval(this.recover, 1000).unref();
|
|
77
212
|
}
|
|
78
|
-
async dispose() {
|
|
213
|
+
async dispose(retain) {
|
|
79
214
|
this.closing = true;
|
|
80
|
-
|
|
215
|
+
this.unwatch();
|
|
81
216
|
this.shutdown.abort();
|
|
82
217
|
await this.reset;
|
|
83
|
-
if (this.base.length > 0) {
|
|
218
|
+
if (!retain && this.base.length > 0) {
|
|
84
219
|
const file = path.join(this.base, "attachment.json");
|
|
85
|
-
|
|
86
|
-
|
|
220
|
+
const attachment = read(file, attachmentSchema);
|
|
221
|
+
if (attachment !== undefined && this.releases(attachment)) {
|
|
222
|
+
write(file, {
|
|
223
|
+
...this.ownerIdentity(),
|
|
224
|
+
generation: attachment.generation,
|
|
225
|
+
attached: false,
|
|
226
|
+
});
|
|
227
|
+
await stopChildren(this.base, this.owner);
|
|
87
228
|
}
|
|
88
229
|
}
|
|
89
230
|
this.ctx = undefined;
|
|
90
231
|
}
|
|
232
|
+
releases(attachment) {
|
|
233
|
+
if (attachment.generation === this.generation) {
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
// An unready replacement never attached, but exiting still releases the launcher-owned children.
|
|
237
|
+
return (this.generation.length === 0 && attachment.attached && attachment.pid === this.host?.owner.pid);
|
|
238
|
+
}
|
|
91
239
|
stop(launch) {
|
|
92
240
|
const base = this.base;
|
|
93
|
-
const
|
|
94
|
-
const targets = identity ? [identity, ...shells(base, launch, identity.token)] : [];
|
|
95
|
-
// Verify all targets before admitting a request or signaling any process.
|
|
96
|
-
const live = targets.filter(verifyProcess);
|
|
241
|
+
const { targets: live, command: id } = requestStop(base, launch);
|
|
97
242
|
if (live.length === 0) {
|
|
98
|
-
return {
|
|
99
|
-
...projection(base, launch, this.ctx),
|
|
243
|
+
return renderReceipt(base, launch, this.ctx, {
|
|
100
244
|
stopAcknowledged: false,
|
|
101
245
|
note: "No matching live actor or owned shell; process exit code remains unknown.",
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
const id = identity && verifyProcess(identity)
|
|
105
|
-
? enqueue(base, launch, "stop", { actorToken: identity.token })
|
|
106
|
-
: undefined;
|
|
107
|
-
if (identity) {
|
|
108
|
-
write(path.join(conversation(base, launch), `stopping-${identity.token}.json`), {
|
|
109
|
-
at: Date.now(),
|
|
110
246
|
});
|
|
111
247
|
}
|
|
112
|
-
live.forEach((target) => {
|
|
113
|
-
signalProcess(target, "SIGTERM");
|
|
114
|
-
});
|
|
115
248
|
const escalation = setTimeout(() => {
|
|
116
249
|
try {
|
|
117
250
|
live.forEach((target) => {
|
|
@@ -123,62 +256,11 @@ class AgentRuntime {
|
|
|
123
256
|
}
|
|
124
257
|
}, 3000);
|
|
125
258
|
escalation.unref();
|
|
126
|
-
return {
|
|
127
|
-
...projection(base, launch, this.ctx),
|
|
259
|
+
return renderReceipt(base, launch, this.ctx, {
|
|
128
260
|
stopRequest: id,
|
|
129
261
|
stopAcknowledged: false,
|
|
130
262
|
note: "Stop requested; neither native admission nor process exit is implied.",
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
interrupted(launch, signal, owned) {
|
|
134
|
-
if (this.shutdown.signal.aborted) {
|
|
135
|
-
return toolResult({
|
|
136
|
-
...projection(this.base, launch, this.ctx),
|
|
137
|
-
wait: "detached",
|
|
138
|
-
note: "Parent runtime disposed; producer was not stopped.",
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
if (signal?.aborted === true) {
|
|
142
|
-
if (owned) {
|
|
143
|
-
this.stop(launch);
|
|
144
|
-
}
|
|
145
|
-
return toolResult({
|
|
146
|
-
...projection(this.base, launch, this.ctx),
|
|
147
|
-
wait: "cancelled",
|
|
148
|
-
note: owned
|
|
149
|
-
? "Owned foreground work was asked to stop; exit is unconfirmed."
|
|
150
|
-
: "Only the wait was cancelled; producer was not stopped.",
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
const identity = actor(this.base, launch);
|
|
154
|
-
if (identity && !alive(identity)) {
|
|
155
|
-
return toolResult({
|
|
156
|
-
...projection(this.base, launch, this.ctx),
|
|
157
|
-
wait: "interrupted",
|
|
158
|
-
note: "Actor is not live; no completion or exit code was invented.",
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
async wait(launch, signal, owned) {
|
|
164
|
-
const held = hold(this.owner, launch.id);
|
|
165
|
-
for (;;) {
|
|
166
|
-
const result = read(path.join(runDirectory(this.base, launch.id), "result.json"), resultSchema);
|
|
167
|
-
if (result) {
|
|
168
|
-
hold(this.owner, launch.id);
|
|
169
|
-
return render(this.base, launch, this.ctx, { receivedResult: true });
|
|
170
|
-
}
|
|
171
|
-
const interruption = this.interrupted(launch, signal, owned);
|
|
172
|
-
if (interruption) {
|
|
173
|
-
if (held) {
|
|
174
|
-
release(this.owner, launch.id);
|
|
175
|
-
}
|
|
176
|
-
return interruption;
|
|
177
|
-
}
|
|
178
|
-
// Cancellation of an observer never removes canonical producer state.
|
|
179
|
-
// oxlint-disable-next-line no-await-in-loop
|
|
180
|
-
await delay(50);
|
|
181
|
-
}
|
|
263
|
+
});
|
|
182
264
|
}
|
|
183
265
|
async delegate(input, signal, context) {
|
|
184
266
|
this.checkMain();
|
|
@@ -197,45 +279,17 @@ class AgentRuntime {
|
|
|
197
279
|
});
|
|
198
280
|
this.checkMain();
|
|
199
281
|
enqueue(this.base, launch, "run");
|
|
200
|
-
await ensureActor(this.base, launch);
|
|
201
|
-
|
|
202
|
-
return this.wait(launch, signal, true);
|
|
203
|
-
}
|
|
204
|
-
return toolResult({
|
|
205
|
-
...projection(this.base, launch, this.ctx),
|
|
282
|
+
await ensureActor(this.base, launch, this.shutdown.signal);
|
|
283
|
+
return renderReceipt(this.base, launch, this.ctx, {
|
|
206
284
|
receipt: "launch",
|
|
207
|
-
note: "Launch accepted, not
|
|
285
|
+
note: "Launch accepted, not completion or native receipt publication. Completion arrives as a notification; do other work or end the turn instead of polling.",
|
|
208
286
|
});
|
|
209
287
|
}
|
|
210
288
|
list(input) {
|
|
211
289
|
const all = list(this.base)
|
|
212
290
|
.map((id) => launchAt(this.base, id, this.owner))
|
|
213
291
|
.sort((a, b) => a.createdAt - b.createdAt);
|
|
214
|
-
|
|
215
|
-
const page = summaries(this.base, all.slice(offset, offset + 50), this.ctx);
|
|
216
|
-
return toolResult({
|
|
217
|
-
runs: page,
|
|
218
|
-
offset,
|
|
219
|
-
nextOffset: offset + page.length < all.length ? offset + page.length : undefined,
|
|
220
|
-
total: all.length,
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
successor(launch, prompt) {
|
|
224
|
-
const id = randomUUID();
|
|
225
|
-
const next = { ...launch, id, predecessor: launch.id, prompt, createdAt: Date.now() };
|
|
226
|
-
fs.mkdirSync(runDirectory(this.base, id), { mode: 0o700 });
|
|
227
|
-
write(path.join(runDirectory(this.base, id), "launch.json"), next, true);
|
|
228
|
-
try {
|
|
229
|
-
write(path.join(runDirectory(this.base, launch.id), "successor.json"), { id }, true);
|
|
230
|
-
}
|
|
231
|
-
catch (error) {
|
|
232
|
-
fs.rmSync(runDirectory(this.base, id), { recursive: true });
|
|
233
|
-
if (!(error instanceof Error && "code" in error && error.code === "EEXIST")) {
|
|
234
|
-
throw error;
|
|
235
|
-
}
|
|
236
|
-
return undefined;
|
|
237
|
-
}
|
|
238
|
-
return next;
|
|
292
|
+
return renderInventory(this.base, all, this.ctx, input);
|
|
239
293
|
}
|
|
240
294
|
continuationResult(launch) {
|
|
241
295
|
const result = read(path.join(runDirectory(this.base, launch.id), "result.json"), resultSchema);
|
|
@@ -248,7 +302,7 @@ class AgentRuntime {
|
|
|
248
302
|
}
|
|
249
303
|
return;
|
|
250
304
|
}
|
|
251
|
-
async retryContinuation(launch, input
|
|
305
|
+
async retryContinuation(launch, input) {
|
|
252
306
|
const identity = actor(this.base, launch);
|
|
253
307
|
const command = read(path.join(conversation(this.base, launch), "commands", `${launch.id}.json`), commandSchema);
|
|
254
308
|
if (input.prompt !== launch.prompt ||
|
|
@@ -256,16 +310,13 @@ class AgentRuntime {
|
|
|
256
310
|
command?.id !== launch.id ||
|
|
257
311
|
command.runId !== launch.id ||
|
|
258
312
|
command.action !== "run") {
|
|
259
|
-
throw new Error(`Run ${launch.id} is awaiting native admission. Retry requires the exact saved prompt, its existing queued Run command, and no live actor. Missing commands or ambiguous process claims require inspection
|
|
313
|
+
throw new Error(`Run ${launch.id} is awaiting native admission. Retry requires the exact saved prompt, its existing queued Run command, and no live actor. Missing commands or ambiguous process claims require inspection. Admission or a canonical result arrives without polling: end the turn or do other work until the completion notification, then inspect once before sending another prompt. No new continuation was launched.`);
|
|
260
314
|
}
|
|
261
315
|
// Only an unadmitted durable command is retryable; keep all process/shell claim guards.
|
|
262
|
-
await ensureActor(this.base, launch);
|
|
263
|
-
|
|
264
|
-
return this.wait(launch, signal, true);
|
|
265
|
-
}
|
|
266
|
-
return toolResult({ ...projection(this.base, launch, this.ctx), receipt: "continuation" });
|
|
316
|
+
await ensureActor(this.base, launch, this.shutdown.signal);
|
|
317
|
+
return renderReceipt(this.base, launch, this.ctx, { receipt: "continuation" });
|
|
267
318
|
}
|
|
268
|
-
async continue(launch, input
|
|
319
|
+
async continue(launch, input) {
|
|
269
320
|
if (input.prompt === undefined) {
|
|
270
321
|
throw new Error("continue requires the entire new prompt.");
|
|
271
322
|
}
|
|
@@ -274,61 +325,30 @@ class AgentRuntime {
|
|
|
274
325
|
}
|
|
275
326
|
const completed = this.continuationResult(launch);
|
|
276
327
|
if (completed === "pending") {
|
|
277
|
-
return this.retryContinuation(launch, input
|
|
328
|
+
return this.retryContinuation(launch, input);
|
|
278
329
|
}
|
|
279
330
|
const identity = actor(this.base, launch);
|
|
280
331
|
if (!completed && identity && alive(identity)) {
|
|
281
332
|
const command = enqueue(this.base, launch, "steer", { prompt: input.prompt });
|
|
282
|
-
|
|
283
|
-
return this.guidance(launch, command, signal);
|
|
284
|
-
}
|
|
285
|
-
return toolResult({
|
|
286
|
-
...projection(this.base, launch, this.ctx),
|
|
333
|
+
return renderReceipt(this.base, launch, this.ctx, {
|
|
287
334
|
receipt: "guidance",
|
|
288
335
|
command,
|
|
289
336
|
note: "Guidance queued, not actual completion or model consumption.",
|
|
290
337
|
});
|
|
291
338
|
}
|
|
292
|
-
|
|
339
|
+
parkFailure(this.base, launch);
|
|
340
|
+
const next = successor(this.base, launch, input.prompt);
|
|
293
341
|
if (!next) {
|
|
294
|
-
return
|
|
295
|
-
...projection(this.base, latest(this.base, launch), this.ctx),
|
|
342
|
+
return renderReceipt(this.base, latest(this.base, launch), this.ctx, {
|
|
296
343
|
receipt: "existing-successor",
|
|
297
344
|
});
|
|
298
345
|
}
|
|
299
346
|
// Retain successor claims on ambiguous after-launch failures: no replay.
|
|
300
347
|
enqueue(this.base, next, "run");
|
|
301
|
-
await ensureActor(this.base, next);
|
|
302
|
-
|
|
303
|
-
return this.wait(next, signal, true);
|
|
304
|
-
}
|
|
305
|
-
return toolResult({ ...projection(this.base, next, this.ctx), receipt: "continuation" });
|
|
348
|
+
await ensureActor(this.base, next, this.shutdown.signal);
|
|
349
|
+
return renderReceipt(this.base, next, this.ctx, { receipt: "continuation" });
|
|
306
350
|
}
|
|
307
|
-
async
|
|
308
|
-
const file = path.join(conversation(this.base, launch), "admissions", `${command}.json`);
|
|
309
|
-
for (;;) {
|
|
310
|
-
const admission = read(file, admissionSchema);
|
|
311
|
-
if (admission) {
|
|
312
|
-
if (admission.state === "stale") {
|
|
313
|
-
return toolResult({
|
|
314
|
-
...projection(this.base, launch, this.ctx),
|
|
315
|
-
command,
|
|
316
|
-
guidance: "stale",
|
|
317
|
-
note: "The native turn settled before guidance admission. Retry the entire prompt as a continuation; no new work was started.",
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
return this.wait(launch, signal, false);
|
|
321
|
-
}
|
|
322
|
-
const interruption = this.interrupted(launch, signal, false);
|
|
323
|
-
if (interruption) {
|
|
324
|
-
return interruption;
|
|
325
|
-
}
|
|
326
|
-
// Observe the actor's actual admission, not a timer-based model-consumption claim.
|
|
327
|
-
// oxlint-disable-next-line no-await-in-loop
|
|
328
|
-
await delay(50);
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
async continueSaved(input, signal, context) {
|
|
351
|
+
async continueSaved(input, context) {
|
|
332
352
|
if (input.prompt === undefined) {
|
|
333
353
|
throw new Error("continue requires the entire new prompt.");
|
|
334
354
|
}
|
|
@@ -342,17 +362,13 @@ class AgentRuntime {
|
|
|
342
362
|
if (created) {
|
|
343
363
|
// The durable adoption claim survives ambiguous after-launch failures.
|
|
344
364
|
enqueue(this.base, launch, "run");
|
|
345
|
-
await ensureActor(this.base, launch);
|
|
346
|
-
}
|
|
347
|
-
if (input.async === false) {
|
|
348
|
-
return this.wait(launch, signal, created);
|
|
365
|
+
await ensureActor(this.base, launch, this.shutdown.signal);
|
|
349
366
|
}
|
|
350
|
-
return
|
|
351
|
-
...projection(this.base, launch, this.ctx),
|
|
367
|
+
return renderReceipt(this.base, launch, this.ctx, {
|
|
352
368
|
receipt: created ? "continuation" : "existing-successor",
|
|
353
369
|
});
|
|
354
370
|
}
|
|
355
|
-
async runs(input,
|
|
371
|
+
async runs(input, context) {
|
|
356
372
|
this.checkMain();
|
|
357
373
|
await this.reset;
|
|
358
374
|
if (input.action === "list") {
|
|
@@ -364,31 +380,33 @@ class AgentRuntime {
|
|
|
364
380
|
}
|
|
365
381
|
if (input.action === "continue" &&
|
|
366
382
|
!fs.existsSync(path.join(runDirectory(this.base, input.id), "launch.json"))) {
|
|
367
|
-
return this.continueSaved({ ...input, id: input.id },
|
|
383
|
+
return this.continueSaved({ ...input, id: input.id }, context);
|
|
368
384
|
}
|
|
369
385
|
const original = launchAt(this.base, input.id, this.owner);
|
|
370
386
|
if (input.action === "inspect") {
|
|
371
|
-
return
|
|
387
|
+
return renderInspection(this.base, original, this.ctx, input);
|
|
372
388
|
}
|
|
373
389
|
const launch = latest(this.base, original);
|
|
374
390
|
if (input.action === "stop") {
|
|
375
|
-
return
|
|
391
|
+
return this.stop(launch);
|
|
376
392
|
}
|
|
377
|
-
|
|
378
|
-
return this.wait(launch, signal, false);
|
|
379
|
-
}
|
|
380
|
-
return this.continue(launch, input, signal);
|
|
393
|
+
return this.continue(launch, input);
|
|
381
394
|
}
|
|
382
395
|
register() {
|
|
383
|
-
|
|
396
|
+
registerCompactMessages(this.pi, ["agent-completion", "subagent-notify"]);
|
|
397
|
+
serveParked(this.pi, () => (this.child || !this.ctx ? undefined : { base: this.base, owner: this.owner }), this.shutdown.signal);
|
|
384
398
|
this.pi.on("session_start", (_event, context) => this.start(context));
|
|
385
|
-
this.pi.on("turn_end", (event) => {
|
|
386
|
-
|
|
399
|
+
this.pi.on("turn_end", (event, context) => {
|
|
400
|
+
// The public pending flag covers user input; the preview also includes custom messages.
|
|
401
|
+
if (settle(this.owner, context.hasPendingMessages() || event.context.pendingMessages.length > 0)) {
|
|
402
|
+
// Released admissions may have been dropped from the queue rather than published.
|
|
403
|
+
this.wake();
|
|
404
|
+
}
|
|
387
405
|
});
|
|
388
406
|
this.pi.on("agent_settled", () => {
|
|
389
407
|
this.scan();
|
|
390
408
|
});
|
|
391
|
-
this.pi.on("session_shutdown", () => this.dispose());
|
|
409
|
+
this.pi.on("session_shutdown", (event) => this.dispose(event.reason === "reload" || retainsChildren(event)));
|
|
392
410
|
if (this.child) {
|
|
393
411
|
this.pi.on("tool_call", (event) => {
|
|
394
412
|
if (event.toolName === "delegate" || event.toolName === "agent_runs") {
|
|
@@ -401,18 +419,20 @@ class AgentRuntime {
|
|
|
401
419
|
this.pi.registerTool({
|
|
402
420
|
name: "delegate",
|
|
403
421
|
label: "Delegate",
|
|
404
|
-
description: "
|
|
422
|
+
description: "Run a child on exactly this prompt; main sessions only. Fork copies the full conversation; default is fresh. Returns a receipt; completion is notified later: do other work or end the turn, don't poll.",
|
|
405
423
|
parameters: delegateSchema,
|
|
406
424
|
exposure: this.child ? "hidden" : "direct",
|
|
425
|
+
renderResult: renderCompactResult,
|
|
407
426
|
execute,
|
|
408
427
|
});
|
|
409
|
-
const runs = (...args) => this.runs(args[1], args[
|
|
428
|
+
const runs = (...args) => this.runs(args[1], args[4]);
|
|
410
429
|
this.pi.registerTool({
|
|
411
430
|
name: "agent_runs",
|
|
412
431
|
label: "Agent runs",
|
|
413
|
-
description: "
|
|
432
|
+
description: "Manage your child runs without waiting; completions are notified. Inspect pages by byte offset, never interrupting. Continue takes the complete new prompt: it steers a live child or resumes the saved conversation.",
|
|
414
433
|
parameters: runsSchema,
|
|
415
434
|
exposure: this.child ? "hidden" : "direct",
|
|
435
|
+
renderResult: renderCompactResult,
|
|
416
436
|
execute: runs,
|
|
417
437
|
});
|
|
418
438
|
}
|