Files
wehub-resource-sync 4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

111 lines
3.8 KiB
Python

"""Quality signals for monitored local agent streams (loop detection, etc.)."""
from __future__ import annotations
import hashlib
from collections import Counter, deque
def _shingle_fingerprint(shingle: tuple[str, ...]) -> int:
"""Return a stable 64-bit int for *shingle* (same input → same value across processes)."""
payload = b"\0".join(t.encode("utf-8") for t in shingle)
digest = hashlib.blake2b(payload, digest_size=8).digest()
return int.from_bytes(digest, "big")
class LoopDetector:
"""Rolling w-shingle detector for repetitive agent output.
Ingests text chunks, tokenizes on whitespace (lowercased), and hashes
consecutive token windows of size ``shingle_size``. Shingles are fingerprinted
with BLAKE2b (8-byte digest) so counts are stable across processes. The last
``window`` emitted fingerprint values are retained; if any fingerprint appears
strictly more than ``threshold`` times in that deque, :attr:`is_looping` is true.
Each logical shingle add or remove is O(1). :meth:`observe` costs
O(number of tokens derived from *chunk*).
"""
__slots__ = (
"_pending",
"_shingle_size",
"_window",
"_shingle_hashes",
"_hash_count",
"_threshold",
"_violators",
)
def __init__(
self,
*,
window: int = 20,
threshold: int = 4,
shingle_size: int = 3,
) -> None:
if window < 1:
raise ValueError("window must be >= 1")
if threshold < 0:
raise ValueError("threshold must be >= 0")
if shingle_size < 1:
raise ValueError("shingle_size must be >= 1")
self._shingle_size = shingle_size
self._window = window
self._threshold = threshold
self._pending: deque[str] = deque(maxlen=shingle_size)
self._shingle_hashes: deque[int] = deque(maxlen=window)
self._hash_count: Counter[int] = Counter()
self._violators: int = 0
def clear(self) -> None:
"""Drop all buffered tokens, shingles, and counts."""
self._pending.clear()
self._shingle_hashes.clear()
self._hash_count.clear()
self._violators = 0
def observe(self, chunk: str) -> None:
"""Feed a text fragment; updates shingles and loop state."""
for token in self._tokens_from_chunk(chunk):
self._pending.append(token)
if len(self._pending) == self._shingle_size:
shingle: tuple[str, ...] = tuple(self._pending)
self._push_hash(_shingle_fingerprint(shingle))
self._pending.popleft()
@property
def is_looping(self) -> bool:
"""True when some shingle fingerprint exceeds ``threshold`` in the rolling window."""
return self._violators > 0
def _tokens_from_chunk(self, chunk: str) -> list[str]:
if not chunk or not chunk.strip():
return []
# ``str.split()`` with no sep splits on arbitrary whitespace and omits empties.
return [p.lower() for p in chunk.split()]
def _push_hash(self, h: int) -> None:
if len(self._shingle_hashes) == self._shingle_hashes.maxlen:
evicted = self._shingle_hashes.popleft()
self._decrement_hash(evicted)
self._shingle_hashes.append(h)
self._increment_hash(h)
def _increment_hash(self, h: int) -> None:
prev = self._hash_count[h]
new = prev + 1
self._hash_count[h] = new
if prev <= self._threshold < new:
self._violators += 1
def _decrement_hash(self, h: int) -> None:
prev = self._hash_count[h]
new = prev - 1
if new == 0:
del self._hash_count[h]
else:
self._hash_count[h] = new
if prev > self._threshold >= new:
self._violators -= 1