@posthog/agent 2.1.137 → 2.1.147

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.
@@ -0,0 +1,581 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import * as fs from "node:fs/promises";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ import type { ContentBlock } from "@agentclientprotocol/sdk";
6
+ import type { PostHogAPIClient } from "../../../posthog-api.js";
7
+ import type { StoredEntry } from "../../../types.js";
8
+
9
+ interface ConversationTurn {
10
+ role: "user" | "assistant";
11
+ content: ContentBlock[];
12
+ toolCalls?: ToolCallInfo[];
13
+ }
14
+
15
+ interface ToolCallInfo {
16
+ toolCallId: string;
17
+ toolName: string;
18
+ input: unknown;
19
+ result?: unknown;
20
+ }
21
+
22
+ interface JsonlConfig {
23
+ sessionId: string;
24
+ cwd: string;
25
+ model?: string;
26
+ version?: string;
27
+ gitBranch?: string;
28
+ slug?: string;
29
+ permissionMode?: string;
30
+ }
31
+
32
+ interface ClaudeCodeMeta {
33
+ toolCallId?: string;
34
+ toolName?: string;
35
+ toolInput?: unknown;
36
+ toolResponse?: unknown;
37
+ }
38
+
39
+ interface SessionUpdate {
40
+ sessionUpdate: string;
41
+ content?: ContentBlock | ContentBlock[];
42
+ _meta?: { claudeCode?: ClaudeCodeMeta };
43
+ }
44
+
45
+ const MAX_PROJECT_KEY_LENGTH = 200;
46
+
47
+ function hashString(s: string): string {
48
+ let hash = 0;
49
+ for (let i = 0; i < s.length; i++) {
50
+ hash = (hash << 5) - hash + s.charCodeAt(i);
51
+ hash |= 0;
52
+ }
53
+ return Math.abs(hash).toString(36);
54
+ }
55
+
56
+ export function getSessionJsonlPath(sessionId: string, cwd: string): string {
57
+ const configDir =
58
+ process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), ".claude");
59
+ let projectKey = cwd.replace(/[^a-zA-Z0-9]/g, "-");
60
+ if (projectKey.length > MAX_PROJECT_KEY_LENGTH) {
61
+ projectKey = `${projectKey.slice(0, MAX_PROJECT_KEY_LENGTH)}-${hashString(cwd)}`;
62
+ }
63
+ return path.join(configDir, "projects", projectKey, `${sessionId}.jsonl`);
64
+ }
65
+
66
+ export function rebuildConversation(
67
+ entries: StoredEntry[],
68
+ ): ConversationTurn[] {
69
+ const turns: ConversationTurn[] = [];
70
+ let currentAssistantContent: ContentBlock[] = [];
71
+ let currentToolCalls: ToolCallInfo[] = [];
72
+
73
+ for (const entry of entries) {
74
+ const method = entry.notification?.method;
75
+ const params = entry.notification?.params as Record<string, unknown>;
76
+
77
+ if (method === "session/update" && params?.update) {
78
+ const update = params.update as SessionUpdate;
79
+
80
+ switch (update.sessionUpdate) {
81
+ case "user_message":
82
+ case "user_message_chunk": {
83
+ if (
84
+ currentAssistantContent.length > 0 ||
85
+ currentToolCalls.length > 0
86
+ ) {
87
+ turns.push({
88
+ role: "assistant",
89
+ content: currentAssistantContent,
90
+ toolCalls:
91
+ currentToolCalls.length > 0 ? currentToolCalls : undefined,
92
+ });
93
+ currentAssistantContent = [];
94
+ currentToolCalls = [];
95
+ }
96
+
97
+ const content = update.content;
98
+ const contentArray = Array.isArray(content)
99
+ ? content
100
+ : content
101
+ ? [content]
102
+ : [];
103
+
104
+ const lastTurn = turns[turns.length - 1];
105
+ if (lastTurn?.role === "user") {
106
+ lastTurn.content.push(...contentArray);
107
+ } else {
108
+ turns.push({ role: "user", content: contentArray });
109
+ }
110
+ break;
111
+ }
112
+
113
+ case "agent_message":
114
+ case "agent_message_chunk":
115
+ case "agent_thought_chunk": {
116
+ const content = update.content;
117
+ if (content && !Array.isArray(content)) {
118
+ if (
119
+ content.type === "text" &&
120
+ currentAssistantContent.length > 0 &&
121
+ currentAssistantContent[currentAssistantContent.length - 1]
122
+ .type === "text"
123
+ ) {
124
+ const lastBlock = currentAssistantContent[
125
+ currentAssistantContent.length - 1
126
+ ] as { type: "text"; text: string };
127
+ lastBlock.text += (
128
+ content as { type: "text"; text: string }
129
+ ).text;
130
+ } else {
131
+ currentAssistantContent.push(content);
132
+ }
133
+ }
134
+ break;
135
+ }
136
+
137
+ case "tool_call":
138
+ case "tool_call_update": {
139
+ const meta = update._meta?.claudeCode;
140
+ if (meta) {
141
+ const { toolCallId, toolName, toolInput, toolResponse } = meta;
142
+
143
+ if (toolCallId && toolName) {
144
+ let toolCall = currentToolCalls.find(
145
+ (tc) => tc.toolCallId === toolCallId,
146
+ );
147
+ if (!toolCall) {
148
+ toolCall = { toolCallId, toolName, input: toolInput };
149
+ currentToolCalls.push(toolCall);
150
+ }
151
+ if (toolResponse !== undefined) {
152
+ toolCall.result = toolResponse;
153
+ }
154
+ }
155
+ }
156
+ break;
157
+ }
158
+
159
+ case "tool_result": {
160
+ const meta = update._meta?.claudeCode;
161
+ if (meta) {
162
+ const { toolCallId, toolResponse } = meta;
163
+ if (toolCallId) {
164
+ const toolCall = currentToolCalls.find(
165
+ (tc) => tc.toolCallId === toolCallId,
166
+ );
167
+ if (toolCall && toolResponse !== undefined) {
168
+ toolCall.result = toolResponse;
169
+ }
170
+ }
171
+ }
172
+ break;
173
+ }
174
+ }
175
+ }
176
+ }
177
+
178
+ if (currentAssistantContent.length > 0 || currentToolCalls.length > 0) {
179
+ turns.push({
180
+ role: "assistant",
181
+ content: currentAssistantContent,
182
+ toolCalls: currentToolCalls.length > 0 ? currentToolCalls : undefined,
183
+ });
184
+ }
185
+
186
+ return turns;
187
+ }
188
+
189
+ const CHARS_PER_TOKEN = 4;
190
+ const DEFAULT_MAX_TOKENS = 150_000;
191
+
192
+ function estimateTurnTokens(turn: ConversationTurn): number {
193
+ let chars = 0;
194
+ for (const block of turn.content) {
195
+ if ("text" in block && typeof block.text === "string") {
196
+ chars += block.text.length;
197
+ }
198
+ }
199
+ if (turn.toolCalls) {
200
+ for (const tc of turn.toolCalls) {
201
+ chars += JSON.stringify(tc.input ?? "").length;
202
+ if (tc.result !== undefined) {
203
+ chars +=
204
+ typeof tc.result === "string"
205
+ ? tc.result.length
206
+ : JSON.stringify(tc.result).length;
207
+ }
208
+ }
209
+ }
210
+ return Math.ceil(chars / CHARS_PER_TOKEN);
211
+ }
212
+
213
+ export function selectRecentTurns(
214
+ turns: ConversationTurn[],
215
+ maxTokens = DEFAULT_MAX_TOKENS,
216
+ ): ConversationTurn[] {
217
+ let budget = maxTokens;
218
+ let startIndex = turns.length;
219
+
220
+ for (let i = turns.length - 1; i >= 0; i--) {
221
+ const cost = estimateTurnTokens(turns[i]);
222
+ if (cost > budget) break;
223
+ budget -= cost;
224
+ startIndex = i;
225
+ }
226
+
227
+ // Ensure we start on a user turn so the conversation is well-formed
228
+ while (startIndex < turns.length && turns[startIndex].role !== "user") {
229
+ startIndex++;
230
+ }
231
+
232
+ return turns.slice(startIndex);
233
+ }
234
+
235
+ const BASE62 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
236
+
237
+ function generateMessageId(): string {
238
+ const bytes = new Uint8Array(24);
239
+ crypto.getRandomValues(bytes);
240
+ let id = "msg_01";
241
+ for (const b of bytes) {
242
+ id += BASE62[b % 62];
243
+ }
244
+ return id;
245
+ }
246
+
247
+ const ADJECTIVES = [
248
+ "bright",
249
+ "calm",
250
+ "daring",
251
+ "eager",
252
+ "fair",
253
+ "gentle",
254
+ "happy",
255
+ "keen",
256
+ "lively",
257
+ "merry",
258
+ "noble",
259
+ "polite",
260
+ "quick",
261
+ "sharp",
262
+ "warm",
263
+ "witty",
264
+ ];
265
+ const VERBS = [
266
+ "blazing",
267
+ "crafting",
268
+ "dashing",
269
+ "flowing",
270
+ "gliding",
271
+ "humming",
272
+ "jumping",
273
+ "linking",
274
+ "melting",
275
+ "nesting",
276
+ "pacing",
277
+ "roaming",
278
+ "sailing",
279
+ "turning",
280
+ "waving",
281
+ "zoning",
282
+ ];
283
+ const NOUNS = [
284
+ "aurora",
285
+ "breeze",
286
+ "cedar",
287
+ "delta",
288
+ "ember",
289
+ "frost",
290
+ "grove",
291
+ "haven",
292
+ "inlet",
293
+ "jewel",
294
+ "knoll",
295
+ "lotus",
296
+ "maple",
297
+ "nexus",
298
+ "oasis",
299
+ "prism",
300
+ ];
301
+
302
+ function generateSlug(): string {
303
+ const pick = (arr: string[]) => arr[Math.floor(Math.random() * arr.length)];
304
+ return `${pick(ADJECTIVES)}-${pick(VERBS)}-${pick(NOUNS)}`;
305
+ }
306
+
307
+ export function conversationTurnsToJsonlEntries(
308
+ turns: ConversationTurn[],
309
+ config: JsonlConfig,
310
+ ): string[] {
311
+ const lines: string[] = [];
312
+ let parentUuid: string | null = null;
313
+ const model = config.model ?? "claude-opus-4-6";
314
+ const version = config.version ?? "2.1.63";
315
+ const gitBranch = config.gitBranch ?? "";
316
+ const slug = config.slug ?? generateSlug();
317
+ const permissionMode = config.permissionMode ?? "default";
318
+ const baseTime = Date.now() - turns.length * 3000;
319
+ let turnIndex = 0;
320
+
321
+ for (const turn of turns) {
322
+ const timestamp = new Date(baseTime + turnIndex * 3000).toISOString();
323
+ turnIndex++;
324
+ if (turn.role === "user") {
325
+ lines.push(
326
+ JSON.stringify({
327
+ type: "queue-operation",
328
+ operation: "enqueue",
329
+ timestamp,
330
+ sessionId: config.sessionId,
331
+ }),
332
+ );
333
+ lines.push(
334
+ JSON.stringify({
335
+ type: "queue-operation",
336
+ operation: "dequeue",
337
+ timestamp,
338
+ sessionId: config.sessionId,
339
+ }),
340
+ );
341
+
342
+ const uuid = randomUUID();
343
+ const textParts = turn.content
344
+ .filter(
345
+ (block) =>
346
+ "text" in block && typeof block.text === "string" && block.text,
347
+ )
348
+ .map((block) => (block as { text: string }).text);
349
+
350
+ const userText = textParts.length > 0 ? textParts.join("") : " ";
351
+
352
+ lines.push(
353
+ JSON.stringify({
354
+ parentUuid,
355
+ isSidechain: false,
356
+ userType: "external",
357
+ cwd: config.cwd,
358
+ sessionId: config.sessionId,
359
+ version,
360
+ gitBranch,
361
+ slug,
362
+ type: "user",
363
+ message: {
364
+ role: "user",
365
+ content: [{ type: "text", text: userText }],
366
+ },
367
+ uuid,
368
+ timestamp,
369
+ permissionMode,
370
+ }),
371
+ );
372
+ parentUuid = uuid;
373
+ } else {
374
+ const allBlocks: unknown[] = [];
375
+
376
+ for (const block of turn.content) {
377
+ const blockType = (block as { type: string }).type;
378
+ if (blockType === "thinking" || blockType === "text") {
379
+ allBlocks.push(block);
380
+ }
381
+ }
382
+
383
+ if (turn.toolCalls) {
384
+ for (const tc of turn.toolCalls) {
385
+ allBlocks.push({
386
+ type: "tool_use",
387
+ id: tc.toolCallId,
388
+ name: tc.toolName,
389
+ input: tc.input,
390
+ });
391
+ }
392
+ }
393
+
394
+ const msgId = generateMessageId();
395
+ const hasToolUse = allBlocks.some(
396
+ (b) => (b as { type: string }).type === "tool_use",
397
+ );
398
+ const lastStopReason = hasToolUse ? "tool_use" : "end_turn";
399
+
400
+ for (let i = 0; i < allBlocks.length; i++) {
401
+ const block = allBlocks[i];
402
+ const isLast = i === allBlocks.length - 1;
403
+ const uuid = randomUUID();
404
+
405
+ lines.push(
406
+ JSON.stringify({
407
+ parentUuid,
408
+ isSidechain: false,
409
+ userType: "external",
410
+ cwd: config.cwd,
411
+ sessionId: config.sessionId,
412
+ version,
413
+ gitBranch,
414
+ slug,
415
+ type: "assistant",
416
+ message: {
417
+ model,
418
+ id: msgId,
419
+ type: "message",
420
+ role: "assistant",
421
+ content: [block],
422
+ stop_reason: isLast ? lastStopReason : null,
423
+ stop_sequence: null,
424
+ usage: {
425
+ input_tokens: 0,
426
+ cache_creation_input_tokens: 0,
427
+ cache_read_input_tokens: 0,
428
+ output_tokens: 0,
429
+ },
430
+ },
431
+ uuid,
432
+ timestamp,
433
+ }),
434
+ );
435
+ parentUuid = uuid;
436
+ }
437
+
438
+ if (turn.toolCalls) {
439
+ for (const tc of turn.toolCalls) {
440
+ if (tc.result === undefined) continue;
441
+
442
+ const uuid = randomUUID();
443
+ const resultText =
444
+ typeof tc.result === "string"
445
+ ? tc.result
446
+ : JSON.stringify(tc.result);
447
+
448
+ lines.push(
449
+ JSON.stringify({
450
+ parentUuid,
451
+ isSidechain: false,
452
+ userType: "external",
453
+ cwd: config.cwd,
454
+ sessionId: config.sessionId,
455
+ version,
456
+ gitBranch,
457
+ slug,
458
+ type: "user",
459
+ message: {
460
+ role: "user",
461
+ content: [
462
+ {
463
+ type: "tool_result",
464
+ tool_use_id: tc.toolCallId,
465
+ content: resultText,
466
+ },
467
+ ],
468
+ },
469
+ uuid,
470
+ timestamp,
471
+ }),
472
+ );
473
+ parentUuid = uuid;
474
+ }
475
+ }
476
+ }
477
+ }
478
+
479
+ return lines;
480
+ }
481
+
482
+ interface HydrationLog {
483
+ info: (msg: string, data?: unknown) => void;
484
+ warn: (msg: string, data?: unknown) => void;
485
+ }
486
+
487
+ export async function hydrateSessionJsonl(params: {
488
+ sessionId: string;
489
+ cwd: string;
490
+ taskId: string;
491
+ runId: string;
492
+ model?: string;
493
+ gitBranch?: string;
494
+ permissionMode?: string;
495
+ posthogAPI: PostHogAPIClient;
496
+ log: HydrationLog;
497
+ }): Promise<void> {
498
+ const { posthogAPI, log } = params;
499
+
500
+ try {
501
+ const jsonlPath = getSessionJsonlPath(params.sessionId, params.cwd);
502
+ try {
503
+ await fs.access(jsonlPath);
504
+ log.info("Local JSONL exists, skipping S3 hydration", {
505
+ sessionId: params.sessionId,
506
+ });
507
+ return;
508
+ } catch {
509
+ // File doesn't exist, proceed with hydration
510
+ }
511
+
512
+ const taskRun = await posthogAPI.getTaskRun(params.taskId, params.runId);
513
+ if (!taskRun.log_url) {
514
+ log.info("No log URL, skipping JSONL hydration");
515
+ return;
516
+ }
517
+
518
+ const entries = await posthogAPI.fetchTaskRunLogs(taskRun);
519
+ if (entries.length === 0) {
520
+ log.info("No S3 log entries, skipping JSONL hydration");
521
+ return;
522
+ }
523
+
524
+ const entryCounts: Record<string, number> = {};
525
+ for (const entry of entries) {
526
+ const method = entry.notification?.method ?? "unknown";
527
+ const entryParams = entry.notification?.params as
528
+ | Record<string, unknown>
529
+ | undefined;
530
+ const update = entryParams?.update as
531
+ | { sessionUpdate?: string }
532
+ | undefined;
533
+ const key = update?.sessionUpdate
534
+ ? `${method}:${update.sessionUpdate}`
535
+ : method;
536
+ entryCounts[key] = (entryCounts[key] ?? 0) + 1;
537
+ }
538
+ log.info("S3 log entry breakdown", {
539
+ totalEntries: entries.length,
540
+ types: entryCounts,
541
+ });
542
+
543
+ const allTurns = rebuildConversation(entries);
544
+ if (allTurns.length === 0) {
545
+ log.info("No conversation in S3 logs, skipping JSONL hydration");
546
+ return;
547
+ }
548
+
549
+ const conversation = selectRecentTurns(allTurns);
550
+ log.info("Selected recent turns for hydration", {
551
+ totalTurns: allTurns.length,
552
+ selectedTurns: conversation.length,
553
+ turnRoles: conversation.map((t) => t.role),
554
+ });
555
+
556
+ const jsonlLines = conversationTurnsToJsonlEntries(conversation, {
557
+ sessionId: params.sessionId,
558
+ cwd: params.cwd,
559
+ model: params.model,
560
+ gitBranch: params.gitBranch,
561
+ permissionMode: params.permissionMode,
562
+ });
563
+
564
+ await fs.mkdir(path.dirname(jsonlPath), { recursive: true });
565
+
566
+ const tmpPath = `${jsonlPath}.tmp.${Date.now()}`;
567
+ await fs.writeFile(tmpPath, `${jsonlLines.join("\n")}\n`);
568
+ await fs.rename(tmpPath, jsonlPath);
569
+
570
+ log.info("Hydrated session JSONL from S3", {
571
+ sessionId: params.sessionId,
572
+ turns: conversation.length,
573
+ lines: jsonlLines.length,
574
+ });
575
+ } catch (err) {
576
+ log.warn("Failed to hydrate session JSONL, continuing", {
577
+ sessionId: params.sessionId,
578
+ error: err instanceof Error ? err.message : String(err),
579
+ });
580
+ }
581
+ }
@@ -101,7 +101,7 @@ export function spawnCodexProcess(options: CodexProcessOptions): CodexProcess {
101
101
  });
