Skip to content

Commit 9442392

Browse files
committed
init resume and tree tracking
1 parent ab085b7 commit 9442392

8 files changed

Lines changed: 1270 additions & 20 deletions

File tree

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/**
2+
* PostHog-specific ACP extensions.
3+
*
4+
* These follow the ACP extensibility model:
5+
* - Custom notification methods are prefixed with `_posthog/`
6+
* - Custom data can be attached via `_meta` fields
7+
*
8+
* Note: When using `extNotification()` from the ACP SDK, it automatically
9+
* adds an extra underscore prefix (e.g., `_posthog/tree_snapshot` becomes
10+
* `__posthog/tree_snapshot` in the log). Code that reads logs should handle both.
11+
*
12+
* See: https://agentclientprotocol.com/docs/extensibility
13+
*/
14+
15+
/**
16+
* Custom notification methods for PostHog-specific events.
17+
* Used with AgentSideConnection.extNotification() or Client.extNotification()
18+
*/
19+
export const POSTHOG_NOTIFICATIONS = {
20+
/** Git branch was created for a task */
21+
BRANCH_CREATED: "_posthog/branch_created",
22+
23+
/** Task run has started execution */
24+
RUN_STARTED: "_posthog/run_started",
25+
26+
/** Task has completed (success or failure) */
27+
TASK_COMPLETE: "_posthog/task_complete",
28+
29+
/** Error occurred during task execution */
30+
ERROR: "_posthog/error",
31+
32+
/** Console/log output from the agent */
33+
CONSOLE: "_posthog/console",
34+
35+
/** Maps a session ID to the underlying SDK session ID (for resumption) */
36+
SDK_SESSION: "_posthog/sdk_session",
37+
38+
/** Tree state snapshot captured (git tree hash + file archive) */
39+
TREE_SNAPSHOT: "_posthog/tree_snapshot",
40+
41+
/** Agent mode changed (interactive/background) */
42+
MODE_CHANGE: "_posthog/mode_change",
43+
44+
/** Request to resume a session from previous state */
45+
SESSION_RESUME: "_posthog/session/resume",
46+
47+
/** User message sent from client to agent */
48+
USER_MESSAGE: "_posthog/user_message",
49+
50+
/** Request to cancel current operation */
51+
CANCEL: "_posthog/cancel",
52+
53+
/** Request to close the session */
54+
CLOSE: "_posthog/close",
55+
56+
/** Agent status update (thinking, working, etc.) */
57+
STATUS: "_posthog/status",
58+
59+
/** Task-level notification (progress, milestones) */
60+
TASK_NOTIFICATION: "_posthog/task_notification",
61+
62+
/** Marks a boundary for log compaction */
63+
COMPACT_BOUNDARY: "_posthog/compact_boundary",
64+
} as const;
65+
66+
export type PostHogNotificationType =
67+
(typeof POSTHOG_NOTIFICATIONS)[keyof typeof POSTHOG_NOTIFICATIONS];
68+
69+
// --- Payload types for each notification ---
70+
71+
export interface BranchCreatedPayload {
72+
branch: string;
73+
}
74+
75+
export interface RunStartedPayload {
76+
sessionId: string;
77+
runId: string;
78+
taskId?: string;
79+
}
80+
81+
export interface TaskCompletePayload {
82+
sessionId: string;
83+
taskId: string;
84+
}
85+
86+
export interface ErrorNotificationPayload {
87+
sessionId: string;
88+
message: string;
89+
error?: unknown;
90+
}
91+
92+
export interface ConsoleNotificationPayload {
93+
sessionId: string;
94+
level: "debug" | "info" | "warn" | "error";
95+
message: string;
96+
}
97+
98+
export interface SdkSessionPayload {
99+
sessionId: string;
100+
sdkSessionId: string;
101+
}
102+
103+
export interface TreeSnapshotPayload {
104+
treeHash: string;
105+
baseCommit: string | null;
106+
archiveUrl?: string;
107+
filesChanged: string[];
108+
filesDeleted?: string[];
109+
timestamp: string;
110+
interrupted?: boolean;
111+
device?: {
112+
type: "local" | "cloud";
113+
name?: string;
114+
};
115+
}
116+
117+
export interface ModeChangePayload {
118+
mode: "interactive" | "background";
119+
previous_mode: "interactive" | "background";
120+
}
121+
122+
export interface SessionResumePayload {
123+
sessionId: string;
124+
fromSnapshot?: string;
125+
}
126+
127+
export interface UserMessagePayload {
128+
content: string;
129+
}
130+
131+
export interface StatusPayload {
132+
sessionId: string;
133+
status: string;
134+
message?: string;
135+
}
136+
137+
export interface TaskNotificationPayload {
138+
sessionId: string;
139+
type: string;
140+
message?: string;
141+
data?: Record<string, unknown>;
142+
}
143+
144+
export interface CompactBoundaryPayload {
145+
sessionId: string;
146+
timestamp: string;
147+
}
148+
149+
export type PostHogNotificationPayload =
150+
| BranchCreatedPayload
151+
| RunStartedPayload
152+
| TaskCompletePayload
153+
| ErrorNotificationPayload
154+
| ConsoleNotificationPayload
155+
| SdkSessionPayload
156+
| TreeSnapshotPayload
157+
| ModeChangePayload
158+
| SessionResumePayload
159+
| UserMessagePayload
160+
| StatusPayload
161+
| TaskNotificationPayload
162+
| CompactBoundaryPayload;

