Skip to content

Plugin architecture

Wuu loads plugins into a Go runtime host and an Electron desktop host. A package may use either host or both, and can also supply declarative contributions. This page explains how the current implementation discovers, activates, composes, and retires those contributions. For package fields and code examples, use the authoring reference.

The Go host owns tool dispatch, execution identity, service routing, persistence and permission boundaries, and resource cleanup. Storage implementations and permission policy are selected services; see replaceable providers. A plugin owns its feature logic, prompts, business state, and background policy. Features such as automation, memory, and subagents use host services instead of maintaining a second session engine.

The desktop host owns windows, application state/actions, input submission, and the bridge to the Go app-server. The selected ui.shell provider owns the application layout and presentation. Plugins register views and contributions through the public SDK; they do not receive an Electron main-process object or direct access to host React state. The host provides React and shared UI components so plugin controls follow the same rendering and styling contracts.

Bundled and external packages use the same runtime and desktop registration contracts. Bundled provenance affects discovery and trust, not the meaning of a tool, capability, or UI registration. A copied package does not become official merely by using a bundled plugin’s ID.

Wuu maintains one official plugin authoring SDK: TypeScript @wuu/plugin-sdk in packages/plugin-sdk. Bundled and external plugins use its runtime and desktop contracts. The runtime transport remains language-independent JSONL: an external process written in another language can implement that contract directly. Wuu does not provide a second official authoring SDK for those processes.

The Go core remains the runtime host. internal/sdk is an internal facade for embedding the host in Wuu entry points; it is not a plugin authoring API.

With Peers enabled, new coordination requests appear as compact cards below the source turn in the default desktop conversation. A card opens the peer in the main conversation; the header offers a return to the source, preserving its unsent draft. Session-message source links also open the main conversation. Cards use the same spacing and corner radius as inline error notices and follow the UI font preference.

The status belongs to the specific request, not the peer’s later activity. Archived or missing targets are unavailable; a failed status refresh shows “Status unavailable”. Cards refresh while the source conversation is visible and are removed when Peers is disabled.

This initial implementation displays requests retained by Peers: at most 64 records, with settled requests eligible for pruning after seven days or when capacity is needed. Older records without a source turn ID have no card. The original tool results and session messages remain in conversation history.

The nine bundled business plugins are todo, ask-user, memory, subagent, peers, automation, goal, dream, and next-message. Their source lives under plugins/<id>/src/; each exports the public TypeScript RuntimePlugin contract. The build produces self-contained runtime.mjs files beside the bundled manifests. Go embeds those packages and remains the runtime host. The former wuu-<id>-plugin Go helper executables are no longer needed.

The desktop starts these runtimes with its own Electron executable in Node mode; the CLI uses Node from PATH or the absolute WUU_NODE_EXECUTABLE override. There is no separate installed npm dependency for a bundled runtime. The same JSONL initialization, service negotiation, activation, cancellation, and shutdown contracts apply to bundled and third-party plugins.

This changes implementation language, not plugin identity or durable data layout. Existing settings, approvals, plugin storage keys, memory Markdown files, and session request identities remain in their existing locations. No export/import step or second Go implementation is used. Module variables and timers are rebuilt on replacement. Automation recovers durable work with stable dispatch identities; Goal restores unfinished goals paused for an explicit resume; Dream retains its settings, run state, and failure backoff. Disabling a plugin stops its owned behavior while preserving durable data and conversation history.

For the edit-and-reload workflow and executable checks, see develop a bundled plugin.

Discovery reads user packages, workspace packages, authorized development directories, and bundled packages. User packages normally live in $WUU_HOME/plugins; workspace packages live in .wuu/plugins under the workspace. Legacy singular plugin directories are also recognized. When IDs collide, later discovery normally replaces the earlier candidate; bundled packages are considered last. An explicitly authorized development package with the same ID overrides its bundled package while retaining development provenance. An ordinary untrusted same-ID copy does not gain that override.

Finding a manifest does not mean running it. The host evaluates enabled state, trust, platform and minimum-version compatibility, package relationships, and runtime negotiation. Required package dependencies use legacy requires IDs or versioned dependencies entries. Missing or incompatible required dependencies leave consumers and their transitive required dependents inactive; breaks and required dependency cycles can reject the activation plan. Matching optional dependencies add ordering when possible, but their absence or failure does not block consumers. conflicts produces a diagnostic rather than choosing a winner automatically.

Installing a local package grants trust to its recorded source and enables it. Same-source updates retain trust and the enabled state; source changes require confirmation. Development authorization applies to a specific directory. Package identity includes content and source context, so an ID or version string alone is not proof that the currently approved bytes are running. See plugin management for the actual install and update flow.

Package relationships do not install dependencies, enable disabled packages, or grant trust. They also do not grant service-call authority: runtime provided_services and required_services negotiate service names and major versions separately. See package relationships.

Without a composition override, the product includes only packages admitted by dependency resolution. Removing or disabling an add-on leaves its required package consumers installed but inactive, with dependency diagnostics; optional and independent packages remain eligible. This does not relax explicit choices: an unavailable module or service provider selected by the user’s composition still rejects preparation. Installing or enabling a consumer selected for activation also requires its dependencies to be available.

App-server clients can submit related enable/disable choices together through extension/package/update with {"updates":[{"id":"plugin:user:provider","action":"disable"},{"id":"plugin:user:consumer","action":"disable"}]}. The host validates the complete selection, prepares one candidate, persists the settings with one compare-and-exchange, then activates that generation. Each entry may include fingerprint to reject a stale catalog choice. The array must be nonempty, contain unique package subject IDs, and use only enable or disable; top-level single-package fields cannot accompany it, even when empty. The original single-package request remains available for trust and pending-package updates. A batch does not grant trust or select dependencies automatically. For selected, trusted packages explicitly enabled by the request, required package and service dependencies must be available during preparation. Validation or preparation failure and a confirmed compare-and-exchange rejection preserve the previous settings and generation. If a write outcome is unknown, reread the configuration before deciding what to do; do not assume rollback or retry automatically. An activation failure after commit reports the committed state as described below.

