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.
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:
- Call
bindings.setState(eid, "queued") if not already running, then
bindings.setState(eid, "running") when the code starts.
- Call
bindings.emitStdout(eid, buf) / emitStderr(eid, buf) for any
captured text output.
- Call
bindings.emitOutput(eid, obj) whenever user code emits a
structured output payload.
- Call
bindings.setError(eid, message) if execution fails.
- 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.
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