Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/Squad.Agents.AI/Squad.Agents.AI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageReadmeFile>README.md</PackageReadmeFile>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<Version>0.2.0</Version>
<Version>0.3.0</Version>
</PropertyGroup>
<ItemGroup>
<Compile Remove="samples/**/*.cs" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="Squad.Agents.AI.Tests" />
<PackageReference Include="Microsoft.Agents.AI.GitHub.Copilot" Version="1.10.0-rc1" />
<!--
Direct reference to GitHub.Copilot.SDK so its build/ targets fire during our build.
Expand Down
23 changes: 21 additions & 2 deletions src/Squad.Agents.AI/SquadAgent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public sealed class SquadAgent : DelegatingAIAgent, IAsyncDisposable
private readonly ILogger? _logger;
private readonly bool _ownsClient;
private readonly SquadAgentOptions _options;
private readonly SquadSubagentTraceMapper? _traceMapper;

// State-bag to thread pre-base-ctor state through chain constructors.
// DelegatingAIAgent requires the inner AIAgent at base() call time, so we
Expand All @@ -36,7 +37,8 @@ private readonly record struct SquadAgentState(
CopilotClient CopilotClient,
ILogger? Logger,
bool OwnsClient,
SquadAgentOptions Options);
SquadAgentOptions Options,
SquadSubagentTraceMapper? TraceMapper);

/// <summary>
/// Initializes a new <see cref="SquadAgent"/> with the Squad team root as the primary required parameter.
Expand Down Expand Up @@ -76,6 +78,7 @@ private SquadAgent(SquadAgentState state)
_logger = state.Logger;
_ownsClient = state.OwnsClient;
_options = state.Options;
_traceMapper = state.TraceMapper;
_logger?.LogInformation("SquadAgent initialized with name '{AgentName}', team root '{TeamRoot}'",
state.Options.AgentName, state.Options.SquadFolderPath);
}
Expand Down Expand Up @@ -135,6 +138,18 @@ private static SquadAgentState BuildStateInternal(SquadAgentOptions options, ILo
{
sessionConfig.SystemMessage = new SystemMessageConfig { Content = options.Instructions };
}

// Wire OnSubagentTrace before ConfigureSession so consumer-supplied ConfigureSession
// callbacks can still override or chain behind our defaults (e.g. compose multiple OnEvent
// handlers, replace IncludeSubAgentStreamingEvents, etc.).
SquadSubagentTraceMapper? traceMapper = null;
if (options.OnSubagentTrace is not null)
{
traceMapper = new SquadSubagentTraceMapper(options.OnSubagentTrace);
sessionConfig.IncludeSubAgentStreamingEvents = true;
sessionConfig.OnEvent = traceMapper.OnSessionEvent;
}
Comment on lines +142 to +151

options.ConfigureSession?.Invoke(sessionConfig);

var inner = client.AsAIAgent(
Expand All @@ -144,7 +159,7 @@ private static SquadAgentState BuildStateInternal(SquadAgentOptions options, ILo
name: options.AgentName ?? "Squad",
description: null);

return new SquadAgentState(inner, client, lf?.CreateLogger<SquadAgent>(), true, options);
return new SquadAgentState(inner, client, lf?.CreateLogger<SquadAgent>(), true, options, traceMapper);
}

// ── Extensibility seam ──────────────────────────────────────────────
Expand Down Expand Up @@ -323,6 +338,10 @@ public async ValueTask DisposeAsync()
if (InnerAgent is IAsyncDisposable innerDisposable)
await innerDisposable.DisposeAsync().ConfigureAwait(false);

// Drain any subagent activities that never received a matching SubagentCompletedEvent
// (e.g. session ended mid-dispatch). Failing to do so leaks Activity instances.
_traceMapper?.Dispose();

_logger?.LogDebug("SquadAgent disposed");
}
}
124 changes: 124 additions & 0 deletions src/Squad.Agents.AI/SquadAgentDiagnostics.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
using System.Diagnostics;

namespace Squad.Agents.AI;

