Skip to content

actcore/act-shims

Repository files navigation

act-shims

Ship a conformant ACT component from a constrained language (C++, C#, Go, …) by implementing a sync, no-stream interface and composing it with a generic prebuilt Rust adapter via wac plug.

ACT's canonical capability interfaces declare their functions as async func and carry a stream<tool-event>-bearing tool-result. Only Rust's wit-bindgen backend generates that cleanly today — the cpp / csharp / go backends either panic on stream<> inside a variant or emit no glue for async func. This repo unblocks them: it pairs the sync mirror WIT (defined in act-spec) with generic prebuilt Rust shims (this repo's deliverable) that lift a sync inner up to the real async ACT surface.

The canonical act:tools / act:sessions WIT is not modified. This repo adds sibling sync packages and the shims to bridge them.

Architecture

                         wac plug
  ┌────────────────────────────────────────────────────────────────┐
  │  composed.wasm  (one conformant ACT component)                  │
  │                                                                │
  │   exports  act:tools/tool-provider@0.2.0   (ASYNC, canonical)  │
  │              ▲                                                  │
  │   ┌──────────┴────────────┐        ┌─────────────────────────┐ │
  │   │  tool-shim/ (Rust)    │ import │  your inner (C++/C#/Go)  │ │
  │   │  exports async  ──────┼───────▶│  exports SYNC            │ │
  │   │  imports SYNC         │  sync  │  act:tools-sync/         │ │
  │   │  pure forwarder       │ interf.│  tool-provider-sync      │ │
  │   └───────────────────────┘        └─────────────────────────┘ │
  │     act:tools-sync is INTERNALIZED (gone from the imports)      │
  └────────────────────────────────────────────────────────────────┘

session-shim/ is the identical shape for stateful components: act:sessions-sync/session-provider-sync@0.1.0 (sync) → async act:sessions/session-provider@0.2.0.

The 0.2.0 improvement: shared types, zero conversion

act:tools@0.2.0 and act:sessions@0.2.0 extracted their data model into function-free, stream-free types interfaces (act:tools/types, act:sessions/types). The sync mirror packages here use those canonical types directly:

// act:tools-sync@0.1.0
interface tool-provider-sync {
  use act:core/types@0.4.0.{cbor, metadata, error};
  use act:tools/types@0.2.0.{tool-event, list-tools-response};   // reuse!

  list-tools: func(metadata: metadata) -> result<list-tools-response, error>;
  call-tool:  func(name: string, arguments: cbor, metadata: metadata) -> list<tool-event>;
}

Because both the shim's async export and the shim's sync import reference the same act:tools/types::tool-event / list-tools-response, wit-bindgen generates one shared Rust type for each. The shim therefore needs no type conversioncall-tool is literally:

async fn call_tool(name: String, arguments: Cbor, metadata: Metadata) -> ToolResult {
    ToolResult::Immediate(inner::call_tool(&name, &arguments, &metadata))
}

This is the key advance over the proof-of-concept (see below), which predated the types split: back then the sync package had to define standalone copies of the data types and the shim carried a conv_* translation layer, because use-ing from the streaming tool-provider interface made the cpp/csharp generators walk the stream<tool-event> and panic. use-ing from the clean types interface does not trip that panic — verified — so the duplication and the conversion code are both gone.

How to use it (from a constrained language)

  1. Author your inner component. Implement, in your language, the SYNC interface act:tools-sync/tool-provider-sync@0.1.0 (or act:sessions-sync/session-provider-sync@0.1.0 for stateful components). Vendor the WIT from wit/deps/ here. Your guest exports only the sync interface — no async, no stream, which is exactly what the constrained-language backends can generate.

  2. Build your inner to a wasm32-wasip2 component.

  3. Compose with the prebuilt shim:

    wac plug tool_shim.wasm --plug your_inner.wasm -o composed.wasm

    wac satisfies the shim's sync import with your inner's sync export and internalizes act:tools-sync entirely. composed.wasm now exports the canonical async act:tools/tool-provider@0.2.0.

  4. Pack and ship:

    act-build pack composed.wasm
    act-build validate composed.wasm

composed.wasm runs on any stock ACT host (CLI, MCP, HTTP) — it is indistinguishable from a natively-async Rust component.

no_std: a clean import surface

The shims build #![no_std] (alloc-only). A pure forwarder has no use for std's runtime, and dropping it removes the ambient WASI imports that std's startup pulls in — imports that aren't even in the shim's WIT contract. The shim provides its own #[global_allocator] (dlmalloc, the same allocator std uses on wasm), a #[panic_handler], and cabi_realloc (which wasm32-wasip2 otherwise inherits from wasi-libc via std). wit-bindgen's own crate is already #![no_std] + alloc, and the generated bindings reference only ::core / alloc, so no codegen changes are needed — just default-features = false on the wit-bindgen dep to drop the (otherwise inert) std feature.

Measured on these shims:

shim std build no_std build
tool_shim.wasm 71.3 KB, 13 WASI imports 36.3 KB, 0 WASI imports
session_shim.wasm 68.6 KB, 13 WASI imports 33.4 KB, 0 WASI imports

The std build imported wasi:io/{poll,error,streams} and wasi:cli/{environment,exit,stdin,stdout,stderr,terminal-*} — none of which a forwarder touches. The no_std shim imports only its declared interfaces (act:core/types, act:tools/types + the sync inner). For an auditable toolchain that's the point: a generic adapter whose import surface is exactly its WIT contract, nothing ambient to explain away.

Only the shim's contribution is cleaned up this way. The composed component's final import surface is the union of shim + your C++/C#/Go inner, so the inner's own language runtime still contributes whatever it imports.

Layout

wit/deps/                            vendored, committed WIT (builds offline)
  act-core-0.4.0/                      canonical act:core
  act-tools-0.2.0/                     canonical act:tools (with types interface)
  act-sessions-0.2.0/                  canonical act:sessions (with types interface)
  act-tools-sync-0.1.0/                vendored from actcore.dev (defined in act-spec)
  act-sessions-sync-0.1.0/             vendored from actcore.dev (defined in act-spec)
crates/tool-shim/                    Rust (no_std): exports async tools, imports sync (no conv)
crates/session-shim/                 Rust (no_std): exports async sessions, imports sync (no conv)
examples/inner-hello/                Rust inner exporting sync tool-provider-sync (smoke test).
                                       A normal std component, and its OWN workspace (excluded
                                       from the shims') so its std wit-bindgen never unifies
                                       with the no_std shims. Builds composed via `wac plug`.
justfile                             build-shims / build-inner / compose / pack / wit / test
wkg-registry.toml                    documents the real `wkg wit fetch` source

The per-crate wit/deps/ entries are symlinks into the shared top-level wit/deps/, so the canonical packages are vendored once.

Build & verify

just build-shims     # cargo → tool_shim.wasm, session_shim.wasm
just build-inner     # cargo → inner_hello.wasm (the example constrained-style inner)
just compose         # wac plug → composed.wasm
just pack            # act-build pack + validate
just wit             # assert each shim's exported/imported surface
just wit-composed    # assert act:tools-sync is internalized in composed.wasm
just test            # act info --tools + act call → "Hello, Ada!"  (needs a 0.2.0 host)
just all

Runtime smoke test — verified. just test runs act info composed.wasm --tools and act call composed.wasm hello … → "Hello, Ada!" against a real act host, and passes on act 0.10.0 (the act:tools@0.2.0 migration has landed). Verified with both the cargo installed act and a local debug build. Static composition is also green (the shim composes, the sync import is internalized, the component packs and validates). Override the binary with ACT=/path/to/act just test.

The no_std shim composes with a normal std inner (inner-hello) and the result runs unmodified — the runtime call passes a string across the ABI, exercising the shim's own cabi_realloc + allocator.

Note: only tool-shim is exercised at runtime here, since inner-hello exports act:tools-sync. session-shim is structurally identical and its build + WIT surface are asserted (just wit), but there is no session inner example to compose-and-run yet.

Why a Rust inner here, when the point is non-Rust?

examples/inner-hello/ is a Rust inner used only as a fast end-to-end smoke test of the shims. The constrained-language proof — a real C++ inner generated by wit-bindgen cpp, composed through this same shim pattern, loaded and run by the stock host — is in components-experimental/sync-shim-poc. That PoC demonstrated the pattern against act:tools@0.1.0 (with standalone type copies + a conv_* layer); this repo is its productization against act:tools@0.2.0, where the types reuse removes the duplication.

Prebuilt shims (consume without building)

The shims are published as OCI components under the act:shim-* family:

Shim OCI reference
act:shim-tools-sync ghcr.io/actcore/act/shim-tools-sync:0.1.0
act:shim-sessions-sync ghcr.io/actcore/act/shim-sessions-sync:0.1.0
wkg oci pull ghcr.io/actcore/act/shim-tools-sync:0.1.0 -o shim-tools-sync.wasm
wac plug shim-tools-sync.wasm --plug your_inner.wasm -o composed.wasm
act-build pack composed.wasm && act-build validate composed.wasm

Limitations

  • Immediate-only. The shim returns tool-result::immediate. True streaming cannot flow through a sync inner — the sync interface has no stream. This covers the common bounded-result case (the vast majority of tools), not incremental producers.
  • Does not fix MoonBit's separate bug. MoonBit's blocker is a different defect (wit-bindgen-moonbit panics on async-export returning a variant with heap cleanup); this pattern addresses the cpp/csharp/go stream<>-in-variant
    • no-async-export family.

License

Dual-licensed under Apache-2.0 OR MIT, at your option.

About

Generic sync→async adapter shims (act:shim-*) for authoring conformant ACT components from constrained languages (C++, Kotlin, C#, Go)

Resources

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT

Stars

Watchers

Forks

Packages

 
 
 

Contributors