Skip to main content
interface EnvironmentSetupContext extends ModuleSetupContext {
  bindings: EnvironmentBindings;
}

interface EnvironmentModule extends Module {
  setup(context: EnvironmentSetupContext): Promise<void>;
  execute(input: ExecutionInput): Promise<ExecutionExitState>;
  kill(eid: number): Promise<void>;
  generateDocs(): Promise<string>;
  generateToolDocs(input: ToolDocsInput): Promise<string>;
}
Environment modules execute submitted process code in a runtime they own. See Environment Modules for the user-facing description.

Setup

setup({ config, secrets, bindings }) is called when the environment is activated. config and secrets carry the module-level settings from the modules database row. bindings is the host’s callback surface, store it and use it to report execution state, capture I/O, and forward tool discovery / invocation requests. The host activates at most one environment at a time. A previously active environment is moved to draining state and torn down only after its in-flight executions finish.

execute(input)

interface ExecutionInput {
  eid: number;
  code: string;
  options?: ExecutionOptions;
}

interface ExecutionOptions {
  timeoutMs: number;
}
Run one process. input.eid is the host-assigned execution id, it matches the process pid exactly and must be the value passed to every bindings.* callback so the host can route stdout, stderr, output, and state changes to the right process record. Return one of "failed" | "success" | "timeout" | "canceled" when execution finishes. The host treats this as the final word; once execute resolves, the process record transitions to idle with this value as its exitState. Inside execute, the environment is expected to:
  1. Call bindings.setState(eid, "queued") if not already running, then bindings.setState(eid, "running") when the code starts.
  2. Call bindings.emitStdout(eid, buf) / emitStderr(eid, buf) for any captured text output.
  3. Call bindings.emitOutput(eid, obj) whenever user code emits a structured output payload.
  4. Call bindings.setError(eid, message) if execution fails.
  5. Resolve the promise with the final ExecutionExitState.
Throwing from execute is also acceptable, the host wraps it as "failed" and records the message as the process error.

kill(eid)

Interrupt a queued or running execution. The corresponding execute promise should resolve as "canceled" (or close to it) shortly after kill returns. Should be safe to call for an unknown eid (return without error).

generateDocs()

Return a Markdown string describing the runtime, globals available inside, idioms, examples. Served at GET /environment/docs. This is what AI clients read to learn how to write code for this environment, so be specific. The bundled typescript-ivm documents its cyrnel.* surface; a custom environment that exposes a different shape should document the shape it exposes.

generateToolDocs(input)

interface ToolDocsInput {
  serviceId: string;
  toolId: string;
  description: string;
  inputSchema: JSONSchema;
  outputSchema: JSONSchema;
}
Return Markdown documenting a single tool as it should be invoked inside this environment. Served at GET /tools/:serviceId/:toolId/docs. If the environment exposes tools as cyrnel.services[s].tools[t].invoke(p), render an example using that. If it exposes them as Python functions, do that instead.

Draining

When the host activates a different environment, the previous one is not torn down immediately. It moves to a draining state:
  • The host no longer dispatches new execute calls to it.
  • In-flight executions continue.
  • The host calls kill(eid) only when a process is explicitly killed.
  • Once execute has resolved for every in-flight eid, the host calls teardown().
Implementations should be safe to call after the active reference has moved on, there is a brief overlap when both environments exist.

Minimal Skeleton

import type {
  EnvironmentBindings,
  EnvironmentModule,
  EnvironmentSetupContext,
  ExecutionExitState,
  ExecutionInput,
  ToolDocsInput,
} from "@cyrnel/sdk";

class MyEnvironment implements EnvironmentModule {
  private bindings!: EnvironmentBindings;

  async setup({ bindings }: EnvironmentSetupContext) {
    this.bindings = bindings;
  }

  async teardown() {}

  async execute(input: ExecutionInput): Promise<ExecutionExitState> {
    this.bindings.setState(input.eid, "running");
    try {
      // ...execute input.code in your runtime, emitting stdout/stderr/output
      return "success";
    } catch (err) {
      this.bindings.setError(input.eid, String(err));
      return "failed";
    }
  }

  async kill(_eid: number) {}

  async generateDocs() {
    return "# My Environment\n\nDocument the runtime here.";
  }

  async generateToolDocs(_input: ToolDocsInput) {
    return "# Tool\n\nDocument how to call this tool here.";
  }
}

export function instantiate(): EnvironmentModule {
  return new MyEnvironment();
}
Note: The manifest metadata (name, version, type, etc.) goes in module.json for custom modules. Only built-in modules ship export const manifest from code. The skeleton above omits it, see Writing a custom environment module for a step-by-step walkthrough and a full working example.
See EnvironmentBindings for the callbacks and Execution for the state machine details.
Last modified on June 24, 2026