Skip to content

feat: add CommunityToolkit.Aspire.Hosting.Squad - first-class Aspire resource for Squad AI-agent teams#1394

Open
tamirdresher wants to merge 13 commits into
CommunityToolkit:mainfrom
tamirdresher:feat/hosting-squad
Open

feat: add CommunityToolkit.Aspire.Hosting.Squad - first-class Aspire resource for Squad AI-agent teams#1394
tamirdresher wants to merge 13 commits into
CommunityToolkit:mainfrom
tamirdresher:feat/hosting-squad

Conversation

@tamirdresher

@tamirdresher tamirdresher commented Jun 10, 2026

Copy link
Copy Markdown

What

Adds a CommunityToolkit.Aspire.Hosting.Squad integration that lets a .NET Aspire AppHost model a Squad AI-agent team as a first-class Aspire resource.

This PR was proposed in #1393.

Why

Squad is a multi-agent CLI orchestration framework whose teams live under a .squad/ workspace folder. Today there's no idiomatic way to surface a Squad team in the Aspire dashboard or pass team identity into downstream service projects. This integration provides one — a logical Resource that auto-discovers the roster, exposes it as dashboard metadata, and implements IResourceWithConnectionString so service projects can .WithReference(squad).

The companion package Squad.Agents.AI (Microsoft Agent Framework adapter, shipped from the upstream Squad repo, OIDC-published from the Squad nuget.org org) is what consuming services use to orchestrate the referenced team via an MAF AIAgent.

What's in this PR

Integration (src/)

Item Status
SquadResource ( Resource, IResourceWithConnectionString )
AddSquad(name, teamRoot) extension on Aspire.Hosting namespace
SquadLifecycleHook — auto-discovers roster from .squad/team.md (table + bullet format), filters to agents with a charter.md
Connection-string descriptor: squad://resource/{name}?teamRoot={path}&agents={csv}&protocol=maf-1.0
[AspireExport] on the public extension method (ASPIREEXPORT008 clean)
In-package README.md for the NuGet description
InternalsVisibleTo for the test project
Full XML docs on every public API

Tests (tests/)

7 xUnit tests in CommunityToolkit.Aspire.Hosting.Squad.Testsall passing on net10.0 (MTP via the test exe; dotnet test hits the documented VSTest deprecation noise).

Example (examples/squad/)

The example is now an end-to-end demonstration of the full Squad-on-Aspire story, not just a "does it build" smoke test:

  • AppHost wires two real Squad teams as Aspire resources via AddSquad:
    • research-squad — AI/ML research team cast from The Matrix (Morpheus, Trinity, Oracle, Tank)
    • dev-squad — full-stack development team cast from The Simpsons (Lisa, Marge, Frink, Comic Book Guy)
    • Both squads are fully-initialized Squad teams (.squad/team.md, per-agent charters, casting registry, etc.) committed to the repo so the example is self-contained — anyone cloning gets a working multi-team setup with zero squad init ceremony.
  • ApiApp consumes both squads via .WithReference(researchSquad).WithReference(devSquad) and exposes a single POST /ask?squad=research|dev endpoint. The Aspire-injected connection strings are picked up automatically (Squad.Agents.AI 0.4.0+ looks up ConnectionStrings:{name} directly — no manual parsing needed).
  • ServiceDefaults with the standard OTLP exporter wiring.
  • Squad.slnx focused solution file at the repo root so F5 / VS solution explorer doesn't load the whole Aspire repo.

Observability surface

Visible in the Aspire dashboard with zero consumer wiring beyond AddSource:

Activity source What you see
Microsoft.Agents.AI.Squad (emitted by Squad.Agents.AI 0.4.0+) One squad.subagent {Name} span per task-tool dispatch, with squad.subagent.start / squad.subagent.message / squad.subagent.completed / squad.subagent.failed ActivityEvents annotated on the span timeline. Plus tags: squad.subagent.name, squad.subagent.display_name, squad.subagent.sdk_agent_id, squad.subagent.reply_preview.
Squad.Hosting.ApiApp (this app) One squad.ask {squad} wrapper span per request — gives the trace tree a clear named root.
Structured Logs (category Squad.Subagent.*) Per-subagent >> start / << done / msg from {Name} log lines via ILogger.

Sample prompts menu

GET / returns a copy-paste menu of prompts that demonstrate each of Squad's coordinator-mode behaviors (Direct, Lightweight, Full), so the demo flow is obvious without reading the source.

Real-world driver

This integration drove three small but high-impact improvements upstream in Squad.Agents.AI, all merged and shipped to NuGet in the past 24h while this PR was open:

  • bradygaster/squad#1271Squad.Agents.AI 0.4.0: default-on OpenTelemetry spans + Aspire-style connection-string lookup (consumers no longer need to wire OnSubagentTrace just to get spans, and AddKeyedSquadAgent("research-squad") resolves ConnectionStrings:research-squad directly).
  • bradygaster/squad#1275Squad.Agents.AI 0.5.0: auto-inject the coordinator agent definition so the wrapped session behaves the same as copilot --agent squad from a terminal.
  • bradygaster/squad#1277Squad.Agents.AI 0.5.1: fix the 0.5.0 SDK-property approach (turned out the SDK property looks up an in-memory registry, not on-disk agent files).

This PR consumes Squad.Agents.AI 0.5.1-preview.8 (the latest shipped preview).

Local verification

All on Windows / .NET 10.0.301 SDK / Aspire 13.4 (centrally pinned):

dotnet build src/CommunityToolkit.Aspire.Hosting.Squad/         -c Release   →  0 warn, 0 err
dotnet build tests/CommunityToolkit.Aspire.Hosting.Squad.Tests/ -c Release   →  0 warn, 0 err
dotnet build examples/squad/.../AppHost.csproj                  -c Debug     →  0 warn, 0 err

tests/CommunityToolkit.Aspire.Hosting.Squad.Tests.exe (MTP on net10.0):
  total: 7   passed: 7   failed: 0   skipped: 0

End-to-end against the live AppHost:

  • Both research-squad and dev-squad show up as Active in the dashboard with their rosters discovered.
  • POST /ask?squad=dev with the dispatch_full_mode_dev sample prompt dispatched real subagents — Lisa (Tech Lead) and Frink (Backend) both replied in their distinct specialist voices (Frink's reply ended in "glavin!" — pure Professor Frink), confirming the task-tool dispatch path through --agent squad is wired correctly.
  • squad.subagent {Lisa} and squad.subagent {Frink} spans appeared in the dashboard Traces view with the full set of timeline ActivityEvents.

Tutorial / docs

Updated walkthrough including the Aspire flow: https://gist.github.com/tamirdresher/d0e38cbadd962de18e8373706eccad97

(docs/create-integration.md says full docs go in a separate PR to microsoft/aspire.dev. Happy to open that simultaneously, or wait for this to land first — let me know your preference.)

PR checklist

  • Created a feature branch in my fork (feat/hosting-squad)
  • Based off latest main (just rebased — 0 commits behind, 12 ahead)
  • No merge commits (rebased)
  • New integration — docs are written (NuGet README + tutorial gist; full aspire.dev page is a follow-up per docs/create-integration.md)
  • Added description of major feature to project description for NuGet package
  • Tests for the changes have been added (7 xUnit, all passing)
  • Contains NO breaking changes
  • Every new API has full XML docs
  • Code follows all style conventions
  • Allow edits by maintainers checked

Closes #1393

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

@github-actions

github-actions Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/CommunityToolkit/Aspire/main/eng/scripts/dogfood-pr.sh | bash -s -- 1394

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/CommunityToolkit/Aspire/main/eng/scripts/dogfood-pr.ps1) } 1394"

tamirdresher pushed a commit to tamirdresher/Aspire-1 that referenced this pull request Jun 10, 2026
The original example AppHost only registered a SquadResource. Per the PR
review request from CommunityToolkit#1394, the example now also demonstrates end-to-end
consumption: a downstream ApiApp project receives the squad://...
connection string via .WithReference(squad), parses the team root, and
uses Squad.Agents.AI to drive a real 3-turn conversation against the
referenced team.

What's added
------------
- examples/squad/CommunityToolkit.Aspire.Hosting.Squad.ApiApp/
    - ASP.NET minimal API; Program.cs reads ConnectionStrings:research-squad
      (squad://... format), extracts teamRoot, registers SquadAgent via
      AddSquadAgent, exposes POST /ask that runs a 3-turn conversation
      through AgentSession (demonstrates session memory across turns).
- examples/squad/CommunityToolkit.Aspire.Hosting.Squad.ServiceDefaults/
    - Standard Aspire ServiceDefaults shape (OTel + service discovery +
      health checks), mirroring the Java example layout.
- examples/squad/CommunityToolkit.Aspire.Hosting.Squad.AppHost/
    - Updated to AddProject<...>("squad-api").WithReference(researchSquad)
- Directory.Packages.props
    - Added Squad.Agents.AI 0.1.0-preview.3 (centralized version), the
      preview that ships the SessionConfig/OnPermissionRequest fix needed
      to drive a real conversation. See bradygaster/squad#1252.
- CommunityToolkit.Aspire.slnx
    - 2 new projects added (ApiApp, ServiceDefaults).
- src/CommunityToolkit.Aspire.Hosting.Squad/README.md
    - New "Consume the team from a service project" section pointing
      callers at the new examples/squad/ runnable.

Local verification
------------------
- dotnet build (Release) of all 5 squad-related projects: 0 warn, 0 err
- All 7 unit tests still pass on net10.0 (MTP)
- Squad.Agents.AI 0.1.0-preview.3 restores from nuget.org cleanly

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@tamirdresher

Copy link
Copy Markdown
Author

📦 Update — example now demonstrates end-to-end consumption (commit 758b33d7)

The example AppHost now also wires a downstream ApiApp via .WithReference(squad) so reviewers can see the full integration loop, not just the resource registration.

New in examples/squad/:

  • CommunityToolkit.Aspire.Hosting.Squad.ApiApp/ — ASP.NET minimal API
    • Parses the squad://... connection string Aspire injects via WithReference
    • Registers SquadAgent (from Squad.Agents.AI) targeting the resolved team root
    • Exposes POST /ask that runs a 3-turn conversation through AgentSession, demonstrating cross-turn session memory
  • CommunityToolkit.Aspire.Hosting.Squad.ServiceDefaults/ — standard Aspire defaults shape (OTel + service discovery + health checks), mirroring the existing Java example layout
  • AppHost: AddProject<...>("squad-api").WithReference(researchSquad)

Companion fix shipped upstream: while building the example I hit a runtime bug in Squad.Agents.AI 0.1.0-preview.2 (missing OnPermissionRequest handler). Fix shipped to nuget.org as Squad.Agents.AI 0.1.0-preview.3 via bradygaster/squad#1252 (merged + published via OIDC). The version is centralized in Directory.Packages.props.

Verified locally:

  • dotnet build (Release) on all 5 squad-related projects → 0 warn, 0 err
  • 7/7 unit tests still pass on net10.0 (MTP)
  • dotnet run --project examples/squad/.../AppHost boots the dashboard with the wired ApiApp visible

Still draft — happy to flip to ready-for-review once you've had a chance to look. Open questions from the original PR body still apply (example folder layout, simultaneous docs PR, dotnet test → MTP migration).

@tamirdresher tamirdresher marked this pull request as ready for review June 10, 2026 20:44
Copilot AI review requested due to automatic review settings June 10, 2026 20:44
@tamirdresher

Copy link
Copy Markdown
Author

Bumped to Squad.Agents.AI 0.2.0-preview.4 in 29edeca, taking out of draft.

The 0.1.0-preview.3 the original PR pinned to was wedged on GitHub.Copilot.SDK 1.0.0-beta.2, which spoke an older CLI protocol the current Copilot CLI no longer implements — runtime file IO failed and the agent would invent "blocked by content exclusion policy" excuses. 0.2.0-preview.4 (shipped tonight from bradygaster/squad#1259) brings the package onto MAF 1.10.0-rc1 + SDK 1.0.0 GA, and the example ApiApp here now successfully reads the team's .squad/ files and dispatches work to specialist sub-agents end to end (verified locally against a real .squad/-initialised team).

There's one temporary footnote — a direct PackageReference to GitHub.Copilot.SDK 1.0.0 had to be added alongside the Squad nuget. The SDK ships its CLI-binary-download MSBuild targets under build/ (which only auto-imports for direct PackageReferences), not buildTransitive/. microsoft/agent-framework#6457 (merged this evening) ships a buildTransitive bridge in Microsoft.Agents.AI.GitHub.Copilot that makes this automatic — once a MAF preview cuts with that change, the direct SDK ref can be dropped from Directory.Packages.props and the ApiApp csproj. Both edit sites carry an inline comment explaining the lifecycle.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds a new Aspire Community Toolkit integration that models a Squad AI-agent team as a first-class .NET Aspire resource, including dashboard metadata, lifecycle events, and an end-to-end example.

Changes:

  • Introduces SquadResource + AddSquad(...) extension with connection-string support and roster discovery from .squad/team.md.
  • Adds dashboard integration (properties, lifecycle state transitions, and dashboard commands).
  • Adds tests and a runnable example AppHost + ApiApp demonstrating .WithReference(...) consumption.

Reviewed changes

Copilot reviewed 25 out of 25 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
tests/CommunityToolkit.Aspire.Hosting.Squad.Tests/SquadResourceCreationTests.cs New tests for resource registration, roster parsing, and connection string shape.
tests/CommunityToolkit.Aspire.Hosting.Squad.Tests/CommunityToolkit.Aspire.Hosting.Squad.Tests.csproj New test project referencing the Squad integration project.
tests/CommunityToolkit.Aspire.Hosting.Squad.Tests/AssemblyInfo.cs Adds an InternalsVisibleTo attribute (currently applied to the test assembly).
src/CommunityToolkit.Aspire.Hosting.Squad/api/CommunityToolkit.Aspire.Hosting.Squad.cs Public API surface file for SquadResource and AddSquad.
src/CommunityToolkit.Aspire.Hosting.Squad/SquadTeamAnnotation.cs Adds an annotation type to carry .squad metadata paths.
src/CommunityToolkit.Aspire.Hosting.Squad/SquadResource.cs Implements the Aspire resource, agent discovery, and squad:// connection string expression.
src/CommunityToolkit.Aspire.Hosting.Squad/SquadLifecycleHook.cs Adds lifecycle subscriber to publish dashboard state and properties.
src/CommunityToolkit.Aspire.Hosting.Squad/SquadDashboardProperties.cs Generates static + live dashboard property snapshots.
src/CommunityToolkit.Aspire.Hosting.Squad/SquadBuilderExtensions.cs Adds AddSquad extension and dashboard commands (open folder, check inbox, Copilot CLI).
src/CommunityToolkit.Aspire.Hosting.Squad/README.md Package README describing usage and roster discovery behavior.
src/CommunityToolkit.Aspire.Hosting.Squad/CommunityToolkit.Aspire.Hosting.Squad.csproj New integration project file (package metadata + Aspire.Hosting reference).
examples/squad/CommunityToolkit.Aspire.Hosting.Squad.ServiceDefaults/Extensions.cs Example service defaults (telemetry/health/discovery) for the sample.
examples/squad/CommunityToolkit.Aspire.Hosting.Squad.ServiceDefaults/CommunityToolkit.Aspire.Hosting.Squad.ServiceDefaults.csproj Example ServiceDefaults project references.
examples/squad/CommunityToolkit.Aspire.Hosting.Squad.AppHost/sample-squad/.squad/team.md Sample Squad roster fixture for the example AppHost.
examples/squad/CommunityToolkit.Aspire.Hosting.Squad.AppHost/sample-squad/.squad/agents/ralph/charter.md Sample agent fixture.
examples/squad/CommunityToolkit.Aspire.Hosting.Squad.AppHost/sample-squad/.squad/agents/picard/charter.md Sample agent fixture.
examples/squad/CommunityToolkit.Aspire.Hosting.Squad.AppHost/Properties/launchSettings.json Launch settings for running the example AppHost.
examples/squad/CommunityToolkit.Aspire.Hosting.Squad.AppHost/Program.cs Example AppHost wiring AddSquad into a downstream ApiApp using .WithReference(...).
examples/squad/CommunityToolkit.Aspire.Hosting.Squad.AppHost/CommunityToolkit.Aspire.Hosting.Squad.AppHost.csproj Example AppHost project file and copying sample-squad to output.
examples/squad/CommunityToolkit.Aspire.Hosting.Squad.ApiApp/Properties/launchSettings.json Launch settings for the example ApiApp.
examples/squad/CommunityToolkit.Aspire.Hosting.Squad.ApiApp/Program.cs Example ApiApp that parses squad://... connection string and runs a 3-turn conversation.
examples/squad/CommunityToolkit.Aspire.Hosting.Squad.ApiApp/CommunityToolkit.Aspire.Hosting.Squad.ApiApp.csproj Example ApiApp package references (Squad.Agents.AI + GitHub.Copilot.SDK).
README.md Adds the Hosting.Squad entry + link targets to the repo-wide integrations table.
Directory.Packages.props Adds central package versions for Squad.Agents.AI and GitHub.Copilot.SDK.
CommunityToolkit.Aspire.slnx Adds the new integration + tests + example projects to the solution.
Comments suppressed due to low confidence (1)

src/CommunityToolkit.Aspire.Hosting.Squad/SquadBuilderExtensions.cs:1

  • The command is named/descried as “Refresh” and claims it “Re-reads .squad/team.md”, but it never actually reads/parses team.md—it only checks file existence and reports the already-cached resource.Agents. Either (mandatory) change the description/name to reflect the actual behavior (e.g., “Report Agents”), or (optional) implement a real re-read and publish updated dashboard properties/state.
using Aspire.Hosting.ApplicationModel;

Comment on lines +74 to +80
var content = File.ReadAllText(rosterPath);
var agents = AgentTableRowRegex.Matches(content)
.Concat(AgentLineRegex.Matches(content))
.Select(m => NormalizeAgentName(m.Groups[1].Value))
.Where(agent => IsKnownAgent(teamRoot, agent))
.Distinct()
.ToList();

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 097369f. Added explicit .Cast<Match>() on both Regex.Matches(...) results so the LINQ Concat/Select binding is unambiguous — defensive against older TFMs where MatchCollection is only non-generic IEnumerable. No behavior change on net10.0 where it already implements IList<Match>.

Comment on lines +282 to +289
startInfo.ArgumentList.Add("/c");
startInfo.ArgumentList.Add("start");
startInfo.ArgumentList.Add(windowTitle);
startInfo.ArgumentList.Add("powershell.exe");
startInfo.ArgumentList.Add("-NoLogo");
startInfo.ArgumentList.Add("-NoExit");
startInfo.ArgumentList.Add("-Command");
startInfo.ArgumentList.Add(copilotCommand);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 097369f. Dropped cmd /c start entirely. StartConsoleWindow now launches powershell.exe directly with UseShellExecute=true (which opens a new console window) and sets the title from inside the new shell via \System.Management.Automation.Internal.Host.InternalHost.UI.RawUI.WindowTitle = '...'. No more cmd-quote-stripping fragility, and we keep the friendly window title.

{
public static partial class SquadBuilderExtensions
{
public static Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.SquadResource> AddSquad(this Aspire.Hosting.IDistributedApplicationBuilder builder, string name, string teamRoot) { throw null; }

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 097369f. Re-ran -t:GenAPIGenerateReferenceAssemblySource; the regenerated file now correctly declares string? teamRoot = null and also picks up SquadTeamAnnotation, which the hand-typed version had missed.

@@ -0,0 +1 @@
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("CommunityToolkit.Aspire.Hosting.Squad.Tests")]

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 097369f — deleted tests/CommunityToolkit.Aspire.Hosting.Squad.Tests/AssemblyInfo.cs. The production csproj already wires <InternalsVisibleTo Include="...Tests" /> correctly, so the test-side attribute was duplicate dead config.

Comment on lines +105 to +110
private static string CreateEmptyTeamRoot()
{
var dir = Path.Combine(Path.GetTempPath(), "ctk-aspire-squad-tests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(dir);
return dir;
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 097369f. SquadResourceCreationTests now implements IDisposable and tracks every temp dir created by the helpers in an instance-scoped _tempRoots list. Dispose() best-effort deletes them recursively, so test residue no longer accumulates in %TEMP% across runs (including CI agents).

@tamirdresher

Copy link
Copy Markdown
Author

Bumped to Squad.Agents.AI 0.3.0-preview.5 (commit 20b763d) and wired the new subagent-observability surface into the example ApiApp.

New in 0.3.0 (shipped today from bradygaster/squad#1265) — a typed OnSubagentTrace callback + an OpenTelemetry ActivitySource named Microsoft.Agents.AI.Squad that emits one span per task-tool dispatch (when the coordinator spawns specialist sub-agents), tagged with squad.subagent.name / squad.subagent.display_name / squad.subagent.sdk_agent_id / squad.subagent.reply_preview. Hosts that .AddSource(SquadAgentDiagnostics.ActivitySourceName) on their OTel tracer see those spans in their backend — in Aspire's case, the dashboard Traces view.

This commit:

  • Bumps Squad.Agents.AI from 0.2.0-preview.4 to 0.3.0-preview.5 in Directory.Packages.props.
  • One-line wire-up of Squad's ActivitySource into the ApiApp's existing OTel tracer.
  • New POST /dispatch endpoint that fires an explicit task-tool dispatch prompt — hitting it from the dashboard produces a multi-span trace showing coordinator + each subagent as separate spans, which is the headline visibility demo.
  • OnSubagentTrace callback wired in the ApiApp's AddSquadAgent so subagent lifecycle (spawn / reply / complete) also surfaces in the dashboard's Structured Logs view alongside the OTel spans.
  • The existing POST /ask 3-turn endpoint kept for the session-memory demo.

Verified locally: AppHost boots clean, the research-squad resource registers 2 agents (ralph, picard) and transitions Spawning -> Active, the ApiApp builds + runs with the new wiring against Squad.Agents.AI 0.3.0-preview.5 restored from nuget.org.

Copilot and others added 12 commits June 11, 2026 19:31
Introduces a new hosting integration that lets a .NET Aspire AppHost
model a Squad (https://github.com/bradygaster/squad) AI-agent team
as a first-class Aspire resource.

Closes CommunityToolkit#1393

What's included
---------------
- `src/CommunityToolkit.Aspire.Hosting.Squad/` — the package
    - `SquadResource : Resource, IResourceWithConnectionString`
      Auto-discovers the agent roster from `.squad/team.md` (table and
      bullet-list formats), only keeping names that map to an existing
      `.squad/agents/{name}/charter.md` to filter typos. Exposes
      `squad://resource/{name}?teamRoot={...}&agents={csv}&protocol=maf-1.0`
      so downstream services can consume the team via `.WithReference`.
    - `SquadBuilderExtensions.AddSquad(name, teamRoot)` decorated with
      `[AspireExport]` (ATS-compatible).
    - `SquadLifecycleHook` publishes Spawning -> Active transitions
      on the dashboard and seeds `SquadDashboardProperties` so the
      roster, protocol version, and team root show on the resource row.
    - Dashboard commands wired via `WithCommand` (open Copilot CLI on
      the team root, etc.).
    - Public API surface file at
      `src/CommunityToolkit.Aspire.Hosting.Squad/api/CommunityToolkit.Aspire.Hosting.Squad.cs`
      per repo convention.
- `tests/CommunityToolkit.Aspire.Hosting.Squad.Tests/` — 7 xUnit unit
  tests covering: resource registration, default roster fallback,
  guard clauses, table-format roster parsing, bullet-format roster
  parsing, and connection-string shape. All pass locally on net10.0.
- `examples/squad/CommunityToolkit.Aspire.Hosting.Squad.AppHost/` —
  minimal AppHost example with a self-contained `sample-squad/.squad/`
  fixture so the example builds and runs without external state.
- Solution + root README updates (table row, link refs).

Out of scope (planned follow-ups, kept separate for reviewability)
------------------------------------------------------------------
- OpenTelemetry signals (agent spawns, token usage, session metrics).
  Depends on the Squad telemetry contract still being designed at
  bradygaster/squad#1144.
- Direct `Squad.Agents.AI` consumer-side extension that constructs an
  MAF `AIAgent` from a referenced squad. The connection-string shape
  here is the integration point; consumer-side wiring is a separate
  package design.

Local verification
------------------
- `dotnet build` on all three new projects (Release) — 0 warnings, 0 errors
- 7/7 tests pass via Microsoft Testing Platform on net10.0
- Example AppHost builds cleanly

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The original example AppHost only registered a SquadResource. Per the PR
review request from CommunityToolkit#1394, the example now also demonstrates end-to-end
consumption: a downstream ApiApp project receives the squad://...
connection string via .WithReference(squad), parses the team root, and
uses Squad.Agents.AI to drive a real 3-turn conversation against the
referenced team.

What's added
------------
- examples/squad/CommunityToolkit.Aspire.Hosting.Squad.ApiApp/
    - ASP.NET minimal API; Program.cs reads ConnectionStrings:research-squad
      (squad://... format), extracts teamRoot, registers SquadAgent via
      AddSquadAgent, exposes POST /ask that runs a 3-turn conversation
      through AgentSession (demonstrates session memory across turns).
- examples/squad/CommunityToolkit.Aspire.Hosting.Squad.ServiceDefaults/
    - Standard Aspire ServiceDefaults shape (OTel + service discovery +
      health checks), mirroring the Java example layout.
- examples/squad/CommunityToolkit.Aspire.Hosting.Squad.AppHost/
    - Updated to AddProject<...>("squad-api").WithReference(researchSquad)
- Directory.Packages.props
    - Added Squad.Agents.AI 0.1.0-preview.3 (centralized version), the
      preview that ships the SessionConfig/OnPermissionRequest fix needed
      to drive a real conversation. See bradygaster/squad#1252.
- CommunityToolkit.Aspire.slnx
    - 2 new projects added (ApiApp, ServiceDefaults).
- src/CommunityToolkit.Aspire.Hosting.Squad/README.md
    - New "Consume the team from a service project" section pointing
      callers at the new examples/squad/ runnable.

Local verification
------------------
- dotnet build (Release) of all 5 squad-related projects: 0 warn, 0 err
- All 7 unit tests still pass on net10.0 (MTP)
- Squad.Agents.AI 0.1.0-preview.3 restores from nuget.org cleanly

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…DK direct ref

Squad.Agents.AI 0.2.0-preview.4 (released earlier today from bradygaster/squad#1259)
brings the package onto Microsoft.Agents.AI.GitHub.Copilot 1.10.0-rc1, which
transitively pulls GitHub.Copilot.SDK 1.0.0 GA. The earlier 0.1.0-preview.3 was
wedged on SDK 1.0.0-beta.2 + a CLI protocol the current copilot CLI no longer
speaks — verified by running the example ApiApp in this branch against a real
.squad-initialised team root and seeing the agent successfully read .squad/team.md
and dispatch work to multiple specialists.

Also add a direct PackageReference to GitHub.Copilot.SDK 1.0.0. This is a
temporary workaround: the SDK ships its CLI-binary-download MSBuild targets
under build/ (only auto-imported for projects with a *direct* PackageReference),
not buildTransitive/. microsoft/agent-framework#6457 (merged 2026-06-10) fixes
this at the MAF adapter level — once a MAF preview ships with that change, the
direct GitHub.Copilot.SDK ref can be dropped from Directory.Packages.props and
the ApiApp csproj. Both edit sites carry an inline comment explaining the lifecycle.

Verified: the hosting library, the ApiApp, and the AppHost all build clean, and
copilot.exe lands at the expected 'bin/{cfg}/{tfm}/runtimes/{rid}/native/' path
in the ApiApp output.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… into ApiApp

Squad.Agents.AI 0.3.0-preview.5 (released today from bradygaster/squad#1265) adds
a first-class subagent observability surface: a typed OnSubagentTrace callback +
an OpenTelemetry ActivitySource named 'Microsoft.Agents.AI.Squad' that emits one
span per task-tool dispatch with squad.subagent.name / squad.subagent.display_name /
squad.subagent.sdk_agent_id / squad.subagent.reply_preview tags.

This commit:

* Bumps Squad.Agents.AI from 0.2.0-preview.4 to 0.3.0-preview.5 in
  Directory.Packages.props.

* Wires the new ActivitySource into the ApiApp's OpenTelemetry tracer (one line
  on top of the AddServiceDefaults() configuration) so every subagent dispatch
  shows up as its own span in the Aspire dashboard's Traces view, with the
  subagent name + a preview of its reply attached as span tags.

* Adds OnSubagentTrace logging in the ApiApp so subagent lifecycle (spawn, reply,
  complete) also surfaces in the dashboard's Structured Logs view — both signals
  are correlated by Activity.Current so the OTel span and the structured log
  share the same trace id.

* Adds a new POST /dispatch endpoint that issues an explicit task-tool dispatch
  prompt ('use the task tool to dispatch two parallel subagents...'). Hitting
  this endpoint from the dashboard produces a multi-span trace showing the
  coordinator + each subagent as separate spans — the headline visibility demo.

* Existing POST /ask 3-turn endpoint kept for the session-memory demo.

Verified locally: AppHost boots clean, the 'research-squad' resource registers
2 agents (ralph, picard from the sample-squad team), and the ApiApp builds + runs
with the new observability wiring against Squad.Agents.AI 0.3.0-preview.5
restored from nuget.org.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…rch + dev)

The previous bundled sample-squad/ had stub one-line charters with nothing to
dispatch to, so /ask returned coordinator role-play and /dispatch produced no
subagent spans in the Aspire dashboard.

Replace it with two real Squad teams created via 'squad init --no-workflows'
and autonomously cast by the Squad coordinator running under 'copilot --yolo':

  research-squad/   Matrix-cast AI/ML research team
                    Morpheus (Lead) - Trinity (ML/Data) - Oracle (Eval) - Tank (Tester)
                    Stack: Python, PyTorch/TensorFlow, Jupyter, MLflow

  dev-squad/        Simpsons-cast full-stack development team
                    Lisa (Lead) - Marge (Frontend) - Frink (Backend) - Comic Book Guy (Tester)

Both squads have full .squad/ scaffolding: team.md, per-agent charter.md and
history.md, casting registry, decisions ledger, ceremonies, routing, and the
.copilot/ skills the agents read. Runtime state (orchestration-log/, log/,
sessions/, decisions/inbox/) is excluded via the per-squad .gitignore that
'squad init' generates.

AppHost wires both as Aspire resources via .AddSquad('research-squad', ...) and
.AddSquad('dev-squad', ...), and references both from the ApiApp via
.WithReference(researchSquad).WithReference(devSquad). Aspire injects two
connection strings under ConnectionStrings:research-squad and
ConnectionStrings:dev-squad.

ApiApp rewritten to:
- Register one SquadAgent per squad via AddKeyedSquadAgent(serviceKey, ...)
  using the keyed-DI overload shipped in Squad.Agents.AI 0.1.0-preview.2
- Resolve the right agent per request via [FromKeyedServices] / ?squad= query
- /ask?squad=research|dev runs a 3-turn conversation against the picked team
- /dispatch?squad=research|dev sends an explicit 'use the task tool to dispatch
  two parallel subagents' prompt so we reliably see real subagent spawns (not
  inline role-play) and one squad.subagent {Name} OTel span per spawn in the
  Aspire dashboard Traces view
- OnSubagentTrace forwards subagent start/done/message to console so the
  structured logs line up with the OTel trace timeline

AppHost launchSettings.json also gains DCP_IDE_REQUEST_TIMEOUT_SECONDS=300 to
prevent the 120s DCP/VS run-session race that falls back to bare dotnet.exe
and produces 'Usage: dotnet [path-to-application]' in the ApiApp logs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…st comments

The ApiApp registered SquadAgent under keys 'research-squad' and 'dev-squad'
(the Aspire resource names) but the /ask and /dispatch endpoints take
?squad=research|dev (short form). The dictionary lookup mismatched and both
endpoints returned {"error":"Unknown squad 'research'..."}.

Fix:
- Register each keyed SquadAgent under the short name ('research', 'dev')
- Resolve the longer 'research-squad' / 'dev-squad' name only when reading
  the connection string (which IS keyed by the Aspire resource name)
- Drop the unused ResearchSquad/DevSquad const

Also clean up the duplicate using + stale comment block in AppHost/Program.cs
left over from the previous edit.

Verified end-to-end with a live run:
- Both squads come up Active in the dashboard (research: 7 agents discovered;
  dev: 6 agents discovered)
- POST /dispatch?squad=research returns the Morpheus + Trinity answers verbatim
- POST /dispatch?squad=dev returns the Lisa + Frink answers verbatim
- ApiApp spawns one copilot.exe child per request (real subagent dispatch, not
  inline role-play)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…folder

When 'squad init' is run from a folder that has a parent git repo (i.e. our
case — the Squad CLI's monorepo detection kicked in because Aspire-1 is a git
repo), the CLI places .github/agents/squad.agent.md at the GIT ROOT instead of
inside the squad folder. That made the example squads non-self-contained:
anyone cloning this repo and pointing SquadFolderPath at research-squad/ or
dev-squad/ would have no coordinator agent file (Copilot resolves
.github/agents/ relative to the consumer's git root, not ours).

For an in-repo example that ships as a finished artifact, the squads need to
own their own coordinator. Copy the same squad.agent.md (v0.9.6-insider.3,
71594 bytes) into each squad's own .github/agents/ directory.

Also expand each .gitignore to cover Copilot CLI SDK runtime files that get
created when a SquadAgent serves a request:
  .squad/session-state/
  .squad/session-store.db
  .squad/session-store.db-shm
  .squad/session-store.db-wal

The bundled .gitignore from 'squad init' already covered .squad/sessions/ but
those SDK-side files use slightly different paths.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two problems with the previous Console.WriteLine approach:

1. Console.WriteLine output did not consistently surface in the Aspire dashboard
   when the SDK fired OnSubagentTrace from its background event pump (different
   thread, no captured stdout context). The user reported empty Console Logs
   even though the callback was firing.

2. The SDK's per-subagent OTel spans (Microsoft.Agents.AI.Squad activity source)
   may end up as orphan traces — the SDK's Channel-based event pump can fire
   on a thread where Activity.Current has been cleared by the AsyncLocal flow,
   so StartActivity has no parent and the span becomes a new root trace
   detached from the HTTP request's trace.

Fixes:

- Use ILogger<>'s structured logging via Microsoft.Extensions.Options'
  Configure<ILoggerFactory> overload, which injects the fully-built
  ILoggerFactory into the SquadAgentOptions post-configure step (runs after
  the host is built, before any RunAsync call). Each subagent
  start / message / completed lands in the Aspire dashboard's Structured Logs
  view with the squad name, subagent name, and message preview as queryable
  fields.

- Add an app-owned ActivitySource ("Squad.Hosting.ApiApp") and wrap each
  /ask and /dispatch handler in a "squad.dispatch {endpoint} {squad}" span.
  This guarantees a single, named root span per request that the user's
  trace search can land on, even when the SDK's per-subagent spans become
  orphans.

- Inside OnSubagentTrace, also call Activity.Current?.AddEvent(...) so each
  subagent lifecycle event (start / message / completed) is attached as an
  OpenTelemetry ActivityEvent to whatever span is current at callback time
  (the SDK's subagent span when open, the dispatch wrapper otherwise).
  Events show up as labelled annotations on the span timeline in the Aspire
  dashboard trace detail view.

- Wire the new app source into AddOpenTelemetry().WithTracing(...) alongside
  the existing Microsoft.Agents.AI.Squad source.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… ApiApp wiring

0.4.0 (bradygaster/squad#1271, merged today) ships two changes that remove
the boilerplate this example needed in 0.3.0:

1. AddKeyedSquadAgent("research-squad") now resolves
   ConnectionStrings:research-squad (Aspire-injected) directly, with a
   fallback to the legacy ConnectionStrings:squad-research-squad form.
   No more manual builder.Configuration.GetConnectionString() + URI parse +
   feeding SquadFolderPath through a configure callback.

2. EmitSubagentActivities defaults to true. Just call
   .AddSource(SquadAgentDiagnostics.ActivitySourceName) on the tracer and
   "squad.subagent {Name}" spans appear in the Aspire dashboard's Traces view
   with lifecycle ActivityEvents (start / message / completed / failed)
   annotated on the timeline — without setting OnSubagentTrace at all.
   OnSubagentTrace becomes a pure customisation hook now, kept here only to
   surface per-subagent ILogger lines in the Structured Logs view.

ApiApp Program.cs:
  - Drop the foreach that called GetConnectionString + ParseSquadTeamRoot
    (16 lines) — replaced by a single AddKeyedSquadAgent("{resource}") call
    per squad, with a separate AddOptions<>().Configure<ILoggerFactory>(...)
    hop for the OnSubagentTrace ILogger callback.
  - Drop the local ParseSquadTeamRoot helper — the SDK does the parsing.
  - Drop the squadTeamRoots dictionary — was a leaky abstraction of the
    SDK's connection-string handling. Replaced with squadKeysByShortName
    that just maps the query-param short name ("research") to the keyed-DI
    key ("research-squad").

Bumped Squad.Agents.AI 0.3.0-preview.5 → 0.4.0-preview.6 in
Directory.Packages.props.

End-to-end verified live against the published 0.4.0-preview.6 package:
  - Both squads register cleanly with no manual connection-string handling
  - POST /dispatch?squad=research returned the Morpheus + Trinity replies
    verbatim (real task-tool subagent dispatch)
  - Smaller, cleaner Program.cs that closely mirrors the README

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…verride

When running 'copilot --agent squad' interactively in a terminal, the
coordinator loads .github/agents/squad.agent.md (1023 lines: eager
execution, parallel fan-out, dispatch via task tool) as its system prompt
and follows that contract. When the same Squad team is invoked through
Squad.Agents.AI's SquadAgent, the underlying copilot.exe child process
was using its default agent — so the coordinator had no instructions to
fan out or dispatch, and the terse 'Be concise' Instructions override we
were passing actively encouraged it to role-play the team in a single
reply instead of spawning real subagents.

Fix:
- Pass --agent squad in CliArgs so the CLI selects squad.agent.md as the
  agent definition for the spawned session
- Drop the per-squad Instructions override (was overriding the full
  squad.agent.md system prompt with one terse line)

Verified live against Squad.Agents.AI 0.4.0-preview.6:
- POST /ask?squad=dev with a Full Mode prompt ('Use the task tool to
  dispatch each of Lisa, Marge, Frink and Comic Book Guy in parallel ...')
  produced the eager-execution narration ('Waiting for all four agents
  to return. Still waiting for Marge.') that only squad.agent.md emits,
  AND returned four distinct subagent replies — proof that --agent squad
  is active and the task tool is dispatching.

Note: 'team, i want every member to say its name' still gets a single
coordinator reply because squad.agent.md classifies that as Direct Mode
('Who's on the team?' → answer from team.md, no spawn). For the
observability demo, use either POST /dispatch?squad=X or send a Full Mode
prompt to /ask.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
0.5.0-preview.7 introduced a regression: setting sessionConfig.Agent ='squad'
caused a runtime failure ('Custom agent ''squad'' not found') because the SDK
property looks up the SDK's CustomAgents registry, NOT the on-disk
.github/agents/*.agent.md files.

0.5.1-preview.8 (bradygaster/squad#1277) reverts to the original --agent CLI
flag approach which actually reads the on-disk agent definition the same
way 'copilot --agent squad' does interactively.

Verified live: POST /dispatch?squad=dev now returns distinct subagent voices
(Lisa: 'A good software architecture makes change boring ...';
 Frink: 'A well-designed API's most important property is a clear, stable
        contract that makes correct usage obvious and reliable—glavin!'),
which confirms (a) --agent squad is now being passed through, (b)
squad.agent.md is loaded as the coordinator's system prompt, and (c) real
subagent dispatch via the task tool is happening (not coordinator role-play).

The ApiApp's Program.cs already drops the manual --agent CLI workaround from
the previous 0.5.0 upgrade commit, so this is a version-only bump.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ld break

ApiApp:
  - Drop /dispatch. The endpoint was just /ask with a hardcoded "Use the task
    tool to dispatch ..." prompt; same SquadAgent.RunAsync path, same coordinator,
    same span/log surface — the only difference was the prompt. Removing it
    makes the API surface honest: there's one endpoint, the caller owns the
    prompt, the coordinator picks the mode (Direct / Lightweight / Full).
  - GET / now returns a copy-paste "sample_prompts" menu (roster_recall_direct_mode,
    multi_turn_memory, dispatch_full_mode_research, dispatch_full_mode_dev),
    each shaped like the /ask body so the user can paste straight through to
    reproduce every mode and see what the corresponding spans look like.
  - Drop the unused AskRequest.Default sample (the / index now owns sample prompts).
  - Rename the wrapper span "squad.dispatch ask {squad}" -> "squad.ask {squad}".

AppHost build break:
  Earlier interactive sessions created scratch C# projects inside the squads
  (e.g. research-squad/UserNameApp/, dev-squad/UserNameApp/) when the user
  asked the team to "write a program in C3 / C#". The Aspire AppHost SDK's
  default <Compile>, <Content>, <None>, and <EmbeddedResource> globs sweep
  any .cs / .csproj at any depth under the AppHost project folder, which
  caused two cascading build errors on the next F5:
    CS8802: Only one compilation unit can have top-level statements.
    CS0579: Duplicate AssemblyInfo attributes.
  Fix in two layers:
    1. AppHost.csproj: explicit <Compile|Content|None|EmbeddedResource Remove>
       on research-squad\** and dev-squad\**. The squads are scaffolding-only
       (.squad/, .github/, .copilot/, .mcp.json) and should never participate
       in the AppHost compile graph.
    2. Each squad's .gitignore: append *.csproj / *.fsproj / *.vbproj / bin/
       / obj/ guards so future scratch projects don't get committed either.
  Also delete the existing UserNameApp/ scratch dirs from both squads.

Squad.slnx:
  New focused solution file — just src/CommunityToolkit.Aspire.Hosting.Squad,
  the AppHost, the ApiApp, the ServiceDefaults, and the tests. Trims the
  noise out of F5 / VS solution explorer for anyone who clones the example
  and only wants to run the Squad bits without loading the rest of the
  Aspire repo's projects.

Verified live against Squad.Agents.AI 0.5.1-preview.8:
  - GET / lists 4 sample_prompts including the full-mode dispatch prompts
  - POST /ask?squad=dev with the dispatch_full_mode_dev sample produced Lisa
    + Frink replies with their distinct specialist voices, confirming real
    subagent dispatch through the task tool.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Five inline review comments from the Copilot reviewer, all addressed:

1. SquadResource.cs: Cast MatchCollection to IEnumerable<Match>
   Explicit .Cast<Match>() on both Regex.Matches results so the LINQ
   Concat/Select binding is unambiguous across TFMs (older targets expose
   MatchCollection only as non-generic IEnumerable). No behavior change on
   net10.0 where it already implements IList<Match>; defensive clarity only.

2. SquadBuilderExtensions.StartConsoleWindow: drop fragile cmd /c start invocation
   The previous approach used 'cmd /c start <windowTitle> powershell.exe ...',
   which fails or misbehaves when the title contains spaces (e.g.,
   'Copilot - research-squad'): cmd's quote-stripping causes 'start' to parse
   the title fragment as part of the command. Now launches powershell.exe
   directly with UseShellExecute=true and sets the title from inside the new
   shell via \System.Management.Automation.Internal.Host.InternalHost.UI.RawUI.WindowTitle, removing all cmd/start parsing
   fragility while preserving the friendly window title.

3. api/*.cs: regenerated to match the real public signature
   The hand-typed surface declared 'string teamRoot' (required) but the
   implementation exposes 'string? teamRoot = null'. Re-ran
   GenAPIGenerateReferenceAssemblySource; the file now correctly shows
   the nullable optional parameter and also picks up SquadTeamAnnotation,
   which the previous version had missed.

4. tests/AssemblyInfo.cs: removed
   InternalsVisibleTo on the test assembly grants visibility to itself
   (no-op). The production csproj already wires InternalsVisibleTo to the
   test assembly correctly; the test-side file was duplicate dead config.

5. tests/SquadResourceCreationTests.cs: deterministic temp cleanup
   Class now implements IDisposable and tracks every temp dir created by
   the helpers (instance-scoped _tempRoots). Dispose() best-effort deletes
   them recursively so test residue no longer accumulates in %TEMP%
   across runs.

Tests: 7/7 pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@tamirdresher

Copy link
Copy Markdown
Author

Round of fixes for the Copilot review shipped in 097369f.

All 5 inline comments addressed (replies posted on each thread):

# File Fix
1 SquadResource.cs Explicit .Cast<Match>() on both Regex.Matches results so LINQ binding is unambiguous across TFMs
2 SquadBuilderExtensions.cs StartConsoleWindow no longer goes through cmd /c start; launches powershell.exe directly via UseShellExecute=true and sets the title from inside the new shell — no more cmd-quote-stripping fragility on titles with spaces
3 api/CommunityToolkit.Aspire.Hosting.Squad.cs Regenerated via -t:GenAPIGenerateReferenceAssemblySource; correctly shows string? teamRoot = null (and picked up SquadTeamAnnotation that was missing)
4 tests/AssemblyInfo.cs Deleted — was a no-op (production csproj already wires InternalsVisibleTo to the test assembly)
5 tests/SquadResourceCreationTests.cs Test class now IDisposable with instance-scoped _tempRoots cleanup — no more %TEMP% residue

Local validation: 7/7 tests pass on net10.0 / Release.

Ready for another look.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: add CommunityToolkit.Aspire.Hosting.Squad - first-class Aspire resource for Squad AI-agent teams

2 participants