From 7a7b3fcdca580c9ca32c863c09eeaee45ef2bab1 Mon Sep 17 00:00:00 2001 From: David Kaya Date: Tue, 7 Apr 2026 19:14:56 +0200 Subject: [PATCH] fix: defer Copilot CLI resolution until agent nodes exist CopilotAgentBundle.CreateAsync unconditionally resolved the Copilot CLI path and started a CopilotClient, even for workflows with no agent nodes (e.g. request-port-only workflows). This caused a hard failure in CI where the copilot binary is not on PATH. Move CLI path resolution, client creation, and agent setup inside a guard that checks whether the workflow actually contains agent nodes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Services/CopilotAgentBundle.cs | 65 ++++++++++--------- 1 file changed, 35 insertions(+), 30 deletions(-) diff --git a/sidecar/src/Aryx.AgentHost/Services/CopilotAgentBundle.cs b/sidecar/src/Aryx.AgentHost/Services/CopilotAgentBundle.cs index c64a1c1..70c261b 100644 --- a/sidecar/src/Aryx.AgentHost/Services/CopilotAgentBundle.cs +++ b/sidecar/src/Aryx.AgentHost/Services/CopilotAgentBundle.cs @@ -38,7 +38,6 @@ internal sealed class CopilotAgentBundle : IAsyncDisposable { List disposables = []; List agents = []; - CopilotClientOptions clientOptions = CopilotCliPathResolver.CreateClientOptions(); ResolvedHookSet configuredHooks = await HookConfigLoader.LoadAsync(command.ProjectPath, cancellationToken) .ConfigureAwait(false); IHookCommandRunner hookCommandRunner = HookCommandRunner.Instance; @@ -52,42 +51,48 @@ internal sealed class CopilotAgentBundle : IAsyncDisposable disposables.Add(toolingBundle); } - // Share a single CopilotClient across all agents to avoid spawning - // multiple CLI processes that race on token refresh during auto-login. - CopilotClient sharedClient = new(clientOptions); - await sharedClient.StartAsync(cancellationToken).ConfigureAwait(false); - IReadOnlyList agentNodes = command.Workflow.GetAllAgentNodes(command.WorkflowLibrary); - foreach ((WorkflowNodeDto definition, int agentIndex) in agentNodes.Select((definition, index) => (definition, index))) + + if (agentNodes.Count > 0) { - SessionConfig sessionConfig = CreateSessionConfig( - command, - definition, - agentIndex, - (request, invocation) => onPermissionRequest(definition, request, invocation), - (request, invocation) => onUserInputRequest(definition, request, invocation), - evt => onSessionEvent?.Invoke(definition, evt), - configuredHooks, - hookCommandRunner); + CopilotClientOptions clientOptions = CopilotCliPathResolver.CreateClientOptions(); - ApplySessionTooling(sessionConfig, toolingBundle?.McpServers, toolingBundle?.Tools); - ApplyPromptInvocation(sessionConfig, command.PromptInvocation); + // Share a single CopilotClient across all agents to avoid spawning + // multiple CLI processes that race on token refresh during auto-login. + CopilotClient sharedClient = new(clientOptions); + await sharedClient.StartAsync(cancellationToken).ConfigureAwait(false); - AryxCopilotAgent agent = new( - sharedClient, - sessionConfig, - ownsClient: false, - id: definition.GetAgentId(), - name: definition.GetAgentName(), - description: NormalizeOptionalString(definition.Config.Description)); + foreach ((WorkflowNodeDto definition, int agentIndex) in agentNodes.Select((definition, index) => (definition, index))) + { + SessionConfig sessionConfig = CreateSessionConfig( + command, + definition, + agentIndex, + (request, invocation) => onPermissionRequest(definition, request, invocation), + (request, invocation) => onUserInputRequest(definition, request, invocation), + evt => onSessionEvent?.Invoke(definition, evt), + configuredHooks, + hookCommandRunner); - agents.Add(agent); - disposables.Add(agent); + ApplySessionTooling(sessionConfig, toolingBundle?.McpServers, toolingBundle?.Tools); + ApplyPromptInvocation(sessionConfig, command.PromptInvocation); + + AryxCopilotAgent agent = new( + sharedClient, + sessionConfig, + ownsClient: false, + id: definition.GetAgentId(), + name: definition.GetAgentName(), + description: NormalizeOptionalString(definition.Config.Description)); + + agents.Add(agent); + disposables.Add(agent); + } + + // The bundle owns the shared client — disposed after all agents. + disposables.Add(sharedClient); } - // The bundle owns the shared client — disposed after all agents. - disposables.Add(sharedClient); - CopilotAgentBundle bundle = new(agents, hasConfiguredHooks: !configuredHooks.IsEmpty); bundle._disposables.AddRange(disposables); return bundle;