agenttic 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (307) hide show
  1. agenttic/__init__.py +128 -0
  2. agenttic/__main__.py +6 -0
  3. agenttic/_decorator.py +133 -0
  4. agenttic/_detect.py +325 -0
  5. agenttic/_env.py +76 -0
  6. agenttic/ab.py +227 -0
  7. agenttic/adapters/__init__.py +0 -0
  8. agenttic/adapters/anthropic_simple.py +240 -0
  9. agenttic/adapters/base.py +40 -0
  10. agenttic/adapters/blackbox_http.py +175 -0
  11. agenttic/adapters/managed_agent.py +267 -0
  12. agenttic/airgap.py +184 -0
  13. agenttic/assistant/__init__.py +31 -0
  14. agenttic/assistant/adapter.py +145 -0
  15. agenttic/assistant/agent.py +253 -0
  16. agenttic/assistant/guard.py +173 -0
  17. agenttic/assistant/posture.py +92 -0
  18. agenttic/assistant/store.py +67 -0
  19. agenttic/assistant/tools.py +245 -0
  20. agenttic/billing/__init__.py +29 -0
  21. agenttic/billing/credits_provider.py +111 -0
  22. agenttic/billing/gateways/__init__.py +8 -0
  23. agenttic/billing/gateways/paypal_gateway.py +136 -0
  24. agenttic/billing/gateways/stripe_gateway.py +99 -0
  25. agenttic/billing/invoices.py +125 -0
  26. agenttic/billing/models.py +119 -0
  27. agenttic/billing/plans.py +132 -0
  28. agenttic/billing/service.py +152 -0
  29. agenttic/billing/store.py +295 -0
  30. agenttic/billing/webhooks.py +251 -0
  31. agenttic/budget.py +112 -0
  32. agenttic/camp/__init__.py +66 -0
  33. agenttic/camp/adapter_agent.py +72 -0
  34. agenttic/camp/agent.py +104 -0
  35. agenttic/camp/environment.py +110 -0
  36. agenttic/camp/gate.py +80 -0
  37. agenttic/camp/holdout.py +69 -0
  38. agenttic/camp/improve.py +283 -0
  39. agenttic/camp/service.py +239 -0
  40. agenttic/camp/store.py +319 -0
  41. agenttic/camp/task.py +59 -0
  42. agenttic/camp/tasks/__init__.py +3 -0
  43. agenttic/camp/tasks/support_triage.py +130 -0
  44. agenttic/camp/trace.py +135 -0
  45. agenttic/camp/trainer.py +122 -0
  46. agenttic/cards/__init__.py +6 -0
  47. agenttic/cards/agency.py +79 -0
  48. agenttic/cards/autofill.py +161 -0
  49. agenttic/cards/autonomy.py +102 -0
  50. agenttic/cards/fields.py +118 -0
  51. agenttic/certification/__init__.py +20 -0
  52. agenttic/certification/__main__.py +16 -0
  53. agenttic/certification/certify.py +328 -0
  54. agenttic/certification/coverage.py +62 -0
  55. agenttic/certification/domains.py +158 -0
  56. agenttic/certification/dossier.py +186 -0
  57. agenttic/certification/elicitation.py +274 -0
  58. agenttic/certification/hashing.py +46 -0
  59. agenttic/certification/mock_provider.py +50 -0
  60. agenttic/certification/profiles.py +192 -0
  61. agenttic/certification/safety_cert.py +694 -0
  62. agenttic/certification/staleness.py +106 -0
  63. agenttic/certification/swe_pack.py +141 -0
  64. agenttic/certification/tiers.py +200 -0
  65. agenttic/cli.py +1488 -0
  66. agenttic/config.py +55 -0
  67. agenttic/connect.py +296 -0
  68. agenttic/copilot/__init__.py +33 -0
  69. agenttic/copilot/agent.py +311 -0
  70. agenttic/copilot/credits.py +201 -0
  71. agenttic/copilot/errors.py +163 -0
  72. agenttic/copilot/knowledge.md +232 -0
  73. agenttic/copilot/service.py +212 -0
  74. agenttic/copilot/skill.py +165 -0
  75. agenttic/copilot/store.py +56 -0
  76. agenttic/copilot/tools.py +408 -0
  77. agenttic/cost.py +198 -0
  78. agenttic/documents.py +92 -0
  79. agenttic/enforce/__init__.py +7 -0
  80. agenttic/enforce/adapter_guard.py +105 -0
  81. agenttic/enforce/approvals.py +113 -0
  82. agenttic/enforce/async_judge.py +141 -0
  83. agenttic/enforce/canaries.py +169 -0
  84. agenttic/enforce/compiler.py +359 -0
  85. agenttic/enforce/dashboard.py +83 -0
  86. agenttic/enforce/export.py +56 -0
  87. agenttic/enforce/feedback.py +44 -0
  88. agenttic/enforce/gateway.py +309 -0
  89. agenttic/enforce/interactive_oversight.py +414 -0
  90. agenttic/enforce/lanes.py +249 -0
  91. agenttic/enforce/ramp.py +239 -0
  92. agenttic/enforce/self_security.py +65 -0
  93. agenttic/feeds/__init__.py +5 -0
  94. agenttic/feeds/risk_api.py +100 -0
  95. agenttic/feeds/webhooks.py +84 -0
  96. agenttic/generator/__init__.py +0 -0
  97. agenttic/generator/pipeline.py +288 -0
  98. agenttic/hardening.py +670 -0
  99. agenttic/harness/__init__.py +0 -0
  100. agenttic/harness/runner.py +171 -0
  101. agenttic/ingest/__init__.py +39 -0
  102. agenttic/ingest/doctor.py +127 -0
  103. agenttic/ingest/emit.py +199 -0
  104. agenttic/ingest/mapping.py +340 -0
  105. agenttic/ingest/otel.py +207 -0
  106. agenttic/interop/__init__.py +15 -0
  107. agenttic/interop/agent_index.py +124 -0
  108. agenttic/interop/inspect_log.py +559 -0
  109. agenttic/issues.py +401 -0
  110. agenttic/leaderboard.py +109 -0
  111. agenttic/live/__init__.py +0 -0
  112. agenttic/live/incidents.py +209 -0
  113. agenttic/live/monitor.py +112 -0
  114. agenttic/metrics/ATTRIBUTION.md +52 -0
  115. agenttic/metrics/__init__.py +17 -0
  116. agenttic/metrics/answer_match.py +261 -0
  117. agenttic/metrics/bfcl_ast_official.py +232 -0
  118. agenttic/metrics/bfcl_reproduce.py +529 -0
  119. agenttic/metrics/calibration.py +39 -0
  120. agenttic/metrics/canonical_checks.py +352 -0
  121. agenttic/metrics/catalog.py +394 -0
  122. agenttic/metrics/datasets/__init__.py +44 -0
  123. agenttic/metrics/datasets/agentdojo.py +276 -0
  124. agenttic/metrics/datasets/agentdojo_data/ATTRIBUTION.md +81 -0
  125. agenttic/metrics/datasets/agentdojo_data/LICENSE +21 -0
  126. agenttic/metrics/datasets/agentdojo_data/agentdojo.sample.json +139 -0
  127. agenttic/metrics/datasets/agentharm.py +92 -0
  128. agenttic/metrics/datasets/agentharm_data/ATTRIBUTION.md +60 -0
  129. agenttic/metrics/datasets/agentharm_data/LICENSE +24 -0
  130. agenttic/metrics/datasets/agentharm_data/agentharm_harmful.sample.json +71 -0
  131. agenttic/metrics/datasets/assistantbench.py +110 -0
  132. agenttic/metrics/datasets/assistantbench_data/ATTRIBUTION.md +62 -0
  133. agenttic/metrics/datasets/assistantbench_data/LICENSE +202 -0
  134. agenttic/metrics/datasets/assistantbench_data/assistant_bench_v1.0_dev.sample.jsonl +16 -0
  135. agenttic/metrics/datasets/base.py +90 -0
  136. agenttic/metrics/datasets/bfcl.py +240 -0
  137. agenttic/metrics/datasets/bfcl_data/ATTRIBUTION.md +51 -0
  138. agenttic/metrics/datasets/bfcl_data/BFCL_v3_live_multiple.sample.answers.json +292 -0
  139. agenttic/metrics/datasets/bfcl_data/BFCL_v3_live_multiple.sample.json +2178 -0
  140. agenttic/metrics/datasets/bfcl_data/BFCL_v3_live_simple.sample.answers.json +293 -0
  141. agenttic/metrics/datasets/bfcl_data/BFCL_v3_live_simple.sample.json +690 -0
  142. agenttic/metrics/datasets/bfcl_data/BFCL_v3_multiple.sample.answers.json +445 -0
  143. agenttic/metrics/datasets/bfcl_data/BFCL_v3_multiple.sample.json +1551 -0
  144. agenttic/metrics/datasets/bfcl_data/BFCL_v3_parallel.sample.answers.json +755 -0
  145. agenttic/metrics/datasets/bfcl_data/BFCL_v3_parallel.sample.json +759 -0
  146. agenttic/metrics/datasets/bfcl_data/BFCL_v3_parallel_multiple.sample.answers.json +621 -0
  147. agenttic/metrics/datasets/bfcl_data/BFCL_v3_parallel_multiple.sample.json +1355 -0
  148. agenttic/metrics/datasets/bfcl_data/BFCL_v3_simple.sample.answers.json +426 -0
  149. agenttic/metrics/datasets/bfcl_data/BFCL_v3_simple.sample.json +892 -0
  150. agenttic/metrics/datasets/bfcl_data/LICENSE +202 -0
  151. agenttic/metrics/datasets/gaia.py +131 -0
  152. agenttic/metrics/datasets/gaia_data/ATTRIBUTION.md +76 -0
  153. agenttic/metrics/datasets/gaia_data/LICENSE.txt +32 -0
  154. agenttic/metrics/datasets/gaia_data/gaia_validation.sample.jsonl +6 -0
  155. agenttic/metrics/datasets/injecagent.py +124 -0
  156. agenttic/metrics/datasets/injecagent_data/ATTRIBUTION.md +72 -0
  157. agenttic/metrics/datasets/injecagent_data/LICENSE +21 -0
  158. agenttic/metrics/datasets/injecagent_data/injecagent.sample.json +222 -0
  159. agenttic/metrics/datasets/swebench.py +203 -0
  160. agenttic/metrics/datasets/swebench_data/ATTRIBUTION.md +81 -0
  161. agenttic/metrics/datasets/swebench_data/LICENSE +21 -0
  162. agenttic/metrics/datasets/swebench_data/swebench_verified.sample.jsonl +7 -0
  163. agenttic/metrics/datasets/tau_bench.py +164 -0
  164. agenttic/metrics/datasets/tau_bench_data/ATTRIBUTION.md +61 -0
  165. agenttic/metrics/datasets/tau_bench_data/LICENSE +21 -0
  166. agenttic/metrics/datasets/tau_bench_data/tau_bench.sample.json +1111 -0
  167. agenttic/metrics/faithfulness.py +242 -0
  168. agenttic/metrics/index.py +42 -0
  169. agenttic/metrics/injection_detect.py +270 -0
  170. agenttic/metrics/judge_quality.py +489 -0
  171. agenttic/metrics/redteam.py +425 -0
  172. agenttic/metrics/redteam_injection_responses.jsonl +24 -0
  173. agenttic/metrics/reliability.py +30 -0
  174. agenttic/metrics/reproduction.py +225 -0
  175. agenttic/metrics/runner.py +169 -0
  176. agenttic/metrics/safety_battery.py +215 -0
  177. agenttic/metrics/safety_catalog.py +114 -0
  178. agenttic/metrics/safety_checks.py +590 -0
  179. agenttic/metrics/safety_suite.py +138 -0
  180. agenttic/metrics/standard_suites.py +257 -0
  181. agenttic/metrics/structured_ir.py +824 -0
  182. agenttic/metrics/swe_catalog.py +107 -0
  183. agenttic/metrics/swe_checks.py +296 -0
  184. agenttic/metrics/swe_suites.py +563 -0
  185. agenttic/metrics/swebench_resolve.py +179 -0
  186. agenttic/metrics/text_overlap.py +573 -0
  187. agenttic/migrations.py +321 -0
  188. agenttic/ops.py +466 -0
  189. agenttic/optimizer.py +561 -0
  190. agenttic/oversight/__init__.py +7 -0
  191. agenttic/oversight/analytics.py +105 -0
  192. agenttic/passport/__init__.py +3 -0
  193. agenttic/passport/issuer.py +136 -0
  194. agenttic/passport/keys.py +203 -0
  195. agenttic/passport/receipts.py +174 -0
  196. agenttic/pricing.py +25 -0
  197. agenttic/py.typed +0 -0
  198. agenttic/registry/__init__.py +0 -0
  199. agenttic/registry/sqlite_store.py +1838 -0
  200. agenttic/registry/store.py +16 -0
  201. agenttic/release/__init__.py +2 -0
  202. agenttic/release/ladder.py +101 -0
  203. agenttic/release/promotion.py +201 -0
  204. agenttic/release/scaffold.py +68 -0
  205. agenttic/release/scaffold_assets/QUICKSTART.md +55 -0
  206. agenttic/release/scaffold_assets/agent_sample.py +50 -0
  207. agenttic/release/scaffold_assets/config.yaml +283 -0
  208. agenttic/release/scaffold_assets/kb.json +4 -0
  209. agenttic/reporting/__init__.py +0 -0
  210. agenttic/reporting/ab_report.py +240 -0
  211. agenttic/reporting/dossier_report.py +172 -0
  212. agenttic/reporting/pdf_report.py +213 -0
  213. agenttic/reporting/scorecard_report.py +155 -0
  214. agenttic/result_cache.py +59 -0
  215. agenttic/retry.py +92 -0
  216. agenttic/scan.py +131 -0
  217. agenttic/schema/__init__.py +10 -0
  218. agenttic/schema/ab.py +136 -0
  219. agenttic/schema/agent.py +62 -0
  220. agenttic/schema/agent_card.py +129 -0
  221. agenttic/schema/certification.py +189 -0
  222. agenttic/schema/enforcement.py +160 -0
  223. agenttic/schema/incident.py +118 -0
  224. agenttic/schema/optimization.py +148 -0
  225. agenttic/schema/passport.py +115 -0
  226. agenttic/schema/release.py +77 -0
  227. agenttic/schema/rubric.py +85 -0
  228. agenttic/schema/scorecard.py +157 -0
  229. agenttic/schema/testcase.py +39 -0
  230. agenttic/schema/trace.py +93 -0
  231. agenttic/scoring/__init__.py +0 -0
  232. agenttic/scoring/calibration.py +97 -0
  233. agenttic/scoring/calibration_corpus.jsonl +52 -0
  234. agenttic/scoring/checks.py +249 -0
  235. agenttic/scoring/corpus.py +224 -0
  236. agenttic/scoring/engine.py +221 -0
  237. agenttic/scoring/fi_eval.py +164 -0
  238. agenttic/scoring/judge.py +261 -0
  239. agenttic/scoring/judge_calibration.py +265 -0
  240. agenttic/scoring/judge_calibration_corpus.jsonl +16 -0
  241. agenttic/secrets.py +124 -0
  242. agenttic/security.py +98 -0
  243. agenttic/server/__init__.py +0 -0
  244. agenttic/server/ab_manager.py +64 -0
  245. agenttic/server/abuse.py +197 -0
  246. agenttic/server/app.py +389 -0
  247. agenttic/server/auth.py +230 -0
  248. agenttic/server/certifications.py +263 -0
  249. agenttic/server/certify_manager.py +68 -0
  250. agenttic/server/connections.py +162 -0
  251. agenttic/server/crypto.py +58 -0
  252. agenttic/server/events.py +205 -0
  253. agenttic/server/executor.py +285 -0
  254. agenttic/server/health.py +253 -0
  255. agenttic/server/keys.py +124 -0
  256. agenttic/server/mailer.py +145 -0
  257. agenttic/server/metrics.py +102 -0
  258. agenttic/server/nodes.py +429 -0
  259. agenttic/server/observability.py +90 -0
  260. agenttic/server/optimizer_manager.py +77 -0
  261. agenttic/server/passwords.py +27 -0
  262. agenttic/server/pats.py +110 -0
  263. agenttic/server/ratelimit.py +129 -0
  264. agenttic/server/routes/__init__.py +0 -0
  265. agenttic/server/routes/ab.py +78 -0
  266. agenttic/server/routes/assistant.py +150 -0
  267. agenttic/server/routes/auth.py +201 -0
  268. agenttic/server/routes/billing.py +326 -0
  269. agenttic/server/routes/camp.py +308 -0
  270. agenttic/server/routes/certifications.py +228 -0
  271. agenttic/server/routes/connect.py +127 -0
  272. agenttic/server/routes/copilot.py +223 -0
  273. agenttic/server/routes/cost.py +60 -0
  274. agenttic/server/routes/dossiers.py +188 -0
  275. agenttic/server/routes/enforce.py +235 -0
  276. agenttic/server/routes/executions.py +229 -0
  277. agenttic/server/routes/feeds.py +14 -0
  278. agenttic/server/routes/hardening.py +145 -0
  279. agenttic/server/routes/ingest.py +48 -0
  280. agenttic/server/routes/leaderboard.py +55 -0
  281. agenttic/server/routes/live.py +142 -0
  282. agenttic/server/routes/optimize.py +80 -0
  283. agenttic/server/routes/passport.py +93 -0
  284. agenttic/server/routes/quickstart.py +108 -0
  285. agenttic/server/routes/resources.py +306 -0
  286. agenttic/server/routes/scan.py +338 -0
  287. agenttic/server/routes/settings.py +112 -0
  288. agenttic/server/routes/standard.py +177 -0
  289. agenttic/server/routes/status.py +22 -0
  290. agenttic/server/routes/workflows.py +54 -0
  291. agenttic/server/sessions.py +61 -0
  292. agenttic/server/store.py +437 -0
  293. agenttic/server/tracing.py +53 -0
  294. agenttic/server/users.py +87 -0
  295. agenttic/server/verification.py +97 -0
  296. agenttic/server/workflow_schema.py +111 -0
  297. agenttic/stats.py +220 -0
  298. agenttic/verifier/__init__.py +17 -0
  299. agenttic/verifier/header.py +36 -0
  300. agenttic/verifier/js/sdk.js +101 -0
  301. agenttic/verifier/js/verify_golden.js +38 -0
  302. agenttic/verifier/sdk.py +151 -0
  303. agenttic-1.0.0.dist-info/METADATA +733 -0
  304. agenttic-1.0.0.dist-info/RECORD +307 -0
  305. agenttic-1.0.0.dist-info/WHEEL +4 -0
  306. agenttic-1.0.0.dist-info/entry_points.txt +3 -0
  307. agenttic-1.0.0.dist-info/licenses/NOTICES.md +39 -0
