1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
//! File edit tool as a runtime state transition.
//!
//! Swapping the advertised edit surface changes the tool list, which the SDK
//! cannot hot-swap on a live runtime. The change rebuilds the runtime and
//! rebinds the session so it lands on the next turn. Session ID and history
//! survive it.
//!
//! The system prompt stays fixed and format-agnostic. The model learns about the
//! new surface from an appended context notice that carries the live schema.
use super::InteractiveRuntime;
/// Result of a successful mid-session edit-tool switch.
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct EditToolChange {
pub(crate) previous: rho_tools::EditFormat,
pub(crate) display: String,
}
impl InteractiveRuntime {
/// Swaps the advertised file edit tool for the next turn.
///
/// Keeps the system prompt fixed for prompt-cache stability, rebuilds the
/// runtime tool list, and appends a model-facing schema notice when the
/// surface actually changes. Returns [`None`] when this run has no edit tool
/// or the selection is already active.
pub(crate) async fn set_edit_tool(
&mut self,
edit_tool: rho_tools::EditFormat,
max_output_bytes: usize,
) -> anyhow::Result<Option<EditToolChange>> {
if self.runs.is_active() {
anyhow::bail!("edit tool cannot change while a run is active");
}
let Some(previous) = self.tools.set_edit_tool(edit_tool, max_output_bytes) else {
return Ok(None);
};
if let Err(error) = self.rebind_current_session().await {
let _ = self.tools.set_edit_tool(previous, max_output_bytes);
return Err(error);
}
match self.append_edit_tool_switch_notice(previous, edit_tool) {
Ok(display) => Ok(Some(EditToolChange { previous, display })),
Err(error) => {
// Restore so a notice failure does not leave the session
// advertising a tool the model was never told about. Surface
// rollback failures instead of dropping them.
if self
.tools
.set_edit_tool(previous, max_output_bytes)
.is_none()
{
return Err(anyhow::anyhow!(
"{error}; rollback failed: could not restore previous edit tool"
));
}
if let Err(rebind_error) = self.rebind_current_session().await {
return Err(anyhow::anyhow!("{error}; rollback failed: {rebind_error}"));
}
Err(error)
}
}
}
fn append_edit_tool_switch_notice(
&mut self,
previous: rho_tools::EditFormat,
current: rho_tools::EditFormat,
) -> anyhow::Result<String> {
let spec = self
.tools
.specs()
.into_iter()
.find(|spec| spec.name == current.tool_name())
.ok_or_else(|| {
anyhow::anyhow!(
"edit tool `{}` is missing after the mid-session switch",
current.tool_name()
)
})?;
let (model, display) = crate::prompt::edit_tool_switch_context(previous, current, &spec);
self.append_user_context_with_display(model, display.clone())?;
Ok(display)
}
pub(crate) fn tool_specs(&self) -> Vec<rho_sdk::model::ToolSpec> {
self.tools.specs()
}
}