Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/agent-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
persist-credentials: false

- name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4

- name: Set up Node 24
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
persist-credentials: false

- name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4

- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
Expand Down Expand Up @@ -49,4 +49,4 @@ jobs:
run: pnpm --filter agent build

- name: Build code
run: pnpm --filter code build
run: pnpm --filter code build
4 changes: 2 additions & 2 deletions .github/workflows/code-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ jobs:
persist-credentials: false

- name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4

- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
Expand Down Expand Up @@ -156,7 +156,7 @@ jobs:
persist-credentials: false

- name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4

- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
persist-credentials: false

- name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4

- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
Expand Down Expand Up @@ -47,7 +47,7 @@ jobs:
persist-credentials: false

- name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4

- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/typecheck.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
persist-credentials: false

- name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4

- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
Expand Down
49 changes: 49 additions & 0 deletions packages/agent/src/adapters/claude/conversion/acp-to-sdk.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { describe, expect, it } from "vitest";
import { promptToClaude } from "./acp-to-sdk";

describe("promptToClaude", () => {
it("renders file resource links as explicit workspace attachments", () => {
const result = promptToClaude({
sessionId: "session-1",
prompt: [
{
type: "resource_link",
uri: "file:///tmp/workspace/.posthog/attachments/run-1/report.pdf",
name: "report.pdf",
},
],
});

expect(result.message.content).toEqual([
{
type: "text",
text: [
"Attached file available in the workspace:",
"- name: report.pdf",
"- path: /tmp/workspace/.posthog/attachments/run-1/report.pdf",
"Use the available tools to inspect this file if needed.",
].join("\n"),
},
]);
});

it("preserves non-file resource links as links", () => {
const result = promptToClaude({
sessionId: "session-1",
prompt: [
{
type: "resource_link",
uri: "https://example.com/report.pdf",
name: "report.pdf",
},
],
});

expect(result.message.content).toEqual([
{
type: "text",
text: "https://example.com/report.pdf",
},
]);
});
});
29 changes: 23 additions & 6 deletions packages/agent/src/adapters/claude/conversion/acp-to-sdk.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as path from "node:path";
import { fileURLToPath } from "node:url";
import type { PromptRequest } from "@agentclientprotocol/sdk";
import type { SDKUserMessage } from "@anthropic-ai/claude-agent-sdk";
import type { ContentBlockParam } from "@anthropic-ai/sdk/resources";
Expand All @@ -11,11 +12,6 @@ function sdkText(value: string): ContentBlockParam {

function formatUriAsLink(uri: string): string {
try {
if (uri.startsWith("file://")) {
const filePath = uri.slice(7);
const name = path.basename(filePath) || filePath;
return `[@${name}](${uri})`;
}
if (uri.startsWith("zed://")) {
const name = path.basename(uri) || uri;
return `[@${name}](${uri})`;
Expand All @@ -26,6 +22,21 @@ function formatUriAsLink(uri: string): string {
}
}

function formatFileAttachment(uri: string): string {
try {
const filePath = fileURLToPath(uri);
const name = path.basename(filePath) || filePath;
return [
"Attached file available in the workspace:",
`- name: ${name}`,
`- path: ${filePath}`,
"Use the available tools to inspect this file if needed.",
].join("\n");
} catch {
return `Attached file available at ${uri}`;
}
}

function transformMcpCommand(text: string): string {
const mcpMatch = text.match(/^\/mcp:([^:\s]+):(\S+)(\s+.*)?$/);
if (mcpMatch) {
Expand All @@ -46,7 +57,13 @@ function processPromptChunk(
break;

case "resource_link":
content.push(sdkText(formatUriAsLink(chunk.uri)));
content.push(
sdkText(
chunk.uri.startsWith("file://")
? formatFileAttachment(chunk.uri)
: formatUriAsLink(chunk.uri),
),
);
break;

case "resource":
Expand Down
32 changes: 32 additions & 0 deletions packages/agent/src/posthog-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,36 @@ describe("PostHogAPIClient", () => {
expect(refreshApiKey).toHaveBeenCalledTimes(1);
expect(mockFetch).toHaveBeenCalledTimes(2);
});

it("downloads artifacts through the backend endpoint", async () => {
const client = new PostHogAPIClient({
apiUrl: "https://app.posthog.com",
getApiKey: vi.fn().mockResolvedValue("token"),
projectId: 7,
});
const bytes = new TextEncoder().encode("hello world");

mockFetch.mockResolvedValueOnce({
ok: true,
arrayBuffer: vi.fn().mockResolvedValue(bytes.buffer),
});

const artifact = await client.downloadArtifact(
"task-1",
"run-1",
"tasks/artifacts/team_1/task_task-1/run_run-1/file.txt",
);

expect(artifact).toEqual(bytes.buffer);
expect(mockFetch).toHaveBeenCalledWith(
"https://app.posthog.com/api/projects/7/tasks/task-1/runs/run-1/artifacts/download/",
expect.objectContaining({
method: "POST",
body: JSON.stringify({
storage_path: "tasks/artifacts/team_1/task_task-1/run_run-1/file.txt",
}),
headers: expect.any(Headers),
}),
);
});
});
43 changes: 13 additions & 30 deletions packages/agent/src/posthog-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ export type TaskRunUpdate = Partial<
| "state"
| "environment"
>
>;
> & {
state_remove_keys?: string[];
};

export class PostHogAPIClient {
private config: PostHogAPIConfig;
Expand Down Expand Up @@ -223,45 +225,26 @@ export class PostHogAPIClient {
return response.artifacts ?? [];
}

async getArtifactPresignedUrl(
taskId: string,
runId: string,
storagePath: string,
): Promise<string | null> {
const teamId = this.getTeamId();
try {
const response = await this.apiRequest<{
url: string;
expires_in: number;
}>(
`/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/artifacts/presign/`,
{
method: "POST",
body: JSON.stringify({ storage_path: storagePath }),
},
);
return response.url;
} catch {
return null;
}
}

/**
* Download artifact content by storage path
* Gets a presigned URL and fetches the content
* Streams the file through the PostHog backend so the sandbox does not need
* direct access to object storage.
*/
async downloadArtifact(
taskId: string,
runId: string,
storagePath: string,
): Promise<ArrayBuffer | null> {
const url = await this.getArtifactPresignedUrl(taskId, runId, storagePath);
if (!url) {
return null;
}
const teamId = this.getTeamId();

try {
const response = await fetch(url);
const response = await this.performRequestWithRetry(
`/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/artifacts/download/`,
{
method: "POST",
body: JSON.stringify({ storage_path: storagePath }),
},
);
if (!response.ok) {
throw new Error(`Failed to download artifact: ${response.status}`);
}
Expand Down
Loading
Loading