diff --git a/codex-rs/core/src/compact_tests.rs b/codex-rs/core/src/compact_tests.rs index cc80b0479b..790f0d7362 100644 --- a/codex-rs/core/src/compact_tests.rs +++ b/codex-rs/core/src/compact_tests.rs @@ -271,6 +271,55 @@ fn compacted_user_message<'a>(text: &str, original: &'a ResponseItem) -> Compact } } +#[tokio::test] +async fn extension_window_prefix_precedes_retained_turns_and_is_not_repeated() { + use codex_extension_api::PreviousWorldStateSection; + use codex_extension_api::RenderedWorldStateFragment; + use codex_extension_api::WorldStateSectionContribution; + + let (session, turn) = crate::session::tests::make_session_and_context().await; + let step = StepContext::for_test(Arc::new(turn)); + let mut world = WorldState::default(); + world.add_extension_section( + WorldStateSectionContribution::new("example", |previous| { + let fragment = (!matches!(previous, PreviousWorldStateSection::Known(_))).then(|| { + RenderedWorldStateFragment::new( + "developer", + ("", ""), + codex_protocol::models::input_text("active policy"), + ) + }); + (Some(json!("active policy")), fragment) + }) + .with_window_prefix() + .with_retained_fragment_matcher(|role, text| { + role == "developer" && text == "active policy" + }), + ); + let retained = vec![user_message("earlier turn"), user_message("latest turn")]; + let (replacement, snapshot) = + build_compaction_replacement_history(&session, &step, &world, annotated(retained.clone())) + .await; + let items = raw(replacement); + let ResponseItem::Message { role, content, .. } = &items[0] else { + panic!("expected a separate developer prefix"); + }; + assert_eq!(role, "developer"); + assert_eq!( + content, + &[codex_protocol::models::input_text( + "active policy" + )] + ); + assert_eq!(items[1], retained[0]); + assert!( + world + .render_history_diff(Some(&snapshot), &items) + .1 + .is_empty() + ); +} + #[test] fn content_items_to_text_joins_non_empty_segments() { let items = vec![ diff --git a/codex-rs/core/src/context/world_state/mod.rs b/codex-rs/core/src/context/world_state/mod.rs index 6724051889..49c6e9ed50 100644 --- a/codex-rs/core/src/context/world_state/mod.rs +++ b/codex-rs/core/src/context/world_state/mod.rs @@ -247,11 +247,20 @@ impl ErasedWorldStateSection for ExtensionWorldStateSection { }); let updates = fragment .into_iter() + .filter(|fragment| !self.0.is_window_prefix() || fragment.role() == "developer") .map(|fragment| { - WorldStateUpdate::fragment(WorldStateContextFragment { + let update = WorldStateUpdate::fragment(WorldStateContextFragment { fragment, content_kind: ContentItemKind(format!("{}.instructions", self.0.id())), - }) + }); + if self.0.is_window_prefix() { + WorldStateUpdate { + placement: Placement::Prefix, + ..update + } + } else { + update + } }) .collect(); (snapshot, updates) diff --git a/codex-rs/core/tests/suite/scenarios.rs b/codex-rs/core/tests/suite/scenarios.rs index 77e63a308a..deccca2eba 100644 --- a/codex-rs/core/tests/suite/scenarios.rs +++ b/codex-rs/core/tests/suite/scenarios.rs @@ -197,6 +197,9 @@ mod frozen_cloud_skills; #[path = "scenarios_compaction_tests.rs"] mod compaction; +#[path = "scenarios_world_state_prefix_tests.rs"] +mod world_state_prefix; + fn skills_extensions() -> Arc> { let mut extensions = ExtensionRegistryBuilder::::new(); install(&mut extensions, |config: &Config| SkillsExtensionConfig { diff --git a/codex-rs/core/tests/suite/scenarios_world_state_prefix_tests.rs b/codex-rs/core/tests/suite/scenarios_world_state_prefix_tests.rs new file mode 100644 index 0000000000..e1b9418167 --- /dev/null +++ b/codex-rs/core/tests/suite/scenarios_world_state_prefix_tests.rs @@ -0,0 +1,210 @@ +//! Exercises extension prefixes through live updates, compaction, and persisted history. + +use super::*; +use codex_extension_api::ContextContributor; +use codex_extension_api::ExtensionFuture; +use codex_extension_api::PreviousWorldStateSection; +use codex_extension_api::RenderedWorldStateFragment; +use codex_extension_api::WorldStateContributionInput; +use codex_extension_api::WorldStateSectionContribution; +use codex_protocol::models::input_text; +use pretty_assertions::assert_eq; +use std::sync::Mutex; + +struct PrefixInstructions { + current: Mutex<&'static str>, +} + +impl ContextContributor for PrefixInstructions { + fn contribute_world_state<'a>( + &'a self, + _input: WorldStateContributionInput<'a>, + ) -> ExtensionFuture<'a, Vec> { + let current = *self.current.lock().expect("current instructions"); + Box::pin(async move { + [ + ("scenario_prefix", "developer"), + ("rejected_user_prefix", "user"), + ("rejected_assistant_prefix", "assistant"), + ] + .into_iter() + .map(|(id, role)| { + let body = format!("{id}: {current}"); + let rendered = format!("{body}"); + WorldStateSectionContribution::new(id, move |previous| { + let snapshot = json!(body); + let unchanged = matches!(previous, PreviousWorldStateSection::Known(previous) if previous == &snapshot); + ( + Some(snapshot), + (!unchanged).then(|| { + RenderedWorldStateFragment::new( + role, + ("", ""), + input_text(body.clone()), + ) + }), + ) + }) + .with_window_prefix() + .with_retained_fragment_matcher(move |candidate_role, text| { + candidate_role == role && text == rendered + }) + }) + .collect() + }) + } +} + +#[test_case::test_case("local"; "local")] +#[test_case::test_case("remote"; "remote")] +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn extension_window_prefix_survives_compaction(compaction: &str) -> Result<()> { + skip_if_no_network!(Ok(())); + + let server = start_mock_server().await; + let compact_response = match compaction { + "local" => ev_assistant_message("summary", "The user is planning a release."), + "remote" => json!({ + "type": "response.output_item.done", + "item": {"type": "compaction", "encrypted_content": "PREFIX_CHECKPOINT"}, + }), + _ => unreachable!(), + }; + let mock = mount_sse_sequence( + &server, + vec![ + sse(vec![ + ev_assistant_message("first", "Plan ready."), + ev_completed("first-response"), + ]), + sse(vec![ + ev_assistant_message("second", "Plan checked."), + ev_completed("second-response"), + ]), + sse(vec![ + ev_assistant_message("third", "Plan updated."), + ev_completed("third-response"), + ]), + sse(vec![compact_response, ev_completed("compact-response")]), + sse(vec![ + ev_assistant_message("continue", "Continue the release."), + ev_completed("continue-response"), + ]), + ], + ) + .await; + let instructions = Arc::new(PrefixInstructions { + current: Mutex::new("A"), + }); + let mut extensions = ExtensionRegistryBuilder::new(); + extensions.prompt_contributor(instructions.clone()); + let local_compaction = compaction == "local"; + let test = test_codex() + .with_extensions(Arc::new(extensions.build())) + .with_model("gpt-6-astra") + .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) + .with_config(move |config| { + configure_scenario_catalog(config); + config.workspace_roots = vec![config.cwd.clone()]; + if local_compaction { + config.model_provider.name = "Local compaction test".to_string(); + } + }) + .build_with_auto_env(&server) + .await?; + + test.submit_turn("Plan the release.").await?; + test.submit_turn("Check the plan.").await?; + *instructions.current.lock().expect("current instructions") = "B"; + test.submit_turn("Update the plan.").await?; + // Capture a new policy during compaction, not only one already rendered in history. + *instructions.current.lock().expect("current instructions") = "C"; + test.codex.submit(Op::Compact).await?; + wait_for_event(&test.codex, |event| { + assert!(!matches!(event, EventMsg::Error(_)), "{event:?}"); + matches!(event, EventMsg::TurnComplete(_)) + }) + .await; + + test.codex.flush_rollout().await?; + let rollout = fs::read_to_string(test.codex.rollout_path().expect("rollout path"))?; + let replacement = rollout + .lines() + .map(codex_rollout::parse_rollout_line) + .collect::, _>>()? + .into_iter() + .find_map(|line| match line.item { + RolloutItem::Compacted(compacted) => compacted.replacement_history, + _ => None, + }) + .expect("persisted compaction replacement") + .into_iter() + .map(|envelope| serde_json::to_value(envelope.item)) + .collect::, _>>()?; + + test.submit_turn("Continue with the release.").await?; + let requests = mock.requests(); + assert_eq!(requests.len(), 5); + let prefix_texts = |items: &[serde_json::Value]| { + items + .iter() + .flat_map(|item| item["content"].as_array().into_iter().flatten()) + .filter_map(|content| content["text"].as_str()) + .filter(|text| text.starts_with("")) + .map(str::to_owned) + .collect::>() + }; + let prefix_a = "scenario_prefix: A".to_string(); + let prefix_b = "scenario_prefix: B".to_string(); + let prefix_c = "scenario_prefix: C".to_string(); + assert_eq!(prefix_texts(&requests[0].input()), vec![prefix_a.clone()]); + assert_eq!(prefix_texts(&requests[1].input()), vec![prefix_a.clone()]); + assert_eq!(prefix_texts(&requests[2].input()), vec![prefix_a, prefix_b]); + assert_eq!(prefix_texts(&replacement), vec![prefix_c.clone()]); + assert_eq!(prefix_texts(&requests[4].input()), vec![prefix_c]); + // Updating the policy preserves the entire old request prefix byte-for-byte. + let unchanged_input = requests[1].input(); + assert_eq!( + &requests[2].input()[..unchanged_input.len()], + unchanged_input + ); + for items in [ + requests[0].input(), + replacement.clone(), + requests[4].input(), + ] { + let prefix_index = items + .iter() + .position(|item| !prefix_texts(std::slice::from_ref(item)).is_empty()) + .expect("prefix"); + let user_index = items + .iter() + .position(|item| item["role"] == "user") + .expect("user context"); + assert!(prefix_index < user_index); + } + for request in &requests { + let body = request.body_json().to_string(); + assert!(!body.contains("rejected_user_prefix")); + assert!(!body.contains("rejected_assistant_prefix")); + } + + insta::assert_snapshot!( + format!("extension_window_prefix_{compaction}"), + context_snapshot::format_context_snapshot( + "An extension supplies A, stays unchanged, appends B, then supplies C during compaction. Only C starts the new window. Non-developer prefix fragments are never rendered.", + &[ + SnapshotEntry::captured(&requests[0]).labeled("Initial policy A"), + SnapshotEntry::captured(&requests[1]).labeled("Unchanged policy A"), + SnapshotEntry::captured(&requests[2]) + .labeled("Policy B appends to existing history"), + SnapshotEntry::captured(&requests[3]).labeled("Compaction request"), + SnapshotEntry::items(&replacement).labeled("Stored replacement before next turn"), + SnapshotEntry::captured(&requests[4]) + .labeled("Unchanged policy C after compaction"), + ], + &ContextSnapshotOptions::default().include_request_settings(), + ) + ); + Ok(()) +} diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__world_state_prefix__extension_window_prefix_local.snap b/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__world_state_prefix__extension_window_prefix_local.snap new file mode 100644 index 0000000000..102fe57c7c --- /dev/null +++ b/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__world_state_prefix__extension_window_prefix_local.snap @@ -0,0 +1,326 @@ +--- +source: core/tests/suite/scenarios_world_state_prefix_tests.rs +expression: "context_snapshot::format_context_snapshot(\"An extension supplies A, stays unchanged, appends B, then supplies C during compaction. Only C starts the new window. Non-developer prefix fragments are never rendered.\",\n&[SnapshotEntry::captured(&requests[0]).labeled(\"Initial policy A\"),\nSnapshotEntry::captured(&requests[1]).labeled(\"Unchanged policy A\"),\nSnapshotEntry::captured(&requests[2]).labeled(\"Policy B appends to existing history\"),\nSnapshotEntry::captured(&requests[3]).labeled(\"Compaction request\"),\nSnapshotEntry::items(&replacement).labeled(\"Stored replacement before next turn\"),\nSnapshotEntry::captured(&requests[4]).labeled(\"Unchanged policy C after compaction\"),],\n&ContextSnapshotOptions::default().include_request_settings(),)" +--- +Scenario: An extension supplies A, stays unchanged, appends B, then supplies C during compaction. Only C starts the new window. Non-developer prefix fragments are never rendered. + +## Window 1 +Settings: + include: ["reasoning.encrypted_content"] + model: "gpt-6-astra" + parallel_tool_calls: false + prompt_cache_key: "" + reasoning: {"context":"all_turns","effort":"low"} + store: false + stream: true + text: {"verbosity":"low"} + tool_choice: "auto" +-- request 1 (turn; Initial policy A) -- +00:additional_tools/developer (3; hash=bf2e30605ff5dc17): + - namespace/functions; hash=fada4a476a3754dd + - custom/exec + - function/wait + - function/request_user_input + - function/request_user_input_async + - namespace/clock: Tools for reading and waiting on time.; hash=f402e8c5e9b5e317 + - function/sleep + - namespace/collaboration: Tools for spawning and managing sub-agents.; hash=2c8d6295978dbe63 + - function/followup_task + - function/interrupt_agent + - function/list_agents + - function/send_message + - function/spawn_agent + - function/wait_agent +01:message/developer: + You are Codex, an agent based on GPT-6. You and the user share one workspace, and your job is to collabo...them until their intended goal is completely handled. [hash=efa50f074efa5803] + + # When to ask the user for permission + + Use your best judgement given task context for when you really need user permission, like a competent co...ork without ending the turn to clarify with the user. [hash=ca7367e658b51388] + + User authorization and preferences persist across turns. Do not request permission again when the user h... any guidelines provided in skills or external files. [hash=911f91c92c72882c] + + + + - Skill naming: If a plugin contributes skills, those skill entries are prefixed with plugin_name: in the Skills list. + - MCP naming: Plugin-provided MCP tools keep standard MCP identifiers such as mcp__server__tool; use tool provenance to tell which plugin they come from. + - Trigger rules: If the user explicitly names a plugin, prefer capabilities associated with that plugin for that turn. + - Relationship to capabilities: Plugins are not invoked directly. Use their underlying skills, MCP tools, and app tools to help solve the task. + - Relevance: Determine what a plugin can help with from explicit user mention or from the plugin-associa..., MCP tools, and apps exposed elsewhere in this turn. [hash=620459a4e7b590f0] + - Missing/blocked: If the user requests a plugin that does not have relevant callable capabilities for t..., say so briefly and continue with the best fallback. [hash=746b0469a8f6cb3e] + +02:message/developer: + scenario_prefix: A +03:message/developer[2]: + [01] + Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `danger-full-access`...ll commands are permitted. Network access is enabled. [hash=c134061cebb94a1b] + Approval policy is currently never. Do not provide the `sandbox_permissions` for any reason, commands will be rejected. + + [02] # Collaboration Mode: Default + + You are now in Default mode. Any previous instructions for other modes (e.g. Plan mode) are no longer active. + + Your active mode changes only when new developer instructions with a different `...<...by themselves. Known mode names are Default and Plan. [hash=e75f6b12fc52d812] + + ## request_user_input availability + + Use the `request_user_input` tool only when it is listed in the available tools for this turn. + + Use the `request_user_input` tool only for optional questions where the answer would materially improve the quality of the work. + + If `request_user_input` returns no answers, continue with best judgment instead of asking again or treating the turn as blocked. + + Never use the `request_user_input` tool for permission requests or permission-related escalations. + +04:message/developer: + 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 an...message to a running agent without triggering a turn. [hash=7d40ff8b58977202] + `send_message` calls may be read by a human, so ensure they are legible. Always put proper spaces between words and/or numbers. + + - All agents use the same current working directory. + - As a result, edits made by one agent are immediately visible to all other agents. + + When calling `wait_agent`, prefer longer waits (minutes) to avoid busy polling. + + There are 4 available concurrency slots, meaning that up to 4 agents can be active at once, including you. + + Full-history forks (`fork_turns` omitted or `"all"`) inherit the parent model and reasoning effort and d... so, set `fork_turns` to `"none"`. [hash=d5928243dc643bcc] +05:message/developer: + Any earlier instruction enabling proactive multi-agent delegation no longer applies. D...elegation, or parallel agent work. [hash=2b6e037067b4390c] +06:message/user: + + + + + + <...e="unrestricted" /> [hash=d40b2f45cb028888] + +07:message/user: + Plan the release. +-- request 2 (turn; Unchanged policy A) -- +08:message/assistant: + Plan ready. +09:message/user: + Check the plan. +-- request 3 (turn; Policy B appends to existing history) -- +10:message/assistant: + Plan checked. +11:message/developer: + scenario_prefix: B +12:message/user: + Update the plan. +-- request 4 (compaction; Compaction request) -- +13:message/assistant: + Plan updated. +14:message/user: + You are performing a CONTEXT CHECKPOINT COMPACTION. Create a handoff summary for another LLM that will resume the task. + + Include: + - Current progress and key decisions made + - Important context, constraints, or user preferences + - What remains to be done (clear next steps) + - Any critical data, examples, or references needed to continue + + Be concise, structured, and focused on helping the next LLM seamlessly continue the work. + + +## Window 2 (after request 4: input diverged at item 02) +Settings: unavailable (items only) +-- request 5 (items; Stored replacement before next turn) -- +00:additional_tools/developer (3; hash=bf2e30605ff5dc17): + - namespace/functions; hash=fada4a476a3754dd + - custom/exec + - function/wait + - function/request_user_input + - function/request_user_input_async + - namespace/clock: Tools for reading and waiting on time.; hash=f402e8c5e9b5e317 + - function/sleep + - namespace/collaboration: Tools for spawning and managing sub-agents.; hash=2c8d6295978dbe63 + - function/followup_task + - function/interrupt_agent + - function/list_agents + - function/send_message + - function/spawn_agent + - function/wait_agent +01:message/developer: + You are Codex, an agent based on GPT-6. You and the user share one workspace, and your job is to collabo...them until their intended goal is completely handled. [hash=efa50f074efa5803] + + # When to ask the user for permission + + Use your best judgement given task context for when you really need user permission, like a competent co...ork without ending the turn to clarify with the user. [hash=ca7367e658b51388] + + User authorization and preferences persist across turns. Do not request permission again when the user h... any guidelines provided in skills or external files. [hash=911f91c92c72882c] + + + + - Skill naming: If a plugin contributes skills, those skill entries are prefixed with plugin_name: in the Skills list. + - MCP naming: Plugin-provided MCP tools keep standard MCP identifiers such as mcp__server__tool; use tool provenance to tell which plugin they come from. + - Trigger rules: If the user explicitly names a plugin, prefer capabilities associated with that plugin for that turn. + - Relationship to capabilities: Plugins are not invoked directly. Use their underlying skills, MCP tools, and app tools to help solve the task. + - Relevance: Determine what a plugin can help with from explicit user mention or from the plugin-associa..., MCP tools, and apps exposed elsewhere in this turn. [hash=620459a4e7b590f0] + - Missing/blocked: If the user requests a plugin that does not have relevant callable capabilities for t..., say so briefly and continue with the best fallback. [hash=746b0469a8f6cb3e] + +02:message/developer: + scenario_prefix: C +03:message/user: + Plan the release. +04:message/user: + Check the plan. +05:message/developer[2]: + [01] + Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `danger-full-access`...ll commands are permitted. Network access is enabled. [hash=c134061cebb94a1b] + Approval policy is currently never. Do not provide the `sandbox_permissions` for any reason, commands will be rejected. + + [02] # Collaboration Mode: Default + + You are now in Default mode. Any previous instructions for other modes (e.g. Plan mode) are no longer active. + + Your active mode changes only when new developer instructions with a different `...<...by themselves. Known mode names are Default and Plan. [hash=e75f6b12fc52d812] + + ## request_user_input availability + + Use the `request_user_input` tool only when it is listed in the available tools for this turn. + + Use the `request_user_input` tool only for optional questions where the answer would materially improve the quality of the work. + + If `request_user_input` returns no answers, continue with best judgment instead of asking again or treating the turn as blocked. + + Never use the `request_user_input` tool for permission requests or permission-related escalations. + +06:message/developer: + 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 an...message to a running agent without triggering a turn. [hash=7d40ff8b58977202] + `send_message` calls may be read by a human, so ensure they are legible. Always put proper spaces between words and/or numbers. + + - All agents use the same current working directory. + - As a result, edits made by one agent are immediately visible to all other agents. + + When calling `wait_agent`, prefer longer waits (minutes) to avoid busy polling. + + There are 4 available concurrency slots, meaning that up to 4 agents can be active at once, including you. + + Full-history forks (`fork_turns` omitted or `"all"`) inherit the parent model and reasoning effort and d... so, set `fork_turns` to `"none"`. [hash=d5928243dc643bcc] +07:message/developer: + Any earlier instruction enabling proactive multi-agent delegation no longer applies. D...elegation, or parallel agent work. [hash=2b6e037067b4390c] +08:message/user: + + + + + + <...e="unrestricted" /> [hash=d40b2f45cb028888] + +09:message/user: + Update the plan. +10:message/user: + Another language model started to solve this problem and produced a summary of its thinking process. You...ion in this summary to assist with your own analysis: [hash=b8ce9f267277728e] + The user is planning a release. + +## Window 3 (after request 5) +Settings: same as window 1 +-- request 6 (turn; Unchanged policy C after compaction) -- +00:additional_tools/developer (3; hash=bf2e30605ff5dc17): + - namespace/functions; hash=fada4a476a3754dd + - custom/exec + - function/wait + - function/request_user_input + - function/request_user_input_async + - namespace/clock: Tools for reading and waiting on time.; hash=f402e8c5e9b5e317 + - function/sleep + - namespace/collaboration: Tools for spawning and managing sub-agents.; hash=2c8d6295978dbe63 + - function/followup_task + - function/interrupt_agent + - function/list_agents + - function/send_message + - function/spawn_agent + - function/wait_agent +01:message/developer: + You are Codex, an agent based on GPT-6. You and the user share one workspace, and your job is to collabo...them until their intended goal is completely handled. [hash=efa50f074efa5803] + + # When to ask the user for permission + + Use your best judgement given task context for when you really need user permission, like a competent co...ork without ending the turn to clarify with the user. [hash=ca7367e658b51388] + + User authorization and preferences persist across turns. Do not request permission again when the user h... any guidelines provided in skills or external files. [hash=911f91c92c72882c] + + + + - Skill naming: If a plugin contributes skills, those skill entries are prefixed with plugin_name: in the Skills list. + - MCP naming: Plugin-provided MCP tools keep standard MCP identifiers such as mcp__server__tool; use tool provenance to tell which plugin they come from. + - Trigger rules: If the user explicitly names a plugin, prefer capabilities associated with that plugin for that turn. + - Relationship to capabilities: Plugins are not invoked directly. Use their underlying skills, MCP tools, and app tools to help solve the task. + - Relevance: Determine what a plugin can help with from explicit user mention or from the plugin-associa..., MCP tools, and apps exposed elsewhere in this turn. [hash=620459a4e7b590f0] + - Missing/blocked: If the user requests a plugin that does not have relevant callable capabilities for t..., say so briefly and continue with the best fallback. [hash=746b0469a8f6cb3e] + +02:message/developer: + scenario_prefix: C +03:message/user: + Plan the release. +04:message/user: + Check the plan. +05:message/developer[2]: + [01] + Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `danger-full-access`...ll commands are permitted. Network access is enabled. [hash=c134061cebb94a1b] + Approval policy is currently never. Do not provide the `sandbox_permissions` for any reason, commands will be rejected. + + [02] # Collaboration Mode: Default + + You are now in Default mode. Any previous instructions for other modes (e.g. Plan mode) are no longer active. + + Your active mode changes only when new developer instructions with a different `...<...by themselves. Known mode names are Default and Plan. [hash=e75f6b12fc52d812] + + ## request_user_input availability + + Use the `request_user_input` tool only when it is listed in the available tools for this turn. + + Use the `request_user_input` tool only for optional questions where the answer would materially improve the quality of the work. + + If `request_user_input` returns no answers, continue with best judgment instead of asking again or treating the turn as blocked. + + Never use the `request_user_input` tool for permission requests or permission-related escalations. + +06:message/developer: + 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 an...message to a running agent without triggering a turn. [hash=7d40ff8b58977202] + `send_message` calls may be read by a human, so ensure they are legible. Always put proper spaces between words and/or numbers. + + - All agents use the same current working directory. + - As a result, edits made by one agent are immediately visible to all other agents. + + When calling `wait_agent`, prefer longer waits (minutes) to avoid busy polling. + + There are 4 available concurrency slots, meaning that up to 4 agents can be active at once, including you. + + Full-history forks (`fork_turns` omitted or `"all"`) inherit the parent model and reasoning effort and d... so, set `fork_turns` to `"none"`. [hash=d5928243dc643bcc] +07:message/developer: + Any earlier instruction enabling proactive multi-agent delegation no longer applies. D...elegation, or parallel agent work. [hash=2b6e037067b4390c] +08:message/user: + + + + + + <...e="unrestricted" /> [hash=d40b2f45cb028888] + +09:message/user: + Update the plan. +10:message/user: + Another language model started to solve this problem and produced a summary of its thinking process. You...ion in this summary to assist with your own analysis: [hash=b8ce9f267277728e] + The user is planning a release. +11:message/user: + Continue with the release. diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__world_state_prefix__extension_window_prefix_remote.snap b/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__world_state_prefix__extension_window_prefix_remote.snap new file mode 100644 index 0000000000..5c750ea812 --- /dev/null +++ b/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__world_state_prefix__extension_window_prefix_remote.snap @@ -0,0 +1,313 @@ +--- +source: core/tests/suite/scenarios_world_state_prefix_tests.rs +expression: "context_snapshot::format_context_snapshot(\"An extension supplies A, stays unchanged, appends B, then supplies C during compaction. Only C starts the new window. Non-developer prefix fragments are never rendered.\",\n&[SnapshotEntry::captured(&requests[0]).labeled(\"Initial policy A\"),\nSnapshotEntry::captured(&requests[1]).labeled(\"Unchanged policy A\"),\nSnapshotEntry::captured(&requests[2]).labeled(\"Policy B appends to existing history\"),\nSnapshotEntry::captured(&requests[3]).labeled(\"Compaction request\"),\nSnapshotEntry::items(&replacement).labeled(\"Stored replacement before next turn\"),\nSnapshotEntry::captured(&requests[4]).labeled(\"Unchanged policy C after compaction\"),],\n&ContextSnapshotOptions::default().include_request_settings(),)" +--- +Scenario: An extension supplies A, stays unchanged, appends B, then supplies C during compaction. Only C starts the new window. Non-developer prefix fragments are never rendered. + +## Window 1 +Settings: + include: ["reasoning.encrypted_content"] + model: "gpt-6-astra" + parallel_tool_calls: false + prompt_cache_key: "" + reasoning: {"context":"all_turns","effort":"low"} + store: false + stream: true + text: {"verbosity":"low"} + tool_choice: "auto" +-- request 1 (turn; Initial policy A) -- +00:additional_tools/developer (3; hash=bf2e30605ff5dc17): + - namespace/functions; hash=fada4a476a3754dd + - custom/exec + - function/wait + - function/request_user_input + - function/request_user_input_async + - namespace/clock: Tools for reading and waiting on time.; hash=f402e8c5e9b5e317 + - function/sleep + - namespace/collaboration: Tools for spawning and managing sub-agents.; hash=2c8d6295978dbe63 + - function/followup_task + - function/interrupt_agent + - function/list_agents + - function/send_message + - function/spawn_agent + - function/wait_agent +01:message/developer: + You are Codex, an agent based on GPT-6. You and the user share one workspace, and your job is to collabo...them until their intended goal is completely handled. [hash=efa50f074efa5803] + + # When to ask the user for permission + + Use your best judgement given task context for when you really need user permission, like a competent co...ork without ending the turn to clarify with the user. [hash=ca7367e658b51388] + + User authorization and preferences persist across turns. Do not request permission again when the user h... any guidelines provided in skills or external files. [hash=911f91c92c72882c] + + + + - Skill naming: If a plugin contributes skills, those skill entries are prefixed with plugin_name: in the Skills list. + - MCP naming: Plugin-provided MCP tools keep standard MCP identifiers such as mcp__server__tool; use tool provenance to tell which plugin they come from. + - Trigger rules: If the user explicitly names a plugin, prefer capabilities associated with that plugin for that turn. + - Relationship to capabilities: Plugins are not invoked directly. Use their underlying skills, MCP tools, and app tools to help solve the task. + - Relevance: Determine what a plugin can help with from explicit user mention or from the plugin-associa..., MCP tools, and apps exposed elsewhere in this turn. [hash=620459a4e7b590f0] + - Missing/blocked: If the user requests a plugin that does not have relevant callable capabilities for t..., say so briefly and continue with the best fallback. [hash=746b0469a8f6cb3e] + +02:message/developer: + scenario_prefix: A +03:message/developer[2]: + [01] + Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `danger-full-access`...ll commands are permitted. Network access is enabled. [hash=c134061cebb94a1b] + Approval policy is currently never. Do not provide the `sandbox_permissions` for any reason, commands will be rejected. + + [02] # Collaboration Mode: Default + + You are now in Default mode. Any previous instructions for other modes (e.g. Plan mode) are no longer active. + + Your active mode changes only when new developer instructions with a different `...<...by themselves. Known mode names are Default and Plan. [hash=e75f6b12fc52d812] + + ## request_user_input availability + + Use the `request_user_input` tool only when it is listed in the available tools for this turn. + + Use the `request_user_input` tool only for optional questions where the answer would materially improve the quality of the work. + + If `request_user_input` returns no answers, continue with best judgment instead of asking again or treating the turn as blocked. + + Never use the `request_user_input` tool for permission requests or permission-related escalations. + +04:message/developer: + 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 an...message to a running agent without triggering a turn. [hash=7d40ff8b58977202] + `send_message` calls may be read by a human, so ensure they are legible. Always put proper spaces between words and/or numbers. + + - All agents use the same current working directory. + - As a result, edits made by one agent are immediately visible to all other agents. + + When calling `wait_agent`, prefer longer waits (minutes) to avoid busy polling. + + There are 4 available concurrency slots, meaning that up to 4 agents can be active at once, including you. + + Full-history forks (`fork_turns` omitted or `"all"`) inherit the parent model and reasoning effort and d... so, set `fork_turns` to `"none"`. [hash=d5928243dc643bcc] +05:message/developer: + Any earlier instruction enabling proactive multi-agent delegation no longer applies. D...elegation, or parallel agent work. [hash=2b6e037067b4390c] +06:message/user: + + + + + + <...e="unrestricted" /> [hash=d40b2f45cb028888] + +07:message/user: + Plan the release. +08:configuration_update:reasoning={"effort":"low"} +-- request 2 (turn; Unchanged policy A) -- +09:message/assistant: + Plan ready. +10:message/user: + Check the plan. +-- request 3 (turn; Policy B appends to existing history) -- +11:message/assistant: + Plan checked. +12:message/developer: + scenario_prefix: B +13:message/user: + Update the plan. +-- request 4 (compaction; Compaction request) -- +14:message/assistant: + Plan updated. +15:compaction_trigger + +## Window 2 (after request 4: input diverged at item 02) +Settings: unavailable (items only) +-- request 5 (items; Stored replacement before next turn) -- +00:additional_tools/developer (3; hash=bf2e30605ff5dc17): + - namespace/functions; hash=fada4a476a3754dd + - custom/exec + - function/wait + - function/request_user_input + - function/request_user_input_async + - namespace/clock: Tools for reading and waiting on time.; hash=f402e8c5e9b5e317 + - function/sleep + - namespace/collaboration: Tools for spawning and managing sub-agents.; hash=2c8d6295978dbe63 + - function/followup_task + - function/interrupt_agent + - function/list_agents + - function/send_message + - function/spawn_agent + - function/wait_agent +01:message/developer: + You are Codex, an agent based on GPT-6. You and the user share one workspace, and your job is to collabo...them until their intended goal is completely handled. [hash=efa50f074efa5803] + + # When to ask the user for permission + + Use your best judgement given task context for when you really need user permission, like a competent co...ork without ending the turn to clarify with the user. [hash=ca7367e658b51388] + + User authorization and preferences persist across turns. Do not request permission again when the user h... any guidelines provided in skills or external files. [hash=911f91c92c72882c] + + + + - Skill naming: If a plugin contributes skills, those skill entries are prefixed with plugin_name: in the Skills list. + - MCP naming: Plugin-provided MCP tools keep standard MCP identifiers such as mcp__server__tool; use tool provenance to tell which plugin they come from. + - Trigger rules: If the user explicitly names a plugin, prefer capabilities associated with that plugin for that turn. + - Relationship to capabilities: Plugins are not invoked directly. Use their underlying skills, MCP tools, and app tools to help solve the task. + - Relevance: Determine what a plugin can help with from explicit user mention or from the plugin-associa..., MCP tools, and apps exposed elsewhere in this turn. [hash=620459a4e7b590f0] + - Missing/blocked: If the user requests a plugin that does not have relevant callable capabilities for t..., say so briefly and continue with the best fallback. [hash=746b0469a8f6cb3e] + +02:message/developer: + scenario_prefix: C +03:message/user: + Plan the release. +04:message/user: + Check the plan. +05:message/developer[2]: + [01] + Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `danger-full-access`...ll commands are permitted. Network access is enabled. [hash=c134061cebb94a1b] + Approval policy is currently never. Do not provide the `sandbox_permissions` for any reason, commands will be rejected. + + [02] # Collaboration Mode: Default + + You are now in Default mode. Any previous instructions for other modes (e.g. Plan mode) are no longer active. + + Your active mode changes only when new developer instructions with a different `...<...by themselves. Known mode names are Default and Plan. [hash=e75f6b12fc52d812] + + ## request_user_input availability + + Use the `request_user_input` tool only when it is listed in the available tools for this turn. + + Use the `request_user_input` tool only for optional questions where the answer would materially improve the quality of the work. + + If `request_user_input` returns no answers, continue with best judgment instead of asking again or treating the turn as blocked. + + Never use the `request_user_input` tool for permission requests or permission-related escalations. + +06:message/developer: + 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 an...message to a running agent without triggering a turn. [hash=7d40ff8b58977202] + `send_message` calls may be read by a human, so ensure they are legible. Always put proper spaces between words and/or numbers. + + - All agents use the same current working directory. + - As a result, edits made by one agent are immediately visible to all other agents. + + When calling `wait_agent`, prefer longer waits (minutes) to avoid busy polling. + + There are 4 available concurrency slots, meaning that up to 4 agents can be active at once, including you. + + Full-history forks (`fork_turns` omitted or `"all"`) inherit the parent model and reasoning effort and d... so, set `fork_turns` to `"none"`. [hash=d5928243dc643bcc] +07:message/developer: + Any earlier instruction enabling proactive multi-agent delegation no longer applies. D...elegation, or parallel agent work. [hash=2b6e037067b4390c] +08:message/user: + + + + + + <...e="unrestricted" /> [hash=d40b2f45cb028888] + +09:message/user: + Update the plan. +10:compaction:encrypted=true; chars=17; hash=4bf78ed3d93cf9a2 + +## Window 3 (after request 5) +Settings: same as window 1 +-- request 6 (turn; Unchanged policy C after compaction) -- +00:additional_tools/developer (3; hash=bf2e30605ff5dc17): + - namespace/functions; hash=fada4a476a3754dd + - custom/exec + - function/wait + - function/request_user_input + - function/request_user_input_async + - namespace/clock: Tools for reading and waiting on time.; hash=f402e8c5e9b5e317 + - function/sleep + - namespace/collaboration: Tools for spawning and managing sub-agents.; hash=2c8d6295978dbe63 + - function/followup_task + - function/interrupt_agent + - function/list_agents + - function/send_message + - function/spawn_agent + - function/wait_agent +01:message/developer: + You are Codex, an agent based on GPT-6. You and the user share one workspace, and your job is to collabo...them until their intended goal is completely handled. [hash=efa50f074efa5803] + + # When to ask the user for permission + + Use your best judgement given task context for when you really need user permission, like a competent co...ork without ending the turn to clarify with the user. [hash=ca7367e658b51388] + + User authorization and preferences persist across turns. Do not request permission again when the user h... any guidelines provided in skills or external files. [hash=911f91c92c72882c] + + + + - Skill naming: If a plugin contributes skills, those skill entries are prefixed with plugin_name: in the Skills list. + - MCP naming: Plugin-provided MCP tools keep standard MCP identifiers such as mcp__server__tool; use tool provenance to tell which plugin they come from. + - Trigger rules: If the user explicitly names a plugin, prefer capabilities associated with that plugin for that turn. + - Relationship to capabilities: Plugins are not invoked directly. Use their underlying skills, MCP tools, and app tools to help solve the task. + - Relevance: Determine what a plugin can help with from explicit user mention or from the plugin-associa..., MCP tools, and apps exposed elsewhere in this turn. [hash=620459a4e7b590f0] + - Missing/blocked: If the user requests a plugin that does not have relevant callable capabilities for t..., say so briefly and continue with the best fallback. [hash=746b0469a8f6cb3e] + +02:message/developer: + scenario_prefix: C +03:message/user: + Plan the release. +04:message/user: + Check the plan. +05:message/developer[2]: + [01] + Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `danger-full-access`...ll commands are permitted. Network access is enabled. [hash=c134061cebb94a1b] + Approval policy is currently never. Do not provide the `sandbox_permissions` for any reason, commands will be rejected. + + [02] # Collaboration Mode: Default + + You are now in Default mode. Any previous instructions for other modes (e.g. Plan mode) are no longer active. + + Your active mode changes only when new developer instructions with a different `...<...by themselves. Known mode names are Default and Plan. [hash=e75f6b12fc52d812] + + ## request_user_input availability + + Use the `request_user_input` tool only when it is listed in the available tools for this turn. + + Use the `request_user_input` tool only for optional questions where the answer would materially improve the quality of the work. + + If `request_user_input` returns no answers, continue with best judgment instead of asking again or treating the turn as blocked. + + Never use the `request_user_input` tool for permission requests or permission-related escalations. + +06:message/developer: + 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 an...message to a running agent without triggering a turn. [hash=7d40ff8b58977202] + `send_message` calls may be read by a human, so ensure they are legible. Always put proper spaces between words and/or numbers. + + - All agents use the same current working directory. + - As a result, edits made by one agent are immediately visible to all other agents. + + When calling `wait_agent`, prefer longer waits (minutes) to avoid busy polling. + + There are 4 available concurrency slots, meaning that up to 4 agents can be active at once, including you. + + Full-history forks (`fork_turns` omitted or `"all"`) inherit the parent model and reasoning effort and d... so, set `fork_turns` to `"none"`. [hash=d5928243dc643bcc] +07:message/developer: + Any earlier instruction enabling proactive multi-agent delegation no longer applies. D...elegation, or parallel agent work. [hash=2b6e037067b4390c] +08:message/user: + + + + + + <...e="unrestricted" /> [hash=d40b2f45cb028888] + +09:message/user: + Update the plan. +10:compaction:encrypted=true; chars=17; hash=4bf78ed3d93cf9a2 +11:message/user: + Continue with the release. diff --git a/codex-rs/ext/extension-api/src/contributors/world_state.rs b/codex-rs/ext/extension-api/src/contributors/world_state.rs index a1e0cd05df..7f30e074ad 100644 --- a/codex-rs/ext/extension-api/src/contributors/world_state.rs +++ b/codex-rs/ext/extension-api/src/contributors/world_state.rs @@ -98,6 +98,7 @@ type LegacyFragmentMatcher = dyn Fn(&str, &str) -> bool + Send + Sync; #[derive(Clone)] pub struct WorldStateSectionContribution { id: &'static str, + window_prefix: bool, render_diff: Arc, matches_legacy_fragment: Arc, matches_retained_fragment: Option>, @@ -118,6 +119,7 @@ impl WorldStateSectionContribution { ) -> Self { Self { id, + window_prefix: false, render_diff: Arc::new(render_diff), matches_legacy_fragment: Arc::new(|_, _| false), matches_retained_fragment: None, @@ -132,6 +134,20 @@ impl WorldStateSectionContribution { self } + /// Render this section's developer fragments as separate messages before retained + /// history when the harness builds a window (including after compaction). Changes + /// within an existing window are still appended as ordinary history updates. + /// Fragments with any other role are not rendered, including in history updates. + /// The producer still owns content size bounds; prefix placement does not truncate. + pub fn with_window_prefix(mut self) -> Self { + self.window_prefix = true; + self + } + + pub fn is_window_prefix(&self) -> bool { + self.window_prefix + } + /// Requires a matching model-visible fragment whenever a persisted snapshot is reused. pub fn with_retained_fragment_matcher( mut self,