Skip to content

windlass

windlass

Windlass — the modular AI application framework.

One elegant API for agents, RAG, tools, MCP, guardrails, evaluation and observability. Windlass is not a wrapper around other libraries; it is an architecture that lets them be swapped, extended and replaced without rewriting your application.

Quick start::

from windlass import Windlass

rag = Windlass.rag()
rag.ingest("./documents")
print(rag.ask("What changed in the API last quarter?"))

agent = Windlass.agent().llm("openai").tool(my_function).memory()
print(agent.run("Summarise my open tickets"))

Three levels of API, always:

  1. Simplerag.ask("...") works with sensible defaults.
  2. Configuredrag.chunker("semantic", chunk_size=1000, overlap=200).
  3. Nativerag.native_store(), agent.native_graph(). Windlass simplifies libraries; it never hides them.

Importing this module is cheap and pulls in no optional dependency. Providers are registered by dotted path and imported only when you actually use them.

Windlass

Windlass()

Factory namespace for every Windlass component and builder.

Example

from windlass import Windlass rag = Windlass.rag() rag.ingest_text("Windlass unifies the AI ecosystem behind one API.") 1 "Windlass" in str(rag.ask("What does Windlass do?").contexts[0].content) True

Source code in src\windlass\api.py
def __init__(self) -> None:  # pragma: no cover - guard against misuse
    raise TypeError(
        "Windlass is a namespace of factories, not something to instantiate. "
        "Call Windlass.rag() or Windlass.agent() instead."
    )

rag staticmethod

rag(container: Container | None = None) -> Any

Start building a RAG pipeline.

Parameters:

Name Type Description Default
container Container | None

Dependency container. Defaults to a child of the process root, so application-wide bindings are inherited.

None

Returns:

Name Type Description
A Any

class:~windlass.rag.builder.RAGBuilder.

Example

from windlass import Windlass rag = (Windlass.rag() ... .chunker("recursive", chunk_size=800) ... .retriever("hybrid") ... .top_k(8)) rag.ingest_text("Vector databases store embeddings.") 1

Source code in src\windlass\api.py
@staticmethod
def rag(container: Container | None = None) -> Any:
    """Start building a RAG pipeline.

    Args:
        container: Dependency container. Defaults to a child of the process
            root, so application-wide bindings are inherited.

    Returns:
        A :class:`~windlass.rag.builder.RAGBuilder`.

    Example:
        >>> from windlass import Windlass
        >>> rag = (Windlass.rag()
        ...        .chunker("recursive", chunk_size=800)
        ...        .retriever("hybrid")
        ...        .top_k(8))
        >>> rag.ingest_text("Vector databases store embeddings.")
        1
    """
    from windlass.rag.builder import RAGBuilder

    return RAGBuilder(container)

agent staticmethod

agent(container: Container | None = None) -> Any

Start building an agent.

Parameters:

Name Type Description Default
container Container | None

Dependency container.

None

Returns:

Name Type Description
An Any

class:~windlass.agent.builder.AgentBuilder.

Example

from windlass import Windlass agent = Windlass.agent().llm("fake", responses=["Hello!"]) agent.run("Say hello").output 'Hello!'

Source code in src\windlass\api.py
@staticmethod
def agent(container: Container | None = None) -> Any:
    """Start building an agent.

    Args:
        container: Dependency container.

    Returns:
        An :class:`~windlass.agent.builder.AgentBuilder`.

    Example:
        >>> from windlass import Windlass
        >>> agent = Windlass.agent().llm("fake", responses=["Hello!"])
        >>> agent.run("Say hello").output
        'Hello!'
    """
    from windlass.agent.builder import AgentBuilder

    return AgentBuilder(container)

supervisor staticmethod

supervisor(
    agents: dict[str, Any],
    *,
    llm: Any = None,
    descriptions: dict[str, str] | None = None,
    **config: Any
) -> Any

Build a multi-agent supervisor.

Parameters:

Name Type Description Default
agents dict[str, Any]

Specialists keyed by the name the supervisor will use.

required
llm Any

The coordinating model. Defaults to the configured provider.

None
descriptions dict[str, str] | None

What each specialist is for. The supervisor routes on this text, so it is worth writing carefully.

None
**config Any

Forwarded to :class:~windlass.agent.supervisor.Supervisor.

{}

Returns:

Name Type Description
A Any

class:~windlass.agent.supervisor.Supervisor.

Example

from windlass import Windlass boss = Windlass.supervisor( ... {"writer": Windlass.agent().llm("fake", responses=["Drafted."])}, ... llm=Windlass.llm("fake", responses=["Done."]), ... ) boss.broadcast("write something")["writer"].output 'Drafted.'

Source code in src\windlass\api.py
@staticmethod
def supervisor(
    agents: dict[str, Any],
    *,
    llm: Any = None,
    descriptions: dict[str, str] | None = None,
    **config: Any,
) -> Any:
    """Build a multi-agent supervisor.

    Args:
        agents: Specialists keyed by the name the supervisor will use.
        llm: The coordinating model. Defaults to the configured provider.
        descriptions: What each specialist is for. The supervisor routes on
            this text, so it is worth writing carefully.
        **config: Forwarded to :class:`~windlass.agent.supervisor.Supervisor`.

    Returns:
        A :class:`~windlass.agent.supervisor.Supervisor`.

    Example:
        >>> from windlass import Windlass
        >>> boss = Windlass.supervisor(
        ...     {"writer": Windlass.agent().llm("fake", responses=["Drafted."])},
        ...     llm=Windlass.llm("fake", responses=["Done."]),
        ... )
        >>> boss.broadcast("write something")["writer"].output
        'Drafted.'
    """
    from windlass.agent.supervisor import Supervisor

    return Supervisor(
        llm=llm or Windlass.llm(),
        agents={k: (v.build() if hasattr(v, "build") else v) for k, v in agents.items()},
        descriptions=descriptions,
        **config,
    )

llm staticmethod

llm(name: str | None = None, /, **config: Any) -> Any

Construct a language model.

Parameters:

Name Type Description Default
name str | None

Registry name, or a bare model id like "gpt-4o". Defaults to the configured provider.

None
**config Any

Provider options.

{}

Returns:

Name Type Description
An Any

class:~windlass.interfaces.llm.LLM.

Raises:

Type Description
ComponentNotFoundError

For an unknown provider.

MissingDependencyError

When the provider's extra is not installed.

Example

from windlass import Windlass Windlass.llm("fake", responses=["hi"]).complete("x").content 'hi'

Source code in src\windlass\api.py
@staticmethod
def llm(name: str | None = None, /, **config: Any) -> Any:
    """Construct a language model.

    Args:
        name: Registry name, or a bare model id like ``"gpt-4o"``. Defaults
            to the configured provider.
        **config: Provider options.

    Returns:
        An :class:`~windlass.interfaces.llm.LLM`.

    Raises:
        ComponentNotFoundError: For an unknown provider.
        MissingDependencyError: When the provider's extra is not installed.

    Example:
        >>> from windlass import Windlass
        >>> Windlass.llm("fake", responses=["hi"]).complete("x").content
        'hi'
    """
    from windlass.agent.builder import _split_model

    if name:
        provider, model = _split_model(name)
        if model and "model" not in config:
            config["model"] = model
        name = provider
    return REGISTRY.create("llm", name or settings().default_llm, **config)

embedding staticmethod

embedding(name: str | None = None, /, **config: Any) -> Any

Construct an embedding model.

Parameters:

Name Type Description Default
name str | None

Registry name. Defaults to the configured provider.

None
**config Any

Provider options.

{}

Returns:

Name Type Description
An Any

class:~windlass.interfaces.embedding.Embedder.

Source code in src\windlass\api.py
@staticmethod
def embedding(name: str | None = None, /, **config: Any) -> Any:
    """Construct an embedding model.

    Args:
        name: Registry name. Defaults to the configured provider.
        **config: Provider options.

    Returns:
        An :class:`~windlass.interfaces.embedding.Embedder`.
    """
    return REGISTRY.create("embedding", name or settings().default_embedding, **config)

vectordb staticmethod

vectordb(name: str | None = None, /, **config: Any) -> Any

Construct a vector store.

Parameters:

Name Type Description Default
name str | None

Registry name. Defaults to the configured store.

None
**config Any

Store options.

{}

Returns:

Name Type Description
A Any

class:~windlass.interfaces.vectordb.VectorStore.

Source code in src\windlass\api.py
@staticmethod
def vectordb(name: str | None = None, /, **config: Any) -> Any:
    """Construct a vector store.

    Args:
        name: Registry name. Defaults to the configured store.
        **config: Store options.

    Returns:
        A :class:`~windlass.interfaces.vectordb.VectorStore`.
    """
    return REGISTRY.create("vectordb", name or settings().default_vectordb, **config)

chunker staticmethod

chunker(name: str | None = None, /, **config: Any) -> Any

Construct a chunker.

Parameters:

Name Type Description Default
name str | None

Registry name. Defaults to the configured strategy.

None
**config Any

Strategy options.

{}

Returns:

Name Type Description
A Any

class:~windlass.interfaces.chunker.Chunker.

Example

from windlass import Windlass len(Windlass.chunker("recursive", chunk_size=20).split_text("a " * 40)) > 1 True

Source code in src\windlass\api.py
@staticmethod
def chunker(name: str | None = None, /, **config: Any) -> Any:
    """Construct a chunker.

    Args:
        name: Registry name. Defaults to the configured strategy.
        **config: Strategy options.

    Returns:
        A :class:`~windlass.interfaces.chunker.Chunker`.

    Example:
        >>> from windlass import Windlass
        >>> len(Windlass.chunker("recursive", chunk_size=20).split_text("a " * 40)) > 1
        True
    """
    return REGISTRY.create("chunker", name or settings().default_chunker, **config)

retriever staticmethod

retriever(name: str | None = None, /, **config: Any) -> Any

Construct a retriever.

Parameters:

Name Type Description Default
name str | None

Registry name. Defaults to the configured strategy.

None
**config Any

Strategy options. Dense strategies need embedder and vectorstore.

{}

Returns:

Name Type Description
A Any

class:~windlass.interfaces.retriever.Retriever.

Source code in src\windlass\api.py
@staticmethod
def retriever(name: str | None = None, /, **config: Any) -> Any:
    """Construct a retriever.

    Args:
        name: Registry name. Defaults to the configured strategy.
        **config: Strategy options. Dense strategies need ``embedder`` and
            ``vectorstore``.

    Returns:
        A :class:`~windlass.interfaces.retriever.Retriever`.
    """
    return REGISTRY.create("retriever", name or settings().default_retriever, **config)

loader staticmethod

loader(name: str | None = None, /, **config: Any) -> Any

Construct a document loader.

Parameters:

Name Type Description Default
name str | None

Registry name. Omit it for automatic format detection.

None
**config Any

Loader options.

{}

Returns:

Name Type Description
A Any

class:~windlass.interfaces.loader.Loader.

Source code in src\windlass\api.py
@staticmethod
def loader(name: str | None = None, /, **config: Any) -> Any:
    """Construct a document loader.

    Args:
        name: Registry name. Omit it for automatic format detection.
        **config: Loader options.

    Returns:
        A :class:`~windlass.interfaces.loader.Loader`.
    """
    if name is None:
        from windlass.rag.loading import AutoLoader

        return AutoLoader(**config)
    return REGISTRY.create("loader", name, **config)

preprocessor staticmethod

preprocessor(name: str = 'clean', /, **config: Any) -> Any

Construct a preprocessor.

Parameters:

Name Type Description Default
name str

Registry name.

'clean'
**config Any

Preprocessor options.

{}

Returns:

Name Type Description
A Any

class:~windlass.interfaces.preprocessor.Preprocessor.

Source code in src\windlass\api.py
@staticmethod
def preprocessor(name: str = "clean", /, **config: Any) -> Any:
    """Construct a preprocessor.

    Args:
        name: Registry name.
        **config: Preprocessor options.

    Returns:
        A :class:`~windlass.interfaces.preprocessor.Preprocessor`.
    """
    return REGISTRY.create("preprocessor", name, **config)

memory staticmethod

memory(name: str = 'window', /, **config: Any) -> Any

Construct a memory backend.

Parameters:

Name Type Description Default
name str

Registry name.

'window'
**config Any

Memory options.

{}

Returns:

Name Type Description
A Any

class:~windlass.interfaces.memory.Memory.

Source code in src\windlass\api.py
@staticmethod
def memory(name: str = "window", /, **config: Any) -> Any:
    """Construct a memory backend.

    Args:
        name: Registry name.
        **config: Memory options.

    Returns:
        A :class:`~windlass.interfaces.memory.Memory`.
    """
    return REGISTRY.create("memory", name, **config)

guardrail staticmethod

guardrail(name: str = 'rules', /, **config: Any) -> Any

Construct a guardrail.

Parameters:

Name Type Description Default
name str

Registry name.

'rules'
**config Any

Policy options.

{}

Returns:

Name Type Description
A Any

class:~windlass.interfaces.guardrail.Guardrail.

Example

from windlass import Windlass Windlass.guardrail(on_violation="redact").validate("mail a@b.com") 'mail [EMAIL]'

Source code in src\windlass\api.py
@staticmethod
def guardrail(name: str = "rules", /, **config: Any) -> Any:
    """Construct a guardrail.

    Args:
        name: Registry name.
        **config: Policy options.

    Returns:
        A :class:`~windlass.interfaces.guardrail.Guardrail`.

    Example:
        >>> from windlass import Windlass
        >>> Windlass.guardrail(on_violation="redact").validate("mail a@b.com")
        'mail [EMAIL]'
    """
    return REGISTRY.create("guardrail", name, **config)

evaluator staticmethod

evaluator(name: str = 'builtin', /, **config: Any) -> Any

Construct an evaluator.

Parameters:

Name Type Description Default
name str

Registry name — "builtin", "ragas" or "deepeval".

'builtin'
**config Any

Evaluator options, including metrics and llm.

{}

Returns:

Name Type Description
An Any

class:~windlass.interfaces.evaluator.Evaluator.

Source code in src\windlass\api.py
@staticmethod
def evaluator(name: str = "builtin", /, **config: Any) -> Any:
    """Construct an evaluator.

    Args:
        name: Registry name — ``"builtin"``, ``"ragas"`` or ``"deepeval"``.
        **config: Evaluator options, including ``metrics`` and ``llm``.

    Returns:
        An :class:`~windlass.interfaces.evaluator.Evaluator`.
    """
    return REGISTRY.create("evaluator", name, **config)

tracer staticmethod

tracer(name: str = 'console', /, **config: Any) -> Any

Construct a tracer.

Parameters:

Name Type Description Default
name str

Registry name.

'console'
**config Any

Tracer options.

{}

Returns:

Name Type Description
A Any

class:~windlass.interfaces.tracer.Tracer.

Source code in src\windlass\api.py
@staticmethod
def tracer(name: str = "console", /, **config: Any) -> Any:
    """Construct a tracer.

    Args:
        name: Registry name.
        **config: Tracer options.

    Returns:
        A :class:`~windlass.interfaces.tracer.Tracer`.
    """
    return REGISTRY.create("tracer", name, **config)

mcp staticmethod

mcp(name: str = 'fastmcp', /, **config: Any) -> Any

Construct an MCP client.

Parameters:

Name Type Description Default
name str

Registry name.

'fastmcp'
**config Any

Transport options.

{}

Returns:

Name Type Description
An Any

class:~windlass.interfaces.mcp.MCPClient.

Source code in src\windlass\api.py
@staticmethod
def mcp(name: str = "fastmcp", /, **config: Any) -> Any:
    """Construct an MCP client.

    Args:
        name: Registry name.
        **config: Transport options.

    Returns:
        An :class:`~windlass.interfaces.mcp.MCPClient`.
    """
    return REGISTRY.create("mcp", name, **config)

checkpointer staticmethod

checkpointer(name: str = 'memory', /, **config: Any) -> Any

Construct a checkpointer.

Parameters:

Name Type Description Default
name str

Registry name — "memory" or "sqlite".

'memory'
**config Any

Checkpointer options.

{}

Returns:

Name Type Description
A Any

class:~windlass.agent.checkpoint.Checkpointer.

Source code in src\windlass\api.py
@staticmethod
def checkpointer(name: str = "memory", /, **config: Any) -> Any:
    """Construct a checkpointer.

    Args:
        name: Registry name — ``"memory"`` or ``"sqlite"``.
        **config: Checkpointer options.

    Returns:
        A :class:`~windlass.agent.checkpoint.Checkpointer`.
    """
    return REGISTRY.create("checkpointer", name, **config)

create staticmethod

create(kind: str, name: str, /, **config: Any) -> Any

Construct any registered component, including custom kinds.

Parameters:

Name Type Description Default
kind str

Component kind.

required
name str

Registry name.

required
**config Any

Constructor options.

{}

Returns:

Type Description
Any

The component.

Source code in src\windlass\api.py
@staticmethod
def create(kind: str, name: str, /, **config: Any) -> Any:
    """Construct any registered component, including custom kinds.

    Args:
        kind: Component kind.
        name: Registry name.
        **config: Constructor options.

    Returns:
        The component.
    """
    return REGISTRY.create(kind, name, **config)

list staticmethod

list(kind: str | None = None) -> Any

List what is registered.

Parameters:

Name Type Description Default
kind str | None

A component kind, or None for everything grouped by kind.

None

Returns:

Type Description
Any

Sorted names for one kind, or a {kind: [names]} mapping.

Example

from windlass import Windlass "recursive" in Windlass.list("chunker") True "llm" in Windlass.list() True

Source code in src\windlass\api.py
@staticmethod
def list(kind: str | None = None) -> Any:
    """List what is registered.

    Args:
        kind: A component kind, or ``None`` for everything grouped by kind.

    Returns:
        Sorted names for one kind, or a ``{kind: [names]}`` mapping.

    Example:
        >>> from windlass import Windlass
        >>> "recursive" in Windlass.list("chunker")
        True
        >>> "llm" in Windlass.list()
        True
    """
    if kind is not None:
        return REGISTRY.names(kind)
    return {k: REGISTRY.names(k) for k in REGISTRY.kinds()}

catalog staticmethod

catalog(kind: str | None = None) -> builtins.list[ComponentSpec]

Return full registry entries, including descriptions and extras.

Parameters:

Name Type Description Default
kind str | None

A component kind, or None for everything.

None

Returns:

Type Description
list[ComponentSpec]

The matching :class:~windlass.core.registry.ComponentSpec objects.

Source code in src\windlass\api.py
@staticmethod
def catalog(kind: str | None = None) -> builtins.list[ComponentSpec]:
    """Return full registry entries, including descriptions and extras.

    Args:
        kind: A component kind, or ``None`` for everything.

    Returns:
        The matching :class:`~windlass.core.registry.ComponentSpec` objects.
    """
    if kind is not None:
        return REGISTRY.specs(kind)
    return [spec for k in REGISTRY.kinds() for spec in REGISTRY.specs(k)]

kinds staticmethod

kinds() -> builtins.list[str]

Return every component kind Windlass knows about.

Source code in src\windlass\api.py
@staticmethod
def kinds() -> builtins.list[str]:
    """Return every component kind Windlass knows about."""
    return REGISTRY.kinds()

configure staticmethod

configure(**overrides: Any) -> WindlassSettings

Update process-wide settings.

Parameters:

Name Type Description Default
**overrides Any

Any field of :class:~windlass.core.config.WindlassSettings.

{}

Returns:

Type Description
WindlassSettings

The new settings.

Example

from windlass import Windlass Windlass.configure(temperature=0.0).temperature 0.0

Source code in src\windlass\api.py
@staticmethod
def configure(**overrides: Any) -> WindlassSettings:
    """Update process-wide settings.

    Args:
        **overrides: Any field of
            :class:`~windlass.core.config.WindlassSettings`.

    Returns:
        The new settings.

    Example:
        >>> from windlass import Windlass
        >>> Windlass.configure(temperature=0.0).temperature
        0.0
    """
    return configure(**overrides)

settings staticmethod

settings() -> WindlassSettings

Return the active settings.

Source code in src\windlass\api.py
@staticmethod
def settings() -> WindlassSettings:
    """Return the active settings."""
    return settings()

container staticmethod

container() -> Container

Return the process-wide root dependency container.

Bindings placed here are inherited by every builder created afterwards, which makes it the right place for application-wide wiring::

Windlass.container().bind_instance("tracer", MyTracer())

Returns:

Type Description
Container

The root :class:~windlass.core.container.Container.

Source code in src\windlass\api.py
@staticmethod
def container() -> Container:
    """Return the process-wide root dependency container.

    Bindings placed here are inherited by every builder created afterwards,
    which makes it the right place for application-wide wiring::

        Windlass.container().bind_instance("tracer", MyTracer())

    Returns:
        The root :class:`~windlass.core.container.Container`.
    """
    return root_container()

plugins staticmethod

plugins(*, strict: bool = False) -> builtins.list[str]

Discover entry-point plugins.

Runs automatically on first registry lookup; call it explicitly to see what loaded, or with strict=True to surface plugin errors.

Parameters:

Name Type Description Default
strict bool

Raise on the first plugin that fails.

False

Returns:

Type Description
list[str]

The names that loaded successfully.

Source code in src\windlass\api.py
@staticmethod
def plugins(*, strict: bool = False) -> builtins.list[str]:
    """Discover entry-point plugins.

    Runs automatically on first registry lookup; call it explicitly to see
    what loaded, or with ``strict=True`` to surface plugin errors.

    Args:
        strict: Raise on the first plugin that fails.

    Returns:
        The names that loaded successfully.
    """
    return REGISTRY.load_plugins(strict=strict)

tools staticmethod

tools(*functions: Any) -> Sequence[Any]

Wrap plain functions as tools.

A convenience for binding several existing functions at once.

Parameters:

Name Type Description Default
*functions Any

Functions to wrap.

()

Returns:

Type Description
Sequence[Any]

The resulting tools.

Example

from windlass import Windlass def ping() -> str: ... '''Return pong.''' ... return "pong" [t.name for t in Windlass.tools(ping)]['ping']

Source code in src\windlass\api.py
@staticmethod
def tools(*functions: Any) -> Sequence[Any]:
    """Wrap plain functions as tools.

    A convenience for binding several existing functions at once.

    Args:
        *functions: Functions to wrap.

    Returns:
        The resulting tools.

    Example:
        >>> from windlass import Windlass
        >>> def ping() -> str:
        ...     '''Return pong.'''
        ...     return "pong"
        >>> [t.name for t in Windlass.tools(ping)]
        ['ping']
    """
    from windlass.interfaces.tool import Tool
    from windlass.tools import FunctionTool

    return [f if isinstance(f, Tool) else FunctionTool(f) for f in functions]

WindlassSettings

Bases: BaseSettings

Process-wide defaults and credentials.

Every field can be set with an environment variable. Provider keys accept both the Windlass-prefixed name and the vendor's conventional name, so an existing OPENAI_API_KEY in your shell just works.

Attributes:

Name Type Description
default_llm str

Provider used when a builder does not name one.

default_model str

Model id used when a builder does not name one.

default_embedding str

Embedding provider used by RAG builders.

default_vectordb str

Vector store used by RAG builders.

default_chunker str

Chunking strategy used by RAG builders.

default_retriever str

Retrieval strategy used by RAG builders.

temperature float

Default sampling temperature.

max_tokens int | None

Default completion ceiling; None defers to the provider.

request_timeout float

Per-request timeout in seconds.

max_concurrency int

Ceiling on in-flight provider calls for batch work.

batch_size int

Default batch size for embedding and indexing.

openai_api_key SecretStr | None

Credential for OpenAI-compatible endpoints.

openai_base_url str | None

Override for proxies and Azure-style gateways.

anthropic_api_key SecretStr | None

Credential for Anthropic.

google_api_key SecretStr | None

Credential for Gemini.

groq_api_key SecretStr | None

Credential for Groq.

huggingface_api_key SecretStr | None

Token for the HuggingFace Inference API.

ollama_base_url str

Where the local Ollama daemon listens.

pinecone_api_key SecretStr | None

Credential for Pinecone.

langsmith_api_key SecretStr | None

Credential for LangSmith tracing.

langfuse_public_key SecretStr | None

Public key for Langfuse.

langfuse_secret_key SecretStr | None

Secret key for Langfuse.

langfuse_host str

Langfuse endpoint.

telemetry bool

Whether Windlass may emit anonymous usage counters. Off by default; Windlass ships no telemetry backend, the flag exists so downstream distributions can honour it.

strict_plugins bool

Fail loudly when a third-party plugin cannot load.

retry RetryConfig

Retry policy.

cache CacheConfig

Cache policy.

Example

s = WindlassSettings(default_llm="anthropic", temperature=0.0) s.default_llm, s.temperature ('anthropic', 0.0)

secret

secret(field: str) -> str | None

Return a credential's plaintext value, or None when unset.

Parameters:

Name Type Description Default
field str

Attribute name, e.g. "openai_api_key".

required

Returns:

Type Description
str | None

The secret value, or None.

Raises:

Type Description
ConfigurationError

If field is not a known setting.

Source code in src\windlass\core\config.py
def secret(self, field: str) -> str | None:
    """Return a credential's plaintext value, or ``None`` when unset.

    Args:
        field: Attribute name, e.g. ``"openai_api_key"``.

    Returns:
        The secret value, or ``None``.

    Raises:
        ConfigurationError: If ``field`` is not a known setting.
    """
    if not hasattr(self, field):
        raise ConfigurationError(f"Unknown setting {field!r}.")
    value = getattr(self, field)
    if value is None:
        return None
    return value.get_secret_value() if isinstance(value, SecretStr) else str(value)

require_secret

require_secret(field: str, *, provider: str, env_var: str) -> str

Return a credential or raise a clear authentication error.

Parameters:

Name Type Description Default
field str

Attribute name holding the credential.

required
provider str

Provider name for the error message.

required
env_var str

The environment variable a user would normally set.

required

Returns:

Type Description
str

The secret value.

Raises:

Type Description
AuthenticationError

When the credential is missing.

Source code in src\windlass\core\config.py
def require_secret(self, field: str, *, provider: str, env_var: str) -> str:
    """Return a credential or raise a clear authentication error.

    Args:
        field: Attribute name holding the credential.
        provider: Provider name for the error message.
        env_var: The environment variable a user would normally set.

    Returns:
        The secret value.

    Raises:
        AuthenticationError: When the credential is missing.
    """
    value = self.secret(field)
    if not value:
        from windlass.core.exceptions import AuthenticationError

        raise AuthenticationError(
            f"No API key configured for the {provider} provider.",
            provider=provider,
            hint=(
                f"Set {env_var} in your environment, add it to a .env file, or pass\n"
                f"    Windlass.agent().llm('{provider}', api_key='...')"
            ),
        )
    return value

masked

masked() -> dict[str, Any]

Return settings as a dict with every secret replaced by '***'.

Safe to log or print. Used by windlass config.

Source code in src\windlass\core\config.py
def masked(self) -> dict[str, Any]:
    """Return settings as a dict with every secret replaced by ``'***'``.

    Safe to log or print. Used by ``windlass config``.
    """
    data = self.model_dump(mode="json")
    for key, value in list(data.items()):
        if isinstance(getattr(self, key, None), SecretStr):
            data[key] = "***" if value else None
    return data

Container

Container(parent: Container | None = None, registry: Registry | None = None)

A hierarchical, thread-safe dependency container.

Parameters:

Name Type Description Default
parent Container | None

Optional parent whose bindings are inherited.

None
registry Registry | None

Component registry used to resolve string names. Defaults to the global :data:~windlass.core.registry.REGISTRY.

None
Example

c = Container() _ = c.bind("greeting", lambda: "hello") c.resolve("greeting") 'hello' child = c.scope() _ = child.bind("greeting", lambda: "hi") c.resolve("greeting"), child.resolve("greeting") ('hello', 'hi')

Source code in src\windlass\core\container.py
def __init__(self, parent: Container | None = None, registry: Registry | None = None) -> None:
    self._parent = parent
    inherited: Registry = parent._registry if parent is not None else REGISTRY
    self._registry: Registry = registry or inherited
    self._bindings: dict[Any, Binding] = {}
    self._lock = threading.RLock()

bind

bind(
    key: Any,
    factory: Callable[..., Any],
    *,
    singleton: bool = True,
    override: bool = True
) -> Container

Bind key to a factory.

Parameters:

Name Type Description Default
key Any

A string, a type, or any hashable token.

required
factory Callable[..., Any]

Zero-argument callable, or one accepting the container.

required
singleton bool

Reuse the first produced value.

True
override bool

Replace an existing binding. When False, an existing binding is kept and the call is a no-op.

True

Returns:

Type Description
Container

self, so calls chain.

Raises:

Type Description
ConfigurationError

If factory is not callable.

Source code in src\windlass\core\container.py
def bind(
    self,
    key: Any,
    factory: Callable[..., Any],
    *,
    singleton: bool = True,
    override: bool = True,
) -> Container:
    """Bind ``key`` to a factory.

    Args:
        key: A string, a type, or any hashable token.
        factory: Zero-argument callable, or one accepting the container.
        singleton: Reuse the first produced value.
        override: Replace an existing binding. When False, an existing
            binding is kept and the call is a no-op.

    Returns:
        ``self``, so calls chain.

    Raises:
        ConfigurationError: If ``factory`` is not callable.
    """
    if not callable(factory):
        raise ConfigurationError(
            f"Binding for {key!r} must be callable, got {type(factory).__name__}.",
            hint="Pass a factory (or use bind_instance for a ready-made object).",
        )
    with self._lock:
        if not override and key in self._bindings:
            return self
        self._bindings[key] = Binding(factory=factory, singleton=singleton)
    return self

bind_instance

bind_instance(key: Any, instance: Any, *, override: bool = True) -> Container

Bind key to an already-constructed object.

Parameters:

Name Type Description Default
key Any

Lookup key.

required
instance Any

The object to return on resolution.

required
override bool

Replace an existing binding.

True

Returns:

Type Description
Container

self.

Source code in src\windlass\core\container.py
def bind_instance(self, key: Any, instance: Any, *, override: bool = True) -> Container:
    """Bind ``key`` to an already-constructed object.

    Args:
        key: Lookup key.
        instance: The object to return on resolution.
        override: Replace an existing binding.

    Returns:
        ``self``.
    """
    with self._lock:
        if not override and key in self._bindings:
            return self
        self._bindings[key] = Binding(
            factory=lambda: instance, singleton=True, value=instance, produced=True
        )
    return self

unbind

unbind(key: Any) -> None

Remove a local binding. Parent bindings become visible again.

Source code in src\windlass\core\container.py
def unbind(self, key: Any) -> None:
    """Remove a local binding. Parent bindings become visible again."""
    with self._lock:
        self._bindings.pop(key, None)

has

has(key: Any) -> bool

Return whether key resolves here or in any ancestor.

Source code in src\windlass\core\container.py
def has(self, key: Any) -> bool:
    """Return whether ``key`` resolves here or in any ancestor."""
    if key in self._bindings:
        return True
    return self._parent.has(key) if self._parent else False

resolve

resolve(key: type[_T]) -> _T
resolve(key: type[_T], default: _T) -> _T
resolve(key: Any, default: Any = ...) -> Any
resolve(key: Any, default: Any = _UNSET) -> Any

Produce the value bound to key.

Singletons are produced at most once, under a lock, so concurrent first resolutions do not both run the factory.

Parameters:

Name Type Description Default
key Any

Lookup key.

required
default Any

Returned instead of raising when nothing is bound.

_UNSET

Returns:

Type Description
Any

The resolved value.

Raises:

Type Description
ConfigurationError

When nothing is bound and no default was given, or when the factory itself fails.

Source code in src\windlass\core\container.py
def resolve(self, key: Any, default: Any = _UNSET) -> Any:
    """Produce the value bound to ``key``.

    Singletons are produced at most once, under a lock, so concurrent first
    resolutions do not both run the factory.

    Args:
        key: Lookup key.
        default: Returned instead of raising when nothing is bound.

    Returns:
        The resolved value.

    Raises:
        ConfigurationError: When nothing is bound and no ``default`` was
            given, or when the factory itself fails.
    """
    binding = self._find(key)
    if binding is None:
        if default is not _UNSET:
            return default
        raise ConfigurationError(
            f"Nothing is bound for {_label(key)}.",
            hint="Bind it with container.bind(...) or pass it explicitly to the builder.",
            context={"key": _label(key), "bound": [_label(k) for k in self.keys()]},
        )

    if binding.singleton and binding.produced:
        return binding.value

    with self._lock:
        if binding.singleton and binding.produced:
            return binding.value
        try:
            value = _invoke(binding.factory, self)
        except ConfigurationError:
            raise
        except Exception as exc:
            raise ConfigurationError(
                f"Factory for {_label(key)} raised {type(exc).__name__}: {exc}"
            ) from exc
        if binding.singleton:
            binding.value = value
            binding.produced = True
        return value

keys

keys() -> list[Any]

Return every key visible from here, including inherited ones.

Source code in src\windlass\core\container.py
def keys(self) -> list[Any]:
    """Return every key visible from here, including inherited ones."""
    seen: dict[Any, None] = {}
    node: Container | None = self
    while node is not None:
        for key in node._bindings:
            seen.setdefault(key, None)
        node = node._parent
    return list(seen)

component

component(kind: str, spec: Any = None, /, **config: Any) -> Any

Resolve a component from a name, an instance, or a factory.

This is the single funnel through which every builder turns user input into a live component, and the reason .llm(...) accepts three different kinds of argument.

Parameters:

Name Type Description Default
kind str

Component kind, e.g. "chunker".

required
spec Any

One of:

  • None — resolve the binding for kind if present, otherwise fail with a clear message.
  • str — a registry name; config is passed to the constructor.
  • an instance — used as-is (config must be empty).
  • a callable — invoked, optionally with the container.
None
**config Any

Constructor keyword arguments for the string form.

{}

Returns:

Type Description
Any

The live component.

Raises:

Type Description
ComponentNotFoundError

For an unknown registry name.

ConfigurationError

For an unusable spec.

Example

c = Container() llm = c.component("llm", "fake", responses=["hi"]) llm.complete("x").content 'hi'

Source code in src\windlass\core\container.py
def component(
    self,
    kind: str,
    spec: Any = None,
    /,
    **config: Any,
) -> Any:
    """Resolve a component from a name, an instance, or a factory.

    This is the single funnel through which every builder turns user input
    into a live component, and the reason ``.llm(...)`` accepts three
    different kinds of argument.

    Args:
        kind: Component kind, e.g. ``"chunker"``.
        spec: One of:

            * ``None`` — resolve the binding for ``kind`` if present,
              otherwise fail with a clear message.
            * ``str`` — a registry name; ``config`` is passed to the
              constructor.
            * an instance — used as-is (``config`` must be empty).
            * a callable — invoked, optionally with the container.
        **config: Constructor keyword arguments for the string form.

    Returns:
        The live component.

    Raises:
        ComponentNotFoundError: For an unknown registry name.
        ConfigurationError: For an unusable ``spec``.

    Example:
        >>> c = Container()
        >>> llm = c.component("llm", "fake", responses=["hi"])
        >>> llm.complete("x").content
        'hi'
    """
    if spec is None:
        if self.has(kind):
            return self.resolve(kind)
        raise ConfigurationError(
            f"No {kind} configured.",
            hint=f"Pass one explicitly, e.g. .{kind}('name'), "
            f"or bind it with container.bind('{kind}', factory).",
        )

    if isinstance(spec, str):
        return self._registry.create(kind, spec, **config)

    if callable(spec) and (inspect.isfunction(spec) or inspect.ismethod(spec)):
        produced = _invoke(spec, self)
        return produced

    if isinstance(spec, type):
        return spec(**config)

    if config:
        raise ConfigurationError(
            f"Cannot apply configuration to an already-constructed {kind}.",
            hint=f"Either pass .{kind}('name', **config) or configure the instance yourself.",
            context={"kind": kind, "config": sorted(config)},
        )
    return spec

bind_component

bind_component(
    kind: str, spec: Any = None, /, *, singleton: bool = True, **config: Any
) -> Container

Bind kind to a lazily resolved component.

The component is not constructed until something resolves it, which keeps builder chains cheap and lets configuration keep changing right up until the first call.

Parameters:

Name Type Description Default
kind str

Component kind, used as the binding key.

required
spec Any

Name, instance or factory — see :meth:component.

None
singleton bool

Reuse the constructed component.

True
**config Any

Constructor keyword arguments.

{}

Returns:

Type Description
Container

self.

Source code in src\windlass\core\container.py
def bind_component(
    self, kind: str, spec: Any = None, /, *, singleton: bool = True, **config: Any
) -> Container:
    """Bind ``kind`` to a lazily resolved component.

    The component is not constructed until something resolves it, which
    keeps builder chains cheap and lets configuration keep changing right up
    until the first call.

    Args:
        kind: Component kind, used as the binding key.
        spec: Name, instance or factory — see :meth:`component`.
        singleton: Reuse the constructed component.
        **config: Constructor keyword arguments.

    Returns:
        ``self``.
    """
    return self.bind(kind, lambda: self.component(kind, spec, **config), singleton=singleton)

scope

scope() -> Container

Return a child container inheriting every binding from this one.

Source code in src\windlass\core\container.py
def scope(self) -> Container:
    """Return a child container inheriting every binding from this one."""
    return Container(parent=self, registry=self._registry)

AgentError

AgentError(
    message: str, *, hint: str | None = None, context: dict[str, Any] | None = None
)

Bases: WindlassError

The agent runtime could not complete the request.

Source code in src\windlass\core\exceptions.py
def __init__(
    self,
    message: str,
    *,
    hint: str | None = None,
    context: dict[str, Any] | None = None,
) -> None:
    self.message = message
    self.hint = hint
    self.context: dict[str, Any] = dict(context or {})
    super().__init__(self._render())

AuthenticationError

AuthenticationError(
    message: str,
    *,
    provider: str | None = None,
    status_code: int | None = None,
    hint: str | None = None,
    context: dict[str, Any] | None = None,
    original: BaseException | None = None
)

Bases: ProviderError

Credentials are missing, expired, or rejected by the provider.

Source code in src\windlass\core\exceptions.py
def __init__(
    self,
    message: str,
    *,
    provider: str | None = None,
    status_code: int | None = None,
    hint: str | None = None,
    context: dict[str, Any] | None = None,
    original: BaseException | None = None,
) -> None:
    self.provider = provider
    self.status_code = status_code
    self.original = original
    ctx = {"provider": provider, **(context or {})}
    if status_code is not None:
        ctx.setdefault("status_code", status_code)
    super().__init__(message, hint=hint, context=ctx)

ComponentNotFoundError

ComponentNotFoundError(kind: str, name: str, available: list[str] | None = None)

Bases: RegistryError

No component is registered under the requested name.

The message lists the available names so typos are obvious.

Source code in src\windlass\core\exceptions.py
def __init__(self, kind: str, name: str, available: list[str] | None = None) -> None:
    self.kind = kind
    self.name = name
    self.available = sorted(available or [])
    options = ", ".join(self.available) if self.available else "(none registered)"
    super().__init__(
        f"No {kind} named {name!r} is registered.",
        hint=(
            f"Available {kind}s: {options}\n"
            f"Register your own with @windlass.register.{kind}('{name}')."
        ),
        context={"kind": kind, "name": name, "available": self.available},
    )

ConfigurationError

ConfigurationError(
    message: str, *, hint: str | None = None, context: dict[str, Any] | None = None
)

Bases: WindlassError

A component was configured with invalid or incomplete settings.

Source code in src\windlass\core\exceptions.py
def __init__(
    self,
    message: str,
    *,
    hint: str | None = None,
    context: dict[str, Any] | None = None,
) -> None:
    self.message = message
    self.hint = hint
    self.context: dict[str, Any] = dict(context or {})
    super().__init__(self._render())

DuplicateComponentError

DuplicateComponentError(kind: str, name: str)

Bases: RegistryError

A component name is already taken and override=False.

Source code in src\windlass\core\exceptions.py
def __init__(self, kind: str, name: str) -> None:
    super().__init__(
        f"A {kind} named {name!r} is already registered.",
        hint="Pass override=True to replace the existing component deliberately.",
        context={"kind": kind, "name": name},
    )

EvaluationError

EvaluationError(
    message: str, *, hint: str | None = None, context: dict[str, Any] | None = None
)

Bases: WindlassError

An evaluation run or metric computation failed.

Source code in src\windlass\core\exceptions.py
def __init__(
    self,
    message: str,
    *,
    hint: str | None = None,
    context: dict[str, Any] | None = None,
) -> None:
    self.message = message
    self.hint = hint
    self.context: dict[str, Any] = dict(context or {})
    super().__init__(self._render())

GuardrailViolation

GuardrailViolation(
    message: str,
    *,
    stage: str = "input",
    rule: str | None = None,
    detections: list[dict[str, Any]] | None = None
)

Bases: WindlassError

A guardrail blocked an input or an output.

Parameters:

Name Type Description Default
message str

What the guardrail objected to.

required
stage str

"input" or "output".

'input'
rule str | None

Identifier of the rule that fired.

None
detections list[dict[str, Any]] | None

Structured detail about each detection.

None
Source code in src\windlass\core\exceptions.py
def __init__(
    self,
    message: str,
    *,
    stage: str = "input",
    rule: str | None = None,
    detections: list[dict[str, Any]] | None = None,
) -> None:
    self.stage = stage
    self.rule = rule
    self.detections = detections or []
    super().__init__(
        message,
        hint="Relax the policy with .guardrails(on_violation='redact') "
        "to sanitise instead of block.",
        context={"stage": stage, "rule": rule, "detections": self.detections},
    )

IngestionError

IngestionError(
    message: str, *, hint: str | None = None, context: dict[str, Any] | None = None
)

Bases: PipelineError

A document could not be loaded, preprocessed, chunked or indexed.

Source code in src\windlass\core\exceptions.py
def __init__(
    self,
    message: str,
    *,
    hint: str | None = None,
    context: dict[str, Any] | None = None,
) -> None:
    self.message = message
    self.hint = hint
    self.context: dict[str, Any] = dict(context or {})
    super().__init__(self._render())

MaxIterationsExceeded

MaxIterationsExceeded(limit: int)

Bases: AgentError

The agent hit its reasoning-step budget without producing an answer.

Source code in src\windlass\core\exceptions.py
def __init__(self, limit: int) -> None:
    super().__init__(
        f"Agent exceeded its budget of {limit} reasoning steps without finishing.",
        hint="Raise the ceiling with .max_iterations(n) or simplify the task.",
        context={"limit": limit},
    )

MCPError

MCPError(
    message: str, *, hint: str | None = None, context: dict[str, Any] | None = None
)

Bases: WindlassError

An MCP server could not be reached or returned an error.

Source code in src\windlass\core\exceptions.py
def __init__(
    self,
    message: str,
    *,
    hint: str | None = None,
    context: dict[str, Any] | None = None,
) -> None:
    self.message = message
    self.hint = hint
    self.context: dict[str, Any] = dict(context or {})
    super().__init__(self._render())

MissingDependencyError

MissingDependencyError(
    feature: str,
    *,
    extra: str,
    package: str | None = None,
    original: BaseException | None = None
)

Bases: WindlassError

An optional dependency required by the requested feature is not installed.

Windlass keeps its core install tiny; every integration lives behind an optional dependency group. When a feature is used without its group being installed this error explains exactly which command fixes it.

Parameters:

Name Type Description Default
feature str

Human readable feature name, e.g. "The Pinecone vector store".

required
extra str

The extras group to install, e.g. "vectordb".

required
package str | None

The distribution that failed to import, for the context dict.

None
original BaseException | None

The underlying :class:ImportError, kept for debugging.

None
Example

raise MissingDependencyError("Evaluation", extra="evaluation") Traceback (most recent call last): windlass.core.exceptions.MissingDependencyError: Evaluation is not installed. Hint: Run: pip install "windlass[evaluation]"

Source code in src\windlass\core\exceptions.py
def __init__(
    self,
    feature: str,
    *,
    extra: str,
    package: str | None = None,
    original: BaseException | None = None,
) -> None:
    self.feature = feature
    self.extra = extra
    self.package = package
    self.original = original
    super().__init__(
        f"{feature} is not installed.",
        hint=f'Run:\n\n    pip install "windlass[{extra}]"',
        context={"feature": feature, "extra": extra, "package": package},
    )

PipelineError

PipelineError(
    message: str, *, hint: str | None = None, context: dict[str, Any] | None = None
)

Bases: WindlassError

A RAG or ingestion pipeline step failed.

Source code in src\windlass\core\exceptions.py
def __init__(
    self,
    message: str,
    *,
    hint: str | None = None,
    context: dict[str, Any] | None = None,
) -> None:
    self.message = message
    self.hint = hint
    self.context: dict[str, Any] = dict(context or {})
    super().__init__(self._render())

PluginError

PluginError(
    message: str, *, hint: str | None = None, context: dict[str, Any] | None = None
)

Bases: WindlassError

A third-party plugin failed to load or misbehaved during discovery.

Source code in src\windlass\core\exceptions.py
def __init__(
    self,
    message: str,
    *,
    hint: str | None = None,
    context: dict[str, Any] | None = None,
) -> None:
    self.message = message
    self.hint = hint
    self.context: dict[str, Any] = dict(context or {})
    super().__init__(self._render())

ProviderError

ProviderError(
    message: str,
    *,
    provider: str | None = None,
    status_code: int | None = None,
    hint: str | None = None,
    context: dict[str, Any] | None = None,
    original: BaseException | None = None
)

Bases: WindlassError

A backing provider (LLM, vector store, ...) returned an error.

Parameters:

Name Type Description Default
message str

What went wrong.

required
provider str | None

Which provider raised it.

None
status_code int | None

HTTP status, when the failure came from an HTTP call. Pass this whenever you have it. Retry classification reads it (see :func:windlass.core.retry.is_retryable), so an adapter that omits it turns a transient 429 or 503 — which one retry would have fixed — into a hard failure of the whole run.

None
hint str | None

Actionable remediation.

None
context dict[str, Any] | None

Structured detail.

None
original BaseException | None

The underlying exception, kept for debugging.

None

Attributes:

Name Type Description
provider

The provider name.

status_code

The HTTP status, when known.

original

The wrapped exception, when there was one.

Example

err = ProviderError("upstream boom", provider="acme", status_code=503) from windlass.core.retry import is_retryable is_retryable(err) True

Source code in src\windlass\core\exceptions.py
def __init__(
    self,
    message: str,
    *,
    provider: str | None = None,
    status_code: int | None = None,
    hint: str | None = None,
    context: dict[str, Any] | None = None,
    original: BaseException | None = None,
) -> None:
    self.provider = provider
    self.status_code = status_code
    self.original = original
    ctx = {"provider": provider, **(context or {})}
    if status_code is not None:
        ctx.setdefault("status_code", status_code)
    super().__init__(message, hint=hint, context=ctx)

ProviderTimeoutError

ProviderTimeoutError(
    message: str,
    *,
    provider: str | None = None,
    status_code: int | None = None,
    hint: str | None = None,
    context: dict[str, Any] | None = None,
    original: BaseException | None = None
)

Bases: ProviderError

The provider did not answer within the configured timeout.

Source code in src\windlass\core\exceptions.py
def __init__(
    self,
    message: str,
    *,
    provider: str | None = None,
    status_code: int | None = None,
    hint: str | None = None,
    context: dict[str, Any] | None = None,
    original: BaseException | None = None,
) -> None:
    self.provider = provider
    self.status_code = status_code
    self.original = original
    ctx = {"provider": provider, **(context or {})}
    if status_code is not None:
        ctx.setdefault("status_code", status_code)
    super().__init__(message, hint=hint, context=ctx)

RateLimitError

RateLimitError(message: str, *, retry_after: float | None = None, **kwargs: Any)

Bases: ProviderError

The provider applied rate limiting or quota exhaustion.

Attributes:

Name Type Description
retry_after

Seconds the provider asked us to wait, when advertised.

Source code in src\windlass\core\exceptions.py
def __init__(self, message: str, *, retry_after: float | None = None, **kwargs: Any) -> None:
    self.retry_after = retry_after
    kwargs.setdefault("context", {})["retry_after"] = retry_after
    super().__init__(message, **kwargs)

RegistryError

RegistryError(
    message: str, *, hint: str | None = None, context: dict[str, Any] | None = None
)

Bases: WindlassError

Base class for component-registry problems.

Source code in src\windlass\core\exceptions.py
def __init__(
    self,
    message: str,
    *,
    hint: str | None = None,
    context: dict[str, Any] | None = None,
) -> None:
    self.message = message
    self.hint = hint
    self.context: dict[str, Any] = dict(context or {})
    super().__init__(self._render())

ResponseError

ResponseError(
    message: str,
    *,
    provider: str | None = None,
    status_code: int | None = None,
    hint: str | None = None,
    context: dict[str, Any] | None = None,
    original: BaseException | None = None
)

Bases: ProviderError

The provider replied, but the payload could not be interpreted.

Source code in src\windlass\core\exceptions.py
def __init__(
    self,
    message: str,
    *,
    provider: str | None = None,
    status_code: int | None = None,
    hint: str | None = None,
    context: dict[str, Any] | None = None,
    original: BaseException | None = None,
) -> None:
    self.provider = provider
    self.status_code = status_code
    self.original = original
    ctx = {"provider": provider, **(context or {})}
    if status_code is not None:
        ctx.setdefault("status_code", status_code)
    super().__init__(message, hint=hint, context=ctx)

RetrievalError

RetrievalError(
    message: str, *, hint: str | None = None, context: dict[str, Any] | None = None
)

Bases: PipelineError

Retrieval failed or returned an unusable result.

Source code in src\windlass\core\exceptions.py
def __init__(
    self,
    message: str,
    *,
    hint: str | None = None,
    context: dict[str, Any] | None = None,
) -> None:
    self.message = message
    self.hint = hint
    self.context: dict[str, Any] = dict(context or {})
    super().__init__(self._render())

SerializationError

SerializationError(
    message: str, *, hint: str | None = None, context: dict[str, Any] | None = None
)

Bases: WindlassError

State could not be serialised to or from a checkpoint.

Source code in src\windlass\core\exceptions.py
def __init__(
    self,
    message: str,
    *,
    hint: str | None = None,
    context: dict[str, Any] | None = None,
) -> None:
    self.message = message
    self.hint = hint
    self.context: dict[str, Any] = dict(context or {})
    super().__init__(self._render())

ToolError

ToolError(
    message: str, *, hint: str | None = None, context: dict[str, Any] | None = None
)

Bases: WindlassError

Base class for tool related failures.

Source code in src\windlass\core\exceptions.py
def __init__(
    self,
    message: str,
    *,
    hint: str | None = None,
    context: dict[str, Any] | None = None,
) -> None:
    self.message = message
    self.hint = hint
    self.context: dict[str, Any] = dict(context or {})
    super().__init__(self._render())

ToolExecutionError

ToolExecutionError(name: str, original: BaseException)

Bases: ToolError

A tool raised while executing.

The original exception is preserved on :attr:original and as the __cause__ of this error.

Source code in src\windlass\core\exceptions.py
def __init__(self, name: str, original: BaseException) -> None:
    self.tool = name
    self.original = original
    super().__init__(
        f"Tool {name!r} raised {type(original).__name__}: {original}",
        context={"tool": name, "error_type": type(original).__name__},
    )

ToolNotFoundError

ToolNotFoundError(name: str, available: list[str] | None = None)

Bases: ToolError

The model asked to call a tool that is not registered.

Source code in src\windlass\core\exceptions.py
def __init__(self, name: str, available: list[str] | None = None) -> None:
    options = ", ".join(sorted(available or [])) or "(no tools bound)"
    super().__init__(
        f"The model requested an unknown tool {name!r}.",
        hint=f"Tools available to this agent: {options}",
        context={"tool": name, "available": sorted(available or [])},
    )

ValidationError

ValidationError(
    message: str, *, hint: str | None = None, context: dict[str, Any] | None = None
)

Bases: WindlassError

User supplied data failed validation before reaching a provider.

Source code in src\windlass\core\exceptions.py
def __init__(
    self,
    message: str,
    *,
    hint: str | None = None,
    context: dict[str, Any] | None = None,
) -> None:
    self.message = message
    self.hint = hint
    self.context: dict[str, Any] = dict(context or {})
    super().__init__(self._render())

WindlassError

WindlassError(
    message: str, *, hint: str | None = None, context: dict[str, Any] | None = None
)

Bases: Exception

Base class for every error raised by Windlass.

Parameters:

Name Type Description Default
message str

Human readable description of what went wrong.

required
hint str | None

Optional actionable remediation shown after the message.

None
context dict[str, Any] | None

Arbitrary structured detail (provider name, model, ids, ...). Useful for logging and for tests that assert on specifics.

None

Attributes:

Name Type Description
message

The raw message without the hint appended.

hint

The remediation hint, if any.

context dict[str, Any]

Structured detail about the failure.

Example

raise WindlassError("boom", hint="try again", context={"attempt": 2}) Traceback (most recent call last): windlass.core.exceptions.WindlassError: boom Hint: try again

Source code in src\windlass\core\exceptions.py
def __init__(
    self,
    message: str,
    *,
    hint: str | None = None,
    context: dict[str, Any] | None = None,
) -> None:
    self.message = message
    self.hint = hint
    self.context: dict[str, Any] = dict(context or {})
    super().__init__(self._render())

AgentInterrupt

AgentInterrupt(
    message: str = "Execution paused for human approval.",
    *,
    payload: Any = None,
    thread_id: str | None = None
)

Bases: AgentError

Execution paused for human review (human-in-the-loop interrupt).

Named with a trailing underscore to avoid shadowing the builtin InterruptedError. Exported as AgentInterrupt from :mod:windlass.

Attributes:

Name Type Description
payload

What the agent wants a human to approve or edit.

thread_id

Checkpoint thread that can be resumed.

Source code in src\windlass\core\exceptions.py
def __init__(
    self,
    message: str = "Execution paused for human approval.",
    *,
    payload: Any = None,
    thread_id: str | None = None,
) -> None:
    self.payload = payload
    self.thread_id = thread_id
    super().__init__(
        message,
        hint="Resume with agent.resume(thread_id=..., approved=True).",
        context={"thread_id": thread_id},
    )

WindlassMemoryError

WindlassMemoryError(
    message: str, *, hint: str | None = None, context: dict[str, Any] | None = None
)

Bases: WindlassError

A memory backend failed. Exported as WindlassMemoryError.

Source code in src\windlass\core\exceptions.py
def __init__(
    self,
    message: str,
    *,
    hint: str | None = None,
    context: dict[str, Any] | None = None,
) -> None:
    self.message = message
    self.hint = hint
    self.context: dict[str, Any] = dict(context or {})
    super().__init__(self._render())

ComponentSpec dataclass

ComponentSpec(
    kind: str,
    name: str,
    target: Any = None,
    path: str | None = None,
    extra: str | None = None,
    aliases: tuple[str, ...] = (),
    description: str = "",
    defaults: dict[str, Any] = dict(),
    origin: str = "user",
)

A registry entry: how to obtain one implementation of a component kind.

Exactly one of target or path is set. path entries are resolved on first use, which is what makes registration free of import cost.

Attributes:

Name Type Description
kind str

The component kind, e.g. "chunker".

name str

The lookup name, e.g. "semantic".

target Any

The resolved class or factory, once known.

path str | None

Dotted "module:Attribute" path for deferred resolution.

extra str | None

Extras group needed for this component, used in error messages.

aliases tuple[str, ...]

Additional names that resolve to this spec.

description str

One-line summary shown by windlass list.

defaults dict[str, Any]

Keyword arguments merged under user config at construction.

origin str

"builtin", "plugin" or "user". Purely informational.

resolve

resolve() -> Any

Import and cache the target if it was registered lazily.

Returns:

Type Description
Any

The class or factory for this component.

Raises:

Type Description
RegistryError

If the dotted path cannot be imported. Missing optional dependencies surface as :class:~windlass.core.exceptions.MissingDependencyError from inside the provider module instead.

Source code in src\windlass\core\registry.py
def resolve(self) -> Any:
    """Import and cache the target if it was registered lazily.

    Returns:
        The class or factory for this component.

    Raises:
        RegistryError: If the dotted path cannot be imported. Missing
            *optional dependencies* surface as
            :class:`~windlass.core.exceptions.MissingDependencyError` from
            inside the provider module instead.
    """
    if self.target is not None:
        return self.target
    if not self.path:  # pragma: no cover - guarded at registration
        raise RegistryError(f"Component {self.kind}:{self.name} has neither target nor path.")
    module_path, _, attr = self.path.partition(":")
    try:
        module = importlib.import_module(module_path)
    except Exception as exc:
        from windlass.core.exceptions import MissingDependencyError

        if isinstance(exc, MissingDependencyError):
            raise
        raise RegistryError(
            f"Failed to import {self.kind} {self.name!r} from {self.path!r}: {exc}",
            hint=(f'Try: pip install "windlass[{self.extra}]"' if self.extra else None),
            context={"kind": self.kind, "name": self.name, "path": self.path},
        ) from exc
    self.target = getattr(module, attr) if attr else module
    return self.target

Registry

Registry(*, discover: bool = False)

A thread-safe, kind-partitioned component registry.

You almost never construct this yourself — use the module-level :data:REGISTRY singleton, or the :data:register decorator namespace.

Parameters:

Name Type Description Default
discover bool

Whether the first lookup should scan installed distributions for entry-point plugins. The process-wide :data:REGISTRY does; a registry you construct yourself does not, so it stays an isolated container holding exactly what you put in it. Without that distinction, installing any third-party Windlass plugin would change the contents of every registry in the process, including the ones tests build to get a clean slate.

False
Example

reg = Registry() _ = reg.register("chunker", "shouty", lambda: None, description="demo") reg.has("chunker", "shouty") True reg.names("chunker") ['shouty']

Opt in explicitly when you do want the installed plugins:

discovered = Registry(discover=True) isinstance(discovered.load_plugins(), list) True

Source code in src\windlass\core\registry.py
def __init__(self, *, discover: bool = False) -> None:
    self._specs: dict[str, dict[str, ComponentSpec]] = {k: {} for k in COMPONENT_KINDS}
    self._aliases: dict[str, dict[str, str]] = {k: {} for k in COMPONENT_KINDS}
    self._lock = threading.RLock()
    self._discover = discover
    self._plugins_loaded = not discover

add_kind

add_kind(kind: str) -> None

Declare a brand new component kind at runtime.

Parameters:

Name Type Description Default
kind str

The new kind's name.

required
Source code in src\windlass\core\registry.py
def add_kind(self, kind: str) -> None:
    """Declare a brand new component kind at runtime.

    Args:
        kind: The new kind's name.
    """
    with self._lock:
        self._specs.setdefault(kind, {})
        self._aliases.setdefault(kind, {})

kinds

kinds() -> list[str]

Return every known component kind, sorted.

Source code in src\windlass\core\registry.py
def kinds(self) -> list[str]:
    """Return every known component kind, sorted."""
    return sorted(self._specs)

register

register(
    kind: str,
    name: str,
    target: Any,
    *,
    aliases: tuple[str, ...] | str = (),
    description: str = "",
    extra: str | None = None,
    defaults: dict[str, Any] | None = None,
    origin: str = "user",
    override: bool = False
) -> ComponentSpec

Register a concrete class or factory.

Parameters:

Name Type Description Default
kind str

Component kind.

required
name str

Lookup name; case-insensitive.

required
target Any

A class or a zero-or-more-argument factory callable.

required
aliases tuple[str, ...] | str

Extra names resolving to the same component.

()
description str

One-line summary for CLI listings and docs.

''
extra str | None

Extras group required, used in error hints.

None
defaults dict[str, Any] | None

Config defaults merged beneath user-supplied kwargs.

None
origin str

builtin / plugin / user.

'user'
override bool

Allow replacing an existing registration.

False

Returns:

Type Description
ComponentSpec

The stored :class:ComponentSpec.

Raises:

Type Description
DuplicateComponentError

If name is taken and override is False.

Source code in src\windlass\core\registry.py
def register(
    self,
    kind: str,
    name: str,
    target: Any,
    *,
    aliases: tuple[str, ...] | str = (),
    description: str = "",
    extra: str | None = None,
    defaults: dict[str, Any] | None = None,
    origin: str = "user",
    override: bool = False,
) -> ComponentSpec:
    """Register a concrete class or factory.

    Args:
        kind: Component kind.
        name: Lookup name; case-insensitive.
        target: A class or a zero-or-more-argument factory callable.
        aliases: Extra names resolving to the same component.
        description: One-line summary for CLI listings and docs.
        extra: Extras group required, used in error hints.
        defaults: Config defaults merged beneath user-supplied kwargs.
        origin: ``builtin`` / ``plugin`` / ``user``.
        override: Allow replacing an existing registration.

    Returns:
        The stored :class:`ComponentSpec`.

    Raises:
        DuplicateComponentError: If ``name`` is taken and ``override`` is False.
    """
    return self._store(
        ComponentSpec(
            kind=kind,
            name=name.lower(),
            target=target,
            aliases=(aliases,) if isinstance(aliases, str) else tuple(aliases),
            description=description,
            extra=extra,
            defaults=dict(defaults or {}),
            origin=origin,
        ),
        override=override,
    )

register_lazy

register_lazy(
    kind: str,
    name: str,
    path: str,
    *,
    aliases: tuple[str, ...] | str = (),
    description: str = "",
    extra: str | None = None,
    defaults: dict[str, Any] | None = None,
    origin: str = "builtin",
    override: bool = False
) -> ComponentSpec

Register a component by dotted path without importing it.

This is how every built-in provider is wired up: the module is only imported the first time somebody actually asks for that component.

Parameters:

Name Type Description Default
kind str

Component kind.

required
name str

Lookup name.

required
path str

"package.module:Attribute".

required
aliases tuple[str, ...] | str

Extra names resolving here.

()
description str

One-line summary.

''
extra str | None

Extras group required by the target module.

None
defaults dict[str, Any] | None

Config defaults.

None
origin str

Provenance label.

'builtin'
override bool

Allow replacing an existing registration.

False

Returns:

Type Description
ComponentSpec

The stored :class:ComponentSpec.

Source code in src\windlass\core\registry.py
def register_lazy(
    self,
    kind: str,
    name: str,
    path: str,
    *,
    aliases: tuple[str, ...] | str = (),
    description: str = "",
    extra: str | None = None,
    defaults: dict[str, Any] | None = None,
    origin: str = "builtin",
    override: bool = False,
) -> ComponentSpec:
    """Register a component by dotted path without importing it.

    This is how every built-in provider is wired up: the module is only
    imported the first time somebody actually asks for that component.

    Args:
        kind: Component kind.
        name: Lookup name.
        path: ``"package.module:Attribute"``.
        aliases: Extra names resolving here.
        description: One-line summary.
        extra: Extras group required by the target module.
        defaults: Config defaults.
        origin: Provenance label.
        override: Allow replacing an existing registration.

    Returns:
        The stored :class:`ComponentSpec`.
    """
    return self._store(
        ComponentSpec(
            kind=kind,
            name=name.lower(),
            path=path,
            aliases=(aliases,) if isinstance(aliases, str) else tuple(aliases),
            description=description,
            extra=extra,
            defaults=dict(defaults or {}),
            origin=origin,
        ),
        override=override,
    )

unregister

unregister(kind: str, name: str) -> None

Remove a component and its aliases. No-op when absent.

Source code in src\windlass\core\registry.py
def unregister(self, kind: str, name: str) -> None:
    """Remove a component and its aliases. No-op when absent."""
    with self._lock:
        bucket = self._bucket(kind)
        resolved = self._aliases[kind].pop(name.lower(), name.lower())
        spec = bucket.pop(resolved, None)
        if spec is not None:
            for alias in spec.aliases:
                self._aliases[kind].pop(alias.lower(), None)

get

get(kind: str, name: str) -> ComponentSpec

Return the spec registered under name.

Parameters:

Name Type Description Default
kind str

Component kind.

required
name str

Name or alias, case-insensitive.

required

Returns:

Type Description
ComponentSpec

The matching spec.

Raises:

Type Description
ComponentNotFoundError

If nothing matches. The error lists all available names for that kind.

Source code in src\windlass\core\registry.py
def get(self, kind: str, name: str) -> ComponentSpec:
    """Return the spec registered under ``name``.

    Args:
        kind: Component kind.
        name: Name or alias, case-insensitive.

    Returns:
        The matching spec.

    Raises:
        ComponentNotFoundError: If nothing matches. The error lists all
            available names for that kind.
    """
    self.ensure_plugins_loaded()
    with self._lock:
        bucket = self._bucket(kind)
        key = name.lower()
        key = self._aliases[kind].get(key, key)
        spec = bucket.get(key)
        if spec is None:
            raise ComponentNotFoundError(kind, name, list(bucket))
        return spec

has

has(kind: str, name: str) -> bool

Return whether name resolves for kind.

Source code in src\windlass\core\registry.py
def has(self, kind: str, name: str) -> bool:
    """Return whether ``name`` resolves for ``kind``."""
    try:
        self.get(kind, name)
    except (ComponentNotFoundError, RegistryError):
        return False
    return True

names

names(kind: str) -> list[str]

Return every registered name for kind, sorted.

Source code in src\windlass\core\registry.py
def names(self, kind: str) -> list[str]:
    """Return every registered name for ``kind``, sorted."""
    self.ensure_plugins_loaded()
    with self._lock:
        return sorted(self._bucket(kind))

specs

specs(kind: str) -> list[ComponentSpec]

Return every spec for kind, sorted by name.

Source code in src\windlass\core\registry.py
def specs(self, kind: str) -> list[ComponentSpec]:
    """Return every spec for ``kind``, sorted by name."""
    self.ensure_plugins_loaded()
    with self._lock:
        return [self._bucket(kind)[n] for n in sorted(self._bucket(kind))]

catalog

catalog() -> dict[str, list[ComponentSpec]]

Return every spec grouped by kind — powers windlass list.

Source code in src\windlass\core\registry.py
def catalog(self) -> dict[str, list[ComponentSpec]]:
    """Return every spec grouped by kind — powers ``windlass list``."""
    return {kind: self.specs(kind) for kind in self.kinds()}

create

create(kind: str, name: str, /, **config: Any) -> Any

Instantiate a registered component.

Registered defaults are merged underneath config, so caller arguments always win.

Parameters:

Name Type Description Default
kind str

Component kind.

required
name str

Name or alias.

required
**config Any

Constructor keyword arguments.

{}

Returns:

Type Description
Any

The constructed component.

Raises:

Type Description
ComponentNotFoundError

If the name is not registered.

ConfigurationError

If the constructor rejects the arguments.

Example

from windlass.core.registry import REGISTRY llm = REGISTRY.create("llm", "fake", responses=["hi"]) llm.complete("anything").content 'hi'

Source code in src\windlass\core\registry.py
def create(self, kind: str, name: str, /, **config: Any) -> Any:
    """Instantiate a registered component.

    Registered defaults are merged *underneath* ``config``, so caller
    arguments always win.

    Args:
        kind: Component kind.
        name: Name or alias.
        **config: Constructor keyword arguments.

    Returns:
        The constructed component.

    Raises:
        ComponentNotFoundError: If the name is not registered.
        ConfigurationError: If the constructor rejects the arguments.

    Example:
        >>> from windlass.core.registry import REGISTRY
        >>> llm = REGISTRY.create("llm", "fake", responses=["hi"])
        >>> llm.complete("anything").content
        'hi'
    """
    spec = self.get(kind, name)
    factory = spec.resolve()
    merged = {**spec.defaults, **config}
    try:
        return factory(**merged)
    except TypeError as exc:
        from windlass.core.exceptions import ConfigurationError

        raise ConfigurationError(
            f"Could not construct {kind} {name!r}: {exc}",
            hint=f"Check the accepted options for {getattr(factory, '__name__', factory)}.",
            context={"kind": kind, "name": name, "config": sorted(merged)},
        ) from exc

ensure_plugins_loaded

ensure_plugins_loaded() -> None

Discover entry-point plugins exactly once, on first lookup.

A no-op unless this registry was constructed with discover=True. :meth:load_plugins remains available on any registry for callers that want discovery on demand.

Source code in src\windlass\core\registry.py
def ensure_plugins_loaded(self) -> None:
    """Discover entry-point plugins exactly once, on first lookup.

    A no-op unless this registry was constructed with ``discover=True``.
    :meth:`load_plugins` remains available on any registry for callers that
    want discovery on demand.
    """
    if self._plugins_loaded:
        return
    with self._lock:
        if self._plugins_loaded:
            return
        self._plugins_loaded = True  # set first: discovery must not recurse
    self.load_plugins()

load_plugins

load_plugins(*, strict: bool = False) -> list[str]

Scan installed distributions for Windlass plugins.

Two entry-point conventions are supported:

  • windlass.plugins — the value is a callable taking the registry; use this to register many components or to do custom setup.
  • windlass.<kind> (e.g. windlass.chunker) — the value is the component class itself, registered under the entry-point name.

Parameters:

Name Type Description Default
strict bool

Raise on the first plugin that fails instead of warning.

False

Returns:

Type Description
list[str]

Names of the plugins that loaded successfully.

Raises:

Type Description
PluginError

Only when strict is True and a plugin fails.

Source code in src\windlass\core\registry.py
def load_plugins(self, *, strict: bool = False) -> list[str]:
    """Scan installed distributions for Windlass plugins.

    Two entry-point conventions are supported:

    * ``windlass.plugins`` — the value is a callable taking the registry;
      use this to register many components or to do custom setup.
    * ``windlass.<kind>`` (e.g. ``windlass.chunker``) — the value is the
      component class itself, registered under the entry-point name.

    Args:
        strict: Raise on the first plugin that fails instead of warning.

    Returns:
        Names of the plugins that loaded successfully.

    Raises:
        PluginError: Only when ``strict`` is True and a plugin fails.
    """
    from importlib.metadata import entry_points

    loaded: list[str] = []

    def _handle(err: Exception, label: str) -> None:
        if strict:
            raise PluginError(
                f"Plugin {label!r} failed to load: {err}", context={"plugin": label}
            ) from err
        _log.warning("Skipping Windlass plugin %r: %s", label, err)

    for ep in entry_points(group=PLUGIN_GROUP):
        try:
            hook = ep.load()
            hook(self)
            loaded.append(ep.name)
        except Exception as exc:
            _handle(exc, ep.name)

    for kind in self.kinds():
        for ep in entry_points(group=f"windlass.{kind}"):
            try:
                self.register_lazy(
                    kind,
                    ep.name,
                    ep.value.replace(":", ":", 1),
                    description=f"Plugin from {ep.value}",
                    origin="plugin",
                    override=True,
                )
                loaded.append(f"{kind}:{ep.name}")
            except Exception as exc:
                _handle(exc, f"{kind}:{ep.name}")

    return loaded

snapshot

snapshot() -> dict[str, dict[str, ComponentSpec]]

Return a shallow copy of the registry state (for tests).

Source code in src\windlass\core\registry.py
def snapshot(self) -> dict[str, dict[str, ComponentSpec]]:
    """Return a shallow copy of the registry state (for tests)."""
    with self._lock:
        return {kind: dict(bucket) for kind, bucket in self._specs.items()}

restore

restore(snapshot: dict[str, dict[str, ComponentSpec]]) -> None

Restore a previously captured :meth:snapshot (for tests).

Source code in src\windlass\core\registry.py
def restore(self, snapshot: dict[str, dict[str, ComponentSpec]]) -> None:
    """Restore a previously captured :meth:`snapshot` (for tests)."""
    with self._lock:
        self._specs = {kind: dict(bucket) for kind, bucket in snapshot.items()}

AgentResponse

Bases: WindlassModel

The final result of an agent run.

Attributes:

Name Type Description
output str

The agent's answer.

messages list[Message]

Full transcript including tool traffic.

steps list[AgentStep]

Structured per-iteration trace.

usage Usage

Aggregate token accounting across the whole run.

latency_ms float

End-to-end wall-clock time.

thread_id str | None

Checkpoint thread, when checkpointing is enabled.

interrupted bool

True when the run paused for human approval.

metadata dict[str, Any]

Runtime annotations.

tool_calls property

tool_calls: list[ToolCall]

Every tool call made during the run, in order.

AgentStep

Bases: WindlassModel

One iteration of an agent's reason/act loop.

Attributes:

Name Type Description
index int

Zero-based step number.

thought str

Assistant text emitted during this step, if any.

tool_calls list[ToolCall]

Tools the model requested.

tool_results list[ToolResult]

Results of executing them.

usage Usage

Tokens spent on this step.

node str

Name of the graph node that ran (multi-agent / LangGraph).

Chunk

Bases: WindlassModel

A retrievable slice of a :class:Document.

Attributes:

Name Type Description
id str

Stable identifier, derived from the document id and index.

content str

The chunk text as it will be embedded and shown to the model.

metadata dict[str, Any]

Document metadata plus chunk-level annotations.

document_id str

The document this came from.

index int

Zero-based position within the document.

embedding list[float] | None

Optional dense vector; populated during ingestion.

parent_id str | None

Set by hierarchical chunkers; points at the larger parent chunk that should be expanded at retrieval time.

start_char int

Character offset in the source document.

end_char int

Exclusive end offset in the source document.

with_embedding

with_embedding(vector: list[float]) -> Chunk

Return a copy carrying vector as its embedding.

Source code in src\windlass\core\types.py
def with_embedding(self, vector: list[float]) -> Chunk:
    """Return a copy carrying ``vector`` as its embedding."""
    return self.model_copy(update={"embedding": vector})

Completion

Bases: WindlassModel

A non-streaming model response.

Attributes:

Name Type Description
content str

The generated text.

tool_calls list[ToolCall]

Tools the model wants invoked, if any.

finish_reason FinishReason

Why generation stopped.

model str

Model identifier reported by the provider.

usage Usage

Token accounting.

raw Any

The untouched provider response object (Level 3 escape hatch).

latency_ms float

Wall-clock time for the call.

to_message

to_message() -> Message

Render this completion as an assistant :class:Message.

Source code in src\windlass\core\types.py
def to_message(self) -> Message:
    """Render this completion as an assistant :class:`Message`."""
    return Message(role=Role.ASSISTANT, content=self.content, tool_calls=self.tool_calls)

Document

Bases: WindlassModel

A source document before chunking.

Attributes:

Name Type Description
id str

Stable identifier. Defaults to a content hash so re-ingestion is idempotent.

content str

Extracted plain text.

metadata dict[str, Any]

Anything worth filtering or citing on later (page, author, section, url, ...). Kept alongside every chunk derived from it.

source str | None

Where the document came from (path, URL, ...).

mimetype str | None

Detected content type, when known.

created_at float

Unix timestamp of ingestion.

Embedding

Bases: WindlassModel

A dense vector plus the text it encodes.

Attributes:

Name Type Description
vector list[float]

The embedding values.

text str

The text that was embedded.

model str

Model identifier that produced the vector.

EvaluationReport

Bases: WindlassModel

Aggregate results of an evaluation run.

Attributes:

Name Type Description
results list[EvaluationResult]

Every individual metric/sample score.

summary dict[str, float]

Mean score per metric.

pass_rate float

Fraction of results that passed their threshold.

samples int

Number of samples evaluated.

EvaluationResult

Bases: WindlassModel

Score for a single metric on a single sample.

Attributes:

Name Type Description
metric str

Metric name, e.g. faithfulness.

score float

Normalised score, conventionally in [0, 1].

passed bool

Whether the score met the metric's threshold.

threshold float | None

The threshold that was applied.

reason str

Explanation, when the metric produces one.

sample_id str

Identifier of the evaluated sample.

FinishReason

Bases: StrEnum

Why the model stopped generating.

GuardrailResult

Bases: WindlassModel

Verdict from a guardrail check.

Attributes:

Name Type Description
allowed bool

False when the content must be blocked.

content str

Possibly rewritten content (redaction returns a sanitised copy).

detections list[dict[str, Any]]

One entry per rule that fired.

rule str | None

The first rule that blocked, if any.

stage str

input or output.

modified property

modified: bool

True when the guardrail rewrote the content.

Message

Bases: WindlassModel

One turn in a conversation.

Attributes:

Name Type Description
role Role

Author of the message.

content str

Text body. May be empty when the turn is purely tool calls.

tool_calls list[ToolCall]

Tools the assistant wants to invoke.

tool_call_id str | None

Set on tool messages to correlate with a call.

name str | None

Optional speaker name (multi-agent transcripts use this).

metadata dict[str, Any]

Free-form annotations that never reach the provider.

Example

Message.user("hello") Message(role=, content='hello', ...)

system classmethod

system(content: str, **kw: Any) -> Message

Build a system message.

Source code in src\windlass\core\types.py
@classmethod
def system(cls, content: str, **kw: Any) -> Message:
    """Build a system message."""
    return cls(role=Role.SYSTEM, content=content, **kw)

user classmethod

user(content: str, **kw: Any) -> Message

Build a user message.

Source code in src\windlass\core\types.py
@classmethod
def user(cls, content: str, **kw: Any) -> Message:
    """Build a user message."""
    return cls(role=Role.USER, content=content, **kw)

assistant classmethod

assistant(content: str = '', **kw: Any) -> Message

Build an assistant message.

Source code in src\windlass\core\types.py
@classmethod
def assistant(cls, content: str = "", **kw: Any) -> Message:
    """Build an assistant message."""
    return cls(role=Role.ASSISTANT, content=content, **kw)

tool classmethod

tool(result: ToolResult) -> Message

Build the tool message that answers a :class:ToolCall.

Source code in src\windlass\core\types.py
@classmethod
def tool(cls, result: ToolResult) -> Message:
    """Build the ``tool`` message that answers a :class:`ToolCall`."""
    return cls(
        role=Role.TOOL,
        content=result.content,
        tool_call_id=result.call_id,
        name=result.name,
        metadata={"is_error": result.is_error},
    )

RAGAnswer

Bases: WindlassModel

The answer produced by a RAG pipeline.

Attributes:

Name Type Description
answer str

The generated answer text.

question str

The question that was asked.

contexts list[ScoredChunk]

Chunks that were placed in the prompt, in prompt order.

usage Usage

Token accounting for the generation call.

latency_ms float

End-to-end wall-clock time including retrieval.

metadata dict[str, Any]

Pipeline annotations (guardrail decisions, rewrites, ...).

sources property

sources: list[str]

Unique, order-preserving list of the sources cited in the context.

Role

Bases: StrEnum

Who authored a :class:Message.

A :class:~enum.StrEnum, so members compare and format as their plain string value — Role.USER == "user" and f"{Role.USER}" both do what you would expect when the value reaches a provider's wire format.

ScoredChunk

Bases: WindlassModel

A chunk together with its retrieval score.

Attributes:

Name Type Description
chunk Chunk

The retrieved chunk.

score float

Relevance score. Higher is better; the scale depends on the retriever (cosine similarity, BM25, reciprocal rank fusion, ...).

rank int

One-based position in the result list.

retriever str

Which retriever produced this hit. Hybrid search keeps this so you can see which leg contributed each result.

content property

content: str

Shortcut to chunk.content.

metadata property

metadata: dict[str, Any]

Shortcut to chunk.metadata.

SearchResult

Bases: WindlassModel

The full result of a retrieval call.

Attributes:

Name Type Description
query str

The query that was executed (post rewriting, if any).

hits list[ScoredChunk]

Ranked chunks.

total int

Number of candidates considered before truncation.

latency_ms float

Wall-clock retrieval time.

texts

texts() -> list[str]

Return just the chunk texts, ranked.

Source code in src\windlass\core\types.py
def texts(self) -> list[str]:
    """Return just the chunk texts, ranked."""
    return [h.chunk.content for h in self.hits]

StreamEvent

Bases: WindlassModel

One incremental update from a streaming call.

Attributes:

Name Type Description
type Literal['text', 'tool_call', 'usage', 'done']

text for content deltas, tool_call when a call is complete, usage for accounting, done for the terminal event.

delta str

Incremental text for text events.

tool_call ToolCall | None

The completed call for tool_call events.

finish_reason FinishReason | None

Present on the done event.

usage Usage | None

Present on usage and done events when the provider reports it.

raw Any

Untouched provider chunk.

ToolCall

Bases: WindlassModel

A model's request to invoke a tool.

Attributes:

Name Type Description
id str

Provider-assigned correlation id; echoed back in the tool result.

name str

Registered tool name.

arguments dict[str, Any]

Parsed keyword arguments for the tool.

raw_arguments str | None

The original JSON string, kept when parsing failed.

ToolResult

Bases: WindlassModel

The outcome of executing a :class:ToolCall.

Attributes:

Name Type Description
call_id str

The :attr:ToolCall.id this answers.

name str

Tool that produced the result.

content str

String rendering handed back to the model.

data Any

The unserialised Python return value, for programmatic access.

is_error bool

True when the tool raised and the error text was returned.

duration_ms float

Wall-clock execution time.

Usage

Bases: WindlassModel

Token accounting for one or more model calls.

Attributes:

Name Type Description
prompt_tokens int

Tokens consumed by the prompt.

completion_tokens int

Tokens produced by the model.

total_tokens int

Sum of the two; computed when a provider omits it.

cached_tokens int

Prompt tokens served from the provider's prompt cache.

cost_usd float | None

Estimated spend, when the adapter can compute it.

calls int

How many provider round-trips this usage aggregates.

LLM

LLM(
    model: str = "",
    *,
    temperature: float | None = None,
    max_tokens: int | None = None,
    timeout: float | None = None,
    system_prompt: str | None = None,
    name: str | None = None,
    **config: Any
)

Bases: Component

Abstract chat-completion model.

Parameters:

Name Type Description Default
model str

Model identifier passed to the provider.

''
temperature float | None

Sampling temperature. Falls back to the global setting.

None
max_tokens int | None

Completion ceiling. None defers to the provider.

None
timeout float | None

Per-request timeout in seconds.

None
system_prompt str | None

Prepended to every call that does not already start with a system message.

None
name str | None

Component name for traces. Defaults to the provider name.

None
**config Any

Provider-specific options forwarded verbatim to the SDK.

{}

Attributes:

Name Type Description
model

The configured model identifier.

temperature float

The configured sampling temperature.

max_tokens int | None

The configured completion ceiling.

supports_tools bool

Whether this provider can do native tool calling.

supports_streaming bool

Whether this provider can stream.

Example

Implementing a provider takes one method::

class MyLLM(LLM):
    provider_name = "mine"

    async def agenerate(self, messages, *, tools=None, **kw):
        text = await my_sdk.chat(messages[-1].content)
        return Completion(content=text, model=self.model)
Source code in src\windlass\interfaces\llm.py
def __init__(
    self,
    model: str = "",
    *,
    temperature: float | None = None,
    max_tokens: int | None = None,
    timeout: float | None = None,
    system_prompt: str | None = None,
    name: str | None = None,
    **config: Any,
) -> None:
    cfg = settings()
    super().__init__(
        name=name or self.provider_name,
        model=model,
        temperature=cfg.temperature if temperature is None else temperature,
        max_tokens=cfg.max_tokens if max_tokens is None else max_tokens,
        timeout=cfg.request_timeout if timeout is None else timeout,
        system_prompt=system_prompt,
        **config,
    )
    self.model = model or self.default_model()
    self.temperature: float = self.config["temperature"]
    self.max_tokens: int | None = self.config["max_tokens"]
    self.timeout: float = self.config["timeout"]
    self.system_prompt: str | None = system_prompt

default_model classmethod

default_model() -> str

Return the model used when the caller does not name one.

Returns:

Type Description
str

A provider-appropriate default model identifier.

Source code in src\windlass\interfaces\llm.py
@classmethod
def default_model(cls) -> str:
    """Return the model used when the caller does not name one.

    Returns:
        A provider-appropriate default model identifier.
    """
    return ""

agenerate abstractmethod async

agenerate(
    messages: list[Message], *, tools: list[dict[str, Any]] | None = None, **kwargs: Any
) -> Completion

Produce one completion.

This is the only method a provider must implement.

Parameters:

Name Type Description Default
messages list[Message]

The conversation, already normalised and with the system prompt applied.

required
tools list[dict[str, Any]] | None

JSON-schema tool definitions in OpenAI function-calling format. Adapters translate these into their own shape.

None
**kwargs Any

Per-call overrides (temperature, max_tokens, response_format, ...).

{}

Returns:

Type Description
Completion

The completion, with raw set to the provider's response.

Raises:

Type Description
ProviderError

For any provider-side failure.

Source code in src\windlass\interfaces\llm.py
@abc.abstractmethod
async def agenerate(
    self,
    messages: list[Message],
    *,
    tools: list[dict[str, Any]] | None = None,
    **kwargs: Any,
) -> Completion:
    """Produce one completion.

    This is the only method a provider must implement.

    Args:
        messages: The conversation, already normalised and with the system
            prompt applied.
        tools: JSON-schema tool definitions in OpenAI function-calling
            format. Adapters translate these into their own shape.
        **kwargs: Per-call overrides (``temperature``, ``max_tokens``,
            ``response_format``, ...).

    Returns:
        The completion, with ``raw`` set to the provider's response.

    Raises:
        ProviderError: For any provider-side failure.
    """

astream_generate async

astream_generate(
    messages: list[Message], *, tools: list[dict[str, Any]] | None = None, **kwargs: Any
) -> AsyncIterator[StreamEvent]

Produce incremental completion events.

The default implementation calls :meth:agenerate and emits the result as a single text event followed by done — correct, but not actually incremental. Providers that support streaming should override this.

Parameters:

Name Type Description Default
messages list[Message]

The normalised conversation.

required
tools list[dict[str, Any]] | None

Tool definitions.

None
**kwargs Any

Per-call overrides.

{}

Yields:

Type Description
AsyncIterator[StreamEvent]

class:~windlass.core.types.StreamEvent values.

Source code in src\windlass\interfaces\llm.py
async def astream_generate(
    self,
    messages: list[Message],
    *,
    tools: list[dict[str, Any]] | None = None,
    **kwargs: Any,
) -> AsyncIterator[StreamEvent]:
    """Produce incremental completion events.

    The default implementation calls :meth:`agenerate` and emits the result
    as a single ``text`` event followed by ``done`` — correct, but not
    actually incremental. Providers that support streaming should override
    this.

    Args:
        messages: The normalised conversation.
        tools: Tool definitions.
        **kwargs: Per-call overrides.

    Yields:
        :class:`~windlass.core.types.StreamEvent` values.
    """
    completion = await self.agenerate(messages, tools=tools, **kwargs)
    if completion.content:
        yield StreamEvent(type="text", delta=completion.content)
    for call in completion.tool_calls:
        yield StreamEvent(type="tool_call", tool_call=call)
    yield StreamEvent(
        type="done", finish_reason=completion.finish_reason, usage=completion.usage
    )

acomplete async

acomplete(
    prompt: PromptLike,
    *,
    tools: list[dict[str, Any]] | None = None,
    retry: bool = True,
    **kwargs: Any
) -> Completion

Generate a completion, with retries and timing.

Parameters:

Name Type Description Default
prompt PromptLike

A string, a message, or a full transcript.

required
tools list[dict[str, Any]] | None

Tool definitions the model may call.

None
retry bool

Apply the configured retry policy to transient failures.

True
**kwargs Any

Per-call overrides forwarded to the provider.

{}

Returns:

Type Description
Completion

The completion. latency_ms is always populated.

Raises:

Type Description
ProviderError

When the provider fails and retries are exhausted.

AuthenticationError

When credentials are missing or rejected.

Performance

One network round trip. Set retry=False on latency-critical paths where you would rather fail fast than wait out a backoff.

Example

import asyncio from windlass.providers.llm.fake import FakeLLM asyncio.run(FakeLLM(responses=["hi"]).acomplete("hello")).content 'hi'

Source code in src\windlass\interfaces\llm.py
async def acomplete(
    self,
    prompt: PromptLike,
    *,
    tools: list[dict[str, Any]] | None = None,
    retry: bool = True,
    **kwargs: Any,
) -> Completion:
    """Generate a completion, with retries and timing.

    Args:
        prompt: A string, a message, or a full transcript.
        tools: Tool definitions the model may call.
        retry: Apply the configured retry policy to transient failures.
        **kwargs: Per-call overrides forwarded to the provider.

    Returns:
        The completion. ``latency_ms`` is always populated.

    Raises:
        ProviderError: When the provider fails and retries are exhausted.
        AuthenticationError: When credentials are missing or rejected.

    Performance:
        One network round trip. Set ``retry=False`` on latency-critical
        paths where you would rather fail fast than wait out a backoff.

    Example:
        >>> import asyncio
        >>> from windlass.providers.llm.fake import FakeLLM
        >>> asyncio.run(FakeLLM(responses=["hi"]).acomplete("hello")).content
        'hi'
    """
    messages = self._prepare(prompt)
    started = time.perf_counter()

    async def _call() -> Completion:
        return await self.agenerate(messages, tools=tools, **kwargs)

    completion = (
        await retry_async(_call, description=f"{self.name}.generate")
        if retry
        else await _call()
    )
    if not completion.latency_ms:
        completion.latency_ms = (time.perf_counter() - started) * 1000
    if not completion.model:
        completion.model = self.model
    return completion

complete

complete(
    prompt: PromptLike,
    *,
    tools: list[dict[str, Any]] | None = None,
    retry: bool = True,
    **kwargs: Any
) -> Completion

Blocking :meth:acomplete.

Safe to call from a notebook or from inside a running event loop; the call is dispatched to a background loop when necessary.

Parameters:

Name Type Description Default
prompt PromptLike

A string, a message, or a full transcript.

required
tools list[dict[str, Any]] | None

Tool definitions the model may call.

None
retry bool

Apply the configured retry policy.

True
**kwargs Any

Per-call overrides.

{}

Returns:

Type Description
Completion

The completion.

Source code in src\windlass\interfaces\llm.py
def complete(
    self,
    prompt: PromptLike,
    *,
    tools: list[dict[str, Any]] | None = None,
    retry: bool = True,
    **kwargs: Any,
) -> Completion:
    """Blocking :meth:`acomplete`.

    Safe to call from a notebook or from inside a running event loop; the
    call is dispatched to a background loop when necessary.

    Args:
        prompt: A string, a message, or a full transcript.
        tools: Tool definitions the model may call.
        retry: Apply the configured retry policy.
        **kwargs: Per-call overrides.

    Returns:
        The completion.
    """
    return run_sync(self.acomplete(prompt, tools=tools, retry=retry, **kwargs))

astream async

astream(
    prompt: PromptLike, *, tools: list[dict[str, Any]] | None = None, **kwargs: Any
) -> AsyncIterator[StreamEvent]

Stream a completion as it is generated.

Parameters:

Name Type Description Default
prompt PromptLike

A string, a message, or a full transcript.

required
tools list[dict[str, Any]] | None

Tool definitions.

None
**kwargs Any

Per-call overrides.

{}

Yields:

Type Description
AsyncIterator[StreamEvent]

Stream events. Text deltas arrive as they are produced; the final

AsyncIterator[StreamEvent]

event is always type="done".

Example

import asyncio from windlass.providers.llm.fake import FakeLLM async def main(): ... llm = FakeLLM(responses=["hey"]) ... return "".join([e.delta async for e in llm.astream("hi")]) asyncio.run(main()) 'hey'

Source code in src\windlass\interfaces\llm.py
async def astream(
    self,
    prompt: PromptLike,
    *,
    tools: list[dict[str, Any]] | None = None,
    **kwargs: Any,
) -> AsyncIterator[StreamEvent]:
    """Stream a completion as it is generated.

    Args:
        prompt: A string, a message, or a full transcript.
        tools: Tool definitions.
        **kwargs: Per-call overrides.

    Yields:
        Stream events. Text deltas arrive as they are produced; the final
        event is always ``type="done"``.

    Example:
        >>> import asyncio
        >>> from windlass.providers.llm.fake import FakeLLM
        >>> async def main():
        ...     llm = FakeLLM(responses=["hey"])
        ...     return "".join([e.delta async for e in llm.astream("hi")])
        >>> asyncio.run(main())
        'hey'
    """
    messages = self._prepare(prompt)
    async for event in self.astream_generate(messages, tools=tools, **kwargs):
        yield event

stream

stream(
    prompt: PromptLike, *, tools: list[dict[str, Any]] | None = None, **kwargs: Any
) -> Iterator[StreamEvent]

Blocking :meth:astream.

Parameters:

Name Type Description Default
prompt PromptLike

A string, a message, or a full transcript.

required
tools list[dict[str, Any]] | None

Tool definitions.

None
**kwargs Any

Per-call overrides.

{}

Yields:

Type Description
StreamEvent

Stream events, pulled one at a time so nothing is buffered ahead of

StreamEvent

the consumer.

Source code in src\windlass\interfaces\llm.py
def stream(
    self,
    prompt: PromptLike,
    *,
    tools: list[dict[str, Any]] | None = None,
    **kwargs: Any,
) -> Iterator[StreamEvent]:
    """Blocking :meth:`astream`.

    Args:
        prompt: A string, a message, or a full transcript.
        tools: Tool definitions.
        **kwargs: Per-call overrides.

    Yields:
        Stream events, pulled one at a time so nothing is buffered ahead of
        the consumer.
    """
    return iter_sync(self.astream(prompt, tools=tools, **kwargs))

astream_text async

astream_text(prompt: PromptLike, **kwargs: Any) -> AsyncIterator[str]

Stream only the text deltas — the common case for chat UIs.

Parameters:

Name Type Description Default
prompt PromptLike

A string, a message, or a full transcript.

required
**kwargs Any

Per-call overrides.

{}

Yields:

Type Description
AsyncIterator[str]

Non-empty text fragments.

Source code in src\windlass\interfaces\llm.py
async def astream_text(self, prompt: PromptLike, **kwargs: Any) -> AsyncIterator[str]:
    """Stream only the text deltas — the common case for chat UIs.

    Args:
        prompt: A string, a message, or a full transcript.
        **kwargs: Per-call overrides.

    Yields:
        Non-empty text fragments.
    """
    async for event in self.astream(prompt, **kwargs):
        if event.type == "text" and event.delta:
            yield event.delta

abatch async

abatch(
    prompts: Sequence[PromptLike], *, concurrency: int | None = None, **kwargs: Any
) -> list[Completion]

Complete many prompts with bounded concurrency.

Parameters:

Name Type Description Default
prompts Sequence[PromptLike]

The prompts to run.

required
concurrency int | None

Maximum simultaneous requests. Defaults to the global max_concurrency setting.

None
**kwargs Any

Per-call overrides applied to every prompt.

{}

Returns:

Type Description
list[Completion]

Completions in the same order as prompts.

Performance

Bounded on purpose — an unbounded fan-out over a few hundred prompts is the fastest way to get rate limited.

Example

import asyncio from windlass.providers.llm.fake import FakeLLM llm = FakeLLM(responses=["a", "b"]) [c.content for c in asyncio.run(llm.abatch(["1", "2"]))]['a', 'b']

Source code in src\windlass\interfaces\llm.py
async def abatch(
    self,
    prompts: Sequence[PromptLike],
    *,
    concurrency: int | None = None,
    **kwargs: Any,
) -> list[Completion]:
    """Complete many prompts with bounded concurrency.

    Args:
        prompts: The prompts to run.
        concurrency: Maximum simultaneous requests. Defaults to the global
            ``max_concurrency`` setting.
        **kwargs: Per-call overrides applied to every prompt.

    Returns:
        Completions in the same order as ``prompts``.

    Performance:
        Bounded on purpose — an unbounded fan-out over a few hundred prompts
        is the fastest way to get rate limited.

    Example:
        >>> import asyncio
        >>> from windlass.providers.llm.fake import FakeLLM
        >>> llm = FakeLLM(responses=["a", "b"])
        >>> [c.content for c in asyncio.run(llm.abatch(["1", "2"]))]
        ['a', 'b']
    """
    from windlass.core.concurrency import gather_bounded

    limit = concurrency or settings().max_concurrency
    return await gather_bounded([self.acomplete(p, **kwargs) for p in prompts], limit=limit)

batch

batch(
    prompts: Sequence[PromptLike], *, concurrency: int | None = None, **kwargs: Any
) -> list[Completion]

Blocking :meth:abatch.

Source code in src\windlass\interfaces\llm.py
def batch(
    self, prompts: Sequence[PromptLike], *, concurrency: int | None = None, **kwargs: Any
) -> list[Completion]:
    """Blocking :meth:`abatch`."""
    return run_sync(self.abatch(prompts, concurrency=concurrency, **kwargs))

Chunker

Chunker(
    *,
    chunk_size: int = 1000,
    overlap: int | None = None,
    min_chunk_size: int = 50,
    keep_separator: bool = True,
    name: str | None = None,
    **config: Any
)

Bases: Component

Abstract text splitter.

Parameters:

Name Type Description Default
chunk_size int

Target size of each chunk, measured in :attr:unit.

1000
overlap int | None

How much each chunk repeats from the previous one. Overlap keeps a fact that straddles a boundary retrievable from either side. None scales it to :data:DEFAULT_OVERLAP_RATIO of chunk_size, so shrinking chunk_size alone always produces a working chunker.

None
min_chunk_size int

Chunks shorter than this are merged into their neighbour rather than emitted — stray one-line fragments pollute retrieval.

50
keep_separator bool

Whether the split separator stays in the output.

True
name str | None

Component name for traces.

None
**config Any

Strategy-specific options.

{}

Attributes:

Name Type Description
unit str

"char" or "token" — how chunk_size is measured.

Raises:

Type Description
ValueError

If an explicit overlap is negative or not smaller than chunk_size. A derived default is never invalid.

Example

Implementing a chunker takes one method::

class LineChunker(Chunker):
    provider_name = "line"

    def split_text(self, text: str) -> list[str]:
        return [ln for ln in text.splitlines() if ln.strip()]
Source code in src\windlass\interfaces\chunker.py
def __init__(
    self,
    *,
    chunk_size: int = 1000,
    overlap: int | None = None,
    min_chunk_size: int = 50,
    keep_separator: bool = True,
    name: str | None = None,
    **config: Any,
) -> None:
    if chunk_size <= 0:
        raise ValueError("chunk_size must be positive")

    if overlap is None:
        # Scale with chunk_size. A fixed default would make the perfectly
        # reasonable `chunker(chunk_size=20)` raise, which is a poor trade:
        # the user changed one obvious knob and got an error about another
        # they never set.
        overlap = int(chunk_size * DEFAULT_OVERLAP_RATIO)
    elif overlap < 0:
        raise ValueError("overlap must not be negative")
    elif overlap >= chunk_size:
        raise ValueError(
            f"overlap ({overlap}) must be smaller than chunk_size ({chunk_size}); "
            "otherwise chunking cannot make progress."
        )
    super().__init__(
        name=name or self.provider_name,
        chunk_size=chunk_size,
        overlap=overlap,
        min_chunk_size=min_chunk_size,
        keep_separator=keep_separator,
        **config,
    )
    self.chunk_size = chunk_size
    self.overlap = overlap
    self.min_chunk_size = min_chunk_size
    self.keep_separator = keep_separator

split_text abstractmethod

split_text(text: str) -> list[str]

Split raw text into chunk strings.

The only method a strategy must implement. Return the pieces in reading order; the base class turns them into :class:Chunk objects with ids, offsets and inherited metadata.

Parameters:

Name Type Description Default
text str

The text to split.

required

Returns:

Type Description
list[str]

Chunk strings in document order.

Source code in src\windlass\interfaces\chunker.py
@abc.abstractmethod
def split_text(self, text: str) -> list[str]:
    """Split raw text into chunk strings.

    The only method a strategy must implement. Return the pieces in reading
    order; the base class turns them into :class:`Chunk` objects with ids,
    offsets and inherited metadata.

    Args:
        text: The text to split.

    Returns:
        Chunk strings in document order.
    """

asplit_text async

asplit_text(text: str) -> list[str]

Async :meth:split_text.

Override this instead when your strategy needs to await something — the semantic chunker calls an embedding model here.

Parameters:

Name Type Description Default
text str

The text to split.

required

Returns:

Type Description
list[str]

Chunk strings in document order.

Source code in src\windlass\interfaces\chunker.py
async def asplit_text(self, text: str) -> list[str]:
    """Async :meth:`split_text`.

    Override this instead when your strategy needs to await something — the
    semantic chunker calls an embedding model here.

    Args:
        text: The text to split.

    Returns:
        Chunk strings in document order.
    """
    return self.split_text(text)

achunk_document async

achunk_document(document: Document) -> list[Chunk]

Split one document into chunks.

Parameters:

Name Type Description Default
document Document

The document to split.

required

Returns:

Type Description
list[Chunk]

Chunks carrying the document's metadata plus chunk_index,

list[Chunk]

chunk_total and character offsets.

Source code in src\windlass\interfaces\chunker.py
async def achunk_document(self, document: Document) -> list[Chunk]:
    """Split one document into chunks.

    Args:
        document: The document to split.

    Returns:
        Chunks carrying the document's metadata plus ``chunk_index``,
        ``chunk_total`` and character offsets.
    """
    pieces = await self.asplit_text(document.content)
    pieces = self._merge_undersized(pieces)
    chunks: list[Chunk] = []
    cursor = 0
    for index, piece in enumerate(pieces):
        start = document.content.find(piece, cursor)
        if start < 0:  # strategy rewrote the text (e.g. added a heading path)
            start = cursor
        else:
            cursor = start + len(piece)
        chunks.append(
            Chunk(
                content=piece,
                document_id=document.id,
                index=index,
                start_char=start,
                end_char=start + len(piece),
                metadata={
                    **document.metadata,
                    "document_id": document.id,
                    "chunk_index": index,
                    "chunk_total": len(pieces),
                    "chunker": self.name,
                    **({"source": document.source} if document.source else {}),
                },
            )
        )
    return chunks

achunk async

achunk(
    documents: Sequence[Document] | Document, *, concurrency: int | None = None
) -> list[Chunk]

Split many documents concurrently.

Parameters:

Name Type Description Default
documents Sequence[Document] | Document

One document or a sequence of them.

required
concurrency int | None

Maximum simultaneous splits. Defaults to the global max_concurrency setting.

None

Returns:

Type Description
list[Chunk]

All chunks, grouped by source document in input order.

Performance

CPU-bound strategies see little benefit from concurrency; strategies that await a model (semantic, contextual) see a lot.

Source code in src\windlass\interfaces\chunker.py
async def achunk(
    self, documents: Sequence[Document] | Document, *, concurrency: int | None = None
) -> list[Chunk]:
    """Split many documents concurrently.

    Args:
        documents: One document or a sequence of them.
        concurrency: Maximum simultaneous splits. Defaults to the global
            ``max_concurrency`` setting.

    Returns:
        All chunks, grouped by source document in input order.

    Performance:
        CPU-bound strategies see little benefit from concurrency; strategies
        that await a model (``semantic``, ``contextual``) see a lot.
    """
    items = [documents] if isinstance(documents, Document) else list(documents)
    if not items:
        return []
    limit = concurrency or settings().max_concurrency
    grouped = await gather_bounded([self.achunk_document(doc) for doc in items], limit=limit)
    return [chunk for group in grouped for chunk in group]

chunk

chunk(documents: Sequence[Document] | Document) -> list[Chunk]

Blocking :meth:achunk.

Source code in src\windlass\interfaces\chunker.py
def chunk(self, documents: Sequence[Document] | Document) -> list[Chunk]:
    """Blocking :meth:`achunk`."""
    return run_sync(self.achunk(documents))

chunk_text

chunk_text(text: str, *, metadata: dict[str, Any] | None = None) -> list[Chunk]

Split a bare string, without constructing a Document first.

Parameters:

Name Type Description Default
text str

The text to split.

required
metadata dict[str, Any] | None

Metadata copied onto every chunk.

None

Returns:

Type Description
list[Chunk]

The resulting chunks.

Example

from windlass.providers.chunkers.recursive import RecursiveChunker RecursiveChunker(chunk_size=100, overlap=10).chunk_text("hello")[0].content 'hello'

Source code in src\windlass\interfaces\chunker.py
def chunk_text(self, text: str, *, metadata: dict[str, Any] | None = None) -> list[Chunk]:
    """Split a bare string, without constructing a Document first.

    Args:
        text: The text to split.
        metadata: Metadata copied onto every chunk.

    Returns:
        The resulting chunks.

    Example:
        >>> from windlass.providers.chunkers.recursive import RecursiveChunker
        >>> RecursiveChunker(chunk_size=100, overlap=10).chunk_text("hello")[0].content
        'hello'
    """
    return run_sync(self.achunk(Document(content=text, metadata=dict(metadata or {}))))

Component

Component(*, name: str | None = None, **config: Any)

Bases: SupportsNative

Abstract base for every registrable Windlass component.

Parameters:

Name Type Description Default
name str | None

Identifier used in traces and error messages. Defaults to the class's registered name, or its class name.

None
**config Any

Arbitrary component configuration, kept on :attr:config.

{}

Attributes:

Name Type Description
name

The component's identifier.

config dict[str, Any]

The configuration it was constructed with.

Example

class Upper(Component): ... def apply(self, text: str) -> str: ... return text.upper() Upper(name="upper").name 'upper'

Source code in src\windlass\interfaces\base.py
def __init__(self, *, name: str | None = None, **config: Any) -> None:
    self.name = name or getattr(type(self), "provider_name", None) or type(self).__name__
    self.config: dict[str, Any] = dict(config)
    self._log = get_logger(f"{self.kind}.{self.name}")

option

option(key: str, default: Any = None) -> Any

Read a configuration value.

Parameters:

Name Type Description Default
key str

Configuration key.

required
default Any

Returned when the key is absent.

None

Returns:

Type Description
Any

The configured value or default.

Source code in src\windlass\interfaces\base.py
def option(self, key: str, default: Any = None) -> Any:
    """Read a configuration value.

    Args:
        key: Configuration key.
        default: Returned when the key is absent.

    Returns:
        The configured value or ``default``.
    """
    return self.config.get(key, default)

require_option

require_option(key: str) -> Any

Read a configuration value that must be present.

Parameters:

Name Type Description Default
key str

Configuration key.

required

Returns:

Type Description
Any

The configured value.

Raises:

Type Description
ConfigurationError

When the key is missing or None.

Source code in src\windlass\interfaces\base.py
def require_option(self, key: str) -> Any:
    """Read a configuration value that must be present.

    Args:
        key: Configuration key.

    Returns:
        The configured value.

    Raises:
        ConfigurationError: When the key is missing or ``None``.
    """
    value = self.config.get(key)
    if value is None:
        raise ConfigurationError(
            f"{type(self).__name__} requires the {key!r} option.",
            hint=f"Pass {key}=... when constructing the {self.kind}.",
            context={"component": self.name, "kind": self.kind, "option": key},
        )
    return value

with_config

with_config(**overrides: Any) -> Component

Return a new component of the same type with merged configuration.

Components are treated as immutable once built; reconfiguring produces a copy so a shared component cannot be mutated out from under another pipeline.

Parameters:

Name Type Description Default
**overrides Any

Configuration to merge over the current values.

{}

Returns:

Type Description
Component

A new instance of the same class.

Source code in src\windlass\interfaces\base.py
def with_config(self, **overrides: Any) -> Component:
    """Return a new component of the same type with merged configuration.

    Components are treated as immutable once built; reconfiguring produces a
    copy so a shared component cannot be mutated out from under another
    pipeline.

    Args:
        **overrides: Configuration to merge over the current values.

    Returns:
        A new instance of the same class.
    """
    merged = {**self.config, **overrides}
    return type(self)(name=self.name, **merged)

aclose async

aclose() -> None

Release resources held by the component.

The default is a no-op. Adapters holding sockets, file handles or thread pools should override it. Safe to call more than once.

Source code in src\windlass\interfaces\base.py
async def aclose(self) -> None:
    """Release resources held by the component.

    The default is a no-op. Adapters holding sockets, file handles or thread
    pools should override it. Safe to call more than once.
    """

close

close() -> None

Blocking counterpart of :meth:aclose.

Source code in src\windlass\interfaces\base.py
def close(self) -> None:
    """Blocking counterpart of :meth:`aclose`."""
    from windlass.core.concurrency import run_sync

    run_sync(self.aclose())

native

native() -> Any

Return the wrapped provider object.

The default returns self, which is correct for pure-Python components that wrap nothing.

Source code in src\windlass\interfaces\base.py
def native(self) -> Any:
    """Return the wrapped provider object.

    The default returns ``self``, which is correct for pure-Python
    components that wrap nothing.
    """
    return self

describe

describe() -> dict[str, Any]

Return a JSON-safe summary of this component.

Used by tracing, by Pipeline.describe() and by the CLI.

Returns:

Type Description
dict[str, Any]

A dict with kind, name, class and config.

Source code in src\windlass\interfaces\base.py
def describe(self) -> dict[str, Any]:
    """Return a JSON-safe summary of this component.

    Used by tracing, by ``Pipeline.describe()`` and by the CLI.

    Returns:
        A dict with ``kind``, ``name``, ``class`` and ``config``.
    """
    return {
        "kind": self.kind,
        "name": self.name,
        "class": f"{type(self).__module__}.{type(self).__qualname__}",
        "config": {k: _safe(k, v) for k, v in self.config.items()},
    }

Embedder

Embedder(
    model: str = "",
    *,
    dimensions: int | None = None,
    batch_size: int | None = None,
    normalize: bool = True,
    cache: Cache | None = None,
    name: str | None = None,
    **config: Any
)

Bases: Component

Abstract text-embedding model.

Parameters:

Name Type Description Default
model str

Model identifier passed to the provider.

''
dimensions int | None

Output dimensionality. Providers that expose a native value should override :meth:dimension.

None
batch_size int | None

How many texts to send per provider request.

None
normalize bool

L2-normalise every vector. Recommended: it makes cosine similarity a plain dot product and matches what FAISS expects.

True
cache Cache | None

Optional cache for embeddings. Embeddings are deterministic, so caching them is always safe.

None
name str | None

Component name for traces.

None
**config Any

Provider-specific options.

{}

Attributes:

Name Type Description
model

Configured model identifier.

batch_size int

Configured request batch size.

normalize bool

Whether vectors are normalised on the way out.

Example

Implementing a provider takes one method::

class MyEmbedder(Embedder):
    provider_name = "mine"

    async def aembed_texts(self, texts, *, kind="document"):
        return [await my_sdk.embed(t) for t in texts]
Source code in src\windlass\interfaces\embedding.py
def __init__(
    self,
    model: str = "",
    *,
    dimensions: int | None = None,
    batch_size: int | None = None,
    normalize: bool = True,
    cache: Cache | None = None,
    name: str | None = None,
    **config: Any,
) -> None:
    super().__init__(
        name=name or self.provider_name,
        model=model,
        dimensions=dimensions,
        batch_size=batch_size or settings().batch_size,
        normalize=normalize,
        **config,
    )
    self.model = model or self.default_model()
    self.batch_size: int = self.config["batch_size"]
    self.normalize: bool = normalize
    self._dimensions = dimensions
    # `cache or NullCache()` looks equivalent and is not: every Cache
    # implements __len__, so a freshly constructed (empty) cache is falsy
    # and would be silently discarded — caching would appear to be
    # configured and never happen. Test for None explicitly.
    self._cache: Cache = NullCache() if cache is None else cache

default_model classmethod

default_model() -> str

Return the model used when the caller does not name one.

Source code in src\windlass\interfaces\embedding.py
@classmethod
def default_model(cls) -> str:
    """Return the model used when the caller does not name one."""
    return ""

aembed_texts abstractmethod async

aembed_texts(texts: list[str], *, kind: str = 'document') -> list[list[float]]

Embed a batch of texts.

This is the only method a provider must implement. It receives at most :attr:batch_size texts, already prefixed for kind.

Parameters:

Name Type Description Default
texts list[str]

The texts to embed. Never empty.

required
kind str

"document" or "query".

'document'

Returns:

Type Description
list[list[float]]

One vector per input, in the same order.

Raises:

Type Description
ProviderError

For any provider-side failure.

Source code in src\windlass\interfaces\embedding.py
@abc.abstractmethod
async def aembed_texts(self, texts: list[str], *, kind: str = "document") -> list[list[float]]:
    """Embed a batch of texts.

    This is the only method a provider must implement. It receives at most
    :attr:`batch_size` texts, already prefixed for ``kind``.

    Args:
        texts: The texts to embed. Never empty.
        kind: ``"document"`` or ``"query"``.

    Returns:
        One vector per input, in the same order.

    Raises:
        ProviderError: For any provider-side failure.
    """

dimension

dimension() -> int

Return the vector dimensionality.

Determined from configuration when possible, otherwise by embedding a one-character probe and measuring the result. The probe runs at most once per instance.

Returns:

Type Description
int

The number of components in each vector.

Example

from windlass.providers.embeddings.hash import HashEmbedder HashEmbedder(dimensions=64).dimension() 64

Source code in src\windlass\interfaces\embedding.py
def dimension(self) -> int:
    """Return the vector dimensionality.

    Determined from configuration when possible, otherwise by embedding a
    one-character probe and measuring the result. The probe runs at most
    once per instance.

    Returns:
        The number of components in each vector.

    Example:
        >>> from windlass.providers.embeddings.hash import HashEmbedder
        >>> HashEmbedder(dimensions=64).dimension()
        64
    """
    if self._dimensions is None:
        self._dimensions = len(self.embed_one("dimension probe"))
    return self._dimensions

aembed async

aembed(
    texts: Sequence[str], *, kind: str = "document", concurrency: int | None = None
) -> list[list[float]]

Embed many texts with batching, bounded concurrency and caching.

Parameters:

Name Type Description Default
texts Sequence[str]

The texts to embed.

required
kind str

"document" for corpus text, "query" for search input.

'document'
concurrency int | None

Maximum simultaneous provider requests. Defaults to the global max_concurrency setting.

None

Returns:

Type Description
list[list[float]]

One vector per input, in input order.

Raises:

Type Description
ProviderError

For any provider-side failure.

ValueError

If kind is not document or query.

Performance

Texts are grouped into :attr:batch_size batches and the batches run concurrently up to concurrency. Cached texts never reach the provider, so re-ingesting a mostly-unchanged corpus is cheap.

Example

import asyncio from windlass.providers.embeddings.hash import HashEmbedder len(asyncio.run(HashEmbedder(dimensions=8).aembed(["a", "b"]))) 2

Source code in src\windlass\interfaces\embedding.py
async def aembed(
    self,
    texts: Sequence[str],
    *,
    kind: str = "document",
    concurrency: int | None = None,
) -> list[list[float]]:
    """Embed many texts with batching, bounded concurrency and caching.

    Args:
        texts: The texts to embed.
        kind: ``"document"`` for corpus text, ``"query"`` for search input.
        concurrency: Maximum simultaneous provider requests. Defaults to the
            global ``max_concurrency`` setting.

    Returns:
        One vector per input, in input order.

    Raises:
        ProviderError: For any provider-side failure.
        ValueError: If ``kind`` is not ``document`` or ``query``.

    Performance:
        Texts are grouped into :attr:`batch_size` batches and the batches
        run concurrently up to ``concurrency``. Cached texts never reach the
        provider, so re-ingesting a mostly-unchanged corpus is cheap.

    Example:
        >>> import asyncio
        >>> from windlass.providers.embeddings.hash import HashEmbedder
        >>> len(asyncio.run(HashEmbedder(dimensions=8).aembed(["a", "b"])))
        2
    """
    if kind not in {"document", "query"}:
        raise ValueError(f"kind must be 'document' or 'query', got {kind!r}")
    if not texts:
        return []

    prefix = self.query_prefix if kind == "query" else self.document_prefix
    prepared = [f"{prefix}{t}" if prefix else t for t in texts]

    # Split into cache hits and the misses we actually have to compute.
    results: list[list[float] | None] = [None] * len(prepared)
    pending: list[tuple[int, str]] = []
    for i, text in enumerate(prepared):
        hit = await self._cache.aget(self._key(text, kind))
        if hit is not None:
            results[i] = hit
        else:
            pending.append((i, text))

    if pending:
        batches = list(batched(pending, self.batch_size))
        limit = concurrency or settings().max_concurrency
        computed = await gather_bounded(
            [self._embed_batch([t for _, t in batch], kind) for batch in batches],
            limit=limit,
        )
        for batch, vectors in zip(batches, computed, strict=True):
            for (index, text), vector in zip(batch, vectors, strict=True):
                final = normalize(vector) if self.normalize else list(vector)
                results[index] = final
                await self._cache.aset(self._key(text, kind), final)

    return [r for r in results if r is not None]

embed

embed(texts: Sequence[str], *, kind: str = 'document') -> list[list[float]]

Blocking :meth:aembed.

Source code in src\windlass\interfaces\embedding.py
def embed(self, texts: Sequence[str], *, kind: str = "document") -> list[list[float]]:
    """Blocking :meth:`aembed`."""
    return run_sync(self.aembed(texts, kind=kind))

aembed_one async

aembed_one(text: str, *, kind: str = 'document') -> list[float]

Embed a single text.

Parameters:

Name Type Description Default
text str

The text to embed.

required
kind str

"document" or "query".

'document'

Returns:

Type Description
list[float]

One vector.

Source code in src\windlass\interfaces\embedding.py
async def aembed_one(self, text: str, *, kind: str = "document") -> list[float]:
    """Embed a single text.

    Args:
        text: The text to embed.
        kind: ``"document"`` or ``"query"``.

    Returns:
        One vector.
    """
    vectors = await self.aembed([text], kind=kind)
    return vectors[0]

embed_one

embed_one(text: str, *, kind: str = 'document') -> list[float]

Blocking :meth:aembed_one.

Source code in src\windlass\interfaces\embedding.py
def embed_one(self, text: str, *, kind: str = "document") -> list[float]:
    """Blocking :meth:`aembed_one`."""
    return run_sync(self.aembed_one(text, kind=kind))

aembed_query async

aembed_query(text: str) -> list[float]

Embed a search query, applying any query instruction prefix.

Source code in src\windlass\interfaces\embedding.py
async def aembed_query(self, text: str) -> list[float]:
    """Embed a search query, applying any query instruction prefix."""
    return await self.aembed_one(text, kind="query")

embed_query

embed_query(text: str) -> list[float]

Blocking :meth:aembed_query.

Source code in src\windlass\interfaces\embedding.py
def embed_query(self, text: str) -> list[float]:
    """Blocking :meth:`aembed_query`."""
    return run_sync(self.aembed_query(text))

set_cache

set_cache(cache: Cache) -> None

Attach a cache to this embedder.

Parameters:

Name Type Description Default
cache Cache

Any :class:~windlass.core.cache.Cache implementation.

required
Source code in src\windlass\interfaces\embedding.py
def set_cache(self, cache: Cache) -> None:
    """Attach a cache to this embedder.

    Args:
        cache: Any :class:`~windlass.core.cache.Cache` implementation.
    """
    self._cache = cache

EvalSample

Bases: WindlassModel

One evaluated interaction.

Attributes:

Name Type Description
id str

Sample identifier, used to correlate results.

question str

The user's input.

answer str

What the system produced.

contexts list[str]

Retrieved texts that were placed in the prompt. Required by faithfulness and context-precision metrics.

reference str

The ground-truth answer, when you have one.

reference_contexts list[str]

The ideal contexts, for retrieval recall metrics.

metadata dict[str, Any]

Anything else worth slicing results by (tenant, model, ...).

Example

EvalSample(question="q", answer="a").id != "" True

from_answer classmethod

from_answer(answer: RAGAnswer, *, reference: str = '') -> EvalSample

Build a sample straight from a RAG answer.

This is the ergonomic path: run your pipeline, feed the answers to the evaluator, done.

Parameters:

Name Type Description Default
answer RAGAnswer

What :meth:~windlass.rag.pipeline.RAGPipeline.ask returned.

required
reference str

Ground truth, when available.

''

Returns:

Type Description
EvalSample

A populated sample.

Example

from windlass.core.types import RAGAnswer EvalSample.from_answer(RAGAnswer(answer="a", question="q")).question 'q'

Source code in src\windlass\interfaces\evaluator.py
@classmethod
def from_answer(cls, answer: RAGAnswer, *, reference: str = "") -> EvalSample:
    """Build a sample straight from a RAG answer.

    This is the ergonomic path: run your pipeline, feed the answers to the
    evaluator, done.

    Args:
        answer: What :meth:`~windlass.rag.pipeline.RAGPipeline.ask` returned.
        reference: Ground truth, when available.

    Returns:
        A populated sample.

    Example:
        >>> from windlass.core.types import RAGAnswer
        >>> EvalSample.from_answer(RAGAnswer(answer="a", question="q")).question
        'q'
    """
    return cls(
        question=answer.question,
        answer=answer.answer,
        contexts=[hit.chunk.content for hit in answer.contexts],
        reference=reference,
        metadata=dict(answer.metadata),
    )

Evaluator

Evaluator(
    *,
    metrics: Sequence[str] | None = None,
    threshold: float = 0.5,
    llm: Any = None,
    concurrency: int | None = None,
    name: str | None = None,
    **config: Any
)

Bases: Component

Abstract evaluation backend.

Parameters:

Name Type Description Default
metrics Sequence[str] | None

Metric names to compute. Which names are valid depends on the backend; ask it with :meth:available_metrics.

None
threshold float

Score at or above which a result counts as passing.

0.5
llm Any

Judge model for metrics that need one. Many quality metrics are themselves LLM calls.

None
concurrency int | None

Maximum simultaneous sample evaluations.

None
name str | None

Component name for traces.

None
**config Any

Backend-specific options.

{}

Attributes:

Name Type Description
metrics list[str]

The configured metric names.

threshold

The configured pass threshold.

Example

Implementing an evaluator takes one method::

class LengthEvaluator(Evaluator):
    provider_name = "length"

    async def aevaluate_sample(self, sample):
        score = min(1.0, len(sample.answer) / 100)
        return [EvaluationResult(metric="length", score=score)]
Source code in src\windlass\interfaces\evaluator.py
def __init__(
    self,
    *,
    metrics: Sequence[str] | None = None,
    threshold: float = 0.5,
    llm: Any = None,
    concurrency: int | None = None,
    name: str | None = None,
    **config: Any,
) -> None:
    super().__init__(
        name=name or self.provider_name,
        metrics=list(metrics or self.default_metrics()),
        threshold=threshold,
        **config,
    )
    self.metrics: list[str] = list(metrics or self.default_metrics())
    self.threshold = threshold
    self.llm = llm
    self.concurrency = concurrency or settings().max_concurrency

default_metrics classmethod

default_metrics() -> tuple[str, ...]

Return the metrics used when the caller does not choose any.

Source code in src\windlass\interfaces\evaluator.py
@classmethod
def default_metrics(cls) -> tuple[str, ...]:
    """Return the metrics used when the caller does not choose any."""
    return ()

available_metrics classmethod

available_metrics() -> tuple[str, ...]

Return every metric name this backend understands.

Source code in src\windlass\interfaces\evaluator.py
@classmethod
def available_metrics(cls) -> tuple[str, ...]:
    """Return every metric name this backend understands."""
    return cls.default_metrics()

aevaluate_sample abstractmethod async

aevaluate_sample(sample: EvalSample) -> list[EvaluationResult]

Score one sample against every configured metric.

Parameters:

Name Type Description Default
sample EvalSample

The interaction to score.

required

Returns:

Name Type Description
One list[EvaluationResult]

class:~windlass.core.types.EvaluationResult per metric.

Raises:

Type Description
EvaluationError

When a metric cannot be computed.

Source code in src\windlass\interfaces\evaluator.py
@abc.abstractmethod
async def aevaluate_sample(self, sample: EvalSample) -> list[EvaluationResult]:
    """Score one sample against every configured metric.

    Args:
        sample: The interaction to score.

    Returns:
        One :class:`~windlass.core.types.EvaluationResult` per metric.

    Raises:
        EvaluationError: When a metric cannot be computed.
    """

aevaluate async

aevaluate(
    samples: Sequence[EvalSample | RAGAnswer | dict[str, Any]],
) -> EvaluationReport

Evaluate a dataset and aggregate the results.

Parameters:

Name Type Description Default
samples Sequence[EvalSample | RAGAnswer | dict[str, Any]]

Samples, RAG answers, or dicts that coerce into samples.

required

Returns:

Type Description
EvaluationReport

An aggregated :class:~windlass.core.types.EvaluationReport.

Raises:

Type Description
EvaluationError

When every sample fails to evaluate.

Performance

Samples run concurrently up to concurrency. LLM-judged metrics dominate the cost, so keep an eye on that budget when evaluating thousands of rows.

Example

import asyncio from windlass.providers.evaluation.builtin import BuiltinEvaluator ev = BuiltinEvaluator(metrics=["answer_relevancy_lexical"]) report = asyncio.run(ev.aevaluate([ ... EvalSample(question="what is rag", answer="rag is retrieval") ... ])) report.samples 1

Source code in src\windlass\interfaces\evaluator.py
async def aevaluate(
    self, samples: Sequence[EvalSample | RAGAnswer | dict[str, Any]]
) -> EvaluationReport:
    """Evaluate a dataset and aggregate the results.

    Args:
        samples: Samples, RAG answers, or dicts that coerce into samples.

    Returns:
        An aggregated :class:`~windlass.core.types.EvaluationReport`.

    Raises:
        EvaluationError: When every sample fails to evaluate.

    Performance:
        Samples run concurrently up to ``concurrency``. LLM-judged metrics
        dominate the cost, so keep an eye on that budget when evaluating
        thousands of rows.

    Example:
        >>> import asyncio
        >>> from windlass.providers.evaluation.builtin import BuiltinEvaluator
        >>> ev = BuiltinEvaluator(metrics=["answer_relevancy_lexical"])
        >>> report = asyncio.run(ev.aevaluate([
        ...     EvalSample(question="what is rag", answer="rag is retrieval")
        ... ]))
        >>> report.samples
        1
    """
    prepared = [self._coerce(s) for s in samples]
    if not prepared:
        return EvaluationReport(samples=0)

    outcomes = await gather_bounded(
        [self.aevaluate_sample(s) for s in prepared],
        limit=self.concurrency,
        return_exceptions=True,
    )

    results: list[EvaluationResult] = []
    failures = 0
    for sample, outcome in zip(prepared, outcomes, strict=True):
        if isinstance(outcome, BaseException):
            failures += 1
            self._log.warning("Evaluation failed for sample %s: %s", sample.id, outcome)
            continue
        for result in outcome:
            if not result.sample_id:
                result.sample_id = sample.id
            if result.threshold is None:
                result.threshold = self.threshold
            result.passed = result.score >= result.threshold
            results.append(result)

    if failures and not results:
        from windlass.core.exceptions import EvaluationError

        raise EvaluationError(
            f"All {failures} samples failed to evaluate.",
            hint="Check that the judge LLM is configured and that samples "
            "carry the fields your metrics need (contexts, reference).",
        )

    return EvaluationReport(results=results, samples=len(prepared))

evaluate

evaluate(
    samples: Sequence[EvalSample | RAGAnswer | dict[str, Any]],
) -> EvaluationReport

Blocking :meth:aevaluate.

Source code in src\windlass\interfaces\evaluator.py
def evaluate(
    self, samples: Sequence[EvalSample | RAGAnswer | dict[str, Any]]
) -> EvaluationReport:
    """Blocking :meth:`aevaluate`."""
    return run_sync(self.aevaluate(samples))

Guardrail

Guardrail(
    *,
    on_violation: str = "block",
    stages: tuple[str, ...] = ("input", "output"),
    name: str | None = None,
    **config: Any
)

Bases: Component

Abstract input/output safety check.

Parameters:

Name Type Description Default
on_violation str

One of block, redact, warn or allow. warn logs and lets the content through unchanged, which is the right setting while you are calibrating a new policy in production.

'block'
stages tuple[str, ...]

Which stages this guardrail runs at — any of input and output.

('input', 'output')
name str | None

Component name for traces.

None
**config Any

Policy-specific options.

{}

Attributes:

Name Type Description
on_violation

The configured action.

stages

The stages this guardrail participates in.

Raises:

Type Description
ValueError

If on_violation is not a recognised action.

Example

Implementing a guardrail takes one method::

class NoShouting(Guardrail):
    provider_name = "no_shouting"

    async def acheck(self, content, *, stage="input", context=None):
        if content.isupper():
            return GuardrailResult(
                allowed=False, content=content,
                rule="shouting", stage=stage,
            )
        return GuardrailResult(allowed=True, content=content, stage=stage)
Source code in src\windlass\interfaces\guardrail.py
def __init__(
    self,
    *,
    on_violation: str = "block",
    stages: tuple[str, ...] = ("input", "output"),
    name: str | None = None,
    **config: Any,
) -> None:
    if on_violation not in VIOLATION_ACTIONS:
        raise ValueError(
            f"on_violation must be one of {VIOLATION_ACTIONS}, got {on_violation!r}"
        )
    super().__init__(
        name=name or self.provider_name,
        on_violation=on_violation,
        stages=stages,
        **config,
    )
    self.on_violation = on_violation
    self.stages = tuple(stages)

acheck abstractmethod async

acheck(
    content: str, *, stage: str = "input", context: dict[str, Any] | None = None
) -> GuardrailResult

Inspect content and return a verdict.

Implementations should report what they found and leave the policy decision to :attr:on_violation — that keeps one detector usable in both blocking and redacting configurations.

Parameters:

Name Type Description Default
content str

The text to inspect.

required
stage str

"input" or "output".

'input'
context dict[str, Any] | None

Extra signals — retrieved chunks, the user id, the tool about to be called.

None

Returns:

Type Description
GuardrailResult

The verdict, with content set to the (possibly rewritten) text.

Source code in src\windlass\interfaces\guardrail.py
@abc.abstractmethod
async def acheck(
    self,
    content: str,
    *,
    stage: str = "input",
    context: dict[str, Any] | None = None,
) -> GuardrailResult:
    """Inspect content and return a verdict.

    Implementations should report *what they found* and leave the policy
    decision to :attr:`on_violation` — that keeps one detector usable in
    both blocking and redacting configurations.

    Args:
        content: The text to inspect.
        stage: ``"input"`` or ``"output"``.
        context: Extra signals — retrieved chunks, the user id, the tool
            about to be called.

    Returns:
        The verdict, with ``content`` set to the (possibly rewritten) text.
    """

avalidate async

avalidate(
    content: str, *, stage: str = "input", context: dict[str, Any] | None = None
) -> str

Check content and apply the configured policy.

Parameters:

Name Type Description Default
content str

The text to check.

required
stage str

"input" or "output".

'input'
context dict[str, Any] | None

Extra signals for the check.

None

Returns:

Type Description
str

The content to use downstream — unchanged, or redacted.

Raises:

Type Description
GuardrailViolation

When a rule fires and on_violation='block'.

Example

import asyncio from windlass.providers.guardrails.rules import RuleGuardrail g = RuleGuardrail(banned_words=["secret"], on_violation="redact") asyncio.run(g.avalidate("the secret plan")) 'the [REDACTED] plan'

Source code in src\windlass\interfaces\guardrail.py
async def avalidate(
    self,
    content: str,
    *,
    stage: str = "input",
    context: dict[str, Any] | None = None,
) -> str:
    """Check content and apply the configured policy.

    Args:
        content: The text to check.
        stage: ``"input"`` or ``"output"``.
        context: Extra signals for the check.

    Returns:
        The content to use downstream — unchanged, or redacted.

    Raises:
        GuardrailViolation: When a rule fires and ``on_violation='block'``.

    Example:
        >>> import asyncio
        >>> from windlass.providers.guardrails.rules import RuleGuardrail
        >>> g = RuleGuardrail(banned_words=["secret"], on_violation="redact")
        >>> asyncio.run(g.avalidate("the secret plan"))
        'the [REDACTED] plan'
    """
    if stage not in self.stages:
        return content

    result = await self.acheck(content, stage=stage, context=context)
    if result.allowed and not result.detections:
        return content

    if self.on_violation == "allow":
        return content
    if self.on_violation == "warn":
        self._log.warning("Guardrail %s flagged %s content: %s", self.name, stage, result.rule)
        return content
    if self.on_violation == "redact":
        return result.content or content

    raise GuardrailViolation(
        f"Guardrail {self.name!r} blocked the {stage}: {result.rule or 'policy violation'}",
        stage=stage,
        rule=result.rule,
        detections=result.detections,
    )

validate

validate(
    content: str, *, stage: str = "input", context: dict[str, Any] | None = None
) -> str

Blocking :meth:avalidate.

Source code in src\windlass\interfaces\guardrail.py
def validate(
    self,
    content: str,
    *,
    stage: str = "input",
    context: dict[str, Any] | None = None,
) -> str:
    """Blocking :meth:`avalidate`."""
    return run_sync(self.avalidate(content, stage=stage, context=context))

check

check(
    content: str, *, stage: str = "input", context: dict[str, Any] | None = None
) -> GuardrailResult

Blocking :meth:acheck — returns the verdict without enforcing it.

Source code in src\windlass\interfaces\guardrail.py
def check(
    self,
    content: str,
    *,
    stage: str = "input",
    context: dict[str, Any] | None = None,
) -> GuardrailResult:
    """Blocking :meth:`acheck` — returns the verdict without enforcing it."""
    return run_sync(self.acheck(content, stage=stage, context=context))

Loader

Loader(
    *,
    encoding: str = "utf-8",
    metadata: dict[str, Any] | None = None,
    recursive: bool = True,
    on_error: str = "skip",
    name: str | None = None,
    **config: Any
)

Bases: Component

Abstract document loader.

Parameters:

Name Type Description Default
encoding str

Text encoding used for character-based formats.

'utf-8'
metadata dict[str, Any] | None

Extra metadata merged into every produced document — useful for tagging a whole corpus with a tenant or collection id.

None
recursive bool

Whether directory sources are walked recursively.

True
on_error str

"raise" aborts the batch, "skip" logs and continues. "skip" is the right default for a 10,000-file corpus where one corrupt PDF should not lose the run.

'skip'
name str | None

Component name for traces.

None
**config Any

Loader-specific options.

{}

Attributes:

Name Type Description
extensions tuple[str, ...]

File suffixes this loader claims, used for auto-detection.

mimetypes tuple[str, ...]

MIME types this loader claims.

Example

Implementing a loader takes one method::

class MyLoader(Loader):
    provider_name = "mine"
    extensions = (".mine",)

    async def aload_source(self, source):
        return [Document(content=read(source), source=str(source))]
Source code in src\windlass\interfaces\loader.py
def __init__(
    self,
    *,
    encoding: str = "utf-8",
    metadata: dict[str, Any] | None = None,
    recursive: bool = True,
    on_error: str = "skip",
    name: str | None = None,
    **config: Any,
) -> None:
    if on_error not in {"raise", "skip"}:
        raise ValueError("on_error must be 'raise' or 'skip'")
    super().__init__(
        name=name or self.provider_name,
        encoding=encoding,
        metadata=metadata or {},
        recursive=recursive,
        on_error=on_error,
        **config,
    )
    self.encoding = encoding
    self.extra_metadata: dict[str, Any] = dict(metadata or {})
    self.recursive = recursive
    self.on_error = on_error

aload_source abstractmethod async

aload_source(source: SourceLike) -> list[Document]

Load one concrete source.

Called once per file. Directory expansion and error handling are done for you by :meth:aload.

Parameters:

Name Type Description Default
source SourceLike

A single path, URL or byte payload.

required

Returns:

Type Description
list[Document]

The documents extracted from it. A PDF may return one document per

list[Document]

page or one for the whole file — that is the loader's choice.

Raises:

Type Description
IngestionError

When the source cannot be parsed.

Source code in src\windlass\interfaces\loader.py
@abc.abstractmethod
async def aload_source(self, source: SourceLike) -> list[Document]:
    """Load one concrete source.

    Called once per file. Directory expansion and error handling are done
    for you by :meth:`aload`.

    Args:
        source: A single path, URL or byte payload.

    Returns:
        The documents extracted from it. A PDF may return one document per
        page or one for the whole file — that is the loader's choice.

    Raises:
        IngestionError: When the source cannot be parsed.
    """

can_handle classmethod

can_handle(source: SourceLike) -> bool

Return whether this loader claims source.

Parameters:

Name Type Description Default
source SourceLike

Path, URL or payload to test.

required

Returns:

Type Description
bool

True when the extension or scheme matches this loader.

Example

from windlass.providers.loaders.text import TextLoader TextLoader.can_handle("notes.txt") True TextLoader.can_handle("scan.pdf") False

Source code in src\windlass\interfaces\loader.py
@classmethod
def can_handle(cls, source: SourceLike) -> bool:
    """Return whether this loader claims ``source``.

    Args:
        source: Path, URL or payload to test.

    Returns:
        True when the extension or scheme matches this loader.

    Example:
        >>> from windlass.providers.loaders.text import TextLoader
        >>> TextLoader.can_handle("notes.txt")
        True
        >>> TextLoader.can_handle("scan.pdf")
        False
    """
    if isinstance(source, str | Path):
        text = str(source)
        if text.startswith(("http://", "https://")):
            return cls.handles_urls
        return Path(text).suffix.lower() in cls.extensions
    return False

aload async

aload(
    source: SourceLike | Sequence[SourceLike], *, concurrency: int | None = None
) -> list[Document]

Load one or many sources.

Directories are expanded (respecting :attr:recursive and this loader's :attr:extensions), and files are read concurrently.

Parameters:

Name Type Description Default
source SourceLike | Sequence[SourceLike]

A path, URL, byte payload, or a sequence of them. A directory path expands to its matching files.

required
concurrency int | None

Maximum simultaneous reads. Defaults to the global max_concurrency setting.

None

Returns:

Type Description
list[Document]

Every extracted document, with :attr:extra_metadata merged in.

Raises:

Type Description
IngestionError

When on_error='raise' and a source fails, or when no source could be read at all.

Performance

Blocking parsers run on the thread pool, so a folder of 500 PDFs is parsed in parallel rather than serially.

Source code in src\windlass\interfaces\loader.py
async def aload(
    self, source: SourceLike | Sequence[SourceLike], *, concurrency: int | None = None
) -> list[Document]:
    """Load one or many sources.

    Directories are expanded (respecting :attr:`recursive` and this loader's
    :attr:`extensions`), and files are read concurrently.

    Args:
        source: A path, URL, byte payload, or a sequence of them. A
            directory path expands to its matching files.
        concurrency: Maximum simultaneous reads. Defaults to the global
            ``max_concurrency`` setting.

    Returns:
        Every extracted document, with :attr:`extra_metadata` merged in.

    Raises:
        IngestionError: When ``on_error='raise'`` and a source fails, or
            when no source could be read at all.

    Performance:
        Blocking parsers run on the thread pool, so a folder of 500 PDFs is
        parsed in parallel rather than serially.
    """
    sources = self._expand(source)
    if not sources:
        return []

    limit = concurrency or settings().max_concurrency
    results = await gather_bounded(
        [self._load_guarded(s) for s in sources], limit=limit, return_exceptions=True
    )

    documents: list[Document] = []
    failures: list[tuple[Any, BaseException]] = []
    for src, result in zip(sources, results, strict=True):
        if isinstance(result, BaseException):
            failures.append((src, result))
            continue
        documents.extend(result)

    if failures:
        if self.on_error == "raise":
            src, exc = failures[0]
            raise IngestionError(
                f"Failed to load {src}: {exc}",
                context={"source": str(src), "failures": len(failures)},
            ) from exc
        for src, exc in failures:
            self._log.warning("Skipping %s: %s", src, exc)
        if not documents:
            src, exc = failures[0]
            raise IngestionError(
                f"No documents could be loaded; the first failure was {src}: {exc}",
                hint="Check the paths and that the right extra is installed.",
                context={"failures": len(failures)},
            ) from exc

    for doc in documents:
        if self.extra_metadata:
            doc.metadata = {**self.extra_metadata, **doc.metadata}
        doc.metadata.setdefault("loader", self.name)
    return documents

load

load(source: SourceLike | Sequence[SourceLike]) -> list[Document]

Blocking :meth:aload.

Source code in src\windlass\interfaces\loader.py
def load(self, source: SourceLike | Sequence[SourceLike]) -> list[Document]:
    """Blocking :meth:`aload`."""
    return run_sync(self.aload(source))

astream_load async

astream_load(source: SourceLike | Sequence[SourceLike]) -> AsyncIterator[Document]

Yield documents one at a time instead of building a list.

Use this for corpora too large to hold in memory: combined with Pipeline.aingest_stream it keeps peak memory flat regardless of corpus size.

Parameters:

Name Type Description Default
source SourceLike | Sequence[SourceLike]

A path, URL, payload, or sequence of them.

required

Yields:

Type Description
AsyncIterator[Document]

Documents as each source finishes parsing.

Source code in src\windlass\interfaces\loader.py
async def astream_load(
    self, source: SourceLike | Sequence[SourceLike]
) -> AsyncIterator[Document]:
    """Yield documents one at a time instead of building a list.

    Use this for corpora too large to hold in memory: combined with
    ``Pipeline.aingest_stream`` it keeps peak memory flat regardless of
    corpus size.

    Args:
        source: A path, URL, payload, or sequence of them.

    Yields:
        Documents as each source finishes parsing.
    """
    for item in self._expand(source):
        try:
            docs = await self.aload_source(item)
        except Exception as exc:
            if self.on_error == "raise":
                raise IngestionError(f"Failed to load {_label(item)}: {exc}") from exc
            self._log.warning("Skipping %s: %s", item, exc)
            continue
        for doc in docs:
            if self.extra_metadata:
                doc.metadata = {**self.extra_metadata, **doc.metadata}
            doc.metadata.setdefault("loader", self.name)
            yield doc

MCPClient

MCPClient(
    *,
    server: str = "",
    namespace: bool = False,
    timeout: float = 30.0,
    name: str | None = None,
    **config: Any
)

Bases: Component

Abstract MCP client.

Parameters:

Name Type Description Default
server str

Server label used in logs and to namespace tools.

''
namespace bool

When True, remote tools are exposed as server_toolname so two servers offering search do not collide.

False
timeout float

Per-call timeout in seconds.

30.0
name str | None

Component name.

None
**config Any

Transport-specific options (command, args, url, env, ...).

{}

Attributes:

Name Type Description
connected

Whether the transport is currently open.

Example

Implementing a client means three methods::

class MyClient(MCPClient):
    provider_name = "mine"

    async def aconnect(self): ...
    async def alist_tools(self): ...
    async def acall_tool(self, name, arguments): ...
Source code in src\windlass\interfaces\mcp.py
def __init__(
    self,
    *,
    server: str = "",
    namespace: bool = False,
    timeout: float = 30.0,
    name: str | None = None,
    **config: Any,
) -> None:
    super().__init__(
        name=name or server or self.provider_name,
        server=server,
        namespace=namespace,
        timeout=timeout,
        **config,
    )
    self.server = server or self.provider_name
    self.namespace = namespace
    self.timeout = timeout
    self.connected = False

aconnect abstractmethod async

aconnect() -> None

Open the transport and perform the MCP handshake.

Must be idempotent — calling it twice should not open two sessions.

Raises:

Type Description
MCPError

When the server cannot be reached or the handshake fails.

Source code in src\windlass\interfaces\mcp.py
@abc.abstractmethod
async def aconnect(self) -> None:
    """Open the transport and perform the MCP handshake.

    Must be idempotent — calling it twice should not open two sessions.

    Raises:
        MCPError: When the server cannot be reached or the handshake fails.
    """

alist_tools abstractmethod async

alist_tools() -> list[Tool]

Discover the server's tools.

Returns:

Type Description
list[Tool]

class:MCPToolProxy instances ready to bind to an agent.

Raises:

Type Description
MCPError

When discovery fails.

Source code in src\windlass\interfaces\mcp.py
@abc.abstractmethod
async def alist_tools(self) -> list[Tool]:
    """Discover the server's tools.

    Returns:
        :class:`MCPToolProxy` instances ready to bind to an agent.

    Raises:
        MCPError: When discovery fails.
    """

acall_tool abstractmethod async

acall_tool(name: str, arguments: dict[str, Any]) -> Any

Invoke a remote tool.

Parameters:

Name Type Description Default
name str

Tool name as advertised by the server (without any namespace prefix Windlass may have added).

required
arguments dict[str, Any]

Arguments matching the remote schema.

required

Returns:

Type Description
Any

The server's result, unwrapped from the MCP content envelope.

Raises:

Type Description
MCPError

When the call fails.

Source code in src\windlass\interfaces\mcp.py
@abc.abstractmethod
async def acall_tool(self, name: str, arguments: dict[str, Any]) -> Any:
    """Invoke a remote tool.

    Args:
        name: Tool name as advertised by the server (without any namespace
            prefix Windlass may have added).
        arguments: Arguments matching the remote schema.

    Returns:
        The server's result, unwrapped from the MCP content envelope.

    Raises:
        MCPError: When the call fails.
    """

alist_resources async

alist_resources() -> list[MCPResource]

Discover readable resources. Returns [] when unsupported.

Source code in src\windlass\interfaces\mcp.py
async def alist_resources(self) -> list[MCPResource]:
    """Discover readable resources. Returns ``[]`` when unsupported."""
    return []

aread_resource async

aread_resource(uri: str) -> str

Read a resource's contents.

Parameters:

Name Type Description Default
uri str

The resource identifier.

required

Returns:

Type Description
str

The resource body as text.

Raises:

Type Description
MCPError

When the resource cannot be read.

Source code in src\windlass\interfaces\mcp.py
async def aread_resource(self, uri: str) -> str:
    """Read a resource's contents.

    Args:
        uri: The resource identifier.

    Returns:
        The resource body as text.

    Raises:
        MCPError: When the resource cannot be read.
    """
    raise MCPError(
        f"{type(self).__name__} does not support reading resources.",
        context={"uri": uri},
    )

alist_prompts async

alist_prompts() -> list[MCPPrompt]

Discover prompt templates. Returns [] when unsupported.

Source code in src\windlass\interfaces\mcp.py
async def alist_prompts(self) -> list[MCPPrompt]:
    """Discover prompt templates. Returns ``[]`` when unsupported."""
    return []

aget_prompt async

aget_prompt(name: str, arguments: dict[str, Any] | None = None) -> str

Instantiate a prompt template.

Parameters:

Name Type Description Default
name str

Prompt name.

required
arguments dict[str, Any] | None

Template arguments.

None

Returns:

Type Description
str

The rendered prompt text.

Raises:

Type Description
MCPError

When the prompt cannot be rendered.

Source code in src\windlass\interfaces\mcp.py
async def aget_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> str:
    """Instantiate a prompt template.

    Args:
        name: Prompt name.
        arguments: Template arguments.

    Returns:
        The rendered prompt text.

    Raises:
        MCPError: When the prompt cannot be rendered.
    """
    raise MCPError(f"{type(self).__name__} does not support prompts.", context={"prompt": name})

adisconnect async

adisconnect() -> None

Close the transport. Safe to call when already closed.

Source code in src\windlass\interfaces\mcp.py
async def adisconnect(self) -> None:
    """Close the transport. Safe to call when already closed."""
    self.connected = False

connect

connect() -> None

Blocking :meth:aconnect.

Source code in src\windlass\interfaces\mcp.py
def connect(self) -> None:
    """Blocking :meth:`aconnect`."""
    run_sync(self.aconnect())

list_tools

list_tools() -> list[Tool]

Blocking :meth:alist_tools.

Source code in src\windlass\interfaces\mcp.py
def list_tools(self) -> list[Tool]:
    """Blocking :meth:`alist_tools`."""
    return run_sync(self.alist_tools())

call_tool

call_tool(name: str, arguments: dict[str, Any] | None = None) -> Any

Blocking :meth:acall_tool.

Source code in src\windlass\interfaces\mcp.py
def call_tool(self, name: str, arguments: dict[str, Any] | None = None) -> Any:
    """Blocking :meth:`acall_tool`."""
    return run_sync(self.acall_tool(name, arguments or {}))

list_resources

list_resources() -> list[MCPResource]

Blocking :meth:alist_resources.

Source code in src\windlass\interfaces\mcp.py
def list_resources(self) -> list[MCPResource]:
    """Blocking :meth:`alist_resources`."""
    return run_sync(self.alist_resources())

read_resource

read_resource(uri: str) -> str

Blocking :meth:aread_resource.

Source code in src\windlass\interfaces\mcp.py
def read_resource(self, uri: str) -> str:
    """Blocking :meth:`aread_resource`."""
    return run_sync(self.aread_resource(uri))

list_prompts

list_prompts() -> list[MCPPrompt]

Blocking :meth:alist_prompts.

Source code in src\windlass\interfaces\mcp.py
def list_prompts(self) -> list[MCPPrompt]:
    """Blocking :meth:`alist_prompts`."""
    return run_sync(self.alist_prompts())

get_prompt

get_prompt(name: str, arguments: dict[str, Any] | None = None) -> str

Blocking :meth:aget_prompt.

Source code in src\windlass\interfaces\mcp.py
def get_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> str:
    """Blocking :meth:`aget_prompt`."""
    return run_sync(self.aget_prompt(name, arguments))

disconnect

disconnect() -> None

Blocking :meth:adisconnect.

Source code in src\windlass\interfaces\mcp.py
def disconnect(self) -> None:
    """Blocking :meth:`adisconnect`."""
    run_sync(self.adisconnect())

aclose async

aclose() -> None

Alias for :meth:adisconnect, so containers can close uniformly.

Source code in src\windlass\interfaces\mcp.py
async def aclose(self) -> None:
    """Alias for :meth:`adisconnect`, so containers can close uniformly."""
    await self.adisconnect()

Memory

Memory(
    *,
    max_messages: int | None = None,
    return_system: bool = False,
    name: str | None = None,
    **config: Any
)

Bases: Component

Abstract conversation / long-term memory.

Memory is keyed by thread_id so one instance can serve many concurrent users — an agent handling a hundred chat sessions needs one memory object, not a hundred.

Parameters:

Name Type Description Default
max_messages int | None

Ceiling on how many messages :meth:aget returns. None means unlimited.

None
return_system bool

Whether system messages are included in recall. Usually False: the system prompt is supplied by the agent, not the history.

False
name str | None

Component name for traces.

None
**config Any

Strategy-specific options.

{}
Example

Implementing a memory takes two methods::

class NullMemory(Memory):
    provider_name = "null"

    async def aadd(self, messages, *, thread_id="default"): ...
    async def aget(self, *, thread_id="default", query=None):
        return []
Source code in src\windlass\interfaces\memory.py
def __init__(
    self,
    *,
    max_messages: int | None = None,
    return_system: bool = False,
    name: str | None = None,
    **config: Any,
) -> None:
    super().__init__(
        name=name or self.provider_name,
        max_messages=max_messages,
        return_system=return_system,
        **config,
    )
    self.max_messages = max_messages
    self.return_system = return_system

aadd abstractmethod async

aadd(messages: Message | Sequence[Message], *, thread_id: str = DEFAULT_THREAD) -> None

Record one or more messages.

Parameters:

Name Type Description Default
messages Message | Sequence[Message]

A message or a sequence of them.

required
thread_id str

Conversation this belongs to.

DEFAULT_THREAD

Raises:

Type Description
MemoryError_

When the backend cannot persist the messages.

Source code in src\windlass\interfaces\memory.py
@abc.abstractmethod
async def aadd(
    self, messages: Message | Sequence[Message], *, thread_id: str = DEFAULT_THREAD
) -> None:
    """Record one or more messages.

    Args:
        messages: A message or a sequence of them.
        thread_id: Conversation this belongs to.

    Raises:
        WindlassMemoryError: When the backend cannot persist the messages.
    """

aget abstractmethod async

aget(
    *,
    thread_id: str = DEFAULT_THREAD,
    query: str | None = None,
    limit: int | None = None
) -> list[Message]

Recall messages for a thread.

Parameters:

Name Type Description Default
thread_id str

Conversation to recall.

DEFAULT_THREAD
query str | None

Current user input. Semantic memories use it to rank recall; recency-based memories ignore it.

None
limit int | None

Override for :attr:max_messages.

None

Returns:

Type Description
list[Message]

Messages in chronological order, oldest first.

Source code in src\windlass\interfaces\memory.py
@abc.abstractmethod
async def aget(
    self,
    *,
    thread_id: str = DEFAULT_THREAD,
    query: str | None = None,
    limit: int | None = None,
) -> list[Message]:
    """Recall messages for a thread.

    Args:
        thread_id: Conversation to recall.
        query: Current user input. Semantic memories use it to rank recall;
            recency-based memories ignore it.
        limit: Override for :attr:`max_messages`.

    Returns:
        Messages in chronological order, oldest first.
    """

aclear async

aclear(*, thread_id: str | None = None) -> None

Forget a thread, or everything when thread_id is None.

Parameters:

Name Type Description Default
thread_id str | None

Thread to clear. None clears every thread.

None
Source code in src\windlass\interfaces\memory.py
async def aclear(self, *, thread_id: str | None = None) -> None:
    """Forget a thread, or everything when ``thread_id`` is ``None``.

    Args:
        thread_id: Thread to clear. ``None`` clears every thread.
    """

athreads async

athreads() -> list[str]

Return the thread ids this memory knows about.

Source code in src\windlass\interfaces\memory.py
async def athreads(self) -> list[str]:
    """Return the thread ids this memory knows about."""
    return []

add

add(messages: Message | Sequence[Message], *, thread_id: str = DEFAULT_THREAD) -> None

Blocking :meth:aadd.

Source code in src\windlass\interfaces\memory.py
def add(
    self, messages: Message | Sequence[Message], *, thread_id: str = DEFAULT_THREAD
) -> None:
    """Blocking :meth:`aadd`."""
    run_sync(self.aadd(messages, thread_id=thread_id))

get

get(
    *,
    thread_id: str = DEFAULT_THREAD,
    query: str | None = None,
    limit: int | None = None
) -> list[Message]

Blocking :meth:aget.

Source code in src\windlass\interfaces\memory.py
def get(
    self,
    *,
    thread_id: str = DEFAULT_THREAD,
    query: str | None = None,
    limit: int | None = None,
) -> list[Message]:
    """Blocking :meth:`aget`."""
    return run_sync(self.aget(thread_id=thread_id, query=query, limit=limit))

clear

clear(*, thread_id: str | None = None) -> None

Blocking :meth:aclear.

Source code in src\windlass\interfaces\memory.py
def clear(self, *, thread_id: str | None = None) -> None:
    """Blocking :meth:`aclear`."""
    run_sync(self.aclear(thread_id=thread_id))

threads

threads() -> list[str]

Blocking :meth:athreads.

Source code in src\windlass\interfaces\memory.py
def threads(self) -> list[str]:
    """Blocking :meth:`athreads`."""
    return run_sync(self.athreads())

Preprocessor

Preprocessor(*, name: str | None = None, **config: Any)

Bases: Component

Abstract document preprocessor.

Parameters:

Name Type Description Default
name str | None

Component name for traces.

None
**config Any

Preprocessor-specific options.

{}
Example

Implementing a preprocessor takes one method::

class Shout(Preprocessor):
    provider_name = "shout"

    async def aprocess_one(self, document):
        return [document.model_copy(
            update={"content": document.content.upper()}
        )]
Source code in src\windlass\interfaces\base.py
def __init__(self, *, name: str | None = None, **config: Any) -> None:
    self.name = name or getattr(type(self), "provider_name", None) or type(self).__name__
    self.config: dict[str, Any] = dict(config)
    self._log = get_logger(f"{self.kind}.{self.name}")

aprocess_one abstractmethod async

aprocess_one(document: Document) -> list[Document]

Transform a single document.

Parameters:

Name Type Description Default
document Document

The document to process.

required

Returns:

Type Description
list[Document]

Zero, one or many documents. Return [] to drop the input.

Raises:

Type Description
IngestionError

When the document cannot be processed and dropping it silently would hide a real problem.

Source code in src\windlass\interfaces\preprocessor.py
@abc.abstractmethod
async def aprocess_one(self, document: Document) -> list[Document]:
    """Transform a single document.

    Args:
        document: The document to process.

    Returns:
        Zero, one or many documents. Return ``[]`` to drop the input.

    Raises:
        IngestionError: When the document cannot be processed and dropping
            it silently would hide a real problem.
    """

aprocess async

aprocess(
    documents: Sequence[Document], *, concurrency: int | None = None
) -> list[Document]

Process a batch of documents concurrently.

Parameters:

Name Type Description Default
documents Sequence[Document]

The documents to process.

required
concurrency int | None

Maximum simultaneous invocations. Defaults to the global max_concurrency setting.

None

Returns:

Type Description
list[Document]

The flattened output, preserving input order.

Performance

Order is preserved even though execution is concurrent, so a preprocessor that calls an LLM (metadata extraction, contextual enrichment) still yields a deterministic corpus.

Source code in src\windlass\interfaces\preprocessor.py
async def aprocess(
    self, documents: Sequence[Document], *, concurrency: int | None = None
) -> list[Document]:
    """Process a batch of documents concurrently.

    Args:
        documents: The documents to process.
        concurrency: Maximum simultaneous invocations. Defaults to the
            global ``max_concurrency`` setting.

    Returns:
        The flattened output, preserving input order.

    Performance:
        Order is preserved even though execution is concurrent, so a
        preprocessor that calls an LLM (metadata extraction, contextual
        enrichment) still yields a deterministic corpus.
    """
    if not documents:
        return []
    limit = concurrency or settings().max_concurrency
    batches = await gather_bounded([self.aprocess_one(doc) for doc in documents], limit=limit)
    return [doc for batch in batches for doc in batch]

process

process(documents: Sequence[Document] | Document) -> list[Document]

Blocking :meth:aprocess.

Parameters:

Name Type Description Default
documents Sequence[Document] | Document

One document or a sequence of them.

required

Returns:

Type Description
list[Document]

The processed documents.

Source code in src\windlass\interfaces\preprocessor.py
def process(self, documents: Sequence[Document] | Document) -> list[Document]:
    """Blocking :meth:`aprocess`.

    Args:
        documents: One document or a sequence of them.

    Returns:
        The processed documents.
    """
    items = [documents] if isinstance(documents, Document) else list(documents)
    return run_sync(self.aprocess(items))

Retriever

Retriever(
    *,
    top_k: int = 5,
    score_threshold: float | None = None,
    rerank: Any = None,
    fetch_k: int | None = None,
    name: str | None = None,
    **config: Any
)

Bases: Component

Abstract retrieval strategy.

Parameters:

Name Type Description Default
top_k int

Default number of chunks to return.

5
score_threshold float | None

Drop hits scoring below this. None keeps everything. Useful for "answer only if we actually found something" flows.

None
rerank Any

Optional reranker applied to the candidate set before truncation. Any object with an arerank(query, hits, k) coroutine.

None
fetch_k int | None

How many candidates to pull before reranking/filtering. Defaults to top_k when no reranker is configured, 4 * top_k when one is.

None
name str | None

Component name for traces.

None
**config Any

Strategy-specific options.

{}

Attributes:

Name Type Description
top_k

The configured result count.

requires_index bool

Whether :meth:aindex must be called before searching.

Example

Implementing a retriever takes one method::

class RandomRetriever(Retriever):
    provider_name = "random"

    async def aretrieve_chunks(self, query, k, *, filters=None, **kw):
        picks = random.sample(self.corpus, k)
        return [ScoredChunk(chunk=c, score=1.0) for c in picks]
Source code in src\windlass\interfaces\retriever.py
def __init__(
    self,
    *,
    top_k: int = 5,
    score_threshold: float | None = None,
    rerank: Any = None,
    fetch_k: int | None = None,
    name: str | None = None,
    **config: Any,
) -> None:
    if top_k <= 0:
        raise ValueError("top_k must be positive")
    super().__init__(
        name=name or self.provider_name,
        top_k=top_k,
        score_threshold=score_threshold,
        fetch_k=fetch_k,
        **config,
    )
    self.top_k = top_k
    self.score_threshold = score_threshold
    self.reranker = rerank
    self.fetch_k = fetch_k or (top_k * 4 if rerank is not None else top_k)

aretrieve_chunks abstractmethod async

aretrieve_chunks(
    query: str, k: int, *, filters: MetadataFilter | None = None, **kwargs: Any
) -> list[ScoredChunk]

Return candidate chunks for query.

The only method a strategy must implement. Thresholding, reranking, truncation and timing are applied by :meth:aretrieve.

Parameters:

Name Type Description Default
query str

The search query.

required
k int

How many candidates to produce. This is fetch_k, not top_k, when a reranker is configured.

required
filters MetadataFilter | None

Metadata constraints.

None
**kwargs Any

Strategy-specific options.

{}

Returns:

Type Description
list[ScoredChunk]

Scored chunks, ideally already sorted by descending score.

Source code in src\windlass\interfaces\retriever.py
@abc.abstractmethod
async def aretrieve_chunks(
    self,
    query: str,
    k: int,
    *,
    filters: MetadataFilter | None = None,
    **kwargs: Any,
) -> list[ScoredChunk]:
    """Return candidate chunks for ``query``.

    The only method a strategy must implement. Thresholding, reranking,
    truncation and timing are applied by :meth:`aretrieve`.

    Args:
        query: The search query.
        k: How many candidates to produce. This is ``fetch_k``, not
            ``top_k``, when a reranker is configured.
        filters: Metadata constraints.
        **kwargs: Strategy-specific options.

    Returns:
        Scored chunks, ideally already sorted by descending score.
    """

aindex async

aindex(chunks: Sequence[Chunk]) -> int

Add chunks to whatever index this retriever maintains.

The default is a no-op, which is right for retrievers that read from a shared vector store. Lexical retrievers override it.

Parameters:

Name Type Description Default
chunks Sequence[Chunk]

Chunks to index.

required

Returns:

Type Description
int

How many chunks were indexed.

Source code in src\windlass\interfaces\retriever.py
async def aindex(self, chunks: Sequence[Chunk]) -> int:
    """Add chunks to whatever index this retriever maintains.

    The default is a no-op, which is right for retrievers that read from a
    shared vector store. Lexical retrievers override it.

    Args:
        chunks: Chunks to index.

    Returns:
        How many chunks were indexed.
    """
    return 0

index

index(chunks: Sequence[Chunk]) -> int

Blocking :meth:aindex.

Source code in src\windlass\interfaces\retriever.py
def index(self, chunks: Sequence[Chunk]) -> int:
    """Blocking :meth:`aindex`."""
    return run_sync(self.aindex(chunks))

aretrieve async

aretrieve(
    query: str,
    k: int | None = None,
    *,
    filters: MetadataFilter | None = None,
    **kwargs: Any
) -> SearchResult

Retrieve chunks for a query.

Parameters:

Name Type Description Default
query str

The search query.

required
k int | None

Override for :attr:top_k.

None
filters MetadataFilter | None

Metadata constraints.

None
**kwargs Any

Strategy-specific options.

{}

Returns:

Name Type Description
A SearchResult

class:~windlass.core.types.SearchResult with ranked hits,

SearchResult

candidate count and latency.

Raises:

Type Description
RetrievalError

When the underlying strategy fails.

Performance

With a reranker configured, fetch_k candidates are retrieved and then narrowed to k. Raising fetch_k improves recall at the cost of one larger rerank call.

Example

import asyncio from windlass.providers.retrievers.bm25 import BM25Retriever from windlass.core.types import Chunk r = BM25Retriever() _ = r.index([Chunk(content="vector search rocks")]) asyncio.run(r.aretrieve("vector")).hits[0].score > 0 True

Source code in src\windlass\interfaces\retriever.py
async def aretrieve(
    self,
    query: str,
    k: int | None = None,
    *,
    filters: MetadataFilter | None = None,
    **kwargs: Any,
) -> SearchResult:
    """Retrieve chunks for a query.

    Args:
        query: The search query.
        k: Override for :attr:`top_k`.
        filters: Metadata constraints.
        **kwargs: Strategy-specific options.

    Returns:
        A :class:`~windlass.core.types.SearchResult` with ranked hits,
        candidate count and latency.

    Raises:
        RetrievalError: When the underlying strategy fails.

    Performance:
        With a reranker configured, ``fetch_k`` candidates are retrieved and
        then narrowed to ``k``. Raising ``fetch_k`` improves recall at the
        cost of one larger rerank call.

    Example:
        >>> import asyncio
        >>> from windlass.providers.retrievers.bm25 import BM25Retriever
        >>> from windlass.core.types import Chunk
        >>> r = BM25Retriever()
        >>> _ = r.index([Chunk(content="vector search rocks")])
        >>> asyncio.run(r.aretrieve("vector")).hits[0].score > 0
        True
    """
    limit = k or self.top_k
    candidates = max(limit, self.fetch_k) if self.reranker is not None else limit
    started = time.perf_counter()

    hits = await self.aretrieve_chunks(query, candidates, filters=filters, **kwargs)

    if self.reranker is not None and hits:
        hits = await self.reranker.arerank(query, hits, limit)

    if self.score_threshold is not None:
        hits = [h for h in hits if h.score >= self.score_threshold]

    hits = sorted(hits, key=lambda h: h.score, reverse=True)[:limit]
    for position, hit in enumerate(hits, start=1):
        hit.rank = position
        if not hit.retriever:
            hit.retriever = self.name

    return SearchResult(
        query=query,
        hits=hits,
        total=len(hits),
        latency_ms=(time.perf_counter() - started) * 1000,
    )

retrieve

retrieve(
    query: str,
    k: int | None = None,
    *,
    filters: MetadataFilter | None = None,
    **kwargs: Any
) -> SearchResult

Blocking :meth:aretrieve.

Source code in src\windlass\interfaces\retriever.py
def retrieve(
    self,
    query: str,
    k: int | None = None,
    *,
    filters: MetadataFilter | None = None,
    **kwargs: Any,
) -> SearchResult:
    """Blocking :meth:`aretrieve`."""
    return run_sync(self.aretrieve(query, k, filters=filters, **kwargs))

abatch_retrieve async

abatch_retrieve(
    queries: Sequence[str],
    k: int | None = None,
    *,
    filters: MetadataFilter | None = None,
    concurrency: int | None = None
) -> list[SearchResult]

Retrieve for many queries concurrently.

Parameters:

Name Type Description Default
queries Sequence[str]

The queries to run.

required
k int | None

Override for :attr:top_k.

None
filters MetadataFilter | None

Metadata constraints applied to every query.

None
concurrency int | None

Maximum simultaneous retrievals.

None

Returns:

Type Description
list[SearchResult]

One result per query, in input order.

Source code in src\windlass\interfaces\retriever.py
async def abatch_retrieve(
    self,
    queries: Sequence[str],
    k: int | None = None,
    *,
    filters: MetadataFilter | None = None,
    concurrency: int | None = None,
) -> list[SearchResult]:
    """Retrieve for many queries concurrently.

    Args:
        queries: The queries to run.
        k: Override for :attr:`top_k`.
        filters: Metadata constraints applied to every query.
        concurrency: Maximum simultaneous retrievals.

    Returns:
        One result per query, in input order.
    """
    from windlass.core.config import settings

    limit = concurrency or settings().max_concurrency
    return await gather_bounded(
        [self.aretrieve(q, k, filters=filters) for q in queries], limit=limit
    )

batch_retrieve

batch_retrieve(queries: Sequence[str], k: int | None = None) -> list[SearchResult]

Blocking :meth:abatch_retrieve.

Source code in src\windlass\interfaces\retriever.py
def batch_retrieve(self, queries: Sequence[str], k: int | None = None) -> list[SearchResult]:
    """Blocking :meth:`abatch_retrieve`."""
    return run_sync(self.abatch_retrieve(queries, k))

Span

Span(
    name: str,
    *,
    kind: str = "chain",
    parent_id: str | None = None,
    metadata: dict[str, Any] | None = None,
    trace_id: str | None = None
)

One timed, attributed unit of work.

Spans are created by :meth:Tracer.span and finished automatically when the context manager exits. Exceptions are recorded before propagating, so a failed run is still fully traced.

Parameters:

Name Type Description Default
name str

Human readable operation name.

required
kind str

One of :data:SPAN_KINDS.

'chain'
parent_id str | None

Enclosing span's id, when nested.

None
metadata dict[str, Any] | None

Static attributes attached at creation.

None
trace_id str | None

Groups spans belonging to one top-level request.

None

Attributes:

Name Type Description
id

Unique span id.

started_at

Monotonic start time.

ended_at float | None

Monotonic end time, set on :meth:end.

error str | None

Error message, when the span failed.

usage Usage | None

Token accounting, for model spans.

Source code in src\windlass\interfaces\tracer.py
def __init__(
    self,
    name: str,
    *,
    kind: str = "chain",
    parent_id: str | None = None,
    metadata: dict[str, Any] | None = None,
    trace_id: str | None = None,
) -> None:
    self.id = uuid.uuid4().hex[:16]
    self.trace_id = trace_id or self.id
    self.parent_id = parent_id
    self.name = name
    self.kind = kind
    self.metadata: dict[str, Any] = dict(metadata or {})
    self.inputs: Any = None
    self.outputs: Any = None
    self.usage: Usage | None = None
    self.error: str | None = None
    self.started_at = time.perf_counter()
    self.ended_at: float | None = None
    self._native: Any = None

duration_ms property

duration_ms: float

Elapsed time in milliseconds — live until the span ends.

set_input

set_input(value: Any) -> Span

Record the operation's input. Returns self for chaining.

Source code in src\windlass\interfaces\tracer.py
def set_input(self, value: Any) -> Span:
    """Record the operation's input. Returns ``self`` for chaining."""
    self.inputs = value
    return self

set_output

set_output(value: Any) -> Span

Record the operation's output. Returns self for chaining.

Source code in src\windlass\interfaces\tracer.py
def set_output(self, value: Any) -> Span:
    """Record the operation's output. Returns ``self`` for chaining."""
    self.outputs = value
    return self

set_usage

set_usage(usage: Usage) -> Span

Record token accounting. Returns self for chaining.

Source code in src\windlass\interfaces\tracer.py
def set_usage(self, usage: Usage) -> Span:
    """Record token accounting. Returns ``self`` for chaining."""
    self.usage = usage
    return self

set_metadata

set_metadata(**fields: Any) -> Span

Merge extra attributes. Returns self for chaining.

Source code in src\windlass\interfaces\tracer.py
def set_metadata(self, **fields: Any) -> Span:
    """Merge extra attributes. Returns ``self`` for chaining."""
    self.metadata.update(fields)
    return self

set_error

set_error(error: BaseException | str) -> Span

Mark the span as failed. Returns self for chaining.

Source code in src\windlass\interfaces\tracer.py
def set_error(self, error: BaseException | str) -> Span:
    """Mark the span as failed. Returns ``self`` for chaining."""
    self.error = str(error)
    return self

end

end() -> Span

Stop the clock. Idempotent.

Source code in src\windlass\interfaces\tracer.py
def end(self) -> Span:
    """Stop the clock. Idempotent."""
    if self.ended_at is None:
        self.ended_at = time.perf_counter()
    return self

attach_native

attach_native(obj: Any) -> Span

Associate the backend's own span object, for Level 3 access.

Source code in src\windlass\interfaces\tracer.py
def attach_native(self, obj: Any) -> Span:
    """Associate the backend's own span object, for Level 3 access."""
    self._native = obj
    return self

native

native() -> Any

Return the backend's span object, if the tracer created one.

Source code in src\windlass\interfaces\tracer.py
def native(self) -> Any:
    """Return the backend's span object, if the tracer created one."""
    return self._native

to_dict

to_dict() -> dict[str, Any]

Return a JSON-safe representation of the span.

Source code in src\windlass\interfaces\tracer.py
def to_dict(self) -> dict[str, Any]:
    """Return a JSON-safe representation of the span."""
    return {
        "id": self.id,
        "trace_id": self.trace_id,
        "parent_id": self.parent_id,
        "name": self.name,
        "kind": self.kind,
        "duration_ms": round(self.duration_ms, 2),
        "metadata": self.metadata,
        "inputs": _clip(self.inputs),
        "outputs": _clip(self.outputs),
        "usage": self.usage.model_dump() if self.usage else None,
        "error": self.error,
    }

Tool

Tool(
    *,
    name: str,
    description: str = "",
    parameters: dict[str, Any] | None = None,
    timeout: float | None = None,
    requires_approval: bool = False,
    **config: Any
)

Bases: Component

Abstract agent-callable capability.

Parameters:

Name Type Description Default
name str

Tool name the model sees. Must match ^[a-zA-Z0-9_-]{1,64}$, which is what the major providers accept.

required
description str

What the tool does. This is prompt text — the model chooses tools based on it, so vague descriptions cause bad calls.

''
parameters dict[str, Any] | None

JSON Schema for the arguments. Derived automatically by the decorator; supply it manually when subclassing.

None
timeout float | None

Seconds before the call is abandoned.

None
requires_approval bool

When True the agent pauses for human approval before invoking it. Use this for anything that spends money or mutates state.

False
**config Any

Tool-specific options.

{}

Attributes:

Name Type Description
description

The model-facing description.

parameters dict[str, Any]

The JSON Schema for arguments.

requires_approval

Whether human-in-the-loop approval is required.

Example

Implementing a stateful tool::

class SearchTool(Tool):
    def __init__(self, client):
        super().__init__(
            name="search",
            description="Search the product catalogue.",
            parameters={
                "type": "object",
                "properties": {"q": {"type": "string"}},
                "required": ["q"],
            },
        )
        self.client = client

    async def acall(self, **kwargs):
        return await self.client.search(kwargs["q"])
Source code in src\windlass\interfaces\tool.py
def __init__(
    self,
    *,
    name: str,
    description: str = "",
    parameters: dict[str, Any] | None = None,
    timeout: float | None = None,
    requires_approval: bool = False,
    **config: Any,
) -> None:
    _validate_name(name)
    super().__init__(
        name=name,
        description=description,
        parameters=parameters,
        timeout=timeout,
        requires_approval=requires_approval,
        **config,
    )
    self.description = description or (self.__doc__ or "").strip().split("\n")[0]
    self.parameters: dict[str, Any] = parameters or {
        "type": "object",
        "properties": {},
    }
    self.timeout = timeout if timeout is not None else DEFAULT_TOOL_TIMEOUT
    self.requires_approval = requires_approval

acall abstractmethod async

acall(**kwargs: Any) -> Any

Execute the tool.

Parameters:

Name Type Description Default
**kwargs Any

Arguments matching :attr:parameters.

{}

Returns:

Type Description
Any

Any JSON-serialisable value. It is rendered to a string for the

Any

model and kept intact on :attr:~windlass.core.types.ToolResult.data.

Raises:

Type Description
Exception

Anything. The agent catches it and reports the failure back to the model rather than crashing the run.

Source code in src\windlass\interfaces\tool.py
@abc.abstractmethod
async def acall(self, **kwargs: Any) -> Any:
    """Execute the tool.

    Args:
        **kwargs: Arguments matching :attr:`parameters`.

    Returns:
        Any JSON-serialisable value. It is rendered to a string for the
        model and kept intact on :attr:`~windlass.core.types.ToolResult.data`.

    Raises:
        Exception: Anything. The agent catches it and reports the failure
            back to the model rather than crashing the run.
    """

schema

schema(*, style: str = 'openai') -> dict[str, Any]

Return the tool definition in a provider's format.

Parameters:

Name Type Description Default
style str

"openai" (the default, also used by Groq and Ollama), "anthropic", or "gemini".

'openai'

Returns:

Type Description
dict[str, Any]

A dict ready to drop into that provider's request.

Raises:

Type Description
ValueError

For an unknown style.

Example

from windlass.tools import tool @tool ... def ping() -> str: ... '''Ping.''' ... return "pong" ping.schema(style="anthropic")["name"] 'ping'

Source code in src\windlass\interfaces\tool.py
def schema(self, *, style: str = "openai") -> dict[str, Any]:
    """Return the tool definition in a provider's format.

    Args:
        style: ``"openai"`` (the default, also used by Groq and Ollama),
            ``"anthropic"``, or ``"gemini"``.

    Returns:
        A dict ready to drop into that provider's request.

    Raises:
        ValueError: For an unknown style.

    Example:
        >>> from windlass.tools import tool
        >>> @tool
        ... def ping() -> str:
        ...     '''Ping.'''
        ...     return "pong"
        >>> ping.schema(style="anthropic")["name"]
        'ping'
    """
    match style:
        case "openai":
            return {
                "type": "function",
                "function": {
                    "name": self.name,
                    "description": self.description,
                    "parameters": self.parameters,
                },
            }
        case "anthropic":
            return {
                "name": self.name,
                "description": self.description,
                "input_schema": self.parameters,
            }
        case "gemini":
            return {
                "name": self.name,
                "description": self.description,
                "parameters": _strip_unsupported(self.parameters),
            }
        case _:
            raise ValueError(
                f"Unknown tool schema style {style!r}; use openai, anthropic or gemini."
            )

ainvoke async

ainvoke(call: ToolCall) -> ToolResult

Execute a model-issued tool call and wrap the outcome.

Failures are captured rather than raised: the agent needs to hand the error text back to the model so it can recover, which is impossible if the exception unwinds the loop.

Parameters:

Name Type Description Default
call ToolCall

The call requested by the model.

required

Returns:

Name Type Description
A ToolResult

class:~windlass.core.types.ToolResult, with is_error set

ToolResult

when the tool raised or timed out.

Performance

Enforces :attr:timeout. A hung tool fails the call, not the run.

Example

import asyncio from windlass.core.types import ToolCall from windlass.tools import tool @tool ... def echo(text: str) -> str: ... '''Echo text.''' ... return text asyncio.run(echo.ainvoke(ToolCall(name="echo", arguments={"text": "hi"}))).content 'hi'

Source code in src\windlass\interfaces\tool.py
async def ainvoke(self, call: ToolCall) -> ToolResult:
    """Execute a model-issued tool call and wrap the outcome.

    Failures are captured rather than raised: the agent needs to hand the
    error text back to the model so it can recover, which is impossible if
    the exception unwinds the loop.

    Args:
        call: The call requested by the model.

    Returns:
        A :class:`~windlass.core.types.ToolResult`, with ``is_error`` set
        when the tool raised or timed out.

    Performance:
        Enforces :attr:`timeout`. A hung tool fails the call, not the run.

    Example:
        >>> import asyncio
        >>> from windlass.core.types import ToolCall
        >>> from windlass.tools import tool
        >>> @tool
        ... def echo(text: str) -> str:
        ...     '''Echo text.'''
        ...     return text
        >>> asyncio.run(echo.ainvoke(ToolCall(name="echo", arguments={"text": "hi"}))).content
        'hi'
    """
    import asyncio

    started = time.perf_counter()
    try:
        value = await asyncio.wait_for(self.acall(**call.arguments), timeout=self.timeout)
    except TimeoutError:
        return ToolResult(
            call_id=call.id,
            name=self.name,
            content=f"Tool {self.name!r} timed out after {self.timeout}s.",
            is_error=True,
            duration_ms=(time.perf_counter() - started) * 1000,
        )
    except Exception as exc:
        self._log.warning("Tool %s failed: %s", self.name, exc)
        return ToolResult(
            call_id=call.id,
            name=self.name,
            content=f"{type(exc).__name__}: {exc}",
            data=None,
            is_error=True,
            duration_ms=(time.perf_counter() - started) * 1000,
        )

    return ToolResult(
        call_id=call.id,
        name=self.name,
        content=render_result(value),
        data=value,
        duration_ms=(time.perf_counter() - started) * 1000,
    )

invoke

invoke(call: ToolCall) -> ToolResult

Blocking :meth:ainvoke.

Source code in src\windlass\interfaces\tool.py
def invoke(self, call: ToolCall) -> ToolResult:
    """Blocking :meth:`ainvoke`."""
    return run_sync(self.ainvoke(call))

arun async

arun(**kwargs: Any) -> ToolResult

Execute the tool directly, outside an agent.

Parameters:

Name Type Description Default
**kwargs Any

Tool arguments.

{}

Returns:

Type Description
ToolResult

The result.

Raises:

Type Description
ToolExecutionError

When the tool fails. Unlike :meth:ainvoke, direct invocation raises — there is no model to recover.

Source code in src\windlass\interfaces\tool.py
async def arun(self, **kwargs: Any) -> ToolResult:
    """Execute the tool directly, outside an agent.

    Args:
        **kwargs: Tool arguments.

    Returns:
        The result.

    Raises:
        ToolExecutionError: When the tool fails. Unlike :meth:`ainvoke`,
            direct invocation raises — there is no model to recover.
    """
    result = await self.ainvoke(ToolCall(name=self.name, arguments=kwargs))
    if result.is_error:
        raise ToolExecutionError(self.name, RuntimeError(result.content))
    return result

run

run(**kwargs: Any) -> ToolResult

Blocking :meth:arun.

Source code in src\windlass\interfaces\tool.py
def run(self, **kwargs: Any) -> ToolResult:
    """Blocking :meth:`arun`."""
    return run_sync(self.arun(**kwargs))

describe

describe() -> dict[str, Any]

Return a JSON-safe summary including the schema.

Source code in src\windlass\interfaces\tool.py
def describe(self) -> dict[str, Any]:
    """Return a JSON-safe summary including the schema."""
    return {
        **super().describe(),
        "description": self.description,
        "parameters": self.parameters,
        "requires_approval": self.requires_approval,
    }

Tracer

Tracer(
    *,
    enabled: bool = True,
    project: str | None = None,
    tags: tuple[str, ...] = (),
    name: str | None = None,
    **config: Any
)

Bases: Component

Abstract tracing backend.

Parameters:

Name Type Description Default
enabled bool

Master switch. Disabled tracers still create spans (so code paths are identical) but never export them.

True
project str | None

Project / session name shown in the backend's UI.

None
tags tuple[str, ...]

Tags applied to every span.

()
name str | None

Component name.

None
**config Any

Backend-specific options.

{}
Example

Implementing a tracer takes one method::

class PrintTracer(Tracer):
    provider_name = "print"

    def start_span(self, span): ...
    def end_span(self, span):
        print(span.to_dict())
Source code in src\windlass\interfaces\tracer.py
def __init__(
    self,
    *,
    enabled: bool = True,
    project: str | None = None,
    tags: tuple[str, ...] = (),
    name: str | None = None,
    **config: Any,
) -> None:
    from windlass.core.config import settings

    super().__init__(
        name=name or self.provider_name,
        enabled=enabled,
        project=project or settings().project,
        tags=tags,
        **config,
    )
    self.enabled = enabled
    self.project = project or settings().project
    self.tags = tuple(tags)
    self._stack: list[Span] = []

start_span abstractmethod

start_span(span: Span) -> None

Called when a span begins.

Parameters:

Name Type Description Default
span Span

The span that just started. Attach a backend object with :meth:Span.attach_native if the backend has one.

required
Source code in src\windlass\interfaces\tracer.py
@abc.abstractmethod
def start_span(self, span: Span) -> None:
    """Called when a span begins.

    Args:
        span: The span that just started. Attach a backend object with
            :meth:`Span.attach_native` if the backend has one.
    """

end_span

end_span(span: Span) -> None

Called when a span ends, successfully or not.

Parameters:

Name Type Description Default
span Span

The finished span.

required
Source code in src\windlass\interfaces\tracer.py
def end_span(self, span: Span) -> None:
    """Called when a span ends, successfully or not.

    Args:
        span: The finished span.
    """

flush

flush() -> None

Force any buffered spans to be exported.

Backends that batch should override this; a process that exits without flushing loses its last traces.

Source code in src\windlass\interfaces\tracer.py
def flush(self) -> None:
    """Force any buffered spans to be exported.

    Backends that batch should override this; a process that exits without
    flushing loses its last traces.
    """

span

span(
    name: str, *, kind: str = "chain", inputs: Any = None, **metadata: Any
) -> Iterator[Span]

Open a span around a block of work.

Spans nest automatically: a span opened inside another becomes its child, which is what produces a readable trace tree from plain code.

Parameters:

Name Type Description Default
name str

Operation name.

required
kind str

One of :data:SPAN_KINDS.

'chain'
inputs Any

The operation's input, recorded immediately.

None
**metadata Any

Extra attributes.

{}

Yields:

Type Description
Span

The live :class:Span.

Raises:

Type Description
Exception

Anything the block raises, after recording it.

Example

from windlass.providers.observability.console import ConsoleTracer with ConsoleTracer(enabled=False).span("work", kind="tool") as s: ... s.set_output(42)

Source code in src\windlass\interfaces\tracer.py
@contextmanager
def span(
    self,
    name: str,
    *,
    kind: str = "chain",
    inputs: Any = None,
    **metadata: Any,
) -> Iterator[Span]:
    """Open a span around a block of work.

    Spans nest automatically: a span opened inside another becomes its
    child, which is what produces a readable trace tree from plain code.

    Args:
        name: Operation name.
        kind: One of :data:`SPAN_KINDS`.
        inputs: The operation's input, recorded immediately.
        **metadata: Extra attributes.

    Yields:
        The live :class:`Span`.

    Raises:
        Exception: Anything the block raises, after recording it.

    Example:
        >>> from windlass.providers.observability.console import ConsoleTracer
        >>> with ConsoleTracer(enabled=False).span("work", kind="tool") as s:
        ...     s.set_output(42)
        <Span tool:work ...>
    """
    parent = self._stack[-1] if self._stack else None
    current = Span(
        name,
        kind=kind,
        parent_id=parent.id if parent else None,
        trace_id=parent.trace_id if parent else None,
        metadata={**dict.fromkeys(self.tags, True), **metadata},
    )
    if inputs is not None:
        current.set_input(inputs)

    self._stack.append(current)
    if self.enabled:
        try:
            self.start_span(current)
        except Exception as exc:
            self._log.debug("Tracer %s failed to start a span: %s", self.name, exc)
    try:
        yield current
    except BaseException as exc:
        current.set_error(exc)
        raise
    finally:
        current.end()
        self._stack.pop()
        if self.enabled:
            try:
                self.end_span(current)
            except Exception as exc:
                self._log.debug("Tracer %s failed to end a span: %s", self.name, exc)

current_span

current_span() -> Span | None

Return the innermost open span, if any.

Source code in src\windlass\interfaces\tracer.py
def current_span(self) -> Span | None:
    """Return the innermost open span, if any."""
    return self._stack[-1] if self._stack else None

event

event(name: str, **fields: Any) -> None

Record a zero-duration event on the current span.

Parameters:

Name Type Description Default
name str

Event name.

required
**fields Any

Event attributes.

{}
Source code in src\windlass\interfaces\tracer.py
def event(self, name: str, **fields: Any) -> None:
    """Record a zero-duration event on the current span.

    Args:
        name: Event name.
        **fields: Event attributes.
    """
    span = self.current_span()
    if span is not None:
        span.metadata.setdefault("events", []).append({"name": name, **fields})

VectorStore

VectorStore(
    *,
    collection: str = "windlass",
    dimensions: int | None = None,
    metric: str = "cosine",
    persist_path: str | None = None,
    name: str | None = None,
    **config: Any
)

Bases: Component

Abstract vector database.

Parameters:

Name Type Description Default
collection str

Name of the collection / index / namespace to use.

'windlass'
dimensions int | None

Vector dimensionality. Required by stores that must create an index up front; inferred on first write by those that can.

None
metric str

Similarity metric — cosine, dot or euclidean.

'cosine'
persist_path str | None

Where an on-disk store should keep its data.

None
name str | None

Component name for traces.

None
**config Any

Store-specific options.

{}

Attributes:

Name Type Description
collection

The active collection name.

metric

The configured similarity metric.

supports_filters bool

Whether metadata filtering is pushed down to the store rather than applied client-side.

supports_hybrid bool

Whether the store has native sparse+dense search.

Example

Implementing a store means four methods::

class MyStore(VectorStore):
    provider_name = "mine"

    async def aadd(self, chunks): ...
    async def asearch(self, vector, k=5, filters=None): ...
    async def adelete(self, ids=None, filters=None): ...
    async def acount(self): ...
Source code in src\windlass\interfaces\vectordb.py
def __init__(
    self,
    *,
    collection: str = "windlass",
    dimensions: int | None = None,
    metric: str = "cosine",
    persist_path: str | None = None,
    name: str | None = None,
    **config: Any,
) -> None:
    if metric not in {"cosine", "dot", "euclidean", "l2"}:
        raise ConfigurationError(
            f"Unsupported metric {metric!r}.",
            hint="Use 'cosine', 'dot' or 'euclidean'.",
        )
    super().__init__(
        name=name or self.provider_name,
        collection=collection,
        dimensions=dimensions,
        metric=metric,
        persist_path=persist_path,
        **config,
    )
    self.collection = collection
    self.dimensions = dimensions
    self.metric = metric
    self.persist_path = persist_path

aadd abstractmethod async

aadd(chunks: Sequence[Chunk]) -> int

Insert or update chunks.

Chunk ids are deterministic, so re-adding the same content must upsert rather than duplicate.

Parameters:

Name Type Description Default
chunks Sequence[Chunk]

Chunks with :attr:~windlass.core.types.Chunk.embedding set.

required

Returns:

Type Description
int

How many chunks were written.

Raises:

Type Description
ConfigurationError

If any chunk is missing its embedding.

ProviderError

For store-side failures.

Source code in src\windlass\interfaces\vectordb.py
@abc.abstractmethod
async def aadd(self, chunks: Sequence[Chunk]) -> int:
    """Insert or update chunks.

    Chunk ids are deterministic, so re-adding the same content must upsert
    rather than duplicate.

    Args:
        chunks: Chunks with :attr:`~windlass.core.types.Chunk.embedding` set.

    Returns:
        How many chunks were written.

    Raises:
        ConfigurationError: If any chunk is missing its embedding.
        ProviderError: For store-side failures.
    """

asearch abstractmethod async

asearch(
    vector: Sequence[float],
    k: int = 5,
    *,
    filters: MetadataFilter | None = None,
    **kwargs: Any
) -> list[ScoredChunk]

Find the k nearest chunks to vector.

Parameters:

Name Type Description Default
vector Sequence[float]

The query embedding.

required
k int

How many results to return.

5
filters MetadataFilter | None

Metadata constraints. Stores without native filtering should call :meth:match_filters client-side.

None
**kwargs Any

Store-specific search options.

{}

Returns:

Type Description
list[ScoredChunk]

Hits sorted by descending score, each with rank set.

Source code in src\windlass\interfaces\vectordb.py
@abc.abstractmethod
async def asearch(
    self,
    vector: Sequence[float],
    k: int = 5,
    *,
    filters: MetadataFilter | None = None,
    **kwargs: Any,
) -> list[ScoredChunk]:
    """Find the ``k`` nearest chunks to ``vector``.

    Args:
        vector: The query embedding.
        k: How many results to return.
        filters: Metadata constraints. Stores without native filtering
            should call :meth:`match_filters` client-side.
        **kwargs: Store-specific search options.

    Returns:
        Hits sorted by descending score, each with ``rank`` set.
    """

adelete abstractmethod async

adelete(
    ids: Sequence[str] | None = None, *, filters: MetadataFilter | None = None
) -> int

Delete chunks by id or by metadata filter.

Parameters:

Name Type Description Default
ids Sequence[str] | None

Chunk ids to remove.

None
filters MetadataFilter | None

Metadata constraints selecting what to remove.

None

Returns:

Type Description
int

How many chunks were deleted.

Raises:

Type Description
ValueError

If neither ids nor filters is given — deleting the entire collection must go through :meth:aclear.

Source code in src\windlass\interfaces\vectordb.py
@abc.abstractmethod
async def adelete(
    self, ids: Sequence[str] | None = None, *, filters: MetadataFilter | None = None
) -> int:
    """Delete chunks by id or by metadata filter.

    Args:
        ids: Chunk ids to remove.
        filters: Metadata constraints selecting what to remove.

    Returns:
        How many chunks were deleted.

    Raises:
        ValueError: If neither ``ids`` nor ``filters`` is given — deleting
            the entire collection must go through :meth:`aclear`.
    """

acount abstractmethod async

acount() -> int

Return how many chunks the collection holds.

Source code in src\windlass\interfaces\vectordb.py
@abc.abstractmethod
async def acount(self) -> int:
    """Return how many chunks the collection holds."""

aget async

aget(ids: Sequence[str]) -> list[Chunk]

Fetch chunks by id.

The default returns []; stores that can look up by id should override it. Parent-child retrieval relies on this to expand a matched child into its parent.

Parameters:

Name Type Description Default
ids Sequence[str]

Chunk ids to fetch.

required

Returns:

Type Description
list[Chunk]

The chunks that exist, in whatever order the store returns them.

Source code in src\windlass\interfaces\vectordb.py
async def aget(self, ids: Sequence[str]) -> list[Chunk]:
    """Fetch chunks by id.

    The default returns ``[]``; stores that can look up by id should
    override it. Parent-child retrieval relies on this to expand a matched
    child into its parent.

    Args:
        ids: Chunk ids to fetch.

    Returns:
        The chunks that exist, in whatever order the store returns them.
    """
    return []

aclear async

aclear() -> None

Remove every chunk from the collection.

The default deletes by listing ids, which is correct but slow; stores with a native truncate should override it.

Source code in src\windlass\interfaces\vectordb.py
async def aclear(self) -> None:
    """Remove every chunk from the collection.

    The default deletes by listing ids, which is correct but slow; stores
    with a native truncate should override it.
    """
    await self.adelete(filters={})

apersist async

apersist() -> None

Flush pending writes to durable storage.

A no-op for stores that write through.

Source code in src\windlass\interfaces\vectordb.py
async def apersist(self) -> None:
    """Flush pending writes to durable storage.

    A no-op for stores that write through.
    """

add

add(chunks: Sequence[Chunk]) -> int

Blocking :meth:aadd.

Source code in src\windlass\interfaces\vectordb.py
def add(self, chunks: Sequence[Chunk]) -> int:
    """Blocking :meth:`aadd`."""
    return run_sync(self.aadd(chunks))

search

search(
    vector: Sequence[float],
    k: int = 5,
    *,
    filters: MetadataFilter | None = None,
    **kwargs: Any
) -> list[ScoredChunk]

Blocking :meth:asearch.

Source code in src\windlass\interfaces\vectordb.py
def search(
    self,
    vector: Sequence[float],
    k: int = 5,
    *,
    filters: MetadataFilter | None = None,
    **kwargs: Any,
) -> list[ScoredChunk]:
    """Blocking :meth:`asearch`."""
    return run_sync(self.asearch(vector, k, filters=filters, **kwargs))

delete

delete(
    ids: Sequence[str] | None = None, *, filters: MetadataFilter | None = None
) -> int

Blocking :meth:adelete.

Source code in src\windlass\interfaces\vectordb.py
def delete(
    self, ids: Sequence[str] | None = None, *, filters: MetadataFilter | None = None
) -> int:
    """Blocking :meth:`adelete`."""
    return run_sync(self.adelete(ids, filters=filters))

count

count() -> int

Blocking :meth:acount.

Source code in src\windlass\interfaces\vectordb.py
def count(self) -> int:
    """Blocking :meth:`acount`."""
    return run_sync(self.acount())

get

get(ids: Sequence[str]) -> list[Chunk]

Blocking :meth:aget.

Source code in src\windlass\interfaces\vectordb.py
def get(self, ids: Sequence[str]) -> list[Chunk]:
    """Blocking :meth:`aget`."""
    return run_sync(self.aget(ids))

clear

clear() -> None

Blocking :meth:aclear.

Source code in src\windlass\interfaces\vectordb.py
def clear(self) -> None:
    """Blocking :meth:`aclear`."""
    run_sync(self.aclear())

persist

persist() -> None

Blocking :meth:apersist.

Source code in src\windlass\interfaces\vectordb.py
def persist(self) -> None:
    """Blocking :meth:`apersist`."""
    run_sync(self.apersist())

validate_embeddings staticmethod

validate_embeddings(chunks: Sequence[Chunk]) -> None

Assert that every chunk carries an embedding.

Parameters:

Name Type Description Default
chunks Sequence[Chunk]

Chunks about to be written.

required

Raises:

Type Description
ConfigurationError

Naming the first chunk that is missing one.

Source code in src\windlass\interfaces\vectordb.py
@staticmethod
def validate_embeddings(chunks: Sequence[Chunk]) -> None:
    """Assert that every chunk carries an embedding.

    Args:
        chunks: Chunks about to be written.

    Raises:
        ConfigurationError: Naming the first chunk that is missing one.
    """
    for chunk in chunks:
        if not chunk.embedding:
            raise ConfigurationError(
                f"Chunk {chunk.id!r} has no embedding.",
                hint="Embed chunks before adding them, or use Pipeline.ingest() "
                "which does it for you.",
                context={"chunk_id": chunk.id},
            )

match_filters staticmethod

match_filters(metadata: dict[str, Any], filters: MetadataFilter | None) -> bool

Evaluate a metadata filter client-side.

Supports plain equality plus the Mongo-style operators $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $contains and $exists. Stores without native filtering use this so that filter semantics are identical across every backend.

Parameters:

Name Type Description Default
metadata dict[str, Any]

The chunk's metadata.

required
filters MetadataFilter | None

The constraints. None or {} matches everything.

required

Returns:

Type Description
bool

True when the metadata satisfies every constraint.

Example

VectorStore.match_filters({"year": 2024}, {"year": {"$gte": 2020}}) True VectorStore.match_filters({"tag": "a"}, {"tag": {"$in": ["b"]}}) False

Source code in src\windlass\interfaces\vectordb.py
@staticmethod
def match_filters(metadata: dict[str, Any], filters: MetadataFilter | None) -> bool:
    """Evaluate a metadata filter client-side.

    Supports plain equality plus the Mongo-style operators ``$eq``, ``$ne``,
    ``$gt``, ``$gte``, ``$lt``, ``$lte``, ``$in``, ``$nin``, ``$contains``
    and ``$exists``. Stores without native filtering use this so that filter
    semantics are identical across every backend.

    Args:
        metadata: The chunk's metadata.
        filters: The constraints. ``None`` or ``{}`` matches everything.

    Returns:
        True when the metadata satisfies every constraint.

    Example:
        >>> VectorStore.match_filters({"year": 2024}, {"year": {"$gte": 2020}})
        True
        >>> VectorStore.match_filters({"tag": "a"}, {"tag": {"$in": ["b"]}})
        False
    """
    if not filters:
        return True
    for key, condition in filters.items():
        value = metadata.get(key)
        if not isinstance(condition, dict):
            if value != condition:
                return False
            continue
        for op, operand in condition.items():
            if not _apply_operator(value, op, operand):
                return False
    return True

rank staticmethod

rank(hits: list[ScoredChunk]) -> list[ScoredChunk]

Sort hits by descending score and assign 1-based ranks.

Parameters:

Name Type Description Default
hits list[ScoredChunk]

Unordered hits.

required

Returns:

Type Description
list[ScoredChunk]

The same objects, sorted and with rank populated.

Source code in src\windlass\interfaces\vectordb.py
@staticmethod
def rank(hits: list[ScoredChunk]) -> list[ScoredChunk]:
    """Sort hits by descending score and assign 1-based ranks.

    Args:
        hits: Unordered hits.

    Returns:
        The same objects, sorted and with ``rank`` populated.
    """
    hits.sort(key=lambda h: h.score, reverse=True)
    for position, hit in enumerate(hits, start=1):
        hit.rank = position
    return hits

FunctionTool

FunctionTool(
    fn: Callable[..., Any],
    *,
    name: str | None = None,
    description: str | None = None,
    parameters: dict[str, Any] | None = None,
    timeout: float | None = None,
    requires_approval: bool = False,
    **config: Any
)

Bases: Tool

A :class:~windlass.interfaces.tool.Tool backed by a Python function.

Normally produced by the :func:tool decorator rather than constructed directly.

Parameters:

Name Type Description Default
fn Callable[..., Any]

The function to wrap. May be sync or async.

required
name str | None

Tool name. Defaults to the function's name.

None
description str | None

Model-facing description. Defaults to the docstring summary.

None
parameters dict[str, Any] | None

JSON Schema override. Derived from the signature by default.

None
timeout float | None

Seconds before the call is abandoned.

None
requires_approval bool

Pause for human approval before running.

False
**config Any

Forwarded to :class:~windlass.interfaces.tool.Tool.

{}

Attributes:

Name Type Description
fn

The wrapped function, still callable directly.

is_async

Whether the function is a coroutine function.

Example

def add(a: int, b: int) -> int: ... '''Add two integers.''' ... return a + b t = FunctionTool(add) t.run(a=2, b=3).data 5

Source code in src\windlass\tools\__init__.py
def __init__(
    self,
    fn: Callable[..., Any],
    *,
    name: str | None = None,
    description: str | None = None,
    parameters: dict[str, Any] | None = None,
    timeout: float | None = None,
    requires_approval: bool = False,
    **config: Any,
) -> None:
    summary, _ = parse_docstring(inspect.getdoc(fn))
    super().__init__(
        name=name or fn.__name__,
        description=description or summary or f"Call {fn.__name__}.",
        parameters=parameters or function_schema(fn),
        timeout=timeout,
        requires_approval=requires_approval,
        **config,
    )
    self.fn = fn
    self.is_async = asyncio.iscoroutinefunction(fn)
    self.__doc__ = fn.__doc__
    self.__name__ = getattr(fn, "__name__", self.name)

acall async

acall(**kwargs: Any) -> Any

Invoke the wrapped function.

Sync functions run on a worker thread, so a blocking HTTP call inside a tool cannot stall the agent's event loop.

Parameters:

Name Type Description Default
**kwargs Any

Arguments matching the schema.

{}

Returns:

Type Description
Any

The function's return value.

Raises:

Type Description
ValidationError

When a required argument is missing.

Exception

Anything the function raises.

Source code in src\windlass\tools\__init__.py
async def acall(self, **kwargs: Any) -> Any:
    """Invoke the wrapped function.

    Sync functions run on a worker thread, so a blocking HTTP call inside a
    tool cannot stall the agent's event loop.

    Args:
        **kwargs: Arguments matching the schema.

    Returns:
        The function's return value.

    Raises:
        ValidationError: When a required argument is missing.
        Exception: Anything the function raises.
    """
    self._validate(kwargs)
    if self.is_async:
        return await self.fn(**kwargs)
    return await to_thread(self.fn, **kwargs)

native

native() -> Any

Return the wrapped function.

Source code in src\windlass\tools\__init__.py
def native(self) -> Any:
    """Return the wrapped function."""
    return self.fn

ToolRegistry

ToolRegistry(tools: Sequence[Tool] | None = None)

The set of tools bound to one agent.

Holds tools by name, renders provider-specific schemas, and executes calls — in parallel when the model requests several at once, which is where a lot of agent latency quietly hides.

Parameters:

Name Type Description Default
tools Sequence[Tool] | None

Initial tools to bind.

None

Attributes:

Name Type Description
tools dict[str, Tool]

Bound tools, keyed by name.

Example

@tool ... def double(x: int) -> int: ... '''Double a number.''' ... return x * 2 registry = ToolRegistry([double]) registry.execute(ToolCall(name="double", arguments={"x": 4})).data 8

Source code in src\windlass\tools\__init__.py
def __init__(self, tools: Sequence[Tool] | None = None) -> None:
    self.tools: dict[str, Tool] = {}
    self._lock = threading.RLock()
    for item in tools or ():
        self.add(item)

add

add(item: Tool | Callable[..., Any]) -> Tool

Bind a tool.

Parameters:

Name Type Description Default
item Tool | Callable[..., Any]

A :class:~windlass.interfaces.tool.Tool, or a plain function which is wrapped automatically.

required

Returns:

Type Description
Tool

The bound tool.

Raises:

Type Description
TypeError

If item is neither a tool nor callable.

Source code in src\windlass\tools\__init__.py
def add(self, item: Tool | Callable[..., Any]) -> Tool:
    """Bind a tool.

    Args:
        item: A :class:`~windlass.interfaces.tool.Tool`, or a plain function
            which is wrapped automatically.

    Returns:
        The bound tool.

    Raises:
        TypeError: If ``item`` is neither a tool nor callable.
    """
    if isinstance(item, Tool):
        built = item
    elif callable(item):
        built = FunctionTool(item)
    else:
        raise TypeError(f"Expected a Tool or a callable, got {type(item).__name__}.")
    with self._lock:
        if built.name in self.tools and self.tools[built.name] is not built:
            _log.debug("Replacing already-bound tool %r.", built.name)
        self.tools[built.name] = built
    return built

extend

extend(items: Sequence[Tool | Callable[..., Any]]) -> ToolRegistry

Bind several tools and return self.

Source code in src\windlass\tools\__init__.py
def extend(self, items: Sequence[Tool | Callable[..., Any]]) -> ToolRegistry:
    """Bind several tools and return ``self``."""
    for item in items:
        self.add(item)
    return self

remove

remove(name: str) -> None

Unbind a tool. No-op when it is not bound.

Source code in src\windlass\tools\__init__.py
def remove(self, name: str) -> None:
    """Unbind a tool. No-op when it is not bound."""
    with self._lock:
        self.tools.pop(name, None)

get

get(name: str) -> Tool

Return a bound tool.

Parameters:

Name Type Description Default
name str

Tool name.

required

Returns:

Type Description
Tool

The tool.

Raises:

Type Description
ToolNotFoundError

When no tool of that name is bound. The message lists what is available.

Source code in src\windlass\tools\__init__.py
def get(self, name: str) -> Tool:
    """Return a bound tool.

    Args:
        name: Tool name.

    Returns:
        The tool.

    Raises:
        ToolNotFoundError: When no tool of that name is bound. The message
            lists what is available.
    """
    with self._lock:
        found = self.tools.get(name)
        if found is None:
            raise ToolNotFoundError(name, list(self.tools))
        return found

names

names() -> list[str]

Return the bound tool names, sorted.

Source code in src\windlass\tools\__init__.py
def names(self) -> list[str]:
    """Return the bound tool names, sorted."""
    with self._lock:
        return sorted(self.tools)

schemas

schemas(style: str = 'openai') -> list[dict[str, Any]]

Return every bound tool's definition in a provider's format.

Parameters:

Name Type Description Default
style str

"openai", "anthropic" or "gemini".

'openai'

Returns:

Type Description
list[dict[str, Any]]

Tool definitions, or [] when nothing is bound — which is what

list[dict[str, Any]]

lets an agent pass tools=None cleanly.

Source code in src\windlass\tools\__init__.py
def schemas(self, style: str = "openai") -> list[dict[str, Any]]:
    """Return every bound tool's definition in a provider's format.

    Args:
        style: ``"openai"``, ``"anthropic"`` or ``"gemini"``.

    Returns:
        Tool definitions, or ``[]`` when nothing is bound — which is what
        lets an agent pass ``tools=None`` cleanly.
    """
    with self._lock:
        return [t.schema(style=style) for t in self.tools.values()]

aexecute async

aexecute(call: ToolCall) -> ToolResult

Execute one tool call.

An unknown tool produces an error :class:~windlass.core.types.ToolResult rather than an exception, so the agent can tell the model it hallucinated a tool and let it recover.

Parameters:

Name Type Description Default
call ToolCall

The call to execute.

required

Returns:

Type Description
ToolResult

The result.

Source code in src\windlass\tools\__init__.py
async def aexecute(self, call: ToolCall) -> ToolResult:
    """Execute one tool call.

    An unknown tool produces an error :class:`~windlass.core.types.ToolResult`
    rather than an exception, so the agent can tell the model it hallucinated
    a tool and let it recover.

    Args:
        call: The call to execute.

    Returns:
        The result.
    """
    try:
        target = self.get(call.name)
    except ToolNotFoundError as exc:
        return ToolResult(
            call_id=call.id,
            name=call.name,
            content=str(exc),
            is_error=True,
        )
    return await target.ainvoke(call)

execute

execute(call: ToolCall) -> ToolResult

Blocking :meth:aexecute.

Source code in src\windlass\tools\__init__.py
def execute(self, call: ToolCall) -> ToolResult:
    """Blocking :meth:`aexecute`."""
    from windlass.core.concurrency import run_sync

    return run_sync(self.aexecute(call))

aexecute_many async

aexecute_many(
    calls: Sequence[ToolCall], *, parallel: bool = True, limit: int = 8
) -> list[ToolResult]

Execute several tool calls.

Parameters:

Name Type Description Default
calls Sequence[ToolCall]

The calls to execute.

required
parallel bool

Run them concurrently. Models routinely request three or four independent lookups in one turn; running those serially triples the user's wait for no reason.

True
limit int

Maximum concurrent executions.

8

Returns:

Type Description
list[ToolResult]

Results in the same order as calls.

Example

import asyncio @tool ... def echo(text: str) -> str: ... '''Echo text.''' ... return text registry = ToolRegistry([echo]) calls = [ToolCall(name="echo", arguments={"text": t}) for t in "ab"][r.content for r in asyncio.run(registry.aexecute_many(calls))] ['a', 'b']

Source code in src\windlass\tools\__init__.py
async def aexecute_many(
    self, calls: Sequence[ToolCall], *, parallel: bool = True, limit: int = 8
) -> list[ToolResult]:
    """Execute several tool calls.

    Args:
        calls: The calls to execute.
        parallel: Run them concurrently. Models routinely request three or
            four independent lookups in one turn; running those serially
            triples the user's wait for no reason.
        limit: Maximum concurrent executions.

    Returns:
        Results in the same order as ``calls``.

    Example:
        >>> import asyncio
        >>> @tool
        ... def echo(text: str) -> str:
        ...     '''Echo text.'''
        ...     return text
        >>> registry = ToolRegistry([echo])
        >>> calls = [ToolCall(name="echo", arguments={"text": t}) for t in "ab"]
        >>> [r.content for r in asyncio.run(registry.aexecute_many(calls))]
        ['a', 'b']
    """
    if not calls:
        return []
    if not parallel or len(calls) == 1:
        return [await self.aexecute(call) for call in calls]
    return await gather_bounded([self.aexecute(c) for c in calls], limit=limit)

execute_many

execute_many(calls: Sequence[ToolCall], *, parallel: bool = True) -> list[ToolResult]

Blocking :meth:aexecute_many.

Source code in src\windlass\tools\__init__.py
def execute_many(self, calls: Sequence[ToolCall], *, parallel: bool = True) -> list[ToolResult]:
    """Blocking :meth:`aexecute_many`."""
    from windlass.core.concurrency import run_sync

    return run_sync(self.aexecute_many(calls, parallel=parallel))

needs_approval

needs_approval(calls: Sequence[ToolCall]) -> list[ToolCall]

Return the calls whose tools require human approval.

Parameters:

Name Type Description Default
calls Sequence[ToolCall]

Calls the model requested.

required

Returns:

Type Description
list[ToolCall]

The subset that must be approved before running.

Source code in src\windlass\tools\__init__.py
def needs_approval(self, calls: Sequence[ToolCall]) -> list[ToolCall]:
    """Return the calls whose tools require human approval.

    Args:
        calls: Calls the model requested.

    Returns:
        The subset that must be approved before running.
    """
    approvals: list[ToolCall] = []
    for call in calls:
        found = self.tools.get(call.name)
        if found is not None and found.requires_approval:
            approvals.append(call)
    return approvals

describe

describe() -> list[dict[str, Any]]

Return a JSON-safe summary of every bound tool.

Source code in src\windlass\tools\__init__.py
def describe(self) -> list[dict[str, Any]]:
    """Return a JSON-safe summary of every bound tool."""
    with self._lock:
        return [t.describe() for t in self.tools.values()]

AgentBuilder

AgentBuilder(container: Container | None = None)

Fluent builder for an agent.

Parameters:

Name Type Description Default
container Container | None

Dependency container. Defaults to a child of the process root.

None
Example

Level 1 — a working agent in two lines, no dependencies::

agent = Windlass.agent().llm("fake", responses=["Hello!"])
print(agent.run("Say hello"))

Level 2 — a real agent::

agent = (Windlass.agent()
         .llm("anthropic", model="claude-sonnet-4-5")
         .tool(search).tool(calculator)
         .system("You are a research assistant. Cite your sources.")
         .memory("summary")
         .guardrails(pii=True, on_violation="redact")
         .checkpoint("sqlite", path="./state.db")
         .max_iterations(15)
         .observe("langfuse"))

Level 3 — a graph you control::

agent = Windlass.agent().llm("openai").tool(search).graph()
graph = agent.native_graph()
graph.add_node("critic", critic_fn)
graph.add_edge("tools", "critic")
agent.recompile()
Source code in src\windlass\agent\builder.py
def __init__(self, container: Container | None = None) -> None:
    self.container = container or root_container().scope()
    self._specs: dict[str, tuple[Any, dict[str, Any]]] = {}
    self._tools: list[Any] = []
    self._mcp_specs: list[tuple[Any, dict[str, Any]]] = []
    self._sub_agents: dict[str, Any] = {}
    self._descriptions: dict[str, str] = {}
    self._options: dict[str, Any] = {
        "system_prompt": None,
        "max_iterations": 10,
        "parallel_tools": True,
        "require_approval": False,
        "max_tool_call_retries": 2,
        "name": "agent",
    }
    self._use_graph = False
    self._runtime: AgentRuntime | None = None

runtime property

runtime: AgentRuntime

The built runtime, constructing it on first access.

llm

llm(spec: Any = None, /, **config: Any) -> Self

Choose the reasoning model.

Parameters:

Name Type Description Default
spec Any

Registry name ("openai", "anthropic", "gemini", "groq", "ollama", "fake"), an :class:~windlass.interfaces.llm.LLM, or a factory. A bare model id like "gpt-4o" is also accepted and mapped to its provider.

None
**config Any

Provider options — model, temperature, api_key, ...

{}

Returns:

Type Description
Self

self.

Example

from windlass import Windlass _ = Windlass.agent().llm("gpt-4o-mini", temperature=0)

Source code in src\windlass\agent\builder.py
def llm(self, spec: Any = None, /, **config: Any) -> Self:
    """Choose the reasoning model.

    Args:
        spec: Registry name (``"openai"``, ``"anthropic"``, ``"gemini"``,
            ``"groq"``, ``"ollama"``, ``"fake"``), an
            :class:`~windlass.interfaces.llm.LLM`, or a factory. A bare model
            id like ``"gpt-4o"`` is also accepted and mapped to its provider.
        **config: Provider options — ``model``, ``temperature``, ``api_key``, ...

    Returns:
        ``self``.

    Example:
        >>> from windlass import Windlass
        >>> _ = Windlass.agent().llm("gpt-4o-mini", temperature=0)
    """
    if isinstance(spec, str):
        provider, model = _split_model(spec)
        if model and "model" not in config:
            config = {**config, "model": model}
        spec = provider
    return self._set("llm", spec, config)

tool

tool(*tools: Any) -> Self

Bind one or more tools.

Parameters:

Name Type Description Default
*tools Any

:class:~windlass.interfaces.tool.Tool instances, functions decorated with :func:~windlass.tools.tool, plain functions (wrapped automatically), or registry names.

()

Returns:

Type Description
Self

self.

Raises:

Type Description
ConfigurationError

When an argument cannot be interpreted as a tool.

Example

from windlass import Windlass, tool @tool ... def now() -> str: ... '''Return the current time.''' ... return "12:00" _ = Windlass.agent().tool(now)

Source code in src\windlass\agent\builder.py
def tool(self, *tools: Any) -> Self:
    """Bind one or more tools.

    Args:
        *tools: :class:`~windlass.interfaces.tool.Tool` instances, functions
            decorated with :func:`~windlass.tools.tool`, plain functions
            (wrapped automatically), or registry names.

    Returns:
        ``self``.

    Raises:
        ConfigurationError: When an argument cannot be interpreted as a tool.

    Example:
        >>> from windlass import Windlass, tool
        >>> @tool
        ... def now() -> str:
        ...     '''Return the current time.'''
        ...     return "12:00"
        >>> _ = Windlass.agent().tool(now)
    """
    from windlass.interfaces.tool import Tool

    for item in tools:
        if item is None:
            continue
        # A Tool subclass is not necessarily callable — only FunctionTool
        # forwards __call__ — so the isinstance check has to come first.
        if not (isinstance(item, Tool | str) or callable(item)):
            raise ConfigurationError(
                f"Cannot use {type(item).__name__} as a tool.",
                hint="Pass a @tool-decorated function, a Tool instance, a plain "
                "callable, or a registered tool name.",
            )
        self._tools.append(item)
    return self._invalidate()

tools

tools(tools: Sequence[Any]) -> Self

Bind a sequence of tools. See :meth:tool.

Source code in src\windlass\agent\builder.py
def tools(self, tools: Sequence[Any]) -> Self:
    """Bind a sequence of tools. See :meth:`tool`."""
    return self.tool(*tools)

mcp

mcp(spec: Any = None, /, **config: Any) -> Self

Connect an MCP server and bind its tools.

Call it repeatedly to connect several servers; their tools are namespaced by server name so identically-named tools do not collide.

Parameters:

Name Type Description Default
spec Any

Registry name ("fastmcp", "static"), an :class:~windlass.interfaces.mcp.MCPClient, or a factory. Omit it for the FastMCP client.

None
**config Any

Transport options — command, args, url, env, server.

{}

Returns:

Type Description
Self

self.

Example

from windlass import Windlass _ = Windlass.agent().mcp(server="fs", command="npx", ... args=["-y", "@modelcontextprotocol/server-filesystem", "."])

Source code in src\windlass\agent\builder.py
def mcp(self, spec: Any = None, /, **config: Any) -> Self:
    """Connect an MCP server and bind its tools.

    Call it repeatedly to connect several servers; their tools are namespaced
    by server name so identically-named tools do not collide.

    Args:
        spec: Registry name (``"fastmcp"``, ``"static"``), an
            :class:`~windlass.interfaces.mcp.MCPClient`, or a factory. Omit it
            for the FastMCP client.
        **config: Transport options — ``command``, ``args``, ``url``, ``env``,
            ``server``.

    Returns:
        ``self``.

    Example:
        >>> from windlass import Windlass
        >>> _ = Windlass.agent().mcp(server="fs", command="npx",
        ...     args=["-y", "@modelcontextprotocol/server-filesystem", "."])
    """
    self._mcp_specs.append((spec if spec is not None else "fastmcp", config))
    return self._invalidate()

memory

memory(spec: Any = None, /, **config: Any) -> Self

Attach conversation memory.

Parameters:

Name Type Description Default
spec Any

Registry name ("window", "buffer", "summary", "vector", "composite"), a :class:~windlass.interfaces.memory.Memory, or a factory. Omit it for a sliding window.

None
**config Any

Memory options.

{}

Returns:

Type Description
Self

self.

Source code in src\windlass\agent\builder.py
def memory(self, spec: Any = None, /, **config: Any) -> Self:
    """Attach conversation memory.

    Args:
        spec: Registry name (``"window"``, ``"buffer"``, ``"summary"``,
            ``"vector"``, ``"composite"``), a
            :class:`~windlass.interfaces.memory.Memory`, or a factory. Omit it
            for a sliding window.
        **config: Memory options.

    Returns:
        ``self``.
    """
    return self._set("memory", spec if spec is not None else "window", config)

guardrails

guardrails(spec: Any = None, /, **config: Any) -> Self

Enable input and output guardrails.

Parameters:

Name Type Description Default
spec Any

Registry name ("rules", "nemo"), a :class:~windlass.interfaces.guardrail.Guardrail, or a factory. Omit it for the dependency-free rule guardrail.

None
**config Any

Policy options.

{}

Returns:

Type Description
Self

self.

Source code in src\windlass\agent\builder.py
def guardrails(self, spec: Any = None, /, **config: Any) -> Self:
    """Enable input and output guardrails.

    Args:
        spec: Registry name (``"rules"``, ``"nemo"``), a
            :class:`~windlass.interfaces.guardrail.Guardrail`, or a factory.
            Omit it for the dependency-free rule guardrail.
        **config: Policy options.

    Returns:
        ``self``.
    """
    return self._set("guardrail", spec if spec is not None else "rules", config)

observe

observe(spec: Any = None, /, **config: Any) -> Self

Enable tracing.

Parameters:

Name Type Description Default
spec Any

Registry name ("console", "langfuse", "langsmith", "memory"), a :class:~windlass.interfaces.tracer.Tracer, or a factory.

None
**config Any

Tracer options.

{}

Returns:

Type Description
Self

self.

Source code in src\windlass\agent\builder.py
def observe(self, spec: Any = None, /, **config: Any) -> Self:
    """Enable tracing.

    Args:
        spec: Registry name (``"console"``, ``"langfuse"``, ``"langsmith"``,
            ``"memory"``), a :class:`~windlass.interfaces.tracer.Tracer`, or a
            factory.
        **config: Tracer options.

    Returns:
        ``self``.
    """
    return self._set("tracer", spec if spec is not None else "console", config)

checkpoint

checkpoint(spec: Any = None, /, **config: Any) -> Self

Enable durable state, resume and human-in-the-loop approval.

Parameters:

Name Type Description Default
spec Any

Registry name ("memory", "sqlite"), a :class:~windlass.agent.checkpoint.Checkpointer, or a factory. Omit it for the in-process store.

None
**config Any

Checkpointer options — path, max_history.

{}

Returns:

Type Description
Self

self.

Note

Approval interrupts require a checkpointer, since a paused run has to be stored somewhere before it can be resumed.

Source code in src\windlass\agent\builder.py
def checkpoint(self, spec: Any = None, /, **config: Any) -> Self:
    """Enable durable state, resume and human-in-the-loop approval.

    Args:
        spec: Registry name (``"memory"``, ``"sqlite"``), a
            :class:`~windlass.agent.checkpoint.Checkpointer`, or a factory.
            Omit it for the in-process store.
        **config: Checkpointer options — ``path``, ``max_history``.

    Returns:
        ``self``.

    Note:
        Approval interrupts require a checkpointer, since a paused run has to
        be stored somewhere before it can be resumed.
    """
    return self._set("checkpointer", spec if spec is not None else "memory", config)

agent

agent(name: str, worker: Any, description: str = '') -> Self

Add a specialist, turning this into a supervisor.

Parameters:

Name Type Description Default
name str

The name the supervisor uses to delegate.

required
worker Any

An agent, builder, or anything with arun.

required
description str

What the specialist is good at. The supervisor routes on this text.

''

Returns:

Type Description
Self

self.

Example

from windlass import Windlass _ = (Windlass.agent().llm("fake") ... .agent("researcher", Windlass.agent().llm("fake"), ... "Finds and summarises source material."))

Source code in src\windlass\agent\builder.py
def agent(self, name: str, worker: Any, description: str = "") -> Self:
    """Add a specialist, turning this into a supervisor.

    Args:
        name: The name the supervisor uses to delegate.
        worker: An agent, builder, or anything with ``arun``.
        description: What the specialist is good at. The supervisor routes on
            this text.

    Returns:
        ``self``.

    Example:
        >>> from windlass import Windlass
        >>> _ = (Windlass.agent().llm("fake")
        ...      .agent("researcher", Windlass.agent().llm("fake"),
        ...             "Finds and summarises source material."))
    """
    self._sub_agents[name] = worker
    if description:
        self._descriptions[name] = description
    return self._invalidate()

system

system(prompt: str) -> Self

Set the system prompt.

Parameters:

Name Type Description Default
prompt str

Instructions prepended to every run.

required

Returns:

Type Description
Self

self.

Source code in src\windlass\agent\builder.py
def system(self, prompt: str) -> Self:
    """Set the system prompt.

    Args:
        prompt: Instructions prepended to every run.

    Returns:
        ``self``.
    """
    self._options["system_prompt"] = prompt
    return self._invalidate()

name

name(value: str) -> Self

Name the agent, for traces and multi-agent routing.

Source code in src\windlass\agent\builder.py
def name(self, value: str) -> Self:
    """Name the agent, for traces and multi-agent routing."""
    self._options["name"] = value
    return self._invalidate()

max_iterations

max_iterations(limit: int) -> Self

Set the reason/act step budget.

Parameters:

Name Type Description Default
limit int

Maximum cycles before :class:~windlass.core.exceptions.MaxIterationsExceeded.

required

Returns:

Type Description
Self

self.

Raises:

Type Description
ValueError

If limit is not positive.

Source code in src\windlass\agent\builder.py
def max_iterations(self, limit: int) -> Self:
    """Set the reason/act step budget.

    Args:
        limit: Maximum cycles before
            :class:`~windlass.core.exceptions.MaxIterationsExceeded`.

    Returns:
        ``self``.

    Raises:
        ValueError: If ``limit`` is not positive.
    """
    if limit <= 0:
        raise ValueError("max_iterations must be positive")
    self._options["max_iterations"] = limit
    return self._invalidate()

tool_call_retries

tool_call_retries(limit: int) -> Self

Set how often a run may recover from an unparseable tool call.

Models occasionally emit a tool call the provider cannot parse — most often by nesting one call inside another's arguments, which the tool-calling protocol has no way to express. Windlass shows the model the problem and lets it retry, the same way it handles an invented tool name.

Parameters:

Name Type Description Default
limit int

Maximum corrections per run. Each one costs an iteration from :meth:max_iterations, so a model that never recovers still terminates. 0 fails on the first malformed call.

required

Returns:

Type Description
Self

self.

Raises:

Type Description
ValueError

If limit is negative.

Example

from windlass import Windlass _ = Windlass.agent().llm("fake").tool_call_retries(3)

Source code in src\windlass\agent\builder.py
def tool_call_retries(self, limit: int) -> Self:
    """Set how often a run may recover from an unparseable tool call.

    Models occasionally emit a tool call the provider cannot parse — most
    often by nesting one call inside another's arguments, which the
    tool-calling protocol has no way to express. Windlass shows the model the
    problem and lets it retry, the same way it handles an invented tool name.

    Args:
        limit: Maximum corrections per run. Each one costs an iteration from
            :meth:`max_iterations`, so a model that never recovers still
            terminates. ``0`` fails on the first malformed call.

    Returns:
        ``self``.

    Raises:
        ValueError: If ``limit`` is negative.

    Example:
        >>> from windlass import Windlass
        >>> _ = Windlass.agent().llm("fake").tool_call_retries(3)
    """
    if limit < 0:
        raise ValueError("tool_call_retries must not be negative")
    self._options["max_tool_call_retries"] = limit
    return self._invalidate()

parallel_tools

parallel_tools(enabled: bool = True) -> Self

Control whether simultaneous tool calls run concurrently.

Parameters:

Name Type Description Default
enabled bool

False forces serial execution — occasionally necessary when tools share mutable state.

True

Returns:

Type Description
Self

self.

Source code in src\windlass\agent\builder.py
def parallel_tools(self, enabled: bool = True) -> Self:
    """Control whether simultaneous tool calls run concurrently.

    Args:
        enabled: False forces serial execution — occasionally necessary when
            tools share mutable state.

    Returns:
        ``self``.
    """
    self._options["parallel_tools"] = enabled
    return self._invalidate()

human_in_the_loop

human_in_the_loop(enabled: bool = True) -> Self

Require human approval before every tool call.

Individual tools can require approval on their own with @tool(requires_approval=True); this turns it on globally.

Parameters:

Name Type Description Default
enabled bool

True to pause before every tool call.

True

Returns:

Type Description
Self

self.

Note

Implies a checkpointer, since a paused run has to be stored. One is enabled automatically if you have not chosen one.

Source code in src\windlass\agent\builder.py
def human_in_the_loop(self, enabled: bool = True) -> Self:
    """Require human approval before *every* tool call.

    Individual tools can require approval on their own with
    ``@tool(requires_approval=True)``; this turns it on globally.

    Args:
        enabled: True to pause before every tool call.

    Returns:
        ``self``.

    Note:
        Implies a checkpointer, since a paused run has to be stored. One is
        enabled automatically if you have not chosen one.
    """
    self._options["require_approval"] = enabled
    if enabled and "checkpointer" not in self._specs:
        self.checkpoint()
    return self._invalidate()

graph

graph(enabled: bool = True) -> Self

Use the LangGraph runtime instead of the built-in loop.

Parameters:

Name Type Description Default
enabled bool

True to compile the agent into a LangGraph state machine.

True

Returns:

Type Description
Self

self.

Raises:

Type Description
MissingDependencyError

At build time, when langgraph is missing.

Source code in src\windlass\agent\builder.py
def graph(self, enabled: bool = True) -> Self:
    """Use the LangGraph runtime instead of the built-in loop.

    Args:
        enabled: True to compile the agent into a LangGraph state machine.

    Returns:
        ``self``.

    Raises:
        MissingDependencyError: At build time, when ``langgraph`` is missing.
    """
    self._use_graph = enabled
    return self._invalidate()

bind

bind(key: str, factory: Any) -> Self

Bind an arbitrary dependency into this builder's container.

Source code in src\windlass\agent\builder.py
def bind(self, key: str, factory: Any) -> Self:
    """Bind an arbitrary dependency into this builder's container."""
    if callable(factory):
        self.container.bind(key, factory)
    else:
        self.container.bind_instance(key, factory)
    return self._invalidate()

build

build() -> AgentRuntime

Resolve every component and construct the runtime.

Called automatically on first use.

Returns:

Name Type Description
An AgentRuntime

class:~windlass.agent.runtime.AgentRuntime, a

AgentRuntime

class:~windlass.agent.graph.LangGraphRuntime, or a

AgentRuntime

class:~windlass.agent.supervisor.Supervisor — whichever the

AgentRuntime

configuration describes.

Raises:

Type Description
ConfigurationError

When a component cannot be constructed.

MissingDependencyError

When a chosen provider's extra is missing.

Source code in src\windlass\agent\builder.py
def build(self) -> AgentRuntime:
    """Resolve every component and construct the runtime.

    Called automatically on first use.

    Returns:
        An :class:`~windlass.agent.runtime.AgentRuntime`, a
        :class:`~windlass.agent.graph.LangGraphRuntime`, or a
        :class:`~windlass.agent.supervisor.Supervisor` — whichever the
        configuration describes.

    Raises:
        ConfigurationError: When a component cannot be constructed.
        MissingDependencyError: When a chosen provider's extra is missing.
    """
    if self._runtime is not None:
        return self._runtime

    from windlass.core.config import settings

    cfg = settings()
    llm = self._resolve("llm", cfg.default_llm)
    registry = ToolRegistry()

    for item in self._tools:
        registry.add(self.container.component("tool", item) if isinstance(item, str) else item)

    for spec, config in self._mcp_specs:
        for remote in self._connect_mcp(spec, config):
            registry.add(remote)

    common: dict[str, Any] = {
        "llm": llm,
        "memory": self._resolve_memory(),
        "guardrail": self._resolve_optional("guardrail"),
        "checkpointer": self._resolve_optional("checkpointer"),
        "tracer": self._resolve_optional("tracer"),
        **self._options,
    }

    if self._sub_agents:
        from windlass.agent.supervisor import Supervisor

        self._runtime = Supervisor(
            agents={k: _built(v) for k, v in self._sub_agents.items()},
            descriptions=self._descriptions,
            tools=list(registry),
            **common,
        )
    elif self._use_graph:
        from windlass.agent.graph import LangGraphRuntime

        self._runtime = LangGraphRuntime(tools=registry, **common)
    else:
        self._runtime = AgentRuntime(tools=registry, **common)

    return self._runtime

run

run(prompt: Any, **kwargs: Any) -> AgentResponse

Run the agent. See :meth:~windlass.agent.runtime.AgentRuntime.run.

Source code in src\windlass\agent\builder.py
def run(self, prompt: Any, **kwargs: Any) -> AgentResponse:
    """Run the agent. See :meth:`~windlass.agent.runtime.AgentRuntime.run`."""
    return self.build().run(prompt, **kwargs)

arun async

arun(prompt: Any, **kwargs: Any) -> AgentResponse

Async :meth:run.

Source code in src\windlass\agent\builder.py
async def arun(self, prompt: Any, **kwargs: Any) -> AgentResponse:
    """Async :meth:`run`."""
    return await self.build().arun(prompt, **kwargs)

stream

stream(prompt: Any, **kwargs: Any) -> Iterator[StreamEvent]

Stream the run. See :meth:~windlass.agent.runtime.AgentRuntime.stream.

Source code in src\windlass\agent\builder.py
def stream(self, prompt: Any, **kwargs: Any) -> Iterator[StreamEvent]:
    """Stream the run. See :meth:`~windlass.agent.runtime.AgentRuntime.stream`."""
    return self.build().stream(prompt, **kwargs)

astream

astream(prompt: Any, **kwargs: Any) -> AsyncIterator[StreamEvent]

Async :meth:stream.

Source code in src\windlass\agent\builder.py
def astream(self, prompt: Any, **kwargs: Any) -> AsyncIterator[StreamEvent]:
    """Async :meth:`stream`."""
    return self.build().astream(prompt, **kwargs)

astream_text

astream_text(prompt: Any, **kwargs: Any) -> AsyncIterator[str]

Stream only the assistant's text.

Source code in src\windlass\agent\builder.py
def astream_text(self, prompt: Any, **kwargs: Any) -> AsyncIterator[str]:
    """Stream only the assistant's text."""
    return self.build().astream_text(prompt, **kwargs)

resume

resume(thread_id: str, **kwargs: Any) -> AgentResponse

Resume an interrupted run.

Source code in src\windlass\agent\builder.py
def resume(self, thread_id: str, **kwargs: Any) -> AgentResponse:
    """Resume an interrupted run."""
    return self.build().resume(thread_id, **kwargs)

aresume async

aresume(thread_id: str, **kwargs: Any) -> AgentResponse

Async :meth:resume.

Source code in src\windlass\agent\builder.py
async def aresume(self, thread_id: str, **kwargs: Any) -> AgentResponse:
    """Async :meth:`resume`."""
    return await self.build().aresume(thread_id, **kwargs)

pending_approvals

pending_approvals(thread_id: str) -> list[Any]

Return the tool calls awaiting approval on a thread.

Source code in src\windlass\agent\builder.py
def pending_approvals(self, thread_id: str) -> list[Any]:
    """Return the tool calls awaiting approval on a thread."""
    return self.build().pending_approvals(thread_id)

state

state(thread_id: str) -> dict[str, Any] | None

Return the latest checkpoint for a thread.

Source code in src\windlass\agent\builder.py
def state(self, thread_id: str) -> dict[str, Any] | None:
    """Return the latest checkpoint for a thread."""
    return self.build().state(thread_id)

history

history(thread_id: str, limit: int = 20) -> list[dict[str, Any]]

Return recent checkpoints for a thread.

Source code in src\windlass\agent\builder.py
def history(self, thread_id: str, limit: int = 20) -> list[dict[str, Any]]:
    """Return recent checkpoints for a thread."""
    return self.build().history(thread_id, limit)

broadcast

broadcast(task: str) -> dict[str, AgentResponse]

Run every specialist on the same task (supervisor only).

Raises:

Type Description
ConfigurationError

When no specialists are configured.

Source code in src\windlass\agent\builder.py
def broadcast(self, task: str) -> dict[str, AgentResponse]:
    """Run every specialist on the same task (supervisor only).

    Raises:
        ConfigurationError: When no specialists are configured.
    """
    runtime = self.build()
    caller = getattr(runtime, "broadcast", None)
    if caller is None:
        raise ConfigurationError(
            "broadcast() needs specialist agents.",
            hint="Add them with .agent('name', worker).",
        )
    return caller(task)

pipeline

pipeline(task: str, order: list[str]) -> AgentResponse

Chain specialists in sequence (supervisor only).

Raises:

Type Description
ConfigurationError

When no specialists are configured.

Source code in src\windlass\agent\builder.py
def pipeline(self, task: str, order: list[str]) -> AgentResponse:
    """Chain specialists in sequence (supervisor only).

    Raises:
        ConfigurationError: When no specialists are configured.
    """
    runtime = self.build()
    caller = getattr(runtime, "pipeline", None)
    if caller is None:
        raise ConfigurationError(
            "pipeline() needs specialist agents.",
            hint="Add them with .agent('name', worker).",
        )
    return caller(task, order)

native_llm

native_llm() -> Any

Return the model provider's own client.

Source code in src\windlass\agent\builder.py
def native_llm(self) -> Any:
    """Return the model provider's own client."""
    return self.build().native_llm()

native_graph

native_graph() -> Any

Return the LangGraph StateGraph.

Raises:

Type Description
AgentError

When the agent was not built with :meth:graph.

Source code in src\windlass\agent\builder.py
def native_graph(self) -> Any:
    """Return the LangGraph ``StateGraph``.

    Raises:
        AgentError: When the agent was not built with :meth:`graph`.
    """
    return self.build().native_graph()

recompile

recompile() -> Any

Recompile the graph after editing it.

Raises:

Type Description
ConfigurationError

When the agent is not graph-backed.

Source code in src\windlass\agent\builder.py
def recompile(self) -> Any:
    """Recompile the graph after editing it.

    Raises:
        ConfigurationError: When the agent is not graph-backed.
    """
    runtime = self.build()
    recompiler = getattr(runtime, "recompile", None)
    if recompiler is None:
        raise ConfigurationError(
            "recompile() only applies to graph-backed agents.",
            hint="Build the agent with .graph().",
        )
    return recompiler()

describe

describe() -> dict[str, Any]

Return a JSON-safe description of the agent.

Source code in src\windlass\agent\builder.py
def describe(self) -> dict[str, Any]:
    """Return a JSON-safe description of the agent."""
    return self.build().describe()

close

close() -> None

Release the model's and MCP clients' resources.

Source code in src\windlass\agent\builder.py
def close(self) -> None:
    """Release the model's and MCP clients' resources."""
    if self._runtime is not None:
        self._runtime.close()

AgentRuntime

AgentRuntime(
    *,
    llm: LLM,
    tools: ToolRegistry | Sequence[Any] | None = None,
    system_prompt: str | None = None,
    max_iterations: int = 10,
    memory: Memory | None = None,
    guardrail: Guardrail | None = None,
    checkpointer: Checkpointer | None = None,
    tracer: Tracer | None = None,
    parallel_tools: bool = True,
    require_approval: bool = False,
    max_tool_call_retries: int = 2,
    name: str = "agent"
)

A tool-calling agent.

Parameters:

Name Type Description Default
llm LLM

The reasoning model. Must support tool calling if tools are bound.

required
tools ToolRegistry | Sequence[Any] | None

Tools the agent may call.

None
system_prompt str | None

Instructions prepended to every run.

None
max_iterations int

Ceiling on reason/act cycles. Prevents a confused agent from looping until your budget runs out.

10
memory Memory | None

Optional conversation memory, keyed by thread_id.

None
guardrail Guardrail | None

Optional input/output policy.

None
checkpointer Checkpointer | None

Optional durable state, enabling resume and time travel.

None
tracer Tracer | None

Observability backend.

None
parallel_tools bool

Execute simultaneous tool calls concurrently.

True
require_approval bool

Pause before every tool call, not just those whose tools declare requires_approval.

False
max_tool_call_retries int

How many times a run may recover from the model emitting an unparseable tool call before giving up. Each recovery costs one iteration from max_iterations, so a model that cannot get it right still terminates. 0 disables recovery and restores the previous behaviour of failing immediately.

2
name str

Agent name, used in traces and multi-agent routing.

'agent'

Attributes:

Name Type Description
tools

The bound :class:~windlass.tools.ToolRegistry.

name

The agent's name.

Raises:

Type Description
AgentError

When tools are bound to a model that cannot call them.

Source code in src\windlass\agent\runtime.py
def __init__(
    self,
    *,
    llm: LLM,
    tools: ToolRegistry | Sequence[Any] | None = None,
    system_prompt: str | None = None,
    max_iterations: int = 10,
    memory: Memory | None = None,
    guardrail: Guardrail | None = None,
    checkpointer: Checkpointer | None = None,
    tracer: Tracer | None = None,
    parallel_tools: bool = True,
    require_approval: bool = False,
    max_tool_call_retries: int = 2,
    name: str = "agent",
) -> None:
    self.llm = llm
    self.tools = tools if isinstance(tools, ToolRegistry) else ToolRegistry(list(tools or []))
    self.system_prompt = system_prompt if system_prompt is not None else DEFAULT_SYSTEM_PROMPT
    self.max_iterations = max(1, max_iterations)
    self.memory = memory
    self.guardrail = guardrail
    self.checkpointer = checkpointer
    self.tracer = tracer or NullTracer()
    self.parallel_tools = parallel_tools
    self.require_approval = require_approval
    self.max_tool_call_retries = max(0, max_tool_call_retries)
    self.name = name

    if len(self.tools) and not llm.supports_tools:
        raise AgentError(
            f"The {llm.name!r} provider does not support tool calling, but "
            f"{len(self.tools)} tool(s) are bound.",
            hint="Use a tool-capable model, or drop the tools.",
            context={"provider": llm.name, "tools": self.tools.names()},
        )

arun async

arun(
    prompt: Any,
    *,
    thread_id: str | None = None,
    max_iterations: int | None = None,
    **llm_kwargs: Any
) -> AgentResponse

Run the agent to completion.

Parameters:

Name Type Description Default
prompt Any

A string, a message, or a full transcript.

required
thread_id str | None

Conversation thread. Required to use memory or checkpointing across calls; generated when omitted.

None
max_iterations int | None

Override for the step budget.

None
**llm_kwargs Any

Per-call model overrides.

{}

Returns:

Type Description
AgentResponse

The answer, transcript, per-step trace and aggregate usage.

Raises:

Type Description
MaxIterationsExceeded

When the budget runs out before an answer.

InterruptedError_

When a tool needs human approval. Resume with :meth:aresume.

GuardrailViolation

When a guardrail blocks the input or output.

AgentError

When the run cannot proceed.

Performance

One model call per iteration, plus tool time. Independent tool calls in the same turn run concurrently, which is usually where the wall clock is won.

Example

import asyncio from windlass import Windlass agent = Windlass.agent().llm("fake", responses=["Done."]) asyncio.run(agent.arun("Say done.")).output 'Done.'

Source code in src\windlass\agent\runtime.py
async def arun(
    self,
    prompt: Any,
    *,
    thread_id: str | None = None,
    max_iterations: int | None = None,
    **llm_kwargs: Any,
) -> AgentResponse:
    """Run the agent to completion.

    Args:
        prompt: A string, a message, or a full transcript.
        thread_id: Conversation thread. Required to use memory or
            checkpointing across calls; generated when omitted.
        max_iterations: Override for the step budget.
        **llm_kwargs: Per-call model overrides.

    Returns:
        The answer, transcript, per-step trace and aggregate usage.

    Raises:
        MaxIterationsExceeded: When the budget runs out before an answer.
        AgentInterrupt: When a tool needs human approval. Resume with
            :meth:`aresume`.
        GuardrailViolation: When a guardrail blocks the input or output.
        AgentError: When the run cannot proceed.

    Performance:
        One model call per iteration, plus tool time. Independent tool calls
        in the same turn run concurrently, which is usually where the wall
        clock is won.

    Example:
        >>> import asyncio
        >>> from windlass import Windlass
        >>> agent = Windlass.agent().llm("fake", responses=["Done."])
        >>> asyncio.run(agent.arun("Say done.")).output
        'Done.'
    """
    thread = thread_id or new_id("thread")
    budget = max_iterations or self.max_iterations
    started = time.perf_counter()

    with self.tracer.span(f"agent.{self.name}", kind="agent", inputs=str(prompt)) as span:
        messages = await self._prepare(prompt, thread)
        steps: list[AgentStep] = []
        usage = Usage(calls=0)

        corrections = 0
        for index in range(budget):
            try:
                completion = await self._think(messages, index, **llm_kwargs)
            except MalformedToolCallError as exc:
                # The model emitted a tool call the provider could not
                # parse — usually by nesting one call inside another's
                # arguments, which the protocol cannot express. That is the
                # same class of mistake as inventing a tool name, so it gets
                # the same treatment: show the model what went wrong and let
                # it try again, rather than ending the run.
                corrections += 1
                if corrections > self.max_tool_call_retries:
                    raise
                _log.warning(
                    "Model produced an unparseable tool call (correction %d/%d): %s",
                    corrections,
                    self.max_tool_call_retries,
                    exc,
                )
                messages.append(Message.user(exc.feedback))
                steps.append(AgentStep(index=index, thought=f"[recovered] {exc}"))
                continue

            usage = usage + completion.usage
            assistant = completion.to_message()
            messages.append(assistant)

            if not completion.tool_calls:
                output = await self._finish(completion.content, thread, prompt, messages)
                steps.append(
                    AgentStep(index=index, thought=completion.content, usage=completion.usage)
                )
                span.set_output(output)
                span.set_usage(usage)
                self._checkpoint(thread, messages, steps, done=True)
                return AgentResponse(
                    output=output,
                    messages=messages,
                    steps=steps,
                    usage=usage,
                    latency_ms=(time.perf_counter() - started) * 1000,
                    thread_id=thread,
                )

            pending = self._approvals(completion.tool_calls)
            if pending:
                self._checkpoint(thread, messages, steps, pending=pending)
                raise AgentInterrupt(
                    f"{len(pending)} tool call(s) need approval: "
                    + ", ".join(c.name for c in pending),
                    payload=[c.model_dump() for c in pending],
                    thread_id=thread,
                )

            results = await self._act(completion.tool_calls)
            messages.extend(Message.tool(r) for r in results)
            steps.append(
                AgentStep(
                    index=index,
                    thought=completion.content,
                    tool_calls=completion.tool_calls,
                    tool_results=results,
                    usage=completion.usage,
                )
            )
            self._checkpoint(thread, messages, steps)

        raise MaxIterationsExceeded(budget)

run

run(
    prompt: Any,
    *,
    thread_id: str | None = None,
    max_iterations: int | None = None,
    **llm_kwargs: Any
) -> AgentResponse

Blocking :meth:arun.

Source code in src\windlass\agent\runtime.py
def run(
    self,
    prompt: Any,
    *,
    thread_id: str | None = None,
    max_iterations: int | None = None,
    **llm_kwargs: Any,
) -> AgentResponse:
    """Blocking :meth:`arun`."""
    return run_sync(
        self.arun(prompt, thread_id=thread_id, max_iterations=max_iterations, **llm_kwargs)
    )

astream async

astream(
    prompt: Any, *, thread_id: str | None = None, **llm_kwargs: Any
) -> AsyncIterator[StreamEvent]

Stream the agent's work as it happens.

You get text deltas as the model produces them, tool_call events when it decides to act, and a final done. That is what a live agent UI needs: users tolerate latency far better when they can see progress.

Parameters:

Name Type Description Default
prompt Any

A string, a message, or a full transcript.

required
thread_id str | None

Conversation thread.

None
**llm_kwargs Any

Per-call model overrides.

{}

Yields:

Type Description
AsyncIterator[StreamEvent]

class:~windlass.core.types.StreamEvent values.

Raises:

Type Description
MaxIterationsExceeded

When the budget runs out.

Source code in src\windlass\agent\runtime.py
async def astream(
    self,
    prompt: Any,
    *,
    thread_id: str | None = None,
    **llm_kwargs: Any,
) -> AsyncIterator[StreamEvent]:
    """Stream the agent's work as it happens.

    You get text deltas as the model produces them, ``tool_call`` events
    when it decides to act, and a final ``done``. That is what a live agent
    UI needs: users tolerate latency far better when they can see progress.

    Args:
        prompt: A string, a message, or a full transcript.
        thread_id: Conversation thread.
        **llm_kwargs: Per-call model overrides.

    Yields:
        :class:`~windlass.core.types.StreamEvent` values.

    Raises:
        MaxIterationsExceeded: When the budget runs out.
    """
    thread = thread_id or new_id("thread")
    messages = await self._prepare(prompt, thread)
    schemas = self._schemas()

    for _ in range(self.max_iterations):
        collected: list[str] = []
        calls: list[ToolCall] = []

        async for event in self.llm.astream(messages, tools=schemas, **llm_kwargs):
            if event.type == "text" and event.delta:
                collected.append(event.delta)
                yield event
            elif event.type == "tool_call" and event.tool_call:
                calls.append(event.tool_call)
                yield event

        text = "".join(collected)
        messages.append(Message(role=Role.ASSISTANT, content=text, tool_calls=calls))

        if not calls:
            await self._finish(text, thread, prompt, messages)
            yield StreamEvent(type="done")
            return

        results = await self._act(calls)
        messages.extend(Message.tool(r) for r in results)

    raise MaxIterationsExceeded(self.max_iterations)

stream

stream(prompt: Any, **kwargs: Any) -> Iterator[StreamEvent]

Blocking :meth:astream.

Source code in src\windlass\agent\runtime.py
def stream(self, prompt: Any, **kwargs: Any) -> Iterator[StreamEvent]:
    """Blocking :meth:`astream`."""
    return iter_sync(self.astream(prompt, **kwargs))

astream_text async

astream_text(prompt: Any, **kwargs: Any) -> AsyncIterator[str]

Stream only the assistant's text — the common case for a chat UI.

Source code in src\windlass\agent\runtime.py
async def astream_text(self, prompt: Any, **kwargs: Any) -> AsyncIterator[str]:
    """Stream only the assistant's text — the common case for a chat UI."""
    async for event in self.astream(prompt, **kwargs):
        if event.type == "text" and event.delta:
            yield event.delta

aresume async

aresume(
    thread_id: str,
    *,
    approved: bool = True,
    feedback: str = "",
    edited_arguments: dict[str, dict[str, Any]] | None = None,
    **llm_kwargs: Any
) -> AgentResponse

Resume a run that paused for human approval.

Parameters:

Name Type Description Default
thread_id str

The thread reported on the :class:AgentInterrupt.

required
approved bool

True to execute the pending calls, False to reject them.

True
feedback str

Message shown to the model when rejecting, so it can try a different approach instead of simply repeating itself.

''
edited_arguments dict[str, dict[str, Any]] | None

Replacement arguments keyed by tool-call id. This is the "human edits the action" case — approve the intent, fix the parameters.

None
**llm_kwargs Any

Per-call model overrides.

{}

Returns:

Type Description
AgentResponse

The completed run.

Raises:

Type Description
AgentError

When there is no checkpoint, or nothing was pending.

Example

from windlass import Windlass, tool, AgentInterrupt @tool(requires_approval=True) ... def deploy(env: str) -> str: ... '''Deploy to an environment.''' ... return f"deployed to {env}" agent = (Windlass.agent() ... .llm("fake", responses=["", "Deployed."], ... tool_calls=[[ToolCall(name="deploy", ... arguments={"env": "prod"})], []]) ... .tool(deploy).checkpoint()) try: ... agent.run("Deploy to prod", thread_id="t1") ... except AgentInterrupt as pause: ... resumed = agent.resume("t1", approved=True) resumed.output 'Deployed.'

Source code in src\windlass\agent\runtime.py
async def aresume(
    self,
    thread_id: str,
    *,
    approved: bool = True,
    feedback: str = "",
    edited_arguments: dict[str, dict[str, Any]] | None = None,
    **llm_kwargs: Any,
) -> AgentResponse:
    """Resume a run that paused for human approval.

    Args:
        thread_id: The thread reported on the :class:`AgentInterrupt`.
        approved: True to execute the pending calls, False to reject them.
        feedback: Message shown to the model when rejecting, so it can try a
            different approach instead of simply repeating itself.
        edited_arguments: Replacement arguments keyed by tool-call id. This
            is the "human edits the action" case — approve the intent, fix
            the parameters.
        **llm_kwargs: Per-call model overrides.

    Returns:
        The completed run.

    Raises:
        AgentError: When there is no checkpoint, or nothing was pending.

    Example:
        >>> from windlass import Windlass, tool, AgentInterrupt
        >>> @tool(requires_approval=True)
        ... def deploy(env: str) -> str:
        ...     '''Deploy to an environment.'''
        ...     return f"deployed to {env}"
        >>> agent = (Windlass.agent()
        ...          .llm("fake", responses=["", "Deployed."],
        ...               tool_calls=[[ToolCall(name="deploy",
        ...                                     arguments={"env": "prod"})], []])
        ...          .tool(deploy).checkpoint())
        >>> try:
        ...     agent.run("Deploy to prod", thread_id="t1")
        ... except AgentInterrupt as pause:
        ...     resumed = agent.resume("t1", approved=True)
        >>> resumed.output
        'Deployed.'
    """
    if self.checkpointer is None:
        raise AgentError(
            "Resuming needs a checkpointer.",
            hint="Enable one with .checkpoint() on the agent builder.",
        )
    snapshot = self.checkpointer.get(thread_id)
    if snapshot is None:
        raise AgentError(
            f"No checkpoint found for thread {thread_id!r}.",
            context={"threads": self.checkpointer.threads()[:20]},
        )
    pending_raw = snapshot.get("pending") or []
    if not pending_raw:
        raise AgentError(
            f"Thread {thread_id!r} is not waiting for approval.",
            hint="Only interrupted runs can be resumed.",
        )

    messages = [Message.model_validate(m) for m in snapshot.get("messages", [])]
    steps = [AgentStep.model_validate(s) for s in snapshot.get("steps", [])]
    pending = [ToolCall.model_validate(c) for c in pending_raw]

    if approved:
        if edited_arguments:
            pending = [
                call.model_copy(
                    update={"arguments": edited_arguments.get(call.id, call.arguments)}
                )
                for call in pending
            ]
        results = await self._act(pending)
    else:
        from windlass.core.types import ToolResult

        reason = feedback or "A human rejected this action."
        results = [
            ToolResult(call_id=c.id, name=c.name, content=reason, is_error=True)
            for c in pending
        ]

    messages.extend(Message.tool(r) for r in results)
    steps.append(AgentStep(index=len(steps), tool_calls=pending, tool_results=results))
    self._checkpoint(thread_id, messages, steps)

    response = await self.arun(
        messages,
        thread_id=thread_id,
        max_iterations=max(1, self.max_iterations - len(steps)),
        **llm_kwargs,
    )

    # ``arun`` starts a fresh step list, so without this the approved call —
    # the one a human explicitly authorised — is absent from ``steps`` while
    # still present in ``messages``. Anything reading ``steps`` as the audit
    # trail would show a payment that no step accounts for.
    if steps:
        combined = [*steps, *response.steps]
        for index, step in enumerate(combined):
            step.index = index
        response.steps = combined
    return response

resume

resume(thread_id: str, **kwargs: Any) -> AgentResponse

Blocking :meth:aresume.

Source code in src\windlass\agent\runtime.py
def resume(self, thread_id: str, **kwargs: Any) -> AgentResponse:
    """Blocking :meth:`aresume`."""
    return run_sync(self.aresume(thread_id, **kwargs))

pending_approvals

pending_approvals(thread_id: str) -> list[ToolCall]

Return the tool calls awaiting approval on a thread.

Parameters:

Name Type Description Default
thread_id str

The thread to inspect.

required

Returns:

Type Description
list[ToolCall]

The pending calls, or [].

Source code in src\windlass\agent\runtime.py
def pending_approvals(self, thread_id: str) -> list[ToolCall]:
    """Return the tool calls awaiting approval on a thread.

    Args:
        thread_id: The thread to inspect.

    Returns:
        The pending calls, or ``[]``.
    """
    if self.checkpointer is None:
        return []
    snapshot = self.checkpointer.get(thread_id) or {}
    return [ToolCall.model_validate(c) for c in snapshot.get("pending") or []]

state

state(thread_id: str) -> dict[str, Any] | None

Return the latest checkpoint for a thread, or None.

Source code in src\windlass\agent\runtime.py
def state(self, thread_id: str) -> dict[str, Any] | None:
    """Return the latest checkpoint for a thread, or ``None``."""
    return self.checkpointer.get(thread_id) if self.checkpointer else None

history

history(thread_id: str, limit: int = 20) -> list[dict[str, Any]]

Return recent checkpoints for a thread, newest first.

Source code in src\windlass\agent\runtime.py
def history(self, thread_id: str, limit: int = 20) -> list[dict[str, Any]]:
    """Return recent checkpoints for a thread, newest first."""
    return self.checkpointer.history(thread_id, limit) if self.checkpointer else []

native_llm

native_llm() -> Any

Return the model provider's own client.

Source code in src\windlass\agent\runtime.py
def native_llm(self) -> Any:
    """Return the model provider's own client."""
    return self.llm.native()

native_graph

native_graph() -> Any

Return the underlying execution graph.

The built-in runtime has no graph — it is a loop. Use .graph() on the agent builder to get a LangGraph-backed runtime whose native_graph() returns a real StateGraph you can add nodes and edges to.

Raises:

Type Description
AgentError

Always, explaining how to get a real graph.

Source code in src\windlass\agent\runtime.py
def native_graph(self) -> Any:
    """Return the underlying execution graph.

    The built-in runtime has no graph — it is a loop. Use ``.graph()`` on the
    agent builder to get a LangGraph-backed runtime whose ``native_graph()``
    returns a real ``StateGraph`` you can add nodes and edges to.

    Raises:
        AgentError: Always, explaining how to get a real graph.
    """
    raise AgentError(
        "The built-in runtime is a loop, not a graph.",
        hint="Build the agent with .graph() to use LangGraph, then call "
        "native_graph() to add nodes, edges and conditional edges.",
    )

describe

describe() -> dict[str, Any]

Return a JSON-safe description of the agent's configuration.

Source code in src\windlass\agent\runtime.py
def describe(self) -> dict[str, Any]:
    """Return a JSON-safe description of the agent's configuration."""
    parts: dict[str, Any] = {
        "name": self.name,
        "runtime": "builtin",
        "llm": self.llm.describe(),
        "tools": self.tools.describe(),
        "max_iterations": self.max_iterations,
        "parallel_tools": self.parallel_tools,
    }
    for label, component in (
        ("memory", self.memory),
        ("guardrail", self.guardrail),
        ("tracer", self.tracer),
    ):
        if component is not None:
            parts[label] = component.describe()
    if self.checkpointer is not None:
        parts["checkpointer"] = type(self.checkpointer).__name__
    return parts

aclose async

aclose() -> None

Release resources held by the model and any MCP connections.

Source code in src\windlass\agent\runtime.py
async def aclose(self) -> None:
    """Release resources held by the model and any MCP connections."""
    for component in (self.llm, self.tracer):
        close = getattr(component, "aclose", None)
        if close is not None:
            try:
                await close()
            except Exception as exc:
                _log.debug("Closing %s failed: %s", component, exc)

close

close() -> None

Blocking :meth:aclose.

Source code in src\windlass\agent\runtime.py
def close(self) -> None:
    """Blocking :meth:`aclose`."""
    run_sync(self.aclose())

Supervisor

Supervisor(
    *,
    llm: LLM,
    agents: dict[str, Any] | None = None,
    descriptions: dict[str, str] | None = None,
    system_prompt: str | None = None,
    max_iterations: int = 10,
    tools: list[Any] | None = None,
    tracer: Tracer | None = None,
    **kwargs: Any
)

Bases: AgentRuntime

An agent that delegates to specialist agents.

Parameters:

Name Type Description Default
llm LLM

The coordinating model. It only routes and summarises, so a mid-sized model is usually the right call.

required
agents dict[str, Any] | None

Workers, keyed by the name the supervisor will use. Values may be :class:~windlass.agent.runtime.AgentRuntime instances, builders, or anything with arun.

None
descriptions dict[str, str] | None

What each worker is for, keyed by the same names. Falls back to the worker's own description.

None
system_prompt str | None

Override for the routing instructions.

None
max_iterations int

Ceiling on routing rounds.

10
tools list[Any] | None

Tools the supervisor may call itself, alongside its workers.

None
tracer Tracer | None

Observability backend.

None
**kwargs Any

Forwarded to :class:~windlass.agent.runtime.AgentRuntime.

{}

Raises:

Type Description
AgentError

When no workers are supplied.

Performance

Workers requested in the same turn run concurrently, so a supervisor that fans out to three specialists waits for the slowest, not the sum.

Source code in src\windlass\agent\supervisor.py
def __init__(
    self,
    *,
    llm: LLM,
    agents: dict[str, Any] | None = None,
    descriptions: dict[str, str] | None = None,
    system_prompt: str | None = None,
    max_iterations: int = 10,
    tools: list[Any] | None = None,
    tracer: Tracer | None = None,
    **kwargs: Any,
) -> None:
    workers = dict(agents or {})
    if not workers:
        raise AgentError(
            "A supervisor needs at least one specialist agent.",
            hint="Supervisor(llm=..., agents={'researcher': agent, ...})",
        )

    notes = dict(descriptions or {})
    agent_tools: list[Tool] = [
        AgentTool(worker, name=name, description=notes.get(name, _describe(worker, name)))
        for name, worker in workers.items()
    ]

    roster = "\n".join(f"  - {t.name}: {t.description}" for t in agent_tools)
    super().__init__(
        llm=llm,
        tools=[*agent_tools, *(tools or [])],
        system_prompt=system_prompt or SUPERVISOR_PROMPT.format(roster=roster),
        max_iterations=max_iterations,
        tracer=tracer or NullTracer(),
        name=kwargs.pop("name", "supervisor"),
        **kwargs,
    )
    self.agents = workers

add_agent

add_agent(name: str, agent: Any, description: str = '') -> Supervisor

Add a specialist after construction.

Parameters:

Name Type Description Default
name str

The name the supervisor will use.

required
agent Any

The worker.

required
description str

What it is good at.

''

Returns:

Type Description
Supervisor

self.

Source code in src\windlass\agent\supervisor.py
def add_agent(self, name: str, agent: Any, description: str = "") -> Supervisor:
    """Add a specialist after construction.

    Args:
        name: The name the supervisor will use.
        agent: The worker.
        description: What it is good at.

    Returns:
        ``self``.
    """
    self.agents[name] = agent
    self.tools.add(
        AgentTool(agent, name=name, description=description or _describe(agent, name))
    )
    return self

abroadcast async

abroadcast(task: str) -> dict[str, AgentResponse]

Run every specialist on the same task, concurrently.

Useful for ensembling — ask three specialists the same question and compare — and for fan-out work where every worker has something to contribute.

Parameters:

Name Type Description Default
task str

The task to send to every worker.

required

Returns:

Type Description
dict[str, AgentResponse]

A {worker_name: response} mapping. Workers that fail are

dict[str, AgentResponse]

reported as an error response rather than raising, so one failure

dict[str, AgentResponse]

does not lose the others' work.

Example

import asyncio from windlass import Windlass boss = Supervisor( ... llm=Windlass.llm("fake"), ... agents={"a": Windlass.agent().llm("fake", responses=["A"])}, ... ) asyncio.run(boss.abroadcast("go"))["a"].output 'A'

Source code in src\windlass\agent\supervisor.py
async def abroadcast(self, task: str) -> dict[str, AgentResponse]:
    """Run every specialist on the same task, concurrently.

    Useful for ensembling — ask three specialists the same question and
    compare — and for fan-out work where every worker has something to
    contribute.

    Args:
        task: The task to send to every worker.

    Returns:
        A ``{worker_name: response}`` mapping. Workers that fail are
        reported as an error response rather than raising, so one failure
        does not lose the others' work.

    Example:
        >>> import asyncio
        >>> from windlass import Windlass
        >>> boss = Supervisor(
        ...     llm=Windlass.llm("fake"),
        ...     agents={"a": Windlass.agent().llm("fake", responses=["A"])},
        ... )
        >>> asyncio.run(boss.abroadcast("go"))["a"].output
        'A'
    """
    names = list(self.agents)

    async def _run(name: str) -> AgentResponse:
        worker = self.agents[name]
        runner = getattr(worker, "arun", None)
        if runner is None:
            raise AgentError(f"{name!r} is not runnable.")
        return await runner(task)

    started = time.perf_counter()
    outcomes = await gather_bounded(
        [_run(n) for n in names], limit=len(names), return_exceptions=True
    )

    results: dict[str, AgentResponse] = {}
    for name, outcome in zip(names, outcomes, strict=True):
        if isinstance(outcome, BaseException):
            _log.warning("Specialist %s failed: %s", name, outcome)
            results[name] = AgentResponse(
                output=f"{name} failed: {outcome}",
                metadata={"error": True, "agent": name},
            )
        else:
            results[name] = outcome
    _log.debug(
        "Broadcast to %d specialists in %.2fs", len(names), time.perf_counter() - started
    )
    return results

broadcast

broadcast(task: str) -> dict[str, AgentResponse]

Blocking :meth:abroadcast.

Source code in src\windlass\agent\supervisor.py
def broadcast(self, task: str) -> dict[str, AgentResponse]:
    """Blocking :meth:`abroadcast`."""
    return run_sync(self.abroadcast(task))

apipeline async

apipeline(task: str, order: list[str]) -> AgentResponse

Chain specialists in sequence, each seeing the previous output.

The other multi-agent shape: research → draft → edit, where each stage genuinely depends on the last.

Parameters:

Name Type Description Default
task str

The initial task.

required
order list[str]

Worker names, in execution order.

required

Returns:

Type Description
AgentResponse

The final worker's response, with every stage recorded in

AgentResponse

attr:~windlass.core.types.AgentResponse.steps.

Raises:

Type Description
AgentError

When a named worker does not exist.

Example

import asyncio from windlass import Windlass boss = Supervisor( ... llm=Windlass.llm("fake"), ... agents={ ... "draft": Windlass.agent().llm("fake", responses=["a draft"]), ... "edit": Windlass.agent().llm("fake", responses=["an edit"]), ... }, ... ) asyncio.run(boss.apipeline("write", ["draft", "edit"])).output 'an edit'

Source code in src\windlass\agent\supervisor.py
async def apipeline(self, task: str, order: list[str]) -> AgentResponse:
    """Chain specialists in sequence, each seeing the previous output.

    The other multi-agent shape: research → draft → edit, where each stage
    genuinely depends on the last.

    Args:
        task: The initial task.
        order: Worker names, in execution order.

    Returns:
        The final worker's response, with every stage recorded in
        :attr:`~windlass.core.types.AgentResponse.steps`.

    Raises:
        AgentError: When a named worker does not exist.

    Example:
        >>> import asyncio
        >>> from windlass import Windlass
        >>> boss = Supervisor(
        ...     llm=Windlass.llm("fake"),
        ...     agents={
        ...         "draft": Windlass.agent().llm("fake", responses=["a draft"]),
        ...         "edit": Windlass.agent().llm("fake", responses=["an edit"]),
        ...     },
        ... )
        >>> asyncio.run(boss.apipeline("write", ["draft", "edit"])).output
        'an edit'
    """
    missing = [n for n in order if n not in self.agents]
    if missing:
        raise AgentError(
            f"Unknown specialist(s): {', '.join(missing)}.",
            context={"available": sorted(self.agents)},
        )

    started = time.perf_counter()
    thread = new_id("pipeline")
    current = task
    steps: list[AgentStep] = []
    transcript: list[Message] = [Message.user(task)]
    usage = Usage(calls=0)

    for index, name in enumerate(order):
        worker = self.agents[name]
        response = await worker.arun(current)
        current = response.output
        usage = usage + response.usage
        transcript.append(Message.assistant(current, name=name))
        steps.append(AgentStep(index=index, thought=current, node=name, usage=response.usage))

    return AgentResponse(
        output=current,
        messages=transcript,
        steps=steps,
        usage=usage,
        latency_ms=(time.perf_counter() - started) * 1000,
        thread_id=thread,
        metadata={"pattern": "pipeline", "order": order},
    )

pipeline

pipeline(task: str, order: list[str]) -> AgentResponse

Blocking :meth:apipeline.

Source code in src\windlass\agent\supervisor.py
def pipeline(self, task: str, order: list[str]) -> AgentResponse:
    """Blocking :meth:`apipeline`."""
    return run_sync(self.apipeline(task, order))

describe

describe() -> dict[str, Any]

Return a JSON-safe description including the roster.

Source code in src\windlass\agent\supervisor.py
def describe(self) -> dict[str, Any]:
    """Return a JSON-safe description including the roster."""
    return {
        **super().describe(),
        "pattern": "supervisor",
        "specialists": sorted(self.agents),
    }

RAGBuilder

RAGBuilder(container: Container | None = None)

Fluent builder for a :class:~windlass.rag.pipeline.RAGPipeline.

Every configuration method returns self, and the pipeline methods (:meth:ingest, :meth:ask, :meth:search, ...) are forwarded, so a builder is a usable pipeline — no explicit .build() required.

Parameters:

Name Type Description Default
container Container | None

Dependency container. Defaults to a child of the process root, so application-wide bindings are inherited.

None

Attributes:

Name Type Description
container

The container backing this builder.

Example

Level 1 — defaults all the way down, no dependencies, no API keys::

rag = Windlass.rag()
rag.ingest_text("Windlass unifies the AI ecosystem.")
print(rag.ask("What does Windlass do?"))

Level 2 — configured::

rag = (Windlass.rag()
       .llm("openai", model="gpt-4o-mini", temperature=0)
       .embedding("huggingface", model="BAAI/bge-small-en-v1.5")
       .chunker("recursive", chunk_size=800, overlap=120)
       .vectordb("faiss", persist_path="./index")
       .retriever("hybrid", weights=[0.6, 0.4])
       .top_k(8))

Level 3 — reach past the abstraction::

index = rag.native_store()      # the raw faiss.Index
client = rag.native_llm()       # the raw openai.AsyncOpenAI
Source code in src\windlass\rag\builder.py
def __init__(self, container: Container | None = None) -> None:
    self.container = container or root_container().scope()
    self._specs: dict[str, tuple[Any, dict[str, Any]]] = {}
    self._options: dict[str, Any] = {
        "prompt": DEFAULT_PROMPT,
        "top_k": 5,
        "max_context_tokens": 6000,
        "include_sources": True,
        "answer_without_context": False,
        "batch_size": 64,
    }
    self._score_threshold: float | None = None
    self._pipeline: RAGPipeline | None = None

pipeline property

pipeline: RAGPipeline

The built pipeline, constructing it on first access.

llm

llm(spec: Any = None, /, **config: Any) -> Self

Choose the generation model.

Parameters:

Name Type Description Default
spec Any

Registry name ("openai", "anthropic", "ollama", "fake"), an :class:~windlass.interfaces.llm.LLM instance, or a factory.

None
**config Any

Provider options — model, temperature, max_tokens, api_key, base_url, ...

{}

Returns:

Type Description
Self

self.

Example

from windlass import Windlass _ = Windlass.rag().llm("fake", responses=["hi"])

Source code in src\windlass\rag\builder.py
def llm(self, spec: Any = None, /, **config: Any) -> Self:
    """Choose the generation model.

    Args:
        spec: Registry name (``"openai"``, ``"anthropic"``, ``"ollama"``,
            ``"fake"``), an :class:`~windlass.interfaces.llm.LLM` instance, or
            a factory.
        **config: Provider options — ``model``, ``temperature``,
            ``max_tokens``, ``api_key``, ``base_url``, ...

    Returns:
        ``self``.

    Example:
        >>> from windlass import Windlass
        >>> _ = Windlass.rag().llm("fake", responses=["hi"])
    """
    return self._set("llm", spec, config)

embedding

embedding(spec: Any = None, /, **config: Any) -> Self

Choose the embedding model.

The same model must embed your corpus and your queries — Windlass uses this one for both, which is why swapping it means re-ingesting.

Parameters:

Name Type Description Default
spec Any

Registry name ("huggingface", "openai", "hash"), an :class:~windlass.interfaces.embedding.Embedder, or a factory.

None
**config Any

Provider options — model, batch_size, device, ...

{}

Returns:

Type Description
Self

self.

Source code in src\windlass\rag\builder.py
def embedding(self, spec: Any = None, /, **config: Any) -> Self:
    """Choose the embedding model.

    The same model must embed your corpus and your queries — Windlass uses
    this one for both, which is why swapping it means re-ingesting.

    Args:
        spec: Registry name (``"huggingface"``, ``"openai"``, ``"hash"``),
            an :class:`~windlass.interfaces.embedding.Embedder`, or a factory.
        **config: Provider options — ``model``, ``batch_size``, ``device``, ...

    Returns:
        ``self``.
    """
    return self._set("embedding", spec, config)

vectordb

vectordb(spec: Any = None, /, **config: Any) -> Self

Choose the vector store.

Parameters:

Name Type Description Default
spec Any

Registry name ("memory", "faiss", "chroma", "pinecone"), a :class:~windlass.interfaces.vectordb.VectorStore, or a factory.

None
**config Any

Store options — collection, persist_path, metric, namespace, ... dimensions is filled in from the embedding model when you omit it.

{}

Returns:

Type Description
Self

self.

Source code in src\windlass\rag\builder.py
def vectordb(self, spec: Any = None, /, **config: Any) -> Self:
    """Choose the vector store.

    Args:
        spec: Registry name (``"memory"``, ``"faiss"``, ``"chroma"``,
            ``"pinecone"``), a :class:`~windlass.interfaces.vectordb.VectorStore`,
            or a factory.
        **config: Store options — ``collection``, ``persist_path``,
            ``metric``, ``namespace``, ... ``dimensions`` is filled in from
            the embedding model when you omit it.

    Returns:
        ``self``.
    """
    return self._set("vectordb", spec, config)

chunker

chunker(spec: Any = None, /, **config: Any) -> Self

Choose the chunking strategy.

The highest-leverage knob in a RAG system: it decides what the retriever can find and what the model gets to read.

Parameters:

Name Type Description Default
spec Any

Registry name ("recursive", "semantic", "token", "markdown", "code", "parent_child"), a :class:~windlass.interfaces.chunker.Chunker, or a factory.

None
**config Any

Strategy options — chunk_size, overlap, threshold, parent_size, ...

{}

Returns:

Type Description
Self

self.

Example

from windlass import Windlass _ = Windlass.rag().chunker("recursive", chunk_size=1000, overlap=200)

Source code in src\windlass\rag\builder.py
def chunker(self, spec: Any = None, /, **config: Any) -> Self:
    """Choose the chunking strategy.

    The highest-leverage knob in a RAG system: it decides what the retriever
    can find and what the model gets to read.

    Args:
        spec: Registry name (``"recursive"``, ``"semantic"``, ``"token"``,
            ``"markdown"``, ``"code"``, ``"parent_child"``), a
            :class:`~windlass.interfaces.chunker.Chunker`, or a factory.
        **config: Strategy options — ``chunk_size``, ``overlap``,
            ``threshold``, ``parent_size``, ...

    Returns:
        ``self``.

    Example:
        >>> from windlass import Windlass
        >>> _ = Windlass.rag().chunker("recursive", chunk_size=1000, overlap=200)
    """
    return self._set("chunker", spec, config)

retriever

retriever(spec: Any = None, /, **config: Any) -> Self

Choose the retrieval strategy.

Parameters:

Name Type Description Default
spec Any

Registry name ("vector", "bm25", "hybrid", "contextual"), a :class:~windlass.interfaces.retriever.Retriever, or a factory.

None
**config Any

Strategy options — top_k, diversity, weights, rerank, ...

{}

Returns:

Type Description
Self

self.

Note

"hybrid" builds a dense leg plus a BM25 leg over the same corpus automatically. Pass retrievers=[...] to fuse your own set.

Source code in src\windlass\rag\builder.py
def retriever(self, spec: Any = None, /, **config: Any) -> Self:
    """Choose the retrieval strategy.

    Args:
        spec: Registry name (``"vector"``, ``"bm25"``, ``"hybrid"``,
            ``"contextual"``), a :class:`~windlass.interfaces.retriever.Retriever`,
            or a factory.
        **config: Strategy options — ``top_k``, ``diversity``, ``weights``,
            ``rerank``, ...

    Returns:
        ``self``.

    Note:
        ``"hybrid"`` builds a dense leg plus a BM25 leg over the same corpus
        automatically. Pass ``retrievers=[...]`` to fuse your own set.
    """
    return self._set("retriever", spec, config)

loader

loader(spec: Any = None, /, **config: Any) -> Self

Pin a document loader.

Parameters:

Name Type Description Default
spec Any

Registry name ("pdf", "docx", "web", ...), a :class:~windlass.interfaces.loader.Loader, or a factory.

None
**config Any

Loader options.

{}

Returns:

Type Description
Self

self.

Note

Optional. With no loader configured, Windlass picks one per file from the extension, which is what you want for a mixed corpus.

Source code in src\windlass\rag\builder.py
def loader(self, spec: Any = None, /, **config: Any) -> Self:
    """Pin a document loader.

    Args:
        spec: Registry name (``"pdf"``, ``"docx"``, ``"web"``, ...), a
            :class:`~windlass.interfaces.loader.Loader`, or a factory.
        **config: Loader options.

    Returns:
        ``self``.

    Note:
        Optional. With no loader configured, Windlass picks one per file from
        the extension, which is what you want for a mixed corpus.
    """
    return self._set("loader", spec, config)

preprocessor

preprocessor(spec: Any = None, /, **config: Any) -> Self

Add a preprocessing stage.

Called repeatedly, stages compose into a chain in call order.

Parameters:

Name Type Description Default
spec Any

Registry name ("clean", "pii", "dedup", "language", "metadata", "tables", "ocr"), a :class:~windlass.interfaces.preprocessor.Preprocessor, or a factory. Omit it for the default cleaner.

None
**config Any

Preprocessor options.

{}

Returns:

Type Description
Self

self.

Example

from windlass import Windlass _ = (Windlass.rag() ... .preprocessor("clean", min_length=50) ... .preprocessor("dedup", threshold=0.95) ... .preprocessor("pii", action="redact"))

Source code in src\windlass\rag\builder.py
def preprocessor(self, spec: Any = None, /, **config: Any) -> Self:
    """Add a preprocessing stage.

    Called repeatedly, stages compose into a chain in call order.

    Args:
        spec: Registry name (``"clean"``, ``"pii"``, ``"dedup"``,
            ``"language"``, ``"metadata"``, ``"tables"``, ``"ocr"``), a
            :class:`~windlass.interfaces.preprocessor.Preprocessor`, or a
            factory. Omit it for the default cleaner.
        **config: Preprocessor options.

    Returns:
        ``self``.

    Example:
        >>> from windlass import Windlass
        >>> _ = (Windlass.rag()
        ...      .preprocessor("clean", min_length=50)
        ...      .preprocessor("dedup", threshold=0.95)
        ...      .preprocessor("pii", action="redact"))
    """
    chain = self._specs.setdefault("preprocessor_chain", ([], {}))[0]
    chain.append((spec if spec is not None else "clean", config))
    return self

memory

memory(spec: Any = None, /, **config: Any) -> Self

Attach conversation memory, making ask multi-turn aware.

Parameters:

Name Type Description Default
spec Any

Registry name ("window", "buffer", "summary", "vector"), a :class:~windlass.interfaces.memory.Memory, or a factory. Omit it for a sliding window.

None
**config Any

Memory options.

{}

Returns:

Type Description
Self

self.

Source code in src\windlass\rag\builder.py
def memory(self, spec: Any = None, /, **config: Any) -> Self:
    """Attach conversation memory, making ``ask`` multi-turn aware.

    Args:
        spec: Registry name (``"window"``, ``"buffer"``, ``"summary"``,
            ``"vector"``), a :class:`~windlass.interfaces.memory.Memory`, or a
            factory. Omit it for a sliding window.
        **config: Memory options.

    Returns:
        ``self``.
    """
    return self._set("memory", spec if spec is not None else "window", config)

guardrails

guardrails(spec: Any = None, /, **config: Any) -> Self

Enable input and output guardrails.

Parameters:

Name Type Description Default
spec Any

Registry name ("rules", "nemo"), a :class:~windlass.interfaces.guardrail.Guardrail, or a factory. Omit it for the dependency-free rule guardrail.

None
**config Any

Policy options — pii, injection, banned_words, on_violation, ...

{}

Returns:

Type Description
Self

self.

Example

from windlass import Windlass _ = Windlass.rag().guardrails(pii=True, on_violation="redact")

Source code in src\windlass\rag\builder.py
def guardrails(self, spec: Any = None, /, **config: Any) -> Self:
    """Enable input and output guardrails.

    Args:
        spec: Registry name (``"rules"``, ``"nemo"``), a
            :class:`~windlass.interfaces.guardrail.Guardrail`, or a factory.
            Omit it for the dependency-free rule guardrail.
        **config: Policy options — ``pii``, ``injection``, ``banned_words``,
            ``on_violation``, ...

    Returns:
        ``self``.

    Example:
        >>> from windlass import Windlass
        >>> _ = Windlass.rag().guardrails(pii=True, on_violation="redact")
    """
    return self._set("guardrail", spec if spec is not None else "rules", config)

observe

observe(spec: Any = None, /, **config: Any) -> Self

Enable tracing.

Parameters:

Name Type Description Default
spec Any

Registry name ("console", "langfuse", "langsmith", "memory"), a :class:~windlass.interfaces.tracer.Tracer, or a factory. Omit it for console tracing.

None
**config Any

Tracer options — project, tags, show_io, ...

{}

Returns:

Type Description
Self

self.

Source code in src\windlass\rag\builder.py
def observe(self, spec: Any = None, /, **config: Any) -> Self:
    """Enable tracing.

    Args:
        spec: Registry name (``"console"``, ``"langfuse"``, ``"langsmith"``,
            ``"memory"``), a :class:`~windlass.interfaces.tracer.Tracer`, or a
            factory. Omit it for console tracing.
        **config: Tracer options — ``project``, ``tags``, ``show_io``, ...

    Returns:
        ``self``.
    """
    return self._set("tracer", spec if spec is not None else "console", config)

prompt

prompt(template: str) -> Self

Override the answer prompt template.

Parameters:

Name Type Description Default
template str

A format string containing {context} and {question}.

required

Returns:

Type Description
Self

self.

Raises:

Type Description
ConfigurationError

When a placeholder is missing.

Source code in src\windlass\rag\builder.py
def prompt(self, template: str) -> Self:
    """Override the answer prompt template.

    Args:
        template: A format string containing ``{context}`` and ``{question}``.

    Returns:
        ``self``.

    Raises:
        ConfigurationError: When a placeholder is missing.
    """
    if "{context}" not in template or "{question}" not in template:
        raise ConfigurationError("The prompt template must contain {context} and {question}.")
    self._options["prompt"] = template
    return self._invalidate()

top_k

top_k(k: int) -> Self

Set how many chunks to retrieve per question.

Parameters:

Name Type Description Default
k int

Number of chunks. Must be positive.

required

Returns:

Type Description
Self

self.

Raises:

Type Description
ValueError

If k is not positive.

Source code in src\windlass\rag\builder.py
def top_k(self, k: int) -> Self:
    """Set how many chunks to retrieve per question.

    Args:
        k: Number of chunks. Must be positive.

    Returns:
        ``self``.

    Raises:
        ValueError: If ``k`` is not positive.
    """
    if k <= 0:
        raise ValueError("top_k must be positive")
    self._options["top_k"] = k
    return self._invalidate()

max_context_tokens

max_context_tokens(tokens: int) -> Self

Cap how much retrieved context reaches the prompt.

Parameters:

Name Type Description Default
tokens int

Token ceiling. Lower-ranked chunks are dropped first.

required

Returns:

Type Description
Self

self.

Source code in src\windlass\rag\builder.py
def max_context_tokens(self, tokens: int) -> Self:
    """Cap how much retrieved context reaches the prompt.

    Args:
        tokens: Token ceiling. Lower-ranked chunks are dropped first.

    Returns:
        ``self``.
    """
    self._options["max_context_tokens"] = tokens
    return self._invalidate()

strict

strict(enabled: bool = True) -> Self

Refuse to answer when retrieval finds nothing.

Removes the model's opportunity to fill an empty context with plausible invention, which is more effective than asking it not to.

Parameters:

Name Type Description Default
enabled bool

True to refuse, False to answer from parametric knowledge.

True

Returns:

Type Description
Self

self.

Note

Dense retrieval returns the nearest vectors, however far away they are — so it is almost never empty, and strict mode alone will rarely fire. Pair it with :meth:min_score to make "found nothing relevant" mean what you expect:

Windlass.rag().strict().min_score(0.25)
Source code in src\windlass\rag\builder.py
def strict(self, enabled: bool = True) -> Self:
    """Refuse to answer when retrieval finds nothing.

    Removes the model's opportunity to fill an empty context with plausible
    invention, which is more effective than asking it not to.

    Args:
        enabled: True to refuse, False to answer from parametric knowledge.

    Returns:
        ``self``.

    Note:
        Dense retrieval returns the *nearest* vectors, however far away they
        are — so it is almost never empty, and strict mode alone will rarely
        fire. Pair it with :meth:`min_score` to make "found nothing relevant"
        mean what you expect:

        ```python
        Windlass.rag().strict().min_score(0.25)
        ```
    """
    self._options["answer_without_context"] = not enabled
    return self._invalidate()

min_score

min_score(threshold: float | None) -> Self

Discard retrieval hits scoring below threshold.

This is what turns strict mode into a real relevance gate. Without it, dense retrieval hands back its k nearest neighbours no matter how unrelated they are, and the model is asked to answer from noise.

Parameters:

Name Type Description Default
threshold float | None

Minimum score to keep, or None to keep everything. The right value depends on the retriever: cosine similarity is bounded in [-1, 1] (0.2-0.4 is a common floor), BM25 is unbounded, and hybrid fusion produces small reciprocal-rank scores. Measure on your own corpus rather than guessing.

required

Returns:

Type Description
Self

self.

Example

from windlass import Windlass rag = Windlass.rag().retriever("vector").strict().min_score(0.3) rag.ingest_text("Paris is the capital of France.") 1 rag.ask("Explain quantum chromodynamics.").metadata["no_context"] True

Source code in src\windlass\rag\builder.py
def min_score(self, threshold: float | None) -> Self:
    """Discard retrieval hits scoring below ``threshold``.

    This is what turns strict mode into a real relevance gate. Without it,
    dense retrieval hands back its ``k`` nearest neighbours no matter how
    unrelated they are, and the model is asked to answer from noise.

    Args:
        threshold: Minimum score to keep, or ``None`` to keep everything.
            The right value depends on the retriever: cosine similarity is
            bounded in ``[-1, 1]`` (0.2-0.4 is a common floor), BM25 is
            unbounded, and hybrid fusion produces small reciprocal-rank
            scores. Measure on your own corpus rather than guessing.

    Returns:
        ``self``.

    Example:
        >>> from windlass import Windlass
        >>> rag = Windlass.rag().retriever("vector").strict().min_score(0.3)
        >>> rag.ingest_text("Paris is the capital of France.")
        1
        >>> rag.ask("Explain quantum chromodynamics.").metadata["no_context"]
        True
    """
    self._score_threshold = threshold
    return self._invalidate()

batch_size

batch_size(size: int) -> Self

Set the ingestion batch size.

Parameters:

Name Type Description Default
size int

Chunks embedded and indexed per batch.

required

Returns:

Type Description
Self

self.

Source code in src\windlass\rag\builder.py
def batch_size(self, size: int) -> Self:
    """Set the ingestion batch size.

    Args:
        size: Chunks embedded and indexed per batch.

    Returns:
        ``self``.
    """
    self._options["batch_size"] = size
    return self._invalidate()

bind

bind(key: str, factory: Any) -> Self

Bind an arbitrary dependency into this builder's container.

The escape hatch for wiring something Windlass has no named slot for.

Parameters:

Name Type Description Default
key str

Binding key.

required
factory Any

A factory callable, or a ready-made instance.

required

Returns:

Type Description
Self

self.

Source code in src\windlass\rag\builder.py
def bind(self, key: str, factory: Any) -> Self:
    """Bind an arbitrary dependency into this builder's container.

    The escape hatch for wiring something Windlass has no named slot for.

    Args:
        key: Binding key.
        factory: A factory callable, or a ready-made instance.

    Returns:
        ``self``.
    """
    if callable(factory):
        self.container.bind(key, factory)
    else:
        self.container.bind_instance(key, factory)
    return self._invalidate()

build

build() -> RAGPipeline

Resolve every component and construct the pipeline.

Called automatically on first use. Call it explicitly when you want construction errors (a missing API key, an uninstalled extra) to surface at startup rather than on the first request.

Returns:

Type Description
RAGPipeline

The assembled :class:~windlass.rag.pipeline.RAGPipeline.

Raises:

Type Description
ConfigurationError

When a component cannot be constructed.

MissingDependencyError

When a chosen provider's extra is not installed.

Example

from windlass import Windlass pipeline = Windlass.rag().build() pipeline.top_k 5

Source code in src\windlass\rag\builder.py
def build(self) -> RAGPipeline:
    """Resolve every component and construct the pipeline.

    Called automatically on first use. Call it explicitly when you want
    construction errors (a missing API key, an uninstalled extra) to surface
    at startup rather than on the first request.

    Returns:
        The assembled :class:`~windlass.rag.pipeline.RAGPipeline`.

    Raises:
        ConfigurationError: When a component cannot be constructed.
        MissingDependencyError: When a chosen provider's extra is not installed.

    Example:
        >>> from windlass import Windlass
        >>> pipeline = Windlass.rag().build()
        >>> pipeline.top_k
        5
    """
    if self._pipeline is not None:
        return self._pipeline

    from windlass.core.config import settings

    cfg = settings()

    llm = self._resolve("llm", cfg.default_llm)
    embedder = self._resolve("embedding", cfg.default_embedding)
    vectorstore = self._resolve_store(embedder, cfg.default_vectordb)
    chunker = self._resolve_chunker(embedder, cfg.default_chunker)
    retriever = self._resolve_retriever(
        embedder, vectorstore, llm, chunker, cfg.default_retriever
    )

    self._pipeline = RAGPipeline(
        llm=llm,
        embedder=embedder,
        vectorstore=vectorstore,
        retriever=retriever,
        chunker=chunker,
        loader=self._resolve_optional("loader"),
        preprocessor=self._resolve_preprocessors(llm),
        guardrail=self._resolve_optional("guardrail"),
        memory=self._resolve_memory(embedder),
        tracer=self._resolve_optional("tracer"),
        **self._options,
    )
    return self._pipeline

ingest

ingest(source: Any, **kwargs: Any) -> int

Ingest a source. See :meth:~windlass.rag.pipeline.RAGPipeline.ingest.

Source code in src\windlass\rag\builder.py
def ingest(self, source: Any, **kwargs: Any) -> int:
    """Ingest a source. See :meth:`~windlass.rag.pipeline.RAGPipeline.ingest`."""
    return self.build().ingest(source, **kwargs)

aingest async

aingest(source: Any, **kwargs: Any) -> int

Async :meth:ingest.

Source code in src\windlass\rag\builder.py
async def aingest(self, source: Any, **kwargs: Any) -> int:
    """Async :meth:`ingest`."""
    return await self.build().aingest(source, **kwargs)

ingest_text

ingest_text(text: str, **kwargs: Any) -> int

Ingest a raw string. See :meth:~windlass.rag.pipeline.RAGPipeline.ingest_text.

Source code in src\windlass\rag\builder.py
def ingest_text(self, text: str, **kwargs: Any) -> int:
    """Ingest a raw string. See :meth:`~windlass.rag.pipeline.RAGPipeline.ingest_text`."""
    return self.build().ingest_text(text, **kwargs)

aingest_text async

aingest_text(text: str, **kwargs: Any) -> int

Async :meth:ingest_text.

Source code in src\windlass\rag\builder.py
async def aingest_text(self, text: str, **kwargs: Any) -> int:
    """Async :meth:`ingest_text`."""
    return await self.build().aingest_text(text, **kwargs)

ingest_documents

ingest_documents(documents: Sequence[Any]) -> int

Ingest pre-loaded documents.

Source code in src\windlass\rag\builder.py
def ingest_documents(self, documents: Sequence[Any]) -> int:
    """Ingest pre-loaded documents."""
    return self.build().ingest_documents(documents)

ask

ask(question: str, **kwargs: Any) -> RAGAnswer

Answer a question. See :meth:~windlass.rag.pipeline.RAGPipeline.ask.

Source code in src\windlass\rag\builder.py
def ask(self, question: str, **kwargs: Any) -> RAGAnswer:
    """Answer a question. See :meth:`~windlass.rag.pipeline.RAGPipeline.ask`."""
    return self.build().ask(question, **kwargs)

aask async

aask(question: str, **kwargs: Any) -> RAGAnswer

Async :meth:ask.

Source code in src\windlass\rag\builder.py
async def aask(self, question: str, **kwargs: Any) -> RAGAnswer:
    """Async :meth:`ask`."""
    return await self.build().aask(question, **kwargs)

stream_ask

stream_ask(question: str, **kwargs: Any) -> Iterator[str]

Stream an answer. See :meth:~windlass.rag.pipeline.RAGPipeline.stream_ask.

Source code in src\windlass\rag\builder.py
def stream_ask(self, question: str, **kwargs: Any) -> Iterator[str]:
    """Stream an answer. See :meth:`~windlass.rag.pipeline.RAGPipeline.stream_ask`."""
    return self.build().stream_ask(question, **kwargs)

astream_ask

astream_ask(question: str, **kwargs: Any) -> AsyncIterator[str]

Async :meth:stream_ask.

Source code in src\windlass\rag\builder.py
def astream_ask(self, question: str, **kwargs: Any) -> AsyncIterator[str]:
    """Async :meth:`stream_ask`."""
    return self.build().astream_ask(question, **kwargs)

search

search(
    query: str, k: int | None = None, *, filters: MetadataFilter | None = None
) -> SearchResult

Retrieve without generating. See :meth:~windlass.rag.pipeline.RAGPipeline.search.

Source code in src\windlass\rag\builder.py
def search(
    self, query: str, k: int | None = None, *, filters: MetadataFilter | None = None
) -> SearchResult:
    """Retrieve without generating. See :meth:`~windlass.rag.pipeline.RAGPipeline.search`."""
    return self.build().search(query, k, filters=filters)

asearch async

asearch(
    query: str, k: int | None = None, *, filters: MetadataFilter | None = None
) -> SearchResult

Async :meth:search.

Source code in src\windlass\rag\builder.py
async def asearch(
    self, query: str, k: int | None = None, *, filters: MetadataFilter | None = None
) -> SearchResult:
    """Async :meth:`search`."""
    return await self.build().asearch(query, k, filters=filters)

evaluate

evaluate(questions: Sequence[Any], **kwargs: Any) -> Any

Evaluate the pipeline. See :meth:~windlass.rag.pipeline.RAGPipeline.evaluate.

Source code in src\windlass\rag\builder.py
def evaluate(self, questions: Sequence[Any], **kwargs: Any) -> Any:
    """Evaluate the pipeline. See :meth:`~windlass.rag.pipeline.RAGPipeline.evaluate`."""
    return self.build().evaluate(questions, **kwargs)

aevaluate async

aevaluate(questions: Sequence[Any], **kwargs: Any) -> Any

Async :meth:evaluate.

Source code in src\windlass\rag\builder.py
async def aevaluate(self, questions: Sequence[Any], **kwargs: Any) -> Any:
    """Async :meth:`evaluate`."""
    return await self.build().aevaluate(questions, **kwargs)

count

count() -> int

Return how many chunks are indexed.

Source code in src\windlass\rag\builder.py
def count(self) -> int:
    """Return how many chunks are indexed."""
    return self.build().count()

save

save(directory: str) -> Any

Persist the index. See :meth:~windlass.rag.pipeline.RAGPipeline.save.

Source code in src\windlass\rag\builder.py
def save(self, directory: str) -> Any:
    """Persist the index. See :meth:`~windlass.rag.pipeline.RAGPipeline.save`."""
    return self.build().save(directory)

load

load(directory: str) -> Self

Restore a persisted index and return self.

Source code in src\windlass\rag\builder.py
def load(self, directory: str) -> Self:
    """Restore a persisted index and return ``self``."""
    self.build().load(directory)
    return self

close

close() -> None

Release every component's resources.

Source code in src\windlass\rag\builder.py
def close(self) -> None:
    """Release every component's resources."""
    if self._pipeline is not None:
        self._pipeline.close()

native_llm

native_llm() -> Any

Return the model provider's own client.

Source code in src\windlass\rag\builder.py
def native_llm(self) -> Any:
    """Return the model provider's own client."""
    return self.build().native_llm()

native_store

native_store() -> Any

Return the vector store's own handle.

Source code in src\windlass\rag\builder.py
def native_store(self) -> Any:
    """Return the vector store's own handle."""
    return self.build().native_store()

native_retriever

native_retriever() -> Any

Return the retriever's underlying object.

Source code in src\windlass\rag\builder.py
def native_retriever(self) -> Any:
    """Return the retriever's underlying object."""
    return self.build().native_retriever()

describe

describe() -> dict[str, Any]

Return a JSON-safe description of the wired pipeline.

Source code in src\windlass\rag\builder.py
def describe(self) -> dict[str, Any]:
    """Return a JSON-safe description of the wired pipeline."""
    return self.build().describe()

RAGPipeline

RAGPipeline(
    *,
    llm: LLM,
    embedder: Embedder,
    vectorstore: VectorStore,
    retriever: Retriever | None = None,
    chunker: Chunker | None = None,
    loader: Loader | None = None,
    preprocessor: Preprocessor | None = None,
    guardrail: Guardrail | None = None,
    memory: Memory | None = None,
    tracer: Tracer | None = None,
    prompt: str = DEFAULT_PROMPT,
    top_k: int = 5,
    max_context_tokens: int = 6000,
    include_sources: bool = True,
    answer_without_context: bool = False,
    batch_size: int = 64
)

A fully wired retrieval-augmented generation pipeline.

Parameters:

Name Type Description Default
llm LLM

Model used to generate answers.

required
embedder Embedder

Model used to embed chunks and queries.

required
vectorstore VectorStore

Where chunk vectors are stored.

required
retriever Retriever | None

Retrieval strategy. Defaults to dense search over vectorstore.

None
chunker Chunker | None

Chunking strategy. Defaults to recursive.

None
loader Loader | None

Loader used by :meth:aingest. Defaults to format auto-detection.

None
preprocessor Preprocessor | None

Optional cleaning/enrichment stage.

None
guardrail Guardrail | None

Optional input/output policy.

None
memory Memory | None

Optional conversation memory, making ask multi-turn aware.

None
tracer Tracer | None

Observability backend.

None
prompt str

Prompt template. Must contain {context} and {question}.

DEFAULT_PROMPT
top_k int

How many chunks to retrieve per question.

5
max_context_tokens int

Ceiling on context placed in the prompt. Chunks are dropped from the tail until it fits, so the highest-ranked context always survives.

6000
include_sources bool

Number each context block and ask the model to cite them.

True
answer_without_context bool

When False, retrieval finding nothing returns :data:NO_CONTEXT_ANSWER instead of calling the model. This is the single most effective anti-hallucination setting in a RAG system.

False
batch_size int

Chunks embedded and indexed per batch during ingestion.

64

Attributes:

Name Type Description
llm

The generation model.

retriever

The retrieval strategy.

vectorstore

The vector store.

Raises:

Type Description
ConfigurationError

When a required component is missing or the prompt template lacks its placeholders.

Source code in src\windlass\rag\pipeline.py
def __init__(
    self,
    *,
    llm: LLM,
    embedder: Embedder,
    vectorstore: VectorStore,
    retriever: Retriever | None = None,
    chunker: Chunker | None = None,
    loader: Loader | None = None,
    preprocessor: Preprocessor | None = None,
    guardrail: Guardrail | None = None,
    memory: Memory | None = None,
    tracer: Tracer | None = None,
    prompt: str = DEFAULT_PROMPT,
    top_k: int = 5,
    max_context_tokens: int = 6000,
    include_sources: bool = True,
    answer_without_context: bool = False,
    batch_size: int = 64,
) -> None:
    if llm is None or embedder is None or vectorstore is None:
        raise ConfigurationError(
            "A RAG pipeline needs an llm, an embedder and a vector store.",
            hint="Build it with Windlass.rag(), which supplies working defaults.",
        )
    if "{context}" not in prompt or "{question}" not in prompt:
        raise ConfigurationError(
            "The RAG prompt template must contain {context} and {question}.",
            context={"prompt": prompt[:200]},
        )

    self.llm = llm
    self.embedder = embedder
    self.vectorstore = vectorstore
    self.chunker = chunker
    self.loader = loader
    self.preprocessor = preprocessor
    self.guardrail = guardrail
    self.memory = memory
    self.tracer = tracer or NullTracer()
    self.prompt = prompt
    self.top_k = top_k
    self.max_context_tokens = max_context_tokens
    self.include_sources = include_sources
    self.answer_without_context = answer_without_context
    self.batch_size = batch_size

    if retriever is None:
        from windlass.providers.retrievers.vector import VectorRetriever

        retriever = VectorRetriever(embedder=embedder, vectorstore=vectorstore, top_k=top_k)
    self.retriever = retriever

aingest async

aingest(
    source: Any, *, metadata: dict[str, Any] | None = None, show_progress: bool = False
) -> int

Load, preprocess, chunk, embed and index a source.

Parameters:

Name Type Description Default
source Any

A path, directory, URL, glob, byte payload, or a sequence of any of those. Directories are walked and each file routed to the right loader by extension.

required
metadata dict[str, Any] | None

Extra metadata attached to every resulting chunk — a tenant id, a corpus name, an ingestion date.

None
show_progress bool

Log a line per stage. Useful on long runs.

False

Returns:

Type Description
int

How many chunks were indexed.

Raises:

Type Description
IngestionError

When nothing could be loaded, or a stage fails.

Performance

Loading, embedding and indexing are all batched and concurrent. Re-ingesting unchanged content is cheap: chunk ids are content hashes, so the store upserts rather than duplicating, and a configured embedding cache skips the model entirely.

Example

import asyncio, tempfile, pathlib from windlass import Windlass folder = pathlib.Path(tempfile.mkdtemp()) _ = (folder / "a.txt").write_text("Retrieval augmented generation.") asyncio.run(Windlass.rag().aingest(folder)) >= 1 True

Source code in src\windlass\rag\pipeline.py
async def aingest(
    self,
    source: Any,
    *,
    metadata: dict[str, Any] | None = None,
    show_progress: bool = False,
) -> int:
    """Load, preprocess, chunk, embed and index a source.

    Args:
        source: A path, directory, URL, glob, byte payload, or a sequence of
            any of those. Directories are walked and each file routed to the
            right loader by extension.
        metadata: Extra metadata attached to every resulting chunk — a
            tenant id, a corpus name, an ingestion date.
        show_progress: Log a line per stage. Useful on long runs.

    Returns:
        How many chunks were indexed.

    Raises:
        IngestionError: When nothing could be loaded, or a stage fails.

    Performance:
        Loading, embedding and indexing are all batched and concurrent.
        Re-ingesting unchanged content is cheap: chunk ids are content
        hashes, so the store upserts rather than duplicating, and a
        configured embedding cache skips the model entirely.

    Example:
        >>> import asyncio, tempfile, pathlib
        >>> from windlass import Windlass
        >>> folder = pathlib.Path(tempfile.mkdtemp())
        >>> _ = (folder / "a.txt").write_text("Retrieval augmented generation.")
        >>> asyncio.run(Windlass.rag().aingest(folder)) >= 1
        True
    """
    started = time.perf_counter()
    with self.tracer.span("ingest", kind="ingestion", inputs=str(source)) as span:
        documents = await self._load(source, metadata)
        if show_progress:
            _log.info("Loaded %d documents", len(documents))

        documents = await self._preprocess(documents)
        if not documents:
            raise IngestionError(
                "Every document was dropped during preprocessing.",
                hint="Relax the preprocessor — a min_length or language filter "
                "is probably too strict.",
            )
        if show_progress:
            _log.info("Preprocessed to %d documents", len(documents))

        chunks = await self._chunk(documents)
        if show_progress:
            _log.info("Produced %d chunks", len(chunks))

        indexed = await self._index(chunks)
        span.set_output({"documents": len(documents), "chunks": indexed})
        span.set_metadata(elapsed_ms=round((time.perf_counter() - started) * 1000, 1))

    if show_progress:
        _log.info("Indexed %d chunks in %.1fs", indexed, time.perf_counter() - started)
    return indexed

ingest

ingest(
    source: Any, *, metadata: dict[str, Any] | None = None, show_progress: bool = False
) -> int

Blocking :meth:aingest.

Source code in src\windlass\rag\pipeline.py
def ingest(
    self, source: Any, *, metadata: dict[str, Any] | None = None, show_progress: bool = False
) -> int:
    """Blocking :meth:`aingest`."""
    return run_sync(self.aingest(source, metadata=metadata, show_progress=show_progress))

aingest_documents async

aingest_documents(documents: Sequence[Document]) -> int

Ingest already-loaded documents, skipping the loader.

Parameters:

Name Type Description Default
documents Sequence[Document]

Documents to preprocess, chunk, embed and index.

required

Returns:

Type Description
int

How many chunks were indexed.

Source code in src\windlass\rag\pipeline.py
async def aingest_documents(self, documents: Sequence[Document]) -> int:
    """Ingest already-loaded documents, skipping the loader.

    Args:
        documents: Documents to preprocess, chunk, embed and index.

    Returns:
        How many chunks were indexed.
    """
    processed = await self._preprocess(list(documents))
    chunks = await self._chunk(processed)
    return await self._index(chunks)

ingest_documents

ingest_documents(documents: Sequence[Document]) -> int

Blocking :meth:aingest_documents.

Source code in src\windlass\rag\pipeline.py
def ingest_documents(self, documents: Sequence[Document]) -> int:
    """Blocking :meth:`aingest_documents`."""
    return run_sync(self.aingest_documents(documents))

aingest_text async

aingest_text(
    text: str, *, source: str | None = None, metadata: dict[str, Any] | None = None
) -> int

Ingest a raw string.

Parameters:

Name Type Description Default
text str

The text to index.

required
source str | None

Label recorded as the document's source, used for citation.

None
metadata dict[str, Any] | None

Extra metadata.

None

Returns:

Type Description
int

How many chunks were indexed.

Example

import asyncio from windlass import Windlass asyncio.run(Windlass.rag().aingest_text("Some knowledge.")) >= 1 True

Source code in src\windlass\rag\pipeline.py
async def aingest_text(
    self, text: str, *, source: str | None = None, metadata: dict[str, Any] | None = None
) -> int:
    """Ingest a raw string.

    Args:
        text: The text to index.
        source: Label recorded as the document's source, used for citation.
        metadata: Extra metadata.

    Returns:
        How many chunks were indexed.

    Example:
        >>> import asyncio
        >>> from windlass import Windlass
        >>> asyncio.run(Windlass.rag().aingest_text("Some knowledge.")) >= 1
        True
    """
    document = Document(content=text, source=source, metadata=dict(metadata or {}))
    return await self.aingest_documents([document])

ingest_text

ingest_text(
    text: str, *, source: str | None = None, metadata: dict[str, Any] | None = None
) -> int

Blocking :meth:aingest_text.

Source code in src\windlass\rag\pipeline.py
def ingest_text(
    self, text: str, *, source: str | None = None, metadata: dict[str, Any] | None = None
) -> int:
    """Blocking :meth:`aingest_text`."""
    return run_sync(self.aingest_text(text, source=source, metadata=metadata))

asearch async

asearch(
    query: str, k: int | None = None, *, filters: MetadataFilter | None = None
) -> SearchResult

Retrieve without generating an answer.

Useful for building a search UI, for debugging why an answer was wrong, and for evaluating retrieval quality independently of generation.

Parameters:

Name Type Description Default
query str

The search query.

required
k int | None

Override for :attr:top_k.

None
filters MetadataFilter | None

Metadata constraints.

None

Returns:

Type Description
SearchResult

The ranked results.

Source code in src\windlass\rag\pipeline.py
async def asearch(
    self, query: str, k: int | None = None, *, filters: MetadataFilter | None = None
) -> SearchResult:
    """Retrieve without generating an answer.

    Useful for building a search UI, for debugging why an answer was wrong,
    and for evaluating retrieval quality independently of generation.

    Args:
        query: The search query.
        k: Override for :attr:`top_k`.
        filters: Metadata constraints.

    Returns:
        The ranked results.
    """
    with self.tracer.span("retrieve", kind="retriever", inputs=query) as span:
        result = await self.retriever.aretrieve(query, k or self.top_k, filters=filters)
        span.set_output([h.chunk.content[:200] for h in result.hits])
        span.set_metadata(hits=len(result.hits))
    return result

search

search(
    query: str, k: int | None = None, *, filters: MetadataFilter | None = None
) -> SearchResult

Blocking :meth:asearch.

Source code in src\windlass\rag\pipeline.py
def search(
    self, query: str, k: int | None = None, *, filters: MetadataFilter | None = None
) -> SearchResult:
    """Blocking :meth:`asearch`."""
    return run_sync(self.asearch(query, k, filters=filters))

aask async

aask(
    question: str,
    *,
    k: int | None = None,
    filters: MetadataFilter | None = None,
    thread_id: str = "default",
    **llm_kwargs: Any
) -> RAGAnswer

Answer a question from the indexed corpus.

The full path: input guardrail, retrieval, context assembly, generation, output guardrail, memory update.

Parameters:

Name Type Description Default
question str

The user's question.

required
k int | None

Override for :attr:top_k.

None
filters MetadataFilter | None

Metadata constraints on retrieval.

None
thread_id str

Conversation thread, when memory is configured.

'default'
**llm_kwargs Any

Per-call model overrides (temperature, ...).

{}

Returns:

Type Description
RAGAnswer

The answer, the contexts it was based on, token usage and latency.

Raises:

Type Description
GuardrailViolation

When a guardrail blocks the question or answer.

PipelineError

When generation fails.

Performance

One retrieval (embedding + store query) plus one model call. Context is truncated to :attr:max_context_tokens from the tail, so a large top_k never blows the context window.

Example

import asyncio from windlass import Windlass rag = Windlass.rag() _ = rag.ingest_text("The Eiffel Tower is in Paris.") asyncio.run(rag.aask("Where is the Eiffel Tower?")).question 'Where is the Eiffel Tower?'

Source code in src\windlass\rag\pipeline.py
async def aask(
    self,
    question: str,
    *,
    k: int | None = None,
    filters: MetadataFilter | None = None,
    thread_id: str = "default",
    **llm_kwargs: Any,
) -> RAGAnswer:
    """Answer a question from the indexed corpus.

    The full path: input guardrail, retrieval, context assembly, generation,
    output guardrail, memory update.

    Args:
        question: The user's question.
        k: Override for :attr:`top_k`.
        filters: Metadata constraints on retrieval.
        thread_id: Conversation thread, when memory is configured.
        **llm_kwargs: Per-call model overrides (``temperature``, ...).

    Returns:
        The answer, the contexts it was based on, token usage and latency.

    Raises:
        GuardrailViolation: When a guardrail blocks the question or answer.
        PipelineError: When generation fails.

    Performance:
        One retrieval (embedding + store query) plus one model call. Context
        is truncated to :attr:`max_context_tokens` from the tail, so a large
        ``top_k`` never blows the context window.

    Example:
        >>> import asyncio
        >>> from windlass import Windlass
        >>> rag = Windlass.rag()
        >>> _ = rag.ingest_text("The Eiffel Tower is in Paris.")
        >>> asyncio.run(rag.aask("Where is the Eiffel Tower?")).question
        'Where is the Eiffel Tower?'
    """
    started = time.perf_counter()
    with self.tracer.span("rag.ask", kind="chain", inputs=question) as span:
        safe_question = await self._guard(question, "input")
        result = await self.asearch(safe_question, k, filters=filters)

        if not result.hits and not self.answer_without_context:
            answer = RAGAnswer(
                answer=NO_CONTEXT_ANSWER,
                question=question,
                latency_ms=(time.perf_counter() - started) * 1000,
                metadata={"no_context": True},
            )
            span.set_output(answer.answer)
            return answer

        context, used = self._build_context(result.hits)
        messages = await self._build_messages(safe_question, context, thread_id)

        with self.tracer.span("generate", kind="llm", inputs=safe_question) as llm_span:
            try:
                completion = await self.llm.acomplete(messages, **llm_kwargs)
            except Exception as exc:
                raise PipelineError(
                    f"Answer generation failed: {exc}",
                    context={"question": question[:200]},
                ) from exc
            llm_span.set_output(completion.content)
            llm_span.set_usage(completion.usage)
            llm_span.set_metadata(model=completion.model)

        safe_answer = await self._guard(completion.content, "output")
        await self._remember(question, safe_answer, thread_id)

        answer = RAGAnswer(
            answer=safe_answer,
            question=question,
            contexts=used,
            usage=completion.usage,
            latency_ms=(time.perf_counter() - started) * 1000,
            metadata={
                "model": completion.model,
                "retrieved": len(result.hits),
                "used_contexts": len(used),
                "retrieval_ms": round(result.latency_ms, 1),
            },
        )
        span.set_output(safe_answer)
        span.set_usage(completion.usage)
    return answer

ask

ask(
    question: str,
    *,
    k: int | None = None,
    filters: MetadataFilter | None = None,
    thread_id: str = "default",
    **llm_kwargs: Any
) -> RAGAnswer

Blocking :meth:aask.

Source code in src\windlass\rag\pipeline.py
def ask(
    self,
    question: str,
    *,
    k: int | None = None,
    filters: MetadataFilter | None = None,
    thread_id: str = "default",
    **llm_kwargs: Any,
) -> RAGAnswer:
    """Blocking :meth:`aask`."""
    return run_sync(
        self.aask(question, k=k, filters=filters, thread_id=thread_id, **llm_kwargs)
    )

astream_ask async

astream_ask(
    question: str,
    *,
    k: int | None = None,
    filters: MetadataFilter | None = None,
    thread_id: str = "default",
    **llm_kwargs: Any
) -> AsyncIterator[str]

Stream an answer token by token.

Retrieval completes first (it must — the context shapes the prompt), then generation streams. Guardrails run on the assembled answer after the stream finishes, so a blocking output policy cannot un-send text that has already reached the user; use redact semantics or non-streaming generation when output blocking must be authoritative.

Parameters:

Name Type Description Default
question str

The user's question.

required
k int | None

Override for :attr:top_k.

None
filters MetadataFilter | None

Metadata constraints.

None
thread_id str

Conversation thread.

'default'
**llm_kwargs Any

Per-call model overrides.

{}

Yields:

Type Description
AsyncIterator[str]

Text fragments as the model produces them.

Source code in src\windlass\rag\pipeline.py
async def astream_ask(
    self,
    question: str,
    *,
    k: int | None = None,
    filters: MetadataFilter | None = None,
    thread_id: str = "default",
    **llm_kwargs: Any,
) -> AsyncIterator[str]:
    """Stream an answer token by token.

    Retrieval completes first (it must — the context shapes the prompt), then
    generation streams. Guardrails run on the assembled answer after the
    stream finishes, so a blocking output policy cannot un-send text that has
    already reached the user; use ``redact`` semantics or non-streaming
    generation when output blocking must be authoritative.

    Args:
        question: The user's question.
        k: Override for :attr:`top_k`.
        filters: Metadata constraints.
        thread_id: Conversation thread.
        **llm_kwargs: Per-call model overrides.

    Yields:
        Text fragments as the model produces them.
    """
    safe_question = await self._guard(question, "input")
    result = await self.asearch(safe_question, k, filters=filters)

    if not result.hits and not self.answer_without_context:
        yield NO_CONTEXT_ANSWER
        return

    context, _ = self._build_context(result.hits)
    messages = await self._build_messages(safe_question, context, thread_id)

    collected: list[str] = []
    async for event in self.llm.astream(messages, **llm_kwargs):
        if event.type == "text" and event.delta:
            collected.append(event.delta)
            yield event.delta

    answer = "".join(collected)
    if self.guardrail is not None:
        await self.guardrail.avalidate(answer, stage="output")
    await self._remember(question, answer, thread_id)

stream_ask

stream_ask(question: str, **kwargs: Any) -> Iterator[str]

Blocking :meth:astream_ask.

Parameters:

Name Type Description Default
question str

The user's question.

required
**kwargs Any

Forwarded to :meth:astream_ask.

{}

Yields:

Type Description
str

Text fragments.

Source code in src\windlass\rag\pipeline.py
def stream_ask(self, question: str, **kwargs: Any) -> Iterator[str]:
    """Blocking :meth:`astream_ask`.

    Args:
        question: The user's question.
        **kwargs: Forwarded to :meth:`astream_ask`.

    Yields:
        Text fragments.
    """
    return iter_sync(self.astream_ask(question, **kwargs))

aevaluate async

aevaluate(
    questions: Sequence[str] | Sequence[dict[str, Any]],
    *,
    evaluator: Any = None,
    metrics: Sequence[str] | None = None
) -> Any

Answer a question set and score the results.

Parameters:

Name Type Description Default
questions Sequence[str] | Sequence[dict[str, Any]]

Question strings, or dicts with question and an optional reference ground-truth answer.

required
evaluator Any

An :class:~windlass.interfaces.evaluator.Evaluator. When omitted, the built-in evaluator is used with this pipeline's model as the judge.

None
metrics Sequence[str] | None

Metric names, passed to the default evaluator.

None

Returns:

Name Type Description
An Any

class:~windlass.core.types.EvaluationReport.

Example

import asyncio from windlass import Windlass rag = Windlass.rag() _ = rag.ingest_text("Paris is the capital of France.") report = asyncio.run(rag.aevaluate( ... [{"question": "Capital of France?", "reference": "Paris"}], ... metrics=["answer_relevancy_lexical"], ... )) report.samples 1

Source code in src\windlass\rag\pipeline.py
async def aevaluate(
    self,
    questions: Sequence[str] | Sequence[dict[str, Any]],
    *,
    evaluator: Any = None,
    metrics: Sequence[str] | None = None,
) -> Any:
    """Answer a question set and score the results.

    Args:
        questions: Question strings, or dicts with ``question`` and an
            optional ``reference`` ground-truth answer.
        evaluator: An :class:`~windlass.interfaces.evaluator.Evaluator`. When
            omitted, the built-in evaluator is used with this pipeline's
            model as the judge.
        metrics: Metric names, passed to the default evaluator.

    Returns:
        An :class:`~windlass.core.types.EvaluationReport`.

    Example:
        >>> import asyncio
        >>> from windlass import Windlass
        >>> rag = Windlass.rag()
        >>> _ = rag.ingest_text("Paris is the capital of France.")
        >>> report = asyncio.run(rag.aevaluate(
        ...     [{"question": "Capital of France?", "reference": "Paris"}],
        ...     metrics=["answer_relevancy_lexical"],
        ... ))
        >>> report.samples
        1
    """
    from windlass.interfaces.evaluator import EvalSample

    if evaluator is None:
        from windlass.providers.evaluation.builtin import BuiltinEvaluator

        evaluator = BuiltinEvaluator(metrics=metrics, llm=self.llm)

    samples: list[EvalSample] = []
    for item in questions:
        if isinstance(item, str):
            question, reference = item, ""
        else:
            question = str(item.get("question", ""))
            reference = str(item.get("reference", "") or item.get("ground_truth", ""))
        answer = await self.aask(question)
        samples.append(EvalSample.from_answer(answer, reference=reference))

    with self.tracer.span("evaluate", kind="evaluation") as span:
        report = await evaluator.aevaluate(samples)
        span.set_output(report.summary)
    return report

evaluate

evaluate(
    questions: Sequence[str] | Sequence[dict[str, Any]],
    *,
    evaluator: Any = None,
    metrics: Sequence[str] | None = None
) -> Any

Blocking :meth:aevaluate.

Source code in src\windlass\rag\pipeline.py
def evaluate(
    self,
    questions: Sequence[str] | Sequence[dict[str, Any]],
    *,
    evaluator: Any = None,
    metrics: Sequence[str] | None = None,
) -> Any:
    """Blocking :meth:`aevaluate`."""
    return run_sync(self.aevaluate(questions, evaluator=evaluator, metrics=metrics))

native_llm

native_llm() -> Any

Return the model provider's own client.

Source code in src\windlass\rag\pipeline.py
def native_llm(self) -> Any:
    """Return the model provider's own client."""
    return self.llm.native()

native_store

native_store() -> Any

Return the vector store's own handle (a FAISS index, a Chroma collection).

Source code in src\windlass\rag\pipeline.py
def native_store(self) -> Any:
    """Return the vector store's own handle (a FAISS index, a Chroma collection)."""
    return self.vectorstore.native()

native_retriever

native_retriever() -> Any

Return the retriever's underlying object.

Source code in src\windlass\rag\pipeline.py
def native_retriever(self) -> Any:
    """Return the retriever's underlying object."""
    return self.retriever.native()

describe

describe() -> dict[str, Any]

Return a JSON-safe description of every wired component.

Useful for logging what a deployment is actually running, and for diffing two environments that behave differently.

Returns:

Type Description
dict[str, Any]

A dict keyed by component role.

Source code in src\windlass\rag\pipeline.py
def describe(self) -> dict[str, Any]:
    """Return a JSON-safe description of every wired component.

    Useful for logging what a deployment is actually running, and for
    diffing two environments that behave differently.

    Returns:
        A dict keyed by component role.
    """
    parts: dict[str, Any] = {
        "llm": self.llm.describe(),
        "embedder": self.embedder.describe(),
        "vectorstore": self.vectorstore.describe(),
        "retriever": self.retriever.describe(),
        "top_k": self.top_k,
        "max_context_tokens": self.max_context_tokens,
    }
    for label, component in (
        ("chunker", self.chunker),
        ("loader", self.loader),
        ("preprocessor", self.preprocessor),
        ("guardrail", self.guardrail),
        ("memory", self.memory),
        ("tracer", self.tracer),
    ):
        if component is not None:
            parts[label] = component.describe()
    return parts

acount async

acount() -> int

Return how many chunks are indexed.

Reads the vector store, falling back to the retriever's own index for purely lexical pipelines where no vectors are stored.

Source code in src\windlass\rag\pipeline.py
async def acount(self) -> int:
    """Return how many chunks are indexed.

    Reads the vector store, falling back to the retriever's own index for
    purely lexical pipelines where no vectors are stored.
    """
    count = await self.vectorstore.acount()
    if count:
        return count
    own = getattr(self.retriever, "chunks", None)
    return len(own) if own is not None else 0

count

count() -> int

Blocking :meth:acount.

Source code in src\windlass\rag\pipeline.py
def count(self) -> int:
    """Blocking :meth:`acount`."""
    return run_sync(self.acount())

aclose async

aclose() -> None

Release every component's resources.

Source code in src\windlass\rag\pipeline.py
async def aclose(self) -> None:
    """Release every component's resources."""
    for component in (
        self.llm,
        self.embedder,
        self.vectorstore,
        self.retriever,
        self.tracer,
    ):
        close = getattr(component, "aclose", None)
        if close is not None:
            try:
                await close()
            except Exception as exc:
                _log.debug("Closing %s failed: %s", component, exc)

close

close() -> None

Blocking :meth:aclose.

Source code in src\windlass\rag\pipeline.py
def close(self) -> None:
    """Blocking :meth:`aclose`."""
    run_sync(self.aclose())

save

save(directory: str | Path) -> Path

Persist the index to disk, when the store supports it.

Parameters:

Name Type Description Default
directory str | Path

Where to write. Created if absent.

required

Returns:

Type Description
Path

The directory written to.

Raises:

Type Description
PipelineError

When the configured store cannot persist.

Example

import tempfile from windlass import Windlass rag = Windlass.rag() _ = rag.ingest_text("something worth keeping") rag.save(tempfile.mkdtemp()).is_dir() True

Source code in src\windlass\rag\pipeline.py
def save(self, directory: str | Path) -> Path:
    """Persist the index to disk, when the store supports it.

    Args:
        directory: Where to write. Created if absent.

    Returns:
        The directory written to.

    Raises:
        PipelineError: When the configured store cannot persist.

    Example:
        >>> import tempfile
        >>> from windlass import Windlass
        >>> rag = Windlass.rag()
        >>> _ = rag.ingest_text("something worth keeping")
        >>> rag.save(tempfile.mkdtemp()).is_dir()
        True
    """
    target = Path(directory)
    target.mkdir(parents=True, exist_ok=True)
    saver = getattr(self.vectorstore, "save", None)
    if not callable(saver):
        raise PipelineError(
            f"{type(self.vectorstore).__name__} does not support saving to disk.",
            hint="Chroma and Pinecone persist automatically; use "
            "vectordb('memory', persist_path=...) or 'faiss' for explicit saves.",
        )
    # The in-memory store saves to a single JSON file; FAISS writes a
    # directory of index plus sidecar.
    if self.vectorstore.provider_name == "memory":
        saver(target / f"{self.vectorstore.collection}.json")
    else:
        saver(target)
    return target

load

load(directory: str | Path) -> RAGPipeline

Restore an index previously written by :meth:save.

Parameters:

Name Type Description Default
directory str | Path

Directory containing the saved index.

required

Returns:

Type Description
RAGPipeline

self, so the call chains.

Raises:

Type Description
PipelineError

When the configured store cannot load.

Source code in src\windlass\rag\pipeline.py
def load(self, directory: str | Path) -> RAGPipeline:
    """Restore an index previously written by :meth:`save`.

    Args:
        directory: Directory containing the saved index.

    Returns:
        ``self``, so the call chains.

    Raises:
        PipelineError: When the configured store cannot load.
    """
    source = Path(directory)
    loader = getattr(self.vectorstore, "load", None)
    if not callable(loader):
        raise PipelineError(
            f"{type(self.vectorstore).__name__} does not support loading from disk."
        )
    if self.vectorstore.provider_name == "memory":
        loader(source / f"{self.vectorstore.collection}.json")
    else:
        loader(source)

    # Rebuild any lexical index from the restored corpus.
    rebuild = getattr(self.vectorstore, "all_chunks", None)
    if callable(rebuild) and getattr(self.retriever, "requires_index", False):
        run_sync(self.retriever.aindex(rebuild()))
    return self

configure

configure(**overrides: Any) -> WindlassSettings

Update the process-wide settings.

Values are merged onto the current settings, so partial updates are fine.

Parameters:

Name Type Description Default
**overrides Any

Any field of :class:WindlassSettings. Nested retry and cache accept either a dict or the model instance.

{}

Returns:

Type Description
WindlassSettings

The new settings object.

Raises:

Type Description
ConfigurationError

If an override is unknown or fails validation.

Example

configure(temperature=0.0, retry={"attempts": 5}).retry.attempts 5 reset_settings()

Source code in src\windlass\core\config.py
def configure(**overrides: Any) -> WindlassSettings:
    """Update the process-wide settings.

    Values are merged onto the current settings, so partial updates are fine.

    Args:
        **overrides: Any field of :class:`WindlassSettings`. Nested ``retry`` and
            ``cache`` accept either a dict or the model instance.

    Returns:
        The new settings object.

    Raises:
        ConfigurationError: If an override is unknown or fails validation.

    Example:
        >>> configure(temperature=0.0, retry={"attempts": 5}).retry.attempts
        5
        >>> reset_settings()
    """
    global _SETTINGS
    with _LOCK:
        current = settings().model_dump()
        unknown = set(overrides) - set(WindlassSettings.model_fields)
        if unknown:
            raise ConfigurationError(
                f"Unknown setting(s): {', '.join(sorted(unknown))}.",
                hint=f"Valid settings: {', '.join(sorted(WindlassSettings.model_fields))}",
            )
        current.update(overrides)
        try:
            _SETTINGS = WindlassSettings(**current)
        except Exception as exc:
            raise ConfigurationError(f"Invalid configuration override: {exc}") from exc
        return _SETTINGS

reset_settings

reset_settings() -> None

Discard cached settings so the next :func:settings call re-reads the env.

Primarily a test helper.

Source code in src\windlass\core\config.py
def reset_settings() -> None:
    """Discard cached settings so the next :func:`settings` call re-reads the env.

    Primarily a test helper.
    """
    global _SETTINGS
    with _LOCK:
        _SETTINGS = None

settings

settings() -> WindlassSettings

Return the process-wide settings, constructing them on first use.

Returns:

Type Description
WindlassSettings

The active :class:WindlassSettings.

Example

isinstance(settings().request_timeout, float) True

Source code in src\windlass\core\config.py
def settings() -> WindlassSettings:
    """Return the process-wide settings, constructing them on first use.

    Returns:
        The active :class:`WindlassSettings`.

    Example:
        >>> isinstance(settings().request_timeout, float)
        True
    """
    global _SETTINGS
    if _SETTINGS is None:
        with _LOCK:
            if _SETTINGS is None:
                _SETTINGS = _build_settings()
    return _SETTINGS

root_container

root_container() -> Container

Return the process-wide root container.

Bindings placed here are inherited by every builder created afterwards, which makes it the right place for application-wide wiring::

root_container().bind_instance("tracer", MyTracer())

Returns:

Type Description
Container

The shared root :class:Container.

Source code in src\windlass\core\container.py
def root_container() -> Container:
    """Return the process-wide root container.

    Bindings placed here are inherited by every builder created afterwards,
    which makes it the right place for application-wide wiring::

        root_container().bind_instance("tracer", MyTracer())

    Returns:
        The shared root :class:`Container`.
    """
    return _ROOT

configure_logging

configure_logging(
    level: int | str = "INFO",
    *,
    stream: Any = None,
    fmt: str | None = None,
    force: bool = False
) -> logging.Logger

Attach a human-readable stream handler to the windlass logger.

Intended for scripts, notebooks and the CLI. Applications with their own logging configuration should not call this.

Parameters:

Name Type Description Default
level int | str

Log level name or numeric value.

'INFO'
stream Any

Target stream. Defaults to sys.stderr.

None
fmt str | None

Custom :mod:logging format string.

None
force bool

Replace handlers that a previous call installed.

False

Returns:

Type Description
Logger

The configured windlass logger.

Example

logger = configure_logging("WARNING") logger.name 'windlass'

Source code in src\windlass\core\logging.py
def configure_logging(
    level: int | str = "INFO",
    *,
    stream: Any = None,
    fmt: str | None = None,
    force: bool = False,
) -> logging.Logger:
    """Attach a human-readable stream handler to the ``windlass`` logger.

    Intended for scripts, notebooks and the CLI. Applications with their own
    logging configuration should not call this.

    Args:
        level: Log level name or numeric value.
        stream: Target stream. Defaults to ``sys.stderr``.
        fmt: Custom :mod:`logging` format string.
        force: Replace handlers that a previous call installed.

    Returns:
        The configured ``windlass`` logger.

    Example:
        >>> logger = configure_logging("WARNING")
        >>> logger.name
        'windlass'
    """
    logger = logging.getLogger(ROOT_LOGGER_NAME)
    logger.setLevel(level if isinstance(level, int) else level.upper())

    existing = [h for h in logger.handlers if getattr(h, "_windlass_handler", False)]
    if existing and not force:
        return logger
    for handler in existing:
        logger.removeHandler(handler)

    handler = logging.StreamHandler(stream or sys.stderr)
    handler.setFormatter(
        logging.Formatter(fmt or "%(asctime)s %(levelname)-7s %(name)s: %(message)s", "%H:%M:%S")
    )
    handler.addFilter(_ContextFilter())
    handler._windlass_handler = True  # type: ignore[attr-defined]
    logger.addHandler(handler)
    logger.propagate = False
    return logger

get_logger

get_logger(name: str) -> logging.Logger

Return a Windlass-namespaced logger.

Parameters:

Name Type Description Default
name str

Usually __name__. A leading windlass. is not duplicated; anything else is nested under windlass..

required

Returns:

Type Description
Logger

A configured :class:logging.Logger.

Example

get_logger("windlass.core.registry").name 'windlass.core.registry' get_logger("my_plugin").name 'windlass.my_plugin'

Source code in src\windlass\core\logging.py
def get_logger(name: str) -> logging.Logger:
    """Return a Windlass-namespaced logger.

    Args:
        name: Usually ``__name__``. A leading ``windlass.`` is not duplicated;
            anything else is nested under ``windlass.``.

    Returns:
        A configured :class:`logging.Logger`.

    Example:
        >>> get_logger("windlass.core.registry").name
        'windlass.core.registry'
        >>> get_logger("my_plugin").name
        'windlass.my_plugin'
    """
    if name == ROOT_LOGGER_NAME or name.startswith(f"{ROOT_LOGGER_NAME}."):
        logger = logging.getLogger(name)
    else:
        logger = logging.getLogger(f"{ROOT_LOGGER_NAME}.{name}")
    if not any(isinstance(f, _ContextFilter) for f in logger.filters):
        logger.addFilter(_ContextFilter())
    return logger

available

available(kind: str) -> list[str]

List registered names for kind in the global registry.

Source code in src\windlass\core\registry.py
def available(kind: str) -> list[str]:
    """List registered names for ``kind`` in the global registry."""
    return REGISTRY.names(kind)

create

create(kind: str, name: str, /, **config: Any) -> Any

Instantiate a component from the global registry.

Thin module-level shortcut for :meth:Registry.create.

Source code in src\windlass\core\registry.py
def create(kind: str, name: str, /, **config: Any) -> Any:
    """Instantiate a component from the global registry.

    Thin module-level shortcut for :meth:`Registry.create`.
    """
    return REGISTRY.create(kind, name, **config)

tool

tool(fn: _F) -> FunctionTool
tool(
    *,
    name: str | None = ...,
    description: str | None = ...,
    parameters: dict[str, Any] | None = ...,
    timeout: float | None = ...,
    requires_approval: bool = ...,
    register: bool = ...
) -> Callable[[_F], FunctionTool]
tool(
    fn: _F | None = None,
    *,
    name: str | None = None,
    description: str | None = None,
    parameters: dict[str, Any] | None = None,
    timeout: float | None = None,
    requires_approval: bool = False,
    register: bool = False
) -> Any

Turn a Python function into an agent-callable tool.

Works bare (@tool) or with arguments (@tool(timeout=5)).

Parameters:

Name Type Description Default
fn _F | None

The function, when used bare.

None
name str | None

Tool name shown to the model. Defaults to the function name.

None
description str | None

Model-facing description. Defaults to the docstring summary. Write a good one — it is how the model decides when to call this.

None
parameters dict[str, Any] | None

JSON Schema override. Derived from type hints by default.

None
timeout float | None

Seconds before the call is abandoned.

None
requires_approval bool

Pause the agent for human approval before running. Use this for anything that spends money or changes state.

False
register bool

Also add the tool to the global registry under name, so it can be referenced as a string.

False

Returns:

Name Type Description
A Any

class:FunctionTool, or a decorator producing one.

Raises:

Type Description
ValidationError

If the resulting name is not provider-legal.

Note

The docstring is not decoration. Its summary becomes the model-facing description, and its Google-style argument section becomes the per-parameter descriptions in the JSON schema. Both are prompt text — the model decides when to call the tool, and what to pass, from them.

Example

Bare::

@tool
def search(query: str) -> list[str]:
    # Docstring: "Search the product catalogue." plus an argument
    # section describing `query`.
    return db.search(query)

Configured::

@tool(name="charge_card", requires_approval=True, timeout=30)
async def charge(amount_cents: int, customer_id: str) -> dict:
    # Approval-gated: the agent pauses before this ever runs.
    return await payments.charge(customer_id, amount_cents)
Source code in src\windlass\tools\__init__.py
def tool(
    fn: _F | None = None,
    *,
    name: str | None = None,
    description: str | None = None,
    parameters: dict[str, Any] | None = None,
    timeout: float | None = None,
    requires_approval: bool = False,
    register: bool = False,
) -> Any:
    """Turn a Python function into an agent-callable tool.

    Works bare (``@tool``) or with arguments (``@tool(timeout=5)``).

    Args:
        fn: The function, when used bare.
        name: Tool name shown to the model. Defaults to the function name.
        description: Model-facing description. Defaults to the docstring summary.
            **Write a good one** — it is how the model decides when to call this.
        parameters: JSON Schema override. Derived from type hints by default.
        timeout: Seconds before the call is abandoned.
        requires_approval: Pause the agent for human approval before running.
            Use this for anything that spends money or changes state.
        register: Also add the tool to the global registry under ``name``, so it
            can be referenced as a string.

    Returns:
        A :class:`FunctionTool`, or a decorator producing one.

    Raises:
        ValidationError: If the resulting name is not provider-legal.

    Note:
        The docstring is not decoration. Its summary becomes the model-facing
        description, and its Google-style argument section becomes the
        per-parameter descriptions in the JSON schema. Both are prompt text — the
        model decides *when* to call the tool, and *what to pass*, from them.

    Example:
        Bare::

            @tool
            def search(query: str) -> list[str]:
                # Docstring: "Search the product catalogue." plus an argument
                # section describing `query`.
                return db.search(query)

        Configured::

            @tool(name="charge_card", requires_approval=True, timeout=30)
            async def charge(amount_cents: int, customer_id: str) -> dict:
                # Approval-gated: the agent pauses before this ever runs.
                return await payments.charge(customer_id, amount_cents)
    """

    def wrap(target: _F) -> FunctionTool:
        built = FunctionTool(
            target,
            name=name,
            description=description,
            parameters=parameters,
            timeout=timeout,
            requires_approval=requires_approval,
        )
        if register:
            REGISTRY.register(
                "tool",
                built.name,
                lambda **_: built,
                description=built.description,
                origin="user",
                override=True,
            )
        return built

    if fn is not None:
        return wrap(fn)
    return wrap