Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion python/DEV_SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,8 @@ uv run poe check -S
```

#### `validate-dependency-bounds-test`
Run workspace-wide dependency compatibility gates at lower and upper resolutions. This runs test + pyright across all packages and stops on first failure:
Run workspace-wide dependency compatibility gates at lower and upper resolutions. This runs tests plus Pyright (or a
package-specific `dependency-pyright` task) across all packages and stops on first failure:
```bash
uv run poe validate-dependency-bounds-test
# Defaults to --package "*"; pass a package to scope test mode
Expand Down
7 changes: 6 additions & 1 deletion python/packages/ag-ui/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ classifiers = [
dependencies = [
"agent-framework-core>=1.11.0,<2",
"ag-ui-protocol>=0.1.19,<0.2",
"fastapi>=0.121.0,<0.138.1",
"fastapi>=0.121.0,<0.140.0",
"sse-starlette>=3.4.5,<4",
"uvicorn[standard]>=0.30.0,<1"
]
Expand All @@ -35,6 +35,11 @@ dev = [
"httpx==0.28.1",
]

[dependency-groups]
test = [
"agent-framework-orchestrations",
]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
Expand Down
4 changes: 4 additions & 0 deletions python/packages/core/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,10 @@ exclude_dirs = ["tests"]
executor.type = "uv"
include = "../../shared_tasks.toml"

[tool.poe.tasks.dependency-pyright]
help = "Run Pyright over core implementation files for isolated dependency validation."
cmd = "pyright --project pyrightconfig.dependency.json"

[tool.poe.tasks.mypy]
help = "Run MyPy for this package."
cmd = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework"
Expand Down
11 changes: 11 additions & 0 deletions python/packages/core/pyrightconfig.dependency.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"extends": "../../pyproject.toml",
"include": [
"agent_framework/*.py",
"agent_framework/_harness",
"agent_framework/_workflows"
],
"exclude": [
"agent_framework/_harness/_agent.py"
]
}
2 changes: 2 additions & 0 deletions python/packages/core/tests/core/test_agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -2983,6 +2983,8 @@ async def test_persist_only_history_provider_still_injects_inmemory(

async def test_shared_local_storage_cross_provider_responses_history_does_not_leak_fc_id() -> None:
"""Responses-specific replay metadata should stay local to Responses when session storage is shared."""
pytest.importorskip("agent_framework_openai")

from openai.types.chat.chat_completion import ChatCompletion, Choice
from openai.types.chat.chat_completion_message import ChatCompletionMessage

Expand Down
4 changes: 3 additions & 1 deletion python/packages/core/tests/core/test_azure_namespace.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# Copyright (c) Microsoft. All rights reserved.

from agent_framework_azure_cosmos import CosmosHistoryProvider
import pytest

import agent_framework.azure as azure

CosmosHistoryProvider = pytest.importorskip("agent_framework_azure_cosmos").CosmosHistoryProvider


def test_azure_namespace_exposes_cosmos_history_provider() -> None:
assert azure.CosmosHistoryProvider is CosmosHistoryProvider
Expand Down
12 changes: 9 additions & 3 deletions python/packages/core/tests/core/test_foundry_namespace.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
# Copyright (c) Microsoft. All rights reserved.

import pytest
from agent_framework_foundry import FoundryChatClient, FoundryMemoryProvider
from agent_framework_foundry_hosting import ResponsesHostServer
from agent_framework_foundry_local import FoundryLocalClient

import agent_framework.azure as azure
import agent_framework.foundry as foundry

_foundry = pytest.importorskip("agent_framework_foundry")
_foundry_hosting = pytest.importorskip("agent_framework_foundry_hosting")
_foundry_local = pytest.importorskip("agent_framework_foundry_local")

FoundryChatClient = _foundry.FoundryChatClient
FoundryMemoryProvider = _foundry.FoundryMemoryProvider
ResponsesHostServer = _foundry_hosting.ResponsesHostServer
FoundryLocalClient = _foundry_local.FoundryLocalClient


def test_foundry_namespace_exposes_cloud_and_local_symbols() -> None:
assert foundry.FoundryChatClient is FoundryChatClient
Expand Down
8 changes: 6 additions & 2 deletions python/packages/core/tests/core/test_harness_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,10 @@
import warnings
from collections.abc import AsyncIterable, Awaitable, Mapping, Sequence
from pathlib import Path
from typing import Any
from typing import TYPE_CHECKING, Any
from unittest.mock import patch

import pytest
from agent_framework_tools.shell import ShellResult

from agent_framework import (
AgentSession,
Expand Down Expand Up @@ -40,6 +39,9 @@
from agent_framework._sessions import ContextProvider, PerServiceCallHistoryPersistingMiddleware
from agent_framework._tools import FunctionInvocationLayer

if TYPE_CHECKING:
from agent_framework_tools.shell import ShellResult


class _FakeChatClient(BaseChatClient[ChatOptions[Any]]):
"""Minimal chat client stub for testing assembly."""
Expand Down Expand Up @@ -867,6 +869,8 @@ async def close(self) -> None:
pass

async def run(self, command: str, *, timeout: float | None = None) -> ShellResult:
from agent_framework_tools.shell import ShellResult

return ShellResult(stdout="", stderr="", exit_code=0, duration_ms=0)

async def __aenter__(self) -> _FakeShellTool:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
executor,
handler,
)
from agent_framework.orchestrations import SequentialBuilder


class _SimpleAgent(BaseAgent):
Expand Down Expand Up @@ -254,6 +253,9 @@ async def _run() -> AgentResponse:


async def test_sequential_adapter_uses_full_conversation() -> None:
pytest.importorskip("agent_framework_orchestrations")
from agent_framework.orchestrations import SequentialBuilder

# Arrange: two streaming agents; the second records what it receives
a1 = _CaptureAgent(id="agent1", name="A1", reply_text="A1 reply")
a2 = _CaptureAgent(id="agent2", name="A2", reply_text="A2 reply")
Expand All @@ -273,6 +275,9 @@ async def test_sequential_adapter_uses_full_conversation() -> None:


async def test_sequential_handoff_preserves_function_call_for_non_reasoning_model() -> None:
pytest.importorskip("agent_framework_orchestrations")
from agent_framework.orchestrations import SequentialBuilder

# Arrange: non-reasoning agent emits function_call + function_result + summary
first = _ToolHistoryAgent(
id="tool_history_agent",
Expand Down
2 changes: 2 additions & 0 deletions python/packages/core/tests/workflow/test_workflow_kwargs.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

import pytest

pytest.importorskip("agent_framework_orchestrations")

from agent_framework import (
AgentResponse,
AgentResponseUpdate,
Expand Down
6 changes: 6 additions & 0 deletions python/scripts/dependencies/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ Run the commands below from the `python/` directory.
- `_dependency_bounds_runtime.py`
- Shared helper used by the validators to build isolated `uv run` commands.
- Reattaches the repo-wide toolchain (`ruff`, `pyright`, `pytest`, `poethepoet`, and related helpers) inside temporary environments so package tasks behave the same way they do in the workspace.
- Resolves internal editable packages from the target's enabled groups and extras, following only base transitive
dependencies and explicitly requested extras so aggregate surfaces such as `core[all]` do not leak into unrelated
package probes.
- Uses a package-defined `dependency-pyright` task when present, allowing dependency probes to type-check the
package implementation without requiring optional lazy namespace packages. Normal repository Pyright tasks are
unchanged. These tasks reuse the root workspace `test` dependency requirements inside their isolated environment.


## Common entrypoints
Expand Down
101 changes: 24 additions & 77 deletions python/scripts/dependencies/_dependency_bounds_lower_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,19 @@
from urllib import request as urllib_request

import tomli
from packaging.requirements import InvalidRequirement, Requirement
from packaging.utils import canonicalize_name
from packaging.version import InvalidVersion, Version
from rich import print

from scripts.dependencies._dependency_bounds_runtime import (
extend_command_with_runtime_tools,
extend_command_with_task,
load_workspace_package_configs,
resolve_internal_editables,
)
from scripts.task_runner import discover_projects, extract_poe_tasks, project_filter_matches

CHECK_TASK_PRIORITY = ("check", "typing", "pyright", "mypy", "lint")
CHECK_TASK_PRIORITY = ("dependency-pyright", "check", "typing", "pyright", "mypy", "lint")
REQ_PATTERN = r"^\s*([A-Za-z0-9_.-]+(?:\[[^\]]+\])?)\s*(.*?)\s*$"
SECTION_HEADER_PATTERN = re.compile(r"^\s*\[([^\]]+)\]\s*$")
INLINE_ARRAY_ASSIGNMENT_PATTERN = re.compile(
Expand Down Expand Up @@ -422,13 +424,6 @@ def _load_package_name(pyproject_file: Path) -> str:
return str(data["project"]["name"])


def _extract_requirement_name(requirement: str) -> str | None:
try:
return Requirement(requirement).name.lower()
except InvalidRequirement:
return None


def _select_validation_tasks(available_tasks: set[str]) -> list[str]:
check_task = next((task for task in CHECK_TASK_PRIORITY if task in available_tasks), None)
tasks: list[str] = []
Expand All @@ -439,62 +434,6 @@ def _select_validation_tasks(available_tasks: set[str]) -> list[str]:
return tasks


def _build_workspace_package_map(workspace_root: Path) -> dict[str, Path]:
package_map: dict[str, Path] = {}
for pyproject_file in sorted((workspace_root / "packages").glob("*/pyproject.toml")):
with pyproject_file.open("rb") as f:
data = tomli.load(f)
package_name = str(data.get("project", {}).get("name", "")).strip()
if package_name:
package_map[package_name] = pyproject_file.parent
return package_map


def _build_internal_graph(workspace_root: Path, package_map: dict[str, Path]) -> dict[str, set[str]]:
graph: dict[str, set[str]] = {}
for package_name, package_path in package_map.items():
pyproject_file = package_path / "pyproject.toml"
with pyproject_file.open("rb") as f:
data = tomli.load(f)
project = data.get("project", {}) or {}
dependencies: list[str] = list(project.get("dependencies", []) or [])
for values in (project.get("optional-dependencies", {}) or {}).values():
dependencies.extend([value for value in (values or []) if isinstance(value, str)])
for values in (data.get("dependency-groups", {}) or {}).values():
dependencies.extend([value for value in (values or []) if isinstance(value, str)])
internal = set()
for dependency in dependencies:
dependency_name = _extract_requirement_name(dependency)
if dependency_name is None:
continue
if dependency_name.startswith("agent-framework"):
for candidate_name in package_map:
if candidate_name.lower() == dependency_name:
internal.add(candidate_name)
break
graph[package_name] = internal
return graph


def _resolve_internal_editables(
package_name: str, package_map: dict[str, Path], graph: dict[str, set[str]]
) -> list[Path]:
visited: set[str] = set()
stack = [package_name]
results: set[Path] = set()
while stack:
current = stack.pop()
if current in visited:
continue
visited.add(current)
for dependency_name in graph.get(current, set()):
dependency_path = package_map.get(dependency_name)
if dependency_path and dependency_name != package_name:
results.add(dependency_path.resolve())
stack.append(dependency_name)
return sorted(results)


def _collect_targets(
pyproject_file: Path,
*,
Expand Down Expand Up @@ -648,7 +587,7 @@ def _run_tasks(
if dependency_pin is not None:
dependency_name, dependency_version = dependency_pin
command.extend(["--with", f"{dependency_name}=={dependency_version}"])
extend_command_with_task(command, task_name)
extend_command_with_task(command, task_name, workspace_root=workspace_root)
try:
result = subprocess.run(
command,
Expand Down Expand Up @@ -1024,8 +963,7 @@ def main() -> None:
output_json_path = (workspace_root / args.output_json).resolve()

# Phase 1: prepare shared workspace metadata and collect package execution plans.
package_map = _build_workspace_package_map(workspace_root)
internal_graph = _build_internal_graph(workspace_root, package_map)
workspace_packages = load_workspace_package_configs(workspace_root)
lock_versions = _load_lock_versions(workspace_root)
catalog = VersionCatalog(lock_versions=lock_versions, source=args.version_source)

Expand All @@ -1036,11 +974,15 @@ def main() -> None:
print(f"[yellow]Skipping {project_path}: missing pyproject.toml[/yellow]")
continue
package_name = _load_package_name(pyproject_file)
with pyproject_file.open("rb") as f:
package_config = tomli.load(f)
project_section = package_config.get("project", {})
optional_dependencies = project_section.get("optional-dependencies", {}) or {}
dependency_groups = package_config.get("dependency-groups", {}) or {}
workspace_package = workspace_packages[str(canonicalize_name(package_name))]
dependency_group_names = sorted(workspace_package.dependency_groups)
include_dev_extra = "dev" in workspace_package.optional_dependencies
optional_extra_names = sorted(
name for name in workspace_package.optional_dependencies if name not in {"all", "dev"}
)
selected_extra_names = list(optional_extra_names)
if include_dev_extra:
selected_extra_names.append("dev")
# Reuse the shared selector matcher so direct optimizer runs accept the
# same short-name package filters as the contributor-facing Poe tasks.
if package_filters and not any(
Expand All @@ -1052,10 +994,15 @@ def main() -> None:
project_path=project_path,
package_name=package_name,
pyproject_path=pyproject_file,
internal_editables=_resolve_internal_editables(package_name, package_map, internal_graph),
dependency_groups=sorted(dependency_groups),
include_dev_extra="dev" in optional_dependencies,
optional_extras=sorted(name for name in optional_dependencies if name not in {"all", "dev"}),
internal_editables=resolve_internal_editables(
package_name,
workspace_packages,
dependency_groups=dependency_group_names,
optional_extras=selected_extra_names,
),
dependency_groups=dependency_group_names,
include_dev_extra=include_dev_extra,
optional_extras=optional_extra_names,
)
)

Expand Down
Loading
Loading