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