Dynamic Scoring¶
Scoring drives every "what to do next" decision in SynthOrg: which task to assign, which agent to pick, which strategy to apply. The scoring layer at synthorg.engine.assignment.scoring is pluggable: each strategy is a ScoringStrategy implementation registered in the strategy registry. This guide shows how to add a custom strategy, expose its hyperparameters, and observe its outputs.
Strategy contract¶
from synthorg.engine.assignment.scoring.protocol import (
ScoringStrategy,
ScoringContext,
ScoreResult,
)
class FreshnessBoostedScorer:
name = "freshness_boosted"
def __init__(self, *, boost_factor: float = 0.2) -> None:
self._boost = boost_factor
async def score(self, context: ScoringContext) -> ScoreResult:
base = await context.base_score()
recency_bonus = self._recency_term(context)
return ScoreResult(
value=base + self._boost * recency_bonus,
details={"base": base, "recency_bonus": recency_bonus},
)
def _recency_term(self, context: ScoringContext) -> float:
elapsed = context.now - context.candidate.last_active
return max(0.0, 1.0 - (elapsed.total_seconds() / 86400))
Registering the strategy¶
Add to the registry:
# src/synthorg/engine/assignment/scoring/__init__.py
from synthorg.core.registry.strategy import StrategyRegistry
from synthorg.engine.assignment.scoring.freshness_boosted import (
FreshnessBoostedScorer,
)
from synthorg.engine.assignment.scoring.protocol import ScoringStrategy
SCORING_STRATEGY_REGISTRY: StrategyRegistry[ScoringStrategy] = StrategyRegistry(
{
FreshnessBoostedScorer.name: FreshnessBoostedScorer,
},
kind="scoring_strategy",
)
Hyperparameter surface¶
Strategies that carry tunable hyperparameters expose them through the settings system so operators can adjust without redeploying:
# src/synthorg/settings/definitions/scoring.py
from synthorg.settings import SettingDefinition
SCORING_DEFINITIONS = (
SettingDefinition(
namespace="scoring",
key="freshness_boost_factor",
default=0.2,
validator=lambda v: 0.0 <= v <= 1.0,
description="Multiplier on the freshness bonus term.",
),
)
The factory reads the resolved value at strategy construction time:
async def build_freshness_boosted(
settings: SettingsService,
) -> FreshnessBoostedScorer:
factor = await settings.get_float(
"scoring", "freshness_boost_factor"
)
return FreshnessBoostedScorer(boost_factor=factor)
Configuration¶
Pick the active strategy via the scoring.strategy setting:
The setting is hot-reloadable: a change via synthorg config set scoring.strategy <name> swaps the active strategy on the next assignment decision.
Worked example: end-to-end test¶
# tests/unit/engine/scoring/test_freshness_boosted.py
import pytest
from synthorg.engine.assignment.scoring.freshness_boosted import (
FreshnessBoostedScorer,
)
@pytest.mark.unit
async def test_recent_candidate_outscores_stale(
scoring_context_factory,
) -> None:
scorer = FreshnessBoostedScorer(boost_factor=0.5)
recent = await scorer.score(scoring_context_factory(hours_idle=0))
stale = await scorer.score(scoring_context_factory(hours_idle=72))
assert recent.value > stale.value
assert recent.details["recency_bonus"] > stale.details["recency_bonus"]
The scoring_context_factory fixture lives in tests/unit/engine/scoring/conftest.py.
Observability¶
Every score emission fires scoring.score.computed with strategy, score, and the details payload. The dashboard Scoring panel charts the rolling p50/p95/p99 score per strategy so operators can detect drift.
For the operator-tunable weights and thresholds across every shipped scorer, see docs/reference/scoring-hyperparameters.md.