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,143 @@
|
||||
using ClawdDotNet.Core.Activity;
|
||||
using ClawdDotNet.Core.Tests.Infrastructure;
|
||||
using ClawdDotNet.Core.Tools;
|
||||
using Shouldly;
|
||||
|
||||
namespace ClawdDotNet.Core.Tests.Activity;
|
||||
|
||||
/// <summary>
|
||||
/// #711: Who is working right now and who reached whom — the live side of the team overview.
|
||||
/// </summary>
|
||||
public sealed class AgentActivityTests
|
||||
{
|
||||
private readonly FakeTimeProvider _clock = new(new DateTimeOffset(2027, 3, 10, 12, 0, 0, TimeSpan.Zero));
|
||||
|
||||
[Fact]
|
||||
public void A_run_is_active_until_its_handle_is_disposed()
|
||||
{
|
||||
var activity = new AgentActivity(_clock);
|
||||
|
||||
using (var run = activity.Begin("r1", "worker", "task", "t-1", null))
|
||||
{
|
||||
var active = activity.ActiveRuns().ShouldHaveSingleItem();
|
||||
active.AgentId.ShouldBe("worker");
|
||||
active.TaskId.ShouldBe("t-1");
|
||||
active.CalledBy.ShouldBeNull();
|
||||
run.Complete("Completed");
|
||||
}
|
||||
|
||||
activity.ActiveRuns().ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Start_and_end_are_announced_with_the_status()
|
||||
{
|
||||
var activity = new AgentActivity(_clock);
|
||||
var log = new List<string>();
|
||||
activity.RunStarted += r => log.Add($"start {r.AgentId}");
|
||||
activity.RunFinished += (r, status) => log.Add($"end {r.AgentId} {status}");
|
||||
|
||||
using (var run = activity.Begin("r1", "worker", "chat", null, null))
|
||||
run.Complete("LoopLimitExceeded");
|
||||
using (activity.Begin("r2", "worker", "chat", null, null)) { }
|
||||
|
||||
log.ShouldBe(["start worker", "end worker LoopLimitExceeded", "start worker", "end worker Aborted"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_delegated_run_records_who_called_whom()
|
||||
{
|
||||
var activity = new AgentActivity(_clock);
|
||||
AgentLink? seen = null;
|
||||
activity.LinkObserved += l => seen = l;
|
||||
var chain = DelegationChain.Root("lead").Append("buyer");
|
||||
|
||||
using (activity.Begin("r1", "buyer", "agent_comm", null, chain)) { }
|
||||
|
||||
seen.ShouldNotBeNull();
|
||||
seen.From.ShouldBe("lead");
|
||||
seen.To.ShouldBe("buyer");
|
||||
seen.Kind.ShouldBe("delegation");
|
||||
activity.RecentLinks(TimeSpan.FromMinutes(1)).ShouldHaveSingleItem().From.ShouldBe("lead");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Old_links_fall_out_of_the_recent_window()
|
||||
{
|
||||
var activity = new AgentActivity(_clock);
|
||||
using (activity.Begin("r1", "buyer", "agent_comm", null, DelegationChain.Root("lead").Append("buyer"))) { }
|
||||
|
||||
_clock.Advance(TimeSpan.FromMinutes(11));
|
||||
|
||||
activity.RecentLinks(TimeSpan.FromMinutes(10)).ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_memory_of_links_is_bounded()
|
||||
{
|
||||
var activity = new AgentActivity(_clock);
|
||||
var chain = DelegationChain.Root("lead").Append("buyer");
|
||||
|
||||
for (var i = 0; i < AgentActivity.RecentLinkCapacity + 50; i++)
|
||||
using (activity.Begin($"r{i}", "buyer", "agent_comm", null, chain)) { }
|
||||
|
||||
activity.RecentLinks(TimeSpan.FromDays(1)).Count.ShouldBe(AgentActivity.RecentLinkCapacity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_subscriber_that_throws_does_not_break_the_run()
|
||||
{
|
||||
var activity = new AgentActivity(_clock);
|
||||
activity.RunStarted += _ => throw new InvalidOperationException("broken page");
|
||||
activity.RunFinished += (_, _) => throw new InvalidOperationException("broken page");
|
||||
|
||||
Should.NotThrow(() =>
|
||||
{
|
||||
using var run = activity.Begin("r1", "worker", "chat", null, null);
|
||||
run.Complete("Completed");
|
||||
});
|
||||
activity.ActiveRuns().ShouldBeEmpty();
|
||||
}
|
||||
|
||||
// ─── through the engine ───
|
||||
|
||||
[Fact]
|
||||
public async Task The_engine_announces_a_delegated_run_with_its_caller()
|
||||
{
|
||||
var fixture = new EngineFixture();
|
||||
fixture.AddAgent("lead");
|
||||
fixture.AddAgent("buyer");
|
||||
fixture.Client.RespondsWithText("Erledigt.");
|
||||
|
||||
var started = new List<ActiveRun>();
|
||||
var finished = new List<string>();
|
||||
fixture.Engine.Activity.RunStarted += started.Add;
|
||||
fixture.Engine.Activity.RunFinished += (_, status) => finished.Add(status);
|
||||
|
||||
var result = await fixture.Engine.SendMessageAsync("lead", "buyer", "Bitte pruefen", "inst", null, default);
|
||||
|
||||
result.Delivered.ShouldBeTrue(result.Error);
|
||||
var run = started.ShouldHaveSingleItem();
|
||||
run.AgentId.ShouldBe("buyer");
|
||||
run.CalledBy.ShouldBe("lead");
|
||||
finished.ShouldBe(["Completed"]);
|
||||
fixture.Engine.Activity.ActiveRuns().ShouldBeEmpty();
|
||||
fixture.Engine.Activity.RecentLinks(TimeSpan.FromMinutes(1)).ShouldHaveSingleItem().To.ShouldBe("buyer");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_fresh_run_is_visible_too()
|
||||
{
|
||||
// Before #711 only runs through ChatAsync counted as "running"; RunAsync (@new) did not.
|
||||
var fixture = new EngineFixture();
|
||||
var agent = fixture.AddAgent("worker");
|
||||
fixture.Client.RespondsWithText("Fertig.");
|
||||
|
||||
var started = new List<ActiveRun>();
|
||||
fixture.Engine.Activity.RunStarted += started.Add;
|
||||
|
||||
await fixture.Engine.RunAsync(agent, "Los", "inst", CancellationToken.None);
|
||||
|
||||
started.ShouldHaveSingleItem().AgentId.ShouldBe("worker");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user