102
102
 
103
103
  child.stderr?.on("data", (data: Buffer) => {
104
- logger.error("codex-acp stderr:", data.toString());
104
+ logger.debug("codex-acp stderr:", data.toString());
105
105
  });
106
106
 
107
107
  child.on("error", (err) => {
package/src/agent.ts CHANGED
@@ -157,6 +157,10 @@ export class Agent {
157
157
  });
158
158
  }
159
159
 
160
+ getPosthogAPI(): PostHogAPIClient | undefined {
161
+ return this.posthogAPI;
162
+ }
163
+
160
164
  async flushAllLogs(): Promise<void> {
161
165
  await this.sessionLogWriter?.flushAll();
162
166
  }
@@ -364,14 +364,15 @@ describe("AgentServer HTTP Mode", () => {
364
364
  expect(prompt).toContain("Do NOT create a new branch");
365
365
  expect(prompt).toContain("https://github.com/org/repo/pull/1");
366
366
  expect(prompt).toContain("gh pr checkout");
367
- expect(prompt).not.toContain("Create a pull request");
367
+ expect(prompt).not.toContain("Create a draft pull request");
368
368
  });
369
369
 
370
370
  it("returns default prompt when no prUrl", () => {
371
371
  const s = createServer();
372
372
  const prompt = (s as unknown as TestableServer).buildCloudSystemPrompt();
373
373
  expect(prompt).toContain("Create a new branch");
374
- expect(prompt).toContain("Create a pull request");
374
+ expect(prompt).toContain("Create a draft pull request");
375
+ expect(prompt).toContain("gh pr create --draft");
375
376
  });
376
377
 
377
378
  it("returns default prompt when prUrl is null", () => {
@@ -380,7 +381,8 @@ describe("AgentServer HTTP Mode", () => {
380
381
  null,
381
382
  );
382
383
  expect(prompt).toContain("Create a new branch");
383
- expect(prompt).toContain("Create a pull request");
384
+ expect(prompt).toContain("Create a draft pull request");
385
+ expect(prompt).toContain("gh pr create --draft");
384
386
  });
385
387
  });
386
388
  });
@@ -706,10 +706,10 @@ After completing the requested changes:
706
706
  1. Create a new branch with a descriptive name based on the work done
707
707
  2. Stage and commit all changes with a clear commit message
708
708
  3. Push the branch to origin
709
- 4. Create a pull request using \`gh pr create\` with a descriptive title and body
709
+ 4. Create a draft pull request using \`gh pr create --draft\` with a descriptive title and body
710
710
 
711
711
  Important:
712
- - Always create the PR. Do not ask for confirmation.
712
+ - Always create the PR as a draft. Do not ask for confirmation.
713
713
  - Do NOT add "Co-Authored-By" trailers to commit messages.
714
714
  - Do NOT add "Generated with [Claude Code]" or similar attribution lines to PR descriptions.
715
715
  `;