feat(agent): add architect, debugger, implementer, tester, security sub-agent roles

Extend delegation system with 5 new specialized roles alongside
researcher/analyst/reviewer. Each role has curated tool access.
Refactor profile lookup to use profile_for_role_name and update
compact/summarizer and tool context accordingly.
This commit is contained in:
ZhenYi 2026-05-18 20:42:57 +08:00
parent b413edccaf
commit 8d144ac139
9 changed files with 276 additions and 66 deletions

View File

@ -15,6 +15,7 @@ fn shared_tools() -> Vec<String> {
"git_file_content".into(),
"git_blob_get".into(),
"git_blob_content".into(),
"git_status".into(),
]
}
@ -25,8 +26,10 @@ fn researcher_tools() -> Vec<String> {
"git_search_commits".into(),
"repo_search".into(),
"repo_doc_search".into(),
"project_bing_search".into(),
"project_arxiv_search".into(),
"project_curl".into(),
"curl_exec".into(),
// Index / overview
"repo_overview".into(),
"repo_readme".into(),
@ -45,6 +48,10 @@ fn researcher_tools() -> Vec<String> {
"git_graph".into(),
"git_commit_info".into(),
"repo_commit_log".into(),
"git_ref_list".into(),
"git_ref_info".into(),
"git_lfs_summary".into(),
"git_lfs_scan_tree".into(),
]
}
@ -65,6 +72,9 @@ fn analyst_tools() -> Vec<String> {
"git_branch_list".into(),
"git_branch_info".into(),
"git_branch_diff".into(),
"git_merge_analysis".into(),
"git_ref_list".into(),
"git_ref_info".into(),
// Project data
"project_list_issues".into(),
"project_list_repos".into(),
@ -81,6 +91,7 @@ fn reviewer_tools() -> Vec<String> {
// Merge status
"git_branches_merged".into(),
"git_branch_info".into(),
"git_merge_analysis".into(),
// Tracking
"project_list_issues".into(),
"project_update_issue".into(),
@ -90,6 +101,108 @@ fn reviewer_tools() -> Vec<String> {
]
}
/// Architect-specific tools (system design & dependency mapping).
fn architect_tools() -> Vec<String> {
vec![
// Repository structure
"repo_overview".into(),
"repo_readme".into(),
"repo_file_tree".into(),
"repo_languages".into(),
"repo_dependencies".into(),
"repo_test_discovery".into(),
// Change and branch context
"repo_diff_summary".into(),
"git_branch_list".into(),
"git_branch_info".into(),
"git_branch_diff".into(),
"git_merge_analysis".into(),
"git_ref_list".into(),
// Project context
"project_list_repos".into(),
"project_list_issues".into(),
]
}
/// Debugger-specific tools (root-cause analysis & history tracing).
fn debugger_tools() -> Vec<String> {
vec![
// Failure and change inspection
"git_show".into(),
"git_diff".into(),
"git_diff_stats".into(),
"git_blame".into(),
"git_log".into(),
"git_commit_info".into(),
"git_ref_info".into(),
// Structural clues
"repo_file_tree".into(),
"repo_dependencies".into(),
"repo_test_discovery".into(),
"repo_doc_search".into(),
// Issue context
"project_list_issues".into(),
]
}
/// Implementer-specific tools (implementation planning & code navigation).
fn implementer_tools() -> Vec<String> {
vec![
// Code and documentation context
"repo_overview".into(),
"repo_readme".into(),
"repo_file_tree".into(),
"repo_doc_index".into(),
"repo_doc_read".into(),
"repo_dependencies".into(),
"repo_test_discovery".into(),
// Current change context
"git_diff".into(),
"git_diff_stats".into(),
"git_show".into(),
"git_branch_info".into(),
// Project context
"project_list_issues".into(),
]
}
/// Tester-specific tools (coverage, regression, and validation planning).
fn tester_tools() -> Vec<String> {
vec![
// Test discovery and changed surface
"git_grep".into(),
"git_diff".into(),
"git_diff_stats".into(),
"git_show".into(),
"repo_file_tree".into(),
"repo_dependencies".into(),
"repo_test_discovery".into(),
// Project context
"project_list_issues".into(),
]
}
/// Security-specific tools (threat modeling & sensitive-code review).
fn security_tools() -> Vec<String> {
vec![
// Sensitive pattern discovery
"git_grep".into(),
"git_diff".into(),
"git_diff_stats".into(),
"git_blame".into(),
"git_log".into(),
// Dependency and surface review
"repo_file_tree".into(),
"repo_dependencies".into(),
"repo_doc_search".into(),
"git_lfs_summary".into(),
"git_lfs_pointer_info".into(),
"git_lfs_object_info".into(),
// Issue context
"project_list_issues".into(),
]
}
/// Supervisor-specific tools (delegation & synthesis).
fn supervisor_tools() -> Vec<String> {
vec![
@ -105,12 +218,31 @@ pub fn tools_for_role(role: &AgentRole) -> Vec<String> {
AgentRole::Researcher => tools.extend(researcher_tools()),
AgentRole::Analyst => tools.extend(analyst_tools()),
AgentRole::Reviewer => tools.extend(reviewer_tools()),
AgentRole::Architect => tools.extend(architect_tools()),
AgentRole::Debugger => tools.extend(debugger_tools()),
AgentRole::Implementer => tools.extend(implementer_tools()),
AgentRole::Tester => tools.extend(tester_tools()),
AgentRole::Security => tools.extend(security_tools()),
AgentRole::Supervisor => tools.extend(supervisor_tools()),
AgentRole::Default => {} // Default role gets only shared tools
}
tools
}
pub fn profile_for_role_name(role: &str) -> AgentExecutionProfile {
match role {
"researcher" => researcher_profile(),
"analyst" => analyst_profile(),
"reviewer" => reviewer_profile(),
"architect" => architect_profile(),
"debugger" => debugger_profile(),
"implementer" => implementer_profile(),
"tester" => tester_profile(),
"security" => security_profile(),
_ => researcher_profile(),
}
}
pub fn supervisor_profile() -> AgentExecutionProfile {
AgentExecutionProfile {
role: AgentRole::Supervisor,
@ -123,6 +255,11 @@ pub fn supervisor_profile() -> AgentExecutionProfile {
- **researcher**: Gathers concrete facts, evidence, and data from tools and context. Best for finding information, searching code, and discovering evidence.\n\
- **analyst**: Builds coherent explanations, highlights causal links, edge cases, and tradeoffs. Best for explaining findings and reasoning about implications.\n\
- **reviewer**: Stress-tests proposals, identifies contradictions, missing assumptions, regressions, and risks. Best for quality checks and risk assessment.\n\
- **architect**: Maps systems, boundaries, dependencies, and long-term design tradeoffs. Best for architecture decisions and refactor strategy.\n\
- **debugger**: Finds likely root causes, reproduction gaps, and suspect changes. Best for failures, regressions, and confusing behavior.\n\
- **implementer**: Turns requirements into concrete implementation steps, affected files, and integration concerns. Best for execution planning.\n\
- **tester**: Designs validation strategy, regression coverage, and edge-case test plans. Best for test planning and release confidence.\n\
- **security**: Reviews auth, data exposure, injection, dependency, and abuse risks. Best for threat modeling and sensitive changes.\n\
- Provide a clear, focused task description for each sub-agent.\n\
- You may call multiple sub-agents in sequence (call one, review its output, then decide to call another).\n\
- You may also call the same role twice with different tasks if needed.\n\
@ -130,7 +267,10 @@ pub fn supervisor_profile() -> AgentExecutionProfile {
## Decision Guide\n\
- Simple factual questions: call researcher only.\n\
- Questions requiring explanation: call researcher then analyst.\n\
- Design/architecture reviews: call researcher, analyst, then reviewer.\n\
- Design/architecture reviews: call researcher, architect, then reviewer.\n\
- Bug or regression diagnosis: call debugger, then tester if validation is needed.\n\
- Implementation requests: call implementer, then reviewer for risk checks.\n\
- Security-sensitive changes: call security, then reviewer.\n\
- If a sub-agent's output is insufficient, call another sub-agent for clarification.\n\
\n\
## Output Rules\n\
@ -201,8 +341,93 @@ pub fn reviewer_profile() -> AgentExecutionProfile {
}
}
pub fn architect_profile() -> AgentExecutionProfile {
AgentExecutionProfile {
role: AgentRole::Architect,
system_prompt: Some(
"You are the architect agent. Map system boundaries, dependencies, data flow, and design tradeoffs. Prefer practical architecture guidance tied to repository evidence. Call out migration risks and long-term maintainability concerns.".to_string(),
),
temperature: Some(0.2),
max_tokens: Some(2000),
top_p: Some(1.0),
frequency_penalty: Some(0.0),
presence_penalty: Some(0.0),
max_tool_depth: Some(5),
allowed_tools: Some(tools_for_role(&AgentRole::Architect)),
disable_orchestration: true,
}
}
pub fn debugger_profile() -> AgentExecutionProfile {
AgentExecutionProfile {
role: AgentRole::Debugger,
system_prompt: Some(
"You are the debugger agent. Identify likely root causes, suspect files or commits, missing reproduction details, and the shortest validation path. Separate evidence from hypotheses and rank hypotheses by plausibility.".to_string(),
),
temperature: Some(0.1),
max_tokens: Some(1800),
top_p: Some(1.0),
frequency_penalty: Some(0.0),
presence_penalty: Some(0.0),
max_tool_depth: Some(6),
allowed_tools: Some(tools_for_role(&AgentRole::Debugger)),
disable_orchestration: true,
}
}
pub fn implementer_profile() -> AgentExecutionProfile {
AgentExecutionProfile {
role: AgentRole::Implementer,
system_prompt: Some(
"You are the implementer agent. Convert requirements into a concrete execution plan: files to touch, sequencing, integration points, and risks. Keep recommendations actionable and avoid broad rewrites unless justified.".to_string(),
),
temperature: Some(0.15),
max_tokens: Some(1800),
top_p: Some(1.0),
frequency_penalty: Some(0.0),
presence_penalty: Some(0.0),
max_tool_depth: Some(5),
allowed_tools: Some(tools_for_role(&AgentRole::Implementer)),
disable_orchestration: true,
}
}
pub fn tester_profile() -> AgentExecutionProfile {
AgentExecutionProfile {
role: AgentRole::Tester,
system_prompt: Some(
"You are the tester agent. Design high-signal validation: unit, integration, regression, and edge-case coverage. Identify what must be tested, what can be skipped, and the fastest commands or checks to build confidence.".to_string(),
),
temperature: Some(0.1),
max_tokens: Some(1600),
top_p: Some(1.0),
frequency_penalty: Some(0.0),
presence_penalty: Some(0.0),
max_tool_depth: Some(4),
allowed_tools: Some(tools_for_role(&AgentRole::Tester)),
disable_orchestration: true,
}
}
pub fn security_profile() -> AgentExecutionProfile {
AgentExecutionProfile {
role: AgentRole::Security,
system_prompt: Some(
"You are the security agent. Review authentication, authorization, data exposure, injection, dependency, secret-handling, and abuse-case risks. Prioritize exploitable issues and concrete mitigations over generic advice.".to_string(),
),
temperature: Some(0.1),
max_tokens: Some(1800),
top_p: Some(1.0),
frequency_penalty: Some(0.0),
presence_penalty: Some(0.0),
max_tool_depth: Some(5),
allowed_tools: Some(tools_for_role(&AgentRole::Security)),
disable_orchestration: true,
}
}
/// Whether to enable multi-agent delegation for this request.
/// Simplified from keyword-based gating: delegation is enabled when tools are available.
pub fn should_enable_delegation(_input: &str, tools_available: bool) -> bool {
tools_available
}
}

View File

@ -2,7 +2,7 @@ use std::pin::Pin;
use std::sync::Arc;
use uuid::Uuid;
use super::agent_profile::{analyst_profile, researcher_profile, reviewer_profile};
use super::agent_profile::profile_for_role_name;
use crate::client::AiClientConfig;
use crate::client::types::{ChatRequestMessage, ToolCall};
use crate::client::{StreamChunk, StreamChunkType, StreamedToolCall, call_stream};
@ -629,13 +629,13 @@ pub async fn execute_chat_stream(
"type": "function",
"function": {
"name": "call_sub_agent",
"description": "Delegate a task to a specialist sub-agent and receive its output.\nAvailable roles:\n- researcher: Gathers facts, evidence, and data. Best for finding information and searching code.\n- analyst: Builds explanations, highlights causal links and tradeoffs. Best for reasoning about implications.\n- reviewer: Stress-tests proposals, identifies risks and contradictions. Best for quality checks.\nProvide a clear, focused task description so the sub-agent knows exactly what to investigate.",
"description": "Delegate a task to a specialist sub-agent and receive its output.\nAvailable roles:\n- researcher: Gathers facts, evidence, and data. Best for finding information and searching code.\n- analyst: Builds explanations, highlights causal links and tradeoffs. Best for reasoning about implications.\n- reviewer: Stress-tests proposals, identifies risks and contradictions. Best for quality checks.\n- architect: Maps systems, dependencies, boundaries, and design tradeoffs. Best for architecture decisions.\n- debugger: Finds root causes, suspect changes, and validation paths. Best for bugs and regressions.\n- implementer: Converts requirements into concrete implementation steps. Best for execution planning.\n- tester: Designs validation and regression coverage. Best for test strategy.\n- security: Reviews auth, data exposure, injection, dependency, and abuse risks. Best for sensitive changes.\nProvide a clear, focused task description so the sub-agent knows exactly what to investigate.",
"parameters": {
"type": "object",
"properties": {
"role": {
"type": "string",
"description": "The sub-agent role to delegate to: researcher, analyst, or reviewer."
"description": "The sub-agent role to delegate to: researcher, analyst, reviewer, architect, debugger, implementer, tester, or security."
},
"task": {
"type": "string",
@ -805,11 +805,7 @@ pub async fn execute_chat_stream(
.unwrap_or("researcher");
let task = args.get("task").and_then(|v| v.as_str()).unwrap_or("");
let profile = match role {
"analyst" => analyst_profile(),
"reviewer" => reviewer_profile(),
_ => researcher_profile(),
};
let profile = profile_for_role_name(role);
// Generate children_id BEFORE starting sub-agent execution
let sub_agent_id = format!("sub-agent-{}", Uuid::now_v7());

View File

@ -87,6 +87,11 @@ pub enum AgentRole {
Researcher,
Analyst,
Reviewer,
Architect,
Debugger,
Implementer,
Tester,
Security,
}
#[derive(Debug, Clone, Default)]

View File

@ -1,9 +1,6 @@
use std::collections::HashMap;
use super::agent_profile::{
analyst_profile, researcher_profile, reviewer_profile, should_enable_delegation,
supervisor_profile,
};
use super::agent_profile::{profile_for_role_name, should_enable_delegation, supervisor_profile};
use super::message_builder::MessageBuilder;
use super::nonstreaming_execution::execute_process;
use super::service::{ProcessResult, StreamResult};
@ -67,9 +64,8 @@ pub async fn execute_orchestrated_process(
supervisor_request.frequency_penalty = profile
.frequency_penalty
.unwrap_or(request.frequency_penalty);
supervisor_request.presence_penalty = profile
.presence_penalty
.unwrap_or(request.presence_penalty);
supervisor_request.presence_penalty =
profile.presence_penalty.unwrap_or(request.presence_penalty);
execute_process(
supervisor_request,
@ -138,9 +134,8 @@ pub async fn execute_orchestrated_stream(
supervisor_request.frequency_penalty = profile
.frequency_penalty
.unwrap_or(request.frequency_penalty);
supervisor_request.presence_penalty = profile
.presence_penalty
.unwrap_or(request.presence_penalty);
supervisor_request.presence_penalty =
profile.presence_penalty.unwrap_or(request.presence_penalty);
super::streaming_execution::execute_process_stream(
supervisor_request,
@ -175,6 +170,11 @@ fn register_call_sub_agent_tool(
- researcher: Gathers facts, evidence, and data. Best for finding information and searching code.\n\
- analyst: Builds explanations, highlights causal links and tradeoffs. Best for reasoning about implications.\n\
- reviewer: Stress-tests proposals, identifies risks and contradictions. Best for quality checks.\n\
- architect: Maps systems, dependencies, boundaries, and design tradeoffs. Best for architecture decisions.\n\
- debugger: Finds root causes, suspect changes, and validation paths. Best for bugs and regressions.\n\
- implementer: Converts requirements into concrete implementation steps. Best for execution planning.\n\
- tester: Designs validation and regression coverage. Best for test strategy.\n\
- security: Reviews auth, data exposure, injection, dependency, and abuse risks. Best for sensitive changes.\n\
Provide a clear, focused task description so the sub-agent knows exactly what to investigate.",
)
.parameters(ToolSchema {
@ -187,7 +187,7 @@ fn register_call_sub_agent_tool(
name: "role".into(),
param_type: "string".into(),
description: Some(
"The sub-agent role to delegate to: researcher, analyst, or reviewer.".into(),
"The sub-agent role to delegate to: researcher, analyst, reviewer, architect, debugger, implementer, tester, or security.".into(),
),
required: true,
properties: None,
@ -224,12 +224,7 @@ fn register_call_sub_agent_tool(
.unwrap_or("")
.to_owned();
let profile = match role.as_str() {
"researcher" => researcher_profile(),
"analyst" => analyst_profile(),
"reviewer" => reviewer_profile(),
_ => researcher_profile(),
};
let profile = profile_for_role_name(role.as_str());
let mut sub_request = captured_request.clone();
sub_request.input = format!(
@ -288,9 +283,12 @@ fn filter_tools_for_sub_agent(
let Some(tools) = original_tools else {
return Vec::new();
};
let allowed = allowed_tools
.as_ref()
.map(|list| list.iter().filter(|n| *n != "call_sub_agent").cloned().collect::<Vec<String>>());
let allowed = allowed_tools.as_ref().map(|list| {
list.iter()
.filter(|n| *n != "call_sub_agent")
.cloned()
.collect::<Vec<String>>()
});
match allowed {
Some(allowed_list) if !allowed_list.is_empty() => tools
@ -308,8 +306,7 @@ fn filter_tools_for_sub_agent(
_ => tools
.iter()
.filter(|tool| {
tool
.get("function")
tool.get("function")
.and_then(|f| f.get("name"))
.and_then(|v| v.as_str())
.is_some_and(|name| name != "call_sub_agent")
@ -317,4 +314,4 @@ fn filter_tools_for_sub_agent(
.cloned()
.collect(),
}
}
}

View File

@ -165,10 +165,7 @@ impl ChatService {
request: AiRequest,
room_tools: ToolRegistry,
) -> Result<ProcessResult> {
let mut merged = self
.tool_registry
.clone()
.unwrap_or_default();
let mut merged = self.tool_registry.clone().unwrap_or_default();
merged.merge(room_tools);
super::nonstreaming_execution::execute_process(
@ -191,10 +188,7 @@ impl ChatService {
on_chunk: StreamCallback,
room_tools: ToolRegistry,
) -> Result<StreamResult> {
let mut merged = self
.tool_registry
.clone()
.unwrap_or_default();
let mut merged = self.tool_registry.clone().unwrap_or_default();
merged.merge(room_tools);
super::streaming_execution::execute_process_stream(

View File

@ -198,9 +198,7 @@ impl super::CompactService {
.unwrap_or_else(|_| estimate_input.len() / 4);
let retain_count = Self::resolve_retain_count(config, estimated_tokens);
if estimated_tokens >= config.token_threshold
&& messages.len() > retain_count
{
if estimated_tokens >= config.token_threshold && messages.len() > retain_count {
let split_index = messages.len().saturating_sub(retain_count);
let (to_summarize, retained_messages) = messages.split_at(split_index);
let from_seq = to_summarize

View File

@ -2,11 +2,11 @@ use models::rooms::room_message::Model as RoomMessageModel;
use models::users::user::{Column as UserCol, Entity as User};
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
use crate::AgentError;
use crate::client::call_with_params;
use crate::client::types::ChatRequestMessage;
use crate::compact::types::{CompactConfig, MessageSummary};
use crate::tokent::{TokenUsage, count_message_text};
use crate::AgentError;
const DEFAULT_MODEL_CONTEXT_LIMIT: usize = 128_000;
const MODEL_INPUT_RATIO_NUMERATOR: usize = 85;
@ -193,7 +193,10 @@ impl super::CompactService {
)
.await?;
Ok((final_summary, if has_usage { Some(total_usage) } else { None }))
Ok((
final_summary,
if has_usage { Some(total_usage) } else { None },
))
}
async fn merge_summary_rounds(
@ -230,11 +233,7 @@ impl super::CompactService {
for pair_text in fitted_pairs {
let prompt = self.build_prompt(kind, true, &pair_text, current_budget);
let (summary, usage) = self
.invoke_summary_prompt(
&prompt,
current_budget,
Self::temperature_for(kind),
)
.invoke_summary_prompt(&prompt, current_budget, Self::temperature_for(kind))
.await?;
Self::accumulate_usage(total_usage, has_usage, usage);
next_round.push(summary);
@ -474,18 +473,16 @@ impl super::CompactService {
}
fn safe_model_input_budget_from_limit(model_context_limit: Option<usize>) -> usize {
let context_limit = model_context_limit.unwrap_or(DEFAULT_MODEL_CONTEXT_LIMIT).max(1);
let context_limit = model_context_limit
.unwrap_or(DEFAULT_MODEL_CONTEXT_LIMIT)
.max(1);
context_limit
.saturating_mul(MODEL_INPUT_RATIO_NUMERATOR)
.saturating_div(MODEL_INPUT_RATIO_DENOMINATOR)
.max(1)
}
fn accumulate_usage(
total: &mut TokenUsage,
has_usage: &mut bool,
usage: Option<TokenUsage>,
) {
fn accumulate_usage(total: &mut TokenUsage, has_usage: &mut bool, usage: Option<TokenUsage>) {
if let Some(usage) = usage {
total.input_tokens += usage.input_tokens;
total.output_tokens += usage.output_tokens;
@ -500,7 +497,10 @@ mod tests {
#[test]
fn room_summary_uses_eighty_five_percent_input_budget() {
assert_eq!(CompactService::safe_model_input_budget_from_limit(Some(1000)), 850);
assert_eq!(
CompactService::safe_model_input_budget_from_limit(Some(1000)),
850
);
}
#[test]

View File

@ -19,8 +19,8 @@ pub use billing::{
initialize_user_billing, persist_billing_error, record_ai_usage, record_user_ai_usage,
};
pub use chat::{
AgentExecutionProfile, AgentRole, AiContextSenderType, AiRequest, AiStreamChunk,
ChatService, Mention, RoomMessageContext, StreamCallback,
AgentExecutionProfile, AgentRole, AiContextSenderType, AiRequest, AiStreamChunk, ChatService,
Mention, RoomMessageContext, StreamCallback,
};
pub use client::types::ChatRequestMessage;
pub use client::{AiCallResponse, AiClientConfig, call_with_params, call_with_retry};

View File

@ -177,12 +177,7 @@ impl ToolContext {
return Err(existing);
}
match current.compare_exchange(
existing,
next,
Ordering::AcqRel,
Ordering::Relaxed,
) {
match current.compare_exchange(existing, next, Ordering::AcqRel, Ordering::Relaxed) {
Ok(_) => return Ok(()),
Err(_) => continue,
}