feat: wired together graph execution and agent graph dispatch

This commit is contained in:
2026-05-14 11:10:45 -06:00
parent 84497d3d65
commit 9bc4f8b621
11 changed files with 270 additions and 12 deletions
+91
View File
@@ -317,6 +317,25 @@ impl Functions {
self.declarations.iter().find(|v| v.name == name)
}
/// Narrow the declared tool list to a caller-supplied whitelist.
/// Entries are matched by exact name. The shorthand `mcp:<server>`
/// expands to the three MCP meta-functions Loki registers per
/// server (`mcp_invoke_<server>`, `mcp_search_<server>`,
/// `mcp_describe_<server>`).
pub fn retain_named(&mut self, allowed: &[String]) {
let mut expanded: std::collections::HashSet<String> = std::collections::HashSet::new();
for entry in allowed {
if let Some(server) = entry.strip_prefix("mcp:") {
expanded.insert(format!("{MCP_INVOKE_META_FUNCTION_NAME_PREFIX}_{server}"));
expanded.insert(format!("{MCP_SEARCH_META_FUNCTION_NAME_PREFIX}_{server}"));
expanded.insert(format!("{MCP_DESCRIBE_META_FUNCTION_NAME_PREFIX}_{server}"));
} else {
expanded.insert(entry.clone());
}
}
self.declarations.retain(|d| expanded.contains(&d.name));
}
pub fn contains(&self, name: &str) -> bool {
self.declarations.iter().any(|v| v.name == name)
}
@@ -1730,4 +1749,76 @@ mod tests {
assert_eq!(result.call.name, "my_tool");
assert_eq!(result.output, json!({"result": "ok"}));
}
fn function_with_names(names: &[&str]) -> Functions {
let declarations = names
.iter()
.map(|n| FunctionDeclaration {
name: (*n).to_string(),
description: String::new(),
parameters: JsonSchema::default(),
agent: false,
})
.collect();
Functions { declarations }
}
#[test]
fn retain_named_keeps_only_exact_matches() {
let mut f = function_with_names(&["read_query", "describe_table", "web_search_loki"]);
f.retain_named(&["read_query".to_string()]);
assert!(f.contains("read_query"));
assert!(!f.contains("describe_table"));
assert!(!f.contains("web_search_loki"));
}
#[test]
fn retain_named_with_empty_list_removes_all() {
let mut f = function_with_names(&["a", "b", "c"]);
f.retain_named(&[]);
assert!(!f.contains("a"));
assert!(!f.contains("b"));
assert!(!f.contains("c"));
assert!(f.is_empty());
}
#[test]
fn retain_named_with_unknown_name_drops_everything() {
let mut f = function_with_names(&["a", "b"]);
f.retain_named(&["nonexistent".to_string()]);
assert!(f.is_empty());
}
#[test]
fn retain_named_with_mcp_shorthand_keeps_all_three_meta_functions() {
let mut f = Functions::default();
f.append_mcp_meta_functions(vec!["github".to_string(), "slack".to_string()]);
f.retain_named(&["mcp:github".to_string()]);
assert!(f.contains("mcp_invoke_github"));
assert!(f.contains("mcp_search_github"));
assert!(f.contains("mcp_describe_github"));
assert!(!f.contains("mcp_invoke_slack"));
assert!(!f.contains("mcp_search_slack"));
assert!(!f.contains("mcp_describe_slack"));
}
#[test]
fn retain_named_mixes_exact_names_and_mcp_shorthand() {
let mut f = function_with_names(&["read_query", "describe_table"]);
f.append_mcp_meta_functions(vec!["pubmed".to_string()]);
f.retain_named(&["read_query".to_string(), "mcp:pubmed".to_string()]);
assert!(f.contains("read_query"));
assert!(!f.contains("describe_table"));
assert!(f.contains("mcp_invoke_pubmed"));
assert!(f.contains("mcp_search_pubmed"));
assert!(f.contains("mcp_describe_pubmed"));
}
#[test]
fn retain_named_with_mcp_shorthand_for_unknown_server_drops_other_servers() {
let mut f = Functions::default();
f.append_mcp_meta_functions(vec!["alpha".to_string()]);
f.retain_named(&["mcp:beta".to_string()]);
assert!(!f.contains("mcp_invoke_alpha"));
}
}
+9
View File
@@ -332,6 +332,15 @@ pub fn run_child_agent(
abort_signal: AbortSignal,
) -> Pin<Box<dyn Future<Output = Result<String>> + Send>> {
Box::pin(async move {
if crate::graph::active_agent_graph_name(&child_ctx).is_some() {
return crate::graph::run_active_agent_graph(
&mut child_ctx,
&initial_input.text(),
abort_signal,
)
.await;
}
let mut accumulated_output = String::new();
let mut input = initial_input;
let app = Arc::clone(&child_ctx.app.config);