@posthog/agent 2.1.35 → 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.35",
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": {
@@ -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,26 +153,29 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
144
153
  ? (meta.permissionMode as TwigExecutionMode)
145
154
  : "default";
146
155
 
147
- const mcpServers = parseMcpServers(params);
148
- await fetchMcpToolMetadata(mcpServers, this.logger);
156
+ const mcpServers = tc.timeSync("parseMcpServers", () =>
157
+ parseMcpServers(params),
158
+ );
149
159
 
150
- const options = buildSessionOptions({
151
- cwd: params.cwd,
152
- mcpServers,
153
- permissionMode,
154
- canUseTool: this.createCanUseTool(sessionId),
155
- logger: this.logger,
156
- systemPrompt: buildSystemPrompt(meta?.systemPrompt),
157
- userProvidedOptions: meta?.claudeCode?.options,
158
- sessionId,
159
- isResume: false,
160
- onModeChange: this.createOnModeChange(sessionId),
161
- onProcessSpawned: this.processCallbacks?.onProcessSpawned,
162
- onProcessExited: this.processCallbacks?.onProcessExited,
163
- });
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
+ );
164
176
 
165
177
  const input = new Pushable<SDKUserMessage>();
166
- const q = query({ prompt: input, options });
178
+ const q = tc.timeSync("sdkQuery", () => query({ prompt: input, options }));
167
179
 
168
180
  const session = this.createSession(
169
181
  sessionId,
@@ -177,25 +189,36 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
177
189
  this.registerPersistence(sessionId, meta as Record<string, unknown>);
178
190
 
179
191
  if (meta?.taskRunId) {
180
- await this.client.extNotification("_posthog/sdk_session", {
181
- taskRunId: meta.taskRunId,
182
- sessionId,
183
- adapter: "claude",
184
- });
192
+ await tc.time("extNotification", () =>
193
+ this.client.extNotification("_posthog/sdk_session", {
194
+ taskRunId: meta.taskRunId!,
195
+ sessionId,
196
+ adapter: "claude",
197
+ }),
198
+ );
185
199
  }
186
200
 
187
- const modelOptions = await this.getModelConfigOptions();
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", () =>
204
+ this.getModelConfigOptions(),
205
+ );
206
+
207
+ // Deferred: slash commands + MCP metadata (not needed to return configOptions)
208
+ this.deferBackgroundFetches(tc, q, sessionId, mcpServers);
209
+
188
210
  session.modelId = modelOptions.currentModelId;
189
211
  await this.trySetModel(q, modelOptions.currentModelId);
190
212
 
191
- this.sendAvailableCommandsUpdate(
192
- sessionId,
193
- await getAvailableSlashCommands(q),
213
+ const configOptions = await tc.time("buildConfigOptions", () =>
214
+ this.buildConfigOptions(modelOptions),
194
215
  );
195
216
 
217
+ tc.summarize("newSession");
218
+
196
219
  return {
197
220
  sessionId,
198
- configOptions: await this.buildConfigOptions(modelOptions),
221
+ configOptions,
199
222
  };
200
223
  }
201
224
 
@@ -206,6 +229,10 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
206
229
  async resumeSession(
207
230
  params: LoadSessionRequest,
208
231
  ): Promise<LoadSessionResponse> {
232
+ const tc = createTimingCollector(this.debug, (msg, data) =>
233
+ this.logger.info(msg, data),
234
+ );
235
+
209
236
  const meta = params._meta as NewSessionMeta | undefined;
210
237
  const sessionId = meta?.sessionId;
211
238
  if (!sessionId) {
@@ -215,8 +242,9 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
215
242
  return {};
216
243
  }
217
244
 
218
- const mcpServers = parseMcpServers(params);
219
- await fetchMcpToolMetadata(mcpServers, this.logger);
245
+ const mcpServers = tc.timeSync("parseMcpServers", () =>
246
+ parseMcpServers(params),
247
+ );
220
248
 
221
249
  const permissionMode: TwigExecutionMode =
222
250
  meta?.permissionMode &&
@@ -224,28 +252,33 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
224
252
  ? (meta.permissionMode as TwigExecutionMode)
225
253
  : "default";
226
254
 
227
- const { query: q, session } = await this.initializeQuery({
228
- cwd: params.cwd,
229
- permissionMode,
230
- mcpServers,
231
- systemPrompt: buildSystemPrompt(meta?.systemPrompt),
232
- userProvidedOptions: meta?.claudeCode?.options,
233
- sessionId,
234
- isResume: true,
235
- additionalDirectories: meta?.claudeCode?.options?.additionalDirectories,
236
- });
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
+ );
237
267
 
238
268
  session.taskRunId = meta?.taskRunId;
239
269
 
240
270
  this.registerPersistence(sessionId, meta as Record<string, unknown>);