/// <summary>
/// Diagnostic constants for Squad.Agents.AI. Consumers wiring OpenTelemetry should
/// add <see cref="ActivitySourceName"/> to their tracer provider to surface subagent
/// dispatch spans (one span per <c>task</c>-tool invocation) in their backend.
/// </summary>
/// <example>
/// <code>
/// builder.Services.AddOpenTelemetry()
/// .WithTracing(t => t.AddSource(SquadAgentDiagnostics.ActivitySourceName));
/// </code>
/// </example>
public static class SquadAgentDiagnostics
{
/// <summary>
/// Name of the <see cref="System.Diagnostics.ActivitySource"/> Squad.Agents.AI emits spans on.
/// </summary>
public const string ActivitySourceName = "Microsoft.Agents.AI.Squad";

/// <summary>
/// Singleton <see cref="System.Diagnostics.ActivitySource"/> used internally to emit subagent
/// dispatch spans. Consumers normally do not interact with this directly; subscribe via
/// <c>AddSource(SquadAgentDiagnostics.ActivitySourceName)</c> on their OpenTelemetry tracer.
/// </summary>
public static readonly ActivitySource ActivitySource = new(ActivitySourceName);
}

/// <summary>
/// Categorises the underlying <c>GitHub.Copilot.SessionEvent</c> for consumers who want a typed view
/// without depending directly on the SDK's polymorphic event hierarchy.
/// </summary>
public enum SquadAgentTraceEventKind
{
/// <summary>An event that does not match any of the well-known categories below.</summary>
Other = 0,

Comment on lines +35 to +39
/// <summary>The coordinator selected a custom agent to dispatch to (precedes <see cref="SubagentStarted"/>).</summary>
SubagentSelected,

/// <summary>A subagent process started (the coordinator's <c>task</c> tool spawned it).</summary>
SubagentStarted,

/// <summary>A subagent finished and returned its result back to the coordinator.</summary>
SubagentCompleted,

/// <summary>A subagent terminated abnormally.</summary>
SubagentFailed,

/// <summary>An assistant turn completed (from the coordinator OR a subagent — see <see cref="SquadAgentTraceEvent.SdkAgentId"/>).</summary>
AssistantMessage,

/// <summary>A tool started executing (the <c>task</c> tool is what spawns subagents).</summary>
ToolStart,

/// <summary>A tool finished executing.</summary>
ToolComplete,

/// <summary>The session became idle (the run is complete).</summary>
SessionIdle,
}

