@posthog/agent 2.3.207 → 2.3.213

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.207",
3
+ "version": "2.3.213",
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": {
@@ -371,8 +371,18 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
371
371
  switch (message.type) {
372
372
  case "system":
373
373
  if (message.subtype === "compact_boundary") {
374
+ // Send used:0 immediately so the client doesn't keep showing
375
+ // the stale pre-compaction context size until the next turn.
374
376
  lastAssistantTotalUsage = 0;
375
377
  promptReplayed = true;
378
+ await this.client.sessionUpdate({
379
+ sessionId: params.sessionId,
380
+ update: {
381
+ sessionUpdate: "usage_update",
382
+ used: 0,
383
+ size: lastContextWindowSize,
384
+ },
385
+ });
376
386
  }
377
387
  if (message.subtype === "local_command_output") {
378
388
  promptReplayed = true;
@@ -530,6 +540,11 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
530
540
  }
531
541
 
532
542
  // Store latest assistant usage (excluding subagents)
543
+ // Sum all token types as a proxy for post-turn context occupancy:
544
+ // current turn's output will become next turn's input.
545
+ // Note: per the Anthropic API, input_tokens excludes cache tokens —
546
+ // cache_read and cache_creation are reported separately, so summing
547
+ // all four fields is not double-counting.
533
548
  if (
534
549
  "usage" in message.message &&
535
550
  message.parent_tool_use_id === null
@@ -544,6 +559,7 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
544
559
  };
545
560
  lastAssistantTotalUsage =
546
561
  usage.input_tokens +
562
+ usage.output_tokens +
547
563
  usage.cache_read_input_tokens +
548
564
  usage.cache_creation_input_tokens;
549
565
 
@@ -252,6 +252,30 @@ describe("AgentServer HTTP Mode", () => {
252
252
  const body = await response.json();
253
253
  expect(body.error).toBe("No active session for this run");
254
254
  });
255
+
256
+ it("accepts structured user_message content", async () => {
257
+ await createServer().start();
258
+ const token = createToken({ run_id: "different-run-id" });
259
+
260
+ const response = await fetch(`http://localhost:${port}/command`, {
261
+ method: "POST",
262
+ headers: {
263
+ Authorization: `Bearer ${token}`,
264
+ "Content-Type": "application/json",
265
+ },
266
+ body: JSON.stringify({
267
+ jsonrpc: "2.0",
268
+ method: "user_message",
269
+ params: {
270
+ content: [{ type: "text", text: "test" }],
271
+ },
272
+ }),
273
+ });
274
+
275
+ expect(response.status).toBe(400);
276
+ const body = await response.json();
277
+ expect(body.error).toBe("No active session for this run");
278
+ });
255
279
  });
256
280
 
