Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- *(agent)* [**breaking**] Make `AgentRunner` the only execution path for configured agents: remove the raw `Completion` and `StreamingCompletion` traits and their `Agent` implementations, make agent execution state private, add runner-backed per-request overrides, and route `Extractor` through the full hook lifecycle. Raw hook-free requests remain available explicitly through `CompletionModel`.
- For managed agent execution, replace `agent.completion(prompt, history).await?.send().await?` with `agent.runner(prompt).history(history).max_turns(3).run().await?`, choosing a turn budget large enough for tool follow-ups.
- For managed streaming execution, replace `agent.stream_completion(prompt, history).await?.stream().await?` with `agent.runner(prompt).history(history).max_turns(3).stream().await`.
- The runner consumes tool calls rather than returning the first raw model response. Callers that handled that response manually, and other intentionally hook-free transport, should start from `model.completion_request(prompt).messages(history)` and then call `.send().await?` or `.stream().await?`.
- `AgentRun::new(prompt).with_history(history)` remains a sans-I/O state machine for custom drivers; it contains no configured agent model, tools, memory, or hooks and is not an alternate configured-agent execution path.
- An `Agent`'s model is fixed and private. Former per-call `.model(...)` / `.model_opt(...)` users should retain the provider `CompletionModel` and use its raw request API, or construct a separate `Agent` for the selected model.
- *(tool)* [**breaking**] Replace the parallel tool-execution APIs with one structured path. Typed tools now implement only `Tool::call(&mut ToolContext, Args) -> Result<Output, Error>`; author-facing errors remain typed until private runtime erasure normalizes them into `ToolExecutionError`, `ToolContext` carries inbound values and host-only result metadata, `ToolResult` is the single runtime observation, and `ToolSet::execute` / `ToolServerHandle::execute` are the dispatch surfaces. Event-specific hook action types make invalid event/action combinations unrepresentable.
- Tool implementations: retain one typed `type Error` for ordinary `?` propagation and direct-call tests; remove `classify_error`, `call_with_extensions`, and `call_structured`. The optional `map_error` method classifies domain failures at the erased boundary, while its default preserves the source as `Other`. Return refusals through `map_error` with `ToolExecutionError::refused`, and attach host-only result metadata with `ToolContext::insert_result`.
- Context: replace `ToolCallExtensions` and `ToolResultExtensions` with `ToolContext`; replace request/runner `.tool_extensions(...)` with `.tool_context(...)`. Each dispatch snapshots inbound context exactly once, isolates tool-local mutations, and publishes only result metadata back to the caller and hooks.
Expand Down
63 changes: 63 additions & 0 deletions crates/rig-core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,69 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- *(agent)* [**breaking**] Make `AgentRunner` the only execution path for configured agents: remove the raw `Completion` and `StreamingCompletion` traits and their `Agent` implementations, make agent execution state private, add runner-backed per-request overrides, and route `Extractor` through the full hook lifecycle. Raw hook-free requests remain available explicitly through `CompletionModel`.

Migration examples:

```rust
// Before: built a one-shot request from configured Agent state, but bypassed
// the AgentRunner lifecycle.
agent.completion(prompt, history).await?.send().await?;

// After, for managed Agent execution: hooks, tools, retrieval, memory, and
// turn accounting all run. Budget enough calls for tool follow-ups.
agent
.runner(prompt)
.history(history)
.max_turns(3)
.run()
.await?;
```

Streaming follows the same boundary:

```rust
// Before
agent.stream_completion(prompt, history).await?.stream().await?;

// After
let stream = agent
.runner(prompt)
.history(history)
.max_turns(3)
.stream()
.await;
```

The runner consumes tool calls instead of returning the first raw model
response. If the old caller handled that response itself, or for any other
intentionally hook-free provider transport, start from the model rather than
an `Agent`:

```rust
model
.completion_request(prompt)
.messages(history)
.send()
.await?;

let stream = model
.completion_request(prompt)
.messages(history)
.stream()
.await?;
```

`AgentRun::new(prompt).with_history(history)` remains the public sans-I/O
state machine for custom drivers. It contains no model, tools, memory, or hook
stack: callers must handle every `AgentRunStep`, perform provider/tool IO, and
feed results back explicitly. It is not a way to execute a configured
`Agent`; use `AgentRunner` for that.

An `Agent` also keeps its configured model private and fixed. Applications
that previously called `.model(...)` or `.model_opt(...)` on the returned raw
request builder should retain the provider `CompletionModel` and use its raw
request API, or construct a separate `Agent` for that model selection.
- *(providers)* [**breaking**] Move Together, OpenRouter, and Mistral embeddings onto the shared `GenericEmbeddingModel`, with provider-specific endpoint and typed request-shaping hooks. Together now forwards configured embedding dimensions, Mistral maps Codestral Embed dimensions to `output_dimension` while rejecting dimensions for fixed-size models, compatible providers may omit usage without weakening OpenAI's public response type, and Base64 response encoding is rejected before sending because the shared parser accepts numeric vectors. Remove the superseded provider-specific embedding response/data types, Together's API envelope module, and OpenRouter's duplicate `EncodingFormat`.
- *(tool)* [**breaking**] Replace the parallel tool-execution APIs with one structured path. Typed tools now implement only `Tool::call(&mut ToolContext, Args) -> Result<Output, Error>`; author-facing errors remain typed until private runtime erasure normalizes them into `ToolExecutionError`, `ToolContext` carries inbound values and host-only result metadata, `ToolResult` is the single runtime observation, and `ToolSet::execute` / `ToolServerHandle::execute` are the dispatch surfaces. Event-specific hook action types make invalid event/action combinations unrepresentable.
- Tool implementations: retain one typed `type Error` for ordinary `?` propagation and direct-call tests; remove `classify_error`, `call_with_extensions`, and `call_structured`. The optional `map_error` method classifies domain failures at the erased boundary, while its default preserves the source as `Other`. Return refusals through `map_error` with `ToolExecutionError::refused`, and attach host-only result metadata with `ToolContext::insert_result`.
Expand Down
184 changes: 58 additions & 126 deletions crates/rig-core/src/agent/completion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ use super::runner::AgentRunner;
use crate::{
agent::prompt_request::streaming::StreamingPromptRequest,
completion::{
Chat, Completion, CompletionError, CompletionModel, CompletionRequestBuilder, Document,
GetTokenUsage, Message, Prompt, PromptError, TypedPrompt,
Chat, CompletionError, CompletionModel, CompletionRequestBuilder, Document, GetTokenUsage,
Message, Prompt, PromptError, ToolDefinition, TypedPrompt,
},
json_utils,
message::ToolChoice,
streaming::{StreamingChat, StreamingCompletion, StreamingPrompt},
tool::server::{ToolRegistrySnapshot, ToolServerHandle},
streaming::{StreamingChat, StreamingPrompt},
tool::server::{ToolRegistrySnapshot, ToolServerError, ToolServerHandle},
vector_store::{VectorStoreError, request::VectorSearchRequest},
wasm_compat::WasmCompatSend,
};
Expand Down Expand Up @@ -224,49 +224,6 @@ pub(crate) fn allowed_tool_names_for_choice(
Ok(allowed)
}

/// Helper function to build a completion request from agent components.
/// This is used by `Agent::completion()` to preserve the public completion API.
#[allow(clippy::too_many_arguments)]
pub(crate) async fn build_completion_request<M: CompletionModel>(
model: &Arc<M>,
prompt: Message,
chat_history: &[Message],
preamble: Option<&str>,
static_context: &[Document],
temperature: Option<f64>,
max_tokens: Option<u64>,
additional_params: Option<&serde_json::Value>,
record_telemetry_content: bool,
tool_choice: Option<&ToolChoice>,
tool_server_handle: &ToolServerHandle,
dynamic_context: &DynamicContextStore,
output_schema: Option<&schemars::Schema>,
) -> Result<CompletionRequestBuilder<M>, CompletionError> {
Ok(build_prepared_completion_request(
model,
prompt,
chat_history,
preamble,
static_context,
temperature,
max_tokens,
additional_params,
record_telemetry_content,
tool_choice,
tool_server_handle,
dynamic_context,
output_schema,
// The single-shot `Agent::completion()` API has no run loop to consume an
// output-tool call, so it always uses native structured output (#1928).
&OutputMode::Native,
None,
// No agent run loop, so no `CompletionCall` hook to override the request.
None,
)
.await?
.builder)
}

/// Helper function to build a completion request from agent components while
/// preserving the executable Rig tool names sent to the provider.
#[allow(clippy::too_many_arguments)]
Expand All @@ -286,6 +243,8 @@ pub(crate) async fn build_prepared_completion_request<M: CompletionModel>(
output_schema: Option<&schemars::Schema>,
output_mode: &OutputMode,
committed_output_tool: Option<&str>,
output_tool_description: Option<&str>,
augment_output_preamble: bool,
request_patch: Option<&RequestPatch>,
) -> Result<PreparedCompletionRequest<M>, CompletionError> {
// Apply a per-turn request patch (the merged patch from every `CompletionCall`
Expand Down Expand Up @@ -520,14 +479,17 @@ pub(crate) async fn build_prepared_completion_request<M: CompletionModel>(
let effective_preamble: Option<String> = {
let base = preamble.map(str::to_owned);
let instruction = match &resolved_mode {
OutputMode::Tool => output_tool_name.as_deref().map(|name| {
format!(
"When you have gathered enough information to answer, call the `{name}` \
OutputMode::Tool if augment_output_preamble => {
output_tool_name.as_deref().map(|name| {
format!(
"When you have gathered enough information to answer, call the `{name}` \
tool exactly once with your final answer. Its arguments are the structured \
result and must satisfy the required schema. Do not return the final answer \
as plain text."
)
}),
)
})
}
OutputMode::Tool => None,
OutputMode::Prompted => output_schema.map(|schema| {
let schema_json = serde_json::to_string(schema.as_value()).unwrap_or_default();
format!(
Expand Down Expand Up @@ -568,9 +530,11 @@ pub(crate) async fn build_prepared_completion_request<M: CompletionModel>(
if let (Some(name), Some(schema)) = (&output_tool_name, output_schema) {
tooldefs.push(crate::completion::ToolDefinition {
name: name.clone(),
description: "Call this tool exactly once with your final answer when you are done. \
Its arguments are the structured result and must satisfy the output \
schema."
description: output_tool_description
.unwrap_or(
"Call this tool exactly once with your final answer when you are done. \
Its arguments are the structured result and must satisfy the output schema.",
)
.to_string(),
parameters: schema.clone().to_value(),
});
Expand Down Expand Up @@ -671,57 +635,66 @@ where
M: CompletionModel,
{
/// Name of the agent used for logging and debugging
pub name: Option<String>,
pub(crate) name: Option<String>,
/// Agent description. Primarily useful when using sub-agents as part of an agent workflow and converting agents to other formats.
pub description: Option<String>,
pub(crate) description: Option<String>,
/// Completion model (e.g.: OpenAI's gpt-3.5-turbo-1106, Cohere's command-r)
pub model: Arc<M>,
pub(crate) model: Arc<M>,
/// System prompt
pub preamble: Option<String>,
pub(crate) preamble: Option<String>,
/// Context documents always available to the agent
pub static_context: Vec<Document>,
pub(crate) static_context: Vec<Document>,
/// Temperature of the model
pub temperature: Option<f64>,
pub(crate) temperature: Option<f64>,
/// Maximum number of tokens for the completion
pub max_tokens: Option<u64>,
pub(crate) max_tokens: Option<u64>,
/// Additional parameters to be passed to the model
pub additional_params: Option<serde_json::Value>,
pub(crate) additional_params: Option<serde_json::Value>,
/// Whether to record sensitive request, response, and tool content on GenAI spans.
///
/// Defaults to `false`. Enabling this can expose prompts, retrieved context,
/// tool results, model responses, and other sensitive or high-cardinality data
/// through OpenTelemetry span attributes, which can increase observability
/// backend storage and query costs.
pub record_telemetry_content: bool,
pub tool_server_handle: ToolServerHandle,
pub(crate) record_telemetry_content: bool,
pub(crate) tool_server_handle: ToolServerHandle,
/// List of vector store, with the sample number
pub dynamic_context: DynamicContextStore,
pub(crate) dynamic_context: DynamicContextStore,
/// Whether or not the underlying LLM should be forced to use a tool before providing a response.
pub tool_choice: Option<ToolChoice>,
pub(crate) tool_choice: Option<ToolChoice>,
/// Default total model-call budget, including the initial call and every
/// retry or continuation. `None` uses the implicit budget of one.
pub default_max_turns: Option<usize>,
pub(crate) default_max_turns: Option<usize>,
/// Default hook stack applied to every prompt request and runner created
/// from this agent. Empty by default.
pub hooks: HookStack<M>,
pub(crate) hooks: HookStack<M>,
/// Optional JSON Schema for structured output. When set, providers that support
/// native structured outputs will constrain the model's response to match this schema.
pub output_schema: Option<schemars::Schema>,
pub(crate) output_schema: Option<schemars::Schema>,
/// How `output_schema` is enforced — tool call, native structured output, or
/// prompt injection (see [`OutputMode`] and issue #1928).
pub output_mode: OutputMode,
pub(crate) output_mode: OutputMode,
/// Optional conversation memory backend that loads/saves history per conversation id.
pub memory: Option<Arc<dyn crate::memory::ConversationMemory>>,
pub(crate) memory: Option<Arc<dyn crate::memory::ConversationMemory>>,
/// Optional default conversation id used when none is set per-request.
pub default_conversation_id: Option<String>,
pub(crate) default_conversation_id: Option<String>,
}

impl<M> Agent<M>
where
M: CompletionModel,
{
/// Returns the name of the agent.
pub(crate) fn name(&self) -> &str {
/// Returns the configured agent name.
pub fn name(&self) -> Option<&str> {
self.name.as_deref()
}

/// Returns the configured agent description.
pub fn description(&self) -> Option<&str> {
self.description.as_deref()
}

pub(crate) fn name_or_default(&self) -> &str {
self.name.as_deref().unwrap_or(UNKNOWN_AGENT_NAME)
}

Expand All @@ -731,38 +704,16 @@ where
pub fn runner(&self, prompt: impl Into<Message>) -> AgentRunner<M> {
AgentRunner::from_agent(self, prompt)
}
}

impl<M> Completion<M> for Agent<M>
where
M: CompletionModel,
{
async fn completion<I, T>(
/// Resolve the provider-facing tool definitions available for a prompt.
///
/// This read-only view does not expose tool dispatch. Agent execution and
/// tool lifecycle hooks remain owned by [`Self::runner`].
pub async fn tool_definitions(
&self,
prompt: impl Into<Message> + WasmCompatSend,
chat_history: I,
) -> Result<CompletionRequestBuilder<M>, CompletionError>
where
I: IntoIterator<Item = T>,
T: Into<Message>,
{
let history: Vec<Message> = chat_history.into_iter().map(Into::into).collect();
build_completion_request(
&self.model,
prompt.into(),
&history,
self.preamble.as_deref(),
&self.static_context,
self.temperature,
self.max_tokens,
self.additional_params.as_ref(),
self.record_telemetry_content,
self.tool_choice.as_ref(),
&self.tool_server_handle,
&self.dynamic_context,
self.output_schema.as_ref(),
)
.await
prompt: Option<String>,
) -> Result<Vec<ToolDefinition>, ToolServerError> {
self.tool_server_handle.get_tool_defs(prompt).await
}
}

Expand Down Expand Up @@ -791,7 +742,7 @@ impl<M> Prompt for &Agent<M>
where
M: CompletionModel + 'static,
{
#[tracing::instrument(skip(self, prompt), fields(agent_name = self.name()))]
#[tracing::instrument(skip(self, prompt), fields(agent_name = self.name_or_default()))]
fn prompt(
&self,
prompt: impl Into<Message> + WasmCompatSend,
Expand All @@ -805,7 +756,7 @@ impl<M> Chat for Agent<M>
where
M: CompletionModel + 'static,
{
#[tracing::instrument(skip(self, prompt, chat_history), fields(agent_name = self.name()))]
#[tracing::instrument(skip(self, prompt, chat_history), fields(agent_name = self.name_or_default()))]
async fn chat(
&self,
prompt: impl Into<Message> + WasmCompatSend,
Expand All @@ -824,25 +775,6 @@ where
}
}

impl<M> StreamingCompletion<M> for Agent<M>
where
M: CompletionModel,
{
async fn stream_completion<I, T>(
&self,
prompt: impl Into<Message> + WasmCompatSend,
chat_history: I,
) -> Result<CompletionRequestBuilder<M>, CompletionError>
where
I: IntoIterator<Item = T> + WasmCompatSend,
T: Into<Message>,
{
// Reuse the existing completion implementation to build the request
// This ensures streaming and non-streaming use the same request building logic
self.completion(prompt, chat_history).await
}
}

impl<M> StreamingPrompt<M, M::StreamingResponse> for Agent<M>
where
M: CompletionModel + 'static,
Expand Down
Loading
Loading