Skip to content

feat(ai): add LangChain v1 agent middleware - #945

Open
gouveags wants to merge 2 commits into
PostHog:mainfrom
gouveags:feat/langchain-v1-middleware
Open

feat(ai): add LangChain v1 agent middleware#945
gouveags wants to merge 2 commits into
PostHog:mainfrom
gouveags:feat/langchain-v1-middleware

Conversation

@gouveags

@gouveags gouveags commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

💡 Motivation and Context

LangChain v1 supports middleware as the main way to package agent integrations, but the Python SDK currently only supports callbacks.

This adds posthog.ai.langchain.middleware.PostHogMiddleware, the Python counterpart to PostHog/posthog-js#4556.

Closes #901.

What changed

  • Added sync and async middleware hooks for agent, model, and tool activity.
  • Kept middleware support in a separate optional module, so the base SDK and existing callback-only imports do not require langchain.
  • Reused the existing LangChain callback conversion and capture logic.
  • Preserved model and tool return values and exceptions exactly; PostHog capture remains fail-open.
  • Kept synchronous delivery off the event-loop thread when Client(sync_mode=True).
  • Added private per-invocation state for trace correlation, privacy-safe checkpoint handling, and cleanup after completed runs.
  • Added per-tool schema fallback so one unsupported tool does not prevent the remaining telemetry from being captured.
  • Added a runnable example and usage guidance.

Use either PostHogMiddleware or CallbackHandler for an agent invocation, not both. Put PostHogMiddleware last in the middleware list so it sees the final model selection and each retry attempt.

On terminal agent failure, LangChain does not call after_agent. In that case, the failed model or tool call is captured without a parent, and no dangling root trace is emitted.

💚 How did you test it?

  • LangChain callback and middleware suites: 105 passed, 8 skipped
  • Full test suite: 2,712 passed, 15 skipped, 38 subtests passed
  • Python 3.10 compatibility
  • Built-wheel smoke test with the minimum supported langchain==1.3.9
  • Ruff formatting and lint
  • Mypy baseline and strict type checks
  • Public API snapshot check
  • Package build and Twine validation
  • Base-package import smoke test without LangChain installed

The middleware tests cover sync and async agent runs, non-blocking async capture, model retries, model and tool failures, returned tool errors, Command results, concurrent invocations, privacy mode, checkpoint cleanup, tool-schema fallback, capture failures, and compatibility with the existing callback integration.

📝 Checklist

  • I reviewed the submitted code.
  • I added tests to verify the changes.
  • I updated the docs if needed.
  • No breaking change or entry added to the changelog.

If releasing new changes

  • Added a Sampo minor changeset.

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Gabriel directed the scope and is the DRI (@gouveags). Codex assisted with implementation, tests, validation, and independent review. No shareable session transcript is available.

The main design choice was to keep middleware support optional and reuse the existing callback capture logic instead of maintaining a second telemetry implementation. Human review is required.

@gouveags

Copy link
Copy Markdown
Contributor Author

@greptileai review

@greptile-apps

greptile-apps Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor
Prompt To Fix All With AI
### Issue 1
posthog/ai/langchain/middleware.py:100
**Recovered failures lose parents**

When an outer retry or recovery middleware handles a model exception and the agent later completes, this call captures the failed attempt without `$ai_parent_id` even though it retains the root trace ID. The failed attempt therefore appears as root-level work instead of a child of the agent. The same issue affects async model failures and sync or async tool failures. Preserve the root relationship for recovered failures, and suppress it only when the invocation is known to have terminated.

### Issue 2
posthog/ai/langchain/middleware.py:114-116
**Async capture blocks execution**

If the supplied client uses `sync_mode=True`, this async wrapper calls synchronous telemetry capture on the event-loop thread after awaiting the model handler. That capture performs HTTP delivery inline, so every model event can block other coroutines; the same applies to agent and tool completion hooks. Run blocking capture work off the event-loop thread or provide an async capture path. The repository directive requires blocking synchronous HTTP calls in async Python to be flagged and avoided.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "feat(ai): add LangChain v1 agent middlew..." | Re-trigger Greptile

Comment thread posthog/ai/langchain/middleware.py
Comment thread posthog/ai/langchain/middleware.py Outdated
@gouveags
gouveags marked this pull request as ready for review September 11, 2026 22:19
@gouveags
gouveags requested a review from a team as a code owner September 11, 2026 22:19
@gouveags

Copy link
Copy Markdown
Contributor Author

@greptileai review

@greptile-apps

greptile-apps Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Reviews (2): Last reviewed commit: "fix(ai): avoid blocking async LangChain ..." | Re-trigger Greptile

@gouveags

Copy link
Copy Markdown
Contributor Author

@greptileai review

@greptile-apps

greptile-apps Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Reviews (3): Last reviewed commit: "fix(ai): avoid blocking async LangChain ..." | Re-trigger Greptile

@marandaneto
marandaneto requested a review from a team September 12, 2026 08:51
@marandaneto

Copy link
Copy Markdown
Member

thanks @gouveags left a comment

@marandaneto marandaneto left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated advisory code review.

try:
response = handler(request)
except BaseException as error:
self._safely_call(self._finish_tool, request.state, run_id, error, False)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blocking: Do not report human-in-the-loop pauses as tool failures — A tool calling LangGraph's interrupt() raises GraphInterrupt to suspend execution normally, but this handler records it as $ai_is_error=True. An agent that resumes and completes successfully therefore still produces a false error span, and can trigger exception autocapture when enabled. Exclude LangGraph control-flow exceptions from failure telemetry in both synchronous and asynchronous wrappers while preserving propagation. Reproduction: reproduced — uv run --frozen pytest posthog/test/ai/langchain/test_review_interrupt.py --timeout=30 -q failed in a disposable regression test because a successful interrupt/resume invocation emitted an error span; a temporary control-flow exception guard made it pass.

@marandaneto
marandaneto requested a review from a team September 12, 2026 09:00
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.

Support LangChain v1 middleware in the Python SDK

2 participants