/// <summary>
/// Typed envelope around a <c>GitHub.Copilot.SessionEvent</c> for consumers who want to surface
/// subagent dispatch + assistant messages in a UI (e.g. an Aspire dashboard) without writing their
/// own polymorphic dispatch over the raw SDK event hierarchy.
/// </summary>
/// <remarks>
/// <para>
/// Squad.Agents.AI converts the raw event stream into <see cref="SquadAgentTraceEvent"/> instances
/// inside its own <c>OnEvent</c> handler when <see cref="SquadAgentOptions.OnSubagentTrace"/> is set,
/// then invokes the consumer's callback. Consumers therefore do not need a transitive reference to
/// <c>GitHub.Copilot.SDK</c> to subscribe to subagent activity.
/// </para>
/// <para>
/// The full underlying <c>GitHub.Copilot.SessionEvent</c> is preserved on <see cref="RawEvent"/> for
/// advanced consumers that need access to the original payload.
/// </para>
/// </remarks>
public sealed record SquadAgentTraceEvent
{
/// <summary>The categorised kind of event.</summary>
public required SquadAgentTraceEventKind Kind { get; init; }

/// <summary>The raw <c>GitHub.Copilot.SessionEvent</c> type name, e.g. <c>SubagentStartedEvent</c>.</summary>
public required string RawEventType { get; init; }

/// <summary>Server-assigned timestamp on the SDK event.</summary>
public DateTimeOffset Timestamp { get; init; }

/// <summary>
/// The SDK agent identifier (e.g. <c>toolu_vrtx_01Fc3uBTKapUoDDMnPUSe2ww</c>) the event is attributed to.
/// Null for events emitted by the root coordinator; non-null for subagent-scoped events when
/// <see cref="SquadAgentOptions.IncludeSubAgentStreamingEvents"/> is on.

Check warning on line 96 in src/Squad.Agents.AI/SquadAgentDiagnostics.cs

View workflow job for this annotation

GitHub Actions / .NET ubuntu-latest

XML comment has cref attribute 'IncludeSubAgentStreamingEvents' that could not be resolved

Check warning on line 96 in src/Squad.Agents.AI/SquadAgentDiagnostics.cs

View workflow job for this annotation

GitHub Actions / .NET ubuntu-latest

XML comment has cref attribute 'IncludeSubAgentStreamingEvents' that could not be resolved

Check warning on line 96 in src/Squad.Agents.AI/SquadAgentDiagnostics.cs

View workflow job for this annotation

GitHub Actions / .NET windows-latest

XML comment has cref attribute 'IncludeSubAgentStreamingEvents' that could not be resolved

Check warning on line 96 in src/Squad.Agents.AI/SquadAgentDiagnostics.cs

View workflow job for this annotation

GitHub Actions / .NET windows-latest

XML comment has cref attribute 'IncludeSubAgentStreamingEvents' that could not be resolved
/// </summary>
public string? SdkAgentId { get; init; }
Comment on lines +93 to +98

/// <summary>Display-friendly name of the spawning subagent (e.g. <c>Picard</c>), when known.</summary>
public string? SubagentName { get; init; }

/// <summary>Long-form display name of the spawning subagent, when known.</summary>
public string? SubagentDisplayName { get; init; }

/// <summary>SDK <c>ToolCallId</c> for <see cref="SquadAgentTraceEventKind.ToolStart"/> / <see cref="SquadAgentTraceEventKind.ToolComplete"/>.</summary>
public string? ToolCallId { get; init; }

/// <summary>
/// Assistant message content for <see cref="SquadAgentTraceEventKind.AssistantMessage"/>; null for other kinds.
/// Consumers may treat this as the subagent's reply when <see cref="SdkAgentId"/> is non-null.
/// </summary>
public string? Content { get; init; }

/// <summary>Tool success flag, for <see cref="SquadAgentTraceEventKind.ToolComplete"/>.</summary>
public bool? Success { get; init; }

/// <summary>
/// The original underlying <c>GitHub.Copilot.SessionEvent</c> instance. Kept as <see cref="object"/> on this
/// type so consumers do not need a transitive reference to the SDK. Cast to the concrete event type for advanced
/// scenarios.
/// </summary>
public object? RawEvent { get; init; }
}
44 changes: 44 additions & 0 deletions src/Squad.Agents.AI/SquadAgentOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@
/// Gets or sets a delegate that customizes the <see cref="SessionConfig"/> used to
/// construct the inner <see cref="Microsoft.Agents.AI.AIAgent"/>. The delegate runs
/// after Squad has applied its defaults (including an <c>ApproveAll</c>
/// <see cref="SessionConfig.OnPermissionRequest"/> handler and the

Check warning on line 118 in src/Squad.Agents.AI/SquadAgentOptions.cs

View workflow job for this annotation

GitHub Actions / .NET ubuntu-latest

XML comment has cref attribute 'OnPermissionRequest' that could not be resolved

Check warning on line 118 in src/Squad.Agents.AI/SquadAgentOptions.cs

View workflow job for this annotation

GitHub Actions / .NET ubuntu-latest

XML comment has cref attribute 'OnPermissionRequest' that could not be resolved

Check warning on line 118 in src/Squad.Agents.AI/SquadAgentOptions.cs

View workflow job for this annotation

GitHub Actions / .NET windows-latest

XML comment has cref attribute 'OnPermissionRequest' that could not be resolved
/// <see cref="Instructions"/> as the appended system message), so it can override or
/// extend any session-scoped setting such as the permission handler, tool list,
/// model name, or hooks.
Expand All @@ -136,6 +136,50 @@
[JsonIgnore]
public Action<SessionConfig>? ConfigureSession { get; set; }

/// <summary>
/// Gets or sets a callback that receives <see cref="SquadAgentTraceEvent"/> instances for every
/// notable session event from the underlying Copilot SDK — including subagent dispatch lifecycle
/// (<c>task</c>-tool spawn / completion), tool calls, and assistant messages from both the root
/// coordinator AND each spawned subagent.
/// </summary>
/// <remarks>
/// <para>
/// Setting this callback has three side effects:
/// </para>
/// <list type="number">
/// <item><see cref="GitHub.Copilot.SessionConfigBase.IncludeSubAgentStreamingEvents"/> is forced to
/// <see langword="true"/> so subagent assistant messages flow up to the parent session (otherwise
/// the subagent's reply stays inside its own session and never reaches the callback).</item>
/// <item>An <see cref="System.Diagnostics.ActivitySource"/> named
/// <see cref="SquadAgentDiagnostics.ActivitySourceName"/> emits one <see cref="System.Diagnostics.Activity"/>
/// per subagent dispatch, tagged with <c>squad.subagent.name</c> and a preview of the subagent's
/// reply. Hosts that <c>.AddSource(SquadAgentDiagnostics.ActivitySourceName)</c> on their
/// OpenTelemetry tracer get these spans in their backend (e.g. the Aspire dashboard).</item>
/// <item><see cref="ConfigureSession"/> may still override <see cref="GitHub.Copilot.SessionConfigBase.OnEvent"/>
/// or <see cref="GitHub.Copilot.SessionConfigBase.IncludeSubAgentStreamingEvents"/>; consumers
/// that need a stacked event handler should call <c>OnSubagentTrace</c> from inside their
/// <see cref="ConfigureSession"/> callback to compose the two.</item>
/// </list>
Comment on lines +158 to +162
/// <para>
/// Consumer callback exceptions are caught and swallowed so a misbehaving subscriber cannot tear
/// down the SDK event loop. Add your own try/catch + logging inside the callback if you need to
/// surface those errors.
/// </para>
/// </remarks>
/// <example>
/// <code>
/// options.OnSubagentTrace = trace =>
/// {
/// if (trace.Kind == SquadAgentTraceEventKind.SubagentStarted)
/// Console.WriteLine($"[spawn] {trace.SubagentName} (id={trace.SdkAgentId})");
/// else if (trace.Kind == SquadAgentTraceEventKind.AssistantMessage && trace.SdkAgentId is not null)

Check warning on line 175 in src/Squad.Agents.AI/SquadAgentOptions.cs

View workflow job for this annotation

GitHub Actions / .NET ubuntu-latest

XML comment has badly formed XML -- 'Whitespace is not allowed at this location.'

Check warning on line 175 in src/Squad.Agents.AI/SquadAgentOptions.cs

View workflow job for this annotation

GitHub Actions / .NET ubuntu-latest

XML comment has badly formed XML -- 'The character(s) '&' cannot be used at this location.'

Check warning on line 175 in src/Squad.Agents.AI/SquadAgentOptions.cs

View workflow job for this annotation

GitHub Actions / .NET ubuntu-latest

XML comment has badly formed XML -- 'Whitespace is not allowed at this location.'

Check warning on line 175 in src/Squad.Agents.AI/SquadAgentOptions.cs

View workflow job for this annotation

GitHub Actions / .NET ubuntu-latest

XML comment has badly formed XML -- 'The character(s) '&' cannot be used at this location.'

Check warning on line 175 in src/Squad.Agents.AI/SquadAgentOptions.cs

View workflow job for this annotation

GitHub Actions / .NET windows-latest

XML comment has badly formed XML -- 'Whitespace is not allowed at this location.'

Check warning on line 175 in src/Squad.Agents.AI/SquadAgentOptions.cs

View workflow job for this annotation

GitHub Actions / .NET windows-latest

XML comment has badly formed XML -- 'Whitespace is not allowed at this location.'

Check warning on line 175 in src/Squad.Agents.AI/SquadAgentOptions.cs

View workflow job for this annotation

GitHub Actions / .NET windows-latest

XML comment has badly formed XML -- 'The character(s) '&' cannot be used at this location.'

Check warning on line 175 in src/Squad.Agents.AI/SquadAgentOptions.cs

View workflow job for this annotation

GitHub Actions / .NET windows-latest

XML comment has badly formed XML -- 'The character(s) '&' cannot be used at this location.'

