feat: Enhance error handling with AgentErrorType and Exception tracking

This commit is contained in:
extio-de
2026-03-14 14:41:26 +01:00
parent b47380ec89
commit 0da1b3c78b
5 changed files with 133 additions and 10 deletions

View File

@@ -22,7 +22,9 @@ public final class AgentContext {
private volatile boolean streaming; private volatile boolean streaming;
private volatile boolean error; private volatile AgentErrorType errorType;
private volatile Exception errorException;
private volatile boolean skipNextCompletion; private volatile boolean skipNextCompletion;
@@ -74,7 +76,8 @@ public final class AgentContext {
this.requestStatistic = other.requestStatistic; this.requestStatistic = other.requestStatistic;
this.lastCompletion = other.lastCompletion; this.lastCompletion = other.lastCompletion;
this.agentContextUpdateConsumer = other.agentContextUpdateConsumer; this.agentContextUpdateConsumer = other.agentContextUpdateConsumer;
this.error = other.error; this.errorType = other.errorType;
this.errorException = other.errorException;
this.skipNextCompletion = other.skipNextCompletion; this.skipNextCompletion = other.skipNextCompletion;
this.skipCache = other.skipCache; this.skipCache = other.skipCache;
this.streaming = other.streaming; this.streaming = other.streaming;
@@ -190,11 +193,29 @@ public final class AgentContext {
} }
public boolean isError() { public boolean isError() {
return this.error; return this.errorType != null;
} }
public void setError(final boolean error) { public AgentErrorType getErrorType() {
this.error = error; return this.errorType;
}
public Exception getErrorException() {
return this.errorException;
}
public void setError(final AgentErrorType errorType, final Exception errorException) {
if (errorType == null) {
this.clearError();
return;
}
this.errorType = errorType;
this.errorException = errorException;
}
public void clearError() {
this.errorType = null;
this.errorException = null;
} }
public boolean isSkipNextCompletion() { public boolean isSkipNextCompletion() {

View File

@@ -0,0 +1,6 @@
package de.extio.lmlib.agent;
public enum AgentErrorType {
PARSING,
GENERAL
}

View File

@@ -111,6 +111,8 @@ public class AgentExecutorService implements InitializingBean, DisposableBean {
} }
catch (final Exception e) { catch (final Exception e) {
LOGGER.error("Error executing agent", e); LOGGER.error("Error executing agent", e);
branchContext.setError(AgentErrorType.GENERAL, e);
responses.add(branchContext);
} }
}, this.branchExecutorService)); }, this.branchExecutorService));
} }

View File

