mirror of
https://github.com/instructkr/claw-code.git
synced 2026-04-06 16:14:49 +08:00
merge: clawcode-issue-9405-plugins-execution-pipeline into main
This commit is contained in:
@@ -1,4 +1,11 @@
|
|||||||
#![allow(dead_code, unused_imports, unused_variables, clippy::unneeded_struct_pattern, clippy::unnecessary_wraps, clippy::unused_self)]
|
#![allow(
|
||||||
|
dead_code,
|
||||||
|
unused_imports,
|
||||||
|
unused_variables,
|
||||||
|
clippy::unneeded_struct_pattern,
|
||||||
|
clippy::unnecessary_wraps,
|
||||||
|
clippy::unused_self
|
||||||
|
)]
|
||||||
mod init;
|
mod init;
|
||||||
mod input;
|
mod input;
|
||||||
mod render;
|
mod render;
|
||||||
@@ -8,6 +15,7 @@ use std::env;
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io::{self, Read, Write};
|
use std::io::{self, Read, Write};
|
||||||
use std::net::TcpListener;
|
use std::net::TcpListener;
|
||||||
|
use std::ops::{Deref, DerefMut};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use std::sync::mpsc::{self, Receiver, RecvTimeoutError, Sender};
|
use std::sync::mpsc::{self, Receiver, RecvTimeoutError, Sender};
|
||||||
@@ -28,7 +36,7 @@ use commands::{
|
|||||||
};
|
};
|
||||||
use compat_harness::{extract_manifest, UpstreamPaths};
|
use compat_harness::{extract_manifest, UpstreamPaths};
|
||||||
use init::initialize_repo;
|
use init::initialize_repo;
|
||||||
use plugins::{PluginManager, PluginManagerConfig};
|
use plugins::{PluginHooks, PluginManager, PluginManagerConfig, PluginRegistry};
|
||||||
use render::{MarkdownStreamState, Spinner, TerminalRenderer};
|
use render::{MarkdownStreamState, Spinner, TerminalRenderer};
|
||||||
use runtime::{
|
use runtime::{
|
||||||
clear_oauth_credentials, generate_pkce_pair, generate_state, load_system_prompt,
|
clear_oauth_credentials, generate_pkce_pair, generate_state, load_system_prompt,
|
||||||
@@ -1475,10 +1483,76 @@ struct LiveCli {
|
|||||||
allowed_tools: Option<AllowedToolSet>,
|
allowed_tools: Option<AllowedToolSet>,
|
||||||
permission_mode: PermissionMode,
|
permission_mode: PermissionMode,
|
||||||
system_prompt: Vec<String>,
|
system_prompt: Vec<String>,
|
||||||
runtime: ConversationRuntime<AnthropicRuntimeClient, CliToolExecutor>,
|
runtime: BuiltRuntime,
|
||||||
session: SessionHandle,
|
session: SessionHandle,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct RuntimePluginState {
|
||||||
|
feature_config: runtime::RuntimeFeatureConfig,
|
||||||
|
tool_registry: GlobalToolRegistry,
|
||||||
|
plugin_registry: PluginRegistry,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct BuiltRuntime {
|
||||||
|
runtime: Option<ConversationRuntime<AnthropicRuntimeClient, CliToolExecutor>>,
|
||||||
|
plugin_registry: PluginRegistry,
|
||||||
|
plugins_active: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BuiltRuntime {
|
||||||
|
fn new(
|
||||||
|
runtime: ConversationRuntime<AnthropicRuntimeClient, CliToolExecutor>,
|
||||||
|
plugin_registry: PluginRegistry,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
runtime: Some(runtime),
|
||||||
|
plugin_registry,
|
||||||
|
plugins_active: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_hook_abort_signal(mut self, hook_abort_signal: runtime::HookAbortSignal) -> Self {
|
||||||
|
let runtime = self
|
||||||
|
.runtime
|
||||||
|
.take()
|
||||||
|
.expect("runtime should exist before installing hook abort signal");
|
||||||
|
self.runtime = Some(runtime.with_hook_abort_signal(hook_abort_signal));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shutdown_plugins(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
if self.plugins_active {
|
||||||
|
self.plugin_registry.shutdown()?;
|
||||||
|
self.plugins_active = false;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for BuiltRuntime {
|
||||||
|
type Target = ConversationRuntime<AnthropicRuntimeClient, CliToolExecutor>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
self.runtime
|
||||||
|
.as_ref()
|
||||||
|
.expect("runtime should exist while built runtime is alive")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DerefMut for BuiltRuntime {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
self.runtime
|
||||||
|
.as_mut()
|
||||||
|
.expect("runtime should exist while built runtime is alive")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for BuiltRuntime {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let _ = self.shutdown_plugins();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct HookAbortMonitor {
|
struct HookAbortMonitor {
|
||||||
stop_tx: Option<Sender<()>>,
|
stop_tx: Option<Sender<()>>,
|
||||||
join_handle: Option<JoinHandle<()>>,
|
join_handle: Option<JoinHandle<()>>,
|
||||||
@@ -1625,13 +1699,7 @@ impl LiveCli {
|
|||||||
fn prepare_turn_runtime(
|
fn prepare_turn_runtime(
|
||||||
&self,
|
&self,
|
||||||
emit_output: bool,
|
emit_output: bool,
|
||||||
) -> Result<
|
) -> Result<(BuiltRuntime, HookAbortMonitor), Box<dyn std::error::Error>> {
|
||||||
(
|
|
||||||
ConversationRuntime<AnthropicRuntimeClient, CliToolExecutor>,
|
|
||||||
HookAbortMonitor,
|
|
||||||
),
|
|
||||||
Box<dyn std::error::Error>,
|
|
||||||
> {
|
|
||||||
let hook_abort_signal = runtime::HookAbortSignal::new();
|
let hook_abort_signal = runtime::HookAbortSignal::new();
|
||||||
let runtime = build_runtime(
|
let runtime = build_runtime(
|
||||||
self.runtime.session().clone(),
|
self.runtime.session().clone(),
|
||||||
@@ -1650,6 +1718,12 @@ impl LiveCli {
|
|||||||
Ok((runtime, hook_abort_monitor))
|
Ok((runtime, hook_abort_monitor))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn replace_runtime(&mut self, runtime: BuiltRuntime) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
self.runtime.shutdown_plugins()?;
|
||||||
|
self.runtime = runtime;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn run_turn(&mut self, input: &str) -> Result<(), Box<dyn std::error::Error>> {
|
fn run_turn(&mut self, input: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let (mut runtime, hook_abort_monitor) = self.prepare_turn_runtime(true)?;
|
let (mut runtime, hook_abort_monitor) = self.prepare_turn_runtime(true)?;
|
||||||
let mut spinner = Spinner::new();
|
let mut spinner = Spinner::new();
|
||||||
@@ -1662,9 +1736,9 @@ impl LiveCli {
|
|||||||
let mut permission_prompter = CliPermissionPrompter::new(self.permission_mode);
|
let mut permission_prompter = CliPermissionPrompter::new(self.permission_mode);
|
||||||
let result = runtime.run_turn(input, Some(&mut permission_prompter));
|
let result = runtime.run_turn(input, Some(&mut permission_prompter));
|
||||||
hook_abort_monitor.stop();
|
hook_abort_monitor.stop();
|
||||||
self.runtime = runtime;
|
|
||||||
match result {
|
match result {
|
||||||
Ok(summary) => {
|
Ok(summary) => {
|
||||||
|
self.replace_runtime(runtime)?;
|
||||||
spinner.finish(
|
spinner.finish(
|
||||||
"✨ Done",
|
"✨ Done",
|
||||||
TerminalRenderer::new().color_theme(),
|
TerminalRenderer::new().color_theme(),
|
||||||
@@ -1681,6 +1755,7 @@ impl LiveCli {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
|
runtime.shutdown_plugins()?;
|
||||||
spinner.fail(
|
spinner.fail(
|
||||||
"❌ Request failed",
|
"❌ Request failed",
|
||||||
TerminalRenderer::new().color_theme(),
|
TerminalRenderer::new().color_theme(),
|
||||||
@@ -1708,7 +1783,7 @@ impl LiveCli {
|
|||||||
let result = runtime.run_turn(input, Some(&mut permission_prompter));
|
let result = runtime.run_turn(input, Some(&mut permission_prompter));
|
||||||
hook_abort_monitor.stop();
|
hook_abort_monitor.stop();
|
||||||
let summary = result?;
|
let summary = result?;
|
||||||
self.runtime = runtime;
|
self.replace_runtime(runtime)?;
|
||||||
self.persist_session()?;
|
self.persist_session()?;
|
||||||
println!(
|
println!(
|
||||||
"{}",
|
"{}",
|
||||||
@@ -1903,7 +1978,7 @@ impl LiveCli {
|
|||||||
let previous = self.model.clone();
|
let previous = self.model.clone();
|
||||||
let session = self.runtime.session().clone();
|
let session = self.runtime.session().clone();
|
||||||
let message_count = session.messages.len();
|
let message_count = session.messages.len();
|
||||||
self.runtime = build_runtime(
|
let runtime = build_runtime(
|
||||||
session,
|
session,
|
||||||
&self.session.id,
|
&self.session.id,
|
||||||
model.clone(),
|
model.clone(),
|
||||||
@@ -1914,6 +1989,7 @@ impl LiveCli {
|
|||||||
self.permission_mode,
|
self.permission_mode,
|
||||||
None,
|
None,
|
||||||
)?;
|
)?;
|
||||||
|
self.replace_runtime(runtime)?;
|
||||||
self.model.clone_from(&model);
|
self.model.clone_from(&model);
|
||||||
println!(
|
println!(
|
||||||
"{}",
|
"{}",
|
||||||
@@ -1948,7 +2024,7 @@ impl LiveCli {
|
|||||||
let previous = self.permission_mode.as_str().to_string();
|
let previous = self.permission_mode.as_str().to_string();
|
||||||
let session = self.runtime.session().clone();
|
let session = self.runtime.session().clone();
|
||||||
self.permission_mode = permission_mode_from_label(normalized);
|
self.permission_mode = permission_mode_from_label(normalized);
|
||||||
self.runtime = build_runtime(
|
let runtime = build_runtime(
|
||||||
session,
|
session,
|
||||||
&self.session.id,
|
&self.session.id,
|
||||||
self.model.clone(),
|
self.model.clone(),
|
||||||
@@ -1959,6 +2035,7 @@ impl LiveCli {
|
|||||||
self.permission_mode,
|
self.permission_mode,
|
||||||
None,
|
None,
|
||||||
)?;
|
)?;
|
||||||
|
self.replace_runtime(runtime)?;
|
||||||
println!(
|
println!(
|
||||||
"{}",
|
"{}",
|
||||||
format_permissions_switch_report(&previous, normalized)
|
format_permissions_switch_report(&previous, normalized)
|
||||||
@@ -1976,7 +2053,7 @@ impl LiveCli {
|
|||||||
|
|
||||||
let session_state = Session::new();
|
let session_state = Session::new();
|
||||||
self.session = create_managed_session_handle(&session_state.session_id)?;
|
self.session = create_managed_session_handle(&session_state.session_id)?;
|
||||||
self.runtime = build_runtime(
|
let runtime = build_runtime(
|
||||||
session_state.with_persistence_path(self.session.path.clone()),
|
session_state.with_persistence_path(self.session.path.clone()),
|
||||||
&self.session.id,
|
&self.session.id,
|
||||||
self.model.clone(),
|
self.model.clone(),
|
||||||
@@ -1987,6 +2064,7 @@ impl LiveCli {
|
|||||||
self.permission_mode,
|
self.permission_mode,
|
||||||
None,
|
None,
|
||||||
)?;
|
)?;
|
||||||
|
self.replace_runtime(runtime)?;
|
||||||
println!(
|
println!(
|
||||||
"Session cleared\n Mode fresh session\n Preserved model {}\n Permission mode {}\n Session {}",
|
"Session cleared\n Mode fresh session\n Preserved model {}\n Permission mode {}\n Session {}",
|
||||||
self.model,
|
self.model,
|
||||||
@@ -2014,7 +2092,7 @@ impl LiveCli {
|
|||||||
let session = Session::load_from_path(&handle.path)?;
|
let session = Session::load_from_path(&handle.path)?;
|
||||||
let message_count = session.messages.len();
|
let message_count = session.messages.len();
|
||||||
let session_id = session.session_id.clone();
|
let session_id = session.session_id.clone();
|
||||||
self.runtime = build_runtime(
|
let runtime = build_runtime(
|
||||||
session,
|
session,
|
||||||
&handle.id,
|
&handle.id,
|
||||||
self.model.clone(),
|
self.model.clone(),
|
||||||
@@ -2025,6 +2103,7 @@ impl LiveCli {
|
|||||||
self.permission_mode,
|
self.permission_mode,
|
||||||
None,
|
None,
|
||||||
)?;
|
)?;
|
||||||
|
self.replace_runtime(runtime)?;
|
||||||
self.session = SessionHandle {
|
self.session = SessionHandle {
|
||||||
id: session_id,
|
id: session_id,
|
||||||
path: handle.path,
|
path: handle.path,
|
||||||
@@ -2104,7 +2183,7 @@ impl LiveCli {
|
|||||||
let session = Session::load_from_path(&handle.path)?;
|
let session = Session::load_from_path(&handle.path)?;
|
||||||
let message_count = session.messages.len();
|
let message_count = session.messages.len();
|
||||||
let session_id = session.session_id.clone();
|
let session_id = session.session_id.clone();
|
||||||
self.runtime = build_runtime(
|
let runtime = build_runtime(
|
||||||
session,
|
session,
|
||||||
&handle.id,
|
&handle.id,
|
||||||
self.model.clone(),
|
self.model.clone(),
|
||||||
@@ -2115,6 +2194,7 @@ impl LiveCli {
|
|||||||
self.permission_mode,
|
self.permission_mode,
|
||||||
None,
|
None,
|
||||||
)?;
|
)?;
|
||||||
|
self.replace_runtime(runtime)?;
|
||||||
self.session = SessionHandle {
|
self.session = SessionHandle {
|
||||||
id: session_id,
|
id: session_id,
|
||||||
path: handle.path,
|
path: handle.path,
|
||||||
@@ -2138,7 +2218,7 @@ impl LiveCli {
|
|||||||
let forked = forked.with_persistence_path(handle.path.clone());
|
let forked = forked.with_persistence_path(handle.path.clone());
|
||||||
let message_count = forked.messages.len();
|
let message_count = forked.messages.len();
|
||||||
forked.save_to_path(&handle.path)?;
|
forked.save_to_path(&handle.path)?;
|
||||||
self.runtime = build_runtime(
|
let runtime = build_runtime(
|
||||||
forked,
|
forked,
|
||||||
&handle.id,
|
&handle.id,
|
||||||
self.model.clone(),
|
self.model.clone(),
|
||||||
@@ -2149,6 +2229,7 @@ impl LiveCli {
|
|||||||
self.permission_mode,
|
self.permission_mode,
|
||||||
None,
|
None,
|
||||||
)?;
|
)?;
|
||||||
|
self.replace_runtime(runtime)?;
|
||||||
self.session = handle;
|
self.session = handle;
|
||||||
println!(
|
println!(
|
||||||
"Session forked\n Parent session {}\n Active session {}\n Branch {}\n File {}\n Messages {}",
|
"Session forked\n Parent session {}\n Active session {}\n Branch {}\n File {}\n Messages {}",
|
||||||
@@ -2187,7 +2268,7 @@ impl LiveCli {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn reload_runtime_features(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
fn reload_runtime_features(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
self.runtime = build_runtime(
|
let runtime = build_runtime(
|
||||||
self.runtime.session().clone(),
|
self.runtime.session().clone(),
|
||||||
&self.session.id,
|
&self.session.id,
|
||||||
self.model.clone(),
|
self.model.clone(),
|
||||||
@@ -2198,6 +2279,7 @@ impl LiveCli {
|
|||||||
self.permission_mode,
|
self.permission_mode,
|
||||||
None,
|
None,
|
||||||
)?;
|
)?;
|
||||||
|
self.replace_runtime(runtime)?;
|
||||||
self.persist_session()
|
self.persist_session()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2206,7 +2288,7 @@ impl LiveCli {
|
|||||||
let removed = result.removed_message_count;
|
let removed = result.removed_message_count;
|
||||||
let kept = result.compacted_session.messages.len();
|
let kept = result.compacted_session.messages.len();
|
||||||
let skipped = removed == 0;
|
let skipped = removed == 0;
|
||||||
self.runtime = build_runtime(
|
let runtime = build_runtime(
|
||||||
result.compacted_session,
|
result.compacted_session,
|
||||||
&self.session.id,
|
&self.session.id,
|
||||||
self.model.clone(),
|
self.model.clone(),
|
||||||
@@ -2217,6 +2299,7 @@ impl LiveCli {
|
|||||||
self.permission_mode,
|
self.permission_mode,
|
||||||
None,
|
None,
|
||||||
)?;
|
)?;
|
||||||
|
self.replace_runtime(runtime)?;
|
||||||
self.persist_session()?;
|
self.persist_session()?;
|
||||||
println!("{}", format_compact_report(removed, kept, skipped));
|
println!("{}", format_compact_report(removed, kept, skipped));
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -2242,7 +2325,9 @@ impl LiveCli {
|
|||||||
)?;
|
)?;
|
||||||
let mut permission_prompter = CliPermissionPrompter::new(self.permission_mode);
|
let mut permission_prompter = CliPermissionPrompter::new(self.permission_mode);
|
||||||
let summary = runtime.run_turn(prompt, Some(&mut permission_prompter))?;
|
let summary = runtime.run_turn(prompt, Some(&mut permission_prompter))?;
|
||||||
Ok(final_assistant_text(&summary).trim().to_string())
|
let text = final_assistant_text(&summary).trim().to_string();
|
||||||
|
runtime.shutdown_plugins()?;
|
||||||
|
Ok(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_internal_prompt_text(
|
fn run_internal_prompt_text(
|
||||||
@@ -3270,14 +3355,32 @@ fn build_system_prompt() -> Result<Vec<String>, Box<dyn std::error::Error>> {
|
|||||||
)?)
|
)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_runtime_plugin_state(
|
fn build_runtime_plugin_state() -> Result<RuntimePluginState, Box<dyn std::error::Error>> {
|
||||||
) -> Result<(runtime::RuntimeFeatureConfig, GlobalToolRegistry), Box<dyn std::error::Error>> {
|
|
||||||
let cwd = env::current_dir()?;
|
let cwd = env::current_dir()?;
|
||||||
let loader = ConfigLoader::default_for(&cwd);
|
let loader = ConfigLoader::default_for(&cwd);
|
||||||
let runtime_config = loader.load()?;
|
let runtime_config = loader.load()?;
|
||||||
|
build_runtime_plugin_state_with_loader(&cwd, &loader, &runtime_config)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_runtime_plugin_state_with_loader(
|
||||||
|
cwd: &Path,
|
||||||
|
loader: &ConfigLoader,
|
||||||
|
runtime_config: &runtime::RuntimeConfig,
|
||||||
|
) -> Result<RuntimePluginState, Box<dyn std::error::Error>> {
|
||||||
let plugin_manager = build_plugin_manager(&cwd, &loader, &runtime_config);
|
let plugin_manager = build_plugin_manager(&cwd, &loader, &runtime_config);
|
||||||
let tool_registry = GlobalToolRegistry::with_plugin_tools(plugin_manager.aggregated_tools()?)?;
|
let plugin_registry = plugin_manager.plugin_registry()?;
|
||||||
Ok((runtime_config.feature_config().clone(), tool_registry))
|
let plugin_hook_config =
|
||||||
|
runtime_hook_config_from_plugin_hooks(plugin_registry.aggregated_hooks()?);
|
||||||
|
let feature_config = runtime_config
|
||||||
|
.feature_config()
|
||||||
|
.clone()
|
||||||
|
.with_hooks(runtime_config.hooks().merged(&plugin_hook_config));
|
||||||
|
let tool_registry = GlobalToolRegistry::with_plugin_tools(plugin_registry.aggregated_tools()?)?;
|
||||||
|
Ok(RuntimePluginState {
|
||||||
|
feature_config,
|
||||||
|
tool_registry,
|
||||||
|
plugin_registry,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_plugin_manager(
|
fn build_plugin_manager(
|
||||||
@@ -3316,6 +3419,14 @@ fn resolve_plugin_path(cwd: &Path, config_home: &Path, value: &str) -> PathBuf {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn runtime_hook_config_from_plugin_hooks(hooks: PluginHooks) -> runtime::RuntimeHookConfig {
|
||||||
|
runtime::RuntimeHookConfig::new(
|
||||||
|
hooks.pre_tool_use,
|
||||||
|
hooks.post_tool_use,
|
||||||
|
hooks.post_tool_use_failure,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
struct InternalPromptProgressState {
|
struct InternalPromptProgressState {
|
||||||
command_label: &'static str,
|
command_label: &'static str,
|
||||||
@@ -3656,9 +3767,42 @@ fn build_runtime(
|
|||||||
allowed_tools: Option<AllowedToolSet>,
|
allowed_tools: Option<AllowedToolSet>,
|
||||||
permission_mode: PermissionMode,
|
permission_mode: PermissionMode,
|
||||||
progress_reporter: Option<InternalPromptProgressReporter>,
|
progress_reporter: Option<InternalPromptProgressReporter>,
|
||||||
) -> Result<ConversationRuntime<AnthropicRuntimeClient, CliToolExecutor>, Box<dyn std::error::Error>>
|
) -> Result<BuiltRuntime, Box<dyn std::error::Error>> {
|
||||||
{
|
let runtime_plugin_state = build_runtime_plugin_state()?;
|
||||||
let (feature_config, tool_registry) = build_runtime_plugin_state()?;
|
build_runtime_with_plugin_state(
|
||||||
|
session,
|
||||||
|
session_id,
|
||||||
|
model,
|
||||||
|
system_prompt,
|
||||||
|
enable_tools,
|
||||||
|
emit_output,
|
||||||
|
allowed_tools,
|
||||||
|
permission_mode,
|
||||||
|
progress_reporter,
|
||||||
|
runtime_plugin_state,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::needless_pass_by_value)]
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn build_runtime_with_plugin_state(
|
||||||
|
session: Session,
|
||||||
|
session_id: &str,
|
||||||
|
model: String,
|
||||||
|
system_prompt: Vec<String>,
|
||||||
|
enable_tools: bool,
|
||||||
|
emit_output: bool,
|
||||||
|
allowed_tools: Option<AllowedToolSet>,
|
||||||
|
permission_mode: PermissionMode,
|
||||||
|
progress_reporter: Option<InternalPromptProgressReporter>,
|
||||||
|
runtime_plugin_state: RuntimePluginState,
|
||||||
|
) -> Result<BuiltRuntime, Box<dyn std::error::Error>> {
|
||||||
|
let RuntimePluginState {
|
||||||
|
feature_config,
|
||||||
|
tool_registry,
|
||||||
|
plugin_registry,
|
||||||
|
} = runtime_plugin_state;
|
||||||
|
plugin_registry.initialize()?;
|
||||||
let mut runtime = ConversationRuntime::new_with_features(
|
let mut runtime = ConversationRuntime::new_with_features(
|
||||||
session,
|
session,
|
||||||
AnthropicRuntimeClient::new(
|
AnthropicRuntimeClient::new(
|
||||||
@@ -3679,7 +3823,7 @@ fn build_runtime(
|
|||||||
if emit_output {
|
if emit_output {
|
||||||
runtime = runtime.with_hook_progress_reporter(Box::new(CliHookProgressReporter));
|
runtime = runtime.with_hook_progress_reporter(Box::new(CliHookProgressReporter));
|
||||||
}
|
}
|
||||||
Ok(runtime)
|
Ok(BuiltRuntime::new(runtime, plugin_registry))
|
||||||
}
|
}
|
||||||
|
|
||||||
struct CliHookProgressReporter;
|
struct CliHookProgressReporter;
|
||||||
@@ -4847,6 +4991,7 @@ fn print_help() {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{
|
use super::{
|
||||||
|
build_runtime_plugin_state_with_loader, build_runtime_with_plugin_state,
|
||||||
create_managed_session_handle, describe_tool_progress, filter_tool_specs,
|
create_managed_session_handle, describe_tool_progress, filter_tool_specs,
|
||||||
format_bughunter_report, format_commit_preflight_report, format_commit_skipped_report,
|
format_bughunter_report, format_commit_preflight_report, format_commit_skipped_report,
|
||||||
format_compact_report, format_cost_report, format_internal_prompt_progress_line,
|
format_compact_report, format_cost_report, format_internal_prompt_progress_line,
|
||||||
@@ -4865,9 +5010,12 @@ mod tests {
|
|||||||
InternalPromptProgressState, LiveCli, SlashCommand, StatusUsage, DEFAULT_MODEL,
|
InternalPromptProgressState, LiveCli, SlashCommand, StatusUsage, DEFAULT_MODEL,
|
||||||
};
|
};
|
||||||
use api::{MessageResponse, OutputContentBlock, Usage};
|
use api::{MessageResponse, OutputContentBlock, Usage};
|
||||||
use plugins::{PluginTool, PluginToolDefinition, PluginToolPermission};
|
use plugins::{
|
||||||
|
PluginManager, PluginManagerConfig, PluginTool, PluginToolDefinition, PluginToolPermission,
|
||||||
|
};
|
||||||
use runtime::{
|
use runtime::{
|
||||||
AssistantEvent, ContentBlock, ConversationMessage, MessageRole, PermissionMode, Session,
|
AssistantEvent, ConfigLoader, ContentBlock, ConversationMessage, MessageRole,
|
||||||
|
PermissionMode, Session,
|
||||||
};
|
};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
@@ -4936,6 +5084,49 @@ mod tests {
|
|||||||
std::env::set_current_dir(previous).expect("cwd should restore");
|
std::env::set_current_dir(previous).expect("cwd should restore");
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn write_plugin_fixture(root: &Path, name: &str, include_hooks: bool, include_lifecycle: bool) {
|
||||||
|
fs::create_dir_all(root.join(".claude-plugin")).expect("manifest dir");
|
||||||
|
if include_hooks {
|
||||||
|
fs::create_dir_all(root.join("hooks")).expect("hooks dir");
|
||||||
|
fs::write(
|
||||||
|
root.join("hooks").join("pre.sh"),
|
||||||
|
"#!/bin/sh\nprintf 'plugin pre hook'\n",
|
||||||
|
)
|
||||||
|
.expect("write hook");
|
||||||
|
}
|
||||||
|
if include_lifecycle {
|
||||||
|
fs::create_dir_all(root.join("lifecycle")).expect("lifecycle dir");
|
||||||
|
fs::write(
|
||||||
|
root.join("lifecycle").join("init.sh"),
|
||||||
|
"#!/bin/sh\nprintf 'init\\n' >> lifecycle.log\n",
|
||||||
|
)
|
||||||
|
.expect("write init lifecycle");
|
||||||
|
fs::write(
|
||||||
|
root.join("lifecycle").join("shutdown.sh"),
|
||||||
|
"#!/bin/sh\nprintf 'shutdown\\n' >> lifecycle.log\n",
|
||||||
|
)
|
||||||
|
.expect("write shutdown lifecycle");
|
||||||
|
}
|
||||||
|
|
||||||
|
let hooks = if include_hooks {
|
||||||
|
",\n \"hooks\": {\n \"PreToolUse\": [\"./hooks/pre.sh\"]\n }"
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
};
|
||||||
|
let lifecycle = if include_lifecycle {
|
||||||
|
",\n \"lifecycle\": {\n \"Init\": [\"./lifecycle/init.sh\"],\n \"Shutdown\": [\"./lifecycle/shutdown.sh\"]\n }"
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
};
|
||||||
|
fs::write(
|
||||||
|
root.join(".claude-plugin").join("plugin.json"),
|
||||||
|
format!(
|
||||||
|
"{{\n \"name\": \"{name}\",\n \"version\": \"1.0.0\",\n \"description\": \"runtime plugin fixture\"{hooks}{lifecycle}\n}}"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.expect("write plugin manifest");
|
||||||
|
}
|
||||||
#[test]
|
#[test]
|
||||||
fn defaults_to_repl_when_no_args() {
|
fn defaults_to_repl_when_no_args() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -6384,6 +6575,89 @@ UU conflicted.rs",
|
|||||||
));
|
));
|
||||||
assert!(!String::from_utf8(out).expect("utf8").contains("step 1"));
|
assert!(!String::from_utf8(out).expect("utf8").contains("step 1"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_runtime_plugin_state_merges_plugin_hooks_into_runtime_features() {
|
||||||
|
let config_home = temp_dir();
|
||||||
|
let workspace = temp_dir();
|
||||||
|
let source_root = temp_dir();
|
||||||
|
fs::create_dir_all(&config_home).expect("config home");
|
||||||
|
fs::create_dir_all(&workspace).expect("workspace");
|
||||||
|
fs::create_dir_all(&source_root).expect("source root");
|
||||||
|
write_plugin_fixture(&source_root, "hook-runtime-demo", true, false);
|
||||||
|
|
||||||
|
let mut manager = PluginManager::new(PluginManagerConfig::new(&config_home));
|
||||||
|
manager
|
||||||
|
.install(source_root.to_str().expect("utf8 source path"))
|
||||||
|
.expect("plugin install should succeed");
|
||||||
|
let loader = ConfigLoader::new(&workspace, &config_home);
|
||||||
|
let runtime_config = loader.load().expect("runtime config should load");
|
||||||
|
let state = build_runtime_plugin_state_with_loader(&workspace, &loader, &runtime_config)
|
||||||
|
.expect("plugin state should load");
|
||||||
|
let pre_hooks = state.feature_config.hooks().pre_tool_use();
|
||||||
|
assert_eq!(pre_hooks.len(), 1);
|
||||||
|
assert!(
|
||||||
|
pre_hooks[0].ends_with("hooks/pre.sh"),
|
||||||
|
"expected installed plugin hook path, got {pre_hooks:?}"
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = fs::remove_dir_all(config_home);
|
||||||
|
let _ = fs::remove_dir_all(workspace);
|
||||||
|
let _ = fs::remove_dir_all(source_root);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_runtime_runs_plugin_lifecycle_init_and_shutdown() {
|
||||||
|
let config_home = temp_dir();
|
||||||
|
let workspace = temp_dir();
|
||||||
|
let source_root = temp_dir();
|
||||||
|
fs::create_dir_all(&config_home).expect("config home");
|
||||||
|
fs::create_dir_all(&workspace).expect("workspace");
|
||||||
|
fs::create_dir_all(&source_root).expect("source root");
|
||||||
|
write_plugin_fixture(&source_root, "lifecycle-runtime-demo", false, true);
|
||||||
|
|
||||||
|
let mut manager = PluginManager::new(PluginManagerConfig::new(&config_home));
|
||||||
|
let install = manager
|
||||||
|
.install(source_root.to_str().expect("utf8 source path"))
|
||||||
|
.expect("plugin install should succeed");
|
||||||
|
let log_path = install.install_path.join("lifecycle.log");
|
||||||
|
let loader = ConfigLoader::new(&workspace, &config_home);
|
||||||
|
let runtime_config = loader.load().expect("runtime config should load");
|
||||||
|
let runtime_plugin_state =
|
||||||
|
build_runtime_plugin_state_with_loader(&workspace, &loader, &runtime_config)
|
||||||
|
.expect("plugin state should load");
|
||||||
|
let mut runtime = build_runtime_with_plugin_state(
|
||||||
|
Session::new(),
|
||||||
|
"runtime-plugin-lifecycle",
|
||||||
|
DEFAULT_MODEL.to_string(),
|
||||||
|
vec!["test system prompt".to_string()],
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
None,
|
||||||
|
PermissionMode::DangerFullAccess,
|
||||||
|
None,
|
||||||
|
runtime_plugin_state,
|
||||||
|
)
|
||||||
|
.expect("runtime should build");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
fs::read_to_string(&log_path).expect("init log should exist"),
|
||||||
|
"init\n"
|
||||||
|
);
|
||||||
|
|
||||||
|
runtime
|
||||||
|
.shutdown_plugins()
|
||||||
|
.expect("plugin shutdown should succeed");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
fs::read_to_string(&log_path).expect("shutdown log should exist"),
|
||||||
|
"init\nshutdown\n"
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = fs::remove_dir_all(config_home);
|
||||||
|
let _ = fs::remove_dir_all(workspace);
|
||||||
|
let _ = fs::remove_dir_all(source_root);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
Reference in New Issue
Block a user