Check warning on line 175 in src/Squad.Agents.AI/SquadAgentOptions.cs

View workflow job for this annotation

GitHub Actions / .NET windows-latest

XML comment has badly formed XML -- 'Whitespace is not allowed at this location.'

Check warning on line 175 in src/Squad.Agents.AI/SquadAgentOptions.cs

View workflow job for this annotation

GitHub Actions / .NET windows-latest

XML comment has badly formed XML -- 'The character(s) '&' cannot be used at this location.'
/// Console.WriteLine($"[{trace.SdkAgentId}] {trace.Content}");
/// };
/// </code>
/// </example>
[JsonIgnore]
public Action<SquadAgentTraceEvent>? OnSubagentTrace { get; set; }

private static readonly string[] TokenPatterns = { "TOKEN", "KEY", "SECRET", "HMAC", "PASSWORD", "CREDENTIAL" };

private static bool IsTokenKey(string key)
Expand Down
136 changes: 136 additions & 0 deletions src/Squad.Agents.AI/SquadSubagentTraceMapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
using System.Diagnostics;
using GitHub.Copilot;

namespace Squad.Agents.AI;

/// <summary>
/// Converts the GitHub.Copilot SDK's polymorphic <see cref="SessionEvent"/> stream into the typed
/// <see cref="SquadAgentTraceEvent"/> envelope and emits matching OpenTelemetry spans on
/// <see cref="SquadAgentDiagnostics.ActivitySource"/>.
/// </summary>
/// <remarks>
/// One <c>Activity</c> per subagent dispatch is opened on <see cref="SubagentStartedEvent"/> and
/// disposed on the matching <see cref="SubagentCompletedEvent"/> / <see cref="SubagentFailedEvent"/>.
/// Inactive subagent <c>Activity</c> instances are tracked by their <c>SdkAgentId</c> so concurrent
/// dispatches each get their own span. The activity is kept alive in a <see cref="System.Collections.Concurrent.ConcurrentDictionary{TKey, TValue}"/>
/// for the duration of the subagent run; if the session ends without a matching completion event
/// (e.g. an unhandled exception in the SDK), <see cref="DisposeAll"/> drains the dictionary so spans

Check warning on line 17 in src/Squad.Agents.AI/SquadSubagentTraceMapper.cs

View workflow job for this annotation

GitHub Actions / .NET ubuntu-latest

XML comment has cref attribute 'DisposeAll' that could not be resolved

Check warning on line 17 in src/Squad.Agents.AI/SquadSubagentTraceMapper.cs

View workflow job for this annotation

GitHub Actions / .NET ubuntu-latest

XML comment has cref attribute 'DisposeAll' that could not be resolved

Check warning on line 17 in src/Squad.Agents.AI/SquadSubagentTraceMapper.cs

View workflow job for this annotation

GitHub Actions / .NET windows-latest

