feat(desktop): embed BoosAPI Desktop client as a subproject
Vendor the rebranded BoosAPI Desktop client (Tauri + Vue) into metapi as a monorepo subdirectory under desktop/. Source only — node_modules, dist, Cargo target and the upstream .git history are excluded. desktop/ keeps its own independent Cargo workspace (desktop/src-tauri with crates/*); it is not part of the backend boosapi crate, so a `cargo check` at the metapi root still compiles only the backend. Build the desktop app from desktop/src-tauri per desktop/README.md. Note: app icons are still placeholder assets pending the final BoosAPI logo. The updater signing keys live outside the repo and are not included.
This commit is contained in:
@@ -0,0 +1,408 @@
|
||||
use crate::api::ApiClient;
|
||||
use crate::costtracker::CostTracker;
|
||||
use crate::hooks::HookConfig;
|
||||
use crate::mcp::{self, McpClient};
|
||||
use crate::tools::ToolRegistry;
|
||||
use crate::types::*;
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
/// Configuration options for creating an agent.
|
||||
pub struct AgentOptions {
|
||||
/// LLM model ID (default: claude-sonnet-4-6-20250514).
|
||||
pub model: Option<String>,
|
||||
/// API key for authentication.
|
||||
pub api_key: Option<String>,
|
||||
/// Custom API base URL.
|
||||
pub base_url: Option<String>,
|
||||
/// Custom LLM provider (bypasses built-in ApiClient creation).
|
||||
pub provider: Option<Arc<dyn crate::LLMProvider>>,
|
||||
/// Working directory for tool execution.
|
||||
pub cwd: Option<String>,
|
||||
/// Custom system prompt (replaces default).
|
||||
pub system_prompt: Option<String>,
|
||||
/// Additional text appended to the default system prompt.
|
||||
pub append_system_prompt: Option<String>,
|
||||
/// Skills context summary for system prompt injection.
|
||||
pub skills_summary: Option<String>,
|
||||
/// Maximum number of agentic turns (default: 100).
|
||||
pub max_turns: Option<u32>,
|
||||
/// Maximum budget in USD.
|
||||
pub max_budget_usd: Option<f64>,
|
||||
/// Permission mode for tool access control.
|
||||
pub permission_mode: Option<PermissionMode>,
|
||||
/// Tool names to allow (whitelist).
|
||||
pub allowed_tools: Option<Vec<String>>,
|
||||
/// Tool names to deny (blacklist).
|
||||
pub disallowed_tools: Option<Vec<String>>,
|
||||
/// Custom permission callback.
|
||||
pub can_use_tool: Option<CanUseToolFn>,
|
||||
/// Callback for user interaction (AskUserQuestion tool).
|
||||
pub ask_fn: Option<crate::tools::askuser::AskUserFn>,
|
||||
/// Custom tools to register.
|
||||
pub custom_tools: Vec<Arc<dyn Tool>>,
|
||||
/// MCP server configurations.
|
||||
pub mcp_servers: HashMap<String, McpServerConfig>,
|
||||
/// Subagent definitions.
|
||||
pub agents: HashMap<String, SubagentDefinition>,
|
||||
/// Extended thinking configuration.
|
||||
pub thinking: Option<ThinkingConfig>,
|
||||
/// Maximum output tokens per response.
|
||||
pub max_tokens: Option<u64>,
|
||||
/// Structured output JSON schema.
|
||||
pub json_schema: Option<Value>,
|
||||
/// Hook configuration.
|
||||
pub hooks: Option<HookConfig>,
|
||||
/// Cancellation token used to stop the running agent loop and tools.
|
||||
pub abort_signal: Option<CancellationToken>,
|
||||
/// Custom HTTP headers for API requests.
|
||||
pub custom_headers: HashMap<String, String>,
|
||||
/// Resume the most recent session in the current working directory.
|
||||
pub continue_session: Option<bool>,
|
||||
/// Resume a specific session by ID.
|
||||
pub resume: Option<String>,
|
||||
/// Fork instead of continuing a session.
|
||||
pub fork_session: Option<bool>,
|
||||
/// Control whether the session is persisted to disk.
|
||||
pub persist_session: Option<bool>,
|
||||
/// Explicit session ID to use (instead of auto-generating one).
|
||||
pub session_id: Option<String>,
|
||||
/// Effort level for reasoning: "low", "medium", "high", "max".
|
||||
pub effort: Option<String>,
|
||||
/// Fallback model if primary is unavailable.
|
||||
pub fallback_model: Option<String>,
|
||||
/// Sandbox configuration for restricting tool execution.
|
||||
pub sandbox: Option<SandboxSettings>,
|
||||
/// Enable debug mode.
|
||||
pub debug: Option<bool>,
|
||||
/// Additional working directories.
|
||||
pub additional_directories: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl Default for AgentOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
model: None,
|
||||
api_key: None,
|
||||
base_url: None,
|
||||
provider: None,
|
||||
cwd: None,
|
||||
system_prompt: None,
|
||||
append_system_prompt: None,
|
||||
skills_summary: None,
|
||||
max_turns: None,
|
||||
max_budget_usd: None,
|
||||
permission_mode: None,
|
||||
allowed_tools: None,
|
||||
disallowed_tools: None,
|
||||
can_use_tool: None,
|
||||
ask_fn: None,
|
||||
custom_tools: Vec::new(),
|
||||
mcp_servers: HashMap::new(),
|
||||
agents: HashMap::new(),
|
||||
thinking: None,
|
||||
max_tokens: None,
|
||||
json_schema: None,
|
||||
hooks: None,
|
||||
abort_signal: None,
|
||||
custom_headers: HashMap::new(),
|
||||
continue_session: None,
|
||||
resume: None,
|
||||
fork_session: None,
|
||||
persist_session: None,
|
||||
session_id: None,
|
||||
effort: None,
|
||||
fallback_model: None,
|
||||
sandbox: None,
|
||||
debug: None,
|
||||
additional_directories: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Subagent definition.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SubagentDefinition {
|
||||
pub description: String,
|
||||
pub instructions: Option<String>,
|
||||
pub tools: Option<Vec<String>>,
|
||||
pub model: Option<String>,
|
||||
}
|
||||
|
||||
/// The main Agent struct that orchestrates the agentic loop.
|
||||
pub struct Agent {
|
||||
pub(crate) api_client: ApiClient,
|
||||
pub(crate) registry: ToolRegistry,
|
||||
/// Conversation messages.
|
||||
pub messages: Vec<Message>,
|
||||
/// Working directory.
|
||||
pub cwd: String,
|
||||
/// Custom system prompt.
|
||||
pub system_prompt: Option<String>,
|
||||
/// Additional system prompt text.
|
||||
pub append_system_prompt: Option<String>,
|
||||
/// Skills context summary.
|
||||
pub skills_summary: Option<String>,
|
||||
/// Maximum agentic turns.
|
||||
pub max_turns: u32,
|
||||
/// Maximum budget in USD.
|
||||
pub max_budget_usd: Option<f64>,
|
||||
pub(crate) cost_tracker: CostTracker,
|
||||
pub(crate) mcp_client: Arc<McpClient>,
|
||||
/// Subagent definitions.
|
||||
pub agents: HashMap<String, SubagentDefinition>,
|
||||
/// Extended thinking configuration.
|
||||
pub thinking: Option<ThinkingConfig>,
|
||||
/// Maximum output tokens.
|
||||
pub max_tokens: Option<u64>,
|
||||
/// Structured output JSON schema.
|
||||
pub json_schema: Option<Value>,
|
||||
/// Hook configuration.
|
||||
pub hooks: HookConfig,
|
||||
pub(crate) can_use_tool: Option<CanUseToolFn>,
|
||||
pub(crate) session_id: String,
|
||||
pub(crate) abort_signal: CancellationToken,
|
||||
}
|
||||
|
||||
impl Agent {
|
||||
/// Create a new agent with the given options.
|
||||
pub async fn new(options: AgentOptions) -> Result<Self, String> {
|
||||
let cwd = options.cwd.unwrap_or_else(|| {
|
||||
std::env::current_dir()
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
});
|
||||
|
||||
let api_client = if let Some(provider) = options.provider {
|
||||
ApiClient::with_provider(provider, options.model)
|
||||
} else {
|
||||
ApiClient::new(options.api_key, options.base_url, options.model)
|
||||
};
|
||||
|
||||
let mut registry = ToolRegistry::default_registry();
|
||||
|
||||
// Replace default AskUserTool with configured one if ask_fn is provided
|
||||
if let Some(ask_fn) = options.ask_fn {
|
||||
registry.register_replace(Arc::new(crate::tools::askuser::AskUserTool::new(ask_fn)));
|
||||
}
|
||||
|
||||
// Register custom tools
|
||||
for tool in options.custom_tools {
|
||||
registry.register(tool);
|
||||
}
|
||||
|
||||
// Apply allowed/disallowed tool filters
|
||||
if let Some(allowed) = &options.allowed_tools {
|
||||
let allowed_refs: Vec<&str> = allowed.iter().map(|s| s.as_str()).collect();
|
||||
registry.retain(&allowed_refs);
|
||||
}
|
||||
if let Some(disallowed) = &options.disallowed_tools {
|
||||
let disallowed_refs: Vec<&str> = disallowed.iter().map(|s| s.as_str()).collect();
|
||||
registry.remove(&disallowed_refs);
|
||||
}
|
||||
|
||||
// Connect MCP servers
|
||||
let mcp_client = Arc::new(McpClient::new());
|
||||
for (name, config) in &options.mcp_servers {
|
||||
match mcp_client.connect(name, config.clone()).await {
|
||||
Ok(tools) => {
|
||||
let mcp_tools = mcp::create_mcp_tools(name, &tools, mcp_client.clone());
|
||||
for tool in mcp_tools {
|
||||
registry.register(tool);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("Failed to connect MCP server '{}': {}", name, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let session_id = uuid::Uuid::new_v4().to_string();
|
||||
|
||||
Ok(Self {
|
||||
api_client,
|
||||
registry,
|
||||
messages: Vec::new(),
|
||||
cwd,
|
||||
system_prompt: options.system_prompt,
|
||||
append_system_prompt: options.append_system_prompt,
|
||||
skills_summary: options.skills_summary,
|
||||
max_turns: options.max_turns.unwrap_or(100),
|
||||
max_budget_usd: options.max_budget_usd,
|
||||
cost_tracker: CostTracker::new(),
|
||||
mcp_client,
|
||||
agents: options.agents,
|
||||
thinking: options.thinking,
|
||||
max_tokens: options.max_tokens,
|
||||
json_schema: options.json_schema,
|
||||
hooks: options.hooks.unwrap_or_default(),
|
||||
can_use_tool: options.can_use_tool,
|
||||
session_id,
|
||||
abort_signal: options.abort_signal.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Execute a query and return a channel of streaming SDK messages.
|
||||
pub async fn query(
|
||||
&mut self,
|
||||
prompt: &str,
|
||||
) -> (mpsc::Receiver<SDKMessage>, tokio::task::JoinHandle<()>) {
|
||||
let (tx, rx) = mpsc::channel(100);
|
||||
|
||||
// Add user message
|
||||
let user_msg = crate::utils::messages::create_user_message(prompt);
|
||||
self.messages.push(user_msg);
|
||||
|
||||
// Clone what we need for the async loop
|
||||
let messages = self.messages.clone();
|
||||
let api_client = self.api_client.clone();
|
||||
let cwd = self.cwd.clone();
|
||||
let system_prompt = self.system_prompt.clone();
|
||||
let append_system_prompt = self.append_system_prompt.clone();
|
||||
let skills_summary = self.skills_summary.clone();
|
||||
let max_turns = self.max_turns;
|
||||
let max_budget_usd = self.max_budget_usd;
|
||||
let cost_tracker = self.cost_tracker.clone();
|
||||
let thinking = self.thinking.clone();
|
||||
let max_tokens = self.max_tokens;
|
||||
let registry = std::sync::Arc::new(self.registry_snapshot());
|
||||
let can_use_tool = self.can_use_tool.clone();
|
||||
let abort_signal = self.abort_signal.clone();
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
let result: Result<Vec<Message>, String> = super::r#loop::run_loop(
|
||||
api_client,
|
||||
messages,
|
||||
registry,
|
||||
&cwd,
|
||||
system_prompt.as_deref(),
|
||||
append_system_prompt.as_deref(),
|
||||
skills_summary.as_deref(),
|
||||
max_turns,
|
||||
max_budget_usd,
|
||||
&cost_tracker,
|
||||
thinking,
|
||||
max_tokens,
|
||||
can_use_tool.as_ref(),
|
||||
abort_signal,
|
||||
tx.clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
if let Err(e) = result {
|
||||
let _ = tx
|
||||
.send(SDKMessage::Error {
|
||||
message: e.to_string(),
|
||||
})
|
||||
.await;
|
||||
}
|
||||
});
|
||||
|
||||
(rx, handle)
|
||||
}
|
||||
|
||||
/// Blocking query that returns a complete QueryResult.
|
||||
pub async fn prompt(&mut self, text: &str) -> Result<QueryResult, String> {
|
||||
let start = std::time::Instant::now();
|
||||
let (mut rx, handle) = self.query(text).await;
|
||||
|
||||
let mut result_text = String::new();
|
||||
let mut usage = Usage::default();
|
||||
let mut num_turns = 0;
|
||||
let mut cost_usd = 0.0;
|
||||
let mut all_messages = Vec::new();
|
||||
|
||||
while let Some(msg) = rx.recv().await {
|
||||
match msg {
|
||||
SDKMessage::Result {
|
||||
text,
|
||||
usage: u,
|
||||
num_turns: t,
|
||||
cost_usd: c,
|
||||
messages,
|
||||
..
|
||||
} => {
|
||||
result_text = text;
|
||||
usage = u;
|
||||
num_turns = t;
|
||||
cost_usd = c;
|
||||
all_messages = messages;
|
||||
}
|
||||
SDKMessage::Assistant { message, .. } => {
|
||||
let text = crate::types::extract_text(&message);
|
||||
if !text.is_empty() {
|
||||
result_text = text;
|
||||
}
|
||||
all_messages.push(message);
|
||||
}
|
||||
SDKMessage::Error { message } => {
|
||||
return Err(message);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let _ = handle.await;
|
||||
let duration_ms = start.elapsed().as_millis() as u64;
|
||||
|
||||
// Update agent's message history
|
||||
self.messages = all_messages.clone();
|
||||
|
||||
Ok(QueryResult {
|
||||
text: result_text,
|
||||
usage,
|
||||
num_turns,
|
||||
cost_usd,
|
||||
duration_ms,
|
||||
messages: all_messages,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get current conversation messages.
|
||||
pub fn get_messages(&self) -> &[Message] {
|
||||
&self.messages
|
||||
}
|
||||
|
||||
/// Clear conversation history.
|
||||
pub fn clear(&mut self) {
|
||||
self.messages.clear();
|
||||
}
|
||||
|
||||
/// Set the model.
|
||||
pub fn set_model(&mut self, model: &str) {
|
||||
self.api_client.set_model(model.to_string());
|
||||
}
|
||||
|
||||
/// Get the current model.
|
||||
pub fn model(&self) -> &str {
|
||||
self.api_client.model()
|
||||
}
|
||||
|
||||
/// Get the cost tracker.
|
||||
pub fn cost_tracker(&self) -> &CostTracker {
|
||||
&self.cost_tracker
|
||||
}
|
||||
|
||||
/// Get session ID.
|
||||
pub fn session_id(&self) -> &str {
|
||||
&self.session_id
|
||||
}
|
||||
|
||||
/// Close the agent and clean up resources.
|
||||
pub async fn close(&self) {
|
||||
self.mcp_client.close_all().await;
|
||||
}
|
||||
|
||||
/// Create a snapshot of the current registry for use in async context.
|
||||
fn registry_snapshot(&self) -> ToolRegistry {
|
||||
let mut snapshot = ToolRegistry::new();
|
||||
for tool in self.registry.all() {
|
||||
snapshot.register(tool);
|
||||
}
|
||||
snapshot
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
use crate::api::ApiClient;
|
||||
use crate::context;
|
||||
use crate::costtracker::CostTracker;
|
||||
use crate::tools::{self, ToolRegistry};
|
||||
use crate::types::*;
|
||||
use crate::utils::{compact, messages as msg_utils, retry};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
/// Run the main agentic loop.
|
||||
pub(crate) async fn run_loop(
|
||||
api_client: ApiClient,
|
||||
mut messages: Vec<Message>,
|
||||
registry: Arc<ToolRegistry>,
|
||||
cwd: &str,
|
||||
system_prompt: Option<&str>,
|
||||
append_system_prompt: Option<&str>,
|
||||
skills_summary: Option<&str>,
|
||||
max_turns: u32,
|
||||
max_budget_usd: Option<f64>,
|
||||
cost_tracker: &CostTracker,
|
||||
thinking: Option<ThinkingConfig>,
|
||||
max_tokens: Option<u64>,
|
||||
can_use_tool: Option<&CanUseToolFn>,
|
||||
abort_signal: CancellationToken,
|
||||
tx: mpsc::Sender<SDKMessage>,
|
||||
) -> Result<Vec<Message>, String> {
|
||||
let tool_context = ToolUseContext::with_abort(cwd.to_string(), abort_signal);
|
||||
|
||||
// Build system prompt blocks
|
||||
let system_blocks =
|
||||
context::build_system_blocks(cwd, system_prompt, append_system_prompt, skills_summary);
|
||||
|
||||
// Build tool definitions for API
|
||||
let api_tools: Vec<ApiToolParam> = registry
|
||||
.all()
|
||||
.iter()
|
||||
.map(|tool| {
|
||||
let schema = tool.input_schema();
|
||||
ApiToolParam {
|
||||
name: tool.name().to_string(),
|
||||
description: tool.description().to_string(),
|
||||
input_schema: serde_json::to_value(&schema).unwrap_or_default(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let retry_config = retry::RetryConfig::default();
|
||||
let mut num_turns: u32 = 0;
|
||||
let mut total_usage = Usage::default();
|
||||
|
||||
// Send system message
|
||||
let _ = tx
|
||||
.send(SDKMessage::System {
|
||||
message: format!(
|
||||
"Agent started with {} tools ({})",
|
||||
api_tools.len(),
|
||||
match api_client.api_type() {
|
||||
crate::api::ApiType::AnthropicMessages => "anthropic",
|
||||
crate::api::ApiType::OpenAICompletions => "openai",
|
||||
}
|
||||
),
|
||||
})
|
||||
.await;
|
||||
|
||||
loop {
|
||||
if tool_context.abort_signal.is_cancelled() {
|
||||
return Err("Agent cancelled".to_string());
|
||||
}
|
||||
|
||||
// Check turn limit
|
||||
num_turns += 1;
|
||||
if num_turns > max_turns {
|
||||
let _ = tx
|
||||
.send(SDKMessage::Progress {
|
||||
message: format!("Reached max turns ({})", max_turns),
|
||||
})
|
||||
.await;
|
||||
break;
|
||||
}
|
||||
|
||||
// Check budget
|
||||
if let Some(budget) = max_budget_usd {
|
||||
let current_cost = cost_tracker.total_cost().await;
|
||||
if current_cost >= budget {
|
||||
let _ = tx
|
||||
.send(SDKMessage::Progress {
|
||||
message: format!("Budget exceeded: ${:.4} >= ${:.4}", current_cost, budget),
|
||||
})
|
||||
.await;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if auto-compaction is needed
|
||||
if compact::should_auto_compact(&messages, api_client.model()) {
|
||||
messages = compact::micro_compact_messages(&messages);
|
||||
}
|
||||
|
||||
// Normalize messages
|
||||
let normalized = msg_utils::normalize_messages(&messages);
|
||||
|
||||
// Call the API via provider abstraction with retry
|
||||
let api_client_ref = &api_client;
|
||||
let system_blocks_ref = &system_blocks;
|
||||
let api_tools_ref = &api_tools;
|
||||
let thinking_ref = &thinking;
|
||||
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
let response = tokio::select! {
|
||||
response = retry::with_retry(&retry_config, || async {
|
||||
api_client_ref
|
||||
.create_message(
|
||||
&normalized,
|
||||
Some(system_blocks_ref.clone()),
|
||||
Some(api_tools_ref.clone()),
|
||||
max_tokens,
|
||||
thinking_ref.clone(),
|
||||
Some(tx.clone()),
|
||||
)
|
||||
.await
|
||||
}) => response,
|
||||
_ = tool_context.abort_signal.cancelled() => {
|
||||
return Err("Agent cancelled".to_string());
|
||||
}
|
||||
};
|
||||
|
||||
let provider_response = match response {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
if retry::is_auth_error(&e) {
|
||||
return Err(format!("Authentication error: {}. Check your API key.", e));
|
||||
}
|
||||
return Err(format!("API error: {}", e));
|
||||
}
|
||||
};
|
||||
|
||||
let api_duration = start.elapsed().as_millis() as u64;
|
||||
cost_tracker.add_api_duration(api_duration).await;
|
||||
cost_tracker
|
||||
.add_usage(api_client.model(), &provider_response.usage)
|
||||
.await;
|
||||
|
||||
let usage = &provider_response.usage;
|
||||
let assistant_msg = provider_response.message;
|
||||
let stop_reason = provider_response.stop_reason;
|
||||
|
||||
// Accumulate usage
|
||||
total_usage.input_tokens += usage.input_tokens;
|
||||
total_usage.output_tokens += usage.output_tokens;
|
||||
total_usage.cache_creation_input_tokens += usage.cache_creation_input_tokens;
|
||||
total_usage.cache_read_input_tokens += usage.cache_read_input_tokens;
|
||||
|
||||
// Send assistant message event
|
||||
let _ = tx
|
||||
.send(SDKMessage::Assistant {
|
||||
message: assistant_msg.clone(),
|
||||
usage: Some(usage.clone()),
|
||||
})
|
||||
.await;
|
||||
|
||||
messages.push(assistant_msg.clone());
|
||||
|
||||
// Check for tool use
|
||||
let tool_uses = extract_tool_uses(&assistant_msg);
|
||||
|
||||
if tool_uses.is_empty() {
|
||||
// No tool calls - we're done (end_turn or max_tokens)
|
||||
break;
|
||||
}
|
||||
|
||||
// Execute tools
|
||||
let tool_start = std::time::Instant::now();
|
||||
let results = tokio::select! {
|
||||
results = tools::execute_tools(
|
||||
&assistant_msg,
|
||||
®istry,
|
||||
&tool_context,
|
||||
can_use_tool,
|
||||
Some(tx.clone()),
|
||||
) => results,
|
||||
_ = tool_context.abort_signal.cancelled() => {
|
||||
return Err("Agent cancelled".to_string());
|
||||
}
|
||||
};
|
||||
let tool_duration = tool_start.elapsed().as_millis() as u64;
|
||||
cost_tracker.add_tool_duration(tool_duration).await;
|
||||
|
||||
// Send tool result events
|
||||
for (id, name, result) in &results {
|
||||
let content_text = result.get_text();
|
||||
let _ = tx
|
||||
.send(SDKMessage::ToolResult {
|
||||
tool_use_id: id.clone(),
|
||||
tool_name: name.clone(),
|
||||
content: content_text,
|
||||
is_error: result.is_error,
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
// Build tool results message and add to conversation
|
||||
let tool_results_msg = tools::build_tool_results_message(&results);
|
||||
messages.push(tool_results_msg);
|
||||
|
||||
// Check stop reason
|
||||
if stop_reason.as_deref() == Some("end_turn") && tool_uses.is_empty() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate final cost
|
||||
let total_cost = cost_tracker.total_cost().await;
|
||||
|
||||
// Extract final text
|
||||
let final_text = messages
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|m| m.role == MessageRole::Assistant)
|
||||
.map(|m| extract_text(m))
|
||||
.unwrap_or_default();
|
||||
|
||||
// Send result event
|
||||
let _ = tx
|
||||
.send(SDKMessage::Result {
|
||||
text: final_text,
|
||||
usage: total_usage,
|
||||
num_turns,
|
||||
cost_usd: total_cost,
|
||||
duration_ms: 0,
|
||||
messages: messages.clone(),
|
||||
})
|
||||
.await;
|
||||
|
||||
Ok(messages)
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
mod agent;
|
||||
pub(crate) mod r#loop;
|
||||
|
||||
pub use agent::*;
|
||||
Reference in New Issue
Block a user