Architecture

Synwire uses a trait-based architecture where each concern is defined by a trait and implementations are swappable at runtime via trait objects.

Trait hierarchy

graph TD
    RunnableCore --> BaseChatModel
    RunnableCore --> Embeddings
    RunnableCore --> VectorStore
    RunnableCore --> Tool
    RunnableCore --> OutputParser
    BaseChatModel --> ChatOpenAI
    BaseChatModel --> ChatOllama
    BaseChatModel --> FakeChatModel
    Embeddings --> OpenAIEmbeddings
    Embeddings --> OllamaEmbeddings
    Embeddings --> FakeEmbeddings
    VectorStore --> InMemoryVectorStore
    Tool --> StructuredTool

Core traits

BaseChatModel

Defines invoke, batch, stream, model_type, and bind_tools. All methods use BoxFuture for dyn-compatibility. The batch and bind_tools methods have default implementations.

RunnableCore

The universal composition primitive. Uses serde_json::Value as the I/O type for object safety and heterogeneous chaining. Heterogeneous chaining in a statically-typed language requires a common I/O boundary; serde_json::Value provides that boundary without sacrificing Send + Sync or object safety.

Embeddings

Simple trait with embed_documents (batch) and embed_query (single text). Some providers use different models for queries versus documents.

VectorStore

Manages document storage and similarity search. Methods accept an &dyn Embeddings to decouple storage from embedding computation.

Tool

Defines name, description, schema, and invoke. Tools are Send + Sync for use across async tasks.

Design principles

Object safety via BoxFuture

Rust traits with async fn methods are not dyn-compatible. Synwire uses manual BoxFuture desugaring:

fn invoke<'a>(
    &'a self,
    input: &'a [Message],
    config: Option<&'a RunnableConfig>,
) -> BoxFuture<'a, Result<ChatResult, SynwireError>>;

This enables Box<dyn BaseChatModel> and &dyn BaseChatModel usage.

serde_json::Value as universal I/O

RunnableCore uses Value rather than generics because:

  • Generic trait parameters prevent Vec<Box<dyn RunnableCore>> for heterogeneous chains
  • Any runnable can compose with any other without explicit type conversions
  • The trade-off (runtime vs compile-time checking) matches the typical use case

Error hierarchy

SynwireError wraps domain-specific error types (ModelError, ToolError, ParseError, etc.) with #[from] conversions. SynwireErrorKind provides discriminant-based matching for retry and fallback logic without inspecting payloads.

Send + Sync everywhere

All traits require Send + Sync because the primary runtime is Tokio with multi-threaded executor. This enables shared ownership via Arc<dyn BaseChatModel> and spawning across task boundaries.

Layered crate architecture

graph BT
    synwire-core --> synwire-orchestrator
    synwire-core --> synwire-checkpoint
    synwire-core --> synwire-llm-openai
    synwire-core --> synwire-llm-ollama
    synwire-checkpoint --> synwire-checkpoint-sqlite
    synwire-core --> synwire
    synwire-core --> synwire-derive
    synwire-core --> synwire-test-utils
    synwire-checkpoint --> synwire-test-utils
    synwire-orchestrator --> synwire-test-utils

synwire-core has zero dependencies on other Synwire crates. Provider crates depend only on synwire-core. The orchestrator depends on synwire-core for trait definitions.