feat: Vorbereiter-Agent (Preparer) für Sessions

- Neues optionales Session-Feld 'preparerAgentId' (DB + Model + API)
- PreparerContext im ChatContext für Agent-Konfiguration
- RunPreparer() in AnthropicService: führt Preparer vor dem Worker aus
  - [FORWARD] → aufbereiteter Prompt wird an Session-Agent weitergegeben
  - Ohne [FORWARD] → Rückfrage an User, Flow stoppt (preparer_query)
- Preparer-System-Prompt: erklärt Rolle, [FORWARD]-Mechanismus, Vorarbeit
- Preparer kann eigene Tools + Fähigkeiten aktiv nutzen
- ChatEndpoint: Preparer-Phase vor StreamChat integriert
- Frontend: SSE-Events (preparer_start/text/tool_use/done)
- Preparer-Bubble (Info-Farbe) mit Status Weitergeleitet/Rückfrage
- [Preparer: Name] → hübscher Header, [FORWARD] ausgeblendet
- [Vorbereitet von Name] → dezenter Header in User-Bubble
- Agent-Picker im Session-Form (vor Review-Agent)
This commit is contained in:
Meleki committed 2026-03-07 15:23:22 +01:00
1 parent f12fbfdc3e
commit 494f431963
12 files changed
+306 -8

No files matched your search

+44
View File
@@ -135,6 +135,50 @@ public static class ChatEndpoint
res.Headers["Cache-Control"] = "no-cache";
res.Headers["Connection"] = "keep-alive";
// ── Preparer-Agent Phase (vor dem Worker) ──
if (ctx.Preparer != null && !isChoiceResponse)
{
var preparerResult = await anthropic.RunPreparer(
res, ctx.Preparer, messages, ctx.ChainId, ctx.Workspace, sessionId, cancellationToken);
if (!preparerResult.ShouldForward)
{
// Preparer hat eine Rückfrage → Antwort als Text streamen, Flow beenden
// Preparer-Antwort wird schon in RunAgentChat in DB gespeichert
await AnthropicService.WriteSseEvent(res, "done", new
{
chainId = ctx.ChainId,
stopReason = "preparer_query",
planMode = ctx.PlanMode
}, cancellationToken);
return;
}
// Preparer ist zufrieden → aufbereiteten Prompt als zusätzlichen Kontext an die History anhängen
if (!string.IsNullOrWhiteSpace(preparerResult.ForwardedPrompt))
{
// Den aufbereiteten Prompt als System-Context-Message einfügen
messages.Add(new Dictionary<string, object>
{
["role"] = "user",
["content"] = new List<object>
{
new { type = "text", text = $"[Vorbereitet von {ctx.Preparer.Agent?.Name ?? "Vorbereiter-Agent"}]:\n{preparerResult.ForwardedPrompt}" }
}
});
// Auch in DB speichern
await db.SaveMessage(new MessageRecord
{
ChainId = ctx.ChainId,
Iteration = 0,
Role = "user",
Type = "text",
Content = $"[Vorbereitet von {ctx.Preparer.Agent?.Name ?? "Vorbereiter-Agent"}]:\n{preparerResult.ForwardedPrompt}"
});
}
}
// Bei Choice-Response: toolChoice auf "auto" setzen, damit Claude frei antworten kann
var effectiveToolChoice = isChoiceResponse ? "auto" : ctx.ToolChoice;
+2 -1
View File
@@ -82,8 +82,9 @@ public static class SessionEndpoint
var reviewAgentId = body.TryGetProperty("reviewAgentId", out var ra) ? ra.GetString() : null;
var maxReviewIterations = body.TryGetProperty("maxReviewIterations", out var mri) ? mri.GetInt32() : 3;
var finalizerAgentId = body.TryGetProperty("finalizerAgentId", out var fa) ? fa.GetString() : null;
var preparerAgentId = body.TryGetProperty("preparerAgentId", out var pra) ? pra.GetString() : null;
var session = await db.CreateSession(userId, name, description, image, agentId, workspace, context, customCommands, avatarScale, avatarOffsetX, avatarOffsetY, reviewAgentId, maxReviewIterations, finalizerAgentId);
var session = await db.CreateSession(userId, name, description, image, agentId, workspace, context, customCommands, avatarScale, avatarOffsetX, avatarOffsetY, reviewAgentId, maxReviewIterations, finalizerAgentId, preparerAgentId);
await ctx.Response.WriteAsJsonAsync(session);
}