agtrace-providers 0.8.0

Internal provider adapters for the agtrace CLI. Not intended for direct use.
Documentation
// Tool call normalization from raw provider data to typed ToolCallPayload variants
//
// Rationale for provider-layer placement:
//   This module contains provider-specific knowledge about tool names and their
//   argument schemas. While the ToolCallPayload enum itself is in agtrace-types
//   (domain model), the logic to map raw tool names to typed variants belongs here.
//
// Design principle:
//   - agtrace-types: Defines domain model structure (ToolCallPayload enum)
//   - agtrace-providers: Knows how to normalize provider data into domain model
//   - This separation keeps types pure and provider logic centralized

use agtrace_types::ToolCallPayload;
use serde_json::Value;

/// Normalize raw tool call data into a typed ToolCallPayload variant
///
/// # Deprecation Notice
///
/// **This function is deprecated.** Use provider-specific `ToolMapper::normalize_call` instead.
///
/// ## Why deprecated?
///
/// This function contains provider-specific logic that violates architecture principles:
/// - Provider details leak into a provider-agnostic layer
/// - Not scalable: adding providers requires modifying this function
/// - Responsibility inversion: provider-specific logic should be in provider implementations
///
/// ## Migration path
///
/// Instead of using this function directly, use the `ToolMapper` trait:
///
/// ```rust,ignore
/// use agtrace_providers::ProviderAdapter;
///
/// let adapter = ProviderAdapter::claude();
/// let payload = adapter.mapper.normalize_call("Read", args, Some("call_123"));
/// ```
///
/// Each provider implements `ToolMapper::normalize_call` with provider-specific logic:
/// - `ClaudeToolMapper` handles Claude Code tools
/// - `CodexToolMapper` handles Codex tools
/// - `GeminiToolMapper` handles Gemini tools
///
/// ## Legacy behavior
///
/// This function provides generic normalization for common tool patterns.
/// It does NOT parse provider-specific details like MCP server/tool names.
/// For full normalization, use provider-specific mappers.
#[deprecated(
    since = "0.3.0",
    note = "Use ToolMapper::normalize_call instead. See function docs for migration path."
)]
pub fn normalize_tool_call(
    name: String,
    arguments: Value,
    provider_call_id: Option<String>,
) -> ToolCallPayload {
    // Try to parse into specific variants based on name
    match name.as_str() {
        "Read" | "Glob" => {
            if let Ok(args) = serde_json::from_value(arguments.clone()) {
                return ToolCallPayload::FileRead {
                    name,
                    arguments: args,
                    provider_call_id,
                };
            }
        }
        "Edit" => {
            if let Ok(args) = serde_json::from_value(arguments.clone()) {
                return ToolCallPayload::FileEdit {
                    name,
                    arguments: args,
                    provider_call_id,
                };
            }
        }
        "Write" => {
            if let Ok(args) = serde_json::from_value(arguments.clone()) {
                return ToolCallPayload::FileWrite {
                    name,
                    arguments: args,
                    provider_call_id,
                };
            }
        }
        "Bash" | "KillShell" | "BashOutput" => {
            if let Ok(args) = serde_json::from_value(arguments.clone()) {
                return ToolCallPayload::Execute {
                    name,
                    arguments: args,
                    provider_call_id,
                };
            }
        }
        "Grep" | "WebSearch" | "WebFetch" => {
            if let Ok(args) = serde_json::from_value(arguments.clone()) {
                return ToolCallPayload::Search {
                    name,
                    arguments: args,
                    provider_call_id,
                };
            }
        }
        _ if name.starts_with("mcp__") => {
            if let Ok(args) = serde_json::from_value(arguments.clone()) {
                return ToolCallPayload::Mcp {
                    name,
                    arguments: args,
                    provider_call_id,
                };
            }
        }
        _ => {}
    }

    // Fallback to generic
    ToolCallPayload::Generic {
        name,
        arguments,
        provider_call_id,
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    #[allow(deprecated)]
    #[test]
    fn test_normalize_file_read() {
        let payload = normalize_tool_call(
            "Read".to_string(),
            json!({"file_path": "/path/to/file.rs"}),
            Some("call_123".to_string()),
        );

        match payload {
            ToolCallPayload::FileRead {
                name,
                arguments,
                provider_call_id,
            } => {
                assert_eq!(name, "Read");
                assert_eq!(arguments.path(), Some("/path/to/file.rs"));
                assert_eq!(provider_call_id, Some("call_123".to_string()));
            }
            _ => panic!("Expected FileRead variant"),
        }
    }

    #[allow(deprecated)]
    #[test]
    fn test_normalize_execute() {
        let payload = normalize_tool_call(
            "Bash".to_string(),
            json!({"command": "ls -la"}),
            Some("call_456".to_string()),
        );

        match payload {
            ToolCallPayload::Execute {
                name,
                arguments,
                provider_call_id,
            } => {
                assert_eq!(name, "Bash");
                assert_eq!(arguments.command, Some("ls -la".to_string()));
                assert_eq!(provider_call_id, Some("call_456".to_string()));
            }
            _ => panic!("Expected Execute variant"),
        }
    }

    #[allow(deprecated)]
    #[test]
    fn test_normalize_mcp_tool() {
        let payload = normalize_tool_call(
            "mcp__o3__search".to_string(),
            json!({"query": "test"}),
            Some("call_789".to_string()),
        );

        match payload {
            ToolCallPayload::Mcp {
                name,
                arguments,
                provider_call_id,
            } => {
                assert_eq!(name, "mcp__o3__search");
                // McpArgs wraps raw JSON, verify it contains the query
                assert_eq!(
                    arguments.inner.get("query").and_then(|v| v.as_str()),
                    Some("test")
                );
                assert_eq!(provider_call_id, Some("call_789".to_string()));
            }
            _ => panic!("Expected Mcp variant"),
        }
    }

    #[allow(deprecated)]
    #[test]
    fn test_normalize_unknown_tool_fallback() {
        let payload = normalize_tool_call(
            "UnknownTool".to_string(),
            json!({"foo": "bar"}),
            Some("call_999".to_string()),
        );

        match payload {
            ToolCallPayload::Generic {
                name,
                arguments,
                provider_call_id,
            } => {
                assert_eq!(name, "UnknownTool");
                assert_eq!(arguments, json!({"foo": "bar"}));
                assert_eq!(provider_call_id, Some("call_999".to_string()));
            }
            _ => panic!("Expected Generic variant for unknown tool"),
        }
    }

    #[allow(deprecated)]
    #[test]
    fn test_normalize_invalid_arguments_fallback() {
        // FileReadArgs has `extra: Value` field, so it accepts any fields
        // This test verifies that invalid fields are captured in `extra`
        let payload = normalize_tool_call(
            "Read".to_string(),
            json!({"invalid_field": 123}),
            Some("call_000".to_string()),
        );

        // Should parse as FileRead with invalid field in `extra`
        match payload {
            ToolCallPayload::FileRead {
                name, arguments, ..
            } => {
                assert_eq!(name, "Read");
                assert_eq!(arguments.file_path, None);
                assert_eq!(arguments.path, None);
                assert_eq!(arguments.pattern, None);
                assert_eq!(arguments.extra.get("invalid_field"), Some(&json!(123)));
            }
            _ => panic!("Expected FileRead variant, got: {:?}", payload.kind()),
        }
    }
}