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>
14 KiB
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
ToolRegistryregistriert
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
<!-- 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:
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):
{
"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:
// 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:
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
allowedTableserlaubt - 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/falseallowedExtensions– 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-ServersmtpHost,smtpPort– SMTP-Serverusername,password– ZugangsdatenallowedRecipients– Whitelist erlaubter Empfänger
Sicherheit:
- Empfänger müssen in
allowedRecipientsstehen
AgentToolContext – Referenz
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
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
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) nutztChatAsync— 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).WakeStatelessnutztRunAsync— 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:
// 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:
{
"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.Corereferenzieren IAgentToolimplementierenInputSchemaals gültiges JSON Schema definieren- Alle Konfiguration aus
context.ToolConfiglesen - Alle
await-Aufrufe mitCancellationTokenversehen context.Loggerfür Logging verwenden (keinConsole.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.ConfigPackageSourceMapping ergänzen - Optional:
IToolJobProviderimplementieren (wenn Hintergrund-Polling nötig) - Optional:
GetJobDefinitions()mit eindeutigenJobTypeIds definieren - Optional:
ExecuteJobAsync()implementieren,IStateStorefü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.logClawdDotNet.Tools.{Name}.*→Tool_{Name}.logClawdDotNet.Host.*→Host.log
Log-Level: Debug, Info, Warn, Error