Port of upstream grok-build 1.0.1 (landed upstream 2026-08-06). The coordinator fire-and-forgot every spawn, so a wide fan-out (graph waves, swarm bursts, model-driven Task storms) could exhaust file descriptors. - kigi-tools task/admission: SubagentLimits — 32 concurrent per session by default (KIGI_MAX_CONCURRENT_SUBAGENTS), queue|fail behavior (KIGI_SUBAGENT_LIMIT_BEHAVIOR), zero clamped to 1, read once at the composition root; parse_positive_env helper. - Each parent session gets one spawn semaphore (map entries live for the process so a resumed session shares its predecessor's limit); handle_subagent_request holds an owned permit for the whole child run — queue awaits FIFO with a unified-log notice at saturation, fail routes through send_pre_spawn_failure. After a queued wait, a closed parent command channel (teardown) fails the spawn instead of running against a dead parent. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KSy9S31pZNWXtRZNVJ1sDf
210 lines
7.9 KiB
Rust
210 lines
7.9 KiB
Rust
use crate::agent::subagent::SubagentSpawnContext;
|
|
use crate::session::SessionCommand;
|
|
use agent_client_protocol as acp;
|
|
use kigi_acp_lib::AcpAgentGatewaySender as GatewaySender;
|
|
use kigi_tools::implementations::kigi::task::types::{SubagentRequest, SubagentResult};
|
|
use std::collections::HashMap;
|
|
use std::path::PathBuf;
|
|
use std::sync::Arc;
|
|
use tokio::sync::{mpsc, oneshot};
|
|
pub(crate) type GatewayOut = <acp::AgentSide as kigi_acp_lib::AcpSide>::OutMessage;
|
|
pub(crate) fn test_gateway() -> GatewaySender {
|
|
let (tx, _rx) = mpsc::unbounded_channel();
|
|
GatewaySender::new(tx)
|
|
}
|
|
/// Like `test_gateway` but returns the receiver; keep it alive for the test.
|
|
pub(crate) fn test_gateway_with_receiver() -> (GatewaySender, mpsc::UnboundedReceiver<GatewayOut>) {
|
|
let (tx, rx) = mpsc::unbounded_channel();
|
|
(GatewaySender::new(tx), rx)
|
|
}
|
|
/// `ctx_with_toggle` with a wired `parent_cmd_tx`.
|
|
pub(crate) fn ctx_with_toggle_and_cmd_tx(
|
|
toggle: HashMap<String, bool>,
|
|
) -> (
|
|
SubagentSpawnContext,
|
|
mpsc::UnboundedReceiver<SessionCommand>,
|
|
) {
|
|
let mut ctx = ctx_with_toggle(toggle);
|
|
let (tx, rx) = mpsc::unbounded_channel();
|
|
ctx.parent_cmd_tx = Some(tx);
|
|
(ctx, rx)
|
|
}
|
|
pub(crate) fn ctx_with_toggle(toggle: HashMap<String, bool>) -> SubagentSpawnContext {
|
|
let (tx, _rx) = mpsc::unbounded_channel();
|
|
SubagentSpawnContext {
|
|
lsp: None,
|
|
spawn_limits:
|
|
kigi_tools::implementations::kigi::task::admission::SubagentLimits::default(),
|
|
spawn_permits: std::sync::Arc::new(tokio::sync::Semaphore::new(
|
|
kigi_tools::implementations::kigi::task::admission::SubagentLimits::default()
|
|
.effective_max_concurrent(),
|
|
)),
|
|
parent_max_turns: None,
|
|
gateway: test_gateway(),
|
|
client_hooks: Default::default(),
|
|
sampling_config: kigi_sampler::SamplerConfig {
|
|
api_key: None,
|
|
base_url: String::new(),
|
|
model: String::new(),
|
|
max_completion_tokens: None,
|
|
temperature: None,
|
|
top_p: None,
|
|
api_backend: Default::default(),
|
|
chat_compat: Default::default(),
|
|
auth_scheme: Default::default(),
|
|
anthropic_oauth: false,
|
|
github_copilot: false,
|
|
openai_codex: false,
|
|
extra_headers: Default::default(),
|
|
context_window: 256_000,
|
|
force_http1: false,
|
|
max_retries: None,
|
|
stream_tool_calls: false,
|
|
idle_timeout_secs: None,
|
|
reasoning_effort: None,
|
|
origin_client: None,
|
|
attribution_callback: None,
|
|
bearer_resolver: None,
|
|
supports_backend_search: false,
|
|
compactions_remaining: None,
|
|
compaction_at_tokens: None,
|
|
doom_loop_recovery: None,
|
|
header_injector: None,
|
|
},
|
|
alpha_test_key: None,
|
|
auth_method_id: acp::AuthMethodId::new("test"),
|
|
model_id: acp::ModelId::new("test"),
|
|
storage_mode: crate::config::StorageMode::Local,
|
|
auth: None,
|
|
parent_cwd: PathBuf::from("/tmp"),
|
|
parent_session_id: "test-parent".into(),
|
|
yolo_mode: false,
|
|
subagent_event_tx: tx,
|
|
hunk_tracker_handle: kigi_hunk_tracker::HunkTrackerHandle::noop(),
|
|
hunk_tracking_enabled: false,
|
|
fs: Arc::new(kigi_workspace::file_system::LocalFs::new(PathBuf::from(
|
|
"/tmp",
|
|
))),
|
|
terminal: Arc::new(crate::terminal::TerminalRunner::new(
|
|
Arc::new(test_gateway()),
|
|
acp::SessionId::new("test"),
|
|
)),
|
|
session_env: Arc::new(HashMap::new()),
|
|
memory_config: None,
|
|
web_search_config: Default::default(),
|
|
web_fetch_config: Default::default(),
|
|
app_builder_deployer_config: Default::default(),
|
|
write_file_enabled: true,
|
|
goal_enabled: false,
|
|
ask_user_question_enabled: true,
|
|
parent_cmd_tx: None,
|
|
parent_session_info: None,
|
|
subagent_roles: HashMap::new(),
|
|
subagent_personas: HashMap::new(),
|
|
persona_io_summaries: Vec::new(),
|
|
parent_chat_state: None,
|
|
available_models: indexmap::IndexMap::new(),
|
|
subagent_model_overrides: HashMap::new(),
|
|
subagent_toggle: toggle,
|
|
disable_web_search: false,
|
|
todo_gate: false,
|
|
remote_settings: None,
|
|
laziness_debug_log: None,
|
|
backend_tools_enabled: true,
|
|
respect_gitignore: false,
|
|
path_not_found_hints: false,
|
|
plugin_registry: None,
|
|
models_manager: Default::default(),
|
|
file_tool_overrides: None,
|
|
agent_config: None,
|
|
hook_registry: None,
|
|
hook_workspace_root: String::new(),
|
|
parent_depth: 0,
|
|
inference_idle_timeout_secs: 600,
|
|
auto_compact_threshold_tiers: crate::agent::subagent::AutoCompactThresholdTiers::default(),
|
|
permission_handle: None,
|
|
worktree_type: crate::util::config::WorktreeType::Linked,
|
|
api_key_provider: None,
|
|
image_description_model: crate::test_support::TEST_MODEL.to_owned(),
|
|
workspace_ops: kigi_workspace::WorkspaceOps::for_test(),
|
|
auth_manager: Arc::new(crate::auth::AuthManager::new(
|
|
std::path::Path::new("/tmp/nonexistent-kigi-test"),
|
|
crate::auth::KimiCodeConfig::default(),
|
|
)),
|
|
attribution_callback: None,
|
|
parent_agent_name: None,
|
|
parent_model_agent_type: None,
|
|
allowed_subagent_types: None,
|
|
parent_mcp_configs: vec![],
|
|
parent_mcp_pool: None,
|
|
parent_tool_snapshot: None,
|
|
parent_skills: None,
|
|
parent_skills_config: kigi_agent::prompt::skills::SkillsConfig::default(),
|
|
parent_compat: kigi_tools::types::compat::CompatConfig::default(),
|
|
auto_wake_delivered: None,
|
|
task_output_tool_name: kigi_tools::reminders::task_completion::DEFAULT_TASK_OUTPUT_TOOL
|
|
.to_string(),
|
|
auto_wake_enabled: true,
|
|
goal_loop_active: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
|
parent_blocking_wait_depth: std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)),
|
|
parent_terminal_backend: None,
|
|
parent_notification_handle: None,
|
|
parent_scheduler_handle: None,
|
|
}
|
|
}
|
|
pub(crate) fn make_request(
|
|
subagent_type: &str,
|
|
) -> (SubagentRequest, oneshot::Receiver<SubagentResult>) {
|
|
let (tx, rx) = oneshot::channel();
|
|
let req = SubagentRequest {
|
|
id: uuid::Uuid::now_v7().to_string(),
|
|
prompt: "do something".into(),
|
|
description: "test task".into(),
|
|
subagent_type: subagent_type.into(),
|
|
parent_session_id: "test-parent".into(),
|
|
parent_prompt_id: Some("parent-prompt".into()),
|
|
resume_from: None,
|
|
cwd: None,
|
|
runtime_overrides: Default::default(),
|
|
run_in_background: false,
|
|
surface_completion: true,
|
|
fork_context: false,
|
|
result_tx: tx,
|
|
};
|
|
(req, rx)
|
|
}
|
|
#[derive(Default)]
|
|
pub(crate) struct DummyLspDispatch;
|
|
#[async_trait::async_trait]
|
|
impl kigi_tools::implementations::lsp::LspBackend for DummyLspDispatch {
|
|
fn ensure_started_background(&self) {}
|
|
async fn ensure_ready(&self) -> Result<(), String> {
|
|
Ok(())
|
|
}
|
|
fn is_ready(&self) -> bool {
|
|
true
|
|
}
|
|
async fn dispatch(
|
|
&self,
|
|
_input: &kigi_tools::implementations::lsp::LspToolInput,
|
|
) -> kigi_tools::implementations::lsp::LspToolResult {
|
|
kigi_tools::implementations::lsp::LspToolResult {
|
|
text: String::new(),
|
|
is_error: false,
|
|
}
|
|
}
|
|
async fn drain_diagnostics(
|
|
&self,
|
|
_timeout: std::time::Duration,
|
|
) -> Option<kigi_tools::implementations::lsp::DiagnosticsSummary> {
|
|
None
|
|
}
|
|
async fn notify_file_changed(&self, _path: &std::path::Path, _content: &str) {}
|
|
async fn read_diagnostics(
|
|
&self,
|
|
_paths: &[std::path::PathBuf],
|
|
) -> Vec<kigi_tools::implementations::lsp::FileDiagnosticEntry> {
|
|
vec![]
|
|
}
|
|
}
|