Befunde aus dem Testbetrieb auf der Linux-VM behoben (#572)
- #578: Abgebrochene Laeufe (Schrittgrenze, Zeitueberschreitung, Abbruch, Ausnahme) tragen ihre bezahlten Token - vorher 0 in RunUsage, fuer das Budget unsichtbar. - #580: WebFetch prueft die aufgeloeste Zieladresse beim Verbinden; ein erlaubter Name, der auf 127.0.0.1 aufloest, wird gesperrt. - #575: Dateien entstehen unter Unix mit 0600 (AtomicFile, Protokolle, FileRW-Anhaengen, state.db), Protokollverzeichnisse mit 0700. - #576: SIGTERM/SIGINT beenden auch mit Fenster geordnet. - #593: OpenRouter-Statusdienst wird gestartet. - #589: Auftragsnachricht nennt aktuelle Zeit, Zeitzone und Termin. - #581: Unvollstaendiges Argument-JSON erreicht das Werkzeug nicht mehr; das Modell bekommt eine verstaendliche Rueckmeldung statt Parsertext. - #591: Weboberflaeche liest die Felder, die API und Ereignisstrom liefern (Freigaben, Protokoll, Taskboard mit Termin, Werkzeug-Einstellungen, Tokennutzung); Ereignisstrom in camelCase. - #592: Chat zeigt Nachrichten nicht mehr doppelt. - #536 (teilweise): A2A-Antworten tragen Absender und Empfaenger, Parser wirft nicht mehr; Datums- und Volltextfilter in Agenten-Chats. - deploy/publish.py: Packager-Pfad net10.0, Schalter --agent-file. - Version 0.1.4. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -137,7 +137,15 @@ public sealed record TaskDto(
|
||||
IReadOnlyList<string> BlockedBy,
|
||||
string Body,
|
||||
DateTime CreatedAt,
|
||||
DateTime UpdatedAt);
|
||||
DateTime UpdatedAt,
|
||||
TaskWhenDto? When = null);
|
||||
|
||||
/// <summary>
|
||||
/// Der Termin einer Aufgabe (<c>cron</c>, <c>every</c>, <c>at</c>) samt Zeitzone. Ohne ihn
|
||||
/// war in der Oberflaeche eine stuendliche Aufgabe von einer einmaligen nicht zu
|
||||
/// unterscheiden (#591).
|
||||
/// </summary>
|
||||
public sealed record TaskWhenDto(string Kind, string Value, string TimeZone);
|
||||
|
||||
/// <summary>Ein eingefrorener Aufruf, der auf Freigabe wartet.</summary>
|
||||
public sealed record StagedCallDto(
|
||||
@@ -148,7 +156,8 @@ public sealed record StagedCallDto(
|
||||
string? Action,
|
||||
string Proposal,
|
||||
string Status,
|
||||
string ArgumentsJson);
|
||||
string ArgumentsJson,
|
||||
DateTime CreatedAt);
|
||||
|
||||
/// <summary>Warum ein Vorschlag abgelehnt wurde.</summary>
|
||||
public sealed record RejectDto(string Reason);
|
||||
|
||||
@@ -119,7 +119,10 @@ internal static class Mapping
|
||||
task.BlockedBy,
|
||||
task.Body,
|
||||
task.CreatedAt,
|
||||
task.UpdatedAt);
|
||||
task.UpdatedAt,
|
||||
task.When is { } when
|
||||
? new TaskWhenDto(when.Kind.ToString().ToLowerInvariant(), when.Value, when.TimeZone)
|
||||
: null);
|
||||
|
||||
/// <summary>
|
||||
/// Ein wartender Aufruf samt seiner eingefrorenen Argumente.
|
||||
@@ -138,7 +141,8 @@ internal static class Mapping
|
||||
call.Action,
|
||||
call.Proposal,
|
||||
call.Status.ToString(),
|
||||
call.ArgumentsJson);
|
||||
call.ArgumentsJson,
|
||||
call.CreatedAt);
|
||||
|
||||
public static UsageDto ToDto(this RunUsage usage)
|
||||
=> new(
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
using ClawdDotNet.Core.Engine;
|
||||
using ClawdDotNet.Web.Contracts;
|
||||
|
||||
namespace ClawdDotNet.Web.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// Baut aus den Chatverläufen der Agenten die Liste der Nachrichten zwischen Agenten (A2A).
|
||||
///
|
||||
/// <para><b>Absender und Empfänger heißen hier, was sie sind.</b> Eine Anfrage steht im
|
||||
/// Verlauf des Empfängers; ihr Absender kommt aus dem Anzeigetext. Die Antwort steht im
|
||||
/// selben Verlauf — Absender ist also der Antwortende, Empfänger der, der zuletzt gefragt
|
||||
/// hat. Vorher trug die Antwort den Antwortenden als <c>target</c> und gar keinen
|
||||
/// Absender, und der Filter der Weboberfläche fand sie beim Fragenden nicht (#536).</para>
|
||||
///
|
||||
/// <para><b>Was hier bewusst noch nicht gelöst ist:</b> Der Absender einer Anfrage wird
|
||||
/// weiterhin aus dem Anzeigetext gelesen. Strukturiert am Eintrag kommt er erst mit
|
||||
/// #207/#208; bis dahin ist das Lesen wenigstens so gebaut, dass ein fehlendes
|
||||
/// Klammerpaar nicht mehr wirft.</para>
|
||||
/// </summary>
|
||||
internal static class AgentCommunicationBuilder
|
||||
{
|
||||
/// <summary>So beginnt eine Nachricht, die ein anderer Agent geschickt hat (AgentEngine).</summary>
|
||||
internal const string Praefix = "[Nachricht von Agent \"";
|
||||
|
||||
public static List<AgentCommunicationDto> Build(
|
||||
IEnumerable<(string AgentId, string DisplayName, IEnumerable<ChatEntry> History)> agents)
|
||||
{
|
||||
var list = new List<AgentCommunicationDto>();
|
||||
|
||||
foreach (var (agentId, displayName, history) in agents)
|
||||
{
|
||||
string? fragendeId = null;
|
||||
string? fragenderName = null;
|
||||
|
||||
foreach (var entry in history)
|
||||
{
|
||||
if (!string.Equals(entry.Source, ChatSource.AgentComm, StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
|
||||
if (entry.Role == "user")
|
||||
{
|
||||
var (id, name, inhalt) = LeseAbsender(entry.Content);
|
||||
fragendeId = id;
|
||||
fragenderName = name;
|
||||
|
||||
list.Add(new AgentCommunicationDto(
|
||||
TargetAgentId: agentId,
|
||||
TargetAgentName: displayName,
|
||||
SourceAgentId: id,
|
||||
SourceAgentName: name,
|
||||
Role: entry.Role,
|
||||
Content: inhalt,
|
||||
Timestamp: entry.Timestamp));
|
||||
}
|
||||
else if (entry.Role == "assistant")
|
||||
{
|
||||
list.Add(new AgentCommunicationDto(
|
||||
TargetAgentId: fragendeId ?? "",
|
||||
TargetAgentName: fragenderName ?? fragendeId ?? "",
|
||||
SourceAgentId: agentId,
|
||||
SourceAgentName: displayName,
|
||||
Role: entry.Role,
|
||||
Content: entry.Content,
|
||||
Timestamp: entry.Timestamp));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return list.OrderBy(c => c.Timestamp).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Liest aus <c>[Nachricht von Agent "Name" (id)]\n\nText</c> Kennung, Name und Text.
|
||||
/// Passt die Form nicht, kommt der Inhalt unverändert und ohne Absender zurück.
|
||||
/// </summary>
|
||||
internal static (string? Id, string? Name, string Inhalt) LeseAbsender(string content)
|
||||
{
|
||||
if (!content.StartsWith(Praefix, StringComparison.Ordinal))
|
||||
return (null, null, content);
|
||||
|
||||
string? name = null;
|
||||
string? id = null;
|
||||
|
||||
var endQuote = content.IndexOf('"', Praefix.Length);
|
||||
if (endQuote > Praefix.Length)
|
||||
{
|
||||
name = content[Praefix.Length..endQuote];
|
||||
|
||||
var openParen = content.IndexOf('(', endQuote);
|
||||
var closeParen = openParen < 0 ? -1 : content.IndexOf(')', openParen);
|
||||
if (openParen > 0 && closeParen > openParen)
|
||||
id = content[(openParen + 1)..closeParen];
|
||||
}
|
||||
|
||||
var doubleNl = content.IndexOf("\n\n", StringComparison.Ordinal);
|
||||
var inhalt = doubleNl > 0 ? content[(doubleNl + 2)..] : content;
|
||||
|
||||
return (id, name, inhalt);
|
||||
}
|
||||
}
|
||||
@@ -107,50 +107,13 @@ internal static class ApiEndpoints
|
||||
if (host.Engine is not { } engine)
|
||||
return Unavailable("Agent-Engine ist nicht aktiv (kein API-Schlüssel).");
|
||||
|
||||
var allAgents = host.AgentAdmin.All;
|
||||
var list = new List<AgentCommunicationDto>();
|
||||
var list = AgentCommunicationBuilder.Build(
|
||||
host.AgentAdmin.All.Select(agent => (
|
||||
agent.AgentId,
|
||||
agent.DisplayName,
|
||||
(IEnumerable<Core.Engine.ChatEntry>)engine.GetChatHistory(agent.AgentId))));
|
||||
|
||||
foreach (var agent in allAgents)
|
||||
{
|
||||
var history = engine.GetChatHistory(agent.AgentId);
|
||||
foreach (var entry in history.Where(e => string.Equals(e.Source, Core.Engine.ChatSource.AgentComm, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
string? senderId = null;
|
||||
string? senderName = null;
|
||||
var content = entry.Content;
|
||||
|
||||
if (entry.Role == "user" && content.StartsWith("[Nachricht von Agent \""))
|
||||
{
|
||||
var endQuote = content.IndexOf('"', 22);
|
||||
if (endQuote > 22)
|
||||
{
|
||||
senderName = content[22..endQuote];
|
||||
var openParen = content.IndexOf('(', endQuote);
|
||||
var closeParen = content.IndexOf(')', openParen);
|
||||
if (openParen > 0 && closeParen > openParen)
|
||||
{
|
||||
senderId = content[(openParen + 1)..closeParen];
|
||||
}
|
||||
}
|
||||
var doubleNl = content.IndexOf("\n\n", StringComparison.Ordinal);
|
||||
if (doubleNl > 0)
|
||||
{
|
||||
content = content[(doubleNl + 2)..];
|
||||
}
|
||||
}
|
||||
|
||||
list.Add(new AgentCommunicationDto(
|
||||
TargetAgentId: agent.AgentId,
|
||||
TargetAgentName: agent.DisplayName,
|
||||
SourceAgentId: senderId,
|
||||
SourceAgentName: senderName,
|
||||
Role: entry.Role,
|
||||
Content: content,
|
||||
Timestamp: entry.Timestamp));
|
||||
}
|
||||
}
|
||||
|
||||
return Results.Ok(list.OrderBy(c => c.Timestamp).ToList());
|
||||
return Results.Ok(list);
|
||||
});
|
||||
|
||||
agents.MapGet("/{agentId}", (AppHost host, string agentId) =>
|
||||
|
||||
@@ -252,6 +252,14 @@ internal static class EventEndpoints
|
||||
context.Response.Headers["X-Accel-Buffering"] = "no";
|
||||
}
|
||||
|
||||
private static string Format(string type, object payload)
|
||||
=> $"event: {type}\ndata: {JsonSerializer.Serialize(payload)}\n\n";
|
||||
/// <summary>
|
||||
/// Ein Ereignis in SSE-Form.
|
||||
///
|
||||
/// <para>Mit den Web-Vorgaben (camelCase) wie jede REST-Antwort (#591). Ohne sie kamen
|
||||
/// Protokollzeilen als <c>Module/Level/Text</c> an, die Ansicht las <c>module/level/text</c>
|
||||
/// — und zeigte von jeder Zeile nur den Instanznamen. Den anonymen Objekten der übrigen
|
||||
/// Ereignisse sah man das nicht an, weil ihre Namen ohnehin klein geschrieben sind.</para>
|
||||
/// </summary>
|
||||
internal static string Format(string type, object payload)
|
||||
=> $"event: {type}\ndata: {JsonSerializer.Serialize(payload, JsonSerializerOptions.Web)}\n\n";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user