mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-26 23:50:17 +00:00
Implement tool approval, fix tool definition refresh, and wire embeddings
This commit addresses three critical issues from code review: 1. Tool approval enforcement: Tools declaring requires_approval() (shell, http, file write/patch, build_software) now gate execution. Adds PendingApproval struct, session-scoped auto-approved tools set, and approval flow with yes/no/always commands. 2. Tool definition refresh: Tool definitions now refresh each iteration in both chat and job loops, so newly built tools become visible immediately within the same session. 3. Worker tool call handling: Changed respond() to respond_with_tools() when select_tools returns empty, properly executing tool calls instead of formatting them as text. Also includes prior work from the plan: - Wire embeddings provider (OpenAI + NEAR AI) to workspace - Load workspace system prompt (identity files) into LLM context - Route heartbeat notifications through channel manager - Enable auto-context compaction when threshold exceeded - Refactor to config structs (AgentDeps, WorkerDeps, LlmCallRecord) - Fix clippy warnings (saturating_sub, too_many_arguments) Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
8af48390a9
commit
2cc9aed364
+431
-62
@@ -10,7 +10,7 @@ use crate::agent::compaction::ContextCompactor;
|
||||
use crate::agent::context_monitor::ContextMonitor;
|
||||
use crate::agent::heartbeat::spawn_heartbeat;
|
||||
use crate::agent::self_repair::DefaultSelfRepair;
|
||||
use crate::agent::session::{Session, ThreadState};
|
||||
use crate::agent::session::{PendingApproval, Session, ThreadState};
|
||||
use crate::agent::session_manager::SessionManager;
|
||||
use crate::agent::submission::{Submission, SubmissionParser, SubmissionResult};
|
||||
use crate::agent::{
|
||||
@@ -27,20 +27,38 @@ use crate::safety::SafetyLayer;
|
||||
use crate::tools::ToolRegistry;
|
||||
use crate::workspace::Workspace;
|
||||
|
||||
/// Result of the agentic loop execution.
|
||||
enum AgenticLoopResult {
|
||||
/// Completed with a response.
|
||||
Response(String),
|
||||
/// A tool requires approval before continuing.
|
||||
NeedApproval {
|
||||
/// The pending approval request to store.
|
||||
pending: PendingApproval,
|
||||
},
|
||||
}
|
||||
|
||||
/// Core dependencies for the agent.
|
||||
///
|
||||
/// Bundles the shared components to reduce argument count.
|
||||
pub struct AgentDeps {
|
||||
pub store: Option<Arc<Store>>,
|
||||
pub llm: Arc<dyn LlmProvider>,
|
||||
pub safety: Arc<SafetyLayer>,
|
||||
pub tools: Arc<ToolRegistry>,
|
||||
pub workspace: Option<Arc<Workspace>>,
|
||||
}
|
||||
|
||||
/// The main agent that coordinates all components.
|
||||
pub struct Agent {
|
||||
config: AgentConfig,
|
||||
store: Option<Arc<Store>>,
|
||||
llm: Arc<dyn LlmProvider>,
|
||||
safety: Arc<SafetyLayer>,
|
||||
tools: Arc<ToolRegistry>,
|
||||
channels: ChannelManager,
|
||||
deps: AgentDeps,
|
||||
channels: Arc<ChannelManager>,
|
||||
context_manager: Arc<ContextManager>,
|
||||
scheduler: Arc<Scheduler>,
|
||||
router: Router,
|
||||
session_manager: Arc<SessionManager>,
|
||||
context_monitor: ContextMonitor,
|
||||
workspace: Option<Arc<Workspace>>,
|
||||
heartbeat_config: Option<HeartbeatConfig>,
|
||||
}
|
||||
|
||||
@@ -48,12 +66,8 @@ impl Agent {
|
||||
/// Create a new agent.
|
||||
pub fn new(
|
||||
config: AgentConfig,
|
||||
store: Option<Arc<Store>>,
|
||||
llm: Arc<dyn LlmProvider>,
|
||||
safety: Arc<SafetyLayer>,
|
||||
tools: Arc<ToolRegistry>,
|
||||
deps: AgentDeps,
|
||||
channels: ChannelManager,
|
||||
workspace: Option<Arc<Workspace>>,
|
||||
heartbeat_config: Option<HeartbeatConfig>,
|
||||
) -> Self {
|
||||
let context_manager = Arc::new(ContextManager::new(config.max_parallel_jobs));
|
||||
@@ -61,29 +75,46 @@ impl Agent {
|
||||
let scheduler = Arc::new(Scheduler::new(
|
||||
config.clone(),
|
||||
context_manager.clone(),
|
||||
llm.clone(),
|
||||
safety.clone(),
|
||||
tools.clone(),
|
||||
store.clone(),
|
||||
deps.llm.clone(),
|
||||
deps.safety.clone(),
|
||||
deps.tools.clone(),
|
||||
deps.store.clone(),
|
||||
));
|
||||
|
||||
Self {
|
||||
config,
|
||||
store,
|
||||
llm,
|
||||
safety,
|
||||
tools,
|
||||
channels,
|
||||
deps,
|
||||
channels: Arc::new(channels),
|
||||
context_manager,
|
||||
scheduler,
|
||||
router: Router::new(),
|
||||
session_manager: Arc::new(SessionManager::new()),
|
||||
context_monitor: ContextMonitor::new(),
|
||||
workspace,
|
||||
heartbeat_config,
|
||||
}
|
||||
}
|
||||
|
||||
// Convenience accessors
|
||||
fn store(&self) -> Option<&Arc<Store>> {
|
||||
self.deps.store.as_ref()
|
||||
}
|
||||
|
||||
fn llm(&self) -> &Arc<dyn LlmProvider> {
|
||||
&self.deps.llm
|
||||
}
|
||||
|
||||
fn safety(&self) -> &Arc<SafetyLayer> {
|
||||
&self.deps.safety
|
||||
}
|
||||
|
||||
fn tools(&self) -> &Arc<ToolRegistry> {
|
||||
&self.deps.tools
|
||||
}
|
||||
|
||||
fn workspace(&self) -> Option<&Arc<Workspace>> {
|
||||
self.deps.workspace.as_ref()
|
||||
}
|
||||
|
||||
/// Run the agent main loop.
|
||||
pub async fn run(self) -> Result<(), Error> {
|
||||
// Start channels
|
||||
@@ -104,30 +135,61 @@ impl Agent {
|
||||
// Spawn heartbeat if enabled
|
||||
let heartbeat_handle = if let Some(ref hb_config) = self.heartbeat_config {
|
||||
if hb_config.enabled {
|
||||
if let Some(ref workspace) = self.workspace {
|
||||
if let Some(workspace) = self.workspace() {
|
||||
let config = AgentHeartbeatConfig::default()
|
||||
.with_interval(std::time::Duration::from_secs(hb_config.interval_secs));
|
||||
|
||||
// Set up notification channel if configured
|
||||
// Set up notification channel
|
||||
let (notify_tx, mut notify_rx) =
|
||||
tokio::sync::mpsc::channel::<OutgoingResponse>(16);
|
||||
|
||||
// Spawn notification forwarder
|
||||
// We can't clone ChannelManager directly, so we just log the notifications
|
||||
// The heartbeat system will handle notifications via the response_tx
|
||||
// Spawn notification forwarder that routes through channel manager
|
||||
let notify_channel = hb_config.notify_channel.clone();
|
||||
let notify_user = hb_config.notify_user.clone();
|
||||
let channels = self.channels.clone();
|
||||
tokio::spawn(async move {
|
||||
while let Some(response) = notify_rx.recv().await {
|
||||
if let (Some(ch), Some(user)) = (¬ify_channel, ¬ify_user) {
|
||||
// Log the heartbeat notification
|
||||
// In a full implementation, we'd route this through a shared channel reference
|
||||
tracing::info!(
|
||||
"Heartbeat notification for {}/{}: {}",
|
||||
ch,
|
||||
user,
|
||||
&response.content
|
||||
);
|
||||
// Route notification to configured channel/user, or broadcast to all
|
||||
match (¬ify_channel, ¬ify_user) {
|
||||
(Some(channel), Some(user)) => {
|
||||
// Send to specific channel and user
|
||||
if let Err(e) =
|
||||
channels.broadcast(channel, user, response.clone()).await
|
||||
{
|
||||
tracing::warn!(
|
||||
"Failed to send heartbeat to {}/{}: {}",
|
||||
channel,
|
||||
user,
|
||||
e
|
||||
);
|
||||
} else {
|
||||
tracing::debug!(
|
||||
"Heartbeat notification sent to {}/{}",
|
||||
channel,
|
||||
user
|
||||
);
|
||||
}
|
||||
}
|
||||
(None, Some(user)) => {
|
||||
// Broadcast to all channels for this user
|
||||
let results = channels.broadcast_all(user, response).await;
|
||||
for (ch, result) in results {
|
||||
if let Err(e) = result {
|
||||
tracing::warn!(
|
||||
"Failed to broadcast heartbeat to {}: {}",
|
||||
ch,
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// No target configured, just log
|
||||
tracing::info!(
|
||||
"Heartbeat notification (no target configured): {}",
|
||||
&response.content
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -139,7 +201,7 @@ impl Agent {
|
||||
Some(spawn_heartbeat(
|
||||
config,
|
||||
workspace.clone(),
|
||||
self.llm.clone(),
|
||||
self.llm().clone(),
|
||||
Some(notify_tx),
|
||||
))
|
||||
} else {
|
||||
@@ -230,11 +292,24 @@ impl Agent {
|
||||
Submission::Resume { checkpoint_id } => {
|
||||
self.process_resume(session, thread_id, checkpoint_id).await
|
||||
}
|
||||
Submission::ExecApproval { .. } => {
|
||||
// Not supported in simple chat flow
|
||||
Ok(SubmissionResult::error(
|
||||
"Approval flow not supported in this context",
|
||||
))
|
||||
Submission::ExecApproval {
|
||||
request_id,
|
||||
approved,
|
||||
always,
|
||||
} => {
|
||||
self.process_approval(
|
||||
message,
|
||||
session,
|
||||
thread_id,
|
||||
Some(request_id),
|
||||
approved,
|
||||
always,
|
||||
)
|
||||
.await
|
||||
}
|
||||
Submission::ApprovalResponse { approved, always } => {
|
||||
self.process_approval(message, session, thread_id, None, approved, always)
|
||||
.await
|
||||
}
|
||||
};
|
||||
|
||||
@@ -244,8 +319,32 @@ impl Agent {
|
||||
SubmissionResult::Ok { message } => Ok(message),
|
||||
SubmissionResult::Error { message } => Ok(Some(format!("Error: {}", message))),
|
||||
SubmissionResult::Interrupted => Ok(Some("Interrupted.".into())),
|
||||
SubmissionResult::NeedApproval { .. } => {
|
||||
Ok(Some("Approval required but not supported.".into()))
|
||||
SubmissionResult::NeedApproval {
|
||||
request_id,
|
||||
tool_name,
|
||||
description,
|
||||
parameters,
|
||||
} => {
|
||||
// Format approval request for user
|
||||
let params_preview = serde_json::to_string_pretty(¶meters)
|
||||
.unwrap_or_else(|_| parameters.to_string());
|
||||
let params_truncated = if params_preview.len() > 200 {
|
||||
format!("{}...", ¶ms_preview[..200])
|
||||
} else {
|
||||
params_preview
|
||||
};
|
||||
Ok(Some(format!(
|
||||
"🔒 Tool requires approval:\n\n\
|
||||
**Tool:** {}\n\
|
||||
**Description:** {}\n\
|
||||
**Parameters:** ```\n{}\n```\n\n\
|
||||
Reply with:\n\
|
||||
- `yes` or `approve` to allow this tool\n\
|
||||
- `always` to always allow this tool in this session\n\
|
||||
- `no` or `deny` to reject\n\n\
|
||||
Request ID: {}",
|
||||
tool_name, description, params_truncated, request_id
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -322,9 +421,9 @@ impl Agent {
|
||||
"Context at {:.1}% capacity, auto-compacting",
|
||||
self.context_monitor.usage_percent(&messages)
|
||||
);
|
||||
let compactor = ContextCompactor::new(self.llm.clone());
|
||||
let compactor = ContextCompactor::new(self.llm().clone());
|
||||
if let Err(e) = compactor
|
||||
.compact(thread, strategy, self.workspace.as_deref())
|
||||
.compact(thread, strategy, self.workspace().map(|w| w.as_ref()))
|
||||
.await
|
||||
{
|
||||
tracing::warn!("Auto-compaction failed: {}", e);
|
||||
@@ -389,9 +488,9 @@ impl Agent {
|
||||
return Ok(SubmissionResult::Interrupted);
|
||||
}
|
||||
|
||||
// Complete or fail the turn
|
||||
// Complete, fail, or request approval
|
||||
match result {
|
||||
Ok(response) => {
|
||||
Ok(AgenticLoopResult::Response(response)) => {
|
||||
thread.complete_turn(&response);
|
||||
let _ = self
|
||||
.channels
|
||||
@@ -399,6 +498,27 @@ impl Agent {
|
||||
.await;
|
||||
Ok(SubmissionResult::response(response))
|
||||
}
|
||||
Ok(AgenticLoopResult::NeedApproval { pending }) => {
|
||||
// Store pending approval in thread and update state
|
||||
let request_id = pending.request_id;
|
||||
let tool_name = pending.tool_name.clone();
|
||||
let description = pending.description.clone();
|
||||
let parameters = pending.parameters.clone();
|
||||
thread.await_approval(pending);
|
||||
let _ = self
|
||||
.channels
|
||||
.send_status(
|
||||
&message.channel,
|
||||
StatusUpdate::Status("Awaiting approval".into()),
|
||||
)
|
||||
.await;
|
||||
Ok(SubmissionResult::NeedApproval {
|
||||
request_id,
|
||||
tool_name,
|
||||
description,
|
||||
parameters,
|
||||
})
|
||||
}
|
||||
Err(e) => {
|
||||
thread.fail_turn(e.to_string());
|
||||
Ok(SubmissionResult::error(e.to_string()))
|
||||
@@ -407,15 +527,34 @@ impl Agent {
|
||||
}
|
||||
|
||||
/// Run the agentic loop: call LLM, execute tools, repeat until text response.
|
||||
///
|
||||
/// Returns `AgenticLoopResult::Response` on completion, or
|
||||
/// `AgenticLoopResult::NeedApproval` if a tool requires user approval.
|
||||
async fn run_agentic_loop(
|
||||
&self,
|
||||
message: &IncomingMessage,
|
||||
session: Arc<Mutex<Session>>,
|
||||
thread_id: Uuid,
|
||||
initial_messages: Vec<ChatMessage>,
|
||||
) -> Result<String, Error> {
|
||||
let reasoning = Reasoning::new(self.llm.clone(), self.safety.clone());
|
||||
let tool_defs = self.tools.tool_definitions().await;
|
||||
) -> Result<AgenticLoopResult, Error> {
|
||||
// Load workspace system prompt (identity files: AGENTS.md, SOUL.md, etc.)
|
||||
let system_prompt = if let Some(ws) = self.workspace() {
|
||||
match ws.system_prompt().await {
|
||||
Ok(prompt) if !prompt.is_empty() => Some(prompt),
|
||||
Ok(_) => None,
|
||||
Err(e) => {
|
||||
tracing::debug!("Could not load workspace system prompt: {}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut reasoning = Reasoning::new(self.llm().clone(), self.safety().clone());
|
||||
if let Some(prompt) = system_prompt {
|
||||
reasoning = reasoning.with_system_prompt(prompt);
|
||||
}
|
||||
|
||||
// Build context with messages that we'll mutate during the loop
|
||||
let mut context_messages = initial_messages;
|
||||
@@ -425,6 +564,7 @@ impl Agent {
|
||||
|
||||
const MAX_TOOL_ITERATIONS: usize = 10;
|
||||
let mut iteration = 0;
|
||||
let mut tools_executed = false;
|
||||
|
||||
loop {
|
||||
iteration += 1;
|
||||
@@ -450,19 +590,38 @@ impl Agent {
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh tool definitions each iteration so newly built tools become visible
|
||||
let tool_defs = self.tools().tool_definitions().await;
|
||||
|
||||
// Call LLM with current context
|
||||
let context = ReasoningContext::new()
|
||||
.with_messages(context_messages.clone())
|
||||
.with_tools(tool_defs.clone());
|
||||
.with_tools(tool_defs);
|
||||
|
||||
let result = reasoning.respond_with_tools(&context).await?;
|
||||
|
||||
match result {
|
||||
RespondResult::Text(text) => {
|
||||
// Final response, return it
|
||||
return Ok(text);
|
||||
// If no tools have been executed yet, prompt the LLM to use tools
|
||||
// This handles the case where the model explains what it will do
|
||||
// instead of actually calling tools
|
||||
if !tools_executed && iteration < 3 {
|
||||
tracing::debug!(
|
||||
"No tools executed yet (iteration {}), prompting for tool use",
|
||||
iteration
|
||||
);
|
||||
context_messages.push(ChatMessage::assistant(&text));
|
||||
context_messages.push(ChatMessage::user(
|
||||
"Please proceed and use the available tools to complete this task.",
|
||||
));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Tools have been executed or we've tried multiple times, return response
|
||||
return Ok(AgenticLoopResult::Response(text));
|
||||
}
|
||||
RespondResult::ToolCalls(tool_calls) => {
|
||||
tools_executed = true;
|
||||
// Execute tools and add results to context
|
||||
let _ = self
|
||||
.channels
|
||||
@@ -487,8 +646,33 @@ impl Agent {
|
||||
}
|
||||
}
|
||||
|
||||
// Execute each tool
|
||||
// Execute each tool (with approval checking)
|
||||
for tc in tool_calls {
|
||||
// Check if tool requires approval
|
||||
if let Some(tool) = self.tools().get(&tc.name).await {
|
||||
if tool.requires_approval() {
|
||||
// Check if auto-approved for this session
|
||||
let is_auto_approved = {
|
||||
let sess = session.lock().await;
|
||||
sess.is_tool_auto_approved(&tc.name)
|
||||
};
|
||||
|
||||
if !is_auto_approved {
|
||||
// Need approval - store pending request and return
|
||||
let pending = PendingApproval {
|
||||
request_id: Uuid::new_v4(),
|
||||
tool_name: tc.name.clone(),
|
||||
parameters: tc.arguments.clone(),
|
||||
description: tool.description().to_string(),
|
||||
tool_call_id: tc.id.clone(),
|
||||
context_messages: context_messages.clone(),
|
||||
};
|
||||
|
||||
return Ok(AgenticLoopResult::NeedApproval { pending });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let tool_result = self
|
||||
.execute_chat_tool(&tc.name, &tc.arguments, &job_ctx)
|
||||
.await;
|
||||
@@ -514,8 +698,9 @@ impl Agent {
|
||||
let result_content = match tool_result {
|
||||
Ok(output) => {
|
||||
// Sanitize output before showing to LLM
|
||||
let sanitized = self.safety.sanitize_tool_output(&tc.name, &output);
|
||||
self.safety.wrap_for_llm(
|
||||
let sanitized =
|
||||
self.safety().sanitize_tool_output(&tc.name, &output);
|
||||
self.safety().wrap_for_llm(
|
||||
&tc.name,
|
||||
&sanitized.content,
|
||||
sanitized.was_modified,
|
||||
@@ -543,7 +728,7 @@ impl Agent {
|
||||
job_ctx: &JobContext,
|
||||
) -> Result<String, Error> {
|
||||
let tool =
|
||||
self.tools
|
||||
self.tools()
|
||||
.get(tool_name)
|
||||
.await
|
||||
.ok_or_else(|| crate::error::ToolError::NotFound {
|
||||
@@ -718,9 +903,9 @@ impl Agent {
|
||||
crate::agent::context_monitor::CompactionStrategy::Summarize { keep_recent: 5 },
|
||||
);
|
||||
|
||||
let compactor = ContextCompactor::new(self.llm.clone());
|
||||
let compactor = ContextCompactor::new(self.llm().clone());
|
||||
match compactor
|
||||
.compact(thread, strategy, self.workspace.as_deref())
|
||||
.compact(thread, strategy, self.workspace().map(|w| w.as_ref()))
|
||||
.await
|
||||
{
|
||||
Ok(result) => {
|
||||
@@ -757,6 +942,190 @@ impl Agent {
|
||||
Ok(SubmissionResult::ok_with_message("Thread cleared."))
|
||||
}
|
||||
|
||||
/// Process an approval or rejection of a pending tool execution.
|
||||
async fn process_approval(
|
||||
&self,
|
||||
message: &IncomingMessage,
|
||||
session: Arc<Mutex<Session>>,
|
||||
thread_id: Uuid,
|
||||
request_id: Option<Uuid>,
|
||||
approved: bool,
|
||||
always: bool,
|
||||
) -> Result<SubmissionResult, Error> {
|
||||
// Get thread state and pending approval
|
||||
let (_thread_state, pending) = {
|
||||
let mut sess = session.lock().await;
|
||||
let thread = sess
|
||||
.threads
|
||||
.get_mut(&thread_id)
|
||||
.ok_or_else(|| Error::from(crate::error::JobError::NotFound { id: thread_id }))?;
|
||||
|
||||
if thread.state != ThreadState::AwaitingApproval {
|
||||
return Ok(SubmissionResult::error("No pending approval request."));
|
||||
}
|
||||
|
||||
let pending = thread.take_pending_approval();
|
||||
(thread.state, pending)
|
||||
};
|
||||
|
||||
let pending = match pending {
|
||||
Some(p) => p,
|
||||
None => return Ok(SubmissionResult::error("No pending approval request.")),
|
||||
};
|
||||
|
||||
// Verify request ID if provided
|
||||
if let Some(req_id) = request_id {
|
||||
if req_id != pending.request_id {
|
||||
// Put it back and return error
|
||||
let mut sess = session.lock().await;
|
||||
if let Some(thread) = sess.threads.get_mut(&thread_id) {
|
||||
thread.await_approval(pending);
|
||||
}
|
||||
return Ok(SubmissionResult::error(
|
||||
"Request ID mismatch. Use the correct request ID.",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if approved {
|
||||
// If always, add to auto-approved set
|
||||
if always {
|
||||
let mut sess = session.lock().await;
|
||||
sess.auto_approve_tool(&pending.tool_name);
|
||||
tracing::info!(
|
||||
"Auto-approved tool '{}' for session {}",
|
||||
pending.tool_name,
|
||||
sess.id
|
||||
);
|
||||
}
|
||||
|
||||
// Reset thread state to processing
|
||||
{
|
||||
let mut sess = session.lock().await;
|
||||
if let Some(thread) = sess.threads.get_mut(&thread_id) {
|
||||
thread.state = ThreadState::Processing;
|
||||
}
|
||||
}
|
||||
|
||||
// Execute the approved tool and continue the loop
|
||||
let job_ctx =
|
||||
JobContext::with_user(&message.user_id, "chat", "Interactive chat session");
|
||||
|
||||
let tool_result = self
|
||||
.execute_chat_tool(&pending.tool_name, &pending.parameters, &job_ctx)
|
||||
.await;
|
||||
|
||||
// Build context including the tool result
|
||||
let mut context_messages = pending.context_messages;
|
||||
|
||||
// Record result in thread
|
||||
{
|
||||
let mut sess = session.lock().await;
|
||||
if let Some(thread) = sess.threads.get_mut(&thread_id) {
|
||||
if let Some(turn) = thread.last_turn_mut() {
|
||||
match &tool_result {
|
||||
Ok(output) => {
|
||||
turn.record_tool_result(serde_json::json!(output));
|
||||
}
|
||||
Err(e) => {
|
||||
turn.record_tool_error(e.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add tool result to context
|
||||
let result_content = match tool_result {
|
||||
Ok(output) => {
|
||||
let sanitized = self
|
||||
.safety()
|
||||
.sanitize_tool_output(&pending.tool_name, &output);
|
||||
self.safety().wrap_for_llm(
|
||||
&pending.tool_name,
|
||||
&sanitized.content,
|
||||
sanitized.was_modified,
|
||||
)
|
||||
}
|
||||
Err(e) => format!("Error: {}", e),
|
||||
};
|
||||
|
||||
context_messages.push(ChatMessage::tool_result(
|
||||
&pending.tool_call_id,
|
||||
&pending.tool_name,
|
||||
result_content,
|
||||
));
|
||||
|
||||
// Continue the agentic loop
|
||||
let result = self
|
||||
.run_agentic_loop(message, session.clone(), thread_id, context_messages)
|
||||
.await;
|
||||
|
||||
// Handle the result
|
||||
let mut sess = session.lock().await;
|
||||
let thread = sess
|
||||
.threads
|
||||
.get_mut(&thread_id)
|
||||
.ok_or_else(|| Error::from(crate::error::JobError::NotFound { id: thread_id }))?;
|
||||
|
||||
match result {
|
||||
Ok(AgenticLoopResult::Response(response)) => {
|
||||
thread.complete_turn(&response);
|
||||
let _ = self
|
||||
.channels
|
||||
.send_status(&message.channel, StatusUpdate::Status("Done".into()))
|
||||
.await;
|
||||
Ok(SubmissionResult::response(response))
|
||||
}
|
||||
Ok(AgenticLoopResult::NeedApproval {
|
||||
pending: new_pending,
|
||||
}) => {
|
||||
let request_id = new_pending.request_id;
|
||||
let tool_name = new_pending.tool_name.clone();
|
||||
let description = new_pending.description.clone();
|
||||
let parameters = new_pending.parameters.clone();
|
||||
thread.await_approval(new_pending);
|
||||
let _ = self
|
||||
.channels
|
||||
.send_status(
|
||||
&message.channel,
|
||||
StatusUpdate::Status("Awaiting approval".into()),
|
||||
)
|
||||
.await;
|
||||
Ok(SubmissionResult::NeedApproval {
|
||||
request_id,
|
||||
tool_name,
|
||||
description,
|
||||
parameters,
|
||||
})
|
||||
}
|
||||
Err(e) => {
|
||||
thread.fail_turn(e.to_string());
|
||||
Ok(SubmissionResult::error(e.to_string()))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Rejected - clear approval and return to idle
|
||||
{
|
||||
let mut sess = session.lock().await;
|
||||
if let Some(thread) = sess.threads.get_mut(&thread_id) {
|
||||
thread.clear_pending_approval();
|
||||
}
|
||||
}
|
||||
|
||||
let _ = self
|
||||
.channels
|
||||
.send_status(&message.channel, StatusUpdate::Status("Rejected".into()))
|
||||
.await;
|
||||
|
||||
Ok(SubmissionResult::response(format!(
|
||||
"Tool '{}' was rejected. The agent will not execute this tool.\n\n\
|
||||
You can continue the conversation or try a different approach.",
|
||||
pending.tool_name
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
async fn process_new_thread(
|
||||
&self,
|
||||
message: &IncomingMessage,
|
||||
@@ -842,7 +1211,7 @@ impl Agent {
|
||||
}
|
||||
|
||||
// Persist new job to database (fire-and-forget)
|
||||
if let Some(ref store) = self.store {
|
||||
if let Some(store) = self.store() {
|
||||
if let Ok(ctx) = self.context_manager.get_context(job_id).await {
|
||||
let store = store.clone();
|
||||
tokio::spawn(async move {
|
||||
@@ -990,7 +1359,7 @@ impl Agent {
|
||||
))),
|
||||
|
||||
"tools" => {
|
||||
let tools = self.tools.list().await;
|
||||
let tools = self.tools().list().await;
|
||||
Ok(Some(format!("Available tools: {}", tools.join(", "))))
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -21,18 +21,18 @@ mod session_manager;
|
||||
pub mod submission;
|
||||
pub mod task;
|
||||
pub mod undo;
|
||||
mod worker;
|
||||
pub mod worker;
|
||||
|
||||
pub use agent_loop::Agent;
|
||||
pub use agent_loop::{Agent, AgentDeps};
|
||||
pub use compaction::{CompactionResult, ContextCompactor};
|
||||
pub use context_monitor::{CompactionStrategy, ContextBreakdown, ContextMonitor};
|
||||
pub use heartbeat::{HeartbeatConfig, HeartbeatResult, HeartbeatRunner, spawn_heartbeat};
|
||||
pub use router::{MessageIntent, Router};
|
||||
pub use scheduler::Scheduler;
|
||||
pub use self_repair::{BrokenTool, RepairResult, RepairTask, SelfRepair, StuckJob};
|
||||
pub use session::{Session, Thread, ThreadState, Turn, TurnState};
|
||||
pub use session::{PendingApproval, Session, Thread, ThreadState, Turn, TurnState};
|
||||
pub use session_manager::SessionManager;
|
||||
pub use submission::{Submission, SubmissionParser, SubmissionResult};
|
||||
pub use task::{Task, TaskContext, TaskHandler, TaskOutput, TaskStatus};
|
||||
pub use undo::{Checkpoint, UndoManager};
|
||||
pub use worker::Worker;
|
||||
pub use worker::{Worker, WorkerDeps};
|
||||
|
||||
+12
-12
@@ -8,8 +8,8 @@ use tokio::sync::{RwLock, mpsc, oneshot};
|
||||
use tokio::task::JoinHandle;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::agent::Worker;
|
||||
use crate::agent::task::{Task, TaskContext, TaskOutput};
|
||||
use crate::agent::worker::{Worker, WorkerDeps};
|
||||
use crate::config::AgentConfig;
|
||||
use crate::context::{ContextManager, JobContext, JobState};
|
||||
use crate::error::{Error, JobError};
|
||||
@@ -109,17 +109,17 @@ impl Scheduler {
|
||||
// Create worker channel
|
||||
let (tx, rx) = mpsc::channel(16);
|
||||
|
||||
// Create worker
|
||||
let worker = Worker::new(
|
||||
job_id,
|
||||
self.context_manager.clone(),
|
||||
self.llm.clone(),
|
||||
self.safety.clone(),
|
||||
self.tools.clone(),
|
||||
self.store.clone(),
|
||||
self.config.job_timeout,
|
||||
self.config.use_planning,
|
||||
);
|
||||
// Create worker with shared dependencies
|
||||
let deps = WorkerDeps {
|
||||
context_manager: self.context_manager.clone(),
|
||||
llm: self.llm.clone(),
|
||||
safety: self.safety.clone(),
|
||||
tools: self.tools.clone(),
|
||||
store: self.store.clone(),
|
||||
timeout: self.config.job_timeout,
|
||||
use_planning: self.config.use_planning,
|
||||
};
|
||||
let worker = Worker::new(job_id, deps);
|
||||
|
||||
// Spawn worker task
|
||||
let handle = tokio::spawn(async move {
|
||||
|
||||
+51
-3
@@ -10,7 +10,7 @@
|
||||
//! - Compaction: Summarize old turns to save context
|
||||
//! - Resume: Continue from a saved checkpoint
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -35,6 +35,9 @@ pub struct Session {
|
||||
pub last_active_at: DateTime<Utc>,
|
||||
/// Session metadata.
|
||||
pub metadata: serde_json::Value,
|
||||
/// Tools that have been auto-approved for this session ("always approve").
|
||||
#[serde(default)]
|
||||
pub auto_approved_tools: HashSet<String>,
|
||||
}
|
||||
|
||||
impl Session {
|
||||
@@ -49,9 +52,20 @@ impl Session {
|
||||
created_at: now,
|
||||
last_active_at: now,
|
||||
metadata: serde_json::Value::Null,
|
||||
auto_approved_tools: HashSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a tool has been auto-approved for this session.
|
||||
pub fn is_tool_auto_approved(&self, tool_name: &str) -> bool {
|
||||
self.auto_approved_tools.contains(tool_name)
|
||||
}
|
||||
|
||||
/// Add a tool to the auto-approved set.
|
||||
pub fn auto_approve_tool(&mut self, tool_name: impl Into<String>) {
|
||||
self.auto_approved_tools.insert(tool_name.into());
|
||||
}
|
||||
|
||||
/// Create a new thread in this session.
|
||||
pub fn create_thread(&mut self) -> &mut Thread {
|
||||
let thread = Thread::new(self.id);
|
||||
@@ -107,6 +121,23 @@ pub enum ThreadState {
|
||||
Interrupted,
|
||||
}
|
||||
|
||||
/// Pending tool approval request stored on a thread.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PendingApproval {
|
||||
/// Unique request ID.
|
||||
pub request_id: Uuid,
|
||||
/// Tool name requiring approval.
|
||||
pub tool_name: String,
|
||||
/// Tool parameters.
|
||||
pub parameters: serde_json::Value,
|
||||
/// Description of what the tool will do.
|
||||
pub description: String,
|
||||
/// Tool call ID from LLM (for proper context continuation).
|
||||
pub tool_call_id: String,
|
||||
/// Context messages at the time of the request (to resume from).
|
||||
pub context_messages: Vec<ChatMessage>,
|
||||
}
|
||||
|
||||
/// A conversation thread within a session.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Thread {
|
||||
@@ -124,6 +155,9 @@ pub struct Thread {
|
||||
pub updated_at: DateTime<Utc>,
|
||||
/// Thread metadata (e.g., title, tags).
|
||||
pub metadata: serde_json::Value,
|
||||
/// Pending approval request (when state is AwaitingApproval).
|
||||
#[serde(default)]
|
||||
pub pending_approval: Option<PendingApproval>,
|
||||
}
|
||||
|
||||
impl Thread {
|
||||
@@ -138,6 +172,7 @@ impl Thread {
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
metadata: serde_json::Value::Null,
|
||||
pending_approval: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,9 +219,22 @@ impl Thread {
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Mark the thread as awaiting approval.
|
||||
pub fn await_approval(&mut self) {
|
||||
/// Mark the thread as awaiting approval with pending request details.
|
||||
pub fn await_approval(&mut self, pending: PendingApproval) {
|
||||
self.state = ThreadState::AwaitingApproval;
|
||||
self.pending_approval = Some(pending);
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Take the pending approval (clearing it from the thread).
|
||||
pub fn take_pending_approval(&mut self) -> Option<PendingApproval> {
|
||||
self.pending_approval.take()
|
||||
}
|
||||
|
||||
/// Clear pending approval and return to idle state.
|
||||
pub fn clear_pending_approval(&mut self) {
|
||||
self.pending_approval = None;
|
||||
self.state = ThreadState::Idle;
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
|
||||
+33
-1
@@ -52,6 +52,30 @@ impl SubmissionParser {
|
||||
}
|
||||
}
|
||||
|
||||
// Approval responses (simple yes/no/always for pending approvals)
|
||||
// These are short enough to check explicitly
|
||||
match lower.as_str() {
|
||||
"yes" | "y" | "approve" | "ok" => {
|
||||
return Submission::ApprovalResponse {
|
||||
approved: true,
|
||||
always: false,
|
||||
};
|
||||
}
|
||||
"always" | "yes always" | "approve always" => {
|
||||
return Submission::ApprovalResponse {
|
||||
approved: true,
|
||||
always: true,
|
||||
};
|
||||
}
|
||||
"no" | "n" | "deny" | "reject" | "cancel" => {
|
||||
return Submission::ApprovalResponse {
|
||||
approved: false,
|
||||
always: false,
|
||||
};
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Default: user input
|
||||
Submission::UserInput {
|
||||
content: content.to_string(),
|
||||
@@ -68,7 +92,7 @@ pub enum Submission {
|
||||
content: String,
|
||||
},
|
||||
|
||||
/// Response to an execution approval request.
|
||||
/// Response to an execution approval request (with explicit request ID).
|
||||
ExecApproval {
|
||||
/// ID of the approval request being responded to.
|
||||
request_id: Uuid,
|
||||
@@ -78,6 +102,14 @@ pub enum Submission {
|
||||
always: bool,
|
||||
},
|
||||
|
||||
/// Simple approval response (yes/no/always) for the current pending approval.
|
||||
ApprovalResponse {
|
||||
/// Whether the execution was approved.
|
||||
approved: bool,
|
||||
/// If true, auto-approve this tool for the rest of the session.
|
||||
always: bool,
|
||||
},
|
||||
|
||||
/// Interrupt the current turn.
|
||||
Interrupt,
|
||||
|
||||
|
||||
+128
-68
@@ -13,22 +13,30 @@ use crate::context::{ContextManager, JobState};
|
||||
use crate::error::Error;
|
||||
use crate::history::Store;
|
||||
use crate::llm::{
|
||||
ActionPlan, ChatMessage, LlmProvider, Reasoning, ReasoningContext, ToolSelection,
|
||||
ActionPlan, ChatMessage, LlmProvider, Reasoning, ReasoningContext, RespondResult, ToolSelection,
|
||||
};
|
||||
use crate::safety::SafetyLayer;
|
||||
use crate::tools::ToolRegistry;
|
||||
|
||||
/// Shared dependencies for worker execution.
|
||||
///
|
||||
/// This bundles the dependencies that are shared across all workers,
|
||||
/// reducing the number of arguments to `Worker::new`.
|
||||
#[derive(Clone)]
|
||||
pub struct WorkerDeps {
|
||||
pub context_manager: Arc<ContextManager>,
|
||||
pub llm: Arc<dyn LlmProvider>,
|
||||
pub safety: Arc<SafetyLayer>,
|
||||
pub tools: Arc<ToolRegistry>,
|
||||
pub store: Option<Arc<Store>>,
|
||||
pub timeout: Duration,
|
||||
pub use_planning: bool,
|
||||
}
|
||||
|
||||
/// Worker that executes a single job.
|
||||
pub struct Worker {
|
||||
job_id: Uuid,
|
||||
context_manager: Arc<ContextManager>,
|
||||
llm: Arc<dyn LlmProvider>,
|
||||
safety: Arc<SafetyLayer>,
|
||||
tools: Arc<ToolRegistry>,
|
||||
store: Option<Arc<Store>>,
|
||||
timeout: Duration,
|
||||
/// Whether to use planning before tool execution.
|
||||
use_planning: bool,
|
||||
deps: WorkerDeps,
|
||||
}
|
||||
|
||||
/// Result of a tool execution with metadata for context building.
|
||||
@@ -37,32 +45,43 @@ struct ToolExecResult {
|
||||
}
|
||||
|
||||
impl Worker {
|
||||
/// Create a new worker.
|
||||
pub fn new(
|
||||
job_id: Uuid,
|
||||
context_manager: Arc<ContextManager>,
|
||||
llm: Arc<dyn LlmProvider>,
|
||||
safety: Arc<SafetyLayer>,
|
||||
tools: Arc<ToolRegistry>,
|
||||
store: Option<Arc<Store>>,
|
||||
timeout: Duration,
|
||||
use_planning: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
job_id,
|
||||
context_manager,
|
||||
llm,
|
||||
safety,
|
||||
tools,
|
||||
store,
|
||||
timeout,
|
||||
use_planning,
|
||||
}
|
||||
/// Create a new worker for a specific job.
|
||||
pub fn new(job_id: Uuid, deps: WorkerDeps) -> Self {
|
||||
Self { job_id, deps }
|
||||
}
|
||||
|
||||
// Convenience accessors to avoid deps.field everywhere
|
||||
fn context_manager(&self) -> &Arc<ContextManager> {
|
||||
&self.deps.context_manager
|
||||
}
|
||||
|
||||
fn llm(&self) -> &Arc<dyn LlmProvider> {
|
||||
&self.deps.llm
|
||||
}
|
||||
|
||||
fn safety(&self) -> &Arc<SafetyLayer> {
|
||||
&self.deps.safety
|
||||
}
|
||||
|
||||
fn tools(&self) -> &Arc<ToolRegistry> {
|
||||
&self.deps.tools
|
||||
}
|
||||
|
||||
fn store(&self) -> Option<&Arc<Store>> {
|
||||
self.deps.store.as_ref()
|
||||
}
|
||||
|
||||
fn timeout(&self) -> Duration {
|
||||
self.deps.timeout
|
||||
}
|
||||
|
||||
fn use_planning(&self) -> bool {
|
||||
self.deps.use_planning
|
||||
}
|
||||
|
||||
/// Fire-and-forget persistence of job status.
|
||||
fn persist_status(&self, status: JobState, reason: Option<String>) {
|
||||
if let Some(ref store) = self.store {
|
||||
if let Some(store) = self.store() {
|
||||
let store = store.clone();
|
||||
let job_id = self.job_id;
|
||||
tokio::spawn(async move {
|
||||
@@ -91,16 +110,13 @@ impl Worker {
|
||||
}
|
||||
|
||||
// Get job context
|
||||
let job_ctx = self.context_manager.get_context(self.job_id).await?;
|
||||
let job_ctx = self.context_manager().get_context(self.job_id).await?;
|
||||
|
||||
// Create reasoning engine
|
||||
let reasoning = Reasoning::new(self.llm.clone(), self.safety.clone());
|
||||
let reasoning = Reasoning::new(self.llm().clone(), self.safety().clone());
|
||||
|
||||
// Build initial reasoning context
|
||||
let tool_defs = self.tools.tool_definitions().await;
|
||||
let mut reason_ctx = ReasoningContext::new()
|
||||
.with_job(&job_ctx.description)
|
||||
.with_tools(tool_defs);
|
||||
// Build initial reasoning context (tool definitions refreshed each iteration in execution_loop)
|
||||
let mut reason_ctx = ReasoningContext::new().with_job(&job_ctx.description);
|
||||
|
||||
// Add system message
|
||||
reason_ctx.messages.push(ChatMessage::system(format!(
|
||||
@@ -116,7 +132,7 @@ Report when the job is complete or if you encounter issues you cannot resolve."#
|
||||
)));
|
||||
|
||||
// Main execution loop with timeout
|
||||
let result = tokio::time::timeout(self.timeout, async {
|
||||
let result = tokio::time::timeout(self.timeout(), async {
|
||||
self.execution_loop(&mut rx, &reasoning, &mut reason_ctx)
|
||||
.await
|
||||
})
|
||||
@@ -148,8 +164,11 @@ Report when the job is complete or if you encounter issues you cannot resolve."#
|
||||
let max_iterations = 50;
|
||||
let mut iteration = 0;
|
||||
|
||||
// Initial tool definitions for planning (will be refreshed in loop)
|
||||
reason_ctx.available_tools = self.tools().tool_definitions().await;
|
||||
|
||||
// Generate plan if planning is enabled
|
||||
let plan = if self.use_planning {
|
||||
let plan = if self.use_planning() {
|
||||
match reasoning.plan(reason_ctx).await {
|
||||
Ok(p) => {
|
||||
tracing::info!(
|
||||
@@ -213,29 +232,61 @@ Report when the job is complete or if you encounter issues you cannot resolve."#
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Refresh tool definitions so newly built tools become visible
|
||||
reason_ctx.available_tools = self.tools().tool_definitions().await;
|
||||
|
||||
// Select next tool(s) to use
|
||||
let selections = reasoning.select_tools(reason_ctx).await?;
|
||||
|
||||
if selections.is_empty() {
|
||||
// No tools selected, ask LLM for next steps
|
||||
let response = reasoning.respond(reason_ctx).await?;
|
||||
// No tools from select_tools, ask LLM directly (may still return tool calls)
|
||||
let respond_result = reasoning.respond_with_tools(reason_ctx).await?;
|
||||
|
||||
if response.to_lowercase().contains("complete")
|
||||
|| response.to_lowercase().contains("finished")
|
||||
|| response.to_lowercase().contains("done")
|
||||
{
|
||||
self.mark_completed().await?;
|
||||
return Ok(());
|
||||
}
|
||||
match respond_result {
|
||||
RespondResult::Text(response) => {
|
||||
// Check for completion keywords
|
||||
let response_lower = response.to_lowercase();
|
||||
if response_lower.contains("complete")
|
||||
|| response_lower.contains("finished")
|
||||
|| response_lower.contains("done")
|
||||
{
|
||||
self.mark_completed().await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Add assistant response to context
|
||||
reason_ctx.messages.push(ChatMessage::assistant(&response));
|
||||
// Add assistant response to context
|
||||
reason_ctx.messages.push(ChatMessage::assistant(&response));
|
||||
|
||||
// Give it one more chance to select a tool
|
||||
if iteration > 3 && iteration % 5 == 0 {
|
||||
reason_ctx.messages.push(ChatMessage::user(
|
||||
"Are you stuck? Do you need help completing this job?",
|
||||
));
|
||||
// Give it one more chance to select a tool
|
||||
if iteration > 3 && iteration % 5 == 0 {
|
||||
reason_ctx.messages.push(ChatMessage::user(
|
||||
"Are you stuck? Do you need help completing this job?",
|
||||
));
|
||||
}
|
||||
}
|
||||
RespondResult::ToolCalls(tool_calls) => {
|
||||
// Model returned tool calls - execute them
|
||||
tracing::debug!(
|
||||
"Job {} respond_with_tools returned {} tool calls",
|
||||
self.job_id,
|
||||
tool_calls.len()
|
||||
);
|
||||
|
||||
for tc in tool_calls {
|
||||
let result = self.execute_tool(&tc.name, &tc.arguments).await;
|
||||
|
||||
// Create synthetic selection for process_tool_result
|
||||
let selection = ToolSelection {
|
||||
tool_name: tc.name.clone(),
|
||||
parameters: tc.arguments.clone(),
|
||||
reasoning: String::new(),
|
||||
alternatives: vec![],
|
||||
};
|
||||
|
||||
self.process_tool_result(reason_ctx, &selection, result)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if selections.len() == 1 {
|
||||
// Single tool: execute directly
|
||||
@@ -282,10 +333,10 @@ Report when the job is complete or if you encounter issues you cannot resolve."#
|
||||
.map(|selection| {
|
||||
let tool_name = selection.tool_name.clone();
|
||||
let params = selection.parameters.clone();
|
||||
let tools = self.tools.clone();
|
||||
let context_manager = self.context_manager.clone();
|
||||
let tools = self.tools().clone();
|
||||
let context_manager = self.context_manager().clone();
|
||||
let job_id = self.job_id;
|
||||
let store = self.store.clone();
|
||||
let store = self.deps.store.clone();
|
||||
|
||||
async move {
|
||||
let result = Self::execute_tool_inner(
|
||||
@@ -321,6 +372,15 @@ Report when the job is complete or if you encounter issues you cannot resolve."#
|
||||
name: tool_name.to_string(),
|
||||
})?;
|
||||
|
||||
// Log warning if tool requires approval (autonomous jobs auto-approve for now)
|
||||
if tool.requires_approval() {
|
||||
tracing::warn!(
|
||||
job_id = %job_id,
|
||||
tool = %tool_name,
|
||||
"Executing sensitive tool in autonomous job (auto-approved)"
|
||||
);
|
||||
}
|
||||
|
||||
// Get job context for the tool
|
||||
let job_ctx = context_manager.get_context(job_id).await?;
|
||||
|
||||
@@ -412,11 +472,11 @@ Report when the job is complete or if you encounter issues you cannot resolve."#
|
||||
Ok(output) => {
|
||||
// Sanitize output
|
||||
let sanitized = self
|
||||
.safety
|
||||
.safety()
|
||||
.sanitize_tool_output(&selection.tool_name, &output);
|
||||
|
||||
// Add to context
|
||||
let wrapped = self.safety.wrap_for_llm(
|
||||
let wrapped = self.safety().wrap_for_llm(
|
||||
&selection.tool_name,
|
||||
&sanitized.content,
|
||||
sanitized.was_modified,
|
||||
@@ -445,7 +505,7 @@ Report when the job is complete or if you encounter issues you cannot resolve."#
|
||||
);
|
||||
|
||||
// Record failure for self-repair tracking
|
||||
if let Some(ref store) = self.store {
|
||||
if let Some(store) = self.store() {
|
||||
let store = store.clone();
|
||||
let tool_name = selection.tool_name.clone();
|
||||
let error_msg = e.to_string();
|
||||
@@ -563,9 +623,9 @@ Report when the job is complete or if you encounter issues you cannot resolve."#
|
||||
params: &serde_json::Value,
|
||||
) -> Result<String, Error> {
|
||||
Self::execute_tool_inner(
|
||||
self.tools.clone(),
|
||||
self.context_manager.clone(),
|
||||
self.store.clone(),
|
||||
self.tools().clone(),
|
||||
self.context_manager().clone(),
|
||||
self.deps.store.clone(),
|
||||
self.job_id,
|
||||
tool_name,
|
||||
params,
|
||||
@@ -574,7 +634,7 @@ Report when the job is complete or if you encounter issues you cannot resolve."#
|
||||
}
|
||||
|
||||
async fn mark_completed(&self) -> Result<(), Error> {
|
||||
self.context_manager
|
||||
self.context_manager()
|
||||
.update_context(self.job_id, |ctx| {
|
||||
ctx.transition_to(
|
||||
JobState::Completed,
|
||||
@@ -595,7 +655,7 @@ Report when the job is complete or if you encounter issues you cannot resolve."#
|
||||
}
|
||||
|
||||
async fn mark_failed(&self, reason: &str) -> Result<(), Error> {
|
||||
self.context_manager
|
||||
self.context_manager()
|
||||
.update_context(self.job_id, |ctx| {
|
||||
ctx.transition_to(JobState::Failed, Some(reason.to_string()))
|
||||
})
|
||||
@@ -610,7 +670,7 @@ Report when the job is complete or if you encounter issues you cannot resolve."#
|
||||
}
|
||||
|
||||
async fn mark_stuck(&self, reason: &str) -> Result<(), Error> {
|
||||
self.context_manager
|
||||
self.context_manager()
|
||||
.update_context(self.job_id, |ctx| ctx.mark_stuck(reason))
|
||||
.await?
|
||||
.map_err(|s| crate::error::JobError::ContextError {
|
||||
|
||||
Reference in New Issue
Block a user