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