Skip to content

F009: Get Calibration Endpoint

Field Value
Feature ID F009
Status Implemented
Priority P1
Depends On F008 (Review Decision)
Blocks None
Decision 30d70c34

Add cstp.getCalibration JSON-RPC method to retrieve confidence calibration statistics, enabling agents to understand their decision-making accuracy and adjust future confidence levels.

  1. Calculate Brier scores for reviewed decisions
  2. Break down accuracy by confidence level
  3. Identify over/under-confidence patterns
  4. Filter by category, stakes, time range
  5. Provide actionable recommendations
  • Real-time calibration updates (batch calculation)
  • Cross-agent calibration comparison (future)
  • Automatic confidence adjustment (agent decides)

cstp.getCalibration

{
"jsonrpc": "2.0",
"method": "cstp.getCalibration",
"params": {
"filters": {
"agent": "emerson",
"category": "architecture",
"minDecisions": 5,
"since": "2026-01-01"
},
"groupBy": "confidenceBucket"
},
"id": 1
}
Field Type Required Description
filters object Filter criteria
filters.agent string Filter by agent ID (from recorded_by)
filters.category string Filter by category
filters.stakes string Filter by stakes level
filters.since string ISO date, only decisions after
filters.until string ISO date, only decisions before
filters.minDecisions int Minimum decisions for stats (default: 5)
groupBy string Group by: confidenceBucket, category, stakes, agent
{
"jsonrpc": "2.0",
"result": {
"overall": {
"brierScore": 0.18,
"accuracy": 0.72,
"totalDecisions": 45,
"reviewedDecisions": 38,
"calibrationGap": -0.08,
"interpretation": "slightly_overconfident"
},
"byConfidenceBucket": [
{
"bucket": "0.9-1.0",
"decisions": 12,
"successRate": 0.75,
"expectedRate": 0.95,
"gap": -0.20,
"interpretation": "overconfident"
},
{
"bucket": "0.7-0.9",
"decisions": 18,
"successRate": 0.78,
"expectedRate": 0.80,
"gap": -0.02,
"interpretation": "well_calibrated"
},
{
"bucket": "0.5-0.7",
"decisions": 8,
"successRate": 0.62,
"expectedRate": 0.60,
"gap": 0.02,
"interpretation": "well_calibrated"
}
],
"recommendations": [
{
"type": "confidence_adjustment",
"message": "When you feel 90%+ confident, outcomes suggest ~75%. Consider 75-80% instead.",
"severity": "warning"
},
{
"type": "strength",
"message": "Your 70-90% confidence range is well calibrated. Trust this range.",
"severity": "info"
}
],
"queryTime": "2026-02-12T14:30:00Z"
},
"id": 1
}
{
"jsonrpc": "2.0",
"result": {
"overall": null,
"byConfidenceBucket": [],
"recommendations": [
{
"type": "insufficient_data",
"message": "Only 3 reviewed decisions. Need at least 5 for calibration stats.",
"severity": "info"
}
],
"queryTime": "2026-02-12T14:30:00Z"
},
"id": 1
}

Measures prediction accuracy. Lower is better.

Brier = (1/N) × Σ(confidence - outcome)²
Where outcome = 1 for success, 0 for failure
Brier Score Interpretation
0.00 - 0.10 Excellent calibration
0.10 - 0.20 Good calibration
0.20 - 0.30 Fair calibration
0.30+ Poor calibration

Difference between stated confidence and actual success rate.

Gap = actual_success_rate - average_confidence
Positive = underconfident (better than you think)
Negative = overconfident (worse than you think)

Group decisions by confidence level:

  • 0.9-1.0: High confidence
  • 0.7-0.9: Moderate-high
  • 0.5-0.7: Moderate
  • 0.0-0.5: Low confidence

