Skip to content
This repository was archived by the owner on Aug 6, 2026. It is now read-only.

Commit ce31756

Browse files
committed
feat: hook up pi runtime to ChatThread
1 parent 3185f38 commit ce31756

82 files changed

Lines changed: 5732 additions & 679 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/code/src/main/di/bindings.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,10 @@ import type {
180180
} from "@posthog/workspace-server/services/local-logs/identifiers";
181181
import type { MCP_PROXY_AUTH } from "@posthog/workspace-server/services/mcp-proxy/identifiers";
182182
import type { McpProxyAuth } from "@posthog/workspace-server/services/mcp-proxy/ports";
183+
import type {
184+
PI_RUNTIME_FACTORY,
185+
PiRuntimeFactory,
186+
} from "@posthog/workspace-server/services/pi-session/identifiers";
183187
import type { PosthogPluginService } from "@posthog/workspace-server/services/posthog-plugin/posthog-plugin";
184188
import type { ProcessTrackingService } from "@posthog/workspace-server/services/process-tracking/process-tracking";
185189
import type {
@@ -344,6 +348,7 @@ export interface MainBindings {
344348
[AGENT_REPO_FILES]: unknown;
345349
[AGENT_AUTH]: unknown;
346350
[AGENT_LOGGER]: RootLogger;
351+
[PI_RUNTIME_FACTORY]: PiRuntimeFactory;
347352

348353
// Logger
349354
[ROOT_LOGGER]: RootLogger;

apps/code/src/main/di/container.ts

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,10 @@ import { OAUTH_CALLBACK_SERVER } from "@posthog/workspace-server/services/oauth-
184184
import { oauthCallbackModule } from "@posthog/workspace-server/services/oauth-callback/oauth-callback.module";
185185
import { onboardingImportModule } from "@posthog/workspace-server/services/onboarding-import/onboarding-import.module";
186186
import { osModule } from "@posthog/workspace-server/services/os/os.module";
187-
import { PI_SESSION_SERVICE } from "@posthog/workspace-server/services/pi-session/identifiers";
187+
import {
188+
PI_RUNTIME_FACTORY,
189+
PI_SESSION_SERVICE,
190+
} from "@posthog/workspace-server/services/pi-session/identifiers";
188191
import type { PiSessionService } from "@posthog/workspace-server/services/pi-session/pi-session";
189192
import { piSessionModule } from "@posthog/workspace-server/services/pi-session/pi-session.module";
190193
import { POSTHOG_PLUGIN_SERVICE } from "@posthog/workspace-server/services/posthog-plugin/identifiers";
@@ -253,6 +256,7 @@ import {
253256
TokenCipherPortAdapter,
254257
} from "../services/auth/port-adapters";
255258
import { DeepLinkService } from "../services/deep-link/service";
259+
import { DesktopPiRuntimeFactory } from "../services/desktop-pi-runtime-factory";
256260
import { DevActionsService } from "../services/dev-actions/service";
257261
import { DevFlagsService } from "../services/dev-flags/service";
258262
import { DevLogsService } from "../services/dev-logs/service";
@@ -318,6 +322,17 @@ import {
318322
WORKTREE_REPOSITORY as MAIN_WORKTREE_REPOSITORY,
319323
} from "./tokens";
320324

325+
async function cancelTaskSessions(
326+
agentService: AgentService,
327+
piSessionService: PiSessionService,
328+
taskId: string,
329+
): Promise<void> {
330+
await Promise.all([
331+
agentService.cancelSessionsByTaskId(taskId),
332+
piSessionService.stop(taskId),
333+
]);
334+
}
335+
321336
export const container = new TypedContainer<MainBindings>({
322337
defaultScope: "Singleton",
323338
});
@@ -359,6 +374,7 @@ container
359374
.bind(MAIN_DEFAULT_ADDITIONAL_DIRECTORY_REPOSITORY)
360375
.toService(DEFAULT_ADDITIONAL_DIRECTORY_REPOSITORY);
361376
container.load(agentModule);
377+
container.bind(PI_RUNTIME_FACTORY).to(DesktopPiRuntimeFactory);
362378
container.load(piSessionModule);
363379
container.bind(AGENT_SLEEP_COORDINATOR).toService(MAIN_SLEEP_SERVICE);
364380
container.bind(AGENT_MCP_APPS).toService(MCP_APPS_SERVICE);
@@ -395,12 +411,12 @@ container.bind(MCP_PROXY_AUTH).toDynamicValue((ctx) => {
395411
});
396412
container.load(archiveModule);
397413
container.bind(ARCHIVE_SESSION_CANCELLER).toDynamicValue((ctx) => ({
398-
cancelSessionsByTaskId: async (taskId: string) => {
399-
await Promise.all([
400-
ctx.get<AgentService>(AGENT_SERVICE).cancelSessionsByTaskId(taskId),
401-
ctx.get<PiSessionService>(PI_SESSION_SERVICE).stop(taskId),
402-
]);
403-
},
414+
cancelSessionsByTaskId: (taskId: string) =>
415+
cancelTaskSessions(
416+
ctx.get<AgentService>(AGENT_SERVICE),
417+
ctx.get<PiSessionService>(PI_SESSION_SERVICE),
418+
taskId,
419+
),
404420
}));
405421
container.bind(ARCHIVE_FILE_WATCHER).toDynamicValue((ctx) => ({
406422
stopWatching: async (worktreePath: string) => {
@@ -411,12 +427,12 @@ container.bind(ARCHIVE_FILE_WATCHER).toDynamicValue((ctx) => ({
411427
}));
412428
container.load(suspensionModule);
413429
container.bind(SUSPENSION_SESSION_CANCELLER).toDynamicValue((ctx) => ({
414-
cancelSessionsByTaskId: async (taskId: string) => {
415-
await Promise.all([
416-
ctx.get<AgentService>(AGENT_SERVICE).cancelSessionsByTaskId(taskId),
417-
ctx.get<PiSessionService>(PI_SESSION_SERVICE).stop(taskId),
418-
]);
419-
},
430+
cancelSessionsByTaskId: (taskId: string) =>
431+
cancelTaskSessions(
432+
ctx.get<AgentService>(AGENT_SERVICE),
433+
ctx.get<PiSessionService>(PI_SESSION_SERVICE),
434+
taskId,
435+
),
420436
}));
421437
container.bind(SUSPENSION_FILE_WATCHER).toDynamicValue((ctx) => ({
422438
stopWatching: async (worktreePath: string) => {
@@ -687,12 +703,12 @@ container.load(workspaceModule);
687703
container.bind(WORKSPACE_AGENT).toDynamicValue((ctx): WorkspaceAgent => {
688704
const agent = ctx.get<AgentService>(AGENT_SERVICE);
689705
return {
690-
cancelSessionsByTaskId: async (taskId) => {
691-
await Promise.all([
692-
agent.cancelSessionsByTaskId(taskId),
693-
ctx.get<PiSessionService>(PI_SESSION_SERVICE).stop(taskId),
694-
]);
695-
},
706+
cancelSessionsByTaskId: (taskId) =>
707+
cancelTaskSessions(
708+
agent,
709+
ctx.get<PiSessionService>(PI_SESSION_SERVICE),
710+
taskId,
711+
),
696712
onAgentFileActivity: (handler) =>
697713
agent.on(AgentServiceEvent.AgentFileActivity, handler),
698714
};
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import type { PiRuntime } from "@posthog/agent/pi/runtime";
2+
import { getLlmGatewayUrl } from "@posthog/agent/posthog-api";
3+
import { getCloudUrlFromRegion } from "@posthog/shared";
4+
import type { AgentAuth } from "@posthog/workspace-server/services/agent/ports";
5+
import type { AuthProxyService } from "@posthog/workspace-server/services/auth-proxy/auth-proxy";
6+
import { describe, expect, it, vi } from "vitest";
7+
import { DesktopPiRuntimeFactory } from "./desktop-pi-runtime-factory";
8+
9+
const createPiRuntime = vi.hoisted(() => vi.fn());
10+
11+
vi.mock("@posthog/agent/pi/runtime", () => ({ createPiRuntime }));
12+
13+
describe("DesktopPiRuntimeFactory", () => {
14+
it("routes Pi through the shared host auth proxy", async () => {
15+
const auth = {
16+
getOAuthCredentials: vi.fn(async () => ({
17+
access: "access-token",
18+
refresh: "refresh-token",
19+
expires: 1,
20+
region: "eu" as const,
21+
})),
22+
} as unknown as AgentAuth;
23+
const authProxy = {
24+
start: vi.fn(async () => "http://127.0.0.1:1234"),
25+
} as unknown as AuthProxyService;
26+
const runtime = {} as PiRuntime;
27+
createPiRuntime.mockReturnValue(runtime);
28+
const factory = new DesktopPiRuntimeFactory(auth, authProxy);
29+
30+
await expect(factory.create({ cwd: "/workspace" })).resolves.toBe(runtime);
31+
expect(authProxy.start).toHaveBeenCalledWith(
32+
getLlmGatewayUrl(getCloudUrlFromRegion("eu")),
33+
);
34+
expect(createPiRuntime).toHaveBeenCalledWith({
35+
cwd: "/workspace",
36+
providerOptions: {
37+
region: "eu",
38+
baseUrl: "http://127.0.0.1:1234",
39+
apiKey: "posthog-code-auth-proxy",
40+
},
41+
env: { POSTHOG_REGION: "eu" },
42+
});
43+
});
44+
});
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { createPiRuntime, type PiRuntime } from "@posthog/agent/pi/runtime";
2+
import { getLlmGatewayUrl } from "@posthog/agent/posthog-api";
3+
import { type CloudRegion, getCloudUrlFromRegion } from "@posthog/shared";
4+
import { AGENT_AUTH } from "@posthog/workspace-server/services/agent/identifiers";
5+
import type { AgentAuth } from "@posthog/workspace-server/services/agent/ports";
6+
import type { AuthProxyService } from "@posthog/workspace-server/services/auth-proxy/auth-proxy";
7+
import { AUTH_PROXY_SERVICE } from "@posthog/workspace-server/services/auth-proxy/identifiers";
8+
import type { PiRuntimeFactory } from "@posthog/workspace-server/services/pi-session/identifiers";
9+
import { inject, injectable } from "inversify";
10+
11+
const PROXY_API_KEY = "posthog-code-auth-proxy";
12+
13+
@injectable()
14+
export class DesktopPiRuntimeFactory implements PiRuntimeFactory {
15+
private proxyRegion?: CloudRegion;
16+
private proxyUrlPromise?: Promise<string>;
17+
18+
constructor(
19+
@inject(AGENT_AUTH) private readonly auth: AgentAuth,
20+
@inject(AUTH_PROXY_SERVICE)
21+
private readonly authProxy: AuthProxyService,
22+
) {}
23+
24+
async create(input: {
25+
cwd: string;
26+
model?: string;
27+
sessionFile?: string;
28+
}): Promise<PiRuntime> {
29+
const credentials = await this.auth.getOAuthCredentials();
30+
if (!credentials) {
31+
throw new Error("Pi requires PostHog authentication");
32+
}
33+
34+
const baseUrl = await this.getProxyUrl(credentials.region);
35+
36+
return createPiRuntime({
37+
...input,
38+
providerOptions: {
39+
region: credentials.region,
40+
baseUrl,
41+
apiKey: PROXY_API_KEY,
42+
},
43+
env: { POSTHOG_REGION: credentials.region },
44+
});
45+
}
46+
47+
private getProxyUrl(region: CloudRegion): Promise<string> {
48+
if (this.proxyRegion !== region || !this.proxyUrlPromise) {
49+
this.proxyRegion = region;
50+
const gatewayUrl = getLlmGatewayUrl(getCloudUrlFromRegion(region));
51+
this.proxyUrlPromise = this.authProxy.start(gatewayUrl);
52+
}
53+
54+
return this.proxyUrlPromise;
55+
}
56+
}

apps/code/src/main/services/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@
33
* This file is auto-generated by vite-plugin-auto-services.ts
44
*/
55

6+
import "./desktop-pi-runtime-factory.js";
67
import "./dev-toolbar.js";
78
import "./settingsStore.js";

apps/code/src/renderer/di/bindings.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ import {
6767
} from "@posthog/core/onboarding/identifiers";
6868
import { PI_RUNNER } from "@posthog/core/pi-runtime/identifiers";
6969
import type { PiRunner } from "@posthog/core/pi-runtime/piRunner";
70+
import {
71+
PI_SESSION_CLIENT,
72+
type PiSessionClient,
73+
} from "@posthog/core/pi-runtime/piSessionController";
7074
import {
7175
type BundleLocalSkill,
7276
CLOUD_ARTIFACT_BUNDLE_LOCAL_SKILL,
@@ -279,6 +283,7 @@ export interface RendererBindings {
279283
[ANALYTICS_TRACKER]: AnalyticsTracker;
280284
[TASK_CREATION_HOST]: ITaskCreationHost;
281285
[PI_RUNNER]: PiRunner;
286+
[PI_SESSION_CLIENT]: PiSessionClient;
282287
[TASK_CREATION_EFFECTS]: TaskCreationEffects;
283288
[RENDERER_TASK_SERVICE]: TaskService;
284289
[TASK_SERVICE]: TaskService;

apps/code/src/renderer/di/container.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@ import { LLM_GATEWAY_SERVICE } from "@posthog/core/llm-gateway/identifiers";
3131
import type { LlmGatewayService } from "@posthog/core/llm-gateway/llm-gateway";
3232
import type { LlmMessage } from "@posthog/core/llm-gateway/schemas";
3333
import { PI_RUNNER } from "@posthog/core/pi-runtime/identifiers";
34+
import { piRuntimeModule } from "@posthog/core/pi-runtime/pi-runtime.module";
3435
import type { PiRunner } from "@posthog/core/pi-runtime/piRunner";
36+
import { PI_SESSION_CLIENT } from "@posthog/core/pi-runtime/piSessionController";
3537
import {
3638
CLOUD_ARTIFACT_BUNDLE_LOCAL_SKILL,
3739
CLOUD_ARTIFACT_READ_FILE_AS_BASE64,
@@ -85,6 +87,7 @@ import {
8587
import { WorkspaceSetupService } from "@posthog/core/workspace/WorkspaceSetupService";
8688
import { setRootContainer } from "@posthog/di/container";
8789
import { HOST_TRPC_CLIENT } from "@posthog/host-router/client";
90+
import { TrpcPiSessionClient } from "@posthog/host-router/pi-session-client";
8891
import {
8992
BROWSER_TABS_CLIENT,
9093
type BrowserTabsClient,
@@ -292,6 +295,8 @@ container
292295
// Bind services
293296
container.bind<ITaskCreationHost>(TASK_CREATION_HOST).to(TrpcTaskCreationHost);
294297
container.bind<PiRunner>(PI_RUNNER).to(TrpcPiRunner);
298+
container.bind(PI_SESSION_CLIENT).to(TrpcPiSessionClient);
299+
container.load(piRuntimeModule);
295300
container.bind(TASK_CREATION_EFFECTS).toConstantValue(taskCreationEffects);
296301
container.bind<TaskService>(RENDERER_TASK_SERVICE).to(TaskService);
297302
container.bind<TaskService>(TASK_SERVICE).toService(RENDERER_TASK_SERVICE);

apps/code/src/renderer/platform-adapters/trpc-pi-runner.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,27 @@ import type {
33
PiRunInput,
44
PiRunner,
55
} from "@posthog/core/pi-runtime/piRunner";
6-
import { resolveService } from "@posthog/di/container";
76
import {
87
HOST_TRPC_CLIENT,
98
type HostTrpcClient,
109
} from "@posthog/host-router/client";
10+
import { inject, injectable } from "inversify";
1111

12-
function hostClient(): HostTrpcClient {
13-
return resolveService<HostTrpcClient>(HOST_TRPC_CLIENT);
14-
}
15-
12+
@injectable()
1613
export class TrpcPiRunner implements PiRunner {
14+
constructor(
15+
@inject(HOST_TRPC_CLIENT) private readonly hostClient: HostTrpcClient,
16+
) {}
17+
1718
async create(input: PiRunInput): Promise<void> {
18-
await hostClient().piSession.start.mutate(input);
19+
await this.hostClient.piSession.start.mutate(input);
1920
}
2021

2122
resume(input: PiResumeInput): Promise<void> {
22-
return hostClient().piSession.resume.mutate(input);
23+
return this.hostClient.piSession.resume.mutate(input);
2324
}
2425

2526
stop(taskId: string): Promise<void> {
26-
return hostClient().piSession.stop.mutate({ taskId });
27+
return this.hostClient.piSession.stop.mutate({ taskId });
2728
}
2829
}

apps/web/src/web-container.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
import "reflect-metadata";
22
import { TypedContainer } from "@inversifyjs/strongly-typed";
3+
import { piRuntimeModule } from "@posthog/core/pi-runtime/pi-runtime.module";
4+
import {
5+
PI_SESSION_CLIENT,
6+
type PiSessionClient,
7+
} from "@posthog/core/pi-runtime/piSessionController";
38
import { setRootContainer } from "@posthog/di/container";
49
import { ROOT_LOGGER, type RootLogger } from "@posthog/di/logger";
510
import {
611
HOST_TRPC_CLIENT,
712
type HostTrpcClient,
813
} from "@posthog/host-router/client";
14+
import { TrpcPiSessionClient } from "@posthog/host-router/pi-session-client";
915
import { sandboxProxyHtml } from "@posthog/shared/mcp-sandbox-proxy";
1016
import {
1117
AUTH_SIDE_EFFECTS,
@@ -36,6 +42,7 @@ import { hostTrpcClient } from "./web-trpc";
3642

3743
interface WebBindings {
3844
[HOST_TRPC_CLIENT]: HostTrpcClient;
45+
[PI_SESSION_CLIENT]: PiSessionClient;
3946
[ROOT_LOGGER]: RootLogger;
4047
[FEATURE_FLAGS]: FeatureFlags;
4148
[ANALYTICS_TRACKER]: AnalyticsTracker;
@@ -53,6 +60,8 @@ export const container = new TypedContainer<WebBindings>({
5360

5461
// Keystone: the same typed host client the renderer binds, over HTTP not IPC.
5562
container.bind(HOST_TRPC_CLIENT).toConstantValue(hostTrpcClient);
63+
container.bind(PI_SESSION_CLIENT).to(TrpcPiSessionClient);
64+
container.load(piRuntimeModule);
5665

5766
// Logger: web uses console; electron uses electron-log. Same RootLogger shape.
5867
const scoped = (name?: string): RootLogger => ({

packages/agent/package.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,18 @@
3232
"types": "./dist/pi/rpc-client.d.ts",
3333
"import": "./dist/pi/rpc-client.js"
3434
},
35+
"./pi/conversation": {
36+
"types": "./dist/pi/conversation/translatePiConversation.d.ts",
37+
"import": "./dist/pi/conversation/translatePiConversation.js"
38+
},
39+
"./pi/runtime": {
40+
"types": "./dist/pi/runtime.d.ts",
41+
"import": "./dist/pi/runtime.js"
42+
},
43+
"./pi/types": {
44+
"types": "./dist/pi/types.d.ts",
45+
"import": "./dist/pi/types.js"
46+
},
3547
"./pr-url-detector": {
3648
"types": "./dist/pr-url-detector.d.ts",
3749
"import": "./dist/pr-url-detector.js"
@@ -140,6 +152,8 @@
140152
"@agentclientprotocol/sdk": "1.1.0",
141153
"@anthropic-ai/claude-agent-sdk": "0.3.197",
142154
"@anthropic-ai/sdk": "0.109.0",
155+
"@earendil-works/pi-agent-core": "catalog:",
156+
"@earendil-works/pi-ai": "catalog:",
143157
"@earendil-works/pi-coding-agent": "catalog:",
144158
"@hono/node-server": "^1.19.9",
145159
"@openai/codex": "0.140.0",

0 commit comments

Comments
 (0)