Add internal shared core for the LangChain-family plugins#1650
Draft
DABH wants to merge 9 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.
ruff's isort (combine-as-imports=false, the repo default) splits aliased imports onto separate statements, so the `name as name` re-export idiom and the `as _shared_*` renames each produced a run of single-name import blocks from the same module. Replace the verbatim re-exports with plain grouped imports plus explicit __all__ lists, and the shadow-renamed internals with namespace imports (_converter.build_data_converter, _runnable_config.strip_runnable_config, _task_cache.cache_key), so each source module contributes a single import statement.
There was a problem hiding this comment.
Pull request overview
This PR factors duplicated machinery across the LangChain-family contrib plugins (langgraph/langsmith/deepagents) into a new internal package temporalio.contrib._langchain, while adding a small public helper current_workflow_stream() to avoid plugins reaching into workflow-streams internals.
Changes:
- Introduces
temporalio.contrib._langchaininternal shared modules (task cache, runnable-config strip/rebuild, message serde, payload converter composition, aio-to-thread override installer, activity helpers, passthrough merge). - Updates langgraph/langsmith/deepagents to delegate to the shared core while preserving released langgraph/langsmith semantics.
- Adds
current_workflow_stream()totemporalio.contrib.workflow_streamsand updates langgraph to use it; adds focused unit tests for the new shared core and the stream accessor.
Reviewed changes
Copilot reviewed 26 out of 27 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| tests/contrib/workflow_streams/test_workflow_streams.py | Adds workflow-level tests for current_workflow_stream() behavior. |
| tests/contrib/langchain_shared/init.py | New test package marker for shared-core tests. |
| tests/contrib/langchain_shared/test_task_cache.py | Tests shared task cache instance isolation, key stability, and langgraph delegation. |
| tests/contrib/langchain_shared/test_serde_messages.py | Tests shared LangChain message/object serde and tool schema export. |
| tests/contrib/langchain_shared/test_runnable_config.py | Byte-identity oracle tests for runnable-config stripping + cache-key stability. |
| tests/contrib/langchain_shared/test_passthrough.py | Tests passthrough-module merge ordering and dedupe. |
| tests/contrib/langchain_shared/test_converter.py | Tests shared data converter composition rules and exclude_unset behavior. |
| tests/contrib/langchain_shared/test_aio_override.py | Tests shared LangSmith aio_to_thread override install-once/retry behavior. |
| tests/contrib/deepagents/test_failures.py | Switches failure translation test to the shared activity helper. |
| temporalio/contrib/workflow_streams/_stream.py | Adds current_workflow_stream() accessor (introspection behind a guard). |
| temporalio/contrib/workflow_streams/init.py | Exports current_workflow_stream as part of the contrib surface. |
| temporalio/contrib/langsmith/_interceptor.py | Delegates aio_to_thread override installation to shared core. |
| temporalio/contrib/langgraph/_workflow.py | Uses current_workflow_stream() instead of private signal name introspection. |
| temporalio/contrib/langgraph/_task_cache.py | Binds langgraph’s cache API surface to shared TaskResultCache. |
| temporalio/contrib/langgraph/_langgraph_config.py | Delegates runnable-config stripping to shared implementation with langgraph whitelist. |
| temporalio/contrib/langgraph/_interceptor.py | Validates streaming config using the new stream accessor. |
| temporalio/contrib/deepagents/_serde.py | Replaces duplicated serde/converter/cache/config/passthrough helpers with shared core. |
| temporalio/contrib/deepagents/_plugin.py | Delegates LangSmith override installation to shared core (exception-tolerant wrapper kept). |
| temporalio/contrib/deepagents/_activity.py | Uses shared activity helpers for auto-heartbeating and HTTP error translation. |
| temporalio/contrib/_langchain/init.py | Adds internal shared package with import-free __init__ and documented import discipline. |
| temporalio/contrib/_langchain/_task_cache.py | New shared continue-as-new task cache + stable task-id/key helpers. |
| temporalio/contrib/_langchain/_runnable_config.py | New shared runnable-config strip/rebuild utilities and JSON-ish predicate. |
| temporalio/contrib/_langchain/_passthrough.py | New shared passthrough-module merge helper. |
| temporalio/contrib/_langchain/_messages.py | New shared LangChain dump/load + tool-schema helper with lazy imports. |
| temporalio/contrib/_langchain/_converter.py | New shared payload converter + DataConverter composition helper. |
| temporalio/contrib/_langchain/_aio_to_thread.py | New shared LangSmith aio_to_thread override + install-once semantics. |
| temporalio/contrib/_langchain/_activity_helpers.py | New shared activity helpers (auto-heartbeater, HTTP error translation). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This reverts commit 100101a.
Tuples pass the check but rehydrate as lists, so a JSON round-trip preserves content, not container type. Applies the substance of the reverted autofix without its syntax damage.
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.