File size: 9,362 Bytes
52a9af3 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 | use crate::config::MultiAgentV2Config;
use crate::context::MultiAgentRoleInstructions;
use crate::session::step_context::StepContext;
use codex_protocol::config_types::MultiAgentMode;
use codex_protocol::openai_models::MultiAgentRoleMessages;
use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::protocol::MultiAgentVersion;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::SubAgentSource;
const DEFAULT_MULTI_AGENT_V2_ROOT_AGENT_USAGE_HINT_TEXT: &str = r#"You are `/root`, the primary agent in a team of agents collaborating to fulfill the user's goals.
At the start of your turn, you are the active agent.
You can spawn sub-agents to handle subtasks, and those sub-agents can spawn their own sub-agents.
All agents in the team, including the agents that you can assign tasks to, are equally intelligent and capable, and have access to the same set of tools.
You can use `spawn_agent` to create a new agent, `followup_task` to give an existing agent a new task and trigger a turn, and `send_message` to pass a message to a running agent without triggering a turn.
Child agents can also spawn their own sub-agents.
You can decide how much context you want to propagate to your sub-agents with the `fork_turns` parameter.
You will receive messages in the analysis channel in the form:
```
Message Type: MESSAGE | FINAL_ANSWER
Task name: <recipient>
Sender: <author>
Payload:
<payload text>
```
They may be addressed as to=/root
"#;
const DEFAULT_MULTI_AGENT_V2_SUBAGENT_USAGE_HINT_TEXT: &str = r#"You are an agent in a team of agents collaborating to complete a task.
You can spawn sub-agents to handle subtasks, and those sub-agents can spawn their own sub-agents. All agents in the team, including the agents that you can assign tasks to, are equally intelligent and capable, and have access to the same set of tools.
You can use `spawn_agent` to create a new agent, `followup_task` to give an existing agent a new task and trigger a turn, and `send_message` to pass a message to a running agent.
Child agents can also spawn their own sub-agents.
When you provide a response in the final channel, that content is immediately delivered back to your parent agent.
You will receive messages in the analysis channel in the form:
```
Message Type: NEW_TASK | MESSAGE | FINAL_ANSWER
Task name: <recipient>
Sender: <author>
Payload:
<payload text>
```
You may also see them addressed as to=/root/..., which indicates your identity is /root/...
"#;
const DEFAULT_MULTI_AGENT_V2_MODEL_OVERRIDE_USAGE_HINT_TEXT: &str = "Full-history forks (`fork_turns` omitted or `\"all\"`) inherit the parent model and reasoning effort and do not accept overrides. Only set `model` or `reasoning_effort` when explicitly requested by the user, applicable `AGENTS.md` instructions, or skill instructions; when doing so, set `fork_turns` to `\"none\"` or a positive integer string.";
const DEFAULT_MULTI_AGENT_V2_WAIT_AGENT_USAGE_HINT_TEXT: &str =
"When calling `wait_agent`, prefer longer waits (minutes) to avoid busy polling.";
const DEFAULT_MULTI_AGENT_V2_SHARED_USAGE_HINT_TEXT: &str = r#"Note that collaboration tools cannot be called from inside `functions.exec`. Call `spawn_agent`, `send_message`, `followup_task`, `wait_agent`, `interrupt_agent`, and `list_agents` only as direct tool calls using the recipient shown in their tool definitions, such as `to=functions.collaboration.spawn_agent`, since they are intentionally absent from the `functions.exec` `tools.*` namespace. Available tools in `functions.exec` are explicitly described with a `tools` namespace in the developer message.
All agents share the same directory. In detail:
- All agents have access to the same container and filesystem as you.
- All agents use the same current working directory.
- As a result, edits made by one agent are immediately visible to all other agents.
"#;
#[derive(Clone, Debug, Default)]
pub(crate) struct ResolvedMultiAgentV2UsageHints {
pub(crate) root: Option<MultiAgentRoleInstructions>,
pub(crate) subagent: Option<MultiAgentRoleInstructions>,
}
pub(super) fn usage_hint_text(
step_context: &StepContext,
session_source: &SessionSource,
) -> Option<MultiAgentRoleInstructions> {
let turn_context = step_context.turn.as_ref();
if turn_context.multi_agent_version != MultiAgentVersion::V2 {
return None;
}
let catalog = step_context
.settings
.model_info
.model_messages
.as_ref()
.and_then(|messages| messages.multi_agent.as_ref())
.and_then(|messages| messages.role.as_ref());
let snapshot = resolve_usage_hints(
&turn_context.config.multi_agent_v2,
catalog,
!turn_context.config.update_plan_enabled && turn_context.config.model_catalog.is_none(),
);
match session_source {
SessionSource::SubAgent(SubAgentSource::ThreadSpawn { .. }) => snapshot.subagent,
SessionSource::Cli
| SessionSource::VSCode
| SessionSource::Exec
| SessionSource::Mcp
| SessionSource::Custom(_)
| SessionSource::Unknown => snapshot.root,
SessionSource::Internal(_) | SessionSource::SubAgent(_) => None,
}
}
pub(crate) fn resolve_usage_hints(
config: &MultiAgentV2Config,
catalog: Option<&MultiAgentRoleMessages>,
omit_update_plan_instructions: bool,
) -> ResolvedMultiAgentV2UsageHints {
let resolve_role = |configured: Option<&str>, catalog: Option<&str>, bundled: &str| {
// Configured roles take precedence; empty configured or catalog roles suppress fallback.
if let Some(configured) = configured {
return (!configured.is_empty())
.then(|| MultiAgentRoleInstructions::unmarked(configured));
}
let base = catalog.unwrap_or(bundled);
if base.is_empty() {
return None;
}
let base = if omit_update_plan_instructions {
crate::context::without_update_plan_instructions(base)
} else {
base.to_string()
};
let max_concurrency = config.max_concurrent_threads_per_session;
let wait_agent_guidance = if config.wait_agent_enabled {
format!("{DEFAULT_MULTI_AGENT_V2_WAIT_AGENT_USAGE_HINT_TEXT}\n\n")
} else {
String::new()
};
let mut text = format!(
"{base}\n{DEFAULT_MULTI_AGENT_V2_SHARED_USAGE_HINT_TEXT}\n{wait_agent_guidance}There are {max_concurrency} available concurrency slots, meaning that up to {max_concurrency} agents can be active at once, including you."
);
if config.expose_spawn_agent_model_overrides {
text.push_str("\n\n");
text.push_str(DEFAULT_MULTI_AGENT_V2_MODEL_OVERRIDE_USAGE_HINT_TEXT);
}
Some(if catalog.is_some() {
MultiAgentRoleInstructions::catalog(text)
} else {
MultiAgentRoleInstructions::unmarked(text)
})
};
ResolvedMultiAgentV2UsageHints {
root: resolve_role(
config.root_agent_usage_hint_text.as_deref(),
catalog.and_then(|messages| messages.root.as_deref()),
DEFAULT_MULTI_AGENT_V2_ROOT_AGENT_USAGE_HINT_TEXT,
),
subagent: resolve_role(
config.subagent_usage_hint_text.as_deref(),
catalog.and_then(|messages| messages.subagent.as_deref()),
DEFAULT_MULTI_AGENT_V2_SUBAGENT_USAGE_HINT_TEXT,
),
}
}
pub(crate) fn effective_multi_agent_mode(step_context: &StepContext) -> Option<MultiAgentMode> {
let turn_context = step_context.turn.as_ref();
let settings = &step_context.settings;
if turn_context.multi_agent_version != MultiAgentVersion::V2 {
return None;
}
let catalog_mode = settings
.model_info
.model_messages
.as_ref()
.and_then(|messages| messages.multi_agent.as_ref())
.and_then(|messages| messages.mode.as_ref());
let mode_hint_text = turn_context
.config
.multi_agent_v2
.multi_agent_mode_hint_text
.as_deref()
.or_else(|| catalog_mode.and_then(|mode| mode.hint_text.as_deref()));
// A configured or catalog hint, including an empty string, defines a custom policy instead
// of an effort-derived built-in policy.
let multi_agent_mode = match mode_hint_text {
Some(hint_text) => MultiAgentMode::Custom(hint_text.to_string()),
None => match settings.effective_reasoning_effort() {
Some(ReasoningEffort::Ultra) => catalog_mode
.and_then(|messages| messages.proactive.clone())
.map(MultiAgentMode::Custom)
.unwrap_or(MultiAgentMode::Proactive),
_ => catalog_mode
.and_then(|messages| messages.explicit.clone())
.map(MultiAgentMode::Custom)
.unwrap_or(MultiAgentMode::ExplicitRequestOnly),
},
};
match &turn_context.session_source {
SessionSource::SubAgent(SubAgentSource::ThreadSpawn { .. })
| SessionSource::Cli
| SessionSource::VSCode
| SessionSource::Exec
| SessionSource::Mcp
| SessionSource::Custom(_)
| SessionSource::Unknown => Some(multi_agent_mode),
SessionSource::Internal(_) | SessionSource::SubAgent(_) => None,
}
}
|