The PRD's first acceptance gate now holds: grep -RinE '\bx\.ai\b|grok' crates/ --include='*.rs' → 0 matches (exempt: NOTICE and third-party license archives, README provenance, and the required 'Based on Grok Build Open Source' attribution, now sourced from version_attribution.txt). Wire-visible renames (both sides in this repo, changed in lockstep): - Auth method id 'grok.com' → 'kimi-code' (AuthMethodKind::KimiCode). - Every x.ai/* and _x.ai/* ACP ext method and meta key → kigi/* / _kigi/* (~200 names; grokShell → kigiShell). Session-file replay keeps a read-side alias for the legacy '_x.ai/session/update' method so existing updates.jsonl histories load; writes emit only the new name (both directions test-pinned). - Agent types grok-build* → kigi* with a documented legacy-prefix alias at resolution time so persisted sessions keep resolving. - ToolNamespace/BuiltinAgentName GrokBuild* → Kigi* (wire snake_case kigi/kigi_concise/kigi_hashline; schema regenerated); grok_build implementation dirs renamed to kigi*. - x-grok-* headers → x-kigi-*, __GROK_* sentinels → __KIGI_*, themes grokday/groknight → kigiday/kiginight (old persisted values fall back to the default theme), web_fetch allowlist xAI hosts → kimi.com + moonshot platforms, changelog CDN → this repo, grok-build changelog archives deleted. - BYOK default endpoint removed: [endpoints] api_base_url is now truly optional with NO default — consumers fail fast with the flag name when unset (no silent x.ai egress). Mock harnesses inject it explicitly. - System-prompt identity fixed: 'released by xAI' → 'an unofficial community CLI for Kimi' (template + regenerated encrypted form). Also repaired pre-existing grok-era test debt found by the sweep: the stale trace_classify default-model pin, the grok-pager UA label test, pty-harness stale-binary reuse and non-hermetic moonshot routing (a PTY test could previously reach the real api.moonshot.cn), and the outdated oauth fixture scope key. Gates: §9 grep 0; fmt clean; workspace check/clippy 0/0 (-D warnings); FULL cargo test --workspace: 234 suites, 21,961 passed, 0 failed; deny advisories ok.
496 lines
22 KiB
Rust
496 lines
22 KiB
Rust
//! `AgentRebuildSpec` — the canonical recipe for constructing an
|
|
//! [`kigi_agent::Agent`] for a given session.
|
|
//!
|
|
//! INVARIANT: This is the **only** place in the shell crate that calls
|
|
//! [`kigi_agent::AgentBuilder::new`]. Both initial session spawn
|
|
//! ([`crate::session::acp_session::spawn_session_actor`]) and zero-turn
|
|
//! harness rebuild
|
|
//! ([`crate::session::acp_session::SessionActor::handle_rebuild_agent_for_definition`])
|
|
//! go through [`AgentRebuildSpec::build_agent`].
|
|
//!
|
|
//! ## Why this exists
|
|
//!
|
|
//! [`kigi_agent::Agent`] owns an [`kigi_tools::bridge::ToolBridge`]
|
|
//! that carries session-scoped channels (notification handle, terminal/fs
|
|
//! backends, subagent senders, scheduler set, plugin registry, attribution
|
|
//! callback). The Agent is therefore session-bound — it cannot be shared
|
|
//! across sessions and cannot be re-rendered from outside its session
|
|
//! context. To rebuild it (e.g. when the user picks a model with a
|
|
//! different `agent_type` before sending any user message), we need to
|
|
//! retain every input that the original `AgentBuilder` chain consumed.
|
|
//! `AgentRebuildSpec` is exactly that retained bag of inputs.
|
|
//!
|
|
//! ## WHEN ADDING A NEW [`kigi_agent::AgentBuilder`]`::with_*` KNOB
|
|
//!
|
|
//! 1. Add the corresponding field to [`AgentRebuildSpec`].
|
|
//! 2. Pass it through in [`AgentRebuildSpec::build_agent`]. The destructure
|
|
//! pattern at the top of `build_agent` forces every field to be used —
|
|
//! drift is a compile error (`#[deny(unused_variables)]`).
|
|
//! 3. Populate the field at the call site in `spawn_session_actor`.
|
|
//!
|
|
//! ## Why some fields are channel senders
|
|
//!
|
|
//! Several `ToolBridge` resources (e.g. `UserQuestionSender`,
|
|
//! `SubagentBackendResource`) are backed by the `tx` half of channels
|
|
//! whose `rx` halves are owned by long-lived coordinator tasks spawned
|
|
//! in `spawn_session_actor`. The subagent channels are wrapped in a
|
|
//! `ChannelBackend` behind `SubagentBackendResource`. On rebuild, we
|
|
//! must reuse the **same** senders so the existing coordinator keeps
|
|
//! receiving requests; we cannot mint a fresh channel without orphaning
|
|
//! the running coordinator.
|
|
use kigi_agent::config::AgentDefinition;
|
|
use kigi_agent::error::AgentBuildError;
|
|
use kigi_agent::prompt::context::PromptAudience;
|
|
use kigi_agent::prompt::skills::SkillsConfig;
|
|
use kigi_agent::{Agent, AgentBuilder, CompactionPolicy, ReminderPolicy};
|
|
use kigi_tools::computer::types::{AsyncFileSystem, TerminalBackend};
|
|
use kigi_tools::implementations::kigi::ask_user_question::types::UserQuestionRequest;
|
|
use kigi_tools::implementations::kigi::deploy_app::AppBuilderDeployerConfig;
|
|
use kigi_tools::implementations::kigi::task::types::{
|
|
MonitorEventBuffer, SubagentEvent, TaskModelValidator,
|
|
};
|
|
use kigi_tools::implementations::kigi::web_fetch::WebFetchConfig;
|
|
use kigi_tools::implementations::lsp::LspBackend;
|
|
use kigi_tools::implementations::web_search::WebSearchConfig;
|
|
use kigi_tools::notification::ToolNotificationHandle;
|
|
use kigi_tools::types::SharedApiKeyProvider;
|
|
use kigi_tools::types::compat::CompatConfig;
|
|
use kigi_tools::types::memory_backend::MemoryBackend;
|
|
use std::collections::HashMap;
|
|
use std::path::PathBuf;
|
|
use std::sync::Arc;
|
|
use tokio::sync::mpsc::UnboundedSender;
|
|
/// Shell-resolved per-tool `ToolConfig.params` JSON maps, bundled into one
|
|
/// named struct so the spawn telescopes carry a single argument instead of
|
|
/// adjacent identically-typed positionals that a caller could transpose.
|
|
#[derive(Debug, Clone, Default)]
|
|
pub(crate) struct ResolvedToolParamsJson {
|
|
/// `[toolset.bash]` overrides for the bash tool(s).
|
|
pub bash: Option<serde_json::Map<String, serde_json::Value>>,
|
|
/// `[toolset.ask_user_question]` timeout policy for the ask tool.
|
|
pub ask_user_question: Option<serde_json::Map<String, serde_json::Value>>,
|
|
}
|
|
/// Cached recipe for building a session-scoped [`Agent`].
|
|
///
|
|
/// See module docs for the invariant: this is the only construction
|
|
/// site for `Agent` in the shell crate. Cloning is intentionally not
|
|
/// derived — the spec lives behind an [`Arc`] and is shared by clone of
|
|
/// that `Arc`.
|
|
pub(crate) struct AgentRebuildSpec {
|
|
pub working_directory: PathBuf,
|
|
pub terminal_backend: Arc<dyn TerminalBackend>,
|
|
pub fs_backend: Arc<dyn AsyncFileSystem>,
|
|
pub tools_notification_handle: ToolNotificationHandle,
|
|
pub bridge_state_path: PathBuf,
|
|
pub session_env: Arc<HashMap<String, String>>,
|
|
pub models_manager: crate::agent::models::ModelsManager,
|
|
pub compaction_policy: CompactionPolicy,
|
|
pub reminder_policy: ReminderPolicy,
|
|
pub memory_enabled: bool,
|
|
pub memory_global_path: Option<String>,
|
|
pub memory_workspace_path: Option<String>,
|
|
pub memory_backend: Option<Arc<dyn MemoryBackend>>,
|
|
pub web_search_config: WebSearchConfig,
|
|
pub backend_search: bool,
|
|
pub web_fetch_config: WebFetchConfig,
|
|
pub app_builder_deployer_config: AppBuilderDeployerConfig,
|
|
pub write_file_enabled: bool,
|
|
pub subagents_enabled: bool,
|
|
pub subagent_toggle: HashMap<String, bool>,
|
|
pub ask_user_question_enabled: bool,
|
|
pub persona_summaries: Vec<String>,
|
|
pub prompt_audience: PromptAudience,
|
|
pub role_instructions: Option<String>,
|
|
pub persona_instructions: Option<String>,
|
|
pub skills_config: SkillsConfig,
|
|
/// Resolved vendor-compat config (from `Config::compat_resolved`), threaded
|
|
/// into skills / rules / AGENTS.md discovery via the builder.
|
|
pub compat: CompatConfig,
|
|
pub context_window_tokens: u64,
|
|
pub prompt_working_directory: Option<String>,
|
|
pub lsp: Option<Arc<dyn LspBackend>>,
|
|
pub plugin_registry: Option<Arc<kigi_agent::plugins::PluginRegistry>>,
|
|
pub api_key_provider: Option<SharedApiKeyProvider>,
|
|
pub attribution_callback: Option<kigi_tools::SharedAttributionCallback>,
|
|
pub tool_params_json: ResolvedToolParamsJson,
|
|
pub subagent_event_tx: Option<UnboundedSender<SubagentEvent>>,
|
|
pub monitor_event_buffer: Option<MonitorEventBuffer>,
|
|
pub user_question_tx: UnboundedSender<UserQuestionRequest>,
|
|
pub subagent_depth: u32,
|
|
pub session_id_str: String,
|
|
pub respect_gitignore: bool,
|
|
pub path_not_found_hints: bool,
|
|
pub mcp_state: Arc<tokio::sync::Mutex<crate::session::mcp_servers::McpState>>,
|
|
pub is_non_interactive: bool,
|
|
pub system_prompt_label: String,
|
|
pub owner_session_id: Option<String>,
|
|
pub parent_scheduler_handle:
|
|
Option<kigi_tools::implementations::kigi::scheduler::types::SchedulerHandle>,
|
|
}
|
|
impl AgentRebuildSpec {
|
|
/// Build a fresh [`Agent`] from this spec and an [`AgentDefinition`].
|
|
///
|
|
/// This is the canonical construction path; see module docs for the
|
|
/// invariant. The destructure pattern below is intentional —
|
|
/// `#[deny(unused_variables)]` ensures any newly added spec field is
|
|
/// used here, otherwise compilation fails.
|
|
#[deny(unused_variables)]
|
|
pub async fn build_agent(
|
|
self: &Arc<Self>,
|
|
definition: AgentDefinition,
|
|
) -> Result<Agent, AgentBuildError> {
|
|
self.build_agent_inner(definition, None, None).await
|
|
}
|
|
/// Build an agent with optional one-shot overrides for initial spawn.
|
|
///
|
|
/// `persisted_skill_names`: restored into the `SkillManager` before
|
|
/// `seed()` to prevent duplicate system-reminder injection on resume.
|
|
///
|
|
/// `preloaded_skills`: parent-discovered skills passed to
|
|
/// `AgentBuilder::with_preloaded_skills()` to bypass filesystem
|
|
/// discovery in subagents.
|
|
///
|
|
/// Both are consumed once — the rebuild path (`build_agent`) passes
|
|
/// `None` for both so zero-turn model switches get fresh discovery.
|
|
pub async fn build_agent_with_initial_overrides(
|
|
self: &Arc<Self>,
|
|
definition: AgentDefinition,
|
|
persisted_skill_names: Option<std::collections::HashSet<String>>,
|
|
preloaded_skills: Option<Vec<kigi_tools::implementations::skills::types::SkillInfo>>,
|
|
) -> Result<Agent, AgentBuildError> {
|
|
self.build_agent_inner(definition, persisted_skill_names, preloaded_skills)
|
|
.await
|
|
}
|
|
#[deny(unused_variables)]
|
|
async fn build_agent_inner(
|
|
self: &Arc<Self>,
|
|
definition: AgentDefinition,
|
|
persisted_skill_names: Option<std::collections::HashSet<String>>,
|
|
preloaded_skills: Option<Vec<kigi_tools::implementations::skills::types::SkillInfo>>,
|
|
) -> Result<Agent, AgentBuildError> {
|
|
let Self {
|
|
working_directory,
|
|
terminal_backend,
|
|
fs_backend,
|
|
tools_notification_handle,
|
|
bridge_state_path,
|
|
session_env,
|
|
models_manager,
|
|
compaction_policy,
|
|
reminder_policy,
|
|
memory_enabled,
|
|
memory_global_path,
|
|
memory_workspace_path,
|
|
memory_backend,
|
|
web_search_config,
|
|
backend_search,
|
|
web_fetch_config,
|
|
app_builder_deployer_config,
|
|
write_file_enabled,
|
|
subagents_enabled,
|
|
subagent_toggle,
|
|
ask_user_question_enabled,
|
|
persona_summaries,
|
|
prompt_audience,
|
|
role_instructions,
|
|
persona_instructions,
|
|
skills_config,
|
|
compat,
|
|
context_window_tokens,
|
|
prompt_working_directory,
|
|
lsp,
|
|
plugin_registry,
|
|
api_key_provider,
|
|
attribution_callback,
|
|
tool_params_json,
|
|
subagent_event_tx,
|
|
monitor_event_buffer,
|
|
user_question_tx,
|
|
subagent_depth,
|
|
session_id_str,
|
|
respect_gitignore,
|
|
path_not_found_hints,
|
|
mcp_state,
|
|
is_non_interactive,
|
|
system_prompt_label,
|
|
owner_session_id,
|
|
parent_scheduler_handle,
|
|
} = self.as_ref();
|
|
let _ = mcp_state;
|
|
#[allow(unused_variables)]
|
|
let is_cursor_template =
|
|
crate::session::is_cursor_system_template(&definition.system_prompt);
|
|
let mut builder = AgentBuilder::new(
|
|
working_directory.clone(),
|
|
terminal_backend.clone(),
|
|
tools_notification_handle.clone(),
|
|
)
|
|
.from_definition(definition)
|
|
.with_compaction_policy(compaction_policy.clone())
|
|
.with_reminder_policy(reminder_policy.clone())
|
|
.with_memory_enabled(*memory_enabled)
|
|
.with_memory_paths(memory_global_path.clone(), memory_workspace_path.clone())
|
|
.with_is_non_interactive(*is_non_interactive)
|
|
.with_system_prompt_label(system_prompt_label.clone())
|
|
.with_session_env(session_env.clone())
|
|
.with_state_path(bridge_state_path.clone())
|
|
.with_web_search_config(web_search_config.clone())
|
|
.with_backend_search(*backend_search)
|
|
.with_app_builder_deployer_config(app_builder_deployer_config.clone())
|
|
.with_web_fetch_config(web_fetch_config.clone())
|
|
.with_write_file_enabled(*write_file_enabled)
|
|
.with_fs(fs_backend.clone())
|
|
.with_subagents_enabled(*subagents_enabled)
|
|
.with_subagent_toggle(subagent_toggle.clone())
|
|
.with_task_model_slugs(
|
|
models_manager
|
|
.available()
|
|
.keys()
|
|
.map(|model_id| model_id.0.to_string())
|
|
.collect::<Vec<_>>(),
|
|
)
|
|
.with_ask_user_question_enabled(*ask_user_question_enabled)
|
|
.with_persona_summaries(persona_summaries.clone())
|
|
.with_prompt_audience(*prompt_audience)
|
|
.with_role_instructions(role_instructions.clone())
|
|
.with_persona_instructions(persona_instructions.clone())
|
|
.with_skills_config(skills_config.clone())
|
|
.with_compat_config(*compat)
|
|
.with_context_window(*context_window_tokens)
|
|
.with_mcp_max_output_bytes(
|
|
crate::util::config::resolve_max_mcp_output_bytes_for_cwd(working_directory),
|
|
);
|
|
if let Some(owner_session_id) = owner_session_id.clone() {
|
|
builder = builder.with_owner_session_id(owner_session_id);
|
|
}
|
|
if let Some(handle) = parent_scheduler_handle.clone() {
|
|
builder = builder.with_parent_scheduler_handle(handle);
|
|
}
|
|
if let Some(memory_backend) = memory_backend.clone() {
|
|
builder = builder.with_memory_backend(memory_backend);
|
|
}
|
|
if let Some(lsp) = lsp.clone() {
|
|
builder = builder.with_lsp(lsp);
|
|
}
|
|
if let Some(plugin_registry) = plugin_registry.clone() {
|
|
builder = builder.with_plugin_registry(plugin_registry);
|
|
}
|
|
if let Some(api_key_provider) = api_key_provider.clone() {
|
|
builder = builder.with_api_key_provider(api_key_provider);
|
|
}
|
|
if let Some(attribution_callback) = attribution_callback.clone() {
|
|
builder = builder.with_attribution_callback(attribution_callback);
|
|
}
|
|
if let Some(bash_params_json) = tool_params_json.bash.clone() {
|
|
builder = builder.with_bash_params(bash_params_json);
|
|
}
|
|
if let Some(ask_user_question_params_json) = tool_params_json.ask_user_question.clone() {
|
|
builder = builder.with_ask_user_question_params(ask_user_question_params_json);
|
|
}
|
|
if let Some(prompt_working_directory) = prompt_working_directory.clone() {
|
|
builder = builder.with_prompt_working_directory(prompt_working_directory);
|
|
}
|
|
if let Some(names) = persisted_skill_names {
|
|
builder = builder.with_persisted_announced_skill_names(names);
|
|
}
|
|
if let Some(skills) = preloaded_skills {
|
|
builder = builder.with_preloaded_skills(skills);
|
|
}
|
|
let agent = builder.build().await?;
|
|
let model_validator = models_manager.clone();
|
|
agent
|
|
.tool_bridge()
|
|
.update_resource(TaskModelValidator::new(move |requested| {
|
|
model_validator.task_model_error(requested)
|
|
}))
|
|
.await;
|
|
if let Some(event_tx) = subagent_event_tx.clone() {
|
|
use kigi_tools::implementations::kigi::task::backend::{
|
|
ChannelBackend, SubagentBackendResource,
|
|
};
|
|
use kigi_tools::implementations::kigi::task::types::{
|
|
SessionIdResource, SubagentDepthCounter, SubagentEventSender,
|
|
};
|
|
let backend = SubagentBackendResource(Arc::new(ChannelBackend::new(event_tx.clone())));
|
|
agent.tool_bridge().update_resource(backend).await;
|
|
agent
|
|
.tool_bridge()
|
|
.update_resource(SubagentDepthCounter(*subagent_depth))
|
|
.await;
|
|
agent
|
|
.tool_bridge()
|
|
.update_resource(SessionIdResource(session_id_str.clone()))
|
|
.await;
|
|
agent
|
|
.tool_bridge()
|
|
.update_resource(SubagentEventSender(event_tx))
|
|
.await;
|
|
if let Some(buffer) = monitor_event_buffer.clone() {
|
|
agent.tool_bridge().update_resource(buffer).await;
|
|
}
|
|
}
|
|
agent
|
|
.tool_bridge()
|
|
.update_resource(kigi_tools::types::resources::RespectGitignore(
|
|
*respect_gitignore,
|
|
))
|
|
.await;
|
|
agent
|
|
.tool_bridge()
|
|
.update_resource(kigi_tools::types::resources::PathNotFoundHints(
|
|
*path_not_found_hints,
|
|
))
|
|
.await;
|
|
{
|
|
use kigi_tools::implementations::kigi::ask_user_question::UserQuestionSender;
|
|
agent
|
|
.tool_bridge()
|
|
.update_resource(UserQuestionSender(user_question_tx.clone()))
|
|
.await;
|
|
}
|
|
Ok(agent)
|
|
}
|
|
}
|
|
/// Build a stub [`AgentRebuildSpec`] for unit tests.
|
|
///
|
|
/// Every field is set to a minimal default suitable for test `SessionActor`
|
|
/// literals and focused `build_agent` tests.
|
|
#[cfg(test)]
|
|
pub(crate) fn test_rebuild_spec_default() -> Arc<AgentRebuildSpec> {
|
|
let (uq_tx, _uq_rx) = tokio::sync::mpsc::unbounded_channel();
|
|
Arc::new(AgentRebuildSpec {
|
|
working_directory: std::env::temp_dir(),
|
|
terminal_backend: Arc::new(
|
|
kigi_tools::computer::local::LocalTerminalBackend::new_local(
|
|
kigi_tools::computer::local::SearchShadowConfig::default(),
|
|
),
|
|
),
|
|
fs_backend: Arc::new(kigi_tools::computer::local::LocalFs),
|
|
tools_notification_handle: ToolNotificationHandle::noop(),
|
|
bridge_state_path: std::env::temp_dir().join("test_tool_state.json"),
|
|
session_env: Arc::new(HashMap::new()),
|
|
models_manager: crate::agent::models::ModelsManager::default(),
|
|
compaction_policy: CompactionPolicy::default(),
|
|
reminder_policy: ReminderPolicy::default(),
|
|
memory_enabled: false,
|
|
memory_global_path: None,
|
|
memory_workspace_path: None,
|
|
memory_backend: None,
|
|
web_search_config: WebSearchConfig::default(),
|
|
backend_search: false,
|
|
web_fetch_config: WebFetchConfig::Disabled,
|
|
app_builder_deployer_config: AppBuilderDeployerConfig::default(),
|
|
write_file_enabled: true,
|
|
subagents_enabled: false,
|
|
subagent_toggle: HashMap::new(),
|
|
ask_user_question_enabled: true,
|
|
persona_summaries: vec![],
|
|
prompt_audience: PromptAudience::Primary,
|
|
role_instructions: None,
|
|
persona_instructions: None,
|
|
skills_config: SkillsConfig::default(),
|
|
compat: CompatConfig::default(),
|
|
context_window_tokens: 256_000,
|
|
prompt_working_directory: None,
|
|
lsp: None,
|
|
plugin_registry: None,
|
|
api_key_provider: None,
|
|
attribution_callback: None,
|
|
tool_params_json: ResolvedToolParamsJson::default(),
|
|
subagent_event_tx: None,
|
|
monitor_event_buffer: None,
|
|
user_question_tx: uq_tx,
|
|
subagent_depth: 0,
|
|
session_id_str: "test-session".to_string(),
|
|
respect_gitignore: false,
|
|
path_not_found_hints: false,
|
|
mcp_state: Arc::new(tokio::sync::Mutex::new(
|
|
crate::session::mcp_servers::McpState::new(vec![]),
|
|
)),
|
|
is_non_interactive: false,
|
|
system_prompt_label: kigi_agent::DEFAULT_SYSTEM_PROMPT_LABEL.to_string(),
|
|
owner_session_id: Some("test-session".to_string()),
|
|
parent_scheduler_handle: None,
|
|
})
|
|
}
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::agent::config::{EndpointsConfig, ModelEntry};
|
|
fn model_entry(internal_id: &str) -> ModelEntry {
|
|
ModelEntry::fallback(internal_id, &EndpointsConfig::default())
|
|
}
|
|
fn task_description(agent: &Agent) -> String {
|
|
let toolset = agent.tool_bridge().toolset();
|
|
let task_name = toolset
|
|
.tool_name_for_kind(kigi_tools::types::tool::ToolKind::Task)
|
|
.expect("Kigi Task tool should be present");
|
|
toolset
|
|
.tool_definitions()
|
|
.into_iter()
|
|
.find(|definition| definition.function.name == task_name)
|
|
.and_then(|definition| definition.function.description)
|
|
.expect("Kigi Task description should be present")
|
|
}
|
|
#[tokio::test(flavor = "current_thread")]
|
|
async fn rebuild_projects_fresh_public_model_keys_into_task_description() {
|
|
tokio::task::LocalSet::new()
|
|
.run_until(async {
|
|
let mut spec = test_rebuild_spec_default();
|
|
Arc::get_mut(&mut spec)
|
|
.expect("test rebuild spec should be uniquely owned")
|
|
.subagents_enabled = true;
|
|
let models_manager = spec.models_manager.clone();
|
|
models_manager
|
|
.insert_test_entry("zeta-public", model_entry("internal-zeta"));
|
|
models_manager
|
|
.insert_test_entry("alpha-public", model_entry("internal-alpha"));
|
|
let mut hidden = model_entry("internal-hidden");
|
|
hidden.info.hidden = true;
|
|
models_manager.insert_test_entry("private-hidden-model", hidden);
|
|
let mut unselectable = model_entry("internal-unselectable");
|
|
unselectable.info.user_selectable = false;
|
|
models_manager
|
|
.insert_test_entry("private-unselectable-model", unselectable);
|
|
let first = spec
|
|
.build_agent(AgentDefinition::default_kigi())
|
|
.await
|
|
.expect("first agent build should succeed");
|
|
let first_description = task_description(&first);
|
|
assert!(
|
|
first_description
|
|
.contains("If the user explicitly asks for the model of a subagent/task, you may ONLY use model slugs from this list:\n\
|
|
- alpha-public\n\
|
|
- zeta-public")
|
|
);
|
|
assert!(! first_description.contains("private-hidden-model"));
|
|
assert!(! first_description.contains("private-unselectable-model"));
|
|
assert!(! first_description.contains("internal-alpha"));
|
|
let validator = first
|
|
.tool_bridge()
|
|
.toolset()
|
|
.get_resource_cloned::<TaskModelValidator>()
|
|
.await
|
|
.expect("Task model validator should be registered");
|
|
assert!(validator.error_for("alpha-public").is_none());
|
|
assert!(validator.error_for("private-hidden-model").is_some());
|
|
models_manager
|
|
.insert_test_entry("beta-public", model_entry("internal-beta"));
|
|
assert!(validator.error_for("beta-public").is_none());
|
|
let rebuilt = spec
|
|
.build_agent(AgentDefinition::default_kigi())
|
|
.await
|
|
.expect("rebuilt agent should succeed");
|
|
let rebuilt_description = task_description(&rebuilt);
|
|
assert!(
|
|
rebuilt_description
|
|
.contains("If the user explicitly asks for the model of a subagent/task, you may ONLY use model slugs from this list:\n\
|
|
- alpha-public\n\
|
|
- beta-public\n\
|
|
- zeta-public")
|
|
);
|
|
})
|
|
.await;
|
|
}
|
|
}
|