packages/agent/src/index.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
export type {
2+
BranchCreatedPayload,
3+
CompactBoundaryPayload,
4+
ConsoleNotificationPayload,
5+
ErrorNotificationPayload,
6+
ModeChangePayload,
7+
PostHogNotificationPayload,
8+
PostHogNotificationType,
9+
RunStartedPayload,
10+
SdkSessionPayload,
11+
SessionResumePayload,
12+
StatusPayload,
13+
TaskCompletePayload,
14+
TaskNotificationPayload,
15+
TreeSnapshotPayload,
16+
UserMessagePayload,
17+
} from "./acp-extensions.js";
18+
export { POSTHOG_NOTIFICATIONS } from "./acp-extensions.js";
19+
export type {
20+
AcpConnectionConfig,
21+
InProcessAcpConnection,
22+
} from "./adapters/acp-connection.js";
23+
export { createAcpConnection } from "./adapters/acp-connection.js";
24+
25+
export type {
26+
AgentConfig,
27+
AgentMode,
28+
DeviceInfo,
29+
LogLevel,
30+
OnLogCallback,
31+
StoredEntry,
32+
StoredNotification,
33+
Task,
34+
TaskRun,
35+
TreeSnapshotEvent,
36+
WorktreeInfo,
37+
} from "./types.js";
38+
39+
export type { TreeSnapshot, TreeTrackerConfig } from "./tree-tracker.js";
40+
export { TreeTracker } from "./tree-tracker.js";
41+
42+
export type {
43+
ConversationTurn,
44+
ResumeConfig,
45+
ResumeState,
46+
ToolCallInfo,
47+
} from "./resume.js";
48+
export { conversationToPromptHistory, resumeFromLog } from "./resume.js";
49+
50+
export { getLlmGatewayUrl } from "./utils/gateway.js";
51+
export type { LoggerConfig } from "./utils/logger.js";
52+
export { Logger } from "./utils/logger.js";
53+
54+
export { Agent } from "./agent.js";
55+
56+
export { PostHogAPIClient } from "./posthog-api.js";
57+
58+
export type { WorktreeConfig } from "./worktree-manager.js";
59+
export { WorktreeManager } from "./worktree-manager.js";

packages/agent/src/posthog-api.ts

Lines changed: 134 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,26 @@
1-
import type { PostHogAPIConfig, StoredEntry, TaskRun } from "./types.js";
1+
import type { ArtifactType, PostHogAPIConfig, StoredEntry, TaskRun, TaskRunArtifact } from "./types.js";
2+
import { getLlmGatewayUrl } from "./utils/gateway.js";
3+
4+
export interface TaskArtifactUploadPayload {
5+
name: string;
6+
type: ArtifactType;
7+
content: string;
8+
content_type?: string;
9+
}
210

311
export type TaskRunUpdate = Partial<
412
Pick<
513
TaskRun,
6-
"status" | "branch" | "stage" | "error_message" | "output" | "state"
14+
| "status"
15+
| "branch"
16+
| "stage"
17+
| "error_message"
18+
| "output"
19+
| "state"
20+
| "environment"
721
>
822
>;
923

