Skip to main content

xdog-agent

Platform scope: Linux only. Non-Linux adapters and native-platform support are outside the current product scope; historical audit evidence is not a support promise.

Agent runtime: tool calling, structured output, steering.

The agent loop that sits on top of xdog-ai: tool registration and dispatch, structured output contracts, and mid-run steering.

Install and CLI

Requires Python 3.12+ on Linux. From the workspace root use uv sync; for a standalone installation use pip install xdog-agent (or pip install 'xdog-agent[acp]' for the optional ACP adapter).

uv run xdog-agent --help
uv run xdog-agent login  # Copilot login; other provider logins use xdog-ai
uv run xdog-agent chat copilot/claude-sonnet-4.5 "Explain this runtime" --no-tools
uv run xdog-agent chat copilot/claude-sonnet-4.5 --no-tools  # Interactive on a TTY
uv run xdog-agent chat --help

Use a model from xdog-ai models <provider>; example model IDs are not an availability promise. With multiple active providers use provider/model, not an ambiguous bare ID. AI owns authentication/routing/protocol adaptation; Agent accepts an injected request adapter and does not choose fallback credentials.

--no-tools disables all tools and application capability discovery/startup. Without it, the CLI creates default bash/filesystem/current-time tools, injects web search, and opens installed application capabilities (which may start owned resources such as the Eyes daemon). Neither CLI tool availability nor SDK registration is an approval gate or OS sandbox. Use only in trusted environments; SDK hosts must install their own before_tool_call policy. --tool-ctx accepts a JSON object or @file; it does not enable submit_result.

SDK quick start

import asyncio

import xdog.ai as ai
from xdog.agent import Agent, AgentConfig, stream_fn_from_provider

async def main():
    # Provider construction is local; prompting needs an existing login and network.
    async with Agent(
        stream_fn_from_provider(ai.provider("copilot")),
        config=AgentConfig(model="claude-sonnet-4.5", system_prompt="You are helpful."),
    ) as agent:
        stream = await agent.prompt("Explain the distinction between a tool and a skill.")
        async for event in stream:
            print(event.type)
        await stream.result()  # Completion only; Agent history is on the Agent.
        print(f"Retained messages: {len(agent.messages)}")

asyncio.run(main())

Bare Agent(...) installs no bash/filesystem tools and discovers no capabilities. It adds web_search/embed only when their functions are injected. Skills and MCP are also explicit constructor inputs. AgentConfig.tool_execution defaults to "parallel"; tool preparation/finalization hooks run serially. The permission hook sees the actual selected tool; transformed arguments must still pass the host's policy before execution. Generic tools are not a security boundary.

Consume the event stream while the producer runs: the default event buffer is bounded (256 events), and awaiting only stream.result() can stall on backpressure. For Agent.prompt(), read retained history from agent.messages or the terminal AgentEndEvent.messages, not the stream result value. agent.subscribe(listener) observes events and returns a disposer; listener callbacks and optional SnapshotEvents are not durable commit ACKs. Always settle/close the Agent on its owning loop before disposing host resources.

Capability SDK quick start

Use the repos and capability API when building a host that needs installed abilities such as Flow. Install xdog-agent and xdog-flow from the same matching release; for the current source checkout, run uv sync at the workspace root.

This complete example uses an offline model adapter. It opens only Flow, never runs a workflow or starts Eyes, and needs no provider credentials:

import asyncio
from pathlib import Path
from tempfile import TemporaryDirectory

from xdog.agent import AgentConfig, BeforeToolCallResult, SkillRepo, ToolRepo
from xdog.agent.capabilities import CapabilitySet
from xdog.ai.types import AssistantMessage, TextContent
from xdog.ai.utils.event_stream import EventStream


def offline_stream(model, context, options):
    return EventStream.empty(
        AssistantMessage(content=(TextContent(text="Offline SDK example"),))
    )


async def deny_tools(context, cancel):
    return BeforeToolCallResult(block=True, reason="This example does not execute tools")


async def main():
    with TemporaryDirectory(prefix="xdog-sdk-example-") as directory:
        workspace = Path(directory)
        # Built-in instances can be narrowed; no custom Registry or Manager.
        host_tools = ToolRepo(cwd=workspace).select(["current_time"])
        # Packaged skills are discovered by default. The optional host directory
        # is registered without being created or executing its contents.
        host_skills = SkillRepo().register(directory=workspace / "skills")
        catalog = CapabilitySet("application")

        async with catalog.select(["flow"]).open() as capabilities:
            async with capabilities.create_agent(
                offline_stream,
                config=AgentConfig(model="offline"),
                tool_repo=host_tools,
                skill_repo=host_skills,
                before_tool_call=deny_tools,
            ) as agent:
                assert {tool.name for tool in agent.local_tools} == {
                    "flow", "skill", "current_time",
                }
                assert agent.active_skills == ()
                stream = await agent.prompt("Describe the available capabilities.")
                async for _event in stream:
                    pass
                await stream.result()
                print("SDK capabilities:", sorted(tool.name for tool in agent.local_tools))
        # Agent closes before the selected capability resources.


asyncio.run(main())

The passed repos are additive: omitting Flow from either host repo does not remove Flow's own contributions. Use register(actual_tool) for extra host instances and register(directory=path) for host skills; no tool/skill enable lists or custom skill-manager subclass are needed. Different same-name tool or skill definitions fail rather than silently overriding each other.

Skills are discoverable, but their bodies are not injected by default. The read-only skill tool loads instructions on demand. Repo selection and skill text never grant execution permission. For a live model, replace offline_stream with the configured adapter from the preceding example and supply the host's real permission policy. The Flow CLI does not inherit an Agent host's sandbox or approval hooks; do not grant general workflow execution to an isolated author without an appropriate execution boundary.

See the capability lifecycle and restoration guide for multi-owner composition, shutdown and snapshot restore.

Host-owned tool inventories

from pathlib import Path
from xdog.agent import ToolRepo
from xdog.agent.tools import create_submit_result_tool

repo = ToolRepo(cwd=Path.cwd())  # Real bash/filesystem/current_time instances.
selected = repo.select(("current_time",))  # Same actual executor, new subset.
receipt = selected.register(create_submit_result_tool())
# agent is an existing idle Agent on the host's execution owner.
selected.publish(agent)
receipt.close()  # Withdraw only this registration, not a later replacement.
selected.publish(agent)
Operation Contract
ToolRepo(*tools, builtin=True, cwd=None) Actual tools plus fresh bash/filesystem/current_time defaults. builtin=False starts empty.
register(*tools) Atomically register actual AgentTool objects; same-name conflicts fail. Returns a close receipt.
unregister(name) Explicitly remove a name before registering its replacement; an old receipt cannot remove the replacement.
names() / get(name) A frozenset of names / the actual tool; missing get raises ValueError.
select(names) / merge(other) New subset / union preserving actual identity; conflicting objects fail, never silently replace.
tools / publish(agent) Tuple inventory / explicit publication using agent.set_tools(), without broadening active selection.

The host owns resources and publication timing; removing a binding does not close a tool's resources or undo its effects. Capability tools join this ordinary inventory through the same SDK. MCP stays separate: pass a host-owned McpRegistry directly to Agent; do not bind its proxies into ToolRepo. local_tools is the ordinary inventory; state.tools is the combined inventory, and active_tools is its effective selection. Use set_active_tools(names, expected_revision=agent.tool_selection_revision) for an idle, revision-checked selection (None means all, [] means none). Extensions contribute hooks, commands, UI and providers, not model tools. See registry reference and the MCP lifecycle example.

Generic structured results

submit_result is an optional Agent tool, not a Flow-specific parser or an Agent constructor option. Opt in with from xdog.agent.tools import create_submit_result_tool, then register an actual instance or pass it directly to Agent. Inject result_schema and result_sink through tool_ctx:

from xdog.agent import Agent, AgentConfig
from xdog.agent.tools import create_submit_result_tool

async def collect_result(stream_fn):
    sink = {}  # A new caller-owned sink for this collection.
    schema = {
        "type": "object",
        "properties": {"summary": {"type": "string"}},
        "required": ["summary"],
        "additionalProperties": False,
    }
    async with Agent(
        stream_fn,
        config=AgentConfig(model="copilot/claude-sonnet-4.5"),
        tools=[create_submit_result_tool()],
        tool_ctx={"result_schema": schema, "result_sink": sink},
    ) as agent:
        stream = await agent.prompt("Summarize the task and call submit_result with {summary: ...}.")
        async for _event in stream:
            pass
        await stream.result()
    if "result" not in sink:
        raise RuntimeError("The model did not submit an accepted result")
    return sink["result"]

Here stream_fn must route the qualified model (for example an adapter over ai.load()). JSON Schema objects with type/properties are validated with fastjsonschema; the legacy flat form {"summary": "string"} requires declared fields and permits extras. With no schema, any JSON value is accepted, including arrays/scalars/null. Validation errors return Error: ... so the model may fix and resubmit. The old flow_output_schema / flow_result_sink keys are rejected.

The tool writes sink["result"] only after validation; use a shared nested dict because ToolDef dispatch shallow-copies the outer context. Without a dict sink, acceptance does not retain a value for the caller. Acceptance does not stop the Agent, guarantee submission, or mean durable task completion. Later successful submissions replace that key; a failed submission does not erase an earlier one. Hosts decide when to accept/persist a result and whether the overall run succeeded.

Skills and Capability SDK

SkillRepo(builtin=True, packaged=None) discovers installed packaged skills by default. SkillRepo(builtin=False) starts without that catalog; an explicit packaged mapping pins chosen packaged sources. Add host directories with skills.register(directory=path, name="host") (chainable). Reusing a directory name with a different path or adding a different same-slug definition fails; there is no implicit shared/group override priority. from_packaged, select and merge are provided by the SDK, not a host-specific Manager.

Discovery exposes an index, not active bodies or permissions. Native Agent still accepts skills=skills and tools=repo.tools. Prefer the explicit read-only create_skill_tool(skills, allowed_slugs=("plan",)) entry for on-demand list / load: it returns metadata/instructions as tool results, without executing them, activating bodies or changing permissions. Its allowlist filters that tool, not an independently supplied Agent skill index.

Explicit host active_skills / set_active_skills still control session/turn body placement. Invalid activation fails; session bodies stay in the cacheable prefix and turn bodies retain their pending/consumed lifecycle. Hosts may prepare SkillPlacement.read(skills, slugs) on owned IO workers and pass prepared_skills. Flow, Eyes and Goal packaged bodies are not activated by default.

xdog.agent.capabilities exports ApplicationPort, CapabilitySet, CapabilityAssembly and discover. Configure repositories and select providers before opening:

from xdog.agent import SkillRepo, ToolRepo
from xdog.agent.capabilities import CapabilitySet

selected = CapabilitySet(
    "application", tool_repo=ToolRepo(), skill_repo=SkillRepo()
).select(("flow",))
async with selected.open() as assembly:
    async with assembly.create_agent(stream_fn, config=config) as agent:
        async for event in await agent.prompt("Describe the available tools"):
            consume(event)

Here stream_fn, config and consume are supplied by the host; see the complete offline example for a standalone program. Discovery is inert; .open() starts only selected providers. It supplies an ApplicationPort by default for application scope; session/group scope requires the actual owner port. Advanced hosts may still explicitly start(host) / aclose() and combine Sets via CapabilityAssembly(*sets).

assembly.create_agent(stream_fn, tool_repo=..., skill_repo=..., config=...) returns a native Agent. The repo arguments are additive union, not replacement or filtering of selected capability contributions. Empty supplemental selections cannot remove those contributions. Conflicting names fail. Assembly does not accept tools / skills or separate enable lists; the underlying Agent retains its original tools / skills parameters. compose_tools(existing) and publish_tools(agent) likewise have no second filter argument. A read-only skill entry is supplied for visible skills unless the host already supplied one.

Assembly is not a runtime, permission layer or another model loop. Hooks, active-tool selection, parent authority and resource ownership remain host-owned. Close Agents before their shared Sets. Eyes binding close detaches without stopping the daemon or cancelling registrations. Flow CLI execution does not inherit host sandbox, approval hooks or tool allowlists; a workflow remains trusted code. Reinject current adapters, repos and permissions before restoring; restore_agent / restore_agent_parts retain saved skill lifecycle semantics.

Snapshots, compaction and cancellation

  • agent.dump() returns versioned JSON at an idle, fully settled checkpoint; the host writes it. restore(snapshot, allowed_models=...) validates then atomically adopts data without model/tool execution. Saved model IDs, tool names or skill text are not authority. Live registries, connections, tool context/sinks, parent handles, permission hooks and compaction policy must be supplied by the host, never reconstructed from snapshot instructions.
  • dump_metadata() plus separately encoded messages / restore_parts() supports long retained histories without the whole-document limit; individual parts and complete tool-call/result pairing are still validated. See snapshot reference.
  • dump(recovery=True) is diagnostic, marked non-resumable, and does not authorize replay of uncertain effects. Unconfirmed tool work requires reconciliation. AgentConfig(emit_snapshots=True) enables immutable safe-boundary observations, not persistence barriers or automatically saved recovery files.
  • Generic automatic compaction defaults off. Opt in with AgentConfig(compaction=CompactionConfig(auto=True, ...)); capacity/reserve must come from the executing adapter's real metadata or explicit trusted host limits, not the historical AgentConfig.context_window default. Manual await agent.compact(instructions) requires idle, paired history and does not enable auto mode. Summary/cleanup failure retains original history. Compaction is context management, not a token/lifetime task budget or durable archival; the host owns archival and persistence.
  • abort() is a cooperative cross-thread stop request. On the owning loop, await agent.cancel_and_wait() cancels and joins the producer and hook cleanup; wait_for_idle() only waits. Always use await agent.aclose() (or async with) for owned Agent cleanup; borrowed providers and MCP registries remain host-owned. Cancellation cannot kill blocking/native or cancellation-suppressing code, revoke approval, roll back effects or justify automatic replay.

