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