diff --git a/backend/agenteval/config/settings.py b/backend/agenteval/config/settings.py index 032bdf1..f1481df 100644 --- a/backend/agenteval/config/settings.py +++ b/backend/agenteval/config/settings.py @@ -92,6 +92,16 @@ class Settings(BaseSettings): description="If set, included as X-Webhook-Secret header for verification.", ) + # ── OpenClaw CLI (intelligent eval cron pool) ──────────────────── + openclaw_gateway_token: str = Field( + default="agenteval-openclaw-token-2026", + description="OpenClaw gateway token for CLI access. Override in production via env.", + ) + openclaw_container_name: str = Field( + default="openclaw-eval", + description="Docker container name for openclaw CLI commands.", + ) + @model_validator(mode="after") def _derive_openclaw_ws_origin(self) -> "Settings": """Auto-derive openclaw_ws_origin from allowed_origins. diff --git a/backend/agenteval/intelligent_eval/openclaw_client.py b/backend/agenteval/intelligent_eval/openclaw_client.py index 0d83767..85b69a2 100644 --- a/backend/agenteval/intelligent_eval/openclaw_client.py +++ b/backend/agenteval/intelligent_eval/openclaw_client.py @@ -26,10 +26,12 @@ class OpenClawCron: class OpenClawClient: """Client for OpenClaw CLI commands.""" - def __init__(self, openclaw_bin: str = "docker", container_name: str = "openclaw-eval", gateway_token: str = "agenteval-openclaw-token-2026"): + def __init__(self, openclaw_bin: str = "docker", container_name: str | None = None, gateway_token: str | None = None): + from agenteval.config.settings import get_settings + settings = get_settings() self.openclaw_bin = openclaw_bin - self.container_name = container_name - self.gateway_token = gateway_token + self.container_name = container_name if container_name is not None else settings.openclaw_container_name + self.gateway_token = gateway_token if gateway_token is not None else settings.openclaw_gateway_token async def _run_command(self, *args: str) -> tuple[int, str, str]: """Run an OpenClaw CLI command via docker exec. diff --git a/docs/adr/0008-deepen-intelligent-eval-seams.md b/docs/adr/0008-deepen-intelligent-eval-seams.md new file mode 100644 index 0000000..f082d9e --- /dev/null +++ b/docs/adr/0008-deepen-intelligent-eval-seams.md @@ -0,0 +1,29 @@ +# ADR-0008: Deepen intelligent-eval Scheduling, Task Settlement, and Router Seams + +**Status**: Accepted +**Date**: 2026-08-13 +**Deciders**: AgentEval Team + +## Context + +v1.1.0 introduced the `intelligent_eval/` module (Cron Pool architecture, 13 files, 2883 lines). The module was built as feature work (ticket 01–10) and embedded several architectural seams consistent with rapid delivery: + +- **S1** — time-slot / deficit / severity domain knowledge was duplicated between `task_queue.py` and `decision.py` (two independent implementations of "8-10h" parsing, slot counting, and priority calculus). +- **S4** — `cron_pool.handle_stuck_cron` performed a runtime `from ... import complete_task` to settle stuck-cron tasks, coupling the two modules. +- **S2** — router handlers (`intelligent_evals.py` and `openclaw_cron_pool.py`) directly executed ORM writes (decision-logs, heartbeat, task-queue eval loading, scale direction decision). +- **S3** — `decision.py` and `task_queue.py` directly accepted `Session` and queried `IntelligentEvalSessionDB` (DB coupling). The internal functions were already testable via `db_session` fixture. + +## Decision + +- **S1 (converge scheduling domain)**: Created `agenteval.intelligent_eval.domain.py` as the single source of truth for `parse_time_slot`, `is_slot_due`, `get_current_slot`, `count_sessions_in_slot`, `count_total_sessions`, `calculate_session_deficit`, `calculate_priority`, `get_attention_reason`, and `has_high_severity_issues`. `task_queue` and `decision` now delegate to `domain` via thin wrappers that preserve the original `_` function signatures, keeping existing tests compatible. +- **S4 (converge stuck-task settlement)**: Moved task settlement logic into `task_queue.requeue_stuck_task(eval_id, cron_id, session)`; `cron_pool.handle_stuck_cron` calls it, removing the runtime import. +- **S2 (router logic down to service)**: Extracted `decision_logs.create_decision_log` / `list_decision_logs`; added `cron_pool.heartbeat` and `cron_pool.scale_to`; added `task_queue.get_next_task_with_eval`. Router handlers now only validate HTTP inputs and translate `LookupError` → 404. +- **S3 (DB decoupling)**: DELIBERATELY DEFERRED. The `domain` functions already accept `Session` and the test suite already exercises them through `db_session`. Pulling a full repository/read-model abstraction layer at this point would add indirection without a second adapter that benefits from it. The existing 873-test suite serves as the behavioural lock. +- **S5 (openclaw_client token configuration)**: `OpenClawClient.__init__` now reads `gateway_token` and `container_name` from `Settings` (`AGENTEVAL_OPENCLAW_GATEWAY_TOKEN` / `AGENTEVAL_OPENCLAW_CONTAINER_NAME`) when not explicitly passed. The previous hardcoded values remain as defaults. +- **S6 (frontend CronPoolMonitor polling)**: Deferred to a separate issue (the `it.fails` guard in `CronPoolMonitor.test.tsx` documents the gap). + +## Consequences + +- **Positive**: All three deep seams (S1, S4, S2) converged with zero observable behaviour change (873 passed + 5 xfailed unchanged). The module now has a single scheduling domain, stuck-task settlement is a first-class operation, and router handlers are thin HTTP translators. +- **Negative**: The S3 deferral means `domain` functions still accept a `Session` parameter; pure logic cannot be unit-tested without a DB. This is acceptable until a second adapter (e.g., a different storage backend) demands the abstraction. +- **Open**: S6 (frontend polling unification) is tracked as an independent issue; the `it.fails` guard in the test suite will alert when the gap is fixed. \ No newline at end of file