From 5669830510008130010b2e82ec0fca27cd06e45b Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Thu, 14 May 2026 13:24:34 -0600 Subject: [PATCH] refactor: migrated llm nodes to use Roles to simplify instructions handling and to function like inline roles --- src/graph/llm.rs | 19 ++++++++++++------- src/graph/types.rs | 37 +++++++++++++++++++++++++------------ 2 files changed, 37 insertions(+), 19 deletions(-) diff --git a/src/graph/llm.rs b/src/graph/llm.rs index 179ec71..8f7be3f 100644 --- a/src/graph/llm.rs +++ b/src/graph/llm.rs @@ -13,7 +13,7 @@ use super::state::StateManager; use super::types::LlmNode; use crate::config::RequestContext; use crate::utils::dimmed_text; -use anyhow::{bail, Context, Result}; +use anyhow::{Context, Result, bail}; use serde_json::Value; const OUTPUT_KEY: &str = "output"; @@ -51,9 +51,14 @@ async fn run( state_manager: &mut StateManager, _parent_ctx: &mut RequestContext, ) -> Result { - let _instructions = state_manager - .interpolate(&node.instructions) - .context("Failed to interpolate llm node instructions")?; + let _instructions: Option = match &node.instructions { + Some(s) => Some( + state_manager + .interpolate(s) + .context("Failed to interpolate llm node instructions")?, + ), + None => None, + }; let _prompt = state_manager .interpolate(&node.prompt) .context("Failed to interpolate llm node prompt")?; @@ -119,7 +124,7 @@ fn apply_state_updates_with_output(node: &LlmNode, state_manager: &mut StateMana fn describe_tools_filter(tools: Option<&[String]>) -> String { match tools { - None => "".into(), + None => "".into(), Some(t) if t.is_empty() => "".into(), Some(t) => t.join(","), } @@ -142,7 +147,7 @@ mod tests { fn node_with(updates: Option>) -> LlmNode { LlmNode { - instructions: "sys".into(), + instructions: Some("sys".into()), prompt: "user".into(), tools: None, model: None, @@ -208,7 +213,7 @@ mod tests { #[test] fn describe_tools_filter_renders_each_case() { - assert_eq!(describe_tools_filter(None), ""); + assert_eq!(describe_tools_filter(None), ""); assert_eq!(describe_tools_filter(Some(&[])), ""); let tools = vec!["a".to_string(), "b".to_string()]; assert_eq!(describe_tools_filter(Some(&tools)), "a,b"); diff --git a/src/graph/types.rs b/src/graph/types.rs index c46f5c3..3e973b7 100644 --- a/src/graph/types.rs +++ b/src/graph/types.rs @@ -215,13 +215,21 @@ pub struct InputNode { /// LLM's response on success, or to an error description on failure. #[derive(Debug, Clone, Deserialize, Serialize)] pub struct LlmNode { - pub instructions: String, - + /// User-turn prompt. Templated against state. REQUIRED. pub prompt: String, - /// Whitelist of tool names. Each entry is either an exact function - /// name or the shorthand `mcp:` (expands to the three MCP - /// meta-functions for that server). Unset = no tools. + /// Optional system prompt. When set, the LLM call uses an inline + /// Role with `instructions` as `Role.prompt`. Templated against + /// state. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub instructions: Option, + + /// Whitelist narrowing the active agent's tool universe. + /// Each entry is either an exact function name (`global_tools` + /// entry or `tools.{sh,py,ts}` subcommand) or the shorthand + /// `mcp:` (where `` must be in the agent's + /// `mcp_servers`). Unset = inherit agent's full set; `[]` = no + /// tools. #[serde(default, skip_serializing_if = "Option::is_none")] pub tools: Option>, @@ -636,7 +644,7 @@ next: review NodeType::Llm(l) => l, _ => panic!("expected Llm variant"), }; - assert_eq!(llm.instructions, "You are a classifier."); + assert_eq!(llm.instructions.as_deref(), Some("You are a classifier.")); assert_eq!(llm.prompt, "Classify: {{input_text}}"); let tools = llm.tools.unwrap(); assert_eq!(tools, vec!["read_query", "mcp:pubmed-search"]); @@ -668,7 +676,7 @@ next: done NodeType::Llm(l) => l, _ => panic!("expected Llm variant"), }; - assert_eq!(llm.instructions, "System."); + assert_eq!(llm.instructions.as_deref(), Some("System.")); assert_eq!(llm.prompt, "User."); assert!(llm.tools.is_none()); assert!(llm.model.is_none()); @@ -678,14 +686,19 @@ next: done } #[test] - fn llm_node_missing_instructions_fails() { + fn llm_node_with_just_prompt_succeeds() { let yaml = r#" -id: bad +id: pure type: llm -prompt: "User only — no system prompt." +prompt: "User-only — no system prompt." "#; - let result: std::result::Result = serde_yaml::from_str(yaml); - assert!(result.is_err()); + let node: Node = serde_yaml::from_str(yaml).unwrap(); + let llm = match node.node_type { + NodeType::Llm(l) => l, + _ => panic!("expected Llm variant"), + }; + assert!(llm.instructions.is_none()); + assert_eq!(llm.prompt, "User-only — no system prompt."); } #[test]