diff --git a/pyproject.toml b/pyproject.toml
index fd1e32b6c..970dd4f2d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "uipath-langchain"
-version = "0.16.13"
+version = "0.16.14"
description = "Python SDK that enables developers to build and deploy LangGraph agents to the UiPath Cloud Platform"
readme = { file = "README.md", content-type = "text/markdown" }
requires-python = ">=3.11"
@@ -10,11 +10,11 @@ dependencies = [
"uipath-platform>=0.2.22, <0.3.0",
"uipath-runtime>=0.13.0, <0.14.0",
"uipath-llm-client>=1.18.0, <1.19.0",
- "langgraph>=1.1.8, <2.0.0",
- "langchain-core>=1.2.27, <2.0.0",
+ "langgraph>=1.2.11, <2.0.0",
+ "langchain-core>=1.6.1, <2.0.0",
"langgraph-checkpoint-sqlite>=3.0.3, <4.0.0",
- "langchain>=1.2.15, <2.0.0",
- "deepagents>=0.5.9, <0.6.0",
+ "langchain>=1.3.18, <2.0.0",
+ "deepagents>=0.7.11, <0.8.0",
"pydantic-settings>=2.6.0",
"python-dotenv>=1.0.1",
"httpx>=0.27.0",
@@ -56,8 +56,12 @@ bedrock = [
fireworks = [
"uipath-langchain-client[fireworks]>=1.18.0, <1.19.0",
]
+code-interpreter = [
+ "langchain-quickjs>=0.3.5, <0.4.0",
+]
all = [
"uipath-langchain-client[all]>=1.18.0, <1.19.0",
+ "langchain-quickjs>=0.3.5, <0.4.0",
]
[project.entry-points."uipath.middlewares"]
diff --git a/samples/deepagent-storage-buckets/pyproject.toml b/samples/deepagent-storage-buckets/pyproject.toml
index 3e44e43de..5473db381 100644
--- a/samples/deepagent-storage-buckets/pyproject.toml
+++ b/samples/deepagent-storage-buckets/pyproject.toml
@@ -5,7 +5,7 @@ description = "DeepAgent with persistant storage in Orchestrator Buckets"
authors = [{ name = "John Doe", email = "john.doe@myemail.com" }]
requires-python = ">=3.11"
dependencies = [
- "deepagents>=0.3.9",
+ "deepagents>=0.7.11, <0.8.0",
"langchain-anthropic>=1.3.1",
"langchain-tavily>=0.2.17",
"langgraph>=1.0.7",
diff --git a/samples/deepagent-storage-buckets/src/deepagent_storage_buckets/buckets_backend.py b/samples/deepagent-storage-buckets/src/deepagent_storage_buckets/buckets_backend.py
index ad0aed3ae..4f5dd9f10 100644
--- a/samples/deepagent-storage-buckets/src/deepagent_storage_buckets/buckets_backend.py
+++ b/samples/deepagent-storage-buckets/src/deepagent_storage_buckets/buckets_backend.py
@@ -21,13 +21,17 @@
FileDownloadResponse,
FileInfo,
FileUploadResponse,
+ GlobResult,
GrepMatch,
+ GrepResult,
+ LsResult,
+ ReadResult,
WriteResult,
)
from deepagents.backends.utils import (
check_empty_content,
- format_content_with_line_numbers,
perform_string_replacement,
+ slice_read_response,
)
from uipath.platform import UiPath
from uipath.platform.common import PagedResult
@@ -51,6 +55,26 @@ class UiPathBucketConfig:
prefix: str = ""
+def _content_str(data: dict[str, Any]) -> str:
+ """File content as a single string.
+
+ Current `FileData` stores `content` as one string. Blobs written by earlier
+ versions of this sample stored a list of lines, so both are accepted.
+ """
+ content = data.get("content", "")
+ if isinstance(content, list):
+ return "\n".join(content)
+ return content
+
+
+def _content_lines(data: dict[str, Any]) -> list[str]:
+ """File content split into lines, for either stored shape."""
+ content = data.get("content", "")
+ if isinstance(content, list):
+ return content
+ return content.splitlines()
+
+
class UiPathBucketBackend(BackendProtocol):
"""UiPath Storage Buckets backend for Deep Agents file operations.
@@ -148,8 +172,7 @@ def _get_file_data(self, path: str) -> dict[str, Any] | None:
try:
return json.loads(content.decode("utf-8"))
except (json.JSONDecodeError, UnicodeDecodeError):
- lines = content.decode("utf-8", errors="replace").splitlines()
- return {"content": lines}
+ return {"content": content.decode("utf-8", errors="replace")}
async def _aget_file_data(self, path: str) -> dict[str, Any] | None:
"""Get file data dict from bucket asynchronously."""
@@ -159,8 +182,7 @@ async def _aget_file_data(self, path: str) -> dict[str, Any] | None:
try:
return json.loads(content.decode("utf-8"))
except (json.JSONDecodeError, UnicodeDecodeError):
- lines = content.decode("utf-8", errors="replace").splitlines()
- return {"content": lines}
+ return {"content": content.decode("utf-8", errors="replace")}
def _put_file_data(
self, path: str, data: dict[str, Any], *, update_modified: bool = True
@@ -256,7 +278,7 @@ async def _alist_files(self, prefix: str = "") -> list[BucketFile]:
return results
- def ls_info(self, path: str) -> list[FileInfo]:
+ def ls(self, path: str) -> LsResult:
"""List files in a directory."""
prefix = path.lstrip("/")
if prefix and not prefix.endswith("/"):
@@ -277,17 +299,19 @@ def ls_info(self, path: str) -> list[FileInfo]:
seen_dirs.add(dir_path)
results.append({"path": dir_path, "is_dir": True})
else:
- results.append({
- "path": vpath,
- "is_dir": False,
- "size": file.size or 0,
- "modified_at": file.last_modified,
- })
+ results.append(
+ {
+ "path": vpath,
+ "is_dir": False,
+ "size": file.size or 0,
+ "modified_at": file.last_modified,
+ }
+ )
results.sort(key=lambda x: x.get("path", ""))
- return results
+ return LsResult(entries=results)
- async def als_info(self, path: str) -> list[FileInfo]:
+ async def als(self, path: str) -> LsResult:
"""List files in a directory asynchronously."""
prefix = path.lstrip("/")
if prefix and not prefix.endswith("/"):
@@ -308,89 +332,79 @@ async def als_info(self, path: str) -> list[FileInfo]:
seen_dirs.add(dir_path)
results.append({"path": dir_path, "is_dir": True})
else:
- results.append({
- "path": vpath,
- "is_dir": False,
- "size": file.size or 0,
- "modified_at": file.last_modified,
- })
+ results.append(
+ {
+ "path": vpath,
+ "is_dir": False,
+ "size": file.size or 0,
+ "modified_at": file.last_modified,
+ }
+ )
results.sort(key=lambda x: x.get("path", ""))
- return results
+ return LsResult(entries=results)
+
+ def read(self, file_path: str, offset: int = 0, limit: int = 2000) -> ReadResult:
+ """Read file content for the requested line range.
- def read(self, file_path: str, offset: int = 0, limit: int = 2000) -> str:
- """Read file content with line numbers."""
+ Returns the raw window plus pagination metadata; the filesystem
+ middleware adds the line-number gutter, so this must not format.
+ """
data = self._get_file_data(file_path)
if data is None:
- return f"Error: File '{file_path}' not found"
+ return ReadResult(error=f"Error: File '{file_path}' not found")
- lines = data.get("content", [])
- if not lines:
+ if not data.get("content"):
empty_msg = check_empty_content("")
if empty_msg:
- return empty_msg
+ return ReadResult(error=empty_msg)
- if offset >= len(lines):
- return f"Error: Line offset {offset} exceeds file length ({len(lines)} lines)"
+ return slice_read_response(data, offset, limit)
- selected = lines[offset : offset + limit]
- return format_content_with_line_numbers(selected, start_line=offset + 1)
+ async def aread(
+ self, file_path: str, offset: int = 0, limit: int = 2000
+ ) -> ReadResult:
+ """Read file content for the requested line range.
- async def aread(self, file_path: str, offset: int = 0, limit: int = 2000) -> str:
- """Read file content with line numbers asynchronously."""
+ Returns the raw window plus pagination metadata; the filesystem
+ middleware adds the line-number gutter, so this must not format.
+ """
data = await self._aget_file_data(file_path)
if data is None:
- return f"Error: File '{file_path}' not found"
+ return ReadResult(error=f"Error: File '{file_path}' not found")
- lines = data.get("content", [])
- if not lines:
+ if not data.get("content"):
empty_msg = check_empty_content("")
if empty_msg:
- return empty_msg
+ return ReadResult(error=empty_msg)
- if offset >= len(lines):
- return f"Error: Line offset {offset} exceeds file length ({len(lines)} lines)"
-
- selected = lines[offset : offset + limit]
- return format_content_with_line_numbers(selected, start_line=offset + 1)
+ return slice_read_response(data, offset, limit)
def write(self, file_path: str, content: str) -> WriteResult:
- """Create a new file."""
- if self._exists(file_path):
- return WriteResult(
- error=f"Cannot write to {file_path} because it already exists. "
- "Read and then make an edit, or write to a new path."
- )
-
+ """Create a file, replacing it if the path already exists."""
now = datetime.now(timezone.utc).isoformat()
data = {
- "content": content.splitlines(),
+ "content": content,
"created_at": now,
"modified_at": now,
}
try:
self._put_file_data(file_path, data, update_modified=False)
- return WriteResult(path=file_path, files_update=None)
+ return WriteResult(path=file_path)
except Exception as e:
return WriteResult(error=f"Error writing file '{file_path}': {e}")
async def awrite(self, file_path: str, content: str) -> WriteResult:
- """Create a new file asynchronously."""
- if await self._aexists(file_path):
- return WriteResult(
- error=f"Cannot write to {file_path} because it already exists. "
- "Read and then make an edit, or write to a new path."
- )
-
+ """Create a file asynchronously, replacing it if the path already exists."""
now = datetime.now(timezone.utc).isoformat()
data = {
- "content": content.splitlines(),
+ "content": content,
"created_at": now,
"modified_at": now,
}
try:
await self._aput_file_data(file_path, data, update_modified=False)
- return WriteResult(path=file_path, files_update=None)
+ return WriteResult(path=file_path)
except Exception as e:
return WriteResult(error=f"Error writing file '{file_path}': {e}")
@@ -406,20 +420,20 @@ def edit(
if data is None:
return EditResult(error=f"Error: File '{file_path}' not found")
- content = "\n".join(data.get("content", []))
- result = perform_string_replacement(content, old_string, new_string, replace_all)
+ content = _content_str(data)
+ result = perform_string_replacement(
+ content, old_string, new_string, replace_all
+ )
if isinstance(result, str):
return EditResult(error=result)
new_content, occurrences = result
- data["content"] = new_content.splitlines()
+ data["content"] = new_content
try:
self._put_file_data(file_path, data)
- return EditResult(
- path=file_path, files_update=None, occurrences=int(occurrences)
- )
+ return EditResult(path=file_path, occurrences=int(occurrences))
except Exception as e:
return EditResult(error=f"Error editing file '{file_path}': {e}")
@@ -435,31 +449,36 @@ async def aedit(
if data is None:
return EditResult(error=f"Error: File '{file_path}' not found")
- content = "\n".join(data.get("content", []))
- result = perform_string_replacement(content, old_string, new_string, replace_all)
+ content = _content_str(data)
+ result = perform_string_replacement(
+ content, old_string, new_string, replace_all
+ )
if isinstance(result, str):
return EditResult(error=result)
new_content, occurrences = result
- data["content"] = new_content.splitlines()
+ data["content"] = new_content
try:
await self._aput_file_data(file_path, data)
- return EditResult(
- path=file_path, files_update=None, occurrences=int(occurrences)
- )
+ return EditResult(path=file_path, occurrences=int(occurrences))
except Exception as e:
return EditResult(error=f"Error editing file '{file_path}': {e}")
- def grep_raw(
- self, pattern: str, path: str | None = None, glob: str | None = None
- ) -> list[GrepMatch] | str:
+ def grep(
+ self,
+ pattern: str,
+ path: str | None = None,
+ glob: str | None = None,
+ *,
+ max_count: int | None = None,
+ ) -> GrepResult:
"""Search for pattern in files."""
try:
regex = re.compile(pattern)
except re.error as e:
- return f"Invalid regex pattern: {e}"
+ return GrepResult(error=f"Invalid regex pattern: {e}")
search_prefix = (path or "/").lstrip("/")
files = self._list_files(search_prefix)
@@ -476,20 +495,27 @@ def grep_raw(
if data is None:
continue
- for line_num, line in enumerate(data.get("content", []), 1):
+ for line_num, line in enumerate(_content_lines(data), 1):
if regex.search(line):
matches.append({"path": vpath, "line": line_num, "text": line})
+ if max_count is not None and len(matches) >= max_count:
+ return GrepResult(matches=matches, truncated=True)
- return matches
+ return GrepResult(matches=matches)
- async def agrep_raw(
- self, pattern: str, path: str | None = None, glob: str | None = None
- ) -> list[GrepMatch] | str:
+ async def agrep(
+ self,
+ pattern: str,
+ path: str | None = None,
+ glob: str | None = None,
+ *,
+ max_count: int | None = None,
+ ) -> GrepResult:
"""Search for pattern in files asynchronously."""
try:
regex = re.compile(pattern)
except re.error as e:
- return f"Invalid regex pattern: {e}"
+ return GrepResult(error=f"Invalid regex pattern: {e}")
search_prefix = (path or "/").lstrip("/")
files = await self._alist_files(search_prefix)
@@ -506,14 +532,17 @@ async def agrep_raw(
if data is None:
continue
- for line_num, line in enumerate(data.get("content", []), 1):
+ for line_num, line in enumerate(_content_lines(data), 1):
if regex.search(line):
matches.append({"path": vpath, "line": line_num, "text": line})
+ if max_count is not None and len(matches) >= max_count:
+ return GrepResult(matches=matches, truncated=True)
- return matches
+ return GrepResult(matches=matches)
- def glob_info(self, pattern: str, path: str = "/") -> list[FileInfo]:
+ def glob(self, pattern: str, path: str | None = None) -> GlobResult:
"""Find files matching a glob pattern."""
+ path = path or "/"
search_prefix = path.lstrip("/")
files = self._list_files(search_prefix)
results: list[FileInfo] = []
@@ -523,18 +552,21 @@ def glob_info(self, pattern: str, path: str = "/") -> list[FileInfo]:
rel_path = vpath[len(path) :].lstrip("/") if path != "/" else vpath[1:]
if fnmatch.fnmatch(rel_path, pattern) or fnmatch.fnmatch(vpath, pattern):
- results.append({
- "path": vpath,
- "is_dir": False,
- "size": file.size or 0,
- "modified_at": file.last_modified,
- })
+ results.append(
+ {
+ "path": vpath,
+ "is_dir": False,
+ "size": file.size or 0,
+ "modified_at": file.last_modified,
+ }
+ )
results.sort(key=lambda x: x.get("path", ""))
- return results
+ return GlobResult(matches=results)
- async def aglob_info(self, pattern: str, path: str = "/") -> list[FileInfo]:
+ async def aglob(self, pattern: str, path: str | None = None) -> GlobResult:
"""Find files matching a glob pattern asynchronously."""
+ path = path or "/"
search_prefix = path.lstrip("/")
files = await self._alist_files(search_prefix)
results: list[FileInfo] = []
@@ -544,15 +576,17 @@ async def aglob_info(self, pattern: str, path: str = "/") -> list[FileInfo]:
rel_path = vpath[len(path) :].lstrip("/") if path != "/" else vpath[1:]
if fnmatch.fnmatch(rel_path, pattern) or fnmatch.fnmatch(vpath, pattern):
- results.append({
- "path": vpath,
- "is_dir": False,
- "size": file.size or 0,
- "modified_at": file.last_modified,
- })
+ results.append(
+ {
+ "path": vpath,
+ "is_dir": False,
+ "size": file.size or 0,
+ "modified_at": file.last_modified,
+ }
+ )
results.sort(key=lambda x: x.get("path", ""))
- return results
+ return GlobResult(matches=results)
def upload_files(self, files: list[tuple[str, bytes]]) -> list[FileUploadResponse]:
"""Upload multiple files."""
@@ -570,7 +604,9 @@ def upload_files(self, files: list[tuple[str, bytes]]) -> list[FileUploadRespons
except LookupError:
responses.append(FileUploadResponse(path=path, error="file_not_found"))
except PermissionError:
- responses.append(FileUploadResponse(path=path, error="permission_denied"))
+ responses.append(
+ FileUploadResponse(path=path, error="permission_denied")
+ )
except Exception:
responses.append(FileUploadResponse(path=path, error="invalid_path"))
@@ -594,7 +630,9 @@ async def aupload_files(
except LookupError:
responses.append(FileUploadResponse(path=path, error="file_not_found"))
except PermissionError:
- responses.append(FileUploadResponse(path=path, error="permission_denied"))
+ responses.append(
+ FileUploadResponse(path=path, error="permission_denied")
+ )
except Exception:
responses.append(FileUploadResponse(path=path, error="invalid_path"))
@@ -609,7 +647,9 @@ def download_files(self, paths: list[str]) -> list[FileDownloadResponse]:
content = self._download_content(path)
if content is None:
responses.append(
- FileDownloadResponse(path=path, content=None, error="file_not_found")
+ FileDownloadResponse(
+ path=path, content=None, error="file_not_found"
+ )
)
else:
responses.append(
@@ -617,7 +657,9 @@ def download_files(self, paths: list[str]) -> list[FileDownloadResponse]:
)
except PermissionError:
responses.append(
- FileDownloadResponse(path=path, content=None, error="permission_denied")
+ FileDownloadResponse(
+ path=path, content=None, error="permission_denied"
+ )
)
except Exception:
responses.append(
@@ -635,7 +677,9 @@ async def adownload_files(self, paths: list[str]) -> list[FileDownloadResponse]:
content = await self._adownload_content(path)
if content is None:
responses.append(
- FileDownloadResponse(path=path, content=None, error="file_not_found")
+ FileDownloadResponse(
+ path=path, content=None, error="file_not_found"
+ )
)
else:
responses.append(
@@ -643,7 +687,9 @@ async def adownload_files(self, paths: list[str]) -> list[FileDownloadResponse]:
)
except PermissionError:
responses.append(
- FileDownloadResponse(path=path, content=None, error="permission_denied")
+ FileDownloadResponse(
+ path=path, content=None, error="permission_denied"
+ )
)
except Exception:
responses.append(
diff --git a/samples/simple-deepagent/pyproject.toml b/samples/simple-deepagent/pyproject.toml
index e82ea56f0..d673a42dc 100644
--- a/samples/simple-deepagent/pyproject.toml
+++ b/samples/simple-deepagent/pyproject.toml
@@ -5,7 +5,7 @@ description = "Simple DeepAgent for research tasks using Tavily search"
authors = [{ name = "John Doe", email = "john.doe@myemail.com" }]
requires-python = ">=3.11"
dependencies = [
- "deepagents>=0.3.9",
+ "deepagents>=0.7.11, <0.8.0",
"langchain-anthropic>=1.3.1",
"langchain-tavily>=0.2.17",
"langgraph>=1.0.7",
diff --git a/src/uipath_langchain/_utils/durable_interrupt/__init__.py b/src/uipath_langchain/_utils/durable_interrupt/__init__.py
index bd36440fb..42f6cabfe 100644
--- a/src/uipath_langchain/_utils/durable_interrupt/__init__.py
+++ b/src/uipath_langchain/_utils/durable_interrupt/__init__.py
@@ -1,13 +1,17 @@
"""Durable interrupt package for side-effect-safe interrupt/resume in LangGraph."""
from .decorator import (
+ SUSPENDS_RUN,
_durable_state,
durable_interrupt,
+ suspends_run,
)
from .skip_interrupt import SkipInterruptValue
__all__ = [
+ "SUSPENDS_RUN",
"durable_interrupt",
"SkipInterruptValue",
"_durable_state",
+ "suspends_run",
]
diff --git a/src/uipath_langchain/_utils/durable_interrupt/decorator.py b/src/uipath_langchain/_utils/durable_interrupt/decorator.py
index 1d304f11f..a3d0d0a16 100644
--- a/src/uipath_langchain/_utils/durable_interrupt/decorator.py
+++ b/src/uipath_langchain/_utils/durable_interrupt/decorator.py
@@ -94,6 +94,41 @@ def _inject_resume(scratchpad: Any, value: Any) -> Any:
return value
+SUSPENDS_RUN = "suspends_run"
+"""Tool-metadata key marking a tool that can suspend the run instead of returning.
+
+Such a tool may raise ``GraphInterrupt`` rather than produce a value: the run
+checkpoints, and the node is replayed from that checkpoint on resume. Callers that
+invoke tools *outside* the graph's tool node -- the QuickJS code interpreter's
+programmatic tool calling, for one -- must not offer these, because a replayed
+node re-runs every call made before the interrupt, and because such bridges
+bypass approval hooks.
+
+The flag describes that behaviour rather than how it is reached. It covers both
+``durable_interrupt`` and a bare ``interrupt()``, and it is set unconditionally on
+a tool that suspends only sometimes -- one returning a ``SkipInterruptValue`` on
+its fast path, say -- since the safe answer for a caller outside the tool node is
+the same either way.
+
+Set it in the tool's ``metadata`` in every factory that can suspend;
+``tests/agent/tools/test_suspends_run_metadata.py`` fails if one forgets.
+"""
+
+
+def suspends_run(tool: Any) -> bool:
+ """Whether ``tool`` suspends the run instead of returning a value.
+
+ Reads :data:`SUSPENDS_RUN` from the tool's metadata, so it is accurate
+ per-tool even where one factory builds both suspending and non-suspending
+ variants (``context_tool`` does, by retrieval mode).
+
+ Fails closed only where the flag is present: an unstamped tool reports
+ ``False``. ``tests/agent/tools/test_suspends_run_metadata.py`` is what keeps
+ that safe, by failing when a suspending factory ships without the stamp.
+ """
+ return bool((getattr(tool, "metadata", None) or {}).get(SUSPENDS_RUN))
+
+
def durable_interrupt(fn: F) -> F:
"""Decorator that executes a side-effecting function exactly once and interrupts.
diff --git a/src/uipath_langchain/agent/advanced/__init__.py b/src/uipath_langchain/agent/advanced/__init__.py
index 1605996aa..dc4440d3d 100644
--- a/src/uipath_langchain/agent/advanced/__init__.py
+++ b/src/uipath_langchain/agent/advanced/__init__.py
@@ -2,13 +2,17 @@
from deepagents import CompiledSubAgent, SubAgent
from deepagents.backends import BackendProtocol, FilesystemBackend
-from deepagents.backends.protocol import BackendFactory
from .agent import (
create_advanced_agent,
create_advanced_agent_graph,
create_conversational_advanced_agent_graph,
)
+from .code_interpreter import (
+ PTC_FILESYSTEM_TOOLS,
+ build_code_interpreter_middleware,
+ ptc_tool_names,
+)
from .types import AdvancedAgentGraphState, ConversationalAdvancedAgentGraphState
from .utils import (
MEMORY_DIR_NAME,
@@ -21,15 +25,17 @@
"MEMORY_DIR_NAME",
"MEMORY_INDEX_FILENAME",
"MEMORY_INDEX_VIRTUAL_PATH",
+ "PTC_FILESYSTEM_TOOLS",
"AdvancedAgentGraphState",
- "BackendFactory",
"BackendProtocol",
"CompiledSubAgent",
"ConversationalAdvancedAgentGraphState",
"FilesystemBackend",
"SubAgent",
+ "build_code_interpreter_middleware",
"create_advanced_agent",
"create_advanced_agent_graph",
"create_conversational_advanced_agent_graph",
"create_state_with_input",
+ "ptc_tool_names",
]
diff --git a/src/uipath_langchain/agent/advanced/agent.py b/src/uipath_langchain/agent/advanced/agent.py
index ea6e1d564..f93a4d3fc 100644
--- a/src/uipath_langchain/agent/advanced/agent.py
+++ b/src/uipath_langchain/agent/advanced/agent.py
@@ -6,14 +6,13 @@
from deepagents import CompiledSubAgent, SubAgent
from deepagents import create_deep_agent as _create_deep_agent
-from deepagents.backends import BackendProtocol
-from deepagents.backends.filesystem import FilesystemBackend
-from deepagents.backends.protocol import BackendFactory
+from deepagents.backends import BackendProtocol, FilesystemBackend
from langchain.agents.middleware import (
AgentMiddleware,
AgentState,
ModelRequest,
ModelResponse,
+ TodoListMiddleware,
)
from langchain.agents.structured_output import ResponseFormat
from langchain_core.language_models import BaseChatModel
@@ -129,7 +128,7 @@ def create_advanced_agent(
system_prompt: str | SystemMessage | None = "",
tools: Sequence[BaseTool] = (),
subagents: Sequence[SubAgent | CompiledSubAgent] = (),
- backend: BackendProtocol | BackendFactory | None = None,
+ backend: BackendProtocol | None = None,
response_format: ResponseFormat[Any] | None = None,
memory: Sequence[str] = (),
middleware: Sequence[AgentMiddleware[Any, Any]] = (),
@@ -143,6 +142,10 @@ def create_advanced_agent(
``skills`` is a list of skill source paths for deepagents' ``SkillsMiddleware``;
``None`` or empty disables it (mirroring ``_create_deep_agent``'s contract).
+
+ ``TodoListMiddleware`` is prepended so ``write_todos`` stays available: it left
+ the deepagents default stack in 0.7.0. Callers may replace it by passing their
+ own instance, which deepagents matches on ``.name``.
"""
return _create_deep_agent(
model=model,
@@ -152,7 +155,7 @@ def create_advanced_agent(
backend=backend,
response_format=response_format,
memory=list(memory) or None,
- middleware=list(middleware),
+ middleware=[TodoListMiddleware(), *middleware],
skills=list(skills) if skills else None,
)
@@ -161,12 +164,13 @@ def create_advanced_agent_graph(
model: BaseChatModel,
tools: Sequence[BaseTool],
system_prompt: str | Callable[[dict[str, Any]], str],
- backend: BackendProtocol | BackendFactory | None,
+ backend: BackendProtocol | None,
response_format: ResponseFormat[Any] | None,
input_schema: type[BaseModel] | None,
output_schema: type[BaseModel],
build_user_message: Callable[[dict[str, Any]], str],
skills: Sequence[str] | None = None,
+ middleware: Sequence[AgentMiddleware[Any, Any]] = (),
) -> StateGraph[Any, Any, Any, Any]:
"""Wrap the advanced agent in a parent graph that maps typed I/O to/from messages.
@@ -190,7 +194,7 @@ def create_advanced_agent_graph(
backend=backend,
response_format=response_format,
memory=memory_sources,
- middleware=runtime_prompt.middleware,
+ middleware=[*runtime_prompt.middleware, *middleware],
skills=skills,
)
@@ -249,9 +253,10 @@ def create_conversational_advanced_agent_graph(
model: BaseChatModel,
tools: Sequence[BaseTool],
system_prompt: str | Callable[[dict[str, Any]], str],
- backend: BackendProtocol | BackendFactory | None,
+ backend: BackendProtocol | None,
skills: Sequence[str] | None = None,
input_schema: type[BaseModel] | None = None,
+ middleware: Sequence[AgentMiddleware[Any, Any]] = (),
) -> StateGraph[Any, Any, Any, Any]:
"""Wrap the advanced agent in a parent graph that speaks the conversational contract.
@@ -279,7 +284,7 @@ def create_conversational_advanced_agent_graph(
system_prompt=runtime_prompt.static_prompt,
backend=backend,
memory=memory_sources,
- middleware=runtime_prompt.middleware,
+ middleware=[*runtime_prompt.middleware, *middleware],
skills=skills,
)
diff --git a/src/uipath_langchain/agent/advanced/code_interpreter.py b/src/uipath_langchain/agent/advanced/code_interpreter.py
new file mode 100644
index 000000000..e44703127
--- /dev/null
+++ b/src/uipath_langchain/agent/advanced/code_interpreter.py
@@ -0,0 +1,185 @@
+"""The QuickJS code interpreter for advanced agents, and what it may call.
+
+``CodeInterpreterMiddleware`` adds one ``eval`` tool: a persistent JavaScript REPL
+in a WASM guest (QuickJS-ng under wasmtime). It serves three purposes in a single
+tool call -- computation, programmatic tool calling (PTC), and subagent
+orchestration through the top-level ``task()`` global.
+
+The guest has no ambient capability: no network, no filesystem, no ``fetch``, no
+``require``, no timers. Everything it can reach arrives through the ``ptc``
+allowlist, which makes that allowlist the entire security surface of the feature.
+It is derived here rather than configured, because the rule that governs it is a
+property of our tools (see :data:`SUSPENDS_RUN`) and not of any one consumer.
+
+Requires the ``code-interpreter`` extra::
+
+ uv add "uipath-langchain[code-interpreter]"
+"""
+
+from __future__ import annotations
+
+import logging
+from collections.abc import Iterable, Sequence
+from typing import Any
+
+from langchain.agents.middleware import AgentMiddleware
+from langchain_core.tools import BaseTool
+
+from uipath_langchain._utils.durable_interrupt import suspends_run
+
+logger = logging.getLogger(__name__)
+
+_MISSING_EXTRA = (
+ "The code interpreter needs the 'code-interpreter' extra. Install it with "
+ '`uv add "uipath-langchain[code-interpreter]"` (or `pip install '
+ '"uipath-langchain[code-interpreter]"`).'
+)
+
+# deepagents' own file tools, safe to reach from inside the REPL: each returns a
+# value, and each routes through the backend, so ``virtual_mode`` still resolves
+# and bounds every path. ``delete`` and ``execute`` are deliberately absent, and
+# ``task`` is reserved -- upstream raises if it is listed, because it is exposed
+# as the ``task()`` global instead.
+PTC_FILESYSTEM_TOOLS: tuple[str, ...] = (
+ "ls",
+ "read_file",
+ "write_file",
+ "edit_file",
+ "glob",
+ "grep",
+)
+
+_RESERVED_TOOL_NAMES = frozenset({"task"})
+
+# Per-eval wall clock. The REPL is for orchestration and arithmetic, not long
+# computation, and a bridged tool call does not consume it.
+DEFAULT_EVAL_TIMEOUT_SECONDS = 5.0
+
+
+def ptc_tool_names(tools: Sequence[BaseTool]) -> list[str]:
+ """Names of the agent tools that may be called from inside the REPL.
+
+ Three exclusions, each for a different reason:
+
+ - **Tools that suspend the run.** One raising ``GraphInterrupt`` never returns
+ a value into the JS ``await``. Worse, the node is replayed from its
+ checkpoint on resume, so the ``eval`` re-runs from the top and every bridged
+ call made before the interrupt fires a second time. Upstream also documents
+ that PTC bridges bypass ``interrupt_on`` approval hooks, so an escalation
+ reached this way would skip its own approval.
+ - **Names that cannot be JavaScript identifiers.** Low-code tool names come
+ from ``agent.json`` and may hold spaces, dots or non-ASCII characters.
+ Upstream raises ``ValueError`` for those from inside ``wrap_model_call``,
+ faulting the run mid-turn, so they are dropped here instead.
+ - **camelCase collisions.** ``get_invoice`` and ``get-invoice`` both become
+ ``getInvoice``, and upstream dedupes by tool name rather than camel name.
+ Every member of a colliding group is dropped: binding one of two
+ identically-named JS functions would silently call the wrong tool.
+
+ An excluded tool stays fully available as an ordinary tool call, so exclusion
+ costs a model round trip, never a capability.
+ """
+ is_valid, to_camel = _name_validators()
+
+ eligible: list[BaseTool] = []
+ for tool in tools:
+ if tool.name in _RESERVED_TOOL_NAMES:
+ continue
+ if suspends_run(tool):
+ logger.debug("Tool %r withheld from PTC: it suspends the run", tool.name)
+ continue
+ if not is_valid(tool.name):
+ logger.info(
+ "Tool %r withheld from PTC: %r is not a valid JavaScript identifier",
+ tool.name,
+ to_camel(tool.name),
+ )
+ continue
+ eligible.append(tool)
+
+ return [t.name for t in _without_camel_collisions(eligible, to_camel)]
+
+
+def build_code_interpreter_middleware(
+ tools: Sequence[BaseTool],
+ *,
+ timeout: float = DEFAULT_EVAL_TIMEOUT_SECONDS,
+ subagents: bool = True,
+) -> list[AgentMiddleware[Any, Any]]:
+ """The code-interpreter middleware for ``tools``, ready to pass as ``middleware``.
+
+ Returned as a list so a caller can splice it into a middleware sequence
+ without branching.
+
+ Args:
+ tools: The agent's tools. Eligible ones become callable from the REPL.
+ timeout: Per-eval wall clock in seconds.
+ subagents: Expose the top-level ``task()`` global when the host has a
+ deepagents ``task`` tool. A no-op for an agent with no subagents.
+
+ Raises:
+ ImportError: If the ``code-interpreter`` extra is not installed.
+ """
+ middleware_cls = _code_interpreter_middleware_cls()
+ exposed = ptc_tool_names(tools)
+ logger.info(
+ "Code interpreter enabled: %d of %d agent tools exposed for PTC",
+ len(exposed),
+ len(tools),
+ )
+ return [
+ middleware_cls(
+ ptc=[*exposed, *PTC_FILESYSTEM_TOOLS],
+ # Globals persist across turns of a LangGraph thread. Whether that
+ # survives a suspend/resume boundary is unverified; "turn" is the
+ # fallback if it does not.
+ mode="thread",
+ subagents=subagents,
+ timeout=timeout,
+ )
+ ]
+
+
+def _without_camel_collisions(
+ tools: Iterable[BaseTool], to_camel: Any
+) -> list[BaseTool]:
+ """Drop every tool whose camelCase name is shared with another tool."""
+ by_camel: dict[str, list[BaseTool]] = {}
+ for tool in tools:
+ by_camel.setdefault(to_camel(tool.name), []).append(tool)
+
+ kept: list[BaseTool] = []
+ for camel, group in by_camel.items():
+ if len(group) > 1:
+ logger.warning(
+ "Tools %s withheld from PTC: their names all map to %r",
+ [t.name for t in group],
+ camel,
+ )
+ continue
+ kept.append(group[0])
+ return kept
+
+
+def _code_interpreter_middleware_cls() -> Any:
+ """Import ``CodeInterpreterMiddleware``, or raise with install guidance."""
+ try:
+ from langchain_quickjs import CodeInterpreterMiddleware
+ except ImportError as exc: # pragma: no cover - exercised via monkeypatch
+ raise ImportError(_MISSING_EXTRA) from exc
+ return CodeInterpreterMiddleware
+
+
+def _name_validators() -> tuple[Any, Any]:
+ """Upstream's identifier rule and camelCase conversion.
+
+ Taken from ``langchain_quickjs._ptc`` rather than reimplemented: a local copy
+ risks drifting *looser* than upstream, and anything upstream rejects raises
+ from inside ``wrap_model_call``, faulting the run rather than degrading. The
+ import is pinned by ``tests/agent/advanced/test_code_interpreter.py``.
+ """
+ try:
+ from langchain_quickjs._ptc import is_valid_ptc_tool_name, to_camel_case
+ except ImportError as exc: # pragma: no cover - exercised via monkeypatch
+ raise ImportError(_MISSING_EXTRA) from exc
+ return is_valid_ptc_tool_name, to_camel_case
diff --git a/src/uipath_langchain/agent/advanced/utils.py b/src/uipath_langchain/agent/advanced/utils.py
index cee975c83..888ef64cd 100644
--- a/src/uipath_langchain/agent/advanced/utils.py
+++ b/src/uipath_langchain/agent/advanced/utils.py
@@ -8,7 +8,6 @@
from typing import Any, NamedTuple, cast
from deepagents.backends import BackendProtocol, FilesystemBackend
-from deepagents.backends.protocol import BackendFactory
from jsonpath_ng import parse as jsonpath_parse # type: ignore[import-untyped]
from pydantic import BaseModel, ConfigDict
from uipath.platform import UiPath
@@ -59,7 +58,7 @@ class _AttachmentDownload(NamedTuple):
async def resolve_input_attachments(
- backend: BackendProtocol | BackendFactory | None,
+ backend: BackendProtocol | None,
attachment_paths: list[str],
input_args: dict[str, Any],
) -> dict[str, Any]:
diff --git a/src/uipath_langchain/agent/tools/client_side_tool.py b/src/uipath_langchain/agent/tools/client_side_tool.py
index b6cc710c3..fe0e93c11 100644
--- a/src/uipath_langchain/agent/tools/client_side_tool.py
+++ b/src/uipath_langchain/agent/tools/client_side_tool.py
@@ -9,7 +9,10 @@
from uipath.agent.models.agent import AgentClientSideToolResourceConfig
from uipath.eval.mocks import mockable
-from uipath_langchain._utils.durable_interrupt import durable_interrupt
+from uipath_langchain._utils.durable_interrupt import (
+ SUSPENDS_RUN,
+ durable_interrupt,
+)
from uipath_langchain.agent.contracts.client_side_tools import (
ClientSideToolInfo as ClientSideToolInfo,
)
@@ -126,6 +129,7 @@ async def wait_for_client_execution() -> dict[str, Any]:
metadata={
IS_CONVERSATIONAL_CLIENT_SIDE_TOOL: True,
"output_schema": resource.output_schema,
+ SUSPENDS_RUN: True,
},
)
diff --git a/src/uipath_langchain/agent/tools/context_tool.py b/src/uipath_langchain/agent/tools/context_tool.py
index 7c1c1a508..5479d8541 100644
--- a/src/uipath_langchain/agent/tools/context_tool.py
+++ b/src/uipath_langchain/agent/tools/context_tool.py
@@ -35,7 +35,10 @@
from uipath.runtime.errors import UiPathErrorCategory
from uipath_langchain._utils import get_execution_folder_path
-from uipath_langchain._utils.durable_interrupt import durable_interrupt
+from uipath_langchain._utils.durable_interrupt import (
+ SUSPENDS_RUN,
+ durable_interrupt,
+)
from uipath_langchain.agent.exceptions import (
AgentRuntimeError,
AgentRuntimeErrorCode,
@@ -471,6 +474,7 @@ async def context_deep_rag_wrapper(
"display_name": resource.name,
"index_name": resource.index_name,
"context_retrieval_mode": resource.settings.retrieval_mode,
+ SUSPENDS_RUN: True,
},
)
tool.set_tool_wrappers(awrapper=context_deep_rag_wrapper)
@@ -624,6 +628,7 @@ async def context_batch_transform_wrapper(
"index_name": resource.index_name,
"context_retrieval_mode": resource.settings.retrieval_mode,
"output_schema": output_model,
+ SUSPENDS_RUN: True,
},
)
tool.set_tool_wrappers(awrapper=job_attachment_wrapper)
diff --git a/src/uipath_langchain/agent/tools/escalation_tool.py b/src/uipath_langchain/agent/tools/escalation_tool.py
index 65d80b1c2..31089e82f 100644
--- a/src/uipath_langchain/agent/tools/escalation_tool.py
+++ b/src/uipath_langchain/agent/tools/escalation_tool.py
@@ -26,7 +26,10 @@
get_current_span_and_trace_ids,
get_execution_folder_path,
)
-from uipath_langchain._utils.durable_interrupt import durable_interrupt
+from uipath_langchain._utils.durable_interrupt import (
+ SUSPENDS_RUN,
+ durable_interrupt,
+)
from uipath_langchain.agent.react.jsonschema_pydantic_converter import (
create_model,
create_output_model,
@@ -514,6 +517,7 @@ async def escalation_wrapper(
argument_properties=channel.argument_properties,
metadata={
"tool_type": "escalation",
+ SUSPENDS_RUN: True,
"display_name": _try_get_channel_app_name(channel) or channel.name,
"channel_type": channel.type,
"recipient": None,
diff --git a/src/uipath_langchain/agent/tools/extraction_tool.py b/src/uipath_langchain/agent/tools/extraction_tool.py
index ea040b223..e825284ce 100644
--- a/src/uipath_langchain/agent/tools/extraction_tool.py
+++ b/src/uipath_langchain/agent/tools/extraction_tool.py
@@ -14,6 +14,7 @@
from uipath.platform.errors import EnrichedException
from uipath.runtime.errors import UiPathErrorCategory
+from uipath_langchain._utils.durable_interrupt import SUSPENDS_RUN
from uipath_langchain.agent.attachments.job_attachments import (
get_job_attachment_paths,
get_job_attachments,
@@ -159,6 +160,7 @@ async def extraction_tool_wrapper(
output_type=ExtractionResponseIXP,
metadata={
"tool_type": "ixp_extraction",
+ SUSPENDS_RUN: True,
"display_name": resource.name,
"project_name": project_name,
"version_tag": version_tag,
diff --git a/src/uipath_langchain/agent/tools/internal_tools/batch_transform_tool.py b/src/uipath_langchain/agent/tools/internal_tools/batch_transform_tool.py
index 3f828c91c..e825799dd 100644
--- a/src/uipath_langchain/agent/tools/internal_tools/batch_transform_tool.py
+++ b/src/uipath_langchain/agent/tools/internal_tools/batch_transform_tool.py
@@ -26,6 +26,7 @@
from uipath.runtime.errors import UiPathErrorCategory
from uipath_langchain._utils.durable_interrupt import (
+ SUSPENDS_RUN,
SkipInterruptValue,
durable_interrupt,
)
@@ -205,6 +206,7 @@ async def upload_result_attachment():
"args_schema": input_model,
"output_schema": output_model,
"retrieval_mode": "BatchTransform",
+ SUSPENDS_RUN: True,
"output_columns": [
{"name": col.name, "description": col.description}
for col in batch_transform_output_columns
diff --git a/src/uipath_langchain/agent/tools/internal_tools/deeprag_tool.py b/src/uipath_langchain/agent/tools/internal_tools/deeprag_tool.py
index 0d866df1f..3425d8fe0 100644
--- a/src/uipath_langchain/agent/tools/internal_tools/deeprag_tool.py
+++ b/src/uipath_langchain/agent/tools/internal_tools/deeprag_tool.py
@@ -24,6 +24,7 @@
from uipath.runtime.errors import UiPathErrorCategory
from uipath_langchain._utils.durable_interrupt import (
+ SUSPENDS_RUN,
SkipInterruptValue,
durable_interrupt,
)
@@ -185,6 +186,7 @@ async def create_deeprag():
"display_name": tool_name,
"args_schema": input_model,
"output_schema": output_model,
+ SUSPENDS_RUN: True,
},
)
tool.set_tool_wrappers(awrapper=job_attachment_wrapper)
diff --git a/src/uipath_langchain/agent/tools/ixp_escalation_tool.py b/src/uipath_langchain/agent/tools/ixp_escalation_tool.py
index 45abfb07c..16396c763 100644
--- a/src/uipath_langchain/agent/tools/ixp_escalation_tool.py
+++ b/src/uipath_langchain/agent/tools/ixp_escalation_tool.py
@@ -21,7 +21,10 @@
)
from uipath.runtime.errors import UiPathErrorCategory
-from uipath_langchain._utils.durable_interrupt import durable_interrupt
+from uipath_langchain._utils.durable_interrupt import (
+ SUSPENDS_RUN,
+ durable_interrupt,
+)
from uipath_langchain.agent.react.types import AgentGraphState
from uipath_langchain.agent.tools.tool_node import (
ToolWrapperMixin,
@@ -183,6 +186,7 @@ async def ixp_escalation_tool_wrapper(
output_type=OutputSchema,
metadata={
"tool_type": "vs_escalation",
+ SUSPENDS_RUN: True,
"display_name": channel.properties.app_name,
"channel_type": channel.type,
"ixp_tool_id": ixp_tool_name,
diff --git a/src/uipath_langchain/agent/tools/process_tool.py b/src/uipath_langchain/agent/tools/process_tool.py
index 721eb0fe5..5fc2fc32c 100644
--- a/src/uipath_langchain/agent/tools/process_tool.py
+++ b/src/uipath_langchain/agent/tools/process_tool.py
@@ -13,7 +13,10 @@
from uipath.runtime.errors import UiPathErrorCategory
from uipath_langchain._utils import get_execution_folder_path
-from uipath_langchain._utils.durable_interrupt import durable_interrupt
+from uipath_langchain._utils.durable_interrupt import (
+ SUSPENDS_RUN,
+ durable_interrupt,
+)
from uipath_langchain.agent.attachments.job_attachments import get_job_attachments
from uipath_langchain.agent.exceptions import raise_for_enriched
from uipath_langchain.agent.react.jsonschema_pydantic_converter import (
@@ -134,6 +137,7 @@ async def start_job():
output_type=output_model,
metadata={
"tool_type": resource.type.lower(),
+ SUSPENDS_RUN: True,
"display_name": process_name,
"folder_path": folder_path,
"args_schema": input_model,
diff --git a/tests/agent/advanced/test_code_interpreter.py b/tests/agent/advanced/test_code_interpreter.py
new file mode 100644
index 000000000..a93adfedb
--- /dev/null
+++ b/tests/agent/advanced/test_code_interpreter.py
@@ -0,0 +1,248 @@
+"""Tests for the QuickJS code interpreter and its PTC allowlist policy.
+
+The allowlist is the whole security surface of this feature: the WASM guest has
+no ambient capability, so anything the sandboxed JS reaches, it reached through
+``ptc``. These cover what must be in it, what must stay out, and that the sandbox
+boundary still holds for the file tools that are in it.
+
+Requires the ``code-interpreter`` extra, which CI installs via
+``uv sync --all-extras``.
+"""
+
+import asyncio
+import sys
+from pathlib import Path
+from typing import Any, Sequence
+
+import pytest
+from deepagents.backends import FilesystemBackend
+from langchain_core.language_models.fake_chat_models import GenericFakeChatModel
+from langchain_core.messages import AIMessage
+from langchain_core.tools import BaseTool, StructuredTool, tool
+
+from uipath_langchain._utils.durable_interrupt import SUSPENDS_RUN
+from uipath_langchain.agent.advanced import (
+ PTC_FILESYSTEM_TOOLS,
+ build_code_interpreter_middleware,
+ create_advanced_agent,
+ ptc_tool_names,
+)
+
+pytest.importorskip("langchain_quickjs", reason="needs the code-interpreter extra")
+
+
+def _tool(name: str, *, suspends: bool = False) -> BaseTool:
+ """A minimal agent tool, optionally flagged as suspending the run."""
+ return StructuredTool.from_function(
+ func=lambda value="": value,
+ name=name,
+ description=f"tool {name}",
+ metadata={SUSPENDS_RUN: True} if suspends else {},
+ )
+
+
+class _ScriptedModel(GenericFakeChatModel):
+ """Replays a fixed script and accepts any tool binding."""
+
+ model_name: str = "test-model-code-interpreter"
+
+ def _get_ls_params(self, stop: list[str] | None = None, **kwargs: Any) -> Any:
+ return {"ls_provider": "openai", "ls_model_name": self.model_name}
+
+ def bind_tools(self, tools: Sequence[Any], **kwargs: Any) -> "_ScriptedModel":
+ return self
+
+
+def _run_js(code: str, workspace: Path, tools: Sequence[BaseTool] = ()) -> str:
+ """Run one ``eval`` call through a real advanced agent, return the tool output."""
+ model = _ScriptedModel(
+ messages=iter(
+ [
+ AIMessage(
+ content="",
+ tool_calls=[{"name": "eval", "args": {"code": code}, "id": "c1"}],
+ ),
+ AIMessage(content="done"),
+ ]
+ )
+ )
+ graph = create_advanced_agent(
+ model=model,
+ tools=list(tools),
+ backend=FilesystemBackend(root_dir=workspace, virtual_mode=True),
+ middleware=build_code_interpreter_middleware(list(tools)),
+ )
+ result = asyncio.run(
+ graph.ainvoke({"messages": [{"role": "user", "content": "go"}]})
+ )
+ tool_messages = [m for m in result["messages"] if m.type == "tool"]
+ assert tool_messages, "the eval tool produced no output"
+ return str(tool_messages[0].content)
+
+
+# --------------------------------------------------------------------------
+# Allowlist policy
+# --------------------------------------------------------------------------
+
+
+def test_suspending_tools_are_withheld() -> None:
+ """A tool that suspends the run must never be reachable from the REPL.
+
+ It cannot return a value into the JS ``await``, a replayed node re-runs every
+ bridged call made before the interrupt, and PTC bypasses approval hooks.
+ """
+ assert ptc_tool_names(
+ [_tool("read_invoice"), _tool("escalate", suspends=True)]
+ ) == ["read_invoice"]
+
+
+@pytest.mark.parametrize(
+ "name",
+ ["Get Invoice", "invoice.total", "2fa_check", "tool!", "faktura_\u010desk\u00e1"],
+ ids=["space", "dot", "leading-digit", "punctuation", "non-ascii"],
+)
+def test_names_that_cannot_be_js_identifiers_are_withheld(name: str) -> None:
+ """Dropped here rather than raising from inside ``wrap_model_call`` mid-run.
+
+ Low-code tool names come from ``agent.json`` and are not constrained to
+ JavaScript identifiers.
+ """
+ assert ptc_tool_names([_tool(name)]) == []
+
+
+def test_camel_case_collisions_are_withheld() -> None:
+ """Two tools that camel-case to one name are both dropped.
+
+ Upstream dedupes by tool name, not camel name, so binding either would
+ silently call the wrong tool.
+ """
+ assert ptc_tool_names([_tool("get_invoice"), _tool("get-invoice")]) == []
+
+
+def test_reserved_task_name_is_withheld() -> None:
+ """``task`` is the top-level ``task()`` global; listing it in ptc raises upstream."""
+ assert ptc_tool_names([_tool("task")]) == []
+
+
+def test_filesystem_tools_exposed_and_dangerous_ones_are_not() -> None:
+ """Workspace file access is offered; ``delete``, ``execute`` and ``task`` are not.
+
+ Asserted on the composed allowlist rather than the middleware's internals.
+ That the allowlist actually reaches the sandbox is covered end to end by
+ ``test_workspace_files_are_reachable_through_the_file_tools``.
+ """
+ exposed = {*ptc_tool_names([_tool("read_invoice")]), *PTC_FILESYSTEM_TOOLS}
+ assert set(PTC_FILESYSTEM_TOOLS) <= exposed
+ assert "read_invoice" in exposed
+ assert exposed.isdisjoint({"delete", "execute", "task", "eval"})
+
+
+def test_factory_returns_one_middleware() -> None:
+ """The factory hands back exactly one entry, spliceable into a sequence."""
+ assert len(build_code_interpreter_middleware([_tool("read_invoice")])) == 1
+
+
+def test_factory_without_the_extra_raises_install_guidance(
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ """The extra must be genuinely optional.
+
+ Importing ``uipath_langchain.agent.advanced`` has to keep working for every
+ consumer that never asked for the code interpreter, so the middleware import
+ is deferred into the factory. Setting the module to ``None`` in
+ ``sys.modules`` is how the stdlib signals "absent", which is what a base
+ install looks like.
+ """
+ monkeypatch.setitem(sys.modules, "langchain_quickjs", None)
+ monkeypatch.setitem(sys.modules, "langchain_quickjs._ptc", None)
+
+ with pytest.raises(ImportError, match="code-interpreter"):
+ build_code_interpreter_middleware([_tool("read_invoice")])
+
+
+def test_private_upstream_helpers_still_resolve() -> None:
+ """Pins the private ``langchain_quickjs._ptc`` import the policy depends on.
+
+ A local copy of the identifier rule risks drifting looser than upstream, and
+ anything upstream rejects raises from inside ``wrap_model_call``. If this
+ fails after a version bump, re-check the helpers before loosening the policy.
+ """
+ from langchain_quickjs._ptc import is_valid_ptc_tool_name, to_camel_case
+
+ assert to_camel_case("read_file") == "readFile"
+ assert is_valid_ptc_tool_name("read_file")
+ assert not is_valid_ptc_tool_name("read file")
+
+
+# --------------------------------------------------------------------------
+# Sandbox behaviour, end to end through a real agent
+# --------------------------------------------------------------------------
+
+
+def test_computation_runs_in_the_sandbox(tmp_path: Path) -> None:
+ """The plain arithmetic case: one eval call, no tools, a value back."""
+ assert "320" in _run_js("10 * 32", tmp_path)
+
+
+def test_programmatic_tool_calling_collapses_round_trips(tmp_path: Path) -> None:
+ """Two bridged tool calls and the arithmetic between them, in one eval call."""
+ seen: list[str] = []
+
+ @tool
+ def lookup_price(sku: str) -> str:
+ """Look up the price of a SKU."""
+ seen.append(sku)
+ return {"A": "10", "B": "32"}[sku]
+
+ code = """
+ const [a, b] = await Promise.all([
+ tools.lookupPrice({ sku: "A" }),
+ tools.lookupPrice({ sku: "B" }),
+ ]);
+ Number(a) * Number(b)
+ """
+ assert "320" in _run_js(code, tmp_path, [lookup_price])
+ assert sorted(seen) == ["A", "B"]
+
+
+def test_workspace_files_are_reachable_through_the_file_tools(tmp_path: Path) -> None:
+ """JS writes and reads a workspace file via the bridged file tools.
+
+ This is what stands in for shell access: mediated by the tool, so the backend
+ still resolves and bounds the path.
+ """
+ code = """
+ await tools.writeFile({ file_path: "/note.txt", content: "hello" });
+ await tools.readFile({ file_path: "/note.txt" })
+ """
+ assert "hello" in _run_js(code, tmp_path)
+ assert (tmp_path / "note.txt").read_text(encoding="utf-8") == "hello"
+
+
+def test_path_traversal_is_still_rejected_through_the_bridge(tmp_path: Path) -> None:
+ """``virtual_mode`` bounds the path even from inside the REPL.
+
+ This is the property that makes bridged file tools an acceptable substitute
+ for shell access: the backend, not the sandbox, resolves every path. The tool
+ reports the refusal as a returned string, so the ``await`` resolves normally
+ and the model sees the error.
+ """
+ workspace = tmp_path / "workspace"
+ workspace.mkdir()
+ escape = tmp_path / "escaped.txt"
+ code = """
+ const r = await tools.writeFile({
+ file_path: "/../escaped.txt", content: "pwned",
+ });
+ JSON.stringify(r)
+ """
+ output = _run_js(code, workspace)
+ assert "Path traversal not allowed" in output
+ assert not escape.exists(), f"traversal escaped the workspace: {escape}"
+ assert list(workspace.rglob("*")) == [], "traversal wrote inside the workspace"
+
+
+def test_sandbox_has_no_ambient_capability(tmp_path: Path) -> None:
+ """No network, no module loader, no process: the guest starts with nothing."""
+ code = "[typeof fetch, typeof require, typeof process].join(',')"
+ assert "undefined,undefined,undefined" in _run_js(code, tmp_path)
diff --git a/tests/agent/advanced/test_conversational_advanced_agent_graph.py b/tests/agent/advanced/test_conversational_advanced_agent_graph.py
index cdb546d8e..706a105e0 100644
--- a/tests/agent/advanced/test_conversational_advanced_agent_graph.py
+++ b/tests/agent/advanced/test_conversational_advanced_agent_graph.py
@@ -1,5 +1,6 @@
"""Tests for the conversational advanced agent wrapper builder."""
+from collections.abc import Sequence
from typing import Any, cast
from unittest.mock import MagicMock, patch
@@ -77,6 +78,21 @@ def test_wrapper_graph_has_conversational_nodes() -> None:
} <= set(graph.nodes)
+def _runtime_prompt_middleware(
+ middleware: Sequence[Any],
+) -> _RuntimeSystemPromptMiddleware | None:
+ """The runtime-prompt middleware in the stack handed to deepagents, if any.
+
+ Located by type rather than by index: ``create_advanced_agent`` also prepends
+ ``TodoListMiddleware``, and callers may append middleware of their own.
+ """
+ found = [m for m in middleware if isinstance(m, _RuntimeSystemPromptMiddleware)]
+ assert len(found) <= 1, (
+ f"expected at most one runtime-prompt middleware, got {found}"
+ )
+ return found[0] if found else None
+
+
def test_callable_system_prompt_enables_runtime_middleware() -> None:
with patch(
"uipath_langchain.agent.advanced.agent._create_deep_agent",
@@ -92,9 +108,8 @@ def test_callable_system_prompt_enables_runtime_middleware() -> None:
call_kwargs = create_deep_agent.call_args.kwargs
assert call_kwargs["system_prompt"] is None
- assert len(call_kwargs["middleware"]) == 1
- middleware = call_kwargs["middleware"][0]
- assert isinstance(middleware, _RuntimeSystemPromptMiddleware)
+ middleware = _runtime_prompt_middleware(call_kwargs["middleware"])
+ assert middleware is not None
assert middleware.state_key == "uipath__system_prompt"
@@ -113,7 +128,7 @@ def test_static_system_prompt_skips_runtime_middleware() -> None:
call_kwargs = create_deep_agent.call_args.kwargs
assert call_kwargs["system_prompt"] == "sys"
- assert call_kwargs["middleware"] == []
+ assert _runtime_prompt_middleware(call_kwargs["middleware"]) is None
@pytest.mark.asyncio
@@ -259,7 +274,8 @@ async def test_runtime_prompt_reaches_deep_agent_model_request() -> None:
captured_requests: list[ModelRequest[Any]] = []
def create_inner_graph(**kwargs: Any) -> Any:
- middleware = kwargs["middleware"][0]
+ middleware = _runtime_prompt_middleware(kwargs["middleware"])
+ assert middleware is not None
def respond(state: BaseModel) -> dict[str, Any]:
state_data = state.model_dump()
diff --git a/tests/agent/advanced/test_create_advanced_agent_graph.py b/tests/agent/advanced/test_create_advanced_agent_graph.py
index 154b3fc66..c2e04dc2a 100644
--- a/tests/agent/advanced/test_create_advanced_agent_graph.py
+++ b/tests/agent/advanced/test_create_advanced_agent_graph.py
@@ -1,5 +1,6 @@
"""Tests for the create_advanced_agent_graph wrapper builder."""
+from collections.abc import Sequence
from typing import Any, cast
from unittest.mock import AsyncMock, MagicMock, patch
@@ -39,6 +40,21 @@ class _PromptNamedInput(BaseModel):
uipath__system_prompt_1: str
+def _runtime_prompt_middleware(
+ middleware: Sequence[Any],
+) -> _RuntimeSystemPromptMiddleware | None:
+ """The runtime-prompt middleware in the stack handed to deepagents, if any.
+
+ Located by type rather than by index: ``create_advanced_agent`` also prepends
+ ``TodoListMiddleware``, and callers may append middleware of their own.
+ """
+ found = [m for m in middleware if isinstance(m, _RuntimeSystemPromptMiddleware)]
+ assert len(found) <= 1, (
+ f"expected at most one runtime-prompt middleware, got {found}"
+ )
+ return found[0] if found else None
+
+
def _mock_model() -> MagicMock:
model = MagicMock(spec=BaseChatModel)
model.profile = None
@@ -76,9 +92,9 @@ def test_callable_system_prompt_enables_runtime_middleware() -> None:
call_kwargs = mock_create.call_args.kwargs
assert call_kwargs["system_prompt"] is None
- assert len(call_kwargs["middleware"]) == 1
- assert isinstance(call_kwargs["middleware"][0], _RuntimeSystemPromptMiddleware)
- assert call_kwargs["middleware"][0].state_key == "uipath__system_prompt"
+ runtime_middleware = _runtime_prompt_middleware(call_kwargs["middleware"])
+ assert runtime_middleware is not None
+ assert runtime_middleware.state_key == "uipath__system_prompt"
def test_static_system_prompt_skips_runtime_middleware() -> None:
@@ -91,7 +107,7 @@ def test_static_system_prompt_skips_runtime_middleware() -> None:
call_kwargs = mock_create.call_args.kwargs
assert call_kwargs["system_prompt"] == "sys"
- assert call_kwargs["middleware"] == []
+ assert _runtime_prompt_middleware(call_kwargs["middleware"]) is None
@pytest.mark.asyncio
@@ -182,7 +198,8 @@ def build_system_prompt(args: dict[str, Any]) -> str:
return f"runtime:{args['question']}"
def create_inner_graph(**kwargs: Any) -> Any:
- middleware = kwargs["middleware"][0]
+ middleware = _runtime_prompt_middleware(kwargs["middleware"])
+ assert middleware is not None
runtime_key = middleware.state_key
def capture_model_request(state: BaseModel) -> dict[str, Any]:
diff --git a/tests/agent/tools/test_suspends_run_metadata.py b/tests/agent/tools/test_suspends_run_metadata.py
new file mode 100644
index 000000000..ddf56ad4b
--- /dev/null
+++ b/tests/agent/tools/test_suspends_run_metadata.py
@@ -0,0 +1,70 @@
+"""Every tool factory that suspends the run must advertise it in tool metadata.
+
+A suspending tool raises ``GraphInterrupt`` instead of returning: the run
+checkpoints and the node is replayed from that checkpoint on resume. Callers that
+invoke tools outside the graph's tool node -- the QuickJS code interpreter's
+programmatic tool calling in particular -- must therefore not offer them, because
+a replayed node re-runs every call made before the interrupt, and because such
+bridges bypass approval hooks.
+
+Deciding eligibility from ``SUSPENDS_RUN`` keeps that policy next to the code that
+suspends, rather than in a central list that silently goes stale. This test is
+what makes the flag trustworthy: it reads the factory sources, so a new
+suspending factory that forgets to stamp it fails here instead of quietly
+becoming reachable from inside the sandbox.
+"""
+
+import ast
+from pathlib import Path
+
+import pytest
+
+from uipath_langchain._utils.durable_interrupt import SUSPENDS_RUN
+
+_TOOLS_DIR = Path(__file__).parents[3] / "src" / "uipath_langchain" / "agent" / "tools"
+
+# Suspends the run without the decorator: a bare ``interrupt()`` call.
+_BARE_INTERRUPT_MODULES = {"extraction_tool.py"}
+
+
+def _suspending_modules() -> list[Path]:
+ """Factory modules that suspend the run, found by reading the source."""
+ found = [
+ path
+ for path in sorted(_TOOLS_DIR.rglob("*.py"))
+ if "@durable_interrupt" in path.read_text(encoding="utf-8")
+ or path.name in _BARE_INTERRUPT_MODULES
+ ]
+ assert found, f"no suspending tool factories found under {_TOOLS_DIR}"
+ return found
+
+
+def _assigns_suspends_run(source: str) -> bool:
+ """Whether the module sets ``SUSPENDS_RUN`` as a dict key to a true constant.
+
+ Parsed rather than grepped so a mention in a comment or docstring does not
+ count as a stamp.
+ """
+ for node in ast.walk(ast.parse(source)):
+ if not isinstance(node, ast.Dict):
+ continue
+ for key, value in zip(node.keys, node.values, strict=False):
+ if (
+ isinstance(key, ast.Name)
+ and key.id == "SUSPENDS_RUN"
+ and isinstance(value, ast.Constant)
+ and value.value is True
+ ):
+ return True
+ return False
+
+
+@pytest.mark.parametrize("module", _suspending_modules(), ids=lambda p: p.name)
+def test_suspending_factory_stamps_the_flag(module: Path) -> None:
+ """A factory that suspends the run stamps ``SUSPENDS_RUN: True`` in metadata."""
+ assert _assigns_suspends_run(module.read_text(encoding="utf-8")), (
+ f"{module.name} suspends the run but does not set "
+ f"{SUSPENDS_RUN!r} in its tool metadata. Add "
+ f"`SUSPENDS_RUN: True` to the tool's metadata dict, or the tool becomes "
+ f"callable from the code interpreter's tools namespace."
+ )
diff --git a/uv.lock b/uv.lock
index 67bb724be..61815988d 100644
--- a/uv.lock
+++ b/uv.lock
@@ -9,7 +9,7 @@ resolution-markers = [
]
[options]
-exclude-newer = "2026-08-26T20:22:33.9044027Z"
+exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values.
exclude-newer-span = "P2D"
[options.exclude-newer-package]
@@ -223,7 +223,7 @@ wheels = [
[[package]]
name = "anthropic"
-version = "0.111.0"
+version = "0.125.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
@@ -235,9 +235,9 @@ dependencies = [
{ name = "sniffio" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/b9/8a/9afc7305a2ce4b52b30e137f83cd2a6a90b918b3997073db11bb5a1de55a/anthropic-0.111.0.tar.gz", hash = "sha256:39cbda0ac17a6d423e5bf609811bd69b26eddf6299d7a468126e05bc711ce826", size = 934001, upload-time = "2026-06-18T17:31:44.733Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/62/f8/6f0560884b5363848347bd640b6c1d04abc25e7aa61787a232f790c6b60a/anthropic-0.125.0.tar.gz", hash = "sha256:e0cdd336580cb7411c1cdab69f80973e9bf4bff7f8e08141811d46307d45c682", size = 1112593, upload-time = "2026-08-19T22:00:42.837Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/f1/bb/09e82a81885d787f350fb55ca9df865b63140dd28b3b5b3104c4ae261657/anthropic-0.111.0-py3-none-any.whl", hash = "sha256:c14edb36ed80da9099acbd26b5cec810d76606c31f32a0d56a4cf9d4fa9e25ae", size = 929774, upload-time = "2026-06-18T17:31:43.116Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/1a/b1bd30cda3790557e8791bec5922a6ec8fabb6fa8b008c76a39cf7be6152/anthropic-0.125.0-py3-none-any.whl", hash = "sha256:3486013602eca76d8b12540764e53654f02cf4951110bca86cf06e67428a9f21", size = 1184067, upload-time = "2026-08-19T22:00:44.596Z" },
]
[package.optional-dependencies]
@@ -550,11 +550,61 @@ wheels = [
[[package]]
name = "bracex"
-version = "2.6"
+version = "3.0.1"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/63/9a/fec38644694abfaaeca2798b58e276a8e61de49e2e37494ace423395febc/bracex-2.6.tar.gz", hash = "sha256:98f1347cd77e22ee8d967a30ad4e310b233f7754dbf31ff3fceb76145ba47dc7", size = 26642, upload-time = "2025-06-22T19:12:31.254Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/ac/01/5f394b8bcd6e5b92f73130990960423bbb19711f906bd9fe9ea5557c667c/bracex-3.0.1.tar.gz", hash = "sha256:4e38e32392e4a4780fe15d644bfc7c8514057cfc3861e060b11814ce829c25e4", size = 44019, upload-time = "2026-07-20T13:43:00.335Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/9d/2a/9186535ce58db529927f6cf5990a849aa9e052eea3e2cfefe20b9e1802da/bracex-2.6-py3-none-any.whl", hash = "sha256:0b0049264e7340b3ec782b5cb99beb325f36c3782a32e36e876452fd49a09952", size = 11508, upload-time = "2025-06-22T19:12:29.781Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/8f/6f7273a7adb8d73fc8d21ede4376a3e475e52f98435c6007f69100dec8ca/bracex-3.0.1-py3-none-any.whl", hash = "sha256:6523ad83aeb5098a4ee597cff0f964442ff74e460bd3fafaffab6a013ff2288c", size = 11940, upload-time = "2026-07-20T13:42:59.268Z" },
+]
+
+[[package]]
+name = "bsdiff4"
+version = "1.2.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/53/b9/4559ede9a4c8c4451688303544da84654643fdc7f28790aca85be80b4b7c/bsdiff4-1.2.6.tar.gz", hash = "sha256:2ab57d01a78b39e29e5accc9cfead4130982ded9dccbc4261bd0e9c51d6b751d", size = 13259, upload-time = "2025-02-19T17:42:33.612Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/10/08/6472d5c2527688b16ad2c2dd09e324281f5e78eea5e4dba5f65a7949f39c/bsdiff4-1.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b151c28098b3c522b1735cdfe5e84e8f164f0ef4a592adb227d7a10727034673", size = 16212, upload-time = "2025-02-19T17:39:53.399Z" },
+ { url = "https://files.pythonhosted.org/packages/33/41/4d1fa5980c01faa0d5c578e41ce73b4df98cd74e33f92323880df0da035e/bsdiff4-1.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:29def064f6bcd13d0d7a82e5caa4848158b7f49c3a8fe44fbef3031456fb7dd2", size = 16031, upload-time = "2025-02-19T17:39:54.481Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/41/188f858a71eb529145b6706f8ac618fd9f719807f46e0cebe2ea482bfe78/bsdiff4-1.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6f4cf8e00116e14e9e6c3fb5747478022a27215a9a65ed223fed82d2cfbc4d3", size = 33763, upload-time = "2025-02-19T17:39:55.438Z" },
+ { url = "https://files.pythonhosted.org/packages/27/ea/84cc364a0c0f6eb3e503bf1625aa62eb411aa7474d1c91ec201812295fcb/bsdiff4-1.2.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:897a260d30acc4df9803f500682eb7951fdc104a3e155787e1e581258f38df50", size = 35705, upload-time = "2025-02-19T17:39:56.601Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/54/c235fd3e95aa3a4ac53de83605723a149a33eb11aff64e49488132b857f8/bsdiff4-1.2.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d994ee6113c3f030bb9f373e917f00db13c026c295fe9f314f23171935d88371", size = 33807, upload-time = "2025-02-19T17:39:57.601Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/8b/010d14d3ab321c1c35fc4145b020c1e76ed8a29a214ce6bcc093ddedee13/bsdiff4-1.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba5028a2aaa8e4cacb224031af9140e05d9c407ba15b59471380badcc4845777", size = 33250, upload-time = "2025-02-19T17:39:58.552Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/91/ae41950f7b823e8061520f3b28d47534b47f314b4148690c4a002d764bd7/bsdiff4-1.2.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1edd3069dc14cecaa804faaae776a5d14f85217c41b3180b794e5fbf684d35dd", size = 35919, upload-time = "2025-02-19T17:40:00.052Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/b4/f29c451e7718d4366a72f9a87a7f3cc76cb56cb5e9305eae087eab83f7a0/bsdiff4-1.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0fb562e451d5b3a7523c67ce04fe541d3a004914e5760a47116883972f5ff8bc", size = 33740, upload-time = "2025-02-19T17:40:01.893Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/73/004b3c4511df3df0d5e591ecd7aaf92c851b22be200283428d3577f4400b/bsdiff4-1.2.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b7309380d8edbd3d46c4ed3930f7062b793bac8f004b32139db7af7c4612e241", size = 37325, upload-time = "2025-02-19T17:40:02.911Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/ea/5fa1d331c4a2e73e4e90a851768749a9960cefcb443da3abaae69e891f06/bsdiff4-1.2.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f2f7504f08181227717fee04f25169d5901322c29d3fd054e4cb61bd60b3ffb4", size = 35791, upload-time = "2025-02-19T17:40:03.866Z" },
+ { url = "https://files.pythonhosted.org/packages/10/04/7616e8abec54562c86742c7bacaaba53c0c4733565ea00e8c5ffe2c5c9ce/bsdiff4-1.2.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:6ad599216e7ee3db5737951d06c43b8e65d5b0db5c42300e85f18d399ec0bc5e", size = 35413, upload-time = "2025-02-19T17:40:05.692Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/d6/3fff18a97e127cc783e02de3c934bca63fabc0d4a379e091973b006cbae6/bsdiff4-1.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cd133a9475c9dfba6243dd07f118ee58a0b7f136c00d316e2d92d3f82169bd9e", size = 32939, upload-time = "2025-02-19T17:40:06.639Z" },
+ { url = "https://files.pythonhosted.org/packages/36/32/2943637e17eca717cdd091625d4198cf7a49dd7d235944a86f1a8a6134fe/bsdiff4-1.2.6-cp311-cp311-win32.whl", hash = "sha256:403e8cc003451a8c4672c345a50aee3cf89d20983701e38fbbb67e07cb808c57", size = 18257, upload-time = "2025-02-19T17:40:07.59Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/f8/83f087ab62bebde26956f084ab272e19d11db5df6700f4f48d29647235fd/bsdiff4-1.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:164a059e1e07932f91d90471a4ef4dac749f2dee780f08501522805398b32ed8", size = 19530, upload-time = "2025-02-19T17:40:08.504Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/58/044dd110fb0a0160f5cacecbfb9904043c8179f8c14093e22b6d8c6b9391/bsdiff4-1.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:69c5052e94ad991c397b5a46f8eab42f2e256c42aa5677896b7a3ea9e3d06adc", size = 16267, upload-time = "2025-02-19T17:40:10.376Z" },
+ { url = "https://files.pythonhosted.org/packages/37/a1/70b74154344486bac9bf438ec309ae502f07df8cd7ca713d58f658769ff4/bsdiff4-1.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:223ae0fc9f386dcf919a09a2029c391a0f0afaf4a5892b9a6e1b622bf42e1ae5", size = 16090, upload-time = "2025-02-19T17:40:11.334Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/90/36531261d8a150fcb8193fe2ad46d939b8a91549976424852f6a2a335689/bsdiff4-1.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48ea2298a281068d82b78454ee58ac7306ed38c9af55afddb04cf796df932d63", size = 33675, upload-time = "2025-02-19T17:40:13.218Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/97/8b73b3684c63e88508ad308229f33a8a5be6c4762e4160f96e2a6fc46906/bsdiff4-1.2.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2534e286ef5ae58767b9b17be64742424ca1e52ec748b0d8f8e24eecd12bc28a", size = 35648, upload-time = "2025-02-19T17:40:14.296Z" },
+ { url = "https://files.pythonhosted.org/packages/52/39/0b1dd6494c743fa2c62bd7c35f5dec9f5802d01c1da1ef75a2e20a481ed4/bsdiff4-1.2.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ff079b0f4cf874af4b6816983557b6b9d45996f88736046653e2d2311fa1876", size = 33772, upload-time = "2025-02-19T17:40:15.341Z" },
+ { url = "https://files.pythonhosted.org/packages/88/23/98fc7482f957602c611203a9e485b9dbf4caf9d918e92453e3729cf5f0b4/bsdiff4-1.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56c2728c96d1d4eb8e089e4797c018a56be3f905f440fb507773f44c567fcd38", size = 33238, upload-time = "2025-02-19T17:40:16.343Z" },
+ { url = "https://files.pythonhosted.org/packages/75/04/c3db957b7a324a3f25f721a82c288e9abe60059a0a2d2f9b3c19fb49cdb2/bsdiff4-1.2.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9deb9b3cdb4d327e43b8c7bd11ed3707587f1183b35fb8a4c06c4f34bce62c6a", size = 35889, upload-time = "2025-02-19T17:40:18.237Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/a8/73d2abfd98a33cd74a0fc491e527d734c222ae18b499a10689f3adbc8d5c/bsdiff4-1.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e87c67b06ac96af6171b774dc8c03d2bde70c67c6488078eff44e0af4864acf6", size = 33606, upload-time = "2025-02-19T17:40:21.398Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/c3/713b3bb3711b62e51f6f67d6d9f63098e4d3a51d8b91e52c962f5c01a2b7/bsdiff4-1.2.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:04bb2948301ad48123d308bf2342c83cae81d7edb52d11bdde00266d89ca071e", size = 37211, upload-time = "2025-02-19T17:40:22.365Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/c5/40559695ea0bd3332c37ef8182fc0f96ceed838ae6b03ca9ddcd8cf0f7df/bsdiff4-1.2.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:43649a44fc21f017be902e19ccf7fb8bac6ef2d7f93d871bbc6bc49acec9ffee", size = 35750, upload-time = "2025-02-19T17:40:23.554Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/9b/eb4683896119ec9d26d1eb3f12efc0d8a902451f4025db12c21c5a82992a/bsdiff4-1.2.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:baa76ec557dc48847c3ed1ff5720b5095c439c868f7568da30dcabbabceb2b92", size = 35364, upload-time = "2025-02-19T17:40:24.485Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/ad/0968b67aecf00873e0e5c07e97ba2300594505d4dbce62702b9f56a62d66/bsdiff4-1.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:701168e2931da777e6e72ae17f22eb519e9ce25ec5108d149c9da7b3b80e1184", size = 32831, upload-time = "2025-02-19T17:40:25.571Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/18/adfcf72780f19cea1fe9948cbfb49890599424e94c752bf7d614093c0fc5/bsdiff4-1.2.6-cp312-cp312-win32.whl", hash = "sha256:f9f2e5e716d35af3252f69a15afc2b166970c98596a1114af4c6d2834fe8e871", size = 18308, upload-time = "2025-02-19T17:40:26.571Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/5d/31672172bb4566c1f1187fa28a1437125d4b5106bc55f9f7b9a75371094c/bsdiff4-1.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:0b29568d1e33e32ea075c12a696b32e4d6cea344d0270a2292075254efd86014", size = 19553, upload-time = "2025-02-19T17:40:27.592Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/56/887d90b0e52ce7b5533a6f1390ab9a68215a70ba34848441730e215ffc1c/bsdiff4-1.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a98d7975a670fc360d894ef2ec00294e6b7b19790c58457e40c8a5d57a1865b0", size = 16260, upload-time = "2025-02-19T17:40:28.614Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/4c/825a16932605d305501ed144ae5567a3dc90c9164a393c61cc0ed68df3f0/bsdiff4-1.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ee4417341712a4bf736694ce9ad3902b8c6fbd3425aadca44df9b66a51bbefa4", size = 16080, upload-time = "2025-02-19T17:40:29.612Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/e2/0cf538a786f47b08e26f3970a6f98c2b7b9d555c01e085425282944a2c7f/bsdiff4-1.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39ddfa2137de44c9743a611d71d263d0cc8c45e5b18ee84ca5ff6b6240be1740", size = 33664, upload-time = "2025-02-19T17:40:31.646Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/c0/44ac255f1d16865e39ef941470e30bb5c362dd216b62837bb13880d1dd36/bsdiff4-1.2.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6474d8f34f89d25fa1803c639cc8ed49121752a56a15b4cd21e9267154cdaf70", size = 35648, upload-time = "2025-02-19T17:40:32.793Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/6b/d5871af38cbb8527652b65463c3dd736b6250828d8d6daf48be712a2ebfe/bsdiff4-1.2.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f8e9c876929c03ef5d448e2626e8b2961040c3a9f0dd3d483643dbccd0e7ff7a", size = 33792, upload-time = "2025-02-19T17:40:35.498Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/1e/7027849a6dc02b580e352b1528899053bd919029b185fbaa14c6f268180b/bsdiff4-1.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46313f0eb8f63efb54a3c4219cd7b5b8a7795012b535f9d0838fe3f2b3349849", size = 33238, upload-time = "2025-02-19T17:40:37.165Z" },
+ { url = "https://files.pythonhosted.org/packages/97/df/c4a3e2bb1c1f9f09c2c5f8a9025c67f5ec7fcc8949338e54cb2d4fba9009/bsdiff4-1.2.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6b5757b1a83829f00ef34953c6865ea82e9c71126e465bc32d029c55da9e45b", size = 35866, upload-time = "2025-02-19T17:40:38.789Z" },
+ { url = "https://files.pythonhosted.org/packages/83/03/76a5aaaa0ccc282b239b3f148f6dd6033d37f79c1d1a89846b712224d132/bsdiff4-1.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:734552992ecc86749a8ef55d03f999f9a47576cc609d7d4d9a7aec274b43ee4d", size = 33688, upload-time = "2025-02-19T17:40:39.762Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/b3/b240d4840a16d923c60e8e9eacf0777cf9378e30610037f6c85324daea85/bsdiff4-1.2.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:853c3221daac6f8d347f12eb0b73ca9dbb7db483e7b5f40b1e2fbb05730645a7", size = 37273, upload-time = "2025-02-19T17:40:41.626Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/84/2223a09c4950a3e419ce94eb0af6d90c1ee562b9962ef2d72515f4ad6271/bsdiff4-1.2.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:94526dc11e56f330c2f4b1e2e9389b958a7891f6c86b5aac83bd9c7a90eb088a", size = 35844, upload-time = "2025-02-19T17:40:42.646Z" },
+ { url = "https://files.pythonhosted.org/packages/18/7b/c02f703b449feb20b245eb803e7d446508b80d5b4065d1eb9cc75d02ae3b/bsdiff4-1.2.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f5474e1d9253564ed0823e2685a403d9dfdbba3c7b70a80f5066d61427848253", size = 35418, upload-time = "2025-02-19T17:40:44.562Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/52/623ee28011b6935f0dfe67397ec27c2a900b9f0bda1b1ec2a5b174c53fb7/bsdiff4-1.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5529731ac88151345a8bb76dad4fdb218af10a8a505161d1aa3d669e49cb7b77", size = 32892, upload-time = "2025-02-19T17:40:45.552Z" },
+ { url = "https://files.pythonhosted.org/packages/44/6c/e740e347bb46ea08ceacf39df56c2ffd2bd20b95d458409ea303fbf2b946/bsdiff4-1.2.6-cp313-cp313-win32.whl", hash = "sha256:c8089827c41b37f7c9192492742289929097c5ab2a6b3a120919fee27fbc01b8", size = 18304, upload-time = "2025-02-19T17:40:47.37Z" },
+ { url = "https://files.pythonhosted.org/packages/88/d1/9be6f6124afab9837db1ffc5801ca1aa86f2077d4224ff729e88fabada71/bsdiff4-1.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:37ff935ba714e0726584dad2bc4c063218b588b110115e8554ebc438ee7bccf3", size = 19543, upload-time = "2025-02-19T17:40:48.369Z" },
]
[[package]]
@@ -955,7 +1005,7 @@ wheels = [
[[package]]
name = "deepagents"
-version = "0.5.9"
+version = "0.7.11"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "langchain" },
@@ -963,11 +1013,12 @@ dependencies = [
{ name = "langchain-core" },
{ name = "langchain-google-genai" },
{ name = "langsmith" },
+ { name = "packaging" },
{ name = "wcmatch" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/06/74/776e606f0508a4d7d4c9d061c797dcde43eed2452fea546ae29047aeaaa6/deepagents-0.5.9.tar.gz", hash = "sha256:74fe0f998641b20bda8adac662a018051c623a3c8e5ed4b6ff9ad53fc493a783", size = 165651, upload-time = "2026-05-10T22:31:17.095Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/cc/e8/dfa6b3a4d43ad87ab28894a6edba167d909fbdd8d59fb356c8db0ebb53c9/deepagents-0.7.11.tar.gz", hash = "sha256:240e31cb60e4a8e3c4f9aa1ab706618bae4ea69ae6495d9fc0e3e773d551927e", size = 293354, upload-time = "2026-08-28T23:10:48.665Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/e8/f6/9698f98da72dfa8dc8e1d0f0542f2407a40a50cfdccf522fdb5cb43e39e2/deepagents-0.5.9-py3-none-any.whl", hash = "sha256:ce24a41763b2793bd21217411e9fb9f187a9128da6789f5a81654da1de9e4c7c", size = 188118, upload-time = "2026-05-10T22:31:15.974Z" },
+ { url = "https://files.pythonhosted.org/packages/39/38/7d3eb8671cafefe8798d945c48bb664abbd92015446fc453093e44895788/deepagents-0.7.11-py3-none-any.whl", hash = "sha256:7f8ca58b7aa8b11fb8a18d054aa5da937700cba221b1104b5eda4469e9531382", size = 320468, upload-time = "2026-08-28T23:10:47.569Z" },
]
[[package]]
@@ -1866,30 +1917,30 @@ wheels = [
[[package]]
name = "langchain"
-version = "1.3.10"
+version = "1.3.18"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "langchain-core" },
{ name = "langgraph" },
{ name = "pydantic" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/3b/f6/e351d85c7828b9b90c5729de66170457c882c754efef0712904cfcd3192d/langchain-1.3.10.tar.gz", hash = "sha256:fd6ac9da86c479e4ff376e772d9e17a9232bd3113e9f2ddcb70cdc4bf7afc119", size = 632522, upload-time = "2026-06-18T19:43:00.86Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/31/e9/0e5425e522b624d7c5c0911957b467b82890b36b0099bf727951f2ee3059/langchain-1.3.18.tar.gz", hash = "sha256:74ce99294f6f2c82ee64c3df39daa6a22085ee1449a718065b3604a88d78bf4d", size = 637052, upload-time = "2026-08-27T17:33:12.702Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/59/f6/a682e68d004a2e23cae6c5c42e3c0d071bc0e7768167bd12277992f096f9/langchain-1.3.10-py3-none-any.whl", hash = "sha256:5da67f21aa56119744ad51b3e46ffac570c88f4fae0876e3b1c6a1c4bc0e344e", size = 133038, upload-time = "2026-06-18T19:42:58.918Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/04/374f6014ed6959dbdab92962c2b09e4d0223ed6a82f65694870b46d2c13f/langchain-1.3.18-py3-none-any.whl", hash = "sha256:f29cbef985848e5cfff5398f3c8c1568994a3a6f2a8f4fa720df30b6e0668b9c", size = 148007, upload-time = "2026-08-27T17:33:11.23Z" },
]
[[package]]
name = "langchain-anthropic"
-version = "1.4.6"
+version = "1.7.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anthropic" },
{ name = "langchain-core" },
{ name = "pydantic" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/e1/f5/cd397b94aeed5fa0e8ab9595b9fb578ac99f424d42220defe6626e6a1a7b/langchain_anthropic-1.4.6.tar.gz", hash = "sha256:78942d4458d883b7d362438a095ed501ed84f44d402622404482481fc973b9da", size = 706540, upload-time = "2026-06-12T16:54:15.352Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/56/fc/52f6d1d6069bafb08626e204c89c49c8dd4a536eedbb94f0b7e78668594d/langchain_anthropic-1.7.0.tar.gz", hash = "sha256:d48e3c118ff8d3eea83f17b50234a2d2ff491a2375d565f212eb990e7e3856cb", size = 750068, upload-time = "2026-08-27T15:23:59.261Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/26/af/927dbbc5a1f5fea1a69adc2883f034cbd1430004e36f4eacd302d500393a/langchain_anthropic-1.4.6-py3-none-any.whl", hash = "sha256:dbd412a956b6b8b0716d9d8460ef71f834a6731cdbfc59e6160482a4a9fb5200", size = 51797, upload-time = "2026-06-12T16:54:14.159Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/ce/e4367713080bc750e1dba845409c058f196543d1a0dc99683e2ef062f581/langchain_anthropic-1.7.0-py3-none-any.whl", hash = "sha256:68b34369aa01dad0c67bc690b8c47e09d06bd30fb28f320ad19ddc71c4445dc0", size = 60475, upload-time = "2026-08-27T15:23:57.989Z" },
]
[[package]]
@@ -1938,9 +1989,10 @@ wheels = [
[[package]]
name = "langchain-core"
-version = "1.4.8"
+version = "1.6.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
+ { name = "httpx" },
{ name = "jsonpatch" },
{ name = "langchain-protocol" },
{ name = "langsmith" },
@@ -1951,9 +2003,9 @@ dependencies = [
{ name = "typing-extensions" },
{ name = "uuid-utils" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/12/e3/bea6d0080acf183332f24dcd74c208aee5857cf8f783c3fb0bd86027d8fb/langchain_core-1.4.8.tar.gz", hash = "sha256:5bf1f8411077c904182ad8f975943d36adcbf579c4e017b3a118b719229ebf9a", size = 957974, upload-time = "2026-06-18T19:39:23.636Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/90/12/aff76ca89c219ebe6f9dd3c5dbc4e3b1cf5450e9fc7037dccad23d45cd7a/langchain_core-1.6.1.tar.gz", hash = "sha256:1b156cb395aac4f009a8a1b38a574c7d948fe2d5f74c96e0d8a5017b4149e04f", size = 1003359, upload-time = "2026-08-27T19:31:14.956Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/13/d6/bdf6f0481cc57ef300d6b1eb48cf1400c0409be715d6eb3cabadd1142a09/langchain_core-1.4.8-py3-none-any.whl", hash = "sha256:d84c28b05e3ba8d4271d0827aad5b592ccdaaf986e76768c23503f0a2045e8aa", size = 557416, upload-time = "2026-06-18T19:39:21.902Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/25/f50dd65673c819aa33d3c34df58c115dbb6ec627d19f93e6e401dd0fc8d7/langchain_core-1.6.1-py3-none-any.whl", hash = "sha256:954a84132a5cb0435d27b910e336347b6744ecc18fbeef1e2de7029a0959841a", size = 571478, upload-time = "2026-08-27T19:31:13.34Z" },
]
[[package]]
@@ -1974,7 +2026,7 @@ wheels = [
[[package]]
name = "langchain-google-genai"
-version = "4.2.5"
+version = "4.3.7"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "filetype" },
@@ -1982,9 +2034,9 @@ dependencies = [
{ name = "langchain-core" },
{ name = "pydantic" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/5e/4b/a1acdba3a86f861d379cb654f234d334c04a4c93178c8c7b0182ddeb9966/langchain_google_genai-4.2.5.tar.gz", hash = "sha256:2abab4be22699a9cc29948b2bf012946f51a0bbf10ab3a4a9a129047234829f8", size = 271850, upload-time = "2026-06-10T01:48:57.06Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/5f/17/dcf759ad83ab3566db1b02c8b21211f43085c66f3b5964f6fe573ffe8664/langchain_google_genai-4.3.7.tar.gz", hash = "sha256:8a57f936b1fbde52776fdde6673fdabd99cfa5cbbb122a926f1cff0ba00f6bc6", size = 373696, upload-time = "2026-08-27T20:44:05.203Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/6a/82/3d4d3dc181ea1756f323dad4d5936239c2f404ea0acb5102316224280634/langchain_google_genai-4.2.5-py3-none-any.whl", hash = "sha256:289699ddb8e1076a76144f83e25e0086e4ce629b196fc103251f2a629e0756e5", size = 69404, upload-time = "2026-06-10T01:48:56.09Z" },
+ { url = "https://files.pythonhosted.org/packages/47/2f/ade5d1a7ecfbc88f7de65c6610ef89d80905b29fa3dd28a764f7028f3d42/langchain_google_genai-4.3.7-py3-none-any.whl", hash = "sha256:8d4b1aa8f2c2e17b8e790d34a7e9a7b8e7d13e9a00175679d17647c235104bf9", size = 79611, upload-time = "2026-08-27T20:44:03.884Z" },
]
[[package]]
@@ -2064,9 +2116,26 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/99/2e/d82db9eec13ad0f72e7aaad5c4bc730ab111934fdc83c85523206eb9b0a0/langchain_protocol-0.0.18-py3-none-any.whl", hash = "sha256:70b53a86fbf9cedc863555effe44da192ab02d556ddbf2cf95b8873adcf41b5a", size = 7221, upload-time = "2026-06-18T17:08:25.996Z" },
]
+[[package]]
+name = "langchain-quickjs"
+version = "0.3.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "bsdiff4" },
+ { name = "deepagents" },
+ { name = "langchain" },
+ { name = "langchain-core" },
+ { name = "langgraph" },
+ { name = "quickjs-rs" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/cf/2a/45e376a9958c61537973458ad62b39c61a3f9e22cdf3598ea0b7b775a155/langchain_quickjs-0.3.5.tar.gz", hash = "sha256:4b4da850c71bb755af21a5cef46d599b0c0ad2dc78170a8399996ac32fc96435", size = 225157, upload-time = "2026-07-29T18:26:32.78Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2e/a1/bf460ae2009f76ec2fabd263ad202f6649c6ae6cb7429f0688c4239ff759/langchain_quickjs-0.3.5-py3-none-any.whl", hash = "sha256:288b276ea7dcc3cfac2b84b7fed1079ac076e1c682bc3e4e2c2ce31d20ea2d2c", size = 45509, upload-time = "2026-07-29T18:26:31.268Z" },
+]
+
[[package]]
name = "langgraph"
-version = "1.2.6"
+version = "1.2.11"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "langchain-core" },
@@ -2076,9 +2145,9 @@ dependencies = [
{ name = "pydantic" },
{ name = "xxhash" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/02/7a/ea09b05bb0cbddfa43bd34fc581357e87fc3f21a751cc0d419688c3106da/langgraph-1.2.6.tar.gz", hash = "sha256:f9b45a34f13930c94d96cdb76277447ad2cc70ec2d18cd2764d7fdadb36cdc1b", size = 714400, upload-time = "2026-06-18T20:58:21.514Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/56/0d/c8e7ee98896659e1b6555db0ab115a9ca899844744645d5d894032bab1d7/langgraph-1.2.11.tar.gz", hash = "sha256:9ecfe11e50d338b34b15cf4d8a442642de103e8ae6971320efba84e4542eb363", size = 725753, upload-time = "2026-08-11T14:00:36.945Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/89/32/772db1b00a9fe42f50320d1aa20caefb76e621eff1f7218b9918093d631d/langgraph-1.2.6-py3-none-any.whl", hash = "sha256:1cf94d3ca124f84f77ce408fa1b06c3dee680a8aafffe364a8fd5d7d03eb8695", size = 246132, upload-time = "2026-06-18T20:58:20.335Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/7f/c5c30e4be99ff821029c7ac872a480676bb179c9f3df85ea3f38d13f86d4/langgraph-1.2.11-py3-none-any.whl", hash = "sha256:8bab70de7b2d00b5300fb289bcf38d8b241400f3184c1e95e8ce706fb0e8686b", size = 248854, upload-time = "2026-08-11T14:00:35.494Z" },
]
[[package]]
@@ -2139,23 +2208,27 @@ wheels = [
[[package]]
name = "langsmith"
-version = "0.8.18"
+version = "0.11.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
+ { name = "anyio" },
+ { name = "distro" },
{ name = "httpx" },
{ name = "orjson", marker = "platform_python_implementation != 'PyPy'" },
{ name = "packaging" },
{ name = "pydantic" },
{ name = "requests" },
{ name = "requests-toolbelt" },
+ { name = "sniffio" },
+ { name = "typing-extensions" },
{ name = "uuid-utils" },
{ name = "websockets" },
{ name = "xxhash" },
{ name = "zstandard" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/9a/d9/a6681aa9847bbbc5ec21abe20a5e233b94e5edcfe39624db607ac7e8ccb4/langsmith-0.8.18.tar.gz", hash = "sha256:32dde9c0e67e053e0fb738921fc8ced768af7b8fa83d7a0e3fd63597cf8776dd", size = 4526988, upload-time = "2026-06-19T13:12:17.123Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/c9/0a/1acb2a3ffbccbe8f8dc358778967c9d2979e8a59b67ceba6eb54474324ab/langsmith-0.11.2.tar.gz", hash = "sha256:927694c939c9fb44187e0126cf718413c45ffce2324d480438e70eb0526e1380", size = 4841592, upload-time = "2026-08-27T22:31:34.004Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/03/70/0e0cc80a3b064c8d6c8d697c3125ed86e39d5a7393ec6dc8b07cb1cf13c4/langsmith-0.8.18-py3-none-any.whl", hash = "sha256:3940183349993faef48e6c7d08e4822ee9cefd906b362d0e3c2d650314d2f282", size = 508108, upload-time = "2026-06-19T13:12:15.348Z" },
+ { url = "https://files.pythonhosted.org/packages/00/d9/cec0c1d24dda8c67a23ad204d0167e50aee202460e9388488661d94513f5/langsmith-0.11.2-py3-none-any.whl", hash = "sha256:75258142d27dffcc5df331479704b23fc3fd812cfca0469119bb9055a842882f", size = 753928, upload-time = "2026-08-27T22:31:31.644Z" },
]
[[package]]
@@ -3817,6 +3890,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
]
+[[package]]
+name = "quickjs-rs"
+version = "0.2.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "wasmtime" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/7f/dc/177301106aede96a709d5577127aaa090364d4abb8b4c4cfb87b12ae2c30/quickjs_rs-0.2.5.tar.gz", hash = "sha256:3ceb30fba27013108fac92f0a716d6a16980131ce5b13f9912848df1f1f2cf09", size = 830147, upload-time = "2026-07-24T20:29:26.377Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d0/1d/e4406d13ce9b9443dbfa59e2a2d5b3e11278ebe322b54de38ae18faf5436/quickjs_rs-0.2.5-py3-none-any.whl", hash = "sha256:e82240af1f1dd1b2e12bcf169a22a8e0e451e356f0688f2fc3bba886d9b2bb20", size = 801138, upload-time = "2026-07-24T20:29:24.194Z" },
+]
+
[[package]]
name = "rdflib"
version = "7.6.0"
@@ -4546,7 +4631,7 @@ wheels = [
[[package]]
name = "uipath-langchain"
-version = "0.16.13"
+version = "0.16.14"
source = { editable = "." }
dependencies = [
{ name = "a2a-sdk" },
@@ -4575,6 +4660,7 @@ dependencies = [
[package.optional-dependencies]
all = [
+ { name = "langchain-quickjs" },
{ name = "uipath-langchain-client", extra = ["all"] },
]
anthropic = [
@@ -4584,6 +4670,9 @@ bedrock = [
{ name = "boto3-stubs" },
{ name = "uipath-langchain-client", extra = ["bedrock"] },
]
+code-interpreter = [
+ { name = "langchain-quickjs" },
+]
fireworks = [
{ name = "uipath-langchain-client", extra = ["fireworks"] },
]
@@ -4612,14 +4701,16 @@ dev = [
requires-dist = [
{ name = "a2a-sdk", specifier = ">=1.1.2,<2.0.0" },
{ name = "boto3-stubs", marker = "extra == 'bedrock'", specifier = ">=1.41.4" },
- { name = "deepagents", specifier = ">=0.5.9,<0.6.0" },
+ { name = "deepagents", specifier = ">=0.7.11,<0.8.0" },
{ name = "httpx", specifier = ">=0.27.0" },
{ name = "jsonpath-ng", specifier = ">=1.7.0" },
{ name = "jsonschema-pydantic-converter", specifier = ">=0.4.0" },
- { name = "langchain", specifier = ">=1.2.15,<2.0.0" },
- { name = "langchain-core", specifier = ">=1.2.27,<2.0.0" },
+ { name = "langchain", specifier = ">=1.3.18,<2.0.0" },
+ { name = "langchain-core", specifier = ">=1.6.1,<2.0.0" },
{ name = "langchain-mcp-adapters", specifier = "==0.2.1" },
- { name = "langgraph", specifier = ">=1.1.8,<2.0.0" },
+ { name = "langchain-quickjs", marker = "extra == 'all'", specifier = ">=0.3.5,<0.4.0" },
+ { name = "langchain-quickjs", marker = "extra == 'code-interpreter'", specifier = ">=0.3.5,<0.4.0" },
+ { name = "langgraph", specifier = ">=1.2.11,<2.0.0" },
{ name = "langgraph-checkpoint-sqlite", specifier = ">=3.0.3,<4.0.0" },
{ name = "mcp", specifier = "==1.26.0" },
{ name = "openinference-instrumentation-langchain", specifier = ">=0.1.69,<0.2.0" },
@@ -4640,7 +4731,7 @@ requires-dist = [
{ name = "uipath-platform", specifier = ">=0.2.22,<0.3.0" },
{ name = "uipath-runtime", specifier = ">=0.13.0,<0.14.0" },
]
-provides-extras = ["anthropic", "vertex", "bedrock", "fireworks", "all"]
+provides-extras = ["anthropic", "vertex", "bedrock", "fireworks", "code-interpreter", "all"]
[package.metadata.requires-dev]
dev = [
@@ -4897,16 +4988,35 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/02/3623e6169bed617ed1e2d372f7c69f92ec28d54c4dfc997055c8578ec148/virtualenv-21.5.1-py3-none-any.whl", hash = "sha256:55aa670b67bbfb991b03fda39bd3276d92c419d702376e98c5df1c9989a26783", size = 4558820, upload-time = "2026-06-16T16:23:56.963Z" },
]
+[[package]]
+name = "wasmtime"
+version = "48.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/42/1f/03a286dc84d83cc3274d5599543558442ba9332b676e6408ee9e1c171199/wasmtime-48.0.0.tar.gz", hash = "sha256:dba27d59209fac703e7d5753af78c2af2c1cd1ed735f520ec76dc31c60a05815", size = 128804, upload-time = "2026-08-20T19:31:57.29Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ad/e6/39da2f4047a281bce7d4651e21785942d630033931eb397cf734e7ccc048/wasmtime-48.0.0-py3-none-android_26_arm64_v8a.whl", hash = "sha256:a55abf132fe238b843a963c68cd1a30d8f686c1bc75d8fbf042d8b7a1d51ee36", size = 8800816, upload-time = "2026-08-20T19:31:28.761Z" },
+ { url = "https://files.pythonhosted.org/packages/be/d0/f4f107166a65ddf8a6d8cf74f0cea33c06a08c74fe686f8e83b194e57e8b/wasmtime-48.0.0-py3-none-android_26_x86_64.whl", hash = "sha256:d8e94276ff6c0c5ce73ee16ccbacb00b3512a4b3a664749380705d81ee06a23c", size = 9731711, upload-time = "2026-08-20T19:31:31.905Z" },
+ { url = "https://files.pythonhosted.org/packages/95/15/20fad0cb2b9cff130c225bf827e16365e02883f504297eccf172c6bb7228/wasmtime-48.0.0-py3-none-any.whl", hash = "sha256:49c9ee43e9cf59ad7453ac65dce0cc4b885837904dd3cfd45faafe930defe14a", size = 8157926, upload-time = "2026-08-20T19:31:34.74Z" },
+ { url = "https://files.pythonhosted.org/packages/89/93/911434c6c4406e6979b6cb67ba889c85633ff8d92eb0cb569fec6e2a43f7/wasmtime-48.0.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:50e1ea81a3bec537d00e076722dfdc48978a56ea24619d8153aa1f75b11796b9", size = 9395773, upload-time = "2026-08-20T19:31:37.312Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/a6/91c9c19ed7f8e164f4db6405d872c9397be9f53e4f325d0adcd5e67598f4/wasmtime-48.0.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ea69889a3c51702e9da5f5f441027ca934f7758f8926a4ed167b0d6877f092e8", size = 8343024, upload-time = "2026-08-20T19:31:39.922Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/92/e144fcf578fc394678c24b042efe45f3b0614acdb87ea95d8b839b208842/wasmtime-48.0.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:58544d539053dff7bd4cf30c40d7a540862d683013c0dfa6ba46a063f5b682f7", size = 9796354, upload-time = "2026-08-20T19:31:42.325Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/c3/a957b226979daaeb09ec024562e9aac05e475a954537e6f150eb60bca84d/wasmtime-48.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:26fce3613fefbe29a28e9d659dca3326e800593858e5758cad086eb802b3b766", size = 8734885, upload-time = "2026-08-20T19:31:44.966Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/bf/00e44d1971307620d6660760ed04796405a5fb1819c8b43ec03ad85efac6/wasmtime-48.0.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:77f6b75db20be065e205e7af814d4e4f06784c3a00eb346e8c76148ecb4afe5a", size = 8786289, upload-time = "2026-08-20T19:31:47.99Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/55/ce68af7734a5a9424dd66a301b11c810215ec7f70230b35bed10ed312e97/wasmtime-48.0.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:62b241c8d5dfb59ff8af1ccaa5351f0ab7aba8cc872f7d80e0e3c95d54c13562", size = 9889697, upload-time = "2026-08-20T19:31:50.854Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/12/5266bebece874ebfa3196c973b917091dd4c55e9e9da55401e312c403044/wasmtime-48.0.0-py3-none-win_amd64.whl", hash = "sha256:21fa500e70f3819a8c0539c3f0be6b3b81ec3c630bb90c47dba4d8a2c1d4c698", size = 8157931, upload-time = "2026-08-20T19:31:53.312Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/a4/bb6c90d99ad893bd42f33aa7fb386deecb55987f012c0c2f5fcaba83106d/wasmtime-48.0.0-py3-none-win_arm64.whl", hash = "sha256:09cd5e14df80a3a8d447428a548583181c568ea2e617419d23600deff21d4b82", size = 7044651, upload-time = "2026-08-20T19:31:55.63Z" },
+]
+
[[package]]
name = "wcmatch"
-version = "10.1"
+version = "11.0.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "bracex" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/79/3e/c0bdc27cf06f4e47680bd5803a07cb3dfd17de84cde92dd217dcb9e05253/wcmatch-10.1.tar.gz", hash = "sha256:f11f94208c8c8484a16f4f48638a85d771d9513f4ab3f37595978801cb9465af", size = 117421, upload-time = "2025-06-22T19:14:02.49Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/57/43/30e407989e313677dbb9d5f045f966549a7254834571e342eaa4b55cc67b/wcmatch-11.0.1.tar.gz", hash = "sha256:1ea2b4fa678b8ca268253798d5963935df39132d47c3e241c0a0732224005e7d", size = 144662, upload-time = "2026-08-14T15:20:40.477Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/eb/d8/0d1d2e9d3fabcf5d6840362adcf05f8cf3cd06a73358140c3a97189238ae/wcmatch-10.1-py3-none-any.whl", hash = "sha256:5848ace7dbb0476e5e55ab63c6bbd529745089343427caa5537f230cc01beb8a", size = 39854, upload-time = "2025-06-22T19:14:00.978Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/77/7a02b0f05b3ffcdbef9719ce3ee0b508d6a29b58e95299f1580055671db3/wcmatch-11.0.1-py3-none-any.whl", hash = "sha256:fd149ecddb9f0a88ea780017d6dde17c994e494e7f7303d4e3c9d6251f978f4b", size = 43449, upload-time = "2026-08-14T15:20:39.379Z" },
]
[[package]]