Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 123 additions & 22 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,45 +40,146 @@ type RuntimeResponse = {
stopReason?: string;
errorMessage?: string;
};
type RuntimeCompleteOptions = {
apiKey?: string;
maxTokens: number;
signal?: AbortSignal;
headers?: Record<string, string>;
reasoning?: SummaryEffort;
disableReasoning: boolean;
};
type RuntimeContext = {
systemPrompt?: string | string[];
messages: unknown[];
};
type RuntimeBindings = {
complete: (
model: unknown,
context: { systemPrompt?: string | string[]; messages: unknown[] },
options: {
apiKey?: string;
maxTokens: number;
signal?: AbortSignal;
headers?: Record<string, string>;
reasoning?: SummaryEffort;
disableReasoning: boolean;
},
context: RuntimeContext,
options: RuntimeCompleteOptions,
) => Promise<RuntimeResponse>;
systemPrompt: string | string[];
convertToLlm: (messages: unknown[]) => unknown[];
serializeConversation: (messages: unknown[]) => string;
};

const OMP_BUNDLED_VIRTUAL_SCHEME = "omp-legacy-pi-bundled:";
const RUNTIME_COMPLETE_HELPER = path.join(MODULE_DIR, "runtime-complete.ts");

type BunSubprocess = {
stdin: {
write(data: string): number | Promise<number>;
end(): number | Promise<number>;
};
stdout: { text(): Promise<string> };
stderr: { text(): Promise<string> };
exited: Promise<number>;
};
type BunRuntime = {
spawn(options: {
cmd: string[];
cwd: string;
stdin: "pipe";
stdout: "pipe";
stderr: "pipe";
signal?: AbortSignal;
}): BunSubprocess;
};

// Bun is injected by the OMP host rather than declared by TypeScript's runtime libs.
const runtimeGlobal = globalThis as typeof globalThis & { Bun?: BunRuntime };

type RuntimeCompleteResult =
| { ok: true; response: RuntimeResponse }
| { ok: false; error: string };

/**
* OMP 17.0.1 rewrites extension imports to a virtual module scheme that its
* compiled Bun runtime cannot resolve. Run only the model request in a fresh
* Bun process, where this plugin's installed dependencies resolve normally.
*/
export async function completeInBunSubprocess(
model: unknown,
context: RuntimeContext,
options: RuntimeCompleteOptions,
helperPath = RUNTIME_COMPLETE_HELPER,
): Promise<RuntimeResponse> {
options.signal?.throwIfAborted();
const bun = runtimeGlobal.Bun;
if (!bun) {
throw new Error("OMP 17.0.1 compatibility mode requires the Bun runtime");
}

const { signal, ...serializableOptions } = options;
let subprocess: BunSubprocess;
try {
subprocess = bun.spawn({
cmd: ["bun", helperPath],
cwd: MODULE_DIR,
stdin: "pipe",
stdout: "pipe",
stderr: "pipe",
signal,
});
} catch (error) {
throw new Error("Failed to start the Bun compatibility helper", { cause: error });
}

await subprocess.stdin.write(JSON.stringify({ model, context, options: serializableOptions }));
await subprocess.stdin.end();
const [exitCode, output] = await Promise.all([
subprocess.exited,
subprocess.stdout.text(),
subprocess.stderr.text(),
]);
signal?.throwIfAborted();

let result: RuntimeCompleteResult;
try {
result = JSON.parse(output) as RuntimeCompleteResult;
} catch (error) {
throw new Error(`Bun compatibility helper returned invalid JSON (exit ${exitCode})`, { cause: error });
}
if (!result.ok) {
throw new Error(`Bun compatibility helper failed: ${result.error}`);
}
if (exitCode !== 0) {
throw new Error(`Bun compatibility helper exited with status ${exitCode}`);
}
return result.response;
Comment on lines +128 to +149
}

let runtimeBindings: Promise<RuntimeBindings> | undefined;

