mirror of
https://github.com/openai/codex.git
synced 2026-11-13 06:45:07 +00:00
Report completed sub-agent activity on parent turns (#40437)
## Why A Multi-Agent V2 child can finish after the parent turn that spawned it, so its successful completion needs to remain associated with that parent turn in notifications and history. ## What changed - Add `completed` to `SubAgentActivityKind` and emit the corresponding started and completed item events on the spawning parent turn when a child succeeds. - Persist completed activity as a canonical turn item in both legacy and paginated history, and expose it through app-server history and schemas. - Render completed activity as terminal in the TUI without counting it as a separate tool call or tool-runtime event. ## Testing - Cover successful completion in legacy and paginated history, late updates to completed parent turns, TUI rendering, analytics, and rollout tracing. - Verify that terminal child errors do not emit completed activity. GitOrigin-RevId: 6c71eb8b10e7327611c6af84f70e294e27d94d02
This commit is contained in:
1 parent
7781f0a5a8
commit
b705b6b076
40 files changed
+466
-39
No files matched your search
@@ -5066,6 +5066,12 @@ async fn turn_event_counts_completed_tool_items() {
|
|||||||
agent_thread_id: "thread-child".to_string(),
|
agent_thread_id: "thread-child".to_string(),
|
||||||
agent_path: "/root/child".to_string(),
|
agent_path: "/root/child".to_string(),
|
||||||
},
|
},
|
||||||
|
ThreadItem::SubAgentActivity {
|
||||||
|
id: "sub-agent-activity-completed".to_string(),
|
||||||
|
kind: SubAgentActivityKind::Completed,
|
||||||
|
agent_thread_id: "thread-child".to_string(),
|
||||||
|
agent_path: "/root/child".to_string(),
|
||||||
|
},
|
||||||
ThreadItem::WebSearch(WebSearchItem {
|
ThreadItem::WebSearch(WebSearchItem {
|
||||||
id: "web-1".to_string(),
|
id: "web-1".to_string(),
|
||||||
query: "codex".to_string(),
|
query: "codex".to_string(),
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ use codex_app_server_protocol::RequestPermissionProfile;
|
|||||||
use codex_app_server_protocol::ServerNotification;
|
use codex_app_server_protocol::ServerNotification;
|
||||||
use codex_app_server_protocol::ServerRequest;
|
use codex_app_server_protocol::ServerRequest;
|
||||||
use codex_app_server_protocol::ServerResponse;
|
use codex_app_server_protocol::ServerResponse;
|
||||||
|
use codex_app_server_protocol::SubAgentActivityKind;
|
||||||
use codex_app_server_protocol::ThreadItem;
|
use codex_app_server_protocol::ThreadItem;
|
||||||
use codex_app_server_protocol::TurnSteerResponse;
|
use codex_app_server_protocol::TurnSteerResponse;
|
||||||
use codex_app_server_protocol::UserInput;
|
use codex_app_server_protocol::UserInput;
|
||||||
@@ -476,6 +477,10 @@ impl TurnToolCounts {
|
|||||||
ThreadItem::FileChange { .. } => self.file_change += 1,
|
ThreadItem::FileChange { .. } => self.file_change += 1,
|
||||||
ThreadItem::McpToolCall { .. } => self.mcp_tool_call += 1,
|
ThreadItem::McpToolCall { .. } => self.mcp_tool_call += 1,
|
||||||
ThreadItem::DynamicToolCall { .. } => self.dynamic_tool_call += 1,
|
ThreadItem::DynamicToolCall { .. } => self.dynamic_tool_call += 1,
|
||||||
|
ThreadItem::SubAgentActivity {
|
||||||
|
kind: SubAgentActivityKind::Completed,
|
||||||
|
..
|
||||||
|
} => return,
|
||||||
ThreadItem::CollabAgentToolCall { id, .. }
|
ThreadItem::CollabAgentToolCall { id, .. }
|
||||||
| ThreadItem::SubAgentActivity { id, .. } => {
|
| ThreadItem::SubAgentActivity { id, .. } => {
|
||||||
if !self.subagent_tool_call_ids.insert(id.clone()) {
|
if !self.subagent_tool_call_ids.insert(id.clone()) {
|
||||||
|
|||||||
@@ -3672,7 +3672,8 @@
|
|||||||
"enum": [
|
"enum": [
|
||||||
"started",
|
"started",
|
||||||
"interacted",
|
"interacted",
|
||||||
"interrupted"
|
"interrupted",
|
||||||
|
"completed"
|
||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|||||||
+2
-1
@@ -19366,7 +19366,8 @@
|
|||||||
"enum": [
|
"enum": [
|
||||||
"started",
|
"started",
|
||||||
"interacted",
|
"interacted",
|
||||||
"interrupted"
|
"interrupted",
|
||||||
|
"completed"
|
||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|||||||
+2
-1
@@ -17118,7 +17118,8 @@
|
|||||||
"enum": [
|
"enum": [
|
||||||
"started",
|
"started",
|
||||||
"interacted",
|
"interacted",
|
||||||
"interrupted"
|
"interrupted",
|
||||||
|
"completed"
|
||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|||||||
+2
-1
@@ -565,7 +565,8 @@
|
|||||||
"enum": [
|
"enum": [
|
||||||
"started",
|
"started",
|
||||||
"interacted",
|
"interacted",
|
||||||
"interrupted"
|
"interrupted",
|
||||||
|
"completed"
|
||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -565,7 +565,8 @@
|
|||||||
"enum": [
|
"enum": [
|
||||||
"started",
|
"started",
|
||||||
"interacted",
|
"interacted",
|
||||||
"interrupted"
|
"interrupted",
|
||||||
|
"completed"
|
||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -711,7 +711,8 @@
|
|||||||
"enum": [
|
"enum": [
|
||||||
"started",
|
"started",
|
||||||
"interacted",
|
"interacted",
|
||||||
"interrupted"
|
"interrupted",
|
||||||
|
"completed"
|
||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -986,7 +986,8 @@
|
|||||||
"enum": [
|
"enum": [
|
||||||
"started",
|
"started",
|
||||||
"interacted",
|
"interacted",
|
||||||
"interrupted"
|
"interrupted",
|
||||||
|
"completed"
|
||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -777,7 +777,8 @@
|
|||||||
"enum": [
|
"enum": [
|
||||||
"started",
|
"started",
|
||||||
"interacted",
|
"interacted",
|
||||||
"interrupted"
|
"interrupted",
|
||||||
|
"completed"
|
||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|||||||
+2
-1
@@ -777,7 +777,8 @@
|
|||||||
"enum": [
|
"enum": [
|
||||||
"started",
|
"started",
|
||||||
"interacted",
|
"interacted",
|
||||||
"interrupted"
|
"interrupted",
|
||||||
|
"completed"
|
||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -777,7 +777,8 @@
|
|||||||
"enum": [
|
"enum": [
|
||||||
"started",
|
"started",
|
||||||
"interacted",
|
"interacted",
|
||||||
"interrupted"
|
"interrupted",
|
||||||
|
"completed"
|
||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -986,7 +986,8 @@
|
|||||||
"enum": [
|
"enum": [
|
||||||
"started",
|
"started",
|
||||||
"interacted",
|
"interacted",
|
||||||
"interrupted"
|
"interrupted",
|
||||||
|
"completed"
|
||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -777,7 +777,8 @@
|
|||||||
"enum": [
|
"enum": [
|
||||||
"started",
|
"started",
|
||||||
"interacted",
|
"interacted",
|
||||||
"interrupted"
|
"interrupted",
|
||||||
|
"completed"
|
||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -986,7 +986,8 @@
|
|||||||
"enum": [
|
"enum": [
|
||||||
"started",
|
"started",
|
||||||
"interacted",
|
"interacted",
|
||||||
"interrupted"
|
"interrupted",
|
||||||
|
"completed"
|
||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|||||||
+2
-1
@@ -777,7 +777,8 @@
|
|||||||
"enum": [
|
"enum": [
|
||||||
"started",
|
"started",
|
||||||
"interacted",
|
"interacted",
|
||||||
"interrupted"
|
"interrupted",
|
||||||
|
"completed"
|
||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -777,7 +777,8 @@
|
|||||||
"enum": [
|
"enum": [
|
||||||
"started",
|
"started",
|
||||||
"interacted",
|
"interacted",
|
||||||
"interrupted"
|
"interrupted",
|
||||||
|
"completed"
|
||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|||||||
+2
-1
@@ -711,7 +711,8 @@
|
|||||||
"enum": [
|
"enum": [
|
||||||
"started",
|
"started",
|
||||||
"interacted",
|
"interacted",
|
||||||
"interrupted"
|
"interrupted",
|
||||||
|
"completed"
|
||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -711,7 +711,8 @@
|
|||||||
"enum": [
|
"enum": [
|
||||||
"started",
|
"started",
|
||||||
"interacted",
|
"interacted",
|
||||||
"interrupted"
|
"interrupted",
|
||||||
|
"completed"
|
||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -711,7 +711,8 @@
|
|||||||
"enum": [
|
"enum": [
|
||||||
"started",
|
"started",
|
||||||
"interacted",
|
"interacted",
|
||||||
"interrupted"
|
"interrupted",
|
||||||
|
"completed"
|
||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|||||||
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -2,4 +2,4 @@
|
|||||||
|
|
||||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
export type SubAgentActivityKind = "started" | "interacted" | "interrupted";
|
export type SubAgentActivityKind = "started" | "interacted" | "interrupted" | "completed";
|
||||||
@@ -1631,6 +1631,7 @@ mod tests {
|
|||||||
use codex_protocol::items::EnteredReviewModeItem as CoreEnteredReviewModeItem;
|
use codex_protocol::items::EnteredReviewModeItem as CoreEnteredReviewModeItem;
|
||||||
use codex_protocol::items::ExitedReviewModeItem as CoreExitedReviewModeItem;
|
use codex_protocol::items::ExitedReviewModeItem as CoreExitedReviewModeItem;
|
||||||
use codex_protocol::items::HookPromptFragment as CoreHookPromptFragment;
|
use codex_protocol::items::HookPromptFragment as CoreHookPromptFragment;
|
||||||
|
use codex_protocol::items::SubAgentActivityItem as CoreSubAgentActivityItem;
|
||||||
use codex_protocol::items::TurnItem as CoreTurnItem;
|
use codex_protocol::items::TurnItem as CoreTurnItem;
|
||||||
use codex_protocol::items::UserMessageItem as CoreUserMessageItem;
|
use codex_protocol::items::UserMessageItem as CoreUserMessageItem;
|
||||||
use codex_protocol::items::build_hook_prompt_message;
|
use codex_protocol::items::build_hook_prompt_message;
|
||||||
@@ -1654,6 +1655,7 @@ mod tests {
|
|||||||
use codex_protocol::protocol::McpToolCallEndEvent;
|
use codex_protocol::protocol::McpToolCallEndEvent;
|
||||||
use codex_protocol::protocol::PatchApplyBeginEvent;
|
use codex_protocol::protocol::PatchApplyBeginEvent;
|
||||||
use codex_protocol::protocol::ReviewTarget;
|
use codex_protocol::protocol::ReviewTarget;
|
||||||
|
use codex_protocol::protocol::SubAgentActivityKind as CoreSubAgentActivityKind;
|
||||||
use codex_protocol::protocol::ThreadRolledBackEvent;
|
use codex_protocol::protocol::ThreadRolledBackEvent;
|
||||||
use codex_protocol::protocol::TurnAbortReason;
|
use codex_protocol::protocol::TurnAbortReason;
|
||||||
use codex_protocol::protocol::TurnAbortedEvent;
|
use codex_protocol::protocol::TurnAbortedEvent;
|
||||||
@@ -4480,6 +4482,57 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn completed_sub_agent_activity_updates_completed_parent_turn() {
|
||||||
|
let child_thread_id = ThreadId::new();
|
||||||
|
let child_path = codex_protocol::AgentPath::root()
|
||||||
|
.join("worker")
|
||||||
|
.expect("worker path");
|
||||||
|
let items = vec![
|
||||||
|
RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent {
|
||||||
|
turn_id: "turn-a".into(),
|
||||||
|
trace_id: None,
|
||||||
|
started_at: None,
|
||||||
|
model_context_window: None,
|
||||||
|
collaboration_mode_kind: Default::default(),
|
||||||
|
})),
|
||||||
|
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
|
||||||
|
turn_id: "turn-a".into(),
|
||||||
|
started_at: None,
|
||||||
|
last_agent_message: None,
|
||||||
|
error: None,
|
||||||
|
completed_at: None,
|
||||||
|
duration_ms: None,
|
||||||
|
time_to_first_token_ms: None,
|
||||||
|
})),
|
||||||
|
RolloutItem::EventMsg(EventMsg::ItemCompleted(ItemCompletedEvent {
|
||||||
|
thread_id: ThreadId::new(),
|
||||||
|
turn_id: "turn-a".into(),
|
||||||
|
item: CoreTurnItem::SubAgentActivity(CoreSubAgentActivityItem {
|
||||||
|
id: "child-turn-completed".into(),
|
||||||
|
kind: CoreSubAgentActivityKind::Completed,
|
||||||
|
agent_thread_id: child_thread_id,
|
||||||
|
agent_path: child_path,
|
||||||
|
}),
|
||||||
|
started_at_ms: None,
|
||||||
|
completed_at_ms: 0,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
|
||||||
|
let turns = build_turns_from_rollout_items(&items);
|
||||||
|
|
||||||
|
assert_eq!(turns.len(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
turns[0].items,
|
||||||
|
vec![ThreadItem::SubAgentActivity {
|
||||||
|
id: "child-turn-completed".into(),
|
||||||
|
kind: crate::protocol::v2::SubAgentActivityKind::Completed,
|
||||||
|
agent_thread_id: child_thread_id.to_string(),
|
||||||
|
agent_path: "/root/worker".into(),
|
||||||
|
}]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ignores_plain_user_response_items_in_rollout_replay() {
|
fn ignores_plain_user_response_items_in_rollout_replay() {
|
||||||
let items = vec![
|
let items = vec![
|
||||||
|
|||||||
@@ -1180,6 +1180,7 @@ pub enum SubAgentActivityKind {
|
|||||||
Started,
|
Started,
|
||||||
Interacted,
|
Interacted,
|
||||||
Interrupted,
|
Interrupted,
|
||||||
|
Completed,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<CoreSubAgentActivityKind> for SubAgentActivityKind {
|
impl From<CoreSubAgentActivityKind> for SubAgentActivityKind {
|
||||||
@@ -1188,6 +1189,7 @@ impl From<CoreSubAgentActivityKind> for SubAgentActivityKind {
|
|||||||
CoreSubAgentActivityKind::Started => SubAgentActivityKind::Started,
|
CoreSubAgentActivityKind::Started => SubAgentActivityKind::Started,
|
||||||
CoreSubAgentActivityKind::Interacted => SubAgentActivityKind::Interacted,
|
CoreSubAgentActivityKind::Interacted => SubAgentActivityKind::Interacted,
|
||||||
CoreSubAgentActivityKind::Interrupted => SubAgentActivityKind::Interrupted,
|
CoreSubAgentActivityKind::Interrupted => SubAgentActivityKind::Interrupted,
|
||||||
|
CoreSubAgentActivityKind::Completed => SubAgentActivityKind::Completed,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3187,7 +3187,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
|
|||||||
|
|
||||||
let sub_agent_activity_item = TurnItem::SubAgentActivity(SubAgentActivityItem {
|
let sub_agent_activity_item = TurnItem::SubAgentActivity(SubAgentActivityItem {
|
||||||
id: "activity-1".to_string(),
|
id: "activity-1".to_string(),
|
||||||
kind: CoreSubAgentActivityKind::Interrupted,
|
kind: CoreSubAgentActivityKind::Completed,
|
||||||
agent_thread_id: receiver_thread_id,
|
agent_thread_id: receiver_thread_id,
|
||||||
agent_path: codex_protocol::AgentPath::root()
|
agent_path: codex_protocol::AgentPath::root()
|
||||||
.join("worker")
|
.join("worker")
|
||||||
@@ -3198,7 +3198,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
|
|||||||
ThreadItem::from(sub_agent_activity_item),
|
ThreadItem::from(sub_agent_activity_item),
|
||||||
ThreadItem::SubAgentActivity {
|
ThreadItem::SubAgentActivity {
|
||||||
id: "activity-1".to_string(),
|
id: "activity-1".to_string(),
|
||||||
kind: SubAgentActivityKind::Interrupted,
|
kind: SubAgentActivityKind::Completed,
|
||||||
agent_thread_id: receiver_thread_id.to_string(),
|
agent_thread_id: receiver_thread_id.to_string(),
|
||||||
agent_path: "/root/worker".to_string(),
|
agent_path: "/root/worker".to_string(),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1640,6 +1640,7 @@ The app-server streams JSON-RPC notifications while a turn is running. Each turn
|
|||||||
- `fileChange` — `{id, changes, status}` describing proposed edits; `changes` list `{path, kind, diff}` and `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, appContext, mcpAppResourceUri?, pluginId, readOnlyHint, result?, error?}` describing MCP calls; `appContext` is `{connectorId, linkId, resourceUri, appName, actionName}` for calls through a trusted MCP app, where `connectorId` identifies the connector that owns the tool, `linkId` identifies the app link, `resourceUri` points to the widget template, `appName` is the connector's display name, and `actionName` is the stable connector `Action.name`. `readOnlyHint` is `true` for read-only tools, `false` for write-capable tools, and `null` when the annotation is unavailable, including older rollout entries. The hint describes tool capability, not whether an invocation succeeded or performed a write; use `status`, `result`, and `error` to determine the execution outcome. `appName` and `actionName` may be null for older rollout entries. The top-level `mcpAppResourceUri` is deprecated and temporarily duplicated for client migration. `tool` identifies the raw MCP tool. `status` is `inProgress`, `completed`, or `failed`.
|
- `mcpToolCall` — `{id, server, tool, status, arguments, appContext, mcpAppResourceUri?, pluginId, readOnlyHint, result?, error?}` describing MCP calls; `appContext` is `{connectorId, linkId, resourceUri, appName, actionName}` for calls through a trusted MCP app, where `connectorId` identifies the connector that owns the tool, `linkId` identifies the app link, `resourceUri` points to the widget template, `appName` is the connector's display name, and `actionName` is the stable connector `Action.name`. `readOnlyHint` is `true` for read-only tools, `false` for write-capable tools, and `null` when the annotation is unavailable, including older rollout entries. The hint describes tool capability, not whether an invocation succeeded or performed a write; use `status`, `result`, and `error` to determine the execution outcome. `appName` and `actionName` may be null for older rollout entries. The top-level `mcpAppResourceUri` is deprecated and temporarily duplicated for client migration. `tool` identifies the raw MCP tool. `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`.
|
- `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`.
|
||||||
|
- `subAgentActivity` — `{id, kind, agentThreadId, agentPath}` describing Multi-Agent V2 lifecycle activity; `kind` is `started`, `interacted`, `interrupted`, or `completed`. A successful child completion is attributed to the parent turn that spawned it, so its `item/completed` notification may arrive after that turn's `turn/completed` notification and is included with that turn when history is read.
|
||||||
- `webSearch` — `{id, query, action?, results?}` 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. For standalone web search, `results` contains the out-of-band structured result DTOs returned by `/v1/alpha/search`; clients should ignore result types and fields they do not understand.
|
- `webSearch` — `{id, query, action?, results?}` 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. For standalone web search, `results` contains the out-of-band structured result DTOs returned by `/v1/alpha/search`; clients should ignore result types and fields they do not understand.
|
||||||
- `imageGeneration` — `{id, status, revisedPrompt, result, transparentBackground, savedPath?}` for a generated image. `transparentBackground` is `true` when the Images API reports a transparent background, `false` when it reports an opaque background, and `null` when the background is automatic, unavailable, or the item has not completed. The field is always present on v2 item payloads, including persisted and resumed items.
|
- `imageGeneration` — `{id, status, revisedPrompt, result, transparentBackground, savedPath?}` for a generated image. `transparentBackground` is `true` when the Images API reports a transparent background, `false` when it reports an opaque background, and `null` when the background is automatic, unavailable, or the item has not completed. The field is always present on v2 item payloads, including persisted and resumed items.
|
||||||
- `imageView` — `{id, path}` emitted when the agent invokes the image viewer tool.
|
- `imageView` — `{id, path}` emitted when the agent invokes the image viewer tool.
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ use crate::thread_manager::ThreadIdGenerator;
|
|||||||
use crate::thread_manager::ThreadManagerState;
|
use crate::thread_manager::ThreadManagerState;
|
||||||
use crate::thread_manager::default_thread_id_generator;
|
use crate::thread_manager::default_thread_id_generator;
|
||||||
use crate::thread_rollout_truncation::truncate_rollout_to_last_n_fork_turns;
|
use crate::thread_rollout_truncation::truncate_rollout_to_last_n_fork_turns;
|
||||||
|
use crate::turn_timing::now_unix_timestamp_ms;
|
||||||
use codex_history::InitialHistory;
|
use codex_history::InitialHistory;
|
||||||
use codex_history::ResumedHistory;
|
use codex_history::ResumedHistory;
|
||||||
use codex_history::RolloutItem;
|
use codex_history::RolloutItem;
|
||||||
@@ -33,11 +34,17 @@ use codex_protocol::ThreadId;
|
|||||||
use codex_protocol::error::CodexErr;
|
use codex_protocol::error::CodexErr;
|
||||||
use codex_protocol::error::CodexErrorDetails;
|
use codex_protocol::error::CodexErrorDetails;
|
||||||
use codex_protocol::error::Result as CodexResult;
|
use codex_protocol::error::Result as CodexResult;
|
||||||
|
use codex_protocol::items::SubAgentActivityItem;
|
||||||
|
use codex_protocol::items::TurnItem;
|
||||||
use codex_protocol::models::ContentItem;
|
use codex_protocol::models::ContentItem;
|
||||||
use codex_protocol::models::MessagePhase;
|
use codex_protocol::models::MessagePhase;
|
||||||
use codex_protocol::models::ResponseItem;
|
use codex_protocol::models::ResponseItem;
|
||||||
|
use codex_protocol::protocol::Event;
|
||||||
use codex_protocol::protocol::EventMsg;
|
use codex_protocol::protocol::EventMsg;
|
||||||
|
use codex_protocol::protocol::HasLegacyEvent;
|
||||||
use codex_protocol::protocol::InterAgentCommunication;
|
use codex_protocol::protocol::InterAgentCommunication;
|
||||||
|
use codex_protocol::protocol::ItemCompletedEvent;
|
||||||
|
use codex_protocol::protocol::ItemStartedEvent;
|
||||||
use codex_protocol::protocol::MultiAgentVersion;
|
use codex_protocol::protocol::MultiAgentVersion;
|
||||||
use codex_protocol::protocol::Op;
|
use codex_protocol::protocol::Op;
|
||||||
use codex_protocol::protocol::SessionSource;
|
use codex_protocol::protocol::SessionSource;
|
||||||
@@ -233,6 +240,55 @@ impl AgentControl {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn emit_sub_agent_activity(
|
||||||
|
&self,
|
||||||
|
thread_id: ThreadId,
|
||||||
|
turn_id: String,
|
||||||
|
item: SubAgentActivityItem,
|
||||||
|
) -> CodexResult<()> {
|
||||||
|
let state = self.upgrade()?;
|
||||||
|
let thread = state.get_thread(thread_id).await?;
|
||||||
|
let started_at_ms = now_unix_timestamp_ms();
|
||||||
|
let item = TurnItem::SubAgentActivity(item);
|
||||||
|
thread
|
||||||
|
.session
|
||||||
|
.send_event_raw(Event {
|
||||||
|
id: turn_id.clone(),
|
||||||
|
msg: EventMsg::ItemStarted(ItemStartedEvent {
|
||||||
|
thread_id,
|
||||||
|
turn_id: turn_id.clone(),
|
||||||
|
item: item.clone(),
|
||||||
|
started_at_ms,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
let completed_at_ms = now_unix_timestamp_ms();
|
||||||
|
let completed = ItemCompletedEvent {
|
||||||
|
thread_id,
|
||||||
|
turn_id: turn_id.clone(),
|
||||||
|
item,
|
||||||
|
started_at_ms: Some(started_at_ms),
|
||||||
|
completed_at_ms,
|
||||||
|
};
|
||||||
|
thread
|
||||||
|
.session
|
||||||
|
.send_event_raw(Event {
|
||||||
|
id: turn_id.clone(),
|
||||||
|
msg: EventMsg::ItemCompleted(completed.clone()),
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
for legacy in completed.as_legacy_events(/*show_raw_agent_reasoning*/ false) {
|
||||||
|
thread
|
||||||
|
.session
|
||||||
|
.send_event_raw(Event {
|
||||||
|
id: turn_id.clone(),
|
||||||
|
msg: legacy,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn send_inter_agent_communication_after_capacity_check(
|
async fn send_inter_agent_communication_after_capacity_check(
|
||||||
&self,
|
&self,
|
||||||
agent_id: ThreadId,
|
agent_id: ThreadId,
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ use codex_protocol::config_types::WebSearchMode;
|
|||||||
use codex_protocol::dynamic_tools::DynamicToolResponse;
|
use codex_protocol::dynamic_tools::DynamicToolResponse;
|
||||||
use codex_protocol::dynamic_tools::DynamicToolSpec;
|
use codex_protocol::dynamic_tools::DynamicToolSpec;
|
||||||
use codex_protocol::items::EnteredReviewModeItem;
|
use codex_protocol::items::EnteredReviewModeItem;
|
||||||
|
use codex_protocol::items::SubAgentActivityItem;
|
||||||
use codex_protocol::items::TurnItem;
|
use codex_protocol::items::TurnItem;
|
||||||
use codex_protocol::items::UserMessageItem;
|
use codex_protocol::items::UserMessageItem;
|
||||||
use codex_protocol::models::ActivePermissionProfile;
|
use codex_protocol::models::ActivePermissionProfile;
|
||||||
@@ -126,6 +127,7 @@ use codex_protocol::protocol::MULTI_AGENT_MODE_OPEN_TAG;
|
|||||||
use codex_protocol::protocol::MultiAgentVersion;
|
use codex_protocol::protocol::MultiAgentVersion;
|
||||||
use codex_protocol::protocol::RawResponseItemEvent;
|
use codex_protocol::protocol::RawResponseItemEvent;
|
||||||
use codex_protocol::protocol::SessionSource;
|
use codex_protocol::protocol::SessionSource;
|
||||||
|
use codex_protocol::protocol::SubAgentActivityKind;
|
||||||
use codex_protocol::protocol::SubAgentSource;
|
use codex_protocol::protocol::SubAgentSource;
|
||||||
use codex_protocol::protocol::ThreadHistoryMode;
|
use codex_protocol::protocol::ThreadHistoryMode;
|
||||||
use codex_protocol::protocol::ThreadSettingsOverrides;
|
use codex_protocol::protocol::ThreadSettingsOverrides;
|
||||||
@@ -2016,6 +2018,26 @@ impl Session {
|
|||||||
child_agent_path: &codex_protocol::AgentPath,
|
child_agent_path: &codex_protocol::AgentPath,
|
||||||
status: AgentStatus,
|
status: AgentStatus,
|
||||||
) {
|
) {
|
||||||
|
if matches!(status, AgentStatus::Completed(_))
|
||||||
|
&& let Some(parent_turn_id) = turn_context.turn_metadata_state.parent_turn_id()
|
||||||
|
&& let Err(err) = self
|
||||||
|
.services
|
||||||
|
.agent_control
|
||||||
|
.emit_sub_agent_activity(
|
||||||
|
parent_thread_id,
|
||||||
|
parent_turn_id,
|
||||||
|
SubAgentActivityItem {
|
||||||
|
id: format!("subagent-completed-{}", turn_context.sub_id),
|
||||||
|
kind: SubAgentActivityKind::Completed,
|
||||||
|
agent_thread_id: self.thread_id,
|
||||||
|
agent_path: child_agent_path.clone(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
debug!("failed to emit completed activity to parent thread {parent_thread_id}: {err}");
|
||||||
|
}
|
||||||
|
|
||||||
let Some(parent_agent_path) = child_agent_path
|
let Some(parent_agent_path) = child_agent_path
|
||||||
.as_str()
|
.as_str()
|
||||||
.rsplit_once('/')
|
.rsplit_once('/')
|
||||||
|
|||||||
@@ -268,6 +268,10 @@ impl TurnMetadataState {
|
|||||||
let _ = self.parent_turn_id.set(parent_turn_id);
|
let _ = self.parent_turn_id.set(parent_turn_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn parent_turn_id(&self) -> Option<String> {
|
||||||
|
self.parent_turn_id.get().cloned()
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn set_root_turn_id(&self, root_turn_id: String) {
|
pub(crate) fn set_root_turn_id(&self, root_turn_id: String) {
|
||||||
if root_turn_id.trim().is_empty() {
|
if root_turn_id.trim().is_empty() {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ use codex_history::RolloutItem;
|
|||||||
use codex_models_manager::bundled_models_response;
|
use codex_models_manager::bundled_models_response;
|
||||||
use codex_protocol::ThreadId;
|
use codex_protocol::ThreadId;
|
||||||
use codex_protocol::config_types::ReasoningSummary;
|
use codex_protocol::config_types::ReasoningSummary;
|
||||||
|
use codex_protocol::items::SubAgentActivityItem;
|
||||||
|
use codex_protocol::items::TurnItem;
|
||||||
use codex_protocol::models::PermissionProfile;
|
use codex_protocol::models::PermissionProfile;
|
||||||
use codex_protocol::openai_models::MultiAgentMessages;
|
use codex_protocol::openai_models::MultiAgentMessages;
|
||||||
use codex_protocol::openai_models::MultiAgentRoleMessages;
|
use codex_protocol::openai_models::MultiAgentRoleMessages;
|
||||||
@@ -20,6 +22,7 @@ use codex_protocol::protocol::EventMsg;
|
|||||||
use codex_protocol::protocol::MULTI_AGENT_MODE_OPEN_TAG;
|
use codex_protocol::protocol::MULTI_AGENT_MODE_OPEN_TAG;
|
||||||
use codex_protocol::protocol::Op;
|
use codex_protocol::protocol::Op;
|
||||||
use codex_protocol::protocol::SessionSource;
|
use codex_protocol::protocol::SessionSource;
|
||||||
|
use codex_protocol::protocol::SubAgentActivityKind;
|
||||||
use codex_protocol::protocol::SubAgentSource;
|
use codex_protocol::protocol::SubAgentSource;
|
||||||
use codex_protocol::protocol::ThreadHistoryMode;
|
use codex_protocol::protocol::ThreadHistoryMode;
|
||||||
use codex_protocol::protocol::ThreadSettingsOverrides;
|
use codex_protocol::protocol::ThreadSettingsOverrides;
|
||||||
@@ -59,6 +62,7 @@ use std::time::Duration;
|
|||||||
use test_case::test_case;
|
use test_case::test_case;
|
||||||
use tokio::time::Instant;
|
use tokio::time::Instant;
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
|
use tokio::time::timeout;
|
||||||
use tracing::Level;
|
use tracing::Level;
|
||||||
use tracing_test::internal::MockWriter;
|
use tracing_test::internal::MockWriter;
|
||||||
use wiremock::MockServer;
|
use wiremock::MockServer;
|
||||||
@@ -1982,11 +1986,25 @@ enum CompletionScenario {
|
|||||||
TerminalError,
|
TerminalError,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test_case(CompletionScenario::Completed ; "completed")]
|
#[test_case(
|
||||||
#[test_case(CompletionScenario::TerminalError ; "terminal_error")]
|
CompletionScenario::Completed,
|
||||||
|
ThreadHistoryMode::Paginated;
|
||||||
|
"completed_paginated"
|
||||||
|
)]
|
||||||
|
#[test_case(
|
||||||
|
CompletionScenario::Completed,
|
||||||
|
ThreadHistoryMode::Legacy;
|
||||||
|
"completed_legacy"
|
||||||
|
)]
|
||||||
|
#[test_case(
|
||||||
|
CompletionScenario::TerminalError,
|
||||||
|
ThreadHistoryMode::Paginated;
|
||||||
|
"terminal_error"
|
||||||
|
)]
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
async fn plaintext_multi_agent_v2_completion_sends_agent_message(
|
async fn plaintext_multi_agent_v2_completion_sends_agent_message(
|
||||||
scenario: CompletionScenario,
|
scenario: CompletionScenario,
|
||||||
|
history_mode: ThreadHistoryMode,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let server = start_mock_server().await;
|
let server = start_mock_server().await;
|
||||||
let spawn_args = serde_json::to_string(&json!({
|
let spawn_args = serde_json::to_string(&json!({
|
||||||
@@ -2018,14 +2036,28 @@ async fn plaintext_multi_agent_v2_completion_sends_agent_message(
|
|||||||
};
|
};
|
||||||
let child_request = mount_response_once_match(
|
let child_request = mount_response_once_match(
|
||||||
&server,
|
&server,
|
||||||
|req: &wiremock::Request| request_has_input_type(req, "agent_message"),
|
|req: &wiremock::Request| {
|
||||||
|
request_has_input_type(req, "agent_message")
|
||||||
|
&& decoded_body(req)
|
||||||
|
.and_then(|body| serde_json::from_slice::<Value>(&body).ok())
|
||||||
|
.and_then(|body| {
|
||||||
|
body["client_metadata"]["x-codex-turn-metadata"]
|
||||||
|
.as_str()
|
||||||
|
.and_then(|metadata| serde_json::from_str::<Value>(metadata).ok())
|
||||||
|
})
|
||||||
|
.is_some_and(|metadata| {
|
||||||
|
metadata.get("parent_turn_id").is_some_and(Value::is_string)
|
||||||
|
})
|
||||||
|
},
|
||||||
sse_response(sse(child_events)).set_delay(Duration::from_secs(1)),
|
sse_response(sse(child_events)).set_delay(Duration::from_secs(1)),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
mount_sse_once_match(
|
mount_sse_once_match(
|
||||||
&server,
|
&server,
|
||||||
|req: &wiremock::Request| {
|
|req: &wiremock::Request| {
|
||||||
body_contains(req, SPAWN_CALL_ID) && !body_contains(req, "Message Type: FINAL_ANSWER")
|
body_contains(req, SPAWN_CALL_ID)
|
||||||
|
&& !request_has_input_type(req, "agent_message")
|
||||||
|
&& !body_contains(req, "Message Type: FINAL_ANSWER")
|
||||||
},
|
},
|
||||||
sse(vec![
|
sse(vec![
|
||||||
ev_response_created("resp-parent-2"),
|
ev_response_created("resp-parent-2"),
|
||||||
@@ -2096,12 +2128,106 @@ async fn plaintext_multi_agent_v2_completion_sends_agent_message(
|
|||||||
config.model_provider.stream_max_retries = Some(0);
|
config.model_provider.stream_max_retries = Some(0);
|
||||||
config.model_provider.supports_websockets = false;
|
config.model_provider.supports_websockets = false;
|
||||||
})
|
})
|
||||||
|
.with_history_mode(history_mode)
|
||||||
.build(&server)
|
.build(&server)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
test.submit_turn(TURN_1_PROMPT).await?;
|
test.submit_turn(TURN_1_PROMPT).await?;
|
||||||
let _ = wait_for_requests(&child_request).await?;
|
let deadline = Instant::now() + Duration::from_secs(2);
|
||||||
test.submit_turn(TURN_2_NO_WAIT_PROMPT).await?;
|
let (child_request, child_turn_metadata) = loop {
|
||||||
|
let child_request = child_request.requests().into_iter().find_map(|request| {
|
||||||
|
let body = request.body_json();
|
||||||
|
let turn_metadata: Value =
|
||||||
|
serde_json::from_str(body["client_metadata"]["x-codex-turn-metadata"].as_str()?)
|
||||||
|
.ok()?;
|
||||||
|
turn_metadata
|
||||||
|
.get("parent_turn_id")
|
||||||
|
.and_then(Value::as_str)?;
|
||||||
|
Some((request, turn_metadata))
|
||||||
|
});
|
||||||
|
if let Some(child_request) = child_request {
|
||||||
|
break child_request;
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
Instant::now() < deadline,
|
||||||
|
"timed out waiting for child request"
|
||||||
|
);
|
||||||
|
sleep(Duration::from_millis(10)).await;
|
||||||
|
};
|
||||||
|
let expected_completed_activity = if matches!(scenario, CompletionScenario::Completed) {
|
||||||
|
let child_body = child_request.body_json();
|
||||||
|
let parent_turn_id = child_turn_metadata["parent_turn_id"]
|
||||||
|
.as_str()
|
||||||
|
.expect("child parent turn ID")
|
||||||
|
.to_string();
|
||||||
|
let child_turn_id = child_body["client_metadata"]["turn_id"]
|
||||||
|
.as_str()
|
||||||
|
.expect("child turn ID")
|
||||||
|
.to_string();
|
||||||
|
let child_thread_id = ThreadId::from_string(
|
||||||
|
child_body["client_metadata"]["thread_id"]
|
||||||
|
.as_str()
|
||||||
|
.expect("child thread ID"),
|
||||||
|
)?;
|
||||||
|
Some((parent_turn_id, child_turn_id, child_thread_id))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
test.codex
|
||||||
|
.start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
|
||||||
|
text: TURN_2_NO_WAIT_PROMPT.to_string(),
|
||||||
|
text_elements: Vec::new(),
|
||||||
|
}]))
|
||||||
|
.await?;
|
||||||
|
let (completed_activity_started, completed_activity_completed) =
|
||||||
|
timeout(Duration::from_secs(5), async {
|
||||||
|
let mut active_turn_id = None;
|
||||||
|
let mut completed_activity_started = None;
|
||||||
|
let mut completed_activity_completed = None;
|
||||||
|
loop {
|
||||||
|
let event = test
|
||||||
|
.codex
|
||||||
|
.next_event()
|
||||||
|
.await
|
||||||
|
.expect("event stream should remain open");
|
||||||
|
match event.msg {
|
||||||
|
EventMsg::TurnStarted(event) => {
|
||||||
|
active_turn_id = Some(event.turn_id);
|
||||||
|
}
|
||||||
|
EventMsg::ItemStarted(event)
|
||||||
|
if matches!(
|
||||||
|
&event.item,
|
||||||
|
TurnItem::SubAgentActivity(SubAgentActivityItem {
|
||||||
|
kind: SubAgentActivityKind::Completed,
|
||||||
|
..
|
||||||
|
})
|
||||||
|
) =>
|
||||||
|
{
|
||||||
|
completed_activity_started = Some(event);
|
||||||
|
}
|
||||||
|
EventMsg::ItemCompleted(event)
|
||||||
|
if matches!(
|
||||||
|
&event.item,
|
||||||
|
TurnItem::SubAgentActivity(SubAgentActivityItem {
|
||||||
|
kind: SubAgentActivityKind::Completed,
|
||||||
|
..
|
||||||
|
})
|
||||||
|
) =>
|
||||||
|
{
|
||||||
|
completed_activity_completed = Some(event);
|
||||||
|
}
|
||||||
|
EventMsg::TurnComplete(event)
|
||||||
|
if active_turn_id.as_deref() == Some(event.turn_id.as_str()) =>
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(completed_activity_started, completed_activity_completed)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("timed out waiting for parent turn completion");
|
||||||
|
|
||||||
let request = wait_for_requests(&agent_request)
|
let request = wait_for_requests(&agent_request)
|
||||||
.await?
|
.await?
|
||||||
@@ -2122,6 +2248,85 @@ async fn plaintext_multi_agent_v2_completion_sends_agent_message(
|
|||||||
})])
|
})])
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if let Some((parent_turn_id, child_turn_id, child_thread_id)) = expected_completed_activity {
|
||||||
|
let started = completed_activity_started.expect("completed activity start event");
|
||||||
|
let completed_event =
|
||||||
|
completed_activity_completed.expect("completed activity completion event");
|
||||||
|
let TurnItem::SubAgentActivity(started_item) = &started.item else {
|
||||||
|
panic!("expected started sub-agent activity");
|
||||||
|
};
|
||||||
|
let TurnItem::SubAgentActivity(completed_event_item) = &completed_event.item else {
|
||||||
|
panic!("expected completed sub-agent activity");
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
(
|
||||||
|
started.thread_id,
|
||||||
|
&started.turn_id,
|
||||||
|
started_item,
|
||||||
|
Some(started.started_at_ms),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
completed_event.thread_id,
|
||||||
|
&completed_event.turn_id,
|
||||||
|
completed_event_item,
|
||||||
|
completed_event.started_at_ms,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assert_eq!(started.turn_id, parent_turn_id);
|
||||||
|
|
||||||
|
test.codex.ensure_rollout_materialized().await;
|
||||||
|
test.codex.flush_rollout().await?;
|
||||||
|
let rollout = codex_rollout::RolloutRecorder::get_rollout_history(
|
||||||
|
&test.codex.rollout_path().expect("parent rollout path"),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
assert!(
|
||||||
|
!rollout.get_rollout_items().iter().any(|item| matches!(
|
||||||
|
item,
|
||||||
|
RolloutItem::EventMsg(EventMsg::SubAgentActivity(activity))
|
||||||
|
if activity.kind == SubAgentActivityKind::Completed
|
||||||
|
)),
|
||||||
|
"legacy completed activity should not be persisted without its parent turn ID"
|
||||||
|
);
|
||||||
|
let completed = rollout
|
||||||
|
.get_rollout_items()
|
||||||
|
.iter()
|
||||||
|
.find_map(|item| match item {
|
||||||
|
RolloutItem::EventMsg(EventMsg::ItemCompleted(completed))
|
||||||
|
if matches!(
|
||||||
|
&completed.item,
|
||||||
|
TurnItem::SubAgentActivity(SubAgentActivityItem {
|
||||||
|
kind: SubAgentActivityKind::Completed,
|
||||||
|
..
|
||||||
|
})
|
||||||
|
) =>
|
||||||
|
{
|
||||||
|
Some(completed)
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.expect("persisted completed sub-agent activity");
|
||||||
|
|
||||||
|
assert_eq!(completed.turn_id, parent_turn_id);
|
||||||
|
let TurnItem::SubAgentActivity(completed_item) = &completed.item else {
|
||||||
|
panic!("expected completed sub-agent activity");
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
completed_item,
|
||||||
|
&SubAgentActivityItem {
|
||||||
|
id: format!("subagent-completed-{child_turn_id}"),
|
||||||
|
kind: SubAgentActivityKind::Completed,
|
||||||
|
agent_thread_id: child_thread_id,
|
||||||
|
agent_path: codex_protocol::AgentPath::root()
|
||||||
|
.join("worker")
|
||||||
|
.expect("worker path"),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
assert!(completed_activity_started.is_none());
|
||||||
|
assert!(completed_activity_completed.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4117,6 +4117,7 @@ pub enum SubAgentActivityKind {
|
|||||||
Started,
|
Started,
|
||||||
Interacted,
|
Interacted,
|
||||||
Interrupted,
|
Interrupted,
|
||||||
|
Completed,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ use codex_protocol::protocol::PatchApplyBeginEvent;
|
|||||||
use codex_protocol::protocol::PatchApplyEndEvent;
|
use codex_protocol::protocol::PatchApplyEndEvent;
|
||||||
use codex_protocol::protocol::PatchApplyStatus;
|
use codex_protocol::protocol::PatchApplyStatus;
|
||||||
use codex_protocol::protocol::SubAgentActivityEvent;
|
use codex_protocol::protocol::SubAgentActivityEvent;
|
||||||
|
use codex_protocol::protocol::SubAgentActivityKind;
|
||||||
use codex_protocol::protocol::TurnAbortReason;
|
use codex_protocol::protocol::TurnAbortReason;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
@@ -352,11 +353,14 @@ pub(crate) fn tool_runtime_trace_event(event: &EventMsg) -> Option<ToolRuntimeTr
|
|||||||
status: ExecutionStatus::Completed,
|
status: ExecutionStatus::Completed,
|
||||||
payload: ToolRuntimePayload::CollabCloseEnd(event),
|
payload: ToolRuntimePayload::CollabCloseEnd(event),
|
||||||
}),
|
}),
|
||||||
EventMsg::SubAgentActivity(event) => Some(ToolRuntimeTraceEvent::Ended {
|
EventMsg::SubAgentActivity(event) if event.kind != SubAgentActivityKind::Completed => {
|
||||||
tool_call_id: &event.event_id,
|
Some(ToolRuntimeTraceEvent::Ended {
|
||||||
status: ExecutionStatus::Completed,
|
tool_call_id: &event.event_id,
|
||||||
payload: ToolRuntimePayload::SubAgentActivity(event),
|
status: ExecutionStatus::Completed,
|
||||||
}),
|
payload: ToolRuntimePayload::SubAgentActivity(event),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
EventMsg::SubAgentActivity(_) => None,
|
||||||
EventMsg::Error(_)
|
EventMsg::Error(_)
|
||||||
| EventMsg::Warning(_)
|
| EventMsg::Warning(_)
|
||||||
| EventMsg::GuardianWarning(_)
|
| EventMsg::GuardianWarning(_)
|
||||||
|
|||||||
@@ -50,6 +50,20 @@ fn sub_agent_activity_is_a_terminal_tool_runtime_event() -> anyhow::Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn completed_sub_agent_activity_is_not_a_tool_runtime_event() -> anyhow::Result<()> {
|
||||||
|
let event = EventMsg::SubAgentActivity(SubAgentActivityEvent {
|
||||||
|
event_id: "child-turn-completed".to_string(),
|
||||||
|
occurred_at_ms: 1234,
|
||||||
|
agent_thread_id: ThreadId::new(),
|
||||||
|
agent_path: AgentPath::try_from("/root/reviewer").map_err(anyhow::Error::msg)?,
|
||||||
|
kind: SubAgentActivityKind::Completed,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert!(tool_runtime_trace_event(&event).is_none());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn exec_command_trace_payloads_use_inferred_native_cwd() -> anyhow::Result<()> {
|
fn exec_command_trace_payloads_use_inferred_native_cwd() -> anyhow::Result<()> {
|
||||||
// Convention inference depends on the URI spelling, not the test host, so exercise both
|
// Convention inference depends on the URI spelling, not the test host, so exercise both
|
||||||
|
|||||||
@@ -235,6 +235,7 @@ impl TraceReducer {
|
|||||||
target_thread_id,
|
target_thread_id,
|
||||||
Some(wall_time_unix_ms),
|
Some(wall_time_unix_ms),
|
||||||
),
|
),
|
||||||
|
(_, SubAgentActivityKind::Completed) => Ok(()),
|
||||||
_ => bail!(
|
_ => bail!(
|
||||||
"sub-agent activity {:?} does not match tool call kind {tool_kind:?}",
|
"sub-agent activity {:?} does not match tool call kind {tool_kind:?}",
|
||||||
payload.kind
|
payload.kind
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use crate::protocol::EventMsg;
|
|||||||
use codex_extension_items::ExtensionItem;
|
use codex_extension_items::ExtensionItem;
|
||||||
use codex_protocol::items::TurnItem;
|
use codex_protocol::items::TurnItem;
|
||||||
use codex_protocol::models::ResponseItem;
|
use codex_protocol::models::ResponseItem;
|
||||||
|
use codex_protocol::protocol::SubAgentActivityKind;
|
||||||
use codex_protocol::protocol::ThreadHistoryMode;
|
use codex_protocol::protocol::ThreadHistoryMode;
|
||||||
|
|
||||||
/// Whether a rollout `item` should be persisted in rollout files.
|
/// Whether a rollout `item` should be persisted in rollout files.
|
||||||
@@ -89,12 +90,18 @@ pub fn should_persist_event_msg(ev: &EventMsg, history_mode: ThreadHistoryMode)
|
|||||||
match ev {
|
match ev {
|
||||||
EventMsg::ItemCompleted(event) => {
|
EventMsg::ItemCompleted(event) => {
|
||||||
// Paginated rollouts store TurnItems.
|
// Paginated rollouts store TurnItems.
|
||||||
// Legacy rollouts keep only items with no raw ResponseItem or legacy equivalent.
|
// Legacy rollouts keep only items with no lossless raw ResponseItem or legacy
|
||||||
|
// equivalent.
|
||||||
matches!(history_mode, ThreadHistoryMode::Paginated)
|
matches!(history_mode, ThreadHistoryMode::Paginated)
|
||||||
|| matches!(
|
|| matches!(
|
||||||
event.item,
|
event.item,
|
||||||
TurnItem::Plan(_) | TurnItem::Extension(ExtensionItem::Sleep(_))
|
TurnItem::Plan(_) | TurnItem::Extension(ExtensionItem::Sleep(_))
|
||||||
)
|
)
|
||||||
|
|| matches!(
|
||||||
|
&event.item,
|
||||||
|
TurnItem::SubAgentActivity(item)
|
||||||
|
if item.kind == SubAgentActivityKind::Completed
|
||||||
|
)
|
||||||
}
|
}
|
||||||
EventMsg::TokenCount(_)
|
EventMsg::TokenCount(_)
|
||||||
| EventMsg::ThreadGoalUpdated(_)
|
| EventMsg::ThreadGoalUpdated(_)
|
||||||
@@ -116,8 +123,13 @@ pub fn should_persist_event_msg(ev: &EventMsg, history_mode: ThreadHistoryMode)
|
|||||||
| EventMsg::ContextCompacted(_)
|
| EventMsg::ContextCompacted(_)
|
||||||
| EventMsg::McpToolCallEnd(_)
|
| EventMsg::McpToolCallEnd(_)
|
||||||
| EventMsg::WebSearchEnd(_)
|
| EventMsg::WebSearchEnd(_)
|
||||||
| EventMsg::ImageGenerationEnd(_)
|
| EventMsg::ImageGenerationEnd(_) => {
|
||||||
| EventMsg::SubAgentActivity(_) => matches!(history_mode, ThreadHistoryMode::Legacy),
|
matches!(history_mode, ThreadHistoryMode::Legacy)
|
||||||
|
}
|
||||||
|
EventMsg::SubAgentActivity(event) => {
|
||||||
|
matches!(history_mode, ThreadHistoryMode::Legacy)
|
||||||
|
&& event.kind != SubAgentActivityKind::Completed
|
||||||
|
}
|
||||||
|
|
||||||
// Transient, non-durable events.
|
// Transient, non-durable events.
|
||||||
EventMsg::Error(_)
|
EventMsg::Error(_)
|
||||||
|
|||||||
@@ -173,6 +173,7 @@ fn activity_summary(item: &ThreadItem) -> Option<String> {
|
|||||||
SubAgentActivityKind::Started => "Started",
|
SubAgentActivityKind::Started => "Started",
|
||||||
SubAgentActivityKind::Interacted => "Contacted",
|
SubAgentActivityKind::Interacted => "Contacted",
|
||||||
SubAgentActivityKind::Interrupted => "Interrupted",
|
SubAgentActivityKind::Interrupted => "Interrupted",
|
||||||
|
SubAgentActivityKind::Completed => "Completed",
|
||||||
};
|
};
|
||||||
return bounded_summary(&format!("{action} {agent_path}"));
|
return bounded_summary(&format!("{action} {agent_path}"));
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
source: tui/src/chatwidget/tests.rs
|
source: tui/src/chatwidget/tests/app_server.rs
|
||||||
expression: rendered
|
expression: rendered
|
||||||
---
|
---
|
||||||
• Interacted with `/root/researcher`
|
• Completed `/root/researcher`
|
||||||
@@ -963,7 +963,7 @@ async fn live_app_server_sub_agent_activity_renders_once() {
|
|||||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||||
let activity = AppServerThreadItem::SubAgentActivity {
|
let activity = AppServerThreadItem::SubAgentActivity {
|
||||||
id: "activity-1".to_string(),
|
id: "activity-1".to_string(),
|
||||||
kind: codex_app_server_protocol::SubAgentActivityKind::Interacted,
|
kind: codex_app_server_protocol::SubAgentActivityKind::Completed,
|
||||||
agent_thread_id: ThreadId::new().to_string(),
|
agent_thread_id: ThreadId::new().to_string(),
|
||||||
agent_path: "/root/researcher".to_string(),
|
agent_path: "/root/researcher".to_string(),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -291,7 +291,7 @@ pub(crate) fn sub_agent_activity_display(item: &ThreadItem) -> Option<SubAgentAc
|
|||||||
let is_running_hint = match kind {
|
let is_running_hint = match kind {
|
||||||
SubAgentActivityKind::Started => true,
|
SubAgentActivityKind::Started => true,
|
||||||
SubAgentActivityKind::Interacted => return None,
|
SubAgentActivityKind::Interacted => return None,
|
||||||
SubAgentActivityKind::Interrupted => false,
|
SubAgentActivityKind::Interrupted | SubAgentActivityKind::Completed => false,
|
||||||
};
|
};
|
||||||
Some(SubAgentActivityDisplay {
|
Some(SubAgentActivityDisplay {
|
||||||
thread_id: parse_thread_id(agent_thread_id)?,
|
thread_id: parse_thread_id(agent_thread_id)?,
|
||||||
@@ -318,6 +318,7 @@ pub(crate) fn sub_agent_activity_summary(kind: SubAgentActivityKind, agent_path:
|
|||||||
SubAgentActivityKind::Started => format!("Started `{agent_path}`"),
|
SubAgentActivityKind::Started => format!("Started `{agent_path}`"),
|
||||||
SubAgentActivityKind::Interacted => format!("Interacted with `{agent_path}`"),
|
SubAgentActivityKind::Interacted => format!("Interacted with `{agent_path}`"),
|
||||||
SubAgentActivityKind::Interrupted => format!("Interrupted `{agent_path}`"),
|
SubAgentActivityKind::Interrupted => format!("Interrupted `{agent_path}`"),
|
||||||
|
SubAgentActivityKind::Completed => format!("Completed `{agent_path}`"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,6 +327,7 @@ fn sub_agent_activity_title(kind: SubAgentActivityKind, agent_path: &str) -> Lin
|
|||||||
SubAgentActivityKind::Started => ("Started ", agent_path),
|
SubAgentActivityKind::Started => ("Started ", agent_path),
|
||||||
SubAgentActivityKind::Interacted => ("Interacted with ", agent_path),
|
SubAgentActivityKind::Interacted => ("Interacted with ", agent_path),
|
||||||
SubAgentActivityKind::Interrupted => ("Interrupted ", agent_path),
|
SubAgentActivityKind::Interrupted => ("Interrupted ", agent_path),
|
||||||
|
SubAgentActivityKind::Completed => ("Completed ", agent_path),
|
||||||
};
|
};
|
||||||
title_spans_line(vec![
|
title_spans_line(vec![
|
||||||
Span::from(prefix).bold(),
|
Span::from(prefix).bold(),
|
||||||
@@ -691,6 +693,26 @@ mod tests {
|
|||||||
assert_eq!(sub_agent_activity_display(&item), None);
|
assert_eq!(sub_agent_activity_display(&item), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn completed_sub_agent_activity_stops_running_liveness() {
|
||||||
|
let thread_id = ThreadId::new();
|
||||||
|
let item = ThreadItem::SubAgentActivity {
|
||||||
|
id: "activity-1".to_string(),
|
||||||
|
kind: SubAgentActivityKind::Completed,
|
||||||
|
agent_thread_id: thread_id.to_string(),
|
||||||
|
agent_path: "/root/child".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
sub_agent_activity_display(&item),
|
||||||
|
Some(SubAgentActivityDisplay {
|
||||||
|
thread_id,
|
||||||
|
agent_path: "/root/child".to_string(),
|
||||||
|
is_running_hint: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn collab_events_snapshot() {
|
fn collab_events_snapshot() {
|
||||||
let sender_thread_id = ThreadId::from_string("00000000-0000-0000-0000-000000000001")
|
let sender_thread_id = ThreadId::from_string("00000000-0000-0000-0000-000000000001")
|
||||||
|
|||||||
Reference in new issue
Block a user