Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
a122aa6
feat(config): apply edit tool and advisor changes mid-session
matthewyjiang Aug 10, 2026
7d028e7
feat(config): add auto edit tool preference by provider
matthewyjiang Aug 10, 2026
df7e296
feat(config): prefer str_replace for xai in auto edit tool
matthewyjiang Aug 10, 2026
ccd343f
docs(config): explain auto edit tool first-party harness defaults
matthewyjiang Aug 10, 2026
fe0120c
fix(tui): speed up tool expand without rebuilding history suffix
matthewyjiang Aug 10, 2026
8ed0e8f
refactor(config): consolidate mid-session edit tool apply path
matthewyjiang Aug 10, 2026
ce6c51b
fix(config): harden mid-session advisor and edit-tool transitions
matthewyjiang Aug 10, 2026
701b345
test(tui): cover Auto edit tool handoff on provider switch
matthewyjiang Aug 10, 2026
062df1c
fix(ci): satisfy rustfmt and clippy on edit-tool branch
matthewyjiang Aug 10, 2026
fcb25aa
fix(runtime): roll back history when advisor notice persistence fails
matthewyjiang Aug 10, 2026
b6c57e5
fix(tui): harden edit-tool switch, history cache, and syntax restart …
matthewyjiang Aug 10, 2026
4a244e1
build(sdk): cut 1.18.0 for Session::replace_history
matthewyjiang Aug 10, 2026
b657548
fix(ci): refresh downstream fixture lockfile for rho-sdk 1.18.0
matthewyjiang Aug 10, 2026
f4ba0a0
fix(tui): align edit-tool rollback notices across histories
matthewyjiang Aug 10, 2026
81cf87d
fix(test): make edit-tool save-failure coverage OS-independent
matthewyjiang Aug 10, 2026
c660b97
fix(test): scope config save-failure injection to the repository
matthewyjiang Aug 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/rho/src/app/automation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -504,7 +504,7 @@ async fn run_session_with_output(
workspace,
workspace_policy: AppPolicy::for_mode(startup.config.permission_mode),
approval_session: startup.approval_session.clone(),
system_prompt: system_prompt.for_advisor_mode(tool_set.advisor_registered()),
system_prompt: system_prompt.get(),
reasoning: sdk_options.runtime.reasoning,
service_tier: sdk_options.runtime.service_tier,
compaction,
Expand Down
89 changes: 89 additions & 0 deletions crates/rho/src/app/interactive_runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,61 @@ impl InteractiveRuntime {
Ok(())
}

/// Swaps the advertised file edit tool for the next turn.
///
/// Keeps the system prompt fixed for prompt-cache stability. Callers should
/// append [`Self::notify_edit_tool_switch`] after a successful save so the
/// model sees the new surface even when older prompt text mentions the
/// previous tool. Returns the previous format when the tool list changed.
pub(crate) async fn set_edit_tool(
&mut self,
edit_tool: rho_tools::EditFormat,
max_output_bytes: usize,
) -> anyhow::Result<Option<rho_tools::EditFormat>> {
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);
};
match self.rebind_current_session().await {
Ok(()) => Ok(Some(previous)),
Err(error) => {
let _ = self.tools.set_edit_tool(previous, max_output_bytes);
Err(error)
}
}
}

/// Appends a model-facing notice that the edit tool changed mid-session.
///
/// Includes the live tool description and input schema so the model can use
/// the new surface without a system-prompt rewrite.
pub(crate) fn notify_edit_tool_switch(
&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) = edit_tool_switch_prompts(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()
}

