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
10 changes: 5 additions & 5 deletions core/src/main/java/com/google/adk/agents/RunConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,14 @@ public enum StreamingMode {
}

/**
* Tool execution mode for the runner, when they are multiple tools requested (by the models or
* callbacks).
* Execution mode when the model requests multiple tools.
*
* <p>NONE: default to PARALLEL.
* <p>NONE: defaults to SEQUENTIAL.
*
* <p>SEQUENTIAL: Multiple tools are executed in the order they are requested.
* <p>SEQUENTIAL: tools execute in request order on the caller thread.
*
* <p>PARALLEL: Multiple tools are executed in parallel.
* <p>PARALLEL: tools execute concurrently on worker threads. Tool implementations must be
* thread-safe.
*/
public enum ToolExecutionMode {
NONE,
Expand Down
10 changes: 3 additions & 7 deletions core/src/main/java/com/google/adk/flows/llmflows/Functions.java
Original file line number Diff line number Diff line change
Expand Up @@ -236,19 +236,15 @@ public static Maybe<Event> handleFunctionCallsLive(
}

/**
* Builds the tool-execution {@link Observable}.
*
* <p>SEQUENTIAL (or a single call, where parallelism is moot) runs on the caller thread via
* {@code concatMapMaybe} to keep synchronous semantics. PARALLEL with multiple calls dispatches
* each tool on a worker so blocking calls run concurrently; {@code concatMapEager} preserves
* input order required by {@link #mergeParallelFunctionResponseEvents}.
* Sequential by default; only {@link ToolExecutionMode#PARALLEL} with multiple calls dispatches
* tools on workers (using {@code concatMapEager} to preserve input order).
*/
private static Observable<Event> buildToolExecutionObservable(
InvocationContext invocationContext,
List<FunctionCall> validFunctionCalls,
Function<FunctionCall, Maybe<Event>> functionCallMapper) {
boolean parallel =
invocationContext.runConfig().toolExecutionMode() != ToolExecutionMode.SEQUENTIAL
invocationContext.runConfig().toolExecutionMode() == ToolExecutionMode.PARALLEL
&& validFunctionCalls.size() > 1;
if (!parallel) {
return Observable.fromIterable(validFunctionCalls).concatMapMaybe(functionCallMapper);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,43 @@ public void getAskUserConfirmationFunctionCalls_eventWithConfirmationFunctionCal
assertThat(result).containsExactly(confirmationCall1, confirmationCall2);
}

// Default ToolExecutionMode.NONE must execute tools sequentially.
@Test
public void handleFunctionCalls_defaultMode_blockingTools_runSequentially() {
long sleepMillis = 300L;
int toolCount = 2;
InvocationContext invocationContext =
createInvocationContext(createRootAgent(), RunConfig.builder().build());

Map<String, BaseTool> tools = new LinkedHashMap<>();
List<Part> callParts = new ArrayList<>();
for (int i = 1; i <= toolCount; i++) {
String toolName = "slow_tool_" + i;
tools.put(toolName, new SleepingTool(toolName, sleepMillis));
callParts.add(
Part.builder()
.functionCall(
FunctionCall.builder()
.id("call_" + i)
.name(toolName)
.args(ImmutableMap.of())
.build())
.build());
}
Event event =
createEvent("event").toBuilder()
.content(Content.fromParts(callParts.toArray(new Part[0])))
.build();

long start = System.currentTimeMillis();
Event functionResponseEvent =
Functions.handleFunctionCalls(invocationContext, event, tools).blockingGet();
long durationMillis = System.currentTimeMillis() - start;

assertThat(functionResponseEvent).isNotNull();
assertThat(durationMillis).isAtLeast((long) toolCount * sleepMillis);
}

@Test
public void handleFunctionCalls_parallel_blockingTools_runConcurrently_twoTools() {
runParallelBlockingToolsTest(/* toolCount= */ 2);
Expand Down
Loading