The runtime carries the admitted package relations into its service lifecycle graph. Matching optional package dependencies order preparation and activation before consumers and cleanup after them, without propagating provider failure. If an optional package edge would form a cycle after service binding, the host omits that ordering edge rather than rejecting the generation. registry.introspect reports the accepted graph in dependencies and omitted package relations in dependency_issues (optional_missing, optional_version_mismatch, or optional_cycle). Package versions remain separate from service major versions; optional package relationships do not grant service access.

Reload does not transfer arbitrary process or module variables into the replacement. Keep durable business state in host storage or session records, and version any persistent data whose format can change. Conversation history and plugin implementation lifetime are separate.

A runtime generation groups the selected packages with their processes, tools, capabilities, hooks, skills, MCP connections, service registry, and owned package snapshots. Building a candidate does not immediately replace the active generation. Development publication may proceed while turns are running: each retained generation owns its package snapshot, so replacing the published source does not change the files used by running work. The host coordinates catalog reads with publication rather than blocking publication for an entire turn.

Automatic observation of strictly additive, inferred add-ons can prepare in the background while new and existing conversations use the committed generation. This requires unchanged configuration and source authority, unchanged existing packages, and healthy required providers. The host rechecks the exact catalog epoch, configuration, package contents, and base generation before publication. A superseding mutation or shutdown cancels the unpublished candidate. Same-ID replacements, removal, disable, trust or configuration changes, and explicit composition selections still use the fenced transition path. Package discovery and immutable snapshot copying remain synchronous; this optimization removes initialization and preparation waits, not those costs. A structurally invalid candidate that rejects the whole graph does not gain an indefinite admission exemption.

Lifecycle v2 separates declarations from state validation. initialize returns contracts without service calls. After binding the complete dependency graph, the host calls prepare in provider-before-consumer order. During this callback, declared services may be called only through methods marked preparation: true; these methods promise no durable writes or external effects. This applies to selected native and process providers alike. Plugins start timers and other active behavior in activate. Preparation can also start MCP connections. These are trusted processes: restrictions on host service calls do not prevent direct filesystem, network, or other external effects during initialization. Preparation is not a side-effect-free sandbox.

Initialize also carries supported_host_features, a list of exact versioned host-semantic names, separate from service discovery. A plugin can return required_host_features; the SDK verifies each requirement against the offer before replying, and the host rejects unsupported requirements. Missing advertisement means no feature support. Unknown advertised names are forward-compatible but grant no other feature. Requirements are not optional and never downgrade. state.documents.authority.v1 is automatically required by the SDK for authority-aware document descriptors; old plugins without requirements remain unchanged.

The runtime transition prepares the candidate, commits the associated package or policy changes, and only then calls native activate. If preparation or the durable commit fails, the candidate is closed and the old runtime remains current; candidate activate is not called.

Providers prepare and activate before their package and service consumers. A required provider’s initialization or activation failure blocks its transitive required consumers, including relationships through declarative-only packages. Blocked consumers do not expose their tools, hooks, skills, MCP servers, or desktop contributions. Independent packages can still activate; a provider’s own runtime failure does not automatically remove its independent declarative contributions.

If an already-active provider process exits, existing recovery checks revoke its transitive required consumers before preparing a replacement. This uses the normal consumer cleanup and blocks future access to their tools, hooks, MCP servers, skills, and desktop contributions. A failed replacement does not re-enable those consumers. Detection follows the existing recovery checks, not an immediate crash notification.

Revocation belongs to the affected generation. Retained conversation and worker toolkits stop listing or loading its plugin-owned skills, including when working in another checkout. Independent workspace skills remain available. A failure in a retained old generation does not revoke a healthy replacement with the same package ID. Already-materialized prompts and conversation history are not rewritten, and completed effects are not undone.

Disable and removal take effect after their changes commit and revoke the affected plugin across retained generations without waiting for an idle turn; inspect the operation result. To restart the app-server without activating plugin contributions, use its safe-mode option.

The desktop also has a native Open Recovery Center… menu entry, available without a working application renderer. Launching the desktop with --safe-mode (or WUU_SAFE_MODE=1) opens an independent diagnostic window without starting the normal application backend or plugins, and without loading the normal configuration. It preserves saved data and does not substitute a different storage provider.

Recovery offers bounded reload attempts, explicit diagnostic/normal restart, and a previewable minimal diagnostic export. Stopping a wait does not cancel backend work. Restart can interrupt running work; external effects may already have happened. Normal restart uses the existing configuration, not a last-known-good rollback. Recovery does not replay tasks or permanently change plugin policy. Reports contain only version, platform and mode, never raw logs, configuration, paths or conversation content. Existing files are not overwritten when exporting.

After the commit succeeds, an activation failure publishes the new generation in a degraded state instead of restoring the old one. The operation reports that the change was committed but runtime activation failed, and the inventory retains the failure diagnostics. Mutation handlers still synchronize committed settings, finish removal cleanup where applicable, and notify clients. Refresh accepts that published state and logs its activation failure rather than treating it as a failed refresh to retry on every watcher interval. External effects already performed by activation cannot be rolled back.

For a strictly additive installation, host preparation can run asynchronously while unrelated turns continue on the committed graph. A successful CLI publication is not a readiness receipt from an already-running app-server. Before starting work that requires the new plugin, observe that same server’s config/read inventory or plugin/inventory/changed notification: the exact published fingerprint must be enabled and have runtime_state: "active" in a committed graph_generation. Report last_error or a failed runtime state rather than retrying dependent model calls. Existing turns still retain their original bindings.

For an ordinary same-ID update, an existing conversation adopts the published generation at its next idle turn boundary. A running turn, its outstanding workers (including terminal finalization), and its managed processes retain their generation until that work settles. The host rebuilds plugin bindings together while retaining conversation history and session identity; the previous generation retires after its last lease is released. An idle superseded runtime is released without waiting for another user message. Catalog-only changes and pre-commit failures do not replace a healthy pinned generation. Later cleanup errors are reported separately and do not roll back the committed change.

Local browser and computer-use control has a shorter lifetime than retained tool and prompt code. Before activating a replacement, the host revokes the outgoing generation’s local-control ownership and stops affected previews. Disable, removal, runtime closure, and an activity-bound MCP connection disconnect or replacement also revoke that owner. Plugin retirement is scoped to the plugin ID and generation ID; MCP connection retirement additionally identifies the exact connection. A late failure cannot stop a healthy replacement. A healthy MCP reconnect in the same plugin generation may serve a new explicit user run. Revocation blocks further local-control actions in the affected run; an old owner cannot reacquire control or switch to another local-control backend. A new explicit instruction is required to start fresh work.

Generation-owned host.background jobs have a different lifetime from conversation workers. Publication stops new background admission in the old generation and cancels its existing jobs and autonomous service executions before the replacement activates. Plugin-created timers and other direct effects still need explicit cleanup; host cancellation cannot undo effects already performed.

During preparation, a candidate that fails to initialize or negotiate required services cannot replace an already healthy runtime. A newly discovered optional plugin may remain failed without disabling other plugins. Intentional removal remains separate from a failed replacement.

When automatic refresh cannot prepare the same candidate, retries back off from one second to a maximum of 30 seconds. During that wait, background notifications and waiting turns share package-content checks at a one-second cadence; configuration or newly published catalog epochs can trigger an immediate check. Changed inputs can retry sooner. Successful refresh resets the delay. Lease contention uses a separate short retry and does not count as a failed candidate. The catalog’s explicit Refresh action bypasses this automatic delay, reports preparation errors, and retains the previous generation on a pre-commit failure.

Reload preserves conversation history and the conversation-scoped prompt_cache_key used for supported provider routing. A stable cache key is not a content hash or a cache-hit guarantee. An implementation-only change can leave the model-facing prefix unchanged; changed tool definitions, prompt sections, or deferred discovery state change request content and can invalidate provider prefix reuse even with the same key.

Dependent packages retire before their providers, including revocation of retained generations after a disable or removal. Optional dependency failures do not cascade into consumers. Retirement closes MCP connections, clears prompt and compaction registrations, cancels plugin executions, shuts down processes, revokes service routing, and removes owned snapshots. Shutdown retains access to the host services needed to release plugin-owned work before routing is closed. Plugins must still stop their own timers, subscriptions, and background work; the host cannot infer arbitrary resources created by extension code.

Native and process runtime clients are owned by a generation-local cordis-go scope. The resolved dependency graph supplies preparation and activation injections; the registry does not maintain a second native-provider lifecycle. Duplicate runtime identities reject the candidate before processes start. Client resources remain owned outside the activation scope so a prepared or failed client can still shut down, and providers remain available until consumer cleanup returns. Wuu serializes lifecycle transitions and waits for cleanup before returning from retirement. Preparation, activation, and cleanup panics become attributable errors. A closed generation cannot reactivate. Executions and generation leases keep the version-retention rules above; Cordis does not independently reload a retained generation.

Cleanup produces structured revocation records, persisted in plugin-generation-revocations.jsonl under Wuu’s home directory. These distinguish publication success from retirement failures and help diagnose resources that did not close normally.

Runtime process writes honor cancellation and a bounded transport deadline. If a frame is interrupted during delivery or an execution cancellation cannot be delivered, the host retires that transport. Canceling an invocation before it is sent leaves other running work intact. Shutdown terminates the runtime process group on macOS/Linux and requests process-tree termination on Windows, with a bounded pipe drain. Windows tree cleanup is best-effort if the runtime has already exited. Unexpected nonzero shutdown exits remain visible in retirement reports. Runtime stderr keeps the most recent 64 KiB and marks omitted earlier output; verbose logs cannot grow this buffer indefinitely. These limits do not shorten a running tool’s execution deadline or replace the generation-retention rules above.

Runtime plugins communicate over JSONL. Versioned services use the host.service.call gateway: providers declare names, versions, and methods; consumers declare service names and major versions. The registry binds consumers to providers and supplies caller identity. A missing required provider prevents the consumer from becoming usable. Duplicate providers for the same name and major version require an explicit choice; an ambiguous candidate is rejected before activation.

Select a runtime service provider in the trusted user configuration:

{
"composition": {
"version": 1,
"services": [
{ "service": "search.provider", "major": 1, "provider": "my-search" }
]
}
}

The selected package must already be enabled and trusted and must declare the requested service major. Bindings do not install or enable packages. Duplicate bindings, an unavailable selected provider, and an ambiguous service reject preparation and preserve the previous running generation. A selected provider that crashes or is removed causes calls to fail; another registered implementation is never used as a fallback. With no explicit binding, a service must have exactly one provider. Registry inspection reports the selection layer in binding_source, or unique-provider for an unambiguous discovered service, alongside its version and provider ID.

Composition versions 1–5 are independent of product CalVer and service versions. Version 1 supports inline profiles and module/service choices; bundles and patches require version 2 or later. New configurations use version 5. Normal startup ignores project overrides of composition, including case variants, because bindings select execution and credential policies. Configuration changes apply during generation preparation; admitted executions retain their registry. Restoring a selection creates a new generation and does not undo external effects or migrate provider data.

