mirror of
https://github.com/openai/codex.git
synced 2026-10-19 17:28:46 +00:00
Expose resumable misalignment details through app server (#40952)
## Why App-server clients need the explanation and steering instruction returned with a misalignment policy violation to offer a user-confirmed continuation. Missing or incomplete details must continue to behave as a terminal block. ## What changed - Parse optional misalignment classification, explanation, and steering details from streamed and HTTP Responses errors and propagate them through core errors. - Include the details in live app-server `error` and `turn/completed` payloads and export the corresponding protocol schemas and TypeScript types. - Keep explanations and steering messages out of serialized rollout events and redact them from debug output. - Document how clients can resume with `turn/start` after user confirmation. ## Testing - Cover streamed, HTTP, and WebSocket-wrapped errors, including malformed and classification-only details. - Verify live app-server notifications expose resumable details without writing the explanation or steering message to the rollout. GitOrigin-RevId: 329258a444c2cd91d0c57ab1720830b33ddcfac5
This commit is contained in:
committed by
copyberry
parent
0e9a2bae52
commit
7276d67081
@@ -442,6 +442,7 @@ fn sample_turn_completed_notification(
|
||||
items: vec![],
|
||||
status,
|
||||
error: codex_error_info.map(|codex_error_info| AppServerTurnError {
|
||||
misalignment: None,
|
||||
message: "turn failed".to_string(),
|
||||
codex_error_info: Some(codex_error_info),
|
||||
additional_details: None,
|
||||
@@ -547,6 +548,7 @@ fn non_steerable_review_error() -> JSONRPCErrorError {
|
||||
message: "cannot steer a review turn".to_string(),
|
||||
data: Some(
|
||||
serde_json::to_value(AppServerTurnError {
|
||||
misalignment: None,
|
||||
message: "cannot steer a review turn".to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::ActiveTurnNotSteerable {
|
||||
turn_kind: NonSteerableTurnKind::Review,
|
||||
|
||||
@@ -2784,6 +2784,47 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"MisalignmentErrorDetails": {
|
||||
"properties": {
|
||||
"detailedExplanation": {
|
||||
"description": "A substantive localized explanation is required before offering continuation.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"errorType": {
|
||||
"description": "Open-ended classification; clients must accept categories added by Responses.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"steer": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MisalignmentSteer"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Instruction to submit as the next turn's user input if continuation is confirmed."
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"MisalignmentSteer": {
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"message"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ModeKind": {
|
||||
"description": "Initial collaboration mode to use when the TUI starts.",
|
||||
"enum": [
|
||||
@@ -5956,6 +5997,18 @@
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"misalignment": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MisalignmentErrorDetails"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Optional public explanation and continuation instruction for a misalignment block."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
+53
@@ -14654,6 +14654,47 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"MisalignmentErrorDetails": {
|
||||
"properties": {
|
||||
"detailedExplanation": {
|
||||
"description": "A substantive localized explanation is required before offering continuation.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"errorType": {
|
||||
"description": "Open-ended classification; clients must accept categories added by Responses.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"steer": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/MisalignmentSteer"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Instruction to submit as the next turn's user input if continuation is confirmed."
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"MisalignmentSteer": {
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"message"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ModeKind": {
|
||||
"description": "Initial collaboration mode to use when the TUI starts.",
|
||||
"enum": [
|
||||
@@ -24043,6 +24084,18 @@
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"misalignment": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/MisalignmentErrorDetails"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Optional public explanation and continuation instruction for a misalignment block."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
+53
@@ -10787,6 +10787,47 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"MisalignmentErrorDetails": {
|
||||
"properties": {
|
||||
"detailedExplanation": {
|
||||
"description": "A substantive localized explanation is required before offering continuation.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"errorType": {
|
||||
"description": "Open-ended classification; clients must accept categories added by Responses.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"steer": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MisalignmentSteer"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Instruction to submit as the next turn's user input if continuation is confirmed."
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"MisalignmentSteer": {
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"message"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ModeKind": {
|
||||
"description": "Initial collaboration mode to use when the TUI starts.",
|
||||
"enum": [
|
||||
@@ -21778,6 +21819,18 @@
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"misalignment": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MisalignmentErrorDetails"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Optional public explanation and continuation instruction for a misalignment block."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
@@ -141,6 +141,47 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"MisalignmentErrorDetails": {
|
||||
"properties": {
|
||||
"detailedExplanation": {
|
||||
"description": "A substantive localized explanation is required before offering continuation.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"errorType": {
|
||||
"description": "Open-ended classification; clients must accept categories added by Responses.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"steer": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MisalignmentSteer"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Instruction to submit as the next turn's user input if continuation is confirmed."
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"MisalignmentSteer": {
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"message"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"NonSteerableTurnKind": {
|
||||
"enum": [
|
||||
"review",
|
||||
@@ -169,6 +210,18 @@
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"misalignment": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MisalignmentErrorDetails"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Optional public explanation and continuation instruction for a misalignment block."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
@@ -634,6 +634,47 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"MisalignmentErrorDetails": {
|
||||
"properties": {
|
||||
"detailedExplanation": {
|
||||
"description": "A substantive localized explanation is required before offering continuation.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"errorType": {
|
||||
"description": "Open-ended classification; clients must accept categories added by Responses.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"steer": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MisalignmentSteer"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Instruction to submit as the next turn's user input if continuation is confirmed."
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"MisalignmentSteer": {
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"message"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"NonSteerableTurnKind": {
|
||||
"enum": [
|
||||
"review",
|
||||
@@ -1636,6 +1677,18 @@
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"misalignment": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MisalignmentErrorDetails"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Optional public explanation and continuation instruction for a misalignment block."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
@@ -738,6 +738,47 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"MisalignmentErrorDetails": {
|
||||
"properties": {
|
||||
"detailedExplanation": {
|
||||
"description": "A substantive localized explanation is required before offering continuation.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"errorType": {
|
||||
"description": "Open-ended classification; clients must accept categories added by Responses.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"steer": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MisalignmentSteer"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Instruction to submit as the next turn's user input if continuation is confirmed."
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"MisalignmentSteer": {
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"message"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"MultiAgentMode": {
|
||||
"description": "Controls the effective multi-agent delegation instructions for a turn. `custom` means the configured mode hint defines the policy instead of a built-in policy.",
|
||||
"oneOf": [
|
||||
@@ -2328,6 +2369,18 @@
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"misalignment": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MisalignmentErrorDetails"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Optional public explanation and continuation instruction for a misalignment block."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
@@ -660,6 +660,47 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"MisalignmentErrorDetails": {
|
||||
"properties": {
|
||||
"detailedExplanation": {
|
||||
"description": "A substantive localized explanation is required before offering continuation.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"errorType": {
|
||||
"description": "Open-ended classification; clients must accept categories added by Responses.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"steer": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MisalignmentSteer"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Instruction to submit as the next turn's user input if continuation is confirmed."
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"MisalignmentSteer": {
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"message"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"NonSteerableTurnKind": {
|
||||
"enum": [
|
||||
"review",
|
||||
@@ -2119,6 +2160,18 @@
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"misalignment": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MisalignmentErrorDetails"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Optional public explanation and continuation instruction for a misalignment block."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
+53
@@ -660,6 +660,47 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"MisalignmentErrorDetails": {
|
||||
"properties": {
|
||||
"detailedExplanation": {
|
||||
"description": "A substantive localized explanation is required before offering continuation.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"errorType": {
|
||||
"description": "Open-ended classification; clients must accept categories added by Responses.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"steer": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MisalignmentSteer"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Instruction to submit as the next turn's user input if continuation is confirmed."
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"MisalignmentSteer": {
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"message"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"NonSteerableTurnKind": {
|
||||
"enum": [
|
||||
"review",
|
||||
@@ -2119,6 +2160,18 @@
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"misalignment": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MisalignmentErrorDetails"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Optional public explanation and continuation instruction for a misalignment block."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
@@ -660,6 +660,47 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"MisalignmentErrorDetails": {
|
||||
"properties": {
|
||||
"detailedExplanation": {
|
||||
"description": "A substantive localized explanation is required before offering continuation.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"errorType": {
|
||||
"description": "Open-ended classification; clients must accept categories added by Responses.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"steer": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MisalignmentSteer"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Instruction to submit as the next turn's user input if continuation is confirmed."
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"MisalignmentSteer": {
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"message"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"NonSteerableTurnKind": {
|
||||
"enum": [
|
||||
"review",
|
||||
@@ -2119,6 +2160,18 @@
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"misalignment": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MisalignmentErrorDetails"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Optional public explanation and continuation instruction for a misalignment block."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
@@ -738,6 +738,47 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"MisalignmentErrorDetails": {
|
||||
"properties": {
|
||||
"detailedExplanation": {
|
||||
"description": "A substantive localized explanation is required before offering continuation.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"errorType": {
|
||||
"description": "Open-ended classification; clients must accept categories added by Responses.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"steer": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MisalignmentSteer"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Instruction to submit as the next turn's user input if continuation is confirmed."
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"MisalignmentSteer": {
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"message"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"MultiAgentMode": {
|
||||
"description": "Controls the effective multi-agent delegation instructions for a turn. `custom` means the configured mode hint defines the policy instead of a built-in policy.",
|
||||
"oneOf": [
|
||||
@@ -2328,6 +2369,18 @@
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"misalignment": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MisalignmentErrorDetails"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Optional public explanation and continuation instruction for a misalignment block."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
@@ -660,6 +660,47 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"MisalignmentErrorDetails": {
|
||||
"properties": {
|
||||
"detailedExplanation": {
|
||||
"description": "A substantive localized explanation is required before offering continuation.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"errorType": {
|
||||
"description": "Open-ended classification; clients must accept categories added by Responses.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"steer": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MisalignmentSteer"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Instruction to submit as the next turn's user input if continuation is confirmed."
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"MisalignmentSteer": {
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"message"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"NonSteerableTurnKind": {
|
||||
"enum": [
|
||||
"review",
|
||||
@@ -2119,6 +2160,18 @@
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"misalignment": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MisalignmentErrorDetails"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Optional public explanation and continuation instruction for a misalignment block."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
@@ -660,6 +660,47 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"MisalignmentErrorDetails": {
|
||||
"properties": {
|
||||
"detailedExplanation": {
|
||||
"description": "A substantive localized explanation is required before offering continuation.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"errorType": {
|
||||
"description": "Open-ended classification; clients must accept categories added by Responses.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"steer": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MisalignmentSteer"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Instruction to submit as the next turn's user input if continuation is confirmed."
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"MisalignmentSteer": {
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"message"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"NonSteerableTurnKind": {
|
||||
"enum": [
|
||||
"review",
|
||||
@@ -2119,6 +2160,18 @@
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"misalignment": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MisalignmentErrorDetails"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Optional public explanation and continuation instruction for a misalignment block."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
@@ -738,6 +738,47 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"MisalignmentErrorDetails": {
|
||||
"properties": {
|
||||
"detailedExplanation": {
|
||||
"description": "A substantive localized explanation is required before offering continuation.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"errorType": {
|
||||
"description": "Open-ended classification; clients must accept categories added by Responses.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"steer": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MisalignmentSteer"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Instruction to submit as the next turn's user input if continuation is confirmed."
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"MisalignmentSteer": {
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"message"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"MultiAgentMode": {
|
||||
"description": "Controls the effective multi-agent delegation instructions for a turn. `custom` means the configured mode hint defines the policy instead of a built-in policy.",
|
||||
"oneOf": [
|
||||
@@ -2328,6 +2369,18 @@
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"misalignment": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MisalignmentErrorDetails"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Optional public explanation and continuation instruction for a misalignment block."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
@@ -660,6 +660,47 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"MisalignmentErrorDetails": {
|
||||
"properties": {
|
||||
"detailedExplanation": {
|
||||
"description": "A substantive localized explanation is required before offering continuation.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"errorType": {
|
||||
"description": "Open-ended classification; clients must accept categories added by Responses.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"steer": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MisalignmentSteer"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Instruction to submit as the next turn's user input if continuation is confirmed."
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"MisalignmentSteer": {
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"message"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"NonSteerableTurnKind": {
|
||||
"enum": [
|
||||
"review",
|
||||
@@ -2119,6 +2160,18 @@
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"misalignment": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MisalignmentErrorDetails"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Optional public explanation and continuation instruction for a misalignment block."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
@@ -634,6 +634,47 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"MisalignmentErrorDetails": {
|
||||
"properties": {
|
||||
"detailedExplanation": {
|
||||
"description": "A substantive localized explanation is required before offering continuation.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"errorType": {
|
||||
"description": "Open-ended classification; clients must accept categories added by Responses.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"steer": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MisalignmentSteer"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Instruction to submit as the next turn's user input if continuation is confirmed."
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"MisalignmentSteer": {
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"message"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"NonSteerableTurnKind": {
|
||||
"enum": [
|
||||
"review",
|
||||
@@ -1636,6 +1677,18 @@
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"misalignment": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MisalignmentErrorDetails"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Optional public explanation and continuation instruction for a misalignment block."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
@@ -660,6 +660,47 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"MisalignmentErrorDetails": {
|
||||
"properties": {
|
||||
"detailedExplanation": {
|
||||
"description": "A substantive localized explanation is required before offering continuation.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"errorType": {
|
||||
"description": "Open-ended classification; clients must accept categories added by Responses.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"steer": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MisalignmentSteer"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Instruction to submit as the next turn's user input if continuation is confirmed."
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"MisalignmentSteer": {
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"message"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"NonSteerableTurnKind": {
|
||||
"enum": [
|
||||
"review",
|
||||
@@ -2119,6 +2160,18 @@
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"misalignment": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MisalignmentErrorDetails"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Optional public explanation and continuation instruction for a misalignment block."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
@@ -634,6 +634,47 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"MisalignmentErrorDetails": {
|
||||
"properties": {
|
||||
"detailedExplanation": {
|
||||
"description": "A substantive localized explanation is required before offering continuation.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"errorType": {
|
||||
"description": "Open-ended classification; clients must accept categories added by Responses.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"steer": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MisalignmentSteer"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Instruction to submit as the next turn's user input if continuation is confirmed."
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"MisalignmentSteer": {
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"message"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"NonSteerableTurnKind": {
|
||||
"enum": [
|
||||
"review",
|
||||
@@ -1636,6 +1677,18 @@
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"misalignment": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MisalignmentErrorDetails"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Optional public explanation and continuation instruction for a misalignment block."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
@@ -634,6 +634,47 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"MisalignmentErrorDetails": {
|
||||
"properties": {
|
||||
"detailedExplanation": {
|
||||
"description": "A substantive localized explanation is required before offering continuation.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"errorType": {
|
||||
"description": "Open-ended classification; clients must accept categories added by Responses.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"steer": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MisalignmentSteer"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Instruction to submit as the next turn's user input if continuation is confirmed."
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"MisalignmentSteer": {
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"message"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"NonSteerableTurnKind": {
|
||||
"enum": [
|
||||
"review",
|
||||
@@ -1636,6 +1677,18 @@
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"misalignment": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MisalignmentErrorDetails"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Optional public explanation and continuation instruction for a misalignment block."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
@@ -634,6 +634,47 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"MisalignmentErrorDetails": {
|
||||
"properties": {
|
||||
"detailedExplanation": {
|
||||
"description": "A substantive localized explanation is required before offering continuation.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"errorType": {
|
||||
"description": "Open-ended classification; clients must accept categories added by Responses.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"steer": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MisalignmentSteer"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Instruction to submit as the next turn's user input if continuation is confirmed."
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"MisalignmentSteer": {
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"message"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"NonSteerableTurnKind": {
|
||||
"enum": [
|
||||
"review",
|
||||
@@ -1636,6 +1677,18 @@
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"misalignment": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MisalignmentErrorDetails"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Optional public explanation and continuation instruction for a misalignment block."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
+18
@@ -0,0 +1,18 @@
|
||||
// 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 { MisalignmentSteer } from "./MisalignmentSteer";
|
||||
|
||||
export type MisalignmentErrorDetails = {
|
||||
/**
|
||||
* Open-ended classification; clients must accept categories added by Responses.
|
||||
*/
|
||||
errorType: string | null,
|
||||
/**
|
||||
* A substantive localized explanation is required before offering continuation.
|
||||
*/
|
||||
detailedExplanation: string | null,
|
||||
/**
|
||||
* Instruction to submit as the next turn's user input if continuation is confirmed.
|
||||
*/
|
||||
steer: MisalignmentSteer | null, };
|
||||
@@ -0,0 +1,5 @@
|
||||
// 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 MisalignmentSteer = { message: string, };
|
||||
@@ -2,5 +2,10 @@
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { CodexErrorInfo } from "./CodexErrorInfo";
|
||||
import type { MisalignmentErrorDetails } from "./MisalignmentErrorDetails";
|
||||
|
||||
export type TurnError = { message: string, codexErrorInfo: CodexErrorInfo | null, additionalDetails: string | null, };
|
||||
export type TurnError = { message: string, codexErrorInfo: CodexErrorInfo | null, additionalDetails: string | null,
|
||||
/**
|
||||
* Optional public explanation and continuation instruction for a misalignment block.
|
||||
*/
|
||||
misalignment: MisalignmentErrorDetails | null, };
|
||||
|
||||
@@ -297,6 +297,8 @@ export type { MemoryCitation } from "./MemoryCitation";
|
||||
export type { MemoryCitationEntry } from "./MemoryCitationEntry";
|
||||
export type { MergeStrategy } from "./MergeStrategy";
|
||||
export type { MigrationDetails } from "./MigrationDetails";
|
||||
export type { MisalignmentErrorDetails } from "./MisalignmentErrorDetails";
|
||||
export type { MisalignmentSteer } from "./MisalignmentSteer";
|
||||
export type { Model } from "./Model";
|
||||
export type { ModelAvailabilityNux } from "./ModelAvailabilityNux";
|
||||
export type { ModelListParams } from "./ModelListParams";
|
||||
|
||||
@@ -1192,6 +1192,7 @@ impl ThreadHistoryBuilder {
|
||||
let changed_turn = if let Some(turn) = self.current_turn.as_mut() {
|
||||
turn.status = TurnStatus::Failed;
|
||||
turn.error = Some(V2TurnError {
|
||||
misalignment: payload.misalignment.clone().map(Into::into),
|
||||
message: payload.message.clone(),
|
||||
codex_error_info: payload.codex_error_info.clone().map(Into::into),
|
||||
additional_details: None,
|
||||
@@ -1250,6 +1251,7 @@ impl ThreadHistoryBuilder {
|
||||
|
||||
fn handle_turn_complete(&mut self, payload: &TurnCompleteEvent) {
|
||||
let terminal_error = payload.error.as_ref().map(|error| V2TurnError {
|
||||
misalignment: error.misalignment.clone().map(Into::into),
|
||||
message: error.message.clone(),
|
||||
codex_error_info: error.codex_error_info.clone().map(Into::into),
|
||||
additional_details: None,
|
||||
@@ -3821,6 +3823,7 @@ mod tests {
|
||||
started_at: Some(10),
|
||||
last_agent_message: None,
|
||||
error: Some(ErrorEvent {
|
||||
misalignment: None,
|
||||
message: "Selected model is at capacity. Please try a different model.".into(),
|
||||
codex_error_info: Some(CodexErrorInfo::ServerOverloaded),
|
||||
}),
|
||||
@@ -3851,6 +3854,7 @@ mod tests {
|
||||
}],
|
||||
status: TurnStatus::Failed,
|
||||
error: Some(TurnError {
|
||||
misalignment: None,
|
||||
message: "Selected model is at capacity. Please try a different model."
|
||||
.into(),
|
||||
codex_error_info: Some(
|
||||
@@ -4203,6 +4207,7 @@ mod tests {
|
||||
delivery: None,
|
||||
}),
|
||||
EventMsg::Error(ErrorEvent {
|
||||
misalignment: None,
|
||||
message: "rollback failed".into(),
|
||||
codex_error_info: Some(CodexErrorInfo::ThreadRollbackFailed),
|
||||
}),
|
||||
@@ -4246,6 +4251,7 @@ mod tests {
|
||||
time_to_first_token_ms: None,
|
||||
}),
|
||||
EventMsg::Error(ErrorEvent {
|
||||
misalignment: None,
|
||||
message: "request-level failure".into(),
|
||||
codex_error_info: Some(CodexErrorInfo::BadRequest),
|
||||
}),
|
||||
@@ -4298,6 +4304,7 @@ mod tests {
|
||||
..Default::default()
|
||||
}),
|
||||
EventMsg::Error(ErrorEvent {
|
||||
misalignment: None,
|
||||
message: "stream failure".into(),
|
||||
codex_error_info: Some(CodexErrorInfo::ResponseStreamDisconnected {
|
||||
http_status_code: Some(502),
|
||||
@@ -4325,6 +4332,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
turns[0].error,
|
||||
Some(TurnError {
|
||||
misalignment: None,
|
||||
message: "stream failure".into(),
|
||||
codex_error_info: Some(
|
||||
crate::protocol::v2::CodexErrorInfo::ResponseStreamDisconnected {
|
||||
@@ -4359,6 +4367,7 @@ mod tests {
|
||||
started_at: Some(10),
|
||||
last_agent_message: None,
|
||||
error: Some(ErrorEvent {
|
||||
misalignment: None,
|
||||
message: "Selected model is at capacity. Please try a different model.".into(),
|
||||
codex_error_info: Some(CodexErrorInfo::ServerOverloaded),
|
||||
}),
|
||||
@@ -4388,6 +4397,7 @@ mod tests {
|
||||
}],
|
||||
status: TurnStatus::Failed,
|
||||
error: Some(TurnError {
|
||||
misalignment: None,
|
||||
message: "Selected model is at capacity. Please try a different model.".into(),
|
||||
codex_error_info: Some(crate::protocol::v2::CodexErrorInfo::ServerOverloaded),
|
||||
additional_details: None,
|
||||
|
||||
@@ -40,6 +40,7 @@ pub fn project_rollout_line(line: &RolloutLine) -> ThreadHistoryChangeSet {
|
||||
TurnStatus::Completed
|
||||
},
|
||||
error: event.error.as_ref().map(|error| TurnError {
|
||||
misalignment: error.misalignment.clone().map(Into::into),
|
||||
message: error.message.clone(),
|
||||
codex_error_info: error.codex_error_info.clone().map(Into::into),
|
||||
additional_details: None,
|
||||
|
||||
@@ -68,6 +68,7 @@ fn projects_turn_lifecycle_without_prior_builder_state() {
|
||||
#[test]
|
||||
fn projects_failed_turn_completion_as_snapshot() {
|
||||
let error = ErrorEvent {
|
||||
misalignment: None,
|
||||
message: "request failed".to_string(),
|
||||
codex_error_info: None,
|
||||
};
|
||||
@@ -91,6 +92,7 @@ fn projects_failed_turn_completion_as_snapshot() {
|
||||
turn_id: "turn-1".to_string(),
|
||||
status: TurnStatus::Failed,
|
||||
error: Some(TurnError {
|
||||
misalignment: None,
|
||||
message: "request failed".to_string(),
|
||||
codex_error_info: None,
|
||||
additional_details: None,
|
||||
|
||||
@@ -5,6 +5,8 @@ use super::TurnStatus;
|
||||
use crate::JsonSchema;
|
||||
use crate::TS;
|
||||
use codex_experimental_api_macros::ExperimentalApi;
|
||||
use codex_protocol::protocol::MisalignmentErrorDetails as CoreMisalignmentErrorDetails;
|
||||
use codex_protocol::protocol::MisalignmentSteer as CoreMisalignmentSteer;
|
||||
use codex_protocol::protocol::SessionSource as CoreSessionSource;
|
||||
use codex_protocol::protocol::SubAgentSource as CoreSubAgentSource;
|
||||
use codex_protocol::protocol::ThreadHistoryMode as CoreThreadHistoryMode;
|
||||
@@ -16,6 +18,7 @@ use schemars::r#gen::SchemaGenerator;
|
||||
use schemars::schema::Schema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::fmt;
|
||||
use std::path::PathBuf;
|
||||
use thiserror::Error;
|
||||
|
||||
@@ -393,4 +396,67 @@ pub struct TurnError {
|
||||
pub codex_error_info: Option<CodexErrorInfo>,
|
||||
#[serde(default)]
|
||||
pub additional_details: Option<String>,
|
||||
/// Optional public explanation and continuation instruction for a misalignment block.
|
||||
#[serde(default)]
|
||||
pub misalignment: Option<MisalignmentErrorDetails>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct MisalignmentErrorDetails {
|
||||
/// Open-ended classification; clients must accept categories added by Responses.
|
||||
pub error_type: Option<String>,
|
||||
/// A substantive localized explanation is required before offering continuation.
|
||||
pub detailed_explanation: Option<String>,
|
||||
/// Instruction to submit as the next turn's user input if continuation is confirmed.
|
||||
pub steer: Option<MisalignmentSteer>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for MisalignmentErrorDetails {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter
|
||||
.debug_struct("MisalignmentErrorDetails")
|
||||
.field("error_type", &self.error_type)
|
||||
.field(
|
||||
"has_detailed_explanation",
|
||||
&self.detailed_explanation.is_some(),
|
||||
)
|
||||
.field("has_steer", &self.steer.is_some())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CoreMisalignmentErrorDetails> for MisalignmentErrorDetails {
|
||||
fn from(value: CoreMisalignmentErrorDetails) -> Self {
|
||||
Self {
|
||||
error_type: value.error_type,
|
||||
detailed_explanation: value.detailed_explanation,
|
||||
steer: value.steer.map(Into::into),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct MisalignmentSteer {
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
impl fmt::Debug for MisalignmentSteer {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter
|
||||
.debug_struct("MisalignmentSteer")
|
||||
.field("message", &"[REDACTED]")
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CoreMisalignmentSteer> for MisalignmentSteer {
|
||||
fn from(value: CoreMisalignmentSteer) -> Self {
|
||||
Self {
|
||||
message: value.message,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1680,7 +1680,7 @@ Because audio is intentionally separate from `ThreadItem`, clients can opt out o
|
||||
The app-server streams JSON-RPC notifications while a turn is running. Each turn emits `turn/started` when it begins running and ends with `turn/completed` (final `turn` status). Token usage events stream separately via `thread/tokenUsage/updated`. Clients subscribe to the events they care about, rendering each item incrementally as updates arrive. The per-item lifecycle is always: `item/started` → zero or more item-specific deltas → `item/completed`.
|
||||
|
||||
- `turn/started` — `{ turn }` with the turn id, empty `items`, and `status: "inProgress"`.
|
||||
- `turn/completed` — `{ turn }` where `turn.status` is `completed`, `interrupted`, or `failed`; successful turns include their final agent message when available, and failures carry `{ error: { message, codexErrorInfo?, additionalDetails? } }`.
|
||||
- `turn/completed` — `{ turn }` where `turn.status` is `completed`, `interrupted`, or `failed`; successful turns include their final agent message when available, and failures carry `{ error: { message, codexErrorInfo?, additionalDetails?, misalignment? } }`.
|
||||
- `turn/diff/updated` — `{ threadId, turnId, diff }` represents the up-to-date snapshot of the turn-level unified diff, emitted after every FileChange item. `diff` is the latest aggregated unified diff across every file change in the turn. UIs can render this to show the full "what changed" view without stitching individual `fileChange` items.
|
||||
- `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`.
|
||||
- `rawResponse/completed` — internal-only; when `thread/start.experimentalRawEvents` is enabled, emits `{ threadId, turnId, responseId, usage }` once for each upstream Responses API completion. `usage` is the exact upstream usage payload mapped to the app-server token breakdown shape and is `null` when the upstream completion omitted usage. Unlike `thread/tokenUsage/updated`, this notification is not accumulated, estimated, persisted, or replayed.
|
||||
@@ -1760,7 +1760,7 @@ There are additional item-specific events:
|
||||
|
||||
Ownership rejections for parent-owned Multi-Agent V2 subagents return JSON-RPC error code `-32600` with message `direct app-server input is not allowed for multi-agent v2 sub-agents`.
|
||||
|
||||
`error` event is emitted whenever the server hits an error mid-turn (for example, upstream model errors or quota limits). Carries the same `{ error: { message, codexErrorInfo?, additionalDetails? } }` payload as `turn.status: "failed"` and may precede that terminal notification.
|
||||
`error` event is emitted whenever the server hits an error mid-turn (for example, upstream model errors or quota limits). Carries the same `{ error: { message, codexErrorInfo?, additionalDetails?, misalignment? } }` payload as `turn.status: "failed"` and may precede that terminal notification.
|
||||
|
||||
`codexErrorInfo` maps to the `CodexErrorInfo` enum. Common values:
|
||||
|
||||
@@ -1783,6 +1783,16 @@ Ownership rejections for parent-owned Multi-Agent V2 subagents return JSON-RPC e
|
||||
|
||||
When an upstream HTTP status is available (for example, from the Responses API or a provider), it is forwarded in `httpStatusCode` on the relevant `codexErrorInfo` variant.
|
||||
|
||||
For `misalignmentPolicyViolation`, optional `misalignment` details contain `errorType`,
|
||||
`detailedExplanation`, and `steer: { message }`. Error categories are open-ended. A category alone
|
||||
remains a terminal block; clients may offer continuation only when both a substantive explanation
|
||||
and a steering message are present. To continue after user confirmation, submit the steering
|
||||
message with the existing `turn/start` method and include
|
||||
`responsesapiClientMetadata: { misalignment_override: JSON.stringify({ timestamp, feedback }) }`,
|
||||
where `timestamp` is the confirmation time in Unix milliseconds and `feedback` is the user's
|
||||
explanation. Misalignment explanation and steering details are delivered live but excluded from
|
||||
persisted rollout errors, so unavailable details after a restart remain a terminal block.
|
||||
|
||||
## Approvals
|
||||
|
||||
Certain actions (shell commands or modifying files) may require explicit user approval depending on the user's config. When `turn/start` is used, the app-server drives an approval flow by sending a server-initiated JSON-RPC request to the client. The client must respond to tell Codex whether to proceed. UIs should present these requests inline with the active turn so users can review the proposed command or diff before choosing.
|
||||
|
||||
@@ -965,6 +965,7 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
}
|
||||
|
||||
let turn_error = TurnError {
|
||||
misalignment: ev.misalignment.map(Into::into),
|
||||
message: ev.message,
|
||||
codex_error_info: ev.codex_error_info.map(V2CodexErrorInfo::from),
|
||||
additional_details: None,
|
||||
@@ -982,6 +983,7 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
// We don't need to update the turn summary store for stream errors as they are intermediate error states for retries,
|
||||
// but we notify the client.
|
||||
let turn_error = TurnError {
|
||||
misalignment: None,
|
||||
message: ev.message,
|
||||
codex_error_info: ev.codex_error_info.map(V2CodexErrorInfo::from),
|
||||
additional_details: ev.additional_details,
|
||||
@@ -1831,6 +1833,7 @@ async fn on_request_permissions_response(
|
||||
conversation_id,
|
||||
&turn_id,
|
||||
TurnError {
|
||||
misalignment: None,
|
||||
message,
|
||||
codex_error_info: None,
|
||||
additional_details: None,
|
||||
@@ -3303,6 +3306,7 @@ mod tests {
|
||||
handle_error(
|
||||
conversation_id,
|
||||
TurnError {
|
||||
misalignment: None,
|
||||
message: "boom".to_string(),
|
||||
codex_error_info: Some(V2CodexErrorInfo::InternalServerError),
|
||||
additional_details: None,
|
||||
@@ -3315,6 +3319,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
turn_summary.last_error,
|
||||
Some(TurnError {
|
||||
misalignment: None,
|
||||
message: "boom".to_string(),
|
||||
codex_error_info: Some(V2CodexErrorInfo::InternalServerError),
|
||||
additional_details: None,
|
||||
@@ -3703,6 +3708,7 @@ mod tests {
|
||||
handle_error(
|
||||
conversation_id,
|
||||
TurnError {
|
||||
misalignment: None,
|
||||
message: "oops".to_string(),
|
||||
codex_error_info: None,
|
||||
additional_details: None,
|
||||
@@ -3753,6 +3759,7 @@ mod tests {
|
||||
handle_error(
|
||||
conversation_id,
|
||||
TurnError {
|
||||
misalignment: None,
|
||||
message: "bad".to_string(),
|
||||
codex_error_info: Some(V2CodexErrorInfo::Other),
|
||||
additional_details: None,
|
||||
@@ -3788,6 +3795,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
n.turn.error,
|
||||
Some(TurnError {
|
||||
misalignment: None,
|
||||
message: "bad".to_string(),
|
||||
codex_error_info: Some(V2CodexErrorInfo::Other),
|
||||
additional_details: None,
|
||||
@@ -4000,6 +4008,7 @@ mod tests {
|
||||
handle_error(
|
||||
conversation_a,
|
||||
TurnError {
|
||||
misalignment: None,
|
||||
message: "a1".to_string(),
|
||||
codex_error_info: Some(V2CodexErrorInfo::BadRequest),
|
||||
additional_details: None,
|
||||
@@ -4021,6 +4030,7 @@ mod tests {
|
||||
handle_error(
|
||||
conversation_b,
|
||||
TurnError {
|
||||
misalignment: None,
|
||||
message: "b1".to_string(),
|
||||
codex_error_info: None,
|
||||
additional_details: None,
|
||||
@@ -4057,6 +4067,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
n.turn.error,
|
||||
Some(TurnError {
|
||||
misalignment: None,
|
||||
message: "a1".to_string(),
|
||||
codex_error_info: Some(V2CodexErrorInfo::BadRequest),
|
||||
additional_details: None,
|
||||
@@ -4075,6 +4086,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
n.turn.error,
|
||||
Some(TurnError {
|
||||
misalignment: None,
|
||||
message: "b1".to_string(),
|
||||
codex_error_info: None,
|
||||
additional_details: None,
|
||||
|
||||
@@ -5669,6 +5669,7 @@ fn stored_turn_to_api_turn(
|
||||
StoredTurnStatus::InProgress => TurnStatus::InProgress,
|
||||
};
|
||||
let error = turn.error.map(|error| TurnError {
|
||||
misalignment: None,
|
||||
message: error.message,
|
||||
codex_error_info: error.codex_error_info,
|
||||
additional_details: error.additional_details,
|
||||
|
||||
@@ -1028,6 +1028,7 @@ impl TurnRequestProcessor {
|
||||
),
|
||||
};
|
||||
let error = TurnError {
|
||||
misalignment: None,
|
||||
message: message.clone(),
|
||||
codex_error_info: Some(CodexErrorInfo::ActiveTurnNotSteerable {
|
||||
turn_kind: turn_kind.into(),
|
||||
|
||||
@@ -4,6 +4,8 @@ use app_test_support::TestAppServer;
|
||||
use codex_app_server_protocol::ClientRequest;
|
||||
use codex_app_server_protocol::CodexErrorInfo;
|
||||
use codex_app_server_protocol::ErrorNotification;
|
||||
use codex_app_server_protocol::MisalignmentErrorDetails;
|
||||
use codex_app_server_protocol::MisalignmentSteer;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_app_server_protocol::TurnCompletedNotification;
|
||||
@@ -20,6 +22,8 @@ use tokio::time::timeout;
|
||||
use wiremock::ResponseTemplate;
|
||||
|
||||
const MESSAGE: &str = "This request violated the misalignment policy.";
|
||||
const EXPLANATION: &str = "The agent attempted to transfer your files externally.";
|
||||
const STEER: &str = "Continue without transferring the user's files externally.";
|
||||
|
||||
#[tokio::test]
|
||||
async fn streamed_policy_violation_completes_turn_with_typed_terminal_error() -> Result<()> {
|
||||
@@ -41,7 +45,10 @@ async fn streamed_policy_violation_completes_turn_with_typed_terminal_error() ->
|
||||
}),
|
||||
]));
|
||||
|
||||
assert_policy_violation_completes_turn_with_typed_terminal_error(response).await
|
||||
assert_policy_violation_completes_turn_with_typed_terminal_error(
|
||||
response, /*expected_misalignment*/ None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -56,7 +63,10 @@ async fn http_400_policy_violation_completes_turn_with_typed_terminal_error() ->
|
||||
}
|
||||
}));
|
||||
|
||||
assert_policy_violation_completes_turn_with_typed_terminal_error(response).await
|
||||
assert_policy_violation_completes_turn_with_typed_terminal_error(
|
||||
response, /*expected_misalignment*/ None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -71,11 +81,100 @@ async fn http_403_policy_violation_completes_turn_with_typed_terminal_error() ->
|
||||
}
|
||||
}));
|
||||
|
||||
assert_policy_violation_completes_turn_with_typed_terminal_error(response).await
|
||||
assert_policy_violation_completes_turn_with_typed_terminal_error(
|
||||
response, /*expected_misalignment*/ None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn streamed_policy_violation_exposes_resumable_misalignment_details() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let response = responses::sse_response(responses::sse(vec![
|
||||
responses::ev_response_created("resp-misalignment"),
|
||||
serde_json::json!({
|
||||
"type": "response.failed",
|
||||
"response": {
|
||||
"id": "resp-misalignment",
|
||||
"status": "failed",
|
||||
"error": {
|
||||
"code": "misalignment_policy_violation",
|
||||
"message": MESSAGE,
|
||||
"misalignment": {
|
||||
"error_type": "unauthorized_data_transfer",
|
||||
"detailed_explanation": EXPLANATION,
|
||||
"steer": { "message": STEER }
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
]));
|
||||
|
||||
assert_policy_violation_completes_turn_with_typed_terminal_error(
|
||||
response,
|
||||
Some(resumable_misalignment()),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn http_403_policy_violation_exposes_resumable_misalignment_details() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let response = ResponseTemplate::new(403).set_body_json(serde_json::json!({
|
||||
"error": {
|
||||
"code": "misalignment_policy_violation",
|
||||
"message": MESSAGE,
|
||||
"misalignment": {
|
||||
"error_type": "unauthorized_data_transfer",
|
||||
"detailed_explanation": EXPLANATION,
|
||||
"steer": { "message": STEER }
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
assert_policy_violation_completes_turn_with_typed_terminal_error(
|
||||
response,
|
||||
Some(resumable_misalignment()),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn classification_only_policy_violation_preserves_the_terminal_error() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let response = responses::sse_response(responses::sse(vec![
|
||||
responses::ev_response_created("resp-misalignment"),
|
||||
serde_json::json!({
|
||||
"type": "response.failed",
|
||||
"response": {
|
||||
"id": "resp-misalignment",
|
||||
"status": "failed",
|
||||
"error": {
|
||||
"code": "misalignment_policy_violation",
|
||||
"message": MESSAGE,
|
||||
"misalignment": { "error_type": "unsafe_activity" }
|
||||
}
|
||||
}
|
||||
}),
|
||||
]));
|
||||
|
||||
assert_policy_violation_completes_turn_with_typed_terminal_error(
|
||||
response,
|
||||
Some(MisalignmentErrorDetails {
|
||||
error_type: Some("unsafe_activity".to_string()),
|
||||
detailed_explanation: None,
|
||||
steer: None,
|
||||
}),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn assert_policy_violation_completes_turn_with_typed_terminal_error(
|
||||
response: ResponseTemplate,
|
||||
expected_misalignment: Option<MisalignmentErrorDetails>,
|
||||
) -> Result<()> {
|
||||
let server = responses::start_mock_server().await;
|
||||
let response_mock = responses::mount_response_once(&server, response).await;
|
||||
@@ -117,6 +216,7 @@ async fn assert_policy_violation_completes_turn_with_typed_terminal_error(
|
||||
error,
|
||||
ErrorNotification {
|
||||
error: TurnError {
|
||||
misalignment: expected_misalignment.clone(),
|
||||
message: MESSAGE.to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::MisalignmentPolicyViolation),
|
||||
additional_details: None,
|
||||
@@ -138,6 +238,7 @@ async fn assert_policy_violation_completes_turn_with_typed_terminal_error(
|
||||
assert_eq!(
|
||||
completed.turn.error,
|
||||
Some(TurnError {
|
||||
misalignment: expected_misalignment,
|
||||
message: MESSAGE.to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::MisalignmentPolicyViolation),
|
||||
additional_details: None,
|
||||
@@ -145,5 +246,25 @@ async fn assert_policy_violation_completes_turn_with_typed_terminal_error(
|
||||
);
|
||||
response_mock.single_request();
|
||||
|
||||
let rollout = tokio::fs::read_to_string(
|
||||
thread
|
||||
.path
|
||||
.as_ref()
|
||||
.expect("non-ephemeral thread should have a rollout path"),
|
||||
)
|
||||
.await?;
|
||||
assert!(!rollout.contains(EXPLANATION));
|
||||
assert!(!rollout.contains(STEER));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn resumable_misalignment() -> MisalignmentErrorDetails {
|
||||
MisalignmentErrorDetails {
|
||||
error_type: Some("unauthorized_data_transfer".to_string()),
|
||||
detailed_explanation: Some(EXPLANATION.to_string()),
|
||||
steer: Some(MisalignmentSteer {
|
||||
message: STEER.to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,6 +137,7 @@ async fn cyber_policy_response_emits_typed_error_notification_v2() -> Result<()>
|
||||
error,
|
||||
ErrorNotification {
|
||||
error: codex_app_server_protocol::TurnError {
|
||||
misalignment: None,
|
||||
message: CYBER_POLICY_MESSAGE.to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::CyberPolicy),
|
||||
additional_details: None,
|
||||
|
||||
@@ -13,6 +13,7 @@ use codex_protocol::error::ConnectionFailedError;
|
||||
use codex_protocol::error::RetryLimitReachedError;
|
||||
use codex_protocol::error::UnexpectedResponseError;
|
||||
use codex_protocol::error::UsageLimitReachedError;
|
||||
use codex_protocol::protocol::MisalignmentErrorDetails;
|
||||
use http::HeaderMap;
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
@@ -55,9 +56,13 @@ pub fn map_api_error(err: ApiError) -> CodexErr {
|
||||
ApiError::CyberPolicy { message } => {
|
||||
CodexErr::new(CodexErrorDetails::CyberPolicy { message })
|
||||
}
|
||||
ApiError::MisalignmentPolicyViolation { message } => {
|
||||
CodexErr::new(CodexErrorDetails::MisalignmentPolicyViolation { message })
|
||||
}
|
||||
ApiError::MisalignmentPolicyViolation {
|
||||
message,
|
||||
misalignment,
|
||||
} => CodexErr::new(CodexErrorDetails::MisalignmentPolicyViolation {
|
||||
message,
|
||||
misalignment,
|
||||
}),
|
||||
ApiError::Transport(transport) => match transport {
|
||||
TransportError::Http {
|
||||
status,
|
||||
@@ -97,6 +102,9 @@ pub fn map_api_error(err: ApiError) -> CodexErr {
|
||||
});
|
||||
return CodexErr::new(CodexErrorDetails::MisalignmentPolicyViolation {
|
||||
message,
|
||||
misalignment: error.get("misalignment").cloned().and_then(|details| {
|
||||
serde_json::from_value::<MisalignmentErrorDetails>(details).ok()
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -199,10 +199,104 @@ fn assert_misalignment_policy_violation_from_http_body(status: http::StatusCode)
|
||||
body: Some(body),
|
||||
}));
|
||||
|
||||
let CodexErrorDetails::MisalignmentPolicyViolation { message } = err.details() else {
|
||||
let CodexErrorDetails::MisalignmentPolicyViolation {
|
||||
message,
|
||||
misalignment,
|
||||
} = err.details()
|
||||
else {
|
||||
panic!("expected CodexErrorDetails::MisalignmentPolicyViolation, got {err:?}");
|
||||
};
|
||||
assert_eq!(message, "This request violated the misalignment policy.");
|
||||
assert_eq!(misalignment, &None);
|
||||
assert!(!err.is_retryable());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn map_api_error_preserves_misalignment_details_from_403_body() {
|
||||
let body = serde_json::json!({
|
||||
"error": {
|
||||
"message": "This request violated the misalignment policy.",
|
||||
"code": "misalignment_policy_violation",
|
||||
"misalignment": {
|
||||
"error_type": "unauthorized_data_transfer",
|
||||
"detailed_explanation": "The agent attempted an external transfer.",
|
||||
"steer": { "message": "Do not transfer the user's files." }
|
||||
}
|
||||
}
|
||||
})
|
||||
.to_string();
|
||||
let err = map_api_error(ApiError::Transport(TransportError::Http {
|
||||
status: http::StatusCode::FORBIDDEN,
|
||||
url: Some("http://example.com/v1/responses".to_string()),
|
||||
headers: None,
|
||||
body: Some(body),
|
||||
}));
|
||||
|
||||
let CodexErrorDetails::MisalignmentPolicyViolation {
|
||||
message,
|
||||
misalignment,
|
||||
} = err.details()
|
||||
else {
|
||||
panic!("expected CodexErrorDetails::MisalignmentPolicyViolation, got {err:?}");
|
||||
};
|
||||
assert_eq!(message, "This request violated the misalignment policy.");
|
||||
assert_eq!(
|
||||
misalignment,
|
||||
&Some(MisalignmentErrorDetails {
|
||||
error_type: Some("unauthorized_data_transfer".to_string()),
|
||||
detailed_explanation: Some("The agent attempted an external transfer.".to_string()),
|
||||
steer: Some(codex_protocol::protocol::MisalignmentSteer {
|
||||
message: "Do not transfer the user's files.".to_string(),
|
||||
}),
|
||||
})
|
||||
);
|
||||
assert!(!err.is_retryable());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn map_api_error_preserves_misalignment_details_from_wrapped_websocket_error() {
|
||||
let body = serde_json::json!({
|
||||
"type": "error",
|
||||
"status": 403,
|
||||
"error": {
|
||||
"message": "This websocket request violated the misalignment policy.",
|
||||
"code": "misalignment_policy_violation",
|
||||
"misalignment": {
|
||||
"error_type": "future_safety_category",
|
||||
"detailed_explanation": "The agent attempted an external transfer.",
|
||||
"steer": { "message": "Do not transfer the user's files." }
|
||||
}
|
||||
}
|
||||
})
|
||||
.to_string();
|
||||
let err = map_api_error(ApiError::Transport(TransportError::Http {
|
||||
status: http::StatusCode::FORBIDDEN,
|
||||
url: Some("ws://example.com/v1/responses".to_string()),
|
||||
headers: None,
|
||||
body: Some(body),
|
||||
}));
|
||||
|
||||
let CodexErrorDetails::MisalignmentPolicyViolation {
|
||||
message,
|
||||
misalignment,
|
||||
} = err.details()
|
||||
else {
|
||||
panic!("expected CodexErrorDetails::MisalignmentPolicyViolation, got {err:?}");
|
||||
};
|
||||
assert_eq!(
|
||||
message,
|
||||
"This websocket request violated the misalignment policy."
|
||||
);
|
||||
assert_eq!(
|
||||
misalignment,
|
||||
&Some(MisalignmentErrorDetails {
|
||||
error_type: Some("future_safety_category".to_string()),
|
||||
detailed_explanation: Some("The agent attempted an external transfer.".to_string()),
|
||||
steer: Some(codex_protocol::protocol::MisalignmentSteer {
|
||||
message: "Do not transfer the user's files.".to_string(),
|
||||
}),
|
||||
})
|
||||
);
|
||||
assert!(!err.is_retryable());
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::rate_limits::RateLimitError;
|
||||
use codex_client::TransportError;
|
||||
use codex_protocol::protocol::MisalignmentErrorDetails;
|
||||
use http::StatusCode;
|
||||
use std::time::Duration;
|
||||
use thiserror::Error;
|
||||
@@ -35,7 +36,10 @@ pub enum ApiError {
|
||||
#[error("cyber policy: {message}")]
|
||||
CyberPolicy { message: String },
|
||||
#[error("misalignment policy violation: {message}")]
|
||||
MisalignmentPolicyViolation { message: String },
|
||||
MisalignmentPolicyViolation {
|
||||
message: String,
|
||||
misalignment: Option<MisalignmentErrorDetails>,
|
||||
},
|
||||
#[error("server overloaded")]
|
||||
ServerOverloaded,
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ use crate::telemetry::SseTelemetry;
|
||||
use codex_client::ByteStream;
|
||||
use codex_client::StreamResponse;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::protocol::MisalignmentErrorDetails;
|
||||
use codex_protocol::protocol::ModelVerification;
|
||||
use codex_protocol::protocol::TokenUsage;
|
||||
use codex_protocol::protocol::TurnModerationMetadataEvent;
|
||||
@@ -107,6 +108,8 @@ struct Error {
|
||||
message: Option<String>,
|
||||
plan_type: Option<String>,
|
||||
resets_at: Option<i64>,
|
||||
#[serde(default)]
|
||||
misalignment: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -428,7 +431,12 @@ pub fn process_responses_event(
|
||||
"This request was blocked due to a misalignment policy violation."
|
||||
.to_string()
|
||||
});
|
||||
response_error = ApiError::MisalignmentPolicyViolation { message };
|
||||
response_error = ApiError::MisalignmentPolicyViolation {
|
||||
message,
|
||||
misalignment: error.misalignment.and_then(|details| {
|
||||
serde_json::from_value::<MisalignmentErrorDetails>(details).ok()
|
||||
}),
|
||||
};
|
||||
} else if matches!(error.code.as_deref(), Some("invalid_prompt" | "bio_policy"))
|
||||
{
|
||||
let message = error
|
||||
@@ -1212,8 +1220,12 @@ mod tests {
|
||||
|
||||
assert_eq!(events.len(), 1);
|
||||
match &events[0] {
|
||||
Err(ApiError::MisalignmentPolicyViolation { message }) => {
|
||||
Err(ApiError::MisalignmentPolicyViolation {
|
||||
message,
|
||||
misalignment,
|
||||
}) => {
|
||||
assert_eq!(message, "This request violated the misalignment policy.");
|
||||
assert_eq!(misalignment, &None);
|
||||
}
|
||||
other => panic!("unexpected event: {other:?}"),
|
||||
}
|
||||
@@ -1239,7 +1251,7 @@ mod tests {
|
||||
|
||||
assert_eq!(events.len(), 1);
|
||||
match &events[0] {
|
||||
Err(ApiError::MisalignmentPolicyViolation { message }) => assert_eq!(
|
||||
Err(ApiError::MisalignmentPolicyViolation { message, .. }) => assert_eq!(
|
||||
message,
|
||||
"This request was blocked due to a misalignment policy violation."
|
||||
),
|
||||
@@ -1248,6 +1260,78 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn misalignment_policy_violation_preserves_public_continuation_details() {
|
||||
let raw_error = json!({
|
||||
"type": "response.failed",
|
||||
"response": {
|
||||
"id": "resp_fatal_misalignment",
|
||||
"status": "failed",
|
||||
"error": {
|
||||
"code": "misalignment_policy_violation",
|
||||
"message": "This request violated the misalignment policy.",
|
||||
"misalignment": {
|
||||
"error_type": "future_safety_category",
|
||||
"detailed_explanation": "The agent attempted an external transfer.",
|
||||
"steer": { "message": "Do not transfer the user's files." }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
let sse = format!("event: response.failed\ndata: {raw_error}\n\n");
|
||||
let events = collect_events(&[sse.as_bytes()]).await;
|
||||
|
||||
assert_eq!(events.len(), 1);
|
||||
match &events[0] {
|
||||
Err(ApiError::MisalignmentPolicyViolation {
|
||||
message,
|
||||
misalignment,
|
||||
}) => {
|
||||
assert_eq!(message, "This request violated the misalignment policy.");
|
||||
assert_eq!(
|
||||
misalignment,
|
||||
&Some(MisalignmentErrorDetails {
|
||||
error_type: Some("future_safety_category".to_string()),
|
||||
detailed_explanation: Some(
|
||||
"The agent attempted an external transfer.".to_string()
|
||||
),
|
||||
steer: Some(codex_protocol::protocol::MisalignmentSteer {
|
||||
message: "Do not transfer the user's files.".to_string(),
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
other => panic!("unexpected event: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn malformed_misalignment_details_preserve_the_fatal_policy_error() {
|
||||
let raw_error = json!({
|
||||
"type": "response.failed",
|
||||
"response": {
|
||||
"id": "resp_fatal_misalignment",
|
||||
"status": "failed",
|
||||
"error": {
|
||||
"code": "misalignment_policy_violation",
|
||||
"message": "This request violated the misalignment policy.",
|
||||
"misalignment": { "steer": { "message": 42 } }
|
||||
}
|
||||
}
|
||||
});
|
||||
let sse = format!("event: response.failed\ndata: {raw_error}\n\n");
|
||||
let events = collect_events(&[sse.as_bytes()]).await;
|
||||
|
||||
assert_eq!(events.len(), 1);
|
||||
assert_matches!(
|
||||
&events[0],
|
||||
Err(ApiError::MisalignmentPolicyViolation {
|
||||
misalignment: None,
|
||||
..
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn content_policy_errors_without_type_are_invalid_requests() {
|
||||
for (code, expected_message) in [
|
||||
@@ -1913,6 +1997,7 @@ mod tests {
|
||||
code: Some("rate_limit_exceeded".to_string()),
|
||||
plan_type: None,
|
||||
resets_at: None,
|
||||
misalignment: None,
|
||||
};
|
||||
|
||||
let delay = try_parse_retry_after(&err);
|
||||
@@ -1927,6 +2012,7 @@ mod tests {
|
||||
code: Some("rate_limit_exceeded".to_string()),
|
||||
plan_type: None,
|
||||
resets_at: None,
|
||||
misalignment: None,
|
||||
};
|
||||
let delay = try_parse_retry_after(&err);
|
||||
assert_eq!(delay, Some(Duration::from_secs_f64(1.898)));
|
||||
@@ -1940,6 +2026,7 @@ mod tests {
|
||||
code: Some("rate_limit_exceeded".to_string()),
|
||||
plan_type: None,
|
||||
resets_at: None,
|
||||
misalignment: None,
|
||||
};
|
||||
let delay = try_parse_retry_after(&err);
|
||||
assert_eq!(delay, Some(Duration::from_secs(35)));
|
||||
|
||||
@@ -487,6 +487,7 @@ async fn on_event_updates_status_from_task_complete() {
|
||||
(None, AgentStatus::Completed(Some("done".to_string()))),
|
||||
(
|
||||
Some(ErrorEvent {
|
||||
misalignment: None,
|
||||
message: "denied".to_string(),
|
||||
codex_error_info: None,
|
||||
}),
|
||||
@@ -509,6 +510,7 @@ async fn on_event_updates_status_from_task_complete() {
|
||||
#[tokio::test]
|
||||
async fn on_event_updates_status_from_error() {
|
||||
let status = agent_status_from_event(&EventMsg::Error(ErrorEvent {
|
||||
misalignment: None,
|
||||
message: "boom".to_string(),
|
||||
codex_error_info: None,
|
||||
}));
|
||||
|
||||
@@ -2310,6 +2310,7 @@ mod tests {
|
||||
.send(Event {
|
||||
id: "prior-turn".to_string(),
|
||||
msg: EventMsg::Error(ErrorEvent {
|
||||
misalignment: None,
|
||||
message: "stale guardian error".to_string(),
|
||||
codex_error_info: None,
|
||||
}),
|
||||
@@ -2351,6 +2352,7 @@ mod tests {
|
||||
.send(Event {
|
||||
id: "current-turn".to_string(),
|
||||
msg: EventMsg::Error(ErrorEvent {
|
||||
misalignment: None,
|
||||
message: "temporary failure".to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::ServerOverloaded),
|
||||
}),
|
||||
|
||||
@@ -588,6 +588,7 @@ pub(crate) async fn run_legacy_after_agent_hook(
|
||||
return false;
|
||||
};
|
||||
let event = EventMsg::Error(codex_protocol::protocol::ErrorEvent {
|
||||
misalignment: None,
|
||||
message,
|
||||
codex_error_info: Some(CodexErrorInfo::Other),
|
||||
});
|
||||
|
||||
@@ -2505,6 +2505,7 @@ async fn send_conversation_error(
|
||||
sess.send_event_raw(Event {
|
||||
id: sub_id,
|
||||
msg: EventMsg::Error(ErrorEvent {
|
||||
misalignment: None,
|
||||
message,
|
||||
codex_error_info: Some(codex_error_info),
|
||||
}),
|
||||
|
||||
@@ -246,6 +246,7 @@ pub async fn thread_rollback(sess: &Arc<Session>, sub_id: String, num_turns: u32
|
||||
sess.send_event_raw(Event {
|
||||
id: sub_id,
|
||||
msg: EventMsg::Error(ErrorEvent {
|
||||
misalignment: None,
|
||||
message: "num_turns must be >= 1".to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::ThreadRollbackFailed),
|
||||
}),
|
||||
@@ -259,6 +260,7 @@ pub async fn thread_rollback(sess: &Arc<Session>, sub_id: String, num_turns: u32
|
||||
sess.send_event_raw(Event {
|
||||
id: sub_id,
|
||||
msg: EventMsg::Error(ErrorEvent {
|
||||
misalignment: None,
|
||||
message: "Cannot rollback while a turn is in progress.".to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::ThreadRollbackFailed),
|
||||
}),
|
||||
@@ -276,6 +278,7 @@ pub async fn thread_rollback(sess: &Arc<Session>, sub_id: String, num_turns: u32
|
||||
sess.send_event_raw(Event {
|
||||
id: turn_context.sub_id.clone(),
|
||||
msg: EventMsg::Error(ErrorEvent {
|
||||
misalignment: None,
|
||||
message: "thread rollback requires persisted thread history".to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::ThreadRollbackFailed),
|
||||
}),
|
||||
@@ -288,6 +291,7 @@ pub async fn thread_rollback(sess: &Arc<Session>, sub_id: String, num_turns: u32
|
||||
sess.send_event_raw(Event {
|
||||
id: turn_context.sub_id.clone(),
|
||||
msg: EventMsg::Error(ErrorEvent {
|
||||
misalignment: None,
|
||||
message: format!("failed to flush thread persistence for rollback replay: {err}"),
|
||||
codex_error_info: Some(CodexErrorInfo::ThreadRollbackFailed),
|
||||
}),
|
||||
@@ -302,6 +306,7 @@ pub async fn thread_rollback(sess: &Arc<Session>, sub_id: String, num_turns: u32
|
||||
sess.send_event_raw(Event {
|
||||
id: turn_context.sub_id.clone(),
|
||||
msg: EventMsg::Error(ErrorEvent {
|
||||
misalignment: None,
|
||||
message: format!("failed to load thread history for rollback replay: {err}"),
|
||||
codex_error_info: Some(CodexErrorInfo::ThreadRollbackFailed),
|
||||
}),
|
||||
@@ -381,6 +386,7 @@ pub async fn set_thread_memory_mode(sess: &Arc<Session>, sub_id: String, mode: T
|
||||
let event = Event {
|
||||
id: sub_id,
|
||||
msg: EventMsg::Error(ErrorEvent {
|
||||
misalignment: None,
|
||||
message: err.to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::Other),
|
||||
}),
|
||||
@@ -452,6 +458,7 @@ pub async fn shutdown(sess: &Arc<Session>, sub_id: String) -> bool {
|
||||
let event = Event {
|
||||
id: sub_id.clone(),
|
||||
msg: EventMsg::Error(ErrorEvent {
|
||||
misalignment: None,
|
||||
message: "Failed to shutdown thread persistence".to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::Other),
|
||||
}),
|
||||
@@ -500,6 +507,7 @@ pub async fn review(
|
||||
let event = Event {
|
||||
id: sub_id,
|
||||
msg: EventMsg::Error(ErrorEvent {
|
||||
misalignment: None,
|
||||
message: err.to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::Other),
|
||||
}),
|
||||
@@ -536,6 +544,7 @@ pub(super) async fn submission_loop(
|
||||
sess.send_event_raw(Event {
|
||||
id: sub.id.clone(),
|
||||
msg: EventMsg::Error(ErrorEvent {
|
||||
misalignment: None,
|
||||
message: err.to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::Other),
|
||||
}),
|
||||
|
||||
@@ -27,6 +27,7 @@ pub(super) async fn update(
|
||||
.send_event_raw(Event {
|
||||
id: submission_id,
|
||||
msg: EventMsg::Error(ErrorEvent {
|
||||
misalignment: None,
|
||||
message: format!("invalid thread settings override: {error}"),
|
||||
codex_error_info: Some(CodexErrorInfo::BadRequest),
|
||||
}),
|
||||
|
||||
@@ -576,6 +576,7 @@ pub(crate) async fn run_turn(
|
||||
sess.emit_turn_error_lifecycle(turn_context.as_ref(), error.clone())
|
||||
.await;
|
||||
let event = EventMsg::Error(ErrorEvent {
|
||||
misalignment: None,
|
||||
message: "Invalid image in your last message. Please remove it and try again."
|
||||
.to_string(),
|
||||
codex_error_info: Some(error),
|
||||
|
||||
@@ -834,6 +834,7 @@ impl Session {
|
||||
self.send_event_raw(Event {
|
||||
id: sub_id,
|
||||
msg: EventMsg::Error(ErrorEvent {
|
||||
misalignment: None,
|
||||
message: message.clone(),
|
||||
codex_error_info: Some(CodexErrorInfo::BadRequest),
|
||||
}),
|
||||
|
||||
@@ -511,6 +511,7 @@ impl Session {
|
||||
self.send_event_raw(Event {
|
||||
id: submission_id,
|
||||
msg: EventMsg::Error(ErrorEvent {
|
||||
misalignment: None,
|
||||
message: format!("failed to submit turn input: {reason:?}"),
|
||||
codex_error_info: Some(CodexErrorInfo::BadRequest),
|
||||
}),
|
||||
|
||||
@@ -652,6 +652,7 @@ async fn admission_revalidates_constraints_before_committing(kind: TurnStartKind
|
||||
assert_eq!(
|
||||
errors,
|
||||
vec![ErrorEvent {
|
||||
misalignment: None,
|
||||
message: expected_message,
|
||||
codex_error_info: Some(CodexErrorInfo::BadRequest),
|
||||
}]
|
||||
|
||||
@@ -375,6 +375,7 @@ async fn send_user_shell_error(session: &Session, turn_context: &TurnContext, me
|
||||
.send_event(
|
||||
turn_context,
|
||||
EventMsg::Error(ErrorEvent {
|
||||
misalignment: None,
|
||||
message: message.to_string(),
|
||||
codex_error_info: None,
|
||||
}),
|
||||
|
||||
@@ -38,6 +38,7 @@ fn failed_turn_does_not_overwrite_output_last_message_file() {
|
||||
items: Vec::new(),
|
||||
status: TurnStatus::Failed,
|
||||
error: Some(codex_app_server_protocol::TurnError {
|
||||
misalignment: None,
|
||||
message: "turn failed".to_string(),
|
||||
additional_details: None,
|
||||
codex_error_info: None,
|
||||
|
||||
@@ -1561,6 +1561,7 @@ fn failed_turn_clears_stale_final_message() {
|
||||
items: Vec::new(),
|
||||
status: TurnStatus::Failed,
|
||||
error: Some(TurnError {
|
||||
misalignment: None,
|
||||
message: "turn failed".to_string(),
|
||||
additional_details: None,
|
||||
codex_error_info: None,
|
||||
@@ -1617,6 +1618,7 @@ fn turn_failure_prefers_structured_error_message() {
|
||||
|
||||
let error = processor.collect_thread_events(ServerNotification::Error(ErrorNotification {
|
||||
error: TurnError {
|
||||
misalignment: None,
|
||||
message: "backend failed".to_string(),
|
||||
codex_error_info: None,
|
||||
additional_details: Some("request id abc".to_string()),
|
||||
|
||||
@@ -7,6 +7,7 @@ use crate::exec_output::ExecToolCallOutput;
|
||||
use crate::network_policy::NetworkPolicyDecisionPayload;
|
||||
use crate::protocol::CodexErrorInfo;
|
||||
use crate::protocol::ErrorEvent;
|
||||
use crate::protocol::MisalignmentErrorDetails;
|
||||
use crate::protocol::RateLimitReachedType;
|
||||
use crate::protocol::RateLimitSnapshot;
|
||||
use crate::protocol::TruncationPolicy;
|
||||
@@ -136,7 +137,10 @@ pub enum CodexErrorDetails {
|
||||
#[error("{message}")]
|
||||
CyberPolicy { message: String },
|
||||
#[error("{message}")]
|
||||
MisalignmentPolicyViolation { message: String },
|
||||
MisalignmentPolicyViolation {
|
||||
message: String,
|
||||
misalignment: Option<MisalignmentErrorDetails>,
|
||||
},
|
||||
#[error("{0}")]
|
||||
ResponseStreamFailed(ResponseStreamFailed),
|
||||
#[error("{0}")]
|
||||
@@ -469,6 +473,12 @@ impl CodexErr {
|
||||
ErrorEvent {
|
||||
message,
|
||||
codex_error_info: Some(self.to_codex_protocol_error()),
|
||||
misalignment: match &self.details {
|
||||
CodexErrorDetails::MisalignmentPolicyViolation { misalignment, .. } => {
|
||||
misalignment.clone()
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1983,11 +1983,59 @@ pub struct ExitedReviewModeEvent {
|
||||
|
||||
// Individual event payload types matching each `EventMsg` variant.
|
||||
|
||||
/// Public, customer-facing details supplied by the Responses API for a misalignment block.
|
||||
#[derive(Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
|
||||
pub struct MisalignmentErrorDetails {
|
||||
/// Open-ended classification; new values must not prevent the error from being surfaced.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub error_type: Option<String>,
|
||||
/// A localized explanation is required before a client may offer continuation.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub detailed_explanation: Option<String>,
|
||||
/// Model-visible instruction to submit if the user elects to continue.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub steer: Option<MisalignmentSteer>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for MisalignmentErrorDetails {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter
|
||||
.debug_struct("MisalignmentErrorDetails")
|
||||
.field("error_type", &self.error_type)
|
||||
.field(
|
||||
"has_detailed_explanation",
|
||||
&self.detailed_explanation.is_some(),
|
||||
)
|
||||
.field("has_steer", &self.steer.is_some())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// Public steering instruction returned alongside a resumable misalignment block.
|
||||
#[derive(Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
|
||||
pub struct MisalignmentSteer {
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
impl fmt::Debug for MisalignmentSteer {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter
|
||||
.debug_struct("MisalignmentSteer")
|
||||
.field("message", &"[REDACTED]")
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
|
||||
pub struct ErrorEvent {
|
||||
pub message: String,
|
||||
#[serde(default)]
|
||||
pub codex_error_info: Option<CodexErrorInfo>,
|
||||
/// Sensitive explanation and steering are delivered live but never enter rollout storage.
|
||||
#[serde(skip)]
|
||||
#[schemars(skip)]
|
||||
#[ts(skip)]
|
||||
pub misalignment: Option<MisalignmentErrorDetails>,
|
||||
}
|
||||
|
||||
impl ErrorEvent {
|
||||
@@ -5565,6 +5613,7 @@ mod tests {
|
||||
#[test]
|
||||
fn rollback_failed_error_does_not_affect_turn_status() {
|
||||
let event = ErrorEvent {
|
||||
misalignment: None,
|
||||
message: "rollback failed".into(),
|
||||
codex_error_info: Some(CodexErrorInfo::ThreadRollbackFailed),
|
||||
};
|
||||
@@ -5574,6 +5623,7 @@ mod tests {
|
||||
#[test]
|
||||
fn active_turn_not_steerable_error_does_not_affect_turn_status() {
|
||||
let event = ErrorEvent {
|
||||
misalignment: None,
|
||||
message: "cannot steer a review turn".into(),
|
||||
codex_error_info: Some(CodexErrorInfo::ActiveTurnNotSteerable {
|
||||
turn_kind: NonSteerableTurnKind::Review,
|
||||
@@ -5585,12 +5635,44 @@ mod tests {
|
||||
#[test]
|
||||
fn generic_error_affects_turn_status() {
|
||||
let event = ErrorEvent {
|
||||
misalignment: None,
|
||||
message: "generic".into(),
|
||||
codex_error_info: Some(CodexErrorInfo::Other),
|
||||
};
|
||||
assert!(event.affects_turn_status());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn misalignment_explanation_and_steer_are_never_serialized_into_error_events() {
|
||||
let event = ErrorEvent {
|
||||
message: "This request violated the misalignment policy.".to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::MisalignmentPolicyViolation),
|
||||
misalignment: Some(MisalignmentErrorDetails {
|
||||
error_type: Some("unauthorized_data_transfer".to_string()),
|
||||
detailed_explanation: Some("Sensitive customer explanation".to_string()),
|
||||
steer: Some(MisalignmentSteer {
|
||||
message: "Sensitive customer steering".to_string(),
|
||||
}),
|
||||
}),
|
||||
};
|
||||
|
||||
let serialized = serde_json::to_value(&event).expect("serialize error event");
|
||||
assert_eq!(
|
||||
serialized,
|
||||
json!({
|
||||
"message": "This request violated the misalignment policy.",
|
||||
"codex_error_info": "misalignment_policy_violation"
|
||||
})
|
||||
);
|
||||
let restored: ErrorEvent =
|
||||
serde_json::from_value(serialized).expect("deserialize persisted error event");
|
||||
assert_eq!(restored.misalignment, None);
|
||||
|
||||
let debug = format!("{event:?}");
|
||||
assert!(!debug.contains("Sensitive customer explanation"));
|
||||
assert!(!debug.contains("Sensitive customer steering"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn realtime_conversation_started_event_uses_realtime_session_id() {
|
||||
let event = RealtimeConversationStartedEvent {
|
||||
|
||||
@@ -6497,6 +6497,7 @@ fn test_session_telemetry(config: &Config, model: &str) -> SessionTelemetry {
|
||||
#[test]
|
||||
fn active_turn_not_steerable_turn_error_extracts_structured_server_error() {
|
||||
let turn_error = AppServerTurnError {
|
||||
misalignment: None,
|
||||
message: "cannot steer a review turn".to_string(),
|
||||
codex_error_info: Some(AppServerCodexErrorInfo::ActiveTurnNotSteerable {
|
||||
turn_kind: AppServerNonSteerableTurnKind::Review,
|
||||
|
||||
@@ -88,6 +88,7 @@ fn deliver_usage_limit_error(app: &mut App) {
|
||||
app.chat_widget.handle_server_notification(
|
||||
ServerNotification::Error(ErrorNotification {
|
||||
error: AppServerTurnError {
|
||||
misalignment: None,
|
||||
message: "Usage limit reached.".to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::UsageLimitExceeded),
|
||||
additional_details: None,
|
||||
|
||||
@@ -86,6 +86,7 @@ async fn misalignment_policy_blocks_queued_turns_and_goal_resumption() -> Result
|
||||
let queued_turn = next_user_turn_op(&mut op_rx);
|
||||
let failed_turn = Turn {
|
||||
error: Some(AppServerTurnError {
|
||||
misalignment: None,
|
||||
message: "misalignment policy violation".to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::MisalignmentPolicyViolation),
|
||||
additional_details: None,
|
||||
@@ -176,6 +177,7 @@ async fn misalignment_policy_in_parent_stops_active_side_conversation() -> Resul
|
||||
parent_thread_id,
|
||||
ServerNotification::Error(ErrorNotification {
|
||||
error: AppServerTurnError {
|
||||
misalignment: None,
|
||||
message: "misalignment policy violation".to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::MisalignmentPolicyViolation),
|
||||
additional_details: None,
|
||||
|
||||
@@ -1173,6 +1173,7 @@ async fn live_app_server_failed_turn_does_not_duplicate_error_history() {
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::Error(ErrorNotification {
|
||||
error: AppServerTurnError {
|
||||
misalignment: None,
|
||||
message: "permission denied".to_string(),
|
||||
codex_error_info: None,
|
||||
additional_details: None,
|
||||
@@ -1196,6 +1197,7 @@ async fn live_app_server_failed_turn_does_not_duplicate_error_history() {
|
||||
items: Vec::new(),
|
||||
status: AppServerTurnStatus::Failed,
|
||||
error: Some(AppServerTurnError {
|
||||
misalignment: None,
|
||||
message: "permission denied".to_string(),
|
||||
codex_error_info: None,
|
||||
additional_details: None,
|
||||
@@ -1330,6 +1332,7 @@ async fn live_app_server_stream_recovery_restores_previous_status_header() {
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::Error(ErrorNotification {
|
||||
error: AppServerTurnError {
|
||||
misalignment: None,
|
||||
message: "Reconnecting... 1/5".to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::Other),
|
||||
additional_details: None,
|
||||
@@ -1408,6 +1411,7 @@ async fn live_app_server_server_overloaded_error_renders_warning() {
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::Error(ErrorNotification {
|
||||
error: AppServerTurnError {
|
||||
misalignment: None,
|
||||
message: "server overloaded".to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::ServerOverloaded),
|
||||
additional_details: None,
|
||||
@@ -1450,6 +1454,7 @@ async fn live_app_server_cyber_policy_error_renders_dedicated_notice() {
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::Error(ErrorNotification {
|
||||
error: AppServerTurnError {
|
||||
misalignment: None,
|
||||
message: "server fallback message".to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::CyberPolicy),
|
||||
additional_details: None,
|
||||
|
||||
@@ -415,6 +415,7 @@ pub(super) fn handle_error(
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::Error(ErrorNotification {
|
||||
error: AppServerTurnError {
|
||||
misalignment: None,
|
||||
message: message.into(),
|
||||
codex_error_info,
|
||||
additional_details: None,
|
||||
@@ -448,6 +449,7 @@ pub(super) fn handle_stream_error_with_replay(
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::Error(ErrorNotification {
|
||||
error: AppServerTurnError {
|
||||
misalignment: None,
|
||||
message: message.into(),
|
||||
codex_error_info: None,
|
||||
additional_details,
|
||||
|
||||
@@ -95,6 +95,7 @@ async fn replayed_failed_turns_preserve_overload_warnings_between_retries() {
|
||||
/*duration_ms*/ None,
|
||||
/*error*/
|
||||
Some(AppServerTurnError {
|
||||
misalignment: None,
|
||||
message: error_message.to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::ServerOverloaded),
|
||||
additional_details: None,
|
||||
@@ -1024,6 +1025,7 @@ async fn replayed_retryable_app_server_error_keeps_turn_running() {
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::Error(ErrorNotification {
|
||||
error: AppServerTurnError {
|
||||
misalignment: None,
|
||||
message: "Reconnecting... 1/5".to_string(),
|
||||
codex_error_info: None,
|
||||
additional_details: Some("Idle timeout waiting for SSE".to_string()),
|
||||
|
||||
@@ -76,6 +76,7 @@ async fn misalignment_policy_failure_stops_the_thread_and_renders_once() {
|
||||
AppServerTurnStatus::Failed,
|
||||
/*duration_ms*/ None,
|
||||
Some(AppServerTurnError {
|
||||
misalignment: None,
|
||||
message: ERROR_MESSAGE.to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::MisalignmentPolicyViolation),
|
||||
additional_details: None,
|
||||
|
||||
@@ -246,6 +246,7 @@ async fn replayed_turn_completions_do_not_start_live_billing_refreshes() {
|
||||
(
|
||||
TurnStatus::Failed,
|
||||
Some(TurnError {
|
||||
misalignment: None,
|
||||
message: "the replayed turn failed".to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::ServerOverloaded),
|
||||
additional_details: None,
|
||||
@@ -323,6 +324,7 @@ async fn replayed_errors_do_not_start_live_billing_refreshes() {
|
||||
|
||||
let notification = ServerNotification::Error(ErrorNotification {
|
||||
error: TurnError {
|
||||
misalignment: None,
|
||||
message: "the replayed turn failed".to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::ServerOverloaded),
|
||||
additional_details: None,
|
||||
|
||||
Reference in New Issue
Block a user