Skip to content
This repository was archived by the owner on Aug 6, 2026. It is now read-only.

Commit 6a15adb

Browse files
authored
feat(cloud-agent): support artifact-backed cloud prompts (#1736)
## Problem Cloud runs need the agent runtime to accept artifact-backed prompts and hydrate those files into the sandbox before prompting the model. https://github.com/user-attachments/assets/535243cb-e761-45b8-8bf1-298031ab7fb1 ## Changes - add artifact-aware prompt schemas and PostHog API calls in the agent package - hydrate cloud-run artifacts into local sandbox files and expose them as prompt resource links
1 parent 78e35a2 commit 6a15adb

15 files changed

Lines changed: 471 additions & 63 deletions

File tree

‎.github/workflows/agent-release.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
persist-credentials: false
2424

2525
- name: Setup pnpm
26-
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
26+
uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
2727

2828
- name: Set up Node 24
2929
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4

‎.github/workflows/build.yml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
persist-credentials: false
2020

2121
- name: Setup pnpm
22-
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
22+
uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
2323

2424
- name: Setup Node.js
2525
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
@@ -49,4 +49,4 @@ jobs:
4949
run: pnpm --filter agent build
5050

5151
- name: Build code
52-
run: pnpm --filter code build
52+
run: pnpm --filter code build

‎.github/workflows/code-release.yml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ jobs:
4646
persist-credentials: false
4747

4848
- name: Setup pnpm
49-
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
49+
uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
5050

5151
- name: Setup Node.js
5252
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
@@ -156,7 +156,7 @@ jobs:
156156
persist-credentials: false
157157

158158
- name: Setup pnpm
159-
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
159+
uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
160160

161161
- name: Setup Node.js
162162
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4

‎.github/workflows/test.yml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
persist-credentials: false
2020

2121
- name: Setup pnpm
22-
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
22+
uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
2323

2424
- name: Setup Node.js
2525
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
@@ -47,7 +47,7 @@ jobs:
4747
persist-credentials: false
4848

4949
- name: Setup pnpm
50-
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
50+
uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
5151

5252
- name: Setup Node.js
5353
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4

‎.github/workflows/typecheck.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
persist-credentials: false
2020

2121
- name: Setup pnpm
22-
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
22+
uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
2323

2424
- name: Setup Node.js
2525
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { describe, expect, it } from "vitest";
2+
import { promptToClaude } from "./acp-to-sdk";
3+
4+
describe("promptToClaude", () => {
5+
it("renders file resource links as explicit workspace attachments", () => {
6+
const result = promptToClaude({
7+
sessionId: "session-1",
8+
prompt: [
9+
{
10+
type: "resource_link",
11+
uri: "file:///tmp/workspace/.posthog/attachments/run-1/report.pdf",
12+
name: "report.pdf",
13+
},
14+
],
15+
});
16+
17+
expect(result.message.content).toEqual([
18+
{
19+
type: "text",
20+
text: [
21+
"Attached file available in the workspace:",
22+
"- name: report.pdf",
23+
"- path: /tmp/workspace/.posthog/attachments/run-1/report.pdf",
24+
"Use the available tools to inspect this file if needed.",
25+
].join("\n"),
26+
},
27+
]);
28+
});
29+
30+
it("preserves non-file resource links as links", () => {
31+
const result = promptToClaude({
32+
sessionId: "session-1",
33+
prompt: [
34+
{
35+
type: "resource_link",
36+
uri: "https://example.com/report.pdf",
37+
name: "report.pdf",
38+
},
39+
],
40+
});
41+
42+
expect(result.message.content).toEqual([
43+
{
44+
type: "text",
45+
text: "https://example.com/report.pdf",
46+
},
47+
]);
48+
});
49+
});

‎packages/agent/src/adapters/claude/conversion/acp-to-sdk.ts‎

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as path from "node:path";
2+
import { fileURLToPath } from "node:url";
23
import type { PromptRequest } from "@agentclientprotocol/sdk";
34
import type { SDKUserMessage } from "@anthropic-ai/claude-agent-sdk";
45
import type { ContentBlockParam } from "@anthropic-ai/sdk/resources";
@@ -11,11 +12,6 @@ function sdkText(value: string): ContentBlockParam {
1112

1213
function formatUriAsLink(uri: string): string {
1314
try {
14-
if (uri.startsWith("file://")) {
15-
const filePath = uri.slice(7);
16-
const name = path.basename(filePath) || filePath;
17-
return `[@${name}](${uri})`;
18-
}
1915
if (uri.startsWith("zed://")) {
2016
const name = path.basename(uri) || uri;
2117
return `[@${name}](${uri})`;
@@ -26,6 +22,21 @@ function formatUriAsLink(uri: string): string {
2622
}
2723
}
2824

25+
function formatFileAttachment(uri: string): string {
26+
try {
27+
const filePath = fileURLToPath(uri);
28+
const name = path.basename(filePath) || filePath;
29+
return [
30+
"Attached file available in the workspace:",
31+
`- name: ${name}`,
32+
`- path: ${filePath}`,
33+
"Use the available tools to inspect this file if needed.",
34+
].join("\n");
35+
} catch {
36+
return `Attached file available at ${uri}`;
37+
}
38+
}
39+
2940
function transformMcpCommand(text: string): string {
3041
const mcpMatch = text.match(/^\/mcp:([^:\s]+):(\S+)(\s+.*)?$/);
3142
if (mcpMatch) {
@@ -46,7 +57,13 @@ function processPromptChunk(
4657
break;
4758

4859
case "resource_link":
49-
content.push(sdkText(formatUriAsLink(chunk.uri)));
60+
content.push(
61+
sdkText(
62+
chunk.uri.startsWith("file://")
63+
? formatFileAttachment(chunk.uri)
64+
: formatUriAsLink(chunk.uri),
65+
),
66+
);
5067
break;
5168

5269
case "resource":

‎packages/agent/src/posthog-api.test.ts‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,36 @@ describe("PostHogAPIClient", () => {
4545
expect(refreshApiKey).toHaveBeenCalledTimes(1);
4646
expect(mockFetch).toHaveBeenCalledTimes(2);
4747
});
48+
49+
it("downloads artifacts through the backend endpoint", async () => {
50+
const client = new PostHogAPIClient({
51+
apiUrl: "https://app.posthog.com",
52+
getApiKey: vi.fn().mockResolvedValue("token"),
53+
projectId: 7,
54+
});
55+
const bytes = new TextEncoder().encode("hello world");
56+
57+
mockFetch.mockResolvedValueOnce({
58+
ok: true,
59+
arrayBuffer: vi.fn().mockResolvedValue(bytes.buffer),
60+
});
61+
62+
const artifact = await client.downloadArtifact(
63+
"task-1",
64+
"run-1",
65+
"tasks/artifacts/team_1/task_task-1/run_run-1/file.txt",
66+
);
67+
68+
expect(artifact).toEqual(bytes.buffer);
69+
expect(mockFetch).toHaveBeenCalledWith(
70+
"https://app.posthog.com/api/projects/7/tasks/task-1/runs/run-1/artifacts/download/",
71+
expect.objectContaining({
72+
method: "POST",
73+
body: JSON.stringify({
74+
storage_path: "tasks/artifacts/team_1/task_task-1/run_run-1/file.txt",
75+
}),
76+
headers: expect.any(Headers),
77+
}),
78+
);
79+
});
4880
});

‎packages/agent/src/posthog-api.ts‎

Lines changed: 13 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@ export type TaskRunUpdate = Partial<
3131
| "state"
3232
| "environment"
3333
>
34-
>;
34+
> & {
35+
state_remove_keys?: string[];
36+
};
3537

3638
export class PostHogAPIClient {
3739
private config: PostHogAPIConfig;
@@ -223,45 +225,26 @@ export class PostHogAPIClient {
223225
return response.artifacts ?? [];
224226
}
225227

226-
async getArtifactPresignedUrl(
227-
taskId: string,
228-
runId: string,
229-
storagePath: string,
230-
): Promise<string | null> {
231-
const teamId = this.getTeamId();
232-
try {
233-
const response = await this.apiRequest<{
234-
url: string;
235-
expires_in: number;
236-
}>(
237-
`/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/artifacts/presign/`,
238-
{
239-
method: "POST",
240-
body: JSON.stringify({ storage_path: storagePath }),
241-
},
242-
);
243-
return response.url;
244-
} catch {
245-
return null;
246-
}
247-
}
248-
249228
/**
250229
* Download artifact content by storage path
251-
* Gets a presigned URL and fetches the content
230+
* Streams the file through the PostHog backend so the sandbox does not need
231+
* direct access to object storage.
252232
*/
253233
async downloadArtifact(
254234
taskId: string,
255235
runId: string,
256236
storagePath: string,
257237
): Promise<ArrayBuffer | null> {
258-
const url = await this.getArtifactPresignedUrl(taskId, runId, storagePath);
259-
if (!url) {
260-
return null;
261-
}
238+
const teamId = this.getTeamId();
262239

263240
try {
264-
const response = await fetch(url);
241+
const response = await this.performRequestWithRetry(
242+
`/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/artifacts/download/`,
243+
{
244+
method: "POST",
245+
body: JSON.stringify({ storage_path: storagePath }),
246+
},
247+
);
265248
if (!response.ok) {
266249
throw new Error(`Failed to download artifact: ${response.status}`);
267250
}

0 commit comments

Comments
 (0)