@posthog/agent 2.3.5 → 2.3.10
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 +3 -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 +8 -0
- package/dist/server/agent-server.js +1077 -578
- package/dist/server/agent-server.js.map +1 -1
- package/dist/server/bin.cjs +1132 -633
- package/dist/server/bin.cjs.map +1 -1
- package/package.json +1 -1
- package/src/acp-extensions.ts +3 -0
- package/src/resume.ts +1 -1
- package/src/sagas/resume-saga.ts +10 -2
- package/src/server/agent-server.ts +265 -30
package/package.json
CHANGED
package/src/acp-extensions.ts
CHANGED
|
@@ -26,6 +26,9 @@ export const POSTHOG_NOTIFICATIONS = {
|
|
|
26
26
|
/** Task has completed (success or failure) */
|
|
27
27
|
TASK_COMPLETE: "_posthog/task_complete",
|
|
28
28
|
|
|
29
|
+
/** Agent finished processing a turn (prompt returned, waiting for next input) */
|
|
30
|
+
TURN_COMPLETE: "_posthog/turn_complete",
|
|
31
|
+
|
|
29
32
|
/** Error occurred during task execution */
|
|
30
33
|
ERROR: "_posthog/error",
|
|
31
34
|
|
package/src/resume.ts
CHANGED
package/src/sagas/resume-saga.ts
CHANGED
|
@@ -26,7 +26,7 @@ export interface ToolCallInfo {
|
|
|
26
26
|
export interface ResumeInput {
|
|
27
27
|
taskId: string;
|
|
28
28
|
runId: string;
|
|
29
|
-
repositoryPath
|
|
29
|
+
repositoryPath?: string;
|
|
30
30
|
apiClient: PostHogAPIClient;
|
|
31
31
|
logger?: Logger;
|
|
32
32
|
}
|
|
@@ -78,7 +78,7 @@ export class ResumeSaga extends Saga<ResumeInput, ResumeOutput> {
|
|
|
78
78
|
// Step 4: Apply snapshot if present (wrapped in step for consistent logging)
|
|
79
79
|
// Note: We use a try/catch inside the step because snapshot failure should NOT fail the saga
|
|
80
80
|
let snapshotApplied = false;
|
|
81
|
-
if (latestSnapshot?.archiveUrl) {
|
|
81
|
+
if (latestSnapshot?.archiveUrl && repositoryPath) {
|
|
82
82
|
this.log.info("Found tree snapshot", {
|
|
83
83
|
treeHash: latestSnapshot.treeHash,
|
|
84
84
|
hasArchiveUrl: true,
|
|
@@ -120,6 +120,14 @@ export class ResumeSaga extends Saga<ResumeInput, ResumeOutput> {
|
|
|
120
120
|
// Inner ApplySnapshotSaga handles its own rollback
|
|
121
121
|
},
|
|
122
122
|
});
|
|
123
|
+
} else if (latestSnapshot?.archiveUrl && !repositoryPath) {
|
|
124
|
+
this.log.warn(
|
|
125
|
+
"Snapshot found but no repositoryPath configured - files cannot be restored",
|
|
126
|
+
{
|
|
127
|
+
treeHash: latestSnapshot.treeHash,
|
|
128
|
+
changes: latestSnapshot.changes?.length ?? 0,
|
|
129
|
+
},
|
|
130
|
+
);
|
|
123
131
|
} else if (latestSnapshot) {
|
|
124
132
|
this.log.warn(
|
|
125
133
|
"Snapshot found but has no archive URL - files cannot be restored",
|
|
@@ -11,7 +11,13 @@ import {
|
|
|
11
11
|
createAcpConnection,
|
|
12
12
|
type InProcessAcpConnection,
|
|
13
13
|
} from "../adapters/acp-connection.js";
|
|
14
|
+
import { selectRecentTurns } from "../adapters/claude/session/jsonl-hydration.js";
|
|
14
15
|
import { PostHogAPIClient } from "../posthog-api.js";
|
|
16
|
+
import {
|
|
17
|
+
type ConversationTurn,
|
|
18
|
+
type ResumeState,
|
|
19
|
+
resumeFromLog,
|
|
20
|
+
} from "../resume.js";
|
|
15
21
|
import { SessionLogWriter } from "../session-log-writer.js";
|
|
16
22
|
import { TreeTracker } from "../tree-tracker.js";
|
|
17
23
|
import type {
|
|
@@ -155,6 +161,7 @@ export class AgentServer {
|
|
|
155
161
|
private posthogAPI: PostHogAPIClient;
|
|
156
162
|
private questionRelayedToSlack = false;
|
|
157
163
|
private detectedPrUrl: string | null = null;
|
|
164
|
+
private resumeState: ResumeState | null = null;
|
|
158
165
|
|
|
159
166
|
private emitConsoleLog = (
|
|
160
167
|
level: LogLevel,
|
|
@@ -380,6 +387,34 @@ export class AgentServer {
|
|
|
380
387
|
|
|
381
388
|
this.logger.info("Auto-initializing session", { taskId, runId, mode });
|
|
382
389
|
|
|
390
|
+
// Check if this is a resume from a previous run
|
|
391
|
+
const resumeRunId = process.env.POSTHOG_RESUME_RUN_ID;
|
|
392
|
+
if (resumeRunId) {
|
|
393
|
+
this.logger.info("Resuming from previous run", {
|
|
394
|
+
resumeRunId,
|
|
395
|
+
currentRunId: runId,
|
|
396
|
+
});
|
|
397
|
+
try {
|
|
398
|
+
this.resumeState = await resumeFromLog({
|
|
399
|
+
taskId,
|
|
400
|
+
runId: resumeRunId,
|
|
401
|
+
repositoryPath: this.config.repositoryPath,
|
|
402
|
+
apiClient: this.posthogAPI,
|
|
403
|
+
logger: new Logger({ debug: true, prefix: "[Resume]" }),
|
|
404
|
+
});
|
|
405
|
+
this.logger.info("Resume state loaded", {
|
|
406
|
+
conversationTurns: this.resumeState.conversation.length,
|
|
407
|
+
snapshotApplied: this.resumeState.snapshotApplied,
|
|
408
|
+
logEntries: this.resumeState.logEntryCount,
|
|
409
|
+
});
|
|
410
|
+
} catch (error) {
|
|
411
|
+
this.logger.warn("Failed to load resume state, starting fresh", {
|
|
412
|
+
error,
|
|
413
|
+
});
|
|
414
|
+
this.resumeState = null;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
383
418
|
// Create a synthetic payload from config (no JWT needed for auto-init)
|
|
384
419
|
const payload: JwtPayload = {
|
|
385
420
|
task_id: taskId,
|
|
@@ -464,6 +499,8 @@ export class AgentServer {
|
|
|
464
499
|
}),
|
|
465
500
|
});
|
|
466
501
|
|
|
502
|
+
this.broadcastTurnComplete(result.stopReason);
|
|
503
|
+
|
|
467
504
|
return { stopReason: result.stopReason };
|
|
468
505
|
}
|
|
469
506
|
|
|
@@ -510,22 +547,23 @@ export class AgentServer {
|
|
|
510
547
|
|
|
511
548
|
this.configureEnvironment();
|
|
512
549
|
|
|
550
|
+
const posthogAPI = new PostHogAPIClient({
|
|
551
|
+
apiUrl: this.config.apiUrl,
|
|
552
|
+
projectId: this.config.projectId,
|
|
553
|
+
getApiKey: () => this.config.apiKey,
|
|
554
|
+
userAgent: `posthog/cloud.hog.dev; version: ${this.config.version ?? packageJson.version}`,
|
|
555
|
+
});
|
|
556
|
+
|
|
513
557
|
const treeTracker = this.config.repositoryPath
|
|
514
558
|
? new TreeTracker({
|
|
515
559
|
repositoryPath: this.config.repositoryPath,
|
|
516
560
|
taskId: payload.task_id,
|
|
517
561
|
runId: payload.run_id,
|
|
562
|
+
apiClient: posthogAPI,
|
|
518
563
|
logger: new Logger({ debug: true, prefix: "[TreeTracker]" }),
|
|
519
564
|
})
|
|
520
565
|
: null;
|
|
521
566
|
|
|
522
|
-
const posthogAPI = new PostHogAPIClient({
|
|
523
|
-
apiUrl: this.config.apiUrl,
|
|
524
|
-
projectId: this.config.projectId,
|
|
525
|
-
getApiKey: () => this.config.apiKey,
|
|
526
|
-
userAgent: `posthog/cloud.hog.dev; version: ${this.config.version ?? packageJson.version}`,
|
|
527
|
-
});
|
|
528
|
-
|
|
529
567
|
const logWriter = new SessionLogWriter({
|
|
530
568
|
posthogAPI,
|
|
531
569
|
logger: new Logger({ debug: true, prefix: "[SessionLogWriter]" }),
|
|
@@ -653,27 +691,61 @@ export class AgentServer {
|
|
|
653
691
|
): Promise<void> {
|
|
654
692
|
if (!this.session) return;
|
|
655
693
|
|
|
656
|
-
|
|
657
|
-
|
|
694
|
+
// Fetch TaskRun early — needed for both resume detection and initial prompt
|
|
695
|
+
let taskRun = prefetchedRun ?? null;
|
|
696
|
+
if (!taskRun) {
|
|
697
|
+
try {
|
|
698
|
+
taskRun = await this.posthogAPI.getTaskRun(
|
|
699
|
+
payload.task_id,
|
|
700
|
+
payload.run_id,
|
|
701
|
+
);
|
|
702
|
+
} catch (error) {
|
|
703
|
+
this.logger.warn("Failed to fetch task run", {
|
|
704
|
+
taskId: payload.task_id,
|
|
705
|
+
runId: payload.run_id,
|
|
706
|
+
error,
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
}
|
|
658
710
|
|
|
659
|
-
|
|
660
|
-
|
|
711
|
+
// Check for resume if not already loaded from env var in autoInitializeSession
|
|
712
|
+
if (!this.resumeState) {
|
|
713
|
+
const resumeRunId = this.getResumeRunId(taskRun);
|
|
714
|
+
if (resumeRunId) {
|
|
715
|
+
this.logger.info("Resuming from previous run (via TaskRun state)", {
|
|
716
|
+
resumeRunId,
|
|
717
|
+
currentRunId: payload.run_id,
|
|
718
|
+
});
|
|
661
719
|
try {
|
|
662
|
-
|
|
663
|
-
payload.task_id,
|
|
664
|
-
|
|
665
|
-
|
|
720
|
+
this.resumeState = await resumeFromLog({
|
|
721
|
+
taskId: payload.task_id,
|
|
722
|
+
runId: resumeRunId,
|
|
723
|
+
repositoryPath: this.config.repositoryPath,
|
|
724
|
+
apiClient: this.posthogAPI,
|
|
725
|
+
logger: new Logger({ debug: true, prefix: "[Resume]" }),
|
|
726
|
+
});
|
|
727
|
+
this.logger.info("Resume state loaded (via TaskRun state)", {
|
|
728
|
+
conversationTurns: this.resumeState.conversation.length,
|
|
729
|
+
snapshotApplied: this.resumeState.snapshotApplied,
|
|
730
|
+
logEntries: this.resumeState.logEntryCount,
|
|
731
|
+
});
|
|
666
732
|
} catch (error) {
|
|
667
|
-
this.logger.warn(
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
runId: payload.run_id,
|
|
672
|
-
error,
|
|
673
|
-
},
|
|
674
|
-
);
|
|
733
|
+
this.logger.warn("Failed to load resume state, starting fresh", {
|
|
734
|
+
error,
|
|
735
|
+
});
|
|
736
|
+
this.resumeState = null;
|
|
675
737
|
}
|
|
676
738
|
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// Resume flow: if we have resume state, format conversation history as context
|
|
742
|
+
if (this.resumeState && this.resumeState.conversation.length > 0) {
|
|
743
|
+
await this.sendResumeMessage(payload, taskRun);
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
try {
|
|
748
|
+
const task = await this.posthogAPI.getTask(payload.task_id);
|
|
677
749
|
|
|
678
750
|
const initialPromptOverride = taskRun
|
|
679
751
|
? this.getInitialPromptOverride(taskRun)
|
|
@@ -700,6 +772,8 @@ export class AgentServer {
|
|
|
700
772
|
stopReason: result.stopReason,
|
|
701
773
|
});
|
|
702
774
|
|
|
775
|
+
this.broadcastTurnComplete(result.stopReason);
|
|
776
|
+
|
|
703
777
|
if (result.stopReason === "end_turn") {
|
|
704
778
|
await this.relayAgentResponse(payload);
|
|
705
779
|
}
|
|
@@ -712,6 +786,127 @@ export class AgentServer {
|
|
|
712
786
|
}
|
|
713
787
|
}
|
|
714
788
|
|
|
789
|
+
private async sendResumeMessage(
|
|
790
|
+
payload: JwtPayload,
|
|
791
|
+
taskRun: TaskRun | null,
|
|
792
|
+
): Promise<void> {
|
|
793
|
+
if (!this.session || !this.resumeState) return;
|
|
794
|
+
|
|
795
|
+
try {
|
|
796
|
+
const conversationSummary = this.formatConversationForResume(
|
|
797
|
+
this.resumeState.conversation,
|
|
798
|
+
);
|
|
799
|
+
|
|
800
|
+
// Read the pending user message from TaskRun state (set by the workflow
|
|
801
|
+
// when the user sends a follow-up message that triggers a resume).
|
|
802
|
+
const pendingUserMessage = this.getPendingUserMessage(taskRun);
|
|
803
|
+
|
|
804
|
+
const sandboxContext = this.resumeState.snapshotApplied
|
|
805
|
+
? `The sandbox environment (all files, packages, and code changes) has been fully restored from a snapshot.`
|
|
806
|
+
: `The sandbox could not be restored from a snapshot (it may have expired). You are starting with a fresh environment but have the full conversation history below.`;
|
|
807
|
+
|
|
808
|
+
let resumePrompt: string;
|
|
809
|
+
if (pendingUserMessage) {
|
|
810
|
+
// Include the pending message as the user's new question so the agent
|
|
811
|
+
// responds to it directly instead of the generic resume context.
|
|
812
|
+
resumePrompt =
|
|
813
|
+
`You are resuming a previous conversation. ${sandboxContext}\n\n` +
|
|
814
|
+
`Here is the conversation history from the previous session:\n\n` +
|
|
815
|
+
`${conversationSummary}\n\n` +
|
|
816
|
+
`The user has sent a new message:\n\n` +
|
|
817
|
+
`${pendingUserMessage}\n\n` +
|
|
818
|
+
`Respond to the user's new message above. You have full context from the previous session.`;
|
|
819
|
+
} else {
|
|
820
|
+
resumePrompt =
|
|
821
|
+
`You are resuming a previous conversation. ${sandboxContext}\n\n` +
|
|
822
|
+
`Here is the conversation history from the previous session:\n\n` +
|
|
823
|
+
`${conversationSummary}\n\n` +
|
|
824
|
+
`Continue from where you left off. The user is waiting for your response.`;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
this.logger.info("Sending resume message", {
|
|
828
|
+
taskId: payload.task_id,
|
|
829
|
+
conversationTurns: this.resumeState.conversation.length,
|
|
830
|
+
promptLength: resumePrompt.length,
|
|
831
|
+
hasPendingUserMessage: !!pendingUserMessage,
|
|
832
|
+
snapshotApplied: this.resumeState.snapshotApplied,
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
// Clear resume state so it's not reused
|
|
836
|
+
this.resumeState = null;
|
|
837
|
+
|
|
838
|
+
const result = await this.session.clientConnection.prompt({
|
|
839
|
+
sessionId: this.session.acpSessionId,
|
|
840
|
+
prompt: [{ type: "text", text: resumePrompt }],
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
this.logger.info("Resume message completed", {
|
|
844
|
+
stopReason: result.stopReason,
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
this.broadcastTurnComplete(result.stopReason);
|
|
848
|
+
} catch (error) {
|
|
849
|
+
this.logger.error("Failed to send resume message", error);
|
|
850
|
+
if (this.session) {
|
|
851
|
+
await this.session.logWriter.flushAll();
|
|
852
|
+
}
|
|
853
|
+
await this.signalTaskComplete(payload, "error");
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
private static RESUME_HISTORY_TOKEN_BUDGET = 50_000;
|
|
858
|
+
private static TOOL_RESULT_MAX_CHARS = 2000;
|
|
859
|
+
|
|
860
|
+
private formatConversationForResume(
|
|
861
|
+
conversation: ConversationTurn[],
|
|
862
|
+
): string {
|
|
863
|
+
const selected = selectRecentTurns(
|
|
864
|
+
conversation,
|
|
865
|
+
AgentServer.RESUME_HISTORY_TOKEN_BUDGET,
|
|
866
|
+
);
|
|
867
|
+
const parts: string[] = [];
|
|
868
|
+
|
|
869
|
+
if (selected.length < conversation.length) {
|
|
870
|
+
parts.push(
|
|
871
|
+
`*(${conversation.length - selected.length} earlier turns omitted)*`,
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
for (const turn of selected) {
|
|
876
|
+
const role = turn.role === "user" ? "User" : "Assistant";
|
|
877
|
+
|
|
878
|
+
const textParts = turn.content
|
|
879
|
+
.filter((block) => block.type === "text")
|
|
880
|
+
.map((block) => (block as { type: "text"; text: string }).text);
|
|
881
|
+
|
|
882
|
+
if (textParts.length > 0) {
|
|
883
|
+
parts.push(`**${role}**: ${textParts.join("\n")}`);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
if (turn.toolCalls?.length) {
|
|
887
|
+
const toolSummary = turn.toolCalls
|
|
888
|
+
.map((tc) => {
|
|
889
|
+
let resultStr = "";
|
|
890
|
+
if (tc.result !== undefined) {
|
|
891
|
+
const raw =
|
|
892
|
+
typeof tc.result === "string"
|
|
893
|
+
? tc.result
|
|
894
|
+
: JSON.stringify(tc.result);
|
|
895
|
+
resultStr =
|
|
896
|
+
raw.length > AgentServer.TOOL_RESULT_MAX_CHARS
|
|
897
|
+
? ` → ${raw.substring(0, AgentServer.TOOL_RESULT_MAX_CHARS)}...(truncated)`
|
|
898
|
+
: ` → ${raw}`;
|
|
899
|
+
}
|
|
900
|
+
return ` - ${tc.toolName}${resultStr}`;
|
|
901
|
+
})
|
|
902
|
+
.join("\n");
|
|
903
|
+
parts.push(`**${role} (tools)**:\n${toolSummary}`);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
return parts.join("\n\n");
|
|
908
|
+
}
|
|
909
|
+
|
|
715
910
|
private getInitialPromptOverride(taskRun: TaskRun): string | null {
|
|
716
911
|
const state = taskRun.state as Record<string, unknown> | undefined;
|
|
717
912
|
const override = state?.initial_prompt_override;
|
|
@@ -723,6 +918,32 @@ export class AgentServer {
|
|
|
723
918
|
return trimmed.length > 0 ? trimmed : null;
|
|
724
919
|
}
|
|
725
920
|
|
|
921
|
+
private getPendingUserMessage(taskRun: TaskRun | null): string | null {
|
|
922
|
+
if (!taskRun) return null;
|
|
923
|
+
const state = taskRun.state as Record<string, unknown> | undefined;
|
|
924
|
+
const message = state?.pending_user_message;
|
|
925
|
+
if (typeof message !== "string") {
|
|
926
|
+
return null;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
const trimmed = message.trim();
|
|
930
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
private getResumeRunId(taskRun: TaskRun | null): string | null {
|
|
934
|
+
// Env var takes precedence (set by backend infra)
|
|
935
|
+
const envRunId = process.env.POSTHOG_RESUME_RUN_ID;
|
|
936
|
+
if (envRunId) return envRunId;
|
|
937
|
+
|
|
938
|
+
// Fallback: read from TaskRun state (set by API when creating the run)
|
|
939
|
+
if (!taskRun) return null;
|
|
940
|
+
const state = taskRun.state as Record<string, unknown> | undefined;
|
|
941
|
+
const stateRunId = state?.resume_from_run_id;
|
|
942
|
+
return typeof stateRunId === "string" && stateRunId.trim().length > 0
|
|
943
|
+
? stateRunId.trim()
|
|
944
|
+
: null;
|
|
945
|
+
}
|
|
946
|
+
|
|
726
947
|
private buildCloudSystemPrompt(prUrl?: string | null): string {
|
|
727
948
|
if (prUrl) {
|
|
728
949
|
return `
|
|
@@ -1161,15 +1382,13 @@ Important:
|
|
|
1161
1382
|
notification,
|
|
1162
1383
|
});
|
|
1163
1384
|
|
|
1164
|
-
// Persist
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
params: paramsWithoutArchive,
|
|
1169
|
-
};
|
|
1385
|
+
// Persist full snapshot (including archiveUrl) so resume can restore files.
|
|
1386
|
+
// archiveUrl is a pre-signed S3 URL that expires — if the user resumes
|
|
1387
|
+
// after expiry, ApplySnapshotSaga fails gracefully and the agent continues
|
|
1388
|
+
// with conversation context but a fresh sandbox (snapshotApplied=false).
|
|
1170
1389
|
this.session.logWriter.appendRawLine(
|
|
1171
1390
|
this.session.payload.run_id,
|
|
1172
|
-
JSON.stringify(
|
|
1391
|
+
JSON.stringify(notification),
|
|
1173
1392
|
);
|
|
1174
1393
|
}
|
|
1175
1394
|
} catch (error) {
|
|
@@ -1177,6 +1396,22 @@ Important:
|
|
|
1177
1396
|
}
|
|
1178
1397
|
}
|
|
1179
1398
|
|
|
1399
|
+
private broadcastTurnComplete(stopReason: string): void {
|
|
1400
|
+
if (!this.session) return;
|
|
1401
|
+
this.broadcastEvent({
|
|
1402
|
+
type: "notification",
|
|
1403
|
+
timestamp: new Date().toISOString(),
|
|
1404
|
+
notification: {
|
|
1405
|
+
jsonrpc: "2.0",
|
|
1406
|
+
method: POSTHOG_NOTIFICATIONS.TURN_COMPLETE,
|
|
1407
|
+
params: {
|
|
1408
|
+
sessionId: this.session.acpSessionId,
|
|
1409
|
+
stopReason,
|
|
1410
|
+
},
|
|
1411
|
+
},
|
|
1412
|
+
});
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1180
1415
|
private broadcastEvent(event: Record<string, unknown>): void {
|
|
1181
1416
|
if (this.session?.sseController) {
|
|
1182
1417
|
this.sendSseEvent(this.session.sseController, event);
|