feat: plugin subsystem progress

This commit is contained in:
Yeachan-Heo
2026-04-01 06:50:18 +00:00
parent 4769353b30
commit bea025b585
9 changed files with 1173 additions and 276 deletions

View File

@@ -35,7 +35,7 @@ use runtime::{
Session, TokenUsage, ToolError, ToolExecutor, UsageTracker,
};
use serde_json::json;
use tools::{execute_tool, mvp_tool_specs, ToolSpec};
use tools::GlobalToolRegistry;
const DEFAULT_MODEL: &str = "claude-opus-4-6";
fn max_tokens_for_model(model: &str) -> u32 {
@@ -301,51 +301,20 @@ fn resolve_model_alias(model: &str) -> &str {
}
fn normalize_allowed_tools(values: &[String]) -> Result<Option<AllowedToolSet>, String> {
if values.is_empty() {
return Ok(None);
}
let canonical_names = mvp_tool_specs()
.into_iter()
.map(|spec| spec.name.to_string())
.collect::<Vec<_>>();
let mut name_map = canonical_names
.iter()
.map(|name| (normalize_tool_name(name), name.clone()))
.collect::<BTreeMap<_, _>>();
for (alias, canonical) in [
("read", "read_file"),
("write", "write_file"),
("edit", "edit_file"),
("glob", "glob_search"),
("grep", "grep_search"),
] {
name_map.insert(alias.to_string(), canonical.to_string());
}
let mut allowed = AllowedToolSet::new();
for value in values {
for token in value
.split(|ch: char| ch == ',' || ch.is_whitespace())
.filter(|token| !token.is_empty())
{
let normalized = normalize_tool_name(token);
let canonical = name_map.get(&normalized).ok_or_else(|| {
format!(
"unsupported tool in --allowedTools: {token} (expected one of: {})",
canonical_names.join(", ")
)
})?;
allowed.insert(canonical.clone());
}
}
Ok(Some(allowed))
current_tool_registry()
.unwrap_or_else(|_| GlobalToolRegistry::builtin())
.normalize_allowed_tools(values)
}
fn normalize_tool_name(value: &str) -> String {
value.trim().replace('-', "_").to_ascii_lowercase()
fn current_tool_registry() -> Result<GlobalToolRegistry, String> {
let cwd = env::current_dir().map_err(|error| error.to_string())?;
let loader = ConfigLoader::default_for(&cwd);
let runtime_config = loader.load().map_err(|error| error.to_string())?;
let plugin_manager = build_plugin_manager(&cwd, &loader, &runtime_config);
let plugin_tools = plugin_manager
.aggregated_tools()
.map_err(|error| error.to_string())?;
GlobalToolRegistry::with_plugin_tools(plugin_tools)
}
fn parse_permission_mode_arg(value: &str) -> Result<PermissionMode, String> {
@@ -375,11 +344,11 @@ fn default_permission_mode() -> PermissionMode {
.map_or(PermissionMode::DangerFullAccess, permission_mode_from_label)
}
fn filter_tool_specs(allowed_tools: Option<&AllowedToolSet>) -> Vec<tools::ToolSpec> {
mvp_tool_specs()
.into_iter()
.filter(|spec| allowed_tools.is_none_or(|allowed| allowed.contains(spec.name)))
.collect()
fn filter_tool_specs(
tool_registry: &GlobalToolRegistry,
allowed_tools: Option<&AllowedToolSet>,
) -> Vec<ToolDefinition> {
tool_registry.definitions(allowed_tools)
}
fn parse_system_prompt_args(args: &[String]) -> Result<CliAction, String> {
@@ -1944,13 +1913,21 @@ fn build_system_prompt() -> Result<Vec<String>, Box<dyn std::error::Error>> {
}
fn build_runtime_plugin_state(
) -> Result<(runtime::RuntimeFeatureConfig, PluginRegistry), Box<dyn std::error::Error>> {
) -> Result<
(
runtime::RuntimeFeatureConfig,
PluginRegistry,
GlobalToolRegistry,
),
Box<dyn std::error::Error>,
> {
let cwd = env::current_dir()?;
let loader = ConfigLoader::default_for(&cwd);
let runtime_config = loader.load()?;
let plugin_manager = build_plugin_manager(&cwd, &loader, &runtime_config);
let plugin_registry = plugin_manager.plugin_registry()?;
Ok((runtime_config.feature_config().clone(), plugin_registry))
let tool_registry = GlobalToolRegistry::with_plugin_tools(plugin_registry.aggregated_tools()?)?;
Ok((runtime_config.feature_config().clone(), plugin_registry, tool_registry))
}
fn build_plugin_manager(
@@ -1999,12 +1976,18 @@ fn build_runtime(
permission_mode: PermissionMode,
) -> Result<ConversationRuntime<AnthropicRuntimeClient, CliToolExecutor>, Box<dyn std::error::Error>>
{
let (feature_config, plugin_registry) = build_runtime_plugin_state()?;
let (feature_config, plugin_registry, tool_registry) = build_runtime_plugin_state()?;
Ok(ConversationRuntime::new_with_plugins(
session,
AnthropicRuntimeClient::new(model, enable_tools, emit_output, allowed_tools.clone())?,
CliToolExecutor::new(allowed_tools, emit_output),
permission_policy(permission_mode),
AnthropicRuntimeClient::new(
model,
enable_tools,
emit_output,
allowed_tools.clone(),
tool_registry.clone(),
)?,
CliToolExecutor::new(allowed_tools.clone(), emit_output, tool_registry.clone()),
permission_policy(permission_mode, &tool_registry),
system_prompt,
feature_config,
plugin_registry,
@@ -2064,6 +2047,7 @@ struct AnthropicRuntimeClient {
enable_tools: bool,
emit_output: bool,
allowed_tools: Option<AllowedToolSet>,
tool_registry: GlobalToolRegistry,
}
impl AnthropicRuntimeClient {
@@ -2072,6 +2056,7 @@ impl AnthropicRuntimeClient {
enable_tools: bool,
emit_output: bool,
allowed_tools: Option<AllowedToolSet>,
tool_registry: GlobalToolRegistry,
) -> Result<Self, Box<dyn std::error::Error>> {
Ok(Self {
runtime: tokio::runtime::Runtime::new()?,
@@ -2081,6 +2066,7 @@ impl AnthropicRuntimeClient {
enable_tools,
emit_output,
allowed_tools,
tool_registry,
})
}
}
@@ -2103,16 +2089,9 @@ impl ApiClient for AnthropicRuntimeClient {
max_tokens: max_tokens_for_model(&self.model),
messages: convert_messages(&request.messages),
system: (!request.system_prompt.is_empty()).then(|| request.system_prompt.join("\n\n")),
tools: self.enable_tools.then(|| {
filter_tool_specs(self.allowed_tools.as_ref())
.into_iter()
.map(|spec| ToolDefinition {
name: spec.name.to_string(),
description: Some(spec.description.to_string()),
input_schema: spec.input_schema,
})
.collect()
}),
tools: self
.enable_tools
.then(|| filter_tool_specs(&self.tool_registry, self.allowed_tools.as_ref())),
tool_choice: self.enable_tools.then_some(ToolChoice::Auto),
stream: true,
};
@@ -2703,14 +2682,20 @@ struct CliToolExecutor {
renderer: TerminalRenderer,
emit_output: bool,
allowed_tools: Option<AllowedToolSet>,
tool_registry: GlobalToolRegistry,
}
impl CliToolExecutor {
fn new(allowed_tools: Option<AllowedToolSet>, emit_output: bool) -> Self {
fn new(
allowed_tools: Option<AllowedToolSet>,
emit_output: bool,
tool_registry: GlobalToolRegistry,
) -> Self {
Self {
renderer: TerminalRenderer::new(),
emit_output,
allowed_tools,
tool_registry,
}
}
}
@@ -2728,7 +2713,7 @@ impl ToolExecutor for CliToolExecutor {
}
let value = serde_json::from_str(input)
.map_err(|error| ToolError::new(format!("invalid tool input JSON: {error}")))?;
match execute_tool(tool_name, &value) {
match self.tool_registry.execute(tool_name, &value) {
Ok(output) => {
if self.emit_output {
let markdown = format_tool_result(tool_name, &output, false);
@@ -2751,18 +2736,15 @@ impl ToolExecutor for CliToolExecutor {
}
}
fn permission_policy(mode: PermissionMode) -> PermissionPolicy {
tool_permission_specs()
fn permission_policy(mode: PermissionMode, tool_registry: &GlobalToolRegistry) -> PermissionPolicy {
tool_registry
.permission_specs(None)
.into_iter()
.fold(PermissionPolicy::new(mode), |policy, spec| {
policy.with_tool_requirement(spec.name, spec.required_permission)
.fold(PermissionPolicy::new(mode), |policy, (name, required_permission)| {
policy.with_tool_requirement(name, required_permission)
})
}
fn tool_permission_specs() -> Vec<ToolSpec> {
mvp_tool_specs()
}
fn convert_messages(messages: &[ConversationMessage]) -> Vec<InputMessage> {
messages
.iter()