diff --git a/src/core/all.rs b/src/core/all.rs index 9a41a97dc..bda4f2fdc 100644 --- a/src/core/all.rs +++ b/src/core/all.rs @@ -124,6 +124,9 @@ fn build_registered_controllers() -> Vec { 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 { 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()); diff --git a/src/core/event_bus/README.md b/src/core/event_bus/README.md index 8a1e40087..70c62b4c6 100644 --- a/src/core/event_bus/README.md +++ b/src/core/event_bus/README.md @@ -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. diff --git a/src/openhuman/agent/README.md b/src/openhuman/agent/README.md index cd2234872..a8c333685 100644 --- a/src/openhuman/agent/README.md +++ b/src/openhuman/agent/README.md @@ -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 diff --git a/src/openhuman/agent/harness/builtin_definitions.rs b/src/openhuman/agent/harness/builtin_definitions.rs index fbb119c30..ece0c6869 100644 --- a/src/openhuman/agent/harness/builtin_definitions.rs +++ b/src/openhuman/agent/harness/builtin_definitions.rs @@ -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 { #[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 ); } diff --git a/src/openhuman/agent/harness/definition.rs b/src/openhuman/agent/harness/definition.rs index 15adf3a74..32bd5ebdb 100644 --- a/src/openhuman/agent/harness/definition.rs +++ b/src/openhuman/agent/harness/definition.rs @@ -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, @@ -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 = 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) } diff --git a/src/openhuman/agent/harness/definition_loader.rs b/src/openhuman/agent/harness/definition_loader.rs index eda6dd059..4d07c4fdd 100644 --- a/src/openhuman/agent/harness/definition_loader.rs +++ b/src/openhuman/agent/harness/definition_loader.rs @@ -95,7 +95,7 @@ pub fn load_dir(dir: &Path, out: &mut Vec) -> 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 diff --git a/src/openhuman/agent/harness/test_support_tests.rs b/src/openhuman/agent/harness/test_support_tests.rs index 415653acd..36820c919 100644 --- a/src/openhuman/agent/harness/test_support_tests.rs +++ b/src/openhuman/agent/harness/test_support_tests.rs @@ -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; diff --git a/src/openhuman/agent/mod.rs b/src/openhuman/agent/mod.rs index 9575efcef..ce662d84c 100644 --- a/src/openhuman/agent/mod.rs +++ b/src/openhuman/agent/mod.rs @@ -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; diff --git a/src/openhuman/agent/tools.rs b/src/openhuman/agent/tools.rs index d08c90c38..fe1e1296d 100644 --- a/src/openhuman/agent/tools.rs +++ b/src/openhuman/agent/tools.rs @@ -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; diff --git a/src/openhuman/agent/triage/decision.rs b/src/openhuman/agent/triage/decision.rs index 005dc09c8..510075441 100644 --- a/src/openhuman/agent/triage/decision.rs +++ b/src/openhuman/agent/triage/decision.rs @@ -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 diff --git a/src/openhuman/agent/triage/evaluator_tests.rs b/src/openhuman/agent/triage/evaluator_tests.rs index dd9f0b82f..8d46718a7 100644 --- a/src/openhuman/agent/triage/evaluator_tests.rs +++ b/src/openhuman/agent/triage/evaluator_tests.rs @@ -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; diff --git a/src/openhuman/agent/agents/archivist/agent.toml b/src/openhuman/agent_registry/agents/archivist/agent.toml similarity index 100% rename from src/openhuman/agent/agents/archivist/agent.toml rename to src/openhuman/agent_registry/agents/archivist/agent.toml diff --git a/src/openhuman/agent/agents/archivist/mod.rs b/src/openhuman/agent_registry/agents/archivist/mod.rs similarity index 100% rename from src/openhuman/agent/agents/archivist/mod.rs rename to src/openhuman/agent_registry/agents/archivist/mod.rs diff --git a/src/openhuman/agent/agents/archivist/prompt.md b/src/openhuman/agent_registry/agents/archivist/prompt.md similarity index 100% rename from src/openhuman/agent/agents/archivist/prompt.md rename to src/openhuman/agent_registry/agents/archivist/prompt.md diff --git a/src/openhuman/agent/agents/archivist/prompt.rs b/src/openhuman/agent_registry/agents/archivist/prompt.rs similarity index 100% rename from src/openhuman/agent/agents/archivist/prompt.rs rename to src/openhuman/agent_registry/agents/archivist/prompt.rs diff --git a/src/openhuman/agent/agents/code_executor/agent.toml b/src/openhuman/agent_registry/agents/code_executor/agent.toml similarity index 100% rename from src/openhuman/agent/agents/code_executor/agent.toml rename to src/openhuman/agent_registry/agents/code_executor/agent.toml diff --git a/src/openhuman/agent/agents/code_executor/mod.rs b/src/openhuman/agent_registry/agents/code_executor/mod.rs similarity index 100% rename from src/openhuman/agent/agents/code_executor/mod.rs rename to src/openhuman/agent_registry/agents/code_executor/mod.rs diff --git a/src/openhuman/agent/agents/code_executor/prompt.md b/src/openhuman/agent_registry/agents/code_executor/prompt.md similarity index 100% rename from src/openhuman/agent/agents/code_executor/prompt.md rename to src/openhuman/agent_registry/agents/code_executor/prompt.md diff --git a/src/openhuman/agent/agents/code_executor/prompt.rs b/src/openhuman/agent_registry/agents/code_executor/prompt.rs similarity index 100% rename from src/openhuman/agent/agents/code_executor/prompt.rs rename to src/openhuman/agent_registry/agents/code_executor/prompt.rs diff --git a/src/openhuman/agent/agents/critic/agent.toml b/src/openhuman/agent_registry/agents/critic/agent.toml similarity index 100% rename from src/openhuman/agent/agents/critic/agent.toml rename to src/openhuman/agent_registry/agents/critic/agent.toml diff --git a/src/openhuman/agent/agents/critic/mod.rs b/src/openhuman/agent_registry/agents/critic/mod.rs similarity index 100% rename from src/openhuman/agent/agents/critic/mod.rs rename to src/openhuman/agent_registry/agents/critic/mod.rs diff --git a/src/openhuman/agent/agents/critic/prompt.md b/src/openhuman/agent_registry/agents/critic/prompt.md similarity index 100% rename from src/openhuman/agent/agents/critic/prompt.md rename to src/openhuman/agent_registry/agents/critic/prompt.md diff --git a/src/openhuman/agent/agents/critic/prompt.rs b/src/openhuman/agent_registry/agents/critic/prompt.rs similarity index 100% rename from src/openhuman/agent/agents/critic/prompt.rs rename to src/openhuman/agent_registry/agents/critic/prompt.rs diff --git a/src/openhuman/agent/agents/crypto_agent/agent.toml b/src/openhuman/agent_registry/agents/crypto_agent/agent.toml similarity index 100% rename from src/openhuman/agent/agents/crypto_agent/agent.toml rename to src/openhuman/agent_registry/agents/crypto_agent/agent.toml diff --git a/src/openhuman/agent/agents/crypto_agent/mod.rs b/src/openhuman/agent_registry/agents/crypto_agent/mod.rs similarity index 100% rename from src/openhuman/agent/agents/crypto_agent/mod.rs rename to src/openhuman/agent_registry/agents/crypto_agent/mod.rs diff --git a/src/openhuman/agent/agents/crypto_agent/prompt.md b/src/openhuman/agent_registry/agents/crypto_agent/prompt.md similarity index 100% rename from src/openhuman/agent/agents/crypto_agent/prompt.md rename to src/openhuman/agent_registry/agents/crypto_agent/prompt.md diff --git a/src/openhuman/agent/agents/crypto_agent/prompt.rs b/src/openhuman/agent_registry/agents/crypto_agent/prompt.rs similarity index 100% rename from src/openhuman/agent/agents/crypto_agent/prompt.rs rename to src/openhuman/agent_registry/agents/crypto_agent/prompt.rs diff --git a/src/openhuman/agent/agents/help/agent.toml b/src/openhuman/agent_registry/agents/help/agent.toml similarity index 100% rename from src/openhuman/agent/agents/help/agent.toml rename to src/openhuman/agent_registry/agents/help/agent.toml diff --git a/src/openhuman/agent/agents/help/mod.rs b/src/openhuman/agent_registry/agents/help/mod.rs similarity index 100% rename from src/openhuman/agent/agents/help/mod.rs rename to src/openhuman/agent_registry/agents/help/mod.rs diff --git a/src/openhuman/agent/agents/help/prompt.md b/src/openhuman/agent_registry/agents/help/prompt.md similarity index 100% rename from src/openhuman/agent/agents/help/prompt.md rename to src/openhuman/agent_registry/agents/help/prompt.md diff --git a/src/openhuman/agent/agents/help/prompt.rs b/src/openhuman/agent_registry/agents/help/prompt.rs similarity index 100% rename from src/openhuman/agent/agents/help/prompt.rs rename to src/openhuman/agent_registry/agents/help/prompt.rs diff --git a/src/openhuman/agent/agents/integrations_agent/agent.toml b/src/openhuman/agent_registry/agents/integrations_agent/agent.toml similarity index 100% rename from src/openhuman/agent/agents/integrations_agent/agent.toml rename to src/openhuman/agent_registry/agents/integrations_agent/agent.toml diff --git a/src/openhuman/agent/agents/integrations_agent/mod.rs b/src/openhuman/agent_registry/agents/integrations_agent/mod.rs similarity index 100% rename from src/openhuman/agent/agents/integrations_agent/mod.rs rename to src/openhuman/agent_registry/agents/integrations_agent/mod.rs diff --git a/src/openhuman/agent/agents/integrations_agent/prompt.md b/src/openhuman/agent_registry/agents/integrations_agent/prompt.md similarity index 100% rename from src/openhuman/agent/agents/integrations_agent/prompt.md rename to src/openhuman/agent_registry/agents/integrations_agent/prompt.md diff --git a/src/openhuman/agent/agents/integrations_agent/prompt.rs b/src/openhuman/agent_registry/agents/integrations_agent/prompt.rs similarity index 100% rename from src/openhuman/agent/agents/integrations_agent/prompt.rs rename to src/openhuman/agent_registry/agents/integrations_agent/prompt.rs diff --git a/src/openhuman/agent/agents/loader.rs b/src/openhuman/agent_registry/agents/loader.rs similarity index 99% rename from src/openhuman/agent/agents/loader.rs rename to src/openhuman/agent_registry/agents/loader.rs index ba0161555..d6e901c3f 100644 --- a/src/openhuman/agent/agents/loader.rs +++ b/src/openhuman/agent_registry/agents/loader.rs @@ -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. diff --git a/src/openhuman/agent/agents/markets_agent/agent.toml b/src/openhuman/agent_registry/agents/markets_agent/agent.toml similarity index 100% rename from src/openhuman/agent/agents/markets_agent/agent.toml rename to src/openhuman/agent_registry/agents/markets_agent/agent.toml diff --git a/src/openhuman/agent/agents/markets_agent/mod.rs b/src/openhuman/agent_registry/agents/markets_agent/mod.rs similarity index 100% rename from src/openhuman/agent/agents/markets_agent/mod.rs rename to src/openhuman/agent_registry/agents/markets_agent/mod.rs diff --git a/src/openhuman/agent/agents/markets_agent/prompt.md b/src/openhuman/agent_registry/agents/markets_agent/prompt.md similarity index 100% rename from src/openhuman/agent/agents/markets_agent/prompt.md rename to src/openhuman/agent_registry/agents/markets_agent/prompt.md diff --git a/src/openhuman/agent/agents/markets_agent/prompt.rs b/src/openhuman/agent_registry/agents/markets_agent/prompt.rs similarity index 100% rename from src/openhuman/agent/agents/markets_agent/prompt.rs rename to src/openhuman/agent_registry/agents/markets_agent/prompt.rs diff --git a/src/openhuman/agent/agents/mcp_setup/agent.toml b/src/openhuman/agent_registry/agents/mcp_setup/agent.toml similarity index 100% rename from src/openhuman/agent/agents/mcp_setup/agent.toml rename to src/openhuman/agent_registry/agents/mcp_setup/agent.toml diff --git a/src/openhuman/agent/agents/mcp_setup/mod.rs b/src/openhuman/agent_registry/agents/mcp_setup/mod.rs similarity index 100% rename from src/openhuman/agent/agents/mcp_setup/mod.rs rename to src/openhuman/agent_registry/agents/mcp_setup/mod.rs diff --git a/src/openhuman/agent/agents/mcp_setup/prompt.md b/src/openhuman/agent_registry/agents/mcp_setup/prompt.md similarity index 100% rename from src/openhuman/agent/agents/mcp_setup/prompt.md rename to src/openhuman/agent_registry/agents/mcp_setup/prompt.md diff --git a/src/openhuman/agent/agents/mcp_setup/prompt.rs b/src/openhuman/agent_registry/agents/mcp_setup/prompt.rs similarity index 100% rename from src/openhuman/agent/agents/mcp_setup/prompt.rs rename to src/openhuman/agent_registry/agents/mcp_setup/prompt.rs diff --git a/src/openhuman/agent/agents/mod.rs b/src/openhuman/agent_registry/agents/mod.rs similarity index 100% rename from src/openhuman/agent/agents/mod.rs rename to src/openhuman/agent_registry/agents/mod.rs diff --git a/src/openhuman/agent/agents/morning_briefing/agent.toml b/src/openhuman/agent_registry/agents/morning_briefing/agent.toml similarity index 100% rename from src/openhuman/agent/agents/morning_briefing/agent.toml rename to src/openhuman/agent_registry/agents/morning_briefing/agent.toml diff --git a/src/openhuman/agent/agents/morning_briefing/mod.rs b/src/openhuman/agent_registry/agents/morning_briefing/mod.rs similarity index 100% rename from src/openhuman/agent/agents/morning_briefing/mod.rs rename to src/openhuman/agent_registry/agents/morning_briefing/mod.rs diff --git a/src/openhuman/agent/agents/morning_briefing/prompt.md b/src/openhuman/agent_registry/agents/morning_briefing/prompt.md similarity index 100% rename from src/openhuman/agent/agents/morning_briefing/prompt.md rename to src/openhuman/agent_registry/agents/morning_briefing/prompt.md diff --git a/src/openhuman/agent/agents/morning_briefing/prompt.rs b/src/openhuman/agent_registry/agents/morning_briefing/prompt.rs similarity index 100% rename from src/openhuman/agent/agents/morning_briefing/prompt.rs rename to src/openhuman/agent_registry/agents/morning_briefing/prompt.rs diff --git a/src/openhuman/agent/agents/orchestrator/agent.toml b/src/openhuman/agent_registry/agents/orchestrator/agent.toml similarity index 100% rename from src/openhuman/agent/agents/orchestrator/agent.toml rename to src/openhuman/agent_registry/agents/orchestrator/agent.toml diff --git a/src/openhuman/agent/agents/orchestrator/mod.rs b/src/openhuman/agent_registry/agents/orchestrator/mod.rs similarity index 100% rename from src/openhuman/agent/agents/orchestrator/mod.rs rename to src/openhuman/agent_registry/agents/orchestrator/mod.rs diff --git a/src/openhuman/agent/agents/orchestrator/prompt.md b/src/openhuman/agent_registry/agents/orchestrator/prompt.md similarity index 100% rename from src/openhuman/agent/agents/orchestrator/prompt.md rename to src/openhuman/agent_registry/agents/orchestrator/prompt.md diff --git a/src/openhuman/agent/agents/orchestrator/prompt.rs b/src/openhuman/agent_registry/agents/orchestrator/prompt.rs similarity index 100% rename from src/openhuman/agent/agents/orchestrator/prompt.rs rename to src/openhuman/agent_registry/agents/orchestrator/prompt.rs diff --git a/src/openhuman/agent/agents/planner/agent.toml b/src/openhuman/agent_registry/agents/planner/agent.toml similarity index 100% rename from src/openhuman/agent/agents/planner/agent.toml rename to src/openhuman/agent_registry/agents/planner/agent.toml diff --git a/src/openhuman/agent/agents/planner/mod.rs b/src/openhuman/agent_registry/agents/planner/mod.rs similarity index 100% rename from src/openhuman/agent/agents/planner/mod.rs rename to src/openhuman/agent_registry/agents/planner/mod.rs diff --git a/src/openhuman/agent/agents/planner/prompt.md b/src/openhuman/agent_registry/agents/planner/prompt.md similarity index 100% rename from src/openhuman/agent/agents/planner/prompt.md rename to src/openhuman/agent_registry/agents/planner/prompt.md diff --git a/src/openhuman/agent/agents/planner/prompt.rs b/src/openhuman/agent_registry/agents/planner/prompt.rs similarity index 100% rename from src/openhuman/agent/agents/planner/prompt.rs rename to src/openhuman/agent_registry/agents/planner/prompt.rs diff --git a/src/openhuman/agent/agents/researcher/agent.toml b/src/openhuman/agent_registry/agents/researcher/agent.toml similarity index 100% rename from src/openhuman/agent/agents/researcher/agent.toml rename to src/openhuman/agent_registry/agents/researcher/agent.toml diff --git a/src/openhuman/agent/agents/researcher/mod.rs b/src/openhuman/agent_registry/agents/researcher/mod.rs similarity index 100% rename from src/openhuman/agent/agents/researcher/mod.rs rename to src/openhuman/agent_registry/agents/researcher/mod.rs diff --git a/src/openhuman/agent/agents/researcher/prompt.md b/src/openhuman/agent_registry/agents/researcher/prompt.md similarity index 100% rename from src/openhuman/agent/agents/researcher/prompt.md rename to src/openhuman/agent_registry/agents/researcher/prompt.md diff --git a/src/openhuman/agent/agents/researcher/prompt.rs b/src/openhuman/agent_registry/agents/researcher/prompt.rs similarity index 100% rename from src/openhuman/agent/agents/researcher/prompt.rs rename to src/openhuman/agent_registry/agents/researcher/prompt.rs diff --git a/src/openhuman/agent/agents/skill_creator/agent.toml b/src/openhuman/agent_registry/agents/skill_creator/agent.toml similarity index 100% rename from src/openhuman/agent/agents/skill_creator/agent.toml rename to src/openhuman/agent_registry/agents/skill_creator/agent.toml diff --git a/src/openhuman/agent/agents/skill_creator/mod.rs b/src/openhuman/agent_registry/agents/skill_creator/mod.rs similarity index 100% rename from src/openhuman/agent/agents/skill_creator/mod.rs rename to src/openhuman/agent_registry/agents/skill_creator/mod.rs diff --git a/src/openhuman/agent/agents/skill_creator/prompt.md b/src/openhuman/agent_registry/agents/skill_creator/prompt.md similarity index 100% rename from src/openhuman/agent/agents/skill_creator/prompt.md rename to src/openhuman/agent_registry/agents/skill_creator/prompt.md diff --git a/src/openhuman/agent/agents/skill_creator/prompt.rs b/src/openhuman/agent_registry/agents/skill_creator/prompt.rs similarity index 100% rename from src/openhuman/agent/agents/skill_creator/prompt.rs rename to src/openhuman/agent_registry/agents/skill_creator/prompt.rs diff --git a/src/openhuman/agent/agents/summarizer/agent.toml b/src/openhuman/agent_registry/agents/summarizer/agent.toml similarity index 100% rename from src/openhuman/agent/agents/summarizer/agent.toml rename to src/openhuman/agent_registry/agents/summarizer/agent.toml diff --git a/src/openhuman/agent/agents/summarizer/mod.rs b/src/openhuman/agent_registry/agents/summarizer/mod.rs similarity index 100% rename from src/openhuman/agent/agents/summarizer/mod.rs rename to src/openhuman/agent_registry/agents/summarizer/mod.rs diff --git a/src/openhuman/agent/agents/summarizer/prompt.md b/src/openhuman/agent_registry/agents/summarizer/prompt.md similarity index 100% rename from src/openhuman/agent/agents/summarizer/prompt.md rename to src/openhuman/agent_registry/agents/summarizer/prompt.md diff --git a/src/openhuman/agent/agents/summarizer/prompt.rs b/src/openhuman/agent_registry/agents/summarizer/prompt.rs similarity index 100% rename from src/openhuman/agent/agents/summarizer/prompt.rs rename to src/openhuman/agent_registry/agents/summarizer/prompt.rs diff --git a/src/openhuman/agent/agents/tool_maker/agent.toml b/src/openhuman/agent_registry/agents/tool_maker/agent.toml similarity index 100% rename from src/openhuman/agent/agents/tool_maker/agent.toml rename to src/openhuman/agent_registry/agents/tool_maker/agent.toml diff --git a/src/openhuman/agent/agents/tool_maker/mod.rs b/src/openhuman/agent_registry/agents/tool_maker/mod.rs similarity index 100% rename from src/openhuman/agent/agents/tool_maker/mod.rs rename to src/openhuman/agent_registry/agents/tool_maker/mod.rs diff --git a/src/openhuman/agent/agents/tool_maker/prompt.md b/src/openhuman/agent_registry/agents/tool_maker/prompt.md similarity index 100% rename from src/openhuman/agent/agents/tool_maker/prompt.md rename to src/openhuman/agent_registry/agents/tool_maker/prompt.md diff --git a/src/openhuman/agent/agents/tool_maker/prompt.rs b/src/openhuman/agent_registry/agents/tool_maker/prompt.rs similarity index 100% rename from src/openhuman/agent/agents/tool_maker/prompt.rs rename to src/openhuman/agent_registry/agents/tool_maker/prompt.rs diff --git a/src/openhuman/agent/agents/tools_agent/agent.toml b/src/openhuman/agent_registry/agents/tools_agent/agent.toml similarity index 100% rename from src/openhuman/agent/agents/tools_agent/agent.toml rename to src/openhuman/agent_registry/agents/tools_agent/agent.toml diff --git a/src/openhuman/agent/agents/tools_agent/mod.rs b/src/openhuman/agent_registry/agents/tools_agent/mod.rs similarity index 100% rename from src/openhuman/agent/agents/tools_agent/mod.rs rename to src/openhuman/agent_registry/agents/tools_agent/mod.rs diff --git a/src/openhuman/agent/agents/tools_agent/prompt.md b/src/openhuman/agent_registry/agents/tools_agent/prompt.md similarity index 100% rename from src/openhuman/agent/agents/tools_agent/prompt.md rename to src/openhuman/agent_registry/agents/tools_agent/prompt.md diff --git a/src/openhuman/agent/agents/tools_agent/prompt.rs b/src/openhuman/agent_registry/agents/tools_agent/prompt.rs similarity index 100% rename from src/openhuman/agent/agents/tools_agent/prompt.rs rename to src/openhuman/agent_registry/agents/tools_agent/prompt.rs diff --git a/src/openhuman/agent/agents/trigger_reactor/agent.toml b/src/openhuman/agent_registry/agents/trigger_reactor/agent.toml similarity index 100% rename from src/openhuman/agent/agents/trigger_reactor/agent.toml rename to src/openhuman/agent_registry/agents/trigger_reactor/agent.toml diff --git a/src/openhuman/agent/agents/trigger_reactor/mod.rs b/src/openhuman/agent_registry/agents/trigger_reactor/mod.rs similarity index 100% rename from src/openhuman/agent/agents/trigger_reactor/mod.rs rename to src/openhuman/agent_registry/agents/trigger_reactor/mod.rs diff --git a/src/openhuman/agent/agents/trigger_reactor/prompt.md b/src/openhuman/agent_registry/agents/trigger_reactor/prompt.md similarity index 100% rename from src/openhuman/agent/agents/trigger_reactor/prompt.md rename to src/openhuman/agent_registry/agents/trigger_reactor/prompt.md diff --git a/src/openhuman/agent/agents/trigger_reactor/prompt.rs b/src/openhuman/agent_registry/agents/trigger_reactor/prompt.rs similarity index 100% rename from src/openhuman/agent/agents/trigger_reactor/prompt.rs rename to src/openhuman/agent_registry/agents/trigger_reactor/prompt.rs diff --git a/src/openhuman/agent/agents/trigger_triage/agent.toml b/src/openhuman/agent_registry/agents/trigger_triage/agent.toml similarity index 100% rename from src/openhuman/agent/agents/trigger_triage/agent.toml rename to src/openhuman/agent_registry/agents/trigger_triage/agent.toml diff --git a/src/openhuman/agent/agents/trigger_triage/mod.rs b/src/openhuman/agent_registry/agents/trigger_triage/mod.rs similarity index 100% rename from src/openhuman/agent/agents/trigger_triage/mod.rs rename to src/openhuman/agent_registry/agents/trigger_triage/mod.rs diff --git a/src/openhuman/agent/agents/trigger_triage/prompt.md b/src/openhuman/agent_registry/agents/trigger_triage/prompt.md similarity index 100% rename from src/openhuman/agent/agents/trigger_triage/prompt.md rename to src/openhuman/agent_registry/agents/trigger_triage/prompt.md diff --git a/src/openhuman/agent/agents/trigger_triage/prompt.rs b/src/openhuman/agent_registry/agents/trigger_triage/prompt.rs similarity index 100% rename from src/openhuman/agent/agents/trigger_triage/prompt.rs rename to src/openhuman/agent_registry/agents/trigger_triage/prompt.rs diff --git a/src/openhuman/agent_registry/defaults.rs b/src/openhuman/agent_registry/defaults.rs new file mode 100644 index 000000000..455e7e5fb --- /dev/null +++ b/src/openhuman/agent_registry/defaults.rs @@ -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 { + 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 { + 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 { + 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)); + } +} diff --git a/src/openhuman/agent_registry/mod.rs b/src/openhuman/agent_registry/mod.rs new file mode 100644 index 000000000..370bfb4af --- /dev/null +++ b/src/openhuman/agent_registry/mod.rs @@ -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}; diff --git a/src/openhuman/agent_registry/ops.rs b/src/openhuman/agent_registry/ops.rs new file mode 100644 index 000000000..f7c1ff4a6 --- /dev/null +++ b/src/openhuman/agent_registry/ops.rs @@ -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, 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, 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 { + 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 { + 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 { + update_agent( + id, + AgentRegistryPatch { + enabled: Some(enabled), + ..AgentRegistryPatch::default() + }, + ) + .await +} + +pub async fn remove_agent(id: &str) -> Result { + 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 { + let mut default_order = Vec::new(); + let mut merged: HashMap = 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" + ); + } +} diff --git a/src/openhuman/agent_registry/rpc.rs b/src/openhuman/agent_registry/rpc.rs new file mode 100644 index 000000000..277ec0f91 --- /dev/null +++ b/src/openhuman/agent_registry/rpc.rs @@ -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, +} + +pub async fn list_rpc(req: ListRequest) -> Result, 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, +} + +pub async fn get_rpc(req: GetRequest) -> Result, 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, + #[serde(default)] + pub model: Option, + #[serde(default)] + pub system_prompt: Option, + #[serde(default)] + pub tool_allowlist: Vec, + #[serde(default)] + pub tool_denylist: Vec, + #[serde(default)] + pub subagents: Vec, + #[serde(default)] + pub tags: Vec, + #[serde(default)] + pub metadata: Option, +} + +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, 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, 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, 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, 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, String> { + Ok(RpcOutcome::new( + RemoveResponse { + removed: ops::remove_agent(&req.id).await?, + }, + vec![], + )) +} diff --git a/src/openhuman/agent_registry/schemas.rs b/src/openhuman/agent_registry/schemas.rs new file mode 100644 index 000000000..31846b72d --- /dev/null +++ b/src/openhuman/agent_registry/schemas.rs @@ -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 { + vec![ + schemas("list"), + schemas("get"), + schemas("create_custom"), + schemas("upsert_custom"), + schemas("update"), + schemas("set_enabled"), + schemas("remove"), + ] +} + +pub fn all_registered_controllers() -> Vec { + 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) -> ControllerFuture { + Box::pin(async move { + let req = parse_value::(Value::Object(params))?; + to_json(rpc::list_rpc(req).await?) + }) +} + +fn handle_get(params: Map) -> ControllerFuture { + Box::pin(async move { + let req = parse_value::(Value::Object(params))?; + to_json(rpc::get_rpc(req).await?) + }) +} + +fn handle_create_custom(params: Map) -> ControllerFuture { + Box::pin(async move { + let req = parse_value::(Value::Object(params))?; + to_json(rpc::create_custom_rpc(req).await?) + }) +} + +fn handle_upsert_custom(params: Map) -> ControllerFuture { + Box::pin(async move { + let req = parse_value::(Value::Object(params))?; + to_json(rpc::upsert_custom_rpc(req).await?) + }) +} + +fn handle_update(params: Map) -> ControllerFuture { + Box::pin(async move { + let req = parse_value::(Value::Object(params))?; + to_json(rpc::update_rpc(req).await?) + }) +} + +fn handle_set_enabled(params: Map) -> ControllerFuture { + Box::pin(async move { + let req = parse_value::(Value::Object(params))?; + to_json(rpc::set_enabled_rpc(req).await?) + }) +} + +fn handle_remove(params: Map) -> ControllerFuture { + Box::pin(async move { + let req = parse_value::(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(v: Value) -> Result { + serde_json::from_value(v).map_err(|e| format!("invalid params: {e}")) +} + +fn to_json(outcome: RpcOutcome) -> Result { + 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"); + } +} diff --git a/src/openhuman/agent_registry/tools.rs b/src/openhuman/agent_registry/tools.rs new file mode 100644 index 000000000..00c30b369 --- /dev/null +++ b/src/openhuman/agent_registry/tools.rs @@ -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; diff --git a/src/openhuman/agent/tools/archetype_delegation.rs b/src/openhuman/agent_registry/tools/archetype_delegation.rs similarity index 100% rename from src/openhuman/agent/tools/archetype_delegation.rs rename to src/openhuman/agent_registry/tools/archetype_delegation.rs diff --git a/src/openhuman/agent/tools/dispatch.rs b/src/openhuman/agent_registry/tools/dispatch.rs similarity index 98% rename from src/openhuman/agent/tools/dispatch.rs rename to src/openhuman/agent_registry/tools/dispatch.rs index fd181506e..42518bb14 100644 --- a/src/openhuman/agent/tools/dispatch.rs +++ b/src/openhuman/agent_registry/tools/dispatch.rs @@ -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() { diff --git a/src/openhuman/agent/tools/skill_delegation.rs b/src/openhuman/agent_registry/tools/skill_delegation.rs similarity index 100% rename from src/openhuman/agent/tools/skill_delegation.rs rename to src/openhuman/agent_registry/tools/skill_delegation.rs diff --git a/src/openhuman/agent/tools/spawn_parallel_agents.rs b/src/openhuman/agent_registry/tools/spawn_parallel_agents.rs similarity index 100% rename from src/openhuman/agent/tools/spawn_parallel_agents.rs rename to src/openhuman/agent_registry/tools/spawn_parallel_agents.rs diff --git a/src/openhuman/agent/tools/spawn_parallel_agents_tests.rs b/src/openhuman/agent_registry/tools/spawn_parallel_agents_tests.rs similarity index 100% rename from src/openhuman/agent/tools/spawn_parallel_agents_tests.rs rename to src/openhuman/agent_registry/tools/spawn_parallel_agents_tests.rs diff --git a/src/openhuman/agent/tools/spawn_subagent.rs b/src/openhuman/agent_registry/tools/spawn_subagent.rs similarity index 100% rename from src/openhuman/agent/tools/spawn_subagent.rs rename to src/openhuman/agent_registry/tools/spawn_subagent.rs diff --git a/src/openhuman/agent/tools/spawn_worker_thread.rs b/src/openhuman/agent_registry/tools/spawn_worker_thread.rs similarity index 100% rename from src/openhuman/agent/tools/spawn_worker_thread.rs rename to src/openhuman/agent_registry/tools/spawn_worker_thread.rs diff --git a/src/openhuman/agent_registry/types.rs b/src/openhuman/agent_registry/types.rs new file mode 100644 index 000000000..5f4f06b08 --- /dev/null +++ b/src/openhuman/agent_registry/types.rs @@ -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, + /// 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, + /// Tool names this agent may use. `"*"` means all currently visible tools. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub tool_allowlist: Vec, + /// Tool names that must be hidden even if they match the allowlist. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub tool_denylist: Vec, + /// Optional delegated agent ids this agent may spawn. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub subagents: Vec, + /// Search/filter tags for UI grouping. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub tags: Vec, + /// 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, +} + +#[derive(Debug, Clone, Default, Deserialize)] +pub struct AgentRegistryPatch { + #[serde(default)] + pub name: Option, + #[serde(default)] + pub description: Option, + #[serde(default)] + pub enabled: Option, + #[serde(default)] + pub model: Option, + #[serde(default)] + pub system_prompt: Option, + #[serde(default)] + pub tool_allowlist: Option>, + #[serde(default)] + pub tool_denylist: Option>, + #[serde(default)] + pub subagents: Option>, + #[serde(default)] + pub tags: Option>, + #[serde(default)] + pub metadata: Option, +} diff --git a/src/openhuman/config/schema/types.rs b/src/openhuman/config/schema/types.rs index 7d84751db..4202ece42 100644 --- a/src/openhuman/config/schema/types.rs +++ b/src/openhuman/config/schema/types.rs @@ -229,6 +229,11 @@ pub struct Config { #[serde(default)] pub memory_sources: Vec, + /// 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(), diff --git a/src/openhuman/mcp_server/resources.rs b/src/openhuman/mcp_server/resources.rs index 262847ecb..89b67d15f 100644 --- a/src/openhuman/mcp_server/resources.rs +++ b/src/openhuman/mcp_server/resources.rs @@ -53,109 +53,109 @@ const RESOURCE_CATALOG: &[PromptResource] = &[ uri: "openhuman://prompts/agents/orchestrator", name: "orchestrator", description: "Chat-tier orchestrator that routes tasks to specialist subagents.", - content: include_str!("../agent/agents/orchestrator/prompt.md"), + content: include_str!("../agent_registry/agents/orchestrator/prompt.md"), }, PromptResource { uri: "openhuman://prompts/agents/planner", name: "planner", description: "Reasoning-tier planner that grounds multi-step plans in integration data.", - content: include_str!("../agent/agents/planner/prompt.md"), + content: include_str!("../agent_registry/agents/planner/prompt.md"), }, PromptResource { uri: "openhuman://prompts/agents/code_executor", name: "code_executor", description: "Sandboxed worker that writes and executes code.", - content: include_str!("../agent/agents/code_executor/prompt.md"), + content: include_str!("../agent_registry/agents/code_executor/prompt.md"), }, PromptResource { uri: "openhuman://prompts/agents/integrations_agent", name: "integrations_agent", description: "Worker that executes Composio integration actions.", - content: include_str!("../agent/agents/integrations_agent/prompt.md"), + content: include_str!("../agent_registry/agents/integrations_agent/prompt.md"), }, PromptResource { uri: "openhuman://prompts/agents/crypto_agent", name: "crypto_agent", description: "Specialist worker for wallet and on-chain operations.", - content: include_str!("../agent/agents/crypto_agent/prompt.md"), + content: include_str!("../agent_registry/agents/crypto_agent/prompt.md"), }, PromptResource { uri: "openhuman://prompts/agents/markets_agent", name: "markets_agent", description: "Specialist worker for prediction-market venues (Polymarket, Kalshi).", - content: include_str!("../agent/agents/markets_agent/prompt.md"), + content: include_str!("../agent_registry/agents/markets_agent/prompt.md"), }, PromptResource { uri: "openhuman://prompts/agents/tools_agent", name: "tools_agent", description: "Generalist worker with access to the full tool surface.", - content: include_str!("../agent/agents/tools_agent/prompt.md"), + content: include_str!("../agent_registry/agents/tools_agent/prompt.md"), }, PromptResource { uri: "openhuman://prompts/agents/tool_maker", name: "tool_maker", description: "Sandboxed worker that creates new tools from descriptions.", - content: include_str!("../agent/agents/tool_maker/prompt.md"), + content: include_str!("../agent_registry/agents/tool_maker/prompt.md"), }, PromptResource { uri: "openhuman://prompts/agents/skill_creator", name: "skill_creator", description: "Sandboxed worker that authors and publishes skill packages.", - content: include_str!("../agent/agents/skill_creator/prompt.md"), + content: include_str!("../agent_registry/agents/skill_creator/prompt.md"), }, PromptResource { uri: "openhuman://prompts/agents/researcher", name: "researcher", description: "Worker that searches the web and synthesises research findings.", - content: include_str!("../agent/agents/researcher/prompt.md"), + content: include_str!("../agent_registry/agents/researcher/prompt.md"), }, PromptResource { uri: "openhuman://prompts/agents/critic", name: "critic", description: "Read-only worker that critiques plans and outputs.", - content: include_str!("../agent/agents/critic/prompt.md"), + content: include_str!("../agent_registry/agents/critic/prompt.md"), }, PromptResource { uri: "openhuman://prompts/agents/archivist", name: "archivist", description: "Background worker that distils conversations into persistent memory.", - content: include_str!("../agent/agents/archivist/prompt.md"), + content: include_str!("../agent_registry/agents/archivist/prompt.md"), }, PromptResource { uri: "openhuman://prompts/agents/trigger_triage", name: "trigger_triage", description: "Read-only worker that classifies incoming automation triggers.", - content: include_str!("../agent/agents/trigger_triage/prompt.md"), + content: include_str!("../agent_registry/agents/trigger_triage/prompt.md"), }, PromptResource { uri: "openhuman://prompts/agents/trigger_reactor", name: "trigger_reactor", description: "Worker that executes actions in response to classified triggers.", - content: include_str!("../agent/agents/trigger_reactor/prompt.md"), + content: include_str!("../agent_registry/agents/trigger_reactor/prompt.md"), }, PromptResource { uri: "openhuman://prompts/agents/morning_briefing", name: "morning_briefing", description: "Read-only worker that assembles a personalised morning briefing.", - content: include_str!("../agent/agents/morning_briefing/prompt.md"), + content: include_str!("../agent_registry/agents/morning_briefing/prompt.md"), }, PromptResource { uri: "openhuman://prompts/agents/summarizer", name: "summarizer", description: "Worker that condenses long documents or conversations.", - content: include_str!("../agent/agents/summarizer/prompt.md"), + content: include_str!("../agent_registry/agents/summarizer/prompt.md"), }, PromptResource { uri: "openhuman://prompts/agents/help", name: "help", description: "Read-only worker that answers questions from documentation.", - content: include_str!("../agent/agents/help/prompt.md"), + content: include_str!("../agent_registry/agents/help/prompt.md"), }, PromptResource { uri: "openhuman://prompts/agents/mcp_setup", name: "mcp_setup", description: "Worker that guides the user through MCP client configuration.", - content: include_str!("../agent/agents/mcp_setup/prompt.md"), + content: include_str!("../agent_registry/agents/mcp_setup/prompt.md"), }, ]; @@ -237,7 +237,7 @@ mod tests { #[test] fn catalog_mirrors_builtins() { - use crate::openhuman::agent::agents::BUILTINS; + use crate::openhuman::agent_registry::agents::BUILTINS; for b in BUILTINS { let expected_uri = format!("openhuman://prompts/agents/{}", b.id); @@ -328,7 +328,7 @@ mod tests { #[test] fn read_resource_returns_content_for_each_subagent() { - use crate::openhuman::agent::agents::BUILTINS; + use crate::openhuman::agent_registry::agents::BUILTINS; for b in BUILTINS { let uri = format!("openhuman://prompts/agents/{}", b.id); let params = json!({ "uri": uri }); diff --git a/src/openhuman/memory_sync/composio/bus.rs b/src/openhuman/memory_sync/composio/bus.rs index 0f6f5516f..b78e38146 100644 --- a/src/openhuman/memory_sync/composio/bus.rs +++ b/src/openhuman/memory_sync/composio/bus.rs @@ -20,7 +20,7 @@ //! published as `TriggerEvaluated` / `TriggerEscalated` events on //! the bus. //! -//! [trigger_triage]: crate::openhuman::agent::agents +//! [trigger_triage]: crate::openhuman::agent_registry::agents //! //! ## Feature flag //! diff --git a/src/openhuman/mod.rs b/src/openhuman/mod.rs index e279c9342..17153f0b3 100644 --- a/src/openhuman/mod.rs +++ b/src/openhuman/mod.rs @@ -18,6 +18,7 @@ pub mod about_app; pub mod accessibility; pub mod agent; pub mod agent_experience; +pub mod agent_registry; pub mod agent_tool_policy; pub mod app_state; pub mod approval; diff --git a/src/openhuman/redirect_links/README.md b/src/openhuman/redirect_links/README.md index b247632b0..1f5d8984d 100644 --- a/src/openhuman/redirect_links/README.md +++ b/src/openhuman/redirect_links/README.md @@ -76,7 +76,7 @@ Insert is atomic (`ON CONFLICT DO NOTHING`) so concurrent shortens of the same U - **Ids are content-addressed, not random**: same URL → same id (deterministic, deduped). Removing a link and re-shortening the same URL yields the same id again. - **Length threshold guards token waste**: the placeholder is ~24 bytes, so URLs below `DEFAULT_MIN_URL_LEN` (80) are left untouched by inbound rewrite. -- **Trailing punctuation handling**: inbound rewrite and public-link tagging strip trailing `. , ; : !` so prose like "see an HTTPS URL ending in a period." doesn't capture the period into the stored/tagged URL. +- **Trailing punctuation handling**: inbound rewrite and public-link tagging strip trailing `. , ; : !` so prose with a URL followed by a period doesn't capture the period into the stored/tagged URL. - **`append_user_id_to_public_links` is anchored to `openhm.xyz`** specifically and rejects lookalikes (`evil-openhm.xyz`, `openhm.xyz.evil.com`); it splits off `#fragment` so `?u=` always lands in the query, and is idempotent against existing `?u=`/`&u=`. - **`id_from_short`** accepts both `openhuman://link/` and a bare hex ``, lowercasing the result; non-hex input returns `None`. - **No agent tools, no event-bus subscribers, no `bus.rs`/`tools.rs`** — this domain is store + ops + RPC only. diff --git a/src/openhuman/skills/README.md b/src/openhuman/skills/README.md index 2ce896088..2464c3684 100644 --- a/src/openhuman/skills/README.md +++ b/src/openhuman/skills/README.md @@ -23,7 +23,7 @@ Discovery, parsing, and per-turn injection of agentskills.io-style skills (a dir - `src/openhuman/tools/traits.rs` — `ToolResult` / `ToolContent` shape shared with the tool registry. - `src/openhuman/workspace/ops.rs` — workspace bootstrap touches the skill directory layout. -- `src/openhuman/agent/agents/integrations_agent/prompt.rs` — integrations agent reads the skill catalog. +- `src/openhuman/agent_registry/agents/integrations_agent/prompt.rs` — integrations agent reads the skill catalog. - `src/openhuman/agent/harness/fork_context.rs` — fork context propagates injected skills. - `src/openhuman/agent/harness/session/turn.rs` — per-turn injection point. - `src/openhuman/agent/prompts/{mod,types}.rs` — render `## Available Skills` catalog section. diff --git a/src/openhuman/skills/registry.rs b/src/openhuman/skills/registry.rs index b55a7d226..5016f7ee0 100644 --- a/src/openhuman/skills/registry.rs +++ b/src/openhuman/skills/registry.rs @@ -181,7 +181,7 @@ pub fn load_skills(workspace_dir: &Path) -> Vec { let mut skills: Vec = Vec::new(); - if let Ok(builtins) = crate::openhuman::agent::agents::load_builtins() { + if let Ok(builtins) = crate::openhuman::agent_registry::agents::load_builtins() { for definition in builtins { skills.push(SkillDefinition { definition, diff --git a/src/openhuman/tools/mod.rs b/src/openhuman/tools/mod.rs index 80ab955aa..1c5bf2987 100644 --- a/src/openhuman/tools/mod.rs +++ b/src/openhuman/tools/mod.rs @@ -12,6 +12,7 @@ pub(crate) mod user_filter; pub(crate) mod implementations; pub use crate::openhuman::agent::tools::*; +pub use crate::openhuman::agent_registry::tools::*; pub use crate::openhuman::audio_toolkit::tools::*; pub use crate::openhuman::codegraph::tools::*; pub use crate::openhuman::composio::tools::*; diff --git a/tests/agent_retrieval_e2e.rs b/tests/agent_retrieval_e2e.rs index e5ebfa3be..10c81b1c2 100644 --- a/tests/agent_retrieval_e2e.rs +++ b/tests/agent_retrieval_e2e.rs @@ -155,7 +155,7 @@ fn alice_phoenix_thread() -> EmailThread { /// TOML was updated accordingly. #[test] fn orchestrator_lists_memory_tree_tools() { - let toml = include_str!("../src/openhuman/agent/agents/orchestrator/agent.toml"); + let toml = include_str!("../src/openhuman/agent_registry/agents/orchestrator/agent.toml"); // Exact entry match — substring match would also hit comments or prefixed names. let has_memory_tree_entry = toml .lines() diff --git a/tests/json_rpc_e2e.rs b/tests/json_rpc_e2e.rs index a77e8f799..1b1a0b041 100644 --- a/tests/json_rpc_e2e.rs +++ b/tests/json_rpc_e2e.rs @@ -992,6 +992,517 @@ async fn json_rpc_tool_registry_lists_and_gets_entries() { rpc_join.abort(); } +#[tokio::test] +async fn json_rpc_agent_registry_manages_defaults_and_custom_agents() { + let _env_lock = json_rpc_e2e_env_lock(); + let tmp = tempdir().expect("tempdir"); + let home = tmp.path(); + let openhuman_home = home.join(".openhuman"); + + let _home_guard = EnvVarGuard::set_to_path("HOME", home); + let _workspace_guard = EnvVarGuard::unset("OPENHUMAN_WORKSPACE"); + let _backend_url_guard = EnvVarGuard::unset("BACKEND_URL"); + let _vite_backend_guard = EnvVarGuard::unset("VITE_BACKEND_URL"); + + write_min_config(&openhuman_home, "http://127.0.0.1:9"); + + let (rpc_addr, rpc_join) = serve_on_ephemeral(build_core_http_router(false)).await; + let rpc_base = format!("http://{rpc_addr}"); + + let list = post_json_rpc( + &rpc_base, + 2862_1, + "openhuman.agent_registry_list", + json!({ "include_disabled": true }), + ) + .await; + let list_result = assert_no_jsonrpc_error(&list, "agent_registry_list"); + let agents = list_result + .get("agents") + .and_then(Value::as_array) + .expect("agent registry list should return agents array"); + let orchestrator = agents + .iter() + .find(|agent| agent.get("id").and_then(Value::as_str) == Some("orchestrator")) + .expect("default registry should include orchestrator"); + assert_eq!( + orchestrator.get("source").and_then(Value::as_str), + Some("default") + ); + assert_eq!( + orchestrator.get("enabled").and_then(Value::as_bool), + Some(true) + ); + + let missing = post_json_rpc( + &rpc_base, + 2862_10, + "openhuman.agent_registry_get", + json!({ "id": "does_not_exist" }), + ) + .await; + assert!( + assert_no_jsonrpc_error(&missing, "agent_registry_get missing")["agent"].is_null(), + "missing agents should return agent:null" + ); + + let update_default = post_json_rpc( + &rpc_base, + 2862_11, + "openhuman.agent_registry_update", + json!({ + "id": "researcher", + "name": "Research Specialist", + "description": "Workspace-specific research specialist.", + "model": "reasoning-v1", + "tool_allowlist": ["tools.web_search", "memory.search"], + "tool_denylist": ["wallet.execute_prepared"], + "tags": ["research", "workspace"], + "metadata": { "pinned_by": "json_rpc_e2e" } + }), + ) + .await; + let update_default_agent = + assert_no_jsonrpc_error(&update_default, "agent_registry_update default") + .get("agent") + .expect("update default should return agent"); + assert_eq!( + update_default_agent.get("name").and_then(Value::as_str), + Some("Research Specialist") + ); + assert_eq!( + update_default_agent + .get("metadata") + .and_then(|metadata| metadata.get("pinned_by")) + .and_then(Value::as_str), + Some("json_rpc_e2e") + ); + + let update_missing = post_json_rpc( + &rpc_base, + 2862_12, + "openhuman.agent_registry_update", + json!({ "id": "missing_agent", "enabled": false }), + ) + .await; + let update_missing_error = + assert_jsonrpc_error(&update_missing, "agent_registry_update missing"); + assert!( + update_missing_error + .get("message") + .and_then(Value::as_str) + .unwrap_or_default() + .contains("not found"), + "unexpected missing-update error: {update_missing_error}" + ); + + let disabled = post_json_rpc( + &rpc_base, + 2862_2, + "openhuman.agent_registry_set_enabled", + json!({ "id": "code_executor", "enabled": false }), + ) + .await; + let disabled_result = assert_no_jsonrpc_error(&disabled, "agent_registry_set_enabled"); + assert_eq!( + disabled_result + .get("agent") + .and_then(|agent| agent.get("id")) + .and_then(Value::as_str), + Some("code_executor") + ); + assert_eq!( + disabled_result + .get("agent") + .and_then(|agent| agent.get("enabled")) + .and_then(Value::as_bool), + Some(false) + ); + + let visible = post_json_rpc( + &rpc_base, + 2862_3, + "openhuman.agent_registry_list", + json!({}), + ) + .await; + let visible_result = assert_no_jsonrpc_error(&visible, "agent_registry_list visible"); + let visible_agents = visible_result + .get("agents") + .and_then(Value::as_array) + .expect("agent registry list should return visible agents array"); + assert!( + !visible_agents + .iter() + .any(|agent| agent.get("id").and_then(Value::as_str) == Some("code_executor")), + "disabled default agent should be hidden unless include_disabled=true" + ); + + let all_after_disable = post_json_rpc( + &rpc_base, + 2862_13, + "openhuman.agent_registry_list", + json!({ "include_disabled": true }), + ) + .await; + let all_after_disable_result = + assert_no_jsonrpc_error(&all_after_disable, "agent_registry_list include disabled"); + let disabled_code_executor = all_after_disable_result + .get("agents") + .and_then(Value::as_array) + .and_then(|agents| { + agents + .iter() + .find(|agent| agent.get("id").and_then(Value::as_str) == Some("code_executor")) + }) + .expect("include_disabled should retain disabled code_executor"); + assert_eq!( + disabled_code_executor + .get("enabled") + .and_then(Value::as_bool), + Some(false) + ); + + let reenabled = post_json_rpc( + &rpc_base, + 2862_14, + "openhuman.agent_registry_set_enabled", + json!({ "id": "code_executor", "enabled": true }), + ) + .await; + assert_eq!( + assert_no_jsonrpc_error(&reenabled, "agent_registry_set_enabled reenable") + .get("agent") + .and_then(|agent| agent.get("enabled")) + .and_then(Value::as_bool), + Some(true) + ); + + let disabled_orchestrator = post_json_rpc( + &rpc_base, + 2862_31, + "openhuman.agent_registry_set_enabled", + json!({ "id": "orchestrator", "enabled": false }), + ) + .await; + let disabled_orchestrator_error = assert_jsonrpc_error( + &disabled_orchestrator, + "agent_registry_set_enabled orchestrator", + ); + assert!( + disabled_orchestrator_error + .get("message") + .and_then(Value::as_str) + .unwrap_or_default() + .contains("orchestrator agent cannot be disabled"), + "unexpected orchestrator-disable error: {disabled_orchestrator_error}" + ); + + let update_orchestrator_disabled = post_json_rpc( + &rpc_base, + 2862_32, + "openhuman.agent_registry_update", + json!({ "id": "orchestrator", "enabled": false }), + ) + .await; + let update_orchestrator_error = assert_jsonrpc_error( + &update_orchestrator_disabled, + "agent_registry_update orchestrator disabled", + ); + assert!( + update_orchestrator_error + .get("message") + .and_then(Value::as_str) + .unwrap_or_default() + .contains("orchestrator agent cannot be disabled"), + "unexpected orchestrator-update error: {update_orchestrator_error}" + ); + + let created = post_json_rpc( + &rpc_base, + 2862_4, + "openhuman.agent_registry_create_custom", + json!({ + "id": "custom_writer", + "name": "Custom Writer", + "description": "Drafts polished workspace updates.", + "model": "reasoning-v1", + "system_prompt": "Write concise, accurate updates.", + "tool_allowlist": ["memory.search", "tools.web_search"], + "tool_denylist": ["wallet.execute_prepared"], + "tags": ["writing", "custom"], + "metadata": { "created_by": "json_rpc_e2e" } + }), + ) + .await; + let created_result = assert_no_jsonrpc_error(&created, "agent_registry_create_custom"); + let custom = created_result + .get("agent") + .expect("create_custom should return agent"); + assert_eq!( + custom.get("id").and_then(Value::as_str), + Some("custom_writer") + ); + assert_eq!(custom.get("source").and_then(Value::as_str), Some("custom")); + assert_eq!(custom.get("enabled").and_then(Value::as_bool), Some(true)); + assert_eq!( + custom + .get("tool_allowlist") + .and_then(Value::as_array) + .and_then(|tools| tools.first()) + .and_then(Value::as_str), + Some("memory.search") + ); + + let get_custom = post_json_rpc( + &rpc_base, + 2862_5, + "openhuman.agent_registry_get", + json!({ "id": "custom_writer" }), + ) + .await; + let get_custom_result = assert_no_jsonrpc_error(&get_custom, "agent_registry_get custom"); + assert_eq!( + get_custom_result + .get("agent") + .and_then(|agent| agent.get("metadata")) + .and_then(|metadata| metadata.get("created_by")) + .and_then(Value::as_str), + Some("json_rpc_e2e") + ); + + let updated_custom = post_json_rpc( + &rpc_base, + 2862_15, + "openhuman.agent_registry_update", + json!({ + "id": "custom_writer", + "name": "Custom Writer v2", + "description": "Drafts polished workspace updates and summaries.", + "enabled": false, + "model": "coding-v1", + "system_prompt": "Write concise updates with citations when available.", + "tool_allowlist": ["memory.search"], + "tool_denylist": ["shell"], + "subagents": ["researcher"], + "tags": ["writing", "custom", "disabled"], + "metadata": { "updated_by": "json_rpc_e2e" } + }), + ) + .await; + let updated_custom_agent = + assert_no_jsonrpc_error(&updated_custom, "agent_registry_update custom") + .get("agent") + .expect("custom update should return agent"); + assert_eq!( + updated_custom_agent.get("name").and_then(Value::as_str), + Some("Custom Writer v2") + ); + assert_eq!( + updated_custom_agent.get("enabled").and_then(Value::as_bool), + Some(false) + ); + assert_eq!( + updated_custom_agent + .get("subagents") + .and_then(Value::as_array) + .and_then(|subagents| subagents.first()) + .and_then(Value::as_str), + Some("researcher") + ); + + let reenabled_custom = post_json_rpc( + &rpc_base, + 2862_16, + "openhuman.agent_registry_set_enabled", + json!({ "id": "custom_writer", "enabled": true }), + ) + .await; + assert_eq!( + assert_no_jsonrpc_error(&reenabled_custom, "agent_registry_set_enabled custom") + .get("agent") + .and_then(|agent| agent.get("enabled")) + .and_then(Value::as_bool), + Some(true) + ); + + let full_upsert = post_json_rpc( + &rpc_base, + 2862_17, + "openhuman.agent_registry_upsert_custom", + json!({ + "agent": { + "id": "custom_reviewer", + "name": "Custom Reviewer", + "description": "Reviews agent plans before execution.", + "source": "default", + "enabled": false, + "model": "reasoning-v1", + "system_prompt": "Review plans for missing validation.", + "tool_allowlist": ["memory.search"], + "tool_denylist": ["shell", "file_write"], + "subagents": ["critic"], + "tags": ["review"], + "metadata": { "entry_shape": "full" } + } + }), + ) + .await; + let full_upsert_agent = assert_no_jsonrpc_error(&full_upsert, "agent_registry_upsert_custom") + .get("agent") + .expect("upsert_custom should return agent"); + assert_eq!( + full_upsert_agent.get("id").and_then(Value::as_str), + Some("custom_reviewer") + ); + assert_eq!( + full_upsert_agent.get("source").and_then(Value::as_str), + Some("custom"), + "upsert_custom should force source=custom even if caller sends another source" + ); + assert_eq!( + full_upsert_agent.get("enabled").and_then(Value::as_bool), + Some(false) + ); + + let visible_after_custom_disable = post_json_rpc( + &rpc_base, + 2862_18, + "openhuman.agent_registry_list", + json!({}), + ) + .await; + let visible_after_custom_disable_result = assert_no_jsonrpc_error( + &visible_after_custom_disable, + "agent_registry_list hides disabled custom", + ); + assert!( + !visible_after_custom_disable_result + .get("agents") + .and_then(Value::as_array) + .expect("agent_registry_list should return agents array") + .iter() + .any(|agent| agent.get("id").and_then(Value::as_str) == Some("custom_reviewer")), + "disabled custom agent should be hidden from default list" + ); + + let default_collision = post_json_rpc( + &rpc_base, + 2862_6, + "openhuman.agent_registry_create_custom", + json!({ + "id": "orchestrator", + "name": "Bad Override", + "description": "Should not replace default agents through custom create." + }), + ) + .await; + let collision_error = assert_jsonrpc_error( + &default_collision, + "agent_registry_create_custom default collision", + ); + assert!( + collision_error + .get("message") + .and_then(Value::as_str) + .unwrap_or_default() + .contains("default agent"), + "unexpected default-collision error: {collision_error}" + ); + + let removed_reviewer = post_json_rpc( + &rpc_base, + 2862_19, + "openhuman.agent_registry_remove", + json!({ "id": "custom_reviewer" }), + ) + .await; + assert_eq!( + assert_no_jsonrpc_error(&removed_reviewer, "agent_registry_remove custom reviewer") + .get("removed") + .and_then(Value::as_bool), + Some(true) + ); + + let removed_custom = post_json_rpc( + &rpc_base, + 2862_7, + "openhuman.agent_registry_remove", + json!({ "id": "custom_writer" }), + ) + .await; + let removed_custom_result = + assert_no_jsonrpc_error(&removed_custom, "agent_registry_remove custom"); + assert_eq!( + removed_custom_result + .get("removed") + .and_then(Value::as_bool), + Some(true) + ); + + let removed_missing = post_json_rpc( + &rpc_base, + 2862_20, + "openhuman.agent_registry_remove", + json!({ "id": "missing_agent" }), + ) + .await; + assert_eq!( + assert_no_jsonrpc_error(&removed_missing, "agent_registry_remove missing") + .get("removed") + .and_then(Value::as_bool), + Some(false) + ); + + let reset_default = post_json_rpc( + &rpc_base, + 2862_21, + "openhuman.agent_registry_remove", + json!({ "id": "researcher" }), + ) + .await; + assert_eq!( + assert_no_jsonrpc_error(&reset_default, "agent_registry_remove default override") + .get("removed") + .and_then(Value::as_bool), + Some(true) + ); + + let reset_code_executor = post_json_rpc( + &rpc_base, + 2862_22, + "openhuman.agent_registry_remove", + json!({ "id": "code_executor" }), + ) + .await; + assert_eq!( + assert_no_jsonrpc_error( + &reset_code_executor, + "agent_registry_remove code_executor override" + ) + .get("removed") + .and_then(Value::as_bool), + Some(true) + ); + + let code_executor = post_json_rpc( + &rpc_base, + 2862_23, + "openhuman.agent_registry_get", + json!({ "id": "code_executor" }), + ) + .await; + assert_eq!( + assert_no_jsonrpc_error(&code_executor, "agent_registry_get reset default") + .get("agent") + .and_then(|agent| agent.get("enabled")) + .and_then(Value::as_bool), + Some(true) + ); + + rpc_join.abort(); +} + #[tokio::test] async fn json_rpc_protocol_auth_and_agent_hello() { let _env_lock = json_rpc_e2e_env_lock();