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>
This commit is contained in:
David Kaya
2026-04-07 19:14:56 +02:00
co-authored by Copilot
parent 794794afe4
commit 7a7b3fcdca
@@ -38,7 +38,6 @@ internal sealed class CopilotAgentBundle : IAsyncDisposable
{ {
List<IAsyncDisposable> disposables = []; List<IAsyncDisposable> disposables = [];
List<AIAgent> agents = []; List<AIAgent> agents = [];
CopilotClientOptions clientOptions = CopilotCliPathResolver.CreateClientOptions();
ResolvedHookSet configuredHooks = await HookConfigLoader.LoadAsync(command.ProjectPath, cancellationToken) ResolvedHookSet configuredHooks = await HookConfigLoader.LoadAsync(command.ProjectPath, cancellationToken)
.ConfigureAwait(false); .ConfigureAwait(false);
IHookCommandRunner hookCommandRunner = HookCommandRunner.Instance; IHookCommandRunner hookCommandRunner = HookCommandRunner.Instance;
@@ -52,12 +51,17 @@ internal sealed class CopilotAgentBundle : IAsyncDisposable
disposables.Add(toolingBundle); disposables.Add(toolingBundle);
} }
IReadOnlyList<WorkflowNodeDto> agentNodes = command.Workflow.GetAllAgentNodes(command.WorkflowLibrary);
if (agentNodes.Count > 0)
{
CopilotClientOptions clientOptions = CopilotCliPathResolver.CreateClientOptions();
// Share a single CopilotClient across all agents to avoid spawning // Share a single CopilotClient across all agents to avoid spawning
// multiple CLI processes that race on token refresh during auto-login. // multiple CLI processes that race on token refresh during auto-login.
CopilotClient sharedClient = new(clientOptions); CopilotClient sharedClient = new(clientOptions);
await sharedClient.StartAsync(cancellationToken).ConfigureAwait(false); await sharedClient.StartAsync(cancellationToken).ConfigureAwait(false);
IReadOnlyList<WorkflowNodeDto> agentNodes = command.Workflow.GetAllAgentNodes(command.WorkflowLibrary);
foreach ((WorkflowNodeDto definition, int agentIndex) in agentNodes.Select((definition, index) => (definition, index))) foreach ((WorkflowNodeDto definition, int agentIndex) in agentNodes.Select((definition, index) => (definition, index)))
{ {
SessionConfig sessionConfig = CreateSessionConfig( SessionConfig sessionConfig = CreateSessionConfig(
@@ -87,6 +91,7 @@ internal sealed class CopilotAgentBundle : IAsyncDisposable
// The bundle owns the shared client — disposed after all agents. // The bundle owns the shared client — disposed after all agents.
disposables.Add(sharedClient); disposables.Add(sharedClient);
}
CopilotAgentBundle bundle = new(agents, hasConfiguredHooks: !configuredHooks.IsEmpty); CopilotAgentBundle bundle = new(agents, hasConfiguredHooks: !configuredHooks.IsEmpty);
bundle._disposables.AddRange(disposables); bundle._disposables.AddRange(disposables);