Exercise 3 — Parallel Agents: @ParallelMapperAgent¶
Code-Along
Timebox: 10 minutes
Persona: Chris — Ops lead
You work in: lab/ (keep Quarkus running)
Files to edit:
lab/src/main/java/com/incidentmanagement/agentic/agents/IncidentAnalysisAgent.javalab/src/main/java/com/incidentmanagement/agentic/workflow/IncidentAnalysisWorkflow.java
Solution fallback
solutions/03-parallel-workflow — open if stuck.
Enterprise context¶
Chris, Apex Systems' Ops lead, needs a complete initial assessment while an incident is still unfolding. On an incident bridge, three questions arrive together: how severe is the failure, who is affected, and what response is likely to help? Waiting for each analysis to finish before starting the next delays the information that downstream responders need to choose a course of action.
The three analyses can read the same incident independently, so Chris needs them to run concurrently and return one coherent result. This lab demonstrates that arrangement with separate severity, impact, and resolution tasks. The resulting typed record gives the next workflow stage all three perspectives together, while the timing and logs let Chris check that the analyses overlap rather than merely appearing as separate entries in a sequential pipeline.
The goal¶
Run three parallel agents — severity, impact, and resolution analysis — concurrently instead of sequentially. Wall-clock time ≈ slowest single call (not the sum). One interface declaration, three concurrent LLM calls, each with a different @SystemMessage injected at runtime.
This introduces two concepts: dynamic @SystemMessage (same agent interface, different prompt per task) and @Output (aggregating parallel results into a typed record before downstream agents read scope).
How AgenticScope connects agents¶
%%{init: {'look':'handDrawn','theme':'base','themeVariables': {'darkMode':false,'background':'#FFFCF5','primaryTextColor':'#1F1F1F','secondaryTextColor':'#1F1F1F','tertiaryTextColor':'#1F1F1F','textColor':'#1F1F1F','lineColor':'#4A4035','edgeLabelBackground':'#FFFCF5','clusterBkg':'#F5F5F0','clusterBorder':'#6B5B45','fontSize':'16px'},'flowchart': {'nodeSpacing':35,'rankSpacing':55,'padding':18}}}%%
flowchart TD
A([IncidentAnalysisAgent x3])
B([Output → List of String])
C([IncidentAnalysisResults])
D([IncidentSupervisorAgent])
A --> B --> C --> D
subgraph parallel["Concurrent — outputKey = incidentAnalysis"]
A1([Severity analyst])
A2([Impact analyst])
A3([Resolution analyst])
end
A1 --> B
A2 --> B
A3 --> B
style A fill:#D4E6F1,stroke:#2E6B8A,color:#1F1F1F,stroke-width:2px
style B fill:#E8E0F0,stroke:#6B5B8A,color:#1F1F1F,stroke-width:2px
style C fill:#E8E0F0,stroke:#6B5B8A,color:#1F1F1F,stroke-width:2px
style D fill:#E8DCC4,stroke:#6B5B45,color:#1F1F1F,stroke-width:2px
style A1 fill:#D8F0D8,stroke:#3D7A3D,color:#1F1F1F,stroke-width:2px
style A2 fill:#D8F0D8,stroke:#3D7A3D,color:#1F1F1F,stroke-width:2px
style A3 fill:#D8F0D8,stroke:#3D7A3D,color:#1F1F1F,stroke-width:2px
style parallel fill:#F5F5F0,stroke:#8B8070,color:#1F1F1F,stroke-width:2px
Each parallel invocation of IncidentAnalysisAgent writes its result under "incidentAnalysis". The @ParallelMapperAgent framework collects these into a List<String> and passes it to the @Output method, which maps them positionally into IncidentAnalysisResults.
Rule
Every agent in a workflow must declare outputKey. Without it, the result is silently dropped from scope and the next agent finds nothing.
Step 1 — Implement IncidentAnalysisAgent (4 min)¶
Open IncidentAnalysisAgent.java.
The method signature is already declared. Add these annotations above it:
@SystemMessage("{task.systemInstructions}")
@UserMessage("""
Incident Information:
System: {incidentInfo.system}
Service: {incidentInfo.service}
Priority: P{incidentInfo.priority}
Current Description: {incidentInfo.description}
Report: {report}
""")
@Agent(description = "Incident analyzer. Using report, determines if action is needed based on task type.",
outputKey = "incidentAnalysis")
Dynamic @SystemMessage — one interface, three roles
The {task.systemInstructions} placeholder is resolved from the AnalysisTask parameter at runtime, not from a compile-time constant. The same interface handles three completely different analysis tasks:
| Task index | systemInstructions content |
|---|---|
| 0 | "You are a severity analyzer. Classify as P1-P4 or SEVERITY_LOW." |
| 1 | "You are a business impact analyzer. Assess revenue/SLA impact or IMPACT_MINIMAL." |
| 2 | "You are a resolution analyzer. If critical, ESCALATION_REQUIRED. Else ESCALATION_NOT_REQUIRED." |
One interface declaration powers three concurrent LLM calls with different roles.
Step 2 — Implement IncidentAnalysisWorkflow (3 min)¶
Open IncidentAnalysisWorkflow.java.
Replace both // TODO blocks with the following two members:
2a — @ParallelMapperAgent method:
@ParallelMapperAgent(
description = "Analyzes incident reports in parallel for severity, impact, and resolution needs",
outputKey = "incidentAnalysisResults",
subAgent = IncidentAnalysisAgent.class,
itemsProvider = "tasks")
IncidentAnalysisResults analyzeIncident(List<AnalysisTask> tasks,
IncidentInfo incidentInfo,
Integer incidentNumber,
String report);
2b — @Output static method:
@Output
static IncidentAnalysisResults output(AgenticScope scope,
List<String> incidentAnalysisResults) {
return new IncidentAnalysisResults(
incidentAnalysisResults.get(0), // severityAnalysis
incidentAnalysisResults.get(1), // impactAnalysis
incidentAnalysisResults.get(2) // resolutionAnalysis
);
}
Why itemsProvider = \"tasks\"?
@ParallelMapperAgent needs to know which method parameter holds the list of items to fan out over. itemsProvider = "tasks" names the tasks parameter — the framework fans out one IncidentAnalysisAgent call per element in tasks. The three calls run concurrently in separate virtual threads.
Why a static @Output method?
@Output is a post-processing step, not an LLM call. It receives the collected results from all parallel invocations and transforms them into a typed record. It runs synchronously after all parallel calls complete. Marking it static makes it clear it is a pure transformation with no injected dependencies.
Save both files. Quarkus hot-reloads.
Step 3 — Verify in the Dev UI (1 min)¶
Open the Agentic Dev UI to see the agent graph. Look for these two entries:
| Agent | Type | Key detail |
|---|---|---|
| IncidentAnalysisWorkflow | ParallelMapperAgent | subAgent = IncidentAnalysisAgent, itemsProvider = "tasks" |
| IncidentAnalysisAgent | Agent | outputKey = "incidentAnalysis" |
This page visualizes every agent's type, outputKey, and sub-agent wiring — use it after every exercise to confirm your changes compiled and registered correctly.
Why not a Java for loop instead of itemsProvider?
AgenticScope must be injected per-invocation so each parallel run gets its own scope context, result slot, and outputKey entry. A Java loop over LLM calls would run sequentially in the same thread with a shared scope — defeating both the parallelism and the scope isolation.
Step 4 — Test parallel execution (2 min)¶
The full workflow isn't wired in lab/ yet (that happens in Exercise 4), so you'll run this test from the solution project.
Stop lab/ first (Ctrl+C), then start the solution:
Open the topology — this solution has IncidentProcessingWorkflow fully wired, so the full agent tree is visible.
Open http://localhost:8080, click View on Incident #5 (email-service/notification-api), and process with:
Expected terminal logs:
The action should be INVESTIGATE or TRIAGE — the parallel analysis flagged this as needing attention. Check the UI — incident status changes to IN_PROGRESS or TRIAGING. (LLM responses are non-deterministic — the exact action may vary.)
Parallelism proof
Note the wall-clock time from when you clicked "Process" to when ResolutionAgent updating... appears (~3–5 seconds). Three parallel LLM calls complete in roughly the time of one. If they ran sequentially, it would take ~3x longer.
Now press s to restart (reset the database), then reload the browser. Process Incident #6 (search-engine/product-search) with:
Expected: Action is MONITOR or RESOLVE — the parallel analysis determined this is a low-severity, low-impact false alarm. Status stays OPEN or changes to RESOLVED.
Stop the solution (Ctrl+C) and restart lab/:
Done when¶
-
IncidentAnalysisAgentandIncidentAnalysisWorkflowappear in the Agentic Dev UI - No compile errors in the Quarkus terminal after hot reload
- Parallel execution tested via solution — wall-clock time confirms concurrency
- You can explain
outputKeyand@Outputfrom memory - You can explain how dynamic SystemMessage enables one interface for 3 tasks