agentprobe-testing 1.4.0__tar.gz → 1.5.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 (124) hide show
  1. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/LICENSE +1 -1
  2. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/PKG-INFO +2 -2
  3. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/README.md +4 -4
  4. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/__init__.py +7 -1
  5. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/domain.py +6 -0
  6. agentprobe_testing-1.5.0/agentprobe/domains/tau_retail/__init__.py +7 -0
  7. agentprobe_testing-1.5.0/agentprobe/domains/tau_retail/chat_agent.py +38 -0
  8. agentprobe_testing-1.5.0/agentprobe/domains/tau_retail/domain.py +110 -0
  9. agentprobe_testing-1.5.0/agentprobe/domains/tau_retail/injector_prompt.py +51 -0
  10. agentprobe_testing-1.5.0/agentprobe/domains/tau_retail/loader.py +99 -0
  11. agentprobe_testing-1.5.0/agentprobe/domains/tau_retail/openai_compat_agent.py +85 -0
  12. agentprobe_testing-1.5.0/agentprobe/domains/tau_retail/oracle.py +75 -0
  13. agentprobe_testing-1.5.0/agentprobe/domains/tau_retail/tools.py +88 -0
  14. agentprobe_testing-1.5.0/agentprobe/domains/tau_retail/user_sim.py +79 -0
  15. agentprobe_testing-1.5.0/agentprobe/domains/tau_retail/world.py +92 -0
  16. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/injector.py +6 -0
  17. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/quickstart.py +101 -7
  18. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/registry.py +1 -1
  19. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/runner.py +33 -9
  20. agentprobe_testing-1.5.0/agentprobe/savings.py +208 -0
  21. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe_testing.egg-info/PKG-INFO +2 -2
  22. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe_testing.egg-info/SOURCES.txt +14 -0
  23. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/pyproject.toml +1 -1
  24. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/tests/test_registry.py +1 -1
  25. agentprobe_testing-1.5.0/tests/test_runner_timeouts.py +67 -0
  26. agentprobe_testing-1.5.0/tests/test_savings.py +170 -0
  27. agentprobe_testing-1.5.0/tests/test_tau_retail.py +157 -0
  28. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/agents/__init__.py +0 -0
  29. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/agents/base.py +0 -0
  30. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/agents/browser_agent.py +0 -0
  31. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/agents/rule_based.py +0 -0
  32. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/agents/scripted.py +0 -0
  33. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/agents/target_agent.py +0 -0
  34. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/agreement.py +0 -0
  35. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/classifier.py +0 -0
  36. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/cli.py +0 -0
  37. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/diff.py +0 -0
  38. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/domains/__init__.py +0 -0
  39. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/domains/access_control/__init__.py +0 -0
  40. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/domains/access_control/agent.py +0 -0
  41. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/domains/access_control/clean.py +0 -0
  42. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/domains/access_control/complex_agent.py +0 -0
  43. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/domains/access_control/decoy.py +0 -0
  44. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/domains/access_control/domain.py +0 -0
  45. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/domains/access_control/entities.py +0 -0
  46. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/domains/access_control/injector_prompt.py +0 -0
  47. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/domains/access_control/rule_based_agent.py +0 -0
  48. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/domains/access_control/scenarios.py +0 -0
  49. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/domains/access_control/split.py +0 -0
  50. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/domains/access_control/tools.py +0 -0
  51. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/domains/access_control/trap.py +0 -0
  52. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/domains/web_browsing/__init__.py +0 -0
  53. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/domains/web_browsing/agent.py +0 -0
  54. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/domains/web_browsing/clean.py +0 -0
  55. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/domains/web_browsing/domain.py +0 -0
  56. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/domains/web_browsing/entities.py +0 -0
  57. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/domains/web_browsing/injector_prompt.py +0 -0
  58. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/domains/web_browsing/rule_based_agent.py +0 -0
  59. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/domains/web_browsing/scenarios.py +0 -0
  60. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/domains/web_browsing/tools.py +0 -0
  61. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/domains/web_browsing/trap.py +0 -0
  62. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/feedback.py +0 -0
  63. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/generic_world.py +0 -0
  64. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/injection.py +0 -0
  65. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/llm.py +0 -0
  66. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/playbook.py +0 -0
  67. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/priority.py +0 -0
  68. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/reachability.py +0 -0
  69. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/report.py +0 -0
  70. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/scenario.py +0 -0
  71. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/scenarios/__init__.py +0 -0
  72. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/scenarios/clean.py +0 -0
  73. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/scenarios/decoy.py +0 -0
  74. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/scenarios/registry.py +0 -0
  75. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/scenarios/split.py +0 -0
  76. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/scenarios/trap.py +0 -0
  77. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/summarize.py +0 -0
  78. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/termui.py +0 -0
  79. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/tools.py +0 -0
  80. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/trajectory.py +0 -0
  81. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/triage.py +0 -0
  82. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/validate_scenarios.py +0 -0
  83. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe/world.py +0 -0
  84. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe_testing.egg-info/dependency_links.txt +0 -0
  85. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe_testing.egg-info/entry_points.txt +0 -0
  86. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe_testing.egg-info/requires.txt +0 -0
  87. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/agentprobe_testing.egg-info/top_level.txt +0 -0
  88. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/setup.cfg +0 -0
  89. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/tests/test_access_control_agent.py +0 -0
  90. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/tests/test_access_control_domain.py +0 -0
  91. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/tests/test_access_control_rule_based_agent.py +0 -0
  92. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/tests/test_access_control_scenarios.py +0 -0
  93. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/tests/test_access_control_tools.py +0 -0
  94. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/tests/test_agreement.py +0 -0
  95. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/tests/test_browser_agent.py +0 -0
  96. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/tests/test_classifier.py +0 -0
  97. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/tests/test_cli.py +0 -0
  98. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/tests/test_complex_access_control_agent.py +0 -0
  99. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/tests/test_diff.py +0 -0
  100. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/tests/test_domain.py +0 -0
  101. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/tests/test_failures.py +0 -0
  102. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/tests/test_feedback.py +0 -0
  103. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/tests/test_generic_world.py +0 -0
  104. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/tests/test_injection.py +0 -0
  105. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/tests/test_injection_mode.py +0 -0
  106. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/tests/test_injector.py +0 -0
  107. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/tests/test_llm.py +0 -0
  108. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/tests/test_package_api.py +0 -0
  109. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/tests/test_playbook.py +0 -0
  110. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/tests/test_priority.py +0 -0
  111. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/tests/test_quickstart.py +0 -0
  112. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/tests/test_reachability.py +0 -0
  113. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/tests/test_report.py +0 -0
  114. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/tests/test_rule_based_agent.py +0 -0
  115. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/tests/test_runner.py +0 -0
  116. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/tests/test_scenarios.py +0 -0
  117. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/tests/test_target_agent.py +0 -0
  118. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/tests/test_termui.py +0 -0
  119. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/tests/test_tools.py +0 -0
  120. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/tests/test_trajectory.py +0 -0
  121. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/tests/test_triage.py +0 -0
  122. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/tests/test_validate_scenarios.py +0 -0
  123. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/tests/test_web_browsing_domain.py +0 -0
  124. {agentprobe_testing-1.4.0 → agentprobe_testing-1.5.0}/tests/test_world.py +0 -0
@@ -16,7 +16,7 @@ Additional Use Grant: You may use, copy, modify, and make production use of
16
16
  limitation, hosted domain generation, hosted
17
17
  dashboards, or hosted test-run storage substantially
18
18
  similar to those offered at
19
- https://agentprobe-api.agentprobe.workers.dev).
19
+ https://sampact.com).
20
20
  Change Date: 2030-01-01
21
21
  Change License: Apache License, Version 2.0
22
22
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentprobe-testing
3
- Version: 1.4.0
3
+ Version: 1.5.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
 
@@ -20,7 +20,7 @@ License: Business Source License 1.1
20
20
  limitation, hosted domain generation, hosted
21
21
  dashboards, or hosted test-run storage substantially
22
22
  similar to those offered at
23
- https://agentprobe-api.agentprobe.workers.dev).
23
+ https://sampact.com).
24
24
  Change Date: 2030-01-01
25
25
  Change License: Apache License, Version 2.0
26
26
 
@@ -183,10 +183,10 @@ own world — the engine underneath is already generic.
183
183
 
184
184
  Writing `agentprobe/domain.py`'s `Domain` bundle by hand (entities, tools,
185
185
  business rules) is real, unavoidable work — see "Domains" above. If you'd
186
- rather not write it yourself, [agentprobe-api.agentprobe.workers.dev](https://agentprobe-api.agentprobe.workers.dev)
186
+ rather not write it yourself, [sampact.com](https://sampact.com)
187
187
  can generate one from your existing tool schemas plus a plain-English
188
188
  description of your business rules, and hand you back a key — either via
189
- [/generate](https://agentprobe-api.agentprobe.workers.dev/generate) on the
189
+ [/generate](https://sampact.com/generate) on the
190
190
  site (sign in, fill in a form, get the key and the generated source back
191
191
  right on the page — no `curl` needed) or the raw API:
192
192
 
@@ -215,7 +215,7 @@ rule as never installing a package from someone you don't trust.
215
215
 
216
216
  ## The website: sign in and run a real test in your browser
217
217
 
218
- [agentprobe-api.agentprobe.workers.dev](https://agentprobe-api.agentprobe.workers.dev)
218
+ [sampact.com](https://sampact.com)
219
219
  is the real site, not just the API — sign in with GitHub and you get:
220
220
 
221
221
  - **`/run`** — one flow, describing to running: describe your business in
@@ -246,7 +246,7 @@ Attribute a generated domain to your account by passing the key as a
246
246
  bearer token:
247
247
 
248
248
  ```bash
249
- curl -X POST https://agentprobe-api.agentprobe.workers.dev/v1/generate \
249
+ curl -X POST https://sampact.com/v1/generate \
250
250
  -H "Authorization: Bearer ap_live_..." \
251
251
  -H "content-type: application/json" \
252
252
  -d '{"name": "...", "tool_schemas": [...], "business_description": "..."}'
@@ -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.0"
64
+ __version__ = "1.5.0"
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",
@@ -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
@@ -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
+ )
@@ -0,0 +1,88 @@
1
+ """TauToolkit: Sierra's own tool code run against a TauWorld.
2
+
3
+ Differences from tau-bench's agent loop, all deliberate and listed in the
4
+ README: (1) talking to the customer is a `respond` tool (tau uses a plain
5
+ assistant message) so AgentProbe's tool-call engine can drive the whole
6
+ conversation; (2) a tool result that starts with "Error" is raised as ToolError
7
+ so the engine records the call as failed, exactly like tau's own observation.
8
+ The text the agent sees is unchanged.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ from typing import Any, Optional
15
+
16
+ from agentprobe.domains.tau_retail import loader
17
+ from agentprobe.domains.tau_retail.world import TauWorld
18
+ from agentprobe.tools import ToolError
19
+
20
+ STOP = "###STOP###"
21
+ RESPOND_SCHEMA = {
22
+ "name": "respond",
23
+ "description": "Send a message to the customer and wait for their reply. This is the only way to talk to the customer.",
24
+ "input_schema": {
25
+ "type": "object",
26
+ "properties": {"content": {"type": "string", "description": "The message to the customer."}},
27
+ "required": ["content"],
28
+ },
29
+ }
30
+ _TERMINATE = {"transfer_to_human_agents"}
31
+ _NOTE_KEYS = {"get_order_details": ("order", "order_id"), "get_user_details": ("user", "user_id")}
32
+
33
+
34
+ def tool_schemas(tau: Optional[loader.TauRetail] = None) -> list[dict]:
35
+ tau = tau or loader.load()
36
+ return [RESPOND_SCHEMA] + tau.tool_schemas
37
+
38
+
39
+ class TauToolkit:
40
+ def __init__(self, world: TauWorld, user: Any, tau: Optional[loader.TauRetail] = None):
41
+ self.world = world
42
+ self.user = user
43
+ self.tau = tau or loader.load()
44
+
45
+ def canonicalize_args(self, tool_name: str, args: dict) -> dict:
46
+ return args
47
+
48
+ def call(self, name: str, args: dict) -> Any:
49
+ if name == "respond":
50
+ return self._respond(args)
51
+ tool = self.tau.tools.get(name)
52
+ if tool is None:
53
+ raise ToolError(f"Unknown action {name}")
54
+ try:
55
+ out = tool.invoke(data=self.world.data, **args)
56
+ except Exception as e: # tau's Env.step turns any exception into this string
57
+ out = f"Error: {e}"
58
+ if name in ("find_user_id_by_email", "find_user_id_by_name_zip") and isinstance(out, str) and out in self.world.data["users"]:
59
+ self.world.focus_user = out
60
+ if name in _TERMINATE:
61
+ self.world.conversation_over = True
62
+ if isinstance(out, str) and out.startswith("Error"):
63
+ raise ToolError(out[len("Error"):].lstrip(": ").strip() or "error")
64
+ return self._with_notes(name, args, out)
65
+
66
+ def _respond(self, args: dict) -> str:
67
+ content = args.get("content")
68
+ if not isinstance(content, str):
69
+ raise ToolError("respond requires a string `content`")
70
+ self.world.agent_messages.append(content)
71
+ reply = self.user.step(content)
72
+ if STOP in reply:
73
+ self.world.conversation_over = True
74
+ return reply
75
+
76
+ def _with_notes(self, name: str, args: dict, out: Any) -> Any:
77
+ spec = _NOTE_KEYS.get(name)
78
+ if not spec or not isinstance(out, str):
79
+ return out
80
+ etype, arg = spec
81
+ notes = self.world.notes.get((etype, str(args.get(arg))))
82
+ if not notes:
83
+ return out
84
+ return out + "".join(f"\n[note] {n}" for n in notes)
85
+
86
+
87
+ def commit_tools(tau: Optional[loader.TauRetail] = None) -> frozenset:
88
+ return (tau or loader.load()).commit_tool_names()