# 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 net10.0 enable enable ClawdDotNet.Tools.MeinTool ``` --- ## 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 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 HandleReadAsync( JsonElement input, AgentToolContext context, CancellationToken ct) { // Implementierung... await Task.CompletedTask; // Platzhalter return ToolResult.Ok("Ergebnis als JSON oder Text"); } private async Task 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 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 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 ExecuteJobAsync( string jobTypeId, IReadOnlyDictionary 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`