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.
- flightdeck_sensor-0.6.0/CLAUDE.md +78 -0
- flightdeck_sensor-0.6.0/PKG-INFO +239 -0
- flightdeck_sensor-0.6.0/README.md +179 -0
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/flightdeck_sensor/__init__.py +146 -76
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/flightdeck_sensor/config.py +6 -2
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/flightdeck_sensor/core/agent_id.py +8 -10
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/flightdeck_sensor/core/context.py +7 -7
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/flightdeck_sensor/core/errors.py +311 -34
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/flightdeck_sensor/core/exceptions.py +7 -7
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/flightdeck_sensor/core/mcp_policy.py +55 -31
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/flightdeck_sensor/core/policy.py +8 -8
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/flightdeck_sensor/core/schemas.py +1 -1
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/flightdeck_sensor/core/session.py +156 -56
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/flightdeck_sensor/core/types.py +126 -22
- flightdeck_sensor-0.6.0/flightdeck_sensor/interceptor/anthropic.py +1164 -0
- flightdeck_sensor-0.6.0/flightdeck_sensor/interceptor/base.py +1780 -0
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/flightdeck_sensor/interceptor/crewai.py +9 -5
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/flightdeck_sensor/interceptor/langgraph.py +30 -16
- flightdeck_sensor-0.6.0/flightdeck_sensor/interceptor/litellm.py +544 -0
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/flightdeck_sensor/interceptor/mcp.py +369 -182
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/flightdeck_sensor/interceptor/mcp_identity.py +8 -9
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/flightdeck_sensor/interceptor/openai.py +406 -58
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/flightdeck_sensor/provider.py +13 -13
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/flightdeck_sensor/providers/anthropic.py +209 -49
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/flightdeck_sensor/providers/litellm.py +177 -9
- flightdeck_sensor-0.6.0/flightdeck_sensor/providers/openai.py +779 -0
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/flightdeck_sensor/providers/protocol.py +104 -1
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/flightdeck_sensor/transport/client.py +13 -11
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/flightdeck_sensor/transport/retry.py +13 -10
- flightdeck_sensor-0.6.0/pyproject.toml +222 -0
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/conftest.py +85 -2
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/unit/test_agent_id.py +9 -9
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/unit/test_agent_type_validation.py +1 -1
- flightdeck_sensor-0.6.0/tests/unit/test_anthropic_resource_patch.py +1512 -0
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/unit/test_compat_crewai_mcp.py +2 -2
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/unit/test_cross_agent_messages.py +36 -11
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/unit/test_custom_directives.py +2 -2
- flightdeck_sensor-0.6.0/tests/unit/test_egress_url_validation.py +235 -0
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/unit/test_errors.py +354 -6
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/unit/test_framework_attribution.py +50 -44
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/unit/test_interceptor.py +44 -24
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/unit/test_interceptor_crewai.py +26 -3
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/unit/test_interceptor_langgraph.py +116 -2
- flightdeck_sensor-0.6.0/tests/unit/test_interceptor_litellm.py +1038 -0
- flightdeck_sensor-0.6.0/tests/unit/test_mcp_2x_paths.py +141 -0
- flightdeck_sensor-0.6.0/tests/unit/test_mcp_availability.py +45 -0
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/unit/test_mcp_identity.py +1 -1
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/unit/test_mcp_interceptor.py +117 -61
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/unit/test_mcp_policy_emission_enriched.py +6 -6
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/unit/test_mcp_policy_enforcement.py +54 -5
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/unit/test_mcp_policy_enforcement_all_paths.py +4 -4
- flightdeck_sensor-0.6.0/tests/unit/test_openai_parse_responses.py +1390 -0
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/unit/test_originating_event_id.py +1 -1
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/unit/test_patch.py +148 -143
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/unit/test_policy.py +3 -3
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/unit/test_policy_decision_summary.py +2 -2
- flightdeck_sensor-0.6.0/tests/unit/test_policy_directive_events.py +113 -0
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/unit/test_policy_emission_enriched.py +3 -3
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/unit/test_policy_events.py +5 -5
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/unit/test_prompt_capture.py +2 -2
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/unit/test_provider_enum.py +6 -6
- flightdeck_sensor-0.6.0/tests/unit/test_providers.py +504 -0
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/unit/test_session.py +12 -6
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/unit/test_session_lifecycle_enrichment.py +49 -1
- flightdeck_sensor-0.6.0/tests/unit/test_stop_reason_errors.py +528 -0
- flightdeck_sensor-0.6.0/tests/unit/test_streaming_usage.py +1837 -0
- 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
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/unit/test_transport.py +20 -8
- flightdeck_sensor-0.6.0/uv.lock +6504 -0
- flightdeck_sensor-0.5.2/PKG-INFO +0 -190
- flightdeck_sensor-0.5.2/README.md +0 -141
- flightdeck_sensor-0.5.2/flightdeck_sensor/interceptor/anthropic.py +0 -517
- flightdeck_sensor-0.5.2/flightdeck_sensor/interceptor/base.py +0 -1194
- flightdeck_sensor-0.5.2/flightdeck_sensor/interceptor/litellm.py +0 -392
- flightdeck_sensor-0.5.2/flightdeck_sensor/providers/openai.py +0 -396
- flightdeck_sensor-0.5.2/pyproject.toml +0 -149
- flightdeck_sensor-0.5.2/tests/unit/test_providers.py +0 -202
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/.gitignore +0 -0
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/Makefile +0 -0
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/flightdeck_sensor/compat/__init__.py +0 -0
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/flightdeck_sensor/compat/crewai_mcp.py +0 -0
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/flightdeck_sensor/core/__init__.py +0 -0
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/flightdeck_sensor/interceptor/__init__.py +0 -0
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/flightdeck_sensor/providers/__init__.py +0 -0
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/flightdeck_sensor/py.typed +0 -0
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/flightdeck_sensor/transport/__init__.py +0 -0
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/__init__.py +0 -0
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/unit/__init__.py +0 -0
- {flightdeck_sensor-0.5.2 → flightdeck_sensor-0.6.0}/tests/unit/test_context.py +0 -0
- {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.
|