use std::path::PathBuf;
use rho_providers::model::ReasoningCapabilities;
use rho_providers::reasoning::ReasoningLevel;
use super::{App, RuntimeModelView};
use crate::herdr::HerdrSession;
use crate::permission::PermissionMode;
#[derive(Clone, Debug, Default)]
pub struct ResumeLaunchOptions {
pub config: Option<PathBuf>,
pub agent: Option<String>,
pub definition: Option<std::sync::Arc<crate::agent::AgentDefinition>>,
pub no_system_prompt: bool,
pub no_tools: bool,
pub no_subagents: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) struct ResumeSelection {
pub provider: String,
pub model: String,
pub auth: String,
pub reasoning: ReasoningLevel,
pub permission_mode: PermissionMode,
}
impl ResumeSelection {
fn from_runtime(runtime: &RuntimeModelView) -> Self {
Self {
provider: runtime.provider.clone(),
model: runtime.model.clone(),
auth: runtime.auth.clone(),
reasoning: runtime.reasoning,
permission_mode: runtime.permission_mode,
}
}
fn from_config(config: &crate::config::Config) -> Self {
Self {
provider: config.provider.clone(),
model: config.model.clone(),
auth: config.auth.clone(),
reasoning: config.reasoning,
permission_mode: config.permission_mode,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) struct HerdrResumeKey {
session_id: Option<String>,
selection: ResumeSelection,
config_modified: Option<std::time::SystemTime>,
}
#[derive(Clone, Debug, Default)]
pub(super) struct HerdrSync {
built: Option<(HerdrResumeKey, Option<HerdrSession>)>,
accepted: Option<HerdrResumeKey>,
held: Option<String>,
}
pub(super) const HERDR_SYNC_INTERVAL: std::time::Duration = std::time::Duration::from_secs(5);
#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) enum HerdrSyncStep {
InSync,
Claim(Option<HerdrSession>),
Report(HerdrSession),
Reclaim(Option<HerdrSession>),
}
impl HerdrSync {
fn step(&self, key: &HerdrResumeKey, session: Option<HerdrSession>) -> HerdrSyncStep {
if self.accepted.as_ref() == Some(key) {
return HerdrSyncStep::InSync;
}
let current = session.as_ref().map(|session| session.id.clone());
match (self.held.as_ref(), session) {
(Some(held), session) if current.as_ref() != Some(held) => {
HerdrSyncStep::Reclaim(session)
}
(_, session) if self.accepted.is_none() => HerdrSyncStep::Claim(session),
(_, Some(session)) => HerdrSyncStep::Report(session),
(_, None) => HerdrSyncStep::InSync,
}
}
pub(super) fn note_sent(&mut self, session: Option<&HerdrSession>) {
if self.held.is_none() {
self.held = session.map(|session| session.id.clone());
}
}
pub(super) fn note_released(&mut self) {
self.held = None;
self.accepted = None;
}
}
impl App {
pub(super) fn herdr_resume_key(&self) -> HerdrResumeKey {
let config_modified = self
.info
.services
.config_repository
.configured_path()
.ok()
.and_then(|path| std::fs::metadata(path).ok())
.and_then(|metadata| metadata.modified().ok());
HerdrResumeKey {
session_id: self.info.session.session_id.clone(),
selection: ResumeSelection::from_runtime(&self.info.runtime),
config_modified,
}
}
pub(super) fn herdr_session(&mut self) -> Option<HerdrSession> {
self.current_herdr_session().1
}
pub(super) fn current_herdr_session(&mut self) -> (HerdrResumeKey, Option<HerdrSession>) {
let key = self.herdr_resume_key();
if let Some((built_key, session)) = &self.herdr_sync.built {
if *built_key == key {
return (key, session.clone());
}
}
let session = self.build_herdr_session();
self.herdr_sync.built = Some((key.clone(), session.clone()));
(key, session)
}
pub(super) fn accept_herdr_key(&mut self, key: HerdrResumeKey) {
self.herdr_sync.accepted = Some(key);
}
pub(super) fn confirm_herdr_claim(
&mut self,
key: HerdrResumeKey,
session: Option<&HerdrSession>,
delivery: crate::herdr::HerdrDelivery,
) {
let session_id = session.map(|session| session.id.as_str());
match delivery {
crate::herdr::HerdrDelivery::Accepted
if self.herdr_sync.held.as_deref() == session_id =>
{
self.accept_herdr_key(key);
}
crate::herdr::HerdrDelivery::Accepted | crate::herdr::HerdrDelivery::Failed => {}
}
}
pub(super) fn herdr_sync_step(&mut self) -> (HerdrResumeKey, HerdrSyncStep) {
let (key, session) = self.current_herdr_session();
let step = self.herdr_sync.step(&key, session);
(key, step)
}
fn build_herdr_session(&self) -> Option<HerdrSession> {
let id = self.info.session.session_id.as_deref()?;
let runtime = &self.info.runtime;
let launch = &self.info.session.resume_launch;
let configured = self
.info
.services
.config_repository
.load()
.ok()
.map(|config| {
let config = match &launch.definition {
Some(definition) => crate::app::relaunch_config(definition, &config),
None => config,
};
ResumeSelection::from_config(&config)
});
let reasoning_capabilities =
rho_providers::model::models_dev::current_reasoning_capabilities(
&runtime.provider,
&runtime.model,
);
Some(HerdrSession {
id: id.to_string(),
resume_argv: Some(resume_argv(
launch,
&ResumeSelection::from_runtime(runtime),
configured.as_ref(),
&reasoning_capabilities,
id,
)),
})
}
}
pub(super) fn resume_argv(
launch: &ResumeLaunchOptions,
live: &ResumeSelection,
configured: Option<&ResumeSelection>,
reasoning_capabilities: &ReasoningCapabilities,
session_id: &str,
) -> Vec<String> {
let mut argv = vec!["rho".to_string()];
if let Some(config) = &launch.config {
argv.extend(["--config".into(), config.display().to_string()]);
}
if let Some(agent) = &launch.agent {
argv.extend(["--agent".into(), agent.clone()]);
}
for (enabled, flag) in [
(launch.no_system_prompt, "--no-system-prompt"),
(launch.no_tools, "--no-tools"),
(launch.no_subagents, "--no-subagents"),
] {
if enabled {
argv.push(flag.into());
}
}
let differs = |same: fn(&ResumeSelection, &ResumeSelection) -> bool| {
configured.is_none_or(|configured| !same(live, configured))
};
let pin_model = differs(|live, configured| {
live.provider == configured.provider
&& live.model == configured.model
&& live.auth == configured.auth
});
if pin_model {
argv.extend([
"--model".into(),
rho_providers::provider::model_reference(&live.provider, &live.model),
]);
if live.auth != rho_providers::provider::KEYLESS_AUTH {
argv.extend(["--auth".into(), live.auth.clone()]);
}
}
let reasoning_configurable = match reasoning_capabilities {
ReasoningCapabilities::Levels(_) | ReasoningCapabilities::Unknown => true,
ReasoningCapabilities::NotConfigurable => false,
};
if reasoning_configurable
&& (pin_model || differs(|live, configured| live.reasoning == configured.reasoning))
{
argv.extend(["--reasoning".into(), live.reasoning.to_string()]);
}
if differs(|live, configured| live.permission_mode == configured.permission_mode) {
argv.extend([
"--permission-mode".into(),
live.permission_mode.as_str().into(),
]);
}
argv.extend(["--resume".into(), session_id.into()]);
argv
}
#[cfg(test)]
#[path = "herdr_resume_tests.rs"]
mod tests;