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:
Shunsuke Hayashi 2025-11-22 23:59:59 +09:00
parent b4e6001c72
commit f64faa6f09
2 changed files with 188 additions and 0 deletions

View file

@ -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);
}
}

View file

@ -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();