monkeybot 2.1.1__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 (178) hide show
  1. monkeybot/__init__.py +3 -0
  2. monkeybot/cli/__init__.py +3 -0
  3. monkeybot/cli/__main__.py +8 -0
  4. monkeybot/cli/audio_io.py +8 -0
  5. monkeybot/cli/gateway_manager.py +17 -0
  6. monkeybot/cli/main.py +22 -0
  7. monkeybot/cli/push_to_talk.py +12 -0
  8. monkeybot/cli/realtime_client.py +13 -0
  9. monkeybot/core/__init__.py +19 -0
  10. monkeybot/core/attachments/__init__.py +22 -0
  11. monkeybot/core/attachments/catalog.py +62 -0
  12. monkeybot/core/attachments/config.py +52 -0
  13. monkeybot/core/attachments/freeze.py +158 -0
  14. monkeybot/core/attachments/resolve.py +70 -0
  15. monkeybot/core/attachments/store.py +180 -0
  16. monkeybot/core/attachments/text.py +72 -0
  17. monkeybot/core/attachments/tools.py +54 -0
  18. monkeybot/core/bootstrap.py +242 -0
  19. monkeybot/core/config/__init__.py +71 -0
  20. monkeybot/core/config/realtime_config.py +150 -0
  21. monkeybot/core/config/runtime_env.py +262 -0
  22. monkeybot/core/config/settings.py +341 -0
  23. monkeybot/core/config/validation.py +249 -0
  24. monkeybot/core/config/yaml_loader.py +45 -0
  25. monkeybot/core/context/__init__.py +781 -0
  26. monkeybot/core/context/campaign_context.py +8 -0
  27. monkeybot/core/context/common.py +14 -0
  28. monkeybot/core/context/curator.py +255 -0
  29. monkeybot/core/context/epoch.py +226 -0
  30. monkeybot/core/context/memory_prompt.py +222 -0
  31. monkeybot/core/context/tool_output_policy.py +270 -0
  32. monkeybot/core/context/tool_result_ingress.py +290 -0
  33. monkeybot/core/context/tool_shapers.py +361 -0
  34. monkeybot/core/hooks/__init__.py +261 -0
  35. monkeybot/core/llm/__init__.py +4 -0
  36. monkeybot/core/llm/provider.py +296 -0
  37. monkeybot/core/llm/realtime_provider.py +203 -0
  38. monkeybot/core/llm/usage.py +57 -0
  39. monkeybot/core/logging_utils.py +24 -0
  40. monkeybot/core/mcp/__init__.py +1 -0
  41. monkeybot/core/mcp/mcp_client.py +1215 -0
  42. monkeybot/core/mcp/ports_mcp.py +109 -0
  43. monkeybot/core/memory/__init__.py +24 -0
  44. monkeybot/core/memory/hook.py +413 -0
  45. monkeybot/core/memory/index_format.py +104 -0
  46. monkeybot/core/memory/integrity.py +180 -0
  47. monkeybot/core/memory/organizer.py +270 -0
  48. monkeybot/core/memory/storage_ops.py +139 -0
  49. monkeybot/core/memory/subsystem.py +91 -0
  50. monkeybot/core/messages/__init__.py +16 -0
  51. monkeybot/core/messages/convert_provider.py +41 -0
  52. monkeybot/core/messages/tool_integrity.py +262 -0
  53. monkeybot/core/messages/transform_context.py +84 -0
  54. monkeybot/core/path_safety.py +11 -0
  55. monkeybot/core/persistence/__init__.py +17 -0
  56. monkeybot/core/persistence/backends.py +236 -0
  57. monkeybot/core/persistence/db.py +28 -0
  58. monkeybot/core/persistence/durable_runs.py +286 -0
  59. monkeybot/core/persistence/firestore.py +658 -0
  60. monkeybot/core/persistence/firestore_scheduled_loops.py +336 -0
  61. monkeybot/core/persistence/history.py +156 -0
  62. monkeybot/core/persistence/postgres.py +895 -0
  63. monkeybot/core/persistence/runs.py +76 -0
  64. monkeybot/core/persistence/scheduled_loops.py +435 -0
  65. monkeybot/core/persistence/session_turn_locks.py +94 -0
  66. monkeybot/core/persistence/sqlite.py +218 -0
  67. monkeybot/core/persistence/sqlite_backend.py +74 -0
  68. monkeybot/core/persistence/thread_summary.py +61 -0
  69. monkeybot/core/persistence/transcript.py +194 -0
  70. monkeybot/core/persistence/usage.py +149 -0
  71. monkeybot/core/prompts/__init__.py +1 -0
  72. monkeybot/core/prompts/harness_prompt.py +197 -0
  73. monkeybot/core/prompts/prompt.py +215 -0
  74. monkeybot/core/runtime/__init__.py +1 -0
  75. monkeybot/core/runtime/context_budget.py +267 -0
  76. monkeybot/core/runtime/events.py +819 -0
  77. monkeybot/core/runtime/input_admission.py +154 -0
  78. monkeybot/core/runtime/loop.py +2374 -0
  79. monkeybot/core/runtime/provider_stream_mapper.py +159 -0
  80. monkeybot/core/runtime/realtime_loop.py +654 -0
  81. monkeybot/core/runtime/utterance_buffer.py +179 -0
  82. monkeybot/core/subagents/__init__.py +1 -0
  83. monkeybot/core/subagents/subagent_proto.py +331 -0
  84. monkeybot/core/subagents/subagent_worker.py +441 -0
  85. monkeybot/core/subagents/worker_pool.py +403 -0
  86. monkeybot/core/testing/__init__.py +1 -0
  87. monkeybot/core/testing/mocks_provider.py +86 -0
  88. monkeybot/core/testing/mocks_realtime_provider.py +137 -0
  89. monkeybot/core/tools/__init__.py +1 -0
  90. monkeybot/core/tools/core_tool_executor.py +1548 -0
  91. monkeybot/core/tools/inspector.py +226 -0
  92. monkeybot/core/tools/loop_inspector.py +45 -0
  93. monkeybot/core/tools/patch.py +480 -0
  94. monkeybot/core/tools/permission.py +284 -0
  95. monkeybot/core/tools/sandbox_executor.py +255 -0
  96. monkeybot/core/tools/spill_inventory.py +35 -0
  97. monkeybot/core/tools/terminal.py +381 -0
  98. monkeybot/core/tools/text_normalize.py +25 -0
  99. monkeybot/core/tools/types.py +33 -0
  100. monkeybot/core/tools/workspace_service.py +710 -0
  101. monkeybot/core/tools/workspace_tools.py +116 -0
  102. monkeybot/core/types/__init__.py +1 -0
  103. monkeybot/core/types/content_blocks.py +644 -0
  104. monkeybot/core/types/interfaces.py +156 -0
  105. monkeybot/core/types/types_tools.py +29 -0
  106. monkeybot/core/workspace/__init__.py +8 -0
  107. monkeybot/core/workspace/factory.py +45 -0
  108. monkeybot/core/workspace/gcs.py +130 -0
  109. monkeybot/core/workspace/local.py +162 -0
  110. monkeybot/core/workspace/protocol.py +45 -0
  111. monkeybot/core/workspace/s3.py +151 -0
  112. monkeybot/core/workspace_layout.py +27 -0
  113. monkeybot/gateway/__init__.py +1 -0
  114. monkeybot/gateway/bootstrap.py +18 -0
  115. monkeybot/gateway/main.py +47 -0
  116. monkeybot/gateway/realtime/__init__.py +31 -0
  117. monkeybot/gateway/realtime/app.py +321 -0
  118. monkeybot/gateway/realtime/deps.py +52 -0
  119. monkeybot/gateway/realtime/errors.py +81 -0
  120. monkeybot/gateway/realtime/guardrails.py +88 -0
  121. monkeybot/gateway/realtime/manager.py +77 -0
  122. monkeybot/gateway/realtime/metrics.py +144 -0
  123. monkeybot/gateway/realtime/routes.py +864 -0
  124. monkeybot/gateway/realtime/session.py +232 -0
  125. monkeybot/gateway/realtime/wire.py +412 -0
  126. monkeybot/gateway/realtime_main.py +49 -0
  127. monkeybot/gateway/sse/__init__.py +1 -0
  128. monkeybot/gateway/sse/app.py +733 -0
  129. monkeybot/gateway/sse/loop_port.py +31 -0
  130. monkeybot/gateway/sse/models.py +177 -0
  131. monkeybot/gateway/sse/reply_body.py +91 -0
  132. monkeybot/gateway/sse/routes.py +1101 -0
  133. monkeybot/gateway/sse/scheduler_routes.py +200 -0
  134. monkeybot/gateway/sse/scheduler_wiring.py +96 -0
  135. monkeybot/gateway/sse/session_bus.py +226 -0
  136. monkeybot/gateway/sse/sse.py +46 -0
  137. monkeybot/gateway/sse/workspace_layout.py +7 -0
  138. monkeybot/observability/__init__.py +220 -0
  139. monkeybot/observability/_state.py +10 -0
  140. monkeybot/observability/instrumentation.py +153 -0
  141. monkeybot/observability/propagation.py +65 -0
  142. monkeybot/observability/spans.py +455 -0
  143. monkeybot/providers/__init__.py +19 -0
  144. monkeybot/providers/_openai_compat.py +450 -0
  145. monkeybot/providers/_utils.py +473 -0
  146. monkeybot/providers/bedrock.py +145 -0
  147. monkeybot/providers/claude.py +125 -0
  148. monkeybot/providers/gemini.py +677 -0
  149. monkeybot/providers/gemini_live.py +398 -0
  150. monkeybot/providers/huggingface.py +129 -0
  151. monkeybot/providers/nvidia.py +104 -0
  152. monkeybot/providers/ollama.py +152 -0
  153. monkeybot/providers/openai.py +127 -0
  154. monkeybot/providers/pricing.py +60 -0
  155. monkeybot/providers/sampling.py +44 -0
  156. monkeybot/providers/vertex_claude.py +148 -0
  157. monkeybot/scaffold/__init__.py +33 -0
  158. monkeybot/scheduler/__init__.py +13 -0
  159. monkeybot/scheduler/__main__.py +4 -0
  160. monkeybot/scheduler/engine.py +333 -0
  161. monkeybot/scheduler/http_invoker.py +61 -0
  162. monkeybot/scheduler/interval.py +77 -0
  163. monkeybot/scheduler/tick_result.py +34 -0
  164. monkeybot/scheduler/worker.py +87 -0
  165. monkeybot/subagents/__init__.py +1 -0
  166. monkeybot/subagents/worker/__init__.py +1 -0
  167. monkeybot/subagents/worker/__main__.py +22 -0
  168. monkeybot/web_search/__init__.py +82 -0
  169. monkeybot/web_search/backends/__init__.py +5 -0
  170. monkeybot/web_search/backends/duckduckgo.py +32 -0
  171. monkeybot/web_search/backends/firecrawl.py +43 -0
  172. monkeybot/web_search/backends/tavily.py +45 -0
  173. monkeybot/web_search/protocol.py +25 -0
  174. monkeybot/web_search/tool.py +56 -0
  175. monkeybot-2.1.1.dist-info/METADATA +318 -0
  176. monkeybot-2.1.1.dist-info/RECORD +178 -0
  177. monkeybot-2.1.1.dist-info/WHEEL +4 -0
  178. monkeybot-2.1.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,220 @@
