-
Notifications
You must be signed in to change notification settings - Fork 6k
feat: Hooks #9093
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Hooks #9093
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -171,6 +171,7 @@ pub struct Agent { | |
|
|
||
| pub(super) retry_manager: RetryManager, | ||
| pub(super) tool_inspection_manager: ToolInspectionManager, | ||
| pub(super) hook_manager: crate::hooks::HookManager, | ||
| container: Mutex<Option<Container>>, | ||
| } | ||
|
|
||
|
|
@@ -282,10 +283,63 @@ impl Agent { | |
| permission_manager, | ||
| provider.clone(), | ||
| ), | ||
| hook_manager: crate::hooks::HookManager::load(std::env::current_dir().ok().as_deref()), | ||
| container: Mutex::new(None), | ||
| } | ||
| } | ||
|
|
||
| /// Emit a lifecycle hook event with no extra context. Useful for events | ||
| /// that have no matcher (e.g. `SessionStart`, `SessionEnd`). | ||
| pub async fn emit_hook(&self, event: crate::hooks::HookEvent, session_id: &str) { | ||
| if !self.hook_manager.has_hooks(event) { | ||
| return; | ||
| } | ||
| self.hook_manager | ||
| .emit(event, crate::hooks::HookContext::new(event, session_id)) | ||
| .await; | ||
| } | ||
|
|
||
| fn with_post_tool_hook( | ||
| &self, | ||
| result: ToolCallResult, | ||
| tool_call: &CallToolRequestParams, | ||
| session: &Session, | ||
| ) -> ToolCallResult { | ||
| let hook_manager = self.hook_manager.clone(); | ||
| let session_id = session.id.clone(); | ||
| let working_dir = session.working_dir.to_string_lossy().to_string(); | ||
| let tool_name = tool_call.name.to_string(); | ||
| let tool_input = tool_call | ||
| .arguments | ||
| .as_ref() | ||
| .map(|a| serde_json::Value::Object(a.clone())); | ||
|
|
||
| let fut = async move { | ||
| let processed_result = | ||
| super::large_response_handler::process_tool_response(result.result.await); | ||
| let event = match &processed_result { | ||
| Ok(call_result) if call_result.is_error != Some(true) => { | ||
| crate::hooks::HookEvent::PostToolUse | ||
| } | ||
| _ => crate::hooks::HookEvent::PostToolUseFailure, | ||
| }; | ||
|
|
||
| if hook_manager.has_hooks(event) { | ||
| let ctx = crate::hooks::HookContext::new(event, &session_id) | ||
| .with_tool(tool_name, tool_input) | ||
| .with_working_dir(working_dir); | ||
|
alexhancock marked this conversation as resolved.
|
||
| hook_manager.emit(event, ctx).await; | ||
| } | ||
|
|
||
| processed_result | ||
| }; | ||
|
|
||
| ToolCallResult { | ||
| notification_stream: result.notification_stream, | ||
| result: Box::new(fut.boxed()), | ||
| } | ||
| } | ||
|
|
||
| /// Create a tool inspection manager with default inspectors | ||
| fn create_tool_inspection_manager( | ||
| permission_manager: Arc<PermissionManager>, | ||
|
|
@@ -613,22 +667,52 @@ impl Agent { | |
| .await | ||
| .record_tool_arguments(&tool_call.arguments, &session.working_dir); | ||
|
|
||
| if self | ||
| .hook_manager | ||
| .has_hooks(crate::hooks::HookEvent::PreToolUse) | ||
| { | ||
| let ctx = | ||
| crate::hooks::HookContext::new(crate::hooks::HookEvent::PreToolUse, &session.id) | ||
| .with_tool( | ||
| tool_call.name.to_string(), | ||
| tool_call | ||
| .arguments | ||
| .as_ref() | ||
| .map(|a| serde_json::Value::Object(a.clone())), | ||
| ) | ||
| .with_working_dir(session.working_dir.to_string_lossy().to_string()); | ||
| self.hook_manager | ||
| .emit(crate::hooks::HookEvent::PreToolUse, ctx) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. PreToolUse would allow blocking tool execution, right? Is that something we'd come back to? Looks like open-plugins itself doesn't specify, but I believe in Claude Code this is designed such that you could e.g. have a hook deny a tool call
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Interesting! Going to merge to take advantage of the green ci but will make a note to revisit in a future change |
||
| .await; | ||
| } | ||
|
|
||
| if tool_call.name == PLATFORM_MANAGE_SCHEDULE_TOOL_NAME { | ||
| let arguments = tool_call | ||
| .arguments | ||
| .clone() | ||
| .map(Value::Object) | ||
| .unwrap_or(Value::Object(serde_json::Map::new())); | ||
| let result = self | ||
| .handle_schedule_management(arguments, request_id.clone()) | ||
| .await; | ||
| let wrapped_result = result.map(CallToolResult::success); | ||
| return (request_id, Ok(ToolCallResult::from(wrapped_result))); | ||
| return ( | ||
| request_id, | ||
| Ok(self.with_post_tool_hook( | ||
| ToolCallResult::from(wrapped_result), | ||
| &tool_call, | ||
| session, | ||
| )), | ||
| ); | ||
| } | ||
|
|
||
| if tool_call.name == FINAL_OUTPUT_TOOL_NAME { | ||
| return if let Some(final_output_tool) = self.final_output_tool.lock().await.as_mut() { | ||
| let result = final_output_tool.execute_tool_call(tool_call.clone()).await; | ||
| (request_id, Ok(result)) | ||
| ( | ||
| request_id, | ||
| Ok(self.with_post_tool_hook(result, &tool_call, session)), | ||
| ) | ||
| } else { | ||
| ( | ||
| request_id, | ||
|
|
@@ -680,14 +764,7 @@ impl Agent { | |
|
|
||
| ( | ||
| request_id, | ||
| Ok(ToolCallResult { | ||
| notification_stream: result.notification_stream, | ||
| result: Box::new( | ||
| result | ||
| .result | ||
| .map(super::large_response_handler::process_tool_response), | ||
| ), | ||
| }), | ||
| Ok(self.with_post_tool_hook(result, &tool_call, session)), | ||
| ) | ||
| } | ||
|
|
||
|
|
@@ -1086,6 +1163,20 @@ impl Agent { | |
|
|
||
| let message_text = user_message.as_concat_text(); | ||
|
|
||
| if self | ||
| .hook_manager | ||
| .has_hooks(crate::hooks::HookEvent::UserPromptSubmit) | ||
| { | ||
| let ctx = crate::hooks::HookContext::new( | ||
| crate::hooks::HookEvent::UserPromptSubmit, | ||
| &session_config.id, | ||
| ) | ||
| .with_message(message_text.clone()); | ||
| self.hook_manager | ||
| .emit(crate::hooks::HookEvent::UserPromptSubmit, ctx) | ||
| .await; | ||
| } | ||
|
|
||
| // Track custom slash command usage (don't track command name for privacy) | ||
| if message_text.trim().starts_with('/') { | ||
| let command = message_text.split_whitespace().next(); | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.