Skip to content

Commit 37c3436

Browse files
chore: implementing cancellation and refactoring
1 parent 05f5efa commit 37c3436

File tree

37 files changed

+1470
-679
lines changed

37 files changed

+1470
-679
lines changed

apps/api/src/lib/chat/core/ChatOrchestrator.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type { CoreChatOptions, Message } from "~/types";
1111
import { AssistantError, ErrorType } from "~/utils/errors";
1212
import { generateId } from "~/utils/id";
1313
import { getLogger } from "~/utils/logger";
14+
import { isAbortError } from "~/utils/abort";
1415

1516
const logger = getLogger({ prefix: "lib/chat/core/ChatOrchestrator" });
1617

@@ -367,7 +368,7 @@ export class ChatOrchestrator {
367368
private determineErrorType(error: any): ErrorType {
368369
if (
369370
error.name === "TimeoutError" ||
370-
error.name === "AbortError" ||
371+
isAbortError(error) ||
371372
error.code === "ECONNRESET" ||
372373
error.code === "ECONNABORTED" ||
373374
error.code === "ETIMEDOUT" ||

apps/api/src/routes/apps/sandbox.ts

Lines changed: 73 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,18 @@
11
import { type Context, Hono } from "hono";
22
import { validator as zValidator, describeRoute, resolver } from "hono-openapi";
3-
import z from "zod/v4";
4-
import { errorResponseSchema } from "@assistant/schemas";
3+
import {
4+
autoConnectSchema,
5+
cancelRunSchema,
6+
errorResponseSchema,
7+
executeSandboxRunSchema,
8+
githubConnectionSchema,
9+
listRunsQuerySchema,
10+
type AutoConnectPayload,
11+
type CancelRunPayload,
12+
type ExecuteSandboxRunPayload,
13+
type GitHubConnectionPayload,
14+
type ListRunsQueryPayload,
15+
} from "@assistant/schemas";
516

617
import { getServiceContext } from "~/lib/context/serviceContext";
718
import { ResponseFactory } from "~/lib/http/ResponseFactory";
@@ -10,53 +21,21 @@ import { createRouteLogger } from "~/middleware/loggerMiddleware";
1021
import type { IUser } from "~/types";
1122
import { executeSandboxRunStream } from "~/services/apps/sandbox/execute-stream";
1223
import {
13-
parseSandboxRunData,
14-
toSandboxRunResponse,
15-
} from "~/services/apps/sandbox/run-data";
24+
getSandboxRunForUser,
25+
listSandboxRunsForUser,
26+
requestSandboxRunCancellation,
27+
} from "~/services/apps/sandbox/runs";
1628
import { listGitHubAppConnectionsForUser } from "~/services/github/connections";
1729
import {
1830
deleteGitHubConnectionForUser,
1931
upsertGitHubConnectionFromDefaultAppForUser,
2032
upsertGitHubConnectionForUser,
2133
} from "~/services/github/manage-connections";
2234
import { AssistantError, ErrorType } from "~/utils/errors";
23-
import { safeParseJson } from "~/utils/json";
24-
import { SANDBOX_RUNS_APP_ID, SANDBOX_RUN_ITEM_TYPE } from "~/constants/app";
2535

2636
const app = new Hono();
2737
const routeLogger = createRouteLogger("apps/sandbox");
2838

29-
const githubConnectionSchema = z.object({
30-
installationId: z.number().int().positive(),
31-
appId: z.string().trim().min(1),
32-
privateKey: z.string().trim().min(1),
33-
webhookSecret: z.string().trim().min(1).optional(),
34-
repositories: z.array(z.string().trim().min(1)).optional(),
35-
});
36-
37-
const executeSandboxRunSchema = z.object({
38-
installationId: z.number().int().positive(),
39-
repo: z
40-
.string()
41-
.trim()
42-
.min(1)
43-
.regex(/^[\w.-]+\/[\w.-]+$/, "repo must be in owner/repo format"),
44-
task: z.string().trim().min(1),
45-
model: z.string().trim().min(1).optional(),
46-
shouldCommit: z.boolean().optional(),
47-
});
48-
49-
const autoConnectSchema = z.object({
50-
installationId: z.number().int().positive(),
51-
repositories: z.array(z.string().trim().min(1)).optional(),
52-
});
53-
54-
const listRunsQuerySchema = z.object({
55-
installationId: z.coerce.number().int().positive().optional(),
56-
repo: z.string().trim().min(1).optional(),
57-
limit: z.coerce.number().int().min(1).max(100).default(30),
58-
});
59-
6039
app.use("/*", (c, next) => {
6140
routeLogger.info(`Processing apps/sandbox route: ${c.req.path}`);
6241
return next();
@@ -177,9 +156,7 @@ app.post(
177156
zValidator("json", githubConnectionSchema),
178157
async (c: Context) => {
179158
const user = c.get("user") as IUser;
180-
const payload = c.req.valid("json" as never) as z.infer<
181-
typeof githubConnectionSchema
182-
>;
159+
const payload = c.req.valid("json" as never) as GitHubConnectionPayload;
183160
const serviceContext = getServiceContext(c);
184161

185162
await upsertGitHubConnectionForUser(serviceContext, user.id, payload);
@@ -216,9 +193,7 @@ app.post(
216193
zValidator("json", autoConnectSchema),
217194
async (c: Context) => {
218195
const user = c.get("user") as IUser;
219-
const payload = c.req.valid("json" as never) as z.infer<
220-
typeof autoConnectSchema
221-
>;
196+
const payload = c.req.valid("json" as never) as AutoConnectPayload;
222197
const serviceContext = getServiceContext(c);
223198

224199
await upsertGitHubConnectionFromDefaultAppForUser(serviceContext, user.id, {
@@ -307,44 +282,16 @@ app.get(
307282
zValidator("query", listRunsQuerySchema),
308283
async (c: Context) => {
309284
const user = c.get("user") as IUser;
310-
const { installationId, repo, limit } = c.req.valid(
311-
"query" as never,
312-
) as z.infer<typeof listRunsQuerySchema>;
285+
const payload = c.req.valid("query" as never) as ListRunsQueryPayload;
313286
const serviceContext = getServiceContext(c);
314-
const records =
315-
await serviceContext.repositories.appData.getAppDataByUserAndApp(
316-
user.id,
317-
SANDBOX_RUNS_APP_ID,
318-
);
319287

320-
const runs = records
321-
.map((record) => {
322-
const parsed = parseSandboxRunData(safeParseJson(record.data));
323-
if (!parsed) {
324-
return null;
325-
}
326-
327-
if (
328-
installationId !== undefined &&
329-
parsed.installationId !== installationId
330-
) {
331-
return null;
332-
}
333-
334-
if (repo && parsed.repo.toLowerCase() !== repo.toLowerCase()) {
335-
return null;
336-
}
337-
338-
return toSandboxRunResponse(parsed);
339-
})
340-
.filter((run): run is ReturnType<typeof toSandboxRunResponse> =>
341-
Boolean(run),
342-
)
343-
.sort(
344-
(a, b) =>
345-
new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(),
346-
)
347-
.slice(0, limit);
288+
const runs = await listSandboxRunsForUser({
289+
context: serviceContext,
290+
userId: user.id,
291+
installationId: payload.installationId,
292+
repo: payload.repo,
293+
limit: payload.limit,
294+
});
348295

349296
return ResponseFactory.success(c, { runs });
350297
},
@@ -375,33 +322,59 @@ app.get(
375322
async (c: Context) => {
376323
const user = c.get("user") as IUser;
377324
const runId = c.req.param("runId");
378-
379325
if (!runId) {
380326
throw new AssistantError("runId is required", ErrorType.PARAMS_ERROR);
381327
}
382328

383-
const serviceContext = getServiceContext(c);
384-
const records =
385-
await serviceContext.repositories.appData.getAppDataByUserAppAndItem(
386-
user.id,
387-
SANDBOX_RUNS_APP_ID,
388-
runId,
389-
SANDBOX_RUN_ITEM_TYPE,
390-
);
329+
const run = await getSandboxRunForUser({
330+
context: getServiceContext(c),
331+
userId: user.id,
332+
runId,
333+
});
391334

392-
if (!records.length) {
393-
throw new AssistantError("Sandbox run not found", ErrorType.NOT_FOUND);
394-
}
335+
return ResponseFactory.success(c, { run });
336+
},
337+
);
395338

396-
const run = parseSandboxRunData(safeParseJson(records[0].data));
397-
if (!run) {
398-
throw new AssistantError(
399-
"Sandbox run payload is invalid",
400-
ErrorType.NOT_FOUND,
401-
);
339+
app.post(
340+
"/runs/:runId/cancel",
341+
describeRoute({
342+
tags: ["apps"],
343+
description: "Cancel a running sandbox run",
344+
responses: {
345+
200: {
346+
description: "Sandbox run cancellation was processed",
347+
content: {
348+
"application/json": {},
349+
},
350+
},
351+
401: {
352+
description: "Unauthorized",
353+
content: {
354+
"application/json": {
355+
schema: resolver(errorResponseSchema),
356+
},
357+
},
358+
},
359+
},
360+
}),
361+
zValidator("json", cancelRunSchema),
362+
async (c: Context) => {
363+
const user = c.get("user") as IUser;
364+
const runId = c.req.param("runId");
365+
const payload = c.req.valid("json" as never) as CancelRunPayload;
366+
if (!runId) {
367+
throw new AssistantError("runId is required", ErrorType.PARAMS_ERROR);
402368
}
403369

404-
return ResponseFactory.success(c, { run: toSandboxRunResponse(run) });
370+
const result = await requestSandboxRunCancellation({
371+
context: getServiceContext(c),
372+
userId: user.id,
373+
runId,
374+
reason: payload.reason,
375+
});
376+
377+
return ResponseFactory.success(c, result);
405378
},
406379
);
407380

@@ -431,9 +404,7 @@ app.post(
431404
async (c: Context) => {
432405
const user = c.get("user") as IUser;
433406
const serviceContext = getServiceContext(c);
434-
const payload = c.req.valid("json" as never) as z.infer<
435-
typeof executeSandboxRunSchema
436-
>;
407+
const payload = c.req.valid("json" as never) as ExecuteSandboxRunPayload;
437408

438409
return executeSandboxRunStream({
439410
env: c.env,

apps/api/src/routes/sandbox.ts

Lines changed: 36 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
import { type Context, Hono } from "hono";
2+
import { validator as zValidator } from "hono-openapi";
3+
import {
4+
executeSandboxWorkerProxySchema,
5+
type ExecuteSandboxWorkerProxyPayload,
6+
} from "@assistant/schemas";
27

38
import { requireAuth } from "~/middleware/auth";
49
import { requirePlan } from "~/middleware/requirePlan";
@@ -9,51 +14,36 @@ const sandbox = new Hono();
914
sandbox.use("*", requireAuth);
1015
sandbox.use("*", requirePlan("pro"));
1116

12-
sandbox.post("/execute", async (c: Context) => {
13-
const ctx = getServiceContext(c);
14-
const user = ctx.requireUser();
15-
let body: Record<string, unknown>;
16-
try {
17-
body = (await c.req.json()) as Record<string, unknown>;
18-
} catch {
19-
return c.json({ error: "Invalid JSON body" }, 400);
20-
}
21-
22-
if (!c.env.SANDBOX_WORKER) {
23-
return c.json({ error: "Sandbox not available" }, 503);
24-
}
25-
26-
if (body.model !== undefined && typeof body.model !== "string") {
27-
return c.json({ error: "model must be a string" }, 400);
28-
}
29-
if (typeof body.repo !== "string" || !body.repo.trim()) {
30-
return c.json({ error: "repo must be a non-empty string" }, 400);
31-
}
32-
if (typeof body.task !== "string" || !body.task.trim()) {
33-
return c.json({ error: "task must be a non-empty string" }, 400);
34-
}
35-
36-
const installationId =
37-
typeof body.installationId === "number" &&
38-
Number.isFinite(body.installationId)
39-
? body.installationId
40-
: undefined;
41-
42-
const response = await executeSandboxWorker({
43-
env: c.env,
44-
context: ctx,
45-
user,
46-
repo: body.repo,
47-
task: body.task,
48-
taskType: typeof body.taskType === "string" ? body.taskType : undefined,
49-
model: typeof body.model === "string" ? body.model : undefined,
50-
shouldCommit: Boolean(body.shouldCommit),
51-
installationId,
52-
stream: c.req.header("accept")?.includes("text/event-stream"),
53-
runId: typeof body.runId === "string" ? body.runId : undefined,
54-
});
55-
56-
return response;
57-
});
17+
sandbox.post(
18+
"/execute",
19+
zValidator("json", executeSandboxWorkerProxySchema),
20+
async (c: Context) => {
21+
const ctx = getServiceContext(c);
22+
const user = ctx.requireUser();
23+
const payload = c.req.valid(
24+
"json" as never,
25+
) as ExecuteSandboxWorkerProxyPayload;
26+
27+
if (!c.env.SANDBOX_WORKER) {
28+
return c.json({ error: "Sandbox not available" }, 503);
29+
}
30+
31+
const response = await executeSandboxWorker({
32+
env: c.env,
33+
context: ctx,
34+
user,
35+
repo: payload.repo,
36+
task: payload.task,
37+
taskType: payload.taskType,
38+
model: payload.model,
39+
shouldCommit: Boolean(payload.shouldCommit),
40+
installationId: payload.installationId,
41+
stream: c.req.header("accept")?.includes("text/event-stream"),
42+
runId: payload.runId,
43+
});
44+
45+
return response;
46+
},
47+
);
5848

5949
export default sandbox;

apps/api/src/routes/user/index.ts

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import { type Context, Hono } from "hono";
22
import { describeRoute, resolver, validator as zValidator } from "hono-openapi";
3-
import z from "zod/v4";
43
import {
4+
githubConnectionSchema,
55
errorResponseSchema,
66
successResponseSchema,
77
storeProviderApiKeySchema,
88
updateUserSettingsResponseSchema,
99
updateUserSettingsSchema,
1010
userModelsResponseSchema,
1111
providersResponseSchema,
12+
type GitHubConnectionPayload,
1213
} from "@assistant/schemas";
1314

1415
import { getServiceContext } from "~/lib/context/serviceContext";
@@ -30,14 +31,6 @@ import exportHistoryRoute from "./export-history";
3031
const app = new Hono();
3132
const routeLogger = createRouteLogger("user");
3233

33-
const githubConnectionSchema = z.object({
34-
installationId: z.number().int().positive(),
35-
appId: z.string().trim().min(1),
36-
privateKey: z.string().trim().min(1),
37-
webhookSecret: z.string().trim().min(1).optional(),
38-
repositories: z.array(z.string().trim().min(1)).optional(),
39-
});
40-
4134
app.use("/*", requireAuth);
4235

4336
app.use("/*", (c, next) => {
@@ -226,9 +219,7 @@ app.post(
226219
);
227220
}
228221

229-
const payload = c.req.valid("json" as never) as z.infer<
230-
typeof githubConnectionSchema
231-
>;
222+
const payload = c.req.valid("json" as never) as GitHubConnectionPayload;
232223
const serviceContext = getServiceContext(c);
233224
await upsertGitHubConnectionForUser(serviceContext, user.id, payload);
234225

0 commit comments

Comments
 (0)