1
+ """OpenTelemetry tracing for monkeybot (optional ``[observability]`` extra).
2
+
3
+ Master switch: ``MONKEYBOT_OTEL_ENABLED`` must be truthy to opt in. When unset,
4
+ tracing stays disabled even if OTel packages are installed (explicit rollout).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import importlib
10
+ import importlib.metadata
11
+ import logging
12
+ import os
13
+ from typing import Any
14
+
15
+ from monkeybot.observability import _state
16
+ from monkeybot.observability.instrumentation import (
17
+ ObservingProvider,
18
+ add_tool_hook_span_event,
19
+ get_current_trace_id_hex_optional,
20
+ instrument_fastapi_app,
21
+ )
22
+ from monkeybot.observability.propagation import extract_traceparent, inject_traceparent
23
+ from monkeybot.observability.spans import (
24
+ set_llm_io,
25
+ set_llm_usage,
26
+ set_run_output,
27
+ set_summarize_turns,
28
+ set_turn_io,
29
+ set_turn_prompt_tokens,
30
+ span_llm,
31
+ span_run,
32
+ span_subagent,
33
+ span_summarize,
34
+ span_tool,
35
+ span_turn,
36
+ truncate,
37
+ )
38
+
39
+ logger = logging.getLogger(__name__)
40
+
41
+ _TRUTHY = frozenset({"1", "true", "yes", "on"})
42
+
43
+
44
+ def _env_truthy(name: str) -> bool:
45
+ raw = os.environ.get(name)
46
+ if raw is None:
47
+ return False
48
+ return raw.strip().lower() in _TRUTHY
49
+
50
+
51
+ def _env_sdk_disabled() -> bool:
52
+ return os.environ.get("OTEL_SDK_DISABLED", "").strip().lower() in _TRUTHY
53
+
54
+
55
+ def is_observability_enabled() -> bool:
56
+ return _state.is_observability_enabled()
57
+
58
+
59
+ def _create_span_processor(exporter: str) -> Any:
60
+ """Build a span processor for ``otlp`` or ``console``. Tests may monkeypatch this."""
61
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor
62
+
63
+ if exporter == "otlp":
64
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
65
+
66
+ return BatchSpanProcessor(OTLPSpanExporter())
67
+ if exporter == "console":
68
+ from opentelemetry.sdk.trace.export import ConsoleSpanExporter
69
+
70
+ return BatchSpanProcessor(ConsoleSpanExporter())
71
+ return None
72
+
73
+
74
+ def init_observability() -> bool:
75
+ if _state._initialized:
76
+ return _state._enabled
77
+
78
+ _state._initialized = True
79
+ _state._enabled = False
80
+
81
+ if not _env_truthy("MONKEYBOT_OTEL_ENABLED"):
82
+ logger.info("observability disabled (MONKEYBOT_OTEL_ENABLED is not set or false)")
83
+ return False
84
+
85
+ if _env_sdk_disabled():
86
+ logger.info("observability disabled (OTEL_SDK_DISABLED=true)")
87
+ return False
88
+
89
+ try:
90
+ trace_api = importlib.import_module("opentelemetry.trace")
91
+ resources = importlib.import_module("opentelemetry.sdk.resources")
92
+ trace_sdk = importlib.import_module("opentelemetry.sdk.trace")
93
+ w3c = importlib.import_module("opentelemetry.trace.propagation.tracecontext")
94
+ propagate = importlib.import_module("opentelemetry.propagate")
95
+ except ImportError:
96
+ logger.warning("observability disabled (OpenTelemetry packages not installed)")
97
+ return False
98
+
99
+ exporter = os.environ.get("OTEL_TRACES_EXPORTER", "").strip().lower()
100
+ if exporter not in {"otlp", "console"}:
101
+ logger.info(
102
+ "observability disabled (OTEL_TRACES_EXPORTER must be otlp or console, got %r)",
103
+ exporter or "<unset>",
104
+ )
105
+ return False
106
+
107
+ service_name = os.environ.get("OTEL_SERVICE_NAME", "monkeybot")
108
+ attrs: dict[str, str] = {"service.name": service_name}
109
+ try:
110
+ version = importlib.metadata.version("monkeybot")
111
+ except importlib.metadata.PackageNotFoundError:
112
+ version = None
113
+ if version:
114
+ attrs["service.version"] = version
115
+
116
+ try:
117
+ processor = _create_span_processor(exporter)
118
+ if processor is None:
119
+ return False
120
+ resource = resources.Resource.create(attrs)
121
+ provider = trace_sdk.TracerProvider(resource=resource)
122
+ provider.add_span_processor(processor)
123
+ trace_api.set_tracer_provider(provider)
124
+ propagate.set_global_textmap(w3c.TraceContextTextMapPropagator())
125
+ _state._enabled = True
126
+ logger.info("observability enabled (exporter=%s, service=%s)", exporter, service_name)
127
+ return True
128
+ except Exception as exc:
129
+ logger.warning("observability init failed: %s", exc)
130
+ return False
131
+
132
+
133
+ def shutdown_observability() -> None:
134
+ if not _state._initialized:
135
+ return
136
+ try:
137
+ if _state._enabled:
138
+ trace_api = importlib.import_module("opentelemetry.trace")
139
+ provider = trace_api.get_tracer_provider()
140
+ shutdown = getattr(provider, "shutdown", None)
141
+ if callable(shutdown):
142
+ try:
143
+ shutdown(timeout_millis=5000)
144
+ except TypeError:
145
+ shutdown()
146
+ except Exception as exc:
147
+ logger.warning("observability shutdown failed: %s", exc)
148
+ finally:
149
+ _state._initialized = False
150
+ _state._enabled = False
151
+
152
+
153
+ class _NoOpTracer:
154
+ def start_span(self, *_a: Any, **_k: Any) -> _NoOpSpan:
155
+ return _NoOpSpan()
156
+
157
+ def start_as_current_span(self, *_a: Any, **_k: Any) -> _NoOpSpan:
158
+ return _NoOpSpan()
159
+
160
+
161
+ class _NoOpSpan:
162
+ def __enter__(self) -> _NoOpSpan:
163
+ return self
164
+
165
+ def __exit__(self, *_a: Any) -> None:
166
+ return None
167
+
168
+ def set_attribute(self, *_a: Any, **_k: Any) -> None:
169
+ return None
170
+
171
+ def record_exception(self, *_a: Any, **_k: Any) -> None:
172
+ return None
173
+
174
+ def set_status(self, *_a: Any, **_k: Any) -> None:
175
+ return None
176
+
177
+ def end(self) -> None:
178
+ return None
179
+
180
+ def is_recording(self) -> bool:
181
+ return False
182
+
183
+
184
+ def get_tracer(name: str = "monkeybot") -> Any:
185
+ if not _state._enabled:
186
+ try:
187
+ from opentelemetry.trace import NoOpTracer
188
+
189
+ return NoOpTracer()
190
+ except ImportError:
191
+ return _NoOpTracer()
192
+ trace_api = importlib.import_module("opentelemetry.trace")
193
+ return trace_api.get_tracer(name)
194
+
195
+
196
+ __all__ = [
197
+ "ObservingProvider",
198
+ "add_tool_hook_span_event",
199
+ "extract_traceparent",
200
+ "get_current_trace_id_hex_optional",
201
+ "instrument_fastapi_app",
202
+ "get_tracer",
203
+ "init_observability",
204
+ "inject_traceparent",
205
+ "is_observability_enabled",
206
+ "set_llm_io",
207
+ "set_llm_usage",
208
+ "set_run_output",
209
+ "set_summarize_turns",
210
+ "set_turn_io",
211
+ "set_turn_prompt_tokens",
212
+ "shutdown_observability",
213
+ "span_llm",
214
+ "span_run",
215
+ "span_subagent",
216
+ "span_summarize",
217
+ "span_tool",
218
+ "span_turn",
219
+ "truncate",
220
+ ]
@@ -0,0 +1,10 @@
1
+ """Process-local observability flags (no OpenTelemetry imports)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ _initialized = False
6
+ _enabled = False
7
+
8
+
9
+ def is_observability_enabled() -> bool:
10
+ return _enabled
@@ -0,0 +1,153 @@
1
+ """Extended instrumentation: provider wrapper, FastAPI, hook span events, trace id helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from collections.abc import AsyncIterator, Sequence
7
+ from typing import TYPE_CHECKING, Any, Literal, cast
8
+
9
+ from monkeybot.core.context import TurnContext
10
+ from monkeybot.core.llm.provider import (
11
+ Message,
12
+ Provider,
13
+ ProviderEvent,
14
+ ToolDef,
15
+ provider_count_input_tokens,
16
+ provider_stream,
17
+ )
18
+ from monkeybot.observability._state import is_observability_enabled
19
+ from monkeybot.observability.spans import span_llm
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+ if TYPE_CHECKING:
24
+ from fastapi import FastAPI
25
+
26
+ HookPhase = Literal["pre_tool", "post_tool"]
27
+
28
+
29
+ class ObservingProvider:
30
+ """Wraps a :class:`~monkeybot.core.llm.provider.Provider` for direct (non-loop) ``stream`` calls."""
31
+
32
+ def __init__(self, inner: Provider, *, turn_ctx: TurnContext | None = None) -> None:
33
+ self._inner = inner
34
+ self._turn_ctx = turn_ctx
35
+
36
+ @property
37
+ def name(self) -> str:
38
+ return self._inner.name
39
+
40
+ @property
41
+ def supports_streaming(self) -> bool:
42
+ return self._inner.supports_streaming
43
+
44
+ async def count_input_tokens(
45
+ self,
46
+ messages: Sequence[Message],
47
+ tools: Sequence[ToolDef],
48
+ *,
49
+ model: str,
50
+ thinking_budget: int | None = None,
51
+ vertex_google_search: bool = False,
52
+ ) -> int:
53
+ return await provider_count_input_tokens(
54
+ self._inner,
55
+ messages,
56
+ tools,
57
+ model=model,
58
+ thinking_budget=thinking_budget,
59
+ vertex_google_search=vertex_google_search,
60
+ )
61
+
62
+ def stream(
63
+ self,
64
+ messages: Sequence[Message],
65
+ tools: Sequence[ToolDef],
66
+ *,
67
+ model: str,
68
+ thinking_budget: int | None = None,
69
+ vertex_google_search: bool = False,
70
+ ) -> AsyncIterator[ProviderEvent]:
71
+ inner_stream = provider_stream(
72
+ self._inner,
73
+ messages,
74
+ tools,
75
+ model=model,
76
+ thinking_budget=thinking_budget,
77
+ vertex_google_search=vertex_google_search,
78
+ )
79
+ if not is_observability_enabled():
80
+ return inner_stream
81
+
82
+ async def _observed() -> AsyncIterator[ProviderEvent]:
83
+ ctx = self._correlation_ctx(model)
84
+ async with span_llm(
85
+ ctx=ctx,
86
+ model=model,
87
+ vertex_google_search=vertex_google_search,
88
+ ):
89
+ async for evt in inner_stream:
90
+ yield evt
91
+
92
+ return _observed()
93
+
94
+ def _correlation_ctx(self, model: str) -> TurnContext:
95
+ if self._turn_ctx is not None:
96
+ return self._turn_ctx
97
+ return TurnContext(
98
+ thread_id="direct",
99
+ request_id="direct",
100
+ agent_md="",
101
+ memory_index=[],
102
+ skills=[],
103
+ tools=[],
104
+ user_id=None,
105
+ parent_run_id=None,
106
+ model=model,
107
+ )
108
+
109
+
110
+ def instrument_fastapi_app(app: object) -> None:
111
+ """Instrument a FastAPI app with OTel HTTP server spans (idempotent, defensive)."""
112
+ if not is_observability_enabled():
113
+ return
114
+ if getattr(app, "_monkeybot_otel_fastapi_instrumented", False):
115
+ return
116
+ try:
117
+ from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
118
+ except ImportError:
119
+ logger.warning(
120
+ "FastAPI OpenTelemetry instrumentation unavailable (install [observability] extra)"
121
+ )
122
+ return
123
+ FastAPIInstrumentor().instrument_app(cast("FastAPI", app))
124
+ cast(Any, app)._monkeybot_otel_fastapi_instrumented = True
125
+
126
+
127
+ def get_current_trace_id_hex_optional() -> str | None:
128
+ """Return W3C trace id (32 lower-hex chars) for the current span, or ``None``."""
129
+ if not is_observability_enabled():
130
+ return None
131
+ from opentelemetry import trace
132
+ from opentelemetry.trace import format_trace_id
133
+
134
+ span = trace.get_current_span()
135
+ ctx = span.get_span_context()
136
+ if not ctx.is_valid:
137
+ return None
138
+ return format_trace_id(ctx.trace_id)
139
+
140
+
141
+ def add_tool_hook_span_event(*, phase: HookPhase, tool_name: str) -> None:
142
+ """Record a hook lifecycle event on the active span (``monkeybot.tool`` when in loop)."""
143
+ if not is_observability_enabled():
144
+ return
145
+ from opentelemetry import trace
146
+
147
+ event_name = (
148
+ "monkeybot.hook.pre_tool" if phase == "pre_tool" else "monkeybot.hook.post_tool"
149
+ )
150
+ span = trace.get_current_span()
151
+ if not span.is_recording():
152
+ return
153
+ span.add_event(event_name, {"tool.name": tool_name})
@@ -0,0 +1,65 @@
1
+ """W3C trace context propagation for subagent subprocesses."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from typing import TYPE_CHECKING
7
+
8
+ from monkeybot.observability._state import is_observability_enabled
9
+
10
+ if TYPE_CHECKING:
11
+ from opentelemetry.context import Context
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ def inject_traceparent(carrier: dict[str, str]) -> None:
17
+ """Inject W3C traceparent (and tracestate if present) from the current context.
18
+
19
+ No-op when observability is disabled, OTel is unavailable, or the current span
20
+ context is invalid. Never raises.
21
+ """
22
+ if not is_observability_enabled():
23
+ return
24
+ try:
25
+ from opentelemetry import propagate, trace
26
+ except ImportError:
27
+ return
28
+ try:
29
+ span_ctx = trace.get_current_span().get_span_context()
30
+ if not span_ctx.is_valid:
31
+ return
32
+ propagate.inject(carrier)
33
+ except Exception:
34
+ return
35
+
36
+
37
+ def extract_traceparent(carrier: dict[str, str]) -> Context | None:
38
+ """Extract an OTel Context from carrier keys ``traceparent`` / ``tracestate``.
39
+
40
+ Returns None when the value is missing, malformed, or yields a non-valid span context.
41
+ Logs a WARNING once per bad value (include no user content). Never raises.
42
+ """
43
+ raw = carrier.get("traceparent")
44
+ if raw is None:
45
+ return None
46
+ if not isinstance(raw, str):
47
+ logger.warning("malformed traceparent in carrier: expected string")
48
+ return None
49
+ traceparent = raw.strip()
50
+ if not traceparent:
51
+ return None
52
+ try:
53
+ from opentelemetry import propagate, trace
54
+ except ImportError:
55
+ return None
56
+ try:
57
+ ctx = propagate.extract(carrier)
58
+ span_ctx = trace.get_current_span(ctx).get_span_context()
59
+ if not span_ctx.is_valid:
60
+ logger.warning("malformed traceparent: invalid span context after extract")
61
+ return None
62
+ return ctx
63
+ except Exception:
64
+ logger.warning("malformed traceparent: propagation extract failed")
65
+ return None