agentprobe-testing 1.4.1__tar.gz → 1.5.1__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 (124) hide show
  1. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/PKG-INFO +1 -1
  2. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/__init__.py +7 -1
  3. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/agents/target_agent.py +3 -1
  4. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/classifier.py +3 -1
  5. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/domain.py +6 -0
  6. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/domains/access_control/agent.py +3 -1
  7. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/domains/access_control/complex_agent.py +3 -1
  8. agentprobe_testing-1.5.1/agentprobe/domains/tau_retail/__init__.py +7 -0
  9. agentprobe_testing-1.5.1/agentprobe/domains/tau_retail/chat_agent.py +38 -0
  10. agentprobe_testing-1.5.1/agentprobe/domains/tau_retail/domain.py +110 -0
  11. agentprobe_testing-1.5.1/agentprobe/domains/tau_retail/injector_prompt.py +51 -0
  12. agentprobe_testing-1.5.1/agentprobe/domains/tau_retail/loader.py +99 -0
  13. agentprobe_testing-1.5.1/agentprobe/domains/tau_retail/openai_compat_agent.py +85 -0
  14. agentprobe_testing-1.5.1/agentprobe/domains/tau_retail/oracle.py +75 -0
  15. agentprobe_testing-1.5.1/agentprobe/domains/tau_retail/tools.py +88 -0
  16. agentprobe_testing-1.5.1/agentprobe/domains/tau_retail/user_sim.py +80 -0
  17. agentprobe_testing-1.5.1/agentprobe/domains/tau_retail/world.py +92 -0
  18. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/domains/web_browsing/agent.py +3 -1
  19. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/injector.py +9 -1
  20. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/llm.py +32 -0
  21. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/quickstart.py +101 -7
  22. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/registry.py +13 -4
  23. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/runner.py +33 -9
  24. agentprobe_testing-1.5.1/agentprobe/savings.py +208 -0
  25. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/summarize.py +2 -1
  26. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/validate_scenarios.py +3 -1
  27. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe_testing.egg-info/PKG-INFO +1 -1
  28. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe_testing.egg-info/SOURCES.txt +14 -0
  29. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/pyproject.toml +1 -1
  30. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/tests/test_llm.py +44 -0
  31. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/tests/test_registry.py +15 -0
  32. agentprobe_testing-1.5.1/tests/test_runner_timeouts.py +67 -0
  33. agentprobe_testing-1.5.1/tests/test_savings.py +170 -0
  34. agentprobe_testing-1.5.1/tests/test_tau_retail.py +157 -0
  35. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/LICENSE +0 -0
  36. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/README.md +0 -0
  37. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/agents/__init__.py +0 -0
  38. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/agents/base.py +0 -0
  39. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/agents/browser_agent.py +0 -0
  40. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/agents/rule_based.py +0 -0
  41. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/agents/scripted.py +0 -0
  42. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/agreement.py +0 -0
  43. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/cli.py +0 -0
  44. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/diff.py +0 -0
  45. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/domains/__init__.py +0 -0
  46. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/domains/access_control/__init__.py +0 -0
  47. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/domains/access_control/clean.py +0 -0
  48. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/domains/access_control/decoy.py +0 -0
  49. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/domains/access_control/domain.py +0 -0
  50. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/domains/access_control/entities.py +0 -0
  51. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/domains/access_control/injector_prompt.py +0 -0
  52. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/domains/access_control/rule_based_agent.py +0 -0
  53. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/domains/access_control/scenarios.py +0 -0
  54. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/domains/access_control/split.py +0 -0
  55. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/domains/access_control/tools.py +0 -0
  56. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/domains/access_control/trap.py +0 -0
  57. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/domains/web_browsing/__init__.py +0 -0
  58. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/domains/web_browsing/clean.py +0 -0
  59. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/domains/web_browsing/domain.py +0 -0
  60. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/domains/web_browsing/entities.py +0 -0
  61. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/domains/web_browsing/injector_prompt.py +0 -0
  62. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/domains/web_browsing/rule_based_agent.py +0 -0
  63. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/domains/web_browsing/scenarios.py +0 -0
  64. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/domains/web_browsing/tools.py +0 -0
  65. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/domains/web_browsing/trap.py +0 -0
  66. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/feedback.py +0 -0
  67. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/generic_world.py +0 -0
  68. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/injection.py +0 -0
  69. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/playbook.py +0 -0
  70. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/priority.py +0 -0
  71. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/reachability.py +0 -0
  72. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/report.py +0 -0
  73. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/scenario.py +0 -0
  74. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/scenarios/__init__.py +0 -0
  75. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/scenarios/clean.py +0 -0
  76. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/scenarios/decoy.py +0 -0
  77. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/scenarios/registry.py +0 -0
  78. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/scenarios/split.py +0 -0
  79. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/scenarios/trap.py +0 -0
  80. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/termui.py +0 -0
  81. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/tools.py +0 -0
  82. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/trajectory.py +0 -0
  83. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/triage.py +0 -0
  84. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe/world.py +0 -0
  85. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe_testing.egg-info/dependency_links.txt +0 -0
  86. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe_testing.egg-info/entry_points.txt +0 -0
  87. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe_testing.egg-info/requires.txt +0 -0
  88. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/agentprobe_testing.egg-info/top_level.txt +0 -0
  89. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/setup.cfg +0 -0
  90. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/tests/test_access_control_agent.py +0 -0
  91. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/tests/test_access_control_domain.py +0 -0
  92. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/tests/test_access_control_rule_based_agent.py +0 -0
  93. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/tests/test_access_control_scenarios.py +0 -0
  94. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/tests/test_access_control_tools.py +0 -0
  95. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/tests/test_agreement.py +0 -0
  96. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/tests/test_browser_agent.py +0 -0
  97. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/tests/test_classifier.py +0 -0
  98. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/tests/test_cli.py +0 -0
  99. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/tests/test_complex_access_control_agent.py +0 -0
  100. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/tests/test_diff.py +0 -0
  101. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/tests/test_domain.py +0 -0
  102. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/tests/test_failures.py +0 -0
  103. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/tests/test_feedback.py +0 -0
  104. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/tests/test_generic_world.py +0 -0
  105. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/tests/test_injection.py +0 -0
  106. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/tests/test_injection_mode.py +0 -0
  107. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/tests/test_injector.py +0 -0
  108. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/tests/test_package_api.py +0 -0
  109. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/tests/test_playbook.py +0 -0
  110. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/tests/test_priority.py +0 -0
  111. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/tests/test_quickstart.py +0 -0
  112. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/tests/test_reachability.py +0 -0
  113. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/tests/test_report.py +0 -0
  114. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/tests/test_rule_based_agent.py +0 -0
  115. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/tests/test_runner.py +0 -0
  116. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/tests/test_scenarios.py +0 -0
  117. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/tests/test_target_agent.py +0 -0
  118. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/tests/test_termui.py +0 -0
  119. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/tests/test_tools.py +0 -0
  120. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/tests/test_trajectory.py +0 -0
  121. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/tests/test_triage.py +0 -0
  122. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/tests/test_validate_scenarios.py +0 -0
  123. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/tests/test_web_browsing_domain.py +0 -0
  124. {agentprobe_testing-1.4.1 → agentprobe_testing-1.5.1}/tests/test_world.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentprobe-testing