/**
* OMP and upstream pi publish the same extension surface from different package
* families. Load only the active host's implementation: importing OMP modules
* families. Static imports cannot support both hosts: importing OMP modules
* under Node evaluates Bun-specific environment code before the hook can load.
*/
async function loadRuntimeBindings(): Promise<RuntimeBindings> {
runtimeBindings ??= (async () => {
if (typeof (globalThis as { Bun?: unknown }).Bun !== "undefined") {
const [ai, codingAgent, compaction] = await Promise.all([
import("@oh-my-pi/pi-ai"),
import("@oh-my-pi/pi-coding-agent"),
import("@oh-my-pi/pi-agent-core/compaction/utils"),
]);
return {
systemPrompt: [COMPACTION_SYSTEM_PROMPT],
complete: ai.complete as RuntimeBindings["complete"],
convertToLlm: codingAgent.convertToLlm as RuntimeBindings["convertToLlm"],
serializeConversation: compaction.serializeConversation as RuntimeBindings["serializeConversation"],
};
if (runtimeGlobal.Bun) {
try {
const [ai, codingAgent, compaction] = await Promise.all([
import("@oh-my-pi/pi-ai"),
import("@oh-my-pi/pi-coding-agent"),
import("@oh-my-pi/pi-agent-core/compaction/utils"),
]);
return {
systemPrompt: [COMPACTION_SYSTEM_PROMPT],
complete: ai.complete as RuntimeBindings["complete"],
convertToLlm: codingAgent.convertToLlm as RuntimeBindings["convertToLlm"],
serializeConversation: compaction.serializeConversation as RuntimeBindings["serializeConversation"],
};
} catch (error) {
if (!String(error).includes(OMP_BUNDLED_VIRTUAL_SCHEME)) throw error;
return {
systemPrompt: [COMPACTION_SYSTEM_PROMPT],
complete: completeInBunSubprocess,
convertToLlm: messages => messages,
serializeConversation: serializePortableConversation,
};
}
}

const [ai, codingAgent] = await Promise.all([
Expand Down
22 changes: 22 additions & 0 deletions runtime-complete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { complete } from "@oh-my-pi/pi-ai";

type CompleteRequest = {
model: Parameters<typeof complete>[0];
context: Parameters<typeof complete>[1];
options: Parameters<typeof complete>[2];
};

type CompleteResult =
| { ok: true; response: unknown }
| { ok: false; error: string };

let result: CompleteResult;
try {
const request = (await Bun.stdin.json()) as CompleteRequest;
const response = await complete(request.model, request.context, request.options);
result = { ok: true, response };
} catch (error) {
result = { ok: false, error: error instanceof Error ? error.message : String(error) };
}

await Bun.write(Bun.stdout, JSON.stringify(result));
21 changes: 21 additions & 0 deletions test/fixtures/runtime-complete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
type Request = {
model?: { fail?: boolean };
context: unknown;
options: unknown;
};

const request = (await Bun.stdin.json()) as Request;
if (request.model?.fail) {
await Bun.write(Bun.stdout, JSON.stringify({ ok: false, error: "fixture completion failed" }));
} else {
await Bun.write(
Bun.stdout,
JSON.stringify({
ok: true,
response: {
content: [{ type: "text", text: JSON.stringify(request) }],
stopReason: "stop",
},
}),
);
}
49 changes: 49 additions & 0 deletions test/omp-runtime.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { describe, expect, test } from "bun:test";
import * as path from "node:path";
import { completeInBunSubprocess } from "../index";

const FIXTURE = path.join(import.meta.dir, "fixtures/runtime-complete.ts");
const CONTEXT = {
systemPrompt: ["compact"],
messages: [{ role: "user", content: "hello" }],
};
const OPTIONS = {
apiKey: "test-key",
headers: { "x-test": "true" },
maxTokens: 256,
reasoning: "low" as const,
disableReasoning: false,
};

describe("OMP 17.0.1 runtime compatibility", () => {
test("runs completion in a clean Bun process", async () => {
const response = await completeInBunSubprocess({ id: "test-model" }, CONTEXT, OPTIONS, FIXTURE);
const payload = JSON.parse(response.content?.[0]?.text ?? "") as {
model: unknown;
context: unknown;
options: Record<string, unknown>;
};

expect(payload).toEqual({
model: { id: "test-model" },
context: CONTEXT,
options: OPTIONS,
});
expect(payload.options).not.toHaveProperty("signal");
});

test("surfaces completion failures from the helper", async () => {
await expect(completeInBunSubprocess({ fail: true }, CONTEXT, OPTIONS, FIXTURE)).rejects.toThrow(
"Bun compatibility helper failed: fixture completion failed",
);
});

test("does not start a helper for an already-aborted request", async () => {
const controller = new AbortController();
controller.abort(new Error("compaction cancelled"));

await expect(
completeInBunSubprocess({ id: "test-model" }, CONTEXT, { ...OPTIONS, signal: controller.signal }, FIXTURE),
).rejects.toThrow("compaction cancelled");
});
});