mirror of
https://github.com/openai/codex.git
synced 2026-11-02 19:43:24 +00:00
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:
co-authored by
Codex
parent
4bcfdafd01
commit
72d53a2c9b
@@ -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(¶ms).expect("params should serialize");
|
||||
assert_eq!(
|
||||
serialized.get("ephemeralContext"),
|
||||
Some(&json!([{
|
||||
"title": "Context from my editor",
|
||||
"text": "## Active file: src/main.rs",
|
||||
}]))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -414,10 +414,18 @@ Turns attach user input (text or images) to a thread and trigger Codex generatio
|
||||
|
||||
You can optionally specify config overrides on the new turn. If specified, these settings become the default for subsequent turns on the same thread. `outputSchema` applies only to the current turn.
|
||||
|
||||
`ephemeralContext` is separate from `input`. Use it for additional model-visible context for this turn, such as IDE/editor 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.
|
||||
|
||||
```json
|
||||
{ "method": "turn/start", "id": 30, "params": {
|
||||
"threadId": "thr_123",
|
||||
"input": [ { "type": "text", "text": "Run tests" } ],
|
||||
"ephemeralContext": [
|
||||
{
|
||||
"title": "Context from my editor",
|
||||
"text": "## Active file: src/main.rs"
|
||||
}
|
||||
],
|
||||
// Below are optional config overrides
|
||||
"cwd": "/Users/me/project",
|
||||
"approvalPolicy": "unlessTrusted",
|
||||
|
||||
@@ -41,6 +41,7 @@ use codex_app_server_protocol::CommandExecWriteParams;
|
||||
use codex_app_server_protocol::ConversationGitInfo;
|
||||
use codex_app_server_protocol::ConversationSummary;
|
||||
use codex_app_server_protocol::DynamicToolSpec as ApiDynamicToolSpec;
|
||||
use codex_app_server_protocol::EphemeralContext as V2EphemeralContext;
|
||||
use codex_app_server_protocol::ExperimentalFeature as ApiExperimentalFeature;
|
||||
use codex_app_server_protocol::ExperimentalFeatureListParams;
|
||||
use codex_app_server_protocol::ExperimentalFeatureListResponse;
|
||||
@@ -4743,8 +4744,19 @@ impl CodexMessageProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_v2_input_limit(items: &[V2UserInput]) -> Result<(), JSONRPCErrorError> {
|
||||
let actual_chars: usize = items.iter().map(V2UserInput::text_char_count).sum();
|
||||
fn validate_v2_input_limit(
|
||||
items: &[V2UserInput],
|
||||
ephemeral_context: Option<&[V2EphemeralContext]>,
|
||||
) -> Result<(), JSONRPCErrorError> {
|
||||
let actual_chars: usize = items
|
||||
.iter()
|
||||
.map(V2UserInput::text_char_count)
|
||||
.sum::<usize>()
|
||||
+ ephemeral_context
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.map(|context| context.title.chars().count() + context.text.chars().count())
|
||||
.sum::<usize>();
|
||||
if actual_chars > MAX_USER_INPUT_TEXT_CHARS {
|
||||
return Err(Self::input_too_large_error(actual_chars));
|
||||
}
|
||||
@@ -5791,7 +5803,9 @@ impl CodexMessageProcessor {
|
||||
params: TurnStartParams,
|
||||
app_server_client_name: Option<String>,
|
||||
) {
|
||||
if let Err(error) = Self::validate_v2_input_limit(¶ms.input) {
|
||||
if let Err(error) =
|
||||
Self::validate_v2_input_limit(¶ms.input, params.ephemeral_context.as_deref())
|
||||
{
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
return;
|
||||
}
|
||||
@@ -5822,6 +5836,12 @@ impl CodexMessageProcessor {
|
||||
.into_iter()
|
||||
.map(V2UserInput::into_core)
|
||||
.collect();
|
||||
let ephemeral_context = params
|
||||
.ephemeral_context
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect();
|
||||
|
||||
let has_any_overrides = params.cwd.is_some()
|
||||
|| params.approval_policy.is_some()
|
||||
@@ -5862,6 +5882,7 @@ impl CodexMessageProcessor {
|
||||
thread.as_ref(),
|
||||
Op::UserInput {
|
||||
items: mapped_items,
|
||||
ephemeral_context,
|
||||
final_output_json_schema: params.output_schema,
|
||||
},
|
||||
)
|
||||
@@ -5921,7 +5942,7 @@ impl CodexMessageProcessor {
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
if let Err(error) = Self::validate_v2_input_limit(¶ms.input) {
|
||||
if let Err(error) = Self::validate_v2_input_limit(¶ms.input, None) {
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -504,6 +504,7 @@ async fn turn_start_jsonrpc_span_parents_core_turn_spans() -> Result<()> {
|
||||
text: "hello".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: None,
|
||||
cwd: None,
|
||||
approval_policy: None,
|
||||
sandbox_policy: None,
|
||||
|
||||
@@ -20,6 +20,7 @@ use codex_app_server_protocol::CollabAgentToolCallStatus;
|
||||
use codex_app_server_protocol::CommandExecutionApprovalDecision;
|
||||
use codex_app_server_protocol::CommandExecutionRequestApprovalResponse;
|
||||
use codex_app_server_protocol::CommandExecutionStatus;
|
||||
use codex_app_server_protocol::EphemeralContext;
|
||||
use codex_app_server_protocol::FileChangeApprovalDecision;
|
||||
use codex_app_server_protocol::FileChangeOutputDeltaNotification;
|
||||
use codex_app_server_protocol::FileChangeRequestApprovalResponse;
|
||||
@@ -239,6 +240,87 @@ async fn turn_start_emits_user_message_item_with_text_elements() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn turn_start_forwards_ephemeral_context_to_model_input() -> Result<()> {
|
||||
let responses = vec![create_final_assistant_message_sse_response("Done")?];
|
||||
let server = create_mock_responses_server_sequence_unchecked(responses).await;
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(
|
||||
codex_home.path(),
|
||||
&server.uri(),
|
||||
"never",
|
||||
&BTreeMap::from([(Feature::Personality, true)]),
|
||||
)?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let thread_req = mcp
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
model: Some("mock-model".to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let thread_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(thread_req)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(thread_resp)?;
|
||||
|
||||
let turn_req = mcp
|
||||
.send_turn_start_request(TurnStartParams {
|
||||
thread_id: thread.id,
|
||||
input: vec![V2UserInput::Text {
|
||||
text: "Hello".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Some(vec![EphemeralContext {
|
||||
title: "Context from my editor".to_string(),
|
||||
text: "## Active file: src/main.rs".to_string(),
|
||||
}]),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(turn_req)),
|
||||
)
|
||||
.await??;
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("turn/completed"),
|
||||
)
|
||||
.await??;
|
||||
|
||||
let requests = server
|
||||
.received_requests()
|
||||
.await
|
||||
.expect("failed to fetch received requests");
|
||||
assert_eq!(requests.len(), 1, "expected one model request");
|
||||
let body = serde_json::from_slice::<serde_json::Value>(&requests[0].body)?;
|
||||
let body_text = body.to_string();
|
||||
assert!(
|
||||
body_text.contains("<additional_context_for_this_turn>"),
|
||||
"expected model request to contain additional_context_for_this_turn wrapper"
|
||||
);
|
||||
assert!(
|
||||
body_text.contains("<title>Context from my editor</title>"),
|
||||
"expected model request to contain ephemeral context title"
|
||||
);
|
||||
assert!(
|
||||
body_text.contains("## Active file: src/main.rs"),
|
||||
"expected model request to contain ephemeral context body"
|
||||
);
|
||||
assert!(
|
||||
body_text.contains("\"Hello\""),
|
||||
"expected model request to include the user text input"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn turn_start_accepts_text_at_limit_with_mention_item() -> Result<()> {
|
||||
let responses = vec![create_final_assistant_message_sse_response("Done")?];
|
||||
@@ -376,6 +458,148 @@ async fn turn_start_rejects_combined_oversized_text_input() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn turn_start_rejects_oversized_ephemeral_context_payload() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(
|
||||
codex_home.path(),
|
||||
"http://localhost/unused",
|
||||
"never",
|
||||
&BTreeMap::from([(Feature::Personality, true)]),
|
||||
)?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let thread_req = mcp
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
model: Some("mock-model".to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let thread_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(thread_req)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(thread_resp)?;
|
||||
|
||||
let title = "ctx".to_string();
|
||||
let text = "x".repeat(MAX_USER_INPUT_TEXT_CHARS - title.chars().count() + 1);
|
||||
let actual_chars = title.chars().count() + text.chars().count();
|
||||
|
||||
let turn_req = mcp
|
||||
.send_turn_start_request(TurnStartParams {
|
||||
thread_id: thread.id,
|
||||
input: vec![],
|
||||
ephemeral_context: Some(vec![EphemeralContext { title, text }]),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let err: JSONRPCError = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_error_message(RequestId::Integer(turn_req)),
|
||||
)
|
||||
.await??;
|
||||
|
||||
assert_eq!(err.error.code, INVALID_PARAMS_ERROR_CODE);
|
||||
assert_eq!(
|
||||
err.error.message,
|
||||
format!("Input exceeds the maximum length of {MAX_USER_INPUT_TEXT_CHARS} characters.")
|
||||
);
|
||||
let data = err.error.data.expect("expected structured error data");
|
||||
assert_eq!(data["input_error_code"], INPUT_TOO_LARGE_ERROR_CODE);
|
||||
assert_eq!(data["max_chars"], MAX_USER_INPUT_TEXT_CHARS);
|
||||
assert_eq!(data["actual_chars"], actual_chars);
|
||||
|
||||
let turn_started = tokio::time::timeout(
|
||||
std::time::Duration::from_millis(250),
|
||||
mcp.read_stream_until_notification_message("turn/started"),
|
||||
)
|
||||
.await;
|
||||
assert!(
|
||||
turn_started.is_err(),
|
||||
"did not expect a turn/started notification for rejected input"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn turn_start_rejects_combined_oversized_input_and_ephemeral_context() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(
|
||||
codex_home.path(),
|
||||
"http://localhost/unused",
|
||||
"never",
|
||||
&BTreeMap::from([(Feature::Personality, true)]),
|
||||
)?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let thread_req = mcp
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
model: Some("mock-model".to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let thread_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(thread_req)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(thread_resp)?;
|
||||
|
||||
let input_text = "x".repeat(MAX_USER_INPUT_TEXT_CHARS / 2);
|
||||
let title = "editor".to_string();
|
||||
let ephemeral_text = "y".repeat(MAX_USER_INPUT_TEXT_CHARS / 2 - title.chars().count() + 1);
|
||||
let actual_chars =
|
||||
input_text.chars().count() + title.chars().count() + ephemeral_text.chars().count();
|
||||
|
||||
let turn_req = mcp
|
||||
.send_turn_start_request(TurnStartParams {
|
||||
thread_id: thread.id,
|
||||
input: vec![V2UserInput::Text {
|
||||
text: input_text,
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Some(vec![EphemeralContext {
|
||||
title,
|
||||
text: ephemeral_text,
|
||||
}]),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let err: JSONRPCError = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_error_message(RequestId::Integer(turn_req)),
|
||||
)
|
||||
.await??;
|
||||
|
||||
assert_eq!(err.error.code, INVALID_PARAMS_ERROR_CODE);
|
||||
assert_eq!(
|
||||
err.error.message,
|
||||
format!("Input exceeds the maximum length of {MAX_USER_INPUT_TEXT_CHARS} characters.")
|
||||
);
|
||||
let data = err.error.data.expect("expected structured error data");
|
||||
assert_eq!(data["input_error_code"], INPUT_TOO_LARGE_ERROR_CODE);
|
||||
assert_eq!(data["max_chars"], MAX_USER_INPUT_TEXT_CHARS);
|
||||
assert_eq!(data["actual_chars"], actual_chars);
|
||||
|
||||
let turn_started = tokio::time::timeout(
|
||||
std::time::Duration::from_millis(250),
|
||||
mcp.read_stream_until_notification_message("turn/started"),
|
||||
)
|
||||
.await;
|
||||
assert!(
|
||||
turn_started.is_err(),
|
||||
"did not expect a turn/started notification for rejected input"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn turn_start_emits_notifications_and_accepts_model_override() -> Result<()> {
|
||||
// Provide a mock server and config so model wiring is valid.
|
||||
@@ -1378,6 +1602,7 @@ async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()> {
|
||||
text: "first turn".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: None,
|
||||
cwd: Some(first_cwd.clone()),
|
||||
approval_policy: Some(codex_app_server_protocol::AskForApproval::Never),
|
||||
sandbox_policy: Some(codex_app_server_protocol::SandboxPolicy::WorkspaceWrite {
|
||||
@@ -1416,6 +1641,7 @@ async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()> {
|
||||
text: "second turn".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: None,
|
||||
cwd: Some(second_cwd.clone()),
|
||||
approval_policy: Some(codex_app_server_protocol::AskForApproval::Never),
|
||||
sandbox_policy: Some(codex_app_server_protocol::SandboxPolicy::DangerFullAccess),
|
||||
|
||||
@@ -305,6 +305,7 @@ impl AgentControl {
|
||||
agent_id,
|
||||
Op::UserInput {
|
||||
items,
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -335,6 +335,7 @@ async fn send_input_submits_user_message() {
|
||||
text: "hello from tests".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
},
|
||||
);
|
||||
@@ -366,6 +367,7 @@ async fn spawn_agent_creates_thread_and_sends_prompt() {
|
||||
text: "spawned".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
},
|
||||
);
|
||||
@@ -442,6 +444,7 @@ async fn spawn_agent_can_fork_parent_thread_history() {
|
||||
text: "child task".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -323,6 +323,7 @@ use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
|
||||
use codex_protocol::protocol::CodexErrorInfo;
|
||||
use codex_protocol::protocol::InitialHistory;
|
||||
use codex_protocol::user_input::EphemeralContext;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_readiness::Readiness;
|
||||
@@ -773,6 +774,7 @@ pub(crate) struct TurnContext {
|
||||
pub(crate) sub_id: String,
|
||||
pub(crate) trace_id: Option<String>,
|
||||
pub(crate) realtime_active: bool,
|
||||
pub(crate) ephemeral_context: Vec<EphemeralContext>,
|
||||
pub(crate) config: Arc<Config>,
|
||||
pub(crate) auth_manager: Option<Arc<AuthManager>>,
|
||||
pub(crate) model_info: ModelInfo,
|
||||
@@ -877,6 +879,7 @@ impl TurnContext {
|
||||
sub_id: self.sub_id.clone(),
|
||||
trace_id: self.trace_id.clone(),
|
||||
realtime_active: self.realtime_active,
|
||||
ephemeral_context: self.ephemeral_context.clone(),
|
||||
config: Arc::new(config),
|
||||
auth_manager: self.auth_manager.clone(),
|
||||
model_info: model_info.clone(),
|
||||
@@ -1308,6 +1311,7 @@ impl Session {
|
||||
sub_id,
|
||||
trace_id: current_span_trace_id(),
|
||||
realtime_active: false,
|
||||
ephemeral_context: Vec::new(),
|
||||
config: per_turn_config.clone(),
|
||||
auth_manager: auth_manager_for_context,
|
||||
model_info: model_info.clone(),
|
||||
@@ -1942,6 +1946,7 @@ impl Session {
|
||||
text,
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
},
|
||||
)
|
||||
@@ -2214,6 +2219,7 @@ impl Session {
|
||||
&self,
|
||||
sub_id: String,
|
||||
updates: SessionSettingsUpdate,
|
||||
ephemeral_context: Vec<EphemeralContext>,
|
||||
) -> ConstraintResult<Arc<TurnContext>> {
|
||||
let (
|
||||
session_configuration,
|
||||
@@ -2274,6 +2280,7 @@ impl Session {
|
||||
session_configuration,
|
||||
updates.final_output_json_schema,
|
||||
sandbox_policy_changed,
|
||||
ephemeral_context,
|
||||
)
|
||||
.await)
|
||||
}
|
||||
@@ -2284,6 +2291,7 @@ impl Session {
|
||||
session_configuration: SessionConfiguration,
|
||||
final_output_json_schema: Option<Option<Value>>,
|
||||
sandbox_policy_changed: bool,
|
||||
ephemeral_context: Vec<EphemeralContext>,
|
||||
) -> Arc<TurnContext> {
|
||||
let per_turn_config = Self::build_per_turn_config(&session_configuration);
|
||||
self.services
|
||||
@@ -2342,6 +2350,7 @@ impl Session {
|
||||
skills_outcome,
|
||||
);
|
||||
turn_context.realtime_active = self.conversation.running_state().await.is_some();
|
||||
turn_context.ephemeral_context = ephemeral_context;
|
||||
|
||||
if let Some(final_schema) = final_output_json_schema {
|
||||
turn_context.final_output_json_schema = final_schema;
|
||||
@@ -2499,7 +2508,7 @@ impl Session {
|
||||
let state = self.state.lock().await;
|
||||
state.session_configuration.clone()
|
||||
};
|
||||
self.new_turn_from_configuration(sub_id, session_configuration, None, false)
|
||||
self.new_turn_from_configuration(sub_id, session_configuration, None, false, Vec::new())
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -4365,7 +4374,7 @@ mod handlers {
|
||||
}
|
||||
|
||||
pub async fn user_input_or_turn(sess: &Arc<Session>, sub_id: String, op: Op) {
|
||||
let (items, updates) = match op {
|
||||
let (items, ephemeral_context, updates) = match op {
|
||||
Op::UserTurn {
|
||||
cwd,
|
||||
approval_policy,
|
||||
@@ -4391,6 +4400,7 @@ mod handlers {
|
||||
});
|
||||
(
|
||||
items,
|
||||
Vec::new(),
|
||||
SessionSettingsUpdate {
|
||||
cwd: Some(cwd),
|
||||
approval_policy: Some(approval_policy),
|
||||
@@ -4407,9 +4417,11 @@ mod handlers {
|
||||
}
|
||||
Op::UserInput {
|
||||
items,
|
||||
ephemeral_context,
|
||||
final_output_json_schema,
|
||||
} => (
|
||||
items,
|
||||
ephemeral_context,
|
||||
SessionSettingsUpdate {
|
||||
final_output_json_schema: Some(final_output_json_schema),
|
||||
..Default::default()
|
||||
@@ -4418,7 +4430,10 @@ mod handlers {
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
let Ok(current_context) = sess.new_turn_with_sub_id(sub_id, updates).await else {
|
||||
let Ok(current_context) = sess
|
||||
.new_turn_with_sub_id(sub_id, updates, ephemeral_context)
|
||||
.await
|
||||
else {
|
||||
// new_turn_with_sub_id already emits the error event.
|
||||
return;
|
||||
};
|
||||
@@ -5222,6 +5237,7 @@ async fn spawn_review_thread(
|
||||
sub_id: review_turn_id,
|
||||
trace_id: current_span_trace_id(),
|
||||
realtime_active: parent_turn_context.realtime_active,
|
||||
ephemeral_context: Vec::new(),
|
||||
config: per_turn_config,
|
||||
auth_manager: auth_manager_for_context,
|
||||
model_info: model_info.clone(),
|
||||
|
||||
@@ -148,6 +148,7 @@ pub(crate) async fn run_codex_thread_one_shot(
|
||||
// Send the initial input to kick off the one-shot turn.
|
||||
io.submit(Op::UserInput {
|
||||
items: input,
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema,
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -761,7 +761,11 @@ async fn resumed_history_injects_initial_context_on_first_context_update_only()
|
||||
session
|
||||
.record_context_updates_and_set_reference_context_item(&turn_context)
|
||||
.await;
|
||||
expected.extend(session.build_initial_context(&turn_context).await);
|
||||
expected.extend(
|
||||
session
|
||||
.build_initial_context_without_reference_context_item(&turn_context)
|
||||
.await,
|
||||
);
|
||||
let history_after_seed = session.clone_history().await;
|
||||
assert_eq!(expected, history_after_seed.raw_items());
|
||||
|
||||
@@ -926,7 +930,7 @@ async fn record_initial_history_reconstructs_forked_transcript() {
|
||||
let reconstruction_turn = session.new_default_turn().await;
|
||||
expected.extend(
|
||||
session
|
||||
.build_initial_context(reconstruction_turn.as_ref())
|
||||
.build_initial_context_without_reference_context_item(reconstruction_turn.as_ref())
|
||||
.await,
|
||||
);
|
||||
let history = session.state.lock().await.clone_history();
|
||||
@@ -3409,7 +3413,9 @@ async fn record_context_updates_and_set_reference_context_item_injects_full_cont
|
||||
.record_context_updates_and_set_reference_context_item(&turn_context)
|
||||
.await;
|
||||
let history = session.clone_history().await;
|
||||
let initial_context = session.build_initial_context(&turn_context).await;
|
||||
let initial_context = session
|
||||
.build_initial_context_without_reference_context_item(&turn_context)
|
||||
.await;
|
||||
assert_eq!(history.raw_items().to_vec(), initial_context);
|
||||
|
||||
let current_context = session.reference_context_item().await;
|
||||
@@ -3453,7 +3459,11 @@ async fn record_context_updates_and_set_reference_context_item_reinjects_full_co
|
||||
|
||||
let history = session.clone_history().await;
|
||||
let mut expected_history = vec![compacted_summary];
|
||||
expected_history.extend(session.build_initial_context(&turn_context).await);
|
||||
expected_history.extend(
|
||||
session
|
||||
.build_initial_context_without_reference_context_item(&turn_context)
|
||||
.await,
|
||||
);
|
||||
assert_eq!(history.raw_items().to_vec(), expected_history);
|
||||
}
|
||||
|
||||
|
||||
@@ -121,6 +121,35 @@ do things
|
||||
assert_eq!(vec!["real user message".to_string()], collected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collect_user_messages_filters_ephemeral_context_entries() {
|
||||
let items = vec![
|
||||
ResponseItem::Message {
|
||||
id: None,
|
||||
role: "user".to_string(),
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "<additional_context_for_this_turn>\n <title>Context from my editor</title>\n <content>\n## Active file: src/main.rs\n </content>\n</additional_context_for_this_turn>"
|
||||
.to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
ResponseItem::Message {
|
||||
id: None,
|
||||
role: "user".to_string(),
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "real user message".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
];
|
||||
|
||||
let collected = collect_user_messages(&items);
|
||||
|
||||
assert_eq!(vec!["real user message".to_string()], collected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_token_limited_compacted_history_truncates_overlong_user_messages() {
|
||||
// Use a small truncation limit so the test remains fast while still validating
|
||||
|
||||
@@ -19,13 +19,24 @@ use tracing::warn;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::model_visible_fragments::is_contextual_user_fragment;
|
||||
use crate::model_visible_fragments::is_ephemeral_context_fragment;
|
||||
use crate::web_search::web_search_action_detail;
|
||||
|
||||
fn strip_ephemeral_context_prefix(message: &[ContentItem]) -> &[ContentItem] {
|
||||
let prefix_len = message
|
||||
.iter()
|
||||
.take_while(|content_item| is_ephemeral_context_fragment(content_item))
|
||||
.count();
|
||||
&message[prefix_len..]
|
||||
}
|
||||
|
||||
pub(crate) fn is_contextual_user_message_content(message: &[ContentItem]) -> bool {
|
||||
message.iter().any(is_contextual_user_fragment)
|
||||
let stripped = strip_ephemeral_context_prefix(message);
|
||||
stripped.is_empty() || stripped.iter().any(is_contextual_user_fragment)
|
||||
}
|
||||
|
||||
fn parse_user_message(message: &[ContentItem]) -> Option<UserMessageItem> {
|
||||
let message = strip_ephemeral_context_prefix(message);
|
||||
if is_contextual_user_message_content(message) {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -218,6 +218,51 @@ fn skips_user_instructions_and_env() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strips_ephemeral_context_prefix_from_user_message() {
|
||||
let item = ResponseItem::Message {
|
||||
id: None,
|
||||
role: "user".to_string(),
|
||||
content: vec![
|
||||
ContentItem::InputText {
|
||||
text: "<additional_context_for_this_turn>\n <title>Context from my editor</title>\n <content>\n## Active file: src/main.rs\n </content>\n</additional_context_for_this_turn>".to_string(),
|
||||
},
|
||||
ContentItem::InputText {
|
||||
text: "Explain this function.".to_string(),
|
||||
},
|
||||
],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
};
|
||||
|
||||
let turn_item = parse_turn_item(&item).expect("expected user message turn item");
|
||||
let TurnItem::UserMessage(user_message) = turn_item else {
|
||||
panic!("expected TurnItem::UserMessage");
|
||||
};
|
||||
assert_eq!(
|
||||
user_message.content,
|
||||
vec![UserInput::Text {
|
||||
text: "Explain this function.".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drops_user_message_when_only_ephemeral_context_prefix_is_present() {
|
||||
let item = ResponseItem::Message {
|
||||
id: None,
|
||||
role: "user".to_string(),
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "<additional_context_for_this_turn>\n <title>Context from my editor</title>\n <content>\n## Active file: src/main.rs\n </content>\n</additional_context_for_this_turn>".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
};
|
||||
|
||||
assert!(parse_turn_item(&item).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_agent_message() {
|
||||
let item = ResponseItem::Message {
|
||||
|
||||
@@ -628,6 +628,7 @@ async fn run_guardian_subagent(
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
items: prompt_items,
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: Some(schema),
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -19,7 +19,9 @@
|
||||
//! - `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(...)`.
|
||||
//! 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.
|
||||
//! `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
|
||||
@@ -187,6 +189,22 @@ pub(crate) trait ModelVisibleContextFragment: Sized {
|
||||
None
|
||||
}
|
||||
|
||||
/// Build zero or more fragments from the current turn state.
|
||||
///
|
||||
/// Most fragments should implement `build(...)` and use this default,
|
||||
/// which lifts the common zero-or-one case into a vector. Override this
|
||||
/// only when one turn-state source intentionally renders multiple
|
||||
/// model-visible content items.
|
||||
fn build_many(
|
||||
turn_context: &TurnContext,
|
||||
reference_context_item: Option<&TurnContextItem>,
|
||||
params: &TurnContextDiffParams<'_>,
|
||||
) -> Vec<Self> {
|
||||
Self::build(turn_context, reference_context_item, params)
|
||||
.into_iter()
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Stable markers used to recognize contextual-user fragments in persisted
|
||||
/// history. Developer fragments should keep the default `None`.
|
||||
fn contextual_user_markers() -> Option<ContextualUserFragmentMarkers> {
|
||||
|
||||
@@ -73,10 +73,13 @@ use codex_protocol::models::developer_realtime_start_text_with_instructions;
|
||||
use codex_protocol::protocol::AgentStatus;
|
||||
use codex_protocol::protocol::ENVIRONMENT_CONTEXT_CLOSE_TAG;
|
||||
use codex_protocol::protocol::ENVIRONMENT_CONTEXT_OPEN_TAG;
|
||||
use codex_protocol::protocol::EPHEMERAL_CONTEXT_CLOSE_TAG;
|
||||
use codex_protocol::protocol::EPHEMERAL_CONTEXT_OPEN_TAG;
|
||||
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;
|
||||
@@ -94,7 +97,7 @@ struct ModelVisibleFragmentRegistration {
|
||||
Option<&TurnContextItem>,
|
||||
&TurnContext,
|
||||
&TurnContextDiffParams<'_>,
|
||||
) -> Option<BuiltTurnStateFragment>,
|
||||
) -> Vec<BuiltTurnStateFragment>,
|
||||
}
|
||||
|
||||
impl ModelVisibleFragmentRegistration {
|
||||
@@ -117,17 +120,19 @@ fn build_registered_turn_state_fragment<F: ModelVisibleContextFragment>(
|
||||
reference_context_item: Option<&TurnContextItem>,
|
||||
turn_context: &TurnContext,
|
||||
params: &TurnContextDiffParams<'_>,
|
||||
) -> Option<BuiltTurnStateFragment> {
|
||||
let fragment = F::build(turn_context, reference_context_item, params)?;
|
||||
match F::Role::MESSAGE_ROLE {
|
||||
MessageRole::Developer => Some(BuiltTurnStateFragment::Developer(
|
||||
DeveloperTextFragment::new(fragment.render_text()),
|
||||
)),
|
||||
MessageRole::User => Some(BuiltTurnStateFragment::ContextualUser(
|
||||
ContextualUserTextFragment::new(fragment.render_text()),
|
||||
)),
|
||||
MessageRole::Assistant | MessageRole::System => None,
|
||||
}
|
||||
) -> Vec<BuiltTurnStateFragment> {
|
||||
F::build_many(turn_context, reference_context_item, params)
|
||||
.into_iter()
|
||||
.filter_map(|fragment| match F::Role::MESSAGE_ROLE {
|
||||
MessageRole::Developer => Some(BuiltTurnStateFragment::Developer(
|
||||
DeveloperTextFragment::new(fragment.render_text()),
|
||||
)),
|
||||
MessageRole::User => Some(BuiltTurnStateFragment::ContextualUser(
|
||||
ContextualUserTextFragment::new(fragment.render_text()),
|
||||
)),
|
||||
MessageRole::Assistant | MessageRole::System => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Canonical ordered registry for all current model-visible fragments.
|
||||
@@ -146,6 +151,7 @@ const REGISTERED_MODEL_VISIBLE_FRAGMENTS: &[ModelVisibleFragmentRegistration] =
|
||||
ModelVisibleFragmentRegistration::of::<SkillsSectionFragment>(),
|
||||
ModelVisibleFragmentRegistration::of::<ChildAgentsInstructionsFragment>(),
|
||||
ModelVisibleFragmentRegistration::of::<EnvironmentContext>(),
|
||||
ModelVisibleFragmentRegistration::of::<EphemeralContext>(),
|
||||
ModelVisibleFragmentRegistration::of::<SkillInstructions>(),
|
||||
ModelVisibleFragmentRegistration::of::<PluginInstructions>(),
|
||||
ModelVisibleFragmentRegistration::of::<UserShellCommandFragment>(),
|
||||
@@ -842,6 +848,32 @@ impl ModelVisibleContextFragment for EnvironmentContext {
|
||||
}
|
||||
}
|
||||
|
||||
impl ModelVisibleContextFragment for EphemeralContext {
|
||||
type Role = ContextualUserContextRole;
|
||||
|
||||
fn render_text(&self) -> String {
|
||||
Self::wrap_contextual_user_body(format!(
|
||||
" <title>{}</title>\n <content>\n{}\n </content>",
|
||||
self.title, self.text
|
||||
))
|
||||
}
|
||||
|
||||
fn build_many(
|
||||
turn_context: &TurnContext,
|
||||
_reference_context_item: Option<&TurnContextItem>,
|
||||
_params: &TurnContextDiffParams<'_>,
|
||||
) -> Vec<Self> {
|
||||
turn_context.ephemeral_context.clone()
|
||||
}
|
||||
|
||||
fn contextual_user_markers() -> Option<ContextualUserFragmentMarkers> {
|
||||
Some(ContextualUserFragmentMarkers::new(
|
||||
EPHEMERAL_CONTEXT_OPEN_TAG,
|
||||
EPHEMERAL_CONTEXT_CLOSE_TAG,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Contextual-user runtime fragments
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1012,6 +1044,13 @@ pub(crate) fn is_contextual_user_fragment(content_item: &ContentItem) -> bool {
|
||||
|| is_legacy_contextual_user_fragment(text)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
pub(crate) fn build_turn_state_fragments(
|
||||
reference_context_item: Option<&TurnContextItem>,
|
||||
turn_context: &TurnContext,
|
||||
@@ -1019,7 +1058,7 @@ pub(crate) fn build_turn_state_fragments(
|
||||
) -> Vec<BuiltTurnStateFragment> {
|
||||
REGISTERED_MODEL_VISIBLE_FRAGMENTS
|
||||
.iter()
|
||||
.filter_map(|registration| {
|
||||
.flat_map(|registration| {
|
||||
(registration.build_turn_state)(reference_context_item, turn_context, params)
|
||||
})
|
||||
.collect()
|
||||
|
||||
@@ -535,6 +535,7 @@ async fn send_input_accepts_structured_items() {
|
||||
text_elements: Vec::new(),
|
||||
},
|
||||
],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
};
|
||||
let captured = manager
|
||||
|
||||
@@ -50,6 +50,7 @@ async fn interrupt_long_running_tool_emits_turn_aborted() {
|
||||
text: "start sleep".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -104,6 +105,7 @@ async fn interrupt_tool_records_history_entries() {
|
||||
text: "start history recording".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -122,6 +124,7 @@ async fn interrupt_tool_records_history_entries() {
|
||||
text: "follow up".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -202,6 +205,7 @@ async fn interrupt_persists_turn_aborted_marker_in_next_request() {
|
||||
text: "start interrupt marker".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -220,6 +224,7 @@ async fn interrupt_persists_turn_aborted_marker_in_next_request() {
|
||||
text: "follow up".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
|
||||
@@ -276,6 +276,7 @@ async fn resume_includes_initial_messages_and_sends_prior_items() {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -634,6 +635,7 @@ async fn includes_conversation_id_and_model_headers_in_request() {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -682,6 +684,7 @@ async fn includes_base_instructions_override_in_request() {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -733,6 +736,7 @@ async fn chatgpt_auth_sends_correct_request() {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -834,6 +838,7 @@ async fn prefers_apikey_when_config_prefers_apikey_even_with_chatgpt_tokens() {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -870,6 +875,7 @@ async fn includes_user_instructions_message_in_request() {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -958,6 +964,7 @@ async fn includes_apps_guidance_as_developer_message_for_chatgpt_auth() {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -1049,6 +1056,7 @@ async fn omits_apps_guidance_for_api_key_auth_even_when_feature_enabled() {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -1118,6 +1126,7 @@ async fn skills_append_to_instructions() {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -1175,6 +1184,7 @@ async fn includes_configured_effort_in_request() -> anyhow::Result<()> {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -1217,6 +1227,7 @@ async fn includes_no_effort_in_request() -> anyhow::Result<()> {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -1257,6 +1268,7 @@ async fn includes_default_reasoning_effort_in_request_when_defined_by_model_info
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -1368,6 +1380,7 @@ async fn configured_reasoning_summary_is_sent() -> anyhow::Result<()> {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -1482,6 +1495,7 @@ async fn reasoning_summary_is_omitted_when_disabled() -> anyhow::Result<()> {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -1538,6 +1552,7 @@ async fn reasoning_summary_none_overrides_model_catalog_default() -> anyhow::Res
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -1574,6 +1589,7 @@ async fn includes_default_verbosity_in_request() -> anyhow::Result<()> {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -1619,6 +1635,7 @@ async fn configured_verbosity_not_sent_for_models_without_support() -> anyhow::R
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -1663,6 +1680,7 @@ async fn configured_verbosity_is_sent() -> anyhow::Result<()> {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -1712,6 +1730,7 @@ async fn includes_developer_instructions_message_in_request() {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -1993,6 +2012,7 @@ async fn token_count_includes_rate_limits_snapshot() {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -2158,6 +2178,7 @@ async fn usage_limit_error_emits_rate_limit_event() -> anyhow::Result<()> {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -2232,6 +2253,7 @@ async fn context_window_error_sets_total_tokens_to_model_window() -> anyhow::Res
|
||||
text: "seed turn".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -2244,6 +2266,7 @@ async fn context_window_error_sets_total_tokens_to_model_window() -> anyhow::Res
|
||||
text: "trigger context window".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -2326,6 +2349,7 @@ async fn incomplete_response_emits_content_filter_error_message() -> anyhow::Res
|
||||
text: "trigger incomplete".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -2423,6 +2447,7 @@ async fn azure_overrides_assign_properties_used_for_responses_url() {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -2507,6 +2532,7 @@ async fn env_var_overrides_loaded_auth() {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -2568,6 +2594,7 @@ async fn history_dedupes_streamed_and_final_messages_across_turns() {
|
||||
text: "U1".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -2581,6 +2608,7 @@ async fn history_dedupes_streamed_and_final_messages_across_turns() {
|
||||
text: "U2".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -2594,6 +2622,7 @@ async fn history_dedupes_streamed_and_final_messages_across_turns() {
|
||||
text: "U3".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
|
||||
@@ -836,6 +836,7 @@ async fn responses_websocket_usage_limit_error_emits_rate_limit_event() {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -921,6 +922,7 @@ async fn responses_websocket_invalid_request_error_with_status_is_forwarded() {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
|
||||
@@ -76,6 +76,7 @@ async fn no_collaboration_instructions_by_default() -> Result<()> {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -125,6 +126,7 @@ async fn user_input_includes_collaboration_instructions_after_override() -> Resu
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -221,6 +223,7 @@ async fn override_then_next_turn_uses_updated_collaboration_instructions() -> Re
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -341,6 +344,7 @@ async fn collaboration_mode_update_emits_new_instruction_message() -> Result<()>
|
||||
text: "hello 1".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -367,6 +371,7 @@ async fn collaboration_mode_update_emits_new_instruction_message() -> Result<()>
|
||||
text: "hello 2".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -422,6 +427,7 @@ async fn collaboration_mode_update_noop_does_not_append() -> Result<()> {
|
||||
text: "hello 1".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -448,6 +454,7 @@ async fn collaboration_mode_update_noop_does_not_append() -> Result<()> {
|
||||
text: "hello 2".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -505,6 +512,7 @@ async fn collaboration_mode_update_emits_new_instruction_message_when_mode_chang
|
||||
text: "hello 1".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -534,6 +542,7 @@ async fn collaboration_mode_update_emits_new_instruction_message_when_mode_chang
|
||||
text: "hello 2".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -592,6 +601,7 @@ async fn collaboration_mode_update_noop_does_not_append_when_mode_is_unchanged()
|
||||
text: "hello 1".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -621,6 +631,7 @@ async fn collaboration_mode_update_noop_does_not_append_when_mode_is_unchanged()
|
||||
text: "hello 2".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -683,6 +694,7 @@ async fn resume_replays_collaboration_instructions() -> Result<()> {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -696,6 +708,7 @@ async fn resume_replays_collaboration_instructions() -> Result<()> {
|
||||
text: "after resume".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -751,6 +764,7 @@ async fn empty_collaboration_instructions_are_ignored() -> Result<()> {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -17,6 +17,7 @@ use codex_protocol::protocol::RolloutItem;
|
||||
use codex_protocol::protocol::RolloutLine;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_protocol::protocol::WarningEvent;
|
||||
use codex_protocol::user_input::EphemeralContext;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use core_test_support::context_snapshot;
|
||||
use core_test_support::context_snapshot::ContextSnapshotOptions;
|
||||
@@ -81,6 +82,13 @@ fn set_test_compact_prompt(config: &mut Config) {
|
||||
config.compact_prompt = Some(SUMMARIZATION_PROMPT.to_string());
|
||||
}
|
||||
|
||||
fn editor_context(path: &str) -> Vec<EphemeralContext> {
|
||||
vec![EphemeralContext {
|
||||
title: "Context from my editor".to_string(),
|
||||
text: format!("## Active file: {path}"),
|
||||
}]
|
||||
}
|
||||
|
||||
fn body_contains_text(body: &str, text: &str) -> bool {
|
||||
body.contains(&json_fragment(text))
|
||||
}
|
||||
@@ -239,6 +247,7 @@ async fn summarize_context_three_requests_and_instructions() {
|
||||
text: "hello world".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -261,6 +270,7 @@ async fn summarize_context_three_requests_and_instructions() {
|
||||
text: THIRD_USER_MSG.into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -437,6 +447,7 @@ async fn manual_compact_uses_custom_prompt() {
|
||||
text: "USER_ONE".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -581,6 +592,7 @@ async fn manual_compact_emits_context_compaction_items() {
|
||||
text: "manual compact".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -744,6 +756,7 @@ async fn multiple_auto_compact_per_task_runs_after_token_limit_hit() {
|
||||
text: user_message.into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -1248,6 +1261,7 @@ async fn auto_compact_runs_after_token_limit_hit() {
|
||||
text: FIRST_AUTO_MSG.into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -1261,6 +1275,7 @@ async fn auto_compact_runs_after_token_limit_hit() {
|
||||
text: SECOND_AUTO_MSG.into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -1274,6 +1289,7 @@ async fn auto_compact_runs_after_token_limit_hit() {
|
||||
text: POST_AUTO_USER_MSG.into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -1442,6 +1458,7 @@ async fn auto_compact_emits_context_compaction_items() {
|
||||
text: user.into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -1520,6 +1537,7 @@ async fn auto_compact_starts_after_turn_started() {
|
||||
text: FIRST_AUTO_MSG.into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -1532,6 +1550,7 @@ async fn auto_compact_starts_after_turn_started() {
|
||||
text: SECOND_AUTO_MSG.into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -1544,6 +1563,7 @@ async fn auto_compact_starts_after_turn_started() {
|
||||
text: POST_AUTO_USER_MSG.into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -2031,6 +2051,7 @@ async fn auto_compact_persists_rollout_entries() {
|
||||
text: FIRST_AUTO_MSG.into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -2043,6 +2064,7 @@ async fn auto_compact_persists_rollout_entries() {
|
||||
text: SECOND_AUTO_MSG.into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -2055,6 +2077,7 @@ async fn auto_compact_persists_rollout_entries() {
|
||||
text: POST_AUTO_USER_MSG.into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -2141,6 +2164,7 @@ async fn manual_compact_retries_after_context_window_error() {
|
||||
text: "first turn".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -2252,6 +2276,7 @@ async fn manual_compact_non_context_failure_retries_then_emits_task_error() {
|
||||
text: "first turn".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -2344,6 +2369,7 @@ async fn manual_compact_twice_preserves_latest_user_messages() {
|
||||
text: first_user_message.into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -2359,6 +2385,7 @@ async fn manual_compact_twice_preserves_latest_user_messages() {
|
||||
text: second_user_message.into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -2374,6 +2401,7 @@ async fn manual_compact_twice_preserves_latest_user_messages() {
|
||||
text: final_user_message.into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -2535,6 +2563,7 @@ async fn auto_compact_allows_multiple_attempts_when_interleaved_with_other_turn_
|
||||
text: user.into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -2637,6 +2666,7 @@ async fn snapshot_request_shape_mid_turn_continuation_compaction() {
|
||||
text: FUNCTION_CALL_LIMIT_MSG.into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -2696,6 +2726,125 @@ async fn snapshot_request_shape_mid_turn_continuation_compaction() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn snapshot_request_shape_mid_turn_compaction_replaces_ephemeral_context() {
|
||||
skip_if_no_network!();
|
||||
|
||||
let server = start_mock_server().await;
|
||||
|
||||
let context_window = 100;
|
||||
let limit = context_window * 90 / 100;
|
||||
let over_limit_tokens = context_window * 95 / 100 + 1;
|
||||
|
||||
let request_log = mount_sse_sequence(
|
||||
&server,
|
||||
vec![
|
||||
sse(vec![
|
||||
ev_assistant_message("m1", FIRST_REPLY),
|
||||
ev_completed_with_tokens("r1", 60),
|
||||
]),
|
||||
sse(vec![
|
||||
ev_function_call(DUMMY_CALL_ID, DUMMY_FUNCTION_NAME, "{}"),
|
||||
ev_completed_with_tokens("r2", over_limit_tokens),
|
||||
]),
|
||||
sse(vec![
|
||||
ev_assistant_message("m3", &auto_summary(AUTO_SUMMARY_TEXT)),
|
||||
ev_completed_with_tokens("r3", 10),
|
||||
]),
|
||||
sse(vec![
|
||||
ev_assistant_message("m4", FINAL_REPLY),
|
||||
ev_completed_with_tokens("r4", 10),
|
||||
]),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
|
||||
let model_provider = non_openai_model_provider(&server);
|
||||
let codex = test_codex()
|
||||
.with_config(move |config| {
|
||||
config.model_provider = model_provider;
|
||||
set_test_compact_prompt(config);
|
||||
config.model_context_window = Some(context_window);
|
||||
config.model_auto_compact_token_limit = Some(limit);
|
||||
})
|
||||
.build(&server)
|
||||
.await
|
||||
.expect("build codex")
|
||||
.codex;
|
||||
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
items: vec![UserInput::Text {
|
||||
text: "SETUP_USER".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: editor_context("src/old.rs"),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
.expect("submit setup user input");
|
||||
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
|
||||
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
items: vec![UserInput::Text {
|
||||
text: FUNCTION_CALL_LIMIT_MSG.to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: editor_context("src/new.rs"),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
.expect("submit active user input");
|
||||
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
|
||||
|
||||
let requests = request_log.requests();
|
||||
assert_eq!(
|
||||
requests.len(),
|
||||
4,
|
||||
"expected setup turn, active turn, compact, and continuation requests"
|
||||
);
|
||||
|
||||
insta::assert_snapshot!(
|
||||
"mid_turn_compaction_replaces_ephemeral_context_shapes",
|
||||
format_labeled_requests_snapshot(
|
||||
"Mid-turn continuation compaction keeps both prior and active ephemeral_context in the compact request, then reinjects only the active turn ephemeral_context into the continuation request.",
|
||||
&[
|
||||
("Local Compaction Request", &requests[2]),
|
||||
("Local Post-Compaction History Layout", &requests[3]),
|
||||
]
|
||||
)
|
||||
);
|
||||
|
||||
let compact_user_texts = requests[2].message_input_texts("user");
|
||||
assert!(
|
||||
compact_user_texts
|
||||
.iter()
|
||||
.any(|text| text.contains("src/old.rs")),
|
||||
"expected compact request to include earlier turn ephemeral context from history"
|
||||
);
|
||||
assert!(
|
||||
compact_user_texts
|
||||
.iter()
|
||||
.any(|text| text.contains("src/new.rs")),
|
||||
"expected compact request to include the active turn ephemeral context"
|
||||
);
|
||||
|
||||
let follow_up_user_texts = requests[3].message_input_texts("user");
|
||||
assert!(
|
||||
follow_up_user_texts
|
||||
.iter()
|
||||
.any(|text| text.contains("src/new.rs")),
|
||||
"expected continuation request to reinject the active turn ephemeral context"
|
||||
);
|
||||
assert!(
|
||||
!follow_up_user_texts
|
||||
.iter()
|
||||
.any(|text| text.contains("src/old.rs")),
|
||||
"did not expect continuation request to retain the compacted-away earlier ephemeral context"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn auto_compact_clamps_config_limit_to_context_window() {
|
||||
skip_if_no_network!();
|
||||
@@ -2831,6 +2980,7 @@ async fn auto_compact_counts_encrypted_reasoning_before_last_user() {
|
||||
text: user.into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -2948,6 +3098,7 @@ async fn auto_compact_runs_when_reasoning_header_clears_between_turns() {
|
||||
text: user.into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -3007,6 +3158,7 @@ async fn snapshot_request_shape_pre_turn_compaction_including_incoming_user_mess
|
||||
text: user.to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -3041,6 +3193,7 @@ async fn snapshot_request_shape_pre_turn_compaction_including_incoming_user_mess
|
||||
text_elements: Vec::new(),
|
||||
},
|
||||
],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -3081,6 +3234,122 @@ async fn snapshot_request_shape_pre_turn_compaction_including_incoming_user_mess
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn snapshot_request_shape_pre_turn_compaction_replaces_ephemeral_context() {
|
||||
skip_if_no_network!();
|
||||
|
||||
let server = start_mock_server().await;
|
||||
|
||||
let request_log = mount_sse_sequence(
|
||||
&server,
|
||||
vec![
|
||||
sse(vec![
|
||||
ev_assistant_message("m1", FIRST_REPLY),
|
||||
ev_completed_with_tokens("r1", 60),
|
||||
]),
|
||||
sse(vec![
|
||||
ev_assistant_message("m2", "SECOND_REPLY"),
|
||||
ev_completed_with_tokens("r2", 500),
|
||||
]),
|
||||
sse(vec![
|
||||
ev_assistant_message("m3", "PRE_TURN_SUMMARY"),
|
||||
ev_completed_with_tokens("r3", 100),
|
||||
]),
|
||||
sse(vec![
|
||||
ev_assistant_message("m4", FINAL_REPLY),
|
||||
ev_completed_with_tokens("r4", 80),
|
||||
]),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
|
||||
let model_provider = non_openai_model_provider(&server);
|
||||
let codex = test_codex()
|
||||
.with_config(move |config| {
|
||||
config.model_provider = model_provider;
|
||||
set_test_compact_prompt(config);
|
||||
config.model_auto_compact_token_limit = Some(200);
|
||||
})
|
||||
.build(&server)
|
||||
.await
|
||||
.expect("build codex")
|
||||
.codex;
|
||||
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
items: vec![UserInput::Text {
|
||||
text: "USER_ONE".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: editor_context("src/old.rs"),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
.expect("submit first user input");
|
||||
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
|
||||
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
items: vec![UserInput::Text {
|
||||
text: "USER_TWO".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
.expect("submit second user input");
|
||||
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
|
||||
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
items: vec![UserInput::Text {
|
||||
text: "USER_THREE".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: editor_context("src/new.rs"),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
.expect("submit third user input");
|
||||
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
|
||||
|
||||
let requests = request_log.requests();
|
||||
assert_eq!(requests.len(), 4, "expected user, user, compact, follow-up");
|
||||
|
||||
insta::assert_snapshot!(
|
||||
"pre_turn_compaction_replaces_ephemeral_context_shapes",
|
||||
format_labeled_requests_snapshot(
|
||||
"Pre-turn auto-compaction keeps prior turn ephemeral_context in the compact request, but the follow-up request carries only the fresh turn ephemeral_context.",
|
||||
&[
|
||||
("Local Compaction Request", &requests[2]),
|
||||
("Local Post-Compaction History Layout", &requests[3]),
|
||||
]
|
||||
)
|
||||
);
|
||||
|
||||
let compact_user_texts = requests[2].message_input_texts("user");
|
||||
assert!(
|
||||
compact_user_texts
|
||||
.iter()
|
||||
.any(|text| text.contains("src/old.rs")),
|
||||
"expected compact request to include the previous turn ephemeral context"
|
||||
);
|
||||
let follow_up_user_texts = requests[3].message_input_texts("user");
|
||||
assert!(
|
||||
follow_up_user_texts
|
||||
.iter()
|
||||
.any(|text| text.contains("src/new.rs")),
|
||||
"expected follow-up request to include the incoming turn ephemeral context"
|
||||
);
|
||||
assert!(
|
||||
!follow_up_user_texts
|
||||
.iter()
|
||||
.any(|text| text.contains("src/old.rs")),
|
||||
"did not expect follow-up request to retain the compacted-away ephemeral context"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
// TODO(ccunningham): Update once pre-turn compaction context-overflow handling includes incoming
|
||||
// user input and emits richer oversized-input messaging.
|
||||
@@ -3251,6 +3520,7 @@ async fn snapshot_request_shape_pre_turn_compaction_context_window_exceeded() {
|
||||
text: "USER_ONE".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -3263,6 +3533,7 @@ async fn snapshot_request_shape_pre_turn_compaction_context_window_exceeded() {
|
||||
text: "USER_TWO".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -3333,6 +3604,7 @@ async fn snapshot_request_shape_manual_compact_without_previous_user_messages()
|
||||
text: "AFTER_MANUAL_EMPTY_COMPACT".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
|
||||
@@ -19,6 +19,7 @@ use codex_protocol::protocol::RealtimeConversationRealtimeEvent;
|
||||
use codex_protocol::protocol::RealtimeEvent;
|
||||
use codex_protocol::protocol::RolloutItem;
|
||||
use codex_protocol::protocol::RolloutLine;
|
||||
use codex_protocol::user_input::EphemeralContext;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use core_test_support::context_snapshot;
|
||||
use core_test_support::context_snapshot::ContextSnapshotOptions;
|
||||
@@ -59,6 +60,13 @@ fn summary_with_prefix(summary: &str) -> String {
|
||||
format!("{SUMMARY_PREFIX}\n{summary}")
|
||||
}
|
||||
|
||||
fn editor_context(path: &str) -> Vec<EphemeralContext> {
|
||||
vec![EphemeralContext {
|
||||
title: "Context from my editor".to_string(),
|
||||
text: format!("## Active file: {path}"),
|
||||
}]
|
||||
}
|
||||
|
||||
fn context_snapshot_options() -> ContextSnapshotOptions {
|
||||
ContextSnapshotOptions::default()
|
||||
.render_mode(ContextSnapshotRenderMode::KindWithTextPrefix { max_chars: 64 })
|
||||
@@ -233,6 +241,7 @@ async fn remote_compact_replaces_history_for_followups() -> Result<()> {
|
||||
text: "hello remote compact".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -247,6 +256,7 @@ async fn remote_compact_replaces_history_for_followups() -> Result<()> {
|
||||
text: "after compact".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -377,6 +387,7 @@ async fn remote_compact_runs_automatically() -> Result<()> {
|
||||
text: "hello remote compact".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -451,6 +462,7 @@ async fn remote_compact_trims_function_call_history_to_fit_context_window() -> R
|
||||
text: first_user_message.into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -462,6 +474,7 @@ async fn remote_compact_trims_function_call_history_to_fit_context_window() -> R
|
||||
text: second_user_message.into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -571,6 +584,7 @@ async fn auto_remote_compact_trims_function_call_history_to_fit_context_window()
|
||||
text: first_user_message.into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -582,6 +596,7 @@ async fn auto_remote_compact_trims_function_call_history_to_fit_context_window()
|
||||
text: second_user_message.into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -599,6 +614,7 @@ async fn auto_remote_compact_trims_function_call_history_to_fit_context_window()
|
||||
text: "turn that triggers auto compact".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -696,6 +712,7 @@ async fn auto_remote_compact_failure_stops_agent_loop() -> Result<()> {
|
||||
text: "turn that exceeds token threshold".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -707,6 +724,7 @@ async fn auto_remote_compact_failure_stops_agent_loop() -> Result<()> {
|
||||
text: "turn that triggers auto compact".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -798,6 +816,7 @@ async fn remote_compact_trim_estimate_uses_session_base_instructions() -> Result
|
||||
text: first_user_message.into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -812,6 +831,7 @@ async fn remote_compact_trim_estimate_uses_session_base_instructions() -> Result
|
||||
text: second_user_message.into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -897,6 +917,7 @@ async fn remote_compact_trim_estimate_uses_session_base_instructions() -> Result
|
||||
text: first_user_message.into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -911,6 +932,7 @@ async fn remote_compact_trim_estimate_uses_session_base_instructions() -> Result
|
||||
text: second_user_message.into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -979,6 +1001,7 @@ async fn remote_manual_compact_emits_context_compaction_items() -> Result<()> {
|
||||
text: "manual remote compact".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -1057,6 +1080,7 @@ async fn remote_manual_compact_failure_emits_task_error_event() -> Result<()> {
|
||||
text: "manual remote compact".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -1139,6 +1163,7 @@ async fn remote_compact_persists_replacement_history_in_rollout() -> Result<()>
|
||||
text: "needs compaction".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -1280,6 +1305,7 @@ async fn remote_compact_and_resume_refresh_stale_developer_instructions() -> Res
|
||||
text: "start remote compact flow".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -1295,6 +1321,7 @@ async fn remote_compact_and_resume_refresh_stale_developer_instructions() -> Res
|
||||
text: "after compact in same session".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -1317,6 +1344,7 @@ async fn remote_compact_and_resume_refresh_stale_developer_instructions() -> Res
|
||||
text: "after resume".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -1411,6 +1439,7 @@ async fn remote_compact_refreshes_stale_developer_instructions_without_resume()
|
||||
text: "start remote compact flow".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -1425,6 +1454,7 @@ async fn remote_compact_refreshes_stale_developer_instructions_without_resume()
|
||||
text: "after compact in same session".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -1494,6 +1524,7 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_restates_realtime_sta
|
||||
text: "USER_ONE".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -1505,6 +1536,7 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_restates_realtime_sta
|
||||
text: "USER_TWO".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -1569,6 +1601,7 @@ async fn remote_request_uses_custom_experimental_realtime_start_instructions() -
|
||||
text: "USER_ONE".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -1627,6 +1660,7 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_restates_realtime_end
|
||||
text: "USER_ONE".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -1640,6 +1674,7 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_restates_realtime_end
|
||||
text: "USER_TWO".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -1712,6 +1747,7 @@ async fn snapshot_request_shape_remote_manual_compact_restates_realtime_start()
|
||||
text: "USER_ONE".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -1726,6 +1762,7 @@ async fn snapshot_request_shape_remote_manual_compact_restates_realtime_start()
|
||||
text: "USER_TWO".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -1806,6 +1843,7 @@ async fn snapshot_request_shape_remote_mid_turn_compaction_does_not_restate_real
|
||||
text: "SETUP_USER".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -1819,6 +1857,7 @@ async fn snapshot_request_shape_remote_mid_turn_compaction_does_not_restate_real
|
||||
text: "USER_TWO".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -1907,6 +1946,7 @@ async fn snapshot_request_shape_remote_compact_resume_restates_realtime_end() ->
|
||||
text: "USER_ONE".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -1934,6 +1974,7 @@ async fn snapshot_request_shape_remote_compact_resume_restates_realtime_end() ->
|
||||
text: "USER_TWO".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -2026,6 +2067,7 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_including_incoming_us
|
||||
text: user.to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -2110,6 +2152,7 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_strips_incoming_model
|
||||
text: "BEFORE_SWITCH_USER".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -2135,6 +2178,7 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_strips_incoming_model
|
||||
text: "AFTER_SWITCH_USER".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -2251,6 +2295,7 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_context_window_exceed
|
||||
text: "USER_ONE".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -2262,6 +2307,7 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_context_window_exceed
|
||||
text: "USER_TWO".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -2344,6 +2390,7 @@ async fn snapshot_request_shape_remote_mid_turn_continuation_compaction() -> Res
|
||||
text: "USER_ONE".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -2372,6 +2419,120 @@ async fn snapshot_request_shape_remote_mid_turn_continuation_compaction() -> Res
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn snapshot_request_shape_remote_mid_turn_compaction_replaces_ephemeral_context() -> Result<()>
|
||||
{
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let harness = TestCodexHarness::with_builder(
|
||||
test_codex()
|
||||
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
|
||||
.with_config(|config| {
|
||||
config.model_auto_compact_token_limit = Some(200);
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
let codex = harness.test().codex.clone();
|
||||
|
||||
let responses_mock = responses::mount_sse_sequence(
|
||||
harness.server(),
|
||||
vec![
|
||||
responses::sse(vec![
|
||||
responses::ev_assistant_message("m1", "REMOTE_SETUP_REPLY"),
|
||||
responses::ev_completed_with_tokens("r1", 60),
|
||||
]),
|
||||
responses::sse(vec![
|
||||
responses::ev_function_call("call-remote-mid-turn", DUMMY_FUNCTION_NAME, "{}"),
|
||||
responses::ev_completed_with_tokens("r2", 500),
|
||||
]),
|
||||
responses::sse(vec![
|
||||
responses::ev_assistant_message("m3", "REMOTE_MID_TURN_FINAL_REPLY"),
|
||||
responses::ev_completed_with_tokens("r3", 80),
|
||||
]),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
|
||||
let compact_mock = responses::mount_compact_user_history_with_summary_once(
|
||||
harness.server(),
|
||||
&summary_with_prefix("REMOTE_MID_TURN_SUMMARY"),
|
||||
)
|
||||
.await;
|
||||
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
items: vec![UserInput::Text {
|
||||
text: "SETUP_USER".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: editor_context("src/old.rs"),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
|
||||
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
items: vec![UserInput::Text {
|
||||
text: "USER_TWO".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: editor_context("src/new.rs"),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
|
||||
|
||||
assert_eq!(compact_mock.requests().len(), 1);
|
||||
let requests = responses_mock.requests();
|
||||
assert_eq!(
|
||||
requests.len(),
|
||||
3,
|
||||
"expected setup turn, active turn, and continuation requests"
|
||||
);
|
||||
|
||||
let compact_request = compact_mock.single_request();
|
||||
let post_compact_request = &requests[2];
|
||||
insta::assert_snapshot!(
|
||||
"remote_mid_turn_compaction_replaces_ephemeral_context_shapes",
|
||||
format_labeled_requests_snapshot(
|
||||
"Remote mid-turn continuation compaction keeps both prior and active ephemeral_context in the compact request, then reinjects only the active turn ephemeral_context into the continuation request.",
|
||||
&[
|
||||
("Remote Compaction Request", &compact_request),
|
||||
(
|
||||
"Remote Post-Compaction History Layout",
|
||||
post_compact_request
|
||||
),
|
||||
]
|
||||
)
|
||||
);
|
||||
|
||||
assert!(
|
||||
compact_request.body_contains_text("src/old.rs"),
|
||||
"expected remote compact request to include earlier turn ephemeral context from history"
|
||||
);
|
||||
assert!(
|
||||
compact_request.body_contains_text("src/new.rs"),
|
||||
"expected remote compact request to include the active turn ephemeral context"
|
||||
);
|
||||
|
||||
let follow_up_user_texts = post_compact_request.message_input_texts("user");
|
||||
assert!(
|
||||
follow_up_user_texts
|
||||
.iter()
|
||||
.any(|text| text.contains("src/new.rs")),
|
||||
"expected remote continuation request to reinject the active turn ephemeral context"
|
||||
);
|
||||
assert!(
|
||||
!follow_up_user_texts
|
||||
.iter()
|
||||
.any(|text| text.contains("src/old.rs")),
|
||||
"did not expect remote continuation request to retain the compacted-away earlier ephemeral context"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn snapshot_request_shape_remote_mid_turn_compaction_summary_only_reinjects_context()
|
||||
-> Result<()> {
|
||||
@@ -2419,6 +2580,7 @@ async fn snapshot_request_shape_remote_mid_turn_compaction_summary_only_reinject
|
||||
text: "USER_ONE".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -2502,6 +2664,7 @@ async fn snapshot_request_shape_remote_mid_turn_compaction_multi_summary_reinjec
|
||||
text: "USER_ONE".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -2516,6 +2679,7 @@ async fn snapshot_request_shape_remote_mid_turn_compaction_multi_summary_reinjec
|
||||
text: "USER_TWO".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -2595,6 +2759,7 @@ async fn snapshot_request_shape_remote_manual_compact_without_previous_user_mess
|
||||
text: "USER_ONE".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -649,6 +649,7 @@ async fn user_turn(conversation: &Arc<CodexThread>, text: &str) {
|
||||
text: text.into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
|
||||
@@ -51,6 +51,7 @@ async fn fork_thread_twice_drops_to_first_message() {
|
||||
text: text.to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
|
||||
@@ -59,6 +59,7 @@ async fn user_message_item_is_emitted() -> anyhow::Result<()> {
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
items: vec![expected_input.clone()],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -115,6 +116,7 @@ async fn assistant_message_item_is_emitted() -> anyhow::Result<()> {
|
||||
text: "please summarize results".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -173,6 +175,7 @@ async fn reasoning_item_is_emitted() -> anyhow::Result<()> {
|
||||
text: "explain your reasoning".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -232,6 +235,7 @@ async fn web_search_item_is_emitted() -> anyhow::Result<()> {
|
||||
text: "find the weather".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -287,6 +291,7 @@ async fn image_generation_call_event_is_emitted() -> anyhow::Result<()> {
|
||||
text: "generate a tiny blue square".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -340,6 +345,7 @@ async fn image_generation_call_event_is_emitted_when_image_save_fails() -> anyho
|
||||
text: "generate an image".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -393,6 +399,7 @@ async fn agent_message_content_delta_has_item_metadata() -> anyhow::Result<()> {
|
||||
text: "please stream text".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -1035,6 +1042,7 @@ async fn reasoning_content_delta_has_item_metadata() -> anyhow::Result<()> {
|
||||
text: "reason through it".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -1094,6 +1102,7 @@ async fn reasoning_raw_content_delta_respects_flag() -> anyhow::Result<()> {
|
||||
text: "show raw reasoning".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -10,6 +10,7 @@ use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::Op;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_protocol::user_input::EphemeralContext;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use core_test_support::context_snapshot;
|
||||
use core_test_support::context_snapshot::ContextSnapshotOptions;
|
||||
@@ -149,6 +150,85 @@ async fn snapshot_model_visible_layout_turn_overrides() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn snapshot_model_visible_layout_ephemeral_context() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let responses = mount_sse_sequence(
|
||||
&server,
|
||||
vec![
|
||||
sse(vec![
|
||||
ev_response_created("resp-1"),
|
||||
ev_assistant_message("msg-1", "turn one complete"),
|
||||
ev_completed("resp-1"),
|
||||
]),
|
||||
sse(vec![
|
||||
ev_response_created("resp-2"),
|
||||
ev_assistant_message("msg-2", "turn two complete"),
|
||||
ev_completed("resp-2"),
|
||||
]),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
|
||||
let test = test_codex()
|
||||
.with_model("gpt-5.2-codex")
|
||||
.build(&server)
|
||||
.await?;
|
||||
|
||||
test.codex
|
||||
.submit(Op::UserInput {
|
||||
items: vec![UserInput::Text {
|
||||
text: "first turn with editor context".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: vec![EphemeralContext {
|
||||
title: "Context from my editor".to_string(),
|
||||
text: "## Active file: src/one.rs".to_string(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
wait_for_event(&test.codex, |event| {
|
||||
matches!(event, EventMsg::TurnComplete(_))
|
||||
})
|
||||
.await;
|
||||
|
||||
test.codex
|
||||
.submit(Op::UserInput {
|
||||
items: vec![UserInput::Text {
|
||||
text: "second turn with refreshed editor context".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: vec![EphemeralContext {
|
||||
title: "Context from my editor".to_string(),
|
||||
text: "## Active file: src/two.rs".to_string(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
wait_for_event(&test.codex, |event| {
|
||||
matches!(event, EventMsg::TurnComplete(_))
|
||||
})
|
||||
.await;
|
||||
|
||||
let requests = responses.requests();
|
||||
assert_eq!(requests.len(), 2, "expected two requests");
|
||||
insta::assert_snapshot!(
|
||||
"model_visible_layout_ephemeral_context",
|
||||
format_labeled_requests_snapshot(
|
||||
"Turns resend fresh ephemeral editor context while keeping it outside durable turn-state diffs.",
|
||||
&[
|
||||
("First Request (With Editor Context)", &requests[0]),
|
||||
("Second Request (Refreshed Editor Context)", &requests[1]),
|
||||
]
|
||||
)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn snapshot_model_visible_layout_cwd_change_refreshes_agents() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
@@ -290,6 +370,7 @@ async fn snapshot_model_visible_layout_resume_with_personality_change() -> Resul
|
||||
text: "seed resume history".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -388,6 +469,7 @@ async fn snapshot_model_visible_layout_resume_override_matches_rollout_model() -
|
||||
text: "seed resume history".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -432,6 +514,7 @@ async fn snapshot_model_visible_layout_resume_override_matches_rollout_model() -
|
||||
text: "first resumed turn after model override".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -106,6 +106,7 @@ async fn responses_api_emits_api_request_event() {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -149,6 +150,7 @@ async fn process_sse_emits_tracing_for_output_item() {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -192,6 +194,7 @@ async fn process_sse_emits_failed_event_on_parse_error() {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -236,6 +239,7 @@ async fn process_sse_records_failed_event_when_stream_closes_without_completed()
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -300,6 +304,7 @@ async fn process_sse_failed_event_records_response_error_message() {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -362,6 +367,7 @@ async fn process_sse_failed_event_logs_parse_error() {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -411,6 +417,7 @@ async fn process_sse_failed_event_logs_missing_error() {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -469,6 +476,7 @@ async fn process_sse_failed_event_logs_response_completed_parse_error() {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -521,6 +529,7 @@ async fn process_sse_emits_completed_telemetry() {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -593,6 +602,7 @@ async fn handle_responses_span_records_response_kind_and_tool_name() {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -677,6 +687,7 @@ async fn record_responses_sets_span_fields_for_response_events() {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -761,6 +772,7 @@ async fn handle_response_item_records_tool_result_for_custom_tool_call() {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -834,6 +846,7 @@ async fn handle_response_item_records_tool_result_for_function_call() {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -917,6 +930,7 @@ async fn handle_response_item_records_tool_result_for_local_shell_missing_ids()
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -984,6 +998,7 @@ async fn handle_response_item_records_tool_result_for_local_shell_call() {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -1092,6 +1107,7 @@ async fn handle_container_exec_autoapprove_from_config_records_tool_decision() {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -1143,6 +1159,7 @@ async fn handle_container_exec_user_approved_records_tool_decision() {
|
||||
text: "approved".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -1209,6 +1226,7 @@ async fn handle_container_exec_user_approved_for_session_records_tool_decision()
|
||||
text: "persist".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -1275,6 +1293,7 @@ async fn handle_sandbox_error_user_approves_retry_records_tool_decision() {
|
||||
text: "retry".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -1341,6 +1360,7 @@ async fn handle_container_exec_user_denies_records_tool_decision() {
|
||||
text: "deny".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -1407,6 +1427,7 @@ async fn handle_sandbox_error_user_approves_for_session_records_tool_decision()
|
||||
text: "persist".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -1474,6 +1495,7 @@ async fn handle_sandbox_error_user_denies_records_tool_decision() {
|
||||
text: "deny".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
|
||||
@@ -103,6 +103,7 @@ async fn injected_user_input_triggers_follow_up_request_with_deltas() {
|
||||
text: "first prompt".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -119,6 +120,7 @@ async fn injected_user_input_triggers_follow_up_request_with_deltas() {
|
||||
text: "second prompt".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
|
||||
@@ -66,6 +66,7 @@ async fn permissions_message_sent_once_on_start() -> Result<()> {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -191,6 +192,7 @@ async fn permissions_message_added_on_override_change() -> Result<()> {
|
||||
text: "hello 1".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -217,6 +219,7 @@ async fn permissions_message_added_on_override_change() -> Result<()> {
|
||||
text: "hello 2".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -264,6 +267,7 @@ async fn permissions_message_not_added_when_no_change() -> Result<()> {
|
||||
text: "hello 1".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -275,6 +279,7 @@ async fn permissions_message_not_added_when_no_change() -> Result<()> {
|
||||
text: "hello 2".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -333,6 +338,7 @@ async fn resume_replays_permissions_messages() -> Result<()> {
|
||||
text: "hello 1".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -361,6 +367,7 @@ async fn resume_replays_permissions_messages() -> Result<()> {
|
||||
text: "hello 2".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -374,6 +381,7 @@ async fn resume_replays_permissions_messages() -> Result<()> {
|
||||
text: "after resume".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -433,6 +441,7 @@ async fn resume_and_fork_append_permissions_messages() -> Result<()> {
|
||||
text: "hello 1".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -461,6 +470,7 @@ async fn resume_and_fork_append_permissions_messages() -> Result<()> {
|
||||
text: "hello 2".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -482,6 +492,7 @@ async fn resume_and_fork_append_permissions_messages() -> Result<()> {
|
||||
text: "after resume".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -510,6 +521,7 @@ async fn resume_and_fork_append_permissions_messages() -> Result<()> {
|
||||
text: "after fork".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -564,6 +576,7 @@ async fn permissions_message_includes_writable_roots() -> Result<()> {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -210,6 +210,7 @@ async fn plugin_instructions_are_split_from_agents_instructions() -> Result<()>
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -313,6 +314,7 @@ async fn explicit_plugin_mentions_inject_plugin_guidance() -> Result<()> {
|
||||
name: "sample".into(),
|
||||
path: format!("plugin://{SAMPLE_PLUGIN_CONFIG_NAME}"),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -160,6 +160,7 @@ async fn prompt_tools_are_consistent_across_requests() -> anyhow::Result<()> {
|
||||
text: "hello 1".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -171,6 +172,7 @@ async fn prompt_tools_are_consistent_across_requests() -> anyhow::Result<()> {
|
||||
text: "hello 2".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -251,6 +253,7 @@ async fn gpt_5_tools_without_apply_patch_append_apply_patch_instructions() -> an
|
||||
text: "hello 1".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -262,6 +265,7 @@ async fn gpt_5_tools_without_apply_patch_append_apply_patch_instructions() -> an
|
||||
text: "hello 2".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -324,6 +328,7 @@ async fn prefixes_context_and_instructions_once_and_consistently_across_requests
|
||||
text: "hello 1".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -335,6 +340,7 @@ async fn prefixes_context_and_instructions_once_and_consistently_across_requests
|
||||
text: "hello 2".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -416,6 +422,7 @@ async fn overrides_turn_context_but_keeps_cached_prefix_and_key_constant() -> an
|
||||
text: "hello 1".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -451,6 +458,7 @@ async fn overrides_turn_context_but_keeps_cached_prefix_and_key_constant() -> an
|
||||
text: "hello 2".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -532,6 +540,7 @@ async fn override_before_first_turn_emits_environment_context() -> anyhow::Resul
|
||||
text: "first message".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -683,6 +692,7 @@ async fn per_turn_overrides_keep_cached_prefix_and_key_constant() -> anyhow::Res
|
||||
text: "hello 1".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -45,6 +45,7 @@ async fn quota_exceeded_emits_single_error_event() -> Result<()> {
|
||||
text: "quota?".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
|
||||
@@ -1938,6 +1938,7 @@ async fn inbound_handoff_request_steers_active_turn() -> Result<()> {
|
||||
text: "first prompt".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -44,6 +44,7 @@ async fn request_body_is_zstd_compressed_for_codex_backend_when_enabled() -> any
|
||||
text: "compress me".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -91,6 +92,7 @@ async fn request_body_is_not_compressed_for_api_key_auth_even_when_enabled() ->
|
||||
text: "do not compress".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -90,6 +90,7 @@ async fn resume_includes_initial_messages_from_rollout_events() -> Result<()> {
|
||||
text: "Record some messages".into(),
|
||||
text_elements: text_elements.clone(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -175,6 +176,7 @@ async fn resume_includes_initial_messages_from_reasoning_events() -> Result<()>
|
||||
text: "Record reasoning messages".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -264,6 +266,7 @@ async fn resume_switches_models_preserves_base_instructions() -> Result<()> {
|
||||
text: "Record initial instructions".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -304,6 +307,7 @@ async fn resume_switches_models_preserves_base_instructions() -> Result<()> {
|
||||
text: "Resume with different model".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -319,6 +323,7 @@ async fn resume_switches_models_preserves_base_instructions() -> Result<()> {
|
||||
text: "Second turn after resume".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -389,6 +394,7 @@ async fn resume_model_switch_is_not_duplicated_after_pre_turn_override() -> Resu
|
||||
text: "Record initial instructions".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -431,6 +437,7 @@ async fn resume_model_switch_is_not_duplicated_after_pre_turn_override() -> Resu
|
||||
text: "first turn after override".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -716,6 +716,7 @@ async fn review_history_surfaces_in_parent_session() {
|
||||
text: followup.clone(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
|
||||
@@ -381,6 +381,7 @@ async fn tool_search_returns_deferred_tools_without_follow_up_tool_injection() -
|
||||
text: "Find the calendar create tool".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
---
|
||||
source: core/tests/suite/compact.rs
|
||||
expression: "format_labeled_requests_snapshot(\"Mid-turn continuation compaction keeps both prior and active ephemeral_context in the compact request, then reinjects only the active turn ephemeral_context into the continuation request.\",\n&[(\"Local Compaction Request\", &requests[2]),\n(\"Local Post-Compaction History Layout\", &requests[3]),])"
|
||||
---
|
||||
Scenario: Mid-turn continuation compaction keeps both prior and active ephemeral_context in the compact request, then reinjects only the active turn ephemeral_context into the continuation request.
|
||||
|
||||
## 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>
|
||||
|
||||
## 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>>
|
||||
[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
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
---
|
||||
source: core/tests/suite/compact.rs
|
||||
expression: "format_labeled_requests_snapshot(\"Pre-turn auto-compaction keeps prior turn ephemeral_context in the compact request, but the follow-up request carries only the fresh turn ephemeral_context.\",\n&[(\"Local Compaction Request\", &requests[2]),\n(\"Local Post-Compaction History Layout\", &requests[3]),])"
|
||||
---
|
||||
Scenario: Pre-turn auto-compaction keeps prior turn ephemeral_context in the compact request, but the follow-up request carries only the fresh turn ephemeral_context.
|
||||
|
||||
## 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>
|
||||
|
||||
## Local Post-Compaction History Layout
|
||||
00:message/user:USER_ONE
|
||||
01:message/user:USER_TWO
|
||||
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>>
|
||||
[03] <additional_context_for_this_turn>\n <title>Context from my edi...
|
||||
05:message/user:USER_THREE
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
---
|
||||
source: core/tests/suite/compact_remote.rs
|
||||
expression: "format_labeled_requests_snapshot(\"Remote mid-turn continuation compaction keeps both prior and active ephemeral_context in the compact request, then reinjects only the active turn ephemeral_context into the continuation request.\",\n&[(\"Remote Compaction Request\", &compact_request),\n(\"Remote Post-Compaction History Layout\", post_compact_request),])"
|
||||
---
|
||||
Scenario: Remote mid-turn continuation compaction keeps both prior and active ephemeral_context in the compact request, then reinjects only the active turn ephemeral_context into the continuation request.
|
||||
|
||||
## 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
|
||||
|
||||
## 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>>
|
||||
[03] <additional_context_for_this_turn>\n <title>Context from my edi...
|
||||
03:message/user:USER_TWO
|
||||
04:compaction:encrypted=true
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
---
|
||||
source: core/tests/suite/model_visible_layout.rs
|
||||
expression: "format_labeled_requests_snapshot(\"Turns resend fresh ephemeral editor context while keeping it outside durable turn-state diffs.\",\n&[(\"First Request (With Editor Context)\", &requests[0]),\n(\"Second Request (Refreshed Editor Context)\", &requests[1]),])"
|
||||
---
|
||||
Scenario: Turns resend fresh ephemeral editor context while keeping it outside durable turn-state diffs.
|
||||
|
||||
## 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
|
||||
|
||||
## 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
|
||||
@@ -95,6 +95,7 @@ async fn continue_after_stream_error() {
|
||||
text: "first message".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
@@ -114,6 +115,7 @@ async fn continue_after_stream_error() {
|
||||
text: "follow up".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
|
||||
@@ -79,6 +79,7 @@ async fn retries_on_early_close() {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
|
||||
@@ -61,6 +61,7 @@ mv "${tmp_path}" "${payload_path}""#,
|
||||
text: "hello world".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -113,6 +113,7 @@ pub async fn run_codex_tool_session(
|
||||
// MCP tool prompts are plain text with no UI element ranges.
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
},
|
||||
trace: None,
|
||||
@@ -160,6 +161,7 @@ pub async fn run_codex_tool_session_reply(
|
||||
// MCP tool prompts are plain text with no UI element ranges.
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
ephemeral_context: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)]
|
||||
|
||||
Reference in New Issue
Block a user