Version answer display instructions and restrict unsupported payloads (#60904)

## What changed

- Replace free-form `AgentMessageDisplay` actions with a version `1` contract using `show`, `hide`, and `block` modes, with typed annotations, replacements, block reasons, and trusted-contact handling.
- Discard invalid or unsupported display instructions and set `moderated` to `true` when reading moderation data in Rust and the Python SDK.
- Update generated JSON schemas, TypeScript types, and Python SDK models to match the contract.

## Testing

Add coverage for display variants, tolerance of extra fields, and unsupported-version or unknown-mode fallback. Update the final-answer moderation test and add Python coverage for thread history and item notifications.

GitOrigin-RevId: ec64070002da67e8e377dccb3728806ac8b290e7
This commit is contained in:
psachdeva-oai
2026-11-11 23:35:05 +00:00
committed by copyberry
parent 9248df5978
commit 8a4b226ad5
39 changed files with 4261 additions and 643 deletions
+144 -49
View File
@@ -2,7 +2,7 @@
# filename: codex_app_server_protocol.v2.schemas.json
from __future__ import annotations
from pydantic import BaseModel, ConfigDict, Field, RootModel
from pydantic import ValidationError, model_validator, BaseModel, ConfigDict, Field, RootModel
from typing import Annotated, Any, Literal
from enum import Enum
@@ -149,18 +149,6 @@ class AgentMessageDeltaNotification(BaseModel):
turn_id: Annotated[str, Field(alias="turnId")]
class AgentMessageDisplay(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
action: Annotated[
str, Field(description="Unknown actions must be left for the producing client to handle.")
]
block_type: Annotated[str | None, Field(alias="blockType")] = None
check_trusted_contacts: Annotated[bool | None, Field(alias="checkTrustedContacts")] = None
cyber_trusted_access: Annotated[bool | None, Field(alias="cyberTrustedAccess")] = None
class InputTextAgentMessageInputContent(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
@@ -185,14 +173,6 @@ class AgentMessageInputContent(
root: InputTextAgentMessageInputContent | EncryptedContentAgentMessageInputContent
class AgentMessageModeration(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
display: AgentMessageDisplay | None = None
moderated: bool
class AgentPath(RootModel[str]):
root: str
@@ -210,6 +190,17 @@ class AnalyticsConfig(BaseModel):
enabled: bool | None = None
class WellbeingPromptHelplineAnswerReplacement(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
kind: Literal["wellbeingPromptHelpline"]
class AnswerReplacement(RootModel[WellbeingPromptHelplineAnswerReplacement]):
root: WellbeingPromptHelplineAnswerReplacement
class AppBranding(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
@@ -470,6 +461,32 @@ class BedrockProviderId(Enum):
amazon_bedrock_runtime = "amazon-bedrock-runtime"
class BlockReason1(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
kind: Literal["protection"]
protection_type: Annotated[
str,
Field(
alias="protectionType",
description="Identifies the same moderation row that Chat writes on the live path.",
),
]
class KindBlockReason(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
kind: Literal["cyber"]
trusted_access: Annotated[bool | None, Field(alias="trustedAccess")] = None
class BlockReason(RootModel[BlockReason1 | KindBlockReason]):
root: BlockReason1 | KindBlockReason
class BrowserUseAccessApprovalLifetime(Enum):
turn = "turn"
thread = "thread"
@@ -2062,6 +2079,11 @@ class GuardianWarningNotification(BaseModel):
]
class HelplineSubject(Enum):
answer = "answer"
prompt = "prompt"
class HookErrorInfo(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
@@ -6919,6 +6941,11 @@ class ToolExposureSurface(Enum):
direct = "direct"
class TrustedContacts(Enum):
check = "check"
skip = "skip"
class TurnDiffUpdatedNotification(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
@@ -7342,6 +7369,36 @@ class AdditionalContextEntry(BaseModel):
value: str
class HideAgentMessageDisplay(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
mode: Annotated[Literal["hide"], Field(title="HideAgentMessageDisplayMode")]
replacement: AnswerReplacement
version: Literal[1]
class BlockAgentMessageDisplay(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
mode: Annotated[Literal["block"], Field(title="BlockAgentMessageDisplayMode")]
reason: BlockReason
version: Literal[1]
class AnswerAnnotation1(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
kind: Literal["wellbeingHelpline"]
subject: HelplineSubject
class AnswerAnnotation(RootModel[AnswerAnnotation1]):
root: AnswerAnnotation1
class AppConfig(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
@@ -10808,34 +10865,6 @@ class UserMessageThreadItem(BaseModel):
type: Annotated[Literal["userMessage"], Field(title="UserMessageThreadItemType")]
class AgentMessageThreadItem(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
completed_at_ms: Annotated[
int | None,
Field(
alias="completedAtMs",
description="Unix timestamp in milliseconds when the item completed, if recorded.",
),
] = None
delivery: AgentMessageDelivery | None = None
id: str
memory_citation: Annotated[MemoryCitation | None, Field(alias="memoryCitation")] = None
moderation: AgentMessageModeration | None = None
phase: MessagePhase | None = None
questions: list[AsyncUserInputQuestion] | None = None
started_at_ms: Annotated[
int | None,
Field(
alias="startedAtMs",
description="Unix timestamp in milliseconds when the item started, if recorded.",
),
] = None
text: str
type: Annotated[Literal["agentMessage"], Field(title="AgentMessageThreadItemType")]
class CommandExecutionThreadItem(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
@@ -11524,6 +11553,44 @@ class AccountSession(BaseModel):
workspaces: list[AccountSessionWorkspace]
class ShowAgentMessageDisplay(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
annotation: AnswerAnnotation | None = None
mode: Annotated[Literal["show"], Field(title="ShowAgentMessageDisplayMode")]
trusted_contacts: Annotated[TrustedContacts, Field(alias="trustedContacts")]
version: Literal[1]
class AgentMessageDisplay(
RootModel[ShowAgentMessageDisplay | HideAgentMessageDisplay | BlockAgentMessageDisplay]
):
root: Annotated[
ShowAgentMessageDisplay | HideAgentMessageDisplay | BlockAgentMessageDisplay,
Field(description="How Chat should present a recovered answer."),
]
class AgentMessageModeration(BaseModel):
@model_validator(mode="before")
@classmethod
def _read_display(cls, data: Any) -> Any:
if not isinstance(data, dict) or "moderated" not in data or data.get("display") is None:
return data
try:
return {**data, "display": AgentMessageDisplay.model_validate(data["display"])}
except ValidationError:
return {**data, "moderated": True, "display": None}
model_config = ConfigDict(
populate_by_name=True,
)
display: AgentMessageDisplay | None = None
moderated: bool
class AppInfo(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
@@ -12517,6 +12584,34 @@ class SessionSource(RootModel[SessionSourceValue | CustomSessionSource | SubAgen
root: SessionSourceValue | CustomSessionSource | SubAgentSessionSource
class AgentMessageThreadItem(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
completed_at_ms: Annotated[
int | None,
Field(
alias="completedAtMs",
description="Unix timestamp in milliseconds when the item completed, if recorded.",
),
] = None
delivery: AgentMessageDelivery | None = None
id: str
memory_citation: Annotated[MemoryCitation | None, Field(alias="memoryCitation")] = None
moderation: AgentMessageModeration | None = None
phase: MessagePhase | None = None
questions: list[AsyncUserInputQuestion] | None = None
started_at_ms: Annotated[
int | None,
Field(
alias="startedAtMs",
description="Unix timestamp in milliseconds when the item started, if recorded.",
),
] = None
text: str
type: Annotated[Literal["agentMessage"], Field(title="AgentMessageThreadItemType")]
class FunctionCallOutputThreadItem(BaseModel):
model_config = ConfigDict(
populate_by_name=True,