odigos-agentcore 0.0.25__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.
@@ -0,0 +1,57 @@
1
+ # Build output: layer zips extracted from the image, per-region publish results, rendered manifests.
2
+ # Never commit a layer zip here — lambda/layers/ is the one place an override belongs.
3
+ dist/
4
+
5
+ # Node / CDK build output. cdk/tsconfig.json is generated by jsii on every build (see cdk/.gitignore).
6
+ node_modules/
7
+
8
+ # Python bytecode from the AgentCore builder and bootstrap tests.
9
+ __pycache__/
10
+ *.pyc
11
+
12
+ .DS_Store
13
+
14
+ # Local .terraform directories
15
+ .terraform/
16
+
17
+ # .tfstate files
18
+ *.tfstate
19
+ *.tfstate.*
20
+
21
+ # Crash log files
22
+ crash.log
23
+ crash.*.log
24
+
25
+ # Exclude all .tfvars files, which are likely to contain sensitive data, such as
26
+ # password, private keys, and other secrets. These should not be part of version
27
+ # control as they are data points which are potentially sensitive and subject
28
+ # to change depending on the environment.
29
+ *.tfvars
30
+ *.tfvars.json
31
+
32
+ # Ignore override files as they are usually used to override resources locally and so
33
+ # are not checked in
34
+ override.tf
35
+ override.tf.json
36
+ *_override.tf
37
+ *_override.tf.json
38
+
39
+ # Ignore transient lock info files created by terraform apply
40
+ .terraform.tfstate.lock.info
41
+
42
+ # Include override files you do wish to add to version control using negated pattern
43
+ # !example_override.tf
44
+
45
+ # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan
46
+ # example: *tfplan*
47
+
48
+ # Ignore CLI configuration files
49
+ .terraformrc
50
+ terraform.rc
51
+
52
+ # Optional: ignore graph output files generated by `terraform graph`
53
+ # *.dot
54
+
55
+ # Optional: ignore plan files saved before destroying Terraform configuration
56
+ # Uncomment the line below if you want to ignore planout files.
57
+ # planout
@@ -0,0 +1,23 @@
1
+ Metadata-Version: 2.5
2
+ Name: odigos-agentcore
3
+ Version: 0.0.25
4
+ Summary: Odigos OpenTelemetry bootstrap for Amazon Bedrock AgentCore Runtime agents
5
+ License-Expression: Apache-2.0
6
+ Requires-Python: >=3.10
7
+ Provides-Extra: adot
8
+ Requires-Dist: aws-opentelemetry-distro==0.21.0; extra == 'adot'
9
+ Provides-Extra: openllmetry
10
+ Requires-Dist: opentelemetry-distro==0.66b1; extra == 'openllmetry'
11
+ Requires-Dist: opentelemetry-exporter-otlp-proto-http==1.45.1; extra == 'openllmetry'
12
+ Requires-Dist: opentelemetry-instrumentation-aiohttp-client==0.66b1; extra == 'openllmetry'
13
+ Requires-Dist: opentelemetry-instrumentation-asgi==0.66b1; extra == 'openllmetry'
14
+ Requires-Dist: opentelemetry-instrumentation-bedrock==0.62.3; extra == 'openllmetry'
15
+ Requires-Dist: opentelemetry-instrumentation-fastapi==0.66b1; extra == 'openllmetry'
16
+ Requires-Dist: opentelemetry-instrumentation-httpx==0.66b1; extra == 'openllmetry'
17
+ Requires-Dist: opentelemetry-instrumentation-logging==0.66b1; extra == 'openllmetry'
18
+ Requires-Dist: opentelemetry-instrumentation-requests==0.66b1; extra == 'openllmetry'
19
+ Requires-Dist: opentelemetry-instrumentation-starlette==0.66b1; extra == 'openllmetry'
20
+ Requires-Dist: opentelemetry-instrumentation-threading==0.66b1; extra == 'openllmetry'
21
+ Requires-Dist: opentelemetry-instrumentation-urllib3==0.66b1; extra == 'openllmetry'
22
+ Requires-Dist: opentelemetry-propagator-aws-xray==1.0.2; extra == 'openllmetry'
23
+ Requires-Dist: opentelemetry-sdk==1.45.1; extra == 'openllmetry'
@@ -0,0 +1 @@
1
+ """Odigos AgentCore bootstrap. Dormant unless the runtime sets ODIGOS_AGENTCORE_ENABLED=true; see bootstrap.py."""
@@ -0,0 +1,4 @@
1
+ """Imported by odigos_agentcore.pth when the interpreter processes the site-packages the package is installed into."""
2
+ from odigos_agentcore.bootstrap import start
3
+
4
+ start()
@@ -0,0 +1,202 @@
1
+ """Odigos AgentCore bootstrap: starts the selected OpenTelemetry profile, then adjusts its telemetry for AgentCore.
2
+
3
+ One copy of this code serves every delivery:
4
+
5
+ - the wheel's ``odigos_agentcore.pth``, which imports it at interpreter start in the environment the package is
6
+ installed into (container images);
7
+ - ``odigos_agentcore/startup/sitecustomize.py``, which ``odigos_agentcore_start.py`` puts on ``PYTHONPATH`` when the
8
+ package was installed into a direct-code ZIP with ``pip install --target``;
9
+ - the prepared bundle's ``bootstrap/python/sitecustomize.py`` (the carrier image layout), which passes the bundle
10
+ root so the profile is taken from ``<root>/profiles/python/<vendor>``.
11
+
12
+ Everything stays dormant unless ``ODIGOS_AGENTCORE_ENABLED=true``. This module imports only what the interpreter
13
+ has already loaded until then, so the dormant cost is one environment lookup.
14
+ """
15
+ import os
16
+ import sys
17
+
18
+ PROFILES = {"python-agentcore-adot": "adot", "python-agentcore-openllmetry": "openllmetry"}
19
+ # A module each profile's SDK installs; how a profile is found in the environment the package is installed into.
20
+ PROFILE_MODULES = {"adot": "amazon.opentelemetry.distro", "openllmetry": "opentelemetry.instrumentation.bedrock"}
21
+
22
+ _initialized = False
23
+
24
+
25
+ def start(root=None):
26
+ """Initialize, failing open: an error is reported by type only and never reaches the application."""
27
+ try:
28
+ initialize(root)
29
+ except Exception as error:
30
+ # Fail open for business traffic. Emit no environment, request content or exception text.
31
+ sys.stderr.write("Odigos AgentCore instrumentation failed to initialize (" + type(error).__name__ + ").\n")
32
+
33
+
34
+ def initialize(root=None):
35
+ """Start the profile ODIGOS_AGENTCORE_DISTRO selects. ``root`` is a prepared bundle's root, or None."""
36
+ global _initialized
37
+ if os.environ.get("ODIGOS_AGENTCORE_ENABLED", "").lower() != "true" or _initialized:
38
+ return
39
+ vendor = PROFILES.get(os.environ.get("ODIGOS_AGENTCORE_DISTRO", ""))
40
+ if vendor is None:
41
+ raise RuntimeError("unsupported Odigos AgentCore Python profile")
42
+ select_profile(vendor, root)
43
+ # Set before the SDK starts: a second entry point in the same process (the .pth and a bundle's sitecustomize)
44
+ # must not instrument again, even after a partial failure.
45
+ _initialized = True
46
+ os.environ["OTEL_PYTHON_DISTRO"] = "aws_distro" if vendor == "adot" else "distro"
47
+ os.environ["OTEL_PYTHON_CONFIGURATOR"] = "aws_configurator" if vendor == "adot" else "configurator"
48
+ os.environ["TRACELOOP_TRACE_CONTENT"] = "false"
49
+ os.environ["OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"] = "false"
50
+ from opentelemetry.instrumentation.auto_instrumentation import initialize as instrument
51
+ instrument()
52
+ suppress_contentless_genai_events()
53
+ redact_genai_content()
54
+ exclude_asgi_message_spans()
55
+ root_invocations_at_the_agent()
56
+
57
+
58
+ def select_profile(vendor, root=None):
59
+ """Make the selected profile importable; return the directory put on sys.path, or None for the environment.
60
+
61
+ A prepared bundle carries each profile at ``<root>/profiles/python/<vendor>``; only the selected one joins
62
+ sys.path, so only it takes part in imports and entry-point discovery. Without that directory the profile is
63
+ the SDK the package's extra installed next to it. Raises when neither is present.
64
+ """
65
+ if root is not None:
66
+ packages = os.path.join(str(root), "profiles", "python", vendor)
67
+ if os.path.isdir(packages):
68
+ if not os.path.isdir(os.path.join(packages, "opentelemetry")):
69
+ raise RuntimeError("selected Odigos AgentCore profile is not packaged")
70
+ sys.path.insert(0, packages)
71
+ return packages
72
+ if not installed(PROFILE_MODULES[vendor]):
73
+ raise RuntimeError("selected Odigos AgentCore profile is not installed")
74
+ return None
75
+
76
+
77
+ def installed(module):
78
+ from importlib.util import find_spec
79
+ try:
80
+ return find_spec(module) is not None
81
+ except (ImportError, ValueError): # A missing parent package raises instead of returning None.
82
+ return False
83
+
84
+
85
+ def suppress_contentless_genai_events():
86
+ # Content capture is always off here, yet botocore's Bedrock instrumentation still emits one GenAI event
87
+ # per prompt message and model choice with the content removed: hundreds of bodiless log records per
88
+ # agent run. The model spans already carry model, token usage and finish reasons, so those emitters get
89
+ # a no-op logger. They create loggers on first use from the global provider, which exists by now.
90
+ from opentelemetry._logs import NoOpLogger, get_logger_provider
91
+ provider = get_logger_provider()
92
+ original = provider.get_logger
93
+
94
+ def get_logger(name, *args, **kwargs):
95
+ if name.startswith("opentelemetry.instrumentation.botocore.bedrock"):
96
+ return NoOpLogger(name)
97
+ return original(name, *args, **kwargs)
98
+ provider.get_logger = get_logger
99
+
100
+
101
+ # GenAI span attributes and span events that carry prompt, response, instruction or tool payload text.
102
+ CONTENT_ATTRIBUTES = frozenset({
103
+ "gen_ai.input.messages", "gen_ai.output.messages", "gen_ai.system_instructions",
104
+ "gen_ai.tool.call.arguments", "gen_ai.tool.call.result", "gen_ai.prompt", "gen_ai.completion",
105
+ })
106
+ CONTENT_ATTRIBUTE_PREFIXES = ("gen_ai.prompt.", "gen_ai.completion.")
107
+ CONTENT_EVENTS = frozenset({"gen_ai.choice", "gen_ai.client.inference.operation.details"})
108
+
109
+
110
+ def is_content_attribute(key):
111
+ return key in CONTENT_ATTRIBUTES or key.startswith(CONTENT_ATTRIBUTE_PREFIXES)
112
+
113
+
114
+ def is_content_event(name):
115
+ return name in CONTENT_EVENTS or (name.startswith("gen_ai.") and name.endswith(".message"))
116
+
117
+
118
+ def redact_genai_content():
119
+ # Content capture is off, yet some frameworks record content regardless: ADOT's LangChain instrumentation
120
+ # writes prompts, responses and tool payloads as span attributes without consulting
121
+ # OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, and Strands' own tracer always adds the conversation as
122
+ # span events. Both are removed from every span as it ends, before any exporter receives it. The SDK freezes
123
+ # a span's attributes just before this hook runs, so they are unfrozen only to remove the content keys.
124
+ from opentelemetry import trace
125
+ from opentelemetry.sdk.trace import SpanProcessor
126
+
127
+ provider = trace.get_tracer_provider()
128
+ if not hasattr(provider, "add_span_processor"):
129
+ return
130
+
131
+ class GenAIContentRedaction(SpanProcessor):
132
+ def _on_ending(self, span):
133
+ try:
134
+ attributes = span._attributes
135
+ content = [key for key in attributes if is_content_attribute(key)]
136
+ if content:
137
+ attributes._immutable = False
138
+ try:
139
+ for key in content:
140
+ del attributes[key]
141
+ finally:
142
+ attributes._immutable = True
143
+ events = span._events
144
+ for event in [event for event in events if is_content_event(event.name)]:
145
+ events._dq.remove(event)
146
+ except Exception:
147
+ pass
148
+
149
+ provider.add_span_processor(GenAIContentRedaction())
150
+
151
+
152
+ def exclude_asgi_message_spans():
153
+ # The ASGI middleware adds an internal span for every message it sends or receives (response start, each
154
+ # body chunk, request body), several per request, each repeating the server span's name. The Starlette and
155
+ # FastAPI instrumentations expose no setting for this, so the middleware's own option becomes the default.
156
+ # The module is loaded only when an ASGI framework instrumentation was installed.
157
+ asgi = sys.modules.get("opentelemetry.instrumentation.asgi")
158
+ if asgi is None:
159
+ return
160
+ middleware = asgi.OpenTelemetryMiddleware
161
+ original = middleware.__init__
162
+
163
+ def __init__(self, *args, **kwargs):
164
+ kwargs.setdefault("exclude_spans", ["receive", "send"])
165
+ original(self, *args, **kwargs)
166
+ middleware.__init__ = __init__
167
+
168
+
169
+ def root_invocations_at_the_agent(environ=None):
170
+ # Every request reaches the agent through the AgentCore data plane, which replaces the caller's span with
171
+ # one of its own (in both traceparent and X-Amzn-Trace-Id) and exports that span only to X-Ray. Left as
172
+ # the parent, it is a span no backend receives. The agent's server span becomes the root instead: it keeps
173
+ # the trace ID, so an instrumented caller's trace still groups with the agent's, and links to AgentCore's
174
+ # span. AgentCore's session ID arrives as baggage and is recorded on the root. Returns whether it was installed:
175
+ # ODIGOS_AGENTCORE_ROOT_INVOCATIONS=false keeps AgentCore's parent, for backends that also ingest its spans.
176
+ environ = os.environ if environ is None else environ
177
+ if environ.get("ODIGOS_AGENTCORE_ROOT_INVOCATIONS", "true").lower() == "false":
178
+ return False
179
+ from opentelemetry import baggage, trace
180
+ from opentelemetry.sdk.trace import SpanProcessor
181
+ from opentelemetry.trace import SpanKind
182
+
183
+ provider = trace.get_tracer_provider()
184
+ if not hasattr(provider, "add_span_processor"):
185
+ return False
186
+
187
+ class AgentCoreInvocationRoot(SpanProcessor):
188
+ def on_start(self, span, parent_context=None):
189
+ try:
190
+ parent = span.parent
191
+ if span.kind != SpanKind.SERVER or parent is None or not parent.is_remote or not hasattr(span, "_parent"):
192
+ return
193
+ span._parent = None
194
+ span.add_link(parent, {"odigos.link.reason": "agentcore_invocation"})
195
+ session = baggage.get_baggage("session.id", parent_context)
196
+ if session:
197
+ span.set_attribute("session.id", str(session))
198
+ except Exception:
199
+ pass # Fail open: the span keeps AgentCore's parent.
200
+
201
+ provider.add_span_processor(AgentCoreInvocationRoot())
202
+ return True
@@ -0,0 +1,9 @@
1
+ """Dormant loader for the package installed into a direct-code ZIP (``pip install --target``).
2
+
3
+ A ZIP root is not a site directory, so ``odigos_agentcore.pth`` is not processed there. ``odigos_agentcore_start.py``
4
+ puts this directory first on ``PYTHONPATH`` and the ZIP root after it; Python imports this module at interpreter start,
5
+ before the application's own imports.
6
+ """
7
+ from odigos_agentcore.bootstrap import start
8
+
9
+ start()
@@ -0,0 +1 @@
1
+ import odigos_agentcore._autostart
@@ -0,0 +1,51 @@
1
+ """Start a direct-code AgentCore application with the Odigos bootstrap.
2
+
3
+ Make this file the runtime's entry point followed by the original one: ["odigos_agentcore_start.py", "app.py",
4
+ <original arguments>]. It finds the bootstrap relative to its own file, in any of these layouts:
5
+
6
+ - installed by ``pip install --target <zip root> odigos-agentcore[...]``: this module and the odigos_agentcore package
7
+ sit at the ZIP root;
8
+ - copied to the ZIP root next to a prepared odigos-agentcore/ bundle;
9
+ - used in place inside that bundle, as odigos-agentcore/bootstrap/python/odigos_agentcore_start.py.
10
+
11
+ The bootstrap stays dormant unless the runtime environment enables it, so the same ZIP runs uninstrumented too.
12
+ """
13
+ import os
14
+ import sys
15
+ from pathlib import Path
16
+
17
+
18
+ def locate(here):
19
+ """Return (ZIP root, directory holding the sitecustomize that loads the bootstrap), or None."""
20
+ bundle = here / "odigos-agentcore" / "bootstrap" / "python"
21
+ if bundle.is_dir():
22
+ return here, bundle
23
+ if here.parent.name == "bootstrap" and (here / "sitecustomize.py").is_file():
24
+ return here.parents[2], here
25
+ startup = here / "odigos_agentcore" / "startup"
26
+ if (startup / "sitecustomize.py").is_file():
27
+ return here, startup
28
+ return None
29
+
30
+
31
+ def main():
32
+ found = locate(Path(__file__).resolve().parent)
33
+ if found is None:
34
+ sys.stderr.write("odigos_agentcore_start.py: the Odigos AgentCore bootstrap is not next to this file.\n")
35
+ sys.exit(2)
36
+ root, bootstrap = found
37
+ if len(sys.argv) < 2:
38
+ sys.stderr.write("odigos_agentcore_start.py: pass the application's entry point as the next entry point element.\n")
39
+ sys.exit(2)
40
+ entry = root / sys.argv[1]
41
+ # sitecustomize runs at interpreter start, before Python adds the script's own directory to sys.path. The root
42
+ # follows it: the package layout imports odigos_agentcore and the SDK from there.
43
+ paths = [str(bootstrap), str(entry.parent), str(root)]
44
+ if os.environ.get("PYTHONPATH"):
45
+ paths.append(os.environ["PYTHONPATH"])
46
+ os.environ["PYTHONPATH"] = os.pathsep.join(dict.fromkeys(paths))
47
+ os.execv(sys.executable, [sys.executable, str(entry), *sys.argv[2:]])
48
+
49
+
50
+ if __name__ == "__main__":
51
+ main()
@@ -0,0 +1,48 @@
1
+ # Generated by agentcore/render_packages.py from versions.env and artifacts.json; do not edit.
2
+ #
3
+ # The wheel ships the odigos_agentcore package, the odigos_agentcore_start.py ZIP wrapper and odigos_agentcore.pth at
4
+ # its root. sitecustomize.py in this directory is the prepared bundle's loader and is not part of the wheel.
5
+
6
+ [build-system]
7
+ requires = ["hatchling>=1.27"]
8
+ build-backend = "hatchling.build"
9
+
10
+ [project]
11
+ name = "odigos-agentcore"
12
+ version = "0.0.25"
13
+ description = "Odigos OpenTelemetry bootstrap for Amazon Bedrock AgentCore Runtime agents"
14
+ license = "Apache-2.0"
15
+ requires-python = ">=3.10"
16
+ dependencies = []
17
+
18
+ # One extra per profile; the installed extra must match the distro the runtime selects.
19
+ [project.optional-dependencies]
20
+ adot = [
21
+ "aws-opentelemetry-distro==0.21.0",
22
+ ]
23
+ openllmetry = [
24
+ "opentelemetry-distro==0.66b1",
25
+ "opentelemetry-sdk==1.45.1",
26
+ "opentelemetry-exporter-otlp-proto-http==1.45.1",
27
+ "opentelemetry-instrumentation-bedrock==0.62.3",
28
+ "opentelemetry-propagator-aws-xray==1.0.2",
29
+ "opentelemetry-instrumentation-asgi==0.66b1",
30
+ "opentelemetry-instrumentation-starlette==0.66b1",
31
+ "opentelemetry-instrumentation-fastapi==0.66b1",
32
+ "opentelemetry-instrumentation-aiohttp-client==0.66b1",
33
+ "opentelemetry-instrumentation-httpx==0.66b1",
34
+ "opentelemetry-instrumentation-requests==0.66b1",
35
+ "opentelemetry-instrumentation-urllib3==0.66b1",
36
+ "opentelemetry-instrumentation-logging==0.66b1",
37
+ "opentelemetry-instrumentation-threading==0.66b1",
38
+ ]
39
+
40
+ [tool.hatch.build.targets.wheel]
41
+ packages = ["odigos_agentcore"]
42
+
43
+ [tool.hatch.build.targets.wheel.force-include]
44
+ "odigos_agentcore.pth" = "odigos_agentcore.pth"
45
+ "odigos_agentcore_start.py" = "odigos_agentcore_start.py"
46
+
47
+ [tool.hatch.build.targets.sdist]
48
+ include = ["odigos_agentcore", "odigos_agentcore.pth", "odigos_agentcore_start.py", "pyproject.toml"]