use std::{
collections::HashMap,
path::{Path, PathBuf},
};
use chrono::{DateTime, Datelike, SecondsFormat, Utc};
use serde_json::{Value, json};
use crate::{
sessions::IngestEvent,
wire::{Message, Part, PartKind, Provenance, ProviderOptions, Session},
};
use super::{
Adapter, AdapterError, AdapterFactory, AdapterYieldStream, DiscoverFuture, Env,
RestoreFidelity, RestoredFile, SkipOracle, by_timestamp_then_id, compact_json, config_path,
empty_options,
extract::{Extracted, extract_compact_repr, extract_raw_record, extract_self_str, extract_str},
extracted_text,
jsonl::{BoundedRow, JsonlTree, jsonl_tree_discover, jsonl_tree_events},
jsonl_bytes, part_id, part_ordinal, raw_record,
};
const NAME: &str = "codex-cli";
pub struct CodexCliFactory;
impl AdapterFactory for CodexCliFactory {
fn name(&self) -> &'static str {
NAME
}
fn open(&self, config: Value) -> Result<Box<dyn Adapter>, AdapterError> {
Ok(Box::new(CodexCliAdapter::new(config_path(NAME, config)?)))
}
fn probe_default(&self, env: &Env) -> Option<Value> {
let path = env.home.join(".codex").join("sessions");
path.exists().then(|| json!({ "path": path }))
}
fn serialize(
&self,
session: &crate::sessions::SessionWithMessages,
fidelity: RestoreFidelity,
) -> Result<Vec<RestoredFile>, AdapterError> {
serialize_session(session, fidelity)
}
}
fn serialize_session(
session: &crate::sessions::SessionWithMessages,
fidelity: RestoreFidelity,
) -> Result<Vec<RestoredFile>, AdapterError> {
let mut records = Vec::new();
if fidelity == RestoreFidelity::Native
&& let Some(raw) = raw_record(&session.session.options)
{
records.push(raw);
} else {
records.push(codex_session_meta(session));
}
let mut messages = session.messages.clone();
messages.sort_by(by_timestamp_then_id);
for message in &messages {
if fidelity == RestoreFidelity::Native
&& let Some(raw) = raw_record(message.message.options())
{
records.push(raw);
continue;
}
if matches!(message.message, Message::System { .. }) {
continue;
}
records.push(codex_response_item(message));
}
Ok(vec![RestoredFile::new(
codex_relative_path(session),
jsonl_bytes(NAME, &records)?,
fidelity,
)])
}
fn codex_relative_path(session: &crate::sessions::SessionWithMessages) -> PathBuf {
let ts = session.session.created_at;
let filename_ts = ts.format("%Y-%m-%dT%H-%M-%S");
PathBuf::from("sessions")
.join(format!("{:04}", ts.year()))
.join(format!("{:02}", ts.month()))
.join(format!("{:02}", ts.day()))
.join(format!(
"rollout-{filename_ts}-{}.jsonl",
session.session.id
))
}
fn codex_session_meta(session: &crate::sessions::SessionWithMessages) -> Value {
json!({
"timestamp": session.session.created_at.to_rfc3339_opts(SecondsFormat::Millis, true),
"type": "session_meta",
"payload": {
"id": session.session.id,
"timestamp": session.session.created_at.to_rfc3339_opts(SecondsFormat::Millis, true),
"cwd": &*session.session.project,
}
})
}
fn codex_response_item(message: &crate::sessions::MessageWithParts) -> Value {
json!({
"timestamp": message.message.timestamp().to_rfc3339_opts(SecondsFormat::Millis, true),
"type": "response_item",
"payload": codex_payload(message),
})
}
fn codex_payload(message: &crate::sessions::MessageWithParts) -> Value {
if let Some(part) = message.parts.first() {
match &part.kind {
PartKind::ToolCall {
call_id,
name,
params,
..
} if matches!(message.message, Message::Assistant { .. }) => {
return json!({
"type": "function_call",
"call_id": extracted_text(call_id),
"name": extracted_text(name),
"arguments": compact_json(params),
});
}
PartKind::ToolResult {
call_id, result, ..
} if matches!(message.message, Message::Tool { .. }) => {
return json!({
"type": "function_call_output",
"call_id": extracted_text(call_id),
"output": result,
});
}
PartKind::Reasoning { text }
if matches!(message.message, Message::Assistant { .. }) =>
{
if let Some(text) = text
&& let Ok(value) = serde_json::from_str::<Value>(text.as_ref())
{
return value;
}
return json!({
"type": "reasoning",
"summary": [{"type": "summary_text", "text": extracted_text(text)}],
});
}
_ => {}
}
}
let is_assistant = matches!(message.message, Message::Assistant { .. });
json!({
"type": "message",
"role": match message.message.role() {
crate::wire::Role::System => "developer",
crate::wire::Role::User => "user",
crate::wire::Role::Assistant => "assistant",
crate::wire::Role::Tool => "tool",
},
"content": message
.parts
.iter()
.map(|part| codex_content_part(part, is_assistant))
.collect::<Vec<_>>(),
})
}
fn codex_content_part(part: &Part, is_assistant: bool) -> Value {
let text_type = if is_assistant {
"output_text"
} else {
"input_text"
};
match &part.kind {
PartKind::Text { text } => json!({
"type": text_type,
"text": extracted_text(text),
}),
PartKind::File { data, .. } => json!({
"type": text_type,
"text": match data {
crate::wire::FileData::String(value) => value.clone(),
crate::wire::FileData::Bytes(value) => format!("<{} bytes>", value.len()),
crate::wire::FileData::Url(value) => value.clone(),
},
}),
other => json!({
"type": text_type,
"text": compact_json(&serde_json::to_value(other).unwrap_or(Value::Null)),
}),
}
}
#[derive(Debug, Clone)]
pub struct CodexCliAdapter {
root: PathBuf,
}
impl CodexCliAdapter {
pub fn new(root: impl Into<PathBuf>) -> Self {
Self { root: root.into() }
}
}
impl Adapter for CodexCliAdapter {
fn discover(&self) -> DiscoverFuture<'_> {
jsonl_tree_discover(self)
}
fn events_with<'a>(&'a self, oracle: &'a dyn SkipOracle) -> AdapterYieldStream<'a> {
jsonl_tree_events(self, oracle)
}
}
impl JsonlTree for CodexCliAdapter {
type State = HashMap<String, Extracted<String>>;
fn name(&self) -> &'static str {
NAME
}
fn root(&self) -> &Path {
&self.root
}
fn peek_session_id(&self, _path: &Path, first_line: &str) -> Option<String> {
let row: Value = serde_json::from_str(first_line).ok()?;
if row.get("type").and_then(Value::as_str) == Some("session_meta") {
row.get("payload")?
.get("id")?
.as_str()
.map(ToOwned::to_owned)
} else if is_legacy_session_row(&row) {
row.get("id")?.as_str().map(ToOwned::to_owned)
} else {
None
}
}
fn session(&self, path: &Path, rows: &[BoundedRow]) -> Result<Session, AdapterError> {
session_from_rows(path, rows)
}
fn events_from_row(
&self,
session: &Session,
row: &BoundedRow,
state: &mut Self::State,
) -> Result<Vec<IngestEvent>, String> {
capture_tool_call_name(&row.value, state);
events_from_row(&session.id, row.line, &row.value, session.created_at, state)
}
}
fn is_legacy_session_row(row: &Value) -> bool {
row.get("type").is_none() && row.get("id").is_some()
}
fn session_from_rows(path: &Path, rows: &[BoundedRow]) -> Result<Session, AdapterError> {
let path_display = path.display().to_string();
let first = rows
.first()
.ok_or_else(|| AdapterError::schema(NAME, path_display.clone(), "empty jsonl session"))?;
let row = &first.value;
let at_first = format!("{path_display}:{}", first.line);
let payload = if row.get("type").and_then(Value::as_str) == Some("session_meta") {
row.get("payload").cloned().unwrap_or(Value::Null)
} else if is_legacy_session_row(row) {
row.clone()
} else {
return Err(AdapterError::schema(
NAME,
at_first,
"first row must be session_meta",
));
};
let id = payload
.get("id")
.and_then(Value::as_str)
.ok_or_else(|| {
AdapterError::schema(NAME, at_first.clone(), "session_meta missing payload.id")
})?
.to_owned();
let created_at = payload
.get("timestamp")
.and_then(Value::as_str)
.and_then(|text| DateTime::parse_from_rfc3339(text).ok())
.map(|dt| dt.with_timezone(&Utc))
.or_else(|| {
row.get("timestamp")
.and_then(Value::as_str)
.and_then(|text| DateTime::parse_from_rfc3339(text).ok())
.map(|dt| dt.with_timezone(&Utc))
})
.ok_or_else(|| {
AdapterError::schema(NAME, at_first, "session_meta has no parseable timestamp")
})?;
let project = match extract_str(&payload, "cwd") {
Some(value) => value,
None => {
let path_str = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(path_display.as_str())
.to_owned();
extract_self_str(&Value::String(path_str)).ok_or_else(|| {
AdapterError::schema(
NAME,
path_display.clone(),
"internal: Value::String produced None from Source::as_str",
)
})?
}
};
let mut options = ProviderOptions::new();
options.insert(
"source".to_owned(),
json!({
"adapter": "codex-cli",
"originator": payload.get("originator"),
"cli_version": payload.get("cli_version"),
"model_provider": payload.get("model_provider"),
"git": payload.get("git"),
"base_instructions": payload.get("base_instructions"),
"instructions": payload.get("instructions"),
"source": payload.get("source"),
"raw_record": extract_raw_record(row),
}),
);
Ok(Session {
id,
parent_session_id: None,
parent_message_id: None,
source_agent: "codex-cli".to_owned(),
created_at,
project,
options,
})
}
fn events_from_row(
session_id: &str,
line: usize,
row: &Value,
default_timestamp: DateTime<Utc>,
tool_call_names: &HashMap<String, Extracted<String>>,
) -> Result<Vec<IngestEvent>, String> {
let kind = row.get("type").and_then(Value::as_str);
if kind == Some("session_meta")
|| is_legacy_session_row(row)
|| (kind.is_none() && row.get("record_type").is_some())
{
return Ok(Vec::new());
}
let (payload, timestamp) = if kind == Some("response_item") {
let timestamp = row
.get("timestamp")
.and_then(Value::as_str)
.and_then(|text| DateTime::parse_from_rfc3339(text).ok())
.map(|dt| dt.with_timezone(&Utc))
.unwrap_or(default_timestamp);
(row.get("payload").unwrap_or(&Value::Null), timestamp)
} else {
(row, default_timestamp)
};
let payload_type = payload.get("type").and_then(Value::as_str).unwrap_or("");
let message_id = format!("{session_id}:{line:06}");
match payload_type {
"message" => message_events(session_id, &message_id, timestamp, payload, row),
"function_call" => Ok(tool_call_events(
session_id,
&message_id,
timestamp,
payload,
row,
)),
"function_call_output" => Ok(tool_result_events(
session_id,
&message_id,
timestamp,
payload,
row,
tool_call_names,
)),
"reasoning" => Ok(reasoning_events(
session_id,
&message_id,
timestamp,
payload,
row,
)),
"custom_tool_call" => Ok(custom_tool_call_events(
session_id,
&message_id,
timestamp,
payload,
row,
)),
"custom_tool_call_output" => Ok(custom_tool_result_events(
session_id,
&message_id,
timestamp,
payload,
row,
)),
_ => Ok(vec![raw_carrier_event(session_id, line, row, timestamp)]),
}
}
fn row_options(row: &Value) -> ProviderOptions {
let mut options = ProviderOptions::new();
options.insert(
"source".to_owned(),
json!({ "raw_record": extract_raw_record(row) }),
);
options
}
fn raw_carrier_event(
session_id: &str,
line: usize,
row: &Value,
timestamp: DateTime<Utc>,
) -> IngestEvent {
IngestEvent::Message(Message::System {
id: row
.get("id")
.and_then(Value::as_str)
.map_or_else(|| format!("{session_id}:{line:06}:raw"), ToOwned::to_owned),
session_id: session_id.to_owned(),
timestamp: row
.get("timestamp")
.and_then(Value::as_str)
.and_then(|text| DateTime::parse_from_rfc3339(text).ok())
.map(|dt| dt.with_timezone(&Utc))
.unwrap_or(timestamp),
content: None,
options: row_options(row),
})
}
fn capture_tool_call_name(row: &Value, map: &mut HashMap<String, Extracted<String>>) {
let payload = match row.get("type").and_then(Value::as_str) {
Some("response_item") => row.get("payload"),
Some(_) => Some(row),
None => None,
};
let Some(payload) = payload else {
return;
};
if payload.get("type").and_then(Value::as_str) != Some("function_call") {
return;
}
let Some(call_id) = payload.get("call_id").and_then(Value::as_str) else {
return;
};
let Some(name) = extract_str(payload, "name") else {
return;
};
map.insert(call_id.to_owned(), name);
}
fn message_events(
session_id: &str,
message_id: &str,
timestamp: DateTime<Utc>,
payload: &Value,
row: &Value,
) -> Result<Vec<IngestEvent>, String> {
let role = payload
.get("role")
.and_then(Value::as_str)
.ok_or_else(|| "message missing role".to_owned())?;
let content = payload
.get("content")
.and_then(Value::as_array)
.cloned()
.unwrap_or_default();
let provenance = message_provenance(role, &content);
let mut parts = Vec::with_capacity(content.len());
for (ordinal, item) in content.iter().enumerate() {
let text = extract_str(item, "text").or_else(|| Some(extract_compact_repr(item)));
parts.push(Part {
session_id: session_id.to_owned(),
id: part_id(message_id, ordinal),
message_id: message_id.to_owned(),
ordinal: part_ordinal(ordinal),
provenance,
options: empty_options(),
kind: PartKind::Text { text },
});
}
let (message, keep_parts) = match role {
"user" => (
Message::User {
id: message_id.to_owned(),
session_id: session_id.to_owned(),
timestamp,
options: row_options(row),
},
true,
),
"assistant" => (
Message::Assistant {
id: message_id.to_owned(),
session_id: session_id.to_owned(),
timestamp,
options: row_options(row),
},
true,
),
"developer" | "system" => (
Message::System {
id: message_id.to_owned(),
session_id: session_id.to_owned(),
timestamp,
content: None,
options: row_options(row),
},
true,
),
other => return Err(format!("unsupported codex-cli role {other}")),
};
let mut events = Vec::with_capacity(parts.len() + 1);
events.push(IngestEvent::Message(message));
if keep_parts {
events.extend(parts.into_iter().map(IngestEvent::Part));
}
Ok(events)
}
fn message_provenance(role: &str, content: &[Value]) -> Provenance {
if role == "developer" || role == "system" {
return Provenance::Injected;
}
if role == "user" {
let injected = content.iter().any(|item| {
item.get("text")
.and_then(Value::as_str)
.is_some_and(is_injected_user_text)
});
if injected {
return Provenance::Injected;
}
}
Provenance::Conversational
}
fn is_injected_user_text(text: &str) -> bool {
let trimmed = text.trim_start();
trimmed.starts_with("<environment_context>")
|| trimmed.starts_with("<user_instructions>")
|| trimmed.starts_with("# AGENTS.md")
}
fn tool_call_events(
session_id: &str,
message_id: &str,
timestamp: DateTime<Utc>,
payload: &Value,
row: &Value,
) -> Vec<IngestEvent> {
let call_id = extract_str(payload, "call_id");
let name = extract_str(payload, "name");
let params = match payload.get("arguments") {
Some(Value::String(text)) => {
serde_json::from_str::<Value>(text).unwrap_or_else(|_| Value::String(text.clone()))
}
Some(other) => other.clone(),
None => Value::Null,
};
let part = Part {
session_id: session_id.to_owned(),
id: part_id(message_id, 0),
message_id: message_id.to_owned(),
ordinal: 0,
provenance: Provenance::Conversational,
options: empty_options(),
kind: PartKind::ToolCall {
call_id,
name,
params,
provider_executed: false,
},
};
vec![
IngestEvent::Message(Message::Assistant {
id: message_id.to_owned(),
session_id: session_id.to_owned(),
timestamp,
options: row_options(row),
}),
IngestEvent::Part(part),
]
}
fn custom_tool_call_events(
session_id: &str,
message_id: &str,
timestamp: DateTime<Utc>,
payload: &Value,
row: &Value,
) -> Vec<IngestEvent> {
let part = Part {
session_id: session_id.to_owned(),
id: part_id(message_id, 0),
message_id: message_id.to_owned(),
ordinal: 0,
provenance: Provenance::Conversational,
options: empty_options(),
kind: PartKind::ToolCall {
call_id: extract_str(payload, "call_id"),
name: extract_str(payload, "name"),
params: payload.get("input").cloned().unwrap_or(Value::Null),
provider_executed: true,
},
};
vec![
IngestEvent::Message(Message::Assistant {
id: message_id.to_owned(),
session_id: session_id.to_owned(),
timestamp,
options: row_options(row),
}),
IngestEvent::Part(part),
]
}
fn custom_tool_result_events(
session_id: &str,
message_id: &str,
timestamp: DateTime<Utc>,
payload: &Value,
row: &Value,
) -> Vec<IngestEvent> {
let part = Part {
session_id: session_id.to_owned(),
id: part_id(message_id, 0),
message_id: message_id.to_owned(),
ordinal: 0,
provenance: Provenance::Injected,
options: empty_options(),
kind: PartKind::ToolResult {
call_id: extract_str(payload, "call_id"),
name: extract_str(payload, "name"),
is_failure: false,
result: payload.get("output").cloned().unwrap_or(Value::Null),
},
};
vec![
IngestEvent::Message(Message::Tool {
id: message_id.to_owned(),
session_id: session_id.to_owned(),
timestamp,
options: row_options(row),
}),
IngestEvent::Part(part),
]
}
fn tool_result_events(
session_id: &str,
message_id: &str,
timestamp: DateTime<Utc>,
payload: &Value,
row: &Value,
tool_call_names: &HashMap<String, Extracted<String>>,
) -> Vec<IngestEvent> {
let call_id = extract_str(payload, "call_id");
let name = call_id
.as_ref()
.and_then(|id| tool_call_names.get(id.as_str()))
.cloned();
let result = payload.get("output").cloned().unwrap_or(Value::Null);
let part = Part {
session_id: session_id.to_owned(),
id: part_id(message_id, 0),
message_id: message_id.to_owned(),
ordinal: 0,
provenance: Provenance::Injected,
options: empty_options(),
kind: PartKind::ToolResult {
call_id,
name,
is_failure: false,
result,
},
};
vec![
IngestEvent::Message(Message::Tool {
id: message_id.to_owned(),
session_id: session_id.to_owned(),
timestamp,
options: row_options(row),
}),
IngestEvent::Part(part),
]
}
fn reasoning_events(
session_id: &str,
message_id: &str,
timestamp: DateTime<Utc>,
payload: &Value,
row: &Value,
) -> Vec<IngestEvent> {
let summary = payload
.get("summary")
.and_then(Value::as_array)
.and_then(|items| {
let joined = items
.iter()
.filter_map(|item| extract_str(item, "text"))
.map(|e| (*e).clone())
.collect::<Vec<_>>()
.join("\n");
if joined.is_empty() {
None
} else {
Some(extract_compact_repr(payload))
}
});
let part = Part {
session_id: session_id.to_owned(),
id: part_id(message_id, 0),
message_id: message_id.to_owned(),
ordinal: 0,
provenance: Provenance::Conversational,
options: empty_options(),
kind: PartKind::Reasoning { text: summary },
};
vec![
IngestEvent::Message(Message::Assistant {
id: message_id.to_owned(),
session_id: session_id.to_owned(),
timestamp,
options: row_options(row),
}),
IngestEvent::Part(part),
]
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::unwrap_used)]
use super::*;
use crate::{handlers::ingest_adapter, sessions::Store, wire::PartKind};
use tempfile::TempDir;
const FIXTURES: &str = "tests/fixtures/adapter/codex_cli/sessions";
#[test]
fn probe_default_finds_codex_sessions_under_home() -> anyhow::Result<()> {
crate::adapter::test_support::assert_probe_default(
&CodexCliFactory,
&[".codex", "sessions"],
)
}
#[tokio::test(flavor = "multi_thread")]
async fn native_restore_is_value_equal_to_fixture_corpus() -> anyhow::Result<()> {
let adapter = CodexCliAdapter::new(FIXTURES);
crate::adapter::test_support::assert_native_restore(
&CodexCliFactory,
&adapter,
std::path::Path::new(FIXTURES)
.parent()
.expect("FIXTURES is nested under a corpus root"),
)
.await
}
#[tokio::test(flavor = "multi_thread")]
async fn codex_cli_adapter_ingests_fixture_corpus_into_canonical_shape() -> anyhow::Result<()> {
let temp = TempDir::new()?;
let store = Store::open_local(temp.path()).await?;
let adapter = CodexCliAdapter::new(FIXTURES);
let summary = ingest_adapter(&store, &adapter, &crate::adapter::NoopOracle, |_| {}).await?;
assert!(summary.accepted() > 0, "ingest must accept rows");
assert_eq!(summary.dropped_events, 0, "no per-event drops expected");
assert_eq!(
summary.dropped_sessions, 0,
"no session-level rejections expected"
);
assert_eq!(summary.skipped_files, 0, "no whole-file skips expected");
let (sessions, messages, parts) = store.row_counts().await?;
assert!(sessions > 0, "at least one codex-cli session");
assert!(messages > 0, "at least one codex-cli message");
assert!(parts > 0, "at least one codex-cli Part");
let mut saw_text_part = false;
for session_id in store.session_ids().await? {
let session = store
.get_session(&session_id)
.await?
.expect("session round-trips");
assert_eq!(session.session.source_agent, "codex-cli");
assert!(
!session.messages.is_empty(),
"session {session_id} must carry messages",
);
for stored in &session.messages {
for part in &stored.parts {
if matches!(part.kind, PartKind::Text { .. }) {
saw_text_part = true;
}
}
}
}
assert!(
saw_text_part,
"codex-cli corpus must contain at least one Text Part",
);
Ok(())
}
#[test]
fn message_provenance_separates_prompts_from_harness_records() {
let prompt = vec![json!({"type": "input_text", "text": "refactor this"})];
assert_eq!(
message_provenance("user", &prompt),
Provenance::Conversational,
);
assert_eq!(
message_provenance("assistant", &[]),
Provenance::Conversational,
);
let developer = vec![json!({"type": "input_text", "text": "you are an agent"})];
assert_eq!(
message_provenance("developer", &developer),
Provenance::Injected,
);
let env = vec![json!({
"type": "input_text",
"text": "<environment_context>cwd=/tmp</environment_context>",
})];
assert_eq!(message_provenance("user", &env), Provenance::Injected);
}
#[test]
fn legacy_rows_normalize_to_payloads() {
let ts = Utc::now();
let map: HashMap<String, Extracted<String>> = HashMap::new();
let first = json!({"id": "s1", "timestamp": "2025-09-13T04:30:17.447Z"});
let state = json!({"record_type": "state"});
assert!(
events_from_row("s1", 1, &first, ts, &map)
.expect("legacy first row parses")
.is_empty(),
);
assert!(
events_from_row("s1", 2, &state, ts, &map)
.expect("state marker parses")
.is_empty(),
);
let message = json!({
"type": "message",
"role": "user",
"content": [{"type": "input_text", "text": "hi"}],
});
let events = events_from_row("s1", 3, &message, ts, &map).expect("legacy message parses");
assert_eq!(events.len(), 2, "message + one Text Part");
assert!(matches!(
events[0],
IngestEvent::Message(Message::User { .. })
));
assert!(matches!(
&events[1],
IngestEvent::Part(part) if matches!(part.kind, PartKind::Text { .. }),
));
}
#[tokio::test(flavor = "multi_thread")]
async fn legacy_rollout_ingests_into_canonical_shape() -> anyhow::Result<()> {
let temp = TempDir::new()?;
let store = Store::open_local(temp.path()).await?;
let adapter = CodexCliAdapter::new(FIXTURES);
ingest_adapter(&store, &adapter, &crate::adapter::NoopOracle, |_| {}).await?;
let session = store
.get_session("67c52f3f-d25e-4194-a006-93de58f28d7c")
.await?
.expect("legacy rollout ingests as a session");
assert_eq!(session.session.source_agent, "codex-cli");
assert_eq!(
session
.session
.created_at
.to_rfc3339_opts(SecondsFormat::Millis, true),
"2025-09-13T04:30:17.447Z",
);
assert_eq!(session.messages.len(), 11, "every legacy data row ingests");
let resolved = session.messages.iter().any(|message| {
message
.parts
.iter()
.any(|part| matches!(&part.kind, PartKind::ToolResult { name: Some(_), .. }))
});
assert!(
resolved,
"legacy function_call_output resolves its tool name"
);
Ok(())
}
}