Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 85 additions & 25 deletions core/src/main/java/com/google/adk/events/Event.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.google.adk.JsonBaseModel;
import com.google.adk.annotations.Experimental;
import com.google.adk.platform.UuidProvider;
import com.google.adk.workflow.NodeInfo;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
Expand Down Expand Up @@ -66,6 +68,8 @@ public class Event extends JsonBaseModel {
private @Nullable String modelVersion;
private @Nullable Transcription inputTranscription;
private @Nullable Transcription outputTranscription;
private @Nullable Object output;
private @Nullable NodeInfo nodeInfo;

private long timestamp;

Expand Down Expand Up @@ -306,6 +310,38 @@ public void setOutputTranscription(@Nullable Transcription outputTranscription)
this.outputTranscription = outputTranscription;
}

/**
* For a workflow node, returns the value handed to successors, distinct from {@link #content()},
* or empty when the event carries no node output. Holds any value Jackson can serialize, such as
* a JSON-native value or a {@link Content}. An event read from JSON holds the JSON-native form (a
* map with string keys, a list, a string, a number, or a boolean).
*/
@Experimental
@JsonProperty("output")
public Optional<Object> output() {
return Optional.ofNullable(output);
}

@Experimental
public void setOutput(@Nullable Object output) {
this.output = output;
}

/**
* Identifies the workflow-node activation that emitted this event; outside a workflow, this value
* is empty or has an empty path.
*/
@Experimental
@JsonProperty("nodeInfo")
public Optional<NodeInfo> nodeInfo() {
return Optional.ofNullable(nodeInfo);
}

@Experimental
public void setNodeInfo(@Nullable NodeInfo nodeInfo) {
this.nodeInfo = nodeInfo;
}

