mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-06 19:58:43 +02:00
fix: improve agent activity reporting
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -87,7 +87,7 @@ The Electron main process maps this to a `SessionEventRecord` with `kind: 'agent
|
||||
- "Code Reviewer is using read_file…"
|
||||
- "Handing off to Summarizer…"
|
||||
|
||||
The activity panel in `ChatPane.tsx` is now wired to this data, showing every agent in the pattern with observed activity such as thinking, tool usage, handoff, or completed. If no event has been observed for an agent yet, the UI now states that no status has been reported instead of inventing a synthetic state.
|
||||
The activity panel in `ChatPane.tsx` is now wired to this data, showing every agent in the pattern with observed activity such as thinking, tool usage, handoff, or completed. If no event has been observed for an agent yet, the UI states that no status has been reported instead of inventing a synthetic state. The panel also keeps the last observed statuses visible after a run completes, and resets them when the next run begins.
|
||||
|
||||
## Files involved
|
||||
|
||||
|
||||
@@ -50,6 +50,8 @@ public sealed class CopilotWorkflowRunner : ITurnWorkflowRunner
|
||||
int fallbackMessageIndex = 0;
|
||||
List<ChatMessageDto> completedMessages = [];
|
||||
AgentIdentity? activeAgent = null;
|
||||
HashSet<string> startedAgents = new(StringComparer.OrdinalIgnoreCase);
|
||||
HashSet<string> completedAgents = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
await using StreamingRun run = await InProcessExecution.RunStreamingAsync(workflow, inputMessages).ConfigureAwait(false);
|
||||
await run.TrySendMessageAsync(new TurnToken(emitEvents: true)).ConfigureAwait(false);
|
||||
@@ -57,16 +59,14 @@ public sealed class CopilotWorkflowRunner : ITurnWorkflowRunner
|
||||
await foreach (WorkflowEvent evt in run.WatchStreamAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
if (evt is ExecutorInvokedEvent invoked
|
||||
&& TryResolveKnownAgentIdentity(command.Pattern, invoked.ExecutorId, out AgentIdentity invokedAgent)
|
||||
&& (!activeAgent.HasValue
|
||||
|| !string.Equals(activeAgent.Value.AgentId, invokedAgent.AgentId, StringComparison.Ordinal)))
|
||||
&& TryResolveKnownAgentIdentity(command.Pattern, invoked.ExecutorId, out AgentIdentity invokedAgent))
|
||||
{
|
||||
activeAgent = invokedAgent;
|
||||
|
||||
await onActivity(CreateActivityEvent(
|
||||
await EmitThinkingIfNeeded(
|
||||
command,
|
||||
activityType: "thinking",
|
||||
agent: invokedAgent)).ConfigureAwait(false);
|
||||
invokedAgent,
|
||||
startedAgents,
|
||||
onActivity).ConfigureAwait(false);
|
||||
}
|
||||
else if (evt is RequestInfoEvent requestInfo)
|
||||
{
|
||||
@@ -89,6 +89,11 @@ public sealed class CopilotWorkflowRunner : ITurnWorkflowRunner
|
||||
if (TryResolveKnownAgentIdentity(command.Pattern, update.ExecutorId, out AgentIdentity updateAgent))
|
||||
{
|
||||
activeAgent = updateAgent;
|
||||
await EmitThinkingIfNeeded(
|
||||
command,
|
||||
updateAgent,
|
||||
startedAgents,
|
||||
onActivity).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await onDelta(new TurnDeltaEventDto
|
||||
@@ -102,22 +107,32 @@ public sealed class CopilotWorkflowRunner : ITurnWorkflowRunner
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
else if (evt is ExecutorCompletedEvent completed
|
||||
&& TryResolveKnownAgentIdentity(command.Pattern, completed.ExecutorId, out AgentIdentity completedAgent)
|
||||
&& activeAgent.HasValue
|
||||
&& string.Equals(activeAgent.Value.AgentId, completedAgent.AgentId, StringComparison.Ordinal))
|
||||
&& TryResolveKnownAgentIdentity(command.Pattern, completed.ExecutorId, out AgentIdentity completedAgent))
|
||||
{
|
||||
await onActivity(CreateActivityEvent(
|
||||
command,
|
||||
activityType: "completed",
|
||||
agent: completedAgent)).ConfigureAwait(false);
|
||||
if (completedAgents.Add(completedAgent.AgentId))
|
||||
{
|
||||
await onActivity(CreateActivityEvent(
|
||||
command,
|
||||
activityType: "completed",
|
||||
agent: completedAgent)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
activeAgent = null;
|
||||
if (activeAgent.HasValue
|
||||
&& string.Equals(activeAgent.Value.AgentId, completedAgent.AgentId, StringComparison.Ordinal))
|
||||
{
|
||||
activeAgent = null;
|
||||
}
|
||||
}
|
||||
else if (evt is WorkflowOutputEvent outputEvent)
|
||||
{
|
||||
List<ChatMessage> allMessages = outputEvent.As<List<ChatMessage>>() ?? [];
|
||||
List<ChatMessage> newMessages = allMessages.Skip(inputMessages.Count).ToList();
|
||||
completedMessages = ConvertOutputMessages(command, newMessages, segments);
|
||||
await EmitCompletedActivitiesForMessages(
|
||||
command,
|
||||
completedMessages,
|
||||
completedAgents,
|
||||
onActivity).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,6 +182,48 @@ public sealed class CopilotWorkflowRunner : ITurnWorkflowRunner
|
||||
toolName: toolName);
|
||||
}
|
||||
|
||||
private static async Task EmitThinkingIfNeeded(
|
||||
RunTurnCommandDto command,
|
||||
AgentIdentity agent,
|
||||
ISet<string> startedAgents,
|
||||
Func<AgentActivityEventDto, Task> onActivity)
|
||||
{
|
||||
if (!startedAgents.Add(agent.AgentId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await onActivity(CreateActivityEvent(
|
||||
command,
|
||||
activityType: "thinking",
|
||||
agent: agent)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static async Task EmitCompletedActivitiesForMessages(
|
||||
RunTurnCommandDto command,
|
||||
IReadOnlyList<ChatMessageDto> messages,
|
||||
ISet<string> completedAgents,
|
||||
Func<AgentActivityEventDto, Task> onActivity)
|
||||
{
|
||||
foreach (ChatMessageDto message in messages)
|
||||
{
|
||||
if (!TryResolveKnownAgentIdentity(command.Pattern, message.AuthorName, out AgentIdentity messageAgent))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!completedAgents.Add(messageAgent.AgentId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
await onActivity(CreateActivityEvent(
|
||||
command,
|
||||
activityType: "completed",
|
||||
agent: messageAgent)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryGetHandoffTarget(
|
||||
PatternDefinitionDto pattern,
|
||||
RequestInfoEvent requestInfo,
|
||||
|
||||
@@ -40,6 +40,8 @@ export function ChatPane({ activity, project, pattern, session, onSend }: ChatPa
|
||||
() => buildAgentActivityRows(activity, pattern.agents),
|
||||
[activity, pattern.agents],
|
||||
);
|
||||
const hasObservedActivity = activityRows.some((row) => !!row.activity);
|
||||
const showActivityPanel = (isBusy || hasObservedActivity) && activityRows.length > 0;
|
||||
|
||||
useEffect(() => {
|
||||
transcriptRef.current?.scrollTo({
|
||||
@@ -138,7 +140,7 @@ export function ChatPane({ activity, project, pattern, session, onSend }: ChatPa
|
||||
})}
|
||||
</div>
|
||||
|
||||
{isBusy && activityRows.length > 0 && (
|
||||
{showActivityPanel && (
|
||||
<div className="mb-4 rounded-xl border border-zinc-800 bg-zinc-900/60 px-4 py-3">
|
||||
<div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.16em] text-zinc-600">
|
||||
Agent activity
|
||||
|
||||
@@ -42,15 +42,11 @@ export function applySessionEventActivity(
|
||||
}
|
||||
|
||||
if (event.kind === 'status') {
|
||||
if (event.status === 'running' || event.status === 'idle' || event.status === 'error') {
|
||||
if (event.status === 'running') {
|
||||
return removeSessionActivity(current, event.sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
if (event.kind === 'error') {
|
||||
return removeSessionActivity(current, event.sessionId);
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ describe('session activity helpers', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('clears stale activity when a session restarts or finishes', () => {
|
||||
test('clears stale activity when a session restarts', () => {
|
||||
const current: SessionActivityMap = {
|
||||
'session-1': {
|
||||
architect: {
|
||||
@@ -111,17 +111,36 @@ describe('session activity helpers', () => {
|
||||
).toEqual({
|
||||
'session-2': current['session-2'],
|
||||
});
|
||||
});
|
||||
|
||||
test('keeps the last observed status after completion or error', () => {
|
||||
const current: SessionActivityMap = {
|
||||
'session-1': {
|
||||
architect: {
|
||||
agentId: 'architect',
|
||||
agentName: 'Architect',
|
||||
activityType: 'completed',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(
|
||||
applySessionEventActivity(current, {
|
||||
sessionId: 'session-2',
|
||||
kind: 'error',
|
||||
sessionId: 'session-1',
|
||||
kind: 'status',
|
||||
occurredAt: '2026-03-23T00:00:00.000Z',
|
||||
status: 'idle',
|
||||
}),
|
||||
).toEqual(current);
|
||||
|
||||
expect(
|
||||
applySessionEventActivity(current, {
|
||||
sessionId: 'session-1',
|
||||
kind: 'error',
|
||||
occurredAt: '2026-03-23T00:00:01.000Z',
|
||||
error: 'Boom',
|
||||
}),
|
||||
).toEqual({
|
||||
'session-1': current['session-1'],
|
||||
});
|
||||
).toEqual(current);
|
||||
});
|
||||
|
||||
test('builds rows for all agents with sensible defaults', () => {
|
||||
|
||||
Reference in New Issue
Block a user