Review-Agent: Default=Approved + vollständige Tool-Boxes für Sub-Agents
Backend:
- Review-Agent: Default = Approved (analog zum Preparer Default=Forward)
Nur bei explizitem [FEEDBACK]-Tag wird Feedback zurückgespielt
Verhindert Loop wenn Review-Agent bestätigt ohne [APPROVED]-Tag
- Neues SSE-Event {agentType}_tool_end für Sub-Agent Tool-Ergebnisse
Sendet toolName, input, result, isError nach Tool-Ausführung
Frontend Streaming (stream-ui.js + chat.js):
- addReviewToolUse: Vollständige Tool-Box mit Loading-Spinner statt nur Name
- finishReviewToolBox: Befüllt Tool-Box nach tool_end mit Details (Pfad, Diff, Result)
- Event-Handler für preparer/review/finalizer_tool_end
Frontend History (history.js):
- _renderReviewGroup: Vollständige Tool-Boxes statt einfache Tool-Use-Indikatoren
- Tool-Result Look-Ahead für korrekte Zuordnung (toolCallId-Match)
- Web-Search-Sources in Review-Bubbles unterstützt
- Tool-Visibility-Filter (isToolVisible) auch für Sub-Agent-Tools
CSS:
- .review-tool-use → .review-tool-box (erbt normale Tool-Box-Styles)
This commit is contained in:
1 parent
f3b6491643
commit
1fc6889ced
5 files changed
+93
-30
No files matched your search
@@ -450,30 +450,35 @@ public class AnthropicService
|
|||||||
|
|
||||||
var result = new ReviewResult { ReviewIterationCount = reviewIterations };
|
var result = new ReviewResult { ReviewIterationCount = reviewIterations };
|
||||||
|
|
||||||
// Prüfe ob [APPROVED]
|
// Default = Approved (analog zum Preparer, wo Default = Forward ist).
|
||||||
if (!string.IsNullOrEmpty(reviewText) && reviewText.Contains("[APPROVED]"))
|
// Nur bei explizitem [FEEDBACK]-Tag wird Feedback an den Worker zurückgespielt.
|
||||||
|
// [APPROVED] wird weiterhin erkannt, ist aber nicht mehr zwingend nötig.
|
||||||
|
if (!string.IsNullOrEmpty(reviewText) && reviewText.Contains("[FEEDBACK]"))
|
||||||
{
|
{
|
||||||
|
// Explizites Feedback – an Worker zurückspielen
|
||||||
|
var feedbackText = reviewText.Replace("[FEEDBACK]", "").Replace("[APPROVED]", "").Trim();
|
||||||
|
result.HasFeedback = true;
|
||||||
|
result.Feedback = $"[Review-Feedback von {review.Agent?.Name ?? "Review-Agent"} (Runde {reviewRound + 1}/{review.MaxReviewIterations})]:\n{feedbackText}";
|
||||||
|
|
||||||
|
await TryWriteSse(response, "review_feedback", new
|
||||||
|
{
|
||||||
|
round = reviewRound + 1,
|
||||||
|
maxRounds = review.MaxReviewIterations,
|
||||||
|
feedback = feedbackText,
|
||||||
|
agentName = review.Agent?.Name ?? "Review-Agent"
|
||||||
|
}, cancellationToken);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Default: Approved (kein [FEEDBACK]-Tag → alles OK)
|
||||||
await TryWriteSse(response, "review_approved", new
|
await TryWriteSse(response, "review_approved", new
|
||||||
{
|
{
|
||||||
round = reviewRound + 1,
|
round = reviewRound + 1,
|
||||||
agentName = review.Agent?.Name ?? "Review-Agent"
|
agentName = review.Agent?.Name ?? "Review-Agent"
|
||||||
}, cancellationToken);
|
}, cancellationToken);
|
||||||
result.HasFeedback = false;
|
result.HasFeedback = false;
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Feedback – an Worker zurückspielen
|
|
||||||
result.HasFeedback = true;
|
|
||||||
result.Feedback = $"[Review-Feedback von {review.Agent?.Name ?? "Review-Agent"} (Runde {reviewRound + 1}/{review.MaxReviewIterations})]:\n{reviewText}";
|
|
||||||
|
|
||||||
await TryWriteSse(response, "review_feedback", new
|
|
||||||
{
|
|
||||||
round = reviewRound + 1,
|
|
||||||
maxRounds = review.MaxReviewIterations,
|
|
||||||
feedback = reviewText,
|
|
||||||
agentName = review.Agent?.Name ?? "Review-Agent"
|
|
||||||
}, cancellationToken);
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -803,6 +808,16 @@ public class AnthropicService
|
|||||||
is_error = isError
|
is_error = isError
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await TryWriteSse(response, $"{agentType}_tool_end", new
|
||||||
|
{
|
||||||
|
toolName,
|
||||||
|
toolId,
|
||||||
|
input = toolInput.ToString(),
|
||||||
|
result = toolResult.Length > 500 ? toolResult[..500] + "..." : toolResult,
|
||||||
|
isError,
|
||||||
|
agentName
|
||||||
|
}, cancellationToken);
|
||||||
|
|
||||||
await _db.SaveMessage(new MessageRecord
|
await _db.SaveMessage(new MessageRecord
|
||||||
{
|
{
|
||||||
ChainId = chainId,
|
ChainId = chainId,
|
||||||
|
|||||||
@@ -1124,12 +1124,9 @@
|
|||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.review-tool-use {
|
.review-tool-box {
|
||||||
font-size: var(--font-size);
|
margin: var(--badge-spacing) 0;
|
||||||
opacity: var(--semi-transparent);
|
border-radius: var(--border-radius);
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--badge-spacing);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.review-status {
|
.review-status {
|
||||||
|
|||||||
@@ -87,6 +87,10 @@ function handleStreamEvent(eventType, data) {
|
|||||||
addReviewToolUse(data);
|
addReviewToolUse(data);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'preparer_tool_end': case 'review_tool_end': case 'finalizer_tool_end':
|
||||||
|
finishReviewToolBox(data);
|
||||||
|
break;
|
||||||
|
|
||||||
case 'preparer_done':
|
case 'preparer_done':
|
||||||
finalizeReviewBubble(data.forwarded ? 'forwarded' : 'query', data, 'preparer');
|
finalizeReviewBubble(data.forwarded ? 'forwarded' : 'query', data, 'preparer');
|
||||||
break;
|
break;
|
||||||
|
|||||||
+35
-6
@@ -320,11 +320,8 @@ function _renderReviewGroup(tagGroup, archived) {
|
|||||||
text = text.replace(/^\[(Review|Preparer|Finalizer): [^\]]+\]\s*/, '');
|
text = text.replace(/^\[(Review|Preparer|Finalizer): [^\]]+\]\s*/, '');
|
||||||
if (text) textParts.push(text);
|
if (text) textParts.push(text);
|
||||||
} else if (m.type === 'tool_use') {
|
} else if (m.type === 'tool_use') {
|
||||||
// Insert tool-use indicator
|
if (!isToolVisible(m.toolName)) continue;
|
||||||
var toolDiv = document.createElement('div');
|
// Flush accumulated text before tool box
|
||||||
toolDiv.className = 'review-tool-use';
|
|
||||||
toolDiv.innerHTML = '<i class="fa fa-wrench"></i> <span class="tool-name">' + esc(m.toolName) + '</span>';
|
|
||||||
// Flush accumulated text before tool indicator
|
|
||||||
if (textParts.length > 0) {
|
if (textParts.length > 0) {
|
||||||
var contentDiv = document.createElement('div');
|
var contentDiv = document.createElement('div');
|
||||||
contentDiv.className = 'review-content markdown';
|
contentDiv.className = 'review-content markdown';
|
||||||
@@ -333,9 +330,41 @@ function _renderReviewGroup(tagGroup, archived) {
|
|||||||
bubble.appendChild(contentDiv);
|
bubble.appendChild(contentDiv);
|
||||||
textParts = [];
|
textParts = [];
|
||||||
}
|
}
|
||||||
|
// Full tool-box (same as normal tool boxes)
|
||||||
|
var toolDiv = document.createElement('div');
|
||||||
|
toolDiv.className = 'tool-box review-tool-box' + (archived ? ' archived' : '');
|
||||||
|
// Look ahead for matching tool_result
|
||||||
|
var toolResult = null;
|
||||||
|
var toolIsError = false;
|
||||||
|
for (var j = i + 1; j < tagGroup.length; j++) {
|
||||||
|
if (tagGroup[j].type === 'tool_result' && tagGroup[j].toolCallId === m.toolCallId) {
|
||||||
|
toolResult = tagGroup[j].content;
|
||||||
|
toolIsError = tagGroup[j].isError;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
renderToolBoxContent(toolDiv, m.toolName, m.content, toolResult, toolIsError);
|
||||||
bubble.appendChild(toolDiv);
|
bubble.appendChild(toolDiv);
|
||||||
|
} else if (m.type === 'web_search_result') {
|
||||||
|
if (!isToolVisible('web_search')) continue;
|
||||||
|
if (textParts.length > 0) {
|
||||||
|
var contentDiv = document.createElement('div');
|
||||||
|
contentDiv.className = 'review-content markdown';
|
||||||
|
contentDiv.innerHTML = renderMarkdown(textParts.join('\n\n'));
|
||||||
|
parseFluentEmojis(contentDiv);
|
||||||
|
bubble.appendChild(contentDiv);
|
||||||
|
textParts = [];
|
||||||
|
}
|
||||||
|
var wsSources = [];
|
||||||
|
try { wsSources = JSON.parse(m.content || '[]'); } catch(e) {}
|
||||||
|
if (wsSources.length > 0) {
|
||||||
|
var wsDiv = document.createElement('div');
|
||||||
|
wsDiv.className = 'tool-box review-tool-box' + (archived ? ' archived' : '');
|
||||||
|
renderToolBoxContent(wsDiv, 'web_search', null, null, false, wsSources);
|
||||||
|
bubble.appendChild(wsDiv);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// tool_result and web_search_result are skipped (same as streaming)
|
// tool_result is consumed by tool_use look-ahead above
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flush remaining text
|
// Flush remaining text
|
||||||
|
|||||||
+21
-3
@@ -87,15 +87,32 @@ function appendToReviewText(text) {
|
|||||||
scrollDown();
|
scrollDown();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let _currentReviewToolBox = null;
|
||||||
|
|
||||||
function addReviewToolUse(data) {
|
function addReviewToolUse(data) {
|
||||||
if (!_reviewBubble) return;
|
if (!_reviewBubble) return;
|
||||||
const toolDiv = document.createElement('div');
|
if (!isToolVisible(data.toolName)) {
|
||||||
toolDiv.className = 'review-tool-use';
|
_currentReviewToolBox = null;
|
||||||
toolDiv.innerHTML = '<i class="fa fa-wrench"></i> <span class="tool-name">' + esc(data.toolName) + '</span>';
|
return;
|
||||||
|
}
|
||||||
|
var toolDiv = document.createElement('div');
|
||||||
|
toolDiv.className = 'tool-box review-tool-box streamed';
|
||||||
|
toolDiv.dataset.toolName = data.toolName;
|
||||||
|
var displayName = TOOL_DISPLAY_NAMES[data.toolName] || data.toolName;
|
||||||
|
toolDiv.innerHTML = '<span class="tool-name">' + esc(displayName) + '</span> <span class="tool-spinner"><i class="fa fa-hourglass-half"></i></span>';
|
||||||
_reviewBubble.appendChild(toolDiv);
|
_reviewBubble.appendChild(toolDiv);
|
||||||
|
_currentReviewToolBox = toolDiv;
|
||||||
scrollDown();
|
scrollDown();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function finishReviewToolBox(data) {
|
||||||
|
if (_currentReviewToolBox) {
|
||||||
|
renderToolBoxContent(_currentReviewToolBox, data.toolName, data.input, data.result, data.isError, data.sources);
|
||||||
|
_currentReviewToolBox = null;
|
||||||
|
scrollDown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function finalizeReviewBubble(type, data, agentType) {
|
function finalizeReviewBubble(type, data, agentType) {
|
||||||
if (!_reviewBubble) return;
|
if (!_reviewBubble) return;
|
||||||
const status = document.createElement('div');
|
const status = document.createElement('div');
|
||||||
@@ -120,5 +137,6 @@ function finalizeReviewBubble(type, data, agentType) {
|
|||||||
_reviewBubble = null;
|
_reviewBubble = null;
|
||||||
_reviewTextEl = null;
|
_reviewTextEl = null;
|
||||||
_reviewText = '';
|
_reviewText = '';
|
||||||
|
_currentReviewToolBox = null;
|
||||||
scrollDown();
|
scrollDown();
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user