mirror of
https://github.com/openai/codex.git
synced 2026-11-07 20:30:33 +00:00
Carry targeted moderation decisions on final-answer messages (#59842)
## What changed
Read `final_answer_moderation` metadata and attach `moderation: { moderated: boolean }` to the completed final-answer message only when its item ID matches. Apply this in both normal and plan modes, and reset the pending decision when a new response starts.
Preserve the answer's native item ID and moderation flag through legacy events and history reconstruction. Combine text parts into one legacy event when a moderation decision is present.
Expose nullable moderation data on app-server agent messages and update the JSON schemas, TypeScript types, and Python models.
## Testing
Add an integration test for moderation on completed answers and their legacy events. Extend protocol conversion tests to cover populated moderation data and serialization as `null` when absent.
GitOrigin-RevId: f286b67731a84b3daf5d10b7644f2e31b62f0e53
This commit is contained in:
@@ -109,6 +109,7 @@ pub(super) fn item_summary(item: cloud::ItemSummary) -> Option<protocol::ThreadI
|
||||
text,
|
||||
questions,
|
||||
} => protocol::ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id,
|
||||
text,
|
||||
phase: None,
|
||||
|
||||
@@ -57,6 +57,7 @@ fn completed_reply_preserves_the_full_text_questions_and_timestamp() -> Result<(
|
||||
"thread".into(),
|
||||
"turn".into(),
|
||||
protocol::ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "reply".into(),
|
||||
text: "Choose one".into(),
|
||||
phase: None,
|
||||
|
||||
@@ -1146,6 +1146,7 @@ mod tests {
|
||||
"thread".to_string(),
|
||||
"turn".to_string(),
|
||||
codex_app_server_protocol::ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "item".to_string(),
|
||||
text: text.to_string(),
|
||||
phase: None,
|
||||
|
||||
@@ -177,6 +177,17 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AgentMessageModeration": {
|
||||
"properties": {
|
||||
"moderated": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"moderated"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AgentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -4750,6 +4761,17 @@
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"moderation": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentMessageModeration"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"phase": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
+22
@@ -7387,6 +7387,17 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"AgentMessageModeration": {
|
||||
"properties": {
|
||||
"moderated": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"moderated"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AgentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -23438,6 +23449,17 @@
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"moderation": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/AgentMessageModeration"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"phase": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
+22
@@ -525,6 +525,17 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"AgentMessageModeration": {
|
||||
"properties": {
|
||||
"moderated": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"moderated"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AgentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -21141,6 +21152,17 @@
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"moderation": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentMessageModeration"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"phase": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -11,6 +11,17 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AgentMessageModeration": {
|
||||
"properties": {
|
||||
"moderated": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"moderated"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AsyncUserInputQuestion": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
@@ -886,6 +897,17 @@
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"moderation": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentMessageModeration"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"phase": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -11,6 +11,17 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AgentMessageModeration": {
|
||||
"properties": {
|
||||
"moderated": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"moderated"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AsyncUserInputQuestion": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
@@ -886,6 +897,17 @@
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"moderation": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentMessageModeration"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"phase": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -11,6 +11,17 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AgentMessageModeration": {
|
||||
"properties": {
|
||||
"moderated": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"moderated"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AsyncUserInputQuestion": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
@@ -1091,6 +1102,17 @@
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"moderation": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentMessageModeration"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"phase": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -31,6 +31,17 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AgentMessageModeration": {
|
||||
"properties": {
|
||||
"moderated": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"moderated"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AgentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1705,6 +1716,17 @@
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"moderation": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentMessageModeration"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"phase": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -11,6 +11,17 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AgentMessageModeration": {
|
||||
"properties": {
|
||||
"moderated": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"moderated"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AsyncUserInputQuestion": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
@@ -886,6 +897,17 @@
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"moderation": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentMessageModeration"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"phase": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -11,6 +11,17 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AgentMessageModeration": {
|
||||
"properties": {
|
||||
"moderated": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"moderated"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AgentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1496,6 +1507,17 @@
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"moderation": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentMessageModeration"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"phase": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
+22
@@ -11,6 +11,17 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AgentMessageModeration": {
|
||||
"properties": {
|
||||
"moderated": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"moderated"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AgentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1496,6 +1507,17 @@
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"moderation": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentMessageModeration"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"phase": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -11,6 +11,17 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AgentMessageModeration": {
|
||||
"properties": {
|
||||
"moderated": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"moderated"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AgentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1496,6 +1507,17 @@
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"moderation": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentMessageModeration"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"phase": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -31,6 +31,17 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AgentMessageModeration": {
|
||||
"properties": {
|
||||
"moderated": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"moderated"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AgentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1757,6 +1768,17 @@
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"moderation": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentMessageModeration"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"phase": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -11,6 +11,17 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AgentMessageModeration": {
|
||||
"properties": {
|
||||
"moderated": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"moderated"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AgentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1496,6 +1507,17 @@
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"moderation": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentMessageModeration"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"phase": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -31,6 +31,17 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AgentMessageModeration": {
|
||||
"properties": {
|
||||
"moderated": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"moderated"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AgentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1705,6 +1716,17 @@
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"moderation": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentMessageModeration"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"phase": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -11,6 +11,17 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AgentMessageModeration": {
|
||||
"properties": {
|
||||
"moderated": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"moderated"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AgentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1496,6 +1507,17 @@
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"moderation": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentMessageModeration"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"phase": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -11,6 +11,17 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AgentMessageModeration": {
|
||||
"properties": {
|
||||
"moderated": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"moderated"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AsyncUserInputQuestion": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
@@ -1091,6 +1102,17 @@
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"moderation": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentMessageModeration"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"phase": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -11,6 +11,17 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AgentMessageModeration": {
|
||||
"properties": {
|
||||
"moderated": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"moderated"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AgentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1496,6 +1507,17 @@
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"moderation": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentMessageModeration"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"phase": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -11,6 +11,17 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AgentMessageModeration": {
|
||||
"properties": {
|
||||
"moderated": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"moderated"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AsyncUserInputQuestion": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
@@ -1091,6 +1102,17 @@
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"moderation": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentMessageModeration"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"phase": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -11,6 +11,17 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AgentMessageModeration": {
|
||||
"properties": {
|
||||
"moderated": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"moderated"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AsyncUserInputQuestion": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
@@ -1091,6 +1102,17 @@
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"moderation": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentMessageModeration"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"phase": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -11,6 +11,17 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AgentMessageModeration": {
|
||||
"properties": {
|
||||
"moderated": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"moderated"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AsyncUserInputQuestion": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
@@ -1091,6 +1102,17 @@
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"moderation": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentMessageModeration"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"phase": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -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 AgentMessageModeration = { moderated: boolean, };
|
||||
@@ -10,6 +10,7 @@ import type { SleepItem } from "../SleepItem";
|
||||
import type { WebSearchItem } from "../WebSearchItem";
|
||||
import type { JsonValue } from "../serde_json/JsonValue";
|
||||
import type { AgentMessageDelivery } from "./AgentMessageDelivery";
|
||||
import type { AgentMessageModeration } from "./AgentMessageModeration";
|
||||
import type { AsyncUserInputQuestion } from "./AsyncUserInputQuestion";
|
||||
import type { CollabAgentState } from "./CollabAgentState";
|
||||
import type { CollabAgentTool } from "./CollabAgentTool";
|
||||
@@ -42,7 +43,7 @@ startedAtMs?: number | null,
|
||||
/**
|
||||
* Unix timestamp in milliseconds when the item completed, if recorded.
|
||||
*/
|
||||
completedAtMs?: number | null, } & ({ "type": "userMessage", id: string, clientId: string | null, content: Array<UserInput>, } | { "type": "hookPrompt", id: string, fragments: Array<HookPromptFragment>, } | { "type": "agentMessage", id: string, text: string, phase: MessagePhase | null, memoryCitation: MemoryCitation | null, delivery: AgentMessageDelivery | null, questions: Array<AsyncUserInputQuestion> | null, } | { "type": "functionCallOutput", id: string, name: string, namespace: string | null, output: FunctionCallOutputBody, } | { "type": "plan", id: string, text: string, } | { "type": "reasoning", id: string, summary: Array<string>, content: Array<string>, } | { "type": "commandExecution", id: string,
|
||||
completedAtMs?: number | null, } & ({ "type": "userMessage", id: string, clientId: string | null, content: Array<UserInput>, } | { "type": "hookPrompt", id: string, fragments: Array<HookPromptFragment>, } | { "type": "agentMessage", moderation: AgentMessageModeration | null, id: string, text: string, phase: MessagePhase | null, memoryCitation: MemoryCitation | null, delivery: AgentMessageDelivery | null, questions: Array<AsyncUserInputQuestion> | null, } | { "type": "functionCallOutput", id: string, name: string, namespace: string | null, output: FunctionCallOutputBody, } | { "type": "plan", id: string, text: string, } | { "type": "reasoning", id: string, summary: Array<string>, content: Array<string>, } | { "type": "commandExecution", id: string,
|
||||
/**
|
||||
* Trusted first-party plugin id when this command resolves to one plugin script.
|
||||
*/
|
||||
|
||||
@@ -20,6 +20,7 @@ export type { AdditionalNetworkPermissions } from "./AdditionalNetworkPermission
|
||||
export type { AdditionalPermissionProfile } from "./AdditionalPermissionProfile";
|
||||
export type { AgentMessageDelivery } from "./AgentMessageDelivery";
|
||||
export type { AgentMessageDeltaNotification } from "./AgentMessageDeltaNotification";
|
||||
export type { AgentMessageModeration } from "./AgentMessageModeration";
|
||||
export type { AllowDenyRequirement } from "./AllowDenyRequirement";
|
||||
export type { AnalyticsConfig } from "./AnalyticsConfig";
|
||||
export type { AppBranding } from "./AppBranding";
|
||||
|
||||
@@ -493,6 +493,7 @@ mod tests {
|
||||
fn assistant_completion_count_is_optional_envelope_metadata() {
|
||||
let item = codex_protocol::items::TurnItem::AgentMessage(
|
||||
codex_protocol::items::AgentMessageItem {
|
||||
moderation: None,
|
||||
id: "answer".into(),
|
||||
content: vec![codex_protocol::items::AgentMessageContent::Text {
|
||||
text: "Answer".into(),
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::protocol::item_builders::build_file_change_begin_item;
|
||||
use crate::protocol::item_builders::build_file_change_end_item;
|
||||
use crate::protocol::item_builders::build_item_from_guardian_event;
|
||||
use crate::protocol::item_builders::review_output_text;
|
||||
use crate::protocol::v2::AgentMessageModeration;
|
||||
use crate::protocol::v2::CollabAgentState;
|
||||
use crate::protocol::v2::CollabAgentTool;
|
||||
use crate::protocol::v2::CollabAgentToolCallStatus;
|
||||
@@ -626,8 +627,18 @@ impl ThreadHistoryBuilder {
|
||||
return;
|
||||
}
|
||||
|
||||
let id = self.next_item_id();
|
||||
let legacy_id = self.next_item_id();
|
||||
let id = payload
|
||||
.moderation
|
||||
.as_ref()
|
||||
.map_or(legacy_id, |decision| decision.item_id.clone());
|
||||
self.push_item_in_current_turn(ThreadItemPayload::AgentMessage {
|
||||
moderation: payload
|
||||
.moderation
|
||||
.as_ref()
|
||||
.map(|decision| AgentMessageModeration {
|
||||
moderated: decision.moderated,
|
||||
}),
|
||||
id,
|
||||
text: payload.message.clone(),
|
||||
phase: payload.phase.clone(),
|
||||
@@ -2164,6 +2175,7 @@ mod tests {
|
||||
..Default::default()
|
||||
}),
|
||||
EventMsg::AgentMessage(AgentMessageEvent {
|
||||
moderation: None,
|
||||
message: "Hi there".into(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
@@ -2185,6 +2197,7 @@ mod tests {
|
||||
..Default::default()
|
||||
}),
|
||||
EventMsg::AgentMessage(AgentMessageEvent {
|
||||
moderation: None,
|
||||
message: "Reply two".into(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
@@ -2226,6 +2239,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
first.items[1].payload,
|
||||
ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "item-2".into(),
|
||||
text: "Hi there".into(),
|
||||
phase: None,
|
||||
@@ -2261,6 +2275,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
second.items[1].payload,
|
||||
ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "item-5".into(),
|
||||
text: "Reply two".into(),
|
||||
phase: None,
|
||||
@@ -2977,6 +2992,7 @@ mod tests {
|
||||
},
|
||||
];
|
||||
let events = vec![EventMsg::AgentMessage(AgentMessageEvent {
|
||||
moderation: None,
|
||||
message: "Final reply".into(),
|
||||
phase: Some(CoreMessagePhase::FinalAnswer),
|
||||
memory_citation: None,
|
||||
@@ -2993,6 +3009,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
turns[0].items[0].payload,
|
||||
ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "item-1".into(),
|
||||
text: "Final reply".into(),
|
||||
phase: Some(CoreMessagePhase::FinalAnswer),
|
||||
@@ -3123,6 +3140,7 @@ mod tests {
|
||||
text: "first content".into(),
|
||||
}),
|
||||
EventMsg::AgentMessage(AgentMessageEvent {
|
||||
moderation: None,
|
||||
message: "interlude".into(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
@@ -3173,6 +3191,7 @@ mod tests {
|
||||
..Default::default()
|
||||
}),
|
||||
EventMsg::AgentMessage(AgentMessageEvent {
|
||||
moderation: None,
|
||||
message: "Working...".into(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
@@ -3197,6 +3216,7 @@ mod tests {
|
||||
..Default::default()
|
||||
}),
|
||||
EventMsg::AgentMessage(AgentMessageEvent {
|
||||
moderation: None,
|
||||
message: "Second attempt complete.".into(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
@@ -3229,6 +3249,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
first_turn.items[1].payload,
|
||||
ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "item-2".into(),
|
||||
text: "Working...".into(),
|
||||
phase: None,
|
||||
@@ -3255,6 +3276,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
second_turn.items[1].payload,
|
||||
ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "item-4".into(),
|
||||
text: "Second attempt complete.".into(),
|
||||
phase: None,
|
||||
@@ -3277,6 +3299,7 @@ mod tests {
|
||||
..Default::default()
|
||||
}),
|
||||
EventMsg::AgentMessage(AgentMessageEvent {
|
||||
moderation: None,
|
||||
message: "A1".into(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
@@ -3292,6 +3315,7 @@ mod tests {
|
||||
..Default::default()
|
||||
}),
|
||||
EventMsg::AgentMessage(AgentMessageEvent {
|
||||
moderation: None,
|
||||
message: "A2".into(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
@@ -3308,6 +3332,7 @@ mod tests {
|
||||
..Default::default()
|
||||
}),
|
||||
EventMsg::AgentMessage(AgentMessageEvent {
|
||||
moderation: None,
|
||||
message: "A3".into(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
@@ -3339,6 +3364,7 @@ mod tests {
|
||||
}],
|
||||
},
|
||||
&ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "item-2".into(),
|
||||
text: "A1".into(),
|
||||
phase: None,
|
||||
@@ -3360,6 +3386,7 @@ mod tests {
|
||||
}],
|
||||
},
|
||||
&ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "item-4".into(),
|
||||
text: "A3".into(),
|
||||
phase: None,
|
||||
@@ -3383,6 +3410,7 @@ mod tests {
|
||||
..Default::default()
|
||||
}),
|
||||
EventMsg::AgentMessage(AgentMessageEvent {
|
||||
moderation: None,
|
||||
message: "A1".into(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
@@ -3398,6 +3426,7 @@ mod tests {
|
||||
..Default::default()
|
||||
}),
|
||||
EventMsg::AgentMessage(AgentMessageEvent {
|
||||
moderation: None,
|
||||
message: "A2".into(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
@@ -4399,6 +4428,7 @@ mod tests {
|
||||
thread_id: ThreadId::default(),
|
||||
turn_id: turn_id.clone(),
|
||||
item: CoreTurnItem::AgentMessage(codex_protocol::items::AgentMessageItem {
|
||||
moderation: None,
|
||||
id: id.into(),
|
||||
content: vec![codex_protocol::items::AgentMessageContent::Text {
|
||||
text: text.into(),
|
||||
@@ -4449,6 +4479,7 @@ mod tests {
|
||||
}));
|
||||
for message in ["draft", "answer"] {
|
||||
builder.handle_event(&EventMsg::AgentMessage(AgentMessageEvent {
|
||||
moderation: None,
|
||||
message: message.into(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
@@ -4758,6 +4789,7 @@ mod tests {
|
||||
time_to_first_token_ms: None,
|
||||
}),
|
||||
EventMsg::AgentMessage(AgentMessageEvent {
|
||||
moderation: None,
|
||||
message: "still in b".into(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
@@ -4954,6 +4986,7 @@ mod tests {
|
||||
duration_ms: None,
|
||||
}),
|
||||
EventMsg::AgentMessage(AgentMessageEvent {
|
||||
moderation: None,
|
||||
message: "still in b".into(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
@@ -5233,6 +5266,7 @@ mod tests {
|
||||
..Default::default()
|
||||
}),
|
||||
EventMsg::AgentMessage(AgentMessageEvent {
|
||||
moderation: None,
|
||||
message: "done".into(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
|
||||
@@ -129,6 +129,7 @@ fn projects_completed_canonical_turn_items() {
|
||||
}],
|
||||
});
|
||||
let agent_item = TurnItem::AgentMessage(AgentMessageItem {
|
||||
moderation: None,
|
||||
id: "agent-1".to_string(),
|
||||
content: vec![AgentMessageContent::Text {
|
||||
text: "done".to_string(),
|
||||
|
||||
@@ -28,6 +28,7 @@ use codex_protocol::approvals::GuardianAssessmentDecisionSource as CoreGuardianA
|
||||
use codex_protocol::approvals::GuardianCommandSource as CoreGuardianCommandSource;
|
||||
use codex_protocol::items::AgentMessageContent as CoreAgentMessageContent;
|
||||
pub use codex_protocol::items::AgentMessageDelivery;
|
||||
use codex_protocol::items::AgentMessageModeration as CoreAgentMessageModeration;
|
||||
pub use codex_protocol::items::AsyncUserInputQuestion;
|
||||
use codex_protocol::items::CollabAgentTool as CoreCollabAgentTool;
|
||||
use codex_protocol::items::CollabAgentToolCallStatus as CoreCollabAgentToolCallStatus;
|
||||
@@ -149,6 +150,21 @@ pub enum CommandAction {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct AgentMessageModeration {
|
||||
pub moderated: bool,
|
||||
}
|
||||
|
||||
impl From<CoreAgentMessageModeration> for AgentMessageModeration {
|
||||
fn from(value: CoreAgentMessageModeration) -> Self {
|
||||
Self {
|
||||
moderated: value.moderated,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
@@ -313,6 +329,8 @@ pub enum ThreadItemPayload {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
AgentMessage {
|
||||
#[serde(default)]
|
||||
moderation: Option<AgentMessageModeration>,
|
||||
id: String,
|
||||
text: String,
|
||||
#[serde(default)]
|
||||
@@ -958,6 +976,7 @@ impl From<CoreTurnItem> for ThreadItemPayload {
|
||||
})
|
||||
.collect::<String>();
|
||||
Self::AgentMessage {
|
||||
moderation: agent.moderation.map(Into::into),
|
||||
id: agent.id,
|
||||
text,
|
||||
phase: agent.phase,
|
||||
|
||||
@@ -3260,6 +3260,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
);
|
||||
|
||||
let agent_item = TurnItem::AgentMessage(AgentMessageItem {
|
||||
moderation: None,
|
||||
id: "agent-1".to_string(),
|
||||
content: vec![
|
||||
AgentMessageContent::Text {
|
||||
@@ -3275,9 +3276,17 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
questions: None,
|
||||
});
|
||||
|
||||
let agent_payload = ThreadItemPayload::from(agent_item);
|
||||
assert_eq!(
|
||||
ThreadItemPayload::from(agent_item),
|
||||
serde_json::to_value(&agent_payload)
|
||||
.unwrap()
|
||||
.get("moderation"),
|
||||
Some(&serde_json::Value::Null),
|
||||
);
|
||||
assert_eq!(
|
||||
agent_payload,
|
||||
ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "agent-1".to_string(),
|
||||
text: "Hello world".to_string(),
|
||||
phase: None,
|
||||
@@ -3288,6 +3297,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
);
|
||||
|
||||
let agent_item_with_phase = TurnItem::AgentMessage(AgentMessageItem {
|
||||
moderation: Some(codex_protocol::items::AgentMessageModeration { moderated: true }),
|
||||
id: "agent-2".to_string(),
|
||||
content: vec![AgentMessageContent::Text {
|
||||
text: "final".to_string(),
|
||||
@@ -3309,6 +3319,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
assert_eq!(
|
||||
ThreadItemPayload::from(agent_item_with_phase),
|
||||
ThreadItemPayload::AgentMessage {
|
||||
moderation: Some(AgentMessageModeration { moderated: true }),
|
||||
id: "agent-2".to_string(),
|
||||
text: "final".to_string(),
|
||||
phase: Some(MessagePhase::FinalAnswer),
|
||||
@@ -3327,6 +3338,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
);
|
||||
|
||||
let async_item = ThreadItemPayload::from(TurnItem::AgentMessage(AgentMessageItem {
|
||||
moderation: None,
|
||||
id: "async-1".to_string(),
|
||||
content: vec![AgentMessageContent::Text {
|
||||
text: "Which?".to_string(),
|
||||
@@ -3343,6 +3355,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
serde_json::to_value(&async_item).unwrap(),
|
||||
json!({
|
||||
"type": "agentMessage", "id": "async-1", "text": "Which?", "phase": "final_answer",
|
||||
"moderation": null,
|
||||
"memoryCitation": null, "delivery": "async", "questions": [{"title": "Which?", "options": null}]
|
||||
})
|
||||
);
|
||||
|
||||
@@ -3508,6 +3508,7 @@ mod tests {
|
||||
thread_id: conversation_id,
|
||||
turn_id: event_turn_id.clone(),
|
||||
item: CoreTurnItem::AgentMessage(CoreAgentMessageItem {
|
||||
moderation: None,
|
||||
id: "msg-1".to_string(),
|
||||
content: vec![
|
||||
CoreAgentMessageContent::Text {
|
||||
@@ -3533,6 +3534,7 @@ mod tests {
|
||||
thread_id: conversation_id,
|
||||
turn_id: event_turn_id.clone(),
|
||||
item: CoreTurnItem::AgentMessage(CoreAgentMessageItem {
|
||||
moderation: None,
|
||||
id: "msg-2".to_string(),
|
||||
content: vec![CoreAgentMessageContent::Text {
|
||||
text: " ".to_string(),
|
||||
|
||||
@@ -1176,6 +1176,7 @@ mod tests {
|
||||
"thread-1".to_string(),
|
||||
"turn-1".to_string(),
|
||||
ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "item-1".to_string(),
|
||||
text: "Still working".to_string(),
|
||||
phase: None,
|
||||
|
||||
@@ -72,6 +72,7 @@ mod tests {
|
||||
fn redacts_mcp_success_result_and_removes_image_generation() {
|
||||
let mut thread = test_thread(vec![
|
||||
ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "agent-1".to_string(),
|
||||
text: "kept".to_string(),
|
||||
phase: None,
|
||||
@@ -132,6 +133,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
thread.turns[0].items[0].payload,
|
||||
ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "agent-1".to_string(),
|
||||
text: "kept".to_string(),
|
||||
phase: None,
|
||||
|
||||
@@ -177,6 +177,7 @@ mod tests {
|
||||
..Default::default()
|
||||
})),
|
||||
RolloutItem::EventMsg(EventMsg::AgentMessage(AgentMessageEvent {
|
||||
moderation: None,
|
||||
message: "first answer".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
|
||||
@@ -2141,6 +2141,7 @@ async fn external_agent_config_import_creates_session_rollouts() -> Result<()> {
|
||||
assert_eq!(
|
||||
imported_items.next_back(),
|
||||
Some(&ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "item-4".into(),
|
||||
text: "<EXTERNAL SESSION IMPORTED>".into(),
|
||||
phase: None,
|
||||
|
||||
@@ -708,6 +708,7 @@ async fn thread_search_occurrences_reads_paginated_projection() -> Result<()> {
|
||||
thread_id,
|
||||
"turn-1",
|
||||
CoreTurnItem::AgentMessage(AgentMessageItem {
|
||||
moderation: None,
|
||||
id: "commentary-1".to_string(),
|
||||
content: vec![AgentMessageContent::Text {
|
||||
text: "commentary needle".to_string(),
|
||||
@@ -722,6 +723,7 @@ async fn thread_search_occurrences_reads_paginated_projection() -> Result<()> {
|
||||
thread_id,
|
||||
"turn-1",
|
||||
CoreTurnItem::AgentMessage(AgentMessageItem {
|
||||
moderation: None,
|
||||
id: "partial-1".to_string(),
|
||||
content: vec![AgentMessageContent::Text {
|
||||
text: "stable partial answer".to_string(),
|
||||
@@ -736,6 +738,7 @@ async fn thread_search_occurrences_reads_paginated_projection() -> Result<()> {
|
||||
thread_id,
|
||||
"turn-1",
|
||||
CoreTurnItem::AgentMessage(AgentMessageItem {
|
||||
moderation: None,
|
||||
id: "final-1".to_string(),
|
||||
content: vec![AgentMessageContent::Text {
|
||||
text: "😀 **Final** \nneedle".to_string(),
|
||||
@@ -2026,6 +2029,7 @@ async fn paginated_history_lists_and_legacy_reads_use_projected_turns_and_items(
|
||||
thread_id,
|
||||
"turn-1",
|
||||
CoreTurnItem::AgentMessage(AgentMessageItem {
|
||||
moderation: None,
|
||||
id: "agent-1".to_string(),
|
||||
content: vec![AgentMessageContent::Text {
|
||||
text: "first".to_string(),
|
||||
@@ -2082,6 +2086,7 @@ async fn paginated_history_lists_and_legacy_reads_use_projected_turns_and_items(
|
||||
content: Vec::new(),
|
||||
},
|
||||
ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "agent-1".to_string(),
|
||||
text: "first".to_string(),
|
||||
phase: None,
|
||||
@@ -2286,6 +2291,7 @@ async fn paginated_history_lists_and_legacy_reads_use_projected_turns_and_items(
|
||||
content: Vec::new(),
|
||||
},
|
||||
ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "agent-1".to_string(),
|
||||
text: "first".to_string(),
|
||||
phase: None,
|
||||
@@ -2532,6 +2538,7 @@ async fn paginated_history_lists_and_legacy_reads_use_projected_turns_and_items(
|
||||
output_deferred: false,
|
||||
item: codex_app_server_protocol::ThreadItem {
|
||||
payload: ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "agent-1".to_string(),
|
||||
text: "first".to_string(),
|
||||
phase: None,
|
||||
@@ -2756,6 +2763,7 @@ fn append_agent_message(path: &Path, timestamp: &str, text: &str) -> anyhow::Res
|
||||
"timestamp": timestamp,
|
||||
"type": "event_msg",
|
||||
"payload": serde_json::to_value(EventMsg::AgentMessage(AgentMessageEvent {
|
||||
moderation: None,
|
||||
message: text.to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
|
||||
@@ -4564,6 +4564,7 @@ async fn thread_resume_token_usage_replay_ignores_stale_interrupted_tail_turn()
|
||||
"timestamp": meta_rfc3339,
|
||||
"type": "event_msg",
|
||||
"payload": serde_json::to_value(EventMsg::AgentMessage(AgentMessageEvent {
|
||||
moderation: None,
|
||||
message: "Still running".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
@@ -4654,6 +4655,7 @@ async fn thread_resume_token_usage_replay_can_belong_to_interrupted_turn() -> Re
|
||||
"timestamp": meta_rfc3339,
|
||||
"type": "event_msg",
|
||||
"payload": serde_json::to_value(EventMsg::AgentMessage(AgentMessageEvent {
|
||||
moderation: None,
|
||||
message: "Interrupted after usage".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
@@ -4976,6 +4978,7 @@ async fn thread_resume_and_read_interrupt_incomplete_rollout_turn_when_thread_is
|
||||
"timestamp": meta_rfc3339,
|
||||
"type": "event_msg",
|
||||
"payload": serde_json::to_value(EventMsg::AgentMessage(AgentMessageEvent {
|
||||
moderation: None,
|
||||
message: "Still running".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
|
||||
@@ -170,6 +170,7 @@ fn parse_agent_message(
|
||||
.map(str::to_string)
|
||||
.unwrap_or_else(|| Uuid::new_v4().to_string());
|
||||
AgentMessageItem {
|
||||
moderation: None,
|
||||
id,
|
||||
content,
|
||||
phase,
|
||||
|
||||
@@ -225,6 +225,7 @@ fn promotes_backing_agent_artifacts_once_without_a_client_request() {
|
||||
);
|
||||
|
||||
let completed = completed_item(TurnItem::AgentMessage(AgentMessageItem {
|
||||
moderation: None,
|
||||
id: "message-1".to_string(),
|
||||
content: vec![AgentMessageContent::Text {
|
||||
text: "[analysis] ::codex-realtime-inline{}\nVisible explanation".to_string(),
|
||||
@@ -304,6 +305,7 @@ fn promotes_distinct_visualizations_once_and_ignores_markdown_fences() {
|
||||
);
|
||||
|
||||
let completed = completed_item(TurnItem::AgentMessage(AgentMessageItem {
|
||||
moderation: None,
|
||||
id: "message-1".to_string(),
|
||||
content: vec![AgentMessageContent::Text {
|
||||
text: text.to_string(),
|
||||
|
||||
@@ -13311,9 +13311,11 @@ async fn tool_calls_reopen_mailbox_delivery_for_current_turn() {
|
||||
cancellation_token: CancellationToken::new(),
|
||||
};
|
||||
|
||||
let output = handle_output_item_done(&mut ctx, item, /*previously_active_item*/ None)
|
||||
.await
|
||||
.expect("tool call should be handled");
|
||||
let output = handle_output_item_done(
|
||||
&mut ctx, item, /*previously_active_item*/ None, /*moderation*/ None,
|
||||
)
|
||||
.await
|
||||
.expect("tool call should be handled");
|
||||
|
||||
assert!(output.needs_follow_up);
|
||||
assert!(output.tool_future.is_some());
|
||||
|
||||
@@ -2731,6 +2731,7 @@ async fn emit_agent_message_in_plan_mode(
|
||||
.remove(&agent_message_id)
|
||||
.unwrap_or_else(|| {
|
||||
TurnItem::AgentMessage(codex_protocol::items::AgentMessageItem {
|
||||
moderation: None,
|
||||
id: agent_message_id.clone(),
|
||||
content: Vec::new(),
|
||||
phase: None,
|
||||
@@ -2785,6 +2786,7 @@ async fn emit_turn_item_in_plan_mode(
|
||||
}
|
||||
|
||||
/// Handle a completed assistant response item in plan mode, returning true if handled.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn handle_assistant_item_done_in_plan_mode(
|
||||
sess: &Session,
|
||||
step_context: &StepContext,
|
||||
@@ -2793,6 +2795,7 @@ async fn handle_assistant_item_done_in_plan_mode(
|
||||
state: &mut PlanModeStreamState,
|
||||
previously_active_item: Option<&TurnItem>,
|
||||
last_agent_message: &mut Option<String>,
|
||||
moderation: Option<&codex_protocol::protocol::TargetedAnswerModeration>,
|
||||
) -> bool {
|
||||
let turn_context = &step_context.turn;
|
||||
if let ResponseItem::Message { role, .. } = item
|
||||
@@ -2801,7 +2804,7 @@ async fn handle_assistant_item_done_in_plan_mode(
|
||||
maybe_complete_plan_item_from_message(sess, turn_context, state, item).await;
|
||||
|
||||
let mut finalized_facts = None;
|
||||
if let Some(finalized_turn_item) = finalize_non_tool_response_item(
|
||||
if let Some(mut finalized_turn_item) = finalize_non_tool_response_item(
|
||||
sess,
|
||||
TurnItemContributorPolicy::Run(turn_store),
|
||||
item,
|
||||
@@ -2809,6 +2812,10 @@ async fn handle_assistant_item_done_in_plan_mode(
|
||||
)
|
||||
.await
|
||||
{
|
||||
crate::stream_events_utils::apply_answer_moderation(
|
||||
&mut finalized_turn_item.turn_item,
|
||||
moderation,
|
||||
);
|
||||
finalized_facts = Some(finalized_turn_item.facts.clone());
|
||||
emit_turn_item_in_plan_mode(
|
||||
sess,
|
||||
@@ -2967,6 +2974,7 @@ async fn try_run_sampling_request(
|
||||
}
|
||||
let sampling_timing_guard = turn_context.turn_timing_state.begin_sampling();
|
||||
let mut response_id_for_assessment = None;
|
||||
let mut answer_moderation: Option<codex_protocol::protocol::TargetedAnswerModeration> = None;
|
||||
let mut pending_assessment: Option<codex_api::GuardianAssessment> = None;
|
||||
let mut response_completed = false;
|
||||
let mut request_token_usage: Option<codex_protocol::protocol::TokenUsage> = None;
|
||||
@@ -3171,6 +3179,7 @@ async fn try_run_sampling_request(
|
||||
|
||||
match event {
|
||||
ResponseEvent::Created { response_id } => {
|
||||
answer_moderation = None;
|
||||
response_id_for_assessment.clone_from(&response_id);
|
||||
if let Some(response_id) = response_id {
|
||||
turn_context
|
||||
@@ -3255,6 +3264,7 @@ async fn try_run_sampling_request(
|
||||
state,
|
||||
previously_streamed_item.as_ref(),
|
||||
&mut last_agent_message,
|
||||
answer_moderation.as_ref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -3306,14 +3316,18 @@ async fn try_run_sampling_request(
|
||||
| ResponseItem::Other => false,
|
||||
};
|
||||
|
||||
let output_result =
|
||||
match handle_output_item_done(&mut ctx, item, previously_streamed_item)
|
||||
.instrument(handle_responses)
|
||||
.await
|
||||
{
|
||||
Ok(output_result) => output_result,
|
||||
Err(err) => break Err(err),
|
||||
};
|
||||
let output_result = match handle_output_item_done(
|
||||
&mut ctx,
|
||||
item,
|
||||
previously_streamed_item,
|
||||
answer_moderation.as_ref(),
|
||||
)
|
||||
.instrument(handle_responses)
|
||||
.await
|
||||
{
|
||||
Ok(output_result) => output_result,
|
||||
Err(err) => break Err(err),
|
||||
};
|
||||
if let Some(tool_future) = output_result.tool_future {
|
||||
in_flight.push_back(tool_future);
|
||||
}
|
||||
@@ -3468,6 +3482,10 @@ async fn try_run_sampling_request(
|
||||
}
|
||||
}
|
||||
ResponseEvent::TurnModerationMetadata(metadata) => {
|
||||
answer_moderation = metadata
|
||||
.metadata
|
||||
.get("final_answer_moderation")
|
||||
.and_then(|marker| serde::Deserialize::deserialize(marker).ok());
|
||||
sess.emit_turn_moderation_metadata(&turn_context, metadata)
|
||||
.await;
|
||||
}
|
||||
|
||||
@@ -84,6 +84,7 @@ async fn plan_mode_uses_contributed_turn_item_for_last_agent_message() {
|
||||
&mut state,
|
||||
/*previously_active_item*/ None,
|
||||
&mut last_agent_message,
|
||||
/*moderation*/ None,
|
||||
)
|
||||
.await;
|
||||
|
||||
|
||||
@@ -332,11 +332,26 @@ pub(crate) async fn finalize_non_tool_response_item(
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn apply_answer_moderation(
|
||||
item: &mut TurnItem,
|
||||
decision: Option<&codex_protocol::protocol::TargetedAnswerModeration>,
|
||||
) {
|
||||
if let (TurnItem::AgentMessage(answer), Some(decision)) = (item, decision)
|
||||
&& answer.phase == Some(codex_protocol::models::MessagePhase::FinalAnswer)
|
||||
&& answer.id == decision.item_id
|
||||
{
|
||||
answer.moderation = Some(codex_protocol::items::AgentMessageModeration {
|
||||
moderated: decision.moderated,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(level = "trace", skip_all)]
|
||||
pub(crate) async fn handle_output_item_done(
|
||||
ctx: &mut HandleOutputCtx,
|
||||
item: ResponseItem,
|
||||
previously_active_item: Option<TurnItem>,
|
||||
moderation: Option<&codex_protocol::protocol::TargetedAnswerModeration>,
|
||||
) -> Result<OutputItemResult> {
|
||||
let mut output = OutputItemResult::default();
|
||||
let plan_mode = ctx.step_context.turn.mode() == ModeKind::Plan;
|
||||
@@ -404,7 +419,7 @@ pub(crate) async fn handle_output_item_done(
|
||||
let finalized_facts = finalized_turn_item
|
||||
.as_ref()
|
||||
.map(|finalized| finalized.facts.clone());
|
||||
if let Some(finalized_turn_item) = finalized_turn_item {
|
||||
if let Some(mut finalized_turn_item) = finalized_turn_item {
|
||||
if previously_active_item.is_none() {
|
||||
ctx.sess
|
||||
.emit_turn_item_started(
|
||||
@@ -414,6 +429,7 @@ pub(crate) async fn handle_output_item_done(
|
||||
.await;
|
||||
}
|
||||
|
||||
apply_answer_moderation(&mut finalized_turn_item.turn_item, moderation);
|
||||
ctx.sess
|
||||
.emit_turn_item_completed_with_metadata(
|
||||
&ctx.step_context.turn,
|
||||
|
||||
@@ -329,9 +329,11 @@ async fn handle_output_item_done_returns_contributed_last_agent_message() {
|
||||
let mut ctx = output_context(Arc::new(session), Arc::new(turn_context)).await;
|
||||
let item = assistant_output_text("original assistant text");
|
||||
|
||||
let output = handle_output_item_done(&mut ctx, item, /*previously_active_item*/ None)
|
||||
.await
|
||||
.expect("assistant message should complete");
|
||||
let output = handle_output_item_done(
|
||||
&mut ctx, item, /*previously_active_item*/ None, /*moderation*/ None,
|
||||
)
|
||||
.await
|
||||
.expect("assistant message should complete");
|
||||
|
||||
assert_eq!(
|
||||
output.last_agent_message.as_deref(),
|
||||
@@ -362,9 +364,11 @@ async fn direct_results_keep_their_own_records_when_call_ids_repeat() {
|
||||
"arguments": arguments.to_string(),
|
||||
}))
|
||||
.expect("direct input");
|
||||
let result = handle_output_item_done(&mut ctx, call, /*previously_active_item*/ None)
|
||||
.await
|
||||
.expect("direct call should be handled");
|
||||
let result = handle_output_item_done(
|
||||
&mut ctx, call, /*previously_active_item*/ None, /*moderation*/ None,
|
||||
)
|
||||
.await
|
||||
.expect("direct call should be handled");
|
||||
assert!(result.needs_follow_up);
|
||||
pending.push((
|
||||
arguments,
|
||||
@@ -421,11 +425,13 @@ async fn direct_results_keep_their_own_records_when_call_ids_repeat() {
|
||||
}))
|
||||
.expect("direct input");
|
||||
pending.push(
|
||||
handle_output_item_done(&mut ctx, call, /*previously_active_item*/ None)
|
||||
.await
|
||||
.expect("direct call should be handled")
|
||||
.tool_future
|
||||
.expect("dispatched direct call"),
|
||||
handle_output_item_done(
|
||||
&mut ctx, call, /*previously_active_item*/ None, /*moderation*/ None,
|
||||
)
|
||||
.await
|
||||
.expect("direct call should be handled")
|
||||
.tool_future
|
||||
.expect("dispatched direct call"),
|
||||
);
|
||||
}
|
||||
let current_config = session.get_config().await;
|
||||
|
||||
@@ -3771,6 +3771,7 @@ fn completed_legacy_event_history_is_not_mid_turn() {
|
||||
..Default::default()
|
||||
})),
|
||||
RolloutItem::EventMsg(EventMsg::AgentMessage(AgentMessageEvent {
|
||||
moderation: None,
|
||||
message: "done".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
|
||||
@@ -129,6 +129,7 @@ impl ToolExecutor<ToolInvocation> for RequestUserInputAsyncHandler {
|
||||
}
|
||||
|
||||
let item = TurnItem::AgentMessage(AgentMessageItem {
|
||||
moderation: None,
|
||||
id: call_id,
|
||||
content: vec![AgentMessageContent::Text {
|
||||
text: messages.join("\n\n"),
|
||||
|
||||
@@ -82,6 +82,7 @@ impl ToolExecutor<ToolInvocation> for SendMessageToUserAsyncHandler {
|
||||
}
|
||||
|
||||
let item = TurnItem::AgentMessage(AgentMessageItem {
|
||||
moderation: None,
|
||||
id: call_id,
|
||||
content: vec![AgentMessageContent::Text {
|
||||
text: message.to_string(),
|
||||
|
||||
@@ -66,6 +66,7 @@ async fn turn_timing_state_records_ttfm_independently_of_ttft() {
|
||||
assert!(
|
||||
state
|
||||
.record_ttfm_for_turn_item(&TurnItem::AgentMessage(AgentMessageItem {
|
||||
moderation: None,
|
||||
id: "msg-1".to_string(),
|
||||
content: Vec::new(),
|
||||
phase: None,
|
||||
@@ -79,6 +80,7 @@ async fn turn_timing_state_records_ttfm_independently_of_ttft() {
|
||||
assert_eq!(
|
||||
state
|
||||
.record_ttfm_for_turn_item(&TurnItem::AgentMessage(AgentMessageItem {
|
||||
moderation: None,
|
||||
id: "msg-2".to_string(),
|
||||
content: Vec::new(),
|
||||
phase: None,
|
||||
|
||||
@@ -457,3 +457,50 @@ async fn model_request_gates_websocket_output_but_not_warmup() -> Result<()> {
|
||||
server.shutdown().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The same final answer carries the targeted public flag on completion and in legacy history.
|
||||
#[tokio::test]
|
||||
async fn final_answer_carries_its_moderation_decision() -> Result<()> {
|
||||
use core_test_support::wait_for_event_match;
|
||||
let server = responses::start_mock_server().await;
|
||||
let mut answer = responses::ev_assistant_message("msg-42", "Answer");
|
||||
answer["item"]["phase"] = json!("final_answer");
|
||||
responses::mount_sse_once(
|
||||
&server,
|
||||
responses::sse(vec![
|
||||
responses::ev_response_created("resp-1"),
|
||||
json!({"type":"response.metadata","metadata":{"openai_chatgpt_moderation_metadata":{
|
||||
"final_answer_moderation":{"item_id":"msg-42","moderated":true},
|
||||
"generation":{"private":"classifier details"}
|
||||
}}}),
|
||||
answer,
|
||||
responses::ev_completed("resp-1"),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
let test = test_codex().build_with_auto_env(&server).await?;
|
||||
test.codex
|
||||
.start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
|
||||
text: "test".into(),
|
||||
text_elements: Vec::new(),
|
||||
}]))
|
||||
.await?;
|
||||
let item = wait_for_event_match(&test.codex, |event| match event {
|
||||
EventMsg::ItemCompleted(done) if done.item.id() == "msg-42" => Some(done.item.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.await;
|
||||
let codex_protocol::items::TurnItem::AgentMessage(message) = item else {
|
||||
panic!("expected assistant answer");
|
||||
};
|
||||
let public = serde_json::to_value(&message)?;
|
||||
assert_eq!(public["moderation"], json!({"moderated": true}));
|
||||
let [EventMsg::AgentMessage(legacy)] = &message.as_legacy_events()[..] else {
|
||||
panic!("expected one legacy answer");
|
||||
};
|
||||
assert_eq!(
|
||||
serde_json::to_value(&legacy.moderation)?,
|
||||
json!({"item_id":"msg-42","moderated":true})
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -367,6 +367,7 @@ async fn freeform_async_message_emits_an_item_without_ending_the_turn(
|
||||
})
|
||||
.await;
|
||||
let expected = serde_json::to_value(TurnItem::AgentMessage(AgentMessageItem {
|
||||
moderation: None,
|
||||
id: CALL_ID.to_string(),
|
||||
content: vec![AgentMessageContent::Text {
|
||||
text: MESSAGE.to_string(),
|
||||
@@ -555,6 +556,7 @@ async fn request_user_input_async_emits_item_and_does_not_end_the_turn(
|
||||
assert_eq!(
|
||||
serde_json::to_value(&started)?,
|
||||
serde_json::to_value(AgentMessageItem {
|
||||
moderation: None,
|
||||
id: CALL_ID.to_string(),
|
||||
content: vec![AgentMessageContent::Text {
|
||||
text: MESSAGE.to_string(),
|
||||
|
||||
@@ -354,6 +354,7 @@ async fn config_summary_entries_include_runtime_workspace_roots() {
|
||||
fn final_message_from_turn_items_uses_latest_agent_message() {
|
||||
let message = final_message_from_turn_items(&[
|
||||
ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "msg-1".to_string(),
|
||||
text: "first".to_string(),
|
||||
phase: None,
|
||||
@@ -366,6 +367,7 @@ fn final_message_from_turn_items_uses_latest_agent_message() {
|
||||
text: "plan".to_string(),
|
||||
},
|
||||
ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "msg-2".to_string(),
|
||||
text: "second".to_string(),
|
||||
phase: None,
|
||||
@@ -428,6 +430,7 @@ fn turn_completed_recovers_final_message_from_turn_items() {
|
||||
root_turn_id: None,
|
||||
items_view: codex_app_server_protocol::TurnItemsView::Full,
|
||||
items: vec![ThreadItem::untimed(ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "msg-1".to_string(),
|
||||
text: "final answer".to_string(),
|
||||
phase: None,
|
||||
@@ -480,6 +483,7 @@ fn turn_completed_overwrites_stale_final_message_from_turn_items() {
|
||||
root_turn_id: None,
|
||||
items_view: codex_app_server_protocol::TurnItemsView::Full,
|
||||
items: vec![ThreadItem::untimed(ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "msg-1".to_string(),
|
||||
text: "final answer".to_string(),
|
||||
phase: None,
|
||||
|
||||
@@ -16,6 +16,7 @@ fn failed_turn_does_not_overwrite_output_last_message_file() {
|
||||
"thread-1".to_string(),
|
||||
"turn-1".to_string(),
|
||||
ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "msg-1".to_string(),
|
||||
text: "partial answer".to_string(),
|
||||
phase: None,
|
||||
|
||||
@@ -391,6 +391,7 @@ fn turn_items_for_thread_returns_matching_turn_items() {
|
||||
items_view: codex_app_server_protocol::TurnItemsView::Full,
|
||||
items: vec![ThreadItem::untimed(
|
||||
AppServerThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "msg-1".to_string(),
|
||||
text: "hello".to_string(),
|
||||
phase: None,
|
||||
@@ -426,6 +427,7 @@ fn turn_items_for_thread_returns_matching_turn_items() {
|
||||
turn_items_for_thread(&thread, "turn-1"),
|
||||
Some(vec![ThreadItem::untimed(
|
||||
AppServerThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "msg-1".to_string(),
|
||||
text: "hello".to_string(),
|
||||
phase: None,
|
||||
|
||||
@@ -312,6 +312,7 @@ fn unsupported_items_do_not_consume_synthetic_ids() {
|
||||
"thread-1".to_string(),
|
||||
"turn-1".to_string(),
|
||||
ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "message-1".to_string(),
|
||||
text: "hello".to_string(),
|
||||
phase: None,
|
||||
@@ -1014,6 +1015,7 @@ fn agent_message_item_updates_final_message() {
|
||||
"thread-1".to_string(),
|
||||
"turn-1".to_string(),
|
||||
ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "msg-1".to_string(),
|
||||
text: "hello".to_string(),
|
||||
phase: None,
|
||||
@@ -1052,6 +1054,7 @@ fn agent_message_item_started_is_ignored() {
|
||||
"thread-1".to_string(),
|
||||
"turn-1".to_string(),
|
||||
ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "msg-1".to_string(),
|
||||
text: "hello".to_string(),
|
||||
phase: None,
|
||||
@@ -1408,6 +1411,7 @@ fn turn_completion_recovers_final_message_from_turn_items() {
|
||||
root_turn_id: None,
|
||||
items_view: codex_app_server_protocol::TurnItemsView::Full,
|
||||
items: vec![ThreadItem::untimed(ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "msg-1".to_string(),
|
||||
text: "final answer".to_string(),
|
||||
phase: None,
|
||||
@@ -1545,6 +1549,7 @@ fn turn_completion_overwrites_stale_final_message_from_turn_items() {
|
||||
"thread-1".to_string(),
|
||||
"turn-1".to_string(),
|
||||
ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "msg-stale".to_string(),
|
||||
text: "stale answer".to_string(),
|
||||
phase: None,
|
||||
@@ -1565,6 +1570,7 @@ fn turn_completion_overwrites_stale_final_message_from_turn_items() {
|
||||
root_turn_id: None,
|
||||
items_view: codex_app_server_protocol::TurnItemsView::Full,
|
||||
items: vec![ThreadItem::untimed(ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "msg-1".to_string(),
|
||||
text: "final answer".to_string(),
|
||||
phase: None,
|
||||
@@ -1601,6 +1607,7 @@ fn turn_completion_preserves_streamed_final_message_when_turn_items_are_empty()
|
||||
"thread-1".to_string(),
|
||||
"turn-1".to_string(),
|
||||
ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "msg-streamed".to_string(),
|
||||
text: "streamed answer".to_string(),
|
||||
phase: None,
|
||||
@@ -1651,6 +1658,7 @@ fn failed_turn_clears_stale_final_message() {
|
||||
"thread-1".to_string(),
|
||||
"turn-1".to_string(),
|
||||
ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "msg-1".to_string(),
|
||||
text: "partial answer".to_string(),
|
||||
phase: None,
|
||||
|
||||
@@ -62,6 +62,7 @@ fn goal_accounting_ignores_plan_mode_turns() {
|
||||
#[test]
|
||||
fn empty_continuations_require_three_turns_without_activity_or_goal_changes() {
|
||||
let empty_final = TurnItem::AgentMessage(AgentMessageItem {
|
||||
moderation: None,
|
||||
id: "empty".into(),
|
||||
content: vec![AgentMessageContent::Text { text: " \n".into() }],
|
||||
phase: Some(MessagePhase::FinalAnswer),
|
||||
|
||||
@@ -134,6 +134,7 @@ pub(super) fn rollout_items_from_messages(messages: Vec<ConversationMessage>) ->
|
||||
last_model_visible_tokens = approx_tokens_from_byte_count_i64(response_item_bytes);
|
||||
items.push(RolloutItem::EventMsg(EventMsg::AgentMessage(
|
||||
AgentMessageEvent {
|
||||
moderation: None,
|
||||
message: message.text.clone(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
@@ -157,6 +158,7 @@ pub(super) fn rollout_items_from_messages(messages: Vec<ConversationMessage>) ->
|
||||
|
||||
fn external_session_imported_marker_item() -> RolloutItem {
|
||||
RolloutItem::EventMsg(EventMsg::AgentMessage(AgentMessageEvent {
|
||||
moderation: None,
|
||||
message: EXTERNAL_SESSION_IMPORTED_MARKER.to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
@@ -261,6 +263,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
turns[1].item_payloads().next_back(),
|
||||
Some(&ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "item-4".into(),
|
||||
text: EXTERNAL_SESSION_IMPORTED_MARKER.into(),
|
||||
phase: None,
|
||||
@@ -295,6 +298,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
turns[0].item_payloads().next_back(),
|
||||
Some(&ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "item-3".into(),
|
||||
text: EXTERNAL_SESSION_IMPORTED_MARKER.into(),
|
||||
phase: None,
|
||||
|
||||
@@ -151,6 +151,11 @@ pub struct AsyncUserInputQuestion {
|
||||
pub options: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, TS, JsonSchema)]
|
||||
pub struct AgentMessageModeration {
|
||||
pub moderated: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
|
||||
/// Assistant-authored message payload used in turn-item streams.
|
||||
///
|
||||
@@ -158,6 +163,9 @@ pub struct AsyncUserInputQuestion {
|
||||
/// should use it when present, but retain legacy completion semantics when it
|
||||
/// is `None`.
|
||||
pub struct AgentMessageItem {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub moderation: Option<AgentMessageModeration>,
|
||||
pub id: String,
|
||||
pub content: Vec<AgentMessageContent>,
|
||||
/// Optional phase metadata carried through from `ResponseItem::Message`.
|
||||
|
||||
@@ -126,18 +126,30 @@ impl UserMessageItem {
|
||||
|
||||
impl AgentMessageItem {
|
||||
pub fn as_legacy_events(&self) -> Vec<EventMsg> {
|
||||
self.content
|
||||
.iter()
|
||||
.map(|c| match c {
|
||||
AgentMessageContent::Text { text } => EventMsg::AgentMessage(AgentMessageEvent {
|
||||
message: text.clone(),
|
||||
phase: self.phase.clone(),
|
||||
memory_citation: self.memory_citation.clone(),
|
||||
delivery: self.delivery,
|
||||
questions: self.questions.clone(),
|
||||
}),
|
||||
let texts = self.content.iter().map(|part| match part {
|
||||
AgentMessageContent::Text { text } => text.as_str(),
|
||||
});
|
||||
let event = |message, moderation| {
|
||||
EventMsg::AgentMessage(AgentMessageEvent {
|
||||
message,
|
||||
moderation,
|
||||
phase: self.phase.clone(),
|
||||
memory_citation: self.memory_citation.clone(),
|
||||
delivery: self.delivery,
|
||||
questions: self.questions.clone(),
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
if let Some(decision) = self.moderation {
|
||||
// The canonical answer has one ID even when the model supplied multiple text parts.
|
||||
return vec![event(
|
||||
texts.collect(),
|
||||
Some(crate::protocol::TargetedAnswerModeration {
|
||||
item_id: self.id.clone(),
|
||||
moderated: decision.moderated,
|
||||
}),
|
||||
)];
|
||||
}
|
||||
texts.map(|text| event(text.to_owned(), None)).collect()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2392,6 +2392,12 @@ pub struct ModelVerificationEvent {
|
||||
pub verifications: Vec<ModelVerification>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
|
||||
pub struct TargetedAnswerModeration {
|
||||
pub item_id: String,
|
||||
pub moderated: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
|
||||
pub struct TurnModerationMetadataEvent {
|
||||
pub metadata: Value,
|
||||
@@ -2790,6 +2796,10 @@ impl fmt::Display for FinalOutput {
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
|
||||
pub struct AgentMessageEvent {
|
||||
/// Legacy history assigns synthetic item IDs. Preserve the native ID for a moderated answer.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub moderation: Option<TargetedAnswerModeration>,
|
||||
pub message: String,
|
||||
#[serde(default)]
|
||||
pub phase: Option<MessagePhase>,
|
||||
|
||||
@@ -66,6 +66,7 @@ fn paginated_session_meta_item(thread_id: ThreadId, cwd: &Path) -> RolloutItem {
|
||||
|
||||
fn agent_message_item(message: &str) -> RolloutItem {
|
||||
RolloutItem::EventMsg(EventMsg::AgentMessage(AgentMessageEvent {
|
||||
moderation: None,
|
||||
message: message.to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
@@ -726,6 +727,7 @@ async fn recorder_materializes_on_flush_with_pending_items() -> std::io::Result<
|
||||
recorder
|
||||
.record_canonical_items(&[RolloutItem::EventMsg(EventMsg::AgentMessage(
|
||||
AgentMessageEvent {
|
||||
moderation: None,
|
||||
message: "buffered-event".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
@@ -993,6 +995,7 @@ async fn persist_reports_filesystem_error_and_retries_buffered_items() -> std::i
|
||||
recorder
|
||||
.record_canonical_items(&[RolloutItem::EventMsg(EventMsg::AgentMessage(
|
||||
AgentMessageEvent {
|
||||
moderation: None,
|
||||
message: "buffered-before-persist".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
@@ -1046,6 +1049,7 @@ async fn writer_state_retries_write_error_before_reporting_flush_success() -> st
|
||||
};
|
||||
state.add_items(vec![RolloutItem::EventMsg(EventMsg::AgentMessage(
|
||||
AgentMessageEvent {
|
||||
moderation: None,
|
||||
message: "queued-after-writer-error".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
|
||||
@@ -1256,6 +1256,7 @@ mod tests {
|
||||
live_thread
|
||||
.append_items(&[
|
||||
RolloutItem::EventMsg(EventMsg::AgentMessage(AgentMessageEvent {
|
||||
moderation: None,
|
||||
message: "commentary".to_string(),
|
||||
phase: Some(MessagePhase::Commentary),
|
||||
memory_citation: None,
|
||||
|
||||
@@ -158,7 +158,15 @@ pub(super) fn completed_item(
|
||||
let result = match event {
|
||||
EventMsg::AgentMessage(event) if !event.message.is_empty() => Some((
|
||||
TurnItem::AgentMessage(AgentMessageItem {
|
||||
id: next_item_id()?,
|
||||
moderation: event.moderation.as_ref().map(|decision| {
|
||||
codex_protocol::items::AgentMessageModeration {
|
||||
moderated: decision.moderated,
|
||||
}
|
||||
}),
|
||||
id: event
|
||||
.moderation
|
||||
.as_ref()
|
||||
.map_or(next_item_id()?, |decision| decision.item_id.clone()),
|
||||
content: vec![AgentMessageContent::Text {
|
||||
text: event.message.clone(),
|
||||
}],
|
||||
|
||||
@@ -172,6 +172,7 @@ fn user_message(text: &str) -> RolloutItem {
|
||||
|
||||
fn agent_message(text: &str) -> RolloutItem {
|
||||
RolloutItem::EventMsg(EventMsg::AgentMessage(AgentMessageEvent {
|
||||
moderation: None,
|
||||
message: text.to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
|
||||
@@ -236,6 +236,7 @@ async fn split_homes_support_backfill_listing_and_paginated_history() {
|
||||
thread_id,
|
||||
"turn-1",
|
||||
TurnItem::AgentMessage(AgentMessageItem {
|
||||
moderation: None,
|
||||
id: "agent-1".to_string(),
|
||||
content: vec![AgentMessageContent::Text {
|
||||
text: "done".to_string(),
|
||||
@@ -330,6 +331,7 @@ async fn paginated_live_append_materializes_turn_items_and_state() {
|
||||
thread_id,
|
||||
"turn-1",
|
||||
TurnItem::AgentMessage(AgentMessageItem {
|
||||
moderation: None,
|
||||
id: "agent-1".to_string(),
|
||||
content: vec![AgentMessageContent::Text {
|
||||
text: "done".to_string(),
|
||||
@@ -646,6 +648,7 @@ async fn paginated_realtime_items_materialize_separately_in_rollout_order() {
|
||||
thread_id,
|
||||
"turn-1",
|
||||
TurnItem::AgentMessage(AgentMessageItem {
|
||||
moderation: None,
|
||||
id: "agent-1".to_string(),
|
||||
content: vec![AgentMessageContent::Text {
|
||||
text: "Result".to_string(),
|
||||
@@ -3183,6 +3186,7 @@ fn completed_item(thread_id: ThreadId, turn_id: &str, item: TurnItem) -> Rollout
|
||||
|
||||
fn agent_message(id: &str, phase: MessagePhase) -> TurnItem {
|
||||
TurnItem::AgentMessage(AgentMessageItem {
|
||||
moderation: None,
|
||||
id: id.to_string(),
|
||||
content: vec![AgentMessageContent::Text {
|
||||
text: id.to_string(),
|
||||
|
||||
@@ -196,6 +196,7 @@ async fn every_page_split_folds_reasoning_before_answer_and_completion_boundarie
|
||||
let mut items = vec![call("call", kind, "completed"), reasoning("detail")];
|
||||
if let Some(text) = answer {
|
||||
items.push(ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "answer".to_owned(),
|
||||
text: text.to_owned(),
|
||||
phase: None,
|
||||
|
||||
@@ -38,6 +38,7 @@ fn agent_status_uses_bounded_buffered_activity() {
|
||||
"thread-child".to_string(),
|
||||
"turn-1".to_string(),
|
||||
ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "message-1".to_string(),
|
||||
text: "Finished checking the focused TUI tests.".to_string(),
|
||||
phase: None,
|
||||
|
||||
@@ -947,6 +947,7 @@ async fn agents_overview_details_render_markdown() {
|
||||
thread.id.clone(),
|
||||
"turn".into(),
|
||||
ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "answer".into(),
|
||||
text: message.into(),
|
||||
phase: None,
|
||||
@@ -1100,6 +1101,7 @@ async fn agents_overview_reasoning_uses_existing_events_and_expires_with_attachm
|
||||
parent.id.clone(),
|
||||
"previous-turn".into(),
|
||||
ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "answer".into(),
|
||||
text: "Previous answer".into(),
|
||||
phase: None,
|
||||
|
||||
@@ -4316,6 +4316,7 @@ async fn handle_start_side_seeds_navigation_before_thread_started() -> Result<()
|
||||
app.enqueue_thread_notification(parent, delta).await?;
|
||||
let streaming = side_frame(&mut app, &mut tui, size)?;
|
||||
let item = ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "reply".into(),
|
||||
text: "Already visible while unfocused.".into(),
|
||||
phase: None,
|
||||
@@ -5969,6 +5970,7 @@ async fn active_thread_file_change_approval_recovers_buffered_changes() {
|
||||
thread_id.to_string(),
|
||||
"turn-active-approval".to_string(),
|
||||
ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "agent-1".to_string(),
|
||||
text: "streaming".to_string(),
|
||||
phase: None,
|
||||
@@ -10702,6 +10704,7 @@ async fn replay_thread_snapshot_replays_turn_history_in_order() {
|
||||
}],
|
||||
}),
|
||||
ThreadItem::untimed(ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "assistant-2".to_string(),
|
||||
text: "done".to_string(),
|
||||
phase: None,
|
||||
|
||||
@@ -23,6 +23,7 @@ fn completed(thread: &str) -> ServerNotification {
|
||||
thread.into(),
|
||||
"turn".into(),
|
||||
ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "answer".into(),
|
||||
text: "Final **answer**".into(),
|
||||
phase: None,
|
||||
@@ -243,6 +244,7 @@ fn evicted_voice_delegation_marker_still_suppresses_private_replay() {
|
||||
"thread".into(),
|
||||
"voice-turn".into(),
|
||||
ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "reasoning".into(),
|
||||
text: "[ANALYSIS] private".into(),
|
||||
phase: None,
|
||||
@@ -378,6 +380,7 @@ fn saved_voice_turn_suppresses_reasoning_without_hiding_typed_reasoning() {
|
||||
#[test]
|
||||
fn snapshot_keeps_typed_output_before_voice_handoff_in_the_same_turn() {
|
||||
let commentary = |id: &str, text: &str| ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: id.into(),
|
||||
text: text.into(),
|
||||
phase: Some(codex_protocol::models::MessagePhase::Commentary),
|
||||
@@ -451,6 +454,7 @@ fn snapshot_keeps_typed_output_before_voice_handoff_in_the_same_turn() {
|
||||
#[test]
|
||||
fn snapshot_keeps_typed_item_completed_after_voice_handoff() {
|
||||
let typed = ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "typed".into(),
|
||||
text: "Typed commentary completed late".into(),
|
||||
phase: Some(codex_protocol::models::MessagePhase::Commentary),
|
||||
@@ -459,6 +463,7 @@ fn snapshot_keeps_typed_item_completed_after_voice_handoff() {
|
||||
delivery: None,
|
||||
};
|
||||
let private = ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "private".into(),
|
||||
text: "Voice-private commentary".into(),
|
||||
phase: Some(codex_protocol::models::MessagePhase::Commentary),
|
||||
@@ -632,6 +637,7 @@ async fn voice_handoff_replay_keeps_public_reasoning_and_hides_private_reasoning
|
||||
thread_id.to_string(),
|
||||
"voice-turn".into(),
|
||||
ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "private-commentary".into(),
|
||||
text: "[COMMENTARY] private after switch".into(),
|
||||
phase: None,
|
||||
|
||||
@@ -75,6 +75,7 @@ pub(super) async fn completed_history_app(
|
||||
}],
|
||||
}),
|
||||
TurnItem::AgentMessage(AgentMessageItem {
|
||||
moderation: None,
|
||||
id: format!("answer-{index}"),
|
||||
content: vec![AgentMessageContent::Text {
|
||||
text: format!("{name} answer"),
|
||||
|
||||
@@ -341,6 +341,7 @@ async fn active_patch_approval_preserves_deferred_startup_protection() -> Result
|
||||
thread_id.to_string(),
|
||||
TURN_ID.to_string(),
|
||||
ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "agent-1".to_string(),
|
||||
text: "streaming".to_string(),
|
||||
phase: None,
|
||||
|
||||
@@ -48,6 +48,7 @@ fn voice_snapshot_directory_keeps_header_width_for_windows_paths() {
|
||||
|
||||
fn test_agent_message(id: &str, text: &str) -> ThreadItemPayload {
|
||||
ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: id.into(),
|
||||
text: text.into(),
|
||||
phase: Some(MessagePhase::FinalAnswer),
|
||||
@@ -743,6 +744,7 @@ async fn retained_caption_consumes_only_one_matching_answer_fallback_on_reattach
|
||||
.into(),
|
||||
);
|
||||
let answer = |id: &str, text: &str| ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: id.into(),
|
||||
text: text.into(),
|
||||
phase: Some(MessagePhase::FinalAnswer),
|
||||
|
||||
@@ -3072,6 +3072,7 @@ async fn older_pagination_reconciles_review_prompts_across_page_boundaries() ->
|
||||
];
|
||||
items.extend((0..97).map(|index| {
|
||||
TurnItem::AgentMessage(AgentMessageItem {
|
||||
moderation: None,
|
||||
id: format!("review-output-{index}"),
|
||||
content: vec![AgentMessageContent::Text {
|
||||
text: format!("review output {index}"),
|
||||
@@ -3270,6 +3271,7 @@ async fn transcript_alt_beginning_loads_every_older_history_page() -> Result<()>
|
||||
thread_id,
|
||||
turn_id: "multi-page-turn".to_string(),
|
||||
item: TurnItem::AgentMessage(AgentMessageItem {
|
||||
moderation: None,
|
||||
id: format!("history-item-{index}"),
|
||||
content: vec![AgentMessageContent::Text {
|
||||
text: format!("history output {index}"),
|
||||
@@ -3687,6 +3689,7 @@ async fn underfilled_scrollback_fetches_older_pages_without_opening_the_transcri
|
||||
thread_id,
|
||||
turn_id: "scrollback-pagination-turn".to_string(),
|
||||
item: TurnItem::AgentMessage(AgentMessageItem {
|
||||
moderation: None,
|
||||
id: format!("scrollback-item-{index}"),
|
||||
content: vec![AgentMessageContent::Text {
|
||||
text: format!("scrollback output {index}"),
|
||||
@@ -5947,6 +5950,7 @@ async fn command_center_read_only_open_requests_and_failure_preservation() -> Re
|
||||
thread_id,
|
||||
turn_id: format!("saved-turn-{index}"),
|
||||
item: TurnItem::AgentMessage(AgentMessageItem {
|
||||
moderation: None,
|
||||
id: format!("saved-answer-{index}"),
|
||||
content: vec![AgentMessageContent::Text {
|
||||
text: "Saved final answer".to_string(),
|
||||
|
||||
@@ -95,6 +95,7 @@ async fn owned_startup_preserves_loading_until_resume_replay_is_applied() -> Res
|
||||
for items in [
|
||||
Vec::new(),
|
||||
vec![ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "answer".into(),
|
||||
text: "Retained **answer** after resume.".into(),
|
||||
phase: None,
|
||||
|
||||
@@ -255,6 +255,7 @@ async fn inactive_thread_user_verification_preserves_foreground_stream() -> Resu
|
||||
foreground_thread_id.to_string(),
|
||||
"turn-foreground".to_string(),
|
||||
ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "message-foreground".to_string(),
|
||||
text: "The foreground answer continues.".to_string(),
|
||||
phase: None,
|
||||
|
||||
@@ -1051,6 +1051,7 @@ mod tests {
|
||||
store.push_notification(serde_json::from_value(expected.clone()).unwrap());
|
||||
store.rebase_buffer_after_session_refresh();
|
||||
expected["params"]["item"]["text"] = serde_json::json!("");
|
||||
expected["params"]["item"]["moderation"] = serde_json::Value::Null;
|
||||
let ThreadBufferedEvent::Notification(actual) = &store.snapshot().events[0] else {
|
||||
panic!("missing live question");
|
||||
};
|
||||
|
||||
@@ -855,6 +855,7 @@ fn title_user_message(id: &str, text: &str) -> ThreadItemPayload {
|
||||
|
||||
fn title_agent_message(id: &str, text: &str, phase: Option<MessagePhase>) -> ThreadItemPayload {
|
||||
ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: id.to_string(),
|
||||
text: text.to_string(),
|
||||
phase,
|
||||
|
||||
@@ -4834,6 +4834,7 @@ mod tests {
|
||||
),
|
||||
ThreadItem::untimed(
|
||||
codex_app_server_protocol::ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "assistant-1".to_string(),
|
||||
text: "assistant reply".to_string(),
|
||||
phase: None,
|
||||
|
||||
@@ -83,6 +83,7 @@ fn user_item(text: &str) -> ThreadItemPayload {
|
||||
|
||||
fn agent_item(item_id: &str, text: &str, phase: Option<MessagePhase>) -> ThreadItemPayload {
|
||||
ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: item_id.to_string(),
|
||||
text: text.to_string(),
|
||||
phase,
|
||||
|
||||
@@ -181,6 +181,7 @@ async fn delegated_async_question_stays_local_and_expires_when_its_turn_ends() {
|
||||
user_item("<realtime_delegation><input>choose</input></realtime_delegation>"),
|
||||
);
|
||||
let answer = ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "question-answer".into(),
|
||||
text: "Which option?".into(),
|
||||
phase: Some(MessagePhase::FinalAnswer),
|
||||
|
||||
@@ -306,6 +306,7 @@ impl ChatWidget {
|
||||
if self.complete_realtime_delegated_agent_item(
|
||||
&turn_id,
|
||||
&ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: id.clone(),
|
||||
text: text.clone(),
|
||||
phase: phase.clone(),
|
||||
@@ -319,6 +320,7 @@ impl ChatWidget {
|
||||
}
|
||||
self.on_agent_message_item_completed(
|
||||
AgentMessageItem {
|
||||
moderation: None,
|
||||
id,
|
||||
content: vec![AgentMessageContent::Text { text }],
|
||||
phase,
|
||||
|
||||
@@ -964,6 +964,7 @@ async fn live_app_server_turn_completed_clears_working_status_after_answer_item(
|
||||
assert_eq!(status.header(), "Working");
|
||||
|
||||
let item = AppServerThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "msg-1".to_string(),
|
||||
text: "Yes. What do you need?".to_string(),
|
||||
phase: Some(MessagePhase::FinalAnswer),
|
||||
@@ -1773,6 +1774,7 @@ async fn live_app_server_turn_completion_repairs_dropped_message_deltas() {
|
||||
);
|
||||
completed_turn.items_view = codex_app_server_protocol::TurnItemsView::Summary;
|
||||
completed_turn.items = vec![ThreadItem::untimed(AppServerThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "msg-1".to_string(),
|
||||
text: concat!(
|
||||
"The transport kept this.\nAnd dropped this.\n\n",
|
||||
|
||||
@@ -14,6 +14,7 @@ fn completed_turn(duration_ms: Option<i64>, completed_at: Option<i64>) -> AppSer
|
||||
completed_at,
|
||||
items: vec![ThreadItem::untimed(
|
||||
AppServerThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "answer-1".to_string(),
|
||||
text: "The change is ready.".to_string(),
|
||||
phase: Some(MessagePhase::FinalAnswer),
|
||||
|
||||
@@ -926,6 +926,7 @@ pub(super) fn replay_agent_message(
|
||||
) {
|
||||
chat.replay_thread_item(
|
||||
AppServerThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: item_id.to_string(),
|
||||
text: text.into(),
|
||||
phase: Some(MessagePhase::FinalAnswer),
|
||||
@@ -1082,6 +1083,7 @@ pub(super) fn complete_assistant_message(
|
||||
chat.thread_id.map(|id| id.to_string()).unwrap_or_default(),
|
||||
"turn-1".to_string(),
|
||||
AppServerThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: item_id.to_string(),
|
||||
text: text.to_string(),
|
||||
phase,
|
||||
|
||||
@@ -460,6 +460,7 @@ async fn replayed_nested_review_prompts_do_not_render_or_seed_composer_history()
|
||||
ThreadItem::untimed(user_message("review-prompt-1", review_prompt)),
|
||||
ThreadItem::untimed(user_message("review-prompt-2", review_prompt)),
|
||||
ThreadItem::untimed(AppServerThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "review-result".to_string(),
|
||||
text: "review result is retained".to_string(),
|
||||
phase: Some(MessagePhase::FinalAnswer),
|
||||
@@ -1038,6 +1039,7 @@ async fn prompt_edit_stops_streaming_without_submitting_queued_input() {
|
||||
let retained_turn = AppServerTurn {
|
||||
items: vec![
|
||||
ThreadItem::untimed(AppServerThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "retained-response".to_string(),
|
||||
text: "retained response".to_string(),
|
||||
phase: Some(MessagePhase::FinalAnswer),
|
||||
@@ -1055,6 +1057,7 @@ async fn prompt_edit_stops_streaming_without_submitting_queued_input() {
|
||||
}],
|
||||
}),
|
||||
ThreadItem::untimed(AppServerThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "private-commentary".to_string(),
|
||||
text: "hidden voice commentary".to_string(),
|
||||
phase: Some(MessagePhase::Commentary),
|
||||
|
||||
@@ -805,6 +805,7 @@ async fn plan_implementation_popup_skips_replayed_turn_complete() {
|
||||
items_view: codex_app_server_protocol::TurnItemsView::Full,
|
||||
items: vec?"),
|
||||
phase: None,
|
||||
|
||||
@@ -1636,6 +1636,7 @@ async fn slash_copy_picker_uses_completed_commentary_during_active_turn() {
|
||||
current_thread_id,
|
||||
"active".to_string(),
|
||||
AppServerThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "active-commentary".to_string(),
|
||||
text: commentary.to_string(),
|
||||
phase: Some(MessagePhase::Commentary),
|
||||
|
||||
@@ -41,6 +41,7 @@ async fn blocked_copy_allows_overlay_exit_rejects_backlog_and_wakes_completion()
|
||||
String::new(),
|
||||
"turn-1".into(),
|
||||
codex_app_server_protocol::ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "msg-1".into(),
|
||||
text: "café\nsecond line".into(),
|
||||
phase: None,
|
||||
|
||||
@@ -7537,6 +7537,7 @@ session_picker_view = "dense"
|
||||
}],
|
||||
}),
|
||||
ThreadItem::untimed(ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: String::from("agent-1"),
|
||||
text: String::from("hello from assistant"),
|
||||
phase: None,
|
||||
|
||||
@@ -40,6 +40,7 @@ fn rollout_user_message(text: &str) -> RolloutItem {
|
||||
|
||||
fn rollout_agent_message(text: &str) -> RolloutItem {
|
||||
RolloutItem::EventMsg(EventMsg::AgentMessage(AgentMessageEvent {
|
||||
moderation: None,
|
||||
message: text.to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
@@ -273,6 +274,7 @@ fn legacy_transcript_preview_falls_back_when_scan_budget_is_exhausted() {
|
||||
#[test]
|
||||
fn transcript_preview_reverse_scan_stops_before_older_items() {
|
||||
let newest = ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: String::from("final"),
|
||||
text: String::from("one\ntwo\nthree\nfour\nfive\nsix"),
|
||||
phase: None,
|
||||
|
||||
@@ -27,6 +27,7 @@ fn agent_message_notification(turn_id: &str, text: &str) -> ServerNotification {
|
||||
"thread-1".to_string(),
|
||||
turn_id.to_string(),
|
||||
ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "message-1".to_string(),
|
||||
text: text.to_string(),
|
||||
phase: None,
|
||||
|
||||
@@ -164,6 +164,7 @@ fn joins_respect_actual_turns_and_intervening_items() {
|
||||
expected[0].transcript_lines(/*width*/ 80),
|
||||
);
|
||||
turns[0].items[1] = ThreadItem::untimed(ThreadItemPayload::AgentMessage {
|
||||
moderation: None,
|
||||
id: "message".to_string(),
|
||||
text: "Visible boundary".to_string(),
|
||||
phase: None,
|
||||
|
||||
@@ -154,6 +154,13 @@ class AgentMessageInputContent(
|
||||
root: InputTextAgentMessageInputContent | EncryptedContentAgentMessageInputContent
|
||||
|
||||
|
||||
class AgentMessageModeration(BaseModel):
|
||||
model_config = ConfigDict(
|
||||
populate_by_name=True,
|
||||
)
|
||||
moderated: bool
|
||||
|
||||
|
||||
class AgentPath(RootModel[str]):
|
||||
root: str
|
||||
|
||||
@@ -10715,6 +10722,7 @@ class AgentMessageThreadItem(BaseModel):
|
||||
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[
|
||||
|
||||
Reference in New Issue
Block a user