Add ephemeral turn context support

Introduce app-server ephemeralContext, render it as additional_context_for_this_turn fragments, and strip it correctly across contextual-user diffing and compaction flows.

Regenerate app-server schema fixtures and add coverage for model-visible layout, compaction, oversize validation, and resumed/forked context reconstruction.

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Charles Cunningham
2026-03-12 12:36:26 -07:00
parent 4bcfdafd01
commit 72d53a2c9b
54 changed files with 1422 additions and 26 deletions

View File

@@ -44,6 +44,7 @@ use crate::plan_tool::UpdatePlanArgs;
use crate::request_permissions::RequestPermissionsEvent;
use crate::request_permissions::RequestPermissionsResponse;
use crate::request_user_input::RequestUserInputResponse;
use crate::user_input::EphemeralContext;
use crate::user_input::UserInput;
use codex_utils_absolute_path::AbsolutePathBuf;
use schemars::JsonSchema;
@@ -80,6 +81,8 @@ pub const USER_INSTRUCTIONS_OPEN_TAG: &str = "<user_instructions>";
pub const USER_INSTRUCTIONS_CLOSE_TAG: &str = "</user_instructions>";
pub const ENVIRONMENT_CONTEXT_OPEN_TAG: &str = "<environment_context>";
pub const ENVIRONMENT_CONTEXT_CLOSE_TAG: &str = "</environment_context>";
pub const EPHEMERAL_CONTEXT_OPEN_TAG: &str = "<additional_context_for_this_turn>";
pub const EPHEMERAL_CONTEXT_CLOSE_TAG: &str = "</additional_context_for_this_turn>";
pub const COLLABORATION_MODE_OPEN_TAG: &str = "<collaboration_mode>";
pub const COLLABORATION_MODE_CLOSE_TAG: &str = "</collaboration_mode>";
pub const REALTIME_CONVERSATION_OPEN_TAG: &str = "<realtime_conversation>";
@@ -209,6 +212,10 @@ pub enum Op {
UserInput {
/// User input items, see `InputItem`
items: Vec<UserInput>,
/// Turn-scoped context that is visible to the model but is not part of
/// the durable turn baseline.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
ephemeral_context: Vec<EphemeralContext>,
/// Optional JSON Schema used to constrain the final assistant message for this turn.
#[serde(skip_serializing_if = "Option::is_none")]
final_output_json_schema: Option<Value>,
@@ -4053,6 +4060,7 @@ mod tests {
fn user_input_serialization_omits_final_output_json_schema_when_none() -> Result<()> {
let op = Op::UserInput {
items: Vec::new(),
ephemeral_context: Vec::new(),
final_output_json_schema: None,
};
@@ -4070,6 +4078,7 @@ mod tests {
op,
Op::UserInput {
items: Vec::new(),
ephemeral_context: Vec::new(),
final_output_json_schema: None,
}
);
@@ -4089,6 +4098,7 @@ mod tests {
});
let op = Op::UserInput {
items: Vec::new(),
ephemeral_context: Vec::new(),
final_output_json_schema: Some(schema.clone()),
};
@@ -4105,6 +4115,33 @@ mod tests {
Ok(())
}
#[test]
fn user_input_serialization_includes_ephemeral_context_when_present() -> Result<()> {
let op = Op::UserInput {
items: Vec::new(),
ephemeral_context: vec![EphemeralContext {
title: "Context from my editor".to_string(),
text: "## Active file: src/main.rs".to_string(),
}],
final_output_json_schema: None,
};
let json_op = serde_json::to_value(op)?;
assert_eq!(
json_op,
json!({
"type": "user_input",
"items": [],
"ephemeral_context": [{
"title": "Context from my editor",
"text": "## Active file: src/main.rs",
}],
})
);
Ok(())
}
#[test]
fn user_input_text_serializes_empty_text_elements() -> Result<()> {
let input = UserInput::Text {

View File

@@ -6,6 +6,14 @@ use ts_rs::TS;
/// Conservative cap so one user message cannot monopolize a large context window.
pub const MAX_USER_INPUT_TEXT_CHARS: usize = 1 << 20;
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, TS, JsonSchema)]
pub struct EphemeralContext {
/// Human-readable title for additional context sent with one turn.
pub title: String,
/// Free-form text payload for additional context sent with one turn.
pub text: String,
}
/// User input
#[non_exhaustive]
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, TS, JsonSchema)]