Die Avalonia-Portierung ist abgeschlossen, damit ist die in ClawdDotNet.slnx angekuendigte Aufgabe "WinForms-Oberflaeche entfernen" faellig. Der Stand davor liegt unter dem Tag vor-fruehjahrsputz-2026-08. Entfernt (56 Dateien, seit dem Herausloesen der Anwendungsschicht nicht mehr Teil des Builds): ClawdDotNet.csproj, Program.cs, sieben frm_*-Formulare, UI/, Models/, EmbeddedUI/, Properties/, Resources/, Services/, das alte Anwendungssymbol und Deploy-Build.ps1 (ersetzt durch deploy/publish.py). Dazu configs/*.json - Beispielkonfigurationen aus der Zeit vor dem Instanzverzeichnis, auf die nur noch die alten Prompts verwiesen. Die vier Entwicklungs-Prompts der Anfangszeit ziehen nach docs/archiv/ um, mit README, das ihren Stand einordnet. Eine Regel darin gilt weiter - die Pflichtfelder fetchedAt/dataAsOf/source der Internet-Tools -, deshalb Archiv statt Loeschen; der WebSearch-Plan verweist auf den neuen Pfad. Toter Code - PlaceholderPageViewModel samt Ansicht: Es gibt keinen Platzhalter-Bereich mehr, seit alle neun Seiten portiert sind. - Snappier als direkter Paketverweis: MongoDB.Driver loest es ohnehin auf dieselbe Fassung auf, der Verweis hob nichts an. Zwei Fehler, die dabei sichtbar wurden - Die taegliche Sicherung lief ins Leere. Die Oberflaeche bot sie an und schrieb Uhrzeit, Zielordner und Anzahl in die Einstellungen, aber der BackupScheduler wurde nirgends erzeugt. Jetzt am AppHost verdrahtet und in den geordneten Abbau aufgenommen. - SettingsPageViewModel hielt die vier Sicherungs-Einstellungen doppelt. Aus der Ansicht waren sie laengst verschwunden, gelesen und beim Speichern zurueckgeschrieben wurden sie weiter: Wer die Uhrzeit auf der Sicherungs-Seite aenderte und danach die Einstellungen speicherte, bekam den alten Wert zurueck. Pakete: keine bekannten Sicherheitsluecken mehr - SQLitePCLRaw.bundle_e_sqlite3 auf 2.1.13 angehoben. Microsoft.Data.Sqlite bringt 2.1.11 mit, darin steckt GHSA-2m69-gcr7-jv3q (NU1903, hoch). - SharpCompress bleibt als direkter Verweis stehen. Beim Aufraeumen erst als ungenutzt entfernt - dabei kam die von MongoDB.Driver gezogene Fassung 0.30.1 mit GHSA-6c8g-7p36-r338 zurueck. Der Verweis ist eine Anhebung, kein Ballast; das steht jetzt als Kommentar dabei. Dokumentation - Roadmap mit Statusblock: A1 und A3 erledigt, A2 nur zur Haelfte - Gate, Policy und Dienst greifen, aber keine Ansicht ruft ApproveAsync auf, ein gestagter Aufruf liegt unbeantwortet. Das ist jetzt Punkt 1 der Reihung. Rocket.Chat steht und kollidiert mit A5 (Matrix) - Entscheidung faellig. - Avalonia-Portierungsleitfaden -> Oberflaechen-Leitfaden: kein Auftrag mehr, sondern Beschreibung des Stands. - Bestandsaufnahme und Linux-Analyse als datierte Befunde gekennzeichnet; der teure Teil der Linux-Analyse (8.900 Zeilen WinForms) ist hinfaellig. - Verweise auf frm_*, WebView2 und ClawdDotNet.csproj in den lebenden Dokumenten richtiggestellt. Build fehlerfrei, 585 Tests gruen (6 uebersprungen). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
467 lines
14 KiB
Markdown
467 lines
14 KiB
Markdown
# ClawdDotNet – Tool-Entwicklungsanleitung
|
||
|
||
## Übersicht
|
||
|
||
Tools sind eigenständige Plugins, die Agenten Zugriff auf externe Systeme geben (Datenbanken, Dateisysteme, E-Mail, APIs etc.). Jedes Tool ist ein separates .NET-Projekt, das ausschließlich `ClawdDotNet.Core` referenziert.
|
||
|
||
**Kernregeln:**
|
||
- Tools sind **niemals voneinander abhängig** (Tool A darf Tool B nicht kennen)
|
||
- Der Core kompiliert und läuft **ohne jedes Tool**
|
||
- Jedes Tool ist **pro Agent konfiguriert** (via `AgentConfig.Tools`)
|
||
- Tools werden zur Laufzeit über die `ToolRegistry` registriert
|
||
|
||
---
|
||
|
||
## Projekt-Struktur
|
||
|
||
```
|
||
ClawdDotNet.sln
|
||
├── src/
|
||
│ ├── ClawdDotNet.Core/ ← Klassenbibliothek (keine Tool-Verweise!)
|
||
│ ├── ClawdDotNet.Tools.MeinTool/ ← Dein Tool-Plugin
|
||
│ │ ├── ClawdDotNet.Tools.MeinTool.csproj
|
||
│ │ └── MeinToolTool.cs
|
||
│ ├── ClawdDotNet.App/ ← Fachschicht ohne Oberflaeche (verweist auf Core + alle Tools)
|
||
│ └── ClawdDotNet.Desktop/ ← Oberflaeche in Avalonia (verweist auf App)
|
||
```
|
||
|
||
### Neues Tool-Projekt anlegen
|
||
|
||
```xml
|
||
<!-- ClawdDotNet.Tools.MeinTool.csproj -->
|
||
<Project Sdk="Microsoft.NET.Sdk">
|
||
<PropertyGroup>
|
||
<TargetFramework>net10.0</TargetFramework>
|
||
<Nullable>enable</Nullable>
|
||
<ImplicitUsings>enable</ImplicitUsings>
|
||
<RootNamespace>ClawdDotNet.Tools.MeinTool</RootNamespace>
|
||
</PropertyGroup>
|
||
|
||
<ItemGroup>
|
||
<!-- NUR Core referenzieren, KEINE anderen Tools -->
|
||
<ProjectReference Include="..\ClawdDotNet.Core\ClawdDotNet.Core.csproj" />
|
||
</ItemGroup>
|
||
|
||
<ItemGroup>
|
||
<!-- Tool-spezifische NuGet-Packages hier -->
|
||
</ItemGroup>
|
||
</Project>
|
||
```
|
||
|
||
---
|
||
|
||
## IAgentTool implementieren
|
||
|
||
Jedes Tool implementiert das Interface `ClawdDotNet.Core.Tools.IAgentTool`:
|
||
|
||
```csharp
|
||
using System.Text.Json;
|
||
using ClawdDotNet.Core.Tools;
|
||
|
||
namespace ClawdDotNet.Tools.MeinTool;
|
||
|
||
public sealed class MeinToolTool : IAgentTool
|
||
{
|
||
// 1. Eindeutiger Name – wird vom LLM in tool_calls verwendet
|
||
public string Name => "MeinTool";
|
||
|
||
// 2. Beschreibung für das LLM (geht in den System-Prompt)
|
||
public string Description => "Beschreibung, was dieses Tool kann...";
|
||
|
||
// 3. JSON Schema des Input-Objekts (OpenAI Function Calling Format)
|
||
public JsonElement InputSchema { get; } = JsonDocument.Parse("""
|
||
{
|
||
"type": "object",
|
||
"properties": {
|
||
"action": {
|
||
"type": "string",
|
||
"enum": ["read", "write"],
|
||
"description": "Die auszuführende Aktion"
|
||
},
|
||
"data": {
|
||
"type": "string",
|
||
"description": "Eingabedaten"
|
||
}
|
||
},
|
||
"required": ["action"]
|
||
}
|
||
""").RootElement.Clone();
|
||
|
||
// 4. Ausführung
|
||
public async Task<ToolResult> ExecuteAsync(
|
||
JsonElement input,
|
||
AgentToolContext context,
|
||
CancellationToken ct)
|
||
{
|
||
// Action aus dem Input lesen
|
||
var action = input.GetProperty("action").GetString()
|
||
?? throw new ArgumentException("'action' is required");
|
||
|
||
// Konfiguration aus dem AgentToolContext lesen
|
||
// NIEMALS hardcodierte Werte, immer aus context.ToolConfig!
|
||
var meineSetting = context.ToolConfig.TryGetValue("meineSetting", out var val)
|
||
? val?.ToString() ?? ""
|
||
: "";
|
||
|
||
// Logger verwenden
|
||
context.Logger.LogInformation("MeinTool executing action: {Action}", action);
|
||
|
||
return action switch
|
||
{
|
||
"read" => await HandleReadAsync(input, context, ct),
|
||
"write" => await HandleWriteAsync(input, context, ct),
|
||
_ => ToolResult.Fail($"Unknown action: {action}")
|
||
};
|
||
}
|
||
|
||
private async Task<ToolResult> HandleReadAsync(
|
||
JsonElement input, AgentToolContext context, CancellationToken ct)
|
||
{
|
||
// Implementierung...
|
||
await Task.CompletedTask; // Platzhalter
|
||
return ToolResult.Ok("Ergebnis als JSON oder Text");
|
||
}
|
||
|
||
private async Task<ToolResult> HandleWriteAsync(
|
||
JsonElement input, AgentToolContext context, CancellationToken ct)
|
||
{
|
||
// Implementierung...
|
||
await Task.CompletedTask; // Platzhalter
|
||
return ToolResult.Ok("Erfolgreich geschrieben");
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Konfiguration pro Agent
|
||
|
||
Die Tool-Konfiguration ist **pro Agent** definiert, nicht global. Das bedeutet: Agent A kann auf Datenbank X zugreifen, Agent B auf Datenbank Y.
|
||
|
||
### In der Config-Datei (z.B. `config.json`):
|
||
|
||
```json
|
||
{
|
||
"agents": [
|
||
{
|
||
"agentId": "analyst",
|
||
"tools": {
|
||
"MeinTool": {
|
||
"meineSetting": "wert-fuer-agent-analyst",
|
||
"andereOption": true
|
||
}
|
||
}
|
||
},
|
||
{
|
||
"agentId": "developer",
|
||
"tools": {
|
||
"MeinTool": {
|
||
"meineSetting": "wert-fuer-agent-developer",
|
||
"andereOption": false
|
||
}
|
||
}
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
### Konfiguration im Tool auslesen:
|
||
|
||
```csharp
|
||
// Im ExecuteAsync:
|
||
var setting = context.ToolConfig["meineSetting"]?.ToString();
|
||
var option = context.ToolConfig.TryGetValue("andereOption", out var v)
|
||
&& v is JsonElement je
|
||
&& je.GetBoolean();
|
||
```
|
||
|
||
**Wichtig:** `context.ToolConfig` enthält nur die Config für dieses Tool und diesen Agent. Du bekommst nie die Config eines anderen Agents oder eines anderen Tools.
|
||
|
||
---
|
||
|
||
## Tool im Host registrieren
|
||
|
||
Im `Host/Program.cs` (oder einer Startup-Klasse) wird das Tool registriert:
|
||
|
||
```csharp
|
||
var registry = new ToolRegistry();
|
||
|
||
// Jedes Tool einzeln registrieren
|
||
registry.Register(new MeinToolTool());
|
||
registry.Register(new DatabaseTool());
|
||
registry.Register(new FileRWTool());
|
||
// ...
|
||
```
|
||
|
||
Der `AgentEngine` nutzt dann die `ToolRegistry`, um für jeden Agent nur die erlaubten Tools bereitzustellen.
|
||
|
||
---
|
||
|
||
## Geplante Tools (Referenz)
|
||
|
||
### Database (`ClawdDotNet.Tools.Database`)
|
||
|
||
| Eigenschaft | Wert |
|
||
|---|---|
|
||
| Name | `Database` |
|
||
| NuGet | `MySqlConnector`, `MongoDB.Driver` |
|
||
| Aktionen | `query`, `insert`, `upsert` |
|
||
|
||
**Config-Felder:**
|
||
- `connectionString` – Datenbankverbindung (pro Agent!)
|
||
- `type` – `"mysql"` oder `"mongodb"`
|
||
- `allowedTables` – Liste erlaubter Tabellen (Whitelist)
|
||
|
||
**Sicherheit:**
|
||
- Parameterized Queries gegen SQL-Injection
|
||
- Nur Tabellen aus `allowedTables` erlaubt
|
||
- Connection String kommt IMMER aus `context.ToolConfig`
|
||
|
||
---
|
||
|
||
### FileRW (`ClawdDotNet.Tools.FileRW`)
|
||
|
||
| Eigenschaft | Wert |
|
||
|---|---|
|
||
| Name | `FileRW` |
|
||
| NuGet | (keine) |
|
||
| Aktionen | `read`, `write`, `append`, `list`, `delete` |
|
||
|
||
**Config-Felder:**
|
||
- `rootPath` – Basisverzeichnis (Agent ist darin eingesperrt)
|
||
- `allowWrite` – `true`/`false`
|
||
- `allowedExtensions` – z.B. `[".txt", ".json", ".html"]`
|
||
|
||
**Sicherheit (Pflicht):**
|
||
- Path-Traversal-Check: `Path.GetFullPath(requested).StartsWith(Path.GetFullPath(rootPath))`
|
||
- Nur erlaubte Dateiendungen
|
||
- Schreibzugriff nur wenn `allowWrite = true`
|
||
|
||
---
|
||
|
||
### Mail (`ClawdDotNet.Tools.Mail`)
|
||
|
||
| Eigenschaft | Wert |
|
||
|---|---|
|
||
| Name | `Mail` |
|
||
| NuGet | `MailKit` |
|
||
| Aktionen | `send`, `read_inbox`, `read_message`, `mark_read` |
|
||
|
||
**Config-Felder:**
|
||
- `imapHost`, `imapPort` – IMAP-Server
|
||
- `smtpHost`, `smtpPort` – SMTP-Server
|
||
- `username`, `password` – Zugangsdaten
|
||
- `allowedRecipients` – Whitelist erlaubter Empfänger
|
||
|
||
**Sicherheit:**
|
||
- Empfänger müssen in `allowedRecipients` stehen
|
||
|
||
---
|
||
|
||
## AgentToolContext – Referenz
|
||
|
||
```csharp
|
||
public sealed record AgentToolContext(
|
||
string AgentId, // ID des ausführenden Agents
|
||
string InstanceId, // ID der laufenden ClawdDotNet-Instanz
|
||
IReadOnlyDictionary<string, object?> ToolConfig, // Tool-Config für DIESEN Agent
|
||
ILogger Logger, // Logger (schreibt in Tool_{ToolName}.log)
|
||
CancellationToken CancellationToken
|
||
);
|
||
```
|
||
|
||
---
|
||
|
||
## ToolResult – Referenz
|
||
|
||
```csharp
|
||
public sealed record ToolResult(
|
||
bool Success,
|
||
string Content, // Geht zurück ans LLM
|
||
string? ErrorMessage = null
|
||
);
|
||
|
||
// Hilfsmethoden:
|
||
ToolResult.Ok("Ergebnis als JSON oder Text");
|
||
ToolResult.Fail("Fehlerbeschreibung");
|
||
```
|
||
|
||
---
|
||
|
||
## Background Jobs (optional)
|
||
|
||
Manche Tools müssen regelmäßig im Hintergrund arbeiten, ohne dabei jedes Mal den Agenten (und damit das LLM) aufzuwecken. Beispiel: Ein Telegram-Tool prüft alle 30 Sekunden auf neue Nachrichten, weckt den Agenten aber **nur** wenn tatsächlich eine neue Nachricht vorliegt.
|
||
|
||
Dafür gibt es das optionale Interface `IToolJobProvider`. Ein Tool kann es **zusätzlich** zu `IAgentTool` implementieren — es ist kein Ersatz.
|
||
|
||
### IToolJobProvider implementieren
|
||
|
||
```csharp
|
||
using ClawdDotNet.Core.Tools;
|
||
using ClawdDotNet.Core.State;
|
||
using Microsoft.Extensions.Logging;
|
||
|
||
public sealed class MeinToolTool : IAgentTool, IToolJobProvider
|
||
{
|
||
// ... IAgentTool-Implementierung wie oben ...
|
||
|
||
// 1. Verfügbare Job-Typen deklarieren
|
||
public IReadOnlyList<ToolJobDefinition> GetJobDefinitions() =>
|
||
[
|
||
new("meintool_check", "MeinTool Check", "Prüft regelmäßig auf Änderungen")
|
||
];
|
||
|
||
// 2. Job-Tick ausführen (kein LLM, nur Tool-Code!)
|
||
public async Task<ToolJobResult> ExecuteJobAsync(
|
||
string jobTypeId,
|
||
IReadOnlyDictionary<string, object?> toolConfig,
|
||
IStateStore stateStore,
|
||
ILogger logger,
|
||
CancellationToken ct)
|
||
{
|
||
if (jobTypeId != "meintool_check")
|
||
return ToolJobResult.NoAction($"Unbekannter Job: {jobTypeId}");
|
||
|
||
// Config auslesen (gleiche Felder wie in ExecuteAsync)
|
||
var apiKey = toolConfig.TryGetValue("apiKey", out var v)
|
||
? v?.ToString() ?? "" : "";
|
||
|
||
// Zustand über IStateStore persistieren (überlebt Neustarts)
|
||
var lastOffset = await stateStore.GetAsync("meintool_last_offset") ?? "0";
|
||
|
||
// Prüfung durchführen...
|
||
var newItems = await CheckForUpdatesAsync(apiKey, lastOffset, ct);
|
||
|
||
if (newItems.Count == 0)
|
||
return ToolJobResult.NoAction("Keine neuen Einträge");
|
||
|
||
// Offset speichern
|
||
await stateStore.SetAsync("meintool_last_offset", newItems.Last().Id);
|
||
|
||
// Agent aufwecken mit Zusammenfassung
|
||
return ToolJobResult.Wake(
|
||
$"{newItems.Count} neue Einträge gefunden:\n" +
|
||
string.Join("\n", newItems.Select(i => $"- {i.Title}")),
|
||
logSummary: $"{newItems.Count} neue Einträge"
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
### ToolJobResult
|
||
|
||
Das Tool gibt deklarativ zurück, ob der Agent geweckt werden soll:
|
||
|
||
| Methode | Effekt |
|
||
|---------|--------|
|
||
| `ToolJobResult.NoAction(logSummary?)` | Nichts tun, optional Log-Eintrag |
|
||
| `ToolJobResult.Wake(wakeMessage, logSummary?)` | Agent **im bestehenden Chat** aufwecken (Default) |
|
||
| `ToolJobResult.WakeStateless(wakeMessage, logSummary?)` | Agent in **isolierter Session** aufwecken (kein Chatverlauf) |
|
||
|
||
**`Wake` vs `WakeStateless`:**
|
||
- `Wake` (Default) nutzt `ChatAsync` — die Nachricht erscheint im Chat-Tab, der Agent behält den gesamten Konversationsverlauf. Ideal für alles was Teil einer laufenden Interaktion ist (Telegram-Nachrichten, fertige Transkripte, E-Mail-Antworten).
|
||
- `WakeStateless` nutzt `RunAsync` — komplett isolierte Ausführung ohne Kontext. Nur für einmalige, kontextfreie Aufgaben verwenden.
|
||
|
||
**Wichtig:** Das Tool hat keinen direkten Zugriff auf den `AgentEngine`. Der `ToolJobScheduler` wertet das Ergebnis aus und weckt den Agenten bei Bedarf.
|
||
|
||
### IStateStore für Zustandstracking
|
||
|
||
`IStateStore` ist ein einfacher Key-Value-Store (SQLite-basiert), der zwischen Job-Ticks und Neustarts persistiert:
|
||
|
||
```csharp
|
||
// Wert lesen (null wenn nicht vorhanden)
|
||
string? value = await stateStore.GetAsync("mein_key");
|
||
|
||
// Wert schreiben
|
||
await stateStore.SetAsync("mein_key", "neuer_wert");
|
||
```
|
||
|
||
Verwende aussagekräftige Keys, z.B. `meintool_poll_offset` oder `meintool_last_check`.
|
||
|
||
### JSON-Konfiguration
|
||
|
||
Tool Jobs werden pro Agent in der `config.json` konfiguriert, parallel zum bestehenden `scheduler`:
|
||
|
||
```json
|
||
{
|
||
"agentId": "support-bot",
|
||
"tools": {
|
||
"MeinTool": { "apiKey": "..." }
|
||
},
|
||
"scheduler": [
|
||
{ "cron": "0 8 * * 1-5", "taskMessage": "Morgenbericht erstellen" }
|
||
],
|
||
"toolJobs": [
|
||
{
|
||
"jobId": "abc12345",
|
||
"toolName": "MeinTool",
|
||
"jobTypeId": "meintool_check",
|
||
"cron": "*/5 * * * *",
|
||
"enabled": true,
|
||
"runOnStart": false
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
| Feld | Beschreibung |
|
||
|------|-------------|
|
||
| `jobId` | Eindeutige ID (wird automatisch generiert) |
|
||
| `toolName` | Name des Tools (muss `IToolJobProvider` implementieren) |
|
||
| `jobTypeId` | Einer der von `GetJobDefinitions()` deklarierten IDs |
|
||
| `cron` | Cron-Ausdruck für die Ausführungsintervalle |
|
||
| `enabled` | Job aktiv/inaktiv |
|
||
| `runOnStart` | Beim Programmstart sofort einmal ausführen |
|
||
|
||
### Unterschied: Agent Wakeup vs Tool Job
|
||
|
||
| | Agent Wakeup (`scheduler`) | Tool Job (`toolJobs`) |
|
||
|---|---|---|
|
||
| **Ausführung** | Voller LLM-Run | Nur Tool-Code (kein LLM) |
|
||
| **Kosten** | Token pro Tick | Kostenlos pro Tick |
|
||
| **Agent-Aufruf** | Immer | Nur bei `ShouldWakeAgent` |
|
||
| **Anwendungsfall** | Regelmäßige Aufgaben | Polling, Monitoring, Checks |
|
||
|
||
---
|
||
|
||
## Checkliste für neue Tools
|
||
|
||
- [ ] Eigenes Projekt `ClawdDotNet.Tools.{Name}` anlegen
|
||
- [ ] Nur `ClawdDotNet.Core` referenzieren
|
||
- [ ] `IAgentTool` implementieren
|
||
- [ ] `InputSchema` als gültiges JSON Schema definieren
|
||
- [ ] Alle Konfiguration aus `context.ToolConfig` lesen
|
||
- [ ] Alle `await`-Aufrufe mit `CancellationToken` versehen
|
||
- [ ] `context.Logger` für Logging verwenden (kein `Console.WriteLine`)
|
||
- [ ] Sicherheitsprüfungen implementieren (je nach Tool-Typ)
|
||
- [ ] Im Host registrieren: `registry.Register(new MeinToolTool());`
|
||
- [ ] In der Solution-Datei (.slnx) einbinden
|
||
- [ ] NuGet-Packages in `NuGet.Config` PackageSourceMapping ergänzen
|
||
- [ ] *Optional:* `IToolJobProvider` implementieren (wenn Hintergrund-Polling nötig)
|
||
- [ ] *Optional:* `GetJobDefinitions()` mit eindeutigen `JobTypeId`s definieren
|
||
- [ ] *Optional:* `ExecuteJobAsync()` implementieren, `IStateStore` für Zustandstracking nutzen
|
||
|
||
---
|
||
|
||
## Logging
|
||
|
||
Das Logging-System trennt automatisch nach Modul. Wenn dein Tool den Logger aus dem `AgentToolContext` verwendet, landen die Logs automatisch in:
|
||
|
||
```
|
||
Logs/
|
||
├── 2026-05-12/
|
||
│ ├── Core.log ← Engine, Scheduler, Config
|
||
│ ├── Host.log ← Oberflaeche
|
||
│ ├── Tool_Database.log ← Database-Tool
|
||
│ ├── Tool_FileRW.log ← FileRW-Tool
|
||
│ ├── Tool_MeinTool.log ← Dein Tool!
|
||
│ └── ...
|
||
```
|
||
|
||
Die Zuordnung geschieht über den Namespace:
|
||
- `ClawdDotNet.Core.*` → `Core.log`
|
||
- `ClawdDotNet.Tools.{Name}.*` → `Tool_{Name}.log`
|
||
- `ClawdDotNet.Host.*` → `Host.log`
|
||
|
||
Log-Level: `Debug`, `Info`, `Warn`, `Error`
|