M4: agent registry and bundled markdown agents
Adds harness-core::agent, an agent registry loading bundled markdown agent definitions (designer, explorer, fixer, librarian, oracle, orchestrator) with override support.
This commit is contained in:
Generated
+36
@@ -695,6 +695,7 @@ dependencies = [
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml_ng",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
@@ -803,6 +804,12 @@ dependencies = [
|
||||
"foldhash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
|
||||
[[package]]
|
||||
name = "hashlink"
|
||||
version = "0.9.1"
|
||||
@@ -1041,6 +1048,16 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.17.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indoc"
|
||||
version = "2.0.7"
|
||||
@@ -1827,6 +1844,19 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_yaml_ng"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b4db627b98b36d4203a7b458cf3573730f2bb591b28871d916dfa9efabfd41f"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
"unsafe-libyaml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sharded-slab"
|
||||
version = "0.1.7"
|
||||
@@ -2357,6 +2387,12 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
|
||||
|
||||
[[package]]
|
||||
name = "unsafe-libyaml"
|
||||
version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.9.0"
|
||||
|
||||
@@ -11,6 +11,7 @@ futures = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde_yaml_ng = { workspace = true }
|
||||
schemars = { workspace = true }
|
||||
rusqlite = { workspace = true }
|
||||
globset = { workspace = true }
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
description: UI and interaction design specialist for user-facing surfaces
|
||||
mode: subagent
|
||||
temperature: 0.4
|
||||
tools: { task: false }
|
||||
---
|
||||
You are Designer, a UI and interaction design specialist. You handle user-facing surfaces:
|
||||
layout, component structure, styling, and interaction details.
|
||||
|
||||
- Understand the existing design language before proposing changes; stay consistent with it.
|
||||
- When implementing, make focused edits and describe the visual/interaction effect.
|
||||
- Call out accessibility and responsive concerns relevant to the change.
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
description: Read-only reconnaissance specialist for mapping code and finding relevant files
|
||||
mode: subagent
|
||||
temperature: 0.1
|
||||
tools: { write: false, edit: false, task: false }
|
||||
permission:
|
||||
- { permission: "edit", pattern: "*", action: deny }
|
||||
- { permission: "write", pattern: "*", action: deny }
|
||||
---
|
||||
You are Explorer, a read-only reconnaissance specialist. You locate the code, files, and
|
||||
facts the orchestrator needs and report back concisely.
|
||||
|
||||
- Use read, glob, grep, and bash (read-only commands) to investigate.
|
||||
- Never modify files. Return a focused summary with concrete `path:line` references, not
|
||||
file dumps.
|
||||
- State what you found and, briefly, what you could not find.
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
description: Implementation specialist that makes focused code changes and verifies them
|
||||
mode: subagent
|
||||
temperature: 0.2
|
||||
tools: { task: false }
|
||||
---
|
||||
You are Fixer, an implementation specialist. You take a well-scoped change, implement it,
|
||||
and verify it compiles/tests.
|
||||
|
||||
- Make the smallest change that satisfies the request; match the surrounding code's style.
|
||||
- Use read/grep to understand context before editing; use bash to build and run tests.
|
||||
- Report exactly what you changed (files and the essence of the diff) and the result of any
|
||||
verification you ran.
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
description: Documentation and knowledge lookup specialist
|
||||
mode: subagent
|
||||
temperature: 0.1
|
||||
tools: { write: false, edit: false, task: false }
|
||||
permission:
|
||||
- { permission: "edit", pattern: "*", action: deny }
|
||||
- { permission: "write", pattern: "*", action: deny }
|
||||
---
|
||||
You are Librarian. You find and summarize documentation, comments, READMEs, config, and
|
||||
other in-repo knowledge on request.
|
||||
|
||||
- Search docs and source for the relevant material with read, glob, and grep.
|
||||
- Quote the authoritative source with its `path:line`; do not invent details.
|
||||
- Return a concise, well-organized summary with pointers back to the sources.
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
description: Deep-reasoning analyst for architecture, debugging, and design trade-offs
|
||||
mode: subagent
|
||||
temperature: 0.3
|
||||
tools: { write: false, edit: false, task: false }
|
||||
permission:
|
||||
- { permission: "edit", pattern: "*", action: deny }
|
||||
- { permission: "write", pattern: "*", action: deny }
|
||||
---
|
||||
You are Oracle, a deep-reasoning analyst. You are consulted for hard questions:
|
||||
root-causing bugs, weighing architectural trade-offs, and reviewing designs.
|
||||
|
||||
- Read whatever code and context you need, but do not modify anything.
|
||||
- Reason carefully and explicitly; state assumptions and the evidence behind conclusions.
|
||||
- Return a decisive recommendation with the reasoning that supports it.
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
description: Primary coordinator that plans work and delegates to specialists
|
||||
mode: primary
|
||||
temperature: 0.2
|
||||
---
|
||||
You are the orchestrator. You plan the work, delegate focused pieces to specialist
|
||||
subagents via the `task` tool, and synthesize their results into a final answer.
|
||||
|
||||
Guidelines:
|
||||
- Break the request into concrete, independently-verifiable pieces.
|
||||
- Prefer delegating reconnaissance and analysis to subagents so your own context stays
|
||||
focused; do the integration and final write-up yourself.
|
||||
- Launch background tasks for long-running independent work, then continue planning.
|
||||
Do not poll running jobs — wait for completion and reconcile terminal jobs before your
|
||||
final response.
|
||||
- Reuse a completed specialist session (by its job alias) when following up on the same
|
||||
thread of work.
|
||||
|
||||
{{SUBAGENTS}}
|
||||
@@ -0,0 +1,466 @@
|
||||
//! Agent definitions and registry.
|
||||
//!
|
||||
//! All agent *behavior* lives in markdown + config — the engine only understands `mode`, tool
|
||||
//! filters, permissions, model, and depth. Definitions are layered (bundled → global → project
|
||||
//! → config patch); later layers win by name. See `docs/04-multiagent.md`.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::config::AgentPatch;
|
||||
use crate::permission::{Rule, Ruleset};
|
||||
use crate::types::ModelRef;
|
||||
|
||||
/// Marker in a primary agent's prompt replaced at load time with the routing list of enabled
|
||||
/// subagents (name + description). Keeps routing text in sync with the enabled agent set.
|
||||
const SUBAGENTS_MARKER: &str = "{{SUBAGENTS}}";
|
||||
|
||||
const BUNDLED: &[(&str, &str)] = &[
|
||||
("orchestrator", include_str!("../../assets/agents/orchestrator.md")),
|
||||
("explorer", include_str!("../../assets/agents/explorer.md")),
|
||||
("oracle", include_str!("../../assets/agents/oracle.md")),
|
||||
("librarian", include_str!("../../assets/agents/librarian.md")),
|
||||
("fixer", include_str!("../../assets/agents/fixer.md")),
|
||||
("designer", include_str!("../../assets/agents/designer.md")),
|
||||
];
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum AgentMode {
|
||||
Primary,
|
||||
#[default]
|
||||
Subagent,
|
||||
All,
|
||||
}
|
||||
|
||||
impl AgentMode {
|
||||
fn parse(s: &str) -> Option<Self> {
|
||||
match s.trim().to_ascii_lowercase().as_str() {
|
||||
"primary" => Some(Self::Primary),
|
||||
"subagent" => Some(Self::Subagent),
|
||||
"all" => Some(Self::All),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this agent can be invoked as a subagent via the `task` tool.
|
||||
pub fn is_subagent(self) -> bool {
|
||||
matches!(self, Self::Subagent | Self::All)
|
||||
}
|
||||
|
||||
/// Whether this agent can drive a top-level (primary) session.
|
||||
pub fn is_primary(self) -> bool {
|
||||
matches!(self, Self::Primary | Self::All)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum AgentSource {
|
||||
Bundled,
|
||||
Global,
|
||||
Project,
|
||||
Config,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AgentDef {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub mode: AgentMode,
|
||||
/// `None` = follow the session model.
|
||||
pub model: Option<ModelRef>,
|
||||
pub temperature: Option<f32>,
|
||||
pub prompt: String,
|
||||
pub permissions: Ruleset,
|
||||
/// Tool enable/disable overrides (wildcard keys allowed); absent = inherit default.
|
||||
pub tools: HashMap<String, bool>,
|
||||
pub max_steps: Option<u32>,
|
||||
pub source: AgentSource,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum AgentError {
|
||||
#[error("agent {0}: {1}")]
|
||||
Frontmatter(String, String),
|
||||
}
|
||||
|
||||
/// YAML frontmatter shape (opencode-compatible).
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
#[serde(default)]
|
||||
struct Frontmatter {
|
||||
description: Option<String>,
|
||||
mode: Option<String>,
|
||||
model: Option<String>,
|
||||
temperature: Option<f32>,
|
||||
tools: Option<HashMap<String, bool>>,
|
||||
permission: Option<Vec<Rule>>,
|
||||
max_steps: Option<u32>,
|
||||
disable: Option<bool>,
|
||||
}
|
||||
|
||||
fn parse_model_ref(s: &str) -> Option<ModelRef> {
|
||||
s.split_once('/')
|
||||
.map(|(p, m)| ModelRef::new(p.trim(), m.trim()))
|
||||
}
|
||||
|
||||
/// Splits a markdown agent file into (frontmatter, body). A file without a leading `---`
|
||||
/// fence is treated as an all-body prompt with empty frontmatter.
|
||||
fn split_frontmatter(content: &str) -> (&str, &str) {
|
||||
let rest = match content.strip_prefix("---\n").or_else(|| content.strip_prefix("---\r\n")) {
|
||||
Some(r) => r,
|
||||
None => return ("", content),
|
||||
};
|
||||
// Find the closing fence line.
|
||||
for delim in ["\n---\n", "\n---\r\n"] {
|
||||
if let Some(idx) = rest.find(delim) {
|
||||
let body_start = idx + delim.len();
|
||||
return (&rest[..idx], &rest[body_start..]);
|
||||
}
|
||||
}
|
||||
// Trailing fence with no body / no trailing newline.
|
||||
if let Some(fm) = rest.strip_suffix("\n---").or(Some(rest)) {
|
||||
if rest.ends_with("\n---") {
|
||||
return (fm, "");
|
||||
}
|
||||
}
|
||||
("", content)
|
||||
}
|
||||
|
||||
/// Parses one markdown agent definition. Returns `Ok(None)` when the file marks itself
|
||||
/// `disable: true`.
|
||||
fn parse_agent(
|
||||
name: &str,
|
||||
source: AgentSource,
|
||||
content: &str,
|
||||
) -> Result<Option<AgentDef>, AgentError> {
|
||||
let (fm_raw, body) = split_frontmatter(content);
|
||||
let fm: Frontmatter = if fm_raw.trim().is_empty() {
|
||||
Frontmatter::default()
|
||||
} else {
|
||||
serde_yaml_ng::from_str(fm_raw)
|
||||
.map_err(|e| AgentError::Frontmatter(name.to_string(), e.to_string()))?
|
||||
};
|
||||
|
||||
if fm.disable == Some(true) {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
Ok(Some(AgentDef {
|
||||
name: name.to_string(),
|
||||
description: fm.description.unwrap_or_default(),
|
||||
mode: fm.mode.as_deref().and_then(AgentMode::parse).unwrap_or_default(),
|
||||
model: fm.model.as_deref().and_then(parse_model_ref),
|
||||
temperature: fm.temperature,
|
||||
prompt: body.trim_end().to_string(),
|
||||
permissions: fm.permission.unwrap_or_default(),
|
||||
tools: fm.tools.unwrap_or_default(),
|
||||
max_steps: fm.max_steps,
|
||||
source,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Applies a config `AgentPatch` onto an existing definition (only set fields override).
|
||||
fn apply_patch(def: &mut AgentDef, patch: &AgentPatch) {
|
||||
if let Some(mode) = patch.mode.as_deref().and_then(AgentMode::parse) {
|
||||
def.mode = mode;
|
||||
}
|
||||
if let Some(model) = patch.model.as_deref().and_then(parse_model_ref) {
|
||||
def.model = Some(model);
|
||||
}
|
||||
if let Some(temp) = patch.temperature {
|
||||
def.temperature = Some(temp);
|
||||
}
|
||||
if let Some(prompt) = &patch.prompt {
|
||||
def.prompt = prompt.clone();
|
||||
}
|
||||
if let Some(tools) = &patch.tools {
|
||||
def.tools.extend(tools.clone());
|
||||
}
|
||||
if let Some(permission) = &patch.permission {
|
||||
def.permissions = permission.clone();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct AgentRegistry {
|
||||
agents: HashMap<String, AgentDef>,
|
||||
}
|
||||
|
||||
impl AgentRegistry {
|
||||
pub fn get(&self, name: &str) -> Option<&AgentDef> {
|
||||
self.agents.get(name)
|
||||
}
|
||||
|
||||
pub fn all(&self) -> Vec<&AgentDef> {
|
||||
self.agents.values().collect()
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.agents.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.agents.is_empty()
|
||||
}
|
||||
|
||||
/// Just the bundled agents — the default when no overrides are configured (tests, headless).
|
||||
pub fn bundled() -> Self {
|
||||
let mut reg = Self::default();
|
||||
reg.load_markdown_layer(BUNDLED.iter().map(|(n, c)| (n.to_string(), *c)), AgentSource::Bundled);
|
||||
reg.generate_routing();
|
||||
reg
|
||||
}
|
||||
|
||||
/// Full layered load: bundled → global dir → project dir → config patches.
|
||||
pub fn load(
|
||||
config_agents: &HashMap<String, AgentPatch>,
|
||||
global_dir: Option<&Path>,
|
||||
project_dir: Option<&Path>,
|
||||
) -> Self {
|
||||
let mut reg = Self::default();
|
||||
reg.load_markdown_layer(
|
||||
BUNDLED.iter().map(|(n, c)| (n.to_string(), *c)),
|
||||
AgentSource::Bundled,
|
||||
);
|
||||
if let Some(dir) = global_dir {
|
||||
reg.load_dir(dir, AgentSource::Global);
|
||||
}
|
||||
if let Some(dir) = project_dir {
|
||||
reg.load_dir(dir, AgentSource::Project);
|
||||
}
|
||||
reg.apply_config(config_agents);
|
||||
reg.generate_routing();
|
||||
reg
|
||||
}
|
||||
|
||||
fn load_markdown_layer<I, S>(&mut self, files: I, source: AgentSource)
|
||||
where
|
||||
I: IntoIterator<Item = (String, S)>,
|
||||
S: AsRef<str>,
|
||||
{
|
||||
for (name, content) in files {
|
||||
match parse_agent(&name, source, content.as_ref()) {
|
||||
Ok(Some(def)) => {
|
||||
self.agents.insert(name, def);
|
||||
}
|
||||
Ok(None) => {
|
||||
self.agents.remove(&name); // disable: true removes an earlier layer
|
||||
}
|
||||
Err(e) => tracing::warn!(error = %e, "skipping malformed agent"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn load_dir(&mut self, dir: &Path, source: AgentSource) {
|
||||
let Ok(entries) = std::fs::read_dir(dir) else {
|
||||
return;
|
||||
};
|
||||
let mut files: Vec<(String, String)> = Vec::new();
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.extension().and_then(|e| e.to_str()) != Some("md") {
|
||||
continue;
|
||||
}
|
||||
let Some(name) = path.file_stem().and_then(|s| s.to_str()) else {
|
||||
continue;
|
||||
};
|
||||
if let Ok(content) = std::fs::read_to_string(&path) {
|
||||
files.push((name.to_string(), content));
|
||||
}
|
||||
}
|
||||
files.sort();
|
||||
self.load_markdown_layer(files, source);
|
||||
}
|
||||
|
||||
fn apply_config(&mut self, config_agents: &HashMap<String, AgentPatch>) {
|
||||
for (name, patch) in config_agents {
|
||||
if patch.disable == Some(true) {
|
||||
self.agents.remove(name);
|
||||
continue;
|
||||
}
|
||||
if let Some(def) = self.agents.get_mut(name) {
|
||||
apply_patch(def, patch);
|
||||
} else if patch.model.is_some() || patch.prompt.is_some() {
|
||||
// Unknown name with enough to stand on its own → custom agent.
|
||||
let mut def = AgentDef {
|
||||
name: name.clone(),
|
||||
description: String::new(),
|
||||
mode: AgentMode::default(),
|
||||
model: None,
|
||||
temperature: None,
|
||||
prompt: String::new(),
|
||||
permissions: Vec::new(),
|
||||
tools: HashMap::new(),
|
||||
max_steps: None,
|
||||
source: AgentSource::Config,
|
||||
};
|
||||
apply_patch(&mut def, patch);
|
||||
self.agents.insert(name.clone(), def);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Replaces `{{SUBAGENTS}}` in every primary agent's prompt with a generated routing list
|
||||
/// of the enabled subagents (so disabling an agent removes it from routing text).
|
||||
fn generate_routing(&mut self) {
|
||||
let mut subagents: Vec<(String, String)> = self
|
||||
.agents
|
||||
.values()
|
||||
.filter(|a| a.mode.is_subagent())
|
||||
.map(|a| (a.name.clone(), a.description.clone()))
|
||||
.collect();
|
||||
subagents.sort();
|
||||
|
||||
let routing = if subagents.is_empty() {
|
||||
"## Agents\n\nNo specialist subagents are available.".to_string()
|
||||
} else {
|
||||
let mut s = String::from("## Agents\n\nDelegate to these specialists via `task`:\n");
|
||||
for (name, desc) in &subagents {
|
||||
s.push_str(&format!("- **{name}** — {desc}\n"));
|
||||
}
|
||||
s
|
||||
};
|
||||
|
||||
for agent in self.agents.values_mut() {
|
||||
if agent.prompt.contains(SUBAGENTS_MARKER) {
|
||||
agent.prompt = agent.prompt.replace(SUBAGENTS_MARKER, routing.trim_end());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::permission::Action;
|
||||
|
||||
#[test]
|
||||
fn bundled_loads_all_six_agents() {
|
||||
let reg = AgentRegistry::bundled();
|
||||
for name in ["orchestrator", "explorer", "oracle", "librarian", "fixer", "designer"] {
|
||||
assert!(reg.get(name).is_some(), "missing {name}");
|
||||
}
|
||||
assert_eq!(reg.len(), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_mode_model_temperature_tools_and_permissions() {
|
||||
let md = "---\n\
|
||||
description: test agent\n\
|
||||
mode: subagent\n\
|
||||
model: anthropic/claude-haiku-4-5\n\
|
||||
temperature: 0.1\n\
|
||||
tools: { write: false, bash: true }\n\
|
||||
permission:\n\
|
||||
\x20 - { permission: \"edit\", pattern: \"*\", action: deny }\n\
|
||||
---\n\
|
||||
You are a test agent.\n";
|
||||
let def = parse_agent("tester", AgentSource::Bundled, md).unwrap().unwrap();
|
||||
assert_eq!(def.description, "test agent");
|
||||
assert_eq!(def.mode, AgentMode::Subagent);
|
||||
assert_eq!(def.model, Some(ModelRef::new("anthropic", "claude-haiku-4-5")));
|
||||
assert_eq!(def.temperature, Some(0.1));
|
||||
assert_eq!(def.tools.get("write"), Some(&false));
|
||||
assert_eq!(def.tools.get("bash"), Some(&true));
|
||||
assert_eq!(def.permissions.len(), 1);
|
||||
assert_eq!(def.permissions[0].action, Action::Deny);
|
||||
assert_eq!(def.prompt, "You are a test agent.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn body_without_frontmatter_is_all_prompt() {
|
||||
let def = parse_agent("x", AgentSource::Global, "just a prompt")
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(def.prompt, "just a prompt");
|
||||
assert_eq!(def.mode, AgentMode::Subagent); // default
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disable_true_removes_agent() {
|
||||
assert!(parse_agent("x", AgentSource::Config, "---\ndisable: true\n---\nbody")
|
||||
.unwrap()
|
||||
.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explorer_denies_writes_and_disables_edit_tool() {
|
||||
let reg = AgentRegistry::bundled();
|
||||
let explorer = reg.get("explorer").unwrap();
|
||||
assert_eq!(explorer.mode, AgentMode::Subagent);
|
||||
assert_eq!(explorer.tools.get("edit"), Some(&false));
|
||||
assert!(explorer
|
||||
.permissions
|
||||
.iter()
|
||||
.any(|r| r.permission == "write" && r.action == Action::Deny));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn orchestrator_routing_lists_subagents_and_drops_marker() {
|
||||
let reg = AgentRegistry::bundled();
|
||||
let prompt = ®.get("orchestrator").unwrap().prompt;
|
||||
assert!(!prompt.contains("{{SUBAGENTS}}"));
|
||||
assert!(prompt.contains("explorer"));
|
||||
assert!(prompt.contains("fixer"));
|
||||
// The orchestrator itself is primary and must not list itself.
|
||||
assert!(!prompt.contains("- **orchestrator**"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_patch_overrides_only_set_fields() {
|
||||
let mut patches = HashMap::new();
|
||||
patches.insert(
|
||||
"explorer".to_string(),
|
||||
AgentPatch {
|
||||
temperature: Some(0.9),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
let reg = AgentRegistry::load(&patches, None, None);
|
||||
let explorer = reg.get("explorer").unwrap();
|
||||
assert_eq!(explorer.temperature, Some(0.9)); // overridden
|
||||
assert_eq!(explorer.mode, AgentMode::Subagent); // untouched
|
||||
assert_eq!(explorer.tools.get("edit"), Some(&false)); // untouched
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_disable_removes_and_unknown_with_model_creates() {
|
||||
let mut patches = HashMap::new();
|
||||
patches.insert(
|
||||
"designer".to_string(),
|
||||
AgentPatch {
|
||||
disable: Some(true),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
patches.insert(
|
||||
"custom".to_string(),
|
||||
AgentPatch {
|
||||
model: Some("openai/gpt-5".into()),
|
||||
prompt: Some("custom prompt".into()),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
let reg = AgentRegistry::load(&patches, None, None);
|
||||
assert!(reg.get("designer").is_none());
|
||||
let custom = reg.get("custom").unwrap();
|
||||
assert_eq!(custom.source, AgentSource::Config);
|
||||
assert_eq!(custom.model, Some(ModelRef::new("openai", "gpt-5")));
|
||||
assert_eq!(custom.prompt, "custom prompt");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_config_agent_without_model_or_prompt_is_ignored() {
|
||||
let mut patches = HashMap::new();
|
||||
patches.insert(
|
||||
"ghost".to_string(),
|
||||
AgentPatch {
|
||||
temperature: Some(0.5),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
let reg = AgentRegistry::load(&patches, None, None);
|
||||
assert!(reg.get("ghost").is_none());
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod agent;
|
||||
pub mod config;
|
||||
pub mod engine;
|
||||
pub mod event;
|
||||
|
||||
Reference in New Issue
Block a user