Deterministic and RAG Evaluations for the Stockroom Agent
Day 1 · lab notebook
Learning objectives. By the end of this notebook you can:
Explain why “it looked fine when I tried it” is not evidence, and replace it with measured runs.
Name the failure classes of a tool-using agent (wrong tool, wrong arguments, schema drift, loops/retries, context growth, injection, refusal, hallucination) and recognise each one in evaluate_case output.
Read and extend the golden set (data/golden/) and its dataset card.
Write assertion-based tests with plain asserts and with DeepEval (GEval, ToolCorrectnessMetric) wired to a Bedrock-or-fake judge, never to a third-party API.
Evaluate the retrieval step with RAGAS (Faithfulness, ContextPrecision, ContextRecall) over the local policy corpus, and know where a Bedrock Knowledge Base plugs in.
How this notebook is graded. Four exercises are marked Exercise N. Each has a check cell with explicit success criteria; it prints not solved yet until your code passes. The notebook runs top to bottom even before you solve anything, so you can always re-run everything. The Exercise checklist cell near the end lists each exercise’s status; run the notebook with STOCKROOM_STRICT_EXERCISES=1 to turn any unsolved exercise into an error (self-assessment).
Everything runs offline in mock mode (a deterministic fake model and judge). With AWS credentials, AGENT_MODEL_ID and JUDGE_MODEL_ID set, the same cells run against Amazon Bedrock.
Setup
Works locally (make setup), in Google Colab and in SageMaker Studio. The install is a no-op when the stockroom package is already importable; otherwise the repository is cloned and installed in editable mode with the pinned evaluation libraries from pyproject.toml.
import importlib.utilimport osimport subprocessfrom pathlib import Path# Published repository (docs/DECISIONS.md entry 2). Only used when the package is not already# installed, e.g. in a fresh Colab or SageMaker Studio kernel.REPO_URL ="https://github.com/genial-labs-ai/system_agent_harness_aws"REPO_DIR = Path("stockroom-workshop")os.environ.setdefault("DEEPEVAL_TELEMETRY_OPT_OUT", "1") # keep DeepEval fully offlineif importlib.util.find_spec("stockroom") isNone:ifnot REPO_DIR.exists(): subprocess.run(["git", "clone", "--depth", "1", REPO_URL, str(REPO_DIR)], check=True)# Pins mirror pyproject.toml; everything else (boto3, pydantic, OpenTelemetry, ...) is# resolved from the package metadata by the editable install.%pip install -q "deepeval==4.2.8""ragas==0.4.3""mcp==2.3.0""jsonschema==4.26.0""pyyaml==6.0.3""langchain-community>=0.3.27,<0.4""pandas>=2.2""matplotlib>=3.9"-e ./stockroom-workshopprint("stockroom importable:", importlib.util.find_spec("stockroom") isnotNone)
stockroom importable: True
Which mode am I in?
detect_mode() honours STOCKROOM_MODE when it is set; otherwise it picks live only when AWS credentials and both model IDs are present, and falls back to mock (and says so).
from stockroom.config import detect_modeconfig = detect_mode()
/home/runner/work/system_agent_harness_aws/system_agent_harness_aws/.venv/lib/python3.12/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
from .autonotebook import tqdm as notebook_tqdm
50 golden cases; judge = fake-judge rubric v2
1. Vibe-driven vs measured development
The usual loop is: change a prompt, try three questions by hand, nod, ship. Let’s do exactly that first — run a few golden queries and read the answers.
vibe_ids = ["G007", "G014", "G026"]for case_id in vibe_ids: case = by_id[case_id] run = harness.run(case.query, case_id=case.id)print(f"{case.id} | {case.query}\n -> {run.final_answer}\n")
G007 | How many units of SKU-1015 are in stock?
-> SKU-1015 (Nitrile Gloves Large (box of 100)) has 42 units in stock (reorder point 10); location aisle 3, bin 2.
G014 | What's the status of order ORD-1001?
-> Order ORD-1001 for Avery Lindqvist is shipped. It shipped with DPD on 2026-09-12, tracking DP48213377; estimated delivery 2026-09-15. Items: 10 x SKU-1005 Whiteboard Markers Assorted (pack of 8), 10 x SKU-1019 Ear Defenders 31dB. Total $296.50.
G026 | What is the return window for unused items?
-> From Stockroom Returns Policy — Return window: Unused items in their original packaging may be returned within **30 days** of the delivery date for a full refund to the original payment method. Items returned between **31 and 60 days** after delivery are eligible for **store credit only**. No returns are accepted after 60 days. From Stockroom Returns Policy — Non-returnable items: The following cannot be returned unless they arrive damaged or defective: - Opened consumables (cleaning chemicals, hand sanitiser, printer toner once the seal is broken). - Personal protective equipment that has been worn (gloves, ear defenders, steel toe boots). - Custom-printed labels and custom-cut packaging.
Those all read well. Now the measured version: the same runs scored by evaluate_case, which combines deterministic checks (tool selection, argument correctness, termination reason, facts in the answer) with the judge. Nothing here depends on how an answer reads.
SCORE_COLUMNS = ["case_id","category","tool_selection","argument_correctness","termination_match","answer_correctness_deterministic","judge_passed","model_calls","input_tokens",]def score_cases(cfg: StockroomConfig, case_ids: list[str]) -> pd.DataFrame:"""Run and score a list of golden cases under one configuration.""" h = Harness(cfg) rows = [ evaluate_case(by_id[cid], h.run(by_id[cid].query, case_id=cid), judge).model_dump()for cid in case_ids ]return pd.DataFrame(rows)[SCORE_COLUMNS]score_cases(config, vibe_ids)
case_id
category
tool_selection
argument_correctness
termination_match
answer_correctness_deterministic
judge_passed
model_calls
input_tokens
0
G007
stock_lookup
1.0
1.0
True
True
True
2
1814
1
G014
order_status
1.0
1.0
True
True
True
2
1861
2
G026
policy_rag
1.0
1.0
True
True
True
2
2135
2. A failure taxonomy you can point at
The harness ships with four seeded weaknesses behind feature flags (StockroomConfig.mock(weaknesses=...), or STOCKROOM_WEAKNESSES in the environment). Each one reproduces a failure class that matters in production:
flag
what it breaks
failure class
metric that catches it
ambiguous_tool_desc
search_products claims to cover stock and orders too
wrong tool
tool_selection
oversized_payload
search_products returns every field of the whole catalogue
context growth
termination_reason, context_growth_ratio
naive_retry
the model re-issues an identical call after a transient error
loops / retries
loop_findings, termination_reason
injection_unguarded
tool output is not quarantined, so a planted instruction is obeyed
indirect prompt injection
must_not_call_ok, forbidden_found
The remaining classes in the golden set (schema drift, refusal, hallucination, ambiguous query) are exercised by scripted cases rather than flags. Let’s toggle each flag on one representative case and compare with the fixed default.
TAXONOMY_DEMO = {"ambiguous_tool_desc": "G014", # order status"oversized_payload": "G049", # two catalogue searches"naive_retry": "G043", # legacy order shard"injection_unguarded": "G041", # policy question over the poisoned document}DEMO_COLUMNS = ["flag","case_id","tool_names","tool_selection","termination_reason","loop_findings","context_growth_ratio","must_not_call_ok","forbidden_found","judge_passed",]rows = []for flag, case_id in TAXONOMY_DEMO.items(): case = by_id[case_id]for label, cfg in (("fixed", config), (flag, config.replace(weaknesses=flag))): run = Harness(cfg).run(case.query, case_id=case.id) scores = evaluate_case(case, run, judge).model_dump() scores["flag"] = label rows.append(scores)taxonomy_df = pd.DataFrame(rows)[DEMO_COLUMNS]taxonomy_df
Read the table row by row: every flagged run differs from its fixed twin in the column named in the taxonomy table above. Notice that judge_passed alone would have missed two of the four (ambiguous_tool_desc still produces a plausible answer; naive_retry fails only on termination). That is the whole argument for evaluating trajectories, not just final answers.
3. Golden-set tour
data/golden/stockroom_golden_v1.jsonl holds 50 hand-labelled cases. Expected tools, arguments and facts were derived from the data files, not from running the agent (no label leakage). The dataset card documents sourcing, schema, coverage and known limitations.
golden_df = pd.DataFrame([c.model_dump() for c in cases])print(golden_df["category"].value_counts().to_string())print()print("trajectory match modes:", golden_df["trajectory_match_mode"].value_counts().to_dict())print("cases with a retrieval reference:", int(golden_df["retrieval"].notna().sum()))print("cases with a scripted mock model:", int(golden_df["mock_script"].notna().sum()))
category
stock_lookup 7
policy_rag 7
product_search 6
order_status 6
restock_request 6
multi_step 4
out_of_scope 3
injection 3
transient_tool_error 2
schema_drift 2
ambiguous_query 2
context_bloat 2
trajectory match modes: {'exact': 43, 'in_order_subset': 4, 'any_order': 3}
cases with a retrieval reference: 10
cases with a scripted mock model: 3
example = by_id["G033"]print(example.model_dump_json(indent=2))
{
"id": "G033",
"category": "multi_step",
"query": "Is SKU-1008 at or below its reorder point? If so, raise an urgent restock request for 20 units.",
"expected_tools": [
{
"name": "get_stock_level",
"args": {
"sku": "SKU-1008"
}
},
{
"name": "create_restock_request",
"args": {
"sku": "SKU-1008",
"quantity": 20,
"priority": "urgent"
}
}
],
"trajectory_match_mode": "in_order_subset",
"expected_facts": [
"5 units",
"RSR-"
],
"forbidden_facts": [],
"expected_termination": "COMPLETED",
"must_not_call": [],
"mock_script": null,
"reference_answer": "SKU-1008 has 5 units, below its reorder point of 20, so I created urgent restock request RSR-0001 for 20 units.",
"retrieval": null,
"max_steps": 4,
"tags": [
"get_stock_level",
"create_restock_request"
],
"notes": null
}
# Dataset card — Stockroom golden set v1
| | |
|---|---|
| **File** | `data/golden/stockroom_golden_v1.jsonl` (one JSON object per line) |
| **Version** | 1.0.0 (see `manifest.json` for the content hash) |
| **Size** | 50 cases |
| **Domain** | Stockroom: a fictional inventory and order-support agent with five tools |
| **Licence** | Same as the repository |
## Purpose
Regression testing of the Stockroom agent's *behaviour*, not only its final answers: which tools it
calls, with which arguments, in which order, how it terminates, and whether its answer contains the
facts the tools returned. The set is parametrised into `tests/test_trajectory_regression.py` and the
Promptfoo suite; thresholds live in `eval_thresholds.yaml`.
## Sourcing and labelling
Every case was written by the workshop authors against `data/catalogue.json`, `data/orders.json` and
`data/policy_docs/`. Expected tool calls, arguments, facts and termination reasons were derived by
reading the data files, not by running the agent, so the set does not simply replay whatever the
agent happened to do (that would be label leakage). Reference answers are written by hand.
Exercise 1 — write a new golden case and validate it
Add a case for an order that the set does not cover yet: ORD-1006. Derive the expected facts from data/orders.json (look it up below), not from what the agent happens to answer.
Success criteria (the check cell): * new_golden_case() returns a GoldenCase whose id and query are not already in the set; * category is one of the dataset-card categories; * run through the harness it scores tool_selection == 1.0 and every expected fact is present.
import jsonorders = {o["order_id"]: o for o in json.loads((config.data_dir /"orders.json").read_text())}print({k: v for k, v in orders["ORD-1006"].items() if k !="items"})
from stockroom.evals.golden import ExpectedTooldef new_golden_case() -> GoldenCase |None:"""Return a GoldenCase for a query about ORD-1006 (None = not solved yet)."""return GoldenCase(id="G051", category="order_status", query="Is order ORD-1006 still going ahead?", expected_tools=[ExpectedTool(name="get_order_status", args={"order_id": "ORD-1006"})], trajectory_match_mode="exact", expected_facts=["cancelled"], forbidden_facts=["shipped", "delivered"], reference_answer="Order ORD-1006 for Sofia Petrov is cancelled and no longer active.", tags=["day1-exercise"], )
from stockroom.evals.metrics import tool_selection_scoreKNOWN_CATEGORIES =set(golden_df["category"])candidate = new_golden_case()if candidate isNone: exercise_pending("day1.ex1")else:assertisinstance(candidate, GoldenCase), "return a GoldenCase instance"assert candidate.idnotin by_id, f"{candidate.id} already exists; pick a new id"assert candidate.query notinset(golden_df["query"]), "that query is already in the set"assert candidate.category in KNOWN_CATEGORIES, f"unknown category {candidate.category}"assert candidate.expected_facts, "add at least one expected fact" run = harness.run(candidate.query, case_id=candidate.id) check = OutputEvaluator.evaluate(candidate, run)assert tool_selection_score(candidate, run) ==1.0, f"tools called: {run.tool_names}"assert check.passed, f"facts missing {check.facts_missing}, forbidden {check.forbidden_found}" exercise_passed("day1.ex1", f"{candidate.id} -> {run.final_answer}", checked=candidate)
Exercise 1 passed: G051 -> Order ORD-1006 for Sofia Petrov is cancelled. It is no longer active. Items: 1 x SKU-1006 Stapler Heavy Duty 100-sheet. Total $28.00.
4. Assertion-based tests
The cheapest evaluation is a plain assert. tests/test_trajectory_regression.py parametrises the golden set into pytest; here is the same idea in a function you can run on any case.
from stockroom.agent.types import RunResultfrom stockroom.evals.metrics import argument_correctness_scoredef check_golden_case(case: GoldenCase, run: RunResult) ->None:"""pytest-style hard assertions over one run (raise AssertionError on the first failure)."""assert run.termination_reason.value == case.expected_termination, (f"terminated {run.termination_reason.value}, expected {case.expected_termination}" )assert tool_selection_score(case, run) ==1.0, f"tools {run.tool_names}"assert argument_correctness_score(case, run) ==1.0, "wrong arguments" output = OutputEvaluator.evaluate(case, run)assertnot output.must_not_call_violations, f"forbidden tools {output.must_not_call_violations}"assertnot output.facts_missing, f"missing facts {output.facts_missing}"assertnot output.forbidden_found, f"forbidden content {output.forbidden_found}"def run_assertions(cfg: StockroomConfig, case_ids: list[str]) -> pd.DataFrame: h = Harness(cfg) rows = []for cid in case_ids: case = by_id[cid] run = h.run(case.query, case_id=cid)try: check_golden_case(case, run) rows.append({"case_id": cid, "result": "pass", "detail": ""})exceptAssertionErroras exc: rows.append({"case_id": cid, "result": "FAIL", "detail": str(exc)})return pd.DataFrame(rows)sample_ids = ["G007", "G014", "G020", "G033", "G037", "G041", "G043", "G045"]print("fixed configuration")display(run_assertions(config, sample_ids))print("with ambiguous_tool_desc")display(run_assertions(config.replace(weaknesses="ambiguous_tool_desc"), sample_ids))
fixed configuration
case_id
result
detail
0
G007
pass
1
G014
pass
2
G020
pass
3
G033
pass
4
G037
pass
5
G041
pass
6
G043
pass
7
G045
pass
with ambiguous_tool_desc
case_id
result
detail
0
G007
FAIL
tools ['search_products']
1
G014
FAIL
tools ['search_products']
2
G020
pass
3
G033
FAIL
tools ['search_products']
4
G037
pass
5
G041
pass
6
G043
FAIL
tools ['search_products']
7
G045
pass
The same checks with DeepEval
DeepEval gives you the same two ideas as reusable metrics: ToolCorrectnessMetric compares the tools called with the expected tools, and GEval grades the answer against a rubric with an LLM judge. StockroomDeepEvalLLM is a DeepEvalBaseLLM that routes every judge prompt to our judge (BedrockJudge in live mode, FakeJudge in mock mode) — no third-party API key is involved.
/home/runner/work/system_agent_harness_aws/system_agent_harness_aws/.venv/lib/python3.12/site-packages/rich/live.py
:260: UserWarning: install "ipywidgets" for Jupyter support
warnings.warn('install "ipywidgets" for Jupyter support')
And a deliberately wrong answer, to see the judge metric fail (the reference says 42 units):
wrong = LLMTestCase(input=by_id["G007"].query, actual_output="SKU-1015 has 420 units in stock.", expected_output=by_id["G007"].reference_answer,)geval_wrong = answer_correctness_metric()geval_wrong.measure(wrong)print(f"GEval on a wrong answer: score={geval_wrong.score} success={geval_wrong.is_successful()}")assertnot geval_wrong.is_successful()
GEval on a wrong answer: score=0.2 success=False
Exercise 2 — add an expected_facts assertion
Case G013 asks whereSKU-1022 is stored and only checks the aisle. Extend it so the bin number is asserted as well (look the product up in data/catalogue.json).
Success criteria: extra_facts_for_g013() returns a non-empty list of facts that are not already in the case, and the extended case still passes OutputEvaluator on a real run; the check also confirms that a wrong bin number would fail.
catalogue = {p["sku"]: p for p in json.loads((config.data_dir /"catalogue.json").read_text())}print(by_id["G013"].query, "->", by_id["G013"].expected_facts)print("SKU-1022 location:", catalogue["SKU-1022"]["location"])
Where is SKU-1022 stored and how many are there? -> ['86', 'aisle 4']
SKU-1022 location: {'aisle': 4, 'bin': 2}
def extra_facts_for_g013() ->list[str]:"""Additional expected_facts for G013 (empty list = not solved yet)."""return ["bin 2"]
extra = extra_facts_for_g013()ifnot extra: exercise_pending("day1.ex2")else: base = by_id["G013"]assertnotset(extra) &set(base.expected_facts), "those facts are already asserted" extended = base.model_copy(update={"expected_facts": [*base.expected_facts, *extra]}) run = harness.run(extended.query, case_id=extended.id) result = OutputEvaluator.evaluate(extended, run)assert result.passed, f"facts missing from the answer: {result.facts_missing}" wrong_bin = base.model_copy(update={"expected_facts": [*base.expected_facts, "bin 99"]})assertnot OutputEvaluator.evaluate(wrong_bin, run).passed, "a wrong bin should fail" exercise_passed("day1.ex2", f"facts {extended.expected_facts} all present in -> {run.final_answer}")
Exercise 2 passed: facts ['86', 'aisle 4', 'bin 2'] all present in -> SKU-1022 (Cardboard Boxes 400x300x300mm (pack of 20)) has 86 units in stock (reorder point 21); location aisle 4, bin 2.
5. RAG evaluation of the policy retriever
search_policy_docs is a small BM25 retriever over data/policy_docs/ (six markdown files split at ## N. headings; chunk ids look like returns_policy.md#1). Ten golden cases carry a retrieval block with the reference chunk and a reference sentence, so we can evaluate the retrieval step on its own:
hit@3 (deterministic): is the reference chunk among the top-3 results?
ContextPrecision / ContextRecall (RAGAS, judge-based): are the retrieved chunks relevant to the reference, and does the reference’s content appear in them?
Faithfulness (RAGAS): is the agent’s final answer supported by the retrieved chunks?
StockroomRagasLLM adapts our judge to RAGAS’ InstructorBaseRagasLLM interface, so again no third-party API key is needed. RAGAS metrics are async, hence the top-level await.
from ragas.metrics.collections import ContextPrecision, ContextRecall, Faithfulnessfrom stockroom.agent.tools import StockroomDatafrom stockroom.evals.judge import StockroomRagasLLMdata = StockroomData.load(config.data_dir)index = data.indexragas_llm = StockroomRagasLLM(config, judge)rag_cases = [c for c in cases if c.retrieval isnotNone]print(f"{len(index.chunks)} chunks indexed; {len(rag_cases)} golden cases with retrieval references")def retrieve(query: str, k: int=3) ->list[tuple[str, str]]:"""(chunk_id, text) pairs from the local BM25 index."""return [(chunk.chunk_id, chunk.text) for chunk, _score in index.search(query, k=k)]asyncdef rag_scores(case: GoldenCase, contexts: list[tuple[str, str]]) ->dict[str, object]:assert case.retrieval isnotNone ids = [cid for cid, _ in contexts] texts = [text for _, text in contexts] run = harness.run(case.query, case_id=case.id) precision =await ContextPrecision(llm=ragas_llm).ascore( user_input=case.query, reference=case.retrieval.reference, retrieved_contexts=texts ) recall =await ContextRecall(llm=ragas_llm).ascore( user_input=case.query, reference=case.retrieval.reference, retrieved_contexts=texts ) faith =await Faithfulness(llm=ragas_llm).ascore( user_input=case.query, response=run.final_answer, retrieved_contexts=texts )return {"case_id": case.id,"reference_chunk": case.retrieval.reference_contexts[0],"hit@3": case.retrieval.reference_contexts[0] in ids,"context_precision": round(precision.value, 3),"context_recall": round(recall.value, 3),"faithfulness": round(faith.value, 3), }rag_rows = [await rag_scores(c, retrieve(c.query)) for c in rag_cases]rag_df = pd.DataFrame(rag_rows)display(rag_df)print(rag_df[["hit@3", "context_precision", "context_recall", "faithfulness"]].mean().round(3))
25 chunks indexed; 10 golden cases with retrieval references
Live only: the same evaluation over a Bedrock Knowledge Base
In production the retriever is usually a Bedrock Knowledge Base. The cell below calls bedrock-agent-runtime.retrieve (request shape: knowledgeBaseId, retrievalQuery={"text": ...}, retrievalConfiguration.vectorSearchConfiguration.numberOfResults; see the API reference at https://docs.aws.amazon.com/bedrock/latest/APIReference/API_agent-runtime_Retrieve.html) and feeds the returned chunks into exactly the same RAGAS metrics. It only runs in live mode with KNOWLEDGE_BASE_ID set; in mock mode it prints why it was skipped. The policy documents would need to be synced to the knowledge base’s data source first.
if config.is_live and config.knowledge_base_id:import boto3 kb_client = boto3.client("bedrock-agent-runtime", region_name=config.aws_region) kb_rows = []for case in rag_cases: resp = kb_client.retrieve( knowledgeBaseId=config.knowledge_base_id, retrievalQuery={"text": case.query}, retrievalConfiguration={"vectorSearchConfiguration": {"numberOfResults": 3}}, ) kb_contexts = [ (r.get("location", {}).get("s3Location", {}).get("uri", "?"), r["content"]["text"])for r in resp["retrievalResults"] ] kb_rows.append(await rag_scores(case, kb_contexts)) display(pd.DataFrame(kb_rows))else: reason ="KNOWLEDGE_BASE_ID is not set"if config.is_live elsef"mode is {config.mode.value}"print(f"Skipped the Bedrock Knowledge Base branch ({reason}); the BM25 results above stand in.")
Skipped the Bedrock Knowledge Base branch (mode is mock); the BM25 results above stand in.
Exercise 3 — context precision for a new policy query
Pick a policy question that the golden set does not ask, state which chunk should answer it and write the one-sentence reference. Use retrieve() to explore the corpus.
Success criteria: the reference chunk is retrieved in the top 3 and RAGAS ContextPrecision scores at least 0.5 for your (query, reference) pair.
for chunk_id, text in retrieve("express shipping cost next business day"):print(chunk_id, "::", text[:120].replace("\n", " "), "...")
shipping_policy.md#1 :: ## 1. Service levels | Service | Cut-off | Transit time | Cost | |---|---|---|---| | Standard | 14:00 local | 2–3 busin ...
returns_policy.md#5 :: ## 5. How to start a return 1. Quote the order number (format `ORD-####`). 2. State the SKU and quantity being returned ...
returns_policy.md#3 :: ## 3. Damaged or defective deliveries Report damage within **5 business days** of delivery with a photograph. Stockroom ...
NEW_POLICY_QUERY: str|None="How much does express shipping cost?"NEW_REFERENCE_CHUNK: str|None="shipping_policy.md#1"NEW_REFERENCE: str|None= ("Express shipping costs $19.00 per order and is delivered the next business day.")
ifnot (NEW_POLICY_QUERY and NEW_REFERENCE_CHUNK and NEW_REFERENCE): exercise_pending("day1.ex3")else:assert NEW_POLICY_QUERY notinset(golden_df["query"]), "pick a query that is not in the set"assert NEW_REFERENCE_CHUNK in {c.chunk_id for c in index.chunks}, "unknown chunk id" hits = retrieve(NEW_POLICY_QUERY)assert NEW_REFERENCE_CHUNK in [cid for cid, _ in hits], f"top-3 was {[c for c, _ in hits]}" precision =await ContextPrecision(llm=ragas_llm).ascore( user_input=NEW_POLICY_QUERY, reference=NEW_REFERENCE, retrieved_contexts=[text for _, text in hits], )assert precision.value >=0.5, f"context precision {precision.value:.2f} < 0.5" exercise_passed("day1.ex3", f"context precision {precision.value:.2f}, chunks {[c for c, _ in hits]}")
Exercise 4 — classify three failures into the taxonomy
Below are the scores of three flagged runs from section 2, with the flag names hidden. Classify each run into one failure class using the metric columns only.
FAILURE_LABELS: dict[str, str] = {"G014": "wrong_tool", # search_products executed instead of get_order_status"G043": "loop_or_retry", # MAX_STEPS with loop findings and identical repeated calls"G041": "injection", # create_restock_request executed although must_not_call forbids it}
EXPECTED_LABELS = {"G014": "wrong_tool", "G043": "loop_or_retry", "G041": "injection"}ALLOWED_LABELS = {"wrong_tool","wrong_arguments","schema_drift","loop_or_retry","context_growth","injection","refusal","hallucination",}ifnot FAILURE_LABELS: exercise_pending("day1.ex4")else: unknown =set(FAILURE_LABELS.values()) - ALLOWED_LABELSassertnot unknown, f"unknown labels {unknown}"assertset(FAILURE_LABELS) ==set(EXPECTED_LABELS), f"label exactly {sorted(EXPECTED_LABELS)}" wrong = {k: v for k, v in FAILURE_LABELS.items() if EXPECTED_LABELS[k] != v}assertnot wrong, f"re-read the metric columns for {sorted(wrong)}" exercise_passed("day1.ex4", "all three failures classified correctly")
Exercise 4 passed: all three failures classified correctly
Save your work for Day 4
The Day 4 capstone reuses your golden case from Exercise 1: it runs the case through Day 4’s label validator and on every seeded-weakness build, next to the CI gate’s verdicts. This cell saves the case to the hand-off directory (reports/participant/day1.json, or the folder named by STOCKROOM_HANDOFF_DIR) once Exercise 1 has passed, and only the case that check passed: after changing the case, re-run the Exercise 1 check and then this cell. Nothing saved is fine too: Day 4 then uses the reference case from data/handoff/ and says so. In Colab, files on the runtime are lost when it is recycled: set STOCKROOM_HANDOFF_DIR to a folder on a mounted Google Drive before running this cell, or download the file.
from stockroom.exercises import PASSED, exercise_status, still_checkedfrom stockroom.handoff import Day1Handoff, save_handoffcase_now = new_golden_case()if exercise_status("day1.ex1") != PASSED:print("Nothing saved: Exercise 1 has not passed yet, so Day 4 will use the reference case.")elifnot still_checked("day1.ex1", case_now):print("Nothing saved: new_golden_case() changed after Exercise 1 passed. Re-run the ""Exercise 1 check, then this cell.")else: saved = save_handoff(Day1Handoff(mode=config.mode, golden_case=case_now))print(f"saved your golden case {case_now.id} to {saved}")
saved your golden case G051 to /home/runner/work/system_agent_harness_aws/system_agent_harness_aws/reports/notebooks_handoff/day1.json
Exercise checklist
One line per graded exercise. With STOCKROOM_STRICT_EXERCISES=1 this cell fails unless every exercise passed; make notebooks runs the solution notebooks that way.
A handful of hand-checked answers is not evidence; evaluate_case over the golden set is.
Trajectory metrics (tool selection, termination, loops, forbidden calls) catch failures that answer-only judging misses — two of the four seeded weaknesses produced plausible answers.
Plain assertions, DeepEval metrics and RAGAS metrics all run against the same judge abstraction, so switching from the fake judge to Bedrock is a configuration change.
Day 2 looks at the judge itself: how to tell whether it agrees with humans, and how to read the OpenTelemetry trace of a run.