Es gab keine Obergrenze. Ein Agent in einer Schleife — etwa durch gegenseitige send_message-Aufrufe — konnte unbeaufsichtigt Guthaben verbrennen; die Credits-Anzeige war rein informativ. Verbrauchsdaten in der Datenbank Voraussetzung dafuer ist eine belastbare Erfassung. Bisher lag der Verbrauch in TokenUsage.json: Bei JEDEM Agenten-Lauf wurde die gesamte Datei geladen, ergaenzt und neu geschrieben, unter einem globalen Lock. Das waechst quadratisch und ist der eigentliche Engpass bei vielen Agenten — unabhaengig davon, welche Datenbank darunter liegt. RunUsage liegt jetzt in einer eigenen Tabelle mit Ortsdatum, damit ein Tagesbudget der Wahrnehmung des Benutzers folgt und die Abfrage ohne Zeitzonenrechnerei auskommt. Betraege werden als Text abgelegt und als decimal gelesen: Ueber REAL zu gehen wuerde bei Cent-Betraegen Rundungsfehler einsammeln, die sich ueber tausende Laeufe summieren. Ein Test weist das mit 1000 Buchungen zu je 0,0001 USD nach. BudgetGuard Zwei Arten von Grenzen, weil sich Kosten nicht immer beziffern lassen: Liefert der Anbieter fuer ein Modell keine Preise, greift die Kostengrenze nicht — die Token-Grenze dagegen immer. Wer sich absichern will, setzt beide. Ist die Summe wegen fehlender Preise unvollstaendig, steht das in der Begruendung; sonst wirkte ein niedriger Verbrauch wie ein noch offener Spielraum. Grenzen gibt es je Agent und je Instanz. Geprueft wird vor der ersten Anfrage, damit ein erschoepftes Budget gar nichts mehr kostet. Neuer Endzustand BudgetExceeded. Nebenbei: Der Namespace Usage kollidierte mit der gleichnamigen Modellklasse fuer Token-Angaben und heisst jetzt Accounting. 357 Tests gruen (209 Core, 148 Tools). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
268 lines
10 KiB
C#
268 lines
10 KiB
C#
using ClawdDotNet.Core.Api;
|
||
using ClawdDotNet.Core.Config;
|
||
using ClawdDotNet.Core.Engine;
|
||
using ClawdDotNet.Core.Logging;
|
||
using ClawdDotNet.Core.Security;
|
||
using ClawdDotNet.Core.Scheduling;
|
||
using ClawdDotNet.Core.Tools;
|
||
using ClawdDotNet.Core.State;
|
||
using ClawdDotNet.Core.Storage;
|
||
using ClawdDotNet.Core.Memory;
|
||
using ClawdDotNet.Core.Accounting;
|
||
using ClawdDotNet.Services;
|
||
using ClawdDotNet.Tools.FileRW;
|
||
using ClawdDotNet.Tools.Telegram;
|
||
using ClawdDotNet.Tools.Mail;
|
||
using ClawdDotNet.Tools.Database;
|
||
using ClawdDotNet.Tools.FTP;
|
||
using ClawdDotNet.Tools.DirectAPI;
|
||
using ClawdDotNet.Tools.WebFetch;
|
||
using ClawdDotNet.Tools.WebMonitor;
|
||
using ClawdDotNet.Tools.AgentComm;
|
||
using ClawdDotNet.Tools.AgentSpawn;
|
||
using ClawdDotNet.Tools.SocialMediaManager;
|
||
using ClawdDotNet.Tools.AgentEditor;
|
||
using ClawdDotNet.Tools.TelegramClient;
|
||
using ClawdDotNet.UI;
|
||
using Microsoft.Extensions.Logging;
|
||
|
||
namespace ClawdDotNet;
|
||
|
||
internal static class Program
|
||
{
|
||
[STAThread]
|
||
static void Main(string[] args)
|
||
{
|
||
ApplicationConfiguration.Initialize();
|
||
|
||
// ─── 0. Embedded UI extrahieren ───
|
||
EmbeddedUiManager.ExtractToTemp();
|
||
|
||
// ─── 1. App-Settings laden ───
|
||
var settingsManager = new SettingsManager();
|
||
settingsManager.Load();
|
||
var appSettings = settingsManager.AppSettings;
|
||
|
||
// ─── 2. InstanceDirectoryManager erstellen ───
|
||
var instancesDir = Path.GetFullPath(appSettings.InstancesDirectory);
|
||
var dirManager = new InstanceDirectoryManager(instancesDir);
|
||
|
||
// ─── 3. Instanz-Verzeichnis auswählen ───
|
||
string instancePath;
|
||
|
||
#if DEBUG
|
||
// Im Debug-Modus: Dev-Instanz automatisch erstellen/starten
|
||
const string devInstanceName = "Dev";
|
||
instancePath = dirManager.GetInstancePath(devInstanceName);
|
||
|
||
if (!Directory.Exists(instancePath))
|
||
{
|
||
dirManager.CreateInstance(devInstanceName);
|
||
}
|
||
#else
|
||
// Im Release-Modus: InstanceManager anzeigen
|
||
var instanceManager = new frm_InstanceManager(dirManager, settingsManager);
|
||
var dialogResult = instanceManager.ShowDialog();
|
||
|
||
if (dialogResult != DialogResult.OK || string.IsNullOrWhiteSpace(instanceManager.SelectedInstancePath))
|
||
{
|
||
return; // Benutzer hat abgebrochen
|
||
}
|
||
|
||
instancePath = instanceManager.SelectedInstancePath;
|
||
instanceManager.Dispose();
|
||
#endif
|
||
|
||
// ─── 4. Instanz-Config aus Verzeichnisstruktur laden ───
|
||
InstanceConfig instanceConfig;
|
||
try
|
||
{
|
||
instanceConfig = dirManager.LoadInstanceConfig(instancePath);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
MessageBox.Show(
|
||
$"Fehler beim Laden der Instanz:\n{instancePath}\n\n{ex.Message}",
|
||
"ClawdDotNet – Fehler",
|
||
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||
return;
|
||
}
|
||
|
||
// ─── 5. Logging-System initialisieren ───
|
||
var logDir = Path.GetFullPath(
|
||
!string.IsNullOrWhiteSpace(instanceConfig.LogDirectory)
|
||
? instanceConfig.LogDirectory
|
||
: appSettings.LogDirectory);
|
||
|
||
var minLevel = Enum.TryParse<ClawdDotNet.Core.Logging.LogLevel>(
|
||
appSettings.MinimumLogLevel, true, out var ml)
|
||
? ml
|
||
: ClawdDotNet.Core.Logging.LogLevel.Info;
|
||
|
||
var loggerFactory = LoggingExtensions.CreateClawdLoggerFactory(logDir, minLevel);
|
||
var coreLogger = loggerFactory.CreateLogger("ClawdDotNet.Core.Startup");
|
||
|
||
coreLogger.LogInformation("ClawdDotNet startet – Instanz: {Instance} ({Id})",
|
||
instanceConfig.InstanceName, instanceConfig.InstanceId);
|
||
coreLogger.LogInformation("Instanz-Verzeichnis: {Path}", instancePath);
|
||
|
||
// ─── 6. Core-Komponenten erzeugen ───
|
||
var toolRegistry = new ToolRegistry();
|
||
toolRegistry.Register(new FileRWTool());
|
||
toolRegistry.Register(new TelegramTool());
|
||
toolRegistry.Register(new MailTool());
|
||
toolRegistry.Register(new DatabaseTool());
|
||
toolRegistry.Register(new FTPTool());
|
||
toolRegistry.Register(new DirectApiTool());
|
||
toolRegistry.Register(new WebFetchTool());
|
||
toolRegistry.Register(new WebMonitorTool());
|
||
toolRegistry.Register(new AgentCommTool());
|
||
toolRegistry.Register(new SocialMediaManagerTool());
|
||
toolRegistry.Register(new AgentSpawnTool());
|
||
toolRegistry.Register(new AgentEditorTool());
|
||
toolRegistry.Register(new ClawdDotNet.Tools.Memory.MemoryTool());
|
||
|
||
// ─── 6a. TelegramClient (MTProto User-API) ───
|
||
TelegramClientManager? tgClientManager = null;
|
||
if (instanceConfig.TelegramClient is not null)
|
||
{
|
||
tgClientManager = new TelegramClientManager(
|
||
instanceConfig,
|
||
instancePath,
|
||
loggerFactory.CreateLogger("ClawdDotNet.Tools.TelegramClient"));
|
||
toolRegistry.Register(new TelegramClientTool(tgClientManager));
|
||
coreLogger.LogInformation("TelegramClient-Tool registriert");
|
||
}
|
||
|
||
var permissionGate = new PermissionGate();
|
||
|
||
// TODO: Tools aus tools/ Ordner laden
|
||
// foreach (var toolDll in Directory.GetFiles("./tools", "*.dll"))
|
||
// LoadToolPlugin(toolDll, toolRegistry);
|
||
|
||
OpenRouterClient? openRouterClient = null;
|
||
AgentEngine? agentEngine = null;
|
||
AgentScheduler? agentScheduler = null;
|
||
ToolJobScheduler? toolJobScheduler = null;
|
||
|
||
if (!string.IsNullOrWhiteSpace(instanceConfig.OpenRouterApiKey))
|
||
{
|
||
openRouterClient = new OpenRouterClient(
|
||
instanceConfig.OpenRouterApiKey,
|
||
loggerFactory.CreateLogger("ClawdDotNet.Core.Api.OpenRouterClient"));
|
||
|
||
// ModelTypeConverter mit dem Client verbinden für PropertyGrid-Dropdown
|
||
ClawdDotNet.Models.ModelTypeConverter.Client = openRouterClient;
|
||
|
||
// ─── 6.1. Speicher initialisieren ───
|
||
// Eine Datenbank je Instanz; StateStore und Gedächtnis teilen sie sich.
|
||
var storage = new SqliteStorage(Path.Combine(instancePath, "state.db"));
|
||
var stateStore = new SqliteStateStore(storage);
|
||
var memoryRepository = new SqliteMemoryRepository(storage);
|
||
var usageRepository = new SqliteUsageRepository(storage);
|
||
|
||
// Preise fürs Budget: Ohne sie greift nur die Token-Grenze.
|
||
var pricingCatalog = new ModelPricingCatalog();
|
||
_ = Task.Run(async () =>
|
||
{
|
||
try { pricingCatalog.Load(await openRouterClient.GetAvailableModelsAsync()); }
|
||
catch { /* Ohne Preise bleibt die Kostengrenze wirkungslos, die Token-Grenze nicht. */ }
|
||
});
|
||
|
||
agentEngine = new AgentEngine(
|
||
openRouterClient, toolRegistry, permissionGate, stateStore, loggerFactory,
|
||
memoryRepository, usageRepository, pricingCatalog)
|
||
{
|
||
InstanceBudget = instanceConfig.Budget
|
||
};
|
||
agentEngine.SetAgentConfigProvider(
|
||
() => instanceConfig.Agents,
|
||
instanceConfig.InstanceId,
|
||
agentId =>
|
||
{
|
||
var agent = instanceConfig.Agents.FirstOrDefault(a => a.AgentId == agentId);
|
||
if (agent is null) return null;
|
||
return string.IsNullOrWhiteSpace(agent.AgentDir) ? null : agent.AgentDir;
|
||
});
|
||
agentEngine.LoadPersistedChats();
|
||
agentScheduler = new AgentScheduler(agentEngine, instanceConfig.InstanceId, loggerFactory);
|
||
agentScheduler.RegisterAll(instanceConfig.Agents);
|
||
|
||
toolJobScheduler = new ToolJobScheduler(agentEngine, toolRegistry, stateStore, loggerFactory, instanceConfig.InstanceId);
|
||
toolJobScheduler.RegisterAll(instanceConfig.Agents);
|
||
|
||
coreLogger.LogInformation("AgentEngine und Scheduler erstellt, Chat-Verläufe geladen");
|
||
}
|
||
else
|
||
{
|
||
coreLogger.LogWarning("Kein OpenRouter API-Key konfiguriert – Agenten sind deaktiviert");
|
||
}
|
||
|
||
// ─── 6b. TelegramClient verbinden (Session oder interaktiver Login) ───
|
||
if (tgClientManager is not null)
|
||
{
|
||
tgClientManager.OnLoginCodeRequired += async (prompt) =>
|
||
{
|
||
string? code = null;
|
||
await Task.Run(() =>
|
||
{
|
||
code = Microsoft.VisualBasic.Interaction.InputBox(
|
||
prompt, "Telegram Verifizierung", "");
|
||
});
|
||
return code ?? "";
|
||
};
|
||
|
||
tgClientManager.On2FAPasswordRequired += async () =>
|
||
{
|
||
string? pw = null;
|
||
await Task.Run(() =>
|
||
{
|
||
pw = Microsoft.VisualBasic.Interaction.InputBox(
|
||
"Bitte 2FA-Passwort eingeben:", "Telegram 2FA", "");
|
||
});
|
||
return pw ?? "";
|
||
};
|
||
|
||
try
|
||
{
|
||
tgClientManager.ConnectAsync(CancellationToken.None)
|
||
.GetAwaiter().GetResult();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
coreLogger.LogError(ex, "Telegram: Login fehlgeschlagen");
|
||
}
|
||
}
|
||
|
||
// ─── 7. MainForm starten ───
|
||
var form = new frm_main(
|
||
settingsManager,
|
||
instanceConfig,
|
||
instancePath,
|
||
logDir,
|
||
loggerFactory,
|
||
toolRegistry,
|
||
dirManager,
|
||
agentEngine,
|
||
agentScheduler,
|
||
toolJobScheduler);
|
||
|
||
Application.Run(form);
|
||
|
||
// ─── 8. Aufräumen ───
|
||
coreLogger.LogInformation("ClawdDotNet wird beendet");
|
||
|
||
if (toolJobScheduler is not null)
|
||
toolJobScheduler.DisposeAsync().AsTask().GetAwaiter().GetResult();
|
||
|
||
if (agentScheduler is not null)
|
||
agentScheduler.DisposeAsync().AsTask().GetAwaiter().GetResult();
|
||
|
||
if (tgClientManager is not null)
|
||
tgClientManager.DisposeAsync().AsTask().GetAwaiter().GetResult();
|
||
|
||
openRouterClient?.Dispose();
|
||
loggerFactory.Dispose();
|
||
}
|
||
}
|