agentproof-sim 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. agentproof/__init__.py +41 -0
  2. agentproof/adapters/__init__.py +14 -0
  3. agentproof/adapters/base.py +19 -0
  4. agentproof/adapters/langchain.py +95 -0
  5. agentproof/adapters/native.py +23 -0
  6. agentproof/adapters/openai_agents.py +62 -0
  7. agentproof/api.py +99 -0
  8. agentproof/cli.py +172 -0
  9. agentproof/core/clock.py +18 -0
  10. agentproof/core/effects.py +109 -0
  11. agentproof/core/events.py +63 -0
  12. agentproof/core/faults.py +294 -0
  13. agentproof/core/invariant.py +109 -0
  14. agentproof/core/redaction.py +32 -0
  15. agentproof/core/result.py +109 -0
  16. agentproof/core/runner.py +216 -0
  17. agentproof/core/scenario.py +13 -0
  18. agentproof/core/trace.py +58 -0
  19. agentproof/core/world.py +126 -0
  20. agentproof/invariants/__init__.py +21 -0
  21. agentproof/invariants/builtin.py +96 -0
  22. agentproof/invariants/temporal.py +5 -0
  23. agentproof/mutations/__init__.py +61 -0
  24. agentproof/mutations/base.py +57 -0
  25. agentproof/mutations/duplication.py +15 -0
  26. agentproof/mutations/ordering.py +10 -0
  27. agentproof/mutations/state.py +15 -0
  28. agentproof/mutations/timing.py +9 -0
  29. agentproof/mutations/tool_faults.py +58 -0
  30. agentproof/py.typed +1 -0
  31. agentproof/pytest_plugin.py +40 -0
  32. agentproof/replay/player.py +74 -0
  33. agentproof/replay/recorder.py +29 -0
  34. agentproof/replay/schema.py +24 -0
  35. agentproof/reporting/__init__.py +7 -0
  36. agentproof/reporting/console.py +34 -0
  37. agentproof/reporting/json_report.py +47 -0
  38. agentproof/reporting/junit.py +66 -0
  39. agentproof/tools/definition.py +26 -0
  40. agentproof/tools/invocation.py +11 -0
  41. agentproof/tools/registry.py +154 -0
  42. agentproof_sim-0.1.1.dist-info/METADATA +407 -0
  43. agentproof_sim-0.1.1.dist-info/RECORD +46 -0
  44. agentproof_sim-0.1.1.dist-info/WHEEL +4 -0
  45. agentproof_sim-0.1.1.dist-info/entry_points.txt +5 -0
  46. agentproof_sim-0.1.1.dist-info/licenses/LICENSE +18 -0
