mirror of
https://github.com/davidkaya/aryx.git
synced 2026-07-24 05:28:46 +02:00
feat: render workflow diagnostics in activity panel
Surface workflow-diagnostic session events (warnings and errors from Agent Framework workflows) in the turn-event log of the Activity Panel. - Add workflow-diagnostic case to formatTurnEventEntry with label/detail formatting that includes executor ID, subworkflow ID, exception type, and diagnostic message when present - Add AlertTriangle icon for diagnostic events with error/warning color - Add 5 tests covering executor-failed, workflow-warning, subworkflow-error, workflow-error, and missing diagnosticKind fallback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useMemo, type ReactNode } from 'react';
|
||||
import { Activity, ArrowRight, BarChart3, CheckCircle2, Clock, Cog, ShieldAlert, Sparkles, Users, Zap } from 'lucide-react';
|
||||
import { Activity, AlertTriangle, ArrowRight, BarChart3, CheckCircle2, Clock, Cog, ShieldAlert, Sparkles, Users, Zap } from 'lucide-react';
|
||||
|
||||
import {
|
||||
buildAgentActivityRows,
|
||||
@@ -190,6 +190,8 @@ function TurnEventIcon({ kind, phase, success }: { kind: SessionEventKind; phase
|
||||
return <Sparkles className={`${base} text-[var(--color-accent-purple)]`} />;
|
||||
case 'session-compaction':
|
||||
return <CheckCircle2 className={`${base} ${phase === 'start' ? 'animate-pulse text-[var(--color-status-warning)]' : 'text-[var(--color-status-success)]'}`} />;
|
||||
case 'workflow-diagnostic':
|
||||
return <AlertTriangle className={`${base} ${success === false ? 'text-[var(--color-status-error)]' : 'text-[var(--color-status-warning)]'}`} />;
|
||||
default:
|
||||
return <Zap className={`${base} text-[var(--color-text-muted)]`} />;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { PatternDefinition } from '@shared/domain/pattern';
|
||||
import type { SessionEventRecord } from '@shared/domain/event';
|
||||
import type { QuotaSnapshot } from '@shared/contracts/sidecar';
|
||||
import type { QuotaSnapshot, WorkflowDiagnosticKind, WorkflowDiagnosticSeverity } from '@shared/contracts/sidecar';
|
||||
|
||||
export interface AgentActivityState {
|
||||
agentId: string;
|
||||
@@ -260,6 +260,22 @@ function formatHookType(hookType: string | undefined): string {
|
||||
return hookTypeLabels[hookType] ?? hookType;
|
||||
}
|
||||
|
||||
const diagnosticLabels: Record<WorkflowDiagnosticKind, string> = {
|
||||
'workflow-warning': 'Workflow warning',
|
||||
'workflow-error': 'Workflow error',
|
||||
'executor-failed': 'Executor failed',
|
||||
'subworkflow-warning': 'Subworkflow warning',
|
||||
'subworkflow-error': 'Subworkflow error',
|
||||
};
|
||||
|
||||
function formatDiagnosticLabel(
|
||||
kind: WorkflowDiagnosticKind | undefined,
|
||||
severity: WorkflowDiagnosticSeverity | undefined,
|
||||
): string {
|
||||
if (kind) return diagnosticLabels[kind] ?? kind;
|
||||
return severity === 'error' ? 'Workflow error' : 'Workflow warning';
|
||||
}
|
||||
|
||||
function formatTurnEventEntry(event: SessionEventRecord): TurnEventEntry | undefined {
|
||||
switch (event.kind) {
|
||||
case 'subagent':
|
||||
@@ -300,6 +316,21 @@ function formatTurnEventEntry(event: SessionEventRecord): TurnEventEntry | undef
|
||||
phase: event.compactionPhase,
|
||||
success: event.compactionSuccess,
|
||||
};
|
||||
case 'workflow-diagnostic': {
|
||||
const label = formatDiagnosticLabel(event.diagnosticKind, event.diagnosticSeverity);
|
||||
const detailParts: string[] = [];
|
||||
if (event.executorId) detailParts.push(event.executorId);
|
||||
if (event.subworkflowId) detailParts.push(event.subworkflowId);
|
||||
if (event.exceptionType) detailParts.push(event.exceptionType);
|
||||
if (event.diagnosticMessage) detailParts.push(event.diagnosticMessage);
|
||||
return {
|
||||
kind: event.kind,
|
||||
occurredAt: event.occurredAt,
|
||||
label,
|
||||
detail: detailParts.length > 0 ? detailParts.join(' · ') : undefined,
|
||||
success: event.diagnosticSeverity === 'error' ? false : undefined,
|
||||
};
|
||||
}
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { describe, expect, test } from 'bun:test';
|
||||
import {
|
||||
applySessionEventActivity,
|
||||
applyAssistantUsageEvent,
|
||||
applyTurnEventLog,
|
||||
buildAgentActivityRows,
|
||||
formatAgentActivityLabel,
|
||||
formatDuration,
|
||||
@@ -527,3 +528,79 @@ describe('usage formatting helpers', () => {
|
||||
expect(formatDuration(150_000)).toBe('2.5m');
|
||||
});
|
||||
});
|
||||
|
||||
describe('workflow diagnostic turn events', () => {
|
||||
function makeDiagnosticEvent(overrides: Partial<SessionEventRecord> = {}): SessionEventRecord {
|
||||
return {
|
||||
sessionId: 'session-1',
|
||||
kind: 'workflow-diagnostic',
|
||||
occurredAt: '2026-03-23T00:00:00.000Z',
|
||||
diagnosticSeverity: 'error',
|
||||
diagnosticKind: 'executor-failed',
|
||||
diagnosticMessage: 'Tool crashed.',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
test('formats executor-failed with full metadata', () => {
|
||||
const result = applyTurnEventLog({}, makeDiagnosticEvent({
|
||||
executorId: 'Primary',
|
||||
exceptionType: 'InvalidOperationException',
|
||||
}));
|
||||
const entries = result['session-1']!;
|
||||
expect(entries).toHaveLength(1);
|
||||
expect(entries[0].label).toBe('Executor failed');
|
||||
expect(entries[0].detail).toBe('Primary · InvalidOperationException · Tool crashed.');
|
||||
expect(entries[0].success).toBe(false);
|
||||
});
|
||||
|
||||
test('formats workflow-warning with message only', () => {
|
||||
const result = applyTurnEventLog({}, makeDiagnosticEvent({
|
||||
diagnosticSeverity: 'warning',
|
||||
diagnosticKind: 'workflow-warning',
|
||||
diagnosticMessage: 'Token budget is nearly exhausted.',
|
||||
executorId: undefined,
|
||||
exceptionType: undefined,
|
||||
}));
|
||||
const entries = result['session-1']!;
|
||||
expect(entries[0].label).toBe('Workflow warning');
|
||||
expect(entries[0].detail).toBe('Token budget is nearly exhausted.');
|
||||
expect(entries[0].success).toBeUndefined();
|
||||
});
|
||||
|
||||
test('formats subworkflow-error with subworkflow ID', () => {
|
||||
const result = applyTurnEventLog({}, makeDiagnosticEvent({
|
||||
diagnosticKind: 'subworkflow-error',
|
||||
subworkflowId: 'subworkflow-review',
|
||||
exceptionType: 'InvalidOperationException',
|
||||
diagnosticMessage: 'Reviewer agent failed.',
|
||||
}));
|
||||
const entries = result['session-1']!;
|
||||
expect(entries[0].label).toBe('Subworkflow error');
|
||||
expect(entries[0].detail).toBe('subworkflow-review · InvalidOperationException · Reviewer agent failed.');
|
||||
});
|
||||
|
||||
test('formats workflow-error without optional fields', () => {
|
||||
const result = applyTurnEventLog({}, makeDiagnosticEvent({
|
||||
diagnosticKind: 'workflow-error',
|
||||
diagnosticMessage: 'Workflow terminated unexpectedly.',
|
||||
executorId: undefined,
|
||||
subworkflowId: undefined,
|
||||
exceptionType: undefined,
|
||||
}));
|
||||
const entries = result['session-1']!;
|
||||
expect(entries[0].label).toBe('Workflow error');
|
||||
expect(entries[0].detail).toBe('Workflow terminated unexpectedly.');
|
||||
expect(entries[0].success).toBe(false);
|
||||
});
|
||||
|
||||
test('falls back to severity when diagnosticKind is missing', () => {
|
||||
const result = applyTurnEventLog({}, makeDiagnosticEvent({
|
||||
diagnosticKind: undefined,
|
||||
diagnosticSeverity: 'warning',
|
||||
diagnosticMessage: 'Something odd happened.',
|
||||
}));
|
||||
const entries = result['session-1']!;
|
||||
expect(entries[0].label).toBe('Workflow warning');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user