@posthog/agent 1.20.0 → 1.21.0

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.
Files changed (52) hide show
  1. package/dist/claude-cli/cli.js +2186 -1872
  2. package/dist/src/adapters/claude/claude-adapter.d.ts +1 -1
  3. package/dist/src/adapters/claude/claude-adapter.d.ts.map +1 -1
  4. package/dist/src/adapters/claude/claude-adapter.js +141 -133
  5. package/dist/src/adapters/claude/claude-adapter.js.map +1 -1
  6. package/dist/src/adapters/types.d.ts +3 -3
  7. package/dist/src/adapters/types.d.ts.map +1 -1
  8. package/dist/src/agent.d.ts +1 -1
  9. package/dist/src/agent.d.ts.map +1 -1
  10. package/dist/src/agent.js +9 -8
  11. package/dist/src/agent.js.map +1 -1
  12. package/dist/src/file-manager.d.ts +10 -0
  13. package/dist/src/file-manager.d.ts.map +1 -1
  14. package/dist/src/file-manager.js +49 -10
  15. package/dist/src/file-manager.js.map +1 -1
  16. package/dist/src/posthog-api.d.ts +2 -1
  17. package/dist/src/posthog-api.d.ts.map +1 -1
  18. package/dist/src/posthog-api.js +11 -0
  19. package/dist/src/posthog-api.js.map +1 -1
  20. package/dist/src/task-progress-reporter.d.ts +12 -4
  21. package/dist/src/task-progress-reporter.d.ts.map +1 -1
  22. package/dist/src/task-progress-reporter.js +271 -117
  23. package/dist/src/task-progress-reporter.js.map +1 -1
  24. package/dist/src/types.d.ts +17 -1
  25. package/dist/src/types.d.ts.map +1 -1
  26. package/dist/src/types.js.map +1 -1
  27. package/dist/src/workflow/config.d.ts.map +1 -1
  28. package/dist/src/workflow/config.js +11 -0
  29. package/dist/src/workflow/config.js.map +1 -1
  30. package/dist/src/workflow/steps/build.js +3 -3
  31. package/dist/src/workflow/steps/build.js.map +1 -1
  32. package/dist/src/workflow/steps/finalize.d.ts +3 -0
  33. package/dist/src/workflow/steps/finalize.d.ts.map +1 -0
  34. package/dist/src/workflow/steps/finalize.js +173 -0
  35. package/dist/src/workflow/steps/finalize.js.map +1 -0
  36. package/dist/src/workflow/steps/plan.js +3 -3
  37. package/dist/src/workflow/steps/plan.js.map +1 -1
  38. package/dist/src/workflow/steps/research.js +3 -3
  39. package/dist/src/workflow/steps/research.js.map +1 -1
  40. package/package.json +1 -1
  41. package/src/adapters/claude/claude-adapter.ts +56 -46
  42. package/src/adapters/types.ts +3 -3
  43. package/src/agent.ts +17 -8
  44. package/src/file-manager.ts +59 -6
  45. package/src/posthog-api.ts +33 -1
  46. package/src/task-progress-reporter.ts +299 -138
  47. package/src/types.ts +20 -1
  48. package/src/workflow/config.ts +11 -0
  49. package/src/workflow/steps/build.ts +3 -3
  50. package/src/workflow/steps/finalize.ts +207 -0
  51. package/src/workflow/steps/plan.ts +3 -3
  52. package/src/workflow/steps/research.ts +3 -3
@@ -1,6 +1,6 @@
1
1
  import type { Logger } from './utils/logger.js';
2
2
  import type { PostHogAPIClient, TaskRunUpdate } from './posthog-api.js';
3
- import type { AgentEvent, TaskRun } from './types.js';
3
+ import type { AgentEvent, TaskRun, LogEntry } from './types.js';
4
4
 
