goosedump 0.12.43

Browse, search, compact, and learn from coding-agent sessions
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (C) Jarkko Sakkinen 2026

//! Shared JSON-to-part extraction helpers for the provider readers.
//!
//! Each provider reader converts its raw JSON shapes into [`Part`] values. The
//! helpers below are the single place where text, tool call, and tool result
//! extraction logic lives. How a role maps onto a message stays in the reader
//! that owns the schema; the JSON-to-part conversion is shared.

use crate::engine::message::{ToolCall, ToolResultData};
use serde_json::Value;

/// Extract a textual field from a content part.
///
/// Recognizes `text`, `input_text`, and `output_text` so Codex's
/// `input_text`/`output_text` content parts and the providers' plain
/// `text` parts all flow through the same helper.
#[must_use]
pub fn part_text(part: &Value) -> Option<&str> {
    part["text"]
        .as_str()
        .or_else(|| part["input_text"].as_str())
        .or_else(|| part["output_text"].as_str())
}

/// Concatenate text from all content parts in a JSON content value.
///
/// Accepts an array of parts, a plain string, or anything else (returning empty).
#[must_use]
pub fn extract_text(content: Option<&Value>) -> String {
    match content {
        Some(Value::Array(parts)) => parts
            .iter()
            .filter_map(part_text)
            .map(str::to_string)
            .collect::<Vec<_>>()
            .join("\n"),
        Some(Value::String(text)) => text.clone(),
        _ => String::new(),
    }
}

/// Parse a tool-call `arguments` value, accepting both a JSON object and a
/// string containing JSON.
#[must_use]
pub fn parse_tool_arguments(raw: &Value) -> Value {
    if let Some(text) = raw.as_str()
        && let Ok(parsed) = serde_json::from_str::<Value>(text)
    {
        return parsed;
    }
    raw.clone()
}

/// Build a [`ToolCall`] from a call id, name, and optional arguments value.
///
/// `id` is the provider correlation id (`tool_use.id` / `call_id`); pass `""`
/// when the source format does not assign one.
#[must_use]
pub fn build_tool_call(
    id: impl Into<String>,
    name: impl Into<String>,
    arguments: Option<&Value>,
) -> ToolCall {
    let arguments = arguments.map_or(Value::Null, parse_tool_arguments);
    ToolCall {
        id: id.into(),
        name: name.into(),
        arguments,
    }
}

/// Build a [`ToolResultData`] from explicit fields.
///
/// `call_id` correlates the result to its originating call (`""` when the
/// source format does not address results by id). `fallback_name` is used when
/// the part has no explicit tool name field (Codex function-call outputs are
/// addressed by `call_id` only).
#[must_use]
pub fn build_tool_result(
    call_id: impl Into<String>,
    fallback_name: impl Into<String>,
    content_text: impl Into<String>,
    is_error: bool,
) -> ToolResultData {
    ToolResultData {
        call_id: call_id.into(),
        tool_name: fallback_name.into(),
        content: content_text.into(),
        is_error,
    }
}