@posthog/agent 2.3.513 → 2.3.519

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.3.513",
3
+ "version": "2.3.519",
4
4
  "repository": "https://github.com/PostHog/code",
5
5
  "description": "TypeScript agent framework wrapping Claude Agent SDK with Git-based task execution for PostHog",
6
6
  "exports": {
@@ -102,9 +102,9 @@
102
102
  "tsx": "^4.20.6",
103
103
  "typescript": "^5.5.0",
104
104
  "vitest": "^2.1.8",
105
- "@posthog/git": "1.0.0",
106
105
  "@posthog/shared": "1.0.0",
107
- "@posthog/enricher": "1.0.0"
106
+ "@posthog/enricher": "1.0.0",
107
+ "@posthog/git": "1.0.0"
108
108
  },
109
109
  "dependencies": {
110
110
  "@agentclientprotocol/sdk": "0.19.0",
@@ -13,7 +13,7 @@ import {
13
13
  import { createTestRepo, type TestRepo } from "../test/fixtures/api";
14
14
  import { createPostHogHandlers } from "../test/mocks/msw-handlers";
15
15
  import type { TaskRun } from "../types";
16
- import { AgentServer } from "./agent-server";
16
+ import { AgentServer, SSE_KEEPALIVE_INTERVAL_MS } from "./agent-server";
17
17
  import { type JwtPayload, SANDBOX_CONNECTION_AUDIENCE } from "./jwt";
18
18
 
19
19
  interface TestableServer {
@@ -274,6 +274,64 @@ describe("AgentServer HTTP Mode", () => {
274
274
  expect(response.status).toBe(200);
275
275
  expect(response.headers.get("content-type")).toBe("text/event-stream");
276
276
  }, 20000);
277
+
278
+ it("emits transport keepalive comments while idle", async () => {
279
+ const keepaliveCallback: { current: (() => void) | null } = {
280
+ current: null,
281
+ };
282
+ const setIntervalSpy = vi
283
+ .spyOn(globalThis, "setInterval")
284
+ .mockImplementation(
285
+ (callback: (_: undefined) => void, timeout?: number) => {
286
+ if (timeout === SSE_KEEPALIVE_INTERVAL_MS) {
287
+ keepaliveCallback.current = () => callback(undefined);
288
+ }
289
+ return setTimeout(() => undefined, 60_000);
290
+ },
291
+ );
292
+
293
+ let reader: ReadableStreamDefaultReader<Uint8Array> | null = null;
294
+ try {
295
+ await createServer().start();
296
+ const token = createToken();
297
+
298
+ const response = await fetch(`http://localhost:${port}/events`, {
299
+ headers: { Authorization: `Bearer ${token}` },
300
+ });
301
+
302
+ expect(response.status).toBe(200);
303
+ expect(response.body).not.toBeNull();
304
+ reader = response.body?.getReader() ?? null;
305
+ expect(reader).not.toBeNull();
306
+ if (!reader) {
307
+ throw new Error("Expected SSE response body reader");
308
+ }
309
+
310
+ await vi.waitFor(() =>
311
+ expect(keepaliveCallback.current).not.toBeNull(),
312
+ );
313
+ const emitKeepalive = keepaliveCallback.current;
314
+ if (!emitKeepalive) {
315
+ throw new Error("Expected keepalive callback to be registered");
316
+ }
317
+ emitKeepalive();
318
+
319
+ const decoder = new TextDecoder();
320
+ let streamText = "";
321
+ for (let attempts = 0; attempts < 5; attempts++) {
322
+ const { done, value } = await reader.read();
323
+ if (done) break;
324
+ streamText += decoder.decode(value, { stream: true });
325
+ if (streamText.includes(": keepalive\n\n")) break;
326
+ }
327
+
328
+ expect(streamText).toContain(": keepalive\n\n");
329
+ expect(streamText).not.toContain('"type":"keepalive"');
330
+ } finally {
331
+ await reader?.cancel();
332
+ setIntervalSpy.mockRestore();
333
+ }
334
+ }, 20000);
277
335
  });
278
336
 
279
337
  describe("POST /command", () => {
@@ -405,6 +463,15 @@ describe("AgentServer HTTP Mode", () => {
405
463
  runId: "test-run-id",
406
464
  taskId: "test-task-id",
407
465
  });
466
+ // Agent reports its semver so clients can gate UI features
467
+ // against agent capabilities (e.g. `>=0.40.1`). The exact value
468
+ // is whatever the agent's package.json was at build time.
469
+ expect(typeof runStarted?.notification?.params?.agentVersion).toBe(
470
+ "string",
471
+ );
472
+ expect(
473
+ (runStarted?.notification?.params?.agentVersion as string).length,
474
+ ).toBeGreaterThan(0);
408
475
  },
409
476
  { timeout: 15000, interval: 100 },
410
477
  );
@@ -73,6 +73,8 @@ const errorWithClassificationSchema = z.object({
73
73
 
74
74
  type MessageCallback = (message: unknown) => void;
75
75
 
76
+ export const SSE_KEEPALIVE_INTERVAL_MS = 25_000;
77
+
76
78
  class NdJsonTap {
77
79
  private decoder = new TextDecoder();
78
80
  private buffer = "";
@@ -329,41 +331,73 @@ export class AgentServer {
329
331
  );
330
332
  }
331
333
 
334
+ let keepaliveInterval: ReturnType<typeof setInterval> | null = null;
335
+ const clearKeepalive = (): void => {
336
+ if (keepaliveInterval) {
337
+ clearInterval(keepaliveInterval);
338
+ keepaliveInterval = null;
339
+ }
340
+ };
341
+
332
342
  const stream = new ReadableStream({
333
343
  start: async (controller) => {
334
- const sseController: SseController = {
344
+ let sseController: SseController | null = null;
345
+ const encoder = new TextEncoder();
346
+ const detachCurrentSseController = (): void => {
347
+ if (sseController) {
348
+ this.detachSseController(sseController);
349
+ }
350
+ };
351
+ const enqueueSseFrame = (frame: string): void => {
352
+ try {
353
+ controller.enqueue(encoder.encode(frame));
354
+ } catch {
355
+ clearKeepalive();
356
+ detachCurrentSseController();
357
+ }
358
+ };
359
+
360
+ sseController = {
335
361
  send: (data: unknown) => {
336
- try {
337
- controller.enqueue(
338
- new TextEncoder().encode(`data: ${JSON.stringify(data)}\n\n`),
339
- );
340
- } catch {
341
- this.detachSseController(sseController);
342
- }
362
+ enqueueSseFrame(`data: ${JSON.stringify(data)}\n\n`);
343
363
  },
344
364
  close: () => {
345
365
  try {
366
+ clearKeepalive();
346
367
  controller.close();
347
368
  } catch {
348
- this.detachSseController(sseController);
369
+ detachCurrentSseController();
349
370
  }
350
371
  },
351
372
  };
352
373
 
353
- if (!this.session || this.session.payload.run_id !== payload.run_id) {
354
- await this.initializeSession(payload, sseController);
355
- } else {
356
- this.session.sseController = sseController;
357
- this.session.hasDesktopConnected = true;
358
- this.replayPendingEvents();
359
- }
374
+ keepaliveInterval = setInterval(() => {
375
+ enqueueSseFrame(": keepalive\n\n");
376
+ }, SSE_KEEPALIVE_INTERVAL_MS);
377
+
378
+ try {
379
+ if (
380
+ !this.session ||
381
+ this.session.payload.run_id !== payload.run_id
382
+ ) {
383
+ await this.initializeSession(payload, sseController);
384
+ } else {
385
+ this.session.sseController = sseController;
386
+ this.session.hasDesktopConnected = true;
387
+ this.replayPendingEvents();
388
+ }
360
389
 
361
- this.sendSseEvent(sseController, {
362
- type: "connected",
363
- run_id: payload.run_id,
364
- });
390
+ this.sendSseEvent(sseController, {
391
+ type: "connected",
392
+ run_id: payload.run_id,
393
+ });
394
+ } catch (error) {
395
+ clearKeepalive();
396
+ throw error;
397
+ }
365
398
  },
366
399
  cancel: () => {
400
+ clearKeepalive();
367
401
  this.logger.debug("SSE connection closed");
368
402
  if (this.session?.sseController) {
369
403
  this.session.sseController = null;
@@ -969,6 +1003,7 @@ export class AgentServer {
969
1003
  sessionId: acpSessionId,
970
1004
  runId: payload.run_id,
971
1005
  taskId: payload.task_id,
1006
+ agentVersion: this.config.version ?? packageJson.version,
972
1007
  },
973
1008
  };
974
1009
  this.broadcastEvent({