Skip to content

Commit 510c111

Browse files
authored
fix(code): create cloud runs before uploading attachments (#1853)
1 parent 1b17df1 commit 510c111

4 files changed

Lines changed: 337 additions & 136 deletions

File tree

apps/code/src/renderer/api/posthogClient.test.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,100 @@ describe("PostHogAPIClient", () => {
125125
expect(post).not.toHaveBeenCalled();
126126
});
127127

128+
it("creates cloud task runs without relying on generated request typing", async () => {
129+
const fetch = vi.fn().mockResolvedValue({
130+
ok: true,
131+
json: async () => ({ id: "run-123", environment: "cloud" }),
132+
});
133+
const client = new PostHogAPIClient(
134+
"http://localhost:8000",
135+
async () => "token",
136+
async () => "token",
137+
123,
138+
);
139+
140+
(
141+
client as unknown as {
142+
api: { baseUrl: string; fetcher: { fetch: typeof fetch } };
143+
}
144+
).api = {
145+
baseUrl: "http://localhost:8000",
146+
fetcher: { fetch },
147+
};
148+
149+
await expect(
150+
client.createTaskRun("task-123", {
151+
environment: "cloud",
152+
mode: "interactive",
153+
branch: "feature/direct-upload",
154+
adapter: "codex",
155+
model: "gpt-5.4",
156+
reasoningLevel: "high",
157+
initialPermissionMode: "auto",
158+
}),
159+
).resolves.toEqual({ id: "run-123", environment: "cloud" });
160+
161+
expect(fetch).toHaveBeenCalledWith(
162+
expect.objectContaining({
163+
method: "post",
164+
path: "/api/projects/123/tasks/task-123/runs/",
165+
overrides: {
166+
body: JSON.stringify({
167+
mode: "interactive",
168+
branch: "feature/direct-upload",
169+
runtime_adapter: "codex",
170+
model: "gpt-5.4",
171+
reasoning_effort: "high",
172+
initial_permission_mode: "auto",
173+
environment: "cloud",
174+
}),
175+
},
176+
}),
177+
);
178+
});
179+
180+
it("starts an existing cloud task run with run-scoped artifact ids", async () => {
181+
const fetch = vi.fn().mockResolvedValue({
182+
ok: true,
183+
json: async () => ({ id: "task-123", latest_run: { id: "run-123" } }),
184+
});
185+
const client = new PostHogAPIClient(
186+
"http://localhost:8000",
187+
async () => "token",
188+
async () => "token",
189+
123,
190+
);
191+
192+
(
193+
client as unknown as {
194+
api: { baseUrl: string; fetcher: { fetch: typeof fetch } };
195+
}
196+
).api = {
197+
baseUrl: "http://localhost:8000",
198+
fetcher: { fetch },
199+
};
200+
201+
await expect(
202+
client.startTaskRun("task-123", "run-123", {
203+
pendingUserMessage: "Read the attached file first",
204+
pendingUserArtifactIds: ["artifact-1"],
205+
}),
206+
).resolves.toEqual({ id: "task-123", latest_run: { id: "run-123" } });
207+
208+
expect(fetch).toHaveBeenCalledWith(
209+
expect.objectContaining({
210+
method: "post",
211+
path: "/api/projects/123/tasks/task-123/runs/run-123/start/",
212+
overrides: {
213+
body: JSON.stringify({
214+
pending_user_message: "Read the attached file first",
215+
pending_user_artifact_ids: ["artifact-1"],
216+
}),
217+
},
218+
}),
219+
);
220+
});
221+
128222
describe("getSignalReport", () => {
129223
function makeClient(fetch: ReturnType<typeof vi.fn>) {
130224
const client = new PostHogAPIClient(

apps/code/src/renderer/api/posthogClient.ts

Lines changed: 154 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,101 @@ export interface FinalizedTaskArtifactUpload {
131131

132132
type CloudRuntimeAdapter = "claude" | "codex";
133133

134+
interface CloudRunOptions {
135+
adapter?: CloudRuntimeAdapter;
136+
model?: string;
137+
reasoningLevel?: string;
138+
sandboxEnvironmentId?: string;
139+
prAuthorshipMode?: PrAuthorshipMode;
140+
runSource?: CloudRunSource;
141+
signalReportId?: string;
142+
githubUserToken?: string;
143+
initialPermissionMode?: PermissionMode;
144+
}
145+
146+
interface CreateTaskRunOptions extends CloudRunOptions {
147+
environment?: "local" | "cloud";
148+
mode?: "interactive" | "background";
149+
branch?: string | null;
150+
}
151+
152+
interface StartTaskRunOptions {
153+
pendingUserMessage?: string;
154+
pendingUserArtifactIds?: string[];
155+
}
156+
157+
function buildCloudRunRequestBody(
158+
options?: CloudRunOptions & {
159+
branch?: string | null;
160+
mode?: "interactive" | "background";
161+
resumeFromRunId?: string;
162+
pendingUserMessage?: string;
163+
pendingUserArtifactIds?: string[];
164+
},
165+
): Record<string, unknown> {
166+
const body: Record<string, unknown> = {
167+
mode: options?.mode ?? "interactive",
168+
};
169+
170+
if (options?.branch) {
171+
body.branch = options.branch;
172+
}
173+
if (options?.adapter) {
174+
body.runtime_adapter = options.adapter;
175+
if (options.model) {
176+
body.model = options.model;
177+
}
178+
if (options.reasoningLevel) {
179+
if (!options.model) {
180+
throw new Error(
181+
"A cloud reasoning level requires a model to be selected.",
182+
);
183+
}
184+
if (
185+
!isSupportedReasoningEffort(
186+
options.adapter,
187+
options.model,
188+
options.reasoningLevel,
189+
)
190+
) {
191+
throw new Error(
192+
`Reasoning effort '${options.reasoningLevel}' is not supported for ${options.adapter} model '${options.model}'.`,
193+
);
194+
}
195+
body.reasoning_effort = options.reasoningLevel;
196+
}
197+
}
198+
if (options?.resumeFromRunId) {
199+
body.resume_from_run_id = options.resumeFromRunId;
200+
}
201+
if (options?.pendingUserMessage) {
202+
body.pending_user_message = options.pendingUserMessage;
203+
}
204+
if (options?.pendingUserArtifactIds?.length) {
205+
body.pending_user_artifact_ids = options.pendingUserArtifactIds;
206+
}
207+
if (options?.sandboxEnvironmentId) {
208+
body.sandbox_environment_id = options.sandboxEnvironmentId;
209+
}
210+
if (options?.prAuthorshipMode) {
211+
body.pr_authorship_mode = options.prAuthorshipMode;
212+
}
213+
if (options?.runSource) {
214+
body.run_source = options.runSource;
215+
}
216+
if (options?.signalReportId) {
217+
body.signal_report_id = options.signalReportId;
218+
}
219+
if (options?.githubUserToken) {
220+
body.github_user_token = options.githubUserToken;
221+
}
222+
if (options?.initialPermissionMode) {
223+
body.initial_permission_mode = options.initialPermissionMode;
224+
}
225+
226+
return body;
227+
}
228+
134229
function isObjectRecord(value: unknown): value is Record<string, unknown> {
135230
return typeof value === "object" && value !== null;
136231
}
@@ -798,78 +893,18 @@ export class PostHogAPIClient {
798893
async runTaskInCloud(
799894
taskId: string,
800895
branch?: string | null,
801-
options?: {
802-
adapter?: CloudRuntimeAdapter;
803-
model?: string;
804-
reasoningLevel?: string;
896+
options?: CloudRunOptions & {
805897
resumeFromRunId?: string;
806898
pendingUserMessage?: string;
807899
pendingUserArtifactIds?: string[];
808-
sandboxEnvironmentId?: string;
809-
prAuthorshipMode?: PrAuthorshipMode;
810-
runSource?: CloudRunSource;
811-
signalReportId?: string;
812-
githubUserToken?: string;
813-
initialPermissionMode?: PermissionMode;
814900
},
815901
): Promise<Task> {
816902
const teamId = await this.getTeamId();
817-
const body: Record<string, unknown> = { mode: "interactive" };
818-
if (branch) {
819-
body.branch = branch;
820-
}
821-
if (options?.adapter) {
822-
body.runtime_adapter = options.adapter;
823-
if (options.model) {
824-
body.model = options.model;
825-
}
826-
if (options.reasoningLevel) {
827-
if (!options.model) {
828-
throw new Error(
829-
"A cloud reasoning level requires a model to be selected.",
830-
);
831-
}
832-
if (
833-
!isSupportedReasoningEffort(
834-
options.adapter,
835-
options.model,
836-
options.reasoningLevel,
837-
)
838-
) {
839-
throw new Error(
840-
`Reasoning effort '${options.reasoningLevel}' is not supported for ${options.adapter} model '${options.model}'.`,
841-
);
842-
}
843-
body.reasoning_effort = options.reasoningLevel;
844-
}
845-
}
846-
if (options?.resumeFromRunId) {
847-
body.resume_from_run_id = options.resumeFromRunId;
848-
}
849-
if (options?.pendingUserMessage) {
850-
body.pending_user_message = options.pendingUserMessage;
851-
}
852-
if (options?.pendingUserArtifactIds?.length) {
853-
body.pending_user_artifact_ids = options.pendingUserArtifactIds;
854-
}
855-
if (options?.sandboxEnvironmentId) {
856-
body.sandbox_environment_id = options.sandboxEnvironmentId;
857-
}
858-
if (options?.prAuthorshipMode) {
859-
body.pr_authorship_mode = options.prAuthorshipMode;
860-
}
861-
if (options?.runSource) {
862-
body.run_source = options.runSource;
863-
}
864-
if (options?.signalReportId) {
865-
body.signal_report_id = options.signalReportId;
866-
}
867-
if (options?.githubUserToken) {
868-
body.github_user_token = options.githubUserToken;
869-
}
870-
if (options?.initialPermissionMode) {
871-
body.initial_permission_mode = options.initialPermissionMode;
872-
}
903+
const body = buildCloudRunRequestBody({
904+
...options,
905+
branch,
906+
mode: "interactive",
907+
});
873908

874909
const data = await this.api.post(
875910
`/api/projects/{project_id}/tasks/{id}/run/`,
@@ -1067,19 +1102,62 @@ export class PostHogAPIClient {
10671102
return await response.json();
10681103
}
10691104

1070-
async createTaskRun(taskId: string): Promise<TaskRun> {
1105+
async createTaskRun(
1106+
taskId: string,
1107+
options?: CreateTaskRunOptions,
1108+
): Promise<TaskRun> {
10711109
const teamId = await this.getTeamId();
1072-
const data = await this.api.post(
1073-
`/api/projects/{project_id}/tasks/{task_id}/runs/`,
1074-
{
1075-
path: { project_id: teamId.toString(), task_id: taskId },
1076-
//@ts-expect-error the generated client does not infer the request type unless explicitly specified on the viewset
1077-
body: {
1078-
environment: "local" as const,
1079-
},
1110+
const url = new URL(
1111+
`${this.api.baseUrl}/api/projects/${teamId}/tasks/${taskId}/runs/`,
1112+
);
1113+
const response = await this.api.fetcher.fetch({
1114+
method: "post",
1115+
url,
1116+
path: `/api/projects/${teamId}/tasks/${taskId}/runs/`,
1117+
overrides: {
1118+
body: JSON.stringify({
1119+
...buildCloudRunRequestBody({
1120+
...options,
1121+
mode: options?.mode ?? "background",
1122+
}),
1123+
environment: options?.environment ?? "local",
1124+
}),
10801125
},
1126+
});
1127+
1128+
if (!response.ok) {
1129+
throw new Error(`Failed to create task run: ${response.statusText}`);
1130+
}
1131+
1132+
return (await response.json()) as TaskRun;
1133+
}
1134+
1135+
async startTaskRun(
1136+
taskId: string,
1137+
runId: string,
1138+
options?: StartTaskRunOptions,
1139+
): Promise<Task> {
1140+
const teamId = await this.getTeamId();
1141+
const url = new URL(
1142+
`${this.api.baseUrl}/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/start/`,
10811143
);
1082-
return data as unknown as TaskRun;
1144+
const response = await this.api.fetcher.fetch({
1145+
method: "post",
1146+
url,
1147+
path: `/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/start/`,
1148+
overrides: {
1149+
body: JSON.stringify({
1150+
pending_user_message: options?.pendingUserMessage,
1151+
pending_user_artifact_ids: options?.pendingUserArtifactIds,
1152+
}),
1153+
},
1154+
});
1155+
1156+
if (!response.ok) {
1157+
throw new Error(`Failed to start task run: ${response.statusText}`);
1158+
}
1159+
1160+
return (await response.json()) as Task;
10831161
}
10841162

10851163
async updateTaskRun(

0 commit comments

Comments
 (0)