Skip to content

feat: Cross-Language SDK — Phases 1-4 (gRPC, WASM Bridges, Module Resolver)#38

Closed
bkrabach wants to merge 101 commits intomainfrom
dev/cross-language-sdk
Closed

feat: Cross-Language SDK — Phases 1-4 (gRPC, WASM Bridges, Module Resolver)#38
bkrabach wants to merge 101 commits intomainfrom
dev/cross-language-sdk

Conversation

@bkrabach
Copy link
Collaborator

@bkrabach bkrabach commented Mar 6, 2026

Summary

Complete Cross-Language SDK implementation: gRPC bridges, WASM Component Model bridges for all 6 module types, and automatic module resolver with transport detection.

Phase 1-2: gRPC Bridges + TypeScript Bindings

  • gRPC tool and orchestrator bridges with bidirectional kernel-service callbacks
  • PyO3 bindings (Python ↔ Rust)
  • Napi-RS bindings (TypeScript ↔ Rust)
  • Proto definitions for all module contracts

Phase 3: WASM Module Loading

  • WASM Component Model bridges for all 6 module types: Tool, Hook, Context, Approval, Provider, Orchestrator
  • WasmEngine wrapper with wasmtime Component Model support
  • 7 WASM E2E tests with pre-compiled fixtures
  • gRPC debt fix (bidirectional kernel-service host for orchestrator)

Phase 4: Cross-Language Module Resolver

  • resolve_module(path) — Auto-detects transport from directory contents. Priority: amplifier.toml.wasm (Component Model metadata) → Python (__init__.py) → error
  • load_module(manifest, engine, coordinator) — Dispatches manifest to correct load_wasm_* bridge, returning LoadedModule enum
  • PyO3 bindingsresolve_module() and load_wasm_from_path() exposed to Python
  • Napi-RS bindingsresolveModule() and loadWasmFromPath() exposed to TypeScript
  • loader_dispatch.py — Wired WASM/gRPC branches to Rust resolver with graceful fallback

New/Modified Files (Phase 4)

Action File
Create crates/amplifier-core/src/module_resolver.rs
Create crates/amplifier-core/tests/module_resolver_e2e.rs
Modify crates/amplifier-core/src/lib.rs
Modify crates/amplifier-core/src/models.rs (added Approval variant)
Modify crates/amplifier-core/Cargo.toml (added toml + tempfile deps)
Modify bindings/python/Cargo.toml (added wasm feature)
Modify bindings/python/src/lib.rs (added resolver functions)
Modify bindings/node/Cargo.toml (added wasm feature)
Modify bindings/node/src/lib.rs (added resolver functions)
Modify python/amplifier_core/loader_dispatch.py

Test Plan

  • 34 module_resolver unit tests passing
  • 14 E2E integration tests passing (resolve → load → execute roundtrip)
  • 7 WASM E2E tests passing (no regressions from Phase 3)
  • Clippy clean: amplifier-core, amplifier-core-py, amplifier-core-node
  • Python syntax valid
cargo test -p amplifier-core --features wasm -- module_resolver --test-threads=1
cargo test -p amplifier-core --features wasm --test module_resolver_e2e --test-threads=1
cargo test -p amplifier-core --features wasm --test wasm_e2e --test-threads=1
cargo clippy -p amplifier-core --features wasm -- -D warnings
cargo clippy -p amplifier-core-py -- -D warnings
cargo clippy -p amplifier-core-node -- -D warnings

bkrabach and others added 30 commits March 4, 2026 11:45
Design for Cross-Language SDK Phase 2: TypeScript/Node.js bindings
via Napi-RS mirroring the proven Python/PyO3 bridge pattern.

Covers:
- Build infrastructure (napi-rs crate at bindings/node/)
- Fully typed TypeScript API surface (4 classes, 6 module interfaces)
- Async bridging strategy (tokio ↔ libuv via ThreadsafeFunction)
- Testing strategy (~65 Vitest tests on the bridge layer)
- Batched dependency upgrades (pyo3 0.28.2, wasmtime latest)
- Three tracked future TODOs (unified Rust storage, Rust-native
  process_hook_result, lib.rs module split)
