@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
|
@@ -8,8 +8,9 @@ import { getBrokerSocketPath } from "./broker/paths.ts";
|
|
|
8
8
|
import { loadConfig, sameListener, type Config, type Identity } from "./bridge-config.ts";
|
|
9
9
|
import { ASK_TIMEOUT, Fault, fingerprint, log } from "./bridge-protocol.ts";
|
|
10
10
|
import { Session } from "./bridge-session.ts";
|
|
11
|
-
import { operationFor, routeFor } from "./bridge-routes.ts";
|
|
11
|
+
import { operationFor, routeFor, type Operation } from "./bridge-routes.ts";
|
|
12
12
|
import { reply } from "./bridge-http.ts";
|
|
13
|
+
import { Launcher, sameIdentity } from "./bridge-launch.ts";
|
|
13
14
|
|
|
14
15
|
const MAX_CONNECTIONS = 128;
|
|
15
16
|
|
|
@@ -24,6 +25,7 @@ class Bridge {
|
|
|
24
25
|
private readonly configPath: string;
|
|
25
26
|
private readonly sessions = new Map<string, Session>();
|
|
26
27
|
private readonly losses = new Map<string, number>();
|
|
28
|
+
private readonly launcher = new Launcher();
|
|
27
29
|
private readonly sockets = new Set<Socket>();
|
|
28
30
|
private readonly connections = new Map<TLSSocket, Connection>();
|
|
29
31
|
private readonly server: Server;
|
|
@@ -88,7 +90,7 @@ class Bridge {
|
|
|
88
90
|
}
|
|
89
91
|
private allowed(identity: Identity): boolean {
|
|
90
92
|
const current = this.config.clients.get(identity.fingerprint256);
|
|
91
|
-
return !this.stopping &&
|
|
93
|
+
return !this.stopping && sameIdentity(identity, current);
|
|
92
94
|
}
|
|
93
95
|
private lost(fp: string, count = 1): void {
|
|
94
96
|
if (count > 0 && this.config.clients.has(fp)) {
|
|
@@ -181,8 +183,9 @@ class Bridge {
|
|
|
181
183
|
});
|
|
182
184
|
});
|
|
183
185
|
process.on("SIGHUP", () => this.reload());
|
|
184
|
-
|
|
185
|
-
process.
|
|
186
|
+
// Retain listeners during dispatch: selected SDK dependencies use signal-exit's listener check.
|
|
187
|
+
process.on("SIGINT", () => this.stop());
|
|
188
|
+
process.on("SIGTERM", () => this.stop());
|
|
186
189
|
}
|
|
187
190
|
private reload(): void {
|
|
188
191
|
if (this.stopping) {
|
|
@@ -195,9 +198,13 @@ class Bridge {
|
|
|
195
198
|
}
|
|
196
199
|
this.config = next;
|
|
197
200
|
this.revokeRemoved();
|
|
201
|
+
this.launcher.revoke(next.clients).catch(() => log("launch_cleanup", { result: "failed" }));
|
|
198
202
|
log("allowlist_reloaded", { result: "accepted", clients: next.clients.size });
|
|
199
203
|
} catch {
|
|
200
204
|
this.config = { ...this.config, clients: new Map() };
|
|
205
|
+
this.launcher
|
|
206
|
+
.revoke(this.config.clients)
|
|
207
|
+
.catch(() => log("launch_cleanup", { result: "failed" }));
|
|
201
208
|
for (const session of this.sessions.values()) {
|
|
202
209
|
session.close(
|
|
203
210
|
new Fault(
|
|
@@ -253,7 +260,11 @@ class Bridge {
|
|
|
253
260
|
for (const socket of this.sockets) {
|
|
254
261
|
socket.destroy();
|
|
255
262
|
}
|
|
256
|
-
await Promise.all([
|
|
263
|
+
await Promise.all([
|
|
264
|
+
...this.handlers,
|
|
265
|
+
...sessions.map((session) => session.settled()),
|
|
266
|
+
this.launcher.shutdown(),
|
|
267
|
+
]);
|
|
257
268
|
log("shutdown", { result: "closed" });
|
|
258
269
|
}
|
|
259
270
|
private admission(req: IncomingMessage): {
|
|
@@ -317,14 +328,16 @@ class Bridge {
|
|
|
317
328
|
const signal = controller.signal;
|
|
318
329
|
req.once("aborted", abort);
|
|
319
330
|
res.once("close", onClose);
|
|
320
|
-
|
|
331
|
+
const duration = route === "POST /v1/ask" ? ASK_TIMEOUT + 15000 : 15000;
|
|
332
|
+
setDeadline(route === "POST /v1/start" ? 75000 : duration);
|
|
321
333
|
const operation = await operationFor(req, route, signal);
|
|
322
334
|
if (operation.route === "POST /v1/ask") {
|
|
323
335
|
setDeadline(operation.timeoutMs + 15000);
|
|
324
336
|
}
|
|
325
|
-
const data = await
|
|
337
|
+
const data = await this.execute(operation, session, {
|
|
326
338
|
signal,
|
|
327
339
|
requestId,
|
|
340
|
+
expires,
|
|
328
341
|
onPeer: (id) => {
|
|
329
342
|
peer = id;
|
|
330
343
|
},
|
|
@@ -351,6 +364,38 @@ class Bridge {
|
|
|
351
364
|
}
|
|
352
365
|
}
|
|
353
366
|
}
|
|
367
|
+
private async execute(
|
|
368
|
+
operation: Operation,
|
|
369
|
+
session: Readonly<Session>,
|
|
370
|
+
context: {
|
|
371
|
+
readonly signal: AbortSignal;
|
|
372
|
+
readonly requestId: string;
|
|
373
|
+
readonly expires: number;
|
|
374
|
+
readonly onPeer: (id: string) => void;
|
|
375
|
+
},
|
|
376
|
+
) {
|
|
377
|
+
switch (operation.route) {
|
|
378
|
+
case "POST /v1/start":
|
|
379
|
+
return this.launcher.start(operation.name, {
|
|
380
|
+
identity: session.identity,
|
|
381
|
+
expires: context.expires,
|
|
382
|
+
signal: context.signal,
|
|
383
|
+
peers: (signal) => session.connectedPeers(signal),
|
|
384
|
+
});
|
|
385
|
+
case "GET /v1/sessions":
|
|
386
|
+
return this.launcher.list(session.identity);
|
|
387
|
+
case "POST /v1/stop":
|
|
388
|
+
return this.launcher.stop(session.identity, operation.sessionId);
|
|
389
|
+
case "GET /v1/list":
|
|
390
|
+
case "GET /v1/inbox":
|
|
391
|
+
case "POST /v1/register":
|
|
392
|
+
case "POST /v1/ask":
|
|
393
|
+
case "POST /v1/send":
|
|
394
|
+
case "POST /v1/reply":
|
|
395
|
+
case "POST /v1/ack":
|
|
396
|
+
return session.execute(operation, context);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
354
399
|
}
|
|
355
400
|
|
|
356
401
|
function main(): void {
|
|
@@ -371,7 +416,7 @@ function main(): void {
|
|
|
371
416
|
new Bridge(loadConfig(path), path).start();
|
|
372
417
|
} catch {
|
|
373
418
|
process.stderr.write(
|
|
374
|
-
"Bridge startup failed: check config schema, file ownership/mode 0600, absolute TLS/cwd paths, and
|
|
419
|
+
"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",
|
|
375
420
|
);
|
|
376
421
|
process.exitCode = 1;
|
|
377
422
|
}
|
|
@@ -110,9 +110,7 @@ export function publishStepOutput(
|
|
|
110
110
|
originalPath.length > 0 &&
|
|
111
111
|
step.outputMode !== "file-only" &&
|
|
112
112
|
step.outputPathFromAgentDefault === true;
|
|
113
|
-
const cleanup = shouldConsume
|
|
114
|
-
? cleanupSingleOutputFile(originalPath, output, undefined)
|
|
115
|
-
: undefined;
|
|
113
|
+
const cleanup = shouldConsume ? cleanupSingleOutputFile(originalPath, output) : undefined;
|
|
116
114
|
const savedPath = relocateOutput(ctx, originalPath, cleanup);
|
|
117
115
|
const reference = outputReference(savedPath, state.resolvedOutput.fullOutput, cleanup);
|
|
118
116
|
const displayOutput = finalizeSingleOutput({
|
|
@@ -402,7 +402,7 @@ export function formatAcceptanceFinalizationPrompt(input: {
|
|
|
402
402
|
return lines.join("\n");
|
|
403
403
|
}
|
|
404
404
|
|
|
405
|
-
|
|
405
|
+
function createFinalizationTurn(input: {
|
|
406
406
|
readonly turn: number;
|
|
407
407
|
readonly prompt: string;
|
|
408
408
|
readonly rawOutput: string;
|
|
@@ -447,7 +447,7 @@ export function createFinalizationProcessFailureTurn(input: {
|
|
|
447
447
|
};
|
|
448
448
|
}
|
|
449
449
|
|
|
450
|
-
|
|
450
|
+
function attachFinalizationToLedger(input: {
|
|
451
451
|
readonly initialLedger: AcceptanceLedger;
|
|
452
452
|
readonly authoritativeLedger: AcceptanceLedger;
|
|
453
453
|
readonly turns: readonly AcceptanceFinalizationTurn[];
|
|
@@ -18,10 +18,8 @@ export {
|
|
|
18
18
|
evaluateAcceptanceReport,
|
|
19
19
|
} from "./acceptance-evaluation.ts";
|
|
20
20
|
export {
|
|
21
|
-
attachFinalizationToLedger,
|
|
22
21
|
buildFinalizationProcessFailureLedger,
|
|
23
22
|
createFinalizationProcessFailureTurn,
|
|
24
|
-
createFinalizationTurn,
|
|
25
23
|
createFinalizationReportRuntime,
|
|
26
24
|
type FinalizationReportSubmission,
|
|
27
25
|
readFinalizationReport,
|
|
@@ -154,6 +154,9 @@ const SAME_MODEL_RECOVERY_PATTERNS = [
|
|
|
154
154
|
/socket/i,
|
|
155
155
|
/connection/i,
|
|
156
156
|
/fetch failed/i,
|
|
157
|
+
// Node fetch reports a response body cut off mid-stream as bare "terminated"; Pi retries it too.
|
|
158
|
+
// Match the whole message: process failures embed stderr and paths that can contain the word.
|
|
159
|
+
/^\s*terminated\s*$/i,
|
|
157
160
|
/network/i,
|
|
158
161
|
/http idle/i,
|
|
159
162
|
/timed? out/i,
|
|
@@ -44,8 +44,6 @@ export function flattenSteps(
|
|
|
44
44
|
return flat;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
export { mapConcurrent } from "../../shared/concurrency.ts";
|
|
48
|
-
|
|
49
47
|
export interface ParallelTaskResult {
|
|
50
48
|
agent: string;
|
|
51
49
|
taskIndex?: number;
|
|
@@ -54,8 +52,6 @@ export interface ParallelTaskResult {
|
|
|
54
52
|
error?: string;
|
|
55
53
|
model?: string;
|
|
56
54
|
attemptedModels?: string[];
|
|
57
|
-
outputTargetPath?: string;
|
|
58
|
-
outputTargetExists?: boolean;
|
|
59
55
|
}
|
|
60
56
|
|
|
61
57
|
function parallelResultStatus(
|
|
@@ -63,30 +59,10 @@ function parallelResultStatus(
|
|
|
63
59
|
hasOutput: boolean,
|
|
64
60
|
): string {
|
|
65
61
|
const error = result.error ?? "";
|
|
66
|
-
const suffix = error.length > 0 ? `: ${error}` : "";
|
|
67
|
-
if (result.exitCode === -1) {
|
|
68
|
-
return `SKIPPED${suffix}`;
|
|
69
|
-
}
|
|
70
|
-
if (result.exitCode !== 0 && result.exitCode !== null) {
|
|
71
|
-
return `FAILED (exit code ${result.exitCode})${suffix}`;
|
|
72
|
-
}
|
|
73
62
|
if (error.length > 0) {
|
|
74
63
|
return `WARNING: ${error}`;
|
|
75
64
|
}
|
|
76
|
-
return
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function emptyOutputStatus(result: ReadonlyInput<ParallelTaskResult>, hasOutput: boolean): string {
|
|
80
|
-
if (hasOutput) {
|
|
81
|
-
return "";
|
|
82
|
-
}
|
|
83
|
-
const target = result.outputTargetPath ?? "";
|
|
84
|
-
if (target.length === 0) {
|
|
85
|
-
return "EMPTY OUTPUT (no textual response returned)";
|
|
86
|
-
}
|
|
87
|
-
return result.outputTargetExists === false
|
|
88
|
-
? `EMPTY OUTPUT (expected output file missing: ${target})`
|
|
89
|
-
: "";
|
|
65
|
+
return hasOutput ? "" : "EMPTY OUTPUT (no textual response returned)";
|
|
90
66
|
}
|
|
91
67
|
|
|
92
68
|
export function aggregateParallelOutputs(
|
|
@@ -325,7 +325,6 @@ function unchangedOutputIdentity(fd: number, outputPath: string, stat: fs.Stats)
|
|
|
325
325
|
export function cleanupSingleOutputFile(
|
|
326
326
|
outputPath: string | undefined,
|
|
327
327
|
fullOutput: string,
|
|
328
|
-
beforeRun: SingleOutputSnapshot | undefined,
|
|
329
328
|
): SingleOutputCleanupResult | undefined {
|
|
330
329
|
if (!nonempty(outputPath)) {
|
|
331
330
|
return undefined;
|
|
@@ -345,9 +344,6 @@ export function cleanupSingleOutputFile(
|
|
|
345
344
|
if (!equal) {
|
|
346
345
|
return { path: absolutePath, action: "skipped", reason: "file changed after capture" };
|
|
347
346
|
}
|
|
348
|
-
if (matchesSnapshot(stat, beforeRun)) {
|
|
349
|
-
return { path: absolutePath, action: "skipped", reason: "file preexisted and was unchanged" };
|
|
350
|
-
}
|
|
351
347
|
fs.unlinkSync(outputPath);
|
|
352
348
|
return { path: absolutePath, action: "deleted" };
|
|
353
349
|
} catch (error) {
|
|
@@ -24,7 +24,7 @@ const STRUCTURED_OUTPUT_INSTRUCTIONS = [
|
|
|
24
24
|
"Do not rely on prose-only completion; if you do not call `structured_output`, the parent will fail this step.",
|
|
25
25
|
].join("\n");
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
const CHILD_SUBAGENT_BOUNDARY_INSTRUCTIONS = [
|
|
28
28
|
"You are a child subagent, not the parent orchestrator.",
|
|
29
29
|
"The parent session owns delegation, orchestration, review fanout, and follow-up worker launches.",
|
|
30
30
|
"Ignore prior parent-only orchestration instructions in inherited conversation history.",
|
|
@@ -32,7 +32,7 @@ export const CHILD_SUBAGENT_BOUNDARY_INSTRUCTIONS = [
|
|
|
32
32
|
"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.",
|
|
33
33
|
].join("\n");
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
const CHILD_FANOUT_BOUNDARY_INSTRUCTIONS = [
|
|
36
36
|
"You are a child subagent with delegation enabled for your assigned task.",
|
|
37
37
|
"You may delegate useful helper work within that task when it saves time or improves quality, using the available delegation tools.",
|
|
38
38
|
"You remain responsible for your assigned result. The original parent owns integration, review synthesis, and final delivery.",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { errorMessage } from "../../shared/unknown.ts";
|
|
2
|
+
import { mapConcurrent } from "../../shared/concurrency.ts";
|
|
2
3
|
import type { DynamicParallelStep } from "../../shared/types/workflow.ts";
|
|
3
4
|
import type {
|
|
4
5
|
AcceptanceLedger,
|
|
@@ -17,7 +18,6 @@ import {
|
|
|
17
18
|
import {
|
|
18
19
|
aggregateParallelOutputs,
|
|
19
20
|
FAIL_FAST_REASON,
|
|
20
|
-
mapConcurrent,
|
|
21
21
|
type ParallelTaskResult,
|
|
22
22
|
} from "./parallel-utils.ts";
|
|
23
23
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
3
|
+
import type * as NativeSession from "@earendil-works/pi-coding-agent";
|
|
4
|
+
|
|
5
|
+
export async function loadNativeSession(packageRoot: string): Promise<typeof NativeSession> {
|
|
6
|
+
// The trusted selected Pi package supplies this exact SDK declaration contract.
|
|
7
|
+
// A computed URL is required: Jiti rewrites literal SDK imports to its own host graph.
|
|
8
|
+
// oxlint-disable-next-line typescript/no-unsafe-assignment
|
|
9
|
+
const sessionModule: typeof NativeSession = await import(
|
|
10
|
+
pathToFileURL(path.join(packageRoot, "dist/index.js")).href
|
|
11
|
+
);
|
|
12
|
+
return sessionModule;
|
|
13
|
+
}
|
|
@@ -1,13 +1,6 @@
|
|
|
1
|
-
import * as path from "node:path";
|
|
2
|
-
import { pathToFileURL } from "node:url";
|
|
3
|
-
import type * as NativeSession from "@earendil-works/pi-coding-agent";
|
|
4
1
|
import { requirePiPackageRoot } from "../runs/shared/pi-spawn.ts";
|
|
2
|
+
import { loadNativeSession } from "./native-session-loader.ts";
|
|
5
3
|
|
|
6
4
|
// Detached Node runners do not get Pi's extension-loader package aliases.
|
|
7
|
-
const selectedSessionURL = pathToFileURL(path.join(requirePiPackageRoot(), "dist/index.js")).href;
|
|
8
|
-
// The trusted selected Pi package supplies this exact SDK declaration contract.
|
|
9
|
-
// A computed URL is required: Jiti rewrites literal SDK imports to its own host graph.
|
|
10
|
-
// oxlint-disable-next-line typescript/no-unsafe-assignment
|
|
11
|
-
const sessionModule: typeof NativeSession = await import(selectedSessionURL);
|
|
12
5
|
export const { buildSessionContext, parseSessionEntries, migrateSessionEntries, SessionManager } =
|
|
13
|
-
|
|
6
|
+
await loadNativeSession(requirePiPackageRoot());
|
package/src/shared/settings.ts
CHANGED
|
@@ -126,7 +126,7 @@ export function resolveStepBehavior(
|
|
|
126
126
|
function resolveChainPath(filePath: string, chainDir: string): string {
|
|
127
127
|
return path.isAbsolute(filePath) ? filePath : path.join(chainDir, filePath);
|
|
128
128
|
}
|
|
129
|
-
/**
|
|
129
|
+
/** Compose read paths and progress instructions for a chain child. */
|
|
130
130
|
export function buildChainInstructions(
|
|
131
131
|
behavior: ResolvedStepBehavior,
|
|
132
132
|
chainDir: string,
|
|
@@ -139,9 +139,6 @@ export function buildChainInstructions(
|
|
|
139
139
|
`[Read from: ${behavior.reads.map((file) => resolveChainPath(file, chainDir)).join(", ")}]`,
|
|
140
140
|
);
|
|
141
141
|
}
|
|
142
|
-
if (behavior.output !== false && behavior.output.length > 0) {
|
|
143
|
-
prefixParts.push(`[Write to: ${resolveChainPath(behavior.output, chainDir)}]`);
|
|
144
|
-
}
|
|
145
142
|
if (behavior.progress) {
|
|
146
143
|
const progressPath = path.join(chainDir, "progress.md");
|
|
147
144
|
suffixParts.push(
|