feat: Integrate Hooks and Workflow with Agent execution
Hooks Integration: - Add HookManager field to Agent struct - Load hooks from HooksConfig on Agent creation - Execute SessionStart hook at agent start - Execute PreTool/PostTool hooks around tool execution - Execute OnError hook on tool failures - Execute SessionEnd hook on completion Workflow Integration: - Add execute_with_agent method to WorkflowManager - Create Agent instance for each workflow step - Execute actual tasks instead of placeholders - Proper error handling and result tracking All 662 tests passing. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
b4e6001c72
commit
f64faa6f09
2 changed files with 188 additions and 0 deletions
|
|
@ -6,6 +6,7 @@ use serde_json::Value;
|
|||
use tokio::sync::mpsc;
|
||||
|
||||
use crate::{AnthropicClient, ContentBlock, Message, Role, StopReason};
|
||||
use crate::hooks::{HookContext, HookEvent, HookManager, HooksConfig};
|
||||
|
||||
use super::{AgentError, AgentEvent, AgentResult, ExecutorRegistry};
|
||||
|
||||
|
|
@ -43,20 +44,33 @@ pub struct Agent {
|
|||
config: AgentConfig,
|
||||
system_prompt: Option<String>,
|
||||
event_tx: Option<mpsc::Sender<AgentEvent>>,
|
||||
hook_manager: HookManager,
|
||||
}
|
||||
|
||||
impl Agent {
|
||||
/// Create new agent with client and tools
|
||||
pub fn new(client: AnthropicClient, executor_registry: ExecutorRegistry) -> Self {
|
||||
// Load hooks from default configuration
|
||||
let hook_manager = HooksConfig::load_default()
|
||||
.map(HookManager::from_config)
|
||||
.unwrap_or_else(|_| HookManager::new());
|
||||
|
||||
Self {
|
||||
client,
|
||||
executor_registry,
|
||||
config: AgentConfig::default(),
|
||||
system_prompt: None,
|
||||
event_tx: None,
|
||||
hook_manager,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set custom hook manager
|
||||
pub fn with_hook_manager(mut self, hook_manager: HookManager) -> Self {
|
||||
self.hook_manager = hook_manager;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set agent configuration
|
||||
pub fn with_config(mut self, config: AgentConfig) -> Self {
|
||||
self.config = config;
|
||||
|
|
@ -128,6 +142,11 @@ impl Agent {
|
|||
|
||||
/// Main agent execution loop
|
||||
pub async fn run(&self, prompt: &str) -> Result<AgentResult, AgentError> {
|
||||
// Execute SessionStart hooks
|
||||
let session_context = HookContext::new()
|
||||
.with_data("prompt", prompt);
|
||||
self.hook_manager.execute(&HookEvent::SessionStart, &session_context).await;
|
||||
|
||||
self.emit_event(AgentEvent::Started {
|
||||
prompt: prompt.to_string(),
|
||||
})
|
||||
|
|
@ -186,6 +205,13 @@ impl Agent {
|
|||
result: result.clone(),
|
||||
})
|
||||
.await;
|
||||
|
||||
// Execute SessionEnd hooks
|
||||
let end_context = HookContext::new()
|
||||
.with_data("iterations", &result.iterations.to_string())
|
||||
.with_data("tool_calls", &result.tool_calls.to_string());
|
||||
self.hook_manager.execute(&HookEvent::SessionEnd, &end_context).await;
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
Some(StopReason::ToolUse) => {
|
||||
|
|
@ -218,6 +244,12 @@ impl Agent {
|
|||
// TODO: Add approval callback mechanism
|
||||
}
|
||||
|
||||
// Execute PreTool hooks
|
||||
let pre_tool_context = HookContext::new()
|
||||
.with_tool(&tool_use.name)
|
||||
.with_data("input", &tool_use.input.to_string());
|
||||
self.hook_manager.execute(&HookEvent::PreTool, &pre_tool_context).await;
|
||||
|
||||
// Execute tool
|
||||
self.emit_event(AgentEvent::ToolExecuting {
|
||||
name: tool_use.name.clone(),
|
||||
|
|
@ -237,6 +269,12 @@ impl Agent {
|
|||
})
|
||||
.await;
|
||||
|
||||
// Execute PostTool hooks
|
||||
let post_tool_context = HookContext::new()
|
||||
.with_tool(&tool_use.name)
|
||||
.with_result(&output.content.to_string());
|
||||
self.hook_manager.execute(&HookEvent::PostTool, &post_tool_context).await;
|
||||
|
||||
// Create tool result
|
||||
let content = serde_json::to_string(&output.content)
|
||||
.unwrap_or_else(|_| output.content.to_string());
|
||||
|
|
@ -253,6 +291,12 @@ impl Agent {
|
|||
})
|
||||
.await;
|
||||
|
||||
// Execute OnError hooks
|
||||
let error_context = HookContext::new()
|
||||
.with_tool(&tool_use.name)
|
||||
.with_error(&e.to_string());
|
||||
self.hook_manager.execute(&HookEvent::OnError, &error_context).await;
|
||||
|
||||
// Add error as tool result
|
||||
results.push(ContentBlock::ToolResult {
|
||||
tool_use_id: tool_use.id,
|
||||
|
|
@ -294,6 +338,13 @@ impl Agent {
|
|||
result: result.clone(),
|
||||
})
|
||||
.await;
|
||||
|
||||
// Execute SessionEnd hooks
|
||||
let end_context = HookContext::new()
|
||||
.with_data("iterations", &result.iterations.to_string())
|
||||
.with_data("tool_calls", &result.tool_calls.to_string());
|
||||
self.hook_manager.execute(&HookEvent::SessionEnd, &end_context).await;
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@ use serde::{Deserialize, Serialize};
|
|||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::agent::{Agent, ExecutorRegistry};
|
||||
use crate::AnthropicClient;
|
||||
|
||||
/// Workflow definition
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Workflow {
|
||||
|
|
@ -378,6 +381,140 @@ impl WorkflowManager {
|
|||
})
|
||||
}
|
||||
|
||||
/// Execute a workflow with actual Agent execution
|
||||
pub async fn execute_with_agent(
|
||||
&self,
|
||||
name: &str,
|
||||
initial_vars: HashMap<String, String>,
|
||||
client: AnthropicClient,
|
||||
registry: ExecutorRegistry,
|
||||
) -> Result<WorkflowResult> {
|
||||
let workflow = self
|
||||
.workflows
|
||||
.get(name)
|
||||
.ok_or_else(|| anyhow::anyhow!("Workflow not found: {}", name))?;
|
||||
|
||||
let start_time = std::time::Instant::now();
|
||||
let mut context = WorkflowContext::new().with_variables(workflow.variables.clone());
|
||||
|
||||
// Add initial variables
|
||||
for (key, value) in initial_vars {
|
||||
context.set_variable(&key, &value);
|
||||
}
|
||||
|
||||
let mut step_results = Vec::new();
|
||||
let mut all_succeeded = true;
|
||||
|
||||
// Build dependency graph and execute
|
||||
let execution_order = self.build_execution_order(workflow)?;
|
||||
|
||||
for step_id in execution_order {
|
||||
let step = workflow
|
||||
.steps
|
||||
.iter()
|
||||
.find(|s| s.id == step_id)
|
||||
.ok_or_else(|| anyhow::anyhow!("Step not found: {}", step_id))?;
|
||||
|
||||
// Check dependencies
|
||||
let deps_satisfied = step.depends_on.iter().all(|dep_id| {
|
||||
context
|
||||
.get_result(dep_id)
|
||||
.map(|r| r.status == StepStatus::Completed)
|
||||
.unwrap_or(false)
|
||||
});
|
||||
|
||||
if !deps_satisfied {
|
||||
let result = StepResult {
|
||||
step_id: step.id.clone(),
|
||||
status: StepStatus::Skipped,
|
||||
output: None,
|
||||
error: Some("Dependencies not satisfied".to_string()),
|
||||
duration_ms: 0,
|
||||
};
|
||||
step_results.push(result.clone());
|
||||
context.add_result(result);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check condition
|
||||
if !self.evaluate_condition(&step.condition, &context) {
|
||||
let result = StepResult {
|
||||
step_id: step.id.clone(),
|
||||
status: StepStatus::Skipped,
|
||||
output: None,
|
||||
error: Some("Condition not met".to_string()),
|
||||
duration_ms: 0,
|
||||
};
|
||||
step_results.push(result.clone());
|
||||
context.add_result(result);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Execute step with Agent
|
||||
let step_start = std::time::Instant::now();
|
||||
let expanded_task = context.expand(&step.task);
|
||||
|
||||
// Create agent for this step
|
||||
let agent = Agent::new(client.clone(), registry.clone());
|
||||
|
||||
// Execute the task
|
||||
let result = match agent.run(&expanded_task).await {
|
||||
Ok(agent_result) => {
|
||||
StepResult {
|
||||
step_id: step.id.clone(),
|
||||
status: StepStatus::Completed,
|
||||
output: Some(agent_result.output),
|
||||
error: None,
|
||||
duration_ms: step_start.elapsed().as_millis() as u64,
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
StepResult {
|
||||
step_id: step.id.clone(),
|
||||
status: StepStatus::Failed,
|
||||
output: None,
|
||||
error: Some(e.to_string()),
|
||||
duration_ms: step_start.elapsed().as_millis() as u64,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Store output variable if specified
|
||||
if let Some(output_var) = &step.output {
|
||||
if let Some(output) = &result.output {
|
||||
context.set_variable(output_var, output);
|
||||
}
|
||||
}
|
||||
|
||||
if result.status == StepStatus::Failed {
|
||||
all_succeeded = false;
|
||||
if matches!(workflow.on_failure, FailurePolicy::Stop) {
|
||||
step_results.push(result.clone());
|
||||
context.add_result(result);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
step_results.push(result.clone());
|
||||
context.add_result(result);
|
||||
}
|
||||
|
||||
let status = if all_succeeded {
|
||||
WorkflowStatus::Completed
|
||||
} else if step_results.iter().any(|r| r.status == StepStatus::Completed) {
|
||||
WorkflowStatus::PartiallyCompleted
|
||||
} else {
|
||||
WorkflowStatus::Failed
|
||||
};
|
||||
|
||||
Ok(WorkflowResult {
|
||||
workflow_name: name.to_string(),
|
||||
status,
|
||||
steps: step_results,
|
||||
duration_ms: start_time.elapsed().as_millis() as u64,
|
||||
})
|
||||
}
|
||||
|
||||
/// Build execution order from dependencies (topological sort)
|
||||
fn build_execution_order(&self, workflow: &Workflow) -> Result<Vec<String>> {
|
||||
let mut order = Vec::new();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue