Fold Apps, Skills, and Plugins into the model-visible fragment registry as ordered developer-envelope fragments, keep the turn-state/contextual-user framework intact, and refresh prompt layout snapshots/docs around the new shape. Co-authored-by: Codex <noreply@openai.com>
14 KiB
Model-visible context
This document describes how model-visible prompt context is represented in the
current codex-rs codebase, and how to add new context without breaking
resume, compaction, backtracking, or history parsing.
The key modules are:
codex-rs/core/src/model_visible_context.rs: shared fragment abstractions, roles, marker helpers, and diff paramscodex-rs/core/src/model_visible_fragments.rs: concrete fragment definitions, the central registry, shared contextual-user detection, and turn-state fragment assemblycodex-rs/core/src/context_manager/updates.rs: the shared developer/contextual-user envelope builders and steady-state per-turn diff assemblycodex-rs/core/src/codex.rs: initial-context assembly and the normal runtime path that decides whether to inject full context or only diffs
Core model
Model-visible prompt context falls into three buckets:
- 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.
- Registered runtime fragments.
These are not derived from
TurnContextdiffs, but they are still modeled as typed fragments because they are emitted into model-visible history and, for contextual-user fragments, must still be recognized during later history parsing. - Developer-only one-off text injections. These are not currently represented as dedicated registered fragments. They are developer-role text added in a few call sites where no contextual-user detection or turn-state diffing is needed.
The single most important distinction is whether the model-visible state is:
- durable 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(...).
Two-envelope rule
Turn-state context assembly always produces exactly two envelopes:
- one
developermessage - one contextual
usermessage
Those envelopes are assembled by the shared builders in
updates.rs.
Adjacent text content items are explicitly separated with \n\n because, from
the model's perspective, adjacent text ContentItems inside one message are
effectively concatenated.
This two-envelope rule applies to turn-state context assembly:
It does not mean every model-visible message in the system is forced into those
two envelopes. Runtime/session-prefix messages may still be emitted as separate
messages when the event itself is what needs to be recorded, and some
initial-only contextual/developer additions are appended to the envelopes
outside registry-driven build(...) assembly.
Current architecture
1. Shared fragment trait
Every named fragment type implements
ModelVisibleContextFragment.
That trait owns:
type Rolerender_text()- optional
build(...)for turn-state fragments - optional contextual-user detection via
contextual_user_markers()ormatches_contextual_user_text() - standard conversions such as
into_message()andinto_response_input_item()
Roles are:
DeveloperContextRoleContextualUserContextRole
2. Central registry
Current fragment types are registered exactly once in
REGISTERED_MODEL_VISIBLE_FRAGMENTS.
That registry is used for:
- contextual-user fragment detection during history parsing
- turn-state fragment assembly for both envelopes by calling
build(...)
Registration is therefore an integration step, not just bookkeeping. Defining the type is not enough; the fragment is not part of the system until it is registered.
3. Build semantics
ModelVisibleContextFragment::build(...)
is the canonical hook for turn-state fragments.
It receives:
- the current
TurnContext reference_context_item: Option<&TurnContextItem>TurnContextDiffParams
reference_context_item is the persisted durable baseline already represented
in model-visible history. A turn-state fragment should compare against that
baseline to avoid emitting duplicate prompt state. When there is no baseline
(None), the fragment should decide whether to emit full current context or
fall back to TurnContextDiffParams for other relevant previous-turn/session
state such as previous_turn_settings.
Turn-state fragments return:
Some(fragment)when model-visible state should be injectedNonewhen the current state is already represented and no update is needed
Fragments that are not turn-state-derived leave build(...) as the default
None.
Initial context vs steady-state diffs
The code intentionally treats these differently.
Full initial context
Codex::build_initial_context(...)
always builds turn-state fragments with reference_context_item = None. In
other words, it asks every turn-state fragment for the full current prompt
state, not a diff.
That is the right behavior for:
- first real user turn in a thread
- any path that must re-establish canonical prompt context from scratch
There is also
build_initial_context_without_reference_context_item(...),
which currently delegates to the same full-context behavior and exists to make
the “ignore any existing baseline” intent explicit in compaction rebuild paths.
Steady-state turn updates
build_settings_update_items(...)
passes the current reference_context_item into the registry-driven turn-state
builder loop, so registered turn-state fragments can emit only the minimal
developer/contextual-user diffs for the next turn.
Normal runtime path
record_context_updates_and_set_reference_context_item(...)
uses this rule:
- if there is no baseline, inject full initial context
- otherwise inject only turn-state diffs
After each real user turn it persists the latest TurnContextItem and advances
the in-memory baseline even if no model-visible diff message was emitted. That
is what keeps later diffing aligned with current state.
Contextual-user detection
Contextual-user fragments share the user role with real user messages, so
history parsing must be able to distinguish:
- contextual state
- true user intent
That is why contextual-user fragments need stable detection.
Preferred path:
- implement
contextual_user_markers()when the fragment has stable fixed wrappers
Fallback path:
- override
matches_contextual_user_text()when detection is genuinely custom (for example the current AGENTS.md wrapper)
The shared detection entrypoint is
is_contextual_user_fragment(...).
It first checks the current registry, then applies a very small legacy shim for
historical user-role wrappers that were shipped previously but are no longer
current fragment types.
Current legacy shim:
- old user-role
<subagent_notification>...</subagent_notification>messages
Those legacy checks should stay bounded and local to detection. Do not create fake current fragment types just to recognize old persisted history.
Current fragment inventory
Registered turn-state developer fragments
These implement build(...) and participate in both full initial context and
steady-state diffs:
ModelInstructionsUpdateFragmentPermissionsUpdateFragmentCustomDeveloperInstructionsUpdateFragmentCollaborationModeUpdateFragmentRealtimeUpdateFragmentPersonalityUpdateFragmentAppsInstructionsFragmentSkillsSectionFragmentPluginInstructions
The capability-guidance fragments are intentionally developer-role, rendered in
Apps -> Skills -> Plugins order, and use stable XML-ish wrappers
(<apps_instructions>, <skills_instructions>, <plugins_instructions>) so
prompt-layout tests can normalize them reliably.
Registered runtime developer fragments
These are typed and registered, but not built from TurnContext diffs:
SubagentRosterContextSubagentNotification
Registered turn-state contextual-user fragments
These implement build(...) and participate in both full initial context and
steady-state diffs:
UserInstructionsFragmentAgentsMdInstructionsJsReplInstructionsFragmentChildAgentsInstructionsFragmentEnvironmentContext
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,
ChildAgentsInstructionsFragment).
Registered runtime contextual-user fragments
These are typed and registered for rendering/detection, but not built from
TurnContext diffs:
SkillInstructionsUserShellCommandFragmentTurnAbortedMarker
SkillInstructions, UserShellCommandFragment, and TurnAbortedMarker are
currently emitted as standalone contextual-user messages.
Current non-registry developer text injections
The current codebase still has a few developer-role prompt injections that are not dedicated registered fragments. These are accurate descriptions of current behavior, not the preferred path for new work.
Current examples include:
- memory-tool initial developer instructions
- commit-attribution guidance in the initial developer envelope
- explicit plugin-mention developer hints
- a few one-off runtime developer messages such as stop-hook and hook-provided additional context
These are acceptable because they are developer-role only and do not need contextual-user detection or turn-state diff reconstruction. But for new model-visible context, prefer a dedicated typed fragment unless the message is truly an isolated one-off developer event.
Choosing the right representation
Use a registered turn-state fragment when:
- the context is derived from
TurnContext/ durable session state - resume, fork, compaction, or backtracking should be able to rebuild it
- it belongs in one of the two turn-state envelopes
Examples:
- permissions policy
- collaboration mode
- realtime start/end state
- apps guidance
- skills catalog guidance
- plugin capability guidance
- custom user instructions
- AGENTS.md instructions
- JS REPL guidance
- child-AGENTS guidance
- environment context
Use a registered runtime fragment when:
- the context is model-visible
- it is not derived from
TurnContextdiffs - it still benefits from standardized rendering and, for contextual-user fragments, shared detection
These fragments may either:
- be appended to an envelope in a non-diff initial-context path
- or be emitted as standalone runtime/session-prefix messages
Examples:
- turn-aborted marker
- user shell command record
- skill injection
- subagent roster / subagent notification
Use a plain developer text wrapper only when:
- the message is developer-role only
- it is a narrow one-off or initial-only addition
- it does not need contextual-user detection
- it does not need to participate in turn-state diff reconstruction
If you are tempted to use a plain String or DeveloperTextFragment, stop and
verify that this is really just an isolated developer-only event and not new
shared prompt state.
Environment context rule
Use <environment_context> only for environment facts derived from
TurnContext that may need turn-to-turn refresh semantics.
Current fields are:
cwdshell- optional
current_date - optional
timezone - optional network allow/deny summaries
Do not put other guidance in <environment_context>, especially:
- permissions/policy instructions
- skills or plugins
- AGENTS instructions
- subagent notices
Those should stay in their own fragment types.
Blessed path for new work
When adding new model-visible context:
- Decide whether the model should see it at all.
- Decide whether it is durable turn/session state or a one-off runtime event.
- If it should be represented as a named fragment, define a typed fragment in
model_visible_fragments.rs. - Implement
ModelVisibleContextFragment. - Set
type Rolecorrectly. - If it is turn-state context, implement
build(...). - If it is contextual-user, provide stable detection with
contextual_user_markers()or custommatches_contextual_user_text(). - Register it exactly once in
REGISTERED_MODEL_VISIBLE_FRAGMENTS, in prompt order. - Use the shared envelope builders or fragment conversions rather than
hand-constructing model-visible
ResponseItem::Messagepayloads in new turn-state paths.
Rule of thumb:
- “This is durable prompt state” => registered typed fragment, usually with
build(...) - “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 sometimes acceptable, but it is the exception, not the default