Skip to content
This repository was archived by the owner on Jun 3, 2026. It is now read-only.

feat: add host model provider interface in wit contract, host model provider to TS and SageMaker support to PY to enable host model calling for wasm architecture#877

Open
mehtarac wants to merge 1 commit into
strands-agents:mainfrom
mehtarac:host_mp
Open

feat: add host model provider interface in wit contract, host model provider to TS and SageMaker support to PY to enable host model calling for wasm architecture#877
mehtarac wants to merge 1 commit into
strands-agents:mainfrom
mehtarac:host_mp

Conversation

@mehtarac

Copy link
Copy Markdown
Member

Description

The existing architecture requires every model provider to be implemented in TypeScript and compiled into the WASM guest. This works for providers with well-supported JS SDKs (Bedrock, Anthropic, OpenAI, Gemini), but creates a barrier for providers like SageMaker, Mistral, and Writer where mature Python implementations already exist. Rather than rewriting each provider in TypeScript, this change introduces a model-provider WIT import that lets the WASM guest delegate model inference back to the Python host — the same pattern used by tool-provider for tool execution.

SageMaker is the first model provider to use this mechanism.

Public API Changes

Python developers can now pass host-side model providers directly to the Agent:

# Before: only in-guest providers (Bedrock, Anthropic, OpenAI, Gemini)
from strands import Agent
from strands.models import BedrockModel

agent = Agent(model=BedrockModel())

# After: host-side providers also supported
from strands import Agent
from strands.models.sagemaker import SageMakerAIModel

model = SageMakerAIModel(
    endpoint_config={"endpoint_name": "my-endpoint", "region_name": "us-east-1"},
    payload_config={"max_tokens": 1024, "temperature": 0.7, "stream": False},
)
agent = Agent(model=model, system_prompt="Be helpful.")
result = agent("Hello!")

The Agent automatically detects host-side models (any model with a stream() method) and routes them through the model-provider WIT import. In-guest providers continue to work unchanged.

The WIT contract adds a host-model variant to model-config and a new model-provider interface with an invoke function.

Related Issues

Documentation PR

Type of Change

Bug fix
New feature
Breaking change
Documentation update
Other (please describe):

Testing

How have you tested the change?

  • I ran npm run check

Checklist

  • [] I have read the CONTRIBUTING document
  • I have added any necessary tests that prove my fix is effective or my feature works
  • I have updated the documentation accordingly
  • I have added an appropriate example to the documentation to outline the feature, or no new docs are needed
  • My changes generate no new warnings
  • Any dependent changes have been merged and published

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

…rovider to TS and SageMaker support to PY to enable host model calling for wasm architecture
@github-actions github-actions Bot added the strands-running <strands-managed> Whether or not an agent is currently running label Apr 23, 2026
Comment thread wit/agent.wit

/// Invoke the host-side model and return the full sequence of events as a JSON array.
///
/// Stopgap: the ideal signature is result<list<model-event>, string>,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The wastime-py fork needs a fix we can make it there. In the meantime this is a temporary work around that allows the review process to continue further. Once the fix is implemented and merged in the fork will update the implementation in the wit contract.

# Detect host-side model providers (have stream() method — e.g. SageMaker, Mistral, Writer).
# These run on the Python side and are invoked by the WASM guest via model-provider import.
model_provider_callback = None
if model is not None and hasattr(model, "stream") and callable(getattr(model, "stream", None)):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: Host-model detection via duck-typing is fragile and may produce false positives. hasattr(model, "stream") would match any model that implements stream() — including custom in-guest model providers that subclass Model and define their own stream(). This means a user who passes a custom Model subclass with a stream() method would unexpectedly get routed to the host-model path.

Suggestion: Use a more explicit mechanism to distinguish host-side models. Consider:

  1. A marker base class or protocol (e.g., HostSideModel) that host-side providers must inherit from, or
  2. A negative check: verify the model does NOT have _to_config_dict() (the in-guest model marker) before falling through to host-model detection, or
  3. Both: if model is not None and not hasattr(model, '_to_config_dict') and hasattr(model, 'stream')