XML comment has cref attribute 'DisposeAll' that could not be resolved
/// still terminate cleanly.
Comment on lines +14 to +18
/// </remarks>
internal sealed class SquadSubagentTraceMapper : IDisposable
{
private readonly Action<SquadAgentTraceEvent>? _onTrace;
private readonly System.Collections.Concurrent.ConcurrentDictionary<string, Activity> _liveSubagentActivities = new(StringComparer.Ordinal);

public SquadSubagentTraceMapper(Action<SquadAgentTraceEvent>? onTrace)
{
_onTrace = onTrace;
}

/// <summary>Hook to set on <see cref="SessionConfigBase.OnEvent"/>.</summary>
public void OnSessionEvent(SessionEvent sessionEvent)
{
if (sessionEvent is null) return;

SquadAgentTraceEvent? envelope = sessionEvent switch
{
SubagentSelectedEvent s => Build(s, SquadAgentTraceEventKind.SubagentSelected, subagentName: s.Data?.AgentName),
SubagentStartedEvent s => Build(s, SquadAgentTraceEventKind.SubagentStarted, subagentName: s.Data?.AgentName, subagentDisplayName: s.Data?.AgentDisplayName),
SubagentCompletedEvent s => Build(s, SquadAgentTraceEventKind.SubagentCompleted, subagentName: s.Data?.AgentName),
SubagentFailedEvent s => Build(s, SquadAgentTraceEventKind.SubagentFailed, subagentName: s.Data?.AgentName, success: false),
AssistantMessageEvent a => Build(a, SquadAgentTraceEventKind.AssistantMessage, content: a.Data?.Content),
ToolExecutionStartEvent t => Build(t, SquadAgentTraceEventKind.ToolStart, toolCallId: t.Data?.ToolCallId),
ToolExecutionCompleteEvent t => Build(t, SquadAgentTraceEventKind.ToolComplete, toolCallId: t.Data?.ToolCallId, success: t.Data?.Success),
SessionIdleEvent _ => Build(sessionEvent, SquadAgentTraceEventKind.SessionIdle),
_ => null,
};

if (envelope is null) return;

// OpenTelemetry: open/close an Activity per subagent run.
if (envelope.Kind == SquadAgentTraceEventKind.SubagentStarted &&
!string.IsNullOrEmpty(envelope.SdkAgentId))
{
var activity = SquadAgentDiagnostics.ActivitySource.StartActivity(
$"squad.subagent {envelope.SubagentName ?? envelope.SdkAgentId}",
ActivityKind.Internal);
if (activity is not null)
{
activity.SetTag("squad.subagent.name", envelope.SubagentName);
activity.SetTag("squad.subagent.display_name", envelope.SubagentDisplayName);
activity.SetTag("squad.subagent.sdk_agent_id", envelope.SdkAgentId);
_liveSubagentActivities[envelope.SdkAgentId!] = activity;
}
}
else if (envelope.Kind == SquadAgentTraceEventKind.AssistantMessage &&
!string.IsNullOrEmpty(envelope.SdkAgentId) &&
_liveSubagentActivities.TryGetValue(envelope.SdkAgentId!, out var liveActivity))
{
// The subagent's assistant message is the substantive output of that dispatch. Tag the span
// with a short preview so backends can group spans by subagent and show the reply at a glance.
// We bound the preview to a sane length even though backends are free to truncate further.
if (!string.IsNullOrEmpty(envelope.Content))
{
var preview = envelope.Content!.Length > 512
? envelope.Content.Substring(0, 512) + "..."
: envelope.Content;
liveActivity.SetTag("squad.subagent.reply_preview", preview);
}
Comment on lines +71 to +78
}
else if (envelope.Kind is SquadAgentTraceEventKind.SubagentCompleted or SquadAgentTraceEventKind.SubagentFailed &&
!string.IsNullOrEmpty(envelope.SdkAgentId) &&
_liveSubagentActivities.TryRemove(envelope.SdkAgentId!, out var completedActivity))
{
completedActivity.SetStatus(
envelope.Kind == SquadAgentTraceEventKind.SubagentFailed
? ActivityStatusCode.Error
: ActivityStatusCode.Ok);
completedActivity.Dispose();
}

// Deliver the typed event to the consumer last so any consumer-side side effects (logging,
// UI updates) see the OTel span as already open/closed.
try
{
_onTrace?.Invoke(envelope);
}
catch
{
// Consumer callback exceptions must never tear down the SDK event loop.
}
}

private static SquadAgentTraceEvent Build(
SessionEvent sessionEvent,
SquadAgentTraceEventKind kind,
string? subagentName = null,
string? subagentDisplayName = null,
string? toolCallId = null,
string? content = null,
bool? success = null)
{
return new SquadAgentTraceEvent
{
Kind = kind,
RawEventType = sessionEvent.GetType().Name,
Timestamp = sessionEvent.Timestamp,
SdkAgentId = sessionEvent.AgentId,
SubagentName = subagentName,
SubagentDisplayName = subagentDisplayName,
ToolCallId = toolCallId,
Content = content,
Success = success,
RawEvent = sessionEvent,
};
}

public void Dispose()
{
foreach (var (_, activity) in _liveSubagentActivities)
{
try { activity.SetStatus(ActivityStatusCode.Error, "Session ended without matching SubagentCompletedEvent"); activity.Dispose(); }
catch { }
}
_liveSubagentActivities.Clear();
}
}
Loading
Loading