1use std::path::PathBuf;
41use std::sync::{Arc, OnceLock};
42
43use rpi_ai::types::ThinkingLevel;
44use rpi_extensions::RuntimeActionHost;
45use rpi_harness::agent_harness::{AgentHarness, HarnessRunOutcome, NavigationOutcome};
46use rpi_harness::session::session::Session;
47use tokio::runtime::Handle;
48
49use crate::session::{default_session_dir, open_session_by_id};
50
51pub struct HarnessActionHost {
61 harness: Arc<OnceLock<Arc<AgentHarness>>>,
65 catalog: Vec<rpi_ai::Model>,
68 cwd: PathBuf,
71 #[allow(dead_code)]
72 runtime: Handle,
73}
74
75impl HarnessActionHost {
76 pub fn new_empty(
83 catalog: Vec<rpi_ai::Model>,
84 cwd: PathBuf,
85 runtime: Handle,
86 ) -> (Self, Arc<OnceLock<Arc<AgentHarness>>>) {
87 let harness = Arc::new(OnceLock::new());
88 (
89 Self { harness: Arc::clone(&harness), catalog, cwd, runtime },
90 harness,
91 )
92 }
93
94 pub fn set_harness(cell: &Arc<OnceLock<Arc<AgentHarness>>>, harness: Arc<AgentHarness>) {
99 let _ = cell.set(harness);
102 }
103
104 fn harness(&self) -> Result<&AgentHarness, String> {
108 self.harness
109 .get()
110 .map(|h| h.as_ref())
111 .ok_or_else(|| "runtime action invoked before harness was built".to_string())
112 }
113
114 fn resolve_model(&self, id: &str) -> Option<rpi_ai::Model> {
117 self.catalog
118 .iter()
119 .find(|m| m.id.eq_ignore_ascii_case(id))
120 .cloned()
121 .or_else(|| {
122 self.catalog
123 .iter()
124 .find(|m| m.id.to_ascii_lowercase().contains(&id.to_ascii_lowercase()))
125 .cloned()
126 })
127 }
128}
129
130fn arg_str(args: &serde_json::Value, key: &str) -> Result<String, String> {
132 args.get(key)
133 .and_then(|v| v.as_str())
134 .map(|s| s.to_string())
135 .ok_or_else(|| format!("missing string field `{key}` in action args"))
136}
137
138fn arg_str_opt(args: &serde_json::Value, key: &str) -> Option<String> {
140 args.get(key).and_then(|v| v.as_str()).map(|s| s.to_string())
141}
142
143fn arg_bool(args: &serde_json::Value, key: &str) -> bool {
145 args.get(key).and_then(|v| v.as_bool()).unwrap_or(false)
146}
147
148fn arg_str_array(args: &serde_json::Value, key: &str) -> Result<Vec<String>, String> {
150 args.get(key)
151 .and_then(|v| v.as_array())
152 .map(|arr| {
153 arr.iter()
154 .filter_map(|v| v.as_str().map(|s| s.to_string()))
155 .collect()
156 })
157 .ok_or_else(|| format!("missing string-array field `{key}` in action args"))
158}
159
160fn run_outcome_json(outcome: HarnessRunOutcome) -> serde_json::Value {
164 match outcome {
165 HarnessRunOutcome::Completed { leaf_id, final_entry_id, final_message } => {
166 serde_json::json!({
167 "status": "completed",
168 "leafId": leaf_id,
169 "finalEntryId": final_entry_id,
170 "text": assistant_text(&final_message),
171 })
172 }
173 HarnessRunOutcome::Aborted { leaf_id, final_entry_id, final_message } => {
174 serde_json::json!({
175 "status": "aborted",
176 "leafId": leaf_id,
177 "finalEntryId": final_entry_id,
178 "text": assistant_text(&final_message),
179 })
180 }
181 HarnessRunOutcome::Failed { leaf_id, error, final_entry_id, final_message } => {
182 serde_json::json!({
183 "status": "failed",
184 "leafId": leaf_id,
185 "error": format!("{error:?}"),
186 "finalEntryId": final_entry_id,
187 "text": final_message.map(|m| assistant_text(&m)).unwrap_or_default(),
188 })
189 }
190 HarnessRunOutcome::Suspended { leaf_id, final_entry_id, .. } => {
191 serde_json::json!({
192 "status": "suspended",
193 "leafId": leaf_id,
194 "finalEntryId": final_entry_id,
195 })
196 }
197 }
198}
199
200fn assistant_text(msg: &rpi_ai::types::AssistantMessage) -> String {
202 msg.content
203 .iter()
204 .filter_map(|b| match b {
205 rpi_ai::types::Content::Text(t) => Some(t.text.as_str()),
206 _ => None,
207 })
208 .collect::<Vec<_>>()
209 .join("")
210}
211
212#[async_trait::async_trait]
213impl RuntimeActionHost for HarnessActionHost {
214 async fn send_message(&self, args: serde_json::Value) -> Result<serde_json::Value, String> {
215 let lane = self.harness()?.lane("main");
218 if let Some(text) = arg_str_opt(&args, "text") {
219 let result = lane.prompt_text(&text, Vec::new()).await.map_err(|e| e.to_string())?;
220 return Ok(run_outcome_json(result.outcome));
221 }
222 let msg = args
223 .get("message")
224 .ok_or_else(|| "missing `message` or `text` field".to_string())?;
225 let message: rpi_agent::AgentMessage =
226 serde_json::from_value(msg.clone()).map_err(|e| format!("invalid message: {e}"))?;
227 let result = lane.prompt_message(message).await.map_err(|e| e.to_string())?;
228 Ok(run_outcome_json(result.outcome))
229 }
230
231 async fn send_user_message(&self, args: serde_json::Value) -> Result<serde_json::Value, String> {
232 let text = arg_str(&args, "text")?;
233 let lane = self.harness()?.lane("main");
234 let result = lane.prompt_text(&text, Vec::new()).await.map_err(|e| e.to_string())?;
235 Ok(run_outcome_json(result.outcome))
236 }
237
238 async fn append_entry(&self, args: serde_json::Value) -> Result<serde_json::Value, String> {
239 if let Some(custom_type) = arg_str_opt(&args, "customType") {
243 let data = args.get("data").cloned();
244 let id = self
245 .harness()?
246 .session()
247 .append_custom_entry(&custom_type, data)
248 .await
249 .map_err(|e| e.to_string())?;
250 return Ok(serde_json::json!({ "entryId": id }));
251 }
252 let msg = args
253 .get("message")
254 .ok_or_else(|| "missing `message` or `customType` field".to_string())?;
255 let message: rpi_agent::AgentMessage =
256 serde_json::from_value(msg.clone()).map_err(|e| format!("invalid message: {e}"))?;
257 let id = self
258 .harness()?
259 .session()
260 .append_message(message)
261 .await
262 .map_err(|e| e.to_string())?;
263 Ok(serde_json::json!({ "entryId": id }))
264 }
265
266 async fn set_session_name(&self, args: serde_json::Value) -> Result<serde_json::Value, String> {
267 let name = arg_str(&args, "name")?;
268 self.harness()?
269 .session()
270 .set_name(Some(&name))
271 .await
272 .map_err(|e| e.to_string())?;
273 Ok(serde_json::Value::Null)
274 }
275
276 async fn get_active_tools(&self, _args: serde_json::Value) -> Result<serde_json::Value, String> {
277 let lane = self.harness()?.lane("main");
278 let tools = lane.get_active_tools().await.map_err(|e| e.to_string())?;
279 Ok(serde_json::json!({ "tools": tools }))
280 }
281
282 async fn set_active_tools(&self, args: serde_json::Value) -> Result<serde_json::Value, String> {
283 let tools = arg_str_array(&args, "tools")?;
284 let lane = self.harness()?.lane("main");
285 lane.set_active_tools(tools).await.map_err(|e| e.to_string())?;
286 Ok(serde_json::Value::Null)
287 }
288
289 async fn set_model(&self, args: serde_json::Value) -> Result<serde_json::Value, String> {
290 let id = arg_str(&args, "model")?;
291 let model = self
292 .resolve_model(&id)
293 .ok_or_else(|| format!("model `{id}` not in catalog"))?;
294 let lane = self.harness()?.lane("main");
295 lane.set_model(model.clone()).await.map_err(|e| e.to_string())?;
296 Ok(serde_json::json!({ "model": model.id }))
297 }
298
299 async fn get_thinking_level(&self, _args: serde_json::Value) -> Result<serde_json::Value, String> {
300 let lane = self.harness()?.lane("main");
301 let level = lane.get_thinking_level().await.map_err(|e| e.to_string())?;
302 Ok(serde_json::json!({ "level": level }))
303 }
304
305 async fn set_thinking_level(&self, args: serde_json::Value) -> Result<serde_json::Value, String> {
306 let level_val = args
307 .get("level")
308 .ok_or_else(|| "missing `level` field".to_string())?;
309 let level: ThinkingLevel = if let Some(s) = level_val.as_str() {
310 serde_json::from_value(serde_json::Value::String(s.to_string()))
311 .map_err(|e| format!("invalid thinking level `{s}`: {e}"))?
312 } else {
313 serde_json::from_value(level_val.clone())
314 .map_err(|e| format!("invalid thinking level: {e}"))?
315 };
316 let lane = self.harness()?.lane("main");
317 lane.set_thinking_level(level).await.map_err(|e| e.to_string())?;
318 Ok(serde_json::Value::Null)
319 }
320
321 async fn compact(&self, args: serde_json::Value) -> Result<serde_json::Value, String> {
322 let custom = arg_str_opt(&args, "customInstructions");
323 let lane = self.harness()?.lane("main");
324 let result = lane
325 .compact(custom.as_deref())
326 .await
327 .map_err(|e| e.to_string())?;
328 Ok(serde_json::json!({ "runId": result.run_id, "outcome": format!("{:?}", result.outcome) }))
329 }
330
331 async fn get_system_prompt(&self, _args: serde_json::Value) -> Result<serde_json::Value, String> {
332 let prompt = self.harness()?
333 .get_system_prompt()
334 .await
335 .map_err(|e| e.to_string())?;
336 Ok(serde_json::json!({ "prompt": prompt }))
337 }
338
339 async fn new_session(&self, _args: serde_json::Value) -> Result<serde_json::Value, String> {
340 let cwd_str = self.cwd.to_string_lossy().to_string();
341 let dir = default_session_dir(&self.cwd);
342 std::fs::create_dir_all(&dir)
343 .map_err(|e| format!("create session dir {}: {e}", dir.display()))?;
344 let session = crate::session::create_jsonl_session(&dir, &cwd_str)
345 .await
346 .map_err(|e| format!("create session: {e}"))?;
347 let id = session.storage().metadata().id.clone();
348 self.harness()?
349 .set_session(session)
350 .await
351 .map_err(|e| e.to_string())?;
352 Ok(serde_json::json!({ "sessionId": id }))
353 }
354
355 async fn fork(&self, _args: serde_json::Value) -> Result<serde_json::Value, String> {
356 let cwd_str = self.cwd.to_string_lossy().to_string();
357 let new_session = crate::session::fork_session_storage(self.harness()?, &cwd_str)
358 .await
359 .map_err(|e| format!("fork session: {e}"))?;
360 let id = new_session.storage().metadata().id.clone();
361 self.harness()?
362 .set_session(new_session)
363 .await
364 .map_err(|e| e.to_string())?;
365 Ok(serde_json::json!({ "sessionId": id }))
366 }
367
368 async fn navigate_tree(&self, args: serde_json::Value) -> Result<serde_json::Value, String> {
369 let target_id = arg_str_opt(&args, "targetId");
370 let summarize = arg_bool(&args, "summarize");
371 let custom = arg_str_opt(&args, "customInstructions");
372 let label = arg_str_opt(&args, "label");
373 let lane = self.harness()?.lane("main");
374 let result = lane
375 .navigate_tree(target_id.as_deref(), summarize, custom.as_deref(), label.as_deref())
376 .await
377 .map_err(|e| e.to_string())?;
378 let status = match &result.outcome {
379 NavigationOutcome::Completed { .. } => "completed",
380 NavigationOutcome::Declined { .. } => "declined",
381 NavigationOutcome::Aborted { .. } => "aborted",
382 NavigationOutcome::Failed { .. } => "failed",
383 };
384 Ok(serde_json::json!({ "runId": result.run_id, "status": status }))
385 }
386
387 async fn switch_session(&self, args: serde_json::Value) -> Result<serde_json::Value, String> {
388 let id = arg_str(&args, "id")?;
389 let cwd_str = self.cwd.to_string_lossy().to_string();
390 let new_session: Session =
391 open_session_by_id(&id, &cwd_str).await.map_err(|e| e.to_string())?;
392 let new_id = new_session.storage().metadata().id.clone();
393 self.harness()?
394 .set_session(new_session)
395 .await
396 .map_err(|e| e.to_string())?;
397 Ok(serde_json::json!({ "sessionId": new_id }))
398 }
399
400 async fn reload(&self, _args: serde_json::Value) -> Result<serde_json::Value, String> {
401 Err("reload not configured (no /reload callback on this bridge)".to_string())
405 }
406}