Lagebild, Stufe 0: Teamuebersicht in der WebUI (#711)
Wer arbeitet, wer hat in den letzten Sekunden wen erreicht, und wo klemmt es, auf einen Blick. Stufe 0 kommt ohne Einheiten aus; mit #683 werden die Agenten zu aufklappbaren Containern, und die Ansicht faellt mit #688 zusammen. Kern: - AgentActivity: fuehrt die laufenden Laeufe (Beginn, Quelle, Aufgabe, Aufrufer) und die letzten Verbindungen (begrenzt auf 500), meldet Start, Ende und Verbindung als Ereignis. Vorher war nur ein Lauf ueber ChatAsync "laufend"; ein frischer Lauf (@new) war unsichtbar, und niemand hielt fest, wer wen gerufen hatte. - Die Engine meldet beide Einstiege an; "arbeitet" beginnt hinter dem Agententor. Anwendung: - TeamOverview stellt den Lagebericht zusammen. Die Zustaende entscheidet der Code aus erfassten Tatsachen: laufender Lauf, offene Freigabe, Status des letzten Belegs, Laufzeit gegen den Median des Agenten (haengt ab dem Dreifachen, fruehestens nach 5 Minuten, ohne Verlauf nach 15). Rangfolge: haengt, wartet auf Freigabe, arbeitet; Problem und Budget nur, solange nichts Neues laeuft. Web: - GET /api/team, Ereignis "activity" (started, finished, link); das Ereignis "run" traegt jetzt die Agenten-Id. - Ansicht #/team mit Cytoscape.js und ELK (bei Bedarf geladen): Layout aus der Struktur statt von Hand, Farbe und Symbol je Zustand, Kanten verblassen nach 30 s, Klick oeffnet eine Seitenleiste (laufende Laeufe, Freigaben, letzter Lauf im Wortlaut) mit "Freigaben oeffnen" vorne, danach Chat und Laeufe. Sprachpakete en/de. - Ein Layout, das lief, solange die Flaeche noch 0 px breit war, wird neu gerechnet, sobald sie eine Breite hat (im Pruefgestell gefunden). Geprueft im Browser mit einem Pruefgestell (API und Ereignisstrom durch Attrappen ersetzt): sieben Agenten in allen Zustaenden, Seitenleiste, Live-Kante und Verblassen. Tests: AgentActivityTests (8), TeamOverviewTests (14). 1.283 gruen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
d587dc3f20
commit
f627918729
@@ -0,0 +1,132 @@
|
||||
using ClawdDotNet.Core.Tools;
|
||||
|
||||
namespace ClawdDotNet.Core.Activity;
|
||||
|
||||
/// <summary>A run that is in progress right now.</summary>
|
||||
public sealed record ActiveRun(
|
||||
string RunId,
|
||||
string AgentId,
|
||||
string Source,
|
||||
string? TaskId,
|
||||
string? CalledBy,
|
||||
DateTime StartedAtUtc);
|
||||
|
||||
/// <summary>
|
||||
/// One agent reaching another — for now a delegated run (<see cref="Kind"/> =
|
||||
/// <c>delegation</c>). Meetings and escalations will add their own kinds.
|
||||
/// </summary>
|
||||
public sealed record AgentLink(string From, string To, string Kind, DateTime AtUtc);
|
||||
|
||||
/// <summary>
|
||||
/// #711: Who is working right now, and who reached whom — the live side of the team overview.
|
||||
///
|
||||
/// <para><b>Why a component of its own.</b> The engine already tracked running chats, but
|
||||
/// only those that went through <c>ChatAsync</c>; a fresh run (<c>@new</c>) was invisible to
|
||||
/// <c>IsRunning</c>. 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.</para>
|
||||
///
|
||||
/// <para><b>Memory only, and bounded.</b> This is the view of "now". What happened
|
||||
/// yesterday is in the audit log; the time line (#711, later) replays it from there.</para>
|
||||
/// </summary>
|
||||
public sealed class AgentActivity
|
||||
{
|
||||
/// <summary>How many recent links are kept for a page that connects late.</summary>
|
||||
public const int RecentLinkCapacity = 500;
|
||||
|
||||
private readonly Lock _lock = new();
|
||||
private readonly Dictionary<string, ActiveRun> _active = new(StringComparer.Ordinal);
|
||||
private readonly Queue<AgentLink> _recentLinks = new();
|
||||
private readonly TimeProvider _clock;
|
||||
|
||||
public AgentActivity(TimeProvider? clock = null) => _clock = clock ?? TimeProvider.System;
|
||||
|
||||
public event Action<ActiveRun>? RunStarted;
|
||||
|
||||
/// <summary>A run ended — with the <c>AgentRunStatus</c> name, or <c>Aborted</c>.</summary>
|
||||
public event Action<ActiveRun, string>? RunFinished;
|
||||
|
||||
public event Action<AgentLink>? LinkObserved;
|
||||
|
||||
/// <summary>
|
||||
/// Announces a run. Dispose the handle when it ends; call <see cref="RunHandle.Complete"/>
|
||||
/// first to report how. A delegated run (<paramref name="chain"/> deeper than its root)
|
||||
/// also records the link from the caller.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>The runs in progress right now.</summary>
|
||||
public IReadOnlyList<ActiveRun> ActiveRuns()
|
||||
{
|
||||
lock (_lock)
|
||||
return [.. _active.Values];
|
||||
}
|
||||
|
||||
/// <summary>Links observed within <paramref name="window"/>, oldest first.</summary>
|
||||
public IReadOnlyList<AgentLink> 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));
|
||||
}
|
||||
|
||||
/// <summary>A subscriber that throws must not break the run it is watching.</summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>How the run ended. Without it, disposal reports <c>Aborted</c>.</summary>
|
||||
public void Complete(string status) => _status = status;
|
||||
|
||||
public void Dispose() => _owner.End(_run, _status);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user