5
5
  interface ProgressMetadata {
6
6
  totalSteps?: number;
@@ -20,6 +20,14 @@ export class TaskProgressReporter {
20
20
  private outputLog: string[] = [];
21
21
  private totalSteps?: number;
22
22
  private lastLogEntry?: string;
23
+ private tokenBuffer: string = '';
24
+ private tokenCount: number = 0;
25
+ private tokenFlushTimer?: NodeJS.Timeout;
26
+ private readonly TOKEN_BATCH_SIZE = 100;
27
+ private readonly TOKEN_FLUSH_INTERVAL_MS = 1000;
28
+ private logWriteQueue: Promise<void> = Promise.resolve();
29
+ private readonly LOG_APPEND_MAX_RETRIES = 3;
30
+ private readonly LOG_APPEND_RETRY_BASE_DELAY_MS = 200;
23
31
 
24
32
  constructor(posthogAPI: PostHogAPIClient | undefined, logger: Logger) {
25
33
  this.posthogAPI = posthogAPI;
@@ -51,6 +59,11 @@ export class TaskProgressReporter {
51
59
  }
52
60
 
53
61
  async complete(): Promise<void> {
62
+ await this.flushTokens(); // Flush any remaining tokens before completion
63
+ if (this.tokenFlushTimer) {
64
+ clearTimeout(this.tokenFlushTimer);
65
+ this.tokenFlushTimer = undefined;
66
+ }
54
67
  await this.update({ status: 'completed' }, 'Task execution completed');
55
68
  }
56
69
 
@@ -63,71 +76,319 @@ export class TaskProgressReporter {
63
76
  await this.update({}, line);
64
77
  }
65
78
 
79
+ private async flushTokens(): Promise<void> {
80
+ if (!this.tokenBuffer || this.tokenCount === 0) {
81
+ return;
82
+ }
83
+
84
+ const buffer = this.tokenBuffer;
85
+ this.tokenBuffer = '';
86
+ this.tokenCount = 0;
87
+
88
+ await this.appendLogEntry({
89
+ type: 'token',
90
+ message: buffer,
91
+ });
92
+ }
93
+
94
+ private scheduleTokenFlush(): void {
95
+ if (this.tokenFlushTimer) {
96
+ return;
97
+ }
98
+
99
+ this.tokenFlushTimer = setTimeout(() => {
100
+ this.tokenFlushTimer = undefined;
101
+ this.flushTokens().catch((err) => {
102
+ this.logger.warn('Failed to flush tokens', { error: err });
103
+ });
104
+ }, this.TOKEN_FLUSH_INTERVAL_MS);
105
+ }
106
+
107
+ private appendLogEntry(entry: LogEntry): Promise<void> {
108
+ if (!this.posthogAPI || !this.runId || !this.taskId) {
109
+ return Promise.resolve();
110
+ }
111
+
112
+ const taskId = this.taskId;
113
+ const runId = this.runId;
114
+
115
+ this.logWriteQueue = this.logWriteQueue
116
+ .catch((error) => {
117
+ // Ensure previous failures don't block subsequent writes
118
+ this.logger.debug('Previous log append failed', {
119
+ taskId,
120
+ runId,
121
+ error: error instanceof Error ? error.message : String(error),
122
+ });
123
+ })
124
+ .then(() => this.writeLogEntry(taskId, runId, entry));
125
+
126
+ return this.logWriteQueue;
127
+ }
128
+
129
+ private async writeLogEntry(
130
+ taskId: string,
131
+ runId: string,
132
+ entry: LogEntry,
133
+ ): Promise<void> {
134
+ if (!this.posthogAPI) {
135
+ return;
136
+ }
137
+
138
+ for (let attempt = 1; attempt <= this.LOG_APPEND_MAX_RETRIES; attempt++) {
139
+ try {
140
+ await this.posthogAPI.appendTaskRunLog(taskId, runId, [entry]);
141
+ return;
142
+ } catch (error) {
143
+ this.logger.warn('Failed to append log entry', {
144
+ taskId,
145
+ runId,
146
+ attempt,
147
+ maxAttempts: this.LOG_APPEND_MAX_RETRIES,
148
+ error: (error as Error).message,
149
+ });
150
+
151
+ if (attempt === this.LOG_APPEND_MAX_RETRIES) {
152
+ return;
153
+ }
154
+
155
+ const delayMs =
156
+ this.LOG_APPEND_RETRY_BASE_DELAY_MS * Math.pow(2, attempt - 1);
157
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
158
+ }
159
+ }
160
+ }
161
+
66
162
  async recordEvent(event: AgentEvent): Promise<void> {
67
163
  if (!this.posthogAPI || !this.runId || !this.taskId) {
68
164
  return;
69
165
  }
70
166
 
71
167
  switch (event.type) {
72
- case 'token':
73
- case 'message_delta':
74
- case 'content_block_start':
75
- case 'content_block_stop':
76
- case 'compact_boundary':
77
- case 'message_start':
78
- case 'message_stop':
79
- case 'metric':
80
- case 'artifact':
81
- case 'raw_sdk_event':
82
- // Skip verbose streaming artifacts from persistence
168
+ case 'token': {
169
+ // Batch tokens for efficiency
170
+ this.tokenBuffer += event.content;
171
+ this.tokenCount++;
172
+
173
+ if (this.tokenCount >= this.TOKEN_BATCH_SIZE) {
174
+ await this.flushTokens();
175
+ if (this.tokenFlushTimer) {
176
+ clearTimeout(this.tokenFlushTimer);
177
+ this.tokenFlushTimer = undefined;
178
+ }
179
+ } else {
180
+ this.scheduleTokenFlush();
181
+ }
83
182
  return;
183
+ }
84
184
 
85
- case 'tool_call': {
86
- const logLine = this.formatToolCallEvent(event);
87
- if (logLine) {
88
- await this.appendLog(logLine);
89
- }
185
+ case 'content_block_start': {
186
+ await this.appendLogEntry({
187
+ type: 'content_block_start',
188
+ message: JSON.stringify({
189
+ index: event.index,
190
+ contentType: event.contentType,
191
+ toolName: event.toolName,
192
+ toolId: event.toolId,
193
+ ts: event.ts,
194
+ }),
195
+ });
90
196
  return;
91
197
  }
92
198
 
93
- case 'tool_result': {
94
- const logLine = this.formatToolResultEvent(event);
95
- if (logLine) {
96
- await this.appendLog(logLine);
97
- }
199
+ case 'content_block_stop': {
200
+ await this.appendLogEntry({
201
+ type: 'content_block_stop',
202
+ message: JSON.stringify({
203
+ index: event.index,
204
+ ts: event.ts,
205
+ }),
206
+ });
98
207
  return;
99
208
  }
100
209
 
101
- case 'status':
102
- // Status events are covered by dedicated progress updates
210
+ case 'message_start': {
211
+ await this.appendLogEntry({
212
+ type: 'message_start',
213
+ message: JSON.stringify({
214
+ messageId: event.messageId,
215
+ model: event.model,
216
+ ts: event.ts,
217
+ }),
218
+ });
103
219
  return;
220
+ }
104
221
 
105
- case 'error':
106
- await this.appendLog(`[error] ${event.message}`);
222
+ case 'message_delta': {
223
+ await this.appendLogEntry({
224
+ type: 'message_delta',
225
+ message: JSON.stringify({
226
+ stopReason: event.stopReason,
227
+ stopSequence: event.stopSequence,
228
+ usage: event.usage,
229
+ ts: event.ts,
230
+ }),
231
+ });
107
232
  return;
233
+ }
108
234
 
109
- case 'done': {
110
- const cost = event.totalCostUsd !== undefined ? ` cost=$${event.totalCostUsd.toFixed(2)}` : '';
111
- await this.appendLog(
112
- `[done] duration=${event.durationMs ?? 'unknown'}ms turns=${event.numTurns ?? 'unknown'}${cost}`
113
- );
235
+ case 'message_stop': {
236
+ await this.appendLogEntry({
237
+ type: 'message_stop',
238
+ message: JSON.stringify({ ts: event.ts }),
239
+ });
240
+ return;
241
+ }
242
+
243
+ case 'status': {
244
+ await this.appendLogEntry({
245
+ type: 'status',
246
+ message: JSON.stringify({
247
+ phase: event.phase,
248
+ kind: event.kind,
249
+ branch: event.branch,
250
+ prUrl: event.prUrl,
251
+ taskId: event.taskId,
252
+ messageId: event.messageId,
253
+ model: event.model,
254
+ ts: event.ts,
255
+ }),
256
+ });
257
+ return;
258
+ }
259
+
260
+ case 'artifact': {
261
+ await this.appendLogEntry({
262
+ type: 'artifact',
263
+ message: JSON.stringify({
264
+ kind: event.kind,
265
+ content: event.content,
266
+ ts: event.ts,
267
+ }),
268
+ });
269
+ return;
270
+ }
271
+
272
+ case 'init': {
273
+ await this.appendLogEntry({
274
+ type: 'init',
275
+ message: JSON.stringify({
276
+ model: event.model,
277
+ tools: event.tools,
278
+ permissionMode: event.permissionMode,
279
+ cwd: event.cwd,
280
+ apiKeySource: event.apiKeySource,
281
+ ts: event.ts,
282
+ }),
283
+ });
114
284
  return;
115
285
  }
116
286
 
117
- case 'init':
118
- // Omit verbose init messages from persisted log
287
+ case 'metric': {
288
+ await this.appendLogEntry({
289
+ type: 'metric',
290
+ message: JSON.stringify({
291
+ key: event.key,
292
+ value: event.value,
293
+ unit: event.unit,
294
+ ts: event.ts,
295
+ }),
296
+ });
119
297
  return;
298
+ }
299
+
300
+ case 'compact_boundary': {
301
+ await this.appendLogEntry({
302
+ type: 'compact_boundary',
303
+ message: JSON.stringify({
304
+ trigger: event.trigger,
305
+ preTokens: event.preTokens,
306
+ ts: event.ts,
307
+ }),
308
+ });
309
+ return;
310
+ }
311
+
312
+ case 'tool_call': {
313
+ await this.appendLogEntry({
314
+ type: 'tool_call',
315
+ message: JSON.stringify({
316
+ toolName: event.toolName,
317
+ callId: event.callId,
318
+ args: event.args,
319
+ parentToolUseId: event.parentToolUseId,
320
+ ts: event.ts,
321
+ }),
322
+ });
323
+ return;
324
+ }
325
+
326
+ case 'tool_result': {
327
+ await this.appendLogEntry({
328
+ type: 'tool_result',
329
+ message: JSON.stringify({
330
+ toolName: event.toolName,
331
+ callId: event.callId,
332
+ result: event.result,
333
+ isError: event.isError,
334
+ parentToolUseId: event.parentToolUseId,
335
+ ts: event.ts,
336
+ }),
337
+ });
338
+ return;
339
+ }
340
+
341
+ case 'error': {
342
+ await this.appendLogEntry({
343
+ type: 'error',
344
+ message: JSON.stringify({
345
+ message: event.message,
346
+ errorType: event.errorType,
347
+ context: event.context,
348
+ ts: event.ts,
349
+ }),
350
+ });
351
+ return;
352
+ }
353
+
354
+ case 'done': {
355
+ await this.appendLogEntry({
356
+ type: 'done',
357
+ message: JSON.stringify({
358
+ result: event.result,
359
+ durationMs: event.durationMs,
360
+ durationApiMs: event.durationApiMs,
361
+ numTurns: event.numTurns,
362
+ totalCostUsd: event.totalCostUsd,
363
+ usage: event.usage,
364
+ modelUsage: event.modelUsage,
365
+ permissionDenials: event.permissionDenials,
366
+ ts: event.ts,
367
+ }),
368
+ });
369
+ return;
370
+ }
120
371
 
121
372
  case 'user_message': {
122
- const summary = this.summarizeUserMessage(event.content);
123
- if (summary) {
124
- await this.appendLog(summary);
125
- }
373
+ await this.appendLogEntry({
374
+ type: 'user_message',
375
+ message: JSON.stringify({
376
+ content: event.content,
377
+ isSynthetic: event.isSynthetic,
378
+ ts: event.ts,
379
+ }),
380
+ });
381
+ return;
382
+ }
383
+
384
+ case 'raw_sdk_event': {
385
+ // Skip raw SDK events - too verbose for persistence
126
386
  return;
127
387
  }
128
388
 
129
389
  default:
130
- // For any unfamiliar event types, avoid spamming the log.
390
+ // For any unfamiliar event types, log them as-is
391
+ this.logger.debug('Unknown event type', { type: (event as any).type });
131
392
  return;
132
393
  }
133
394
  }
@@ -168,104 +429,4 @@ export class TaskProgressReporter {
168
429
  }
169
430
  }
170
431
 
171
- private summarizeUserMessage(content?: string): string | null {
172
- if (!content) {
173
- return null;
174
- }
175
- const trimmed = content.trim();
176
- if (!trimmed) {
177
- return null;
178
- }
179
-
180
- const fileUpdateMatch = trimmed.match(/The file\s+([^\s]+)\s+has been updated/i);
181
- if (fileUpdateMatch) {
182
- return `[user] file updated: ${fileUpdateMatch[1]}`;
183
- }
184
-
185
- if (/Todos have been modified/i.test(trimmed)) {
186
- return '[todo] list updated';
187
- }
188
-
189
- const diffMatch = trimmed.match(/diff --git a\/([^\s]+) b\/([^\s]+)/);
190
- if (diffMatch) {
191
- return `[diff] ${diffMatch[2] ?? diffMatch[1]}`;
192
- }
193
-
194
- const gitStatusMatch = trimmed.match(/^On branch ([^\n]+)/);
195
- if (gitStatusMatch) {
196
- return `[git] status ${gitStatusMatch[1]}`;
197
- }
198
-
199
- if (/This Bash command contains multiple operations/i.test(trimmed)) {
200
- return '[approval] multi-step command pending';
201
- }
202
-
203
- if (/This command requires approval/i.test(trimmed)) {
204
- return '[approval] command awaiting approval';
205
- }
206
-
207
- if (/^Exit plan mode\?/i.test(trimmed)) {
208
- return null;
209
- }
210
-
211
- if (trimmed.includes('node_modules')) {
212
- return null;
213
- }
214
-
215
- if (trimmed.includes('total ') && trimmed.includes('drwx')) {
216
- return null;
217
- }
218
-
219
- if (trimmed.includes('→')) {
220
- return null;
221
- }
222
-
223
- if (trimmed.split('\n').length > 2) {
224
- return null;
225
- }
226
-
227
- const normalized = trimmed.replace(/\s+/g, ' ');
228
- const maxLen = 120;
229
- if (!normalized) {
230
- return null;
231
- }
232
- const preview = normalized.length > maxLen ? `${normalized.slice(0, maxLen)}…` : normalized;
233
- return `[user] ${preview}`;
234
- }
235
-
236
-
237
- private truncateMultiline(text: string, max = 160): string {
238
- if (!text) {
239
- return '';
240
- }
241
- const compact = text.replace(/\s+/g, ' ').trim();
242
- return compact.length > max ? `${compact.slice(0, max)}…` : compact;
243
- }
244
-
245
- private formatToolCallEvent(event: Extract<AgentEvent, { type: 'tool_call' }>): string | null {
246
- // File operations to track
247
- const fileOps = ['read_file', 'write', 'search_replace', 'delete_file', 'glob_file_search', 'file_search', 'list_dir', 'edit_notebook'];
248
- // Terminal commands to track
249
- const terminalOps = ['run_terminal_cmd', 'bash', 'shell'];
250
-
251
- if (fileOps.includes(event.toolName)) {
252
- // Extract file path from args
253
- const path = event.args?.target_file || event.args?.file_path || event.args?.target_notebook || event.args?.target_directory || '';
254
- return `[tool] ${event.toolName}${path ? `: ${path}` : ''}`;
255
- } else if (terminalOps.includes(event.toolName)) {
256
- // Extract command from args
257
- const cmd = event.args?.command || '';
258
- const truncated = cmd.length > 80 ? `${cmd.slice(0, 80)}…` : cmd;
259
- return `[cmd] ${truncated}`;
260
- }
261
-
262
- // Skip other tools from persistence
263
- return null;
264
- }
265
-
266
- private formatToolResultEvent(event: Extract<AgentEvent, { type: 'tool_result' }>): string | null {
267
- // We don't need to log tool results separately - tool calls are sufficient
268
- // This keeps the log concise
269
- return null;
270
- }
271
432
  }
package/src/types.ts CHANGED
@@ -32,6 +32,17 @@ export interface LogEntry {
32
32
  [key: string]: unknown; // Allow additional fields
33
33
  }
34
34
 
35
+ export type ArtifactType = 'plan' | 'context' | 'reference' | 'output' | 'artifact';
36
+
37
+ export interface TaskRunArtifact {
38
+ name: string;
39
+ type: ArtifactType;
40
+ size?: number;
41
+ content_type?: string;
42
+ storage_path?: string;
43
+ uploaded_at?: string;
44
+ }
45
+
35
46
  // TaskRun model - represents individual execution runs of tasks
36
47
  export interface TaskRun {
37
48
  id: string;
@@ -43,6 +54,7 @@ export interface TaskRun {
43
54
  error_message: string | null;
44
55
  output: Record<string, unknown> | null; // Structured output (PR URL, commit SHA, etc.)
45
56
  state: Record<string, unknown>; // Intermediate run state (defaults to {}, never null)
57
+ artifacts?: TaskRunArtifact[];
46
58
  created_at: string;
47
59
  updated_at: string;
48
60
  completed_at: string | null;
@@ -51,10 +63,17 @@ export interface TaskRun {
51
63
  export interface SupportingFile {
52
64
  name: string;
53
65
  content: string;
54
- type: 'plan' | 'context' | 'reference' | 'output';
66
+ type: ArtifactType;
55
67
  created_at: string;
56
68
  }
57
69
 
70
+ export interface TaskArtifactUploadPayload {
71
+ name: string;
72
+ type: ArtifactType;
73
+ content: string;
74
+ content_type?: string;
75
+ }
76
+
58
77
  export enum PermissionMode {
59
78
  PLAN = "plan",
60
79
  DEFAULT = "default",
@@ -2,6 +2,7 @@ import type { WorkflowDefinition } from './types.js';
2
2
  import { researchStep } from './steps/research.js';
3
3
  import { planStep } from './steps/plan.js';
4
4
  import { buildStep } from './steps/build.js';
5
+ import { finalizeStep } from './steps/finalize.js';
5
6
 
6
7
  const MODELS = {
7
8
  SONNET: "claude-sonnet-4-5",
@@ -39,4 +40,14 @@ export const TASK_WORKFLOW: WorkflowDefinition = [
39
40
  push: true,
40
41
  run: buildStep,
41
42
  },
43
+ {
44
+ id: 'finalize',
45
+ name: 'Finalize',
46
+ agent: 'system', // not used
47
+ model: MODELS.HAIKU, // not used
48
+ permissionMode: 'plan', // not used
49
+ commit: true,
50
+ push: true,
51
+ run: finalizeStep,
52
+ },
42
53
  ];
@@ -88,9 +88,9 @@ export const buildStep: WorkflowStepRunner = async ({ step, context }) => {
88
88
 
89
89
  for await (const message of response) {
90
90
  emitEvent(adapter.createRawSDKEvent(message));
91
- const transformed = adapter.transform(message);
92
- if (transformed) {
93
- emitEvent(transformed);
91
+ const transformedEvents = adapter.transform(message);
92
+ for (const event of transformedEvents) {
93
+ emitEvent(event);
94
94
  }
95
95
 
96
96
  const todoList = await todoManager.checkAndPersistFromMessage(message, task.id);