agent-sessions 0.2.1

Stream Claude Code and Codex JSONL sessions as typed Rust events with source offsets
Documentation
mod tools;
mod usage;
use crate::parser::{Parsed, State, flag, string, text_projection};
use crate::{Event, EventKinds, LineErrorKind, Message, MetaUpdate, Role};
use serde_json::Value;

pub(crate) fn parse(v: &Value, state: &mut State, p: &mut Parsed) -> Result<(), LineErrorKind> {
    if p.accounting == crate::AccountingPolicy::UsageStatistics
        && v.get("type").is_none()
        && v.get("message").is_none()
    {
        return Ok(());
    }
    let kind = match v.get("type").and_then(Value::as_str) {
        Some(kind) => kind,
        None if p.accounting == crate::AccountingPolicy::UsageStatistics => "assistant",
        None => return Err(LineErrorKind::MissingField("type")),
    };
    let session_id = string(v, "sessionId").or_else(|| string(v, "session_id"));
    if let Some(id) = &session_id {
        state.session_id = Some(id.clone());
    }
    let model = v
        .pointer("/message/model")
        .and_then(Value::as_str)
        .map(str::to_owned);
    if model.is_some() {
        state.model.clone_from(&model);
    }
    let meta = MetaUpdate {
        session_id,
        model,
        cwd: string(v, "cwd"),
        git_branch: string(v, "gitBranch"),
        agent_version: string(v, "version"),
        ..Default::default()
    };
    if meta != MetaUpdate::default() {
        p.emit(0, Event::Meta(meta));
    }
    p.message_id = v
        .pointer("/message/id")
        .and_then(Value::as_str)
        .map(str::to_owned);
    match kind {
        "user" | "assistant" => {
            let m = match v.get("message").filter(|m| m.is_object()) {
                Some(m) => m,
                None if p.accounting == crate::AccountingPolicy::UsageStatistics
                    && !p.wants(EventKinds::MESSAGE) =>
                {
                    return Ok(());
                }
                None => return Err(LineErrorKind::MissingField("message")),
            };
            if p.wants(EventKinds::MESSAGE) {
                let c = m
                    .get("content")
                    .ok_or(LineErrorKind::MissingField("content"))?;
                let (body, text_segments) = text_projection(c, p)?;
                let is_meta = flag(
                    v.get("isMeta")
                        .or_else(|| v.get("is_meta"))
                        .or_else(|| m.get("isMeta"))
                        .or_else(|| m.get("is_meta")),
                    "isMeta",
                )?;
                p.emit(
                    1,
                    Event::Message(Message {
                        role: if kind == "user" {
                            Role::User
                        } else {
                            Role::Assistant
                        },
                        text: body,
                        text_segments,
                        is_meta,
                        is_sidechain: flag(v.get("isSidechain"), "isSidechain")?,
                        parent_id: string(v, "parentUuid"),
                    }),
                );
            }
            if (p.wants(EventKinds::TOOL_CALL) || p.wants(EventKinds::TOOL_RESULT))
                && let Some(c) = m.get("content")
            {
                tools::parse(c, p)?;
            }
            if p.wants(EventKinds::USAGE)
                && let Some(u) = m.get("usage").filter(|u| !u.is_null())
            {
                p.emit(2, Event::Usage(usage::parse(m, u, p.accounting)?));
            }
        }
        "progress" => {
            if (p.wants(EventKinds::TOOL_CALL) || p.wants(EventKinds::TOOL_RESULT))
                && let Some(m) = v.pointer("/data/message/message")
            {
                p.message_id = string(m, "id").or_else(|| {
                    v.pointer("/data/message/id")
                        .and_then(Value::as_str)
                        .map(str::to_owned)
                });
                if let Some(c) = m.get("content") {
                    tools::parse(c, p)?;
                }
            }
            p.ignored.push("progress".into());
        }
        "system" if p.wants(EventKinds::MESSAGE) => {
            if let Some(c) = v.pointer("/message/content").or_else(|| v.get("content")) {
                let (body, text_segments) = text_projection(c, p)?;
                p.emit(
                    1,
                    Event::Message(Message {
                        role: Role::System,
                        text: body,
                        text_segments,
                        is_meta: false,
                        is_sidechain: false,
                        parent_id: None,
                    }),
                );
            } else {
                p.ignored.push(kind.into());
            }
        }
        "system"
        | "attachment"
        | "summary"
        | "last-prompt"
        | "mode"
        | "permission-mode"
        | "ai-title"
        | "atis-latch"
        | "queue-operation"
        | "pr-link"
        | "file-history-snapshot"
        | "file-history-delta"
        | "custom-title"
        | "agent-name"
        | "cost-state"
        | "started"
        | "result"
        | "frame-link" => p.ignored.push(kind.into()),
        other => p.unknown.push(other.into()),
    }
    Ok(())
}