Files
codex/codex-rs/core/src/session/multi_agents.rs
T
Abhinav b4507997e0 Use captured step settings when building model context (#44200)
## Why

Model changes during an active turn could leave context using the turn's initial model and settings, even after requests switched to the new model.

## What changed

- Build model, personality, approval, collaboration, app, plugin, and multi-agent instructions from captured step settings.
- Resolve multi-agent policy from the step's effective reasoning effort.
- Use the captured step's context window for full-context metadata and retain that step when rebuilding initial context for compaction.

## Testing

Add regression coverage for model switches updating context without duplicating updates, delegation policy following selected or model-default reasoning effort, and compaction preserving retained context after a model change.

GitOrigin-RevId: 3615c7a3f13f15fdcce9ad13d23311340e0aea88
2026-09-09 16:48:03 +00:00

204 lines
9.1 KiB
Rust

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,
}
}