Skip to content

Add internal shared core for the LangChain-family plugins#1650

Draft
DABH wants to merge 9 commits into
deepagents-pluginfrom
langchain-shared-core
Draft

Add internal shared core for the LangChain-family plugins#1650
DABH wants to merge 9 commits into
deepagents-pluginfrom
langchain-shared-core

Conversation

@DABH

@DABH DABH commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

What

Stacked on #1644. The three LangChain-family plugins (contrib.langgraph, contrib.langsmith, contrib.deepagents) duplicated machinery; this PR factors it into temporalio/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 both langsmith/_interceptor.py and deepagents/_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-hex cache_key), wrapped in a TaskResultCache class 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 — parameterized strip_runnable_config with 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), the exclude_unset Pydantic 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_SIGNAL stays private.

Verification

  • 25 new unit tests under tests/contrib/langchain_shared/ (byte-identity oracle, cache-instance isolation incl. across copy_context(), override install-once/retry semantics, serde round-trips, converter composition, passthrough merge) + a current_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:

    Scenario Mode Result
    langgraph_plugin offline suite (10) + langsmith_tracing offline suite + deepagents_plugin offline suite (23 tests total) pytest
    langgraph_plugin/graph_api/{hello_world, react_agent, human_in_the_loop, continue_as_new} live ✅ 4/4
    langgraph_plugin/functional_api/{hello_world, control_flow, continue_as_new, human_in_the_loop, react_agent} live ✅ 5/5
    deepagents_plugin/{hello_world, react_agent, subagents, filesystem_backend, streaming, continue_as_new, human_in_the_loop} live, real Claude ✅ 7/7
    langgraph_plugin/graph_api/langsmith_tracing + functional_api/langsmith_tracing live, keyed LangSmith ✅ full trace trees land (RunWorkflow root → activity → LangGraph → ChatAnthropic)
    deepagents_plugin/langsmith_tracing live, keyed LangSmith ✅ middleware chain + TemporalModel + ChatAnthropic spans land, correctly linked¹
    langsmith_tracing/{basic, chatbot} live, real OpenAI + keyed LangSmith ✅ full trees rooted at the client-side spans

    Human-in-the-loop scenarios were driven with real approve inputs; LANGSMITH_TRACING=true set for the tracing scenarios (the langsmith SDK's env gate — without it the plugin correctly creates no runs). A fresh-venv pip 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.

DABH added 5 commits July 15, 2026 15:15
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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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._langchain internal 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() to temporalio.contrib.workflow_streams and 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.

Comment thread temporalio/contrib/_langchain/_runnable_config.py
DABH and others added 3 commits July 15, 2026 19:24
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
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.
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