@posthog/agent 2.1.115 → 2.1.120

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.1.115",
3
+ "version": "2.1.120",
4
4
  "repository": "https://github.com/PostHog/twig",
5
5
  "description": "TypeScript agent framework wrapping Claude Agent SDK with Git-based task execution for PostHog",
6
6
  "exports": {
@@ -71,8 +71,8 @@
71
71
  "tsx": "^4.20.6",
72
72
  "typescript": "^5.5.0",
73
73
  "vitest": "^2.1.8",
74
- "@twig/git": "1.0.0",
75
- "@posthog/shared": "1.0.0"
74
+ "@posthog/shared": "1.0.0",
75
+ "@twig/git": "1.0.0"
76
76
  },
77
77
  "dependencies": {
78
78
  "@agentclientprotocol/sdk": "^0.14.0",
@@ -195,7 +195,7 @@ function createClaudeConnection(config: AcpConnectionConfig): AcpConnection {
195
195
 
196
196
  let agent: ClaudeAcpAgent | null = null;
197
197
  const agentConnection = new AgentSideConnection((client) => {
198
- agent = new ClaudeAcpAgent(client, logWriter, config.processCallbacks);
198
+ agent = new ClaudeAcpAgent(client, config.processCallbacks);
199
199
  logger.info(`Created ${agent.adapterName} agent`);
200
200
  return agent;
201
201
  }, agentStream);
@@ -31,8 +31,6 @@ import {
31
31
  } from "@anthropic-ai/claude-agent-sdk";
32
32
  import { v7 as uuidv7 } from "uuid";
33
33
  import packageJson from "../../../package.json" with { type: "json" };
34
- import type { SessionContext } from "../../otel-log-writer.js";
35
- import type { SessionLogWriter } from "../../session-log-writer.js";
36
34
  import { unreachable, withTimeout } from "../../utils/common.js";
37
35
  import { Logger } from "../../utils/logger.js";
38
36
  import { Pushable } from "../../utils/streams.js";
@@ -79,17 +77,11 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
79
77
  toolUseCache: ToolUseCache;
80
78
  backgroundTerminals: { [key: string]: BackgroundTerminal } = {};
81
79
  clientCapabilities?: ClientCapabilities;
82
- private logWriter?: SessionLogWriter;
83
80
  private options?: ClaudeAcpAgentOptions;
84
81
  private lastSentConfigOptions?: SessionConfigOption[];
85
82
 
86
- constructor(
87
- client: AgentSideConnection,
88
- logWriter?: SessionLogWriter,
89
- options?: ClaudeAcpAgentOptions,
90
- ) {
83
+ constructor(client: AgentSideConnection, options?: ClaudeAcpAgentOptions) {
91
84
  super(client);
92
- this.logWriter = logWriter;
93
85
  this.options = options;
94
86
  this.toolUseCache = {};
95
87
  this.logger = new Logger({ debug: true, prefix: "[ClaudeAcpAgent]" });
@@ -139,7 +131,14 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
139
131
  this.checkAuthStatus();
140
132
 
141
133
  const meta = params._meta as NewSessionMeta | undefined;
134
+ const taskId = meta?.persistence?.taskId;
142
135
  const sessionId = uuidv7();
136
+ this.logger.info("Creating new session", {
137
+ sessionId,
138
+ taskId,
139
+ taskRunId: meta?.taskRunId,
140
+ cwd: params.cwd,
141
+ });
143
142
  const permissionMode: TwigExecutionMode =
144
143
  meta?.permissionMode &&
145
144
  TWIG_EXECUTION_MODES.includes(meta.permissionMode as TwigExecutionMode)
@@ -177,7 +176,6 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
177
176
  options.abortController as AbortController,
178
177
  );
179
178
  session.taskRunId = meta?.taskRunId;
180
- this.registerPersistence(sessionId, meta as Record<string, unknown>);
181
179
 
182
180
  if (meta?.taskRunId) {
183
181
  await this.client.extNotification("_posthog/sdk_session", {
@@ -218,6 +216,7 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
218
216
  params: LoadSessionRequest,
219
217
  ): Promise<LoadSessionResponse> {
220
218
  const meta = params._meta as NewSessionMeta | undefined;
219
+ const taskId = meta?.persistence?.taskId;
221
220
  const sessionId = meta?.sessionId;
222
221
  if (!sessionId) {
223
222
  throw new Error("Cannot resume session without sessionId");
@@ -226,6 +225,13 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
226
225
  return {};
227
226
  }
228
227
 
228
+ this.logger.info("Resuming session", {
229
+ sessionId,
230
+ taskId,
231
+ taskRunId: meta?.taskRunId,
232
+ cwd: params.cwd,
233
+ });
234
+
229
235
  const mcpServers = parseMcpServers(params);
230
236
 
231
237
  const permissionMode: TwigExecutionMode =
@@ -245,20 +251,42 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
245
251
  additionalDirectories: meta?.claudeCode?.options?.additionalDirectories,
246
252
  });
247
253
 
248
- session.taskRunId = meta?.taskRunId;
254
+ this.logger.info("Session query initialized, awaiting resumption", {
255
+ sessionId,
256
+ taskId,
257
+ taskRunId: meta?.taskRunId,
258
+ });
249
259
 
250
- this.registerPersistence(sessionId, meta as Record<string, unknown>);
260
+ session.taskRunId = meta?.taskRunId;
251
261
 
252
- // Validate the resumed session is alive. For stale sessions this throws
262
+ // Check the resumed session is alive. For stale sessions this throws
253
263
  // (e.g. "No conversation found"), preventing a broken session.
254
- const validation = await withTimeout(
255
- q.initializationResult(),
256
- SESSION_VALIDATION_TIMEOUT_MS,
257
- );
258
- if (validation.result === "timeout") {
259
- throw new Error("Session validation timed out");
264
+ try {
265
+ const result = await withTimeout(
266
+ q.initializationResult(),
267
+ SESSION_VALIDATION_TIMEOUT_MS,
268
+ );
269
+ if (result.result === "timeout") {
270
+ throw new Error(
271
+ `Session resumption timed out for sessionId=${sessionId}`,
272
+ );
273
+ }
274
+ } catch (err) {
275
+ this.logger.error("Session resumption failed", {
276
+ sessionId,
277
+ taskId,
278
+ taskRunId: meta?.taskRunId,
279
+ error: err instanceof Error ? err.message : String(err),
280
+ });
281
+ throw err;
260
282
  }
261
283
 
284
+ this.logger.info("Session resumed successfully", {
285
+ sessionId,
286
+ taskId,
287
+ taskRunId: meta?.taskRunId,
288
+ });
289
+
262
290
  // Deferred: slash commands + MCP metadata (not needed to return configOptions)
263
291
  this.deferBackgroundFetches(q, sessionId);
264
292
 
@@ -527,16 +555,6 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
527
555
  });
528
556
  }
529
557
 
530
- private registerPersistence(
531
- sessionId: string,
532
- meta: Record<string, unknown> | undefined,
533
- ) {
534
- const persistence = meta?.persistence as SessionContext | undefined;
535
- if (persistence && this.logWriter) {
536
- this.logWriter.register(sessionId, persistence);
537
- }
538
- }
539
-
540
558
  private sendAvailableCommandsUpdate(
541
559
  sessionId: string,
542
560
  availableCommands: AvailableCommand[],
@@ -87,6 +87,12 @@ export function promptToClaude(prompt: PromptRequest): SDKUserMessage {
87
87
  const content: ContentBlockParam[] = [];
88
88
  const context: ContentBlockParam[] = [];
89
89
 
90
+ const prContext = (prompt._meta as Record<string, unknown> | undefined)
91
+ ?.prContext;
92
+ if (typeof prContext === "string") {
93
+ content.push(sdkText(prContext));
94
+ }
95
+
90
96
  for (const chunk of prompt.prompt) {
91
97
  processPromptChunk(chunk, content, context);
92
98
  }
@@ -276,9 +276,15 @@ async function handleAskUserQuestionTool(
276
276
  });
277
277
 
278
278
  if (response.outcome?.outcome !== "selected") {
279
+ const customMessage = (
280
+ response._meta as Record<string, unknown> | undefined
281
+ )?.message;
279
282
  return {
280
283
  behavior: "deny",
281
- message: "User cancelled the questions",
284
+ message:
285
+ typeof customMessage === "string"
286
+ ? customMessage
287
+ : "User cancelled the questions",
282
288
  interrupt: true,
283
289
  };
284
290
  }
@@ -56,6 +56,7 @@ export type NewSessionMeta = {
56
56
  systemPrompt?: unknown;
57
57
  sessionId?: string;
58
58
  permissionMode?: string;
59
+ persistence?: { taskId?: string; runId?: string; logUrl?: string };
59
60
  claudeCode?: {
60
61
  options?: Options;
61
62
  };
@@ -137,6 +137,21 @@ export class PostHogAPIClient {
137
137
  );
138
138
  }
139
139
 
140
+ async relayMessage(
141
+ taskId: string,
142
+ runId: string,
143
+ text: string,
144
+ ): Promise<void> {
145
+ const teamId = this.getTeamId();
146
+ await this.apiRequest<{ status: string }>(
147
+ `/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/relay_message/`,
148
+ {
149
+ method: "POST",
150
+ body: JSON.stringify({ text }),
151
+ },
152
+ );
153
+ }
154
+
140
155
  async uploadTaskArtifacts(
141
156
  taskId: string,
142
157
  runId: string,
@@ -3,6 +3,7 @@ import { type SetupServerApi, setupServer } from "msw/node";
3
3
  import { afterEach, beforeEach, describe, expect, it } from "vitest";
4
4
  import { createTestRepo, type TestRepo } from "../test/fixtures/api.js";
5
5
  import { createPostHogHandlers } from "../test/mocks/msw-handlers.js";
6
+ import type { TaskRun } from "../types.js";
6
7
  import { AgentServer } from "./agent-server.js";
7
8
  import { type JwtPayload, SANDBOX_CONNECTION_AUDIENCE } from "./jwt.js";
8
9
 
@@ -217,4 +218,112 @@ describe("AgentServer HTTP Mode", () => {
217
218
  expect(body.error).toBe("Not found");
218
219
  });
219
220
  });
221
+
222
+ describe("getInitialPromptOverride", () => {
223
+ it("returns override string from run state", () => {
224
+ const s = createServer();
225
+ const run = {
226
+ state: { initial_prompt_override: "do something else" },
227
+ } as unknown as TaskRun;
228
+ const result = (s as any).getInitialPromptOverride(run);
229
+ expect(result).toBe("do something else");
230
+ });
231
+
232
+ it("returns null when override is absent", () => {
233
+ const s = createServer();
234
+ const run = { state: {} } as unknown as TaskRun;
235
+ const result = (s as any).getInitialPromptOverride(run);
236
+ expect(result).toBeNull();
237
+ });
238
+
239
+ it("returns null for whitespace-only override", () => {
240
+ const s = createServer();
241
+ const run = {
242
+ state: { initial_prompt_override: " " },
243
+ } as unknown as TaskRun;
244
+ const result = (s as any).getInitialPromptOverride(run);
245
+ expect(result).toBeNull();
246
+ });
247
+
248
+ it("returns null for non-string override", () => {
249
+ const s = createServer();
250
+ const run = {
251
+ state: { initial_prompt_override: 42 },
252
+ } as unknown as TaskRun;
253
+ const result = (s as any).getInitialPromptOverride(run);
254
+ expect(result).toBeNull();
255
+ });
256
+ });
257
+
258
+ describe("detectedPrUrl tracking", () => {
259
+ it("stores PR URL when detectAndAttachPrUrl finds a match", () => {
260
+ const s = createServer();
261
+ const payload = {
262
+ task_id: "test-task-id",
263
+ run_id: "test-run-id",
264
+ };
265
+ const update = {
266
+ _meta: {
267
+ claudeCode: {
268
+ toolName: "Bash",
269
+ toolResponse: {
270
+ stdout:
271
+ "https://github.com/PostHog/posthog/pull/42\nCreating pull request...",
272
+ },
273
+ },
274
+ },
275
+ };
276
+
277
+ (s as any).detectAndAttachPrUrl(payload, update);
278
+ expect((s as any).detectedPrUrl).toBe(
279
+ "https://github.com/PostHog/posthog/pull/42",
280
+ );
281
+ });
282
+
283
+ it("does not set detectedPrUrl when no PR URL is found", () => {
284
+ const s = createServer();
285
+ const payload = {
286
+ task_id: "test-task-id",
287
+ run_id: "test-run-id",
288
+ };
289
+ const update = {
290
+ _meta: {
291
+ claudeCode: {
292
+ toolName: "Bash",
293
+ toolResponse: { stdout: "just some output" },
294
+ },
295
+ },
296
+ };
297
+
298
+ (s as any).detectAndAttachPrUrl(payload, update);
299
+ expect((s as any).detectedPrUrl).toBeNull();
300
+ });
301
+ });
302
+
303
+ describe("buildCloudSystemPrompt", () => {
304
+ it("returns PR-aware prompt when prUrl is provided", () => {
305
+ const s = createServer();
306
+ const prompt = (s as any).buildCloudSystemPrompt(
307
+ "https://github.com/org/repo/pull/1",
308
+ );
309
+ expect(prompt).toContain("Do NOT create a new branch");
310
+ expect(prompt).toContain("https://github.com/org/repo/pull/1");
311
+ expect(prompt).toContain("gh pr checkout");
312
+ expect(prompt).not.toContain("Create a pull request");
313
+ });
314
+
315
+ it("returns default prompt when no prUrl", () => {
316
+ const s = createServer();
317
+ const prompt = (s as any).buildCloudSystemPrompt();
318
+ expect(prompt).toContain("Create a new branch");
319
+ expect(prompt).toContain("Create a pull request");
320
+ });
321
+
322
+ it("returns default prompt when prUrl is null", () => {
323
+ const s = createServer();
324
+ const prompt = (s as any).buildCloudSystemPrompt(null);
325
+ expect(prompt).toContain("Create a new branch");
326
+ expect(prompt).toContain("Create a pull request");
327
+ });
328
+ });
220
329
  });