fix p44
This commit is contained in:
@@ -2682,6 +2682,7 @@ final readonly class AgentRunner
|
||||
{
|
||||
$fullOutput = '';
|
||||
$thinkingNoticeShown = false;
|
||||
$stoppedByFinalAnswerGuard = false;
|
||||
$chunker = new StreamChunker();
|
||||
|
||||
$this->thinkSuppressor->reset();
|
||||
@@ -2706,11 +2707,36 @@ final readonly class AgentRunner
|
||||
continue;
|
||||
}
|
||||
|
||||
$fullOutput .= $cleanToken;
|
||||
$guardReason = null;
|
||||
$cleanToken = $this->guardFinalAnswerToken($fullOutput, $cleanToken, $guardReason);
|
||||
|
||||
$chunk = $chunker->push($cleanToken);
|
||||
if ($chunk !== null) {
|
||||
yield $this->systemMsg($chunk, 'answer');
|
||||
if ($cleanToken !== '') {
|
||||
$fullOutput .= $cleanToken;
|
||||
|
||||
$chunk = $chunker->push($cleanToken);
|
||||
if ($chunk !== null) {
|
||||
yield $this->systemMsg($chunk, 'answer');
|
||||
}
|
||||
}
|
||||
|
||||
if ($guardReason !== null) {
|
||||
$stoppedByFinalAnswerGuard = true;
|
||||
|
||||
$finalChunk = $chunker->flush();
|
||||
if ($finalChunk !== null) {
|
||||
yield $this->systemMsg($finalChunk, 'answer');
|
||||
}
|
||||
|
||||
$guardMessage = $this->agentRunnerConfig->getFinalAnswerGuardTruncationMessage();
|
||||
$fullOutput .= $guardMessage;
|
||||
yield $this->systemMsg($guardMessage, 'answer');
|
||||
|
||||
$this->agentLogger->warning('Final answer guard stopped LLM output', [
|
||||
'reason' => $guardReason,
|
||||
'outputLength' => mb_strlen($fullOutput, 'UTF-8'),
|
||||
]);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
@@ -2730,6 +2756,10 @@ final readonly class AgentRunner
|
||||
return $fullOutput;
|
||||
}
|
||||
|
||||
if ($stoppedByFinalAnswerGuard) {
|
||||
return $fullOutput;
|
||||
}
|
||||
|
||||
$finalChunk = $chunker->flush();
|
||||
if ($finalChunk !== null) {
|
||||
yield $this->systemMsg($finalChunk, 'answer');
|
||||
@@ -2747,6 +2777,112 @@ final readonly class AgentRunner
|
||||
return $fullOutput;
|
||||
}
|
||||
|
||||
private function guardFinalAnswerToken(string $currentOutput, string $nextToken, ?string &$reason): string
|
||||
{
|
||||
$reason = null;
|
||||
|
||||
if (!$this->agentRunnerConfig->isFinalAnswerGuardEnabled()) {
|
||||
return $nextToken;
|
||||
}
|
||||
|
||||
$maxOutputChars = max(1000, $this->agentRunnerConfig->getFinalAnswerGuardMaxOutputChars());
|
||||
$currentChars = mb_strlen($currentOutput, 'UTF-8');
|
||||
$nextChars = mb_strlen($nextToken, 'UTF-8');
|
||||
|
||||
if (($currentChars + $nextChars) > $maxOutputChars) {
|
||||
$reason = 'max_output_chars';
|
||||
$remainingChars = max(0, $maxOutputChars - $currentChars);
|
||||
|
||||
return $remainingChars > 0 ? mb_substr($nextToken, 0, $remainingChars, 'UTF-8') : '';
|
||||
}
|
||||
|
||||
$candidate = $currentOutput . $nextToken;
|
||||
$cutoffBytes = $this->detectRepeatedFinalAnswerLineCutoff($candidate);
|
||||
|
||||
if ($cutoffBytes === null) {
|
||||
return $nextToken;
|
||||
}
|
||||
|
||||
$reason = 'repeated_line';
|
||||
$currentBytes = strlen($currentOutput);
|
||||
|
||||
if ($cutoffBytes <= $currentBytes) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return mb_strcut($nextToken, 0, $cutoffBytes - $currentBytes, 'UTF-8');
|
||||
}
|
||||
|
||||
private function detectRepeatedFinalAnswerLineCutoff(string $text): ?int
|
||||
{
|
||||
if (!$this->agentRunnerConfig->isFinalAnswerRepeatedLineGuardEnabled()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (mb_strlen($text, 'UTF-8') < max(0, $this->agentRunnerConfig->getFinalAnswerRepeatedLineMinOutputChars())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (preg_match_all('/[^\r\n]+/u', $text, $matches, PREG_OFFSET_CAPTURE) === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$lines = $matches[0] ?? [];
|
||||
$window = max(10, $this->agentRunnerConfig->getFinalAnswerRepeatedLineTrailingWindowLines());
|
||||
if (count($lines) > $window) {
|
||||
$lines = array_slice($lines, -$window);
|
||||
}
|
||||
|
||||
$counts = [];
|
||||
$maxRepetitions = max(1, $this->agentRunnerConfig->getFinalAnswerRepeatedLineMaxRepetitions());
|
||||
|
||||
foreach ($lines as $lineMatch) {
|
||||
$line = (string) ($lineMatch[0] ?? '');
|
||||
$offset = (int) ($lineMatch[1] ?? 0);
|
||||
$normalizedLine = $this->normalizeFinalAnswerLineForRepetitionGuard($line);
|
||||
|
||||
if ($normalizedLine === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$counts[$normalizedLine] = ($counts[$normalizedLine] ?? 0) + 1;
|
||||
|
||||
if ($counts[$normalizedLine] > $maxRepetitions) {
|
||||
return $offset;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function normalizeFinalAnswerLineForRepetitionGuard(string $line): string
|
||||
{
|
||||
$line = html_entity_decode(strip_tags($line), ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
$line = preg_replace('/^\s*(?:[-*•]+|\d+[.)])\s*/u', '', $line) ?? $line;
|
||||
$line = preg_replace('/\s+/u', ' ', $line) ?? $line;
|
||||
$line = trim($line, " \t\n\r\0\x0B:;.-");
|
||||
|
||||
if ($line === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
foreach ($this->agentRunnerConfig->getFinalAnswerRepeatedLineIgnorePatterns() as $pattern) {
|
||||
try {
|
||||
if (@preg_match($pattern, $line) === 1) {
|
||||
return '';
|
||||
}
|
||||
} catch (Throwable) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (mb_strlen($line, 'UTF-8') < max(1, $this->agentRunnerConfig->getFinalAnswerRepeatedLineMinLineChars())) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return mb_strtolower($line, 'UTF-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a deterministic safety answer for environments where the LLM returns no tokens.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user