diff --git a/codex-rs/core/src/model_visible_context.rs b/codex-rs/core/src/model_visible_context.rs index 8ad69a888a..88a78cfe5d 100644 --- a/codex-rs/core/src/model_visible_context.rs +++ b/codex-rs/core/src/model_visible_context.rs @@ -18,10 +18,11 @@ //! - `DeveloperContextRole` for developer guidance/policy //! - `ContextualUserContextRole` for contextual user-role state that must be //! parsed as context rather than literal user intent -//! - If the fragment is durable turn/session state that should rebuild across -//! resume, compaction, backtracking, or fork, implement `build(...)` for the -//! common zero-or-one case, or override `build_many(...)` when a fragment -//! source needs to emit multiple content items from one turn-state source. +//! - If the fragment is turn/session state that should be rebuilt from the +//! current `TurnContext` during initial-context assembly or diffing, +//! implement `build(...)` for the common zero-or-one case, or override +//! `build_many(...)` when a fragment source needs to emit multiple content +//! items from one turn-state source. //! `reference_context_item` is the baseline already represented in //! model-visible history; compare against it to avoid duplicates, and use //! `TurnContextDiffParams` for other runtime/session inputs such as diff --git a/codex-rs/core/src/model_visible_fragments.rs b/codex-rs/core/src/model_visible_fragments.rs index 2d88f56f9c..9cd40f1210 100644 --- a/codex-rs/core/src/model_visible_fragments.rs +++ b/codex-rs/core/src/model_visible_fragments.rs @@ -14,7 +14,8 @@ //! `matches_contextual_user_text()` for custom matching. //! 4. If the fragment is derived from `TurnContext` and should participate in //! initial-context assembly and turn-to-turn diffing, implementing -//! `build(...)`. +//! `build(...)` for the common zero-or-one case or overriding +//! `build_many(...)` for zero-or-many sources. //! 5. Registering the fragment exactly once in //! `REGISTERED_MODEL_VISIBLE_FRAGMENTS` in the rough order it should appear //! in model-visible context. @@ -79,7 +80,6 @@ use codex_protocol::protocol::TurnContextItem; use codex_protocol::protocol::TurnContextNetworkItem; use codex_protocol::protocol::USER_INSTRUCTIONS_CLOSE_TAG; use codex_protocol::protocol::USER_INSTRUCTIONS_OPEN_TAG; -use codex_protocol::user_input::EphemeralContext; use serde::Deserialize; use serde::Serialize; use std::path::PathBuf; @@ -151,7 +151,7 @@ const REGISTERED_MODEL_VISIBLE_FRAGMENTS: &[ModelVisibleFragmentRegistration] = ModelVisibleFragmentRegistration::of::(), ModelVisibleFragmentRegistration::of::(), ModelVisibleFragmentRegistration::of::(), - ModelVisibleFragmentRegistration::of::(), + ModelVisibleFragmentRegistration::of::(), ModelVisibleFragmentRegistration::of::(), ModelVisibleFragmentRegistration::of::(), ModelVisibleFragmentRegistration::of::(), @@ -848,7 +848,14 @@ impl ModelVisibleContextFragment for EnvironmentContext { } } -impl ModelVisibleContextFragment for EphemeralContext { +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename = "ephemeral_context_fragment", rename_all = "snake_case")] +pub(crate) struct EphemeralContextFragment { + title: String, + text: String, +} + +impl ModelVisibleContextFragment for EphemeralContextFragment { type Role = ContextualUserContextRole; fn render_text(&self) -> String { @@ -863,7 +870,17 @@ impl ModelVisibleContextFragment for EphemeralContext { _reference_context_item: Option<&TurnContextItem>, _params: &TurnContextDiffParams<'_>, ) -> Vec { - turn_context.ephemeral_context.clone() + // Ephemeral context is current-turn-scoped state: rebuild the active + // turn's latest snapshot, but never diff it against persisted + // reference_context_item history from older turns. + turn_context + .ephemeral_context + .iter() + .map(|context| Self { + title: context.title.clone(), + text: context.text.clone(), + }) + .collect() } fn contextual_user_markers() -> Option { @@ -1048,7 +1065,7 @@ pub(crate) fn is_ephemeral_context_fragment(content_item: &ContentItem) -> bool let ContentItem::InputText { text } = content_item else { return false; }; - EphemeralContext::matches_contextual_user_text(text) + EphemeralContextFragment::matches_contextual_user_text(text) } pub(crate) fn build_turn_state_fragments( diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact__mid_turn_compaction_replaces_ephemeral_context_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact__mid_turn_compaction_replaces_ephemeral_context_shapes.snap index bea758420a..b833fa6445 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact__mid_turn_compaction_replaces_ephemeral_context_shapes.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact__mid_turn_compaction_replaces_ephemeral_context_shapes.snap @@ -6,24 +6,24 @@ Scenario: Mid-turn continuation compaction keeps both prior and active ephemeral ## Local Compaction Request 00:message/developer: -01:message/user[2]: - [01] - [02] > -02:message/user:\n Context from my edi... -03:message/user:SETUP_USER -04:message/assistant:FIRST_REPLY -05:message/user:<additional_context_for_this_turn>\n <title>Context from my edi... -06:message/user:function call limit push -07:function_call/test_tool -08:function_call_output:unsupported call: test_tool -09:message/user:<SUMMARIZATION_PROMPT> +01:message/user[3]: + [01] <SKILLS_SECTION> + [02] <ENVIRONMENT_CONTEXT:cwd=<CWD#1>> + [03] <additional_context_for_this_turn>\n <title>Context from my edi... +02:message/user:SETUP_USER +03:message/assistant:FIRST_REPLY +04:message/user:<additional_context_for_this_turn>\n <title>Context from my edi... +05:message/user:function call limit push +06:function_call/test_tool +07:function_call_output:unsupported call: test_tool +08:message/user:<SUMMARIZATION_PROMPT> ## Local Post-Compaction History Layout 00:message/user:SETUP_USER 01:message/developer:<PERMISSIONS_INSTRUCTIONS> 02:message/user[3]: - [01] <AGENTS_MD> - [02] <ENVIRONMENT_CONTEXT:cwd=<CWD>> + [01] <SKILLS_SECTION> + [02] <ENVIRONMENT_CONTEXT:cwd=<CWD#1>> [03] <additional_context_for_this_turn>\n <title>Context from my edi... 03:message/user:function call limit push 04:message/user:<COMPACTION_SUMMARY>\nAUTO_SUMMARY diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_replaces_ephemeral_context_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_replaces_ephemeral_context_shapes.snap index 580e8443f1..68427eea5a 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_replaces_ephemeral_context_shapes.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_replaces_ephemeral_context_shapes.snap @@ -6,15 +6,15 @@ Scenario: Pre-turn auto-compaction keeps prior turn ephemeral_context in the com ## Local Compaction Request 00:message/developer:<PERMISSIONS_INSTRUCTIONS> -01:message/user[2]: - [01] <AGENTS_MD> - [02] <ENVIRONMENT_CONTEXT:cwd=<CWD>> -02:message/user:<additional_context_for_this_turn>\n <title>Context from my edi... -03:message/user:USER_ONE -04:message/assistant:FIRST_REPLY -05:message/user:USER_TWO -06:message/assistant:SECOND_REPLY -07:message/user:<SUMMARIZATION_PROMPT> +01:message/user[3]: + [01] <SKILLS_SECTION> + [02] <ENVIRONMENT_CONTEXT:cwd=<CWD#1>> + [03] <additional_context_for_this_turn>\n <title>Context from my edi... +02:message/user:USER_ONE +03:message/assistant:FIRST_REPLY +04:message/user:USER_TWO +05:message/assistant:SECOND_REPLY +06:message/user:<SUMMARIZATION_PROMPT> ## Local Post-Compaction History Layout 00:message/user:USER_ONE @@ -22,7 +22,7 @@ Scenario: Pre-turn auto-compaction keeps prior turn ephemeral_context in the com 02:message/user:<COMPACTION_SUMMARY>\nPRE_TURN_SUMMARY 03:message/developer:<PERMISSIONS_INSTRUCTIONS> 04:message/user[3]: - [01] <AGENTS_MD> - [02] <ENVIRONMENT_CONTEXT:cwd=<CWD>> + [01] <SKILLS_SECTION> + [02] <ENVIRONMENT_CONTEXT:cwd=<CWD#1>> [03] <additional_context_for_this_turn>\n <title>Context from my edi... 05:message/user:USER_THREE diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_replaces_ephemeral_context_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_replaces_ephemeral_context_shapes.snap index 7246172803..e56ef901bc 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_replaces_ephemeral_context_shapes.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_replaces_ephemeral_context_shapes.snap @@ -6,23 +6,23 @@ Scenario: Remote mid-turn continuation compaction keeps both prior and active ep ## Remote Compaction Request 00:message/developer:<PERMISSIONS_INSTRUCTIONS> -01:message/user[2]: - [01] <AGENTS_MD> - [02] <ENVIRONMENT_CONTEXT:cwd=<CWD>> -02:message/user:<additional_context_for_this_turn>\n <title>Context from my edi... -03:message/user:SETUP_USER -04:message/assistant:REMOTE_SETUP_REPLY -05:message/user:<additional_context_for_this_turn>\n <title>Context from my edi... -06:message/user:USER_TWO -07:function_call/test_tool -08:function_call_output:unsupported call: test_tool +01:message/user[3]: + [01] <SKILLS_SECTION> + [02] <ENVIRONMENT_CONTEXT:cwd=<CWD#1>> + [03] <additional_context_for_this_turn>\n <title>Context from my edi... +02:message/user:SETUP_USER +03:message/assistant:REMOTE_SETUP_REPLY +04:message/user:<additional_context_for_this_turn>\n <title>Context from my edi... +05:message/user:USER_TWO +06:function_call/test_tool +07:function_call_output:unsupported call: test_tool ## Remote Post-Compaction History Layout 00:message/user:SETUP_USER 01:message/developer:<PERMISSIONS_INSTRUCTIONS> 02:message/user[3]: - [01] <AGENTS_MD> - [02] <ENVIRONMENT_CONTEXT:cwd=<CWD>> + [01] <SKILLS_SECTION> + [02] <ENVIRONMENT_CONTEXT:cwd=<CWD#1>> [03] <additional_context_for_this_turn>\n <title>Context from my edi... 03:message/user:USER_TWO 04:compaction:encrypted=true diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__model_visible_layout__model_visible_layout_ephemeral_context.snap b/codex-rs/core/tests/suite/snapshots/all__suite__model_visible_layout__model_visible_layout_ephemeral_context.snap index 3b559869db..9823daff66 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__model_visible_layout__model_visible_layout_ephemeral_context.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__model_visible_layout__model_visible_layout_ephemeral_context.snap @@ -6,19 +6,19 @@ Scenario: Turns resend fresh ephemeral editor context while keeping it outside d ## First Request (With Editor Context) 00:message/developer:<PERMISSIONS_INSTRUCTIONS> -01:message/user[2]: - [01] <AGENTS_MD> - [02] <ENVIRONMENT_CONTEXT:cwd=<CWD>> -02:message/user:<additional_context_for_this_turn>\n <title>Context from my editor\n \n## Act... -03:message/user:first turn with editor context +01:message/user[3]: + [01] + [02] > + [03] \n Context from my editor\n \n## Act... +02:message/user:first turn with editor context ## Second Request (Refreshed Editor Context) 00:message/developer: -01:message/user[2]: - [01] - [02] > -02:message/user:\n Context from my editor\n \n## Act... -03:message/user:first turn with editor context -04:message/assistant:turn one complete -05:message/user:\n Context from my editor\n \n## Act... -06:message/user:second turn with refreshed editor context +01:message/user[3]: + [01] + [02] > + [03] \n Context from my editor\n \n## Act... +02:message/user:first turn with editor context +03:message/assistant:turn one complete +04:message/user:\n Context from my editor\n \n## Act... +05:message/user:second turn with refreshed editor context diff --git a/docs/model-visible-context.md b/docs/model-visible-context.md index d8eea9eb88..1af71fd7fb 100644 --- a/docs/model-visible-context.md +++ b/docs/model-visible-context.md @@ -23,10 +23,13 @@ The key modules are: Model-visible prompt context falls into three buckets: 1. Turn-state fragments. - These are derived from current durable turn/session state and are the ones - that must survive history-mutating flows such as resume, compaction, - backtracking, and fork by being rebuilt from current state plus an optional - persisted baseline. + These are derived from current `TurnContext` state. Most represent durable + turn/session state that must survive history-mutating flows such as resume, + compaction, backtracking, and fork by being rebuilt from current state plus + an optional persisted baseline. Some, such as ephemeral context, are + intentionally current-turn-scoped: they rebuild from the active + `TurnContext`, but are not persisted in the durable baseline for older + turns. 2. Registered runtime fragments. These are not derived from `TurnContext` diffs, but they are still modeled as typed fragments because they are emitted into model-visible history and, @@ -39,7 +42,7 @@ Model-visible prompt context falls into three buckets: The single most important distinction is whether the model-visible state is: -- durable turn/session state that should be rebuilt from `TurnContext` +- turn/session state that should be rebuilt from `TurnContext` - or a one-off event/message that is only relevant because it just happened That determines whether the fragment needs `build(...)`. @@ -79,7 +82,7 @@ That trait owns: - `type Role` - `render_text()` -- optional `build(...)` for turn-state fragments +- optional `build(...)` / `build_many(...)` for turn-state fragments - optional contextual-user detection via `contextual_user_markers()` or `matches_contextual_user_text()` - standard conversions such as `into_message()` and `into_response_input_item()` @@ -106,7 +109,13 @@ registered. ### 3. Build semantics [`ModelVisibleContextFragment::build(...)`](/Users/ccunningham/code/codex-worktree-tria/codex-rs/core/src/model_visible_context.rs#L187) -is the canonical hook for turn-state fragments. +is the canonical hook for the common zero-or-one turn-state case. + +When one turn-state source intentionally renders multiple model-visible +content items, override +[`ModelVisibleContextFragment::build_many(...)`](/Users/ccunningham/code/codex-worktree-tria/codex-rs/core/src/model_visible_context.rs#L199) +instead. The default `build_many(...)` implementation just lifts +`build(...) -> Option` into `Vec`. It receives: @@ -225,8 +234,8 @@ These are typed and registered, but not built from `TurnContext` diffs: ### Registered turn-state contextual-user fragments -These implement `build(...)` and participate in both full initial context and -steady-state diffs: +These implement `build(...)` or `build_many(...)` and participate in both full +initial context and steady-state diffs: - `UserInstructionsFragment` - `AgentsMdInstructions` @@ -234,11 +243,14 @@ steady-state diffs: - `SkillsSectionFragment` - `ChildAgentsInstructionsFragment` - `EnvironmentContext` +- `EphemeralContextFragment` Some of these are true steady-state diff fragments (`UserInstructionsFragment`, -`AgentsMdInstructions`, `EnvironmentContext`). Others intentionally rebuild only -when there is no baseline and therefore behave as initial-context fragments -expressed through the same `build(...)` hook (`JsReplInstructionsFragment`, +`AgentsMdInstructions`, `EnvironmentContext`). `EphemeralContextFragment` +intentionally uses `build_many(...)` because one turn can inject multiple +ephemeral context content items. Others intentionally rebuild only when there +is no baseline and therefore behave as initial-context fragments expressed +through the same `build(...)` hook (`JsReplInstructionsFragment`, `SkillsSectionFragment`, `ChildAgentsInstructionsFragment`). ### Registered runtime contextual-user fragments @@ -360,6 +372,8 @@ When adding new model-visible context: 4. Implement `ModelVisibleContextFragment`. 5. Set `type Role` correctly. 6. If it is turn-state context, implement `build(...)`. + Override `build_many(...)` instead when one turn-state source should emit + multiple content items. 7. If it is contextual-user, provide stable detection with `contextual_user_markers()` or custom `matches_contextual_user_text()`. 8. Register it exactly once in `REGISTERED_MODEL_VISIBLE_FRAGMENTS`, in prompt @@ -371,7 +385,7 @@ When adding new model-visible context: Rule of thumb: - “This is durable prompt state” => registered typed fragment, usually with - `build(...)` + `build(...)` or, for zero-or-many cases, `build_many(...)` - “This is a one-off contextual/runtime marker” => registered typed fragment, usually without `build(...)` - “This is an isolated developer-only text event” => plain developer text is