mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-28 13:32:23 +00:00
fix(subagent): lazy-register toolkit actions filtered out of fuzzy top-K (#1162)
This commit is contained in:
@@ -37,6 +37,37 @@ use crate::openhuman::context::prompt::{
|
||||
use crate::openhuman::providers::{ChatMessage, ChatRequest, Provider, ToolCall};
|
||||
use crate::openhuman::tools::{Tool, ToolCategory, ToolSpec};
|
||||
|
||||
/// Lazy resolver that lets `integrations_agent` recover when the model
|
||||
/// calls a Composio action slug that exists in the bound toolkit's full
|
||||
/// catalogue but was filtered out of the up-front fuzzy top-K. On a
|
||||
/// match we build the [`ComposioActionTool`] on demand so the call
|
||||
/// dispatches normally instead of dead-ending in
|
||||
/// `Error: tool '...' is not available`.
|
||||
struct LazyToolkitResolver {
|
||||
client: crate::openhuman::composio::ComposioClient,
|
||||
actions: Vec<crate::openhuman::context::prompt::ConnectedIntegrationTool>,
|
||||
}
|
||||
|
||||
impl LazyToolkitResolver {
|
||||
fn resolve(&self, name: &str) -> Option<Box<dyn Tool>> {
|
||||
let action = self.actions.iter().find(|a| a.name == name)?;
|
||||
Some(Box::new(
|
||||
crate::openhuman::composio::ComposioActionTool::new(
|
||||
self.client.clone(),
|
||||
action.name.clone(),
|
||||
action.description.clone(),
|
||||
action.parameters.clone(),
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
/// Slugs from the bound toolkit, for inclusion in unknown-tool
|
||||
/// errors so the model can self-correct without burning a turn.
|
||||
fn known_slugs(&self) -> Vec<&str> {
|
||||
self.actions.iter().map(|a| a.name.as_str()).collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Run a sub-agent based on its definition and a task prompt.
|
||||
///
|
||||
/// This is the primary entry point for agent delegation. It performs the following:
|
||||
@@ -207,6 +238,7 @@ async fn run_typed_mode(
|
||||
// are stripped from the parent-filtered indices in this path so
|
||||
// the model only sees one way to call each action.
|
||||
let mut dynamic_tools: Vec<Box<dyn Tool>> = Vec::new();
|
||||
let mut lazy_resolver: Option<LazyToolkitResolver> = None;
|
||||
let is_integrations_agent_with_toolkit =
|
||||
definition.id == "integrations_agent" && toolkit_filter.is_some();
|
||||
|
||||
@@ -346,6 +378,15 @@ async fn run_typed_mode(
|
||||
action_count = dynamic_tools.len(),
|
||||
"[subagent_runner:typed] dynamically registered per-action composio tools"
|
||||
);
|
||||
// Stash the full catalogue so the inner loop can lazily
|
||||
// register actions that the fuzzy top-K dropped — the
|
||||
// model often picks the right slug anyway and the
|
||||
// existing fuzzy filter exists only to keep schemas out
|
||||
// of the system prompt, not to gate execution.
|
||||
lazy_resolver = Some(LazyToolkitResolver {
|
||||
client: client.clone(),
|
||||
actions: integration.tools.clone(),
|
||||
});
|
||||
} else {
|
||||
tracing::warn!(
|
||||
agent_id = %definition.id,
|
||||
@@ -614,9 +655,10 @@ async fn run_typed_mode(
|
||||
parent.provider.as_ref(),
|
||||
&mut history,
|
||||
&parent.all_tools,
|
||||
&dynamic_tools,
|
||||
dynamic_tools,
|
||||
&filtered_specs,
|
||||
&allowed_names,
|
||||
allowed_names,
|
||||
lazy_resolver,
|
||||
&model,
|
||||
temperature,
|
||||
definition.max_iterations,
|
||||
@@ -718,9 +760,10 @@ async fn run_fork_mode(
|
||||
parent.provider.as_ref(),
|
||||
&mut history,
|
||||
&parent.all_tools,
|
||||
&fork_extra_tools,
|
||||
fork_extra_tools,
|
||||
fork.tool_specs.as_slice(),
|
||||
&allowed_names,
|
||||
allowed_names,
|
||||
None,
|
||||
&model,
|
||||
temperature,
|
||||
max_iterations,
|
||||
@@ -769,9 +812,10 @@ async fn run_inner_loop(
|
||||
provider: &dyn Provider,
|
||||
history: &mut Vec<ChatMessage>,
|
||||
parent_tools: &[Box<dyn Tool>],
|
||||
extra_tools: &[Box<dyn Tool>],
|
||||
mut extra_tools: Vec<Box<dyn Tool>>,
|
||||
tool_specs: &[ToolSpec],
|
||||
allowed_names: &HashSet<String>,
|
||||
mut allowed_names: HashSet<String>,
|
||||
lazy_resolver: Option<LazyToolkitResolver>,
|
||||
model: &str,
|
||||
temperature: f64,
|
||||
max_iterations: usize,
|
||||
@@ -1056,6 +1100,29 @@ async fn run_inner_loop(
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
// Lazy registration: if the call is for an unknown tool but
|
||||
// matches a real action slug in the bound toolkit's full
|
||||
// catalogue, build the [`ComposioActionTool`] on the spot and
|
||||
// admit it to the allowlist for this and subsequent turns.
|
||||
// The fuzzy top-K filter exists to keep schemas out of the
|
||||
// system prompt, not to gate execution — when the model
|
||||
// names the slug correctly we should just dispatch.
|
||||
if !allowed_names.contains(&call.name) {
|
||||
if let Some(resolver) = lazy_resolver.as_ref() {
|
||||
if let Some(tool) = resolver.resolve(&call.name) {
|
||||
tracing::info!(
|
||||
task_id = %task_id,
|
||||
agent_id = %agent_id,
|
||||
tool = %call.name,
|
||||
"[subagent_runner] lazily registered toolkit action outside fuzzy top-K"
|
||||
);
|
||||
allowed_names.insert(tool.name().to_string());
|
||||
extra_tools.push(tool);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let result_text = if !allowed_names.contains(&call.name) {
|
||||
tracing::warn!(
|
||||
task_id = %task_id,
|
||||
@@ -1063,9 +1130,17 @@ async fn run_inner_loop(
|
||||
tool = %call.name,
|
||||
"[subagent_runner] tool not in allowlist for this sub-agent"
|
||||
);
|
||||
let mut available: Vec<&str> = allowed_names.iter().map(|s| s.as_str()).collect();
|
||||
if let Some(resolver) = lazy_resolver.as_ref() {
|
||||
available.extend(resolver.known_slugs());
|
||||
}
|
||||
available.sort_unstable();
|
||||
available.dedup();
|
||||
format!(
|
||||
"Error: tool '{}' is not available to the {} sub-agent",
|
||||
call.name, agent_id
|
||||
"Error: tool '{}' is not available to the {} sub-agent. Available tools: {}",
|
||||
call.name,
|
||||
agent_id,
|
||||
available.join(", ")
|
||||
)
|
||||
} else if let Some(tool) = extra_tools
|
||||
.iter()
|
||||
|
||||
Reference in New Issue
Block a user