md

Context object

Most tools, commands, and events receive ctx.

ctx is the extension's view of the current run. It gives you what you need without handing over private host internals: current directory, UI intent, session helpers, state, model helpers, cancellation, and a few session controls.

ctx.cwd
Current working directory.
ctx.has_ui
Boolean indicating whether UI capabilities are available.
ctx.binding
Session and extension identity information when available.
ctx.ui
Host-owned UI API, or nil when unavailable.
ctx.state
Session-persisted state API scoped to the current extension.
ctx.session
Session information and semantic session helpers when bound.
ctx.ai
Sessionless AI helper API.
ctx.models
Visible model catalog helpers when bound.
ctx.signal
Cancellation signal when available; otherwise nil.
ctx.is_idle()
Return whether the active session is idle, when bound.
ctx.abort()
Request abort on the active run, when bound.
ctx.has_pending_messages()
Return whether the session has queued/pending messages, when bound.
ctx.shutdown()
Request shutdown when that capability is bound.
ctx.get_context_usage()
Return context usage information when bound.
ctx.get_system_prompt()
Return the active system prompt when bound.

Context ui api

The UI API publishes presentation intent. Extensions do not own terminal components or redraw. Say what you want the user to see; let the host decide where it fits. Recreate live UI surfaces from extension lifecycle events when sessions change.

ctx.ui.message(text, opts?)
Publish short feedback. opts.id is the dedupe/update key; opts.kind is info, warning, error, or success. The host chooses a visible, low-disruption placement.
ctx.ui.status({ id, text, value, lifetime? })
Publish compact retained state by stable id. text or value is shown; nil clears the item.
ctx.ui.progress({ id, status?, title?, current?, total?, detail?, indeterminate?, text?, lifetime? })
Publish retained progress lifecycle state. status is running, done, error, or cancelled. The host formats and places it.
ctx.ui.report({ id, title, body, format?, transient? })
Publish a readable document/report. format = "text" is the default and current supported format. body is plain text; zi owns splitting, scrolling, rendering, and preserving report presentation state by id where supported.
ctx.ui.pick({ title, placeholder?, empty_text?, options|items, timeout_ms? })
Request selection from stable options. Items may carry value, label, description, search, and static preview. Returns { status, value, item? } when submitted.
ctx.ui.prompt({ kind = "confirm"|"select"|"input"|"editor", ... })
Generic modal interaction request. Returns an envelope with status and, when submitted, value.
ctx.ui.set_editor_text(text)
Set host editor text.
ctx.ui.paste_to_editor(text)
Paste text into the host editor.
ctx.ui.clear_editor_text()
Clear host editor text.
ctx.ui.get_editor_text()
Request editor text when the host exposes that capability. Returns nil when unavailable.

Surface option tables may include placement and lifetime. lifetime is session or until_input.

Context state api

ctx.state is a per-extension session-persisted map. Use it for small choices and facts your extension owns.

ctx.state.get(key)
Return a JSON-compatible value or nil.
ctx.state.set(key, value)
Persist a JSON-compatible value for this extension's state owner.
ctx.state.delete(key)
Write a tombstone for the key.

State is scoped to the extension and active session. It may survive session changes according to the lifecycle rules in extension lifecycle. UI handles, prompts, jobs, and provider handles are live objects and should be recreated when needed.

Context session api

ctx.session.info()
Return semantic session information.
ctx.session.name()
Return the session name, or nil.
ctx.session.rename(name)
Set or clear the session name. Returns boolean success.
ctx.session.messages({ limit?, include_tools? })
Return recent semantic messages from the current visible branch. Default limit is 50; maximum is 500. include_tools defaults to true. Returned messages include durable entry_id values.
ctx.session.tool_results(tool_name)
Return recorded tool results for a tool.
ctx.session.append_note({ kind, title?, body, source_entry_id? })
Append a semantic session note. source_entry_id links the note to a durable session entry. sourceEntryId is accepted as an alias. Notes are durable custom session artifacts, not transcript mutation. Returns boolean success.
ctx.session.notes({ kind?, source_entry_id?, limit? })
Return session notes. Default limit is 50; maximum is 500.
ctx.session.label(entry_id, label)
Append a lightweight durable label for a session entry. Passing nil or an empty string clears the label. Labels have no built-in meanings, colors, or priorities. Returns boolean success.
ctx.session.labels({ target_entry_id?, limit? })
Return label entries, optionally filtered to a target session entry. Default limit is 50; maximum is 500.
ctx.session.entry(entry_id)
Return one semantic session entry by durable id, or nil when not found. Message entries return role/text fields when they map to one semantic message; labels return target_entry_id and label; extension notes return note fields.
ctx.session.entries({ label?, limit? })
Return semantic target entries by simple predicates. Currently supports label; latest label wins per target entry, cleared labels are excluded, and returned rows are target entries rather than label entries. Default limit is 50; maximum is 500.

