@fitchmultz/pi-subagents 0.50.0 → 0.51.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 +51 -2
- package/README.md +36 -5
- package/dist/agent/contracts.js +6 -24
- package/dist/agent/host.js +3 -16
- package/dist/agent/index.js +207 -173
- 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 +346 -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/process.js +198 -25
- package/dist/agent/receipts.js +92 -20
- package/dist/agent/runner.js +125 -20
- package/dist/agent/shell.js +2 -1
- package/dist/agent/storage.js +29 -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 +27 -21
- package/dist/pi-intercom/broker/client-last.d.ts +13 -0
- package/dist/pi-intercom/broker/client-last.js +17 -5
- package/dist/pi-intercom/broker/client.d.ts +60 -0
- package/dist/pi-intercom/broker/client.js +47 -16
- 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 +26 -0
- package/dist/pi-intercom/broker/last.js +57 -32
- 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 +19 -0
- 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 +10 -2
- 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 +12 -38
- package/dist/pi-intercom/index.js +3 -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/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 +2 -0
- package/docs/development.md +40 -1
- package/docs/intercom-bridge.md +9 -4
- package/docs/intercom.md +226 -6
- package/docs/performance.md +87 -11
- package/docs/reference.md +157 -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/contracts.ts +8 -34
- package/src/agent/host.ts +19 -20
- package/src/agent/index.ts +224 -190
- 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 +405 -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/process.ts +240 -34
- package/src/agent/receipts.ts +114 -24
- package/src/agent/runner.ts +125 -27
- package/src/agent/shell.ts +2 -1
- package/src/agent/storage.ts +27 -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 +27 -23
- package/src/pi-intercom/broker/client-last.ts +25 -12
- package/src/pi-intercom/broker/client.ts +55 -33
- 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 +86 -33
- 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 +25 -0
- package/src/pi-intercom/connection.ts +21 -10
- 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 +18 -49
- package/src/pi-intercom/index.ts +3 -0
- package/src/pi-intercom/last-message.ts +10 -19
- 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/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,63 @@
|
|
|
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, requestStop, signalProcess, stopChildren, } from "./process.js";
|
|
6
5
|
import { isChild, errorText } from "./host.js";
|
|
7
|
-
import {
|
|
6
|
+
import { resultSchema, attachmentSchema, admissionSchema, commandSchema, } from "./contracts.js";
|
|
7
|
+
import { delegateSchema, runsSchema } from "./tool-schemas.js";
|
|
8
8
|
import { enqueue, latest, launchAt, list, read, runDirectory, ownerDirectory, conversation, write, } from "./storage.js";
|
|
9
|
-
import {
|
|
9
|
+
import { notifications, deliver, settle } from "./notifications.js";
|
|
10
10
|
import { prepareLaunch } from "./launch.js";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
11
|
+
import { renderReceipt, renderInventory, renderInspection } from "./view.js";
|
|
12
|
+
import { legacyDirectory, legacyNotifications } from "./legacy.js";
|
|
13
|
+
import { DirectoryWatch, pathStamp } from "./wake.js";
|
|
14
14
|
import { savedContinuation } from "./saved.js";
|
|
15
15
|
import { executionCwd } from "./cwd.js";
|
|
16
16
|
import { getPiAgentDir } from "../pi-intercom/agent-dir.js";
|
|
17
|
+
import { registerCompactMessages, renderCompactResult } from "../shared/compact-cards.js";
|
|
18
|
+
/** `handoff` and `provisional` mean another worker under the same launcher may still own these children. */
|
|
19
|
+
function retainsChildren(event) {
|
|
20
|
+
const disposition = Reflect.get(event, "disposition");
|
|
21
|
+
return disposition === "handoff" || disposition === "provisional";
|
|
22
|
+
}
|
|
23
|
+
function managedHost(context) {
|
|
24
|
+
const host = Reflect.get(context, "managedHost");
|
|
25
|
+
if (typeof host !== "object" || host === null) {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
const owner = Reflect.get(host, "owner");
|
|
29
|
+
const pid = typeof owner === "object" && owner !== null ? Reflect.get(owner, "pid") : undefined;
|
|
30
|
+
const ready = Reflect.get(host, "ready");
|
|
31
|
+
return typeof pid === "number" && Number.isSafeInteger(pid) && ready instanceof Promise
|
|
32
|
+
? { owner: { pid }, ready: ready.then((accepted) => accepted === true) }
|
|
33
|
+
: undefined;
|
|
34
|
+
}
|
|
35
|
+
/** Legacy results are optional; their reconciliation reports an invalid configured root. */
|
|
36
|
+
function legacyInput() {
|
|
37
|
+
try {
|
|
38
|
+
return [legacyDirectory()];
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
17
44
|
class AgentRuntime {
|
|
18
45
|
ctx;
|
|
46
|
+
// A managed launcher owns children across worker replacement; otherwise this process does.
|
|
47
|
+
host;
|
|
48
|
+
accepted = false;
|
|
19
49
|
base = "";
|
|
20
50
|
owner = "";
|
|
21
51
|
generation = "";
|
|
52
|
+
// Polling remains only while change hints are unavailable or a reconciliation failed.
|
|
22
53
|
timer;
|
|
54
|
+
woken;
|
|
55
|
+
// Change hints for the owner directory, legacy results and every awaited canonical file.
|
|
56
|
+
watches = new Map();
|
|
57
|
+
awaited = new Set();
|
|
58
|
+
// Input stamps the last reconciliation saw; a one-second probe recovers dropped hints.
|
|
59
|
+
observed = new Map();
|
|
60
|
+
probe;
|
|
23
61
|
closing = false;
|
|
24
62
|
reset = Promise.resolve();
|
|
25
63
|
shutdown = new AbortController();
|
|
@@ -37,24 +75,120 @@ class AgentRuntime {
|
|
|
37
75
|
throw new Error("Agent runtime is not attached.");
|
|
38
76
|
}
|
|
39
77
|
}
|
|
78
|
+
wake = () => {
|
|
79
|
+
this.woken ??= setImmediate(() => {
|
|
80
|
+
this.woken = undefined;
|
|
81
|
+
this.scan();
|
|
82
|
+
}).unref();
|
|
83
|
+
};
|
|
84
|
+
recover = () => {
|
|
85
|
+
for (const [file, seen] of this.observed) {
|
|
86
|
+
if (pathStamp(file) !== seen) {
|
|
87
|
+
this.wake();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
};
|
|
40
92
|
scan = () => {
|
|
93
|
+
clearTimeout(this.timer);
|
|
41
94
|
if (this.closing || !this.ctx || this.child) {
|
|
42
95
|
return;
|
|
43
96
|
}
|
|
97
|
+
const inputs = () => [this.base, ...legacyInput(), ...this.awaited];
|
|
98
|
+
const before = new Map(inputs().map((file) => [file, pathStamp(file)]));
|
|
99
|
+
let reconciled = true;
|
|
44
100
|
try {
|
|
45
|
-
|
|
46
|
-
|
|
101
|
+
const { notices, awaited } = notifications(this.ctx, this.base, this.owner);
|
|
102
|
+
this.awaited = awaited;
|
|
103
|
+
try {
|
|
104
|
+
notices.unshift(...legacyNotifications(this.ctx));
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
console.error(`Legacy completion reconciliation: ${errorText(error)}`);
|
|
108
|
+
}
|
|
109
|
+
deliver(this.pi, this.ctx, notices);
|
|
47
110
|
}
|
|
48
111
|
catch (error) {
|
|
112
|
+
reconciled = false;
|
|
49
113
|
console.error(`Agent completion reconciliation: ${errorText(error)}`);
|
|
50
114
|
}
|
|
115
|
+
// Newly awaited files were absent when read, or are rechecked once by the probe.
|
|
116
|
+
this.observed = new Map(inputs().map((file) => [file, before.get(file) ?? ""]));
|
|
117
|
+
const next = this.watch();
|
|
118
|
+
if (!reconciled || next === "poll") {
|
|
119
|
+
this.timer = setTimeout(this.scan, 200).unref();
|
|
120
|
+
}
|
|
121
|
+
else if (next === "rescan") {
|
|
122
|
+
// Publications before arming had no hint; reconcile once more after it.
|
|
123
|
+
this.wake();
|
|
124
|
+
}
|
|
51
125
|
};
|
|
126
|
+
/** Watches every directory that can publish an awaited input; legacy results may never exist. */
|
|
127
|
+
watch() {
|
|
128
|
+
const legacy = legacyInput();
|
|
129
|
+
const directories = [
|
|
130
|
+
...new Set([this.base, ...legacy, ...[...this.awaited].map((file) => path.dirname(file))]),
|
|
131
|
+
];
|
|
132
|
+
for (const [directory, watch] of this.watches) {
|
|
133
|
+
if (!directories.includes(directory)) {
|
|
134
|
+
watch.close();
|
|
135
|
+
this.watches.delete(directory);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const states = directories.map((directory) => {
|
|
139
|
+
let watch = this.watches.get(directory);
|
|
140
|
+
if (watch === undefined) {
|
|
141
|
+
const every = directory === this.base || legacy.includes(directory);
|
|
142
|
+
watch = new DirectoryWatch(directory, (file) => every || file === null || this.awaited.has(path.join(directory, file)), this.wake);
|
|
143
|
+
this.watches.set(directory, watch);
|
|
144
|
+
}
|
|
145
|
+
return [directory, watch.ensure()];
|
|
146
|
+
});
|
|
147
|
+
if (states.some(([directory, state]) => state === "unwatched" && !legacy.includes(directory))) {
|
|
148
|
+
return "poll";
|
|
149
|
+
}
|
|
150
|
+
return states.some(([, state]) => state === "armed") ? "rescan" : "hints";
|
|
151
|
+
}
|
|
152
|
+
unwatch() {
|
|
153
|
+
clearTimeout(this.timer);
|
|
154
|
+
clearInterval(this.probe);
|
|
155
|
+
this.observed.clear();
|
|
156
|
+
clearImmediate(this.woken);
|
|
157
|
+
this.woken = undefined;
|
|
158
|
+
for (const watch of this.watches.values()) {
|
|
159
|
+
watch.close();
|
|
160
|
+
}
|
|
161
|
+
this.watches.clear();
|
|
162
|
+
this.awaited = new Set();
|
|
163
|
+
}
|
|
52
164
|
start(context) {
|
|
53
|
-
this.
|
|
54
|
-
|
|
165
|
+
this.host = managedHost(context);
|
|
166
|
+
if (this.host === undefined || this.accepted || this.child) {
|
|
167
|
+
this.reset = this.reset.then(() => this.attach(context));
|
|
168
|
+
return this.reset;
|
|
169
|
+
}
|
|
170
|
+
// Attach and publish only after the launcher retires rollback: an unready replacement
|
|
171
|
+
// must neither adopt children nor append completions to a journal it may abandon.
|
|
172
|
+
// Readiness follows session_start, so never await it here.
|
|
173
|
+
this.owner = context.sessionManager.getSessionId();
|
|
174
|
+
this.base = ownerDirectory(this.agentDir, this.owner);
|
|
175
|
+
this.host.ready
|
|
176
|
+
.then(async (accepted) => {
|
|
177
|
+
if (accepted && !this.closing) {
|
|
178
|
+
this.accepted = true;
|
|
179
|
+
await this.start(context);
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
.catch((error) => {
|
|
183
|
+
console.error(`Agent runtime attachment: ${errorText(error)}`);
|
|
184
|
+
});
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
ownerIdentity() {
|
|
188
|
+
return ownIdentity(this.host?.owner.pid);
|
|
55
189
|
}
|
|
56
190
|
async attach(context) {
|
|
57
|
-
|
|
191
|
+
this.unwatch();
|
|
58
192
|
this.ctx = context;
|
|
59
193
|
if (this.child) {
|
|
60
194
|
return;
|
|
@@ -64,54 +198,52 @@ class AgentRuntime {
|
|
|
64
198
|
this.generation = randomUUID();
|
|
65
199
|
await fs.promises.mkdir(this.base, { recursive: true, mode: 0o700 });
|
|
66
200
|
write(path.join(this.base, "attachment.json"), {
|
|
67
|
-
...
|
|
201
|
+
...this.ownerIdentity(),
|
|
68
202
|
generation: this.generation,
|
|
69
203
|
attached: true,
|
|
70
204
|
});
|
|
71
205
|
if (this.closing) {
|
|
72
206
|
return;
|
|
73
207
|
}
|
|
74
|
-
|
|
75
|
-
this.timer.unref();
|
|
76
|
-
this.
|
|
208
|
+
// Recovery and watch setup stay off the awaited startup path.
|
|
209
|
+
this.timer = setTimeout(this.scan, 200).unref();
|
|
210
|
+
this.probe = setInterval(this.recover, 1000).unref();
|
|
77
211
|
}
|
|
78
|
-
async dispose() {
|
|
212
|
+
async dispose(retain) {
|
|
79
213
|
this.closing = true;
|
|
80
|
-
|
|
214
|
+
this.unwatch();
|
|
81
215
|
this.shutdown.abort();
|
|
82
216
|
await this.reset;
|
|
83
|
-
if (this.base.length > 0) {
|
|
217
|
+
if (!retain && this.base.length > 0) {
|
|
84
218
|
const file = path.join(this.base, "attachment.json");
|
|
85
|
-
|
|
86
|
-
|
|
219
|
+
const attachment = read(file, attachmentSchema);
|
|
220
|
+
if (attachment !== undefined && this.releases(attachment)) {
|
|
221
|
+
write(file, {
|
|
222
|
+
...this.ownerIdentity(),
|
|
223
|
+
generation: attachment.generation,
|
|
224
|
+
attached: false,
|
|
225
|
+
});
|
|
226
|
+
await stopChildren(this.base, this.owner);
|
|
87
227
|
}
|
|
88
228
|
}
|
|
89
229
|
this.ctx = undefined;
|
|
90
230
|
}
|
|
231
|
+
releases(attachment) {
|
|
232
|
+
if (attachment.generation === this.generation) {
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
// An unready replacement never attached, but exiting still releases the launcher-owned children.
|
|
236
|
+
return (this.generation.length === 0 && attachment.attached && attachment.pid === this.host?.owner.pid);
|
|
237
|
+
}
|
|
91
238
|
stop(launch) {
|
|
92
239
|
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);
|
|
240
|
+
const { targets: live, command: id } = requestStop(base, launch);
|
|
97
241
|
if (live.length === 0) {
|
|
98
|
-
return {
|
|
99
|
-
...projection(base, launch, this.ctx),
|
|
242
|
+
return renderReceipt(base, launch, this.ctx, {
|
|
100
243
|
stopAcknowledged: false,
|
|
101
244
|
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
245
|
});
|
|
111
246
|
}
|
|
112
|
-
live.forEach((target) => {
|
|
113
|
-
signalProcess(target, "SIGTERM");
|
|
114
|
-
});
|
|
115
247
|
const escalation = setTimeout(() => {
|
|
116
248
|
try {
|
|
117
249
|
live.forEach((target) => {
|
|
@@ -123,62 +255,11 @@ class AgentRuntime {
|
|
|
123
255
|
}
|
|
124
256
|
}, 3000);
|
|
125
257
|
escalation.unref();
|
|
126
|
-
return {
|
|
127
|
-
...projection(base, launch, this.ctx),
|
|
258
|
+
return renderReceipt(base, launch, this.ctx, {
|
|
128
259
|
stopRequest: id,
|
|
129
260
|
stopAcknowledged: false,
|
|
130
261
|
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
|
-
}
|
|
262
|
+
});
|
|
182
263
|
}
|
|
183
264
|
async delegate(input, signal, context) {
|
|
184
265
|
this.checkMain();
|
|
@@ -197,28 +278,17 @@ class AgentRuntime {
|
|
|
197
278
|
});
|
|
198
279
|
this.checkMain();
|
|
199
280
|
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),
|
|
281
|
+
await ensureActor(this.base, launch, this.shutdown.signal);
|
|
282
|
+
return renderReceipt(this.base, launch, this.ctx, {
|
|
206
283
|
receipt: "launch",
|
|
207
|
-
note: "Launch accepted, not
|
|
284
|
+
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
285
|
});
|
|
209
286
|
}
|
|
210
287
|
list(input) {
|
|
211
288
|
const all = list(this.base)
|
|
212
289
|
.map((id) => launchAt(this.base, id, this.owner))
|
|
213
290
|
.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
|
-
});
|
|
291
|
+
return renderInventory(this.base, all, this.ctx, input);
|
|
222
292
|
}
|
|
223
293
|
successor(launch, prompt) {
|
|
224
294
|
const id = randomUUID();
|
|
@@ -248,7 +318,7 @@ class AgentRuntime {
|
|
|
248
318
|
}
|
|
249
319
|
return;
|
|
250
320
|
}
|
|
251
|
-
async retryContinuation(launch, input
|
|
321
|
+
async retryContinuation(launch, input) {
|
|
252
322
|
const identity = actor(this.base, launch);
|
|
253
323
|
const command = read(path.join(conversation(this.base, launch), "commands", `${launch.id}.json`), commandSchema);
|
|
254
324
|
if (input.prompt !== launch.prompt ||
|
|
@@ -256,16 +326,13 @@ class AgentRuntime {
|
|
|
256
326
|
command?.id !== launch.id ||
|
|
257
327
|
command.runId !== launch.id ||
|
|
258
328
|
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
|
|
329
|
+
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
330
|
}
|
|
261
331
|
// 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" });
|
|
332
|
+
await ensureActor(this.base, launch, this.shutdown.signal);
|
|
333
|
+
return renderReceipt(this.base, launch, this.ctx, { receipt: "continuation" });
|
|
267
334
|
}
|
|
268
|
-
async continue(launch, input
|
|
335
|
+
async continue(launch, input) {
|
|
269
336
|
if (input.prompt === undefined) {
|
|
270
337
|
throw new Error("continue requires the entire new prompt.");
|
|
271
338
|
}
|
|
@@ -274,16 +341,12 @@ class AgentRuntime {
|
|
|
274
341
|
}
|
|
275
342
|
const completed = this.continuationResult(launch);
|
|
276
343
|
if (completed === "pending") {
|
|
277
|
-
return this.retryContinuation(launch, input
|
|
344
|
+
return this.retryContinuation(launch, input);
|
|
278
345
|
}
|
|
279
346
|
const identity = actor(this.base, launch);
|
|
280
347
|
if (!completed && identity && alive(identity)) {
|
|
281
348
|
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),
|
|
349
|
+
return renderReceipt(this.base, launch, this.ctx, {
|
|
287
350
|
receipt: "guidance",
|
|
288
351
|
command,
|
|
289
352
|
note: "Guidance queued, not actual completion or model consumption.",
|
|
@@ -291,44 +354,16 @@ class AgentRuntime {
|
|
|
291
354
|
}
|
|
292
355
|
const next = this.successor(launch, input.prompt);
|
|
293
356
|
if (!next) {
|
|
294
|
-
return
|
|
295
|
-
...projection(this.base, latest(this.base, launch), this.ctx),
|
|
357
|
+
return renderReceipt(this.base, latest(this.base, launch), this.ctx, {
|
|
296
358
|
receipt: "existing-successor",
|
|
297
359
|
});
|
|
298
360
|
}
|
|
299
361
|
// Retain successor claims on ambiguous after-launch failures: no replay.
|
|
300
362
|
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" });
|
|
363
|
+
await ensureActor(this.base, next, this.shutdown.signal);
|
|
364
|
+
return renderReceipt(this.base, next, this.ctx, { receipt: "continuation" });
|
|
306
365
|
}
|
|
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) {
|
|
366
|
+
async continueSaved(input, context) {
|
|
332
367
|
if (input.prompt === undefined) {
|
|
333
368
|
throw new Error("continue requires the entire new prompt.");
|
|
334
369
|
}
|
|
@@ -342,17 +377,13 @@ class AgentRuntime {
|
|
|
342
377
|
if (created) {
|
|
343
378
|
// The durable adoption claim survives ambiguous after-launch failures.
|
|
344
379
|
enqueue(this.base, launch, "run");
|
|
345
|
-
await ensureActor(this.base, launch);
|
|
346
|
-
}
|
|
347
|
-
if (input.async === false) {
|
|
348
|
-
return this.wait(launch, signal, created);
|
|
380
|
+
await ensureActor(this.base, launch, this.shutdown.signal);
|
|
349
381
|
}
|
|
350
|
-
return
|
|
351
|
-
...projection(this.base, launch, this.ctx),
|
|
382
|
+
return renderReceipt(this.base, launch, this.ctx, {
|
|
352
383
|
receipt: created ? "continuation" : "existing-successor",
|
|
353
384
|
});
|
|
354
385
|
}
|
|
355
|
-
async runs(input,
|
|
386
|
+
async runs(input, context) {
|
|
356
387
|
this.checkMain();
|
|
357
388
|
await this.reset;
|
|
358
389
|
if (input.action === "list") {
|
|
@@ -364,31 +395,32 @@ class AgentRuntime {
|
|
|
364
395
|
}
|
|
365
396
|
if (input.action === "continue" &&
|
|
366
397
|
!fs.existsSync(path.join(runDirectory(this.base, input.id), "launch.json"))) {
|
|
367
|
-
return this.continueSaved({ ...input, id: input.id },
|
|
398
|
+
return this.continueSaved({ ...input, id: input.id }, context);
|
|
368
399
|
}
|
|
369
400
|
const original = launchAt(this.base, input.id, this.owner);
|
|
370
401
|
if (input.action === "inspect") {
|
|
371
|
-
return
|
|
402
|
+
return renderInspection(this.base, original, this.ctx, input);
|
|
372
403
|
}
|
|
373
404
|
const launch = latest(this.base, original);
|
|
374
405
|
if (input.action === "stop") {
|
|
375
|
-
return
|
|
406
|
+
return this.stop(launch);
|
|
376
407
|
}
|
|
377
|
-
|
|
378
|
-
return this.wait(launch, signal, false);
|
|
379
|
-
}
|
|
380
|
-
return this.continue(launch, input, signal);
|
|
408
|
+
return this.continue(launch, input);
|
|
381
409
|
}
|
|
382
410
|
register() {
|
|
383
|
-
|
|
411
|
+
registerCompactMessages(this.pi, ["agent-completion", "subagent-notify"]);
|
|
384
412
|
this.pi.on("session_start", (_event, context) => this.start(context));
|
|
385
|
-
this.pi.on("turn_end", (event) => {
|
|
386
|
-
|
|
413
|
+
this.pi.on("turn_end", (event, context) => {
|
|
414
|
+
// The public pending flag covers user input; the preview also includes custom messages.
|
|
415
|
+
if (settle(this.owner, context.hasPendingMessages() || event.context.pendingMessages.length > 0)) {
|
|
416
|
+
// Released admissions may have been dropped from the queue rather than published.
|
|
417
|
+
this.wake();
|
|
418
|
+
}
|
|
387
419
|
});
|
|
388
420
|
this.pi.on("agent_settled", () => {
|
|
389
421
|
this.scan();
|
|
390
422
|
});
|
|
391
|
-
this.pi.on("session_shutdown", () => this.dispose());
|
|
423
|
+
this.pi.on("session_shutdown", (event) => this.dispose(event.reason === "reload" || retainsChildren(event)));
|
|
392
424
|
if (this.child) {
|
|
393
425
|
this.pi.on("tool_call", (event) => {
|
|
394
426
|
if (event.toolName === "delegate" || event.toolName === "agent_runs") {
|
|
@@ -401,18 +433,20 @@ class AgentRuntime {
|
|
|
401
433
|
this.pi.registerTool({
|
|
402
434
|
name: "delegate",
|
|
403
435
|
label: "Delegate",
|
|
404
|
-
description: "
|
|
436
|
+
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
437
|
parameters: delegateSchema,
|
|
406
438
|
exposure: this.child ? "hidden" : "direct",
|
|
439
|
+
renderResult: renderCompactResult,
|
|
407
440
|
execute,
|
|
408
441
|
});
|
|
409
|
-
const runs = (...args) => this.runs(args[1], args[
|
|
442
|
+
const runs = (...args) => this.runs(args[1], args[4]);
|
|
410
443
|
this.pi.registerTool({
|
|
411
444
|
name: "agent_runs",
|
|
412
445
|
label: "Agent runs",
|
|
413
|
-
description: "
|
|
446
|
+
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
447
|
parameters: runsSchema,
|
|
415
448
|
exposure: this.child ? "hidden" : "direct",
|
|
449
|
+
renderResult: renderCompactResult,
|
|
416
450
|
execute: runs,
|
|
417
451
|
});
|
|
418
452
|
}
|