Add Investigate button on Patrol findings

Pulse Intelligence direction named seven contextual Assistant entry
points: Explain, Investigate, Prepare fix, Verify fix, Why did
this happen, Remember this is expected, Create rule from this.
Explain was the only first-class one. Investigate was rolled into
"Discuss with Assistant" with a generic open-ended prompt.

Splits Investigate off as its own action. Where Explain says
"tell me what we already know," Investigate says "go find out
what's true right now":

- Widens PatrolAssistantFindingIntent to 'discuss' | 'explain' |
  'investigate'.
- buildPatrolAssistantFindingPrompt gains an investigate branch:
  the prompt explicitly instructs the LLM to use its Pulse tools
  (metrics, alerts, resource state, recent changes, correlations)
  to gather fresh evidence, then synthesize root cause +
  confidence + safe next step + whether the recommended action
  still holds. Any command-running step must route through
  governed approval, not the LLM's own judgment.
- FindingsPanel adds an Investigate button between Explain and
  Copy summary, and a handleInvestigateFinding handler that
  routes through openFindingInAssistant with the new intent.
- Both Explain and Investigate now set autoSendInitialPrompt:
  true (single condition update); Discuss stays false.

Test: a new investigate-intent prompt-builder case asserts the
prompt mentions active tool use, the synthesis dimensions
(root cause, confidence, safe next step), and the governed-
approval safety boundary, and isn't the Discuss seed.
This commit is contained in:
rcourtman
2026-05-11 09:14:21 +01:00
parent 8230275126
commit 362e5d37bd
3 changed files with 84 additions and 11 deletions
@@ -518,7 +518,7 @@ export const FindingsPanel: Component<FindingsPanelProps> = (props) => {
const openFindingInAssistant = async (
finding: UnifiedFinding,
intent: 'discuss' | 'explain',
intent: 'discuss' | 'explain' | 'investigate',
) => {
await aiIntelligenceStore.loadPendingApprovals();
const subject = getFindingSubjectPresentation(finding).label;
@@ -559,15 +559,15 @@ export const FindingsPanel: Component<FindingsPanelProps> = (props) => {
nextStepAction,
intent,
});
// Explain is an action-style entry point — clicking the button is the
// operator asking "do the explaining for me," not "open chat so I can
// type a question." Auto-submit the handoff prompt so the analysis 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 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.
aiChatStore.openWithPrompt(handoff.prompt, {
...handoff.context,
autoSendInitialPrompt: intent === 'explain',
autoSendInitialPrompt: intent === 'explain' || intent === 'investigate',
});
};
@@ -581,6 +581,17 @@ export const FindingsPanel: Component<FindingsPanelProps> = (props) => {
await openFindingInAssistant(finding, 'explain');
};
// Investigate is the active counterpart to Explain. Explain says "tell me
// what we already know"; Investigate says "go find out what's true right
// now." The prompt instructs the LLM to use its Pulse tools (metrics,
// alerts, state, recent changes, correlations) rather than just narrate
// the attached context, and the surface auto-sends so the work is in
// flight when the operator lands.
const handleInvestigateFinding = async (finding: UnifiedFinding, e: Event) => {
e.stopPropagation();
await openFindingInAssistant(finding, 'investigate');
};
// 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
@@ -1173,7 +1184,7 @@ export const FindingsPanel: Component<FindingsPanelProps> = (props) => {
type="button"
onClick={(e) => handleExplainFinding(finding, e)}
class="px-2 py-1 rounded border border-border hover:bg-surface-hover flex items-center gap-1"
title="Open Assistant with an explanation prompt"
title="Ask Pulse Assistant to explain what we know about this finding using the attached evidence"
>
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
@@ -1185,6 +1196,22 @@ export const FindingsPanel: Component<FindingsPanelProps> = (props) => {
</svg>
Explain
</button>
<button
type="button"
onClick={(e) => handleInvestigateFinding(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 actively investigate using metrics, alerts, and state — not just summarize"
>
<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="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
Investigate
</button>
<button
type="button"
onClick={(e) => handleCopyFindingSummary(finding, e)}
@@ -951,6 +951,36 @@ describe('patrolInvestigationContextModel', () => {
expect(explainPrompt).toContain('vm-101');
});
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
// right now" — the prompt must explicitly direct the LLM toward its
// Pulse tools (metrics, alerts, state) rather than just summarizing
// the attached briefing.
const prompt = buildPatrolAssistantFindingPrompt({
title: 'Backup job failing',
subject: 'vm-101',
description: 'Datastore quota exhausted',
intent: 'investigate',
});
expect(prompt).toContain('Investigate this Patrol finding now');
expect(prompt).toContain('Backup job failing');
expect(prompt).toContain('vm-101');
// Active-tool-use instruction — the differentiator vs Explain.
expect(prompt.toLowerCase()).toContain('actively use the pulse tools');
expect(prompt.toLowerCase()).toMatch(/metrics|alerts|resource state|recent changes/);
// Synthesis instruction — root cause + confidence + safe next step.
expect(prompt.toLowerCase()).toContain('root cause');
expect(prompt.toLowerCase()).toContain('confidence');
expect(prompt.toLowerCase()).toContain('safe next step');
// Safety: any command-running must route through governed approval,
// not the LLM's own judgment.
expect(prompt.toLowerCase()).toContain('governed approval');
// Must not be the open-ended Discuss prompt.
expect(prompt).not.toContain("I'd like to discuss");
});
it('surfaces investigation impact and rollback when the backend record carries them', () => {
const presentation = buildPatrolInvestigationRecordPresentation({
id: 'record-2',
@@ -86,7 +86,14 @@ export interface PatrolInvestigationRecordPresentation {
error?: string;
}
export type PatrolAssistantFindingIntent = 'discuss' | 'explain';
// 'discuss' = open-ended chat entry, operator drives.
// 'explain' = action-style "do the explaining," auto-sent.
// 'investigate' = action-style "run the investigation now using tools" —
// the prompt explicitly invites the LLM to query metrics/alerts/state
// 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';
export interface PatrolAssistantFindingPromptInput {
title: string;
@@ -526,10 +533,19 @@ export function buildPatrolAssistantFindingPrompt(
'Walk me through what we know, why it matters for the affected workloads, ' +
'how confident the analysis is, and whether the recommended action is the right next step. ' +
'Use the structured investigation record and operational memory in the attached context as the primary source.';
} else if (input.intent === 'investigate') {
prompt =
`Investigate this Patrol finding now: "${title}" on ${subject}. ` +
'Do not just summarize the attached context — actively use the Pulse tools available to you ' +
'(metrics, alerts, resource state, recent changes, correlations) to gather fresh evidence ' +
'about the current state of the affected resource and any related resources. ' +
'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 {
prompt = `I'd like to discuss this Patrol finding: "${title}" on ${subject}.`;
}
if (hasRecord && input.intent !== 'explain') {
if (hasRecord && input.intent !== 'explain' && input.intent !== 'investigate') {
prompt +=
'\n\nPulse Patrol has a structured investigation record for this finding. Use that record as the main context before suggesting next actions.';
}