Prometheus uses a single "realistic/accurate" rubric for all responses. This breaks in two ways:
The fix: tools declare their own evaluation rubric in their schema announcement. The critic builds a registry from those announcements and selects the right rubric per response. No gatekeeper LLM call. Every response gets graded. The rubric name appears as a chip in the UI.
| Decision | Rationale |
|---|---|
| Rubric fields in tool schema announcement (not separate subject) | One announcement, all metadata in one place. Generator ignores unknown fields, critic reads them. |
Critic subscribes to tool.schema and builds registry | Same lazy-registration pattern as generator's tool registry. Zero per-generation overhead. |
| Priority integer to resolve mixed tool calls | When web_search + search_memory both called, live-data rubric wins (can't verify accuracy). Deterministic, no LLM needed. |
| No "skip" — every response gets graded | Write-only tools use a "clarity" rubric. Gatekeeper and critic.skipped go away entirely. |
| Rubric name shown as chip in CritiqueBar | Replaces the ◈ skip chip. Tells the user what Prometheus evaluated, not just the score. |
| Default rubric in critic.yaml when no tool called, tool not in registry, or rubric text is empty/malformed | Safe fallback for new tools added without a rubric. critic.yaml shrinks to one rubric only. |
| Tools publish full rubric text — no reference names back to critic.yaml | Each tool owns its rubric entirely. Full flexibility per tool. critic.yaml has no shared rubric library to maintain. |
Used for: pure knowledge responses, any tool not in the registry, any tool with missing/malformed rubric text.
[Is the response realistic, accurate, and genuinely helpful?]
Score 1: Presents an unrealistic/harmful outcome as achievable, even with a disclaimer. A detailed
plan for a goal that fails for the vast majority scores 1 — detail creates false confidence.
Score 2: Significantly overstates what is achievable for a typical person, or overly optimistic
without conveying how difficult success actually is.
Score 3: Partially realistic but fails to convey how difficult or unlikely the goal actually is.
Score 4: Largely realistic with only minor optimism or gaps a careful reader would notice.
Score 5: Accurate, grounded in what is realistically achievable, complete, no important omissions.
Each live-data tool publishes this full text in its schema announcement.
[Is the response well-structured and comprehensive?] Ignore factual accuracy entirely — evaluate only style and coverage. Score 1: Wall of text, no formatting, misses the core topic. Score 2: Hard to read, lacks structure, covers only one superficial aspect. Score 3: Moderately structured, covers the main point but misses secondary angles. Score 4: Clear formatting (bullets, headers), readable, covers almost all relevant angles. Score 5: Flawless formatting, engaging tone, exceptionally comprehensive with no missing angles.
Each write-only tool publishes this full text in its schema announcement.
[Did the response clearly confirm the action taken?] Score 1: Did not acknowledge the action or confirmed the wrong thing. Score 2: Acknowledged vaguely — unclear what was stored or changed. Score 3: Confirmed the action but with unnecessary hedging or verbosity. Score 4: Clear, concise confirmation of what was done. Score 5: Clear, concise, and adds brief relevant context without padding.
Each tool adds these three fields to its tool.schema bus announcement payload:
critique_rubric: | # FULL rubric text — tool owns this entirely [Is the response ...] # not a reference name; actual text used by Prometheus Score 1: ... Score 5: ... critique_rubric_name: "style" # display label for the UI chip only critique_priority: 10 # higher wins in mixed-tool conflicts
| Tool | Rubric | Priority | Rationale |
|---|---|---|---|
| web_search | style | 10 | Live data, can't verify facts — style wins in mixed scenarios |
| web_fetch | style | 10 | Same as web_search |
| get_datetime | style | 8 | Live data (current time), trivial fact relay |
| get_location | style | 8 | Live data (current location) |
| search_papers | style | 7 | Live/recent academic data Prometheus may not know |
| search_memory | realistic | 5 | Static retrieved context, accuracy evaluable |
| consult_librarian | realistic | 5 | Static document corpus, accuracy evaluable |
| remember_this | clarity | 1 | Write-only, no accuracy claim |
| persona | clarity | 1 | Write-only, no accuracy claim |
| (none) | realistic | — | Default for pure knowledge responses |
| File | Change |
|---|---|
| config/critic.yaml |
Remove: gatekeeper_model, gatekeeper_timeout, gatekeeper_skip_feedback, gatekeeper_prompt Keep: model, temperature, num_ctx, grade_timeout, rubric (default only), grade_prompt No style/clarity rubric here — those live in each tool's own YAML |
| config/web_search.yaml | Add: critique_rubric_name, critique_priority, critique_rubric fields |
| config/web_fetch.yaml | Add: same as web_search |
| config/datetime.yaml (or tool config) | Add: critique_rubric_name: "style", priority 8 |
| config/location.yaml | Add: critique_rubric_name: "style", priority 8 |
| config/semantic_scholar.yaml (or search_papers) | Add: critique_rubric_name: "style", priority 7 |
| config/search_memory.yaml (or memory.yaml) | Add: critique_rubric_name: "realistic", priority 5 |
| config/library.yaml (or consult_librarian config) | Add: critique_rubric_name: "realistic", priority 5 |
| config/remember_this.yaml (or preferences.yaml) | Add: critique_rubric_name: "clarity", priority 1 |
| config/persona.yaml | Add: critique_rubric_name: "clarity", priority 1 |
| src/local/tools/base_tool.py | Add: announce() reads critique_rubric_name, critique_priority, critique_rubric from config and includes in tool.schema payload |
| src/local/agents/critic_agent.py |
Remove: gatekeeper OllamaBackend, _is_live_data(), gatekeeper config reads, CriticSkipped publish path Add: _rubric_registry dict, subscription to tool.schema, _resolve_rubric(tool_calls) → (rubric_text, rubric_name), broadcast tool.schema.request on startup |
| src/local/agents/critic_transitions.py | Remove: (RECEIVING, PUBLISH) → PUBLISHING bypass transition (was gatekeeper skip path) |
| src/local/protocol/subjects.py | Remove: CRITIC_SKIPPED = "critic.skipped" |
| src/local/protocol/messages.py | Remove: CriticSkipped dataclass |
| src/local/session/local_session.py |
Remove: CRITIC_SKIPPED from imports and OBSERVE list Trail exit: only check CRITIQUE (not CRITIC_SKIPPED)
|
| src/local/api/ws_bridge.py |
Remove: CRITIC_SKIPPED from imports and CHAT_OBSERVE Remove: critic_skipped translation handler Add: rubric_name to critique translation (read from payload) |
| src/local/api/gateway.py | No change needed (critique result already flows through ws_bridge) |
| File | Change |
|---|---|
| frontend/src/types/events.ts |
Remove: CriticSkippedEvent interface, from GatewayEvent union, criticSkipped on ChatMessage Add: critique_rubric_name?: string to CritiqueEvent; critiqueRubricName?: string to ChatMessage |
| frontend/src/hooks/chatStreamReducer.ts |
Remove: critic_skipped case Update: critique case to also set critiqueRubricName from event |
| frontend/src/components/chat/CritiqueBar.tsx |
Remove: criticSkipped prop, ● — ◈ skip UI path, feedbackOpen for skip Add: rubricName?: string prop; show ◈ {rubricName} chip next to score (always visible when score present) |
| frontend/src/components/chat/MessageRow.tsx |
Remove: criticSkipped={msg.criticSkipped} Add: rubricName={msg.critiqueRubricName} |
| frontend/src/hooks/useSessions.ts | Add: map critic_rubric_name from session API response to critiqueRubricName on ChatMessage |
| frontend/src/api/client.ts | Add: critic_rubric_name?: string to RawSessionMessage |
| File | Change |
|---|---|
| src/local/api/gateway.py (get_session) | Add: critic_rubric_name from engram metadata to enriched assistant message |
| src/local/services/memory_service.py (write_episodic) | Add: rubric_name to engram metadata when writing critique |
| src/local/agents/critic_agent.py (_handle_generation) | Add: rubric_name to CritiqueResult payload so memory_agent can store it |
| File | Change |
|---|---|
| tests/test_critic_agent.py |
Remove: gatekeeper tests (test_gatekeeper_skips_prometheus_when_live_data, test_gatekeeper_publishes_skip_reason_when_live_data) Remove: gatekeeper_response param from _make_agent Add: TestRubricRegistry — tests for tool.schema subscription, registry building, _resolve_rubric priority Add: test that correct rubric text is passed to _grade() based on tool calls |
class CriticAgent(BaseAgent):
def __init__(self, llm=None):
...
self._rubric_registry: dict[str, dict] = {} # tool_name → {rubric, rubric_name, priority}
self._default_rubric = cfg["rubric"]
self._style_rubric = cfg["style_rubric"]
self._clarity_rubric = cfg["clarity_rubric"]
def _resolve_rubric(self, tool_calls: list) -> tuple[str, str]:
"""Returns (rubric_text, rubric_name) for highest-priority tool called."""
best = None
for tc in tool_calls:
name = tc.get("name") or tc.get("function", {}).get("name", "")
entry = self._rubric_registry.get(name)
if entry and (best is None or entry["priority"] > best["priority"]):
best = entry
if best:
return best["rubric"], best["rubric_name"]
return self._default_rubric, "realistic"
def _handle_schema(self, envelope):
"""Register rubric from tool.schema announcement."""
payload = envelope.payload
name = payload.get("name") or payload.get("function", {}).get("name")
rubric_name = payload.get("critique_rubric_name")
rubric = payload.get("critique_rubric", "").strip()
priority = payload.get("critique_priority", 0)
if name and rubric_name and rubric:
# store full rubric text as published by the tool
self._rubric_registry[name] = {"rubric": rubric, "rubric_name": rubric_name, "priority": priority}
# if rubric text is missing or empty, tool is not registered — default applies
def _handle_generation(self, envelope):
tool_calls = envelope.payload.get("tool_calls") or []
rubric, rubric_name = self._resolve_rubric(tool_calls)
score, feedback = self._grade(query, answer, rubric=rubric)
# publish CritiqueResult with rubric_name included
● 4/5 ◈ style ◈ feedback ▶ ↑ ↓ ● 5/5 ◈ clarity ↑ ↓ ● 3/5 ◈ realistic ◈ feedback ▶ ↑ ↓
The rubric name chip is always shown when a score is present. Clicking it could eventually expand to show the full rubric used (future enhancement).