Child execution belongs to the application, not this SDK. ChildClient, ChildHandle, ChildBackend, ChildSpec and injected ParentHandle are inert contracts/data access. Coding and Claw own their respective child runtimes, stores, workers and grants; there is no Agent ChildSupervisor or SDK child loop. Do not infer child permission from role labels or restored records. See child contracts and host behavior.

Detailed reference

The following sections retain protocol limits and useful host API usage. Mentions of Coding/Claw describe those applications' adapters, not services automatically created by importing or constructing Agent.

Identified steering and follow-up inputs

For interactive clients, queue_input(id, text, "steer" | "follow_up") returns an immutable QueuedInput. pending_inputs() snapshots its lane and selected state; change_input(id, "edit" | "remove" | "move", ...) acts only before a checkpoint selects it. Move direction is -1/+1 within its lane. All these operations belong on the agent's owning event loop; GUI threads must marshal requests to that loop. Existing steer() and follow_up() APIs remain unchanged.

InputConsumedEvent is emitted only after admission to agent history, not on acceptance/dequeue. Selected but unadmitted inputs remain recoverable. After a run settles, drain_inputs(prefix="") returns/removes unconsumed identified inputs; a prefix scopes cleanup without removing other owners or legacy inputs. Persistence remains the caller's responsibility. IDs/receipts do not replace pre-tool hooks, grant permission, or provide OS isolation.

Separate MCP registry and Agent integration

xdog.agent.mcp.McpRegistry owns configured stdio/HTTP connections on one background asyncio loop, so SDK callers can use proxies across per-turn event loops. Declaring servers does not launch commands. connect, refresh, disconnect, status, tools and aclose are the explicit lifecycle surface; synchronous close() requests owned cleanup without blocking terminal restoration. Hosts own command trust, idle tool-set changes and permission hooks.

