22
33import json
44import os
5+ from typing import Any
56
67
78class LangGraphConfig :
@@ -15,13 +16,25 @@ def __init__(self, config_path: str = "langgraph.json"):
1516 config_path: Path to langgraph.json file
1617 """
1718 self .config_path = config_path
18- self ._graphs : dict [str , str ] | None = None
19+ self ._raw : dict [str , Any ] | None = None
1920
2021 @property
2122 def exists (self ) -> bool :
2223 """Check if langgraph.json exists."""
2324 return os .path .exists (self .config_path )
2425
26+ def _load (self ) -> dict [str , Any ]:
27+ if self ._raw is not None :
28+ return self ._raw
29+ if not self .exists :
30+ raise FileNotFoundError (f"Config file not found: { self .config_path } " )
31+ try :
32+ with open (self .config_path , "r" ) as f :
33+ self ._raw = json .load (f )
34+ except json .JSONDecodeError as e :
35+ raise ValueError (f"Invalid JSON in { self .config_path } : { e } " ) from e
36+ return self ._raw
37+
2538 @property
2639 def graphs (self ) -> dict [str , str ]:
2740 """
@@ -30,30 +43,62 @@ def graphs(self) -> dict[str, str]:
3043 Returns:
3144 Dictionary mapping graph names to file paths (e.g., {"agent": "agent.py:graph"})
3245 """
33- if self ._graphs is None :
34- self ._graphs = self ._load_graphs ()
35- return self ._graphs
46+ config = self ._load ()
47+ if "graphs" not in config :
48+ raise ValueError ("Missing required 'graphs' field in langgraph.json" )
49+ graphs = config ["graphs" ]
50+ if not isinstance (graphs , dict ):
51+ raise ValueError ("'graphs' must be a dictionary" )
52+ return graphs
3653
37- def _load_graphs (self ) -> dict [str , str ]:
38- """Load graph definitions from langgraph.json."""
39- if not self .exists :
40- raise FileNotFoundError (f"Config file not found: { self .config_path } " )
41-
42- try :
43- with open (self .config_path , "r" ) as f :
44- config = json .load (f )
45-
46- if "graphs" not in config :
47- raise ValueError ("Missing required 'graphs' field in langgraph.json" )
54+ @property
55+ def allowed_msgpack_modules (self ) -> list [tuple [str , str ]] | None :
56+ """Read `checkpointer.serde.allowed_msgpack_modules` from langgraph.json.
4857
49- graphs = config [ "graphs" ]
50- if not isinstance ( graphs , dict ):
51- raise ValueError ( "'graphs' must be a dictionary" )
58+ Returns the list of `(module, class_name)` pairs the user opted into,
59+ or `None` if no opt-in — in which case the runtime keeps langgraph's
60+ default permissive behavior.
5261
53- return graphs
62+ Schema (matches langgraph CLI's `CheckpointerConfig.serde`):
5463
55- except json .JSONDecodeError as e :
56- raise ValueError (f"Invalid JSON in { self .config_path } : { e } " ) from e
64+ {
65+ "checkpointer": {
66+ "serde": {
67+ "allowed_msgpack_modules": [
68+ ["my_app.state", "MyState"]
69+ ]
70+ }
71+ }
72+ }
73+ """
74+ config = self ._load ()
75+ checkpointer = config .get ("checkpointer" )
76+ if not isinstance (checkpointer , dict ):
77+ return None
78+ serde = checkpointer .get ("serde" )
79+ if not isinstance (serde , dict ):
80+ return None
81+ modules = serde .get ("allowed_msgpack_modules" )
82+ if modules is None :
83+ return None
84+ if not isinstance (modules , list ):
85+ raise ValueError (
86+ "'checkpointer.serde.allowed_msgpack_modules' must be a list "
87+ "of [module, class_name] pairs"
88+ )
89+ result : list [tuple [str , str ]] = []
90+ for entry in modules :
91+ if (
92+ not isinstance (entry , list )
93+ or len (entry ) != 2
94+ or not all (isinstance (part , str ) for part in entry )
95+ ):
96+ raise ValueError (
97+ f"Invalid entry in checkpointer.serde.allowed_msgpack_modules: "
98+ f"{ entry !r} (expected [module, class_name])"
99+ )
100+ result .append ((entry [0 ], entry [1 ]))
101+ return result
57102
58103 @property
59104 def entrypoints (self ) -> list [str ]:
0 commit comments