/** The timestamp of the event. */
@JsonProperty("timestamp")
public long timestamp() {
Expand Down Expand Up @@ -415,6 +451,8 @@ public static class Builder {
private @Nullable String modelVersion;
private @Nullable Transcription inputTranscription;
private @Nullable Transcription outputTranscription;
private @Nullable Object output;
private @Nullable NodeInfo nodeInfo;
private @Nullable Long timestamp;

@JsonCreator
Expand Down Expand Up @@ -592,6 +630,22 @@ public Builder outputTranscription(@Nullable Transcription value) {
return this;
}

@Experimental
@CanIgnoreReturnValue
@JsonProperty("output")
public Builder output(@Nullable Object value) {
this.output = value;
return this;
}

@Experimental
@CanIgnoreReturnValue
@JsonProperty("nodeInfo")
public Builder nodeInfo(@Nullable NodeInfo value) {
this.nodeInfo = value;
return this;
}

public Event build() {
Event event = new Event();
event.setId(id);
Expand All @@ -616,6 +670,8 @@ public Event build() {
timestamp().orElseGet(() -> InstantSource.system().instant().toEpochMilli()));
event.setInputTranscription(inputTranscription);
event.setOutputTranscription(outputTranscription);
event.setOutput(output);
event.setNodeInfo(nodeInfo);
return event;
}
}
Expand All @@ -631,30 +687,30 @@ public static Event fromJson(String json) {

/** Creates a builder pre-filled with this event's values. */
public Builder toBuilder() {
Builder builder =
new Builder()
.id(this.id)
.invocationId(this.invocationId)
.author(this.author)
.content(this.content)
.actions(this.actions)
.longRunningToolIds(this.longRunningToolIds)
.partial(this.partial)
.turnComplete(this.turnComplete)
.errorCode(this.errorCode)
.errorMessage(this.errorMessage)
.finishReason(this.finishReason)
.usageMetadata(this.usageMetadata)
.avgLogprobs(this.avgLogprobs)
.interrupted(this.interrupted)
.branch(this.branch)
.groundingMetadata(this.groundingMetadata)
.customMetadata(this.customMetadata)
.modelVersion(this.modelVersion)
.inputTranscription(this.inputTranscription)
.outputTranscription(this.outputTranscription)
.timestamp(this.timestamp);
return builder;
return new Builder()
.id(this.id)
.invocationId(this.invocationId)
.author(this.author)
.content(this.content)
.actions(this.actions)
.longRunningToolIds(this.longRunningToolIds)
.partial(this.partial)
.turnComplete(this.turnComplete)
.errorCode(this.errorCode)
.errorMessage(this.errorMessage)
.finishReason(this.finishReason)
.usageMetadata(this.usageMetadata)
.avgLogprobs(this.avgLogprobs)
.interrupted(this.interrupted)
.branch(this.branch)
.groundingMetadata(this.groundingMetadata)
.customMetadata(this.customMetadata)
.modelVersion(this.modelVersion)
.inputTranscription(this.inputTranscription)
.outputTranscription(this.outputTranscription)
.output(this.output)
.nodeInfo(this.nodeInfo)
.timestamp(this.timestamp);
}

@Override
Expand Down Expand Up @@ -685,7 +741,9 @@ public boolean equals(Object obj) {
&& Objects.equals(customMetadata, other.customMetadata)
&& Objects.equals(modelVersion, other.modelVersion)
&& Objects.equals(inputTranscription, other.inputTranscription)
&& Objects.equals(outputTranscription, other.outputTranscription);
&& Objects.equals(outputTranscription, other.outputTranscription)
&& Objects.equals(output, other.output)
&& Objects.equals(nodeInfo, other.nodeInfo);
}

@Override
Expand Down Expand Up @@ -716,6 +774,8 @@ public int hashCode() {
modelVersion,
inputTranscription,
outputTranscription,
output,
nodeInfo,
timestamp);
}
}
43 changes: 41 additions & 2 deletions core/src/main/java/com/google/adk/events/EventActions.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,18 @@
*/
package com.google.adk.events;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.google.adk.JsonBaseModel;
import com.google.adk.annotations.Experimental;
import com.google.adk.sessions.State;
import com.google.adk.workflow.Route;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
Expand All @@ -47,6 +52,7 @@ public class EventActions extends JsonBaseModel {
private @Nullable EventCompaction compaction;
private @Nullable Object setModelResponse;
private @Nullable String rewindBeforeInvocationId;
private @Nullable ImmutableList<Route> route;

/** Default constructor for Jackson. */
public EventActions() {
Expand All @@ -72,6 +78,7 @@ private EventActions(Builder builder) {
this.compaction = builder.compaction;
this.setModelResponse = builder.setModelResponse;
this.rewindBeforeInvocationId = builder.rewindBeforeInvocationId;
this.route = builder.route;
}

@JsonProperty("skipSummarization")
Expand Down Expand Up @@ -252,6 +259,23 @@ public void setRewindBeforeInvocationId(@Nullable String rewindBeforeInvocationI
this.rewindBeforeInvocationId = rewindBeforeInvocationId;
}

/**
* For a workflow node, returns the routes this event selects; an edge is followed when it has one
* of these routes. The value is optional because an empty list is still a routing decision that
* selects no route, whereas an absent value means the event makes no routing decision. When
* merging, an empty list replaces the existing routes, while an absent value keeps them.
*/
@Experimental
@JsonProperty("route")
public Optional<ImmutableList<Route>> route() {
return Optional.ofNullable(route);
}

@Experimental
public void setRoute(@Nullable List<Route> route) {
this.route = route == null ? null : ImmutableList.copyOf(route);
}

public static Builder builder() {
return new Builder();
}
Expand Down Expand Up @@ -280,7 +304,8 @@ public boolean equals(Object o) {
&& Objects.equals(agentState, that.agentState)
&& Objects.equals(compaction, that.compaction)
&& Objects.equals(setModelResponse, that.setModelResponse)
&& Objects.equals(rewindBeforeInvocationId, that.rewindBeforeInvocationId);
&& Objects.equals(rewindBeforeInvocationId, that.rewindBeforeInvocationId)
&& Objects.equals(route, that.route);
}

@Override
Expand All @@ -298,7 +323,8 @@ public int hashCode() {
agentState,
compaction,
setModelResponse,
rewindBeforeInvocationId);
rewindBeforeInvocationId,
route);
}