@@ -0,0 +1,216 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ import uuid
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from agentproof.adapters.base import AgentRunResult
9
+ from agentproof.core.faults import AgentProofError, MutationSpec, UnsupportedMutationError
10
+ from agentproof.core.invariant import Invariant, evaluate_invariants
11
+ from agentproof.core.result import InvariantFailure, RunResult, SuiteResult
12
+ from agentproof.core.scenario import Scenario
13
+ from agentproof.core.world import World
14
+ from agentproof.mutations.base import Mutation
15
+ from agentproof.replay.recorder import write_repro_artifact
16
+
17
+
18
+ class ScenarioRunner:
19
+ def __init__(
20
+ self,
21
+ *,
22
+ adapter: Any,
23
+ scenarios: list[Scenario],
24
+ invariants: list[Invariant],
25
+ mutations: list[Mutation],
26
+ ) -> None:
27
+ self.adapter = adapter
28
+ self.scenarios = scenarios
29
+ self.invariants = invariants
30
+ self.mutations = mutations
31
+
32
+ async def run(
33
+ self,
34
+ *,
35
+ scenario: str | None = None,
36
+ mutations: list[Mutation] | None = None,
37
+ mutation_name: str | None = None,
38
+ seed: int = 42,
39
+ artifacts_dir: Path = Path(".agentproof/runs"),
40
+ store_artifacts: bool = True,
41
+ source_path: str | None = None,
42
+ suite_name: str | None = None,
43
+ ) -> SuiteResult:
44
+ selected_scenarios = [item for item in self.scenarios if scenario in (None, item.name)]
45
+ if not selected_scenarios:
46
+ raise ValueError(f"unknown scenario: {scenario}")
47
+
48
+ suite_run_id = f"ap_{uuid.uuid4().hex[:12]}"
49
+ results: list[RunResult] = []
50
+ for scenario_item in selected_scenarios:
51
+ baseline = await self._run_one(
52
+ scenario=scenario_item,
53
+ mutation=None,
54
+ seed=seed,
55
+ suite_run_id=suite_run_id,
56
+ artifacts_dir=artifacts_dir,
57
+ store_artifacts=store_artifacts,
58
+ source_path=source_path,
59
+ suite_name=suite_name,
60
+ )
61
+ results.append(baseline)
62
+ if baseline.status != "PASS":
63
+ continue
64
+
65
+ candidate_mutations = mutations if mutations is not None else self.mutations
66
+ for mutation in candidate_mutations:
67
+ if mutation_name is not None and mutation.spec.type != mutation_name:
68
+ continue
69
+ expanded = await self._expand_mutation(scenario_item, mutation, seed)
70
+ for item in expanded:
71
+ results.append(
72
+ await self._run_one(
73
+ scenario=scenario_item,
74
+ mutation=item,
75
+ seed=seed,
76
+ suite_run_id=suite_run_id,
77
+ artifacts_dir=artifacts_dir,
78
+ store_artifacts=store_artifacts,
79
+ source_path=source_path,
80
+ suite_name=suite_name,
81
+ )
82
+ )
83
+ return SuiteResult(
84
+ run_id=suite_run_id,
85
+ results=results,
86
+ metadata={"source_path": source_path, "suite_name": suite_name},
87
+ )
88
+
89
+ async def _expand_mutation(
90
+ self,
91
+ scenario: Scenario,
92
+ mutation: Mutation,
93
+ seed: int,
94
+ ) -> list[Mutation]:
95
+ if mutation.target is not None or mutation.spec.type in {
96
+ "duplicate_user_request",
97
+ "delayed_event",
98
+ "duplicate_event",
99
+ "reorder_tool_results",
100
+ }:
101
+ return [mutation]
102
+ world = World(seed=seed)
103
+ await _maybe_await(scenario.func(world))
104
+ tools = world.tools.all()
105
+ if not tools:
106
+ return [mutation]
107
+ result: list[Mutation] = []
108
+ for tool in tools:
109
+ if mutation.spec.type == "timeout_after_commit" and tool.effect not in {
110
+ "write",
111
+ "delete",
112
+ "external",
113
+ "financial",
114
+ "privileged",
115
+ }:
116
+ continue
117
+ result.append(
118
+ type(mutation)(
119
+ target=tool.name,
120
+ occurrence=mutation.occurrence,
121
+ params=mutation.params,
122
+ severity=mutation.severity,
123
+ )
124
+ )
125
+ return result or [mutation]
126
+
127
+ async def _run_one(
128
+ self,
129
+ *,
130
+ scenario: Scenario,
131
+ mutation: Mutation | None,
132
+ seed: int,
133
+ suite_run_id: str,
134
+ artifacts_dir: Path,
135
+ store_artifacts: bool,
136
+ source_path: str | None,
137
+ suite_name: str | None,
138
+ ) -> RunResult:
139
+ world = World(seed=seed, metadata={"source_path": source_path, "suite_name": suite_name})
140
+ run_id = world.next_id("run")
141
+ run_id = f"{suite_run_id}_{run_id}"
142
+ status = "PASS"
143
+ output: str | None = None
144
+ error_message: str | None = None
145
+ invariant_failures: list[InvariantFailure] = []
146
+ invariant_errors: list[InvariantFailure] = []
147
+ initial_snapshot: dict[str, Any] = {}
148
+
149
+ try:
150
+ await _maybe_await(scenario.func(world))
151
+ initial_snapshot = world.snapshot()
152
+ if mutation is not None:
153
+ mutation.install(world)
154
+ inputs = world.faults.prepare_user_inputs(world.user_inputs)
155
+ for user_input in inputs:
156
+ adapter_result = await self.adapter.run(world=world, user_input=user_input)
157
+ output = _stringify_output(adapter_result)
158
+ world.trace.record(
159
+ "agent_output",
160
+ getattr(self.adapter, "name", type(self.adapter).__name__),
161
+ {"output": output, "metadata": getattr(adapter_result, "metadata", {})},
162
+ )
163
+ except UnsupportedMutationError as exc:
164
+ status = "UNSUPPORTED"
165
+ error_message = str(exc)
166
+ except AgentProofError as exc:
167
+ status = "AGENT_ERROR"
168
+ error_message = str(exc)
169
+ except Exception as exc:
170
+ status = "AGENT_ERROR"
171
+ error_message = f"{type(exc).__name__}: {exc}"
172
+
173
+ if status == "PASS":
174
+ invariant_failures, invariant_errors = await evaluate_invariants(self.invariants, world)
175
+ if invariant_errors:
176
+ status = "TEST_ERROR"
177
+ error_message = invariant_errors[0].message
178
+ elif invariant_failures:
179
+ status = "BASELINE_FAILURE" if mutation is None else "INVARIANT_FAILURE"
180
+ error_message = invariant_failures[0].message
181
+
182
+ mutation_spec: MutationSpec | None = mutation.spec if mutation is not None else None
183
+ result = RunResult(
184
+ run_id=run_id,
185
+ scenario=scenario.name,
186
+ adapter=getattr(self.adapter, "name", type(self.adapter).__name__),
187
+ seed=seed,
188
+ mutation=mutation_spec,
189
+ severity=mutation_spec.severity if mutation_spec else "high",
190
+ status=status, # type: ignore[arg-type]
191
+ final_output=output,
192
+ trace=world.trace.all(),
193
+ effects=world.effects.all(),
194
+ violated_invariants=[failure.name for failure in invariant_failures],
195
+ invariant_failures=invariant_failures + invariant_errors,
196
+ error_message=error_message,
197
+ initial_world_snapshot=initial_snapshot,
198
+ metadata={"source_path": source_path, "suite_name": suite_name},
199
+ )
200
+ if store_artifacts and result.failed:
201
+ result.artifact_path = write_repro_artifact(result, artifacts_dir)
202
+ return result
203
+
204
+
205
+ async def _maybe_await(value: Any) -> Any:
206
+ if inspect.isawaitable(value):
207
+ return await value
208
+ return value
209
+
210
+
211
+ def _stringify_output(adapter_result: AgentRunResult | Any) -> str | None:
212
+ if isinstance(adapter_result, AgentRunResult):
213
+ return adapter_result.final_output
214
+ if adapter_result is None:
215
+ return None
216
+ return str(adapter_result)
@@ -0,0 +1,13 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Awaitable, Callable
4
+ from dataclasses import dataclass
5
+ from typing import Any
6
+
7
+ ScenarioFunc = Callable[[Any], Awaitable[None] | None]
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class Scenario:
12
+ name: str
13
+ func: ScenarioFunc
@@ -0,0 +1,58 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Literal
4
+
5
+ from pydantic import BaseModel, ConfigDict, Field
6
+
7
+ from agentproof.core.redaction import redact
8
+
9
+ TraceKind = Literal[
10
+ "user_input",
11
+ "tool_call",
12
+ "tool_result",
13
+ "tool_error",
14
+ "effect",
15
+ "fault",
16
+ "state_change",
17
+ "event_scheduled",
18
+ "event_delivered",
19
+ "agent_output",
20
+ "invariant_pass",
21
+ "invariant_fail",
22
+ ]
23
+
24
+
25
+ class TraceEvent(BaseModel):
26
+ model_config = ConfigDict(extra="forbid")
27
+
28
+ seq: int
29
+ timestamp: float
30
+ kind: TraceKind
31
+ name: str
32
+ data: dict[str, Any] = Field(default_factory=dict)
33
+
34
+
35
+ class TraceRecorder:
36
+ def __init__(self, *, clock: Any) -> None:
37
+ self._clock = clock
38
+ self._events: list[TraceEvent] = []
39
+
40
+ def record(self, kind: TraceKind, name: str, data: dict[str, Any] | None = None) -> TraceEvent:
41
+ event = TraceEvent(
42
+ seq=len(self._events) + 1,
43
+ timestamp=float(self._clock.now()),
44
+ kind=kind,
45
+ name=name,
46
+ data=redact(data or {}),
47
+ )
48
+ self._events.append(event)
49
+ return event
50
+
51
+ def all(self) -> list[TraceEvent]:
52
+ return list(self._events)
53
+
54
+ def clear(self) -> None:
55
+ self._events.clear()
56
+
57
+ def snapshot(self) -> list[dict[str, Any]]:
58
+ return [event.model_dump(mode="json") for event in self._events]
@@ -0,0 +1,126 @@
1
+ from __future__ import annotations
2
+
3
+ import copy
4
+ import random
5
+ from collections.abc import Iterator, MutableMapping
6
+ from typing import Any
7
+
8
+ from agentproof.core.clock import VirtualClock
9
+ from agentproof.core.effects import EffectLedger
10
+ from agentproof.core.events import EventQueue
11
+ from agentproof.core.faults import FaultController
12
+ from agentproof.core.redaction import redact
13
+ from agentproof.core.trace import TraceRecorder
14
+
15
+
16
+ class StateStore(MutableMapping[str, Any]):
17
+ def __init__(self, world: World, data: dict[str, Any] | None = None) -> None:
18
+ object.__setattr__(self, "_world", world)
19
+ object.__setattr__(self, "_data", copy.deepcopy(data or {}))
20
+ object.__setattr__(self, "_trace_enabled", True)
21
+
22
+ def __getitem__(self, key: str) -> Any:
23
+ return self._data[key]
24
+
25
+ def __setitem__(self, key: str, value: Any) -> None:
26
+ self._data[key] = value
27
+ if self._trace_enabled:
28
+ self._world.trace.record(
29
+ "state_change", key, {"path": [key], "value": copy.deepcopy(value)}
30
+ )
31
+
32
+ def __delitem__(self, key: str) -> None:
33
+ del self._data[key]
34
+ if self._trace_enabled:
35
+ self._world.trace.record("state_change", key, {"path": [key], "deleted": True})
36
+
37
+ def __iter__(self) -> Iterator[str]:
38
+ return iter(self._data)
39
+
40
+ def __len__(self) -> int:
41
+ return len(self._data)
42
+
43
+ def __getattr__(self, key: str) -> Any:
44
+ try:
45
+ return self._data[key]
46
+ except KeyError as exc:
47
+ raise AttributeError(key) from exc
48
+
49
+ def __setattr__(self, key: str, value: Any) -> None:
50
+ if key.startswith("_"):
51
+ object.__setattr__(self, key, value)
52
+ else:
53
+ self[key] = value
54
+
55
+ def snapshot(self) -> dict[str, Any]:
56
+ return copy.deepcopy(self._data)
57
+
58
+ def replace(self, data: dict[str, Any]) -> None:
59
+ self._trace_enabled = False
60
+ try:
61
+ self._data.clear()
62
+ self._data.update(copy.deepcopy(data))
63
+ finally:
64
+ self._trace_enabled = True
65
+
66
+
67
+ class World:
68
+ """Stateful simulated environment exposed to an agent through virtual tools."""
69
+
70
+ def __init__(self, *, seed: int = 0, metadata: dict[str, Any] | None = None) -> None:
71
+ self.seed = seed
72
+ self.random = random.Random(seed)
73
+ self.clock = VirtualClock()
74
+ self.trace = TraceRecorder(clock=self.clock)
75
+ self.state = StateStore(self)
76
+ self.effects = EffectLedger()
77
+ self.faults = FaultController(self)
78
+ self.events = EventQueue(self)
79
+ self.metadata: dict[str, Any] = dict(metadata or {})
80
+ self.user_inputs: list[str] = []
81
+ self._id_counters: dict[str, int] = {}
82
+
83
+ from agentproof.tools.registry import ToolRegistry
84
+
85
+ self.tools = ToolRegistry(self)
86
+
87
+ def input(self, text: str) -> None:
88
+ self.user_inputs.append(text)
89
+ self.trace.record("user_input", "user", {"text": text})
90
+
91
+ def next_id(self, prefix: str) -> str:
92
+ self._id_counters[prefix] = self._id_counters.get(prefix, 0) + 1
93
+ return f"{prefix}_{self._id_counters[prefix]:03d}"
94
+
95
+ def snapshot(self) -> dict[str, Any]:
96
+ return {
97
+ "seed": self.seed,
98
+ "clock": self.clock.now(),
99
+ "state": self.state.snapshot(),
100
+ "inputs": copy.deepcopy(self.user_inputs),
101
+ "effects": self.effects.snapshot(),
102
+ "metadata": redact(copy.deepcopy(self.metadata)),
103
+ }
104
+
105
+ def clone_from_initial(self) -> World:
106
+ cloned = World(seed=self.seed, metadata=copy.deepcopy(self.metadata))
107
+ cloned.state.replace(self.state.snapshot())
108
+ cloned.user_inputs = copy.deepcopy(self.user_inputs)
109
+ return cloned
110
+
111
+ def set_path(self, path: list[Any], value: Any) -> None:
112
+ if not path:
113
+ raise ValueError("path must not be empty")
114
+ cursor: Any = self.state._data
115
+ for key in path[:-1]:
116
+ cursor = cursor.setdefault(key, {}) if isinstance(cursor, dict) else cursor[key]
117
+ final_key = path[-1]
118
+ if isinstance(cursor, dict):
119
+ cursor[final_key] = value
120
+ else:
121
+ cursor[final_key] = value
122
+ self.trace.record(
123
+ "state_change",
124
+ ".".join(str(item) for item in path),
125
+ {"path": path, "value": copy.deepcopy(value)},
126
+ )
@@ -0,0 +1,21 @@
1
+ from __future__ import annotations
2
+
3
+ from agentproof.invariants.builtin import (
4
+ at_most_once,
5
+ forbid_resource,
6
+ max_effect_sum,
7
+ max_tool_calls,
8
+ never_call,
9
+ no_duplicate_effects,
10
+ requires_before,
11
+ )
12
+
13
+ __all__ = [
14
+ "at_most_once",
15
+ "forbid_resource",
16
+ "max_effect_sum",
17
+ "max_tool_calls",
18
+ "never_call",
19
+ "no_duplicate_effects",
20
+ "requires_before",
21
+ ]
@@ -0,0 +1,96 @@
1
+ from __future__ import annotations
2
+
3
+ import fnmatch
4
+ from typing import Any
5
+
6
+ from agentproof.core.invariant import Invariant, invariant
7
+
8
+
9
+ def at_most_once(tool_name: str, *, severity: str = "medium") -> Invariant:
10
+ return max_tool_calls(tool_name, 1, severity=severity)
11
+
12
+
13
+ def max_tool_calls(tool_name: str, count: int, *, severity: str = "medium") -> Invariant:
14
+ @invariant(name=f"max_tool_calls_{tool_name}_{count}", severity=severity)
15
+ def check(world: Any) -> None:
16
+ observed = len(
17
+ [
18
+ event
19
+ for event in world.trace.all()
20
+ if event.kind == "tool_call" and event.name == tool_name
21
+ ]
22
+ )
23
+ assert observed <= count, f"{tool_name} called {observed} times, expected <= {count}"
24
+
25
+ return check
26
+
27
+
28
+ def never_call(tool_name: str, *, severity: str = "high") -> Invariant:
29
+ @invariant(name=f"never_call_{tool_name}", severity=severity)
30
+ def check(world: Any) -> None:
31
+ observed = len(
32
+ [
33
+ event
34
+ for event in world.trace.all()
35
+ if event.kind == "tool_call" and event.name == tool_name
36
+ ]
37
+ )
38
+ assert observed == 0, f"{tool_name} was called {observed} times"
39
+
40
+ return check
41
+
42
+
43
+ def requires_before(required_tool: str, action_tool: str, *, severity: str = "high") -> Invariant:
44
+ @invariant(name=f"requires_{required_tool}_before_{action_tool}", severity=severity)
45
+ def check(world: Any) -> None:
46
+ required_seen = False
47
+ for event in world.trace.all():
48
+ if event.kind == "tool_call" and event.name == required_tool:
49
+ required_seen = True
50
+ if event.kind == "tool_call" and event.name == action_tool:
51
+ assert required_seen, f"{action_tool} called before {required_tool}"
52
+
53
+ return check
54
+
55
+
56
+ def max_effect_sum(
57
+ effect_type: str,
58
+ field: str,
59
+ maximum: float,
60
+ *,
61
+ where: dict[str, Any] | None = None,
62
+ severity: str = "high",
63
+ ) -> Invariant:
64
+ @invariant(name=f"max_effect_sum_{effect_type}_{field}", severity=severity)
65
+ def check(world: Any) -> None:
66
+ observed = world.effects.sum(type=effect_type, field=field, where=where)
67
+ assert observed <= maximum, f"{field} sum {observed:.2f}, expected <= {maximum:.2f}"
68
+
69
+ return check
70
+
71
+
72
+ def no_duplicate_effects(
73
+ effect_type: str,
74
+ *,
75
+ key_fields: list[str],
76
+ severity: str = "high",
77
+ ) -> Invariant:
78
+ @invariant(name=f"no_duplicate_effects_{effect_type}", severity=severity)
79
+ def check(world: Any) -> None:
80
+ duplicates = world.effects.duplicate_keys(type=effect_type, key_fields=key_fields)
81
+ assert not duplicates, f"duplicate {effect_type} effects for keys {duplicates}"
82
+
83
+ return check
84
+
85
+
86
+ def forbid_resource(resource_pattern: str, *, severity: str = "critical") -> Invariant:
87
+ @invariant(name=f"forbid_resource_{resource_pattern}", severity=severity)
88
+ def check(world: Any) -> None:
89
+ forbidden = [
90
+ effect.resource
91
+ for effect in world.effects.all()
92
+ if effect.resource and fnmatch.fnmatch(effect.resource, resource_pattern)
93
+ ]
94
+ assert not forbidden, f"forbidden resources affected: {forbidden}"
95
+
96
+ return check
@@ -0,0 +1,5 @@
1
+ from __future__ import annotations
2
+
3
+ from agentproof.invariants.builtin import requires_before
4
+
5
+ __all__ = ["requires_before"]
@@ -0,0 +1,61 @@
1
+ from __future__ import annotations
2
+
3
+ from agentproof.mutations.base import MUTATION_TYPES, Mutation, mutation_from_spec
4
+ from agentproof.mutations.duplication import DuplicateEvent, DuplicateUserRequest
5
+ from agentproof.mutations.ordering import ReorderToolResults
6
+ from agentproof.mutations.state import StaleState, StateChangedAfterRead
7
+ from agentproof.mutations.timing import DelayedEvent
8
+ from agentproof.mutations.tool_faults import (
9
+ DuplicateToolResult,
10
+ MalformedResponse,
11
+ MissingField,
12
+ PermissionDenied,
13
+ RateLimited,
14
+ TimeoutAfterCommit,
15
+ ToolError,
16
+ ToolLatency,
17
+ ToolTimeout,
18
+ )
19
+
20
+
21
+ def standard_reliability_pack() -> list[Mutation]:
22
+ """Return stable single-fault mutations used by the default deterministic pack."""
23
+
24
+ return [
25
+ ToolTimeout(),
26
+ TimeoutAfterCommit(severity="high"),
27
+ ToolError(),
28
+ ToolLatency(params={"seconds": 1.0}),
29
+ RateLimited(),
30
+ MalformedResponse(),
31
+ MissingField(params={"field": "status"}),
32
+ DuplicateUserRequest(severity="high"),
33
+ StaleState(params={"value": {}}),
34
+ StateChangedAfterRead(params={"path": ["agentproof", "state_changed"], "value": True}),
35
+ PermissionDenied(),
36
+ DelayedEvent(),
37
+ DuplicateEvent(),
38
+ ]
39
+
40
+
41
+ __all__ = [
42
+ "MUTATION_TYPES",
43
+ "DelayedEvent",
44
+ "DuplicateEvent",
45
+ "DuplicateToolResult",
46
+ "DuplicateUserRequest",
47
+ "MalformedResponse",
48
+ "MissingField",
49
+ "Mutation",
50
+ "PermissionDenied",
51
+ "RateLimited",
52
+ "ReorderToolResults",
53
+ "StaleState",
54
+ "StateChangedAfterRead",
55
+ "TimeoutAfterCommit",
56
+ "ToolError",
57
+ "ToolLatency",
58
+ "ToolTimeout",
59
+ "mutation_from_spec",
60
+ "standard_reliability_pack",
61
+ ]
@@ -0,0 +1,57 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+ from dataclasses import dataclass, field
5
+ from typing import Any, ClassVar
6
+
7
+ from agentproof.core.faults import MutationSpec, Severity
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class Mutation:
12
+ target: str | None = None
13
+ occurrence: int | None = 1
14
+ params: Mapping[str, Any] = field(default_factory=dict)
15
+ severity: Severity = "medium"
16
+
17
+ mutation_type: ClassVar[str] = "mutation"
18
+ description: ClassVar[str] = "Generic mutation."
19
+ stable: ClassVar[bool] = True
20
+
21
+ @property
22
+ def spec(self) -> MutationSpec:
23
+ return MutationSpec(
24
+ type=self.mutation_type,
25
+ target=self.target,
26
+ occurrence=self.occurrence,
27
+ params=dict(self.params),
28
+ severity=self.severity,
29
+ )
30
+
31
+ def install(self, world: Any) -> None:
32
+ world.faults.install(self.spec)
33
+
34
+
35
+ def mutation_from_spec(spec: MutationSpec) -> Mutation:
36
+ cls = MUTATION_TYPES.get(spec.type)
37
+ if cls is None:
38
+ return Mutation(
39
+ target=spec.target,
40
+ occurrence=spec.occurrence,
41
+ params=spec.params,
42
+ severity=spec.severity,
43
+ )
44
+ return cls(
45
+ target=spec.target,
46
+ occurrence=spec.occurrence,
47
+ params=spec.params,
48
+ severity=spec.severity,
49
+ )
50
+
51
+
52
+ MUTATION_TYPES: dict[str, type[Mutation]] = {}
53
+
54
+
55
+ def register_mutation(cls: type[Mutation]) -> type[Mutation]:
56
+ MUTATION_TYPES[cls.mutation_type] = cls
57
+ return cls
@@ -0,0 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ from agentproof.mutations.base import Mutation, register_mutation
4
+
5
+
6
+ @register_mutation
7
+ class DuplicateUserRequest(Mutation):
8
+ mutation_type = "duplicate_user_request"
9
+ description = "Deliver the same user input to the adapter twice in one run."
10
+
11
+
12
+ @register_mutation
13
+ class DuplicateEvent(Mutation):
14
+ mutation_type = "duplicate_event"
15
+ description = "Deliver a scheduled event more than once."
@@ -0,0 +1,10 @@
1
+ from __future__ import annotations
2
+
3
+ from agentproof.mutations.base import Mutation, register_mutation
4
+
5
+
6
+ @register_mutation
7
+ class ReorderToolResults(Mutation):
8
+ mutation_type = "reorder_tool_results"
9
+ description = "Experimental placeholder for controlled parallel result reordering."
10
+ stable = False