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:
@@ -183,6 +183,11 @@ public sealed class AgentEngine : IAgentMessageRouter, Staging.IFrozenCallExecut
|
||||
logger.LogInformation("Agent run started: {AgentId}, model={Model}",
|
||||
agentConfig.AgentId, agentConfig.Model);
|
||||
|
||||
// #578: Vor dem try, damit auch die Abbruchzweige wissen, was bis dahin bezahlt
|
||||
// wurde. Vorher lag sie im try, und ein Lauf an der Schrittgrenze ging mit 0 Token
|
||||
// und 0 USD in RunUsage - fuer das Tagesbudget unsichtbar.
|
||||
var tally = new TokenTally();
|
||||
|
||||
try
|
||||
{
|
||||
var tools = _toolRegistry.GetForAgent(agentConfig);
|
||||
@@ -198,7 +203,6 @@ public sealed class AgentEngine : IAgentMessageRouter, Staging.IFrozenCallExecut
|
||||
|
||||
string? finalMessage = null;
|
||||
var truncated = false;
|
||||
var tally = new TokenTally();
|
||||
var cachingEnabled = PromptCache.IsEnabledFor(agentConfig.PromptCaching, agentConfig.Model);
|
||||
|
||||
while (true)
|
||||
@@ -315,7 +319,7 @@ public sealed class AgentEngine : IAgentMessageRouter, Staging.IFrozenCallExecut
|
||||
|
||||
var result = new AgentRunResult(
|
||||
agentConfig.AgentId, AgentRunStatus.Cancelled, null,
|
||||
loopGuard.Steps, loopGuard.Tokens, sw.Elapsed);
|
||||
loopGuard.Steps, loopGuard.Tokens, sw.Elapsed) with { PromptTokens = tally.Prompt, CompletionTokens = tally.Completion, CachedTokens = tally.Cached, ReportedCostUsd = tally.ReportedCost };
|
||||
OnRunCompleted?.Invoke(agentConfig.Model, result);
|
||||
return result;
|
||||
}
|
||||
@@ -326,7 +330,7 @@ public sealed class AgentEngine : IAgentMessageRouter, Staging.IFrozenCallExecut
|
||||
|
||||
var result = new AgentRunResult(
|
||||
agentConfig.AgentId, AgentRunStatus.Cancelled, null,
|
||||
loopGuard.Steps, loopGuard.Tokens, sw.Elapsed);
|
||||
loopGuard.Steps, loopGuard.Tokens, sw.Elapsed) with { PromptTokens = tally.Prompt, CompletionTokens = tally.Completion, CachedTokens = tally.Cached, ReportedCostUsd = tally.ReportedCost };
|
||||
OnRunCompleted?.Invoke(agentConfig.Model, result);
|
||||
return result;
|
||||
}
|
||||
@@ -338,7 +342,7 @@ public sealed class AgentEngine : IAgentMessageRouter, Staging.IFrozenCallExecut
|
||||
|
||||
var result = new AgentRunResult(
|
||||
agentConfig.AgentId, AgentRunStatus.LoopLimitExceeded, null,
|
||||
loopGuard.Steps, loopGuard.Tokens, sw.Elapsed, ex);
|
||||
loopGuard.Steps, loopGuard.Tokens, sw.Elapsed, ex) with { PromptTokens = tally.Prompt, CompletionTokens = tally.Completion, CachedTokens = tally.Cached, ReportedCostUsd = tally.ReportedCost };
|
||||
OnRunCompleted?.Invoke(agentConfig.Model, result);
|
||||
return result;
|
||||
}
|
||||
@@ -349,7 +353,7 @@ public sealed class AgentEngine : IAgentMessageRouter, Staging.IFrozenCallExecut
|
||||
|
||||
var result = new AgentRunResult(
|
||||
agentConfig.AgentId, AgentRunStatus.Failed, null,
|
||||
loopGuard.Steps, loopGuard.Tokens, sw.Elapsed, ex);
|
||||
loopGuard.Steps, loopGuard.Tokens, sw.Elapsed, ex) with { PromptTokens = tally.Prompt, CompletionTokens = tally.Completion, CachedTokens = tally.Cached, ReportedCostUsd = tally.ReportedCost };
|
||||
OnRunCompleted?.Invoke(agentConfig.Model, result);
|
||||
return result;
|
||||
}
|
||||
@@ -441,6 +445,9 @@ public sealed class AgentEngine : IAgentMessageRouter, Staging.IFrozenCallExecut
|
||||
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(runCt, timeoutCts.Token);
|
||||
var ct = linkedCts.Token;
|
||||
|
||||
// #578: siehe RunCoreAsync - auch Abbrueche tragen ihre bezahlten Token.
|
||||
var tally = new TokenTally();
|
||||
|
||||
try
|
||||
{
|
||||
var tools = _toolRegistry.GetForAgent(agentConfig);
|
||||
@@ -472,7 +479,6 @@ public sealed class AgentEngine : IAgentMessageRouter, Staging.IFrozenCallExecut
|
||||
|
||||
string? finalMessage = null;
|
||||
var truncated = false;
|
||||
var tally = new TokenTally();
|
||||
var cachingEnabled = PromptCache.IsEnabledFor(agentConfig.PromptCaching, agentConfig.Model);
|
||||
|
||||
while (true)
|
||||
@@ -587,7 +593,7 @@ public sealed class AgentEngine : IAgentMessageRouter, Staging.IFrozenCallExecut
|
||||
AddChatEntry(agentConfig.AgentId, "assistant", cancelMsg, source);
|
||||
var result = new AgentRunResult(
|
||||
agentConfig.AgentId, AgentRunStatus.Cancelled, cancelMsg,
|
||||
loopGuard.Steps, loopGuard.Tokens, sw.Elapsed);
|
||||
loopGuard.Steps, loopGuard.Tokens, sw.Elapsed) with { PromptTokens = tally.Prompt, CompletionTokens = tally.Completion, CachedTokens = tally.Cached, ReportedCostUsd = tally.ReportedCost };
|
||||
OnRunCompleted?.Invoke(agentConfig.Model, result);
|
||||
return result;
|
||||
}
|
||||
@@ -599,7 +605,7 @@ public sealed class AgentEngine : IAgentMessageRouter, Staging.IFrozenCallExecut
|
||||
AddChatEntry(agentConfig.AgentId, "assistant", limitMsg, source);
|
||||
var result = new AgentRunResult(
|
||||
agentConfig.AgentId, AgentRunStatus.LoopLimitExceeded, limitMsg,
|
||||
loopGuard.Steps, loopGuard.Tokens, sw.Elapsed, ex);
|
||||
loopGuard.Steps, loopGuard.Tokens, sw.Elapsed, ex) with { PromptTokens = tally.Prompt, CompletionTokens = tally.Completion, CachedTokens = tally.Cached, ReportedCostUsd = tally.ReportedCost };
|
||||
OnRunCompleted?.Invoke(agentConfig.Model, result);
|
||||
return result;
|
||||
}
|
||||
@@ -611,7 +617,7 @@ public sealed class AgentEngine : IAgentMessageRouter, Staging.IFrozenCallExecut
|
||||
AddChatEntry(agentConfig.AgentId, "assistant", errorMsg, source);
|
||||
var result = new AgentRunResult(
|
||||
agentConfig.AgentId, AgentRunStatus.Failed, errorMsg,
|
||||
loopGuard.Steps, loopGuard.Tokens, sw.Elapsed, ex);
|
||||
loopGuard.Steps, loopGuard.Tokens, sw.Elapsed, ex) with { PromptTokens = tally.Prompt, CompletionTokens = tally.Completion, CachedTokens = tally.Cached, ReportedCostUsd = tally.ReportedCost };
|
||||
OnRunCompleted?.Invoke(agentConfig.Model, result);
|
||||
return result;
|
||||
}
|
||||
@@ -1244,6 +1250,18 @@ public sealed class AgentEngine : IAgentMessageRouter, Staging.IFrozenCallExecut
|
||||
return JsonSerializer.Serialize(ToolResult.Fail($"Tool '{toolName}' not found."));
|
||||
}
|
||||
|
||||
// #581: Unvollstaendiges Argument-JSON gar nicht erst ans Tool reichen. Vorher
|
||||
// bekam das Modell den Parsertext ("Expected depth to be zero...") und
|
||||
// wiederholte denselben Aufruf bis zur Schrittgrenze. Beobachtet bei sehr
|
||||
// grossen Argumenten: Das JSON brach genau vor dem grossen Wert ab.
|
||||
if (!string.IsNullOrWhiteSpace(arguments) && !IstVollstaendigesJson(arguments))
|
||||
{
|
||||
var hinweis = UnvollstaendigeArgumente(toolName, arguments, agentConfig.LoopGuard.MaxResponseTokens);
|
||||
logger.LogWarning("Unvollstaendige Argumente fuer {Tool} ({Laenge} Zeichen)", toolName, arguments.Length);
|
||||
await Audit(AuditStatus.Error, hinweis);
|
||||
return JsonSerializer.Serialize(new { error = hinweis });
|
||||
}
|
||||
|
||||
// Staging-Durchsetzung (A2): irreversible Aktionen werden vorgeschlagen statt
|
||||
// ausgeführt. Eine Prompt-Injection kann so nur einen Vorschlag erzeugen.
|
||||
if (_stagingGate is not null)
|
||||
@@ -1332,6 +1350,33 @@ public sealed class AgentEngine : IAgentMessageRouter, Staging.IFrozenCallExecut
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Ob die Argumente eines Werkzeugaufrufs vollstaendiges JSON sind (#581).</summary>
|
||||
internal static bool IstVollstaendigesJson(string arguments)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var _ = JsonDocument.Parse(arguments);
|
||||
return true;
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Die Rueckmeldung ans Modell, wenn Argumente abgeschnitten ankamen (#581): was los
|
||||
/// ist und was stattdessen zu tun ist - nicht der Text eines JSON-Parsers.
|
||||
/// </summary>
|
||||
internal static string UnvollstaendigeArgumente(string toolName, string arguments, int maxResponseTokens)
|
||||
{
|
||||
var ende = arguments.Length <= 40 ? arguments : "..." + arguments[^40..];
|
||||
return $"Die Argumente fuer '{toolName}' sind kein vollstaendiges JSON ({arguments.Length} Zeichen, "
|
||||
+ $"enden mit: {ende}). Meist war ein Argument zu gross fuer eine einzelne Antwort "
|
||||
+ $"(Ausgabegrenze {maxResponseTokens} Tokens) und wurde abgeschnitten. Denselben Aufruf "
|
||||
+ "nicht wiederholen - den Inhalt auf mehrere kleinere Aufrufe verteilen.";
|
||||
}
|
||||
|
||||
private AgentToolContext BuildToolContext(
|
||||
AgentConfig agentConfig, string instanceId, string toolName, CancellationToken ct,
|
||||
DelegationChain? chain = null)
|
||||
|
||||
Reference in New Issue
Block a user