Die Betriebskontrolle im Testbetrieb meldete Stundendateien als fehlend, die es gab: 105 Eintraege in der Reihenfolge des Dateisystems, vierzeilig als eingeruecktes JSON. list liefert jetzt die 50 zuletzt geaenderten Eintraege, neueste zuerst, eine Zeile je Eintrag, mit Gesamtzahl im Kopf; pattern, sort, order und limit steuern das. Dabei aufgefallen (#675): Directory.GetFiles wertet Trennzeichen im Muster aus - pattern "../*" fuehrte an der Pfadpruefung vorbei aus dem Verzeichnis heraus, in list wie in read_many. Muster mit '/', '\' oder '..' werden jetzt abgewiesen. Und derselbe Befund ist keine zweite Aufgabe (#674): task_find beantwortet die Frage vor dem Anlegen, task_create legt bei gleichbedeutendem Titel einer offenen Aufgabe keine zweite an, sondern vermerkt das erneute Auftreten an der bestehenden. force=true geht trotzdem, Erledigtes blockiert nichts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,9 @@ public sealed class FileRWTool : IAgentTool, IToolStateProbe
|
||||
"'read_many' liest mehrere Dateien in EINEM Aufruf - entweder 'paths' (Liste) oder 'path' " +
|
||||
"(Verzeichnis) mit 'pattern' (z.B. \"berlin_2026-09-20_*.txt\"). Nimm das, statt viele " +
|
||||
"Dateien einzeln zu lesen: jeder Einzelaufruf kostet einen Schritt.\n" +
|
||||
"'list' zeigt standardmäßig die 50 zuletzt geänderten Einträge, neueste zuerst; " +
|
||||
"'pattern', 'sort', 'order' und 'limit' steuern das. Frage nach einer bestimmten Datei " +
|
||||
"lieber mit 'pattern', statt eine lange Liste zu durchsuchen.\n" +
|
||||
"Bei copy kann zwischen Workspaces kopiert werden.\n" +
|
||||
"'stock_add' fügt einen neuen Datenpunkt zur Aktien-Wissensdatenbank hinzu (shared:stocks/{ticker}/). " +
|
||||
"Geschützte Pfade (protectedPaths) erlauben nur das Erstellen neuer Dateien — kein Überschreiben oder Löschen.";
|
||||
@@ -64,7 +67,21 @@ public sealed class FileRWTool : IAgentTool, IToolStateProbe
|
||||
},
|
||||
"pattern": {
|
||||
"type": "string",
|
||||
"description": "Nur für read_many: Dateimuster im Verzeichnis 'path', z.B. 'berlin_2026-09-20_*.txt'. Ohne Muster werden alle Dateien des Verzeichnisses gelesen."
|
||||
"description": "Für read_many und list: Dateimuster im Verzeichnis 'path', z.B. 'berlin_2026-09-20_*.txt'. Ohne Muster gilt das ganze Verzeichnis."
|
||||
},
|
||||
"sort": {
|
||||
"type": "string",
|
||||
"enum": ["modified", "name"],
|
||||
"description": "Nur für list: Sortierung. Standard 'modified' (neueste zuerst)."
|
||||
},
|
||||
"order": {
|
||||
"type": "string",
|
||||
"enum": ["desc", "asc"],
|
||||
"description": "Nur für list: Richtung. Standard absteigend bei 'modified', aufsteigend bei 'name'."
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Nur für list: Höchstzahl der Einträge. Standard 50, Maximum 500. Der Kopf der Antwort nennt immer die Gesamtzahl."
|
||||
},
|
||||
"destinationWorkspace": {
|
||||
"type": "string",
|
||||
@@ -520,6 +537,8 @@ public sealed class FileRWTool : IAgentTool, IToolStateProbe
|
||||
var muster = input.TryGetProperty("pattern", out var m) && m.ValueKind == JsonValueKind.String
|
||||
? m.GetString()
|
||||
: null;
|
||||
if (MusterFehler(muster) is { } musterFehler)
|
||||
return ([], musterFehler);
|
||||
|
||||
var gefunden = Directory.GetFiles(verzeichnis, string.IsNullOrWhiteSpace(muster) ? "*" : muster)
|
||||
.OrderBy(f => f, StringComparer.Ordinal)
|
||||
@@ -616,6 +635,25 @@ public sealed class FileRWTool : IAgentTool, IToolStateProbe
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Wie viele Eintraege ein <c>list</c> ohne ausdrueckliches <c>limit</c> zeigt.</summary>
|
||||
public const int StandardEintraege = 50;
|
||||
|
||||
/// <summary>Obergrenze fuer <c>limit</c> - darueber wird die Antwort zur Heuhaufensuche.</summary>
|
||||
public const int MaxEintraege = 500;
|
||||
|
||||
/// <summary>
|
||||
/// #673: Verzeichnis auflisten - sortiert, gedeckelt, kompakt.
|
||||
///
|
||||
/// <para>Anlass: Die Betriebskontrolle im Testbetrieb meldete Stundendateien als fehlend,
|
||||
/// die es gab. Das Verzeichnis lieferte 105 Eintraege in der Reihenfolge des Dateisystems,
|
||||
/// vierzeilig als eingeruecktes JSON. Darin zwei bestimmte Namen zu finden ist Sucharbeit,
|
||||
/// und ein guenstiges Modell erledigt sie falsch.</para>
|
||||
///
|
||||
/// <para>Also: neueste zuerst (umstellbar ueber <c>sort</c> und <c>order</c>), <c>pattern</c>
|
||||
/// zum Eingrenzen, eine Zeile je Eintrag - und der Kopf sagt, wie viele Eintraege es
|
||||
/// insgesamt gibt. Eine gedeckelte Liste, die ihre Deckelung verschweigt, waere die
|
||||
/// schlimmere Falle.</para>
|
||||
/// </summary>
|
||||
private async Task<ToolResult> HandleListAsync(JsonElement input, string rootPath, string workspace, AgentToolContext context, CancellationToken ct)
|
||||
{
|
||||
var path = GetAndValidatePath(input, rootPath, workspace, false, context);
|
||||
@@ -626,21 +664,91 @@ public sealed class FileRWTool : IAgentTool, IToolStateProbe
|
||||
+ AndererWorkspaceHinweis("list", workspace, RelativerPfad(input), context, verzeichnis: true));
|
||||
}
|
||||
|
||||
var relativeDirPath = Path.GetRelativePath(rootPath, path);
|
||||
var entries = Directory.GetFileSystemEntries(path)
|
||||
.Select(e => new
|
||||
{
|
||||
Name = Path.GetFileName(e),
|
||||
Type = Directory.Exists(e) ? "directory" : "file",
|
||||
Size = Directory.Exists(e) ? 0 : new FileInfo(e).Length,
|
||||
LastModified = File.GetLastWriteTime(e)
|
||||
})
|
||||
var muster = GetOptionalString(input, "pattern");
|
||||
if (MusterFehler(muster) is { } musterFehler)
|
||||
return ToolResult.Fail(musterFehler);
|
||||
|
||||
List<string> pfade;
|
||||
try
|
||||
{
|
||||
pfade = Directory.GetFileSystemEntries(path, string.IsNullOrWhiteSpace(muster) ? "*" : muster).ToList();
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
return ToolResult.Fail($"Ungueltiges Muster '{muster}': {ex.Message}");
|
||||
}
|
||||
|
||||
var eintraege = pfade
|
||||
.Select(e => new Verzeichniseintrag(
|
||||
Path.GetFileName(e),
|
||||
Directory.Exists(e),
|
||||
Directory.Exists(e) ? 0 : new FileInfo(e).Length,
|
||||
File.GetLastWriteTime(e)))
|
||||
.ToList();
|
||||
|
||||
var header = $"[{workspace} Workspace] Verzeichnis: {workspace}:/{(relativeDirPath == "." ? "" : relativeDirPath)}\n";
|
||||
return ToolResult.Ok(header + JsonSerializer.Serialize(entries, new JsonSerializerOptions { WriteIndented = true }));
|
||||
var nachName = string.Equals(GetOptionalString(input, "sort"), "name", StringComparison.OrdinalIgnoreCase);
|
||||
var absteigend = GetOptionalString(input, "order") is { } o
|
||||
? string.Equals(o, "desc", StringComparison.OrdinalIgnoreCase)
|
||||
: !nachName; // neueste zuerst, Namen aufsteigend
|
||||
|
||||
IEnumerable<Verzeichniseintrag> sortiert = nachName
|
||||
? (absteigend
|
||||
? eintraege.OrderByDescending(e => e.Name, StringComparer.OrdinalIgnoreCase)
|
||||
: eintraege.OrderBy(e => e.Name, StringComparer.OrdinalIgnoreCase))
|
||||
: (absteigend
|
||||
? eintraege.OrderByDescending(e => e.Geaendert)
|
||||
: eintraege.OrderBy(e => e.Geaendert));
|
||||
|
||||
var grenze = Math.Clamp(GetInt(input, "limit") ?? StandardEintraege, 1, MaxEintraege);
|
||||
var gezeigt = sortiert.Take(grenze).ToList();
|
||||
|
||||
var relativeDirPath = Path.GetRelativePath(rootPath, path);
|
||||
var ordnung = nachName
|
||||
? (absteigend ? "Name absteigend" : "Name aufsteigend")
|
||||
: (absteigend ? "neueste zuerst" : "aelteste zuerst");
|
||||
|
||||
var text = new StringBuilder();
|
||||
text.Append($"[{workspace} Workspace] Verzeichnis: {workspace}:/{(relativeDirPath == "." ? "" : relativeDirPath)}");
|
||||
text.Append($" - {gezeigt.Count} von {eintraege.Count} Eintraegen, {ordnung}.");
|
||||
if (!string.IsNullOrWhiteSpace(muster))
|
||||
text.Append($" Muster: {muster}.");
|
||||
if (eintraege.Count > gezeigt.Count)
|
||||
text.Append($"\n{eintraege.Count - gezeigt.Count} weitere nicht gezeigt - grenze mit 'pattern' ein oder setze 'limit' (max {MaxEintraege}).");
|
||||
text.Append("\nGeaendert Groesse Name\n");
|
||||
|
||||
foreach (var e in gezeigt)
|
||||
{
|
||||
var groesse = e.IstVerzeichnis ? "<DIR>" : e.Groesse.ToString();
|
||||
text.Append($"{e.Geaendert:yyyy-MM-dd HH:mm} {groesse,9} {e.Name}{(e.IstVerzeichnis ? "/" : "")}\n");
|
||||
}
|
||||
|
||||
return await Task.FromResult(ToolResult.Ok(text.ToString().TrimEnd()));
|
||||
}
|
||||
|
||||
private readonly record struct Verzeichniseintrag(string Name, bool IstVerzeichnis, long Groesse, DateTime Geaendert);
|
||||
|
||||
/// <summary>
|
||||
/// Ein Muster ist ein Dateiname mit Platzhaltern, kein Pfad. <c>Directory.GetFiles</c>
|
||||
/// wertet Trennzeichen im Muster aus — <c>pattern: "../*"</c> listet damit das
|
||||
/// Elternverzeichnis, an der Pfadpruefung vorbei. Deshalb hier und nicht erst dort.
|
||||
/// </summary>
|
||||
private static string? MusterFehler(string? muster)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(muster)) return null;
|
||||
|
||||
if (muster.Contains('/') || muster.Contains('\\') || muster.Contains(".."))
|
||||
return $"Ungueltiges Muster '{muster}': ein Muster ist ein Dateiname mit Platzhaltern "
|
||||
+ "(z.B. 'berlin_2026-09-21_*.txt'), kein Pfad. Das Verzeichnis gehoert in 'path'.";
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string? GetOptionalString(JsonElement input, string name)
|
||||
=> input.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.String ? v.GetString() : null;
|
||||
|
||||
private static int? GetInt(JsonElement input, string name)
|
||||
=> input.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.Number && v.TryGetInt32(out var i) ? i : null;
|
||||
|
||||
private async Task<ToolResult> HandleDeleteAsync(JsonElement input, string rootPath, string workspace, AgentToolContext context, CancellationToken ct)
|
||||
{
|
||||
var path = GetAndValidatePath(input, rootPath, workspace, false, context);
|
||||
|
||||
Reference in New Issue
Block a user