From fce08140bf652d0fc6f143c4c200ad70f9f7156c Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Mon, 18 May 2026 12:55:17 -0600 Subject: [PATCH] fix: error when users try to start a session on a graph agent --- src/config/request_context.rs | 95 ++++++++++++++++++++++++++++++++++- src/repl/mod.rs | 19 ++++--- 2 files changed, 107 insertions(+), 7 deletions(-) diff --git a/src/config/request_context.rs b/src/config/request_context.rs index 4986041..4313e87 100644 --- a/src/config/request_context.rs +++ b/src/config/request_context.rs @@ -37,6 +37,7 @@ use std::fs::{File, OpenOptions, read_dir, read_to_string, remove_dir_all, remov use std::io::Write; use std::path::{Path, PathBuf}; use std::sync::Arc; +use crate::graph; pub struct AutoContinueConfig { pub enabled: bool, @@ -1007,6 +1008,7 @@ impl RequestContext { }) .map(|v| v.name.to_string()) .collect(); + if let Some(agent) = &self.agent { declaration_names.extend( agent @@ -1031,6 +1033,7 @@ impl RequestContext { if item.is_empty() { continue; } + if let Some(values) = app.mapping_tools.get(item) { tool_names.extend( values @@ -1089,9 +1092,11 @@ impl RequestContext { && !v.name.starts_with(MCP_DESCRIBE_META_FUNCTION_NAME_PREFIX) }) .collect(); + if let Some(ref tool_names) = role_filter { agent_functions.retain(|v| tool_names.contains(&v.name)); } + let tool_names: HashSet = agent_functions .iter() .filter_map(|v| { @@ -1157,6 +1162,7 @@ impl RequestContext { if item.is_empty() { continue; } + let item_invoke_name = format!("{}_{item}", MCP_INVOKE_META_FUNCTION_NAME_PREFIX); let item_search_name = @@ -1226,9 +1232,11 @@ impl RequestContext { || v.name.starts_with(MCP_DESCRIBE_META_FUNCTION_NAME_PREFIX) }) .collect(); + if let Some(ref server_names) = role_filter { agent_functions.retain(|v| server_names.contains(&v.name)); } + let tool_names: HashSet = agent_functions .iter() .filter_map(|v| { @@ -2168,6 +2176,14 @@ impl RequestContext { ) .await?; + let is_graph_agent = graph::agent_has_graph(agent_name); + if is_graph_agent && session_name.is_some() { + bail!( + "Graph-based agent '{agent_name}' does not support sessions. \ + The graph manages its own state; re-run without a session." + ); + } + let mcp_servers = if app.mcp_server_support { (!agent.mcp_server_names().is_empty()).then(|| agent.mcp_server_names().join(",")) } else { @@ -2189,8 +2205,10 @@ impl RequestContext { ); } + // Graph agents manage their own state; never engage a session, + // not even an inherited app-level `agent_session` default. let session_name = session_name.map(|v| v.to_string()).or_else(|| { - if self.macro_flag { + if self.macro_flag || is_graph_agent { None } else { agent.agent_session().map(|v| v.to_string()) @@ -3583,4 +3601,79 @@ mod tests { "Agent should not be set when session check fails" ); } + + #[test] + #[serial] + fn use_agent_errors_when_graph_agent_given_explicit_session() { + let _guard = TestConfigDirGuard::new(); + let mut ctx = create_test_ctx(); + + let app = ctx.app.config.clone(); + let agent_name = format!( + "test_graph_agent_{}", + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos() + ); + let agent_dir = paths::agent_data_dir(&agent_name); + create_dir_all(&agent_dir).unwrap(); + write( + agent_dir.join("graph.yaml"), + format!( + "name: {agent_name}\nversion: \"1.0\"\nstart: done\nnodes:\n done:\n type: end\n output: ok\n" + ), + ) + .unwrap(); + + let abort = utils::create_abort_signal(); + let result = run_async(ctx.use_agent(&app, &agent_name, Some("test_session"), abort)); + + assert!(result.is_err()); + assert!( + result + .unwrap_err() + .to_string() + .contains("does not support sessions") + ); + assert!( + ctx.agent.is_none(), + "Agent should not be set when the graph-agent session guard fails" + ); + } + + #[test] + #[serial] + fn use_agent_skips_inherited_session_for_graph_agent() { + let _guard = TestConfigDirGuard::new(); + let mut ctx = create_test_ctx(); + ctx.update_app_config(|app| app.agent_session = Some("inherited".to_string())); + + let app = ctx.app.config.clone(); + let agent_name = format!( + "test_graph_agent_{}", + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos() + ); + let agent_dir = paths::agent_data_dir(&agent_name); + create_dir_all(&agent_dir).unwrap(); + write( + agent_dir.join("graph.yaml"), + format!( + "name: {agent_name}\nversion: \"1.0\"\nstart: done\nnodes:\n done:\n type: end\n output: ok\n" + ), + ) + .unwrap(); + + let abort = utils::create_abort_signal(); + run_async(ctx.use_agent(&app, &agent_name, None, abort)).unwrap(); + + assert!(ctx.agent.is_some(), "Graph agent should load successfully"); + assert!( + ctx.session.is_none(), + "Graph agent must not engage a session, not even an inherited default" + ); + } } diff --git a/src/repl/mod.rs b/src/repl/mod.rs index 32ee455..bbe020d 100644 --- a/src/repl/mod.rs +++ b/src/repl/mod.rs @@ -17,7 +17,7 @@ use crate::utils::{ AbortSignal, abortable_run_with_spinner, create_abort_signal, dimmed_text, set_text, temp_file, }; -use crate::resolve_oauth_client; +use crate::{graph, resolve_oauth_client}; use anyhow::{Context, Result, bail}; use crossterm::cursor::SetCursorStyle; use fancy_regex::Regex; @@ -32,6 +32,7 @@ use reedline::{ use reedline::{MenuBuilder, Signal}; use std::sync::LazyLock; use std::{env, process, sync::Arc}; +use log::warn; const MENU_NAME: &str = "completion_menu"; @@ -497,6 +498,12 @@ pub async fn run_repl_command( ), }, ".session" => { + if let Some(name) = graph::active_agent_graph_name(ctx) { + bail!( + "Graph-based agent '{name}' does not support sessions. \ + The graph manages its own state." + ); + } let app = Arc::clone(&ctx.app.config); ctx.use_session(app.as_ref(), args, abort_signal.clone()) .await?; @@ -508,7 +515,7 @@ pub async fn run_repl_command( }; eprintln!("\n📢 {}", color.italic().paint("Autonaming the session."),); if let Err(err) = ctx.autoname_session(app.as_ref()).await { - log::warn!("Failed to autonaming the session: {err}"); + warn!("Failed to autonaming the session: {err}"); } if let Some(session) = ctx.session.as_mut() { session.set_autonaming(false); @@ -860,10 +867,10 @@ async fn ask( let app = Arc::clone(&ctx.app.config); - if crate::graph::active_agent_graph_name(ctx).is_some() { + if graph::active_agent_graph_name(ctx).is_some() { ctx.before_chat_completion(&input)?; let output = - crate::graph::run_active_agent_graph(ctx, &input.text(), abort_signal.clone()).await?; + graph::run_active_agent_graph(ctx, &input.text(), abort_signal.clone()).await?; app.print_markdown(&output)?; ctx.after_chat_completion(app.as_ref(), &input, &output, &[])?; return Ok(()); @@ -937,7 +944,7 @@ async fn ask( }; eprintln!("\n📢 {}", color.italic().paint("Autonaming the session."),); if let Err(err) = ctx.autoname_session(app.as_ref()).await { - log::warn!("Failed to autonaming the session: {err}"); + warn!("Failed to autonaming the session: {err}"); } if let Some(session) = ctx.session.as_mut() { session.set_autonaming(false); @@ -964,7 +971,7 @@ async fn ask( eprintln!("\n📢 {}", color.italic().paint("Compressing the session."),); if let Err(err) = ctx.compress_session().await { - log::warn!("Failed to compress the session: {err}"); + warn!("Failed to compress the session: {err}"); } if let Some(session) = ctx.session.as_mut() { session.set_compressing(false);