Add 6 enum types (HookAction, SessionState, ContextInjectionRole, ApprovalDefault, UserMessageLevel, Role) as #[napi(string_enum)]
Add 4 struct types (JsToolResult, JsToolSpec, JsHookResult, JsSessionConfig) as #[napi(object)]
Include bidirectional From conversions for HookAction and SessionState
Add types.test.ts with 6 passing tests verifying all enum variants and values
Auto-generated index.js and index.d.ts updated from napi build

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
Wrap amplifier_core::CancellationToken as JsCancellationToken for Node.js bindings.

Implementation includes:
- JsCancellationToken struct wrapping amplifier_core::CancellationToken
- Constructor: new() creates uncancelled token
- Getters: isCancelled, isGraceful, isImmediate properties
- Methods: requestGraceful(), requestImmediate(), reset()
- All methods support optional reason string parameter
- Auto-generated TypeScript declarations via NAPI-RS

Testing:
- 7 passing tests covering all state transitions:
  - Default state (not cancelled, not graceful, not immediate)
  - requestGraceful transitions (isCancelled=true, isGraceful=true, isImmediate=false)
  - requestImmediate transitions (isCancelled=true, isImmediate=true)
  - Escalation from graceful to immediate
  - reset() returns to uncancelled state
  - requestGraceful with reason string
  - requestImmediate with reason string

🤖 Generated with Amplifier

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
- Added JsHookHandlerBridge struct that bridges JS callback functions to Rust HookHandler trait via ThreadsafeFunction
- Added JsHookRegistry class wrapping amplifier_core::HookRegistry with register/emit/listHandlers/setDefaultFields methods
- Added bidirectional From conversions for ContextInjectionRole, UserMessageLevel, ApprovalDefault
- Added hook_result_to_js converter function
- Added 6 tests covering empty registry, emit with no handlers, JS handler registration, handler listing, deny action, and default field merging

🤖 Generated with Amplifier

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…id dep

Two quality improvements:
- JsHookHandlerBridge now logs an eprintln when JSON parse of JS handler result fails, instead of silently falling back to HookResult::default()
- Removed unused `uuid` dependency from Cargo.toml
…sertions

- Changed get_capability to return Result<Option<String>> instead of Option<String>
- Now properly propagates serialization errors via Error::from_reason instead of silent "null" fallback
- Matches the config() getter's error propagation pattern for consistency
- Tightened toDict test assertions to verify actual field values instead of only key existence
- All 29 tests pass, no regressions
- Addresses code quality review suggestions

🤖 Generated with Amplifier

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
- Simplify cleanup test: remove redundant resolves.not.toThrow()
- Add negative constructor test for invalid JSON config
- Add comment documenting hooks() transient instance behavior
…ontention, add fallback comments

- set_initialized() now logs via eprintln when lock contention prevents mutation (was silent no-op)
- coordinator() uses cached parsed HashMap instead of re-parsing config_json on every call
- try_lock() fallback defaults in is_initialized() and status() now have inline comments explaining why the defaults are safe

All 41 tests pass. No behavior changes.
- Eliminate double JSON parse in JsAmplifierSession constructor by deriving HashMap from already-parsed serde_json::Value via serde_json::from_value()
- Update stale TODO(task-6) comment on JsCoordinator::hooks() to reflect current status
- Strengthen coordinator getter test to verify coordinator was built from session config
bkrabach and others added 27 commits March 5, 2026 17:34
Implements Task 12 of 19: WasmContextBridge.

- Create bridges/wasm_context.rs with WasmContextBridge struct
- Implement ContextManager trait (add_message, get_messages,
  get_messages_for_request, set_messages, clear)
- KEY DIFFERENCE from WasmToolBridge/WasmHookBridge: stateful design
  uses a single persistent (Store, Instance) pair wrapped in
  tokio::sync::Mutex, so WASM internal state survives across calls
