Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/coding-agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"@step-harness/providers": "^0.84.4",
"@step-harness/pi-tui": "^0.84.4",
"@step-harness/config": "workspace:*",
"@jitl/quickjs-singlefile-mjs-release-sync": "0.32.0",
"@modelcontextprotocol/sdk": "1.27.1",
"@silvia-odwyer/photon-node": "0.3.4",
"chalk": "5.6.2",
Expand All @@ -51,6 +52,7 @@
"jiti": "2.7.0",
"minimatch": "10.2.5",
"proper-lockfile": "4.1.2",
"quickjs-emscripten-core": "0.32.0",
"semver": "7.8.0",
"smol-toml": "1.8.0",
"typebox": "1.3.7",
Expand Down
8 changes: 7 additions & 1 deletion packages/coding-agent/src/features/workflow/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,12 @@ export {
workflowHash,
} from "./journal.ts";
export { formatWorkflowStatus, listSavedWorkflows, listWorkflowRuns, WorkflowProgressStore } from "./progress.ts";
export { WorkflowRuntime, WorkflowSchemaError, workflowToolResult } from "./runtime.ts";
export {
defaultWorkflowVmExecutor,
WorkflowRuntime,
WorkflowSchemaError,
workflowToolResult,
} from "./runtime.ts";
export { validateWorkflowSchema } from "./schema.ts";
export {
createStepWorkflowExtension,
Expand All @@ -49,3 +54,4 @@ export {
} from "./tool-profile.ts";
export type * from "./types.ts";
export { isIsolatedVmAvailable, loadIsolatedVm, runInIsolatedVm, WORKFLOW_MAX_SCRIPT_BYTES } from "./vm.ts";
export { isQuickJsVmAvailable, runInQuickJs } from "./vm-quickjs.ts";
18 changes: 12 additions & 6 deletions packages/coding-agent/src/features/workflow/registration-gate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ function envFlag(value: string | undefined): boolean {

export type WorkflowRegistrationDecision =
| { enabled: true }
| { enabled: false; reason: "not-enabled" | "disabled-by-env" | "vm-unavailable" | "vm-unsupported-runtime" };
| { enabled: false; reason: "not-enabled" | "disabled-by-env" | "vm-unavailable" };

/**
* Startup warning for the one refusal that contradicts the default-on
* registration: every other reason honors an explicit "off" or an unfixable
* runtime fact and stays silent.
* registration: every other reason honors an explicit "off" or a runtime that
* has a working executor anyway.
*/
export const WORKFLOW_VM_UNAVAILABLE_WARNING =
"Workflow tools are unavailable this session: the isolated-vm native module failed to load. Rebuild or reinstall isolated-vm to restore the workflow tool, /workflows, and /ultraloop, or set STEP_DISABLE_WORKFLOW=1 to silence this warning.";
Expand All @@ -31,6 +31,12 @@ export const WORKFLOW_VM_UNAVAILABLE_WARNING =
* the ultraloop opt-in. An embedder's `enabled: false` or
* STEP_DISABLE_WORKFLOW=1 turns registration off; STEP_ENABLE_WORKFLOW is no
* longer read.
*
* A runtime that cannot host isolated-vm is not a refusal. The V8-native addon
* never loads on the shipped executable's JavaScriptCore engine, so that host
* runs workflows through the bundled QuickJS WebAssembly executor instead (see
* `vm-quickjs.ts`) and registers normally. Only a V8 host whose addon failed to
* load is a real, fixable gap — that one warns.
*/
export function resolveWorkflowRegistration(
options: { enabled?: boolean; vmExecutor?: unknown } = {},
Expand All @@ -40,9 +46,9 @@ export function resolveWorkflowRegistration(
if (envFlag(process.env.STEP_DISABLE_WORKFLOW)) return { enabled: false, reason: "disabled-by-env" };
if (options.enabled === false) return { enabled: false, reason: "not-enabled" };
if (!vmAvailable && !options.vmExecutor) {
// A missing native module is fixable on a V8 runtime (warn so the user can
// reinstall it); on a non-V8 runtime it never loads, so refuse silently.
return { enabled: false, reason: vmHostable ? "vm-unavailable" : "vm-unsupported-runtime" };
// Non-V8 host: QuickJS stands in for isolated-vm, so workflows are available.
if (!vmHostable) return { enabled: true };
return { enabled: false, reason: "vm-unavailable" };
}
return { enabled: true };
}
Expand Down
22 changes: 20 additions & 2 deletions packages/coding-agent/src/features/workflow/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,32 @@ import type {
WorkflowUsage,
} from "./types.ts";
import { emptyWorkflowUsage, mergeWorkflowUsage, workflowUsageTokens } from "./types.ts";
import { runInIsolatedVm, type WorkflowVmHost, type WorkflowVmOptions, type WorkflowVmResult } from "./vm.ts";
import {
isIsolatedVmAvailable,
runInIsolatedVm,
type WorkflowVmHost,
type WorkflowVmOptions,
type WorkflowVmResult,
} from "./vm.ts";
import { runInQuickJs } from "./vm-quickjs.ts";

const DEFAULT_MAX_ITERATIONS = 20;
const MAX_MAX_ITERATIONS = 100;
const DEFAULT_STAGNATION_LIMIT = 3;
const DEFAULT_AGENT_TIMEOUT_MS = 30 * 60 * 1_000;
const MAX_AGENT_TIMEOUT_MS = 60 * 60 * 1_000;

/**
* Pick the sandbox for this host. isolated-vm is a V8-native addon, so it is
* absent on the shipped executable's JavaScriptCore engine; QuickJS compiled to
* WebAssembly runs anywhere and stands in there. A V8 host with the addon
* installed keeps using it, so nothing changes for a source/Node run. An
* explicit `vmExecutor` still wins over both.
*/
export function defaultWorkflowVmExecutor(): NonNullable<WorkflowRuntimeOptions["vmExecutor"]> {
return isIsolatedVmAvailable() ? runInIsolatedVm : runInQuickJs;
}

export interface WorkflowRuntimeOptions {
cwd: string;
runId: string;
Expand Down Expand Up @@ -141,7 +159,7 @@ export class WorkflowRuntime {
this.signal = options.signal;
this.nestedWorkflow = options.nestedWorkflow;
this.now = options.now ?? Date.now;
this.vmExecutor = options.vmExecutor ?? runInIsolatedVm;
this.vmExecutor = options.vmExecutor ?? defaultWorkflowVmExecutor();
const startedAt = this.readNow();
this.startedAt = startedAt;
const initial: WorkflowProgress = {
Expand Down
Loading
Loading