feat: enhance LLM call control with AgentSkipCompletion enum and update related methods

This commit is contained in:
extio-de
2026-06-02 22:17:51 +02:00
parent 14f43624db
commit 4453b7c2d1
4 changed files with 137 additions and 115 deletions

View File

@@ -1069,7 +1069,13 @@ If `context.setStreaming(true)` is enabled and the handler implements `StreamedA
### Skipping LLM Calls ### Skipping LLM Calls
Set `context.setSkipNextCompletion(true)` to turn the next step into a processing-only pass. The flag resets automatically after that step. Use `context.setSkipNextCompletion(...)` to control LLM call behavior for the next step. The flag resets automatically after that step.
| Value | Behavior |
|-------|----------|
| `AgentSkipCompletion.NO_SKIP` | Executes the LLM call normally (default) |
| `AgentSkipCompletion.SETUP_CONVERSATION_ONLY` | Sets up the conversation but skips the LLM call |
| `AgentSkipCompletion.SKIP` | Skips the entire turn, including conversation setup |
### Cache Bypass Controls ### Cache Bypass Controls

View File

@@ -27,7 +27,7 @@ public final class AgentContext {
private volatile Exception errorException; private volatile Exception errorException;
private volatile boolean skipNextCompletion; private volatile AgentSkipCompletion skipNextCompletion;
private volatile boolean skipCache; private volatile boolean skipCache;
@@ -235,11 +235,11 @@ public final class AgentContext {
this.errorException = null; this.errorException = null;
} }
public boolean isSkipNextCompletion() { public AgentSkipCompletion isSkipNextCompletion() {
return this.skipNextCompletion; return this.skipNextCompletion;
} }
public void setSkipNextCompletion(final boolean skipNextCompletion) { public void setSkipNextCompletion(final AgentSkipCompletion skipNextCompletion) {
this.skipNextCompletion = skipNextCompletion; this.skipNextCompletion = skipNextCompletion;
} }

View File

@@ -0,0 +1,13 @@
package de.extio.lmlib.agent;
public enum AgentSkipCompletion {
/** Execute the LLM call normally */
NO_SKIP,
/** Only set up the conversation without executing the LLM call */
SETUP_CONVERSATION_ONLY,
/** Skip the turn entirely */
SKIP
}

View File

@@ -134,7 +134,7 @@ public interface BaseAgent {
context.setError(AgentErrorType.GENERAL, ex); context.setError(AgentErrorType.GENERAL, ex);
return List.of(context); return List.of(context);
} }
final boolean skipCompletion = context.isSkipNextCompletion(); final AgentSkipCompletion skipCompletion = context.isSkipNextCompletion();
splits.addAll(this.applyTemplate(context)); splits.addAll(this.applyTemplate(context));
final var tasks = new ArrayList<CompletableFuture<?>>(splits.size()); final var tasks = new ArrayList<CompletableFuture<?>>(splits.size());
@@ -142,9 +142,10 @@ public interface BaseAgent {
tasks.add(CompletableFuture.runAsync(() -> { tasks.add(CompletableFuture.runAsync(() -> {
try { try {
split.context().clearError(); split.context().clearError();
final boolean skipCompletionInSplit = skipCompletion || this.agentType(split.context()) == AgentType.PROCESSING_ONLY; final boolean skipCompletionInSplitEntirely = skipCompletion == AgentSkipCompletion.SKIP || this.agentType(split.context()) == AgentType.PROCESSING_ONLY;
if (skipCompletionInSplit) { final boolean skipCompletionInSplitSetupConversation = skipCompletion == AgentSkipCompletion.SETUP_CONVERSATION_ONLY;
split.context().setSkipNextCompletion(false); if (skipCompletionInSplitEntirely || skipCompletionInSplitSetupConversation) {
split.context().setSkipNextCompletion(AgentSkipCompletion.NO_SKIP);
split.context().getGraph().add("○"); split.context().getGraph().add("○");
} }
else { else {
@@ -153,133 +154,135 @@ public interface BaseAgent {
} }
split.context().getGraph().add(this.name()); split.context().getGraph().add(this.name());
if (!skipCompletionInSplit) { if (!skipCompletionInSplitEntirely) {
final var completionClient = Objects.requireNonNull(client); final var completionClient = Objects.requireNonNull(client);
final boolean skipCache = split.context().isSkipCache() || split.context().isAlwaysSkipCache(); final boolean skipCache = split.context().isSkipCache() || split.context().isAlwaysSkipCache();
split.context().setSkipCache(false); split.context().setSkipCache(false);
final var conversation = this.setupConversation(split); final var conversation = this.setupConversation(split);
final var requestedModelProfile = this.modelProfile(split.context()); final var requestedModelProfile = this.modelProfile(split.context());
final var supportsToolCalling = requestedModelProfile != null ? completionClient.supportsToolCalling(requestedModelProfile) : completionClient.supportsToolCalling(this.modelCategory(split.context())); if (!skipCompletionInSplitSetupConversation) {
var toolCallData = supportsToolCalling ? this.toolCallData(split.context()) : null; final var supportsToolCalling = requestedModelProfile != null ? completionClient.supportsToolCalling(requestedModelProfile) : completionClient.supportsToolCalling(this.modelCategory(split.context()));
this.rememberToolCallData(split.context(), toolCallData); var toolCallData = supportsToolCalling ? this.toolCallData(split.context()) : null;
this.rememberToolCallData(split.context(), toolCallData);
boolean retryOuterLoop; boolean retryOuterLoop;
boolean parseable = false; boolean parseable = false;
Exception parseException = null; Exception parseException = null;
int parseAttempts = 0; int parseAttempts = 0;
int toolCallRounds = 0; int toolCallRounds = 0;
do { do {
retryOuterLoop = false; retryOuterLoop = false;
while (parseAttempts < 2) { while (parseAttempts < 2) {
LOGGER.debug("Conversation: {}", conversation); LOGGER.debug("Conversation: {}", conversation);
final var modelProfile = this.modelProfile(split.context()); final var modelProfile = this.modelProfile(split.context());
final var responseHandler = this.responseHandler(split.context()); final var responseHandler = this.responseHandler(split.context());
long requestStart = 0; long requestStart = 0;
Completion completion = null; Completion completion = null;
if (split.context().isStreaming()) { if (split.context().isStreaming()) {
if (responseHandler instanceof final StreamedAgentResponseHandler streamedResponseHandler) {
streamedResponseHandler.beforeStream(split.context());
}
final Consumer<Chunk> chunkConsumer = chunk -> {
if (responseHandler instanceof final StreamedAgentResponseHandler streamedResponseHandler) { if (responseHandler instanceof final StreamedAgentResponseHandler streamedResponseHandler) {
if (streamedResponseHandler.handleChunk(chunk, split.context()) && split.context().getAgentContextUpdateConsumer() != null) { streamedResponseHandler.beforeStream(split.context());
split.context().getAgentContextUpdateConsumer().accept(split.context()); }
} final Consumer<Chunk> chunkConsumer = chunk -> {
if (responseHandler instanceof final StreamedAgentResponseHandler streamedResponseHandler) {
if (streamedResponseHandler.handleChunk(chunk, split.context()) && split.context().getAgentContextUpdateConsumer() != null) {
split.context().getAgentContextUpdateConsumer().accept(split.context());
}
}
};
if (modelProfile != null) {
requestStart = System.currentTimeMillis();
completion = completionClient.streamConversation(modelProfile, conversation, chunkConsumer, toolCallData, skipCache);
}
else {
requestStart = System.currentTimeMillis();
completion = completionClient.streamConversation(this.modelCategory(split.context()), conversation, chunkConsumer, toolCallData, skipCache);
} }
};
if (modelProfile != null) {
requestStart = System.currentTimeMillis();
completion = completionClient.streamConversation(modelProfile, conversation, chunkConsumer, toolCallData, skipCache);
} }
else { else {
requestStart = System.currentTimeMillis(); if (modelProfile != null) {
completion = completionClient.streamConversation(this.modelCategory(split.context()), conversation, chunkConsumer, toolCallData, skipCache); requestStart = System.currentTimeMillis();
completion = completionClient.conversation(modelProfile, conversation, toolCallData, skipCache);
}
else {
requestStart = System.currentTimeMillis();
completion = completionClient.conversation(this.modelCategory(split.context()), conversation, toolCallData, skipCache);
}
} }
} final var durationMs = System.currentTimeMillis() - requestStart;
else { split.context().getGraph().add("(" + durationMs + " ms)");
if (modelProfile != null) { split.context().setLastCompletion(completion);
requestStart = System.currentTimeMillis(); split.context().getRequestStatistic().add(completion);
completion = completionClient.conversation(modelProfile, conversation, toolCallData, skipCache); if (completion.finishReason() != null) {
switch (completion.finishReason()) {
case TOKEN_LIMIT_REACHED:
split.context().getGraph().add("⏹");
break;
case CONTENT_FILTERED:
split.context().getGraph().add("⛔");
break;
case TOOL_CALLS:
split.context().getGraph().add("🔧");
break;
case ERROR:
split.context().getGraph().add("❌");
continue;
default:
break;
}
} }
else {
requestStart = System.currentTimeMillis();
completion = completionClient.conversation(this.modelCategory(split.context()), conversation, toolCallData, skipCache);
}
}
final var durationMs = System.currentTimeMillis() - requestStart;
split.context().getGraph().add("(" + durationMs + " ms)");
split.context().setLastCompletion(completion);
split.context().getRequestStatistic().add(completion);
if (completion.finishReason() != null) {
switch (completion.finishReason()) {
case TOKEN_LIMIT_REACHED:
split.context().getGraph().add("⏹");
break;
case CONTENT_FILTERED:
split.context().getGraph().add("⛔");
break;
case TOOL_CALLS:
split.context().getGraph().add("🔧");
break;
case ERROR:
split.context().getGraph().add("❌");
continue;
default:
break;
}
}
if (!completion.toolCalls().isEmpty() && responseHandler instanceof final ToolCallingAgentResponseHandler toolCallingAgentResponseHandler) { if (!completion.toolCalls().isEmpty() && responseHandler instanceof final ToolCallingAgentResponseHandler toolCallingAgentResponseHandler) {
if (toolCallRounds >= MAX_TOOL_CALL_ROUNDS) { if (toolCallRounds >= MAX_TOOL_CALL_ROUNDS) {
parseException = new IllegalStateException("Too many tool call rounds"); parseException = new IllegalStateException("Too many tool call rounds");
break;
}
final var toolCallResults = new ToolCallingAgentResponseHandler.ToolCallResults();
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;
}
}
try {
if (!responseHandler.handle(completion, split.context())) {
split.context().getGraph().add("⚠");
LOGGER.warn("{} Cannot parse response: {}", this.name(), completion.response());
split.context().setSkipCache(true);
parseAttempts++;
continue;
}
parseable = true;
break; break;
} }
final var toolCallResults = new ToolCallingAgentResponseHandler.ToolCallResults(); catch (final Exception e) {
if (toolCallingAgentResponseHandler.handleToolCalls(completion, split.context(), toolCallResults)) { parseException = e;
toolCallRounds++;
this.appendToolCallResults(conversation, completion, toolCallResults.results());
if (toolCallData != null && toolCallData.hasTools()) {
toolCallData = ToolCallData.auto(toolCallData.tools());
}
continue;
}
}
try {
if (!responseHandler.handle(completion, split.context())) {
split.context().getGraph().add("⚠"); split.context().getGraph().add("⚠");
LOGGER.warn("{} Cannot parse response: {}", this.name(), completion.response()); LOGGER.warn("{} Cannot parse response: {}", this.name(), completion.response(), e);
split.context().setSkipCache(true); split.context().setSkipCache(true);
parseAttempts++; parseAttempts++;
continue; continue;
} }
parseable = true;
break;
} }
catch (final Exception e) { if (!parseable) {
parseException = e; if (this.retryOnParsingError(split.context())) {
split.context().getGraph().add("⚠"); parseAttempts = 0;
LOGGER.warn("{} Cannot parse response: {}", this.name(), completion.response(), e); toolCallRounds = 0;
split.context().setSkipCache(true); retryOuterLoop = true;
parseAttempts++; continue;
continue; }
LOGGER.warn("{} Response is still not parseable after last attempt", this.name());
split.context().getGraph().add("☢");
split.context().setError(AgentErrorType.PARSING, parseException);
return;
} }
} } while (retryOuterLoop);
if (!parseable) { }
if (this.retryOnParsingError(split.context())) {
parseAttempts = 0;
toolCallRounds = 0;
retryOuterLoop = true;
continue;
}
LOGGER.warn("{} Response is still not parseable after last attempt", this.name());
split.context().getGraph().add("☢");
split.context().setError(AgentErrorType.PARSING, parseException);
return;
}
} while (retryOuterLoop);
} }
this.postProcess(split.context()); this.postProcess(split.context());
@@ -465,8 +468,8 @@ public interface BaseAgent {
return conversation; return conversation;
} }
private void appendLastCompletionToConversation(final AgentContext context, final boolean skipCompletion) { private void appendLastCompletionToConversation(final AgentContext context, final AgentSkipCompletion skipCompletion) {
if (skipCompletion || context.getConversation() == null || context.getLastCompletion() == null) { if (context.getConversation() == null || context.getLastCompletion() == null || (skipCompletion != null && skipCompletion != AgentSkipCompletion.NO_SKIP)) {
return; return;
} }
final var currentAgent = context.getAgents().get(context.getCurrentAgentName()); final var currentAgent = context.getAgents().get(context.getCurrentAgentName());
@@ -496,7 +499,7 @@ public interface BaseAgent {
context.setValue("_toolCallData", null); context.setValue("_toolCallData", null);
} }
private void nextAgent(final AgentContext context, final boolean skipCompletion) { private void nextAgent(final AgentContext context, final AgentSkipCompletion skipCompletion) {
context.setNextAgent(this.chooseNextAgent(context)); context.setNextAgent(this.chooseNextAgent(context));
if (context.getNextAgent() != null) { if (context.getNextAgent() != null) {