# Fundamentals 02 — Agent types: Messages, ModelOutput, SessionRecord, and the Stream contract
The types every provider, tool and loop component shares. All of them live in
`foundation_ai::types` (re-exported from `types/base_types.rs` and
`types/agentic.rs`) unless noted.
---
## 1. `Messages` — the normalized conversation format
Each backend has its own wire format; `Messages` is the internal one. There are
three variants. System prompts are **not** a variant — they ride on
`ModelInteraction::system_prompt`, and loop-injected instructions are
`Messages::User` with `role: MessageRole::System`.
```rust
pub enum Messages {
User {
id: Id, // scru128, time-ordered
role: MessageRole, // User | Agent | System | Tool | Custom(String)
content: UserModelContent,
signature: Option<String>,
},
Assistant {
id: Id,
model: ModelId,
timestamp: SystemTime,
usage: UsageReport,
content: ModelOutput, // one output per message
stop_reason: StopReason,
provider: ModelProviders,
error_detail: Option<String>,
signature: Option<String>,
metadata: Option<Vec<GenerationMetadata>>, // logprobs, fingerprint, timing
},
ToolResult {
id: Id,
tool_call_id: String, // matches ModelOutput::ToolCall::id
name: String,
timestamp: SystemTime,
details: Option<String>,
content: UserModelContent,
error_detail: Option<String>,
signature: Option<String>,
},
}
```
- `id` is a `foundation_compact::ids::Id` (scru128). Make one with
`foundation_compact::ids::new_scru128()`; legacy JSON without an `id`
deserializes with a fresh one. `Messages::id()` reads it from any variant.
- `MessageRole` tells apart human input (`User`), peer agents (`Agent`),
instructions and loop redirects (`System`) and tool context (`Tool`). It
serializes to the lowercase strings providers expect.
- A streaming backend yields **one `Messages::Assistant` per chunk**, each
carrying a single `ModelOutput`. What a text chunk holds depends on the
backend: the HTTP backends (OpenAI, Responses, Anthropic) send the **whole
text so far**, so the last chunk is the answer; llama.cpp and Candle send
**only the new piece**, so the answer is the concatenation. The agent loop
handles both and persists one merged message per turn.
- `Messages::is_context_overflow(context_window)` recognises overflow errors
from ~15 providers' error strings, plus "silent" overflow where usage exceeds
the window.
Building a message:
```rust
let prompt = Messages::user("Hi"); // role User, text content, fresh id
let steer = Messages::agent("Stop and summarise."); // role Agent
let note = Messages::system("Be brief."); // role System
let prompt: Messages = "Hi".into(); // From<&str> / From<String> = Messages::user
```
Session methods take `impl Into<Messages>`, so `agent.run_turn("Hi")` works
directly. The struct literal (`Messages::User { id, role, content, signature }`)
is still there for other content types.
## 2. `ModelOutput` — what the assistant produces
```rust
pub enum ModelOutput {
Text(TextContent),
Image(ImageContent),
ThinkingContent { thinking: String, signature: Option<String> },
ToolCall {
id: String, // correlation id
name: String,
arguments: Option<ToolArguments>, // the model's JSON object
signature: Option<String>,
depends_on: Vec<String>, // ids of calls this waits on
execution_hint: ExecutionHint, // Unspecified | Parallel | Sequential
},
Embedding { dimensions: usize, values: Vec<f32> },
}
```
There is no refusal variant; refusals surface through `StopReason` /
`GenerationMetadata::RefusalReason`.
## 3. `UserModelContent`
```rust
pub enum UserModelContent {
Text(TextContent), // { content: String, signature: Option<String> }
Image(ImageContent), // { b64: String, mime_type: MimeType }
}
```
One content item per message — there is no multi-part variant.
## 4. `StopReason`
| `Stop` | Natural end |
| `Length` | Hit `max_tokens` |
| `ToolUse` | The model wants tools (`"tool_calls"` maps here too) |
| `Error` | Provider error — see `error_detail` |
| `Aborted` | Cancelled |
| `Message(String)` | Any other provider value, preserved |
## 5. `ToolArguments` — tool argument values
```rust
pub type ToolArguments = serde_json::Map<String, serde_json::Value>;
```
A tool call's arguments are the JSON object the model sent, unchanged from
the backend to the tool: every backend already receives JSON, so there is no
conversion to lose information in. Schema validation checks this object, and
tools read it with `agentic::ToolArgs` (`str`, `i64`, `usize`, `f64`, `bool`,
their `opt_*` forms, `opt_value`, and `parse::<T>()` for a whole struct).
Arguments that are not a JSON object (or not JSON at all) are logged and the
call carries no arguments, so validation reports the missing fields to the
model.
## 6. `Args` — a JSON Schema plus its validator
```rust
pub struct Args {
pub schema: serde_json::Value,
pub validator: foundation_jsonschema::ValidationOptions,
}
```
Build it with:
- `Args::new(scheme::object().required("q", scheme::string()).build())` — the
`foundation_jsonschema::scheme` builder (preferred)
- `Args::from_value(json!({ "type": "object", ... }))` — a raw schema
- `Args::empty()` — a tool with no arguments
## 7. Tools as the model sees them
```rust
pub struct ToolDefinition {
pub name: String,
pub category: String,
pub description: String,
pub arguments: Args,
pub returns: Option<Args>,
}
pub enum Tool {
SingleCommand(ToolDefinition),
MultiCommands(String, Vec<ToolDefinition>), // e.g. "memory" → add/remove/replace
}
pub struct ToolDeclarations {
pub shed: Option<Tool>, // the discovery meta-tool
pub tools: Vec<Tool>, // the tools `shed` has activated
}
```
Doc 04 covers how these are built and rendered per provider.
## 8. `ModelInteraction` and `ModelParams` — a request
```rust
pub struct ModelInteraction {
pub system_prompt: Option<String>,
pub soul: Option<String>,
pub tools_shed: ToolDeclarations,
pub messages: Vec<Messages>,
pub chat_template: Option<String>, // local backends: override the model's template
pub tool_choice: Option<ToolChoice>, // Auto | None | Required | Function(..)
}
```
`ModelParams` (default in parentheses): `max_tokens` (2048), `temperature`
(0.7), `top_p` (0.9), `top_k` (40.0), `repeat_penalty` (1.1), `seed`,
`stop_tokens`, `thinking_level`, `thinking_budget`, `cache_retention`,
`output_format` (`Text` / `JsonObject` / `JsonSchema`), `frequency_penalty`,
`presence_penalty`, `logit_bias`. Backends ignore what they don't support.
## 9. `UsageReport`
```rust
pub struct UsageReport {
pub input: f64,
pub output: f64,
pub cache_read: f64,
pub cache_write: f64,
pub total_tokens: f64,
pub cost: UsageCosting,
}
```
Streaming backends report **cumulative** usage on each chunk.
## 10. The `Stream` contract
`Stream<D, P>` comes from `foundation_core::valtron`:
| `Stream::Init` | Started |
| `Stream::Pending(P)` | Progress, no data |
| `Stream::Next(D)` | A data item |
| `Stream::Spread(items)` | Several items at once |
| `Stream::Wait`, `Stream::Delayed(_)`, `Stream::Ignore` | Scheduling signals — skip them |
At the **model** level (`Model::stream`): `D = Messages`, `P = ModelState`
(`GeneratingTokens(Option<UsageReport>)`, `GeneratingEmbeddings`, `Finished`,
`Error(String)`).
At the **agent** level (`AgentSession::run_turn_stream`, which returns a
`TurnStream`): `D = SessionRecord`, `P = AgentProgress` — the alias is
`agentic::internals::AgentStream`. `TurnStream::events()` maps these items to
`TurnEvent`s (`Text`, `Thinking`, `ToolCall`, `ToolResult`, `Retract`,
`Progress`, then a terminal `Failed` or `Done(TurnSummary)`), skipping the
scheduling signals and the memory records.
## 11. `SessionRecord` — what a turn produces
```rust
pub enum SessionRecord {
Conversation { message: Messages },
Retracted { id, reason: String, timestamp }, // drop the assistant output streamed so far
WorkingMemory { id, facts: Vec<MemoryFact>, version: u64, timestamp },
Observation { id, observations: Vec<ObservationEntry>, token_count: u64, timestamp },
Reflection { id, reflections: Vec<ReflectionEntry>, generated_at,
observation_token_count_before: u64, reflection_token_count_after: u64 },
FailedAction { error: AgenticError, trace: StructuredErrorTrace }, // never persisted
Summary { message_count: u64, usage: TokenSnapshot }, // end of turn
}
```
## 12. `AgentProgress` — progress hints
`Initializing { step }`, `Generating { model, tokens_so_far }`,
`ToolCallRequested { name }`, `ExecutingTools { total, completed }`,
`ToolCallCancelled { id }`, `ProcessingMemory { kind }`,
`FlushingRecords { count }`, `Steering { source }`,
`TurnComplete { usage }`, `SessionEnding`.
`AgentProgress` is `#[non_exhaustive]`, so a `match` needs a `_` arm. Each
variant is an advisory hint about upcoming records, not a framing guarantee.
## 13. `ModelId`
```rust
pub enum ModelId {
Name(String, Option<Quantization>),
Alias(String, Option<Quantization>),
Group(String, Option<Quantization>),
Architecture(String, Option<Quantization>),
}
```
The second field is a GGUF quantization (`Q4_K_M`, `Q8_0`, …) for local
models; cloud models use `None`. Routing matches on `ModelId::name()` only
(Doc 03).
## Vector types
`VectorEntry`, `VectorMatch` and the `VectorStore` trait belong to
`foundation_vectors`, not this crate. Doc 07 shows how they are used here.