mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
feat(agent-registry): add configurable agent registry (#2993)
This commit is contained in:
@@ -124,6 +124,9 @@ fn build_registered_controllers() -> Vec<RegisteredController> {
|
||||
controllers.extend(crate::openhuman::webview_apis::all_webview_apis_registered_controllers());
|
||||
// Agent definition and prompt inspection
|
||||
controllers.extend(crate::openhuman::agent::all_agent_registered_controllers());
|
||||
// User-facing agent registry: defaults, enablement, custom agents, tool policy.
|
||||
controllers
|
||||
.extend(crate::openhuman::agent_registry::all_agent_registry_registered_controllers());
|
||||
// Local procedural operating experience for agent self-learning
|
||||
controllers
|
||||
.extend(crate::openhuman::agent_experience::all_agent_experience_registered_controllers());
|
||||
@@ -298,6 +301,7 @@ fn build_declared_controller_schemas() -> Vec<ControllerSchema> {
|
||||
schemas.extend(crate::openhuman::mcp_registry::all_mcp_registry_controller_schemas());
|
||||
schemas.extend(crate::openhuman::webview_apis::all_webview_apis_controller_schemas());
|
||||
schemas.extend(crate::openhuman::agent::all_agent_controller_schemas());
|
||||
schemas.extend(crate::openhuman::agent_registry::all_agent_registry_controller_schemas());
|
||||
schemas.extend(crate::openhuman::agent_experience::all_agent_experience_controller_schemas());
|
||||
schemas.extend(crate::openhuman::health::all_health_controller_schemas());
|
||||
schemas.extend(crate::openhuman::doctor::all_doctor_controller_schemas());
|
||||
|
||||
@@ -28,7 +28,7 @@ In-process pub/sub plus typed request/response. Owns the global `EventBus` singl
|
||||
## Called by
|
||||
|
||||
- ~33 sites across the workspace. Hot consumers:
|
||||
- `src/openhuman/agent/bus.rs`, `agent/triage/{events,evaluator,escalation}.rs`, `agent/tools/{dispatch,spawn_subagent}.rs` — agent + sub-agent events.
|
||||
- `src/openhuman/agent/bus.rs`, `agent/triage/{events,evaluator,escalation}.rs`, `agent_registry/tools/{dispatch,spawn_subagent}.rs` — agent + sub-agent events.
|
||||
- `src/openhuman/memory/conversations/bus.rs` — conversation persistence subscriber.
|
||||
- `src/openhuman/channels/bus.rs` — `ChannelInboundSubscriber`.
|
||||
- `src/openhuman/cron/{bus,scheduler}.rs` — `CronDeliverySubscriber` + `CronJobTriggered` emission.
|
||||
|
||||
@@ -13,7 +13,7 @@ Multi-agent orchestration domain. Owns the LLM tool-calling loop, sub-agent disp
|
||||
- `pub trait ToolDispatcher` / `pub struct ParsedToolCall` / `pub struct ToolExecutionResult` — `dispatcher.rs:14-50` — pluggable tool-call format (XML / JSON / P-Format).
|
||||
- `pub mod triage` (`run_triage`, `apply_decision`, `TriggerEnvelope`, `TriageDecision`, `TriageAction`) — `triage/mod.rs:34-45` — classify external triggers, escalate to sub-agents.
|
||||
- `pub mod prompts::SystemPromptBuilder` — `prompts/` — system-prompt section composer.
|
||||
- `pub mod agents` — `agents/mod.rs` — built-in archetypes (orchestrator, planner, researcher, code_executor, summarizer, archivist, trigger_triage, trigger_reactor, etc.).
|
||||
- Built-in archetypes live in `src/openhuman/agent_registry/agents/`; this module stays focused on harness/runtime behavior.
|
||||
- RPC `agent.chat`, `agent.chat_simple`, `agent.server_status`, `agent.list_definitions`, `agent.get_definition`, `agent.reload_definitions`, `agent.triage_evaluate` — `schemas.rs:17-158`.
|
||||
|
||||
## Calls into
|
||||
@@ -34,7 +34,7 @@ Multi-agent orchestration domain. Owns the LLM tool-calling loop, sub-agent disp
|
||||
- `src/openhuman/composio/bus.rs` — Composio trigger envelopes go through `agent::triage`.
|
||||
- `src/openhuman/notifications/rpc.rs` — surfaces agent runs to the UI.
|
||||
- `src/openhuman/learning/{reflection,tool_tracker,user_profile}.rs` — read transcripts + tool outcomes.
|
||||
- `src/openhuman/agent/tools/{dispatch,spawn_subagent}.rs` — `spawn_subagent` tool delegates here.
|
||||
- `src/openhuman/agent_registry/tools/{dispatch,spawn_subagent}.rs` — `spawn_subagent` tool delegates here.
|
||||
- `src/core/all.rs` — controller registry wires `all_agent_registered_controllers`.
|
||||
|
||||
## Tests
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Built-in [`AgentDefinition`]s.
|
||||
//!
|
||||
//! The authoritative list of built-in agents lives in
|
||||
//! [`crate::openhuman::agent::agents`] — each agent is a subfolder
|
||||
//! [`crate::openhuman::agent_registry::agents`] — each agent is a subfolder
|
||||
//! containing `agent.toml` + `prompt.md`. This module is a thin
|
||||
//! wrapper that loads that set.
|
||||
//!
|
||||
@@ -17,8 +17,8 @@ use super::definition::DefinitionSource;
|
||||
/// Panics if the baked-in built-in TOML fails to parse. `include_str!`
|
||||
/// guarantees at compile time that each file exists, but the actual
|
||||
/// TOML parse happens at runtime; the unit tests in
|
||||
/// [`crate::openhuman::agent::agents`] verify in CI that every entry in
|
||||
/// [`crate::openhuman::agent::agents::BUILTINS`] still parses cleanly.
|
||||
/// [`crate::openhuman::agent_registry::agents`] verify in CI that every entry in
|
||||
/// [`crate::openhuman::agent_registry::agents::BUILTINS`] still parses cleanly.
|
||||
///
|
||||
/// In `#[cfg(test)]` builds the list additionally contains
|
||||
/// [`test_inherit_echo_def`] — a sub-agent with `ModelSpec::Inherit`
|
||||
@@ -29,7 +29,7 @@ use super::definition::DefinitionSource;
|
||||
/// test's `MockProvider`). It is never compiled into release builds.
|
||||
pub fn all() -> Vec<AgentDefinition> {
|
||||
#[allow(unused_mut)]
|
||||
let mut defs = crate::openhuman::agent::agents::load_builtins()
|
||||
let mut defs = crate::openhuman::agent_registry::agents::load_builtins()
|
||||
.expect("built-in agent TOML must always parse (see agents/*/agent.toml)");
|
||||
#[cfg(test)]
|
||||
{
|
||||
@@ -123,7 +123,7 @@ mod tests {
|
||||
// +2 for the cfg(test) inherit-based test defs appended by all().
|
||||
assert_eq!(
|
||||
defs.len(),
|
||||
crate::openhuman::agent::agents::BUILTINS.len() + 2
|
||||
crate::openhuman::agent_registry::agents::BUILTINS.len() + 2
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! An [`AgentDefinition`] fully specifies a sub-agent: its core prompt, model,
|
||||
//! allowed tool set, runtime limits, and which sections of the parent system
|
||||
//! prompt to omit. Built-in definitions live in
|
||||
//! [`crate::openhuman::agent::agents`] — one subfolder per agent, each
|
||||
//! [`crate::openhuman::agent_registry::agents`] — one subfolder per agent, each
|
||||
//! holding an `agent.toml` (metadata) and `prompt.md` (system prompt). A
|
||||
//! thin wrapper in [`super::builtin_definitions`] loads them and appends
|
||||
//! the synthetic `fork` definition. Users can ship custom definitions as
|
||||
@@ -167,8 +167,8 @@ pub struct AgentDefinition {
|
||||
/// 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::agent::tools::ArchetypeDelegationTool
|
||||
/// [`SkillDelegationTool`]: crate::openhuman::agent::tools::SkillDelegationTool
|
||||
/// [`ArchetypeDelegationTool`]: crate::openhuman::agent_registry::tools::ArchetypeDelegationTool
|
||||
/// [`SkillDelegationTool`]: crate::openhuman::agent_registry::tools::SkillDelegationTool
|
||||
#[serde(default)]
|
||||
pub subagents: Vec<SubagentEntry>,
|
||||
|
||||
@@ -481,7 +481,7 @@ pub enum SandboxMode {
|
||||
#[serde(tag = "kind", content = "path")]
|
||||
pub enum DefinitionSource {
|
||||
/// Built-in definition shipped as part of the binary (loaded from
|
||||
/// [`crate::openhuman::agent::agents`]).
|
||||
/// [`crate::openhuman::agent_registry::agents`]).
|
||||
#[default]
|
||||
Builtin,
|
||||
/// Loaded from a TOML file at the given absolute path.
|
||||
@@ -572,15 +572,17 @@ impl AgentDefinitionRegistry {
|
||||
// merged in — a workspace TOML can legally replace a built-in
|
||||
// (same id) and is held to the same spawn-hierarchy contract
|
||||
// as the bundled set. See
|
||||
// [`super::super::agents::loader::validate_tier_hierarchy`].
|
||||
// [`crate::openhuman::agent_registry::agents::loader::validate_tier_hierarchy`].
|
||||
let snapshot: Vec<AgentDefinition> = reg.list().into_iter().cloned().collect();
|
||||
super::super::agents::validate_tier_hierarchy(&snapshot).map_err(|e| {
|
||||
anyhow::anyhow!(
|
||||
"agent registry rejected after merging workspace overrides from {}: {}",
|
||||
workspace.display(),
|
||||
e
|
||||
)
|
||||
})?;
|
||||
crate::openhuman::agent_registry::agents::validate_tier_hierarchy(&snapshot).map_err(
|
||||
|e| {
|
||||
anyhow::anyhow!(
|
||||
"agent registry rejected after merging workspace overrides from {}: {}",
|
||||
workspace.display(),
|
||||
e
|
||||
)
|
||||
},
|
||||
)?;
|
||||
|
||||
Ok(reg)
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ pub fn load_dir(dir: &Path, out: &mut Vec<AgentDefinition>) -> Result<()> {
|
||||
///
|
||||
/// Rejects definitions that omit (or leave blank) their `system_prompt`
|
||||
/// — built-in agents are loaded separately and have their prompts
|
||||
/// injected by [`crate::openhuman::agent::agents::load_builtins`], so a
|
||||
/// injected by [`crate::openhuman::agent_registry::agents::load_builtins`], so a
|
||||
/// file-loaded definition that arrives with the
|
||||
/// [`defaults::empty_inline_prompt`] placeholder is always a caller
|
||||
/// mistake. Custom definitions must set either
|
||||
|
||||
@@ -1548,8 +1548,8 @@ impl Tool for TestDelegationTool {
|
||||
|
||||
#[tokio::test]
|
||||
async fn orchestrator_prompt_drives_composio_call_via_delegation_chain() {
|
||||
use crate::openhuman::agent::agents::orchestrator::prompt as orch_prompt;
|
||||
use crate::openhuman::agent::prompts::types::ConnectedIntegration;
|
||||
use crate::openhuman::agent_registry::agents::orchestrator::prompt as orch_prompt;
|
||||
use crate::openhuman::composio::ComposioActionTool;
|
||||
use crate::openhuman::config::TEST_ENV_LOCK;
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
//! - **[`harness::session::Agent`]**: The primary entry point for running a
|
||||
//! conversation. It manages the loop of sending prompts to a provider and
|
||||
//! executing the resulting tool calls.
|
||||
//! - **[`agents`]**: Definitions for built-in specialized agents (Orchestrator,
|
||||
//! Code Executor, Researcher, etc.).
|
||||
//! - **[`crate::openhuman::agent_registry::agents`]**: Definitions for built-in
|
||||
//! specialized agents (Orchestrator, Code Executor, Researcher, etc.).
|
||||
//! - **[`triage`]**: A high-performance pipeline for classifying and responding
|
||||
//! to external triggers (webhooks, cron jobs) using small local models.
|
||||
//! - **[`dispatcher`]**: Pluggable strategies for how tool calls are formatted
|
||||
@@ -18,7 +18,6 @@
|
||||
//! - **[`harness::subagent_runner`]**: Logic for spawning "sub-agents" from
|
||||
//! within a parent agent's tool loop, enabling hierarchical delegation.
|
||||
|
||||
pub mod agents;
|
||||
pub mod bus;
|
||||
pub mod cost;
|
||||
pub mod debug;
|
||||
|
||||
@@ -1,21 +1,12 @@
|
||||
mod archetype_delegation;
|
||||
mod ask_clarification;
|
||||
mod delegate;
|
||||
mod delegate_to_personality;
|
||||
mod dispatch;
|
||||
mod plan_exit;
|
||||
pub mod remember_preference;
|
||||
mod run_skill;
|
||||
pub mod save_preference;
|
||||
mod skill_delegation;
|
||||
mod spawn_parallel_agents;
|
||||
mod spawn_subagent;
|
||||
pub mod spawn_worker_thread;
|
||||
mod todo;
|
||||
|
||||
pub(crate) use dispatch::dispatch_subagent;
|
||||
|
||||
pub use archetype_delegation::ArchetypeDelegationTool;
|
||||
pub use ask_clarification::AskClarificationTool;
|
||||
pub use delegate::DelegateTool;
|
||||
pub use delegate_to_personality::DelegateToPersonalityTool;
|
||||
@@ -23,8 +14,4 @@ pub use plan_exit::{PlanExitTool, PLAN_EXIT_MARKER};
|
||||
pub use remember_preference::RememberPreferenceTool;
|
||||
pub use run_skill::{RunSkillTool, RUN_SKILL_TOOL_NAME};
|
||||
pub use save_preference::SavePreferenceTool;
|
||||
pub use skill_delegation::SkillDelegationTool;
|
||||
pub use spawn_parallel_agents::SpawnParallelAgentsTool;
|
||||
pub use spawn_subagent::SpawnSubagentTool;
|
||||
pub use spawn_worker_thread::SpawnWorkerThreadTool;
|
||||
pub use todo::TodoTool;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! local model is likely to produce.
|
||||
//!
|
||||
//! The contract is described in
|
||||
//! `src/openhuman/agent/agents/trigger_triage/prompt.md` — the triage
|
||||
//! `src/openhuman/agent_registry/agents/trigger_triage/prompt.md` — the triage
|
||||
//! agent must end its reply with a JSON object of the form:
|
||||
//!
|
||||
//! ```json
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::*;
|
||||
use crate::openhuman::agent::agents::BUILTINS;
|
||||
use crate::openhuman::agent::bus::{mock_agent_run_turn, AgentTurnResponse};
|
||||
use crate::openhuman::agent::harness::AgentDefinitionRegistry;
|
||||
use crate::openhuman::agent_registry::agents::BUILTINS;
|
||||
use crate::openhuman::inference::provider::Provider;
|
||||
use async_trait::async_trait;
|
||||
use serde_json::json;
|
||||
|
||||
+2
-2
@@ -26,11 +26,11 @@
|
||||
//!
|
||||
//! The synthetic `fork` definition is *not* listed here — it's a
|
||||
//! byte-stable replay of the parent and has no standalone prompt. It is
|
||||
//! added by [`super::harness::builtin_definitions::all`] on top of the
|
||||
//! added by [`crate::openhuman::agent::harness::builtin_definitions::all`] on top of the
|
||||
//! loader output.
|
||||
//!
|
||||
//! Workspace-level overrides (`$OPENHUMAN_WORKSPACE/agents/*.toml`) are
|
||||
//! handled separately by [`super::harness::definition_loader`] and merged
|
||||
//! handled separately by [`crate::openhuman::agent::harness::definition_loader`] and merged
|
||||
//! into the global registry, where they replace built-ins on `id`
|
||||
//! collision.
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
//! Default registry entries derived from shipped harness definitions.
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::openhuman::agent::harness::definition::{
|
||||
AgentDefinition, ModelSpec, SubagentEntry, ToolScope,
|
||||
};
|
||||
|
||||
use super::types::{AgentRegistryEntry, AgentRegistrySource};
|
||||
|
||||
pub fn default_agents() -> Vec<AgentRegistryEntry> {
|
||||
crate::openhuman::agent_registry::agents::load_builtins()
|
||||
.map(|defs| {
|
||||
defs.into_iter()
|
||||
.map(default_entry_from_definition)
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_else(|err| {
|
||||
tracing::warn!(
|
||||
error = %err,
|
||||
"[agent_registry] failed to load default agent definitions"
|
||||
);
|
||||
Vec::new()
|
||||
})
|
||||
}
|
||||
|
||||
fn default_entry_from_definition(def: AgentDefinition) -> AgentRegistryEntry {
|
||||
AgentRegistryEntry {
|
||||
id: def.id.clone(),
|
||||
name: def.display_name().to_string(),
|
||||
description: def.when_to_use,
|
||||
source: AgentRegistrySource::Default,
|
||||
enabled: true,
|
||||
model: model_to_registry_value(&def.model),
|
||||
system_prompt: None,
|
||||
tool_allowlist: tools_to_allowlist(&def.tools, &def.extra_tools),
|
||||
tool_denylist: def.disallowed_tools,
|
||||
subagents: def
|
||||
.subagents
|
||||
.into_iter()
|
||||
.filter_map(|entry| match entry {
|
||||
SubagentEntry::AgentId(id) => Some(id),
|
||||
SubagentEntry::Skills(_) => None,
|
||||
})
|
||||
.collect(),
|
||||
tags: vec![def.agent_tier.as_str().to_string()],
|
||||
metadata: Value::Null,
|
||||
}
|
||||
}
|
||||
|
||||
fn model_to_registry_value(model: &ModelSpec) -> Option<String> {
|
||||
match model {
|
||||
ModelSpec::Inherit => Some("inherit".to_string()),
|
||||
ModelSpec::Exact(value) => Some(value.clone()),
|
||||
ModelSpec::Hint(value) => Some(format!("hint:{value}")),
|
||||
}
|
||||
}
|
||||
|
||||
fn tools_to_allowlist(scope: &ToolScope, extra_tools: &[String]) -> Vec<String> {
|
||||
let mut tools = match scope {
|
||||
ToolScope::Wildcard => vec!["*".to_string()],
|
||||
ToolScope::Named(names) => names.clone(),
|
||||
};
|
||||
for tool in extra_tools {
|
||||
if !tools.contains(tool) {
|
||||
tools.push(tool.clone());
|
||||
}
|
||||
}
|
||||
tools
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn default_agents_include_core_personas() {
|
||||
let agents = default_agents();
|
||||
let ids: Vec<&str> = agents.iter().map(|agent| agent.id.as_str()).collect();
|
||||
assert!(ids.contains(&"orchestrator"));
|
||||
assert!(ids.contains(&"researcher"));
|
||||
assert!(ids.contains(&"code_executor"));
|
||||
assert!(agents
|
||||
.iter()
|
||||
.all(|agent| agent.source == AgentRegistrySource::Default));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
//! User-facing agent registry.
|
||||
//!
|
||||
//! This high-level domain owns the product registry of agents: shipped
|
||||
//! defaults, user-authored custom agents, enable/disable state, and tool
|
||||
//! visibility policy. The lower-level `openhuman::agent` module remains the
|
||||
//! execution harness and prompt/runtime implementation.
|
||||
|
||||
pub mod agents;
|
||||
mod defaults;
|
||||
mod ops;
|
||||
mod rpc;
|
||||
mod schemas;
|
||||
pub mod tools;
|
||||
pub mod types;
|
||||
|
||||
pub use defaults::default_agents;
|
||||
pub use ops::{
|
||||
get_agent, list_agents, merge_entries, remove_agent, set_agent_enabled, update_agent,
|
||||
upsert_custom_agent,
|
||||
};
|
||||
pub use schemas::{
|
||||
all_controller_schemas as all_agent_registry_controller_schemas,
|
||||
all_registered_controllers as all_agent_registry_registered_controllers,
|
||||
};
|
||||
pub use types::{AgentRegistryConfig, AgentRegistryEntry, AgentRegistryPatch, AgentRegistrySource};
|
||||
@@ -0,0 +1,267 @@
|
||||
//! Config-backed operations for the user-facing agent registry.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::openhuman::config::rpc as config_rpc;
|
||||
|
||||
use super::defaults::default_agents;
|
||||
use super::types::{AgentRegistryEntry, AgentRegistryPatch, AgentRegistrySource};
|
||||
|
||||
const ORCHESTRATOR_AGENT_ID: &str = "orchestrator";
|
||||
|
||||
pub async fn list_agents(include_disabled: bool) -> Result<Vec<AgentRegistryEntry>, String> {
|
||||
let config = config_rpc::load_config_with_timeout().await?;
|
||||
Ok(merge_entries(
|
||||
&config.agent_registry.entries,
|
||||
include_disabled,
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn get_agent(id: &str) -> Result<Option<AgentRegistryEntry>, String> {
|
||||
let id = id.trim();
|
||||
Ok(list_agents(true)
|
||||
.await?
|
||||
.into_iter()
|
||||
.find(|agent| agent.id == id))
|
||||
}
|
||||
|
||||
pub async fn upsert_custom_agent(
|
||||
mut entry: AgentRegistryEntry,
|
||||
) -> Result<AgentRegistryEntry, String> {
|
||||
entry.source = AgentRegistrySource::Custom;
|
||||
entry.validate()?;
|
||||
|
||||
if default_agents().iter().any(|agent| agent.id == entry.id) {
|
||||
return Err(format!(
|
||||
"agent '{}' is a default agent; use update_agent to override it",
|
||||
entry.id
|
||||
));
|
||||
}
|
||||
|
||||
let mut config = config_rpc::load_config_with_timeout().await?;
|
||||
match config
|
||||
.agent_registry
|
||||
.entries
|
||||
.iter_mut()
|
||||
.find(|agent| agent.id == entry.id)
|
||||
{
|
||||
Some(existing) => *existing = entry.clone(),
|
||||
None => config.agent_registry.entries.push(entry.clone()),
|
||||
}
|
||||
config
|
||||
.save()
|
||||
.await
|
||||
.map_err(|e| format!("failed to save config: {e:#}"))?;
|
||||
Ok(entry)
|
||||
}
|
||||
|
||||
pub async fn update_agent(
|
||||
id: &str,
|
||||
patch: AgentRegistryPatch,
|
||||
) -> Result<AgentRegistryEntry, String> {
|
||||
let id = id.trim();
|
||||
if id.is_empty() {
|
||||
return Err("id is required".to_string());
|
||||
}
|
||||
|
||||
let defaults = default_agents();
|
||||
let mut config = config_rpc::load_config_with_timeout().await?;
|
||||
let entry = match config
|
||||
.agent_registry
|
||||
.entries
|
||||
.iter_mut()
|
||||
.find(|agent| agent.id == id)
|
||||
{
|
||||
Some(existing) => existing,
|
||||
None => {
|
||||
let base = defaults
|
||||
.iter()
|
||||
.find(|agent| agent.id == id)
|
||||
.cloned()
|
||||
.ok_or_else(|| format!("agent '{id}' not found"))?;
|
||||
config.agent_registry.entries.push(base);
|
||||
config
|
||||
.agent_registry
|
||||
.entries
|
||||
.last_mut()
|
||||
.expect("pushed entry")
|
||||
}
|
||||
};
|
||||
|
||||
apply_patch(entry, patch);
|
||||
entry.validate()?;
|
||||
ensure_orchestrator_enabled(entry)?;
|
||||
let updated = entry.clone();
|
||||
config
|
||||
.save()
|
||||
.await
|
||||
.map_err(|e| format!("failed to save config: {e:#}"))?;
|
||||
Ok(updated)
|
||||
}
|
||||
|
||||
pub async fn set_agent_enabled(id: &str, enabled: bool) -> Result<AgentRegistryEntry, String> {
|
||||
update_agent(
|
||||
id,
|
||||
AgentRegistryPatch {
|
||||
enabled: Some(enabled),
|
||||
..AgentRegistryPatch::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn remove_agent(id: &str) -> Result<bool, String> {
|
||||
let id = id.trim();
|
||||
if id.is_empty() {
|
||||
return Err("id is required".to_string());
|
||||
}
|
||||
|
||||
let mut config = config_rpc::load_config_with_timeout().await?;
|
||||
let before = config.agent_registry.entries.len();
|
||||
config.agent_registry.entries.retain(|agent| agent.id != id);
|
||||
let removed = config.agent_registry.entries.len() < before;
|
||||
if removed {
|
||||
config
|
||||
.save()
|
||||
.await
|
||||
.map_err(|e| format!("failed to save config: {e:#}"))?;
|
||||
}
|
||||
Ok(removed)
|
||||
}
|
||||
|
||||
pub fn merge_entries(
|
||||
configured: &[AgentRegistryEntry],
|
||||
include_disabled: bool,
|
||||
) -> Vec<AgentRegistryEntry> {
|
||||
let mut default_order = Vec::new();
|
||||
let mut merged: HashMap<String, AgentRegistryEntry> = HashMap::new();
|
||||
for agent in default_agents() {
|
||||
default_order.push(agent.id.clone());
|
||||
merged.insert(agent.id.clone(), agent);
|
||||
}
|
||||
|
||||
let mut custom_order = Vec::new();
|
||||
for entry in configured {
|
||||
if matches!(entry.source, AgentRegistrySource::Custom) && !merged.contains_key(&entry.id) {
|
||||
custom_order.push(entry.id.clone());
|
||||
}
|
||||
merged.insert(entry.id.clone(), entry.clone());
|
||||
}
|
||||
|
||||
let mut result = Vec::new();
|
||||
for id in default_order.into_iter().chain(custom_order) {
|
||||
if let Some(agent) = merged.remove(&id) {
|
||||
if include_disabled || agent.enabled {
|
||||
result.push(agent);
|
||||
}
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn apply_patch(entry: &mut AgentRegistryEntry, patch: AgentRegistryPatch) {
|
||||
if let Some(name) = patch.name {
|
||||
entry.name = name;
|
||||
}
|
||||
if let Some(description) = patch.description {
|
||||
entry.description = description;
|
||||
}
|
||||
if let Some(enabled) = patch.enabled {
|
||||
entry.enabled = enabled;
|
||||
}
|
||||
if let Some(model) = patch.model {
|
||||
entry.model = Some(model);
|
||||
}
|
||||
if let Some(system_prompt) = patch.system_prompt {
|
||||
entry.system_prompt = Some(system_prompt);
|
||||
}
|
||||
if let Some(tool_allowlist) = patch.tool_allowlist {
|
||||
entry.tool_allowlist = tool_allowlist;
|
||||
}
|
||||
if let Some(tool_denylist) = patch.tool_denylist {
|
||||
entry.tool_denylist = tool_denylist;
|
||||
}
|
||||
if let Some(subagents) = patch.subagents {
|
||||
entry.subagents = subagents;
|
||||
}
|
||||
if let Some(tags) = patch.tags {
|
||||
entry.tags = tags;
|
||||
}
|
||||
if let Some(metadata) = patch.metadata {
|
||||
entry.metadata = metadata;
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_orchestrator_enabled(entry: &AgentRegistryEntry) -> Result<(), String> {
|
||||
if entry.id == ORCHESTRATOR_AGENT_ID && !entry.enabled {
|
||||
return Err("orchestrator agent cannot be disabled".to_string());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::Value;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn custom_agent(id: &str, enabled: bool) -> AgentRegistryEntry {
|
||||
AgentRegistryEntry {
|
||||
id: id.to_string(),
|
||||
name: "Custom".to_string(),
|
||||
description: "Handles custom work.".to_string(),
|
||||
source: AgentRegistrySource::Custom,
|
||||
enabled,
|
||||
model: Some("reasoning-v1".to_string()),
|
||||
system_prompt: Some("Do custom work.".to_string()),
|
||||
tool_allowlist: vec!["memory.search".to_string()],
|
||||
tool_denylist: Vec::new(),
|
||||
subagents: Vec::new(),
|
||||
tags: vec!["custom".to_string()],
|
||||
metadata: Value::Null,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_entries_applies_default_overrides_and_filters_disabled() {
|
||||
let configured = vec![AgentRegistryEntry {
|
||||
id: "researcher".to_string(),
|
||||
name: "Researcher".to_string(),
|
||||
description: "Disabled for this workspace.".to_string(),
|
||||
source: AgentRegistrySource::Default,
|
||||
enabled: false,
|
||||
model: None,
|
||||
system_prompt: None,
|
||||
tool_allowlist: vec!["*".to_string()],
|
||||
tool_denylist: Vec::new(),
|
||||
subagents: Vec::new(),
|
||||
tags: Vec::new(),
|
||||
metadata: Value::Null,
|
||||
}];
|
||||
|
||||
let visible = merge_entries(&configured, false);
|
||||
assert!(!visible.iter().any(|agent| agent.id == "researcher"));
|
||||
|
||||
let all = merge_entries(&configured, true);
|
||||
let researcher = all.iter().find(|agent| agent.id == "researcher").unwrap();
|
||||
assert!(!researcher.enabled);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_entries_appends_custom_agents() {
|
||||
let configured = vec![custom_agent("finance_analyst", true)];
|
||||
let merged = merge_entries(&configured, true);
|
||||
assert!(merged.iter().any(|agent| agent.id == "orchestrator"));
|
||||
assert_eq!(merged.last().unwrap().id, "finance_analyst");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ensure_orchestrator_enabled_rejects_disabled_orchestrator() {
|
||||
let mut entry = custom_agent("orchestrator", false);
|
||||
entry.source = AgentRegistrySource::Default;
|
||||
assert_eq!(
|
||||
ensure_orchestrator_enabled(&entry).unwrap_err(),
|
||||
"orchestrator agent cannot be disabled"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
//! RPC payloads and handlers for the agent registry domain.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::rpc::RpcOutcome;
|
||||
|
||||
use super::ops;
|
||||
use super::types::{AgentRegistryEntry, AgentRegistryPatch, AgentRegistrySource};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ListRequest {
|
||||
#[serde(default)]
|
||||
pub include_disabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ListResponse {
|
||||
pub agents: Vec<AgentRegistryEntry>,
|
||||
}
|
||||
|
||||
pub async fn list_rpc(req: ListRequest) -> Result<RpcOutcome<ListResponse>, String> {
|
||||
Ok(RpcOutcome::new(
|
||||
ListResponse {
|
||||
agents: ops::list_agents(req.include_disabled).await?,
|
||||
},
|
||||
vec![],
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct GetRequest {
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct GetResponse {
|
||||
pub agent: Option<AgentRegistryEntry>,
|
||||
}
|
||||
|
||||
pub async fn get_rpc(req: GetRequest) -> Result<RpcOutcome<GetResponse>, String> {
|
||||
Ok(RpcOutcome::new(
|
||||
GetResponse {
|
||||
agent: ops::get_agent(&req.id).await?,
|
||||
},
|
||||
vec![],
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct CreateCustomRequest {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
#[serde(default)]
|
||||
pub enabled: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub model: Option<String>,
|
||||
#[serde(default)]
|
||||
pub system_prompt: Option<String>,
|
||||
#[serde(default)]
|
||||
pub tool_allowlist: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub tool_denylist: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub subagents: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub tags: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub metadata: Option<Value>,
|
||||
}
|
||||
|
||||
impl CreateCustomRequest {
|
||||
fn into_entry(self) -> AgentRegistryEntry {
|
||||
AgentRegistryEntry {
|
||||
id: self.id,
|
||||
name: self.name,
|
||||
description: self.description,
|
||||
source: AgentRegistrySource::Custom,
|
||||
enabled: self.enabled.unwrap_or(true),
|
||||
model: self.model,
|
||||
system_prompt: self.system_prompt,
|
||||
tool_allowlist: self.tool_allowlist,
|
||||
tool_denylist: self.tool_denylist,
|
||||
subagents: self.subagents,
|
||||
tags: self.tags,
|
||||
metadata: self.metadata.unwrap_or(Value::Null),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_custom_rpc(
|
||||
req: CreateCustomRequest,
|
||||
) -> Result<RpcOutcome<AgentResponse>, String> {
|
||||
Ok(RpcOutcome::new(
|
||||
AgentResponse {
|
||||
agent: ops::upsert_custom_agent(req.into_entry()).await?,
|
||||
},
|
||||
vec![],
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct UpsertCustomRequest {
|
||||
pub agent: AgentRegistryEntry,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct AgentResponse {
|
||||
pub agent: AgentRegistryEntry,
|
||||
}
|
||||
|
||||
pub async fn upsert_custom_rpc(
|
||||
req: UpsertCustomRequest,
|
||||
) -> Result<RpcOutcome<AgentResponse>, String> {
|
||||
Ok(RpcOutcome::new(
|
||||
AgentResponse {
|
||||
agent: ops::upsert_custom_agent(req.agent).await?,
|
||||
},
|
||||
vec![],
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct UpdateRequest {
|
||||
pub id: String,
|
||||
#[serde(flatten)]
|
||||
pub patch: AgentRegistryPatch,
|
||||
}
|
||||
|
||||
pub async fn update_rpc(req: UpdateRequest) -> Result<RpcOutcome<AgentResponse>, String> {
|
||||
Ok(RpcOutcome::new(
|
||||
AgentResponse {
|
||||
agent: ops::update_agent(&req.id, req.patch).await?,
|
||||
},
|
||||
vec![],
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SetEnabledRequest {
|
||||
pub id: String,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
pub async fn set_enabled_rpc(req: SetEnabledRequest) -> Result<RpcOutcome<AgentResponse>, String> {
|
||||
Ok(RpcOutcome::new(
|
||||
AgentResponse {
|
||||
agent: ops::set_agent_enabled(&req.id, req.enabled).await?,
|
||||
},
|
||||
vec![],
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct RemoveRequest {
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct RemoveResponse {
|
||||
pub removed: bool,
|
||||
}
|
||||
|
||||
pub async fn remove_rpc(req: RemoveRequest) -> Result<RpcOutcome<RemoveResponse>, String> {
|
||||
Ok(RpcOutcome::new(
|
||||
RemoveResponse {
|
||||
removed: ops::remove_agent(&req.id).await?,
|
||||
},
|
||||
vec![],
|
||||
))
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
//! Controller-registry schemas for `openhuman.agent_registry_*`.
|
||||
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
use crate::core::all::{ControllerFuture, RegisteredController};
|
||||
use crate::core::{ControllerSchema, FieldSchema, TypeSchema};
|
||||
use crate::rpc::RpcOutcome;
|
||||
|
||||
use super::rpc;
|
||||
|
||||
const NAMESPACE: &str = "agent_registry";
|
||||
|
||||
pub fn all_controller_schemas() -> Vec<ControllerSchema> {
|
||||
vec![
|
||||
schemas("list"),
|
||||
schemas("get"),
|
||||
schemas("create_custom"),
|
||||
schemas("upsert_custom"),
|
||||
schemas("update"),
|
||||
schemas("set_enabled"),
|
||||
schemas("remove"),
|
||||
]
|
||||
}
|
||||
|
||||
pub fn all_registered_controllers() -> Vec<RegisteredController> {
|
||||
vec![
|
||||
RegisteredController {
|
||||
schema: schemas("list"),
|
||||
handler: handle_list,
|
||||
},
|
||||
RegisteredController {
|
||||
schema: schemas("get"),
|
||||
handler: handle_get,
|
||||
},
|
||||
RegisteredController {
|
||||
schema: schemas("create_custom"),
|
||||
handler: handle_create_custom,
|
||||
},
|
||||
RegisteredController {
|
||||
schema: schemas("upsert_custom"),
|
||||
handler: handle_upsert_custom,
|
||||
},
|
||||
RegisteredController {
|
||||
schema: schemas("update"),
|
||||
handler: handle_update,
|
||||
},
|
||||
RegisteredController {
|
||||
schema: schemas("set_enabled"),
|
||||
handler: handle_set_enabled,
|
||||
},
|
||||
RegisteredController {
|
||||
schema: schemas("remove"),
|
||||
handler: handle_remove,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
pub fn schemas(function: &str) -> ControllerSchema {
|
||||
match function {
|
||||
"list" => ControllerSchema {
|
||||
namespace: NAMESPACE,
|
||||
function: "list",
|
||||
description: "List default and custom agents available in the high-level registry.",
|
||||
inputs: vec![FieldSchema {
|
||||
name: "include_disabled",
|
||||
ty: TypeSchema::Bool,
|
||||
comment: "When true, include disabled agents in the response.",
|
||||
required: false,
|
||||
}],
|
||||
outputs: vec![FieldSchema {
|
||||
name: "agents",
|
||||
ty: TypeSchema::Array(Box::new(TypeSchema::Ref("AgentRegistryEntry"))),
|
||||
comment: "Registry entries in default-first order.",
|
||||
required: true,
|
||||
}],
|
||||
},
|
||||
"get" => ControllerSchema {
|
||||
namespace: NAMESPACE,
|
||||
function: "get",
|
||||
description: "Get one agent registry entry by id.",
|
||||
inputs: vec![required_string("id", "Agent id.")],
|
||||
outputs: vec![FieldSchema {
|
||||
name: "agent",
|
||||
ty: TypeSchema::Option(Box::new(TypeSchema::Ref("AgentRegistryEntry"))),
|
||||
comment: "Agent registry entry if found.",
|
||||
required: false,
|
||||
}],
|
||||
},
|
||||
"upsert_custom" => ControllerSchema {
|
||||
namespace: NAMESPACE,
|
||||
function: "upsert_custom",
|
||||
description: "Create or replace a custom user-authored agent with its tool policy.",
|
||||
inputs: vec![FieldSchema {
|
||||
name: "agent",
|
||||
ty: TypeSchema::Ref("AgentRegistryEntry"),
|
||||
comment: "Custom agent entry. Source is forced to custom.",
|
||||
required: true,
|
||||
}],
|
||||
outputs: vec![agent_output()],
|
||||
},
|
||||
"create_custom" => ControllerSchema {
|
||||
namespace: NAMESPACE,
|
||||
function: "create_custom",
|
||||
description: "Create or replace a custom user-authored agent from flat RPC params.",
|
||||
inputs: vec![
|
||||
required_string("id", "Custom agent id."),
|
||||
required_string("name", "Display name."),
|
||||
required_string("description", "When this agent should be used."),
|
||||
optional_bool("enabled", "Enable or disable this agent. Defaults to true."),
|
||||
optional_string("model", "Model id or route hint."),
|
||||
optional_string("system_prompt", "Custom instructions."),
|
||||
optional_string_array("tool_allowlist", "Allowed tool names; '*' means all."),
|
||||
optional_string_array("tool_denylist", "Denied tool names."),
|
||||
optional_string_array("subagents", "Delegated agent ids."),
|
||||
optional_string_array("tags", "UI grouping/search tags."),
|
||||
FieldSchema {
|
||||
name: "metadata",
|
||||
ty: TypeSchema::Json,
|
||||
comment: "Free-form metadata.",
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
outputs: vec![agent_output()],
|
||||
},
|
||||
"update" => ControllerSchema {
|
||||
namespace: NAMESPACE,
|
||||
function: "update",
|
||||
description: "Patch either a default-agent override or a custom agent.",
|
||||
inputs: vec![
|
||||
required_string("id", "Agent id."),
|
||||
optional_string("name", "New display name."),
|
||||
optional_string("description", "New description."),
|
||||
optional_bool("enabled", "Enable or disable this agent."),
|
||||
optional_string("model", "Model id or route hint."),
|
||||
optional_string("system_prompt", "Custom instructions."),
|
||||
optional_string_array("tool_allowlist", "Allowed tool names; '*' means all."),
|
||||
optional_string_array("tool_denylist", "Denied tool names."),
|
||||
optional_string_array("subagents", "Delegated agent ids."),
|
||||
optional_string_array("tags", "UI grouping/search tags."),
|
||||
FieldSchema {
|
||||
name: "metadata",
|
||||
ty: TypeSchema::Json,
|
||||
comment: "Free-form metadata.",
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
outputs: vec![agent_output()],
|
||||
},
|
||||
"set_enabled" => ControllerSchema {
|
||||
namespace: NAMESPACE,
|
||||
function: "set_enabled",
|
||||
description: "Enable or disable a default or custom agent.",
|
||||
inputs: vec![
|
||||
required_string("id", "Agent id."),
|
||||
FieldSchema {
|
||||
name: "enabled",
|
||||
ty: TypeSchema::Bool,
|
||||
comment: "Desired enabled state.",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
outputs: vec![agent_output()],
|
||||
},
|
||||
"remove" => ControllerSchema {
|
||||
namespace: NAMESPACE,
|
||||
function: "remove",
|
||||
description: "Remove a custom agent or reset a default-agent override.",
|
||||
inputs: vec![required_string("id", "Agent id.")],
|
||||
outputs: vec![FieldSchema {
|
||||
name: "removed",
|
||||
ty: TypeSchema::Bool,
|
||||
comment: "True when a configured entry was removed.",
|
||||
required: true,
|
||||
}],
|
||||
},
|
||||
other => panic!("unknown agent_registry schema function: {other}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_list(params: Map<String, Value>) -> ControllerFuture {
|
||||
Box::pin(async move {
|
||||
let req = parse_value::<rpc::ListRequest>(Value::Object(params))?;
|
||||
to_json(rpc::list_rpc(req).await?)
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_get(params: Map<String, Value>) -> ControllerFuture {
|
||||
Box::pin(async move {
|
||||
let req = parse_value::<rpc::GetRequest>(Value::Object(params))?;
|
||||
to_json(rpc::get_rpc(req).await?)
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_create_custom(params: Map<String, Value>) -> ControllerFuture {
|
||||
Box::pin(async move {
|
||||
let req = parse_value::<rpc::CreateCustomRequest>(Value::Object(params))?;
|
||||
to_json(rpc::create_custom_rpc(req).await?)
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_upsert_custom(params: Map<String, Value>) -> ControllerFuture {
|
||||
Box::pin(async move {
|
||||
let req = parse_value::<rpc::UpsertCustomRequest>(Value::Object(params))?;
|
||||
to_json(rpc::upsert_custom_rpc(req).await?)
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_update(params: Map<String, Value>) -> ControllerFuture {
|
||||
Box::pin(async move {
|
||||
let req = parse_value::<rpc::UpdateRequest>(Value::Object(params))?;
|
||||
to_json(rpc::update_rpc(req).await?)
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_set_enabled(params: Map<String, Value>) -> ControllerFuture {
|
||||
Box::pin(async move {
|
||||
let req = parse_value::<rpc::SetEnabledRequest>(Value::Object(params))?;
|
||||
to_json(rpc::set_enabled_rpc(req).await?)
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_remove(params: Map<String, Value>) -> ControllerFuture {
|
||||
Box::pin(async move {
|
||||
let req = parse_value::<rpc::RemoveRequest>(Value::Object(params))?;
|
||||
to_json(rpc::remove_rpc(req).await?)
|
||||
})
|
||||
}
|
||||
|
||||
fn required_string(name: &'static str, comment: &'static str) -> FieldSchema {
|
||||
FieldSchema {
|
||||
name,
|
||||
ty: TypeSchema::String,
|
||||
comment,
|
||||
required: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn optional_string(name: &'static str, comment: &'static str) -> FieldSchema {
|
||||
FieldSchema {
|
||||
name,
|
||||
ty: TypeSchema::Option(Box::new(TypeSchema::String)),
|
||||
comment,
|
||||
required: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn optional_bool(name: &'static str, comment: &'static str) -> FieldSchema {
|
||||
FieldSchema {
|
||||
name,
|
||||
ty: TypeSchema::Option(Box::new(TypeSchema::Bool)),
|
||||
comment,
|
||||
required: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn optional_string_array(name: &'static str, comment: &'static str) -> FieldSchema {
|
||||
FieldSchema {
|
||||
name,
|
||||
ty: TypeSchema::Array(Box::new(TypeSchema::String)),
|
||||
comment,
|
||||
required: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn agent_output() -> FieldSchema {
|
||||
FieldSchema {
|
||||
name: "agent",
|
||||
ty: TypeSchema::Ref("AgentRegistryEntry"),
|
||||
comment: "Updated agent registry entry.",
|
||||
required: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_value<T: DeserializeOwned>(v: Value) -> Result<T, String> {
|
||||
serde_json::from_value(v).map_err(|e| format!("invalid params: {e}"))
|
||||
}
|
||||
|
||||
fn to_json<T: serde::Serialize>(outcome: RpcOutcome<T>) -> Result<Value, String> {
|
||||
outcome.into_cli_compatible_json()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn all_controller_schemas_and_registered_controllers_stay_in_sync() {
|
||||
let schemas = all_controller_schemas();
|
||||
let controllers = all_registered_controllers();
|
||||
assert_eq!(schemas.len(), controllers.len());
|
||||
assert!(schemas.iter().all(|schema| schema.namespace == NAMESPACE));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "unknown agent_registry schema function")]
|
||||
fn schemas_panics_on_unknown_function() {
|
||||
schemas("missing");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
#[path = "tools/archetype_delegation.rs"]
|
||||
mod archetype_delegation;
|
||||
#[path = "tools/dispatch.rs"]
|
||||
mod dispatch;
|
||||
#[path = "tools/skill_delegation.rs"]
|
||||
mod skill_delegation;
|
||||
#[path = "tools/spawn_parallel_agents.rs"]
|
||||
mod spawn_parallel_agents;
|
||||
#[path = "tools/spawn_subagent.rs"]
|
||||
mod spawn_subagent;
|
||||
#[path = "tools/spawn_worker_thread.rs"]
|
||||
pub mod spawn_worker_thread;
|
||||
|
||||
pub(crate) use dispatch::dispatch_subagent;
|
||||
|
||||
pub use archetype_delegation::ArchetypeDelegationTool;
|
||||
pub use skill_delegation::{SkillDelegationTool, INTEGRATIONS_DELEGATE_TOOL_NAME};
|
||||
pub use spawn_parallel_agents::SpawnParallelAgentsTool;
|
||||
pub use spawn_subagent::SpawnSubagentTool;
|
||||
pub use spawn_worker_thread::SpawnWorkerThreadTool;
|
||||
+1
-1
@@ -125,7 +125,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::openhuman::tools::traits::Tool;
|
||||
|
||||
use super::super::AskClarificationTool;
|
||||
use crate::openhuman::agent::tools::AskClarificationTool;
|
||||
|
||||
#[test]
|
||||
fn ask_clarification_tool_re_exported() {
|
||||
@@ -0,0 +1,106 @@
|
||||
//! Public data model for the user-facing agent registry.
|
||||
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AgentRegistrySource {
|
||||
Default,
|
||||
Custom,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct AgentRegistryEntry {
|
||||
/// Stable id used by routing and UI selection.
|
||||
pub id: String,
|
||||
/// User-facing name.
|
||||
pub name: String,
|
||||
/// Short explanation of when this agent should be used.
|
||||
pub description: String,
|
||||
/// Default agents ship with OpenHuman; custom agents are user-authored.
|
||||
pub source: AgentRegistrySource,
|
||||
/// Whether this agent is available for selection/delegation.
|
||||
#[serde(default = "default_true")]
|
||||
pub enabled: bool,
|
||||
/// Optional model or route hint for this agent.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub model: Option<String>,
|
||||
/// Optional custom instructions. Default agents usually keep this empty
|
||||
/// because their runtime prompts are generated by the harness.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub system_prompt: Option<String>,
|
||||
/// Tool names this agent may use. `"*"` means all currently visible tools.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub tool_allowlist: Vec<String>,
|
||||
/// Tool names that must be hidden even if they match the allowlist.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub tool_denylist: Vec<String>,
|
||||
/// Optional delegated agent ids this agent may spawn.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub subagents: Vec<String>,
|
||||
/// Search/filter tags for UI grouping.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub tags: Vec<String>,
|
||||
/// Free-form UI/runtime metadata. Keep execution-critical fields typed.
|
||||
#[serde(default, skip_serializing_if = "Value::is_null")]
|
||||
pub metadata: Value,
|
||||
}
|
||||
|
||||
impl AgentRegistryEntry {
|
||||
pub fn validate(&self) -> Result<(), String> {
|
||||
if self.id.trim().is_empty() {
|
||||
return Err("id is required".to_string());
|
||||
}
|
||||
if self
|
||||
.id
|
||||
.chars()
|
||||
.any(|ch| !(ch.is_ascii_alphanumeric() || ch == '_' || ch == '-'))
|
||||
{
|
||||
return Err("id may only contain ASCII letters, numbers, '_' and '-'".to_string());
|
||||
}
|
||||
if self.name.trim().is_empty() {
|
||||
return Err("name is required".to_string());
|
||||
}
|
||||
if self.description.trim().is_empty() {
|
||||
return Err("description is required".to_string());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct AgentRegistryConfig {
|
||||
/// User-authored agents plus persisted overrides for shipped default agents.
|
||||
#[serde(default)]
|
||||
pub entries: Vec<AgentRegistryEntry>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize)]
|
||||
pub struct AgentRegistryPatch {
|
||||
#[serde(default)]
|
||||
pub name: Option<String>,
|
||||
#[serde(default)]
|
||||
pub description: Option<String>,
|
||||
#[serde(default)]
|
||||
pub enabled: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub model: Option<String>,
|
||||
#[serde(default)]
|
||||
pub system_prompt: Option<String>,
|
||||
#[serde(default)]
|
||||
pub tool_allowlist: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
pub tool_denylist: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
pub subagents: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
pub tags: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
pub metadata: Option<Value>,
|
||||
}
|
||||
@@ -229,6 +229,11 @@ pub struct Config {
|
||||
#[serde(default)]
|
||||
pub memory_sources: Vec<crate::openhuman::memory_sources::types::MemorySourceEntry>,
|
||||
|
||||
/// User-facing agent registry — shipped default agents plus user-authored
|
||||
/// custom agents and persisted enable/disable/tool-policy overrides.
|
||||
#[serde(default)]
|
||||
pub agent_registry: crate::openhuman::agent_registry::types::AgentRegistryConfig,
|
||||
|
||||
#[serde(default)]
|
||||
pub computer_control: ComputerControlConfig,
|
||||
|
||||
@@ -667,6 +672,7 @@ impl Default for Config {
|
||||
proxy: ProxyConfig::default(),
|
||||
cost: CostConfig::default(),
|
||||
memory_sources: Vec::new(),
|
||||
agent_registry: crate::openhuman::agent_registry::types::AgentRegistryConfig::default(),
|
||||
computer_control: ComputerControlConfig::default(),
|
||||
agents: HashMap::new(),
|
||||
local_ai: LocalAiConfig::default(),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user