@posthog/agent 2.0.0 → 2.0.2

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 (131) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +221 -219
  3. package/dist/adapters/claude/conversion/tool-use-to-acp.d.ts +21 -0
  4. package/dist/adapters/claude/conversion/tool-use-to-acp.js +547 -0
  5. package/dist/adapters/claude/conversion/tool-use-to-acp.js.map +1 -0
  6. package/dist/adapters/claude/permissions/permission-options.d.ts +13 -0
  7. package/dist/adapters/claude/permissions/permission-options.js +117 -0
  8. package/dist/adapters/claude/permissions/permission-options.js.map +1 -0
  9. package/dist/adapters/claude/questions/utils.d.ts +132 -0
  10. package/dist/adapters/claude/questions/utils.js +63 -0
  11. package/dist/adapters/claude/questions/utils.js.map +1 -0
  12. package/dist/adapters/claude/tools.d.ts +18 -0
  13. package/dist/adapters/claude/tools.js +95 -0
  14. package/dist/adapters/claude/tools.js.map +1 -0
  15. package/dist/agent-DBQY1BfC.d.ts +123 -0
  16. package/dist/agent.d.ts +5 -0
  17. package/dist/agent.js +3656 -0
  18. package/dist/agent.js.map +1 -0
  19. package/dist/claude-cli/cli.js +3695 -2746
  20. package/dist/claude-cli/vendor/ripgrep/COPYING +3 -0
  21. package/dist/claude-cli/vendor/ripgrep/arm64-darwin/rg +0 -0
  22. package/dist/claude-cli/vendor/ripgrep/arm64-darwin/ripgrep.node +0 -0
  23. package/dist/claude-cli/vendor/ripgrep/arm64-linux/rg +0 -0
  24. package/dist/claude-cli/vendor/ripgrep/arm64-linux/ripgrep.node +0 -0
  25. package/dist/claude-cli/vendor/ripgrep/x64-darwin/rg +0 -0
  26. package/dist/claude-cli/vendor/ripgrep/x64-darwin/ripgrep.node +0 -0
  27. package/dist/claude-cli/vendor/ripgrep/x64-linux/rg +0 -0
  28. package/dist/claude-cli/vendor/ripgrep/x64-linux/ripgrep.node +0 -0
  29. package/dist/claude-cli/vendor/ripgrep/x64-win32/rg.exe +0 -0
  30. package/dist/claude-cli/vendor/ripgrep/x64-win32/ripgrep.node +0 -0
  31. package/dist/gateway-models.d.ts +24 -0
  32. package/dist/gateway-models.js +93 -0
  33. package/dist/gateway-models.js.map +1 -0
  34. package/dist/index.d.ts +170 -1157
  35. package/dist/index.js +9373 -5135
  36. package/dist/index.js.map +1 -1
  37. package/dist/logger-DDBiMOOD.d.ts +24 -0
  38. package/dist/posthog-api.d.ts +40 -0
  39. package/dist/posthog-api.js +175 -0
  40. package/dist/posthog-api.js.map +1 -0
  41. package/dist/server/agent-server.d.ts +41 -0
  42. package/dist/server/agent-server.js +10503 -0
  43. package/dist/server/agent-server.js.map +1 -0
  44. package/dist/server/bin.d.ts +1 -0
  45. package/dist/server/bin.js +10558 -0
  46. package/dist/server/bin.js.map +1 -0
  47. package/dist/types.d.ts +129 -0
  48. package/dist/types.js +1 -0
  49. package/dist/types.js.map +1 -0
  50. package/package.json +65 -13
  51. package/src/acp-extensions.ts +98 -16
  52. package/src/adapters/acp-connection.ts +494 -0
  53. package/src/adapters/base-acp-agent.ts +150 -0
  54. package/src/adapters/claude/claude-agent.ts +596 -0
  55. package/src/adapters/claude/conversion/acp-to-sdk.ts +102 -0
  56. package/src/adapters/claude/conversion/sdk-to-acp.ts +571 -0
  57. package/src/adapters/claude/conversion/tool-use-to-acp.ts +618 -0
  58. package/src/adapters/claude/hooks.ts +64 -0
  59. package/src/adapters/claude/mcp/tool-metadata.ts +102 -0
  60. package/src/adapters/claude/permissions/permission-handlers.ts +433 -0
  61. package/src/adapters/claude/permissions/permission-options.ts +103 -0
  62. package/src/adapters/claude/plan/utils.ts +56 -0
  63. package/src/adapters/claude/questions/utils.ts +92 -0
  64. package/src/adapters/claude/session/commands.ts +38 -0
  65. package/src/adapters/claude/session/mcp-config.ts +37 -0
  66. package/src/adapters/claude/session/models.ts +12 -0
  67. package/src/adapters/claude/session/options.ts +236 -0
  68. package/src/adapters/claude/tool-meta.ts +143 -0
  69. package/src/adapters/claude/tools.ts +53 -688
  70. package/src/adapters/claude/types.ts +61 -0
  71. package/src/adapters/codex/spawn.ts +130 -0
  72. package/src/agent.ts +96 -587
  73. package/src/execution-mode.ts +43 -0
  74. package/src/gateway-models.ts +135 -0
  75. package/src/index.ts +79 -0
  76. package/src/otel-log-writer.test.ts +105 -0
  77. package/src/otel-log-writer.ts +94 -0
  78. package/src/posthog-api.ts +75 -235
  79. package/src/resume.ts +115 -0
  80. package/src/sagas/apply-snapshot-saga.test.ts +690 -0
  81. package/src/sagas/apply-snapshot-saga.ts +88 -0
  82. package/src/sagas/capture-tree-saga.test.ts +892 -0
  83. package/src/sagas/capture-tree-saga.ts +141 -0
  84. package/src/sagas/resume-saga.test.ts +558 -0
  85. package/src/sagas/resume-saga.ts +332 -0
  86. package/src/sagas/test-fixtures.ts +250 -0
  87. package/src/server/agent-server.test.ts +220 -0
  88. package/src/server/agent-server.ts +748 -0
  89. package/src/server/bin.ts +88 -0
  90. package/src/server/jwt.ts +65 -0
  91. package/src/server/schemas.ts +47 -0
  92. package/src/server/types.ts +13 -0
  93. package/src/server/utils/retry.test.ts +122 -0
  94. package/src/server/utils/retry.ts +61 -0
  95. package/src/server/utils/sse-parser.test.ts +93 -0
  96. package/src/server/utils/sse-parser.ts +46 -0
  97. package/src/session-log-writer.test.ts +140 -0
  98. package/src/session-log-writer.ts +137 -0
  99. package/src/test/assertions.ts +114 -0
  100. package/src/test/controllers/sse-controller.ts +107 -0
  101. package/src/test/fixtures/api.ts +111 -0
  102. package/src/test/fixtures/config.ts +33 -0
  103. package/src/test/fixtures/notifications.ts +92 -0
  104. package/src/test/mocks/claude-sdk.ts +251 -0
  105. package/src/test/mocks/msw-handlers.ts +48 -0
  106. package/src/test/setup.ts +114 -0
  107. package/src/test/wait.ts +41 -0
  108. package/src/tree-tracker.ts +173 -0
  109. package/src/types.ts +54 -137
  110. package/src/utils/acp-content.ts +58 -0
  111. package/src/utils/async-mutex.test.ts +104 -0
  112. package/src/utils/async-mutex.ts +31 -0
  113. package/src/utils/common.ts +15 -0
  114. package/src/utils/gateway.ts +9 -6
  115. package/src/utils/logger.ts +0 -30
  116. package/src/utils/streams.ts +220 -0
  117. package/CLAUDE.md +0 -331
  118. package/src/adapters/claude/claude.ts +0 -1947
  119. package/src/adapters/claude/mcp-server.ts +0 -810
  120. package/src/adapters/claude/utils.ts +0 -267
  121. package/src/adapters/connection.ts +0 -95
  122. package/src/file-manager.ts +0 -273
  123. package/src/git-manager.ts +0 -577
  124. package/src/schemas.ts +0 -241
  125. package/src/session-store.ts +0 -259
  126. package/src/task-manager.ts +0 -163
  127. package/src/todo-manager.ts +0 -180
  128. package/src/tools/registry.ts +0 -134
  129. package/src/tools/types.ts +0 -133
  130. package/src/utils/tapped-stream.ts +0 -60
  131. package/src/worktree-manager.ts +0 -974
package/src/agent.ts CHANGED
@@ -1,299 +1,142 @@
1
- import {
2
- type Client,
3
- ClientSideConnection,
4
- type ContentBlock,
5
- ndJsonStream,
6
- PROTOCOL_VERSION,
7
- } from "@agentclientprotocol/sdk";
8
- import { POSTHOG_NOTIFICATIONS } from "./acp-extensions.js";
9
1
  import {
10
2
  createAcpConnection,
11
3
  type InProcessAcpConnection,
12
- } from "./adapters/connection.js";
13
- import { PostHogFileManager } from "./file-manager.js";
14
- import { GitManager } from "./git-manager.js";
4
+ } from "./adapters/acp-connection.js";
5
+ import {
6
+ BLOCKED_MODELS,
7
+ DEFAULT_GATEWAY_MODEL,
8
+ fetchArrayModels,
9
+ } from "./gateway-models.js";
15
10
  import { PostHogAPIClient } from "./posthog-api.js";
16
- import { SessionStore } from "./session-store.js";
17
- import { TaskManager } from "./task-manager.js";
18
- import type {
19
- AgentConfig,
20
- CanUseTool,
21
- StoredNotification,
22
- Task,
23
- TaskExecutionOptions,
24
- } from "./types.js";
11
+ import { SessionLogWriter } from "./session-log-writer.js";
12
+ import type { AgentConfig, TaskExecutionOptions } from "./types.js";
25
13
  import { Logger } from "./utils/logger.js";
26
14
 
27
- /**
28
- * Type for sending ACP notifications
29
- */
30
- type SendNotification = (
31
- method: string,
32
- params: Record<string, unknown>,
33
- ) => Promise<void>;
34
-
35
15
  export class Agent {
36
- private workingDirectory: string;
37
- private taskManager: TaskManager;
38
16
  private posthogAPI?: PostHogAPIClient;
39
- private fileManager: PostHogFileManager;
40
- private gitManager: GitManager;
41
17
  private logger: Logger;
42
18
  private acpConnection?: InProcessAcpConnection;
43
- private mcpServers?: Record<string, any>;
44
- private canUseTool?: CanUseTool;
45
- private currentRunId?: string;
46
- private sessionStore?: SessionStore;
19
+ private taskRunId?: string;
20
+ private sessionLogWriter?: SessionLogWriter;
47
21
  public debug: boolean;
48
22
 
49
23
  constructor(config: AgentConfig) {
50
- this.workingDirectory = config.workingDirectory || process.cwd();
51
- this.canUseTool = config.canUseTool;
52
24
  this.debug = config.debug || false;
53
-
54
- // Build default PostHog MCP server configuration
55
- const posthogMcpUrl =
56
- config.posthogMcpUrl ||
57
- process.env.POSTHOG_MCP_URL ||
58
- "https://mcp.posthog.com/mcp";
59
-
60
- // Add auth if API key provided
61
- const headers: Record<string, string> = {};
62
- if (config.getPosthogApiKey) {
63
- headers.Authorization = `Bearer ${config.getPosthogApiKey()}`;
64
- }
65
-
66
- const defaultMcpServers = {
67
- posthog: {
68
- type: "http" as const,
69
- url: posthogMcpUrl,
70
- ...(Object.keys(headers).length > 0 ? { headers } : {}),
71
- },
72
- };
73
-
74
- // Merge default PostHog MCP with user-provided servers (user config takes precedence)
75
- this.mcpServers = {
76
- ...defaultMcpServers,
77
- ...config.mcpServers,
78
- };
79
25
  this.logger = new Logger({
80
26
  debug: this.debug,
81
27
  prefix: "[PostHog Agent]",
82
28
  onLog: config.onLog,
83
29
  });
84
- this.taskManager = new TaskManager();
85
30
 
86
- this.fileManager = new PostHogFileManager(
87
- this.workingDirectory,
88
- this.logger.child("FileManager"),
89
- );
90
- this.gitManager = new GitManager({
91
- repositoryPath: this.workingDirectory,
92
- logger: this.logger.child("GitManager"),
93
- });
31
+ if (config.posthog) {
32
+ this.posthogAPI = new PostHogAPIClient(config.posthog);
33
+ }
94
34
 
95
- if (
96
- config.posthogApiUrl &&
97
- config.getPosthogApiKey &&
98
- config.posthogProjectId
99
- ) {
100
- this.posthogAPI = new PostHogAPIClient({
101
- apiUrl: config.posthogApiUrl,
102
- getApiKey: config.getPosthogApiKey,
103
- projectId: config.posthogProjectId,
35
+ if (config.otelTransport) {
36
+ // OTEL pipeline: use OtelLogWriter only (no S3 writer)
37
+ this.sessionLogWriter = new SessionLogWriter({
38
+ otelConfig: {
39
+ posthogHost: config.otelTransport.host,
40
+ apiKey: config.otelTransport.apiKey,
41
+ logsPath: config.otelTransport.logsPath,
42
+ },
43
+ logger: this.logger.child("SessionLogWriter"),
44
+ });
45
+ } else if (config.posthog) {
46
+ // Legacy: use S3 writer via PostHog API
47
+ this.sessionLogWriter = new SessionLogWriter({
48
+ posthogAPI: this.posthogAPI,
49
+ logger: this.logger.child("SessionLogWriter"),
104
50
  });
105
-
106
- // Create SessionStore from the API client for ACP connection
107
- this.sessionStore = new SessionStore(
108
- this.posthogAPI,
109
- this.logger.child("SessionStore"),
110
- );
111
51
  }
112
52
  }
113
53
 
114
- /**
115
- * Enable or disable debug logging
116
- */
117
- setDebug(enabled: boolean) {
118
- this.debug = enabled;
119
- this.logger.setDebug(enabled);
120
- }
121
-
122
- /**
123
- * Configure LLM gateway environment variables for Claude Code CLI.
124
- */
125
- private async _configureLlmGateway(): Promise<void> {
54
+ private _configureLlmGateway(_adapter?: "claude" | "codex"): {
55
+ gatewayUrl: string;
56
+ apiKey: string;
57
+ } | null {
126
58
  if (!this.posthogAPI) {
127
- return;
59
+ return null;
128
60
  }
129
61
 
130
62
  try {
131
63
  const gatewayUrl = this.posthogAPI.getLlmGatewayUrl();
132
64
  const apiKey = this.posthogAPI.getApiKey();
65
+
66
+ process.env.OPENAI_BASE_URL = `${gatewayUrl}/v1`;
67
+ process.env.OPENAI_API_KEY = apiKey;
133
68
  process.env.ANTHROPIC_BASE_URL = gatewayUrl;
134
69
  process.env.ANTHROPIC_AUTH_TOKEN = apiKey;
135
- this.ensureOpenAIGatewayEnv(gatewayUrl, apiKey);
136
- this.ensureGeminiGatewayEnv(gatewayUrl, apiKey);
70
+
71
+ return { gatewayUrl, apiKey };
137
72
  } catch (error) {
138
73
  this.logger.error("Failed to configure LLM gateway", error);
139
74
  throw error;
140
75
  }
141
76
  }
142
77
 
143
- /**
144
- * @deprecated Use runTaskV2() for local execution or runTaskCloud() for cloud execution.
145
- * This method used the old workflow system which has been removed.
146
- */
147
- async runTask(
148
- _taskId: string,
149
- _taskRunId: string,
150
- _options: import("./types.js").TaskExecutionOptions = {},
151
- ): Promise<void> {
152
- throw new Error(
153
- "runTask() is deprecated. Use runTaskV2() for local execution or runTaskCloud() for cloud execution.",
154
- );
155
- }
156
-
157
- /**
158
- * Creates an in-process ACP connection for client communication.
159
- * Sets up git branch for the task, configures LLM gateway.
160
- * The client handles all prompting/querying via the returned streams.
161
- *
162
- * @returns InProcessAcpConnection with clientStreams for the client to use
163
- */
164
- async runTaskV2(
78
+ async run(
165
79
  taskId: string,
166
80
  taskRunId: string,
167
- options: import("./types.js").TaskExecutionOptions = {},
81
+ options: TaskExecutionOptions = {},
168
82
  ): Promise<InProcessAcpConnection> {
169
- await this._configureLlmGateway();
170
-
171
- const isCloudMode = options.isCloudMode ?? false;
172
- const _cwd = options.repositoryPath || this.workingDirectory;
173
-
174
- // Use taskRunId as sessionId - they are the same identifier
175
- this.currentRunId = taskRunId;
176
-
177
- this.acpConnection = createAcpConnection({
178
- framework: options.framework,
179
- sessionStore: this.sessionStore,
180
- sessionId: taskRunId,
181
- taskId,
182
- });
183
-
184
- const sendNotification: SendNotification = async (method, params) => {
185
- this.logger.debug(`Notification: ${method}`, params);
186
- await this.acpConnection?.agentConnection.extNotification?.(
187
- method,
188
- params,
189
- );
190
- };
191
-
192
- if (!options.isReconnect) {
193
- await sendNotification(POSTHOG_NOTIFICATIONS.RUN_STARTED, {
194
- sessionId: taskRunId,
195
- runId: taskRunId,
83
+ const gatewayConfig = this._configureLlmGateway(options.adapter);
84
+
85
+ this.taskRunId = taskRunId;
86
+
87
+ let allowedModelIds: Set<string> | undefined;
88
+ let sanitizedModel =
89
+ options.model && !BLOCKED_MODELS.has(options.model)
90
+ ? options.model
91
+ : undefined;
92
+ if (options.adapter === "codex" && gatewayConfig) {
93
+ const models = await fetchArrayModels({
94
+ gatewayUrl: gatewayConfig.gatewayUrl,
196
95
  });
197
- }
96
+ const codexModelIds = models
97
+ .filter((model) => {
98
+ if (BLOCKED_MODELS.has(model.id)) return false;
99
+ if (model.owned_by) {
100
+ return model.owned_by === "openai";
101
+ }
102
+ return model.id.startsWith("gpt-") || model.id.startsWith("openai/");
103
+ })
104
+ .map((model) => model.id);
198
105
 
199
- // Only fetch task when we need the slug for git branch creation
200
- if (!options.skipGitBranch) {
201
- const task = options.task ?? (await this.fetchTask(taskId));
202
- const taskSlug = (task as any).slug || task.id;
203
- try {
204
- await this.prepareTaskBranch(taskSlug, isCloudMode, sendNotification);
205
- } catch (error) {
206
- const errorMessage =
207
- error instanceof Error ? error.message : String(error);
208
- this.logger.error("Failed to prepare task branch", {
209
- error: errorMessage,
210
- });
211
- await sendNotification(POSTHOG_NOTIFICATIONS.ERROR, {
212
- sessionId: taskRunId,
213
- message: errorMessage,
214
- });
215
- throw error;
106
+ if (codexModelIds.length > 0) {
107
+ allowedModelIds = new Set(codexModelIds);
216
108
  }
217
- }
218
-
219
- return this.acpConnection;
220
- }
221
109
 
222
- // PostHog task operations
223
- async fetchTask(taskId: string): Promise<Task> {
224
- if (!this.posthogAPI) {
225
- const error = new Error(
226
- "PostHog API not configured. Provide posthogApiUrl and posthogApiKey in constructor.",
227
- );
228
- this.logger.error("PostHog API not configured", error);
229
- throw error;
110
+ if (!sanitizedModel || !allowedModelIds?.has(sanitizedModel)) {
111
+ sanitizedModel = codexModelIds[0];
112
+ }
230
113
  }
231
- return this.posthogAPI.fetchTask(taskId);
232
- }
233
-
234
- getPostHogClient(): PostHogAPIClient | undefined {
235
- return this.posthogAPI;
236
- }
237
-
238
- /**
239
- * Send a notification to a cloud task run's S3 log.
240
- * The cloud runner will pick up new notifications via interrupt polling.
241
- */
242
- async sendNotification(
243
- taskId: string,
244
- runId: string,
245
- notification: StoredNotification,
246
- ): Promise<void> {
247
- if (!this.posthogAPI) {
248
- throw new Error(
249
- "PostHog API not configured. Cannot send notification to cloud task.",
250
- );
114
+ if (!sanitizedModel) {
115
+ sanitizedModel = DEFAULT_GATEWAY_MODEL;
251
116
  }
252
117
 
253
- await this.posthogAPI.appendTaskRunLog(taskId, runId, [notification]);
254
- this.logger.debug("Notification sent to cloud task", {
255
- taskId,
256
- runId,
257
- method: notification.notification.method,
258
- });
259
- }
260
-
261
- async getTaskFiles(taskId: string): Promise<any[]> {
262
- this.logger.debug("Getting task files", { taskId });
263
- const files = await this.fileManager.getTaskFiles(taskId);
264
- this.logger.debug("Found task files", { taskId, fileCount: files.length });
265
- return files;
266
- }
267
-
268
- async createPullRequest(
269
- taskId: string,
270
- branchName: string,
271
- taskTitle: string,
272
- taskDescription: string,
273
- customBody?: string,
274
- ): Promise<string> {
275
- this.logger.info("Creating pull request", {
118
+ this.acpConnection = createAcpConnection({
119
+ adapter: options.adapter,
120
+ logWriter: this.sessionLogWriter,
121
+ taskRunId,
276
122
  taskId,
277
- branchName,
278
- taskTitle,
123
+ deviceType: "local",
124
+ logger: this.logger,
125
+ processCallbacks: options.processCallbacks,
126
+ allowedModelIds,
127
+ codexOptions:
128
+ options.adapter === "codex" && gatewayConfig
129
+ ? {
130
+ cwd: options.repositoryPath,
131
+ apiBaseUrl: `${gatewayConfig.gatewayUrl}/v1`,
132
+ apiKey: gatewayConfig.apiKey,
133
+ binaryPath: options.codexBinaryPath,
134
+ model: sanitizedModel,
135
+ }
136
+ : undefined,
279
137
  });
280
138
 
281
- const defaultBody = `## Task Details
282
- **Task ID**: ${taskId}
283
- **Description**: ${taskDescription}
284
-
285
- ## Changes
286
- This PR implements the changes described in the task.`;
287
- const prBody = customBody || defaultBody;
288
-
289
- const prUrl = await this.gitManager.createPullRequest(
290
- branchName,
291
- taskTitle,
292
- prBody,
293
- );
294
-
295
- this.logger.info("Pull request created", { taskId, prUrl });
296
- return prUrl;
139
+ return this.acpConnection;
297
140
  }
298
141
 
299
142
  async attachPullRequestToTask(
@@ -303,7 +146,7 @@ This PR implements the changes described in the task.`;
303
146
  ): Promise<void> {
304
147
  this.logger.info("Attaching PR to task run", { taskId, prUrl, branchName });
305
148
 
306
- if (!this.posthogAPI || !this.currentRunId) {
149
+ if (!this.posthogAPI || !this.taskRunId) {
307
150
  const error = new Error(
308
151
  "PostHog API not configured or no active run. Cannot attach PR to task.",
309
152
  );
@@ -318,356 +161,22 @@ This PR implements the changes described in the task.`;
318
161
  updates.branch = branchName;
319
162
  }
320
163
 
321
- await this.posthogAPI.updateTaskRun(taskId, this.currentRunId, updates);
164
+ await this.posthogAPI.updateTaskRun(taskId, this.taskRunId, updates);
322
165
  this.logger.debug("PR attached to task run", {
323
166
  taskId,
324
- runId: this.currentRunId,
167
+ taskRunId: this.taskRunId,
325
168
  prUrl,
326
169
  });
327
170
  }
328
171
 
329
- async updateTaskBranch(taskId: string, branchName: string): Promise<void> {
330
- this.logger.info("Updating task run branch", { taskId, branchName });
331
-
332
- if (!this.posthogAPI || !this.currentRunId) {
333
- const error = new Error(
334
- "PostHog API not configured or no active run. Cannot update branch.",
335
- );
336
- this.logger.error("PostHog API not configured", error);
337
- throw error;
338
- }
339
-
340
- await this.posthogAPI.updateTaskRun(taskId, this.currentRunId, {
341
- branch: branchName,
342
- });
343
- this.logger.debug("Task run branch updated", {
344
- taskId,
345
- runId: this.currentRunId,
346
- branchName,
347
- });
348
- }
349
-
350
- // Execution management
351
- cancelTask(taskId: string): void {
352
- // Find the execution for this task and cancel it
353
- for (const [executionId, execution] of this.taskManager.executionStates) {
354
- if (execution.taskId === taskId && execution.status === "running") {
355
- this.taskManager.cancelExecution(executionId);
356
- break;
357
- }
358
- }
359
- }
360
-
361
- getTaskExecutionStatus(taskId: string): string | null {
362
- // Find the execution for this task
363
- for (const execution of this.taskManager.executionStates.values()) {
364
- if (execution.taskId === taskId) {
365
- return execution.status;
366
- }
367
- }
368
- return null;
369
- }
370
-
371
- private async prepareTaskBranch(
372
- taskSlug: string,
373
- isCloudMode: boolean,
374
- sendNotification: SendNotification,
375
- ): Promise<void> {
376
- if (await this.gitManager.hasChanges()) {
377
- throw new Error(
378
- "Cannot start task with uncommitted changes. Please commit or stash your changes first.",
379
- );
380
- }
381
-
382
- // If we're running in a worktree, we're already on the correct branch
383
- // (the worktree was created with its own branch). Skip branch creation.
384
- const isWorktree = await this.gitManager.isWorktree();
385
- if (isWorktree) {
386
- const currentBranch = await this.gitManager.getCurrentBranch();
387
- this.logger.info("Running in worktree, using existing branch", {
388
- branch: currentBranch,
389
- });
390
- await sendNotification(POSTHOG_NOTIFICATIONS.BRANCH_CREATED, {
391
- branch: currentBranch,
392
- });
393
- return;
394
- }
395
-
396
- await this.gitManager.resetToDefaultBranchIfNeeded();
397
-
398
- const existingBranch = await this.gitManager.getTaskBranch(taskSlug);
399
- if (!existingBranch) {
400
- const branchName = await this.gitManager.createTaskBranch(taskSlug);
401
- await sendNotification(POSTHOG_NOTIFICATIONS.BRANCH_CREATED, {
402
- branch: branchName,
403
- });
404
-
405
- await this.gitManager.addAllPostHogFiles();
406
-
407
- // Only commit if there are changes or we're in cloud mode
408
- if (isCloudMode) {
409
- await this.gitManager.commitAndPush(`Initialize task ${taskSlug}`, {
410
- allowEmpty: true,
411
- });
412
- } else {
413
- // Check if there are any changes before committing
414
- const hasChanges = await this.gitManager.hasStagedChanges();
415
- if (hasChanges) {
416
- await this.gitManager.commitChanges(`Initialize task ${taskSlug}`);
417
- }
418
- }
419
- } else {
420
- this.logger.info("Switching to existing task branch", {
421
- branch: existingBranch,
422
- });
423
- await this.gitManager.switchToBranch(existingBranch);
424
- }
172
+ async flushAllLogs(): Promise<void> {
173
+ await this.sessionLogWriter?.flushAll();
425
174
  }
426
175
 
427
- private ensureOpenAIGatewayEnv(gatewayUrl?: string, token?: string): void {
428
- const resolvedGatewayUrl = gatewayUrl || process.env.ANTHROPIC_BASE_URL;
429
- const resolvedToken = token || process.env.ANTHROPIC_AUTH_TOKEN;
430
-
431
- if (resolvedGatewayUrl) {
432
- process.env.OPENAI_BASE_URL = resolvedGatewayUrl;
433
- }
434
-
435
- if (resolvedToken) {
436
- process.env.OPENAI_API_KEY = resolvedToken;
437
- }
438
- }
439
-
440
- private ensureGeminiGatewayEnv(gatewayUrl?: string, token?: string): void {
441
- const resolvedGatewayUrl = gatewayUrl || process.env.ANTHROPIC_BASE_URL;
442
- const resolvedToken = token || process.env.ANTHROPIC_AUTH_TOKEN;
443
-
444
- if (resolvedGatewayUrl) {
445
- process.env.GEMINI_BASE_URL = resolvedGatewayUrl;
446
- }
447
-
448
- if (resolvedToken) {
449
- process.env.GEMINI_API_KEY = resolvedToken;
450
- }
451
- }
452
-
453
- async runTaskCloud(
454
- taskId: string,
455
- taskRunId: string,
456
- options: TaskExecutionOptions = {},
457
- ): Promise<void> {
458
- await this._configureLlmGateway();
459
-
460
- const task = await this.fetchTask(taskId);
461
- const cwd = options.repositoryPath || this.workingDirectory;
462
- const taskSlug = (task as any).slug || task.id;
463
-
464
- this.currentRunId = taskRunId;
465
-
466
- this.logger.info("Starting cloud task execution", {
467
- taskId: task.id,
468
- taskSlug,
469
- taskRunId,
470
- cwd,
471
- });
472
-
473
- if (!this.sessionStore) {
474
- throw new Error(
475
- "SessionStore required for cloud mode. Ensure PostHog API credentials are configured.",
476
- );
477
- }
478
-
479
- // Start session in SessionStore (updates task run status to in_progress)
480
- const taskRun = await this.sessionStore.start(taskRunId, taskId, taskRunId);
481
- this.logger.debug("Session started", {
482
- taskRunId,
483
- logUrl: taskRun?.log_url,
484
- });
485
-
486
- // Create internal ACP connection with S3 persistence
487
- const acpConnection = createAcpConnection({
488
- sessionStore: this.sessionStore,
489
- sessionId: taskRunId,
490
- taskId: task.id,
491
- });
492
-
493
- // Create client connection using the client-side streams
494
- const clientStream = ndJsonStream(
495
- acpConnection.clientStreams.writable as WritableStream<Uint8Array>,
496
- acpConnection.clientStreams.readable as ReadableStream<Uint8Array>,
497
- );
498
-
499
- // Create auto-approving client for headless cloud mode
500
- const cloudClient: Client = {
501
- async requestPermission(params) {
502
- const allowOption = params.options.find(
503
- (o) => o.kind === "allow_once" || o.kind === "allow_always",
504
- );
505
- return {
506
- outcome: {
507
- outcome: "selected",
508
- optionId: allowOption?.optionId ?? params.options[0].optionId,
509
- },
510
- };
511
- },
512
- async sessionUpdate(_params) {
513
- // Notifications are already being persisted to S3 via tapped streams
514
- },
515
- };
516
-
517
- const clientConnection = new ClientSideConnection(
518
- (_agent) => cloudClient,
519
- clientStream,
520
- );
521
-
522
- try {
523
- // Initialize the connection
524
- await clientConnection.initialize({
525
- protocolVersion: PROTOCOL_VERSION,
526
- clientCapabilities: {},
527
- });
528
-
529
- // Create new session
530
- await clientConnection.newSession({
531
- cwd,
532
- mcpServers: [],
533
- _meta: { sessionId: taskRunId },
534
- });
535
-
536
- // Prepare git branch if not skipped
537
- if (!options.skipGitBranch) {
538
- const sendNotification: SendNotification = async (method, params) => {
539
- this.logger.debug(`Notification: ${method}`, params);
540
- await acpConnection.agentConnection.extNotification?.(method, params);
541
- };
542
- await this.prepareTaskBranch(taskSlug, true, sendNotification);
543
- }
544
-
545
- // Build initial prompt from task description
546
- const initialPrompt: ContentBlock[] = [
547
- {
548
- type: "text",
549
- text: `# Task: ${task.title}\n\n${task.description}`,
550
- },
551
- ];
552
-
553
- // Track the last known log entry count for interrupt polling
554
- let lastKnownEntryCount = 0;
555
- let isPolling = true;
556
-
557
- // Start interrupt polling in background
558
- const pollForInterrupts = async () => {
559
- while (isPolling) {
560
- await new Promise((resolve) => setTimeout(resolve, 2000)); // Poll every 2 seconds
561
- if (!isPolling) break;
562
-
563
- try {
564
- const newEntries = await this.sessionStore?.pollForNewEntries(
565
- taskRunId,
566
- lastKnownEntryCount,
567
- );
568
-
569
- for (const entry of newEntries ?? []) {
570
- lastKnownEntryCount++;
571
- // Look for user_message notifications
572
- if (
573
- entry.notification?.method === "sessionUpdate" &&
574
- (entry.notification?.params as any)?.sessionUpdate ===
575
- "user_message"
576
- ) {
577
- const content = (entry.notification?.params as any)?.content;
578
- if (content) {
579
- this.logger.info("Processing user interrupt", { content });
580
- // Send as new prompt - will be processed after current prompt completes
581
- await clientConnection.prompt({
582
- sessionId: taskRunId,
583
- prompt: Array.isArray(content) ? content : [content],
584
- });
585
- }
586
- }
587
- }
588
- } catch (err) {
589
- this.logger.warn("Interrupt polling error", { error: err });
590
- }
591
- }
592
- };
593
-
594
- // Start polling in background (don't await)
595
- const pollingPromise = pollForInterrupts();
596
-
597
- // Send initial prompt and wait for completion
598
- this.logger.info("Sending initial prompt to agent");
599
- const result = await clientConnection.prompt({
600
- sessionId: taskRunId,
601
- prompt: initialPrompt,
602
- });
603
-
604
- // Stop interrupt polling
605
- isPolling = false;
606
- await pollingPromise;
607
-
608
- this.logger.info("Task execution complete", {
609
- taskId: task.id,
610
- stopReason: result.stopReason,
611
- });
612
-
613
- const branchName = await this.gitManager.getCurrentBranch();
614
- const hasChanges = await this.gitManager.hasChanges();
615
- const shouldCreatePR = options.createPR ?? false;
616
-
617
- if (hasChanges) {
618
- this.logger.info("Committing uncommitted changes", { taskId: task.id });
619
- await this.gitManager.commitImplementation(
620
- task.id,
621
- task.title,
622
- task.description ?? undefined,
623
- );
624
- }
625
-
626
- const defaultBranch = await this.gitManager.getDefaultBranch();
627
- if (branchName !== defaultBranch) {
628
- this.logger.info("Pushing branch", { branchName, taskId: task.id });
629
- await this.gitManager.pushBranch(branchName);
630
-
631
- if (shouldCreatePR) {
632
- this.logger.info("Creating PR", { branchName, taskId: task.id });
633
-
634
- const prUrl = await this.createPullRequest(
635
- task.id,
636
- branchName,
637
- task.title,
638
- task.description ?? "",
639
- );
640
-
641
- this.logger.info("PR created", { prUrl, taskId: task.id });
642
-
643
- try {
644
- await this.attachPullRequestToTask(task.id, prUrl, branchName);
645
- } catch (err) {
646
- this.logger.warn("Could not attach PR to task", {
647
- error: err instanceof Error ? err.message : String(err),
648
- });
649
- }
650
- }
651
- }
652
-
653
- await this.sessionStore.complete(taskRunId);
654
- } catch (error) {
655
- const errorMessage =
656
- error instanceof Error ? error.message : String(error);
657
- this.logger.error("Cloud task execution failed", {
658
- taskId: task.id,
659
- error: errorMessage,
660
- });
661
- await this.sessionStore.fail(taskRunId, errorMessage);
662
- throw error;
176
+ async cleanup(): Promise<void> {
177
+ if (this.sessionLogWriter && this.taskRunId) {
178
+ await this.sessionLogWriter.flush(this.taskRunId);
663
179
  }
180
+ await this.acpConnection?.cleanup();
664
181
  }
665
182
  }
666
-
667
- export type {
668
- AgentConfig,
669
- ExecutionResult,
670
- SupportingFile,
671
- Task,
672
- } from "./types.js";
673
- export { PermissionMode } from "./types.js";