WIT Interfaces
WASM artifacts — capsules, tools, drivers, and hooks — talk to the runtime through WIT
interfaces. This page lists each interface, who uses it, and what its behaviour means for you.
The type definitions themselves live in the repository under
crates/capsule-runtime/wit/;
each row below links to its source, and Package versioning lists the
version each one carries.
Interfaces
| Interface | Used by | Purpose |
|---|---|---|
murmur:tool/run |
Exported by tool and driver components | The entrypoint the runtime calls to run a tool or an inference driver |
murmur:tool-registry/invoke |
Imported by capsule components | Call an allowlisted tool by name |
murmur:capsule/run |
Exported by capsule components | The capsule entrypoint |
murmur:artifact-manager/manage |
Provided by the runtime to capsule components | List, describe, and pull artifacts during a session |
murmur:shell/execute |
Implemented natively by the runtime | Run an allowlisted shell binary |
murmur:message/send |
Provided by the runtime to capsule components | Send an A2A task to a peer capsule |
murmur:task/task |
Imported by tool components | Pause the agent loop and wait for external input |
murmur:text/chunks |
Imported by tool and driver components | Emit response and thinking chunks to the session's SSE stream |
murmur:hook/lifecycle |
Exported by hook artifacts | The lifecycle handlers the runtime calls |
murmur:runtime/inference |
Provided by the runtime to hook components | Run one LLM completion through the capsule's configured driver |
murmur:runtime/tokens |
Provided by the runtime to hook components | Count the tokens in a string the way the runtime counts them |
murmur:task-io/read |
Provided by the runtime to hook components | Read the task's input text and the agent's result text |
murmur:conversation/read |
Provided by the runtime to hook components | Read the capsule's durable conversation record |
Worlds
A world is what your component's source compiles against with wit_bindgen::generate!.
| World | Imports | Exports | Defined in |
|---|---|---|---|
capsule |
tool-registry/invoke |
capsule/run |
guest/worlds.wit |
tool |
task/task, text/chunks |
tool/run |
guest/worlds.wit |
driver |
text/chunks |
tool/run |
guest/worlds.wit |
hook |
runtime/inference, runtime/tokens, task-io/read, conversation/read |
hook/lifecycle |
hook/worlds.wit |
runtime-host |
artifact-manager/manage, shell/execute, tool-registry/invoke, message/send |
— | host/host.wit |
Agent capsules compile against no world — the agent loop runs inside the runtime, and the capsule is defined by its manifest alone.
murmur:tool/run
A tool-result carries a metadata list — a free-form key/value channel back to the runtime.
These keys are reserved:
| Key | Value | Effect |
|---|---|---|
continuation_id |
Opaque id string | Tells the runtime the driver is holding conversation state provider-side. See Stateful driver continuation. |
state_effect |
"read" or "mutate" |
How this call affected the resource it addressed. Recorded on the call's tool_call trace event. |
resource_id |
Opaque, tool-defined string | Which resource this call addressed. Recorded verbatim on the tool_call trace event. |
state_effect and resource_id together let mur trace spot redundant calls without knowing
anything about your tool. Omit either and the call counts as "unknown": it is never credited as a
redundant read.
For resource_id, scope the value by addressing scheme (for example "sym:Foo::bar") so it
cannot collide with an unrelated resource of the same name. Two calls address the same resource
when their resource_id values are byte-equal. Without one, the redundancy detector guesses
from path-like input fields (path, file, file_path, filepath, filename) — so declare
the key if your resource is a symbol, URI, or query.
Returning a large result
A tool with more output than it wants to carry in data writes the output to a file and returns
the filename in data-path. Name that file relative to the session workdir, the
directory the tool's own . resolves to. When the runtime reads the file on the tool's behalf, it
resolves the name beneath that directory and refuses anything that leaves it:
data-path names |
Outcome |
|---|---|
| A regular file under the session workdir | Its contents become the result |
An absolute path, or one climbing out with .. |
The call fails |
| A path passing through a symlink, wherever the symlink points | The call fails |
| A file that does not exist | The call fails |
| A file over 10 MiB | The call fails |
A path holding % escapes |
Decoded before it is resolved, so %2e%2e fails like .. |
A result carrying data uses it verbatim and never opens data-path.
Stateful driver continuation
By default the runtime resends the full conversation history on every turn. A driver holding
conversation state provider-side can opt out by returning a non-empty continuation_id.
| Situation | What the runtime does |
|---|---|
Driver returns a non-empty continuation_id |
Holds the id for the rest of the session loop |
Next turn, same context_id |
Sends only the messages appended since the driver last acknowledged state, plus the held id |
Next turn, different context_id |
Full resend — a continuation is never reused across unrelated tasks |
| Driver omits the key or returns an empty string | Drops the held id; the next turn is a full resend |
A hook commits replace-context (for example compaction) |
Drops the held id; the next turn resends the post-compaction history |
Token accounting for the compaction threshold is always computed from the full conversation, not the smaller payload actually sent, so continuation never changes when compaction fires.
Prompt cache routing hint
Every request the runtime hands a driver carries a top-level prompt_cache_key string. Its value
is fixed for every turn of a task, and it does not change when history is compacted or when a
held continuation_id is dropped:
| Task | prompt_cache_key |
|---|---|
| Has a context id, and the value fits in 64 characters | <capsule-name>:<version>:<context-id> |
| Has no context id, and the value fits in 64 characters | <capsule-name>:<version> |
| The value would run past 64 characters | The leading characters of the same value, then : and a 16-character hex suffix |
A provider that routes on the value keeps a task's turns on one machine, so each turn reaches the cache entry the previous turn warmed. Two runs of the same capsule get different values, because a context id is minted per task — including when the value was shortened.
Shortening keeps the value inside the 64 characters the OpenAI Chat and Responses APIs accept; a capsule name of ordinary length already runs past that once a context id is appended. A shortened value still opens with the capsule's own name, so a key seen in a payload dump names the capsule that produced it.
A driver reads the field only if it declares it, so a driver that ignores it runs unchanged.
Forward it only where the provider defines a field of its own for it: the OpenAI Chat and
Responses APIs accept prompt_cache_key in the request body, and the Anthropic Messages API
rejects a body carrying any field it does not define.
The [Capsule] block the runtime prepends to every system prompt is a pure function of the
capsule's name and version — it names no host-specific path, so it is byte-identical across every
launch of the same capsule and version. Prefix caching depends on that stability: a provider
matches its cache against the bytes it has seen before, and the first block of every prompt would
otherwise never repeat.
Reported token usage
A driver response may carry a top-level usage object holding the provider's own token counts
for that call:
{
"stop_reason": "end_turn",
"content": [{"type": "text", "text": "..."}],
"usage": {
"input_tokens": 12043,
"output_tokens": 218,
"cached_tokens": 11780,
"cache_write_tokens": 0
}
}
| Member | Value |
|---|---|
input_tokens |
Tokens the provider billed for the request |
output_tokens |
Tokens the provider billed for the completion |
cached_tokens |
Request tokens served from the provider's prompt cache |
cache_write_tokens |
Request tokens written into the provider's prompt cache |
Every member is optional and every member is a non-negative integer. A driver reports whichever
members its provider returned; a provider with no prompt cache reports no cache members. Omit a
member the provider did not report rather than sending 0 — the runtime keeps the two apart, and
a 0 reads as a genuine cache miss on the trace.
Where the two provider shapes carry each number:
| Member | Anthropic Messages API | OpenAI Chat and Responses APIs |
|---|---|---|
input_tokens |
usage.input_tokens |
usage.prompt_tokens |
output_tokens |
usage.output_tokens |
usage.completion_tokens |
cached_tokens |
usage.cache_read_input_tokens |
usage.prompt_tokens_details.cached_tokens |
cache_write_tokens |
usage.cache_creation_input_tokens |
Not reported |
The runtime records the reported numbers on the call's inference trace event and its
capsule.inference span, and acts on none of them: the compaction threshold and every budget
decision keep running on the runtime's own pre-flight estimate. See
Observability schemas for the field names.
A malformed report degrades to no report rather than failing the call:
| What the driver returns | What the runtime records |
|---|---|
No usage |
Nothing |
A usage that is not an object |
Nothing |
| A member that is not a non-negative integer | Nothing for that member; its well-formed siblings are still recorded |
| A member the runtime does not define | Nothing for that member |
An omitted number is absent from the trace event, never written as 0.
murmur:artifact-manager/manage
Lets a capsule component inspect and install artifacts mid-session: list and describe report
what is installed, diagnostics returns the session id alongside that list, and pull installs a
new artifact. search and remove are unimplemented: calling either returns an error.
pull resolves an artifact from the session's registry, verifies its bytes against the registry
hash and any pinned murmur.lock entry, then installs it under <workdir>/tools/<name>/ and
updates murmur.lock. A hash mismatch, or a version or hash that conflicts with an existing
murmur.lock pin, returns an error before anything is written. A pulled artifact is immediately
visible to list() and describe() and, for WASM tools, callable via invoke().
murmur:shell/execute
The contract for running an allowlisted shell binary. The runtime implements it natively.
| Behaviour | Detail |
|---|---|
| Non-zero exit code | Data, not an error — the call only errs on spawn/IO failure |
truncated: true |
stdout or stderr exceeded 16 KiB and was cut; full-output-path points at the untruncated log in the workdir |
| Allowlist | binary must appear in capabilities.shell.allow; anything else is rejected before the process is spawned |
murmur:task/task
request-input pauses the agent loop at a decision boundary and waits for a reply delivered by
message/send. From the component's perspective the call is synchronous: pass a prompt string,
receive the reply string.
Call it when the agent has reached a decision it cannot make on its own — ambiguous requirements, or a consequential action that needs approval — and a human or supervisor capsule is expected to reply.
It is available in the tool world only, and only while the capsule is running an A2A task.
Called anywhere else, it returns an error immediately.
Timeout: lifecycle.input_timeout_secs in the manifest (see
lifecycle.input_timeout_secs). When it fires,
the component is aborted and the task transitions to failed.
murmur:message/send
Sends a task to a peer capsule. send takes the peer URL and a message — a message-id, an
optional context-id, and the text — and returns the peer's task-id, context-id, and
state. The runtime handles the JSON-RPC 2.0 wire format, so the capsule never sees it.
| Behaviour | Detail |
|---|---|
| Allowlist | The peer URL must appear in the sender's capabilities.network.allow. Otherwise the call returns Err("network policy: '...' not in capabilities.network.allow") and no connection is made. |
| Tracing | With OTel configured, the runtime injects a W3C traceparent header so the peer's session span nests under the sender's. |
| Origin | The runtime stamps x-murmur-task-origin: peer and the sending task's own x-murmur-task-trust on every request, so the receiving capsule inherits the sender's trust class. The message record has no field for either, so a capsule cannot set them. See Task origin and trust class. |
| Result state | task-result.state is the peer's response to the send: submitted, working, input-required, completed, failed, rejected, or canceled. Poll the peer's tasks/get endpoint for the final state. |
murmur:hook/lifecycle
Hook artifacts (runtime: hook) export this interface. The runtime calls each handler
synchronously. Returning an error logs it to logs/hook-<name>.log in the workdir and the
session continues — except at on-compaction, where the error ends the session, because the
runtime has no other way back under the token budget:
on-compaction error |
Session exit_status |
|---|---|
Follows a run-inference call a spend ceiling refused |
spend_ceiling_reached |
| Any other | failed |
| Handler | Fires |
|---|---|
on-stage |
Once at capsule staging, before any session starts |
on-session-start |
Once per capsule launch, before the first task's work begins |
on-task-start |
Once per task, before that task's first inference turn |
on-inference |
After each inference driver response is parsed |
on-tool-call |
Twice per model-requested tool invocation: once before it is dispatched, once after it returns or errors |
on-shell |
Twice per allowed shell command: once before it is dispatched, once after it returns |
on-compaction |
When the session token threshold is reached, before any history is replaced |
on-task-end |
Once per task, immediately after that task's agent loop returns |
on-session-end |
Once per capsule launch, after the task loop exits |
With task_acceptance: none or single, on-task-start coincides with on-session-start; with
queue it fires once per queued task. session-id is the session identifier — ses_ followed by
32 hex characters — and is the same value in every hook event of a run.
on-task-start and on-task-end are optional exports: a component that omits them loads and is
simply never dispatched for them. The six session handlers are required — a component missing one
fails to load with an error naming it. A missing on-stage is logged rather than fatal, and
staging continues.
The two dispatches of a gated event
on-tool-call and on-shell are each dispatched twice, and outcome is what tells the two
apart:
outcome |
Dispatch | Meaning |
|---|---|---|
none |
Decision point, before the call | The call has not run. A returned deny refuses it. |
some(...) |
Observation, after the call | The call has run. Nothing returned can change that. |
The decision point runs only for a hook whose own murmur.yaml declares
commit_policy: deny on binding: on-shell or binding: on-tool-call. Every other hook sees
the observation dispatch alone.
What each handler can commit
A handler may return any hook-output arm, but the runtime commits only one arm per event:
| Handler | Committed arm |
|---|---|
on-stage |
write-manifests |
on-inference |
artifact |
on-compaction |
replace-context |
on-task-end |
reopen-task |
on-task-start |
seed-context |
on-shell, on-tool-call |
deny, at the decision point only |
| All others | none — only none is silent |
This table is also enforced ahead of time. A hook artifact declares in its own murmur.yaml
which handler it binds to (binding:) and what it expects committed (commit_policy:), and the
runtime checks the pair against this table when the capsule is staged. A commit_policy the
binding's handler cannot commit (say commit_policy: reopen-task on binding: on-stage) fails
staging with an error naming the binding, the declared policy, and the one this binding honors,
rather than becoming a mid-session dispatch fault. A hook with no binding: receives every
event, so any commit_policy is valid for it. See Hook contract
fields.
Returning none is always silent, and is the normal case for an observational hook. Returning
any other non-none arm is a loud but non-fatal fault:
- A line naming the hook, the handler, and the discarded arm is appended to
logs/hook-<name>.log. - A
hook_dispatch_errorevent is written totrace.jsonl— see Session trace schema. The one exception ison-stage, which runs before the trace file exists. Anasynchook's fault reaches the trace by the same path; the trace and the log are the only places it appears, since the agent loop never sees an async hook's return value.
reopen-task is a control arm: returning it from on-task-end re-runs the task's agent loop
with the arm's string injected as feedback, subject to lifecycle.max_task_reopens and
inference.max_turns. See Task
reopening.
seed-context is honored at on-task-start alone: the first bound blocking hook to return it
wins, and its messages are placed at the head of the task's context, ahead of any loaded history
and ahead of the task message. The list is chronological, oldest first. The host measures it
against task-start-event.budget-tokens — context.max_tokens × context.seed_budget — and
commits it whole, drops its oldest messages, summarizes the overflowing front through the
compaction hook, or refuses it; every outcome is recorded as a
context_seed event, and a refusal never fails the task.
Returning it from any other handler is the same loud-but-non-fatal fault as any other unhonored
arm. Only binding: on-task-start accepts it as a commit_policy.
deny is a control arm honored at the decision-point dispatch of on-shell and on-tool-call
alone: the call is not made, the agent is handed a result naming the hook and the reason, and a
call_denied event is written. It only narrows — there is
no arm that permits a call, and the manifest's own capability check still runs first, so a hook
can never make a capsule able to do something its manifest did not allow. Returning it from any
other handler, or from the observation dispatch of these two, is the same loud-but-non-fatal fault
as any other unhonored arm. See Policy hooks for the
fail-closed rule that governs a policy hook's failures.
Event field notes
compaction-event.modelandcompaction-event.system-promptcarryinference.compaction.modelandinference.compaction.system_promptverbatim, so a compaction hook knows which model and which prompt to use for its own summarization call. Settinginference.compaction.system_prompt_fileinstead delivers that file's contents. Either field is absent when the manifest leaves it unset, and the hook resolves its own default.message.idandmessage.source-idare runtime bookkeeping carried alongside a message'sroleandcontent.idismsg_followed by a uuid — an identity, so two byte-identical messages still carry different ids.source-idis opaque: whatever produced the content sets it, and the runtime records it verbatim without parsing it. Both are stripped before the driver payload is built, so neither reaches the provider — a uuid at the head of a cached prefix would break prompt-prefix caching on every request. The runtime mints anidfor every message it builds out of a hook-returned message list, so anida hook sets on a message it returns is replaced rather than kept; asource-ida hook sets is carried verbatim, and the field is absent when the hook set none.task-start-event.context-windowis the capsule'scontext.max_tokens, or0when the manifest declares nocontext:block. It is precomputed so a hook sizing its work against the window never has to know which model the session runs.task-start-event.budget-tokensis the ceiling aseed-contextreturned from this event is enforced against:context.max_tokens×context.seed_budget, rounded down.task-start-event.prior-tokensis the token count of the conversation history the task will load underlifecycle.conversation: threaded, and0under every other conversation mode. Read0as "the runtime has not computed this" and decline, rather than as an unbounded budget.shell-event.binaryis the program the shell tool invoked — a canonicalized absolute path (for example/usr/bin/pytest) when the runtime resolved the name againstPATH, and the bare invoked name when nothing resolved.shell-event.commandcarries the argument list alone (for a shell interpreter, the script text passed via-c), truncated to 200 characters. It is a display string.shell-event.argvis the exact argument list the runtime passes to the executable andshell-event.scriptis the-cbody (absent for every other form), both untruncated — a policy must decide on those two and onbinary, never oncommand.-
shell-event.recipeis the body of the recipe a build-tool call names, read by the runtime out of the capsule's workdir. A hook holds no grant on that directory and reads nothing itself.Invocation Body make <target>the target's recipe lines, from GNUmakefile,makefileorMakefilejust <recipe>the recipe's indented body, from justfile,.justfile,JustfileorJUSTFILEnpm run <script>the string value of scripts.<script>inpackage.jsonThe body is the file's own text: variables, interpolations, conditionals and recipe arguments are carried as written. It is absent when the call names no recipe, and when the runtime could not resolve one:
- no recipe file in the workdir, or one that is unreadable, oversized or not UTF-8
- a target, recipe or script that is not defined, or one reached through a
makepattern rule or ajustalias - a form that could resolve to more than one body: two
makerules for one target, a duplicatejustrecipe, amakerecipe written on the rule line astarget: ; cmd, or annpmscript with apre/postsibling - an invocation carrying flags beyond the recipe file and working-directory selectors, or a
makevariable override such asmake build VERSION=2
Read an absent body as "the runtime has no body to show", which is a statement about the resolution and not about the call. -
tool-event.inputis the exact tool input JSON the tool will receive, untruncated, and is what a policy decides on. -shell-event.outcomeandtool-event.outcomecarry what the call produced, and are absent at the decision point — see The two dispatches of a gated event.
murmur:runtime/inference
A runtime-provided import available to any hook component that declares it. Nothing needs to be exported and nothing in the manifest changes.
run-inference runs exactly one LLM completion through the capsule's configured driver
(inference.driver.artifact).
| Case | Behaviour |
|---|---|
No model given |
Uses the manifest's inference.model |
model given |
Sends that model. If the driver or provider rejects it the call returns an error; the runtime never silently falls back. To retry on the primary model, call again without a model. |
No system-prompt given |
No system prompt is sent |
| No driver configured | Returns an error naming inference.driver.artifact. The import still links, so the hook itself runs. |
| A spend ceiling refuses the call | Returns an error starting spend ceiling reached: that names inference.max_session_tokens or spend.machine_tokens_per_day. Nothing is sent, and retrying will not succeed. |
model-used is the model string the runtime actually sent. input-tokens and output-tokens
are runtime-side tiktoken counts of the request payload and the raw driver response.
Every call that reaches the driver, success or failure, writes one inference record to
trace.jsonl and one capsule.inference OTel span carrying origin: "hook:<hook name>" and
model.
murmur:runtime/tokens
A runtime-provided import available to any hook component that declares it. It needs no capability grant and no manifest entry: counting text reaches no resource.
count returns the runtime's own cl100k_base count of the string — the same number behind the
compaction trigger and the context-occupancy calculation. A hook measuring a payload against a
budget and the runtime enforcing that budget therefore agree on what the payload costs, which a
hook's own tokenizer would not guarantee.
murmur:task-io/read
A runtime-provided import available to any hook component that declares it. It hands a hook the text of the task its capsule was given and the result text the agent produced, so an output gate or an archiver needs no filesystem grant to see either.
Reading is granted per hook with
capabilities.task_io.read: true on that hook's entry in the
capsule manifest. A hook without the key still links and still runs — every function returns
not-granted.
| Function | Returns |
|---|---|
input-len(form) |
Byte length of the task input |
read-input(form, offset, max-bytes) |
A window of the task input |
output-len() |
Byte length of the agent's result text |
read-output(offset, max-bytes) |
A window of the result text |
Ask for a length first, then read the window you can afford: the runtime imposes no truncation cap
of its own. A read returns the longest prefix of the value from offset that fits in max-bytes
and ends on a character boundary, so a multi-byte character is never split. Advance offset by the
byte length of what you got back. An empty return with offset still below the length means
max-bytes was too small for the next character.
form picks which rendering of the input to read:
| Form | Text |
|---|---|
as-given |
Exactly what this attempt's agent loop was handed, including any feedback a previous reopen appended. What an output gate judges against. |
original |
The task before any reopen feedback was appended. What an archiver or cost-attribution hook wants. |
On a task that has never been reopened the two are identical.
When a task is readable
A task is in scope from the moment one of its attempts enters the agent loop until the runtime finishes with that task, reopens included.
| Lifecycle event | input-* |
output-* |
|---|---|---|
on-stage |
no-task |
no-task |
on-session-start |
no-task |
no-task |
on-task-start |
no-task |
no-task |
on-inference, on-tool-call, on-shell, on-compaction |
This attempt's input | no-output until the loop finishes |
on-task-end |
This attempt's input | This attempt's result text, or no-output if it ended without one |
on-session-end |
no-task |
no-task |
on-task-start fires before the task enters the loop and on-session-end after the last task has
left it, so an archiver binds to on-task-end. On the second attempt of a reopened task the output
is cleared at attempt start: a hook judging attempt 2 never sees attempt 1's result.
An execution_mode: async hook reads whatever is in scope when its worker reaches the call, which
may be after the task has left scope. Bind a hook that reads the task as blocking.
| Error | Meaning |
|---|---|
not-granted |
The hook's entry does not declare capabilities.task_io.read: true |
no-task |
No task is in scope at this lifecycle event |
no-output |
A task is in scope but the loop produced no result text |
out-of-range |
offset is past the end of the value, or inside a multi-byte character |
murmur:conversation/read
A runtime-provided import available to any hook component that declares it. It hands a hook the
capsule's durable conversation record — every message the
runtime put in front of the model, newest first, paged — without a filesystem grant. No artifact
gets a filesystem path into ~/.murmur/conversations/; this interface is the only way in. The
runtime is the only writer, and there is no write function.
Reading is granted per hook with
capabilities.conversation.read: true on that hook's entry in the
capsule manifest. A hook without the key still links and still runs — read-messages returns
not-granted.
| Function | Returns |
|---|---|
read-messages(cursor, limit) |
One message-page: messages, next-cursor, total |
messages[0] is the most recently appended message, and each carries the id its record line
holds.
| Field | Meaning |
|---|---|
cursor |
An opaque token. none starts at the newest message; pass back the next-cursor of the page you just read, unmodified. |
next-cursor |
none once a page has reached the oldest message in the record, which is what ends a paging loop. |
limit |
Clamped to 1..=100, so 0 reads one message and a loop always makes progress. |
total |
Parseable messages in the whole record at the moment the page was served, not in this page. |
A cursor stays valid while the runtime appends to the record underneath a paging hook: it names a position counted from the oldest message.
total is a snapshot, not a loop invariant. The runtime appends while a hook pages, so it grows
from one page to the next and the page lengths of a completed walk can sum to less than the final
total. Every page is a consistent snapshot anchored at the oldest message, so nothing repeats and
nothing is skipped. Never size a paging loop from total; stop on next-cursor: none.
A message whose role is "tool" reaches a reader with its content replaced by a JSON object,
because a tool result carries more than the one string content has room for.
| Key | Value |
|---|---|
__murmur_tool_msg__ |
Always true, and what marks the envelope |
tool_call_id |
The call this result answers, or null |
is_error |
Whether that call failed, or null |
body |
The tool result as the runtime holds it |
This is the same encoding a compaction hook receives on
murmur:hook/lifecycle's message — one encoding, in both places.
| Error | Meaning |
|---|---|
not-granted |
The hook's entry does not declare capabilities.conversation.read: true |
invalid-cursor |
A cursor naming a position outside the record |
unavailable: <reason> |
The record exists and could not be read |
A record that does not exist is not an error: the read succeeds with no messages and total: 0.
That is what a hook sees before the first task of a launch, under
context.record: off, and in a capsule running
inference.transport: process — whose CLI owns its own conversation, so the runtime puts no
message list in front of a model and writes no record.
The record in scope is the one belonging to the task the hook is being dispatched for, which is
the context-id on task-start-event. A hook bound to on-task-start therefore reads the
conversation its task is about to continue.
Package versioning
Every murmur:* package declares an explicit @x.y.z version, so the contract a compiled
.wasm artifact was built against is readable from the component binary itself.
| Package | Version |
|---|---|
murmur:tool |
0.1.0 |
murmur:tool-registry |
0.1.0 |
murmur:capsule |
0.1.0 |
murmur:artifact-manager |
0.1.0 |
murmur:shell |
0.1.0 |
murmur:message |
0.1.0 |
murmur:task |
0.1.0 |
murmur:task-io |
0.1.0 |
murmur:conversation |
0.1.0 |
murmur:text |
0.1.0 |
murmur:hook |
0.8.0 |
murmur:runtime |
0.3.0 |
murmur:host |
0.1.0 |
murmur:runtime-guest |
0.1.0 |
| Tier | When |
|---|---|
| Patch | Doc or comment edits, no ABI change |
| Minor | A wholly new function or interface the runtime treats as optional to export |
| Breaking | Any signature change, any change to an existing record or variant, or removing or renaming a function or interface |
Every murmur:* package is pre-1.0, so the minor field is the breaking axis: a breaking change to
0.4.0 ships as 0.5.0, not 1.0.0. Only a patch moves the patch field.
Adding a field to an existing record or a case to an existing variant is always breaking,
never minor: every field and case is positional in the binary encoding, so an addition changes
the shape of every call that carries the type.
A wholly new interface goes in a new package at 0.1.0 when the package that would otherwise
host it already has published consumers. The package version is part of every instance name in
that package, so a minor bump renames interfaces the new one has nothing to do with, and every
artifact importing one of them stops loading until rebuilt. The exception is a change set that
already forces that rebuild for another reason, in which case the new interface joins the existing
package instead: murmur:runtime/tokens sits in murmur:runtime because the same bump that
introduced it took murmur:hook to 0.6.0 and rebuilt every hook regardless.
One accepted version per interface. The runtime resolves each interface by its versioned name
and nothing else — there is no compatibility fallback for an earlier version or for an
unversioned name. An artifact built against a different version fails to load, with an error
naming the version the runtime expects and a rebuild hint (mur install for a default artifact,
or a source rebuild otherwise). When a package is bumped, every artifact exporting it must be
rebuilt and republished.
An installed artifact carries the versioned interface names its own component declares in the
wit_contracts key of its <name>-<version>.meta.json; see
Local artifact cache.
mur list --contract <PREFIX> lists the installed artifacts touching a package.
Full policy in
wit/VERSIONING.md.