Promote Patrol related-resource handoff evidence

This commit is contained in:
rcourtman 2026-05-13 13:35:37 +01:00
parent 8c7a01186e
commit 288bffe9cb
2 changed files with 84 additions and 5 deletions

View file

@ -166,6 +166,51 @@ describe('patrolInvestigationContextModel', () => {
expect(handoff.context.handoffContext).not.toContain('online to online');
});
it('includes bounded related-resource context in assessment handoff evidence', () => {
const longRelatedResource =
'storage-pool-with-a-very-long-description-that-keeps-going-beyond-the-handoff-limit-for-operators';
const handoff = buildPatrolAssessmentAssistantHandoff({
assessment: {
title: 'Issues detected',
},
supportingEvidence: {
recentChanges: [
{
id: 'change-related',
observedAt: '2026-05-06T12:08:00Z',
resourceId: 'vm-100',
kind: 'metric_anomaly',
sourceType: 'heuristic',
sourceAdapter: 'proxmox_adapter',
confidence: 'high',
reason: 'CPU pressure increased after storage activity',
relatedResources: [
'backup-job',
'cache-node',
longRelatedResource,
'db-primary',
'db-replica',
],
},
],
},
activeFindings: [],
});
const relatedEvidence = handoff.context.briefing?.evidence?.find((line) =>
line.includes('related resources'),
);
expect(relatedEvidence).toContain('related resources backup-job');
expect(relatedEvidence).toContain('and 1 more');
expect(relatedEvidence).not.toContain(longRelatedResource);
expect(relatedEvidence).not.toContain('db-replica');
expect(handoff.context.handoffContext).toContain('related resources backup-job');
expect(handoff.context.handoffContext).toContain('and 1 more');
expect(handoff.context.handoffContext).not.toContain(longRelatedResource);
expect(handoff.context.handoffContext).not.toContain('db-replica');
});
it('builds a model-only Assistant handoff for the current Patrol assessment', () => {
const handoff = buildPatrolAssessmentAssistantHandoff({
assessment: {
@ -326,6 +371,10 @@ describe('patrolInvestigationContextModel', () => {
expect(handoff.context.handoffContext).toContain(
'Recent Change 1: Metric anomaly: CPU spike after backup job',
);
expect(handoff.context.handoffContext).toContain('related resources backup-job');
expect(handoff.context.briefing?.evidence).toEqual(
expect.arrayContaining([expect.stringContaining('related resources backup-job')]),
);
expect(handoff.context.handoffContext).toContain(
'Recent Change 2: Command executed: execution event recorded',
);

View file

@ -354,6 +354,8 @@ const MAX_ASSESSMENT_RESOURCES = 8;
const MAX_ASSESSMENT_HANDOFF_ACTIONS = 4;
const MAX_PATROL_RUN_HANDOFF_RESOURCES = 8;
const MAX_PATROL_BRIEFING_SUGGESTED_PROMPTS = 3;
const MAX_ASSESSMENT_RELATED_RESOURCE_LABELS = 4;
const MAX_ASSESSMENT_RELATED_RESOURCE_LABEL_LENGTH = 80;
const SAME_STATE_CHANGED_FIELD_LABELS: Record<string, string> = {
'docker.command': 'Docker command',
'docker.updateStatus': 'Docker image status',
@ -2065,6 +2067,7 @@ function formatAssessmentRecentChangeEvidence(change: ResourceChange): string |
return [
summary,
resource ? `resource ${resource}` : undefined,
formatAssessmentRecentChangeRelatedResources(change),
observedAt ? `observed ${observedAt}` : undefined,
]
.filter(isNonEmptyString)
@ -2085,10 +2088,6 @@ function formatAssessmentCorrelationEvidence(correlation: ResourceCorrelation):
}
function formatAssessmentRecentChangeContextLine(change: ResourceChange, index: number): string {
const relatedResources = (change.relatedResources ?? [])
.map(normalizeText)
.filter(isNonEmptyString)
.slice(0, 4);
const parts = [
formatAssessmentRecentChangeSummary(change),
normalizeText(change.id) ? `change ${normalizeText(change.id)}` : undefined,
@ -2105,12 +2104,43 @@ function formatAssessmentRecentChangeContextLine(change: ResourceChange, index:
? `${formatIdentifierLabel(change.confidence)?.toLowerCase()} confidence`
: undefined,
normalizeText(change.actor) ? `actor ${truncateContextText(change.actor, 80)}` : undefined,
relatedResources.length > 0 ? `related ${relatedResources.join(', ')}` : undefined,
formatAssessmentRecentChangeRelatedResources(change),
].filter(isNonEmptyString);
return `Recent Change ${index}: ${parts.join('; ')}`;
}
function formatAssessmentRecentChangeRelatedResources(
change: ResourceChange,
): string | undefined {
const labels: string[] = [];
for (const relatedResource of change.relatedResources ?? []) {
const label = truncateContextText(
relatedResource,
MAX_ASSESSMENT_RELATED_RESOURCE_LABEL_LENGTH,
);
if (label && !labels.includes(label)) {
labels.push(label);
}
}
if (labels.length === 0) return undefined;
const visibleLabels = labels.slice(0, MAX_ASSESSMENT_RELATED_RESOURCE_LABELS);
const omittedCount = labels.length - visibleLabels.length;
return `related resources ${formatAssessmentRelatedResourceList(visibleLabels, omittedCount)}`;
}
function formatAssessmentRelatedResourceList(labels: string[], omittedCount: number): string {
if (labels.length === 1) {
return omittedCount > 0 ? `${labels[0]}, and ${omittedCount} more` : labels[0];
}
if (labels.length === 2 && omittedCount === 0) {
return `${labels[0]} and ${labels[1]}`;
}
const visibleList = labels.join(', ');
return omittedCount > 0 ? `${visibleList}, and ${omittedCount} more` : visibleList;
}
function formatAssessmentCorrelationContextLine(
correlation: ResourceCorrelation,
index: number,