Align ephemeral context with fragment conventions

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Charles Cunningham
2026-03-13 12:22:26 -07:00
parent cc2e3e4996
commit bbb827666f
7 changed files with 104 additions and 72 deletions

View File

@@ -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

View File

@@ -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::<SkillsSectionFragment>(),
ModelVisibleFragmentRegistration::of::<ChildAgentsInstructionsFragment>(),
ModelVisibleFragmentRegistration::of::<EnvironmentContext>(),
ModelVisibleFragmentRegistration::of::<EphemeralContext>(),
ModelVisibleFragmentRegistration::of::<EphemeralContextFragment>(),
ModelVisibleFragmentRegistration::of::<SkillInstructions>(),
ModelVisibleFragmentRegistration::of::<PluginInstructions>(),
ModelVisibleFragmentRegistration::of::<UserShellCommandFragment>(),
@@ -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<Self> {
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<ContextualUserFragmentMarkers> {
@@ -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(

View File

@@ -6,24 +6,24 @@ Scenario: Mid-turn continuation compaction keeps both prior and active ephemeral
## 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: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

View File

@@ -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

View File

@@ -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

View File

@@ -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</title>\n <content>\n## Act...
03:message/user:first turn with editor context
01:message/user[3]:
[01] <SKILLS_SECTION>
[02] <ENVIRONMENT_CONTEXT:cwd=<CWD#1>>
[03] <additional_context_for_this_turn>\n <title>Context from my editor</title>\n <content>\n## Act...
02:message/user:first turn with editor context
## Second Request (Refreshed 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</title>\n <content>\n## Act...
03:message/user:first turn with editor context
04:message/assistant:turn one complete
05:message/user:<additional_context_for_this_turn>\n <title>Context from my editor</title>\n <content>\n## Act...
06:message/user:second turn with refreshed editor context
01:message/user[3]:
[01] <SKILLS_SECTION>
[02] <ENVIRONMENT_CONTEXT:cwd=<CWD#1>>
[03] <additional_context_for_this_turn>\n <title>Context from my editor</title>\n <content>\n## Act...
02:message/user:first turn with editor context
03:message/assistant:turn one complete
04:message/user:<additional_context_for_this_turn>\n <title>Context from my editor</title>\n <content>\n## Act...
05:message/user:second turn with refreshed editor context

View File

@@ -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<Self>` into `Vec<Self>`.
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