Version 1/2 configurations receive a read-only compatibility projection for behavior extracted from the host: selecting wuu.session-execution adds wuu.agent.policy.default; selecting the session consumer or kernel adds wuu.model-execution and the native model provider wuu.model.native; selecting a shipped desktop surface adds wuu.default-application. Legacy product sessions also select wuu.engines.native and wuu.engine-execution; a kernel-only state scope does not acquire engines. An explicit replacement binding or an explicit removal is preserved, and empty products remain empty. Introspection marks these additions as migration:v1-to-v3 or migration:v2-to-v3. The original configuration is not rewritten, so it remains available for recovery with the previous executable.

Version 3 retains its original catalog and does not repeat these execution migrations. Versions 1–3 also project the explicit resource-authority boundary with migration:vN-to-v4: session hosts select wuu.resource-access and the native resource policy unless excluded or explicitly replaced. Version 4 never fills missing dependencies: include the required providers or select replacements. Versions 1–4 also preserve combined feature presentation with migration:vN-to-v5 when a renderer and the old goal or automation runtime are selected; explicit UI exclusions remain excluded. Version 5 selects their runtime and UI packages independently. Before converting an old source to version 5, explicitly include its projected providers. See configuration upgrades. Do not downgrade by changing the version number alone; restore the previous compatible source and provider versions.

Developers and running agents can use the public host.composition service to try an independent runtime, explicitly select it for a logical agent, and undo selection without publishing a global generation.

A named runtime composition can also select the complete module set. For example, after installing and enabling both service composition example packages:

{
"composition": {
"version": 1,
"profile": "writing",
"profiles": {
"writing": {
"modules": ["text-analysis-example", "writing-report-example"],
"services": [
{ "service": "example.text.analysis", "major": 1, "provider": "text-analysis-example" }
]
}
}
}
}

profile selects one entry in profiles. A profile supplies modules, ordered bundles, or patches, including all dependencies. Module order does not determine service selection or lifecycle order. An explicit empty array starts no modules. Omitting a profile, modules, and patches preserves enabled, trusted discovery defaults. An unknown profile or bundle, repeated bundle, duplicate module, excluded binding target, unavailable selected module, or failed selected runtime rejects preparation. Ordinary module selection cannot install, enable, or trust a package. Filtering happens before runtime, hook, skill, and MCP contributions are loaded.

Bundles are reusable static configuration. For example, the same analysis capability can be shared by several profiles:

{
"composition": {
"version": 3,
"profile": "writing",
"bundles": {
"analysis": {
"patches": [{
"modules": { "text-analysis-example": true },
"services": [{ "service": "example.text.analysis", "major": 1, "provider": "text-analysis-example" }]
}]
}
},
"profiles": {
"analysis": { "bundles": ["analysis"] },
"writing": {
"bundles": ["analysis"],
"patches": [{ "modules": { "writing-report-example": true } }]
}
}
}
}

Resolution starts with an empty module set and applies the selected profile’s bundles in listed order, then its inline modules / services, then its patches. Top-level modules / services follow, and top-level patches apply last. Each inline modules array replaces the complete set; services override by name and major. A patches-only composition also starts empty. Bundles contain patches and optional module defaults; they do not execute code or include other bundles.

Bundles in version 2 and later may declare optional_modules as an array of package IDs. These packages are enabled by default for the profile and selected when available; a missing, incompatible, untrusted, or explicitly disabled package does not block the composition. This default never grants trust. An explicit module selection or patch overrides the optional default. A required service binding still requires its provider to be available.

In a patch, modules maps IDs to true (include) or false (exclude). services replaces individual bindings; remove_services accepts { "service": "...", "major": 1 } entries to remove explicit selections. Removing a binding returns to unique-provider resolution, so multiple remaining providers still fail. Excluding a module does not silently erase bindings that target it: remove or replace those in the same composition. Duplicate or conflicting operations within one patch fail; later patches may intentionally override earlier choices. Empty arrays survive configuration saves. Resolution builds fresh state, so one profile cannot mutate another.

Native modules include kernel (the native service collection), wuu.default-loop (driver.default), wuu.tool-execution (the consumer for tool security services), and wuu.session-execution (the consumer for the selected driver). State, permissions, Project coordination and the application shell have their own provider contracts. Explicit module sets include all needed dependencies. Excluding kernel removes its services and result materializer; it does not select a substitute. Safe mode starts no composition modules, even when the configured selection refers to unavailable packages; discovery remains available for recovery.

plugin/registry/introspect returns composition.profile, ordered layers, the last module-set change in module_source, and each selected module’s source, package version and current runtime state. Sources identify inline choices (profile:<name> or composition) and zero-based patches (bundle:<name>:patch:<index>, profile:<name>:patch:<index>, or composition:patch:<index>). Native module versions identify the product build; service versions remain separate. Packages without a runtime have no runtime state. This diagnostic also works when the native registry-introspection provider is excluded. Same-ID updates retain running executions; removing a module revokes it across retained generations, with consumers completing cleanup before their providers stop.

Package dependencies and selected service dependencies share one generation lifecycle graph. Providers activate before consumers; cleanup runs in reverse order while provider resources remain available. A required missing service, incompatible major, or failed provider blocks transitive consumers. An absent optional service does not block activation; an available optional service participates in ordering. Cycles across package and service edges reject preparation before activation. Retained generations keep their own resolved graph when newer versions change requirements. Removing a provider stops its required consumers; independent plugins remain active.

wuu.state.sqlite and wuu.state.jsonl provide state.documents@1; an external provider can implement the same contract. The product’s session store uses selected documents for history, execution/recovery state, driver bindings and Project state. Legacy conversion preserves its source and rejects unknown nonempty tables or conflicting destination state. Selecting a backend alone does not perform a data transfer.

