Skip to content

Commit b55d622

Browse files
tamirdresherCopilot
andcommitted
feat: add Squad.Agents.AI sample app demonstrating DI, keyed DI, BYOK, and streaming
- samples/squad-agents-ai-sample/Program.cs: four runnable flows Flow 1 -- AddSquadAgent + RunAsync (basic DI) Flow 2 -- AddKeyedSquadAgent x2 + GetRequiredKeyedService<SquadAgent> Flow 3 -- ConfigureCopilotClient delegate (BYOK token + env var injection) Flow 4 -- RunStreamingAsync with await foreach token-by-token output - samples/squad-agents-ai-sample/Squad.Agents.AI.Sample.csproj: net10.0, project reference to src/Squad.Agents.AI, Microsoft.Extensions.Hosting 10.0.0 - samples/squad-agents-ai-sample/README.md: prerequisites, run commands, per-flow walkthrough, troubleshooting table - .github/workflows/squad-agents-ai-ci.yml: adds paths trigger and restore + build steps for the sample (no run step -- requires live CLI) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 4ac667c commit b55d622

4 files changed

Lines changed: 477 additions & 0 deletions

File tree

‎.github/workflows/squad-agents-ai-ci.yml‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ on:
55
paths:
66
- 'src/Squad.Agents.AI/**'
77
- 'test/Squad.Agents.AI.Tests/**'
8+
- 'samples/squad-agents-ai-sample/**'
89
- '.github/workflows/squad-agents-ai-ci.yml'
910
- 'Directory.*.props'
1011
- 'Directory.*.json'
@@ -15,6 +16,7 @@ on:
1516
paths:
1617
- 'src/Squad.Agents.AI/**'
1718
- 'test/Squad.Agents.AI.Tests/**'
19+
- 'samples/squad-agents-ai-sample/**'
1820
- '.github/workflows/squad-agents-ai-ci.yml'
1921
- 'Directory.*.props'
2022
- 'Directory.*.json'
@@ -55,12 +57,18 @@ jobs:
5557
- name: Restore tests
5658
run: dotnet restore test/Squad.Agents.AI.Tests/Squad.Agents.AI.Tests.csproj -p:NuGetAudit=true
5759

60+
- name: Restore sample
61+
run: dotnet restore samples/squad-agents-ai-sample/Squad.Agents.AI.Sample.csproj -p:NuGetAudit=true
62+
5863
- name: Build package
5964
run: dotnet build src/Squad.Agents.AI/Squad.Agents.AI.csproj --no-restore -c Release
6065

6166
- name: Build tests
6267
run: dotnet build test/Squad.Agents.AI.Tests/Squad.Agents.AI.Tests.csproj --no-restore -c Release
6368

