mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
[codex] migrate environment context to model world state (#29249)
## Why Environment context is model-visible state, but it is currently assembled from transient turn values and diffed through environment-specific paths. That makes initial injection, turn-to-turn updates, and changes that happen within a turn use different baselines. This PR introduces the smallest useful model world-state slice: environments only, with one in-memory baseline and one renderer for full state and diffs. ## What changed - Add a typed `WorldState` container whose sections render fragments relative to an optional previous value. Full rendering uses the same diff path with no previous state. - Replace the parallel `EnvironmentContext` representation with an `EnvironmentsState` section keyed by environment ID and rendered in deterministic order. - Preserve the legacy single-environment output while supporting multiple environments, starting environments, unavailable tombstones, and changes to persisted turn-context values. - Store the latest complete `WorldState` on `ContextManager` and use it for both turn-boundary and mid-turn environment diffs. - Build initial and post-compaction context from the same world-state builder, then retain the rendered state as the next baseline. - Seed the in-memory baseline from the latest `TurnContextItem` when resuming an existing rollout; the world state itself is not serialized. - Keep non-world settings updates on their existing path and merge rendered world-state fragments at the session consumer. ## Known limitation A legacy `TurnContextItem` only reconstructs the primary environment as `local`; it cannot faithfully recover a remote-primary environment ID after resume. Live state uses the exact environment IDs once a complete baseline is established. ## Test plan - `just test -p codex-core world_state` - `just test -p codex-core record_context_updates` - `just test -p codex-core deferred_executor_` - `just test -p codex-core build_initial_context` - `just test -p codex-core rollout_reconstruction` - `just test -p codex-core process_compacted_history_reinjects_full_initial_context`
This commit is contained in:
committed by
GitHub
Unverified
parent
2c351cb864
commit
3b32d861c5
@@ -3,7 +3,6 @@ use codex_protocol::items::parse_hook_prompt_fragment;
|
||||
use codex_protocol::models::ContentItem;
|
||||
|
||||
use super::AdditionalContextUserFragment;
|
||||
use super::EnvironmentContext;
|
||||
use super::FragmentRegistration;
|
||||
use super::FragmentRegistrationProxy;
|
||||
use super::InternalModelContextFragment;
|
||||
@@ -16,10 +15,11 @@ use super::SubagentNotification;
|
||||
use super::TurnAborted;
|
||||
use super::UserInstructions;
|
||||
use super::UserShellCommand;
|
||||
use super::world_state::EnvironmentsState;
|
||||
|
||||
static USER_INSTRUCTIONS_REGISTRATION: FragmentRegistrationProxy<UserInstructions> =
|
||||
FragmentRegistrationProxy::new();
|
||||
static ENVIRONMENT_CONTEXT_REGISTRATION: FragmentRegistrationProxy<EnvironmentContext> =
|
||||
static ENVIRONMENT_CONTEXT_REGISTRATION: FragmentRegistrationProxy<EnvironmentsState> =
|
||||
FragmentRegistrationProxy::new();
|
||||
static ADDITIONAL_CONTEXT_REGISTRATION: FragmentRegistrationProxy<AdditionalContextUserFragment> =
|
||||
FragmentRegistrationProxy::new();
|
||||
|
||||
@@ -1,119 +1,13 @@
|
||||
use crate::session::step_context::StepContext;
|
||||
use crate::session::turn_context::TurnContext;
|
||||
use crate::session::turn_context::TurnEnvironment;
|
||||
use crate::shell::Shell;
|
||||
use codex_protocol::models::ManagedFileSystemPermissions;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::permissions::FileSystemAccessMode;
|
||||
use codex_protocol::permissions::FileSystemPath;
|
||||
use codex_protocol::permissions::FileSystemSandboxEntry;
|
||||
use codex_protocol::permissions::FileSystemSpecialPath;
|
||||
use codex_protocol::protocol::TurnContextItem;
|
||||
use codex_protocol::protocol::TurnContextNetworkItem;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use std::collections::HashSet;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use super::ContextualUserFragment;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct EnvironmentContext {
|
||||
pub(crate) environments: EnvironmentContextEnvironments,
|
||||
pub(crate) current_date: Option<String>,
|
||||
pub(crate) timezone: Option<String>,
|
||||
pub(crate) network: Option<NetworkContext>,
|
||||
pub(crate) filesystem: Option<FileSystemContext>,
|
||||
pub(crate) subagents: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct EnvironmentContextEnvironment {
|
||||
pub(crate) id: String,
|
||||
pub(crate) cwd: PathUri,
|
||||
status: EnvironmentContextEnvironmentStatus,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
enum EnvironmentContextEnvironmentStatus {
|
||||
Starting,
|
||||
Ready { shell: String },
|
||||
}
|
||||
|
||||
impl EnvironmentContextEnvironment {
|
||||
fn from_turn_environments(environments: &[TurnEnvironment], shell: &Shell) -> Vec<Self> {
|
||||
environments
|
||||
.iter()
|
||||
.map(|environment| Self {
|
||||
id: environment.environment_id.clone(),
|
||||
cwd: environment.cwd().clone(),
|
||||
status: EnvironmentContextEnvironmentStatus::Ready {
|
||||
shell: environment
|
||||
.shell
|
||||
.as_ref()
|
||||
.map(|shell| shell.name().to_string())
|
||||
.unwrap_or_else(|| shell.name().to_string()),
|
||||
},
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl EnvironmentContextEnvironmentStatus {
|
||||
fn equals_except_shell(&self, other: &Self) -> bool {
|
||||
matches!(
|
||||
(self, other),
|
||||
(Self::Starting, Self::Starting) | (Self::Ready { .. }, Self::Ready { .. })
|
||||
)
|
||||
}
|
||||
|
||||
fn render(&self, indent: &str) -> String {
|
||||
match self {
|
||||
Self::Starting => format!("{indent}<status>starting</status>"),
|
||||
Self::Ready { shell } => format!("{indent}<shell>{shell}</shell>"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) enum EnvironmentContextEnvironments {
|
||||
None,
|
||||
Single(EnvironmentContextEnvironment),
|
||||
Multiple(Vec<EnvironmentContextEnvironment>),
|
||||
}
|
||||
|
||||
impl EnvironmentContextEnvironments {
|
||||
fn from_vec(environments: Vec<EnvironmentContextEnvironment>) -> Self {
|
||||
let mut environments = environments;
|
||||
match environments.pop() {
|
||||
None => Self::None,
|
||||
Some(environment) if environments.is_empty() => Self::Single(environment),
|
||||
Some(environment) => {
|
||||
environments.push(environment);
|
||||
Self::Multiple(environments)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn equals_except_shell(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(Self::None, Self::None) => true,
|
||||
(Self::Single(left), Self::Single(right)) => {
|
||||
left.cwd == right.cwd && left.status.equals_except_shell(&right.status)
|
||||
}
|
||||
(Self::Multiple(left), Self::Multiple(right)) => {
|
||||
left.len() == right.len()
|
||||
&& left.iter().zip(right.iter()).all(|(left, right)| {
|
||||
left.id == right.id
|
||||
&& left.cwd == right.cwd
|
||||
&& left.status.equals_except_shell(&right.status)
|
||||
})
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct FileSystemContext {
|
||||
workspace_roots: Vec<String>,
|
||||
@@ -137,7 +31,7 @@ enum ManagedFileSystemContext {
|
||||
}
|
||||
|
||||
impl FileSystemContext {
|
||||
fn from_permission_profile(
|
||||
pub(super) fn from_permission_profile(
|
||||
permission_profile: &PermissionProfile,
|
||||
workspace_roots: &[AbsolutePathBuf],
|
||||
) -> Self {
|
||||
@@ -163,7 +57,7 @@ impl FileSystemContext {
|
||||
}
|
||||
}
|
||||
|
||||
fn render(&self) -> String {
|
||||
pub(super) fn render(&self) -> String {
|
||||
let mut rendered = "<filesystem>".to_string();
|
||||
if !self.workspace_roots.is_empty() {
|
||||
rendered.push_str("<workspace_roots>");
|
||||
@@ -303,7 +197,7 @@ fn push_text_element(rendered: &mut String, name: &str, value: &str) {
|
||||
rendered.push_str(&format!("</{name}>"));
|
||||
}
|
||||
|
||||
fn push_xml_escaped_text(rendered: &mut String, value: &str) {
|
||||
pub(crate) fn push_xml_escaped_text(rendered: &mut String, value: &str) {
|
||||
for ch in value.chars() {
|
||||
match ch {
|
||||
'&' => rendered.push_str("&"),
|
||||
@@ -330,7 +224,7 @@ impl NetworkContext {
|
||||
}
|
||||
}
|
||||
|
||||
fn render(&self) -> String {
|
||||
pub(super) fn render(&self) -> String {
|
||||
let mut rendered = "<network enabled=\"true\">".to_string();
|
||||
Self::push_rendered_domain_element(&mut rendered, "allowed", &self.allowed_domains);
|
||||
Self::push_rendered_domain_element(&mut rendered, "denied", &self.denied_domains);
|
||||
@@ -348,302 +242,3 @@ impl NetworkContext {
|
||||
rendered_network.push_str(&format!("</{name}>"));
|
||||
}
|
||||
}
|
||||
|
||||
impl EnvironmentContext {
|
||||
pub(crate) fn new(
|
||||
environments: Vec<EnvironmentContextEnvironment>,
|
||||
current_date: Option<String>,
|
||||
timezone: Option<String>,
|
||||
network: Option<NetworkContext>,
|
||||
subagents: Option<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
environments: EnvironmentContextEnvironments::from_vec(environments),
|
||||
current_date,
|
||||
timezone,
|
||||
network,
|
||||
filesystem: None,
|
||||
subagents,
|
||||
}
|
||||
}
|
||||
|
||||
fn new_with_environments(
|
||||
environments: EnvironmentContextEnvironments,
|
||||
current_date: Option<String>,
|
||||
timezone: Option<String>,
|
||||
network: Option<NetworkContext>,
|
||||
filesystem: Option<FileSystemContext>,
|
||||
subagents: Option<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
environments,
|
||||
current_date,
|
||||
timezone,
|
||||
network,
|
||||
filesystem,
|
||||
subagents,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn from_step_context(step_context: &StepContext, shell: &Shell) -> Option<Self> {
|
||||
let mut environments = EnvironmentContextEnvironment::from_turn_environments(
|
||||
&step_context.environments.turn_environments,
|
||||
shell,
|
||||
);
|
||||
environments.extend(
|
||||
step_context
|
||||
.environments
|
||||
.starting
|
||||
.iter()
|
||||
.map(|environment| EnvironmentContextEnvironment {
|
||||
id: environment.selection.environment_id.clone(),
|
||||
cwd: environment.selection.cwd.clone(),
|
||||
status: EnvironmentContextEnvironmentStatus::Starting,
|
||||
}),
|
||||
);
|
||||
|
||||
Self::environment_only(environments)
|
||||
}
|
||||
|
||||
pub(crate) fn from_attached_environments(
|
||||
environments: &[TurnEnvironment],
|
||||
shell: &Shell,
|
||||
) -> Option<Self> {
|
||||
Self::environment_only(EnvironmentContextEnvironment::from_turn_environments(
|
||||
environments,
|
||||
shell,
|
||||
))
|
||||
}
|
||||
|
||||
fn environment_only(environments: Vec<EnvironmentContextEnvironment>) -> Option<Self> {
|
||||
(!environments.is_empty()).then(|| {
|
||||
Self::new(
|
||||
environments,
|
||||
/*current_date*/ None,
|
||||
/*timezone*/ None,
|
||||
/*network*/ None,
|
||||
/*subagents*/ None,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Compares two environment contexts, ignoring the shell. Useful when
|
||||
/// comparing turn to turn, since the initial environment_context will
|
||||
/// include the shell, and then it is not configurable from turn to turn.
|
||||
pub(crate) fn equals_except_shell(&self, other: &EnvironmentContext) -> bool {
|
||||
self.environments.equals_except_shell(&other.environments)
|
||||
&& self.current_date == other.current_date
|
||||
&& self.timezone == other.timezone
|
||||
&& self.network == other.network
|
||||
&& self.filesystem == other.filesystem
|
||||
&& self.subagents == other.subagents
|
||||
}
|
||||
|
||||
pub(crate) fn diff_from_turn_context_item(
|
||||
before: &TurnContextItem,
|
||||
after: &EnvironmentContext,
|
||||
) -> Self {
|
||||
let before_network = Self::network_from_turn_context_item(before);
|
||||
let before_filesystem = Self::filesystem_from_turn_context_item(before);
|
||||
let environments = match &after.environments {
|
||||
EnvironmentContextEnvironments::Single(environment) => {
|
||||
if PathUri::from_abs_path(&before.cwd) != environment.cwd {
|
||||
EnvironmentContextEnvironments::Single(EnvironmentContextEnvironment {
|
||||
id: String::new(),
|
||||
cwd: environment.cwd.clone(),
|
||||
status: environment.status.clone(),
|
||||
})
|
||||
} else {
|
||||
EnvironmentContextEnvironments::None
|
||||
}
|
||||
}
|
||||
EnvironmentContextEnvironments::Multiple(environments) => {
|
||||
EnvironmentContextEnvironments::Multiple(environments.clone())
|
||||
}
|
||||
EnvironmentContextEnvironments::None => EnvironmentContextEnvironments::None,
|
||||
};
|
||||
let network = if before_network != after.network {
|
||||
after.network.clone()
|
||||
} else {
|
||||
before_network
|
||||
};
|
||||
let filesystem = if before_filesystem != after.filesystem {
|
||||
after.filesystem.clone()
|
||||
} else {
|
||||
before_filesystem
|
||||
};
|
||||
EnvironmentContext::new_with_environments(
|
||||
environments,
|
||||
after.current_date.clone(),
|
||||
after.timezone.clone(),
|
||||
network,
|
||||
filesystem,
|
||||
/*subagents*/ None,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn from_turn_context(turn_context: &TurnContext, shell: &Shell) -> Self {
|
||||
let mut context = Self::new(
|
||||
EnvironmentContextEnvironment::from_turn_environments(
|
||||
&turn_context.environments.turn_environments,
|
||||
shell,
|
||||
),
|
||||
turn_context.current_date.clone(),
|
||||
turn_context.timezone.clone(),
|
||||
Self::network_from_turn_context(turn_context),
|
||||
/*subagents*/ None,
|
||||
);
|
||||
context.filesystem = Some(FileSystemContext::from_permission_profile(
|
||||
&turn_context.permission_profile,
|
||||
&turn_context.config.effective_workspace_roots(),
|
||||
));
|
||||
context
|
||||
}
|
||||
|
||||
pub(crate) fn from_turn_context_item(
|
||||
turn_context_item: &TurnContextItem,
|
||||
shell: String,
|
||||
) -> Self {
|
||||
Self::new_with_environments(
|
||||
EnvironmentContextEnvironments::from_vec(vec![EnvironmentContextEnvironment {
|
||||
id: String::new(),
|
||||
cwd: PathUri::from_abs_path(&turn_context_item.cwd),
|
||||
status: EnvironmentContextEnvironmentStatus::Ready { shell },
|
||||
}]),
|
||||
turn_context_item.current_date.clone(),
|
||||
turn_context_item.timezone.clone(),
|
||||
Self::network_from_turn_context_item(turn_context_item),
|
||||
Self::filesystem_from_turn_context_item(turn_context_item),
|
||||
/*subagents*/ None,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn with_subagents(mut self, subagents: String) -> Self {
|
||||
if !subagents.is_empty() {
|
||||
self.subagents = Some(subagents);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
fn network_from_turn_context(turn_context: &TurnContext) -> Option<NetworkContext> {
|
||||
let network = turn_context
|
||||
.config
|
||||
.config_layer_stack
|
||||
.requirements()
|
||||
.network
|
||||
.as_ref()?;
|
||||
|
||||
Some(NetworkContext::new(
|
||||
network
|
||||
.domains
|
||||
.as_ref()
|
||||
.and_then(codex_config::NetworkDomainPermissionsToml::allowed_domains)
|
||||
.unwrap_or_default(),
|
||||
network
|
||||
.domains
|
||||
.as_ref()
|
||||
.and_then(codex_config::NetworkDomainPermissionsToml::denied_domains)
|
||||
.unwrap_or_default(),
|
||||
))
|
||||
}
|
||||
|
||||
fn network_from_turn_context_item(
|
||||
turn_context_item: &TurnContextItem,
|
||||
) -> Option<NetworkContext> {
|
||||
let TurnContextNetworkItem {
|
||||
allowed_domains,
|
||||
denied_domains,
|
||||
} = turn_context_item.network.as_ref()?;
|
||||
Some(NetworkContext::new(
|
||||
allowed_domains.clone(),
|
||||
denied_domains.clone(),
|
||||
))
|
||||
}
|
||||
|
||||
fn filesystem_from_turn_context_item(
|
||||
turn_context_item: &TurnContextItem,
|
||||
) -> Option<FileSystemContext> {
|
||||
Some(FileSystemContext::from_permission_profile(
|
||||
&turn_context_item.permission_profile(),
|
||||
&workspace_roots_from_turn_context_item(turn_context_item),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn workspace_roots_from_turn_context_item(
|
||||
turn_context_item: &TurnContextItem,
|
||||
) -> Vec<AbsolutePathBuf> {
|
||||
if let Some(workspace_roots) = turn_context_item.workspace_roots.as_ref() {
|
||||
return workspace_roots.clone();
|
||||
}
|
||||
|
||||
vec![turn_context_item.cwd.clone()]
|
||||
}
|
||||
|
||||
impl ContextualUserFragment for EnvironmentContext {
|
||||
fn role(&self) -> &'static str {
|
||||
"user"
|
||||
}
|
||||
|
||||
fn markers(&self) -> (&'static str, &'static str) {
|
||||
Self::type_markers()
|
||||
}
|
||||
|
||||
fn type_markers() -> (&'static str, &'static str) {
|
||||
(
|
||||
codex_protocol::protocol::ENVIRONMENT_CONTEXT_OPEN_TAG,
|
||||
codex_protocol::protocol::ENVIRONMENT_CONTEXT_CLOSE_TAG,
|
||||
)
|
||||
}
|
||||
|
||||
fn body(&self) -> String {
|
||||
let mut lines = Vec::new();
|
||||
match &self.environments {
|
||||
EnvironmentContextEnvironments::Single(environment) => {
|
||||
let cwd = environment.cwd.inferred_native_path_string();
|
||||
lines.push(format!(" <cwd>{cwd}</cwd>"));
|
||||
lines.push(environment.status.render(" "));
|
||||
}
|
||||
EnvironmentContextEnvironments::Multiple(environments) => {
|
||||
lines.push(" <environments>".to_string());
|
||||
for environment in environments {
|
||||
lines.push(format!(" <environment id=\"{}\">", environment.id));
|
||||
let cwd = environment.cwd.inferred_native_path_string();
|
||||
lines.push(format!(" <cwd>{cwd}</cwd>"));
|
||||
lines.push(environment.status.render(" "));
|
||||
lines.push(" </environment>".to_string());
|
||||
}
|
||||
lines.push(" </environments>".to_string());
|
||||
}
|
||||
EnvironmentContextEnvironments::None => {}
|
||||
}
|
||||
if let Some(current_date) = &self.current_date {
|
||||
lines.push(format!(" <current_date>{current_date}</current_date>"));
|
||||
}
|
||||
if let Some(timezone) = &self.timezone {
|
||||
lines.push(format!(" <timezone>{timezone}</timezone>"));
|
||||
}
|
||||
match &self.network {
|
||||
Some(network) => {
|
||||
lines.push(format!(" {}", network.render()));
|
||||
}
|
||||
None => {
|
||||
// TODO(mbolin): Include this line if it helps the model.
|
||||
// lines.push(" <network enabled=\"false\" />".to_string());
|
||||
}
|
||||
}
|
||||
if let Some(filesystem) = &self.filesystem {
|
||||
lines.push(format!(" {}", filesystem.render()));
|
||||
}
|
||||
if let Some(subagents) = &self.subagents {
|
||||
lines.push(" <subagents>".to_string());
|
||||
lines.extend(subagents.lines().map(|line| format!(" {line}")));
|
||||
lines.push(" </subagents>".to_string());
|
||||
}
|
||||
format!("\n{}\n", lines.join("\n"))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "environment_context_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -32,6 +32,7 @@ mod token_budget_context;
|
||||
mod turn_aborted;
|
||||
mod user_instructions;
|
||||
mod user_shell_command;
|
||||
pub(crate) mod world_state;
|
||||
|
||||
pub(crate) use approved_command_prefix_saved::ApprovedCommandPrefixSaved;
|
||||
pub(crate) use apps_instructions::AppsInstructions;
|
||||
@@ -47,7 +48,6 @@ pub(crate) use collaboration_mode_instructions::CollaborationModeInstructions;
|
||||
pub(crate) use contextual_user_message::is_contextual_user_fragment;
|
||||
pub(crate) use contextual_user_message::parse_visible_hook_prompt_message;
|
||||
pub(crate) use current_time_reminder::CurrentTimeReminder;
|
||||
pub(crate) use environment_context::EnvironmentContext;
|
||||
pub(crate) use guardian_followup_review_reminder::GuardianFollowupReviewReminder;
|
||||
pub(crate) use hook_additional_context::HookAdditionalContext;
|
||||
pub(crate) use image_generation_instructions::ImageGenerationInstructions;
|
||||
|
||||
@@ -0,0 +1,387 @@
|
||||
use super::WorldStateSection;
|
||||
use crate::context::ContextualUserFragment;
|
||||
use crate::context::environment_context::FileSystemContext;
|
||||
use crate::context::environment_context::NetworkContext;
|
||||
use crate::context::environment_context::push_xml_escaped_text;
|
||||
use crate::environment_selection::TurnEnvironmentSnapshot;
|
||||
use crate::session::turn_context::TurnContext;
|
||||
use codex_exec_server::LOCAL_ENVIRONMENT_ID;
|
||||
use codex_protocol::protocol::TurnContextItem;
|
||||
use codex_protocol::protocol::TurnContextNetworkItem;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
/// Environment values visible to the model.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub(crate) struct EnvironmentsState {
|
||||
environments: BTreeMap<String, EnvironmentState>,
|
||||
current_date: Option<String>,
|
||||
timezone: Option<String>,
|
||||
network: Option<NetworkContext>,
|
||||
filesystem: Option<FileSystemContext>,
|
||||
subagents: Option<String>,
|
||||
}
|
||||
|
||||
impl EnvironmentsState {
|
||||
pub(crate) fn from_turn_context(turn_context: &TurnContext) -> Self {
|
||||
Self::from_turn_context_with_environments(turn_context, &turn_context.environments)
|
||||
}
|
||||
|
||||
pub(crate) fn from_turn_context_with_environments(
|
||||
turn_context: &TurnContext,
|
||||
environments: &TurnEnvironmentSnapshot,
|
||||
) -> Self {
|
||||
Self {
|
||||
environments: environment_states(environments),
|
||||
current_date: turn_context.current_date.clone(),
|
||||
timezone: turn_context.timezone.clone(),
|
||||
network: network_from_turn_context(turn_context),
|
||||
filesystem: Some(FileSystemContext::from_permission_profile(
|
||||
&turn_context.permission_profile,
|
||||
&turn_context.config.effective_workspace_roots(),
|
||||
)),
|
||||
subagents: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn from_turn_context_item(turn_context_item: &TurnContextItem) -> Self {
|
||||
Self {
|
||||
environments: [(
|
||||
LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
EnvironmentState {
|
||||
cwd: PathUri::from_abs_path(&turn_context_item.cwd),
|
||||
status: EnvironmentStatus::Available,
|
||||
shell: None,
|
||||
},
|
||||
)]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
current_date: turn_context_item.current_date.clone(),
|
||||
timezone: turn_context_item.timezone.clone(),
|
||||
network: network_from_turn_context_item(turn_context_item),
|
||||
filesystem: Some(FileSystemContext::from_permission_profile(
|
||||
&turn_context_item.permission_profile(),
|
||||
&workspace_roots_from_turn_context_item(turn_context_item),
|
||||
)),
|
||||
subagents: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn with_subagents(mut self, subagents: String) -> Self {
|
||||
if !subagents.is_empty() {
|
||||
self.subagents = Some(subagents);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
fn rendered_full(&self) -> RenderedEnvironments {
|
||||
RenderedEnvironments {
|
||||
updates: self
|
||||
.environments
|
||||
.iter()
|
||||
.map(|(id, environment)| {
|
||||
(id.clone(), EnvironmentUpdate::Current(environment.clone()))
|
||||
})
|
||||
.collect(),
|
||||
legacy_single: is_legacy_single(&self.environments),
|
||||
current_date: self.current_date.clone(),
|
||||
timezone: self.timezone.clone(),
|
||||
network: self.network.clone(),
|
||||
filesystem: self.filesystem.clone(),
|
||||
subagents: self.subagents.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WorldStateSection for EnvironmentsState {
|
||||
fn render_diff(&self, previous: Option<&Self>) -> Option<Box<dyn ContextualUserFragment>> {
|
||||
let empty = Self::default();
|
||||
let previous = previous.unwrap_or(&empty);
|
||||
let turn_context_values_changed = self.current_date != previous.current_date
|
||||
|| self.timezone != previous.timezone
|
||||
|| self.network != previous.network
|
||||
|| self.filesystem != previous.filesystem;
|
||||
let mut updates = self
|
||||
.environments
|
||||
.iter()
|
||||
.filter(|(id, environment)| {
|
||||
previous
|
||||
.environments
|
||||
.get(*id)
|
||||
.is_none_or(|previous| !environment.has_same_diff_value(previous))
|
||||
})
|
||||
.map(|(id, environment)| (id.clone(), EnvironmentUpdate::Current(environment.clone())))
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
updates.extend(
|
||||
previous
|
||||
.environments
|
||||
.keys()
|
||||
.filter(|id| !self.environments.contains_key(*id))
|
||||
.map(|id| (id.clone(), EnvironmentUpdate::Unavailable)),
|
||||
);
|
||||
let legacy_single = is_legacy_single(&self.environments)
|
||||
&& updates
|
||||
.values()
|
||||
.all(|update| matches!(update, EnvironmentUpdate::Current(_)));
|
||||
(!updates.is_empty() || turn_context_values_changed).then(|| {
|
||||
Box::new(RenderedEnvironments {
|
||||
updates,
|
||||
legacy_single,
|
||||
current_date: self.current_date.clone(),
|
||||
timezone: self.timezone.clone(),
|
||||
network: self.network.clone(),
|
||||
filesystem: self.filesystem.clone(),
|
||||
subagents: self.subagents.clone(),
|
||||
}) as Box<dyn ContextualUserFragment>
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ContextualUserFragment for EnvironmentsState {
|
||||
fn role(&self) -> &'static str {
|
||||
"user"
|
||||
}
|
||||
|
||||
fn markers(&self) -> (&'static str, &'static str) {
|
||||
Self::type_markers()
|
||||
}
|
||||
|
||||
fn type_markers() -> (&'static str, &'static str) {
|
||||
environment_context_markers()
|
||||
}
|
||||
|
||||
fn body(&self) -> String {
|
||||
self.rendered_full().body()
|
||||
}
|
||||
}
|
||||
|
||||
struct RenderedEnvironments {
|
||||
updates: BTreeMap<String, EnvironmentUpdate>,
|
||||
legacy_single: bool,
|
||||
current_date: Option<String>,
|
||||
timezone: Option<String>,
|
||||
network: Option<NetworkContext>,
|
||||
filesystem: Option<FileSystemContext>,
|
||||
subagents: Option<String>,
|
||||
}
|
||||
|
||||
enum EnvironmentUpdate {
|
||||
Current(EnvironmentState),
|
||||
Unavailable,
|
||||
}
|
||||
|
||||
impl ContextualUserFragment for RenderedEnvironments {
|
||||
fn role(&self) -> &'static str {
|
||||
"user"
|
||||
}
|
||||
|
||||
fn markers(&self) -> (&'static str, &'static str) {
|
||||
Self::type_markers()
|
||||
}
|
||||
|
||||
fn type_markers() -> (&'static str, &'static str) {
|
||||
environment_context_markers()
|
||||
}
|
||||
|
||||
fn body(&self) -> String {
|
||||
let mut rendered = "\n".to_string();
|
||||
if self.legacy_single {
|
||||
if let Some(EnvironmentUpdate::Current(environment)) = self.updates.values().next() {
|
||||
push_environment_values(&mut rendered, environment, " ");
|
||||
}
|
||||
} else if !self.updates.is_empty() {
|
||||
rendered.push_str(" <environments>\n");
|
||||
for (id, update) in &self.updates {
|
||||
match update {
|
||||
EnvironmentUpdate::Current(environment) => {
|
||||
rendered.push_str(" <environment id=\"");
|
||||
push_xml_escaped_text(&mut rendered, id);
|
||||
rendered.push('"');
|
||||
rendered.push_str(">\n");
|
||||
push_environment_values(&mut rendered, environment, " ");
|
||||
rendered.push_str(" </environment>\n");
|
||||
}
|
||||
EnvironmentUpdate::Unavailable => {
|
||||
rendered.push_str(" <environment id=\"");
|
||||
push_xml_escaped_text(&mut rendered, id);
|
||||
rendered.push_str("\" status=\"unavailable\" />\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
rendered.push_str(" </environments>\n");
|
||||
}
|
||||
push_optional_element(&mut rendered, "current_date", self.current_date.as_deref());
|
||||
push_optional_element(&mut rendered, "timezone", self.timezone.as_deref());
|
||||
if let Some(network) = &self.network {
|
||||
rendered.push_str(" ");
|
||||
rendered.push_str(&network.render());
|
||||
rendered.push('\n');
|
||||
}
|
||||
if let Some(filesystem) = &self.filesystem {
|
||||
rendered.push_str(" ");
|
||||
rendered.push_str(&filesystem.render());
|
||||
rendered.push('\n');
|
||||
}
|
||||
if let Some(subagents) = &self.subagents {
|
||||
rendered.push_str(" <subagents>\n");
|
||||
for line in subagents.lines() {
|
||||
rendered.push_str(" ");
|
||||
rendered.push_str(line);
|
||||
rendered.push('\n');
|
||||
}
|
||||
rendered.push_str(" </subagents>\n");
|
||||
}
|
||||
rendered
|
||||
}
|
||||
}
|
||||
|
||||
fn push_environment_values(rendered: &mut String, environment: &EnvironmentState, indent: &str) {
|
||||
rendered.push_str(indent);
|
||||
rendered.push_str("<cwd>");
|
||||
push_xml_escaped_text(rendered, &environment.cwd.inferred_native_path_string());
|
||||
rendered.push_str("</cwd>\n");
|
||||
if environment.status == EnvironmentStatus::Starting {
|
||||
rendered.push_str(indent);
|
||||
rendered.push_str("<status>starting</status>\n");
|
||||
}
|
||||
if let Some(shell) = &environment.shell {
|
||||
rendered.push_str(indent);
|
||||
rendered.push_str("<shell>");
|
||||
push_xml_escaped_text(rendered, shell);
|
||||
rendered.push_str("</shell>\n");
|
||||
}
|
||||
}
|
||||
|
||||
fn push_optional_element(rendered: &mut String, name: &str, value: Option<&str>) {
|
||||
let Some(value) = value else {
|
||||
return;
|
||||
};
|
||||
rendered.push_str(" <");
|
||||
rendered.push_str(name);
|
||||
rendered.push('>');
|
||||
push_xml_escaped_text(rendered, value);
|
||||
rendered.push_str("</");
|
||||
rendered.push_str(name);
|
||||
rendered.push_str(">\n");
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct EnvironmentState {
|
||||
cwd: PathUri,
|
||||
status: EnvironmentStatus,
|
||||
shell: Option<String>,
|
||||
}
|
||||
|
||||
impl EnvironmentState {
|
||||
fn has_same_diff_value(&self, other: &Self) -> bool {
|
||||
self.cwd == other.cwd
|
||||
&& self.status == other.status
|
||||
&& self
|
||||
.shell
|
||||
.as_ref()
|
||||
.zip(other.shell.as_ref())
|
||||
.is_none_or(|(current, previous)| current == previous)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum EnvironmentStatus {
|
||||
Starting,
|
||||
Available,
|
||||
}
|
||||
|
||||
fn environment_states(snapshot: &TurnEnvironmentSnapshot) -> BTreeMap<String, EnvironmentState> {
|
||||
let mut environments = snapshot
|
||||
.turn_environments
|
||||
.iter()
|
||||
.map(|environment| {
|
||||
(
|
||||
environment.environment_id.clone(),
|
||||
EnvironmentState {
|
||||
cwd: environment.cwd().clone(),
|
||||
status: EnvironmentStatus::Available,
|
||||
shell: environment
|
||||
.shell
|
||||
.as_ref()
|
||||
.map(|shell| shell.name().to_string()),
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
for environment in &snapshot.starting {
|
||||
environments
|
||||
.entry(environment.selection.environment_id.clone())
|
||||
.or_insert_with(|| EnvironmentState {
|
||||
cwd: environment.selection.cwd.clone(),
|
||||
status: EnvironmentStatus::Starting,
|
||||
shell: None,
|
||||
});
|
||||
}
|
||||
environments
|
||||
}
|
||||
|
||||
fn is_legacy_single(environments: &BTreeMap<String, EnvironmentState>) -> bool {
|
||||
environments.len() == 1
|
||||
&& environments
|
||||
.values()
|
||||
.all(|environment| environment.status == EnvironmentStatus::Available)
|
||||
}
|
||||
|
||||
fn environment_context_markers() -> (&'static str, &'static str) {
|
||||
(
|
||||
codex_protocol::protocol::ENVIRONMENT_CONTEXT_OPEN_TAG,
|
||||
codex_protocol::protocol::ENVIRONMENT_CONTEXT_CLOSE_TAG,
|
||||
)
|
||||
}
|
||||
|
||||
fn network_from_turn_context(turn_context: &TurnContext) -> Option<NetworkContext> {
|
||||
let network = turn_context
|
||||
.config
|
||||
.config_layer_stack
|
||||
.requirements()
|
||||
.network
|
||||
.as_ref()?;
|
||||
|
||||
Some(NetworkContext::new(
|
||||
network
|
||||
.domains
|
||||
.as_ref()
|
||||
.and_then(codex_config::NetworkDomainPermissionsToml::allowed_domains)
|
||||
.unwrap_or_default(),
|
||||
network
|
||||
.domains
|
||||
.as_ref()
|
||||
.and_then(codex_config::NetworkDomainPermissionsToml::denied_domains)
|
||||
.unwrap_or_default(),
|
||||
))
|
||||
}
|
||||
|
||||
fn network_from_turn_context_item(turn_context_item: &TurnContextItem) -> Option<NetworkContext> {
|
||||
let TurnContextNetworkItem {
|
||||
allowed_domains,
|
||||
denied_domains,
|
||||
} = turn_context_item.network.as_ref()?;
|
||||
Some(NetworkContext::new(
|
||||
allowed_domains.clone(),
|
||||
denied_domains.clone(),
|
||||
))
|
||||
}
|
||||
|
||||
fn workspace_roots_from_turn_context_item(
|
||||
turn_context_item: &TurnContextItem,
|
||||
) -> Vec<AbsolutePathBuf> {
|
||||
if let Some(workspace_roots) = turn_context_item.workspace_roots.as_ref() {
|
||||
return workspace_roots.clone();
|
||||
}
|
||||
|
||||
vec![turn_context_item.cwd.clone()]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "environment_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "environment_render_tests.rs"]
|
||||
mod render_tests;
|
||||
+72
-170
@@ -1,6 +1,5 @@
|
||||
use crate::shell::ShellType;
|
||||
|
||||
use super::EnvironmentContextEnvironmentStatus::Ready;
|
||||
use super::*;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::permissions::FileSystemAccessMode;
|
||||
@@ -31,17 +30,43 @@ fn test_abs_path(unix_path: &str) -> AbsolutePathBuf {
|
||||
test_path_buf(unix_path).abs()
|
||||
}
|
||||
|
||||
fn environment(id: &str, cwd: PathUri, shell: impl Into<String>) -> (String, EnvironmentState) {
|
||||
(
|
||||
id.to_string(),
|
||||
EnvironmentState {
|
||||
cwd,
|
||||
status: EnvironmentStatus::Available,
|
||||
shell: Some(shell.into()),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn environment_state(
|
||||
environments: impl IntoIterator<Item = (String, EnvironmentState)>,
|
||||
current_date: Option<String>,
|
||||
timezone: Option<String>,
|
||||
network: Option<NetworkContext>,
|
||||
subagents: Option<String>,
|
||||
) -> EnvironmentsState {
|
||||
EnvironmentsState {
|
||||
environments: environments.into_iter().collect(),
|
||||
current_date,
|
||||
timezone,
|
||||
network,
|
||||
filesystem: None,
|
||||
subagents,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_workspace_write_environment_context() {
|
||||
let cwd = test_path_buf("/repo");
|
||||
let context = EnvironmentContext::new(
|
||||
vec![EnvironmentContextEnvironment {
|
||||
id: "local".to_string(),
|
||||
cwd: PathUri::from_abs_path(&cwd.abs()),
|
||||
status: Ready {
|
||||
shell: fake_shell_name(),
|
||||
},
|
||||
}],
|
||||
let context = environment_state(
|
||||
[environment(
|
||||
"local",
|
||||
PathUri::from_abs_path(&cwd.abs()),
|
||||
fake_shell_name(),
|
||||
)],
|
||||
Some("2026-02-26".to_string()),
|
||||
Some("America/Los_Angeles".to_string()),
|
||||
/*network*/ None,
|
||||
@@ -63,14 +88,12 @@ fn serialize_workspace_write_environment_context() {
|
||||
|
||||
#[test]
|
||||
fn serialize_environment_context_with_foreign_windows_cwd() {
|
||||
let context = EnvironmentContext::new(
|
||||
vec![EnvironmentContextEnvironment {
|
||||
id: "remote".to_string(),
|
||||
cwd: PathUri::parse("file:///C:/windows").expect("Windows cwd URI"),
|
||||
status: Ready {
|
||||
shell: "powershell".to_string(),
|
||||
},
|
||||
}],
|
||||
let context = environment_state(
|
||||
[environment(
|
||||
"remote",
|
||||
PathUri::parse("file:///C:/windows").expect("Windows cwd URI"),
|
||||
"powershell",
|
||||
)],
|
||||
/*current_date*/ None,
|
||||
/*timezone*/ None,
|
||||
/*network*/ None,
|
||||
@@ -92,14 +115,12 @@ fn serialize_environment_context_with_network() {
|
||||
vec!["api.example.com".to_string(), "*.openai.com".to_string()],
|
||||
vec!["blocked.example.com".to_string()],
|
||||
);
|
||||
let context = EnvironmentContext::new(
|
||||
vec![EnvironmentContextEnvironment {
|
||||
id: "local".to_string(),
|
||||
cwd: PathUri::from_abs_path(&test_abs_path("/repo")),
|
||||
status: Ready {
|
||||
shell: fake_shell_name(),
|
||||
},
|
||||
}],
|
||||
let context = environment_state(
|
||||
[environment(
|
||||
"local",
|
||||
PathUri::from_abs_path(&test_abs_path("/repo")),
|
||||
fake_shell_name(),
|
||||
)],
|
||||
Some("2026-02-26".to_string()),
|
||||
Some("America/Los_Angeles".to_string()),
|
||||
Some(network),
|
||||
@@ -156,14 +177,12 @@ fn serialize_environment_context_with_full_filesystem_profile() {
|
||||
AbsolutePathBuf::resolve_path_against_base(Path::new("private/**"), repo.as_path());
|
||||
let other_repo_private_glob =
|
||||
AbsolutePathBuf::resolve_path_against_base(Path::new("private/**"), other_repo.as_path());
|
||||
let mut context = EnvironmentContext::new(
|
||||
vec![EnvironmentContextEnvironment {
|
||||
id: "local".to_string(),
|
||||
cwd: PathUri::from_abs_path(&test_abs_path("/repo")),
|
||||
status: Ready {
|
||||
shell: fake_shell_name(),
|
||||
},
|
||||
}],
|
||||
let mut context = environment_state(
|
||||
[environment(
|
||||
"local",
|
||||
PathUri::from_abs_path(&test_abs_path("/repo")),
|
||||
fake_shell_name(),
|
||||
)],
|
||||
/*current_date*/ None,
|
||||
/*timezone*/ None,
|
||||
/*network*/ None,
|
||||
@@ -219,7 +238,7 @@ fn turn_context_item_filesystem_uses_workspace_roots_instead_of_cwd() {
|
||||
summary: codex_protocol::config_types::ReasoningSummary::Auto,
|
||||
};
|
||||
|
||||
let context = EnvironmentContext::from_turn_context_item(&item, fake_shell_name()).render();
|
||||
let context = EnvironmentsState::from_turn_context_item(&item).render();
|
||||
|
||||
assert!(
|
||||
context.contains(&format!(
|
||||
@@ -246,7 +265,7 @@ fn turn_context_item_filesystem_uses_workspace_roots_instead_of_cwd() {
|
||||
|
||||
#[test]
|
||||
fn serialize_read_only_environment_context() {
|
||||
let context = EnvironmentContext::new(
|
||||
let context = environment_state(
|
||||
Vec::new(),
|
||||
Some("2026-02-26".to_string()),
|
||||
Some("America/Los_Angeles".to_string()),
|
||||
@@ -262,111 +281,14 @@ fn serialize_read_only_environment_context() {
|
||||
assert_eq!(context.render(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn equals_except_shell_compares_cwd() {
|
||||
let context1 = EnvironmentContext::new(
|
||||
vec![EnvironmentContextEnvironment {
|
||||
id: "local".to_string(),
|
||||
cwd: PathUri::from_abs_path(&test_abs_path("/repo")),
|
||||
status: Ready {
|
||||
shell: fake_shell_name(),
|
||||
},
|
||||
}],
|
||||
/*current_date*/ None,
|
||||
/*timezone*/ None,
|
||||
/*network*/ None,
|
||||
/*subagents*/ None,
|
||||
);
|
||||
let context2 = EnvironmentContext::new(
|
||||
vec![EnvironmentContextEnvironment {
|
||||
id: "local".to_string(),
|
||||
cwd: PathUri::from_abs_path(&test_abs_path("/repo")),
|
||||
status: Ready {
|
||||
shell: fake_shell_name(),
|
||||
},
|
||||
}],
|
||||
/*current_date*/ None,
|
||||
/*timezone*/ None,
|
||||
/*network*/ None,
|
||||
/*subagents*/ None,
|
||||
);
|
||||
assert!(context1.equals_except_shell(&context2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn equals_except_shell_compares_cwd_differences() {
|
||||
let context1 = EnvironmentContext::new(
|
||||
vec![EnvironmentContextEnvironment {
|
||||
id: "local".to_string(),
|
||||
cwd: PathUri::from_abs_path(&test_abs_path("/repo1")),
|
||||
status: Ready {
|
||||
shell: fake_shell_name(),
|
||||
},
|
||||
}],
|
||||
/*current_date*/ None,
|
||||
/*timezone*/ None,
|
||||
/*network*/ None,
|
||||
/*subagents*/ None,
|
||||
);
|
||||
let context2 = EnvironmentContext::new(
|
||||
vec![EnvironmentContextEnvironment {
|
||||
id: "local".to_string(),
|
||||
cwd: PathUri::from_abs_path(&test_abs_path("/repo2")),
|
||||
status: Ready {
|
||||
shell: fake_shell_name(),
|
||||
},
|
||||
}],
|
||||
/*current_date*/ None,
|
||||
/*timezone*/ None,
|
||||
/*network*/ None,
|
||||
/*subagents*/ None,
|
||||
);
|
||||
|
||||
assert!(!context1.equals_except_shell(&context2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn equals_except_shell_ignores_shell() {
|
||||
let context1 = EnvironmentContext::new(
|
||||
vec![EnvironmentContextEnvironment {
|
||||
id: "local".to_string(),
|
||||
cwd: PathUri::from_abs_path(&test_abs_path("/repo")),
|
||||
status: Ready {
|
||||
shell: "bash".to_string(),
|
||||
},
|
||||
}],
|
||||
/*current_date*/ None,
|
||||
/*timezone*/ None,
|
||||
/*network*/ None,
|
||||
/*subagents*/ None,
|
||||
);
|
||||
let context2 = EnvironmentContext::new(
|
||||
vec![EnvironmentContextEnvironment {
|
||||
id: "other".to_string(),
|
||||
cwd: PathUri::from_abs_path(&test_abs_path("/repo")),
|
||||
status: Ready {
|
||||
shell: "zsh".to_string(),
|
||||
},
|
||||
}],
|
||||
/*current_date*/ None,
|
||||
/*timezone*/ None,
|
||||
/*network*/ None,
|
||||
/*subagents*/ None,
|
||||
);
|
||||
|
||||
assert!(context1.equals_except_shell(&context2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_environment_context_with_subagents() {
|
||||
let context = EnvironmentContext::new(
|
||||
vec![EnvironmentContextEnvironment {
|
||||
id: "local".to_string(),
|
||||
cwd: PathUri::from_abs_path(&test_abs_path("/repo")),
|
||||
status: Ready {
|
||||
shell: fake_shell_name(),
|
||||
},
|
||||
}],
|
||||
let context = environment_state(
|
||||
[environment(
|
||||
"local",
|
||||
PathUri::from_abs_path(&test_abs_path("/repo")),
|
||||
fake_shell_name(),
|
||||
)],
|
||||
Some("2026-02-26".to_string()),
|
||||
Some("America/Los_Angeles".to_string()),
|
||||
/*network*/ None,
|
||||
@@ -394,22 +316,10 @@ fn serialize_environment_context_with_subagents() {
|
||||
fn serialize_environment_context_with_multiple_selected_environments() {
|
||||
let local_cwd = test_path_buf("/repo/local");
|
||||
let remote_cwd = test_path_buf("/repo/remote");
|
||||
let context = EnvironmentContext::new(
|
||||
vec![
|
||||
EnvironmentContextEnvironment {
|
||||
id: "local".to_string(),
|
||||
cwd: PathUri::from_abs_path(&local_cwd.abs()),
|
||||
status: Ready {
|
||||
shell: "bash".to_string(),
|
||||
},
|
||||
},
|
||||
EnvironmentContextEnvironment {
|
||||
id: "remote".to_string(),
|
||||
cwd: PathUri::from_abs_path(&remote_cwd.abs()),
|
||||
status: Ready {
|
||||
shell: "bash".to_string(),
|
||||
},
|
||||
},
|
||||
let context = environment_state(
|
||||
[
|
||||
environment("local", PathUri::from_abs_path(&local_cwd.abs()), "bash"),
|
||||
environment("remote", PathUri::from_abs_path(&remote_cwd.abs()), "bash"),
|
||||
],
|
||||
Some("2026-02-26".to_string()),
|
||||
Some("America/Los_Angeles".to_string()),
|
||||
@@ -443,22 +353,14 @@ fn serialize_environment_context_with_multiple_selected_environments() {
|
||||
fn serialize_environment_context_prefers_environment_shell_when_present() {
|
||||
let local_cwd = test_path_buf("/repo/local");
|
||||
let remote_cwd = test_path_buf("/repo/remote");
|
||||
let context = EnvironmentContext::new(
|
||||
vec![
|
||||
EnvironmentContextEnvironment {
|
||||
id: "local".to_string(),
|
||||
cwd: PathUri::from_abs_path(&local_cwd.abs()),
|
||||
status: Ready {
|
||||
shell: "powershell".to_string(),
|
||||
},
|
||||
},
|
||||
EnvironmentContextEnvironment {
|
||||
id: "remote".to_string(),
|
||||
cwd: PathUri::from_abs_path(&remote_cwd.abs()),
|
||||
status: Ready {
|
||||
shell: "cmd".to_string(),
|
||||
},
|
||||
},
|
||||
let context = environment_state(
|
||||
[
|
||||
environment(
|
||||
"local",
|
||||
PathUri::from_abs_path(&local_cwd.abs()),
|
||||
"powershell",
|
||||
),
|
||||
environment("remote", PathUri::from_abs_path(&remote_cwd.abs()), "cmd"),
|
||||
],
|
||||
/*current_date*/ None,
|
||||
/*timezone*/ None,
|
||||
@@ -0,0 +1,256 @@
|
||||
use super::*;
|
||||
use crate::context::ContextualUserFragment;
|
||||
use crate::context::world_state::WorldState;
|
||||
use anyhow::Result;
|
||||
use codex_exec_server::LOCAL_ENVIRONMENT_ID;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn renders_full_environment_state() -> Result<()> {
|
||||
let context = EnvironmentsState {
|
||||
environments: [
|
||||
("laptop".to_string(), available("file:///repo", "zsh")?),
|
||||
(
|
||||
"devbox".to_string(),
|
||||
available("file:///workspace", "bash")?,
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut world_state = WorldState::default();
|
||||
world_state.add_section(context);
|
||||
|
||||
assert_eq!(
|
||||
vec![user_message(
|
||||
r#"<environment_context>
|
||||
<environments>
|
||||
<environment id="devbox">
|
||||
<cwd>/workspace</cwd>
|
||||
<shell>bash</shell>
|
||||
</environment>
|
||||
<environment id="laptop">
|
||||
<cwd>/repo</cwd>
|
||||
<shell>zsh</shell>
|
||||
</environment>
|
||||
</environments>
|
||||
</environment_context>"#,
|
||||
)],
|
||||
render_fragments(world_state.render_full()),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn renders_only_changed_environments() -> Result<()> {
|
||||
let mut previous = WorldState::default();
|
||||
previous.add_section(EnvironmentsState {
|
||||
environments: [
|
||||
("laptop".to_string(), available("file:///repo", "bash")?),
|
||||
("devbox".to_string(), starting("file:///workspace")?),
|
||||
("old".to_string(), available("file:///old", "sh")?),
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
..Default::default()
|
||||
});
|
||||
let mut current = WorldState::default();
|
||||
current.add_section(EnvironmentsState {
|
||||
environments: [
|
||||
("laptop".to_string(), available("file:///repo", "zsh")?),
|
||||
(
|
||||
"devbox".to_string(),
|
||||
available("file:///workspace", "powershell")?,
|
||||
),
|
||||
("remote".to_string(), starting("file:///remote")?),
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
vec![user_message(
|
||||
r#"<environment_context>
|
||||
<environments>
|
||||
<environment id="devbox">
|
||||
<cwd>/workspace</cwd>
|
||||
<shell>powershell</shell>
|
||||
</environment>
|
||||
<environment id="laptop">
|
||||
<cwd>/repo</cwd>
|
||||
<shell>zsh</shell>
|
||||
</environment>
|
||||
<environment id="old" status="unavailable" />
|
||||
<environment id="remote">
|
||||
<cwd>/remote</cwd>
|
||||
<status>starting</status>
|
||||
</environment>
|
||||
</environments>
|
||||
</environment_context>"#,
|
||||
)],
|
||||
render_fragments(current.render_diff(&previous)),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn persisted_turn_context_values_render_a_diff() -> Result<()> {
|
||||
let environments = EnvironmentsState {
|
||||
environments: [(
|
||||
LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
available("file:///repo", "zsh")?,
|
||||
)]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
..Default::default()
|
||||
};
|
||||
let mut previous = WorldState::default();
|
||||
previous.add_section(EnvironmentsState {
|
||||
current_date: Some("2026-06-19".to_string()),
|
||||
timezone: Some("UTC".to_string()),
|
||||
network: Some(NetworkContext::new(
|
||||
vec!["old.example.com".to_string()],
|
||||
vec![],
|
||||
)),
|
||||
filesystem: Some(FileSystemContext::from_permission_profile(
|
||||
&PermissionProfile::Disabled,
|
||||
&[],
|
||||
)),
|
||||
..environments.clone()
|
||||
});
|
||||
let mut current = WorldState::default();
|
||||
current.add_section(EnvironmentsState {
|
||||
current_date: Some("2026-06-20".to_string()),
|
||||
timezone: Some("America/Los_Angeles".to_string()),
|
||||
network: Some(NetworkContext::new(
|
||||
vec!["new.example.com".to_string()],
|
||||
vec!["blocked.example.com".to_string()],
|
||||
)),
|
||||
filesystem: Some(FileSystemContext::from_permission_profile(
|
||||
&PermissionProfile::External {
|
||||
network: NetworkSandboxPolicy::Restricted,
|
||||
},
|
||||
&[],
|
||||
)),
|
||||
..environments
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
vec![user_message(
|
||||
r#"<environment_context>
|
||||
<current_date>2026-06-20</current_date>
|
||||
<timezone>America/Los_Angeles</timezone>
|
||||
<network enabled="true"><allowed>new.example.com</allowed><denied>blocked.example.com</denied></network>
|
||||
<filesystem><permission_profile type="external"><file_system type="external" /></permission_profile></filesystem>
|
||||
</environment_context>"#,
|
||||
)],
|
||||
render_fragments(current.render_diff(&previous)),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_environment_diff_ignores_unknown_shell() -> Result<()> {
|
||||
let previous = EnvironmentsState {
|
||||
environments: [(
|
||||
LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
EnvironmentState {
|
||||
cwd: PathUri::parse("file:///repo")?,
|
||||
status: EnvironmentStatus::Available,
|
||||
shell: None,
|
||||
},
|
||||
)]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
..Default::default()
|
||||
};
|
||||
let current = EnvironmentsState {
|
||||
environments: [(
|
||||
LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
available("file:///repo", "zsh")?,
|
||||
)]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
None,
|
||||
render_fragment(WorldStateSection::render_diff(¤t, Some(&previous)))
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn removed_legacy_environment_renders_unavailable() -> Result<()> {
|
||||
let previous = EnvironmentsState {
|
||||
environments: [(
|
||||
LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
available("file:///repo", "bash")?,
|
||||
)]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
Some(user_message(
|
||||
r#"<environment_context>
|
||||
<environments>
|
||||
<environment id="local" status="unavailable" />
|
||||
</environments>
|
||||
</environment_context>"#,
|
||||
)),
|
||||
render_fragment(WorldStateSection::render_diff(
|
||||
&EnvironmentsState::default(),
|
||||
Some(&previous),
|
||||
)),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn available(cwd: &str, shell: &str) -> Result<EnvironmentState> {
|
||||
Ok(EnvironmentState {
|
||||
cwd: PathUri::parse(cwd)?,
|
||||
status: EnvironmentStatus::Available,
|
||||
shell: Some(shell.to_string()),
|
||||
})
|
||||
}
|
||||
|
||||
fn starting(cwd: &str) -> Result<EnvironmentState> {
|
||||
Ok(EnvironmentState {
|
||||
cwd: PathUri::parse(cwd)?,
|
||||
status: EnvironmentStatus::Starting,
|
||||
shell: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn render_fragments(fragments: Vec<Box<dyn ContextualUserFragment>>) -> Vec<ResponseItem> {
|
||||
fragments
|
||||
.into_iter()
|
||||
.map(ContextualUserFragment::into_boxed_response_item)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn render_fragment(fragment: Option<Box<dyn ContextualUserFragment>>) -> Option<ResponseItem> {
|
||||
fragment.map(ContextualUserFragment::into_boxed_response_item)
|
||||
}
|
||||
|
||||
fn user_message(text: &str) -> ResponseItem {
|
||||
ResponseItem::Message {
|
||||
id: None,
|
||||
role: "user".to_string(),
|
||||
content: vec![ContentItem::InputText {
|
||||
text: text.to_string(),
|
||||
}],
|
||||
phase: None,
|
||||
metadata: None,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
mod environment;
|
||||
|
||||
use crate::context::ContextualUserFragment;
|
||||
use indexmap::IndexMap;
|
||||
use std::any::Any;
|
||||
use std::any::TypeId;
|
||||
use std::fmt;
|
||||
|
||||
pub(crate) use environment::EnvironmentsState;
|
||||
|
||||
trait ErasedWorldStateSection: Send + Sync {
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
|
||||
fn render_diff(&self, previous: Option<&dyn Any>) -> Option<Box<dyn ContextualUserFragment>>;
|
||||
}
|
||||
|
||||
impl<S: WorldStateSection> ErasedWorldStateSection for S {
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn render_diff(&self, previous: Option<&dyn Any>) -> Option<Box<dyn ContextualUserFragment>> {
|
||||
let previous = match previous {
|
||||
Some(previous) => {
|
||||
let Some(previous) = previous.downcast_ref::<S>() else {
|
||||
unreachable!("world-state section type must match its type ID");
|
||||
};
|
||||
Some(previous)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
WorldStateSection::render_diff(self, previous)
|
||||
}
|
||||
}
|
||||
|
||||
/// A typed portion of the state visible to the model.
|
||||
///
|
||||
/// Implementations own how their current state is rendered relative to an
|
||||
/// earlier value of the same section type. A missing previous value requests
|
||||
/// the section's complete current representation.
|
||||
pub(crate) trait WorldStateSection: Any + Send + Sync {
|
||||
fn render_diff(&self, previous: Option<&Self>) -> Option<Box<dyn ContextualUserFragment>>;
|
||||
}
|
||||
|
||||
/// A snapshot of the model-visible world with one section per concrete type.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct WorldState {
|
||||
sections: IndexMap<TypeId, Box<dyn ErasedWorldStateSection>>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for WorldState {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("WorldState")
|
||||
.field("section_count", &self.sections.len())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl WorldState {
|
||||
pub(crate) fn add_section<S: WorldStateSection>(&mut self, section: S) {
|
||||
self.sections.insert(TypeId::of::<S>(), Box::new(section));
|
||||
}
|
||||
|
||||
pub(crate) fn render_full(&self) -> Vec<Box<dyn ContextualUserFragment>> {
|
||||
self.render_diff(&Self::default())
|
||||
}
|
||||
|
||||
pub(crate) fn render_diff(&self, previous: &Self) -> Vec<Box<dyn ContextualUserFragment>> {
|
||||
self.sections
|
||||
.iter()
|
||||
.filter_map(|(type_id, section)| {
|
||||
let previous = previous
|
||||
.sections
|
||||
.get(type_id)
|
||||
.map(|section| section.as_any());
|
||||
section.render_diff(previous)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::context::EnvironmentContext;
|
||||
use crate::context::ContextualUserFragment;
|
||||
use crate::context::world_state::WorldState;
|
||||
use crate::context_manager::normalize;
|
||||
use crate::event_mapping::has_non_contextual_dev_message_content;
|
||||
use crate::event_mapping::is_contextual_dev_message_content;
|
||||
@@ -28,6 +29,7 @@ use codex_utils_output_truncation::truncate_function_output_items_with_policy;
|
||||
use codex_utils_output_truncation::truncate_text;
|
||||
use std::num::NonZeroUsize;
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
/// Transcript of thread history
|
||||
@@ -49,8 +51,8 @@ pub(crate) struct ContextManager {
|
||||
/// also clear this when it trims a mixed initial-context developer bundle
|
||||
/// whose non-diff fragments no longer exist in the surviving history.
|
||||
reference_context_item: Option<TurnContextItem>,
|
||||
/// Environment state most recently appended to model-visible history.
|
||||
environment_context_baseline: Option<EnvironmentContext>,
|
||||
/// World state most recently appended to model-visible history.
|
||||
world_state_baseline: Option<Arc<WorldState>>,
|
||||
}
|
||||
|
||||
impl ContextManager {
|
||||
@@ -62,7 +64,7 @@ impl ContextManager {
|
||||
&None, &None, /*model_context_window*/ None,
|
||||
),
|
||||
reference_context_item: None,
|
||||
environment_context_baseline: None,
|
||||
world_state_baseline: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,15 +84,20 @@ impl ContextManager {
|
||||
self.reference_context_item.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn update_environment_context_baseline(
|
||||
pub(crate) fn update_world_state(
|
||||
&mut self,
|
||||
context: &EnvironmentContext,
|
||||
) -> bool {
|
||||
if self.environment_context_baseline.as_ref() == Some(context) {
|
||||
return false;
|
||||
}
|
||||
self.environment_context_baseline = Some(context.clone());
|
||||
true
|
||||
world_state: WorldState,
|
||||
) -> Vec<Box<dyn ContextualUserFragment>> {
|
||||
let fragments = self.world_state_baseline.as_deref().map_or_else(
|
||||
|| world_state.render_full(),
|
||||
|previous| world_state.render_diff(previous),
|
||||
);
|
||||
self.world_state_baseline = Some(Arc::new(world_state));
|
||||
fragments
|
||||
}
|
||||
|
||||
pub(crate) fn set_world_state_baseline(&mut self, world_state: WorldState) {
|
||||
self.world_state_baseline = Some(Arc::new(world_state));
|
||||
}
|
||||
|
||||
pub(crate) fn set_token_usage_full(&mut self, context_window: i64) {
|
||||
@@ -178,14 +185,14 @@ impl ContextManager {
|
||||
// its corresponding counterpart to keep the invariants intact without
|
||||
// running a full normalization pass.
|
||||
normalize::remove_corresponding_for(&mut self.items, &removed);
|
||||
self.environment_context_baseline = None;
|
||||
self.world_state_baseline = None;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn replace(&mut self, items: Vec<ResponseItem>) {
|
||||
self.items = items;
|
||||
self.history_version = self.history_version.saturating_add(1);
|
||||
self.environment_context_baseline = None;
|
||||
self.world_state_baseline = None;
|
||||
}
|
||||
|
||||
/// Replace image content in the last turn if it originated from a tool output.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use super::*;
|
||||
use crate::context::EnvironmentContext;
|
||||
use crate::context::world_state::EnvironmentsState;
|
||||
use crate::context::world_state::WorldState;
|
||||
use base64::Engine;
|
||||
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
|
||||
use codex_protocol::AgentPath;
|
||||
@@ -75,17 +76,22 @@ fn create_history_with_items(items: Vec<ResponseItem>) -> ContextManager {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn environment_context_baseline_deduplicates_until_history_is_replaced() {
|
||||
let context =
|
||||
EnvironmentContext::from_turn_context_item(&reference_context_item(), "bash".to_string());
|
||||
fn world_state_baseline_deduplicates_until_history_is_replaced() {
|
||||
let world_state = || {
|
||||
let mut state = WorldState::default();
|
||||
state.add_section(EnvironmentsState::from_turn_context_item(
|
||||
&reference_context_item(),
|
||||
));
|
||||
state
|
||||
};
|
||||
let mut history = ContextManager::new();
|
||||
|
||||
assert!(history.update_environment_context_baseline(&context));
|
||||
assert!(!history.update_environment_context_baseline(&context));
|
||||
assert_eq!(1, history.update_world_state(world_state()).len());
|
||||
assert!(history.update_world_state(world_state()).is_empty());
|
||||
|
||||
history.replace(Vec::new());
|
||||
|
||||
assert!(history.update_environment_context_baseline(&context));
|
||||
assert_eq!(1, history.update_world_state(world_state()).len());
|
||||
}
|
||||
|
||||
fn user_msg(text: &str) -> ResponseItem {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use crate::context::CollaborationModeInstructions;
|
||||
use crate::context::ContextualUserFragment;
|
||||
use crate::context::EnvironmentContext;
|
||||
use crate::context::ModelSwitchInstructions;
|
||||
use crate::context::MultiAgentModeInstructions;
|
||||
use crate::context::PermissionsInstructions;
|
||||
@@ -10,7 +9,6 @@ use crate::context::RealtimeStartInstructions;
|
||||
use crate::context::RealtimeStartWithInstructions;
|
||||
use crate::session::PreviousTurnSettings;
|
||||
use crate::session::turn_context::TurnContext;
|
||||
use crate::shell::Shell;
|
||||
use codex_execpolicy::Policy;
|
||||
use codex_features::Feature;
|
||||
use codex_protocol::config_types::MultiAgentMode;
|
||||
@@ -20,27 +18,6 @@ use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::openai_models::ModelInfo;
|
||||
use codex_protocol::protocol::TurnContextItem;
|
||||
|
||||
fn build_environment_update_item(
|
||||
previous: Option<&TurnContextItem>,
|
||||
next: &TurnContext,
|
||||
shell: &Shell,
|
||||
) -> Option<ResponseItem> {
|
||||
if !next.config.include_environment_context {
|
||||
return None;
|
||||
}
|
||||
|
||||
let prev = previous?;
|
||||
let prev_context = EnvironmentContext::from_turn_context_item(prev, shell.name().to_string());
|
||||
let next_context = EnvironmentContext::from_turn_context(next, shell);
|
||||
if prev_context.equals_except_shell(&next_context) {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(ContextualUserFragment::into(
|
||||
EnvironmentContext::diff_from_turn_context_item(prev, &next_context),
|
||||
))
|
||||
}
|
||||
|
||||
fn build_permissions_update_item(
|
||||
previous: Option<&TurnContextItem>,
|
||||
next: &TurnContext,
|
||||
@@ -220,6 +197,26 @@ pub(crate) fn build_contextual_user_message(text_sections: Vec<String>) -> Optio
|
||||
build_text_message("user", text_sections)
|
||||
}
|
||||
|
||||
pub(crate) fn merge_contextual_fragments(
|
||||
fragments: Vec<Box<dyn ContextualUserFragment>>,
|
||||
) -> Vec<ResponseItem> {
|
||||
let mut messages: Vec<(&str, Vec<String>)> = Vec::with_capacity(fragments.len());
|
||||
for fragment in fragments {
|
||||
let role = fragment.role();
|
||||
let text = fragment.render();
|
||||
match messages.last_mut() {
|
||||
Some((previous_role, text_sections)) if *previous_role == role => {
|
||||
text_sections.push(text);
|
||||
}
|
||||
_ => messages.push((role, vec![text])),
|
||||
}
|
||||
}
|
||||
messages
|
||||
.into_iter()
|
||||
.filter_map(|(role, text_sections)| build_text_message(role, text_sections))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn build_text_message(role: &str, text_sections: Vec<String>) -> Option<ResponseItem> {
|
||||
if text_sections.is_empty() {
|
||||
return None;
|
||||
@@ -243,7 +240,6 @@ pub(crate) fn build_settings_update_items(
|
||||
previous: Option<&TurnContextItem>,
|
||||
previous_turn_settings: Option<&PreviousTurnSettings>,
|
||||
next: &TurnContext,
|
||||
shell: &Shell,
|
||||
exec_policy: &Policy,
|
||||
personality_feature_enabled: bool,
|
||||
) -> Vec<ResponseItem> {
|
||||
@@ -251,7 +247,6 @@ pub(crate) fn build_settings_update_items(
|
||||
// model-visible item emitted by build_initial_context. Persist the remaining
|
||||
// inputs or add explicit replay events so fork/resume can diff everything
|
||||
// deterministically.
|
||||
let contextual_user_message = build_environment_update_item(previous, next, shell);
|
||||
let developer_update_sections = [
|
||||
// Keep model-switch instructions first so model-specific guidance is read before
|
||||
// any other context diffs on this turn.
|
||||
@@ -266,12 +261,7 @@ pub(crate) fn build_settings_update_items(
|
||||
.flatten()
|
||||
.collect();
|
||||
|
||||
let mut items = Vec::with_capacity(2);
|
||||
if let Some(developer_message) = build_developer_update_item(developer_update_sections) {
|
||||
items.push(developer_message);
|
||||
}
|
||||
if let Some(contextual_user_message) = contextual_user_message {
|
||||
items.push(contextual_user_message);
|
||||
}
|
||||
items
|
||||
build_developer_update_item(developer_update_sections)
|
||||
.into_iter()
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ use crate::context::NetworkRuleSaved;
|
||||
use crate::context::PermissionsInstructions;
|
||||
use crate::context::PersonalitySpecInstructions;
|
||||
use crate::context::RecommendedPluginsInstructions;
|
||||
use crate::context::world_state::WorldState;
|
||||
use crate::current_time::TimeProvider;
|
||||
use crate::default_skill_metadata_budget;
|
||||
use crate::environment_selection::TurnEnvironmentSnapshot;
|
||||
@@ -223,6 +224,7 @@ pub(crate) mod time_reminder;
|
||||
mod token_budget;
|
||||
pub(crate) mod turn;
|
||||
pub(crate) mod turn_context;
|
||||
mod world_state;
|
||||
use self::config_lock::export_config_lock_if_configured;
|
||||
use self::config_lock::validate_config_lock_if_configured;
|
||||
#[cfg(test)]
|
||||
@@ -243,6 +245,9 @@ use self::turn::collect_explicit_app_ids_from_skill_items;
|
||||
use self::turn::realtime_text_for_event;
|
||||
use self::turn_context::TurnContext;
|
||||
use self::turn_context::TurnSkillsContext;
|
||||
use self::world_state::build_world_state_from_environment_snapshot;
|
||||
use self::world_state::build_world_state_from_turn_context;
|
||||
use self::world_state::build_world_state_from_turn_context_item;
|
||||
#[cfg(test)]
|
||||
mod rollout_reconstruction_tests;
|
||||
|
||||
@@ -1377,9 +1382,15 @@ impl Session {
|
||||
// will be processed again if the rollout is reconstructed in a future session.
|
||||
// This meets image resizing requirements without modifying persisted rollouts.
|
||||
prepare_response_items(&mut history);
|
||||
let world_state_baseline = reference_context_item
|
||||
.as_ref()
|
||||
.map(build_world_state_from_turn_context_item);
|
||||
{
|
||||
let mut state = self.state.lock().await;
|
||||
state.replace_history(history, reference_context_item);
|
||||
if let Some(world_state) = world_state_baseline {
|
||||
state.history.set_world_state_baseline(world_state);
|
||||
}
|
||||
let fallback_ids = state.auto_compact_window_ids();
|
||||
let window_id = window_id.unwrap_or(fallback_ids.window_id);
|
||||
state.restore_auto_compact_window(
|
||||
@@ -1674,19 +1685,17 @@ impl Session {
|
||||
) -> Vec<ResponseItem> {
|
||||
// TODO: Make context updates a pure diff of persisted previous/current TurnContextItem
|
||||
// state so replay/backtracking is deterministic. Runtime inputs that affect model-visible
|
||||
// context (shell, exec policy, feature gates, previous-turn bridge) should be persisted
|
||||
// context (exec policy, feature gates, previous-turn bridge) should be persisted
|
||||
// state or explicit non-state replay events.
|
||||
let previous_turn_settings = {
|
||||
let state = self.state.lock().await;
|
||||
state.previous_turn_settings()
|
||||
};
|
||||
let shell = self.user_shell();
|
||||
let exec_policy = self.services.exec_policy.current();
|
||||
crate::context_manager::updates::build_settings_update_items(
|
||||
reference_context_item,
|
||||
previous_turn_settings.as_ref(),
|
||||
current_context,
|
||||
shell.as_ref(),
|
||||
exec_policy.as_ref(),
|
||||
self.features.enabled(Feature::Personality),
|
||||
)
|
||||
@@ -2757,24 +2766,19 @@ impl Session {
|
||||
return;
|
||||
}
|
||||
|
||||
let shell = self.user_shell();
|
||||
let Some(environment_context) =
|
||||
crate::context::EnvironmentContext::from_step_context(step_context, shell.as_ref())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let changed = {
|
||||
let world_state =
|
||||
build_world_state_from_environment_snapshot(turn_context, &step_context.environments);
|
||||
let items = {
|
||||
let mut state = self.state.lock().await;
|
||||
state
|
||||
.history
|
||||
.update_environment_context_baseline(&environment_context)
|
||||
crate::context_manager::updates::merge_contextual_fragments(
|
||||
state.history.update_world_state(world_state),
|
||||
)
|
||||
};
|
||||
if !changed {
|
||||
if items.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let item = ContextualUserFragment::into(environment_context);
|
||||
self.record_conversation_items(turn_context, &[item]).await;
|
||||
self.record_conversation_items(turn_context, &items).await;
|
||||
}
|
||||
|
||||
pub(crate) async fn record_inter_agent_communication(
|
||||
@@ -2886,9 +2890,17 @@ impl Session {
|
||||
replacement_history: Some(items.clone()),
|
||||
..compacted_item
|
||||
};
|
||||
let world_state_baseline = if reference_context_item.is_some() {
|
||||
Some(self.build_world_state(turn_context).await)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
{
|
||||
let mut state = self.state.lock().await;
|
||||
state.replace_history(items, reference_context_item.clone());
|
||||
if let Some(world_state) = world_state_baseline {
|
||||
state.history.set_world_state_baseline(world_state);
|
||||
}
|
||||
}
|
||||
|
||||
self.persist_rollout_items(&[RolloutItem::Compacted(compacted_item)])
|
||||
@@ -3017,6 +3029,28 @@ impl Session {
|
||||
pub(crate) async fn build_initial_context(
|
||||
&self,
|
||||
turn_context: &TurnContext,
|
||||
) -> Vec<ResponseItem> {
|
||||
let world_state = self.build_world_state(turn_context).await;
|
||||
self.build_initial_context_with_world_state(turn_context, &world_state)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn build_world_state(&self, turn_context: &TurnContext) -> WorldState {
|
||||
let environment_subagents = if turn_context.config.include_environment_context {
|
||||
self.services
|
||||
.agent_control
|
||||
.format_environment_context_subagents(self.thread_id)
|
||||
.await
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
build_world_state_from_turn_context(turn_context, &environment_subagents)
|
||||
}
|
||||
|
||||
async fn build_initial_context_with_world_state(
|
||||
&self,
|
||||
turn_context: &TurnContext,
|
||||
world_state: &WorldState,
|
||||
) -> Vec<ResponseItem> {
|
||||
let mut developer_sections = Vec::<String>::with_capacity(8);
|
||||
let mut contextual_user_sections = Vec::<String>::with_capacity(2);
|
||||
@@ -3257,18 +3291,12 @@ impl Session {
|
||||
.render(),
|
||||
);
|
||||
}
|
||||
if turn_context.config.include_environment_context {
|
||||
let shell = self.user_shell();
|
||||
let subagents = self
|
||||
.services
|
||||
.agent_control
|
||||
.format_environment_context_subagents(self.thread_id)
|
||||
.await;
|
||||
contextual_user_sections.push(
|
||||
crate::context::EnvironmentContext::from_turn_context(turn_context, shell.as_ref())
|
||||
.with_subagents(subagents)
|
||||
.render(),
|
||||
);
|
||||
for fragment in world_state.render_full() {
|
||||
match fragment.role() {
|
||||
"developer" => developer_sections.push(fragment.render()),
|
||||
"user" => contextual_user_sections.push(fragment.render()),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let multi_agent_v2_usage_hint_text =
|
||||
@@ -3370,12 +3398,16 @@ impl Session {
|
||||
state.start_new_context_window_if_requested()
|
||||
};
|
||||
let (window_number, window_ids) = window?;
|
||||
let context_items = self.build_initial_context(turn_context).await;
|
||||
let world_state = self.build_world_state(turn_context).await;
|
||||
let context_items = self
|
||||
.build_initial_context_with_world_state(turn_context, &world_state)
|
||||
.await;
|
||||
let turn_context_item = turn_context.to_turn_context_item();
|
||||
let replacement_history = context_items;
|
||||
{
|
||||
let mut state = self.state.lock().await;
|
||||
state.replace_history(replacement_history.clone(), Some(turn_context_item.clone()));
|
||||
state.history.set_world_state_baseline(world_state);
|
||||
};
|
||||
self.persist_rollout_items(&[
|
||||
RolloutItem::Compacted(CompactedItem {
|
||||
@@ -3406,15 +3438,15 @@ impl Session {
|
||||
/// steady-state turns that emit model-visible context updates.
|
||||
///
|
||||
/// When the reference snapshot is missing, this injects full initial context. Otherwise, it
|
||||
/// emits only settings diff items.
|
||||
/// emits only context diffs.
|
||||
///
|
||||
/// If full context is injected and a model switch occurred, this prepends the
|
||||
/// `<model_switch>` developer message so model-specific instructions are not lost.
|
||||
///
|
||||
/// This is the normal runtime path that establishes a new `reference_context_item`.
|
||||
/// Mid-turn compaction is the other path that can re-establish that baseline when it
|
||||
/// reinjects full initial context into replacement history. Other non-regular tasks
|
||||
/// intentionally do not update the baseline.
|
||||
/// Mid-turn compaction is the other path that can re-establish that reference when it
|
||||
/// reinjects full initial context into replacement history. Live world-state changes may
|
||||
/// independently advance their in-memory baseline within a turn.
|
||||
#[instrument(level = "trace", skip_all)]
|
||||
pub(crate) async fn record_context_updates_and_set_reference_context_item(
|
||||
&self,
|
||||
@@ -3425,40 +3457,43 @@ impl Session {
|
||||
state.reference_context_item()
|
||||
};
|
||||
let turn_context_item = turn_context.to_turn_context_item();
|
||||
if reference_context_item.as_ref() == Some(&turn_context_item) {
|
||||
return;
|
||||
}
|
||||
let turn_context_changed = reference_context_item.as_ref() != Some(&turn_context_item);
|
||||
let should_inject_full_context = reference_context_item.is_none();
|
||||
let world_state = self.build_world_state(turn_context).await;
|
||||
let mut context_items = if should_inject_full_context {
|
||||
self.build_initial_context(turn_context).await
|
||||
let context_items = self
|
||||
.build_initial_context_with_world_state(turn_context, &world_state)
|
||||
.await;
|
||||
self.state
|
||||
.lock()
|
||||
.await
|
||||
.history
|
||||
.set_world_state_baseline(world_state);
|
||||
context_items
|
||||
} else {
|
||||
// Steady-state path: append only built-in context diffs here; turn-scoped extension
|
||||
// context is added below.
|
||||
self.build_settings_update_items(reference_context_item.as_ref(), turn_context)
|
||||
.await
|
||||
let mut context_items = self
|
||||
.build_settings_update_items(reference_context_item.as_ref(), turn_context)
|
||||
.await;
|
||||
let world_state_items = {
|
||||
let mut state = self.state.lock().await;
|
||||
crate::context_manager::updates::merge_contextual_fragments(
|
||||
state.history.update_world_state(world_state),
|
||||
)
|
||||
};
|
||||
context_items.extend(world_state_items);
|
||||
context_items
|
||||
};
|
||||
if !should_inject_full_context {
|
||||
if !should_inject_full_context && turn_context_changed {
|
||||
context_items.extend(
|
||||
self.build_turn_context_contribution_items(turn_context)
|
||||
.await,
|
||||
);
|
||||
}
|
||||
let initial_environment_context = if should_inject_full_context
|
||||
&& !context_items.is_empty()
|
||||
&& turn_context.config.include_environment_context
|
||||
&& turn_context
|
||||
.config
|
||||
.features
|
||||
.enabled(Feature::DeferredExecutor)
|
||||
{
|
||||
let shell = self.user_shell();
|
||||
crate::context::EnvironmentContext::from_attached_environments(
|
||||
&turn_context.environments.turn_environments,
|
||||
shell.as_ref(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if !turn_context_changed && context_items.is_empty() {
|
||||
return;
|
||||
}
|
||||
if !context_items.is_empty() {
|
||||
self.record_conversation_items(turn_context, &context_items)
|
||||
.await;
|
||||
@@ -3468,15 +3503,10 @@ impl Session {
|
||||
self.persist_rollout_items(&[RolloutItem::TurnContext(turn_context_item.clone())])
|
||||
.await;
|
||||
|
||||
// Advance the in-memory diff baseline even when this turn emitted no model-visible
|
||||
// context items. This keeps later runtime diffing aligned with the current turn state.
|
||||
// Advance the persisted-settings baseline even when this turn emitted no model-visible
|
||||
// context items.
|
||||
let mut state = self.state.lock().await;
|
||||
state.set_reference_context_item(Some(turn_context_item));
|
||||
if let Some(environment_context) = initial_environment_context {
|
||||
state
|
||||
.history
|
||||
.update_environment_context_baseline(&environment_context);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn update_token_usage_info(
|
||||
|
||||
@@ -7349,7 +7349,7 @@ async fn spawn_task_does_not_update_previous_turn_settings_for_non_run_turn_task
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn build_settings_update_items_emits_environment_item_for_network_changes() {
|
||||
async fn record_context_updates_emits_environment_item_for_network_changes() {
|
||||
let (session, previous_context) = make_session_and_context().await;
|
||||
let previous_context = Arc::new(previous_context);
|
||||
let mut current_context = previous_context
|
||||
@@ -7396,10 +7396,8 @@ async fn build_settings_update_items_emits_environment_item_for_network_changes(
|
||||
.expect("rebuild config layer stack with network requirements");
|
||||
current_context.config = Arc::new(config);
|
||||
|
||||
let reference_context_item = previous_context.to_turn_context_item();
|
||||
let update_items = session
|
||||
.build_settings_update_items(Some(&reference_context_item), ¤t_context)
|
||||
.await;
|
||||
let update_items =
|
||||
record_context_update_items(&session, &previous_context, ¤t_context).await;
|
||||
|
||||
let environment_update = user_input_texts(&update_items)
|
||||
.into_iter()
|
||||
@@ -7411,50 +7409,40 @@ async fn build_settings_update_items_emits_environment_item_for_network_changes(
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn environment_context_uses_session_shell_when_environment_shell_is_absent() {
|
||||
let (mut session, mut turn_context) = make_session_and_context().await;
|
||||
session.services.user_shell = Arc::new(crate::shell::Shell {
|
||||
shell_type: crate::shell::ShellType::PowerShell,
|
||||
shell_path: PathBuf::from("powershell"),
|
||||
});
|
||||
for environment in &mut turn_context.environments.turn_environments {
|
||||
environment.shell = None;
|
||||
}
|
||||
|
||||
let session_shell = session.user_shell();
|
||||
let environment_context = crate::context::EnvironmentContext::from_turn_context(
|
||||
&turn_context,
|
||||
session_shell.as_ref(),
|
||||
)
|
||||
.render();
|
||||
assert!(
|
||||
environment_context.contains("<shell>powershell</shell>"),
|
||||
"{environment_context}"
|
||||
async fn record_context_updates_emits_environment_item_for_cwd_changes() {
|
||||
let (session, previous_context) = make_session_and_context().await;
|
||||
let previous_context = Arc::new(previous_context);
|
||||
let mut current_context = previous_context
|
||||
.with_model(
|
||||
previous_context.model_info.slug.clone(),
|
||||
&session.services.models_manager,
|
||||
)
|
||||
.await;
|
||||
let cwd = test_path_buf("/new-repo").abs();
|
||||
let environment = current_context.environments.turn_environments[0].clone();
|
||||
current_context.environments.turn_environments[0] = TurnEnvironment::new(
|
||||
environment.environment_id,
|
||||
environment.environment,
|
||||
PathUri::from_abs_path(&cwd),
|
||||
environment.shell,
|
||||
);
|
||||
|
||||
let primary_environment = turn_context
|
||||
.environments
|
||||
.turn_environments
|
||||
.first_mut()
|
||||
.expect("primary environment");
|
||||
primary_environment.shell = Some(crate::shell::Shell {
|
||||
shell_type: crate::shell::ShellType::Cmd,
|
||||
shell_path: PathBuf::from("cmd"),
|
||||
});
|
||||
let update_items =
|
||||
record_context_update_items(&session, &previous_context, ¤t_context).await;
|
||||
|
||||
let environment_context = crate::context::EnvironmentContext::from_turn_context(
|
||||
&turn_context,
|
||||
session_shell.as_ref(),
|
||||
)
|
||||
.render();
|
||||
let environment_update = user_input_texts(&update_items)
|
||||
.into_iter()
|
||||
.find(|text| text.contains("<environment_context>"))
|
||||
.expect("environment update item should be emitted");
|
||||
assert!(
|
||||
environment_context.contains("<shell>cmd</shell>"),
|
||||
"{environment_context}"
|
||||
environment_update.contains(&format!("<cwd>{}</cwd>", cwd.display())),
|
||||
"{environment_update}"
|
||||
);
|
||||
assert!(!environment_update.contains("<environments>"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn build_settings_update_items_emits_environment_item_for_time_changes() {
|
||||
async fn record_context_updates_emits_environment_item_for_time_changes() {
|
||||
let (session, previous_context) = make_session_and_context().await;
|
||||
let previous_context = Arc::new(previous_context);
|
||||
let mut current_context = previous_context
|
||||
@@ -7466,10 +7454,8 @@ async fn build_settings_update_items_emits_environment_item_for_time_changes() {
|
||||
current_context.current_date = Some("2026-02-27".to_string());
|
||||
current_context.timezone = Some("Europe/Berlin".to_string());
|
||||
|
||||
let reference_context_item = previous_context.to_turn_context_item();
|
||||
let update_items = session
|
||||
.build_settings_update_items(Some(&reference_context_item), ¤t_context)
|
||||
.await;
|
||||
let update_items =
|
||||
record_context_update_items(&session, &previous_context, ¤t_context).await;
|
||||
|
||||
let environment_update = user_input_texts(&update_items)
|
||||
.into_iter()
|
||||
@@ -7480,7 +7466,7 @@ async fn build_settings_update_items_emits_environment_item_for_time_changes() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn build_settings_update_items_omits_environment_item_when_disabled() {
|
||||
async fn record_context_updates_omits_environment_item_when_disabled() {
|
||||
let (session, previous_context) = make_session_and_context().await;
|
||||
let previous_context = Arc::new(previous_context);
|
||||
let mut current_context = previous_context
|
||||
@@ -7492,12 +7478,16 @@ async fn build_settings_update_items_omits_environment_item_when_disabled() {
|
||||
let mut config = (*current_context.config).clone();
|
||||
config.include_environment_context = false;
|
||||
current_context.config = Arc::new(config);
|
||||
current_context.current_date = Some("2026-02-27".to_string());
|
||||
let environment = current_context.environments.turn_environments[0].clone();
|
||||
current_context.environments.turn_environments[0] = TurnEnvironment::new(
|
||||
environment.environment_id,
|
||||
environment.environment,
|
||||
PathUri::from_abs_path(&test_path_buf("/new-repo").abs()),
|
||||
environment.shell,
|
||||
);
|
||||
|
||||
let reference_context_item = previous_context.to_turn_context_item();
|
||||
let update_items = session
|
||||
.build_settings_update_items(Some(&reference_context_item), ¤t_context)
|
||||
.await;
|
||||
let update_items =
|
||||
record_context_update_items(&session, &previous_context, ¤t_context).await;
|
||||
|
||||
let user_texts = user_input_texts(&update_items);
|
||||
assert!(
|
||||
@@ -7508,6 +7498,23 @@ async fn build_settings_update_items_omits_environment_item_when_disabled() {
|
||||
);
|
||||
}
|
||||
|
||||
async fn record_context_update_items(
|
||||
session: &Session,
|
||||
previous_context: &TurnContext,
|
||||
current_context: &TurnContext,
|
||||
) -> Vec<ResponseItem> {
|
||||
session
|
||||
.record_context_updates_and_set_reference_context_item(previous_context)
|
||||
.await;
|
||||
let previous_len = session.clone_history().await.raw_items().len();
|
||||
|
||||
session
|
||||
.record_context_updates_and_set_reference_context_item(current_context)
|
||||
.await;
|
||||
let history = session.clone_history().await;
|
||||
history.raw_items()[previous_len..].to_vec()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn build_settings_update_items_emits_realtime_start_when_session_becomes_live() {
|
||||
let (session, previous_context) = make_session_and_context().await;
|
||||
@@ -7769,9 +7776,11 @@ async fn record_context_updates_includes_turn_context_fragments_on_steady_state_
|
||||
});
|
||||
let mut previous_context_item = turn_context.to_turn_context_item();
|
||||
previous_context_item.turn_id = Some("previous-turn-id".to_string());
|
||||
let world_state = session.build_world_state(&turn_context).await;
|
||||
{
|
||||
let mut state = session.state.lock().await;
|
||||
state.set_reference_context_item(Some(previous_context_item));
|
||||
state.history.set_world_state_baseline(world_state);
|
||||
}
|
||||
|
||||
session
|
||||
@@ -8441,9 +8450,11 @@ async fn record_context_updates_and_set_reference_context_item_persists_baseline
|
||||
.with_model(next_model.to_string(), &session.services.models_manager)
|
||||
.await;
|
||||
let previous_context_item = previous_context.to_turn_context_item();
|
||||
let world_state = session.build_world_state(&previous_context).await;
|
||||
{
|
||||
let mut state = session.state.lock().await;
|
||||
state.set_reference_context_item(Some(previous_context_item.clone()));
|
||||
state.history.set_world_state_baseline(world_state);
|
||||
}
|
||||
let rollout_path = attach_thread_persistence(&mut session).await;
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
use super::turn_context::TurnContext;
|
||||
use crate::context::world_state::EnvironmentsState;
|
||||
use crate::context::world_state::WorldState;
|
||||
use crate::environment_selection::TurnEnvironmentSnapshot;
|
||||
use codex_protocol::protocol::TurnContextItem;
|
||||
|
||||
pub(super) fn build_world_state_from_turn_context(
|
||||
turn_context: &TurnContext,
|
||||
environment_subagents: &str,
|
||||
) -> WorldState {
|
||||
let mut world_state = WorldState::default();
|
||||
if turn_context.config.include_environment_context {
|
||||
world_state.add_section(
|
||||
EnvironmentsState::from_turn_context(turn_context)
|
||||
.with_subagents(environment_subagents.to_string()),
|
||||
);
|
||||
}
|
||||
world_state
|
||||
}
|
||||
|
||||
pub(super) fn build_world_state_from_environment_snapshot(
|
||||
turn_context: &TurnContext,
|
||||
environments: &TurnEnvironmentSnapshot,
|
||||
) -> WorldState {
|
||||
let mut world_state = WorldState::default();
|
||||
if turn_context.config.include_environment_context {
|
||||
world_state.add_section(EnvironmentsState::from_turn_context_with_environments(
|
||||
turn_context,
|
||||
environments,
|
||||
));
|
||||
}
|
||||
world_state
|
||||
}
|
||||
|
||||
pub(super) fn build_world_state_from_turn_context_item(
|
||||
turn_context_item: &TurnContextItem,
|
||||
) -> WorldState {
|
||||
let mut world_state = WorldState::default();
|
||||
world_state.add_section(EnvironmentsState::from_turn_context_item(turn_context_item));
|
||||
world_state
|
||||
}
|
||||
Reference in New Issue
Block a user