BlaiseBlaise
Documentation
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;
}

On this page