MCP proxies do not enter ToolRepo. Bind the host-owned MCP registry to Agent(mcp_registry=registry) or agent.set_mcp_registry(registry). local_tools is the ordinary inventory; state.tools and active_tools are the complete visible inventory used for UI, permissions and execution. set_tools() replaces only ordinary tools. Registry connections remain explicitly host-owned, and closing an Agent does not close a borrowed registry (for example a Claw group's original MCP owner).

from xdog.agent import Agent, ToolRepo
from xdog.agent.mcp import McpRegistry

ordinary = ToolRepo(cwd=workspace)
mcp = McpRegistry(cwd=workspace)
mcp.register("service", {"command": "/trusted/path/server", "args": []})
agent = Agent(stream_fn, tools=ordinary.tools, mcp_registry=mcp)
# Registration is inert. Connect only under the host's explicit trust/policy.
await mcp.connect("service")
agent.sync_mcp_tools()  # No IO; each model request also snapshots the current inventory.
# The host closes Agent before its own MCP registry.
await agent.aclose()
await mcp.aclose()

Duplicate local/MCP names fail without overriding either source. Explicit active selection never grows when a remote tool disappears/reappears. A child uses an explicitly granted McpRegistry.from_tools(...) manifest rather than the parent's unrestricted connection registry. A stale proxy cannot borrow a refreshed spec or replacement connection. Conversation restoration never constructs connections, loads credentials or automatically invokes tools.

Configuration is {server_id: {command, args, env, timeout, allow_tools}}. Commands are argv (no shell); cwd is host-selected. IDs are bounded ASCII names, metadata/status never exposes configured environment values, and unsupported transports fail visibly. allow_tools is host policy, not a protocol annotation. The current discovery probe uses MCP 2026-07-28 request metadata and server/discover; recognized version rejection is not treated as a legacy peer. Only method-not-found triggers legacy initialize/initialized for supported 2025-11-25/2025-06-18/2025-03-26/2024-11-05 versions. Sampling and autonomous model execution are not supported. Elicitation and MCP Tasks are also removed; only explicitly configured Roots disclosure is retained for owned requests.

Paginated tool discovery validates identities and object JSON Schemas (Draft 2020-12), rejects external schema references instead of fetching them, and produces stable namespaced aliases capped below provider tool-name limits. Input and advertised structured output are validated. Text/images and inert resource data are adapted to the existing AgentTool interface; linked resources are not fetched. Server execution errors and unsupported content are visible errors, not silent success/drop. Tool annotations never authorize execution.

Requests are correlated/multiplexed with bounded frames, stderr is separately bounded/drained, timeout/cancellation fences late responses, and EOF/malformed streams disable the connection. Legacy cancellation is best effort, not rollback; current stdio never sends forbidden unsolicited cancellation notifications. Neither calls nor connections automatically replay/reconnect. Owned child/process groups are terminated/escalated/reaped at disconnect/shutdown. Connection/schema mutation must occur while the host's agent is idle. HTTP and inert context previews and explicitly reviewed public-PKCE OAuth are described below. Removed input/task capabilities are not a promise of future model/callback delegation.

Explicit HTTP MCP connections (current and legacy)

A server can use {"transport":"http","url":"https://host/mcp"} instead of stdio argv. Connections still start only on explicit host management. The current 2026-07-28 HTTP path sends matching per-request protocol/client metadata and HTTP Mcp-Method, Mcp-Client-Info, MCP-Protocol-Version, tool Mcp-Name, and valid x-mcp-header argument mirrors. Modern discovery reads supportedVersions and server identity from _meta, not the legacy protocolVersion result shape. This also corrects the initial stdio discovery implementation/fixture from pass 11.

JSON and response-scoped SSE are decoded with bounded line/event/body sizes and correlated IDs. UTF-8 may cross network chunks; multiline data and CR/LF delimiters are supported. Incomplete/oversized/mismatched/current-unsolicited responses fail visibly, never become successful tool data. Header mirrors validate properties-only primitive paths, case-insensitive uniqueness and integer range, encode unsafe or ambiguous values with the prescribed base64 sentinel, and retain arguments in JSON. An invalid header tool is rejected with status diagnostics, not silently registered.

Legacy Streamable HTTP fallback distinguishes non-protocol HTTP rejection from recognized modern negotiation/unknown-method errors. Legacy initialize/initialized, protocol/session headers, optional GET notification stream, best-effort cancellation and owned session DELETE are supported. Session expiry requires explicit reconnect; POST tool calls are never replayed. Cursor-based legacy SSE resumption is supported as described below; this is not autonomous connection recovery or POST retry.

HTTP endpoints are explicit and reject URL userinfo/fragments/reserved protocol header overrides. TLS is default; non-loopback plaintext requires explicit allow_insecure_http: true. headers supplies static headers and headers_env maps header names to environment-variable names read at connect. Credential values are absent from status/errors. No redirects, ambient proxy inheritance, resource fetches, token refresh or OAuth/browser activity occurs automatically. 401/403 and redirects are explicit failures without downgrade/credential forwarding. Interactive OAuth uses the explicit public-PKCE surface below, never an automatic auth retry.

Configured MCP Roots and retained transport continuations

Roots are configured disclosure metadata only. A server config may include "roots": [{"uri":"file:///absolute/workspace","name":"Workspace"}], at most 16 validated local absolute file URIs. Nothing enumerates the filesystem, infers Roots from cwd, grants tool access or creates an OS sandbox. Explicit connect admits that configured disclosure to the selected peer; disconnect withdraws it. There is no mutable Roots picker, enable/replace review or Roots-change UI.

Legacy roots/list answers stay within the configured connection ceiling. Modern 2026-07-28 input_required responses remain supported for owned tools/call, prompts/get and resources/read: only configured Roots and bounded opaque requestState continuations are accepted. Original request arguments and identity remain fenced; continuation rounds, embedded inputs, bytes and deadlines are bounded. An unsupported sibling input rejects the round before any partial Roots disclosure. Disconnect/cancel settles the original owner, not a successor.

Sampling, Elicitation and MCP Tasks are removed from SDK/native/gateway registration, capability advertisement and review/control surfaces. Legacy requests are refused; modern embedded unsupported families/task augmentation fail closed, with no callback, model/tool work or partial completion. The former Sampling/Elicitation/Tasks/Roots-management keys are not replacement controls. Basic extension input/select/confirm and provider OAuth callbacks are separate retained interfaces, not MCP Elicitation.

Ordinary modern Roots/request-state continuations do not authorize retrying an uncertain operation. Active resource/catalog/combined subscriptions are removed. Ordinary response-scoped SSE and request telemetry remain. Legacy cursor-based GET recovery retains the original identity/deadline and never repeats a tool POST; current stateless HTTP does not acquire legacy GET resumption. No disconnect, failed response or continuation silently reconnects, replays POSTs or expands configured disclosure.

Inert MCP resources and prompt previews

McpRegistry.catalog(server, kind) accepts resource, template, or prompt and validates bounded pagination/identities/arguments. Resource-only and prompt-only peers can connect without advertising or requesting tools. resource(server, uri) and prompt(server, name, arguments) return frozen McpDocument data; they do not call a model, fetch linked URIs or insert messages. HTTP reads/gets reuse the owned transport and URI/name header mirrors. Prompt arguments are named strings, with required/unknown names checked against the catalog. Deferred/interactive results and invalid roles/content fail explicitly.

Text previews quote resource and user/assistant prompt roles as untrusted data. Binary blocks retain base64 and MIME metadata for raw export, but draft_text() refuses binary rather than silently dropping it. This is not model image-resource insertion or automatic prompt execution. Current host browsers expose explicit connection/catalog/read/preview/draft/export gestures; connection/schema changes still use host idle ownership and existing tool permission policies.

Cursor-based legacy SSE recovery

Legacy HTTP can resume an interrupted POST response (including initialization) with GET to the same configured endpoint using its server-issued Last-Event-ID. The original integer request ID, issued session ID, requested/negotiated legacy protocol version and absolute request timeout remain in force. A GET retrieves that response; it never sends another tools/call POST. Concurrent requests own independent cursors. Optional legacy notification GET streams have separate cursor ownership and bounded resumption.

Only complete SSE events commit a cursor, including empty priming events. A partial successor cannot skip unreceived content. UTF-8 IDs (up to 1,024 bytes), byte-split CR/LF/CRLF, NUL-ID ignoring and empty-ID reset are handled explicitly; unsafe HTTP-header controls/oversized IDs and invalid JSON/identities fail. Server retry intervals are bounded to 50 ms–5 s; there are at most three recovery GETs. No cursor/reset, exhaustion, authentication failure, redirect, unavailable stream or expired session fails visibly without replay, credential forwarding or automatic initialize/reconnect. After a notification stream exhausts, disconnect/connect explicitly to restart it. Remote effects cannot be rolled back by cancellation.

Current stateless HTTP does not use GET resumption. Request-scoped progress/log notifications are tolerated as telemetry, not tool/model content; unrelated server requests/notifications remain errors. Current resource reads mirror the URI in Mcp-Name (not an invented Mcp-Uri). Cancellation/disconnect closes owned reads and bounded waits, and existing Coding approvals/Claw allow_tools gates remain unchanged. Real third-party replay retention behavior remains unverified.

Task planning skill and host access scope

plan, supplied by optional xdog-skills, is discovered (not activated) through the normal packaged-skill mechanism for objective/scope/dependency notes, acceptance criteria and evidence-backed progress. It grants no permissions, creates no parallel task database and starts no second Agent loop. The former xdog.agent.planning/PlanOwner/PlanState, dedicated plan inspector and update_plan tool are removed.

xdog.agent.access supplies the separate host-owned AccessState/AccessScope. Coding/Claw expose /readonly on|off|status as native user controls, never model or extension tools. Restriction changes persist before publication and require idle ownership. Only explicit user release can clear read-only scope; release leaves ordinary permissions, MCP allow-tools gates and child access unchanged. Saved access/legacy planning state is decoded fail-closed, not turned into executable plan commands or restored tool grants. Skills/task text cannot release the scope.

SDK embedders still supply pre-tool policy. The native read-only allowlist is local filesystem read/ls/grep/find and current time; shell, external/extension and child control calls are not inferred safe. Permission gates are not an OS sandbox.

Cancellation-safe owned work

xdog.agent.async_work contains lightweight shared primitives, not another runtime or permission layer:

  • run_owned(callable, *args, **kwargs) offloads synchronous work with caller contextvars. Cancellation waits for the actual thread and consumes a late failure; a canceled delivery is never replaced by a successful return value.
  • wait_owned(future_or_task) joins an already-owned Future/Task without propagating caller cancellation to it. Its actual error takes precedence; otherwise deferred caller cancellation is raised after successful settlement.

These contracts intentionally differ. Keep locks, capacity slots, writer leases, transaction receipts and the single shared close task at the calling owner until its helper settles. Neither helper retries, replays effects, creates an Agent, changes tool permissions, or owns a service lifecycle. xdog.agent.child_transfer.settled_image_work(work=...) retains its one-argument signature; the retired module import path is not retained.

Explicit extension process owners

xdog.agent.extensions.ExtensionAPI exposes the trusted Python registration API; xdog.agent.extensions.runtime.ExtensionRuntime owns activation, command proxies, events, hook transforms, declarative UI and disposal. Extensions no longer register model tools; installed tool contributions belong to Capabilities and the host-owned Agent ToolRepo SDK. It reuses MCP's bounded owned stdio process and manager loop primitives, with private extension methods; it is not a hosted control plane, new agent loop, or OS sandbox. Worker startup uses a trusted absolute bootstrap with safe-path mode, relative extension imports get isolated package identities, and settings cross the owned pipe rather than process argv. Python code can access the same filesystem/network as the user. Hosts must supply explicit trust, enforce their permission gates AFTER transformed arguments, and route structured UI to their own event loop instead of letting an extension write raw terminal control sequences. See client READMEs for the v1 scope and the remaining broader extension API gap.

Plugin terminal key/paste/release interception and observation are not supported. There is no on_terminal_input registration, terminal callback worker protocol or terminal event capability. Native input, shortcuts and approval focus stay owned by the clients; submitted-input hooks remain separate and do not receive raw keystrokes.

The private extension-worker RPC protocol, MCP JSON-RPC and ACP transport are retained. They are distinct from Coding's removed --rpc JSON-lines frontend; there is no Coding frontend draft/editor/custom-UI or progress-receipt API.

Durable extension entries and context hooks

Extensions can now call api.append_entry("note", {"text": "durable data"}) from an active callback. Host-owned JSON entries are bounded, independently namespaced per extension and committed before the callback receipt is returned. Subsequent callback context contains that extension's context["entries"]; unload/reload retains it. Coding session/branch activation follows its selected durable snapshot; Claw only rotates its current context, not process globals. Old owners cannot publish entry writes after navigation. Close invalidates late writes, so shutdown callbacks should not append entries.

Register api.context(callback) to transform the ephemeral message list before EACH model invocation. The callback receives serialized typed user/assistant/tool content plus its entry snapshot and returns a message list. It may inject summaries or filter complete history batches, but does not edit the canonical transcript, system prompt, static cache prefix, host read-only scope or tool permissions. Coding also composes an existing SDK transform before its extension pipeline.

def setup(api):
    async def remember(args, context):
        api.append_entry("note", {"text": args})
        return "Saved entry"
    async def model_context(messages, context):
        notes = [row["data"]["text"] for row in context["entries"] if row["kind"] == "note"]
        if notes:
            messages.append({"role": "user", "content": [
                {"type": "text", "text": "Context note: " + notes[-1]}]})
        return messages
    api.command("remember", "Persist a note", remember)
    api.context(model_context)

Unknown/system roles, unsupported parts, invalid images/signatures and incomplete/ invented tool-result batches fail explicitly, not by silently dropping content. No-op transforms preserve original typed messages. Images and signed thinking/tool parts survive chunked full-context transfers beyond the individual extension-worker RPC frame limit. Transfers are checksummed, order/identity fenced and spooled off-loop, with a 128 MiB explicit context limit, 64 KiB blocks and an absolute whole-transfer deadline.

Entry capacity is 128 entries across a session, 16 KiB per entry and 512 KiB total. Invalid/corrupt stored state fails instead of quietly resetting it. Persistence failure keeps the previous snapshot. Cross-loop cancellation settles committed metadata/file ownership before the host releases its lifecycle reservation; it cannot roll back an already completed write. Context-hook failures disable the extension and do not trigger another model call or automatic replay.

Durable entries and context hooks do not grant editor/session mutation, raw terminal interception or custom-layout authority. Registered shortcuts, basic dialogs, rich results and session-owned providers have their separate contracts below. Extension code is still trusted code with the user's privileges, not an OS sandbox.

Interactive extension dialogs

Active extension callbacks now support await api.select(title, choices), await api.input(title, message="", default="") and await api.confirm(title, message). Optional timeout is bounded to 0.1–600 s. Select/input return None on cancel/no UI; confirm returns False. Cancel answers this dialog only—it is not tool approval, permission revocation or automatic turn submission. Trusted extension code decides what to do with the answer.

The worker suspends the callback and returns an owned pending-callback/dialog receipt. The host displays a focused basic dialog, then resumes that SAME callback with the validated answer; it does not rerun the callback to obtain the answer. Multiple sequential dialogs work. Without an interactive host, canceled values are returned immediately rather than hanging unattended execution.

Basic dialogs sanitize control codes, scroll long details, preserve the main editor draft and input cursor, and require a rendered visible question/action before Enter can answer. Select/confirm pastes do not submit; tiny/resize/release/repeat/unpainted selection input cannot confirm. Escape/Ctrl+C cancel the dialog. Tool approval and output review retain higher focus priority. An affirmative extension confirmation never grants the underlying agent tool permission or switches host read-only scope.

Dialog IDs are host-generated and scoped to their session/request/run/connection. Timeout, abort, navigation, disconnect and shutdown settle/cancel pending futures; late/foreign answers cannot resume a successor's callback. Claw command waits are connection-owned background jobs so the socket can still process UI replies and other controls while the group remains reserved. Context-hook dialogs still share the context transfer's absolute deadline; the UI does not renew model/transport budgets. Callback effects remain trusted code, not OS-enforced isolation.

These are basic input/select/confirm dialogs, not plugin forms or a layout API. Custom renderers, nested dialogs, extension overlays and terminal interception are removed. Registered command shortcuts and submitted-input hooks remain separate; none of these interfaces grants tool permission or full Pi API compatibility.

Session metadata and separately retained history

Agent.dump() / restore() remain versioned whole-document snapshots with their existing protocol size bound. Hosts retaining long histories can instead save agent.dump_metadata() plus separately encoded message fragments, then call agent.restore_parts(metadata, messages, allowed_models=...) on a fresh Agent. xdog.agent.snapshots.encode_snapshot_messages / decode_snapshot_messages retain string user messages, timestamps, signatures and full tool pairs without a whole-history lifetime byte cap. Metadata and each message header/content part remain bounded JSON; unconfirmed work, invalid pairing, changed model/skill bindings and mismatched compaction summaries still fail closed. Export requires a safe checkpoint. Neither path stores live providers/tools/credentials or starts a model. Actual storage, owner scope and runtime cleanup remain host responsibilities.

Child SDK and host-owned runtimes

xdog.agent.children provides inert ChildBackend, ChildClient, ChildHandle and ChildSpec contracts. Shared configuration, policy and data helpers live in xdog.agent.child_*; Agent owns no child execution loop, store or extension worker. Coding's CodingChildRuntime owns its host-local loop, while Claw's ClawChildRuntime borrows the original application loop. Host child ports expose these execution owners as children.runtime. Each host factory supplies a fresh Agent and MUST implement host permission/read-only policies, independent child history/system prompts and explicit task handoff. Coding/Claw provide permission-aware factories and /agents commands. The SDK alone does not grant access or provide a UI. No generic access string is an approval or a sandbox.

In the two shipped hosts, a safely completed child can receive an explicit new message: its history/metadata are rebound to a fresh authorized Agent, only the new input executes, then the Agent and its workers are closed again. Coding's child loop/thread retires at idle; Claw never stops its borrowed gateway loop. This is not automatic replay of interrupted, failed or unconfirmed effects. See docs/child-continuation.md for acceptance and legacy-record limits.

The shipped hosts expose one model-facing child tool, agent: use action="spawn" with task and optional access/inherit_context/config; use list, presets, inspect, steer, follow-up, cancel, wait, close or queue with the applicable id/text. list and presets require no child ID. Spawn-only fields cannot be mixed into control actions, and caller-supplied ownership is rejected before permission hooks. The previous split tool names are not exposed; old transcript calls/results remain historical data and are never rewritten or replayed. An explicitly saved old tool selection is not automatically widened to the merged tool: reselect deliberately.

Main-Agent agent(action="queue", id=..., text=JSON) and host children.runtime.control("queue", ...) retain the owner-scoped queue/recovery API. Edit/remove/move/return, explicit recover/recover_images/recover_task, and separate durable discard remain guarded by revisions, selected-input/byte limits, write-ahead journals and caller permission hooks. The main-Agent wrapper returns images as typed ImageContent with bounded receipt metadata, not raw base64 text. Native child queue panels expose inspection only; they cannot mutate or recover those records. Restoring records/copying recovery never restarts child execution.

There is no child-runtime-imposed elapsed-time deadline, model-call cap, cumulative reported-token budget, fixed concurrent-child limit or retained-record count limit. The main Agent/host owns lifecycle and resource management through cancel/wait/close; reported calls/tokens are usage counters, not stop thresholds. Actual model output and context ceilings still apply. Children cannot recursively delegate through the agent tool, or change the parent plan.

Protocol capacities remain: 32 identified queued inputs, 16 KiB text per input, 512 KiB single-message text capacity, image/retained-byte limits and per-file record limits. Complete history is not killed at a cumulative 512 KiB or 2,000 messages: the host-owned ChildStore splits large histories into immutable linked chunks and atomically commits the record index after those chunks are durable. It neither silently clips history nor drops half a tool batch. Missing/corrupt chunks fail visibly rather than returning a partial history. Persistence callbacks and all parallel tool siblings settle before lifecycle completion; failed callbacks are visible. Transcripts carry a checkpoint index; restored child history with incomplete tool batches is rejected before making a request. Synchronous close() requests disposal; await aclose() settles the owner thread.

Generic Agent auto-compaction remains off by default (CompactionConfig.auto=False). Constructing a ChildClient alone does not enable it. Coding and Claw child factories explicitly pass AgentConfig(compaction=selected.compaction(child_stream)), where child_stream = selected.stream(actual_adapter), and wire archive_compaction=store.archive_compaction into their child runtime. Adapter wrappers preserve the executing adapter's live resolve_model metadata. The helper opts into the existing built-in policy using real context/prompt/output capacity; it returns None if metadata, context capacity or an output reserve is unknown. That case is not auto-compaction: a host requiring it must reject the start or provide trusted capacity/reserve explicitly through CompactionConfig(auto=True, ...), not invent a fallback window. No separate set_compaction hook or second Agent loop is needed.

For supervised auto-compaction, archive_compaction(spec, generation, messages) is a synchronous durable callback. The child runtime waits for its owned thread work to persist the complete paired original history before the native summary request starts. The hosts’ ChildStore.archive_compaction implements this callback; store.compaction_archives(child_id) reads the retained full snapshots without replaying work. The built-in planner/summary/commit policy remains authoritative: archival, summarization or stream-cleanup failure does not commit a history cut; cancellation waits for owned archival/cleanup. A missing callback fails explicitly when compaction is needed. The native summarizer's per-request timeout and capacity checks still apply; these are not child lifetime budgets. Current inspected history may contain a summary, while the complete pre-compaction snapshots remain archived.

Agent.set_stream_fn() replaces an idle request adapter. Owner-loop-only await agent.cancel_and_wait() cancels and joins the producer, including hook cleanup; abort() remains the normal cooperative cross-thread stop request. Async cancellation cannot terminate blocking/native or cancellation-suppressing code, revoke approvals, or roll back tool effects.

Factories may be asynchronous; the child runtime adds no deadline to factory preparation or the child run. Cancellation still waits for owned preparation/cleanup to settle. Optional host admission/start/finalization/event/forget callbacks support stable snapshots, atomic interruption markers, extension cleanup and durable catalogs. idle_scope() freezes child admission throughout a host's idle-only context/policy change. ChildStore scopes records by session/branch; loading never resumes work. Both clients retain parent validation/context authority while independently owning child extension workers, granted tool executors and entries. Focused child UIs and independently owned select/input/confirm child dialogs are now wired in both applications. Task-specific labels/instructions, tool subsets and fresh context are applied through validated ChildConfig. Per-child model/reasoning is now wired through the actual request adapter; named specialist presets are now wired too; complex extension forms/renderers are removed.

The host-local ChildExtensions captures the origin UI binding at admission, creates an independent per-child DialogBroker, and fences replies by child/session/lease identity. Detach returns canceled values to pending and subsequent questions, without replay/adoption. Inherited parent validation still runs under silent_extension_ui(); it never borrows a child's or parent's unrelated question reservation. Actual child callback questions are UI answers, not host tool approvals or host access changes.

ChildConfig is frozen and validated independently of UI policy. children.runtime.start accepts a bounded configuration dictionary; the factory receives its corresponding ChildSpec, and the child runtime enforces the tool subset and message/input protocol capacities. Actual per-request model output/context limits remain separate from child configuration; there is no cumulative request/token budget. Specialist instructions compose with parent restrictions using child_prompt(). Unsupported tool names fail before a model request; zero selected tools is an empty executable set. parse_child_start() supports explicit lexical flags without shell execution and preserves the task suffix. Configurations persist/restore without work replay. The removed timeout, max_model_calls and cumulative max_tokens keys (and CLI --timeout, --calls, --tokens) are rejected for new starts/presets. Historical child records can still be read: their old budget fields are inert and do not resume work or reapply a budget. These settings never provide approval, a second tool loop, sandboxing, automatic provider activation or model fallback.

Provider adapters from stream_fn_from_provider() implement optional ModelStreamFn.resolve_model() cached metadata. resolve_child_model() uses that actual adapter for canonical routing, reasoning/capability/limits—never an ambient ai.load() fallback or model sync. Custom adapters lacking the contract fail closed on explicit overrides. Model switches remove continuity from the child copy and recheck transformed image/context data before forwarding. Requested config and constructed effective choices persist separately. Existing host permission gates, protocol capacities, settlement and default inherited behavior remain intact.

ChildPresets is a frozen data-only catalog of bounded ChildConfig templates. resolve() expands sparse explicit overrides without changing access/host policy. The child runtime exposes the catalog through agent presets and applies the same expansion to agent(action="spawn")/direct starts. Unknown/chained or privilege-bearing templates fail before admission. Persisted records contain expanded settings and provenance, not live template pointers or automatic replay instructions.

Native child editor recovery is transient

Coding and Claw expose only reviewed same-session child input reopen and image preview. They have no child editor disk save/load/delete, cross-session catalog or autosave controls; /agents saved refuses without disk/background work. Existing child snapshot files are not inspected, migrated or deleted. Reopening preserves current editing text/images/settings/cursor, never a prior execution approval. Native primary /draft persistence remains separate.

This does not remove actual Agent child journals, initial-task records, stopped Agent/SDK image/task recovery or Agent-tool-owned queued-image return/backups. Those durable records are execution/recovery data, not child editor autosave or a resurrected cross-session draft UI. Generic worker/provider OAuth callbacks and read-only extension menus also remain distinct from the removed MCP input/task families.

Explicit MCP OAuth profile and SDK

HTTP server configuration may include an explicit pre-registered public-client profile. This currently uses a restricted pinned-issuer discovery profile, not complete automatic OAuth registration/interoperability:

{
  "remote": {
    "transport": "http",
    "url": "https://resource.example/mcp",
    "oauth": {
      "issuer": "https://issuer.example/tenant",
      "client_id": "pre-registered-cli-client",
      "scopes": ["tools:read"],
      "callback_port": 0
    }
  }
}

The SDK exposes await manager.manage_auth("remote", "prepare"). Inspect the returned resource/issuer/client/scopes/endpoints before explicitly calling manage_auth("remote", "start", review_id). Open the returned authorization URL manually in the operator's browser—no auto-launch/code paste. The issuer's registered client must accept the implementation's loopback callback at http://127.0.0.1:PORT/callback; port zero selects a free port, not a promise that an arbitrary issuer accepts it. Status/cancel/forget are explicit actions too.

Actual S256/code exchange, state/issuer/resource/scope checks, bounded callback ownership and memory-only exact-resource credentials are implemented. A subsequent explicit connect still registers tools; authorization does not change tool approvals or allow_tools. Forget erases local credentials/disconnects proxies, not remote revocation. Tokens stay private in memory unless separately reviewed for an explicit supported store; they locally expire within one hour and are never automatically refreshed/replayed. OAuth cannot mix Authorization header credentials or remote plaintext resource URLs. HTTPS verification remains on; optional ca_file is an explicit absolute trust-file path.

The MCP client is pre-registered/public-PKCE only. Resource/issuer discovery uses bounded credential-free GETs, exact resource/issuer matching, operator scope ceilings, issuer-origin endpoints, TLS verification and uncompressed JSON. This is not hosted client-metadata registration or DCR. Unsupported registration/authentication modes are refused, not silently downgraded. Explicit refresh and both Linux credential stores are retained separately; no operation implies automatic login or tool trust.

UI hosts use manage_auth(..., lease=..., alive=...) and detach_auth(lease) to fence consent and late credential commitment. A foreign lease cannot prepare/start/cancel/ forget another view's transaction. Metadata cancellations settle actual coroutine ownership; terminal/client close may be awaited idempotently after synchronous close. Host guards are not tool approvals. Completed safe status can include a consumed review for local forget; waiting status exposes no reusable live URI/receipt. Public-client consent, explicit refresh and storage keep distinct receipts. Full external-provider interoperability is not inferred from this bounded profile.

Whole-page consent proof belongs to the actually focused review. Blur/preemption, resize or changed endpoints/scopes clears confirmability and requires fresh full review; background painting does not grant authorization or tool permission.

Authorization metadata discovery

Resource denial challenges now retain their resource_metadata pointer and operation-specific scopes for an explicit subsequent authorization, without replaying failed MCP POSTs/tools. Quoted multi-scheme headers are parsed with bounds; malformed or ambiguous challenges are not silently discarded. Without a pointer, path-specific then root protected-resource metadata are tried. Issuers use ordered OAuth and OIDC well-known candidates; only absent GET routes (404/405), never bad identity/200 data, redirects or token/code errors, permit fallback.

Pointer metadata uses no existing MCP credentials/cookies. Its origin must be the configured resource/issuer or an explicitly approved HTTPS origin in the optional OAuth metadata_origins array (at most eight origin-only URLs). This approval does not change the pinned issuer or authorization/token endpoint trust. Resource/issuer identities still match exactly. Challenge scopes are operation-specific and take priority without assuming they are listed in scopes_supported; the configured scopes remain an operator ceiling. Scope-less listings are displayed as no request, and over-ceiling requirements fail rather than silently expanding access. Actual review data carries discovery URLs and scope-source provenance. Consent/permissions/ leases/explicit connect remain separate. Client-metadata and dynamic registration are removed; explicit public refresh and both retained Linux stores do not imply universal external-provider interoperability.

Pre-registered public-client boundary

MCP OAuth accepts only an existing pre-registered public client ID, S256 PKCE and token authentication none. registration: "pre-registered" and token_auth_method: "none" are the defaults and the only supported values. Metadata-client-ID/hosted-client-document registration, dynamic registration, confidential client-secret methods and private-key JWT/signing are removed. Unsupported profiles fail closed before authentication/network work; no secret source is read, signer started, registration POST sent or method/store fallback attempted. There is no remote client-registration management API.

Pinned resource/issuer metadata discovery remains part of explicit public-PKCE authorization; it is not client metadata registration. Resource, issuer, scopes, TLS trust, loopback callback and actual endpoint review still apply. Authorization never connects tools or grants their permission. Explicit reviewed refresh and credential save/restore below remain independent operations, not automatic login or recovery of an unsupported profile. Old records bound to removed authentication/ registration methods are not silently migrated or downgraded to public grants.

Explicit in-memory OAuth refresh

Refresh is an operator action, never a background token retry. If the issuer supplied a refresh token, open authorization status and press r, review the resource, actual grant scopes, pinned endpoints, client ID and public method none, then confirm with a fresh physical Enter. This sends one resource-bound refresh POST, not a new browser login or client registration. Claw requires mcp_refresh:1; unsupported gateways cannot submit it.

SDK hosts use manage_auth("remote", "refresh_prepare") followed by manage_auth("remote", "refresh", review_id) under the same live consent lease. Receipts are single-use and expire after five minutes. Refresh uses the previous grant's scopes, not the broader configured ceiling. Valid replies may narrow, never widen, scopes. A new refresh token replaces the prior one only after reply validation; omission retains the old token only after a valid response.

The refresh capability is consumed before exchange. Cancellation, lost/invalid responses or revoked ownership discard it rather than replay an uncertain token. c cancels a pending refresh; leaving/disconnecting settles owned work and fences late commits. A still-valid old access token may remain usable after refresh failure; authorize again if needed. An existing saved snapshot is removed before rotating refresh, so restart cannot replay stale credentials. A completed replacement grant is saved only by another explicit reviewed save.

Refresh never replays MCP requests, connects tools, changes approvals or falls back to another client/authentication/storage method. The two retained Linux credential backends are documented separately; neither implies startup restore, automatic refresh, remote revocation or universal issuer interoperability.

Explicit Linux MCP credential storage

MCP credentials retain two explicitly selected Linux backends: McpRegistry(..., credential_dir=..., credential_backend="file") (the default backend) or credential_backend="secret-service". Without a credential directory, the manager remains memory-only. Neither backend silently substitutes for another; unknown/unsupported/unavailable choices fail closed.

For a completed resource-bound grant, use manage_auth(..., "credentials_save_prepare"), review the returned resource/profile/scopes/TLS/ backend disclosure, then confirm "credentials_save" with that exact review_id. credentials_restore_prepare/credentials_restore and credentials_forget_prepare/credentials_forget use separate owned receipts. Restore checks exact resource, pre-registered public profile, trust-file content, expiry and scope binding; it performs no discovery, token request, connection, model call or tool approval. Nothing automatically saves or restores at startup.

file is private local plaintext, not encrypted/keychain storage. It requires owned no-follow 0700 directories/0600 regular files, bounded records and atomic fsynced writes. Unsafe links, ownership/mode/type violations or uncertain writes refuse without an alternate storage path. File permissions are not an OS sandbox.

secret-service uses an already running Linux Secret Service and unlocked default collection, existing session D-Bus address, /usr/bin/python3 GI bindings and libsecret typelib. The directory is a namespace, not a plaintext token path. XDOG does not install/start/unlock a vault, create a collection or initiate login. The helper uses an encrypted Secret Service session and owned timeout/cancellation cleanup. At-rest protection is the vault's responsibility. Locked, missing, unsafe or ambiguous storage refuses with no plaintext fallback.

Save/erase/restore are explicit reviewed actions. An admitted storage mutation may have committed even when cancellation loses its receipt: inspect the selected store rather than replay. Public diagnostics/history never contain token values. Saved snapshots are invalidated before rotating refresh; a replacement grant is not saved implicitly. Forget is local erasure, not remote revocation. Provider OAuth uses its separate explicit Secret Service vault contract below.

Explicit MCP completions

await manager.complete(server, kind, name, argument, prefix, context) returns a frozen McpCompletion (publicly exported from xdog.agent.mcp). kind is prompt or template; name is the catalog prompt name or exact URI template. Context contains other already-edited named string arguments, excluding the current argument. The server must explicitly advertise completions and the reference/variables must still exist in its catalog; this call never connects a server or registers tools.

The request uses completion/complete, ref/prompt or ref/resource and optional context.arguments on the existing current/legacy stdio/HTTP connection. Bounded responses retain ranking, up to 100 strings, optional total and hasMore; invalid, oversized or deferred results fail rather than silently truncate/coerce. Catalog URI-template entries expose named-variable metadata to explicit SDK callers. One active/one queued explicit intent per server and a cancellable 200 ms interval bound traffic; canceled/disconnected intents settle without retry or successor replay. The configured timeout bounds queue wait, catalog and POST together. No prompt/resource retrieval, tool execution or URL following occurs.

This lower-level MCP completion API remains available to explicit SDK callers; Coding and Claw no longer expose the manual named-argument completion picker or negotiate its native UI transport. Native prompt arguments use an explicit JSON object, and resource reads use an explicitly supplied resolved URI. Extension command argument completion is separate and retained. Suggestions never grant tool permission or authorization. Opaque resource completion support is described below; these SDK APIs do not imply native completion controls.

Opaque resource-reference completions

manager.complete(server, "resource", uri, documented_argument, prefix, context) now supports ref/resource outside advertised named templates. The URI is opaque: no catalog/schema is invented or fetched, no URI is followed, and no resource is read. The explicit caller supplies the name/context; the server validates their meaning. The current argument must not also appear in context. Existing byte/count, capability, one-active/one-successor, overall timeout and cancellation/connection fences remain authoritative. Prompt/template catalog validation is unchanged.

Both stdio and HTTP negotiation now admit a completion-only server when its completions capability is an object, without inventing tools/resource/prompt support. The frozen result and bounded codec include kind resource. The shared resource_completion_document() formatter remains available to SDK callers as untrusted inert data, explicitly NOT a resource read or resolved URI. Native opaque- resource completion pickers and their Claw UI negotiation are removed; ordinary explicit resource reads, previews and insert/export controls remain separate.

Opaque references are supported by the explicit SDK API, not by a native picker. Negotiation, ownership, bounds and cancellation retain their SDK-level checks; offline peer evidence does not establish all third-party interoperability.

Validated inert MCP content and retained annotations

Tool results and resource/prompt previews share bounded validation for text, audio, embedded resources, resource links, annotations and opaque _meta dictionaries. Nonfinite/non-JSON metadata, malformed or excessive base64, unsafe/empty resource URIs, conflicting text/blob payloads, invalid annotation audiences/priorities and invalid timestamp forms fail explicitly. Metadata does not grant permission or change a message's actual role. A rejected result is not retried or silently repaired.

Binary input and decoded payloads are bounded separately (4 MiB decoded per block), with the existing 8 MiB transport/content ceiling and 1 MiB retained metadata ceiling. Actual typed model image projection retains the existing PNG/JPEG/WebP admission checks. Inert prompt image previews still retain other encoded formats for explicit inspection/export; they do not claim provider support or silently draft binary data.

Text/image block annotations and vendor fields survive as indexed details["mcp_content_metadata"] and a visible [MCP content metadata] text block. This preserves metadata in the normal model/tool UI, source viewers and persisted sessions without copying image bytes into details. Prompt/resource previews retain inner/outer metadata in their existing text fields, including alongside binary summaries; the Claw document codec remains compatible rather than inventing new fields.

Audio, embedded blobs and resource links remain inert retained data. The client never follows the URI, opens a file, plays audio or automatically admits binary preview data to a model. Explicit export/inspection and existing tool-policy gates remain authoritative. Audio playback/native model audio support, further binary artifact APIs and broader content/provider interoperability remain open.

The opt-in independent serving-SDK checks construct a content corpus using its published schemas and exercise real stdio/HTTP mixed-content tools. They are not claims of every codec, issuer or native-platform implementation being verified.

Typed empty MCP binary previews and gateway compatibility

McpBlock.content_type preserves text, image, audio, embedded text/blob and link identity in previews and private JSON exports. is_binary uses the declared type, not only nonempty bytes. Empty audio/image/blob payloads remain binary, display 0 bytes, retain their annotations and cannot silently become draftable text. Genuine empty text remains text. Existing nonempty legacy blocks still decode.

Claw negotiates mcp_content_types:1 independently. Upgraded gateway/viewer pairs transport explicit types through the existing checksummed, session-fenced chunks. For older viewers the gateway retains the legacy shape for ordinary text/nonempty binary data, but refuses a downgrade that would lose an empty binary's identity. Capability changes during a read/transfer fence commitment without replay. An upgraded client receiving untyped previews from an older gateway cannot infer their payload type from MIME/metadata or insert them; preview/private export remain available and the update requirement is explicit.

Typed private exports retain both an empty data string and its content type. Destination entry now retains the submitted path after its nested editor clears, with real focused cursor propagation, blur and edit/review transitions. Enter first reviews the path; only a fresh visible review permits saving. No implicit overwrite, upload, provider admission, playback or tool-permission change is introduced. These checks do not establish audio codec/playback or complete content/platform parity.

MCP JSON Schema and typed structured output

Current-protocol MCP results now retain any finite JSON value in structuredContent: objects, arrays, strings, numbers, booleans and explicit null. Missing content is tracked separately from null by structured_content_present; advertised output schemas must be satisfied. Legacy negotiated profiles still require object results. A successful text summary no longer hides canonical structured data: when no exact canonical JSON text block is already present, a labelled structured-output block is added to the tool content used by the model, full-source viewer and persistence. Compact rows may still collapse it; Ctrl+O or the transcript viewer inspects it.

JSON Schema defaults to 2020-12; recognized declared draft-04/06/07/2019-09/2020-12 semantics are used rather than silently substituting another dialect. Local pointers, anchors/dynamic references and bundled resource IDs resolve only inside the advertised document. External documents are never fetched, including through an ID/base change. Unknown dialects, unresolved references and unsupported required vocabularies fail closed. Unreferenced examples/defaults remain data; explicitly referenced targets are checked as schemas. Normal nullable unions, boolean property schemas and conditionals are supported. MCP arguments remain objects; model parameters get an object-root intersection while validation keeps all raw advertised constraints.

MCP uses an optional sync/async AgentTool argument validator before permission hooks and validates effective arguments again immediately before the remote call. Coding and Claw extension pre-hooks use the same check when they rewrite arguments. Existing permission/read-only/allow_tools rules still decide execution; a schema/readOnly hint never approves a call. Coding's parameter prompt formatter now handles boolean/union property declarations rather than crashing on a valid schema.

The validation helper is local, owned and cancellable, started only for explicitly connected tool schemas. It runs isolated Python imports (-I) with only the trusted installed/vendored library root explicitly added, without a second agent/model loop or schema URL retrieval. A three-second deadline includes queue wait/validation; timeout/cancel kills and reaps that exact helper, including a child still being created. A later explicit validation may start a fresh local helper; remote MCP calls are never retried by it. Serialized schema caching is capped at 32 entries/4 MiB. Schema/value bytes, depth and node counts are bounded; these are application limits, not OS-enforced filesystem/network/memory sandboxing.

Bounded JSON tool details now survive save/reload/branches in both clients, preserving typed structured output and null-presence flags. Live/non-JSON handles stay transient. Transcripts may contain sensitive structured values: keep them private. Actual current/ legacy peers, both app permission paths and Linux/private-tmux inspectors are verified; provider-specific full-schema acceptance and mixed-dialect/native-platform interoperability are not inferred. Format-assertion/custom required vocabularies, ambiguous HTTP header mappings through schema composition, richer content/client capabilities and external credential/provider interoperability gaps remain open. Earlier object-only/current structured-result limits are superseded by this pass.

Schema packaging includes a direct referencing dependency and the portable SDK bundle dependency list. The helper resolves its own trusted library root, so vendored bundles work without accepting project/PYTHONPATH shadow modules. An actual vendored helper test covers this; generated-output/vendored-package licence boundaries are unchanged. Lock updates only declare the already-resolved dependency, not unrelated version upgrades.

Rich extension command artifacts

Extension command callbacks now accept the same typed text/image/structured JSON/ details result builder. Legacy string commands remain strings. The SDK runtime.command() returns either str or a frozen ExtensionCommandResult with extension/command identity, a validated result snapshot and safe summary. For example, a command can return:

return api.result([
    {"type":"text","text":"Command report"},
    {"type":"image","data":png_base64,"mimeType":"image/png"},
], structured=[False, 0, None], details={"enabled":False})

Both interactive clients append a read-only command artifact, not a tool result or agent message. Images render through the existing image component; base64 is not printed as prose. Ctrl+O/the transcript viewer inspects the complete text, typed JSON and details. Transcript copy/export can inspect its textual system-source record. Original image bytes and typed metadata remain owned in the local artifact; this does not pretend a text-only transcript export includes binary images.

Returning /quit, a URI or other command-looking text is data, never executed. Rich artifacts cannot trigger a model prompt, mutate the main draft, approve tools or change host read-only scope or permission modes. The prior draft/focus and normal approval priority remain; callbacks still require trusted activation/idle ownership. Results are fenced by command/session/connection identity; canceled/stale/foreign output is not appended to a new owner and callbacks are not replayed. Artifacts are local UI results, not newly persisted canonical agent-history entries or automatically saved files. Legacy command/builtin skill prompt behavior is unchanged.

Claw independently negotiates extension_command_results: 1 in hello/pong. Its controlled response carries a validated artifact separately from the safe output summary. Older viewers get a visible update-required summary without image bytes, not a dict stringification containing base64; no callback is rerun to degrade it. New clients reject unnegotiated/malformed artifacts rather than guessing semantics. The rich envelope's existing byte/count/image/JSON limits apply, keeping control responses below the receiving transport budget.

Independent real command workers, both production UI paths, permission/draft/history non-mutation, system-source inspection, stale-owner rejection, older-viewer behavior, cancellation and malformed returns are tested, plus both private-tmux image/JSON inspectors and parent terminal restoration. Returning an artifact does not grant editor/session/navigation mutations, custom rendering/layouts or raw terminal access; those extension interfaces are removed. Read-only command inspection, provider registration and explicit artifact export/reload have their own contracts below.

Lossless command artifact export and read-only reload

/export PATH artifact captures the latest owned rich command artifact, including original image bytes, text, structured JSON and typed details. It is private JSON only (json may be explicit); implicit binary copy/share or lossy text/Markdown artifact export is refused. Supplying a path does NOT bypass review. The existing output panel shows image MIME/size/checksum and a no-redaction/sensitive-binary warning, without rendering base64. Enter writes only the reviewed immutable snapshot.

Files use the existing private 0600, same-directory staging/fsync and no-overwrite commit path. Cancel, hidden/tiny/resize review, owner/context changes and stale jobs cannot authorize an unreviewed write. A commit already made is not remotely undone; cancellation is not rollback. No upload, provider request, tool call or automatic artifact save is introduced. Ordinary textual transcript exports retain their prior semantics and are not falsely labelled binary exports.

/artifact PATH explicitly reads a bounded regular artifact file off the UI thread and appends a read-only image/JSON command display. It does not activate extensions, execute stored command names/text/URIs, change permissions or host read-only scope, or send input to the model. The 4 MiB file envelope is versioned as xdog.extension-command/v1; its SHA-256 covers canonical validated identity/content/metadata. Checksums detect corruption, NOT authenticity: treat files as untrusted data. Unsupported versions, tampered/malformed data, image/MIME/type failures and FIFO/nonregular inputs are refused. Original image bytes and false/zero/null/omission remain exact on reload.

Artifacts are selected by session/connection/history epoch. Old-owner results cannot be exported or appended to a new owner; loading snapshots publishes only under the captured context. A read can be canceled by closing/changing owner, suppressing late publication. An OS-blocked filesystem thread cannot be forcibly interrupted; these checks/time/memory bounds are not OS-enforced sandboxing or filesystem confinement. Loaded artifacts become owned display data and can be re-exported after a new review.

Both actual clients and private-tmux workflows verify binary save/reload, explicit review, private mode/no overwrite, owner fencing and no-model/draft non-mutation. SDK artifact_bytes/artifact_from_bytes/read_artifact expose the same validated codec. Artifact export/reload does not restore removed extension custom rendering, host mutations or terminal interception. Further MCP/credential and native-platform interoperability remains separate work.

Command session metadata

Read-only command metadata never opens or activates a session browser.

Failed extension inventory and cancellation

Failed/disconnected/unloaded worker registrations withdraw from the current tool inventory. Historical alias identities and stale-executor/argument guards remain; withdrawal does not authorize an old approved call or unload a mandatory validator. Projection settles on the owning Agent/command/child loop, including callback errors and cancellation. Other healthy workers/native tools remain unchanged. Child refreshes retain their explicit selected-tool subset. A post-hook failure preserves the actual tool result instead of replacing it with invented success.

Failure catalog updates are descriptive, owner-scoped metadata: they cannot adopt a different session, activate/reload a worker or approve tool execution. Old or foreign cached contributions never become current merely because a callback failed. Reload/unload remain explicit user actions; uncertain callbacks are not replayed.

Registered command argument completion

Trusted Python commands may register an independent bounded data callback:

async def complete(prefix, context):
    return [{"value": "release", "label": "Release", "description": "Argument data"}]
api.command("deploy", "Deploy command", execute_deploy, complete=complete)

runtime.command_completions() exposes current registration identities, and await session.complete_command(command, prefix, identity) in BOTH client SDKs queries the corresponding worker without executing its command callback or model. The callback has no interactive UI/editor/navigation/session-mutation authority. Items are plain single-line text (32 unique entries/64 KiB result); the prefix and value/label/description bounds are validated on both sides. Failed/timed-out uncertain callbacks settle/reap their workers and withdraw current declarations; no retry/replay or fallback callback is implied.

Replacement/disposal changes the command completion identity. Stale references and registration changes during callback execution yield no completion data; an old disposer cannot remove a replacement. Both client SDK wrappers settle their owning Agent's tool projection if that worker fails.

Both interactive parent editors now query registered argument callbacks in the background. Suggestions replace only the cursor-local token (including its right suffix); quoted whitespace stays inside that token, with quotes passed literally rather than shell-expanded. Tab or Enter inserts without submitting. A subsequent Enter executes the typed command only through its normal command/permission path. Pending queries cannot turn Enter into an accidental command submission. Escape dismisses the suggestions and ignores their late result; Tab reopens explicitly.

One query is in flight plus one coalesced latest draft. Original session/context, command registration, full draft and cursor are revalidated before inserting. Changed cursor/draft/registration/connection results never overwrite the current editor. Claw separately negotiates extension_argument_completion: 1, runs the query under the actual idle group/worker owner, and does not activate a cold session or borrow the UI-command/model approval context. Previous gateways return unavailable.

Escape dismissal, loss of editor focus, hidden suggestions and application exit now signal the owned completion task immediately. One cancellation is sent per in-flight owner; its slot remains held until actual cleanup settles, so repeated Escape cannot interrupt worker reaping or launch a successor early. Uncertain callbacks are not replayed: their worker is reaped and current registrations are withdrawn; explicit reload is required if the extension remains desired. Coding settles its owned completion threads before retiring the extension manager at exit. Claw separately negotiates extension_argument_cancel: 1 and pins cancel messages to the original request/connection/group/session. Older cancel-unsupported peers retain the bounded deadline fallback and never receive guessed cancellation.

Argument menus now render each supplied label as presentation and show its actual insertion value in brackets when they differ, alongside the description. Duplicate labels are allowed but duplicate insertion values are not; selection and replacement stay keyed to the value. Unicode/narrow layouts use the shared bounded renderer. Changing only a label refreshes presentation without selecting a different value. Unsafe/oversized label or description data rejects the entire result, not a partial menu. Coding reconstructs owned row copies after immutable cross-thread delivery.

Completion callbacks may read context["argument_context"] when the current caller supports full argument context. It is a copied dict with arguments (the captured text after the command), cursor, start and end (Unicode code-point offsets relative to that argument body). The token prefix is checked against arguments[start:cursor]; this enables later-argument suggestions based on preceding choices without reading the rest of a session or granting editor mutation authority. Host selection still replaces its original cursor-local span. Callback edits to this dict do not change the editor or its captured request.

Prefix-only SDK/legacy queries explicitly supply None for that context; they cannot impersonate a full draft. New Claw editor queries independently negotiate extension_argument_context: 1. Unnegotiated/invalid/foreign-scope metadata is refused before callback admission. Withdrawing support cancels captured contextual jobs (including before task startup), refuses late contextual output and does not silently replay the query as prefix-only. A user may explicitly edit/Tab to request new data under the remaining capability. Existing prefix-only callbacks remain supported; argument_input= is optional on both SDK wrappers.

More expressive suffix semantics and broader argument API interoperability remain open. Callback-supplied spaces/quotes are literal replacement data; the host never silently adds shell quoting or interprets the resulting argument. Independent child prompt completion is not claimed. Native file/slash-name completion is retained.

Extension terminal command-menu inspection

page = await api.get_command_catalog(query="", offset=0, revision="")
# Continue using page["next_offset"] and page["revision"].

A live native host may bind read-only menu inspection to an eligible owned command, lifecycle/pre/post-tool, submitted-input or context callback. It reads the same sanitized terminal menu used by /help and completion, in 32-entry pages: normalized name/description, source label, session identity, revision and explicit pagination only. Host availability and exact invocation/session/child admission remain authoritative; a detached/headless or unsupported callback gets None, not another owner's menu. Independent child callbacks cannot borrow the parent's menu/editor. Provider/setup callbacks receive no menu control authority.

Worker-supplied routing/host identity is refused. The host's trusted invocation identity is separate from plugin request fields and carries no mutation grant. Changed menu/session/registration invalidates a former revision and returns status="stale", no entries and no next page. Restart at offset zero explicitly; there is no silent merge, command execution, model input or automatic replay. Claw separately negotiates extension_command_catalog:1 and its generation; old session/inspection support does not substitute for that exact bridge. Session/tree catalogs below retain their separately scoped read-only contract.

Read-only extension session metadata

await api.get_session_name() returns the actual owning session's name, or None when unavailable; an empty name is not a borrowed parent value. get_session, get_model, get_context_usage, get_active_tools and get_tool_inventory retain bounded, owner-projected read-only data. Session/tree and command catalogs remain read-only; has_pending_messages is a boolean query, not a queue control API. Callback admission, role and session/child provenance remain checked; queries neither start a model nor grant permissions.

Extensions cannot read/replace the unsent editor, rename/navigate sessions, select tools, request compaction or enqueue prompt/context/follow-up input through host control APIs. Native USER /name, /model, /compact, /sessions and /tools remain independent. Provider/command/hook registration and basic input/confirm/select/status remain supported; model tools use the host SDK instead.

Claw's read-only session bridge uses extension_session:1 and extension_session_inspection:1; terminal command-menu inspection separately uses extension_command_catalog:1 and its generation. No editor/navigation/mutation capability or separate extension name/active-tools flag is needed. Native session_name_read and tool_inventory_selection controls remain independent. Session/tree catalogs read the bound backend directly; listing them does not grant plugin navigation authority.

Read-only command model and context inspection

Interactive extension command callbacks can inspect the actual active owner:

model = await api.get_model()
usage = await api.get_context_usage()

get_model() returns a bounded public snapshot: session ID, actual selected model ID/name/provider, effective context/prompt ceilings, declared output-token limit, reasoning/tool/input/effort/vision capabilities and current thinking level. Unknown capabilities remain None, not fabricated support. Header credentials, base URLs, endpoint/protocol internals and other provider-private configuration are excluded. Stale metadata for another model is not used; results are fresh JSON snapshots, not mutable provider/model objects. Zero vision constraints mean unreported limits, consistent with the original model metadata, not a zero-image permission setting.

get_context_usage() reports the last matching assistant response's input, output, cache-read and cache-write counters, their sum, the effective prompt ceiling and percentage. It explicitly labels source: "last-response" and messages_since_usage: later messages, tools/images, changed system prefixes and future prompts are not presented as a measured current prompt. Missing usage or only another model's measurements yields source: "unavailable" with tokens, percent, components and age all None. No character/token estimate, provider request, model activation or hidden-history/system-prompt disclosure is introduced. The percentage can exceed 100 rather than silently clipping it.

Coding reads its captured idle command session. Claw resolves the read against the original active runtime/broker under the existing reserved command group, not cached UI footer/catalog data or a client-supplied measurement. Both methods preserve session, draft, permission/read-only policy and native terminal focus.

Claw separately requires extension_session_inspection:1 in addition to extension_session:1; older session-capable viewers receive an unavailable result, not unknown dialogs, borrowed authority or a silently fabricated snapshot. This command bridge still uses its original negotiated ownership. The invocation snapshot support below extends read-only inspection to other callback scopes; these reads cannot mutate models, and provider authentication has a separate contract.

Invocation-owned inspection in hooks, children and headless runs

get_model() and get_context_usage() also work in lifecycle, pre/post-tool, context/input/provider callbacks and headless commands when their actual runtime supplies an inspection producer. Coding and Claw bind their own Agent sessions automatically. Standalone ExtensionRuntime users may explicitly pass an inspection= callback producing an owner-validated envelope; a runtime without one continues to report unavailable rather than inventing model/usage data.

These non-command getters read the callback-entry snapshot, not a live query across arbitrary awaits. Every call returns a fresh validated copy. A new invocation receives a new snapshot, so a later model/context change cannot silently rewrite the old callback's observation. The worker receives only bounded public fields; hidden prompts/history, credentials/endpoints and model/provider work are not introduced. Caller-supplied context cannot override the protected inspection/session/child identity. Inconsistent or foreign producer snapshots become unavailable.

This is independent of command authority: hooks/child callbacks cannot borrow another invocation's get_session or catalog authority. Editor/session/navigation mutations are removed for all extension callbacks. Interactive parent commands retain the fresh read-only host bridge and Claw inspection capability gate; non-UI read-only callbacks do not send new dialogs to older viewers.

Child factories now bind the actual independent child Agent before initialization and startup hooks, retaining parent validation and the same inherited permission restrictions. Child snapshots use their own model/limits/options/history, never the parent's inspection producer. Inherited entries retain their original provenance; inherited parent response counters are excluded from the child's measurements until the child actually receives its own response. Changing/closing the original parent session invalidates the child producer rather than adopting another conversation.

The owned-worker tests cover headless hooks/context/commands, original task copies, model changes and spoofed caller identity. Both real clients exercise UI and headless parent/child callbacks with independent model selection, explicit tool-policy allow/deny and persisted snapshots. This closes the earlier command-only inspection limitation, not every native/provider/auth/host API or native-platform requirement.

Command session/tree catalogs

Interactive extension commands can now read actual host navigation metadata without opening a dialog or activating context:

first = await api.list_sessions(query="project")
if first is not None and first["has_more"]:
    second = await api.list_sessions(query="project", offset=first["next_offset"], revision=first["revision"])
tree = await api.get_session_tree()  # current host session
branch = await api.get_session_tree(target_id, branch_id=branch_id)

Pages contain scope, owner_session, resolved tree session_id/branch_id, query, offset/total, at most 50 entries, has_more/next_offset and membership revision. Entries are inert bounded id/title/detail/kind/session/branch/checkpoint/depth/active metadata and safe checkpoint previews—not full hidden conversations or system prompts. Text/type/ID/offset/count/frame validation rejects malformed responses rather than coercing them. Page encoding is capped at 256 KiB, query at 1000 UTF-8 bytes and catalog traversal at one million entries.

Command session pages use stable identity order, not the native browser's recency order, so the command's own durable entries cannot move sessions between pages. Pass the returned revision with continuation calls to refuse a changed membership/ owner/query/target rather than mix catalogs. It is a membership/topology identity revision, NOT a frozen full-history/metadata version; previews/active flags may be fresh on each read. Restart at offset zero after a None refusal. Leaving revision empty explicitly requests a fresh current page. Native browser behavior is unchanged.

Coding reads the existing session-manager/tree sources off-thread under captured command/session context; Claw reads the owning GroupRuntime catalog, not a client cache or launch-directory guess. Claw tree targets remain restricted to that group. Host catalog operations preserve their actual filesystem owner until creation/read settles on cancellation, then refuse late adoption. An OS-blocked thread cannot be forcibly killed; these limits/ownership checks are not an OS sandbox.

Only actual interactive extension-worker command invocations receive catalog authority. Hooks/context/child/unattended callbacks return None; original worker invocation and host extension/command/group/session ownership are independently checked. Missing/foreign/unreadable targets or membership changes yield unavailable without disabling the extension or replaying reads. Invalid plugin request/response codecs still fail closed. No model turn, permission/read-only mutation or session activation is introduced.

Real workers, both live hosts' 61-entry multi-page catalogs, search/tree/branch previews, revision ordering/changed membership, foreign-group refusal, cancellation settlement and production terminal catalog commands are verified. These catalogs are read-only: they do not expose the removed navigation/editor/session mutation, custom-rendering or terminal-interception APIs. MCP/credential/native-platform interoperability remains subject to its own limits.

Runtime-registered extension shortcuts

Trusted extensions can register a shortcut and its actual command callback:

async def inspect_model(args, context):
    model = await api.get_model()
    return model["id"] if model is not None else "Model metadata unavailable"
api.shortcut("inspect", "alt+k", "Inspect the current model", inspect_model)

Registration contributes the namespaced /ext-EXTENSION-NAME command plus a canonical default key; pressing it uses the EXISTING interactive command owner, not another tool loop or arbitrary input interception. Plugins must already be explicitly trusted/activated; a key does not discover/load an extension. Callbacks retain normal idle/child/tool-policy/session/connection checks and only the retained read-only command host queries and basic dialogs. No model turn or tool approval is implicitly created by a shortcut itself.

/keys lists effective extension keys and disabled conflicts. Built-in and USER editor bindings always win. When different extensions claim one effective key, ALL colliding shortcuts are disabled (manual commands remain available). Typing, unmodified editor/dialog keys, Escape, Ctrl+C/D/L/Z and confirmation/recovery routes cannot be captured. Release/repeat events cannot duplicate callback invocation. Registration capacity is 32 per extension/64 across active registrations.

The existing keybindings.json can remap or unbind a registered shortcut using its full lowercase command name, for example:

{"ext-keyed-compose":"alt+x","ext-keyed-blocked":[]}

Multiple explicit keys may be provided as a list. Overrides still reject duplicate/ built-in collisions and reserved routes. Configuring an inactive command does NOT activate a plugin; /keys reports it unavailable. Changes take effect when the client loads its keymap, like existing prompt bindings. Terminal encodings can make some Ctrl/Alt combinations indistinguishable from Enter/Tab; choose an unambiguous key or a terminal with extended keyboard support. Alt+K/Alt+X are physically tested.

Dispatch is confined to the ready idle prompt: approvals, extension dialogs, viewers, completion pickers, child/MCP panels, overlays, output reviews, active commands/model work and closing/disconnected owners retain priority. Unload/ failure/reload updates registrations; stale keys cannot adopt a new callback. Coding now refreshes actual registration snapshots after extension commands even if a plugin publishes no status or notices. Claw separately advertises extension_shortcuts: 1; missing support is both dispatch-disabled and reported in effective help.

Real workers, both UI command/editor paths, user remap/unbind/inactive overrides, reserved/default/user/inter-extension conflicts, contextual suppression, repeat, load/reload/unload and both private-tmux default/remapped key paths are verified. Arbitrary terminal interception and custom renderers are removed, not pending shortcut features. Provider registration and read-only host queries retain their separate contracts; MCP/platform interoperability is not implied by a shortcut.

Submitted-input extension hooks

Trusted extensions can register api.on_input(callback) for newly admitted human input, before its response model call or canonical user-message commit:

async def prepare(text, metadata, context):
    # metadata: prompt/steer/follow_up source, active channel and image metadata.
    if text.startswith("reject:"):
        return {"action":"reject","reason":"Explain why this input was refused"}
    return {"action":"transform","text":"Prepared input: " + text}
api.on_input(prepare)

None or { "action": "continue" } leaves text unchanged; a string is shorthand for transformed text. Explicit transform/reject objects have exact typed fields; unknown/handled/image/command-action payloads are NOT guessed. Text is limited to 1 MiB UTF-8, rejection reasons to 2000 bytes and callbacks to 32 per extension. Callbacks run in deterministic registration/activation order in their actual owned worker. Missing/malformed/failed/timed-out hook decisions refuse admission and quarantine the failed worker; known failed filters cannot silently pass raw input until explicitly unloaded/reloaded. A normal explicit rejection keeps the extension active. Callback side effects are not rollback and are not replayed.

The Agent's optional input_transform hook now covers initial input, identified steering/follow-ups and plain explicit continuation before canonical admission. Transforms preserve queued identity/recovery receipts: consumption happens only when the transformed message is committed. Rejected selected identified input remains recoverable with original text/images; plain rejected steering/follow-up messages enter an explicit-recovery quarantine (agent.drain_rejected_messages()), not a live queue that could replay them on the next prompt. Initial rejected input belongs to its caller, not a fabricated assistant/provider response. Hook cancellation is settled before idle/owner release. Preexisting Coding SDK input hooks are composed rather than replaced.

The extension API sees text plus image MIME/encoded-size metadata, never original base64. Its output cannot silently remove/replace images: transformed messages keep original typed attachments/timestamps. All later model capability, tool argument, approval, host read-only and child permission gates remain authoritative. A transformed string resembling /quit/a URI stays model input data, not a reparsed host command. Editor/session/navigation mutation authority is absent from both hook and command APIs.

Both interactive clients show the actual transformed user text/queued consumption, with visible notices. Initial refusal restores original draft/attachments without model input or an automatic replay. Coding uses the current worker-owned image batch for recovery, not a potentially lagging UI active-image pointer. Claw applies its existing per-connection/run input-ID normalization/fencing to transform/refusal receipts too. Child agents use their own extension worker/input history, not parent history. Claw scheduler/goal/system initial inputs are explicitly NOT human input hooks; queued human steering/follow-up remains intercepted. Channel metadata is the active turn channel, not a claim of a separate queued sender's identity.

Real workers, generic initial/steer/follow-up/continuation/abort receipts, both live clients' typed attachments/canonical history/refusal/recovery, actual queued follow-up images/consumption, child isolation/system exclusion and private-tmux transform/ refusal recovery are tested. Submitted-input transformation and the handled outcomes below remain supported; neither exposes raw-key observation/interception, custom rendering or host mutations. MCP/credential/native-platform interoperability remains subject to its own limits.

Pre-turn compaction of PREVIOUS history retains its existing lifecycle; the input hook is a pre-admission/user-response check, not a veto of independent maintenance work. Failed reload of a previously registered input filter remains failed/required, not an implicit unload: only successful reload or explicit unload changes that policy. Coding print mode reports a refused input as a nonzero error, not a fabricated successful model answer. The removed Coding frontend RPC has no error or completion-receipt interface.

Terminal verification also caught a generic resize race during output review: painting after dimensions changed during tick callbacks could skip the next loop's cursor query. That paint is now deferred until the normal input loop re-anchors. Existing visible-confirmation/query assertions were retained, not weakened.

Explicit handled-input outcomes

Submitted-input hooks may now return an explicit handled decision with bounded feedback, for example:

if text.startswith("local-help:"):
    return {"action":"handled","feedback":"Extension handled this request; no model answer was generated"}

Feedback is required, nonempty and at most 64 KiB UTF-8; extra image/command/action fields are rejected. Handled is NOT rejection and NOT canonical/model history admission. It stops the remaining input-hook chain for that request and emits a separate InputHandledEvent logical-consumption receipt with original source/ID/ attachments. Identified inputs are removed exactly once; they do not also emit InputConsumedEvent or return for automatic replay. SDK callers can inspect/drain handled receipts with agent.drain_handled_inputs().

If all newly supplied input is handled, the host does not invoke the response model with old history just to obtain an answer. Other genuinely queued unhandled requests may still run normally, preserving ONE_AT_A_TIME steering boundaries. Explicit agent.prompt([]) retains its preexisting old-context continuation semantics: it contains no submitted request to intercept. If one request is handled and a later request rejects/fails, the handled receipt remains committed rather than falsely rolled back; remaining original input stays recoverable. Trusted callback side effects are not an OS sandbox or rollback transaction.

Original image bytes are never given to the hook, and handled decisions cannot silently discard attachments. Both interactive clients retain them in the editable composer without restoring/replaying the consumed text. Queue/run/connection ID fences apply to the handled receipt too. Feedback is labelled system/extension data, not fabricated assistant prose or tool approval. Permission, host read-only, tool and model capability gates are unchanged; returned command-looking feedback is never executed.

Print mode outputs explicit feedback and returns success for handled-only input; Coding JSON output emits a valid system/input_handled envelope (optional real assistant output is separate), not a counterfeit model message. Claw sends a content-free final signal after actual handled feedback, releasing run ownership without synthesizing a response. Other non-TUI channels receive extension feedback as channel output, not model generation.

Child supervision retains labelled input feedback as separate bounded metadata (64 entries/64 KiB), with zero model calls when the task is handled. It survives child record reload and appears as SYSTEM feedback in source inspection, not invented user/assistant history. Parent conversations remain unchanged. New shipped clients are jointly verified; older-viewer handled-outcome interoperability remains unproven rather than being inferred from JSON accepting extra event types.

Generic initial/queued/old-history/queue-mode receipts, both live initial and queued image paths, child result/source metadata, text/JSON completion and production terminal model-call counters are tested. Plugin terminal key/paste/release interception and observation APIs are removed, as are custom renderers and extension host mutations. Handled outcomes preserve the ordinary permission and ownership boundaries; remaining MCP/credential/platform interoperability is separate.

Session-owned extension provider terminal events

Trusted extensions may declare callback providers with api.register_provider(name, models, callback). Their session-local model aliases are ext-EXTENSION-PROVIDER/MODEL; they do not modify the global provider registry. The callback returns an async iterator of bounded JSON events (text, thinking, tool_call, usage, done, or error). An explicitly selected alias never falls back to another provider or silently replays a failed request.

A deliberate public failure or cancellation has its own terminal event:

yield {
    "type": "error",
    "message": "Service temporarily unavailable",  # Public diagnostic, not an exception dump.
    "stop_reason": "error",                       # Or "aborted" for provider cancellation.
    "response_id": "optional-provider-response-id",
}

message must be nonempty and at most 8,192 UTF-8 bytes. It is intentionally visible in the UI and persisted history: never put credentials or private exception diagnostics here. Unexpected callback/validation/cleanup exceptions instead produce the generic no-fallback failure. Terminal messages retain prior text, reasoning, signatures and usage; proposed tools in an error/aborted message are not executed. Coding and Claw keep partial text visible, distinguish provider cancellation, and persist the terminal metadata. Child supervision reports a provider-aborted child as canceled rather than completed or failed.

Async generators and custom async iterators with an optional asynchronous aclose() are supported. Normal/typed-terminal and explicit close operations invoke that iterator's cleanup once; the host settles owned input-transfer/stream cleanup before publishing completion. Cleanup failure cannot report success. Uncertain interrupted worker RPCs still quarantine/reap their owned worker; forced termination cannot promise that extension cleanup code ran or undo its effects. The existing agent tool loop and host permission hooks remain authoritative; trusted extension workers and permission checks are not OS sandboxes.

Explicit extension provider OAuth and retained vault storage

Trusted provider declarations can retain a private ProviderOAuth binding through api.register_provider(..., oauth=...). Login/refresh/key/model callbacks execute inside the owned extension worker; credentials and callback objects are not public catalog/UI data. Generic ProviderAuth bindings remain separate. These provider callbacks are not MCP registration methods or an Elicitation capability.

Both native hosts expose /extensions auth list or menu, explicit login ID, refresh ID and logout ID, plus /login [EXTENSION_PROVIDER_ID] as a thin selection/login alias. Every operation requires its original idle session/provider registration and a separate owned review; URL opening is a distinct reviewed user gesture, never automatic. Sensitive replies go only to the original worker, not model history or a replacement question. Cancellation/reload/disconnect retires that owner without automatic callback replay. Login/refresh does not grant tools, select a model or change permissions. Logout clears worker memory, not saved data.

Provider-defined OAuth callbacks are independent of MCP's public-PKCE profile. Env/resolver credentials remain private and lazy. A grant that expires while an async key callback is suspended cannot admit a business stream; failure does not silently log in, refresh, replay a callback or choose another provider.

Provider credential persistence is retained through explicit /extensions auth save ID secret-service /ABSOLUTE/NAMESPACE, with equivalent restore and forget actions. This is Linux Secret Service vault only, with an explicit backend/absolute namespace and provider source/profile binding. It shares the owned helper's no-start/no-unlock, cancellation/receipt and no-replay protections. No automatic save/restore/login occurs; missing/locked/unsafe vaults refuse. There is no plaintext file fallback for provider credentials. MCP's separately selected file or secret-service credential backend does not change this provider-vault contract.

Live provider declarations

Provider declarations may also change during an owned command or lifecycle hook:

remove = api.register_provider("local", models, callback)
api.register_provider("local", updated_models, updated_callback, replace=True)
remove()  # Safe: an old receipt cannot remove the replacement.
api.unregister_provider("local")  # Explicit removal; returns whether it existed.

Duplicate names remain errors unless replace=True is explicit. Changed catalogs are sent only when their bounded revision changes, validated atomically by the current worker owner, and reflected in both clients' /model controls without restarting the session. Initial declarations and live changes share the same limits: four providers per extension, 32 models per provider, 64 models per session. Invalid snapshots cannot publish a partial catalog, even from a callback reply. Provider-only updates do not enqueue unchanged terminal/UI snapshots.

Each stream pins its registration identity independently of unrelated provider additions. Replacing/removing its declaration refuses further output and settles that owned stream; it does not call the replacement or base provider automatically. Current declarations constrain offered tools, reasoning options, output limits and actual image context, even if a caller has stale model metadata. Clients refresh selected-model limits after commands and turn-start lifecycle hooks, before prompt preparation and compaction. Removing a selected alias preserves the user's explicit choice; its next use fails rather than silently selecting another provider.

Declarations and Python callbacks belong to their worker/session. A saved model choice is not a serialized callback or credential. An extension must redeclare needed callbacks during setup/session startup, using its existing trusted settings or durable entries as appropriate. Independent child workers do not borrow a parent's mutable callback registry or automatically replay its commands.

Retained MCP interoperability scope

The independent serving-SDK fixture covers retained discovery, schemas, ordinary tool/resource/prompt/completion paths and owned transport cleanup. Active watch APIs and their subscription-specific checks are no longer current capabilities. See tests/fixtures/mcp_official_sdk/README.md for the optional local fixture setup; this is not a claim of general third-party conformance or completed release gates.

MCP prompts may return resource_link blocks, not only text, binary data or embedded resource contents. These are now retained as link metadata, NOT fetched content. The complete JSON block—including annotations and vendor metadata—remains available in preview/search, raw export and explicit editable-draft insertion. URI/name/string bounds, finite JSON retention, size validation and the existing user/assistant-only prompt-role policy remain enforced. No link dereference, file/URL read, tool grant, model turn or automatic draft insertion is introduced.

Independent serving-SDK tests also cover explicit resource reads, prompt catalogs, named prompt completion, URI-template completion, and prompt retrieval over stdio and HTTP. Suggestions do not retrieve prompts/resources: choosing a suggestion edits the argument form, and a separate gesture retrieves the preview. A further explicit gesture inserts untrusted preview data into the composer, still unsent.

Rich execution failures are results, not discarded content

Tools may return AgentToolResult(content=..., details=..., is_error=True) to report a deliberate execution failure while retaining typed text/images/metadata. The error flag must be a real boolean. The existing sequential/parallel tool loop passes it to finalization hooks, tool-end events and persisted ToolResultMessage data; final flags remain aligned if a trusted hook explicitly overrides status. Transport exceptions, cancellations and invalid flags still produce error outcomes.

MCP isError results now use this path instead of raising an exception containing only flattened text. Images and finite structured diagnostics remain inspectable and available in the next context, full-source viewers and persisted history. The advertised success output schema is not applied to a deliberate error payload; content/encoding/size/JSON validation still applies. Callers invoking a proxy tool directly must inspect result.is_error, not expect an exception for a standards-valid isError response. Invalid responses/transport errors still raise.

SDK/Capability tools can return a typed AgentToolResult with is_error=True. This is an execution-result flag, not a successful extension command-artifact/navigation receipt; command artifacts reject it rather than turning a failed callback into a successful artifact. Coding/Claw keep their existing denial/error post-hook protections and permission policy: retaining diagnostics neither executes a denied call nor turns it into success. No automatic retry, second tool loop, file/image read or model turn is added. The normal agent may continue reasoning from the marked tool error.

Provider prompt, reasoning and vision metadata

Callback provider model declarations can include max_prompt_tokens (zero means unreported), supported_efforts (unknown or an explicit bounded list), and vision_limits (max_prompt_images, max_prompt_image_size, supported_media_types). Prompt ceilings cannot exceed the total context window; limits/MIME/effort metadata are validated before catalog admission. Model IDs may include ordinary native routing punctuation such as dots, slashes and colons, without changing their session-owned namespace or resolving them as URLs/files.

These declarations become actual frozen Model metadata—not display-only flags. Coding uses the separate prompt ceiling for its context limit; Claw refreshes it and checks compaction against the stricter prompt ceiling. Independent children inherit actual limits and validate requested efforts through their existing model resolver. Vision count/byte/MIME and unsupported reasoning requests are rejected before callback invocation; a stale caller cannot bypass the currently declared capabilities. Omitted metadata retains the original behavior.

When a provider advertises only specific efforts, choose an advertised level or off; incompatible configured startup preferences fail with cleanup, not a hidden fallback/downgrade. Coding's explicit preference setter likewise rejects unsupported levels without altering live options or persisted settings. Callback/context model budgets and existing host permission/tool restrictions remain unchanged.

CatalogStreamFn is an optional extension of the existing cached ModelStreamFn contract. Its models() enumerates the same configured adapter's active cached models—never another runtime, catalog sync, login or provider activation. stream_fn_from_provider() implements this contract, and session-owned extension adapters compose it with their independent declarations. Non-catalog adapters keep working; they do not gain an ambient catalog or hidden fallback.

Owned event/hook listener cleanup

api.on(event, callback), before_tool, after_tool, context and on_input return idempotent zero-argument disposers. Each disposer removes its own registration, not another registration of the same callback or a later replacement. Existing extensions may ignore the return value as before. Registrations require callables and are bounded to 32 per event/hook kind.

Dispatch snapshots the listeners already admitted for that invocation. Self-removal cannot accidentally skip the next callback, and listeners added during dispatch start on the next invocation rather than unpredictably running in the current one. No callbacks or disposers cross the worker protocol.

The worker reports a bounded monotonic hook declaration when registrations change. The host adopts it only from the current worker owner; stale/duplicate metadata cannot re-arm disposed listeners and conflicting same-revision data fails. New event/input/ context/tool hooks become live without worker restart; explicit disposal updates the same caches. Removing an input hook is an owned action, not a failed-validator escape: failed/unloaded/retired workers cannot clear another owner's required validation. Tool permissions, session/child/UI leases and no-replay cancellation remain unchanged.

Simple extension status

api.status(key, text) / api.clear_status(key) publish or remove bounded, owner-scoped text status. Failed/unloaded/retired workers cannot change another extension's contribution. Widget layout, custom frames and nested/overlay control APIs are removed; normal progress and host permission focus remain authoritative.

Live owned command and shortcut catalogs

api.command(name, description, callback, replace=False) now returns an idempotent registration disposer. Duplicate names still fail by default, including ambiguous case-only aliases. replace=True explicitly replaces the same name; an old disposer cannot remove its replacement. Callable/name/description and 32-command limits are checked before mutation.

api.shortcut(...) returns a disposer for its owned binding and command. Removing a current command removes dangling bindings for that alias; removing an old shortcut cannot delete a later command replacement. Existing host reserved-key/conflict/ remapping and prompt-only ownership rules remain unchanged. Callers ignoring return values retain their existing setup behavior.

The actual worker reports bounded monotonic command/shortcut declarations when registrations change. Current-owner catalogs update without reconnect/reload, and both clients refresh autocomplete and shortcut resolution from the existing complete snapshot. Stale/retired declarations cannot re-arm disposed aliases, conflicting same-revision declarations fail, and host aggregate capacities remain enforced. An already admitted callback keeps its original execution; disposal affects future admission rather than silently replaying or switching that callback. Unknown removed commands are refused without calling a now-missing worker command or disabling a healthy extension. This is metadata management, not an automatic model turn or permission/session/editor mutation.

Host-owned model tool registration

ToolRepo is an SDK instance owned by the host, not a process-global runtime. It creates actual builtin tools and keeps owner-local tool bindings. Capability tools enter the ordinary host inventory through this SDK; MCP uses its separate McpRegistry, consumed directly by Agent. Registration does not activate a tool, grant permissions, create a worker or start a model.

Extension model-tool registration (api.tool, ExtensionRuntime.tools/sync_tools, worker tools/list / tools/call) has been removed. Extensions still provide commands, hooks, UI and model providers; pre/post-tool hooks and read-only tool inventory queries inspect the host's actual tools without contributing tools. See host-owned tool registry.

MCP browsing and manual refresh

Active resource/catalog/combined notification watches are removed from the SDK, transports and native UI. There are no watch/unwatch methods, editable subscription filters, watch jobs or background subscription listeners. Former w resource watch and Alt+T/P/U/W catalog/combined controls are not supported.

Ordinary MCP tools, resource/prompt catalogs, explicit reads/completions and manual refresh remain. Passive tool-catalog invalidation on supported legacy notices only marks cached definitions dirty; it does not fetch a catalog/resource, register tools or start a model turn. Configured Roots and exact request-state continuations, response-scoped SSE, cancellation and bounded legacy GET recovery remain separate transport features. They never authorize uncertain tool POST replay or automatic reconnect. Normal MCP chat/tool traffic and private extension-worker JSON-RPC are not removed by this simplification.

Read-only invocation tool inventory

await api.get_tool_inventory(query="", offset=0, revision="") returns a paged snapshot of the actual owning Agent's configured model-facing tool definitions. This works in commands, hooks, context/provider callbacks and headless invocations where the host supplies an inventory. Parent and child runtimes project independent owners; explicit child tool profiles are applied before startup hooks and retained during final startup inventory projection.

Pages include session_id, owner_id, model, api_enabled, source, revision, status, query, offset, total, next_offset, and tools. Each tool contains only its public name, description, and finite JSON parameters schema. No executor, argument-validation callback, tool context, provider credentials, approval receipt or permission state is included. Schemas are model-facing data: authors must not embed secrets in their public definitions. Reading them never resolves schema references, executes tools, registers new tools or submits a model turn.

api_enabled reflects the Agent's capability gate (supports_tool_calls is not False), not a claim that a provider definitely supports every schema or any tool is approved. Text-only/explicitly unsupported models can still inspect configured local definitions with this flag False. Permission/read-only/child access gates are unchanged and apply separately on actual execution.

At most 32 definitions return per page. Continue with next_offset and the same revision to read every definition; the complete captured inventory is bounded to 2,048 tools and 512 KiB of finite JSON. Empty inventories are valid. Unavailable, foreign, excessive or unstable owner snapshots return None rather than claiming an empty/approved tool set or failing the extension solely for optional inspection. Invalid query/offset/revision input is rejected. Search is a case-insensitive literal substring over name/description, not an executable expression.

Snapshots are captured at callback admission, copied for each read and do not silently refresh during an awaited callback. A later invocation sees newly projected Agent definitions. The metadata revision binds owner/model/capability/definitions; a stale revision returns status stale, no rows and no continuation. It is NOT an executor registration identity, approval or authorization token. Live extension/MCP registration metadata remains distinct from the Agent inventory until the owning loop projects it normally; inspection never mutates that loop or a global registry.

Host SDKs can supply runtime.tool_inventory = lambda: capture_tools(agent, session_id, owner_id) using xdog.agent.extensions.tool_inventory.capture. The runtime overwrites caller-supplied inspection fields and the worker validates the actual owner before installing an invocation context. Existing UI/gateway messages are unchanged: no new viewer capability or automatic UI/model action is inferred.

Unchanged tool-inventory metadata is now cached by content fingerprint in the owned extension worker. Hosts negotiate the private xdog/info capability tool_inventory_cache:1; older workers continue receiving inline snapshots. The first acknowledged invocation transfers the full snapshot, then subsequent calls carry a reference instead of repeating schemas. Parent/session/model/capability and schema changes produce a different fingerprint. This optimizes IPC bytes, not the permission policy or the actual configured tool inventory.

Both sides keep at most eight snapshot identities; the worker owns at most eight 512-KiB snapshots. Resolving a reference validates the owner and returns a copy, so an already admitted callback retains its snapshot even after cache eviction. Missing references are explicitly unavailable. The host clears that cache entry and transfers metadata on a later invocation, never automatically replaying the current command/hook/provider callback. Reload starts a new worker/cache; stale references cannot resolve to a successor's snapshot.

Optional inventory metadata is omitted for an invocation when it would exceed the existing 8-MiB extension-worker RPC frame budget. That invocation reports inventory unavailable, not empty/approved, and a smaller later invocation can inspect it normally. The generic transport still rejects genuinely oversized requests; no frame limit, tool argument, registration proof or approval gate is relaxed. Caller-supplied inline/ref fields remain overwritten by the host's actual projection.

The native /tools viewer also distinguishes valid inventory readiness from ordinary source-index readiness. Failed/loading/withdrawn/disconnected inventories cannot be exported under another scope or replaced with canonical conversation records. Reconnect clears old view admission; after handshake, explicit refresh reads the actual current owner. A valid zero-tool snapshot remains distinguishable from an unavailable snapshot. This does not automatically replay tool calls or agent input.

Owned compaction extension hooks

Trusted extensions in Coding and Claw can register two lifecycle callbacks:

def setup(api):
    def before(context):
        prepared = context["compaction"]
        # None / {} continues with the selected session's normal summarizer.
        if not prepared["messages"]:
            return None
        # Or explicitly veto: {"cancel": True, "reason": "Preserve this context"}.
        # A custom summary replaces ONLY the summary request, not permissions.
        return {
            "summary": "A complete summary of the older conversation...",
            "first_kept_index": prepared["first_kept_index"],
            "details": {"format": "my-summary-v1"},
        }

    def committed(context):
        receipt = context["compaction"]
        api.append_entry("note", {"compacted_messages": receipt["before"]})

    dispose_before = api.on("session_before_compact", before)
    dispose_after = api.on("session_compact", committed)

The before callback receives a copied preparation containing the complete typed messages, default first_kept_index, original_count, estimated tokens_before, context_limit, automatic, optional user instructions, and prior committed metadata in previous. It is not a clipped transcript preview: large preparations use the existing owned/checksummed transfer (128 MiB maximum, not one 8 MiB frame). Callbacks and extensions receive independent copies; changing the preparation cannot edit canonical history or the copy used by another callback.

Returning None/{} continues; { "cancel": true, "reason": "..." } vetoes without replacing history. A custom result requires a nonempty summary up to 256 KiB; optional details must be finite JSON up to 64 KiB. A supplied retention index must start a complete user turn and retain the latest user turn and its tool exchanges. Indices describe the original preparation, not the compacted conversation. Only one custom provider may win: conflicting summaries, invalid results, failed hooks, and unsupported worker operation versions fail closed, without a model fallback. Existing registration disposers remain owned and idempotent.

session_compact is emitted only after disk and in-memory history commit. It receives source, retention index, original count, copied details, automatic/manual flag, and before/after counts. Source/details metadata survives session persistence and branch restore. An admitted commit and its observer dispatch settle before compaction ownership is released, including repeated caller cancellation.

These callbacks have api.has_ui == False (an availability hint, not authority) and cannot borrow command editor, browser/navigation, or dialog authority. Trusted extension code still has the process user's permissions; this is not an OS sandbox. Claw retains its normal guarded durable-memory flush/archive stage even when an extension supplies the summary. Automatic veto keeps existing context; manual veto is reported explicitly. Cancellation during a silent before callback retires its worker; reload or unload explicitly rather than replaying its effects.

Committed compaction metadata may additionally contain recovery_branch_id. In Coding and Claw this identifies a durable, complete pre-compaction checkpoint in the owning session tree. It is an opaque navigation identity, not a filesystem path or permission grant. The before hook's previous metadata retains prior recovery links; repeated compaction does not replace earlier checkpoints. Core history and access/extension state recovery still uses normal host navigation and its idle, ownership, and complete-tool-batch checks.

Hosts preparing an extension owner before session navigation may create an ExtensionEntries owner with a buffered persistence sink, then call host-side bind_persistence(callback) after successful admission. This transfers the existing validated entry owner to durable live persistence without replaying setup callbacks. It is not exposed as a worker permission or a navigation operation. The host still owns admission, cancellation, current session/model identity, and external side-effect limitations; explicitly trusted extension code is not sandboxed by this mechanism.

Complete rendered-context image admission

The shared Agent loop now checks typed images in the actual rendered Context after configured context hooks/message conversion and before every model adapter request. Retained user images, tool-result images and explicitly hook-added images all count against available model image limits. The validator never prunes images or silently converts them to text to make a request succeed. Rejection retains canonical messages/tool results and produces an explicit error.

Metadata comes only from the configured stream adapter's cached resolve_model interface; registered providers use the same contract. Declared positive count, byte and media limits and advertised image input are enforced. This does NOT apply the four-new-attachment composer cap to a model advertising a larger whole-context limit. Unspecified byte limits use the existing neutral 32 MiB payload safety bound, not an invented vendor limit. Base64/typed MIME checks are not a complete raster decoder or a guarantee of provider wire-format conformance.

Validation runs off the asynchronous loop. Recheck the same adapter's image profile after validation: changed/disappeared/malformed metadata refuses the request, without guessing successor limits or replay. A cooperative abort during validation also prevents the adapter request after byte checks settle.

Opaque SDK streams (or transparent wrappers returning no metadata) remain caller-owned: the validator does NOT borrow another/global provider's catalog or claim that unknown limits passed. Such callers need to supply their own capability contract/validation. Native/registered metadata-backed app paths are exercised; unknown-provider/raster/native-platform interoperability remains open.

Agent Client Protocol (ACP)

xdog.agent.acp is an opt-in, harness-neutral adapter/backend contract using the official Python ACP SDK; it does not import coding, Eyes, TUI, claw or flow. xdog-agent acp exposes standard stdio initialize/new/load/prompt/update/cancel interoperation. Permission/tool policy stays in the injected backend. JSON/frame validation rejects duplicate/nonfinite/reserved routing metadata; stdout is only protocol. Progress is not final prompt completion.

Install xdog-agent[acp] to use ACP; the SDK is not required by base Agent. The optional dependency is agent-client-protocol>=0.12.1,<0.13 on Python 3.12–3.14 (the current SDK is excluded by marker on 3.15+). Normal non-ACP imports remain independent of that SDK. Coding's session ownership and durable inbox extensions are product policy, not a second generic Agent loop. Historical external-event records retain only codec/projection compatibility, not a live event host. Agent.prompt also accepts a list of independent Agent messages in one turn; coding's bounded SessionController batches use that path without another tool loop. Coding's automatic durable Inbox, batch IDs and message-cancellation policy are harness policy, not forced into the generic ACP backend. See ACP session routing.

Basic extension interaction scope

Trusted extensions can use api.input, api.confirm and api.select with normal cancellation, plus bounded notifications and simple text status. Arbitrary custom renderers, typed plugin forms, nested dialogs, extension overlays/fullscreen frames and widget layouts are removed. Native approval and recovery reviews remain host- owned; they expose no plugin layout or editor/session-mutation authority. Core TUI overlays, transcript fullscreen, Unicode, clipping, cursor and tool-approval behavior remain.

Explicit read-only extension views

Trusted hosts can register an owner-bound JSON read port on an existing ExtensionRuntime.views and configured extension callbacks can call await api.read_view(name, query={...}). This is independent of TUI/dialog permission and cannot execute host methods or tools. Missing/revoked views return unavailable, never a fake empty dataset. Queries/snapshots are finite, bounded JSON; actual client/owner/binding identity is checked across awaits and through callback publication. Coalesced view_changed hints use the existing process owner without starting a dormant worker or model. See docs/extension-owner-views.md in the repository for the contract and limits.

Public extension command names

api.command(..., public_name="show-state") exposes /show-state while retaining its internal extension/command identity. The same declaration drives execution, help, argument completion and shortcuts. Hosts reserve builtin/skill names; conflicting names are rejected rather than shadowed. This is a user command, not a model tool. See docs/extension-public-commands.md in the repository for the snapshot, ownership and stable-keybinding contract.

Installed host capabilities

xdog.agent.capabilities composes trusted installed xdog.capabilities entry points on actual session/group/application owners. Discovery is inert; a snapshot cannot name code to import. CapabilitySet owns startup/flush/close, while pinned ParentPort and ChildPort handles retain actual tool identity and existing permission gates. This layer owns no Goal or observation schema and starts no second Agent loop. See docs/capability-boundary-design.md in the repository.

SDK capability assembly

CapabilitySet(scope, tool_repo=..., skill_repo=...).select(names).open(host) opens only the selected provider subset and yields CapabilityAssembly. Application scope may omit host; session/group require real owner ports. Explicit start / aclose remains available for advanced multi-scope hosts. Discovery and selection do not start resources or construct an Agent.

assembly.create_agent(stream_fn, tool_repo=None, skill_repo=None, config=None, **Agentkwargs) adds repositories by union to capability/Set contributions and returns a native Agent. Supplemental .select(()) cannot remove capabilities. Different same-name tools or skill definitions fail instead of shadowing another owner. There is no second enable list. Native Agent still uses tools / skills.

SkillRepo().register(directory=path, name="host") adds a local source to default packaged discovery; builtin=False disables that default catalog. Skill bodies are not activated by discovery. The read-only skill list/load entry retrieves instructions on demand; explicit host activation remains independent of tools. Flow, Eyes and Goal do not activate packaged bodies by default.

Flow starts no daemon, and its actual CLI execution does not inherit the host Agent sandbox or approval policy. Selecting Eyes then opening the Set ensures its independent shared daemon; closing the binding only detaches. Close Agents before Sets. Child delegation and restore must retain current real owner grants; no snapshot restores repo objects, credentials, connections or permissions. See SDK capability assembly for a runnable offline example and tool repositories.

Capability contributions are optional: skill_names advertises packaged skills, tool_names advertises tools, and a binding may additionally own runtime resources or register extension hooks. An empty tool contribution is valid. None of these metadata labels grants authority or starts an Agent loop. Flow/Eyes bodies remain opt-in. Explicit activation now requires a valid loadable body; missing skills are not silently reported active.

Use assembly.restore_agent(agent, snapshot, allowed_models=...) (or restore_agent_parts) when restoring with live capability defaults. It validates saved bindings before atomically adding missing current default session skills; consumed/pending turn skill lifecycle, current executors and host approval policy are preserved. Native Agent.restore() remains independent of capability discovery.

Part of xdog

This package is one piece of xdog, a local-first toolkit for building, running and scheduling LLM workflows. The centrepiece is xdog-flow — a typed workflow format and compiler.

Documentation: https://xdog.942295.xyz

Licence

Copyright (c) 2026 HugeMan <942295.xyz>

GNU Affero General Public License v3.0 or later — see LICENSE. Output compiled by xdog-flow is exempt; see the Generated Output Exception.

Metadata

Release files for xdog-agent 2.5.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for xdog-agent 2.5.0
File Size Uploaded Provenance
xdog_agent-2.5.0.tar.gz 687.8 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for xdog-agent 2.5.0
File Interpreter ABI Platform
xdog_agent-2.5.0-py3-none-any.whl Python 3 none any Details

Total release size: 1.1 MB

Release files / xdog_agent-2.5.0.tar.gz

Download URL xdog_agent-2.5.0.tar.gz
Size 687.8 kB
Tags Source
SHA-256 checksum
How to use checksums
7ca9b2ca5bf843c0679c6b44495ce2a22e6c9b9e9fe89c2bbfe560eb9f144983
BLAKE2b-256 checksum
How to use checksums
deb90d7f47c60275d7bcb86259183750efdd1ff4f4aba19a411267ba056aaffd
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 24, 2026.

Transparency log

Release files / xdog_agent-2.5.0-py3-none-any.whl

Download URL xdog_agent-2.5.0-py3-none-any.whl
Size 421.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f8e587eae320e91c13cd227645f350769bbd745598381f91333654f5c14603ae
BLAKE2b-256 checksum
How to use checksums
cf5d0944a22eb036463295915ca9c42913f9499ecc701a6c6ca0358763abb25a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 24, 2026.

Transparency log

Security for xdog-agent 2.5.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for xdog_agent-2.5.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 24, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page