Consumers declare requireDocumentStore() and use documentStore(host); providers declare documentStoreDescriptor(). The provider guide defines bounded queries, revision-pinned cursors, exact-transaction retry receipts, optional compaction/import and offline transfer. Clients stay with their captured generation. Ordinary methods do not advertise cleanup authority; shutdown persistence requires the lifecycle owner’s admission.

Providers may opt into documentStoreDescriptor({ checkpoint: true }) and implement checkpoint {} with state.documents.checkpoint.input.v1 / output.v1, available during preparation. It returns { version: 1, revision, documents_digest, receipts_digest } using the same canonical digests as full export. The host uses this committed checkpoint for startup and still proves that live peers share storage. Both native providers retain a generation/revision-bound checkpoint and update it from changed keys and the new receipt; missing or stale metadata is rebuilt. Preparing an old store computes its first checkpoint without writing, and activation may persist it if the binding and revision are unchanged. Ordinary startup is not a full physical integrity audit: explicit check, export and migration still inspect complete data. Providers without this capability retain paged verification. A wrong checkpoint fails admission; capability errors never fall back to another provider.

Desktop startup prioritizes the selected workspace and restores the conversation identified by window recovery when available. A failed historical resume keeps the original data and catalog entry and opens a usable draft with the reason. Completed legacy imports are authoritative; startup no longer fingerprints the retired source database. Legacy projection cleanup runs in resumable batches and skips undecodable records. Current conversations retain atomic commits and execution recovery.

SQLite uses full synchronous WAL transactions. JSONL appends checksummed records and rebuilds its index from the log. It discards only an incomplete final record; complete malformed records fail with state_corrupt. Both serialize operations through an OS advisory lock and verify the same durable authority pointer. A migration prepares and verifies a new generation, then atomically publishes that pointer. All older handles subsequently fail with state_binding_changed, including handles in other processes. Merely selecting a different backend fails with state_migration_required. Stop business consumers, migrate, then select and restart the target provider. An interruption before publication retains the source; after publication the target is authoritative. Old generations remain for inspection, never for dual writes. Missing authority in a nonempty directory fails rather than silently creating an empty store.

Administrative commands operate on an explicit document-store directory:

Terminal window
wuu state check --dir "$STORE_DIR"
wuu state export --dir "$STORE_DIR" --file snapshot.json
wuu state import --dir "$NEW_STORE_DIR" --backend jsonl --file snapshot.json
wuu state migrate --dir "$STORE_DIR" --from sqlite --to jsonl

Snapshots include documents and the full durable receipt sequence. Import requires an empty destination and verifies the restored data before publication; it never overwrites existing state. check validates data and reports counts and binding identity without document values. Filesystem barriers must succeed; unsupported directory syncing is an error, and an error after pointer publication explicitly reports the committed binding. Validate durability on the target filesystem and platform before relying on a replacement. Receipts are retained without pruning. export {} still materializes the whole snapshot; providers explicitly opt into paged export with documentStoreDescriptor({ paged_export: true }), and clients use exportPage() on the same method with v2 input/output schemas. Checkpoint scans pin revision and generation across ordered document and receipt pages, requesting 32 MiB and 256 items by default. Requests allow at most 32 MiB and 10000 items. Multi-item pages fit the byte target; one oversized record may exceed it, subject to the existing external 64 MiB frame limit and no fallback. Payload memory is O(page + largest record) plus O(receipt IDs) validation metadata. JSONL cold replay of large frames remains a separate memory boundary. See paged export for the complete contract. Run node scripts/document-state-e2e.mjs to install only the public SDK in an independent directory and verify the process consumer, restart retries, backend migration, revocation, and CLI transfers. Repeatable evidence stays in .tmp/e2e/document-state/.

An empty driver profile selects driver.default through the service graph. The native wuu.default-loop module and external providers use the same driver protocol, declared dependencies, cancellation, and cleanup. Excluding that module stops its driver; a composition can instead include an external provider of driver.default. Without a provider, new turns fail. The experimental driver.<profile> bridge pins its provider and callback table to the same generation. Descriptor, creation, and resume calls honor turn cancellation. A descriptor must supply a non-empty ID and a semantic version; errors are returned before creating an instance. Creation, resume, and terminal checkpoints must match the selected driver ID, version, and checkpoint contract. Unknown terminal statuses or incompatible checkpoints fail without replacing the last accepted state. Late instances returned after cancellation receive bounded shutdown. If a run fails or is canceled after saving a checkpoint, its result retains the latest accepted checkpoint so application finalization cannot overwrite it with the instance’s initial state. Already observed history remains available for final persistence even when cancellation prevents a terminal receipt from arriving. driver.<profile> version 2 returns a structured failure alongside a failed status, receipt, and checkpoint. It preserves cancellation, deadlines, empty-answer and summary-output-limit codes, provider classification, and spent recovery budgets. Use driverServiceDescriptor(profile) and return DriverRunResult; service version 2 is independent of the driver’s identity and checkpoint versions.

Conversations durably bind the service profile, provider ID, driver ID/version, and checkpoint contract before creating or resuming an instance. A changed default does not replace that identity. Rebuilt runtimes resolve the saved profile and reject a different provider or incompatible version before inference. The saved provider must still be selected for that service in the composition; changing the binding for the same service makes the old conversation read-only until its original selection is restored. Binding and checkpoint writes compare the stored identity atomically. A missing session or failed write cannot acknowledge a checkpoint. Fork creation inherits the source binding in the same transaction, while starting without the source execution checkpoint. Explicitly ephemeral conversations keep their binding and checkpoint in memory across runtime rebuilds; closing the conversation or process discards that state.

