1mod error;
16mod execution;
17mod invoke;
18mod model;
19mod plugin_dispatch;
20mod settings;
21
22#[cfg(test)]
23mod tests;
24
25pub use error::RunnerError;
26pub(crate) use error::{
27 RetryableReason, RunnerFailureClass, runner_execution_error, runner_execution_error_with_source,
28};
29
30pub(crate) use execution::{
31 BuiltInRunnerPlugin, ResolvedRunnerCliOptions, RunnerPlugin, ctrlc_state,
32};
33
34pub(crate) use model::{
35 default_model_for_runner, parse_model, parse_reasoning_effort, resolve_model_for_runner,
36 validate_model_for_runner,
37};
38
39pub(crate) use settings::{
40 AgentSettings, PhaseSettingsMatrix, ResolvedPhaseSettings, resolve_agent_settings,
41 resolve_phase_settings_matrix,
42};
43
44#[allow(unused)]
46const _: () = {
47 fn _use_resolved_phase_settings(_: ResolvedPhaseSettings) {}
48};
49
50use crate::commands::run::PhaseType;
51use crate::contracts::{ClaudePermissionMode, Model, ReasoningEffort, Runner};
52use crate::plugins::registry::PluginRegistry;
53use crate::redaction::redact_text;
54use anyhow::Result;
55use std::fmt;
56use std::path::Path;
57use std::process::ExitStatus;
58use std::sync::Arc;
59use std::time::Duration;
60
61pub type OutputHandler = Arc<Box<dyn Fn(&str) + Send + Sync>>;
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum OutputStream {
68 Terminal,
70 HandlerOnly,
72}
73
74impl OutputStream {
75 pub fn streams_to_terminal(self) -> bool {
77 matches!(self, OutputStream::Terminal)
78 }
79}
80
81pub(crate) struct RunnerOutput {
82 pub status: ExitStatus,
83 pub stdout: String,
84 pub stderr: String,
85 pub session_id: Option<String>,
86}
87
88impl fmt::Display for RunnerOutput {
89 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90 write!(
91 f,
92 "status: {}\nstdout: {}\nstderr: {}",
93 self.status,
94 redact_text(&self.stdout),
95 redact_text(&self.stderr)
96 )
97 }
98}
99
100impl fmt::Debug for RunnerOutput {
101 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102 f.debug_struct("RunnerOutput")
103 .field("status", &self.status)
104 .field("stdout", &redact_text(&self.stdout))
105 .field("stderr", &redact_text(&self.stderr))
106 .field("session_id", &self.session_id.as_deref())
107 .finish()
108 }
109}
110
111#[derive(Clone, Copy)]
112pub struct RunnerBinaries<'a> {
113 pub codex: &'a str,
114 pub opencode: &'a str,
115 pub gemini: &'a str,
116 pub claude: &'a str,
117 pub cursor: &'a str,
118 pub kimi: &'a str,
119 pub pi: &'a str,
120}
121
122pub(crate) fn resolve_binaries(agent: &crate::contracts::AgentConfig) -> RunnerBinaries<'_> {
123 let codex = agent.codex_bin.as_deref().unwrap_or("codex");
124 let opencode = agent.opencode_bin.as_deref().unwrap_or("opencode");
125 let gemini = agent.gemini_bin.as_deref().unwrap_or("gemini");
126 let claude = agent.claude_bin.as_deref().unwrap_or("claude");
127 let cursor = agent.cursor_bin.as_deref().unwrap_or("agent");
128 let kimi = agent.kimi_bin.as_deref().unwrap_or("kimi");
129 let pi = agent.pi_bin.as_deref().unwrap_or("pi");
130 RunnerBinaries {
131 codex,
132 opencode,
133 gemini,
134 claude,
135 cursor,
136 kimi,
137 pi,
138 }
139}
140
141pub(crate) fn extract_final_assistant_response(stdout: &str) -> Option<String> {
142 execution::extract_final_assistant_response(stdout)
143}
144
145fn runner_label(runner: Runner) -> String {
146 runner.id().to_string()
147}
148
149#[allow(clippy::too_many_arguments)]
150pub(crate) fn run_prompt(
151 runner: Runner,
152 work_dir: &Path,
153 bins: RunnerBinaries<'_>,
154 model: Model,
155 reasoning_effort: Option<ReasoningEffort>,
156 runner_cli: execution::ResolvedRunnerCliOptions,
157 prompt: &str,
158 timeout: Option<Duration>,
159 permission_mode: Option<ClaudePermissionMode>,
160 output_handler: Option<OutputHandler>,
161 output_stream: OutputStream,
162 phase_type: PhaseType,
163 session_id: Option<String>,
164 plugins: Option<&PluginRegistry>,
165) -> Result<RunnerOutput, RunnerError> {
166 invoke::dispatch(
167 invoke::RunnerDispatchContext {
168 runner,
169 work_dir,
170 bins,
171 model,
172 reasoning_effort,
173 runner_cli,
174 timeout,
175 permission_mode,
176 output_handler,
177 output_stream,
178 phase_type,
179 plugins,
180 },
181 invoke::RunnerInvocation::Prompt { prompt, session_id },
182 )
183}
184
185#[allow(clippy::too_many_arguments)]
186pub(crate) fn resume_session(
187 runner: Runner,
188 work_dir: &Path,
189 bins: RunnerBinaries<'_>,
190 model: Model,
191 reasoning_effort: Option<ReasoningEffort>,
192 runner_cli: execution::ResolvedRunnerCliOptions,
193 session_id: &str,
194 message: &str,
195 permission_mode: Option<ClaudePermissionMode>,
196 timeout: Option<Duration>,
197 output_handler: Option<OutputHandler>,
198 output_stream: OutputStream,
199 phase_type: PhaseType,
200 plugins: Option<&PluginRegistry>,
201) -> Result<RunnerOutput, RunnerError> {
202 invoke::dispatch(
203 invoke::RunnerDispatchContext {
204 runner,
205 work_dir,
206 bins,
207 model,
208 reasoning_effort,
209 runner_cli,
210 timeout,
211 permission_mode,
212 output_handler,
213 output_stream,
214 phase_type,
215 plugins,
216 },
217 invoke::RunnerInvocation::Resume {
218 session_id,
219 message,
220 },
221 )
222}