mirror of
https://github.com/openai/codex.git
synced 2026-11-10 21:00:47 +00:00
Preserve display instructions in answer moderation (#60172)
## Why Recovered answer history needs presentation instructions alongside the moderation flag so clients can apply the intended display behavior. ## What changed - Add optional `display` metadata with an `action` and optional `blockType`, `cyberTrustedAccess`, and `checkTrustedContacts` fields. - Carry display metadata through final answers, legacy events, thread history reconstruction, and legacy rollout migration. - Expose the metadata in the app-server protocol and update generated JSON schemas, TypeScript types, and Python SDK models. Missing display fields remain compatible with existing stored moderation data; the app-server emits absent optional values as `null`. ## Testing Extend the final-answer moderation test to cover display metadata in answer items and legacy events. Add serialization coverage for missing display data, nullable fields, and preservation of an explicit `false` value. GitOrigin-RevId: b798a2ecc53c2a94c7659ef3d9663b3143b70a1a
This commit is contained in:
@@ -638,6 +638,7 @@ impl ThreadHistoryBuilder {
|
||||
.as_ref()
|
||||
.map(|decision| AgentMessageModeration {
|
||||
moderated: decision.moderated,
|
||||
display: decision.display.clone().map(Into::into),
|
||||
}),
|
||||
id,
|
||||
text: payload.message.clone(),
|
||||
|
||||
@@ -28,6 +28,7 @@ use codex_protocol::approvals::GuardianAssessmentDecisionSource as CoreGuardianA
|
||||
use codex_protocol::approvals::GuardianCommandSource as CoreGuardianCommandSource;
|
||||
use codex_protocol::items::AgentMessageContent as CoreAgentMessageContent;
|
||||
pub use codex_protocol::items::AgentMessageDelivery;
|
||||
use codex_protocol::items::AgentMessageDisplay as CoreAgentMessageDisplay;
|
||||
use codex_protocol::items::AgentMessageModeration as CoreAgentMessageModeration;
|
||||
pub use codex_protocol::items::AsyncUserInputQuestion;
|
||||
use codex_protocol::items::CollabAgentTool as CoreCollabAgentTool;
|
||||
@@ -150,17 +151,42 @@ pub enum CommandAction {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct AgentMessageModeration {
|
||||
pub moderated: bool,
|
||||
pub display: Option<AgentMessageDisplay>,
|
||||
}
|
||||
|
||||
impl From<CoreAgentMessageModeration> for AgentMessageModeration {
|
||||
fn from(value: CoreAgentMessageModeration) -> Self {
|
||||
Self {
|
||||
moderated: value.moderated,
|
||||
display: value.display.map(Into::into),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A presentation instruction for Chat to apply to this answer when recovering history.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct AgentMessageDisplay {
|
||||
/// Unknown actions must be left for the producing client to handle.
|
||||
pub action: String,
|
||||
pub block_type: Option<String>,
|
||||
pub cyber_trusted_access: Option<bool>,
|
||||
pub check_trusted_contacts: Option<bool>,
|
||||
}
|
||||
|
||||
impl From<CoreAgentMessageDisplay> for AgentMessageDisplay {
|
||||
fn from(value: CoreAgentMessageDisplay) -> Self {
|
||||
Self {
|
||||
action: value.action,
|
||||
block_type: value.block_type,
|
||||
cyber_trusted_access: value.cyber_trusted_access,
|
||||
check_trusted_contacts: value.check_trusted_contacts,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3170,6 +3170,35 @@ fn audio_references_round_trip_through_core_and_json() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn answer_moderation_exposes_nullable_display_fields() {
|
||||
for (stored, expected) in [
|
||||
(
|
||||
json!({"moderated": false}),
|
||||
json!({"moderated": false, "display": null}),
|
||||
),
|
||||
(
|
||||
json!({"moderated": true, "display": {
|
||||
"action": "answerHelpline",
|
||||
"checkTrustedContacts": false,
|
||||
}}),
|
||||
json!({"moderated": true, "display": {
|
||||
"action": "answerHelpline",
|
||||
"blockType": null,
|
||||
"cyberTrustedAccess": null,
|
||||
"checkTrustedContacts": false,
|
||||
}}),
|
||||
),
|
||||
] {
|
||||
let core: codex_protocol::items::AgentMessageModeration =
|
||||
serde_json::from_value(stored).unwrap();
|
||||
assert_eq!(
|
||||
serde_json::to_value(AgentMessageModeration::from(core)).unwrap(),
|
||||
expected
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
let user_item = TurnItem::UserMessage(UserMessageItem {
|
||||
@@ -3297,7 +3326,10 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
);
|
||||
|
||||
let agent_item_with_phase = TurnItem::AgentMessage(AgentMessageItem {
|
||||
moderation: Some(codex_protocol::items::AgentMessageModeration { moderated: true }),
|
||||
moderation: Some(codex_protocol::items::AgentMessageModeration {
|
||||
moderated: true,
|
||||
display: None,
|
||||
}),
|
||||
id: "agent-2".to_string(),
|
||||
content: vec![AgentMessageContent::Text {
|
||||
text: "final".to_string(),
|
||||
@@ -3319,7 +3351,10 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
assert_eq!(
|
||||
ThreadItemPayload::from(agent_item_with_phase),
|
||||
ThreadItemPayload::AgentMessage {
|
||||
moderation: Some(AgentMessageModeration { moderated: true }),
|
||||
moderation: Some(AgentMessageModeration {
|
||||
moderated: true,
|
||||
display: None
|
||||
}),
|
||||
id: "agent-2".to_string(),
|
||||
text: "final".to_string(),
|
||||
phase: Some(MessagePhase::FinalAnswer),
|
||||
|
||||
Reference in New Issue
Block a user