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:
psachdeva-oai
2026-11-07 21:31:08 +00:00
committed by copyberry
parent 33b3f80a09
commit 202b590e84
35 changed files with 948 additions and 9 deletions
+22 -1
View File
@@ -151,9 +151,30 @@ pub struct AsyncUserInputQuestion {
pub options: Option<Vec<String>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, TS, JsonSchema)]
/// A presentation instruction for Chat to apply to this answer when recovering history.
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, TS, JsonSchema)]
#[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,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub block_type: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub cyber_trusted_access: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub check_trusted_contacts: Option<bool>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, TS, JsonSchema)]
pub struct AgentMessageModeration {
pub moderated: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub display: Option<AgentMessageDisplay>,
}
#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
+2 -1
View File
@@ -139,13 +139,14 @@ impl AgentMessageItem {
questions: self.questions.clone(),
})
};
if let Some(decision) = self.moderation {
if let Some(decision) = &self.moderation {
// The canonical answer has one ID even when the model supplied multiple text parts.
return vec![event(
texts.collect(),
Some(crate::protocol::TargetedAnswerModeration {
item_id: self.id.clone(),
moderated: decision.moderated,
display: decision.display.clone(),
}),
)];
}
+3
View File
@@ -2400,6 +2400,9 @@ pub struct ModelVerificationEvent {
pub struct TargetedAnswerModeration {
pub item_id: String,
pub moderated: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub display: Option<crate::items::AgentMessageDisplay>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]