async def get_reviewed_decisions(
decisions_path: str | None = None,
agent: str | None = None,
category: str | None = None,
stakes: str | None = None,
since: str | None = None,
until: str | None = None,
) -> list[dict]:
"""Get all reviewed decisions matching filters."""
base = Path(decisions_path or DECISIONS_PATH)
decisions = []
for yaml_file in base.rglob("*-decision-*.yaml"):
with open(yaml_file) as f:
data = yaml.safe_load(f)
# Filter: must be reviewed with outcome
if data.get("status") != "reviewed":
continue
if "outcome" not in data:
continue
# Apply filters
if agent and data.get("recorded_by") != agent:
continue
if category and data.get("category") != category:
continue
if stakes and data.get("stakes") != stakes:
continue
if since and data.get("date", "") < since:
continue
if until and data.get("date", "") > until:
continue
decisions.append(data)
return decisions
@dataclass
class ConfidenceBucket:
bucket: str
decisions: int
success_rate: float
expected_rate: float
gap: float
interpretation: str
@dataclass
class CalibrationResult:
brier_score: float
accuracy: float
total_decisions: int
reviewed_decisions: int
calibration_gap: float
interpretation: str
@dataclass
class CalibrationRecommendation:
type: str
message: str
severity: str # info, warning, error
def calculate_calibration(decisions: list[dict]) -> CalibrationResult | None:
"""Calculate calibration metrics from reviewed decisions."""
if len(decisions) < 5:
return None
outcomes = []
confidences = []
for d in decisions:
confidence = d.get("confidence", 0.5)
outcome = 1.0 if d.get("outcome") == "success" else 0.0
# Partial success = 0.5
if d.get("outcome") == "partial":
outcome = 0.5
outcomes.append(outcome)
confidences.append(confidence)
# Brier score
brier = sum((c - o) ** 2 for c, o in zip(confidences, outcomes)) / len(decisions)
# Accuracy (binary: success or not)
accuracy = sum(1 for o in outcomes if o >= 0.5) / len(decisions)
# Calibration gap
avg_confidence = sum(confidences) / len(confidences)
gap = accuracy - avg_confidence
# Interpretation
if abs(gap) < 0.05:
interpretation = "well_calibrated"
elif gap < -0.10:
interpretation = "overconfident"
elif gap < 0:
interpretation = "slightly_overconfident"
elif gap > 0.10:
interpretation = "underconfident"
else:
interpretation = "slightly_underconfident"
return CalibrationResult(
brier_score=round(brier, 3),
accuracy=round(accuracy, 3),
total_decisions=len(decisions),
reviewed_decisions=len(decisions),
calibration_gap=round(gap, 3),
interpretation=interpretation,
)
def calculate_buckets(decisions: list[dict]) -> list[ConfidenceBucket]:
"""Calculate calibration by confidence bucket."""
buckets = {
"0.9-1.0": {"min": 0.9, "max": 1.0, "decisions": [], "expected": 0.95},
"0.7-0.9": {"min": 0.7, "max": 0.9, "decisions": [], "expected": 0.80},
"0.5-0.7": {"min": 0.5, "max": 0.7, "decisions": [], "expected": 0.60},
"0.0-0.5": {"min": 0.0, "max": 0.5, "decisions": [], "expected": 0.25},
}
for d in decisions:
conf = d.get("confidence", 0.5)
for name, bucket in buckets.items():
if bucket["min"] <= conf < bucket["max"] or (bucket["max"] == 1.0 and conf == 1.0):
bucket["decisions"].append(d)
break
results = []
for name, bucket in buckets.items():
if len(bucket["decisions"]) < 3:
continue
successes = sum(1 for d in bucket["decisions"] if d.get("outcome") == "success")
success_rate = successes / len(bucket["decisions"])
gap = success_rate - bucket["expected"]
if abs(gap) < 0.10:
interpretation = "well_calibrated"
elif gap < 0:
interpretation = "overconfident"
else:
interpretation = "underconfident"
results.append(ConfidenceBucket(
bucket=name,
decisions=len(bucket["decisions"]),
success_rate=round(success_rate, 2),
expected_rate=bucket["expected"],
gap=round(gap, 2),
interpretation=interpretation,
))
return results
def generate_recommendations(
overall: CalibrationResult | None,
buckets: list[ConfidenceBucket],
) -> list[CalibrationRecommendation]:
"""Generate actionable recommendations."""
recs = []
if overall is None:
recs.append(CalibrationRecommendation(
type="insufficient_data",
message="Need at least 5 reviewed decisions for calibration.",
severity="info",
))
return recs
# Overall calibration feedback
if overall.interpretation == "overconfident":
recs.append(CalibrationRecommendation(
type="confidence_adjustment",
message=f"You're overconfident by {abs(overall.calibration_gap)*100:.0f}%. Consider lowering confidence estimates.",
severity="warning",
))
elif overall.interpretation == "underconfident":
recs.append(CalibrationRecommendation(
type="confidence_adjustment",
message=f"You're underconfident by {overall.calibration_gap*100:.0f}%. Trust yourself more.",
severity="info",
))
# Bucket-specific feedback
for bucket in buckets:
if bucket.interpretation == "overconfident" and bucket.gap < -0.15:
recs.append(CalibrationRecommendation(
type="bucket_warning",
message=f"At {bucket.bucket} confidence, actual success is {bucket.success_rate*100:.0f}%. Adjust to ~{bucket.success_rate*100:.0f}%.",
severity="warning",
))
# Strength recognition
well_calibrated = [b for b in buckets if b.interpretation == "well_calibrated"]
if well_calibrated:
ranges = ", ".join(b.bucket for b in well_calibrated)
recs.append(CalibrationRecommendation(
type="strength",
message=f"Well calibrated in {ranges} range. Trust these estimates.",
severity="info",
))
return recs
async def _handle_get_calibration(params: dict[str, Any], agent_id: str) -> dict[str, Any]:
filters = params.get("filters", {})
decisions = await get_reviewed_decisions(
category=filters.get("category"),
stakes=filters.get("stakes"),
since=filters.get("since"),
until=filters.get("until"),
)
min_decisions = filters.get("minDecisions", 5)
overall = calculate_calibration(decisions) if len(decisions) >= min_decisions else None
buckets = calculate_buckets(decisions)
recommendations = generate_recommendations(overall, buckets)
return {
"overall": overall.to_dict() if overall else None,
"byConfidenceBucket": [b.to_dict() for b in buckets],
"recommendations": [r.to_dict() for r in recommendations],
"queryTime": datetime.now(UTC).isoformat(),
}
dispatcher.register("cstp.getCalibration", _handle_get_calibration)

