@posthog/agent 2.3.696 → 2.3.703

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.696",
3
+ "version": "2.3.703",
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": {
@@ -106,8 +106,8 @@
106
106
  "tsx": "^4.20.6",
107
107
  "typescript": "^5.5.0",
108
108
  "vitest": "^2.1.8",
109
- "@posthog/enricher": "1.0.0",
110
109
  "@posthog/shared": "1.0.0",
110
+ "@posthog/enricher": "1.0.0",
111
111
  "@posthog/git": "1.0.0"
112
112
  },
113
113
  "dependencies": {
@@ -0,0 +1,153 @@
1
+ import type { AgentSideConnection } from "@agentclientprotocol/sdk";
2
+ import type { SDKMessage } from "@anthropic-ai/claude-agent-sdk";
3
+ import { beforeEach, describe, expect, it, vi } from "vitest";
4
+ import { createMockQuery, type MockQuery } from "../../test/mocks/claude-sdk";
5
+ import { Pushable } from "../../utils/streams";
6
+
7
+ vi.mock("@anthropic-ai/claude-agent-sdk", () => ({
8
+ query: vi.fn(),
9
+ }));
10
+
11
+ vi.mock("./mcp/tool-metadata", () => ({
12
+ fetchMcpToolMetadata: vi.fn().mockResolvedValue(undefined),
13
+ getConnectedMcpServerNames: vi.fn().mockReturnValue([]),
14
+ setMcpToolApprovalStates: vi.fn(),
15
+ isMcpToolReadOnly: vi.fn().mockReturnValue(false),
16
+ getMcpToolMetadata: vi.fn().mockReturnValue(undefined),
17
+ getMcpToolApprovalState: vi.fn().mockReturnValue(undefined),
18
+ }));
19
+
20
+ const { ClaudeAcpAgent } = await import("./claude-agent");
21
+ type Agent = InstanceType<typeof ClaudeAcpAgent>;
22
+
23
+ interface ClientMocks {
24
+ sessionUpdate: ReturnType<typeof vi.fn>;
25
+ extNotification: ReturnType<typeof vi.fn>;
26
+ }
27
+
28
+ function makeAgent(): { agent: Agent; client: ClientMocks } {
29
+ const client: ClientMocks = {
30
+ sessionUpdate: vi.fn().mockResolvedValue(undefined),
31
+ extNotification: vi.fn().mockResolvedValue(undefined),
32
+ };
33
+ const agent = new ClaudeAcpAgent(client as unknown as AgentSideConnection);
34
+ return { agent, client };
35
+ }
36
+
37
+ function installFakeSession(agent: Agent, sessionId: string): MockQuery {
38
+ const query = createMockQuery();
39
+ const input = new Pushable();
40
+ const abortController = new AbortController();
41
+
42
+ const session = {
43
+ query,
44
+ queryOptions: { sessionId, cwd: "/tmp/repo", abortController },
45
+ input,
46
+ cancelled: false,
47
+ interruptReason: undefined,
48
+ settingsManager: { dispose: vi.fn(), getRepoRoot: () => "/tmp/repo" },
49
+ permissionMode: "default" as const,
50
+ abortController,
51
+ accumulatedUsage: {
52
+ inputTokens: 0,
53
+ outputTokens: 0,
54
+ cachedReadTokens: 0,
55
+ cachedWriteTokens: 0,
56
+ },
57
+ configOptions: [],
58
+ promptRunning: false,
59
+ pendingMessages: new Map(),
60
+ nextPendingOrder: 0,
61
+ cwd: "/tmp/repo",
62
+ notificationHistory: [] as unknown[],
63
+ taskRunId: "run-1",
64
+ lastContextWindowSize: 200_000,
65
+ modelId: "claude-sonnet-4-6",
66
+ };
67
+
68
+ (agent as unknown as { session: typeof session }).session = session;
69
+ (agent as unknown as { sessionId: string }).sessionId = sessionId;
70
+
71
+ return query;
72
+ }
73
+
74
+ function findUnsupportedChunkText(
75
+ calls: ClientMocks["sessionUpdate"]["mock"]["calls"],
76
+ ): string | undefined {
77
+ const match = calls.find(([call]) => {
78
+ const update = (
79
+ call as {
80
+ update?: { sessionUpdate?: string; content?: { text?: string } };
81
+ }
82
+ ).update;
83
+ return (
84
+ update?.sessionUpdate === "agent_message_chunk" &&
85
+ update?.content?.text?.toLowerCase().includes("unsupported")
86
+ );
87
+ });
88
+ return (match?.[0] as { update: { content: { text: string } } } | undefined)
89
+ ?.update.content.text;
90
+ }
91
+
92
+ describe("ClaudeAcpAgent.prompt — early idle handling", () => {
93
+ beforeEach(() => {
94
+ vi.clearAllMocks();
95
+ });
96
+
97
+ const cases = [
98
+ {
99
+ label: "unsupported slash command surfaces error and ends turn",
100
+ sessionId: "s-slash",
101
+ prompt: "/plugin install slack",
102
+ expectsUnsupportedChunk: true,
103
+ commandInMessage: "/plugin",
104
+ },
105
+ {
106
+ label: "non-slash prompt with early idle is silently skipped",
107
+ sessionId: "s-regular",
108
+ prompt: "hello",
109
+ expectsUnsupportedChunk: false,
110
+ commandInMessage: null,
111
+ },
112
+ ] as const;
113
+
114
+ it.each(cases)("$label", async (tc) => {
115
+ const { agent, client } = makeAgent();
116
+ const query = installFakeSession(agent, tc.sessionId);
117
+
118
+ const promptPromise = agent.prompt({
119
+ sessionId: tc.sessionId,
120
+ prompt: [{ type: "text", text: tc.prompt }],
121
+ });
122
+
123
+ // Let the prompt loop start awaiting the first SDK message.
124
+ await new Promise((resolve) => setImmediate(resolve));
125
+
126
+ query._mockHelpers.sendMessage({
127
+ type: "system",
128
+ subtype: "session_state_changed",
129
+ state: "idle",
130
+ } as unknown as SDKMessage);
131
+ query._mockHelpers.complete();
132
+
133
+ if (tc.expectsUnsupportedChunk) {
134
+ const result = await promptPromise;
135
+ expect(result.stopReason).toBe("end_turn");
136
+
137
+ const text = findUnsupportedChunkText(client.sessionUpdate.mock.calls);
138
+ expect(text).toBeDefined();
139
+ if (tc.commandInMessage) {
140
+ expect(text).toContain(tc.commandInMessage);
141
+ }
142
+ } else {
143
+ // No unsupported chunk; loop falls through to the existing
144
+ // "Session did not end in result" failure path.
145
+ await expect(promptPromise).rejects.toThrow(
146
+ /Session did not end in result/,
147
+ );
148
+ expect(
149
+ findUnsupportedChunkText(client.sessionUpdate.mock.calls),
150
+ ).toBeUndefined();
151
+ }
152
+ });
153
+ });
@@ -496,6 +496,28 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
496
496
  (message as Record<string, unknown>).state === "idle"
497
497
  ) {
498
498
  if (!promptReplayed) {
499
+ // The SDK consumed a slash command we do not handle locally
500
+ // and produced no output (e.g. /plugin in a non-interactive
501
+ // context). Without this branch we would loop forever waiting
502
+ // for an echo that never comes; surface a clear error instead.
503
+ if (commandMatch) {
504
+ const cmd = commandMatch[1];
505
+ this.logger.warn(
506
+ "Slash command produced no output; treating as unsupported",
507
+ { sessionId: params.sessionId, command: cmd },
508
+ );
509
+ await this.client.sessionUpdate({
510
+ sessionId: params.sessionId,
511
+ update: {
512
+ sessionUpdate: "agent_message_chunk",
513
+ content: {
514
+ type: "text",
515
+ text: `Unsupported slash command: \`${cmd}\`. PostHog Code does not implement this command.`,
516
+ },
517
+ },
518
+ });
519
+ return { stopReason: "end_turn" };
520
+ }
499
521
  this.logger.debug("Skipping idle state before prompt replay", {
500
522
  sessionId: params.sessionId,
501
523
  });