Compare commits

...

1 Commits

Author SHA1 Message Date
Vincent Koc
f2f893c14a refactor(agents): extract code mode runtime package 2026-05-30 14:55:20 +01:00
15 changed files with 730 additions and 652 deletions

View File

@@ -0,0 +1,34 @@
{
"name": "@openclaw/code-mode-runtime",
"version": "0.0.0-private",
"private": true,
"files": [
"dist"
],
"type": "module",
"main": "./dist/index.mjs",
"types": "./dist/index.d.mts",
"exports": {
".": {
"types": "./dist/index.d.mts",
"import": "./dist/index.mjs",
"default": "./dist/index.mjs"
},
"./types": {
"types": "./dist/types.d.mts",
"import": "./dist/types.mjs",
"default": "./dist/types.mjs"
},
"./worker": {
"types": "./dist/worker.d.mts",
"import": "./dist/worker.mjs",
"default": "./dist/worker.mjs"
}
},
"scripts": {
"build": "tsdown src/index.ts src/types.ts src/worker.ts --no-config --platform node --format esm --dts --out-dir dist --clean"
},
"dependencies": {
"quickjs-wasi": "3.0.0"
}
}

View File

@@ -0,0 +1,18 @@
import path from "node:path";
import { fileURLToPath } from "node:url";
export type {
CodeModeBridgeMethod,
CodeModeFailureCode,
CodeModePendingBridgeRequest,
CodeModeRuntimeConfig,
CodeModeSettledBridgeRequest,
CodeModeWorkerInput,
CodeModeWorkerResult,
} from "./types.js";
export function resolveCodeModeRuntimeWorkerUrl(currentModuleUrl = import.meta.url): URL {
const currentPath = fileURLToPath(currentModuleUrl);
const extension = path.extname(currentPath) || ".js";
return new URL(`./worker${extension}`, currentModuleUrl);
}

View File

@@ -0,0 +1,62 @@
export type CodeModeBridgeMethod = "search" | "describe" | "call" | "yield";
export type CodeModeRuntimeConfig = {
timeoutMs: number;
memoryLimitBytes: number;
maxPendingToolCalls: number;
maxSnapshotBytes: number;
};
export type CodeModePendingBridgeRequest = {
id: string;
method: CodeModeBridgeMethod;
args: unknown[];
};
export type CodeModeSettledBridgeRequest = {
id: string;
ok: boolean;
value?: unknown;
error?: string;
};
export type CodeModeWorkerInput =
| {
kind: "exec";
source: string;
config: CodeModeRuntimeConfig;
catalog: unknown[];
}
| {
kind: "resume";
snapshotBytes: Uint8Array;
config: CodeModeRuntimeConfig;
settledRequests: CodeModeSettledBridgeRequest[];
};
export type CodeModeFailureCode =
| "invalid_input"
| "runtime_unavailable"
| "timeout"
| "output_limit_exceeded"
| "snapshot_limit_exceeded"
| "internal_error";
export type CodeModeWorkerResult =
| {
status: "completed";
value: unknown;
output: unknown[];
}
| {
status: "waiting";
snapshotBytes: Uint8Array;
pendingRequests: CodeModePendingBridgeRequest[];
output: unknown[];
}
| {
status: "failed";
error: string;
code: CodeModeFailureCode;
output: unknown[];
};

View File

