opi_coding_agent/
harness.rs1use std::path::{Path, PathBuf};
7
8use opi_agent::Agent;
9use opi_agent::event::AgentEvent;
10use opi_agent::hooks::AgentHooks;
11use opi_agent::loop_types::{AgentError, AgentLoopConfig};
12use opi_agent::message::AgentMessage;
13use opi_agent::tool::Tool;
14use opi_ai::message::Message;
15use opi_ai::provider::Provider;
16
17use crate::config::OpiConfig;
18use crate::prompt::SystemPromptBuilder;
19use crate::tool::{BashTool, EditTool, GlobTool, GrepTool, ReadTool, WriteTool};
20
21pub struct CodingHarness {
23 agent: Agent,
24 config: OpiConfig,
25 system_prompt: String,
26}
27
28impl CodingHarness {
29 pub fn new(
31 provider: Box<dyn Provider>,
32 model: String,
33 config: OpiConfig,
34 workspace_root: PathBuf,
35 ) -> Self {
36 Self::new_with_hooks(
37 provider,
38 model,
39 config,
40 workspace_root,
41 Box::new(CodingAgentHooks),
42 None,
43 )
44 }
45
46 pub fn new_with_hooks(
48 provider: Box<dyn Provider>,
49 model: String,
50 config: OpiConfig,
51 workspace_root: PathBuf,
52 hooks: Box<dyn AgentHooks>,
53 user_system_prompt: Option<String>,
54 ) -> Self {
55 let tools = Self::build_tools(&workspace_root);
56 let tool_defs: Vec<_> = tools.iter().map(|t| t.definition()).collect();
57 let mut builder = SystemPromptBuilder::new().tools(tool_defs);
58 if let Some(content) = user_system_prompt {
59 builder = builder.user_system(content);
60 }
61 let system_prompt = builder.build();
62
63 let agent_config = AgentLoopConfig {
64 max_turns: config.defaults.max_iterations,
65 ..Default::default()
66 };
67
68 let agent = Agent::new(
69 provider,
70 tools,
71 model,
72 Some(system_prompt.clone()),
73 agent_config,
74 hooks,
75 );
76
77 Self {
78 agent,
79 config,
80 system_prompt,
81 }
82 }
83
84 pub fn add_tool(&mut self, tool: Box<dyn Tool>) {
86 self.agent.add_tool(tool);
87 }
88
89 pub async fn prompt(&mut self, text: &str) -> Result<Vec<AgentMessage>, AgentError> {
91 self.agent.prompt(text).await
92 }
93
94 pub async fn continue_(&mut self, text: &str) -> Result<Vec<AgentMessage>, AgentError> {
96 self.agent.continue_(text).await
97 }
98
99 pub fn subscribe(&mut self, callback: Box<dyn Fn(&AgentEvent) + Send + Sync>) {
101 self.agent.subscribe(callback);
102 }
103
104 pub fn system_prompt(&self) -> &str {
106 &self.system_prompt
107 }
108
109 pub fn config(&self) -> &OpiConfig {
111 &self.config
112 }
113
114 pub fn cancel(&self) {
116 self.agent.abort();
117 }
118
119 pub fn cancel_token(&self) -> tokio_util::sync::CancellationToken {
121 self.agent.cancel_token()
122 }
123
124 fn build_tools(workspace_root: &Path) -> Vec<Box<dyn Tool>> {
125 vec![
126 Box::new(ReadTool::new(workspace_root.to_path_buf())),
127 Box::new(WriteTool::new(workspace_root.to_path_buf())),
128 Box::new(EditTool::new(workspace_root.to_path_buf())),
129 Box::new(BashTool::new(workspace_root.to_path_buf())),
130 Box::new(GlobTool::new(workspace_root.to_path_buf())),
131 Box::new(GrepTool::new(workspace_root.to_path_buf())),
132 ]
133 }
134}
135
136struct CodingAgentHooks;
142
143impl AgentHooks for CodingAgentHooks {
144 fn convert_to_llm(&self, messages: &[AgentMessage]) -> Result<Vec<Message>, AgentError> {
145 let mut result = Vec::new();
146 for msg in messages {
147 if let AgentMessage::Llm(m) = msg {
148 result.push(m.clone());
149 }
150 }
151 Ok(result)
152 }
153}
154
155pub struct InteractiveCodingHooks {
157 pub allow_mutating: bool,
158}
159
160impl InteractiveCodingHooks {
161 pub fn new(allow_mutating: bool) -> Self {
162 Self { allow_mutating }
163 }
164
165 fn is_mutating_tool(name: &str) -> bool {
166 matches!(name, "write" | "edit" | "bash")
167 }
168}
169
170impl AgentHooks for InteractiveCodingHooks {
171 fn convert_to_llm(&self, messages: &[AgentMessage]) -> Result<Vec<Message>, AgentError> {
172 let mut result = Vec::new();
173 for msg in messages {
174 if let AgentMessage::Llm(m) = msg {
175 result.push(m.clone());
176 }
177 }
178 Ok(result)
179 }
180
181 fn before_tool_call(
182 &self,
183 ctx: opi_agent::hooks::BeforeToolCallContext,
184 ) -> std::pin::Pin<
185 Box<dyn std::future::Future<Output = opi_agent::hooks::BeforeToolCallResult> + Send>,
186 > {
187 use opi_agent::hooks::BeforeToolCallResult;
188 let allow = self.allow_mutating || !Self::is_mutating_tool(&ctx.tool_name);
189 Box::pin(async move {
190 if allow {
191 BeforeToolCallResult::Allow
192 } else {
193 BeforeToolCallResult::Deny {
194 reason: format!(
195 "mutating tool '{}' blocked in interactive mode (use --allow-mutating to override)",
196 ctx.tool_name
197 ),
198 }
199 }
200 })
201 }
202}