@posthog/agent 1.9.0 → 1.11.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/README.md +8 -5
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/src/agent-registry.d.ts.map +1 -1
- package/dist/src/agent-registry.js +6 -0
- package/dist/src/agent-registry.js.map +1 -1
- package/dist/src/agent.d.ts +5 -0
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +370 -29
- package/dist/src/agent.js.map +1 -1
- package/dist/src/agents/research.d.ts +2 -0
- package/dist/src/agents/research.d.ts.map +1 -0
- package/dist/src/agents/research.js +105 -0
- package/dist/src/agents/research.js.map +1 -0
- package/dist/src/file-manager.d.ts +19 -0
- package/dist/src/file-manager.d.ts.map +1 -1
- package/dist/src/file-manager.js +39 -0
- package/dist/src/file-manager.js.map +1 -1
- package/dist/src/git-manager.d.ts +4 -0
- package/dist/src/git-manager.d.ts.map +1 -1
- package/dist/src/git-manager.js +41 -0
- package/dist/src/git-manager.js.map +1 -1
- package/dist/src/posthog-api.d.ts +16 -57
- package/dist/src/posthog-api.d.ts.map +1 -1
- package/dist/src/posthog-api.js +38 -38
- package/dist/src/posthog-api.js.map +1 -1
- package/dist/src/prompt-builder.d.ts +1 -0
- package/dist/src/prompt-builder.d.ts.map +1 -1
- package/dist/src/prompt-builder.js +40 -0
- package/dist/src/prompt-builder.js.map +1 -1
- package/dist/src/stage-executor.d.ts +1 -0
- package/dist/src/stage-executor.d.ts.map +1 -1
- package/dist/src/stage-executor.js +43 -0
- package/dist/src/stage-executor.js.map +1 -1
- package/dist/src/structured-extraction.d.ts +22 -0
- package/dist/src/structured-extraction.d.ts.map +1 -0
- package/dist/src/structured-extraction.js +136 -0
- package/dist/src/structured-extraction.js.map +1 -0
- package/dist/src/task-progress-reporter.d.ts +2 -5
- package/dist/src/task-progress-reporter.d.ts.map +1 -1
- package/dist/src/task-progress-reporter.js +37 -39
- package/dist/src/task-progress-reporter.js.map +1 -1
- package/dist/src/types.d.ts +31 -3
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js.map +1 -1
- package/dist/src/workflow-types.d.ts +1 -1
- package/dist/src/workflow-types.d.ts.map +1 -1
- package/package.json +4 -3
- package/src/agent-registry.ts +6 -0
- package/src/agent.ts +409 -26
- package/src/agents/research.ts +103 -0
- package/src/file-manager.ts +64 -0
- package/src/git-manager.ts +52 -0
- package/src/posthog-api.ts +57 -92
- package/src/prompt-builder.ts +53 -0
- package/src/stage-executor.ts +50 -0
- package/src/structured-extraction.ts +167 -0
- package/src/task-progress-reporter.ts +38 -44
- package/src/types.ts +39 -3
- package/src/workflow-types.ts +1 -1
package/src/agent.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { WorkflowRegistry } from './workflow-registry.js';
|
|
|
16
16
|
import { StageExecutor } from './stage-executor.js';
|
|
17
17
|
import { PromptBuilder } from './prompt-builder.js';
|
|
18
18
|
import { TaskProgressReporter } from './task-progress-reporter.js';
|
|
19
|
+
import { OpenAIExtractor, type ExtractedQuestion, type ExtractedQuestionWithAnswer } from './structured-extraction.js';
|
|
19
20
|
|
|
20
21
|
export class Agent {
|
|
21
22
|
private workingDirectory: string;
|
|
@@ -31,6 +32,8 @@ export class Agent {
|
|
|
31
32
|
private workflowRegistry: WorkflowRegistry;
|
|
32
33
|
private stageExecutor: StageExecutor;
|
|
33
34
|
private progressReporter: TaskProgressReporter;
|
|
35
|
+
private promptBuilder: PromptBuilder;
|
|
36
|
+
private extractor?: OpenAIExtractor;
|
|
34
37
|
private mcpServers?: Record<string, any>;
|
|
35
38
|
public debug: boolean;
|
|
36
39
|
|
|
@@ -88,7 +91,7 @@ export class Agent {
|
|
|
88
91
|
}
|
|
89
92
|
|
|
90
93
|
this.workflowRegistry = new WorkflowRegistry(this.posthogAPI);
|
|
91
|
-
|
|
94
|
+
this.promptBuilder = new PromptBuilder({
|
|
92
95
|
getTaskFiles: (taskId: string) => this.getTaskFiles(taskId),
|
|
93
96
|
generatePlanTemplate: (vars) => this.templateManager.generatePlan(vars),
|
|
94
97
|
posthogClient: this.posthogAPI,
|
|
@@ -97,12 +100,17 @@ export class Agent {
|
|
|
97
100
|
this.stageExecutor = new StageExecutor(
|
|
98
101
|
this.agentRegistry,
|
|
99
102
|
this.logger,
|
|
100
|
-
promptBuilder,
|
|
103
|
+
this.promptBuilder,
|
|
101
104
|
undefined, // eventHandler set via setEventHandler below
|
|
102
105
|
this.mcpServers
|
|
103
106
|
);
|
|
104
107
|
this.stageExecutor.setEventHandler((event) => this.emitEvent(event));
|
|
105
108
|
this.progressReporter = new TaskProgressReporter(this.posthogAPI, this.logger);
|
|
109
|
+
|
|
110
|
+
// Initialize OpenAI extractor if API key is available
|
|
111
|
+
if (process.env.OPENAI_API_KEY) {
|
|
112
|
+
this.extractor = new OpenAIExtractor(this.logger.child('OpenAIExtractor'));
|
|
113
|
+
}
|
|
106
114
|
}
|
|
107
115
|
|
|
108
116
|
/**
|
|
@@ -158,13 +166,8 @@ export class Agent {
|
|
|
158
166
|
await this.posthogAPI.updateTask(task.id, { workflow: workflowId } as any);
|
|
159
167
|
(task as any).workflow = workflowId;
|
|
160
168
|
}
|
|
161
|
-
if (!(task as any).current_stage && workflow.stages.length > 0) {
|
|
162
|
-
const firstStage = [...workflow.stages].sort((a, b) => a.position - b.position)[0];
|
|
163
|
-
await this.posthogAPI.updateTaskStage(task.id, firstStage.id);
|
|
164
|
-
(task as any).current_stage = firstStage.id;
|
|
165
|
-
}
|
|
166
169
|
} catch (e) {
|
|
167
|
-
this.logger.warn('Failed to sync task workflow
|
|
170
|
+
this.logger.warn('Failed to sync task workflow before execution', { error: (e as Error).message });
|
|
168
171
|
}
|
|
169
172
|
}
|
|
170
173
|
|
|
@@ -172,11 +175,21 @@ export class Agent {
|
|
|
172
175
|
this.logger.info('Starting workflow execution', { taskId: task.id, workflowId, executionId });
|
|
173
176
|
this.taskManager.startExecution(task.id, 'plan_and_build', executionId);
|
|
174
177
|
await this.progressReporter.start(task.id, {
|
|
175
|
-
workflowId,
|
|
176
|
-
workflowRunId: executionId,
|
|
177
178
|
totalSteps: orderedStages.length,
|
|
178
179
|
});
|
|
179
180
|
|
|
181
|
+
// Set initial stage on the newly created run
|
|
182
|
+
const firstStage = orderedStages[0];
|
|
183
|
+
if (this.posthogAPI && this.progressReporter.runId && firstStage) {
|
|
184
|
+
try {
|
|
185
|
+
await this.posthogAPI.updateTaskRun(task.id, this.progressReporter.runId, {
|
|
186
|
+
current_stage: firstStage.id
|
|
187
|
+
});
|
|
188
|
+
} catch (e) {
|
|
189
|
+
this.logger.warn('Failed to set initial stage on run', { error: (e as Error).message });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
180
193
|
try {
|
|
181
194
|
let startIndex = 0;
|
|
182
195
|
const currentStageId = (task as any).current_stage as string | undefined;
|
|
@@ -200,11 +213,17 @@ export class Agent {
|
|
|
200
213
|
if (idx >= 0) startIndex = idx;
|
|
201
214
|
}
|
|
202
215
|
|
|
203
|
-
// Align server-side stage when restarting from
|
|
204
|
-
if (this.posthogAPI) {
|
|
216
|
+
// Align server-side stage when restarting from a different stage
|
|
217
|
+
if (this.posthogAPI && this.progressReporter.runId) {
|
|
205
218
|
const targetStage = orderedStages[startIndex];
|
|
206
219
|
if (targetStage && targetStage.id !== currentStageId) {
|
|
207
|
-
try {
|
|
220
|
+
try {
|
|
221
|
+
await this.posthogAPI.updateTaskRun(task.id, this.progressReporter.runId, {
|
|
222
|
+
current_stage: targetStage.id
|
|
223
|
+
});
|
|
224
|
+
} catch (e) {
|
|
225
|
+
this.logger.warn('Failed to update run stage', { error: (e as Error).message });
|
|
226
|
+
}
|
|
208
227
|
}
|
|
209
228
|
}
|
|
210
229
|
|
|
@@ -317,14 +336,352 @@ export class Agent {
|
|
|
317
336
|
this.emitEvent(this.adapter.createStatusEvent('stage_complete', { stage: stage.key }));
|
|
318
337
|
}
|
|
319
338
|
|
|
339
|
+
// Adaptive task execution - 3-phase workflow (research → plan → build)
|
|
340
|
+
async runTask(taskOrId: Task | string, options: import('./types.js').TaskExecutionOptions = {}): Promise<void> {
|
|
341
|
+
await this._configureLlmGateway();
|
|
342
|
+
|
|
343
|
+
const task = typeof taskOrId === 'string' ? await this.fetchTask(taskOrId) : taskOrId;
|
|
344
|
+
const cwd = options.repositoryPath || this.workingDirectory;
|
|
345
|
+
const isCloudMode = options.isCloudMode ?? false;
|
|
346
|
+
const taskSlug = (task as any).slug || task.id;
|
|
347
|
+
|
|
348
|
+
this.logger.info('Starting adaptive task execution', { taskId: task.id, taskSlug, isCloudMode });
|
|
349
|
+
|
|
350
|
+
// Initialize progress reporter for task run tracking (needed for PR attachment)
|
|
351
|
+
await this.progressReporter.start(task.id, { totalSteps: 3 }); // 3 phases: research, plan, build
|
|
352
|
+
this.emitEvent(this.adapter.createStatusEvent('run_started', { runId: this.progressReporter.runId }));
|
|
353
|
+
|
|
354
|
+
// Phase 1: Branch check
|
|
355
|
+
const existingBranch = await this.gitManager.getTaskBranch(taskSlug);
|
|
356
|
+
if (!existingBranch) {
|
|
357
|
+
this.logger.info('Creating task branch', { taskSlug });
|
|
358
|
+
const branchName = `posthog/task-${taskSlug}`;
|
|
359
|
+
await this.gitManager.createOrSwitchToBranch(branchName);
|
|
360
|
+
this.emitEvent(this.adapter.createStatusEvent('branch_created', { branch: branchName }));
|
|
361
|
+
|
|
362
|
+
// Initial commit
|
|
363
|
+
await this.fileManager.ensureGitignore();
|
|
364
|
+
await this.gitManager.addAllPostHogFiles();
|
|
365
|
+
if (isCloudMode) {
|
|
366
|
+
await this.gitManager.commitAndPush(`Initialize task ${taskSlug}`, { allowEmpty: true });
|
|
367
|
+
} else {
|
|
368
|
+
await this.gitManager.commitChanges(`Initialize task ${taskSlug}`);
|
|
369
|
+
}
|
|
370
|
+
} else {
|
|
371
|
+
this.logger.info('Switching to existing task branch', { branch: existingBranch });
|
|
372
|
+
await this.gitManager.switchToBranch(existingBranch);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Phase 2: Research
|
|
376
|
+
const researchExists = await this.fileManager.readResearch(task.id);
|
|
377
|
+
if (!researchExists) {
|
|
378
|
+
this.logger.info('Starting research phase', { taskId: task.id });
|
|
379
|
+
this.emitEvent(this.adapter.createStatusEvent('phase_start', { phase: 'research' }));
|
|
380
|
+
|
|
381
|
+
// Run research agent
|
|
382
|
+
const researchPrompt = await this.promptBuilder.buildResearchPrompt(task, cwd);
|
|
383
|
+
const { RESEARCH_SYSTEM_PROMPT } = await import('./agents/research.js');
|
|
384
|
+
const fullPrompt = RESEARCH_SYSTEM_PROMPT + '\n\n' + researchPrompt;
|
|
385
|
+
|
|
386
|
+
const baseOptions: Record<string, any> = {
|
|
387
|
+
model: 'claude-sonnet-4-5-20250929',
|
|
388
|
+
cwd,
|
|
389
|
+
permissionMode: 'plan',
|
|
390
|
+
settingSources: ['local'],
|
|
391
|
+
mcpServers: this.mcpServers,
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
const response = query({
|
|
395
|
+
prompt: fullPrompt,
|
|
396
|
+
options: { ...baseOptions, ...(options.queryOverrides || {}) },
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
let researchContent = '';
|
|
400
|
+
for await (const message of response) {
|
|
401
|
+
this.emitEvent(this.adapter.createRawSDKEvent(message));
|
|
402
|
+
const transformed = this.adapter.transform(message);
|
|
403
|
+
if (transformed) {
|
|
404
|
+
this.emitEvent(transformed);
|
|
405
|
+
}
|
|
406
|
+
if (message.type === 'assistant' && message.message?.content) {
|
|
407
|
+
for (const c of message.message.content) {
|
|
408
|
+
if (c.type === 'text' && c.text) researchContent += c.text + '\n';
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Write research.md
|
|
414
|
+
if (researchContent.trim()) {
|
|
415
|
+
await this.fileManager.writeResearch(task.id, researchContent.trim());
|
|
416
|
+
this.logger.info('Research completed', { taskId: task.id });
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Commit research
|
|
420
|
+
await this.gitManager.addAllPostHogFiles();
|
|
421
|
+
|
|
422
|
+
// Extract questions using structured output and save to questions.json
|
|
423
|
+
if (this.extractor) {
|
|
424
|
+
try {
|
|
425
|
+
this.logger.info('Extracting questions from research.md', { taskId: task.id });
|
|
426
|
+
const questions = await this.extractQuestionsFromResearch(task.id, false);
|
|
427
|
+
|
|
428
|
+
this.logger.info('Questions extracted successfully', { taskId: task.id, count: questions.length });
|
|
429
|
+
|
|
430
|
+
// Save questions.json
|
|
431
|
+
await this.fileManager.writeQuestions(task.id, {
|
|
432
|
+
questions,
|
|
433
|
+
answered: false,
|
|
434
|
+
answers: null,
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
this.logger.info('Questions saved to questions.json', { taskId: task.id });
|
|
438
|
+
|
|
439
|
+
// Emit event for Array to pick up (local mode)
|
|
440
|
+
if (!isCloudMode) {
|
|
441
|
+
this.emitEvent({
|
|
442
|
+
type: 'artifact',
|
|
443
|
+
ts: Date.now(),
|
|
444
|
+
kind: 'research_questions',
|
|
445
|
+
content: questions,
|
|
446
|
+
});
|
|
447
|
+
this.logger.info('Emitted research_questions artifact event', { taskId: task.id });
|
|
448
|
+
}
|
|
449
|
+
} catch (error) {
|
|
450
|
+
this.logger.error('Failed to extract questions', { error: error instanceof Error ? error.message : String(error) });
|
|
451
|
+
this.emitEvent({
|
|
452
|
+
type: 'error',
|
|
453
|
+
ts: Date.now(),
|
|
454
|
+
message: `Failed to extract questions: ${error instanceof Error ? error.message : String(error)}`,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
} else {
|
|
458
|
+
this.logger.warn('OpenAI extractor not available (OPENAI_API_KEY not set), skipping question extraction');
|
|
459
|
+
this.emitEvent({
|
|
460
|
+
type: 'status',
|
|
461
|
+
ts: Date.now(),
|
|
462
|
+
phase: 'extraction_skipped',
|
|
463
|
+
message: 'Question extraction skipped - OPENAI_API_KEY not configured',
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (isCloudMode) {
|
|
468
|
+
await this.gitManager.commitAndPush(`Research phase for ${task.title}`);
|
|
469
|
+
} else {
|
|
470
|
+
await this.gitManager.commitChanges(`Research phase for ${task.title}`);
|
|
471
|
+
this.emitEvent(this.adapter.createStatusEvent('phase_complete', { phase: 'research' }));
|
|
472
|
+
return; // Local mode: return to user
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Phase 3: Auto-answer questions (cloud mode only)
|
|
477
|
+
if (isCloudMode) {
|
|
478
|
+
const questionsData = await this.fileManager.readQuestions(task.id);
|
|
479
|
+
if (questionsData && !questionsData.answered) {
|
|
480
|
+
this.logger.info('Auto-answering research questions', { taskId: task.id });
|
|
481
|
+
|
|
482
|
+
// Extract questions with recommended answers using structured output
|
|
483
|
+
if (this.extractor) {
|
|
484
|
+
const questionsWithAnswers = await this.extractQuestionsFromResearch(task.id, true) as ExtractedQuestionWithAnswer[];
|
|
485
|
+
|
|
486
|
+
// Save answers to questions.json
|
|
487
|
+
await this.fileManager.writeQuestions(task.id, {
|
|
488
|
+
questions: questionsWithAnswers.map(qa => ({
|
|
489
|
+
id: qa.id,
|
|
490
|
+
question: qa.question,
|
|
491
|
+
options: qa.options,
|
|
492
|
+
})),
|
|
493
|
+
answered: true,
|
|
494
|
+
answers: questionsWithAnswers.map(qa => ({
|
|
495
|
+
questionId: qa.id,
|
|
496
|
+
selectedOption: qa.recommendedAnswer,
|
|
497
|
+
customInput: qa.justification,
|
|
498
|
+
})),
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
this.logger.info('Auto-answers saved to questions.json', { taskId: task.id });
|
|
502
|
+
await this.gitManager.addAllPostHogFiles();
|
|
503
|
+
await this.gitManager.commitAndPush(`Answer research questions for ${task.title}`);
|
|
504
|
+
} else {
|
|
505
|
+
this.logger.warn('OpenAI extractor not available, skipping auto-answer');
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Phase 4: Plan
|
|
511
|
+
const planExists = await this.readPlan(task.id);
|
|
512
|
+
if (!planExists) {
|
|
513
|
+
// Check if questions have been answered
|
|
514
|
+
const questionsData = await this.fileManager.readQuestions(task.id);
|
|
515
|
+
if (!questionsData || !questionsData.answered) {
|
|
516
|
+
this.logger.info('Waiting for user answers to research questions');
|
|
517
|
+
this.emitEvent(this.adapter.createStatusEvent('phase_complete', { phase: 'research_questions' }));
|
|
518
|
+
return; // Wait for user to answer questions
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
this.logger.info('Starting planning phase', { taskId: task.id });
|
|
522
|
+
this.emitEvent(this.adapter.createStatusEvent('phase_start', { phase: 'planning' }));
|
|
523
|
+
|
|
524
|
+
// Build context with research + questions + answers
|
|
525
|
+
const research = await this.fileManager.readResearch(task.id);
|
|
526
|
+
let researchContext = '';
|
|
527
|
+
if (research) {
|
|
528
|
+
researchContext += `## Research Analysis\n\n${research}\n\n`;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Add questions and answers
|
|
532
|
+
researchContext += `## Implementation Decisions\n\n`;
|
|
533
|
+
const answers = questionsData.answers || [];
|
|
534
|
+
for (const question of questionsData.questions) {
|
|
535
|
+
const answer = answers.find((a: any) => a.questionId === question.id);
|
|
536
|
+
|
|
537
|
+
researchContext += `### ${question.question}\n\n`;
|
|
538
|
+
if (answer) {
|
|
539
|
+
researchContext += `**Selected:** ${answer.selectedOption}\n`;
|
|
540
|
+
if (answer.customInput) {
|
|
541
|
+
researchContext += `**Details:** ${answer.customInput}\n`;
|
|
542
|
+
}
|
|
543
|
+
} else {
|
|
544
|
+
this.logger.warn('No answer found for question', { questionId: question.id });
|
|
545
|
+
researchContext += `**Selected:** Not answered\n`;
|
|
546
|
+
}
|
|
547
|
+
researchContext += '\n';
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Run planning agent with full context
|
|
551
|
+
const planningPrompt = await this.promptBuilder.buildPlanningPrompt(task, cwd);
|
|
552
|
+
const { PLANNING_SYSTEM_PROMPT } = await import('./agents/planning.js');
|
|
553
|
+
const fullPrompt = PLANNING_SYSTEM_PROMPT + '\n\n' + planningPrompt + '\n\n' + researchContext;
|
|
554
|
+
|
|
555
|
+
const baseOptions: Record<string, any> = {
|
|
556
|
+
model: 'claude-sonnet-4-5-20250929',
|
|
557
|
+
cwd,
|
|
558
|
+
permissionMode: 'plan',
|
|
559
|
+
settingSources: ['local'],
|
|
560
|
+
mcpServers: this.mcpServers,
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
const response = query({
|
|
564
|
+
prompt: fullPrompt,
|
|
565
|
+
options: { ...baseOptions, ...(options.queryOverrides || {}) },
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
let planContent = '';
|
|
569
|
+
for await (const message of response) {
|
|
570
|
+
this.emitEvent(this.adapter.createRawSDKEvent(message));
|
|
571
|
+
const transformed = this.adapter.transform(message);
|
|
572
|
+
if (transformed) {
|
|
573
|
+
this.emitEvent(transformed);
|
|
574
|
+
}
|
|
575
|
+
if (message.type === 'assistant' && message.message?.content) {
|
|
576
|
+
for (const c of message.message.content) {
|
|
577
|
+
if (c.type === 'text' && c.text) planContent += c.text + '\n';
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Write plan.md
|
|
583
|
+
if (planContent.trim()) {
|
|
584
|
+
await this.writePlan(task.id, planContent.trim());
|
|
585
|
+
this.logger.info('Plan completed', { taskId: task.id });
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Commit plan
|
|
589
|
+
await this.gitManager.addAllPostHogFiles();
|
|
590
|
+
if (isCloudMode) {
|
|
591
|
+
await this.gitManager.commitAndPush(`Planning phase for ${task.title}`);
|
|
592
|
+
} else {
|
|
593
|
+
await this.gitManager.commitChanges(`Planning phase for ${task.title}`);
|
|
594
|
+
this.emitEvent(this.adapter.createStatusEvent('phase_complete', { phase: 'planning' }));
|
|
595
|
+
return; // Local mode: return to user
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Phase 5: Build
|
|
600
|
+
const latestRun = task.latest_run;
|
|
601
|
+
const prExists = latestRun?.output && (latestRun.output as any).pr_url;
|
|
602
|
+
|
|
603
|
+
if (!prExists) {
|
|
604
|
+
this.logger.info('Starting build phase', { taskId: task.id });
|
|
605
|
+
this.emitEvent(this.adapter.createStatusEvent('phase_start', { phase: 'build' }));
|
|
606
|
+
|
|
607
|
+
// Run execution agent
|
|
608
|
+
const executionPrompt = await this.promptBuilder.buildExecutionPrompt(task, cwd);
|
|
609
|
+
const { EXECUTION_SYSTEM_PROMPT } = await import('./agents/execution.js');
|
|
610
|
+
const fullPrompt = EXECUTION_SYSTEM_PROMPT + '\n\n' + executionPrompt;
|
|
611
|
+
|
|
612
|
+
const { PermissionMode } = await import('./types.js');
|
|
613
|
+
const permissionMode = options.permissionMode || PermissionMode.ACCEPT_EDITS;
|
|
614
|
+
const baseOptions: Record<string, any> = {
|
|
615
|
+
model: 'claude-sonnet-4-5-20250929',
|
|
616
|
+
cwd,
|
|
617
|
+
permissionMode,
|
|
618
|
+
settingSources: ['local'],
|
|
619
|
+
mcpServers: this.mcpServers,
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
const response = query({
|
|
623
|
+
prompt: fullPrompt,
|
|
624
|
+
options: { ...baseOptions, ...(options.queryOverrides || {}) },
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
for await (const message of response) {
|
|
628
|
+
this.emitEvent(this.adapter.createRawSDKEvent(message));
|
|
629
|
+
const transformed = this.adapter.transform(message);
|
|
630
|
+
if (transformed) {
|
|
631
|
+
this.emitEvent(transformed);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Commit and push implementation
|
|
636
|
+
// Stage ALL changes (not just .posthog/)
|
|
637
|
+
const hasChanges = await this.gitManager.hasChanges();
|
|
638
|
+
if (hasChanges) {
|
|
639
|
+
await this.gitManager.addFiles(['.']); // Stage all changes
|
|
640
|
+
await this.gitManager.commitChanges(`Implementation for ${task.title}`);
|
|
641
|
+
|
|
642
|
+
// Push to origin
|
|
643
|
+
const branchName = await this.gitManager.getCurrentBranch();
|
|
644
|
+
await this.gitManager.pushBranch(branchName);
|
|
645
|
+
|
|
646
|
+
this.logger.info('Implementation committed and pushed', { taskId: task.id });
|
|
647
|
+
} else {
|
|
648
|
+
this.logger.warn('No changes to commit in build phase', { taskId: task.id });
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Create PR
|
|
652
|
+
const branchName = await this.gitManager.getCurrentBranch();
|
|
653
|
+
const prUrl = await this.createPullRequest(task.id, branchName, task.title, task.description);
|
|
654
|
+
this.logger.info('Pull request created', { taskId: task.id, prUrl });
|
|
655
|
+
this.emitEvent(this.adapter.createStatusEvent('pr_created', { prUrl }));
|
|
656
|
+
|
|
657
|
+
// Attach PR to task run
|
|
658
|
+
try {
|
|
659
|
+
await this.attachPullRequestToTask(task.id, prUrl, branchName);
|
|
660
|
+
this.logger.info('PR attached to task successfully', { taskId: task.id, prUrl });
|
|
661
|
+
} catch (error) {
|
|
662
|
+
this.logger.warn('Could not attach PR to task', { error: error instanceof Error ? error.message : String(error) });
|
|
663
|
+
}
|
|
664
|
+
} else {
|
|
665
|
+
this.logger.info('PR already exists, skipping build phase', { taskId: task.id });
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Phase 6: Complete
|
|
669
|
+
await this.progressReporter.complete();
|
|
670
|
+
this.logger.info('Task execution complete', { taskId: task.id });
|
|
671
|
+
this.emitEvent(this.adapter.createStatusEvent('task_complete', { taskId: task.id }));
|
|
672
|
+
}
|
|
673
|
+
|
|
320
674
|
async progressToNextStage(taskId: string, currentStageKey?: string): Promise<void> {
|
|
321
|
-
if (!this.posthogAPI
|
|
675
|
+
if (!this.posthogAPI || !this.progressReporter.runId) {
|
|
676
|
+
throw new Error('PostHog API not configured or no active run. Cannot progress stage.');
|
|
677
|
+
}
|
|
322
678
|
try {
|
|
323
|
-
await this.posthogAPI.
|
|
679
|
+
await this.posthogAPI.progressTaskRun(taskId, this.progressReporter.runId);
|
|
324
680
|
} catch (error) {
|
|
325
681
|
if (error instanceof Error && error.message.includes('No next stage available')) {
|
|
326
|
-
this.logger.warn('No next stage available when attempting to progress
|
|
682
|
+
this.logger.warn('No next stage available when attempting to progress run', {
|
|
327
683
|
taskId,
|
|
684
|
+
runId: this.progressReporter.runId,
|
|
328
685
|
stage: currentStageKey,
|
|
329
686
|
error: error.message,
|
|
330
687
|
});
|
|
@@ -423,6 +780,25 @@ export class Agent {
|
|
|
423
780
|
this.logger.debug('Reading plan', { taskId });
|
|
424
781
|
return await this.fileManager.readPlan(taskId);
|
|
425
782
|
}
|
|
783
|
+
|
|
784
|
+
async extractQuestionsFromResearch(taskId: string, includeAnswers: boolean = false): Promise<ExtractedQuestion[] | ExtractedQuestionWithAnswer[]> {
|
|
785
|
+
this.logger.info('Extracting questions from research.md', { taskId, includeAnswers });
|
|
786
|
+
|
|
787
|
+
if (!this.extractor) {
|
|
788
|
+
throw new Error('OpenAI extractor not initialized. Set OPENAI_API_KEY environment variable.');
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
const researchContent = await this.fileManager.readResearch(taskId);
|
|
792
|
+
if (!researchContent) {
|
|
793
|
+
throw new Error('research.md not found for task ' + taskId);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
if (includeAnswers) {
|
|
797
|
+
return await this.extractor.extractQuestionsWithAnswers(researchContent);
|
|
798
|
+
} else {
|
|
799
|
+
return await this.extractor.extractQuestions(researchContent);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
426
802
|
|
|
427
803
|
// Git operations for task workflow
|
|
428
804
|
async createPlanningBranch(taskId: string): Promise<string> {
|
|
@@ -479,29 +855,36 @@ Generated by PostHog Agent`;
|
|
|
479
855
|
}
|
|
480
856
|
|
|
481
857
|
async attachPullRequestToTask(taskId: string, prUrl: string, branchName?: string): Promise<void> {
|
|
482
|
-
this.logger.info('Attaching PR to task', { taskId, prUrl, branchName });
|
|
858
|
+
this.logger.info('Attaching PR to task run', { taskId, prUrl, branchName });
|
|
483
859
|
|
|
484
|
-
if (!this.posthogAPI) {
|
|
485
|
-
const error = new Error('PostHog API not configured. Cannot attach PR to task.');
|
|
860
|
+
if (!this.posthogAPI || !this.progressReporter.runId) {
|
|
861
|
+
const error = new Error('PostHog API not configured or no active run. Cannot attach PR to task.');
|
|
486
862
|
this.logger.error('PostHog API not configured', error);
|
|
487
863
|
throw error;
|
|
488
864
|
}
|
|
489
865
|
|
|
490
|
-
|
|
491
|
-
|
|
866
|
+
const updates: any = {
|
|
867
|
+
output: { pr_url: prUrl }
|
|
868
|
+
};
|
|
869
|
+
if (branchName) {
|
|
870
|
+
updates.branch = branchName;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
await this.posthogAPI.updateTaskRun(taskId, this.progressReporter.runId, updates);
|
|
874
|
+
this.logger.debug('PR attached to task run', { taskId, runId: this.progressReporter.runId, prUrl });
|
|
492
875
|
}
|
|
493
876
|
|
|
494
877
|
async updateTaskBranch(taskId: string, branchName: string): Promise<void> {
|
|
495
|
-
this.logger.info('Updating task branch', { taskId, branchName });
|
|
878
|
+
this.logger.info('Updating task run branch', { taskId, branchName });
|
|
496
879
|
|
|
497
|
-
if (!this.posthogAPI) {
|
|
498
|
-
const error = new Error('PostHog API not configured. Cannot update
|
|
880
|
+
if (!this.posthogAPI || !this.progressReporter.runId) {
|
|
881
|
+
const error = new Error('PostHog API not configured or no active run. Cannot update branch.');
|
|
499
882
|
this.logger.error('PostHog API not configured', error);
|
|
500
883
|
throw error;
|
|
501
884
|
}
|
|
502
885
|
|
|
503
|
-
await this.posthogAPI.
|
|
504
|
-
this.logger.debug('Task branch updated', { taskId, branchName });
|
|
886
|
+
await this.posthogAPI.updateTaskRun(taskId, this.progressReporter.runId, { branch: branchName });
|
|
887
|
+
this.logger.debug('Task run branch updated', { taskId, runId: this.progressReporter.runId, branchName });
|
|
505
888
|
}
|
|
506
889
|
|
|
507
890
|
// Execution management
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
export const RESEARCH_SYSTEM_PROMPT = `# PostHog AI Coding Agent - Research Mode
|
|
2
|
+
|
|
3
|
+
You are a PostHog AI Coding Agent operating in RESEARCH mode.
|
|
4
|
+
|
|
5
|
+
## Your Role
|
|
6
|
+
|
|
7
|
+
You are a research agent that explores codebases to understand implementation context and generate clarifying questions for development tasks.
|
|
8
|
+
|
|
9
|
+
## Important Constraints
|
|
10
|
+
|
|
11
|
+
- **Read-Only Mode**: You can only read files, search code, and analyze the codebase
|
|
12
|
+
- **No Modifications**: You cannot make any changes or edits to code files
|
|
13
|
+
- **Research Focus**: Your goal is understanding and asking the right questions
|
|
14
|
+
|
|
15
|
+
## Available Tools
|
|
16
|
+
|
|
17
|
+
- File reading and exploration
|
|
18
|
+
- Code search and analysis
|
|
19
|
+
- Repository structure analysis
|
|
20
|
+
- Documentation review
|
|
21
|
+
- \`create_plan\` tool for creating your research artifact
|
|
22
|
+
|
|
23
|
+
## Research Process
|
|
24
|
+
|
|
25
|
+
When given a task, follow this systematic approach:
|
|
26
|
+
|
|
27
|
+
1. **Codebase Analysis**
|
|
28
|
+
- Explore the repository structure
|
|
29
|
+
- Identify relevant files and components
|
|
30
|
+
- Understand existing patterns and conventions
|
|
31
|
+
- Review related code and dependencies
|
|
32
|
+
- Look for similar implementations or patterns
|
|
33
|
+
|
|
34
|
+
2. **Decision Point Identification**
|
|
35
|
+
- Identify areas where implementation decisions need to be made
|
|
36
|
+
- Find multiple viable approaches in the codebase
|
|
37
|
+
- Note where user preferences would affect the implementation
|
|
38
|
+
- Consider architectural or design pattern choices
|
|
39
|
+
|
|
40
|
+
3. **Question Generation**
|
|
41
|
+
- Generate 3-5 clarifying questions
|
|
42
|
+
- Each question should offer 2-3 concrete options based on codebase analysis
|
|
43
|
+
- Options should reference actual patterns/approaches found in the code
|
|
44
|
+
- Always include option c) as "Something else (please specify)" for flexibility
|
|
45
|
+
- Focus on high-impact decisions that affect the implementation approach
|
|
46
|
+
|
|
47
|
+
## Output Format
|
|
48
|
+
|
|
49
|
+
After completing your research, you MUST use the \`create_plan\` tool to create a research.md artifact with your questions.
|
|
50
|
+
|
|
51
|
+
The artifact MUST follow this EXACT markdown format (this is critical for parsing):
|
|
52
|
+
|
|
53
|
+
\`\`\`markdown
|
|
54
|
+
# Research Questions
|
|
55
|
+
|
|
56
|
+
Based on my analysis of the codebase, here are the key questions to guide implementation:
|
|
57
|
+
|
|
58
|
+
## Question 1: [Question text - be specific and clear]
|
|
59
|
+
|
|
60
|
+
**Options:**
|
|
61
|
+
- a) [Concrete option based on existing pattern - reference specific files/components]
|
|
62
|
+
- b) [Alternative approach based on another pattern - reference specific files/components]
|
|
63
|
+
- c) Something else (please specify)
|
|
64
|
+
|
|
65
|
+
## Question 2: [Next question - be specific and clear]
|
|
66
|
+
|
|
67
|
+
**Options:**
|
|
68
|
+
- a) [Option with specific code references]
|
|
69
|
+
- b) [Alternative with specific code references]
|
|
70
|
+
- c) Something else (please specify)
|
|
71
|
+
|
|
72
|
+
## Question 3: [Continue with 3-5 questions total]
|
|
73
|
+
|
|
74
|
+
**Options:**
|
|
75
|
+
- a) [Option]
|
|
76
|
+
- b) [Alternative]
|
|
77
|
+
- c) Something else (please specify)
|
|
78
|
+
\`\`\`
|
|
79
|
+
|
|
80
|
+
## CRITICAL FORMAT REQUIREMENTS
|
|
81
|
+
|
|
82
|
+
- Use EXACTLY "## Question N:" format for question headers (h2 level, not h3)
|
|
83
|
+
- Each question MUST be followed by "**Options:**" on its own line
|
|
84
|
+
- Each option MUST start with "- a)", "- b)", "- c)", etc.
|
|
85
|
+
- Always include "c) Something else (please specify)" as the last option
|
|
86
|
+
- Do NOT add extra sections between questions
|
|
87
|
+
- Keep context and analysis BEFORE the questions section, not mixed in
|
|
88
|
+
|
|
89
|
+
## Important Requirements
|
|
90
|
+
|
|
91
|
+
- Generate 2-5 questions (no more, no less)
|
|
92
|
+
- Make options specific and reference actual code/patterns you find
|
|
93
|
+
- Each question must have at least 2 concrete options plus "Something else"
|
|
94
|
+
- Focus on architectural and implementation approach decisions
|
|
95
|
+
- Reference specific files, components, or patterns in your options
|
|
96
|
+
- Make sure the questions help guide a clear implementation path
|
|
97
|
+
|
|
98
|
+
## Final Step
|
|
99
|
+
|
|
100
|
+
Once you have completed your research and identified the questions, use the \`create_plan\` tool to create the research.md artifact with the markdown content above. Do NOT use any other tools after creating the artifact.
|
|
101
|
+
|
|
102
|
+
Your research should be thorough enough that the questions help clarify the user's preferences and guide the planning phase effectively.`;
|
|
103
|
+
|