diff --git a/src/Squad.Agents.AI/README.md b/src/Squad.Agents.AI/README.md index a6dff3e0c..485695b26 100644 --- a/src/Squad.Agents.AI/README.md +++ b/src/Squad.Agents.AI/README.md @@ -152,15 +152,17 @@ Parsed URI query keys: `teamRoot`, `cliPath`, `cwd`, `cliArgs` (semicolon-separa ## Coordinator agent selection -`SquadAgent` exists to wrap a Squad coordinator team, so the SDK sets `SessionConfig.Agent` to `"squad"` by default. That's the SDK-native equivalent of running `copilot --agent squad` from a terminal — it tells the wrapped Copilot session to load `.github/agents/squad.agent.md` as the agent definition, which is what teaches the coordinator to eager-execute, fan out, and dispatch through the `task` tool. Without it, the wrapped session uses its built-in generic agent and the coordinator role-plays responses inline instead of spawning real subagents — producing SDK behavior that does NOT match running `copilot --agent squad` interactively against the same team root. +`SquadAgent` exists to wrap a Squad coordinator team, so it passes `--agent squad` to the underlying `copilot.exe` child process by default. That tells the CLI to load `.github/agents/squad.agent.md` as the agent definition — which is what teaches the coordinator to eager-execute, fan out, and dispatch through the `task` tool. Without it, the CLI uses its built-in generic agent and the coordinator role-plays responses inline instead of spawning real subagents — producing SDK behavior that does NOT match running `copilot --agent squad` interactively against the same team root. | Scenario | What the SDK does | |---|---| -| Default — `squad.agent.md` exists | Sets `sessionConfig.Agent = "squad"` | -| `squad.agent.md` not found at `.github/agents/` | Leaves `sessionConfig.Agent` unset (graceful degradation for not-yet-initialized teams) | -| Consumer sets `AgentFileName = "data"` (and file exists) | Sets `sessionConfig.Agent = "data"` | -| Consumer sets `AgentFileName = null` | Leaves `sessionConfig.Agent` unset | -| Consumer overrides `sessionConfig.Agent` inside `ConfigureSession` | The `ConfigureSession` callback runs after the default is applied, so it always wins | +| Default — `squad.agent.md` exists | Auto-adds `--agent squad` | +| `squad.agent.md` not found at `.github/agents/` | Silently skips `--agent` (graceful degradation for not-yet-initialized teams; logs a Debug message) | +| Consumer sets `AgentFileName = "data"` (and file exists) | Auto-adds `--agent data` | +| Consumer sets `AgentFileName = null` | Skips `--agent` entirely | +| Consumer already supplied `--agent X` in `CliArgs` | Respects the explicit value; does not add a second `--agent` | + +> SDK note: `SessionConfigBase.Agent` exists but looks up the name in the SDK's `CustomAgents` registry, NOT in `.github/agents/*.agent.md` files on disk. The CLI `--agent` flag is currently the only path that reads the on-disk agent definition. ## Subagent observability — first-class OpenTelemetry @@ -208,7 +210,7 @@ builder.Services.AddSquadAgent(o => | `ConfigureCopilotClient` | Advanced delegate for customizing `CopilotClientOptions`. Routing properties are guarded; see BYOK section. | | `TraceEvents` | Enables verbose SDK logging and emits a startup warning when enabled. | | `AgentName` | Display name for the resulting `AIAgent`; defaults to `Squad`. | -| `AgentFileName` | Name of the agent definition under `.github/agents/{name}.agent.md` to load via `SessionConfig.Agent` (the SDK equivalent of the Copilot CLI's `--agent` flag). Defaults to `"squad"`. Set to `null` to skip auto-selection; the file-existence check makes this safe to leave on for not-yet-initialized teams. Override `sessionConfig.Agent` from inside `ConfigureSession` for full control. | +| `AgentFileName` | Name of the agent definition under `.github/agents/{name}.agent.md` to load via the CLI's `--agent` flag. Defaults to `"squad"`. Set to `null` to skip auto-injection; the file-existence check makes this safe to leave on for not-yet-initialized teams. | | `Instructions` | Optional system instructions passed to the inner Copilot agent. | | `EmitSubagentActivities` | Whether the SDK opens an OpenTelemetry `Activity` per subagent dispatch and annotates lifecycle events. Defaults to `true`. Set to `false` to handle telemetry from your own `OnSubagentTrace` callback. | | `OnSubagentTrace` | Optional callback invoked for every typed `SquadAgentTraceEvent`. Independent of `EmitSubagentActivities` — both can be on simultaneously. | diff --git a/src/Squad.Agents.AI/Squad.Agents.AI.csproj b/src/Squad.Agents.AI/Squad.Agents.AI.csproj index e2187fc5b..3657a6800 100644 --- a/src/Squad.Agents.AI/Squad.Agents.AI.csproj +++ b/src/Squad.Agents.AI/Squad.Agents.AI.csproj @@ -17,7 +17,7 @@ MIT README.md true - 0.5.0 + 0.5.1 diff --git a/src/Squad.Agents.AI/SquadAgent.cs b/src/Squad.Agents.AI/SquadAgent.cs index afdca9653..3511f54c8 100644 --- a/src/Squad.Agents.AI/SquadAgent.cs +++ b/src/Squad.Agents.AI/SquadAgent.cs @@ -134,36 +134,6 @@ private static SquadAgentState BuildStateInternal(SquadAgentOptions options, ILo ConfigDirectory = squadConfigDir, EnableConfigDiscovery = true, }; - - // ── Default coordinator agent selection ──────────────────────────── - // SquadAgent wraps a Squad coordinator team. The SDK's SessionConfig.Agent - // property is the first-class equivalent of the Copilot CLI's --agent flag — - // it selects which discovered agent definition (e.g. .github/agents/squad.agent.md) - // to load as the session's system prompt. Without it the CLI uses its built-in - // generic agent and the coordinator role-plays responses inline instead of - // dispatching real subagents — exactly the inconsistency between - // `copilot --agent squad` (CLI) and SquadAgent.RunAsync (SDK) that this - // default eliminates. - // - // We set Agent = AgentFileName (default "squad") unless: - // 1. The host explicitly configured a different Agent via ConfigureSession - // (handled implicitly — ConfigureSession runs after us and wins) - // 2. AgentFileName is null or whitespace (explicit opt-out) - // 3. The agent file does not exist at the conventional path on disk - // (graceful degradation for folders that are not yet Squad-initialized, - // where the CLI would error on `--agent squad`) - // ─────────────────────────────────────────────────────────────────── - if (!string.IsNullOrWhiteSpace(options.AgentFileName) && !string.IsNullOrWhiteSpace(teamRoot)) - { - var agentFilePath = Path.Combine(teamRoot, ".github", "agents", $"{options.AgentFileName}.agent.md"); - if (File.Exists(agentFilePath)) - { - sessionConfig.Agent = options.AgentFileName; - } - // If the file is missing we leave SessionConfig.Agent unset; the SDK will - // start with its default agent, which is what 0.4.x did anyway. - } - if (!string.IsNullOrEmpty(options.Instructions)) { sessionConfig.SystemMessage = new SystemMessageConfig { Content = options.Instructions }; @@ -273,6 +243,51 @@ private static CopilotClient CreateCopilotClient(SquadAgentOptions options, ILog combinedCliArgs.Add("--allow-all"); } + // ── Default coordinator agent selection ──────────────────────────── + // SquadAgent wraps a Squad coordinator team. The CLI's `--agent squad` + // flag is what teaches the coordinator to eager-execute, fan out, and + // dispatch via the task tool by loading `.github/agents/squad.agent.md` + // as the agent definition. Without it the CLI uses its built-in generic + // agent and the coordinator role-plays responses inline instead of + // spawning real subagents — producing SDK behaviour that does NOT match + // running `copilot --agent squad` interactively against the same team + // root. + // + // Note on SessionConfig.Agent: that SDK property exists but selects + // from the SDK's CustomAgents registry, NOT from `.github/agents/*.agent.md` + // files (verified at v1.0.0 — setting it without populating CustomAgents + // produces `Custom agent 'squad' not found`). The CLI `--agent` flag is + // currently the only path that reads the on-disk agent definition. + // + // We auto-add `--agent {AgentFileName}` (default "squad") unless: + // 1. The host already supplied --agent explicitly in CliArgs + // 2. AgentFileName is null or whitespace (explicit opt-out) + // 3. The agent file does not exist at the conventional path on disk + // (graceful degradation for folders that are not yet Squad-initialized, + // where the CLI would error on `--agent squad`) + // ─────────────────────────────────────────────────────────────────── + bool hostSuppliedAgent = options.CliArgs.Any(a => + string.Equals(a, "--agent", StringComparison.OrdinalIgnoreCase)); + if (!hostSuppliedAgent && !string.IsNullOrWhiteSpace(options.AgentFileName)) + { + var defaultAgentTeamRoot = options.Cwd ?? options.SquadFolderPath; + if (!string.IsNullOrWhiteSpace(defaultAgentTeamRoot)) + { + var agentFilePath = Path.Combine(defaultAgentTeamRoot, ".github", "agents", $"{options.AgentFileName}.agent.md"); + if (File.Exists(agentFilePath)) + { + combinedCliArgs.Add("--agent"); + combinedCliArgs.Add(options.AgentFileName); + } + else + { + logger?.LogDebug( + "SquadAgentOptions.AgentFileName is '{AgentFileName}' but the file was not found at '{AgentFilePath}'. Skipping --agent argument; the CLI will fall back to its default agent.", + options.AgentFileName, agentFilePath); + } + } + } + combinedCliArgs.AddRange(options.CliArgs); // Only override the SDK's default child-process connection when the consumer diff --git a/src/Squad.Agents.AI/SquadAgentOptions.cs b/src/Squad.Agents.AI/SquadAgentOptions.cs index a696c1174..f9d0ab619 100644 --- a/src/Squad.Agents.AI/SquadAgentOptions.cs +++ b/src/Squad.Agents.AI/SquadAgentOptions.cs @@ -83,31 +83,30 @@ public sealed class SquadAgentOptions public string AgentName { get; set; } = "Squad"; /// - /// Gets or sets the Squad coordinator agent definition file (under .github/agents/) to - /// load as the wrapped session's agent. Defaults to "squad", which selects - /// .github/agents/squad.agent.md — the Squad coordinator system prompt that drives - /// eager execution, parallel fan-out, and dispatch through the task tool. + /// Gets or sets the Copilot CLI agent definition file (under .github/agents/) the + /// underlying copilot.exe child process should load via the CLI's --agent + /// flag. Defaults to "squad", which selects .github/agents/squad.agent.md + /// — the Squad coordinator system prompt that drives eager execution, parallel fan-out, + /// and dispatch through the task tool. /// /// /// - /// The whole reason exists is to wrap a Squad coordinator team, so - /// the SDK sets by default. Without it, - /// the wrapped session uses its built-in generic agent and the coordinator role-plays - /// responses inline instead of dispatching real subagents — exactly the inconsistency - /// between copilot --agent squad (CLI) and SquadAgent.RunAsync (SDK) that this - /// default eliminates. + /// The whole reason exists is to wrap a Squad coordinator team, + /// so the SDK passes --agent squad by default. Without it the CLI uses its built-in + /// generic agent and the coordinator role-plays responses inline instead of dispatching + /// real subagents — exactly the inconsistency between copilot --agent squad (CLI) + /// and SquadAgent.RunAsync (SDK) that this default eliminates. /// /// - /// When the named file does not exist at - /// {team-root}/.github/agents/{AgentFileName}.agent.md (e.g. the team root is not - /// Squad-initialized yet), the SDK silently leaves - /// unset so the session can still start with the SDK's default agent. + /// When the named file does not exist at {team-root}/.github/agents/{AgentFileName}.agent.md + /// (e.g. the team root is not Squad-initialized yet), the SDK silently skips the + /// --agent argument so the CLI can still start with its default agent. /// /// - /// Set to (or whitespace) to disable the auto-selection entirely. Set - /// to a different name (e.g. "data") to load a custom agent file. To override the - /// final value, set sessionConfig.Agent from inside — - /// that callback runs after this default is applied. + /// Set to (or whitespace) to disable the auto-inject entirely. Set + /// to a different name (e.g. "data") to load a custom agent file. If the consumer + /// already supplied --agent in , that explicit value wins and + /// the default is not added. /// /// public string? AgentFileName { get; set; } = "squad"; diff --git a/test/Squad.Agents.AI.Tests/SquadAgentDefaultAgentFlagTests.cs b/test/Squad.Agents.AI.Tests/SquadAgentDefaultAgentFlagTests.cs new file mode 100644 index 000000000..e335a1066 --- /dev/null +++ b/test/Squad.Agents.AI.Tests/SquadAgentDefaultAgentFlagTests.cs @@ -0,0 +1,220 @@ +using System.Reflection; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Squad.Agents.AI.Tests; + +/// +/// Tests for the 0.5.x behaviour where SquadAgent auto-injects +/// --agent {AgentFileName} into the underlying copilot.exe CLI args, so +/// the wrapped agent picks up .github/agents/squad.agent.md by default +/// — matching copilot --agent squad from a terminal. +/// +/// +/// Note: an earlier 0.5.0 attempt set SessionConfig.Agent instead, which +/// turned out to look up the name in the SDK's CustomAgents registry +/// rather than from .github/agents/*.agent.md on disk (produces +/// Custom agent 'squad' not found). The CLI's --agent flag is +/// currently the only path that reads the on-disk agent definition, so 0.5.1 +/// reverts to the CLI-args approach. +/// +public class SquadAgentDefaultAgentFlagTests : IDisposable +{ + private readonly string _tempRoot; + + public SquadAgentDefaultAgentFlagTests() + { + // Each test gets its own throwaway team root so file-existence checks + // are deterministic and we can scaffold or omit the squad.agent.md file + // per scenario without contaminating siblings. + _tempRoot = Path.Combine(Path.GetTempPath(), "squad-agents-ai-tests", Guid.NewGuid().ToString("n")); + Directory.CreateDirectory(_tempRoot); + } + + public void Dispose() + { + try { Directory.Delete(_tempRoot, recursive: true); } + catch { /* best effort */ } + } + + private string ScaffoldAgentFile(string agentName) + { + var dir = Path.Combine(_tempRoot, ".github", "agents"); + Directory.CreateDirectory(dir); + var path = Path.Combine(dir, $"{agentName}.agent.md"); + File.WriteAllText(path, $"# {agentName}\n\nstub agent file for tests"); + return path; + } + + [Fact] + public void Options_AgentFileName_DefaultsToSquad() + { + var options = new SquadAgentOptions(); + Assert.Equal("squad", options.AgentFileName); + } + + [Fact] + public void AddSquadAgent_AutoInjectsAgentSquad_WhenSquadAgentMdExists() + { + ScaffoldAgentFile("squad"); + + var agent = CreateAgent(opts => + { + opts.SquadFolderPath = _tempRoot; + opts.Cwd = _tempRoot; + }); + + var args = GetConnectionArgs(agent); + AssertContainsAgent(args, "squad"); + } + + [Fact] + public void AddSquadAgent_DoesNotInjectAgent_WhenAgentFileMissing() + { + // No .github/agents/squad.agent.md scaffolded — the SDK should NOT + // pass --agent squad because the CLI would error on a missing file. + var agent = CreateAgent(opts => + { + opts.SquadFolderPath = _tempRoot; + opts.Cwd = _tempRoot; + }); + + var args = GetConnectionArgs(agent); + Assert.DoesNotContain("--agent", args); + } + + [Fact] + public void AddSquadAgent_RespectsExplicitAgentInCliArgs() + { + // Scaffold the default file so we'd auto-inject if the consumer hadn't + // already supplied --agent. + ScaffoldAgentFile("squad"); + ScaffoldAgentFile("data"); + + var agent = CreateAgent(opts => + { + opts.SquadFolderPath = _tempRoot; + opts.Cwd = _tempRoot; + opts.CliArgs.Add("--agent"); + opts.CliArgs.Add("data"); + }); + + var args = GetConnectionArgs(agent); + + // Exactly ONE --agent in the args, and it's "data" (the user-supplied one). + var agentIndices = args.Select((a, i) => (a, i)) + .Where(t => string.Equals(t.a, "--agent", StringComparison.OrdinalIgnoreCase)) + .Select(t => t.i) + .ToList(); + Assert.Single(agentIndices); + Assert.Equal("data", args[agentIndices[0] + 1]); + } + + [Fact] + public void AddSquadAgent_RespectsCustomAgentFileName_WhenFileExists() + { + ScaffoldAgentFile("data"); + + var agent = CreateAgent(opts => + { + opts.SquadFolderPath = _tempRoot; + opts.Cwd = _tempRoot; + opts.AgentFileName = "data"; + }); + + var args = GetConnectionArgs(agent); + AssertContainsAgent(args, "data"); + } + + [Fact] + public void AddSquadAgent_DoesNotInjectAgent_WhenAgentFileNameIsNull() + { + // Opt-out: setting AgentFileName=null disables the auto-inject even if + // squad.agent.md exists. Used when the consumer wants to drive the + // session with a custom SystemMessage instead of a charter file. + ScaffoldAgentFile("squad"); + + var agent = CreateAgent(opts => + { + opts.SquadFolderPath = _tempRoot; + opts.Cwd = _tempRoot; + opts.AgentFileName = null; + }); + + var args = GetConnectionArgs(agent); + Assert.DoesNotContain("--agent", args); + } + + [Fact] + public void AddSquadAgent_DoesNotInjectAgent_WhenAgentFileNameIsWhitespace() + { + ScaffoldAgentFile("squad"); + + var agent = CreateAgent(opts => + { + opts.SquadFolderPath = _tempRoot; + opts.Cwd = _tempRoot; + opts.AgentFileName = " "; + }); + + var args = GetConnectionArgs(agent); + Assert.DoesNotContain("--agent", args); + } + + // ── helpers ───────────────────────────────────────────────────────────── + + private static SquadAgent CreateAgent(Action configure) + { + var services = new ServiceCollection(); + services.AddSingleton(new ConfigurationBuilder().Build()); + services.AddLogging(); + services.AddSquadAgent(opts => + { + opts.CliPath = @"C:\fake-copilot\copilot.exe"; + configure(opts); + }); + return services.BuildServiceProvider().GetRequiredService(); + } + + private static IList GetConnectionArgs(SquadAgent agent) + { + var client = GetField(agent, "_copilotClient"); + var clientOptions = GetField(client, "_options"); + var connection = GetProperty(clientOptions, "Connection"); + Assert.NotNull(connection); + var args = GetProperty>(connection!, "Args"); + Assert.NotNull(args); + return args!; + } + + private static void AssertContainsAgent(IList args, string expectedName) + { + // CLI args must be a `--agent ` adjacent pair, not just two unrelated tokens. + for (var i = 0; i < args.Count - 1; i++) + { + if (string.Equals(args[i], "--agent", StringComparison.OrdinalIgnoreCase) && + string.Equals(args[i + 1], expectedName, StringComparison.Ordinal)) + { + return; + } + } + Assert.Fail($"Expected `--agent {expectedName}` in CLI args; got: [{string.Join(", ", args)}]"); + } + + private static T GetField(object instance, string fieldName) + { + var f = instance.GetType().GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); + Assert.NotNull(f); + var v = f!.GetValue(instance); + Assert.NotNull(v); + return (T)v!; + } + + private static T? GetProperty(object instance, string propertyName) + { + var p = instance.GetType().GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + Assert.NotNull(p); + return (T?)p!.GetValue(instance); + } +} diff --git a/test/Squad.Agents.AI.Tests/SquadAgentDefaultAgentTests.cs b/test/Squad.Agents.AI.Tests/SquadAgentDefaultAgentTests.cs deleted file mode 100644 index 92469a7ed..000000000 --- a/test/Squad.Agents.AI.Tests/SquadAgentDefaultAgentTests.cs +++ /dev/null @@ -1,194 +0,0 @@ - using System.Reflection; -using GitHub.Copilot; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Xunit; - -namespace Squad.Agents.AI.Tests; - -/// -/// Tests for the 0.5.0 behaviour where SquadAgent auto-sets -/// (the SDK's first-class equivalent of -/// the Copilot CLI's --agent flag) to , -/// so the wrapped session picks up .github/agents/squad.agent.md by default — -/// matching copilot --agent squad from the terminal. -/// -public class SquadAgentDefaultAgentTests : IDisposable -{ - private readonly string _tempRoot; - - public SquadAgentDefaultAgentTests() - { - // Each test gets its own throwaway team root so file-existence checks - // are deterministic and we can scaffold or omit the squad.agent.md file - // per scenario without contaminating siblings. - _tempRoot = Path.Combine(Path.GetTempPath(), "squad-agents-ai-tests", Guid.NewGuid().ToString("n")); - Directory.CreateDirectory(_tempRoot); - } - - public void Dispose() - { - try { Directory.Delete(_tempRoot, recursive: true); } - catch { /* best effort */ } - } - - private string ScaffoldAgentFile(string agentName) - { - var dir = Path.Combine(_tempRoot, ".github", "agents"); - Directory.CreateDirectory(dir); - var path = Path.Combine(dir, $"{agentName}.agent.md"); - File.WriteAllText(path, $"# {agentName}\n\nstub agent file for tests"); - return path; - } - - [Fact] - public void Options_AgentFileName_DefaultsToSquad() - { - var options = new SquadAgentOptions(); - Assert.Equal("squad", options.AgentFileName); - } - - [Fact] - public void AddSquadAgent_AutoSetsSessionConfigAgentToSquad_WhenSquadAgentMdExists() - { - ScaffoldAgentFile("squad"); - - var agent = CreateAgent(opts => - { - opts.SquadFolderPath = _tempRoot; - opts.Cwd = _tempRoot; - }); - - var sessionConfig = GetInnerSessionConfig(agent); - Assert.Equal("squad", sessionConfig.Agent); - } - - [Fact] - public void AddSquadAgent_LeavesAgentUnset_WhenAgentFileMissing() - { - // No .github/agents/squad.agent.md scaffolded — the SDK should NOT - // set sessionConfig.Agent because the underlying session would error - // when asked to load a non-existent agent. - var agent = CreateAgent(opts => - { - opts.SquadFolderPath = _tempRoot; - opts.Cwd = _tempRoot; - }); - - var sessionConfig = GetInnerSessionConfig(agent); - Assert.Null(sessionConfig.Agent); - } - - [Fact] - public void AddSquadAgent_RespectsCustomAgentFileName_WhenFileExists() - { - ScaffoldAgentFile("data"); - - var agent = CreateAgent(opts => - { - opts.SquadFolderPath = _tempRoot; - opts.Cwd = _tempRoot; - opts.AgentFileName = "data"; - }); - - var sessionConfig = GetInnerSessionConfig(agent); - Assert.Equal("data", sessionConfig.Agent); - } - - [Fact] - public void AddSquadAgent_LeavesAgentUnset_WhenAgentFileNameIsNull() - { - // Opt-out: setting AgentFileName=null disables the auto-set even if - // squad.agent.md exists. Used when the consumer wants to drive the - // session with a custom SystemMessage instead of an agent file. - ScaffoldAgentFile("squad"); - - var agent = CreateAgent(opts => - { - opts.SquadFolderPath = _tempRoot; - opts.Cwd = _tempRoot; - opts.AgentFileName = null; - }); - - var sessionConfig = GetInnerSessionConfig(agent); - Assert.Null(sessionConfig.Agent); - } - - [Fact] - public void AddSquadAgent_LeavesAgentUnset_WhenAgentFileNameIsWhitespace() - { - ScaffoldAgentFile("squad"); - - var agent = CreateAgent(opts => - { - opts.SquadFolderPath = _tempRoot; - opts.Cwd = _tempRoot; - opts.AgentFileName = " "; - }); - - var sessionConfig = GetInnerSessionConfig(agent); - Assert.Null(sessionConfig.Agent); - } - - [Fact] - public void AddSquadAgent_ConfigureSession_CanOverrideAutoSetAgent() - { - // ConfigureSession runs AFTER the default is applied, so consumers retain - // full control — they can replace the auto-set value with anything else - // (or null it out) from inside their own callback. - ScaffoldAgentFile("squad"); - ScaffoldAgentFile("custom"); - - var agent = CreateAgent(opts => - { - opts.SquadFolderPath = _tempRoot; - opts.Cwd = _tempRoot; - opts.ConfigureSession = sessionConfig => - { - // Confirm the default was applied before this callback ran. - Assert.Equal("squad", sessionConfig.Agent); - sessionConfig.Agent = "custom"; - }; - }); - - var actualSessionConfig = GetInnerSessionConfig(agent); - Assert.Equal("custom", actualSessionConfig.Agent); - } - - // ── helpers ───────────────────────────────────────────────────────────── - - private static SquadAgent CreateAgent(Action configure) - { - var services = new ServiceCollection(); - services.AddSingleton(new ConfigurationBuilder().Build()); - services.AddLogging(); - services.AddSquadAgent(opts => - { - opts.CliPath = @"C:\fake-copilot\copilot.exe"; - configure(opts); - }); - return services.BuildServiceProvider().GetRequiredService(); - } - - /// - /// Reach into the SquadAgent → DelegatingAIAgent.InnerAgent → ChatClientAgent.SessionConfig - /// to assert against the SessionConfig the SDK was constructed with. - /// - private static SessionConfig GetInnerSessionConfig(SquadAgent agent) - { - var innerProp = typeof(Microsoft.Agents.AI.DelegatingAIAgent).GetProperty( - "InnerAgent", - BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); - Assert.NotNull(innerProp); - var inner = innerProp!.GetValue(agent); - Assert.NotNull(inner); - - var configField = inner!.GetType().GetField( - "_sessionConfig", - BindingFlags.Instance | BindingFlags.NonPublic); - Assert.NotNull(configField); - var config = configField!.GetValue(inner); - Assert.NotNull(config); - return (SessionConfig)config!; - } -}