@posthog/agent 2.3.261 → 2.3.267

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.261",
3
+ "version": "2.3.267",
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"
@@ -82,8 +86,8 @@
82
86
  "tsx": "^4.20.6",
83
87
  "typescript": "^5.5.0",
84
88
  "vitest": "^2.1.8",
85
- "@posthog/git": "1.0.0",
86
- "@posthog/shared": "1.0.0"
89
+ "@posthog/shared": "1.0.0",
90
+ "@posthog/git": "1.0.0"
87
91
  },
88
92
  "dependencies": {
89
93
  "@agentclientprotocol/sdk": "0.16.1",
@@ -63,6 +63,9 @@ export const POSTHOG_NOTIFICATIONS = {
63
63
 
64
64
  /** Token usage update for a session turn */
65
65
  USAGE_UPDATE: "_posthog/usage_update",
66
+
67
+ /** Response to a relayed permission request (plan approval, question) */
68
+ PERMISSION_RESPONSE: "_posthog/permission_response",
66
69
  } as const;
67
70
 
68
71
  type NotificationMethod =
@@ -490,11 +490,17 @@ export async function canUseTool(
490
490
  return planFileResult;
491
491
  }
492
492
 
493
- // if (session.permissionMode === "dontAsk") {
494
- // const message = "Tool not pre-approved. Denied by dontAsk mode.";
495
- // await emitToolDenial(context, message);
496
- // return { behavior: "deny", message, interrupt: false };
497
- // }
493
+ // In plan mode, deny tools that aren't in the allowed set. The agent must
494
+ // write its plan to ~/.claude/plans/ and call ExitPlanMode before it can
495
+ // use write or bash tools. Without this guard, cloud runs auto-approve
496
+ // restricted tools and the agent skips planning entirely.
497
+ if (session.permissionMode === "plan") {
498
+ const message =
499
+ "This tool is not available in plan mode. Write your plan " +
500
+ `to a file in ${getClaudePlansDir()} and call ExitPlanMode when ready.`;
501
+ await emitToolDenial(context, message);
502
+ return { behavior: "deny", message, interrupt: false };
503
+ }
498
504
 
499
505
  return handleDefaultPermissionFlow(context);
500
506
  }
@@ -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
+ }
@@ -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();