@posthog/agent 1.5.0 → 1.7.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.
@@ -1,32 +1,246 @@
1
- import type { Task } from './types.js';
1
+ import type { Task, UrlMention, PostHogResource } from './types.js';
2
2
  import type { TemplateVariables } from './template-manager.js';
3
3
  import { Logger } from './utils/logger.js';
4
+ import { promises as fs } from 'fs';
5
+ import { join } from 'path';
4
6
 
5
7
  export interface PromptBuilderDeps {
6
8
  getTaskFiles: (taskId: string) => Promise<any[]>;
7
9
  generatePlanTemplate: (vars: TemplateVariables) => Promise<string>;
10
+ posthogClient?: { fetchResourceByUrl: (mention: UrlMention) => Promise<PostHogResource> };
8
11
  logger?: Logger;
9
12
  }
10
13
 
11
14
  export class PromptBuilder {
12
15
  private getTaskFiles: PromptBuilderDeps['getTaskFiles'];
13
16
  private generatePlanTemplate: PromptBuilderDeps['generatePlanTemplate'];
17
+ private posthogClient?: PromptBuilderDeps['posthogClient'];
14
18
  private logger: Logger;
15
19
 
16
20
  constructor(deps: PromptBuilderDeps) {
17
21
  this.getTaskFiles = deps.getTaskFiles;
18
22
  this.generatePlanTemplate = deps.generatePlanTemplate;
23
+ this.posthogClient = deps.posthogClient;
19
24
  this.logger = deps.logger || new Logger({ debug: false, prefix: '[PromptBuilder]' });
20
25
  }
21
26
 
22
- async buildPlanningPrompt(task: Task): Promise<string> {
27
+ /**
28
+ * Extract file paths from XML tags in description
29
+ * Format: <file path="relative/path.ts" />
30
+ */
31
+ private extractFilePaths(description: string): string[] {
32
+ const fileTagRegex = /<file\s+path="([^"]+)"\s*\/>/g;
33
+ const paths: string[] = [];
34
+ let match: RegExpExecArray | null;
35
+
36
+ while ((match = fileTagRegex.exec(description)) !== null) {
37
+ paths.push(match[1]);
38
+ }
39
+
40
+ return paths;
41
+ }
42
+
43
+ /**
44
+ * Read file contents from repository
45
+ */
46
+ private async readFileContent(repositoryPath: string, filePath: string): Promise<string | null> {
47
+ try {
48
+ const fullPath = join(repositoryPath, filePath);
49
+ const content = await fs.readFile(fullPath, 'utf8');
50
+ return content;
51
+ } catch (error) {
52
+ this.logger.warn(`Failed to read referenced file: ${filePath}`, { error });
53
+ return null;
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Extract URL mentions from XML tags in description
59
+ * Formats: <error id="..." />, <experiment id="..." />, <url href="..." />
60
+ */
61
+ private extractUrlMentions(description: string): UrlMention[] {
62
+ const mentions: UrlMention[] = [];
63
+
64
+ // PostHog resource mentions: <error id="..." />, <experiment id="..." />, etc.
65
+ const resourceRegex = /<(error|experiment|insight|feature_flag)\s+id="([^"]+)"\s*\/>/g;
66
+ let match: RegExpExecArray | null;
67
+
68
+ while ((match = resourceRegex.exec(description)) !== null) {
69
+ const [, type, id] = match;
70
+ mentions.push({
71
+ url: '', // Will be reconstructed if needed
72
+ type: type as any,
73
+ id,
74
+ label: this.generateUrlLabel('', type as any),
75
+ });
76
+ }
77
+
78
+ // Generic URL mentions: <url href="..." />
79
+ const urlRegex = /<url\s+href="([^"]+)"\s*\/>/g;
80
+ while ((match = urlRegex.exec(description)) !== null) {
81
+ const [, url] = match;
82
+ mentions.push({
83
+ url,
84
+ type: 'generic',
85
+ label: this.generateUrlLabel(url, 'generic'),
86
+ });
87
+ }
88
+
89
+ return mentions;
90
+ }
91
+
92
+ /**
93
+ * Generate a display label for a URL mention
94
+ */
95
+ private generateUrlLabel(url: string, type: string): string {
96
+ try {
97
+ const urlObj = new URL(url);
98
+ switch (type) {
99
+ case 'error':
100
+ const errorMatch = url.match(/error_tracking\/([a-f0-9-]+)/);
101
+ return errorMatch ? `Error ${errorMatch[1].slice(0, 8)}...` : 'Error';
102
+ case 'experiment':
103
+ const expMatch = url.match(/experiments\/(\d+)/);
104
+ return expMatch ? `Experiment #${expMatch[1]}` : 'Experiment';
105
+ case 'insight':
106
+ return 'Insight';
107
+ case 'feature_flag':
108
+ return 'Feature Flag';
109
+ default:
110
+ return urlObj.hostname;
111
+ }
112
+ } catch {
113
+ return 'URL';
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Process URL references and fetch their content
119
+ */
120
+ private async processUrlReferences(
121
+ description: string
122
+ ): Promise<{ description: string; referencedResources: PostHogResource[] }> {
123
+ const urlMentions = this.extractUrlMentions(description);
124
+ const referencedResources: PostHogResource[] = [];
125
+
126
+ if (urlMentions.length === 0 || !this.posthogClient) {
127
+ return { description, referencedResources };
128
+ }
129
+
130
+ // Fetch all referenced resources
131
+ for (const mention of urlMentions) {
132
+ try {
133
+ const resource = await this.posthogClient.fetchResourceByUrl(mention);
134
+ referencedResources.push(resource);
135
+ } catch (error) {
136
+ this.logger.warn(`Failed to fetch resource from URL: ${mention.url}`, { error });
137
+ // Add a placeholder resource for failed fetches
138
+ referencedResources.push({
139
+ type: mention.type,
140
+ id: mention.id || '',
141
+ url: mention.url,
142
+ title: mention.label || 'Unknown Resource',
143
+ content: `Failed to fetch resource from ${mention.url}: ${error}`,
144
+ metadata: {},
145
+ });
146
+ }
147
+ }
148
+
149
+ // Replace URL tags with just the label for readability
150
+ let processedDescription = description;
151
+ for (const mention of urlMentions) {
152
+ if (mention.type === 'generic') {
153
+ // Generic URLs: <url href="..." />
154
+ const escapedUrl = mention.url.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
155
+ processedDescription = processedDescription.replace(
156
+ new RegExp(`<url\\s+href="${escapedUrl}"\\s*/>`, 'g'),
157
+ `@${mention.label}`
158
+ );
159
+ } else {
160
+ // PostHog resources: <error id="..." />, <experiment id="..." />, etc.
161
+ const escapedType = mention.type.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
162
+ const escapedId = mention.id ? mention.id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') : '';
163
+ processedDescription = processedDescription.replace(
164
+ new RegExp(`<${escapedType}\\s+id="${escapedId}"\\s*/>`, 'g'),
165
+ `@${mention.label}`
166
+ );
167
+ }
168
+ }
169
+
170
+ return { description: processedDescription, referencedResources };
171
+ }
172
+
173
+ /**
174
+ * Process description to extract file tags and read contents
175
+ * Returns processed description and referenced file contents
176
+ */
177
+ private async processFileReferences(
178
+ description: string,
179
+ repositoryPath?: string
180
+ ): Promise<{ description: string; referencedFiles: Array<{ path: string; content: string }> }> {
181
+ const filePaths = this.extractFilePaths(description);
182
+ const referencedFiles: Array<{ path: string; content: string }> = [];
183
+
184
+ if (filePaths.length === 0 || !repositoryPath) {
185
+ return { description, referencedFiles };
186
+ }
187
+
188
+ // Read all referenced files
189
+ for (const filePath of filePaths) {
190
+ const content = await this.readFileContent(repositoryPath, filePath);
191
+ if (content !== null) {
192
+ referencedFiles.push({ path: filePath, content });
193
+ }
194
+ }
195
+
196
+ // Replace file tags with just the filename for readability
197
+ let processedDescription = description;
198
+ for (const filePath of filePaths) {
199
+ const fileName = filePath.split('/').pop() || filePath;
200
+ processedDescription = processedDescription.replace(
201
+ new RegExp(`<file\\s+path="${filePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}"\\s*/>`, 'g'),
202
+ `@${fileName}`
203
+ );
204
+ }
205
+
206
+ return { description: processedDescription, referencedFiles };
207
+ }
208
+
209
+ async buildPlanningPrompt(task: Task, repositoryPath?: string): Promise<string> {
210
+ // Process file references in description
211
+ const { description: descriptionAfterFiles, referencedFiles } = await this.processFileReferences(
212
+ task.description,
213
+ repositoryPath
214
+ );
215
+
216
+ // Process URL references in description
217
+ const { description: processedDescription, referencedResources } = await this.processUrlReferences(
218
+ descriptionAfterFiles
219
+ );
220
+
23
221
  let prompt = '';
24
- prompt += `## Current Task\n\n**Task**: ${task.title}\n**Description**: ${task.description}`;
222
+ prompt += `## Current Task\n\n**Task**: ${task.title}\n**Description**: ${processedDescription}`;
25
223
 
26
224
  if ((task as any).primary_repository) {
27
225
  prompt += `\n**Repository**: ${(task as any).primary_repository}`;
28
226
  }
29
227
 
228
+ // Add referenced files from @ mentions
229
+ if (referencedFiles.length > 0) {
230
+ prompt += `\n\n## Referenced Files\n\n`;
231
+ for (const file of referencedFiles) {
232
+ prompt += `### ${file.path}\n\`\`\`\n${file.content}\n\`\`\`\n\n`;
233
+ }
234
+ }
235
+
236
+ // Add referenced resources from URL mentions
237
+ if (referencedResources.length > 0) {
238
+ prompt += `\n\n## Referenced Resources\n\n`;
239
+ for (const resource of referencedResources) {
240
+ prompt += `### ${resource.title} (${resource.type})\n**URL**: ${resource.url}\n\n${resource.content}\n\n`;
241
+ }
242
+ }
243
+
30
244
  try {
31
245
  const taskFiles = await this.getTaskFiles(task.id);
32
246
  const contextFiles = taskFiles.filter((f: any) => f.type === 'context' || f.type === 'reference');
@@ -43,7 +257,7 @@ export class PromptBuilder {
43
257
  const templateVariables = {
44
258
  task_id: task.id,
45
259
  task_title: task.title,
46
- task_description: task.description,
260
+ task_description: processedDescription,
47
261
  date: new Date().toISOString().split('T')[0],
48
262
  repository: ((task as any).primary_repository || '') as string,
49
263
  };
@@ -55,14 +269,41 @@ export class PromptBuilder {
55
269
  return prompt;
56
270
  }
57
271
 
58
- async buildExecutionPrompt(task: Task): Promise<string> {
272
+ async buildExecutionPrompt(task: Task, repositoryPath?: string): Promise<string> {
273
+ // Process file references in description
274
+ const { description: descriptionAfterFiles, referencedFiles } = await this.processFileReferences(
275
+ task.description,
276
+ repositoryPath
277
+ );
278
+
279
+ // Process URL references in description
280
+ const { description: processedDescription, referencedResources } = await this.processUrlReferences(
281
+ descriptionAfterFiles
282
+ );
283
+
59
284
  let prompt = '';
60
- prompt += `## Current Task\n\n**Task**: ${task.title}\n**Description**: ${task.description}`;
285
+ prompt += `## Current Task\n\n**Task**: ${task.title}\n**Description**: ${processedDescription}`;
61
286
 
62
287
  if ((task as any).primary_repository) {
63
288
  prompt += `\n**Repository**: ${(task as any).primary_repository}`;
64
289
  }
65
290
 
291
+ // Add referenced files from @ mentions
292
+ if (referencedFiles.length > 0) {
293
+ prompt += `\n\n## Referenced Files\n\n`;
294
+ for (const file of referencedFiles) {
295
+ prompt += `### ${file.path}\n\`\`\`\n${file.content}\n\`\`\`\n\n`;
296
+ }
297
+ }
298
+
299
+ // Add referenced resources from URL mentions
300
+ if (referencedResources.length > 0) {
301
+ prompt += `\n\n## Referenced Resources\n\n`;
302
+ for (const resource of referencedResources) {
303
+ prompt += `### ${resource.title} (${resource.type})\n**URL**: ${resource.url}\n\n${resource.content}\n\n`;
304
+ }
305
+ }
306
+
66
307
  try {
67
308
  const taskFiles = await this.getTaskFiles(task.id);
68
309
  const hasPlan = taskFiles.some((f: any) => f.type === 'plan');
@@ -71,7 +71,7 @@ export class StageExecutor {
71
71
  }
72
72
 
73
73
  private async runPlanning(task: Task, cwd: string, options: WorkflowExecutionOptions, stageKey: string): Promise<WorkflowStageExecutionResult> {
74
- const contextPrompt = await this.promptBuilder.buildPlanningPrompt(task);
74
+ const contextPrompt = await this.promptBuilder.buildPlanningPrompt(task, cwd);
75
75
  let prompt = PLANNING_SYSTEM_PROMPT + '\n\n' + contextPrompt;
76
76
 
77
77
  const stageOverrides = options.stageOverrides?.[stageKey] || options.stageOverrides?.['plan'];
@@ -118,7 +118,7 @@ export class StageExecutor {
118
118
  }
119
119
 
120
120
  private async runExecution(task: Task, cwd: string, permissionMode: WorkflowExecutionOptions['permissionMode'], options: WorkflowExecutionOptions, stageKey: string): Promise<WorkflowStageExecutionResult> {
121
- const contextPrompt = await this.promptBuilder.buildExecutionPrompt(task);
121
+ const contextPrompt = await this.promptBuilder.buildExecutionPrompt(task, cwd);
122
122
  let prompt = EXECUTION_SYSTEM_PROMPT + '\n\n' + contextPrompt;
123
123
 
124
124
  const stageOverrides = options.stageOverrides?.[stageKey];
package/src/types.ts CHANGED
@@ -256,4 +256,23 @@ export interface AgentConfig {
256
256
  export interface PostHogAPIConfig {
257
257
  apiUrl: string;
258
258
  apiKey: string;
259
+ }
260
+
261
+ // URL mention types
262
+ export type ResourceType = 'error' | 'experiment' | 'insight' | 'feature_flag' | 'generic';
263
+
264
+ export interface PostHogResource {
265
+ type: ResourceType;
266
+ id: string;
267
+ url: string;
268
+ title?: string;
269
+ content: string;
270
+ metadata?: Record<string, any>;
271
+ }
272
+
273
+ export interface UrlMention {
274
+ url: string;
275
+ type: ResourceType;
276
+ id?: string;
277
+ label?: string;
259
278
  }