241
- this.sendAvailableCommandsUpdate(
242
- sessionId,
243
- await getAvailableSlashCommands(q),
271
+
272
+ // Deferred: slash commands + MCP metadata (not needed to return configOptions)
273
+ this.deferBackgroundFetches(tc, q, sessionId, mcpServers);
274
+
275
+ const configOptions = await tc.time("buildConfigOptions", () =>
276
+ this.buildConfigOptions(),
244
277
  );
245
278
 
246
- return {
247
- configOptions: await this.buildConfigOptions(),
248
- };
279
+ tc.summarize("resumeSession");
280
+
281
+ return { configOptions };
249
282
  }
250
283
 
251
284
  async prompt(params: PromptRequest): Promise<PromptResponse> {
@@ -354,8 +387,8 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
354
387
  isResume: config.isResume,
355
388
  additionalDirectories: config.additionalDirectories,
356
389
  onModeChange: this.createOnModeChange(config.sessionId),
357
- onProcessSpawned: this.processCallbacks?.onProcessSpawned,
358
- onProcessExited: this.processCallbacks?.onProcessExited,
390
+ onProcessSpawned: this.options?.onProcessSpawned,
391
+ onProcessExited: this.options?.onProcessExited,
359
392
  });
360
393
 
361
394
  const q = query({ prompt: input, options });
@@ -491,6 +524,30 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
491
524
  }
492
525
  }
493
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
+
494
551
  private registerPersistence(
495
552
  sessionId: string,
496
553
  meta: Record<string, unknown> | undefined,
@@ -245,11 +245,7 @@ function clearStatsigCache(): void {
245
245
  process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), ".claude"),
246
246
  "statsig",
247
247
  );
248
- try {
249
- if (fs.existsSync(statsigPath)) {
250
- fs.rmSync(statsigPath, { recursive: true, force: true });
251
- }
252
- } catch {
253
- // Ignore errors - cache clearing is best-effort
254
- }
248
+ fs.rm(statsigPath, { recursive: true, force: true }, () => {
249
+ // Best-effort, ignore errors
250
+ });
255
251
  }
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:
@@ -26,6 +26,14 @@ type ArrayModelsResponse =
26
26
  }
27
27
  | Array<{ id?: string; owned_by?: string }>;
28
28
 
29
+ const CACHE_TTL = 10 * 60 * 1000; // 10 minutes
30
+
31
+ let gatewayModelsCache: {
32
+ models: GatewayModel[];
33
+ expiry: number;
34
+ url: string;
35
+ } | null = null;
36
+
29
37
  export async function fetchGatewayModels(
30
38
  options?: FetchGatewayModelsOptions,
31
39
  ): Promise<GatewayModel[]> {
@@ -34,6 +42,14 @@ export async function fetchGatewayModels(
34
42
  return [];
35
43
  }
36
44
 
45
+ if (
46
+ gatewayModelsCache &&
47
+ gatewayModelsCache.url === gatewayUrl &&
48
+ Date.now() < gatewayModelsCache.expiry
49
+ ) {
50
+ return gatewayModelsCache.models;
51
+ }
52
+
37
53
  const modelsUrl = `${gatewayUrl}/v1/models`;
38
54
 
39
55
  try {
@@ -44,8 +60,13 @@ export async function fetchGatewayModels(
44
60
  }
45
61
 
46
62
  const data = (await response.json()) as GatewayModelsResponse;
47
- const models = data.data ?? [];
48
- return models.filter((m) => !BLOCKED_MODELS.has(m.id));
63
+ const models = (data.data ?? []).filter((m) => !BLOCKED_MODELS.has(m.id));
64
+ gatewayModelsCache = {
65
+ models,
66
+ expiry: Date.now() + CACHE_TTL,
67
+ url: gatewayUrl,
68
+ };
69
+ return models;
49
70
  } catch {
50
71
  return [];
51
72
  }
@@ -70,6 +91,12 @@ export interface ArrayModelInfo {
70
91
  owned_by?: string;
71
92
  }
72
93
 
94
+ let arrayModelsCache: {
95
+ models: ArrayModelInfo[];
96
+ expiry: number;
97
+ url: string;
98
+ } | null = null;
99
+
73
100
  export async function fetchArrayModels(
74
101
  options?: FetchGatewayModelsOptions,
75
102
  ): Promise<ArrayModelInfo[]> {
@@ -78,6 +105,14 @@ export async function fetchArrayModels(
78
105
  return [];
79
106
  }
80
107
 
108
+ if (
109
+ arrayModelsCache &&
110
+ arrayModelsCache.url === gatewayUrl &&
111
+ Date.now() < arrayModelsCache.expiry
112
+ ) {
113
+ return arrayModelsCache.models;
114
+ }
115
+
81
116
  try {
82
117
  const base = new URL(gatewayUrl);
83
118
  base.pathname = "/array/v1/models";
@@ -97,6 +132,11 @@ export async function fetchArrayModels(
97
132
  if (!id) continue;
98
133
  results.push({ id, owned_by: model?.owned_by });
99
134
  }
135
+ arrayModelsCache = {
136
+ models: results,
137
+ expiry: Date.now() + CACHE_TTL,
138
+ url: gatewayUrl,
139
+ };
100
140
  return results;
101
141
  } catch {
102
142
  return [];