agentprobe-testing 1.8.0__tar.gz → 1.9.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 (142) hide show
  1. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/PKG-INFO +1 -1
  2. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/__init__.py +6 -1
  3. agentprobe_testing-1.9.0/agentprobe/focus.py +77 -0
  4. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/quickstart.py +12 -0
  5. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/report.py +25 -1
  6. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/savings.py +7 -2
  7. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe_testing.egg-info/PKG-INFO +1 -1
  8. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe_testing.egg-info/SOURCES.txt +2 -0
  9. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/pyproject.toml +1 -1
  10. agentprobe_testing-1.9.0/tests/test_focus.py +87 -0
  11. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/LICENSE +0 -0
  12. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/README.md +0 -0
  13. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/agents/__init__.py +0 -0
  14. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/agents/base.py +0 -0
  15. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/agents/browser_agent.py +0 -0
  16. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/agents/openai_chat.py +0 -0
  17. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/agents/rule_based.py +0 -0
  18. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/agents/scripted.py +0 -0
  19. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/agents/target_agent.py +0 -0
  20. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/agreement.py +0 -0
  21. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/classifier.py +0 -0
  22. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/cli.py +0 -0
  23. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/connect.py +0 -0
  24. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/diff.py +0 -0
  25. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/domain.py +0 -0
  26. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/domains/__init__.py +0 -0
  27. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/domains/access_control/__init__.py +0 -0
  28. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/domains/access_control/agent.py +0 -0
  29. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/domains/access_control/clean.py +0 -0
  30. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/domains/access_control/complex_agent.py +0 -0
  31. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/domains/access_control/decoy.py +0 -0
  32. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/domains/access_control/domain.py +0 -0
  33. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/domains/access_control/entities.py +0 -0
  34. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/domains/access_control/injector_prompt.py +0 -0
  35. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/domains/access_control/rule_based_agent.py +0 -0
  36. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/domains/access_control/scenarios.py +0 -0
  37. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/domains/access_control/split.py +0 -0
  38. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/domains/access_control/tools.py +0 -0
  39. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/domains/access_control/trap.py +0 -0
  40. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/domains/tau_retail/__init__.py +0 -0
  41. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/domains/tau_retail/chat_agent.py +0 -0
  42. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/domains/tau_retail/domain.py +0 -0
  43. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/domains/tau_retail/injector_prompt.py +0 -0
  44. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/domains/tau_retail/loader.py +0 -0
  45. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/domains/tau_retail/openai_compat_agent.py +0 -0
  46. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/domains/tau_retail/oracle.py +0 -0
  47. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/domains/tau_retail/tools.py +0 -0
  48. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/domains/tau_retail/user_sim.py +0 -0
  49. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/domains/tau_retail/world.py +0 -0
  50. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/domains/web_browsing/__init__.py +0 -0
  51. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/domains/web_browsing/agent.py +0 -0
  52. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/domains/web_browsing/clean.py +0 -0
  53. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/domains/web_browsing/domain.py +0 -0
  54. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/domains/web_browsing/entities.py +0 -0
  55. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/domains/web_browsing/injector_prompt.py +0 -0
  56. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/domains/web_browsing/rule_based_agent.py +0 -0
  57. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/domains/web_browsing/scenarios.py +0 -0
  58. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/domains/web_browsing/tools.py +0 -0
  59. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/domains/web_browsing/trap.py +0 -0
  60. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/feedback.py +0 -0
  61. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/fixes.py +0 -0
  62. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/generic_world.py +0 -0
  63. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/injection.py +0 -0
  64. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/injector.py +0 -0
  65. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/llm.py +0 -0
  66. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/local_runner.py +0 -0
  67. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/mcp_server.py +0 -0
  68. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/playbook.py +0 -0
  69. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/priority.py +0 -0
  70. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/reachability.py +0 -0
  71. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/registry.py +0 -0
  72. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/runner.py +0 -0
  73. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/sandbox.py +0 -0
  74. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/scenario.py +0 -0
  75. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/scenarios/__init__.py +0 -0
  76. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/scenarios/clean.py +0 -0
  77. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/scenarios/decoy.py +0 -0
  78. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/scenarios/registry.py +0 -0
  79. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/scenarios/split.py +0 -0
  80. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/scenarios/trap.py +0 -0
  81. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/summarize.py +0 -0
  82. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/termui.py +0 -0
  83. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/toolcheck.py +0 -0
  84. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/toolimport.py +0 -0
  85. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/tools.py +0 -0
  86. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/trajectory.py +0 -0
  87. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/triage.py +0 -0
  88. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/validate_scenarios.py +0 -0
  89. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe/world.py +0 -0
  90. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe_testing.egg-info/dependency_links.txt +0 -0
  91. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe_testing.egg-info/entry_points.txt +0 -0
  92. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe_testing.egg-info/requires.txt +0 -0
  93. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/agentprobe_testing.egg-info/top_level.txt +0 -0
  94. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/setup.cfg +0 -0
  95. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/tests/test_access_control_agent.py +0 -0
  96. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/tests/test_access_control_domain.py +0 -0
  97. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/tests/test_access_control_rule_based_agent.py +0 -0
  98. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/tests/test_access_control_scenarios.py +0 -0
  99. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/tests/test_access_control_tools.py +0 -0
  100. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/tests/test_agreement.py +0 -0
  101. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/tests/test_browser_agent.py +0 -0
  102. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/tests/test_classifier.py +0 -0
  103. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/tests/test_cli.py +0 -0
  104. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/tests/test_complex_access_control_agent.py +0 -0
  105. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/tests/test_connect.py +0 -0
  106. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/tests/test_diff.py +0 -0
  107. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/tests/test_domain.py +0 -0
  108. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/tests/test_failures.py +0 -0
  109. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/tests/test_feedback.py +0 -0
  110. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/tests/test_fixes.py +0 -0
  111. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/tests/test_generic_world.py +0 -0
  112. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/tests/test_injection.py +0 -0
  113. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/tests/test_injection_mode.py +0 -0
  114. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/tests/test_injector.py +0 -0
  115. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/tests/test_llm.py +0 -0
  116. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/tests/test_local_runner.py +0 -0
  117. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/tests/test_mcp_server.py +0 -0
  118. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/tests/test_package_api.py +0 -0
  119. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/tests/test_playbook.py +0 -0
  120. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/tests/test_priority.py +0 -0
  121. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/tests/test_quickstart.py +0 -0
  122. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/tests/test_reachability.py +0 -0
  123. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/tests/test_registry.py +0 -0
  124. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/tests/test_report.py +0 -0
  125. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/tests/test_rule_based_agent.py +0 -0
  126. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/tests/test_runner.py +0 -0
  127. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/tests/test_runner_connection.py +0 -0
  128. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/tests/test_runner_timeouts.py +0 -0
  129. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/tests/test_sandbox.py +0 -0
  130. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/tests/test_savings.py +0 -0
  131. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/tests/test_scenarios.py +0 -0
  132. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/tests/test_target_agent.py +0 -0
  133. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/tests/test_tau_retail.py +0 -0
  134. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/tests/test_termui.py +0 -0
  135. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/tests/test_toolcheck.py +0 -0
  136. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/tests/test_toolimport.py +0 -0
  137. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/tests/test_tools.py +0 -0
  138. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/tests/test_trajectory.py +0 -0
  139. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/tests/test_triage.py +0 -0
  140. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/tests/test_validate_scenarios.py +0 -0
  141. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.0}/tests/test_web_browsing_domain.py +0 -0
  142. {agentprobe_testing-1.8.0 → agentprobe_testing-1.9.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.8.0
3
+ Version: 1.9.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
 
@@ -56,6 +56,7 @@ from agentprobe.registry import (
56
56
  )
57
57
  from agentprobe.report import InjectionRecord, Report
58
58
  from agentprobe.runner import run_recovery, run_robustness_pair
59
+ from agentprobe.focus import FOCUS_KINDS, FOCUS_LABELS, focus_kind_order, normalize_focus
59
60
  from agentprobe.savings import FixedFaultInjector, estimate_cost, fixed_fault_for, model_injector_for, retest_failures
60
61
  from agentprobe.connect import MODES as CONNECTION_MODES, connect
61
62
  from agentprobe.sandbox import DriverAgent, ToolBox, ToolCallError
@@ -65,7 +66,7 @@ from agentprobe.scenario import CommitPattern, FactPattern, GoalSpec, Scenario
65
66
  from agentprobe.scenarios.registry import ALL_SCENARIOS as TICKET_SCENARIOS
66
67
  from agentprobe.scenarios.registry import BY_ID as TICKET_SCENARIOS_BY_ID
67
68
 
68
- __version__ = "1.8.0"
69
+ __version__ = "1.9.0"
69
70
 
70
71
  __all__ = [
71
72
  "CONNECTION_MODES",
@@ -79,6 +80,10 @@ __all__ = [
79
80
  "estimate_cost",
80
81
  "fixed_fault_for",
81
82
  "model_injector_for",
83
+ "FOCUS_KINDS",
84
+ "FOCUS_LABELS",
85
+ "focus_kind_order",
86
+ "normalize_focus",
82
87
  "retest_failures",
83
88
  "Agent",
84
89
  "AgentAction",
@@ -0,0 +1,77 @@
1
+ """Tell the test which failures cost you the most, so those are tried first.
2
+
3
+ `focus` is a list of failure kind names (InjectionKind values), most important first. The adaptive
4
+ injector works through its failure kinds in order, so putting yours first means they are the ones
5
+ still covered if a run stops early (for example on a spending budget). The report then says, for each
6
+ kind you chose, whether it was actually tested and how the agent did, and says so plainly when a
7
+ chosen failure could not be tried in a scenario.
8
+
9
+ injector = ap.model_injector_for(domain, focus=["FALSE_SUCCESS", "PROMPT_INJECTION"])
10
+ report = ap.quick_test_all(scenarios, agent, injector_factory=lambda: injector_for(...), focus=[...])
11
+ """
12
+ from __future__ import annotations
13
+
14
+ from typing import Iterable, Optional
15
+
16
+ from agentprobe.injection import InjectionKind
17
+ from agentprobe.injector import _DEFAULT_KIND_ORDER
18
+
19
+ # Plain-language names for each kind, in the words a customer would use. Also what the website shows.
20
+ FOCUS_LABELS: dict[str, str] = {
21
+ "PHANTOM_SUCCESS": "A system says it did something, but nothing happened",
22
+ "FALSE_SUCCESS": "A system reports a success that isn't true",
23
+ "UNVERIFIED_OUTCOME": "A result is pending or unconfirmed, and the agent calls it done",
24
+ "TOOL_ERROR": "A tool call fails",
25
+ "PROMPT_INJECTION": "Hidden instructions in ordinary-looking data",
26
+ "SOCIAL_PRESSURE": "A customer pushes with emotion or authority",
27
+ "SCOPE_CREEP": "A request that goes beyond what the agent should do",
28
+ "STALE_READ": "A record changes after the agent already read it",
29
+ "CONTRADICTION": "A new fact contradicts an earlier one",
30
+ "LATE_INFO": "Something important arrives only after the agent acted",
31
+ "MISSING_INFO": "A field the agent needs is missing",
32
+ "AMBIGUITY": "A second, equally valid record appears",
33
+ "DEPENDENT_FOLLOWUP": "A note arrives that follows from the agent's own action",
34
+ "DISTRACTOR": "An irrelevant note appears",
35
+ "FREEFORM": "A failure the injector invents for the situation",
36
+ }
37
+
38
+ FOCUS_KINDS: tuple[str, ...] = tuple(FOCUS_LABELS)
39
+
40
+
41
+ def normalize_focus(values: Optional[Iterable[str]]) -> list[str]:
42
+ """Validated, de-duplicated focus list in the order given. Raises ValueError on an unknown kind."""
43
+ out: list[str] = []
44
+ for v in values or []:
45
+ name = str(v).strip().upper()
46
+ if not name:
47
+ continue
48
+ if name not in FOCUS_LABELS:
49
+ raise ValueError(f"unknown failure kind {v!r}; choose from: {', '.join(FOCUS_KINDS)}")
50
+ if name not in out:
51
+ out.append(name)
52
+ return out
53
+
54
+
55
+ def focus_kind_order(focus: Optional[Iterable[str]]) -> list[InjectionKind]:
56
+ """The injector's kind order: your kinds first (in your order), then the usual order."""
57
+ chosen = [InjectionKind(n) for n in normalize_focus(focus)]
58
+ rest = [k for k in _DEFAULT_KIND_ORDER if k not in chosen]
59
+ return chosen + rest
60
+
61
+
62
+ def focus_coverage(per_kind: list[dict], focus: Iterable[str]) -> list[dict]:
63
+ """For each chosen kind: was it tested, and how did the agent do. `per_kind` is Report._compute()["per_kind"]."""
64
+ rows = {r["kind"]: r for r in per_kind}
65
+ out = []
66
+ for name in normalize_focus(focus):
67
+ r = rows.get(name)
68
+ out.append(
69
+ {
70
+ "kind": name,
71
+ "label": FOCUS_LABELS[name],
72
+ "tested": r is not None and r["n"] > 0,
73
+ "n": r["n"] if r else 0,
74
+ "handled": r["handled"] if r else 0,
75
+ }
76
+ )
77
+ return out
@@ -14,6 +14,7 @@ import time
14
14
  import urllib.request
15
15
  from typing import Any, Callable, Optional
16
16
 
17
+ from agentprobe.focus import normalize_focus as _normalize_focus
17
18
  from agentprobe.agents.base import Agent, AgentAction
18
19
  from agentprobe.domain import Domain, TICKET_DOMAIN
19
20
  from agentprobe.injection import injection_was_triggered
@@ -165,6 +166,7 @@ def quick_test(
165
166
  record_injections: bool = False,
166
167
  summarize_failures: bool = False,
167
168
  priority_overrides: Optional[dict] = None,
169
+ focus: Optional[list[str]] = None,
168
170
  ) -> Report:
169
171
  """Run one scenario and get back a ready Report -- the one-call version
170
172
  of run_robustness_pair()/run_recovery() + Report(). `agent` can be a
@@ -221,6 +223,7 @@ def quick_test(
221
223
  priority_overrides=clean_overrides(priority_overrides),
222
224
  injection_mode=injection_mode,
223
225
  tool_expectations={"known": sorted(domain.all_tools), "required": {scenario.id: sorted(required_tools(scenario))}},
226
+ focus_kinds=_normalize_focus(focus),
224
227
  )
225
228
  if recording is not None:
226
229
  # Stashed here (not a Report field -- Report is a general-purpose
@@ -262,6 +265,7 @@ def quick_test_all(
262
265
  stop_when_achieved: bool = False,
263
266
  preflight: bool = False,
264
267
  agent_tools: Optional[list[str]] = None,
268
+ focus: Optional[list[str]] = None,
265
269
  ) -> Report:
266
270
  """Run every scenario in `scenarios` (a dict[str, Scenario] -- e.g.
267
271
  TICKET_SCENARIOS or a FetchedDomain's .scenarios -- or a plain list of
@@ -269,6 +273,9 @@ def quick_test_all(
269
273
  Report -- the batch version of calling quick_test() once per scenario
270
274
  and merging the results by hand.
271
275
 
276
+ `focus` is a list of failure kinds that cost you the most (see focus.py): the report says whether each
277
+ was actually tested. To make the injector try them first, build it with `model_injector_for(..., focus=...)`.
278
+
272
279
  `injector_factory` is a zero-arg callable returning a FRESH Injector
273
280
  instance per scenario (same shape as wrap_agent's target_factory), e.g.
274
281
  `injector_factory=lambda: ModelInjector()`. This must be a factory, not
@@ -338,6 +345,10 @@ def quick_test_all(
338
345
  )
339
346
 
340
347
  target_factory = agent if _looks_like_factory(agent) else wrap_agent(agent)
348
+ from agentprobe.focus import normalize_focus
349
+
350
+ focus_kinds = normalize_focus(focus) # refuse an unknown kind before spending anything
351
+
341
352
  raw_make_injector = injector_factory if injector_factory is not None else (lambda: NullInjector())
342
353
  if not callable(raw_make_injector):
343
354
  raise ValueError(
@@ -446,6 +457,7 @@ def quick_test_all(
446
457
  "required": {sc.id: sorted(required_tools(sc)) for _id, sc in items},
447
458
  },
448
459
  )
460
+ report.focus_kinds = focus_kinds
449
461
  report._scenarios_run = len(all_chaos)
450
462
  report._skipped = skipped
451
463
  report._budget_stopped = budget_stopped
@@ -327,6 +327,9 @@ class Report:
327
327
  the "What failed" section (built straight from data the run already
328
328
  has) it costs an extra API call. None means none was requested."""
329
329
  failure_narrative_cost_usd: float = 0.0
330
+ focus_kinds: list[str] = field(default_factory=list)
331
+ """Failure kinds the reader said cost them the most, most important first (see focus.py). The
332
+ report says, for each, whether it was actually tested."""
330
333
 
331
334
  def _compute(self) -> dict:
332
335
  """Shared computation behind both render() and render_html() --
@@ -871,7 +874,7 @@ class Report:
871
874
  )
872
875
 
873
876
  priorities_html = self._priorities_html(esc, c) + self._prompt_fixes_html(esc, c)
874
- trust_html = self._landing_html(esc) + self._tool_check_html(esc)
877
+ trust_html = self._focus_html(esc) + self._landing_html(esc) + self._tool_check_html(esc)
875
878
  failures_html = self._failures_html(esc, c)
876
879
 
877
880
  return f"""<!doctype html>
@@ -950,6 +953,27 @@ class Report:
950
953
  f"<tbody>{''.join(rows)}</tbody></table></div></section><!--/priority-table-->"
951
954
  )
952
955
 
956
+ def focus_coverage(self) -> list[dict]:
957
+ """For each failure kind the reader asked to be tested first: tested or not, and how it went."""
958
+ from agentprobe.focus import focus_coverage
959
+
960
+ return focus_coverage(self._compute()["per_kind"], self.focus_kinds)
961
+
962
+ def _focus_html(self, esc) -> str:
963
+ if not self.focus_kinds:
964
+ return ""
965
+ rows = []
966
+ for r in self.focus_coverage():
967
+ if r["tested"]:
968
+ status = f"tested {r['n']} time{'s' if r['n'] != 1 else ''}: the agent handled {r['handled']} of {r['n']}"
969
+ else:
970
+ status = "<strong>not tested</strong>: it could not be tried in these scenarios (or the run ended first)"
971
+ rows.append(f"<li><strong>{esc(r['label'])}</strong>: {status}</li>")
972
+ return (
973
+ "<div class='injector-error-banner trust-banner focus-banner'><strong>You asked us to test these first</strong>"
974
+ f"<ul>{''.join(rows)}</ul></div>"
975
+ )
976
+
953
977
  def _landing_html(self, esc) -> str:
954
978
  """A banner when failures were expected but did not (all) land: a pass in such a scenario
955
979
  is not evidence of anything."""
@@ -159,8 +159,13 @@ def fixed_fault_for(scenario: Scenario, kind: InjectionKind = InjectionKind.FALS
159
159
  CHEAP_INJECTOR_MODEL = "claude-haiku-4-5-20251001"
160
160
 
161
161
 
162
- def model_injector_for(domain: Domain = TICKET_DOMAIN, model: str = CHEAP_INJECTOR_MODEL, **kwargs: Any) -> ModelInjector:
163
- """The adaptive injector on a chosen (by default cheaper) model, wired to `domain`."""
162
+ def model_injector_for(domain: Domain = TICKET_DOMAIN, model: str = CHEAP_INJECTOR_MODEL, focus: Optional[list[str]] = None, **kwargs: Any) -> ModelInjector:
163
+ """The adaptive injector on a chosen (by default cheaper) model, wired to `domain`.
164
+ `focus` lists the failure kinds that cost you the most; the injector tries those first."""
165
+ if focus:
166
+ from agentprobe.focus import focus_kind_order
167
+
168
+ kwargs["kind_order"] = focus_kind_order(focus)
164
169
  if domain.injector_system_prompt is None:
165
170
  return ModelInjector(model=model, **kwargs)
166
171
  return ModelInjector(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentprobe-testing
3
- Version: 1.8.0
3
+ Version: 1.9.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
 
@@ -10,6 +10,7 @@ agentprobe/diff.py
10
10
  agentprobe/domain.py
11
11
  agentprobe/feedback.py
12
12
  agentprobe/fixes.py
13
+ agentprobe/focus.py
13
14
  agentprobe/generic_world.py
14
15
  agentprobe/injection.py
15
16
  agentprobe/injector.py
@@ -104,6 +105,7 @@ tests/test_domain.py
104
105
  tests/test_failures.py
105
106
  tests/test_feedback.py
106
107
  tests/test_fixes.py
108
+ tests/test_focus.py
107
109
  tests/test_generic_world.py
108
110
  tests/test_injection.py
109
111
  tests/test_injection_mode.py
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agentprobe-testing"
3
- version = "1.8.0"
3
+ version = "1.9.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,87 @@
1
+ import pytest
2
+
3
+ from agentprobe.agents.base import AgentAction
4
+ from agentprobe.focus import FOCUS_KINDS, FOCUS_LABELS, focus_coverage, focus_kind_order, normalize_focus
5
+ from agentprobe.injection import InjectionKind
6
+ from agentprobe.injector import _DEFAULT_KIND_ORDER
7
+ from agentprobe.quickstart import quick_test_all
8
+ from agentprobe.savings import fixed_fault_for, model_injector_for
9
+ from agentprobe.scenarios.registry import BY_ID
10
+
11
+ S1 = BY_ID["clean-1"]
12
+
13
+
14
+ def _agent(task, history):
15
+ calls = [h["tool_name"] for h in history]
16
+ if "get_ticket" not in calls:
17
+ return AgentAction(kind="tool_call", tool_name="get_ticket", tool_args={"id": "T-1001"})
18
+ if "issue_refund" not in calls:
19
+ return AgentAction(kind="tool_call", tool_name="issue_refund", tool_args={"ticket_id": "T-1001", "amount": 49.99})
20
+ if "close_ticket" not in calls:
21
+ return AgentAction(kind="tool_call", tool_name="close_ticket", tool_args={"ticket_id": "T-1001", "resolution": "refunded"})
22
+ return AgentAction(kind="final_answer", text="done")
23
+
24
+
25
+ def test_every_injection_kind_has_a_plain_label():
26
+ assert set(FOCUS_KINDS) == {k.value for k in InjectionKind}
27
+ assert all(FOCUS_LABELS[k] for k in FOCUS_KINDS)
28
+
29
+
30
+ def test_normalize_validates_dedupes_and_keeps_order():
31
+ assert normalize_focus(["false_success", "TOOL_ERROR", "FALSE_SUCCESS", " "]) == ["FALSE_SUCCESS", "TOOL_ERROR"]
32
+ assert normalize_focus(None) == []
33
+ with pytest.raises(ValueError, match="unknown failure kind"):
34
+ normalize_focus(["NOT_A_KIND"])
35
+
36
+
37
+ def test_kind_order_puts_the_chosen_kinds_first_then_the_usual_order():
38
+ order = focus_kind_order(["SCOPE_CREEP", "FALSE_SUCCESS"])
39
+ assert order[:2] == [InjectionKind.SCOPE_CREEP, InjectionKind.FALSE_SUCCESS]
40
+ assert [k for k in order[2:]] == [k for k in _DEFAULT_KIND_ORDER if k not in (InjectionKind.SCOPE_CREEP, InjectionKind.FALSE_SUCCESS)]
41
+ assert focus_kind_order(None) == list(_DEFAULT_KIND_ORDER)
42
+
43
+
44
+ def test_model_injector_for_uses_the_focus_order():
45
+ inj = model_injector_for(focus=["PROMPT_INJECTION", "LATE_INFO"])
46
+ assert inj._kind_order[:2] == [InjectionKind.PROMPT_INJECTION, InjectionKind.LATE_INFO]
47
+
48
+
49
+ def test_coverage_marks_tested_and_untested_kinds():
50
+ rows = focus_coverage([{"kind": "TOOL_ERROR", "handled": 2, "n": 3}], ["TOOL_ERROR", "SCOPE_CREEP"])
51
+ assert rows[0] | {"label": ""} == {"kind": "TOOL_ERROR", "label": "", "tested": True, "n": 3, "handled": 2}
52
+ assert rows[1]["tested"] is False and rows[1]["n"] == 0
53
+
54
+
55
+ def test_report_says_which_chosen_failures_were_tested_and_which_were_not():
56
+ report = quick_test_all(
57
+ [S1], _agent, injector_factory=lambda sc: fixed_fault_for(sc, InjectionKind.TOOL_ERROR), focus=["TOOL_ERROR", "SCOPE_CREEP"]
58
+ )
59
+ assert report.focus_kinds == ["TOOL_ERROR", "SCOPE_CREEP"]
60
+ html = report.render_html()
61
+ assert "You asked us to test these first" in html
62
+ assert FOCUS_LABELS["TOOL_ERROR"] in html and FOCUS_LABELS["SCOPE_CREEP"] in html
63
+ cov = {r["kind"]: r for r in report.focus_coverage()}
64
+ assert cov["SCOPE_CREEP"]["tested"] is False
65
+ assert "not tested" in html
66
+
67
+
68
+ def test_no_focus_means_no_focus_section():
69
+ report = quick_test_all([S1], _agent)
70
+ assert report.focus_kinds == []
71
+ assert "You asked us to test these first" not in report.render_html()
72
+
73
+
74
+ def test_bad_focus_is_refused_up_front():
75
+ with pytest.raises(ValueError):
76
+ quick_test_all([S1], _agent, focus=["NOPE"])
77
+
78
+
79
+ def test_the_websites_list_of_failure_types_matches_the_library():
80
+ import re
81
+ from pathlib import Path
82
+
83
+ js = (Path(__file__).resolve().parent.parent / "server" / "src" / "focus.js").read_text()
84
+ site = re.findall(r'\["([A-Z_]+)", "', js)
85
+ assert sorted(site) == sorted(FOCUS_KINDS)
86
+ labels = dict(re.findall(r'\["([A-Z_]+)", "([^"]+)"\]', js))
87
+ assert labels == FOCUS_LABELS