@posthog/agent 2.3.168 → 2.3.169

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@posthog/agent",
3
- "version": "2.3.168",
3
+ "version": "2.3.169",
4
4
  "repository": "https://github.com/PostHog/code",
5
5
  "description": "TypeScript agent framework wrapping Claude Agent SDK with Git-based task execution for PostHog",
6
6
  "exports": {
@@ -1,18 +1,15 @@
1
1
  import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk";
2
- import { POSTHOG_NOTIFICATIONS } from "../acp-extensions";
3
- import { formatModelId } from "../gateway-models";
4
2
  import type { SessionLogWriter } from "../session-log-writer";
5
3
  import type { ProcessSpawnedCallback } from "../types";
6
4
  import { Logger } from "../utils/logger";
7
5
  import {
8
6
  createBidirectionalStreams,
9
7
  createTappedWritableStream,
10
- nodeReadableToWebReadable,
11
- nodeWritableToWebWritable,
12
8
  type StreamPair,
13
9
  } from "../utils/streams";
14
10
  import { ClaudeAcpAgent } from "./claude/claude-agent";
15
- import { type CodexProcessOptions, spawnCodexProcess } from "./codex/spawn";
11
+ import { CodexAcpAgent } from "./codex/codex-agent";
12
+ import type { CodexProcessOptions } from "./codex/spawn";
16
13
 
17
14
  type AgentAdapter = "claude" | "codex";
18
15
 
@@ -37,108 +34,6 @@ export type AcpConnection = {
37
34
 
38
35
  export type InProcessAcpConnection = AcpConnection;
39
36
 
40
- type ModelOption = { value?: string; name?: string };
41
- type ModelGroup = { group?: string; name?: string; options?: ModelOption[] };
42
-
43
- type ConfigOption = {
44
- id?: string;
45
- category?: string | null;
46
- currentValue?: string;
47
- options?: Array<ModelOption | ModelGroup>;
48
- };
49
-
50
- function isGroupedOptions(
51
- options: NonNullable<ConfigOption["options"]>,
52
- ): options is ModelGroup[] {
53
- return options.length > 0 && "group" in options[0];
54
- }
55
-
56
- function formatOption(o: ModelOption): ModelOption {
57
- if (!o.value) return o;
58
- return { ...o, name: formatModelId(o.value) };
59
- }
60
-
61
- function filterModelConfigOptions(
62
- msg: Record<string, unknown>,
63
- allowedModelIds: Set<string>,
64
- ): Record<string, unknown> | null {
65
- const payload = msg as {
66
- method?: string;
67
- result?: { configOptions?: ConfigOption[] };
68
- params?: {
69
- update?: { sessionUpdate?: string; configOptions?: ConfigOption[] };
70
- };
71
- };
72
-
73
- const configOptions =
74
- payload.result?.configOptions ?? payload.params?.update?.configOptions;
75
- if (!configOptions) return null;
76
-
77
- const filtered = configOptions.map((opt) => {
78
- if (opt.category !== "model" || !opt.options) return opt;
79
-
80
- const options = opt.options;
81
- if (isGroupedOptions(options)) {
82
- const filteredOptions = options.map((group) => ({
83
- ...group,
84
- options: (group.options ?? [])
85
- .filter((o) => o?.value && allowedModelIds.has(o.value))
86
- .map(formatOption),
87
- }));
88
- const flat = filteredOptions.flatMap((g) => g.options ?? []);
89
- const currentAllowed =
90
- opt.currentValue && allowedModelIds.has(opt.currentValue);
91
- const nextCurrent =
92
- currentAllowed || flat.length === 0 ? opt.currentValue : flat[0]?.value;
93
-
94
- return {
95
- ...opt,
96
- currentValue: nextCurrent,
97
- options: filteredOptions,
98
- };
99
- }
100
-
101
- const valueOptions = options as ModelOption[];
102
- const filteredOptions = valueOptions
103
- .filter((o) => o?.value && allowedModelIds.has(o.value))
104
- .map(formatOption);
105
- const currentAllowed =
106
- opt.currentValue && allowedModelIds.has(opt.currentValue);
107
- const nextCurrent =
108
- currentAllowed || filteredOptions.length === 0
109
- ? opt.currentValue
110
- : filteredOptions[0]?.value;
111
-
112
- return {
113
- ...opt,
114
- currentValue: nextCurrent,
115
- options: filteredOptions,
116
- };
117
- });
118
-
119
- if (payload.result?.configOptions) {
120
- return { ...msg, result: { ...payload.result, configOptions: filtered } };
121
- }
122
- if (payload.params?.update?.configOptions) {
123
- return {
124
- ...msg,
125
- params: {
126
- ...payload.params,
127
- update: { ...payload.params.update, configOptions: filtered },
128
- },
129
- };
130
- }
131
- return null;
132
- }
133
-
134
- function extractReasoningEffort(
135
- configOptions: ConfigOption[] | undefined,
136
- ): string | undefined {
137
- if (!configOptions) return undefined;
138
- const option = configOptions.find((opt) => opt.id === "reasoning_effort");
139
- return option?.currentValue ?? undefined;
140
- }
141
-
142
37
  /**
143
38
  * Creates an ACP connection with the specified agent framework.
144
39
  *
@@ -234,247 +129,51 @@ function createClaudeConnection(config: AcpConnectionConfig): AcpConnection {
234
129
  };
235
130
  }
236
131
 
132
+ /**
133
+ * Creates an ACP connection to codex-acp via an in-process proxy agent.
134
+ *
135
+ * The CodexAcpAgent implements the ACP Agent interface and delegates to
136
+ * the codex-acp binary over a ClientSideConnection. This replaces the
137
+ * previous raw stream transform approach and gives us proper interception
138
+ * points for PostHog-specific features.
139
+ */
237
140
  function createCodexConnection(config: AcpConnectionConfig): AcpConnection {
238
141
  const logger =
239
142
  config.logger?.child("CodexConnection") ??
240
143
  new Logger({ debug: true, prefix: "[CodexConnection]" });
241
144
 
242
145
  const { logWriter } = config;
243
- const allowedModelIds = config.allowedModelIds;
244
-
245
- const codexProcess = spawnCodexProcess({
246
- ...config.codexOptions,
247
- logger,
248
- processCallbacks: config.processCallbacks,
249
- });
250
-
251
- let clientReadable = nodeReadableToWebReadable(codexProcess.stdout);
252
- let clientWritable = nodeWritableToWebWritable(codexProcess.stdin);
253
-
254
- let isLoadingSession = false;
255
- let loadRequestId: string | number | null = null;
256
- let newSessionRequestId: string | number | null = null;
257
- let sdkSessionEmitted = false;
258
- const reasoningEffortBySessionId = new Map<string, string>();
259
- let injectedConfigId = 0;
260
-
261
- const decoder = new TextDecoder();
262
- const encoder = new TextEncoder();
263
- let readBuffer = "";
264
-
265
- const taskRunId = config.taskRunId;
266
-
267
- const filteringReadable = clientReadable.pipeThrough(
268
- new TransformStream<Uint8Array, Uint8Array>({
269
- transform(chunk, controller) {
270
- readBuffer += decoder.decode(chunk, { stream: true });
271
- const lines = readBuffer.split("\n");
272
- readBuffer = lines.pop() ?? "";
273
-
274
- const outputLines: string[] = [];
275
-
276
- for (const line of lines) {
277
- const trimmed = line.trim();
278
- if (!trimmed) {
279
- outputLines.push(line);
280
- continue;
281
- }
282
-
283
- let shouldFilter = false;
284
146
 
285
- try {
286
- const msg = JSON.parse(trimmed);
287
- const sessionId =
288
- msg?.params?.sessionId ?? msg?.result?.sessionId ?? null;
289
- const configOptions =
290
- msg?.result?.configOptions ?? msg?.params?.update?.configOptions;
291
- if (sessionId && configOptions) {
292
- const effort = extractReasoningEffort(configOptions);
293
- if (effort) {
294
- reasoningEffortBySessionId.set(sessionId, effort);
295
- }
296
- }
297
-
298
- if (
299
- !sdkSessionEmitted &&
300
- newSessionRequestId !== null &&
301
- msg.id === newSessionRequestId &&
302
- "result" in msg
303
- ) {
304
- const sessionId = msg.result?.sessionId;
305
- if (sessionId && taskRunId) {
306
- const sdkSessionNotification = {
307
- jsonrpc: "2.0",
308
- method: POSTHOG_NOTIFICATIONS.SDK_SESSION,
309
- params: {
310
- taskRunId,
311
- sessionId,
312
- adapter: "codex",
313
- },
314
- };
315
- outputLines.push(JSON.stringify(sdkSessionNotification));
316
- sdkSessionEmitted = true;
317
- }
318
- newSessionRequestId = null;
319
- }
320
-
321
- if (isLoadingSession) {
322
- if (msg.id === loadRequestId && "result" in msg) {
323
- logger.debug("session/load complete, resuming stream");
324
- isLoadingSession = false;
325
- loadRequestId = null;
326
- } else if (msg.method === "session/update") {
327
- shouldFilter = true;
328
- }
329
- }
330
-
331
- if (!shouldFilter && allowedModelIds && allowedModelIds.size > 0) {
332
- const updated = filterModelConfigOptions(msg, allowedModelIds);
333
- if (updated) {
334
- outputLines.push(JSON.stringify(updated));
335
- continue;
336
- }
337
- }
338
- } catch {
339
- // Not valid JSON, pass through
340
- }
341
-
342
- if (!shouldFilter) {
343
- outputLines.push(line);
344
- const isChunkNoise =
345
- trimmed.includes('"sessionUpdate":"agent_message_chunk"') ||
346
- trimmed.includes('"sessionUpdate":"agent_thought_chunk"');
347
- if (!isChunkNoise) {
348
- logger.debug("codex-acp stdout:", trimmed);
349
- }
350
- }
351
- }
352
-
353
- if (outputLines.length > 0) {
354
- const output = `${outputLines.join("\n")}\n`;
355
- controller.enqueue(encoder.encode(output));
356
- }
357
- },
358
- flush(controller) {
359
- if (readBuffer.trim()) {
360
- controller.enqueue(encoder.encode(readBuffer));
361
- }
362
- },
363
- }),
364
- );
365
- clientReadable = filteringReadable;
366
-
367
- const originalWritable = clientWritable;
368
- clientWritable = new WritableStream({
369
- write(chunk) {
370
- const text = decoder.decode(chunk, { stream: true });
371
- const trimmed = text.trim();
372
- logger.debug("codex-acp stdin:", trimmed);
373
-
374
- try {
375
- const msg = JSON.parse(trimmed);
376
- if (
377
- msg.method === "session/set_config_option" &&
378
- msg.params?.configId === "reasoning_effort" &&
379
- msg.params?.sessionId &&
380
- msg.params?.value
381
- ) {
382
- reasoningEffortBySessionId.set(
383
- msg.params.sessionId,
384
- msg.params.value,
385
- );
386
- }
387
- if (msg.method === "session/prompt" && msg.params?.sessionId) {
388
- const effort = reasoningEffortBySessionId.get(msg.params.sessionId);
389
- if (effort) {
390
- const injection = {
391
- jsonrpc: "2.0",
392
- id: `reasoning_effort_${Date.now()}_${injectedConfigId++}`,
393
- method: "session/set_config_option",
394
- params: {
395
- sessionId: msg.params.sessionId,
396
- configId: "reasoning_effort",
397
- value: effort,
398
- },
399
- };
400
- const injectionLine = `${JSON.stringify(injection)}\n`;
401
- const writer = originalWritable.getWriter();
402
- return writer
403
- .write(encoder.encode(injectionLine))
404
- .then(() => writer.releaseLock())
405
- .then(() => {
406
- const nextWriter = originalWritable.getWriter();
407
- return nextWriter
408
- .write(chunk)
409
- .finally(() => nextWriter.releaseLock());
410
- });
411
- }
412
- }
413
- if (msg.method === "session/new" && msg.id) {
414
- logger.debug("session/new detected, tracking request ID");
415
- newSessionRequestId = msg.id;
416
- } else if (msg.method === "session/load" && msg.id) {
417
- logger.debug("session/load detected, pausing stream updates");
418
- isLoadingSession = true;
419
- loadRequestId = msg.id;
420
- }
421
- } catch {
422
- // Not valid JSON
423
- }
424
-
425
- const writer = originalWritable.getWriter();
426
- return writer.write(chunk).finally(() => writer.releaseLock());
427
- },
428
- close() {
429
- const writer = originalWritable.getWriter();
430
- return writer.close().finally(() => writer.releaseLock());
431
- },
432
- });
147
+ // Create bidirectional streams for client ↔ agent communication
148
+ const streams = createBidirectionalStreams();
433
149
 
434
- const shouldTapLogs = config.taskRunId && logWriter;
150
+ let agentWritable = streams.agent.writable;
151
+ let clientWritable = streams.client.writable;
435
152
 
436
- if (shouldTapLogs && config.taskRunId) {
437
- const taskRunId = config.taskRunId;
438
- if (!logWriter.isRegistered(taskRunId)) {
439
- logWriter.register(taskRunId, {
440
- taskId: config.taskId ?? taskRunId,
441
- runId: taskRunId,
153
+ // Tap streams for session log writing
154
+ if (config.taskRunId && logWriter) {
155
+ if (!logWriter.isRegistered(config.taskRunId)) {
156
+ logWriter.register(config.taskRunId, {
157
+ taskId: config.taskId ?? config.taskRunId,
158
+ runId: config.taskRunId,
159
+ deviceType: config.deviceType,
442
160
  });
443
161
  }
444
162
 
445
- clientWritable = createTappedWritableStream(clientWritable, {
163
+ const taskRunId = config.taskRunId;
164
+ agentWritable = createTappedWritableStream(streams.agent.writable, {
446
165
  onMessage: (line) => {
447
166
  logWriter.appendRawLine(taskRunId, line);
448
167
  },
449
168
  logger,
450
169
  });
451
170
 
452
- const originalReadable = clientReadable;
453
- const logDecoder = new TextDecoder();
454
- let logBuffer = "";
455
-
456
- clientReadable = originalReadable.pipeThrough(
457
- new TransformStream<Uint8Array, Uint8Array>({
458
- transform(chunk, controller) {
459
- logBuffer += logDecoder.decode(chunk, { stream: true });
460
- const lines = logBuffer.split("\n");
461
- logBuffer = lines.pop() ?? "";
462
-
463
- for (const line of lines) {
464
- if (line.trim()) {
465
- logWriter.appendRawLine(taskRunId, line);
466
- }
467
- }
468
-
469
- controller.enqueue(chunk);
470
- },
471
- flush() {
472
- if (logBuffer.trim()) {
473
- logWriter.appendRawLine(taskRunId, logBuffer);
474
- }
475
- },
476
- }),
477
- );
171
+ clientWritable = createTappedWritableStream(streams.client.writable, {
172
+ onMessage: (line) => {
173
+ logWriter.appendRawLine(taskRunId, line);
174
+ },
175
+ logger,
176
+ });
478
177
  } else {
479
178
  logger.info("Tapped streams NOT enabled for Codex", {
480
179
  hasTaskRunId: !!config.taskRunId,
@@ -482,18 +181,38 @@ function createCodexConnection(config: AcpConnectionConfig): AcpConnection {
482
181
  });
483
182
  }
484
183
 
184
+ const agentStream = ndJsonStream(agentWritable, streams.agent.readable);
185
+
186
+ let agent: CodexAcpAgent | null = null;
187
+ const agentConnection = new AgentSideConnection((client) => {
188
+ agent = new CodexAcpAgent(client, {
189
+ codexProcessOptions: config.codexOptions ?? {},
190
+ processCallbacks: config.processCallbacks,
191
+ });
192
+ logger.info(`Created ${agent.adapterName} agent`);
193
+ return agent;
194
+ }, agentStream);
195
+
485
196
  return {
486
- agentConnection: undefined,
197
+ agentConnection,
487
198
  clientStreams: {
488
- readable: clientReadable,
199
+ readable: streams.client.readable,
489
200
  writable: clientWritable,
490
201
  },
491
202
  cleanup: async () => {
492
203
  logger.info("Cleaning up Codex connection");
493
- codexProcess.kill();
204
+
205
+ if (agent) {
206
+ await agent.closeSession();
207
+ }
494
208
 
495
209
  try {
496
- await clientWritable.close();
210
+ await streams.client.writable.close();
211
+ } catch {
212
+ // Stream may already be closed
213
+ }
214
+ try {
215
+ await streams.agent.writable.close();
497
216
  } catch {
498
217
  // Stream may already be closed
499
218
  }
@@ -24,14 +24,25 @@ import {
24
24
  isAnthropicModel,
25
25
  } from "../gateway-models";
26
26
  import { Logger } from "../utils/logger";
27
- import type { SettingsManager } from "./claude/session/settings";
27
+ /**
28
+ * Shared settings manager interface that both Claude's SettingsManager
29
+ * and Codex's CodexSettingsManager implement. BaseAcpAgent only calls
30
+ * dispose() on this; each adapter's Session type narrows it to the
31
+ * concrete implementation.
32
+ */
33
+ export interface BaseSettingsManager {
34
+ dispose(): void;
35
+ getCwd(): string;
36
+ setCwd(cwd: string): Promise<void>;
37
+ initialize(): Promise<void>;
38
+ }
28
39
 
29
40
  export interface BaseSession {
30
41
  notificationHistory: SessionNotification[];
31
42
  cancelled: boolean;
32
43
  interruptReason?: string;
33
44
  abortController: AbortController;
34
- settingsManager: SettingsManager;
45
+ settingsManager: BaseSettingsManager;
35
46
  }
36
47
 
37
48
  const DEFAULT_CONTEXT_WINDOW = 200_000;