diff --git a/core/src/main/java/com/google/adk/sessions/SessionJsonConverter.java b/core/src/main/java/com/google/adk/sessions/SessionJsonConverter.java
index 1e6c5327a..3107fa25c 100644
--- a/core/src/main/java/com/google/adk/sessions/SessionJsonConverter.java
+++ b/core/src/main/java/com/google/adk/sessions/SessionJsonConverter.java
@@ -16,26 +16,48 @@
package com.google.adk.sessions;
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
+import com.fasterxml.jackson.databind.module.SimpleModule;
import com.google.adk.JsonBaseModel;
import com.google.adk.events.Event;
import com.google.adk.events.EventActions;
+import com.google.adk.events.EventCompaction;
import com.google.adk.events.ToolConfirmation;
+import com.google.common.base.CaseFormat;
import com.google.common.base.Splitter;
+import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
+import com.google.common.collect.Maps;
import com.google.genai.types.Content;
+import com.google.genai.types.CustomMetadata;
import com.google.genai.types.FinishReason;
import com.google.genai.types.GenerateContentResponseUsageMetadata;
import com.google.genai.types.GroundingMetadata;
+import com.google.genai.types.StringList;
+import java.io.IOException;
import java.io.UncheckedIOException;
import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Base64;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@@ -43,11 +65,47 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-/** Handles JSON serialization and deserialization for session-related objects. */
+/**
+ * Static utilities for converting session events to and from Vertex AI Sessions API JSON.
+ *
+ *
Events are stored as Python ADK stores them: the typed fields carry what the API keeps, and
+ * {@code rawEvent} carries a copy of the event in the shape Python ADK reads.
+ */
final class SessionJsonConverter {
private static final ObjectMapper objectMapper = JsonBaseModel.getMapper();
+
+ /**
+ * Maps event JSON stored in API {@code Struct}s as Python ADK does: null fields, but not null map
+ * values, are left out on write, and the URL-safe base64 Python ADK writes is accepted on read.
+ */
+ private static final ObjectMapper storedJsonMapper =
+ objectMapper
+ .copy()
+ .setDefaultPropertyInclusion(
+ JsonInclude.Value.construct(JsonInclude.Include.NON_NULL, JsonInclude.Include.ALWAYS))
+ .registerModule(
+ new SimpleModule().addDeserializer(byte[].class, new LenientBase64Deserializer()));
+
private static final Logger logger = LoggerFactory.getLogger(SessionJsonConverter.class);
+ // customMetadata keys under which Python ADK stores fields the API does not keep.
+ private static final String USAGE_METADATA_KEY = "_usage_metadata";
+ private static final String COMPACTION_KEY = "_compaction";
+
+ /**
+ * {@code EventActions} keys Python ADK rejects ({@code setModelResponse} only before 2.x), left
+ * out of {@code rawEvent} so that it can read the event.
+ */
+ private static final ImmutableSet PYTHON_UNSUPPORTED_ACTION_KEYS =
+ ImmutableSet.of("deletedArtifactIds", "setModelResponse");
+
+ /**
+ * Keys holding user data ({@code FunctionCall.args}, {@code FunctionResponse.response} and {@code
+ * Part.partMetadata}), whose nested keys {@link #snakeToCamelKeys} leaves as written.
+ */
+ private static final ImmutableSet USER_DATA_KEYS =
+ ImmutableSet.of("args", "response", "part_metadata", "partMetadata");
+
private SessionJsonConverter() {}
/**
@@ -68,6 +126,7 @@ static String convertEventToJson(Event event) {
* @throws UncheckedIOException if serialization fails.
*/
static String convertEventToJson(Event event, boolean useIsoString) {
+ EventActions actions = event.actions();
Map metadataJson = new HashMap<>();
event.partial().ifPresent(v -> metadataJson.put("partial", v));
event.turnComplete().ifPresent(v -> metadataJson.put("turnComplete", v));
@@ -75,7 +134,18 @@ static String convertEventToJson(Event event, boolean useIsoString) {
event.branch().ifPresent(v -> metadataJson.put("branch", v));
event.longRunningToolIds().ifPresent(v -> putIfNotEmpty(metadataJson, "longRunningToolIds", v));
event.groundingMetadata().ifPresent(v -> metadataJson.put("groundingMetadata", v));
- event.usageMetadata().ifPresent(v -> metadataJson.put("usageMetadata", v));
+ Map customMetadataJson =
+ event
+ .customMetadata()
+ .map(SessionJsonConverter::customMetadataToJson)
+ .orElseGet(LinkedHashMap::new);
+ event.usageMetadata().ifPresent(v -> customMetadataJson.put(USAGE_METADATA_KEY, v));
+ if (actions != null) {
+ actions
+ .compaction()
+ .ifPresent(v -> customMetadataJson.put(COMPACTION_KEY, compactionToJson(v)));
+ }
+ putIfNotEmpty(metadataJson, "customMetadata", customMetadataJson);
Map eventJson = new HashMap<>();
eventJson.put("author", event.author());
eventJson.put("invocationId", event.invocationId());
@@ -95,36 +165,80 @@ static String convertEventToJson(Event event, boolean useIsoString) {
event.errorMessage().ifPresent(errorMessage -> eventJson.put("errorMessage", errorMessage));
eventJson.put("eventMetadata", metadataJson);
- if (event.actions() != null) {
+ if (actions != null) {
+ // The API keeps only these actions fields and drops the rest; rawEvent carries them.
Map actionsJson = new HashMap<>();
- EventActions actions = event.actions();
actions.skipSummarization().ifPresent(v -> actionsJson.put("skipSummarization", v));
actionsJson.put("stateDelta", stateDeltaToJson(actions.stateDelta()));
putIfNotEmpty(actionsJson, "artifactDelta", actions.artifactDelta());
- actions
- .transferToAgent()
- .ifPresent(
- v -> {
- actionsJson.put("transferAgent", v);
- });
+ actions.transferToAgent().ifPresent(v -> actionsJson.put("transferAgent", v));
actions.escalate().ifPresent(v -> actionsJson.put("escalate", v));
- if (actions.endOfAgent()) {
- actionsJson.put("endOfAgent", actions.endOfAgent());
- }
putIfNotEmpty(actionsJson, "requestedAuthConfigs", actions.requestedAuthConfigs());
- putIfNotEmpty(
- actionsJson, "requestedToolConfirmations", actions.requestedToolConfirmations());
- actions.agentState().ifPresent(v -> actionsJson.put("agentState", v));
eventJson.put("actions", actionsJson);
}
event.content().ifPresent(c -> eventJson.put("content", SessionUtils.encodeContent(c)));
try {
+ eventJson.put("rawEvent", rawEventToJson(event));
return objectMapper.writeValueAsString(eventJson);
} catch (JsonProcessingException e) {
throw new UncheckedIOException(e);
}
}
+ /** Serializes the event in the shape Python ADK writes to and reads from {@code rawEvent}. */
+ private static Map rawEventToJson(Event event) throws JsonProcessingException {
+ Map rawEvent =
+ storedJsonMapper.readValue(
+ storedJsonMapper.writeValueAsString(event),
+ new TypeReference