257
281
  describe("404 handling", () => {
@@ -1,3 +1,4 @@
1
+ import type { ContentBlock } from "@agentclientprotocol/sdk";
1
2
  import {
2
3
  ClientSideConnection,
3
4
  ndJsonStream,
@@ -30,6 +31,11 @@ import type {
30
31
  import { AsyncMutex } from "../utils/async-mutex";
31
32
  import { getLlmGatewayUrl } from "../utils/gateway";
32
33
  import { Logger } from "../utils/logger";
34
+ import {
35
+ deserializeCloudPrompt,
36
+ normalizeCloudPromptContent,
37
+ promptBlocksToText,
38
+ } from "./cloud-prompt";
33
39
  import { type JwtPayload, JwtValidationError, validateJwt } from "./jwt";
34
40
  import { jsonRpcRequestSchema, validateCommandParams } from "./schemas";
35
41
  import type { AgentServerConfig } from "./types";
@@ -487,17 +493,20 @@ export class AgentServer {
487
493
  switch (method) {
488
494
  case POSTHOG_NOTIFICATIONS.USER_MESSAGE:
489
495
  case "user_message": {
490
- const content = params.content as string;
496
+ const prompt = normalizeCloudPromptContent(
497
+ params.content as string | ContentBlock[],
498
+ );
499
+ const promptPreview = promptBlocksToText(prompt);
491
500
 
492
501
  this.logger.info(
493
- `Processing user message (detectedPrUrl=${this.detectedPrUrl ?? "none"}): ${content.substring(0, 100)}...`,
502
+ `Processing user message (detectedPrUrl=${this.detectedPrUrl ?? "none"}): ${promptPreview.substring(0, 100)}...`,
494
503
  );
495
504
 
496
505
  this.session.logWriter.resetTurnMessages(this.session.payload.run_id);
497
506
 
498
507
  const result = await this.session.clientConnection.prompt({
499
508
  sessionId: this.session.acpSessionId,
500
- prompt: [{ type: "text", text: content }],
509
+ prompt,
501
510
  ...(this.detectedPrUrl && {
502
511
  _meta: {
503
512
  prContext:
@@ -837,24 +846,33 @@ export class AgentServer {
837
846
  const initialPromptOverride = taskRun
838
847
  ? this.getInitialPromptOverride(taskRun)
839
848
  : null;
840
- const initialPrompt = initialPromptOverride ?? task.description;
849
+ const pendingUserPrompt = this.getPendingUserPrompt(taskRun);
850
+ let initialPrompt: ContentBlock[] = [];
851
+ if (pendingUserPrompt?.length) {
852
+ initialPrompt = pendingUserPrompt;
853
+ } else if (initialPromptOverride) {
854
+ initialPrompt = [{ type: "text", text: initialPromptOverride }];
855
+ } else if (task.description) {
856
+ initialPrompt = [{ type: "text", text: task.description }];
857
+ }
841
858
 
842
- if (!initialPrompt) {
859
+ if (initialPrompt.length === 0) {
843
860
  this.logger.warn("Task has no description, skipping initial message");
844
861
  return;
845
862
  }
846
863
 
847
864
  this.logger.info("Sending initial task message", {
848
865
  taskId: payload.task_id,
849
- descriptionLength: initialPrompt.length,
866
+ descriptionLength: promptBlocksToText(initialPrompt).length,
850
867
  usedInitialPromptOverride: !!initialPromptOverride,
868
+ usedPendingUserMessage: !!pendingUserPrompt?.length,
851
869
  });
852
870
 
853
871
  this.session.logWriter.resetTurnMessages(payload.run_id);
854
872
 
855
873
  const result = await this.session.clientConnection.prompt({
856
874
  sessionId: this.session.acpSessionId,
857
- prompt: [{ type: "text", text: initialPrompt }],
875
+ prompt: initialPrompt,
858
876
  });
859
877
 
860
878
  this.logger.info("Initial task message completed", {
@@ -886,38 +904,49 @@ export class AgentServer {
886
904
  this.resumeState.conversation,
887
905
  );
888
906
 
889
- // Read the pending user message from TaskRun state (set by the workflow
907
+ // Read the pending user prompt from TaskRun state (set by the workflow
890
908
  // when the user sends a follow-up message that triggers a resume).
891
- const pendingUserMessage = this.getPendingUserMessage(taskRun);
909
+ const pendingUserPrompt = this.getPendingUserPrompt(taskRun);
892
910
 
893
911
  const sandboxContext = this.resumeState.snapshotApplied
894
912
  ? `The workspace environment (all files, packages, and code changes) has been fully restored from where you left off.`
895
913
  : `The workspace files from the previous session were not restored (the file snapshot may have expired), so you are starting with a fresh environment. Your conversation history is fully preserved below.`;
896
914
 
897
- let resumePrompt: string;
898
- if (pendingUserMessage) {
899
- // Include the pending message as the user's new question so the agent
900
- // responds to it directly instead of the generic resume context.
901
- resumePrompt =
902
- `You are resuming a previous conversation. ${sandboxContext}\n\n` +
903
- `Here is the conversation history from the previous session:\n\n` +
904
- `${conversationSummary}\n\n` +
905
- `The user has sent a new message:\n\n` +
906
- `${pendingUserMessage}\n\n` +
907
- `Respond to the user's new message above. You have full context from the previous session.`;
915
+ let resumePromptBlocks: ContentBlock[];
916
+ if (pendingUserPrompt?.length) {
917
+ resumePromptBlocks = [
918
+ {
919
+ type: "text",
920
+ text:
921
+ `You are resuming a previous conversation. ${sandboxContext}\n\n` +
922
+ `Here is the conversation history from the previous session:\n\n` +
923
+ `${conversationSummary}\n\n` +
924
+ `The user has sent a new message:\n\n`,
925
+ },
926
+ ...pendingUserPrompt,
927
+ {
928
+ type: "text",
929
+ text: "\n\nRespond to the user's new message above. You have full context from the previous session.",
930
+ },
931
+ ];
908
932
  } else {
909
- resumePrompt =
910
- `You are resuming a previous conversation. ${sandboxContext}\n\n` +
911
- `Here is the conversation history from the previous session:\n\n` +
912
- `${conversationSummary}\n\n` +
913
- `Continue from where you left off. The user is waiting for your response.`;
933
+ resumePromptBlocks = [
934
+ {
935
+ type: "text",
936
+ text:
937
+ `You are resuming a previous conversation. ${sandboxContext}\n\n` +
938
+ `Here is the conversation history from the previous session:\n\n` +
939
+ `${conversationSummary}\n\n` +
940
+ `Continue from where you left off. The user is waiting for your response.`,
941
+ },
942
+ ];
914
943
  }
915
944
 
916
945
  this.logger.info("Sending resume message", {
917
946
  taskId: payload.task_id,
918
947
  conversationTurns: this.resumeState.conversation.length,
919
- promptLength: resumePrompt.length,
920
- hasPendingUserMessage: !!pendingUserMessage,
948
+ promptLength: promptBlocksToText(resumePromptBlocks).length,
949
+ hasPendingUserMessage: !!pendingUserPrompt?.length,
921
950
  snapshotApplied: this.resumeState.snapshotApplied,
922
951
  });
923
952
 
@@ -928,7 +957,7 @@ export class AgentServer {
928
957
 
929
958
  const result = await this.session.clientConnection.prompt({
930
959
  sessionId: this.session.acpSessionId,
931
- prompt: [{ type: "text", text: resumePrompt }],
960
+ prompt: resumePromptBlocks,
932
961
  });
933
962
 
934
963
  this.logger.info("Resume message completed", {
@@ -1013,7 +1042,7 @@ export class AgentServer {
1013
1042
  return trimmed.length > 0 ? trimmed : null;
1014
1043
  }
1015
1044
 
1016
- private getPendingUserMessage(taskRun: TaskRun | null): string | null {
1045
+ private getPendingUserPrompt(taskRun: TaskRun | null): ContentBlock[] | null {
1017
1046
  if (!taskRun) return null;
1018
1047
  const state = taskRun.state as Record<string, unknown> | undefined;
1019
1048
  const message = state?.pending_user_message;
@@ -1021,8 +1050,8 @@ export class AgentServer {
1021
1050
  return null;
1022
1051
  }
1023
1052
 
1024
- const trimmed = message.trim();
1025
- return trimmed.length > 0 ? trimmed : null;
1053
+ const prompt = deserializeCloudPrompt(message);
1054
+ return prompt.length > 0 ? prompt : null;
1026
1055
  }
1027
1056
 
1028
1057
  private getResumeRunId(taskRun: TaskRun | null): string | null {
@@ -0,0 +1,13 @@
1
+ import type { ContentBlock } from "@agentclientprotocol/sdk";
2
+ import { deserializeCloudPrompt, promptBlocksToText } from "@posthog/shared";
3
+
4
+ export { deserializeCloudPrompt, promptBlocksToText };
5
+
6
+ export function normalizeCloudPromptContent(
7
+ content: string | ContentBlock[],
8
+ ): ContentBlock[] {
9
+ if (typeof content === "string") {
10
+ return deserializeCloudPrompt(content);
11
+ }
12
+ return content;
13
+ }
@@ -371,6 +371,53 @@ describe("Question relay", () => {
371
371
  });
372
372
 
373
373
  describe("sendInitialTaskMessage prompt source", () => {
374
+ it("uses pending user prompt blocks when present", async () => {
375
+ vi.spyOn(server.posthogAPI, "getTask").mockResolvedValue({
376
+ id: "test-task-id",
377
+ title: "t",
378
+ description: "original task description",
379
+ } as unknown as Task);
380
+ vi.spyOn(server.posthogAPI, "getTaskRun").mockResolvedValue({
381
+ id: "test-run-id",
382
+ task: "test-task-id",
383
+ state: {
384
+ pending_user_message:
385
+ '__twig_cloud_prompt_v1__:{"blocks":[{"type":"text","text":"read this attachment"},{"type":"resource","resource":{"uri":"attachment://test.txt","text":"hello from file","mimeType":"text/plain"}}]}',
386
+ },
387
+ } as unknown as TaskRun);
388
+
389
+ const promptSpy = vi.fn().mockResolvedValue({ stopReason: "max_tokens" });
390
+ server.session = {
391
+ payload: TEST_PAYLOAD,
392
+ acpSessionId: "acp-session",
393
+ clientConnection: { prompt: promptSpy },
394
+ logWriter: {
395
+ flushAll: vi.fn().mockResolvedValue(undefined),
396
+ getFullAgentResponse: vi.fn().mockReturnValue(null),
397
+ resetTurnMessages: vi.fn(),
398
+ flush: vi.fn().mockResolvedValue(undefined),
399
+ isRegistered: vi.fn().mockReturnValue(true),
400
+ },
401
+ };
402
+
403
+ await server.sendInitialTaskMessage(TEST_PAYLOAD);
404
+
405
+ expect(promptSpy).toHaveBeenCalledWith({
406
+ sessionId: "acp-session",
407
+ prompt: [
408
+ { type: "text", text: "read this attachment" },
409
+ {
410
+ type: "resource",
411
+ resource: {
412
+ uri: "attachment://test.txt",
413
+ text: "hello from file",
414
+ mimeType: "text/plain",
415
+ },
416
+ },
417
+ ],
418
+ });
419
+ });
420
+
374
421
  it("uses run state initial_prompt_override when present", async () => {
375
422
  vi.spyOn(server.posthogAPI, "getTask").mockResolvedValue({
376
423
  id: "test-task-id",
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from "vitest";
2
- import { mcpServersSchema } from "./schemas";
2
+ import { mcpServersSchema, validateCommandParams } from "./schemas";
3
3
 
4
4
  describe("mcpServersSchema", () => {
5
5
  it("accepts a valid HTTP server", () => {
@@ -115,3 +115,21 @@ describe("mcpServersSchema", () => {
115
115
  expect(result.success).toBe(false);
116
116
  });
117
117
  });
118
+
119
+ describe("validateCommandParams", () => {
120
+ it("accepts structured user_message content arrays", () => {
121
+ const result = validateCommandParams("user_message", {
122
+ content: [{ type: "text", text: "hello" }],
123
+ });
124
+
125
+ expect(result.success).toBe(true);
126
+ });
127
+
128
+ it("rejects empty content array", () => {
129
+ const result = validateCommandParams("user_message", {
130
+ content: [],
131
+ });
132
+
133
+ expect(result.success).toBe(false);
134
+ });
135
+ });
@@ -42,7 +42,10 @@ export const jsonRpcRequestSchema = z.object({
42
42
  export type JsonRpcRequest = z.infer<typeof jsonRpcRequestSchema>;
43
43
 
44
44
  export const userMessageParamsSchema = z.object({
45
- content: z.string().min(1, "Content is required"),
45
+ content: z.union([
46
+ z.string().min(1, "Content is required"),
47
+ z.array(z.record(z.string(), z.unknown())).min(1, "Content is required"),
48
+ ]),
46
49
  });
47
50
 
48
51
  export const commandParamsSchemas = {