@posthog/agent 2.3.209 → 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/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 +1 -1
- package/dist/server/agent-server.js +82 -25
- package/dist/server/agent-server.js.map +1 -1
- package/dist/server/bin.cjs +82 -25
- 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 +60 -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,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
|
|
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"}): ${
|
|
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
|
|
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
|
|
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 (
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
898
|
-
if (
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
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
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
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:
|
|
920
|
-
hasPendingUserMessage: !!
|
|
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:
|
|
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
|
|
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
|
|
1025
|
-
return
|
|
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
|
+
});
|
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 = {
|