- Add module to bridges/mod.rs behind #[cfg(feature = "wasm")]
- E2E test with memory-context.wasm: add→get→add→get→clear→get roundtrip
  verifies that state persists across method calls on the same bridge
- Compile-time trait-object check: Arc<dyn ContextManager>

Tests: cargo test -p amplifier-core --features wasm --lib -- wasm_context
  => 1 passed (memory_context_stateful_roundtrip)
…omponent Model

- Create crates/amplifier-core/src/bridges/wasm_approval.rs
  - WasmApprovalBridge struct holds Arc<Engine> + Component (stateless, like WasmHookBridge)
  - from_bytes() compiles component, from_file() convenience loader
  - Implements ApprovalProvider: request_approval() uses spawn_blocking, fresh instance per call
  - Looks up 'request-approval' export at root level or inside amplifier:modules/approval-provider@1.0.0
  - Serializes ApprovalRequest to JSON bytes, deserializes ApprovalResponse from result bytes
  - Compile-time trait-object check: Arc<dyn ApprovalProvider>
  - E2E test with auto-approve.wasm: verifies approved=true and reason is Some

- Modify crates/amplifier-core/src/bridges/mod.rs
  - Add #[cfg(feature = "wasm")] pub mod wasm_approval; after wasm_context line
- Create tests/fixtures/wasm/src/echo-provider/ with minimal WIT,
  Cargo.toml, lib.rs, and generated bindings.rs
- WIT defines provider-module world without WASI HTTP import (echo
  provider is pure-compute, no real HTTP needed)
- EchoProvider implements Provider trait: name='echo-provider',
  get_info returns ProviderInfo, list_models returns one echo-model,
  complete returns canned ChatResponse with 'Echo response from WASM
  provider', parse_tool_calls returns empty vec
- Extend export_provider! macro in amplifier-guest to add WASM
  Component Model exports (implements bindings Guest trait + calls
  bindings::export!), matching the pattern of export_tool!, export_hook!,
  export_context!, and export_approval!
- Also fixes export_provider! metavariable from :ty to :ident so
  bindings::export! call is valid
- Compile and commit echo-provider.wasm (231 KB) to fixtures dir
- Add RED-first tests test_echo_provider_wasm_fixture_exists_and_has_valid_size
  and test_echo_provider_wasm_fixture_has_wasm_magic_bytes in
  amplifier-guest wasm_fixture_tests module
- All 86 amplifier-guest tests pass
…ost import (Task 15)

- Update export_orchestrator! macro to support WASM Component Model exports
  - Changed $orch_type:ty to $orch_type:ident for impl block compatibility
  - Added #[cfg(target_arch = "wasm32")] Guest trait implementation for orchestrator
  - Deserializes request bytes as JSON to extract prompt, calls Orchestrator::execute
  - Wires up bindings::export! for Component Model integration
- Create tests/fixtures/wasm/src/passthrough-orchestrator/ fixture
  - wit/orchestrator.wit: minimal WIT with kernel-service import + orchestrator export
  - Cargo.toml: cargo-component setup targeting orchestrator-module world
  - src/lib.rs: PassthroughOrchestrator calling kernel_service::execute_tool via WIT import
  - src/bindings.rs: generated by cargo component build (committed for reference)
  - Cargo.lock: dependency lockfile
  - .gitignore: excludes /target/ build artifacts
- Compile and commit tests/fixtures/wasm/passthrough-orchestrator.wasm (155 KB)
- Add wasm_fixture_tests for passthrough-orchestrator.wasm (exists + magic bytes)

All 88 amplifier-guest tests pass (10 WASM fixture tests, including 2 new ones).
…ules

Task 16 of 19: WasmProviderBridge

- Create bridges/wasm_provider.rs with WasmProviderBridge struct
- Follows WasmToolBridge pattern: compiles component once, caches metadata
  (provider name + ProviderInfo) from get-info call at load time
