mirror of
https://github.com/openai/codex.git
synced 2026-10-18 17:18:52 +00:00
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:
@@ -1027,6 +1027,7 @@ mod tests {
|
||||
text: text.to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -148,6 +148,12 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"AgentMessageDelivery": {
|
||||
"enum": [
|
||||
"async"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AgentMessageDeltaNotification": {
|
||||
"properties": {
|
||||
"delta": {
|
||||
@@ -4194,6 +4200,17 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"delivery": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentMessageDelivery"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
+17
@@ -6568,6 +6568,12 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"AgentMessageDelivery": {
|
||||
"enum": [
|
||||
"async"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AgentMessageDeltaNotification": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
@@ -19799,6 +19805,17 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"delivery": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/AgentMessageDelivery"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
+17
@@ -313,6 +313,12 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"AgentMessageDelivery": {
|
||||
"enum": [
|
||||
"async"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AgentMessageDeltaNotification": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
@@ -17551,6 +17557,17 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"delivery": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentMessageDelivery"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"id": {
|
||||
"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.",
|
||||
"type": "string"
|
||||
},
|
||||
"AgentMessageDelivery": {
|
||||
"enum": [
|
||||
"async"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ByteRange": {
|
||||
"properties": {
|
||||
"end": {
|
||||
@@ -650,6 +656,17 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"delivery": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentMessageDelivery"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"id": {
|
||||
"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.",
|
||||
"type": "string"
|
||||
},
|
||||
"AgentMessageDelivery": {
|
||||
"enum": [
|
||||
"async"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ByteRange": {
|
||||
"properties": {
|
||||
"end": {
|
||||
@@ -650,6 +656,17 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"delivery": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentMessageDelivery"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"id": {
|
||||
"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.",
|
||||
"type": "string"
|
||||
},
|
||||
"AgentMessageDelivery": {
|
||||
"enum": [
|
||||
"async"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ByteRange": {
|
||||
"properties": {
|
||||
"end": {
|
||||
@@ -796,6 +802,17 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"delivery": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentMessageDelivery"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -25,6 +25,12 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AgentMessageDelivery": {
|
||||
"enum": [
|
||||
"async"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AgentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1353,6 +1359,17 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"delivery": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentMessageDelivery"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"id": {
|
||||
"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.",
|
||||
"type": "string"
|
||||
},
|
||||
"AgentMessageDelivery": {
|
||||
"enum": [
|
||||
"async"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AgentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1144,6 +1150,17 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"delivery": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentMessageDelivery"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
+17
@@ -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.",
|
||||
"type": "string"
|
||||
},
|
||||
"AgentMessageDelivery": {
|
||||
"enum": [
|
||||
"async"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AgentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1144,6 +1150,17 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"delivery": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentMessageDelivery"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"id": {
|
||||
"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.",
|
||||
"type": "string"
|
||||
},
|
||||
"AgentMessageDelivery": {
|
||||
"enum": [
|
||||
"async"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AgentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1144,6 +1150,17 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"delivery": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentMessageDelivery"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -25,6 +25,12 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AgentMessageDelivery": {
|
||||
"enum": [
|
||||
"async"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AgentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1353,6 +1359,17 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"delivery": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentMessageDelivery"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"id": {
|
||||
"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.",
|
||||
"type": "string"
|
||||
},
|
||||
"AgentMessageDelivery": {
|
||||
"enum": [
|
||||
"async"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AgentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1144,6 +1150,17 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"delivery": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentMessageDelivery"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -25,6 +25,12 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AgentMessageDelivery": {
|
||||
"enum": [
|
||||
"async"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AgentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1353,6 +1359,17 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"delivery": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentMessageDelivery"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"id": {
|
||||
"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.",
|
||||
"type": "string"
|
||||
},
|
||||
"AgentMessageDelivery": {
|
||||
"enum": [
|
||||
"async"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AgentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1144,6 +1150,17 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"delivery": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentMessageDelivery"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"id": {
|
||||
"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.",
|
||||
"type": "string"
|
||||
},
|
||||
"AgentMessageDelivery": {
|
||||
"enum": [
|
||||
"async"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AgentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1144,6 +1150,17 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"delivery": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentMessageDelivery"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"id": {
|
||||
"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.",
|
||||
"type": "string"
|
||||
},
|
||||
"AgentMessageDelivery": {
|
||||
"enum": [
|
||||
"async"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ByteRange": {
|
||||
"properties": {
|
||||
"end": {
|
||||
@@ -796,6 +802,17 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"delivery": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentMessageDelivery"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"id": {
|
||||
"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.",
|
||||
"type": "string"
|
||||
},
|
||||
"AgentMessageDelivery": {
|
||||
"enum": [
|
||||
"async"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ByteRange": {
|
||||
"properties": {
|
||||
"end": {
|
||||
@@ -796,6 +802,17 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"delivery": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentMessageDelivery"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"id": {
|
||||
"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.",
|
||||
"type": "string"
|
||||
},
|
||||
"AgentMessageDelivery": {
|
||||
"enum": [
|
||||
"async"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ByteRange": {
|
||||
"properties": {
|
||||
"end": {
|
||||
@@ -796,6 +802,17 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"delivery": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentMessageDelivery"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
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 AgentMessageDelivery = "async";
|
||||
@@ -8,6 +8,7 @@ import type { ReasoningEffort } from "../ReasoningEffort";
|
||||
import type { SleepItem } from "../SleepItem";
|
||||
import type { WebSearchItem } from "../WebSearchItem";
|
||||
import type { JsonValue } from "../serde_json/JsonValue";
|
||||
import type { AgentMessageDelivery } from "./AgentMessageDelivery";
|
||||
import type { CollabAgentState } from "./CollabAgentState";
|
||||
import type { CollabAgentTool } from "./CollabAgentTool";
|
||||
import type { CollabAgentToolCallStatus } from "./CollabAgentToolCallStatus";
|
||||
@@ -27,7 +28,7 @@ import type { PatchApplyStatus } from "./PatchApplyStatus";
|
||||
import type { SubAgentActivityKind } from "./SubAgentActivityKind";
|
||||
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.
|
||||
*/
|
||||
|
||||
@@ -14,6 +14,7 @@ export type { AdditionalContextKind } from "./AdditionalContextKind";
|
||||
export type { AdditionalFileSystemPermissions } from "./AdditionalFileSystemPermissions";
|
||||
export type { AdditionalNetworkPermissions } from "./AdditionalNetworkPermissions";
|
||||
export type { AdditionalPermissionProfile } from "./AdditionalPermissionProfile";
|
||||
export type { AgentMessageDelivery } from "./AgentMessageDelivery";
|
||||
export type { AgentMessageDeltaNotification } from "./AgentMessageDeltaNotification";
|
||||
export type { AnalyticsConfig } from "./AnalyticsConfig";
|
||||
export type { AppBranding } from "./AppBranding";
|
||||
|
||||
@@ -28,7 +28,7 @@ use crate::protocol::v2::WebSearchItem;
|
||||
use crate::protocol::v2::web_search_action_from_core;
|
||||
use codex_extension_items::image_generation::ImageGenerationItem;
|
||||
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::AgentReasoningRawContentEvent;
|
||||
use codex_protocol::protocol::AgentStatus;
|
||||
@@ -331,11 +331,7 @@ impl ThreadHistoryBuilder {
|
||||
pub fn handle_event(&mut self, event: &EventMsg) {
|
||||
match event {
|
||||
EventMsg::UserMessage(payload) => self.handle_user_message(payload),
|
||||
EventMsg::AgentMessage(payload) => self.handle_agent_message(
|
||||
payload.message.clone(),
|
||||
payload.phase.clone(),
|
||||
payload.memory_citation.clone().map(Into::into),
|
||||
),
|
||||
EventMsg::AgentMessage(payload) => self.handle_agent_message(payload),
|
||||
EventMsg::AgentReasoning(payload) => self.handle_agent_reasoning(payload),
|
||||
EventMsg::AgentReasoningRawContent(payload) => {
|
||||
self.handle_agent_reasoning_raw_content(payload)
|
||||
@@ -491,22 +487,18 @@ impl ThreadHistoryBuilder {
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_agent_message(
|
||||
&mut self,
|
||||
text: String,
|
||||
phase: Option<MessagePhase>,
|
||||
memory_citation: Option<crate::protocol::v2::MemoryCitation>,
|
||||
) {
|
||||
if text.is_empty() {
|
||||
fn handle_agent_message(&mut self, payload: &AgentMessageEvent) {
|
||||
if payload.message.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let id = self.next_item_id();
|
||||
self.push_item_in_current_turn(ThreadItem::AgentMessage {
|
||||
id,
|
||||
text,
|
||||
phase,
|
||||
memory_citation,
|
||||
text: payload.message.clone(),
|
||||
phase: payload.phase.clone(),
|
||||
memory_citation: payload.memory_citation.clone().map(Into::into),
|
||||
delivery: payload.delivery,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1628,6 +1620,7 @@ impl From<&PendingTurn> for Turn {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::protocol::v2::AgentMessageDelivery;
|
||||
use crate::protocol::v2::CommandExecutionSource;
|
||||
use codex_extension_items::ExtensionItem as CoreExtensionItem;
|
||||
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::WebSearchAction as CoreWebSearchAction;
|
||||
use codex_protocol::parse_command::ParsedCommand;
|
||||
use codex_protocol::protocol::AgentMessageEvent;
|
||||
use codex_protocol::protocol::AgentReasoningEvent;
|
||||
use codex_protocol::protocol::AgentReasoningRawContentEvent;
|
||||
use codex_protocol::protocol::ApplyPatchApprovalRequestEvent;
|
||||
@@ -1693,6 +1685,7 @@ mod tests {
|
||||
message: "Hi there".into(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}),
|
||||
EventMsg::AgentReasoning(AgentReasoningEvent {
|
||||
text: "thinking".into(),
|
||||
@@ -1712,6 +1705,7 @@ mod tests {
|
||||
message: "Reply two".into(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -1750,6 +1744,7 @@ mod tests {
|
||||
text: "Hi there".into(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -1783,6 +1778,7 @@ mod tests {
|
||||
text: "Reply two".into(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -2330,11 +2326,12 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preserves_agent_message_phase_in_history() {
|
||||
fn preserves_agent_message_phase_and_delivery_in_history() {
|
||||
let events = vec![EventMsg::AgentMessage(AgentMessageEvent {
|
||||
message: "Final reply".into(),
|
||||
phase: Some(CoreMessagePhase::FinalAnswer),
|
||||
memory_citation: None,
|
||||
delivery: Some(AgentMessageDelivery::Async),
|
||||
})];
|
||||
|
||||
let items = events
|
||||
@@ -2348,8 +2345,9 @@ mod tests {
|
||||
ThreadItem::AgentMessage {
|
||||
id: "item-1".into(),
|
||||
text: "Final reply".into(),
|
||||
phase: Some(MessagePhase::FinalAnswer),
|
||||
phase: Some(CoreMessagePhase::FinalAnswer),
|
||||
memory_citation: None,
|
||||
delivery: Some(AgentMessageDelivery::Async),
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -2458,6 +2456,7 @@ mod tests {
|
||||
message: "interlude".into(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}),
|
||||
EventMsg::AgentReasoning(AgentReasoningEvent {
|
||||
text: "second summary".into(),
|
||||
@@ -2506,6 +2505,7 @@ mod tests {
|
||||
message: "Working...".into(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}),
|
||||
EventMsg::TurnAborted(TurnAbortedEvent {
|
||||
turn_id: Some("turn-1".into()),
|
||||
@@ -2526,6 +2526,7 @@ mod tests {
|
||||
message: "Second attempt complete.".into(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -2557,6 +2558,7 @@ mod tests {
|
||||
text: "Working...".into(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -2581,6 +2583,7 @@ mod tests {
|
||||
text: "Second attempt complete.".into(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -2600,6 +2603,7 @@ mod tests {
|
||||
message: "A1".into(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}),
|
||||
EventMsg::UserMessage(UserMessageEvent {
|
||||
client_id: None,
|
||||
@@ -2613,6 +2617,7 @@ mod tests {
|
||||
message: "A2".into(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}),
|
||||
EventMsg::ThreadRolledBack(ThreadRolledBackEvent { num_turns: 1 }),
|
||||
EventMsg::UserMessage(UserMessageEvent {
|
||||
@@ -2627,6 +2632,7 @@ mod tests {
|
||||
message: "A3".into(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -2657,6 +2663,7 @@ mod tests {
|
||||
text: "A1".into(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
},
|
||||
]
|
||||
);
|
||||
@@ -2676,6 +2683,7 @@ mod tests {
|
||||
text: "A3".into(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
},
|
||||
]
|
||||
);
|
||||
@@ -2696,6 +2704,7 @@ mod tests {
|
||||
message: "A1".into(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}),
|
||||
EventMsg::UserMessage(UserMessageEvent {
|
||||
client_id: None,
|
||||
@@ -2709,6 +2718,7 @@ mod tests {
|
||||
message: "A2".into(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}),
|
||||
EventMsg::ThreadRolledBack(ThreadRolledBackEvent { num_turns: 99 }),
|
||||
];
|
||||
@@ -3741,6 +3751,7 @@ mod tests {
|
||||
message: "still in b".into(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}),
|
||||
EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: "turn-b".into(),
|
||||
@@ -3917,6 +3928,7 @@ mod tests {
|
||||
message: "still in b".into(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -4180,6 +4192,7 @@ mod tests {
|
||||
message: "done".into(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}),
|
||||
EventMsg::Error(ErrorEvent {
|
||||
message: "rollback failed".into(),
|
||||
|
||||
@@ -122,6 +122,7 @@ fn projects_completed_canonical_turn_items() {
|
||||
}],
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
});
|
||||
|
||||
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::GuardianCommandSource as CoreGuardianCommandSource;
|
||||
use codex_protocol::items::AgentMessageContent as CoreAgentMessageContent;
|
||||
pub use codex_protocol::items::AgentMessageDelivery;
|
||||
use codex_protocol::items::CollabAgentTool as CoreCollabAgentTool;
|
||||
use codex_protocol::items::CollabAgentToolCallStatus as CoreCollabAgentToolCallStatus;
|
||||
use codex_protocol::items::CommandExecutionStatus as CoreCommandExecutionStatus;
|
||||
@@ -250,6 +251,8 @@ pub enum ThreadItem {
|
||||
phase: Option<MessagePhase>,
|
||||
#[serde(default)]
|
||||
memory_citation: Option<MemoryCitation>,
|
||||
#[serde(default)]
|
||||
delivery: Option<AgentMessageDelivery>,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
@@ -833,6 +836,7 @@ impl From<CoreTurnItem> for ThreadItem {
|
||||
text,
|
||||
phase: agent.phase,
|
||||
memory_citation: agent.memory_citation.map(Into::into),
|
||||
delivery: agent.delivery,
|
||||
}
|
||||
}
|
||||
CoreTurnItem::Plan(plan) => ThreadItem::Plan {
|
||||
|
||||
@@ -2942,6 +2942,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
],
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
@@ -2951,6 +2952,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
text: "Hello world".to_string(),
|
||||
phase: 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()],
|
||||
}),
|
||||
delivery: None,
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
@@ -2986,6 +2989,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
}],
|
||||
thread_ids: vec!["rollout-1".to_string()],
|
||||
}),
|
||||
delivery: None,
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
- `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).
|
||||
- `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.
|
||||
|
||||
@@ -2225,6 +2225,7 @@ mod tests {
|
||||
message: "after rollback".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
})),
|
||||
];
|
||||
let stored_thread = StoredThread {
|
||||
@@ -3634,6 +3635,7 @@ mod tests {
|
||||
],
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}),
|
||||
started_at_ms: Some(0),
|
||||
completed_at_ms: 0,
|
||||
@@ -3651,6 +3653,7 @@ mod tests {
|
||||
}],
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}),
|
||||
started_at_ms: Some(0),
|
||||
completed_at_ms: 0,
|
||||
|
||||
@@ -67,15 +67,18 @@ use crate::transport::CHANNEL_CAPACITY;
|
||||
use crate::transport::OutboundConnectionState;
|
||||
use crate::transport::route_outgoing_envelope;
|
||||
use codex_analytics::AppServerRpcTransport;
|
||||
use codex_app_server_protocol::AgentMessageDelivery;
|
||||
use codex_app_server_protocol::ClientNotification;
|
||||
use codex_app_server_protocol::ClientRequest;
|
||||
use codex_app_server_protocol::ConfigWarningNotification;
|
||||
use codex_app_server_protocol::InitializeParams;
|
||||
use codex_app_server_protocol::ItemCompletedNotification;
|
||||
use codex_app_server_protocol::JSONRPCErrorError;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::Result;
|
||||
use codex_app_server_protocol::ServerNotification;
|
||||
use codex_app_server_protocol::ServerRequest;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_arg0::Arg0DispatchPaths;
|
||||
use codex_config::CloudConfigBundleLoader;
|
||||
use codex_config::LoaderOverrides;
|
||||
@@ -111,6 +114,13 @@ fn server_notification_requires_delivery(notification: &ServerNotification) -> b
|
||||
| ServerNotification::ThreadQueueChanged(_)
|
||||
| ServerNotification::ThreadSettingsUpdated(_)
|
||||
| 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(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
},
|
||||
ThreadItem::McpToolCall {
|
||||
id: "mcp-1".to_string(),
|
||||
@@ -122,6 +123,7 @@ mod tests {
|
||||
text: "kept".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
|
||||
@@ -177,6 +177,7 @@ mod tests {
|
||||
message: "first answer".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
})),
|
||||
RolloutItem::EventMsg(EventMsg::TokenCount(TokenCountEvent {
|
||||
info: None,
|
||||
|
||||
@@ -2041,6 +2041,7 @@ async fn external_agent_config_import_creates_session_rollouts() -> Result<()> {
|
||||
text: "<EXTERNAL SESSION IMPORTED>".into(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -537,6 +537,7 @@ async fn thread_search_occurrences_reads_paginated_projection() -> Result<()> {
|
||||
}],
|
||||
phase: Some(MessagePhase::Commentary),
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}),
|
||||
),
|
||||
paginated_completed_item(
|
||||
@@ -549,6 +550,7 @@ async fn thread_search_occurrences_reads_paginated_projection() -> Result<()> {
|
||||
}],
|
||||
phase: Some(MessagePhase::FinalAnswer),
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}),
|
||||
),
|
||||
paginated_turn_completed("turn-1"),
|
||||
@@ -1577,6 +1579,7 @@ async fn paginated_history_lists_and_legacy_reads_use_projected_turns_and_items(
|
||||
}],
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}),
|
||||
),
|
||||
paginated_completed_item(
|
||||
@@ -1628,6 +1631,7 @@ async fn paginated_history_lists_and_legacy_reads_use_projected_turns_and_items(
|
||||
text: "first".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
},
|
||||
],
|
||||
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(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
},
|
||||
],
|
||||
items_view: TurnItemsView::Summary,
|
||||
@@ -2058,6 +2063,7 @@ fn append_agent_message(path: &Path, timestamp: &str, text: &str) -> anyhow::Res
|
||||
message: text.to_string(),
|
||||
phase: 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(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}))?,
|
||||
})
|
||||
.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(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}))?,
|
||||
})
|
||||
.to_string(),
|
||||
@@ -3697,6 +3699,7 @@ async fn thread_resume_and_read_interrupt_incomplete_rollout_turn_when_thread_is
|
||||
message: "Still running".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}))?,
|
||||
})
|
||||
.to_string(),
|
||||
|
||||
@@ -171,6 +171,7 @@ fn parse_agent_message(
|
||||
content,
|
||||
phase,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2020,6 +2020,7 @@ async fn emit_agent_message_in_plan_mode(
|
||||
content: Vec::new(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
})
|
||||
});
|
||||
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(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
})),
|
||||
]);
|
||||
|
||||
|
||||
@@ -65,6 +65,7 @@ async fn turn_timing_state_records_ttfm_independently_of_ttft() {
|
||||
content: Vec::new(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}))
|
||||
.await
|
||||
.is_some()
|
||||
@@ -76,6 +77,7 @@ async fn turn_timing_state_records_ttfm_independently_of_ttft() {
|
||||
content: Vec::new(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}))
|
||||
.await,
|
||||
None
|
||||
|
||||
@@ -250,6 +250,7 @@ fn final_message_from_turn_items_uses_latest_agent_message() {
|
||||
text: "first".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
},
|
||||
ThreadItem::Plan {
|
||||
id: "plan-1".to_string(),
|
||||
@@ -260,6 +261,7 @@ fn final_message_from_turn_items_uses_latest_agent_message() {
|
||||
text: "second".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -318,6 +320,7 @@ fn turn_completed_recovers_final_message_from_turn_items() {
|
||||
text: "final answer".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}],
|
||||
status: TurnStatus::Completed,
|
||||
error: None,
|
||||
@@ -366,6 +369,7 @@ fn turn_completed_overwrites_stale_final_message_from_turn_items() {
|
||||
text: "final answer".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}],
|
||||
status: TurnStatus::Completed,
|
||||
error: None,
|
||||
|
||||
@@ -18,6 +18,7 @@ fn failed_turn_does_not_overwrite_output_last_message_file() {
|
||||
text: "partial answer".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
},
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
|
||||
@@ -363,6 +363,7 @@ fn turn_items_for_thread_returns_matching_turn_items() {
|
||||
text: "hello".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}],
|
||||
status: codex_app_server_protocol::TurnStatus::Completed,
|
||||
error: None,
|
||||
@@ -393,6 +394,7 @@ fn turn_items_for_thread_returns_matching_turn_items() {
|
||||
text: "hello".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: 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(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
},
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
@@ -926,6 +927,7 @@ fn agent_message_item_updates_final_message() {
|
||||
text: "hello".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
},
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
@@ -961,6 +963,7 @@ fn agent_message_item_started_is_ignored() {
|
||||
text: "hello".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
},
|
||||
thread_id: "thread-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(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}],
|
||||
status: TurnStatus::Completed,
|
||||
error: None,
|
||||
@@ -1438,6 +1442,7 @@ fn turn_completion_overwrites_stale_final_message_from_turn_items() {
|
||||
text: "stale answer".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
},
|
||||
thread_id: "thread-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(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}],
|
||||
status: TurnStatus::Completed,
|
||||
error: None,
|
||||
@@ -1488,6 +1494,7 @@ fn turn_completion_preserves_streamed_final_message_when_turn_items_are_empty()
|
||||
text: "streamed answer".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
},
|
||||
thread_id: "thread-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(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
},
|
||||
thread_id: "thread-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(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
},
|
||||
)));
|
||||
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(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -251,6 +253,7 @@ mod tests {
|
||||
text: EXTERNAL_SESSION_IMPORTED_MARKER.into(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -283,6 +286,7 @@ mod tests {
|
||||
text: EXTERNAL_SESSION_IMPORTED_MARKER.into(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
})
|
||||
);
|
||||
let last_turn_complete = imported
|
||||
|
||||
@@ -113,6 +113,14 @@ pub enum AgentMessageContent {
|
||||
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)]
|
||||
/// Assistant-authored message payload used in turn-item streams.
|
||||
///
|
||||
@@ -132,6 +140,9 @@ pub struct AgentMessageItem {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
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)]
|
||||
|
||||
@@ -105,6 +105,7 @@ impl AgentMessageItem {
|
||||
message: text.clone(),
|
||||
phase: self.phase.clone(),
|
||||
memory_citation: self.memory_citation.clone(),
|
||||
delivery: self.delivery,
|
||||
}),
|
||||
})
|
||||
.collect()
|
||||
|
||||
@@ -32,6 +32,7 @@ use crate::dynamic_tools::DynamicToolCallRequest;
|
||||
use crate::dynamic_tools::DynamicToolResponse;
|
||||
use crate::dynamic_tools::DynamicToolSpec;
|
||||
use crate::error::Result as CodexResult;
|
||||
use crate::items::AgentMessageDelivery;
|
||||
use crate::items::TurnItem;
|
||||
use crate::mcp::CallToolResult;
|
||||
use crate::mcp::RequestId;
|
||||
@@ -2336,6 +2337,9 @@ pub struct AgentMessageEvent {
|
||||
pub phase: Option<MessagePhase>,
|
||||
#[serde(default)]
|
||||
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)]
|
||||
|
||||
@@ -65,6 +65,7 @@ fn agent_message_item(message: &str) -> RolloutItem {
|
||||
message: message.to_string(),
|
||||
phase: 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(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
},
|
||||
))])
|
||||
.await?;
|
||||
@@ -891,6 +893,7 @@ async fn persist_reports_filesystem_error_and_retries_buffered_items() -> std::i
|
||||
message: "buffered-before-persist".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
},
|
||||
))])
|
||||
.await?;
|
||||
@@ -942,6 +945,7 @@ async fn writer_state_retries_write_error_before_reporting_flush_success() -> st
|
||||
message: "queued-after-writer-error".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
},
|
||||
))]);
|
||||
|
||||
|
||||
@@ -1025,6 +1025,7 @@ mod tests {
|
||||
message: "commentary".to_string(),
|
||||
phase: Some(MessagePhase::Commentary),
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
})),
|
||||
RolloutItem::ResponseItem(
|
||||
ResponseItem::FunctionCallOutput {
|
||||
|
||||
@@ -94,6 +94,7 @@ pub(super) fn completed_item(
|
||||
}],
|
||||
phase: event.phase.clone(),
|
||||
memory_citation: event.memory_citation.clone(),
|
||||
delivery: event.delivery,
|
||||
}),
|
||||
None,
|
||||
)),
|
||||
|
||||
@@ -156,6 +156,7 @@ fn agent_message(text: &str) -> RolloutItem {
|
||||
message: text.to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -226,6 +226,7 @@ async fn split_homes_support_backfill_listing_and_paginated_history() {
|
||||
}],
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}),
|
||||
),
|
||||
turn_completed("turn-1"),
|
||||
@@ -318,6 +319,7 @@ async fn paginated_live_append_materializes_turn_items_and_state() {
|
||||
}],
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}),
|
||||
),
|
||||
turn_completed("turn-1"),
|
||||
@@ -2343,6 +2345,7 @@ fn agent_message(id: &str, phase: MessagePhase) -> TurnItem {
|
||||
}],
|
||||
phase: Some(phase),
|
||||
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(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
},
|
||||
thread_id: "thread-child".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
|
||||
@@ -3497,6 +3497,7 @@ async fn active_thread_file_change_approval_recovers_buffered_changes() {
|
||||
text: "streaming".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
},
|
||||
}),
|
||||
/*replay_kind*/ None,
|
||||
@@ -7235,6 +7236,7 @@ async fn replay_thread_snapshot_replays_turn_history_in_order() {
|
||||
text: "done".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
},
|
||||
],
|
||||
status: TurnStatus::Completed,
|
||||
|
||||
@@ -366,6 +366,7 @@ async fn older_pagination_reconciles_review_prompts_across_page_boundaries() ->
|
||||
}],
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
})
|
||||
}));
|
||||
items.extend([
|
||||
@@ -554,6 +555,7 @@ async fn transcript_home_loads_every_older_history_page() -> Result<()> {
|
||||
}],
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}),
|
||||
started_at_ms: None,
|
||||
completed_at_ms: 0,
|
||||
@@ -919,6 +921,7 @@ async fn underfilled_scrollback_fetches_older_pages_without_opening_the_transcri
|
||||
}],
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}),
|
||||
started_at_ms: None,
|
||||
completed_at_ms: 0,
|
||||
|
||||
@@ -3360,6 +3360,7 @@ mod tests {
|
||||
text: "assistant reply".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
},
|
||||
],
|
||||
status: TurnStatus::Completed,
|
||||
|
||||
@@ -94,6 +94,8 @@ impl ChatWidget {
|
||||
text,
|
||||
phase,
|
||||
memory_citation,
|
||||
delivery,
|
||||
..
|
||||
} => {
|
||||
self.on_agent_message_item_completed(
|
||||
AgentMessageItem {
|
||||
@@ -117,6 +119,7 @@ impl ChatWidget {
|
||||
rollout_ids: citation.thread_ids,
|
||||
}
|
||||
}),
|
||||
delivery,
|
||||
},
|
||||
&turn_id,
|
||||
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(),
|
||||
phase: Some(MessagePhase::FinalAnswer),
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
};
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::ItemCompleted(ItemCompletedNotification {
|
||||
@@ -1236,6 +1237,7 @@ async fn live_app_server_turn_completion_repairs_dropped_message_deltas() {
|
||||
.to_string(),
|
||||
phase: Some(MessagePhase::FinalAnswer),
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}];
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::TurnCompleted(TurnCompletedNotification {
|
||||
|
||||
@@ -767,6 +767,7 @@ pub(super) fn replay_agent_message(
|
||||
text: text.into(),
|
||||
phase: Some(MessagePhase::FinalAnswer),
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
},
|
||||
"turn-1".to_string(),
|
||||
replay_kind,
|
||||
@@ -919,6 +920,7 @@ pub(super) fn complete_assistant_message(
|
||||
text: text.to_string(),
|
||||
phase,
|
||||
memory_citation: None,
|
||||
delivery: 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(),
|
||||
phase: Some(MessagePhase::FinalAnswer),
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
},
|
||||
],
|
||||
..app_server_turn(
|
||||
|
||||
@@ -869,6 +869,7 @@ async fn plan_implementation_popup_skips_replayed_turn_complete() {
|
||||
text: "Plan details".to_string(),
|
||||
phase: Some(MessagePhase::FinalAnswer),
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}],
|
||||
status: AppServerTurnStatus::Completed,
|
||||
error: None,
|
||||
@@ -907,6 +908,7 @@ async fn plan_implementation_popup_shows_once_when_replay_precedes_live_turn_com
|
||||
text: "Plan details".to_string(),
|
||||
phase: Some(MessagePhase::FinalAnswer),
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}],
|
||||
status: AppServerTurnStatus::Completed,
|
||||
error: None,
|
||||
|
||||
@@ -6332,6 +6332,7 @@ session_picker_view = "dense"
|
||||
text: String::from("hello from assistant"),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
},
|
||||
ThreadItem::Plan {
|
||||
id: String::from("plan-1"),
|
||||
|
||||
@@ -41,6 +41,7 @@ fn rollout_agent_message(text: &str) -> RolloutItem {
|
||||
message: text.to_string(),
|
||||
phase: 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"),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
};
|
||||
let mut lines = Vec::new();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user