This would make the dispatch explicit rather than relying on duck-typing that could be accidentally triggered.

from dataclasses import dataclass
from typing import Any, TypedDict

import boto3

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: boto3 is imported at module top-level, but it's only listed as a test dependency ([project.optional-dependencies].test) in pyproject.toml, not a core dependency. Since SageMakerAIModel is re-exported from strands.models.__init__, any from strands.models import ... or import strands.models will trigger this import and fail with ModuleNotFoundError for users who don't have boto3 installed.

Suggestion: Either:

  1. Make boto3 an optional dependency and use a lazy import: boto3 = None; try: import boto3; except ImportError: pass (with a clear error at __init__ time), or
  2. Remove SageMakerAIModel from the top-level strands.models.__init__ re-export so it must be imported explicitly (from strands.models.sagemaker import SageMakerAIModel), or
  3. Add boto3 and botocore as core dependencies (less ideal as it bloats the package for non-SageMaker users)

The existing in-guest model providers (Bedrock, Anthropic, OpenAI, Gemini) don't have this issue because they don't import SDK clients — they delegate to the TS side.

self.function = FunctionCall(**kwargs.get("function", {"name": "", "arguments": ""}))


class SageMakerAIModel:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: SageMakerAIModel does not extend the Model base class, unlike all other model providers (BedrockModel(Model), AnthropicModel(Model), etc.). This breaks the established pattern and means SageMakerAIModel won't pass isinstance(model, Model) checks.

Suggestion: Have SageMakerAIModel extend Model. Since host-side models don't need _to_config_dict(), you could either leave it unimplemented (raise NotImplementedError) or implement it to return the host-model config. This also helps with the duck-typing detection issue in agent/__init__.py — you could check isinstance(model, Model) and not hasattr(model, '_to_config_dict') or introduce a HostModel(Model) base in Python.

logger = logging.getLogger(__name__)


def _run_in_thread(coro: Any) -> Any:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: _run_in_thread is functionally identical to the "running loop" branch of _run_sync in _wasm_host.py. This duplication violates the DRY principle and the repo guideline to "reduce code duplication, even if the refactoring takes extra effort."

Suggestion: Extract a shared utility (e.g., in _wasm_host.py or a new _async_util.py) and import it from both locations.

args.toolSpecs = toolSpecs
}
events = this._invoke(args)
} catch (err: any) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: err is typed as any (line 93: catch (err: any)), which is explicitly disallowed by the repo's ESLint rules and AGENTS.md ("Never use any type").

Suggestion: Use catch (err: unknown) and narrow the type appropriately:

catch (err: unknown) {
  const message = err instanceof Error ? err.message : String(err)
  logger.error('HostModel: host invoke failed', { error: message })
  throw new Error(`Host model provider error: ${message}`)
}


