From f64faa6f0980ebeb2f45ec58444f178a3c82168d Mon Sep 17 00:00:00 2001 From: Shunsuke Hayashi Date: Sat, 22 Nov 2025 23:59:59 +0900 Subject: [PATCH] feat: Integrate Hooks and Workflow with Agent execution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- crates/miyabi-core/src/agent/core.rs | 51 ++++++++++ crates/miyabi-core/src/workflow.rs | 137 +++++++++++++++++++++++++++ 2 files changed, 188 insertions(+) diff --git a/crates/miyabi-core/src/agent/core.rs b/crates/miyabi-core/src/agent/core.rs index 1b89c08..ccd535a 100644 --- a/crates/miyabi-core/src/agent/core.rs +++ b/crates/miyabi-core/src/agent/core.rs @@ -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, event_tx: Option>, + 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 { + // 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); } } diff --git a/crates/miyabi-core/src/workflow.rs b/crates/miyabi-core/src/workflow.rs index 447877d..414e573 100644 --- a/crates/miyabi-core/src/workflow.rs +++ b/crates/miyabi-core/src/workflow.rs @@ -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, + client: AnthropicClient, + registry: ExecutorRegistry, + ) -> Result { + 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> { let mut order = Vec::new();