Skip to main content

agent_works/multi_agent/
config.rs

1//! Multi-agent configuration.
2//!
3//! Controls whether multi-agent capabilities are enabled and sets resource limits.
4//! Multi-agent is enabled by default — users who don't need it can disable via
5//! `.without_multi_agent()` or feature gate.
6
7use agent_base::ReasoningEffort;
8
9/// Permission mode for spawned child agents.
10///
11/// Controls whether sub-agents run with full tool access or are restricted by
12/// the parent's [`ToolPolicy`](agent_base::ToolPolicy). The mode is resolved at
13/// the runtime layer on every spawn, so it cannot be bypassed by the LLM.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
15pub enum ChildPermissionMode {
16    /// Child agents have full permission — every tool auto-approves.
17    ///
18    /// This matches the historical behaviour (children carried no tool policy),
19    /// so it is the default to avoid surprising existing users.
20    #[default]
21    Full,
22    /// Child agents have no permission — the parent's "dangerous tools" are
23    /// denied when the parent has a [`ToolPolicy`](agent_base::ToolPolicy); if
24    /// the parent has none, every tool is denied via [`DenyAllToolPolicy`](agent_base::DenyAllToolPolicy).
25    None,
26    /// The parent agent decides per-spawn via `SpawnAgentArgs.full_permission`.
27    PerSpawn,
28}
29
30/// Configuration for the multi-agent subsystem.
31///
32/// # Default
33///
34/// Multi-agent is **enabled** by default with limits of 8 sub-agents and depth 1.
35/// Disable it with:
36///
37/// ```rust,ignore
38/// use agent_works::AgentBuilder;
39///
40/// let agent = AgentBuilder::new(client)
41///     .without_multi_agent()
42///     .build()?;
43/// ```
44///
45/// # Limits
46///
47/// Limits only take effect when `enabled` is `true`.
48#[derive(Clone, Debug)]
49pub struct MultiAgentConfig {
50    /// Enable multi-agent capabilities.
51    ///
52    /// When `true` (default): all 6 multi-agent tools are registered, and the main
53    /// agent's system prompt is augmented with usage guidance.
54    ///
55    /// When `false`: no multi-agent tools are registered, and the system
56    /// prompt does not mention multi-agent capabilities.
57    pub enabled: bool,
58
59    /// Maximum number of concurrent sub-agents.
60    ///
61    /// Default: 8. Only enforced when `enabled` is `true`.
62    pub max_sub_agents: usize,
63
64    /// Maximum nesting depth for sub-agents.
65    ///
66    /// Default: 1 (only direct children of root can spawn). A value of 0 means
67    /// no sub-agents can be spawned at all. Only enforced when `enabled` is `true`.
68    pub max_agent_depth: i32,
69
70    /// Permission mode for spawned child agents.
71    ///
72    /// Default: [`ChildPermissionMode::Full`].
73    pub child_permission_mode: ChildPermissionMode,
74
75    /// Business tool names to EXCLUDE from child agents.
76    ///
77    /// By default children inherit every business tool registered on the parent.
78    /// Some tools are inherently root-level (e.g. `decompose`/`merge` orchestration):
79    /// a leaf agent that lacks `spawn_agent` should not be handed a "split this into
80    /// parallel sub-agents" tool — it would plan work it cannot execute. Mark such
81    /// tools here so `build_child_runtime` skips them, and children do the work
82    /// inline instead of trying to orchestrate further.
83    pub child_excluded_tools: Vec<String>,
84
85    /// Optional reasoning-effort override applied to every child agent.
86    ///
87    /// Child agents do narrow, focused slices, so they rarely need the parent's
88    /// full reasoning depth — and on reasoning-heavy models (deepseek-v4-pro) an
89    /// unbounded child can "think" itself into a runaway (30KB+ of reasoning with
90    /// no tool call or answer). Setting e.g. [`ReasoningEffort::Low`] caps that
91    /// cost. `None` (default) leaves the child on the framework default.
92    pub child_reasoning_effort: Option<ReasoningEffort>,
93
94    /// Whether child agents are *recommended* to be read-only.
95    ///
96    /// The framework is domain-agnostic: it cannot classify which business tools
97    /// mutate state and which merely read, so this is a **prompt-level suggestion
98    /// only** — it does not remove or gate any tool. When `true` (default),
99    /// [`MultiAgentRuntime::build_child_runtime`] appends a read-only nudge to
100    /// every child's system prompt, telling it to investigate and report rather
101    /// than mutate.
102    ///
103    /// For a *hard* guarantee, exclude mutating tools at the business layer via
104    /// [`MultiAgentConfig::child_excluded_tools`]; the framework only *suggests*
105    /// read-only, it cannot enforce it. Set this to `false` when children are
106    /// meant to write (the codex-style symmetric model).
107    pub child_read_only: bool,
108}
109
110impl Default for MultiAgentConfig {
111    fn default() -> Self {
112        Self {
113            enabled: true,
114            max_sub_agents: 8,
115            max_agent_depth: 1,
116            child_permission_mode: ChildPermissionMode::Full,
117            child_excluded_tools: Vec::new(),
118            child_reasoning_effort: None,
119            child_read_only: true,
120        }
121    }
122}
123
124impl MultiAgentConfig {
125    /// Create a config with multi-agent enabled and default limits.
126    pub fn enabled() -> Self {
127        Self {
128            enabled: true,
129            ..Default::default()
130        }
131    }
132
133    /// Create a config with multi-agent enabled and custom limits.
134    pub fn with_limits(max_sub_agents: usize, max_agent_depth: i32) -> Self {
135        Self {
136            enabled: true,
137            max_sub_agents,
138            max_agent_depth,
139            child_permission_mode: ChildPermissionMode::Full,
140            child_excluded_tools: Vec::new(),
141            child_reasoning_effort: None,
142            child_read_only: true,
143        }
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    #[test]
152    fn default_is_enabled() {
153        let config = MultiAgentConfig::default();
154        assert!(config.enabled);
155        assert_eq!(config.max_sub_agents, 8);
156        assert_eq!(config.max_agent_depth, 1);
157    }
158
159    #[test]
160    fn default_children_are_read_only() {
161        assert!(MultiAgentConfig::default().child_read_only);
162        assert!(MultiAgentConfig::enabled().child_read_only);
163        assert!(MultiAgentConfig::with_limits(4, 2).child_read_only);
164    }
165
166    #[test]
167    fn default_permission_mode_is_full() {
168        assert_eq!(
169            MultiAgentConfig::default().child_permission_mode,
170            ChildPermissionMode::Full
171        );
172        assert_eq!(
173            MultiAgentConfig::enabled().child_permission_mode,
174            ChildPermissionMode::Full
175        );
176        assert_eq!(
177            MultiAgentConfig::with_limits(4, 2).child_permission_mode,
178            ChildPermissionMode::Full
179        );
180    }
181
182    #[test]
183    fn enabled_shortcut() {
184        let config = MultiAgentConfig::enabled();
185        assert!(config.enabled);
186        assert_eq!(config.max_sub_agents, 8);
187        assert_eq!(config.max_agent_depth, 1);
188    }
189
190    #[test]
191    fn with_limits() {
192        let config = MultiAgentConfig::with_limits(4, 2);
193        assert!(config.enabled);
194        assert_eq!(config.max_sub_agents, 4);
195        assert_eq!(config.max_agent_depth, 2);
196    }
197}