@posthog/agent 2.3.209 → 2.3.216
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/dist/agent.js +1 -1
- package/dist/agent.js.map +1 -1
- package/dist/posthog-api.js +1 -1
- package/dist/posthog-api.js.map +1 -1
- package/dist/server/agent-server.d.ts +4 -1
- package/dist/server/agent-server.js +9365 -9254
- package/dist/server/agent-server.js.map +1 -1
- package/dist/server/bin.cjs +9347 -9236
- package/dist/server/bin.cjs.map +1 -1
- package/package.json +1 -1
- package/src/server/agent-server.test.ts +24 -0
- package/src/server/agent-server.ts +113 -31
- package/src/server/cloud-prompt.ts +13 -0
- package/src/server/question-relay.test.ts +47 -0
- package/src/server/schemas.test.ts +19 -1
- package/src/server/schemas.ts +4 -1
package/package.json
CHANGED
|
@@ -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,9 +1,11 @@
|
|
|
1
|
+
import type { ContentBlock } from "@agentclientprotocol/sdk";
|
|
1
2
|
import {
|
|
2
3
|
ClientSideConnection,
|
|
3
4
|
ndJsonStream,
|
|
4
5
|
PROTOCOL_VERSION,
|
|
5
6
|
} from "@agentclientprotocol/sdk";
|
|
6
7
|
import { type ServerType, serve } from "@hono/node-server";
|
|
8
|
+
import { getCurrentBranch } from "@posthog/git/queries";
|
|
7
9
|
import { Hono } from "hono";
|
|
8
10
|
import packageJson from "../../package.json" with { type: "json" };
|
|
9
11
|
import { POSTHOG_NOTIFICATIONS } from "../acp-extensions";
|
|
@@ -30,6 +32,11 @@ import type {
|
|
|
30
32
|
import { AsyncMutex } from "../utils/async-mutex";
|
|
31
33
|
import { getLlmGatewayUrl } from "../utils/gateway";
|
|
32
34
|
import { Logger } from "../utils/logger";
|
|
35
|
+
import {
|
|
36
|
+
deserializeCloudPrompt,
|
|
37
|
+
normalizeCloudPromptContent,
|
|
38
|
+
promptBlocksToText,
|
|
39
|
+
} from "./cloud-prompt";
|
|
33
40
|
import { type JwtPayload, JwtValidationError, validateJwt } from "./jwt";
|
|
34
41
|
import { jsonRpcRequestSchema, validateCommandParams } from "./schemas";
|
|
35
42
|
import type { AgentServerConfig } from "./types";
|
|
@@ -161,6 +168,7 @@ export class AgentServer {
|
|
|
161
168
|
private posthogAPI: PostHogAPIClient;
|
|
162
169
|
private questionRelayedToSlack = false;
|
|
163
170
|
private detectedPrUrl: string | null = null;
|
|
171
|
+
private lastReportedBranch: string | null = null;
|
|
164
172
|
private resumeState: ResumeState | null = null;
|
|
165
173
|
// Guards against concurrent session initialization. autoInitializeSession() and
|
|
166
174
|
// the GET /events SSE handler can both call initializeSession() — the SSE connection
|
|
@@ -487,17 +495,20 @@ export class AgentServer {
|
|
|
487
495
|
switch (method) {
|
|
488
496
|
case POSTHOG_NOTIFICATIONS.USER_MESSAGE:
|
|
489
497
|
case "user_message": {
|
|
490
|
-
const
|
|
498
|
+
const prompt = normalizeCloudPromptContent(
|
|
499
|
+
params.content as string | ContentBlock[],
|
|
500
|
+
);
|
|
501
|
+
const promptPreview = promptBlocksToText(prompt);
|
|
491
502
|
|
|
492
503
|
this.logger.info(
|
|
493
|
-
`Processing user message (detectedPrUrl=${this.detectedPrUrl ?? "none"}): ${
|
|
504
|
+
`Processing user message (detectedPrUrl=${this.detectedPrUrl ?? "none"}): ${promptPreview.substring(0, 100)}...`,
|
|
494
505
|
);
|
|
495
506
|
|
|
496
507
|
this.session.logWriter.resetTurnMessages(this.session.payload.run_id);
|
|
497
508
|
|
|
498
509
|
const result = await this.session.clientConnection.prompt({
|
|
499
510
|
sessionId: this.session.acpSessionId,
|
|
500
|
-
prompt
|
|
511
|
+
prompt,
|
|
501
512
|
...(this.detectedPrUrl && {
|
|
502
513
|
_meta: {
|
|
503
514
|
prContext:
|
|
@@ -515,6 +526,10 @@ export class AgentServer {
|
|
|
515
526
|
stopReason: result.stopReason,
|
|
516
527
|
});
|
|
517
528
|
|
|
529
|
+
if (result.stopReason === "end_turn") {
|
|
530
|
+
void this.syncCloudBranchMetadata(this.session.payload);
|
|
531
|
+
}
|
|
532
|
+
|
|
518
533
|
this.broadcastTurnComplete(result.stopReason);
|
|
519
534
|
|
|
520
535
|
if (result.stopReason === "end_turn") {
|
|
@@ -837,30 +852,43 @@ export class AgentServer {
|
|
|
837
852
|
const initialPromptOverride = taskRun
|
|
838
853
|
? this.getInitialPromptOverride(taskRun)
|
|
839
854
|
: null;
|
|
840
|
-
const
|
|
855
|
+
const pendingUserPrompt = this.getPendingUserPrompt(taskRun);
|
|
856
|
+
let initialPrompt: ContentBlock[] = [];
|
|
857
|
+
if (pendingUserPrompt?.length) {
|
|
858
|
+
initialPrompt = pendingUserPrompt;
|
|
859
|
+
} else if (initialPromptOverride) {
|
|
860
|
+
initialPrompt = [{ type: "text", text: initialPromptOverride }];
|
|
861
|
+
} else if (task.description) {
|
|
862
|
+
initialPrompt = [{ type: "text", text: task.description }];
|
|
863
|
+
}
|
|
841
864
|
|
|
842
|
-
if (
|
|
865
|
+
if (initialPrompt.length === 0) {
|
|
843
866
|
this.logger.warn("Task has no description, skipping initial message");
|
|
844
867
|
return;
|
|
845
868
|
}
|
|
846
869
|
|
|
847
870
|
this.logger.info("Sending initial task message", {
|
|
848
871
|
taskId: payload.task_id,
|
|
849
|
-
descriptionLength: initialPrompt.length,
|
|
872
|
+
descriptionLength: promptBlocksToText(initialPrompt).length,
|
|
850
873
|
usedInitialPromptOverride: !!initialPromptOverride,
|
|
874
|
+
usedPendingUserMessage: !!pendingUserPrompt?.length,
|
|
851
875
|
});
|
|
852
876
|
|
|
853
877
|
this.session.logWriter.resetTurnMessages(payload.run_id);
|
|
854
878
|
|
|
855
879
|
const result = await this.session.clientConnection.prompt({
|
|
856
880
|
sessionId: this.session.acpSessionId,
|
|
857
|
-
prompt:
|
|
881
|
+
prompt: initialPrompt,
|
|
858
882
|
});
|
|
859
883
|
|
|
860
884
|
this.logger.info("Initial task message completed", {
|
|
861
885
|
stopReason: result.stopReason,
|
|
862
886
|
});
|
|
863
887
|
|
|
888
|
+
if (result.stopReason === "end_turn") {
|
|
889
|
+
void this.syncCloudBranchMetadata(payload);
|
|
890
|
+
}
|
|
891
|
+
|
|
864
892
|
this.broadcastTurnComplete(result.stopReason);
|
|
865
893
|
|
|
866
894
|
if (result.stopReason === "end_turn") {
|
|
@@ -886,38 +914,49 @@ export class AgentServer {
|
|
|
886
914
|
this.resumeState.conversation,
|
|
887
915
|
);
|
|
888
916
|
|
|
889
|
-
// Read the pending user
|
|
917
|
+
// Read the pending user prompt from TaskRun state (set by the workflow
|
|
890
918
|
// when the user sends a follow-up message that triggers a resume).
|
|
891
|
-
const
|
|
919
|
+
const pendingUserPrompt = this.getPendingUserPrompt(taskRun);
|
|
892
920
|
|
|
893
921
|
const sandboxContext = this.resumeState.snapshotApplied
|
|
894
922
|
? `The workspace environment (all files, packages, and code changes) has been fully restored from where you left off.`
|
|
895
923
|
: `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
924
|
|
|
897
|
-
let
|
|
898
|
-
if (
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
925
|
+
let resumePromptBlocks: ContentBlock[];
|
|
926
|
+
if (pendingUserPrompt?.length) {
|
|
927
|
+
resumePromptBlocks = [
|
|
928
|
+
{
|
|
929
|
+
type: "text",
|
|
930
|
+
text:
|
|
931
|
+
`You are resuming a previous conversation. ${sandboxContext}\n\n` +
|
|
932
|
+
`Here is the conversation history from the previous session:\n\n` +
|
|
933
|
+
`${conversationSummary}\n\n` +
|
|
934
|
+
`The user has sent a new message:\n\n`,
|
|
935
|
+
},
|
|
936
|
+
...pendingUserPrompt,
|
|
937
|
+
{
|
|
938
|
+
type: "text",
|
|
939
|
+
text: "\n\nRespond to the user's new message above. You have full context from the previous session.",
|
|
940
|
+
},
|
|
941
|
+
];
|
|
908
942
|
} else {
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
943
|
+
resumePromptBlocks = [
|
|
944
|
+
{
|
|
945
|
+
type: "text",
|
|
946
|
+
text:
|
|
947
|
+
`You are resuming a previous conversation. ${sandboxContext}\n\n` +
|
|
948
|
+
`Here is the conversation history from the previous session:\n\n` +
|
|
949
|
+
`${conversationSummary}\n\n` +
|
|
950
|
+
`Continue from where you left off. The user is waiting for your response.`,
|
|
951
|
+
},
|
|
952
|
+
];
|
|
914
953
|
}
|
|
915
954
|
|
|
916
955
|
this.logger.info("Sending resume message", {
|
|
917
956
|
taskId: payload.task_id,
|
|
918
957
|
conversationTurns: this.resumeState.conversation.length,
|
|
919
|
-
promptLength:
|
|
920
|
-
hasPendingUserMessage: !!
|
|
958
|
+
promptLength: promptBlocksToText(resumePromptBlocks).length,
|
|
959
|
+
hasPendingUserMessage: !!pendingUserPrompt?.length,
|
|
921
960
|
snapshotApplied: this.resumeState.snapshotApplied,
|
|
922
961
|
});
|
|
923
962
|
|
|
@@ -928,13 +967,17 @@ export class AgentServer {
|
|
|
928
967
|
|
|
929
968
|
const result = await this.session.clientConnection.prompt({
|
|
930
969
|
sessionId: this.session.acpSessionId,
|
|
931
|
-
prompt:
|
|
970
|
+
prompt: resumePromptBlocks,
|
|
932
971
|
});
|
|
933
972
|
|
|
934
973
|
this.logger.info("Resume message completed", {
|
|
935
974
|
stopReason: result.stopReason,
|
|
936
975
|
});
|
|
937
976
|
|
|
977
|
+
if (result.stopReason === "end_turn") {
|
|
978
|
+
void this.syncCloudBranchMetadata(payload);
|
|
979
|
+
}
|
|
980
|
+
|
|
938
981
|
this.broadcastTurnComplete(result.stopReason);
|
|
939
982
|
|
|
940
983
|
if (result.stopReason === "end_turn") {
|
|
@@ -1013,7 +1056,7 @@ export class AgentServer {
|
|
|
1013
1056
|
return trimmed.length > 0 ? trimmed : null;
|
|
1014
1057
|
}
|
|
1015
1058
|
|
|
1016
|
-
private
|
|
1059
|
+
private getPendingUserPrompt(taskRun: TaskRun | null): ContentBlock[] | null {
|
|
1017
1060
|
if (!taskRun) return null;
|
|
1018
1061
|
const state = taskRun.state as Record<string, unknown> | undefined;
|
|
1019
1062
|
const message = state?.pending_user_message;
|
|
@@ -1021,8 +1064,8 @@ export class AgentServer {
|
|
|
1021
1064
|
return null;
|
|
1022
1065
|
}
|
|
1023
1066
|
|
|
1024
|
-
const
|
|
1025
|
-
return
|
|
1067
|
+
const prompt = deserializeCloudPrompt(message);
|
|
1068
|
+
return prompt.length > 0 ? prompt : null;
|
|
1026
1069
|
}
|
|
1027
1070
|
|
|
1028
1071
|
private getResumeRunId(taskRun: TaskRun | null): string | null {
|
|
@@ -1139,6 +1182,44 @@ ${attributionInstructions}
|
|
|
1139
1182
|
`;
|
|
1140
1183
|
}
|
|
1141
1184
|
|
|
1185
|
+
private async getCurrentGitBranch(): Promise<string | null> {
|
|
1186
|
+
if (!this.config.repositoryPath) {
|
|
1187
|
+
return null;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
try {
|
|
1191
|
+
return await getCurrentBranch(this.config.repositoryPath);
|
|
1192
|
+
} catch (error) {
|
|
1193
|
+
this.logger.warn("Failed to determine current git branch", {
|
|
1194
|
+
repositoryPath: this.config.repositoryPath,
|
|
1195
|
+
error,
|
|
1196
|
+
});
|
|
1197
|
+
return null;
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
private async syncCloudBranchMetadata(payload: JwtPayload): Promise<void> {
|
|
1202
|
+
const branchName = await this.getCurrentGitBranch();
|
|
1203
|
+
if (!branchName || branchName === this.lastReportedBranch) {
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
try {
|
|
1208
|
+
await this.posthogAPI.updateTaskRun(payload.task_id, payload.run_id, {
|
|
1209
|
+
branch: branchName,
|
|
1210
|
+
output: { head_branch: branchName },
|
|
1211
|
+
});
|
|
1212
|
+
this.lastReportedBranch = branchName;
|
|
1213
|
+
} catch (error) {
|
|
1214
|
+
this.logger.warn("Failed to attach current branch to task run", {
|
|
1215
|
+
taskId: payload.task_id,
|
|
1216
|
+
runId: payload.run_id,
|
|
1217
|
+
branchName,
|
|
1218
|
+
error,
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1142
1223
|
private async signalTaskComplete(
|
|
1143
1224
|
payload: JwtPayload,
|
|
1144
1225
|
stopReason: string,
|
|
@@ -1527,6 +1608,7 @@ ${attributionInstructions}
|
|
|
1527
1608
|
}
|
|
1528
1609
|
|
|
1529
1610
|
this.pendingEvents = [];
|
|
1611
|
+
this.lastReportedBranch = null;
|
|
1530
1612
|
this.session = null;
|
|
1531
1613
|
}
|
|
1532
1614
|
|
|
@@ -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
|
+
});
|
package/src/server/schemas.ts
CHANGED
|
@@ -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.
|
|
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 = {
|