diff --git a/.changeset/late-lobsters-inject.md b/.changeset/late-lobsters-inject.md new file mode 100644 index 0000000..f3b933a --- /dev/null +++ b/.changeset/late-lobsters-inject.md @@ -0,0 +1,5 @@ +--- +"@aliou/pi-processes": patch +--- + +Inject the session `PI_*` variables (`PI_SESSION_ID`, `PI_SESSION_FILE`, `PI_PROVIDER`, `PI_MODEL`, `PI_REASONING_LEVEL`) into processes started via the `process` tool's `start` action, mirroring the environment pi's bash tool exposes. diff --git a/extensions/processes/tools/start/index.test.ts b/extensions/processes/tools/start/index.test.ts index 405dc06..1722275 100644 --- a/extensions/processes/tools/start/index.test.ts +++ b/extensions/processes/tools/start/index.test.ts @@ -25,7 +25,13 @@ const processInfo: ProcessInfo = { errorMessage: null, }; -const ctx = { cwd: "/repo" } as ExtensionContext; +const ctx = { + cwd: "/repo", + sessionManager: { + getSessionId: () => "session-123", + getSessionFile: () => "/tmp/session-123.jsonl", + }, +} as ExtensionContext; function createFakeRegistry(): NotificationRegistry { return createNotificationRegistry(); @@ -71,7 +77,12 @@ describe("executeStart", () => { registry, ); - expect(start).toHaveBeenCalledWith("dev", "pnpm dev", "/repo"); + expect(start).toHaveBeenCalledWith( + "dev", + "pnpm dev", + "/repo", + expect.anything(), + ); expect(details.notify).toEqual({ onSuccess: "turn", onFailure: "turn", @@ -154,7 +165,12 @@ describe("executeStart", () => { registry, ); - expect(start).toHaveBeenCalledWith("dev", "pnpm dev", "/explicit"); + expect(start).toHaveBeenCalledWith( + "dev", + "pnpm dev", + "/explicit", + expect.anything(), + ); expect(details.process.cwd).toBe("/explicit"); }); @@ -174,7 +190,33 @@ describe("executeStart", () => { registry, ); - expect(start).toHaveBeenCalledWith("dev", "pnpm dev", "/repo"); + expect(start).toHaveBeenCalledWith( + "dev", + "pnpm dev", + "/repo", + expect.anything(), + ); + }); + + it("starts with PI_* session variables injected into the child env", () => { + const start = vi.fn((..._args: unknown[]) => processInfo); + const manager = { start } as unknown as ProcessManager; + const registry = createFakeRegistry(); + + executeStart( + { + action: "start", + name: "dev", + command: "pnpm dev", + }, + manager, + ctx, + registry, + ); + + const env = start.mock.calls[0]?.[3] as NodeJS.ProcessEnv; + expect(env.PI_SESSION_ID).toBe("session-123"); + expect(env.PI_SESSION_FILE).toBe("/tmp/session-123.jsonl"); }); }); diff --git a/extensions/processes/tools/start/index.ts b/extensions/processes/tools/start/index.ts index 0557d0c..43f4ee7 100644 --- a/extensions/processes/tools/start/index.ts +++ b/extensions/processes/tools/start/index.ts @@ -5,6 +5,7 @@ import type { NotificationRegistry } from "../../notifications/registry"; import { normalizeNotifyConfig } from "../notify"; import type { ProcessesParamsType, StartDetails } from "../schema"; import { formatMatcherForModel } from "../watch-format"; +import { buildSessionEnv } from "./session-env"; export function executeStart( params: ProcessesParamsType, @@ -23,7 +24,12 @@ export function executeStart( const notify = normalizeNotifyConfig(params.notify); const cwd = params.cwd ?? ctx.cwd; - const process = manager.start(params.name, params.command, cwd); + const process = manager.start( + params.name, + params.command, + cwd, + buildSessionEnv(ctx), + ); notifications.register(process.id, notify); return { diff --git a/extensions/processes/tools/start/session-env.test.ts b/extensions/processes/tools/start/session-env.test.ts new file mode 100644 index 0000000..8c799a9 --- /dev/null +++ b/extensions/processes/tools/start/session-env.test.ts @@ -0,0 +1,62 @@ +import type { ExtensionContext } from "@earendil-works/pi-coding-agent"; +import { describe, expect, it } from "vitest"; +import { buildSessionEnv } from "./session-env"; + +function makeCtx(overrides: Partial = {}): ExtensionContext { + return { + sessionManager: { + getSessionId: () => "session-123", + getSessionFile: () => "/tmp/session-123.jsonl", + }, + model: { provider: "neuralwatt", id: "glm-5.3-flash-flex" }, + thinkingLevel: "high", + ...overrides, + } as unknown as ExtensionContext; +} + +describe("buildSessionEnv", () => { + it("injects the same PI_* session variables as pi's bash tool", () => { + const env = buildSessionEnv(makeCtx()); + + expect(env.PI_SESSION_ID).toBe("session-123"); + expect(env.PI_SESSION_FILE).toBe("/tmp/session-123.jsonl"); + expect(env.PI_PROVIDER).toBe("neuralwatt"); + expect(env.PI_MODEL).toBe("glm-5.3-flash-flex"); + expect(env.PI_REASONING_LEVEL).toBe("high"); + }); + + it("copies the parent environment instead of mutating process.env", () => { + const before = { ...process.env }; + const env = buildSessionEnv(makeCtx()); + + env.PI_SESSION_ID = "changed"; + expect({ ...process.env }).toEqual(before); + expect(env.PI_CODING_AGENT).toBe(process.env.PI_CODING_AGENT); + }); + + it("leaves PI_SESSION_FILE to the ambient env when the session has no file", () => { + const env = buildSessionEnv( + makeCtx({ + sessionManager: { + getSessionId: () => "session-123", + getSessionFile: () => undefined, + }, + } as unknown as Partial), + ); + + expect(env.PI_SESSION_ID).toBe("session-123"); + // Passthrough: an ambient value is not ours to replace. + expect(env.PI_SESSION_FILE).toBe(process.env.PI_SESSION_FILE); + }); + + it("leaves provider, model, and reasoning level to the ambient env when no model is set", () => { + const env = buildSessionEnv( + makeCtx({ model: undefined, thinkingLevel: undefined }), + ); + + expect(env.PI_SESSION_ID).toBe("session-123"); + expect(env.PI_PROVIDER).toBe(process.env.PI_PROVIDER); + expect(env.PI_MODEL).toBe(process.env.PI_MODEL); + expect(env.PI_REASONING_LEVEL).toBe(process.env.PI_REASONING_LEVEL); + }); +}); diff --git a/extensions/processes/tools/start/session-env.ts b/extensions/processes/tools/start/session-env.ts new file mode 100644 index 0000000..3c8f5c3 --- /dev/null +++ b/extensions/processes/tools/start/session-env.ts @@ -0,0 +1,18 @@ +import type { ExtensionContext } from "@earendil-works/pi-coding-agent"; + +/** + * Env for a spawned process: the parent env plus the same PI_* session + * variables pi's bash tool exposes. + */ +export function buildSessionEnv(ctx: ExtensionContext): NodeJS.ProcessEnv { + const env = { ...process.env }; + env.PI_SESSION_ID = ctx.sessionManager.getSessionId(); + const sessionFile = ctx.sessionManager.getSessionFile(); + if (sessionFile) env.PI_SESSION_FILE = sessionFile; + if (ctx.model) { + env.PI_PROVIDER = ctx.model.provider; + env.PI_MODEL = ctx.model.id; + } + if (ctx.thinkingLevel) env.PI_REASONING_LEVEL = ctx.thinkingLevel; + return env; +} diff --git a/src/manager/index.ts b/src/manager/index.ts index 9ac359c..6f3af9b 100644 --- a/src/manager/index.ts +++ b/src/manager/index.ts @@ -54,8 +54,13 @@ export class ProcessManager { return () => this.events.off("event", listener); } - start(name: string, command: string, cwd: string): ProcessInfo { - const managed = this.runtime.start(name, command, cwd); + start( + name: string, + command: string, + cwd: string, + env?: NodeJS.ProcessEnv, + ): ProcessInfo { + const managed = this.runtime.start(name, command, cwd, env); return formatProcess(managed); } diff --git a/src/manager/process-runtime-controller.ts b/src/manager/process-runtime-controller.ts index 1ad0665..b8de253 100644 --- a/src/manager/process-runtime-controller.ts +++ b/src/manager/process-runtime-controller.ts @@ -50,8 +50,18 @@ export class ProcessRuntimeController { this.getConfiguredShellPath = deps.getConfiguredShellPath; } - start(name: string, command: string, cwd: string): ManagedProcessRecord { - const child = spawnCommand(command, cwd, this.getConfiguredShellPath()); + start( + name: string, + command: string, + cwd: string, + env?: NodeJS.ProcessEnv, + ): ManagedProcessRecord { + const child = spawnCommand( + command, + cwd, + this.getConfiguredShellPath(), + env, + ); return this.register(name, command, cwd, child, {}); }