- Implements Provider trait (5 methods):
  - name() -> cached provider id
  - get_info() -> cached ProviderInfo
  - list_models() -> spawn_blocking + WASM list-models export
  - complete(ChatRequest) -> spawn_blocking + WASM complete export
  - parse_tool_calls(&ChatResponse) -> synchronous WASM call (trait is sync)
- Uses amplifier:modules/provider@1.0.0 interface name with fallback
  lookup (root-level first, then nested interface export)
- Update bridges/mod.rs: add #[cfg(feature = "wasm")] pub mod wasm_provider

E2E tests with echo-provider.wasm fixture (4 tests, all pass):
- load_echo_provider_name: name() == "echo-provider"
- echo_provider_get_info: info.id and info.display_name correct
- echo_provider_list_models: returns model with id "echo-model"
- echo_provider_complete: ChatResponse has non-empty content
- Compile-time check: WasmProviderBridge satisfies Arc<dyn Provider>
Implements Task 17 of 19: WasmOrchestratorBridge.

The orchestrator is the most complex WASM bridge because it must
register kernel-service host import functions on the Linker before
instantiation — these call back into the Coordinator for tool
execution, provider completions, hook emission, context access,
and capability management.

Key design decisions:
- Host import closures capture Arc<Coordinator> by clone and use
  tokio::runtime::Handle::current().block_on() to drive async
  coordinator calls from within the synchronous WASM context
  (safe because WASM runs inside spawn_blocking)
- Uses the existing WasmState / create_linker_and_store from
  wasm_tool.rs; the coordinator is captured in closures rather
  than stored in the Store state
- Only prompt is forwarded to the WASM guest as {"prompt": "..."};
  context/providers/tools/hooks/coordinator from Orchestrator::execute()
  are not serialized (guest uses kernel-service imports instead)
- OrchestratorExecuteFunc type alias silences clippy complex-type warning

All 7 kernel-service functions implemented:
  execute-tool, complete-with-provider, emit-hook, get-messages,
  add-message, get-capability, register-capability

Tests added (4 total):
- passthrough_orchestrator_calls_echo_tool: E2E with FakeTool
- passthrough_orchestrator_with_default_fake_tool: default FakeTool
- passthrough_orchestrator_with_wasm_echo_tool: WASM-to-WASM path
- passthrough_orchestrator_tool_not_found_returns_error: error case

All 40 bridge tests pass. Clippy clean.
…ansport.rs

- Add load_wasm_hook, load_wasm_context, load_wasm_approval,
  load_wasm_provider, and load_wasm_orchestrator to transport.rs
- Each returns Arc<dyn Trait> for runtime polymorphism
- load_wasm_orchestrator accepts an extra Arc<Coordinator> parameter
- Add transport-level tests for all 6 wasm loaders using WASM fixtures
- Tests use env!(CARGO_MANIFEST_DIR) for robust fixture path resolution
- load_wasm_tool signature was already updated in Task 10 (no change needed)

Task 18 of 19: Transport Dispatch
- Add crates/amplifier-core/tests/wasm_e2e.rs: comprehensive E2E tests
  covering all 6 WASM module types via the public transport::load_wasm_*
  API (tool, hook, context, approval, provider, orchestrator)
- Remove crates/amplifier-core/tests/wasm_tool_e2e.rs: old stub that only
  had two compile-time smoke tests
- Add tests/fixtures/wasm/build-fixtures.sh: shell script to recompile all
  6 WASM fixture crates from source via cargo-component

E2E tests (all 7 pass):
  - tool_load_from_bytes: name/spec verification
  - tool_execute_roundtrip: JSON input echoed back
  - hook_handler_deny: action==Deny, reason contains 'Denied'
  - context_manager_roundtrip: stateful get/add/clear sequence
  - approval_auto_approve: approved==true
  - provider_complete: name/info/models/complete roundtrip
  - orchestrator_calls_kernel: WASM→kernel-service→WASM tool chain
Approved design for the module resolver glue layer that connects
foundation URI resolution to Rust transport loading:

