mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-30 06:27:13 +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…"
|
- "Code Reviewer is using read_file…"
|
||||||
- "Handing off to Summarizer…"
|
- "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
|
## Files involved
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ public sealed class CopilotWorkflowRunner : ITurnWorkflowRunner
|
|||||||
int fallbackMessageIndex = 0;
|
int fallbackMessageIndex = 0;
|
||||||
List<ChatMessageDto> completedMessages = [];
|
List<ChatMessageDto> completedMessages = [];
|
||||||
AgentIdentity? activeAgent = null;
|
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 using StreamingRun run = await InProcessExecution.RunStreamingAsync(workflow, inputMessages).ConfigureAwait(false);
|
||||||
await run.TrySendMessageAsync(new TurnToken(emitEvents: true)).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))
|
await foreach (WorkflowEvent evt in run.WatchStreamAsync(cancellationToken).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
if (evt is ExecutorInvokedEvent invoked
|
if (evt is ExecutorInvokedEvent invoked
|
||||||
&& TryResolveKnownAgentIdentity(command.Pattern, invoked.ExecutorId, out AgentIdentity invokedAgent)
|
&& TryResolveKnownAgentIdentity(command.Pattern, invoked.ExecutorId, out AgentIdentity invokedAgent))
|
||||||
&& (!activeAgent.HasValue
|
|
||||||
|| !string.Equals(activeAgent.Value.AgentId, invokedAgent.AgentId, StringComparison.Ordinal)))
|
|
||||||
{
|
{
|
||||||
activeAgent = invokedAgent;
|
activeAgent = invokedAgent;
|
||||||
|
await EmitThinkingIfNeeded(
|
||||||
await onActivity(CreateActivityEvent(
|
|
||||||
command,
|
command,
|
||||||
activityType: "thinking",
|
invokedAgent,
|
||||||
agent: invokedAgent)).ConfigureAwait(false);
|
startedAgents,
|
||||||
|
onActivity).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
else if (evt is RequestInfoEvent requestInfo)
|
else if (evt is RequestInfoEvent requestInfo)
|
||||||
{
|
{
|
||||||
@@ -89,6 +89,11 @@ public sealed class CopilotWorkflowRunner : ITurnWorkflowRunner
|
|||||||
if (TryResolveKnownAgentIdentity(command.Pattern, update.ExecutorId, out AgentIdentity updateAgent))
|
if (TryResolveKnownAgentIdentity(command.Pattern, update.ExecutorId, out AgentIdentity updateAgent))
|
||||||
{
|
{
|
||||||
activeAgent = updateAgent;
|
activeAgent = updateAgent;
|
||||||
|
await EmitThinkingIfNeeded(
|
||||||
|
command,
|
||||||
|
updateAgent,
|
||||||
|
startedAgents,
|
||||||
|
onActivity).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
await onDelta(new TurnDeltaEventDto
|
await onDelta(new TurnDeltaEventDto
|
||||||
@@ -102,22 +107,32 @@ public sealed class CopilotWorkflowRunner : ITurnWorkflowRunner
|
|||||||
}).ConfigureAwait(false);
|
}).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
else if (evt is ExecutorCompletedEvent completed
|
else if (evt is ExecutorCompletedEvent completed
|
||||||
&& TryResolveKnownAgentIdentity(command.Pattern, completed.ExecutorId, out AgentIdentity completedAgent)
|
&& TryResolveKnownAgentIdentity(command.Pattern, completed.ExecutorId, out AgentIdentity completedAgent))
|
||||||
&& activeAgent.HasValue
|
|
||||||
&& string.Equals(activeAgent.Value.AgentId, completedAgent.AgentId, StringComparison.Ordinal))
|
|
||||||
{
|
{
|
||||||
await onActivity(CreateActivityEvent(
|
if (completedAgents.Add(completedAgent.AgentId))
|
||||||
command,
|
{
|
||||||
activityType: "completed",
|
await onActivity(CreateActivityEvent(
|
||||||
agent: completedAgent)).ConfigureAwait(false);
|
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)
|
else if (evt is WorkflowOutputEvent outputEvent)
|
||||||
{
|
{
|
||||||
List<ChatMessage> allMessages = outputEvent.As<List<ChatMessage>>() ?? [];
|
List<ChatMessage> allMessages = outputEvent.As<List<ChatMessage>>() ?? [];
|
||||||
List<ChatMessage> newMessages = allMessages.Skip(inputMessages.Count).ToList();
|
List<ChatMessage> newMessages = allMessages.Skip(inputMessages.Count).ToList();
|
||||||
completedMessages = ConvertOutputMessages(command, newMessages, segments);
|
completedMessages = ConvertOutputMessages(command, newMessages, segments);
|
||||||
|
await EmitCompletedActivitiesForMessages(
|
||||||
|
command,
|
||||||
|
completedMessages,
|
||||||
|
completedAgents,
|
||||||
|
onActivity).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,6 +182,48 @@ public sealed class CopilotWorkflowRunner : ITurnWorkflowRunner
|
|||||||
toolName: toolName);
|
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(
|
private static bool TryGetHandoffTarget(
|
||||||
PatternDefinitionDto pattern,
|
PatternDefinitionDto pattern,
|
||||||
RequestInfoEvent requestInfo,
|
RequestInfoEvent requestInfo,
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ export function ChatPane({ activity, project, pattern, session, onSend }: ChatPa
|
|||||||
() => buildAgentActivityRows(activity, pattern.agents),
|
() => buildAgentActivityRows(activity, pattern.agents),
|
||||||
[activity, pattern.agents],
|
[activity, pattern.agents],
|
||||||
);
|
);
|
||||||
|
const hasObservedActivity = activityRows.some((row) => !!row.activity);
|
||||||
|
const showActivityPanel = (isBusy || hasObservedActivity) && activityRows.length > 0;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
transcriptRef.current?.scrollTo({
|
transcriptRef.current?.scrollTo({
|
||||||
@@ -138,7 +140,7 @@ export function ChatPane({ activity, project, pattern, session, onSend }: ChatPa
|
|||||||
})}
|
})}
|
||||||
</div>
|
</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-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">
|
<div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.16em] text-zinc-600">
|
||||||
Agent activity
|
Agent activity
|
||||||
|
|||||||
@@ -42,15 +42,11 @@ export function applySessionEventActivity(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (event.kind === 'status') {
|
if (event.kind === 'status') {
|
||||||
if (event.status === 'running' || event.status === 'idle' || event.status === 'error') {
|
if (event.status === 'running') {
|
||||||
return removeSessionActivity(current, event.sessionId);
|
return removeSessionActivity(current, event.sessionId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.kind === 'error') {
|
|
||||||
return removeSessionActivity(current, event.sessionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return current;
|
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 = {
|
const current: SessionActivityMap = {
|
||||||
'session-1': {
|
'session-1': {
|
||||||
architect: {
|
architect: {
|
||||||
@@ -111,17 +111,36 @@ describe('session activity helpers', () => {
|
|||||||
).toEqual({
|
).toEqual({
|
||||||
'session-2': current['session-2'],
|
'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(
|
expect(
|
||||||
applySessionEventActivity(current, {
|
applySessionEventActivity(current, {
|
||||||
sessionId: 'session-2',
|
sessionId: 'session-1',
|
||||||
kind: 'error',
|
kind: 'status',
|
||||||
occurredAt: '2026-03-23T00:00:00.000Z',
|
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',
|
error: 'Boom',
|
||||||
}),
|
}),
|
||||||
).toEqual({
|
).toEqual(current);
|
||||||
'session-1': current['session-1'],
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('builds rows for all agents with sensible defaults', () => {
|
test('builds rows for all agents with sensible defaults', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user