# Core Crate (`chat-core`)
The foundational crate defining all traits, types, and the `Chat` engine. Providers depend on this crate — it never depends on any provider.
## Architecture
```
src/
├── lib.rs # Public re-exports
├── traits.rs # CompletionProvider, StreamProvider, EmbeddingsProvider, ChatProvider
├── builder.rs # ChatBuilder (type-state pattern)
├── error.rs # ChatError, ChatFailure
├── macros/mod.rs # Procedural macros (retry_strategy!)
├── utils.rs # Internal helpers
├── chat/
│ ├── mod.rs # Chat<CP, Output> struct, scoped-tool dispatch, tool_call pre/post steps
│ ├── completion.rs # Unstructured + Structured completion loop
│ ├── embed.rs # Embedding logic
│ ├── state.rs # Type states: Unstructured, Structured<T>, Streamed, Embedded
│ └── stream.rs # Streaming loop with pause/resume (feature-gated on "stream")
└── types/
├── messages/ # Messages, Content, PartEnum, File, Embeddings, Reasoning, Tool
├── tools.rs # ToolDeclarations, Action, ScopedCollection, strategy closures
├── response.rs # ChatResponse, StructuredResponse<T>, EmbeddingsResponse,
│ # StreamEvent (incl. Paused), PauseReason
├── options.rs # ChatOptions (temperature, max_tokens, top_p, metadata)
├── provider_meta.rs# ProviderMeta (provider self-description)
├── callback.rs # CallbackStrategy, RetryStrategy
└── metadata/ # Metadata, Usage (token counts)
```
## Key Traits
All provider crates implement these from `traits.rs`:
### `CompletionProvider` (required)
```rust
#[async_trait]
pub trait CompletionProvider: Send + Sync {
async fn complete(
&mut self,
messages: &mut Messages,
tool_declarations: Option<&dyn ToolDeclarations>,
options: Option<&ChatOptions>,
structured_output: Option<&schemars::Schema>,
) -> Result<ChatResponse, ChatFailure>;
fn metadata(&self) -> Option<&ProviderMeta> { None }
}
```
Providers receive `tool_declarations` — a type-erased view that only exposes `.json()`. They never see strategies, metadata, or execution — the chat loop owns all of that.
### `StreamProvider` (optional, feature-gated on `stream`)
```rust
#[async_trait]
pub trait StreamProvider: Send + Sync {
async fn stream(
&mut self,
messages: &mut Messages,
tool_declarations: Option<&dyn ToolDeclarations>,
options: Option<&ChatOptions>,
) -> Result<BoxStream<'static, Result<StreamEvent, ChatError>>, ChatError>;
fn on_stream_done(&mut self, _response: &ChatResponse) {}
}
```
### `ChatProvider` (blanket supertrait, `stream` feature)
Any type implementing both `CompletionProvider` and `StreamProvider` automatically implements `ChatProvider`. The router uses this for its `StreamRouter`.
### `EmbeddingsProvider` (optional)
```rust
#[async_trait]
pub trait EmbeddingsProvider: Send + Sync {
async fn embed(&self, messages: &mut Messages) -> Result<EmbeddingsResponse, ChatFailure>;
}
```
## Type-State Builder
`ChatBuilder<CP, Output>` uses phantom types to enforce valid configurations at compile time:
- `Unstructured` (default) — `.complete()` returns `ChatResponse`; `.stream()` returns `BoxStream<StreamEvent>`
- `Structured<T>` — `.complete()` returns `StructuredResponse<T>` (T: JsonSchema + DeserializeOwned)
- `Embedded` — `.embed()` returns `EmbeddingsResponse`
State transitions are one-way from `Unstructured`: calling `.with_structured_output::<T>()` or `.with_embeddings()` consumes the builder into the new state.
## Message System
`Messages` wraps `Vec<Content>`. Pushing a `Content` with the same role as the last message merges them (appends parts) rather than creating a new entry.
`Content` has a `role` (User, System, Model) and `parts` containing `PartEnum` variants:
- `Text`, `Reasoning`, `Tool`, `Structured(Value)`, `File`, `Embeddings`
`Tool` is a single part combining a `FunctionCall` with its `ToolStatus` lifecycle — replacing the previous split of `FunctionCall` / `FunctionResponse` parts. Status transitions through: `Pending` → `Approved` / `Rejected` → `Running` → `Completed` / `Failed`. Only resolved states (`Completed`, `Rejected`, `Failed`) ever reach the wire; the rest are local-only and governed by the chat loop's HITL flow.
`File` is either `Url { url, mimetype }` or `Bytes { bytes, mimetype }`.
`Messages::find_tool_mut(id)` locates a `Tool` part anywhere in history by `CallId` — used by HITL callers to resolve a paused tool (`tool.approve(...)` / `tool.reject(...)`).
## Scoped Tool Collections & Strategies
Tools live in `ScopedCollection<M, F>` — a typed `ToolCollection<M>` paired with a strategy closure `F: Fn(&FunctionCall, &M) -> Action`. The metadata type `M` is whatever the user defines via `#[tool(...)]` attributes (e.g. a struct with `requires_approval`, `safety`, rate limits, audit tags).
```rust
let raw = ToolCollection::<ApprovalMeta>::collect_tools()?;
});
let chat = ChatBuilder::new().with_model(client).with_scoped_tools(scoped).build();
```
`Action` variants:
- `Execute` — run the tool immediately.
- `RequireApproval` — mark the tool `Pending`, pause the loop so a human can decide.
- `Schedule { at }` — defer execution; caller persists and resumes later.
- `Reject { reason }` — mark the tool `Rejected` without running it.
A single `Chat` can hold multiple scoped collections with different metadata types. The chat loop dispatches each model-emitted tool call to the collection that owns its name.
## Tool Call Loop
`Chat::tool_call()` runs as a pre-step (before each provider turn) and post-step (after each provider response):
1. Walk the last `Content`'s `Tool` parts.
2. For each `Pending` tool, call the owning collection's strategy closure → `Action`.
3. Execute `Approved`/`Execute` tools; mark results `Completed` / `Failed`.
4. If any tool returned `RequireApproval` or `Schedule`, return a `ToolCallPass` carrying a `PauseReason` and the loop halts.
In streaming mode (`chat::stream.rs`), a pause yields `StreamEvent::Paused(PauseReason)` and terminates the stream. The caller resolves the pending tools on `messages` and calls `chat.stream(&mut messages)` again — the next pre-step runs the newly-approved tools and continues into the next provider turn on the same logical conversational turn. History is preserved across calls via the shared `&mut Messages`.
`PauseReason` variants:
- `AwaitingApproval { tool_ids }`
- `Scheduled { tool_ids, at }`
- `Mixed { ... }` — when a single turn produces more than one pause category
## Error Model
- `ChatError` — enum of error variants (Network, Provider, RateLimited, MaxStepsExceeded, InvalidResponse, Callback, Other)
- `ChatFailure` — wraps `ChatError` with optional `Metadata`, so partial token usage is preserved even on failure
## Caveats
- `Messages::push()` silently merges consecutive same-role messages. Intentional for tool call flows; can be surprising if you expect separate entries.
- Only resolved `ToolStatus` states (`Completed`, `Rejected`, `Failed`) serialize to the wire via `Tool::to_tuple`. Providers never see `Pending`/`Approved`/`Running`.
- Feature flag `stream` must be enabled at every layer: `chat-core/stream`, and propagated through `chat-rs/stream` to each provider.
- Metadata is provider-specific (`HashMap<String, Value>` on `specific`) — no schema enforcement.