@posthog/agent 1.1.0 → 1.2.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.
- package/CLAUDE.md +68 -35
- package/README.md +46 -14
- package/dist/src/agent.d.ts +4 -1
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +38 -8
- package/dist/src/agent.js.map +1 -1
- package/dist/src/event-transformer.d.ts +2 -0
- package/dist/src/event-transformer.d.ts.map +1 -1
- package/dist/src/event-transformer.js +53 -5
- package/dist/src/event-transformer.js.map +1 -1
- package/dist/src/posthog-api.d.ts +34 -0
- package/dist/src/posthog-api.d.ts.map +1 -1
- package/dist/src/posthog-api.js +38 -0
- package/dist/src/posthog-api.js.map +1 -1
- package/dist/src/stage-executor.d.ts +4 -2
- package/dist/src/stage-executor.d.ts.map +1 -1
- package/dist/src/stage-executor.js +16 -5
- package/dist/src/stage-executor.js.map +1 -1
- package/dist/src/task-progress-reporter.d.ts +44 -0
- package/dist/src/task-progress-reporter.d.ts.map +1 -0
- package/dist/src/task-progress-reporter.js +234 -0
- package/dist/src/task-progress-reporter.js.map +1 -0
- package/package.json +1 -1
- package/src/agent.ts +41 -8
- package/src/event-transformer.ts +61 -7
- package/src/posthog-api.ts +79 -0
- package/src/stage-executor.ts +24 -8
- package/src/task-progress-reporter.ts +287 -0
package/src/stage-executor.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
|
2
2
|
import { Logger } from './utils/logger.js';
|
|
3
3
|
import { EventTransformer } from './event-transformer.js';
|
|
4
4
|
import { AgentRegistry } from './agent-registry.js';
|
|
5
|
-
import type { Task } from './types.js';
|
|
5
|
+
import type { AgentEvent, Task } from './types.js';
|
|
6
6
|
import type { WorkflowStage, WorkflowStageExecutionResult, WorkflowExecutionOptions } from './workflow-types.js';
|
|
7
7
|
import { PLANNING_SYSTEM_PROMPT } from './agents/planning.js';
|
|
8
8
|
import { EXECUTION_SYSTEM_PROMPT } from './agents/execution.js';
|
|
@@ -14,8 +14,14 @@ export class StageExecutor {
|
|
|
14
14
|
private logger: Logger;
|
|
15
15
|
private eventTransformer: EventTransformer;
|
|
16
16
|
private promptBuilder: PromptBuilder;
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
private eventHandler?: (event: AgentEvent) => void;
|
|
18
|
+
|
|
19
|
+
constructor(
|
|
20
|
+
registry: AgentRegistry,
|
|
21
|
+
logger: Logger,
|
|
22
|
+
promptBuilder?: PromptBuilder,
|
|
23
|
+
eventHandler?: (event: AgentEvent) => void,
|
|
24
|
+
) {
|
|
19
25
|
this.registry = registry;
|
|
20
26
|
this.logger = logger.child('StageExecutor');
|
|
21
27
|
this.eventTransformer = new EventTransformer();
|
|
@@ -24,6 +30,11 @@ export class StageExecutor {
|
|
|
24
30
|
generatePlanTemplate: async () => '',
|
|
25
31
|
logger,
|
|
26
32
|
});
|
|
33
|
+
this.eventHandler = eventHandler;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
setEventHandler(handler?: (event: AgentEvent) => void): void {
|
|
37
|
+
this.eventHandler = handler;
|
|
27
38
|
}
|
|
28
39
|
|
|
29
40
|
async execute(task: Task, stage: WorkflowStage, options: WorkflowExecutionOptions): Promise<WorkflowStageExecutionResult> {
|
|
@@ -85,8 +96,11 @@ export class StageExecutor {
|
|
|
85
96
|
let plan = '';
|
|
86
97
|
for await (const message of response) {
|
|
87
98
|
const transformed = this.eventTransformer.transform(message);
|
|
88
|
-
if (transformed
|
|
89
|
-
|
|
99
|
+
if (transformed) {
|
|
100
|
+
if (transformed.type !== 'token') {
|
|
101
|
+
this.logger.debug('Planning event', { type: transformed.type });
|
|
102
|
+
}
|
|
103
|
+
this.eventHandler?.(transformed);
|
|
90
104
|
}
|
|
91
105
|
if (message.type === 'assistant' && message.message?.content) {
|
|
92
106
|
for (const c of message.message.content) {
|
|
@@ -125,12 +139,14 @@ export class StageExecutor {
|
|
|
125
139
|
const results: any[] = [];
|
|
126
140
|
for await (const message of response) {
|
|
127
141
|
const transformed = this.eventTransformer.transform(message);
|
|
128
|
-
if (transformed
|
|
129
|
-
|
|
142
|
+
if (transformed) {
|
|
143
|
+
if (transformed.type !== 'token') {
|
|
144
|
+
this.logger.debug('Execution event', { type: transformed.type });
|
|
145
|
+
}
|
|
146
|
+
this.eventHandler?.(transformed);
|
|
130
147
|
}
|
|
131
148
|
results.push(message);
|
|
132
149
|
}
|
|
133
150
|
return { results };
|
|
134
151
|
}
|
|
135
152
|
}
|
|
136
|
-
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import type { Logger } from './utils/logger.js';
|
|
2
|
+
import type { PostHogAPIClient, TaskProgressRecord, TaskProgressUpdate } from './posthog-api.js';
|
|
3
|
+
import type { AgentEvent } from './types.js';
|
|
4
|
+
|
|
5
|
+
interface ProgressMetadata {
|
|
6
|
+
workflowId?: string;
|
|
7
|
+
workflowRunId?: string;
|
|
8
|
+
activityId?: string;
|
|
9
|
+
totalSteps?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Persists task execution progress to PostHog so clients can poll for updates.
|
|
14
|
+
*
|
|
15
|
+
* The reporter is intentionally best-effort – failures are logged but never
|
|
16
|
+
* allowed to break the agent execution flow.
|
|
17
|
+
*/
|
|
18
|
+
export class TaskProgressReporter {
|
|
19
|
+
private posthogAPI?: PostHogAPIClient;
|
|
20
|
+
private logger: Logger;
|
|
21
|
+
private progressRecord?: TaskProgressRecord;
|
|
22
|
+
private taskId?: string;
|
|
23
|
+
private outputLog: string[] = [];
|
|
24
|
+
private totalSteps?: number;
|
|
25
|
+
private lastLogEntry?: string;
|
|
26
|
+
|
|
27
|
+
constructor(posthogAPI: PostHogAPIClient | undefined, logger: Logger) {
|
|
28
|
+
this.posthogAPI = posthogAPI;
|
|
29
|
+
this.logger = logger.child('TaskProgressReporter');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
get progressId(): string | undefined {
|
|
33
|
+
return this.progressRecord?.id;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async start(taskId: string, metadata: ProgressMetadata = {}): Promise<void> {
|
|
37
|
+
if (!this.posthogAPI) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
this.taskId = taskId;
|
|
42
|
+
this.totalSteps = metadata.totalSteps;
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const record = await this.posthogAPI.createTaskProgress(taskId, {
|
|
46
|
+
status: 'started',
|
|
47
|
+
current_step: 'initializing',
|
|
48
|
+
total_steps: metadata.totalSteps ?? 0,
|
|
49
|
+
completed_steps: 0,
|
|
50
|
+
workflow_id: metadata.workflowId,
|
|
51
|
+
workflow_run_id: metadata.workflowRunId,
|
|
52
|
+
activity_id: metadata.activityId,
|
|
53
|
+
output_log: '',
|
|
54
|
+
});
|
|
55
|
+
this.progressRecord = record;
|
|
56
|
+
this.outputLog = record.output_log ? record.output_log.split('\n') : [];
|
|
57
|
+
this.logger.debug('Created task progress record', { taskId, progressId: record.id });
|
|
58
|
+
} catch (error) {
|
|
59
|
+
this.logger.warn('Failed to create task progress record', { taskId, error: (error as Error).message });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async stageStarted(stageKey: string, stageIndex: number): Promise<void> {
|
|
64
|
+
await this.update({
|
|
65
|
+
status: 'in_progress',
|
|
66
|
+
current_step: stageKey,
|
|
67
|
+
completed_steps: Math.min(stageIndex, this.totalSteps ?? stageIndex),
|
|
68
|
+
}, `Stage started: ${stageKey}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async stageCompleted(stageKey: string, completedStages: number): Promise<void> {
|
|
72
|
+
await this.update({
|
|
73
|
+
status: 'in_progress',
|
|
74
|
+
current_step: stageKey,
|
|
75
|
+
completed_steps: Math.min(completedStages, this.totalSteps ?? completedStages),
|
|
76
|
+
}, `Stage completed: ${stageKey}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async branchCreated(stageKey: string, branchName: string): Promise<void> {
|
|
80
|
+
await this.appendLog(`Branch created (${stageKey}): ${branchName}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async commitMade(stageKey: string, kind: 'plan' | 'implementation'): Promise<void> {
|
|
84
|
+
await this.appendLog(`Commit made (${stageKey}, ${kind})`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async pullRequestCreated(stageKey: string, prUrl: string): Promise<void> {
|
|
88
|
+
await this.appendLog(`Pull request created (${stageKey}): ${prUrl}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async noNextStage(stageKey?: string): Promise<void> {
|
|
92
|
+
await this.appendLog(
|
|
93
|
+
stageKey
|
|
94
|
+
? `No next stage available after '${stageKey}'. Execution halted.`
|
|
95
|
+
: 'No next stage available. Execution halted.'
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async complete(): Promise<void> {
|
|
100
|
+
await this.update({ status: 'completed', completed_steps: this.totalSteps }, 'Workflow execution completed');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async fail(error: Error | string): Promise<void> {
|
|
104
|
+
const message = typeof error === 'string' ? error : error.message;
|
|
105
|
+
await this.update({ status: 'failed', error_message: message }, `Workflow execution failed: ${message}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async appendLog(line: string): Promise<void> {
|
|
109
|
+
await this.update({}, line);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async recordEvent(event: AgentEvent): Promise<void> {
|
|
113
|
+
if (!this.posthogAPI || !this.progressId || !this.taskId) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
switch (event.type) {
|
|
118
|
+
case 'token':
|
|
119
|
+
case 'message_delta':
|
|
120
|
+
case 'content_block_start':
|
|
121
|
+
case 'content_block_stop':
|
|
122
|
+
case 'compact_boundary':
|
|
123
|
+
case 'tool_call':
|
|
124
|
+
case 'tool_result':
|
|
125
|
+
case 'message_start':
|
|
126
|
+
case 'message_stop':
|
|
127
|
+
case 'metric':
|
|
128
|
+
case 'artifact':
|
|
129
|
+
// Skip verbose streaming artifacts from persistence
|
|
130
|
+
return;
|
|
131
|
+
|
|
132
|
+
case 'file_write':
|
|
133
|
+
await this.appendLog(this.formatFileWriteEvent(event));
|
|
134
|
+
return;
|
|
135
|
+
|
|
136
|
+
case 'diff':
|
|
137
|
+
await this.appendLog(this.formatDiffEvent(event));
|
|
138
|
+
return;
|
|
139
|
+
|
|
140
|
+
case 'status':
|
|
141
|
+
// Status events are covered by dedicated progress updates
|
|
142
|
+
return;
|
|
143
|
+
|
|
144
|
+
case 'error':
|
|
145
|
+
await this.appendLog(`[error] ${event.message}`);
|
|
146
|
+
return;
|
|
147
|
+
|
|
148
|
+
case 'done': {
|
|
149
|
+
const cost = event.totalCostUsd !== undefined ? ` cost=$${event.totalCostUsd.toFixed(2)}` : '';
|
|
150
|
+
await this.appendLog(
|
|
151
|
+
`[done] duration=${event.durationMs ?? 'unknown'}ms turns=${event.numTurns ?? 'unknown'}${cost}`
|
|
152
|
+
);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
case 'init':
|
|
157
|
+
// Omit verbose init messages from persisted log
|
|
158
|
+
return;
|
|
159
|
+
|
|
160
|
+
case 'user_message': {
|
|
161
|
+
const summary = this.summarizeUserMessage(event.content);
|
|
162
|
+
if (summary) {
|
|
163
|
+
await this.appendLog(summary);
|
|
164
|
+
}
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
default:
|
|
169
|
+
// For any unfamiliar event types, avoid spamming the log.
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private async update(update: TaskProgressUpdate, logLine?: string): Promise<void> {
|
|
175
|
+
if (!this.posthogAPI || !this.progressId || !this.taskId) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (logLine) {
|
|
180
|
+
if (logLine !== this.lastLogEntry) {
|
|
181
|
+
this.outputLog.push(logLine);
|
|
182
|
+
this.lastLogEntry = logLine;
|
|
183
|
+
}
|
|
184
|
+
update.output_log = this.outputLog.join('\n');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
const record = await this.posthogAPI.updateTaskProgress(this.taskId, this.progressId, update);
|
|
189
|
+
// Sync local cache with server response to avoid drift if server modifies values
|
|
190
|
+
this.progressRecord = record;
|
|
191
|
+
if (record.output_log !== undefined && record.output_log !== null) {
|
|
192
|
+
this.outputLog = record.output_log ? record.output_log.split('\n') : [];
|
|
193
|
+
}
|
|
194
|
+
} catch (error) {
|
|
195
|
+
this.logger.warn('Failed to update task progress record', {
|
|
196
|
+
taskId: this.taskId,
|
|
197
|
+
progressId: this.progressId,
|
|
198
|
+
error: (error as Error).message,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private summarizeUserMessage(content?: string): string | null {
|
|
204
|
+
if (!content) {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
const trimmed = content.trim();
|
|
208
|
+
if (!trimmed) {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const fileUpdateMatch = trimmed.match(/The file\s+([^\s]+)\s+has been updated/i);
|
|
213
|
+
if (fileUpdateMatch) {
|
|
214
|
+
return `[user] file updated: ${fileUpdateMatch[1]}`;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (/Todos have been modified/i.test(trimmed)) {
|
|
218
|
+
return '[todo] list updated';
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const diffMatch = trimmed.match(/diff --git a\/([^\s]+) b\/([^\s]+)/);
|
|
222
|
+
if (diffMatch) {
|
|
223
|
+
return `[diff] ${diffMatch[2] ?? diffMatch[1]}`;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const gitStatusMatch = trimmed.match(/^On branch ([^\n]+)/);
|
|
227
|
+
if (gitStatusMatch) {
|
|
228
|
+
return `[git] status ${gitStatusMatch[1]}`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (/This Bash command contains multiple operations/i.test(trimmed)) {
|
|
232
|
+
return '[approval] multi-step command pending';
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (/This command requires approval/i.test(trimmed)) {
|
|
236
|
+
return '[approval] command awaiting approval';
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (/^Exit plan mode\?/i.test(trimmed)) {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (trimmed.includes('node_modules')) {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (trimmed.includes('total ') && trimmed.includes('drwx')) {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (trimmed.includes('→')) {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (trimmed.split('\n').length > 2) {
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const normalized = trimmed.replace(/\s+/g, ' ');
|
|
260
|
+
const maxLen = 120;
|
|
261
|
+
if (!normalized) {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
const preview = normalized.length > maxLen ? `${normalized.slice(0, maxLen)}…` : normalized;
|
|
265
|
+
return `[user] ${preview}`;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private formatFileWriteEvent(event: Extract<AgentEvent, { type: 'file_write' }>): string {
|
|
269
|
+
const size = event.bytes !== undefined ? ` (${event.bytes} bytes)` : '';
|
|
270
|
+
return `[file] wrote ${event.path}${size}`;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
private formatDiffEvent(event: Extract<AgentEvent, { type: 'diff' }>): string {
|
|
274
|
+
const summary = event.summary
|
|
275
|
+
? event.summary.trim()
|
|
276
|
+
: this.truncateMultiline(event.patch ?? '', 160);
|
|
277
|
+
return `[diff] ${event.file}${summary ? ` | ${summary}` : ''}`;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private truncateMultiline(text: string, max = 160): string {
|
|
281
|
+
if (!text) {
|
|
282
|
+
return '';
|
|
283
|
+
}
|
|
284
|
+
const compact = text.replace(/\s+/g, ' ').trim();
|
|
285
|
+
return compact.length > max ? `${compact.slice(0, max)}…` : compact;
|
|
286
|
+
}
|
|
287
|
+
}
|