Skip to content

feat(instrument): Typed Pydantic event foundation + agno reference (1/17 adapters)#129

Closed
mmercuri wants to merge 4 commits into
feat/instrument-multitenancy-org-id-propagationfrom
feat/instrument-typed-events-foundation
Closed

feat(instrument): Typed Pydantic event foundation + agno reference (1/17 adapters)#129
mmercuri wants to merge 4 commits into
feat/instrument-multitenancy-org-id-propagationfrom
feat/instrument-typed-events-foundation

Conversation

@mmercuri

Copy link
Copy Markdown
Contributor

Summary

Establishes the typed-event foundation for the instrument layer and ports the agno framework adapter to the new emission contract as the reference implementation. 16 framework adapters + protocol/provider adapters remain on the legacy emit_dict_event path — see docs/adapters/typed-events-followups.md for the per-adapter backlog. This is a stacked PR over PR #118 (multi-tenancy org_id propagation).

Honest disclosure (CLAUDE.md item 11)

This PR delivers 1/17 framework adapters migrated. The remaining 16 still emit via emit_dict_event and trigger DeprecationWarning on every call site. The foundation is what unblocks per-adapter migration PRs to land in parallel without coordinating on the dict-vs-typed shape. The legacy path is preserved (with deprecation warning) so existing customer apps don't break mid-rollout. Each unmigrated adapter is listed with current site count + recommended migration order in the followups doc.

What's in this PR

Foundation

  • New module src/layerlens/instrument/_compat/events.py — vendors canonical stratix.core.events payload models behind a Pydantic v1/v2-compatible facade. Exposes ALL_TYPED_EVENTS registry (12 canonical event types), BaseEvent Protocol, validate_typed_event() / coerce_to_dict() helpers, and TypedEventValidationError.
  • Per-adapter ALLOW_UNREGISTERED_EVENTS toggle on BaseAdapter. Default False (strict). Importer-style adapters opt into True for source data whose taxonomy diverges from the canonical schema.

Dual-path emission contract

  • emit_event(typed_payload) — preferred. Runs canonical schema validation that REJECTS malformed inputs by raising TypedEventValidationError. Validation failures also increment the circuit-breaker error counter (CLAUDE.md "never silently skip failing operations").
  • emit_dict_event(event_type, dict) — legacy. Emits DeprecationWarning on every call. Does NOT run canonical validation (the 16 unmigrated adapters use adapter-specific dict shapes that intentionally diverge — running the validator would reject 100% of unmigrated emissions). Multi-tenant org_id stamp still runs.

agno reference migration

  • All 11 emit sites in agno/lifecycle.py migrated from emit_dict_eventemit_event. Verified by grep -c "emit_dict_event(" src/.../agno/ returning 0.
  • Agno-specific provenance moved into canonical models' metadata / attributes / parameters slots.
  • Previously-emitted agent.state.change (no real state hashes) folded into AgentOutputEvent.metadata.run_status. The canonical AgentStateChangeEvent requires sha256:<hex64> before/after hashes that agno cannot surface honestly.
  • New _sha256_of(value) helper produces canonical sha256:<hex64> strings for AgentHandoffEvent.handoff_context_hash.

Tests

  • tests/instrument/adapters/_base/test_typed_events.py23 tests (registry contract 2, validate_typed_event behaviour 7, coerce_to_dict 2, dual-path emission 12).
  • tests/instrument/adapters/frameworks/test_agno_adapter.py extended with 4 typed-event regression tests; existing assertions updated to read canonical payload paths.

