feat: send_to_session tool – agent-to-agent messaging

- Neues Tool send_to_session: Agent kann Nachrichten in andere Sessions schicken
- Ziel-Session per ID oder Name (case-insensitive) auffindbar
- User-Message wird nicht in DB gespeichert (wie Cronjob) – keine Bubble im Frontend
- Nachricht wird als '[Nachricht von Agent "X"]' kontextualisiert injiziert
- Sicherheitscheck: Ziel-Session muss demselben User gehören, keine eigene Session
- Zirkuläre Abhängigkeit ChatService ↔ AnthropicService via SetChatService() gelöst
- tool-ui.js: Display-Name für send_to_session
This commit is contained in:
Meleki committed 2026-03-11 12:56:36 +01:00
1 parent 20d891c9ef
commit c18b94cd4e
5 files changed
+99 -11

No files matched your search

+18 -5
View File
@@ -24,6 +24,7 @@ public class AnthropicService
private readonly bool _payloadLoggingGzip;
private readonly VectorSearchService _vectorSearch;
private readonly EmbeddingBackgroundService _embeddingBg;
private ChatService _chatService;
public AnthropicService(IHttpClientFactory httpFactory, string token, ToolService toolService, DatabaseService db, MemoryService memoryService = null, bool payloadLogging = false, bool payloadLoggingGzip = false, VectorSearchService vectorSearch = null, EmbeddingBackgroundService embeddingBg = null)
{
@@ -38,6 +39,15 @@ public class AnthropicService
_embeddingBg = embeddingBg;
}
/// <summary>
/// Wird nach der Konstruktion gesetzt um zirkuläre Abhängigkeit zu vermeiden
/// (ChatService → AnthropicService → ChatService).
/// </summary>
public void SetChatService(ChatService chatService)
{
_chatService = chatService;
}
public async Task StreamChat(
HttpResponse response,
string modelId,
@@ -47,6 +57,7 @@ public class AnthropicService
string workspace,
string sessionId = null,
string agentId = null,
string userId = null,
bool useMemory = true,
string webSearchVersion = null,
int maxIterations = 100,
@@ -183,7 +194,7 @@ public class AnthropicService
return;
}
var (stopReason, assistantContent, executedTools, streamErrorInfo) = await ProcessStream(response, httpResp, usage, workspace, agentId, workCt, sseCt, remoteMgr, sessionId);
var (stopReason, assistantContent, executedTools, streamErrorInfo) = await ProcessStream(response, httpResp, usage, workspace, agentId, workCt, sseCt, remoteMgr, sessionId, userId);
lastStopReason = stopReason;
promptTrimRetries = 0; // Erfolgreicher API-Call → Retry-Counter resetten
@@ -1204,7 +1215,8 @@ public class AnthropicService
CancellationToken workCt = default,
CancellationToken sseCt = default,
RemoteConnectionManager remoteMgr = null,
string sessionId = null)
string sessionId = null,
string userId = null)
{
var stream = await httpResp.Content.ReadAsStreamAsync(workCt);
// Bei Cancellation den Stream disposen, damit ReadLineAsync sofort abbricht.
@@ -1386,7 +1398,7 @@ public class AnthropicService
string toolResult;
bool isError = false;
try { toolResult = await _toolService.ExecuteTool(currentToolName, inputEl, workspace, _memoryService, agentId, workCt, remoteMgr: remoteMgr, sessionId: sessionId, vectorSearch: _vectorSearch, embeddingBg: _embeddingBg); }
try { toolResult = await _toolService.ExecuteTool(currentToolName, inputEl, workspace, _memoryService, agentId, workCt, remoteMgr: remoteMgr, sessionId: sessionId, vectorSearch: _vectorSearch, embeddingBg: _embeddingBg, db: _db, chatService: _chatService, anthropicService: this, userId: userId); }
catch (OperationCanceledException) { throw; }
catch (Exception ex) { toolResult = $"Fehler: {ex.Message}"; isError = true; }
@@ -1593,7 +1605,8 @@ public class AnthropicService
var hasWorkspace = !string.IsNullOrEmpty(workspace);
var includeMemory = useMemory && _memoryService != null && !string.IsNullOrEmpty(agentId);
var includeMemorySearch = _vectorSearch != null && _vectorSearch.GetEmbeddingCount() > 0;
var hasTools = hasWorkspace || includeMemory || includeRemote || includeMemorySearch;
var includeSendToSession = _db != null && _chatService != null;
var hasTools = hasWorkspace || includeMemory || includeRemote || includeMemorySearch || includeSendToSession;
var hasWebSearch = !string.IsNullOrEmpty(webSearchVersion);
if (!hasTools && !hasWebSearch) return;
@@ -1601,7 +1614,7 @@ public class AnthropicService
var tools = new List<object>();
if (hasTools)
{
var allTools = _toolService.GetToolDefinitions(includeFileTools: hasWorkspace, includeMemory: includeMemory, includeUserChoice: allowUserChoice, includeRemote: includeRemote, includeMemorySearch: includeMemorySearch);
var allTools = _toolService.GetToolDefinitions(includeFileTools: hasWorkspace, includeMemory: includeMemory, includeUserChoice: allowUserChoice, includeRemote: includeRemote, includeMemorySearch: includeMemorySearch, includeSendToSession: includeSendToSession);
if (planMode)
allTools = _toolService.FilterReadOnlyTools(allTools);
tools.AddRange(allTools);