mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-06 19:58:43 +02:00
fix: suppress noisy hook lifecycle events
Stop forwarding hook lifecycle events when a project has no configured .github/hooks/*.json commands so the UI only receives meaningful hook activity. Also expose the configured-hook state on the bundle, cover the suppression wiring with tests, and document the updated event semantics. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
+1
-1
@@ -206,7 +206,7 @@ The protocol also carries **turn-scoped lifecycle events** alongside output delt
|
|||||||
|
|
||||||
- **Sub-agent events**: started, completed, failed, selected, deselected — surfaced when custom agents are defined
|
- **Sub-agent events**: started, completed, failed, selected, deselected — surfaced when custom agents are defined
|
||||||
- **Skill invocation events**: emitted when an agent-side skill is triggered
|
- **Skill invocation events**: emitted when an agent-side skill is triggered
|
||||||
- **Hook lifecycle events**: start and end of registered hooks, including Aryx-managed SDK hooks and project hook files discovered from `.github/hooks/*.json`
|
- **Hook lifecycle events**: start and end of configured project hook commands discovered from `.github/hooks/*.json`; Aryx suppresses the SDK's built-in no-op hook chatter so the UI only sees meaningful hook activity
|
||||||
- **Session compaction events**: start and complete, with token-reduction metrics when infinite sessions trigger context trimming
|
- **Session compaction events**: start and complete, with token-reduction metrics when infinite sessions trigger context trimming
|
||||||
- **Session usage events**: current token count and context-window limit for usage-bar rendering
|
- **Session usage events**: current token count and context-window limit for usage-bar rendering
|
||||||
- **Pending-messages-modified events**: emitted when mid-turn steering changes the pending message queue
|
- **Pending-messages-modified events**: emitted when mid-turn steering changes the pending message queue
|
||||||
|
|||||||
@@ -13,13 +13,16 @@ internal sealed class CopilotAgentBundle : IAsyncDisposable
|
|||||||
{
|
{
|
||||||
private readonly List<IAsyncDisposable> _disposables = [];
|
private readonly List<IAsyncDisposable> _disposables = [];
|
||||||
|
|
||||||
private CopilotAgentBundle(IReadOnlyList<AIAgent> agents)
|
internal CopilotAgentBundle(IReadOnlyList<AIAgent> agents, bool hasConfiguredHooks)
|
||||||
{
|
{
|
||||||
Agents = agents;
|
Agents = agents;
|
||||||
|
HasConfiguredHooks = hasConfiguredHooks;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IReadOnlyList<AIAgent> Agents { get; }
|
public IReadOnlyList<AIAgent> Agents { get; }
|
||||||
|
|
||||||
|
public bool HasConfiguredHooks { get; }
|
||||||
|
|
||||||
public static async Task<CopilotAgentBundle> CreateAsync(
|
public static async Task<CopilotAgentBundle> CreateAsync(
|
||||||
RunTurnCommandDto command,
|
RunTurnCommandDto command,
|
||||||
Func<PatternAgentDefinitionDto, PermissionRequest, PermissionInvocation, Task<PermissionRequestResult>> onPermissionRequest,
|
Func<PatternAgentDefinitionDto, PermissionRequest, PermissionInvocation, Task<PermissionRequestResult>> onPermissionRequest,
|
||||||
@@ -72,7 +75,7 @@ internal sealed class CopilotAgentBundle : IAsyncDisposable
|
|||||||
disposables.Add(agent);
|
disposables.Add(agent);
|
||||||
}
|
}
|
||||||
|
|
||||||
CopilotAgentBundle bundle = new(agents);
|
CopilotAgentBundle bundle = new(agents, hasConfiguredHooks: !configuredHooks.IsEmpty);
|
||||||
bundle._disposables.AddRange(disposables);
|
bundle._disposables.AddRange(disposables);
|
||||||
return bundle;
|
return bundle;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ internal sealed class CopilotTurnExecutionState
|
|||||||
|
|
||||||
public bool HasPendingExitPlanModeRequest { get; private set; }
|
public bool HasPendingExitPlanModeRequest { get; private set; }
|
||||||
|
|
||||||
|
public bool SuppressHookLifecycleEvents { get; set; }
|
||||||
|
|
||||||
public async Task EmitThinkingIfNeeded(
|
public async Task EmitThinkingIfNeeded(
|
||||||
AgentIdentity agent,
|
AgentIdentity agent,
|
||||||
Func<SidecarEventDto, Task> onEvent)
|
Func<SidecarEventDto, Task> onEvent)
|
||||||
@@ -113,11 +115,17 @@ internal sealed class CopilotTurnExecutionState
|
|||||||
break;
|
break;
|
||||||
case HookStartEvent hookStart:
|
case HookStartEvent hookStart:
|
||||||
ActiveAgent = agent;
|
ActiveAgent = agent;
|
||||||
_pendingEvents.Enqueue(CreateHookLifecycleEvent(agent, "start", hookStart.Data));
|
if (!SuppressHookLifecycleEvents)
|
||||||
|
{
|
||||||
|
_pendingEvents.Enqueue(CreateHookLifecycleEvent(agent, "start", hookStart.Data));
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case HookEndEvent hookEnd:
|
case HookEndEvent hookEnd:
|
||||||
ActiveAgent = agent;
|
ActiveAgent = agent;
|
||||||
_pendingEvents.Enqueue(CreateHookLifecycleEvent(agent, "end", hookEnd.Data));
|
if (!SuppressHookLifecycleEvents)
|
||||||
|
{
|
||||||
|
_pendingEvents.Enqueue(CreateHookLifecycleEvent(agent, "end", hookEnd.Data));
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case SessionUsageInfoEvent usageInfo:
|
case SessionUsageInfoEvent usageInfo:
|
||||||
ActiveAgent = agent;
|
ActiveAgent = agent;
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ public sealed class CopilotWorkflowRunner : ITurnWorkflowRunner
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
runCancellation.Token);
|
runCancellation.Token);
|
||||||
|
ConfigureHookLifecycleEventSuppression(state, bundle);
|
||||||
Workflow workflow = bundle.BuildWorkflow(command.Pattern);
|
Workflow workflow = bundle.BuildWorkflow(command.Pattern);
|
||||||
List<ChatMessage> inputMessages = command.Messages.Select(WorkflowTranscriptProjector.ToChatMessage).ToList();
|
List<ChatMessage> inputMessages = command.Messages.Select(WorkflowTranscriptProjector.ToChatMessage).ToList();
|
||||||
WorkflowTranscriptProjector.AttachMessageMode(inputMessages, command.MessageMode);
|
WorkflowTranscriptProjector.AttachMessageMode(inputMessages, command.MessageMode);
|
||||||
@@ -118,6 +119,16 @@ public sealed class CopilotWorkflowRunner : ITurnWorkflowRunner
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static void ConfigureHookLifecycleEventSuppression(
|
||||||
|
CopilotTurnExecutionState state,
|
||||||
|
CopilotAgentBundle bundle)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(state);
|
||||||
|
ArgumentNullException.ThrowIfNull(bundle);
|
||||||
|
|
||||||
|
state.SuppressHookLifecycleEvents = !bundle.HasConfiguredHooks;
|
||||||
|
}
|
||||||
|
|
||||||
private static async Task EmitPendingEventsAsync(
|
private static async Task EmitPendingEventsAsync(
|
||||||
CopilotTurnExecutionState state,
|
CopilotTurnExecutionState state,
|
||||||
Func<SidecarEventDto, Task> onEvent)
|
Func<SidecarEventDto, Task> onEvent)
|
||||||
|
|||||||
@@ -53,6 +53,15 @@ public sealed class CopilotAgentBundleTests
|
|||||||
Assert.Equal(["glob", "view"], sessionConfig.AvailableTools);
|
Assert.Equal(["glob", "view"], sessionConfig.AvailableTools);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Constructor_StoresWhetherHooksAreConfigured()
|
||||||
|
{
|
||||||
|
CopilotAgentBundle bundle = new([], hasConfiguredHooks: true);
|
||||||
|
|
||||||
|
Assert.True(bundle.HasConfiguredHooks);
|
||||||
|
Assert.Empty(bundle.Agents);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task CreateConfiguredSessionConfig_MergesInstructionsAndConvertsHandoffDeclarations()
|
public async Task CreateConfiguredSessionConfig_MergesInstructionsAndConvertsHandoffDeclarations()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -212,23 +212,7 @@ public sealed class CopilotTurnExecutionStateTests
|
|||||||
RunTurnCommandDto command = CreateCommand();
|
RunTurnCommandDto command = CreateCommand();
|
||||||
CopilotTurnExecutionState state = new(command);
|
CopilotTurnExecutionState state = new(command);
|
||||||
|
|
||||||
state.ObserveSessionEvent(
|
state.ObserveSessionEvent(command.Pattern.Agents[0], CreateHookStartEvent());
|
||||||
command.Pattern.Agents[0],
|
|
||||||
SessionEvent.FromJson(
|
|
||||||
"""
|
|
||||||
{
|
|
||||||
"type": "hook.start",
|
|
||||||
"data": {
|
|
||||||
"hookInvocationId": "hook-1",
|
|
||||||
"hookType": "postToolUse",
|
|
||||||
"input": {
|
|
||||||
"toolName": "view"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"id": "66666666-6666-6666-6666-666666666666",
|
|
||||||
"timestamp": "2026-03-27T00:00:00Z"
|
|
||||||
}
|
|
||||||
"""));
|
|
||||||
|
|
||||||
HookLifecycleEventDto evt = Assert.Single(state.DrainPendingEvents().OfType<HookLifecycleEventDto>());
|
HookLifecycleEventDto evt = Assert.Single(state.DrainPendingEvents().OfType<HookLifecycleEventDto>());
|
||||||
Assert.Equal("start", evt.Phase);
|
Assert.Equal("start", evt.Phase);
|
||||||
@@ -237,6 +221,36 @@ public sealed class CopilotTurnExecutionStateTests
|
|||||||
Assert.NotNull(evt.Input);
|
Assert.NotNull(evt.Input);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ObserveSessionEvent_HookEnd_QueuesHookLifecycleEvent()
|
||||||
|
{
|
||||||
|
RunTurnCommandDto command = CreateCommand();
|
||||||
|
CopilotTurnExecutionState state = new(command);
|
||||||
|
|
||||||
|
state.ObserveSessionEvent(command.Pattern.Agents[0], CreateHookEndEvent());
|
||||||
|
|
||||||
|
HookLifecycleEventDto evt = Assert.Single(state.DrainPendingEvents().OfType<HookLifecycleEventDto>());
|
||||||
|
Assert.Equal("end", evt.Phase);
|
||||||
|
Assert.Equal("postToolUse", evt.HookType);
|
||||||
|
Assert.Equal("hook-1", evt.HookInvocationId);
|
||||||
|
Assert.True(evt.Success);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ObserveSessionEvent_HookLifecycleEvents_AreSuppressedWhenConfigured()
|
||||||
|
{
|
||||||
|
RunTurnCommandDto command = CreateCommand();
|
||||||
|
CopilotTurnExecutionState state = new(command)
|
||||||
|
{
|
||||||
|
SuppressHookLifecycleEvents = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
state.ObserveSessionEvent(command.Pattern.Agents[0], CreateHookStartEvent());
|
||||||
|
state.ObserveSessionEvent(command.Pattern.Agents[0], CreateHookEndEvent());
|
||||||
|
|
||||||
|
Assert.Empty(state.DrainPendingEvents());
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ObserveSessionEvent_SessionCompactionComplete_QueuesCompactionEvent()
|
public void ObserveSessionEvent_SessionCompactionComplete_QueuesCompactionEvent()
|
||||||
{
|
{
|
||||||
@@ -295,6 +309,45 @@ public sealed class CopilotTurnExecutionStateTests
|
|||||||
Assert.Equal("agent-1", evt.AgentId);
|
Assert.Equal("agent-1", evt.AgentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static SessionEvent CreateHookStartEvent()
|
||||||
|
{
|
||||||
|
return SessionEvent.FromJson(
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"type": "hook.start",
|
||||||
|
"data": {
|
||||||
|
"hookInvocationId": "hook-1",
|
||||||
|
"hookType": "postToolUse",
|
||||||
|
"input": {
|
||||||
|
"toolName": "view"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": "66666666-6666-6666-6666-666666666666",
|
||||||
|
"timestamp": "2026-03-27T00:00:00Z"
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SessionEvent CreateHookEndEvent()
|
||||||
|
{
|
||||||
|
return SessionEvent.FromJson(
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"type": "hook.end",
|
||||||
|
"data": {
|
||||||
|
"hookInvocationId": "hook-1",
|
||||||
|
"hookType": "postToolUse",
|
||||||
|
"success": true,
|
||||||
|
"output": {
|
||||||
|
"status": "ok"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": "99999999-9999-9999-9999-999999999999",
|
||||||
|
"timestamp": "2026-03-27T00:00:00Z"
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
|
||||||
private static RunTurnCommandDto CreateCommand()
|
private static RunTurnCommandDto CreateCommand()
|
||||||
{
|
{
|
||||||
return new RunTurnCommandDto
|
return new RunTurnCommandDto
|
||||||
|
|||||||
@@ -11,6 +11,18 @@ namespace Aryx.AgentHost.Tests;
|
|||||||
|
|
||||||
public sealed class CopilotWorkflowRunnerTests
|
public sealed class CopilotWorkflowRunnerTests
|
||||||
{
|
{
|
||||||
|
[Fact]
|
||||||
|
public void ConfigureHookLifecycleEventSuppression_SetsStateFromBundle()
|
||||||
|
{
|
||||||
|
RunTurnCommandDto command = CreateApprovalCommand();
|
||||||
|
CopilotTurnExecutionState state = new(command);
|
||||||
|
CopilotAgentBundle bundle = new(Array.Empty<AIAgent>(), hasConfiguredHooks: false);
|
||||||
|
|
||||||
|
CopilotWorkflowRunner.ConfigureHookLifecycleEventSuppression(state, bundle);
|
||||||
|
|
||||||
|
Assert.True(state.SuppressHookLifecycleEvents);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void SelectNewOutputMessages_SkipsFullTranscriptPrefix()
|
public void SelectNewOutputMessages_SkipsFullTranscriptPrefix()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user