Durable message ids compose with notes and labels. This is useful for memory that should be inspectable later, not hidden inside an extension:

lua
zi.on("message", function(event, ctx)
  local message = event.message or {}
  if message.role == "user" and message.text and message.text:match("decision") then
    ctx.session.label(message.entry_id, "decision")
    ctx.session.append_note({
      kind = "observation",
      body = "decision candidate",
      source_entry_id = message.entry_id,
    })
  end
end)

Later, an extension can query and dereference:

lua
for _, entry in ipairs(ctx.session.entries({ label = "decision", limit = 20 })) do
  local notes = ctx.session.notes({ source_entry_id = entry.entry_id })
end

Context model and ai api

ctx.models.list()
Return visible model catalog entries.
ctx.models.current()
Return the current model.
ctx.models.get(ref)
Resolve a model by id, provider/id string, or model-like table.
ctx.ai.complete(request)
Run a sessionless model completion. It does not mutate the transcript and does not run tools. Use it for side questions, summaries, or classification that should not become part of the main conversation. For provider/model registration, see providers.

ctx.ai.complete accepts either a prompt string or a table:

lua
ctx.ai.complete({
  model = "provider/model-id",
  prompt = "summarize this",
  system_prompt = "optional system text",
  max_tokens = 800,
  reasoning = "low",
})

reasoning may be off, minimal, low, medium, high, xhigh, or a boolean (true maps high, false maps off).

The result shape is:

lua
{ status = "completed", text = "..." }
{ status = "error", error = "..." }
{ status = "cancelled" }

System command helper

zi.system(argv, opts?) runs an OS command directly from an argv array. It is yieldable and should be called from command/tool execution contexts, not extension load/register code. Prefer bounded, explicit commands over shell strings.

lua
local result = zi.system({ "git", "status", "--short" }, {
  cwd = ctx.cwd,
  timeout_ms = 5000,
})

There is no implicit shell string form. Use an explicit shell when needed:

lua
zi.system({ "/bin/sh", "-c", "echo $HOME" })

Options:

cwd
Working directory.
stdin
Optional stdin string.
env
Optional string map overlaid on the inherited environment.
clear_env
Boolean. When true, only env is used.
timeout_ms
Optional timeout in milliseconds.
max_stdout_bytes, max_stderr_bytes
Optional bounded capture limits.
text
Boolean. Defaults true and normalizes CRLF to LF.

Result shapes:

lua
{ status = "completed", code = 0, signal = nil, stdout = "...", stderr = "..." }
{ status = "error", error = "...", stdout = "", stderr = "" }
{ status = "timeout", error = "timed out after ...ms", stdout = "...partial...", stderr = "...partial..." }

Non-zero exits are status = "completed"; inspect code.

Spawn helper

zi.spawn(opts) runs delegated child zi work through batch JSON mode and returns a table shaped like a tool result. This is for real delegation, not ordinary helper logic. Prefer normal tools, commands, and context model and ai api when a child agent is not required.

lua
local result = zi.spawn({
  task = "inspect the API surface",
  model = "optional-model",
  tools = "bash,read,grep",
  system_append = "extra child-agent guidance",
  cwd = ctx.cwd,
  on = {
    message = function(event) end,
  },
})

Fields:

task
Required child prompt.
model
Optional child model.
tools
Optional comma-separated child tool filter.
system_append
Optional text appended to the child system prompt.
cwd
Optional child working directory. Defaults to ..
on
Optional table of event-name callback functions.

Caveats:

  • zi.spawn is yieldable and should be called only from yieldable execution contexts.
  • callbacks in on must not yield.
  • abort forwarding depends on the execution context. Prefer spawning from tool or command work where cancellation is expected.