Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/late-lobsters-inject.md
Original file line number Diff line number Diff line change
@@ -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.
50 changes: 46 additions & 4 deletions extensions/processes/tools/start/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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");
});

Expand All @@ -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");
});
});

Expand Down
8 changes: 7 additions & 1 deletion extensions/processes/tools/start/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 {
Expand Down
62 changes: 62 additions & 0 deletions extensions/processes/tools/start/session-env.test.ts
Original file line number Diff line number Diff line change
@@ -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> = {}): 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<ExtensionContext>),
);

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);
});
});
18 changes: 18 additions & 0 deletions extensions/processes/tools/start/session-env.ts
Original file line number Diff line number Diff line change
@@ -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;
}
9 changes: 7 additions & 2 deletions src/manager/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
14 changes: 12 additions & 2 deletions src/manager/process-runtime-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, {});
}

Expand Down
Loading