package murmur:driver@0.6.0;
/// The contract between the runtime and a process driver: an artifact that knows how to drive
/// one harness CLI. Pure translation; the runtime grants it nothing.
///
/// The runtime keeps one driver instance for a whole run, so `parse` may rely on what `launch`
/// was given.
interface process {
/// How a turn in progress can be stopped gracefully.
enum interrupt-method {
/// Writing `launch-plan.interrupt-stdin` to the harness's stdin interrupts the turn.
stdin-message,
/// Sending `SIGINT` to the harness interrupts the turn.
signal-int,
/// The harness cannot be interrupted gracefully; the runtime kills it instead.
unsupported,
}
/// What a harness need may do with a host path.
enum path-access {
/// Read files and list directories.
read,
/// Read, write and create, as the workdir allows, but never execute.
read-write,
}
/// A host directory the harness needs beyond what the capsule's class gives every command.
record path-need {
/// `~/<rest>`, under the home of the account `mur` runs as, or an absolute path. A directory.
path: string,
access: path-access,
}
/// What the harness needs beyond the capsule's own grants. Each need is granted only where the
/// capsule manifest also allows it; the launch is refused otherwise. `required-env` is the
/// environment half.
record harness-needs {
paths: list<path-need>,
/// Hosts the harness connects to, each spelled as a `capabilities.network.allow` entry.
hosts: list<string>,
}
/// What the driver drives, read once when the driver is loaded.
record description {
/// The name of the harness this driver drives.
harness: string,
/// The executable the runtime resolves and runs, by name or path.
binary: string,
/// The arguments after `binary` that make the harness print its version.
version-args: list<string>,
/// The harness versions this driver release was tested against.
tested-versions: list<string>,
/// How a turn in progress is stopped.
interrupt: interrupt-method,
/// Environment variable names the harness cannot run without. Each must be declared in
/// the manifest's `capabilities.env.allow`, or the capsule is refused at load.
required-env: list<string>,
/// Whether `parse` emits `text-delta` events.
streams-text: bool,
/// Whether `parse` emits `usage` events for the harness it drives.
///
/// A driver whose harness reports no token counts sets this `false`, and the runtime then
/// records no counts for its turns. A manifest that sets `inference.max_session_tokens`, or
/// a machine ceiling in effect, against such a driver is refused at load: a ceiling that can
/// never be reached is not a ceiling.
reports-usage: bool,
/// How the runtime asks the harness whether it is signed in for this user, without
/// spending a turn.
login-check: login-check,
/// What the harness needs beyond the capsule's own grants. Each need must also be allowed by
/// the capsule manifest — paths by `capabilities.harness.paths`, hosts by
/// `capabilities.network.allow` — or the launch is refused with `E-CAP-021`. A malformed
/// need is refused when the driver is loaded (`E-RUN-032`). An empty `harness-needs` is valid.
harness-needs: harness-needs,
}
/// How the runtime asks the harness whether it is signed in for this user, without spending
/// a turn.
variant login-check {
/// Run `description.binary` with these arguments, the way the version check runs it, and
/// hand what it printed to `read-login`. Never empty: a driver naming no arguments is
/// refused at load, since the bare binary may start a session.
command(list<string>),
/// The harness offers no way to ask without a turn; the reason, shown to the person cut to
/// its first line and 200 characters.
unavailable(string),
}
/// What the login command printed.
record login-output {
/// The exit code; `none` when a signal ended the command.
exit-code: option<s32>,
/// Everything the command wrote to stdout.
stdout: string,
/// Everything the command wrote to stderr.
stderr: string,
}
/// Whether the harness is signed in, as the driver reads `login-output`.
variant login-state {
/// Signed in for this user.
logged-in,
/// Not signed in for this user; the reason, which names how to sign in and carries no
/// credential.
logged-out(string),
/// The output could not be read; the reason.
unknown(string),
}
/// The endpoint through which the harness calls the capsule's tools.
record bridge {
/// The server name the driver registers the bridge under in the harness's configuration.
server-name: string,
/// The URL the bridge listens on.
url: string,
/// The bearer token every request to `url` must carry.
bearer-token: string,
/// The capsule's tool names, bare. The driver names them the way its harness needs, and
/// strips that naming again in `parse`.
tool-names: list<string>,
}
/// Whether a turn starts a harness session or continues one.
enum session-mode {
/// Start a session with the given id.
new,
/// Continue the session with the given id.
resume,
}
/// The harness session a turn runs in.
record session {
/// The harness session id.
id: string,
/// Whether the session is started or continued.
mode: session-mode,
}
/// Everything the driver needs to plan one turn.
record launch-request {
/// The manifest's `inference.model`; `none` leaves the choice to the harness.
model: option<string>,
/// The runtime's full system prompt for the turn, notices included.
system-prompt: string,
/// The manifest's `inference.driver.config` as JSON; `none` when the manifest wrote none.
config: option<string>,
/// The tool bridge; `none` when the capsule exposes no tools to the harness.
bridge: option<bridge>,
/// The harness session this turn runs in.
session: session,
/// The version the harness printed for `description.version-args`; `none` when it could
/// not be read.
harness-version: option<string>,
/// The task text for this turn.
task: string,
}
/// A file the runtime writes before spawning the harness.
record driver-file {
/// The file name, relative to the files directory.
name: string,
/// The file's contents.
contents: string,
}
/// How to spawn the harness for one turn.
///
/// The runtime writes `files` into a private directory readable only by the capsule's user,
/// and replaces the token `{files_dir}` in `args` and in `env-set` values with that
/// directory's path.
record launch-plan {
/// The full argument list after the binary.
args: list<string>,
/// Variables to set in the harness's environment, after the ones
/// `capabilities.env.allow` passes through.
env-set: list<tuple<string, string>>,
/// Files to write before spawning.
files: list<driver-file>,
/// Bytes written to stdin once the harness starts; `none` writes nothing.
stdin: option<list<u8>>,
/// Whether stdin stays open after `stdin` is written.
keep-stdin-open: bool,
/// Bytes that interrupt the turn when written to stdin, for `interrupt-method.stdin-message`.
interrupt-stdin: option<list<u8>>,
}
/// The harness's report of the session it started.
record session-info {
/// The harness session id.
id: string,
/// How the harness is billing: `subscription`, `api-key`, or another value the driver
/// reads from its harness. Anything but `subscription` is warned about and traced.
auth: string,
/// The model the harness reports it is running; `none` when it does not say.
model: option<string>,
}
/// What the harness has spent on this run so far.
///
/// Every member is **cumulative for the harness run**, never the increment since the last
/// report: a harness that sends a running count as it streams and a final total at the end is
/// reported verbatim, both times. The runtime attributes, to each turn it closes, only the
/// growth of each member over what it has already attributed, so reporting the same total
/// twice adds nothing and a member that shrinks contributes zero. A driver that reports
/// deltas instead would be counted several times over.
///
/// Every member is independently optional. `none` means *this harness does not report this
/// count* and is never the same fact as `0`, which means the harness reported spending none:
/// the runtime keeps the two apart all the way into the trace.
///
/// `cache-read` and `cache-creation` sit **beside** `input`, never inside it. A harness that
/// folds its cache counts into its input count must subtract them before reporting, so that
/// `input` is always the uncached prompt alone. `thinking` is a **subset** of `output`, not an
/// addition to it.
///
/// No cost figure and no context-window size is carried. A price a harness computed against
/// its own table is a different claim from a token count, and the runtime has no equivalent
/// on its other transport to make it mean the same thing on both.
record usage {
/// Prompt tokens, excluding anything `cache-read` or `cache-creation` covers.
input: option<u64>,
/// Generated tokens, including the `thinking` subset below.
output: option<u64>,
/// Prompt tokens served from the provider's cache.
cache-read: option<u64>,
/// Prompt tokens written to the provider's cache.
cache-creation: option<u64>,
/// The part of `output` the harness reports as reasoning.
thinking: option<u64>,
}
/// A tool the agent called.
record tool-call-info {
/// The call id, matching the `tool-result-info.id` of its result.
id: string,
/// The bare artifact name, with any prefix the driver added in `launch` stripped.
name: string,
/// The call's input as JSON.
input: string,
}
/// A tool call the model has begun writing, before its input is complete.
record tool-call-start {
/// The call id: the `tool-call-info.id` the complete call will carry.
id: string,
/// The bare artifact name, with any prefix the driver added in `launch` stripped: the
/// `tool-call-info.name` the complete call will carry.
name: string,
}
/// How much of a started call's input the model has written so far.
record tool-call-progress {
/// The id of the `tool-call-start` this reports on.
id: string,
/// Bytes of the call's input JSON written so far, **cumulative for the call**, never the
/// increment since the last report. A report smaller than an earlier one is ignored.
input-bytes: u64,
}
/// The result of a tool call.
record tool-result-info {
/// The id of the `tool-call-info` this answers.
id: string,
/// The tool's output.
output: string,
/// Whether the tool reported failure.
is-error: bool,
}
/// The harness is retrying a request to its provider.
record retry-info {
/// Which attempt this is.
attempt: u32,
/// Why the previous attempt failed.
reason: string,
}
/// Why a turn failed.
enum failure-kind {
/// The harness could not authenticate.
auth,
/// The harness ran out of quota or was rate limited.
quota,
/// The harness stopped at its turn limit.
max-turns,
/// The turn was interrupted.
canceled,
/// The harness reported an error of its own.
harness-error,
/// Anything else.
other,
}
/// A failed turn.
record turn-failure {
/// Why it failed.
kind: failure-kind,
/// The failure as the harness described it.
message: string,
}
/// Whether a subscription window still has room, as the harness reports it.
enum quota-state {
/// The window has room.
ok,
/// The harness warns that the window is close to its limit.
warning,
/// The window is used up until it resets.
exhausted,
}
/// One subscription window a harness reports: a span of time over which the person's
/// subscription allows a certain amount of use.
///
/// The driver translates its harness's spelling into these names and units: a percentage
/// becomes a fraction, seconds become milliseconds, and a harness that numbers its windows
/// (`primary`, `secondary`) is given names. Every member but `name` is independently optional,
/// and `none` means the harness did not report that member, never zero.
record quota-window {
/// A stable lowercase label for the window, which the driver documents, such as `five-hour`
/// or `seven-day`: 1 to 64 characters of `a`-`z`, `0`-`9` and `-`, starting with a letter or
/// digit. A window with any other name is refused and noted in the trace.
name: string,
/// How much of the window is used, as a fraction from 0 to 1. A value outside that range,
/// or not finite, refuses the window.
used: option<f64>,
/// When the window resets, in milliseconds since the Unix epoch.
resets-at: option<u64>,
/// How long the window is, in minutes.
window-minutes: option<u64>,
/// Whether the window still has room, as the harness reports it.
state: option<quota-state>,
}
/// One thing the harness did, read out of its output.
variant event {
/// The harness's session id, and how it is billing.
session-started(session-info),
/// Streamed text, appended to the message in progress.
text-delta(string),
/// The complete text of one message. It replaces any `text-delta` already streamed for
/// that message; it is never appended to them.
text(string),
/// Streamed thinking, appended to the thinking in progress.
thinking-delta(string),
/// The complete thinking of one message, replacing any `thinking-delta` streamed for it.
thinking(string),
/// A tool the agent called.
tool-call(tool-call-info),
/// The result of a tool call.
tool-result(tool-result-info),
/// The harness is retrying a request to its provider.
retry(retry-info),
/// What the harness has spent on this run so far, cumulative. May arrive any number of
/// times; the runtime records each member's growth against the turn open when it arrives.
usage(usage),
/// The turn finished; the payload is the final answer.
turn-end(string),
/// The turn failed.
turn-failed(turn-failure),
/// A diagnostic for the trace only, such as a line the driver could not read.
note(string),
/// A tool call the model has begun writing. How much of its input is written follows as
/// `tool-call-progress`, and the complete call as `tool-call` with the same id and name.
/// Optional: a driver whose harness does not report a call until it is complete never sends it.
tool-call-started(tool-call-start),
/// How much of a started call's input has been written so far.
tool-call-progress(tool-call-progress),
/// The subscription windows the harness reports as used, as a snapshot of the windows it
/// names. A later report replaces a window of the same name whole and leaves the others
/// standing, so a driver whose harness sends sparse updates merges them into its own last
/// report before sending one. The runtime records and forwards it and never acts on it.
/// Optional: a driver whose harness reports no windows never sends it. May arrive after the
/// terminal event.
quota(list<quota-window>),
/// The harness's prediction of what the person will ask next. Optional: a driver whose
/// harness makes no such prediction never sends it. May arrive after the terminal event.
suggested-prompt(string),
}
/// How the harness exited, for a run whose output ended without a terminal event.
record exit-status {
/// The exit code; `none` when the harness was ended by a signal.
code: option<s32>,
/// The signal that ended the harness; `none` when it exited.
signal: option<s32>,
/// The last part of the harness's stderr.
stderr-tail: string,
/// Whether the runtime interrupted the turn.
interrupted: bool,
/// Whether `parse` returned a `turn-end` or `turn-failed` for this run.
saw-terminal: bool,
}
/// What the driver drives. Called once, when the driver is loaded.
describe: func() -> description;
/// Plans one turn; `err` refuses it with a message the runtime reports.
launch: func(request: launch-request) -> result<launch-plan, string>;
/// Reads a batch of complete stdout lines into events. May return none.
///
/// After the batch holding the terminal event (`turn-end` or `turn-failed`), the runtime keeps
/// reading the harness's output until the harness exits or a short grace runs out. From those
/// lines, and from the rest of the terminal event's batch, only `quota`, `suggested-prompt` and
/// `note` are taken; every other event is noted in the trace and ignored, and never changes the
/// turn's outcome, its result or its spend.
parse: func(lines: list<string>) -> list<event>;
/// Classifies a run whose output ended without `turn-end` or `turn-failed`. Returns only
/// `turn-end` or `turn-failed`; when `exit.interrupted` is true and no terminal event was
/// seen it must return `turn-failed` with kind `canceled`, whatever the exit code says.
classify-exit: func(exit: exit-status) -> event;
/// Reads what the `login-check.command` printed into whether the harness is signed in.
/// Called only for a driver whose `describe` names a `command`, and only when the command
/// finished within the bound. The reasons it returns are shown to the person, cut to their
/// first line and 200 characters, so they must carry no credential.
read-login: func(output: login-output) -> login-state;
}
/// A process driver exports the `process` interface and imports nothing of Murmur's: it is
/// granted nothing.
world process-driver {
export process;
}