@posthog/agent 2.3.168 → 2.3.171

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,386 @@
1
+ /**
2
+ * In-process ACP proxy agent for Codex.
3
+ *
4
+ * Implements the ACP Agent interface and delegates to the codex-acp binary
5
+ * via a ClientSideConnection. This gives us interception points for:
6
+ * - PostHog-specific notifications (sdk_session, usage_update, turn_complete)
7
+ * - Session resume/fork (not natively supported by codex-acp)
8
+ * - Usage accumulation
9
+ * - System prompt injection
10
+ */
11
+
12
+ import {
13
+ type AgentSideConnection,
14
+ type AuthenticateRequest,
15
+ type CancelNotification,
16
+ ClientSideConnection,
17
+ type ForkSessionRequest,
18
+ type ForkSessionResponse,
19
+ type InitializeRequest,
20
+ type InitializeResponse,
21
+ type ListSessionsRequest,
22
+ type ListSessionsResponse,
23
+ type LoadSessionRequest,
24
+ type LoadSessionResponse,
25
+ type NewSessionRequest,
26
+ type NewSessionResponse,
27
+ ndJsonStream,
28
+ type PromptRequest,
29
+ type PromptResponse,
30
+ type ResumeSessionRequest,
31
+ type ResumeSessionResponse,
32
+ type SetSessionConfigOptionRequest,
33
+ type SetSessionConfigOptionResponse,
34
+ type SetSessionModeRequest,
35
+ type SetSessionModeResponse,
36
+ } from "@agentclientprotocol/sdk";
37
+ import packageJson from "../../../package.json" with { type: "json" };
38
+ import { POSTHOG_NOTIFICATIONS } from "../../acp-extensions";
39
+ import type { ProcessSpawnedCallback } from "../../types";
40
+ import { Logger } from "../../utils/logger";
41
+ import {
42
+ nodeReadableToWebReadable,
43
+ nodeWritableToWebWritable,
44
+ } from "../../utils/streams";
45
+ import { BaseAcpAgent, type BaseSession } from "../base-acp-agent";
46
+ import { createCodexClient } from "./codex-client";
47
+ import {
48
+ type CodexSessionState,
49
+ createSessionState,
50
+ resetUsage,
51
+ } from "./session-state";
52
+ import { CodexSettingsManager } from "./settings";
53
+ import {
54
+ type CodexProcess,
55
+ type CodexProcessOptions,
56
+ spawnCodexProcess,
57
+ } from "./spawn";
58
+
59
+ interface NewSessionMeta {
60
+ taskRunId?: string;
61
+ taskId?: string;
62
+ systemPrompt?: string;
63
+ permissionMode?: string;
64
+ model?: string;
65
+ persistence?: { taskId?: string; runId?: string; logUrl?: string };
66
+ claudeCode?: {
67
+ options?: Record<string, unknown>;
68
+ };
69
+ additionalRoots?: string[];
70
+ disableBuiltInTools?: boolean;
71
+ allowedDomains?: string[];
72
+ }
73
+
74
+ export interface CodexAcpAgentOptions {
75
+ codexProcessOptions: CodexProcessOptions;
76
+ processCallbacks?: ProcessSpawnedCallback;
77
+ }
78
+
79
+ type CodexSession = BaseSession & {
80
+ settingsManager: CodexSettingsManager;
81
+ };
82
+
83
+ export class CodexAcpAgent extends BaseAcpAgent {
84
+ readonly adapterName = "codex";
85
+ declare session: CodexSession;
86
+ private codexProcess: CodexProcess;
87
+ private codexConnection!: ClientSideConnection;
88
+ private sessionState!: CodexSessionState;
89
+
90
+ constructor(client: AgentSideConnection, options: CodexAcpAgentOptions) {
91
+ super(client);
92
+ this.logger = new Logger({ debug: true, prefix: "[CodexAcpAgent]" });
93
+
94
+ // Spawn the codex-acp subprocess
95
+ this.codexProcess = spawnCodexProcess({
96
+ ...options.codexProcessOptions,
97
+ logger: this.logger,
98
+ processCallbacks: options.processCallbacks,
99
+ });
100
+
101
+ // Create ACP connection to codex-acp over stdin/stdout
102
+ const codexReadable = nodeReadableToWebReadable(this.codexProcess.stdout);
103
+ const codexWritable = nodeWritableToWebWritable(this.codexProcess.stdin);
104
+ const codexStream = ndJsonStream(codexWritable, codexReadable);
105
+
106
+ // Set up session with CodexSettingsManager
107
+ const cwd = options.codexProcessOptions.cwd ?? process.cwd();
108
+ const settingsManager = new CodexSettingsManager(cwd);
109
+ const abortController = new AbortController();
110
+ this.session = {
111
+ abortController,
112
+ settingsManager,
113
+ notificationHistory: [],
114
+ cancelled: false,
115
+ };
116
+
117
+ // Create the ClientSideConnection to codex-acp.
118
+ // The Client handler delegates all requests from codex-acp to the upstream
119
+ // PostHog Code client via our AgentSideConnection.
120
+ this.codexConnection = new ClientSideConnection(
121
+ (_agent) =>
122
+ createCodexClient(
123
+ this.client,
124
+ this.logger,
125
+ this.sessionState ?? {
126
+ sessionId: "",
127
+ cwd: "",
128
+ modeId: "default",
129
+ configOptions: [],
130
+ accumulatedUsage: {
131
+ inputTokens: 0,
132
+ outputTokens: 0,
133
+ cachedReadTokens: 0,
134
+ cachedWriteTokens: 0,
135
+ },
136
+ cancelled: false,
137
+ },
138
+ ),
139
+ codexStream,
140
+ );
141
+ }
142
+
143
+ async initialize(request: InitializeRequest): Promise<InitializeResponse> {
144
+ // Initialize settings
145
+ await this.session.settingsManager.initialize();
146
+
147
+ // Forward to codex-acp
148
+ const response = await this.codexConnection.initialize(request);
149
+
150
+ // Merge our enhanced capabilities
151
+ return {
152
+ ...response,
153
+ agentCapabilities: {
154
+ ...response.agentCapabilities,
155
+ sessionCapabilities: {
156
+ ...response.agentCapabilities?.sessionCapabilities,
157
+ resume: {},
158
+ fork: {},
159
+ },
160
+ _meta: {
161
+ posthog: {
162
+ resumeSession: true,
163
+ },
164
+ },
165
+ },
166
+ agentInfo: {
167
+ name: packageJson.name,
168
+ title: "Codex Agent",
169
+ version: packageJson.version,
170
+ },
171
+ };
172
+ }
173
+
174
+ async newSession(params: NewSessionRequest): Promise<NewSessionResponse> {
175
+ const meta = params._meta as NewSessionMeta | undefined;
176
+
177
+ const response = await this.codexConnection.newSession(params);
178
+
179
+ // Initialize session state
180
+ this.sessionState = createSessionState(response.sessionId, params.cwd, {
181
+ taskRunId: meta?.taskRunId,
182
+ taskId: meta?.taskId ?? meta?.persistence?.taskId,
183
+ modeId: response.modes?.currentModeId ?? "default",
184
+ modelId: response.models?.currentModelId,
185
+ });
186
+ this.sessionId = response.sessionId;
187
+ this.sessionState.configOptions = response.configOptions ?? [];
188
+
189
+ // Emit _posthog/sdk_session so the app can track the session
190
+ if (meta?.taskRunId) {
191
+ await this.client.extNotification(POSTHOG_NOTIFICATIONS.SDK_SESSION, {
192
+ taskRunId: meta.taskRunId,
193
+ sessionId: response.sessionId,
194
+ adapter: "codex",
195
+ });
196
+ }
197
+
198
+ this.logger.info("Codex session created", {
199
+ sessionId: response.sessionId,
200
+ taskRunId: meta?.taskRunId,
201
+ });
202
+
203
+ return response;
204
+ }
205
+
206
+ async loadSession(params: LoadSessionRequest): Promise<LoadSessionResponse> {
207
+ const response = await this.codexConnection.loadSession(params);
208
+
209
+ // Update session state
210
+ this.sessionState = createSessionState(params.sessionId, params.cwd);
211
+ this.sessionId = params.sessionId;
212
+ this.sessionState.configOptions = response.configOptions ?? [];
213
+
214
+ return response;
215
+ }
216
+
217
+ async unstable_resumeSession(
218
+ params: ResumeSessionRequest,
219
+ ): Promise<ResumeSessionResponse> {
220
+ // codex-acp doesn't support resume natively, use loadSession instead
221
+ const loadResponse = await this.codexConnection.loadSession({
222
+ sessionId: params.sessionId,
223
+ cwd: params.cwd,
224
+ mcpServers: params.mcpServers ?? [],
225
+ });
226
+
227
+ this.sessionState = createSessionState(params.sessionId, params.cwd);
228
+ this.sessionId = params.sessionId;
229
+ this.sessionState.configOptions = loadResponse.configOptions ?? [];
230
+
231
+ const meta = params._meta as NewSessionMeta | undefined;
232
+ if (meta?.taskRunId) {
233
+ await this.client.extNotification(POSTHOG_NOTIFICATIONS.SDK_SESSION, {
234
+ taskRunId: meta.taskRunId,
235
+ sessionId: params.sessionId,
236
+ adapter: "codex",
237
+ });
238
+ }
239
+
240
+ return {
241
+ modes: loadResponse.modes,
242
+ models: loadResponse.models,
243
+ configOptions: loadResponse.configOptions,
244
+ };
245
+ }
246
+
247
+ async unstable_forkSession(
248
+ params: ForkSessionRequest,
249
+ ): Promise<ForkSessionResponse> {
250
+ // Create a new session via codex-acp (fork isn't natively supported)
251
+ const newResponse = await this.codexConnection.newSession({
252
+ cwd: params.cwd,
253
+ mcpServers: params.mcpServers ?? [],
254
+ _meta: params._meta,
255
+ });
256
+
257
+ this.sessionState = createSessionState(newResponse.sessionId, params.cwd);
258
+ this.sessionId = newResponse.sessionId;
259
+ this.sessionState.configOptions = newResponse.configOptions ?? [];
260
+
261
+ return newResponse;
262
+ }
263
+
264
+ async listSessions(
265
+ params: ListSessionsRequest,
266
+ ): Promise<ListSessionsResponse> {
267
+ return this.codexConnection.listSessions(params);
268
+ }
269
+
270
+ async unstable_listSessions(
271
+ params: ListSessionsRequest,
272
+ ): Promise<ListSessionsResponse> {
273
+ return this.codexConnection.listSessions(params);
274
+ }
275
+
276
+ async prompt(params: PromptRequest): Promise<PromptResponse> {
277
+ if (this.sessionState) {
278
+ this.sessionState.cancelled = false;
279
+ this.sessionState.interruptReason = undefined;
280
+ resetUsage(this.sessionState);
281
+ }
282
+
283
+ const response = await this.codexConnection.prompt(params);
284
+
285
+ if (this.sessionState && response.usage) {
286
+ // Accumulate token usage from the prompt response
287
+ this.sessionState.accumulatedUsage.inputTokens +=
288
+ response.usage.inputTokens ?? 0;
289
+ this.sessionState.accumulatedUsage.outputTokens +=
290
+ response.usage.outputTokens ?? 0;
291
+ this.sessionState.accumulatedUsage.cachedReadTokens +=
292
+ response.usage.cachedReadTokens ?? 0;
293
+ this.sessionState.accumulatedUsage.cachedWriteTokens +=
294
+ response.usage.cachedWriteTokens ?? 0;
295
+ }
296
+
297
+ if (this.sessionState?.taskRunId) {
298
+ const { accumulatedUsage } = this.sessionState;
299
+
300
+ await this.client.extNotification(POSTHOG_NOTIFICATIONS.TURN_COMPLETE, {
301
+ sessionId: params.sessionId,
302
+ stopReason: response.stopReason ?? "end_turn",
303
+ usage: {
304
+ inputTokens: accumulatedUsage.inputTokens,
305
+ outputTokens: accumulatedUsage.outputTokens,
306
+ cachedReadTokens: accumulatedUsage.cachedReadTokens,
307
+ cachedWriteTokens: accumulatedUsage.cachedWriteTokens,
308
+ totalTokens:
309
+ accumulatedUsage.inputTokens +
310
+ accumulatedUsage.outputTokens +
311
+ accumulatedUsage.cachedReadTokens +
312
+ accumulatedUsage.cachedWriteTokens,
313
+ },
314
+ });
315
+
316
+ if (response.usage) {
317
+ await this.client.extNotification("_posthog/usage_update", {
318
+ sessionId: params.sessionId,
319
+ used: {
320
+ inputTokens: response.usage.inputTokens ?? 0,
321
+ outputTokens: response.usage.outputTokens ?? 0,
322
+ cachedReadTokens: response.usage.cachedReadTokens ?? 0,
323
+ cachedWriteTokens: response.usage.cachedWriteTokens ?? 0,
324
+ },
325
+ cost: null,
326
+ });
327
+ }
328
+ }
329
+
330
+ return response;
331
+ }
332
+
333
+ protected async interrupt(): Promise<void> {
334
+ if (this.sessionState) {
335
+ this.sessionState.cancelled = true;
336
+ }
337
+ await this.codexConnection.cancel({
338
+ sessionId: this.sessionId,
339
+ });
340
+ }
341
+
342
+ async cancel(params: CancelNotification): Promise<void> {
343
+ if (this.sessionState) {
344
+ this.sessionState.cancelled = true;
345
+ const meta = params._meta as { interruptReason?: string } | undefined;
346
+ if (meta?.interruptReason) {
347
+ this.sessionState.interruptReason = meta.interruptReason;
348
+ }
349
+ }
350
+ await this.codexConnection.cancel(params);
351
+ }
352
+
353
+ async setSessionMode(
354
+ params: SetSessionModeRequest,
355
+ ): Promise<SetSessionModeResponse> {
356
+ const response = await this.codexConnection.setSessionMode(params);
357
+ if (this.sessionState) {
358
+ this.sessionState.modeId = params.modeId;
359
+ }
360
+ return response ?? {};
361
+ }
362
+
363
+ async setSessionConfigOption(
364
+ params: SetSessionConfigOptionRequest,
365
+ ): Promise<SetSessionConfigOptionResponse> {
366
+ const response = await this.codexConnection.setSessionConfigOption(params);
367
+ if (this.sessionState && response.configOptions) {
368
+ this.sessionState.configOptions = response.configOptions;
369
+ }
370
+ return response;
371
+ }
372
+
373
+ async authenticate(_params: AuthenticateRequest): Promise<void> {
374
+ // Auth handled externally
375
+ }
376
+
377
+ async closeSession(): Promise<void> {
378
+ this.logger.info("Closing Codex session", { sessionId: this.sessionId });
379
+ this.session.settingsManager.dispose();
380
+ try {
381
+ this.codexProcess.kill();
382
+ } catch (err) {
383
+ this.logger.warn("Failed to kill codex-acp process", { error: err });
384
+ }
385
+ }
386
+ }
@@ -0,0 +1,168 @@
1
+ /**
2
+ * ACP Client implementation for communicating with codex-acp subprocess.
3
+ *
4
+ * This acts as the "client" from codex-acp's perspective: it receives
5
+ * permission requests, session updates, file I/O, and terminal operations
6
+ * from codex-acp and delegates them to the upstream PostHog Code client.
7
+ */
8
+
9
+ import type {
10
+ AgentSideConnection,
11
+ Client,
12
+ CreateTerminalRequest,
13
+ CreateTerminalResponse,
14
+ KillTerminalRequest,
15
+ KillTerminalResponse,
16
+ ReadTextFileRequest,
17
+ ReadTextFileResponse,
18
+ ReleaseTerminalRequest,
19
+ ReleaseTerminalResponse,
20
+ RequestPermissionRequest,
21
+ RequestPermissionResponse,
22
+ SessionNotification,
23
+ TerminalHandle,
24
+ TerminalOutputRequest,
25
+ TerminalOutputResponse,
26
+ WaitForTerminalExitRequest,
27
+ WaitForTerminalExitResponse,
28
+ WriteTextFileRequest,
29
+ WriteTextFileResponse,
30
+ } from "@agentclientprotocol/sdk";
31
+ import type { Logger } from "../../utils/logger";
32
+ import type { CodexSessionState } from "./session-state";
33
+
34
+ export interface CodexClientCallbacks {
35
+ /** Called when a usage_update session notification is received */
36
+ onUsageUpdate?: (update: Record<string, unknown>) => void;
37
+ }
38
+
39
+ /**
40
+ * Creates an ACP Client that delegates all requests from codex-acp
41
+ * to the upstream PostHog Code client (via AgentSideConnection).
42
+ */
43
+ export function createCodexClient(
44
+ upstreamClient: AgentSideConnection,
45
+ logger: Logger,
46
+ sessionState: CodexSessionState,
47
+ callbacks?: CodexClientCallbacks,
48
+ ): Client {
49
+ // Track terminal handles for delegation
50
+ const terminalHandles = new Map<string, TerminalHandle>();
51
+
52
+ return {
53
+ async requestPermission(
54
+ params: RequestPermissionRequest,
55
+ ): Promise<RequestPermissionResponse> {
56
+ logger.debug("Relaying permission request to upstream", {
57
+ sessionId: params.sessionId,
58
+ });
59
+ return upstreamClient.requestPermission(params);
60
+ },
61
+
62
+ async sessionUpdate(params: SessionNotification): Promise<void> {
63
+ const update = params.update as Record<string, unknown> | undefined;
64
+ if (update?.sessionUpdate === "usage_update") {
65
+ const used = update.used as number | undefined;
66
+ const size = update.size as number | undefined;
67
+ if (used !== undefined) sessionState.contextUsed = used;
68
+ if (size !== undefined) sessionState.contextSize = size;
69
+
70
+ // Accumulate per-message token usage when available
71
+ const inputTokens = update.inputTokens as number | undefined;
72
+ const outputTokens = update.outputTokens as number | undefined;
73
+ if (inputTokens !== undefined) {
74
+ sessionState.accumulatedUsage.inputTokens += inputTokens;
75
+ }
76
+ if (outputTokens !== undefined) {
77
+ sessionState.accumulatedUsage.outputTokens += outputTokens;
78
+ }
79
+ const cachedRead = update.cachedReadTokens as number | undefined;
80
+ const cachedWrite = update.cachedWriteTokens as number | undefined;
81
+ if (cachedRead !== undefined) {
82
+ sessionState.accumulatedUsage.cachedReadTokens += cachedRead;
83
+ }
84
+ if (cachedWrite !== undefined) {
85
+ sessionState.accumulatedUsage.cachedWriteTokens += cachedWrite;
86
+ }
87
+
88
+ callbacks?.onUsageUpdate?.(update);
89
+ }
90
+
91
+ await upstreamClient.sessionUpdate(params);
92
+ },
93
+
94
+ async readTextFile(
95
+ params: ReadTextFileRequest,
96
+ ): Promise<ReadTextFileResponse> {
97
+ return upstreamClient.readTextFile(params);
98
+ },
99
+
100
+ async writeTextFile(
101
+ params: WriteTextFileRequest,
102
+ ): Promise<WriteTextFileResponse> {
103
+ return upstreamClient.writeTextFile(params);
104
+ },
105
+
106
+ async createTerminal(
107
+ params: CreateTerminalRequest,
108
+ ): Promise<CreateTerminalResponse> {
109
+ const handle = await upstreamClient.createTerminal(params);
110
+ terminalHandles.set(handle.id, handle);
111
+ return { terminalId: handle.id };
112
+ },
113
+
114
+ async terminalOutput(
115
+ params: TerminalOutputRequest,
116
+ ): Promise<TerminalOutputResponse> {
117
+ const handle = terminalHandles.get(params.terminalId);
118
+ if (!handle) {
119
+ return { output: "", truncated: false };
120
+ }
121
+ return handle.currentOutput();
122
+ },
123
+
124
+ async releaseTerminal(
125
+ params: ReleaseTerminalRequest,
126
+ ): Promise<ReleaseTerminalResponse | undefined> {
127
+ const handle = terminalHandles.get(params.terminalId);
128
+ if (handle) {
129
+ terminalHandles.delete(params.terminalId);
130
+ const result = await handle.release();
131
+ return result ?? undefined;
132
+ }
133
+ },
134
+
135
+ async waitForTerminalExit(
136
+ params: WaitForTerminalExitRequest,
137
+ ): Promise<WaitForTerminalExitResponse> {
138
+ const handle = terminalHandles.get(params.terminalId);
139
+ if (!handle) {
140
+ return { exitCode: 1 };
141
+ }
142
+ return handle.waitForExit();
143
+ },
144
+
145
+ async killTerminal(
146
+ params: KillTerminalRequest,
147
+ ): Promise<KillTerminalResponse | undefined> {
148
+ const handle = terminalHandles.get(params.terminalId);
149
+ if (handle) {
150
+ return handle.kill();
151
+ }
152
+ },
153
+
154
+ async extMethod(
155
+ method: string,
156
+ params: Record<string, unknown>,
157
+ ): Promise<Record<string, unknown>> {
158
+ return upstreamClient.extMethod(method, params);
159
+ },
160
+
161
+ async extNotification(
162
+ method: string,
163
+ params: Record<string, unknown>,
164
+ ): Promise<void> {
165
+ return upstreamClient.extNotification(method, params);
166
+ },
167
+ };
168
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Session state tracking for Codex proxy agent.
3
+ * Tracks usage accumulation, model/mode state, and config options.
4
+ */
5
+
6
+ import type { SessionConfigOption } from "@agentclientprotocol/sdk";
7
+
8
+ export interface CodexUsage {
9
+ inputTokens: number;
10
+ outputTokens: number;
11
+ cachedReadTokens: number;
12
+ cachedWriteTokens: number;
13
+ }
14
+
15
+ export interface CodexSessionState {
16
+ sessionId: string;
17
+ cwd: string;
18
+ modelId?: string;
19
+ modeId: string;
20
+ configOptions: SessionConfigOption[];
21
+ accumulatedUsage: CodexUsage;
22
+ contextSize?: number;
23
+ contextUsed?: number;
24
+ cancelled: boolean;
25
+ interruptReason?: string;
26
+ taskRunId?: string;
27
+ taskId?: string;
28
+ }
29
+
30
+ export function createSessionState(
31
+ sessionId: string,
32
+ cwd: string,
33
+ opts?: {
34
+ taskRunId?: string;
35
+ taskId?: string;
36
+ modeId?: string;
37
+ modelId?: string;
38
+ },
39
+ ): CodexSessionState {
40
+ return {
41
+ sessionId,
42
+ cwd,
43
+ modeId: opts?.modeId ?? "default",
44
+ modelId: opts?.modelId,
45
+ configOptions: [],
46
+ accumulatedUsage: {
47
+ inputTokens: 0,
48
+ outputTokens: 0,
49
+ cachedReadTokens: 0,
50
+ cachedWriteTokens: 0,
51
+ },
52
+ cancelled: false,
53
+ taskRunId: opts?.taskRunId,
54
+ taskId: opts?.taskId,
55
+ };
56
+ }
57
+
58
+ export function resetUsage(state: CodexSessionState): void {
59
+ state.accumulatedUsage = {
60
+ inputTokens: 0,
61
+ outputTokens: 0,
62
+ cachedReadTokens: 0,
63
+ cachedWriteTokens: 0,
64
+ };
65
+ }