mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-06 11:48:46 +02:00
fix: scope turn-activity panel metrics to per-agent events in sequential workflows
In sequential workflows, multiple collapsible turn-activity panels shared the same SessionRunRecord and displayed identical aggregate metrics (tool calls, approvals, etc.) instead of per-agent counts. Root cause: ChatPane grouped thinking messages correctly per agent, but passed the full unfiltered run object to every TurnActivityPanel, so summarizeActivity() counted all events from the entire run for each panel. Changes: - Extract filterEventsByAgent() and summarizeActivity() to runTimelineFormatting.ts as pure, testable helpers - Add agentNames (derived from thinking message authors) and isLastRunPanel to the DisplayItem turn-activity variant in ChatPane - TurnActivityPanel now filters run.events through filterEventsByAgent() so each panel only counts events belonging to its agents - Git summary / discard actions only render on the last panel of a run - Per-agent timing derived from scoped events instead of run-level start Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -2,10 +2,12 @@ import { describe, expect, test } from 'bun:test';
|
||||
|
||||
import {
|
||||
collapseTimelineEvents,
|
||||
filterEventsByAgent,
|
||||
formatEventLabel,
|
||||
formatRunDuration,
|
||||
formatRunStatusLabel,
|
||||
formatRunTimestamp,
|
||||
summarizeActivity,
|
||||
truncateContent,
|
||||
} from '@renderer/lib/runTimelineFormatting';
|
||||
import type { RunTimelineEventRecord } from '@shared/domain/runTimeline';
|
||||
@@ -134,3 +136,139 @@ describe('run timeline formatting', () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
/* ── filterEventsByAgent ──────────────────────────────────── */
|
||||
|
||||
describe('filterEventsByAgent', () => {
|
||||
const sharedEvents: RunTimelineEventRecord[] = [
|
||||
createEvent({ id: 'e-start', kind: 'run-started' }),
|
||||
createEvent({ id: 'e-tc-w1', kind: 'tool-call', agentName: 'Writer', toolName: 'edit' }),
|
||||
createEvent({ id: 'e-tc-w2', kind: 'tool-call', agentName: 'Writer', toolName: 'grep' }),
|
||||
createEvent({ id: 'e-ap-w', kind: 'approval', agentName: 'Writer' }),
|
||||
createEvent({ id: 'e-hoff', kind: 'handoff', sourceAgentName: 'Writer', targetAgentName: 'Reviewer' }),
|
||||
createEvent({ id: 'e-tc-r1', kind: 'tool-call', agentName: 'Reviewer', toolName: 'view' }),
|
||||
createEvent({ id: 'e-tc-r2', kind: 'tool-call', agentName: 'Reviewer', toolName: 'grep' }),
|
||||
createEvent({ id: 'e-msg-r', kind: 'message', agentName: 'Reviewer', content: 'Done.' }),
|
||||
createEvent({ id: 'e-end', kind: 'run-completed' }),
|
||||
];
|
||||
|
||||
test('returns all events when agentNames is undefined (single-agent)', () => {
|
||||
const result = filterEventsByAgent(sharedEvents, undefined);
|
||||
expect(result).toHaveLength(sharedEvents.length);
|
||||
});
|
||||
|
||||
test('returns all events when agentNames is an empty set', () => {
|
||||
const result = filterEventsByAgent(sharedEvents, new Set());
|
||||
expect(result).toHaveLength(sharedEvents.length);
|
||||
});
|
||||
|
||||
test('filters to Writer agent events only', () => {
|
||||
const result = filterEventsByAgent(sharedEvents, new Set(['Writer']));
|
||||
const ids = result.map((e) => e.id);
|
||||
expect(ids).toEqual(['e-tc-w1', 'e-tc-w2', 'e-ap-w', 'e-hoff']);
|
||||
});
|
||||
|
||||
test('filters to Reviewer agent events only', () => {
|
||||
const result = filterEventsByAgent(sharedEvents, new Set(['Reviewer']));
|
||||
const ids = result.map((e) => e.id);
|
||||
expect(ids).toEqual(['e-hoff', 'e-tc-r1', 'e-tc-r2', 'e-msg-r']);
|
||||
});
|
||||
|
||||
test('excludes run-level events from agent-scoped results', () => {
|
||||
const result = filterEventsByAgent(sharedEvents, new Set(['Writer']));
|
||||
const kinds = result.map((e) => e.kind);
|
||||
expect(kinds).not.toContain('run-started');
|
||||
expect(kinds).not.toContain('run-completed');
|
||||
});
|
||||
|
||||
test('excludes events with no agentName in agent-scoped mode', () => {
|
||||
const events: RunTimelineEventRecord[] = [
|
||||
createEvent({ id: 'e1', kind: 'tool-call', agentName: undefined, toolName: 'unknown' }),
|
||||
createEvent({ id: 'e2', kind: 'tool-call', agentName: 'Writer', toolName: 'edit' }),
|
||||
];
|
||||
const result = filterEventsByAgent(events, new Set(['Writer']));
|
||||
expect(result.map((e) => e.id)).toEqual(['e2']);
|
||||
});
|
||||
|
||||
test('includes handoff events for both source and target agents', () => {
|
||||
const handoff = sharedEvents.find((e) => e.kind === 'handoff')!;
|
||||
const writerResult = filterEventsByAgent([handoff], new Set(['Writer']));
|
||||
const reviewerResult = filterEventsByAgent([handoff], new Set(['Reviewer']));
|
||||
expect(writerResult).toHaveLength(1);
|
||||
expect(reviewerResult).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
/* ── summarizeActivity ────────────────────────────────────── */
|
||||
|
||||
describe('summarizeActivity', () => {
|
||||
test('counts thinking steps from messages with content', () => {
|
||||
const messages = [
|
||||
{ content: 'step 1' },
|
||||
{ content: '' },
|
||||
{ content: 'step 2' },
|
||||
];
|
||||
const summary = summarizeActivity(messages, []);
|
||||
expect(summary.thinkingSteps).toBe(2);
|
||||
expect(summary.toolCalls).toBe(0);
|
||||
});
|
||||
|
||||
test('counts tool calls, handoffs, and approvals from events', () => {
|
||||
const events: RunTimelineEventRecord[] = [
|
||||
createEvent({ id: 'e1', kind: 'tool-call', agentName: 'A' }),
|
||||
createEvent({ id: 'e2', kind: 'tool-call', agentName: 'A' }),
|
||||
createEvent({ id: 'e3', kind: 'handoff', sourceAgentName: 'A', targetAgentName: 'B' }),
|
||||
createEvent({ id: 'e4', kind: 'approval', agentName: 'A' }),
|
||||
];
|
||||
const summary = summarizeActivity([], events);
|
||||
expect(summary).toMatchObject({
|
||||
thinkingSteps: 0,
|
||||
toolCalls: 2,
|
||||
handoffs: 1,
|
||||
approvals: 1,
|
||||
hasError: false,
|
||||
});
|
||||
});
|
||||
|
||||
test('detects run-failed events', () => {
|
||||
const events: RunTimelineEventRecord[] = [
|
||||
createEvent({ id: 'e1', kind: 'run-failed' }),
|
||||
];
|
||||
const summary = summarizeActivity([], events);
|
||||
expect(summary.hasError).toBe(true);
|
||||
});
|
||||
|
||||
test('produces correct per-agent summary when combined with filterEventsByAgent', () => {
|
||||
const allEvents: RunTimelineEventRecord[] = [
|
||||
createEvent({ id: 'e-start', kind: 'run-started' }),
|
||||
createEvent({ id: 'e1', kind: 'tool-call', agentName: 'Writer' }),
|
||||
createEvent({ id: 'e2', kind: 'tool-call', agentName: 'Writer' }),
|
||||
createEvent({ id: 'e3', kind: 'tool-call', agentName: 'Writer' }),
|
||||
createEvent({ id: 'e4', kind: 'approval', agentName: 'Writer' }),
|
||||
createEvent({ id: 'e5', kind: 'handoff', sourceAgentName: 'Writer', targetAgentName: 'Reviewer' }),
|
||||
createEvent({ id: 'e6', kind: 'tool-call', agentName: 'Reviewer' }),
|
||||
createEvent({ id: 'e7', kind: 'approval', agentName: 'Reviewer' }),
|
||||
createEvent({ id: 'e-end', kind: 'run-completed' }),
|
||||
];
|
||||
|
||||
const writerEvents = filterEventsByAgent(allEvents, new Set(['Writer']));
|
||||
const writerSummary = summarizeActivity([{ content: 'think1' }, { content: 'think2' }], writerEvents);
|
||||
expect(writerSummary).toMatchObject({
|
||||
thinkingSteps: 2,
|
||||
toolCalls: 3,
|
||||
handoffs: 1,
|
||||
approvals: 1,
|
||||
hasError: false,
|
||||
});
|
||||
|
||||
const reviewerEvents = filterEventsByAgent(allEvents, new Set(['Reviewer']));
|
||||
const reviewerSummary = summarizeActivity([{ content: 'think3' }], reviewerEvents);
|
||||
expect(reviewerSummary).toMatchObject({
|
||||
thinkingSteps: 1,
|
||||
toolCalls: 1,
|
||||
handoffs: 1,
|
||||
approvals: 1,
|
||||
hasError: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user