feat: add retryOnParsingError method to BaseAgent for enhanced error handling

This commit is contained in:
extio-de
2026-06-01 22:28:30 +02:00
parent 2083d4f33f
commit e1a70f5397
2 changed files with 100 additions and 85 deletions

View File

@@ -870,6 +870,7 @@ final var blogPostAgent = new Agent(
| `postProcess(context)` | Runs after completion. | no-op | | `postProcess(context)` | Runs after completion. | no-op |
| `merge(contexts)` | Merges split contexts after branching. | `null` | | `merge(contexts)` | Merges split contexts after branching. | `null` |
| `chooseNextAgent(context)` | Selects the next agent or ends the flow. Required. | none | | `chooseNextAgent(context)` | Selects the next agent or ends the flow. Required. | none |
| `retryOnParsingError(context)` | When `true`, triggers an outer retry loop that resets both parse attempts and tool call rounds after a parsing failure, allowing the agent to request a fresh completion from scratch. The conversation can be modified before retrying. | `false` |
### Agent Tool Calling ### Agent Tool Calling

View File

@@ -111,6 +111,10 @@ public interface BaseAgent {
default AgentNext chooseNextAgent(final AgentContext context) { default AgentNext chooseNextAgent(final AgentContext context) {
return AgentNext.END; return AgentNext.END;
} }
default boolean retryOnParsingError(final AgentContext context) {
return false;
}
default List<AgentContext> execute(final Client client, final ExecutorService agentExecutorService, final AgentContext context) { default List<AgentContext> execute(final Client client, final ExecutorService agentExecutorService, final AgentContext context) {
final var splits = new ArrayList<Split>(); final var splits = new ArrayList<Split>();
@@ -159,113 +163,123 @@ public interface BaseAgent {
var toolCallData = supportsToolCalling ? this.toolCallData(split.context()) : null; var toolCallData = supportsToolCalling ? this.toolCallData(split.context()) : null;
this.rememberToolCallData(split.context(), toolCallData); this.rememberToolCallData(split.context(), toolCallData);
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;
while (parseAttempts < 2) { do {
LOGGER.debug("Conversation: {}", conversation); retryOuterLoop = false;
while (parseAttempts < 2) {
final var modelProfile = this.modelProfile(split.context()); LOGGER.debug("Conversation: {}", conversation);
final var responseHandler = this.responseHandler(split.context());
long requestStart = 0; final var modelProfile = this.modelProfile(split.context());
Completion completion = null; final var responseHandler = this.responseHandler(split.context());
if (split.context().isStreaming()) { long requestStart = 0;
if (responseHandler instanceof final StreamedAgentResponseHandler streamedResponseHandler) { Completion completion = null;
streamedResponseHandler.beforeStream(split.context()); if (split.context().isStreaming()) {
}
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(); if (!completion.toolCalls().isEmpty() && responseHandler instanceof final ToolCallingAgentResponseHandler toolCallingAgentResponseHandler) {
completion = completionClient.conversation(this.modelCategory(split.context()), conversation, toolCallData, skipCache); if (toolCallRounds >= MAX_TOOL_CALL_ROUNDS) {
} parseException = new IllegalStateException("Too many tool call rounds");
}
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; break;
case CONTENT_FILTERED: }
split.context().getGraph().add("⛔"); final var toolCallResults = new ToolCallingAgentResponseHandler.ToolCallResults();
break; if (toolCallingAgentResponseHandler.handleToolCalls(completion, split.context(), toolCallResults)) {
case TOOL_CALLS: toolCallRounds++;
split.context().getGraph().add("🔧"); this.appendToolCallResults(conversation, completion, toolCallResults.results());
break; if (toolCallData != null && toolCallData.hasTools()) {
case ERROR: toolCallData = ToolCallData.auto(toolCallData.tools());
split.context().getGraph().add("❌"); }
continue; continue;
default: }
break;
} }
}
try {
if (!completion.toolCalls().isEmpty() && responseHandler instanceof final ToolCallingAgentResponseHandler toolCallingAgentResponseHandler) { if (!responseHandler.handle(completion, split.context())) {
if (toolCallRounds >= MAX_TOOL_CALL_ROUNDS) { split.context().getGraph().add("⚠");
parseException = new IllegalStateException("Too many tool call rounds"); 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) {
LOGGER.warn("{} Response is still not parseable after last attempt", this.name());
split.context().getGraph().add("☢");
split.context().setError(AgentErrorType.PARSING, parseException);
return;
}
} }
this.postProcess(split.context()); this.postProcess(split.context());