Add Why button on Patrol findings (diagnostic / cause)

Third of the seven contextual Assistant entries from the captured
Pulse Intelligence direction. Where Explain says "tell me what we
know" and Investigate says "go find out what's true now," Why
says "what caused this":

- Widens PatrolAssistantFindingIntent to include 'why'.
- buildPatrolAssistantFindingPrompt gains a why branch that
  directs the LLM toward cause signals — recent changes around
  detection time, learned correlations, prior incident memory,
  regression history — rather than current state. The prompt
  asks for: what most likely caused this to fire now, what
  evidence in the attached context supports that cause, what
  would have to be true for the cause to recur. Tool calls for
  verification are allowed; state-changing commands still
  require operator approval.
- FindingsPanel adds a Why button between Investigate and Copy
  summary, with a handleWhyFinding handler routing through
  openFindingInAssistant.
- autoSendInitialPrompt now triggers for explain / investigate /
  why; Discuss with Assistant unchanged.

Test: new why-intent prompt-builder case asserts the prompt
mentions cause-focused signals (recent changes, correlations,
prior incidents, regressions), the synthesis dimensions (caused,
evidence, recur), and the operator-approval boundary, and isn't
either Discuss or Investigate phrasing.
This commit is contained in:
rcourtman
2026-05-11 09:18:09 +01:00
parent 3b6fc3ef31
commit dee757c927
3 changed files with 89 additions and 10 deletions
@@ -518,7 +518,7 @@ export const FindingsPanel: Component<FindingsPanelProps> = (props) => {
const openFindingInAssistant = async (
finding: UnifiedFinding,
intent: 'discuss' | 'explain' | 'investigate',
intent: 'discuss' | 'explain' | 'investigate' | 'why',
) => {
await aiIntelligenceStore.loadPendingApprovals();
const subject = getFindingSubjectPresentation(finding).label;
@@ -559,15 +559,17 @@ export const FindingsPanel: Component<FindingsPanelProps> = (props) => {
nextStepAction,
intent,
});
// Explain and Investigate are action-style entry points — clicking
// the button is the operator asking "do this for me," not "open chat
// so I can type a question." Auto-submit the handoff prompt so the
// work is already in flight when the drawer is on screen. Discuss
// with Assistant keeps the previous behaviour (pre-fill, operator
// drives) because that entry is open-ended by intent.
// Explain, Investigate, and Why-did-this-happen are action-style
// entry points — clicking the button is the operator asking "do this
// for me," not "open chat so I can type a question." Auto-submit the
// handoff prompt so the work is already in flight when the drawer is
// on screen. Discuss with Assistant keeps the previous behaviour
// (pre-fill, operator drives) because that entry is open-ended by
// intent.
aiChatStore.openWithPrompt(handoff.prompt, {
...handoff.context,
autoSendInitialPrompt: intent === 'explain' || intent === 'investigate',
autoSendInitialPrompt:
intent === 'explain' || intent === 'investigate' || intent === 'why',
});
};
@@ -592,6 +594,16 @@ export const FindingsPanel: Component<FindingsPanelProps> = (props) => {
await openFindingInAssistant(finding, 'investigate');
};
// Why is the diagnostic counterpart. Where Explain says "what we know"
// and Investigate says "what's true now," Why says "what caused this":
// the prompt focuses the LLM on recent changes around detection time,
// learned correlations, prior incidents, and regression history rather
// than current state.
const handleWhyFinding = async (finding: UnifiedFinding, e: Event) => {
e.stopPropagation();
await openFindingInAssistant(finding, 'why');
};
// Copy a Markdown summary of the finding to the clipboard so the operator
// can paste it into a chat, ticket, or incident channel. The shape mirrors
// the seven-question schema (title + impact + recommendation + trust
@@ -1212,6 +1224,22 @@ export const FindingsPanel: Component<FindingsPanelProps> = (props) => {
</svg>
Investigate
</button>
<button
type="button"
onClick={(e) => handleWhyFinding(finding, e)}
class="px-2 py-1 rounded border border-border hover:bg-surface-hover flex items-center gap-1"
title="Ask Pulse Assistant to explain the cause using recent changes, correlations, and incident history"
>
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
Why
</button>
<button
type="button"
onClick={(e) => handleCopyFindingSummary(finding, e)}
@@ -951,6 +951,37 @@ describe('patrolInvestigationContextModel', () => {
expect(explainPrompt).toContain('vm-101');
});
it('seeds a why-intent prompt that focuses on cause, not current state', () => {
// Why-did-this-happen is the diagnostic counterpart to Explain. Where
// Explain says "tell me what we know" and Investigate says "go find
// out what's true now," Why focuses the LLM on cause — recent
// changes, correlations, prior incidents, regression patterns. Not
// "is it bad right now" but "what made it become bad."
const prompt = buildPatrolAssistantFindingPrompt({
title: 'Backup job failing',
subject: 'vm-101',
description: 'Datastore quota exhausted',
intent: 'why',
});
expect(prompt).toContain('Why did this Patrol finding happen');
expect(prompt).toContain('Backup job failing');
expect(prompt).toContain('vm-101');
expect(prompt.toLowerCase()).toContain('cause, not on current state');
// The diagnostic signals the prompt directs the LLM toward.
expect(prompt.toLowerCase()).toMatch(/recent changes|correlations|prior incidents|regression/);
// Synthesis: what caused it, what evidence supports the cause, what
// would have to be true for it to recur.
expect(prompt.toLowerCase()).toContain('most likely caused');
expect(prompt.toLowerCase()).toContain('evidence');
expect(prompt.toLowerCase()).toContain('recur');
// Safety: cause investigation may need tools but must not run
// anything that changes state without operator approval.
expect(prompt.toLowerCase()).toContain('operator approval');
expect(prompt).not.toContain("I'd like to discuss");
expect(prompt).not.toContain('Investigate this Patrol finding now');
});
it('seeds an investigate-intent prompt that instructs active tool use, not just narration', () => {
// Investigate is the action counterpart to Explain. Where Explain says
// "tell me what we know," Investigate says "go find out what's true
@@ -93,7 +93,11 @@ export interface PatrolInvestigationRecordPresentation {
// rather than just narrate the attached context, and the surface
// auto-sends the prompt so the work is already in flight on the screen
// the operator lands on.
export type PatrolAssistantFindingIntent = 'discuss' | 'explain' | 'investigate';
// 'why' = diagnostic "why did this happen" — focuses the LLM on cause
// rather than current state: recent changes around detection time,
// learned correlations, prior incident memory, regression history.
// Action-style and auto-sent like Explain and Investigate.
export type PatrolAssistantFindingIntent = 'discuss' | 'explain' | 'investigate' | 'why';
export interface PatrolAssistantFindingPromptInput {
title: string;
@@ -542,10 +546,26 @@ export function buildPatrolAssistantFindingPrompt(
'Then synthesize: what is the root cause given the current evidence, ' +
'what is your confidence, what is the safe next step, and is the recommended action still right? ' +
'If any command-running step is involved, surface it for governed approval rather than executing on your own judgment.';
} else if (input.intent === 'why') {
prompt =
`Why did this Patrol finding happen: "${title}" on ${subject}? ` +
'Focus on cause, not on current state. Look at recent changes around the detection time, ' +
'learned correlations with other resources, prior incidents for this resource, and any ' +
'regression history that suggests a recurring trigger. ' +
'Then answer: what most likely caused this to fire now, ' +
'what evidence in the attached context supports that cause, ' +
'and what would have to be true for the cause to recur. ' +
'If the cause requires verification through a Pulse tool call, do that; do not run anything ' +
'that changes state without operator approval.';
} else {
prompt = `I'd like to discuss this Patrol finding: "${title}" on ${subject}.`;
}
if (hasRecord && input.intent !== 'explain' && input.intent !== 'investigate') {
if (
hasRecord &&
input.intent !== 'explain' &&
input.intent !== 'investigate' &&
input.intent !== 'why'
) {
prompt +=
'\n\nPulse Patrol has a structured investigation record for this finding. Use that record as the main context before suggesting next actions.';
}