Import des bestehenden Projektstands in Git. - .NET 10 WinForms Anwendung (Multi-Agent / Tool-System) - .gitignore fuer Build-Artefakte, Secrets und Runtime-Daten ergaenzt Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
466 lines
14 KiB
Markdown
466 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.Host/ ← WinForms-App (verweist auf Core + alle Tools)
|
||
```
|
||
|
||
### 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 ← WinForms UI
|
||
│ ├── 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`
|