Skip to content

Commit edab653

Browse files
Kaspresteipete
authored andcommitted
fix(code-mode): return structured worker error codes
1 parent 0d8c9ca commit edab653

3 files changed

Lines changed: 131 additions & 24 deletions

File tree

src/agents/code-mode.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -828,5 +828,68 @@ describe("Code Mode", () => {
828828
await expect(heartbeat).resolves.toBe("main-event-loop-alive");
829829
expect(details.status).toBe("failed");
830830
expect(String(details.error)).toContain("timeout exceeded");
831+
expect(details.code).toBe("timeout");
832+
});
833+
834+
it("classifies missing worker runtime as unavailable", async () => {
835+
const config = resolveCodeModeConfig({ tools: { codeMode: true } } as never);
836+
const missingWorkerUrl = new URL("./missing-code-mode.worker.js", import.meta.url);
837+
838+
const result = await __testing.runCodeModeWorker(
839+
{
840+
kind: "exec",
841+
source: "return 1;",
842+
config,
843+
catalog: [],
844+
},
845+
500,
846+
missingWorkerUrl,
847+
);
848+
849+
expect(result.status).toBe("failed");
850+
expect(result).toMatchObject({
851+
code: "runtime_unavailable",
852+
});
853+
});
854+
855+
it("classifies nonzero worker exits as unavailable", async () => {
856+
const config = resolveCodeModeConfig({ tools: { codeMode: true } } as never);
857+
const exitingWorkerUrl = new URL("data:text/javascript,process.exit(1)");
858+
859+
const result = await __testing.runCodeModeWorker(
860+
{
861+
kind: "exec",
862+
source: "return 1;",
863+
config,
864+
catalog: [],
865+
},
866+
500,
867+
exitingWorkerUrl,
868+
);
869+
870+
expect(result.status).toBe("failed");
871+
expect(result).toMatchObject({
872+
code: "runtime_unavailable",
873+
});
874+
});
875+
876+
it("does not classify guest interrupted errors as timeouts", async () => {
877+
const config = resolveCodeModeConfig({ tools: { codeMode: true } } as never);
878+
879+
const result = await __testing.runCodeModeWorker(
880+
{
881+
kind: "exec",
882+
source: 'throw new Error("interrupted");',
883+
config,
884+
catalog: [],
885+
},
886+
500,
887+
);
888+
889+
expect(result.status).toBe("failed");
890+
expect(result).toMatchObject({
891+
code: "internal_error",
892+
error: "interrupted",
893+
});
831894
});
832895
});

src/agents/code-mode.ts

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ type CodeModeRunState = {
101101

102102
type CodeModeToolContext = ToolSearchToolContext;
103103

104+
type CodeModeFailureCode = "invalid_input" | "runtime_unavailable" | "timeout" | "internal_error";
105+
104106
type CodeModeWorkerResult =
105107
| {
106108
status: "completed";
@@ -116,7 +118,7 @@ type CodeModeWorkerResult =
116118
| {
117119
status: "failed";
118120
error: string;
119-
code: "invalid_input" | "internal_error";
121+
code: CodeModeFailureCode;
120122
output: unknown[];
121123
};
122124

@@ -504,13 +506,31 @@ function codeModeWorkerUrl(): URL {
504506
return resolveCodeModeWorkerUrl(import.meta.url);
505507
}
506508

509+
function failedCodeModeWorkerResult(
510+
error: unknown,
511+
code: CodeModeFailureCode,
512+
): Extract<CodeModeWorkerResult, { status: "failed" }> {
513+
return {
514+
status: "failed",
515+
error: errorMessage(error),
516+
code,
517+
output: [],
518+
};
519+
}
520+
507521
async function runCodeModeWorker(
508522
workerData: unknown,
509523
timeoutMs: number,
524+
workerUrl?: URL,
510525
): Promise<CodeModeWorkerResult> {
511-
const worker = new Worker(codeModeWorkerUrl(), {
512-
workerData,
513-
});
526+
let worker: Worker;
527+
try {
528+
worker = new Worker(workerUrl ?? codeModeWorkerUrl(), {
529+
workerData,
530+
});
531+
} catch (error) {
532+
return failedCodeModeWorkerResult(error, "runtime_unavailable");
533+
}
514534
let timer: ReturnType<typeof setTimeout> | undefined;
515535
try {
516536
return await new Promise<CodeModeWorkerResult>((resolve) => {
@@ -527,7 +547,7 @@ async function runCodeModeWorker(
527547
finish({
528548
status: "failed",
529549
error: "code mode worker timeout exceeded",
530-
code: "internal_error",
550+
code: "timeout",
531551
output: [],
532552
});
533553
}, timeoutMs);
@@ -545,21 +565,16 @@ async function runCodeModeWorker(
545565
);
546566
});
547567
worker.once("error", (error) => {
548-
finish({
549-
status: "failed",
550-
error: errorMessage(error),
551-
code: "internal_error",
552-
output: [],
553-
});
568+
finish(failedCodeModeWorkerResult(error, "runtime_unavailable"));
554569
});
555570
worker.once("exit", (code) => {
556571
if (code !== 0) {
557-
finish({
558-
status: "failed",
559-
error: `code mode worker exited with code ${code}`,
560-
code: "internal_error",
561-
output: [],
562-
});
572+
finish(
573+
failedCodeModeWorkerResult(
574+
new Error(`code mode worker exited with code ${code}`),
575+
"runtime_unavailable",
576+
),
577+
);
563578
}
564579
});
565580
});
@@ -949,6 +964,7 @@ export const testing = {
949964
activeRuns,
950965
resumingRunIds,
951966
codeModeWorkerUrl,
967+
runCodeModeWorker,
952968
resolveCodeModeWorkerUrl,
953969
resolveCodeModeConfig,
954970
getTypescriptRuntimePromise: () => typescriptRuntimePromise,

src/agents/code-mode.worker.ts

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,38 @@ type CodeModeWorkerResult =
5353
| {
5454
status: "failed";
5555
error: string;
56-
code: "invalid_input" | "internal_error";
56+
code: "invalid_input" | "runtime_unavailable" | "timeout" | "internal_error";
5757
output: unknown[];
5858
};
5959

60+
class CodeModeWorkerFailure extends Error {
61+
readonly code: Extract<CodeModeWorkerResult, { status: "failed" }>["code"];
62+
63+
constructor(
64+
code: Extract<CodeModeWorkerResult, { status: "failed" }>["code"],
65+
message: string,
66+
options?: ErrorOptions,
67+
) {
68+
super(message, options);
69+
this.name = "CodeModeWorkerFailure";
70+
this.code = code;
71+
}
72+
}
73+
74+
class CodeModeGuestError extends Error {
75+
constructor(message: string) {
76+
super(message);
77+
this.name = "CodeModeGuestError";
78+
}
79+
}
80+
81+
function isQuickJsInterruptedError(error: unknown): boolean {
82+
if (error instanceof CodeModeGuestError) {
83+
return false;
84+
}
85+
return errorMessage(error) === "interrupted";
86+
}
87+
6088
type VmRun = {
6189
vm: QuickJS;
6290
didTimeout: () => boolean;
@@ -329,7 +357,7 @@ async function readCompletedResult(vm: QuickJS, resultHandle: JSValueHandle): Pr
329357
const settled = await vm.resolvePromise(resultHandle);
330358
if ("error" in settled) {
331359
try {
332-
throw new Error(errorMessage(vm.dump(settled.error)));
360+
throw new CodeModeGuestError(errorMessage(vm.dump(settled.error)));
333361
} finally {
334362
settled.error.dispose();
335363
}
@@ -391,8 +419,8 @@ async function runExec(input: Extract<CodeModeWorkerInput, { kind: "exec" }>) {
391419
resultHandle.dispose();
392420
}
393421
} catch (error) {
394-
if (didTimeout()) {
395-
throw new Error("code mode timeout exceeded", { cause: error });
422+
if (didTimeout() || isQuickJsInterruptedError(error)) {
423+
throw new CodeModeWorkerFailure("timeout", "code mode timeout exceeded", { cause: error });
396424
}
397425
throw error;
398426
} finally {
@@ -448,8 +476,8 @@ async function runResume(input: Extract<CodeModeWorkerInput, { kind: "resume" }>
448476
resultHandle.dispose();
449477
}
450478
} catch (error) {
451-
if (didTimeout()) {
452-
throw new Error("code mode timeout exceeded", { cause: error });
479+
if (didTimeout() || isQuickJsInterruptedError(error)) {
480+
throw new CodeModeWorkerFailure("timeout", "code mode timeout exceeded", { cause: error });
453481
}
454482
throw error;
455483
} finally {
@@ -496,7 +524,7 @@ async function main(): Promise<CodeModeWorkerResult> {
496524
return {
497525
status: "failed",
498526
error: errorMessage(error),
499-
code: "internal_error",
527+
code: error instanceof CodeModeWorkerFailure ? error.code : "internal_error",
500528
output: [],
501529
};
502530
}

0 commit comments

Comments
 (0)