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 {
90 harness: Arc::clone(&harness),
91 catalog,
92 cwd,
93 runtime,
94 },
95 harness,
96 )
97 }
98
99 pub fn set_harness(cell: &Arc<OnceLock<Arc<AgentHarness>>>, harness: Arc<AgentHarness>) {
104 let _ = cell.set(harness);
107 }
108
109 fn harness(&self) -> Result<&AgentHarness, String> {
113 self.harness
114 .get()
115 .map(|h| h.as_ref())
116 .ok_or_else(|| "runtime action invoked before harness was built".to_string())
117 }
118
119 fn resolve_model(&self, id: &str) -> Option<rpi_ai::Model> {
122 self.catalog
123 .iter()
124 .find(|m| m.id.eq_ignore_ascii_case(id))
125 .cloned()
126 .or_else(|| {
127 self.catalog
128 .iter()
129 .find(|m| m.id.to_ascii_lowercase().contains(&id.to_ascii_lowercase()))
130 .cloned()
131 })
132 }
133}
134
135fn arg_str(args: &serde_json::Value, key: &str) -> Result<String, String> {
137 args.get(key)
138 .and_then(|v| v.as_str())
139 .map(|s| s.to_string())
140 .ok_or_else(|| format!("missing string field `{key}` in action args"))
141}
142
143fn arg_str_opt(args: &serde_json::Value, key: &str) -> Option<String> {
145 args.get(key)
146 .and_then(|v| v.as_str())
147 .map(|s| s.to_string())
148}
149
150fn arg_bool(args: &serde_json::Value, key: &str) -> bool {
152 args.get(key).and_then(|v| v.as_bool()).unwrap_or(false)
153}
154
155fn arg_str_array(args: &serde_json::Value, key: &str) -> Result<Vec<String>, String> {
157 args.get(key)
158 .and_then(|v| v.as_array())
159 .map(|arr| {
160 arr.iter()
161 .filter_map(|v| v.as_str().map(|s| s.to_string()))
162 .collect()
163 })
164 .ok_or_else(|| format!("missing string-array field `{key}` in action args"))
165}
166
167fn run_outcome_json(outcome: HarnessRunOutcome) -> serde_json::Value {
171 match outcome {
172 HarnessRunOutcome::Completed {
173 leaf_id,
174 final_entry_id,
175 final_message,
176 } => {
177 serde_json::json!({
178 "status": "completed",
179 "leafId": leaf_id,
180 "finalEntryId": final_entry_id,
181 "text": assistant_text(&final_message),
182 })
183 }
184 HarnessRunOutcome::Aborted {
185 leaf_id,
186 final_entry_id,
187 final_message,
188 } => {
189 serde_json::json!({
190 "status": "aborted",
191 "leafId": leaf_id,
192 "finalEntryId": final_entry_id,
193 "text": assistant_text(&final_message),
194 })
195 }
196 HarnessRunOutcome::Failed {
197 leaf_id,
198 error,
199 final_entry_id,
200 final_message,
201 } => {
202 serde_json::json!({
203 "status": "failed",
204 "leafId": leaf_id,
205 "error": format!("{error:?}"),
206 "finalEntryId": final_entry_id,
207 "text": final_message.map(|m| assistant_text(&m)).unwrap_or_default(),
208 })
209 }
210 HarnessRunOutcome::Suspended {
211 leaf_id,
212 final_entry_id,
213 ..
214 } => {
215 serde_json::json!({
216 "status": "suspended",
217 "leafId": leaf_id,
218 "finalEntryId": final_entry_id,
219 })
220 }
221 }
222}
223
224fn assistant_text(msg: &rpi_ai::types::AssistantMessage) -> String {
226 msg.content
227 .iter()
228 .filter_map(|b| match b {
229 rpi_ai::types::Content::Text(t) => Some(t.text.as_str()),
230 _ => None,
231 })
232 .collect::<Vec<_>>()
233 .join("")
234}
235
236#[async_trait::async_trait]
237impl RuntimeActionHost for HarnessActionHost {
238 async fn send_message(&self, args: serde_json::Value) -> Result<serde_json::Value, String> {
239 let lane = self.harness()?.lane("main");
242 if let Some(text) = arg_str_opt(&args, "text") {
243 let result = lane
244 .prompt_text(&text, Vec::new())
245 .await
246 .map_err(|e| e.to_string())?;
247 return Ok(run_outcome_json(result.outcome));
248 }
249 let msg = args
250 .get("message")
251 .ok_or_else(|| "missing `message` or `text` field".to_string())?;
252 let message: rpi_agent::AgentMessage =
253 serde_json::from_value(msg.clone()).map_err(|e| format!("invalid message: {e}"))?;
254 let result = lane
255 .prompt_message(message)
256 .await
257 .map_err(|e| e.to_string())?;
258 Ok(run_outcome_json(result.outcome))
259 }
260
261 async fn send_user_message(
262 &self,
263 args: serde_json::Value,
264 ) -> Result<serde_json::Value, String> {
265 let text = arg_str(&args, "text")?;
266 let lane = self.harness()?.lane("main");
267 let result = lane
268 .prompt_text(&text, Vec::new())
269 .await
270 .map_err(|e| e.to_string())?;
271 Ok(run_outcome_json(result.outcome))
272 }
273
274 async fn append_entry(&self, args: serde_json::Value) -> Result<serde_json::Value, String> {
275 if let Some(custom_type) = arg_str_opt(&args, "customType") {
279 let data = args.get("data").cloned();
280 let id = self
281 .harness()?
282 .session()
283 .append_custom_entry(&custom_type, data)
284 .await
285 .map_err(|e| e.to_string())?;
286 return Ok(serde_json::json!({ "entryId": id }));
287 }
288 let msg = args
289 .get("message")
290 .ok_or_else(|| "missing `message` or `customType` field".to_string())?;
291 let message: rpi_agent::AgentMessage =
292 serde_json::from_value(msg.clone()).map_err(|e| format!("invalid message: {e}"))?;
293 let id = self
294 .harness()?
295 .session()
296 .append_message(message)
297 .await
298 .map_err(|e| e.to_string())?;
299 Ok(serde_json::json!({ "entryId": id }))
300 }
301
302 async fn set_session_name(&self, args: serde_json::Value) -> Result<serde_json::Value, String> {
303 let name = arg_str(&args, "name")?;
304 self.harness()?
305 .session()
306 .set_name(Some(&name))
307 .await
308 .map_err(|e| e.to_string())?;
309 Ok(serde_json::Value::Null)
310 }
311
312 async fn get_active_tools(
313 &self,
314 _args: serde_json::Value,
315 ) -> Result<serde_json::Value, String> {
316 let lane = self.harness()?.lane("main");
317 let tools = lane.get_active_tools().await.map_err(|e| e.to_string())?;
318 Ok(serde_json::json!({ "tools": tools }))
319 }
320
321 async fn set_active_tools(&self, args: serde_json::Value) -> Result<serde_json::Value, String> {
322 let tools = arg_str_array(&args, "tools")?;
323 let lane = self.harness()?.lane("main");
324 lane.set_active_tools(tools)
325 .await
326 .map_err(|e| e.to_string())?;
327 Ok(serde_json::Value::Null)
328 }
329
330 async fn set_model(&self, args: serde_json::Value) -> Result<serde_json::Value, String> {
331 let id = arg_str(&args, "model")?;
332 let model = self
333 .resolve_model(&id)
334 .ok_or_else(|| format!("model `{id}` not in catalog"))?;
335 let lane = self.harness()?.lane("main");
336 lane.set_model(model.clone())
337 .await
338 .map_err(|e| e.to_string())?;
339 Ok(serde_json::json!({ "model": model.id }))
340 }
341
342 async fn get_thinking_level(
343 &self,
344 _args: serde_json::Value,
345 ) -> Result<serde_json::Value, String> {
346 let lane = self.harness()?.lane("main");
347 let level = lane.get_thinking_level().await.map_err(|e| e.to_string())?;
348 Ok(serde_json::json!({ "level": level }))
349 }
350
351 async fn set_thinking_level(
352 &self,
353 args: serde_json::Value,
354 ) -> Result<serde_json::Value, String> {
355 let level_val = args
356 .get("level")
357 .ok_or_else(|| "missing `level` field".to_string())?;
358 let level: ThinkingLevel = if let Some(s) = level_val.as_str() {
359 serde_json::from_value(serde_json::Value::String(s.to_string()))
360 .map_err(|e| format!("invalid thinking level `{s}`: {e}"))?
361 } else {
362 serde_json::from_value(level_val.clone())
363 .map_err(|e| format!("invalid thinking level: {e}"))?
364 };
365 let lane = self.harness()?.lane("main");
366 lane.set_thinking_level(level)
367 .await
368 .map_err(|e| e.to_string())?;
369 Ok(serde_json::Value::Null)
370 }
371
372 async fn compact(&self, args: serde_json::Value) -> Result<serde_json::Value, String> {
373 let custom = arg_str_opt(&args, "customInstructions");
374 let lane = self.harness()?.lane("main");
375 let result = lane
376 .compact(custom.as_deref())
377 .await
378 .map_err(|e| e.to_string())?;
379 Ok(
380 serde_json::json!({ "runId": result.run_id, "outcome": format!("{:?}", result.outcome) }),
381 )
382 }
383
384 async fn get_system_prompt(
385 &self,
386 _args: serde_json::Value,
387 ) -> Result<serde_json::Value, String> {
388 let prompt = self
389 .harness()?
390 .get_system_prompt()
391 .await
392 .map_err(|e| e.to_string())?;
393 Ok(serde_json::json!({ "prompt": prompt }))
394 }
395
396 async fn new_session(&self, _args: serde_json::Value) -> Result<serde_json::Value, String> {
397 let cwd_str = self.cwd.to_string_lossy().to_string();
398 let dir = default_session_dir(&self.cwd);
399 std::fs::create_dir_all(&dir)
400 .map_err(|e| format!("create session dir {}: {e}", dir.display()))?;
401 let session = crate::session::create_jsonl_session(&dir, &cwd_str)
402 .await
403 .map_err(|e| format!("create session: {e}"))?;
404 let id = session.storage().metadata().id.clone();
405 self.harness()?
406 .set_session(session)
407 .await
408 .map_err(|e| e.to_string())?;
409 Ok(serde_json::json!({ "sessionId": id }))
410 }
411
412 async fn fork(&self, _args: serde_json::Value) -> Result<serde_json::Value, String> {
413 let cwd_str = self.cwd.to_string_lossy().to_string();
414 let new_session = crate::session::fork_session_storage(self.harness()?, &cwd_str)
415 .await
416 .map_err(|e| format!("fork session: {e}"))?;
417 let id = new_session.storage().metadata().id.clone();
418 self.harness()?
419 .set_session(new_session)
420 .await
421 .map_err(|e| e.to_string())?;
422 Ok(serde_json::json!({ "sessionId": id }))
423 }
424
425 async fn navigate_tree(&self, args: serde_json::Value) -> Result<serde_json::Value, String> {
426 let target_id = arg_str_opt(&args, "targetId");
427 let summarize = arg_bool(&args, "summarize");
428 let custom = arg_str_opt(&args, "customInstructions");
429 let label = arg_str_opt(&args, "label");
430 let lane = self.harness()?.lane("main");
431 let result = lane
432 .navigate_tree(
433 target_id.as_deref(),
434 summarize,
435 custom.as_deref(),
436 label.as_deref(),
437 )
438 .await
439 .map_err(|e| e.to_string())?;
440 let status = match &result.outcome {
441 NavigationOutcome::Completed { .. } => "completed",
442 NavigationOutcome::Declined { .. } => "declined",
443 NavigationOutcome::Aborted { .. } => "aborted",
444 NavigationOutcome::Failed { .. } => "failed",
445 };
446 Ok(serde_json::json!({ "runId": result.run_id, "status": status }))
447 }
448
449 async fn switch_session(&self, args: serde_json::Value) -> Result<serde_json::Value, String> {
450 let id = arg_str(&args, "id")?;
451 let cwd_str = self.cwd.to_string_lossy().to_string();
452 let new_session: Session = open_session_by_id(&id, &cwd_str)
453 .await
454 .map_err(|e| e.to_string())?;
455 let new_id = new_session.storage().metadata().id.clone();
456 self.harness()?
457 .set_session(new_session)
458 .await
459 .map_err(|e| e.to_string())?;
460 Ok(serde_json::json!({ "sessionId": new_id }))
461 }
462
463 async fn reload(&self, _args: serde_json::Value) -> Result<serde_json::Value, String> {
464 Err("reload not configured (no /reload callback on this bridge)".to_string())
468 }
469}