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-13 11:41:35 -07:00
co-authored by Codex
parent 4bcfdafd01
commit 72d53a2c9b
54 changed files with 1422 additions and 26 deletions
@@ -513,6 +513,23 @@
],
"type": "object"
},
"EphemeralContext": {
"properties": {
"text": {
"description": "Free-form text payload for additional context sent with one turn.",
"type": "string"
},
"title": {
"description": "Human-readable title for additional context sent with one turn.",
"type": "string"
}
},
"required": [
"text",
"title"
],
"type": "object"
},
"ExperimentalFeatureListParams": {
"properties": {
"cursor": {
@@ -2941,6 +2958,16 @@
],
"description": "Override the reasoning effort for this turn and subsequent turns."
},
"ephemeralContext": {
"description": "Additional model-visible context for this turn, such as editor or IDE state. This context is not re-injected automatically after compaction, so clients should send a fresh snapshot on each turn instead of expecting it to carry forward automatically.",
"items": {
"$ref": "#/definitions/EphemeralContext"
},
"type": [
"array",
"null"
]
},
"input": {
"items": {
"$ref": "#/definitions/UserInput"
@@ -6828,6 +6828,23 @@
],
"type": "object"
},
"EphemeralContext": {
"properties": {
"text": {
"description": "Free-form text payload for additional context sent with one turn.",
"type": "string"
},
"title": {
"description": "Human-readable title for additional context sent with one turn.",
"type": "string"
}
},
"required": [
"text",
"title"
],
"type": "object"
},
"ErrorNotification": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
@@ -13206,6 +13223,16 @@
],
"description": "Override the reasoning effort for this turn and subsequent turns."
},
"ephemeralContext": {
"description": "Additional model-visible context for this turn, such as editor or IDE state. This context is not re-injected automatically after compaction, so clients should send a fresh snapshot on each turn instead of expecting it to carry forward automatically.",
"items": {
"$ref": "#/definitions/v2/EphemeralContext"
},
"type": [
"array",
"null"
]
},
"input": {
"items": {
"$ref": "#/definitions/v2/UserInput"
@@ -90,6 +90,23 @@
],
"type": "object"
},
"EphemeralContext": {
"properties": {
"text": {
"description": "Free-form text payload for additional context sent with one turn.",
"type": "string"
},
"title": {
"description": "Human-readable title for additional context sent with one turn.",
"type": "string"
}
},
"required": [
"text",
"title"
],
"type": "object"
},
"ModeKind": {
"description": "Initial collaboration mode to use when the TUI starts.",
"enum": [
@@ -520,6 +537,16 @@
],
"description": "Override the reasoning effort for this turn and subsequent turns."
},
"ephemeralContext": {
"description": "Additional model-visible context for this turn, such as editor or IDE state. This context is not re-injected automatically after compaction, so clients should send a fresh snapshot on each turn instead of expecting it to carry forward automatically.",
"items": {
"$ref": "#/definitions/EphemeralContext"
},
"type": [
"array",
"null"
]
},
"input": {
"items": {
"$ref": "#/definitions/UserInput"
@@ -0,0 +1,13 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type EphemeralContext = {
/**
* Human-readable title for additional context sent with one turn.
*/
title: string,
/**
* Free-form text payload for additional context sent with one turn.
*/
text: string, };
@@ -8,10 +8,17 @@ import type { ReasoningSummary } from "../ReasoningSummary";
import type { ServiceTier } from "../ServiceTier";
import type { JsonValue } from "../serde_json/JsonValue";
import type { AskForApproval } from "./AskForApproval";
import type { EphemeralContext } from "./EphemeralContext";
import type { SandboxPolicy } from "./SandboxPolicy";
import type { UserInput } from "./UserInput";
export type TurnStartParams = {threadId: string, input: Array<UserInput>, /**
* Additional model-visible context for this turn, such as editor or IDE state.
* This context is not re-injected automatically after compaction, so
* clients should send a fresh snapshot on each turn instead of expecting
* it to carry forward automatically.
*/
ephemeralContext?: Array<EphemeralContext> | null, /**
* Override the working directory for this turn and subsequent turns.
*/
cwd?: string | null, /**
@@ -76,6 +76,7 @@ export type { DynamicToolCallParams } from "./DynamicToolCallParams";
export type { DynamicToolCallResponse } from "./DynamicToolCallResponse";
export type { DynamicToolCallStatus } from "./DynamicToolCallStatus";
export type { DynamicToolSpec } from "./DynamicToolSpec";
export type { EphemeralContext } from "./EphemeralContext";
export type { ErrorNotification } from "./ErrorNotification";
export type { ExecPolicyAmendment } from "./ExecPolicyAmendment";
export type { ExperimentalFeature } from "./ExperimentalFeature";
@@ -79,6 +79,7 @@ use codex_protocol::protocol::TokenUsage as CoreTokenUsage;
use codex_protocol::protocol::TokenUsageInfo as CoreTokenUsageInfo;
use codex_protocol::request_permissions::PermissionGrantScope as CorePermissionGrantScope;
use codex_protocol::user_input::ByteRange as CoreByteRange;
use codex_protocol::user_input::EphemeralContext as CoreEphemeralContext;
use codex_protocol::user_input::TextElement as CoreTextElement;
use codex_protocol::user_input::UserInput as CoreUserInput;
use codex_utils_absolute_path::AbsolutePathBuf;
@@ -3602,6 +3603,27 @@ pub enum TurnStatus {
}
// Turn APIs
#[derive(
Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS, ExperimentalApi,
)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
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,
}
impl From<EphemeralContext> for CoreEphemeralContext {
fn from(value: EphemeralContext) -> Self {
Self {
title: value.title,
text: value.text,
}
}
}
#[derive(
Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS, ExperimentalApi,
)]
@@ -3610,6 +3632,12 @@ pub enum TurnStatus {
pub struct TurnStartParams {
pub thread_id: String,
pub input: Vec<UserInput>,
/// Additional model-visible context for this turn, such as editor or IDE state.
/// This context is not re-injected automatically after compaction, so
/// clients should send a fresh snapshot on each turn instead of expecting
/// it to carry forward automatically.
#[ts(optional = nullable)]
pub ephemeral_context: Option<Vec<EphemeralContext>>,
/// Override the working directory for this turn and subsequent turns.
#[ts(optional = nullable)]
pub cwd: Option<PathBuf>,
@@ -7140,6 +7168,7 @@ mod tests {
let without_override = TurnStartParams {
thread_id: "thread_123".to_string(),
input: vec![],
ephemeral_context: None,
cwd: None,
approval_policy: None,
sandbox_policy: None,
@@ -7155,4 +7184,35 @@ mod tests {
serde_json::to_value(&without_override).expect("params should serialize");
assert_eq!(serialized_without_override.get("serviceTier"), None);
}
#[test]
fn turn_start_params_serialize_ephemeral_context() {
let params = TurnStartParams {
thread_id: "thread_123".to_string(),
input: vec![],
ephemeral_context: Some(vec![EphemeralContext {
title: "Context from my editor".to_string(),
text: "## Active file: src/main.rs".to_string(),
}]),
cwd: None,
approval_policy: None,
sandbox_policy: None,
model: None,
service_tier: None,
effort: None,
summary: None,
output_schema: None,
collaboration_mode: None,
personality: None,
};
let serialized = serde_json::to_value(&params).expect("params should serialize");
assert_eq!(
serialized.get("ephemeralContext"),
Some(&json!([{
"title": "Context from my editor",
"text": "## Active file: src/main.rs",
}]))
);
}
}