Files
codex/codex-rs/rollout-trace/src/protocol_event_tests.rs
T
Steve Coffey b0a6b8d86f Record resolved model and reasoning effort in sub-agent activity (#51463)
## What changed

Add `model` and `reasoningEffort` to `subAgentActivity` items for multi-agent v2 spawns. Capture startup settings before the child begins its turn and resolve the reported effort against model metadata, including model defaults, while preserving the child's configured effort.

Carry these fields through live notifications, persisted activity, thread history, and legacy rollout migration. Update the JSON schemas and generated TypeScript and Python types. Older records and other activity kinds retain null values.

## Testing

Add coverage for resolved spawn settings matching the child's request across legacy and paginated history, preserving configured effort, forwarding metadata through events and history, and reading and migrating older records without the new fields.

GitOrigin-RevId: a9e4241ae21bb8db1ead293f78ffba8257cca0ae
2026-10-06 19:29:40 +00:00

152 lines
5.2 KiB
Rust

use codex_protocol::AgentPath;
use codex_protocol::ThreadId;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::ExecCommandBeginEvent;
use codex_protocol::protocol::ExecCommandEndEvent;
use codex_protocol::protocol::ExecCommandSource;
use codex_protocol::protocol::ExecCommandStatus;
use codex_protocol::protocol::SubAgentActivityEvent;
use codex_protocol::protocol::SubAgentActivityKind;
use pretty_assertions::assert_eq;
use serde_json::json;
use std::time::Duration;
use super::ToolRuntimeTraceEvent;
use super::tool_runtime_trace_event;
use crate::ExecutionStatus;
#[test]
fn sub_agent_activity_is_a_terminal_tool_runtime_event() -> anyhow::Result<()> {
let agent_thread_id = ThreadId::new();
let event = EventMsg::SubAgentActivity(SubAgentActivityEvent {
model: Some("gpt-5".into()),
reasoning_effort: Some(codex_protocol::openai_models::ReasoningEffort::High),
event_id: "call-spawn".to_string(),
occurred_at_ms: 1234,
agent_thread_id,
agent_path: AgentPath::try_from("/root/reviewer").map_err(anyhow::Error::msg)?,
kind: SubAgentActivityKind::Started,
});
let Some(ToolRuntimeTraceEvent::Ended {
tool_call_id,
status,
payload,
}) = tool_runtime_trace_event(&event)
else {
panic!("expected terminal tool runtime event");
};
assert_eq!(tool_call_id, "call-spawn");
assert_eq!(status, ExecutionStatus::Completed);
assert_eq!(
serde_json::to_value(payload)?,
json!({
"event_id": "call-spawn",
"occurred_at_ms": 1234,
"agent_thread_id": agent_thread_id,
"agent_path": "/root/reviewer",
"kind": "started",
"model": "gpt-5",
"reasoning_effort": "high"
})
);
Ok(())
}
#[test]
fn completed_sub_agent_activity_is_not_a_tool_runtime_event() -> anyhow::Result<()> {
let event = EventMsg::SubAgentActivity(SubAgentActivityEvent {
model: None,
reasoning_effort: None,
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]
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
// Windows and POSIX paths on every platform.
let begin = EventMsg::ExecCommandBegin(ExecCommandBeginEvent {
call_id: "call-begin".to_string(),
plugin_id: Some("sample@openai-curated".to_string()),
script_path: Some("scripts/run.py".to_string()),
process_id: Some("process-1".to_string()),
turn_id: "turn-1".to_string(),
started_at_ms: 1234,
command: vec!["pwd".to_string()],
cwd: "file:///C:/windows".parse()?,
parsed_cmd: Vec::new(),
source: ExecCommandSource::Agent,
interaction_input: None,
});
let end = EventMsg::ExecCommandEnd(ExecCommandEndEvent {
call_id: "call-end".to_string(),
plugin_id: Some("sample@openai-curated".to_string()),
script_path: Some("scripts/run.py".to_string()),
process_id: None,
turn_id: "turn-1".to_string(),
completed_at_ms: 2345,
command: vec!["pwd".to_string()],
cwd: "file:///workspace/project".parse()?,
parsed_cmd: Vec::new(),
source: ExecCommandSource::UnifiedExecInteraction,
interaction_input: Some("input".to_string()),
aggregated_output: "combined output".to_string(),
exit_code: 0,
duration: Duration::from_millis(250),
status: ExecCommandStatus::Completed,
});
let Some(ToolRuntimeTraceEvent::Started { payload, .. }) = tool_runtime_trace_event(&begin)
else {
panic!("expected started tool runtime event");
};
assert_eq!(
serde_json::to_value(payload)?,
json!({
"call_id": "call-begin",
"plugin_id": "sample@openai-curated",
"script_path": "scripts/run.py",
"process_id": "process-1",
"turn_id": "turn-1",
"started_at_ms": 1234,
"command": ["pwd"],
"cwd": r"C:\windows",
"parsed_cmd": [],
"source": "agent"
})
);
let Some(ToolRuntimeTraceEvent::Ended { payload, .. }) = tool_runtime_trace_event(&end) else {
panic!("expected ended tool runtime event");
};
assert_eq!(
serde_json::to_value(payload)?,
json!({
"call_id": "call-end",
"plugin_id": "sample@openai-curated",
"script_path": "scripts/run.py",
"turn_id": "turn-1",
"completed_at_ms": 2345,
"command": ["pwd"],
"cwd": "/workspace/project",
"parsed_cmd": [],
"source": "unified_exec_interaction",
"interaction_input": "input",
"aggregated_output": "combined output",
"exit_code": 0,
"duration": {"secs": 0, "nanos": 250000000},
"status": "completed"
})
);
Ok(())
}