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