The selected document store retains driver identities and checkpoints. Legacy conversion preserves the old session_driver_bindings records and pins historical native conversations to their known default. Old external checkpoints without a recorded profile/provider fail with a migration diagnostic; the host cannot infer a missing binding safely. Backups must preserve bindings, checkpoints and history together. Older versions that ignore these identities cannot safely resume replacement drivers.

Embedded Go callers must also select a driver explicitly. StreamRunner requires LoopDriver or AcquireLoopDriver; a worker manager without AcquireLoopDriver fails before lifecycle hooks, inference, or tool execution. loopdriver.AcquireLocal(driver) binds a caller-owned in-process driver and checks its persisted identity on every acquisition.

Background workers use the selected driver too. Before effects start, they save the service profile, provider ID, driver ID/version and checkpoint contract. Follow-up and restart retain that binding; unavailable or incompatible providers fail explicitly. Records from before driver selection retain the native default identity. Worker executions hold a generation lease through terminal persistence and cleanup, and use distinct callback IDs from their parent. Pending checkpoints preserve their execution input separately from the last committed terminal result, so crash recovery cannot erase a result awaiting delivery. A terminal snapshot commits the matching execution ID. Worker checkpoints are part of the worker’s recovery data and must be kept with its history; older versions that ignore driver bindings cannot safely resume workers using a replacement driver.

All drivers, including the default multi-round provider, declare driver.turn major 4 and driver.checkpoint major 1. The SDK’s driverTurn(host, execution) exposes these operations:

OperationObservable behavior
read()Returns bound model defaults, available tools, and current active history with recovered tool receipts.
settings()Returns selected budgets, compaction settings, system-section metadata and available resources without executing them.
contextState() / saveContextState(state)Reads or persists identity-bound request insertions and context estimates. Save insertions before model dispatch. An absent retained clears insertions; absent usage leaves estimates unchanged. Estimates do not replace authoritative inference usage.
observe(observation)Reports request shape and compaction decisions. before_compact and after_compact invoke configured hooks; after_compact carries a classified compact.failure when the proposal failed.
compact(messages, budget)Requests a proposal from the selected compaction provider using explicit recovery observations. The driver validates and commits it separately. A classified failure throws DriverOperationError.
archiveContext()Archives accepted originals and returns their durable addresses without replacing active context.
admitInput()Consumes queued input at the driver’s chosen boundary and returns new contributions plus active context. Steering input is archived before its durable addresses are returned.
requestContext()Reads request-only context segments, including typed blocks and their model-message projections. The driver decides assembly and retention; reading does not append them to history.
transformRequest(request)Applies configured request transforms to an independently copied request. It does not admit input or call a model. A forced tool must be present in the resulting tool surface.
context()Returns active model history, an execution-local revision, the durable archive head, and whether durable replacement is available.
replaceContext(revision, messages)Archives accepted originals, then atomically commits the driver’s replacement and any concurrent durable tail. Stale revisions fail before writing.
model(request)Executes one streaming model request through the selected model pipeline. Returns the assistant response. It does not admit input, append request context, apply request transforms, or execute tools. A failed operation throws DriverModelError, retaining round.messages, reported round.usage, and structured failure evidence.
modelWithTools(request, batchID)Explicitly permits safe prefix tools to start during model streaming. Reserves a unique batch ID before dispatch. A successful nonempty tool_batch_id must be joined through tools() with all returned calls in order, before admitting input, submitting other effects, replacing context, or completing.
tools(batchID, messages, calls)Executes an explicit batch through the selected executor and durable tool ledger. Batch IDs are unique within an execution; call IDs are unique within a model round. A new model operation may reuse a provider call ID after previous batches settle. Calls can be chosen directly; missing assistant declarations are recorded before execution. Pending calls can be submitted in subsets; the reply contains only tool result messages.
complete(content, messages?, completion?)Returns a terminal receipt and preserves finish reason, stop reason and truncation metadata. Model and tool messages are already recorded; optional messages are additional authored output.
checkpoint(checkpoint)Saves state for this exact driver identity and checkpoint contract. Save intent before issuing external effects.

Operations belong to the active execution and provider generation, run serially, and reject further effects after cancellation or completion. Model events use the session’s event stream. Tool cleanup completes before its execution resources are released. Failed steering persistence prevents the new input from reaching the model. Cancellation does not undo external effects; an interrupted tool batch must be reconciled from durable receipts before it can be resumed safely.

Native driver modules can use the same independent turn operations. Their callback dependencies are explicit, and calls pass through the service registry with the provider’s execution identity. An undeclared callback is rejected even for a native module. Model requests and context operations preserve client message IDs, transcript sequence numbers, steering markers, and tool invocation IDs; these are correlation metadata, not call authority.

driver.turn 4.0 adds the resources used by the default algorithm and scopes call IDs to each model round. Update the SDK and required service major together; earlier majors are rejected. Input admission, request context, and request transforms remain explicit: call admitInput() at the loop’s input boundary, apply the driver’s compaction policy, assemble requestContext() segments, and call transformRequest() before model(). Retrying a prepared model request does not consume more input or apply transforms twice. The checkpoint contract and default driver’s state format remain version 1; existing session identities and histories are retained.

Next-step model selection is an opt-in addition within major 4. The selected engine declares both host-managed-continuation and step-model-selection; identity does not grant support. Older replacement engines explicitly reject intent. API admission and durable intent consumption/recovery validate the same retained engine and driver descriptors. A driver declares step_model_selection, admits input before each model step, then re-reads read() and settings() before assembling prompts, tool definitions, media policy and budgets. Preserve read().model_selection_revision through transformRequest() and model() / modelWithTools(); stale revisions or mismatched model/provider identities fail before dispatch. The default driver implements this boundary. Drivers without the capability keep whole-turn behavior and explicitly reject next-step intent.

