Skip to content

Commit 0cc5778

Browse files
committed
Add Google ADK Java integration samples
1 parent e4db5f7 commit 0cc5778

33 files changed

Lines changed: 1392 additions & 0 deletions

‎README.md‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,23 @@ To run any of the SpringBoot samples in your Temporal Cloud namespace:
228228

229229
3. Follow the previous section from step 2
230230

231+
### Running Google ADK samples
232+
233+
The [Google ADK samples](googleadk) use the Public Preview
234+
[`io.temporal:google-adk`](https://github.com/temporalio/ai-integrations/tree/main/java/google-adk)
235+
integration. They cover a basic agent, Activity tools, parallel agents, MCP, human approval, and a
236+
conversation that continues as new. The module requires Temporal Java SDK 1.40.0 or later.
237+
238+
Run the Worker and a sample in separate terminals after starting `temporal server start-dev`:
239+
240+
```bash
241+
export GOOGLE_API_KEY=your_key
242+
./gradlew :googleadk:worker
243+
./gradlew :googleadk:starter --args='basic'
244+
```
245+
246+
See the [sample README](googleadk/README.md) for the other scenarios and credential-free tests.
247+
231248
### Running Spring AI Samples
232249

233250
The Spring AI samples demonstrate the [Temporal Spring AI integration](https://github.com/temporalio/sdk-java/tree/master/temporal-spring-ai), which makes Spring AI agents durable on Temporal — model calls run as Temporal Activities recorded in Workflow history, and tools are dispatched per their type so they fit Workflow execution.

‎googleadk/README.md‎

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
# Google ADK on Temporal
2+
3+
Run native [Google ADK Java](https://github.com/google/adk-java) agents in Temporal Workflows using
4+
the standalone [`io.temporal:google-adk:0.1.0`](https://central.sonatype.com/artifact/io.temporal/google-adk)
5+
integration. This integration is **Public Preview**, not Generally Available. APIs and configuration
6+
may change before General Availability.
7+
8+
Model calls and MCP operations run as Temporal Activities. Agent orchestration, ordinary function
9+
tools, and session state stay in the Workflow. Use `TemporalAdk.runnerBuilder()` to keep ADK's time,
10+
identifiers, and parallel execution deterministic, and `TemporalAdk.run()` to await an agent turn.
11+
12+
## Requirements
13+
14+
- Java 17 or later.
15+
- `io.temporal:google-adk:0.1.0`, which brings Google ADK Java 1.12.0.
16+
- Temporal Java SDK and testing library 1.40.0 or later. The module declares a strict `[1.40.0,2.0)`
17+
version range, preferring 1.40.0, independently of the other samples' SDK version.
18+
- For a live run, a local Temporal Service (`temporal server start-dev`) and `GOOGLE_API_KEY` in the
19+
Worker's environment. The Google GenAI client also supports Vertex AI's environment configuration.
20+
21+
The plugin's version is independent of the SDK version. The plugin does not supply `temporal-sdk`;
22+
the application adds it explicitly. Dependencies come from Maven Central.
23+
24+
## Run a sample
25+
26+
Start a local Temporal Service:
27+
28+
```bash
29+
temporal server start-dev
30+
```
31+
32+
In another terminal, start the shared Worker from the repository root:
33+
34+
```bash
35+
export GOOGLE_API_KEY=your_key
36+
./gradlew :googleadk:worker
37+
```
38+
39+
Then run one of these commands in a third terminal:
40+
41+
```bash
42+
./gradlew :googleadk:starter --args='basic'
43+
./gradlew :googleadk:starter --args='tools'
44+
./gradlew :googleadk:starter --args='multiagent'
45+
./gradlew :googleadk:starter --args='mcp'
46+
```
47+
48+
The starter prints the agent's answer. All samples share the `google-adk-samples` Task Queue and
49+
start a new Workflow ID for each invocation. Inspect their Activities at `http://localhost:8233`.
50+
51+
| Scenario | Source | What it shows |
52+
| --- | --- | --- |
53+
| Basic | [`basic/`](src/main/java/io/temporal/samples/googleadk/basic) | A native `LlmAgent` with `TemporalModel`; one model call becomes `googleadk.InvokeModel`. |
54+
| Activity tools | [`tools/`](src/main/java/io/temporal/samples/googleadk/tools) | `ActivityTools.create` exposes a weather Activity to the agent. The Activity returns sample weather data; replace its body with provider I/O. |
55+
| Multiple agents | [`multiagent/`](src/main/java/io/temporal/samples/googleadk/multiagent) | A `ParallelAgent` schedules two model Activities concurrently on Workflow threads. |
56+
| MCP | [`mcp/`](src/main/java/io/temporal/samples/googleadk/mcp) | A self-contained Java stdio echo server, started on first use by the Worker. Listing and calling tools become Activities. No Node or separately running MCP service is required. |
57+
| Human approval | [`humanintheloop/`](src/main/java/io/temporal/samples/googleadk/humanintheloop) | An Activity tool waits for a human decision delivered by Signal. The shipping Activity returns sample data and makes no fulfillment request. |
58+
| Conversation | [`chat/`](src/main/java/io/temporal/samples/googleadk/chat) | Signals queue messages, a Query returns the latest reply, and a session snapshot preserves the conversation through Continue-As-New. |
59+
60+
[`SampleWorker`](src/main/java/io/temporal/samples/googleadk/SampleWorker.java) registers the
61+
`GoogleAdkPlugin` on the Temporal Client, so its Workers inherit the model/MCP Activities and payload
62+
converter. Credentials stay on the Worker. Native GenAI retries are disabled with one HTTP attempt;
63+
Temporal Activity retries handle model failures. Register the plugin on external clients too, as
64+
[`SampleStarter`](src/main/java/io/temporal/samples/googleadk/SampleStarter.java) does.
65+
66+
## Approve or reject a tool call
67+
68+
Start a fulfillment Workflow:
69+
70+
```bash
71+
./gradlew :googleadk:starter --args='approval'
72+
```
73+
74+
Use the printed Workflow ID in place of `WORKFLOW_ID`. Query the pending tool name and arguments,
75+
then approve or reject the calls from that turn:
76+
77+
```bash
78+
./gradlew :googleadk:starter --args='pending WORKFLOW_ID'
79+
./gradlew :googleadk:starter --args='decide WORKFLOW_ID true'
80+
# Or reject instead:
81+
./gradlew :googleadk:starter --args='decide WORKFLOW_ID false'
82+
./gradlew :googleadk:starter --args='result WORKFLOW_ID'
83+
```
84+
85+
`pending` can initially be empty while the model runs; query again once the agent asks for approval.
86+
The decision command sends a Signal; `result` waits for the final answer. The Workflow uses `Workflow.await` while paused,
87+
then supplies `Hitl.confirmationResponse` to the next ADK turn. A rejected call never schedules the
88+
shipping Activity. If the model asks for another confirmation, query and decide again.
89+
90+
## Continue a conversation as new
91+
92+
```bash
93+
./gradlew :googleadk:starter --args='chat'
94+
./gradlew :googleadk:starter --args='message WORKFLOW_ID My name is Ada.'
95+
./gradlew :googleadk:starter --args='reply WORKFLOW_ID'
96+
./gradlew :googleadk:starter --args='message WORKFLOW_ID Suggest a trip to Chicago.'
97+
./gradlew :googleadk:starter --args='message WORKFLOW_ID What should I pack?'
98+
./gradlew :googleadk:starter --args='message WORKFLOW_ID What is my name?'
99+
./gradlew :googleadk:starter --args='reply WORKFLOW_ID'
100+
./gradlew :googleadk:starter --args='finish WORKFLOW_ID'
101+
```
102+
103+
`message` queues a turn without waiting for the model. Query again after the model Activity completes
104+
to see its new reply. After three turns, the Workflow exports its ADK session, continues as new, and
105+
imports the session in the next Run. Queued messages and the latest reply travel with the snapshot.
106+
`finish` drains pending messages and returns the final answer. Continue-As-New limits each Run's
107+
history; the snapshot still contains the conversation and must fit within Temporal's payload limits.
108+
109+
## Test without provider credentials
110+
111+
```bash
112+
./gradlew :googleadk:test :googleadk:spotlessCheck
113+
```
114+
115+
The tests use `ScriptedModel` from the published integration and `TestWorkflowEnvironment`; they
116+
need no API keys or external Temporal Service. The MCP test starts the real local Java echo server.
117+
Tests verify Activity retries and results, concurrent model scheduling, approval and rejection,
118+
session snapshots and queued messages across Continue-As-New, and replay on a fresh Worker without
119+
repeating model or tool calls.
120+
121+
## Limitations
122+
123+
- Plain ADK function tools run in the Workflow and must be deterministic. Put network, file, and
124+
other I/O in Activities or MCP tools.
125+
- Build Runners with `TemporalAdk.runnerBuilder()`. Do not override agent-level schedulers or
126+
executors, or use RxJava blocking APIs inside a Workflow.
127+
- Streaming chunks arrive after the model Activity completes. Token streaming out of a Workflow
128+
and live bidirectional mode are unsupported.
129+
- Session snapshots preserve session-scoped state and events. They do not carry app-, user-, or
130+
temporary-scoped state.
131+
- ADK OpenTelemetry spans are emitted again during replay when a tracing SDK is installed.
132+
133+
See the [integration README](https://github.com/temporalio/ai-integrations/tree/main/java/google-adk)
134+
for the full API and limitations.

‎googleadk/build.gradle‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// ADK samples consume the independently versioned integration from Maven Central.
2+
dependencies {
3+
implementation 'io.temporal:google-adk:0.1.0'
4+
implementation('io.temporal:temporal-sdk') {
5+
version {
6+
strictly '[1.40.0,2.0['
7+
prefer '1.40.0'
8+
}
9+
}
10+
testImplementation('io.temporal:temporal-testing') {
11+
version {
12+
strictly '[1.40.0,2.0['
13+
prefer '1.40.0'
14+
}
15+
}
16+
implementation 'io.modelcontextprotocol.sdk:mcp:2.0.0'
17+
implementation 'io.modelcontextprotocol.sdk:mcp-json-jackson3:2.0.0'
18+
runtimeOnly 'ch.qos.logback:logback-classic:1.5.18'
19+
20+
testImplementation platform('org.junit:junit-bom:5.11.4')
21+
testImplementation 'org.junit.jupiter:junit-jupiter'
22+
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
23+
24+
errorproneJavac 'com.google.errorprone:javac:9+181-r4173-1'
25+
errorprone 'com.google.errorprone:error_prone_core:2.28.0'
26+
}
27+
28+
tasks.register('worker', JavaExec) {
29+
classpath = sourceSets.main.runtimeClasspath
30+
mainClass = 'io.temporal.samples.googleadk.SampleWorker'
31+
}
32+
33+
tasks.register('starter', JavaExec) {
34+
classpath = sourceSets.main.runtimeClasspath
35+
mainClass = 'io.temporal.samples.googleadk.SampleStarter'
36+
}
37+
38+
test {
39+
// Gradle's test worker uses its own bootstrap classpath. The MCP subprocess
40+
// needs the sample application's classpath, just as it has in the worker task.
41+
doFirst {
42+
systemProperty 'googleadk.echo.classpath', sourceSets.main.runtimeClasspath.asPath
43+
}
44+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package io.temporal.samples.googleadk;
2+
3+
import com.google.adk.events.Event;
4+
import com.google.genai.types.Content;
5+
import java.util.List;
6+
7+
/** Shared names and a deterministic helper for reading an agent's final answer. */
8+
public final class AgentSupport {
9+
public static final String MODEL = "gemini-2.5-flash";
10+
public static final String TASK_QUEUE = "google-adk-samples";
11+
public static final String USER = "user-1";
12+
public static final String SESSION = "session-1";
13+
14+
private AgentSupport() {}
15+
16+
public static String lastText(List<Event> events) {
17+
for (int i = events.size() - 1; i >= 0; i--) {
18+
String text = events.get(i).content().map(Content::text).orElse("");
19+
if (text != null && !text.isEmpty()) {
20+
return text;
21+
}
22+
}
23+
return "";
24+
}
25+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
package io.temporal.samples.googleadk;
2+
3+
import io.temporal.client.WorkflowClient;
4+
import io.temporal.client.WorkflowClientOptions;
5+
import io.temporal.client.WorkflowOptions;
6+
import io.temporal.googleadk.GoogleAdkPlugin;
7+
import io.temporal.samples.googleadk.basic.BasicWorkflow;
8+
import io.temporal.samples.googleadk.chat.ChatInput;
9+
import io.temporal.samples.googleadk.chat.ChatWorkflow;
10+
import io.temporal.samples.googleadk.humanintheloop.ApprovalWorkflow;
11+
import io.temporal.samples.googleadk.mcp.McpWorkflow;
12+
import io.temporal.samples.googleadk.multiagent.MultiAgentWorkflow;
13+
import io.temporal.samples.googleadk.tools.ToolsWorkflow;
14+
import io.temporal.serviceclient.WorkflowServiceStubs;
15+
import java.util.Arrays;
16+
import java.util.UUID;
17+
18+
/** Starts samples or sends messages to their existing Workflow Executions. */
19+
public final class SampleStarter {
20+
private SampleStarter() {}
21+
22+
public static void main(String[] args) {
23+
if (args.length == 0) {
24+
throw new IllegalArgumentException(
25+
"Choose basic, tools, multiagent, mcp, approval, chat, pending, decide, result, message, reply, or finish.");
26+
}
27+
WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs();
28+
try {
29+
// Register the plugin on external clients too, so Queries/Signals use the same converter.
30+
WorkflowClient client =
31+
WorkflowClient.newInstance(
32+
service,
33+
WorkflowClientOptions.newBuilder()
34+
.setPlugins(GoogleAdkPlugin.newBuilder().build())
35+
.build());
36+
String id = "google-adk-" + UUID.randomUUID();
37+
WorkflowOptions options =
38+
WorkflowOptions.newBuilder()
39+
.setTaskQueue(AgentSupport.TASK_QUEUE)
40+
.setWorkflowId(id)
41+
.build();
42+
switch (args[0]) {
43+
case "basic":
44+
// @@@SNIPSTART java-google-adk-starter
45+
BasicWorkflow workflow = client.newWorkflowStub(BasicWorkflow.class, options);
46+
System.out.println(workflow.ask("What does Durable Execution mean?"));
47+
// @@@SNIPEND
48+
break;
49+
case "tools":
50+
System.out.println(
51+
client
52+
.newWorkflowStub(ToolsWorkflow.class, options)
53+
.ask("What's the weather in Chicago?"));
54+
break;
55+
case "multiagent":
56+
client
57+
.newWorkflowStub(MultiAgentWorkflow.class, options)
58+
.plan("Chicago")
59+
.forEach(System.out::println);
60+
break;
61+
case "mcp":
62+
System.out.println(
63+
client.newWorkflowStub(McpWorkflow.class, options).echo("Hello from Temporal"));
64+
break;
65+
case "approval":
66+
ApprovalWorkflow approval = client.newWorkflowStub(ApprovalWorkflow.class, options);
67+
WorkflowClient.start(approval::fulfill, "Ship order demo-order, after confirmation.");
68+
System.out.println("Workflow ID: " + id);
69+
break;
70+
case "chat":
71+
ChatWorkflow chat = client.newWorkflowStub(ChatWorkflow.class, options);
72+
WorkflowClient.start(chat::chat, ChatInput.empty());
73+
System.out.println("Workflow ID: " + id);
74+
break;
75+
case "pending":
76+
requireArgs(args, 2);
77+
client
78+
.newWorkflowStub(ApprovalWorkflow.class, args[1])
79+
.pendingConfirmations()
80+
.forEach(System.out::println);
81+
break;
82+
case "decide":
83+
requireArgs(args, 3);
84+
if (!args[2].equals("true") && !args[2].equals("false")) {
85+
throw new IllegalArgumentException("Decision must be true or false.");
86+
}
87+
ApprovalWorkflow pending = client.newWorkflowStub(ApprovalWorkflow.class, args[1]);
88+
pending.decide(Boolean.parseBoolean(args[2]));
89+
System.out.println("Decision sent.");
90+
break;
91+
case "result":
92+
requireArgs(args, 2);
93+
System.out.println(client.newUntypedWorkflowStub(args[1]).getResult(String.class));
94+
break;
95+
case "message":
96+
requireArgs(args, 3);
97+
client
98+
.newWorkflowStub(ChatWorkflow.class, args[1])
99+
.message(String.join(" ", Arrays.copyOfRange(args, 2, args.length)));
100+
break;
101+
case "reply":
102+
requireArgs(args, 2);
103+
System.out.println(client.newWorkflowStub(ChatWorkflow.class, args[1]).lastReply());
104+
break;
105+
case "finish":
106+
requireArgs(args, 2);
107+
ChatWorkflow conversation = client.newWorkflowStub(ChatWorkflow.class, args[1]);
108+
conversation.finish();
109+
System.out.println(
110+
io.temporal.client.WorkflowStub.fromTyped(conversation).getResult(String.class));
111+
break;
112+
default:
113+
throw new IllegalArgumentException("Unknown command: " + args[0]);
114+
}
115+
} finally {
116+
service.shutdown();
117+
}
118+
}
119+
120+
private static void requireArgs(String[] args, int count) {
121+
if (args.length < count) {
122+
throw new IllegalArgumentException("Missing arguments; see googleadk/README.md.");
123+
}
124+
}
125+
}

0 commit comments

Comments
 (0)