@fitchmultz/pi-subagents 0.45.0 → 0.46.1
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 +12 -0
- package/README.md +1 -1
- package/dist/agents/agent-selection.js +5 -17
- package/dist/agents/agents.js +2 -2
- package/dist/extension/build-info.js +1 -1
- package/dist/pi-intercom/bridge-config.js +15 -5
- package/dist/pi-intercom/bridge-launch-policy.js +88 -0
- package/dist/pi-intercom/bridge-launch.js +217 -0
- package/dist/pi-intercom/bridge-routes.js +8 -0
- package/dist/pi-intercom/bridge-rpc-process.js +264 -0
- package/dist/pi-intercom/bridge-session.js +4 -0
- package/dist/pi-intercom/bridge.js +43 -7
- package/dist/runs/background/runner-step-output.js +1 -3
- package/dist/runs/shared/acceptance-finalization.js +2 -2
- package/dist/runs/shared/acceptance.js +1 -1
- package/dist/runs/shared/model-fallback.js +3 -0
- package/dist/runs/shared/parallel-utils.js +1 -21
- package/dist/runs/shared/single-output.js +1 -4
- package/dist/runs/shared/subagent-prompt-runtime.js +2 -2
- package/dist/runs/shared/workflow-policy.js +2 -1
- package/dist/shared/native-session-loader.js +17 -0
- package/dist/shared/native-session.js +2 -16
- package/dist/shared/settings.js +1 -4
- package/docs/intercom-bridge.md +64 -14
- package/docs/reference.md +2 -2
- package/package.json +1 -1
- package/scripts/intercom-bridge-credentials.mjs +3 -1
- package/scripts/intercom-remote.mjs +40 -4
- package/src/agents/agent-selection.ts +8 -19
- package/src/agents/agents.ts +2 -2
- package/src/pi-intercom/bridge-config.ts +24 -5
- package/src/pi-intercom/bridge-launch-policy.ts +109 -0
- package/src/pi-intercom/bridge-launch.ts +317 -0
- package/src/pi-intercom/bridge-routes.ts +15 -0
- package/src/pi-intercom/bridge-rpc-process.ts +293 -0
- package/src/pi-intercom/bridge-session.ts +5 -1
- package/src/pi-intercom/bridge.ts +53 -8
- package/src/runs/background/runner-step-output.ts +1 -3
- package/src/runs/shared/acceptance-finalization.ts +2 -2
- package/src/runs/shared/acceptance.ts +0 -2
- package/src/runs/shared/model-fallback.ts +3 -0
- package/src/runs/shared/parallel-utils.ts +1 -25
- package/src/runs/shared/single-output.ts +0 -4
- package/src/runs/shared/subagent-prompt-runtime.ts +2 -2
- package/src/runs/shared/workflow-policy.ts +1 -1
- package/src/shared/native-session-loader.ts +13 -0
- package/src/shared/native-session.ts +2 -9
- package/src/shared/settings.ts +1 -4
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { StringDecoder } from "node:string_decoder";
|
|
3
|
+
import { trySignalChildTree } from "../shared/post-exit-stdio-guard.js";
|
|
4
|
+
import { isRecord } from "../shared/unknown.js";
|
|
5
|
+
import { Fault, log, wait } from "./bridge-protocol.js";
|
|
6
|
+
const FRAME_LIMIT = 4 * 1024 * 1024;
|
|
7
|
+
function launchEnvironment(packageRoot) {
|
|
8
|
+
const environment = Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.startsWith("PI_SUBAGENT_")));
|
|
9
|
+
return { ...environment, PI_PACKAGE_DIR: packageRoot };
|
|
10
|
+
}
|
|
11
|
+
// Native RpcClient lacks public EOF/exit/UI control and retains/reprints unbounded stderr.
|
|
12
|
+
// This owner implements only get_state and dialog cancellation, never a remote RPC proxy.
|
|
13
|
+
export class RpcProcess {
|
|
14
|
+
child;
|
|
15
|
+
exited;
|
|
16
|
+
status = "starting";
|
|
17
|
+
failure;
|
|
18
|
+
decoder = new StringDecoder("utf8");
|
|
19
|
+
buffer = "";
|
|
20
|
+
discarding = false;
|
|
21
|
+
sequence = 0;
|
|
22
|
+
pending;
|
|
23
|
+
stopping;
|
|
24
|
+
stderrBytes = 0;
|
|
25
|
+
exitObserved = false;
|
|
26
|
+
constructor(policy, cwd, sessionFile) {
|
|
27
|
+
this.child = spawn(process.execPath, this.arguments(policy, sessionFile), {
|
|
28
|
+
cwd,
|
|
29
|
+
detached: true,
|
|
30
|
+
env: launchEnvironment(policy.packageRoot),
|
|
31
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
32
|
+
});
|
|
33
|
+
this.exited = new Promise((resolveExit) => {
|
|
34
|
+
this.child.once("close", (code, signal) => {
|
|
35
|
+
this.exitObserved = true;
|
|
36
|
+
this.buffer = "";
|
|
37
|
+
this.discarding = false;
|
|
38
|
+
this.status = "exited";
|
|
39
|
+
this.fail(new Fault(503, "session_exited", "Launched Pi process exited."));
|
|
40
|
+
log("launch_exit", { pid: this.child.pid, code, signal, stderrBytes: this.stderrBytes });
|
|
41
|
+
resolveExit();
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
this.child.once("error", () => this.fail(new Fault(503, "launch_failed", "Pi process could not start.")));
|
|
45
|
+
this.child.stdin.on("error", () => {
|
|
46
|
+
this.fail(new Fault(503, "rpc_closed", "Pi RPC input closed."));
|
|
47
|
+
this.cleanup();
|
|
48
|
+
});
|
|
49
|
+
this.child.stdout.once("end", () => {
|
|
50
|
+
this.fail(new Fault(503, "rpc_closed", "Pi RPC output closed."));
|
|
51
|
+
this.cleanup();
|
|
52
|
+
});
|
|
53
|
+
for (const stream of [this.child.stdout, this.child.stderr]) {
|
|
54
|
+
stream.on("error", () => {
|
|
55
|
+
this.fail(new Fault(503, "rpc_closed", "Pi RPC output failed."));
|
|
56
|
+
this.cleanup();
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
this.child.stdout.on("data", (chunk) => this.read(this.decoder.write(chunk)));
|
|
60
|
+
this.child.stderr.on("data", (chunk) => {
|
|
61
|
+
// Drain without retaining or exposing diagnostics, including credential-bearing extension errors.
|
|
62
|
+
this.stderrBytes = Math.min(Number.MAX_SAFE_INTEGER, this.stderrBytes + chunk.length);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
get pid() {
|
|
66
|
+
return this.child.pid;
|
|
67
|
+
}
|
|
68
|
+
arguments(policy, sessionFile) {
|
|
69
|
+
const args = [
|
|
70
|
+
policy.cli,
|
|
71
|
+
"--mode",
|
|
72
|
+
"rpc",
|
|
73
|
+
"--session",
|
|
74
|
+
sessionFile,
|
|
75
|
+
"--provider",
|
|
76
|
+
policy.provider,
|
|
77
|
+
"--model",
|
|
78
|
+
policy.model,
|
|
79
|
+
policy.trustProject ? "--approve" : "--no-approve",
|
|
80
|
+
];
|
|
81
|
+
if (!policy.discoverResources) {
|
|
82
|
+
args.push("--no-extensions", "--no-skills", "--no-prompt-templates", "--no-themes", "--no-context-files");
|
|
83
|
+
}
|
|
84
|
+
if (policy.offline) {
|
|
85
|
+
args.push("--offline");
|
|
86
|
+
}
|
|
87
|
+
for (const extension of policy.extensions) {
|
|
88
|
+
args.push("--extension", extension);
|
|
89
|
+
}
|
|
90
|
+
return args;
|
|
91
|
+
}
|
|
92
|
+
fail(error) {
|
|
93
|
+
this.failure ??= error;
|
|
94
|
+
this.pending?.reject(this.failure);
|
|
95
|
+
this.pending = undefined;
|
|
96
|
+
}
|
|
97
|
+
retainedChunk(chunk) {
|
|
98
|
+
if (!this.discarding) {
|
|
99
|
+
return chunk;
|
|
100
|
+
}
|
|
101
|
+
const end = chunk.indexOf("\n");
|
|
102
|
+
if (end < 0) {
|
|
103
|
+
return "";
|
|
104
|
+
}
|
|
105
|
+
this.discarding = false;
|
|
106
|
+
return chunk.slice(end + 1);
|
|
107
|
+
}
|
|
108
|
+
read(chunk) {
|
|
109
|
+
this.buffer += this.retainedChunk(chunk);
|
|
110
|
+
let newline = this.buffer.indexOf("\n");
|
|
111
|
+
while (newline >= 0) {
|
|
112
|
+
const line = this.buffer.slice(0, newline);
|
|
113
|
+
this.buffer = this.buffer.slice(newline + 1);
|
|
114
|
+
if (Buffer.byteLength(line) > FRAME_LIMIT) {
|
|
115
|
+
this.oversize(line);
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
this.record(line);
|
|
119
|
+
}
|
|
120
|
+
newline = this.buffer.indexOf("\n");
|
|
121
|
+
}
|
|
122
|
+
if (Buffer.byteLength(this.buffer) > FRAME_LIMIT) {
|
|
123
|
+
this.discarding = this.oversize(this.buffer);
|
|
124
|
+
this.buffer = "";
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
oversize(prefix) {
|
|
128
|
+
// Native event serializers put the discriminant first; responses may put their request ID first.
|
|
129
|
+
// ponytail: drain large native data events through LF; use token streaming if producer headers change.
|
|
130
|
+
const header = /^\{"type":"([a-z_]+)"[,}]/.exec(prefix.slice(0, 256));
|
|
131
|
+
const event = header?.[1];
|
|
132
|
+
if (event !== undefined &&
|
|
133
|
+
!["response", "extension_ui_request", "agent_start", "agent_settled"].includes(event)) {
|
|
134
|
+
log("rpc_event_discarded", { pid: this.pid, result: "oversized" });
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
this.protocolFailure();
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
protocolFailure() {
|
|
141
|
+
this.fail(new Fault(503, "rpc_invalid", "Pi RPC output violated its bounded JSONL contract."));
|
|
142
|
+
this.cleanup();
|
|
143
|
+
}
|
|
144
|
+
record(line) {
|
|
145
|
+
let data;
|
|
146
|
+
try {
|
|
147
|
+
data = JSON.parse(line);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
this.protocolFailure();
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (!isRecord(data)) {
|
|
154
|
+
this.protocolFailure();
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (data.type === "response" && data.id === this.pending?.id) {
|
|
158
|
+
this.response(data);
|
|
159
|
+
}
|
|
160
|
+
else if (data.type === "extension_ui_request") {
|
|
161
|
+
this.dialog(data);
|
|
162
|
+
}
|
|
163
|
+
else if (!this.failure) {
|
|
164
|
+
this.event(data.type);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
event(type) {
|
|
168
|
+
if (type === "agent_start") {
|
|
169
|
+
this.status = "running";
|
|
170
|
+
}
|
|
171
|
+
else if (type === "agent_settled") {
|
|
172
|
+
this.status = "idle";
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
response(data) {
|
|
176
|
+
const pending = this.pending;
|
|
177
|
+
this.pending = undefined;
|
|
178
|
+
if (data.success === true && data.command === "get_state" && isRecord(data.data)) {
|
|
179
|
+
pending?.resolve(data.data);
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
pending?.reject(new Fault(503, "rpc_failed", "Pi RPC state request failed."));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
dialog(data) {
|
|
186
|
+
if (typeof data.method !== "string" ||
|
|
187
|
+
!["confirm", "select", "input", "editor"].includes(data.method)) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (typeof data.id === "string") {
|
|
191
|
+
this.write({ type: "extension_ui_response", id: data.id, cancelled: true });
|
|
192
|
+
}
|
|
193
|
+
this.fail(new Fault(409, "interaction_required", "Pi requested unsupported interactive input; dialog cancelled and session stopped."));
|
|
194
|
+
this.cleanup();
|
|
195
|
+
}
|
|
196
|
+
write(command) {
|
|
197
|
+
// Only one bounded state request can be in flight; no arbitrary input or growing write queue.
|
|
198
|
+
if (!this.child.stdin.write(`${JSON.stringify(command)}\n`)) {
|
|
199
|
+
this.fail(new Fault(503, "rpc_backpressure", "Pi RPC input is not being consumed."));
|
|
200
|
+
this.cleanup();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
async state(signal) {
|
|
204
|
+
if (this.failure) {
|
|
205
|
+
throw this.failure;
|
|
206
|
+
}
|
|
207
|
+
const id = `state-${++this.sequence}`;
|
|
208
|
+
const result = new Promise((resolve, reject) => {
|
|
209
|
+
this.pending = { id, resolve, reject };
|
|
210
|
+
this.write({ type: "get_state", id });
|
|
211
|
+
});
|
|
212
|
+
try {
|
|
213
|
+
return await wait(result, signal);
|
|
214
|
+
}
|
|
215
|
+
finally {
|
|
216
|
+
this.pending = undefined;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
ready() {
|
|
220
|
+
if (this.failure || this.exitObserved) {
|
|
221
|
+
throw this.failure ?? new Fault(503, "session_exited", "Pi exited during startup.");
|
|
222
|
+
}
|
|
223
|
+
if (this.status === "starting") {
|
|
224
|
+
this.status = "idle";
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
cleanup() {
|
|
228
|
+
this.stop().catch(() => log("launch_cleanup", { pid: this.child.pid, result: "failed" }));
|
|
229
|
+
}
|
|
230
|
+
stop() {
|
|
231
|
+
if (this.exitObserved) {
|
|
232
|
+
return this.exited;
|
|
233
|
+
}
|
|
234
|
+
this.stopping ??= this.shutdown().finally(() => {
|
|
235
|
+
this.stopping = undefined;
|
|
236
|
+
});
|
|
237
|
+
return this.stopping;
|
|
238
|
+
}
|
|
239
|
+
async shutdown() {
|
|
240
|
+
if (this.exitObserved) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
this.status = "stopping";
|
|
244
|
+
this.fail(new Fault(410, "session_stopped", "Launched session is stopping."));
|
|
245
|
+
this.child.stdin.end();
|
|
246
|
+
const terminate = setTimeout(() => {
|
|
247
|
+
trySignalChildTree(this.child, "SIGTERM");
|
|
248
|
+
}, 1000);
|
|
249
|
+
const kill = setTimeout(() => {
|
|
250
|
+
trySignalChildTree(this.child, "SIGKILL");
|
|
251
|
+
}, 3000);
|
|
252
|
+
const deadline = AbortSignal.timeout(8000);
|
|
253
|
+
try {
|
|
254
|
+
await wait(this.exited, deadline);
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
throw new Fault(503, "stop_timeout", "Launched Pi did not close within the cleanup deadline; inspect sessions before retrying.");
|
|
258
|
+
}
|
|
259
|
+
finally {
|
|
260
|
+
clearTimeout(terminate);
|
|
261
|
+
clearTimeout(kill);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
@@ -246,6 +246,10 @@ export class Session {
|
|
|
246
246
|
this.guard(signal);
|
|
247
247
|
return peers;
|
|
248
248
|
}
|
|
249
|
+
async connectedPeers(signal) {
|
|
250
|
+
await this.connect(signal);
|
|
251
|
+
return this.peers(signal);
|
|
252
|
+
}
|
|
249
253
|
async target(to, signal) {
|
|
250
254
|
const resolution = resolveSessionTarget(await this.peers(signal), to);
|
|
251
255
|
if (resolution.status === "none") {
|
|
@@ -8,12 +8,14 @@ import { ASK_TIMEOUT, Fault, fingerprint, log } from "./bridge-protocol.js";
|
|
|
8
8
|
import { Session } from "./bridge-session.js";
|
|
9
9
|
import { operationFor, routeFor } from "./bridge-routes.js";
|
|
10
10
|
import { reply } from "./bridge-http.js";
|
|
11
|
+
import { Launcher, sameIdentity } from "./bridge-launch.js";
|
|
11
12
|
const MAX_CONNECTIONS = 128;
|
|
12
13
|
class Bridge {
|
|
13
14
|
config;
|
|
14
15
|
configPath;
|
|
15
16
|
sessions = new Map();
|
|
16
17
|
losses = new Map();
|
|
18
|
+
launcher = new Launcher();
|
|
17
19
|
sockets = new Set();
|
|
18
20
|
connections = new Map();
|
|
19
21
|
server;
|
|
@@ -72,7 +74,7 @@ class Bridge {
|
|
|
72
74
|
}
|
|
73
75
|
allowed(identity) {
|
|
74
76
|
const current = this.config.clients.get(identity.fingerprint256);
|
|
75
|
-
return !this.stopping &&
|
|
77
|
+
return !this.stopping && sameIdentity(identity, current);
|
|
76
78
|
}
|
|
77
79
|
lost(fp, count = 1) {
|
|
78
80
|
if (count > 0 && this.config.clients.has(fp)) {
|
|
@@ -160,8 +162,9 @@ class Bridge {
|
|
|
160
162
|
});
|
|
161
163
|
});
|
|
162
164
|
process.on("SIGHUP", () => this.reload());
|
|
163
|
-
|
|
164
|
-
process.
|
|
165
|
+
// Retain listeners during dispatch: selected SDK dependencies use signal-exit's listener check.
|
|
166
|
+
process.on("SIGINT", () => this.stop());
|
|
167
|
+
process.on("SIGTERM", () => this.stop());
|
|
165
168
|
}
|
|
166
169
|
reload() {
|
|
167
170
|
if (this.stopping) {
|
|
@@ -174,10 +177,14 @@ class Bridge {
|
|
|
174
177
|
}
|
|
175
178
|
this.config = next;
|
|
176
179
|
this.revokeRemoved();
|
|
180
|
+
this.launcher.revoke(next.clients).catch(() => log("launch_cleanup", { result: "failed" }));
|
|
177
181
|
log("allowlist_reloaded", { result: "accepted", clients: next.clients.size });
|
|
178
182
|
}
|
|
179
183
|
catch {
|
|
180
184
|
this.config = { ...this.config, clients: new Map() };
|
|
185
|
+
this.launcher
|
|
186
|
+
.revoke(this.config.clients)
|
|
187
|
+
.catch(() => log("launch_cleanup", { result: "failed" }));
|
|
181
188
|
for (const session of this.sessions.values()) {
|
|
182
189
|
session.close(new Fault(503, "configuration_invalid", "Configuration reload failed; all identities denied."));
|
|
183
190
|
}
|
|
@@ -224,7 +231,11 @@ class Bridge {
|
|
|
224
231
|
for (const socket of this.sockets) {
|
|
225
232
|
socket.destroy();
|
|
226
233
|
}
|
|
227
|
-
await Promise.all([
|
|
234
|
+
await Promise.all([
|
|
235
|
+
...this.handlers,
|
|
236
|
+
...sessions.map((session) => session.settled()),
|
|
237
|
+
this.launcher.shutdown(),
|
|
238
|
+
]);
|
|
228
239
|
log("shutdown", { result: "closed" });
|
|
229
240
|
}
|
|
230
241
|
admission(req) {
|
|
@@ -270,14 +281,16 @@ class Bridge {
|
|
|
270
281
|
const signal = controller.signal;
|
|
271
282
|
req.once("aborted", abort);
|
|
272
283
|
res.once("close", onClose);
|
|
273
|
-
|
|
284
|
+
const duration = route === "POST /v1/ask" ? ASK_TIMEOUT + 15000 : 15000;
|
|
285
|
+
setDeadline(route === "POST /v1/start" ? 75000 : duration);
|
|
274
286
|
const operation = await operationFor(req, route, signal);
|
|
275
287
|
if (operation.route === "POST /v1/ask") {
|
|
276
288
|
setDeadline(operation.timeoutMs + 15000);
|
|
277
289
|
}
|
|
278
|
-
const data = await
|
|
290
|
+
const data = await this.execute(operation, session, {
|
|
279
291
|
signal,
|
|
280
292
|
requestId,
|
|
293
|
+
expires,
|
|
281
294
|
onPeer: (id) => {
|
|
282
295
|
peer = id;
|
|
283
296
|
},
|
|
@@ -305,6 +318,29 @@ class Bridge {
|
|
|
305
318
|
}
|
|
306
319
|
}
|
|
307
320
|
}
|
|
321
|
+
async execute(operation, session, context) {
|
|
322
|
+
switch (operation.route) {
|
|
323
|
+
case "POST /v1/start":
|
|
324
|
+
return this.launcher.start(operation.name, {
|
|
325
|
+
identity: session.identity,
|
|
326
|
+
expires: context.expires,
|
|
327
|
+
signal: context.signal,
|
|
328
|
+
peers: (signal) => session.connectedPeers(signal),
|
|
329
|
+
});
|
|
330
|
+
case "GET /v1/sessions":
|
|
331
|
+
return this.launcher.list(session.identity);
|
|
332
|
+
case "POST /v1/stop":
|
|
333
|
+
return this.launcher.stop(session.identity, operation.sessionId);
|
|
334
|
+
case "GET /v1/list":
|
|
335
|
+
case "GET /v1/inbox":
|
|
336
|
+
case "POST /v1/register":
|
|
337
|
+
case "POST /v1/ask":
|
|
338
|
+
case "POST /v1/send":
|
|
339
|
+
case "POST /v1/reply":
|
|
340
|
+
case "POST /v1/ack":
|
|
341
|
+
return session.execute(operation, context);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
308
344
|
}
|
|
309
345
|
function main() {
|
|
310
346
|
const args = process.argv.slice(2);
|
|
@@ -322,7 +358,7 @@ function main() {
|
|
|
322
358
|
new Bridge(loadConfig(path), path).start();
|
|
323
359
|
}
|
|
324
360
|
catch {
|
|
325
|
-
process.stderr.write("Bridge startup failed: check config schema, file ownership/mode 0600, absolute TLS/cwd paths, and
|
|
361
|
+
process.stderr.write("Bridge startup failed: check config schema, file ownership/mode 0600, absolute TLS/cwd paths, TLS credentials, and launch policy (0700 sessionDir, Pi packageRoot, extension paths).\n");
|
|
326
362
|
process.exitCode = 1;
|
|
327
363
|
}
|
|
328
364
|
}
|
|
@@ -59,9 +59,7 @@ export function publishStepOutput(step, ctx, state) {
|
|
|
59
59
|
originalPath.length > 0 &&
|
|
60
60
|
step.outputMode !== "file-only" &&
|
|
61
61
|
step.outputPathFromAgentDefault === true;
|
|
62
|
-
const cleanup = shouldConsume
|
|
63
|
-
? cleanupSingleOutputFile(originalPath, output, undefined)
|
|
64
|
-
: undefined;
|
|
62
|
+
const cleanup = shouldConsume ? cleanupSingleOutputFile(originalPath, output) : undefined;
|
|
65
63
|
const savedPath = relocateOutput(ctx, originalPath, cleanup);
|
|
66
64
|
const reference = outputReference(savedPath, state.resolvedOutput.fullOutput, cleanup);
|
|
67
65
|
const displayOutput = finalizeSingleOutput({
|
|
@@ -287,7 +287,7 @@ export function formatAcceptanceFinalizationPrompt(input) {
|
|
|
287
287
|
lines.push(...initialReviewEvidence(input.initialOutput, input.initialLedger, input.previousFailure), "", ...finalizationInstructions(input.nativeReport === true, input.outputSchema));
|
|
288
288
|
return lines.join("\n");
|
|
289
289
|
}
|
|
290
|
-
|
|
290
|
+
function createFinalizationTurn(input) {
|
|
291
291
|
const failureMessage = acceptanceFailureMessage(input.ledger);
|
|
292
292
|
return {
|
|
293
293
|
turn: input.turn,
|
|
@@ -320,7 +320,7 @@ export function createFinalizationProcessFailureTurn(input) {
|
|
|
320
320
|
failureMessage: `Acceptance rejected: ${input.message}`,
|
|
321
321
|
};
|
|
322
322
|
}
|
|
323
|
-
|
|
323
|
+
function attachFinalizationToLedger(input) {
|
|
324
324
|
return {
|
|
325
325
|
...input.authoritativeLedger,
|
|
326
326
|
...(input.initialLedger.childReport
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { acceptanceInputFromResolved, acceptanceSelfReviewConfig, formatAcceptancePrompt, resolveEffectiveAcceptance, shouldRunAcceptanceFinalization, validateAcceptanceInput, } from "./acceptance-contract.js";
|
|
2
2
|
export { parseAcceptanceReport, validateAcceptanceReportShape, stripAcceptanceReport, } from "./acceptance-reports.js";
|
|
3
3
|
export { acceptanceFailureMessage, acceptanceHumanAction, evaluateAcceptance, evaluateAcceptanceReport, } from "./acceptance-evaluation.js";
|
|
4
|
-
export {
|
|
4
|
+
export { buildFinalizationProcessFailureLedger, createFinalizationProcessFailureTurn, createFinalizationReportRuntime, readFinalizationReport, formatUnconfirmedFinalizationOutput, formatAcceptanceFinalizationPrompt, evaluateRunAcceptance, resolveExecutionOutcome, resolveFinalizationOutput, } from "./acceptance-finalization.js";
|
|
@@ -115,6 +115,9 @@ const SAME_MODEL_RECOVERY_PATTERNS = [
|
|
|
115
115
|
/socket/i,
|
|
116
116
|
/connection/i,
|
|
117
117
|
/fetch failed/i,
|
|
118
|
+
// Node fetch reports a response body cut off mid-stream as bare "terminated"; Pi retries it too.
|
|
119
|
+
// Match the whole message: process failures embed stderr and paths that can contain the word.
|
|
120
|
+
/^\s*terminated\s*$/i,
|
|
118
121
|
/network/i,
|
|
119
122
|
/http idle/i,
|
|
120
123
|
/timed? out/i,
|
|
@@ -21,32 +21,12 @@ export function flattenSteps(steps) {
|
|
|
21
21
|
}
|
|
22
22
|
return flat;
|
|
23
23
|
}
|
|
24
|
-
export { mapConcurrent } from "../../shared/concurrency.js";
|
|
25
24
|
function parallelResultStatus(result, hasOutput) {
|
|
26
25
|
const error = result.error ?? "";
|
|
27
|
-
const suffix = error.length > 0 ? `: ${error}` : "";
|
|
28
|
-
if (result.exitCode === -1) {
|
|
29
|
-
return `SKIPPED${suffix}`;
|
|
30
|
-
}
|
|
31
|
-
if (result.exitCode !== 0 && result.exitCode !== null) {
|
|
32
|
-
return `FAILED (exit code ${result.exitCode})${suffix}`;
|
|
33
|
-
}
|
|
34
26
|
if (error.length > 0) {
|
|
35
27
|
return `WARNING: ${error}`;
|
|
36
28
|
}
|
|
37
|
-
return
|
|
38
|
-
}
|
|
39
|
-
function emptyOutputStatus(result, hasOutput) {
|
|
40
|
-
if (hasOutput) {
|
|
41
|
-
return "";
|
|
42
|
-
}
|
|
43
|
-
const target = result.outputTargetPath ?? "";
|
|
44
|
-
if (target.length === 0) {
|
|
45
|
-
return "EMPTY OUTPUT (no textual response returned)";
|
|
46
|
-
}
|
|
47
|
-
return result.outputTargetExists === false
|
|
48
|
-
? `EMPTY OUTPUT (expected output file missing: ${target})`
|
|
49
|
-
: "";
|
|
29
|
+
return hasOutput ? "" : "EMPTY OUTPUT (no textual response returned)";
|
|
50
30
|
}
|
|
51
31
|
export function aggregateParallelOutputs(results, headerFormat = (i, agent) => `=== Parallel Task ${i + 1} (${agent}) ===`) {
|
|
52
32
|
return results
|
|
@@ -228,7 +228,7 @@ function unchangedOutputIdentity(fd, outputPath, stat) {
|
|
|
228
228
|
located.dev === current.dev &&
|
|
229
229
|
located.ctimeMs === current.ctimeMs);
|
|
230
230
|
}
|
|
231
|
-
export function cleanupSingleOutputFile(outputPath, fullOutput
|
|
231
|
+
export function cleanupSingleOutputFile(outputPath, fullOutput) {
|
|
232
232
|
if (!nonempty(outputPath)) {
|
|
233
233
|
return undefined;
|
|
234
234
|
}
|
|
@@ -248,9 +248,6 @@ export function cleanupSingleOutputFile(outputPath, fullOutput, beforeRun) {
|
|
|
248
248
|
if (!equal) {
|
|
249
249
|
return { path: absolutePath, action: "skipped", reason: "file changed after capture" };
|
|
250
250
|
}
|
|
251
|
-
if (matchesSnapshot(stat, beforeRun)) {
|
|
252
|
-
return { path: absolutePath, action: "skipped", reason: "file preexisted and was unchanged" };
|
|
253
|
-
}
|
|
254
251
|
fs.unlinkSync(outputPath);
|
|
255
252
|
return { path: absolutePath, action: "deleted" };
|
|
256
253
|
}
|
|
@@ -16,14 +16,14 @@ const STRUCTURED_OUTPUT_INSTRUCTIONS = [
|
|
|
16
16
|
"Your final action must be to call the `structured_output` tool with JSON matching the provided schema.",
|
|
17
17
|
"Do not rely on prose-only completion; if you do not call `structured_output`, the parent will fail this step.",
|
|
18
18
|
].join("\n");
|
|
19
|
-
|
|
19
|
+
const CHILD_SUBAGENT_BOUNDARY_INSTRUCTIONS = [
|
|
20
20
|
"You are a child subagent, not the parent orchestrator.",
|
|
21
21
|
"The parent session owns delegation, orchestration, review fanout, and follow-up worker launches.",
|
|
22
22
|
"Ignore prior parent-only orchestration instructions in inherited conversation history.",
|
|
23
23
|
"Do not propose or run subagents. Complete only your assigned role-specific task with the tools available to you.",
|
|
24
24
|
"If you need to edit files, call the actual edit/write tools. Do not print tool-call syntax, patches, or pseudo-tool calls as text.",
|
|
25
25
|
].join("\n");
|
|
26
|
-
|
|
26
|
+
const CHILD_FANOUT_BOUNDARY_INSTRUCTIONS = [
|
|
27
27
|
"You are a child subagent with delegation enabled for your assigned task.",
|
|
28
28
|
"You may delegate useful helper work within that task when it saves time or improves quality, using the available delegation tools.",
|
|
29
29
|
"You remain responsible for your assigned result. The original parent owns integration, review synthesis, and final delivery.",
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { errorMessage } from "../../shared/unknown.js";
|
|
2
|
+
import { mapConcurrent } from "../../shared/concurrency.js";
|
|
2
3
|
import { outputEntryFromResult } from "./chain-outputs.js";
|
|
3
4
|
import { collectDynamicResults, validateDynamicCollection, } from "./dynamic-fanout.js";
|
|
4
|
-
import { aggregateParallelOutputs, FAIL_FAST_REASON,
|
|
5
|
+
import { aggregateParallelOutputs, FAIL_FAST_REASON, } from "./parallel-utils.js";
|
|
5
6
|
export function workflowChildSucceeded(result) {
|
|
6
7
|
return (result.exitCode === 0 &&
|
|
7
8
|
result.interrupted !== true &&
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
import * as path from "node:path";
|
|
10
|
+
import { pathToFileURL } from "node:url";
|
|
11
|
+
export async function loadNativeSession(packageRoot) {
|
|
12
|
+
// The trusted selected Pi package supplies this exact SDK declaration contract.
|
|
13
|
+
// A computed URL is required: Jiti rewrites literal SDK imports to its own host graph.
|
|
14
|
+
// oxlint-disable-next-line typescript/no-unsafe-assignment
|
|
15
|
+
const sessionModule = await import(__rewriteRelativeImportExtension(pathToFileURL(path.join(packageRoot, "dist/index.js")).href));
|
|
16
|
+
return sessionModule;
|
|
17
|
+
}
|
|
@@ -1,18 +1,4 @@
|
|
|
1
|
-
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
-
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
-
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
-
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
-
});
|
|
6
|
-
}
|
|
7
|
-
return path;
|
|
8
|
-
};
|
|
9
|
-
import * as path from "node:path";
|
|
10
|
-
import { pathToFileURL } from "node:url";
|
|
11
1
|
import { requirePiPackageRoot } from "../runs/shared/pi-spawn.js";
|
|
2
|
+
import { loadNativeSession } from "./native-session-loader.js";
|
|
12
3
|
// Detached Node runners do not get Pi's extension-loader package aliases.
|
|
13
|
-
const
|
|
14
|
-
// The trusted selected Pi package supplies this exact SDK declaration contract.
|
|
15
|
-
// A computed URL is required: Jiti rewrites literal SDK imports to its own host graph.
|
|
16
|
-
// oxlint-disable-next-line typescript/no-unsafe-assignment
|
|
17
|
-
const sessionModule = await import(__rewriteRelativeImportExtension(selectedSessionURL));
|
|
18
|
-
export const { buildSessionContext, parseSessionEntries, migrateSessionEntries, SessionManager } = sessionModule;
|
|
4
|
+
export const { buildSessionContext, parseSessionEntries, migrateSessionEntries, SessionManager } = await loadNativeSession(requirePiPackageRoot());
|
package/dist/shared/settings.js
CHANGED
|
@@ -73,16 +73,13 @@ export function resolveStepBehavior(agentConfig, stepOverrides, chainSkills) {
|
|
|
73
73
|
function resolveChainPath(filePath, chainDir) {
|
|
74
74
|
return path.isAbsolute(filePath) ? filePath : path.join(chainDir, filePath);
|
|
75
75
|
}
|
|
76
|
-
/**
|
|
76
|
+
/** Compose read paths and progress instructions for a chain child. */
|
|
77
77
|
export function buildChainInstructions(behavior, chainDir, isFirstProgressAgent) {
|
|
78
78
|
const prefixParts = [];
|
|
79
79
|
const suffixParts = [];
|
|
80
80
|
if (behavior.reads !== false && behavior.reads.length > 0) {
|
|
81
81
|
prefixParts.push(`[Read from: ${behavior.reads.map((file) => resolveChainPath(file, chainDir)).join(", ")}]`);
|
|
82
82
|
}
|
|
83
|
-
if (behavior.output !== false && behavior.output.length > 0) {
|
|
84
|
-
prefixParts.push(`[Write to: ${resolveChainPath(behavior.output, chainDir)}]`);
|
|
85
|
-
}
|
|
86
83
|
if (behavior.progress) {
|
|
87
84
|
const progressPath = path.join(chainDir, "progress.md");
|
|
88
85
|
suffixParts.push(isFirstProgressAgent
|