Test cases:

  • Calibration with sufficient data
  • Insufficient data response
  • Filter by category
  • Bucket calculations
  • Recommendation generation
  • Edge cases (all success, all failure)

File Action Description
a2a/cstp/calibration_service.py Create Calibration calculation logic
a2a/cstp/decision_service.py Modify Add get_reviewed_decisions
a2a/cstp/dispatcher.py Modify Register new method
tests/test_calibration_service.py Create Unit tests
tests/test_f009_get_calibration.py Create Integration tests

┌────────────────────────────────────────────────────────────┐
│ │
│ recordDecision ──▶ reviewDecision ──▶ getCalibration │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ "confidence: 0.85" "outcome: success" "Brier: 0.18" │
│ │ │
│ ▼ │
│ ◀───────────────────────────────────────────── │
│ Next decision: "Similar decisions had 72% success. │
│ Your 85% confidence may be high." │
│ │
└────────────────────────────────────────────────────────────┘

Phase Time
Data Collection 1h
Calibration Calculation 2h
Recommendations 1h
Dispatcher Integration 30m
Tests 1h
Total ~5.5h

  • Category-specific calibration: “You’re overconfident in architecture but well-calibrated in process”
  • Trend analysis: “Your calibration improved 15% this month”
  • Cross-agent comparison: “Your Brier score is better than 70% of agents”
  • Active nudging: Inject calibration warnings into queryDecisions responses