Docs

  • docs/adapters/typed-events.md — full migration guide with per-adapter checklist and worked example.
  • docs/adapters/typed-events-followups.md — per-adapter backlog with current emit_dict_event site counts (honest, vs the spec's optimistic projections).

Stacking

Acceptance

Check Result
uv run pytest tests/instrument/adapters/_base/test_typed_events.py -x 23/23 pass
uv run pytest tests/instrument/adapters/frameworks/test_agno_adapter.py -x 16/16 pass
uv run pytest tests/instrument/adapters/frameworks/ 112/112 pass (no regression in 16 unmigrated)
grep -c "emit_dict_event(" src/layerlens/instrument/adapters/frameworks/agno/ 0
mypy --strict src/.../events.py src/.../adapters/_base/adapter.py clean
mypy --strict src/.../adapters/frameworks/agno/lifecycle.py clean
ruff check (all modified + new files) clean

Test plan

  • Foundation tests pass (tests/instrument/adapters/_base/test_typed_events.py)
  • Agno migration tests pass (tests/instrument/adapters/frameworks/test_agno_adapter.py)
  • No regression in 16 unmigrated adapters
  • Zero emit_dict_event( call sites in agno
  • Mypy strict clean on events.py, adapter.py, agno/lifecycle.py
  • Ruff clean on all modified + new files
  • Honest disclosure of 16 adapters still pending (no "planned backlog" dressing)

mmercuri and others added 4 commits April 25, 2026 19:13
Bootstraps the LayerLens instrument layer with the abstract base classes,
adapter registry, capture configuration, event sinks, vendored event
schemas, and pydantic v1/v2 compatibility shim that every concrete
adapter (frameworks, protocols, providers) will depend on.

Scope
-----
- src/layerlens/instrument/__init__.py: lean re-export surface
- src/layerlens/instrument/_vendored/: frozen ateam event schemas (no
  runtime ateam dependency)
- src/layerlens/instrument/adapters/_base/: BaseAdapter, AdapterRegistry,
  AdapterStatus, AdapterHealth, AdapterCapability, ReplayableTrace,
  CaptureConfig, EventSink, TraceStoreSink, IngestionPipelineSink,
  PydanticCompat
- src/layerlens/_compat/pydantic.py: model_dump/model_validate shim
  spanning pydantic v1 + v2
- scripts/{port_adapter,port_protocol,emit_adapter_manifest,
  regen_dep_baselines}.py: codegen helpers used to port the rest of M1
- tests/instrument/{test_base_layer,test_lazy_imports,
  test_default_install,test_resolved_dep_tree}.py + _baselines/
- .github/workflows/dep-tree-guard.yaml: CI gate that locks the default
  install footprint
- docs/adapters/: CONTRIBUTING, STATUS, pydantic-compatibility, testing,
  PERSONA_REVIEW

Blast radius
------------
- Pure additions. No public surface changes outside the new
  layerlens.instrument namespace.
- Default `pip install layerlens` install set is unchanged (verified by
  test_default_install.py against the new baseline).
- Lazy adapter discovery: importing layerlens.instrument MUST NOT pull
  in any optional adapter dep (verified by test_lazy_imports.py).

Test plan
---------
- uv run pytest tests/instrument/test_base_layer.py
  tests/instrument/test_lazy_imports.py -x  -> 45 passed
- The dep-tree-guard workflow exercises test_default_install.py and
  test_resolved_dep_tree.py against the new baselines on every PR.

LAY-3400 umbrella: this PR is the prerequisite for the M1.B/M1.C/M1.D
adapter ports, M7 protocol certification, and M8 Cohere/Mistral.
Auto-fixed by 'ruff check --fix'. No behavior change.
…base

Brings in the foundational instrument package (capture, registry,
pydantic_compat, _vendored event models, _compat/pydantic shim) so
this branch can build/test in isolation. PR #118
(feat/instrument-multitenancy-org-id-propagation) modifies _base
files but does not include the foundation modules they import — a
stacked-PR situation. Merging base-foundation here resolves the
import chain and produces a self-contained branch.

Conflict resolution used -X ours so the org_id propagation changes
from PR #118 are preserved on every conflict.

No functional code authored in this commit — pure dependency merge.
…/17)

Establishes the typed-event foundation for the instrument layer and
ports the agno framework adapter to the new emission contract as the
reference implementation. 16 framework adapters + provider/protocol
adapters remain on the legacy emit_dict_event path; see
docs/adapters/typed-events-followups.md for the per-adapter backlog.

Foundation changes
------------------

* New module src/layerlens/instrument/_compat/events.py vendors the
  canonical event payload models from
  ateam/stratix/core/events/{l1_io,l3_model,l4_environment,l5_tools,
  cross_cutting}.py through layerlens.instrument._vendored. Exposes
  ALL_TYPED_EVENTS registry (12 canonical event types), BaseEvent
  Protocol, and validate_typed_event() / coerce_to_dict() helpers.
* New TypedEventValidationError raises when payloads fail canonical
  schema validation. The validator REJECTS malformed inputs rather
  than silently emitting them — CLAUDE.md "never silently skip
  failing operations" applied to the emission boundary.
* BaseAdapter gains a per-adapter ALLOW_UNREGISTERED_EVENTS class
  attribute (default False). Strict adapters reject unknown event
  types; importer-style adapters (langfuse, benchmark_import) opt
  into True for source data whose taxonomy genuinely diverges from
  the canonical schema.

Dual-path emission contract
---------------------------

* emit_event(typed_payload) is the preferred path. It runs canonical
  schema validation through validate_typed_event() before forwarding
  the payload to the stratix client. Validation failures raise
  TypedEventValidationError AND increment the circuit-breaker error
  count (so persistent validation bugs eventually trip the breaker).
* emit_dict_event(event_type, dict) is the legacy path. It now emits
  a DeprecationWarning on every call. It does NOT run canonical
  validation — the 16 unmigrated adapters use adapter-specific dict
  shapes that intentionally diverge from the canonical schema; the
  warning is what keeps the gap visible in CI until each adapter
  migrates. The org_id stamp still runs on this path so
  multi-tenant scoping is preserved during the transition.

agno reference migration
------------------------

All 11 emit_dict_event call sites in
src/layerlens/instrument/adapters/frameworks/agno/lifecycle.py
replaced with emit_event(typed_payload) calls. Verified with
`grep -c emit_dict_event src/.../agno/` returning 0. Agno-specific
provenance fields (framework, agent_name, timestamp_ns) moved into
the canonical models' metadata / attributes / parameters slots.
The previously-emitted agent.state.change marker (which carried only
event_subtype with no real state hashes) is folded into the
AgentOutputEvent.metadata.run_status field — the canonical
AgentStateChangeEvent requires sha256:<hex64> before/after hashes,
which agno cannot surface without upstream instrumentation.
AgentHandoffEvent emissions now always carry a sha256
handoff_context_hash via a new _sha256_of helper.

Tests
-----

* tests/instrument/adapters/_base/test_typed_events.py — 23 tests
  covering registry contract (2), validate_typed_event behaviour (7),
  coerce_to_dict (2), and dual-path emission via BaseAdapter (12).
* tests/instrument/adapters/frameworks/test_agno_adapter.py extended
  with 4 typed-event regression tests
  (test_agno_emits_typed_payloads_only,
  test_agno_emit_does_not_warn_after_migration,
  test_agno_typed_handoff_validates_canonical_hash,
  test_agno_typed_emission_records_org_id) and existing assertions
  updated to read from canonical payload paths.
* No regression in 112 tests across the 16 unmigrated framework
  adapters (validated via pytest tests/instrument/adapters/frameworks/).

Honest disclosure (CLAUDE.md item 11)
-------------------------------------

This PR delivers 1/17 framework adapters migrated. The remaining 16
adapters still emit via emit_dict_event and trigger
DeprecationWarning on every call site. Each unmigrated adapter is
listed with current emit_dict_event site count in
docs/adapters/typed-events-followups.md, including the recommended
migration order. The legacy path will be removed in the next major
SDK release (2.0.0) once all adapters have migrated; CI should run
pytest -W error::DeprecationWarning against post-migration adapters
to enforce no new emit_dict calls slip in.

Acceptance
----------

* `uv run pytest tests/instrument/adapters/_base/test_typed_events.py`: 23/23 pass
* `uv run pytest tests/instrument/adapters/frameworks/test_agno_adapter.py`: 16/16 pass
* `uv run pytest tests/instrument/adapters/frameworks/`: 128/128 pass (no regression)
* `grep -c emit_dict_event src/layerlens/instrument/adapters/frameworks/agno/`: 0
* `mypy --strict src/layerlens/instrument/_compat/events.py src/layerlens/instrument/adapters/_base/adapter.py`: clean
* `ruff check`: clean across all modified + new files
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants