diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index e19a547..8a1b61e 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -854,6 +854,7 @@ Ein **Agent** definiert die Persönlichkeit und Fähigkeiten der AI: | `WebSearchVersion`| Web-Suche aktivieren (z.B. `web_search_20260209`) | | `AllowUserChoice` | `ask_user_choice`-Tool verfügbar (Default: true) | | `TaskInstruction` | Aufgabe/Anweisung – optionaler Text, wird als `## Anweisungen`-Block in System-Prompt eingefügt | +| `Effort` | Reasoning-Effort-Level für Claude. `""` (=high, Default), `low`, `medium`, `high`, `xhigh` (nur Opus 4.7+), `max` (nur Opus 4.6+/Sonnet 4.6). API: als `output_config.effort` gesendet. CLI: via Env-Var `CLAUDE_CODE_EFFORT_LEVEL`. Bei OpenAI-Providern ignoriert. | | `IsPublic` | Andere User können den Agent in ihren Sessions verwenden (aber nicht bearbeiten) | ### Model-Auflösung diff --git a/ARCHITECTURE_QUICK.md b/ARCHITECTURE_QUICK.md index 8cf5c0e..1ad0b82 100644 --- a/ARCHITECTURE_QUICK.md +++ b/ARCHITECTURE_QUICK.md @@ -87,7 +87,7 @@ User 1──* TelegramBot 1──* Session (via telegram_bot_id) |---------|-------| | `users` | Login, DisplayName, IsAdmin, Persona, Settings (JSON), DefaultPath | | `tokens` | Auth-Tokens → User | -| `agents` | AI-Agenten (Model – z.B. `claude-sonnet-4-6` oder `nvidia:model-id`, CustomCommands, AutoCommit, UseSoul/Identity/UserInfo/Memory/AutoContext, UseGitHubSearch, UseSerperSearch, WebSearch, AllowUserChoice, TaskInstruction, AvatarScale/Offset, AvatarSmall, IsPublic) | +| `agents` | AI-Agenten (Model – z.B. `claude-sonnet-4-6` oder `nvidia:model-id`, CustomCommands, AutoCommit, UseSoul/Identity/UserInfo/Memory/AutoContext, UseGitHubSearch, UseSerperSearch, WebSearch, AllowUserChoice, TaskInstruction, Effort, AvatarScale/Offset, AvatarSmall, IsPublic) | | `sessions` | Chat-Sessions (Description, Image, ImageSmall, Workspace, Context, CustomCommands, TelegramBotId, TelegramChatId, AvatarScale/Offset, PreparerAgentId, ReviewAgentId, MaxReviewIterations, FinalizerAgentId) | | `chains` | Ein Request-Response-Zyklus (Tokens, Timing, Iterations, PlanMode, ToolChoice, ToolNames, StreamErrorInfo, ArchiveId) | | `messages` | Einzelne Nachrichten (text / tool_use / tool_result / error / web_search_result / auto_context), Tag für Sub-Agent-Markierung oder Import-Herkunft (`openclaw-import`), thought_signature (Gemini) | diff --git a/Endpoints/AgentEndpoint.cs b/Endpoints/AgentEndpoint.cs index f2ffda3..f028653 100644 --- a/Endpoints/AgentEndpoint.cs +++ b/Endpoints/AgentEndpoint.cs @@ -71,6 +71,7 @@ public static class AgentEndpoint a.WebSearchVersion, a.AllowUserChoice, a.TaskInstruction, + a.Effort, a.AvatarScale, a.AvatarOffsetX, a.AvatarOffsetY, @@ -137,6 +138,7 @@ public static class AgentEndpoint WebSearchVersion = body.TryGetProperty("webSearchVersion", out var wsv2) ? wsv2.GetString() ?? "" : "", AllowUserChoice = !body.TryGetProperty("allowUserChoice", out var auc) || auc.GetBoolean(), TaskInstruction = body.TryGetProperty("taskInstruction", out var ti) ? ti.GetString() : null, + Effort = body.TryGetProperty("effort", out var ef) ? ef.GetString() ?? "" : "", AvatarScale = body.TryGetProperty("avatarScale", out var asc) ? asc.GetDouble() : 1.0, AvatarOffsetX = body.TryGetProperty("avatarOffsetX", out var aox) ? aox.GetDouble() : 0, AvatarOffsetY = body.TryGetProperty("avatarOffsetY", out var aoy) ? aoy.GetDouble() : 0, @@ -184,6 +186,7 @@ public static class AgentEndpoint } if (body.TryGetProperty("allowUserChoice", out var auc)) agent.AllowUserChoice = auc.GetBoolean(); if (body.TryGetProperty("taskInstruction", out var ti)) agent.TaskInstruction = ti.GetString(); + if (body.TryGetProperty("effort", out var ef)) agent.Effort = ef.GetString() ?? ""; if (body.TryGetProperty("avatarScale", out var asc)) agent.AvatarScale = asc.GetDouble(); if (body.TryGetProperty("avatarOffsetX", out var aox)) agent.AvatarOffsetX = aox.GetDouble(); if (body.TryGetProperty("avatarOffsetY", out var aoy)) agent.AvatarOffsetY = aoy.GetDouble(); diff --git a/Endpoints/ChatEndpoint.cs b/Endpoints/ChatEndpoint.cs index 9aea217..7fd8f1f 100644 --- a/Endpoints/ChatEndpoint.cs +++ b/Endpoints/ChatEndpoint.cs @@ -261,7 +261,7 @@ public static class ChatEndpoint if (anthropic.IsCliAvailable) { - await anthropic.StreamChatViaCli(res, ctx.ModelId, ctx.SystemPrompt, messages, ctx.ChainId, workspace: ctx.Workspace, allowUserChoice: ctx.Agent?.AllowUserChoice ?? true, planMode: body.PlanMode, webSearch: !string.IsNullOrEmpty(ctx.WebSearchVersion), sessionId: sessionId, agentId: ctx.AgentId, remoteMgr: remoteMgr, unityMcpUrl: ctx.Session?.UnityMcpUrl, unityMcpToken: ctx.Session?.UnityMcpToken, useAutoContext: ctx.Agent?.UseAutoContext ?? true, workCt: workCt, sseCt: sseCt); + await anthropic.StreamChatViaCli(res, ctx.ModelId, ctx.SystemPrompt, messages, ctx.ChainId, workspace: ctx.Workspace, allowUserChoice: ctx.Agent?.AllowUserChoice ?? true, planMode: body.PlanMode, webSearch: !string.IsNullOrEmpty(ctx.WebSearchVersion), sessionId: sessionId, agentId: ctx.AgentId, remoteMgr: remoteMgr, unityMcpUrl: ctx.Session?.UnityMcpUrl, unityMcpToken: ctx.Session?.UnityMcpToken, useAutoContext: ctx.Agent?.UseAutoContext ?? true, effort: ctx.Effort, workCt: workCt, sseCt: sseCt); } else { @@ -271,7 +271,7 @@ public static class ChatEndpoint return; } - await anthropic.StreamChat(res, ctx.ModelId, ctx.SystemPrompt, messages, ctx.ChainId, ctx.Workspace, apiToken: anthropicToken, sessionId: sessionId, agentId: ctx.AgentId, userId: userId, useMemory: ctx.UseMemory, webSearchVersion: ctx.WebSearchVersion, maxIterations: ctx.MaxIterations, toolChoice: effectiveToolChoice, review: ctx.Review, finalizer: ctx.Finalizer, allowUserChoice: ctx.Agent?.AllowUserChoice ?? true, planMode: ctx.PlanMode, useAutoContext: ctx.Agent?.UseAutoContext ?? true, useGitHubSearch: ctx.Agent?.UseGitHubSearch ?? false, useSerperSearch: ctx.Agent?.UseSerperSearch ?? false, workCt: workCt, hardCt: hardCt, sseCt: sseCt, remoteMgr: remoteMgr); + await anthropic.StreamChat(res, ctx.ModelId, ctx.SystemPrompt, messages, ctx.ChainId, ctx.Workspace, apiToken: anthropicToken, sessionId: sessionId, agentId: ctx.AgentId, userId: userId, useMemory: ctx.UseMemory, webSearchVersion: ctx.WebSearchVersion, maxIterations: ctx.MaxIterations, toolChoice: effectiveToolChoice, review: ctx.Review, finalizer: ctx.Finalizer, allowUserChoice: ctx.Agent?.AllowUserChoice ?? true, planMode: ctx.PlanMode, useAutoContext: ctx.Agent?.UseAutoContext ?? true, useGitHubSearch: ctx.Agent?.UseGitHubSearch ?? false, useSerperSearch: ctx.Agent?.UseSerperSearch ?? false, effort: ctx.Effort, workCt: workCt, hardCt: hardCt, sseCt: sseCt, remoteMgr: remoteMgr); } } finally diff --git a/Models/AgentRecord.cs b/Models/AgentRecord.cs index 9fe1c7a..36d3eb0 100644 --- a/Models/AgentRecord.cs +++ b/Models/AgentRecord.cs @@ -23,6 +23,13 @@ public class AgentRecord public string WebSearchVersion { get; set; } = ""; public bool AllowUserChoice { get; set; } = true; public string TaskInstruction { get; set; } + /// + /// Reasoning-Effort-Level für Claude (Opus 4.5+, Sonnet 4.6+). + /// Werte: "" (= high, weglassen), "low", "medium", "high", "xhigh" (Opus 4.7+), "max" (Opus 4.6+, Sonnet 4.6). + /// API-Pfad: wird als output_config.effort gesendet. CLI-Pfad: wird via CLAUDE_CODE_EFFORT_LEVEL gesetzt. + /// Bei OpenAI-Providern (MiniMax/Nvidia/etc.) wird das Feld stillschweigend ignoriert. + /// + public string Effort { get; set; } = ""; public double AvatarScale { get; set; } = 1.0; public double AvatarOffsetX { get; set; } public double AvatarOffsetY { get; set; } diff --git a/Models/ChatContext.cs b/Models/ChatContext.cs index cda5d43..dd268b1 100644 --- a/Models/ChatContext.cs +++ b/Models/ChatContext.cs @@ -17,6 +17,7 @@ public class ChatContext public string ToolChoice { get; set; } public bool UseMemory { get; set; } public string WebSearchVersion { get; set; } + public string Effort { get; set; } public int MaxIterations { get; set; } public AgentRecord Agent { get; set; } public SessionRecord Session { get; set; } @@ -43,6 +44,7 @@ public class SubAgentContext public bool UseMemory { get; set; } public string WebSearchVersion { get; set; } public string ToolChoice { get; set; } + public string Effort { get; set; } public AgentRecord Agent { get; set; } // Preparer-spezifisch diff --git a/Services/AnthropicService.Agents.cs b/Services/AnthropicService.Agents.cs index 48678d1..2bbdd19 100644 --- a/Services/AnthropicService.Agents.cs +++ b/Services/AnthropicService.Agents.cs @@ -77,7 +77,7 @@ public partial class AnthropicService preparer.Workspace, preparer.UseMemory, preparer.WebSearchVersion, preparer.ToolChoice, preparerMessages, chainId, sessionId, apiToken, usage, 0, "preparer", preparer.Agent?.Name ?? "Vorbereiter-Agent", - cancellationToken, cancellationToken); + cancellationToken, cancellationToken, preparer.Effort); } var result = new PreparerResult { IterationCount = iterations, ResponseText = text }; @@ -185,7 +185,7 @@ public partial class AnthropicService review.Workspace, review.UseMemory, review.WebSearchVersion, review.ToolChoice, reviewMessages, chainId, sessionId, apiToken, usage, baseIterations, "review", review.Agent?.Name ?? "Review-Agent", - workCt, sseCt); + workCt, sseCt, review.Effort); } var result = new ReviewResult { ReviewIterationCount = iterations }; @@ -256,7 +256,7 @@ public partial class AnthropicService finalizer.Workspace, finalizer.UseMemory, finalizer.WebSearchVersion, finalizer.ToolChoice, finalizerMessages, chainId, sessionId, apiToken, usage, baseIterations, "finalizer", finalizer.Agent?.Name ?? "Finalizer-Agent", - workCt, sseCt); + workCt, sseCt, finalizer.Effort); } await SseHelper.TryWriteSse(response, "finalizer_done", new @@ -446,7 +446,8 @@ public partial class AnthropicService int baseIteration, int maxIterations, CancellationToken ct, - NonStreamingLoopCallbacks cb = null) + NonStreamingLoopCallbacks cb = null, + string effort = null) { cb ??= new NonStreamingLoopCallbacks(); int iterations = 0; @@ -465,6 +466,10 @@ public partial class AnthropicService ["messages"] = messages }; + // Reasoning-Effort (Opus 4.5+, Sonnet 4.6+). "" / "high" / null = Default weglassen. + if (!string.IsNullOrEmpty(effort) && effort != "high") + payload["output_config"] = new { effort }; + if (systemPrompt is List spBlocks && spBlocks.Count > 0) { // Cache-Breakpoint auf den letzten System-Prompt-Block (1h TTL). @@ -640,7 +645,8 @@ public partial class AnthropicService string agentType, string agentName, CancellationToken workCt, - CancellationToken sseCt) + CancellationToken sseCt, + string effort = null) { return RunNonStreamingToolLoop( modelId, systemPrompt, @@ -657,7 +663,8 @@ public partial class AnthropicService SseResponse = response, SseCt = sseCt, SsePrefix = agentType, AgentName = agentName, Tag = agentType - }); + }, + effort); } /// @@ -722,6 +729,7 @@ public partial class AnthropicService systemPrompt: systemText.ToString(), workspace: sub.Workspace, hasWorkspace: !string.IsNullOrEmpty(sub.Workspace), + effort: sub.Effort, cancellationToken: workCt ); @@ -785,7 +793,8 @@ public partial class AnthropicService CancellationToken = ct, VectorSearch = _vectorSearch, EmbeddingBg = _embeddingBg, Db = _db, ChatService = _chatService, AnthropicService = this, OpenAiService = _openAiService }) - }); + }, + ctx.Effort); var finalStopReason = text.Length > 0 ? "end_turn" : "error"; await FinalizeChain(chain, usage, iterations, finalStopReason, sw); diff --git a/Services/AnthropicService.Cli.cs b/Services/AnthropicService.Cli.cs index d16e9fa..c1b8384 100644 --- a/Services/AnthropicService.Cli.cs +++ b/Services/AnthropicService.Cli.cs @@ -37,6 +37,7 @@ public partial class AnthropicService string unityMcpUrl = null, string unityMcpToken = null, bool useAutoContext = true, + string effort = null, CancellationToken workCt = default, CancellationToken sseCt = default) { @@ -250,6 +251,7 @@ public partial class AnthropicService webSearch: webSearch, mcpConfigPath: mcpConfigPath, hasUnityMcp: !string.IsNullOrEmpty(unityMcpUrl), + effort: effort, cancellationToken: workCt ); @@ -491,6 +493,7 @@ public partial class AnthropicService systemPrompt: systemText.ToString(), workspace: ctx.Workspace, hasWorkspace: !string.IsNullOrEmpty(ctx.Workspace), + effort: ctx.Effort, cancellationToken: cancellationToken ); diff --git a/Services/AnthropicService.Streaming.cs b/Services/AnthropicService.Streaming.cs index c7f24e9..aa56e38 100644 --- a/Services/AnthropicService.Streaming.cs +++ b/Services/AnthropicService.Streaming.cs @@ -36,6 +36,7 @@ public partial class AnthropicService bool useAutoContext = true, bool useGitHubSearch = false, bool useSerperSearch = false, + string effort = null, CancellationToken workCt = default, CancellationToken hardCt = default, CancellationToken sseCt = default, @@ -97,6 +98,11 @@ public partial class AnthropicService ["stream"] = true }; + // Reasoning-Effort (Opus 4.5+, Sonnet 4.6+). "" / "high" / null = Default weglassen. + // Werte: low / medium / high / xhigh (Opus 4.7+) / max (Opus 4.6+, Sonnet 4.6). + if (!string.IsNullOrEmpty(effort) && effort != "high") + payload["output_config"] = new { effort }; + if (systemPrompt != null && systemPrompt.Count > 0) { // Cache-Breakpoint auf den letzten System-Prompt-Block (1h TTL). diff --git a/Services/ChatService.cs b/Services/ChatService.cs index 9337990..1c7aabd 100644 --- a/Services/ChatService.cs +++ b/Services/ChatService.cs @@ -194,6 +194,7 @@ Für welche Plattform soll die App entwickelt werden? Web, Mobile oder Desktop? ToolChoice = toolChoice, UseMemory = agent.UseMemory, WebSearchVersion = agent.WebSearchVersion, + Effort = agent.Effort, MaxIterations = maxIterations, Agent = agent, Session = session, @@ -880,6 +881,7 @@ Für welche Plattform soll die App entwickelt werden? Web, Mobile oder Desktop? UseMemory = agent.UseMemory, WebSearchVersion = agent.WebSearchVersion, ToolChoice = hasTools ? "any" : "auto", + Effort = agent.Effort, Agent = agent }; } diff --git a/Services/ClaudeCliService.cs b/Services/ClaudeCliService.cs index 411a29f..5368086 100644 --- a/Services/ClaudeCliService.cs +++ b/Services/ClaudeCliService.cs @@ -56,7 +56,7 @@ public class ClaudeCliService return dir; } - private ProcessStartInfo CreateCliStartInfo(List args, bool redirectInput = false) + private ProcessStartInfo CreateCliStartInfo(List args, bool redirectInput = false, string effort = null) { var psi = new ProcessStartInfo { @@ -75,6 +75,11 @@ public class ClaudeCliService // dieser Flag holt die 1h-TTL zurück (gilt für API-Key, Bedrock, Vertex, Foundry). psi.Environment["ENABLE_PROMPT_CACHING_1H"] = "1"; + // Effort-Level via Env-Var setzen (höchste Priorität, deckt auch "max" und "ultracode" ab). + // Das "--effort"-Flag unterstützt nur low/medium/high/xhigh – Env-Var deckt alles. + if (!string.IsNullOrEmpty(effort) && effort != "high") + psi.Environment["CLAUDE_CODE_EFFORT_LEVEL"] = effort; + foreach (var arg in args) psi.ArgumentList.Add(arg); @@ -140,6 +145,7 @@ public class ClaudeCliService bool webSearch = false, string mcpConfigPath = null, bool hasUnityMcp = false, + string effort = null, CancellationToken cancellationToken = default) { if (!_isAvailable) @@ -223,14 +229,15 @@ public class ClaudeCliService args.Add(mcpConfigPath); } - return await RunCliStreamingAsync(args, prompt, onEvent, cancellationToken); + return await RunCliStreamingAsync(args, prompt, onEvent, cancellationToken, effort); } private async Task RunCliStreamingAsync( List args, string stdinPrompt, Func onEvent, - CancellationToken cancellationToken) + CancellationToken cancellationToken, + string effort = null) { var result = new CliChatResult(); var fullOutput = new StringBuilder(); @@ -250,7 +257,7 @@ public class ClaudeCliService try { - var startInfo = CreateCliStartInfo(args, redirectInput: true); + var startInfo = CreateCliStartInfo(args, redirectInput: true, effort: effort); using var process = new Process { StartInfo = startInfo }; diff --git a/Services/DatabaseService.Agents.cs b/Services/DatabaseService.Agents.cs index 4b03053..6cabd97 100644 --- a/Services/DatabaseService.Agents.cs +++ b/Services/DatabaseService.Agents.cs @@ -19,8 +19,8 @@ public partial class DatabaseService agent.CreatedAt = now; agent.UpdatedAt = now; await conn.ExecuteAsync(@" - INSERT INTO agents (agent_id, user_id, name, avatar, avatar_small, description, model, tool_choice, custom_commands, auto_commit, use_soul, use_identity, use_user_info, use_memory, use_auto_context, use_github_search, use_serper_search, use_web_search, web_search_version, allow_user_choice, task_instruction, avatar_scale, avatar_offset_x, avatar_offset_y, is_public, is_deleted, created_at, updated_at) - VALUES (@AgentId, @UserId, @Name, @Avatar, @AvatarSmall, @Description, @Model, @ToolChoice, @CustomCommands, @AutoCommit, @UseSoul, @UseIdentity, @UseUserInfo, @UseMemory, @UseAutoContext, @UseGitHubSearch, @UseSerperSearch, @UseWebSearch, @WebSearchVersion, @AllowUserChoice, @TaskInstruction, @AvatarScale, @AvatarOffsetX, @AvatarOffsetY, @IsPublic, 0, @CreatedAt, @UpdatedAt) + INSERT INTO agents (agent_id, user_id, name, avatar, avatar_small, description, model, tool_choice, custom_commands, auto_commit, use_soul, use_identity, use_user_info, use_memory, use_auto_context, use_github_search, use_serper_search, use_web_search, web_search_version, allow_user_choice, task_instruction, effort, avatar_scale, avatar_offset_x, avatar_offset_y, is_public, is_deleted, created_at, updated_at) + VALUES (@AgentId, @UserId, @Name, @Avatar, @AvatarSmall, @Description, @Model, @ToolChoice, @CustomCommands, @AutoCommit, @UseSoul, @UseIdentity, @UseUserInfo, @UseMemory, @UseAutoContext, @UseGitHubSearch, @UseSerperSearch, @UseWebSearch, @WebSearchVersion, @AllowUserChoice, @TaskInstruction, @Effort, @AvatarScale, @AvatarOffsetX, @AvatarOffsetY, @IsPublic, 0, @CreatedAt, @UpdatedAt) ", agent); return agent; } @@ -55,7 +55,7 @@ public partial class DatabaseService model = @Model, tool_choice = @ToolChoice, custom_commands = @CustomCommands, auto_commit = @AutoCommit, use_soul = @UseSoul, use_identity = @UseIdentity, use_user_info = @UseUserInfo, use_memory = @UseMemory, use_auto_context = @UseAutoContext, use_github_search = @UseGitHubSearch, use_serper_search = @UseSerperSearch, use_web_search = @UseWebSearch, web_search_version = @WebSearchVersion, - allow_user_choice = @AllowUserChoice, task_instruction = @TaskInstruction, + allow_user_choice = @AllowUserChoice, task_instruction = @TaskInstruction, effort = @Effort, avatar_scale = @AvatarScale, avatar_offset_x = @AvatarOffsetX, avatar_offset_y = @AvatarOffsetY, is_public = @IsPublic, updated_at = @UpdatedAt WHERE agent_id = @AgentId AND user_id = @UserId diff --git a/Services/DatabaseService.cs b/Services/DatabaseService.cs index 15c8958..027efe3 100644 --- a/Services/DatabaseService.cs +++ b/Services/DatabaseService.cs @@ -196,6 +196,7 @@ public partial class DatabaseService TryAddColumn(conn, "sessions", "unity_mcp_url", "TEXT"); TryAddColumn(conn, "sessions", "unity_mcp_token", "TEXT"); TryAddColumn(conn, "messages", "tag", "TEXT"); + TryAddColumn(conn, "agents", "effort", "TEXT NOT NULL DEFAULT ''"); // Migration: 1M-Suffix entfernen (Mai 2026). Seit März 2026 ist 1M-Context Default // bei Opus 4.6/4.7/4.8 + Sonnet 4.6 – kein "[1m]"-Suffix mehr nötig. diff --git a/wwwroot/index.html b/wwwroot/index.html index 913dd19..cae705a 100644 --- a/wwwroot/index.html +++ b/wwwroot/index.html @@ -240,6 +240,15 @@ + +