@fitchmultz/pi-subagents 0.52.17 → 0.52.19
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 -0
- package/README.md +4 -0
- package/dist/agent/host.js +12 -0
- package/dist/agent/index.js +70 -56
- package/dist/agent/launch.js +12 -3
- package/dist/agent/notifications.js +5 -5
- package/dist/agent/parking.js +26 -15
- package/dist/agent/runner.js +18 -16
- package/dist/agent/tool-schemas.js +2 -0
- package/dist/agent/usage.js +42 -3
- package/dist/agent/view.js +13 -13
- package/dist/agent/wake.js +17 -15
- package/dist/build-info.js +1 -1
- package/dist/pi-intercom/broker/broker.js +2 -2
- package/dist/pi-intercom/broker/snapshots.d.ts +25 -7
- package/dist/pi-intercom/broker/snapshots.js +56 -25
- package/dist/pi-intercom/broker/spawn-claim.js +89 -0
- package/dist/pi-intercom/broker/spawn.js +31 -78
- package/dist/pi-intercom/inbound-delivery.js +2 -6
- package/dist/pi-intercom/lifecycle.js +26 -26
- package/dist/pi-intercom/reply-tracker.js +47 -55
- package/dist/pi-intercom/session-events.js +2 -0
- package/dist/shared/post-exit-stdio-guard.js +44 -37
- package/docs/development.md +25 -0
- package/docs/intercom.md +19 -2
- package/docs/reference.md +16 -5
- package/package.json +1 -1
- package/scripts/build.mjs +41 -14
- package/skills/pi-subagents/SKILL.md +8 -4
- package/src/agent/contracts.ts +17 -0
- package/src/agent/host.ts +19 -0
- package/src/agent/index.ts +91 -62
- package/src/agent/launch.ts +16 -3
- package/src/agent/notifications.ts +5 -5
- package/src/agent/parking.ts +39 -17
- package/src/agent/runner.ts +20 -21
- package/src/agent/tool-schemas.ts +2 -0
- package/src/agent/usage.ts +48 -3
- package/src/agent/view.ts +15 -13
- package/src/agent/wake.ts +19 -15
- package/src/pi-intercom/broker/broker.ts +2 -1
- package/src/pi-intercom/broker/snapshots.ts +82 -37
- package/src/pi-intercom/broker/spawn-claim.ts +98 -0
- package/src/pi-intercom/broker/spawn.ts +42 -82
- package/src/pi-intercom/inbound-delivery.ts +2 -6
- package/src/pi-intercom/lifecycle.ts +38 -27
- package/src/pi-intercom/reply-tracker.ts +50 -58
- package/src/pi-intercom/session-events.ts +3 -1
- package/src/shared/post-exit-stdio-guard.ts +48 -40
|
@@ -14,6 +14,7 @@ profile discovery, loader, role body, chain DSL, or nested delegation.
|
|
|
14
14
|
delegate({
|
|
15
15
|
prompt: "Complete assignment with constraints and expected output",
|
|
16
16
|
name: "short-name",
|
|
17
|
+
thinking: "low",
|
|
17
18
|
});
|
|
18
19
|
```
|
|
19
20
|
|
|
@@ -23,12 +24,14 @@ expected result, and stop boundary. Do not rely on a hidden profile to add them.
|
|
|
23
24
|
|
|
24
25
|
Fresh context is default; `context:"fork"` preserves the native conversation,
|
|
25
26
|
including prior tool results. A forked child still cannot delegate. Optional
|
|
26
|
-
`cwd` and `
|
|
27
|
-
settings to implement an override.
|
|
27
|
+
`cwd`, `model`, and `thinking` are explicit launch overrides; do not change global
|
|
28
|
+
auth/model settings to implement an override. `thinking` accepts `off`, `minimal`,
|
|
29
|
+
`low`, `medium`, `high`, `xhigh`, or `max`. Omit it to inherit the parent's effort.
|
|
30
|
+
Pi clamps the child's effort to its model capabilities without changing the parent.
|
|
28
31
|
`model:"provider/model:thinking"` uses Pi's native suffix resolver when the host
|
|
29
32
|
exports it; a literal model ID wins before suffix parsing. Without that host API,
|
|
30
|
-
use
|
|
31
|
-
silently.
|
|
33
|
+
use `thinking` with an omitted model or literal ID. If both the field and a native
|
|
34
|
+
suffix supply effort, they must match. Never retry another model silently.
|
|
32
35
|
|
|
33
36
|
Every launch, continuation and guidance call returns a receipt; no tool waits for
|
|
34
37
|
child completion. Continue useful independent work; when child evidence gates the
|
|
@@ -45,6 +48,7 @@ invocation exits after its final turn and stops any remaining children.
|
|
|
45
48
|
- `inspect` reads a run by ID; follow returned offsets for complete output.
|
|
46
49
|
- `continue` takes a complete follow-up `prompt` and keeps the saved child chat.
|
|
47
50
|
Active work receives guidance; completed work gets an immutable successor.
|
|
51
|
+
Both keep the child's saved effort, independent of later parent changes.
|
|
48
52
|
- `stop` requests termination. Inspect actual process evidence; request acceptance
|
|
49
53
|
does not prove actor or command exit.
|
|
50
54
|
|
package/src/agent/contracts.ts
CHANGED
|
@@ -93,6 +93,23 @@ export const resultSchema = Type.Object({
|
|
|
93
93
|
),
|
|
94
94
|
});
|
|
95
95
|
export type Result = Static<typeof resultSchema>;
|
|
96
|
+
export type AccountingState =
|
|
97
|
+
| { readonly kind: "unrecorded"; readonly terminalEntryId: string | undefined }
|
|
98
|
+
| {
|
|
99
|
+
readonly kind: "verified";
|
|
100
|
+
readonly contributions: Contribution[];
|
|
101
|
+
readonly terminalEntryId: string | undefined;
|
|
102
|
+
}
|
|
103
|
+
| {
|
|
104
|
+
readonly kind: "pending";
|
|
105
|
+
readonly pending: NonNullable<Result["accountingPending"]>;
|
|
106
|
+
readonly error: string | undefined;
|
|
107
|
+
}
|
|
108
|
+
| {
|
|
109
|
+
readonly kind: "unavailable";
|
|
110
|
+
readonly error: string;
|
|
111
|
+
readonly terminalEntryId: string | undefined;
|
|
112
|
+
};
|
|
96
113
|
export const commandSchema = Type.Object({
|
|
97
114
|
id: Type.String(),
|
|
98
115
|
runId: Type.String(),
|
package/src/agent/host.ts
CHANGED
|
@@ -9,6 +9,25 @@ import { readIntercomLineage } from "../pi-intercom/identity.ts";
|
|
|
9
9
|
|
|
10
10
|
export { errorMessage as errorText } from "../shared/unknown.ts";
|
|
11
11
|
|
|
12
|
+
/** Native fork lifetime owner; structural detection keeps official Pi supported. */
|
|
13
|
+
export interface ManagedHost {
|
|
14
|
+
readonly owner: { readonly pid: number };
|
|
15
|
+
readonly ready: Promise<boolean>;
|
|
16
|
+
}
|
|
17
|
+
export function managedHost(context: SDK.ExtensionContext): ManagedHost | undefined {
|
|
18
|
+
const host: unknown = Reflect.get(context, "managedHost");
|
|
19
|
+
if (typeof host !== "object" || host === null) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
const owner: unknown = Reflect.get(host, "owner");
|
|
23
|
+
const pid: unknown =
|
|
24
|
+
typeof owner === "object" && owner !== null ? Reflect.get(owner, "pid") : undefined;
|
|
25
|
+
const ready: unknown = Reflect.get(host, "ready");
|
|
26
|
+
return typeof pid === "number" && Number.isSafeInteger(pid) && ready instanceof Promise
|
|
27
|
+
? { owner: { pid }, ready: ready.then((accepted) => accepted === true) }
|
|
28
|
+
: undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
12
31
|
/** A launcher may still own children while handing off or replacing a worker. */
|
|
13
32
|
export function retainsChildren(event: object): boolean {
|
|
14
33
|
const disposition: unknown = Reflect.get(event, "disposition");
|
package/src/agent/index.ts
CHANGED
|
@@ -17,7 +17,14 @@ import {
|
|
|
17
17
|
signalProcess,
|
|
18
18
|
} from "./process.ts";
|
|
19
19
|
import { stopChildren } from "./shutdown.ts";
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
isChild,
|
|
22
|
+
errorText,
|
|
23
|
+
managedHost,
|
|
24
|
+
rememberSDK,
|
|
25
|
+
retainsChildren,
|
|
26
|
+
type ManagedHost,
|
|
27
|
+
} from "./host.ts";
|
|
21
28
|
import { serveParked } from "./parking.ts";
|
|
22
29
|
import {
|
|
23
30
|
resultSchema,
|
|
@@ -54,24 +61,20 @@ import { getPiAgentDir } from "../pi-intercom/agent-dir.ts";
|
|
|
54
61
|
import { registerCompactMessages, renderCompactResult } from "../shared/compact-cards.ts";
|
|
55
62
|
import { BackgroundReporter } from "../shared/background-reporting.ts";
|
|
56
63
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
readonly owner:
|
|
60
|
-
readonly
|
|
61
|
-
|
|
62
|
-
function managedHost(context: ExtensionContext): ManagedHost | undefined {
|
|
63
|
-
const host: unknown = Reflect.get(context, "managedHost");
|
|
64
|
-
if (typeof host !== "object" || host === null) {
|
|
65
|
-
return undefined;
|
|
66
|
-
}
|
|
67
|
-
const owner: unknown = Reflect.get(host, "owner");
|
|
68
|
-
const pid: unknown =
|
|
69
|
-
typeof owner === "object" && owner !== null ? Reflect.get(owner, "pid") : undefined;
|
|
70
|
-
const ready: unknown = Reflect.get(host, "ready");
|
|
71
|
-
return typeof pid === "number" && Number.isSafeInteger(pid) && ready instanceof Promise
|
|
72
|
-
? { owner: { pid }, ready: ready.then((accepted) => accepted === true) }
|
|
73
|
-
: undefined;
|
|
64
|
+
interface AttachmentBinding {
|
|
65
|
+
readonly context: ExtensionContext;
|
|
66
|
+
readonly owner: string;
|
|
67
|
+
readonly base: string;
|
|
68
|
+
readonly host: ManagedHost | undefined;
|
|
74
69
|
}
|
|
70
|
+
type AttachmentState =
|
|
71
|
+
| { readonly kind: "detached" }
|
|
72
|
+
| { readonly kind: "waiting"; readonly binding: AttachmentBinding }
|
|
73
|
+
| {
|
|
74
|
+
readonly kind: "attaching" | "attached";
|
|
75
|
+
readonly binding: AttachmentBinding;
|
|
76
|
+
readonly generation: string;
|
|
77
|
+
};
|
|
75
78
|
|
|
76
79
|
/** Legacy results are optional; their reconciliation reports an invalid configured root. */
|
|
77
80
|
function legacyInput(): string[] {
|
|
@@ -82,14 +85,8 @@ function legacyInput(): string[] {
|
|
|
82
85
|
}
|
|
83
86
|
}
|
|
84
87
|
class AgentRuntime {
|
|
85
|
-
private
|
|
88
|
+
private attachment: AttachmentState = { kind: "detached" };
|
|
86
89
|
private readonly reporter = new BackgroundReporter(() => this.ctx);
|
|
87
|
-
// A managed launcher owns children across worker replacement; otherwise this process does.
|
|
88
|
-
private host?: ManagedHost | undefined;
|
|
89
|
-
private accepted = false;
|
|
90
|
-
private base = "";
|
|
91
|
-
private owner = "";
|
|
92
|
-
private generation = "";
|
|
93
90
|
// Polling remains only while change hints are unavailable or a reconciliation failed.
|
|
94
91
|
private timer?: NodeJS.Timeout;
|
|
95
92
|
private woken?: NodeJS.Immediate | undefined;
|
|
@@ -99,7 +96,6 @@ class AgentRuntime {
|
|
|
99
96
|
// Input stamps the last reconciliation saw; a one-second probe recovers dropped hints.
|
|
100
97
|
private observed = new Map<string, string>();
|
|
101
98
|
private probe?: NodeJS.Timeout;
|
|
102
|
-
private closing = false;
|
|
103
99
|
private reset: Promise<void> = Promise.resolve();
|
|
104
100
|
private readonly shutdown = new AbortController();
|
|
105
101
|
private readonly child = isChild();
|
|
@@ -109,11 +105,28 @@ class AgentRuntime {
|
|
|
109
105
|
this.pi = pi;
|
|
110
106
|
this.watches = new CompletionWatches(this.reporter, this.wake);
|
|
111
107
|
}
|
|
108
|
+
private get binding(): AttachmentBinding {
|
|
109
|
+
if (this.attachment.kind === "detached") {
|
|
110
|
+
throw new Error("Agent runtime is not attached.");
|
|
111
|
+
}
|
|
112
|
+
return this.attachment.binding;
|
|
113
|
+
}
|
|
114
|
+
private get ctx(): ExtensionContext | undefined {
|
|
115
|
+
return this.attachment.kind === "attaching" || this.attachment.kind === "attached"
|
|
116
|
+
? this.attachment.binding.context
|
|
117
|
+
: undefined;
|
|
118
|
+
}
|
|
119
|
+
private get base(): string {
|
|
120
|
+
return this.binding.base;
|
|
121
|
+
}
|
|
122
|
+
private get owner(): string {
|
|
123
|
+
return this.binding.owner;
|
|
124
|
+
}
|
|
112
125
|
private checkMain(): void {
|
|
113
126
|
if (isChild()) {
|
|
114
127
|
throw new Error("Only a main session may delegate or control agent runs.");
|
|
115
128
|
}
|
|
116
|
-
if (this.
|
|
129
|
+
if (this.shutdown.signal.aborted || this.attachment.kind !== "attached") {
|
|
117
130
|
throw new Error("Agent runtime is not attached.");
|
|
118
131
|
}
|
|
119
132
|
}
|
|
@@ -133,17 +146,18 @@ class AgentRuntime {
|
|
|
133
146
|
};
|
|
134
147
|
private readonly scan = (): void => {
|
|
135
148
|
clearTimeout(this.timer);
|
|
136
|
-
if (this.
|
|
149
|
+
if (this.shutdown.signal.aborted || this.attachment.kind !== "attached" || this.child) {
|
|
137
150
|
return;
|
|
138
151
|
}
|
|
152
|
+
const { context } = this.attachment.binding;
|
|
139
153
|
const inputs = () => [this.base, ...legacyInput(), ...this.awaited];
|
|
140
154
|
const before = new Map(inputs().map((file) => [file, pathStamp(file)]));
|
|
141
155
|
let reconciled = true;
|
|
142
156
|
try {
|
|
143
|
-
const { notices, awaited } = notifications(
|
|
157
|
+
const { notices, awaited } = notifications(context, this.base, this.owner);
|
|
144
158
|
this.awaited = awaited;
|
|
145
|
-
notices.unshift(...reconcileLegacy(
|
|
146
|
-
deliver(this.pi,
|
|
159
|
+
notices.unshift(...reconcileLegacy(context, this.reporter));
|
|
160
|
+
deliver(this.pi, context, notices);
|
|
147
161
|
this.reporter.recover("completion-scan");
|
|
148
162
|
} catch (error) {
|
|
149
163
|
reconciled = false;
|
|
@@ -172,23 +186,34 @@ class AgentRuntime {
|
|
|
172
186
|
this.awaited = new Set();
|
|
173
187
|
}
|
|
174
188
|
private start(context: ExtensionContext): Promise<void> | undefined {
|
|
189
|
+
if (this.shutdown.signal.aborted) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
175
192
|
this.reporter.start();
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
193
|
+
const owner = context.sessionManager.getSessionId();
|
|
194
|
+
const host = managedHost(context);
|
|
195
|
+
const binding = { context, owner, base: ownerDirectory(this.agentDir, owner), host };
|
|
196
|
+
if (
|
|
197
|
+
host === undefined ||
|
|
198
|
+
this.attachment.kind === "attached" ||
|
|
199
|
+
this.attachment.kind === "attaching" ||
|
|
200
|
+
this.child
|
|
201
|
+
) {
|
|
202
|
+
this.reset = this.reset.then(() => this.attach(binding));
|
|
179
203
|
return this.reset;
|
|
180
204
|
}
|
|
181
205
|
// Attach and publish only after the launcher retires rollback: an unready replacement
|
|
182
206
|
// must neither adopt children nor append completions to a journal it may abandon.
|
|
183
207
|
// Readiness follows session_start, so never await it here.
|
|
184
|
-
|
|
185
|
-
this.
|
|
186
|
-
|
|
187
|
-
.then(
|
|
188
|
-
if (accepted && !this.
|
|
189
|
-
this.
|
|
190
|
-
|
|
208
|
+
const waiting: AttachmentState = { kind: "waiting", binding };
|
|
209
|
+
this.attachment = waiting;
|
|
210
|
+
host.ready
|
|
211
|
+
.then((accepted) => {
|
|
212
|
+
if (accepted && !this.shutdown.signal.aborted && this.attachment === waiting) {
|
|
213
|
+
this.reset = this.reset.then(() => this.attach(binding));
|
|
214
|
+
return this.reset;
|
|
191
215
|
}
|
|
216
|
+
return null;
|
|
192
217
|
})
|
|
193
218
|
.catch((error: unknown) => {
|
|
194
219
|
this.reporter.report(undefined, `Agent runtime attachment: ${errorText(error)}`);
|
|
@@ -196,37 +221,36 @@ class AgentRuntime {
|
|
|
196
221
|
return undefined;
|
|
197
222
|
}
|
|
198
223
|
private ownerIdentity(): Omit<ProcessIdentity, "token"> {
|
|
199
|
-
return ownIdentity(this.host?.owner.pid);
|
|
224
|
+
return ownIdentity(this.binding.host?.owner.pid);
|
|
200
225
|
}
|
|
201
|
-
private async attach(
|
|
226
|
+
private async attach(binding: AttachmentBinding): Promise<void> {
|
|
202
227
|
this.unwatch();
|
|
203
|
-
|
|
228
|
+
const attaching: AttachmentState = { kind: "attaching", binding, generation: randomUUID() };
|
|
229
|
+
this.attachment = attaching;
|
|
204
230
|
if (this.child) {
|
|
231
|
+
this.attachment = { ...attaching, kind: "attached" };
|
|
205
232
|
return;
|
|
206
233
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
write(path.join(this.base, "attachment.json"), {
|
|
212
|
-
...this.ownerIdentity(),
|
|
213
|
-
generation: this.generation,
|
|
234
|
+
await fs.promises.mkdir(binding.base, { recursive: true, mode: 0o700 });
|
|
235
|
+
write(path.join(binding.base, "attachment.json"), {
|
|
236
|
+
...ownIdentity(binding.host?.owner.pid),
|
|
237
|
+
generation: attaching.generation,
|
|
214
238
|
attached: true,
|
|
215
239
|
});
|
|
216
|
-
if (this.
|
|
240
|
+
if (this.shutdown.signal.aborted) {
|
|
217
241
|
return;
|
|
218
242
|
}
|
|
243
|
+
this.attachment = { ...attaching, kind: "attached" };
|
|
219
244
|
// Recovery and watch setup stay off the awaited startup path.
|
|
220
245
|
this.timer = setTimeout(this.scan, 200).unref();
|
|
221
246
|
this.probe = setInterval(this.recover, 1000).unref();
|
|
222
247
|
}
|
|
223
248
|
private async dispose(retain: boolean): Promise<void> {
|
|
224
249
|
this.reporter.shutdown();
|
|
225
|
-
this.closing = true;
|
|
226
250
|
this.unwatch();
|
|
227
251
|
this.shutdown.abort();
|
|
228
252
|
await this.reset;
|
|
229
|
-
if (!retain && this.
|
|
253
|
+
if (!retain && !this.child && this.attachment.kind !== "detached") {
|
|
230
254
|
const file = path.join(this.base, "attachment.json");
|
|
231
255
|
const attachment = read(file, attachmentSchema);
|
|
232
256
|
if (attachment !== undefined && this.releases(attachment)) {
|
|
@@ -240,16 +264,20 @@ class AgentRuntime {
|
|
|
240
264
|
);
|
|
241
265
|
}
|
|
242
266
|
}
|
|
243
|
-
this.
|
|
267
|
+
this.attachment = { kind: "detached" };
|
|
244
268
|
}
|
|
245
269
|
private releases(attachment: Attachment): boolean {
|
|
246
|
-
|
|
247
|
-
|
|
270
|
+
const state = this.attachment;
|
|
271
|
+
switch (state.kind) {
|
|
272
|
+
case "detached":
|
|
273
|
+
return false;
|
|
274
|
+
case "waiting":
|
|
275
|
+
// An unready replacement can release the launcher's existing generation on quit.
|
|
276
|
+
return attachment.attached && attachment.pid === state.binding.host?.owner.pid;
|
|
277
|
+
case "attaching":
|
|
278
|
+
case "attached":
|
|
279
|
+
return attachment.generation === state.generation;
|
|
248
280
|
}
|
|
249
|
-
// An unready replacement never attached, but exiting still releases the launcher-owned children.
|
|
250
|
-
return (
|
|
251
|
-
this.generation.length === 0 && attachment.attached && attachment.pid === this.host?.owner.pid
|
|
252
|
-
);
|
|
253
281
|
}
|
|
254
282
|
private stop(launch: Launch) {
|
|
255
283
|
const base = this.base;
|
|
@@ -433,6 +461,7 @@ class AgentRuntime {
|
|
|
433
461
|
this.pi.on("turn_end", (event, context) => {
|
|
434
462
|
// The public pending flag covers user input; the preview also includes custom messages.
|
|
435
463
|
if (
|
|
464
|
+
this.attachment.kind === "attached" &&
|
|
436
465
|
settle(this.owner, context.hasPendingMessages() || event.context.pendingMessages.length > 0)
|
|
437
466
|
) {
|
|
438
467
|
// Released admissions may have been dropped from the queue rather than published.
|
|
@@ -459,7 +488,7 @@ class AgentRuntime {
|
|
|
459
488
|
name: "delegate",
|
|
460
489
|
label: "Delegate",
|
|
461
490
|
description:
|
|
462
|
-
"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.",
|
|
491
|
+
"Run a child on exactly this prompt; main sessions only. Fork copies the full conversation; default is fresh. Optional thinking overrides inherited effort for this child. Returns a receipt; completion is notified later: do other work or end the turn, don't poll.",
|
|
463
492
|
parameters: delegateSchema,
|
|
464
493
|
exposure: this.child ? "hidden" : "direct",
|
|
465
494
|
renderResult: renderCompactResult,
|
package/src/agent/launch.ts
CHANGED
|
@@ -8,6 +8,16 @@ import type { DelegateInput } from "./tool-schemas.ts";
|
|
|
8
8
|
import { loadSDK, packageRoot, runtimeEntry } from "./host.ts";
|
|
9
9
|
import { runDirectory, write } from "./storage.ts";
|
|
10
10
|
|
|
11
|
+
function selectedThinking(
|
|
12
|
+
requested: ThinkingLevel | undefined,
|
|
13
|
+
inherited: ThinkingLevel,
|
|
14
|
+
suffix?: ThinkingLevel,
|
|
15
|
+
): ThinkingLevel {
|
|
16
|
+
if (requested !== undefined && suffix !== undefined && requested !== suffix) {
|
|
17
|
+
throw new Error("thinking conflicts with the model's thinking suffix.");
|
|
18
|
+
}
|
|
19
|
+
return suffix ?? requested ?? inherited;
|
|
20
|
+
}
|
|
11
21
|
function selectedModel(
|
|
12
22
|
input: DelegateInput,
|
|
13
23
|
context: ExtensionContext,
|
|
@@ -18,7 +28,7 @@ function selectedModel(
|
|
|
18
28
|
if (!context.model) {
|
|
19
29
|
throw new Error("No selected model; no fallback was used.");
|
|
20
30
|
}
|
|
21
|
-
return { model: context.model, thinking };
|
|
31
|
+
return { model: context.model, thinking: selectedThinking(input.thinking, thinking) };
|
|
22
32
|
}
|
|
23
33
|
const slash = input.model.indexOf("/");
|
|
24
34
|
if (slash < 1) {
|
|
@@ -29,7 +39,7 @@ function selectedModel(
|
|
|
29
39
|
input.model.slice(slash + 1),
|
|
30
40
|
);
|
|
31
41
|
if (model) {
|
|
32
|
-
return { model, thinking };
|
|
42
|
+
return { model, thinking: selectedThinking(input.thinking, thinking) };
|
|
33
43
|
}
|
|
34
44
|
if (typeof sdk.resolveModelReference !== "function") {
|
|
35
45
|
throw new Error(
|
|
@@ -49,7 +59,10 @@ function selectedModel(
|
|
|
49
59
|
if (resolved.status !== "resolved") {
|
|
50
60
|
throw new Error(`Model ${input.model} is unavailable; no fallback was used.`);
|
|
51
61
|
}
|
|
52
|
-
return {
|
|
62
|
+
return {
|
|
63
|
+
model: resolved.model,
|
|
64
|
+
thinking: selectedThinking(input.thinking, thinking, resolved.thinkingLevel),
|
|
65
|
+
};
|
|
53
66
|
}
|
|
54
67
|
function targetTrust(
|
|
55
68
|
sdk: Awaited<ReturnType<typeof loadSDK>>,
|
|
@@ -6,7 +6,7 @@ import { readOutputPage } from "../shared/output-page.ts";
|
|
|
6
6
|
import { journalStamp } from "../shared/native-journal.ts";
|
|
7
7
|
import type { Launch, Result } from "./contracts.ts";
|
|
8
8
|
import { launchAt, list, runDirectory } from "./storage.ts";
|
|
9
|
-
import { readCanonicalResult, readResult } from "./usage.ts";
|
|
9
|
+
import { accountingState, readCanonicalResult, readResult } from "./usage.ts";
|
|
10
10
|
|
|
11
11
|
function isStringSet(value: unknown): value is Set<string> {
|
|
12
12
|
if (!(value instanceof Set)) {
|
|
@@ -160,7 +160,7 @@ function unchangedPending(pending: Loaded | undefined): boolean {
|
|
|
160
160
|
function load(base: string, id: string, owner: string): Loaded {
|
|
161
161
|
const launch = launchAt(base, id, owner);
|
|
162
162
|
const result = readCanonicalResult(base, launch);
|
|
163
|
-
if (result
|
|
163
|
+
if (accountingState(result).kind !== "pending") {
|
|
164
164
|
return { launch, result };
|
|
165
165
|
}
|
|
166
166
|
// Only pending runs need this pre-read stamp to observe concurrent publication.
|
|
@@ -169,7 +169,7 @@ function load(base: string, id: string, owner: string): Loaded {
|
|
|
169
169
|
return { launch, result: readResult(base, launch), journal };
|
|
170
170
|
}
|
|
171
171
|
function cacheableResult(loaded: Loaded): boolean {
|
|
172
|
-
return loaded.result
|
|
172
|
+
return accountingState(loaded.result).kind !== "pending" || loaded.journal !== undefined;
|
|
173
173
|
}
|
|
174
174
|
interface Scan {
|
|
175
175
|
readonly base: string;
|
|
@@ -198,7 +198,7 @@ function completedRun({ base, owner, known, settled }: Scan, id: string): Comple
|
|
|
198
198
|
const order = { endedAt: loaded.result.endedAt, createdAt: loaded.launch.createdAt };
|
|
199
199
|
// Pending accounting retries on child changes, even after the completion receipt.
|
|
200
200
|
// Ordinary settled history retains its launch/result-only idle probes.
|
|
201
|
-
const pending = loaded.result.
|
|
201
|
+
const pending = accountingState(loaded.result).kind === "pending" ? loaded : undefined;
|
|
202
202
|
if (launch !== undefined && result !== undefined && cacheableResult(loaded)) {
|
|
203
203
|
settled.set(id, { launch, result, pending, ...order });
|
|
204
204
|
}
|
|
@@ -216,7 +216,7 @@ function completed(base: string, owner: string, awaited: Set<string>): Completed
|
|
|
216
216
|
const run = completedRun(scan, id);
|
|
217
217
|
if (run === undefined) {
|
|
218
218
|
awaited.add(path.join(runDirectory(base, id), "result.json"));
|
|
219
|
-
} else if (run.loaded
|
|
219
|
+
} else if (run.loaded && accountingState(run.loaded.result).kind === "pending") {
|
|
220
220
|
awaited.add(run.loaded.launch.sessionFile);
|
|
221
221
|
}
|
|
222
222
|
return run ?? [];
|
package/src/agent/parking.ts
CHANGED
|
@@ -29,7 +29,14 @@ import type { NativeHistory } from "../pi-intercom/last-message.ts";
|
|
|
29
29
|
type SDK = Awaited<ReturnType<typeof loadSDK>>;
|
|
30
30
|
type Restore = NonNullable<SDK["readWorkingSession"]>;
|
|
31
31
|
export type Runtime = Awaited<ReturnType<SDK["createAgentSessionRuntime"]>>;
|
|
32
|
+
export type ParkOutcome = "resident" | "exit-parked" | "exit-failed";
|
|
32
33
|
type SessionManager = Parameters<SDK["createAgentSessionRuntime"]>[1]["sessionManager"];
|
|
34
|
+
type OpenSource =
|
|
35
|
+
| { readonly kind: "journal" }
|
|
36
|
+
| { readonly kind: "parked"; readonly state: string; readonly saved: WorkingState };
|
|
37
|
+
type RestorePhase =
|
|
38
|
+
| { readonly kind: "unopened" }
|
|
39
|
+
| { readonly kind: "opening" | "adopted"; readonly source: OpenSource };
|
|
33
40
|
interface WorkingHold {
|
|
34
41
|
readonly state: WorkingState;
|
|
35
42
|
readonly sleepReady: boolean;
|
|
@@ -156,10 +163,7 @@ export function parkedState(
|
|
|
156
163
|
}
|
|
157
164
|
/** One actor generation's idle parking and exact restore; commands stay with the runner. */
|
|
158
165
|
export class Parking {
|
|
159
|
-
|
|
160
|
-
/** Set before entering either native open, whose effects may outlive an exception. */
|
|
161
|
-
opened = false;
|
|
162
|
-
private restored?: string;
|
|
166
|
+
private restore: RestorePhase = { kind: "unopened" };
|
|
163
167
|
private since = 0;
|
|
164
168
|
private leaf?: string | null;
|
|
165
169
|
private shell?: string | undefined;
|
|
@@ -179,36 +183,48 @@ export class Parking {
|
|
|
179
183
|
private get file(): string {
|
|
180
184
|
return path.join(this.dir, "parked.json");
|
|
181
185
|
}
|
|
186
|
+
get saved(): WorkingState | undefined {
|
|
187
|
+
return this.restore.kind !== "unopened" && this.restore.source.kind === "parked"
|
|
188
|
+
? this.restore.source.saved
|
|
189
|
+
: undefined;
|
|
190
|
+
}
|
|
191
|
+
/** Set before entering either native open, whose effects may outlive an exception. */
|
|
192
|
+
get opened(): boolean {
|
|
193
|
+
return this.restore.kind !== "unopened";
|
|
194
|
+
}
|
|
182
195
|
/** Parked authority is exact state or nothing; never a journal-only reopen. */
|
|
183
196
|
open(): SessionManager {
|
|
184
197
|
const { sdk, launch } = this;
|
|
185
198
|
const { openWorkingSession: restore } = sdk;
|
|
186
199
|
const parked = parkedState(this.dir, launch, restore && sdk.readWorkingSession);
|
|
187
|
-
this.
|
|
200
|
+
this.restore = {
|
|
201
|
+
kind: "opening",
|
|
202
|
+
source: parked ? { kind: "parked", ...parked } : { kind: "journal" },
|
|
203
|
+
};
|
|
188
204
|
if (!parked || !restore) {
|
|
189
205
|
return sdk.SessionManager.open(launch.sessionFile, undefined, launch.cwd);
|
|
190
206
|
}
|
|
191
|
-
this.saved = parked.saved;
|
|
192
|
-
this.restored = parked.state;
|
|
193
207
|
return restore(parked.saved);
|
|
194
208
|
}
|
|
195
209
|
/** The ready restored generation owns this state now; a later park writes a new cut. */
|
|
196
210
|
adopt(): void {
|
|
197
|
-
if (this.
|
|
211
|
+
if (this.restore.kind !== "opening") {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const { source } = this.restore;
|
|
215
|
+
if (source.kind === "parked") {
|
|
198
216
|
fs.rmSync(this.file);
|
|
199
|
-
fs.rmSync(
|
|
217
|
+
fs.rmSync(source.state, { force: true });
|
|
200
218
|
}
|
|
219
|
+
this.restore = { kind: "adopted", source };
|
|
201
220
|
}
|
|
202
|
-
/**
|
|
203
|
-
async park(runtime: Runtime, actor: Resident): Promise<
|
|
221
|
+
/** Both exit outcomes have disposed the runtime; only one publishes parked authority. */
|
|
222
|
+
async park(runtime: Runtime, actor: Resident): Promise<ParkOutcome> {
|
|
204
223
|
if (!actor.pi || !this.due(runtime.session, actor.busy) || this.hasShell()) {
|
|
205
|
-
return
|
|
224
|
+
return "resident";
|
|
206
225
|
}
|
|
207
226
|
const hold = await this.prepare(runtime, actor.pi, actor);
|
|
208
|
-
|
|
209
|
-
await this.commit(runtime, hold, actor.errors);
|
|
210
|
-
}
|
|
211
|
-
return hold !== undefined;
|
|
227
|
+
return hold ? this.commit(runtime, hold, actor.errors) : "resident";
|
|
212
228
|
}
|
|
213
229
|
private hasShell(): boolean {
|
|
214
230
|
const witness = this.shell === undefined ? undefined : shell(this.shell, this.token);
|
|
@@ -259,7 +275,11 @@ export class Parking {
|
|
|
259
275
|
return undefined;
|
|
260
276
|
}
|
|
261
277
|
/** Commit: native strict exit, then capture; failure leaves no parked authority. */
|
|
262
|
-
private async commit(
|
|
278
|
+
private async commit(
|
|
279
|
+
runtime: Runtime,
|
|
280
|
+
hold: WorkingHold,
|
|
281
|
+
errors: string[],
|
|
282
|
+
): Promise<Exclude<ParkOutcome, "resident">> {
|
|
263
283
|
const save = this.sdk.writeWorkingSession;
|
|
264
284
|
if (save && finalizing(runtime)) {
|
|
265
285
|
runtime.setFinalization((final) => this.capture(final, save), { strictShutdown: true });
|
|
@@ -275,7 +295,9 @@ export class Parking {
|
|
|
275
295
|
if (read(this.file, parkedSchema)?.token !== this.token) {
|
|
276
296
|
// Truthful boundary: retained input stays queued and nothing restarts implicitly.
|
|
277
297
|
write(path.join(this.dir, "park-failed.json"), { token: this.token, errors });
|
|
298
|
+
return "exit-failed";
|
|
278
299
|
}
|
|
300
|
+
return "exit-parked";
|
|
279
301
|
}
|
|
280
302
|
private async capture(
|
|
281
303
|
session: AgentSession,
|
package/src/agent/runner.ts
CHANGED
|
@@ -8,12 +8,13 @@ import {
|
|
|
8
8
|
type Command,
|
|
9
9
|
type Launch,
|
|
10
10
|
type Result,
|
|
11
|
+
type AccountingState,
|
|
11
12
|
} from "./contracts.ts";
|
|
12
13
|
import { launchAt, missing, read, runDirectory, write } from "./storage.ts";
|
|
13
14
|
import { ownIdentity, ParentCheck, shells, signalProcess } from "./process.ts";
|
|
14
15
|
import { errorText, loadSDK } from "./host.ts";
|
|
15
|
-
import { Parking, type Runtime } from "./parking.ts";
|
|
16
|
-
import { UsageIndex } from "./usage.ts";
|
|
16
|
+
import { Parking, type ParkOutcome, type Runtime } from "./parking.ts";
|
|
17
|
+
import { accountingFields, UsageIndex } from "./usage.ts";
|
|
17
18
|
import { initializeCwd } from "./cwd.ts";
|
|
18
19
|
import { childRuntime } from "./child-runtime.ts";
|
|
19
20
|
import { ActorObservation, type ObservationStats } from "./observation.ts";
|
|
@@ -56,7 +57,7 @@ class Actor {
|
|
|
56
57
|
private stopping = false;
|
|
57
58
|
private stopDeadline?: NodeJS.Timeout;
|
|
58
59
|
private nextParentCheck = 0;
|
|
59
|
-
private parking =
|
|
60
|
+
private parking: ParkOutcome = "resident";
|
|
60
61
|
private readonly parent = new ParentCheck();
|
|
61
62
|
// Raised when durable commands may need admission: change hints and stop.
|
|
62
63
|
private readonly wake = new Wake();
|
|
@@ -110,10 +111,7 @@ class Actor {
|
|
|
110
111
|
owner: launch.owner,
|
|
111
112
|
status: completionStatus(this.stopping, message, error),
|
|
112
113
|
endedAt: Date.now(),
|
|
113
|
-
...(accounting
|
|
114
|
-
? { terminalEntryId: terminal.id }
|
|
115
|
-
: {}),
|
|
116
|
-
...accounting,
|
|
114
|
+
...accountingFields(accounting),
|
|
117
115
|
...(failure !== undefined ? { error: failure } : {}),
|
|
118
116
|
};
|
|
119
117
|
fs.writeFileSync(path.join(runDirectory(this.base, launch.id), "output.txt"), output, {
|
|
@@ -121,20 +119,21 @@ class Actor {
|
|
|
121
119
|
});
|
|
122
120
|
write(path.join(runDirectory(this.base, launch.id), "result.json"), result);
|
|
123
121
|
}
|
|
124
|
-
private usage(
|
|
125
|
-
launch: Launch,
|
|
126
|
-
baseline: ReadonlySet<string>,
|
|
127
|
-
terminal?: string,
|
|
128
|
-
): Pick<Result, "contributions" | "accountingError" | "accountingPending"> {
|
|
122
|
+
private usage(launch: Launch, baseline: ReadonlySet<string>, terminal?: string): AccountingState {
|
|
129
123
|
try {
|
|
130
|
-
return { contributions: this.accounting.read(launch, baseline, terminal) };
|
|
131
|
-
} catch (error) {
|
|
132
124
|
return {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
: {}),
|
|
125
|
+
kind: "verified",
|
|
126
|
+
contributions: this.accounting.read(launch, baseline, terminal),
|
|
127
|
+
terminalEntryId: terminal,
|
|
137
128
|
};
|
|
129
|
+
} catch (error) {
|
|
130
|
+
return terminal === undefined
|
|
131
|
+
? { kind: "unavailable", error: errorText(error), terminalEntryId: undefined }
|
|
132
|
+
: {
|
|
133
|
+
kind: "pending",
|
|
134
|
+
error: errorText(error),
|
|
135
|
+
pending: { terminalCandidate: terminal, baseline: [...baseline] },
|
|
136
|
+
};
|
|
138
137
|
}
|
|
139
138
|
}
|
|
140
139
|
private output(message: AssistantMessage | undefined): string {
|
|
@@ -384,7 +383,7 @@ class Actor {
|
|
|
384
383
|
} finally {
|
|
385
384
|
clearInterval(watch);
|
|
386
385
|
}
|
|
387
|
-
return this.parking;
|
|
386
|
+
return this.parking !== "resident";
|
|
388
387
|
}
|
|
389
388
|
private failure(error: unknown, unopened?: string): void {
|
|
390
389
|
const launch = this.current ?? this.initial;
|
|
@@ -494,7 +493,7 @@ class Actor {
|
|
|
494
493
|
process.off("SIGTERM", this.stop);
|
|
495
494
|
process.off("SIGINT", this.stop);
|
|
496
495
|
const observation: ObservationStats | undefined = this.observation?.close();
|
|
497
|
-
if (
|
|
496
|
+
if (this.parking === "resident") {
|
|
498
497
|
await runtime?.dispose();
|
|
499
498
|
}
|
|
500
499
|
if (this.stopping) {
|
|
@@ -506,7 +505,7 @@ class Actor {
|
|
|
506
505
|
errors: this.errors,
|
|
507
506
|
...(observation === undefined ? {} : { observation }),
|
|
508
507
|
});
|
|
509
|
-
if (this.parking) {
|
|
508
|
+
if (this.parking !== "resident") {
|
|
510
509
|
// Parked authority requires actual exit; no leftover extension handle may keep it alive.
|
|
511
510
|
process.exit();
|
|
512
511
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Static } from "typebox";
|
|
2
2
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
3
3
|
import { Type } from "./typebox.ts";
|
|
4
|
+
import { thinkingSchema } from "./contracts.ts";
|
|
4
5
|
|
|
5
6
|
// Main-session tool parameters only; the detached runner must not load pi-ai values.
|
|
6
7
|
export const delegateSchema = Type.Object(
|
|
@@ -15,6 +16,7 @@ export const delegateSchema = Type.Object(
|
|
|
15
16
|
"Exact provider/model ID (:thinking and any case need host support); no fuzzy match or fallback.",
|
|
16
17
|
}),
|
|
17
18
|
),
|
|
19
|
+
thinking: Type.Optional(thinkingSchema),
|
|
18
20
|
context: Type.Optional(StringEnum(["fresh", "fork"] as const)),
|
|
19
21
|
},
|
|
20
22
|
{ additionalProperties: false },
|