diff --git a/crates/tui/src/core/engine/turn_loop.rs b/crates/tui/src/core/engine/turn_loop.rs index a7a0c36c51..0ad8870352 100644 --- a/crates/tui/src/core/engine/turn_loop.rs +++ b/crates/tui/src/core/engine/turn_loop.rs @@ -1203,6 +1203,10 @@ impl Engine { "Turn ending with {running} sub-agent(s) still running in the background; they'll report when done." ))) .await; + // Inject a waiting hint so the model does not poll + // with peek/status/sleep on the next turn (issue #4097). + self.add_session_message(waiting_for_subagents_runtime_message(running)) + .await; } } if subagent_completions > 0 { @@ -2507,6 +2511,28 @@ XML unless the user explicitly asks to debug sub-agent internals.\n\n\ ) } +fn waiting_for_subagents_runtime_message(running: usize) -> Message { + Message { + role: "user".to_string(), + content: vec![ + ContentBlock::Text { + text: format!( + "\n\ +This is an internal runtime event, not user input. Your {running} sub-agent(s) \ +are still running. Do NOT poll them with agent(action=\"peek\") or \ +agent(action=\"status\"). Do NOT use sleep or any shell blocking primitive as a \ +waiting strategy. The runtime will deliver sentinels \ +automatically when each child finishes — polling will never make that happen \ +sooner. Stop immediately: emit zero tool calls and end the turn.\n\ +" + ), + cache_control: None, + }, + runtime_event_turn_metadata_block(UserInputProvenance::SubAgentHandoff), + ], + } +} + fn subagent_completion_runtime_message(payload: &str) -> Message { // Role is "user", not "system": some OpenAI-compatible backends apply a // strict chat template (e.g. vLLM serving Qwen3) that requires any system diff --git a/crates/tui/src/prompts/constitution.md b/crates/tui/src/prompts/constitution.md index 94de6c95c6..f7d80d39e6 100644 --- a/crates/tui/src/prompts/constitution.md +++ b/crates/tui/src/prompts/constitution.md @@ -373,6 +373,15 @@ Reach for them when the work is genuinely independent: launch more; do not invent a smaller per-turn limit. To fan out more gently you can lower `[subagents].launch_concurrency` (how many start at once); the default is the full running cap. +- **Waiting, not polling**: When you end a turn because every remaining task + depends on background results that haven't arrived yet, do **not** poll those + children with `agent(action="peek")` or `agent(action="status")` on your next + turn. Do not use `sleep` or any shell blocking primitive as a waiting strategy. + The runtime delivers `` sentinels automatically as + user messages the moment a child finishes — polling will never make that happen + sooner, it only burns turns and budget. If you receive a new turn with no new + user message and your children are still running, stop immediately: emit zero + tool calls and end the turn. ## Thinking Delegation