- Werkzeugkontext kennt Lauf und Aufgabe des Laufs (RunId, RunTaskId); die Delegationskette traegt den Lauf des Aufrufers weiter (RunId, CallerRunId) - Aufgaben tragen origin_run / origin_task (Datei, DB, Taskboard "Arose from"), gestempelt, nie vom Agenten behauptet - Kommentare nennen den Lauf, in dem sie geschrieben wurden - Laufbeleg: CallerRunId; Laufansicht fuehrt zu "Delegiert von" und "Delegiert an" - #677: task_update ersetzt den Rumpf nicht mehr, Abnahmekriterien aendert nur der Ersteller; Kommentare der Fremdinhalt-Stufe auf fremden Aufgaben kommen gerahmt an - WebUI: Herkunft auf der Aufgabenkarte mit Link zum Lauf Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
140 lines
4.8 KiB
C#
140 lines
4.8 KiB
C#
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>#697: A run in progress, by its id.</summary>
|
|
public ActiveRun? Find(string runId)
|
|
{
|
|
lock (_lock)
|
|
return _active.GetValueOrDefault(runId);
|
|
}
|
|
|
|
/// <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);
|
|
}
|
|
}
|