mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-28 13:32:23 +00:00
Introduces the schema change needed to make agent definitions the single source
of truth for both direct tools and delegation targets:
- `subagents: Vec<SubagentEntry>` — declarative list of agents this agent can
spawn, expanding at build time into synthesised delegate_* tools on the LLM's
function-calling surface. Supports two TOML shapes via `#[serde(untagged)]`:
* Bare string (`"researcher"`) → `SubagentEntry::AgentId`
* Inline table (`{ skills = "*" }`) → `SubagentEntry::Skills(SkillsWildcard)`
The `Skills` variant expands dynamically to one delegate_{toolkit} tool per
connected Composio toolkit at runtime.
- `delegate_name: Option<String>` — optional override for the tool name this
agent is exposed as when another agent lists it in `subagents`. Defaults to
`delegate_{id}` when absent, lets the researcher agent be exposed as
`research`, code_executor as `run_code`, etc.
Schema only — no runtime behavior change yet. Follow-up commits wire the field
into `collect_orchestrator_tools`, the dispatch path, and the debug dump.
TOML placement note: `subagents = [...]` must appear before the `[tools]` table
header in agent TOMLs. Once a table section opens, every subsequent top-level
key is consumed by that table, so placing `subagents` after `[tools]` parses it
as `tools.subagents` and fails deserializing ToolScope. The test doc-comment
records this constraint.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
af454afbb3
commit
8f92155b4d
Generated
+1
-1
@@ -4298,7 +4298,7 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
||||
|
||||
[[package]]
|
||||
name = "openhuman"
|
||||
version = "0.52.5"
|
||||
version = "0.52.6"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"anyhow",
|
||||
|
||||
@@ -65,6 +65,8 @@ pub fn fork_definition() -> AgentDefinition {
|
||||
sandbox_mode: SandboxMode::None,
|
||||
background: false,
|
||||
uses_fork_context: true,
|
||||
subagents: vec![],
|
||||
delegate_name: None,
|
||||
source: DefinitionSource::Builtin,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,12 +115,90 @@ pub struct AgentDefinition {
|
||||
#[serde(default, skip_serializing_if = "is_false")]
|
||||
pub uses_fork_context: bool,
|
||||
|
||||
// ── delegation surface ─────────────────────────────────────────────
|
||||
/// Subagents this agent is allowed to spawn via synthesised
|
||||
/// `delegate_*` tools. Each entry expands at agent-build time into
|
||||
/// one tool the LLM can call in its function-calling schema:
|
||||
///
|
||||
/// * [`SubagentEntry::AgentId`] — one [`ArchetypeDelegationTool`]
|
||||
/// whose name defaults to `delegate_{agent_id}` (or the target
|
||||
/// agent's `delegate_name` override) and whose description is the
|
||||
/// target agent's [`AgentDefinition::when_to_use`].
|
||||
///
|
||||
/// * [`SubagentEntry::Skills`] — one [`SkillDelegationTool`] per
|
||||
/// connected Composio toolkit, each named `delegate_{toolkit}`,
|
||||
/// all routing to the generic `skills_agent` with an appropriate
|
||||
/// `skill_filter` pre-populated.
|
||||
///
|
||||
/// `subagents` is intentionally separate from [`AgentDefinition::tools`]
|
||||
/// so that reading a TOML makes the distinction obvious: `tools` is
|
||||
/// "what I execute directly", `subagents` is "what I can delegate to".
|
||||
///
|
||||
/// [`ArchetypeDelegationTool`]: crate::openhuman::tools::impl::agent::ArchetypeDelegationTool
|
||||
/// [`SkillDelegationTool`]: crate::openhuman::tools::impl::agent::SkillDelegationTool
|
||||
#[serde(default)]
|
||||
pub subagents: Vec<SubagentEntry>,
|
||||
|
||||
/// Optional override for the tool name this agent is exposed as when
|
||||
/// another agent lists it in its [`subagents`]. Defaults to
|
||||
/// `delegate_{id}` when absent. Kept separate from `display_name` so
|
||||
/// the UI display and the LLM tool name can diverge (e.g.
|
||||
/// `display_name = "Researcher"`, `delegate_name = "research"`).
|
||||
#[serde(default)]
|
||||
pub delegate_name: Option<String>,
|
||||
|
||||
// ── source bookkeeping ──────────────────────────────────────────────
|
||||
/// Tracks where the definition was loaded from (Builtin vs. File).
|
||||
#[serde(skip)]
|
||||
pub source: DefinitionSource,
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Subagent delegation entries
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// One entry in [`AgentDefinition::subagents`]. Parses from TOML as either
|
||||
/// a bare string (agent id) or an inline table (`{ skills = "*" }`) thanks
|
||||
/// to `#[serde(untagged)]`.
|
||||
///
|
||||
/// # TOML shapes
|
||||
///
|
||||
/// ```toml
|
||||
/// subagents = [
|
||||
/// "researcher", # AgentId("researcher")
|
||||
/// "code_executor", # AgentId("code_executor")
|
||||
/// { skills = "*" }, # Skills { pattern: "*" }
|
||||
/// ]
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(untagged)]
|
||||
pub enum SubagentEntry {
|
||||
/// Delegate to a specific built-in or custom agent by id.
|
||||
AgentId(String),
|
||||
/// Expand at build time to one `delegate_{toolkit}` tool per
|
||||
/// connected Composio toolkit, each routing to the generic
|
||||
/// `skills_agent` with `skill_filter` pre-set.
|
||||
Skills(SkillsWildcard),
|
||||
}
|
||||
|
||||
/// The `{ skills = "*" }` inline table in a `subagents` list.
|
||||
///
|
||||
/// Today only `"*"` is meaningful (expand to every connected toolkit).
|
||||
/// Future: a `Vec<String>` variant to restrict expansion to specific
|
||||
/// toolkit slugs (e.g. `{ skills = ["gmail", "notion"] }`).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct SkillsWildcard {
|
||||
/// Glob / wildcard pattern. Only `"*"` is currently supported.
|
||||
pub skills: String,
|
||||
}
|
||||
|
||||
impl SkillsWildcard {
|
||||
/// True when this wildcard should expand to every connected toolkit.
|
||||
pub fn matches_all(&self) -> bool {
|
||||
self.skills == "*"
|
||||
}
|
||||
}
|
||||
|
||||
fn is_false(b: &bool) -> bool {
|
||||
!b
|
||||
}
|
||||
@@ -411,6 +489,8 @@ mod tests {
|
||||
sandbox_mode: SandboxMode::None,
|
||||
background: false,
|
||||
uses_fork_context: false,
|
||||
subagents: vec![],
|
||||
delegate_name: None,
|
||||
source: DefinitionSource::Builtin,
|
||||
}
|
||||
}
|
||||
@@ -466,4 +546,103 @@ mod tests {
|
||||
def2.display_name = Some("Beta Specialist".into());
|
||||
assert_eq!(def2.display_name(), "Beta Specialist");
|
||||
}
|
||||
|
||||
// ── subagents parsing ─────────────────────────────────────────────
|
||||
|
||||
/// Parses a minimal TOML document with a `subagents` list containing
|
||||
/// both a bare agent-id string and an inline `{ skills = "*" }` table.
|
||||
/// Ensures the `#[serde(untagged)]` enum routes each shape to the
|
||||
/// correct variant without the TOML needing explicit tags.
|
||||
///
|
||||
/// NOTE: `subagents = [...]` must appear **before** the `[tools]`
|
||||
/// table header in the TOML — once you open a TOML table section,
|
||||
/// every subsequent top-level key is consumed by that table, so
|
||||
/// `subagents` placed after `[tools]` would be parsed as
|
||||
/// `tools.subagents` and fail because `ToolScope` is an enum, not
|
||||
/// a struct with a `subagents` field.
|
||||
#[test]
|
||||
fn subagents_parses_mixed_string_and_table_entries() {
|
||||
let toml_src = r#"
|
||||
id = "orchestrator"
|
||||
when_to_use = "Routes work to the right specialist"
|
||||
temperature = 0.4
|
||||
max_iterations = 15
|
||||
|
||||
subagents = [
|
||||
"researcher",
|
||||
"code_executor",
|
||||
{ skills = "*" },
|
||||
]
|
||||
|
||||
[tools]
|
||||
named = ["query_memory"]
|
||||
"#;
|
||||
let def: AgentDefinition = toml::from_str(toml_src).expect("toml parse");
|
||||
assert_eq!(def.subagents.len(), 3);
|
||||
assert_eq!(
|
||||
def.subagents[0],
|
||||
SubagentEntry::AgentId("researcher".into())
|
||||
);
|
||||
assert_eq!(
|
||||
def.subagents[1],
|
||||
SubagentEntry::AgentId("code_executor".into())
|
||||
);
|
||||
assert_eq!(
|
||||
def.subagents[2],
|
||||
SubagentEntry::Skills(SkillsWildcard { skills: "*".into() })
|
||||
);
|
||||
}
|
||||
|
||||
/// `subagents` is optional — omitting it should yield an empty Vec
|
||||
/// rather than a deserialization error. Most non-delegating agents
|
||||
/// (welcome, archivist, code_executor, etc.) will not list any.
|
||||
#[test]
|
||||
fn subagents_defaults_to_empty_when_omitted() {
|
||||
let toml_src = r#"
|
||||
id = "welcome"
|
||||
when_to_use = "First agent a new user speaks to"
|
||||
temperature = 0.7
|
||||
max_iterations = 6
|
||||
|
||||
[tools]
|
||||
named = ["complete_onboarding", "memory_recall"]
|
||||
"#;
|
||||
let def: AgentDefinition = toml::from_str(toml_src).expect("toml parse");
|
||||
assert!(def.subagents.is_empty());
|
||||
assert!(def.delegate_name.is_none());
|
||||
}
|
||||
|
||||
/// The `delegate_name` field lets an agent expose itself under a
|
||||
/// shorter / more natural tool name than `delegate_{id}`. For example
|
||||
/// the `researcher` agent is exposed as `research` in the
|
||||
/// orchestrator's tool list.
|
||||
#[test]
|
||||
fn delegate_name_overrides_default() {
|
||||
let toml_src = r#"
|
||||
id = "researcher"
|
||||
when_to_use = "Web & docs crawler"
|
||||
delegate_name = "research"
|
||||
temperature = 0.4
|
||||
max_iterations = 8
|
||||
"#;
|
||||
let def: AgentDefinition = toml::from_str(toml_src).expect("toml parse");
|
||||
assert_eq!(def.delegate_name.as_deref(), Some("research"));
|
||||
}
|
||||
|
||||
/// `SkillsWildcard::matches_all` is the predicate the tool builder
|
||||
/// checks before expanding a wildcard into per-toolkit tools. Only
|
||||
/// the literal `"*"` should be accepted today — any other pattern
|
||||
/// (reserved for future specific-toolkit lists) must not match.
|
||||
#[test]
|
||||
fn skills_wildcard_only_star_matches_all() {
|
||||
let star = SkillsWildcard {
|
||||
skills: "*".into(),
|
||||
};
|
||||
assert!(star.matches_all());
|
||||
|
||||
let specific = SkillsWildcard {
|
||||
skills: "gmail".into(),
|
||||
};
|
||||
assert!(!specific.matches_all());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -791,6 +791,8 @@ mod tests {
|
||||
sandbox_mode: super::super::definition::SandboxMode::None,
|
||||
background: false,
|
||||
uses_fork_context: false,
|
||||
subagents: vec![],
|
||||
delegate_name: None,
|
||||
source: super::super::definition::DefinitionSource::Builtin,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -644,6 +644,8 @@ mod tests {
|
||||
sandbox_mode: SandboxMode::None,
|
||||
background: false,
|
||||
uses_fork_context: false,
|
||||
subagents: vec![],
|
||||
delegate_name: None,
|
||||
source: DefinitionSource::Builtin,
|
||||
}
|
||||
}
|
||||
@@ -890,6 +892,8 @@ mod tests {
|
||||
sandbox_mode: SandboxMode::None,
|
||||
background: false,
|
||||
uses_fork_context: false,
|
||||
subagents: vec![],
|
||||
delegate_name: None,
|
||||
source: DefinitionSource::Builtin,
|
||||
};
|
||||
|
||||
@@ -935,6 +939,8 @@ mod tests {
|
||||
sandbox_mode: SandboxMode::None,
|
||||
background: false,
|
||||
uses_fork_context: false,
|
||||
subagents: vec![],
|
||||
delegate_name: None,
|
||||
source: DefinitionSource::Builtin,
|
||||
};
|
||||
|
||||
@@ -988,6 +994,8 @@ mod tests {
|
||||
sandbox_mode: SandboxMode::None,
|
||||
background: false,
|
||||
uses_fork_context: false,
|
||||
subagents: vec![],
|
||||
delegate_name: None,
|
||||
source: DefinitionSource::Builtin,
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user