agentprobe-testing 1.2.0__tar.gz → 1.4.0__tar.gz

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 (110) hide show
  1. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/PKG-INFO +1 -1
  2. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/__init__.py +1 -1
  3. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/priority.py +14 -0
  4. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/quickstart.py +26 -1
  5. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/registry.py +1 -0
  6. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/report.py +14 -2
  7. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe_testing.egg-info/PKG-INFO +1 -1
  8. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe_testing.egg-info/SOURCES.txt +1 -0
  9. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/pyproject.toml +1 -1
  10. agentprobe_testing-1.4.0/tests/test_injection_mode.py +80 -0
  11. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_priority.py +49 -0
  12. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/LICENSE +0 -0
  13. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/README.md +0 -0
  14. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/agents/__init__.py +0 -0
  15. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/agents/base.py +0 -0
  16. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/agents/browser_agent.py +0 -0
  17. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/agents/rule_based.py +0 -0
  18. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/agents/scripted.py +0 -0
  19. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/agents/target_agent.py +0 -0
  20. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/agreement.py +0 -0
  21. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/classifier.py +0 -0
  22. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/cli.py +0 -0
  23. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/diff.py +0 -0
  24. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domain.py +0 -0
  25. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/__init__.py +0 -0
  26. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/access_control/__init__.py +0 -0
  27. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/access_control/agent.py +0 -0
  28. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/access_control/clean.py +0 -0
  29. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/access_control/complex_agent.py +0 -0
  30. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/access_control/decoy.py +0 -0
  31. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/access_control/domain.py +0 -0
  32. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/access_control/entities.py +0 -0
  33. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/access_control/injector_prompt.py +0 -0
  34. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/access_control/rule_based_agent.py +0 -0
  35. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/access_control/scenarios.py +0 -0
  36. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/access_control/split.py +0 -0
  37. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/access_control/tools.py +0 -0
  38. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/access_control/trap.py +0 -0
  39. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/web_browsing/__init__.py +0 -0
  40. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/web_browsing/agent.py +0 -0
  41. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/web_browsing/clean.py +0 -0
  42. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/web_browsing/domain.py +0 -0
  43. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/web_browsing/entities.py +0 -0
  44. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/web_browsing/injector_prompt.py +0 -0
  45. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/web_browsing/rule_based_agent.py +0 -0
  46. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/web_browsing/scenarios.py +0 -0
  47. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/web_browsing/tools.py +0 -0
  48. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/web_browsing/trap.py +0 -0
  49. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/feedback.py +0 -0
  50. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/generic_world.py +0 -0
  51. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/injection.py +0 -0
  52. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/injector.py +0 -0
  53. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/llm.py +0 -0
  54. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/playbook.py +0 -0
  55. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/reachability.py +0 -0
  56. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/runner.py +0 -0
  57. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/scenario.py +0 -0
  58. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/scenarios/__init__.py +0 -0
  59. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/scenarios/clean.py +0 -0
  60. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/scenarios/decoy.py +0 -0
  61. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/scenarios/registry.py +0 -0
  62. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/scenarios/split.py +0 -0
  63. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/scenarios/trap.py +0 -0
  64. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/summarize.py +0 -0
  65. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/termui.py +0 -0
  66. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/tools.py +0 -0
  67. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/trajectory.py +0 -0
  68. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/triage.py +0 -0
  69. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/validate_scenarios.py +0 -0
  70. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/world.py +0 -0
  71. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe_testing.egg-info/dependency_links.txt +0 -0
  72. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe_testing.egg-info/entry_points.txt +0 -0
  73. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe_testing.egg-info/requires.txt +0 -0
  74. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe_testing.egg-info/top_level.txt +0 -0
  75. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/setup.cfg +0 -0
  76. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_access_control_agent.py +0 -0
  77. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_access_control_domain.py +0 -0
  78. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_access_control_rule_based_agent.py +0 -0
  79. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_access_control_scenarios.py +0 -0
  80. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_access_control_tools.py +0 -0
  81. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_agreement.py +0 -0
  82. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_browser_agent.py +0 -0
  83. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_classifier.py +0 -0
  84. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_cli.py +0 -0
  85. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_complex_access_control_agent.py +0 -0
  86. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_diff.py +0 -0
  87. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_domain.py +0 -0
  88. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_failures.py +0 -0
  89. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_feedback.py +0 -0
  90. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_generic_world.py +0 -0
  91. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_injection.py +0 -0
  92. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_injector.py +0 -0
  93. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_llm.py +0 -0
  94. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_package_api.py +0 -0
  95. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_playbook.py +0 -0
  96. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_quickstart.py +0 -0
  97. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_reachability.py +0 -0
  98. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_registry.py +0 -0
  99. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_report.py +0 -0
  100. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_rule_based_agent.py +0 -0
  101. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_runner.py +0 -0
  102. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_scenarios.py +0 -0
  103. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_target_agent.py +0 -0
  104. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_termui.py +0 -0
  105. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_tools.py +0 -0
  106. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_trajectory.py +0 -0
  107. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_triage.py +0 -0
  108. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_validate_scenarios.py +0 -0
  109. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_web_browsing_domain.py +0 -0
  110. {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_world.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentprobe-testing
3
- Version: 1.2.0
3
+ Version: 1.4.0
4
4
  Summary: Adaptive chaos-testing for LLM agents: a live model-driven Injector that reads an agent's real trajectory and decides where to break something, instead of scripting perturbations in advance.
5
5
  License: Business Source License 1.1
6
6
 
@@ -60,7 +60,7 @@ from agentprobe.scenario import CommitPattern, FactPattern, GoalSpec, Scenario
60
60
  from agentprobe.scenarios.registry import ALL_SCENARIOS as TICKET_SCENARIOS
61
61
  from agentprobe.scenarios.registry import BY_ID as TICKET_SCENARIOS_BY_ID
62
62
 
63
- __version__ = "1.2.0"
63
+ __version__ = "1.4.0"
64
64
 
65
65
  __all__ = [
66
66
  "Agent",
@@ -118,6 +118,20 @@ def rank(issues: list[dict], overrides: Optional[dict[str, str]] = None) -> list
118
118
  return ranked
119
119
 
120
120
 
121
+ def clean_overrides(raw: object) -> dict[str, str]:
122
+ """Keep only well-formed {FAILURE_KIND: "High"|"Medium"|"Low"} entries from
123
+ whatever a caller hands in (the hosted runner gets this from the website's
124
+ saved edits, over the wire), so a malformed or hostile value can never
125
+ reach a report."""
126
+ if not isinstance(raw, dict):
127
+ return {}
128
+ return {
129
+ k: v
130
+ for k, v in raw.items()
131
+ if isinstance(k, str) and isinstance(v, str) and v in TIERS and 1 <= len(k) <= 40 and k.replace("_", "").isalnum() and k.upper() == k
132
+ }
133
+
134
+
121
135
  def encode(issues: list[dict]) -> str:
122
136
  """ASCII-only JSON, base64'd, so it can sit inside an HTML comment with no
123
137
  escaping concerns. The website reads it back to re-rank a stored report
@@ -17,7 +17,8 @@ from typing import Any, Callable, Optional
17
17
  from agentprobe.agents.base import Agent, AgentAction
18
18
  from agentprobe.domain import Domain, TICKET_DOMAIN
19
19
  from agentprobe.injection import injection_was_triggered
20
- from agentprobe.injector import Injector, NullInjector, RecordingInjector, serialize_armed_injection
20
+ from agentprobe.injector import Injector, ModelInjector, NullInjector, RecordingInjector, ReplayInjector, serialize_armed_injection
21
+ from agentprobe.priority import clean_overrides
21
22
  from agentprobe.report import InjectionRecord, Report
22
23
  from agentprobe.runner import run_recovery, run_robustness_pair
23
24
  from agentprobe.scenario import Scenario
@@ -162,6 +163,7 @@ def quick_test(
162
163
  domain_key: Optional[str] = None,
163
164
  record_injections: bool = False,
164
165
  summarize_failures: bool = False,
166
+ priority_overrides: Optional[dict] = None,
165
167
  ) -> Report:
166
168
  """Run one scenario and get back a ready Report -- the one-call version
167
169
  of run_robustness_pair()/run_recovery() + Report(). `agent` can be a
@@ -184,6 +186,7 @@ def quick_test(
184
186
  injector = injector if injector is not None else NullInjector()
185
187
  target_factory = agent if _looks_like_factory(agent) else wrap_agent(agent)
186
188
  resolved_injector_model = injector_model if injector_model is not None else type(injector).__name__
189
+ injection_mode = _injection_mode(injector)
187
190
 
188
191
  recording: Optional[RecordingInjector] = None
189
192
  if record_injections:
@@ -214,6 +217,8 @@ def quick_test(
214
217
  chaos_trajectories=[chaos],
215
218
  injection_records=injection_records,
216
219
  injector_cost_usd=_injector_cost_usd(injector),
220
+ priority_overrides=clean_overrides(priority_overrides),
221
+ injection_mode=injection_mode,
217
222
  )
218
223
  if recording is not None:
219
224
  # Stashed here (not a Report field -- Report is a general-purpose
@@ -249,6 +254,7 @@ def quick_test_all(
249
254
  record_injections: bool = False,
250
255
  max_duration_seconds: Optional[float] = None,
251
256
  summarize_failures: bool = False,
257
+ priority_overrides: Optional[dict] = None,
252
258
  ) -> Report:
253
259
  """Run every scenario in `scenarios` (a dict[str, Scenario] -- e.g.
254
260
  TICKET_SCENARIOS or a FetchedDomain's .scenarios -- or a plain list of
@@ -296,6 +302,7 @@ def quick_test_all(
296
302
  all_records: list[InjectionRecord] = []
297
303
  total_injector_cost = 0.0
298
304
  combined_injector_model: Optional[str] = None
305
+ combined_injection_mode: Optional[str] = None
299
306
  start = time.monotonic()
300
307
  scenarios_run = 0
301
308
 
@@ -307,6 +314,7 @@ def quick_test_all(
307
314
  resolved_injector_model = injector_model if injector_model is not None else type(injector).__name__
308
315
  if combined_injector_model is None:
309
316
  combined_injector_model = resolved_injector_model
317
+ combined_injection_mode = _injection_mode(injector)
310
318
 
311
319
  recording: Optional[RecordingInjector] = None
312
320
  if record_injections:
@@ -352,6 +360,8 @@ def quick_test_all(
352
360
  chaos_trajectories=all_chaos,
353
361
  injection_records=all_records,
354
362
  injector_cost_usd=total_injector_cost,
363
+ priority_overrides=clean_overrides(priority_overrides),
364
+ injection_mode=combined_injection_mode,
355
365
  )
356
366
  report._scenarios_run = len(all_chaos)
357
367
  if summarize_failures:
@@ -391,6 +401,21 @@ def assert_passes(report: Report, save_html_on_failure: Optional[str] = None) ->
391
401
  raise AssertionError("agentprobe: " + "; ".join(failures))
392
402
 
393
403
 
404
+ def _injection_mode(injector: Injector) -> Optional[str]:
405
+ """How this run's failures were chosen, for labeling its report: an
406
+ adaptive ModelInjector decides afresh each time ("exploration" -- finds new
407
+ weaknesses, but two runs aren't directly comparable), a ReplayInjector
408
+ re-fires a recorded sequence ("pinned" -- the same failures every time, so
409
+ scores compare). Anything else (none, hardcoded) isn't labeled. Looks
410
+ through a RecordingInjector's wrapping."""
411
+ inner = getattr(injector, "_inner", injector)
412
+ if isinstance(inner, ReplayInjector):
413
+ return "pinned"
414
+ if isinstance(inner, ModelInjector):
415
+ return "exploration"
416
+ return None
417
+
418
+
394
419
  def _injector_cost_usd(injector: Injector) -> float:
395
420
  """ModelInjector is the only injector type with real API cost (none/
396
421
  hardcoded/replay never call a model) -- same logic as cli.py's version,
@@ -175,6 +175,7 @@ def upload_run(
175
175
  # caller's explicit consent to pool them, anonymously, into
176
176
  # aggregate benchmarks; False (the default) keeps them private.
177
177
  "findings": report.findings(),
178
+ "injection_mode": report.injection_mode,
178
179
  "share_anonymized": bool(share_anonymized),
179
180
  }
180
181
  if recorded_injections is None:
@@ -306,6 +306,11 @@ class Report:
306
306
  holds Trajectories/InjectionRecords, not the Injector instances
307
307
  themselves. 0.0 (and omitted from output) for none/hardcoded/replay
308
308
  injectors, which never call a model at all."""
309
+ injection_mode: Optional[str] = None
310
+ """How this run's failures were chosen: "exploration" (an adaptive injector
311
+ decided them -- finds new weaknesses, but two runs aren't directly
312
+ comparable) or "pinned" (a recorded sequence was replayed -- the same
313
+ failures every time, so scores compare). None when unknown/unlabeled."""
309
314
  priority_overrides: dict[str, str] = field(default_factory=dict)
310
315
  """Tiers (High/Medium/Low) the reader has set for particular failure kinds,
311
316
  e.g. {"FALSE_SUCCESS": "High"}. They replace the automatic estimate in
@@ -412,6 +417,13 @@ class Report:
412
417
  )
413
418
  return out
414
419
 
420
+ def injection_mode_label(self) -> Optional[str]:
421
+ """Plain-language form of injection_mode for report headers."""
422
+ return {
423
+ "exploration": "adaptive failures (exploration: not directly comparable with other runs)",
424
+ "pinned": "replayed failures (pinned: comparable with other replays of the same recording)",
425
+ }.get(self.injection_mode or "")
426
+
415
427
  def priorities(self) -> list[dict]:
416
428
  """The run's issues ranked highest to lowest priority, with any
417
429
  `priority_overrides` applied. See priority.py for how it's scored."""
@@ -475,7 +487,7 @@ class Report:
475
487
  lines = [
476
488
  blue(
477
489
  f"scenarios: {c['n']} mode: {self.mode} injector: {self.injector_model} "
478
- f"target: {self.target_model}"
490
+ f"target: {self.target_model}" + (f" failures: {self.injection_mode_label()}" if self.injection_mode_label() else "")
479
491
  ),
480
492
  ]
481
493
  if c["clean_passed"] is not None:
@@ -811,7 +823,7 @@ class Report:
811
823
  <div class="page">
812
824
  <header>
813
825
  <div class="title-row"><span class="health-dot health-dot--{health}"></span><h1>AgentProbe report</h1></div>
814
- <div class="subtitle">mode: {esc(self.mode)} &middot; injector: {esc(self.injector_model)} &middot; target: {esc(self.target_model)} &middot; {esc(c['n'])} scenarios</div>
826
+ <div class="subtitle">mode: {esc(self.mode)} &middot; injector: {esc(self.injector_model)} &middot; target: {esc(self.target_model)} &middot; {esc(c['n'])} scenarios{(' &middot; ' + esc(self.injection_mode_label())) if self.injection_mode_label() else ''}</div>
815
827
  </header>
816
828
  {injector_error_banner}
817
829
  <div class="stats">
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentprobe-testing
3
- Version: 1.2.0
3
+ Version: 1.4.0
4
4
  Summary: Adaptive chaos-testing for LLM agents: a live model-driven Injector that reads an agent's real trajectory and decides where to break something, instead of scripting perturbations in advance.
5
5
  License: Business Source License 1.1
6
6
 
@@ -85,6 +85,7 @@ tests/test_failures.py
85
85
  tests/test_feedback.py
86
86
  tests/test_generic_world.py
87
87
  tests/test_injection.py
88
+ tests/test_injection_mode.py
88
89
  tests/test_injector.py
89
90
  tests/test_llm.py
90
91
  tests/test_package_api.py
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agentprobe-testing"
3
- version = "1.2.0"
3
+ version = "1.4.0"
4
4
  description = "Adaptive chaos-testing for LLM agents: a live model-driven Injector that reads an agent's real trajectory and decides where to break something, instead of scripting perturbations in advance."
5
5
  requires-python = ">=3.11"
6
6
  dependencies = ["anthropic>=1.0.0", "python-dotenv>=1.0.0"]
@@ -0,0 +1,80 @@
1
+ """Labeling a run exploration (adaptive injector) or pinned (replayed recording)."""
2
+
3
+ from agentprobe.agents.scripted import ScriptedAgent
4
+ from agentprobe.injection import ArmedInjection, Injection, InjectionKind, ResponseSignature, Trigger
5
+ from agentprobe.injector import HardcodedToolErrorInjector, ModelInjector, NullInjector, RecordingInjector, ReplayInjector, serialize_armed_injection, deserialize_armed_injection
6
+ from agentprobe.quickstart import _injection_mode, quick_test, quick_test_all
7
+ from agentprobe.registry import upload_run
8
+ from agentprobe.scenarios.registry import BY_ID
9
+
10
+ AGENT = lambda: ScriptedAgent(script=[("get_ticket", {"id": "T-1001"})])
11
+
12
+
13
+ def _armed(step=1):
14
+ return ArmedInjection(
15
+ injection=Injection(kind=InjectionKind.TOOL_ERROR, payload={"tool_name": "close_ticket", "count": 1}, intent="i",
16
+ expected_signature=ResponseSignature("s"), rationale="r"),
17
+ trigger=Trigger.on_next_action(),
18
+ armed_at_step=step,
19
+ )
20
+
21
+
22
+ def test_injection_mode_reads_the_injector_type_through_a_recording_wrapper():
23
+ adaptive = ModelInjector.__new__(ModelInjector) # only its type matters here; no client needed
24
+ replay = ReplayInjector([_armed()])
25
+ assert _injection_mode(adaptive) == "exploration"
26
+ assert _injection_mode(replay) == "pinned"
27
+ assert _injection_mode(RecordingInjector(adaptive)) == "exploration"
28
+ assert _injection_mode(RecordingInjector(replay)) == "pinned"
29
+ assert _injection_mode(NullInjector()) is None
30
+ assert _injection_mode(HardcodedToolErrorInjector()) is None
31
+
32
+
33
+ def test_a_replayed_run_is_labeled_pinned_in_the_report():
34
+ report = quick_test(BY_ID["clean-1"], AGENT, injector=ReplayInjector([]), mode="recovery")
35
+ assert report.injection_mode == "pinned"
36
+ assert "replayed failures (pinned: comparable with other replays of the same recording)" in report.render_html()
37
+ assert "failures: replayed failures (pinned" in report.render()
38
+
39
+
40
+ def test_recording_a_replay_still_labels_it_pinned():
41
+ report = quick_test(BY_ID["clean-1"], AGENT, injector=ReplayInjector([]), mode="recovery", record_injections=True)
42
+ assert report.injection_mode == "pinned"
43
+
44
+
45
+ def test_an_unlabeled_injector_adds_no_mode_text():
46
+ report = quick_test(BY_ID["clean-1"], AGENT, injector=NullInjector(), mode="recovery")
47
+ assert report.injection_mode is None and report.injection_mode_label() is None
48
+ assert "pinned" not in report.render_html() and "exploration" not in report.render_html()
49
+
50
+
51
+ def test_the_exploration_label_says_runs_are_not_directly_comparable():
52
+ from agentprobe.report import Report
53
+
54
+ r = Report("robustness", "m", "t", [], [], injection_mode="exploration")
55
+ assert "adaptive failures (exploration: not directly comparable with other runs)" in r.render_html()
56
+
57
+
58
+ def test_quick_test_all_labels_the_combined_report_from_its_injectors():
59
+ scenarios = {k: BY_ID[k] for k in ("clean-1", "clean-2")}
60
+ report = quick_test_all(scenarios, AGENT, injector_factory=lambda: ReplayInjector([]), mode="recovery")
61
+ assert report.injection_mode == "pinned"
62
+
63
+
64
+ def test_upload_run_sends_the_mode_so_the_website_can_label_it():
65
+ sent = {}
66
+ report = quick_test(BY_ID["clean-1"], AGENT, injector=ReplayInjector([]), mode="recovery")
67
+ upload_run(report, api_key="k", poster=lambda url, body, key: sent.update(body))
68
+ assert sent["injection_mode"] == "pinned"
69
+ report2 = quick_test(BY_ID["clean-1"], AGENT, injector=NullInjector(), mode="recovery")
70
+ upload_run(report2, api_key="k", poster=lambda url, body, key: sent.update(body))
71
+ assert sent["injection_mode"] is None
72
+
73
+
74
+ def test_a_serialized_recording_survives_the_round_trip_the_website_relies_on():
75
+ # The website stores serialize_armed_injection() dicts and the runner rebuilds them with
76
+ # deserialize_armed_injection() before replaying -- this is the contract between the two.
77
+ original = [_armed(2), _armed(5)]
78
+ rebuilt = ReplayInjector([deserialize_armed_injection(serialize_armed_injection(a)) for a in original])
79
+ assert sorted(rebuilt._by_step) == [2, 5]
80
+ assert rebuilt._by_step[2].injection.kind == InjectionKind.TOOL_ERROR
@@ -139,3 +139,52 @@ def test_html_stays_script_free_and_the_text_report_lists_priorities():
139
139
  out = r.render()
140
140
  assert "priorities (automated estimate -- review before acting):" in out
141
141
  assert "1. [High] Trusts a tool that did nothing -- 1 scenario(s)" in out
142
+
143
+
144
+ # --- overrides flowing in from outside (the hosted runner / Temporal worker) ---
145
+
146
+ def test_clean_overrides_keeps_only_well_formed_entries():
147
+ from agentprobe.priority import clean_overrides
148
+
149
+ assert clean_overrides({"FALSE_SUCCESS": "High", "STALE_READ": "Low"}) == {"FALSE_SUCCESS": "High", "STALE_READ": "Low"}
150
+ dirty = {"false_success": "High", "STALE READ": "Low", "TOOL_ERROR": "Urgent", "DISTRACTOR": 3, 5: "High", "X" * 41: "High", "": "High", "OK_KIND": "Medium"}
151
+ assert clean_overrides(dirty) == {"OK_KIND": "Medium"}
152
+ for junk in (None, "High", ["High"], 7):
153
+ assert clean_overrides(junk) == {}
154
+
155
+
156
+ def test_quick_test_ranks_with_the_overrides_it_is_given():
157
+ from agentprobe.agents.scripted import ScriptedAgent
158
+ from agentprobe.injector import NullInjector
159
+ from agentprobe.quickstart import quick_test
160
+ from agentprobe.scenarios.registry import BY_ID
161
+
162
+ agent = lambda: ScriptedAgent(script=[("get_ticket", {"id": "T-1001"})]) # never finishes -> the chaos run fails
163
+ report = quick_test(BY_ID["clean-1"], agent, injector=NullInjector(), mode="recovery", priority_overrides={"UNATTRIBUTED": "High", "BAD": "nope", "x": "Low"})
164
+ assert report.priority_overrides == {"UNATTRIBUTED": "High"} # junk dropped
165
+ ranked = report.priorities()
166
+ assert [(r["kind"], r["tier"], r["edited"]) for r in ranked] == [("UNATTRIBUTED", "High", True)]
167
+ assert "priority--high'>High</span> <span class='muted'>(edited)" in report.render_html()
168
+
169
+
170
+ def test_quick_test_without_overrides_ranks_automatically():
171
+ from agentprobe.agents.scripted import ScriptedAgent
172
+ from agentprobe.injector import NullInjector
173
+ from agentprobe.quickstart import quick_test
174
+ from agentprobe.scenarios.registry import BY_ID
175
+
176
+ report = quick_test(BY_ID["clean-1"], lambda: ScriptedAgent(script=[("get_ticket", {"id": "T-1001"})]), injector=NullInjector(), mode="recovery")
177
+ assert report.priority_overrides == {}
178
+ assert [(r["tier"], r["edited"]) for r in report.priorities()] == [("Medium", False)]
179
+
180
+
181
+ def test_quick_test_all_carries_the_overrides_onto_the_combined_report():
182
+ from agentprobe.agents.scripted import ScriptedAgent
183
+ from agentprobe.injector import NullInjector
184
+ from agentprobe.quickstart import quick_test_all
185
+ from agentprobe.scenarios.registry import BY_ID
186
+
187
+ scenarios = {k: BY_ID[k] for k in ("clean-1", "clean-2")}
188
+ report = quick_test_all(scenarios, lambda: ScriptedAgent(script=[("get_ticket", {"id": "T-1001"})]), injector_factory=NullInjector, mode="recovery", priority_overrides={"UNATTRIBUTED": "Low"})
189
+ assert report.priority_overrides == {"UNATTRIBUTED": "Low"}
190
+ assert all(r["tier"] == "Low" and r["edited"] for r in report.priorities())