mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
[codex] Forward turn moderation metadata through app-server (#25710)
## Why
First-party backends can supply turn-scoped moderation metadata that
app-server clients need for client-side presentation. Exposing this as
an experimental typed notification lets opted-in clients consume it
without interpreting raw Responses API events.
## What changed
- forward `response.metadata.openai_chatgpt_moderation_metadata` from
Responses API SSE and WebSocket streams as turn-scoped moderation
metadata
- emit the experimental app-server v2 `turn/moderationMetadata`
notification with `{ threadId, turnId, metadata }`
- add app-server integration coverage for the typed moderation metadata
notification
## Testing
- `just test -p codex-core
build_ws_client_metadata_includes_window_lineage_and_turn_metadata`
- `just test -p codex-core` (fails locally: 46 failures and 1 timeout,
primarily missing `test_stdio_server` and shell snapshot timeouts)
- `just test -p codex-app-server-protocol`
- `just test -p codex-app-server
turn_moderation_metadata_emits_typed_notification_v2`
- `just test -p codex-app-server` (fails locally: 792 passed, 10 failed,
and 5 timed out; failures are in existing environment-sensitive tests,
primarily because nested macOS `sandbox-exec` is not permitted)
- `just write-app-server-schema --experimental --schema-root
/tmp/codex-app-server-schema-experimental`
This commit is contained in:
committed by
GitHub
Unverified
parent
6dc28ba6e0
commit
55aa071b17
@@ -4909,6 +4909,23 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"TurnModerationMetadataNotification": {
|
||||
"properties": {
|
||||
"metadata": true,
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"turnId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"metadata",
|
||||
"threadId",
|
||||
"turnId"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"TurnPlanStep": {
|
||||
"properties": {
|
||||
"status": {
|
||||
@@ -6248,6 +6265,26 @@
|
||||
"title": "Model/verificationNotification",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
"enum": [
|
||||
"turn/moderationMetadata"
|
||||
],
|
||||
"title": "Turn/moderationMetadataNotificationMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/TurnModerationMetadataNotification"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Turn/moderationMetadataNotification",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
|
||||
+39
@@ -4904,6 +4904,26 @@
|
||||
"title": "Model/verificationNotification",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
"enum": [
|
||||
"turn/moderationMetadata"
|
||||
],
|
||||
"title": "Turn/moderationMetadataNotificationMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/v2/TurnModerationMetadataNotification"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Turn/moderationMetadataNotification",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
@@ -18601,6 +18621,25 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"TurnModerationMetadataNotification": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"metadata": true,
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"turnId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"metadata",
|
||||
"threadId",
|
||||
"turnId"
|
||||
],
|
||||
"title": "TurnModerationMetadataNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"TurnPlanStep": {
|
||||
"properties": {
|
||||
"status": {
|
||||
|
||||
+39
@@ -12420,6 +12420,26 @@
|
||||
"title": "Model/verificationNotification",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
"enum": [
|
||||
"turn/moderationMetadata"
|
||||
],
|
||||
"title": "Turn/moderationMetadataNotificationMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/TurnModerationMetadataNotification"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Turn/moderationMetadataNotification",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
@@ -16418,6 +16438,25 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"TurnModerationMetadataNotification": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"metadata": true,
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"turnId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"metadata",
|
||||
"threadId",
|
||||
"turnId"
|
||||
],
|
||||
"title": "TurnModerationMetadataNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"TurnPlanStep": {
|
||||
"properties": {
|
||||
"status": {
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"metadata": true,
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"turnId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"metadata",
|
||||
"threadId",
|
||||
"turnId"
|
||||
],
|
||||
"title": "TurnModerationMetadataNotification",
|
||||
"type": "object"
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
+6
@@ -0,0 +1,6 @@
|
||||
// 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.
|
||||
import type { JsonValue } from "../serde_json/JsonValue";
|
||||
|
||||
export type TurnModerationMetadataNotification = { threadId: string, turnId: string, metadata: JsonValue, };
|
||||
@@ -447,6 +447,7 @@ export type { TurnError } from "./TurnError";
|
||||
export type { TurnInterruptParams } from "./TurnInterruptParams";
|
||||
export type { TurnInterruptResponse } from "./TurnInterruptResponse";
|
||||
export type { TurnItemsView } from "./TurnItemsView";
|
||||
export type { TurnModerationMetadataNotification } from "./TurnModerationMetadataNotification";
|
||||
export type { TurnPlanStep } from "./TurnPlanStep";
|
||||
export type { TurnPlanStepStatus } from "./TurnPlanStepStatus";
|
||||
export type { TurnPlanUpdatedNotification } from "./TurnPlanUpdatedNotification";
|
||||
|
||||
@@ -1549,6 +1549,8 @@ server_notification_definitions! {
|
||||
ContextCompacted => "thread/compacted" (v2::ContextCompactedNotification),
|
||||
ModelRerouted => "model/rerouted" (v2::ModelReroutedNotification),
|
||||
ModelVerification => "model/verification" (v2::ModelVerificationNotification),
|
||||
#[experimental("turn/moderationMetadata")]
|
||||
TurnModerationMetadata => "turn/moderationMetadata" (v2::TurnModerationMetadataNotification),
|
||||
Warning => "warning" (v2::WarningNotification),
|
||||
GuardianWarning => "guardianWarning" (v2::GuardianWarningNotification),
|
||||
DeprecationNotice => "deprecationNotice" (v2::DeprecationNoticeNotification),
|
||||
@@ -3222,6 +3224,21 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn turn_moderation_metadata_notification_is_marked_experimental() {
|
||||
let notification =
|
||||
ServerNotification::TurnModerationMetadata(v2::TurnModerationMetadataNotification {
|
||||
thread_id: "thr_123".to_string(),
|
||||
turn_id: "turn_123".to_string(),
|
||||
metadata: json!({"presentation": "inline"}),
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
crate::experimental_api::ExperimentalApi::experimental_reason(¬ification),
|
||||
Some("turn/moderationMetadata")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thread_realtime_started_notification_is_marked_experimental() {
|
||||
let notification =
|
||||
|
||||
@@ -8,6 +8,7 @@ use codex_protocol::protocol::ModelVerification as CoreModelVerification;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use serde_json::Value as JsonValue;
|
||||
use ts_rs::TS;
|
||||
|
||||
v2_enum_from_core!(
|
||||
@@ -152,3 +153,12 @@ pub struct ModelVerificationNotification {
|
||||
pub turn_id: String,
|
||||
pub verifications: Vec<ModelVerification>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct TurnModerationMetadataNotification {
|
||||
pub thread_id: String,
|
||||
pub turn_id: String,
|
||||
pub metadata: JsonValue,
|
||||
}
|
||||
|
||||
@@ -1244,6 +1244,7 @@ The app-server streams JSON-RPC notifications while a turn is running. Each turn
|
||||
- `turn/plan/updated` — `{ turnId, explanation?, plan }` whenever the agent shares or changes its plan; each `plan` entry is `{ step, status }` with `status` in `pending`, `inProgress`, or `completed`.
|
||||
- `model/rerouted` — `{ threadId, turnId, fromModel, toModel, reason }` when the backend reroutes a request to a different model (for example, due to high-risk cyber safety checks).
|
||||
- `model/verification` — `{ threadId, turnId, verifications }` when the backend flags additional account verification, such as `trustedAccessForCyber`.
|
||||
- `turn/moderationMetadata` — experimental; `{ threadId, turnId, metadata }` when a first-party backend supplies turn-scoped moderation metadata for client-side presentation.
|
||||
|
||||
Today both notifications carry an empty `items` array even when item events were streamed; rely on `item/*` notifications for the canonical item list until this is fixed.
|
||||
|
||||
|
||||
@@ -75,6 +75,7 @@ use codex_app_server_protocol::TurnDiffUpdatedNotification;
|
||||
use codex_app_server_protocol::TurnError;
|
||||
use codex_app_server_protocol::TurnInterruptResponse;
|
||||
use codex_app_server_protocol::TurnItemsView;
|
||||
use codex_app_server_protocol::TurnModerationMetadataNotification;
|
||||
use codex_app_server_protocol::TurnPlanStep;
|
||||
use codex_app_server_protocol::TurnPlanUpdatedNotification;
|
||||
use codex_app_server_protocol::TurnStartedNotification;
|
||||
@@ -337,6 +338,16 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
.send_server_notification(ServerNotification::ModelVerification(notification))
|
||||
.await;
|
||||
}
|
||||
EventMsg::TurnModerationMetadata(event) => {
|
||||
let notification = TurnModerationMetadataNotification {
|
||||
thread_id: conversation_id.to_string(),
|
||||
turn_id: event_turn_id.clone(),
|
||||
metadata: event.metadata,
|
||||
};
|
||||
outgoing
|
||||
.send_server_notification(ServerNotification::TurnModerationMetadata(notification))
|
||||
.await;
|
||||
}
|
||||
EventMsg::RealtimeConversationStarted(event) => {
|
||||
let notification = ThreadRealtimeStartedNotification {
|
||||
thread_id: conversation_id.to_string(),
|
||||
|
||||
@@ -725,6 +725,7 @@ mod tests {
|
||||
use codex_app_server_protocol::RateLimitWindow;
|
||||
use codex_app_server_protocol::ServerResponse;
|
||||
use codex_app_server_protocol::ToolRequestUserInputParams;
|
||||
use codex_app_server_protocol::TurnModerationMetadataNotification;
|
||||
use codex_protocol::ThreadId;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
@@ -951,6 +952,31 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_turn_moderation_metadata_notification_serialization() {
|
||||
let notification =
|
||||
ServerNotification::TurnModerationMetadata(TurnModerationMetadataNotification {
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
metadata: json!({"presentation": "inline"}),
|
||||
});
|
||||
|
||||
let jsonrpc_notification = OutgoingMessage::AppServerNotification(notification);
|
||||
assert_eq!(
|
||||
json!({
|
||||
"method": "turn/moderationMetadata",
|
||||
"params": {
|
||||
"threadId": "thread-1",
|
||||
"turnId": "turn-1",
|
||||
"metadata": {"presentation": "inline"},
|
||||
},
|
||||
}),
|
||||
serde_json::to_value(jsonrpc_notification)
|
||||
.expect("ensure the notification serializes correctly"),
|
||||
"ensure the notification serializes correctly"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn server_request_response_from_result_decodes_typed_response() {
|
||||
let request = ServerRequest::CommandExecutionRequestApproval {
|
||||
|
||||
@@ -15,6 +15,7 @@ use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_app_server_protocol::TurnModerationMetadataNotification;
|
||||
use codex_app_server_protocol::TurnStartParams;
|
||||
use codex_app_server_protocol::TurnStartResponse;
|
||||
use codex_app_server_protocol::UserInput;
|
||||
@@ -309,6 +310,87 @@ async fn model_verification_emits_typed_notification_and_warning_v2() -> Result<
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn turn_moderation_metadata_emits_typed_notification_v2() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
let body = responses::sse(vec![
|
||||
responses::ev_response_created("resp-1"),
|
||||
serde_json::json!({
|
||||
"type": "response.metadata",
|
||||
"sequence_number": 1,
|
||||
"response_id": "resp-1",
|
||||
"metadata": {
|
||||
"openai_chatgpt_moderation_metadata": {
|
||||
"presentation": "inline"
|
||||
}
|
||||
}
|
||||
}),
|
||||
responses::ev_assistant_message("msg-1", "Done"),
|
||||
responses::ev_completed("resp-1"),
|
||||
]);
|
||||
let response = responses::sse_response(body);
|
||||
let _response_mock = responses::mount_response_once(&server, response).await;
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri())?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let thread_req = mcp
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
model: Some(REQUESTED_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.clone(),
|
||||
client_user_message_id: None,
|
||||
input: vec![UserInput::Text {
|
||||
text: "trigger moderation metadata".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let turn_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(turn_req)),
|
||||
)
|
||||
.await??;
|
||||
let turn_start: TurnStartResponse = to_response(turn_resp)?;
|
||||
|
||||
let notification = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("turn/moderationMetadata"),
|
||||
)
|
||||
.await??;
|
||||
let metadata: TurnModerationMetadataNotification =
|
||||
serde_json::from_value(notification.params.ok_or_else(|| {
|
||||
anyhow::anyhow!("turn/moderationMetadata notifications must include params")
|
||||
})?)?;
|
||||
assert_eq!(
|
||||
metadata,
|
||||
TurnModerationMetadataNotification {
|
||||
thread_id: thread.id,
|
||||
turn_id: turn_start.turn.id,
|
||||
metadata: serde_json::json!({"presentation": "inline"}),
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn collect_turn_notifications_and_validate_no_warning_item(
|
||||
mcp: &mut TestAppServer,
|
||||
) -> Result<ModelReroutedNotification> {
|
||||
|
||||
@@ -6,6 +6,7 @@ use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
|
||||
use codex_protocol::protocol::ModelVerification;
|
||||
use codex_protocol::protocol::RateLimitSnapshot;
|
||||
use codex_protocol::protocol::TokenUsage;
|
||||
use codex_protocol::protocol::TurnModerationMetadataEvent;
|
||||
use codex_protocol::protocol::W3cTraceContext;
|
||||
use futures::Stream;
|
||||
use serde::Deserialize;
|
||||
@@ -78,6 +79,8 @@ pub enum ResponseEvent {
|
||||
ServerModel(String),
|
||||
/// Emitted when the server recommends additional account verification.
|
||||
ModelVerifications(Vec<ModelVerification>),
|
||||
/// Emitted when the server includes moderation metadata for first-party turn presentation.
|
||||
TurnModerationMetadata(TurnModerationMetadataEvent),
|
||||
/// Emitted when `X-Reasoning-Included: true` is present on the response,
|
||||
/// meaning the server already accounted for past reasoning tokens and the
|
||||
/// client should not re-estimate them.
|
||||
|
||||
@@ -683,6 +683,7 @@ async fn run_websocket_response_stream(
|
||||
}
|
||||
};
|
||||
let model_verifications = event.model_verifications();
|
||||
let turn_moderation_metadata = event.turn_moderation_metadata();
|
||||
if event.kind() == "codex.rate_limits" {
|
||||
if let Some(snapshot) = parse_rate_limit_event(&text) {
|
||||
let _ = tx_event.send(Ok(ResponseEvent::RateLimits(snapshot))).await;
|
||||
@@ -707,6 +708,16 @@ async fn run_websocket_response_stream(
|
||||
"response event consumer dropped".to_string(),
|
||||
));
|
||||
}
|
||||
if let Some(metadata) = turn_moderation_metadata
|
||||
&& tx_event
|
||||
.send(Ok(ResponseEvent::TurnModerationMetadata(metadata)))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return Err(ApiError::Stream(
|
||||
"response event consumer dropped".to_string(),
|
||||
));
|
||||
}
|
||||
match process_responses_event(event) {
|
||||
Ok(Some(event)) => {
|
||||
let is_completed = matches!(event, ResponseEvent::Completed { .. });
|
||||
|
||||
@@ -8,6 +8,7 @@ use codex_client::StreamResponse;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::protocol::ModelVerification;
|
||||
use codex_protocol::protocol::TokenUsage;
|
||||
use codex_protocol::protocol::TurnModerationMetadataEvent;
|
||||
use eventsource_stream::Eventsource;
|
||||
use futures::StreamExt;
|
||||
use serde::Deserialize;
|
||||
@@ -193,6 +194,18 @@ impl ResponsesStreamEvent {
|
||||
.and_then(|metadata| metadata.get("openai_verification_recommendation"))
|
||||
.and_then(model_verifications_from_json_value)
|
||||
}
|
||||
|
||||
pub(crate) fn turn_moderation_metadata(&self) -> Option<TurnModerationMetadataEvent> {
|
||||
if self.kind() != "response.metadata" {
|
||||
return None;
|
||||
}
|
||||
|
||||
self.metadata
|
||||
.as_ref()
|
||||
.and_then(|metadata| metadata.get("openai_chatgpt_moderation_metadata"))
|
||||
.cloned()
|
||||
.map(|metadata| TurnModerationMetadataEvent { metadata })
|
||||
}
|
||||
}
|
||||
|
||||
fn header_openai_model_value_from_json(value: &Value) -> Option<String> {
|
||||
@@ -444,6 +457,7 @@ pub async fn process_sse(
|
||||
}
|
||||
};
|
||||
let model_verifications = event.model_verifications();
|
||||
let turn_moderation_metadata = event.turn_moderation_metadata();
|
||||
|
||||
if let Some(model) = event.response_model()
|
||||
&& last_server_model.as_deref() != Some(model.as_str())
|
||||
@@ -465,6 +479,14 @@ pub async fn process_sse(
|
||||
{
|
||||
return;
|
||||
}
|
||||
if let Some(metadata) = turn_moderation_metadata
|
||||
&& tx_event
|
||||
.send(Ok(ResponseEvent::TurnModerationMetadata(metadata)))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
match process_responses_event(event) {
|
||||
Ok(Some(event)) => {
|
||||
@@ -1215,6 +1237,41 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn process_sse_emits_turn_moderation_metadata_field() {
|
||||
let events = run_sse(vec![
|
||||
json!({
|
||||
"type": "response.metadata",
|
||||
"metadata": {
|
||||
"openai_chatgpt_moderation_metadata": {
|
||||
"presentation": "inline"
|
||||
}
|
||||
}
|
||||
}),
|
||||
json!({
|
||||
"type": "response.completed",
|
||||
"response": {
|
||||
"id": "resp-1"
|
||||
}
|
||||
}),
|
||||
])
|
||||
.await;
|
||||
|
||||
assert_matches!(
|
||||
&events[0],
|
||||
ResponseEvent::TurnModerationMetadata(result)
|
||||
if result.metadata == json!({"presentation": "inline"})
|
||||
);
|
||||
assert_matches!(
|
||||
&events[1],
|
||||
ResponseEvent::Completed {
|
||||
response_id,
|
||||
token_usage: None,
|
||||
end_turn: None,
|
||||
} if response_id == "resp-1"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn responses_stream_event_response_model_reads_top_level_headers() {
|
||||
let ev: ResponsesStreamEvent = serde_json::from_value(json!({
|
||||
|
||||
@@ -359,6 +359,7 @@ use codex_protocol::protocol::ThreadMemoryMode;
|
||||
use codex_protocol::protocol::TokenCountEvent;
|
||||
use codex_protocol::protocol::TokenUsage;
|
||||
use codex_protocol::protocol::TokenUsageInfo;
|
||||
use codex_protocol::protocol::TurnModerationMetadataEvent;
|
||||
use codex_protocol::protocol::WarningEvent;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use codex_tools::ToolEnvironmentMode;
|
||||
@@ -2637,6 +2638,15 @@ impl Session {
|
||||
.await;
|
||||
}
|
||||
|
||||
pub(crate) async fn emit_turn_moderation_metadata(
|
||||
self: &Arc<Self>,
|
||||
turn_context: &Arc<TurnContext>,
|
||||
metadata: TurnModerationMetadataEvent,
|
||||
) {
|
||||
self.send_event(turn_context, EventMsg::TurnModerationMetadata(metadata))
|
||||
.await;
|
||||
}
|
||||
|
||||
pub(crate) async fn replace_history(
|
||||
&self,
|
||||
items: Vec<ResponseItem>,
|
||||
|
||||
@@ -1392,6 +1392,7 @@ pub(super) fn realtime_text_for_event(msg: &EventMsg) -> Option<String> {
|
||||
| EventMsg::RealtimeConversationClosed(_)
|
||||
| EventMsg::ModelReroute(_)
|
||||
| EventMsg::ModelVerification(_)
|
||||
| EventMsg::TurnModerationMetadata(_)
|
||||
| EventMsg::ContextCompacted(_)
|
||||
| EventMsg::ThreadRolledBack(_)
|
||||
| EventMsg::TurnStarted(_)
|
||||
@@ -2057,6 +2058,10 @@ async fn try_run_sampling_request(
|
||||
.await;
|
||||
}
|
||||
}
|
||||
ResponseEvent::TurnModerationMetadata(metadata) => {
|
||||
sess.emit_turn_moderation_metadata(&turn_context, metadata)
|
||||
.await;
|
||||
}
|
||||
ResponseEvent::ServerReasoningIncluded(included) => {
|
||||
sess.set_server_reasoning_included(included).await;
|
||||
}
|
||||
|
||||
@@ -147,6 +147,7 @@ fn response_event_records_turn_ttft(event: &ResponseEvent) -> bool {
|
||||
ResponseEvent::Created
|
||||
| ResponseEvent::ServerModel(_)
|
||||
| ResponseEvent::ModelVerifications(_)
|
||||
| ResponseEvent::TurnModerationMetadata(_)
|
||||
| ResponseEvent::ServerReasoningIncluded(_)
|
||||
| ResponseEvent::ToolCallInputDelta { .. }
|
||||
| ResponseEvent::Completed { .. }
|
||||
|
||||
@@ -266,7 +266,8 @@ async fn run_codex_tool_session_inner(
|
||||
}
|
||||
EventMsg::Warning(_)
|
||||
| EventMsg::GuardianWarning(_)
|
||||
| EventMsg::ModelVerification(_) => {
|
||||
| EventMsg::ModelVerification(_)
|
||||
| EventMsg::TurnModerationMetadata(_) => {
|
||||
continue;
|
||||
}
|
||||
EventMsg::GuardianAssessment(_) => {
|
||||
|
||||
@@ -1208,6 +1208,7 @@ impl SessionTelemetry {
|
||||
}
|
||||
ResponseEvent::ServerModel(_) => "server_model".into(),
|
||||
ResponseEvent::ModelVerifications(_) => "model_verifications".into(),
|
||||
ResponseEvent::TurnModerationMetadata(_) => "turn_moderation_metadata".into(),
|
||||
ResponseEvent::ServerReasoningIncluded(_) => "server_reasoning_included".into(),
|
||||
ResponseEvent::RateLimits(_) => "rate_limits".into(),
|
||||
ResponseEvent::ModelsEtag(_) => "models_etag".into(),
|
||||
|
||||
@@ -1221,6 +1221,9 @@ pub enum EventMsg {
|
||||
/// Backend recommends additional account verification for this turn.
|
||||
ModelVerification(ModelVerificationEvent),
|
||||
|
||||
/// Backend moderation metadata intended for first-party turn presentation.
|
||||
TurnModerationMetadata(TurnModerationMetadataEvent),
|
||||
|
||||
/// Conversation history was compacted (either automatically or manually).
|
||||
ContextCompacted(ContextCompactedEvent),
|
||||
|
||||
@@ -1885,6 +1888,11 @@ pub struct ModelVerificationEvent {
|
||||
pub verifications: Vec<ModelVerification>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
|
||||
pub struct TurnModerationMetadataEvent {
|
||||
pub metadata: Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
|
||||
pub struct ContextCompactedEvent;
|
||||
|
||||
|
||||
@@ -224,6 +224,7 @@ pub(crate) fn tool_runtime_trace_event(event: &EventMsg) -> Option<ToolRuntimeTr
|
||||
| EventMsg::RealtimeConversationSdp(_)
|
||||
| EventMsg::ModelReroute(_)
|
||||
| EventMsg::ModelVerification(_)
|
||||
| EventMsg::TurnModerationMetadata(_)
|
||||
| EventMsg::ContextCompacted(_)
|
||||
| EventMsg::ThreadRolledBack(_)
|
||||
| EventMsg::ThreadGoalUpdated(_)
|
||||
@@ -297,6 +298,7 @@ pub(crate) fn wrapped_protocol_event_type(event: &EventMsg) -> Option<&'static s
|
||||
| EventMsg::RealtimeConversationSdp(_)
|
||||
| EventMsg::ModelReroute(_)
|
||||
| EventMsg::ModelVerification(_)
|
||||
| EventMsg::TurnModerationMetadata(_)
|
||||
| EventMsg::ContextCompacted(_)
|
||||
| EventMsg::ThreadSettingsApplied(_)
|
||||
| EventMsg::TokenCount(_)
|
||||
|
||||
@@ -117,6 +117,7 @@ pub fn should_persist_event_msg(ev: &EventMsg) -> bool {
|
||||
| EventMsg::RealtimeConversationClosed(_)
|
||||
| EventMsg::ModelReroute(_)
|
||||
| EventMsg::ModelVerification(_)
|
||||
| EventMsg::TurnModerationMetadata(_)
|
||||
| EventMsg::AgentReasoningSectionBreak(_)
|
||||
| EventMsg::RawResponseItem(_)
|
||||
| EventMsg::SessionConfigured(_)
|
||||
|
||||
@@ -118,6 +118,9 @@ pub(super) fn server_notification_thread_target(
|
||||
ServerNotification::ModelVerification(notification) => {
|
||||
Some(notification.thread_id.as_str())
|
||||
}
|
||||
ServerNotification::TurnModerationMetadata(notification) => {
|
||||
Some(notification.thread_id.as_str())
|
||||
}
|
||||
ServerNotification::ThreadRealtimeStarted(notification) => {
|
||||
Some(notification.thread_id.as_str())
|
||||
}
|
||||
|
||||
@@ -233,6 +233,7 @@ impl ChatWidget {
|
||||
| ServerNotification::RemoteControlStatusChanged(_)
|
||||
| ServerNotification::ExternalAgentConfigImportCompleted(_)
|
||||
| ServerNotification::FsChanged(_)
|
||||
| ServerNotification::TurnModerationMetadata(_)
|
||||
| ServerNotification::FuzzyFileSearchSessionUpdated(_)
|
||||
| ServerNotification::FuzzyFileSearchSessionCompleted(_)
|
||||
| ServerNotification::ThreadRealtimeTranscriptDelta(_)
|
||||
|
||||
Reference in New Issue
Block a user