@posthog/agent 2.3.171 → 2.3.173

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.171",
3
+ "version": "2.3.173",
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": {
@@ -64,4 +64,7 @@ export const POSTHOG_NOTIFICATIONS = {
64
64
 
65
65
  /** Marks a boundary for log compaction */
66
66
  COMPACT_BOUNDARY: "_posthog/compact_boundary",
67
+
68
+ /** Token usage update for a session turn */
69
+ USAGE_UPDATE: "_posthog/usage_update",
67
70
  } as const;
@@ -44,6 +44,7 @@ import {
44
44
  } from "@anthropic-ai/claude-agent-sdk";
45
45
  import { v7 as uuidv7 } from "uuid";
46
46
  import packageJson from "../../../package.json" with { type: "json" };
47
+ import { POSTHOG_NOTIFICATIONS } from "../../acp-extensions";
47
48
  import { unreachable, withTimeout } from "../../utils/common";
48
49
  import { Logger } from "../../utils/logger";
49
50
  import { Pushable } from "../../utils/streams";
@@ -442,16 +443,19 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
442
443
  });
443
444
  }
444
445
 
445
- await this.client.extNotification("_posthog/usage_update", {
446
- sessionId: params.sessionId,
447
- used: {
448
- inputTokens: message.usage.input_tokens,
449
- outputTokens: message.usage.output_tokens,
450
- cachedReadTokens: message.usage.cache_read_input_tokens,
451
- cachedWriteTokens: message.usage.cache_creation_input_tokens,
446
+ await this.client.extNotification(
447
+ POSTHOG_NOTIFICATIONS.USAGE_UPDATE,
448
+ {
449
+ sessionId: params.sessionId,
450
+ used: {
451
+ inputTokens: message.usage.input_tokens,
452
+ outputTokens: message.usage.output_tokens,
453
+ cachedReadTokens: message.usage.cache_read_input_tokens,
454
+ cachedWriteTokens: message.usage.cache_creation_input_tokens,
455
+ },
456
+ cost: message.total_cost_usd,
452
457
  },
453
- cost: message.total_cost_usd,
454
- });
458
+ );
455
459
 
456
460
  const usage: Usage = {
457
461
  inputTokens: this.session.accumulatedUsage.inputTokens,
@@ -12,7 +12,6 @@
12
12
  import {
13
13
  type AgentSideConnection,
14
14
  type AuthenticateRequest,
15
- type CancelNotification,
16
15
  ClientSideConnection,
17
16
  type ForkSessionRequest,
18
17
  type ForkSessionResponse,
@@ -36,6 +35,10 @@ import {
36
35
  } from "@agentclientprotocol/sdk";
37
36
  import packageJson from "../../../package.json" with { type: "json" };
38
37
  import { POSTHOG_NOTIFICATIONS } from "../../acp-extensions";
38
+ import {
39
+ CODE_EXECUTION_MODES,
40
+ type CodeExecutionMode,
41
+ } from "../../execution-mode";
39
42
  import type { ProcessSpawnedCallback } from "../../types";
40
43
  import { Logger } from "../../utils/logger";
41
44
  import {
@@ -80,12 +83,26 @@ type CodexSession = BaseSession & {
80
83
  settingsManager: CodexSettingsManager;
81
84
  };
82
85
 
86
+ function toCodeExecutionMode(mode?: string): CodeExecutionMode {
87
+ if (mode && (CODE_EXECUTION_MODES as readonly string[]).includes(mode)) {
88
+ return mode as CodeExecutionMode;
89
+ }
90
+ return "default";
91
+ }
92
+
93
+ const CODEX_NATIVE_MODE: Record<CodeExecutionMode, string> = {
94
+ default: "default",
95
+ acceptEdits: "default",
96
+ plan: "plan",
97
+ bypassPermissions: "default",
98
+ };
99
+
83
100
  export class CodexAcpAgent extends BaseAcpAgent {
84
101
  readonly adapterName = "codex";
85
102
  declare session: CodexSession;
86
103
  private codexProcess: CodexProcess;
87
- private codexConnection!: ClientSideConnection;
88
- private sessionState!: CodexSessionState;
104
+ private codexConnection: ClientSideConnection;
105
+ private sessionState: CodexSessionState;
89
106
 
90
107
  constructor(client: AgentSideConnection, options: CodexAcpAgentOptions) {
91
108
  super(client);
@@ -114,28 +131,14 @@ export class CodexAcpAgent extends BaseAcpAgent {
114
131
  cancelled: false,
115
132
  };
116
133
 
134
+ this.sessionState = createSessionState("", cwd);
135
+
117
136
  // Create the ClientSideConnection to codex-acp.
118
137
  // The Client handler delegates all requests from codex-acp to the upstream
119
138
  // PostHog Code client via our AgentSideConnection.
120
139
  this.codexConnection = new ClientSideConnection(
121
140
  (_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
- ),
141
+ createCodexClient(this.client, this.logger, this.sessionState),
139
142
  codexStream,
140
143
  );
141
144
  }
@@ -182,6 +185,7 @@ export class CodexAcpAgent extends BaseAcpAgent {
182
185
  taskId: meta?.taskId ?? meta?.persistence?.taskId,
183
186
  modeId: response.modes?.currentModeId ?? "default",
184
187
  modelId: response.models?.currentModelId,
188
+ permissionMode: toCodeExecutionMode(meta?.permissionMode),
185
189
  });
186
190
  this.sessionId = response.sessionId;
187
191
  this.sessionState.configOptions = response.configOptions ?? [];
@@ -205,9 +209,11 @@ export class CodexAcpAgent extends BaseAcpAgent {
205
209
 
206
210
  async loadSession(params: LoadSessionRequest): Promise<LoadSessionResponse> {
207
211
  const response = await this.codexConnection.loadSession(params);
212
+ const meta = params._meta as NewSessionMeta | undefined;
208
213
 
209
- // Update session state
210
- this.sessionState = createSessionState(params.sessionId, params.cwd);
214
+ this.sessionState = createSessionState(params.sessionId, params.cwd, {
215
+ permissionMode: toCodeExecutionMode(meta?.permissionMode),
216
+ });
211
217
  this.sessionId = params.sessionId;
212
218
  this.sessionState.configOptions = response.configOptions ?? [];
213
219
 
@@ -224,11 +230,15 @@ export class CodexAcpAgent extends BaseAcpAgent {
224
230
  mcpServers: params.mcpServers ?? [],
225
231
  });
226
232
 
227
- this.sessionState = createSessionState(params.sessionId, params.cwd);
233
+ const meta = params._meta as NewSessionMeta | undefined;
234
+ this.sessionState = createSessionState(params.sessionId, params.cwd, {
235
+ taskRunId: meta?.taskRunId,
236
+ taskId: meta?.taskId ?? meta?.persistence?.taskId,
237
+ permissionMode: toCodeExecutionMode(meta?.permissionMode),
238
+ });
228
239
  this.sessionId = params.sessionId;
229
240
  this.sessionState.configOptions = loadResponse.configOptions ?? [];
230
241
 
231
- const meta = params._meta as NewSessionMeta | undefined;
232
242
  if (meta?.taskRunId) {
233
243
  await this.client.extNotification(POSTHOG_NOTIFICATIONS.SDK_SESSION, {
234
244
  taskRunId: meta.taskRunId,
@@ -254,7 +264,12 @@ export class CodexAcpAgent extends BaseAcpAgent {
254
264
  _meta: params._meta,
255
265
  });
256
266
 
257
- this.sessionState = createSessionState(newResponse.sessionId, params.cwd);
267
+ const meta = params._meta as NewSessionMeta | undefined;
268
+ this.sessionState = createSessionState(newResponse.sessionId, params.cwd, {
269
+ taskRunId: meta?.taskRunId,
270
+ taskId: meta?.taskId ?? meta?.persistence?.taskId,
271
+ permissionMode: toCodeExecutionMode(meta?.permissionMode),
272
+ });
258
273
  this.sessionId = newResponse.sessionId;
259
274
  this.sessionState.configOptions = newResponse.configOptions ?? [];
260
275
 
@@ -270,31 +285,21 @@ export class CodexAcpAgent extends BaseAcpAgent {
270
285
  async unstable_listSessions(
271
286
  params: ListSessionsRequest,
272
287
  ): Promise<ListSessionsResponse> {
273
- return this.codexConnection.listSessions(params);
288
+ return this.listSessions(params);
274
289
  }
275
290
 
276
291
  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
- }
292
+ this.session.cancelled = false;
293
+ this.session.interruptReason = undefined;
294
+ resetUsage(this.sessionState);
282
295
 
283
296
  const response = await this.codexConnection.prompt(params);
284
297
 
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
- }
298
+ // Usage is already accumulated via sessionUpdate notifications in
299
+ // codex-client.ts. Do NOT also add response.usage here or tokens
300
+ // get double-counted.
296
301
 
297
- if (this.sessionState?.taskRunId) {
302
+ if (this.sessionState.taskRunId) {
298
303
  const { accumulatedUsage } = this.sessionState;
299
304
 
300
305
  await this.client.extNotification(POSTHOG_NOTIFICATIONS.TURN_COMPLETE, {
@@ -314,7 +319,7 @@ export class CodexAcpAgent extends BaseAcpAgent {
314
319
  });
315
320
 
316
321
  if (response.usage) {
317
- await this.client.extNotification("_posthog/usage_update", {
322
+ await this.client.extNotification(POSTHOG_NOTIFICATIONS.USAGE_UPDATE, {
318
323
  sessionId: params.sessionId,
319
324
  used: {
320
325
  inputTokens: response.usage.inputTokens ?? 0,
@@ -331,32 +336,24 @@ export class CodexAcpAgent extends BaseAcpAgent {
331
336
  }
332
337
 
333
338
  protected async interrupt(): Promise<void> {
334
- if (this.sessionState) {
335
- this.sessionState.cancelled = true;
336
- }
337
339
  await this.codexConnection.cancel({
338
340
  sessionId: this.sessionId,
339
341
  });
340
342
  }
341
343
 
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
344
  async setSessionMode(
354
345
  params: SetSessionModeRequest,
355
346
  ): Promise<SetSessionModeResponse> {
356
- const response = await this.codexConnection.setSessionMode(params);
357
- if (this.sessionState) {
358
- this.sessionState.modeId = params.modeId;
359
- }
347
+ const requestedMode = toCodeExecutionMode(params.modeId);
348
+ const nativeMode = CODEX_NATIVE_MODE[requestedMode];
349
+
350
+ const response = await this.codexConnection.setSessionMode({
351
+ ...params,
352
+ modeId: nativeMode,
353
+ });
354
+
355
+ this.sessionState.modeId = nativeMode;
356
+ this.sessionState.permissionMode = requestedMode;
360
357
  return response ?? {};
361
358
  }
362
359
 
@@ -364,7 +361,7 @@ export class CodexAcpAgent extends BaseAcpAgent {
364
361
  params: SetSessionConfigOptionRequest,
365
362
  ): Promise<SetSessionConfigOptionResponse> {
366
363
  const response = await this.codexConnection.setSessionConfigOption(params);
367
- if (this.sessionState && response.configOptions) {
364
+ if (response.configOptions) {
368
365
  this.sessionState.configOptions = response.configOptions;
369
366
  }
370
367
  return response;
@@ -376,6 +373,7 @@ export class CodexAcpAgent extends BaseAcpAgent {
376
373
 
377
374
  async closeSession(): Promise<void> {
378
375
  this.logger.info("Closing Codex session", { sessionId: this.sessionId });
376
+ this.session.abortController.abort();
379
377
  this.session.settingsManager.dispose();
380
378
  try {
381
379
  this.codexProcess.kill();
@@ -23,11 +23,13 @@ import type {
23
23
  TerminalHandle,
24
24
  TerminalOutputRequest,
25
25
  TerminalOutputResponse,
26
+ ToolKind,
26
27
  WaitForTerminalExitRequest,
27
28
  WaitForTerminalExitResponse,
28
29
  WriteTextFileRequest,
29
30
  WriteTextFileResponse,
30
31
  } from "@agentclientprotocol/sdk";
32
+ import type { CodeExecutionMode } from "../../execution-mode";
31
33
  import type { Logger } from "../../utils/logger";
32
34
  import type { CodexSessionState } from "./session-state";
33
35
 
@@ -36,6 +38,33 @@ export interface CodexClientCallbacks {
36
38
  onUsageUpdate?: (update: Record<string, unknown>) => void;
37
39
  }
38
40
 
41
+ const AUTO_APPROVED_KINDS: Record<CodeExecutionMode, Set<ToolKind>> = {
42
+ default: new Set(["read", "search", "fetch", "think"]),
43
+ acceptEdits: new Set(["read", "edit", "search", "fetch", "think"]),
44
+ plan: new Set(["read", "search", "fetch", "think"]),
45
+ bypassPermissions: new Set([
46
+ "read",
47
+ "edit",
48
+ "delete",
49
+ "move",
50
+ "search",
51
+ "execute",
52
+ "think",
53
+ "fetch",
54
+ "switch_mode",
55
+ "other",
56
+ ]),
57
+ };
58
+
59
+ function shouldAutoApprove(
60
+ mode: CodeExecutionMode,
61
+ kind: ToolKind | null | undefined,
62
+ ): boolean {
63
+ if (mode === "bypassPermissions") return true;
64
+ if (!kind) return false;
65
+ return AUTO_APPROVED_KINDS[mode]?.has(kind) ?? false;
66
+ }
67
+
39
68
  /**
40
69
  * Creates an ACP Client that delegates all requests from codex-acp
41
70
  * to the upstream PostHog Code client (via AgentSideConnection).
@@ -46,16 +75,31 @@ export function createCodexClient(
46
75
  sessionState: CodexSessionState,
47
76
  callbacks?: CodexClientCallbacks,
48
77
  ): Client {
49
- // Track terminal handles for delegation
50
78
  const terminalHandles = new Map<string, TerminalHandle>();
51
79
 
52
80
  return {
53
81
  async requestPermission(
54
82
  params: RequestPermissionRequest,
55
83
  ): Promise<RequestPermissionResponse> {
56
- logger.debug("Relaying permission request to upstream", {
57
- sessionId: params.sessionId,
58
- });
84
+ const kind = params.toolCall?.kind as ToolKind | null | undefined;
85
+
86
+ if (shouldAutoApprove(sessionState.permissionMode, kind)) {
87
+ logger.debug("Auto-approving permission", {
88
+ mode: sessionState.permissionMode,
89
+ kind,
90
+ toolCallId: params.toolCall?.toolCallId,
91
+ });
92
+ const allowOption = params.options?.find(
93
+ (o) => o.kind === "allow_once" || o.kind === "allow_always",
94
+ );
95
+ return {
96
+ outcome: {
97
+ outcome: "selected",
98
+ optionId: allowOption?.optionId ?? "allow",
99
+ },
100
+ };
101
+ }
102
+
59
103
  return upstreamClient.requestPermission(params);
60
104
  },
61
105
 
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import type { SessionConfigOption } from "@agentclientprotocol/sdk";
7
+ import type { CodeExecutionMode } from "../../execution-mode";
7
8
 
8
9
  export interface CodexUsage {
9
10
  inputTokens: number;
@@ -21,8 +22,7 @@ export interface CodexSessionState {
21
22
  accumulatedUsage: CodexUsage;
22
23
  contextSize?: number;
23
24
  contextUsed?: number;
24
- cancelled: boolean;
25
- interruptReason?: string;
25
+ permissionMode: CodeExecutionMode;
26
26
  taskRunId?: string;
27
27
  taskId?: string;
28
28
  }
@@ -35,6 +35,7 @@ export function createSessionState(
35
35
  taskId?: string;
36
36
  modeId?: string;
37
37
  modelId?: string;
38
+ permissionMode?: CodeExecutionMode;
38
39
  },
39
40
  ): CodexSessionState {
40
41
  return {
@@ -49,7 +50,7 @@ export function createSessionState(
49
50
  cachedReadTokens: 0,
50
51
  cachedWriteTokens: 0,
51
52
  },
52
- cancelled: false,
53
+ permissionMode: opts?.permissionMode ?? "default",
53
54
  taskRunId: opts?.taskRunId,
54
55
  taskId: opts?.taskId,
55
56
  };
@@ -46,6 +46,8 @@ function buildConfigArgs(options: CodexProcessOptions): string[] {
46
46
  if (options.instructions) {
47
47
  const escaped = options.instructions
48
48
  .replace(/\\/g, "\\\\")
49
+ .replace(/\n/g, "\\n")
50
+ .replace(/\r/g, "\\r")
49
51
  .replace(/"/g, '\\"');
50
52
  args.push("-c", `instructions="${escaped}"`);
51
53
  }
@@ -1,7 +1,7 @@
1
1
  import { IS_ROOT } from "./utils/common";
2
2
 
3
3
  export interface ModeInfo {
4
- id: CodeExecutionMode;
4
+ id: string;
5
5
  name: string;
6
6
  description: string;
7
7
  }
@@ -57,3 +57,39 @@ export function getAvailableModes(): ModeInfo[] {
57
57
  ? availableModes.filter((m) => m.id !== "bypassPermissions")
58
58
  : availableModes;
59
59
  }
60
+
61
+ // --- Codex-native modes ---
62
+
63
+ export const CODEX_NATIVE_MODES = ["auto", "read-only", "full-access"] as const;
64
+
65
+ export type CodexNativeMode = (typeof CODEX_NATIVE_MODES)[number];
66
+
67
+ /** Union of all permission mode IDs across adapters */
68
+ export type PermissionMode = CodeExecutionMode | CodexNativeMode;
69
+
70
+ const codexModes: ModeInfo[] = [
71
+ {
72
+ id: "read-only",
73
+ name: "Read Only",
74
+ description: "Read-only access, no file modifications",
75
+ },
76
+ {
77
+ id: "auto",
78
+ name: "Auto",
79
+ description: "Standard behavior, prompts for dangerous operations",
80
+ },
81
+ ];
82
+
83
+ if (ALLOW_BYPASS) {
84
+ codexModes.push({
85
+ id: "full-access",
86
+ name: "Full Access",
87
+ description: "Auto-accept all permission requests",
88
+ });
89
+ }
90
+
91
+ export function getAvailableCodexModes(): ModeInfo[] {
92
+ return IS_ROOT
93
+ ? codexModes.filter((m) => m.id !== "full-access")
94
+ : codexModes;
95
+ }