@posthog/agent 2.3.425 → 2.3.449

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.425",
3
+ "version": "2.3.449",
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": {
@@ -1047,6 +1047,9 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
1047
1047
  }
1048
1048
  const previousMode = this.session.permissionMode;
1049
1049
  this.session.permissionMode = modeId as CodeExecutionMode;
1050
+ if (modeId === "plan" && previousMode !== "plan") {
1051
+ this.session.modeBeforePlan = previousMode;
1052
+ }
1050
1053
  try {
1051
1054
  await this.session.query.setPermissionMode(modeId as CodeExecutionMode);
1052
1055
  } catch (error) {
@@ -1343,7 +1346,11 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
1343
1346
  private createOnModeChange() {
1344
1347
  return async (newMode: CodeExecutionMode) => {
1345
1348
  if (this.session) {
1349
+ const previousMode = this.session.permissionMode;
1346
1350
  this.session.permissionMode = newMode;
1351
+ if (newMode === "plan" && previousMode !== "plan") {
1352
+ this.session.modeBeforePlan = previousMode;
1353
+ }
1347
1354
  }
1348
1355
  await this.updateConfigOption("mode", newMode);
1349
1356
  };
@@ -142,7 +142,7 @@ async function requestPlanApproval(
142
142
  context: ToolHandlerContext,
143
143
  updatedInput: Record<string, unknown>,
144
144
  ): Promise<RequestPermissionResponse> {
145
- const { client, sessionId, toolUseID } = context;
145
+ const { client, sessionId, toolUseID, session } = context;
146
146
 
147
147
  const toolInfo = toolInfoFromToolUse({
148
148
  name: context.toolName,
@@ -150,7 +150,7 @@ async function requestPlanApproval(
150
150
  });
151
151
 
152
152
  return await client.requestPermission({
153
- options: buildExitPlanModePermissionOptions(),
153
+ options: buildExitPlanModePermissionOptions(session.modeBeforePlan),
154
154
  sessionId,
155
155
  toolCall: {
156
156
  toolCallId: toolUseID,
@@ -0,0 +1,51 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { buildExitPlanModePermissionOptions } from "./permission-options";
3
+
4
+ describe("buildExitPlanModePermissionOptions", () => {
5
+ it("does not relabel any option when no previous mode is provided", () => {
6
+ const options = buildExitPlanModePermissionOptions();
7
+ for (const opt of options) {
8
+ expect(opt.name).not.toMatch(/^Yes, continue/);
9
+ }
10
+ expect(options[options.length - 1].optionId).toBe("reject_with_feedback");
11
+ });
12
+
13
+ it("promotes the previous mode to the first position with a continue label", () => {
14
+ const options = buildExitPlanModePermissionOptions("default");
15
+ expect(options[0]).toMatchObject({
16
+ optionId: "default",
17
+ name: "Yes, continue manually approving edits",
18
+ });
19
+ expect(options[options.length - 1].optionId).toBe("reject_with_feedback");
20
+ });
21
+
22
+ it("relabels the auto option when it is the previous mode", () => {
23
+ const options = buildExitPlanModePermissionOptions("auto");
24
+ expect(options[0]).toMatchObject({
25
+ optionId: "auto",
26
+ name: 'Yes, continue in "auto" mode',
27
+ });
28
+ });
29
+
30
+ it("relabels the acceptEdits option when it is the previous mode", () => {
31
+ const options = buildExitPlanModePermissionOptions("acceptEdits");
32
+ expect(options[0]).toMatchObject({
33
+ optionId: "acceptEdits",
34
+ name: "Yes, continue auto-accepting edits",
35
+ });
36
+ });
37
+
38
+ it("ignores an unknown previous mode", () => {
39
+ const options = buildExitPlanModePermissionOptions("plan");
40
+ expect(options[0].name).toMatch(/^Yes, /);
41
+ expect(options[0].name).not.toMatch(/^Yes, continue/);
42
+ expect(options[options.length - 1].optionId).toBe("reject_with_feedback");
43
+ });
44
+
45
+ it("always keeps the reject option last", () => {
46
+ for (const previousMode of ["auto", "acceptEdits", "default", undefined]) {
47
+ const options = buildExitPlanModePermissionOptions(previousMode);
48
+ expect(options[options.length - 1].optionId).toBe("reject_with_feedback");
49
+ }
50
+ });
51
+ });
@@ -92,7 +92,16 @@ export function buildPermissionOptions(
92
92
  return permissionOptions("Yes, always allow");
93
93
  }
94
94
 
95
- export function buildExitPlanModePermissionOptions(): PermissionOption[] {
95
+ const CONTINUE_LABELS: Record<string, string> = {
96
+ auto: 'Yes, continue in "auto" mode',
97
+ acceptEdits: "Yes, continue auto-accepting edits",
98
+ default: "Yes, continue manually approving edits",
99
+ bypassPermissions: "Yes, continue bypassing all permissions",
100
+ };
101
+
102
+ export function buildExitPlanModePermissionOptions(
103
+ previousMode?: string,
104
+ ): PermissionOption[] {
96
105
  const options: PermissionOption[] = [];
97
106
 
98
107
  if (ALLOW_BYPASS) {
@@ -119,13 +128,30 @@ export function buildExitPlanModePermissionOptions(): PermissionOption[] {
119
128
  name: "Yes, and manually approve edits",
120
129
  optionId: "default",
121
130
  },
122
- {
123
- kind: "reject_once",
124
- name: "No, and tell the agent what to do differently",
125
- optionId: "reject_with_feedback",
126
- _meta: { customInput: true },
127
- },
128
131
  );
129
132
 
133
+ const previousIndex = previousMode
134
+ ? options.findIndex((opt) => opt.optionId === previousMode)
135
+ : -1;
136
+ if (previousIndex > 0) {
137
+ const [previous] = options.splice(previousIndex, 1);
138
+ const continueLabel = CONTINUE_LABELS[previous.optionId];
139
+ options.unshift(
140
+ continueLabel ? { ...previous, name: continueLabel } : previous,
141
+ );
142
+ } else if (previousIndex === 0) {
143
+ const continueLabel = CONTINUE_LABELS[options[0].optionId];
144
+ if (continueLabel) {
145
+ options[0] = { ...options[0], name: continueLabel };
146
+ }
147
+ }
148
+
149
+ options.push({
150
+ kind: "reject_once",
151
+ name: "No, and tell the agent what to do differently",
152
+ optionId: "reject_with_feedback",
153
+ _meta: { customInput: true },
154
+ });
155
+
130
156
  return options;
131
157
  }
@@ -46,6 +46,7 @@ export type Session = BaseSession & {
46
46
  input: Pushable<SDKUserMessage>;
47
47
  settingsManager: SettingsManager;
48
48
  permissionMode: CodeExecutionMode;
49
+ modeBeforePlan?: CodeExecutionMode;
49
50
  modelId?: string;
50
51
  cwd: string;
51
52
  taskRunId?: string;
@@ -202,6 +202,44 @@ describe("AgentServer HTTP Mode", () => {
202
202
  }, 30000);
203
203
  });
204
204
 
205
+ describe("turn completion", () => {
206
+ it("persists structured turn completion notifications", () => {
207
+ const appendRawLine = vi.fn();
208
+ const testServer = new AgentServer({
209
+ port,
210
+ jwtPublicKey: TEST_PUBLIC_KEY,
211
+ repositoryPath: repo.path,
212
+ apiUrl: "http://localhost:8000",
213
+ apiKey: "test-api-key",
214
+ projectId: 1,
215
+ mode: "interactive",
216
+ taskId: "test-task-id",
217
+ runId: "test-run-id",
218
+ }) as unknown as {
219
+ session: unknown;
220
+ broadcastTurnComplete(stopReason: string): void;
221
+ };
222
+ testServer.session = {
223
+ acpSessionId: "session-1",
224
+ payload: { run_id: "run-1" },
225
+ logWriter: { appendRawLine },
226
+ };
227
+
228
+ testServer.broadcastTurnComplete("end_turn");
229
+
230
+ expect(appendRawLine).toHaveBeenCalledTimes(1);
231
+ expect(appendRawLine.mock.calls[0][0]).toBe("run-1");
232
+ expect(JSON.parse(appendRawLine.mock.calls[0][1])).toEqual({
233
+ jsonrpc: "2.0",
234
+ method: "_posthog/turn_complete",
235
+ params: {
236
+ sessionId: "session-1",
237
+ stopReason: "end_turn",
238
+ },
239
+ });
240
+ });
241
+ });
242
+
205
243
  describe("GET /events", () => {
206
244
  it("returns 401 without authorization header", async () => {
207
245
  await createServer().start();
@@ -2226,18 +2226,25 @@ ${attributionInstructions}
2226
2226
 
2227
2227
  private broadcastTurnComplete(stopReason: string): void {
2228
2228
  if (!this.session) return;
2229
+ const notification = {
2230
+ jsonrpc: "2.0",
2231
+ method: POSTHOG_NOTIFICATIONS.TURN_COMPLETE,
2232
+ params: {
2233
+ sessionId: this.session.acpSessionId,
2234
+ stopReason,
2235
+ },
2236
+ };
2237
+
2229
2238
  this.broadcastEvent({
2230
2239
  type: "notification",
2231
2240
  timestamp: new Date().toISOString(),
2232
- notification: {
2233
- jsonrpc: "2.0",
2234
- method: POSTHOG_NOTIFICATIONS.TURN_COMPLETE,
2235
- params: {
2236
- sessionId: this.session.acpSessionId,
2237
- stopReason,
2238
- },
2239
- },
2241
+ notification,
2240
2242
  });
2243
+
2244
+ this.session.logWriter.appendRawLine(
2245
+ this.session.payload.run_id,
2246
+ JSON.stringify(notification),
2247
+ );
2241
2248
  }
2242
2249
 
2243
2250
  private broadcastEvent(event: Record<string, unknown>): void {