@@ -0,0 +1,555 @@
import { randomUUID } from "node:crypto";
import { readFile } from "node:fs/promises";
import { createRequire } from "node:module";
import { parentPort, workerData } from "node:worker_threads";
import { EvalFlags, Intrinsics, JSException, QuickJS, type JSValueHandle } from "quickjs-wasi";
import type {
CodeModePendingBridgeRequest,
CodeModeRuntimeConfig,
CodeModeSettledBridgeRequest,
CodeModeWorkerInput,
CodeModeWorkerResult,
} from "./types.js";
const require = createRequire(import.meta.url);
const QUICKJS_WASM_PATH = require.resolve("quickjs-wasi/quickjs.wasm");
let quickJsWasmModulePromise: Promise<WebAssembly.Module> | undefined;
class CodeModeWorkerFailure extends Error {
readonly code: Extract<CodeModeWorkerResult, { status: "failed" }>["code"];
constructor(
code: Extract<CodeModeWorkerResult, { status: "failed" }>["code"],
message: string,
options?: ErrorOptions,
) {
super(message, options);
this.name = "CodeModeWorkerFailure";
this.code = code;
}
}
class CodeModeWorkerFailureWithOutput extends CodeModeWorkerFailure {
readonly output: unknown[];
constructor(
code: Extract<CodeModeWorkerResult, { status: "failed" }>["code"],
message: string,
output: unknown[],
options?: ErrorOptions,
) {
super(code, message, options);
this.name = "CodeModeWorkerFailureWithOutput";
this.output = output;
}
}
class CodeModeGuestError extends Error {
constructor(message: string) {
super(message);
this.name = "CodeModeGuestError";
}
}
function isQuickJsInterruptedError(error: unknown): boolean {
if (error instanceof CodeModeGuestError) {
return false;
}
return errorMessage(error) === "interrupted";
}
type VmRun = {
vm: QuickJS;
didTimeout: () => boolean;
};
function getQuickJsWasmModule(): Promise<WebAssembly.Module> {
quickJsWasmModulePromise ??= readFile(QUICKJS_WASM_PATH).then((bytes) =>
WebAssembly.compile(bytes),
);
return quickJsWasmModulePromise;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
function toJsonSafe(value: unknown): unknown {
if (value === undefined) {
return null;
}
try {
return JSON.parse(JSON.stringify(value)) as unknown;
} catch {
if (value instanceof Error) {
return { name: value.name, message: value.message };
}
if (value === null) {
return null;
}
switch (typeof value) {
case "string":
case "number":
case "boolean":
return value;
case "bigint":
case "symbol":
case "function":
return String(value);
default:
return Object.prototype.toString.call(value);
}
}
}
function errorMessage(error: unknown): string {
if (error instanceof JSException) {
return error.stack || error.message || String(error);
}
if (error instanceof Error) {
return error.message || String(error);
}
return String(error);
}
const CONTROLLER_SOURCE = String.raw`
(() => {
const output = [];
const pending = new Map();
const catalog = Array.isArray(globalThis.__openclawCatalog) ? globalThis.__openclawCatalog : [];
function safe(value) {
if (value === undefined) return null;
try {
return JSON.parse(JSON.stringify(value));
} catch {
if (value instanceof Error) {
return { name: value.name, message: value.message };
}
if (value === null) return null;
const type = typeof value;
if (type === "string" || type === "number" || type === "boolean") return value;
return String(value);
}
}
function asText(value) {
if (typeof value === "string") return value;
const encoded = JSON.stringify(safe(value));
return typeof encoded === "string" ? encoded : String(value);
}
function request(method, args) {
const id = String(globalThis.__openclawHostRequest(String(method), JSON.stringify(safe(args ?? []))));
return new Promise((resolve, reject) => {
pending.set(id, { resolve, reject });
});
}
function settle(id, ok, payload) {
const entry = pending.get(String(id));
if (!entry) return false;
pending.delete(String(id));
let parsed = null;
try {
parsed = JSON.parse(String(payload));
} catch {
parsed = String(payload);
}
if (ok) {
entry.resolve(parsed);
} else {
const error = new Error(typeof parsed === "string" ? parsed : parsed?.message ?? "nested tool failed");
entry.reject(error);
}
return true;
}
const baseTools = Object.create(null);
Object.defineProperties(baseTools, {
search: { value: (query, options) => request("search", [query, options]), enumerable: true },
describe: { value: (id) => request("describe", [id]), enumerable: true },
call: { value: (id, input) => request("call", [id, input]), enumerable: true },
});
const safeNameCounts = new Map();
for (const tool of catalog) {
const name = typeof tool?.name === "string" ? tool.name : "";
if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name)) continue;
safeNameCounts.set(name, (safeNameCounts.get(name) ?? 0) + 1);
}
for (const tool of catalog) {
const name = typeof tool?.name === "string" ? tool.name : "";
const id = typeof tool?.id === "string" ? tool.id : "";
if (!id || safeNameCounts.get(name) !== 1 || Object.prototype.hasOwnProperty.call(baseTools, name)) {
continue;
}
Object.defineProperty(baseTools, name, {
value: (input) => request("call", [id, input]),
enumerable: true,
});
}
Object.defineProperties(globalThis, {
ALL_TOOLS: { value: Object.freeze(catalog.slice()), enumerable: true },
tools: { value: Object.freeze(baseTools), enumerable: true },
text: { value: (value) => output.push({ type: "text", text: asText(value) }), enumerable: true },
json: { value: (value) => output.push({ type: "json", value: safe(value) }), enumerable: true },
yield_control: { value: (reason) => request("yield", [reason]), enumerable: true },
__openclawSettleBridge: { value: settle },
__openclawTakeOutput: { value: () => output.splice(0) },
});
})();
`;
function buildUserSource(code: string): string {
return `globalThis.__openclawResult = (async () => {\n${code}\n})()`;
}
function createHostRequestHandler(params: {
vm: QuickJS;
pendingRequests: CodeModePendingBridgeRequest[];
config: CodeModeRuntimeConfig;
}): (this: JSValueHandle, method: JSValueHandle, argsJson: JSValueHandle) => JSValueHandle {
return (methodHandle, argsHandle) => {
if (params.pendingRequests.length >= params.config.maxPendingToolCalls) {
throw new Error("too many pending code mode tool calls");
}
const method = methodHandle.toString();
if (method !== "search" && method !== "describe" && method !== "call" && method !== "yield") {
throw new Error("unsupported code mode bridge method");
}
let args: unknown = [];
try {
args = JSON.parse(argsHandle.toString()) as unknown;
} catch {
args = [];
}
const id = `bridge:${params.pendingRequests.length + 1}:${randomUUID()}`;
params.pendingRequests.push({
id,
method,
args: Array.isArray(args) ? args : [],
});
return params.vm.newString(id);
};
}
async function createVm(params: {
catalog: unknown[];
config: CodeModeRuntimeConfig;
pendingRequests: CodeModePendingBridgeRequest[];
}): Promise<VmRun> {
const startedAt = Date.now();
let timedOut = false;
const vm = await QuickJS.create({
wasm: await getQuickJsWasmModule(),
memoryLimit: params.config.memoryLimitBytes,
intrinsics: Intrinsics.ALL,
timezoneOffset: 0,
interruptHandler: () => {
timedOut = Date.now() - startedAt > params.config.timeoutMs;
return timedOut;
},
});
const catalogHandle = vm.hostToHandle(params.catalog);
try {
vm.setProp(vm.global, "__openclawCatalog", catalogHandle);
} finally {
catalogHandle.dispose();
}
const hostRequest = vm.newFunction(
"__openclawHostRequest",
createHostRequestHandler({
vm,
pendingRequests: params.pendingRequests,
config: params.config,
}),
);
try {
vm.setProp(vm.global, "__openclawHostRequest", hostRequest);
} finally {
hostRequest.dispose();
}
vm.evalCode(CONTROLLER_SOURCE, "openclaw-code-mode:controller.js").dispose();
return { vm, didTimeout: () => timedOut };
}
async function restoreVm(params: {
snapshotBytes: Uint8Array;
config: CodeModeRuntimeConfig;
pendingRequests: CodeModePendingBridgeRequest[];
}): Promise<VmRun> {
const startedAt = Date.now();
let timedOut = false;
const snapshot = QuickJS.deserializeSnapshot(params.snapshotBytes);
const vm = await QuickJS.restore(snapshot, {
wasm: await getQuickJsWasmModule(),
memoryLimit: params.config.memoryLimitBytes,
intrinsics: Intrinsics.ALL,
timezoneOffset: 0,
interruptHandler: () => {
timedOut = Date.now() - startedAt > params.config.timeoutMs;
return timedOut;
},
});
vm.registerHostCallback(
"__openclawHostRequest",
createHostRequestHandler({
vm,
pendingRequests: params.pendingRequests,
config: params.config,
}),
);
return { vm, didTimeout: () => timedOut };
}
function takeOutput(vm: QuickJS): unknown[] {
const take = vm.global.getProp("__openclawTakeOutput");
try {
const output = vm.callFunction(take, vm.undefined);
try {
const dumped = vm.dump(output);
return Array.isArray(dumped) ? (dumped as unknown[]) : [];
} finally {
output.dispose();
}
} finally {
take.dispose();
}
}
function takeOutputSafely(vm: QuickJS): unknown[] {
try {
return takeOutput(vm);
} catch {
return [];
}
}
function throwWorkerFailureWithOutput(params: {
error: unknown;
didTimeout: () => boolean;
output: unknown[];
vm: QuickJS;
}): never {
const timedOut = params.didTimeout() || isQuickJsInterruptedError(params.error);
const failureOutput = params.output.length > 0 ? params.output : takeOutputSafely(params.vm);
if (timedOut) {
throw new CodeModeWorkerFailureWithOutput(
"timeout",
"code mode timeout exceeded",
failureOutput,
{ cause: params.error },
);
}
if (params.error instanceof CodeModeWorkerFailure) {
throw new CodeModeWorkerFailureWithOutput(
params.error.code,
params.error.message,
failureOutput,
{ cause: params.error },
);
}
if (failureOutput.length > 0) {
throw new CodeModeWorkerFailureWithOutput(
"internal_error",
errorMessage(params.error),
failureOutput,
{ cause: params.error },
);
}
throw params.error;
}
function drainPendingJobs(vm: QuickJS): void {
for (let index = 0; index < 1000; index += 1) {
if (vm.executePendingJobs() === 0) {
return;
}
}
throw new Error("code mode pending job limit exceeded");
}
function getResultHandle(vm: QuickJS): JSValueHandle {
return vm.global.getProp("__openclawResult");
}
async function readCompletedResult(vm: QuickJS, resultHandle: JSValueHandle): Promise<unknown> {
if (!resultHandle.isPromise) {
return toJsonSafe(vm.dump(resultHandle));
}
const settled = await vm.resolvePromise(resultHandle);
if ("error" in settled) {
try {
throw new CodeModeGuestError(errorMessage(vm.dump(settled.error)));
} finally {
settled.error.dispose();
}
}
try {
return toJsonSafe(vm.dump(settled.value));
} finally {
settled.value.dispose();
}
}
function waitingResult(params: {
vm: QuickJS;
pendingRequests: CodeModePendingBridgeRequest[];
output: unknown[];
config: CodeModeRuntimeConfig;
}): CodeModeWorkerResult {
const snapshotBytes = QuickJS.serializeSnapshot(params.vm.snapshot());
if (snapshotBytes.byteLength > params.config.maxSnapshotBytes) {
throw new CodeModeWorkerFailure("snapshot_limit_exceeded", "code mode snapshot limit exceeded");
}
return {
status: "waiting",
snapshotBytes,
pendingRequests: params.pendingRequests,
output: params.output,
};
}
async function runExec(input: Extract<CodeModeWorkerInput, { kind: "exec" }>) {
const pendingRequests: CodeModePendingBridgeRequest[] = [];
const { vm, didTimeout } = await createVm({
catalog: input.catalog,
config: input.config,
pendingRequests,
});
let output: unknown[] = [];
try {
vm.evalCode(
buildUserSource(input.source),
"openclaw-code-mode:user.js",
EvalFlags.ASYNC,
).dispose();
drainPendingJobs(vm);
output = takeOutput(vm);
const resultHandle = getResultHandle(vm);
try {
if (pendingRequests.length > 0) {
return waitingResult({ vm, pendingRequests, output, config: input.config });
}
if (resultHandle.isPromise && resultHandle.promiseState === 0) {
throw new Error("code mode promise is pending without host work");
}
return {
status: "completed" as const,
value: await readCompletedResult(vm, resultHandle),
output,
};
} finally {
resultHandle.dispose();
}
} catch (error) {
return throwWorkerFailureWithOutput({ error, didTimeout, output, vm });
} finally {
vm.dispose();
}
}
async function runResume(input: Extract<CodeModeWorkerInput, { kind: "resume" }>) {
const pendingRequests: CodeModePendingBridgeRequest[] = [];
const { vm, didTimeout } = await restoreVm({
snapshotBytes: input.snapshotBytes,
config: input.config,
pendingRequests,
});
let output: unknown[] = [];
try {
const settle = vm.global.getProp("__openclawSettleBridge");
try {
for (const request of input.settledRequests) {
const id = vm.newString(request.id);
const payload = vm.newString(JSON.stringify(request.ok ? request.value : request.error));
try {
vm.callFunction(
settle,
vm.undefined,
id,
request.ok ? vm.true : vm.false,
payload,
).dispose();
} finally {
id.dispose();
payload.dispose();
}
}
} finally {
settle.dispose();
}
drainPendingJobs(vm);
output = takeOutput(vm);
const resultHandle = getResultHandle(vm);
try {
if (pendingRequests.length > 0) {
return waitingResult({ vm, pendingRequests, output, config: input.config });
}
if (resultHandle.isPromise && resultHandle.promiseState === 0) {
throw new Error("code mode promise is pending without host work");
}
return {
status: "completed" as const,
value: await readCompletedResult(vm, resultHandle),
output,
};
} finally {
resultHandle.dispose();
}
} catch (error) {
return throwWorkerFailureWithOutput({ error, didTimeout, output, vm });
} finally {
vm.dispose();
}
}
async function main(): Promise<CodeModeWorkerResult> {
const input = workerData as unknown;
if (!isRecord(input) || !isRecord(input.config)) {
return {
status: "failed",
error: "invalid code mode worker input",
code: "invalid_input",
output: [],
};
}
try {
if (input.kind === "exec" && typeof input.source === "string") {
return await runExec({
kind: "exec",
source: input.source,
config: input.config as CodeModeRuntimeConfig,
catalog: Array.isArray(input.catalog) ? input.catalog : [],
});
}
if (input.kind === "resume" && input.snapshotBytes instanceof Uint8Array) {
return await runResume({
kind: "resume",
snapshotBytes: input.snapshotBytes,
config: input.config as CodeModeRuntimeConfig,
settledRequests: Array.isArray(input.settledRequests)
? (input.settledRequests as CodeModeSettledBridgeRequest[])
: [],
});
}
return {
status: "failed",
error: "invalid code mode worker input",
code: "invalid_input",
output: [],
};
} catch (error) {
return {
status: "failed",
error: errorMessage(error),
code: error instanceof CodeModeWorkerFailure ? error.code : "internal_error",
output: error instanceof CodeModeWorkerFailureWithOutput ? error.output : [],
};
}
}
// oxlint-disable-next-line unicorn/require-post-message-target-origin -- Node worker_threads MessagePort, not window.postMessage.
parentPort?.postMessage(await main());

View File

@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist"
},
"include": ["src/**/*"]
}

6
pnpm-lock.yaml generated
View File

@@ -1788,6 +1788,12 @@ importers:
specifier: 2.9.0
version: 2.9.0
packages/code-mode-runtime:
dependencies:
quickjs-wasi:
specifier: 3.0.0
version: 3.0.0
packages/gateway-client:
dependencies:
'@openclaw/gateway-protocol':

View File

@@ -8,6 +8,7 @@ const RUN_NODE_PACKAGE_SOURCE_ROOTS = [
// Root runtime code imports these package sources through tsconfig aliases,
// while pnpm dev/watch still runs the root dist entrypoint. Treat them like
// src/ so edits restart the same process that consumes them.
"packages/code-mode-runtime/src",
"packages/gateway-client/src",
"packages/gateway-protocol/src",
"packages/markdown-core/src",

View File

@@ -1,3 +1,4 @@
import { resolveCodeModeRuntimeWorkerUrl } from "@openclaw/code-mode-runtime";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { setPluginToolMeta } from "../plugins/tools.js";
import {
@@ -182,6 +183,13 @@ describe("Code Mode", () => {
);
});
it("resolves the package worker URL in the runtime package dist", () => {
expect(
resolveCodeModeRuntimeWorkerUrl("file:///repo/dist/packages/code-mode-runtime/index.js")
.pathname,
).toBe("/repo/dist/packages/code-mode-runtime/worker.js");
});
it("hides all normal tools behind exec and wait", () => {
const { config, catalogRef, tools: codeModeTools } = createCodeModeHarness();
const shellExec = fakeTool("exec", "Run shell command");

View File

@@ -2,6 +2,12 @@ import { randomUUID } from "node:crypto";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { Worker } from "node:worker_threads";
import type {
CodeModeFailureCode,
CodeModePendingBridgeRequest as PendingBridgeRequest,
CodeModeSettledBridgeRequest as SettledBridgeRequest,
CodeModeWorkerResult,
} from "@openclaw/code-mode-runtime";
import { Type } from "typebox";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { isRecord } from "../shared/record-coerce.js";
@@ -68,21 +74,6 @@ export type CodeModeConfig = {
maxSearchLimit: number;
};
type CodeModeBridgeMethod = "search" | "describe" | "call" | "yield";
type PendingBridgeRequest = {
id: string;
method: CodeModeBridgeMethod;
args: unknown[];
};
type SettledBridgeRequest = {
id: string;
ok: boolean;
value?: unknown;
error?: string;
};
type PendingBridgeState = PendingBridgeRequest & {
promise: Promise<SettledBridgeRequest>;
settled?: SettledBridgeRequest;
@@ -103,33 +94,6 @@ type CodeModeRunState = {
type CodeModeToolContext = ToolSearchToolContext;
type CodeModeFailureCode =
| "invalid_input"
| "runtime_unavailable"
| "timeout"
| "output_limit_exceeded"
| "snapshot_limit_exceeded"
| "internal_error";
type CodeModeWorkerResult =
| {
status: "completed";
value: unknown;
output: unknown[];
}
| {
status: "waiting";
snapshotBytes: Uint8Array;
pendingRequests: PendingBridgeRequest[];
output: unknown[];
}
| {
status: "failed";
error: string;
code: CodeModeFailureCode;
output: unknown[];
};
const activeRuns = new Map<string, CodeModeRunState>();
const resumingRunIds = new Set<string>();
let typescriptRuntimePromise: Promise<typeof import("typescript")> | null = null;

View File

@@ -1,608 +1 @@
import { randomUUID } from "node:crypto";
import { readFile } from "node:fs/promises";
import { createRequire } from "node:module";
import { parentPort, workerData } from "node:worker_threads";
import { EvalFlags, Intrinsics, JSException, QuickJS, type JSValueHandle } from "quickjs-wasi";
const require = createRequire(import.meta.url);
const QUICKJS_WASM_PATH = require.resolve("quickjs-wasi/quickjs.wasm");
let quickJsWasmModulePromise: Promise<WebAssembly.Module> | undefined;
type CodeModeBridgeMethod = "search" | "describe" | "call" | "yield";
type CodeModeConfig = {
timeoutMs: number;
memoryLimitBytes: number;
maxPendingToolCalls: number;
maxSnapshotBytes: number;
};
type PendingBridgeRequest = {
id: string;
method: CodeModeBridgeMethod;
args: unknown[];
};
type SettledBridgeRequest = {
id: string;
ok: boolean;
value?: unknown;
error?: string;
};
type CodeModeWorkerInput =
| {
kind: "exec";
source: string;
config: CodeModeConfig;
catalog: unknown[];
}
| {
kind: "resume";
snapshotBytes: Uint8Array;
config: CodeModeConfig;
settledRequests: SettledBridgeRequest[];
};
type CodeModeWorkerResult =
| {
status: "completed";
value: unknown;
output: unknown[];
}
| {
status: "waiting";
snapshotBytes: Uint8Array;
pendingRequests: PendingBridgeRequest[];
output: unknown[];
}
| {
status: "failed";
error: string;
code:
| "invalid_input"
| "runtime_unavailable"
| "timeout"
| "snapshot_limit_exceeded"
| "internal_error";
output: unknown[];
};
class CodeModeWorkerFailure extends Error {
readonly code: Extract<CodeModeWorkerResult, { status: "failed" }>["code"];
constructor(
code: Extract<CodeModeWorkerResult, { status: "failed" }>["code"],
message: string,
options?: ErrorOptions,
) {
super(message, options);
this.name = "CodeModeWorkerFailure";
this.code = code;
}
}
class CodeModeWorkerFailureWithOutput extends CodeModeWorkerFailure {
readonly output: unknown[];
constructor(
code: Extract<CodeModeWorkerResult, { status: "failed" }>["code"],
message: string,
output: unknown[],
options?: ErrorOptions,
) {
super(code, message, options);
this.name = "CodeModeWorkerFailureWithOutput";
this.output = output;
}
}
class CodeModeGuestError extends Error {
constructor(message: string) {
super(message);
this.name = "CodeModeGuestError";
}
}
function isQuickJsInterruptedError(error: unknown): boolean {
if (error instanceof CodeModeGuestError) {
return false;
}
return errorMessage(error) === "interrupted";
}
type VmRun = {
vm: QuickJS;
didTimeout: () => boolean;
};
function getQuickJsWasmModule(): Promise<WebAssembly.Module> {
quickJsWasmModulePromise ??= readFile(QUICKJS_WASM_PATH).then((bytes) =>
WebAssembly.compile(bytes),
);
return quickJsWasmModulePromise;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
function toJsonSafe(value: unknown): unknown {
if (value === undefined) {
return null;
}
try {
return JSON.parse(JSON.stringify(value)) as unknown;
} catch {
if (value instanceof Error) {
return { name: value.name, message: value.message };
}
if (value === null) {
return null;
}
switch (typeof value) {
case "string":
case "number":
case "boolean":
return value;
case "bigint":
case "symbol":
case "function":
return String(value);
default:
return Object.prototype.toString.call(value);
}
}
}
function errorMessage(error: unknown): string {
if (error instanceof JSException) {
return error.stack || error.message || String(error);
}
if (error instanceof Error) {
return error.message || String(error);
}
return String(error);
}
const CONTROLLER_SOURCE = String.raw`
(() => {
const output = [];
const pending = new Map();
const catalog = Array.isArray(globalThis.__openclawCatalog) ? globalThis.__openclawCatalog : [];
function safe(value) {
if (value === undefined) return null;
try {
return JSON.parse(JSON.stringify(value));
} catch {
if (value instanceof Error) {
return { name: value.name, message: value.message };
}
if (value === null) return null;
const type = typeof value;
if (type === "string" || type === "number" || type === "boolean") return value;
return String(value);
}
}
function asText(value) {
if (typeof value === "string") return value;
const encoded = JSON.stringify(safe(value));
return typeof encoded === "string" ? encoded : String(value);
}
function request(method, args) {
const id = String(globalThis.__openclawHostRequest(String(method), JSON.stringify(safe(args ?? []))));
return new Promise((resolve, reject) => {
pending.set(id, { resolve, reject });
});
}
function settle(id, ok, payload) {
const entry = pending.get(String(id));
if (!entry) return false;
pending.delete(String(id));
let parsed = null;
try {
parsed = JSON.parse(String(payload));
} catch {
parsed = String(payload);
}
if (ok) {
entry.resolve(parsed);
} else {
const error = new Error(typeof parsed === "string" ? parsed : parsed?.message ?? "nested tool failed");
entry.reject(error);
}
return true;
}
const baseTools = Object.create(null);
Object.defineProperties(baseTools, {
search: { value: (query, options) => request("search", [query, options]), enumerable: true },
describe: { value: (id) => request("describe", [id]), enumerable: true },
call: { value: (id, input) => request("call", [id, input]), enumerable: true },
});
const safeNameCounts = new Map();
for (const tool of catalog) {
const name = typeof tool?.name === "string" ? tool.name : "";
if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name)) continue;
safeNameCounts.set(name, (safeNameCounts.get(name) ?? 0) + 1);
}
for (const tool of catalog) {
const name = typeof tool?.name === "string" ? tool.name : "";
const id = typeof tool?.id === "string" ? tool.id : "";
if (!id || safeNameCounts.get(name) !== 1 || Object.prototype.hasOwnProperty.call(baseTools, name)) {
continue;
}
Object.defineProperty(baseTools, name, {
value: (input) => request("call", [id, input]),
enumerable: true,
});
}
Object.defineProperties(globalThis, {
ALL_TOOLS: { value: Object.freeze(catalog.slice()), enumerable: true },
tools: { value: Object.freeze(baseTools), enumerable: true },
text: { value: (value) => output.push({ type: "text", text: asText(value) }), enumerable: true },
json: { value: (value) => output.push({ type: "json", value: safe(value) }), enumerable: true },
yield_control: { value: (reason) => request("yield", [reason]), enumerable: true },
__openclawSettleBridge: { value: settle },
__openclawTakeOutput: { value: () => output.splice(0) },
});
})();
`;
function buildUserSource(code: string): string {
return `globalThis.__openclawResult = (async () => {\n${code}\n})()`;
}
function createHostRequestHandler(params: {
vm: QuickJS;
pendingRequests: PendingBridgeRequest[];
config: CodeModeConfig;
}): (this: JSValueHandle, method: JSValueHandle, argsJson: JSValueHandle) => JSValueHandle {
return (methodHandle, argsHandle) => {
if (params.pendingRequests.length >= params.config.maxPendingToolCalls) {
throw new Error("too many pending code mode tool calls");
}
const method = methodHandle.toString();
if (method !== "search" && method !== "describe" && method !== "call" && method !== "yield") {
throw new Error("unsupported code mode bridge method");
}
let args: unknown = [];
try {
args = JSON.parse(argsHandle.toString()) as unknown;
} catch {
args = [];
}
const id = `bridge:${params.pendingRequests.length + 1}:${randomUUID()}`;
params.pendingRequests.push({
id,
method,
args: Array.isArray(args) ? args : [],
});
return params.vm.newString(id);
};
}
async function createVm(params: {
catalog: unknown[];
config: CodeModeConfig;
pendingRequests: PendingBridgeRequest[];
}): Promise<VmRun> {
const startedAt = Date.now();
let timedOut = false;
const vm = await QuickJS.create({
wasm: await getQuickJsWasmModule(),
memoryLimit: params.config.memoryLimitBytes,
intrinsics: Intrinsics.ALL,
timezoneOffset: 0,
interruptHandler: () => {
timedOut = Date.now() - startedAt > params.config.timeoutMs;
return timedOut;
},
});
const catalogHandle = vm.hostToHandle(params.catalog);
try {
vm.setProp(vm.global, "__openclawCatalog", catalogHandle);
} finally {
catalogHandle.dispose();
}
const hostRequest = vm.newFunction(
"__openclawHostRequest",
createHostRequestHandler({
vm,
pendingRequests: params.pendingRequests,
config: params.config,
}),
);
try {
vm.setProp(vm.global, "__openclawHostRequest", hostRequest);
} finally {
hostRequest.dispose();
}
vm.evalCode(CONTROLLER_SOURCE, "openclaw-code-mode:controller.js").dispose();
return { vm, didTimeout: () => timedOut };
}
async function restoreVm(params: {
snapshotBytes: Uint8Array;
config: CodeModeConfig;
pendingRequests: PendingBridgeRequest[];
}): Promise<VmRun> {
const startedAt = Date.now();
let timedOut = false;
const snapshot = QuickJS.deserializeSnapshot(params.snapshotBytes);
const vm = await QuickJS.restore(snapshot, {
wasm: await getQuickJsWasmModule(),
memoryLimit: params.config.memoryLimitBytes,
intrinsics: Intrinsics.ALL,
timezoneOffset: 0,
interruptHandler: () => {
timedOut = Date.now() - startedAt > params.config.timeoutMs;
return timedOut;
},
});
vm.registerHostCallback(
"__openclawHostRequest",
createHostRequestHandler({
vm,
pendingRequests: params.pendingRequests,
config: params.config,
}),
);
return { vm, didTimeout: () => timedOut };
}
function takeOutput(vm: QuickJS): unknown[] {
const take = vm.global.getProp("__openclawTakeOutput");
try {
const output = vm.callFunction(take, vm.undefined);
try {
const dumped = vm.dump(output);
return Array.isArray(dumped) ? (dumped as unknown[]) : [];
} finally {
output.dispose();
}
} finally {
take.dispose();
}
}
function takeOutputSafely(vm: QuickJS): unknown[] {
try {
return takeOutput(vm);
} catch {
return [];
}
}
function throwWorkerFailureWithOutput(params: {
error: unknown;
didTimeout: () => boolean;
output: unknown[];
vm: QuickJS;
}): never {
const timedOut = params.didTimeout() || isQuickJsInterruptedError(params.error);
const failureOutput = params.output.length > 0 ? params.output : takeOutputSafely(params.vm);
if (timedOut) {
throw new CodeModeWorkerFailureWithOutput(
"timeout",
"code mode timeout exceeded",
failureOutput,
{ cause: params.error },
);
}
if (params.error instanceof CodeModeWorkerFailure) {
throw new CodeModeWorkerFailureWithOutput(
params.error.code,
params.error.message,
failureOutput,
{ cause: params.error },
);
}
if (failureOutput.length > 0) {
throw new CodeModeWorkerFailureWithOutput(
"internal_error",
errorMessage(params.error),
failureOutput,
{ cause: params.error },
);
}
throw params.error;
}
function drainPendingJobs(vm: QuickJS): void {
for (let index = 0; index < 1000; index += 1) {
if (vm.executePendingJobs() === 0) {
return;
}
}
throw new Error("code mode pending job limit exceeded");
}
function getResultHandle(vm: QuickJS): JSValueHandle {
return vm.global.getProp("__openclawResult");
}
async function readCompletedResult(vm: QuickJS, resultHandle: JSValueHandle): Promise<unknown> {
if (!resultHandle.isPromise) {
return toJsonSafe(vm.dump(resultHandle));
}
const settled = await vm.resolvePromise(resultHandle);
if ("error" in settled) {
try {
throw new CodeModeGuestError(errorMessage(vm.dump(settled.error)));
} finally {
settled.error.dispose();
}
}
try {
return toJsonSafe(vm.dump(settled.value));
} finally {
settled.value.dispose();
}
}
function waitingResult(params: {
vm: QuickJS;
pendingRequests: PendingBridgeRequest[];
output: unknown[];
config: CodeModeConfig;
}): CodeModeWorkerResult {
const snapshotBytes = QuickJS.serializeSnapshot(params.vm.snapshot());
if (snapshotBytes.byteLength > params.config.maxSnapshotBytes) {
throw new CodeModeWorkerFailure("snapshot_limit_exceeded", "code mode snapshot limit exceeded");
}
return {
status: "waiting",
snapshotBytes,
pendingRequests: params.pendingRequests,
output: params.output,
};
}
async function runExec(input: Extract<CodeModeWorkerInput, { kind: "exec" }>) {
const pendingRequests: PendingBridgeRequest[] = [];
const { vm, didTimeout } = await createVm({
catalog: input.catalog,
config: input.config,
pendingRequests,
});
let output: unknown[] = [];
try {
vm.evalCode(
buildUserSource(input.source),
"openclaw-code-mode:user.js",
EvalFlags.ASYNC,
).dispose();
drainPendingJobs(vm);
output = takeOutput(vm);
const resultHandle = getResultHandle(vm);
try {
if (pendingRequests.length > 0) {
return waitingResult({ vm, pendingRequests, output, config: input.config });
}
if (resultHandle.isPromise && resultHandle.promiseState === 0) {
throw new Error("code mode promise is pending without host work");
}
return {
status: "completed" as const,
value: await readCompletedResult(vm, resultHandle),
output,
};
} finally {
resultHandle.dispose();
}
} catch (error) {
return throwWorkerFailureWithOutput({ error, didTimeout, output, vm });
} finally {
vm.dispose();
}
}
async function runResume(input: Extract<CodeModeWorkerInput, { kind: "resume" }>) {
const pendingRequests: PendingBridgeRequest[] = [];
const { vm, didTimeout } = await restoreVm({
snapshotBytes: input.snapshotBytes,
config: input.config,
pendingRequests,
});
let output: unknown[] = [];
try {
const settle = vm.global.getProp("__openclawSettleBridge");
try {
for (const request of input.settledRequests) {
const id = vm.newString(request.id);
const payload = vm.newString(JSON.stringify(request.ok ? request.value : request.error));
try {
vm.callFunction(
settle,
vm.undefined,
id,
request.ok ? vm.true : vm.false,
payload,
).dispose();
} finally {
id.dispose();
payload.dispose();
}
}
} finally {
settle.dispose();
}
drainPendingJobs(vm);
output = takeOutput(vm);
const resultHandle = getResultHandle(vm);
try {
if (pendingRequests.length > 0) {
return waitingResult({ vm, pendingRequests, output, config: input.config });
}
if (resultHandle.isPromise && resultHandle.promiseState === 0) {
throw new Error("code mode promise is pending without host work");
}
return {
status: "completed" as const,
value: await readCompletedResult(vm, resultHandle),
output,
};
} finally {
resultHandle.dispose();
}
} catch (error) {
return throwWorkerFailureWithOutput({ error, didTimeout, output, vm });
} finally {
vm.dispose();
}
}
async function main(): Promise<CodeModeWorkerResult> {
const input = workerData as unknown;
if (!isRecord(input) || !isRecord(input.config)) {
return {
status: "failed",
error: "invalid code mode worker input",
code: "invalid_input",
output: [],
};
}
try {
if (input.kind === "exec" && typeof input.source === "string") {
return await runExec({
kind: "exec",
source: input.source,
config: input.config as CodeModeConfig,
catalog: Array.isArray(input.catalog) ? input.catalog : [],
});
}
if (input.kind === "resume" && input.snapshotBytes instanceof Uint8Array) {
return await runResume({
kind: "resume",
snapshotBytes: input.snapshotBytes,
config: input.config as CodeModeConfig,
settledRequests: Array.isArray(input.settledRequests)
? (input.settledRequests as SettledBridgeRequest[])
: [],
});
}
return {
status: "failed",
error: "invalid code mode worker input",
code: "invalid_input",
output: [],
};
} catch (error) {
return {
status: "failed",
error: errorMessage(error),
code: error instanceof CodeModeWorkerFailure ? error.code : "internal_error",
output: error instanceof CodeModeWorkerFailureWithOutput ? error.output : [],
};
}
}
// oxlint-disable-next-line unicorn/require-post-message-target-origin -- Node worker_threads MessagePort, not window.postMessage.
parentPort?.postMessage(await main());
import "../../packages/code-mode-runtime/src/worker.ts";

View File

@@ -130,6 +130,7 @@ describe("watch-node script", () => {
];
expect(watchPaths).toEqual(runNodeWatchedPaths);
expect(watchPaths).toContain("extensions");
expect(watchPaths).toContain("packages/code-mode-runtime/src");
expect(watchPaths).toContain("packages/gateway-client/src");
expect(watchPaths).toContain("packages/gateway-protocol/src");
expect(watchPaths).toContain("packages/markdown-core/src");
@@ -139,6 +140,8 @@ describe("watch-node script", () => {
expect(watchOptions.ignoreInitial).toBe(true);
expect(watchOptions.ignored("src")).toBe(false);
expect(watchOptions.ignored("src/infra")).toBe(false);
expect(watchOptions.ignored("packages/code-mode-runtime/src/worker.ts")).toBe(false);
expect(watchOptions.ignored("packages/code-mode-runtime/src/worker.test.ts")).toBe(true);
expect(watchOptions.ignored("packages/gateway-client/src/client.ts")).toBe(false);
expect(watchOptions.ignored("packages/gateway-client/src/client.test.ts")).toBe(true);
expect(watchOptions.ignored("packages/gateway-protocol/src/schema/cron.ts")).toBe(false);

View File

@@ -8,7 +8,7 @@ import { listGitTrackedFiles, toRepoRelativePath } from "../../src/test-utils/re
const repoRoot = path.resolve(import.meta.dirname, "../..");
const CODE_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
const IGNORED_DIRS = new Set([".cache", ".git", "build", "coverage", "dist", "node_modules"]);
const ROOTS = ["src", "extensions", "scripts", "ui"] as const;
const ROOTS = ["src", "extensions", "packages", "scripts", "ui"] as const;
const SUPPRESSION_PATTERN = /(?:oxlint|eslint)-disable(?:-next-line)?\s+([@/\w-]+)(?:\s+--|$)/u;
type SuppressionEntry = {
@@ -185,11 +185,11 @@ describe("production lint suppressions", () => {
"extensions/telegram/src/telegram-ingress-worker.runtime.ts|unicorn/require-post-message-target-origin|1",
"extensions/telegram/src/telegram-ingress-worker.ts|unicorn/require-post-message-target-origin|1",
"extensions/whatsapp/src/document-filename.ts|no-control-regex|1",
"packages/code-mode-runtime/src/worker.ts|unicorn/require-post-message-target-origin|1",
"scripts/e2e/mcp-channels-harness.ts|unicorn/prefer-add-event-listener|1",
"scripts/lib/extension-package-boundary.ts|typescript/no-unnecessary-type-parameters|1",
"scripts/lib/plugin-npm-release.ts|typescript/no-unnecessary-type-parameters|1",
"src/agents/agent-scope.ts|no-control-regex|1",
"src/agents/code-mode.worker.ts|unicorn/require-post-message-target-origin|1",
"src/agents/embedded-agent-runner/run/images.ts|no-control-regex|1",
"src/agents/subagent-spawn.ts|no-control-regex|1",
"src/channels/plugins/channel-runtime-surface.types.ts|typescript/no-unnecessary-type-parameters|1",

View File

@@ -173,6 +173,14 @@ export const sharedVitestConfig = {
find: "@openclaw/whatsapp/api.js",
replacement: path.join(repoRoot, "extensions", "whatsapp", "api.ts"),
},
{
find: "@openclaw/code-mode-runtime/worker",
replacement: path.join(repoRoot, "packages", "code-mode-runtime", "src", "worker.ts"),
},
{
find: "@openclaw/code-mode-runtime",
replacement: path.join(repoRoot, "packages", "code-mode-runtime", "src", "index.ts"),
},
{
find: "@openclaw/gateway-client/readiness",
replacement: path.join(repoRoot, "packages", "gateway-client", "src", "readiness.ts"),

View File

@@ -27,6 +27,8 @@
"openclaw/extension-api": ["./src/extensionAPI.ts"],
"openclaw/plugin-sdk": ["./src/plugin-sdk/index.ts"],
"openclaw/plugin-sdk/*": ["./src/plugin-sdk/*.ts"],
"@openclaw/code-mode-runtime": ["./packages/code-mode-runtime/src/index.ts"],
"@openclaw/code-mode-runtime/*": ["./packages/code-mode-runtime/src/*"],
"@openclaw/agent-core": ["./packages/agent-core/src/index.ts"],
"@openclaw/agent-core/*": ["./packages/agent-core/src/*"],
"@openclaw/llm-core": ["./packages/llm-core/src/index.ts"],

View File

@@ -372,6 +372,16 @@ function buildGatewayClientDistEntries(): Record<string, string> {
};
}
function buildCodeModeRuntimeDistEntries(): Record<string, string> {
return {
// The root package still ships a stable agents/code-mode.worker.js entry.
// These entries make the extracted runtime package buildable on its own.
index: "packages/code-mode-runtime/src/index.ts",
types: "packages/code-mode-runtime/src/types.ts",
worker: "packages/code-mode-runtime/src/worker.ts",
};
}
function buildNetPolicyDistEntries(): Record<string, string> {
return {
// These subpaths are imported by root runtime code and exported by the
@@ -593,6 +603,12 @@ export default defineConfig([
neverBundle: shouldExternalizeGatewayClientDependency,
},
}),
nodeWorkspacePackageBuildConfig({
clean: true,
dts: RUN_NODE_SKIP_DTS_BUILD ? false : undefined,
entry: buildCodeModeRuntimeDistEntries(),
outDir: "packages/code-mode-runtime/dist",
}),
nodeWorkspacePackageBuildConfig({
clean: true,
dts: RUN_NODE_SKIP_DTS_BUILD ? false : undefined,