For an active durable conversation with these engine and driver capabilities, call config/model/update with { "thread_id": "…", "provider": "configured-provider", "model": "configured-model", "apply_at": "next_step" }. Its model_selection response acknowledges the durable revision, not a change to an in-flight request. Omit apply_at for existing idle-only selection behavior. Durable next-step intents use the separate optional session.domain@1.SetNextStepModelSelection(id, intent) operation, not SetRuntimeSelection. Pass { ExpectedRevision, ExpectedEngineID, Selection } from the latest Find result; Selection contains the model fields and unchanged PermissionMode / ApproveForMe. Providers must atomically compare the stored revision and engine, reject mismatches with session_model_selection_changed, reject permission changes, and commit only the model fields with model_selection_apply_at: "next_step" and the revision incremented exactly once. Concurrent requests using the same revision have one winner. Providers offering this operation must also advance the same revision and clear the pending boundary for legacy model-selection writes (SetRuntimeSelection / SetModelSelection). Restore reads the durable winner.

Older session providers that do not advertise this operation reject with method_not_found before any write. Consumers must not fall back to SetRuntimeSelection or roll back a failed acknowledgement over concurrent changes. The existing SetRuntimeSelection contract remains turn-bound. Malformed or inconsistent acknowledgements from the new operation mean session_outcome_unknown: inspect durable state before retrying. A storage acknowledgement is not runtime authorization: consumption and restart revalidate engine and driver capabilities before dispatch.

A prepared step and its retries retain one model/provider, prompt, media capability policy, options, tool authority and usage lineage. The next successful-step admission reads the latest intent and rebuilds model-specific input before budgeting or dispatch. Selection does not replace the active executor, change permissions or execution environment, or adopt a new plugin generation. Only configured enabled choices compatible with the retained tool surface are accepted; new connections or replacement generations require an idle turn boundary. Cancellation cannot start another step. Restarts preserve and revalidate intent; unavailable selected providers fail instead of silently falling back. Input receipts record the selection revision when enabled.

Only opted-in next-step executions add model_step events and per-step metadata; ordinary turns preserve their event ordering and history shape. These events report the actually dispatched configured/API model, provider, operation ID, revision and context ceiling. Thread running_model* fields are separate from the desired selection; turn/usage carries the current step identity with turn-wide aggregate counters. Durable usage and traces retain model_steps attribution; a mixed-model turn has no misleading single-model tag. Usage queries expand these per-operation records without counting the aggregate twice, while turn recovery retains one aggregate record.

Continuation is additive within major 4. settings().resume_sampling marks an explicit request to sample after a previously completed response, so a driver must not return that response as the new execution’s terminal result. A failed model round can include completed_messages: provider-confirmed complete items that remain valid for subsequent requests. Other partial output remains visible in history but is marked as an incomplete stream fragment and excluded from resumed model input.

Model-operation failures return failure alongside partial assistant messages and reported usage. The SDK converts it to DriverModelError; native drivers receive FailureError with the partial ModelRound. Failure evidence includes origin, category, provider code, HTTP status, retry delay, context-overflow classification, and the recovery executor’s stopping decision and attempt counts when available. A retryable cause does not grant a fresh retry budget. Transport, authorization, and malformed-request errors remain service errors. Direct protocol consumers must inspect failure; a response containing partial output does not imply success. Failed responses retain visible text and images in history, while incomplete tool declarations and provider replay state remain only in the failure reply.

round.preempted is a successful model round cut short by newly admitted user input. Keep its visible partial messages in history and continue the same turn with that input; do not surface it as a transport failure.

Save intent before calling modelWithTools(), since tools may start before the model reply arrives. round.tool_work_started reports accepted work even when the model fails. Failed model operations cancel and drain their tools; their batch IDs cannot be reused. Call IDs remain reserved until the next accepted model operation. Successful streaming receipts wait for the original executions rather than issuing them again. Execution cleanup also drains orphaned and nested work before releasing tool providers. A model stream cannot automatically replay after tools have started, including when no durable ledger is configured.

Before either model operation dispatches, the execution service commits a model-input receipt when a receipt store is configured. It records the selected driver and execution identity, the final messages and durable input addresses, tools, and request settings under the model operation ID. A failed receipt write prevents model and streaming-tool execution. Cancellation during the write may leave a committed receipt without a submission; a receipt alone is not proof that the provider accepted or completed the request. Native and process drivers use this same barrier.

Replacing active context preserves the original transcript. If archive succeeds but checkpoint commit fails, the archive stays durable and the previous active context remains; read a fresh revision before retrying. Cancellation after commit does not undo the committed context. Missing archive or atomic checkpoint services make durable replacement unavailable. Embedded callers can explicitly pass { volatile: true } only when no durable context writer is configured. That mode replaces execution memory, may archive originals if available, and promises no context checkpoint. It is never selected implicitly by the resource service.

The evidence workflow example uses only the public SDK. It plans one tool batch, commits an active context containing the current task and evidence, then runs a synthesis stage with tools disabled. This example requires durable context replacement. Its checkpoint records the current stage; recovery consumes settled tool receipts instead of replaying their effects. Run node scripts/workflow-driver-e2e.mjs from a checkout to install a packed SDK in an independent directory and verify real file evidence, original-history preservation, committed-context recovery after a default-profile change, interruption before the planned history is flushed, driver-controlled context-overflow recovery, and provider removal. The check also runs an independent SDK driver that reads a real file before the model finishes streaming and joins its durable result once. The same check runs the workflow as driver.default with the native default module excluded; set the example runtime environment variable WUU_WORKFLOW_PROFILE=default to declare that service. Repeatable logs and the installed package remain under .tmp/e2e/workflow-driver/.

