diff --git a/codex-rs/app-server-protocol/schema/json/EventMsg.json b/codex-rs/app-server-protocol/schema/json/EventMsg.json index f399912e03b..f39b3bf39a0 100644 --- a/codex-rs/app-server-protocol/schema/json/EventMsg.json +++ b/codex-rs/app-server-protocol/schema/json/EventMsg.json @@ -2775,6 +2775,95 @@ ], "title": "CollabCloseEndEventMsg", "type": "object" + }, + { + "description": "Collab interaction: resume begin.", + "properties": { + "call_id": { + "description": "Identifier for the collab tool call.", + "type": "string" + }, + "receiver_thread_id": { + "allOf": [ + { + "$ref": "#/definitions/ThreadId" + } + ], + "description": "Thread ID of the receiver." + }, + "sender_thread_id": { + "allOf": [ + { + "$ref": "#/definitions/ThreadId" + } + ], + "description": "Thread ID of the sender." + }, + "type": { + "enum": [ + "collab_resume_begin" + ], + "title": "CollabResumeBeginEventMsgType", + "type": "string" + } + }, + "required": [ + "call_id", + "receiver_thread_id", + "sender_thread_id", + "type" + ], + "title": "CollabResumeBeginEventMsg", + "type": "object" + }, + { + "description": "Collab interaction: resume end.", + "properties": { + "call_id": { + "description": "Identifier for the collab tool call.", + "type": "string" + }, + "receiver_thread_id": { + "allOf": [ + { + "$ref": "#/definitions/ThreadId" + } + ], + "description": "Thread ID of the receiver." + }, + "sender_thread_id": { + "allOf": [ + { + "$ref": "#/definitions/ThreadId" + } + ], + "description": "Thread ID of the sender." + }, + "status": { + "allOf": [ + { + "$ref": "#/definitions/AgentStatus" + } + ], + "description": "Last known status of the receiver agent reported to the sender agent after resume." + }, + "type": { + "enum": [ + "collab_resume_end" + ], + "title": "CollabResumeEndEventMsgType", + "type": "string" + } + }, + "required": [ + "call_id", + "receiver_thread_id", + "sender_thread_id", + "status", + "type" + ], + "title": "CollabResumeEndEventMsg", + "type": "object" } ] }, @@ -7375,6 +7464,95 @@ ], "title": "CollabCloseEndEventMsg", "type": "object" + }, + { + "description": "Collab interaction: resume begin.", + "properties": { + "call_id": { + "description": "Identifier for the collab tool call.", + "type": "string" + }, + "receiver_thread_id": { + "allOf": [ + { + "$ref": "#/definitions/ThreadId" + } + ], + "description": "Thread ID of the receiver." + }, + "sender_thread_id": { + "allOf": [ + { + "$ref": "#/definitions/ThreadId" + } + ], + "description": "Thread ID of the sender." + }, + "type": { + "enum": [ + "collab_resume_begin" + ], + "title": "CollabResumeBeginEventMsgType", + "type": "string" + } + }, + "required": [ + "call_id", + "receiver_thread_id", + "sender_thread_id", + "type" + ], + "title": "CollabResumeBeginEventMsg", + "type": "object" + }, + { + "description": "Collab interaction: resume end.", + "properties": { + "call_id": { + "description": "Identifier for the collab tool call.", + "type": "string" + }, + "receiver_thread_id": { + "allOf": [ + { + "$ref": "#/definitions/ThreadId" + } + ], + "description": "Thread ID of the receiver." + }, + "sender_thread_id": { + "allOf": [ + { + "$ref": "#/definitions/ThreadId" + } + ], + "description": "Thread ID of the sender." + }, + "status": { + "allOf": [ + { + "$ref": "#/definitions/AgentStatus" + } + ], + "description": "Last known status of the receiver agent reported to the sender agent after resume." + }, + "type": { + "enum": [ + "collab_resume_end" + ], + "title": "CollabResumeEndEventMsgType", + "type": "string" + } + }, + "required": [ + "call_id", + "receiver_thread_id", + "sender_thread_id", + "status", + "type" + ], + "title": "CollabResumeEndEventMsg", + "type": "object" } ], "title": "EventMsg" diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index 0e2f94f4d8e..a1d56d492fe 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -617,6 +617,7 @@ "enum": [ "spawnAgent", "sendInput", + "resumeAgent", "wait", "closeAgent" ], @@ -3353,6 +3354,95 @@ ], "title": "CollabCloseEndEventMsg", "type": "object" + }, + { + "description": "Collab interaction: resume begin.", + "properties": { + "call_id": { + "description": "Identifier for the collab tool call.", + "type": "string" + }, + "receiver_thread_id": { + "allOf": [ + { + "$ref": "#/definitions/ThreadId" + } + ], + "description": "Thread ID of the receiver." + }, + "sender_thread_id": { + "allOf": [ + { + "$ref": "#/definitions/ThreadId" + } + ], + "description": "Thread ID of the sender." + }, + "type": { + "enum": [ + "collab_resume_begin" + ], + "title": "CollabResumeBeginEventMsgType", + "type": "string" + } + }, + "required": [ + "call_id", + "receiver_thread_id", + "sender_thread_id", + "type" + ], + "title": "CollabResumeBeginEventMsg", + "type": "object" + }, + { + "description": "Collab interaction: resume end.", + "properties": { + "call_id": { + "description": "Identifier for the collab tool call.", + "type": "string" + }, + "receiver_thread_id": { + "allOf": [ + { + "$ref": "#/definitions/ThreadId" + } + ], + "description": "Thread ID of the receiver." + }, + "sender_thread_id": { + "allOf": [ + { + "$ref": "#/definitions/ThreadId" + } + ], + "description": "Thread ID of the sender." + }, + "status": { + "allOf": [ + { + "$ref": "#/definitions/AgentStatus" + } + ], + "description": "Last known status of the receiver agent reported to the sender agent after resume." + }, + "type": { + "enum": [ + "collab_resume_end" + ], + "title": "CollabResumeEndEventMsgType", + "type": "string" + } + }, + "required": [ + "call_id", + "receiver_thread_id", + "sender_thread_id", + "status", + "type" + ], + "title": "CollabResumeEndEventMsg", + "type": "object" } ] }, diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 3b6e61fc234..a1ad810b66e 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -4782,6 +4782,95 @@ ], "title": "CollabCloseEndEventMsg", "type": "object" + }, + { + "description": "Collab interaction: resume begin.", + "properties": { + "call_id": { + "description": "Identifier for the collab tool call.", + "type": "string" + }, + "receiver_thread_id": { + "allOf": [ + { + "$ref": "#/definitions/v2/ThreadId" + } + ], + "description": "Thread ID of the receiver." + }, + "sender_thread_id": { + "allOf": [ + { + "$ref": "#/definitions/v2/ThreadId" + } + ], + "description": "Thread ID of the sender." + }, + "type": { + "enum": [ + "collab_resume_begin" + ], + "title": "CollabResumeBeginEventMsgType", + "type": "string" + } + }, + "required": [ + "call_id", + "receiver_thread_id", + "sender_thread_id", + "type" + ], + "title": "CollabResumeBeginEventMsg", + "type": "object" + }, + { + "description": "Collab interaction: resume end.", + "properties": { + "call_id": { + "description": "Identifier for the collab tool call.", + "type": "string" + }, + "receiver_thread_id": { + "allOf": [ + { + "$ref": "#/definitions/v2/ThreadId" + } + ], + "description": "Thread ID of the receiver." + }, + "sender_thread_id": { + "allOf": [ + { + "$ref": "#/definitions/v2/ThreadId" + } + ], + "description": "Thread ID of the sender." + }, + "status": { + "allOf": [ + { + "$ref": "#/definitions/AgentStatus" + } + ], + "description": "Last known status of the receiver agent reported to the sender agent after resume." + }, + "type": { + "enum": [ + "collab_resume_end" + ], + "title": "CollabResumeEndEventMsgType", + "type": "string" + } + }, + "required": [ + "call_id", + "receiver_thread_id", + "sender_thread_id", + "status", + "type" + ], + "title": "CollabResumeEndEventMsg", + "type": "object" } ], "title": "EventMsg" @@ -10181,6 +10270,7 @@ "enum": [ "spawnAgent", "sendInput", + "resumeAgent", "wait", "closeAgent" ], diff --git a/codex-rs/app-server-protocol/schema/json/v1/ForkConversationResponse.json b/codex-rs/app-server-protocol/schema/json/v1/ForkConversationResponse.json index a5838e89e7a..dbd854af29b 100644 --- a/codex-rs/app-server-protocol/schema/json/v1/ForkConversationResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v1/ForkConversationResponse.json @@ -2775,6 +2775,95 @@ ], "title": "CollabCloseEndEventMsg", "type": "object" + }, + { + "description": "Collab interaction: resume begin.", + "properties": { + "call_id": { + "description": "Identifier for the collab tool call.", + "type": "string" + }, + "receiver_thread_id": { + "allOf": [ + { + "$ref": "#/definitions/ThreadId" + } + ], + "description": "Thread ID of the receiver." + }, + "sender_thread_id": { + "allOf": [ + { + "$ref": "#/definitions/ThreadId" + } + ], + "description": "Thread ID of the sender." + }, + "type": { + "enum": [ + "collab_resume_begin" + ], + "title": "CollabResumeBeginEventMsgType", + "type": "string" + } + }, + "required": [ + "call_id", + "receiver_thread_id", + "sender_thread_id", + "type" + ], + "title": "CollabResumeBeginEventMsg", + "type": "object" + }, + { + "description": "Collab interaction: resume end.", + "properties": { + "call_id": { + "description": "Identifier for the collab tool call.", + "type": "string" + }, + "receiver_thread_id": { + "allOf": [ + { + "$ref": "#/definitions/ThreadId" + } + ], + "description": "Thread ID of the receiver." + }, + "sender_thread_id": { + "allOf": [ + { + "$ref": "#/definitions/ThreadId" + } + ], + "description": "Thread ID of the sender." + }, + "status": { + "allOf": [ + { + "$ref": "#/definitions/AgentStatus" + } + ], + "description": "Last known status of the receiver agent reported to the sender agent after resume." + }, + "type": { + "enum": [ + "collab_resume_end" + ], + "title": "CollabResumeEndEventMsgType", + "type": "string" + } + }, + "required": [ + "call_id", + "receiver_thread_id", + "sender_thread_id", + "status", + "type" + ], + "title": "CollabResumeEndEventMsg", + "type": "object" } ] }, diff --git a/codex-rs/app-server-protocol/schema/json/v1/ResumeConversationResponse.json b/codex-rs/app-server-protocol/schema/json/v1/ResumeConversationResponse.json index 718b17aa289..0f274eb7e59 100644 --- a/codex-rs/app-server-protocol/schema/json/v1/ResumeConversationResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v1/ResumeConversationResponse.json @@ -2775,6 +2775,95 @@ ], "title": "CollabCloseEndEventMsg", "type": "object" + }, + { + "description": "Collab interaction: resume begin.", + "properties": { + "call_id": { + "description": "Identifier for the collab tool call.", + "type": "string" + }, + "receiver_thread_id": { + "allOf": [ + { + "$ref": "#/definitions/ThreadId" + } + ], + "description": "Thread ID of the receiver." + }, + "sender_thread_id": { + "allOf": [ + { + "$ref": "#/definitions/ThreadId" + } + ], + "description": "Thread ID of the sender." + }, + "type": { + "enum": [ + "collab_resume_begin" + ], + "title": "CollabResumeBeginEventMsgType", + "type": "string" + } + }, + "required": [ + "call_id", + "receiver_thread_id", + "sender_thread_id", + "type" + ], + "title": "CollabResumeBeginEventMsg", + "type": "object" + }, + { + "description": "Collab interaction: resume end.", + "properties": { + "call_id": { + "description": "Identifier for the collab tool call.", + "type": "string" + }, + "receiver_thread_id": { + "allOf": [ + { + "$ref": "#/definitions/ThreadId" + } + ], + "description": "Thread ID of the receiver." + }, + "sender_thread_id": { + "allOf": [ + { + "$ref": "#/definitions/ThreadId" + } + ], + "description": "Thread ID of the sender." + }, + "status": { + "allOf": [ + { + "$ref": "#/definitions/AgentStatus" + } + ], + "description": "Last known status of the receiver agent reported to the sender agent after resume." + }, + "type": { + "enum": [ + "collab_resume_end" + ], + "title": "CollabResumeEndEventMsgType", + "type": "string" + } + }, + "required": [ + "call_id", + "receiver_thread_id", + "sender_thread_id", + "status", + "type" + ], + "title": "CollabResumeEndEventMsg", + "type": "object" } ] }, diff --git a/codex-rs/app-server-protocol/schema/json/v1/SessionConfiguredNotification.json b/codex-rs/app-server-protocol/schema/json/v1/SessionConfiguredNotification.json index a85b78281b4..87102cf8927 100644 --- a/codex-rs/app-server-protocol/schema/json/v1/SessionConfiguredNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v1/SessionConfiguredNotification.json @@ -2775,6 +2775,95 @@ ], "title": "CollabCloseEndEventMsg", "type": "object" + }, + { + "description": "Collab interaction: resume begin.", + "properties": { + "call_id": { + "description": "Identifier for the collab tool call.", + "type": "string" + }, + "receiver_thread_id": { + "allOf": [ + { + "$ref": "#/definitions/ThreadId" + } + ], + "description": "Thread ID of the receiver." + }, + "sender_thread_id": { + "allOf": [ + { + "$ref": "#/definitions/ThreadId" + } + ], + "description": "Thread ID of the sender." + }, + "type": { + "enum": [ + "collab_resume_begin" + ], + "title": "CollabResumeBeginEventMsgType", + "type": "string" + } + }, + "required": [ + "call_id", + "receiver_thread_id", + "sender_thread_id", + "type" + ], + "title": "CollabResumeBeginEventMsg", + "type": "object" + }, + { + "description": "Collab interaction: resume end.", + "properties": { + "call_id": { + "description": "Identifier for the collab tool call.", + "type": "string" + }, + "receiver_thread_id": { + "allOf": [ + { + "$ref": "#/definitions/ThreadId" + } + ], + "description": "Thread ID of the receiver." + }, + "sender_thread_id": { + "allOf": [ + { + "$ref": "#/definitions/ThreadId" + } + ], + "description": "Thread ID of the sender." + }, + "status": { + "allOf": [ + { + "$ref": "#/definitions/AgentStatus" + } + ], + "description": "Last known status of the receiver agent reported to the sender agent after resume." + }, + "type": { + "enum": [ + "collab_resume_end" + ], + "title": "CollabResumeEndEventMsgType", + "type": "string" + } + }, + "required": [ + "call_id", + "receiver_thread_id", + "sender_thread_id", + "status", + "type" + ], + "title": "CollabResumeEndEventMsg", + "type": "object" } ] }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json index 56ef8132310..7fc1dab9b61 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json @@ -52,6 +52,7 @@ "enum": [ "spawnAgent", "sendInput", + "resumeAgent", "wait", "closeAgent" ], diff --git a/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json index 625c99af935..5ed713e580d 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json @@ -52,6 +52,7 @@ "enum": [ "spawnAgent", "sendInput", + "resumeAgent", "wait", "closeAgent" ], diff --git a/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json index dfbd76a20b5..3187f75f771 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json @@ -194,6 +194,7 @@ "enum": [ "spawnAgent", "sendInput", + "resumeAgent", "wait", "closeAgent" ], diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json index 61b12ceff3a..094db44d730 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json @@ -207,6 +207,7 @@ "enum": [ "spawnAgent", "sendInput", + "resumeAgent", "wait", "closeAgent" ], diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json index 50c3d60e370..dcac91e0ddf 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json @@ -194,6 +194,7 @@ "enum": [ "spawnAgent", "sendInput", + "resumeAgent", "wait", "closeAgent" ], diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json index c4fa3b2028f..5760be15cbc 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json @@ -194,6 +194,7 @@ "enum": [ "spawnAgent", "sendInput", + "resumeAgent", "wait", "closeAgent" ], diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json index 0534f6e16e3..901fc829c32 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json @@ -207,6 +207,7 @@ "enum": [ "spawnAgent", "sendInput", + "resumeAgent", "wait", "closeAgent" ], diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json index 148bbe7d7d0..6c6f2d1effd 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json @@ -194,6 +194,7 @@ "enum": [ "spawnAgent", "sendInput", + "resumeAgent", "wait", "closeAgent" ], diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json index c85d7ce97ac..97599464870 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json @@ -207,6 +207,7 @@ "enum": [ "spawnAgent", "sendInput", + "resumeAgent", "wait", "closeAgent" ], diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json index 44aca775c18..86d1e2940bd 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json @@ -194,6 +194,7 @@ "enum": [ "spawnAgent", "sendInput", + "resumeAgent", "wait", "closeAgent" ], diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json index 055c3fd4a97..86f9f659258 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json @@ -194,6 +194,7 @@ "enum": [ "spawnAgent", "sendInput", + "resumeAgent", "wait", "closeAgent" ], diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json index 798caa59959..99d1f569c19 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json @@ -194,6 +194,7 @@ "enum": [ "spawnAgent", "sendInput", + "resumeAgent", "wait", "closeAgent" ], diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json index 65b2a66be04..19bf47db878 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json @@ -194,6 +194,7 @@ "enum": [ "spawnAgent", "sendInput", + "resumeAgent", "wait", "closeAgent" ], diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json index 1a63c0d7d02..c70f6f7cdfc 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json @@ -194,6 +194,7 @@ "enum": [ "spawnAgent", "sendInput", + "resumeAgent", "wait", "closeAgent" ], diff --git a/codex-rs/app-server-protocol/schema/typescript/CollabResumeBeginEvent.ts b/codex-rs/app-server-protocol/schema/typescript/CollabResumeBeginEvent.ts new file mode 100644 index 00000000000..b7036667232 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/CollabResumeBeginEvent.ts @@ -0,0 +1,18 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ThreadId } from "./ThreadId"; + +export type CollabResumeBeginEvent = { +/** + * Identifier for the collab tool call. + */ +call_id: string, +/** + * Thread ID of the sender. + */ +sender_thread_id: ThreadId, +/** + * Thread ID of the receiver. + */ +receiver_thread_id: ThreadId, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/CollabResumeEndEvent.ts b/codex-rs/app-server-protocol/schema/typescript/CollabResumeEndEvent.ts new file mode 100644 index 00000000000..9eed3103600 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/CollabResumeEndEvent.ts @@ -0,0 +1,24 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AgentStatus } from "./AgentStatus"; +import type { ThreadId } from "./ThreadId"; + +export type CollabResumeEndEvent = { +/** + * Identifier for the collab tool call. + */ +call_id: string, +/** + * Thread ID of the sender. + */ +sender_thread_id: ThreadId, +/** + * Thread ID of the receiver. + */ +receiver_thread_id: ThreadId, +/** + * Last known status of the receiver agent reported to the sender agent after + * resume. + */ +status: AgentStatus, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/EventMsg.ts b/codex-rs/app-server-protocol/schema/typescript/EventMsg.ts index c18088eaaf8..2c0514870f2 100644 --- a/codex-rs/app-server-protocol/schema/typescript/EventMsg.ts +++ b/codex-rs/app-server-protocol/schema/typescript/EventMsg.ts @@ -17,6 +17,8 @@ import type { CollabAgentSpawnBeginEvent } from "./CollabAgentSpawnBeginEvent"; import type { CollabAgentSpawnEndEvent } from "./CollabAgentSpawnEndEvent"; import type { CollabCloseBeginEvent } from "./CollabCloseBeginEvent"; import type { CollabCloseEndEvent } from "./CollabCloseEndEvent"; +import type { CollabResumeBeginEvent } from "./CollabResumeBeginEvent"; +import type { CollabResumeEndEvent } from "./CollabResumeEndEvent"; import type { CollabWaitingBeginEvent } from "./CollabWaitingBeginEvent"; import type { CollabWaitingEndEvent } from "./CollabWaitingEndEvent"; import type { ContextCompactedEvent } from "./ContextCompactedEvent"; @@ -72,4 +74,4 @@ import type { WebSearchEndEvent } from "./WebSearchEndEvent"; * Response event from the agent * NOTE: Make sure none of these values have optional types, as it will mess up the extension code-gen. */ -export type EventMsg = { "type": "error" } & ErrorEvent | { "type": "warning" } & WarningEvent | { "type": "context_compacted" } & ContextCompactedEvent | { "type": "thread_rolled_back" } & ThreadRolledBackEvent | { "type": "task_started" } & TurnStartedEvent | { "type": "task_complete" } & TurnCompleteEvent | { "type": "token_count" } & TokenCountEvent | { "type": "agent_message" } & AgentMessageEvent | { "type": "user_message" } & UserMessageEvent | { "type": "agent_message_delta" } & AgentMessageDeltaEvent | { "type": "agent_reasoning" } & AgentReasoningEvent | { "type": "agent_reasoning_delta" } & AgentReasoningDeltaEvent | { "type": "agent_reasoning_raw_content" } & AgentReasoningRawContentEvent | { "type": "agent_reasoning_raw_content_delta" } & AgentReasoningRawContentDeltaEvent | { "type": "agent_reasoning_section_break" } & AgentReasoningSectionBreakEvent | { "type": "session_configured" } & SessionConfiguredEvent | { "type": "thread_name_updated" } & ThreadNameUpdatedEvent | { "type": "mcp_startup_update" } & McpStartupUpdateEvent | { "type": "mcp_startup_complete" } & McpStartupCompleteEvent | { "type": "mcp_tool_call_begin" } & McpToolCallBeginEvent | { "type": "mcp_tool_call_end" } & McpToolCallEndEvent | { "type": "web_search_begin" } & WebSearchBeginEvent | { "type": "web_search_end" } & WebSearchEndEvent | { "type": "exec_command_begin" } & ExecCommandBeginEvent | { "type": "exec_command_output_delta" } & ExecCommandOutputDeltaEvent | { "type": "terminal_interaction" } & TerminalInteractionEvent | { "type": "exec_command_end" } & ExecCommandEndEvent | { "type": "view_image_tool_call" } & ViewImageToolCallEvent | { "type": "exec_approval_request" } & ExecApprovalRequestEvent | { "type": "request_user_input" } & RequestUserInputEvent | { "type": "dynamic_tool_call_request" } & DynamicToolCallRequest | { "type": "elicitation_request" } & ElicitationRequestEvent | { "type": "apply_patch_approval_request" } & ApplyPatchApprovalRequestEvent | { "type": "deprecation_notice" } & DeprecationNoticeEvent | { "type": "background_event" } & BackgroundEventEvent | { "type": "undo_started" } & UndoStartedEvent | { "type": "undo_completed" } & UndoCompletedEvent | { "type": "stream_error" } & StreamErrorEvent | { "type": "patch_apply_begin" } & PatchApplyBeginEvent | { "type": "patch_apply_end" } & PatchApplyEndEvent | { "type": "turn_diff" } & TurnDiffEvent | { "type": "get_history_entry_response" } & GetHistoryEntryResponseEvent | { "type": "mcp_list_tools_response" } & McpListToolsResponseEvent | { "type": "list_custom_prompts_response" } & ListCustomPromptsResponseEvent | { "type": "list_skills_response" } & ListSkillsResponseEvent | { "type": "list_remote_skills_response" } & ListRemoteSkillsResponseEvent | { "type": "remote_skill_downloaded" } & RemoteSkillDownloadedEvent | { "type": "skills_update_available" } | { "type": "plan_update" } & UpdatePlanArgs | { "type": "turn_aborted" } & TurnAbortedEvent | { "type": "shutdown_complete" } | { "type": "entered_review_mode" } & ReviewRequest | { "type": "exited_review_mode" } & ExitedReviewModeEvent | { "type": "raw_response_item" } & RawResponseItemEvent | { "type": "item_started" } & ItemStartedEvent | { "type": "item_completed" } & ItemCompletedEvent | { "type": "agent_message_content_delta" } & AgentMessageContentDeltaEvent | { "type": "plan_delta" } & PlanDeltaEvent | { "type": "reasoning_content_delta" } & ReasoningContentDeltaEvent | { "type": "reasoning_raw_content_delta" } & ReasoningRawContentDeltaEvent | { "type": "collab_agent_spawn_begin" } & CollabAgentSpawnBeginEvent | { "type": "collab_agent_spawn_end" } & CollabAgentSpawnEndEvent | { "type": "collab_agent_interaction_begin" } & CollabAgentInteractionBeginEvent | { "type": "collab_agent_interaction_end" } & CollabAgentInteractionEndEvent | { "type": "collab_waiting_begin" } & CollabWaitingBeginEvent | { "type": "collab_waiting_end" } & CollabWaitingEndEvent | { "type": "collab_close_begin" } & CollabCloseBeginEvent | { "type": "collab_close_end" } & CollabCloseEndEvent; +export type EventMsg = { "type": "error" } & ErrorEvent | { "type": "warning" } & WarningEvent | { "type": "context_compacted" } & ContextCompactedEvent | { "type": "thread_rolled_back" } & ThreadRolledBackEvent | { "type": "task_started" } & TurnStartedEvent | { "type": "task_complete" } & TurnCompleteEvent | { "type": "token_count" } & TokenCountEvent | { "type": "agent_message" } & AgentMessageEvent | { "type": "user_message" } & UserMessageEvent | { "type": "agent_message_delta" } & AgentMessageDeltaEvent | { "type": "agent_reasoning" } & AgentReasoningEvent | { "type": "agent_reasoning_delta" } & AgentReasoningDeltaEvent | { "type": "agent_reasoning_raw_content" } & AgentReasoningRawContentEvent | { "type": "agent_reasoning_raw_content_delta" } & AgentReasoningRawContentDeltaEvent | { "type": "agent_reasoning_section_break" } & AgentReasoningSectionBreakEvent | { "type": "session_configured" } & SessionConfiguredEvent | { "type": "thread_name_updated" } & ThreadNameUpdatedEvent | { "type": "mcp_startup_update" } & McpStartupUpdateEvent | { "type": "mcp_startup_complete" } & McpStartupCompleteEvent | { "type": "mcp_tool_call_begin" } & McpToolCallBeginEvent | { "type": "mcp_tool_call_end" } & McpToolCallEndEvent | { "type": "web_search_begin" } & WebSearchBeginEvent | { "type": "web_search_end" } & WebSearchEndEvent | { "type": "exec_command_begin" } & ExecCommandBeginEvent | { "type": "exec_command_output_delta" } & ExecCommandOutputDeltaEvent | { "type": "terminal_interaction" } & TerminalInteractionEvent | { "type": "exec_command_end" } & ExecCommandEndEvent | { "type": "view_image_tool_call" } & ViewImageToolCallEvent | { "type": "exec_approval_request" } & ExecApprovalRequestEvent | { "type": "request_user_input" } & RequestUserInputEvent | { "type": "dynamic_tool_call_request" } & DynamicToolCallRequest | { "type": "elicitation_request" } & ElicitationRequestEvent | { "type": "apply_patch_approval_request" } & ApplyPatchApprovalRequestEvent | { "type": "deprecation_notice" } & DeprecationNoticeEvent | { "type": "background_event" } & BackgroundEventEvent | { "type": "undo_started" } & UndoStartedEvent | { "type": "undo_completed" } & UndoCompletedEvent | { "type": "stream_error" } & StreamErrorEvent | { "type": "patch_apply_begin" } & PatchApplyBeginEvent | { "type": "patch_apply_end" } & PatchApplyEndEvent | { "type": "turn_diff" } & TurnDiffEvent | { "type": "get_history_entry_response" } & GetHistoryEntryResponseEvent | { "type": "mcp_list_tools_response" } & McpListToolsResponseEvent | { "type": "list_custom_prompts_response" } & ListCustomPromptsResponseEvent | { "type": "list_skills_response" } & ListSkillsResponseEvent | { "type": "list_remote_skills_response" } & ListRemoteSkillsResponseEvent | { "type": "remote_skill_downloaded" } & RemoteSkillDownloadedEvent | { "type": "skills_update_available" } | { "type": "plan_update" } & UpdatePlanArgs | { "type": "turn_aborted" } & TurnAbortedEvent | { "type": "shutdown_complete" } | { "type": "entered_review_mode" } & ReviewRequest | { "type": "exited_review_mode" } & ExitedReviewModeEvent | { "type": "raw_response_item" } & RawResponseItemEvent | { "type": "item_started" } & ItemStartedEvent | { "type": "item_completed" } & ItemCompletedEvent | { "type": "agent_message_content_delta" } & AgentMessageContentDeltaEvent | { "type": "plan_delta" } & PlanDeltaEvent | { "type": "reasoning_content_delta" } & ReasoningContentDeltaEvent | { "type": "reasoning_raw_content_delta" } & ReasoningRawContentDeltaEvent | { "type": "collab_agent_spawn_begin" } & CollabAgentSpawnBeginEvent | { "type": "collab_agent_spawn_end" } & CollabAgentSpawnEndEvent | { "type": "collab_agent_interaction_begin" } & CollabAgentInteractionBeginEvent | { "type": "collab_agent_interaction_end" } & CollabAgentInteractionEndEvent | { "type": "collab_waiting_begin" } & CollabWaitingBeginEvent | { "type": "collab_waiting_end" } & CollabWaitingEndEvent | { "type": "collab_close_begin" } & CollabCloseBeginEvent | { "type": "collab_close_end" } & CollabCloseEndEvent | { "type": "collab_resume_begin" } & CollabResumeBeginEvent | { "type": "collab_resume_end" } & CollabResumeEndEvent; diff --git a/codex-rs/app-server-protocol/schema/typescript/index.ts b/codex-rs/app-server-protocol/schema/typescript/index.ts index a6ff6fbaf15..c0c00a0c818 100644 --- a/codex-rs/app-server-protocol/schema/typescript/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/index.ts @@ -37,6 +37,8 @@ export type { CollabAgentSpawnBeginEvent } from "./CollabAgentSpawnBeginEvent"; export type { CollabAgentSpawnEndEvent } from "./CollabAgentSpawnEndEvent"; export type { CollabCloseBeginEvent } from "./CollabCloseBeginEvent"; export type { CollabCloseEndEvent } from "./CollabCloseEndEvent"; +export type { CollabResumeBeginEvent } from "./CollabResumeBeginEvent"; +export type { CollabResumeEndEvent } from "./CollabResumeEndEvent"; export type { CollabWaitingBeginEvent } from "./CollabWaitingBeginEvent"; export type { CollabWaitingEndEvent } from "./CollabWaitingEndEvent"; export type { CollaborationMode } from "./CollaborationMode"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/CollabAgentTool.ts b/codex-rs/app-server-protocol/schema/typescript/v2/CollabAgentTool.ts index 11db4dbf9af..3637853a389 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/CollabAgentTool.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/CollabAgentTool.ts @@ -2,4 +2,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type CollabAgentTool = "spawnAgent" | "sendInput" | "wait" | "closeAgent"; +export type CollabAgentTool = "spawnAgent" | "sendInput" | "resumeAgent" | "wait" | "closeAgent"; diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index a978fec6db7..27753cdc591 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -2456,6 +2456,7 @@ pub enum CommandExecutionStatus { pub enum CollabAgentTool { SpawnAgent, SendInput, + ResumeAgent, Wait, CloseAgent, } diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index a6d49f2e4cd..a2d17635d16 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -497,7 +497,7 @@ Today both notifications carry an empty `items` array even when item events were - `commandExecution` — `{id, command, cwd, status, commandActions, aggregatedOutput?, exitCode?, durationMs?}` for sandboxed commands; `status` is `inProgress`, `completed`, `failed`, or `declined`. - `fileChange` — `{id, changes, status}` describing proposed edits; `changes` list `{path, kind, diff}` and `status` is `inProgress`, `completed`, `failed`, or `declined`. - `mcpToolCall` — `{id, server, tool, status, arguments, result?, error?}` describing MCP calls; `status` is `inProgress`, `completed`, or `failed`. -- `collabToolCall` — `{id, tool, status, senderThreadId, receiverThreadId?, newThreadId?, prompt?, agentStatus?}` describing collab tool calls (`spawn_agent`, `send_input`, `wait`, `close_agent`); `status` is `inProgress`, `completed`, or `failed`. +- `collabToolCall` — `{id, tool, status, senderThreadId, receiverThreadId?, newThreadId?, prompt?, agentStatus?}` describing collab tool calls (`spawn_agent`, `send_input`, `resume_agent`, `wait`, `close_agent`); `status` is `inProgress`, `completed`, or `failed`. - `webSearch` — `{id, query, action?}` for a web search request issued by the agent; `action` mirrors the Responses API web_search action payload (`search`, `open_page`, `find_in_page`) and may be omitted until completion. - `imageView` — `{id, path}` emitted when the agent invokes the image viewer tool. - `enteredReviewMode` — `{id, review}` sent when the reviewer starts; `review` is a short user-facing label such as `"current changes"` or the requested target description. diff --git a/codex-rs/app-server/src/bespoke_event_handling.rs b/codex-rs/app-server/src/bespoke_event_handling.rs index 1d3d7ff0596..bdf0a58baab 100644 --- a/codex-rs/app-server/src/bespoke_event_handling.rs +++ b/codex-rs/app-server/src/bespoke_event_handling.rs @@ -596,6 +596,28 @@ pub(crate) async fn apply_bespoke_event_handling( .send_server_notification(ServerNotification::ItemCompleted(notification)) .await; } + EventMsg::CollabResumeBegin(begin_event) => { + let item = collab_resume_begin_item(begin_event); + let notification = ItemStartedNotification { + thread_id: conversation_id.to_string(), + turn_id: event_turn_id.clone(), + item, + }; + outgoing + .send_server_notification(ServerNotification::ItemStarted(notification)) + .await; + } + EventMsg::CollabResumeEnd(end_event) => { + let item = collab_resume_end_item(end_event); + let notification = ItemCompletedNotification { + thread_id: conversation_id.to_string(), + turn_id: event_turn_id.clone(), + item, + }; + outgoing + .send_server_notification(ServerNotification::ItemCompleted(notification)) + .await; + } EventMsg::AgentMessageContentDelta(event) => { let codex_protocol::protocol::AgentMessageContentDeltaEvent { item_id, delta, .. } = event; @@ -1758,6 +1780,44 @@ async fn on_command_execution_request_approval_response( } } +fn collab_resume_begin_item( + begin_event: codex_core::protocol::CollabResumeBeginEvent, +) -> ThreadItem { + ThreadItem::CollabAgentToolCall { + id: begin_event.call_id, + tool: CollabAgentTool::ResumeAgent, + status: V2CollabToolCallStatus::InProgress, + sender_thread_id: begin_event.sender_thread_id.to_string(), + receiver_thread_ids: vec![begin_event.receiver_thread_id.to_string()], + prompt: None, + agents_states: HashMap::new(), + } +} + +fn collab_resume_end_item(end_event: codex_core::protocol::CollabResumeEndEvent) -> ThreadItem { + let status = match &end_event.status { + codex_protocol::protocol::AgentStatus::Errored(_) + | codex_protocol::protocol::AgentStatus::NotFound => V2CollabToolCallStatus::Failed, + _ => V2CollabToolCallStatus::Completed, + }; + let receiver_id = end_event.receiver_thread_id.to_string(); + let agents_states = [( + receiver_id.clone(), + V2CollabAgentStatus::from(end_event.status), + )] + .into_iter() + .collect(); + ThreadItem::CollabAgentToolCall { + id: end_event.call_id, + tool: CollabAgentTool::ResumeAgent, + status, + sender_thread_id: end_event.sender_thread_id.to_string(), + receiver_thread_ids: vec![receiver_id], + prompt: None, + agents_states, + } +} + /// similar to handle_mcp_tool_call_begin in exec async fn construct_mcp_tool_call_notification( begin_event: McpToolCallBeginEvent, @@ -1838,6 +1898,8 @@ mod tests { use anyhow::anyhow; use anyhow::bail; use codex_app_server_protocol::TurnPlanStepStatus; + use codex_core::protocol::CollabResumeBeginEvent; + use codex_core::protocol::CollabResumeEndEvent; use codex_core::protocol::CreditsSnapshot; use codex_core::protocol::McpInvocation; use codex_core::protocol::RateLimitSnapshot; @@ -1882,6 +1944,55 @@ mod tests { assert_eq!(completion_status, None); } + #[test] + fn collab_resume_begin_maps_to_item_started_resume_agent() { + let event = CollabResumeBeginEvent { + call_id: "call-1".to_string(), + sender_thread_id: ThreadId::new(), + receiver_thread_id: ThreadId::new(), + }; + + let item = collab_resume_begin_item(event.clone()); + let expected = ThreadItem::CollabAgentToolCall { + id: event.call_id, + tool: CollabAgentTool::ResumeAgent, + status: V2CollabToolCallStatus::InProgress, + sender_thread_id: event.sender_thread_id.to_string(), + receiver_thread_ids: vec![event.receiver_thread_id.to_string()], + prompt: None, + agents_states: HashMap::new(), + }; + assert_eq!(item, expected); + } + + #[test] + fn collab_resume_end_maps_to_item_completed_resume_agent() { + let event = CollabResumeEndEvent { + call_id: "call-2".to_string(), + sender_thread_id: ThreadId::new(), + receiver_thread_id: ThreadId::new(), + status: codex_protocol::protocol::AgentStatus::NotFound, + }; + + let item = collab_resume_end_item(event.clone()); + let receiver_id = event.receiver_thread_id.to_string(); + let expected = ThreadItem::CollabAgentToolCall { + id: event.call_id, + tool: CollabAgentTool::ResumeAgent, + status: V2CollabToolCallStatus::Failed, + sender_thread_id: event.sender_thread_id.to_string(), + receiver_thread_ids: vec![receiver_id.clone()], + prompt: None, + agents_states: [( + receiver_id, + V2CollabAgentStatus::from(codex_protocol::protocol::AgentStatus::NotFound), + )] + .into_iter() + .collect(), + }; + assert_eq!(item, expected); + } + #[tokio::test] async fn test_handle_error_records_message() -> Result<()> { let conversation_id = ThreadId::new(); diff --git a/codex-rs/core/src/agent/control.rs b/codex-rs/core/src/agent/control.rs index a600a0d8b6b..bee0dd1b273 100644 --- a/codex-rs/core/src/agent/control.rs +++ b/codex-rs/core/src/agent/control.rs @@ -5,7 +5,9 @@ use crate::error::Result as CodexResult; use crate::thread_manager::ThreadManagerState; use codex_protocol::ThreadId; use codex_protocol::protocol::Op; +use codex_protocol::protocol::SessionSource; use codex_protocol::user_input::UserInput; +use std::path::PathBuf; use std::sync::Arc; use std::sync::Weak; use tokio::sync::watch; @@ -39,7 +41,7 @@ impl AgentControl { &self, config: crate::config::Config, prompt: String, - session_source: Option, + session_source: Option, ) -> CodexResult { let state = self.upgrade()?; let reservation = self.state.reserve_spawn_slot(config.agent_max_threads)?; @@ -65,6 +67,32 @@ impl AgentControl { Ok(new_thread.thread_id) } + /// Resume an existing agent thread from a recorded rollout file. + pub(crate) async fn resume_agent_from_rollout( + &self, + config: crate::config::Config, + rollout_path: PathBuf, + session_source: SessionSource, + ) -> CodexResult { + let state = self.upgrade()?; + let reservation = self.state.reserve_spawn_slot(config.agent_max_threads)?; + + let resumed_thread = state + .resume_thread_from_rollout_with_source( + config, + rollout_path, + self.clone(), + session_source, + ) + .await?; + reservation.commit(resumed_thread.thread_id); + // Resumed threads are re-registered in-memory and need the same listener + // attachment path as freshly spawned threads. + state.notify_thread_created(resumed_thread.thread_id); + + Ok(resumed_thread.thread_id) + } + /// Send a `user` prompt to an existing agent thread. pub(crate) async fn send_prompt( &self, @@ -287,6 +315,24 @@ mod tests { ); } + #[tokio::test] + async fn resume_agent_errors_when_manager_dropped() { + let control = AgentControl::default(); + let (_home, config) = test_config().await; + let err = control + .resume_agent_from_rollout( + config, + PathBuf::from("/tmp/missing-rollout.jsonl"), + SessionSource::Exec, + ) + .await + .expect_err("resume_agent should fail without a manager"); + assert_eq!( + err.to_string(), + "unsupported operation: thread manager dropped" + ); + } + #[tokio::test] async fn send_prompt_errors_when_thread_missing() { let harness = AgentControlHarness::new().await; @@ -518,4 +564,88 @@ mod tests { .await .expect("shutdown agent"); } + + #[tokio::test] + async fn resume_agent_respects_max_threads_limit() { + let max_threads = 1usize; + let (_home, config) = test_config_with_cli_overrides(vec![( + "agents.max_threads".to_string(), + TomlValue::Integer(max_threads as i64), + )]) + .await; + let manager = ThreadManager::with_models_provider_and_home( + CodexAuth::from_api_key("dummy"), + config.model_provider.clone(), + config.codex_home.clone(), + ); + let control = manager.agent_control(); + + let resumable_id = control + .spawn_agent(config.clone(), "hello".to_string(), None) + .await + .expect("spawn_agent should succeed"); + let rollout_path = manager + .get_thread(resumable_id) + .await + .expect("thread should exist") + .rollout_path() + .expect("rollout path should exist"); + let _ = control + .shutdown_agent(resumable_id) + .await + .expect("shutdown resumable thread"); + + let active_id = control + .spawn_agent(config.clone(), "occupy".to_string(), None) + .await + .expect("spawn_agent should succeed for active slot"); + + let err = control + .resume_agent_from_rollout(config, rollout_path, SessionSource::Exec) + .await + .expect_err("resume should respect max threads"); + let CodexErr::AgentLimitReached { + max_threads: seen_max_threads, + } = err + else { + panic!("expected CodexErr::AgentLimitReached"); + }; + assert_eq!(seen_max_threads, max_threads); + + let _ = control + .shutdown_agent(active_id) + .await + .expect("shutdown active thread"); + } + + #[tokio::test] + async fn resume_agent_releases_slot_after_resume_failure() { + let max_threads = 1usize; + let (_home, config) = test_config_with_cli_overrides(vec![( + "agents.max_threads".to_string(), + TomlValue::Integer(max_threads as i64), + )]) + .await; + let manager = ThreadManager::with_models_provider_and_home( + CodexAuth::from_api_key("dummy"), + config.model_provider.clone(), + config.codex_home.clone(), + ); + let control = manager.agent_control(); + + let missing_rollout = config.codex_home.join("sessions/missing-rollout.jsonl"); + let _ = control + .resume_agent_from_rollout(config.clone(), missing_rollout, SessionSource::Exec) + .await + .expect_err("resume should fail for missing rollout path"); + + let resumed_id = control + .spawn_agent(config, "hello".to_string(), None) + .await + .expect("spawn should succeed after failed resume"); + let _ = control + .shutdown_agent(resumed_id) + .await + .expect("shutdown resumed thread"); + } } diff --git a/codex-rs/core/src/rollout/policy.rs b/codex-rs/core/src/rollout/policy.rs index 587b68a913b..5043c9ddf58 100644 --- a/codex-rs/core/src/rollout/policy.rs +++ b/codex-rs/core/src/rollout/policy.rs @@ -109,6 +109,8 @@ pub(crate) fn should_persist_event_msg(ev: &EventMsg) -> bool { | EventMsg::CollabWaitingBegin(_) | EventMsg::CollabWaitingEnd(_) | EventMsg::CollabCloseBegin(_) - | EventMsg::CollabCloseEnd(_) => false, + | EventMsg::CollabCloseEnd(_) + | EventMsg::CollabResumeBegin(_) + | EventMsg::CollabResumeEnd(_) => false, } } diff --git a/codex-rs/core/src/thread_manager.rs b/codex-rs/core/src/thread_manager.rs index e702d9a0043..13f9efafb06 100644 --- a/codex-rs/core/src/thread_manager.rs +++ b/codex-rs/core/src/thread_manager.rs @@ -405,6 +405,25 @@ impl ThreadManagerState { .await } + pub(crate) async fn resume_thread_from_rollout_with_source( + &self, + config: Config, + rollout_path: PathBuf, + agent_control: AgentControl, + session_source: SessionSource, + ) -> CodexResult { + let initial_history = RolloutRecorder::get_rollout_history(&rollout_path).await?; + self.spawn_thread_with_source( + config, + initial_history, + Arc::clone(&self.auth_manager), + agent_control, + session_source, + Vec::new(), + ) + .await + } + /// Spawn a new thread with optional history and register it with the manager. pub(crate) async fn spawn_thread( &self, diff --git a/codex-rs/core/src/tools/handlers/collab.rs b/codex-rs/core/src/tools/handlers/collab.rs index e6f0c3c9592..bfe213d11f5 100644 --- a/codex-rs/core/src/tools/handlers/collab.rs +++ b/codex-rs/core/src/tools/handlers/collab.rs @@ -22,8 +22,12 @@ use codex_protocol::protocol::CollabAgentSpawnBeginEvent; use codex_protocol::protocol::CollabAgentSpawnEndEvent; use codex_protocol::protocol::CollabCloseBeginEvent; use codex_protocol::protocol::CollabCloseEndEvent; +use codex_protocol::protocol::CollabResumeBeginEvent; +use codex_protocol::protocol::CollabResumeEndEvent; use codex_protocol::protocol::CollabWaitingBeginEvent; use codex_protocol::protocol::CollabWaitingEndEvent; +use codex_protocol::protocol::SessionSource; +use codex_protocol::protocol::SubAgentSource; use serde::Deserialize; use serde::Serialize; @@ -71,6 +75,7 @@ impl ToolHandler for CollabHandler { match tool_name.as_str() { "spawn_agent" => spawn::handle(session, turn, call_id, arguments).await, "send_input" => send_input::handle(session, turn, call_id, arguments).await, + "resume_agent" => resume_agent::handle(session, turn, call_id, arguments).await, "wait" => wait::handle(session, turn, call_id, arguments).await, "close_agent" => close_agent::handle(session, turn, call_id, arguments).await, other => Err(FunctionCallError::RespondToModel(format!( @@ -86,8 +91,6 @@ mod spawn { use crate::agent::exceeds_thread_spawn_depth_limit; use crate::agent::next_thread_spawn_depth; - use codex_protocol::protocol::SessionSource; - use codex_protocol::protocol::SubAgentSource; use std::sync::Arc; #[derive(Debug, Deserialize)] @@ -148,10 +151,7 @@ mod spawn { .spawn_agent( config, prompt.clone(), - Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn { - parent_thread_id: session.conversation_id, - depth: child_depth, - })), + Some(thread_spawn_source(session.conversation_id, child_depth)), ) .await .map_err(collab_spawn_error); @@ -279,6 +279,152 @@ mod send_input { } } +mod resume_agent { + use super::*; + use crate::agent::next_thread_spawn_depth; + use crate::rollout::find_thread_path_by_id_str; + use std::sync::Arc; + + #[derive(Debug, Deserialize)] + struct ResumeAgentArgs { + id: String, + } + + #[derive(Debug, Deserialize, Serialize, PartialEq, Eq)] + pub(super) struct ResumeAgentResult { + pub(super) status: AgentStatus, + } + + pub async fn handle( + session: Arc, + turn: Arc, + call_id: String, + arguments: String, + ) -> Result { + let args: ResumeAgentArgs = parse_arguments(&arguments)?; + let receiver_thread_id = agent_id(&args.id)?; + let child_depth = next_thread_spawn_depth(&turn.session_source); + if exceeds_thread_spawn_depth_limit(child_depth) { + return Err(FunctionCallError::RespondToModel( + "Agent depth limit reached. Solve the task yourself.".to_string(), + )); + } + + session + .send_event( + &turn, + CollabResumeBeginEvent { + call_id: call_id.clone(), + sender_thread_id: session.conversation_id, + receiver_thread_id, + } + .into(), + ) + .await; + + let mut status = session + .services + .agent_control + .get_status(receiver_thread_id) + .await; + let error = if matches!(status, AgentStatus::NotFound) { + // If the thread is no longer active, attempt to restore it from rollout. + match try_resume_closed_agent( + &session, + &turn, + receiver_thread_id, + &args.id, + child_depth, + ) + .await + { + Ok(resumed_status) => { + status = resumed_status; + None + } + Err(err) => { + status = session + .services + .agent_control + .get_status(receiver_thread_id) + .await; + Some(err) + } + } + } else { + None + }; + + session + .send_event( + &turn, + CollabResumeEndEvent { + call_id, + sender_thread_id: session.conversation_id, + receiver_thread_id, + status: status.clone(), + } + .into(), + ) + .await; + + if let Some(err) = error { + return Err(err); + } + + let content = serde_json::to_string(&ResumeAgentResult { status }).map_err(|err| { + FunctionCallError::Fatal(format!("failed to serialize resume_agent result: {err}")) + })?; + + Ok(ToolOutput::Function { + body: FunctionCallOutputBody::Text(content), + success: Some(true), + }) + } + + async fn try_resume_closed_agent( + session: &Arc, + turn: &Arc, + receiver_thread_id: ThreadId, + receiver_id: &str, + child_depth: i32, + ) -> Result { + let rollout_path = find_thread_path_by_id_str( + turn.config.codex_home.as_path(), + receiver_id, + ) + .await + .map_err(|err| { + FunctionCallError::RespondToModel(format!( + "tool failed: failed to locate rollout for agent {receiver_thread_id}: {err}" + )) + })? + .ok_or_else(|| { + FunctionCallError::RespondToModel(format!( + "agent with id {receiver_thread_id} not found" + )) + })?; + + let config = build_agent_resume_config(turn.as_ref(), child_depth)?; + let resumed_thread_id = session + .services + .agent_control + .resume_agent_from_rollout( + config, + rollout_path, + thread_spawn_source(session.conversation_id, child_depth), + ) + .await + .map_err(|err| collab_agent_error(receiver_thread_id, err))?; + + Ok(session + .services + .agent_control + .get_status(resumed_thread_id) + .await) + } +} + mod wait { use super::*; use crate::agent::status::is_final; @@ -585,14 +731,39 @@ fn collab_agent_error(agent_id: ThreadId, err: CodexErr) -> FunctionCallError { } } +fn thread_spawn_source(parent_thread_id: ThreadId, depth: i32) -> SessionSource { + SessionSource::SubAgent(SubAgentSource::ThreadSpawn { + parent_thread_id, + depth, + }) +} + fn build_agent_spawn_config( base_instructions: &BaseInstructions, turn: &TurnContext, child_depth: i32, +) -> Result { + let mut config = build_agent_shared_config(turn, child_depth)?; + config.base_instructions = Some(base_instructions.text.clone()); + Ok(config) +} + +fn build_agent_resume_config( + turn: &TurnContext, + child_depth: i32, +) -> Result { + let mut config = build_agent_shared_config(turn, child_depth)?; + // For resume, keep base instructions sourced from rollout/session metadata. + config.base_instructions = None; + Ok(config) +} + +fn build_agent_shared_config( + turn: &TurnContext, + child_depth: i32, ) -> Result { let base_config = turn.config.clone(); let mut config = (*base_config).clone(); - config.base_instructions = Some(base_instructions.text.clone()); config.model = Some(turn.model_info.slug.clone()); config.model_provider = turn.provider.clone(); config.model_reasoning_effort = turn.reasoning_effort; @@ -883,6 +1054,193 @@ mod tests { .expect("shutdown should submit"); } + #[tokio::test] + async fn resume_agent_rejects_invalid_id() { + let (session, turn) = make_session_and_context().await; + let invocation = invocation( + Arc::new(session), + Arc::new(turn), + "resume_agent", + function_payload(json!({"id": "not-a-uuid"})), + ); + let Err(err) = CollabHandler.handle(invocation).await else { + panic!("invalid id should be rejected"); + }; + let FunctionCallError::RespondToModel(msg) = err else { + panic!("expected respond-to-model error"); + }; + assert!(msg.starts_with("invalid agent id not-a-uuid:")); + } + + #[tokio::test] + async fn resume_agent_reports_missing_agent() { + let (mut session, turn) = make_session_and_context().await; + let manager = thread_manager(); + session.services.agent_control = manager.agent_control(); + let agent_id = ThreadId::new(); + let invocation = invocation( + Arc::new(session), + Arc::new(turn), + "resume_agent", + function_payload(json!({"id": agent_id.to_string()})), + ); + let Err(err) = CollabHandler.handle(invocation).await else { + panic!("missing agent should be reported"); + }; + assert_eq!( + err, + FunctionCallError::RespondToModel(format!("agent with id {agent_id} not found")) + ); + } + + #[tokio::test] + async fn resume_agent_noops_for_active_agent() { + let (mut session, turn) = make_session_and_context().await; + let manager = thread_manager(); + session.services.agent_control = manager.agent_control(); + let config = turn.config.as_ref().clone(); + let thread = manager.start_thread(config).await.expect("start thread"); + let agent_id = thread.thread_id; + let status_before = manager.agent_control().get_status(agent_id).await; + let invocation = invocation( + Arc::new(session), + Arc::new(turn), + "resume_agent", + function_payload(json!({"id": agent_id.to_string()})), + ); + + let output = CollabHandler + .handle(invocation) + .await + .expect("resume_agent should succeed"); + let ToolOutput::Function { + body: FunctionCallOutputBody::Text(content), + success, + .. + } = output + else { + panic!("expected function output"); + }; + let result: resume_agent::ResumeAgentResult = + serde_json::from_str(&content).expect("resume_agent result should be json"); + assert_eq!(result.status, status_before); + assert_eq!(success, Some(true)); + + let thread_ids = manager.list_thread_ids().await; + assert_eq!(thread_ids, vec![agent_id]); + + let _ = thread + .thread + .submit(Op::Shutdown {}) + .await + .expect("shutdown should submit"); + } + + #[tokio::test] + async fn resume_agent_restores_closed_agent_and_accepts_send_input() { + let (mut session, turn) = make_session_and_context().await; + let manager = thread_manager(); + session.services.agent_control = manager.agent_control(); + let config = turn.config.as_ref().clone(); + let thread = manager.start_thread(config).await.expect("start thread"); + let agent_id = thread.thread_id; + let _ = manager + .agent_control() + .shutdown_agent(agent_id) + .await + .expect("shutdown agent"); + assert_eq!( + manager.agent_control().get_status(agent_id).await, + AgentStatus::NotFound + ); + let session = Arc::new(session); + let turn = Arc::new(turn); + + let resume_invocation = invocation( + session.clone(), + turn.clone(), + "resume_agent", + function_payload(json!({"id": agent_id.to_string()})), + ); + let output = CollabHandler + .handle(resume_invocation) + .await + .expect("resume_agent should succeed"); + let ToolOutput::Function { + body: FunctionCallOutputBody::Text(content), + success, + .. + } = output + else { + panic!("expected function output"); + }; + let result: resume_agent::ResumeAgentResult = + serde_json::from_str(&content).expect("resume_agent result should be json"); + assert_ne!(result.status, AgentStatus::NotFound); + assert_eq!(success, Some(true)); + + let send_invocation = invocation( + session, + turn, + "send_input", + function_payload(json!({"id": agent_id.to_string(), "message": "hello"})), + ); + let output = CollabHandler + .handle(send_invocation) + .await + .expect("send_input should succeed after resume"); + let ToolOutput::Function { + body: FunctionCallOutputBody::Text(content), + success, + .. + } = output + else { + panic!("expected function output"); + }; + let result: serde_json::Value = + serde_json::from_str(&content).expect("send_input result should be json"); + let submission_id = result + .get("submission_id") + .and_then(|value| value.as_str()) + .unwrap_or_default(); + assert!(!submission_id.is_empty()); + assert_eq!(success, Some(true)); + + let _ = manager + .agent_control() + .shutdown_agent(agent_id) + .await + .expect("shutdown resumed agent"); + } + + #[tokio::test] + async fn resume_agent_rejects_when_depth_limit_exceeded() { + let (mut session, mut turn) = make_session_and_context().await; + let manager = thread_manager(); + session.services.agent_control = manager.agent_control(); + + turn.session_source = SessionSource::SubAgent(SubAgentSource::ThreadSpawn { + parent_thread_id: session.conversation_id, + depth: MAX_THREAD_SPAWN_DEPTH, + }); + + let invocation = invocation( + Arc::new(session), + Arc::new(turn), + "resume_agent", + function_payload(json!({"id": ThreadId::new().to_string()})), + ); + let Err(err) = CollabHandler.handle(invocation).await else { + panic!("resume should fail when depth limit exceeded"); + }; + assert_eq!( + err, + FunctionCallError::RespondToModel( + "Agent depth limit reached. Solve the task yourself.".to_string() + ) + ); + } + #[derive(Debug, Deserialize, PartialEq, Eq)] struct WaitResult { status: HashMap, @@ -1259,4 +1617,35 @@ mod tests { assert_eq!(config.user_instructions, base_config.user_instructions); } + + #[tokio::test] + async fn build_agent_resume_config_clears_base_instructions() { + let (_session, mut turn) = make_session_and_context().await; + let mut base_config = (*turn.config).clone(); + base_config.base_instructions = Some("caller-base".to_string()); + turn.config = Arc::new(base_config); + + let config = build_agent_resume_config(&turn, 0).expect("resume config"); + + let mut expected = (*turn.config).clone(); + expected.base_instructions = None; + expected.model = Some(turn.model_info.slug.clone()); + expected.model_provider = turn.provider.clone(); + expected.model_reasoning_effort = turn.reasoning_effort; + expected.model_reasoning_summary = turn.reasoning_summary; + expected.developer_instructions = turn.developer_instructions.clone(); + expected.compact_prompt = turn.compact_prompt.clone(); + expected.shell_environment_policy = turn.shell_environment_policy.clone(); + expected.codex_linux_sandbox_exe = turn.codex_linux_sandbox_exe.clone(); + expected.cwd = turn.cwd.clone(); + expected + .approval_policy + .set(turn.approval_policy) + .expect("approval policy set"); + expected + .sandbox_policy + .set(turn.sandbox_policy) + .expect("sandbox policy set"); + assert_eq!(config, expected); + } } diff --git a/codex-rs/core/src/tools/spec.rs b/codex-rs/core/src/tools/spec.rs index 26fccf318ac..e9fc8759761 100644 --- a/codex-rs/core/src/tools/spec.rs +++ b/codex-rs/core/src/tools/spec.rs @@ -522,6 +522,29 @@ fn create_send_input_tool() -> ToolSpec { }) } +fn create_resume_agent_tool() -> ToolSpec { + let mut properties = BTreeMap::new(); + properties.insert( + "id".to_string(), + JsonSchema::String { + description: Some("Agent id to resume.".to_string()), + }, + ); + + ToolSpec::Function(ResponsesApiTool { + name: "resume_agent".to_string(), + description: + "Resume a previously closed agent by id so it can receive send_input and wait calls." + .to_string(), + strict: false, + parameters: JsonSchema::Object { + properties, + required: Some(vec!["id".to_string()]), + additional_properties: Some(false.into()), + }, + }) +} + fn create_wait_tool() -> ToolSpec { let mut properties = BTreeMap::new(); properties.insert( @@ -1410,10 +1433,12 @@ pub(crate) fn build_specs( let collab_handler = Arc::new(CollabHandler); builder.push_spec(create_spawn_agent_tool()); builder.push_spec(create_send_input_tool()); + builder.push_spec(create_resume_agent_tool()); builder.push_spec(create_wait_tool()); builder.push_spec(create_close_agent_tool()); builder.register_handler("spawn_agent", collab_handler.clone()); builder.register_handler("send_input", collab_handler.clone()); + builder.register_handler("resume_agent", collab_handler.clone()); builder.register_handler("wait", collab_handler.clone()); builder.register_handler("close_agent", collab_handler); } @@ -1670,7 +1695,13 @@ mod tests { let (tools, _) = build_specs(&tools_config, None, &[]).build(); assert_contains_tool_names( &tools, - &["spawn_agent", "send_input", "wait", "close_agent"], + &[ + "spawn_agent", + "send_input", + "resume_agent", + "wait", + "close_agent", + ], ); } diff --git a/codex-rs/exec/src/event_processor_with_human_output.rs b/codex-rs/exec/src/event_processor_with_human_output.rs index cbe45b92f19..6b154b1799e 100644 --- a/codex-rs/exec/src/event_processor_with_human_output.rs +++ b/codex-rs/exec/src/event_processor_with_human_output.rs @@ -796,6 +796,8 @@ impl EventProcessor for EventProcessorWithHumanOutput { | EventMsg::UndoStarted(_) | EventMsg::ThreadRolledBack(_) | EventMsg::RequestUserInput(_) + | EventMsg::CollabResumeBegin(_) + | EventMsg::CollabResumeEnd(_) | EventMsg::DynamicToolCallRequest(_) => {} } CodexStatus::Running diff --git a/codex-rs/mcp-server/src/codex_tool_runner.rs b/codex-rs/mcp-server/src/codex_tool_runner.rs index 6513c9e6f74..6f6d2ec7823 100644 --- a/codex-rs/mcp-server/src/codex_tool_runner.rs +++ b/codex-rs/mcp-server/src/codex_tool_runner.rs @@ -364,6 +364,8 @@ async fn run_codex_tool_session_inner( | EventMsg::CollabWaitingEnd(_) | EventMsg::CollabCloseBegin(_) | EventMsg::CollabCloseEnd(_) + | EventMsg::CollabResumeBegin(_) + | EventMsg::CollabResumeEnd(_) | EventMsg::DeprecationNotice(_) => { // For now, we do not do anything extra for these // events. Note that diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 078ea31d18a..c6686a33c02 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -879,6 +879,10 @@ pub enum EventMsg { CollabCloseBegin(CollabCloseBeginEvent), /// Collab interaction: close end. CollabCloseEnd(CollabCloseEndEvent), + /// Collab interaction: resume begin. + CollabResumeBegin(CollabResumeBeginEvent), + /// Collab interaction: resume end. + CollabResumeEnd(CollabResumeEndEvent), } impl From for EventMsg { @@ -929,6 +933,18 @@ impl From for EventMsg { } } +impl From for EventMsg { + fn from(event: CollabResumeBeginEvent) -> Self { + EventMsg::CollabResumeBegin(event) + } +} + +impl From for EventMsg { + fn from(event: CollabResumeEndEvent) -> Self { + EventMsg::CollabResumeEnd(event) + } +} + /// Agent lifecycle status, derived from emitted events. #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS, Default)] #[serde(rename_all = "snake_case")] @@ -2476,6 +2492,29 @@ pub struct CollabCloseEndEvent { pub status: AgentStatus, } +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)] +pub struct CollabResumeBeginEvent { + /// Identifier for the collab tool call. + pub call_id: String, + /// Thread ID of the sender. + pub sender_thread_id: ThreadId, + /// Thread ID of the receiver. + pub receiver_thread_id: ThreadId, +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)] +pub struct CollabResumeEndEvent { + /// Identifier for the collab tool call. + pub call_id: String, + /// Thread ID of the sender. + pub sender_thread_id: ThreadId, + /// Thread ID of the receiver. + pub receiver_thread_id: ThreadId, + /// Last known status of the receiver agent reported to the sender agent after + /// resume. + pub status: AgentStatus, +} + #[cfg(test)] mod tests { use super::*; diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 23e63820b2f..6657d1f34cc 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -3890,6 +3890,8 @@ impl ChatWidget { EventMsg::CollabWaitingEnd(ev) => self.on_collab_event(collab::waiting_end(ev)), EventMsg::CollabCloseBegin(_) => {} EventMsg::CollabCloseEnd(ev) => self.on_collab_event(collab::close_end(ev)), + EventMsg::CollabResumeBegin(ev) => self.on_collab_event(collab::resume_begin(ev)), + EventMsg::CollabResumeEnd(ev) => self.on_collab_event(collab::resume_end(ev)), EventMsg::ThreadRolledBack(_) => {} EventMsg::RawResponseItem(_) | EventMsg::ItemStarted(_) diff --git a/codex-rs/tui/src/collab.rs b/codex-rs/tui/src/collab.rs index b6c7d809596..e26bb8785a3 100644 --- a/codex-rs/tui/src/collab.rs +++ b/codex-rs/tui/src/collab.rs @@ -5,6 +5,8 @@ use codex_core::protocol::AgentStatus; use codex_core::protocol::CollabAgentInteractionEndEvent; use codex_core::protocol::CollabAgentSpawnEndEvent; use codex_core::protocol::CollabCloseEndEvent; +use codex_core::protocol::CollabResumeBeginEvent; +use codex_core::protocol::CollabResumeEndEvent; use codex_core::protocol::CollabWaitingBeginEvent; use codex_core::protocol::CollabWaitingEndEvent; use codex_protocol::ThreadId; @@ -97,6 +99,34 @@ pub(crate) fn close_end(ev: CollabCloseEndEvent) -> PlainHistoryCell { collab_event("Agent closed", details) } +pub(crate) fn resume_begin(ev: CollabResumeBeginEvent) -> PlainHistoryCell { + let CollabResumeBeginEvent { + call_id, + sender_thread_id: _, + receiver_thread_id, + } = ev; + let details = vec![ + detail_line("call", call_id), + detail_line("receiver", receiver_thread_id.to_string()), + ]; + collab_event("Resuming agent", details) +} + +pub(crate) fn resume_end(ev: CollabResumeEndEvent) -> PlainHistoryCell { + let CollabResumeEndEvent { + call_id, + sender_thread_id: _, + receiver_thread_id, + status, + } = ev; + let details = vec![ + detail_line("call", call_id), + detail_line("receiver", receiver_thread_id.to_string()), + status_line(&status), + ]; + collab_event("Agent resumed", details) +} + fn collab_event(title: impl Into, details: Vec>) -> PlainHistoryCell { let title = title.into(); let mut lines: Vec> =