Story-1011: MatterChatAgent v1 guardrails + golden evals (756-C)

Story 1011Epic 749๐Ÿ”— Chain of CustodyAgent Platform17 files ยท +828/โˆ’12

PR #25 ยท pramaan-io/pramaan-agents ยท merged 2026-06-03

Why this matters

An agent answering in court can't afford a hallucinated citation, a leaked matter, or a runaway bill.

Admissibility needs 4 hard checks, not a polite filter.

End-of-pipeline check is too late โ€” the breach already happened.

The fix โ€” Check, cap, lock, verify โ€” every run, every answer.

Roadmap ยท Agent Platform

How it flows

sequenceDiagram
  autonumber
  participant request as Agent run
  participant controller as Guardrail controller
  participant model as Model + tools
  participant recorder as MatterChat recorder
  request->>controller: AgentRunRequest
  controller->>controller: circuit_breaker.check_before_run
  controller->>model: cost_cap.check_before_model_call
  model->>model: tool call โ†’ tenant_boundary
  model->>controller: validate_answer (citations)
  controller-->>recorder: record_run_outcome

circuit_breaker.py ยท +100 โ€” new methods

Methodin โ†’ outwhat it does
CircuitStateStore(class)
load(firm_id) โ†’ list[dict[str, Any]]
save(firm_id, samples) โ†’ None

What changed, file by file

Implements the CircuitBreakerGuardrail that tracks per-firm error rate in a rolling window and emits the circuit_open status, blocking cascading retries on the MatterChat recorder path.

new classsignature
CircuitStateStore(class)
load(firm_id) โ†’ list[dict[str, Any]]
save(firm_id, samples) โ†’ None
DynamoDBCircuitStateStore(class)
CircuitBreakerGuardrail(store, threshold, window_seconds, min_samples)
check_before_run(firm_id) โ†’ None
record_outcome(firm_id, error) โ†’ None
_fresh_samples(firm_id) โ†’ list[dict[str, Any]]

Hosts the GuardrailController, GuardrailRunContext and GuardrailToolCallRecorder that orchestrate the four guardrails across run, model, tool and answer stages of every AgentRunRequest.

new classsignature
GuardrailRunContext(request, run_id, source_verification, cost_cap)
GuardrailController(circuit_breaker)
context_for(request, run_id) โ†’ GuardrailRunContext
record_run_outcome(firm_id, error) โ†’ None
wrap_recorder(recorder, context) โ†’ ToolCallRecorder
before_model_call(context, prompt_text, user_message, history) โ†’ None
after_model_call(context, input_tokens, output_tokens) โ†’ None
validate_answer(context, content) โ†’ None
GuardrailToolCallRecorder(inner, context, _tool_names)
after_tool_call(event) โ†’ None

Implements the CostCapGuardrail that estimates USD cost from token counts and aborts the model call before spend exceeds the configured cap, emitting the aborted_cost status.

new classsignature
CostCapGuardrail(cap_usd, input_price_per_million, output_price_per_million, spent_usd)
check_before_model_call(prompt_text, user_message, history_text) โ†’ None
record_model_usage(input_tokens, output_tokens) โ†’ None
_cost(input_tokens, output_tokens) โ†’ Decimal
_ensure_under_cap(additional_usd) โ†’ None
estimate_tokens(text) โ†’ int
_decimal_env(name, default) โ†’ Decimal

Defines the SourceVerificationGuardrail that re-checks every citation in the final answer against the fetched quotes before the answer leaves the agent.

new classsignature
SourceVerificationGuardrail(fetched_quotes)
validate_answer(content) โ†’ None
citations_in(content) โ†’ list[tuple[str, str]]

Defines the TenantBoundaryGuardrail that validates firm_id and matter_id on every tool call so no cross-tenant data access can slip through the MatterChat recorder.

new classsignature
TenantBoundaryGuardrail(firm_id, matter_id)
_uuid_from_input(value) โ†’ UUID | None

StrandsHarness now holds guardrail_controller and guardrail_context fields so the four guardrails are invoked inside the agent loop, not bolted on after.

New fields: guardrail_controller : GuardrailController | Noneguardrail_context : GuardrailRunContext | None

Agent runtime app now owns a GuardrailController instance, wiring the four guardrails into every AgentRunRequest path that flows through the runtime.

New fields: guardrails : GuardrailController

Added the _failure_status helper that maps runtime errors to the canonical block statuses (circuit_open, aborted_cost, guardrail_blocked) before they are persisted to the run record.

new methodsignature
_failure_status(error) โ†’ str

Introduces GuardrailBlockedError and GuardrailTerminalError so guardrail failures are distinguishable from regular runtime errors throughout the recorder path.

new classsignature
GuardrailBlockedError(class)
GuardrailTerminalError(class)
__init__(status, message) โ†’ None

MatterChat tool schema gained an optional matter_id field so the TenantBoundaryGuardrail can verify which matter a tool call is operating on before it executes.

New fields: matter_id : UUID | None

Files changed (17)

Fileยฑ
docs/walkthroughs/story-1011-walkthrough.html+78
scripts/check_story_walkthrough.py+6 โˆ’1
src/pramaan_agents/db/store.py+22 โˆ’4
src/pramaan_agents/runtime/app.py+23 โˆ’2
src/pramaan_agents/runtime/guardrails/__init__.py+11
src/pramaan_agents/runtime/guardrails/circuit_breaker.py โ˜…+100
src/pramaan_agents/runtime/guardrails/controller.py โ˜…+84
src/pramaan_agents/runtime/guardrails/cost_cap_check.py โ˜…+67
src/pramaan_agents/runtime/guardrails/errors.py+13
src/pramaan_agents/runtime/guardrails/source_verification.py+41
src/pramaan_agents/runtime/guardrails/tenant_boundary_check.py+35
src/pramaan_agents/runtime/strands_harness.py+26 โˆ’3
src/pramaan_agents/tools/matter_chat_tools.py+2 โˆ’1
tests/golden/matter_chat_agent/test_golden_eval.py+122
tests/runtime/test_guardrails.py+175
tests/test_run_truth.py+2 โˆ’1
tests/test_story_walkthrough_check.py+21