@posthog/agent 2.3.385 → 2.3.387
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/adapters/claude/session/jsonl-hydration.d.ts +1 -0
- package/dist/agent.d.ts +1 -0
- package/dist/agent.js +21 -2
- package/dist/agent.js.map +1 -1
- package/dist/handoff-checkpoint.d.ts +39 -0
- package/dist/handoff-checkpoint.js +6679 -0
- package/dist/handoff-checkpoint.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/logger-RC7sPv0S.d.ts +24 -0
- package/dist/posthog-api.d.ts +1 -0
- package/dist/posthog-api.js +19 -2
- package/dist/posthog-api.js.map +1 -1
- package/dist/resume.d.ts +71 -0
- package/dist/resume.js +6838 -0
- package/dist/resume.js.map +1 -0
- package/dist/server/agent-server.d.ts +5 -18
- package/dist/server/agent-server.js +1373 -432
- package/dist/server/agent-server.js.map +1 -1
- package/dist/server/bin.cjs +1370 -429
- package/dist/server/bin.cjs.map +1 -1
- package/dist/server/schemas.d.ts +191 -0
- package/dist/server/schemas.js +108 -0
- package/dist/server/schemas.js.map +1 -0
- package/dist/tree-tracker.d.ts +68 -0
- package/dist/tree-tracker.js +6431 -0
- package/dist/tree-tracker.js.map +1 -0
- package/dist/types.d.ts +18 -1
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -1
- package/package.json +17 -1
- package/src/acp-extensions.ts +3 -0
- package/src/handoff-checkpoint.test.ts +183 -0
- package/src/handoff-checkpoint.ts +361 -0
- package/src/posthog-api.test.ts +29 -0
- package/src/posthog-api.ts +5 -1
- package/src/resume.ts +58 -1
- package/src/sagas/apply-snapshot-saga.ts +7 -0
- package/src/sagas/capture-tree-saga.ts +10 -3
- package/src/sagas/resume-saga.ts +32 -0
- package/src/sagas/test-fixtures.ts +46 -0
- package/src/server/agent-server.ts +76 -57
- package/src/server/schemas.ts +21 -2
- package/src/types.ts +24 -0
package/src/resume.ts
CHANGED
|
@@ -16,14 +16,20 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import type { ContentBlock } from "@agentclientprotocol/sdk";
|
|
19
|
+
import { selectRecentTurns } from "./adapters/claude/session/jsonl-hydration";
|
|
19
20
|
import type { PostHogAPIClient } from "./posthog-api";
|
|
20
21
|
import { ResumeSaga } from "./sagas/resume-saga";
|
|
21
|
-
import type {
|
|
22
|
+
import type {
|
|
23
|
+
DeviceInfo,
|
|
24
|
+
GitCheckpointEvent,
|
|
25
|
+
TreeSnapshotEvent,
|
|
26
|
+
} from "./types";
|
|
22
27
|
import { Logger } from "./utils/logger";
|
|
23
28
|
|
|
24
29
|
export interface ResumeState {
|
|
25
30
|
conversation: ConversationTurn[];
|
|
26
31
|
latestSnapshot: TreeSnapshotEvent | null;
|
|
32
|
+
latestGitCheckpoint: GitCheckpointEvent | null;
|
|
27
33
|
/** Whether the tree snapshot was successfully applied (files restored) */
|
|
28
34
|
snapshotApplied: boolean;
|
|
29
35
|
interrupted: boolean;
|
|
@@ -95,6 +101,7 @@ export async function resumeFromLog(
|
|
|
95
101
|
return {
|
|
96
102
|
conversation: result.data.conversation as ConversationTurn[],
|
|
97
103
|
latestSnapshot: result.data.latestSnapshot,
|
|
104
|
+
latestGitCheckpoint: result.data.latestGitCheckpoint,
|
|
98
105
|
snapshotApplied: result.data.snapshotApplied,
|
|
99
106
|
interrupted: result.data.interrupted,
|
|
100
107
|
lastDevice: result.data.lastDevice,
|
|
@@ -113,3 +120,53 @@ export function conversationToPromptHistory(
|
|
|
113
120
|
content: turn.content,
|
|
114
121
|
}));
|
|
115
122
|
}
|
|
123
|
+
|
|
124
|
+
const RESUME_HISTORY_TOKEN_BUDGET = 50_000;
|
|
125
|
+
const TOOL_RESULT_MAX_CHARS = 2000;
|
|
126
|
+
|
|
127
|
+
export function formatConversationForResume(
|
|
128
|
+
conversation: ConversationTurn[],
|
|
129
|
+
): string {
|
|
130
|
+
const selected = selectRecentTurns(conversation, RESUME_HISTORY_TOKEN_BUDGET);
|
|
131
|
+
const parts: string[] = [];
|
|
132
|
+
|
|
133
|
+
if (selected.length < conversation.length) {
|
|
134
|
+
parts.push(
|
|
135
|
+
`*(${conversation.length - selected.length} earlier turns omitted)*`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
for (const turn of selected) {
|
|
140
|
+
const role = turn.role === "user" ? "User" : "Assistant";
|
|
141
|
+
|
|
142
|
+
const textParts = turn.content
|
|
143
|
+
.filter((block) => block.type === "text")
|
|
144
|
+
.map((block) => (block as { type: "text"; text: string }).text);
|
|
145
|
+
|
|
146
|
+
if (textParts.length > 0) {
|
|
147
|
+
parts.push(`**${role}**: ${textParts.join("\n")}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (turn.toolCalls?.length) {
|
|
151
|
+
const toolSummary = turn.toolCalls
|
|
152
|
+
.map((tc) => {
|
|
153
|
+
let resultStr = "";
|
|
154
|
+
if (tc.result !== undefined) {
|
|
155
|
+
const raw =
|
|
156
|
+
typeof tc.result === "string"
|
|
157
|
+
? tc.result
|
|
158
|
+
: JSON.stringify(tc.result);
|
|
159
|
+
resultStr =
|
|
160
|
+
raw.length > TOOL_RESULT_MAX_CHARS
|
|
161
|
+
? ` → ${raw.substring(0, TOOL_RESULT_MAX_CHARS)}...(truncated)`
|
|
162
|
+
: ` → ${raw}`;
|
|
163
|
+
}
|
|
164
|
+
return ` - ${tc.toolName}${resultStr}`;
|
|
165
|
+
})
|
|
166
|
+
.join("\n");
|
|
167
|
+
parts.push(`**${role} (tools)**:\n${toolSummary}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return parts.join("\n\n");
|
|
172
|
+
}
|
|
@@ -59,6 +59,13 @@ export class ApplySnapshotSaga extends Saga<
|
|
|
59
59
|
const base64Content = Buffer.from(arrayBuffer).toString("utf-8");
|
|
60
60
|
const binaryContent = Buffer.from(base64Content, "base64");
|
|
61
61
|
await writeFile(archivePath, binaryContent);
|
|
62
|
+
this.log.info("Tree archive downloaded", {
|
|
63
|
+
treeHash: snapshot.treeHash,
|
|
64
|
+
snapshotBytes: binaryContent.byteLength,
|
|
65
|
+
snapshotWireBytes: arrayBuffer.byteLength,
|
|
66
|
+
totalBytes: binaryContent.byteLength,
|
|
67
|
+
totalWireBytes: arrayBuffer.byteLength,
|
|
68
|
+
});
|
|
62
69
|
},
|
|
63
70
|
rollback: async () => {
|
|
64
71
|
if (this.archivePath) {
|
|
@@ -113,6 +113,8 @@ export class CaptureTreeSaga extends Saga<CaptureTreeInput, CaptureTreeOutput> {
|
|
|
113
113
|
execute: async () => {
|
|
114
114
|
const archiveContent = await readFile(archivePath);
|
|
115
115
|
const base64Content = archiveContent.toString("base64");
|
|
116
|
+
const snapshotBytes = archiveContent.byteLength;
|
|
117
|
+
const snapshotWireBytes = Buffer.byteLength(base64Content, "utf-8");
|
|
116
118
|
|
|
117
119
|
const artifacts = await apiClient.uploadTaskArtifacts(taskId, runId, [
|
|
118
120
|
{
|
|
@@ -123,12 +125,17 @@ export class CaptureTreeSaga extends Saga<CaptureTreeInput, CaptureTreeOutput> {
|
|
|
123
125
|
},
|
|
124
126
|
]);
|
|
125
127
|
|
|
126
|
-
|
|
128
|
+
const uploadedArtifact = artifacts[0];
|
|
129
|
+
if (uploadedArtifact?.storage_path) {
|
|
127
130
|
this.log.info("Tree archive uploaded", {
|
|
128
|
-
storagePath:
|
|
131
|
+
storagePath: uploadedArtifact.storage_path,
|
|
129
132
|
treeHash,
|
|
133
|
+
snapshotBytes,
|
|
134
|
+
snapshotWireBytes,
|
|
135
|
+
totalBytes: snapshotBytes,
|
|
136
|
+
totalWireBytes: snapshotWireBytes,
|
|
130
137
|
});
|
|
131
|
-
return
|
|
138
|
+
return uploadedArtifact.storage_path;
|
|
132
139
|
}
|
|
133
140
|
|
|
134
141
|
return undefined;
|
package/src/sagas/resume-saga.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { PostHogAPIClient } from "../posthog-api";
|
|
|
5
5
|
import { TreeTracker } from "../tree-tracker";
|
|
6
6
|
import type {
|
|
7
7
|
DeviceInfo,
|
|
8
|
+
GitCheckpointEvent,
|
|
8
9
|
StoredNotification,
|
|
9
10
|
TreeSnapshotEvent,
|
|
10
11
|
} from "../types";
|
|
@@ -34,6 +35,7 @@ export interface ResumeInput {
|
|
|
34
35
|
export interface ResumeOutput {
|
|
35
36
|
conversation: ConversationTurn[];
|
|
36
37
|
latestSnapshot: TreeSnapshotEvent | null;
|
|
38
|
+
latestGitCheckpoint: GitCheckpointEvent | null;
|
|
37
39
|
snapshotApplied: boolean;
|
|
38
40
|
interrupted: boolean;
|
|
39
41
|
lastDevice?: DeviceInfo;
|
|
@@ -75,6 +77,11 @@ export class ResumeSaga extends Saga<ResumeInput, ResumeOutput> {
|
|
|
75
77
|
Promise.resolve(this.findLatestTreeSnapshot(entries)),
|
|
76
78
|
);
|
|
77
79
|
|
|
80
|
+
const latestGitCheckpoint = await this.readOnlyStep(
|
|
81
|
+
"find_git_checkpoint",
|
|
82
|
+
() => Promise.resolve(this.findLatestGitCheckpoint(entries)),
|
|
83
|
+
);
|
|
84
|
+
|
|
78
85
|
// Step 4: Apply snapshot if present (wrapped in step for consistent logging)
|
|
79
86
|
// Note: We use a try/catch inside the step because snapshot failure should NOT fail the saga
|
|
80
87
|
let snapshotApplied = false;
|
|
@@ -158,6 +165,7 @@ export class ResumeSaga extends Saga<ResumeInput, ResumeOutput> {
|
|
|
158
165
|
return {
|
|
159
166
|
conversation,
|
|
160
167
|
latestSnapshot,
|
|
168
|
+
latestGitCheckpoint,
|
|
161
169
|
snapshotApplied,
|
|
162
170
|
interrupted: latestSnapshot?.interrupted ?? false,
|
|
163
171
|
lastDevice,
|
|
@@ -169,6 +177,7 @@ export class ResumeSaga extends Saga<ResumeInput, ResumeOutput> {
|
|
|
169
177
|
return {
|
|
170
178
|
conversation: [],
|
|
171
179
|
latestSnapshot: null,
|
|
180
|
+
latestGitCheckpoint: null,
|
|
172
181
|
snapshotApplied: false,
|
|
173
182
|
interrupted: false,
|
|
174
183
|
logEntryCount: 0,
|
|
@@ -197,6 +206,29 @@ export class ResumeSaga extends Saga<ResumeInput, ResumeOutput> {
|
|
|
197
206
|
return null;
|
|
198
207
|
}
|
|
199
208
|
|
|
209
|
+
private findLatestGitCheckpoint(
|
|
210
|
+
entries: StoredNotification[],
|
|
211
|
+
): GitCheckpointEvent | null {
|
|
212
|
+
const sdkPrefixedMethod = `_${POSTHOG_NOTIFICATIONS.GIT_CHECKPOINT}`;
|
|
213
|
+
|
|
214
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
215
|
+
const entry = entries[i];
|
|
216
|
+
const method = entry.notification?.method;
|
|
217
|
+
if (
|
|
218
|
+
method === sdkPrefixedMethod ||
|
|
219
|
+
method === POSTHOG_NOTIFICATIONS.GIT_CHECKPOINT
|
|
220
|
+
) {
|
|
221
|
+
const params = entry.notification?.params as
|
|
222
|
+
| GitCheckpointEvent
|
|
223
|
+
| undefined;
|
|
224
|
+
if (params?.checkpointId && params?.checkpointRef) {
|
|
225
|
+
return params;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
|
|
200
232
|
private findLastDeviceInfo(
|
|
201
233
|
entries: StoredNotification[],
|
|
202
234
|
): DeviceInfo | undefined {
|
|
@@ -67,6 +67,52 @@ export async function createTestRepo(prefix = "test-repo"): Promise<TestRepo> {
|
|
|
67
67
|
};
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
export async function cloneTestRepo(
|
|
71
|
+
sourcePath: string,
|
|
72
|
+
prefix = "test-repo-clone",
|
|
73
|
+
): Promise<TestRepo> {
|
|
74
|
+
const clonePath = join(
|
|
75
|
+
tmpdir(),
|
|
76
|
+
`${prefix}-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
77
|
+
);
|
|
78
|
+
await execFileAsync("git", ["clone", sourcePath, clonePath]);
|
|
79
|
+
await execFileAsync("git", ["config", "user.email", "test@test.com"], {
|
|
80
|
+
cwd: clonePath,
|
|
81
|
+
});
|
|
82
|
+
await execFileAsync("git", ["config", "user.name", "Test"], {
|
|
83
|
+
cwd: clonePath,
|
|
84
|
+
});
|
|
85
|
+
await execFileAsync("git", ["config", "commit.gpgsign", "false"], {
|
|
86
|
+
cwd: clonePath,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const git = async (args: string[]): Promise<string> => {
|
|
90
|
+
const { stdout } = await execFileAsync("git", args, { cwd: clonePath });
|
|
91
|
+
return stdout.trim();
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
path: clonePath,
|
|
96
|
+
cleanup: () => rm(clonePath, { recursive: true, force: true }),
|
|
97
|
+
git,
|
|
98
|
+
writeFile: async (relativePath: string, content: string) => {
|
|
99
|
+
const fullPath = join(clonePath, relativePath);
|
|
100
|
+
const dir = join(fullPath, "..");
|
|
101
|
+
await mkdir(dir, { recursive: true });
|
|
102
|
+
await writeFile(fullPath, content);
|
|
103
|
+
},
|
|
104
|
+
readFile: async (relativePath: string) => {
|
|
105
|
+
return readFile(join(clonePath, relativePath), "utf-8");
|
|
106
|
+
},
|
|
107
|
+
deleteFile: async (relativePath: string) => {
|
|
108
|
+
await rm(join(clonePath, relativePath), { force: true });
|
|
109
|
+
},
|
|
110
|
+
exists: (relativePath: string) => {
|
|
111
|
+
return existsSync(join(clonePath, relativePath));
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
70
116
|
export function createMockLogger(): SagaLogger {
|
|
71
117
|
return {
|
|
72
118
|
info: vi.fn(),
|
|
@@ -25,12 +25,12 @@ import {
|
|
|
25
25
|
type AgentErrorClassification,
|
|
26
26
|
classifyAgentError,
|
|
27
27
|
} from "../adapters/claude/conversion/sdk-to-acp";
|
|
28
|
-
import { selectRecentTurns } from "../adapters/claude/session/jsonl-hydration";
|
|
29
28
|
import type { PermissionMode } from "../execution-mode";
|
|
30
29
|
import { DEFAULT_CODEX_MODEL } from "../gateway-models";
|
|
30
|
+
import { HandoffCheckpointTracker } from "../handoff-checkpoint";
|
|
31
31
|
import { PostHogAPIClient } from "../posthog-api";
|
|
32
32
|
import {
|
|
33
|
-
|
|
33
|
+
formatConversationForResume,
|
|
34
34
|
type ResumeState,
|
|
35
35
|
resumeFromLog,
|
|
36
36
|
} from "../resume";
|
|
@@ -39,6 +39,8 @@ import { TreeTracker } from "../tree-tracker";
|
|
|
39
39
|
import type {
|
|
40
40
|
AgentMode,
|
|
41
41
|
DeviceInfo,
|
|
42
|
+
GitCheckpointEvent,
|
|
43
|
+
HandoffLocalGitState,
|
|
42
44
|
LogLevel,
|
|
43
45
|
TaskRun,
|
|
44
46
|
TaskRunArtifact,
|
|
@@ -53,7 +55,11 @@ import {
|
|
|
53
55
|
promptBlocksToText,
|
|
54
56
|
} from "./cloud-prompt";
|
|
55
57
|
import { type JwtPayload, JwtValidationError, validateJwt } from "./jwt";
|
|
56
|
-
import {
|
|
58
|
+
import {
|
|
59
|
+
handoffLocalGitStateSchema,
|
|
60
|
+
jsonRpcRequestSchema,
|
|
61
|
+
validateCommandParams,
|
|
62
|
+
} from "./schemas";
|
|
57
63
|
import type { AgentServerConfig } from "./types";
|
|
58
64
|
|
|
59
65
|
const agentErrorClassificationSchema = z.enum([
|
|
@@ -186,6 +192,7 @@ interface ActiveSession {
|
|
|
186
192
|
permissionMode: PermissionMode;
|
|
187
193
|
/** Whether a desktop client has ever connected via SSE during this session */
|
|
188
194
|
hasDesktopConnected: boolean;
|
|
195
|
+
pendingHandoffGitState?: HandoffLocalGitState;
|
|
189
196
|
}
|
|
190
197
|
|
|
191
198
|
function getTaskRunStateString(
|
|
@@ -662,6 +669,10 @@ export class AgentServer {
|
|
|
662
669
|
case POSTHOG_NOTIFICATIONS.CLOSE:
|
|
663
670
|
case "close": {
|
|
664
671
|
this.logger.debug("Close requested");
|
|
672
|
+
const localGitState = this.extractHandoffLocalGitState(params);
|
|
673
|
+
if (localGitState && this.session) {
|
|
674
|
+
this.session.pendingHandoffGitState = localGitState;
|
|
675
|
+
}
|
|
665
676
|
await this.cleanupSession();
|
|
666
677
|
return { closed: true };
|
|
667
678
|
}
|
|
@@ -959,6 +970,7 @@ export class AgentServer {
|
|
|
959
970
|
logWriter,
|
|
960
971
|
permissionMode: initialPermissionMode,
|
|
961
972
|
hasDesktopConnected: sseController !== null,
|
|
973
|
+
pendingHandoffGitState: undefined,
|
|
962
974
|
};
|
|
963
975
|
|
|
964
976
|
this.logger = new Logger({
|
|
@@ -1147,7 +1159,7 @@ export class AgentServer {
|
|
|
1147
1159
|
if (!this.session || !this.resumeState) return;
|
|
1148
1160
|
|
|
1149
1161
|
try {
|
|
1150
|
-
const conversationSummary =
|
|
1162
|
+
const conversationSummary = formatConversationForResume(
|
|
1151
1163
|
this.resumeState.conversation,
|
|
1152
1164
|
);
|
|
1153
1165
|
|
|
@@ -1229,59 +1241,6 @@ export class AgentServer {
|
|
|
1229
1241
|
}
|
|
1230
1242
|
}
|
|
1231
1243
|
|
|
1232
|
-
private static RESUME_HISTORY_TOKEN_BUDGET = 50_000;
|
|
1233
|
-
private static TOOL_RESULT_MAX_CHARS = 2000;
|
|
1234
|
-
|
|
1235
|
-
private formatConversationForResume(
|
|
1236
|
-
conversation: ConversationTurn[],
|
|
1237
|
-
): string {
|
|
1238
|
-
const selected = selectRecentTurns(
|
|
1239
|
-
conversation,
|
|
1240
|
-
AgentServer.RESUME_HISTORY_TOKEN_BUDGET,
|
|
1241
|
-
);
|
|
1242
|
-
const parts: string[] = [];
|
|
1243
|
-
|
|
1244
|
-
if (selected.length < conversation.length) {
|
|
1245
|
-
parts.push(
|
|
1246
|
-
`*(${conversation.length - selected.length} earlier turns omitted)*`,
|
|
1247
|
-
);
|
|
1248
|
-
}
|
|
1249
|
-
|
|
1250
|
-
for (const turn of selected) {
|
|
1251
|
-
const role = turn.role === "user" ? "User" : "Assistant";
|
|
1252
|
-
|
|
1253
|
-
const textParts = turn.content
|
|
1254
|
-
.filter((block) => block.type === "text")
|
|
1255
|
-
.map((block) => (block as { type: "text"; text: string }).text);
|
|
1256
|
-
|
|
1257
|
-
if (textParts.length > 0) {
|
|
1258
|
-
parts.push(`**${role}**: ${textParts.join("\n")}`);
|
|
1259
|
-
}
|
|
1260
|
-
|
|
1261
|
-
if (turn.toolCalls?.length) {
|
|
1262
|
-
const toolSummary = turn.toolCalls
|
|
1263
|
-
.map((tc) => {
|
|
1264
|
-
let resultStr = "";
|
|
1265
|
-
if (tc.result !== undefined) {
|
|
1266
|
-
const raw =
|
|
1267
|
-
typeof tc.result === "string"
|
|
1268
|
-
? tc.result
|
|
1269
|
-
: JSON.stringify(tc.result);
|
|
1270
|
-
resultStr =
|
|
1271
|
-
raw.length > AgentServer.TOOL_RESULT_MAX_CHARS
|
|
1272
|
-
? ` → ${raw.substring(0, AgentServer.TOOL_RESULT_MAX_CHARS)}...(truncated)`
|
|
1273
|
-
: ` → ${raw}`;
|
|
1274
|
-
}
|
|
1275
|
-
return ` - ${tc.toolName}${resultStr}`;
|
|
1276
|
-
})
|
|
1277
|
-
.join("\n");
|
|
1278
|
-
parts.push(`**${role} (tools)**:\n${toolSummary}`);
|
|
1279
|
-
}
|
|
1280
|
-
}
|
|
1281
|
-
|
|
1282
|
-
return parts.join("\n\n");
|
|
1283
|
-
}
|
|
1284
|
-
|
|
1285
1244
|
private getInitialPromptOverride(taskRun: TaskRun): string | null {
|
|
1286
1245
|
const state = taskRun.state as Record<string, unknown> | undefined;
|
|
1287
1246
|
const override = state?.initial_prompt_override;
|
|
@@ -2158,6 +2117,12 @@ ${attributionInstructions}
|
|
|
2158
2117
|
|
|
2159
2118
|
this.logger.debug("Cleaning up session");
|
|
2160
2119
|
|
|
2120
|
+
try {
|
|
2121
|
+
await this.captureHandoffCheckpoint();
|
|
2122
|
+
} catch (error) {
|
|
2123
|
+
this.logger.error("Failed to capture handoff checkpoint", error);
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2161
2126
|
try {
|
|
2162
2127
|
await this.captureTreeState();
|
|
2163
2128
|
} catch (error) {
|
|
@@ -2234,6 +2199,60 @@ ${attributionInstructions}
|
|
|
2234
2199
|
}
|
|
2235
2200
|
}
|
|
2236
2201
|
|
|
2202
|
+
private async captureHandoffCheckpoint(): Promise<void> {
|
|
2203
|
+
if (!this.session?.treeTracker || !this.session.pendingHandoffGitState) {
|
|
2204
|
+
return;
|
|
2205
|
+
}
|
|
2206
|
+
if (!this.posthogAPI) {
|
|
2207
|
+
this.logger.warn(
|
|
2208
|
+
"Skipping handoff checkpoint capture: PostHog API client is not configured",
|
|
2209
|
+
);
|
|
2210
|
+
return;
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
const tracker = new HandoffCheckpointTracker({
|
|
2214
|
+
repositoryPath: this.config.repositoryPath ?? "/tmp/workspace",
|
|
2215
|
+
taskId: this.session.payload.task_id,
|
|
2216
|
+
runId: this.session.payload.run_id,
|
|
2217
|
+
apiClient: this.posthogAPI,
|
|
2218
|
+
logger: this.logger.child("HandoffCheckpoint"),
|
|
2219
|
+
});
|
|
2220
|
+
|
|
2221
|
+
const checkpoint = await tracker.captureForHandoff(
|
|
2222
|
+
this.session.pendingHandoffGitState,
|
|
2223
|
+
);
|
|
2224
|
+
if (!checkpoint) return;
|
|
2225
|
+
|
|
2226
|
+
const checkpointWithDevice: GitCheckpointEvent = {
|
|
2227
|
+
...checkpoint,
|
|
2228
|
+
device: this.session.deviceInfo,
|
|
2229
|
+
};
|
|
2230
|
+
|
|
2231
|
+
const notification = {
|
|
2232
|
+
jsonrpc: "2.0" as const,
|
|
2233
|
+
method: POSTHOG_NOTIFICATIONS.GIT_CHECKPOINT,
|
|
2234
|
+
params: checkpointWithDevice,
|
|
2235
|
+
};
|
|
2236
|
+
|
|
2237
|
+
this.broadcastEvent({
|
|
2238
|
+
type: "notification",
|
|
2239
|
+
timestamp: new Date().toISOString(),
|
|
2240
|
+
notification,
|
|
2241
|
+
});
|
|
2242
|
+
|
|
2243
|
+
this.session.logWriter.appendRawLine(
|
|
2244
|
+
this.session.payload.run_id,
|
|
2245
|
+
JSON.stringify(notification),
|
|
2246
|
+
);
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
private extractHandoffLocalGitState(
|
|
2250
|
+
params: Record<string, unknown>,
|
|
2251
|
+
): HandoffLocalGitState | null {
|
|
2252
|
+
const result = handoffLocalGitStateSchema.safeParse(params.localGitState);
|
|
2253
|
+
return result.success ? result.data : null;
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2237
2256
|
private broadcastTurnComplete(stopReason: string): void {
|
|
2238
2257
|
if (!this.session) return;
|
|
2239
2258
|
this.broadcastEvent({
|
package/src/server/schemas.ts
CHANGED
|
@@ -5,6 +5,19 @@ const httpHeaderSchema = z.object({
|
|
|
5
5
|
value: z.string(),
|
|
6
6
|
});
|
|
7
7
|
|
|
8
|
+
const nullishString = z
|
|
9
|
+
.string()
|
|
10
|
+
.nullish()
|
|
11
|
+
.transform((value) => value ?? null);
|
|
12
|
+
|
|
13
|
+
export const handoffLocalGitStateSchema = z.object({
|
|
14
|
+
head: nullishString,
|
|
15
|
+
branch: nullishString,
|
|
16
|
+
upstreamHead: nullishString,
|
|
17
|
+
upstreamRemote: nullishString,
|
|
18
|
+
upstreamMergeRef: nullishString,
|
|
19
|
+
});
|
|
20
|
+
|
|
8
21
|
const remoteMcpServerSchema = z.object({
|
|
9
22
|
type: z.enum(["http", "sse"]),
|
|
10
23
|
name: z.string().min(1, "MCP server name is required"),
|
|
@@ -83,13 +96,19 @@ export const refreshSessionParamsSchema = z.object({
|
|
|
83
96
|
mcpServers: mcpServersSchema,
|
|
84
97
|
});
|
|
85
98
|
|
|
99
|
+
export const closeParamsSchema = z
|
|
100
|
+
.object({
|
|
101
|
+
localGitState: handoffLocalGitStateSchema.optional(),
|
|
102
|
+
})
|
|
103
|
+
.optional();
|
|
104
|
+
|
|
86
105
|
export const commandParamsSchemas = {
|
|
87
106
|
user_message: userMessageParamsSchema,
|
|
88
107
|
"posthog/user_message": userMessageParamsSchema,
|
|
89
108
|
cancel: z.object({}).optional(),
|
|
90
109
|
"posthog/cancel": z.object({}).optional(),
|
|
91
|
-
close:
|
|
92
|
-
"posthog/close":
|
|
110
|
+
close: closeParamsSchema,
|
|
111
|
+
"posthog/close": closeParamsSchema,
|
|
93
112
|
permission_response: permissionResponseParamsSchema,
|
|
94
113
|
"posthog/permission_response": permissionResponseParamsSchema,
|
|
95
114
|
set_config_option: setConfigOptionParamsSchema,
|
package/src/types.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
GitHandoffCheckpoint,
|
|
3
|
+
HandoffLocalGitState as GitHandoffLocalGitState,
|
|
4
|
+
} from "@posthog/git/handoff";
|
|
5
|
+
|
|
1
6
|
/**
|
|
2
7
|
* Stored custom notification following ACP extensibility model.
|
|
3
8
|
* Custom notifications use underscore-prefixed methods (e.g., `_posthog/phase_start`).
|
|
@@ -196,3 +201,22 @@ export interface TreeSnapshot {
|
|
|
196
201
|
export interface TreeSnapshotEvent extends TreeSnapshot {
|
|
197
202
|
device?: DeviceInfo;
|
|
198
203
|
}
|
|
204
|
+
|
|
205
|
+
export type HandoffLocalGitState = GitHandoffLocalGitState;
|
|
206
|
+
|
|
207
|
+
export interface GitCheckpoint extends GitHandoffCheckpoint {
|
|
208
|
+
artifactPath?: string;
|
|
209
|
+
indexArtifactPath?: string;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export interface GitCheckpointEvent extends GitCheckpoint {
|
|
213
|
+
device?: DeviceInfo;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Keeps the emitted `@posthog/agent/types` entrypoint as a runtime ESM module.
|
|
218
|
+
*
|
|
219
|
+
* `export {}` is stripped by tsup in this package, which leaves `dist/types.js`
|
|
220
|
+
* empty and breaks downstream type resolution for the exported subpath.
|
|
221
|
+
*/
|
|
222
|
+
export const AGENT_TYPES_MODULE = true;
|