using ClawdDotNet.Core.Tools; namespace ClawdDotNet.Core.Activity; /// A run that is in progress right now. public sealed record ActiveRun( string RunId, string AgentId, string Source, string? TaskId, string? CalledBy, DateTime StartedAtUtc); /// /// One agent reaching another — for now a delegated run ( = /// delegation). Meetings and escalations will add their own kinds. /// public sealed record AgentLink(string From, string To, string Kind, DateTime AtUtc); /// /// #711: Who is working right now, and who reached whom — the live side of the team overview. /// /// Why a component of its own. The engine already tracked running chats, but /// only those that went through ChatAsync; a fresh run (@new) was invisible to /// IsRunning. And nothing recorded that agent A had called agent B — the delegation /// chain knew it for the length of the call and then forgot. Here both are kept and /// announced, independent of which engine path a run takes. /// /// Memory only, and bounded. This is the view of "now". What happened /// yesterday is in the audit log; the time line (#711, later) replays it from there. /// public sealed class AgentActivity { /// How many recent links are kept for a page that connects late. public const int RecentLinkCapacity = 500; private readonly Lock _lock = new(); private readonly Dictionary _active = new(StringComparer.Ordinal); private readonly Queue _recentLinks = new(); private readonly TimeProvider _clock; public AgentActivity(TimeProvider? clock = null) => _clock = clock ?? TimeProvider.System; public event Action? RunStarted; /// A run ended — with the AgentRunStatus name, or Aborted. public event Action? RunFinished; public event Action? LinkObserved; /// /// Announces a run. Dispose the handle when it ends; call /// first to report how. A delegated run ( deeper than its root) /// also records the link from the caller. /// public RunHandle Begin(string runId, string agentId, string source, string? taskId, DelegationChain? chain) { var now = _clock.GetUtcNow().UtcDateTime; var calledBy = chain is { Depth: > 0 } ? chain.Agents[^2] : null; var run = new ActiveRun(runId, agentId, source, taskId, calledBy, now); AgentLink? link = calledBy is null ? null : new AgentLink(calledBy, agentId, "delegation", now); lock (_lock) { _active[runId] = run; if (link is not null) { _recentLinks.Enqueue(link); while (_recentLinks.Count > RecentLinkCapacity) _recentLinks.Dequeue(); } } Raise(() => RunStarted?.Invoke(run)); if (link is not null) Raise(() => LinkObserved?.Invoke(link)); return new RunHandle(this, run); } /// #697: A run in progress, by its id. public ActiveRun? Find(string runId) { lock (_lock) return _active.GetValueOrDefault(runId); } /// The runs in progress right now. public IReadOnlyList ActiveRuns() { lock (_lock) return [.. _active.Values]; } /// Links observed within , oldest first. public IReadOnlyList RecentLinks(TimeSpan window) { var since = _clock.GetUtcNow().UtcDateTime - window; lock (_lock) return [.. _recentLinks.Where(l => l.AtUtc >= since)]; } private void End(ActiveRun run, string status) { lock (_lock) { if (!_active.Remove(run.RunId)) return; } Raise(() => RunFinished?.Invoke(run, status)); } /// A subscriber that throws must not break the run it is watching. private static void Raise(Action raise) { try { raise(); } catch { /* observers are best effort */ } } public sealed class RunHandle : IDisposable { private readonly AgentActivity _owner; private readonly ActiveRun _run; private string _status = "Aborted"; internal RunHandle(AgentActivity owner, ActiveRun run) { _owner = owner; _run = run; } /// How the run ended. Without it, disposal reports Aborted. public void Complete(string status) => _status = status; public void Dispose() => _owner.End(_run, _status); } }