SessionEventParser.java

/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *--------------------------------------------------------------------------------------------*/

package com.github.copilot.sdk.events;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;

import java.util.logging.Level;
import java.util.logging.Logger;

import java.util.HashMap;
import java.util.Map;

/**
 * Parser for session events that handles polymorphic deserialization.
 * <p>
 * This class deserializes JSON event data into the appropriate
 * {@link AbstractSessionEvent} subclass based on the "type" field. It is used
 * internally by the SDK to convert server events to Java objects.
 *
 * <h2>Supported Event Types</h2>
 * <ul>
 * <li><strong>Session</strong>: session.start, session.resume, session.error,
 * session.idle, session.info, etc.</li>
 * <li><strong>Assistant</strong>: assistant.message, assistant.message_delta,
 * assistant.turn_start, assistant.turn_end, etc.</li>
 * <li><strong>Tool</strong>: tool.execution_start, tool.execution_complete,
 * etc.</li>
 * <li><strong>User</strong>: user.message, pending_messages.modified</li>
 * <li><strong>Subagent</strong>: subagent.started, subagent.completed,
 * etc.</li>
 * </ul>
 *
 * @see AbstractSessionEvent
 * @since 1.0.0
 */
public class SessionEventParser {

    private static final Logger LOG = Logger.getLogger(SessionEventParser.class.getName());
    private static final ObjectMapper MAPPER;
    private static final Map<String, Class<? extends AbstractSessionEvent>> TYPE_MAP = new HashMap<>();

    static {
        MAPPER = new ObjectMapper();
        MAPPER.registerModule(new JavaTimeModule());
        MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

        TYPE_MAP.put("session.start", SessionStartEvent.class);
        TYPE_MAP.put("session.resume", SessionResumeEvent.class);
        TYPE_MAP.put("session.error", SessionErrorEvent.class);
        TYPE_MAP.put("session.idle", SessionIdleEvent.class);
        TYPE_MAP.put("session.info", SessionInfoEvent.class);
        TYPE_MAP.put("session.model_change", SessionModelChangeEvent.class);
        TYPE_MAP.put("session.handoff", SessionHandoffEvent.class);
        TYPE_MAP.put("session.truncation", SessionTruncationEvent.class);
        TYPE_MAP.put("session.snapshot_rewind", SessionSnapshotRewindEvent.class);
        TYPE_MAP.put("session.usage_info", SessionUsageInfoEvent.class);
        TYPE_MAP.put("session.compaction_start", SessionCompactionStartEvent.class);
        TYPE_MAP.put("session.compaction_complete", SessionCompactionCompleteEvent.class);
        TYPE_MAP.put("session.context_changed", SessionContextChangedEvent.class);
        TYPE_MAP.put("user.message", UserMessageEvent.class);
        TYPE_MAP.put("pending_messages.modified", PendingMessagesModifiedEvent.class);
        TYPE_MAP.put("assistant.turn_start", AssistantTurnStartEvent.class);
        TYPE_MAP.put("assistant.intent", AssistantIntentEvent.class);
        TYPE_MAP.put("assistant.reasoning", AssistantReasoningEvent.class);
        TYPE_MAP.put("assistant.reasoning_delta", AssistantReasoningDeltaEvent.class);
        TYPE_MAP.put("assistant.message", AssistantMessageEvent.class);
        TYPE_MAP.put("assistant.message_delta", AssistantMessageDeltaEvent.class);
        TYPE_MAP.put("assistant.turn_end", AssistantTurnEndEvent.class);
        TYPE_MAP.put("assistant.usage", AssistantUsageEvent.class);
        TYPE_MAP.put("abort", AbortEvent.class);
        TYPE_MAP.put("tool.user_requested", ToolUserRequestedEvent.class);
        TYPE_MAP.put("tool.execution_start", ToolExecutionStartEvent.class);
        TYPE_MAP.put("tool.execution_partial_result", ToolExecutionPartialResultEvent.class);
        TYPE_MAP.put("tool.execution_progress", ToolExecutionProgressEvent.class);
        TYPE_MAP.put("tool.execution_complete", ToolExecutionCompleteEvent.class);
        TYPE_MAP.put("subagent.started", SubagentStartedEvent.class);
        TYPE_MAP.put("subagent.completed", SubagentCompletedEvent.class);
        TYPE_MAP.put("subagent.failed", SubagentFailedEvent.class);
        TYPE_MAP.put("subagent.selected", SubagentSelectedEvent.class);
        TYPE_MAP.put("hook.start", HookStartEvent.class);
        TYPE_MAP.put("hook.end", HookEndEvent.class);
        TYPE_MAP.put("system.message", SystemMessageEvent.class);
        TYPE_MAP.put("session.shutdown", SessionShutdownEvent.class);
        TYPE_MAP.put("skill.invoked", SkillInvokedEvent.class);
    }

    /**
     * Parses a JsonNode into the appropriate SessionEvent subclass.
     *
     * @param node
     *            the JSON node representing an event
     * @return the parsed event, or {@code null} if parsing fails or type is unknown
     */
    public static AbstractSessionEvent parse(JsonNode node) {
        try {
            String type = node.has("type") ? node.get("type").asText() : null;

            if (type == null) {
                LOG.warning("Missing 'type' field in event");
                return null;
            }

            Class<? extends AbstractSessionEvent> eventClass = TYPE_MAP.get(type);
            if (eventClass == null) {
                LOG.fine("Unknown event type: " + type);
                return null;
            }

            return MAPPER.treeToValue(node, eventClass);
        } catch (Exception e) {
            LOG.log(Level.SEVERE, "Failed to parse session event", e);
            return null;
        }
    }
}