Conversation
Port of the pyagentspec LangGraph adapter to LangGraph JS, exposed as the agentspec/adapters/langgraph subpath export. Loader (AgentSpec -> LangGraph JS): Agent via langchain createAgent (tools, structured outputs, middleware, checkpointer), Swarm via @langchain/langgraph-swarm, ManagerWorkers as a hand-built hierarchical graph (__manager__ node, __delegate_to__ tools, Send routing), and Flow as a StateGraph with all 12 node executors. Server/client/remote tools with requires_confirmation interrupts, MCP tools/toolboxes via @langchain/mcp-adapters, OpenAI/OpenAI-compatible/vLLM/Ollama LLM configs. State keys, node names, interrupt payloads and error texts match the Python adapter for cross-SDK compatibility. Exporter (LangGraph JS -> AgentSpec): structured tools, chat models, createAgent ReactAgents (via their public options), and generic StateGraphs as Flows with conditional-edge expansion. Shared adapters/common layer: loader/exporter bases, component load policy (StdioTransport blocked by default), templating, URL allow-list validation, and the fetch-based remote tool executor (no redirects, 5s timeout, matching httpx defaults). Also: serializer support for [component, customId] disaggregation pairs (mirroring Python), and the ajv npm override scoped to ajv@^6 so @modelcontextprotocol/sdk gets the ajv v8 it needs. All LangChain packages are optional peer dependencies; the core SDK entry is unchanged. Documented divergences from the Python adapter (async API, no RetryPolicy/urlAllowList fields yet, OciGenAiConfig and mTLS MCP transports unsupported, tracing seams only) are listed in the README. 310 new tests (1047 total).
… node execution One buildTemplatedHttpRequest in adapters/common now serves both the RemoteTool executor and the ApiNode executor, ending a ~100-line duplication that had already drifted: the ApiNode copy read the Content-Type header with ?? where Python (and the RemoteTool copy) use falsy coalescing — the || behavior is now shared, aligning the empty-string-header edge with the Python adapter. node-execution.ts becomes a re-export barrel over focused modules (python-parity value coercion, executor base, and per-node executors), none above 1000 lines. LlmNodeExecutor tracks structured generation by the presence of the structured model alone, and MapNodeExecutor drops an unused constructor parameter and gives the non-sized-iterable error its own accurate message.
…licy groups, prune tracing seam jsonSchemasHaveSameType now lives only in src/property.ts — the adapter's private re-implementation is deleted, and the canonical copy gains the Python adapter's 100-entry union-length guard it was missing. Component-policy group membership is derived from the SDK's runtime component unions (with drift-pinning tests where derivation is not clean), so a new union member can no longer silently escape a group-level block-list. The tracing module drops two never-called callback builders and passes the spec component through the patchWithExecutionSpan identity seam, making the future tracing port's attachment sites honest. Loader/exporter bases pass typed closures instead of re-deriving a string mode tag.
…pe graph vocabulary ManagerWorkersNodeExecutor goes back to Python's template-method shape, overriding two protected hooks instead of re-implementing the parent executor around four shadow fields. ReactAgentInfo is replaced by createReactAgent(agent, context, overrides), convertNode is typed so the three *Like identity interfaces and their casts disappear, and subflow validation gets one assertInvocableGraph home. The adapter's core vocabulary is consolidated: adapters/common/guards.ts (strict and loose record guards under separate names), InvocableGraph and DynamicStateGraph in types.ts, and graph-introspection.ts as the single owner of everything probed from LangGraph internals, shared by both converter directions. ToolRegistry states its real two-member contract, Flow StateGraph compilation moves to langgraph-converter-flow.ts, and the minor-bundle cleanups (duplicate Agent guard, identical LLM config cases, client-tool confirmation reuse, one optional-peer-import helper) land alongside.
…ared helpers flow-nodes.test.ts (1,434 lines) becomes per-node suites mirroring the Python test layout, the exporter's state-graph-flow coverage moves to exporter-flow.test.ts, and the flow/message helpers that had been copied verbatim between suites now live in test-helpers.ts. No assertions added or removed.
Resolves the residual gaps found while auditing the quality-review remediation against the pre-refactor behavior baseline.
…and MCP auth to the SDK Ports the Python SDK surfaces the TypeScript SDK was missing: the RetryPolicy component (attached to RemoteTool, ApiNode, and the LLM configs that carry it in Python, with the same version-gated serialization), urlAllowList on RemoteTool and ApiNode, the concrete bare LlmConfig component with api_provider dispatch, and the OAuth auth configuration components on remote MCP transports with their secrets registered as sensitive fields. Wire-format field names and version gates match pyagentspec, so specs carrying these fields now round-trip through the TypeScript SDK instead of being silently stripped.
Async-only port of pyagentspec.tracing: execution/LLM/tool/node span classes and their start/end events, the message model traces carry, the SpanProcessor interface with registration, and AsyncLocalStorage-based trace context that keeps parallel async branches isolated. Class and field names match the Python package so span processors written against either SDK see the same shapes. Python's sync/async bridging has no JS equivalent and is not ported.
… bare LlmConfig into the LangGraph adapter RemoteTool and ApiNode requests now honor the spec's RetryPolicy with the full Python engine — per-tool timeouts, bounded exponential backoff with all four jitter modes, Retry-After parsing with the 30s cap, recoverable-status matching, and the no-retry rule for TLS failures — and enforce urlAllowList on every rendered URL, suppressing the unrestricted-templated-URL warning when a list is configured. LLM configs map retryPolicy to ChatOpenAI retries/timeout in both converter directions, the bare LlmConfig component dispatches by api_provider, and transport auth configuration survives load→export untouched (runtime OAuth is unwired, matching Python).
… adapter The identity tracing seams become real emission: execution spans wrap agent, flow, and manager-workers runs (streaming preserved), every converted chat model carries the LLM callback handler emitting generation spans with request, streamed-chunk, and response events, server/remote/MCP tools emit tool-execution spans (client tools excluded, as in Python), and flow nodes are wrapped in node-execution spans with exception events from CatchException. Span and event payloads match the Python adapter so processors work across SDKs.
… new SDK surfaces
…t, LlmConfig, auth, and tracing support
…e key SessionParameters was not registered as a model-object field, so a transport's read timeout serialized as session_parameters.readTimeoutSeconds where pyagentspec writes and expects read_timeout_seconds. A non-default timeout was therefore dropped whenever a spec crossed SDKs, silently falling back to the 60s default on the Python side. Registering the field on both the serialization and deserialization plugins fixes the emitted key and lets Python-authored blocks parse.
…ts in tool errors A hostile spec could turn one tool call into a high-rate request flood or a silent event-loop spin. max_attempts is unbounded and the retry delays can be configured to zero, so the elapsed-time cap alone permitted roughly 300,000 requests from a single call (measured at ~513 req/s against a real server); an invalid negative Retry-After was honored as a zero-second wait, letting a hostile server erase the operator's own backoff; and a non-finite request_timeout made AbortSignal.timeout throw a RangeError that the engine then retried as if it were a transient transport failure, spinning for the full 600s window without emitting a single request. One call now makes at most MAX_HTTP_ATTEMPTS_PER_CALL attempts separated by at least MIN_RETRY_DELAY_SECONDS (the same attack now measures 100 requests at 17 req/s), an invalid Retry-After is treated as absent per RFC 9110 so the configured backoff applies, request timeouts are clamped to a delay a timer can express, and RangeError is classified as a permanent local failure rather than a transient one. TLS handshake failures join certificate failures as non-retryable. Errors raised for a non-2xx response no longer echo the query string, which routinely carries credentials into model context and logs. Also sets an explicit vitest testTimeout: the suite has always run on the 5s default, which is too tight for the tests that dynamically import the LangChain peer packages, so whichever suite pulled a package in first would time out on a loaded machine and the failure rotated between files.
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.
Port of the pyagentspec LangGraph adapter to LangGraph JS, exposed as the
agentspec/adapters/langgraphsubpath export, plus the SDK surfaces it depends on. All LangChain packages are optional peer dependencies; the core SDK entry is unchanged.Loader (Agent Spec → LangGraph JS)
createAgent(converted model/tools, structured outputs via tool strategy, middleware pass-through, checkpointer, declared-inputs state extension)@langchain/langgraph-swarmwith per-relationship handoff tools__manager__node,__delegate_to__tools,Sendrouting, workers looping back)@langchain/mcp-adapters,requiresConfirmationinterruptsLlmConfig→ ChatOpenAI, Ollama → ChatOllamaState keys, node names, tool names, interrupt payloads, and error texts match the Python adapter for cross-SDK compatibility.
Exporter (LangGraph JS → Agent Spec)
Structured tools → ServerTool, chat models → LLM configs (including retry/timeout →
RetryPolicy),createAgentagents → Agent via their publicoptions, generic StateGraphs → Flow with conditional-edge expansion, and disaggregated export with[component, customId]pairs.SDK additions
The TypeScript SDK was missing spec surfaces the Python SDK has, which meant specs carrying them loaded with the capability silently stripped. Added with pyagentspec-matching wire names and version gates:
RetryPolicycomponent, on RemoteTool, ApiNode, and the LLM configs — with the full Python retry engine wired into the adapter: bounded exponential backoff, all four jitter modes,Retry-Afterparsing with the 30s cap, recoverable-status matching, per-request timeouts, and no retry on TLS failuresurlAllowListon RemoteTool and ApiNode, now enforced on every rendered (post-template) URL — previously the validators existed but were always passedundefined, so a spec that constrained egress in Python performed unrestricted egress in TypeScriptLlmConfigwithapi_providerdispatchsrc/tracing/— async-only port ofpyagentspec.tracing: span and event classes, theSpanProcessorinterface, andAsyncLocalStorage-based trace context. The adapter emits Python-parity spans for agent/flow/manager-workers runs, LLM generations (including streamed chunks), tool executions, and flow nodes, so span processors work across SDKsAlso fixes MCP
session_parametersserializing under a camelCase key, which silently dropped a non-default read timeout when a spec crossed SDKs.Documented divergences
Async-only API;
OciGenAiConfigand mTLS MCP transports unsupported (no JS packages); MCPauthis representation-only (as in Python); several small tracing payload differences where Python's own sync and async paths disagree. Full list in the README.Quality
apiKeyenv-substitution leak, and a trace-context corruption bug under concurrent runssrc/property.ts, and derived component-policy groups from the SDK unions so new union members cannot silently escape block-listsajvnpm override is scoped toajv@^6so@modelcontextprotocol/sdkgets the ajv v8 it requiresUsage
Quickstart, the supported-components table, and a
SpanProcessorsnippet are in the README's LangGraph adapter section;examples/09-langgraph-adapter.tsis a fully offline runnable example.