> ## Documentation Index
> Fetch the complete documentation index at: https://metalworks.lab2a.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Protocols

> The versioned ChatModel, SearchProvider, EmbeddingProvider, and storage-repo protocols metalworks owns.

These are the swappable interfaces. Implement one only if you want to bring your own
model, search, embeddings, corpus, or storage that metalworks doesn't already ship.
If you're using a known provider (Anthropic / OpenAI / Google) with the built-in
storage, you don't need this — see [Configuration](/docs/configuration).

Each protocol below is the exact shape your adapter has to match. The conformance
suites in `metalworks.testing` are the contract: if your adapter passes them, it
works everywhere the protocol is used.

**Versioning.** The protocols are versioned as a unit
(`metalworks.PROTOCOLS_VERSION`); minor versions add keyword-only parameters with
defaults, major versions break. Your adapter declares which version it implements.

## ChatModel

```python theme={null}
class ChatModel(Protocol):
    protocol_version: ClassVar[str]
    model_id: str
    capabilities: ChatCapabilities  # native_structured, tool_calls, native_grounding, thinking

    def complete_text(self, *, system: str, user: str, max_tokens: int = 1024,
                      temperature: float = 0.7, thinking_budget: int = 0,
                      timeout_s: float = 120.0) -> TextResult: ...

    def complete_structured(self, *, system: str, user: str, output_model: type[T],
                            max_tokens: int = 1024, temperature: float = 0.7,
                            thinking_budget: int = 0, timeout_s: float = 120.0) -> T: ...
```

`thinking_budget` is in tokens; adapters map it to their provider's mechanism.
Dispatch on `capabilities.native_grounding`, never `isinstance`.

## GroundedChatModel

```python theme={null}
class GroundedChatModel(Protocol):
    def complete_grounded(self, *, system: str, user: str, max_tokens: int = 2048,
                          temperature: float = 0.7, timeout_s: float = 180.0) -> GroundedResult: ...
```

`GroundedResult` carries `text`, `chunks: tuple[GroundingChunk, ...]`, and
`supports: tuple[GroundingSupport, ...]`. Support spans are **character**
offsets into `text` (adapters convert provider byte offsets), so finding-to-
citation overlap bucketing is correct for non-ASCII output.

## SearchProvider

```python theme={null}
class SearchProvider(Protocol):
    protocol_version: ClassVar[str]
    provider_id: str
    def search(self, *, query: str, max_results: int = 10,
               recency_days: int | None = None) -> list[SearchResult]: ...
```

External search (Exa, Tavily) is a separate interface from chat. Model-native
grounding lives on `GroundedChatModel`. The research web stream prefers internal
grounding and falls back to an external `SearchProvider` + structured synthesis.

## EmbeddingProvider

```python theme={null}
class EmbeddingProvider(Protocol):
    protocol_version: ClassVar[str]
    model_id: str
    dim: int
    def embed(self, texts: Sequence[str], *,
              task: Literal["document", "query"] = "document") -> list[list[float]]: ...
```

Anything that persists vectors stores an `IndexIdentity` (model id + dim) and
hard-fails with `EmbeddingModelMismatch` on a mismatch. Vectors from different
models are geometrically incompatible; same-dimension swaps degrade retrieval
silently, so the guard is a hard error, not a warning.

## Storage repos

Typed repositories, not a generic document store (production tables are
columnar). One backend object implements as many as it supports.

```python theme={null}
class CorpusRepo(Protocol):
    def upsert_posts(self, posts: Sequence[RedditPost]) -> None: ...
    def upsert_comments(self, comments: Sequence[RedditComment]) -> None: ...
    def get_posts(self, post_ids: Sequence[str]) -> list[RedditPost]: ...
    def get_comments_for_posts(self, post_ids: Sequence[str]) -> list[RedditComment]: ...
```

`BriefRepo`, `RunRepo`, `AccountRepo`, `OpportunityRepo`, and `InboxRepo` follow
the same shape. Backends shipped in core: `MemoryStores` and `SqliteStores`
(WAL, serialized writer). Hosted backends (Postgres/PostgREST) are a custom
store you implement downstream — see [Bring your own store](/docs/custom-store).
Verify any backend with `metalworks.testing.check_all_repos`.

## Errors

Every error carries an actionable `fix` and a machine-readable envelope used by
the MCP tools:

```python theme={null}
err.envelope()  # {"error_code": ..., "message": ..., "fix": ..., "docs_url": ...}
```
