210 lines · 9.1 KB · TypeScript
RawDownload
import { Effect } from "effect";import type { SandboxToolInvoker } from "@executor-js/codemode-core";import { makeQuickJsExecutor } from "@executor-js/runtime-quickjs"; import {  WorkflowError,  type DriveOutcome,  type SuspendMarker,  type WorkflowBridge,  type WorkflowBridgeResult,  type WorkflowDriver,} from "../seams/workflow-runner";import type { ArtifactStore, SnapshotId } from "../seams/artifact-store";import { bundleEntry } from "../pipeline/bundle"; // ---------------------------------------------------------------------------// QuickJS-backed WorkflowDriver (self-hosted).//// The author's `defineWorkflow({ run(step, { db }) })` body runs INSIDE QuickJS// on every replay (the same isolation the tool handlers use), NOT in-process.// Each `step.*` / `db.sql` call crosses the single `__wf` bridge to the host,// which services it against the journal (`WorkflowBridge`). This is what makes// the orchestrator substrate-neutral: everything crossing the boundary is JSON,// so the cloud backing can be an RPC.//// The shim's `__runWorkflow()` returns a STRUCTURED envelope, never throws a// string-matched sentinel://   { status: "completed", output }//   { status: "suspended", marker: { suspend: "sleep" | "event", event? } }//   { status: "failed", retryable, message, retryAfter? }// so the runner reads typed fields (retryable-vs-fatal is a discriminator, the// suspend marker is structured), never `error.includes("...")`.// --------------------------------------------------------------------------- const WORKFLOW_TIMEOUT_MS = 60_000; /** True if a host bridge result is a structured suspend (vs a plain value). */const isSuspend = (r: WorkflowBridgeResult): r is SuspendMarker => "suspend" in r; // The workflow shim: a `step` facade + a `db` client, both routing through the// `__wf` bridge, plus `__runWorkflow` that drives the authored body and returns// the structured envelope. `globalThis.__artifact` is set by the compiled// bundle (the workflow def). Kept as a plain string (QuickJS evals a string).const workflowShim = (input: unknown): string => `var __wfInput = ${JSON.stringify(input ?? {})};var __wf = function(op) { return tools.__wf(op); };// A tagged suspension we throw to unwind the body; caught by __runWorkflow and// turned into a structured { status: "suspended" } (no string-matching).function __Suspend(marker) { this.__wfSuspend = marker; }var __await = async function(op) {  var res = await __wf(op);  if (res && res.suspend) throw new __Suspend({ suspend: res.suspend, event: res.event });  if (res && res.error) {    // A structured step error (e.g. step.tool's bound tool threw). Re-throw with    // the typed retryable discriminator so __runWorkflow classifies the run.    var err = new Error(res.error.message);    err.retryable = res.error.retryable === true;    if (typeof res.error.retryAfter === "number") err.retryAfter = res.error.retryAfter;    throw err;  }  return res.value;};var step = {  do: async function(name, fn) {    var j = await __wf({ kind: "step.check", step: name });    if (j && j.value && j.value.journaled) return j.value.output;    var value = await fn();    await __wf({ kind: "step.record", step: name, value: value === undefined ? null : value });    return value;  },  tool: async function(address, args) {    return __await({ kind: "step.tool", step: "tool:" + address, address: address, args: args || {} });  },  sleep: async function(name, ms) { await __await({ kind: "step.sleep", step: "sleep:" + name, ms: ms }); },  waitForEvent: async function(name, opts) {    return __await({ kind: "step.waitForEvent", step: "wait:" + name });  },  notify: async function(msg) { await __wf({ kind: "step.notify", msg: msg }); },};var __db = {  sql: function(strings) {    var values = Array.prototype.slice.call(arguments, 1);    var sql = "";    for (var i = 0; i < strings.length; i++) { sql += strings[i]; if (i < values.length) sql += "?"; }    return __wf({ kind: "db.sql", sql: sql, params: values }).then(function(res){ return res.value; });  },};globalThis.__runWorkflow = async function() {  var def = globalThis.__artifact && (globalThis.__artifact.default || globalThis.__artifact);  if (!def || typeof def.run !== "function") {    return { status: "failed", retryable: false, message: "workflow bundle did not produce a run() function" };  }  try {    var output = await def.run(step, { db: __db, input: __wfInput });    return { status: "completed", output: output === undefined ? null : output };  } catch (e) {    if (e && e.__wfSuspend) return { status: "suspended", marker: e.__wfSuspend };    // A typed control error from the author: RetryableError / FatalError carry a    // discriminator the runtime reads. Anything else is fatal.    var retryable = !!(e && (e.retryable === true || e.name === "RetryableError" || (e.constructor && e.constructor.name === "RetryableError")));    var retryAfter = e && typeof e.retryAfter === "number" ? e.retryAfter : undefined;    var message = e && e.message ? String(e.message) : String(e);    return { status: "failed", retryable: retryable, message: message, retryAfter: retryAfter };  }};`; // Wrap the CJS bundle so `require` resolves the platform module shims and the// def lands on `globalThis.__artifact`. Mirrors the tool sandbox's wrapper.const workflowPrelude = `var __executorApp = {  connection: function(integration, opts) { return { __decl: 'single', integration: integration, description: opts && opts.description }; },  connections: function(integration, opts) { return { __decl: 'array', integration: integration, description: opts && opts.description }; },  catalog: function() { return { __decl: 'catalog' }; },  defineTool: function(def) { return def; },  defineWorkflow: function(def) { globalThis.__artifact = def; return def; },};var __executorUi = { config: function(){}, useQuery: function(){ return {}; }, useTool: function(){ return {}; } };function __require(id) {  if (id === 'executor:app') return __executorApp;  if (id === 'executor:ui') return __executorUi;  if (id === 'executor:ui/components') return {};  throw new Error('module not available in sandbox: ' + id);}`; const wrapBundle = (bundle: string): string => `var module = { exports: {} };var exports = module.exports;var require = __require;(function(module, exports, require){${bundle}})(module, exports, require);`; const buildWorkflowCode = (bundle: string, input: unknown): string =>  workflowPrelude + wrapBundle(bundle) + workflowShim(input) + "\nreturn await __runWorkflow();"; export interface QuickjsWorkflowDriverDeps {  readonly artifactStore: ArtifactStore;  readonly timeoutMs?: number;} export const makeQuickjsWorkflowDriver = (deps: QuickjsWorkflowDriverDeps): WorkflowDriver => {  const executor = makeQuickJsExecutor({ timeoutMs: deps.timeoutMs ?? WORKFLOW_TIMEOUT_MS });   // Re-bundle the workflow entry from the pinned snapshot on each drive; the  // runner never caches source that could drift. A published snapshot is  // immutable, so a per-(snapshot,entry) cache would be safe, but the source of  // truth is always the committed snapshot.  const loadBundle = (    scope: string,    snapshotId: string,    entryPath: string,  ): Effect.Effect<string, WorkflowError> =>    Effect.gen(function* () {      const scopeStore = yield* deps.artifactStore        .forScope(scope)        .pipe(Effect.mapError((c) => new WorkflowError({ message: c.message, cause: c })));      const files = yield* scopeStore        .read(snapshotId as SnapshotId)        .pipe(Effect.mapError((c) => new WorkflowError({ message: c.message, cause: c })));      const bundle = yield* bundleEntry({ files, entry: entryPath }).pipe(        Effect.mapError((c) => new WorkflowError({ message: c.message, cause: c })),      );      return bundle.code;    });   return {    drive: (input, bridge: WorkflowBridge) =>      Effect.gen(function* () {        const code = yield* loadBundle(input.scope, input.snapshotId, input.entryPath);         // The invoker decodes the routed `__wf` op and forwards to the host        // bridge. Everything crossing is JSON (the cloud version is RPC).        const invoker: SandboxToolInvoker = {          invoke: (call: { path: string; args: unknown }) => {            if (call.path !== "__wf") {              return Effect.fail(                new WorkflowError({                  message: `unexpected workflow sandbox call: ${call.path}`,                }),              ) as never;            }            return bridge.call(call.args as never) as never;          },        };         const result = yield* executor          .execute(buildWorkflowCode(code, input.input), invoker)          .pipe(            Effect.mapError(              (cause) => new WorkflowError({ message: "workflow drive failed", cause }),            ),          );         if (result.error) {          // A sandbox-level error (timeout, syntax) is fatal for the run.          return {            status: "failed",            retryable: false,            message: result.error,          } satisfies DriveOutcome;        }        return result.result as DriveOutcome;      }),  };}; export { isSuspend };