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.
- agenttic/__init__.py +128 -0
- agenttic/__main__.py +6 -0
- agenttic/_decorator.py +133 -0
- agenttic/_detect.py +325 -0
- agenttic/_env.py +76 -0
- agenttic/ab.py +227 -0
- agenttic/adapters/__init__.py +0 -0
- agenttic/adapters/anthropic_simple.py +240 -0
- agenttic/adapters/base.py +40 -0
- agenttic/adapters/blackbox_http.py +175 -0
- agenttic/adapters/managed_agent.py +267 -0
- agenttic/airgap.py +184 -0
- agenttic/assistant/__init__.py +31 -0
- agenttic/assistant/adapter.py +145 -0
- agenttic/assistant/agent.py +253 -0
- agenttic/assistant/guard.py +173 -0
- agenttic/assistant/posture.py +92 -0
- agenttic/assistant/store.py +67 -0
- agenttic/assistant/tools.py +245 -0
- agenttic/billing/__init__.py +29 -0
- agenttic/billing/credits_provider.py +111 -0
- agenttic/billing/gateways/__init__.py +8 -0
- agenttic/billing/gateways/paypal_gateway.py +136 -0
- agenttic/billing/gateways/stripe_gateway.py +99 -0
- agenttic/billing/invoices.py +125 -0
- agenttic/billing/models.py +119 -0
- agenttic/billing/plans.py +132 -0
- agenttic/billing/service.py +152 -0
- agenttic/billing/store.py +295 -0
- agenttic/billing/webhooks.py +251 -0
- agenttic/budget.py +112 -0
- agenttic/camp/__init__.py +66 -0
- agenttic/camp/adapter_agent.py +72 -0
- agenttic/camp/agent.py +104 -0
- agenttic/camp/environment.py +110 -0
- agenttic/camp/gate.py +80 -0
- agenttic/camp/holdout.py +69 -0
- agenttic/camp/improve.py +283 -0
- agenttic/camp/service.py +239 -0
- agenttic/camp/store.py +319 -0
- agenttic/camp/task.py +59 -0
- agenttic/camp/tasks/__init__.py +3 -0
- agenttic/camp/tasks/support_triage.py +130 -0
- agenttic/camp/trace.py +135 -0
- agenttic/camp/trainer.py +122 -0
- agenttic/cards/__init__.py +6 -0
- agenttic/cards/agency.py +79 -0
- agenttic/cards/autofill.py +161 -0
- agenttic/cards/autonomy.py +102 -0
- agenttic/cards/fields.py +118 -0
- agenttic/certification/__init__.py +20 -0
- agenttic/certification/__main__.py +16 -0
- agenttic/certification/certify.py +328 -0
- agenttic/certification/coverage.py +62 -0
- agenttic/certification/domains.py +158 -0
- agenttic/certification/dossier.py +186 -0
- agenttic/certification/elicitation.py +274 -0
- agenttic/certification/hashing.py +46 -0
- agenttic/certification/mock_provider.py +50 -0
- agenttic/certification/profiles.py +192 -0
- agenttic/certification/safety_cert.py +694 -0
- agenttic/certification/staleness.py +106 -0
- agenttic/certification/swe_pack.py +141 -0
- agenttic/certification/tiers.py +200 -0
- agenttic/cli.py +1488 -0
- agenttic/config.py +55 -0
- agenttic/connect.py +296 -0
- agenttic/copilot/__init__.py +33 -0
- agenttic/copilot/agent.py +311 -0
- agenttic/copilot/credits.py +201 -0
- agenttic/copilot/errors.py +163 -0
- agenttic/copilot/knowledge.md +232 -0
- agenttic/copilot/service.py +212 -0
- agenttic/copilot/skill.py +165 -0
- agenttic/copilot/store.py +56 -0
- agenttic/copilot/tools.py +408 -0
- agenttic/cost.py +198 -0
- agenttic/documents.py +92 -0
- agenttic/enforce/__init__.py +7 -0
- agenttic/enforce/adapter_guard.py +105 -0
- agenttic/enforce/approvals.py +113 -0
- agenttic/enforce/async_judge.py +141 -0
- agenttic/enforce/canaries.py +169 -0
- agenttic/enforce/compiler.py +359 -0
- agenttic/enforce/dashboard.py +83 -0
- agenttic/enforce/export.py +56 -0
- agenttic/enforce/feedback.py +44 -0
- agenttic/enforce/gateway.py +309 -0
- agenttic/enforce/interactive_oversight.py +414 -0
- agenttic/enforce/lanes.py +249 -0
- agenttic/enforce/ramp.py +239 -0
- agenttic/enforce/self_security.py +65 -0
- agenttic/feeds/__init__.py +5 -0
- agenttic/feeds/risk_api.py +100 -0
- agenttic/feeds/webhooks.py +84 -0
- agenttic/generator/__init__.py +0 -0
- agenttic/generator/pipeline.py +288 -0
- agenttic/hardening.py +670 -0
- agenttic/harness/__init__.py +0 -0
- agenttic/harness/runner.py +171 -0
- agenttic/ingest/__init__.py +39 -0
- agenttic/ingest/doctor.py +127 -0
- agenttic/ingest/emit.py +199 -0
- agenttic/ingest/mapping.py +340 -0
- agenttic/ingest/otel.py +207 -0
- agenttic/interop/__init__.py +15 -0
- agenttic/interop/agent_index.py +124 -0
- agenttic/interop/inspect_log.py +559 -0
- agenttic/issues.py +401 -0
- agenttic/leaderboard.py +109 -0
- agenttic/live/__init__.py +0 -0
- agenttic/live/incidents.py +209 -0
- agenttic/live/monitor.py +112 -0
- agenttic/metrics/ATTRIBUTION.md +52 -0
- agenttic/metrics/__init__.py +17 -0
- agenttic/metrics/answer_match.py +261 -0
- agenttic/metrics/bfcl_ast_official.py +232 -0
- agenttic/metrics/bfcl_reproduce.py +529 -0
- agenttic/metrics/calibration.py +39 -0
- agenttic/metrics/canonical_checks.py +352 -0
- agenttic/metrics/catalog.py +394 -0
- agenttic/metrics/datasets/__init__.py +44 -0
- agenttic/metrics/datasets/agentdojo.py +276 -0
- agenttic/metrics/datasets/agentdojo_data/ATTRIBUTION.md +81 -0
- agenttic/metrics/datasets/agentdojo_data/LICENSE +21 -0
- agenttic/metrics/datasets/agentdojo_data/agentdojo.sample.json +139 -0
- agenttic/metrics/datasets/agentharm.py +92 -0
- agenttic/metrics/datasets/agentharm_data/ATTRIBUTION.md +60 -0
- agenttic/metrics/datasets/agentharm_data/LICENSE +24 -0
- agenttic/metrics/datasets/agentharm_data/agentharm_harmful.sample.json +71 -0
- agenttic/metrics/datasets/assistantbench.py +110 -0
- agenttic/metrics/datasets/assistantbench_data/ATTRIBUTION.md +62 -0
- agenttic/metrics/datasets/assistantbench_data/LICENSE +202 -0
- agenttic/metrics/datasets/assistantbench_data/assistant_bench_v1.0_dev.sample.jsonl +16 -0
- agenttic/metrics/datasets/base.py +90 -0
- agenttic/metrics/datasets/bfcl.py +240 -0
- agenttic/metrics/datasets/bfcl_data/ATTRIBUTION.md +51 -0
- agenttic/metrics/datasets/bfcl_data/BFCL_v3_live_multiple.sample.answers.json +292 -0
- agenttic/metrics/datasets/bfcl_data/BFCL_v3_live_multiple.sample.json +2178 -0
- agenttic/metrics/datasets/bfcl_data/BFCL_v3_live_simple.sample.answers.json +293 -0
- agenttic/metrics/datasets/bfcl_data/BFCL_v3_live_simple.sample.json +690 -0
- agenttic/metrics/datasets/bfcl_data/BFCL_v3_multiple.sample.answers.json +445 -0
- agenttic/metrics/datasets/bfcl_data/BFCL_v3_multiple.sample.json +1551 -0
- agenttic/metrics/datasets/bfcl_data/BFCL_v3_parallel.sample.answers.json +755 -0
- agenttic/metrics/datasets/bfcl_data/BFCL_v3_parallel.sample.json +759 -0
- agenttic/metrics/datasets/bfcl_data/BFCL_v3_parallel_multiple.sample.answers.json +621 -0
- agenttic/metrics/datasets/bfcl_data/BFCL_v3_parallel_multiple.sample.json +1355 -0
- agenttic/metrics/datasets/bfcl_data/BFCL_v3_simple.sample.answers.json +426 -0
- agenttic/metrics/datasets/bfcl_data/BFCL_v3_simple.sample.json +892 -0
- agenttic/metrics/datasets/bfcl_data/LICENSE +202 -0
- agenttic/metrics/datasets/gaia.py +131 -0
- agenttic/metrics/datasets/gaia_data/ATTRIBUTION.md +76 -0
- agenttic/metrics/datasets/gaia_data/LICENSE.txt +32 -0
- agenttic/metrics/datasets/gaia_data/gaia_validation.sample.jsonl +6 -0
- agenttic/metrics/datasets/injecagent.py +124 -0
- agenttic/metrics/datasets/injecagent_data/ATTRIBUTION.md +72 -0
- agenttic/metrics/datasets/injecagent_data/LICENSE +21 -0
- agenttic/metrics/datasets/injecagent_data/injecagent.sample.json +222 -0
- agenttic/metrics/datasets/swebench.py +203 -0
- agenttic/metrics/datasets/swebench_data/ATTRIBUTION.md +81 -0
- agenttic/metrics/datasets/swebench_data/LICENSE +21 -0
- agenttic/metrics/datasets/swebench_data/swebench_verified.sample.jsonl +7 -0
- agenttic/metrics/datasets/tau_bench.py +164 -0
- agenttic/metrics/datasets/tau_bench_data/ATTRIBUTION.md +61 -0
- agenttic/metrics/datasets/tau_bench_data/LICENSE +21 -0
- agenttic/metrics/datasets/tau_bench_data/tau_bench.sample.json +1111 -0
- agenttic/metrics/faithfulness.py +242 -0
- agenttic/metrics/index.py +42 -0
- agenttic/metrics/injection_detect.py +270 -0
- agenttic/metrics/judge_quality.py +489 -0
- agenttic/metrics/redteam.py +425 -0
- agenttic/metrics/redteam_injection_responses.jsonl +24 -0
- agenttic/metrics/reliability.py +30 -0
- agenttic/metrics/reproduction.py +225 -0
- agenttic/metrics/runner.py +169 -0
- agenttic/metrics/safety_battery.py +215 -0
- agenttic/metrics/safety_catalog.py +114 -0
- agenttic/metrics/safety_checks.py +590 -0
- agenttic/metrics/safety_suite.py +138 -0
- agenttic/metrics/standard_suites.py +257 -0
- agenttic/metrics/structured_ir.py +824 -0
- agenttic/metrics/swe_catalog.py +107 -0
- agenttic/metrics/swe_checks.py +296 -0
- agenttic/metrics/swe_suites.py +563 -0
- agenttic/metrics/swebench_resolve.py +179 -0
- agenttic/metrics/text_overlap.py +573 -0
- agenttic/migrations.py +321 -0
- agenttic/ops.py +466 -0
- agenttic/optimizer.py +561 -0
- agenttic/oversight/__init__.py +7 -0
- agenttic/oversight/analytics.py +105 -0
- agenttic/passport/__init__.py +3 -0
- agenttic/passport/issuer.py +136 -0
- agenttic/passport/keys.py +203 -0
- agenttic/passport/receipts.py +174 -0
- agenttic/pricing.py +25 -0
- agenttic/py.typed +0 -0
- agenttic/registry/__init__.py +0 -0
- agenttic/registry/sqlite_store.py +1838 -0
- agenttic/registry/store.py +16 -0
- agenttic/release/__init__.py +2 -0
- agenttic/release/ladder.py +101 -0
- agenttic/release/promotion.py +201 -0
- agenttic/release/scaffold.py +68 -0
- agenttic/release/scaffold_assets/QUICKSTART.md +55 -0
- agenttic/release/scaffold_assets/agent_sample.py +50 -0
- agenttic/release/scaffold_assets/config.yaml +283 -0
- agenttic/release/scaffold_assets/kb.json +4 -0
- agenttic/reporting/__init__.py +0 -0
- agenttic/reporting/ab_report.py +240 -0
- agenttic/reporting/dossier_report.py +172 -0
- agenttic/reporting/pdf_report.py +213 -0
- agenttic/reporting/scorecard_report.py +155 -0
- agenttic/result_cache.py +59 -0
- agenttic/retry.py +92 -0
- agenttic/scan.py +131 -0
- agenttic/schema/__init__.py +10 -0
- agenttic/schema/ab.py +136 -0
- agenttic/schema/agent.py +62 -0
- agenttic/schema/agent_card.py +129 -0
- agenttic/schema/certification.py +189 -0
- agenttic/schema/enforcement.py +160 -0
- agenttic/schema/incident.py +118 -0
- agenttic/schema/optimization.py +148 -0
- agenttic/schema/passport.py +115 -0
- agenttic/schema/release.py +77 -0
- agenttic/schema/rubric.py +85 -0
- agenttic/schema/scorecard.py +157 -0
- agenttic/schema/testcase.py +39 -0
- agenttic/schema/trace.py +93 -0
- agenttic/scoring/__init__.py +0 -0
- agenttic/scoring/calibration.py +97 -0
- agenttic/scoring/calibration_corpus.jsonl +52 -0
- agenttic/scoring/checks.py +249 -0
- agenttic/scoring/corpus.py +224 -0
- agenttic/scoring/engine.py +221 -0
- agenttic/scoring/fi_eval.py +164 -0
- agenttic/scoring/judge.py +261 -0
- agenttic/scoring/judge_calibration.py +265 -0
- agenttic/scoring/judge_calibration_corpus.jsonl +16 -0
- agenttic/secrets.py +124 -0
- agenttic/security.py +98 -0
- agenttic/server/__init__.py +0 -0
- agenttic/server/ab_manager.py +64 -0
- agenttic/server/abuse.py +197 -0
- agenttic/server/app.py +389 -0
- agenttic/server/auth.py +230 -0
- agenttic/server/certifications.py +263 -0
- agenttic/server/certify_manager.py +68 -0
- agenttic/server/connections.py +162 -0
- agenttic/server/crypto.py +58 -0
- agenttic/server/events.py +205 -0
- agenttic/server/executor.py +285 -0
- agenttic/server/health.py +253 -0
- agenttic/server/keys.py +124 -0
- agenttic/server/mailer.py +145 -0
- agenttic/server/metrics.py +102 -0
- agenttic/server/nodes.py +429 -0
- agenttic/server/observability.py +90 -0
- agenttic/server/optimizer_manager.py +77 -0
- agenttic/server/passwords.py +27 -0
- agenttic/server/pats.py +110 -0
- agenttic/server/ratelimit.py +129 -0
- agenttic/server/routes/__init__.py +0 -0
- agenttic/server/routes/ab.py +78 -0
- agenttic/server/routes/assistant.py +150 -0
- agenttic/server/routes/auth.py +201 -0
- agenttic/server/routes/billing.py +326 -0
- agenttic/server/routes/camp.py +308 -0
- agenttic/server/routes/certifications.py +228 -0
- agenttic/server/routes/connect.py +127 -0
- agenttic/server/routes/copilot.py +223 -0
- agenttic/server/routes/cost.py +60 -0
- agenttic/server/routes/dossiers.py +188 -0
- agenttic/server/routes/enforce.py +235 -0
- agenttic/server/routes/executions.py +229 -0
- agenttic/server/routes/feeds.py +14 -0
- agenttic/server/routes/hardening.py +145 -0
- agenttic/server/routes/ingest.py +48 -0
- agenttic/server/routes/leaderboard.py +55 -0
- agenttic/server/routes/live.py +142 -0
- agenttic/server/routes/optimize.py +80 -0
- agenttic/server/routes/passport.py +93 -0
- agenttic/server/routes/quickstart.py +108 -0
- agenttic/server/routes/resources.py +306 -0
- agenttic/server/routes/scan.py +338 -0
- agenttic/server/routes/settings.py +112 -0
- agenttic/server/routes/standard.py +177 -0
- agenttic/server/routes/status.py +22 -0
- agenttic/server/routes/workflows.py +54 -0
- agenttic/server/sessions.py +61 -0
- agenttic/server/store.py +437 -0
- agenttic/server/tracing.py +53 -0
- agenttic/server/users.py +87 -0
- agenttic/server/verification.py +97 -0
- agenttic/server/workflow_schema.py +111 -0
- agenttic/stats.py +220 -0
- agenttic/verifier/__init__.py +17 -0
- agenttic/verifier/header.py +36 -0
- agenttic/verifier/js/sdk.js +101 -0
- agenttic/verifier/js/verify_golden.js +38 -0
- agenttic/verifier/sdk.py +151 -0
- agenttic-1.0.0.dist-info/METADATA +733 -0
- agenttic-1.0.0.dist-info/RECORD +307 -0
- agenttic-1.0.0.dist-info/WHEEL +4 -0
- agenttic-1.0.0.dist-info/entry_points.txt +3 -0
- 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
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
|