Add async delivery metadata to agent messages (#39312)

## What changed

- Add an optional `delivery` field to agent message events and app-server
  `agentMessage` items, with `"async"` identifying a user-visible message sent
  without ending the current turn.
- Preserve the marker through legacy event conversion, thread history
  materialization, replay, and generated JSON and TypeScript schemas.
- Require delivery of async agent message completion notifications on the
  in-process app-server transport.

## Testing

- Cover preservation of async delivery metadata in thread history.
- Cover delivery-required classification for async agent messages.

GitOrigin-RevId: f92ba25de1293ee271404badfdf0d117d6530329
This commit is contained in:
rka-oai authored and copyberry committed 2026-08-18 23:05:52 +00:00
1 parent 7d9990fa30
commit fb356f3d2c
64 files changed
+467 -21

No files matched your search

+1
View File
@@ -1027,6 +1027,7 @@ mod tests {
text: text.to_string(), text: text.to_string(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}, },
}) })
} }
@@ -148,6 +148,12 @@
}, },
"type": "object" "type": "object"
}, },
"AgentMessageDelivery": {
"enum": [
"async"
],
"type": "string"
},
"AgentMessageDeltaNotification": { "AgentMessageDeltaNotification": {
"properties": { "properties": {
"delta": { "delta": {
@@ -4194,6 +4200,17 @@
}, },
{ {
"properties": { "properties": {
"delivery": {
"anyOf": [
{
"$ref": "#/definitions/AgentMessageDelivery"
},
{
"type": "null"
}
],
"default": null
},
"id": { "id": {
"type": "string" "type": "string"
}, },
@@ -6568,6 +6568,12 @@
}, },
"type": "object" "type": "object"
}, },
"AgentMessageDelivery": {
"enum": [
"async"
],
"type": "string"
},
"AgentMessageDeltaNotification": { "AgentMessageDeltaNotification": {
"$schema": "http://json-schema.org/draft-07/schema#", "$schema": "http://json-schema.org/draft-07/schema#",
"properties": { "properties": {
@@ -19799,6 +19805,17 @@
}, },
{ {
"properties": { "properties": {
"delivery": {
"anyOf": [
{
"$ref": "#/definitions/v2/AgentMessageDelivery"
},
{
"type": "null"
}
],
"default": null
},
"id": { "id": {
"type": "string" "type": "string"
}, },
@@ -313,6 +313,12 @@
}, },
"type": "object" "type": "object"
}, },
"AgentMessageDelivery": {
"enum": [
"async"
],
"type": "string"
},
"AgentMessageDeltaNotification": { "AgentMessageDeltaNotification": {
"$schema": "http://json-schema.org/draft-07/schema#", "$schema": "http://json-schema.org/draft-07/schema#",
"properties": { "properties": {
@@ -17551,6 +17557,17 @@
}, },
{ {
"properties": { "properties": {
"delivery": {
"anyOf": [
{
"$ref": "#/definitions/AgentMessageDelivery"
},
{
"type": "null"
}
],
"default": null
},
"id": { "id": {
"type": "string" "type": "string"
}, },
@@ -5,6 +5,12 @@
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", "description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
"type": "string" "type": "string"
}, },
"AgentMessageDelivery": {
"enum": [
"async"
],
"type": "string"
},
"ByteRange": { "ByteRange": {
"properties": { "properties": {
"end": { "end": {
@@ -650,6 +656,17 @@
}, },
{ {
"properties": { "properties": {
"delivery": {
"anyOf": [
{
"$ref": "#/definitions/AgentMessageDelivery"
},
{
"type": "null"
}
],
"default": null
},
"id": { "id": {
"type": "string" "type": "string"
}, },
@@ -5,6 +5,12 @@
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", "description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
"type": "string" "type": "string"
}, },
"AgentMessageDelivery": {
"enum": [
"async"
],
"type": "string"
},
"ByteRange": { "ByteRange": {
"properties": { "properties": {
"end": { "end": {
@@ -650,6 +656,17 @@
}, },
{ {
"properties": { "properties": {
"delivery": {
"anyOf": [
{
"$ref": "#/definitions/AgentMessageDelivery"
},
{
"type": "null"
}
],
"default": null
},
"id": { "id": {
"type": "string" "type": "string"
}, },
@@ -5,6 +5,12 @@
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", "description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
"type": "string" "type": "string"
}, },
"AgentMessageDelivery": {
"enum": [
"async"
],
"type": "string"
},
"ByteRange": { "ByteRange": {
"properties": { "properties": {
"end": { "end": {
@@ -796,6 +802,17 @@
}, },
{ {
"properties": { "properties": {
"delivery": {
"anyOf": [
{
"$ref": "#/definitions/AgentMessageDelivery"
},
{
"type": "null"
}
],
"default": null
},
"id": { "id": {
"type": "string" "type": "string"
}, },
@@ -25,6 +25,12 @@
], ],
"type": "object" "type": "object"
}, },
"AgentMessageDelivery": {
"enum": [
"async"
],
"type": "string"
},
"AgentPath": { "AgentPath": {
"type": "string" "type": "string"
}, },
@@ -1353,6 +1359,17 @@
}, },
{ {
"properties": { "properties": {
"delivery": {
"anyOf": [
{
"$ref": "#/definitions/AgentMessageDelivery"
},
{
"type": "null"
}
],
"default": null
},
"id": { "id": {
"type": "string" "type": "string"
}, },
@@ -5,6 +5,12 @@
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", "description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
"type": "string" "type": "string"
}, },
"AgentMessageDelivery": {
"enum": [
"async"
],
"type": "string"
},
"AgentPath": { "AgentPath": {
"type": "string" "type": "string"
}, },
@@ -1144,6 +1150,17 @@
}, },
{ {
"properties": { "properties": {
"delivery": {
"anyOf": [
{
"$ref": "#/definitions/AgentMessageDelivery"
},
{
"type": "null"
}
],
"default": null
},
"id": { "id": {
"type": "string" "type": "string"
}, },
@@ -5,6 +5,12 @@
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", "description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
"type": "string" "type": "string"
}, },
"AgentMessageDelivery": {
"enum": [
"async"
],
"type": "string"
},
"AgentPath": { "AgentPath": {
"type": "string" "type": "string"
}, },
@@ -1144,6 +1150,17 @@
}, },
{ {
"properties": { "properties": {
"delivery": {
"anyOf": [
{
"$ref": "#/definitions/AgentMessageDelivery"
},
{
"type": "null"
}
],
"default": null
},
"id": { "id": {
"type": "string" "type": "string"
}, },
@@ -5,6 +5,12 @@
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", "description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
"type": "string" "type": "string"
}, },
"AgentMessageDelivery": {
"enum": [
"async"
],
"type": "string"
},
"AgentPath": { "AgentPath": {
"type": "string" "type": "string"
}, },
@@ -1144,6 +1150,17 @@
}, },
{ {
"properties": { "properties": {
"delivery": {
"anyOf": [
{
"$ref": "#/definitions/AgentMessageDelivery"
},
{
"type": "null"
}
],
"default": null
},
"id": { "id": {
"type": "string" "type": "string"
}, },
@@ -25,6 +25,12 @@
], ],
"type": "object" "type": "object"
}, },
"AgentMessageDelivery": {
"enum": [
"async"
],
"type": "string"
},
"AgentPath": { "AgentPath": {
"type": "string" "type": "string"
}, },
@@ -1353,6 +1359,17 @@
}, },
{ {
"properties": { "properties": {
"delivery": {
"anyOf": [
{
"$ref": "#/definitions/AgentMessageDelivery"
},
{
"type": "null"
}
],
"default": null
},
"id": { "id": {
"type": "string" "type": "string"
}, },
@@ -5,6 +5,12 @@
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", "description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
"type": "string" "type": "string"
}, },
"AgentMessageDelivery": {
"enum": [
"async"
],
"type": "string"
},
"AgentPath": { "AgentPath": {
"type": "string" "type": "string"
}, },
@@ -1144,6 +1150,17 @@
}, },
{ {
"properties": { "properties": {
"delivery": {
"anyOf": [
{
"$ref": "#/definitions/AgentMessageDelivery"
},
{
"type": "null"
}
],
"default": null
},
"id": { "id": {
"type": "string" "type": "string"
}, },
@@ -25,6 +25,12 @@
], ],
"type": "object" "type": "object"
}, },
"AgentMessageDelivery": {
"enum": [
"async"
],
"type": "string"
},
"AgentPath": { "AgentPath": {
"type": "string" "type": "string"
}, },
@@ -1353,6 +1359,17 @@
}, },
{ {
"properties": { "properties": {
"delivery": {
"anyOf": [
{
"$ref": "#/definitions/AgentMessageDelivery"
},
{
"type": "null"
}
],
"default": null
},
"id": { "id": {
"type": "string" "type": "string"
}, },
@@ -5,6 +5,12 @@
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", "description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
"type": "string" "type": "string"
}, },
"AgentMessageDelivery": {
"enum": [
"async"
],
"type": "string"
},
"AgentPath": { "AgentPath": {
"type": "string" "type": "string"
}, },
@@ -1144,6 +1150,17 @@
}, },
{ {
"properties": { "properties": {
"delivery": {
"anyOf": [
{
"$ref": "#/definitions/AgentMessageDelivery"
},
{
"type": "null"
}
],
"default": null
},
"id": { "id": {
"type": "string" "type": "string"
}, },
@@ -5,6 +5,12 @@
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", "description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
"type": "string" "type": "string"
}, },
"AgentMessageDelivery": {
"enum": [
"async"
],
"type": "string"
},
"AgentPath": { "AgentPath": {
"type": "string" "type": "string"
}, },
@@ -1144,6 +1150,17 @@
}, },
{ {
"properties": { "properties": {
"delivery": {
"anyOf": [
{
"$ref": "#/definitions/AgentMessageDelivery"
},
{
"type": "null"
}
],
"default": null
},
"id": { "id": {
"type": "string" "type": "string"
}, },
@@ -5,6 +5,12 @@
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", "description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
"type": "string" "type": "string"
}, },
"AgentMessageDelivery": {
"enum": [
"async"
],
"type": "string"
},
"ByteRange": { "ByteRange": {
"properties": { "properties": {
"end": { "end": {
@@ -796,6 +802,17 @@
}, },
{ {
"properties": { "properties": {
"delivery": {
"anyOf": [
{
"$ref": "#/definitions/AgentMessageDelivery"
},
{
"type": "null"
}
],
"default": null
},
"id": { "id": {
"type": "string" "type": "string"
}, },
@@ -5,6 +5,12 @@
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", "description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
"type": "string" "type": "string"
}, },
"AgentMessageDelivery": {
"enum": [
"async"
],
"type": "string"
},
"ByteRange": { "ByteRange": {
"properties": { "properties": {
"end": { "end": {
@@ -796,6 +802,17 @@
}, },
{ {
"properties": { "properties": {
"delivery": {
"anyOf": [
{
"$ref": "#/definitions/AgentMessageDelivery"
},
{
"type": "null"
}
],
"default": null
},
"id": { "id": {
"type": "string" "type": "string"
}, },
@@ -5,6 +5,12 @@
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", "description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
"type": "string" "type": "string"
}, },
"AgentMessageDelivery": {
"enum": [
"async"
],
"type": "string"
},
"ByteRange": { "ByteRange": {
"properties": { "properties": {
"end": { "end": {
@@ -796,6 +802,17 @@
}, },
{ {
"properties": { "properties": {
"delivery": {
"anyOf": [
{
"$ref": "#/definitions/AgentMessageDelivery"
},
{
"type": "null"
}
],
"default": null
},
"id": { "id": {
"type": "string" "type": "string"
}, },
@@ -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 AgentMessageDelivery = "async";
@@ -8,6 +8,7 @@ import type { ReasoningEffort } from "../ReasoningEffort";
import type { SleepItem } from "../SleepItem"; import type { SleepItem } from "../SleepItem";
import type { WebSearchItem } from "../WebSearchItem"; import type { WebSearchItem } from "../WebSearchItem";
import type { JsonValue } from "../serde_json/JsonValue"; import type { JsonValue } from "../serde_json/JsonValue";
import type { AgentMessageDelivery } from "./AgentMessageDelivery";
import type { CollabAgentState } from "./CollabAgentState"; import type { CollabAgentState } from "./CollabAgentState";
import type { CollabAgentTool } from "./CollabAgentTool"; import type { CollabAgentTool } from "./CollabAgentTool";
import type { CollabAgentToolCallStatus } from "./CollabAgentToolCallStatus"; import type { CollabAgentToolCallStatus } from "./CollabAgentToolCallStatus";
@@ -27,7 +28,7 @@ import type { PatchApplyStatus } from "./PatchApplyStatus";
import type { SubAgentActivityKind } from "./SubAgentActivityKind"; import type { SubAgentActivityKind } from "./SubAgentActivityKind";
import type { UserInput } from "./UserInput"; import type { UserInput } from "./UserInput";
export type ThreadItem = { "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, } | { "type": "plan", id: string, text: string, } | { "type": "reasoning", id: string, summary: Array<string>, content: Array<string>, } | { "type": "commandExecution", id: string, export type ThreadItem = { "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, } | { "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. * Trusted first-party plugin id when this command resolves to one plugin script.
*/ */
@@ -14,6 +14,7 @@ export type { AdditionalContextKind } from "./AdditionalContextKind";
export type { AdditionalFileSystemPermissions } from "./AdditionalFileSystemPermissions"; export type { AdditionalFileSystemPermissions } from "./AdditionalFileSystemPermissions";
export type { AdditionalNetworkPermissions } from "./AdditionalNetworkPermissions"; export type { AdditionalNetworkPermissions } from "./AdditionalNetworkPermissions";
export type { AdditionalPermissionProfile } from "./AdditionalPermissionProfile"; export type { AdditionalPermissionProfile } from "./AdditionalPermissionProfile";
export type { AgentMessageDelivery } from "./AgentMessageDelivery";
export type { AgentMessageDeltaNotification } from "./AgentMessageDeltaNotification"; export type { AgentMessageDeltaNotification } from "./AgentMessageDeltaNotification";
export type { AnalyticsConfig } from "./AnalyticsConfig"; export type { AnalyticsConfig } from "./AnalyticsConfig";
export type { AppBranding } from "./AppBranding"; export type { AppBranding } from "./AppBranding";
@@ -28,7 +28,7 @@ use crate::protocol::v2::WebSearchItem;
use crate::protocol::v2::web_search_action_from_core; use crate::protocol::v2::web_search_action_from_core;
use codex_extension_items::image_generation::ImageGenerationItem; use codex_extension_items::image_generation::ImageGenerationItem;
use codex_protocol::items::parse_hook_prompt_message; use codex_protocol::items::parse_hook_prompt_message;
use codex_protocol::models::MessagePhase; use codex_protocol::protocol::AgentMessageEvent;
use codex_protocol::protocol::AgentReasoningEvent; use codex_protocol::protocol::AgentReasoningEvent;
use codex_protocol::protocol::AgentReasoningRawContentEvent; use codex_protocol::protocol::AgentReasoningRawContentEvent;
use codex_protocol::protocol::AgentStatus; use codex_protocol::protocol::AgentStatus;
@@ -331,11 +331,7 @@ impl ThreadHistoryBuilder {
pub fn handle_event(&mut self, event: &EventMsg) { pub fn handle_event(&mut self, event: &EventMsg) {
match event { match event {
EventMsg::UserMessage(payload) => self.handle_user_message(payload), EventMsg::UserMessage(payload) => self.handle_user_message(payload),
EventMsg::AgentMessage(payload) => self.handle_agent_message( EventMsg::AgentMessage(payload) => self.handle_agent_message(payload),
payload.message.clone(),
payload.phase.clone(),
payload.memory_citation.clone().map(Into::into),
),
EventMsg::AgentReasoning(payload) => self.handle_agent_reasoning(payload), EventMsg::AgentReasoning(payload) => self.handle_agent_reasoning(payload),
EventMsg::AgentReasoningRawContent(payload) => { EventMsg::AgentReasoningRawContent(payload) => {
self.handle_agent_reasoning_raw_content(payload) self.handle_agent_reasoning_raw_content(payload)
@@ -491,22 +487,18 @@ impl ThreadHistoryBuilder {
}); });
} }
fn handle_agent_message( fn handle_agent_message(&mut self, payload: &AgentMessageEvent) {
&mut self, if payload.message.is_empty() {
text: String,
phase: Option<MessagePhase>,
memory_citation: Option<crate::protocol::v2::MemoryCitation>,
) {
if text.is_empty() {
return; return;
} }
let id = self.next_item_id(); let id = self.next_item_id();
self.push_item_in_current_turn(ThreadItem::AgentMessage { self.push_item_in_current_turn(ThreadItem::AgentMessage {
id, id,
text, text: payload.message.clone(),
phase, phase: payload.phase.clone(),
memory_citation, memory_citation: payload.memory_citation.clone().map(Into::into),
delivery: payload.delivery,
}); });
} }
@@ -1628,6 +1620,7 @@ impl From<&PendingTurn> for Turn {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::protocol::v2::AgentMessageDelivery;
use crate::protocol::v2::CommandExecutionSource; use crate::protocol::v2::CommandExecutionSource;
use codex_extension_items::ExtensionItem as CoreExtensionItem; use codex_extension_items::ExtensionItem as CoreExtensionItem;
use codex_extension_items::sleep::SleepItem as CoreSleepItem; use codex_extension_items::sleep::SleepItem as CoreSleepItem;
@@ -1646,7 +1639,6 @@ mod tests {
use codex_protocol::models::MessagePhase as CoreMessagePhase; use codex_protocol::models::MessagePhase as CoreMessagePhase;
use codex_protocol::models::WebSearchAction as CoreWebSearchAction; use codex_protocol::models::WebSearchAction as CoreWebSearchAction;
use codex_protocol::parse_command::ParsedCommand; use codex_protocol::parse_command::ParsedCommand;
use codex_protocol::protocol::AgentMessageEvent;
use codex_protocol::protocol::AgentReasoningEvent; use codex_protocol::protocol::AgentReasoningEvent;
use codex_protocol::protocol::AgentReasoningRawContentEvent; use codex_protocol::protocol::AgentReasoningRawContentEvent;
use codex_protocol::protocol::ApplyPatchApprovalRequestEvent; use codex_protocol::protocol::ApplyPatchApprovalRequestEvent;
@@ -1693,6 +1685,7 @@ mod tests {
message: "Hi there".into(), message: "Hi there".into(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}), }),
EventMsg::AgentReasoning(AgentReasoningEvent { EventMsg::AgentReasoning(AgentReasoningEvent {
text: "thinking".into(), text: "thinking".into(),
@@ -1712,6 +1705,7 @@ mod tests {
message: "Reply two".into(), message: "Reply two".into(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}), }),
]; ];
@@ -1750,6 +1744,7 @@ mod tests {
text: "Hi there".into(), text: "Hi there".into(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
} }
); );
assert_eq!( assert_eq!(
@@ -1783,6 +1778,7 @@ mod tests {
text: "Reply two".into(), text: "Reply two".into(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
} }
); );
} }
@@ -2330,11 +2326,12 @@ mod tests {
} }
#[test] #[test]
fn preserves_agent_message_phase_in_history() { fn preserves_agent_message_phase_and_delivery_in_history() {
let events = vec![EventMsg::AgentMessage(AgentMessageEvent { let events = vec![EventMsg::AgentMessage(AgentMessageEvent {
message: "Final reply".into(), message: "Final reply".into(),
phase: Some(CoreMessagePhase::FinalAnswer), phase: Some(CoreMessagePhase::FinalAnswer),
memory_citation: None, memory_citation: None,
delivery: Some(AgentMessageDelivery::Async),
})]; })];
let items = events let items = events
@@ -2348,8 +2345,9 @@ mod tests {
ThreadItem::AgentMessage { ThreadItem::AgentMessage {
id: "item-1".into(), id: "item-1".into(),
text: "Final reply".into(), text: "Final reply".into(),
phase: Some(MessagePhase::FinalAnswer), phase: Some(CoreMessagePhase::FinalAnswer),
memory_citation: None, memory_citation: None,
delivery: Some(AgentMessageDelivery::Async),
} }
); );
} }
@@ -2458,6 +2456,7 @@ mod tests {
message: "interlude".into(), message: "interlude".into(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}), }),
EventMsg::AgentReasoning(AgentReasoningEvent { EventMsg::AgentReasoning(AgentReasoningEvent {
text: "second summary".into(), text: "second summary".into(),
@@ -2506,6 +2505,7 @@ mod tests {
message: "Working...".into(), message: "Working...".into(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}), }),
EventMsg::TurnAborted(TurnAbortedEvent { EventMsg::TurnAborted(TurnAbortedEvent {
turn_id: Some("turn-1".into()), turn_id: Some("turn-1".into()),
@@ -2526,6 +2526,7 @@ mod tests {
message: "Second attempt complete.".into(), message: "Second attempt complete.".into(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}), }),
]; ];
@@ -2557,6 +2558,7 @@ mod tests {
text: "Working...".into(), text: "Working...".into(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
} }
); );
@@ -2581,6 +2583,7 @@ mod tests {
text: "Second attempt complete.".into(), text: "Second attempt complete.".into(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
} }
); );
} }
@@ -2600,6 +2603,7 @@ mod tests {
message: "A1".into(), message: "A1".into(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}), }),
EventMsg::UserMessage(UserMessageEvent { EventMsg::UserMessage(UserMessageEvent {
client_id: None, client_id: None,
@@ -2613,6 +2617,7 @@ mod tests {
message: "A2".into(), message: "A2".into(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}), }),
EventMsg::ThreadRolledBack(ThreadRolledBackEvent { num_turns: 1 }), EventMsg::ThreadRolledBack(ThreadRolledBackEvent { num_turns: 1 }),
EventMsg::UserMessage(UserMessageEvent { EventMsg::UserMessage(UserMessageEvent {
@@ -2627,6 +2632,7 @@ mod tests {
message: "A3".into(), message: "A3".into(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}), }),
]; ];
@@ -2657,6 +2663,7 @@ mod tests {
text: "A1".into(), text: "A1".into(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}, },
] ]
); );
@@ -2676,6 +2683,7 @@ mod tests {
text: "A3".into(), text: "A3".into(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}, },
] ]
); );
@@ -2696,6 +2704,7 @@ mod tests {
message: "A1".into(), message: "A1".into(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}), }),
EventMsg::UserMessage(UserMessageEvent { EventMsg::UserMessage(UserMessageEvent {
client_id: None, client_id: None,
@@ -2709,6 +2718,7 @@ mod tests {
message: "A2".into(), message: "A2".into(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}), }),
EventMsg::ThreadRolledBack(ThreadRolledBackEvent { num_turns: 99 }), EventMsg::ThreadRolledBack(ThreadRolledBackEvent { num_turns: 99 }),
]; ];
@@ -3741,6 +3751,7 @@ mod tests {
message: "still in b".into(), message: "still in b".into(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}), }),
EventMsg::TurnComplete(TurnCompleteEvent { EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-b".into(), turn_id: "turn-b".into(),
@@ -3917,6 +3928,7 @@ mod tests {
message: "still in b".into(), message: "still in b".into(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}), }),
]; ];
@@ -4180,6 +4192,7 @@ mod tests {
message: "done".into(), message: "done".into(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}), }),
EventMsg::Error(ErrorEvent { EventMsg::Error(ErrorEvent {
message: "rollback failed".into(), message: "rollback failed".into(),
@@ -122,6 +122,7 @@ fn projects_completed_canonical_turn_items() {
}], }],
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}); });
let user_changes = project(item_completed(thread_id, "turn-1", user_item.clone())); let user_changes = project(item_completed(thread_id, "turn-1", user_item.clone()));
@@ -24,6 +24,7 @@ use codex_protocol::approvals::GuardianAssessmentAction as CoreGuardianAssessmen
use codex_protocol::approvals::GuardianAssessmentDecisionSource as CoreGuardianAssessmentDecisionSource; use codex_protocol::approvals::GuardianAssessmentDecisionSource as CoreGuardianAssessmentDecisionSource;
use codex_protocol::approvals::GuardianCommandSource as CoreGuardianCommandSource; use codex_protocol::approvals::GuardianCommandSource as CoreGuardianCommandSource;
use codex_protocol::items::AgentMessageContent as CoreAgentMessageContent; use codex_protocol::items::AgentMessageContent as CoreAgentMessageContent;
pub use codex_protocol::items::AgentMessageDelivery;
use codex_protocol::items::CollabAgentTool as CoreCollabAgentTool; use codex_protocol::items::CollabAgentTool as CoreCollabAgentTool;
use codex_protocol::items::CollabAgentToolCallStatus as CoreCollabAgentToolCallStatus; use codex_protocol::items::CollabAgentToolCallStatus as CoreCollabAgentToolCallStatus;
use codex_protocol::items::CommandExecutionStatus as CoreCommandExecutionStatus; use codex_protocol::items::CommandExecutionStatus as CoreCommandExecutionStatus;
@@ -250,6 +251,8 @@ pub enum ThreadItem {
phase: Option<MessagePhase>, phase: Option<MessagePhase>,
#[serde(default)] #[serde(default)]
memory_citation: Option<MemoryCitation>, memory_citation: Option<MemoryCitation>,
#[serde(default)]
delivery: Option<AgentMessageDelivery>,
}, },
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")] #[ts(rename_all = "camelCase")]
@@ -833,6 +836,7 @@ impl From<CoreTurnItem> for ThreadItem {
text, text,
phase: agent.phase, phase: agent.phase,
memory_citation: agent.memory_citation.map(Into::into), memory_citation: agent.memory_citation.map(Into::into),
delivery: agent.delivery,
} }
} }
CoreTurnItem::Plan(plan) => ThreadItem::Plan { CoreTurnItem::Plan(plan) => ThreadItem::Plan {
@@ -2942,6 +2942,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
], ],
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}); });
assert_eq!( assert_eq!(
@@ -2951,6 +2952,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
text: "Hello world".to_string(), text: "Hello world".to_string(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
} }
); );
@@ -2969,6 +2971,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
}], }],
rollout_ids: vec!["rollout-1".to_string()], rollout_ids: vec!["rollout-1".to_string()],
}), }),
delivery: None,
}); });
assert_eq!( assert_eq!(
@@ -2986,6 +2989,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
}], }],
thread_ids: vec!["rollout-1".to_string()], thread_ids: vec!["rollout-1".to_string()],
}), }),
delivery: None,
} }
); );
+1 -1
View File
@@ -1603,7 +1603,7 @@ The app-server streams JSON-RPC notifications while a turn is running. Each turn
`ThreadItem` is the tagged union carried in turn responses and `item/*` notifications. Currently we support events for the following items: `ThreadItem` is the tagged union carried in turn responses and `item/*` notifications. Currently we support events for the following items:
- `userMessage` — `{id, clientId, content}` where `clientId` is the optional `clientUserMessageId` supplied to `turn/start` or `turn/steer`, and `content` is a list of user inputs (`text`, `image`, `localImage`, `audio`, or `localAudio`). - `userMessage` — `{id, clientId, content}` where `clientId` is the optional `clientUserMessageId` supplied to `turn/start` or `turn/steer`, and `content` is a list of user inputs (`text`, `image`, `localImage`, `audio`, or `localAudio`).
- `agentMessage` — `{id, text}` containing the accumulated agent reply. - `agentMessage` — `{id, text, phase, memoryCitation, delivery}` containing the accumulated agent reply. `delivery: "async"` identifies a user-visible message sent without ending the current turn; ordinary agent messages have `delivery: null`.
- `plan` — `{id, text}` emitted for plan-mode turns; plan text can stream via `item/plan/delta` (experimental). - `plan` — `{id, text}` emitted for plan-mode turns; plan text can stream via `item/plan/delta` (experimental).
- `reasoning` — `{id, summary, content}` where `summary` holds streamed reasoning summaries (applicable for most OpenAI models) and `content` holds raw reasoning blocks (applicable for e.g. open source models). - `reasoning` — `{id, summary, content}` where `summary` holds streamed reasoning summaries (applicable for most OpenAI models) and `content` holds raw reasoning blocks (applicable for e.g. open source models).
- `commandExecution` — `{id, pluginId?, scriptPath?, command, cwd, status, commandActions, aggregatedOutput?, exitCode?, durationMs?}` for sandboxed commands; `pluginId` is present only for commands attributed to a trusted first-party plugin, newly attributed items also include `scriptPath` as a safe `/`-separated path relative to the trusted plugin root, older history may omit `scriptPath`, and `status` is `inProgress`, `completed`, `failed`, or `declined`. Ordinary execution items and their replay expose `command` and `commandActions` as redacted display values, not executable commands. - `commandExecution` — `{id, pluginId?, scriptPath?, command, cwd, status, commandActions, aggregatedOutput?, exitCode?, durationMs?}` for sandboxed commands; `pluginId` is present only for commands attributed to a trusted first-party plugin, newly attributed items also include `scriptPath` as a safe `/`-separated path relative to the trusted plugin root, older history may omit `scriptPath`, and `status` is `inProgress`, `completed`, `failed`, or `declined`. Ordinary execution items and their replay expose `command` and `commandActions` as redacted display values, not executable commands.
@@ -2225,6 +2225,7 @@ mod tests {
message: "after rollback".to_string(), message: "after rollback".to_string(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
})), })),
]; ];
let stored_thread = StoredThread { let stored_thread = StoredThread {
@@ -3634,6 +3635,7 @@ mod tests {
], ],
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}), }),
started_at_ms: Some(0), started_at_ms: Some(0),
completed_at_ms: 0, completed_at_ms: 0,
@@ -3651,6 +3653,7 @@ mod tests {
}], }],
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}), }),
started_at_ms: Some(0), started_at_ms: Some(0),
completed_at_ms: 0, completed_at_ms: 0,
+24
View File
@@ -67,15 +67,18 @@ use crate::transport::CHANNEL_CAPACITY;
use crate::transport::OutboundConnectionState; use crate::transport::OutboundConnectionState;
use crate::transport::route_outgoing_envelope; use crate::transport::route_outgoing_envelope;
use codex_analytics::AppServerRpcTransport; use codex_analytics::AppServerRpcTransport;
use codex_app_server_protocol::AgentMessageDelivery;
use codex_app_server_protocol::ClientNotification; use codex_app_server_protocol::ClientNotification;
use codex_app_server_protocol::ClientRequest; use codex_app_server_protocol::ClientRequest;
use codex_app_server_protocol::ConfigWarningNotification; use codex_app_server_protocol::ConfigWarningNotification;
use codex_app_server_protocol::InitializeParams; use codex_app_server_protocol::InitializeParams;
use codex_app_server_protocol::ItemCompletedNotification;
use codex_app_server_protocol::JSONRPCErrorError; use codex_app_server_protocol::JSONRPCErrorError;
use codex_app_server_protocol::RequestId; use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::Result; use codex_app_server_protocol::Result;
use codex_app_server_protocol::ServerNotification; use codex_app_server_protocol::ServerNotification;
use codex_app_server_protocol::ServerRequest; use codex_app_server_protocol::ServerRequest;
use codex_app_server_protocol::ThreadItem;
use codex_arg0::Arg0DispatchPaths; use codex_arg0::Arg0DispatchPaths;
use codex_config::CloudConfigBundleLoader; use codex_config::CloudConfigBundleLoader;
use codex_config::LoaderOverrides; use codex_config::LoaderOverrides;
@@ -111,6 +114,13 @@ fn server_notification_requires_delivery(notification: &ServerNotification) -> b
| ServerNotification::ThreadQueueChanged(_) | ServerNotification::ThreadQueueChanged(_)
| ServerNotification::ThreadSettingsUpdated(_) | ServerNotification::ThreadSettingsUpdated(_)
| ServerNotification::ExternalAgentConfigImportCompleted(_) | ServerNotification::ExternalAgentConfigImportCompleted(_)
| ServerNotification::ItemCompleted(ItemCompletedNotification {
item: ThreadItem::AgentMessage {
delivery: Some(AgentMessageDelivery::Async),
..
},
..
})
) )
} }
@@ -1011,5 +1021,19 @@ mod tests {
}, },
) )
)); ));
assert!(server_notification_requires_delivery(
&ServerNotification::ItemCompleted(ItemCompletedNotification {
item: ThreadItem::AgentMessage {
id: "item-1".to_string(),
text: "Still working".to_string(),
phase: None,
memory_citation: None,
delivery: Some(AgentMessageDelivery::Async),
},
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
completed_at_ms: 0,
})
));
} }
} }
@@ -73,6 +73,7 @@ mod tests {
text: "kept".to_string(), text: "kept".to_string(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}, },
ThreadItem::McpToolCall { ThreadItem::McpToolCall {
id: "mcp-1".to_string(), id: "mcp-1".to_string(),
@@ -122,6 +123,7 @@ mod tests {
text: "kept".to_string(), text: "kept".to_string(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
} }
); );
assert_eq!( assert_eq!(
@@ -177,6 +177,7 @@ mod tests {
message: "first answer".to_string(), message: "first answer".to_string(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
})), })),
RolloutItem::EventMsg(EventMsg::TokenCount(TokenCountEvent { RolloutItem::EventMsg(EventMsg::TokenCount(TokenCountEvent {
info: None, info: None,
@@ -2041,6 +2041,7 @@ async fn external_agent_config_import_creates_session_rollouts() -> Result<()> {
text: "<EXTERNAL SESSION IMPORTED>".into(), text: "<EXTERNAL SESSION IMPORTED>".into(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}) })
); );
@@ -537,6 +537,7 @@ async fn thread_search_occurrences_reads_paginated_projection() -> Result<()> {
}], }],
phase: Some(MessagePhase::Commentary), phase: Some(MessagePhase::Commentary),
memory_citation: None, memory_citation: None,
delivery: None,
}), }),
), ),
paginated_completed_item( paginated_completed_item(
@@ -549,6 +550,7 @@ async fn thread_search_occurrences_reads_paginated_projection() -> Result<()> {
}], }],
phase: Some(MessagePhase::FinalAnswer), phase: Some(MessagePhase::FinalAnswer),
memory_citation: None, memory_citation: None,
delivery: None,
}), }),
), ),
paginated_turn_completed("turn-1"), paginated_turn_completed("turn-1"),
@@ -1577,6 +1579,7 @@ async fn paginated_history_lists_and_legacy_reads_use_projected_turns_and_items(
}], }],
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}), }),
), ),
paginated_completed_item( paginated_completed_item(
@@ -1628,6 +1631,7 @@ async fn paginated_history_lists_and_legacy_reads_use_projected_turns_and_items(
text: "first".to_string(), text: "first".to_string(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}, },
], ],
items_view: TurnItemsView::Full, items_view: TurnItemsView::Full,
@@ -1813,6 +1817,7 @@ async fn paginated_history_lists_and_legacy_reads_use_projected_turns_and_items(
text: "first".to_string(), text: "first".to_string(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}, },
], ],
items_view: TurnItemsView::Summary, items_view: TurnItemsView::Summary,
@@ -2058,6 +2063,7 @@ fn append_agent_message(path: &Path, timestamp: &str, text: &str) -> anyhow::Res
message: text.to_string(), message: text.to_string(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}))?, }))?,
}) })
)?; )?;
@@ -3303,6 +3303,7 @@ async fn thread_resume_token_usage_replay_ignores_stale_interrupted_tail_turn()
message: "Still running".to_string(), message: "Still running".to_string(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}))?, }))?,
}) })
.to_string(), .to_string(),
@@ -3389,6 +3390,7 @@ async fn thread_resume_token_usage_replay_can_belong_to_interrupted_turn() -> Re
message: "Interrupted after usage".to_string(), message: "Interrupted after usage".to_string(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}))?, }))?,
}) })
.to_string(), .to_string(),
@@ -3697,6 +3699,7 @@ async fn thread_resume_and_read_interrupt_incomplete_rollout_turn_when_thread_is
message: "Still running".to_string(), message: "Still running".to_string(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}))?, }))?,
}) })
.to_string(), .to_string(),
+1
View File
@@ -171,6 +171,7 @@ fn parse_agent_message(
content, content,
phase, phase,
memory_citation: None, memory_citation: None,
delivery: None,
} }
} }
+1
View File
@@ -2020,6 +2020,7 @@ async fn emit_agent_message_in_plan_mode(
content: Vec::new(), content: Vec::new(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}) })
}); });
sess.emit_turn_item_started(turn_context, &start_item).await; sess.emit_turn_item_started(turn_context, &start_item).await;
@@ -2100,6 +2100,7 @@ fn completed_legacy_event_history_is_not_mid_turn() {
message: "done".to_string(), message: "done".to_string(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
})), })),
]); ]);
+2
View File
@@ -65,6 +65,7 @@ async fn turn_timing_state_records_ttfm_independently_of_ttft() {
content: Vec::new(), content: Vec::new(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
})) }))
.await .await
.is_some() .is_some()
@@ -76,6 +77,7 @@ async fn turn_timing_state_records_ttfm_independently_of_ttft() {
content: Vec::new(), content: Vec::new(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
})) }))
.await, .await,
None None
@@ -250,6 +250,7 @@ fn final_message_from_turn_items_uses_latest_agent_message() {
text: "first".to_string(), text: "first".to_string(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}, },
ThreadItem::Plan { ThreadItem::Plan {
id: "plan-1".to_string(), id: "plan-1".to_string(),
@@ -260,6 +261,7 @@ fn final_message_from_turn_items_uses_latest_agent_message() {
text: "second".to_string(), text: "second".to_string(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}, },
]); ]);
@@ -318,6 +320,7 @@ fn turn_completed_recovers_final_message_from_turn_items() {
text: "final answer".to_string(), text: "final answer".to_string(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}], }],
status: TurnStatus::Completed, status: TurnStatus::Completed,
error: None, error: None,
@@ -366,6 +369,7 @@ fn turn_completed_overwrites_stale_final_message_from_turn_items() {
text: "final answer".to_string(), text: "final answer".to_string(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}], }],
status: TurnStatus::Completed, status: TurnStatus::Completed,
error: None, error: None,
@@ -18,6 +18,7 @@ fn failed_turn_does_not_overwrite_output_last_message_file() {
text: "partial answer".to_string(), text: "partial answer".to_string(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}, },
thread_id: "thread-1".to_string(), thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(), turn_id: "turn-1".to_string(),
+2
View File
@@ -363,6 +363,7 @@ fn turn_items_for_thread_returns_matching_turn_items() {
text: "hello".to_string(), text: "hello".to_string(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}], }],
status: codex_app_server_protocol::TurnStatus::Completed, status: codex_app_server_protocol::TurnStatus::Completed,
error: None, error: None,
@@ -393,6 +394,7 @@ fn turn_items_for_thread_returns_matching_turn_items() {
text: "hello".to_string(), text: "hello".to_string(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}]) }])
); );
assert_eq!(turn_items_for_thread(&thread, "missing-turn"), None); assert_eq!(turn_items_for_thread(&thread, "missing-turn"), None);
@@ -305,6 +305,7 @@ fn unsupported_items_do_not_consume_synthetic_ids() {
text: "hello".to_string(), text: "hello".to_string(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}, },
thread_id: "thread-1".to_string(), thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(), turn_id: "turn-1".to_string(),
@@ -926,6 +927,7 @@ fn agent_message_item_updates_final_message() {
text: "hello".to_string(), text: "hello".to_string(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}, },
thread_id: "thread-1".to_string(), thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(), turn_id: "turn-1".to_string(),
@@ -961,6 +963,7 @@ fn agent_message_item_started_is_ignored() {
text: "hello".to_string(), text: "hello".to_string(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}, },
thread_id: "thread-1".to_string(), thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(), turn_id: "turn-1".to_string(),
@@ -1311,6 +1314,7 @@ fn turn_completion_recovers_final_message_from_turn_items() {
text: "final answer".to_string(), text: "final answer".to_string(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}], }],
status: TurnStatus::Completed, status: TurnStatus::Completed,
error: None, error: None,
@@ -1438,6 +1442,7 @@ fn turn_completion_overwrites_stale_final_message_from_turn_items() {
text: "stale answer".to_string(), text: "stale answer".to_string(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}, },
thread_id: "thread-1".to_string(), thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(), turn_id: "turn-1".to_string(),
@@ -1456,6 +1461,7 @@ fn turn_completion_overwrites_stale_final_message_from_turn_items() {
text: "final answer".to_string(), text: "final answer".to_string(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}], }],
status: TurnStatus::Completed, status: TurnStatus::Completed,
error: None, error: None,
@@ -1488,6 +1494,7 @@ fn turn_completion_preserves_streamed_final_message_when_turn_items_are_empty()
text: "streamed answer".to_string(), text: "streamed answer".to_string(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}, },
thread_id: "thread-1".to_string(), thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(), turn_id: "turn-1".to_string(),
@@ -1534,6 +1541,7 @@ fn failed_turn_clears_stale_final_message() {
text: "partial answer".to_string(), text: "partial answer".to_string(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}, },
thread_id: "thread-1".to_string(), thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(), turn_id: "turn-1".to_string(),
@@ -135,6 +135,7 @@ pub(super) fn rollout_items_from_messages(messages: Vec<ConversationMessage>) ->
message: message.text.clone(), message: message.text.clone(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}, },
))); )));
items.push(RolloutItem::ResponseItem(response_item(message).into())); items.push(RolloutItem::ResponseItem(response_item(message).into()));
@@ -156,6 +157,7 @@ fn external_session_imported_marker_item() -> RolloutItem {
message: EXTERNAL_SESSION_IMPORTED_MARKER.to_string(), message: EXTERNAL_SESSION_IMPORTED_MARKER.to_string(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
})) }))
} }
@@ -251,6 +253,7 @@ mod tests {
text: EXTERNAL_SESSION_IMPORTED_MARKER.into(), text: EXTERNAL_SESSION_IMPORTED_MARKER.into(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
} }
); );
} }
@@ -283,6 +286,7 @@ mod tests {
text: EXTERNAL_SESSION_IMPORTED_MARKER.into(), text: EXTERNAL_SESSION_IMPORTED_MARKER.into(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}) })
); );
let last_turn_complete = imported let last_turn_complete = imported
+11
View File
@@ -113,6 +113,14 @@ pub enum AgentMessageContent {
Text { text: String }, Text { text: String },
} }
#[derive(Debug, Clone, Copy, Deserialize, Serialize, TS, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub enum AgentMessageDelivery {
Async,
}
#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)] #[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
/// Assistant-authored message payload used in turn-item streams. /// Assistant-authored message payload used in turn-item streams.
/// ///
@@ -132,6 +140,9 @@ pub struct AgentMessageItem {
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)] #[ts(optional)]
pub memory_citation: Option<MemoryCitation>, pub memory_citation: Option<MemoryCitation>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub delivery: Option<AgentMessageDelivery>,
} }
#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)] #[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
+1
View File
@@ -105,6 +105,7 @@ impl AgentMessageItem {
message: text.clone(), message: text.clone(),
phase: self.phase.clone(), phase: self.phase.clone(),
memory_citation: self.memory_citation.clone(), memory_citation: self.memory_citation.clone(),
delivery: self.delivery,
}), }),
}) })
.collect() .collect()
+4
View File
@@ -32,6 +32,7 @@ use crate::dynamic_tools::DynamicToolCallRequest;
use crate::dynamic_tools::DynamicToolResponse; use crate::dynamic_tools::DynamicToolResponse;
use crate::dynamic_tools::DynamicToolSpec; use crate::dynamic_tools::DynamicToolSpec;
use crate::error::Result as CodexResult; use crate::error::Result as CodexResult;
use crate::items::AgentMessageDelivery;
use crate::items::TurnItem; use crate::items::TurnItem;
use crate::mcp::CallToolResult; use crate::mcp::CallToolResult;
use crate::mcp::RequestId; use crate::mcp::RequestId;
@@ -2336,6 +2337,9 @@ pub struct AgentMessageEvent {
pub phase: Option<MessagePhase>, pub phase: Option<MessagePhase>,
#[serde(default)] #[serde(default)]
pub memory_citation: Option<MemoryCitation>, pub memory_citation: Option<MemoryCitation>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub delivery: Option<AgentMessageDelivery>,
} }
#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq, JsonSchema, TS)] #[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
+4
View File
@@ -65,6 +65,7 @@ fn agent_message_item(message: &str) -> RolloutItem {
message: message.to_string(), message: message.to_string(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
})) }))
} }
@@ -644,6 +645,7 @@ async fn recorder_materializes_on_flush_with_pending_items() -> std::io::Result<
message: "buffered-event".to_string(), message: "buffered-event".to_string(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}, },
))]) ))])
.await?; .await?;
@@ -891,6 +893,7 @@ async fn persist_reports_filesystem_error_and_retries_buffered_items() -> std::i
message: "buffered-before-persist".to_string(), message: "buffered-before-persist".to_string(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}, },
))]) ))])
.await?; .await?;
@@ -942,6 +945,7 @@ async fn writer_state_retries_write_error_before_reporting_flush_success() -> st
message: "queued-after-writer-error".to_string(), message: "queued-after-writer-error".to_string(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}, },
))]); ))]);
+1
View File
@@ -1025,6 +1025,7 @@ mod tests {
message: "commentary".to_string(), message: "commentary".to_string(),
phase: Some(MessagePhase::Commentary), phase: Some(MessagePhase::Commentary),
memory_citation: None, memory_citation: None,
delivery: None,
})), })),
RolloutItem::ResponseItem( RolloutItem::ResponseItem(
ResponseItem::FunctionCallOutput { ResponseItem::FunctionCallOutput {
@@ -94,6 +94,7 @@ pub(super) fn completed_item(
}], }],
phase: event.phase.clone(), phase: event.phase.clone(),
memory_citation: event.memory_citation.clone(), memory_citation: event.memory_citation.clone(),
delivery: event.delivery,
}), }),
None, None,
)), )),
@@ -156,6 +156,7 @@ fn agent_message(text: &str) -> RolloutItem {
message: text.to_string(), message: text.to_string(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
})) }))
} }
@@ -226,6 +226,7 @@ async fn split_homes_support_backfill_listing_and_paginated_history() {
}], }],
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}), }),
), ),
turn_completed("turn-1"), turn_completed("turn-1"),
@@ -318,6 +319,7 @@ async fn paginated_live_append_materializes_turn_items_and_state() {
}], }],
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}), }),
), ),
turn_completed("turn-1"), turn_completed("turn-1"),
@@ -2343,6 +2345,7 @@ fn agent_message(id: &str, phase: MessagePhase) -> TurnItem {
}], }],
phase: Some(phase), phase: Some(phase),
memory_citation: None, memory_citation: None,
delivery: None,
}) })
} }
@@ -37,6 +37,7 @@ fn agent_status_uses_bounded_buffered_activity() {
text: "Finished checking the focused TUI tests.".to_string(), text: "Finished checking the focused TUI tests.".to_string(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}, },
thread_id: "thread-child".to_string(), thread_id: "thread-child".to_string(),
turn_id: "turn-1".to_string(), turn_id: "turn-1".to_string(),
+2
View File
@@ -3497,6 +3497,7 @@ async fn active_thread_file_change_approval_recovers_buffered_changes() {
text: "streaming".to_string(), text: "streaming".to_string(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}, },
}), }),
/*replay_kind*/ None, /*replay_kind*/ None,
@@ -7235,6 +7236,7 @@ async fn replay_thread_snapshot_replays_turn_history_in_order() {
text: "done".to_string(), text: "done".to_string(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}, },
], ],
status: TurnStatus::Completed, status: TurnStatus::Completed,
@@ -366,6 +366,7 @@ async fn older_pagination_reconciles_review_prompts_across_page_boundaries() ->
}], }],
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}) })
})); }));
items.extend([ items.extend([
@@ -554,6 +555,7 @@ async fn transcript_home_loads_every_older_history_page() -> Result<()> {
}], }],
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}), }),
started_at_ms: None, started_at_ms: None,
completed_at_ms: 0, completed_at_ms: 0,
@@ -919,6 +921,7 @@ async fn underfilled_scrollback_fetches_older_pages_without_opening_the_transcri
}], }],
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}), }),
started_at_ms: None, started_at_ms: None,
completed_at_ms: 0, completed_at_ms: 0,
+1
View File
@@ -3360,6 +3360,7 @@ mod tests {
text: "assistant reply".to_string(), text: "assistant reply".to_string(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}, },
], ],
status: TurnStatus::Completed, status: TurnStatus::Completed,
+3
View File
@@ -94,6 +94,8 @@ impl ChatWidget {
text, text,
phase, phase,
memory_citation, memory_citation,
delivery,
..
} => { } => {
self.on_agent_message_item_completed( self.on_agent_message_item_completed(
AgentMessageItem { AgentMessageItem {
@@ -117,6 +119,7 @@ impl ChatWidget {
rollout_ids: citation.thread_ids, rollout_ids: citation.thread_ids,
} }
}), }),
delivery,
}, },
&turn_id, &turn_id,
from_replay, from_replay,
@@ -621,6 +621,7 @@ async fn live_app_server_turn_completed_clears_working_status_after_answer_item(
text: "Yes. What do you need?".to_string(), text: "Yes. What do you need?".to_string(),
phase: Some(MessagePhase::FinalAnswer), phase: Some(MessagePhase::FinalAnswer),
memory_citation: None, memory_citation: None,
delivery: None,
}; };
chat.handle_server_notification( chat.handle_server_notification(
ServerNotification::ItemCompleted(ItemCompletedNotification { ServerNotification::ItemCompleted(ItemCompletedNotification {
@@ -1236,6 +1237,7 @@ async fn live_app_server_turn_completion_repairs_dropped_message_deltas() {
.to_string(), .to_string(),
phase: Some(MessagePhase::FinalAnswer), phase: Some(MessagePhase::FinalAnswer),
memory_citation: None, memory_citation: None,
delivery: None,
}]; }];
chat.handle_server_notification( chat.handle_server_notification(
ServerNotification::TurnCompleted(TurnCompletedNotification { ServerNotification::TurnCompleted(TurnCompletedNotification {
@@ -767,6 +767,7 @@ pub(super) fn replay_agent_message(
text: text.into(), text: text.into(),
phase: Some(MessagePhase::FinalAnswer), phase: Some(MessagePhase::FinalAnswer),
memory_citation: None, memory_citation: None,
delivery: None,
}, },
"turn-1".to_string(), "turn-1".to_string(),
replay_kind, replay_kind,
@@ -919,6 +920,7 @@ pub(super) fn complete_assistant_message(
text: text.to_string(), text: text.to_string(),
phase, phase,
memory_citation: None, memory_citation: None,
delivery: None,
}, },
}), }),
/*replay_kind*/ None, /*replay_kind*/ None,
@@ -332,6 +332,7 @@ async fn replayed_nested_review_prompts_do_not_render_or_seed_composer_history()
text: "review result is retained".to_string(), text: "review result is retained".to_string(),
phase: Some(MessagePhase::FinalAnswer), phase: Some(MessagePhase::FinalAnswer),
memory_citation: None, memory_citation: None,
delivery: None,
}, },
], ],
..app_server_turn( ..app_server_turn(
@@ -869,6 +869,7 @@ async fn plan_implementation_popup_skips_replayed_turn_complete() {
text: "Plan details".to_string(), text: "Plan details".to_string(),
phase: Some(MessagePhase::FinalAnswer), phase: Some(MessagePhase::FinalAnswer),
memory_citation: None, memory_citation: None,
delivery: None,
}], }],
status: AppServerTurnStatus::Completed, status: AppServerTurnStatus::Completed,
error: None, error: None,
@@ -907,6 +908,7 @@ async fn plan_implementation_popup_shows_once_when_replay_precedes_live_turn_com
text: "Plan details".to_string(), text: "Plan details".to_string(),
phase: Some(MessagePhase::FinalAnswer), phase: Some(MessagePhase::FinalAnswer),
memory_citation: None, memory_citation: None,
delivery: None,
}], }],
status: AppServerTurnStatus::Completed, status: AppServerTurnStatus::Completed,
error: None, error: None,
+1
View File
@@ -6332,6 +6332,7 @@ session_picker_view = "dense"
text: String::from("hello from assistant"), text: String::from("hello from assistant"),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}, },
ThreadItem::Plan { ThreadItem::Plan {
id: String::from("plan-1"), id: String::from("plan-1"),
@@ -41,6 +41,7 @@ fn rollout_agent_message(text: &str) -> RolloutItem {
message: text.to_string(), message: text.to_string(),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
})) }))
} }
@@ -258,6 +259,7 @@ fn transcript_preview_reverse_scan_stops_before_older_items() {
text: String::from("one\ntwo\nthree\nfour\nfive\nsix"), text: String::from("one\ntwo\nthree\nfour\nfive\nsix"),
phase: None, phase: None,
memory_citation: None, memory_citation: None,
delivery: None,
}; };
let mut lines = Vec::new(); let mut lines = Vec::new();