Compare commits

..
Author SHA1 Message Date
Eugene Yurtsev febf241b30 x 2025-11-07 11:50:47 -05:00
18 changed files with 402 additions and 687 deletions
+2 -48
View File
@@ -129,25 +129,11 @@ REDIRECT_MAP = {
"how-tos/human_in_the_loop/edit-graph-state.ipynb": "https://docs.langchain.com/oss/python/langgraph/use-time-travel",
# LGP mintlify migration redirects
"examples/index.md": "https://docs.langchain.com/oss/python/learn",
"guides/index.md": "https://docs.langchain.com/oss/python/langgraph/overview",
"concepts/index.md": "https://docs.langchain.com/oss/python/langgraph/overview",
"tutorials/index.md": "https://docs.langchain.com/oss/python/learn",
"llms-txt-overview.md": "https://docs.langchain.com/llms.txt",
"tutorials/rag/langgraph_adaptive_rag.md": "https://docs.langchain.com/oss/python/langgraph/agentic-rag",
"tutorials/multi_agent/multi-agent-collaboration.ipynb": "https://docs.langchain.com/oss/python/langchain/multi-agent",
"how-tos/create-react-agent-manage-message-history.ipynb": "https://docs.langchain.com/oss/python/langgraph/add-memory",
"how-tos/many-tools.ipynb": "https://docs.langchain.com/oss/python/langchain/tools",
"tutorials/customer-support/customer-support.ipynb": "https://docs.langchain.com/oss/python/langgraph/agentic-rag",
"how-tos/react-agent-structured-output.ipynb": "https://docs.langchain.com/oss/python/langchain/agents#structured-output",
"tutorials/code_assistant/langgraph_code_assistant.ipynb": "https://docs.langchain.com/oss/python/langgraph/agentic-rag",
"tutorials/multi_agent/hierarchical_agent_teams.ipynb": "https://docs.langchain.com/oss/python/langchain/supervisor",
"tutorials/auth/getting_started.md": "https://docs.langchain.com/langsmith/auth",
"tutorials/auth/resource_auth.md": "https://docs.langchain.com/langsmith/resource-auth",
"tutorials/auth/add_auth_server.md": "https://docs.langchain.com/langsmith/add-auth-server",
"how-tos/use-remote-graph.md": "https://docs.langchain.com/langsmith/use-remote-graph",
"how-tos/autogen-integration.md": "https://docs.langchain.com/langsmith/autogen-integration",
"how-tos/human_in_the_loop/wait-user-input.ipynb": "https://docs.langchain.com/oss/python/langgraph/interrupts",
"cloud/how-tos/use_stream_react.md": "https://docs.langchain.com/langsmith/use-stream-react",
"cloud/how-tos/generative_ui_react.md": "https://docs.langchain.com/langsmith/generative-ui-react",
"concepts/langgraph_platform.md": "https://docs.langchain.com/langsmith/deployments",
@@ -233,15 +219,12 @@ REDIRECT_MAP = {
"tutorials/get-started/6-time-travel.md": "https://docs.langchain.com/oss/python/langgraph/quickstart",
"tutorials/langsmith/local-server.md": "https://docs.langchain.com/oss/python/langgraph/local-server",
"tutorials/workflows.md": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
"tutorials/plan-and-execute/plan-and-execute.ipynb": "https://docs.langchain.com/oss/python/langchain/middleware/built-in#to-do-list",
"tutorials/langgraph-platform/local-server/local-server.md": "https://docs.langchain.com/langsmith/local-server",
"concepts/agentic_concepts.md": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
"guides/index.md": "https://docs.langchain.com/oss/python/langchain/overview",
"agents/overview.md": "https://docs.langchain.com/oss/python/langchain/agents",
"agents/run_agents.md": "https://docs.langchain.com/oss/python/langgraph/quickstart",
"concepts/low_level.md": "https://docs.langchain.com/oss/python/langgraph/graph-api",
"how-tos/graph-api.md": "https://docs.langchain.com/oss/python/langgraph/graph-api",
"how-tos/react-agent-from-scratch.ipynb": "https://docs.langchain.com/oss/python/langchain/quickstart",
"concepts/functional_api.md": "https://docs.langchain.com/oss/python/langgraph/functional-api",
"how-tos/use-functional-api.md": "https://docs.langchain.com/oss/python/langgraph/functional-api",
"concepts/pregel.md": "https://docs.langchain.com/oss/python/langgraph/pregel",
@@ -299,8 +282,8 @@ REDIRECT_MAP = {
"reference/supervisor.md": "https://reference.langchain.com/python/langgraph/supervisor/",
"reference/swarm.md": "https://reference.langchain.com/python/langgraph/swarm/",
"reference/mcp.md": "https://reference.langchain.com/python/langgraph/mcp/",
"cloud/reference/sdk/python_sdk_ref.md": "https://reference.langchain.com/python/langsmith/deployment/sdk/",
"reference/remote_graph.md": "https://reference.langchain.com/python/langsmith/deployment/remote_graph/",
"cloud/reference/sdk/python_sdk_ref.md": "https://reference.langchain.com/python/platform/python_sdk/",
"reference/remote_graph.md": "https://reference.langchain.com/python/platform/remote_graph/",
# additional exclude-search entries from mkdocs.yml
"additional-resources/index.md": "https://docs.langchain.com/oss/python/langchain/overview",
@@ -810,17 +793,6 @@ def on_post_build(config):
# Track which paths have explicit redirects
redirected_paths = set()
# Collect all existing HTML files in the site
all_html_files = set()
for root, dirs, files in os.walk(site_dir):
for file in files:
if file.endswith(".html"):
# Get relative path from site_dir
html_path = os.path.relpath(os.path.join(root, file), site_dir)
# Normalize path separators to forward slashes
html_path = html_path.replace(os.sep, "/")
all_html_files.add(html_path)
# Process explicit redirects from REDIRECT_MAP
for page_old, page_new in REDIRECT_MAP.items():
# Convert .ipynb to .md for path calculation
@@ -875,24 +847,6 @@ def on_post_build(config):
_write_html(site_dir, old_html_path, new_html_path)
# Create catch-all redirects for any HTML files not explicitly redirected
catchall_url = "https://docs.langchain.com/oss/python/langgraph/overview"
for html_file in all_html_files:
# Skip if this file is already explicitly redirected
if html_file in redirected_paths:
continue
# Skip the root index.html (we handle that separately)
if html_file == "index.html":
continue
# Skip reference documentation (keep those accessible)
if html_file.startswith("reference/"):
continue
# Create redirect for this unmapped file
_write_html(site_dir, html_file, catchall_url)
# Create root index.html redirect
root_redirect_html = """<!doctype html>
<html lang="en">
@@ -91,12 +91,7 @@ WHERE expires_at IS NOT NULL;
VECTOR_MIGRATIONS: Sequence[Migration] = [
Migration(
"""
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'vector') THEN
CREATE EXTENSION vector;
END IF;
END $$;
CREATE EXTENSION IF NOT EXISTS vector;
""",
),
Migration(
@@ -0,0 +1,16 @@
{
"permissions": {
"allow": [
"Bash(rg:*)",
"Bash(python:*)",
"Bash(grep:*)",
"Bash(sed:*)",
"Bash(awk:*)",
"Bash(uv run mypy:*)",
"Bash(uv run:*)",
"Bash(make test:*)",
"Bash(make test_parallel:*)"
],
"deny": []
}
}
+4 -4
View File
@@ -151,13 +151,13 @@ def task(
@task
def add_one_task(a: int) -> int:
def add_one(a: int) -> int:
return a + 1
@entrypoint()
def add_one(numbers: list[int]) -> list[int]:
futures = [add_one_task(n) for n in numbers]
futures = [add_one(n) for n in numbers]
results = [f.result() for f in futures]
return results
@@ -173,13 +173,13 @@ def task(
@task
async def add_one_task(a: int) -> int:
async def add_one(a: int) -> int:
return a + 1
@entrypoint()
async def add_one(numbers: list[int]) -> list[int]:
futures = [add_one_task(n) for n in numbers]
futures = [add_one(n) for n in numbers]
return asyncio.gather(*futures)
+1 -9
View File
@@ -110,20 +110,12 @@ def _get_node_name(node: StateNode[Any, ContextT]) -> str:
class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
"""A graph whose nodes communicate by reading and writing to a shared state.
The signature of each node is `State -> Partial<State>`.
The signature of each node is State -> Partial<State>.
Each state key can optionally be annotated with a reducer function that
will be used to aggregate the values of that key received from multiple nodes.
The signature of a reducer function is `(Value, Value) -> Value`.
!!! warning
`StateGraph` is a builder class and cannot be used directly for execution.
You must first call `.compile()` to create an executable graph that supports
methods like `invoke()`, `stream()`, `astream()`, and `ainvoke()`. See the
`CompiledStateGraph` documentation for more details.
Args:
state_schema: The schema class that defines the state.
context_schema: The schema class that defines the runtime context.
+249 -351
View File
@@ -258,11 +258,9 @@ def apply_writes(
next_version = None
else:
next_version = get_next_version(
(
max(checkpoint["channel_versions"].values())
if checkpoint["channel_versions"]
else None
),
max(checkpoint["channel_versions"].values())
if checkpoint["channel_versions"]
else None,
None,
)
@@ -493,11 +491,6 @@ def prepare_next_tasks(
PUSH_TRIGGER = (PUSH,)
class _TaskIDFn(Protocol):
def __call__(self, namespace: bytes, *parts: str | bytes) -> str:
pass
def prepare_single_task(
task_path: tuple[Any, ...],
task_id_checksum: str | None,
@@ -527,50 +520,250 @@ def prepare_single_task(
task_id_func = _xxhash_str if checkpoint["v"] > 1 else _uuid5_str
if task_path[0] == PUSH and isinstance(task_path[-1], Call):
return prepare_push_task_functional(
cast(tuple[str, tuple, int, str, Call], task_path),
task_id_checksum,
checkpoint=checkpoint,
checkpoint_id_bytes=checkpoint_id_bytes,
pending_writes=pending_writes,
channels=channels,
managed=managed,
config=config,
step=step,
stop=stop,
for_execution=for_execution,
store=store,
checkpointer=checkpointer,
manager=manager,
cache_policy=cache_policy,
retry_policy=retry_policy,
parent_ns=parent_ns,
task_id_func=task_id_func,
# (PUSH, parent task path, idx of PUSH write, id of parent task, Call)
task_path_t = cast(tuple[str, tuple, int, str, Call], task_path)
call = task_path_t[-1]
proc_ = get_runnable_for_task(call.func)
name = proc_.name
if name is None:
raise ValueError("`call` functions must have a `__name__` attribute")
# create task id
triggers: Sequence[str] = PUSH_TRIGGER
checkpoint_ns = f"{parent_ns}{NS_SEP}{name}" if parent_ns else name
task_id = task_id_func(
checkpoint_id_bytes,
checkpoint_ns,
str(step),
name,
PUSH,
task_path_str(task_path[1]),
str(task_path[2]),
)
task_checkpoint_ns = f"{checkpoint_ns}:{task_id}"
# we append True to the task path to indicate that a call is being
# made, so we should not return interrupts from this task (responsibility lies with the parent)
task_path = (*task_path[:3], True)
metadata = {
"langgraph_step": step,
"langgraph_node": name,
"langgraph_triggers": triggers,
"langgraph_path": task_path,
"langgraph_checkpoint_ns": task_checkpoint_ns,
}
if task_id_checksum is not None:
assert task_id == task_id_checksum, f"{task_id} != {task_id_checksum}"
if for_execution:
writes: deque[tuple[str, Any]] = deque()
cache_policy = call.cache_policy or cache_policy
if cache_policy:
args_key = cache_policy.key_func(*call.input[0], **call.input[1])
cache_key: CacheKey | None = CacheKey(
(
CACHE_NS_WRITES,
(identifier(call.func) or "__dynamic__"),
),
xxh3_128_hexdigest(
args_key.encode() if isinstance(args_key, str) else args_key,
),
cache_policy.ttl,
)
else:
cache_key = None
scratchpad = _scratchpad(
config[CONF].get(CONFIG_KEY_SCRATCHPAD),
pending_writes,
task_id,
xxh3_128_hexdigest(task_checkpoint_ns.encode()),
config[CONF].get(CONFIG_KEY_RESUME_MAP),
step,
stop,
)
runtime = cast(
Runtime, configurable.get(CONFIG_KEY_RUNTIME, DEFAULT_RUNTIME)
)
runtime = runtime.override(store=store)
return PregelExecutableTask(
name,
call.input,
proc_,
writes,
patch_config(
merge_configs(config, {"metadata": metadata}),
run_name=name,
callbacks=call.callbacks
or (manager.get_child(f"graph:step:{step}") if manager else None),
configurable={
CONFIG_KEY_TASK_ID: task_id,
# deque.extend is thread-safe
CONFIG_KEY_SEND: writes.extend,
CONFIG_KEY_READ: partial(
local_read,
scratchpad,
channels,
managed,
PregelTaskWrites(task_path, name, writes, triggers),
),
CONFIG_KEY_CHECKPOINTER: (
checkpointer or configurable.get(CONFIG_KEY_CHECKPOINTER)
),
CONFIG_KEY_CHECKPOINT_MAP: {
**configurable.get(CONFIG_KEY_CHECKPOINT_MAP, {}),
parent_ns: checkpoint["id"],
},
CONFIG_KEY_CHECKPOINT_ID: None,
CONFIG_KEY_CHECKPOINT_NS: task_checkpoint_ns,
CONFIG_KEY_SCRATCHPAD: scratchpad,
CONFIG_KEY_RUNTIME: runtime,
},
),
triggers,
call.retry_policy or retry_policy,
cache_key,
task_id,
task_path,
)
else:
return PregelTask(task_id, name, task_path)
elif task_path[0] == PUSH:
return prepare_push_task_send(
cast(tuple[str, tuple], task_path),
task_id_checksum,
checkpoint=checkpoint,
checkpoint_id_bytes=checkpoint_id_bytes,
pending_writes=pending_writes,
channels=channels,
managed=managed,
config=config,
step=step,
processes=processes,
stop=stop,
for_execution=for_execution,
store=store,
checkpointer=checkpointer,
manager=manager,
cache_policy=cache_policy,
retry_policy=retry_policy,
parent_ns=parent_ns,
task_id_func=task_id_func,
)
if len(task_path) == 2:
# SEND tasks, executed in superstep n+1
# (PUSH, idx of pending send)
idx = cast(int, task_path[1])
if not channels[TASKS].is_available():
return
sends: Sequence[Send] = channels[TASKS].get()
if idx < 0 or idx >= len(sends):
return
packet = sends[idx]
if not isinstance(packet, Send):
logger.warning(
f"Ignoring invalid packet type {type(packet)} in pending sends"
)
return
if packet.node not in processes:
logger.warning(
f"Ignoring unknown node name {packet.node} in pending sends"
)
return
# find process
proc = processes[packet.node]
proc_node = proc.node
if proc_node is None:
return
# create task id
triggers = PUSH_TRIGGER
checkpoint_ns = (
f"{parent_ns}{NS_SEP}{packet.node}" if parent_ns else packet.node
)
task_id = task_id_func(
checkpoint_id_bytes,
checkpoint_ns,
str(step),
packet.node,
PUSH,
str(idx),
)
else:
logger.warning(f"Ignoring invalid PUSH task path {task_path}")
return
task_checkpoint_ns = f"{checkpoint_ns}:{task_id}"
# we append False to the task path to indicate that a call is not being made
# so we should return interrupts from this task
task_path = (*task_path[:3], False)
metadata = {
"langgraph_step": step,
"langgraph_node": packet.node,
"langgraph_triggers": triggers,
"langgraph_path": task_path,
"langgraph_checkpoint_ns": task_checkpoint_ns,
}
if task_id_checksum is not None:
assert task_id == task_id_checksum, f"{task_id} != {task_id_checksum}"
if for_execution:
if proc.metadata:
metadata.update(proc.metadata)
writes = deque()
cache_policy = proc.cache_policy or cache_policy
if cache_policy:
args_key = cache_policy.key_func(packet.arg)
cache_key = CacheKey(
(
CACHE_NS_WRITES,
(identifier(proc) or "__dynamic__"),
packet.node,
),
xxh3_128_hexdigest(
args_key.encode() if isinstance(args_key, str) else args_key,
),
cache_policy.ttl,
)
else:
cache_key = None
scratchpad = _scratchpad(
config[CONF].get(CONFIG_KEY_SCRATCHPAD),
pending_writes,
task_id,
xxh3_128_hexdigest(task_checkpoint_ns.encode()),
config[CONF].get(CONFIG_KEY_RESUME_MAP),
step,
stop,
)
runtime = cast(
Runtime, configurable.get(CONFIG_KEY_RUNTIME, DEFAULT_RUNTIME)
)
runtime = runtime.override(
store=store, previous=checkpoint["channel_values"].get(PREVIOUS, None)
)
additional_config: RunnableConfig = {
"metadata": metadata,
"tags": proc.tags,
}
return PregelExecutableTask(
packet.node,
packet.arg,
proc_node,
writes,
patch_config(
merge_configs(config, additional_config),
run_name=packet.node,
callbacks=(
manager.get_child(f"graph:step:{step}") if manager else None
),
configurable={
CONFIG_KEY_TASK_ID: task_id,
# deque.extend is thread-safe
CONFIG_KEY_SEND: writes.extend,
CONFIG_KEY_READ: partial(
local_read,
scratchpad,
channels,
managed,
PregelTaskWrites(task_path, packet.node, writes, triggers),
),
CONFIG_KEY_CHECKPOINTER: (
checkpointer or configurable.get(CONFIG_KEY_CHECKPOINTER)
),
CONFIG_KEY_CHECKPOINT_MAP: {
**configurable.get(CONFIG_KEY_CHECKPOINT_MAP, {}),
parent_ns: checkpoint["id"],
},
CONFIG_KEY_CHECKPOINT_ID: None,
CONFIG_KEY_CHECKPOINT_NS: task_checkpoint_ns,
CONFIG_KEY_SCRATCHPAD: scratchpad,
CONFIG_KEY_RUNTIME: runtime,
},
),
triggers,
proc.retry_policy or retry_policy,
cache_key,
task_id,
task_path,
writers=proc.flat_writers,
subgraphs=proc.subgraphs,
)
else:
return PregelTask(task_id, packet.node, task_path)
elif task_path[0] == PULL:
# (PULL, node name)
name = cast(str, task_path[1])
@@ -641,7 +834,7 @@ def prepare_single_task(
if node := proc.node:
if proc.metadata:
metadata.update(proc.metadata)
writes: deque[tuple[str, Any]] = deque()
writes = deque()
cache_policy = proc.cache_policy or cache_policy
if cache_policy:
args_key = cache_policy.key_func(val)
@@ -652,11 +845,9 @@ def prepare_single_task(
name,
),
xxh3_128_hexdigest(
(
args_key.encode()
if isinstance(args_key, str)
else args_key
),
args_key.encode()
if isinstance(args_key, str)
else args_key,
),
cache_policy.ttl,
)
@@ -679,9 +870,7 @@ def prepare_single_task(
node,
writes,
patch_config(
merge_configs(
config, cast(RunnableConfig, additional_config)
),
merge_configs(config, additional_config),
run_name=name,
callbacks=(
manager.get_child(f"graph:step:{step}")
@@ -730,297 +919,6 @@ def prepare_single_task(
return PregelTask(task_id, name, task_path[:3])
def prepare_push_task_functional(
task_path: tuple[str, tuple, int, str, Call],
# (PUSH, parent task path, idx of PUSH write, id of parent task, Call)
task_id_checksum: str | None,
*,
checkpoint: Checkpoint,
checkpoint_id_bytes: bytes,
pending_writes: list[PendingWrite],
channels: Mapping[str, BaseChannel],
managed: ManagedValueMapping,
config: RunnableConfig,
step: int,
stop: int,
for_execution: bool,
store: BaseStore | None = None,
checkpointer: BaseCheckpointSaver | None = None,
manager: None | ParentRunManager | AsyncParentRunManager = None,
cache_policy: CachePolicy | None = None,
retry_policy: Sequence[RetryPolicy] = (),
parent_ns: str,
# namespace: bytes, *parts: str | bytes
task_id_func: _TaskIDFn,
) -> PregelTask | PregelExecutableTask:
"""Prepare a push task with an attached caller. Used for the functional API."""
configurable = config.get(CONF, {})
call = task_path[-1]
proc_ = get_runnable_for_task(call.func)
name = proc_.name
if name is None:
raise ValueError("`call` functions must have a `__name__` attribute")
# create task id
triggers: Sequence[str] = PUSH_TRIGGER
checkpoint_ns = f"{parent_ns}{NS_SEP}{name}" if parent_ns else name
task_id = task_id_func(
checkpoint_id_bytes,
checkpoint_ns,
str(step),
name,
PUSH,
task_path_str(task_path[1]),
str(task_path[2]),
)
task_checkpoint_ns = f"{checkpoint_ns}:{task_id}"
# we append True to the task path to indicate that a call is being
# made, so we should not return interrupts from this task (responsibility lies with the parent)
in_progress_task_path = (*task_path[:3], True)
metadata = {
"langgraph_step": step,
"langgraph_node": name,
"langgraph_triggers": triggers,
"langgraph_path": in_progress_task_path,
"langgraph_checkpoint_ns": task_checkpoint_ns,
}
if task_id_checksum is not None:
assert task_id == task_id_checksum, f"{task_id} != {task_id_checksum}"
if for_execution:
writes: deque[tuple[str, Any]] = deque()
cache_policy = call.cache_policy or cache_policy
if cache_policy:
args_key = cache_policy.key_func(*call.input[0], **call.input[1])
cache_key: CacheKey | None = CacheKey(
(
CACHE_NS_WRITES,
(identifier(call.func) or "__dynamic__"),
),
xxh3_128_hexdigest(
args_key.encode() if isinstance(args_key, str) else args_key,
),
cache_policy.ttl,
)
else:
cache_key = None
scratchpad = _scratchpad(
configurable.get(CONFIG_KEY_SCRATCHPAD),
pending_writes,
task_id,
xxh3_128_hexdigest(task_checkpoint_ns.encode()),
configurable.get(CONFIG_KEY_RESUME_MAP),
step,
stop,
)
runtime = cast(Runtime, configurable.get(CONFIG_KEY_RUNTIME, DEFAULT_RUNTIME))
runtime = runtime.override(store=store)
return PregelExecutableTask(
name,
call.input,
proc_,
writes,
patch_config(
merge_configs(config, {"metadata": metadata}),
run_name=name,
callbacks=call.callbacks
or (manager.get_child(f"graph:step:{step}") if manager else None),
configurable={
CONFIG_KEY_TASK_ID: task_id,
# deque.extend is thread-safe
CONFIG_KEY_SEND: writes.extend,
CONFIG_KEY_READ: partial(
local_read,
scratchpad,
channels,
managed,
PregelTaskWrites(in_progress_task_path, name, writes, triggers),
),
CONFIG_KEY_CHECKPOINTER: (
checkpointer or configurable.get(CONFIG_KEY_CHECKPOINTER)
),
CONFIG_KEY_CHECKPOINT_MAP: {
**configurable.get(CONFIG_KEY_CHECKPOINT_MAP, {}),
parent_ns: checkpoint["id"],
},
CONFIG_KEY_CHECKPOINT_ID: None,
CONFIG_KEY_CHECKPOINT_NS: task_checkpoint_ns,
CONFIG_KEY_SCRATCHPAD: scratchpad,
CONFIG_KEY_RUNTIME: runtime,
},
),
triggers,
call.retry_policy or retry_policy,
cache_key,
task_id,
in_progress_task_path,
)
else:
return PregelTask(task_id, name, in_progress_task_path)
def prepare_push_task_send(
task_path: tuple[str, tuple],
# (PUSH, parent task path)
task_id_checksum: str | None,
*,
checkpoint: Checkpoint,
checkpoint_id_bytes: bytes,
pending_writes: list[PendingWrite],
channels: Mapping[str, BaseChannel],
managed: ManagedValueMapping,
config: RunnableConfig,
step: int,
stop: int,
for_execution: bool,
store: BaseStore | None = None,
checkpointer: BaseCheckpointSaver | None = None,
manager: None | ParentRunManager | AsyncParentRunManager = None,
cache_policy: CachePolicy | None = None,
retry_policy: Sequence[RetryPolicy] = (),
parent_ns: str,
task_id_func: _TaskIDFn,
processes: Mapping[str, PregelNode],
) -> PregelTask | PregelExecutableTask | None:
if len(task_path) == 2:
# SEND tasks, executed in superstep n+1
# (PUSH, idx of pending send)
idx = cast(int, task_path[1])
if not channels[TASKS].is_available():
return
sends: Sequence[Send] = channels[TASKS].get()
if idx < 0 or idx >= len(sends):
return
packet = sends[idx]
if not isinstance(packet, Send):
logger.warning(
f"Ignoring invalid packet type {type(packet)} in pending sends"
)
return
if packet.node not in processes:
logger.warning(f"Ignoring unknown node name {packet.node} in pending sends")
return
# find process
proc = processes[packet.node]
proc_node = proc.node
if proc_node is None:
return
# create task id
triggers = PUSH_TRIGGER
checkpoint_ns = (
f"{parent_ns}{NS_SEP}{packet.node}" if parent_ns else packet.node
)
task_id = task_id_func(
checkpoint_id_bytes,
checkpoint_ns,
str(step),
packet.node,
PUSH,
str(idx),
)
else:
logger.warning(f"Ignoring invalid PUSH task path {task_path}")
return
configurable = config.get(CONF, {})
task_checkpoint_ns = f"{checkpoint_ns}:{task_id}"
# we append False to the task path to indicate that a call is not being made
# so we should return interrupts from this task
translated_task_path = (*task_path[:3], False)
metadata = {
"langgraph_step": step,
"langgraph_node": packet.node,
"langgraph_triggers": triggers,
"langgraph_path": translated_task_path,
"langgraph_checkpoint_ns": task_checkpoint_ns,
}
if task_id_checksum is not None:
assert task_id == task_id_checksum, f"{task_id} != {task_id_checksum}"
if for_execution:
if proc.metadata:
metadata.update(proc.metadata)
writes: deque[tuple[str, Any]] = deque()
cache_policy = proc.cache_policy or cache_policy
if cache_policy:
args_key = cache_policy.key_func(packet.arg)
cache_key = CacheKey(
(
CACHE_NS_WRITES,
(identifier(proc) or "__dynamic__"),
packet.node,
),
xxh3_128_hexdigest(
args_key.encode() if isinstance(args_key, str) else args_key,
),
cache_policy.ttl,
)
else:
cache_key = None
scratchpad = _scratchpad(
config[CONF].get(CONFIG_KEY_SCRATCHPAD),
pending_writes,
task_id,
xxh3_128_hexdigest(task_checkpoint_ns.encode()),
config[CONF].get(CONFIG_KEY_RESUME_MAP),
step,
stop,
)
runtime = cast(Runtime, configurable.get(CONFIG_KEY_RUNTIME, DEFAULT_RUNTIME))
runtime = runtime.override(
store=store, previous=checkpoint["channel_values"].get(PREVIOUS, None)
)
additional_config: RunnableConfig = {
"metadata": metadata,
"tags": proc.tags,
}
return PregelExecutableTask(
packet.node,
packet.arg,
proc_node,
writes,
patch_config(
merge_configs(config, additional_config),
run_name=packet.node,
callbacks=(
manager.get_child(f"graph:step:{step}") if manager else None
),
configurable={
CONFIG_KEY_TASK_ID: task_id,
# deque.extend is thread-safe
CONFIG_KEY_SEND: writes.extend,
CONFIG_KEY_READ: partial(
local_read,
scratchpad,
channels,
managed,
PregelTaskWrites(
translated_task_path, packet.node, writes, triggers
),
),
CONFIG_KEY_CHECKPOINTER: (
checkpointer or configurable.get(CONFIG_KEY_CHECKPOINTER)
),
CONFIG_KEY_CHECKPOINT_MAP: {
**configurable.get(CONFIG_KEY_CHECKPOINT_MAP, {}),
parent_ns: checkpoint["id"],
},
CONFIG_KEY_CHECKPOINT_ID: None,
CONFIG_KEY_CHECKPOINT_NS: task_checkpoint_ns,
CONFIG_KEY_SCRATCHPAD: scratchpad,
CONFIG_KEY_RUNTIME: runtime,
},
),
triggers,
proc.retry_policy or retry_policy,
cache_key,
task_id,
translated_task_path,
writers=proc.flat_writers,
subgraphs=proc.subgraphs,
)
else:
return PregelTask(task_id, packet.node, translated_task_path)
def checkpoint_null_version(
checkpoint: Checkpoint,
) -> V | None:
+9 -9
View File
@@ -9,7 +9,7 @@ from collections.abc import Awaitable, Callable, Sequence
from dataclasses import replace
from typing import Any
from langgraph._internal._config import patch_configurable
from langgraph._internal._config import patch_configurable, recast_checkpoint_ns
from langgraph._internal._constants import (
CONF,
CONFIG_KEY_CHECKPOINT_NS,
@@ -43,16 +43,16 @@ def run_with_retry(
except ParentCommand as exc:
ns: str = config[CONF][CONFIG_KEY_CHECKPOINT_NS]
cmd = exc.args[0]
if cmd.graph in (ns, task.name):
if cmd.graph in (recast_checkpoint_ns(ns), task.name):
# this command is for the current graph, handle it
for w in task.writers:
w.invoke(cmd, config)
break
elif cmd.graph == Command.PARENT:
# this command is for the parent graph, assign it to the parent
parts = ns.split(NS_SEP)
if parts[-1].isdigit():
parts.pop()
# normalize namespace by removing task IDs
recast_ns = recast_checkpoint_ns(ns)
parts = recast_ns.split(NS_SEP)
parent_ns = NS_SEP.join(parts[:-1])
exc.args = (replace(cmd, graph=parent_ns),)
# bubble up
@@ -138,16 +138,16 @@ async def arun_with_retry(
except ParentCommand as exc:
ns: str = config[CONF][CONFIG_KEY_CHECKPOINT_NS]
cmd = exc.args[0]
if cmd.graph in (ns, task.name):
if cmd.graph in (recast_checkpoint_ns(ns), task.name):
# this command is for the current graph, handle it
for w in task.writers:
w.invoke(cmd, config)
break
elif cmd.graph == Command.PARENT:
# this command is for the parent graph, assign it to the parent
parts = ns.split(NS_SEP)
if parts[-1].isdigit():
parts.pop()
# normalize namespace by removing task IDs
recast_ns = recast_checkpoint_ns(ns)
parts = recast_ns.split(NS_SEP)
parent_ns = NS_SEP.join(parts[:-1])
exc.args = (replace(cmd, graph=parent_ns),)
# bubble up
+11 -13
View File
@@ -2431,7 +2431,6 @@ class Pregel(
Will be emitted as 2-tuples `(LLM token, metadata)`.
- `"checkpoints"`: Emit an event when a checkpoint is created, in the same format as returned by `get_state()`.
- `"tasks"`: Emit events when tasks start and finish, including their results and errors.
- `"debug"`: Emit debug events with as much information as possible for each step.
You can pass a list as the `stream_mode` parameter to stream multiple modes at once.
The streamed outputs will be tuples of `(mode, data)`.
@@ -2698,8 +2697,6 @@ class Pregel(
- `"custom"`: Emit custom data from inside nodes or tasks using `StreamWriter`.
- `"messages"`: Emit LLM messages token-by-token together with metadata for any LLM invocations inside nodes or tasks.
Will be emitted as 2-tuples `(LLM token, metadata)`.
- `"checkpoints"`: Emit an event when a checkpoint is created, in the same format as returned by `get_state()`.
- `"tasks"`: Emit events when tasks start and finish, including their results and errors.
- `"debug"`: Emit debug events with as much information as possible for each step.
You can pass a list as the `stream_mode` parameter to stream multiple modes at once.
@@ -3104,30 +3101,31 @@ class Pregel(
durability: Durability | None = None,
**kwargs: Any,
) -> dict[str, Any] | Any:
"""Asynchronously run the graph with a single input and config.
"""Asynchronously invoke the graph on a single input.
Args:
input: The input data for the graph. It can be a dictionary or any other type.
config: The configuration for the graph run.
input: The input data for the computation. It can be a dictionary or any other type.
config: The configuration for the computation.
context: The static context to use for the run.
!!! version-added "Added in version 0.6.0"
stream_mode: The stream mode for the graph run.
stream_mode: The stream mode for the computation.
print_mode: Accepts the same values as `stream_mode`, but only prints the output to the console, for debugging purposes. Does not affect the output of the graph in any way.
output_keys: The output keys to retrieve from the graph run.
interrupt_before: The nodes to interrupt the graph run before.
interrupt_after: The nodes to interrupt the graph run after.
output_keys: The output keys to include in the result.
interrupt_before: The nodes to interrupt before.
interrupt_after: The nodes to interrupt after.
durability: The durability mode for the graph execution, defaults to `"async"`.
Options are:
- `"sync"`: Changes are persisted synchronously before the next step starts.
- `"async"`: Changes are persisted asynchronously while the next step executes.
- `"exit"`: Changes are persisted only when the graph exits.
**kwargs: Additional keyword arguments to pass to the graph run.
**kwargs: Additional keyword arguments.
Returns:
The output of the graph run. If `stream_mode` is `"values"`, it returns the latest output.
If `stream_mode` is not `"values"`, it returns a list of output chunks.
The result of the computation. If `stream_mode` is `"values"`, it returns the latest value.
If `stream_mode` is `"chunks"`, it returns a list of chunks.
"""
output_keys = output_keys if output_keys is not None else self.output_channels
latest: dict[str, Any] | Any = None
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "langgraph"
version = "1.0.3"
version = "1.0.2"
description = "Building stateful, multi-actor applications with LLMs"
authors = []
requires-python = ">=3.10"
+72
View File
@@ -7911,6 +7911,78 @@ def test_parent_command_goto(
}
@pytest.mark.parametrize("subgraph_persist", [True, False])
def test_parent_command_goto_deeply_nested(
sync_checkpointer: BaseCheckpointSaver, subgraph_persist: bool
) -> None:
"""Test Command.PARENT with goto in deeply nested graphs (3+ levels).
This tests the fix for issue #6409 where Command.PARENT with goto
would fail in graphs with 3 or more levels of nesting due to
namespace comparison issues.
"""
class State(TypedDict):
messages: Annotated[list[str], operator.add]
# Level 3 (deepest): sub_sub_graph
def sub_sub_node(state):
"""Returns Command.PARENT to jump to grandparent's node."""
return Command(
graph=Command.PARENT,
goto="sub_node_3",
update={"messages": ["sub_sub_node"]},
)
sub_sub_builder = StateGraph(State)
sub_sub_builder.add_node("sub_sub_node", sub_sub_node)
sub_sub_builder.add_edge(START, "sub_sub_node")
sub_sub_graph = sub_sub_builder.compile(checkpointer=subgraph_persist)
# Level 2 (middle): sub_graph
def sub_node_1(state):
return {"messages": ["sub_node_1"]}
def sub_node_3(state):
"""Target node for Command.PARENT goto."""
return {"messages": ["sub_node_3"]}
sub_builder = StateGraph(State)
sub_builder.add_node("sub_node_1", sub_node_1)
sub_builder.add_node("sub_node_2", sub_sub_graph)
sub_builder.add_node("sub_node_3", sub_node_3)
sub_builder.add_edge(START, "sub_node_1")
sub_builder.add_edge("sub_node_1", "sub_node_2")
sub_graph = sub_builder.compile(checkpointer=subgraph_persist)
# Level 1 (top): main_graph
def main_node_1(state):
return {"messages": ["main_node_1"]}
main_builder = StateGraph(State)
main_builder.add_node("main_node_1", main_node_1)
main_builder.add_node("main_node_2", sub_graph)
main_builder.add_edge(START, "main_node_1")
main_builder.add_edge("main_node_1", "main_node_2")
main_graph = main_builder.compile(sync_checkpointer, name="main")
config = {"configurable": {"thread_id": 1}}
result = main_graph.invoke(input={"messages": ["start"]}, config=config)
# Verify the execution order includes all expected nodes.
# Note: When subgraphs have persistent checkpointers, parent state
# is passed down, which may cause message duplication at subgraph boundaries.
# The key assertion is that:
# 1. All expected messages appear in order
# 2. sub_node_3 executed (proving Command.PARENT goto worked)
expected_messages = ["main_node_1", "sub_node_1", "sub_sub_node", "sub_node_3"]
assert all(msg in result["messages"] for msg in expected_messages), (
f"Expected all messages {expected_messages} to be in result {result['messages']}"
)
# Verify sub_node_3 executed last (the Command.PARENT goto target)
assert result["messages"][-1] == "sub_node_3"
@pytest.mark.parametrize("with_timeout", [True, False])
def test_timeout_with_parent_command(
sync_checkpointer: BaseCheckpointSaver, with_timeout: bool
+3 -3
View File
@@ -1,5 +1,5 @@
version = 1
revision = 2
revision = 3
requires-python = ">=3.10"
resolution-markers = [
"python_full_version >= '3.14'",
@@ -1345,7 +1345,7 @@ wheels = [
[[package]]
name = "langgraph"
version = "1.0.3"
version = "1.0.2"
source = { editable = "." }
dependencies = [
{ name = "langchain-core" },
@@ -1710,7 +1710,7 @@ test = [
[[package]]
name = "langgraph-prebuilt"
version = "1.0.4"
version = "1.0.2"
source = { editable = "../prebuilt" }
dependencies = [
{ name = "langchain-core" },
-21
View File
@@ -1,21 +0,0 @@
{
"permissions": {
"allow": [
"Bash(make test:*)",
"Bash(uv run pytest:*)",
"Bash(LANGGRAPH_TEST_FAST=0 make start-services:*)",
"Bash(LANGGRAPH_TEST_FAST=0 uv run:*)",
"Bash(EXIT_CODE=$?)",
"Bash(make stop-services:*)",
"Bash(exit $EXIT_CODE)",
"Read(//Users/sydney_runkle/oss/langgraph/**)",
"Bash(python3:*)",
"Bash(find:*)",
"Bash(python -m pytest:*)",
"Bash(python:*)",
"Read(//tmp/**)"
],
"deny": [],
"ask": []
}
}
@@ -63,7 +63,7 @@ class AgentState(TypedDict):
@deprecated(
"AgentStatePydantic has been deprecated in favor of AgentState in `langchain.agents`.",
"AgentStatePydantic has been moved to `langchain.agents`. Please update your import to `from langchain.agents import AgentStatePydantic`.",
category=LangGraphDeprecatedSinceV10,
)
class AgentStatePydantic(BaseModel):
@@ -78,11 +78,11 @@ with warnings.catch_warnings():
warnings.filterwarnings(
"ignore",
category=LangGraphDeprecatedSinceV10,
message="AgentState has been moved to `langchain.agents`.*",
message="AgentState has been moved to langchain.agents.*",
)
@deprecated(
"AgentStateWithStructuredResponse has been deprecated in favor of AgentState in `langchain.agents`.",
"AgentStateWithStructuredResponse has been moved to `langchain.agents`. Please update your import to `from langchain.agents import AgentStateWithStructuredResponse`.",
category=LangGraphDeprecatedSinceV10,
)
class AgentStateWithStructuredResponse(AgentState):
@@ -95,11 +95,11 @@ with warnings.catch_warnings():
warnings.filterwarnings(
"ignore",
category=LangGraphDeprecatedSinceV10,
message="AgentStatePydantic has been deprecated in favor of AgentState in `langchain.agents`.",
message="AgentStatePydantic has been moved to langchain.agents.*",
)
@deprecated(
"AgentStateWithStructuredResponsePydantic has been deprecated in favor of AgentState in `langchain.agents`.",
"AgentStateWithStructuredResponsePydantic has been moved to `langchain.agents`. Please update your import to `from langchain.agents import AgentStateWithStructuredResponsePydantic`.",
category=LangGraphDeprecatedSinceV10,
)
class AgentStateWithStructuredResponsePydantic(AgentStatePydantic):
+11 -37
View File
@@ -124,7 +124,7 @@ class _ToolCallRequestOverrides(TypedDict, total=False):
@dataclass
class ToolCallRequest(Generic[ContextT, StateT]):
class ToolCallRequest:
"""Tool execution request passed to tool call interceptors.
Attributes:
@@ -133,45 +133,18 @@ class ToolCallRequest(Generic[ContextT, StateT]):
registered with the `ToolNode`. When tool is `None`, interceptors can
handle the request without validation. If the interceptor calls `execute()`,
validation will occur and raise an error for unregistered tools.
state: Agent state (`dict`, `list`, or `BaseModel`).
runtime: LangGraph runtime context (optional, `None` if outside graph).
state: Agent state (`dict`, `list`, or `BaseModel`). Pulled from `runtime.state`.
"""
tool_call: ToolCall
tool: BaseTool | None
runtime: ToolRuntime[ContextT, StateT]
@property
def state(self) -> StateT:
"""Get the state from the runtime.
Returns:
The current graph state from the runtime context.
"""
return self.runtime.state
def __setattr__(self, name: str, value: Any) -> None:
"""Raise deprecation warning when setting attributes directly.
Direct attribute assignment is deprecated. Use the `override()` method instead.
"""
import warnings
# Allow setting attributes during initialization
if not hasattr(self, "__dataclass_fields__") or not hasattr(self, name):
object.__setattr__(self, name, value)
else:
warnings.warn(
f"Setting attribute '{name}' on ToolCallRequest is deprecated. "
"Use the override() method instead to create a new instance with modified values.",
DeprecationWarning,
stacklevel=2,
)
object.__setattr__(self, name, value)
state: Any
runtime: ToolRuntime
def override(
self, **overrides: Unpack[_ToolCallRequestOverrides]
) -> ToolCallRequest[ContextT, StateT]:
) -> ToolCallRequest:
"""Replace the request with a new request with the given overrides.
Returns a new `ToolCallRequest` instance with the specified attributes replaced.
@@ -229,9 +202,8 @@ Examples:
```python
def handler(request, execute):
modified_call = {**request.tool_call, "args": {**request.tool_call["args"], "value": request.tool_call["args"]["value"] * 2}}
modified_request = request.override(tool_call=modified_call)
return execute(modified_request)
request.tool_call["args"]["value"] *= 2
return execute(request)
```
Retry on error (execute multiple times):
@@ -955,10 +927,11 @@ class ToolNode(RunnableCallable):
# to short-circuit requests for unregistered tools
tool = self.tools_by_name.get(call["name"])
# Create the tool request with runtime
# Create the tool request with state and runtime
tool_request = ToolCallRequest(
tool_call=call,
tool=tool,
state=tool_runtime.state,
runtime=tool_runtime,
)
@@ -1111,10 +1084,11 @@ class ToolNode(RunnableCallable):
# to short-circuit requests for unregistered tools
tool = self.tools_by_name.get(call["name"])
# Create the tool request with runtime
# Create the tool request with state and runtime
tool_request = ToolCallRequest(
tool_call=call,
tool=tool,
state=tool_runtime.state,
runtime=tool_runtime,
)
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "langgraph-prebuilt"
version = "1.0.4"
version = "1.0.2"
description = "Library with high-level APIs for creating and executing LangGraph agents and tools."
authors = []
requires-python = ">=3.10"
+13 -105
View File
@@ -13,7 +13,6 @@ from langgraph.types import Command
from langgraph.prebuilt.tool_node import (
ToolCallRequest,
ToolNode,
ToolRuntime,
)
pytestmark = pytest.mark.anyio
@@ -131,17 +130,11 @@ def test_modify_arguments() -> None:
execute: Callable[[ToolCallRequest], ToolMessage | Command],
) -> ToolMessage | Command:
"""Handler that doubles the input arguments."""
# Modify the arguments using override method
modified_call = {
**request.tool_call,
"args": {
**request.tool_call["args"],
"a": request.tool_call["args"]["a"] * 2,
"b": request.tool_call["args"]["b"] * 2,
},
}
modified_request = request.override(tool_call=modified_call)
return execute(modified_request)
# Modify the arguments
request.tool_call["args"]["a"] *= 2
request.tool_call["args"]["b"] *= 2
return execute(request)
tool_node = ToolNode([add], wrap_tool_call=modify_args_handler)
@@ -343,23 +336,16 @@ def test_tool_call_request_dataclass() -> None:
"""Test ToolCallRequest dataclass."""
tool_call: ToolCall = {"name": "add", "args": {"a": 1, "b": 2}, "id": "call_1"}
state: dict = {"messages": []}
tool_runtime = ToolRuntime(
state=state,
config={},
context=None,
store=None,
stream_writer=Mock(),
tool_call_id="call_1",
)
runtime = None
request = ToolCallRequest(
tool_call=tool_call, tool=add, runtime=tool_runtime
)
tool_call=tool_call, tool=add, state=state, runtime=runtime
) # type: ignore[arg-type]
assert request.tool_call == tool_call
assert request.tool == add
assert request.state == state
assert request.runtime is tool_runtime
assert request.runtime is None
assert request.tool_call["name"] == "add"
@@ -376,17 +362,10 @@ async def test_handler_with_async_execution() -> None:
execute: Callable[[ToolCallRequest], ToolMessage | Command],
) -> ToolMessage | Command:
"""Handler that modifies arguments."""
# Add 10 to both arguments using override method
modified_call = {
**request.tool_call,
"args": {
**request.tool_call["args"],
"a": request.tool_call["args"]["a"] + 10,
"b": request.tool_call["args"]["b"] + 10,
},
}
modified_request = request.override(tool_call=modified_call)
return execute(modified_request)
# Add 10 to both arguments
request.tool_call["args"]["a"] += 10
request.tool_call["args"]["b"] += 10
return execute(request)
tool_node = ToolNode([async_add], wrap_tool_call=modifying_handler)
@@ -1326,74 +1305,3 @@ async def test_state_extraction_with_tool_call_with_context_async() -> None:
assert state_seen[0] == actual_state
assert "__type" not in state_seen[0]
assert "tool_call" not in state_seen[0]
def test_tool_call_request_is_frozen() -> None:
"""Test that ToolCallRequest raises deprecation warnings on direct attribute reassignment."""
tool_call: ToolCall = {"name": "add", "args": {"a": 1, "b": 2}, "id": "call_1"}
state: dict = {"messages": []}
tool_runtime = ToolRuntime(
state=state,
config={},
context=None,
store=None,
stream_writer=Mock(),
tool_call_id="call_1",
)
request = ToolCallRequest(
tool_call=tool_call, tool=add, runtime=tool_runtime
)
# Test that direct attribute reassignment raises DeprecationWarning
with pytest.warns(
DeprecationWarning,
match="Setting attribute 'tool_call' on ToolCallRequest is deprecated",
):
request.tool_call = {"name": "other", "args": {}, "id": "call_2"} # type: ignore[misc]
with pytest.warns(
DeprecationWarning,
match="Setting attribute 'tool' on ToolCallRequest is deprecated",
):
request.tool = None # type: ignore[misc]
# state is now a property, so setting it will raise a deprecation warning
# (and then fail with AttributeError after the warning)
with pytest.warns(
DeprecationWarning,
match="Setting attribute 'state' on ToolCallRequest is deprecated",
):
with pytest.raises(AttributeError):
request.state = {} # type: ignore[misc]
with pytest.warns(
DeprecationWarning,
match="Setting attribute 'runtime' on ToolCallRequest is deprecated",
):
request.runtime = None # type: ignore[misc]
# Test that override method works correctly
new_tool_call: ToolCall = {
"name": "multiply",
"args": {"x": 5, "y": 10},
"id": "call_3",
}
# Original request should be unchanged (note: it was modified by the warnings tests above)
# So we create a fresh request to test override properly
fresh_request = ToolCallRequest(
tool_call=tool_call, tool=add, runtime=tool_runtime
)
fresh_new_request = fresh_request.override(tool_call=new_tool_call)
# Original request should be unchanged
assert fresh_request.tool_call == tool_call
assert fresh_request.tool_call["name"] == "add"
# New request should have the updated tool_call
assert fresh_new_request.tool_call == new_tool_call
assert fresh_new_request.tool_call["name"] == "multiply"
assert fresh_new_request.tool == add # Other fields should remain the same
assert fresh_new_request.state == state
assert fresh_new_request.runtime is tool_runtime
-71
View File
@@ -1610,74 +1610,3 @@ def test_tool_node_stream_writer() -> None:
},
),
]
def test_tool_call_request_setattr_deprecation_warning():
"""Test that ToolCallRequest raises a deprecation warning on direct attribute modification."""
import warnings
from unittest.mock import Mock
from langgraph.prebuilt.tool_node import ToolCallRequest, ToolRuntime
# Create a mock ToolCall
tool_call = {"name": "test", "args": {"a": 1}, "id": "call_1", "type": "tool_call"}
# Create a ToolRuntime
tool_runtime = ToolRuntime(
state={"messages": []},
config={},
context=None,
store=None,
stream_writer=Mock(),
tool_call_id="call_1",
)
# Create a ToolCallRequest
request = ToolCallRequest(
tool_call=tool_call,
tool=None,
runtime=tool_runtime,
)
# Test 1: Direct attribute assignment should raise deprecation warning but still work
with pytest.warns(DeprecationWarning, match="deprecated.*override"):
request.tool_call = {"name": "other", "args": {}, "id": "call_2"}
# Verify the attribute was actually modified
assert request.tool_call == {"name": "other", "args": {}, "id": "call_2"}
# Reset for further tests
with warnings.catch_warnings():
warnings.simplefilter("ignore")
request.tool_call = tool_call
# Test 2: override method should work without warnings
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
new_tool_call = {
"name": "new_tool",
"args": {"b": 2},
"id": "call_3",
"type": "tool_call",
}
new_request = request.override(tool_call=new_tool_call)
# Verify no warning was raised
assert len(w) == 0
# Verify original is unchanged
assert request.tool_call == tool_call
# Verify new request has updated values
assert new_request.tool_call == new_tool_call
# Test 3: Initialization should not trigger warning
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
ToolCallRequest(
tool_call=tool_call,
tool=None,
runtime=tool_runtime,
)
# Verify no warning was raised during initialization
assert len(w) == 0
+3 -3
View File
@@ -1,5 +1,5 @@
version = 1
revision = 2
revision = 3
requires-python = ">=3.10"
[[package]]
@@ -246,7 +246,7 @@ wheels = [
[[package]]
name = "langgraph"
version = "1.0.3"
version = "1.0.2"
source = { editable = "../langgraph" }
dependencies = [
{ name = "langchain-core" },
@@ -467,7 +467,7 @@ test = [
[[package]]
name = "langgraph-prebuilt"
version = "1.0.4"
version = "1.0.2"
source = { editable = "." }
dependencies = [
{ name = "langchain-core" },