3
- Version: 1.4.1
3
+ Version: 1.5.1
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,13 +56,19 @@ 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.savings import FixedFaultInjector, estimate_cost, fixed_fault_for, model_injector_for, retest_failures
59
60
  from agentprobe.scenario import CommitPattern, FactPattern, GoalSpec, Scenario
60
61
  from agentprobe.scenarios.registry import ALL_SCENARIOS as TICKET_SCENARIOS
61
62
  from agentprobe.scenarios.registry import BY_ID as TICKET_SCENARIOS_BY_ID
62
63
 
63
- __version__ = "1.4.1"
64
+ __version__ = "1.5.1"
64
65
 
65
66
  __all__ = [
67
+ "FixedFaultInjector",
68
+ "estimate_cost",
69
+ "fixed_fault_for",
70
+ "model_injector_for",
71
+ "retest_failures",
66
72
  "Agent",
67
73
  "AgentAction",
68
74
  "AppliedInjection",
@@ -16,6 +16,8 @@ from agentprobe.llm import cacheable_system, create_deterministic, response_cost
16
16
 
17
17
  import anthropic
18
18
 
19
+
20
+ from agentprobe.llm import make_client
19
21
  from agentprobe.agents.base import Agent, AgentAction
20
22
 
21
23
  SYSTEM_PROMPT = """You are a customer support agent. You have read tools \
@@ -44,7 +46,7 @@ class TargetAgent(Agent):
44
46
  max_tokens: int = 1024,
45
47
  system_prompt: Optional[str] = None,
46
48
  ):
47
- self._client = anthropic.Anthropic()
49
+ self._client = make_client()
48
50
  self._model = model
49
51
  self._max_tokens = max_tokens
50
52
  self._system_prompt = system_prompt if system_prompt is not None else SYSTEM_PROMPT
@@ -11,6 +11,8 @@ from enum import Enum
11
11
 
12
12
  import anthropic
13
13
 
14
+
15
+ from agentprobe.llm import make_client
14
16
  from agentprobe.injection import AppliedInjection
15
17
  from agentprobe.llm import cacheable_system, create_deterministic, response_cost_usd
16
18
  from agentprobe.trajectory import Step
@@ -126,7 +128,7 @@ def classify_response(
126
128
  ) -> ClassifiedResponse:
127
129
  """`steps` is the full trajectory, not pre-filtered to post-injection
128
130
  steps -- see _render_classification_prompt for why."""
129
- client = anthropic.Anthropic()
131
+ client = make_client()
130
132
  response = create_deterministic(
131
133
  client,
132
134
  model=model,
@@ -87,6 +87,12 @@ class Domain:
87
87
  pre_dispatch's ON_TOOL_CALL check, so the injector would silently arm
88
88
  on every step and never once fire (0 fired, 100% expired, no error)."""
89
89
 
90
+ uninjectable_tools: frozenset = frozenset()
91
+ """Tools that are a communication channel rather than a system call (e.g.
92
+ the tau-bench `respond` tool, which talks to the customer). The runner never
93
+ fires a tool-response injection (error, phantom/false success, unverified
94
+ outcome) on them; triggers stay armed until a different tool is called."""
95
+
90
96
  @property
91
97
  def all_tools(self) -> frozenset:
92
98
  """Derived from tool_schemas rather than stored separately -- the
@@ -15,6 +15,8 @@ from agentprobe.llm import cacheable_system, create_deterministic, response_cost
15
15
 
16
16
  import anthropic
17
17
 
18
+
19
+ from agentprobe.llm import make_client
18
20
  from agentprobe.agents.base import Agent, AgentAction
19
21
 
20
22
  SYSTEM_PROMPT = """You are an IT access-control agent. You have read tools \
@@ -29,7 +31,7 @@ reply with a short final summary of what you did, in plain text."""
29
31
 
30
32
  class AccessControlAgent(Agent):
31
33
  def __init__(self, model: str = "claude-haiku-4-5-20251001", max_tokens: int = 1024):
32
- self._client = anthropic.Anthropic()
34
+ self._client = make_client()
33
35
  self._model = model
34
36
  self._max_tokens = max_tokens
35
37
  self._messages: list[dict[str, Any]] = []
@@ -17,6 +17,8 @@ from agentprobe.llm import cacheable_system, create_deterministic, response_cost
17
17
 
18
18
  import anthropic
19
19
 
20
+
21
+ from agentprobe.llm import make_client
20
22
  from agentprobe.agents.base import Agent, AgentAction
21
23
 
22
24
  COMPLEX_SYSTEM_PROMPT = """You are a senior IT access-control specialist handling access requests \
@@ -62,7 +64,7 @@ summary of what you decided and why."""
62
64
 
63
65
  class ComplexAccessControlAgent(Agent):
64
66
  def __init__(self, model: str = "claude-haiku-4-5-20251001", max_tokens: int = 1536):
65
- self._client = anthropic.Anthropic()
67
+ self._client = make_client()
66
68
  self._model = model
67
69
  self._max_tokens = max_tokens
68
70
  self._messages: list[dict[str, Any]] = []
@@ -0,0 +1,7 @@
1
+ """Sierra's tau-bench retail environment, run unmodified inside AgentProbe.
2
+
3
+ tau-bench (https://github.com/sierra-research/tau-bench, MIT, (c) Sierra) is
4
+ NOT vendored here. Point TAU_BENCH_DIR at a checkout and this package loads
5
+ Sierra's own tool code, database JSON, task list and policy straight from it,
6
+ so every result is measured against their ground truth, not a re-creation.
7
+ """
@@ -0,0 +1,38 @@
1
+ """tau-bench semantics for a plain assistant message: it IS the reply to the
2
+ customer (their `respond` action), not the end of the run. The run ends when the
3
+ customer says ###STOP### or the agent transfers to a human."""
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import Any
8
+
9
+ from agentprobe.agents.base import AgentAction
10
+ from agentprobe.agents.target_agent import TargetAgent
11
+ from agentprobe.domains.tau_retail.tools import STOP
12
+
13
+
14
+ class TauChatAgent(TargetAgent):
15
+ def start(self, task: str, tool_schemas: list[dict]) -> None:
16
+ super().start(task, tool_schemas)
17
+ self._pending_text_reply = False
18
+ self._over = False
19
+
20
+ def next_action(self) -> AgentAction:
21
+ if self._over:
22
+ return AgentAction(kind="final_answer", text="")
23
+ action = super().next_action()
24
+ if action.kind == "final_answer" and action.text:
25
+ self._pending_text_reply = True
26
+ return AgentAction(kind="tool_call", tool_name="respond", tool_args={"content": action.text},
27
+ latency_s=action.latency_s, cost_usd=action.cost_usd)
28
+ return action
29
+
30
+ def observe(self, tool_name: str, tool_args: dict, result: Any, ok: bool) -> None:
31
+ if STOP in str(result) or tool_name == "transfer_to_human_agents":
32
+ self._over = True
33
+ if self._pending_text_reply:
34
+ # The agent spoke in plain text: the customer's reply is the next user turn.
35
+ self._pending_text_reply = False
36
+ self._messages.append({"role": "user", "content": str(result)})
37
+ return
38
+ super().observe(tool_name, tool_args, result, ok)
@@ -0,0 +1,110 @@
1
+ """Wires Sierra's retail env into AgentProbe: one Domain + Scenario per run
2
+ (the customer simulator is per-run state, so the Domain closes over it)."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from dataclasses import dataclass
7
+ from typing import Any, Optional
8
+
9
+ from agentprobe.domain import Domain
10
+ from agentprobe.domains.tau_retail import loader, oracle
11
+ from agentprobe.domains.tau_retail.injector_prompt import INJECTION_KINDS, TAU_RETAIL_INJECTOR_SYSTEM_PROMPT
12
+ from agentprobe.domains.tau_retail.tools import TauToolkit, commit_tools, tool_schemas
13
+ from agentprobe.domains.tau_retail.world import TauWorld, TauWorldSpec
14
+ from agentprobe.runner import run_target
15
+ from agentprobe.scenario import CommitPattern, GoalSpec, Scenario
16
+
17
+ BASELINE_STEPS = 2
18
+ """Most tau tasks have a single commit, so the default "wait for the first commit" would
19
+ wake the injector only after the one thing worth breaking has happened."""
20
+
21
+ MAX_STEPS = 30
22
+ """tau-bench's default max_num_steps. Here a step is any tool call, including
23
+ `respond`, which matches their count of agent actions."""
24
+
25
+
26
+ def build_domain(user: Any, tau: Optional[loader.TauRetail] = None, injector_system_prompt: Optional[str] = None) -> Domain:
27
+ tau = tau or loader.load()
28
+ worlds: list[TauWorld] = []
29
+
30
+ def world_factory(spec: TauWorldSpec) -> TauWorld:
31
+ w = TauWorld(tau.fresh_data(), {"policy": tau.wiki})
32
+ worlds.append(w)
33
+ return w
34
+
35
+ domain = Domain(
36
+ name="tau_retail",
37
+ world_state_factory=world_factory,
38
+ toolkit_factory=lambda w: TauToolkit(w, user, tau),
39
+ tool_schemas=tool_schemas(tau),
40
+ commit_tools=commit_tools(tau),
41
+ precondition_checker=lambda tool, args, world: None,
42
+ entity_id_arg="order_id",
43
+ injector_system_prompt=injector_system_prompt,
44
+ injector_entity_types=("order", "user") if injector_system_prompt else None,
45
+ default_hardcoded_tool="cancel_pending_order",
46
+ uninjectable_tools=frozenset({"respond"}),
47
+ )
48
+ object.__setattr__(domain, "_worlds", worlds)
49
+ return domain
50
+
51
+
52
+ def build_scenario(task_index: int, opening_message: str, tau: Optional[loader.TauRetail] = None,
53
+ baseline_until=2) -> Scenario:
54
+ tau = tau or loader.load()
55
+ task = tau.tasks[task_index]
56
+ commits = commit_tools(tau)
57
+ required = [CommitPattern(a.name, dict(a.kwargs)) for a in task.actions if a.name in commits]
58
+ return Scenario(
59
+ id=f"tau-retail-{task_index}",
60
+ seed=task_index,
61
+ world=TauWorldSpec(task_index),
62
+ task=opening_message,
63
+ goal=GoalSpec(required_commits=required),
64
+ baseline_until=baseline_until,
65
+ max_steps=MAX_STEPS,
66
+ max_cost_usd=2.0,
67
+ )
68
+
69
+
70
+ def build_model_injector(model: str = "claude-sonnet-5", tau: Optional[loader.TauRetail] = None):
71
+ """The adaptive Injector, limited to the database-safe kinds."""
72
+ from agentprobe.injector import ModelInjector
73
+
74
+ tau = tau or loader.load()
75
+ return ModelInjector(
76
+ model=model,
77
+ kind_order=list(INJECTION_KINDS),
78
+ system_prompt=TAU_RETAIL_INJECTOR_SYSTEM_PROMPT,
79
+ all_tools=frozenset(s["name"] for s in tool_schemas(tau)),
80
+ commit_tools=commit_tools(tau),
81
+ entity_types=("order", "user"),
82
+ )
83
+
84
+
85
+ def build_target(model: str = "claude-sonnet-5-5", tau: Optional[loader.TauRetail] = None):
86
+ """tau-bench's own agent setup: a Claude tool-calling loop whose system prompt is Sierra's policy."""
87
+ from agentprobe.domains.tau_retail.chat_agent import TauChatAgent
88
+
89
+ tau = tau or loader.load()
90
+ return TauChatAgent(model=model, max_tokens=2048, system_prompt=tau.wiki)
91
+
92
+
93
+ @dataclass
94
+ class TauRun:
95
+ trajectory: Any
96
+ grade: oracle.Grade
97
+ world: TauWorld
98
+ user_cost_usd: float
99
+
100
+
101
+ def run_task(task_index: int, agent, injector, user, tau: Optional[loader.TauRetail] = None,
102
+ baseline_until=2, injector_system_prompt: Optional[str] = None, variant: str = "chaos") -> TauRun:
103
+ """One tau-bench task end to end: customer opens, agent+injector run, Sierra's grader scores."""
104
+ tau = tau or loader.load()
105
+ opening = user.reset()
106
+ domain = build_domain(user, tau, injector_system_prompt)
107
+ scenario = build_scenario(task_index, opening, tau, baseline_until)
108
+ traj = run_target(scenario, agent, injector, variant=variant, domain=domain)
109
+ world = domain._worlds[-1]
110
+ return TauRun(traj, oracle.grade(world, tau.tasks[task_index], tau), world, getattr(user, "cost_usd", 0.0))
@@ -0,0 +1,51 @@
1
+ """Injector prompt for the tau-bench retail domain.
2
+
3
+ Built from the access-control prompt (the generic trigger semantics, kind
4
+ definitions and rules are identical across domains) with its domain-specific
5
+ opening -- the task description and the tool list -- replaced. The experiment
6
+ offers the Injector only the four tool-response kinds (see INJECTION_KINDS),
7
+ which never touch the database, so Sierra's ground-truth final state stays the
8
+ correct answer under injection and the note/entity kinds' vocabulary is unused.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from agentprobe.domains.access_control.injector_prompt import ACCESS_CONTROL_INJECTOR_SYSTEM_PROMPT as _BASE
14
+ from agentprobe.injection import InjectionKind
15
+
16
+ INJECTION_KINDS = [
17
+ InjectionKind.TOOL_ERROR,
18
+ InjectionKind.PHANTOM_SUCCESS,
19
+ InjectionKind.UNVERIFIED_OUTCOME,
20
+ InjectionKind.FALSE_SUCCESS,
21
+ ]
22
+ """Kinds that change only what a tool call RETURNS (or whether it runs), never
23
+ the stored data. Under these, "database equals ground truth" stays the right
24
+ success test."""
25
+
26
+ _OPEN = _BASE.index("You choose a TRIGGER")
27
+ _HEAD = """You are the Injector in an adversarial testing harness. \
28
+ A Target agent is a retail customer-service agent talking to a customer: it can look up \
29
+ users, orders and products and change orders; you watch its trajectory and, once \
30
+ past the baseline phase, may arm exactly one fragility probe per step (or none). \
31
+ An armed injection does NOT fire immediately -- it fires later, whenever its \
32
+ trigger condition actually occurs. You never talk to the Target and never see \
33
+ its internal reasoning -- only its tool calls and their results.
34
+
35
+ The real tools -- use these exact names, never invent one:
36
+ reads: find_user_id_by_email, find_user_id_by_name_zip, get_user_details, get_order_details, \
37
+ get_product_details, list_all_product_types, calculate, think
38
+ talking to the customer: respond
39
+ commits: cancel_pending_order, modify_pending_order_address, modify_pending_order_items, \
40
+ modify_pending_order_payment, modify_user_address, return_delivered_order_items, \
41
+ exchange_delivered_order_items, transfer_to_human_agents
42
+ Commit tools take an "order_id" (modify_user_address takes "user_id"). A trigger naming a tool \
43
+ that doesn't exist can never fire, silently wasting the injection.
44
+
45
+ You may only use the kinds you are offered. They change what a tool call returns, never the \
46
+ stored data: the correct end state of the database is fixed, so an injection can make the task \
47
+ harder but never change what the right answer is.
48
+
49
+ """
50
+
51
+ TAU_RETAIL_INJECTOR_SYSTEM_PROMPT = _HEAD + _BASE[_OPEN:]
@@ -0,0 +1,99 @@
1
+ """Loads Sierra's retail env from a tau-bench checkout without importing the
2
+ `tau_bench` package proper (its __init__ pulls litellm/openai/etc.). Only the
3
+ pure-Python tool, data and task modules are used; `tau_bench.types` is a
4
+ dataclass stand-in with the same fields (their original is pydantic)."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import copy
9
+ import importlib
10
+ import os
11
+ import sys
12
+ import types
13
+ from dataclasses import dataclass, field
14
+ from functools import lru_cache
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+
19
+ @dataclass
20
+ class Action:
21
+ name: str
22
+ kwargs: dict[str, Any]
23
+
24
+
25
+ @dataclass
26
+ class Task:
27
+ user_id: str
28
+ actions: list[Action]
29
+ instruction: str
30
+ outputs: list[str]
31
+ annotator: str = ""
32
+
33
+
34
+ class TauBenchNotFound(RuntimeError):
35
+ pass
36
+
37
+
38
+ def tau_dir() -> Path:
39
+ raw = os.environ.get("TAU_BENCH_DIR")
40
+ if not raw:
41
+ raise TauBenchNotFound("set TAU_BENCH_DIR to a checkout of sierra-research/tau-bench")
42
+ root = Path(raw)
43
+ if not (root / "tau_bench" / "envs" / "retail" / "tools").is_dir():
44
+ raise TauBenchNotFound(f"{root} does not look like a tau-bench checkout")
45
+ return root
46
+
47
+
48
+ def _package(name: str, path: Path) -> types.ModuleType:
49
+ mod = types.ModuleType(name)
50
+ mod.__path__ = [str(path)]
51
+ sys.modules[name] = mod
52
+ return mod
53
+
54
+
55
+ @dataclass
56
+ class TauRetail:
57
+ root: Path
58
+ tools: dict[str, Any]
59
+ tasks: list[Task]
60
+ wiki: str
61
+ tool_schemas: list[dict]
62
+ _load_data: Any = field(repr=False, default=None)
63
+
64
+ def fresh_data(self) -> dict:
65
+ return self._load_data()
66
+
67
+ def commit_tool_names(self) -> frozenset:
68
+ return frozenset(
69
+ {
70
+ "cancel_pending_order", "exchange_delivered_order_items", "modify_pending_order_address",
71
+ "modify_pending_order_items", "modify_pending_order_payment", "modify_user_address",
72
+ "return_delivered_order_items", "transfer_to_human_agents",
73
+ }
74
+ )
75
+
76
+
77
+ @lru_cache(maxsize=1)
78
+ def load(root: str | None = None) -> TauRetail:
79
+ base = Path(root) if root else tau_dir()
80
+ pkg = base / "tau_bench"
81
+ for name in [n for n in sys.modules if n == "tau_bench" or n.startswith("tau_bench.")]:
82
+ del sys.modules[name]
83
+ _package("tau_bench", pkg)
84
+ _package("tau_bench.envs", pkg / "envs")
85
+ _package("tau_bench.envs.retail", pkg / "envs" / "retail")
86
+ types_mod = types.ModuleType("tau_bench.types")
87
+ types_mod.Task, types_mod.Action = Task, Action
88
+ sys.modules["tau_bench.types"] = types_mod
89
+ importlib.invalidate_caches()
90
+ tools_mod = importlib.import_module("tau_bench.envs.retail.tools")
91
+ data_mod = importlib.import_module("tau_bench.envs.retail.data")
92
+ tasks_mod = importlib.import_module("tau_bench.envs.retail.tasks_test")
93
+ wiki = (pkg / "envs" / "retail" / "wiki.md").read_text()
94
+ tools = {t.get_info()["function"]["name"]: t for t in tools_mod.ALL_TOOLS}
95
+ schemas = []
96
+ for t in tools_mod.ALL_TOOLS:
97
+ fn = t.get_info()["function"]
98
+ schemas.append({"name": fn["name"], "description": fn["description"], "input_schema": copy.deepcopy(fn["parameters"])})
99
+ return TauRetail(base, tools, list(tasks_mod.TASKS_TEST), wiki, schemas, data_mod.load_data)
@@ -0,0 +1,85 @@
1
+ """Tool-calling agent for any OpenAI-compatible chat API (OpenRouter, Moonshot, ...),
2
+ with tau-bench chat semantics: a plain message is a reply to the customer."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ import os
8
+ import time
9
+ import urllib.request
10
+ from typing import Any, Callable, Optional
11
+
12
+ from agentprobe.agents.base import AgentAction
13
+ from agentprobe.agents.base import Agent
14
+ from agentprobe.domains.tau_retail.tools import STOP
15
+
16
+ OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions"
17
+
18
+
19
+ class OpenAICompatChatAgent(Agent):
20
+ def __init__(self, model: str, system_prompt: str, api_key: Optional[str] = None, url: str = OPENROUTER_URL,
21
+ price_per_mtok: tuple[float, float] = (0.0, 0.0), post: Optional[Callable[[dict], dict]] = None,
22
+ max_tokens: int = 2048):
23
+ self.model, self.system_prompt, self.url = model, system_prompt, url
24
+ self.api_key = api_key or os.environ.get("OPENROUTER_API_KEY", "")
25
+ self.price = price_per_mtok
26
+ self.max_tokens = max_tokens
27
+ self._post = post or self._http_post
28
+ self.messages: list[dict] = []
29
+ self.tools: list[dict] = []
30
+ self._pending_call_id: Optional[str] = None
31
+ self._over = False
32
+
33
+ def _http_post(self, body: dict) -> dict:
34
+ req = urllib.request.Request(self.url, data=json.dumps(body).encode(), method="POST",
35
+ headers={"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"})
36
+ with urllib.request.urlopen(req, timeout=180) as r:
37
+ return json.load(r)
38
+
39
+ def start(self, task: str, tool_schemas: list[dict]) -> None:
40
+ self.tools = [{"type": "function", "function": {"name": s["name"], "description": s["description"],
41
+ "parameters": s["input_schema"]}} for s in tool_schemas]
42
+ self.messages = [{"role": "system", "content": self.system_prompt}, {"role": "user", "content": task}]
43
+ self._pending_call_id, self._over = None, False
44
+
45
+ def next_action(self) -> AgentAction:
46
+ if self._over:
47
+ return AgentAction(kind="final_answer", text="")
48
+ t0 = time.monotonic()
49
+ data = self._post({"model": self.model, "messages": self.messages, "tools": self.tools,
50
+ "parallel_tool_calls": False, "temperature": 0, "max_tokens": self.max_tokens})
51
+ msg = data["choices"][0]["message"]
52
+ usage = data.get("usage") or {}
53
+ cost = usage.get("cost")
54
+ if cost is None:
55
+ cost = (usage.get("prompt_tokens", 0) * self.price[0] + usage.get("completion_tokens", 0) * self.price[1]) / 1e6
56
+ lat = time.monotonic() - t0
57
+ calls = msg.get("tool_calls") or []
58
+ if calls:
59
+ call = calls[0]
60
+ self.messages.append({"role": "assistant", "content": msg.get("content"), "tool_calls": [call]})
61
+ self._pending_call_id = call["id"]
62
+ try:
63
+ args = json.loads(call["function"].get("arguments") or "{}")
64
+ except json.JSONDecodeError:
65
+ args = {}
66
+ return AgentAction(kind="tool_call", tool_name=call["function"]["name"], tool_args=args,
67
+ latency_s=lat, cost_usd=cost)
68
+ text = msg.get("content") or ""
69
+ self.messages.append({"role": "assistant", "content": text})
70
+ self._pending_call_id = None
71
+ # tau semantics: plain text is a reply to the customer.
72
+ return AgentAction(kind="tool_call", tool_name="respond", tool_args={"content": text}, latency_s=lat, cost_usd=cost)
73
+
74
+ def observe(self, tool_name: str, tool_args: dict, result: Any, ok: bool) -> None:
75
+ text = str(result) if ok else f"Error: {result.get('error', result)}"
76
+ if STOP in text or tool_name == "transfer_to_human_agents":
77
+ self._over = True
78
+ if self._pending_call_id is None: # plain-text reply -> customer's answer is a user turn
79
+ self.messages.append({"role": "user", "content": text})
80
+ else:
81
+ self.messages.append({"role": "tool", "tool_call_id": self._pending_call_id, "content": text})
82
+ self._pending_call_id = None
83
+
84
+ def receive_message(self, text: str) -> None:
85
+ self.messages.append({"role": "user", "content": text})
@@ -0,0 +1,75 @@
1
+ """Ground-truth grading, a port of tau-bench Env.calculate_reward: replay the
2
+ task's annotated actions on a fresh DB, compare DB hashes, then check required
3
+ output strings. Plus a row-level diff so a failure says WHAT differed.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass, field
9
+
10
+ from agentprobe.domains.tau_retail import loader
11
+ from agentprobe.domains.tau_retail.world import TauWorld, data_hash
12
+
13
+ _TERMINATE = {"transfer_to_human_agents"}
14
+
15
+
16
+ @dataclass
17
+ class Grade:
18
+ reward: float
19
+ db_match: bool
20
+ outputs_ok: bool
21
+ missing_outputs: list[str] = field(default_factory=list)
22
+ changed_rows: list[str] = field(default_factory=list)
23
+ """Rows (table/id) whose final state differs from ground truth, either way."""
24
+ unexpected_writes: list[str] = field(default_factory=list)
25
+ """Rows the agent changed that ground truth leaves untouched."""
26
+ missing_writes: list[str] = field(default_factory=list)
27
+ """Rows ground truth changes that the agent left untouched."""
28
+
29
+ def as_dict(self) -> dict:
30
+ return dict(self.__dict__)
31
+
32
+
33
+ def ground_truth_data(task: loader.Task, tau: loader.TauRetail) -> dict:
34
+ data = tau.fresh_data()
35
+ for action in task.actions:
36
+ if action.name == "respond" or action.name in _TERMINATE:
37
+ continue
38
+ try:
39
+ tau.tools[action.name].invoke(data=data, **action.kwargs)
40
+ except Exception:
41
+ pass # tau swallows these into an "Error:" observation too
42
+ return data
43
+
44
+
45
+ def _row_diff(a: dict, b: dict) -> set[str]:
46
+ out = set()
47
+ for table in set(a) | set(b):
48
+ ra, rb = a.get(table, {}), b.get(table, {})
49
+ for k in set(ra) | set(rb):
50
+ if ra.get(k) != rb.get(k):
51
+ out.add(f"{table}/{k}")
52
+ return out
53
+
54
+
55
+ def grade(world: TauWorld, task: loader.Task, tau: loader.TauRetail) -> Grade:
56
+ gt = ground_truth_data(task, tau)
57
+ db_match = data_hash(world.data) == data_hash(gt)
58
+ outputs_ok, missing = True, []
59
+ said = [m.lower().replace(",", "") for m in world.agent_messages]
60
+ for out in task.outputs:
61
+ if not any(out.lower() in m for m in said):
62
+ outputs_ok = False
63
+ missing.append(out)
64
+ changed = _row_diff(world.data, gt)
65
+ original = tau.fresh_data()
66
+ by_agent, by_gt = _row_diff(world.data, original), _row_diff(gt, original)
67
+ return Grade(
68
+ reward=1.0 if (db_match and outputs_ok) else 0.0,
69
+ db_match=db_match,
70
+ outputs_ok=outputs_ok,
71
+ missing_outputs=missing,
72
+ changed_rows=sorted(changed),
73
+ unexpected_writes=sorted(by_agent - by_gt),
74
+ missing_writes=sorted(by_gt - by_agent),
75
+ )