69+
- name: Build sample
70+
run: dotnet build samples/squad-agents-ai-sample/Squad.Agents.AI.Sample.csproj --no-restore -c Release
71+
6472
- name: Test
6573
run: dotnet test test/Squad.Agents.AI.Tests/Squad.Agents.AI.Tests.csproj --no-build -c Release --logger trx --results-directory TestResults
6674

Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
1+
// ============================================================
2+
// Squad.Agents.AI — Sample application
3+
//
4+
// Demonstrates four distinct integration patterns:
5+
// Flow 1 — Basic DI registration and a single prompt
6+
// Flow 2 — Keyed DI with two agents under different service keys
7+
// Flow 3 — BYOK / ConfigureCopilotClient delegate
8+
// Flow 4 — Streaming via RunStreamingAsync
9+
//
10+
// Run: dotnet run --project samples/squad-agents-ai-sample/
11+
// Run one flow: dotnet run --project samples/squad-agents-ai-sample/ -- --flow=1
12+
// ============================================================
13+
14+
using Microsoft.Agents.AI;
15+
using Microsoft.Extensions.DependencyInjection;
16+
using Microsoft.Extensions.Hosting;
17+
using Microsoft.Extensions.Logging;
18+
using Squad.Agents.AI;
19+
20+
// ── Argument parsing ─────────────────────────────────────────────────────────
21+
int? selectedFlow = null;
22+
foreach (var arg in args)
23+
{
24+
if (arg.StartsWith("--flow=", StringComparison.OrdinalIgnoreCase) &&
25+
int.TryParse(arg["--flow=".Length..], out var n) &&
26+
n is >= 1 and <= 4)
27+
{
28+
selectedFlow = n;
29+
}
30+
}
31+
32+
bool RunFlow(int flow) => selectedFlow is null || selectedFlow == flow;
33+
34+
// Squad team root: read from env var or fall back to current directory.
35+
// Real-world usage: set SQUAD_TEAM_ROOT to the path of an initialized Squad team.
36+
var teamRoot = System.Environment.GetEnvironmentVariable("SQUAD_TEAM_ROOT")
37+
?? System.IO.Directory.GetCurrentDirectory();
38+
39+
PrintBanner($"Squad.Agents.AI v0.1 — sample run (team root: {teamRoot})");
40+
Console.WriteLine();
41+
42+
// ─────────────────────────────────────────────────────────────────────────────
43+
// Flow 1: Basic DI registration — simplest path to running an agent
44+
// ─────────────────────────────────────────────────────────────────────────────
45+
if (RunFlow(1))
46+
{
47+
PrintHeader("Flow 1 — Basic DI registration");
48+
49+
var host1 = Host.CreateApplicationBuilder(args);
50+
host1.Logging.SetMinimumLevel(LogLevel.Warning); // keep sample output clean
51+
host1.Services.AddSquadAgent(o =>
52+
{
53+
o.SquadFolderPath = teamRoot;
54+
o.AgentName = "SampleSquad";
55+
o.Instructions = "You are a helpful assistant. Respond concisely.";
56+
});
57+
58+
using var app1 = host1.Build();
59+
60+
var agent1 = app1.Services.GetRequiredService<SquadAgent>();
61+
62+
Console.WriteLine($"Agent name : {agent1.Name}");
63+
Console.WriteLine("Sending : \"What is 2 + 2?\"");
64+
65+
var result1 = await RunWithErrorHandlingAsync(async () =>
66+
{
67+
var session1 = await agent1.CreateSessionAsync();
68+
return await agent1.RunAsync("What is 2 + 2?", session1);
69+
});
70+
71+
if (result1 is not null)
72+
Console.WriteLine($"Response : {result1.Text}");
73+
74+
await DisposeIfNeeded(agent1);
75+
PrintDone("Flow 1");
76+
}
77+
78+
// ─────────────────────────────────────────────────────────────────────────────
79+
// Flow 2: Keyed DI — two agents registered under distinct service keys
80+
// ─────────────────────────────────────────────────────────────────────────────
81+
if (RunFlow(2))
82+
{
83+
PrintHeader("Flow 2 — Keyed DI (two agents)");
84+
85+
var host2 = Host.CreateApplicationBuilder(args);
86+
host2.Logging.SetMinimumLevel(LogLevel.Warning);
87+
88+
// "alpha" agent — focuses on concise answers
89+
host2.Services.AddKeyedSquadAgent("alpha", o =>
90+
{
91+
o.SquadFolderPath = teamRoot;
92+
o.AgentName = "AlphaSquad";
93+
o.Instructions = "Be concise. Answer in one sentence.";
94+
});
95+
96+
// "beta" agent — verbose mode
97+
host2.Services.AddKeyedSquadAgent("beta", o =>
98+
{
99+
o.SquadFolderPath = teamRoot;
100+
o.AgentName = "BetaSquad";
101+
o.Instructions = "Provide a detailed explanation with examples.";
102+
});
103+
104+
using var app2 = host2.Build();
105+
106+
var alphaAgent = app2.Services.GetRequiredKeyedService<SquadAgent>("alpha");
107+
var betaAgent = app2.Services.GetRequiredKeyedService<SquadAgent>("beta");
108+
109+
Console.WriteLine($"[alpha] name: {alphaAgent.Name}");
110+
Console.WriteLine($"[beta] name: {betaAgent.Name}");
111+
Console.WriteLine();
112+
113+
var alphaResult = await RunWithErrorHandlingAsync(async () =>
114+
{
115+
var s = await alphaAgent.CreateSessionAsync();
116+
return await alphaAgent.RunAsync("Explain the concept of dependency injection.", s);
117+
});
118+
if (alphaResult is not null)
119+
Console.WriteLine($"[alpha] {alphaResult.Text}");
120+
121+
Console.WriteLine();
122+
123+
var betaResult = await RunWithErrorHandlingAsync(async () =>
124+
{
125+
var s = await betaAgent.CreateSessionAsync();
126+
return await betaAgent.RunAsync("Explain the concept of dependency injection.", s);
127+
});
128+
if (betaResult is not null)
129+
Console.WriteLine($"[beta] {betaResult.Text}");
130+
131+
await DisposeIfNeeded(alphaAgent);
132+
await DisposeIfNeeded(betaAgent);
133+
PrintDone("Flow 2");
134+
}
135+
136+
// ─────────────────────────────────────────────────────────────────────────────
137+
// Flow 3: BYOK / ConfigureCopilotClient delegate
138+
//
139+
// ConfigureCopilotClient lets you post-process CopilotClientOptions after
140+
// Squad applies its standard values. The routing gate ensures Cwd/CliPath/
141+
// CliArgs cannot be replaced by accident; everything else is fair game.
142+
// ─────────────────────────────────────────────────────────────────────────────
143+
if (RunFlow(3))
144+
{
145+
PrintHeader("Flow 3 — BYOK / ConfigureCopilotClient delegate");
146+
147+
// Simulate a token from a credential store (never hardcode real tokens).
148+
// In production replace this with Key Vault, managed identity, etc.
149+
const string simulatedToken = "ghp_EXAMPLE_REPLACE_WITH_REAL_TOKEN";
150+
151+
var host3 = Host.CreateApplicationBuilder(args);
152+
host3.Logging.SetMinimumLevel(LogLevel.Warning);
153+
host3.Services.AddSquadAgent(o =>
154+
{
155+
o.SquadFolderPath = teamRoot;
156+
o.AgentName = "BYOKSquad";
157+
158+
// ConfigureCopilotClient is the BYOK extension point.
159+
// It runs after Squad populates its own values; use it to inject a
160+
// custom token or environment variable from your own credential store.
161+
o.ConfigureCopilotClient = clientOpts =>
162+
{
163+
// Inject a token from an external credential store
164+
clientOpts.GitHubToken = simulatedToken;
165+
166+
// Inject custom process-level variables by assigning a new dictionary.
167+
// IReadOnlyDictionary indexer is read-only; always assign a new instance.
168+
clientOpts.Environment = new Dictionary<string, string>
169+
{
170+
["SQUAD_SAMPLE_VAR"] = "demo-value"
171+
};
172+
};
173+
});
174+
175+
using var app3 = host3.Build();
176+
177+
var agent3 = app3.Services.GetRequiredService<SquadAgent>();
178+
Console.WriteLine($"Agent name : {agent3.Name}");
179+
Console.WriteLine("BYOK delegate registered — custom token + env var will be forwarded to Copilot SDK.");
180+
Console.WriteLine("Sending : \"Ping\"");
181+
182+
var result3 = await RunWithErrorHandlingAsync(async () =>
183+
{
184+
var s = await agent3.CreateSessionAsync();
185+
return await agent3.RunAsync("Ping", s);
186+
});
187+
if (result3 is not null)
188+
Console.WriteLine($"Response : {result3.Text}");
189+
190+
await DisposeIfNeeded(agent3);
191+
PrintDone("Flow 3");
192+
}
193+
194+
// ─────────────────────────────────────────────────────────────────────────────
195+
// Flow 4: Streaming — print each response token as it arrives
196+
// ─────────────────────────────────────────────────────────────────────────────
197+
if (RunFlow(4))
198+
{
199+
PrintHeader("Flow 4 — Streaming (RunStreamingAsync)");
200+
201+
var host4 = Host.CreateApplicationBuilder(args);
202+
host4.Logging.SetMinimumLevel(LogLevel.Warning);
203+
host4.Services.AddSquadAgent(o =>
204+
{
205+
o.SquadFolderPath = teamRoot;
206+
o.AgentName = "StreamSquad";
207+
});
208+
209+
using var app4 = host4.Build();
210+
211+
var agent4 = app4.Services.GetRequiredService<SquadAgent>();
212+
Console.WriteLine($"Agent name : {agent4.Name}");
213+
Console.WriteLine("Streaming : \"Count to five slowly.\"");
214+
Console.WriteLine("Tokens :");
215+
Console.Write(" ");
216+
217+
var streamOk = await RunStreamingWithErrorHandlingAsync(async () =>
218+
{
219+
var s = await agent4.CreateSessionAsync();
220+
await foreach (var update in agent4.RunStreamingAsync("Count to five slowly.", s))
221+
{
222+
var text = update.Text;
223+
if (!string.IsNullOrEmpty(text))
224+
Console.Write(text);
225+
}
226+
});
227+
228+
if (streamOk)
229+
Console.WriteLine(); // newline after streaming tokens
230+
231+
await DisposeIfNeeded(agent4);
232+
PrintDone("Flow 4");
233+
}
234+
235+
Console.WriteLine();
236+
PrintBanner("All requested flows completed.");
237+
238+
// ─────────────────────────────────────────────────────────────────────────────
239+
// Helpers
240+
// ─────────────────────────────────────────────────────────────────────────────
241+
242+
static void PrintBanner(string text)
243+
{
244+
var line = new string('=', Math.Max(50, text.Length + 4));
245+
Console.ForegroundColor = ConsoleColor.Cyan;
246+
Console.WriteLine(line);
247+
Console.WriteLine($" {text}");
248+
Console.WriteLine(line);
249+
Console.ResetColor();
250+
}
251+
252+
static void PrintHeader(string title)
253+
{
254+
Console.WriteLine();
255+
Console.ForegroundColor = ConsoleColor.Yellow;
256+
Console.WriteLine($"── {title} ──");
257+
Console.ResetColor();
258+
}
259+
260+
static void PrintDone(string flow)
261+
{
262+
Console.ForegroundColor = ConsoleColor.Green;
263+
Console.WriteLine($"[{flow} ✓]");
264+
Console.ResetColor();
265+
}
266+
267+
static async Task<AgentResponse?> RunWithErrorHandlingAsync(Func<Task<AgentResponse>> action)
268+
{
269+
try
270+
{
271+
return await action();
272+
}
273+
catch (Exception ex) when (IsCliMissingException(ex))
274+
{
275+
PrintCliError();
276+
return null;
277+
}
278+
catch (Exception ex)
279+
{
280+
Console.ForegroundColor = ConsoleColor.Red;
281+
Console.WriteLine($"[ERROR] {ex.GetType().Name}: {ex.Message}");
282+
Console.ResetColor();
283+
return null;
284+
}
285+
}
286+
287+
static async Task<bool> RunStreamingWithErrorHandlingAsync(Func<Task> action)
288+
{
289+
try
290+
{
291+
await action();
292+
return true;
293+
}
294+
catch (Exception ex) when (IsCliMissingException(ex))
295+
{
296+
PrintCliError();
297+
return false;
298+
}
299+
catch (Exception ex)
300+
{
301+
Console.ForegroundColor = ConsoleColor.Red;
302+
Console.WriteLine($"\n[ERROR] {ex.GetType().Name}: {ex.Message}");
303+
Console.ResetColor();
304+
return false;
305+
}
306+
}
307+
308+
static bool IsCliMissingException(Exception ex)
309+
{
310+
var msg = ex.Message;
311+
return msg.Contains("copilot", StringComparison.OrdinalIgnoreCase) ||
312+
msg.Contains("not found", StringComparison.OrdinalIgnoreCase) ||
313+
msg.Contains("cannot find", StringComparison.OrdinalIgnoreCase) ||
314+
msg.Contains("No such file", StringComparison.OrdinalIgnoreCase) ||
315+
ex is System.ComponentModel.Win32Exception;
316+
}
317+
318+
static void PrintCliError()
319+
{
320+
Console.ForegroundColor = ConsoleColor.Red;
321+
Console.WriteLine();
322+
Console.WriteLine("┌─────────────────────────────────────────────────────────┐");
323+
Console.WriteLine("│ GitHub Copilot CLI was not found on PATH │");
324+
Console.WriteLine("│ │");
325+
Console.WriteLine("│ Install it and sign in before running this sample: │");
326+
Console.WriteLine("│ https://github.com/github/copilot-cli │");
327+
Console.WriteLine("│ │");
328+
Console.WriteLine("│ See the sample README.md for full prerequisites. │");
329+
Console.WriteLine("└─────────────────────────────────────────────────────────┘");
330+
Console.ResetColor();
331+
}
332+
333+
static async ValueTask DisposeIfNeeded(SquadAgent agent)
334+
{
335+
if (agent is IAsyncDisposable d)
336+
await d.DisposeAsync();
337+
}

0 commit comments

Comments
 (0)