fix(coding-agent): support bundled experimental processes

This commit is contained in:
Armin Ronacher 2026-08-24 14:17:17 +02:00
parent c399e34658
commit a789067a0d
9 changed files with 54 additions and 17 deletions

View file

@ -23,6 +23,10 @@ export const isBunBinary =
/** Detect if Bun is the runtime (compiled binary or bun run) */
export const isBunRuntime = !!process.versions.bun;
/** Detect the esbuild-bundled Node.js distribution. */
declare const PI_BUNDLED_NODE: boolean;
export const isBundledNode = typeof PI_BUNDLED_NODE !== "undefined" && PI_BUNDLED_NODE;
// =============================================================================
// Install Method Detection
// =============================================================================

View file

@ -21,7 +21,7 @@ import { createJiti } from "jiti/static";
import * as _bundledTypebox from "typebox";
import * as _bundledTypeboxCompile from "typebox/compile";
import * as _bundledTypeboxValue from "typebox/value";
import { CONFIG_DIR_NAME, getAgentDir, isBunBinary } from "../../config.ts";
import { CONFIG_DIR_NAME, getAgentDir, isBunBinary, isBundledNode } from "../../config.ts";
// NOTE: This import works because loader.ts exports are NOT re-exported from index.ts,
// avoiding a circular dependency. Extensions can import from @earendil-works/pi-coding-agent.
import * as _bundledPiCodingAgent from "../../index.ts";
@ -78,8 +78,6 @@ const require = createRequire(import.meta.url);
const isNodeSeaBinary =
("sea" in process.features && process.features.sea === true) ||
process.getBuiltinModule("node:sea")?.isSea() === true;
declare const PI_BUNDLED_NODE: boolean;
const isBundledNode = typeof PI_BUNDLED_NODE !== "undefined" && PI_BUNDLED_NODE;
const isTypeScriptSourceRuntime = !isBunBinary && path.extname(fileURLToPath(import.meta.url)) === ".ts";
/**

View file

@ -0,0 +1,10 @@
#!/usr/bin/env node
import { runCoordinatorProcess } from "./coordinator.ts";
import { consumeInternalProcessRole } from "./process.ts";
const role = consumeInternalProcessRole();
if (role !== "coordinator") throw new Error("Coordinator entrypoint requires an internal coordinator invocation");
void runCoordinatorProcess(process.argv.slice(2)).catch((error: unknown) => {
console.error(error);
process.exit(1);
});

View file

@ -1,13 +1,12 @@
import { randomUUID } from "node:crypto";
import { chmod, lstat, unlink } from "node:fs/promises";
import { createConnection, createServer, type Server, type Socket } from "node:net";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";
import Type, { type Static } from "typebox";
import { Check } from "typebox/value";
import {
consumeInternalProcessRole,
encodeControlLine,
isDirectInternalProcessEntry,
MAX_CONTROL_LINE_BYTES,
spawnInternalProcess,
} from "./process.ts";
@ -603,7 +602,7 @@ async function cleanupSocket(path: string): Promise<void> {
});
}
if (process.argv[1] !== undefined && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
if (isDirectInternalProcessEntry(import.meta.url)) {
const role = consumeInternalProcessRole();
if (role !== "coordinator") throw new Error("Coordinator entrypoint requires an internal coordinator invocation");
void runCoordinatorProcess(process.argv.slice(2)).catch((error: unknown) => {

View file

@ -1,11 +1,22 @@
import { type ChildProcess, spawn } from "node:child_process";
import { fileURLToPath } from "node:url";
import { isBunBinary } from "../config.ts";
import { join, resolve } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { getPackageDir, isBunBinary, isBundledNode } from "../config.ts";
export const INTERNAL_PROCESS_ENV = "__PI_INTERNAL_SPAWN";
export type InternalProcessRole = "coordinator" | "server" | "session-worker";
/** Detect a directly executed source or unbundled internal-process module. */
export function isDirectInternalProcessEntry(moduleUrl: string): boolean {
return (
!isBunBinary &&
!isBundledNode &&
process.argv[1] !== undefined &&
resolve(process.argv[1]) === fileURLToPath(moduleUrl)
);
}
/** Read and validate an internal process role without consuming it. */
export function getInternalProcessRole(): InternalProcessRole | undefined {
const role = process.env[INTERNAL_PROCESS_ENV];
@ -71,6 +82,10 @@ export async function terminateInternalProcess(child: ChildProcess): Promise<voi
function defaultEntryUrl(role: InternalProcessRole, override: URL | undefined): URL {
if (override) return override;
if (isBundledNode) {
const entry = role === "coordinator" ? "coordinator.js" : "cli.js";
return pathToFileURL(join(getPackageDir(), "dist", "bundle", entry));
}
const javaScript = import.meta.url.endsWith(".js");
if (role === "coordinator") {
return new URL(javaScript ? "coordinator.js" : "coordinator.ts", import.meta.url);

View file

@ -1,8 +1,7 @@
import { randomUUID } from "node:crypto";
import { chmod, lstat, mkdir, readFile, writeFile } from "node:fs/promises";
import { homedir } from "node:os";
import { isAbsolute, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { isAbsolute, join } from "node:path";
import {
BACKGROUND_CONTEXT,
type JsonlSessionMetadata,
@ -19,7 +18,12 @@ import lockfile from "proper-lockfile";
import { getAgentDir } from "../config.ts";
import { resolvePath } from "../utils/paths.ts";
import { CoordinatorConnection, type CoordinatorStartupLease, ensureCoordinator } from "./coordinator.ts";
import { consumeInternalProcessRole, spawnInternalProcess, terminateInternalProcess } from "./process.ts";
import {
consumeInternalProcessRole,
isDirectInternalProcessEntry,
spawnInternalProcess,
terminateInternalProcess,
} from "./process.ts";
import { createExperimentalServerServices } from "./services/server.ts";
import { SessionWorkerManager } from "./session-worker-manager.ts";
@ -621,7 +625,7 @@ export async function runServerProcess(args: readonly string[]): Promise<void> {
}
}
if (process.argv[1] !== undefined && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
if (isDirectInternalProcessEntry(import.meta.url)) {
const role = consumeInternalProcessRole();
if (role !== "server") throw new Error("Server entrypoint requires an internal server invocation");
void runServerProcess(process.argv.slice(2)).catch((error: unknown) => {

View file

@ -1,7 +1,6 @@
import { randomUUID } from "node:crypto";
import { createConnection, type Socket } from "node:net";
import { isAbsolute, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { isAbsolute } from "node:path";
import {
AgentHarness,
type AgentHarness as AgentHarnessInstance,
@ -48,7 +47,12 @@ import {
toWireLaneSnapshot,
toWireRunResult,
} from "./harness-wire-adapter.ts";
import { consumeInternalProcessRole, encodeControlLine, MAX_CONTROL_LINE_BYTES } from "./process.ts";
import {
consumeInternalProcessRole,
encodeControlLine,
isDirectInternalProcessEntry,
MAX_CONTROL_LINE_BYTES,
} from "./process.ts";
import {
createSessionWorkerServices,
ServiceOperationResultSchema,
@ -952,7 +956,7 @@ export function runSessionWorkerProcess(args: readonly string[]): Promise<void>
return runSessionWorkerWithHarness(args, createCodingAgentHarness);
}
if (process.argv[1] !== undefined && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
if (isDirectInternalProcessEntry(import.meta.url)) {
const role = consumeInternalProcessRole();
if (role !== "session-worker") {
throw new Error("Session worker entrypoint requires an internal session-worker invocation");

View file

@ -65,7 +65,7 @@ if [[ "$use_stable" == true ]]; then
exec "$stable_pi" ${args[@]+"${args[@]}"}
fi
dev_pi="$repo_dir/packages/coding-agent/dist/cli.js"
dev_pi="$repo_dir/packages/coding-agent/dist/bundle/cli.js"
if [[ ! -x "$dev_pi" ]]; then
echo "error: development pi build not found; run \`npm run build\` in $repo_dir" >&2
exit 1

View file

@ -113,6 +113,7 @@ function outputBytes(metafiles) {
for (const entry of [
join(codingAgentDistDir, "cli.js"),
join(codingAgentDistDir, "experimental", "coordinator-entry.js"),
join(codingAgentDistDir, "index.js"),
join(codingAgentDistDir, "rpc-entry.js"),
join(codingAgentDistDir, "client", "index.js"),
@ -134,6 +135,7 @@ const mainResult = await build({
entryPoints: {
cli: join(codingAgentDistDir, "cli.js"),
client: join(codingAgentDistDir, "client", "index.js"),
coordinator: join(codingAgentDistDir, "experimental", "coordinator-entry.js"),
index: join(codingAgentDistDir, "index.js"),
"rpc-entry": join(codingAgentDistDir, "rpc-entry.js"),
},
@ -177,6 +179,7 @@ if (dirname(imageResizeOutput) !== dirname(imageResizeWorkerOutput)) {
validateExternalImports([mainResult.metafile, lazyResult.metafile]);
chmodSync(join(bundleDir, "cli.js"), 0o755);
chmodSync(join(bundleDir, "coordinator.js"), 0o755);
chmodSync(join(bundleDir, "rpc-entry.js"), 0o755);
const files = new Set([...Object.keys(mainResult.metafile.outputs), ...Object.keys(lazyResult.metafile.outputs)]).size;