feat: plugin hooks + tool registry + CLI integration

This commit is contained in:
Yeachan-Heo
2026-04-01 06:55:39 +00:00
parent bea025b585
commit 0a4cea5ab2
6 changed files with 526 additions and 102 deletions

View File

@@ -178,8 +178,10 @@ where
feature_config: RuntimeFeatureConfig,
plugin_registry: PluginRegistry,
) -> Result<Self, RuntimeError> {
let plugin_hook_runner = PluginHookRunner::from_registry(&plugin_registry)
.map_err(|error| RuntimeError::new(format!("plugin hook registration failed: {error}")))?;
let plugin_hook_runner =
PluginHookRunner::from_registry(&plugin_registry).map_err(|error| {
RuntimeError::new(format!("plugin hook registration failed: {error}"))
})?;
plugin_registry
.initialize()
.map_err(|error| RuntimeError::new(format!("plugin initialization failed: {error}")))?;
@@ -202,6 +204,7 @@ where
self
}
#[allow(clippy::too_many_lines)]
pub fn run_turn(
&mut self,
user_input: impl Into<String>,
@@ -275,56 +278,64 @@ where
if plugin_pre_hook_result.is_denied() {
let deny_message =
format!("PreToolUse hook denied tool `{tool_name}`");
let mut messages = pre_hook_result.messages().to_vec();
messages.extend(plugin_pre_hook_result.messages().iter().cloned());
ConversationMessage::tool_result(
tool_use_id,
tool_name,
format_hook_message(
plugin_pre_hook_result.messages(),
&deny_message,
),
format_hook_message(&messages, &deny_message),
true,
)
} else {
let (mut output, mut is_error) =
match self.tool_executor.execute(&tool_name, &input) {
Ok(output) => (output, false),
Err(error) => (error.to_string(), true),
};
output = merge_hook_feedback(pre_hook_result.messages(), output, false);
output = merge_hook_feedback(
plugin_pre_hook_result.messages(),
output,
false,
);
let (mut output, mut is_error) =
match self.tool_executor.execute(&tool_name, &input) {
Ok(output) => (output, false),
Err(error) => (error.to_string(), true),
};
output =
merge_hook_feedback(pre_hook_result.messages(), output, false);
output = merge_hook_feedback(
plugin_pre_hook_result.messages(),
output,
false,
);
let post_hook_result = self
.hook_runner
.run_post_tool_use(&tool_name, &input, &output, is_error);
if post_hook_result.is_denied() {
is_error = true;
}
output = merge_hook_feedback(
post_hook_result.messages(),
output,
post_hook_result.is_denied(),
);
let plugin_post_hook_result =
self.run_plugin_post_tool_use(&tool_name, &input, &output, is_error);
if plugin_post_hook_result.is_denied() {
is_error = true;
}
output = merge_hook_feedback(
plugin_post_hook_result.messages(),
output,
plugin_post_hook_result.is_denied(),
);
let hook_output = output.clone();
let post_hook_result = self.hook_runner.run_post_tool_use(
&tool_name,
&input,
&hook_output,
is_error,
);
let plugin_post_hook_result = self.run_plugin_post_tool_use(
&tool_name,
&input,
&hook_output,
is_error,
);
if post_hook_result.is_denied() {
is_error = true;
}
if plugin_post_hook_result.is_denied() {
is_error = true;
}
output = merge_hook_feedback(
post_hook_result.messages(),
output,
post_hook_result.is_denied(),
);
output = merge_hook_feedback(
plugin_post_hook_result.messages(),
output,
plugin_post_hook_result.is_denied(),
);
ConversationMessage::tool_result(
tool_use_id,
tool_name,
output,
is_error,
)
ConversationMessage::tool_result(
tool_use_id,
tool_name,
output,
is_error,
)
}
}
}
@@ -449,11 +460,11 @@ fn flush_text_block(text: &mut String, blocks: &mut Vec<ContentBlock>) {
}
}
fn format_hook_message(result: &HookRunResult, fallback: &str) -> String {
if result.messages().is_empty() {
fn format_hook_message(messages: &[String], fallback: &str) -> String {
if messages.is_empty() {
fallback.to_string()
} else {
result.messages().join("\n")
messages.join("\n")
}
}