feat(exceptions): add typed exception hierarchy#1
Conversation
- AgentException (base RuntimeException for full pipeline) - ScanException (moved from inner class in SecuritySupervisorAgentService) - McpToolNullResponseException (MCP returned null/empty — stops LLM loops) - McpToolExecutionException (MCP tool threw during execution) - MaxLlmCallsExceededException (loop guard circuit-breaker) - AgentRoutingException (supervisor cannot determine valid route) Fixes: ScanException was an inner class — non-standard, untestable. All exceptions extend AgentException for single-catch supervisor handling.
There was a problem hiding this comment.
Pull request overview
This PR introduces a typed exception hierarchy and supporting utilities to make the agent pipeline more robust (loop guards, MCP null-response circuit breakers), while also extracting a few service/tool contracts to enable easier unit testing and starting to scaffold multi-turn conversation context.
Changes:
- Added
AgentExceptionhierarchy (e.g.,ScanException,McpToolNullResponseException,MaxLlmCallsExceededException) and integrated loop guards/null-response validation into agents. - Added shared utilities (
AgentUtils,McpResponseValidator) and a compact supervisor routing prompt to reduce token usage on subsequent routing calls. - Extracted interfaces for services/tool registries and added
ConversationContextplumbing (history loaded from Redis and passed through state).
Reviewed changes
Copilot reviewed 26 out of 26 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| 02-Cybersecurity/Vulnerability-Detection-Agent-Java/vulnerability-intel-mcp-server/src/main/java/com/agent/service/VulnerabilityService.java | Implements new service contract interface. |
| 02-Cybersecurity/Vulnerability-Detection-Agent-Java/vulnerability-intel-mcp-server/src/main/java/com/agent/service/IVulnerabilityService.java | New interface to support mocking/testing of vulnerability analysis service. |
| 02-Cybersecurity/Vulnerability-Detection-Agent-Java/security-supervisor-agent/src/main/java/com/agent/util/McpResponseValidator.java | New validator to fail fast on null/empty MCP tool responses. |
| 02-Cybersecurity/Vulnerability-Detection-Agent-Java/security-supervisor-agent/src/main/java/com/agent/util/AgentUtils.java | New shared utilities for MCP response extraction, markdown stripping, and empty checks. |
| 02-Cybersecurity/Vulnerability-Detection-Agent-Java/security-supervisor-agent/src/main/java/com/agent/tool/ToolRegistry.java | New tool registry interface intended to enable mocking. |
| 02-Cybersecurity/Vulnerability-Detection-Agent-Java/security-supervisor-agent/src/main/java/com/agent/tool/McpToolRegistry.java | Registry now implements ToolRegistry interface. |
| 02-Cybersecurity/Vulnerability-Detection-Agent-Java/security-supervisor-agent/src/main/java/com/agent/service/SecuritySupervisorAgentService.java | Wires ConversationContext into initial state and externalizes ScanException. |
| 02-Cybersecurity/Vulnerability-Detection-Agent-Java/security-supervisor-agent/src/main/java/com/agent/prompt/PromptLibrary.java | Adds compact supervisor routing prompt; refactors prompt docs. |
| 02-Cybersecurity/Vulnerability-Detection-Agent-Java/security-supervisor-agent/src/main/java/com/agent/model/ConversationContext.java | New model to carry session history and build multi-message LLM inputs. |
| 02-Cybersecurity/Vulnerability-Detection-Agent-Java/security-supervisor-agent/src/main/java/com/agent/exception/ScanException.java | New top-level exception replacing former inner class. |
| 02-Cybersecurity/Vulnerability-Detection-Agent-Java/security-supervisor-agent/src/main/java/com/agent/exception/McpToolNullResponseException.java | New typed exception for MCP null/empty tool outputs. |
| 02-Cybersecurity/Vulnerability-Detection-Agent-Java/security-supervisor-agent/src/main/java/com/agent/exception/McpToolExecutionException.java | New typed exception for MCP tool execution failures. |
| 02-Cybersecurity/Vulnerability-Detection-Agent-Java/security-supervisor-agent/src/main/java/com/agent/exception/MaxLlmCallsExceededException.java | New typed exception for LLM loop guard circuit-breaker. |
| 02-Cybersecurity/Vulnerability-Detection-Agent-Java/security-supervisor-agent/src/main/java/com/agent/exception/AgentRoutingException.java | New typed exception for invalid supervisor routing outcomes. |
| 02-Cybersecurity/Vulnerability-Detection-Agent-Java/security-supervisor-agent/src/main/java/com/agent/exception/AgentException.java | New base exception for agent pipeline failures. |
| 02-Cybersecurity/Vulnerability-Detection-Agent-Java/security-supervisor-agent/src/main/java/com/agent/controller/ScanController.java | Updates import to new ScanException location. |
| 02-Cybersecurity/Vulnerability-Detection-Agent-Java/security-supervisor-agent/src/main/java/com/agent/agent/sub_agents/VulnerabilityAgent.java | Adds loop guard, early exits, shared utils, and null-response validation. |
| 02-Cybersecurity/Vulnerability-Detection-Agent-Java/security-supervisor-agent/src/main/java/com/agent/agent/sub_agents/GithubRepoAgent.java | Adds loop guard, shared utils, and MCP null-response validation. |
| 02-Cybersecurity/Vulnerability-Detection-Agent-Java/security-supervisor-agent/src/main/java/com/agent/agent/sub_agents/DirectAnswerAgent.java | Uses ConversationContext history to build multi-message prompts when available. |
| 02-Cybersecurity/Vulnerability-Detection-Agent-Java/security-supervisor-agent/src/main/java/com/agent/agent/sub_agents/DependencyParserAgent.java | Adds loop guard and shared utils; treats empty parser output as valid. |
| 02-Cybersecurity/Vulnerability-Detection-Agent-Java/security-supervisor-agent/src/main/java/com/agent/agent/SupervisorAgent.java | Adds supervisor iteration guard, compact routing prompt, and typed exception handling. |
| 02-Cybersecurity/Vulnerability-Detection-Agent-Java/github-repo-mcp-server/src/main/java/com/agent/service/impl/GithubRepoServiceImpl.java | Adds an additional repo service implementation (currently duplicates existing logic). |
| 02-Cybersecurity/Vulnerability-Detection-Agent-Java/github-repo-mcp-server/src/main/java/com/agent/service/IGithubRepoService.java | Adds repo service interface (currently has naming/compilation issues). |
| 02-Cybersecurity/Vulnerability-Detection-Agent-Java/github-repo-mcp-server/src/main/java/com/agent/service/GithubRepoService.java | Updates existing repo service to implement the new interface. |
| 02-Cybersecurity/Vulnerability-Detection-Agent-Java/dependency-parser-mcp-server/src/main/java/com/agent/service/IDependencyParserService.java | Adds dependency parser service interface. |
| 02-Cybersecurity/Vulnerability-Detection-Agent-Java/dependency-parser-mcp-server/src/main/java/com/agent/service/DependencyParserService.java | Updates implementation to implement new interface. |
Comments suppressed due to low confidence (1)
02-Cybersecurity/Vulnerability-Detection-Agent-Java/security-supervisor-agent/src/main/java/com/agent/agent/sub_agents/VulnerabilityAgent.java:103
tool()throwsMcpToolExecutionException, butinvoke()catches it in the genericcatch (Exception e)and converts it into afinalAnswer, so the typed exception won’t reachSupervisorAgent(despite being part of the new exception hierarchy). Either rethrowMcpToolExecutionExceptionhere or don’t throw it fromtool()and keep the existing string-based error propagation.
} catch (McpToolNullResponseException | MaxLlmCallsExceededException e) {
// Re-throw typed exceptions to SupervisorAgent for proper handling
throw e;
} catch (Exception e) {
log.error("[VulnerabilityAgent] Unexpected error", e);
Map<String, Object> err = new HashMap<>(incomingState);
err.put("finalAnswer", "VulnerabilityAgent error: " + e.getMessage());
return err;
}
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| private ConversationContext buildConversationContext(String sessionId, String currentQuery) { | ||
| try { | ||
| java.util.List<String> rawHistory = sessionStore.getHistory(sessionId); |
There was a problem hiding this comment.
currentQuery is not used in buildConversationContext(...), which is misleading and will be flagged by IDEs/static analysis. Either remove the parameter or use it (e.g., to exclude the current turn from history, or to append it explicitly).
| /** | ||
| * A single chat turn: user query + agent response summary. | ||
| * Stored in Redis as JSON via SessionStore.saveChatEntry(). | ||
| */ | ||
| @Data | ||
| @AllArgsConstructor | ||
| public static class ChatMessage { | ||
|
|
||
| /** Original user query for this turn */ | ||
| private String userQuery; | ||
|
|
||
| /** | ||
| * Compact summary of the agent's response for this turn. | ||
| * Used as the assistant message in history — not the full vulnerability report, | ||
| * just enough context for the LLM to understand what was done. | ||
| */ | ||
| private String agentSummary; | ||
|
|
There was a problem hiding this comment.
This model (and its Javadoc) assumes Redis history entries contain an agentSummary field, but SessionStore.saveChatEntry() currently writes agentOutput. Unless those keys are aligned, agentSummary will always be null and assistant turns won’t be included when building message history. Consider renaming this field to agentOutput (or updating SessionStore/buildConversationContext to write/read agentSummary).
| } catch (McpToolNullResponseException | MaxLlmCallsExceededException e) { | ||
| throw e; | ||
|
|
||
| } catch (Exception e) { | ||
| log.error("[GithubRepoAgent] Error", e); | ||
| log.error("[GithubRepoAgent] Unexpected error", e); | ||
| Map<String, Object> err = new HashMap<>(incomingState); | ||
| err.put("finalAnswer", "GithubRepoAgent error: " + e.getMessage()); | ||
| return err; |
There was a problem hiding this comment.
tool() throws McpToolExecutionException, but invoke() catches it in the generic catch (Exception e) and converts it into a finalAnswer, so the typed exception will never propagate to SupervisorAgent (and the toolName context is effectively lost). Either rethrow McpToolExecutionException here (alongside the other typed exceptions) or avoid wrapping/throwing it in tool() and keep the current finalAnswer path.
| @Slf4j | ||
| @Service | ||
| public class GithubRepoServiceImpl implements IGithubRepoService { | ||
|
|
There was a problem hiding this comment.
This introduces a second @Service implementation of the repo service with effectively identical logic to com.agent.service.GithubRepoService. Once the IGithubRepoService interface is corrected/used for injection, Spring autowiring by that interface will become ambiguous (two beans). Consider keeping a single implementation (either move the existing class into impl and delete/convert the other, or remove this duplicate).
| /** | ||
| * Build ConversationContext from Redis session history. | ||
| * Each history entry is a JSON string of {userQuery, agentSummary, timestamp}. | ||
| * If history is empty or an error occurs, returns an empty context. | ||
| */ | ||
| private ConversationContext buildConversationContext(String sessionId, String currentQuery) { | ||
| try { | ||
| java.util.List<String> rawHistory = sessionStore.getHistory(sessionId); | ||
| java.util.List<ConversationContext.ChatMessage> messages = new java.util.ArrayList<>(); | ||
|
|
||
| for (String entry : rawHistory) { | ||
| try { | ||
| com.fasterxml.jackson.databind.JsonNode node = objectMapper.readTree(entry); | ||
| String query = node.path("userQuery").asText(null); | ||
| String summary = node.path("agentSummary").asText(null); | ||
| String ts = node.path("timestamp").asText(Instant.now().toString()); | ||
| if (query != null) { | ||
| messages.add(new ConversationContext.ChatMessage(query, summary, ts)); | ||
| } |
There was a problem hiding this comment.
buildConversationContext() parses history entries using agentSummary, but SessionStore.saveChatEntry() persists the field as agentOutput. As a result, assistant history will always be null and ConversationContext.buildMessages() will only replay user turns. Align the JSON field names (e.g., read agentOutput here or write agentSummary in SessionStore) and update the method/Javadoc accordingly.
There was a problem hiding this comment.
@copilot open a new pull request to apply changes based on this feedback
| ConversationContext ctx = (ConversationContext) state.data().get("conversationContext"); | ||
|
|
||
| String prompt = PromptLibrary.DIRECT_ANSWER_AGENT_PROMPT.formatted(userQuery); | ||
| List<Message> messages = (ctx != null && ctx.hasHistory()) | ||
| ? ctx.buildMessages(prompt) | ||
| : List.of(new UserMessage(prompt)); |
There was a problem hiding this comment.
This unchecked cast can throw ClassCastException if the state contains a different type (or a serialized Map) under conversationContext. Prefer an instanceof ConversationContext guard and treat non-matching values as “no history”.
| } catch (MaxLlmCallsExceededException e) { | ||
| log.error("[Supervisor] LLM loop guard triggered — agent={} limit={}", e.getAgentName(), e.getLimit()); | ||
| Map<String, Object> err = new HashMap<>(initialState); | ||
| err.put("finalAnswer", "Analysis stopped: agent '" + e.getAgentName() + "' exceeded " + e.getLimit() + " LLM calls."); | ||
| return err; | ||
|
|
||
| } catch (McpToolNullResponseException e) { | ||
| log.warn("[Supervisor] MCP null response — tool={}", e.getToolName()); | ||
| Map<String, Object> err = new HashMap<>(initialState); | ||
| err.put("finalAnswer", "No data returned from tool '" + e.getToolName() + "'. The MCP server may be unavailable."); | ||
| return err; | ||
|
|
||
| } catch (AgentRoutingException e) { | ||
| log.error("[Supervisor] Routing failed — received='{}'", e.getReceivedRoute()); | ||
| Map<String, Object> err = new HashMap<>(initialState); | ||
| err.put("finalAnswer", "Pipeline routing error: " + e.getMessage()); | ||
| return err; | ||
|
|
There was a problem hiding this comment.
The new AgentException hierarchy includes McpToolExecutionException, but SupervisorAgent.invoke() doesn’t handle it (or AgentException) explicitly, so execution failures won’t get the intended typed handling/message. Consider adding a catch (AgentException e) (or at least McpToolExecutionException) to produce a consistent user-facing error and keep the “single-catch” goal described in the PR.
| * in unit tests. Each agent injects ToolRegistry rather than McpToolRegistry | ||
| * directly, so tests can substitute a stub without bringing up MCP servers. |
There was a problem hiding this comment.
This Javadoc claims “Each agent injects ToolRegistry rather than McpToolRegistry”, but the current agents still inject McpToolRegistry (see GithubRepoAgent/DependencyParserAgent/VulnerabilityAgent constructors). Update the comment or change the injection types so the documentation matches reality.
| * in unit tests. Each agent injects ToolRegistry rather than McpToolRegistry | |
| * directly, so tests can substitute a stub without bringing up MCP servers. | |
| * in unit tests. Production agents may still inject McpToolRegistry | |
| * directly, but tests can inject ToolRegistry implementations or stubs | |
| * without bringing up MCP servers. |
| * IDependencyParserService — contract for dependency file parsers. | ||
| * | ||
| * Extracted from the concrete class to enable mock injection in unit tests. | ||
| * Implementation: {@link impl.DependencyParserServiceImpl} |
There was a problem hiding this comment.
The Javadoc references impl.DependencyParserServiceImpl, but there is no such class in this module (the implementation is com.agent.service.DependencyParserService). Update the link to the correct implementation so IDE navigation and generated docs aren’t broken.
| * Implementation: {@link impl.DependencyParserServiceImpl} | |
| * Implementation: {@link DependencyParserService} |
…lization; implement Serializable interface and improve dependency parsing logic
|
@parthloglogn I've opened a new pull request, #2, to work on those changes. Once the pull request is ready, I'll request review from you. |
Fixes: ScanException was an inner class — non-standard, untestable. All exceptions extend AgentException for single-catch supervisor handling.