flightdeck-sensor 0.5.2__tar.gz → 0.6.0__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 (90) hide show
  1. flightdeck_sensor-0.6.0/CLAUDE.md +78 -0
  2. flightdeck_sensor-0.6.0/PKG-INFO +239 -0
  3. flightdeck_sensor-0.6.0/README.md +179 -0
  4. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/flightdeck_sensor/__init__.py +146 -76
  5. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/flightdeck_sensor/config.py +6 -2
  6. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/flightdeck_sensor/core/agent_id.py +8 -10
  7. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/flightdeck_sensor/core/context.py +7 -7
  8. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/flightdeck_sensor/core/errors.py +311 -34
  9. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/flightdeck_sensor/core/exceptions.py +7 -7
  10. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/flightdeck_sensor/core/mcp_policy.py +55 -31
  11. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/flightdeck_sensor/core/policy.py +8 -8
  12. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/flightdeck_sensor/core/schemas.py +1 -1
  13. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/flightdeck_sensor/core/session.py +156 -56
  14. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/flightdeck_sensor/core/types.py +126 -22
  15. flightdeck_sensor-0.6.0/flightdeck_sensor/interceptor/anthropic.py +1164 -0
  16. flightdeck_sensor-0.6.0/flightdeck_sensor/interceptor/base.py +1780 -0
  17. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/flightdeck_sensor/interceptor/crewai.py +9 -5
  18. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/flightdeck_sensor/interceptor/langgraph.py +30 -16
  19. flightdeck_sensor-0.6.0/flightdeck_sensor/interceptor/litellm.py +544 -0
  20. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/flightdeck_sensor/interceptor/mcp.py +369 -182
  21. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/flightdeck_sensor/interceptor/mcp_identity.py +8 -9
  22. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/flightdeck_sensor/interceptor/openai.py +406 -58
  23. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/flightdeck_sensor/provider.py +13 -13
  24. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/flightdeck_sensor/providers/anthropic.py +209 -49
  25. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/flightdeck_sensor/providers/litellm.py +177 -9
  26. flightdeck_sensor-0.6.0/flightdeck_sensor/providers/openai.py +779 -0
  27. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/flightdeck_sensor/providers/protocol.py +104 -1
  28. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/flightdeck_sensor/transport/client.py +13 -11
  29. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/flightdeck_sensor/transport/retry.py +13 -10
  30. flightdeck_sensor-0.6.0/pyproject.toml +222 -0
  31. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/conftest.py +85 -2
  32. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/unit/test_agent_id.py +9 -9
  33. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/unit/test_agent_type_validation.py +1 -1
  34. flightdeck_sensor-0.6.0/tests/unit/test_anthropic_resource_patch.py +1512 -0
  35. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/unit/test_compat_crewai_mcp.py +2 -2
  36. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/unit/test_cross_agent_messages.py +36 -11
  37. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/unit/test_custom_directives.py +2 -2
  38. flightdeck_sensor-0.6.0/tests/unit/test_egress_url_validation.py +235 -0
  39. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/unit/test_errors.py +354 -6
  40. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/unit/test_framework_attribution.py +50 -44
  41. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/unit/test_interceptor.py +44 -24
  42. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/unit/test_interceptor_crewai.py +26 -3
  43. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/unit/test_interceptor_langgraph.py +116 -2
  44. flightdeck_sensor-0.6.0/tests/unit/test_interceptor_litellm.py +1038 -0
  45. flightdeck_sensor-0.6.0/tests/unit/test_mcp_2x_paths.py +141 -0
  46. flightdeck_sensor-0.6.0/tests/unit/test_mcp_availability.py +45 -0
  47. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/unit/test_mcp_identity.py +1 -1
  48. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/unit/test_mcp_interceptor.py +117 -61
  49. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/unit/test_mcp_policy_emission_enriched.py +6 -6
  50. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/unit/test_mcp_policy_enforcement.py +54 -5
  51. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/unit/test_mcp_policy_enforcement_all_paths.py +4 -4
  52. flightdeck_sensor-0.6.0/tests/unit/test_openai_parse_responses.py +1390 -0
  53. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/unit/test_originating_event_id.py +1 -1
  54. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/unit/test_patch.py +148 -143
  55. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/unit/test_policy.py +3 -3
  56. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/unit/test_policy_decision_summary.py +2 -2
  57. flightdeck_sensor-0.6.0/tests/unit/test_policy_directive_events.py +113 -0
  58. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/unit/test_policy_emission_enriched.py +3 -3
  59. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/unit/test_policy_events.py +5 -5
  60. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/unit/test_prompt_capture.py +2 -2
  61. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/unit/test_provider_enum.py +6 -6
  62. flightdeck_sensor-0.6.0/tests/unit/test_providers.py +504 -0
  63. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/unit/test_session.py +12 -6
  64. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/unit/test_session_lifecycle_enrichment.py +49 -1
  65. flightdeck_sensor-0.6.0/tests/unit/test_stop_reason_errors.py +528 -0
  66. flightdeck_sensor-0.6.0/tests/unit/test_streaming_usage.py +1837 -0
  67. flightdeck_sensor-0.5.2/tests/unit/test_d150_tool_capture_destination.py → flightdeck_sensor-0.6.0/tests/unit/test_tool_capture_destination.py +11 -12
  68. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/unit/test_transport.py +20 -8
  69. flightdeck_sensor-0.6.0/uv.lock +6504 -0
  70. flightdeck_sensor-0.5.2/PKG-INFO +0 -190
  71. flightdeck_sensor-0.5.2/README.md +0 -141
  72. flightdeck_sensor-0.5.2/flightdeck_sensor/interceptor/anthropic.py +0 -517
  73. flightdeck_sensor-0.5.2/flightdeck_sensor/interceptor/base.py +0 -1194
  74. flightdeck_sensor-0.5.2/flightdeck_sensor/interceptor/litellm.py +0 -392
  75. flightdeck_sensor-0.5.2/flightdeck_sensor/providers/openai.py +0 -396
  76. flightdeck_sensor-0.5.2/pyproject.toml +0 -149
  77. flightdeck_sensor-0.5.2/tests/unit/test_providers.py +0 -202
  78. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/.gitignore +0 -0
  79. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/Makefile +0 -0
  80. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/flightdeck_sensor/compat/__init__.py +0 -0
  81. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/flightdeck_sensor/compat/crewai_mcp.py +0 -0
  82. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/flightdeck_sensor/core/__init__.py +0 -0
  83. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/flightdeck_sensor/interceptor/__init__.py +0 -0
  84. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/flightdeck_sensor/providers/__init__.py +0 -0
  85. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/flightdeck_sensor/py.typed +0 -0
  86. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/flightdeck_sensor/transport/__init__.py +0 -0
  87. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/__init__.py +0 -0
  88. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/unit/__init__.py +0 -0
  89. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/unit/test_context.py +0 -0
  90. {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/unit/test_llm_event_enrichment.py +0 -0
@@ -0,0 +1,78 @@
1
+ # Flightdeck Sensor -- Claude Code Rules
2
+
3
+ > Component-specific rules moved out of the root CLAUDE.md so they load only
4
+ > when working under sensor/. All root CLAUDE.md rules still apply; the
5
+ > numbering below keeps the original rule numbers.
6
+
7
+ ## Sensor Rules
8
+
9
+ 27. **The sensor must never add meaningful latency to the agent's hot path.**
10
+ All control plane communication is fire-and-forget or background.
11
+ Never introduce synchronous blocking calls in the LLM call intercept path.
12
+
13
+ 28. **The sensor must fail open.** If the control plane is unreachable and
14
+ FLIGHTDECK_UNAVAILABLE_POLICY=continue, the agent proceeds with no enforcement.
15
+ Do not raise exceptions for connectivity failures.
16
+
17
+ 29. **Token counting carries over from tokencap.** Do not rewrite the counting logic.
18
+ Extend it. Pre-call estimation, post-call reconciliation, delta correction -- these
19
+ are proven and must not be changed without Supervisor approval.
20
+
21
+ 30. **capture_prompts defaults to False.** The default init() call never captures
22
+ content. Always verify this default has not been accidentally changed.
23
+
24
+ 31. **init() limit param fires WARN only.** Never upgrade a local limit to BLOCK
25
+ or DEGRADE regardless of what the server policy says. See DECISIONS.md D035.
26
+
27
+ 32. **The sensor is a library wrapper, not an OS agent.** Never add background
28
+ threads, polling loops, or daemon threads to the sensor beyond the existing
29
+ event queue drain thread. If a feature requires background activity
30
+ independent of LLM calls, it does not belong in the sensor.
31
+
32
+ ---
33
+
34
+ ## Framework Coverage Discipline
35
+
36
+ 40d. **Framework coverage discipline.** Any phase that adds
37
+ framework support OR changes framework-emission behaviour
38
+ MUST include BOTH:
39
+
40
+ 1. **Real-provider playground demos** per affected framework --
41
+ manual, NOT in CI (they cost money and need live API
42
+ credentials). Live under ``playground/`` and self-skip
43
+ (exit 2) when the relevant API key / framework / optional
44
+ gateway URL is missing so ``make playground-all`` runs
45
+ cleanly on any box. Driven via ``make playground-<script>``
46
+ targets. Each demo asserts payload shape inline using
47
+ ``print_result`` + ``raise AssertionError``; ``run_all.py``
48
+ exits 0 only when every script returned 0 (PASS) or 2
49
+ (SKIP). Results documented in the phase's audit doc before
50
+ PR merge.
51
+ 2. **Integration tests** per framework × behaviour combo,
52
+ mock-free (or lightly mocked at the network boundary),
53
+ running in CI via the existing Integration job. Seed a
54
+ realistic event payload for each new framework + behaviour
55
+ combination and verify end-to-end landing.
56
+
57
+ V-pass for such a phase MUST enumerate the playground demos
58
+ and integration tests that will be added before
59
+ implementation starts. Skipping either is a phase-gate
60
+ failure.
61
+
62
+ Why: Phase 4 (agent communication coverage hardening) shipped
63
+ embeddings, streaming semantics, structured error events, and
64
+ session-lifecycle edge-case fixes. Mock-only coverage would
65
+ have let a future SDK upgrade silently break the classifier
66
+ (anthropic renames ``RateLimitError`` to ``QuotaError`` and
67
+ our classifier falls through to ``other``; no CI gate catches
68
+ it). The playground matrix is the only thing that exercises
69
+ the real class hierarchy every provider ships.
70
+
71
+ Applies to: every phase from Phase 4 onwards that touches
72
+ ``sensor/flightdeck_sensor/interceptor/*``, adds a new
73
+ interceptor file, or changes the event-emission shape for an
74
+ existing framework.
75
+
76
+ Does NOT apply to: non-framework sensor work (transport,
77
+ policy, directives) — those are covered by the standard unit
78
+ + integration suites.
@@ -0,0 +1,239 @@
1
+ Metadata-Version: 2.5
2
+ Name: flightdeck-sensor
3
+ Version: 0.6.0
4
+ Summary: In-process agent observability sensor for Flightdeck
5
+ Project-URL: Repository, https://github.com/flightdeckhq/flightdeck
6
+ Author-email: Omri Arnon <omri.arnon@gmail.com>
7
+ License-Expression: Apache-2.0
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: Apache Software License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Classifier: Typing :: Typed
17
+ Requires-Python: <3.15,>=3.11
18
+ Requires-Dist: pydantic<3,>=2.10
19
+ Provides-Extra: anthropic
20
+ Requires-Dist: anthropic>=0.86; extra == 'anthropic'
21
+ Provides-Extra: dev
22
+ Requires-Dist: anthropic>=0.86; extra == 'dev'
23
+ Requires-Dist: crewai>=1.14; (python_version < '3.14') and extra == 'dev'
24
+ Requires-Dist: httpx>=0.25; extra == 'dev'
25
+ Requires-Dist: langchain-anthropic<2,>=1; extra == 'dev'
26
+ Requires-Dist: langchain-mcp-adapters<0.4,>=0.3; extra == 'dev'
27
+ Requires-Dist: langchain-openai<2,>=1; extra == 'dev'
28
+ Requires-Dist: langchain<2,>=1; extra == 'dev'
29
+ Requires-Dist: langgraph<2,>=1; extra == 'dev'
30
+ Requires-Dist: litellm>=1.100; extra == 'dev'
31
+ Requires-Dist: llama-index-llms-anthropic>=0.1; extra == 'dev'
32
+ Requires-Dist: llama-index-llms-openai>=0.1; extra == 'dev'
33
+ Requires-Dist: llama-index-tools-mcp>=0.1; extra == 'dev'
34
+ Requires-Dist: mcp<2,>=1.28; extra == 'dev'
35
+ Requires-Dist: mcpadapt>=0.0.6; extra == 'dev'
36
+ Requires-Dist: mypy>=2.4; extra == 'dev'
37
+ Requires-Dist: openai>=1.0; extra == 'dev'
38
+ Requires-Dist: pytest-asyncio>=1.0; extra == 'dev'
39
+ Requires-Dist: pytest-cov>=7.0; extra == 'dev'
40
+ Requires-Dist: pytest-xdist<4,>=3.5; extra == 'dev'
41
+ Requires-Dist: pytest>=9; extra == 'dev'
42
+ Requires-Dist: respx>=0.20; extra == 'dev'
43
+ Requires-Dist: ruff>=0.3; extra == 'dev'
44
+ Provides-Extra: litellm
45
+ Requires-Dist: litellm>=1.83.0; extra == 'litellm'
46
+ Provides-Extra: mcp
47
+ Requires-Dist: mcp<3,>=1.28; extra == 'mcp'
48
+ Provides-Extra: openai
49
+ Requires-Dist: openai>=1.0; extra == 'openai'
50
+ Requires-Dist: tiktoken>=0.5; extra == 'openai'
51
+ Provides-Extra: test
52
+ Requires-Dist: mypy>=2.4; extra == 'test'
53
+ Requires-Dist: pytest-asyncio>=1.0; extra == 'test'
54
+ Requires-Dist: pytest-xdist<4,>=3.5; extra == 'test'
55
+ Requires-Dist: pytest>=9; extra == 'test'
56
+ Requires-Dist: respx>=0.20; extra == 'test'
57
+ Requires-Dist: ruff>=0.3; extra == 'test'
58
+ Requires-Dist: tiktoken>=0.5; extra == 'test'
59
+ Description-Content-Type: text/markdown
60
+
61
+ # flightdeck-sensor
62
+
63
+ In-process agent observability sensor for [Flightdeck](https://github.com/flightdeckhq/flightdeck).
64
+
65
+ ## Local development venv
66
+
67
+ Flightdeck pins **Python 3.12** for local development. The sensor
68
+ supports `3.11 ≤ x < 3.15` (pyproject `requires-python`); `sensor/.venv`
69
+ is the canonical interpreter for every Make target that runs Python.
70
+
71
+ Recreate from a fresh clone or after a `rm -rf sensor/.venv`:
72
+
73
+ ```bash
74
+ python3.12 -m venv sensor/.venv
75
+ ./sensor/.venv/bin/python -m pip install -e ".[dev,anthropic,openai]"
76
+ ```
77
+
78
+ `make -C sensor install` does the same once the venv exists. To get
79
+ the exact versions CI tests with, install from the lockfile instead:
80
+
81
+ ```bash
82
+ cd sensor && uv sync --locked --python 3.12 --extra dev --extra anthropic --extra openai
83
+ ```
84
+
85
+ CI installs from `sensor/uv.lock` into the `actions/setup-python`
86
+ interpreter and overrides via env (`PYTHON=python make -C sensor
87
+ test`). The lock covers Python 3.11 to 3.14 and both CI lanes: the
88
+ framework lane (`dev` extra) and the newest-SDK lane (`sdk-majors`
89
+ dependency group). `uv lock` resolves only releases at least 14 days
90
+ old, anthropic a day old (`[tool.uv]` in `pyproject.toml`).
91
+
92
+ ## Optional `session_id` hint
93
+
94
+ By default `init()` auto-generates a fresh UUID every time the process
95
+ starts. Orchestrators that re-run the same logical workflow (Temporal,
96
+ Airflow, cron) can instead pass a stable identifier; if the backend
97
+ already has a row for that session, the new execution is attached to it
98
+ and appears as a continuation of the prior run in the fleet view.
99
+
100
+ Supply the hint via either the `session_id=` kwarg or the
101
+ `FLIGHTDECK_SESSION_ID` environment variable. The env var takes
102
+ precedence.
103
+
104
+ The value MUST parse as a canonical UUID (any version) -- the
105
+ sessions table column is UUID-typed. If you pass a non-UUID the
106
+ sensor logs a warning and falls back to auto-generating one.
107
+ Orchestrators that use string identifiers (Temporal workflow_id,
108
+ Airflow dag_run_id) should hash the identifier into a deterministic
109
+ UUID with `uuid.uuid5`.
110
+
111
+ ### Temporal workflow example
112
+
113
+ ```python
114
+ import uuid
115
+ import flightdeck_sensor as fd
116
+ from temporalio import workflow
117
+
118
+ # Pick any fixed namespace UUID for your deployment. The same
119
+ # workflow_id + namespace always produces the same session UUID,
120
+ # so re-runs of the same workflow all map to the same sessions row.
121
+ FLIGHTDECK_NS = uuid.UUID("00000000-0000-0000-0000-000000000001")
122
+
123
+
124
+ @workflow.defn
125
+ class MyWorkflow:
126
+ @workflow.run
127
+ async def run(self, input):
128
+ ctx = workflow.info()
129
+ fd.init(
130
+ server="http://flightdeck.internal/ingest",
131
+ token="ftd_...",
132
+ session_id=str(uuid.uuid5(FLIGHTDECK_NS, ctx.workflow_id)),
133
+ )
134
+ # If this workflow_id has run before, the backend attaches
135
+ # this execution to the existing session automatically; the
136
+ # sensor logs INFO on the first response that confirms it.
137
+ ...
138
+ ```
139
+
140
+ The sensor logs a single WARNING at `init()` time whenever a custom
141
+ `session_id` is in play so the behaviour is visible in operational
142
+ logs, and an INFO line on the first response where the backend
143
+ confirms attachment. See ARCHITECTURE.md ("Session attachment flow")
144
+ for the full protocol.
145
+
146
+ ## Framework support
147
+
148
+ `flightdeck_sensor.patch()` installs six interceptors process-wide:
149
+ three LLM-call interceptors (class-level or module-level), one MCP
150
+ interceptor, and two sub-agent interceptors. Every LLM call that
151
+ flows through a patched entry point emits a `pre_call` / `post_call`
152
+ event pair without any framework-specific wiring; the MCP and
153
+ sub-agent interceptors emit their own event types, described in the
154
+ table.
155
+
156
+ | Framework | Interceptor | Entry points |
157
+ | --- | --- | --- |
158
+ | Anthropic SDK — every client class (`Anthropic`, `AnthropicBedrock`, `AnthropicVertex`, `AnthropicFoundry`, `AnthropicAWS`, `AnthropicBedrockMantle`, `AnthropicGoogleCloud` and their async forms), direct or via a framework that constructs one internally (LangChain, LlamaIndex, CrewAI native) | `patch_anthropic_classes` | `messages.create` (including `stream=True`), `.stream`, `.parse`, async + sync, beta resources, the raw view (`with_raw_response`) and the streaming view (`with_streaming_response`, metered at `parse()`); streamed calls are metered from the in-stream usage events; every `post_call` carries `provider_metadata.platform` (`anthropic`, `bedrock`, `bedrock-mantle`, `aws`, `vertex`, `google-cloud`, `foundry`, or a gateway's host) |
159
+ | OpenAI SDK (direct or via a framework that constructs `OpenAI()` internally — LangChain, LlamaIndex, CrewAI native) | `patch_openai_classes` | `OpenAI.chat.completions.create` / `.parse`, `.responses.create` / `.parse`, and their `with_raw_response` counterparts (`create` / `parse`), `.embeddings.create`, async + sync; `create(stream=True)` on chat and Responses is metered from the chat usage chunk and `response.completed`; the `.stream()` helpers and `with_streaming_response` are not intercepted |
160
+ | litellm (router that aggregates many providers behind one function surface) | `patch_litellm_functions` | `litellm.completion`, `litellm.acompletion`, `litellm.embedding`, `litellm.aembedding` |
161
+ | MCP (official Python `mcp` SDK, reached transitively by `langchain-mcp-adapters`, `llama-index-tools-mcp` and `mcpadapt`) | `patch_mcp_classes` | `mcp.client.session.ClientSession.initialize` on mcp 1.x / `.adopt` on mcp 2.x (server fingerprint capture, every 2.x handshake path), `.list_tools`, `.call_tool`, `.list_resources`, `.read_resource`, `.list_prompts`, `.get_prompt` (one `mcp_*` event type each), plus the stdio / SSE / streamable-HTTP / websocket transport client factories and mcp 2.x's `Client._build_session` for transport attribution. `call_tool` also enforces the MCP Protection Policy. Supported SDK range: `mcp>=1.20,<3`. |
162
+ | CrewAI (sub-agent sessions) | `patch_crewai_classes` | `crewai.Agent.execute_task` / `.aexecute_task` — each Crew member's turn lands as a child session (`session_start` / `session_end`) with `parent_session_id`, `agent_role` taken from `Agent.role`, and incoming / outgoing message capture gated on `capture_prompts` |
163
+ | LangGraph (sub-agent sessions) | `patch_langgraph_classes` | `langgraph.graph.StateGraph.add_node` — the registered node callable is wrapped so each node run lands as a child session with `agent_role` taken from the node name; every node is treated as agent-bearing unless narrowed with `init(langgraph_agent_node_pattern=...)` |
164
+
165
+ ### litellm example
166
+
167
+ litellm's Anthropic route uses raw httpx instead of the Anthropic
168
+ SDK, so it bypasses the SDK-class patches. The litellm interceptor
169
+ patches `litellm.completion` / `litellm.acompletion` directly to
170
+ close this gap. An Anthropic model string via litellm exercises the
171
+ bypass case:
172
+
173
+ ```python
174
+ import flightdeck_sensor
175
+ import litellm
176
+
177
+ flightdeck_sensor.init(
178
+ server="http://flightdeck.internal/ingest",
179
+ token="ftd_...",
180
+ )
181
+ flightdeck_sensor.patch()
182
+
183
+ # After patch(), this call routes through the sensor's pre/post-call
184
+ # plumbing regardless of which underlying provider litellm picks for
185
+ # the model string.
186
+ response = litellm.completion(
187
+ model="claude-haiku-4-5-20251001",
188
+ messages=[{"role": "user", "content": "hi"}],
189
+ max_tokens=5,
190
+ )
191
+ ```
192
+
193
+ Install the optional dependency: `pip install flightdeck-sensor[litellm]`
194
+ (the extra requires `litellm>=1.83.0`; 1.82.7 and 1.82.8 were compromised
195
+ builds).
196
+
197
+ ### What the litellm interceptor catches, and what it doesn't
198
+
199
+ **Catches.** Direct callers of `litellm.completion(**kwargs)` and
200
+ `litellm.acompletion(**kwargs)` — the vast majority of litellm
201
+ integrations, including the default chat-completion surface exposed
202
+ by user code, Router, and frameworks that route through the public
203
+ API (CrewAI non-native flavors, langchain-community's litellm
204
+ adapter, etc.) — streaming included: `stream=True` on either returns
205
+ the sensor's stream guard around litellm's `CustomStreamWrapper`,
206
+ iterated the same way (`for` / `async for`), metered from the token
207
+ counts litellm reports in its trailing usage chunk; that chunk is
208
+ kept from a caller who did not ask for `stream_options.include_usage`.
209
+ A call litellm serves through the OpenAI SDK (which the sensor also
210
+ patches) is counted once, by the litellm wrapper. Embedding calls
211
+ through `litellm.embedding(**kwargs)` and `litellm.aembedding(**kwargs)`
212
+ are wrapped the same way, so embedding-model token usage lands too
213
+ (`12_litellm.py` in the playground exercises sync and async streaming,
214
+ the single-count assertion and the capture round-trip).
215
+
216
+ **Does NOT catch.**
217
+ - **Lower-level litellm entry points.** Some integrations reach
218
+ past `completion` into `litellm.llms.custom_httpx.http_handler`
219
+ or other internal helpers directly. Those calls bypass the
220
+ module-level wrapper. A broader httpx-level interceptor was
221
+ considered when the litellm interceptor was scoped and deferred —
222
+ if framework reports surface calls that slip past the current
223
+ coverage, file a new issue.
224
+ - **`litellm.text_completion` / `atext_completion` and
225
+ `litellm.responses` / `aresponses`.** Neither is wrapped (root
226
+ `README.md` Roadmap), but some routes are still reported. The legacy
227
+ completion API calls `completion` through the name bound in
228
+ `litellm.main`, which the module-attribute swap does not reach; on
229
+ an OpenAI chat model the OpenAI interceptor reports the SDK call it
230
+ makes, and litellm's own httpx routes are unobserved. The Responses
231
+ API on a provider without a native Responses API (for example
232
+ Anthropic) is bridged through the patched `litellm.completion` and reported
233
+ once as a chat call; native routes (OpenAI) go through litellm's own
234
+ httpx handler and are unobserved.
235
+ - **SDK calls made from inside a litellm call.** The litellm wrapper
236
+ marks the call it serves so the SDK interceptors pass it through;
237
+ the mark covers the whole call, so an SDK call made while it is in
238
+ flight by a litellm callback, guardrail or custom provider, or by a
239
+ task created inside the call, is neither policed nor metered.
@@ -0,0 +1,179 @@
1
+ # flightdeck-sensor
2
+
3
+ In-process agent observability sensor for [Flightdeck](https://github.com/flightdeckhq/flightdeck).
4
+
5
+ ## Local development venv
6
+
7
+ Flightdeck pins **Python 3.12** for local development. The sensor
8
+ supports `3.11 ≤ x < 3.15` (pyproject `requires-python`); `sensor/.venv`
9
+ is the canonical interpreter for every Make target that runs Python.
10
+
11
+ Recreate from a fresh clone or after a `rm -rf sensor/.venv`:
12
+
13
+ ```bash
14
+ python3.12 -m venv sensor/.venv
15
+ ./sensor/.venv/bin/python -m pip install -e ".[dev,anthropic,openai]"
16
+ ```
17
+
18
+ `make -C sensor install` does the same once the venv exists. To get
19
+ the exact versions CI tests with, install from the lockfile instead:
20
+
21
+ ```bash
22
+ cd sensor && uv sync --locked --python 3.12 --extra dev --extra anthropic --extra openai
23
+ ```
24
+
25
+ CI installs from `sensor/uv.lock` into the `actions/setup-python`
26
+ interpreter and overrides via env (`PYTHON=python make -C sensor
27
+ test`). The lock covers Python 3.11 to 3.14 and both CI lanes: the
28
+ framework lane (`dev` extra) and the newest-SDK lane (`sdk-majors`
29
+ dependency group). `uv lock` resolves only releases at least 14 days
30
+ old, anthropic a day old (`[tool.uv]` in `pyproject.toml`).
31
+
32
+ ## Optional `session_id` hint
33
+
34
+ By default `init()` auto-generates a fresh UUID every time the process
35
+ starts. Orchestrators that re-run the same logical workflow (Temporal,
36
+ Airflow, cron) can instead pass a stable identifier; if the backend
37
+ already has a row for that session, the new execution is attached to it
38
+ and appears as a continuation of the prior run in the fleet view.
39
+
40
+ Supply the hint via either the `session_id=` kwarg or the
41
+ `FLIGHTDECK_SESSION_ID` environment variable. The env var takes
42
+ precedence.
43
+
44
+ The value MUST parse as a canonical UUID (any version) -- the
45
+ sessions table column is UUID-typed. If you pass a non-UUID the
46
+ sensor logs a warning and falls back to auto-generating one.
47
+ Orchestrators that use string identifiers (Temporal workflow_id,
48
+ Airflow dag_run_id) should hash the identifier into a deterministic
49
+ UUID with `uuid.uuid5`.
50
+
51
+ ### Temporal workflow example
52
+
53
+ ```python
54
+ import uuid
55
+ import flightdeck_sensor as fd
56
+ from temporalio import workflow
57
+
58
+ # Pick any fixed namespace UUID for your deployment. The same
59
+ # workflow_id + namespace always produces the same session UUID,
60
+ # so re-runs of the same workflow all map to the same sessions row.
61
+ FLIGHTDECK_NS = uuid.UUID("00000000-0000-0000-0000-000000000001")
62
+
63
+
64
+ @workflow.defn
65
+ class MyWorkflow:
66
+ @workflow.run
67
+ async def run(self, input):
68
+ ctx = workflow.info()
69
+ fd.init(
70
+ server="http://flightdeck.internal/ingest",
71
+ token="ftd_...",
72
+ session_id=str(uuid.uuid5(FLIGHTDECK_NS, ctx.workflow_id)),
73
+ )
74
+ # If this workflow_id has run before, the backend attaches
75
+ # this execution to the existing session automatically; the
76
+ # sensor logs INFO on the first response that confirms it.
77
+ ...
78
+ ```
79
+
80
+ The sensor logs a single WARNING at `init()` time whenever a custom
81
+ `session_id` is in play so the behaviour is visible in operational
82
+ logs, and an INFO line on the first response where the backend
83
+ confirms attachment. See ARCHITECTURE.md ("Session attachment flow")
84
+ for the full protocol.
85
+
86
+ ## Framework support
87
+
88
+ `flightdeck_sensor.patch()` installs six interceptors process-wide:
89
+ three LLM-call interceptors (class-level or module-level), one MCP
90
+ interceptor, and two sub-agent interceptors. Every LLM call that
91
+ flows through a patched entry point emits a `pre_call` / `post_call`
92
+ event pair without any framework-specific wiring; the MCP and
93
+ sub-agent interceptors emit their own event types, described in the
94
+ table.
95
+
96
+ | Framework | Interceptor | Entry points |
97
+ | --- | --- | --- |
98
+ | Anthropic SDK — every client class (`Anthropic`, `AnthropicBedrock`, `AnthropicVertex`, `AnthropicFoundry`, `AnthropicAWS`, `AnthropicBedrockMantle`, `AnthropicGoogleCloud` and their async forms), direct or via a framework that constructs one internally (LangChain, LlamaIndex, CrewAI native) | `patch_anthropic_classes` | `messages.create` (including `stream=True`), `.stream`, `.parse`, async + sync, beta resources, the raw view (`with_raw_response`) and the streaming view (`with_streaming_response`, metered at `parse()`); streamed calls are metered from the in-stream usage events; every `post_call` carries `provider_metadata.platform` (`anthropic`, `bedrock`, `bedrock-mantle`, `aws`, `vertex`, `google-cloud`, `foundry`, or a gateway's host) |
99
+ | OpenAI SDK (direct or via a framework that constructs `OpenAI()` internally — LangChain, LlamaIndex, CrewAI native) | `patch_openai_classes` | `OpenAI.chat.completions.create` / `.parse`, `.responses.create` / `.parse`, and their `with_raw_response` counterparts (`create` / `parse`), `.embeddings.create`, async + sync; `create(stream=True)` on chat and Responses is metered from the chat usage chunk and `response.completed`; the `.stream()` helpers and `with_streaming_response` are not intercepted |
100
+ | litellm (router that aggregates many providers behind one function surface) | `patch_litellm_functions` | `litellm.completion`, `litellm.acompletion`, `litellm.embedding`, `litellm.aembedding` |
101
+ | MCP (official Python `mcp` SDK, reached transitively by `langchain-mcp-adapters`, `llama-index-tools-mcp` and `mcpadapt`) | `patch_mcp_classes` | `mcp.client.session.ClientSession.initialize` on mcp 1.x / `.adopt` on mcp 2.x (server fingerprint capture, every 2.x handshake path), `.list_tools`, `.call_tool`, `.list_resources`, `.read_resource`, `.list_prompts`, `.get_prompt` (one `mcp_*` event type each), plus the stdio / SSE / streamable-HTTP / websocket transport client factories and mcp 2.x's `Client._build_session` for transport attribution. `call_tool` also enforces the MCP Protection Policy. Supported SDK range: `mcp>=1.20,<3`. |
102
+ | CrewAI (sub-agent sessions) | `patch_crewai_classes` | `crewai.Agent.execute_task` / `.aexecute_task` — each Crew member's turn lands as a child session (`session_start` / `session_end`) with `parent_session_id`, `agent_role` taken from `Agent.role`, and incoming / outgoing message capture gated on `capture_prompts` |
103
+ | LangGraph (sub-agent sessions) | `patch_langgraph_classes` | `langgraph.graph.StateGraph.add_node` — the registered node callable is wrapped so each node run lands as a child session with `agent_role` taken from the node name; every node is treated as agent-bearing unless narrowed with `init(langgraph_agent_node_pattern=...)` |
104
+
105
+ ### litellm example
106
+
107
+ litellm's Anthropic route uses raw httpx instead of the Anthropic
108
+ SDK, so it bypasses the SDK-class patches. The litellm interceptor
109
+ patches `litellm.completion` / `litellm.acompletion` directly to
110
+ close this gap. An Anthropic model string via litellm exercises the
111
+ bypass case:
112
+
113
+ ```python
114
+ import flightdeck_sensor
115
+ import litellm
116
+
117
+ flightdeck_sensor.init(
118
+ server="http://flightdeck.internal/ingest",
119
+ token="ftd_...",
120
+ )
121
+ flightdeck_sensor.patch()
122
+
123
+ # After patch(), this call routes through the sensor's pre/post-call
124
+ # plumbing regardless of which underlying provider litellm picks for
125
+ # the model string.
126
+ response = litellm.completion(
127
+ model="claude-haiku-4-5-20251001",
128
+ messages=[{"role": "user", "content": "hi"}],
129
+ max_tokens=5,
130
+ )
131
+ ```
132
+
133
+ Install the optional dependency: `pip install flightdeck-sensor[litellm]`
134
+ (the extra requires `litellm>=1.83.0`; 1.82.7 and 1.82.8 were compromised
135
+ builds).
136
+
137
+ ### What the litellm interceptor catches, and what it doesn't
138
+
139
+ **Catches.** Direct callers of `litellm.completion(**kwargs)` and
140
+ `litellm.acompletion(**kwargs)` — the vast majority of litellm
141
+ integrations, including the default chat-completion surface exposed
142
+ by user code, Router, and frameworks that route through the public
143
+ API (CrewAI non-native flavors, langchain-community's litellm
144
+ adapter, etc.) — streaming included: `stream=True` on either returns
145
+ the sensor's stream guard around litellm's `CustomStreamWrapper`,
146
+ iterated the same way (`for` / `async for`), metered from the token
147
+ counts litellm reports in its trailing usage chunk; that chunk is
148
+ kept from a caller who did not ask for `stream_options.include_usage`.
149
+ A call litellm serves through the OpenAI SDK (which the sensor also
150
+ patches) is counted once, by the litellm wrapper. Embedding calls
151
+ through `litellm.embedding(**kwargs)` and `litellm.aembedding(**kwargs)`
152
+ are wrapped the same way, so embedding-model token usage lands too
153
+ (`12_litellm.py` in the playground exercises sync and async streaming,
154
+ the single-count assertion and the capture round-trip).
155
+
156
+ **Does NOT catch.**
157
+ - **Lower-level litellm entry points.** Some integrations reach
158
+ past `completion` into `litellm.llms.custom_httpx.http_handler`
159
+ or other internal helpers directly. Those calls bypass the
160
+ module-level wrapper. A broader httpx-level interceptor was
161
+ considered when the litellm interceptor was scoped and deferred —
162
+ if framework reports surface calls that slip past the current
163
+ coverage, file a new issue.
164
+ - **`litellm.text_completion` / `atext_completion` and
165
+ `litellm.responses` / `aresponses`.** Neither is wrapped (root
166
+ `README.md` Roadmap), but some routes are still reported. The legacy
167
+ completion API calls `completion` through the name bound in
168
+ `litellm.main`, which the module-attribute swap does not reach; on
169
+ an OpenAI chat model the OpenAI interceptor reports the SDK call it
170
+ makes, and litellm's own httpx routes are unobserved. The Responses
171
+ API on a provider without a native Responses API (for example
172
+ Anthropic) is bridged through the patched `litellm.completion` and reported
173
+ once as a chat call; native routes (OpenAI) go through litellm's own
174
+ httpx handler and are unobserved.
175
+ - **SDK calls made from inside a litellm call.** The litellm wrapper
176
+ marks the call it serves so the SDK interceptors pass it through;
177
+ the mark covers the whole call, so an SDK call made while it is in
178
+ flight by a litellm callback, guardrail or custom provider, or by a
179
+ task created inside the call, is neither policed nor metered.