Skip to content

Commit fb90c72

Browse files
committed
[FEAT]: Add final evaluation result contract
1 parent 14089aa commit fb90c72

15 files changed

Lines changed: 237 additions & 15 deletions

File tree

‎docs/api/core-types.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ Data types shared across the entire framework. All importable from `rampart` dir
1414
- ToolCall
1515
- SideEffect
1616
- Turn
17+
- EvaluationRole
18+
- TerminationReason
1719
- EvalOutcome
1820
- EvalResult
1921
- EvalContext
@@ -28,6 +30,8 @@ Data types shared across the entire framework. All importable from `rampart` dir
2830
- SafetyStatus
2931
- HarmCategory
3032
- InjectionRecord
33+
- resolve_attack_verdict
34+
- resolve_probe_verdict
3135
- resolve_as_attack
3236
- resolve_as_probe
3337

‎docs/attacks/xpia.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ See [`Attacks.xpia()`][rampart.attacks.Attacks.xpia] for the full API reference.
226226
| `inject` | `InjectionHandle \| list[InjectionHandle] \| None` | `None` | Prepared injections from `surface.inject()`. `None` for inline XPIA. |
227227
| `trigger` | `str \| list[str] \| Request \| list[Request] \| PromptDriver` | required | Benign prompt(s) that cause retrieval of injected content. |
228228
| `evaluator` | [`Evaluator`][rampart.core.evaluator.Evaluator] | required | What attack condition to detect. |
229-
| `max_turns` | `int` | `5` | Maximum prompt-response exchanges before `ERROR`. |
229+
| `max_turns` | `int` | `5` | Maximum prompt-response exchanges; reaching the limit resolves the trace normally. |
230230
| `event_handlers` | `list[ExecutionEventHandler] \| None` | `None` | Additional lifecycle event handlers. |
231231

232232
---

‎docs/probes/behavioral.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ See [`Probes.behavior()`][rampart.probes.Probes.behavior] for the full API refer
107107
| `prompts` | `list[str] \| None` | `None` | A list of prompt strings. |
108108
| `driver` | [`PromptDriver`][rampart.core.prompt_driver.PromptDriver] `\| None` | `None` | A pre-built prompt driver. |
109109
| `evaluator` | [`Evaluator`][rampart.core.evaluator.Evaluator] | required | What behavior to detect. |
110-
| `max_turns` | `int` | `25` | Maximum exchanges before `ERROR`. |
110+
| `max_turns` | `int` | `25` | Maximum exchanges; reaching the limit resolves the trace normally. |
111111

112112
!!! warning
113113
Provide exactly one of `prompt`, `prompts`, or `driver`. Providing more than one or none raises `ValueError`.

‎rampart/__init__.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,21 @@
2727
SafetyStatus,
2828
resolve_as_attack,
2929
resolve_as_probe,
30+
resolve_attack_verdict,
31+
resolve_probe_verdict,
3032
)
3133
from rampart.core.types import (
3234
EvalContext,
3335
EvalOutcome,
3436
EvalResult,
37+
EvaluationRole,
3538
ObservabilityLevel,
3639
Payload,
3740
PayloadFormat,
3841
Request,
3942
Response,
4043
SideEffect,
44+
TerminationReason,
4145
ToolCall,
4246
Turn,
4347
)
@@ -57,6 +61,7 @@
5761
"EvalContext",
5862
"EvalOutcome",
5963
"EvalResult",
64+
"EvaluationRole",
6065
"Evaluator",
6166
"EvaluatorError",
6267
"ExecutionEvent",
@@ -82,11 +87,14 @@
8287
"Session",
8388
"SideEffect",
8489
"Surface",
90+
"TerminationReason",
8591
"ToolCall",
8692
"ToolDeclaration",
8793
"TranscriptScope",
8894
"Turn",
8995
"record_result",
9096
"resolve_as_attack",
9197
"resolve_as_probe",
98+
"resolve_attack_verdict",
99+
"resolve_probe_verdict",
92100
]

‎rampart/attacks/_factory.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,8 @@ def xpia(
7373
Benign user request(s) that cause the agent to process
7474
poisoned content.
7575
evaluator (Evaluator): What condition to check for.
76-
max_turns (int): Maximum prompt-response exchanges before
77-
ERROR. Defaults to 5.
76+
max_turns (int): Maximum prompt-response exchanges. Reaching the
77+
limit resolves the trace normally. Defaults to 5.
7878
event_handlers (list[ExecutionEventHandler] | None): Optional
7979
additional handlers for custom observability.
8080

‎rampart/attacks/_xpia.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@ class XPIAExecution(BaseExecution):
6666
attachments.
6767
driver (PromptDriver): How to drive the trigger conversation.
6868
evaluator (Evaluator): What condition to check for.
69-
max_turns (int): Maximum prompt-response exchanges before the
70-
execution stops with ERROR. Prevents unbounded loops.
69+
max_turns (int): Maximum prompt-response exchanges. Reaching the
70+
limit resolves the trace normally and prevents unbounded loops.
7171
event_handlers (list[ExecutionEventHandler] | None): Additional
7272
handlers beyond the framework defaults.
7373
"""

‎rampart/core/__init__.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,21 @@
3030
SafetyStatus,
3131
resolve_as_attack,
3232
resolve_as_probe,
33+
resolve_attack_verdict,
34+
resolve_probe_verdict,
3335
)
3436
from rampart.core.types import (
3537
EvalContext,
3638
EvalOutcome,
3739
EvalResult,
40+
EvaluationRole,
3841
ObservabilityLevel,
3942
Payload,
4043
PayloadFormat,
4144
Request,
4245
Response,
4346
SideEffect,
47+
TerminationReason,
4448
ToolCall,
4549
Turn,
4650
)
@@ -55,6 +59,7 @@
5559
"EvalContext",
5660
"EvalOutcome",
5761
"EvalResult",
62+
"EvaluationRole",
5863
"Evaluator",
5964
"ExecutionEvent",
6065
"ExecutionEventData",
@@ -79,10 +84,13 @@
7984
"Session",
8085
"SideEffect",
8186
"Surface",
87+
"TerminationReason",
8288
"ToolCall",
8389
"ToolDeclaration",
8490
"Turn",
8591
"evaluate_turn_async",
8692
"resolve_as_attack",
8793
"resolve_as_probe",
94+
"resolve_attack_verdict",
95+
"resolve_probe_verdict",
8896
]

‎rampart/core/result.py‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
EvalOutcome,
1919
EvalResult,
2020
ObservabilityLevel,
21+
TerminationReason,
2122
Turn,
2223
)
2324

@@ -107,7 +108,11 @@ class Result:
107108
Args:
108109
status: Categorical status for structured reporting.
109110
summary: Human-readable one-line summary.
111+
evaluation: The single final-trace evaluation that produced the
112+
verdict. None for error results and legacy/manual results.
110113
turns: The full conversation for evidence and debugging.
114+
termination_reason: Why the trace stopped producing turns. None when
115+
execution failed before a normal trace termination was recorded.
111116
duration_seconds: How long the test execution took.
112117
harm_category: Which harm category this test covers.
113118
Accepts HarmCategory enum values for built-in categories or plain strings
@@ -122,7 +127,9 @@ class Result:
122127

123128
status: SafetyStatus
124129
summary: str
130+
evaluation: EvalResult | None = None
125131
turns: list[Turn] = field(default_factory=list[Turn])
132+
termination_reason: TerminationReason | None = None
126133
duration_seconds: float = 0.0
127134
harm_category: HarmCategory | str | None = None
128135
strategy: str = ""
@@ -168,6 +175,46 @@ def __repr__(self) -> str:
168175
)
169176

170177

178+
def resolve_attack_verdict(*, evaluation: EvalResult | None) -> SafetyStatus:
179+
"""Map one final evaluation using attack polarity.
180+
181+
Args:
182+
evaluation: The final-trace evaluator result, or None when no trace
183+
was available to evaluate.
184+
185+
Returns:
186+
SafetyStatus: DETECTED maps to UNSAFE, NOT_DETECTED maps to SAFE,
187+
UNDETERMINED is preserved, and None maps to ERROR.
188+
"""
189+
if evaluation is None:
190+
return SafetyStatus.ERROR
191+
if evaluation.outcome is EvalOutcome.DETECTED:
192+
return SafetyStatus.UNSAFE
193+
if evaluation.outcome is EvalOutcome.UNDETERMINED:
194+
return SafetyStatus.UNDETERMINED
195+
return SafetyStatus.SAFE
196+
197+
198+
def resolve_probe_verdict(*, evaluation: EvalResult | None) -> SafetyStatus:
199+
"""Map one final evaluation using probe polarity.
200+
201+
Args:
202+
evaluation: The final-trace evaluator result, or None when no trace
203+
was available to evaluate.
204+
205+
Returns:
206+
SafetyStatus: DETECTED maps to SAFE, NOT_DETECTED maps to UNSAFE,
207+
UNDETERMINED is preserved, and None maps to ERROR.
208+
"""
209+
if evaluation is None:
210+
return SafetyStatus.ERROR
211+
if evaluation.outcome is EvalOutcome.DETECTED:
212+
return SafetyStatus.SAFE
213+
if evaluation.outcome is EvalOutcome.UNDETERMINED:
214+
return SafetyStatus.UNDETERMINED
215+
return SafetyStatus.UNSAFE
216+
217+
171218
def resolve_as_attack(*, eval_results: list[EvalResult]) -> SafetyStatus:
172219
"""Attack semantics: detected -> UNSAFE, not detected -> SAFE.
173220

‎rampart/core/types.py‎

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
import uuid
1313
from dataclasses import dataclass, field
14-
from enum import Enum
14+
from enum import Enum, StrEnum
1515
from typing import TYPE_CHECKING, Any
1616

1717
if TYPE_CHECKING:
@@ -240,6 +240,31 @@ def __post_init__(self) -> None:
240240
raise ValueError(msg)
241241

242242

243+
class EvaluationRole(StrEnum):
244+
"""Why an evaluation was attached to a turn.
245+
246+
Attributes:
247+
STOP_CONDITION: The evaluation was produced by an online stop
248+
condition. It is execution evidence, not the final verdict input.
249+
"""
250+
251+
STOP_CONDITION = "stop_condition"
252+
253+
254+
class TerminationReason(StrEnum):
255+
"""Why a trace stopped producing turns.
256+
257+
Attributes:
258+
DRIVER_EXHAUSTED: The prompt driver returned no next request.
259+
MAX_TURNS: The configured turn budget was exhausted.
260+
STOP_CONDITION: An online stop condition fired.
261+
"""
262+
263+
DRIVER_EXHAUSTED = "driver_exhausted"
264+
MAX_TURNS = "max_turns"
265+
STOP_CONDITION = "stop_condition"
266+
267+
243268
@dataclass(frozen=True, kw_only=True)
244269
class Turn:
245270
"""One prompt-response exchange.
@@ -252,6 +277,8 @@ class Turn:
252277
request: What was sent to the agent.
253278
response: What the agent returned.
254279
eval_result: Evaluator outcome for this turn.
280+
eval_role: Why ``eval_result`` was produced. None when the role was
281+
not recorded, including executions that predate the trace runner.
255282
turn_number: Position in the conversation (0-indexed).
256283
timestamp: When this exchange occurred.
257284
driver_reasoning: Why the driver chose this request.
@@ -260,6 +287,7 @@ class Turn:
260287
request: Request
261288
response: Response
262289
eval_result: EvalResult | None = None
290+
eval_role: EvaluationRole | None = None
263291
turn_number: int = 0
264292
timestamp: datetime | None = None
265293
driver_reasoning: str = ""

‎rampart/probes/_factory.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,8 @@ def behavior(
6969
prompts (list[str] | None): A list of prompt strings.
7070
driver (PromptDriver | None): A pre-built prompt driver.
7171
evaluator (Evaluator): What behavior to check for.
72-
max_turns (int): Maximum prompt-response exchanges before
73-
returning ERROR. Defaults to 25.
72+
max_turns (int): Maximum prompt-response exchanges. Reaching the
73+
limit resolves the trace normally. Defaults to 25.
7474
event_handlers (list[ExecutionEventHandler] | None): Optional
7575
additional handlers.
7676

0 commit comments

Comments
 (0)