diff --git a/libs/cli/js-examples/package.json b/libs/cli/js-examples/package.json index 9ea0bb4fb..298cb5aac 100644 --- a/libs/cli/js-examples/package.json +++ b/libs/cli/js-examples/package.json @@ -36,7 +36,7 @@ "@types/jest": "^30.0.0", "@typescript-eslint/eslint-plugin": "^8.68.0", "@typescript-eslint/parser": "^8.68.0", - "dotenv": "^17.4.2", + "dotenv": "^18.0.4", "eslint": "^10.9.1", "eslint-config-prettier": "^10.1.8", "eslint-plugin-import": "^2.32.0", diff --git a/libs/cli/js-examples/yarn.lock b/libs/cli/js-examples/yarn.lock index d6fbf57f5..f577a8961 100644 --- a/libs/cli/js-examples/yarn.lock +++ b/libs/cli/js-examples/yarn.lock @@ -2072,10 +2072,10 @@ doctrine@^2.1.0: dependencies: esutils "^2.0.2" -dotenv@^17.4.2: - version "17.4.2" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-17.4.2.tgz#c07e54a746e11eba021dd9e1047ced5afdc1c034" - integrity sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw== +dotenv@^18.0.4: + version "18.0.4" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-18.0.4.tgz#419bbe784f30915bfeb10e2d407e13ca5528504f" + integrity sha512-jGt9uI5BIxpVO15kvy+iEFqNAwxYClKWnkptLjSNPnFLC003Imd0ofr3aEVsbpHdSBdSsbcApNIlz4nbyGMwdA== dunder-proto@^1.0.0, dunder-proto@^1.0.1: version "1.0.1" diff --git a/libs/sdk-py/tests/streaming/test_lifecycle_namespace.py b/libs/sdk-py/tests/streaming/test_lifecycle_namespace.py new file mode 100644 index 000000000..e22bb51f6 --- /dev/null +++ b/libs/sdk-py/tests/streaming/test_lifecycle_namespace.py @@ -0,0 +1,67 @@ +"""Child lifecycle completion must not finish the parent thread's output.""" + +from __future__ import annotations + +import asyncio +from typing import cast + +import httpx +import pytest +from langchain_protocol import Event + +from langgraph_sdk._async.http import HttpClient +from langgraph_sdk._async.threads import ThreadsClient +from langgraph_sdk._sync.http import SyncHttpClient +from langgraph_sdk._sync.stream import _BlockingResult +from langgraph_sdk._sync.threads import SyncThreadsClient +from streaming._events import input_requested_event, lifecycle_event + + +@pytest.mark.parametrize("phase", ["completed", "failed"]) +async def test_async_child_terminal_does_not_finish_root(phase): + async with httpx.AsyncClient(base_url="http://test") as raw: + thread = ThreadsClient(HttpClient(raw)).stream( + thread_id="t-1", assistant_id="agent" + ) + thread._run_done = asyncio.get_running_loop().create_future() + await thread._apply_lifecycle_event( + cast(Event, input_requested_event(namespace=["sub:1"])) + ) + await thread._apply_lifecycle_event( + cast(Event, lifecycle_event(namespace=["sub:1"], event=phase)) + ) + assert not thread._run_done.done() + assert thread.interrupted + assert thread.interrupts[0]["namespace"] == ["sub:1"] + + await thread._apply_lifecycle_event( + cast(Event, lifecycle_event(event="completed")) + ) + assert thread._run_done.done() + assert (await thread._run_done).error is None + assert not thread.interrupted + assert thread.interrupts == [] + + +@pytest.mark.parametrize("phase", ["completed", "failed"]) +def test_sync_child_terminal_does_not_finish_root(phase): + with httpx.Client(base_url="http://test") as raw: + thread = SyncThreadsClient(SyncHttpClient(raw)).stream( + thread_id="t-1", assistant_id="agent" + ) + thread._run_done = _BlockingResult() + thread._apply_lifecycle_event( + cast(Event, input_requested_event(namespace=["sub:1"])) + ) + thread._apply_lifecycle_event( + cast(Event, lifecycle_event(namespace=["sub:1"], event=phase)) + ) + assert not thread._run_done.done() + assert thread.interrupted + assert thread.interrupts[0]["namespace"] == ["sub:1"] + + thread._apply_lifecycle_event(cast(Event, lifecycle_event(event="completed"))) + assert thread._run_done.done() + assert thread._run_done.result().error is None + assert not thread.interrupted + assert thread.interrupts == []