@posthog/agent 2.3.263 → 2.3.278

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.263",
3
+ "version": "2.3.278",
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": {
@@ -48,6 +48,10 @@
48
48
  "types": "./dist/adapters/claude/session/models.d.ts",
49
49
  "import": "./dist/adapters/claude/session/models.js"
50
50
  },
51
+ "./adapters/reasoning-effort": {
52
+ "types": "./dist/adapters/reasoning-effort.d.ts",
53
+ "import": "./dist/adapters/reasoning-effort.js"
54
+ },
51
55
  "./execution-mode": {
52
56
  "types": "./dist/execution-mode.d.ts",
53
57
  "import": "./dist/execution-mode.js"
@@ -100,7 +100,7 @@ export function buildExitPlanModePermissionOptions(): PermissionOption[] {
100
100
  if (ALLOW_BYPASS) {
101
101
  options.push({
102
102
  kind: "allow_always",
103
- name: "Yes, auto-accept all permissions",
103
+ name: "Yes, bypass all permissions",
104
104
  optionId: "bypassPermissions",
105
105
  });
106
106
  }
@@ -0,0 +1,16 @@
1
+ interface ReasoningEffortOption {
2
+ value: string;
3
+ name: string;
4
+ }
5
+
6
+ const CODEX_REASONING_EFFORT_OPTIONS: ReasoningEffortOption[] = [
7
+ { value: "low", name: "Low" },
8
+ { value: "medium", name: "Medium" },
9
+ { value: "high", name: "High" },
10
+ ];
11
+
12
+ export function getReasoningEffortOptions(
13
+ _modelId: string,
14
+ ): ReasoningEffortOption[] {
15
+ return CODEX_REASONING_EFFORT_OPTIONS;
16
+ }
@@ -11,6 +11,7 @@ export interface CodexProcessOptions {
11
11
  apiBaseUrl?: string;
12
12
  apiKey?: string;
13
13
  model?: string;
14
+ reasoningEffort?: string;
14
15
  instructions?: string;
15
16
  binaryPath?: string;
16
17
  logger?: Logger;
@@ -52,6 +53,10 @@ function buildConfigArgs(options: CodexProcessOptions): string[] {
52
53
  args.push("-c", `model="${options.model}"`);
53
54
  }
54
55
 
56
+ if (options.reasoningEffort) {
57
+ args.push("-c", `model_reasoning_effort="${options.reasoningEffort}"`);
58
+ }
59
+
55
60
  if (options.instructions) {
56
61
  const escaped = options.instructions
57
62
  .replace(/\\/g, "\\\\")
@@ -0,0 +1,35 @@
1
+ import { getEffortOptions as getClaudeEffortOptions } from "./claude/session/models";
2
+ import { getReasoningEffortOptions as getCodexReasoningEffortOptions } from "./codex/models";
3
+
4
+ export type RuntimeAdapter = "claude" | "codex";
5
+
6
+ export type SupportedReasoningEffort = "low" | "medium" | "high" | "max";
7
+
8
+ export interface ReasoningEffortOption {
9
+ value: SupportedReasoningEffort;
10
+ name: string;
11
+ }
12
+
13
+ export function getReasoningEffortOptions(
14
+ adapter: RuntimeAdapter,
15
+ modelId: string,
16
+ ): ReasoningEffortOption[] | null {
17
+ const options =
18
+ adapter === "codex"
19
+ ? getCodexReasoningEffortOptions(modelId)
20
+ : getClaudeEffortOptions(modelId);
21
+
22
+ return options as ReasoningEffortOption[] | null;
23
+ }
24
+
25
+ export function isSupportedReasoningEffort(
26
+ adapter: RuntimeAdapter,
27
+ modelId: string,
28
+ value: string,
29
+ ): value is SupportedReasoningEffort {
30
+ return (
31
+ getReasoningEffortOptions(adapter, modelId)?.some(
32
+ (option) => option.value === value,
33
+ ) ?? false
34
+ );
35
+ }
@@ -35,8 +35,8 @@ const availableModes: ModeInfo[] = [
35
35
  if (ALLOW_BYPASS) {
36
36
  availableModes.push({
37
37
  id: "bypassPermissions",
38
- name: "Auto-accept Permissions",
39
- description: "Auto-accept all permission requests",
38
+ name: "Bypass Permissions",
39
+ description: "Bypass all permission prompts",
40
40
  });
41
41
  }
42
42
 
@@ -84,7 +84,7 @@ if (ALLOW_BYPASS) {
84
84
  codexModes.push({
85
85
  id: "full-access",
86
86
  name: "Full Access",
87
- description: "Auto-accept all permission requests",
87
+ description: "Bypass all permission prompts",
88
88
  });
89
89
  }
90
90
 
@@ -21,6 +21,17 @@ interface TestableServer {
21
21
  detectedPrUrl: string | null;
22
22
  buildCloudSystemPrompt(prUrl?: string | null): string;
23
23
  buildDetectedPrContext(prUrl: string): string;
24
+ buildSessionSystemPrompt(prUrl?: string | null): string | { append: string };
25
+ buildCodexInstructions(systemPrompt: string | { append: string }): string;
26
+ getRuntimeAdapter(): "claude" | "codex";
27
+ }
28
+
29
+ let nextTestPort = 20000;
30
+
31
+ function getNextTestPort(): number {
32
+ const port = nextTestPort;
33
+ nextTestPort += 1;
34
+ return port;
24
35
  }
25
36
 
26
37
  // The Claude Agent SDK has an internal readMessages() loop that rejects with
@@ -112,14 +123,16 @@ JwIDAQAB
112
123
 
113
124
  describe("AgentServer HTTP Mode", () => {
114
125
  let repo: TestRepo;
115
- let server: AgentServer;
126
+ let server: AgentServer | undefined;
116
127
  let mswServer: SetupServerApi;
117
128
  let appendLogCalls: unknown[][];
118
- const port = 3099;
129
+ let port: number;
119
130
 
120
131
  beforeEach(async () => {
121
132
  repo = await createTestRepo("agent-server-http");
122
133
  appendLogCalls = [];
134
+ // Use a unique high port per test to avoid reuse and browser-blocked ports.
135
+ port = getNextTestPort();
123
136
  mswServer = setupServer(
124
137
  ...createPostHogHandlers({
125
138
  baseUrl: "http://localhost:8000",
@@ -132,12 +145,15 @@ describe("AgentServer HTTP Mode", () => {
132
145
  afterEach(async () => {
133
146
  if (server) {
134
147
  await server.stop();
148
+ server = undefined;
135
149
  }
136
150
  mswServer.close();
137
151
  await repo.cleanup();
138
152
  });
139
153
 
140
- const createServer = () => {
154
+ const createServer = (
155
+ overrides: Partial<ConstructorParameters<typeof AgentServer>[0]> = {},
156
+ ) => {
141
157
  server = new AgentServer({
142
158
  port,
143
159
  jwtPublicKey: TEST_PUBLIC_KEY,
@@ -148,6 +164,7 @@ describe("AgentServer HTTP Mode", () => {
148
164
  mode: "interactive",
149
165
  taskId: "test-task-id",
150
166
  runId: "test-run-id",
167
+ ...overrides,
151
168
  });
152
169
  return server;
153
170
  };
@@ -176,7 +193,7 @@ describe("AgentServer HTTP Mode", () => {
176
193
 
177
194
  expect(response.status).toBe(200);
178
195
  expect(body).toEqual({ status: "ok", hasSession: true });
179
- });
196
+ }, 30000);
180
197
  });
181
198
 
182
199
  describe("GET /events", () => {
@@ -188,7 +205,7 @@ describe("AgentServer HTTP Mode", () => {
188
205
 
189
206
  expect(response.status).toBe(401);
190
207
  expect(body.error).toBe("Missing authorization header");
191
- });
208
+ }, 20000);
192
209
 
193
210
  it("returns 401 with invalid token", async () => {
194
211
  await createServer().start();
@@ -200,7 +217,7 @@ describe("AgentServer HTTP Mode", () => {
200
217
 
201
218
  expect(response.status).toBe(401);
202
219
  expect(body.code).toBe("invalid_signature");
203
- });
220
+ }, 20000);
204
221
 
205
222
  it("accepts valid JWT and returns SSE stream", async () => {
206
223
  await createServer().start();
@@ -212,7 +229,7 @@ describe("AgentServer HTTP Mode", () => {
212
229
 
213
230
  expect(response.status).toBe(200);
214
231
  expect(response.headers.get("content-type")).toBe("text/event-stream");
215
- });
232
+ }, 20000);
216
233
  });
217
234
 
218
235
  describe("POST /command", () => {
@@ -230,7 +247,7 @@ describe("AgentServer HTTP Mode", () => {
230
247
  });
231
248
 
232
249
  expect(response.status).toBe(401);
233
- });
250
+ }, 20000);
234
251
 
235
252
  it("returns 400 when run_id does not match active session", async () => {
236
253
  await createServer().start();
@@ -252,7 +269,7 @@ describe("AgentServer HTTP Mode", () => {
252
269
  expect(response.status).toBe(400);
253
270
  const body = await response.json();
254
271
  expect(body.error).toBe("No active session for this run");
255
- });
272
+ }, 20000);
256
273
 
257
274
  it("accepts structured user_message content", async () => {
258
275
  await createServer().start();
@@ -276,7 +293,7 @@ describe("AgentServer HTTP Mode", () => {
276
293
  expect(response.status).toBe(400);
277
294
  const body = await response.json();
278
295
  expect(body.error).toBe("No active session for this run");
279
- });
296
+ }, 20000);
280
297
  });
281
298
 
282
299
  describe("404 handling", () => {
@@ -288,7 +305,7 @@ describe("AgentServer HTTP Mode", () => {
288
305
 
289
306
  expect(response.status).toBe(404);
290
307
  expect(body.error).toBe("Not found");
291
- });
308
+ }, 20000);
292
309
  });
293
310
 
294
311
  describe("getInitialPromptOverride", () => {
@@ -335,6 +352,48 @@ describe("AgentServer HTTP Mode", () => {
335
352
  });
336
353
  });
337
354
 
355
+ describe("runtime adapter selection", () => {
356
+ it("defaults to claude when no runtime adapter is configured", () => {
357
+ const s = createServer();
358
+
359
+ expect((s as unknown as TestableServer).getRuntimeAdapter()).toBe(
360
+ "claude",
361
+ );
362
+ });
363
+
364
+ it("uses codex when the runtime adapter is configured", () => {
365
+ const s = createServer({ runtimeAdapter: "codex" });
366
+
367
+ expect((s as unknown as TestableServer).getRuntimeAdapter()).toBe(
368
+ "codex",
369
+ );
370
+ });
371
+
372
+ it("flattens append-style prompts into plain codex instructions", () => {
373
+ const s = createServer({
374
+ claudeCode: {
375
+ systemPrompt: {
376
+ type: "preset",
377
+ preset: "claude_code",
378
+ append: "User codex instructions",
379
+ },
380
+ },
381
+ });
382
+
383
+ const sessionPrompt = (
384
+ s as unknown as TestableServer
385
+ ).buildSessionSystemPrompt("https://github.com/PostHog/code/pull/1");
386
+
387
+ expect(typeof sessionPrompt).toBe("object");
388
+ expect(
389
+ (s as unknown as TestableServer).buildCodexInstructions(sessionPrompt),
390
+ ).toContain("User codex instructions");
391
+ expect(
392
+ (s as unknown as TestableServer).buildCodexInstructions(sessionPrompt),
393
+ ).toContain("Cloud Task Execution");
394
+ });
395
+ });
396
+
338
397
  describe("detectedPrUrl tracking", () => {
339
398
  it("stores PR URL when detectAndAttachPrUrl finds a match", () => {
340
399
  const s = createServer();
@@ -19,6 +19,7 @@ import {
19
19
  } from "../adapters/acp-connection";
20
20
  import { selectRecentTurns } from "../adapters/claude/session/jsonl-hydration";
21
21
  import type { CodeExecutionMode } from "../execution-mode";
22
+ import { DEFAULT_CODEX_MODEL } from "../gateway-models";
22
23
  import { PostHogAPIClient } from "../posthog-api";
23
24
  import {
24
25
  type ConversationTurn,
@@ -168,6 +169,20 @@ interface ActiveSession {
168
169
  hasDesktopConnected: boolean;
169
170
  }
170
171
 
172
+ function getTaskRunStateString(
173
+ taskRun: TaskRun | null,
174
+ key: string,
175
+ ): string | null {
176
+ const state = taskRun?.state;
177
+
178
+ if (!state || typeof state !== "object") {
179
+ return null;
180
+ }
181
+
182
+ const value = (state as Record<string, unknown>)[key];
183
+ return typeof value === "string" ? value : null;
184
+ }
185
+
171
186
  export class AgentServer {
172
187
  private config: AgentServerConfig;
173
188
  private logger: Logger;
@@ -242,6 +257,10 @@ export class AgentServer {
242
257
  this.app = this.createApp();
243
258
  }
244
259
 
260
+ private getRuntimeAdapter(): "claude" | "codex" {
261
+ return this.config.runtimeAdapter ?? "claude";
262
+ }
263
+
245
264
  private getEffectiveMode(payload: JwtPayload): AgentMode {
246
265
  return payload.mode ?? this.config.mode;
247
266
  }
@@ -702,6 +721,39 @@ export class AgentServer {
702
721
 
703
722
  this.configureEnvironment();
704
723
 
724
+ const [preTaskRun, preTask] = await Promise.all([
725
+ this.posthogAPI
726
+ .getTaskRun(payload.task_id, payload.run_id)
727
+ .catch((err) => {
728
+ this.logger.warn("Failed to fetch task run for session context", {
729
+ taskId: payload.task_id,
730
+ runId: payload.run_id,
731
+ error: err,
732
+ });
733
+ return null;
734
+ }),
735
+ this.posthogAPI.getTask(payload.task_id).catch((err) => {
736
+ this.logger.warn("Failed to fetch task for session context", {
737
+ taskId: payload.task_id,
738
+ error: err,
739
+ });
740
+ return null;
741
+ }),
742
+ ]);
743
+
744
+ const prUrl = getTaskRunStateString(preTaskRun, "slack_notified_pr_url");
745
+
746
+ if (prUrl) {
747
+ this.detectedPrUrl = prUrl;
748
+ }
749
+
750
+ const runtimeAdapter = this.getRuntimeAdapter();
751
+ const sessionSystemPrompt = this.buildSessionSystemPrompt(prUrl);
752
+ const codexInstructions =
753
+ runtimeAdapter === "codex"
754
+ ? this.buildCodexInstructions(sessionSystemPrompt)
755
+ : undefined;
756
+
705
757
  const posthogAPI = new PostHogAPIClient({
706
758
  apiUrl: this.config.apiUrl,
707
759
  projectId: this.config.projectId,
@@ -725,10 +777,23 @@ export class AgentServer {
725
777
  });
726
778
 
727
779
  const acpConnection = createAcpConnection({
780
+ adapter: runtimeAdapter,
728
781
  taskRunId: payload.run_id,
729
782
  taskId: payload.task_id,
730
783
  deviceType: deviceInfo.type,
731
784
  logWriter,
785
+ logger: this.logger,
786
+ codexOptions:
787
+ runtimeAdapter === "codex"
788
+ ? {
789
+ cwd: this.config.repositoryPath ?? "/tmp/workspace",
790
+ apiBaseUrl: process.env.OPENAI_BASE_URL,
791
+ apiKey: this.config.apiKey,
792
+ model: this.config.model ?? DEFAULT_CODEX_MODEL,
793
+ reasoningEffort: this.config.reasoningEffort,
794
+ instructions: codexInstructions,
795
+ }
796
+ : undefined,
732
797
  onStructuredOutput: async (output) => {
733
798
  await this.posthogAPI.setTaskRunOutput(
734
799
  payload.task_id,
@@ -773,37 +838,6 @@ export class AgentServer {
773
838
  clientCapabilities: {},
774
839
  });
775
840
 
776
- const [preTaskRun, preTask] = await Promise.all([
777
- this.posthogAPI
778
- .getTaskRun(payload.task_id, payload.run_id)
779
- .catch((err) => {
780
- this.logger.warn("Failed to fetch task run for session context", {
781
- taskId: payload.task_id,
782
- runId: payload.run_id,
783
- error: err,
784
- });
785
- return null;
786
- }),
787
- this.posthogAPI.getTask(payload.task_id).catch((err) => {
788
- this.logger.warn("Failed to fetch task for session context", {
789
- taskId: payload.task_id,
790
- error: err,
791
- });
792
- return null;
793
- }),
794
- ]);
795
-
796
- const prUrl =
797
- typeof (preTaskRun?.state as Record<string, unknown>)
798
- ?.slack_notified_pr_url === "string"
799
- ? ((preTaskRun?.state as Record<string, unknown>)
800
- .slack_notified_pr_url as string)
801
- : null;
802
-
803
- if (prUrl) {
804
- this.detectedPrUrl = prUrl;
805
- }
806
-
807
841
  const runState = preTaskRun?.state as Record<string, unknown> | undefined;
808
842
  // Cloud runs default to bypassPermissions (auto-approve everything).
809
843
  // Only PostHog Code sets initial_permission_mode explicitly (e.g., "plan").
@@ -811,21 +845,27 @@ export class AgentServer {
811
845
  typeof runState?.initial_permission_mode === "string"
812
846
  ? (runState.initial_permission_mode as CodeExecutionMode)
813
847
  : "bypassPermissions";
814
-
815
848
  const sessionResponse = await clientConnection.newSession({
816
849
  cwd: this.config.repositoryPath ?? "/tmp/workspace",
817
850
  mcpServers: this.config.mcpServers ?? [],
818
851
  _meta: {
819
852
  sessionId: payload.run_id,
820
853
  taskRunId: payload.run_id,
821
- systemPrompt: this.buildSessionSystemPrompt(prUrl),
854
+ systemPrompt: sessionSystemPrompt,
855
+ ...(this.config.model && { model: this.config.model }),
822
856
  allowedDomains: this.config.allowedDomains,
823
857
  jsonSchema: preTask?.json_schema ?? null,
824
858
  permissionMode: initialPermissionMode,
825
859
  ...(this.config.claudeCode?.plugins?.length && {
826
860
  claudeCode: {
827
861
  options: {
828
- plugins: this.config.claudeCode.plugins,
862
+ ...(this.config.claudeCode?.plugins?.length && {
863
+ plugins: this.config.claudeCode.plugins,
864
+ }),
865
+ ...(runtimeAdapter === "claude" &&
866
+ this.config.reasoningEffort && {
867
+ effort: this.config.reasoningEffort,
868
+ }),
829
869
  },
830
870
  },
831
871
  }),
@@ -1197,6 +1237,14 @@ export class AgentServer {
1197
1237
  return { append: cloudAppend };
1198
1238
  }
1199
1239
 
1240
+ private buildCodexInstructions(
1241
+ systemPrompt: string | { append: string },
1242
+ ): string {
1243
+ return typeof systemPrompt === "string"
1244
+ ? systemPrompt
1245
+ : systemPrompt.append;
1246
+ }
1247
+
1200
1248
  private getCloudInteractionOrigin(): string | undefined {
1201
1249
  return (
1202
1250
  process.env.POSTHOG_CODE_INTERACTION_ORIGIN ??
package/src/server/bin.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from "commander";
3
3
  import { z } from "zod/v4";
4
+ import { isSupportedReasoningEffort } from "../adapters/reasoning-effort";
4
5
  import { AgentServer } from "./agent-server";
5
6
  import { claudeCodeConfigSchema, mcpServersSchema } from "./schemas";
6
7
 
@@ -26,6 +27,11 @@ const envSchema = z.object({
26
27
  })
27
28
  .regex(/^\d+$/, "POSTHOG_PROJECT_ID must be a numeric string")
28
29
  .transform((val) => parseInt(val, 10)),
30
+ POSTHOG_CODE_RUNTIME_ADAPTER: z.enum(["claude", "codex"]).optional(),
31
+ POSTHOG_CODE_MODEL: z.string().optional(),
32
+ POSTHOG_CODE_REASONING_EFFORT: z
33
+ .enum(["low", "medium", "high", "max"])
34
+ .optional(),
29
35
  });
30
36
 
31
37
  const program = new Command();
@@ -124,6 +130,21 @@ program
124
130
  .filter(Boolean)
125
131
  : undefined;
126
132
 
133
+ if (
134
+ env.POSTHOG_CODE_RUNTIME_ADAPTER &&
135
+ env.POSTHOG_CODE_MODEL &&
136
+ env.POSTHOG_CODE_REASONING_EFFORT &&
137
+ !isSupportedReasoningEffort(
138
+ env.POSTHOG_CODE_RUNTIME_ADAPTER,
139
+ env.POSTHOG_CODE_MODEL,
140
+ env.POSTHOG_CODE_REASONING_EFFORT,
141
+ )
142
+ ) {
143
+ program.error(
144
+ `POSTHOG_CODE_REASONING_EFFORT '${env.POSTHOG_CODE_REASONING_EFFORT}' is not supported for ${env.POSTHOG_CODE_RUNTIME_ADAPTER} model '${env.POSTHOG_CODE_MODEL}'.`,
145
+ );
146
+ }
147
+
127
148
  const server = new AgentServer({
128
149
  port: parseInt(options.port, 10),
129
150
  jwtPublicKey: env.JWT_PUBLIC_KEY,
@@ -139,6 +160,9 @@ program
139
160
  baseBranch: options.baseBranch,
140
161
  claudeCode,
141
162
  allowedDomains,
163
+ runtimeAdapter: env.POSTHOG_CODE_RUNTIME_ADAPTER,
164
+ model: env.POSTHOG_CODE_MODEL,
165
+ reasoningEffort: env.POSTHOG_CODE_REASONING_EFFORT,
142
166
  });
143
167
 
144
168
  process.on("SIGINT", async () => {
@@ -24,4 +24,7 @@ export interface AgentServerConfig {
24
24
  baseBranch?: string;
25
25
  claudeCode?: ClaudeCodeConfig;
26
26
  allowedDomains?: string[];
27
+ runtimeAdapter?: "claude" | "codex";
28
+ model?: string;
29
+ reasoningEffort?: "low" | "medium" | "high" | "max";
27
30
  }
@@ -20,6 +20,30 @@ export function createPostHogHandlers(options: PostHogHandlersOptions = {}) {
20
20
  } = options;
21
21
 
22
22
  return [
23
+ // GET local LLM gateway models - session initialization fetches these in the
24
+ // background for command/model metadata.
25
+ http.get("http://localhost:3308/:product/v1/models", () => {
26
+ return HttpResponse.json({
27
+ object: "list",
28
+ data: [
29
+ {
30
+ id: "claude-opus-4-6",
31
+ owned_by: "anthropic",
32
+ context_window: 200000,
33
+ supports_streaming: true,
34
+ supports_vision: true,
35
+ },
36
+ {
37
+ id: "gpt-5.4",
38
+ owned_by: "openai",
39
+ context_window: 200000,
40
+ supports_streaming: true,
41
+ supports_vision: true,
42
+ },
43
+ ],
44
+ });
45
+ }),
46
+
23
47
  // POST /append_log/ - Agent log entries
24
48
  http.post(
25
49
  `${baseUrl}/api/projects/:projectId/tasks/:taskId/runs/:runId/append_log/`,