#![allow(dead_code)]
use std::collections::HashMap;
use std::path::Path;
use std::time::Duration;
use crate::commands::run::PhaseType;
use crate::contracts::{ClaudePermissionMode, Model, Runner};
use crate::plugins::registry::PluginRegistry;
use crate::runner::{
OutputHandler, OutputStream, ResolvedRunnerCliOptions, RunnerError, RunnerOutput,
};
use super::builtin_plugins::BuiltInRunnerPlugin;
use super::plugin_trait::{ResumeContext, RunContext, RunnerMetadata, RunnerPlugin};
use super::process::run_with_streaming_json;
use super::response::ResponseParserRegistry;
pub struct PluginExecutor {
built_ins: HashMap<Runner, BuiltInRunnerPlugin>,
response_parsers: ResponseParserRegistry,
}
impl Default for PluginExecutor {
fn default() -> Self {
Self::new()
}
}
impl PluginExecutor {
pub fn new() -> Self {
let mut built_ins = HashMap::new();
built_ins.insert(Runner::Codex, BuiltInRunnerPlugin::Codex);
built_ins.insert(Runner::Opencode, BuiltInRunnerPlugin::Opencode);
built_ins.insert(Runner::Gemini, BuiltInRunnerPlugin::Gemini);
built_ins.insert(Runner::Claude, BuiltInRunnerPlugin::Claude);
built_ins.insert(Runner::Kimi, BuiltInRunnerPlugin::Kimi);
built_ins.insert(Runner::Pi, BuiltInRunnerPlugin::Pi);
built_ins.insert(Runner::Cursor, BuiltInRunnerPlugin::Cursor);
Self {
built_ins,
response_parsers: ResponseParserRegistry::new(),
}
}
pub fn metadata(&self, runner: &Runner) -> RunnerMetadata {
match runner {
Runner::Plugin(plugin_id) => RunnerMetadata {
id: plugin_id.clone(),
name: format!("Plugin: {}", plugin_id),
supports_resume: true, default_model: None,
},
_ => self
.built_ins
.get(runner)
.map(|p| p.metadata())
.unwrap_or_else(|| RunnerMetadata {
id: runner.id().to_string(),
name: runner.id().to_string(),
supports_resume: false,
default_model: None,
}),
}
}
#[allow(clippy::too_many_arguments)]
pub fn run(
&self,
runner: Runner,
work_dir: &Path,
bin: &str,
model: Model,
reasoning_effort: Option<crate::contracts::ReasoningEffort>,
runner_cli: ResolvedRunnerCliOptions,
prompt: &str,
timeout: Option<Duration>,
permission_mode: Option<ClaudePermissionMode>,
output_handler: Option<OutputHandler>,
output_stream: OutputStream,
phase_type: PhaseType,
session_id: Option<String>,
plugins: Option<&PluginRegistry>,
) -> Result<RunnerOutput, RunnerError> {
match &runner {
Runner::Plugin(plugin_id) => self.run_external_plugin(
plugin_id,
work_dir,
bin,
runner_cli,
model,
prompt,
timeout,
output_handler.clone(),
output_stream,
session_id,
plugins,
),
_ => {
let plugin = self.built_ins.get(&runner).ok_or_else(|| {
RunnerError::Other(anyhow::anyhow!(
"No plugin implementation for runner: {}",
runner.id()
))
})?;
let ctx = RunContext {
work_dir,
bin,
model,
prompt,
timeout,
output_handler: output_handler.clone(),
output_stream,
runner_cli,
reasoning_effort,
permission_mode,
phase_type: Some(phase_type),
session_id: session_id.clone(),
};
let (cmd, payload, _guards) = plugin.build_run_command(ctx)?;
let mut output = run_with_streaming_json(
cmd,
payload.as_deref(),
runner.clone(),
bin,
timeout,
output_handler.clone(),
output_stream,
)?;
if self.requires_managed_session_id(&runner) {
output.session_id = session_id;
}
Ok(output)
}
}
}
#[allow(clippy::too_many_arguments)]
#[allow(clippy::type_complexity)]
pub fn resume(
&self,
runner: Runner,
work_dir: &Path,
bin: &str,
model: Model,
reasoning_effort: Option<crate::contracts::ReasoningEffort>,
runner_cli: ResolvedRunnerCliOptions,
session_id: &str,
message: &str,
timeout: Option<Duration>,
permission_mode: Option<ClaudePermissionMode>,
output_handler: Option<OutputHandler>,
output_stream: OutputStream,
phase_type: PhaseType,
plugins: Option<&PluginRegistry>,
) -> Result<RunnerOutput, RunnerError> {
match &runner {
Runner::Plugin(plugin_id) => self.resume_external_plugin(
plugin_id,
work_dir,
bin,
runner_cli,
model,
session_id,
message,
timeout,
output_handler.clone(),
output_stream,
plugins,
),
_ => {
let plugin = self.built_ins.get(&runner).ok_or_else(|| {
RunnerError::Other(anyhow::anyhow!(
"No plugin implementation for runner: {}",
runner.id()
))
})?;
let ctx = ResumeContext {
work_dir,
bin,
model,
session_id,
message,
timeout,
output_handler: output_handler.clone(),
output_stream,
runner_cli,
reasoning_effort,
permission_mode,
phase_type: Some(phase_type),
};
let (cmd, payload, _guards) = plugin.build_resume_command(ctx)?;
run_with_streaming_json(
cmd,
payload.as_deref(),
runner,
bin,
timeout,
output_handler.clone(),
output_stream,
)
}
}
}
pub fn requires_managed_session_id(&self, runner: &Runner) -> bool {
match runner {
Runner::Plugin(_) => false, _ => self
.built_ins
.get(runner)
.map(|p| p.requires_managed_session_id())
.unwrap_or(false),
}
}
pub fn extract_final_response(&self, runner: &Runner, stdout: &str) -> Option<String> {
self.response_parsers.extract_final_response(runner, stdout)
}
#[allow(clippy::too_many_arguments)]
fn run_external_plugin(
&self,
plugin_id: &str,
work_dir: &Path,
bin: &str,
runner_cli: ResolvedRunnerCliOptions,
model: Model,
prompt: &str,
timeout: Option<Duration>,
output_handler: Option<OutputHandler>,
output_stream: OutputStream,
session_id: Option<String>,
plugins: Option<&PluginRegistry>,
) -> Result<RunnerOutput, RunnerError> {
let plugin_config_json = plugins
.and_then(|p| p.plugin_config_blob(plugin_id))
.map(|v| super::serialize_plugin_env_json(plugin_id, bin, "plugin config", &v))
.transpose()?;
super::plugin::run_plugin_runner(
work_dir,
bin,
plugin_id,
runner_cli,
model,
prompt,
timeout,
output_handler,
output_stream,
session_id.as_deref(),
plugin_config_json,
)
}
#[allow(clippy::too_many_arguments)]
fn resume_external_plugin(
&self,
plugin_id: &str,
work_dir: &Path,
bin: &str,
runner_cli: ResolvedRunnerCliOptions,
model: Model,
session_id: &str,
message: &str,
timeout: Option<Duration>,
output_handler: Option<OutputHandler>,
output_stream: OutputStream,
plugins: Option<&PluginRegistry>,
) -> Result<RunnerOutput, RunnerError> {
let plugin_config_json = plugins
.and_then(|p| p.plugin_config_blob(plugin_id))
.map(|v| super::serialize_plugin_env_json(plugin_id, bin, "plugin config", &v))
.transpose()?;
super::plugin::run_plugin_runner_resume(
work_dir,
bin,
plugin_id,
runner_cli,
model,
session_id,
message,
timeout,
output_handler,
output_stream,
plugin_config_json,
)
}
}
#[allow(clippy::too_many_arguments)]
pub fn run_builtin_prompt(
plugin: BuiltInRunnerPlugin,
work_dir: &Path,
bin: &str,
runner_cli: ResolvedRunnerCliOptions,
model: Model,
prompt: &str,
timeout: Option<Duration>,
output_handler: Option<OutputHandler>,
output_stream: OutputStream,
) -> Result<RunnerOutput, RunnerError> {
let executor = PluginExecutor::new();
let runner = match plugin {
BuiltInRunnerPlugin::Pi => Runner::Pi,
_ => {
return Err(RunnerError::Other(anyhow::anyhow!(
"run_builtin_prompt only supports Pi; use executor.run() for other runners"
)));
}
};
executor.run(
runner,
work_dir,
bin,
model,
None, runner_cli,
prompt,
timeout,
None, output_handler,
output_stream,
crate::commands::run::PhaseType::Planning,
None, None, )
}
#[allow(clippy::too_many_arguments)]
pub fn run_builtin_resume(
plugin: BuiltInRunnerPlugin,
work_dir: &Path,
bin: &str,
runner_cli: ResolvedRunnerCliOptions,
model: Model,
session_id: &str,
message: &str,
timeout: Option<Duration>,
output_handler: Option<OutputHandler>,
output_stream: OutputStream,
) -> Result<RunnerOutput, RunnerError> {
let executor = PluginExecutor::new();
let runner = match plugin {
BuiltInRunnerPlugin::Pi => Runner::Pi,
_ => {
return Err(RunnerError::Other(anyhow::anyhow!(
"run_builtin_resume only supports Pi; use executor.resume() for other runners"
)));
}
};
executor.resume(
runner,
work_dir,
bin,
model,
None, runner_cli,
session_id,
message,
timeout,
None, output_handler,
output_stream,
crate::commands::run::PhaseType::Planning,
None, )
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn plugin_executor_creates_with_all_built_ins() {
let executor = PluginExecutor::new();
for runner in [
Runner::Codex,
Runner::Opencode,
Runner::Gemini,
Runner::Claude,
Runner::Kimi,
Runner::Pi,
Runner::Cursor,
] {
let metadata = executor.metadata(&runner);
assert!(!metadata.id.is_empty());
}
}
#[test]
fn plugin_executor_kimi_requires_managed_session() {
let executor = PluginExecutor::new();
assert!(executor.requires_managed_session_id(&Runner::Kimi));
assert!(!executor.requires_managed_session_id(&Runner::Codex));
}
#[test]
fn plugin_executor_external_plugin_metadata() {
let executor = PluginExecutor::new();
let runner = Runner::Plugin("test.plugin".to_string());
let metadata = executor.metadata(&runner);
assert_eq!(metadata.id, "test.plugin");
assert!(metadata.supports_resume); }
}