diff --git a/backend/apps/agents/browser/browser_metrics.py b/backend/apps/agents/browser/browser_metrics.py index 19da5f4d..f3f7967e 100644 --- a/backend/apps/agents/browser/browser_metrics.py +++ b/backend/apps/agents/browser/browser_metrics.py @@ -22,6 +22,7 @@ import json import logging import os import re +import threading import time from collections import Counter @@ -191,4 +192,30 @@ def record_task(session_id, browser_id, task, status, started_at, turns, f"tools={len(action_log)} tok_in={summary['tokens_in']} tok_out={summary['tokens_out']} " f"recurring_errs={summary['recurring_errors'][:2]}" ) + _maybe_self_audit() return summary + + +_AUDIT_EVERY_N = 25 # refresh the learning self-audit roughly this often +_task_count = 0 + + +def _maybe_self_audit() -> None: + """Every N finished tasks, refresh the self-audit report in a daemon thread so + it never adds latency to a run (the audit is ~3ms but stays off the hot path). + Proposal-only: it writes a report a human reads, it changes nothing.""" + global _task_count + _task_count += 1 + if _task_count % _AUDIT_EVERY_N != 0: + return + + def _run(): + try: + from backend.apps.agents.browser import browser_self_audit + browser_self_audit.run_and_write() + except Exception: + pass + try: + threading.Thread(target=_run, name="browser-self-audit", daemon=True).start() + except Exception: + pass diff --git a/backend/tests/test_browser_self_audit.py b/backend/tests/test_browser_self_audit.py index 8c9fbf71..5d8fd8b3 100644 --- a/backend/tests/test_browser_self_audit.py +++ b/backend/tests/test_browser_self_audit.py @@ -60,6 +60,32 @@ def test_clean_history_proposes_nothing(): assert "learning cleanly" in audit.render_report(r) +def test_audit_fires_every_n_finished_tasks(monkeypatch, tmp_path): + # the trigger refreshes the report once every N tasks, off the hot path. Make + # threads synchronous so the test is deterministic, and use a small N. + from backend.apps.agents.browser import browser_metrics as m + monkeypatch.setenv("OPENSWARM_BROWSER_METRICS_DIR", str(tmp_path)) + m._metrics_dir_cache = None + m._task_count = 0 + monkeypatch.setattr(m, "_AUDIT_EVERY_N", 5) + + class _SyncThread: + def __init__(self, target=None, **kw): + self._t = target + + def start(self): + self._t() + monkeypatch.setattr(m.threading, "Thread", _SyncThread) + + log = [{"tool": "BrowserClickIndex", "elapsed_ms": 5, "result_summary": "ok"}] + report = tmp_path / "self_audit_report.md" + for i in range(4): + m.record_task(f"s{i}", "b1", "t", "completed", 0, 7, log, {}) + assert not report.exists(), "audit fired before N tasks" + m.record_task("s5", "b1", "t", "completed", 0, 7, log, {}) + assert report.exists(), "audit did not fire at the Nth task" + + def test_run_and_write_emits_a_report_file_and_never_raises(): d = tempfile.mkdtemp() _write(d, "tasks.jsonl", [])