flightdeck-sensor 0.5.0__tar.gz → 0.5.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/PKG-INFO +1 -1
  2. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/__init__.py +2 -0
  3. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/core/agent_id.py +1 -4
  4. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/core/context.py +10 -13
  5. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/core/errors.py +1 -5
  6. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/core/schemas.py +3 -1
  7. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/core/session.py +7 -1
  8. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/core/types.py +8 -1
  9. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/interceptor/anthropic.py +17 -5
  10. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/interceptor/base.py +26 -22
  11. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/interceptor/crewai.py +1 -0
  12. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/interceptor/langgraph.py +1 -0
  13. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/interceptor/litellm.py +38 -11
  14. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/interceptor/openai.py +13 -8
  15. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/provider.py +1 -0
  16. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/providers/anthropic.py +2 -0
  17. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/providers/litellm.py +4 -4
  18. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/providers/openai.py +2 -3
  19. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/transport/client.py +3 -8
  20. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/pyproject.toml +8 -1
  21. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/conftest.py +5 -4
  22. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_agent_id.py +14 -37
  23. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_agent_type_validation.py +7 -10
  24. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_compat_crewai_mcp.py +6 -4
  25. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_cross_agent_messages.py +8 -5
  26. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_custom_directives.py +1 -5
  27. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_errors.py +28 -9
  28. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_framework_attribution.py +27 -10
  29. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_interceptor.py +36 -20
  30. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_interceptor_crewai.py +9 -6
  31. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_interceptor_langgraph.py +4 -4
  32. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_llm_event_enrichment.py +94 -38
  33. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_mcp_interceptor.py +27 -11
  34. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_patch.py +24 -27
  35. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_policy.py +18 -8
  36. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_policy_events.py +13 -31
  37. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_prompt_capture.py +12 -4
  38. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_provider_enum.py +4 -2
  39. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_providers.py +17 -15
  40. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_transport.py +18 -22
  41. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/.gitignore +0 -0
  42. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/Makefile +0 -0
  43. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/README.md +0 -0
  44. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/compat/__init__.py +0 -0
  45. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/compat/crewai_mcp.py +0 -0
  46. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/config.py +0 -0
  47. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/core/__init__.py +0 -0
  48. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/core/exceptions.py +0 -0
  49. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/core/mcp_policy.py +0 -0
  50. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/core/policy.py +0 -0
  51. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/interceptor/__init__.py +0 -0
  52. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/interceptor/mcp.py +0 -0
  53. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/interceptor/mcp_identity.py +0 -0
  54. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/providers/__init__.py +0 -0
  55. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/providers/protocol.py +0 -0
  56. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/py.typed +0 -0
  57. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/transport/__init__.py +0 -0
  58. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/transport/retry.py +0 -0
  59. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/__init__.py +0 -0
  60. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/__init__.py +0 -0
  61. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_context.py +0 -0
  62. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_d150_tool_capture_destination.py +0 -0
  63. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_mcp_identity.py +0 -0
  64. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_mcp_policy_emission_enriched.py +0 -0
  65. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_mcp_policy_enforcement.py +0 -0
  66. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_mcp_policy_enforcement_all_paths.py +0 -0
  67. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_originating_event_id.py +0 -0
  68. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_policy_decision_summary.py +0 -0
  69. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_policy_emission_enriched.py +0 -0
  70. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_session.py +0 -0
  71. {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_session_lifecycle_enrichment.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flightdeck-sensor
3
- Version: 0.5.0
3
+ Version: 0.5.2
4
4
  Summary: In-process agent observability sensor for Flightdeck
5
5
  License-Expression: Apache-2.0
6
6
  Classifier: Development Status :: 3 - Alpha
@@ -118,6 +118,8 @@ def _resolve_user_name() -> str:
118
118
 
119
119
  return pwd.getpwuid(os.getuid()).pw_name
120
120
  except Exception:
121
+ # pwd missing or UID unmapped (distroless, scratch, Windows):
122
+ # fall through to the env-var probes below.
121
123
  pass
122
124
  for key in ("USER", "USERNAME", "LOGNAME"):
123
125
  v = os.environ.get(key)
@@ -74,10 +74,7 @@ def derive_agent_id(
74
74
  Returns a :class:`uuid.UUID`. Callers that need the canonical
75
75
  string form use ``str(derive_agent_id(...))``.
76
76
  """
77
- path = (
78
- f"flightdeck://{agent_type}/{user}@{hostname}"
79
- f"/{client_type}/{agent_name}"
80
- )
77
+ path = f"flightdeck://{agent_type}/{user}@{hostname}/{client_type}/{agent_name}"
81
78
  if agent_role is not None:
82
79
  trimmed = agent_role.strip()
83
80
  if trimmed:
@@ -33,6 +33,7 @@ import re
33
33
  import socket
34
34
  import subprocess
35
35
  import sys
36
+ from pathlib import Path
36
37
  from typing import Any, Protocol
37
38
 
38
39
  # Subprocess timeout for ``git`` invocations in GitCollector (M-5).
@@ -80,7 +81,7 @@ class ProcessCollector(BaseCollector):
80
81
  result["pid"] = os.getpid()
81
82
  argv0 = sys.argv[0] if sys.argv else ""
82
83
  if argv0:
83
- result["process_name"] = os.path.basename(argv0)
84
+ result["process_name"] = Path(argv0).name
84
85
  return result
85
86
 
86
87
 
@@ -96,11 +97,7 @@ class OSCollector(BaseCollector):
96
97
  class UserCollector(BaseCollector):
97
98
  def _gather(self) -> dict[str, Any]:
98
99
  # USER on Unix, USERNAME on Windows, LOGNAME as final fallback.
99
- user = (
100
- os.environ.get("USER")
101
- or os.environ.get("USERNAME")
102
- or os.environ.get("LOGNAME")
103
- )
100
+ user = os.environ.get("USER") or os.environ.get("USERNAME") or os.environ.get("LOGNAME")
104
101
  return {"user": user} if user else {}
105
102
 
106
103
 
@@ -149,7 +146,7 @@ class GitCollector(BaseCollector):
149
146
  if remote:
150
147
  # Strip embedded credentials before extracting the repo name.
151
148
  clean = re.sub(r"https?://[^@]+@", "https://", remote)
152
- repo = os.path.basename(clean.rstrip("/"))
149
+ repo = Path(clean.rstrip("/")).name
153
150
  if repo.endswith(".git"):
154
151
  repo = repo[:-4]
155
152
  if repo:
@@ -186,10 +183,7 @@ class KubernetesCollector(BaseCollector):
186
183
 
187
184
  class DockerComposeCollector(BaseCollector):
188
185
  def applies(self) -> bool:
189
- return bool(
190
- os.environ.get("COMPOSE_PROJECT_NAME")
191
- or os.environ.get("COMPOSE_SERVICE")
192
- )
186
+ return bool(os.environ.get("COMPOSE_PROJECT_NAME") or os.environ.get("COMPOSE_SERVICE"))
193
187
 
194
188
  def _gather(self) -> dict[str, Any]:
195
189
  result: dict[str, Any] = {"orchestration": "docker-compose"}
@@ -207,7 +201,7 @@ class DockerCollector(BaseCollector):
207
201
  # /.dockerenv only exists inside Linux containers; on Windows
208
202
  # or Mac hosts running Python natively this collector
209
203
  # correctly returns nothing.
210
- return os.path.exists("/.dockerenv")
204
+ return Path("/.dockerenv").exists()
211
205
 
212
206
  def _gather(self) -> dict[str, Any]:
213
207
  return {"orchestration": "docker"}
@@ -224,7 +218,7 @@ class AWSECSCollector(BaseCollector):
224
218
  result: dict[str, Any] = {"orchestration": "aws-ecs"}
225
219
  task = os.environ.get("ECS_TASK_DEFINITION")
226
220
  if task:
227
- result["ecs_task"] = os.path.basename(task)
221
+ result["ecs_task"] = Path(task).name
228
222
  return result
229
223
 
230
224
 
@@ -397,6 +391,7 @@ def collect() -> dict[str, Any]:
397
391
  if collector.applies():
398
392
  ctx.update(collector.collect())
399
393
  except Exception:
394
+ # best-effort: a single broken collector must never crash init().
400
395
  pass
401
396
 
402
397
  for collector in ORCHESTRATION_COLLECTORS:
@@ -405,6 +400,7 @@ def collect() -> dict[str, Any]:
405
400
  ctx.update(collector.collect())
406
401
  break
407
402
  except Exception:
403
+ # best-effort: a single broken collector must never crash init().
408
404
  pass
409
405
 
410
406
  for collector in OTHER_COLLECTORS:
@@ -412,6 +408,7 @@ def collect() -> dict[str, Any]:
412
408
  if collector.applies():
413
409
  ctx.update(collector.collect())
414
410
  except Exception:
411
+ # best-effort: a single broken collector must never crash init().
415
412
  pass
416
413
 
417
414
  return ctx
@@ -166,11 +166,7 @@ def _override_from_status_and_code(
166
166
  # OpenAI sometimes returns 503 "engine overloaded"; distinguish on
167
167
  # the message hint when present. The combined condition keeps ruff's
168
168
  # SIM102 happy without losing the 503-vs-500 branch clarity.
169
- if (
170
- http_status == 503
171
- and provider_error_code
172
- and "overload" in provider_error_code.lower()
173
- ):
169
+ if http_status == 503 and provider_error_code and "overload" in provider_error_code.lower():
174
170
  return ErrorType.OVERLOADED
175
171
  if http_status == 400 and provider_error_code:
176
172
  code = provider_error_code.lower()
@@ -11,6 +11,8 @@ from typing import Any
11
11
 
12
12
  from pydantic import BaseModel
13
13
 
14
+ from flightdeck_sensor.core.types import _DEFAULT_GRACE_PERIOD_MS
15
+
14
16
 
15
17
  class DirectivePayloadSchema(BaseModel):
16
18
  """Payload for action='custom' directives."""
@@ -48,7 +50,7 @@ class DirectiveResponseSchema(BaseModel):
48
50
 
49
51
  action: str
50
52
  reason: str | None = None
51
- grace_period_ms: int = 5000
53
+ grace_period_ms: int = _DEFAULT_GRACE_PERIOD_MS
52
54
  degrade_to: str | None = None
53
55
  payload: dict[str, Any] | None = None
54
56
 
@@ -148,6 +148,12 @@ _PREFLIGHT_TIMEOUT_SECS = 1
148
148
  # directive queue but NOT event throughput.
149
149
  _CUSTOM_DIRECTIVE_HANDLER_TIMEOUT_SECS = 5
150
150
 
151
+ # Per-(provider, request_id) retry-counter LRU bound. Eviction-on-overflow
152
+ # keeps memory flat for long-lived sessions making millions of calls; the
153
+ # counter only answers a short-window question ("did this retry chain give
154
+ # up"), so dropping the oldest entries is safe.
155
+ _RETRY_COUNTER_LRU_SIZE = 256
156
+
151
157
  # D126 § 6 sub-agent message routing thresholds.
152
158
  #
153
159
  # Bodies up to ``SUBAGENT_INLINE_THRESHOLD_BYTES`` ride inline on the
@@ -404,7 +410,7 @@ class Session:
404
410
  current = self._retry_counters.get(key, 0) + 1
405
411
  self._retry_counters[key] = current
406
412
  self._retry_counters.move_to_end(key)
407
- while len(self._retry_counters) > 256:
413
+ while len(self._retry_counters) > _RETRY_COUNTER_LRU_SIZE:
408
414
  self._retry_counters.popitem(last=False)
409
415
  return current
410
416
 
@@ -235,13 +235,20 @@ class SensorConfig:
235
235
  self.api_url = self.server
236
236
 
237
237
 
238
+ # Default directive grace period (ms): the window an agent gets to wind down
239
+ # the current call before an enforcement action (degrade / block / shutdown)
240
+ # takes effect. Mirrored as the default in DirectiveResponseSchema so a
241
+ # wire-absent value resolves identically.
242
+ _DEFAULT_GRACE_PERIOD_MS = 5000
243
+
244
+
238
245
  @dataclass(frozen=True)
239
246
  class Directive:
240
247
  """A control-plane directive received in the event response envelope."""
241
248
 
242
249
  action: DirectiveAction
243
250
  reason: str
244
- grace_period_ms: int = 5000
251
+ grace_period_ms: int = _DEFAULT_GRACE_PERIOD_MS
245
252
  payload: dict[str, Any] = field(default_factory=dict)
246
253
 
247
254
 
@@ -94,6 +94,7 @@ try:
94
94
  from anthropic.resources.beta.beta import (
95
95
  Beta as _OrigBetaImported,
96
96
  )
97
+
97
98
  _OrigAnthropic: type | None = _anthropic_module.Anthropic
98
99
  _OrigAsyncAnthropic: type | None = _anthropic_module.AsyncAnthropic
99
100
  # The Beta / AsyncBeta classes are imported at module load so the
@@ -162,7 +163,10 @@ class SensorMessages:
162
163
  """
163
164
  if self._is_async:
164
165
  return base.call_stream_async(
165
- self._real.stream, kwargs, self._session, self._provider,
166
+ self._real.stream,
167
+ kwargs,
168
+ self._session,
169
+ self._provider,
166
170
  )
167
171
  return base.call_stream(self._real.stream, kwargs, self._session, self._provider)
168
172
 
@@ -425,19 +429,27 @@ def patch_anthropic_classes(quiet: bool = False) -> None:
425
429
  return
426
430
 
427
431
  _patch_one_class(
428
- _OrigAnthropic, is_async=False, quiet=quiet,
432
+ _OrigAnthropic,
433
+ is_async=False,
434
+ quiet=quiet,
429
435
  descriptor_cls=_AnthropicMessagesDescriptor,
430
436
  )
431
437
  _patch_one_class(
432
- _OrigAsyncAnthropic, is_async=True, quiet=quiet,
438
+ _OrigAsyncAnthropic,
439
+ is_async=True,
440
+ quiet=quiet,
433
441
  descriptor_cls=_AnthropicMessagesDescriptor,
434
442
  )
435
443
  _patch_one_class(
436
- _OrigBeta, is_async=False, quiet=quiet,
444
+ _OrigBeta,
445
+ is_async=False,
446
+ quiet=quiet,
437
447
  descriptor_cls=_AnthropicBetaMessagesDescriptor,
438
448
  )
439
449
  _patch_one_class(
440
- _OrigAsyncBeta, is_async=True, quiet=quiet,
450
+ _OrigAsyncBeta,
451
+ is_async=True,
452
+ quiet=quiet,
441
453
  descriptor_cls=_AnthropicBetaMessagesDescriptor,
442
454
  )
443
455
 
@@ -37,6 +37,7 @@ def _safe_pct(used: int, limit: int | None) -> int:
37
37
  return 0
38
38
  return (used * 100) // limit
39
39
 
40
+
40
41
  if TYPE_CHECKING:
41
42
  from collections.abc import Iterator
42
43
  from types import TracebackType
@@ -159,7 +160,12 @@ def call_stream(
159
160
  estimated, estimated_via = provider.estimate_tokens(kwargs)
160
161
  call_kwargs = _pre_call(session, provider, kwargs, estimated, estimated_via)
161
162
  return GuardedStream(
162
- real_fn, call_kwargs, session, provider, estimated, estimated_via,
163
+ real_fn,
164
+ call_kwargs,
165
+ session,
166
+ provider,
167
+ estimated,
168
+ estimated_via,
163
169
  )
164
170
 
165
171
 
@@ -180,7 +186,12 @@ def call_stream_async(
180
186
  estimated, estimated_via = provider.estimate_tokens(kwargs)
181
187
  call_kwargs = _pre_call(session, provider, kwargs, estimated, estimated_via)
182
188
  return GuardedAsyncStream(
183
- real_fn, call_kwargs, session, provider, estimated, estimated_via,
189
+ real_fn,
190
+ call_kwargs,
191
+ session,
192
+ provider,
193
+ estimated,
194
+ estimated_via,
184
195
  )
185
196
 
186
197
 
@@ -301,9 +312,7 @@ class GuardedStream:
301
312
  # already propagating.
302
313
  try:
303
314
  is_stream_error = self._first_chunk_t is not None
304
- abort_reason = (
305
- "error_mid_stream" if is_stream_error else "error_before_stream"
306
- )
315
+ abort_reason = "error_mid_stream" if is_stream_error else "error_before_stream"
307
316
  # Re-examine the exception class: APITimeoutError and
308
317
  # plain TimeoutError both map to ``timeout`` per
309
318
  # taxonomy, but abort_reason stays more descriptive.
@@ -345,7 +354,8 @@ class GuardedStream:
345
354
  )
346
355
  except Exception:
347
356
  _log.warning(
348
- "Failed to post stream reconciliation event", exc_info=True,
357
+ "Failed to post stream reconciliation event",
358
+ exc_info=True,
349
359
  )
350
360
 
351
361
  # Iterator protocol. Self-iterating so we can timestamp every chunk
@@ -521,6 +531,7 @@ class GuardedAsyncStream:
521
531
  # ``AttributeError: coroutine object has no
522
532
  # attribute '__aenter__'`` (caught by Rule 40d smoke).
523
533
  import inspect
534
+
524
535
  if inspect.iscoroutine(self._ctx):
525
536
  self._ctx = await self._ctx
526
537
  self._stream = await self._ctx.__aenter__()
@@ -564,9 +575,7 @@ class GuardedAsyncStream:
564
575
  if exc_val is not None:
565
576
  try:
566
577
  is_stream_error = self._first_chunk_t is not None
567
- abort_reason = (
568
- "error_mid_stream" if is_stream_error else "error_before_stream"
569
- )
578
+ abort_reason = "error_mid_stream" if is_stream_error else "error_before_stream"
570
579
  if type(exc_val).__name__ in ("APITimeoutError", "TimeoutError"):
571
580
  abort_reason = "timeout"
572
581
  _emit_error(
@@ -602,7 +611,8 @@ class GuardedAsyncStream:
602
611
  )
603
612
  except Exception:
604
613
  _log.warning(
605
- "Failed to post async stream reconciliation event", exc_info=True,
614
+ "Failed to post async stream reconciliation event",
615
+ exc_info=True,
606
616
  )
607
617
 
608
618
  def __aiter__(self) -> Any:
@@ -723,8 +733,7 @@ def _pre_call(
723
733
  warn_token_limit = session.policy.token_limit
724
734
  pre_call_decision = PolicyDecisionSummary(
725
735
  policy_id=result.policy_id or ("local" if local_warn else ""),
726
- scope=result.matched_policy_scope
727
- or ("local_failsafe" if local_warn else ""),
736
+ scope=result.matched_policy_scope or ("local_failsafe" if local_warn else ""),
728
737
  decision="warn",
729
738
  reason=(
730
739
  f"Pre-call check: {session.tokens_used + estimated}/"
@@ -846,8 +855,7 @@ def _pre_call(
846
855
  local_warn = warn_source == "local"
847
856
  warn_decision = PolicyDecisionSummary(
848
857
  policy_id=result.policy_id or ("local" if local_warn else ""),
849
- scope=result.matched_policy_scope
850
- or ("local_failsafe" if local_warn else ""),
858
+ scope=result.matched_policy_scope or ("local_failsafe" if local_warn else ""),
851
859
  decision="warn",
852
860
  reason=(
853
861
  f"Token usage {session.tokens_used}/{warn_token_limit} "
@@ -1013,15 +1021,12 @@ def _post_call(
1013
1021
  try:
1014
1022
  post_result = session.policy.check(session_total, 0)
1015
1023
  if post_result.decision in (PolicyDecision.WARN, PolicyDecision.BLOCK):
1016
- decision_str = (
1017
- "warn" if post_result.decision == PolicyDecision.WARN else "block"
1018
- )
1024
+ decision_str = "warn" if post_result.decision == PolicyDecision.WARN else "block"
1019
1025
  post_source = post_result.source or "server"
1020
1026
  local = post_source == "local"
1021
1027
  post_decision = PolicyDecisionSummary(
1022
1028
  policy_id=post_result.policy_id or ("local" if local else ""),
1023
- scope=post_result.matched_policy_scope
1024
- or ("local_failsafe" if local else ""),
1029
+ scope=post_result.matched_policy_scope or ("local_failsafe" if local else ""),
1025
1030
  decision=decision_str,
1026
1031
  reason=(
1027
1032
  f"Post-call cumulative usage crossed {decision_str} "
@@ -1084,6 +1089,7 @@ def _post_call(
1084
1089
  from flightdeck_sensor.interceptor.mcp import (
1085
1090
  _build_tool_capture_content,
1086
1091
  )
1092
+
1087
1093
  capture_content = _build_tool_capture_content(
1088
1094
  tool_input=inv.tool_input,
1089
1095
  tool_output=None,
@@ -1161,9 +1167,7 @@ def _emit_error(
1161
1167
  # str at runtime. cast(str, ...) tells mypy the result is str
1162
1168
  # without manufacturing a third "" fallback that would conflate
1163
1169
  # missing-key, present-but-None, and present-but-empty cases.
1164
- provider_name = cast(
1165
- "str", error_payload.get("provider") or getattr(provider, "name", "")
1166
- )
1170
+ provider_name = cast("str", error_payload.get("provider") or getattr(provider, "name", ""))
1167
1171
  # Trust-boundary check: error_payload is sensor-internal but
1168
1172
  # composed from provider-supplied error attributes (request_id
1169
1173
  # is whatever the SDK exposed). isinstance enforces the
@@ -86,6 +86,7 @@ def _ensure_imported() -> bool:
86
86
  return _CREWAI_AVAILABLE
87
87
  try:
88
88
  from crewai import Agent as _Agent
89
+
89
90
  _CrewAIAgent = _Agent
90
91
  _CREWAI_AVAILABLE = True
91
92
  except ImportError:
@@ -85,6 +85,7 @@ def _ensure_imported() -> bool:
85
85
  return _LANGGRAPH_AVAILABLE
86
86
  try:
87
87
  from langgraph.graph import StateGraph
88
+
88
89
  _StateGraph = StateGraph
89
90
  _LANGGRAPH_AVAILABLE = True
90
91
  except ImportError:
@@ -68,6 +68,7 @@ _STREAMING_NOT_SUPPORTED_MSG = (
68
68
 
69
69
  try:
70
70
  import litellm as _litellm_module
71
+
71
72
  _OrigCompletion: Any = _litellm_module.completion
72
73
  _OrigAcompletion: Any = _litellm_module.acompletion
73
74
  # Phase 4: embedding / aembedding are the two entry points for
@@ -131,8 +132,7 @@ class SensorLitellm:
131
132
  ) -> None:
132
133
  if not _LITELLM_AVAILABLE:
133
134
  raise ImportError(
134
- "litellm is not installed. "
135
- "Install with `pip install flightdeck-sensor[litellm]`."
135
+ "litellm is not installed. Install with `pip install flightdeck-sensor[litellm]`."
136
136
  )
137
137
  self._session = session
138
138
  self._provider = provider or LitellmProvider(
@@ -158,7 +158,10 @@ class SensorLitellm:
158
158
  if kwargs.get("stream"):
159
159
  raise NotImplementedError(_STREAMING_NOT_SUPPORTED_MSG)
160
160
  return await base.call_async(
161
- _OrigAcompletion, kwargs, self._session, self._provider,
161
+ _OrigAcompletion,
162
+ kwargs,
163
+ self._session,
164
+ self._provider,
162
165
  )
163
166
 
164
167
  # Phase 4: embeddings parallel to completion. Same delegation pattern;
@@ -171,26 +174,34 @@ class SensorLitellm:
171
174
  chat completions.
172
175
  """
173
176
  from flightdeck_sensor.core.types import EventType
177
+
174
178
  if _OrigEmbedding is None:
175
179
  raise RuntimeError(
176
180
  "litellm.embedding is not available on the installed version; "
177
181
  "upgrade litellm or use a different embedding client."
178
182
  )
179
183
  return base.call(
180
- _OrigEmbedding, kwargs, self._session, self._provider,
184
+ _OrigEmbedding,
185
+ kwargs,
186
+ self._session,
187
+ self._provider,
181
188
  event_type=EventType.EMBEDDINGS,
182
189
  )
183
190
 
184
191
  async def aembedding(self, **kwargs: Any) -> Any:
185
192
  """Intercept ``litellm.aembedding(**kwargs)``."""
186
193
  from flightdeck_sensor.core.types import EventType
194
+
187
195
  if _OrigAembedding is None:
188
196
  raise RuntimeError(
189
197
  "litellm.aembedding is not available on the installed version; "
190
198
  "upgrade litellm or use a different embedding client."
191
199
  )
192
200
  return await base.call_async(
193
- _OrigAembedding, kwargs, self._session, self._provider,
201
+ _OrigAembedding,
202
+ kwargs,
203
+ self._session,
204
+ self._provider,
194
205
  event_type=EventType.EMBEDDINGS,
195
206
  )
196
207
 
@@ -273,11 +284,15 @@ def patch_litellm_functions(quiet: bool = False) -> None:
273
284
  if session is None or orig_embedding is None:
274
285
  return orig_embedding(**kwargs) if orig_embedding else None
275
286
  from flightdeck_sensor.core.types import EventType
287
+
276
288
  provider = LitellmProvider(
277
289
  capture_prompts=session.config.capture_prompts,
278
290
  )
279
291
  return base.call(
280
- orig_embedding, kwargs, session, provider,
292
+ orig_embedding,
293
+ kwargs,
294
+ session,
295
+ provider,
281
296
  event_type=EventType.EMBEDDINGS,
282
297
  )
283
298
 
@@ -286,11 +301,15 @@ def patch_litellm_functions(quiet: bool = False) -> None:
286
301
  if session is None or orig_aembedding is None:
287
302
  return await orig_aembedding(**kwargs) if orig_aembedding else None
288
303
  from flightdeck_sensor.core.types import EventType
304
+
289
305
  provider = LitellmProvider(
290
306
  capture_prompts=session.config.capture_prompts,
291
307
  )
292
308
  return await base.call_async(
293
- orig_aembedding, kwargs, session, provider,
309
+ orig_aembedding,
310
+ kwargs,
311
+ session,
312
+ provider,
294
313
  event_type=EventType.EMBEDDINGS,
295
314
  )
296
315
 
@@ -331,16 +350,24 @@ def unpatch_litellm_functions(quiet: bool = False) -> None:
331
350
  return
332
351
 
333
352
  orig_completion = getattr(
334
- _litellm_module, "_flightdeck_orig_completion", None,
353
+ _litellm_module,
354
+ "_flightdeck_orig_completion",
355
+ None,
335
356
  )
336
357
  orig_acompletion = getattr(
337
- _litellm_module, "_flightdeck_orig_acompletion", None,
358
+ _litellm_module,
359
+ "_flightdeck_orig_acompletion",
360
+ None,
338
361
  )
339
362
  orig_embedding = getattr(
340
- _litellm_module, "_flightdeck_orig_embedding", None,
363
+ _litellm_module,
364
+ "_flightdeck_orig_embedding",
365
+ None,
341
366
  )
342
367
  orig_aembedding = getattr(
343
- _litellm_module, "_flightdeck_orig_aembedding", None,
368
+ _litellm_module,
369
+ "_flightdeck_orig_aembedding",
370
+ None,
344
371
  )
345
372
  if orig_completion is not None:
346
373
  _litellm_module.completion = orig_completion
@@ -81,6 +81,7 @@ _log = logging.getLogger("flightdeck_sensor.interceptor.openai")
81
81
 
82
82
  try:
83
83
  import openai as _openai_module
84
+
84
85
  _OrigOpenAI: type | None = _openai_module.OpenAI
85
86
  _OrigAsyncOpenAI: type | None = _openai_module.AsyncOpenAI
86
87
  _OPENAI_AVAILABLE = True
@@ -144,6 +145,7 @@ class SensorCompletions:
144
145
  self._session,
145
146
  self._provider,
146
147
  )
148
+
147
149
  return _async_stream_create()
148
150
  return base.call_stream(
149
151
  self._real.create,
@@ -226,9 +228,7 @@ class _SensorCompletionsRawResponseWrapper:
226
228
  """Intercept the raw-response create closure with the sensor pipeline."""
227
229
  real_fn = self._real.create
228
230
  if self._is_async:
229
- return base.call_async(
230
- real_fn, kwargs, self._session, self._provider
231
- )
231
+ return base.call_async(real_fn, kwargs, self._session, self._provider)
232
232
  return base.call(real_fn, kwargs, self._session, self._provider)
233
233
 
234
234
  def __getattr__(self, name: str) -> Any:
@@ -300,9 +300,7 @@ class SensorResponses:
300
300
  """Intercept responses.create() -- sync or async dispatch."""
301
301
  real_fn = self._real.create
302
302
  if self._is_async:
303
- return base.call_async(
304
- real_fn, kwargs, self._session, self._provider
305
- )
303
+ return base.call_async(real_fn, kwargs, self._session, self._provider)
306
304
  return base.call(real_fn, kwargs, self._session, self._provider)
307
305
 
308
306
  def __getattr__(self, name: str) -> Any:
@@ -352,13 +350,20 @@ class SensorEmbeddings:
352
350
  """
353
351
  real_fn = self._real.create
354
352
  from flightdeck_sensor.core.types import EventType
353
+
355
354
  if self._is_async:
356
355
  return base.call_async(
357
- real_fn, kwargs, self._session, self._provider,
356
+ real_fn,
357
+ kwargs,
358
+ self._session,
359
+ self._provider,
358
360
  event_type=EventType.EMBEDDINGS,
359
361
  )
360
362
  return base.call(
361
- real_fn, kwargs, self._session, self._provider,
363
+ real_fn,
364
+ kwargs,
365
+ self._session,
366
+ self._provider,
362
367
  event_type=EventType.EMBEDDINGS,
363
368
  )
364
369
 
@@ -14,6 +14,7 @@ Python ``>= 3.10`` (sensor/pyproject.toml requires-python). ``StrEnum``
14
14
  landed in 3.11. The mixin form gives the same "member IS a string"
15
15
  semantics on 3.10.
16
16
  """
17
+
17
18
  from __future__ import annotations
18
19
 
19
20
  from enum import Enum
@@ -194,6 +194,7 @@ class AnthropicProvider:
194
194
  # MessageStream returns a real Message which
195
195
  # ``model_dump`` handles cleanly.
196
196
  import inspect
197
+
197
198
  if not inspect.iscoroutine(candidate):
198
199
  response = candidate
199
200
  else:
@@ -209,6 +210,7 @@ class AnthropicProvider:
209
210
  # are not JSON-serialisable so the wire payload stays
210
211
  # valid even on exotic response shapes.
211
212
  import json
213
+
212
214
  resp_dict = {}
213
215
  for k, v in dict(response.__dict__).items():
214
216
  try:
@@ -78,7 +78,9 @@ class LitellmProvider:
78
78
  what is reachable on the ModelResponse object."""
79
79
  try:
80
80
  request_id = getattr(response, "id", None) or getattr(
81
- response, "_request_id", None,
81
+ response,
82
+ "_request_id",
83
+ None,
82
84
  )
83
85
  model = getattr(response, "model", None)
84
86
  out: dict[str, Any] = {}
@@ -172,9 +174,7 @@ class LitellmProvider:
172
174
  vectors = []
173
175
  for entry in data:
174
176
  emb = getattr(entry, "embedding", None) or (
175
- entry.get("embedding")
176
- if isinstance(entry, dict)
177
- else None
177
+ entry.get("embedding") if isinstance(entry, dict) else None
178
178
  )
179
179
  if emb is not None:
180
180
  vectors.append(list(emb))
@@ -258,9 +258,7 @@ class OpenAIProvider:
258
258
  vectors = []
259
259
  for entry in data:
260
260
  emb = getattr(entry, "embedding", None) or (
261
- entry.get("embedding")
262
- if isinstance(entry, dict)
263
- else None
261
+ entry.get("embedding") if isinstance(entry, dict) else None
264
262
  )
265
263
  if emb is not None:
266
264
  vectors.append(list(emb))
@@ -294,6 +292,7 @@ class OpenAIProvider:
294
292
  # ``Object of type AsyncStream is not JSON
295
293
  # serializable`` (caught by Rule 40d smoke).
296
294
  import json as _json
295
+
297
296
  resp_dict = {}
298
297
  for k, v in dict(response.__dict__).items():
299
298
  try: