feat(model): 专家和子代理支持独立配置 provider/model
- 新增 ModelResolver 解析器,按 frontmatter 中的 provider/model 名覆盖基础 LLMProviderConfig - Expert/SubagentDef 数据结构新增 provider/model 字段,frontmatter 解析与渲染支持往返 - AgentFactory 和 DefaultSubAgentRuntime 持有 ModelResolver,在创建 agent 时解析模型覆盖 - HTTP API 新增 /api/model-options 端点,ExpertResponse/Create/Update 和 SubagentUpdateRequest 支持 provider/model - update_expert/update_subagent 支持 provider/model 字段写回 frontmatter - 保持架构解耦:ModelResolver 位于 config 底层,不引入新的跨模块依赖
This commit is contained in:
parent
003eab4f21
commit
dc9211548a
@ -1049,6 +1049,80 @@ impl std::fmt::Display for ConfigError {
|
||||
|
||||
impl std::error::Error for ConfigError {}
|
||||
|
||||
/// Provider/Model 解析器:按可选的 provider/model 名覆盖基础 LLMProviderConfig。
|
||||
///
|
||||
/// 用于专家和子代理独立配置模型:frontmatter 中的 `provider`/`model` 字段
|
||||
/// 引用 `config.json` 的 `providers`/`models` 表,由本结构解析覆盖。
|
||||
/// 保留 base 中的 agent 级参数(max_tool_iterations 等)和 image_context 配置。
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ModelResolver {
|
||||
providers: HashMap<String, ProviderConfig>,
|
||||
models: HashMap<String, ModelConfig>,
|
||||
}
|
||||
|
||||
impl ModelResolver {
|
||||
pub fn new(providers: HashMap<String, ProviderConfig>, models: HashMap<String, ModelConfig>) -> Self {
|
||||
Self { providers, models }
|
||||
}
|
||||
|
||||
/// 从 Config 构造。
|
||||
pub fn from_config(config: &Config) -> Self {
|
||||
Self::new(config.providers.clone(), config.models.clone())
|
||||
}
|
||||
|
||||
/// 按可选的 provider/model 名解析覆盖 base 中的对应部分。
|
||||
///
|
||||
/// - `provider_name=None` 时保留 base 的 provider 部分
|
||||
/// - `model_name=None` 时保留 base 的 model 部分
|
||||
/// - agent 级参数和 image_context 始终保留 base 的值
|
||||
pub fn resolve(
|
||||
&self,
|
||||
provider_name: Option<&str>,
|
||||
model_name: Option<&str>,
|
||||
base: &LLMProviderConfig,
|
||||
) -> Result<LLMProviderConfig, ConfigError> {
|
||||
let mut result = base.clone();
|
||||
|
||||
if let Some(name) = provider_name.map(str::trim).filter(|s| !s.is_empty()) {
|
||||
let provider = self
|
||||
.providers
|
||||
.get(name)
|
||||
.ok_or(ConfigError::ProviderNotFound(name.to_string()))?;
|
||||
result.provider_type = provider.provider_type.clone();
|
||||
result.name = name.to_string();
|
||||
result.base_url = provider.base_url.clone();
|
||||
result.api_key = provider.api_key.clone();
|
||||
result.extra_headers = provider.extra_headers.clone();
|
||||
result.llm_timeout_secs = provider.llm_timeout_secs;
|
||||
result.memory_maintenance_timeout_secs = provider.memory_maintenance_timeout_secs;
|
||||
}
|
||||
|
||||
if let Some(name) = model_name.map(str::trim).filter(|s| !s.is_empty()) {
|
||||
let model = self
|
||||
.models
|
||||
.get(name)
|
||||
.ok_or(ConfigError::ModelNotFound(name.to_string()))?;
|
||||
result.model_id = model.model_id.clone();
|
||||
result.temperature = model.temperature;
|
||||
result.max_tokens = model.max_tokens;
|
||||
result.context_window_tokens = model.context_window_tokens;
|
||||
result.model_extra = model.extra.clone();
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// 列出所有可用的 provider 名(供前端下拉框)。
|
||||
pub fn provider_names(&self) -> Vec<String> {
|
||||
self.providers.keys().cloned().collect()
|
||||
}
|
||||
|
||||
/// 列出所有可用的 model 名(供前端下拉框)。
|
||||
pub fn model_names(&self) -> Vec<String> {
|
||||
self.models.keys().cloned().collect()
|
||||
}
|
||||
}
|
||||
|
||||
fn load_env_file() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let env_path = Path::new(".env");
|
||||
if env_path.exists() {
|
||||
|
||||
@ -25,6 +25,10 @@ pub struct Expert {
|
||||
pub path: PathBuf,
|
||||
/// 工具与技能加载策略。全为空表示沿用主智能体默认配置(不过滤)。
|
||||
pub capability: CapabilityPolicy,
|
||||
/// 可选的 provider 名(引用 config.json 的 providers 表)。None 时继承主智能体。
|
||||
pub provider: Option<String>,
|
||||
/// 可选的 model 名(引用 config.json 的 models 表)。None 时继承主智能体。
|
||||
pub model: Option<String>,
|
||||
}
|
||||
|
||||
/// Where an expert definition was discovered from.
|
||||
@ -93,6 +97,12 @@ pub struct ExpertWithStatus {
|
||||
/// 工具与技能加载策略。
|
||||
#[serde(default)]
|
||||
pub capability: CapabilityPolicy,
|
||||
/// 可选的 provider 名(引用 config.json 的 providers 表)。None 时继承主智能体。
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub provider: Option<String>,
|
||||
/// 可选的 model 名(引用 config.json 的 models 表)。None 时继承主智能体。
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub model: Option<String>,
|
||||
}
|
||||
|
||||
/// Result of an enable/disable operation.
|
||||
@ -331,6 +341,8 @@ impl ExpertRuntime {
|
||||
path: expert.path.display().to_string(),
|
||||
disabled_in_scopes: scopes.iter().map(|s| s.as_str().to_string()).collect(),
|
||||
capability: expert.capability.clone(),
|
||||
provider: expert.provider.clone(),
|
||||
model: expert.model.clone(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
@ -353,6 +365,8 @@ impl ExpertRuntime {
|
||||
description: &str,
|
||||
body: &str,
|
||||
capability: &CapabilityPolicy,
|
||||
provider: &Option<String>,
|
||||
model: &Option<String>,
|
||||
reload: bool,
|
||||
) -> Result<Expert, String> {
|
||||
validate_expert_name(name)?;
|
||||
@ -365,7 +379,7 @@ impl ExpertRuntime {
|
||||
));
|
||||
}
|
||||
|
||||
write_expert_file(&path, name, description, body, capability)?;
|
||||
write_expert_file(&path, name, description, body, capability, provider, model)?;
|
||||
let expert = parse_expert_file(&path, scope.into())?;
|
||||
if reload {
|
||||
let _ = self.reload()?;
|
||||
@ -380,6 +394,8 @@ impl ExpertRuntime {
|
||||
description: Option<&str>,
|
||||
body: Option<&str>,
|
||||
capability: Option<&CapabilityPolicy>,
|
||||
provider: Option<&Option<String>>,
|
||||
model: Option<&Option<String>>,
|
||||
reload: bool,
|
||||
) -> Result<Expert, String> {
|
||||
validate_expert_name(name)?;
|
||||
@ -392,8 +408,10 @@ impl ExpertRuntime {
|
||||
let next_description = description.unwrap_or(&existing.description);
|
||||
let next_body = body.unwrap_or(&existing.body);
|
||||
let next_capability = capability.cloned().unwrap_or(existing.capability);
|
||||
let next_provider = provider.cloned().unwrap_or(existing.provider);
|
||||
let next_model = model.cloned().unwrap_or(existing.model);
|
||||
|
||||
write_expert_file(&path, name, next_description, next_body, &next_capability)?;
|
||||
write_expert_file(&path, name, next_description, next_body, &next_capability, &next_provider, &next_model)?;
|
||||
let expert = parse_expert_file(&path, scope.into())?;
|
||||
if reload {
|
||||
let _ = self.reload()?;
|
||||
@ -742,6 +760,10 @@ struct ExpertFrontmatter {
|
||||
allowed_subagents: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
denied_subagents: Vec<String>,
|
||||
#[serde(default)]
|
||||
provider: Option<String>,
|
||||
#[serde(default)]
|
||||
model: Option<String>,
|
||||
}
|
||||
|
||||
/// 规范化字符串列表:去除空白项与首尾空格。
|
||||
@ -762,6 +784,8 @@ fn render_expert_file(
|
||||
description: &str,
|
||||
body: &str,
|
||||
capability: &CapabilityPolicy,
|
||||
provider: &Option<String>,
|
||||
model: &Option<String>,
|
||||
) -> Result<String, String> {
|
||||
if description.trim().is_empty() {
|
||||
return Err("description is required and cannot be empty".to_string());
|
||||
@ -783,6 +807,10 @@ fn render_expert_file(
|
||||
allowed_subagents: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
denied_subagents: Vec<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
provider: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
model: Option<String>,
|
||||
}
|
||||
|
||||
let yaml = serde_yaml::to_string(&ExpertFrontmatterOwned {
|
||||
@ -794,6 +822,8 @@ fn render_expert_file(
|
||||
denied_tools: capability.denied_tools.clone(),
|
||||
allowed_subagents: capability.allowed_subagents.clone(),
|
||||
denied_subagents: capability.denied_subagents.clone(),
|
||||
provider: provider.clone(),
|
||||
model: model.clone(),
|
||||
})
|
||||
.map_err(|err| format!("failed to render expert frontmatter: {}", err))?;
|
||||
|
||||
@ -812,8 +842,10 @@ fn write_expert_file(
|
||||
description: &str,
|
||||
body: &str,
|
||||
capability: &CapabilityPolicy,
|
||||
provider: &Option<String>,
|
||||
model: &Option<String>,
|
||||
) -> Result<(), String> {
|
||||
let content = render_expert_file(name, description, body, capability)?;
|
||||
let content = render_expert_file(name, description, body, capability, provider, model)?;
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.map_err(|err| format!("failed to create expert directory: {}", err))?;
|
||||
@ -890,6 +922,15 @@ fn parse_expert_file(path: &Path, source: ExpertSource) -> Result<Expert, String
|
||||
denied_subagents: normalize_string_list(frontmatter.denied_subagents),
|
||||
};
|
||||
|
||||
let provider = frontmatter
|
||||
.provider
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty());
|
||||
let model = frontmatter
|
||||
.model
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty());
|
||||
|
||||
Ok(Expert {
|
||||
name,
|
||||
description: description.to_string(),
|
||||
@ -897,6 +938,8 @@ fn parse_expert_file(path: &Path, source: ExpertSource) -> Result<Expert, String
|
||||
source,
|
||||
path: path.to_path_buf(),
|
||||
capability,
|
||||
provider,
|
||||
model,
|
||||
})
|
||||
}
|
||||
|
||||
@ -1094,7 +1137,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_render_expert_file_requires_description() {
|
||||
let err = render_expert_file("demo", " ", "body", &CapabilityPolicy::default()).unwrap_err();
|
||||
let err = render_expert_file("demo", " ", "body", &CapabilityPolicy::default(), &None, &None).unwrap_err();
|
||||
assert!(err.contains("description"));
|
||||
}
|
||||
|
||||
@ -1130,8 +1173,10 @@ mod tests {
|
||||
denied_skills: vec!["skill_c".to_string()],
|
||||
allowed_tools: Some(vec!["read".to_string(), "mcp_fs_echo".to_string()]),
|
||||
denied_tools: vec!["bash".to_string()],
|
||||
allowed_subagents: None,
|
||||
denied_subagents: vec![],
|
||||
};
|
||||
let rendered = render_expert_file("cap", "desc", "body", &policy).unwrap();
|
||||
let rendered = render_expert_file("cap", "desc", "body", &policy, &None, &None).unwrap();
|
||||
// 白名单/黑名单字段都应出现
|
||||
assert!(rendered.contains("allowed_skills:"));
|
||||
assert!(rendered.contains("denied_skills:"));
|
||||
@ -1143,7 +1188,7 @@ mod tests {
|
||||
// 写入磁盘再读回,策略应一致
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let file_path = temp.path().join("EXPERT.md");
|
||||
write_expert_file(&file_path, "cap", "desc", "body", &policy).unwrap();
|
||||
write_expert_file(&file_path, "cap", "desc", "body", &policy, &None, &None).unwrap();
|
||||
let expert = parse_expert_file(&file_path, ExpertSource::Project).unwrap();
|
||||
assert_eq!(expert.capability, policy);
|
||||
}
|
||||
@ -1151,7 +1196,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_empty_capability_omits_keys() {
|
||||
// 空策略不应输出多余 frontmatter 键,保持旧文件格式兼容
|
||||
let rendered = render_expert_file("plain", "desc", "body", &CapabilityPolicy::default()).unwrap();
|
||||
let rendered = render_expert_file("plain", "desc", "body", &CapabilityPolicy::default(), &None, &None).unwrap();
|
||||
assert!(!rendered.contains("allowed_skills"));
|
||||
assert!(!rendered.contains("denied_skills"));
|
||||
assert!(!rendered.contains("allowed_tools"));
|
||||
@ -1227,6 +1272,8 @@ mod tests {
|
||||
"翻译专家",
|
||||
"你是一名专业翻译。",
|
||||
&CapabilityPolicy::default(),
|
||||
&None,
|
||||
&None,
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
@ -1240,6 +1287,8 @@ mod tests {
|
||||
"dup",
|
||||
"body",
|
||||
&CapabilityPolicy::default(),
|
||||
&None,
|
||||
&None,
|
||||
true,
|
||||
);
|
||||
assert!(dup.is_err());
|
||||
@ -1251,6 +1300,8 @@ mod tests {
|
||||
Some("更新翻译专家"),
|
||||
Some("你是一名中文教师。"),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
@ -1259,7 +1310,7 @@ mod tests {
|
||||
|
||||
// update with None preserves fields
|
||||
let updated_none = runtime
|
||||
.update_expert(ExpertScope::Project, "translator", None, None, None, true)
|
||||
.update_expert(ExpertScope::Project, "translator", None, None, None, None, None, true)
|
||||
.unwrap();
|
||||
assert_eq!(updated_none.description, "更新翻译专家");
|
||||
assert_eq!(updated_none.body, "你是一名中文教师。");
|
||||
@ -1294,6 +1345,8 @@ mod tests {
|
||||
"编程专家",
|
||||
"你是一名编程专家。",
|
||||
&CapabilityPolicy::default(),
|
||||
&None,
|
||||
&None,
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
@ -1344,6 +1397,8 @@ mod tests {
|
||||
"写作专家",
|
||||
"你是一名写作专家。",
|
||||
&CapabilityPolicy::default(),
|
||||
&None,
|
||||
&None,
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
@ -1385,6 +1440,8 @@ mod tests {
|
||||
"代码审查专家",
|
||||
"你是一名代码审查专家。",
|
||||
&CapabilityPolicy::default(),
|
||||
&None,
|
||||
&None,
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
@ -1435,6 +1492,8 @@ mod tests {
|
||||
"规划专家",
|
||||
"你是一名规划专家。",
|
||||
&CapabilityPolicy::default(),
|
||||
&None,
|
||||
&None,
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
@ -1570,6 +1629,8 @@ mod tests {
|
||||
"教师专家",
|
||||
"你是一名中文教师,请用中文回答。",
|
||||
&CapabilityPolicy::default(),
|
||||
&None,
|
||||
&None,
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
@ -1608,6 +1669,8 @@ mod tests {
|
||||
"无 body 的专家",
|
||||
"", // body empty
|
||||
&CapabilityPolicy::default(),
|
||||
&None,
|
||||
&None,
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::agent::{AgentError, AgentLoop, CompositeSystemPromptProvider, SystemPromptProvider};
|
||||
use crate::config::LLMProviderConfig;
|
||||
use crate::config::{LLMProviderConfig, ModelResolver};
|
||||
use crate::domain::CapabilityPolicy;
|
||||
use crate::experts::ExpertPromptProvider;
|
||||
use crate::experts::ExpertRuntime;
|
||||
@ -48,6 +48,8 @@ pub(crate) struct AgentFactory {
|
||||
subagent_runtime: Arc<SubagentRuntime>,
|
||||
reinject_every: usize,
|
||||
prompt_repository: Arc<dyn PromptInjectionRepository>,
|
||||
/// Provider/Model 解析器:按专家 frontmatter 中的 provider/model 字段覆盖基础配置
|
||||
model_resolver: Arc<ModelResolver>,
|
||||
/// 实例创建时间戳(用于区分新旧 AgentFactory 实例)
|
||||
instance_id: u64,
|
||||
}
|
||||
@ -73,6 +75,7 @@ impl AgentFactory {
|
||||
subagent_runtime: Arc<SubagentRuntime>,
|
||||
reinject_every: usize,
|
||||
prompt_repository: Arc<dyn PromptInjectionRepository>,
|
||||
model_resolver: Arc<ModelResolver>,
|
||||
) -> Self {
|
||||
// 使用 Arc 指针地址作为实例标识符,用于区分新旧 AgentFactory 实例
|
||||
let instance_id = Arc::as_ptr(&tools) as u64;
|
||||
@ -88,6 +91,7 @@ impl AgentFactory {
|
||||
subagent_runtime,
|
||||
reinject_every,
|
||||
prompt_repository,
|
||||
model_resolver,
|
||||
instance_id,
|
||||
}
|
||||
}
|
||||
@ -95,13 +99,40 @@ impl AgentFactory {
|
||||
pub(crate) fn create(&self, request: AgentBuildRequest<'_>) -> Result<AgentLoop, AgentError> {
|
||||
let session_id = persistent_session_id(request.channel_name, request.session_chat_id);
|
||||
|
||||
// 读取所选专家(用于工具过滤 + 子代理策略 + 模型覆盖)
|
||||
let expert = self.experts.selected_expert_for(&session_id);
|
||||
let expert_capability = expert.as_ref().map(|e| e.capability.clone());
|
||||
|
||||
// 按专家 frontmatter 中的 provider/model 字段解析覆盖基础 provider_config。
|
||||
// 引用不存在的 provider/model 名时报错并阻止会话(用户主动选择的角色,配置错误应明确反馈)。
|
||||
let effective_provider_config = match &expert {
|
||||
Some(e) if e.provider.is_some() || e.model.is_some() => {
|
||||
let resolved = self.model_resolver.resolve(
|
||||
e.provider.as_deref(),
|
||||
e.model.as_deref(),
|
||||
&request.provider_config,
|
||||
)
|
||||
.map_err(|e| AgentError::Other(e.to_string()))?;
|
||||
tracing::info!(
|
||||
instance_id = self.instance_id,
|
||||
session_id = %session_id,
|
||||
expert = %e.name,
|
||||
provider = %resolved.name,
|
||||
model_id = %resolved.model_id,
|
||||
"AgentFactory: applied expert model override"
|
||||
);
|
||||
resolved
|
||||
}
|
||||
_ => request.provider_config.clone(),
|
||||
};
|
||||
|
||||
// 诊断日志:记录 agent 实际使用的配置和实例 ID
|
||||
tracing::info!(
|
||||
instance_id = self.instance_id,
|
||||
channel = %request.channel_name,
|
||||
session_id = %session_id,
|
||||
provider = %request.provider_config.name,
|
||||
model_id = %request.provider_config.model_id,
|
||||
provider = %effective_provider_config.name,
|
||||
model_id = %effective_provider_config.model_id,
|
||||
tool_count = self.tools.tool_names().len(),
|
||||
"AgentFactory: creating agent with config"
|
||||
);
|
||||
@ -109,19 +140,13 @@ impl AgentFactory {
|
||||
// 创建组合的系统提示词提供者(与命令侧 /save 等共享同一构建逻辑)
|
||||
let system_prompt_provider = build_system_prompt_provider(
|
||||
self.reinject_every,
|
||||
request.provider_config.clone(),
|
||||
effective_provider_config.clone(),
|
||||
self.prompt_repository.clone(),
|
||||
self.skills.clone(),
|
||||
self.experts.clone(),
|
||||
self.subagent_runtime.clone(),
|
||||
);
|
||||
|
||||
// 读取所选专家的 capability(用于工具过滤 + 子代理策略注入 ToolContext)
|
||||
let expert_capability = self
|
||||
.experts
|
||||
.selected_expert_for(&session_id)
|
||||
.map(|e| e.capability);
|
||||
|
||||
// 按所选专家的工具策略过滤工具集(含内置 + MCP 工具)。
|
||||
// 无专家或专家未声明工具策略时,复用共享的 Arc<ToolRegistry>(零拷贝)。
|
||||
let base_tool_count = self.tools.tool_names().len();
|
||||
@ -142,7 +167,7 @@ impl AgentFactory {
|
||||
};
|
||||
|
||||
AgentLoop::with_tools_and_system_prompt_provider(
|
||||
request.provider_config,
|
||||
effective_provider_config,
|
||||
tools,
|
||||
system_prompt_provider,
|
||||
Some(self.skills.clone()),
|
||||
|
||||
@ -263,6 +263,13 @@ pub struct ToolsListResponse {
|
||||
pub tools: Vec<ToolInfo>,
|
||||
}
|
||||
|
||||
/// GET /api/model-options 返回可用的 provider/model 名列表(供专家/子代理编辑下拉框)。
|
||||
#[derive(Serialize)]
|
||||
pub struct ModelOptionsResponse {
|
||||
pub providers: Vec<String>,
|
||||
pub models: Vec<String>,
|
||||
}
|
||||
|
||||
/// GET /api/tools — Return all registered tools (builtin + MCP) with name/description/source.
|
||||
/// 通过 SessionManager::tools() 只读访问 ToolRegistry,不修改状态。
|
||||
pub async fn tools_list(
|
||||
@ -295,6 +302,18 @@ pub async fn tools_list(
|
||||
Json(ToolsListResponse { total, tools })
|
||||
}
|
||||
|
||||
/// GET /api/model-options — 返回 config.json 中配置的 provider/model 名列表。
|
||||
pub async fn model_options(
|
||||
State(state): State<Arc<GatewayState>>,
|
||||
) -> Json<ModelOptionsResponse> {
|
||||
let config = state.config.read().await;
|
||||
let resolver = crate::config::ModelResolver::from_config(&config);
|
||||
Json(ModelOptionsResponse {
|
||||
providers: resolver.provider_names(),
|
||||
models: resolver.model_names(),
|
||||
})
|
||||
}
|
||||
|
||||
/// POST /api/skills/toggle — Enable or disable a specific skill
|
||||
pub async fn skills_toggle(
|
||||
State(state): State<Arc<GatewayState>>,
|
||||
@ -477,6 +496,10 @@ pub struct SubagentUpdateRequest {
|
||||
pub body: Option<String>,
|
||||
#[serde(default)]
|
||||
pub capability: Option<CapabilityPolicy>,
|
||||
#[serde(default)]
|
||||
pub provider: Option<String>,
|
||||
#[serde(default)]
|
||||
pub model: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@ -499,6 +522,8 @@ pub async fn subagents_update(
|
||||
req.description.as_deref(),
|
||||
req.body.as_deref(),
|
||||
req.capability.as_ref(),
|
||||
Some(&req.provider),
|
||||
Some(&req.model),
|
||||
true,
|
||||
)
|
||||
.map_err(|err| {
|
||||
@ -524,6 +549,8 @@ pub async fn subagents_update(
|
||||
source: updated.source.as_str().to_string(),
|
||||
disabled_in_scopes: vec![],
|
||||
capability: updated.capability.clone(),
|
||||
provider: updated.provider.clone(),
|
||||
model: updated.model.clone(),
|
||||
});
|
||||
|
||||
Ok(Json(SubagentUpdateResponse {
|
||||
@ -570,6 +597,10 @@ pub struct ExpertCreateRequest {
|
||||
pub scope: String,
|
||||
#[serde(default)]
|
||||
pub capability: CapabilityPolicy,
|
||||
#[serde(default)]
|
||||
pub provider: Option<String>,
|
||||
#[serde(default)]
|
||||
pub model: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@ -580,6 +611,10 @@ pub struct ExpertUpdateRequest {
|
||||
pub body: Option<String>,
|
||||
#[serde(default)]
|
||||
pub capability: Option<CapabilityPolicy>,
|
||||
#[serde(default)]
|
||||
pub provider: Option<String>,
|
||||
#[serde(default)]
|
||||
pub model: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@ -621,6 +656,10 @@ pub struct ExpertResponse {
|
||||
pub path: String,
|
||||
#[serde(default)]
|
||||
pub capability: CapabilityPolicy,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub provider: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub model: Option<String>,
|
||||
}
|
||||
|
||||
impl From<Expert> for ExpertResponse {
|
||||
@ -632,6 +671,8 @@ impl From<Expert> for ExpertResponse {
|
||||
source: expert.source.as_str().to_string(),
|
||||
path: expert.path.display().to_string(),
|
||||
capability: expert.capability,
|
||||
provider: expert.provider,
|
||||
model: expert.model,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -742,7 +783,16 @@ pub async fn experts_create(
|
||||
|
||||
let expert = state
|
||||
.experts
|
||||
.create_expert(scope, &req.name, &req.description, &req.body, &req.capability, true)
|
||||
.create_expert(
|
||||
scope,
|
||||
&req.name,
|
||||
&req.description,
|
||||
&req.body,
|
||||
&req.capability,
|
||||
&req.provider,
|
||||
&req.model,
|
||||
true,
|
||||
)
|
||||
.map_err(|err| {
|
||||
let status = if err.contains("already exists") {
|
||||
StatusCode::CONFLICT
|
||||
@ -771,6 +821,8 @@ pub async fn experts_update(
|
||||
req.description.as_deref(),
|
||||
req.body.as_deref(),
|
||||
req.capability.as_ref(),
|
||||
Some(&req.provider),
|
||||
Some(&req.model),
|
||||
true,
|
||||
)
|
||||
.map_err(|err| {
|
||||
|
||||
@ -108,6 +108,7 @@ impl GatewayState {
|
||||
session_ttl_hours,
|
||||
mcp_config,
|
||||
Some(bus.clone()),
|
||||
Arc::new(crate::config::ModelResolver::from_config(&config)),
|
||||
)?;
|
||||
|
||||
// 诊断日志:记录新 GatewayState 的创建(用于排查重启后是否使用了新状态)
|
||||
@ -242,6 +243,7 @@ pub async fn run(
|
||||
.route("/api/skills", routing::get(http::skills_list))
|
||||
.route("/api/skills/toggle", routing::post(http::skills_toggle))
|
||||
.route("/api/tools", routing::get(http::tools_list))
|
||||
.route("/api/model-options", routing::get(http::model_options))
|
||||
.route("/api/subagents", routing::get(http::subagents_list))
|
||||
.route("/api/subagents/toggle", routing::post(http::subagents_toggle))
|
||||
.route("/api/subagents/update", routing::put(http::subagents_update))
|
||||
@ -265,6 +267,7 @@ pub async fn run(
|
||||
.route("/api/skills", routing::get(http::skills_list))
|
||||
.route("/api/skills/toggle", routing::post(http::skills_toggle))
|
||||
.route("/api/tools", routing::get(http::tools_list))
|
||||
.route("/api/model-options", routing::get(http::model_options))
|
||||
.route("/api/subagents", routing::get(http::subagents_list))
|
||||
.route("/api/subagents/toggle", routing::post(http::subagents_toggle))
|
||||
.route("/api/subagents/update", routing::put(http::subagents_update))
|
||||
|
||||
@ -8,7 +8,7 @@ use tokio::sync::RwLock;
|
||||
|
||||
use crate::agent::AgentError;
|
||||
use crate::bus::MessageBus;
|
||||
use crate::config::{LLMProviderConfig, MemoryMaintenanceConfig, SubagentsConfig, TaskConfig};
|
||||
use crate::config::{LLMProviderConfig, MemoryMaintenanceConfig, ModelResolver, SubagentsConfig, TaskConfig};
|
||||
use crate::gateway::tool_registry_factory::ToolRegistryFactory;
|
||||
use crate::mcp::McpInitializer;
|
||||
use crate::mcp::client::McpClientManager;
|
||||
@ -53,6 +53,7 @@ pub(crate) fn build_session_manager(
|
||||
session_ttl_hours: Option<u64>,
|
||||
mcp_config: crate::mcp::McpConfig,
|
||||
bus: Option<Arc<MessageBus>>,
|
||||
model_resolver: Arc<ModelResolver>,
|
||||
) -> Result<(SessionManager, Arc<dyn TaskRepository>, Option<Arc<McpClientManager>>, Arc<SubagentRuntime>), AgentError> {
|
||||
build_session_manager_with_sender(
|
||||
agent_prompt_reinject_every,
|
||||
@ -70,6 +71,7 @@ pub(crate) fn build_session_manager(
|
||||
session_ttl_hours,
|
||||
mcp_config,
|
||||
bus,
|
||||
model_resolver,
|
||||
)
|
||||
}
|
||||
|
||||
@ -90,6 +92,7 @@ pub(crate) fn build_session_manager_with_sender(
|
||||
session_ttl_hours: Option<u64>,
|
||||
mcp_config: crate::mcp::McpConfig,
|
||||
bus: Option<Arc<MessageBus>>,
|
||||
model_resolver: Arc<ModelResolver>,
|
||||
) -> Result<(SessionManager, Arc<dyn TaskRepository>, Option<Arc<McpClientManager>>, Arc<SubagentRuntime>), AgentError> {
|
||||
let store = Arc::new(
|
||||
SessionStore::new()
|
||||
@ -211,6 +214,7 @@ pub(crate) fn build_session_manager_with_sender(
|
||||
conversations.clone(),
|
||||
subagent_tools.clone(),
|
||||
provider_config.clone(),
|
||||
model_resolver.clone(),
|
||||
subagent_runtime.clone(),
|
||||
bus.clone(),
|
||||
store.clone(),
|
||||
@ -275,6 +279,7 @@ pub(crate) fn build_session_manager_with_sender(
|
||||
subagent_runtime.clone(),
|
||||
agent_prompt_reinject_every as usize,
|
||||
prompt_repository.clone(),
|
||||
model_resolver.clone(),
|
||||
);
|
||||
let session_factory = SessionFactory::new(
|
||||
provider_config.clone(),
|
||||
|
||||
@ -258,6 +258,11 @@ impl Session {
|
||||
let experts = Arc::new(crate::experts::ExpertRuntime::from_config(
|
||||
crate::config::ExpertsConfig::default(),
|
||||
));
|
||||
// Session::new 仅用于测试/简单场景,传入空 ModelResolver(无 provider/model 可解析覆盖)。
|
||||
let model_resolver = Arc::new(crate::config::ModelResolver::new(
|
||||
std::collections::HashMap::new(),
|
||||
std::collections::HashMap::new(),
|
||||
));
|
||||
let agent_factory = AgentFactory::new(
|
||||
tools,
|
||||
skills.clone(),
|
||||
@ -265,6 +270,7 @@ impl Session {
|
||||
subagent_runtime,
|
||||
agent_prompt_reinject_every as usize,
|
||||
prompt_repository.clone(),
|
||||
model_resolver,
|
||||
);
|
||||
Self::with_factories(
|
||||
channel_name,
|
||||
@ -688,6 +694,11 @@ impl SessionManager {
|
||||
let experts = Arc::new(crate::experts::ExpertRuntime::from_config(
|
||||
crate::config::ExpertsConfig::default(),
|
||||
));
|
||||
// SessionManager::new 用于测试/简单场景,传入空 ModelResolver(无 provider/model 可解析覆盖)。
|
||||
let model_resolver = Arc::new(crate::config::ModelResolver::new(
|
||||
std::collections::HashMap::new(),
|
||||
std::collections::HashMap::new(),
|
||||
));
|
||||
super::runtime::build_session_manager(
|
||||
agent_prompt_reinject_every,
|
||||
show_tool_results,
|
||||
@ -703,6 +714,7 @@ impl SessionManager {
|
||||
session_ttl_hours,
|
||||
mcp_config,
|
||||
None,
|
||||
model_resolver,
|
||||
)
|
||||
.map(|(session_manager, _, _, _)| session_manager)
|
||||
}
|
||||
|
||||
@ -105,6 +105,8 @@ mod tests {
|
||||
max_execution_secs: None,
|
||||
source: SubagentSource::Builtin,
|
||||
path: None,
|
||||
provider: None,
|
||||
model: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -349,6 +349,8 @@ pub struct DefaultSubAgentRuntime {
|
||||
conversation_repository: Arc<dyn ConversationRepository>,
|
||||
subagent_tools: Arc<ToolRegistry>,
|
||||
provider_config: LLMProviderConfig,
|
||||
/// Provider/Model 解析器:按子代理 def 中的 provider/model 字段覆盖基础配置
|
||||
model_resolver: Arc<crate::config::ModelResolver>,
|
||||
/// 子代理运行时协调层(管理禁用状态)
|
||||
subagent_runtime: Arc<SubagentRuntime>,
|
||||
bus: Option<Arc<MessageBus>>,
|
||||
@ -364,6 +366,7 @@ impl DefaultSubAgentRuntime {
|
||||
conversation_repository: Arc<dyn ConversationRepository>,
|
||||
subagent_tools: Arc<ToolRegistry>,
|
||||
provider_config: LLMProviderConfig,
|
||||
model_resolver: Arc<crate::config::ModelResolver>,
|
||||
subagent_runtime: Arc<SubagentRuntime>,
|
||||
bus: Option<Arc<MessageBus>>,
|
||||
store: Arc<SessionStore>,
|
||||
@ -375,6 +378,7 @@ impl DefaultSubAgentRuntime {
|
||||
conversation_repository,
|
||||
subagent_tools,
|
||||
provider_config,
|
||||
model_resolver,
|
||||
subagent_runtime,
|
||||
bus,
|
||||
store,
|
||||
@ -468,8 +472,25 @@ impl DefaultSubAgentRuntime {
|
||||
let child_depth = parent_nesting_depth + 1;
|
||||
let tools = self.build_subagent_tools_registry(def, child_depth);
|
||||
|
||||
// 按 def 中的 provider/model 字段解析覆盖基础 provider_config。
|
||||
// 引用不存在的 provider/model 名时返回错误(反馈给 LLM 重试,与 def 缺失即拒绝的安全范式一致)。
|
||||
let effective_provider_config = match def {
|
||||
Some(d) if d.provider.is_some() || d.model.is_some() => {
|
||||
self.model_resolver
|
||||
.resolve(d.provider.as_deref(), d.model.as_deref(), &self.provider_config)
|
||||
.map_err(|e| {
|
||||
TaskError::AgentCreationFailed(format!(
|
||||
"subagent '{}' model resolution failed: {}",
|
||||
def.map(|d| d.name.as_str()).unwrap_or("?"),
|
||||
e
|
||||
))
|
||||
})?
|
||||
}
|
||||
_ => self.provider_config.clone(),
|
||||
};
|
||||
|
||||
AgentLoop::with_tools_and_system_prompt_provider(
|
||||
AgentRuntimeConfig::from(self.provider_config.clone()),
|
||||
AgentRuntimeConfig::from(effective_provider_config),
|
||||
tools,
|
||||
prompt_provider,
|
||||
None, // 子代理不需要 skill provider
|
||||
@ -727,11 +748,22 @@ impl SubAgentRuntime for DefaultSubAgentRuntime {
|
||||
} else {
|
||||
self.skills.system_index_prompt()
|
||||
};
|
||||
// 同步解析 def 中的 provider/model 覆盖,保证环境提示中的模型名与实际使用的模型一致
|
||||
let effective_provider_config = match (def.provider.is_some(), def.model.is_some()) {
|
||||
(true, _) | (_, true) => self
|
||||
.model_resolver
|
||||
.resolve(def.provider.as_deref(), def.model.as_deref(), &self.provider_config)
|
||||
.map_err(|e| TaskError::AgentCreationFailed(format!(
|
||||
"subagent '{}' model resolution failed: {}",
|
||||
def.name, e
|
||||
)))?,
|
||||
_ => self.provider_config.clone(),
|
||||
};
|
||||
let system_prompt = SubagentPromptBuilder::build(
|
||||
&def,
|
||||
&task.description,
|
||||
&task.prompt,
|
||||
&self.provider_config,
|
||||
&effective_provider_config,
|
||||
skills_index.as_deref(),
|
||||
);
|
||||
|
||||
@ -1051,6 +1083,12 @@ pub struct SubagentWithStatus {
|
||||
/// 工具与技能加载策略。
|
||||
#[serde(default)]
|
||||
pub capability: CapabilityPolicy,
|
||||
/// 可选的 provider 名(引用 config.json 的 providers 表)。None 时继承主智能体。
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub provider: Option<String>,
|
||||
/// 可选的 model 名(引用 config.json 的 models 表)。None 时继承主智能体。
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub model: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@ -1205,6 +1243,8 @@ impl SubagentRuntime {
|
||||
source: def.source.as_str().to_string(),
|
||||
disabled_in_scopes: scopes.iter().map(|s| s.as_str().to_string()).collect(),
|
||||
capability: def.capability.clone(),
|
||||
provider: def.provider.clone(),
|
||||
model: def.model.clone(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
@ -1398,7 +1438,7 @@ impl SubagentRuntime {
|
||||
|
||||
/// 更新子代理定义(写回 SUBAGENT.md frontmatter)。
|
||||
/// 对齐 `ExpertRuntime::update_expert`。
|
||||
/// - `description`/`body`/`capability` 为 None 时保留原值。
|
||||
/// - `description`/`body`/`capability`/`provider`/`model` 为 None 时保留原值。
|
||||
/// - `prompt_template`/`max_execution_secs` 不在 UI 暴露编辑,始终保留原值。
|
||||
/// - builtin 子代理(`source == Builtin`、`path == None`)禁止 update。
|
||||
pub fn update_subagent(
|
||||
@ -1407,6 +1447,8 @@ impl SubagentRuntime {
|
||||
description: Option<&str>,
|
||||
body: Option<&str>,
|
||||
capability: Option<&CapabilityPolicy>,
|
||||
provider: Option<&Option<String>>,
|
||||
model: Option<&Option<String>>,
|
||||
reload: bool,
|
||||
) -> Result<SubagentDef, String> {
|
||||
let def = {
|
||||
@ -1430,6 +1472,8 @@ impl SubagentRuntime {
|
||||
let next_description = description.unwrap_or(&def.description);
|
||||
let next_body = body.unwrap_or(def.body.as_deref().unwrap_or(""));
|
||||
let next_capability = capability.cloned().unwrap_or_else(|| def.capability.clone());
|
||||
let next_provider = provider.cloned().unwrap_or(def.provider);
|
||||
let next_model = model.cloned().unwrap_or(def.model);
|
||||
|
||||
write_subagent_file(
|
||||
path,
|
||||
@ -1439,6 +1483,8 @@ impl SubagentRuntime {
|
||||
next_body,
|
||||
&next_capability,
|
||||
def.max_execution_secs,
|
||||
&next_provider,
|
||||
&next_model,
|
||||
)?;
|
||||
|
||||
let new_def = parse_subagent_file(path, def.source.clone())?;
|
||||
@ -1565,6 +1611,10 @@ struct SubagentFrontmatter {
|
||||
denied_subagents: Vec<String>,
|
||||
#[serde(default)]
|
||||
max_execution_secs: Option<u64>,
|
||||
#[serde(default)]
|
||||
provider: Option<String>,
|
||||
#[serde(default)]
|
||||
model: Option<String>,
|
||||
}
|
||||
|
||||
/// 从根目录加载所有子代理
|
||||
@ -1657,6 +1707,15 @@ fn parse_subagent_file(path: &Path, source: SubagentSource) -> Result<SubagentDe
|
||||
denied_subagents: frontmatter.denied_subagents,
|
||||
};
|
||||
|
||||
let provider = frontmatter
|
||||
.provider
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty());
|
||||
let model = frontmatter
|
||||
.model
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty());
|
||||
|
||||
Ok(SubagentDef {
|
||||
name,
|
||||
description: frontmatter.description.trim().to_string(),
|
||||
@ -1666,6 +1725,8 @@ fn parse_subagent_file(path: &Path, source: SubagentSource) -> Result<SubagentDe
|
||||
max_execution_secs: frontmatter.max_execution_secs,
|
||||
source,
|
||||
path: Some(path.to_path_buf()),
|
||||
provider,
|
||||
model,
|
||||
})
|
||||
}
|
||||
|
||||
@ -1678,6 +1739,8 @@ fn render_subagent_file(
|
||||
body: &str,
|
||||
capability: &CapabilityPolicy,
|
||||
max_execution_secs: Option<u64>,
|
||||
provider: &Option<String>,
|
||||
model: &Option<String>,
|
||||
) -> Result<String, String> {
|
||||
if description.trim().is_empty() {
|
||||
return Err("description is required and cannot be empty".to_string());
|
||||
@ -1703,6 +1766,10 @@ fn render_subagent_file(
|
||||
denied_subagents: Vec<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
max_execution_secs: Option<u64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
provider: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
model: Option<String>,
|
||||
}
|
||||
|
||||
let fm = SubagentFrontmatterOwned {
|
||||
@ -1720,6 +1787,8 @@ fn render_subagent_file(
|
||||
allowed_subagents: capability.allowed_subagents.clone(),
|
||||
denied_subagents: capability.denied_subagents.clone(),
|
||||
max_execution_secs,
|
||||
provider: provider.clone(),
|
||||
model: model.clone(),
|
||||
};
|
||||
|
||||
let yaml = serde_yaml::to_string(&fm)
|
||||
@ -1742,6 +1811,8 @@ fn write_subagent_file(
|
||||
body: &str,
|
||||
capability: &CapabilityPolicy,
|
||||
max_execution_secs: Option<u64>,
|
||||
provider: &Option<String>,
|
||||
model: &Option<String>,
|
||||
) -> Result<(), String> {
|
||||
let content = render_subagent_file(
|
||||
name,
|
||||
@ -1750,6 +1821,8 @@ fn write_subagent_file(
|
||||
body,
|
||||
capability,
|
||||
max_execution_secs,
|
||||
provider,
|
||||
model,
|
||||
)?;
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
@ -1965,6 +2038,8 @@ mod tests {
|
||||
denied_skills: Vec::new(),
|
||||
allowed_tools: allowed.map(|v| v.iter().map(|x| x.to_string()).collect()),
|
||||
denied_tools: denied.iter().map(|x| x.to_string()).collect(),
|
||||
allowed_subagents: None,
|
||||
denied_subagents: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
@ -2125,10 +2200,14 @@ mod tests {
|
||||
denied_skills: Vec::new(),
|
||||
allowed_tools: Some(vec!["read".to_string(), "todo_write".to_string()]),
|
||||
denied_tools: vec!["bash".to_string()],
|
||||
allowed_subagents: None,
|
||||
denied_subagents: vec![],
|
||||
},
|
||||
max_execution_secs: None,
|
||||
source: SubagentSource::Builtin,
|
||||
path: None,
|
||||
provider: None,
|
||||
model: None,
|
||||
});
|
||||
let runtime = SubagentRuntime::new(
|
||||
SubagentsConfig::default(),
|
||||
@ -2154,6 +2233,8 @@ mod tests {
|
||||
denied_skills: vec!["skill_b".to_string()],
|
||||
allowed_tools: Some(vec!["read".to_string()]),
|
||||
denied_tools: vec!["bash".to_string()],
|
||||
allowed_subagents: None,
|
||||
denied_subagents: vec![],
|
||||
};
|
||||
let content = render_subagent_file(
|
||||
"demo",
|
||||
@ -2162,6 +2243,8 @@ mod tests {
|
||||
"body instructions",
|
||||
&cap,
|
||||
Some(1800),
|
||||
&None,
|
||||
&None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@ -2197,6 +2280,8 @@ mod tests {
|
||||
"body",
|
||||
&cap,
|
||||
None,
|
||||
&None,
|
||||
&None,
|
||||
)
|
||||
.unwrap();
|
||||
// 空 capability 字段不应出现在 YAML 中
|
||||
@ -2221,6 +2306,8 @@ mod tests {
|
||||
"initial body",
|
||||
&CapabilityPolicy::default(),
|
||||
None,
|
||||
&None,
|
||||
&None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@ -2236,9 +2323,11 @@ mod tests {
|
||||
denied_skills: vec!["skill_x".to_string()],
|
||||
allowed_tools: Some(vec!["read".to_string()]),
|
||||
denied_tools: vec!["bash".to_string()],
|
||||
allowed_subagents: None,
|
||||
denied_subagents: vec![],
|
||||
};
|
||||
let updated = runtime
|
||||
.update_subagent("demo", Some("updated desc"), None, Some(&new_cap), false)
|
||||
.update_subagent("demo", Some("updated desc"), None, Some(&new_cap), Some(&None), Some(&None), false)
|
||||
.unwrap();
|
||||
assert_eq!(updated.description, "updated desc");
|
||||
assert_eq!(updated.capability.denied_skills, vec!["skill_x".to_string()]);
|
||||
@ -2262,6 +2351,8 @@ mod tests {
|
||||
Some("new desc"),
|
||||
None,
|
||||
None,
|
||||
Some(&None),
|
||||
Some(&None),
|
||||
false,
|
||||
);
|
||||
assert!(result.is_err());
|
||||
|
||||
@ -70,6 +70,10 @@ pub struct SubagentDef {
|
||||
pub source: SubagentSource,
|
||||
/// 文件路径(仅自定义类型)
|
||||
pub path: Option<PathBuf>,
|
||||
/// 可选的 provider 名(引用 config.json 的 providers 表)。None 时继承主智能体。
|
||||
pub provider: Option<String>,
|
||||
/// 可选的 model 名(引用 config.json 的 models 表)。None 时继承主智能体。
|
||||
pub model: Option<String>,
|
||||
}
|
||||
|
||||
impl SubagentDef {
|
||||
@ -84,6 +88,8 @@ impl SubagentDef {
|
||||
max_execution_secs: None,
|
||||
source: SubagentSource::Builtin,
|
||||
path: None,
|
||||
provider: None,
|
||||
model: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -187,6 +187,7 @@ mod tests {
|
||||
task_id: None,
|
||||
parent_task_id: None,
|
||||
tool_call_id: None,
|
||||
parent_capability: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -472,6 +472,7 @@ mod tests {
|
||||
task_id: None,
|
||||
parent_task_id: None,
|
||||
tool_call_id: None,
|
||||
parent_capability: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user