diff --git a/crates/loopal-agent-client/src/client.rs b/crates/loopal-agent-client/src/client.rs index 8e366fb..7048eba 100644 --- a/crates/loopal-agent-client/src/client.rs +++ b/crates/loopal-agent-client/src/client.rs @@ -1,6 +1,6 @@ //! IPC client — wraps `Connection` with agent protocol methods. -use std::path::Path; +use std::path::PathBuf; use std::sync::Arc; use serde_json::Value; @@ -12,6 +12,22 @@ use loopal_ipc::protocol::methods; use loopal_ipc::transport::Transport; use loopal_protocol::{AgentEvent, ControlCommand, Envelope, UserQuestionResponse}; +/// Parameters for `agent/start` IPC request. +#[derive(Debug, Default)] +pub struct StartAgentParams { + pub cwd: PathBuf, + pub model: Option, + pub mode: Option, + pub prompt: Option, + pub permission_mode: Option, + pub no_sandbox: bool, + pub resume: Option, + pub lifecycle: Option, + pub agent_type: Option, + /// Nesting depth (0 = root). Propagated from parent. + pub depth: Option, +} + /// High-level agent IPC client. pub struct AgentClient { connection: Arc, @@ -44,29 +60,18 @@ impl AgentClient { } /// Send `agent/start` to begin the agent loop. - #[allow(clippy::too_many_arguments)] - pub async fn start_agent( - &self, - cwd: &Path, - model: Option<&str>, - mode: Option<&str>, - prompt: Option<&str>, - permission_mode: Option<&str>, - no_sandbox: bool, - resume: Option<&str>, - lifecycle: Option<&str>, - agent_type: Option<&str>, - ) -> anyhow::Result { + pub async fn start_agent(&self, p: &StartAgentParams) -> anyhow::Result { let params = serde_json::json!({ - "cwd": cwd.to_string_lossy(), - "model": model, - "mode": mode, - "prompt": prompt, - "permission_mode": permission_mode, - "no_sandbox": no_sandbox, - "resume": resume, - "lifecycle": lifecycle, - "agent_type": agent_type, + "cwd": p.cwd.to_string_lossy(), + "model": p.model, + "mode": p.mode, + "prompt": p.prompt, + "permission_mode": p.permission_mode, + "no_sandbox": p.no_sandbox, + "resume": p.resume, + "lifecycle": p.lifecycle, + "agent_type": p.agent_type, + "depth": p.depth, }); let result = self .connection diff --git a/crates/loopal-agent-client/src/lib.rs b/crates/loopal-agent-client/src/lib.rs index c6c4cf6..c3da1b2 100644 --- a/crates/loopal-agent-client/src/lib.rs +++ b/crates/loopal-agent-client/src/lib.rs @@ -9,5 +9,5 @@ mod client; mod process; pub use bridge::{BridgeHandles, start_bridge}; -pub use client::{AgentClient, AgentClientEvent}; +pub use client::{AgentClient, AgentClientEvent, StartAgentParams}; pub use process::AgentProcess; diff --git a/crates/loopal-agent-hub/src/dispatch/dispatch_handlers.rs b/crates/loopal-agent-hub/src/dispatch/dispatch_handlers.rs index 4e97c54..f6652f1 100644 --- a/crates/loopal-agent-hub/src/dispatch/dispatch_handlers.rs +++ b/crates/loopal-agent-hub/src/dispatch/dispatch_handlers.rs @@ -161,6 +161,7 @@ pub async fn handle_spawn_agent( let prompt = params["prompt"].as_str().map(String::from); let permission_mode = params["permission_mode"].as_str().map(String::from); let agent_type = params["agent_type"].as_str().map(String::from); + let depth = params["depth"].as_u64().map(|v| v as u32); // Parent: use explicit "parent" field from params if present (cross-hub), // otherwise use from_agent (local spawn). @@ -182,6 +183,7 @@ pub async fn handle_spawn_agent( parent, permission_mode, agent_type, + depth, ) .await }); diff --git a/crates/loopal-agent-hub/src/spawn_manager.rs b/crates/loopal-agent-hub/src/spawn_manager.rs index 78bd759..1d03458 100644 --- a/crates/loopal-agent-hub/src/spawn_manager.rs +++ b/crates/loopal-agent-hub/src/spawn_manager.rs @@ -22,6 +22,7 @@ pub async fn spawn_and_register( parent: Option, permission_mode: Option, agent_type: Option, + depth: Option, ) -> Result { info!(agent = %name, parent = ?parent, "spawn: forking process"); let agent_proc = loopal_agent_client::AgentProcess::spawn(None) @@ -36,18 +37,19 @@ pub async fn spawn_and_register( return Err(format!("agent initialize failed: {e}")); } info!(agent = %name, "spawn: starting agent"); + let model_for_registry = model.clone(); let session_id = match client - .start_agent( - std::path::Path::new(&cwd), - model.as_deref(), - Some("act"), - prompt.as_deref(), - permission_mode.as_deref(), - false, - None, - Some("ephemeral"), // sub-agents always exit on idle - agent_type.as_deref(), - ) + .start_agent(&loopal_agent_client::StartAgentParams { + cwd: std::path::PathBuf::from(&cwd), + model, + mode: Some("act".to_string()), + prompt, + permission_mode, + lifecycle: Some("ephemeral".to_string()), // sub-agents always exit on idle + agent_type, + depth, + ..Default::default() + }) .await { Ok(sid) => Some(sid), @@ -65,7 +67,7 @@ pub async fn spawn_and_register( conn, incoming_rx, parent.as_deref(), - model.as_deref(), + model_for_registry.as_deref(), session_id.as_deref(), ) .await; diff --git a/crates/loopal-agent-hub/tests/suite/e2e_bootstrap_test.rs b/crates/loopal-agent-hub/tests/suite/e2e_bootstrap_test.rs index 4788d61..a867c56 100644 --- a/crates/loopal-agent-hub/tests/suite/e2e_bootstrap_test.rs +++ b/crates/loopal-agent-hub/tests/suite/e2e_bootstrap_test.rs @@ -50,17 +50,12 @@ async fn full_bootstrap_hub_to_agent_roundtrip() { let cwd = std::env::temp_dir(); client - .start_agent( - &cwd, - None, // use default model - Some("act"), - None, // no initial prompt - None, - true, // no sandbox - None, - None, // lifecycle: default - None, // agent_type - ) + .start_agent(&loopal_agent_client::StartAgentParams { + cwd: cwd.clone(), + mode: Some("act".to_string()), + no_sandbox: true, + ..Default::default() + }) .await .expect("start_agent should work"); diff --git a/crates/loopal-agent-server/src/agent_setup.rs b/crates/loopal-agent-server/src/agent_setup.rs index 1778770..299c3c6 100644 --- a/crates/loopal-agent-server/src/agent_setup.rs +++ b/crates/loopal-agent-server/src/agent_setup.rs @@ -81,8 +81,7 @@ pub fn build_with_frontend( task_store: Arc::new(TaskStore::new(tasks_dir)), hub_connection, cwd: cwd.to_path_buf(), - depth: 0, - max_depth: 3, + depth: start.depth.unwrap_or(0), agent_name: "main".into(), parent_event_tx: Some(event_tx), cancel_token: None, @@ -136,6 +135,7 @@ pub fn build_with_frontend( &config.memory, start.agent_type.as_deref(), features, + start.depth.unwrap_or(0), ); crate::prompt_post::append_runtime_sections(&mut system_prompt, &kernel); diff --git a/crates/loopal-agent-server/src/memory_adapter.rs b/crates/loopal-agent-server/src/memory_adapter.rs index a71e4d4..4067e3f 100644 --- a/crates/loopal-agent-server/src/memory_adapter.rs +++ b/crates/loopal-agent-server/src/memory_adapter.rs @@ -50,6 +50,7 @@ impl MemoryProcessor for ServerMemoryProcessor { permission_mode: None, target_hub: None, agent_type: None, + depth: self.shared.depth + 1, }; spawn_agent(&self.shared, params).await?; info!("memory-maintainer agent spawned via Hub"); diff --git a/crates/loopal-agent-server/src/params.rs b/crates/loopal-agent-server/src/params.rs index b69ef24..a5c04db 100644 --- a/crates/loopal-agent-server/src/params.rs +++ b/crates/loopal-agent-server/src/params.rs @@ -18,6 +18,8 @@ pub struct StartParams { pub lifecycle: loopal_runtime::LifecycleMode, /// Agent type for fragment selection (e.g. "explore", "plan"). pub agent_type: Option, + /// Nesting depth (0 = root). Propagated from parent via IPC. + pub depth: Option, } /// Build a Kernel from config (production path: MCP, tools). diff --git a/crates/loopal-agent-server/src/session_start.rs b/crates/loopal-agent-server/src/session_start.rs index 138e9c6..97b1eeb 100644 --- a/crates/loopal-agent-server/src/session_start.rs +++ b/crates/loopal-agent-server/src/session_start.rs @@ -64,6 +64,7 @@ pub(crate) async fn start_session( resume: params["resume"].as_str().map(String::from), lifecycle, agent_type: params["agent_type"].as_str().map(String::from), + depth: params["depth"].as_u64().map(|v| v as u32), }; let mut config = load_config(&cwd)?; diff --git a/crates/loopal-agent-server/tests/suite/hub_harness.rs b/crates/loopal-agent-server/tests/suite/hub_harness.rs index a3c8431..75f0057 100644 --- a/crates/loopal-agent-server/tests/suite/hub_harness.rs +++ b/crates/loopal-agent-server/tests/suite/hub_harness.rs @@ -131,6 +131,7 @@ pub async fn build_hub_harness_with( resume: None, lifecycle: loopal_runtime::LifecycleMode::Persistent, agent_type: None, + depth: None, }; let (hub_conn, _hub_peer) = loopal_ipc::duplex_pair(); let hub_connection = std::sync::Arc::new(loopal_ipc::Connection::new(hub_conn)); diff --git a/crates/loopal-agent/src/shared.rs b/crates/loopal-agent/src/shared.rs index 548e17a..e21200b 100644 --- a/crates/loopal-agent/src/shared.rs +++ b/crates/loopal-agent/src/shared.rs @@ -75,10 +75,9 @@ pub struct AgentShared { pub hub_connection: Arc, /// Initial working directory. Immutable after construction. pub cwd: PathBuf, - /// Current nesting depth (0 = root agent). + /// Current nesting depth (0 = root agent). Propagated via IPC so agents + /// can sense how deep they are in the delegation chain. pub depth: u32, - /// Maximum allowed nesting depth. - pub max_depth: u32, /// Name of the current agent. pub agent_name: String, /// Event sender for forwarding sub-agent events up the chain. diff --git a/crates/loopal-agent/src/spawn.rs b/crates/loopal-agent/src/spawn.rs index 4e840f6..a598c1f 100644 --- a/crates/loopal-agent/src/spawn.rs +++ b/crates/loopal-agent/src/spawn.rs @@ -21,6 +21,8 @@ pub struct SpawnParams { pub target_hub: Option, /// Agent type for fragment selection (e.g. "explore", "plan"). pub agent_type: Option, + /// Nesting depth of the child agent (parent depth + 1). + pub depth: u32, } /// Result returned from Hub after spawning. @@ -48,6 +50,7 @@ pub async fn spawn_agent( "prompt": params.prompt, "permission_mode": params.permission_mode, "agent_type": params.agent_type, + "depth": params.depth, }); if let Some(ref hub) = params.target_hub { request["target_hub"] = json!(hub); diff --git a/crates/loopal-agent/src/tools/collaboration/agent.rs b/crates/loopal-agent/src/tools/collaboration/agent.rs index e1831e2..529d90f 100644 --- a/crates/loopal-agent/src/tools/collaboration/agent.rs +++ b/crates/loopal-agent/src/tools/collaboration/agent.rs @@ -93,12 +93,6 @@ async fn action_spawn( .get("target_hub") .and_then(|v| v.as_str()) .map(String::from); - if shared.depth >= shared.max_depth { - return Ok(ToolResult::error(format!( - "Maximum nesting depth ({}) reached", - shared.max_depth - ))); - } let mut config = subagent_type .and_then(|t| load_agent_configs(&shared.cwd).remove(t)) @@ -131,6 +125,7 @@ async fn action_spawn( permission_mode: Some(perm_mode.to_string()), target_hub, agent_type: subagent_type.map(String::from), + depth: shared.depth + 1, }, ) .await; diff --git a/crates/loopal-agent/tests/suite/bridge_chain_test.rs b/crates/loopal-agent/tests/suite/bridge_chain_test.rs index 9e7df19..94f4ef0 100644 --- a/crates/loopal-agent/tests/suite/bridge_chain_test.rs +++ b/crates/loopal-agent/tests/suite/bridge_chain_test.rs @@ -40,17 +40,11 @@ async fn full_chain_sub_agent_result_delivered_to_parent() { let client = AgentClient::new(client_t); client.initialize().await.expect("initialize"); client - .start_agent( - fixture.path(), - None, - None, - Some("research this project"), - None, - false, - None, - None, - None, - ) + .start_agent(&loopal_agent_client::StartAgentParams { + cwd: fixture.path().to_path_buf(), + prompt: Some("research this project".to_string()), + ..Default::default() + }) .await .expect("start_agent"); diff --git a/crates/loopal-agent/tests/suite/bridge_child_test.rs b/crates/loopal-agent/tests/suite/bridge_child_test.rs index 52350af..d5bc626 100644 --- a/crates/loopal-agent/tests/suite/bridge_child_test.rs +++ b/crates/loopal-agent/tests/suite/bridge_child_test.rs @@ -57,17 +57,11 @@ pub(crate) async fn start_bridge_client( let client = AgentClient::new(client_t); client.initialize().await.expect("initialize"); client - .start_agent( - fixture.path(), - None, - None, - Some("work"), - None, - false, - None, - None, - None, - ) + .start_agent(&loopal_agent_client::StartAgentParams { + cwd: fixture.path().to_path_buf(), + prompt: Some("work".to_string()), + ..Default::default() + }) .await .expect("start_agent"); @@ -148,17 +142,11 @@ async fn bridge_cancel_sends_shutdown() { let client = AgentClient::new(client_t); client.initialize().await.unwrap(); client - .start_agent( - fixture.path(), - None, - None, - Some("slow task"), - None, - false, - None, - None, - None, - ) + .start_agent(&loopal_agent_client::StartAgentParams { + cwd: fixture.path().to_path_buf(), + prompt: Some("slow task".to_string()), + ..Default::default() + }) .await .unwrap(); diff --git a/crates/loopal-context/src/system_prompt.rs b/crates/loopal-context/src/system_prompt.rs index b22ec75..e37c7d1 100644 --- a/crates/loopal-context/src/system_prompt.rs +++ b/crates/loopal-context/src/system_prompt.rs @@ -15,6 +15,7 @@ pub fn build_system_prompt( memory: &str, agent_type: Option<&str>, features: Vec, + agent_depth: u32, ) -> String { let mut registry = FragmentRegistry::new(system_fragments()); @@ -54,6 +55,7 @@ pub fn build_system_prompt( features, agent_name: None, agent_type: agent_type.map(String::from), + agent_depth, }; builder.build(&ctx) diff --git a/crates/loopal-context/tests/suite/system_prompt_agent_test.rs b/crates/loopal-context/tests/suite/system_prompt_agent_test.rs index b39e46d..dda60ae 100644 --- a/crates/loopal-context/tests/suite/system_prompt_agent_test.rs +++ b/crates/loopal-context/tests/suite/system_prompt_agent_test.rs @@ -29,6 +29,7 @@ fn explore_subagent_full_prompt() { "", Some("explore"), vec![], + 0, ); // Explore-specific content present @@ -62,7 +63,7 @@ fn explore_subagent_full_prompt() { #[test] fn root_agent_excludes_agent_fragments() { - let result = build_system_prompt("Base", &[], "act", "/workspace", "", "", None, vec![]); + let result = build_system_prompt("Base", &[], "act", "/workspace", "", "", None, vec![], 0); // No agent fragments in root prompt assert!( !result.contains("sub-agent named"), @@ -78,7 +79,7 @@ fn root_agent_excludes_agent_fragments() { #[test] fn plan_subagent_gets_plan_fragment() { - let result = build_system_prompt("", &[], "act", "/work", "", "", Some("plan"), vec![]); + let result = build_system_prompt("", &[], "act", "/work", "", "", Some("plan"), vec![], 0); assert!( result.contains("software architect"), "plan fragment should be included" @@ -95,7 +96,7 @@ fn plan_subagent_gets_plan_fragment() { #[test] fn general_subagent_gets_default_fragment() { - let result = build_system_prompt("", &[], "act", "/work", "", "", Some("general"), vec![]); + let result = build_system_prompt("", &[], "act", "/work", "", "", Some("general"), vec![], 0); // Default sub-agent fragment (fallback for unknown types) assert!( result.contains("sub-agent named"), diff --git a/crates/loopal-context/tests/suite/system_prompt_test.rs b/crates/loopal-context/tests/suite/system_prompt_test.rs index ce3c2e1..abe2ab5 100644 --- a/crates/loopal-context/tests/suite/system_prompt_test.rs +++ b/crates/loopal-context/tests/suite/system_prompt_test.rs @@ -3,7 +3,17 @@ use loopal_tool_api::ToolDefinition; #[test] fn includes_instructions() { - let result = build_system_prompt("You are helpful.", &[], "act", "/tmp", "", "", None, vec![]); + let result = build_system_prompt( + "You are helpful.", + &[], + "act", + "/tmp", + "", + "", + None, + vec![], + 0, + ); assert!(result.contains("You are helpful.")); } @@ -14,7 +24,7 @@ fn tool_schemas_not_in_system_prompt() { description: "Read a file".into(), input_schema: serde_json::json!({"type": "object"}), }]; - let result = build_system_prompt("Base", &tools, "act", "/workspace", "", "", None, vec![]); + let result = build_system_prompt("Base", &tools, "act", "/workspace", "", "", None, vec![], 0); // Tool schemas should NOT appear in system prompt — they go via ChatParams.tools assert!(!result.contains("# Available Tools")); assert!(!result.contains("## read")); @@ -24,7 +34,7 @@ fn tool_schemas_not_in_system_prompt() { #[test] fn includes_fragments() { - let result = build_system_prompt("Base", &[], "act", "/workspace", "", "", None, vec![]); + let result = build_system_prompt("Base", &[], "act", "/workspace", "", "", None, vec![], 0); // Core fragments should be present assert!( result.contains("Output Efficiency"), @@ -49,6 +59,7 @@ fn cwd_available_in_subagent_prompt() { "", Some("general"), vec![], + 0, ); assert!( result.contains("/Users/dev/project"), @@ -59,7 +70,17 @@ fn cwd_available_in_subagent_prompt() { #[test] fn includes_skills() { let skills = "# Available Skills\n- /commit: Generate a git commit message"; - let result = build_system_prompt("Base", &[], "act", "/workspace", skills, "", None, vec![]); + let result = build_system_prompt( + "Base", + &[], + "act", + "/workspace", + skills, + "", + None, + vec![], + 0, + ); assert!(result.contains("Available Skills")); assert!(result.contains("/commit")); } @@ -75,6 +96,7 @@ fn includes_memory() { "## Key Patterns\n- Use DI", None, vec![], + 0, ); assert!(result.contains("# Project Memory")); assert!(result.contains("Key Patterns")); @@ -82,7 +104,7 @@ fn includes_memory() { #[test] fn empty_memory_no_section() { - let result = build_system_prompt("Base", &[], "act", "/workspace", "", "", None, vec![]); + let result = build_system_prompt("Base", &[], "act", "/workspace", "", "", None, vec![], 0); assert!(!result.contains("Project Memory")); } @@ -94,7 +116,7 @@ fn tool_conditional_fragments() { description: "Execute commands".into(), input_schema: serde_json::json!({"type": "object"}), }]; - let result = build_system_prompt("Base", &tools, "act", "/workspace", "", "", None, vec![]); + let result = build_system_prompt("Base", &tools, "act", "/workspace", "", "", None, vec![], 0); assert!( result.contains("Bash Tool Guidelines"), "bash guidelines missing when Bash tool present" @@ -102,7 +124,7 @@ fn tool_conditional_fragments() { // Without Bash tool → no bash guidelines let result_no_bash = - build_system_prompt("Base", &[], "act", "/workspace", "", "", None, vec![]); + build_system_prompt("Base", &[], "act", "/workspace", "", "", None, vec![], 0); assert!( !result_no_bash.contains("Bash Tool Guidelines"), "bash guidelines should not appear without Bash" @@ -121,6 +143,7 @@ fn feature_conditional_fragments() { "", None, vec!["memory".into()], + 0, ); assert!( with_memory.contains("Memory System"), @@ -128,7 +151,7 @@ fn feature_conditional_fragments() { ); // Without "memory" feature → no memory guidance - let without = build_system_prompt("Base", &[], "act", "/workspace", "", "", None, vec![]); + let without = build_system_prompt("Base", &[], "act", "/workspace", "", "", None, vec![], 0); assert!( !without.contains("Memory System"), "memory guidance should not appear without memory feature" @@ -144,6 +167,7 @@ fn feature_conditional_fragments() { "", None, vec!["hooks".into()], + 0, ); assert!( with_hooks.contains("hooks"), @@ -160,6 +184,7 @@ fn feature_conditional_fragments() { "", None, vec!["style_explanatory".into()], + 0, ); assert!( with_style.contains("Explanatory"), @@ -176,6 +201,7 @@ fn feature_conditional_fragments() { "", None, vec!["subagent".into()], + 0, ); assert!( with_subagent.contains("Sub-Agent Usage"), @@ -229,11 +255,30 @@ fn report_token_usage() { let mem = "## Architecture\n- 17 Rust crates\n- 200-line limit"; let skills = "# Available Skills\n- /commit: Git commit\n- /review-pr: Review PR"; - let bare = build_system_prompt("", &[], "act", "/project", "", "", None, vec![]); - let with_tools = build_system_prompt("", &tools, "act", "/project", "", "", None, vec![]); - let full_act = build_system_prompt(instr, &tools, "act", "/project", skills, mem, None, vec![]); - let full_plan = - build_system_prompt(instr, &tools, "plan", "/project", skills, mem, None, vec![]); + let bare = build_system_prompt("", &[], "act", "/project", "", "", None, vec![], 0); + let with_tools = build_system_prompt("", &tools, "act", "/project", "", "", None, vec![], 0); + let full_act = build_system_prompt( + instr, + &tools, + "act", + "/project", + skills, + mem, + None, + vec![], + 0, + ); + let full_plan = build_system_prompt( + instr, + &tools, + "plan", + "/project", + skills, + mem, + None, + vec![], + 0, + ); let t_bare = estimate_tokens(&bare); let t_tools = estimate_tokens(&with_tools); diff --git a/crates/loopal-meta-hub/tests/e2e/cluster_harness.rs b/crates/loopal-meta-hub/tests/e2e/cluster_harness.rs index 475b7ab..6b989bf 100644 --- a/crates/loopal-meta-hub/tests/e2e/cluster_harness.rs +++ b/crates/loopal-meta-hub/tests/e2e/cluster_harness.rs @@ -106,7 +106,12 @@ impl HubHandle { client.initialize().await.expect("initialize"); let cwd = std::env::temp_dir(); client - .start_agent(&cwd, None, Some("act"), None, None, true, None, None, None) + .start_agent(&loopal_agent_client::StartAgentParams { + cwd: cwd.clone(), + mode: Some("act".to_string()), + no_sandbox: true, + ..Default::default() + }) .await .expect("start_agent"); diff --git a/crates/loopal-prompt-system/prompts/tools/agent-guidelines.md b/crates/loopal-prompt-system/prompts/tools/agent-guidelines.md index 7b6694b..ff5207d 100644 --- a/crates/loopal-prompt-system/prompts/tools/agent-guidelines.md +++ b/crates/loopal-prompt-system/prompts/tools/agent-guidelines.md @@ -33,6 +33,16 @@ Scale concurrency to task complexity: - **Avoid redundant exploration.** Don't split one search across many explore agents — give one agent a comprehensive, well-scoped prompt instead. Reserve multiple explore agents for genuinely separate areas of the codebase. - **Consider the cost.** A complex multi-area refactoring may justify several parallel agents; a focused bug investigation rarely does. Match the parallelism to the real breadth of the work. +## Delegation Depth + +Your current depth in the agent tree is **{{ agent_depth }}** (0 = root, 1 = first-level sub-agent, etc.). Every sub-agent you spawn can itself spawn further sub-agents. This makes cascading delegation a real risk — if every agent subdivides its work, agent count grows exponentially and each individual agent ends up doing very little useful work. + +**The deeper you are, the more you should prefer doing the work yourself:** +- At depth 0 (root): spawning sub-agents for broad tasks is natural. +- At depth 1+: you were created to handle a specific scope. Consider whether your tools (Glob, Grep, Read, Edit, Bash) are sufficient before spawning children. +- Only delegate further if your assigned scope genuinely contains multiple independent sub-problems that would each benefit from a separate context. +- Give children **narrowly scoped, concrete tasks** — not vague directives that they would need to subdivide again. + ## Agent Types - **explore**: READ-ONLY. Fast at finding files, searching code, reading content. Cannot modify anything. diff --git a/crates/loopal-prompt/src/context.rs b/crates/loopal-prompt/src/context.rs index c3b049a..b20e78c 100644 --- a/crates/loopal-prompt/src/context.rs +++ b/crates/loopal-prompt/src/context.rs @@ -40,6 +40,8 @@ pub struct PromptContext { /// Agent type for fragment selection. Some(_) implies this is a sub-agent. #[serde(skip_serializing_if = "Option::is_none")] pub agent_type: Option, + /// Nesting depth (0 = root). Available as `{{ agent_depth }}` in templates. + pub agent_depth: u32, } impl PromptContext { @@ -66,6 +68,7 @@ impl Default for PromptContext { features: Vec::new(), agent_name: None, agent_type: None, + agent_depth: 0, } } } diff --git a/crates/loopal-runtime/tests/agent_loop/llm_test.rs b/crates/loopal-runtime/tests/agent_loop/llm_test.rs index 9e2b58b..bf6d726 100644 --- a/crates/loopal-runtime/tests/agent_loop/llm_test.rs +++ b/crates/loopal-runtime/tests/agent_loop/llm_test.rs @@ -210,6 +210,7 @@ fn report_real_system_prompt_tokens() { "", None, vec![], + 0, ); runner.params.config.system_prompt = real_prompt.clone(); let params = runner @@ -228,6 +229,7 @@ fn report_real_system_prompt_tokens() { "", None, vec![], + 0, ); let fragment_tokens = loopal_context::estimate_tokens(&prompt_no_tools); diff --git a/crates/loopal-test-support/src/agent_ctx.rs b/crates/loopal-test-support/src/agent_ctx.rs index dd2de73..c139b9d 100644 --- a/crates/loopal-test-support/src/agent_ctx.rs +++ b/crates/loopal-test-support/src/agent_ctx.rs @@ -67,7 +67,6 @@ fn agent_tool_context_inner( hub_connection, cwd, depth: 0, - max_depth: 3, agent_name: "test".into(), parent_event_tx: None, cancel_token: None, diff --git a/crates/loopal-test-support/src/ipc_harness.rs b/crates/loopal-test-support/src/ipc_harness.rs index d945b2e..e8b5123 100644 --- a/crates/loopal-test-support/src/ipc_harness.rs +++ b/crates/loopal-test-support/src/ipc_harness.rs @@ -68,17 +68,11 @@ pub async fn build_ipc_harness( let client = loopal_agent_client::AgentClient::new(client_transport); client.initialize().await.expect("IPC initialize failed"); client - .start_agent( - fixture.path(), - None, - None, - Some("hello"), - None, - false, - None, - None, - None, - ) + .start_agent(&loopal_agent_client::StartAgentParams { + cwd: fixture.path().to_path_buf(), + prompt: Some("hello".to_string()), + ..Default::default() + }) .await .expect("agent/start failed"); diff --git a/crates/loopal-test-support/src/wiring.rs b/crates/loopal-test-support/src/wiring.rs index 67c18e1..7b08dc1 100644 --- a/crates/loopal-test-support/src/wiring.rs +++ b/crates/loopal-test-support/src/wiring.rs @@ -97,7 +97,6 @@ pub(crate) async fn wire(builder: HarnessBuilder) -> (SpawnedHarness, AgentLoopR hub_connection, cwd, depth: 0, - max_depth: 3, agent_name: "main".to_string(), parent_event_tx: Some(event_tx), cancel_token: None, diff --git a/src/bootstrap/hub_bootstrap.rs b/src/bootstrap/hub_bootstrap.rs index 7a99453..ae167d5 100644 --- a/src/bootstrap/hub_bootstrap.rs +++ b/src/bootstrap/hub_bootstrap.rs @@ -60,17 +60,17 @@ pub async fn bootstrap_hub_and_agent( None // default: persistent (server decides based on prompt) }; let root_session_id = client - .start_agent( - cwd, - Some(&config.settings.model), - Some(mode_str), - prompt.as_deref(), - cli.permission.as_deref(), - cli.no_sandbox, - resume, - lifecycle_str, - None, // root agent has no agent_type - ) + .start_agent(&loopal_agent_client::StartAgentParams { + cwd: cwd.to_path_buf(), + model: Some(config.settings.model.clone()), + mode: Some(mode_str.to_string()), + prompt: prompt.clone(), + permission_mode: cli.permission.clone(), + no_sandbox: cli.no_sandbox, + resume: resume.map(String::from), + lifecycle: lifecycle_str.map(String::from), + ..Default::default() + }) .await?; let (root_conn, incoming_rx) = client.into_parts();