Add internal shared core for the LangChain-family plugins#1650
Draft
DABH wants to merge 5 commits into
Draft
Conversation
The langgraph plugin located the workflow's stream by importing the private publish-signal name and introspecting the registered handler's __self__ — fragile coupling to another contrib package's internals. Expose one audited accessor that owns the introspection (with an isinstance guard) and migrate langgraph's presence check and instance fetch onto it. The interceptor's error message is unchanged; the workflow wrapper gains a defensive error on a path the interceptor already makes unreachable.
The LangSmith workflow-safety override for @Traceable's aio_to_thread seam existed twice — in contrib.langsmith and contrib.deepagents — and the two copies interact through langsmith's single process-wide override slot (an earlier deepagents uninstall would have stripped the langsmith plugin's override permanently). Introduce temporalio.contrib._langchain, an internal shared package for the LangChain-family plugins (import-free __init__, lazy third-party imports, ships unconditionally like other contrib infrastructure), and move the override there with langsmith's install-once semantics: failures propagate and leave the flag unset so a retry can succeed. langsmith calls the shared installer bare (its dependency is guaranteed); deepagents keeps its exception-tolerant wrapper for workers without langsmith installed.
The langgraph and deepagents plugins each implemented the same ContextVar-backed dict cache for carrying completed results across continue-as-new, with different key derivations. Move the langgraph mechanism (the released, vetted one) into the shared core as TaskResultCache — one instance per plugin, so the caches never share state — plus its task_id/cache_key helpers verbatim. langgraph's _task_cache module becomes a delegating surface with identical function signatures and semantics (identity-preserving set, same 32-hex keys); deepagents rebuilds its result cache on the shared mechanism, keeping copy-on-set and snapshot semantics at its own layer. deepagents cache keys change format (previously 64-hex over a different composition) — acceptable while unreleased; only an in-flight continue-as-new carrying cache hits across a worker upgrade would notice.
langgraph and deepagents each stripped RunnableConfig for the activity boundary with deliberately different selection rules: langgraph whitelists its seven checkpoint/resumption configurable keys and passes metadata through untouched; deepagents keeps any JSON-safe non-dunder configurable entry and filters metadata values. Move the mechanism to the shared core with langgraph's released output shape as the base (tags/metadata always present, truthy run_name/run_id, whitelist iterated in whitelist order) and the selection rules as parameters. langgraph's strip becomes a thin wrapper over its whitelist — a byte-identity test embeds the pre-refactor implementation verbatim and string-compares JSON dumps plus derived continue-as-new cache keys across a corpus, so released payloads and carried caches are pinned. set_langgraph_config now shares the whitelist constant instead of duplicating the tuple. deepagents adopts the shared shape (accepted deltas on an unreleased plugin: always-present tags/metadata, truthy gates, no str() coercion of run ids) and re-exports the shared rebuild/is_jsonish under its existing names.
…rough merge Move the remaining family-generic machinery out of the deepagents plugin into the shared core: langchain_core dumpd/load message and object serialization plus the OpenAI tool-schema advertisement; the exclude_unset Pydantic payload converter and its compose-or-refuse data-converter builder (error text now names the refusing plugin); the activity auto-heartbeater and duck-typed LLM HTTP error translation; and the order-preserving passthrough-list merge (mechanism only — each plugin keeps its own module list, since the lists are released behavior for langgraph/langsmith). deepagents re-exports everything under its existing names, so its sibling modules and tests are unchanged. The converter module is the one core module with an eager contrib.pydantic import; it is only imported by plugins that wire the converter, and the core __init__ stays import-free.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Stacked on #1644. The three LangChain-family plugins (
contrib.langgraph,contrib.langsmith,contrib.deepagents) duplicated machinery; this PR factors it intotemporalio/contrib/_langchain/— an internal package (underscore-prefixed, no public-API commitment) that ships unconditionally like other contrib infrastructure, with an import-free__init__and lazy third-party imports so it loads with none of the family distributions installed (the 3.10 CI lanes, where the deepagents extra cannot install, exercise exactly that).Prime directive applied throughout: langgraph/langsmith are released and vetted — their observable behavior is frozen; deepagents (unreleased) adapts to their semantics wherever copies disagreed.
_aio_to_thread— the LangSmith workflow-safety override existed in bothlangsmith/_interceptor.pyanddeepagents/_plugin.py, interacting through langsmith's single process-wide override slot. One copy now, with langsmith's install-once semantics (failures propagate, flag stays unset for retry); langsmith calls it bare, deepagents keeps its exception-tolerant wrapper._task_cache— langgraph's continue-as-new cache mechanism verbatim (task_id, 32-hexcache_key), wrapped in aTaskResultCacheclass so each plugin gets its own ContextVar instance and caches never share state. langgraph's module delegates with identical signatures and identity semantics; deepagents rebuilds its result cache on it (its unreleased cache-key format changes — only an in-flight continue-as-new carrying hits across a worker upgrade would notice)._runnable_config— parameterizedstrip_runnable_configwith langgraph's released output shape as the base and the selection rules (checkpoint-key whitelist vs JSON-safe filter) as parameters. Byte-identity is test-enforced: the pre-refactor langgraph implementation is embedded verbatim in the test suite and string-compared (JSON dumps, capturing key order) against the new path across a corpus of configs, including derived cache keys._messages/_converter/_activity_helpers/_passthrough— family-generic serde (dumpd/load, tool schemas), theexclude_unsetPydantic converter + compose-or-refuse builder, the activity auto-heartbeater and duck-typed LLM HTTP error translation, and the passthrough-list merge (mechanism only — each plugin keeps its own list, since the lists are released behavior).workflow_streams.current_workflow_stream()(small public addition): langgraph previously imported the private publish-signal name and introspected the handler's__self__; the accessor owns that introspection with an isinstance guard._PUBLISH_SIGNALstays private.Verification
25 new unit tests under
tests/contrib/langchain_shared/(byte-identity oracle, cache-instance isolation incl. acrosscopy_context(), override install-once/retry semantics, serde round-trips, converter composition, passthrough merge) + acurrent_workflow_stream()test.All existing suites unchanged and green as regression oracles: langgraph (incl. the continue-as-new cache oracle and streaming), langsmith (incl. the 1150-line interceptor suite and
test_background_io), deepagents, workflow_streams.Whole repo: 1511 passed / 28 skipped; pyright, basedpyright (0 warnings), mypy (379 files), pydocstyle, pydoctor, ruff all clean repo-wide.
Samples verification: every LangGraph, LangSmith, and Deep Agents sample in samples-python runs green against this branch — offline suites plus live end-to-end runs (real dev server, real model calls; LangSmith scenarios verified by querying the LangSmith API for the landed trace trees). Full matrix:
langgraph_pluginoffline suite (10) +langsmith_tracingoffline suite +deepagents_pluginoffline suite (23 tests total)langgraph_plugin/graph_api/{hello_world, react_agent, human_in_the_loop, continue_as_new}langgraph_plugin/functional_api/{hello_world, control_flow, continue_as_new, human_in_the_loop, react_agent}deepagents_plugin/{hello_world, react_agent, subagents, filesystem_backend, streaming, continue_as_new, human_in_the_loop}langgraph_plugin/graph_api/langsmith_tracing+functional_api/langsmith_tracingRunWorkflowroot → activity → LangGraph →ChatAnthropic)deepagents_plugin/langsmith_tracingTemporalModel+ChatAnthropicspans land, correctly linked¹langsmith_tracing/{basic, chatbot}Human-in-the-loop scenarios were driven with real approve inputs;
LANGSMITH_TRACING=trueset for the tracing scenarios (the langsmith SDK's env gate — without it the plugin correctly creates no runs). A fresh-venvpip install "temporalio[…] @ git+…@langchain-shared-core"was verified to build and import all four packages.¹ One pre-existing nit found (not a delta from this PR — this composition's first keyed run): the outermost in-workflow agent-root span can miss the final trace flush because the sample's single-process driver exits immediately after the workflow result; every other span posts and links correctly. Will track on the samples PR.
Commits are staged for review in dependency order: accessor → core+override → cache → config strip → serde/converter/helpers.