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
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
use crate::claim_manager::ClaimManager;
use crate::completion_evaluated_prompt::CompletionEvaluatedPrompt;
use crate::error::Error;
use crate::mcp_server::McpServerConnection;
use rig::OneOrMany;
use rig::completion::{AssistantContent, Completion, CompletionModel, Message};
use rig::message::UserContent;
use rig::tool::ToolDyn;
use std::collections::HashSet;
use tracing::info;
pub struct Agent<M: CompletionModel> {
completion_agent: rig::agent::Agent<M>,
mcp_connections: Vec<ValidatedMcpServerConnection>,
revalidating_tooling: HashSet<String>,
agent_name: String,
agent_version: String,
preamble: Option<CompletionEvaluatedPrompt>,
claim_manager: Option<ClaimManager>,
}
struct ValidatedMcpServerConnection {
connection: McpServerConnection,
tools_validated: bool,
}
pub struct CompletionResult {
/// Entire message history
pub messages: Vec<Message>,
/// The texts returned by the completion agent. It is possible for this to be empty
pub texts: Vec<String>,
/// Quantity of tools used. If this is non-zero, it is likely texts are empty.
pub tools_used: u32,
}
impl<M: CompletionModel> Agent<M> {
///
/// Creates a new Coral agent using an underlying completion agent.
pub fn new(completion_agent: rig::agent::Agent<M>) -> Self {
Self {
completion_agent,
mcp_connections: Vec::new(),
revalidating_tooling: HashSet::new(),
agent_name: env!("CARGO_PKG_NAME").to_string(),
agent_version: env!("CARGO_PKG_VERSION").to_string(),
preamble: None,
claim_manager: None,
}
}
///
/// Agent name. Used to identify this agent in MCP servers.
pub fn agent_name(mut self, name: impl Into<String>) -> Self {
self.agent_name = name.into();
self
}
///
/// Agent version. Used to identify this agent in MCP servers.
pub fn agent_version(mut self, version: impl Into<String>) -> Self {
self.agent_version = version.into();
self
}
///
/// Adds an MCP server to the Agent. MCP server tools will be evaluated before requests are
/// made
pub fn mcp_server(mut self, connection: McpServerConnection) -> Self {
self.mcp_connections.push(ValidatedMcpServerConnection {
connection,
tools_validated: false,
});
self
}
///
/// Sets the preamble for this agent to a specific [`CompletionEvaluatedPrompt`] instance. Note
/// that if this is not set, the default string provided to the inner agent model will be used.
///
/// The preamble will be evaluated in each call to [`Self::run_completion`].
pub fn preamble(mut self, preamble: CompletionEvaluatedPrompt) -> Self {
self.preamble = Some(preamble);
self
}
///
/// Sets the claim manager to use it with this Agent. If no claim manager is set, no claims
/// will be made for this agent. If you plan to export an agent, you must claim from the agent.
pub fn claim_manager(mut self, claim_manager: ClaimManager) -> Self {
self.claim_manager = Some(claim_manager);
self
}
///
/// This function is responsible for making sure every [`McpServerConnection`] provided to this
/// agent has their tools validated as requested by the connection for a completion request.
///
/// A single [`McpServerConnection`] may choose:
/// - To have tooling skipped
/// - To have tooling evaluated once
/// - To have tooling evaluated before every completion
async fn validate_mcp_tooling(&mut self) -> Result<(), Error> {
// Remove any tooling that revalidates
self.revalidating_tooling.retain(|mcp_tool_name| {
self.completion_agent
.static_tools
.retain(|tool_name| tool_name != mcp_tool_name);
self.completion_agent.tools.delete_tool(mcp_tool_name);
false
});
let mut tools = Vec::new();
for mcp in self.mcp_connections.iter_mut() {
if (mcp.tools_validated && !mcp.connection.revalidate_tooling)
|| mcp.connection.skip_tooling
{
continue;
}
let mcp_tools = mcp.connection.get_tools().await?;
if !mcp.tools_validated {
for tool in mcp_tools.iter() {
info!(
"adding tool \"{}\" from mcp server \"{}\"",
tool.name(),
mcp.connection.identifier
);
}
}
mcp.tools_validated = true;
// If this MCP connection revalidates tooling, the list of tools that are revalidated
// needs to be recorded so that it can be removed from the completion agent on the next
// time this function is called
if mcp.connection.revalidate_tooling {
self.revalidating_tooling
.extend(mcp_tools.iter().map(|tool| tool.name().clone()))
}
tools.extend(mcp_tools);
}
// Add new or revalidated tooling to the completion agent's tooling
let agent_tools = std::mem::take(&mut self.completion_agent.tools);
self.completion_agent
.static_tools
.extend(tools.iter().map(|tool| tool.name().clone()));
self.completion_agent.tools = tools.into_iter().fold(agent_tools, |mut toolset, tool| {
toolset.add_tool(tool);
toolset
});
Ok(())
}
///
/// If there was a preamble provided to this agent, this function will evaluate it, and if the
/// evaluation succeeds, the inner model's preamble field will be overwritten to this newly
/// evaluated prompt.
///
/// If there was no preamble provided to this agent, nothing will happen here.
///
/// If the evaluation of the prompt fails (e.g., failure to locate a resource), this function will
/// return an error.
async fn validate_preamble(&mut self) -> Result<(), Error> {
if let Some(prompt) = &self.preamble {
match prompt.evaluate().await {
Ok(prompt) => self.completion_agent.preamble = prompt,
Err(e) => return Err(e),
}
}
Ok(())
}
/// Performs a completion request
///
/// This function, in order:
/// 1. Validates all tooling and documents on any connected MCP server (that require validation)
/// 2. Performs one completion request to the underlying completion agent
/// 3. Runs any tool calls that came back from the request
/// 4. Appends all messages in the response and any tool call results to the message history
///
/// If telemetry is enabled, the last step of this function will be to post telemetry data
/// to the Coral server.
///
/// # Arguments
/// * `messages` - The full message history for this completion request. It is assumed that
/// this contains the necessary prompts for the completion. This function will panic if given
/// an empty message history.
///
pub async fn run_completion(
&mut self,
mut messages: Vec<Message>,
) -> Result<CompletionResult, Error> {
self.validate_mcp_tooling().await?;
self.validate_preamble().await?;
// Take the last message from the stack as a prompt
let prompt = messages
.pop()
.expect("cannot send completion with no messages");
let resp = self
.completion_agent
.completion(prompt.clone(), messages.clone())
.await
.map_err(Error::CompletionError)?
.send()
.await
.map_err(Error::CompletionError)?;
messages.push(prompt);
messages.push(Message::Assistant {
id: None,
content: resp.choice.clone(),
});
if let Some(claim_manager) = &self.claim_manager {
claim_manager.claim_tokens(&resp.usage).await?;
}
let mut tools_used = 0;
let mut texts = Vec::new();
for choice in resp.choice {
match choice {
AssistantContent::ToolCall(tool_call) => {
tools_used = tools_used + 1;
let output = self
.completion_agent
.tools
.call(
&tool_call.function.name,
tool_call.function.arguments.to_string(),
)
.await
.map_err(Error::ToolsetError)?;
if let Some(claim_manager) = &self.claim_manager {
claim_manager
.claim_tool_call(tool_call.function.name.clone())
.await?;
}
messages.push(if let Some(call_id) = tool_call.call_id {
UserContent::tool_result_with_call_id(
tool_call.id.clone(),
call_id,
OneOrMany::one(output.into()),
)
.into()
} else {
UserContent::tool_result(
tool_call.id.clone(),
OneOrMany::one(output.into()),
)
.into()
})
}
AssistantContent::Text(text) => {
texts.push(text.text.clone());
}
_ => {}
}
}
if let Some(claim_manager) = &self.claim_manager {
if tools_used == 0 {
claim_manager.claim_iteration().await?;
} else {
claim_manager.claim_tool_iteration().await?;
}
}
Ok(CompletionResult {
messages,
texts,
tools_used,
})
}
}