Skip to content

Exercise 2 — Policy as Prompt

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/DiagnosticAgent.java

Solution fallback

solutions/02-maintenance-agent — includes DiagnosticAgent wired into a composed workflow.


Enterprise context

Chris, Apex Systems' Ops lead, is responsible for consistent diagnostic guidance across the operations team. The same symptom can require different handling depending on its priority and service context: a login outage needs immediate investigation, while a minor asset-loading delay may justify monitoring first. As runbooks change, Chris needs to update the guidance without letting each operator invent a different response.

This lab gives Chris a way to express diagnostic policy in the agent's instructions and observe how that policy affects the same incident. The agent produces a diagnostic plan rather than executing maintenance, so Chris can compare the suggestions before introducing tools that change systems. The hot-reload experiment makes the effect of a prompt edit visible; in an enterprise workflow, that edit would also need review and validation before release.

The goal

Discover that @SystemMessage is the policy — not code logic, not conditional branches. You'll add DiagnosticAgent (a text-only agent with no tool), then run a live tuning experiment: edit a single string in @SystemMessage, hot-reload, and watch the same incident produce completely different agent behavior.

This is the key insight: changing a prompt changes the policy. No redeploy, no if/else, no feature flag.


Step 1 — Implement DiagnosticAgent (4 min)

Open DiagnosticAgent.java.

The method signature is already declared. Add these annotations above it:

@SystemMessage("""
    You handle intake for the diagnostic department of an IT incident management system.
    Based on the incident report, determine what specific diagnostic actions are needed
    and provide a detailed root cause analysis plan.
    Be specific about what actions are needed based on the incident report.

    Available diagnostic actions include:
    - Log analysis
    - Service restart
    - Config rollback
    - Dependency check
    - Performance profiling
    - Network trace (packet capture, DNS, connectivity)

    For infrastructure issues like connectivity or DNS problems, include network trace in your plan.

    Provide your response as a structured diagnostic plan listing the specific actions needed.
    If no diagnostic action is needed based on the request, respond with "DIAGNOSTIC_NOT_REQUIRED".
    """)
@UserMessage("""
    Incident Information:
    System: {incidentInfo.system}
    Service: {incidentInfo.service}
    Priority: P{incidentInfo.priority}
    Incident Number: {incidentNumber}

    Diagnostic Request:
    {diagnosticRequest}
    """)
@Agent(description = "Incident diagnostic specialist. Using incident information and request, determines what diagnostic actions are needed.",
       outputKey = "analysisResult")
Why no @ToolBox here?

DiagnosticAgent returns a plan as text — it does not write to the database. The supervisor in Exercise 4 reads this text plan and decides whether to escalate. Text-only agents are faster and cheaper: no tool-call round-trips to the LLM.

Compare with TriageAgent: TriageAgent must call TriageTool.requestTriage() to actually mutate IncidentStatus. DiagnosticAgent only produces a recommendation. The supervisor decides what happens next.

Save the file. Quarkus hot-reloads. Check the terminal for any compile errors.


Step 2 — Verify the compile (1 min)

DiagnosticAgent wires into the supervisor orchestration in Exercise 4, so it can't be tested end-to-end yet. But you can confirm it compiled correctly:

  1. Check the Quarkus terminal — no red stack traces after hot reload
  2. Open the Agentic Dev UI — confirm DiagnosticAgent appears in the agent list with its outputKey and description

If you see Unsatisfied dependency or AmbiguousResolutionException, double-check: DiagnosticAgent must be an interface (not a class), with no @ApplicationScoped annotation.


Step 3 — @SystemMessage tuning experiment (5 min)

This is one of the most important insights in this lab: @SystemMessage is a policy declaration, not code logic.

The full workflow isn't wired in lab/ yet (that happens in Exercise 4), so you'll run this experiment from the solution project.

Stop lab/ first (Ctrl+C), then start the solution:

cd ../solutions/02-maintenance-agent
./mvnw quarkus:dev

Open TriageFeedbackAgent.java in the solution project and find this line in the @SystemMessage:

If no triage actions are needed based on the report, respond with "TRIAGE_NOT_REQUIRED".

Replace that line with the strict version:

Only request triage for CRITICAL issues: complete service outages,
data loss or corruption, security breaches, or cascading failures affecting multiple systems.
For intermittent errors, slow responses, or single-user complaints, respond with "TRIAGE_NOT_REQUIRED".

Why TriageFeedbackAgent and not TriageAgent?

TriageFeedbackAgent is the agent that decides whether triage is needed — its output (triageRequest) feeds into the @Output method that picks MONITOR vs TRIAGE. TriageAgent only executes triage actions after the decision is already made. Changing the wrong agent's prompt would have no visible effect.

Quarkus hot-reloads in ~1 second. Open http://localhost:8080, click View on Incident #7 (monitoring/alerting-api), and process it with:

Alert threshold slightly too sensitive, causing a few extra notifications

How to confirm: Check the Quarkus terminal logs and the incident status in the UI.

Expected terminal logs (minor report):

ReportAnalysisWorkflow executing...
  ├─ TriageFeedbackAgent analyzing...
  └─ DiagnosticFeedbackAgent analyzing...
IncidentAssignmentWorkflow evaluating conditions...
ResolutionAgent updating...
  └─ Incident #7 action: MONITOR

The key line is Incident #7 action: MONITOR or Incident #7 action: RESOLVE — the agents decided neither triage nor diagnostic escalation is needed for a minor alert sensitivity issue. Status stays OPEN or changes to RESOLVED. (LLM responses are non-deterministic — the exact action may vary between runs.)

Now press s to restart (reset the database), then reload the browser to see incident #7 back to OPEN. Process it with a critical report:

Complete monitoring blackout — zero alerts firing, all dashboards showing stale data, on-call has no visibility

Expected terminal logs (critical report):

ReportAnalysisWorkflow executing...
  ├─ TriageFeedbackAgent analyzing...
  └─ DiagnosticFeedbackAgent analyzing...
IncidentAssignmentWorkflow evaluating conditions...
ResolutionAgent updating...
  └─ Incident #7 action: INVESTIGATE

The key difference: the action should be INVESTIGATE or TRIAGE — a more aggressive response than the minor report. The DiagnosticFeedbackAgent flagged this as needing root cause analysis. Incident status changes to IN_PROGRESS or TRIAGING in the UI.

Key insight

You changed agent behavior by editing a string — no conditional logic, no redeploy cycle beyond hot reload. The @SystemMessage is the policy. This is what "declarative AI engineering" means.

Revert TriageFeedbackAgent.java's @SystemMessage back to the original (simpler) version.

Stop the solution (Ctrl+C) and restart the lab for the next exercise:

cd ../../lab
./mvnw quarkus:dev

Done when

  • DiagnosticAgent.java compiles — no errors (interface, outputKey="analysisResult", no CDI scope, no @ToolBox)
  • @SystemMessage threshold experiment completed — strict vs lenient behavior observed
  • You can articulate: when does an agent need @ToolBox? When is text-only output correct?