export class HostModel extends Model<HostModelConfig> {
private _config: HostModelConfig
private _invoke: (args: {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: The _invoke callback type signature is duplicated three times in this file (class property declaration, constructor parameter, and the args object in stream()). This makes the code harder to maintain — any change requires updating all three locations.

Suggestion: Extract a named type alias:

export type HostModelInvokeArgs = {
  messages: string
  systemPrompt?: string
  toolSpecs?: Array<{ name: string; description: string; inputSchema: string }>
  config: string
}

export type HostModelInvokeFn = (args: HostModelInvokeArgs) => Array<{ data: string }>

Then use HostModelInvokeFn for the class property and constructor parameter.

from collections.abc import Mapping
from typing import Any

from typing_extensions import get_type_hints

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: _validation.py imports typing_extensions.get_type_hints, but typing_extensions is not listed as a dependency in pyproject.toml. This will cause an ImportError at runtime.

Suggestion: Either add typing_extensions to the project dependencies, or use typing.get_type_hints from the standard library (available since Python 3.5).

raw_specs = getattr(args, "tool-specs")
tool_specs_json: str | None = None
if raw_specs is not None:
import json as _json

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: json is imported inline twice with aliases (_json, _json2) on lines 514 and 530 instead of using the module-level import. This is unusual and unnecessary — json is a stdlib module with negligible import cost.

Suggestion: Use the module-level import json that already exists at the top of the file (or add it if missing). Remove the inline aliases.

Comment thread strands-wasm/entry.ts
}): Array<{ data: string }> {
const result = hostModelInvoke(args);
let jsonStr: string;
if (typeof result === 'object' && result !== null && 'tag' in result) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: The unwrapHostModelResult function extensively uses as any casts (lines 180-186) to work around the WIT result type. This bypasses TypeScript's type safety and is prohibited by the repo's coding standards.

Suggestion: Define a proper type for the WIT result variant:

type WitResult = { tag: 'ok'; val: string } | { tag: 'err'; val: string }

Then use type narrowing instead of casts:

function unwrapHostModelResult(args: ...): Array<{ data: string }> {
  const result = hostModelInvoke(args) as WitResult
  if (result.tag === 'err') {
    throw new Error(result.val)
  }
  const eventStrings: string[] = JSON.parse(result.val)
  return eventStrings.map((s) => ({ data: s }))
}


for tool_deltas in tool_calls.values():
if not tool_deltas[0]["function"].get("name"):
raise Exception("The model did not provide a tool name.")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: raise Exception(...) is used instead of a more specific exception type. Bare Exception makes it harder for callers to distinguish tool-name errors from other failures.

Suggestion: Use ValueError or a custom exception type, consistent with the error handling patterns in the rest of the file.

Comment thread wit/agent.wit
/// Currently unused in the invoke return type due to a wasmtime-py
/// limitation — see the comment on model-provider.invoke below.
/// Retained for future use when the return type can be upgraded to
/// result<list<model-event>, string>.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: The model-event record is defined in the WIT contract but explicitly documented as unused ("Currently unused in the invoke return type due to a wasmtime-py limitation"). WIT contracts define the public API surface between host and guest — unused types add dead weight and can confuse future contributors.

Suggestion: Remove model-event from the WIT contract for now and add it back when the wasmtime-py limitation is resolved. The comment block already documents the workaround rationale, which is sufficient. Alternatively, if you want to keep it as a documentation-of-intent, consider adding it as a WIT comment rather than a live type.

@github-actions

Copy link
Copy Markdown
Contributor

Review Summary

Assessment: Request Changes

This PR introduces a well-architected pattern for delegating model inference from the WASM guest back to the Python host — an important capability for providers where mature Python implementations already exist. The WIT contract additions are clean and the overall data flow (Python → WIT → TS HostModel → WIT → Python adapter → provider) is sound.

However, there are several issues that should be addressed before merging.

Review Categories
  • Correctness: The host-model detection heuristic (hasattr(model, "stream")) risks false positives against custom in-guest models. A more explicit dispatch mechanism is needed.
  • Dependencies: boto3 is imported eagerly at module top-level but is only a test dependency; typing_extensions is imported but not declared as a dependency at all. Both will cause ImportError for users.
  • Testing: No unit tests for HostModelAdapter, HostModel (TS), or SageMakerAIModel. The sole integration test is gated behind an env var. This doesn't meet the 80%+ coverage requirement.
  • Type Safety: Several any casts in the TS code (host-model.ts, entry.ts) violate the repo's no-any rule.
  • Code Duplication: _run_in_thread in host_adapter.py duplicates _run_sync in _wasm_host.py.
  • API Review: This PR introduces a new public model class (SageMakerAIModel) and a new WIT interface (model-provider). Given the scope, it should carry the needs-api-review label per the API Bar Raising process.

The extensibility pattern here is strong — making it easy to add new host-side providers (Mistral, Writer, etc.) in the future.

@github-actions github-actions Bot removed the strands-running <strands-managed> Whether or not an agent is currently running label Apr 23, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants