Squad.Agents.AI 0.4.0: default-on subagent observability + Aspire-style connection-string lookup#1271
Merged
tamirdresher merged 1 commit intoJun 11, 2026
Conversation
…le connection-string lookup
Two small but high-impact changes that remove ~30 lines of boilerplate from every
consumer (Aspire and otherwise) and make the OpenTelemetry story self-explanatory.
## 1. EmitSubagentActivities (default true) — telemetry independent of callback
Today, the per-subagent OpenTelemetry Activity emission is a side-effect of setting
`OnSubagentTrace`. A host that just wants `squad.subagent {Name}` spans in their
backend has to wire a callback they don't need. `Microsoft.Agents.AI.Squad` is
silent until then.
0.4.0 makes activity emission the default:
* New `SquadAgentOptions.EmitSubagentActivities` (defaults to `true`).
* `SquadAgent` installs `SquadSubagentTraceMapper` whenever
`EmitSubagentActivities || OnSubagentTrace != null`, so spans flow with zero
extra wiring.
* `OnSubagentTrace` becomes a pure customisation hook (logging, dashboards,
metrics) — independent of telemetry. Set `EmitSubagentActivities = false` to
opt out of built-in spans when you want to handle telemetry yourself.
Plus richer span shape: every lifecycle phase is now an
`ActivityEvent` on the live subagent span (visible as annotated markers on the
timeline in Aspire / Jaeger / etc.):
* `squad.subagent.start` — on SubagentStarted
* `squad.subagent.message` — on AssistantMessage (with message_preview tag)
* `squad.subagent.completed` — on SubagentCompleted
* `squad.subagent.failed` — on SubagentFailed
Net effect for a consumer:
builder.Services.AddOpenTelemetry()
.WithTracing(t => t.AddSource(SquadAgentDiagnostics.ActivitySourceName));
builder.Services.AddSquadAgent(o => o.SquadFolderPath = "/team");
…and the dashboard lights up. No callback wiring, no Activity.Current?.AddEvent
plumbing in the host.
## 2. Aspire-style connection-string lookup (with legacy fallback)
Aspire injects connection strings under the literal resource name —
e.g. an AppHost that calls `builder.AddSquad("research-squad", ...)` exposes
`ConnectionStrings:research-squad` to the consumer. The 0.3.0 SDK only looked at
`ConnectionStrings:squad-research-squad` (prefixed), so Aspire consumers had to
manually call `Configuration.GetConnectionString(name)`, parse the URI, and feed
`SquadFolderPath` into the configure callback themselves.
0.4.0 tries the literal name first and falls back to the legacy prefixed form:
| Style | Example | Lookup |
|--------------------------------------|---------------------------------------------|--------------------------------------------|
| Aspire-style direct (tried first) | `AddSquadAgent("research-squad")` | `ConnectionStrings:research-squad` |
| Legacy prefixed fallback | `AddSquadAgent("research")` | `ConnectionStrings:squad-research` |
Both work. Existing consumers using `ConnectionStrings:squad-{name}` continue
unchanged; new Aspire consumers get the natural one-line registration.
## Tests
54 → 64 tests, all passing on net8.0/9.0/10.0.
New `SquadAgentDefaultObservabilityTests` covers:
* Default-on activity emission (without consumer callback)
* Opt-out path (`EmitSubagentActivities = false`) — span suppression + callback
still fires
* Each ActivityEvent name (`start` / `message` / `completed` / `failed`)
* Connection-string precedence: Aspire-direct preferred, prefixed fallback used,
same rule applies for keyed registrations
Existing `SquadSubagentTraceTests` and the new class share an
`[Collection("SquadActivityListeners")]` so they run serially — process-global
`ActivityListener` state caused cross-test pollution otherwise.
## Files
* `src/Squad.Agents.AI/SquadAgentOptions.cs` — new `EmitSubagentActivities`
property + reworked `OnSubagentTrace` XML doc to clarify independence.
* `src/Squad.Agents.AI/SquadAgent.cs` — install trace mapper when telemetry OR
callback is requested.
* `src/Squad.Agents.AI/SquadSubagentTraceMapper.cs` — accept
`emitActivities` flag; gate `StartActivity`/`Dispose` on it; add
`ActivityEvent` annotations at every lifecycle boundary.
* `src/Squad.Agents.AI/SquadAgentOptionsConfigurator.cs` — accept a list of
candidate connection-string names; first non-empty wins.
* `src/Squad.Agents.AI/SquadServiceCollectionExtensions.cs` — new
`GetConnectionStringNames` returns `[name, "squad-"+name]` so both Aspire
and legacy conventions resolve.
* `src/Squad.Agents.AI/Squad.Agents.AI.csproj` — bump 0.3.0 → 0.4.0.
* `src/Squad.Agents.AI/README.md` — new "Subagent observability" section,
updated "Aspire / configuration path" section, two new option rows in the
Key Options table.
## Backward compatibility
Fully backward compatible. The two-arg `SquadSubagentTraceMapper` constructor
defaults `emitActivities` to `true`, `EmitSubagentActivities` defaults to
`true`, and the legacy `ConnectionStrings:squad-{name}` lookup still resolves.
Net change for an existing consumer that had OnSubagentTrace set: nothing (mapper
runs in both 0.3.0 and 0.4.0 because OnSubagentTrace is non-null). Net change
for a consumer that did NOT set OnSubagentTrace but did AddSource: they now get
spans they always asked for.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Contributor
🟡 Impact Analysis — PR #1271Risk tier: 🟡 MEDIUM 📊 Summary
🎯 Risk Factors
📦 Modules Affectedroot (7 files)
tests (2 files)
This report is generated automatically for every PR. See #733 for details. |
Contributor
🛫 PR Readiness Check
PR Scope: 🔧 Infrastructure
|
| Status | Check | Details |
|---|---|---|
| ✅ | Single commit | 1 commit — clean history |
| ✅ | Not in draft | Ready for review |
| ✅ | Branch up to date | Up to date with dev |
| ❌ | Copilot review | No Copilot review yet — it may still be processing |
| ✅ | Changeset present | No source files changed — changeset not required |
| ✅ | Scope clean | No .squad/ or docs/proposals/ files |
| ✅ | No merge conflicts | No merge conflicts |
| ✅ | Copilot threads resolved | No Copilot review threads |
| ❌ | CI passing | 9 check(s) still running |
Files Changed (9 files, +556 −89)
| File | +/− |
|---|---|
src/Squad.Agents.AI/README.md |
+43 −1 |
src/Squad.Agents.AI/Squad.Agents.AI.csproj |
+1 −1 |
src/Squad.Agents.AI/SquadAgent.cs |
+8 −5 |
src/Squad.Agents.AI/SquadAgentOptions.cs |
+54 −6 |
src/Squad.Agents.AI/SquadAgentOptionsConfigurator.cs |
+66 −33 |
src/Squad.Agents.AI/SquadServiceCollectionExtensions.cs |
+31 −8 |
src/Squad.Agents.AI/SquadSubagentTraceMapper.cs |
+66 −35 |
test/Squad.Agents.AI.Tests/SquadAgentDefaultObservabilityTests.cs |
+286 −0 |
test/Squad.Agents.AI.Tests/SquadSubagentTraceTests.cs |
+1 −0 |
Total: +556 −89
This check runs automatically on every push. Fix any ❌ items and push again.
See CONTRIBUTING.md and PR Requirements for details.
Contributor
There was a problem hiding this comment.
Pull request overview
This PR updates the Squad.Agents.AI .NET SDK to make subagent OpenTelemetry emission default-on (independent of OnSubagentTrace) and to support Aspire-style connection string naming with a legacy fallback, reducing consumer boilerplate.
Changes:
- Add
SquadAgentOptions.EmitSubagentActivities(defaulttrue) and installSquadSubagentTraceMapperwhen either telemetry emission orOnSubagentTraceis enabled. - Enrich subagent spans with lifecycle
ActivityEvents (start,message,completed,failed) and keep callback behavior independent. - Update DI/configuration to prefer
ConnectionStrings:{name}first, then fall back to legacyConnectionStrings:squad-{name}, with new tests + README updates.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| test/Squad.Agents.AI.Tests/SquadSubagentTraceTests.cs | Serializes ActivityListener-based tests to avoid global listener cross-test interference. |
| test/Squad.Agents.AI.Tests/SquadAgentDefaultObservabilityTests.cs | Adds test coverage for default-on telemetry, opt-out behavior, lifecycle events, and connection string precedence. |
| src/Squad.Agents.AI/SquadSubagentTraceMapper.cs | Adds emitActivities toggle and emits lifecycle ActivityEvents on the subagent span. |
| src/Squad.Agents.AI/SquadServiceCollectionExtensions.cs | Switches from single connection-string name to an ordered candidate-name chain (direct then prefixed). |
| src/Squad.Agents.AI/SquadAgentOptionsConfigurator.cs | Updates config binding to try multiple connection string names in order. |
| src/Squad.Agents.AI/SquadAgentOptions.cs | Introduces EmitSubagentActivities and updates OnSubagentTrace docs to reflect decoupled telemetry. |
| src/Squad.Agents.AI/SquadAgent.cs | Installs the trace mapper when telemetry and/or callback is enabled, and passes the emit flag through. |
| src/Squad.Agents.AI/Squad.Agents.AI.csproj | Bumps package version to 0.4.0. |
| src/Squad.Agents.AI/README.md | Documents new connection-string lookup behavior and the default-on subagent telemetry story. |
Comment on lines
+59
to
+71
| // Try each candidate name in order; first non-empty wins. This lets a single | ||
| // AddSquadAgent("research") call resolve either ConnectionStrings:research | ||
| // (Aspire-style) or ConnectionStrings:squad-research (legacy SDK convention). | ||
| string? connectionString = null; | ||
| foreach (var candidate in _connectionStringNames) | ||
| { | ||
| var value = _configuration.GetConnectionString(candidate); | ||
| if (!string.IsNullOrWhiteSpace(value)) | ||
| { | ||
| connectionString = value; | ||
| break; | ||
| } | ||
| } |
tamirdresher
pushed a commit
to tamirdresher/Aspire-1
that referenced
this pull request
Jun 11, 2026
… 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>
tamirdresher
pushed a commit
to tamirdresher/Aspire-1
that referenced
this pull request
Jun 11, 2026
… 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>
10 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two small but high-impact changes that remove ~30 lines of boilerplate from every consumer (Aspire and otherwise) and make the OpenTelemetry story self-explanatory.
1.
EmitSubagentActivities(defaulttrue) — telemetry independent of callbackToday the per-subagent OTel
Activityemission is a side-effect of settingOnSubagentTrace. A host that just wantssquad.subagent {Name}spans in their backend has to wire a callback they don't need.Microsoft.Agents.AI.Squadis silent until then.0.4.0 makes activity emission the default:
SquadAgentOptions.EmitSubagentActivities(defaults totrue).SquadAgentinstallsSquadSubagentTraceMapperwheneverEmitSubagentActivities || OnSubagentTrace != null, so spans flow with zero extra wiring.OnSubagentTracebecomes a pure customisation hook (logging, dashboards, metrics) — independent of telemetry. SetEmitSubagentActivities = falseto opt out of built-in spans.Richer span shape: every lifecycle phase is now an
ActivityEventon the live subagent span (visible as annotated markers on the timeline in Aspire / Jaeger / etc.):squad.subagent.start— on SubagentStartedsquad.subagent.message— on AssistantMessage (withmessage_previewtag)squad.subagent.completed— on SubagentCompletedsquad.subagent.failed— on SubagentFailedNet effect for a consumer:
…and the dashboard lights up. No callback wiring, no
Activity.Current?.AddEventplumbing in the host.2. Aspire-style connection-string lookup (with legacy fallback)
Aspire injects connection strings under the literal resource name — e.g.
builder.AddSquad("research-squad", ...)exposesConnectionStrings:research-squadto the consumer. The 0.3.0 SDK only looked atConnectionStrings:squad-research-squad(prefixed), so Aspire consumers had to manually callConfiguration.GetConnectionString(name), parse the URI, and feedSquadFolderPathinto the configure callback themselves.0.4.0 tries the literal name first and falls back to the legacy prefixed form:
AddSquadAgent("research-squad")ConnectionStrings:research-squadAddSquadAgent("research")ConnectionStrings:squad-researchBoth work. Existing consumers using
ConnectionStrings:squad-{name}continue unchanged; new Aspire consumers get the natural one-line registration.Tests
54 → 64 tests, all passing on
net8.0/net9.0/net10.0.New
SquadAgentDefaultObservabilityTestscovers:EmitSubagentActivities = false) — span suppression + callback still firesActivityEventname (start/message/completed/failed)Existing
SquadSubagentTraceTestsand the new class share an[Collection(""SquadActivityListeners"")]so they run serially — process-globalActivityListenerstate caused cross-test pollution otherwise.Backward compatibility
Fully backward compatible. The two-arg
SquadSubagentTraceMapperconstructor defaultsemitActivitiestotrue,EmitSubagentActivitiesdefaults totrue, and the legacyConnectionStrings:squad-{name}lookup still resolves.OnSubagentTraceset → no change in behaviour (mapper runs in both 0.3.0 and 0.4.0 becauseOnSubagentTraceis non-null).AddSource(...)but noOnSubagentTrace→ gains the spans they always asked for.Real-world motivation
This came out of building the
CommunityToolkit.Aspire.Hosting.Squadexample (CommunityToolkit/Aspire#1394). The 0.3.0 consumer code needed:With 0.4.0 this collapses to:
…and the OTel spans appear in the Aspire dashboard automatically.