- Split architecture: Rust transport detection, Python/TS URI resolution
- WASM component WIT metadata parsing for module type detection
- Three runtime paths: Python (importlib), WASM (wasmtime), gRPC (explicit)
- PyO3 + Napi-RS bindings serve both Python and TypeScript hosts
- loader_dispatch.py WASM/gRPC branches wired to Rust kernel
- ModuleManifest struct as the resolver's output contract

Phase 4 of 5 in the Cross-Language SDK plan.
…_resolver

- Reorder module_resolver declaration in lib.rs to alphabetical position (between models and retry)
- Change AmbiguousWasmInterface error format from Debug format to human-readable comma-joined string
- Code quality refinement with no behavioral changes
…cted test

Improved test to leverage the existing PartialEq derive for more idiomatic Rust
testing by using full struct assert_eq! instead of field-by-field assertions.
No behavioral changes — code quality improvement from review.

🤖 Generated with Amplifier

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
- Add `toml = "0.8"` crate dependency
- Add `Approval` variant to `ModuleType` enum
- Implement `parse_module_type()` helper mapping strings to ModuleType
- Implement `parse_amplifier_toml()` for explicit transport/type override
- Support gRPC (with endpoint), WASM (with optional artifact), and
  Python/Native transports
- Add 6 tests: grpc, wasm, python transports + error cases
The code at lines 75-80 in module_resolver.rs correctly returns an error
with 'unknown module type: {type_str}' for unrecognized type values, but
there was no test exercising that path. This test (parse_toml_unknown_module_type_errors)
verifies the error handling for unknown module types, completing the test
coverage identified by the code quality review.

🤖 Generated with Amplifier

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
- Add inline comment to clarify WASM artifact Vec::new() is intentional
- Add missing Approval variant assertion to module_type_serializes_as_lowercase test

These are code quality improvements with no behavior changes.
Add WASM component metadata parser for module type detection via Component Model inspection.

- Added KNOWN_INTERFACES constant mapping interface name prefixes to ModuleType:
  - amplifier:modules/tool -> Tool
  - amplifier:modules/hook-handler -> Hook
  - amplifier:modules/context-manager -> Context
  - amplifier:modules/approval-provider -> Approval
  - amplifier:modules/provider -> Provider
  - amplifier:modules/orchestrator -> Orchestrator

- Added detect_wasm_module_type() function gated behind #[cfg(feature = "wasm")]
  - Loads WASM component using wasmtime::component::Component::new
  - Iterates over component_type.exports() to match interface names
  - Returns UnknownWasmInterface if zero matches
  - Returns AmbiguousWasmInterface if more than one match
  - Returns Ok(ModuleType) if exactly one match

- Added fixture helper functions for tests:
  - fixture_path() - resolves .wasm fixture paths from tests/fixtures/wasm/
  - fixture_bytes() - reads fixture file contents
  - make_engine() - creates wasmtime::Engine for testing

- Added 6 integration tests using real .wasm fixtures:
  - detect_wasm_module_type_tool (echo-tool.wasm)
  - detect_wasm_module_type_hook (deny-hook.wasm)
  - detect_wasm_module_type_context (memory-context.wasm)
  - detect_wasm_module_type_approval (auto-approve.wasm)
  - detect_wasm_module_type_provider (echo-provider.wasm)
  - detect_wasm_module_type_orchestrator (passthrough-orchestrator.wasm)

All tests pass with real WASM fixtures. Clippy clean with zero warnings.

🤖 Generated with Amplifier

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
@bkrabach bkrabach changed the title feat: Phase 3 — WASM Module Loading via WebAssembly Component Model feat: Cross-Language SDK — Phases 1-4 (gRPC, WASM Bridges, Module Resolver) Mar 7, 2026
@bkrabach
Copy link
Collaborator Author

bkrabach commented Mar 7, 2026

Closing — dev/cross-language-sdk is the integration branch for ongoing cross-language SDK development (Phases 2-4 + gRPC debt fix). A single PR to main will be created when all work is dogfooted and ready for release. This avoids premature PRs that supersede each other.

@bkrabach bkrabach closed this Mar 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant