backend: serve single-agent task templates with their status and results, rename the kinds in storage

This commit is contained in:
justme committed 2026-10-22 07:02:28 +08:00
1 parent e77880d372
commit 3a9ea46d6f
45 files changed
+763 -216

No files matched your search

@@ -45,3 +45,12 @@ public sealed record CompareResult(
string Metric,
QueryRange Range,
SourcePlan Plan);
/// <summary>单个 Agent 任务(目标任务)的时序:和线路时序同一套分层与汇总,只是没有线路、事件和评分。</summary>
public sealed record TargetSeriesResult(
QueryRange Range,
SourcePlan Plan,
IReadOnlyList<long> T,
IReadOnlyDictionary<string, double?[]> Values,
SummaryResult Summary,
IReadOnlyDictionary<string, string> Versions);
@@ -138,6 +138,61 @@ public sealed class QueryService
return Result<SeriesResult>.Ok(new SeriesResult(line, p.Range, p.Plan, bucketed.T, values, summary, gaps, events, versions));
}
/// <summary>某个 Agent 上一个单个 Agent 任务的时序(序列 <c>target:&lt;任务 id&gt;</c>)。</summary>
public async ValueTask<Result<TargetSeriesResult>> TargetSeriesAsync(NodeId agent, TaskId task, RangeRequest request, MetricSelection selection, CancellationToken ct)
{
Result<(QueryRange Range, SourcePlan Plan)> planned = Plan(request, "3h");
if (!planned.TryGet(out (QueryRange Range, SourcePlan Plan) p, out FleetError? error))
{
return Result<TargetSeriesResult>.Fail(error);
}
Result<IReadOnlyList<SourceRow>> loaded = await _loader.LoadAsync(p.Plan, p.Range, agent, TargetSeries(task), ct);
if (!loaded.TryGet(out IReadOnlyList<SourceRow>? rows, out error))
{
return Result<TargetSeriesResult>.Fail(error);
}
BucketedRows bucketed = BucketedRows.Create(rows, p.Range);
int n = p.Range.Points;
var values = selection.ExportIds.ToDictionary(id => id, _ => new double?[n], StringComparer.Ordinal);
double widthSeconds = p.Plan.LevelWidthMs / 1000.0;
for (int i = 0; i < n; i++)
{
if (bucketed.Sets[i] is not { } set)
{
continue;
}
Dictionary<string, double> exported = ExportEvaluator.Evaluate(set, selection.ExportIds, bucketed.RowCounts[i] * widthSeconds);
foreach (string id in selection.ExportIds)
{
if (exported.TryGetValue(id, out double v))
{
values[id][i] = v;
}
}
}
var versions = new Dictionary<string, string>(StringComparer.Ordinal);
foreach (string id in selection.ExportIds)
{
if (MetricRegistry.TryFindExport(id, out MetricDef def, out _))
{
versions[id] = def.Fingerprint;
}
}
SummaryResult summary = Summarize(bucketed.Merge(), bucketed.TotalRows, p.Plan, p.Range, selection, new Dictionary<string, double>());
return Result<TargetSeriesResult>.Ok(new TargetSeriesResult(p.Range, p.Plan, bucketed.T, values, summary, versions));
}
/// <summary>这个单个 Agent 任务最后一个样本的时刻(Unix 毫秒);还没有数据返回 null。</summary>
public ValueTask<long?> TargetLatestAsync(NodeId agent, TaskId task, CancellationToken ct) =>
_loader.LatestSampleMsAsync(agent, TargetSeries(task), _time.GetUtcNow().ToUnixTimeMilliseconds(), ct);
private static string TargetSeries(TaskId task) => "target:" + task.Value;
/// <summary>这条线路最早的数据(Unix 毫秒);没有数据返回 null。</summary>
public ValueTask<long?> EarliestAsync(LineKey line, CancellationToken ct) =>
_loader.EarliestSampleMsAsync(line.Listener, line.Series, _time.GetUtcNow().ToUnixTimeMilliseconds(), ct);
@@ -0,0 +1,39 @@
using Fleet.Server;
namespace MoShiro.Backend.Application;
/// <summary>一个 Agent 上正在(或应该)运行的单个 Agent 任务:这个 Agent 的有效任务集里 <c>scope=target</c> 的那些。</summary>
public sealed record TargetView(AgentView Agent, EffectiveTask Task);
/// <summary>单个 Agent 任务目录:按 Agent 展开模板、覆盖和专属任务,列出每个 Agent 实际执行的目标。</summary>
public sealed class TargetService
{
private readonly AgentService _agents;
private readonly ITaskStore _tasks;
public TargetService(AgentService agents, ITaskStore tasks)
{
_agents = agents;
_tasks = tasks;
}
public async ValueTask<IReadOnlyList<TargetView>> ListAsync(Func<AgentView, bool> include, CancellationToken ct)
{
IReadOnlyList<TaskRecord> all = await _tasks.ListAsync(ct);
var result = new List<TargetView>();
foreach (AgentView view in await _agents.ListAsync(ct))
{
if (view.Agent.Status != NodeStatus.Active || !include(view))
{
continue;
}
foreach (EffectiveTask task in EffectiveTasks.Compute(all, view.Agent, filterByAgentKinds: false).Where(t => t.Scope == EffectiveTasks.TargetScope))
{
result.Add(new TargetView(view, task));
}
}
return result;
}
}