/** Builder for {@link EventActions}. */
Expand All @@ -316,6 +342,7 @@ public static class Builder {
private @Nullable EventCompaction compaction;
private @Nullable Object setModelResponse;
private @Nullable String rewindBeforeInvocationId;
private @Nullable ImmutableList<Route> route;

public Builder() {
this.stateDelta = new ConcurrentHashMap<>();
Expand All @@ -340,6 +367,7 @@ private Builder(EventActions eventActions) {
this.compaction = eventActions.compaction;
this.setModelResponse = eventActions.setModelResponse;
this.rewindBeforeInvocationId = eventActions.rewindBeforeInvocationId;
this.route = eventActions.route;
}

@CanIgnoreReturnValue
Expand Down Expand Up @@ -467,6 +495,16 @@ public Builder rewindBeforeInvocationId(@Nullable String value) {
return this;
}

// ADK Python writes a single route as a bare value rather than a one-element list.
@Experimental
@CanIgnoreReturnValue
@JsonProperty("route")
@JsonFormat(with = JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
public Builder route(@Nullable List<Route> value) {
this.route = value == null ? null : ImmutableList.copyOf(value);
return this;
}

@CanIgnoreReturnValue
public Builder merge(EventActions other) {
other.skipSummarization().ifPresent(this::skipSummarization);
Expand All @@ -482,6 +520,7 @@ public Builder merge(EventActions other) {
other.compaction().ifPresent(this::compaction);
other.setModelResponse().ifPresent(this::setModelResponse);
other.rewindBeforeInvocationId().ifPresent(this::rewindBeforeInvocationId);
other.route().ifPresent(this::route);
return this;
}

Expand Down
98 changes: 98 additions & 0 deletions core/src/main/java/com/google/adk/workflow/NodeInfo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.adk.workflow;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonSetter;
import com.fasterxml.jackson.annotation.Nulls;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.google.adk.annotations.Experimental;
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.util.List;

/**
* The identity of the workflow-node activation that emitted an {@link com.google.adk.events.Event}.
*/
@Experimental
@AutoValue
@JsonDeserialize(builder = NodeInfo.Builder.class)
public abstract class NodeInfo {

/**
* Returns the emitting node's path. Segments are {@code /}-separated and each is {@code
* name@runId}, so a node of workflow {@code wf} reads {@code wf@1/a@1}; an empty path means the
* event did not come from a workflow node.
*/
@JsonProperty("path")
public abstract String path();

/**
* Returns the node paths this event's output counts for: the emitting node's path, followed by
* the paths of the ancestor workflows that use this event as their output. It is set on an event
* that carries the node's output, either in its {@code output} field or as a message-as-output
* event whose content is the output; an empty list means the output counts for no node.
*/
@JsonProperty("outputFor")
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public abstract ImmutableList<String> outputFor();

/**
* Returns {@code true} if this event's content is the node's output, so no separate output event
* follows.
*/
@JsonProperty("messageAsOutput")
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
public abstract boolean messageAsOutput();

public static Builder builder() {
return new AutoValue_NodeInfo.Builder()
.path("")
.outputFor(ImmutableList.of())
.messageAsOutput(false);
}

public abstract Builder toBuilder();

/** Builder for {@link NodeInfo}. */
@AutoValue.Builder
public abstract static class Builder {

@JsonCreator
static Builder create() {
return builder();
}

@CanIgnoreReturnValue
@JsonProperty("path")
public abstract Builder path(String path);

@CanIgnoreReturnValue
@JsonProperty("outputFor")
@JsonSetter(nulls = Nulls.AS_EMPTY)
public abstract Builder outputFor(List<String> outputFor);

@CanIgnoreReturnValue
@JsonProperty("messageAsOutput")
public abstract Builder messageAsOutput(boolean messageAsOutput);

public abstract NodeInfo build();
}
}
Loading
Loading