feat: Cross-Language SDK — Phases 1-4 (gRPC, WASM Bridges, Module Resolver)#38
Closed
feat: Cross-Language SDK — Phases 1-4 (gRPC, WASM Bridges, Module Resolver)#38
Conversation
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>
…age, enhance docs
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
…ew_detached, log serialization errors
…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
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
…play and equality tests
…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>
Collaborator
Author
|
Closing — |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
Phase 3: WASM Module Loading
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) → errorload_module(manifest, engine, coordinator)— Dispatches manifest to correctload_wasm_*bridge, returningLoadedModuleenumresolve_module()andload_wasm_from_path()exposed to PythonresolveModule()andloadWasmFromPath()exposed to TypeScriptloader_dispatch.py— Wired WASM/gRPC branches to Rust resolver with graceful fallbackNew/Modified Files (Phase 4)
crates/amplifier-core/src/module_resolver.rscrates/amplifier-core/tests/module_resolver_e2e.rscrates/amplifier-core/src/lib.rscrates/amplifier-core/src/models.rs(addedApprovalvariant)crates/amplifier-core/Cargo.toml(addedtoml+tempfiledeps)bindings/python/Cargo.toml(addedwasmfeature)bindings/python/src/lib.rs(added resolver functions)bindings/node/Cargo.toml(addedwasmfeature)bindings/node/src/lib.rs(added resolver functions)python/amplifier_core/loader_dispatch.pyTest Plan
amplifier-core,amplifier-core-py,amplifier-core-node