@posthog/agent 2.1.45 → 2.1.47

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@posthog/agent",
3
- "version": "2.1.45",
3
+ "version": "2.1.47",
4
4
  "repository": "https://github.com/PostHog/twig",
5
5
  "description": "TypeScript agent framework wrapping Claude Agent SDK with Git-based task execution for PostHog",
6
6
  "exports": {
@@ -72,8 +72,8 @@
72
72
  "tsx": "^4.20.6",
73
73
  "typescript": "^5.5.0",
74
74
  "vitest": "^2.1.8",
75
- "@twig/git": "1.0.0",
76
- "@posthog/shared": "1.0.0"
75
+ "@posthog/shared": "1.0.0",
76
+ "@twig/git": "1.0.0"
77
77
  },
78
78
  "dependencies": {
79
79
  "@agentclientprotocol/sdk": "^0.14.0",
@@ -23,6 +23,8 @@ export type AcpConnectionConfig = {
23
23
  /** Deployment environment - "local" for desktop, "cloud" for cloud sandbox */
24
24
  deviceType?: "local" | "cloud";
25
25
  logger?: Logger;
26
+ /** Enable dev-only instrumentation (timing, verbose logging) */
27
+ debug?: boolean;
26
28
  processCallbacks?: ProcessSpawnedCallback;
27
29
  codexOptions?: CodexProcessOptions;
28
30
  allowedModelIds?: Set<string>;
@@ -194,7 +196,10 @@ function createClaudeConnection(config: AcpConnectionConfig): AcpConnection {
194
196
 
195
197
  let agent: ClaudeAcpAgent | null = null;
196
198
  const agentConnection = new AgentSideConnection((client) => {
197
- agent = new ClaudeAcpAgent(client, logWriter, config.processCallbacks);
199
+ agent = new ClaudeAcpAgent(client, logWriter, {
200
+ ...config.processCallbacks,
201
+ debug: config.debug,
202
+ });
198
203
  logger.info(`Created ${agent.adapterName} agent`);
199
204
  return agent;
200
205
  }, agentStream);
@@ -29,6 +29,7 @@ import {
29
29
  type SDKMessage,
30
30
  type SDKUserMessage,
31
31
  } from "@anthropic-ai/claude-agent-sdk";
32
+ import { createTimingCollector } from "@posthog/shared";
32
33
  import { v7 as uuidv7 } from "uuid";
33
34
  import packageJson from "../../../package.json" with { type: "json" };
34
35
  import type { SessionContext } from "../../otel-log-writer.js";
@@ -69,6 +70,8 @@ import type {
69
70
  export interface ClaudeAcpAgentOptions {
70
71
  onProcessSpawned?: (info: ProcessSpawnedInfo) => void;
71
72
  onProcessExited?: (pid: number) => void;
73
+ /** Enable dev-only instrumentation (timing, verbose logging) */
74
+ debug?: boolean;
72
75
  }
73
76
 
74
77
  export class ClaudeAcpAgent extends BaseAcpAgent {
@@ -78,17 +81,19 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
78
81
  backgroundTerminals: { [key: string]: BackgroundTerminal } = {};
79
82
  clientCapabilities?: ClientCapabilities;
80
83
  private logWriter?: SessionLogWriter;
81
- private processCallbacks?: ClaudeAcpAgentOptions;
84
+ private options?: ClaudeAcpAgentOptions;
82
85
  private lastSentConfigOptions?: SessionConfigOption[];
86
+ private debug: boolean;
83
87
 
84
88
  constructor(
85
89
  client: AgentSideConnection,
86
90
  logWriter?: SessionLogWriter,
87
- processCallbacks?: ClaudeAcpAgentOptions,
91
+ options?: ClaudeAcpAgentOptions,
88
92
  ) {
89
93
  super(client);
90
94
  this.logWriter = logWriter;
91
- this.processCallbacks = processCallbacks;
95
+ this.options = options;
96
+ this.debug = options?.debug ?? false;
92
97
  this.toolUseCache = {};
93
98
  this.logger = new Logger({ debug: true, prefix: "[ClaudeAcpAgent]" });
94
99
  }
@@ -136,6 +141,10 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
136
141
  async newSession(params: NewSessionRequest): Promise<NewSessionResponse> {
137
142
  this.checkAuthStatus();
138
143
 
144
+ const tc = createTimingCollector(this.debug, (msg, data) =>
145
+ this.logger.info(msg, data),
146
+ );
147
+
139
148
  const meta = params._meta as NewSessionMeta | undefined;
140
149
  const sessionId = uuidv7();
141
150
  const permissionMode: TwigExecutionMode =
@@ -144,29 +153,29 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
144
153
  ? (meta.permissionMode as TwigExecutionMode)
145
154
  : "default";
146
155
 
147
- const mcpServers = parseMcpServers(params);
148
-
149
- // Fire off MCP metadata fetch early — it populates a module-level cache
150
- // used later during permission checks, not needed by buildSessionOptions or query()
151
- const mcpMetadataPromise = fetchMcpToolMetadata(mcpServers, this.logger);
156
+ const mcpServers = tc.timeSync("parseMcpServers", () =>
157
+ parseMcpServers(params),
158
+ );
152
159
 
153
- const options = buildSessionOptions({
154
- cwd: params.cwd,
155
- mcpServers,
156
- permissionMode,
157
- canUseTool: this.createCanUseTool(sessionId),
158
- logger: this.logger,
159
- systemPrompt: buildSystemPrompt(meta?.systemPrompt),
160
- userProvidedOptions: meta?.claudeCode?.options,
161
- sessionId,
162
- isResume: false,
163
- onModeChange: this.createOnModeChange(sessionId),
164
- onProcessSpawned: this.processCallbacks?.onProcessSpawned,
165
- onProcessExited: this.processCallbacks?.onProcessExited,
166
- });
160
+ const options = tc.timeSync("buildSessionOptions", () =>
161
+ buildSessionOptions({
162
+ cwd: params.cwd,
163
+ mcpServers,
164
+ permissionMode,
165
+ canUseTool: this.createCanUseTool(sessionId),
166
+ logger: this.logger,
167
+ systemPrompt: buildSystemPrompt(meta?.systemPrompt),
168
+ userProvidedOptions: meta?.claudeCode?.options,
169
+ sessionId,
170
+ isResume: false,
171
+ onModeChange: this.createOnModeChange(sessionId),
172
+ onProcessSpawned: this.options?.onProcessSpawned,
173
+ onProcessExited: this.options?.onProcessExited,
174
+ }),
175
+ );
167
176
 
168
177
  const input = new Pushable<SDKUserMessage>();
169
- const q = query({ prompt: input, options });
178
+ const q = tc.timeSync("sdkQuery", () => query({ prompt: input, options }));
170
179
 
171
180
  const session = this.createSession(
172
181
  sessionId,
@@ -180,28 +189,36 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
180
189
  this.registerPersistence(sessionId, meta as Record<string, unknown>);
181
190
 
182
191
  if (meta?.taskRunId) {
183
- await this.client.extNotification("_posthog/sdk_session", {
184
- taskRunId: meta.taskRunId,
185
- sessionId,
186
- adapter: "claude",
187
- });
192
+ await tc.time("extNotification", () =>
193
+ this.client.extNotification("_posthog/sdk_session", {
194
+ taskRunId: meta.taskRunId!,
195
+ sessionId,
196
+ adapter: "claude",
197
+ }),
198
+ );
188
199
  }
189
200
 
190
- // Run model config, slash commands, and MCP metadata fetch in parallel
191
- const [modelOptions, slashCommands] = await Promise.all([
201
+ // Only await model config slash commands and MCP metadata are deferred
202
+ // since they're not needed to return configOptions to the client.
203
+ const modelOptions = await tc.time("fetchModels", () =>
192
204
  this.getModelConfigOptions(),
193
- getAvailableSlashCommands(q),
194
- mcpMetadataPromise,
195
- ]);
205
+ );
206
+
207
+ // Deferred: slash commands + MCP metadata (not needed to return configOptions)
208
+ this.deferBackgroundFetches(tc, q, sessionId, mcpServers);
196
209
 
197
210
  session.modelId = modelOptions.currentModelId;
198
211
  await this.trySetModel(q, modelOptions.currentModelId);
199
212
 
200
- this.sendAvailableCommandsUpdate(sessionId, slashCommands);
213
+ const configOptions = await tc.time("buildConfigOptions", () =>
214
+ this.buildConfigOptions(modelOptions),
215
+ );
216
+
217
+ tc.summarize("newSession");
201
218
 
202
219
  return {
203
220
  sessionId,
204
- configOptions: await this.buildConfigOptions(modelOptions),
221
+ configOptions,
205
222
  };
206
223
  }
207
224
 
@@ -212,6 +229,10 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
212
229
  async resumeSession(
213
230
  params: LoadSessionRequest,
214
231
  ): Promise<LoadSessionResponse> {
232
+ const tc = createTimingCollector(this.debug, (msg, data) =>
233
+ this.logger.info(msg, data),
234
+ );
235
+
215
236
  const meta = params._meta as NewSessionMeta | undefined;
216
237
  const sessionId = meta?.sessionId;
217
238
  if (!sessionId) {
@@ -221,10 +242,9 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
221
242
  return {};
222
243
  }
223
244
 
224
- const mcpServers = parseMcpServers(params);
225
-
226
- // Fire off MCP metadata fetch early — populates cache for permission checks
227
- const mcpMetadataPromise = fetchMcpToolMetadata(mcpServers, this.logger);
245
+ const mcpServers = tc.timeSync("parseMcpServers", () =>
246
+ parseMcpServers(params),
247
+ );
228
248
 
229
249
  const permissionMode: TwigExecutionMode =
230
250
  meta?.permissionMode &&
@@ -232,32 +252,33 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
232
252
  ? (meta.permissionMode as TwigExecutionMode)
233
253
  : "default";
234
254
 
235
- const { query: q, session } = await this.initializeQuery({
236
- cwd: params.cwd,
237
- permissionMode,
238
- mcpServers,
239
- systemPrompt: buildSystemPrompt(meta?.systemPrompt),
240
- userProvidedOptions: meta?.claudeCode?.options,
241
- sessionId,
242
- isResume: true,
243
- additionalDirectories: meta?.claudeCode?.options?.additionalDirectories,
244
- });
255
+ const { query: q, session } = await tc.time("initializeQuery", () =>
256
+ this.initializeQuery({
257
+ cwd: params.cwd,
258
+ permissionMode,
259
+ mcpServers,
260
+ systemPrompt: buildSystemPrompt(meta?.systemPrompt),
261
+ userProvidedOptions: meta?.claudeCode?.options,
262
+ sessionId,
263
+ isResume: true,
264
+ additionalDirectories: meta?.claudeCode?.options?.additionalDirectories,
265
+ }),
266
+ );
245
267
 
246
268
  session.taskRunId = meta?.taskRunId;
247
269
 
248
270
  this.registerPersistence(sessionId, meta as Record<string, unknown>);
249
271
 
250
- // Run slash commands fetch and MCP metadata in parallel
251
- const [slashCommands] = await Promise.all([
252
- getAvailableSlashCommands(q),
253
- mcpMetadataPromise,
254
- ]);
272
+ // Deferred: slash commands + MCP metadata (not needed to return configOptions)
273
+ this.deferBackgroundFetches(tc, q, sessionId, mcpServers);
255
274
 
256
- this.sendAvailableCommandsUpdate(sessionId, slashCommands);
275
+ const configOptions = await tc.time("buildConfigOptions", () =>
276
+ this.buildConfigOptions(),
277
+ );
257
278
 
258
- return {
259
- configOptions: await this.buildConfigOptions(),
260
- };
279
+ tc.summarize("resumeSession");
280
+
281
+ return { configOptions };
261
282
  }
262
283
 
263
284
  async prompt(params: PromptRequest): Promise<PromptResponse> {
@@ -366,8 +387,8 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
366
387
  isResume: config.isResume,
367
388
  additionalDirectories: config.additionalDirectories,
368
389
  onModeChange: this.createOnModeChange(config.sessionId),
369
- onProcessSpawned: this.processCallbacks?.onProcessSpawned,
370
- onProcessExited: this.processCallbacks?.onProcessExited,
390
+ onProcessSpawned: this.options?.onProcessSpawned,
391
+ onProcessExited: this.options?.onProcessExited,
371
392
  });
372
393
 
373
394
  const q = query({ prompt: input, options });
@@ -503,6 +524,30 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
503
524
  }
504
525
  }
505
526
 
527
+ /**
528
+ * Fire-and-forget: fetch slash commands and MCP tool metadata in parallel.
529
+ * Both populate caches used later — neither is needed to return configOptions.
530
+ */
531
+ private deferBackgroundFetches(
532
+ tc: ReturnType<typeof createTimingCollector>,
533
+ q: Query,
534
+ sessionId: string,
535
+ mcpServers: ReturnType<typeof parseMcpServers>,
536
+ ): void {
537
+ Promise.all([
538
+ tc.time("slashCommands", () => getAvailableSlashCommands(q)),
539
+ tc.time("mcpMetadata", () =>
540
+ fetchMcpToolMetadata(mcpServers, this.logger),
541
+ ),
542
+ ])
543
+ .then(([slashCommands]) => {
544
+ this.sendAvailableCommandsUpdate(sessionId, slashCommands);
545
+ })
546
+ .catch((err) => {
547
+ this.logger.warn("Failed to fetch deferred session data", { err });
548
+ });
549
+ }
550
+
506
551
  private registerPersistence(
507
552
  sessionId: string,
508
553
  meta: Record<string, unknown> | undefined,
package/src/agent.ts CHANGED
@@ -111,6 +111,7 @@ export class Agent {
111
111
  taskId,
112
112
  deviceType: "local",
113
113
  logger: this.logger,
114
+ debug: this.debug,
114
115
  processCallbacks: options.processCallbacks,
115
116
  allowedModelIds,
116
117
  codexOptions: