1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
use serde::{Deserialize, Serialize};
use std::fmt;
use crate::{
AgentOutput, BoxError, ContentPart, Document, Documents, FunctionDefinition, Json, Message,
Resource,
};
/// LLM completion capability exposed by an agent context.
pub trait CompletionFeatures: Sized {
/// Generates a completion for the request and optional resources.
fn completion(
&self,
req: CompletionRequest,
resources: Vec<Resource>,
) -> impl Future<Output = Result<AgentOutput, BoxError>> + Send;
/// Returns the name of the model.
fn model_name(&self) -> String;
}
/// Provider-agnostic reasoning/thinking effort requested for a completion.
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ModelEffort {
/// Smallest reasoning budget supported by the provider.
Minimal,
/// Low reasoning budget.
Low,
/// Medium reasoning budget.
Medium,
/// High reasoning budget.
High,
/// Maximum reasoning budget supported by the provider.
Max,
}
impl ModelEffort {
/// Returns the lowercase wire value for this effort level.
pub fn as_str(self) -> &'static str {
match self {
Self::Minimal => "minimal",
Self::Low => "low",
Self::Medium => "medium",
Self::High => "high",
Self::Max => "max",
}
}
}
impl fmt::Display for ModelEffort {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
/// Provider-neutral completion request.
#[derive(Debug, Clone, Default)]
pub struct CompletionRequest {
/// System instructions sent to the completion provider.
pub instructions: String,
/// Role used for `prompt` and `content`; defaults to `user` when omitted.
pub role: Option<String>,
/// The chat history to be sent to the completion model provider.
///
/// This is the provider-neutral, persistable view (see [`ContentPart`]). Adapters
/// convert it into provider messages, which is lossy by design: a provider's per-turn
/// intermediate state does not survive the round trip. Use [`Self::raw_history`] to keep
/// a round lossless.
pub chat_history: Vec<Message>,
/// Provider-specific history used by model adapters. It is empty for most callers.
///
/// # Why this exists
///
/// Some providers require their own opaque per-turn state to be echoed back on the next
/// request — Anthropic's `thinking.signature`, Gemini's `thoughtSignature`. Round-tripping
/// that through [`ContentPart`] would mean polluting the persisted wire type with
/// provider-specific fields, so instead the adapter stores the provider's own message
/// JSON here verbatim.
///
/// # The contract
///
/// - Each turn, the runner appends the response's
/// [`AgentOutput::raw_history`](crate::model::AgentOutput::raw_history) onto this field
/// and clears [`Self::chat_history`], so `raw_history` accumulates across one complete
/// reasoning round.
/// - Every adapter must send `raw_history` **before** the messages converted from
/// `chat_history`, so provider state stays intact for the whole round.
/// - It belongs to the model that produced it. Adapters send it verbatim, so handing one
/// provider another's messages makes the request unparseable and the provider rejects the
/// whole call. A caller that reroutes a live conversation to a different model must clear
/// this field and replay `chat_history` instead.
/// - It is scoped to one in-process round only: the engine clears it at the RPC boundary
/// and it is `#[serde(skip)]` on `AgentOutput`, so it never reaches a persisted
/// conversation or a remote caller.
///
/// A resumed conversation therefore replays from `chat_history` alone and legitimately
/// carries no provider intermediate state. Adapters must tolerate that rather than emit a
/// block the provider will reject.
pub raw_history: Vec<Json>,
/// The documents to embed into the prompt.
pub documents: Documents,
/// Prompt sent to the completion provider using `role`.
/// It can be empty.
pub prompt: String,
/// The content parts to be sent to the completion model provider.
/// It can be empty.
pub content: Vec<ContentPart>,
/// The tools to be sent to the completion model provider.
pub tools: Vec<FunctionDefinition>,
/// Whether the tool choice is required.
pub tool_choice_required: bool,
/// Sampling temperature requested from the provider, usually in the `[0.0, 2.0]` range.
pub temperature: Option<f64>,
/// Upper bound for the number of tokens that can be generated for a response.
pub max_output_tokens: Option<usize>,
/// An object specifying the JSON format that the model must output.
pub output_schema: Option<Json>,
/// The stop sequence to be sent to the completion model provider.
/// OpenAI's Responses adapter rejects non-empty sequences because that API
/// does not support them.
pub stop: Option<Vec<String>>,
/// The name or label of the model to be used for the completion request.
pub model: Option<String>,
/// Optional reasoning/thinking effort for providers and models that support it.
pub effort: Option<ModelEffort>,
}
impl CompletionRequest {
/// Adds a document to the request.
pub fn context(mut self, id: String, text: String) -> Self {
self.documents.append(Document::from_text(id, text));
self
}
/// Adds multiple documents to the request.
pub fn append_documents(mut self, docs: Documents) -> Self {
self.documents.extend(docs);
self
}
/// Adds multiple tools to the request.
pub fn append_tools(mut self, tools: Vec<FunctionDefinition>) -> Self {
self.tools.extend(tools);
self
}
}