The default agent.DefaultDriver owns the multi-round, compaction, overflow-recovery and termination policy. Its model, tool, context, observation and checkpoint effects use these same public resources. The former whole-loop gateway and host-owned loop entry have been removed; embedded callers must select the provider explicitly. The workflow example resumes its saved task context; it does not merge a new user task into an interrupted workflow. Driver profiles are currently exposed through the debug app-server --driver option, rather than a desktop selection control.

Kernel services cover storage, settings, sessions, history, workspaces, artifacts, and execution operations. Feature services use the same routing mechanism. A service method’s schema identifiers describe its contract; they are not permission to call another plugin’s private process or read its private storage.

Tool and capability dispatch create bounded execution scopes. Progress, child-tool calls, questions, and artifacts attach to a live execution. Cancellation closes that scope without waiting for plugin acknowledgement, and late updates cannot revive it. Plugins receive an abort signal and must propagate it to cancellable work. Service-registry shutdown rejects new work, drains existing calls, and cancels what remains when its shutdown context expires.

Native and process service providers use the same declarations, method checks, and callback scopes. Native consumers also declare dependencies and participate in the same lifecycle graph. A caller named kernel has no special authority. Driver instance IDs travel in domain payloads; they cannot be used as service execution IDs. service.invoke.execution_id belongs to the provider callback. When the consumer calls from a live invocation, caller_scope supplies its authenticated identity, kind (tool or callback), and session/workspace attribution. The SDK exposes this as execution.caller, including a separate signal. That signal survives individual service responses and aborts when the caller ends or the provider stops. Use it for resources such as a model stream that spans start/read/cancel calls. Use execution.signal for work bounded to the current callback. A nested call uses the provider’s own execution ID; caller attribution never grants permission to impersonate the caller or its tool authority.

A method may declare cleanup: true to release resources after autonomous admission stops. Cleanup must not acquire resources or start work, and receives no caller scope. Registry snapshots identify implementations by provider ID and binding source; the former kernel flag has been removed. These contract changes do not change product CalVer or stored user data.

Capabilities compose at defined points, such as adding prompt context or observing turn completion. They are not unrestricted interception of the agent loop or provider request. The authoring reference lists supported capabilities and error policies.

The desktop loads an eligible package’s ESM entry and calls activate(api). Registrations belong to that package generation. The host validates declared contributions and view targets before publishing it, then disposes the prior desktop generation. A failed candidate registration is disposed without replacing the existing desktop generation.

Each candidate has a 30-second asynchronous staging budget covering the desktop module request, ESM import, and activate(api). Timeout, unload, or a newer inventory sync settles pending activation without waiting for the plugin to finish. Its partial registrations are cleaned up and late registrations are rejected. A timed-out replacement leaves its last working generation active.

api.signal is the generation’s lifetime signal. Pass it to cancellable work and register resource cleanup before waiting. It is aborted on failed or superseded staging and on disposal of a published generation; successful activation clears the startup deadline. This is cooperative cancellation: it cannot interrupt synchronous JavaScript, stop an already-running import, or undo external side effects.

Required providers must finish desktop activation successfully before consumers start. Independent branches may initialize concurrently. A provider replacement also reloads unchanged consumers; removing or disabling a required provider blocks them. Cleanup disposes consumers before providers. A failed replacement can preserve that package’s previous desktop generation, but consumers do not activate against the unsuccessfully staged required replacement. Optional startup failures do not block consumers.

This desktop transaction is separate from the Go runtime transaction. Do not assume one atomic rollback spans package installation, runtime activation, and every renderer window. Inspect the runtime and desktop diagnostics for the surface that failed.

The UI extension points serve different purposes:

Extension pointRole
Views and placementsPlugin-owned content in host-managed layout regions
SlotsSmall contributions at fixed insertion points
UI contributionsContent or wrappers targeting an explicitly owned slot
PresentersRendering of a versioned semantic snapshot with host-owned actions
Themes and scoped stylesAppearance within the published token and styling contracts

Presenters receive snapshots rather than mutable host stores. An advertised action still passes the host’s current writable-state and argument checks. Error boundaries and default rendering protect supported surfaces from ordinary contribution failures; they are not a security sandbox for malicious code. See desktop plugins for targets and composition rules.

See plugin compatibility and upgrades for the supported protocol/lifecycle boundaries, deprecation rules and retained-artifact gate.

Plugins are trusted code running with user authority. Manifest permissions and service declarations describe contracts but do not isolate a process from the machine. Tool permission checks and filesystem process confinement protect their respective host execution paths; they do not turn arbitrary plugin code into untrusted sandboxed code.

A committed disable or removal revokes that plugin across retained generations without waiting for an idle turn: native executions are canceled, owned MCP servers cannot reconnect, and future legacy hooks and transforms are removed. Already-running legacy hooks keep their caller lifetime; existing transcript content and completed external effects are not erased. Same-ID updates instead let active work finish on its original implementation. Superseded idle runtimes are released without waiting for another user message; outstanding conversation workers and managed processes can keep an older implementation alive. Plugins with activation-owned background effects must tolerate that overlap while work drains. Storage and settings normally remain, and completed external effects are not undone. Safe mode and default-UI recovery provide ways to regain control after extension failures. Wuu does not audit or certify third-party packages; see the security model.

Compatibility has several independent layers: product CalVer, package schema, runtime capability protocol, service major versions, and desktop snapshot versions. Match the SDK and host version you target. wuu plugin dev ./extension.ts also accepts one default-exported TypeScript runtime plugin without a manifest, build, package installation, or SDK dependency install. It snapshots the source and embedded SDK into a private candidate and requires Node.js 22.13 or newer. Single-file imports are limited to Node builtins and @wuu/plugin-sdk; use a package directory for other dependencies. See the quickstart. Build, package validation, runtime negotiation tests, and actual feature or UI checks each verify a different part of that path.