Skip to content

Squad.Agents.AI 0.4.0: default-on subagent observability + Aspire-style connection-string lookup#1271

Merged
tamirdresher merged 1 commit into
bradygaster:devfrom
tamirdresher:feat/squad-agents-ai-emit-activities
Jun 11, 2026
Merged

Squad.Agents.AI 0.4.0: default-on subagent observability + Aspire-style connection-string lookup#1271
tamirdresher merged 1 commit into
bradygaster:devfrom
tamirdresher:feat/squad-agents-ai-emit-activities

Conversation

@tamirdresher

Copy link
Copy Markdown
Collaborator

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 (default true) — telemetry independent of callback

Today the per-subagent OTel 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.

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. 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 Looks up
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/net9.0/net10.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.

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.

  • Existing consumer with OnSubagentTrace set → no change in behaviour (mapper runs in both 0.3.0 and 0.4.0 because OnSubagentTrace is non-null).
  • Existing consumer with AddSource(...) but no OnSubagentTracegains the spans they always asked for.

Real-world motivation

This came out of building the CommunityToolkit.Aspire.Hosting.Squad example (CommunityToolkit/Aspire#1394). The 0.3.0 consumer code needed:

var cs = builder.Configuration.GetConnectionString(""research-squad"")
    ?? throw new InvalidOperationException(""Missing connection string ..."");
var root = ParseSquadTeamRoot(cs)
    ?? throw new InvalidOperationException(""Could not parse teamRoot ..."");
builder.Services.AddKeyedSquadAgent(""research"", opts =>
{
    opts.SquadFolderPath = root;
    opts.OnSubagentTrace = trace => { /* needed just to enable spans */ };
});

With 0.4.0 this collapses to:

builder.Services.AddKeyedSquadAgent(""research-squad"");

…and the OTel spans appear in the Aspire dashboard automatically.

…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>
Copilot AI review requested due to automatic review settings June 11, 2026 13:58
@github-actions

Copy link
Copy Markdown
Contributor

🟡 Impact Analysis — PR #1271

Risk tier: 🟡 MEDIUM

📊 Summary

Metric Count
Files changed 9
Files added 1
Files modified 8
Files deleted 0
Modules touched 2

🎯 Risk Factors

  • 9 files changed (6-20 → MEDIUM)
  • 2 modules touched (2-4 → MEDIUM)

📦 Modules Affected

root (7 files)
  • src/Squad.Agents.AI/README.md
  • src/Squad.Agents.AI/Squad.Agents.AI.csproj
  • src/Squad.Agents.AI/SquadAgent.cs
  • src/Squad.Agents.AI/SquadAgentOptions.cs
  • src/Squad.Agents.AI/SquadAgentOptionsConfigurator.cs
  • src/Squad.Agents.AI/SquadServiceCollectionExtensions.cs
  • src/Squad.Agents.AI/SquadSubagentTraceMapper.cs
tests (2 files)
  • test/Squad.Agents.AI.Tests/SquadAgentDefaultObservabilityTests.cs
  • test/Squad.Agents.AI.Tests/SquadSubagentTraceTests.cs

This report is generated automatically for every PR. See #733 for details.

@github-actions

Copy link
Copy Markdown
Contributor

🛫 PR Readiness Check

ℹ️ This comment updates on each push. Last checked: commit 09e4113

PR Scope: 🔧 Infrastructure

⚠️ 2 item(s) to address before review

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.

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

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 (default true) and install SquadSubagentTraceMapper when either telemetry emission or OnSubagentTrace is 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 legacy ConnectionStrings: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 tamirdresher merged commit c076ffb into bradygaster:dev Jun 11, 2026
16 checks passed
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>
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.

2 participants