pub(crate) async fn shutdown(&mut self) {
if self.runs.is_active() {
debug_assert_eq!(
Expand Down Expand Up @@ -886,6 +941,40 @@ impl InteractiveRuntime {
}
}

/// Model and display text for a mid-session edit-tool switch.
///
/// The system prompt stays fixed for prompt caching. This notice carries the
/// new tool contract so the model stops using the previous surface.
fn edit_tool_switch_prompts(
previous: rho_tools::EditFormat,
current: rho_tools::EditFormat,
spec: &rho_sdk::model::ToolSpec,
) -> (String, String) {
let schema = serde_json::to_string_pretty(&spec.input_schema).unwrap_or_else(|_| "{}".into());
let model = format!(
"[edit tool switched]\n\n\
The file edit tool changed mid-session. Do not call `{previous_name}` anymore.\n\
Use `{current_name}` for edits to existing UTF-8 files from now on.\n\
Prefer `write` only to create or fully rewrite a file.\n\
Any earlier system-prompt guidance about `{previous_name}` is superseded by this notice and the live tool list.\n\n\
Previous tool: `{previous_name}` ({previous_label})\n\
Current tool: `{current_name}` ({current_label})\n\n\
Tool schema for `{current_name}`:\n\
description:\n\
{description}\n\n\
input_schema:\n\
{schema}\n",
previous_name = previous.tool_name(),
current_name = current.tool_name(),
previous_label = previous.as_str(),
current_label = current.as_str(),
description = spec.description,
schema = schema,
);
let display = format!("edit tool switched to {}", current.as_str());
(model, display)
}

#[cfg(test)]
#[path = "interactive_runtime_tests.rs"]
mod tests;
60 changes: 44 additions & 16 deletions crates/rho/src/app/interactive_runtime_advisor.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
//! Advisor mode as a runtime state transition.
//!
//! Advisor mode changes the tool list and the system prompt, neither of which
//! the SDK can swap on a live runtime. Turning it on or off therefore rebuilds
//! the runtime and rebinds the session, the same move a permission-mode change
//! makes, so the change lands on the next turn and the session ID and history
//! survive it.
//! Advisor mode changes the advertised tool list, which the SDK cannot swap on
//! a live runtime. Turning it on or off therefore rebuilds the runtime and
//! rebinds the session so the change lands on the next turn. The session ID and
//! history survive it.
//!
//! The system prompt stays fixed for prompt-cache stability. The model learns
//! about the tool list change from an appended context notice (with the tool
//! schema when enabling) rather than a rewritten system prompt.

use std::sync::Arc;

Expand All @@ -20,31 +23,33 @@ use super::super::{
use super::InteractiveRuntime;

impl InteractiveRuntime {
/// The system prompt for the tools this run currently offers.
/// Fixed system prompt for this session.
///
/// Mid-session tool list changes keep this value stable and tell the model
/// through appended context instead.
pub(super) fn active_system_prompt(&self) -> SystemPrompt {
self.system_prompt
.for_advisor_mode(self.tools.advisor_registered())
self.system_prompt.get()
}

/// Applies an advisor mode or advisor model change to the next turn.
///
/// `model` is the advisor model to use, or `None` when advisor mode is off
/// or has no model yet; those are the same thing to the executor. The live
/// tool reads the new model at once. Registering or removing the `advisor`
/// tool also changes the tool list and the system prompt, so it needs the
/// same runtime rebuild as a permission-mode change; the session ID and
/// history survive it.
/// tool rebuilds the runtime without rewriting the system prompt, then
/// appends a context notice. Returns display text for a transcript notice
/// when the tool list changed.
pub(crate) async fn set_advisor(
&mut self,
model: Option<InternalAgentModelConfig>,
) -> anyhow::Result<()> {
) -> anyhow::Result<Option<String>> {
let Some(store) = self.tools.advisor().cloned() else {
return Ok(());
return Ok(None);
};
let registered = model.is_some();
if registered == self.tools.advisor_registered() {
store.set_model(model);
return Ok(());
return Ok(None);
}
if self.runs.is_active() {
anyhow::bail!("advisor mode cannot change while a run is active");
Expand All @@ -56,7 +61,8 @@ impl InteractiveRuntime {
match self.rebind_current_session().await {
Ok(()) => {
store.set_model(model);
Ok(())
let display = self.append_advisor_switch_notice(registered)?;
Ok(Some(display))
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}
Err(error) => {
self.tools.set_advisor_registered(!registered);
Expand All @@ -65,10 +71,32 @@ impl InteractiveRuntime {
}
}

fn append_advisor_switch_notice(&mut self, enabled: bool) -> anyhow::Result<String> {
let (model, display) = if enabled {
let spec = self
.tools
.specs()
.into_iter()
.find(|spec| spec.name == crate::tools::advisor::TOOL_NAME)
.ok_or_else(|| {
anyhow::anyhow!("advisor tool is missing after it was registered")
})?;
crate::prompt::advisor_enabled_context(&spec)
} else {
crate::prompt::advisor_disabled_context()
};
self.append_user_context_with_display(model, display.clone())?;
Ok(display)
}

/// Rebuilds the SDK runtime around the current tools and prompt, then
/// rebinds the live session onto it. The live runtime is replaced only
/// after the replacement is ready, so a failure leaves the session intact.
async fn rebind_current_session(&mut self) -> anyhow::Result<()> {
///
/// Callers that change the advertised tool list should keep the system
/// prompt fixed for prompt-cache stability and tell the model about the
/// change with an appended context message instead.
pub(super) async fn rebind_current_session(&mut self) -> anyhow::Result<()> {
let snapshot = self.sessions.session().snapshot();
let replacement_runtime = build_runtime(RuntimeBuildOptions {
provider: Arc::clone(self.provider.provider()),
Expand Down
2 changes: 1 addition & 1 deletion crates/rho/src/app/interactive_runtime_startup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ pub(super) async fn initialize(
approval_session: approval_handler
.clone()
.map(rho_sdk::ApprovalSession::from_shared),
system_prompt: system_prompt.for_advisor_mode(tools.advisor_registered()),
system_prompt: system_prompt.get(),
reasoning: sdk_options.runtime.reasoning,
service_tier: sdk_options.runtime.service_tier,
compaction: compaction.clone(),
Expand Down
Loading
Loading