fix: parse more codex output (#439)
This commit is contained in:
@@ -275,34 +275,98 @@ pub enum CodexJson {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
|
||||||
|
pub struct McpInvocation {
|
||||||
|
pub server: String,
|
||||||
|
pub tool: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub arguments: Option<serde_json::Value>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
|
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
|
||||||
#[serde(tag = "type")]
|
#[serde(tag = "type")]
|
||||||
pub enum CodexMsgContent {
|
pub enum CodexMsgContent {
|
||||||
#[serde(rename = "agent_message")]
|
#[serde(rename = "agent_message")]
|
||||||
AgentMessage { message: String },
|
AgentMessage { message: String },
|
||||||
|
|
||||||
#[serde(rename = "agent_reasoning")]
|
#[serde(rename = "agent_reasoning")]
|
||||||
AgentReasoning { text: String },
|
AgentReasoning { text: String },
|
||||||
|
|
||||||
|
#[serde(rename = "agent_reasoning_raw_content")]
|
||||||
|
AgentReasoningRawContent { text: String },
|
||||||
|
|
||||||
|
#[serde(rename = "agent_reasoning_raw_content_delta")]
|
||||||
|
AgentReasoningRawContentDelta { delta: String },
|
||||||
|
|
||||||
#[serde(rename = "error")]
|
#[serde(rename = "error")]
|
||||||
Error { message: Option<String> },
|
Error { message: Option<String> },
|
||||||
|
|
||||||
|
#[serde(rename = "mcp_tool_call_begin")]
|
||||||
|
McpToolCallBegin {
|
||||||
|
call_id: String,
|
||||||
|
invocation: McpInvocation,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[serde(rename = "mcp_tool_call_end")]
|
||||||
|
McpToolCallEnd {
|
||||||
|
call_id: String,
|
||||||
|
invocation: McpInvocation,
|
||||||
|
#[serde(default)]
|
||||||
|
duration: serde_json::Value,
|
||||||
|
result: serde_json::Value,
|
||||||
|
},
|
||||||
|
|
||||||
#[serde(rename = "exec_command_begin")]
|
#[serde(rename = "exec_command_begin")]
|
||||||
ExecCommandBegin {
|
ExecCommandBegin {
|
||||||
call_id: Option<String>,
|
call_id: Option<String>,
|
||||||
command: Vec<String>,
|
command: Vec<String>,
|
||||||
cwd: Option<String>,
|
cwd: Option<String>,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
#[serde(rename = "exec_command_output_delta")]
|
||||||
|
ExecCommandOutputDelta {
|
||||||
|
call_id: Option<String>,
|
||||||
|
// "stdout" | "stderr" typically
|
||||||
|
stream: Option<String>,
|
||||||
|
// Could be bytes or string; keep flexible
|
||||||
|
chunk: Option<serde_json::Value>,
|
||||||
|
},
|
||||||
|
|
||||||
#[serde(rename = "exec_command_end")]
|
#[serde(rename = "exec_command_end")]
|
||||||
ExecCommandEnd {
|
ExecCommandEnd {
|
||||||
call_id: Option<String>,
|
call_id: Option<String>,
|
||||||
stdout: Option<String>,
|
stdout: Option<String>,
|
||||||
stderr: Option<String>,
|
stderr: Option<String>,
|
||||||
|
// Codex protocol has exit_code + duration; CLI may provide success; keep optional
|
||||||
success: Option<bool>,
|
success: Option<bool>,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
#[serde(rename = "exec_approval_request")]
|
||||||
|
ExecApprovalRequest {
|
||||||
|
call_id: Option<String>,
|
||||||
|
command: Vec<String>,
|
||||||
|
cwd: Option<String>,
|
||||||
|
reason: Option<String>,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[serde(rename = "apply_patch_approval_request")]
|
||||||
|
ApplyPatchApprovalRequest {
|
||||||
|
call_id: Option<String>,
|
||||||
|
changes: std::collections::HashMap<String, serde_json::Value>,
|
||||||
|
reason: Option<String>,
|
||||||
|
grant_root: Option<String>,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[serde(rename = "background_event")]
|
||||||
|
BackgroundEvent { message: String },
|
||||||
|
|
||||||
#[serde(rename = "patch_apply_begin")]
|
#[serde(rename = "patch_apply_begin")]
|
||||||
PatchApplyBegin {
|
PatchApplyBegin {
|
||||||
call_id: Option<String>,
|
call_id: Option<String>,
|
||||||
auto_approved: Option<bool>,
|
auto_approved: Option<bool>,
|
||||||
changes: std::collections::HashMap<String, serde_json::Value>,
|
changes: std::collections::HashMap<String, serde_json::Value>,
|
||||||
},
|
},
|
||||||
|
|
||||||
#[serde(rename = "patch_apply_end")]
|
#[serde(rename = "patch_apply_end")]
|
||||||
PatchApplyEnd {
|
PatchApplyEnd {
|
||||||
call_id: Option<String>,
|
call_id: Option<String>,
|
||||||
@@ -310,18 +374,23 @@ pub enum CodexMsgContent {
|
|||||||
stderr: Option<String>,
|
stderr: Option<String>,
|
||||||
success: Option<bool>,
|
success: Option<bool>,
|
||||||
},
|
},
|
||||||
#[serde(rename = "mcp_tool_call_begin")]
|
|
||||||
McpToolCallBegin {
|
#[serde(rename = "turn_diff")]
|
||||||
call_id: String,
|
TurnDiff { unified_diff: String },
|
||||||
server: String,
|
|
||||||
tool: String,
|
#[serde(rename = "get_history_entry_response")]
|
||||||
arguments: serde_json::Value,
|
GetHistoryEntryResponse {
|
||||||
|
offset: Option<usize>,
|
||||||
|
log_id: Option<u64>,
|
||||||
|
entry: Option<serde_json::Value>,
|
||||||
},
|
},
|
||||||
#[serde(rename = "mcp_tool_call_end")]
|
|
||||||
McpToolCallEnd {
|
#[serde(rename = "plan_update")]
|
||||||
call_id: String,
|
PlanUpdate {
|
||||||
result: serde_json::Value,
|
#[serde(flatten)]
|
||||||
|
value: serde_json::Value,
|
||||||
},
|
},
|
||||||
|
|
||||||
#[serde(rename = "task_started")]
|
#[serde(rename = "task_started")]
|
||||||
TaskStarted,
|
TaskStarted,
|
||||||
#[serde(rename = "task_complete")]
|
#[serde(rename = "task_complete")]
|
||||||
@@ -334,6 +403,7 @@ pub enum CodexMsgContent {
|
|||||||
reasoning_output_tokens: Option<u64>,
|
reasoning_output_tokens: Option<u64>,
|
||||||
total_tokens: Option<u64>,
|
total_tokens: Option<u64>,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Catch-all for unknown message types
|
// Catch-all for unknown message types
|
||||||
#[serde(other)]
|
#[serde(other)]
|
||||||
Unknown,
|
Unknown,
|
||||||
@@ -422,34 +492,89 @@ impl CodexJson {
|
|||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
CodexMsgContent::McpToolCallBegin {
|
CodexMsgContent::McpToolCallBegin { invocation, .. } => {
|
||||||
server,
|
let tool_name = format!("mcp_{}", invocation.tool);
|
||||||
tool,
|
let content = invocation.tool.clone();
|
||||||
call_id: _,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
let tool_name = format!("mcp_{tool}");
|
|
||||||
let content = tool.clone();
|
|
||||||
|
|
||||||
Some(vec![NormalizedEntry {
|
Some(vec![NormalizedEntry {
|
||||||
timestamp: None,
|
timestamp: None,
|
||||||
entry_type: NormalizedEntryType::ToolUse {
|
entry_type: NormalizedEntryType::ToolUse {
|
||||||
tool_name,
|
tool_name,
|
||||||
action_type: ActionType::Other {
|
action_type: ActionType::Other {
|
||||||
description: format!("MCP tool call to {tool} from {server}"),
|
description: format!(
|
||||||
|
"MCP tool call to {} from {}",
|
||||||
|
invocation.tool, invocation.server
|
||||||
|
),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
content,
|
content,
|
||||||
metadata: Some(metadata),
|
metadata: Some(metadata),
|
||||||
}])
|
}])
|
||||||
}
|
}
|
||||||
|
CodexMsgContent::ExecApprovalRequest {
|
||||||
|
command,
|
||||||
|
cwd,
|
||||||
|
reason,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
let command_str = command.join(" ");
|
||||||
|
let mut parts = vec![format!("command: `{}`", command_str)];
|
||||||
|
if let Some(c) = cwd {
|
||||||
|
parts.push(format!("cwd: {c}"));
|
||||||
|
}
|
||||||
|
if let Some(r) = reason {
|
||||||
|
parts.push(format!("reason: {r}"));
|
||||||
|
}
|
||||||
|
let content =
|
||||||
|
format!("Execution approval requested — {}", parts.join(" "));
|
||||||
|
Some(vec![NormalizedEntry {
|
||||||
|
timestamp: None,
|
||||||
|
entry_type: NormalizedEntryType::SystemMessage,
|
||||||
|
content,
|
||||||
|
metadata: None,
|
||||||
|
}])
|
||||||
|
}
|
||||||
|
CodexMsgContent::ApplyPatchApprovalRequest {
|
||||||
|
changes,
|
||||||
|
reason,
|
||||||
|
grant_root,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
let mut parts = vec![format!("files: {}", changes.len())];
|
||||||
|
if let Some(root) = grant_root {
|
||||||
|
parts.push(format!("grant_root: {root}"));
|
||||||
|
}
|
||||||
|
if let Some(r) = reason {
|
||||||
|
parts.push(format!("reason: {r}"));
|
||||||
|
}
|
||||||
|
let content = format!("Patch approval requested — {}", parts.join(" "));
|
||||||
|
Some(vec![NormalizedEntry {
|
||||||
|
timestamp: None,
|
||||||
|
entry_type: NormalizedEntryType::SystemMessage,
|
||||||
|
content,
|
||||||
|
metadata: None,
|
||||||
|
}])
|
||||||
|
}
|
||||||
|
CodexMsgContent::PlanUpdate { value } => Some(vec![NormalizedEntry {
|
||||||
|
timestamp: None,
|
||||||
|
entry_type: NormalizedEntryType::SystemMessage,
|
||||||
|
content: "Plan update".to_string(),
|
||||||
|
metadata: Some(value.clone()),
|
||||||
|
}]),
|
||||||
|
|
||||||
// Ignored message types
|
// Ignored message types
|
||||||
CodexMsgContent::ExecCommandEnd { .. }
|
CodexMsgContent::AgentReasoningRawContent { .. }
|
||||||
|
| CodexMsgContent::AgentReasoningRawContentDelta { .. }
|
||||||
|
| CodexMsgContent::ExecCommandOutputDelta { .. }
|
||||||
|
| CodexMsgContent::GetHistoryEntryResponse { .. }
|
||||||
|
| CodexMsgContent::ExecCommandEnd { .. }
|
||||||
| CodexMsgContent::PatchApplyEnd { .. }
|
| CodexMsgContent::PatchApplyEnd { .. }
|
||||||
| CodexMsgContent::McpToolCallEnd { .. }
|
| CodexMsgContent::McpToolCallEnd { .. }
|
||||||
| CodexMsgContent::TaskStarted
|
| CodexMsgContent::TaskStarted
|
||||||
| CodexMsgContent::TaskComplete { .. }
|
| CodexMsgContent::TaskComplete { .. }
|
||||||
| CodexMsgContent::TokenCount { .. }
|
| CodexMsgContent::TokenCount { .. }
|
||||||
|
| CodexMsgContent::TurnDiff { .. }
|
||||||
|
| CodexMsgContent::BackgroundEvent { .. }
|
||||||
| CodexMsgContent::Unknown => None,
|
| CodexMsgContent::Unknown => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -815,8 +940,8 @@ invalid json line here
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_normalize_logs_mcp_tool_calls() {
|
fn test_normalize_logs_mcp_tool_calls() {
|
||||||
let logs = r#"{"id":"1","msg":{"type":"mcp_tool_call_begin","call_id":"call_KHwEJyaUuL5D8sO7lPfImx7I","server":"vibe_kanban","tool":"list_projects","arguments":{}}}
|
let logs = r#"{"id":"1","msg":{"type":"mcp_tool_call_begin","call_id":"call_KHwEJyaUuL5D8sO7lPfImx7I","invocation":{"server":"vibe_kanban","tool":"list_projects","arguments":{}}}}
|
||||||
{"id":"1","msg":{"type":"mcp_tool_call_end","call_id":"call_KHwEJyaUuL5D8sO7lPfImx7I","result":{"Ok":{"content":[{"text":"Projects listed successfully"}],"isError":false}}}}
|
{"id":"1","msg":{"type":"mcp_tool_call_end","call_id":"call_KHwEJyaUuL5D8sO7lPfImx7I","invocation":{"server":"vibe_kanban","tool":"list_projects","arguments":{}},"result":{"Ok":{"content":[{"text":"Projects listed successfully"}],"isError":false}}}}
|
||||||
{"id":"1","msg":{"type":"agent_message","message":"Here are your projects"}}"#;
|
{"id":"1","msg":{"type":"agent_message","message":"Here are your projects"}}"#;
|
||||||
|
|
||||||
let entries = parse_test_json_lines(logs);
|
let entries = parse_test_json_lines(logs);
|
||||||
@@ -848,10 +973,10 @@ invalid json line here
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_normalize_logs_mcp_tool_call_multiple() {
|
fn test_normalize_logs_mcp_tool_call_multiple() {
|
||||||
let logs = r#"{"id":"1","msg":{"type":"mcp_tool_call_begin","call_id":"call_1","server":"vibe_kanban","tool":"create_task","arguments":{"title":"Test task"}}}
|
let logs = r#"{"id":"1","msg":{"type":"mcp_tool_call_begin","call_id":"call_1","invocation":{"server":"vibe_kanban","tool":"create_task","arguments":{"title":"Test task"}}}}
|
||||||
{"id":"1","msg":{"type":"mcp_tool_call_end","call_id":"call_1","result":{"Ok":{"content":[{"text":"Task created"}],"isError":false}}}}
|
{"id":"1","msg":{"type":"mcp_tool_call_end","call_id":"call_1","invocation":{"server":"vibe_kanban","tool":"create_task","arguments":{"title":"Test task"}},"result":{"Ok":{"content":[{"text":"Task created"}],"isError":false}}}}
|
||||||
{"id":"1","msg":{"type":"mcp_tool_call_begin","call_id":"call_2","server":"vibe_kanban","tool":"list_tasks","arguments":{}}}
|
{"id":"1","msg":{"type":"mcp_tool_call_begin","call_id":"call_2","invocation":{"server":"vibe_kanban","tool":"list_tasks","arguments":{}}}}
|
||||||
{"id":"1","msg":{"type":"mcp_tool_call_end","call_id":"call_2","result":{"Ok":{"content":[{"text":"Tasks listed"}],"isError":false}}}}"#;
|
{"id":"1","msg":{"type":"mcp_tool_call_end","call_id":"call_2","invocation":{"server":"vibe_kanban","tool":"list_tasks","arguments":{}},"result":{"Ok":{"content":[{"text":"Tasks listed"}],"isError":false}}}}"#;
|
||||||
|
|
||||||
let entries = parse_test_json_lines(logs);
|
let entries = parse_test_json_lines(logs);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user