agenttic/__init__.py ADDED
@@ -0,0 +1,128 @@
1
+ """agenttic — the public, supported entry point to the Agenttic platform.
2
+
3
+ This umbrella package is a **semver'd promise** (SPEC-8 Hard Rule 36): every
4
+ name it exports is a stable, supported surface. The implementation lives in the
5
+ internal ``agenttic.*`` package, which is not part of the public contract and may
6
+ change without notice. Import from here, never from ``ascore``.
7
+
8
+ The whole point of Agenttic distribution: a developer who has never seen it can
9
+
10
+ pip install agenttic
11
+
12
+ add one line
13
+
14
+ from agenttic import trace
15
+ agent = trace(my_agent) # wraps LangGraph / OpenAI Agents / anything
16
+
17
+ or certify from the shell
18
+
19
+ agenttic certify --mock # → a signed safety grade, no API key
20
+
21
+ The base install pulls **no framework SDK** and imports none (Hard Rule 37):
22
+ ``import agenttic`` works with zero of LangGraph/OpenAI-Agents/LangChain
23
+ present. Framework support is optional and lazy — ``agenttic[langgraph]``,
24
+ ``agenttic[openai]``, ``agenttic[all]`` — and is imported only when you actually
25
+ wrap a matching object.
26
+
27
+ Public surface (exactly ``__all__``; a test fails if anything else leaks):
28
+
29
+ * :func:`trace` — auto-detecting wrapper for any agent framework
30
+ * :func:`instrument` — decorator (and :func:`session` context manager) for
31
+ custom/homegrown agents
32
+ * :func:`certify` — certify an agent against a profile → evidence dossier
33
+ * :func:`verify` — recompute a dossier's hashes offline (signed-grade check)
34
+ * :class:`Trace`, :class:`Span` — the canonical run type the pipeline consumes
35
+ """
36
+ # NB: no ``from __future__ import annotations`` — it would bind the name
37
+ # ``annotations`` into this namespace and leak it past the public-surface test.
38
+ # Aliased private so it never leaks into the public surface (Hard Rule 36).
39
+ from typing import Any as _Any
40
+
41
+ # Version: single source of truth for the package. `agenttic` is now both the
42
+ # public umbrella and the internal engine (the former `ascore` package was
43
+ # folded in during the rename), so the version lives here directly. The
44
+ # distribution `version` in pyproject.toml is kept in lock-step (asserted by a
45
+ # test).
46
+ __version__ = "1.0.0"
47
+
48
+ # Certification surface — re-exported directly (core, no framework SDKs).
49
+ from agenttic.certification.certify import certify as certify
50
+ from agenttic.certification.dossier import verify as verify
51
+
52
+ # The canonical run type the ingest/certification pipeline consumes.
53
+ from agenttic.schema.trace import Span as Span
54
+ from agenttic.schema.trace import Trace as Trace
55
+
56
+ __all__ = [
57
+ "trace",
58
+ "instrument",
59
+ "session",
60
+ "certify",
61
+ "verify",
62
+ "Trace",
63
+ "Span",
64
+ ]
65
+
66
+
67
+ # ---------------------------------------------------------------------------
68
+ # The framework-facing surface (trace / instrument / session) is defined in the
69
+ # internal submodules ``agenttic._detect`` and ``agenttic._decorator``. We
70
+ # forward to them lazily so that ``import agenttic`` never pulls a framework SDK
71
+ # (Hard Rule 37) and so the umbrella imports cleanly regardless of which extras
72
+ # are present. The lazy import is per-call and cheap.
73
+ # ---------------------------------------------------------------------------
74
+ def trace(agent: _Any, *args: _Any, **kwargs: _Any) -> _Any:
75
+ """Wrap any agent so its runs emit a canonical Agenttic run.
76
+
77
+ Auto-detects the framework from the object's public shape and dispatches to
78
+ the right adapter — no need to name the framework:
79
+
80
+ from agenttic import trace
81
+ agent = trace(compiled_langgraph_graph) # → LangGraph adapter
82
+ agent = trace(openai_agents_agent) # → OpenAI Agents adapter
83
+ agent = trace(my_plain_callable) # → generic OTel wrapper
84
+
85
+ Wrapping is **behavior-identical** (Hard Rule 38): the returned object
86
+ delegates everything to the original and returns byte-identical outputs;
87
+ it only observes. Spans go to ``target`` (falling back to the
88
+ ``AGENTTIC_TARGET`` / ``OTEL_EXPORTER_OTLP_ENDPOINT`` env vars); with no
89
+ target configured it is a no-op that logs where to set one — it never
90
+ phones home. ``enforce=True`` opts into the SPEC-4 gateway at the ramp's
91
+ non-blocking default posture. See :func:`agenttic._detect.trace`.
92
+ """
93
+ from ._detect import trace as _impl
94
+ return _impl(agent, *args, **kwargs)
95
+
96
+
97
+ def instrument(*args: _Any, **kwargs: _Any) -> _Any:
98
+ """Decorate a custom ``query -> response`` function to emit a canonical run.
99
+
100
+ from agenttic import instrument
101
+
102
+ @instrument
103
+ def my_agent(query: str) -> str:
104
+ ...
105
+
106
+ Captures input, output and timing. If it can see into a supported LLM/tool
107
+ client called inside, it records the tool trajectory; otherwise it marks the
108
+ trajectory ``partial`` and logs why — it never fabricates tool calls
109
+ (Hard Rule 39). Same target/enforce and no-phone-home semantics as
110
+ :func:`trace`. See :func:`agenttic._decorator.instrument`.
111
+ """
112
+ from ._decorator import instrument as _impl
113
+ return _impl(*args, **kwargs)
114
+
115
+
116
+ def session(*args: _Any, **kwargs: _Any) -> _Any:
117
+ """Context-manager form of :func:`instrument` for code that isn't a single
118
+ function::
119
+
120
+ with agenttic.session(agent_id="my-agent") as run:
121
+ run.input = query
122
+ run.output = do_work(query)
123
+
124
+ Produces the same canonical run as the decorator. See
125
+ :func:`agenttic._decorator.session`.
126
+ """
127
+ from ._decorator import session as _impl
128
+ return _impl(*args, **kwargs)
agenttic/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Run the CLI with ``python -m agenttic`` (equivalent to the ``agenttic``
2
+ console script; ``ascore`` remains as a deprecated alias)."""
3
+ from agenttic.cli import app
4
+
5
+ if __name__ == "__main__":
6
+ app()
agenttic/_decorator.py ADDED
@@ -0,0 +1,133 @@
1
+ """@instrument decorator + session() context manager (SPEC-8 Step 42).
2
+
3
+ For custom / homegrown agents that aren't a supported framework: wrap any
4
+ ``query -> response`` function (or an arbitrary block of code) so it emits a
5
+ canonical Agenttic run — input, output, timing.
6
+
7
+ from agenttic import instrument, session
8
+
9
+ @instrument # or @instrument(agent_id="my-agent")
10
+ def my_agent(query: str) -> str:
11
+ ...
12
+
13
+ with session(agent_id="my-agent") as run: # for code that isn't one fn
14
+ run.input = query
15
+ run.output = do_work(query)
16
+
17
+ Honest degradation (Hard Rule 39): when the tool trajectory can't be observed
18
+ (the general case for an opaque callable), the run is marked ``partial`` with a
19
+ logged reason — Agenttic never invents tool calls. Same target/enforce and
20
+ no-silent-phone-home semantics as :func:`agenttic.trace` (Hard Rule 38): with no
21
+ target configured, wrapping runs the code unchanged and emits nothing, logging
22
+ where to configure a target.
23
+ """
24
+ from __future__ import annotations
25
+
26
+ import logging
27
+ import time
28
+ from typing import Any
29
+
30
+ from agenttic.ingest.emit import SpanEmitter
31
+
32
+ # The generic-run mechanism is shared with the auto-detecting trace() fallback:
33
+ # @instrument and trace(plain_callable) produce the same canonical run.
34
+ from ._detect import (
35
+ _GENERIC_PARTIAL_REASON,
36
+ _guidance_once,
37
+ _resolve_enforce,
38
+ _resolve_target,
39
+ _wrap_callable,
40
+ )
41
+
42
+ log = logging.getLogger("agenttic")
43
+
44
+ DEFAULT_AGENT_ID = "instrumented-agent"
45
+ _SCOPE = "agenttic_instrument"
46
+
47
+
48
+ def instrument(fn: Any = None, *, agent_id: str = DEFAULT_AGENT_ID,
49
+ target: str | None = None, enforce: Any = False,
50
+ agent_config_hash: str = "", sink: list | None = None,
51
+ reg: Any = None, cfg: Any = None):
52
+ """Decorate a ``query -> response`` function so each call emits a canonical
53
+ run. Usable bare (``@instrument``) or parameterized
54
+ (``@instrument(agent_id=...)``). Sync and async functions are both
55
+ supported; the wrapper is behavior-identical (returns byte-identical output,
56
+ re-raises the same exception)."""
57
+ def decorate(func: Any):
58
+ endpoint = _resolve_target(target, cfg)
59
+ posture = _resolve_enforce(enforce, cfg)
60
+ if endpoint is None and sink is None:
61
+ _guidance_once()
62
+ return _wrap_callable(
63
+ func, agent_id=agent_id, agent_config_hash=agent_config_hash,
64
+ endpoint=endpoint, auth_header=None, sink=sink, enforce=posture,
65
+ reg=reg, cfg=cfg, scope_name=_SCOPE)
66
+
67
+ if fn is not None: # @instrument
68
+ return decorate(fn)
69
+ return decorate # @instrument(...) / instrument(...)(fn)
70
+
71
+
72
+ class _Session:
73
+ """The object yielded by :func:`session`. Set ``.input`` and ``.output``;
74
+ on exit, one canonical (partial) run is emitted."""
75
+
76
+ def __init__(self, *, agent_id: str, agent_config_hash: str,
77
+ endpoint: str | None, auth_header: str | None,
78
+ sink: list | None, enforce: Any, reg: Any, cfg: Any,
79
+ input: Any = None):
80
+ self.agent_id = agent_id
81
+ self.input = input
82
+ self.output: Any = None
83
+ self._agent_config_hash = agent_config_hash
84
+ self._endpoint = endpoint
85
+ self._auth_header = auth_header
86
+ self._sink = sink
87
+ self._enforce = enforce
88
+ self._reg = reg
89
+ self._cfg = cfg
90
+ self._t0: int | None = None
91
+ self._guard = None
92
+
93
+ def __enter__(self) -> "_Session":
94
+ self._t0 = time.time_ns()
95
+ if self._enforce:
96
+ from agenttic.enforce.adapter_guard import build_enforce_guard
97
+ self._guard = build_enforce_guard(
98
+ self.agent_id, self._enforce, reg=self._reg, cfg=self._cfg)
99
+ self._guard.begin()
100
+ return self
101
+
102
+ def __exit__(self, exc_type, exc, tb) -> bool:
103
+ latency_ms = (time.time_ns() - (self._t0 or time.time_ns())) / 1e6
104
+ emitter = SpanEmitter(
105
+ self.agent_id, agent_config_hash=self._agent_config_hash,
106
+ endpoint=self._endpoint, auth_header=self._auth_header,
107
+ sink=self._sink, scope_name=_SCOPE)
108
+ emitter.emit_agent_run(
109
+ input=self.input,
110
+ output=(None if exc_type else self.output),
111
+ latency_ms=latency_ms, partial=True,
112
+ reason=(f"raised {exc_type.__name__}: {exc}" if exc_type
113
+ else _GENERIC_PARTIAL_REASON))
114
+ emitter.flush()
115
+ if self._guard is not None:
116
+ self._guard.end()
117
+ return False # never suppress the caller's exception (behavior-identical)
118
+
119
+
120
+ def session(*, agent_id: str = DEFAULT_AGENT_ID, target: str | None = None,
121
+ enforce: Any = False, agent_config_hash: str = "",
122
+ sink: list | None = None, reg: Any = None, cfg: Any = None,
123
+ input: Any = None) -> _Session:
124
+ """Context-manager form of :func:`instrument` for code that isn't a single
125
+ function. Produces the same canonical run as the decorator."""
126
+ endpoint = _resolve_target(target, cfg)
127
+ posture = _resolve_enforce(enforce, cfg)
128
+ if endpoint is None and sink is None:
129
+ _guidance_once()
130
+ return _Session(
131
+ agent_id=agent_id, agent_config_hash=agent_config_hash,
132
+ endpoint=endpoint, auth_header=None, sink=sink, enforce=posture,
133
+ reg=reg, cfg=cfg, input=input)
agenttic/_detect.py ADDED
@@ -0,0 +1,325 @@
1
+ """Auto-detecting ``trace()`` (SPEC-8 Step 41).
2
+
3
+ One import for everyone::
4
+
5
+ from agenttic import trace
6
+ agent = trace(anything)
7
+
8
+ ``trace`` inspects the object's **public shape** and dispatches to the right
9
+ adapter without the caller ever naming the framework:
10
+
11
+ * a LangGraph compiled graph → the ``agenttic-langgraph`` adapter
12
+ * an OpenAI Agents SDK agent → the ``agenttic-openai-agents`` adapter
13
+ * anything else callable → a generic OTel-emitting wrapper (Step 42's
14
+ mechanism, :func:`agenttic._decorator` — a partial-trajectory canonical run)
15
+
16
+ Detection is duck-typed on documented type/attribute signatures — we never
17
+ import a framework's private modules, and we never import the framework SDK just
18
+ to *detect* (so a fixture that merely mimics the shape dispatches correctly, and
19
+ a missing SDK is simply "not this one"). Loading the matched adapter is behind
20
+ ``try/except ImportError``: if the object looks like LangGraph but
21
+ ``agenttic-langgraph`` isn't installed, we fall back to the generic wrapper
22
+ rather than crash. The resolution order and the matched adapter are logged at
23
+ DEBUG.
24
+
25
+ Guarantees: behavior-identical wrapping (Hard Rule 38 — observe, never block or
26
+ mutate), and no telemetry by default (Hard Rule 38 — with no target configured
27
+ it is a no-op that logs where to set one; it never phones home). ``enforce=True``
28
+ opts into the SPEC-4 gateway at the ramp's non-blocking default posture.
29
+ """
30
+ from __future__ import annotations
31
+
32
+ import importlib
33
+ import logging
34
+ import os
35
+ from typing import Any
36
+
37
+ log = logging.getLogger("agenttic")
38
+
39
+ _GUIDANCE_LOGGED = False
40
+
41
+
42
+ # --- target resolution / no-phone-home guidance (Hard Rule 38) -------------
43
+ def _config_get(cfg: Any, *keys: str) -> Any:
44
+ """Read cfg['distribution'][key] from a passed dict, else from the config
45
+ file named by AGENTTIC_CONFIG, else None. Never raises."""
46
+ for key in keys:
47
+ if isinstance(cfg, dict):
48
+ val = (cfg.get("distribution") or {}).get(key)
49
+ if val:
50
+ return val
51
+ path = os.environ.get("AGENTTIC_CONFIG")
52
+ if path and os.path.exists(path):
53
+ try:
54
+ from agenttic.config import load_config
55
+ dist = (load_config(path).get("distribution") or {})
56
+ for key in keys:
57
+ if dist.get(key):
58
+ return dist[key]
59
+ except Exception: # noqa: BLE001 — config is best-effort for the library
60
+ return None
61
+ return None
62
+
63
+
64
+ def _resolve_target(target: str | None, cfg: Any = None) -> str | None:
65
+ """Where spans go, in priority order: explicit ``target`` arg → the
66
+ AGENTTIC_TARGET / OTEL_EXPORTER_OTLP_ENDPOINT env vars → the ``distribution.
67
+ target`` config key (dict or AGENTTIC_CONFIG file). None => don't emit."""
68
+ if target:
69
+ return target
70
+ env = (os.environ.get("AGENTTIC_TARGET")
71
+ or os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT"))
72
+ if env:
73
+ return env
74
+ return _config_get(cfg, "target")
75
+
76
+
77
+ # Non-blocking postures only — blocking ones (enforce_reads/enforce_all) must be
78
+ # ramped up deliberately server-side, never turned on by a library flag (Rules
79
+ # 31, 35). build_enforce_guard rejects a blocking posture loudly; we surface the
80
+ # same contract here so `trace(enforce=...)` fails fast with a clear message.
81
+ _NON_BLOCKING_POSTURES = {"observe", "shadow"}
82
+
83
+
84
+ def _resolve_enforce(enforce: Any, cfg: Any = None) -> Any:
85
+ """Resolve the opt-in enforce posture. ``False`` → off. ``True`` → the
86
+ configured ``distribution.enforce_posture`` (default ``shadow``). An explicit
87
+ string is validated as non-blocking. Returns the posture string, or False."""
88
+ if not enforce:
89
+ return False
90
+ posture = (_config_get(cfg, "enforce_posture") or "shadow") \
91
+ if enforce is True else str(enforce)
92
+ if posture not in _NON_BLOCKING_POSTURES:
93
+ raise ValueError(
94
+ f"agenttic.trace: enforce posture {posture!r} is blocking; the "
95
+ "library only permits non-blocking postures "
96
+ f"({', '.join(sorted(_NON_BLOCKING_POSTURES))}). Ramp up blocking "
97
+ "enforcement deliberately via the gateway, not a library flag.")
98
+ return posture
99
+
100
+
101
+ def _guidance_once() -> None:
102
+ """Log — exactly once — how to configure a target. No silent phone-home:
103
+ with no target and no sink, wrapping still runs the agent but emits nothing,
104
+ and the user is told how to turn telemetry on (Hard Rule 38)."""
105
+ global _GUIDANCE_LOGGED
106
+ if _GUIDANCE_LOGGED:
107
+ return
108
+ log.warning(
109
+ "agenttic.trace: no target configured — the wrapped agent runs normally "
110
+ "but NO telemetry is emitted (nothing leaves this process). To send runs "
111
+ "to your Agenttic instance, pass target='https://your-agenttic/v1/traces' "
112
+ "or set the AGENTTIC_TARGET (or OTEL_EXPORTER_OTLP_ENDPOINT) env var.")
113
+ _GUIDANCE_LOGGED = True
114
+
115
+
116
+ # --- framework detection (public shape only; never a private import) -------
117
+ def _type_name(obj: Any) -> str:
118
+ try:
119
+ return type(obj).__name__
120
+ except Exception: # pragma: no cover - exotic objects
121
+ return ""
122
+
123
+
124
+ def is_langgraph(obj: Any) -> bool:
125
+ """A LangGraph compiled graph: the public ``CompiledStateGraph`` / ``Pregel``
126
+ type name, or the LangChain Runnable shape (``invoke`` + ``stream`` +
127
+ ``get_graph`` callables) that a compiled graph exposes."""
128
+ if _type_name(obj) in {"CompiledStateGraph", "CompiledGraph", "Pregel"}:
129
+ return True
130
+ return all(callable(getattr(obj, m, None))
131
+ for m in ("invoke", "stream", "get_graph"))
132
+
133
+
134
+ def is_openai_agent(obj: Any) -> bool:
135
+ """An OpenAI Agents SDK ``Agent``: its public attribute shape
136
+ (``instructions`` + ``tools`` + ``handoffs`` + ``name``). ``Runner`` is
137
+ separate; users pass the agent."""
138
+ if _type_name(obj) == "Agent" and hasattr(obj, "instructions") \
139
+ and hasattr(obj, "tools"):
140
+ return True
141
+ return (hasattr(obj, "instructions") and hasattr(obj, "tools")
142
+ and hasattr(obj, "handoffs") and hasattr(obj, "name"))
143
+
144
+
145
+ def _missing_extra(framework: str, extra: str, type_name: str) -> ImportError:
146
+ """A recognized framework object with no adapter installed: an actionable
147
+ error (install the extra), never an opaque crash."""
148
+ return ImportError(
149
+ f"agenttic.trace: this looks like a {framework} object ({type_name}), "
150
+ f"but the {framework} adapter isn't installed. Install it with: "
151
+ f"pip install 'agenttic[{extra}]'")
152
+
153
+
154
+ def _load_adapter(module_name: str):
155
+ """Import a framework adapter, or return None if it isn't installed
156
+ (a missing SDK is "not available", never a crash)."""
157
+ try:
158
+ return importlib.import_module(module_name)
159
+ except ImportError:
160
+ return None
161
+
162
+
163
+ # --- the one public entry point -------------------------------------------
164
+ def trace(agent: Any, *, target: str | None = None, enforce: Any = False,
165
+ agent_id: str | None = None, agent_config_hash: str = "",
166
+ auth_header: str | None = None, sink: list | None = None,
167
+ reg: Any = None, cfg: Any = None) -> Any:
168
+ """Wrap ``agent`` so its runs emit a canonical Agenttic run — dispatching to
169
+ the matching framework adapter by shape, or to a generic wrapper.
170
+
171
+ ``target`` selects where spans go (defaults to the AGENTTIC_TARGET /
172
+ OTEL_EXPORTER_OTLP_ENDPOINT env vars); with nothing configured wrapping is a
173
+ logged no-op that never phones home. ``enforce=True`` routes tool calls
174
+ through the SPEC-4 gateway at the ramp's non-blocking default posture (opt-in
175
+ only). ``sink`` captures the OTLP payload in-process (tests / air-gapped)."""
176
+ endpoint = _resolve_target(target, cfg)
177
+ enforce = _resolve_enforce(enforce, cfg)
178
+ if enforce:
179
+ log.debug("agenttic.trace: enforce posture = %s (non-blocking, opt-in)",
180
+ enforce)
181
+ if endpoint is None and sink is None:
182
+ _guidance_once()
183
+
184
+ if is_langgraph(agent):
185
+ adapter = _load_adapter("agenttic_langgraph")
186
+ if adapter is not None:
187
+ log.debug("agenttic.trace: matched LangGraph adapter for %s",
188
+ _type_name(agent))
189
+ return adapter.trace(
190
+ agent, agent_id=agent_id or "langgraph-agent",
191
+ agent_config_hash=agent_config_hash, endpoint=endpoint,
192
+ auth_header=auth_header, sink=sink, enforce=enforce,
193
+ reg=reg, cfg=cfg)
194
+ if not callable(agent):
195
+ raise _missing_extra("LangGraph", "langgraph", _type_name(agent))
196
+ log.debug("agenttic.trace: %s looks like LangGraph but agenttic-langgraph "
197
+ "is not installed — using the generic wrapper", _type_name(agent))
198
+ elif is_openai_agent(agent):
199
+ adapter = _load_adapter("agenttic_openai_agents")
200
+ if adapter is not None:
201
+ log.debug("agenttic.trace: matched OpenAI Agents adapter for %s",
202
+ _type_name(agent))
203
+ return adapter.trace(
204
+ agent, agent_id=agent_id or "openai-agent",
205
+ agent_config_hash=agent_config_hash, endpoint=endpoint,
206
+ auth_header=auth_header, sink=sink, enforce=enforce,
207
+ reg=reg, cfg=cfg)
208
+ if not callable(agent):
209
+ raise _missing_extra("OpenAI Agents", "openai", _type_name(agent))
210
+ log.debug("agenttic.trace: %s looks like OpenAI Agents but "
211
+ "agenttic-openai-agents is not installed — using the generic "
212
+ "wrapper", _type_name(agent))
213
+
214
+ log.debug("agenttic.trace: no known framework matched %s — generic OTel "
215
+ "wrapper (partial trajectory)", _type_name(agent))
216
+ return _generic_trace(
217
+ agent, agent_id=agent_id or "agent",
218
+ agent_config_hash=agent_config_hash, endpoint=endpoint,
219
+ auth_header=auth_header, sink=sink, enforce=enforce, reg=reg, cfg=cfg)
220
+
221
+
222
+ # --- generic fallback: wrap an arbitrary callable --------------------------
223
+ _GENERIC_PARTIAL_REASON = (
224
+ "the wrapped function's internal tool/LLM calls are not observable from "
225
+ "outside, so the tool trajectory is recorded as partial (never fabricated)")
226
+
227
+
228
+ def _wrap_callable(fn: Any, *, agent_id: str, agent_config_hash: str = "",
229
+ endpoint: str | None = None, auth_header: str | None = None,
230
+ sink: list | None = None, enforce: Any = False,
231
+ reg: Any = None, cfg: Any = None, scope_name: str = "agenttic_generic"):
232
+ """Return a behavior-identical wrapper around ``fn`` that emits one
233
+ partial-trajectory canonical run per call. Shared by the generic ``trace()``
234
+ fallback and the ``@instrument`` decorator (Step 42)."""
235
+ import functools
236
+ import time
237
+
238
+ from agenttic.ingest.emit import SpanEmitter
239
+
240
+ def _emit(inp: Any, out: Any, err: Exception | None, t0: int) -> None:
241
+ latency_ms = (time.time_ns() - t0) / 1e6
242
+ emitter = SpanEmitter(agent_id, agent_config_hash=agent_config_hash,
243
+ endpoint=endpoint, auth_header=auth_header,
244
+ sink=sink, scope_name=scope_name)
245
+ emitter.emit_agent_run(
246
+ input=inp, output=(None if err else out), latency_ms=latency_ms,
247
+ partial=True,
248
+ reason=(f"raised {type(err).__name__}: {err}" if err
249
+ else _GENERIC_PARTIAL_REASON))
250
+ emitter.flush()
251
+
252
+ def _guard():
253
+ if not enforce:
254
+ return None
255
+ from agenttic.enforce.adapter_guard import build_enforce_guard
256
+ return build_enforce_guard(agent_id, enforce, reg=reg, cfg=cfg)
257
+
258
+ is_async = _is_coro(fn)
259
+
260
+ if is_async:
261
+ @functools.wraps(fn)
262
+ async def awrapper(*args: Any, **kwargs: Any) -> Any:
263
+ guard = _guard()
264
+ if guard is not None:
265
+ guard.begin()
266
+ t0 = time.time_ns()
267
+ out = None
268
+ err: Exception | None = None
269
+ try:
270
+ out = await fn(*args, **kwargs)
271
+ return out
272
+ except Exception as e: # behavior-identical: observe then re-raise
273
+ err = e
274
+ raise
275
+ finally:
276
+ _emit(_first_arg(args, kwargs), out, err, t0)
277
+ if guard is not None:
278
+ guard.end()
279
+ return awrapper
280
+
281
+ @functools.wraps(fn)
282
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
283
+ guard = _guard()
284
+ if guard is not None:
285
+ guard.begin()
286
+ t0 = time.time_ns()
287
+ out = None
288
+ err = None
289
+ try:
290
+ out = fn(*args, **kwargs)
291
+ return out
292
+ except Exception as e: # behavior-identical: observe then re-raise
293
+ err = e
294
+ raise
295
+ finally:
296
+ _emit(_first_arg(args, kwargs), out, err, t0)
297
+ if guard is not None:
298
+ guard.end()
299
+ return wrapper
300
+
301
+
302
+ def _generic_trace(fn: Any, **kw: Any):
303
+ if not callable(fn):
304
+ raise TypeError(
305
+ "agenttic.trace: don't know how to trace a "
306
+ f"{type(fn).__name__!r}; pass a LangGraph graph, an OpenAI Agents "
307
+ "agent, or a callable (query -> response).")
308
+ return _wrap_callable(fn, **kw)
309
+
310
+
311
+ def _is_coro(fn: Any) -> bool:
312
+ import asyncio
313
+ if asyncio.iscoroutinefunction(fn):
314
+ return True
315
+ call = getattr(fn, "__call__", None)
316
+ return call is not None and asyncio.iscoroutinefunction(call)
317
+
318
+
319
+ def _first_arg(args: tuple, kwargs: dict) -> Any:
320
+ """The agent's query: the first positional arg, else the first kwarg."""
321
+ if args:
322
+ return args[0]
323
+ if kwargs:
324
+ return next(iter(kwargs.values()))
325
+ return None
agenttic/_env.py ADDED
@@ -0,0 +1,76 @@
1
+ """Environment-variable back-compat shim for the ``ascore`` → ``agenttic`` rename.
2
+
3
+ Every variable that used to be read as ``ASCORE_<NAME>`` is now read
4
+ ``AGENTTIC_<NAME>``-first, falling back to the legacy ``ASCORE_<NAME>`` (with a
5
+ ``DeprecationWarning``) so existing deployments keep working unchanged.
6
+
7
+ This is load-bearing in production: node1's ``.env`` supplies
8
+ ``ASCORE_CERT_SIGNING_KEY`` and ``ASCORE_PASSPORT_SIGNING_KEY``. If the renamed
9
+ code stopped honoring the ``ASCORE_*`` names, cert/passport signing would fail
10
+ closed and the app would 502. Do NOT rename those vars on the host; the shim
11
+ keeps reading them (while nudging operators toward ``AGENTTIC_*``).
12
+
13
+ Precedence, for a shimmed name ``X``:
14
+
15
+ AGENTTIC_X > ASCORE_X (deprecated) > default
16
+
17
+ Only names that already start with ``ASCORE_`` / ``AGENTTIC_`` participate in
18
+ the dance; any other name (``ANTHROPIC_API_KEY``, ``FI_API_KEY``, …) is read
19
+ verbatim, exactly like ``os.environ.get``.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import os
25
+ import warnings
26
+ from typing import Mapping
27
+
28
+ _NEW_PREFIX = "AGENTTIC_"
29
+ _OLD_PREFIX = "ASCORE_"
30
+
31
+
32
+ def _suffix(name: str) -> str | None:
33
+ """The bare suffix of a shimmed var (``ASCORE_DB``/``AGENTTIC_DB`` → ``DB``),
34
+ or ``None`` if ``name`` is not part of the rename shim."""
35
+ if name.startswith(_NEW_PREFIX):
36
+ return name[len(_NEW_PREFIX):]
37
+ if name.startswith(_OLD_PREFIX):
38
+ return name[len(_OLD_PREFIX):]
39
+ return None
40
+
41
+
42
+ def candidate_names(name: str) -> tuple[str, str | None]:
43
+ """Return ``(new_name, old_name)`` for a shimmed var, or ``(name, None)`` for
44
+ a name that does not participate in the rename shim."""
45
+ suf = _suffix(name)
46
+ if suf is None:
47
+ return name, None
48
+ return _NEW_PREFIX + suf, _OLD_PREFIX + suf
49
+
50
+
51
+ def warn_legacy(old: str, new: str, *, stacklevel: int = 3) -> None:
52
+ """Emit the deprecation nudge for reading a legacy ``ASCORE_*`` var."""
53
+ warnings.warn(
54
+ f"Environment variable {old} is deprecated; set {new} instead "
55
+ f"({old} is still honored for backward compatibility).",
56
+ DeprecationWarning,
57
+ stacklevel=stacklevel,
58
+ )
59
+
60
+
61
+ def get_env(name: str, default: str | None = None, *,
62
+ environ: Mapping[str, str] | None = None) -> str | None:
63
+ """Drop-in for ``os.environ.get(name, default)`` with rename back-compat.
64
+
65
+ For an ``ASCORE_*``/``AGENTTIC_*`` name: prefer ``AGENTTIC_<X>``; else fall
66
+ back to ``ASCORE_<X>`` (emitting a ``DeprecationWarning``); else return
67
+ ``default``. For any other name, behaves exactly like ``os.environ.get``.
68
+ """
69
+ env = os.environ if environ is None else environ
70
+ new, old = candidate_names(name)
71
+ if new in env:
72
+ return env[new]
73
+ if old is not None and old in env:
74
+ warn_legacy(old, new)
75
+ return env[old]
76
+ return default