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