Runner and PlugsReference
Plug API types
The type declarations of @blaiseagent/runner that plug authors use, with their TSDoc.
Everything that plug code imports from @blaiseagent/runner: definePlug, the handler and context
types, and the message types. The prose reference is Plug API.
AgentSpawnSpec
/** The agent process the runner is about to spawn. */
export interface AgentSpawnSpec {
command: string;
args: string[];
env: Record<string, string>;
cwd: string;
}definePlug
/**
* Stamps the definition with `PLUG_API_VERSION`, which the loader checks
* both to recognise a plug and to refuse an incompatible one, and infers
* `ctx.inputs`, `ctx.state` and `ctx.persisted` types.
*/
export function definePlug<
Schema extends StandardSchemaV1 = StandardSchemaV1<Record<string, never>>,
S = undefined,
PSchema extends StandardSchemaV1 = StandardSchemaV1<undefined>,
>(definition: {
readonly name: string;
readonly description?: string;
readonly inputs?: Schema;
readonly persisted?: PSchema;
readonly resumable?: boolean;
readonly onLoad?: LoadHandler<InferOutput<Schema>, S, InferOutput<PSchema>>;
readonly onMigrate?: MigrateHandler<InferOutput<Schema>, InferOutput<PSchema>>;
readonly onSession?: SessionHandler<InferOutput<Schema>, S, InferOutput<PSchema>>;
readonly onTeardown?: TeardownHandler<InferOutput<Schema>, S, InferOutput<PSchema>>;
readonly onMessage?: MessageHandler<InferOutput<Schema>, S, InferOutput<PSchema>>;
}): Plug<InferOutput<Schema>, S, InferOutput<PSchema>>;EndReason
/** Why a session, or one of its runs, stopped. Decided by Blaise. */
export type EndReason =
"idle" | "completed" | "closed" | "cancelled" | "timeout" | "expired" | "failed";ExecOptions
export interface ExecOptions {
/** On `ctx.host`, defaults to `ctx.agent.cwd`. */
cwd?: string;
/** On `ctx.host`, merged over `ctx.agent.env`. */
env?: Record<string, string>;
/** Reject on non-zero exit (default true). */
check?: boolean;
}ExecResult
export interface ExecResult {
readonly exitCode: number;
readonly stdout: string;
readonly stderr: string;
}ForwardSpec
export interface ForwardSpec {
/** `to-runner`: the host's `remotePort` reaches the runner's `local`. */
readonly direction: "to-runner" | "to-host";
readonly remotePort: number;
readonly local: {
readonly host: string;
readonly port: number;
};
}Host
/** The machine the agent runs on. Paths are paths on that machine. */
export interface Host {
readonly info: HostInfo;
readonly fs: HostFs;
exec(command: string | readonly string[], options?: ExecOptions): Promise<ExecResult>;
spawn(spec: AgentSpawnSpec): Promise<HostProcess>;
/** Port forwarding, where the transport supports it (SSH: both ways). */
forward?(spec: ForwardSpec): Promise<AsyncDisposable>;
}HostFs
/**
* The file system of a host: the `node:vfs` API (today `node-vfs-polyfill`)
* on every host, the local one included (a `RealFSProvider`). Instance
* management (provider, mounting, virtual cwd) is not exposed to plugs.
* Prefer `promises`: on remote hosts the sync methods block the run's
* process on a network round trip.
*/
export type HostFs = Omit<
VirtualFileSystem,
| "provider"
| "mountPoint"
| "mounted"
| "mount"
| "unmount"
| "shouldHandle"
| "overlay"
| "virtualCwdEnabled"
| "cwd"
| "chdir"
| "resolvePath"
| typeof Symbol.dispose
>;HostInfo
export interface HostInfo {
/** "local" | "docker" | "k8s" | "cloudflare" | "daytona" | "exe-dev" | … */
readonly kind: string;
readonly os: "linux" | "darwin";
readonly home: string;
readonly tmpdir: string;
/** Default agent cwd on this host. */
readonly workspace: string;
/** Survives the session (VMs): process cleanup is critical. */
readonly persistent: boolean;
}HostProcess
/** A process on a host, with byte streams (the agent's are the ACP stream). */
export interface HostProcess {
readonly stdin: WritableStream<Uint8Array>;
readonly stdout: ReadableStream<Uint8Array>;
readonly stderr: ReadableStream<Uint8Array>;
readonly exited: Promise<number>;
kill(signal?: NodeJS.Signals): void;
}InferOutput
export type InferOutput<S extends StandardSchemaV1> = NonNullable<
S["~standard"]["types"]
>["output"];LoadContext
/** What `onLoad` and `onMigrate` see: the context before state exists. */
export type LoadContext<I, P> = Omit<PlugContext<I, never, P>, "state">;LoadHandler
/**
* Runs once per run, after the plug's inputs are evaluated and its persisted
* state is loaded, before any other handler. Returns `ctx.state`, which the
* other handlers may then mutate.
*/
export type LoadHandler<I, S, P> = (ctx: LoadContext<I, P>) => S | Promise<S>;Message
/**
* Every ACP message, discriminated by `direction` and `method`. `to_agent`
* flows plug 1 → plug N → agent; `from_agent` flows agent → plug N → plug 1
* → Blaise.
*/
export type Message = Variants<"to_agent", ToAgent> | Variants<"from_agent", FromAgent>;MessageHandler
/**
* Every message passes through `onMessage`. Narrow on `message.method` (and
* `message.direction`) to act on it; pass everything else on unchanged.
* Pass through (`return next(message)`), modify, post-process the result,
* short-circuit (return a result / drop a notification without calling
* `next`), answer a request with an error (throw a `RequestError`), or throw
* anything else to fail the session.
*/
export type MessageHandler<I, S, P> = (
ctx: PlugContext<I, S, P>,
message: Message,
next: <Msg extends Message>(message: Msg) => Promise<MessageResult<Msg>>
) => Promise<MessageResult<Message>> | MessageResult<Message>;MessageResult
/** The result a message resolves with (distributes over unions). */
export type MessageResult<Msg extends Message> = Msg extends {
direction: "to_agent";
method: infer M extends keyof ToAgent;
}
? ToAgent[M][1]
: Msg extends {
direction: "from_agent";
method: infer M extends keyof FromAgent;
}
? FromAgent[M][1]
: never;MigrateHandler
/**
* Called when the stored state no longer validates against `persisted` (the
* plug changed its schema). Receives the raw stored value; returns the
* migrated value, which is validated and written back, or `undefined` to
* drop it. Without `onMigrate`, an invalid value fails the run.
*/
export type MigrateHandler<I, P> = (
ctx: LoadContext<I, undefined>,
stored: unknown
) => Promise<P | undefined> | P | undefined;PauseInfo
/**
* What `next()` resolves with in `onSession`. The end of every run is a
* pause: make the environment cheap (stop servers, stop the container).
* `mayResume` is false when a teardown follows immediately, so work that only
* a later resume would use (snapshotting, committing an image) can be skipped.
*/
export interface PauseInfo {
readonly reason: Exclude<EndReason, "expired">;
readonly mayResume: boolean;
/** Set when `reason` is `failed`. */
readonly error?: unknown;
}PersistedState
/**
* Small JSON state hosted by Blaise per session and plug `id`, surviving
* across runs. Only for what cannot be re-derived from the config (ids that a
* provider assigns). Not data hosting (a few KB), and not for long-lived
* secrets. Encrypted at rest; deleted after a successful teardown.
*/
export interface PersistedState<P> {
/**
* Loaded and validated before the run (migrated by `onMigrate` if it no
* longer validates); `undefined` on the first run.
*/
readonly value: P | undefined;
/** Writes through to Blaise; durable when the promise resolves. */
set(value: P): Promise<void>;
clear(): Promise<void>;
}Plug
/** What a plug module default-exports: a definition stamped by `definePlug`. */
export interface Plug<I, S, P> extends PlugDefinition<I, S, P> {
readonly plugApi: {
readonly major: number;
readonly minor: number;
};
}PLUG_API_VERSION
/** Version of this API. The loader refuses plugs stamped with another major. */
export const PLUG_API_VERSION: {
readonly major: 1;
readonly minor: 0;
};PlugContext
/** One context per plug per run, shared by all of the plug's handlers. */
export interface PlugContext<Inputs, State, Persisted = undefined> {
/** `with:` after `${{ }}` evaluation and schema validation. */
readonly inputs: Inputs;
/** Per-run state of this plug, returned by `onLoad`. In memory, mutable. */
readonly state: State;
/** Per-session state of this plug, hosted by Blaise across runs. */
readonly persisted: PersistedState<Persisted>;
readonly session: SessionInfo;
readonly run: RunInfo;
readonly plug: {
readonly id: string | undefined;
readonly index: number;
};
/** Outputs of earlier plugs, keyed by plug id (`setup.<id>.outputs`). */
readonly setup: Readonly<
Record<
string,
{
readonly outputs: Readonly<Record<string, string>>;
}
>
>;
/**
* Draft of the agent process. Mutable until `next()` is called; frozen
* afterwards. The innermost plug sees the final spec. In `onTeardown` no
* agent starts, but `cwd` and `env` still apply to inner plugs' commands.
*/
readonly agent: AgentSpawnSpec;
/** The runner's own environment. Never contains Blaise tokens. */
readonly runnerEnv: Readonly<Record<string, string | undefined>>;
readonly log: PlugLogger;
/** Aborted when the session is shutting down. */
readonly signal: AbortSignal;
/**
* Where the agent runs: the runner's machine, or the host passed to
* `next()` by an outer plug. Hosts connect on first use. Bound to this plug: `exec` defaults to
* `ctx.agent.cwd`/`env`, and relative `fs` paths resolve against
* `ctx.agent.cwd`.
*/
readonly host: Host;
/** Always the runner's own machine, for runner-side files and commands. */
readonly runner: Host;
/** Expose `setup.<id>.outputs.<name>` to later plugs. */
setOutput(name: string, value: string): void;
}PlugDefinition
export interface PlugDefinition<I, S, P> {
/** Shown in logs and errors, e.g. "acme/redact-secrets". */
readonly name: string;
readonly description?: string;
readonly inputs?: StandardSchemaV1<unknown, I>;
/**
* Schema of the Blaise-hosted state; validated when loaded. A plug that
* declares it must be given an `id` in `setup:` (checked at load).
*/
readonly persisted?: StandardSchemaV1<unknown, P>;
/**
* Default `true`. Set `false` if `onSession` cannot handle `resume` runs;
* `listen` then refuses to start while Blaise keeps idle sessions paused.
* Blaise alone decides whether a session pauses: if it asks to pause a chain
* with such a plug, the runner logs a warning and pauses.
*/
readonly resumable?: boolean;
readonly onLoad?: LoadHandler<I, S, P>;
readonly onMigrate?: MigrateHandler<I, P>;
readonly onSession?: SessionHandler<I, S, P>;
readonly onTeardown?: TeardownHandler<I, S, P>;
readonly onMessage?: MessageHandler<I, S, P>;
}PlugLogger
export interface PlugLogger {
debug(message: string, fields?: Record<string, unknown>): void;
info(message: string, fields?: Record<string, unknown>): void;
warn(message: string, fields?: Record<string, unknown>): void;
error(message: string, fields?: Record<string, unknown>): void;
}RunInfo
/**
* A session spans one or more **runs** (one runner process each):
* - `start`: first run; `onSession` creates the environment.
* - `resume`: a later run after a pause; `onSession` re-attaches.
* - `teardown`: the session ended while paused; only `onTeardown` runs, with
* no agent. (A session that ends during a run is paused and then
* torn down within that same run.)
*/
export interface RunInfo {
readonly kind: "start" | "resume" | "teardown";
/** 1 for the first run of the session. */
readonly number: number;
}SessionHandler
/**
* Wraps one `start` or `resume` run. Code before `next()` attaches (create on
* `start`, re-attach on `resume`); code after it pauses. `next()` never
* rejects: it resolves with `PauseInfo`, including failures of inner plugs.
* Throwing here fails the run. Passing `host` makes every inner plug (and the
* agent) run on that host; the runner closes it when `next()` resolves.
*/
export type SessionHandler<I, S, P> = (
ctx: PlugContext<I, S, P>,
next: (options?: { readonly host?: Host }) => Promise<PauseInfo>
) => Promise<void>;SessionInfo
export interface SessionInfo {
readonly id: string;
readonly executionId: string;
readonly agent: {
readonly name: string;
};
}StandardIssue
export interface StandardIssue {
readonly message: string;
readonly path?: ReadonlyArray<
| PropertyKey
| {
readonly key: PropertyKey;
}
>;
}StandardSchemaV1
export interface StandardSchemaV1<Input = unknown, Output = Input> {
readonly "~standard": {
readonly version: 1;
readonly vendor: string;
readonly validate: (value: unknown) => StandardResult<Output> | Promise<StandardResult<Output>>;
readonly types?: {
readonly input: Input;
readonly output: Output;
};
};
}TeardownHandler
/**
* Wraps the teardown of a paused environment: right after the pause when the
* session ended during a run, or in a `teardown` run later. No agent runs;
* plugs without `onTeardown` are skipped. Code before `next()` restores
* access for inner plugs (hosts are lazy, so nothing is woken unless an inner
* plug uses it); code after it destroys what the plug left behind.
*/
export type TeardownHandler<I, S, P> = (
ctx: PlugContext<I, S, P>,
next: (options?: { readonly host?: Host }) => Promise<TeardownInfo>
) => Promise<void>;TeardownInfo
/** What `next()` resolves with in `onTeardown`. */
export interface TeardownInfo {
readonly reason: Exclude<EndReason, "idle">;
readonly error?: unknown;
/** True when cleanup itself failed, rather than the session that triggered cleanup. */
readonly cleanupFailed?: boolean;
}