Skip to content

Commit 28f2606

Browse files
damianmomotgooglecopybara-github
authored andcommitted
feat: run a single workflow node
PiperOrigin-RevId: 1002809551
1 parent dc56f11 commit 28f2606

13 files changed

Lines changed: 1684 additions & 111 deletions

File tree

‎core/src/main/java/com/google/adk/agents/BaseAgent.java‎

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import com.google.adk.telemetry.Tracing;
3434
import com.google.adk.utils.AgentEnums.AgentOrigin;
3535
import com.google.adk.workflow.BaseNode;
36+
import com.google.adk.workflow.NodeExecutionState;
3637
import com.google.common.collect.ImmutableList;
3738
import com.google.errorprone.annotations.CanIgnoreReturnValue;
3839
import com.google.errorprone.annotations.DoNotCall;
@@ -269,16 +270,20 @@ public ImmutableList<? extends AfterAgentCallback> canonicalAfterAgentCallbacks(
269270
* instance.
270271
*
271272
* @param parentContext Parent context to copy.
272-
* @return new context with updated branch name.
273+
* @param keepBranch Whether to keep the parent's branch instead of appending this agent's name.
274+
* @return the new context for this agent.
273275
*/
274-
private InvocationContext createInvocationContext(InvocationContext parentContext) {
276+
private InvocationContext createInvocationContext(
277+
InvocationContext parentContext, boolean keepBranch) {
275278
InvocationContext.Builder builder = parentContext.toBuilder();
276279
builder.agent(this);
277-
// Check for branch to be truthy (not None, not empty string),
278-
parentContext
279-
.branch()
280-
.filter(s -> !s.isEmpty())
281-
.ifPresent(branch -> builder.branch(branch + "." + name()));
280+
if (!keepBranch) {
281+
// Check for branch to be truthy (not None, not empty string),
282+
parentContext
283+
.branch()
284+
.filter(s -> !s.isEmpty())
285+
.ifPresent(branch -> builder.branch(branch + "." + name()));
286+
}
282287
return builder.build();
283288
}
284289

@@ -289,24 +294,26 @@ private InvocationContext createInvocationContext(InvocationContext parentContex
289294
* @return stream of agent-generated events.
290295
*/
291296
public Flowable<Event> runAsync(InvocationContext parentContext) {
292-
return runWithCallbacks(parentContext, this::runAsyncImpl);
297+
return runWithCallbacks(parentContext, /* keepBranch= */ false, this::runAsyncImpl);
293298
}
294299

295300
/**
296301
* Runs the agent with the given implementation.
297302
*
298303
* @param parentContext Parent context to inherit.
304+
* @param keepBranch Whether to keep the parent's branch instead of appending this agent's name.
299305
* @param runImplementation The agent-specific logic to run.
300306
* @return stream of agent-generated events.
301307
*/
302308
private Flowable<Event> runWithCallbacks(
303309
InvocationContext parentContext,
310+
boolean keepBranch,
304311
Function<InvocationContext, Flowable<Event>> runImplementation) {
305312
return Flowable.using(
306313
() -> {
307314
Context otelContext = Context.current();
308315
return Instrumentation.recordAgentInvocation(
309-
createInvocationContext(parentContext), this, otelContext);
316+
createInvocationContext(parentContext, keepBranch), this, otelContext);
310317
},
311318
agentInvocation -> {
312319
InvocationContext invocationContext = agentInvocation.getCtx();
@@ -445,7 +452,7 @@ private Maybe<Event> callCallback(
445452
* @return stream of agent-generated events.
446453
*/
447454
public Flowable<Event> runLive(InvocationContext parentContext) {
448-
return runWithCallbacks(parentContext, this::runLiveImpl);
455+
return runWithCallbacks(parentContext, /* keepBranch= */ false, this::runLiveImpl);
449456
}
450457

451458
/**
@@ -488,14 +495,26 @@ private Event checkpointEvent(InvocationContext context, EventActions actions) {
488495
}
489496

490497
/**
491-
* Runs this agent as a workflow node through its {@link #runAsync} lifecycle, including its
492-
* callbacks. The node input is not used.
498+
* Runs this agent as a workflow node, on the node's branch so the nodes after it see its events.
499+
* It runs the callbacks and {@link #runAsyncImpl} that {@link #runAsync} runs, but does not call
500+
* an override of {@link #runAsync}. The node input is not used, and each event's author is copied
501+
* into the node state so that the agent's events keep their own authors instead of an inherited
502+
* author or the node's name.
503+
*
504+
* @throws IllegalStateException if {@code context} is not the context of a workflow node.
493505
*/
494506
@Experimental
495507
@Override
496508
public Flowable<Event> runNode(
497509
com.google.adk.agents.Context context, @Nullable Object nodeInput) {
498-
return runAsync(context.invocationContext());
510+
NodeExecutionState nodeState = context.requireNodeState();
511+
return runWithCallbacks(context.invocationContext(), /* keepBranch= */ true, this::runAsyncImpl)
512+
.doOnNext(
513+
event -> {
514+
if (!isNullOrEmpty(event.author())) {
515+
nodeState.eventAuthor(event.author());
516+
}
517+
});
499518
}
500519

501520
/**

‎core/src/main/java/com/google/adk/agents/Context.java‎

Lines changed: 94 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,16 @@
1616

1717
package com.google.adk.agents;
1818

19+
import com.google.adk.annotations.Experimental;
1920
import com.google.adk.artifacts.ListArtifactsResponse;
2021
import com.google.adk.events.EventActions;
2122
import com.google.adk.events.ToolConfirmation;
2223
import com.google.adk.memory.SearchMemoryResponse;
2324
import com.google.adk.sessions.State;
25+
import com.google.adk.workflow.NodeExecutionState;
26+
import com.google.adk.workflow.Route;
2427
import com.google.common.base.Preconditions;
28+
import com.google.common.collect.ImmutableList;
2529
import com.google.genai.types.Part;
2630
import io.reactivex.rxjava3.core.Completable;
2731
import io.reactivex.rxjava3.core.Maybe;
@@ -31,12 +35,13 @@
3135
import org.jspecify.annotations.Nullable;
3236

3337
/**
34-
* The context passed to callbacks and tools during an invocation.
38+
* The context passed to callbacks, tools, and workflow nodes during an invocation.
3539
*
3640
* <p>Agent and model callbacks receive a {@link CallbackContext}, and tools and tool callbacks a
3741
* {@link com.google.adk.tools.ToolContext}; both extend this class, so a helper that serves both,
3842
* or a function tool's {@code toolContext} parameter, can take a {@code Context}. {@link
39-
* #requestConfirmation()} needs a function call ID, which only a tool call has.
43+
* #requestConfirmation()} requires a function call ID, which only a tool call has, while {@link
44+
* #requireNodeState()} and the other workflow node methods require the context of a workflow node.
4045
*/
4146
public class Context extends ReadonlyContext {
4247

@@ -45,6 +50,7 @@ public class Context extends ReadonlyContext {
4550

4651
private final State state;
4752
private final @Nullable String eventId;
53+
private final @Nullable NodeExecutionState nodeState;
4854
private Optional<String> functionCallId = Optional.empty();
4955
private Optional<ToolConfirmation> toolConfirmation = Optional.empty();
5056

@@ -59,10 +65,32 @@ public class Context extends ReadonlyContext {
5965
InvocationContext invocationContext,
6066
@Nullable EventActions eventActions,
6167
@Nullable String eventId) {
68+
this(invocationContext, eventActions, eventId, /* nodeState= */ null);
69+
}
70+
71+
/**
72+
* Creates the context of the workflow node run whose state is {@code nodeState}. Only the
73+
* workflow runtime creates a node state.
74+
*/
75+
@Experimental
76+
public Context(InvocationContext invocationContext, NodeExecutionState nodeState) {
77+
this(
78+
invocationContext,
79+
/* eventActions= */ null,
80+
/* eventId= */ null,
81+
Preconditions.checkNotNull(nodeState));
82+
}
83+
84+
private Context(
85+
InvocationContext invocationContext,
86+
@Nullable EventActions eventActions,
87+
@Nullable String eventId,
88+
@Nullable NodeExecutionState nodeState) {
6289
super(invocationContext);
6390
this.eventActions = eventActions != null ? eventActions : EventActions.builder().build();
6491
this.state = new State(invocationContext.session().state(), this.eventActions.stateDelta());
6592
this.eventId = eventId;
93+
this.nodeState = nodeState;
6694
}
6795

6896
/** Returns the delta-aware state of the current context. */
@@ -223,4 +251,68 @@ public Single<SearchMemoryResponse> searchMemory(String query) {
223251
.searchMemory(
224252
invocationContext.session().appName(), invocationContext.session().userId(), query);
225253
}
254+
255+
/**
256+
* Returns the state of the workflow node run this context belongs to.
257+
*
258+
* @throws IllegalStateException if this is not the context of a workflow node.
259+
*/
260+
@Experimental
261+
public final NodeExecutionState requireNodeState() {
262+
Preconditions.checkState(
263+
nodeState != null, "Only the context of a workflow node has a node state.");
264+
return nodeState;
265+
}
266+
267+
/**
268+
* Returns the author of the events this workflow node emits, or empty to use the node's name.
269+
*
270+
* @throws IllegalStateException if this is not the context of a workflow node.
271+
*/
272+
@Experimental
273+
public final String eventAuthor() {
274+
return requireNodeState().eventAuthor();
275+
}
276+
277+
/**
278+
* Sets the author of the events this workflow node emits.
279+
*
280+
* @throws IllegalStateException if this is not the context of a workflow node.
281+
*/
282+
@Experimental
283+
public final void eventAuthor(String eventAuthor) {
284+
requireNodeState().eventAuthor(eventAuthor);
285+
}
286+
287+
/**
288+
* Returns this workflow node's output, or empty if it has produced none.
289+
*
290+
* @throws IllegalStateException if this is not the context of a workflow node.
291+
*/
292+
@Experimental
293+
public final Optional<Object> output() {
294+
return requireNodeState().output();
295+
}
296+
297+
/**
298+
* Returns the routes this workflow node selected, or empty if it made no routing decision. An
299+
* empty list is a decision that selects no route.
300+
*
301+
* @throws IllegalStateException if this is not the context of a workflow node.
302+
*/
303+
@Experimental
304+
public final Optional<ImmutableList<Route>> routes() {
305+
return requireNodeState().selectedRoutes();
306+
}
307+
308+
/**
309+
* Selects this workflow node's routes, which decide the outgoing edges to follow, replacing any
310+
* earlier selection.
311+
*
312+
* @throws IllegalStateException if this is not the context of a workflow node.
313+
*/
314+
@Experimental
315+
public final void routes(List<? extends Route> routes) {
316+
requireNodeState().selectRoutes(routes);
317+
}
226318
}

0 commit comments

Comments
 (0)