generateContent(LlmRequest request, boolean stream) {
+ seen.add(request);
+ Content last = request.contents().isEmpty() ? null : request.contents().get(request.contents().size() - 1);
+ boolean hasToolResult = last != null && last.parts().isPresent()
+ && last.parts().get().stream().anyMatch(p -> p.functionResponse().isPresent());
+
+ Content reply;
+ if (hasToolResult) {
+ String result = last.parts().get().stream()
+ .map(p -> p.functionResponse().get().response().orElse(Map.of()).toString())
+ .findFirst().orElse("");
+ reply = text("Findings: " + result);
+ } else if (!request.tools().isEmpty()) {
+ String tool = request.tools().keySet().iterator().next();
+ reply = Content.builder().role("model")
+ .parts(Part.fromFunctionCall(tool, Map.of("orderId", "A-1001"))).build();
+ } else {
+ String instruction = request.getFirstSystemInstruction().orElse("");
+ int at = instruction.indexOf("Findings:");
+ String facts = "(no findings were passed in)";
+ if (at >= 0) {
+ int end = instruction.indexOf('\n', at); // ADK appends an identity paragraph after ours
+ facts = end > 0 ? instruction.substring(at, end) : instruction.substring(at);
+ }
+ reply = text("Hello, here is your update. " + facts);
+ }
+ return Flowable.just(LlmResponse.builder().content(reply).build());
+ }
+
+ @Override
+ public BaseLlmConnection connect(LlmRequest request) {
+ throw new UnsupportedOperationException("live connections are not needed here");
+ }
+
+ private static Content text(String s) {
+ return Content.builder().role("model").parts(Part.fromText(s)).build();
+ }
+}
diff --git a/adk/src/main/java/com/ankurm/adk/SupportPipeline.java b/adk/src/main/java/com/ankurm/adk/SupportPipeline.java
new file mode 100644
index 0000000..ddac6cf
--- /dev/null
+++ b/adk/src/main/java/com/ankurm/adk/SupportPipeline.java
@@ -0,0 +1,46 @@
+package com.ankurm.adk;
+
+import com.google.adk.agents.BaseAgent;
+import com.google.adk.agents.LlmAgent;
+import com.google.adk.agents.SequentialAgent;
+import com.google.adk.models.BaseLlm;
+import com.google.adk.tools.FunctionTool;
+
+/**
+ * Two agents in a fixed order:
+ *
+ * researcher (has the lookupOrder tool, saves its answer under state key "findings")
+ * |
+ * writer (no tools, its instruction reads {findings} from session state)
+ *
+ * A {@link SequentialAgent} runs its sub-agents one after the other in the same session, so the
+ * shared session state is how the first agent hands work to the second.
+ */
+public final class SupportPipeline {
+
+ private SupportPipeline() {}
+
+ public static BaseAgent build(BaseLlm model) {
+ LlmAgent researcher = LlmAgent.builder()
+ .name("researcher")
+ .description("Looks up the order the customer asks about.")
+ .model(model)
+ .instruction("Find the order the customer mentions by calling the lookupOrder tool.")
+ .tools(FunctionTool.create(OrderTools.class, "lookupOrder"))
+ .outputKey("findings")
+ .build();
+
+ LlmAgent writer = LlmAgent.builder()
+ .name("writer")
+ .description("Writes the customer reply from the research.")
+ .model(model)
+ .instruction("Write a short friendly reply to the customer using only this: {findings}")
+ .build();
+
+ return SequentialAgent.builder()
+ .name("support_pipeline")
+ .description("Research the order, then write the reply.")
+ .subAgents(researcher, writer)
+ .build();
+ }
+}
diff --git a/adk/src/test/java/com/ankurm/adk/SupportPipelineTest.java b/adk/src/test/java/com/ankurm/adk/SupportPipelineTest.java
new file mode 100644
index 0000000..0da08dc
--- /dev/null
+++ b/adk/src/test/java/com/ankurm/adk/SupportPipelineTest.java
@@ -0,0 +1,99 @@
+package com.ankurm.adk;
+
+import com.google.adk.agents.BaseAgent;
+import com.google.adk.events.Event;
+import com.google.adk.runner.InMemoryRunner;
+import com.google.adk.sessions.Session;
+import com.google.genai.types.Content;
+import com.google.genai.types.Part;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class SupportPipelineTest {
+
+ private static final String APP = "support-app";
+
+ private static String describe(Event e) {
+ StringBuilder sb = new StringBuilder();
+ e.content().flatMap(Content::parts).ifPresent(parts -> {
+ for (Part p : parts) {
+ p.text().ifPresent(t -> sb.append("text: ").append(t));
+ p.functionCall().ifPresent(f -> sb.append("call ").append(f.name().orElse("?"))
+ .append(f.args().orElse(java.util.Map.of())));
+ p.functionResponse().ifPresent(f -> sb.append("result ").append(f.name().orElse("?"))
+ .append(f.response().orElse(java.util.Map.of())));
+ }
+ });
+ return sb.toString();
+ }
+
+ @Test
+ void twoAgentsRunInOrderAndShareSessionState() throws Exception {
+ var t = new Transcript("01-two-agents-in-order.txt");
+ var llm = new ScriptedLlm();
+ BaseAgent root = SupportPipeline.build(llm);
+ var runner = new InMemoryRunner(root, APP);
+ Session session = runner.sessionService().createSession(APP, "user-1").blockingGet();
+
+ List events = runner.runAsync("user-1", session.id(),
+ Content.fromParts(Part.fromText("Where is my order A-1001?"))).toList().blockingGet();
+
+ for (Event e : events) {
+ t.line(String.format("%-11s %s", e.author(), describe(e)));
+ }
+
+ Session after = runner.sessionService().getSession(APP, "user-1", session.id(), java.util.Optional.empty()).blockingGet();
+ t.line("session state keys: " + after.state().keySet());
+ t.line("state[findings] = " + after.state().get("findings"));
+ t.write();
+
+ assertEquals("researcher", events.get(0).author());
+ assertEquals("writer", events.get(events.size() - 1).author());
+ assertTrue(after.state().containsKey("findings"));
+ assertTrue(describe(events.get(events.size() - 1)).contains("BlueDart"));
+ }
+
+ @Test
+ void theWriterModelReceivesTheResearcherOutputInItsInstruction() throws Exception {
+ var t = new Transcript("02-state-injected-into-instruction.txt");
+ var llm = new ScriptedLlm();
+ var runner = new InMemoryRunner(SupportPipeline.build(llm), APP);
+ Session session = runner.sessionService().createSession(APP, "user-2").blockingGet();
+ runner.runAsync("user-2", session.id(), Content.fromParts(Part.fromText("Status of A-1001?")))
+ .toList().blockingGet();
+
+ t.line("model calls made: " + llm.seen.size());
+ for (int i = 0; i < llm.seen.size(); i++) {
+ var r = llm.seen.get(i);
+ t.line("call " + (i + 1) + ": tools offered = " + r.tools().keySet());
+ }
+ String writerInstruction = llm.seen.get(llm.seen.size() - 1).getFirstSystemInstruction().orElse("");
+ t.line("writer system instruction contains the findings: " + writerInstruction.contains("BlueDart"));
+ t.write();
+
+ assertEquals(3, llm.seen.size()); // researcher x2 (call, then answer), writer x1
+ assertTrue(writerInstruction.contains("BlueDart"));
+ assertFalse(writerInstruction.contains("{findings}"));
+ }
+
+ @Test
+ void sessionsAreSeparatePerUser() throws Exception {
+ var t = new Transcript("03-sessions-per-user.txt");
+ var runner = new InMemoryRunner(SupportPipeline.build(new ScriptedLlm()), APP);
+ Session s1 = runner.sessionService().createSession(APP, "user-1").blockingGet();
+ Session s2 = runner.sessionService().createSession(APP, "user-2").blockingGet();
+ runner.runAsync("user-1", s1.id(), Content.fromParts(Part.fromText("order A-1001"))).toList().blockingGet();
+
+ var a = runner.sessionService().getSession(APP, "user-1", s1.id(), java.util.Optional.empty()).blockingGet();
+ var b = runner.sessionService().getSession(APP, "user-2", s2.id(), java.util.Optional.empty()).blockingGet();
+ t.line("user-1 events: " + a.events().size() + ", keys: " + a.state().keySet());
+ t.line("user-2 events: " + b.events().size() + ", keys: " + b.state().keySet());
+ t.write();
+
+ assertTrue(a.events().size() > 0);
+ assertEquals(0, b.events().size());
+ }
+}
diff --git a/adk/src/test/java/com/ankurm/adk/Transcript.java b/adk/src/test/java/com/ankurm/adk/Transcript.java
new file mode 100644
index 0000000..465f03a
--- /dev/null
+++ b/adk/src/test/java/com/ankurm/adk/Transcript.java
@@ -0,0 +1,23 @@
+package com.ankurm.adk;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Collects lines and writes them to output/NN-name.txt, so the test IS the transcript. */
+final class Transcript {
+ private final List lines = new ArrayList<>();
+ private final String file;
+
+ Transcript(String file) { this.file = file; }
+
+ void line(String s) { lines.add(s); }
+
+ void write() throws IOException {
+ Path dir = Path.of("output");
+ Files.createDirectories(dir);
+ Files.write(dir.resolve(file), lines);
+ }
+}
diff --git a/pom.xml b/pom.xml
index 1e49008..61af776 100644
--- a/pom.xml
+++ b/pom.xml
@@ -15,6 +15,7 @@
jlama
onnx-djl
langgraph4j
+ adk
@@ -27,6 +28,7 @@
0.38.0
1.9.5
+ 1.11.0
1.31.0
4.1.1
6.1.3