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.
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