@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/dist/agent.js +122 -33
- package/dist/agent.js.map +1 -1
- package/dist/posthog-api.d.ts +1 -0
- package/dist/posthog-api.js +11 -1
- package/dist/posthog-api.js.map +1 -1
- package/dist/server/agent-server.d.ts +7 -0
- package/dist/server/agent-server.js +317 -44
- package/dist/server/agent-server.js.map +1 -1
- package/dist/server/bin.cjs +317 -44
- package/dist/server/bin.cjs.map +1 -1
- package/package.json +3 -3
- package/src/adapters/acp-connection.ts +1 -1
- package/src/adapters/claude/claude-agent.ts +47 -29
- package/src/adapters/claude/conversion/acp-to-sdk.ts +6 -0
- package/src/adapters/claude/permissions/permission-handlers.ts +7 -1
- package/src/adapters/claude/types.ts +1 -0
- package/src/posthog-api.ts +15 -0
- package/src/server/agent-server.test.ts +109 -0
- package/src/server/agent-server.ts +261 -12
- package/src/server/question-relay.test.ts +343 -0
- package/src/session-log-writer.test.ts +19 -0
- package/src/session-log-writer.ts +72 -12
- package/src/test/mocks/msw-handlers.ts +25 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@posthog/agent",
|
|
3
|
-
"version": "2.1.
|
|
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
|
-
"@
|
|
75
|
-
"@
|
|
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,
|
|
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
|
-
|
|
254
|
+
this.logger.info("Session query initialized, awaiting resumption", {
|
|
255
|
+
sessionId,
|
|
256
|
+
taskId,
|
|
257
|
+
taskRunId: meta?.taskRunId,
|
|
258
|
+
});
|
|
249
259
|
|
|
250
|
-
|
|
260
|
+
session.taskRunId = meta?.taskRunId;
|
|
251
261
|
|
|
252
|
-
//
|
|
262
|
+
// Check the resumed session is alive. For stale sessions this throws
|
|
253
263
|
// (e.g. "No conversation found"), preventing a broken session.
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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:
|
|
284
|
+
message:
|
|
285
|
+
typeof customMessage === "string"
|
|
286
|
+
? customMessage
|
|
287
|
+
: "User cancelled the questions",
|
|
282
288
|
interrupt: true,
|
|
283
289
|
};
|
|
284
290
|
}
|
package/src/posthog-api.ts
CHANGED
|
@@ -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
|
});
|