diff --git a/README.md b/README.md
index 551cf16..88b29fc 100644
--- a/README.md
+++ b/README.md
@@ -913,3 +913,49 @@ AgentRequestStatistic [requests=8, cachedPrompts=0, inTokens=27393, outTokens=48
```
This trace shows a typical pattern: summarize, identify multiple features, fan out into parallel analysis, merge results, and generate a final answer while preserving usage statistics.
+
+---
+
+# Example Output of Unit Test AgentTest#agenticFlow
+
+## Announcing the New `BaseAgent` Framework: High-Performance, Context-Aware AI Orchestration
+
+We are thrilled to introduce the new **`BaseAgent`** framework, a robust Java-based abstraction designed to streamline complex AI workflows. Built on proven patterns similar to LangChain, this framework empowers developers to create scalable, multi-path agent systems with minimal boilerplate.
+
+At its core, `BaseAgent` provides a standardized lifecycle for interacting with LLMs, handling everything from dynamic prompt generation to sophisticated tool usage and state management. It is engineered to handle high-concurrency workloads while maintaining precise control over conversation history and execution flow.
+
+## Key Capabilities
+
+### 🚀 Parallel Execution with Automatic Aggregation
+Stop managing multiple agent instances manually. The framework automatically decomposes a single agent context into parallel execution paths using **`Split` objects**.
+* **Concurrency:** Leverages `CompletableFuture` and `ExecutorService` to run agent variations simultaneously.
+* **Aggregation:** Automatically filters errors, sorts results, and merges successful outputs before determining the next workflow step.
+* **Visualization:** Includes built-in graph markers to visually track parallel branches and merge points during execution.
+
+### 🌿 Conditional Prompt Branching
+Ditch code duplication for multi-scenario prompts. The framework detects special placeholders (e.g., `[[key]]`) in your templates and dynamically generates distinct execution threads based on context variable values.
+* **Dynamic Paths:** Automatically creates separate agent runs for each unique value in a list (e.g., processing different user IDs or product options in parallel).
+* **Hidden Logic:** Supports "hidden" branches (`[[key|hidden]]`) to influence execution flow without exposing internal logic to the LLM.
+* **Smart Limits:** Calculates branch counts based on the smallest variable list to ensure balanced parallel execution.
+
+### 🔁 Iterative Tool Calling with Retry Logic
+Enable agents to perform complex, multi-step tasks autonomously. The framework implements a robust "Plan-Act-Observed" loop within the execution engine.
+* **Automatic Injection:** Detects tool calls, executes them, and seamlessly appends results back into the conversation history for the LLM to read in the next turn.
+* **Safety Limits:** Prevents infinite loops by capping tool call rounds (default: 4) and retry attempts for parsing failures (default: 2).
+* **Resilience:** Gracefully handles malformed responses or parsing errors, marking specific splits as failed while allowing others to succeed.
+
+### 💬 Dynamic Conversation State Management
+Maintain perfect context awareness across multi-agent chains without manual state tracking.
+* **Smart Initialization:** Automatically constructs conversation histories based on `AgentType`, handling system prompts, new starts, and continuations.
+* **Seamless Transitions:** intelligently merges system prompts into user turns or clears history when switching to completion-only agents.
+* **State Propagation:** Ensures tool call data and conversation turns are correctly preserved or cleared as the workflow moves between agents.
+
+### 🔄 Intelligent Agent Chaining and Routing
+Navigate complex workflows with automatic decision-making based on execution outcomes.
+* **Outcome-Based Routing:** The `nextAgent` logic dynamically selects the subsequent agent based on the current context and agent type (e.g., routing to a `COMPLETION` agent after a `PROCESSING_ONLY` step).
+* **Flexible Types:** Supports diverse agent roles including `START_CONVERSATION`, `CONVERSATION`, `COMPLETION`, and `PROCESSING_ONLY`.
+* **Error Containment:** Isolates execution errors within specific splits, preventing a single failure from halting the entire workflow.
+
+Qwen3.5-9B-Q8_0
+[M, CodeSummarizer, (19184 ms), ↴, M, FeatureIdentifier, (5273 ms), →, ⇉ 1/5, M, FeatureAnalyzer, (52686 ms), 5↣1, →, M, AnswerGenerator, (20392 ms)]
+AgentRequestStatistic [requests=8, cachedPrompts=0, inTokens=28281, cachedInTokens=13498, outTokens=6677, reasoningOutTokens=0, requestDuration=PT4M11.98566101S, getEffectiveDuration()=PT2M32.67066665S, getTps()=228.98, getOutTps()=43.73, getCost()=0]
diff --git a/pom.xml b/pom.xml
index b013fb6..2c23c7c 100755
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
lm-lib
de.extio
lm-lib
- 2.5.7
+ 2.5.8
25
diff --git a/src/main/java/de/extio/lmlib/agent/BaseAgent.java b/src/main/java/de/extio/lmlib/agent/BaseAgent.java
index e3189d4..1d6ebb1 100644
--- a/src/main/java/de/extio/lmlib/agent/BaseAgent.java
+++ b/src/main/java/de/extio/lmlib/agent/BaseAgent.java
@@ -68,6 +68,10 @@ public interface BaseAgent {
}
default ToolCallData toolCallData(final AgentContext context) {
+ final var configuredToolCallData = context.getValue("_toolCallData", ToolCallData.class);
+ if (configuredToolCallData != null) {
+ return configuredToolCallData;
+ }
final var toolDefinitions = this.toolDefinitions(context);
if (toolDefinitions == null || toolDefinitions.isEmpty()) {
final var rememberedToolCallData = context.getToolCallData();
@@ -157,7 +161,7 @@ public interface BaseAgent {
final boolean skipCache = split.context().isSkipCache() || split.context().isAlwaysSkipCache();
split.context().setSkipCache(false);
final var conversation = this.setupConversation(split);
- final var toolCallData = completionClient.supportsToolCalling() ? this.toolCallData(split.context()) : null;
+ var toolCallData = completionClient.supportsToolCalling() ? this.toolCallData(split.context()) : null;
this.rememberToolCallData(split.context(), toolCallData);
boolean parseable = false;
@@ -214,6 +218,9 @@ public interface BaseAgent {
if (toolCallingAgentResponseHandler.handleToolCalls(completion, split.context(), toolCallResults)) {
toolCallRounds++;
this.appendToolCallResults(conversation, completion, toolCallResults.results());
+ if (toolCallData != null && toolCallData.hasTools()) {
+ toolCallData = ToolCallData.auto(toolCallData.tools());
+ }
continue;
}
}
@@ -450,6 +457,7 @@ public interface BaseAgent {
private void clearRememberedToolCallData(final AgentContext context) {
context.setToolCallData(null);
+ context.setValue("_toolCallData", null);
}
private void nextAgent(final AgentContext context, final boolean skipCompletion) {
diff --git a/src/test/java/de/extio/lmlib/agent/AgentTest.java b/src/test/java/de/extio/lmlib/agent/AgentTest.java
index e50a8c0..70af2ec 100644
--- a/src/test/java/de/extio/lmlib/agent/AgentTest.java
+++ b/src/test/java/de/extio/lmlib/agent/AgentTest.java
@@ -11,6 +11,7 @@ import java.util.Map;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicReference;
+import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
@@ -27,6 +28,7 @@ import de.extio.lmlib.agent.responsehandler.TextAgentResponseHandler;
import de.extio.lmlib.agent.responsehandler.ToolCallingAgentResponseHandler;
import de.extio.lmlib.client.ClientService;
import de.extio.lmlib.client.Conversation;
+import de.extio.lmlib.client.ToolCall;
import de.extio.lmlib.client.ToolCallData;
import de.extio.lmlib.client.ToolDefinition;
import de.extio.lmlib.client.ToolParameters;
@@ -35,7 +37,7 @@ import de.extio.lmlib.profile.ModelCategory;
import de.extio.lmlib.profile.ModelProfileService;
/**
- * These tests have been verified with Llama 3.1 8B, Gemma2 27B, Gemma3 27B and GPT-OSS models
+ * These tests have been verified with Llama 3.1 8B, Gemma2 27B, Gemma3 27B, GPT-OSS 20B, and Qwen3.5 9B models
*/
@Disabled("This test requires a running Llama server or a cloud subscription")
@SpringBootTest(webEnvironment = WebEnvironment.NONE)
@@ -193,31 +195,47 @@ public class AgentTest {
@Test
void toolCallingAgenticFlow() {
+ final var weatherTool = new ToolDefinition(
+ "get_weather",
+ "Gets the current weather for a given location.",
+ ToolParameters.create(Map.of("location", "City and country, for example Berlin, Germany")),
+ true);
+ final var trafficTool = new ToolDefinition(
+ "get_traffic",
+ "Gets the current traffic for a given location.",
+ ToolParameters.create(Map.of("location", "City and country, for example Berlin, Germany")),
+ true);
final var agents = Map.of(
"ToolCaller",
new Agent("ToolCaller",
AgentType.START_CONVERSATION,
ModelCategory.MEDIUM,
null,
- "Use the provided tool when the user asks for weather information. After receiving tool results, respond using the forecast from the tool output.",
- "What is the weather in Berlin, Germany? Use the tool.",
- List.of(new ToolDefinition(
- "get_weather",
- "Gets the current weather for a given location.",
- ToolParameters.create(Map.of("location", "City and country, for example Berlin, Germany")),
- true)),
+ "You must call both provided tools in the same turn for this request. First call get_weather and get_traffic for Berlin, Germany. After both tool results are available, answer in one sentence that explicitly includes the exact facts from both tool outputs. Do not answer before both tool calls have been made.",
+ "What is the weather and how is the traffic in Berlin, Germany? Call both tools in the same turn.",
+ List.of(weatherTool, trafficTool),
new ToolCallingAgentResponseHandler(
new TextAgentResponseHandler("answer"),
- (completion, context, toolCallResults) -> {
+ (completion, agentContext, toolCallResults) -> {
if (completion.toolCalls().isEmpty()) {
return false;
}
- context.setStringValue("toolName", completion.toolCalls().getFirst().name());
- context.setStringValue("toolArguments", completion.toolCalls().getFirst().arguments());
- toolCallResults.add(completion.toolCalls().getFirst(), ToolParameters.create().add("forecast", "Berlin is rainy and 21C"));
- return true;
+ agentContext.setStringValues("toolNames", completion.toolCalls().stream().map(ToolCall::name).toList());
+ agentContext.setStringValues("toolArguments", completion.toolCalls().stream().map(toolCall -> toolCall.arguments() == null ? "" : toolCall.arguments()).toList());
+ var handledToolCall = false;
+ for (final var toolCall : completion.toolCalls()) {
+ if ("get_weather".equals(toolCall.name())) {
+ toolCallResults.add(toolCall, ToolParameters.create().add("forecast", "Berlin is rainy and 21C"));
+ handledToolCall = true;
+ }
+ else if ("get_traffic".equals(toolCall.name())) {
+ toolCallResults.add(toolCall, ToolParameters.create().add("traffic", "Berlin traffic is congested"));
+ handledToolCall = true;
+ }
+ }
+ return handledToolCall;
}),
- null,
+ context -> context.setValue("_toolCallData", ToolCallData.required(List.of(weatherTool, trafficTool)).withParallelToolCalls(true)),
null,
null,
AgentNext::end));
@@ -227,11 +245,13 @@ public class AgentTest {
final var resultContexts = this.agentExecutor.walk(agents.get("ToolCaller"), context);
final var resultContext = resultContexts.getFirst();
+ final var answer = resultContext.getStringValue("answer");
assertEquals(1, resultContexts.size());
- assertEquals("get_weather", resultContext.getStringValue("toolName"));
- assertTrue(resultContext.getStringValue("toolArguments").contains("Berlin"));
- assertTrue(Grader2.assessScoreBinary("Does the text say that Berlin is rainy and 21C?", resultContext.getStringValue("answer"), ModelCategory.MEDIUM, this.modelProfileService, this.clientService));
+ assertTrue(this.hasToolCall(resultContext, "get_weather", "Berlin"));
+ assertTrue(this.hasToolCall(resultContext, "get_traffic", "Berlin"));
+ assertTrue(Grader2.assessScoreBinary("Does the text say that Berlin is rainy and 21C?", answer, ModelCategory.MEDIUM, this.modelProfileService, this.clientService));
+ assertTrue(Grader2.assessScoreBinary("Does the text say that traffic in Berlin is congested?", answer, ModelCategory.MEDIUM, this.modelProfileService, this.clientService));
}
@Test
@@ -417,5 +437,19 @@ public class AgentTest {
LOGGER.info(number0 + " + " + number1 + " = " + expected + "; " + resultContext.getGraph().toString() + " " + resultContext.getRequestStatistic().toString());
}
}
+
+ private boolean hasToolCall(final AgentContext context, final String toolName, final String requiredArgumentPart) {
+ final var toolNames = context.getStringValues("toolNames");
+ final var toolArguments = context.getStringValues("toolArguments");
+ if (toolNames == null || toolArguments == null || toolNames.size() != toolArguments.size()) {
+ return false;
+ }
+ for (int i = 0; i < toolNames.size(); i++) {
+ if (toolName.equals(toolNames.get(i)) && toolArguments.get(i) != null && toolArguments.get(i).contains(requiredArgumentPart)) {
+ return true;
+ }
+ }
+ return false;
+ }
}