10-
export function getLlmGatewayUrl(posthogHost: string): string {
11-
const url = new URL(posthogHost);
12-
const hostname = url.hostname;
13-
14-
// TODO: Migrate to twig
15-
if (hostname === "localhost" || hostname === "127.0.0.1") {
16-
return `${url.protocol}//localhost:3308/array`;
17-
}
18-
19-
const regionMatch = hostname.match(/^(us|eu)\.posthog\.com$/);
20-
const region = regionMatch ? regionMatch[1] : "us";
21-
22-
// TODO: Migrate to twig
23-
return `https://gateway.${region}.posthog.com/array`;
24-
}
25-
2624
export class PostHogAPIClient {
2725
private config: PostHogAPIConfig;
2826

@@ -84,6 +82,13 @@ export class PostHogAPIClient {
8482
return getLlmGatewayUrl(this.baseUrl);
8583
}
8684

85+
async getTaskRun(taskId: string, runId: string): Promise<TaskRun> {
86+
const teamId = this.getTeamId();
87+
return this.apiRequest<TaskRun>(
88+
`/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/`,
89+
);
90+
}
91+
8792
async updateTaskRun(
8893
taskId: string,
8994
runId: string,
@@ -113,4 +118,115 @@ export class PostHogAPIClient {
113118
},
114119
);
115120
}
121+
122+
async uploadTaskArtifacts(
123+
taskId: string,
124+
runId: string,
125+
artifacts: TaskArtifactUploadPayload[],
126+
): Promise<TaskRunArtifact[]> {
127+
if (!artifacts.length) {
128+
return [];
129+
}
130+
131+
const teamId = this.getTeamId();
132+
const response = await this.apiRequest<{ artifacts: TaskRunArtifact[] }>(
133+
`/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/artifacts/`,
134+
{
135+
method: "POST",
136+
body: JSON.stringify({ artifacts }),
137+
},
138+
);
139+
140+
return response.artifacts ?? [];
141+
}
142+
143+
async getArtifactPresignedUrl(
144+
taskId: string,
145+
runId: string,
146+
storagePath: string,
147+
): Promise<string | null> {
148+
const teamId = this.getTeamId();
149+
try {
150+
const response = await this.apiRequest<{
151+
url: string;
152+
expires_in: number;
153+
}>(
154+
`/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/artifacts/presign/`,
155+
{
156+
method: "POST",
157+
body: JSON.stringify({ storage_path: storagePath }),
158+
},
159+
);
160+
return response.url;
161+
} catch {
162+
return null;
163+
}
164+
}
165+
166+
/**
167+
* Download artifact content by storage path
168+
* Gets a presigned URL and fetches the content
169+
*/
170+
async downloadArtifact(
171+
taskId: string,
172+
runId: string,
173+
storagePath: string,
174+
): Promise<ArrayBuffer | null> {
175+
const url = await this.getArtifactPresignedUrl(taskId, runId, storagePath);
176+
if (!url) {
177+
return null;
178+
}
179+
180+
try {
181+
const response = await fetch(url);
182+
if (!response.ok) {
183+
throw new Error(`Failed to download artifact: ${response.status}`);
184+
}
185+
return response.arrayBuffer();
186+
} catch {
187+
return null;
188+
}
189+
}
190+
191+
/**
192+
* Fetch logs for a task run via the logs API endpoint
193+
* @param taskRun - The task run to fetch logs for
194+
* @returns Array of stored entries, or empty array if no logs available
195+
*/
196+
async fetchTaskRunLogs(taskRun: TaskRun): Promise<StoredEntry[]> {
197+
const teamId = this.getTeamId();
198+
199+
try {
200+
const response = await fetch(
201+
`${this.baseUrl}/api/projects/${teamId}/tasks/${taskRun.task}/runs/${taskRun.id}/logs`,
202+
{ headers: this.headers },
203+
);
204+
205+
if (!response.ok) {
206+
if (response.status === 404) {
207+
return [];
208+
}
209+
throw new Error(
210+
`Failed to fetch logs: ${response.status} ${response.statusText}`,
211+
);
212+
}
213+
214+
const content = await response.text();
215+
216+
if (!content.trim()) {
217+
return [];
218+
}
219+
220+
// Parse newline-delimited JSON
221+
return content
222+
.trim()
223+
.split("\n")
224+
.map((line) => JSON.parse(line) as StoredEntry);
225+
} catch (error) {
226+
throw new Error(
227+
`Failed to fetch task run logs: ${error instanceof Error ? error.message : String(error)}`,
228+
);
229+
}
230+
}
231+
116232
}

0 commit comments

Comments
 (0)