@@ -80,17 +80,26 @@ public interface BaseAgent {
final var splits = new ArrayList<Split>(); final var splits = new ArrayList<Split>();
context.setNextAgent(null); context.setNextAgent(null);
context.clearError();
try {
this.preProcess(context); this.preProcess(context);
if (context.getAgentContextUpdateConsumer() != null) { if (context.getAgentContextUpdateConsumer() != null) {
context.getAgentContextUpdateConsumer().accept(context); context.getAgentContextUpdateConsumer().accept(context);
} }
}
catch (final Exception ex) {
LOGGER.error("Error during agent execution", ex);
context.setError(AgentErrorType.GENERAL, ex);
return List.of(context);
}
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());
for (final var split : splits) { for (final var split : splits) {
tasks.add(CompletableFuture.runAsync(() -> { tasks.add(CompletableFuture.runAsync(() -> {
try { try {
split.context().clearError();
final boolean skipCompletion = split.context().isSkipNextCompletion() || this.agentType(split.context()) == AgentType.PROCESSING_ONLY; final boolean skipCompletion = split.context().isSkipNextCompletion() || this.agentType(split.context()) == AgentType.PROCESSING_ONLY;
if (skipCompletion) { if (skipCompletion) {
split.context().setSkipNextCompletion(false); split.context().setSkipNextCompletion(false);
@@ -108,6 +117,7 @@ public interface BaseAgent {
final var conversation = this.setupConversation(split); final var conversation = this.setupConversation(split);
boolean parseable = false; boolean parseable = false;
Exception parseException = null;
for (int i = 0; i < 2; i++) { for (int i = 0; i < 2; i++) {
LOGGER.debug("Conversation: {}", conversation); LOGGER.debug("Conversation: {}", conversation);
@@ -162,6 +172,7 @@ public interface BaseAgent {
break; break;
} }
catch (final Exception e) { catch (final Exception e) {
parseException = e;
split.context().getGraph().add("⚠"); split.context().getGraph().add("⚠");
LOGGER.warn("{} Cannot parse response: {}", this.name(), completion.response(), e); LOGGER.warn("{} Cannot parse response: {}", this.name(), completion.response(), e);
split.context().setSkipCache(true); split.context().setSkipCache(true);
@@ -171,7 +182,7 @@ public interface BaseAgent {
if (!parseable) { if (!parseable) {
LOGGER.warn("{} Response is still not parseable after last attempt", this.name()); LOGGER.warn("{} Response is still not parseable after last attempt", this.name());
split.context().getGraph().add("☢"); split.context().getGraph().add("☢");
split.context().setError(true); split.context().setError(AgentErrorType.PARSING, parseException);
return; return;
} }
} }
@@ -183,7 +194,7 @@ public interface BaseAgent {
} }
catch (final Exception ex) { catch (final Exception ex) {
LOGGER.error("Error during agent execution", ex); LOGGER.error("Error during agent execution", ex);
split.context().setError(true); split.context().setError(AgentErrorType.GENERAL, ex);
} }
}, agentExecutorService)); }, agentExecutorService));
} }

View File

@@ -0,0 +1,83 @@
package de.extio.lmlib.agent;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.util.Map;
import java.math.BigDecimal;
import java.time.Duration;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.junit.jupiter.api.Test;
import de.extio.lmlib.agent.responsehandler.AgentResponseHandler;
import de.extio.lmlib.client.Client;
import de.extio.lmlib.client.Completion;
import de.extio.lmlib.client.CompletionStatistics;
import de.extio.lmlib.profile.ModelCategory;
class AgentErrorHandlingTest {
@Test
void parsingErrorsKeepTheirTypeWithoutAnExceptionWhenTheHandlerReturnsFalse() {
final Client client = mock(Client.class);
when(client.conversation(any(ModelCategory.class), any(), anyBoolean())).thenReturn(new Completion("not json", null, null, new CompletionStatistics(1, Duration.ofMillis(1), 1, 0, 1, 0, BigDecimal.ZERO, false)));
final AgentResponseHandler responseHandler = (completion, context) -> false;
final var agent = new Agent("ParseAgent",
AgentType.COMPLETION,
ModelCategory.MEDIUM,
null,
null,
"prompt",
responseHandler,
null,
null,
null,
AgentNext::end);
final var context = new AgentContext(Map.of(agent.name(), agent));
try (ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor()) {
final var results = agent.execute(client, executorService, context);
final var result = results.getFirst();
assertTrue(result.isError());
assertEquals(AgentErrorType.PARSING, result.getErrorType());
assertNull(result.getErrorException());
}
}
@Test
void thrownExceptionsAreStoredAsGeneralErrors() {
final var exception = new IllegalStateException("boom");
final var agent = new Agent("BrokenAgent",
AgentType.PROCESSING_ONLY,
ModelCategory.MEDIUM,
null,
null,
null,
null,
context -> {
throw exception;
},
null,
null,
AgentNext::end);
final var context = new AgentContext(Map.of(agent.name(), agent));
try (ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor()) {
final var results = agent.execute(null, executorService, context);
final var result = results.getFirst();
assertTrue(result.isError());
assertEquals(AgentErrorType.GENERAL, result.getErrorType());
assertInstanceOf(IllegalStateException.class, result.getErrorException());
assertEquals("boom", result.getErrorException().getMessage());
}
}
}