2785 lines
113 KiB
Python
2785 lines
113 KiB
Python
from __future__ import annotations
|
|
|
|
import ast
|
|
import dataclasses
|
|
import enum
|
|
import importlib
|
|
import inspect
|
|
import json
|
|
import logging
|
|
import sys
|
|
import traceback
|
|
import typing
|
|
from collections.abc import Callable, Iterable, Mapping
|
|
from copy import deepcopy
|
|
from importlib.util import find_spec
|
|
from pathlib import Path
|
|
from types import FunctionType, ModuleType, TracebackType, UnionType
|
|
from typing import (
|
|
Any,
|
|
ForwardRef,
|
|
Literal,
|
|
TypeAlias,
|
|
Union,
|
|
cast,
|
|
get_args,
|
|
get_origin,
|
|
get_type_hints,
|
|
)
|
|
|
|
import typing_extensions
|
|
from pydantic import BaseModel
|
|
from typing_extensions import NotRequired, Required
|
|
|
|
|
|
@dataclasses.dataclass(frozen=True)
|
|
class OptionalDependencyInstallation:
|
|
dependency_module: str
|
|
extra: str | None = None
|
|
requirement: str | None = None
|
|
unsupported_platforms: tuple[str, ...] = ()
|
|
|
|
def is_supported_on_current_platform(self) -> bool:
|
|
return sys.platform not in self.unsupported_platforms
|
|
|
|
|
|
@dataclasses.dataclass(frozen=True)
|
|
class SubmoduleExportPolicy:
|
|
modules: dict[str, dict[str, dict[str, str]]]
|
|
dependency_installations: tuple[OptionalDependencyInstallation, ...]
|
|
canonical_imports: tuple[dict[str, str], ...] = ()
|
|
public_class_contracts: tuple[dict[str, Any], ...] = ()
|
|
public_properties: tuple[dict[str, Any], ...] = ()
|
|
public_type_aliases: tuple[dict[str, str], ...] = ()
|
|
public_typed_dicts: tuple[dict[str, Any], ...] = ()
|
|
|
|
|
|
def load_api_contract(path: Path) -> dict[str, Any]:
|
|
contract = cast(dict[str, Any], json.loads(path.read_text(encoding="utf-8")))
|
|
_add_legacy_literal_types(contract)
|
|
return contract
|
|
|
|
|
|
def load_submodule_export_policy(path: Path) -> SubmoduleExportPolicy:
|
|
value = json.loads(path.read_text(encoding="utf-8"))
|
|
if not isinstance(value, dict):
|
|
raise ValueError("submodule export policy must be an object")
|
|
unknown_top_level_fields = sorted(
|
|
set(value)
|
|
- {
|
|
"canonical_imports",
|
|
"modules",
|
|
"optional_dependencies",
|
|
"public_class_contracts",
|
|
"public_properties",
|
|
"public_type_aliases",
|
|
"public_typed_dicts",
|
|
}
|
|
)
|
|
if unknown_top_level_fields:
|
|
raise ValueError(
|
|
f"submodule export policy has unknown fields: {unknown_top_level_fields!r}"
|
|
)
|
|
modules = value.get("modules")
|
|
if not isinstance(modules, dict):
|
|
raise ValueError("submodule export policy modules must be an object keyed by module name")
|
|
policy: dict[str, dict[str, dict[str, str]]] = {}
|
|
for module_name, declarations in modules.items():
|
|
if type(module_name) is not str or not module_name:
|
|
raise ValueError("submodule export policy module names must be non-empty strings")
|
|
if not isinstance(declarations, dict):
|
|
raise ValueError(f"submodule export policy for {module_name} must be an object")
|
|
unknown_fields = sorted(set(declarations) - {"optional_bindings", "optional_exports"})
|
|
if unknown_fields:
|
|
raise ValueError(
|
|
f"submodule export policy for {module_name} has unknown fields: {unknown_fields!r}"
|
|
)
|
|
policy[module_name] = {
|
|
"optional_bindings": _optional_dependency_modules(
|
|
declarations.get("optional_bindings", {}), field_name="optional_bindings"
|
|
),
|
|
"optional_exports": _optional_dependency_modules(
|
|
declarations.get("optional_exports", {}), field_name="optional_exports"
|
|
),
|
|
}
|
|
|
|
dependencies = value.get("optional_dependencies")
|
|
if not isinstance(dependencies, dict):
|
|
raise ValueError("submodule export policy optional_dependencies must be an object")
|
|
dependency_installations: list[OptionalDependencyInstallation] = []
|
|
for module_name, installation in dependencies.items():
|
|
if type(module_name) is not str or not module_name:
|
|
raise ValueError("optional dependency module names must be non-empty strings")
|
|
if not isinstance(installation, dict):
|
|
raise ValueError(
|
|
f"optional dependency installation for {module_name} must be an object"
|
|
)
|
|
unknown_fields = sorted(
|
|
set(installation) - {"extra", "requirement", "unsupported_platforms"}
|
|
)
|
|
if unknown_fields:
|
|
raise ValueError(
|
|
f"optional dependency installation for {module_name} has unknown fields: "
|
|
f"{unknown_fields!r}"
|
|
)
|
|
configured = [field for field in ("extra", "requirement") if field in installation]
|
|
if len(configured) != 1:
|
|
raise ValueError(
|
|
f"optional dependency installation for {module_name} must declare exactly one "
|
|
"of extra or requirement"
|
|
)
|
|
field_name = configured[0]
|
|
install_value = installation[field_name]
|
|
if type(install_value) is not str or not install_value:
|
|
raise ValueError(
|
|
f"optional dependency installation {field_name} for {module_name} must be a "
|
|
"non-empty string"
|
|
)
|
|
unsupported_platforms = installation.get("unsupported_platforms", [])
|
|
if (
|
|
not isinstance(unsupported_platforms, list)
|
|
or not all(type(platform) is str and platform for platform in unsupported_platforms)
|
|
or len(unsupported_platforms) != len(set(unsupported_platforms))
|
|
):
|
|
raise ValueError(
|
|
f"optional dependency installation unsupported_platforms for {module_name} "
|
|
"must be a list of unique non-empty strings"
|
|
)
|
|
dependency_installations.append(
|
|
OptionalDependencyInstallation(
|
|
dependency_module=module_name,
|
|
extra=install_value if field_name == "extra" else None,
|
|
requirement=install_value if field_name == "requirement" else None,
|
|
unsupported_platforms=tuple(unsupported_platforms),
|
|
)
|
|
)
|
|
|
|
referenced_dependencies = {
|
|
dependency
|
|
for module_policy in policy.values()
|
|
for declarations in module_policy.values()
|
|
for dependency in declarations.values()
|
|
}
|
|
missing_installations = sorted(referenced_dependencies - set(dependencies))
|
|
unused_installations = sorted(set(dependencies) - referenced_dependencies)
|
|
if missing_installations:
|
|
raise ValueError(
|
|
"submodule export policy dependencies are missing installation declarations: "
|
|
f"{missing_installations!r}"
|
|
)
|
|
if unused_installations:
|
|
raise ValueError(
|
|
"submodule export policy has unused dependency installation declarations: "
|
|
f"{unused_installations!r}"
|
|
)
|
|
return SubmoduleExportPolicy(
|
|
modules=policy,
|
|
dependency_installations=tuple(
|
|
sorted(
|
|
dependency_installations, key=lambda installation: installation.dependency_module
|
|
)
|
|
),
|
|
canonical_imports=_canonical_import_policy(value.get("canonical_imports", [])),
|
|
public_class_contracts=_public_class_contract_policy(
|
|
value.get("public_class_contracts", [])
|
|
),
|
|
public_properties=_public_property_policy(value.get("public_properties", [])),
|
|
public_type_aliases=_public_type_alias_policy(value.get("public_type_aliases", [])),
|
|
public_typed_dicts=_public_typed_dict_policy(value.get("public_typed_dicts", [])),
|
|
)
|
|
|
|
|
|
def _canonical_import_policy(value: object) -> tuple[dict[str, str], ...]:
|
|
if not isinstance(value, list):
|
|
raise ValueError("submodule export policy canonical_imports must be a list")
|
|
required_fields = {"canonical_module", "canonical_name", "module", "name"}
|
|
entries: list[dict[str, str]] = []
|
|
identities: set[tuple[str, str]] = set()
|
|
for entry in value:
|
|
if not isinstance(entry, dict) or set(entry) != required_fields:
|
|
raise ValueError(
|
|
"submodule export policy canonical_imports entries must contain exactly "
|
|
"canonical_module, canonical_name, module, and name"
|
|
)
|
|
if not all(type(entry[field]) is str and entry[field] for field in required_fields):
|
|
raise ValueError(
|
|
"submodule export policy canonical_imports values must be non-empty strings"
|
|
)
|
|
identity = (entry["module"], entry["name"])
|
|
if identity in identities:
|
|
raise ValueError(
|
|
"submodule export policy canonical_imports must not repeat "
|
|
f"{entry['module']}.{entry['name']}"
|
|
)
|
|
identities.add(identity)
|
|
entries.append({field: entry[field] for field in sorted(required_fields)})
|
|
return tuple(entries)
|
|
|
|
|
|
def _public_property_policy(value: object) -> tuple[dict[str, Any], ...]:
|
|
if not isinstance(value, list):
|
|
raise ValueError("submodule export policy public_properties must be a list")
|
|
entries: list[dict[str, Any]] = []
|
|
identities: set[tuple[str, str, str]] = set()
|
|
for entry in value:
|
|
if not isinstance(entry, dict):
|
|
raise ValueError("submodule export policy public_properties entries must be objects")
|
|
owner_fields = {"class_name", "factory_name"} & set(entry)
|
|
required_fields = {"module", "names", *owner_fields}
|
|
if len(owner_fields) != 1 or set(entry) != required_fields:
|
|
raise ValueError(
|
|
"submodule export policy public_properties entries must contain exactly "
|
|
"module, names, and one of class_name or factory_name"
|
|
)
|
|
owner_field = next(iter(owner_fields))
|
|
module_name = entry["module"]
|
|
owner_name = entry[owner_field]
|
|
names = entry["names"]
|
|
if type(module_name) is not str or not module_name:
|
|
raise ValueError(
|
|
"submodule export policy public_properties module must be a non-empty string"
|
|
)
|
|
if type(owner_name) is not str or not owner_name:
|
|
raise ValueError(
|
|
f"submodule export policy public_properties {owner_field} must be a non-empty "
|
|
"string"
|
|
)
|
|
if (
|
|
not isinstance(names, list)
|
|
or not names
|
|
or not all(type(name) is str and name for name in names)
|
|
or len(names) != len(set(names))
|
|
):
|
|
raise ValueError(
|
|
"submodule export policy public_properties names must be a non-empty list of "
|
|
"unique non-empty strings"
|
|
)
|
|
identity = (owner_field, module_name, owner_name)
|
|
if identity in identities:
|
|
raise ValueError(
|
|
"submodule export policy public_properties must not repeat "
|
|
f"{module_name}.{owner_name}"
|
|
)
|
|
identities.add(identity)
|
|
normalized_entry = {
|
|
owner_field: owner_name,
|
|
"module": module_name,
|
|
"names": list(names),
|
|
}
|
|
entries.append(normalized_entry)
|
|
return tuple(entries)
|
|
|
|
|
|
def _public_class_contract_policy(value: object) -> tuple[dict[str, Any], ...]:
|
|
if not isinstance(value, list):
|
|
raise ValueError("submodule export policy public_class_contracts must be a list")
|
|
required_fields = {"class_name", "module"}
|
|
contract_fields = {"abstract", "abstract_members"}
|
|
entries: list[dict[str, Any]] = []
|
|
identities: set[tuple[str, str]] = set()
|
|
for entry in value:
|
|
if (
|
|
not isinstance(entry, dict)
|
|
or not required_fields.issubset(entry)
|
|
or not set(entry).issubset(required_fields | contract_fields)
|
|
or not (set(entry) & contract_fields)
|
|
):
|
|
raise ValueError(
|
|
"submodule export policy public_class_contracts entries must contain exactly "
|
|
"module, class_name, and at least one of abstract or abstract_members"
|
|
)
|
|
module_name = entry["module"]
|
|
class_name = entry["class_name"]
|
|
if type(module_name) is not str or not module_name:
|
|
raise ValueError(
|
|
"submodule export policy public_class_contracts module must be a non-empty string"
|
|
)
|
|
if type(class_name) is not str or not class_name:
|
|
raise ValueError(
|
|
"submodule export policy public_class_contracts class_name must be a non-empty "
|
|
"string"
|
|
)
|
|
if "abstract" in entry and type(entry["abstract"]) is not bool:
|
|
raise ValueError(
|
|
"submodule export policy public_class_contracts abstract must be a boolean"
|
|
)
|
|
abstract_members = entry.get("abstract_members")
|
|
if "abstract_members" in entry and (
|
|
not isinstance(abstract_members, list)
|
|
or not abstract_members
|
|
or not all(type(name) is str and name for name in abstract_members)
|
|
or len(abstract_members) != len(set(abstract_members))
|
|
):
|
|
raise ValueError(
|
|
"submodule export policy public_class_contracts abstract_members must be a "
|
|
"non-empty list of unique non-empty strings"
|
|
)
|
|
identity = (module_name, class_name)
|
|
if identity in identities:
|
|
raise ValueError(
|
|
"submodule export policy public_class_contracts must not repeat "
|
|
f"{module_name}.{class_name}"
|
|
)
|
|
identities.add(identity)
|
|
normalized_entry: dict[str, Any] = {
|
|
"class_name": class_name,
|
|
"module": module_name,
|
|
}
|
|
if "abstract" in entry:
|
|
normalized_entry["abstract"] = entry["abstract"]
|
|
if "abstract_members" in entry:
|
|
normalized_entry["abstract_members"] = sorted(abstract_members)
|
|
entries.append(normalized_entry)
|
|
return tuple(entries)
|
|
|
|
|
|
def _public_typed_dict_policy(value: object) -> tuple[dict[str, Any], ...]:
|
|
if not isinstance(value, list):
|
|
raise ValueError("submodule export policy public_typed_dicts must be a list")
|
|
required_fields = {"class_name", "module", "names"}
|
|
entries: list[dict[str, Any]] = []
|
|
identities: set[tuple[str, str]] = set()
|
|
for entry in value:
|
|
if not isinstance(entry, dict) or set(entry) != required_fields:
|
|
raise ValueError(
|
|
"submodule export policy public_typed_dicts entries must contain exactly "
|
|
"class_name, module, and names"
|
|
)
|
|
module_name = entry["module"]
|
|
class_name = entry["class_name"]
|
|
names = entry["names"]
|
|
if type(module_name) is not str or not module_name:
|
|
raise ValueError(
|
|
"submodule export policy public_typed_dicts module must be a non-empty string"
|
|
)
|
|
if type(class_name) is not str or not class_name:
|
|
raise ValueError(
|
|
"submodule export policy public_typed_dicts class_name must be a non-empty string"
|
|
)
|
|
if (
|
|
not isinstance(names, list)
|
|
or not names
|
|
or not all(type(name) is str and name for name in names)
|
|
or len(names) != len(set(names))
|
|
):
|
|
raise ValueError(
|
|
"submodule export policy public_typed_dicts names must be a non-empty list of "
|
|
"unique non-empty strings"
|
|
)
|
|
identity = (module_name, class_name)
|
|
if identity in identities:
|
|
raise ValueError(
|
|
"submodule export policy public_typed_dicts must not repeat "
|
|
f"{module_name}.{class_name}"
|
|
)
|
|
identities.add(identity)
|
|
entries.append({"class_name": class_name, "module": module_name, "names": list(names)})
|
|
return tuple(entries)
|
|
|
|
|
|
def _public_type_alias_policy(value: object) -> tuple[dict[str, str], ...]:
|
|
if not isinstance(value, list):
|
|
raise ValueError("submodule export policy public_type_aliases must be a list")
|
|
required_fields = {"module", "name"}
|
|
entries: list[dict[str, str]] = []
|
|
identities: set[tuple[str, str]] = set()
|
|
for entry in value:
|
|
if not isinstance(entry, dict) or set(entry) != required_fields:
|
|
raise ValueError(
|
|
"submodule export policy public_type_aliases entries must contain exactly "
|
|
"module and name"
|
|
)
|
|
if not all(type(entry[field]) is str and entry[field] for field in required_fields):
|
|
raise ValueError(
|
|
"submodule export policy public_type_aliases values must be non-empty strings"
|
|
)
|
|
identity = (entry["module"], entry["name"])
|
|
if identity in identities:
|
|
raise ValueError(
|
|
"submodule export policy public_type_aliases must not repeat "
|
|
f"{entry['module']}.{entry['name']}"
|
|
)
|
|
identities.add(identity)
|
|
entries.append({"module": entry["module"], "name": entry["name"]})
|
|
return tuple(entries)
|
|
|
|
|
|
def _add_legacy_literal_types(value: object) -> None:
|
|
if isinstance(value, dict):
|
|
if value.get("kind") == "literal" and "value" in value and "type" not in value:
|
|
literal = value["value"]
|
|
value["type"] = f"{type(literal).__module__}.{type(literal).__qualname__}"
|
|
for child in value.values():
|
|
_add_legacy_literal_types(child)
|
|
elif isinstance(value, list):
|
|
for child in value:
|
|
_add_legacy_literal_types(child)
|
|
|
|
|
|
def _redaction_observables(
|
|
error: BaseException | None,
|
|
records: Iterable[logging.LogRecord],
|
|
) -> str:
|
|
values: list[str] = []
|
|
seen: dict[int, object] = {}
|
|
|
|
def visit_exception_state(value: object) -> None:
|
|
value_id = id(value)
|
|
if value_id in seen:
|
|
return
|
|
# Keep visited objects alive so a later temporary object cannot reuse an id and be
|
|
# mistaken for a cycle. Traceback frame locals are materialized as temporary dicts.
|
|
seen[value_id] = value
|
|
|
|
if isinstance(value, BaseException):
|
|
state = vars(value)
|
|
values.append(repr(state))
|
|
visit_exception_state(value.args)
|
|
visit_exception_state(value.__cause__)
|
|
visit_exception_state(value.__context__)
|
|
visit_exception_state(value.__traceback__)
|
|
visit_exception_state(state)
|
|
elif isinstance(value, TracebackType):
|
|
module_name = value.tb_frame.f_globals.get("__name__", "")
|
|
if module_name == "agents" or module_name.startswith("agents."):
|
|
visit_exception_state(value.tb_frame.f_locals)
|
|
visit_exception_state(value.tb_next)
|
|
elif isinstance(value, Mapping):
|
|
for key, item in value.items():
|
|
visit_exception_state(key)
|
|
visit_exception_state(item)
|
|
elif (
|
|
dataclasses.is_dataclass(value)
|
|
and not isinstance(value, type)
|
|
and (type(value).__module__ == "agents" or type(value).__module__.startswith("agents."))
|
|
):
|
|
for field in dataclasses.fields(value):
|
|
visit_exception_state(getattr(value, field.name))
|
|
elif isinstance(value, list | tuple | set | frozenset):
|
|
for item in value:
|
|
visit_exception_state(item)
|
|
elif isinstance(value, str | bytes | int | float | bool | None):
|
|
values.append(repr(value))
|
|
|
|
if error is not None:
|
|
values.extend(
|
|
(
|
|
str(error),
|
|
repr(error),
|
|
repr(error.__cause__),
|
|
repr(error.__context__),
|
|
"".join(traceback.format_exception(error)),
|
|
)
|
|
)
|
|
visit_exception_state(error)
|
|
for record in records:
|
|
values.extend((record.getMessage(), repr(record.args), repr(record.__dict__)))
|
|
visit_exception_state(record.__dict__)
|
|
if record.exc_info is not None:
|
|
values.append("".join(traceback.format_exception(*record.exc_info)))
|
|
visit_exception_state(record.exc_info)
|
|
return "\n".join(values)
|
|
|
|
|
|
def _deserialize_common_sandbox_session_state(payload: dict[str, object]) -> Any:
|
|
from agents.sandbox.session import SandboxSessionState
|
|
|
|
persisted_payload = deepcopy(payload)
|
|
state = SandboxSessionState.model_validate(persisted_payload)
|
|
return SandboxSessionState._mark_persisted_path_grants(state, payload=persisted_payload)
|
|
|
|
|
|
def _default_contract(value: object) -> dict[str, object]:
|
|
if value is inspect.Parameter.empty or value is dataclasses.MISSING:
|
|
return {"kind": "required"}
|
|
if value.__class__.__name__ == "_HAS_DEFAULT_FACTORY_CLASS":
|
|
return {"kind": "factory"}
|
|
if value is None or isinstance(value, bool | int | float | str):
|
|
return {
|
|
"kind": "literal",
|
|
"type": f"{type(value).__module__}.{type(value).__qualname__}",
|
|
"value": value,
|
|
}
|
|
voice_testing = sys.modules.get("agents.voice.testing")
|
|
if voice_testing is not None and value is getattr(voice_testing, "_START_NOT_CONFIGURED", None):
|
|
return {
|
|
"kind": "sentinel",
|
|
"identity": "agents.voice.testing._START_NOT_CONFIGURED",
|
|
}
|
|
value_type = f"{type(value).__module__}.{type(value).__qualname__}"
|
|
from agents.mcp.server import _UNSET as mcp_failure_error_unset
|
|
from agents.retry import _UNSET as retry_unset
|
|
from agents.tool import _UNSET_FAILURE_ERROR_FUNCTION as failure_error_function_unset
|
|
from agents.tool_context import _MISSING as tool_context_missing
|
|
|
|
sentinel_identities = (
|
|
(retry_unset, "agents.retry._UNSET"),
|
|
(mcp_failure_error_unset, "agents.mcp.server._UNSET"),
|
|
(failure_error_function_unset, "agents.tool._UNSET_FAILURE_ERROR_FUNCTION"),
|
|
(tool_context_missing, "agents.tool_context._MISSING"),
|
|
)
|
|
for sentinel, identity in sentinel_identities:
|
|
if value is sentinel:
|
|
return {"kind": "sentinel", "identity": identity}
|
|
if value_type == "pydantic.fields.FieldInfo":
|
|
return {"kind": "repr", "type": value_type, "value": repr(value)}
|
|
if isinstance(value, enum.Enum):
|
|
return {
|
|
"kind": "enum",
|
|
"type": value_type,
|
|
"name": value.name,
|
|
"value": _default_contract(value.value),
|
|
}
|
|
if isinstance(value, type):
|
|
return {
|
|
"kind": "type",
|
|
"identity": f"{value.__module__}.{value.__qualname__}",
|
|
}
|
|
if isinstance(value, tuple | list):
|
|
return {
|
|
"kind": "sequence",
|
|
"type": value_type,
|
|
"items": [_default_contract(item) for item in value],
|
|
}
|
|
if isinstance(value, dict):
|
|
return {
|
|
"kind": "mapping",
|
|
"type": value_type,
|
|
"items": [
|
|
[_default_contract(key), _default_contract(item)] for key, item in value.items()
|
|
],
|
|
}
|
|
if value_type.startswith("agents.") and callable(getattr(value, "model_dump", None)):
|
|
dumped = value.model_dump(mode="python") # type: ignore[attr-defined]
|
|
return {
|
|
"kind": "model",
|
|
"type": value_type,
|
|
"value": _default_contract(dumped),
|
|
}
|
|
if dataclasses.is_dataclass(value) and not isinstance(value, type):
|
|
return {
|
|
"kind": "dataclass",
|
|
"type": value_type,
|
|
"fields": [
|
|
{"name": field.name, "value": _default_contract(getattr(value, field.name))}
|
|
for field in dataclasses.fields(value)
|
|
],
|
|
}
|
|
if type(value) is FunctionType and value.__module__.startswith("agents."):
|
|
return {
|
|
"kind": "callable",
|
|
"identity": f"{value.__module__}.{value.__qualname__}",
|
|
}
|
|
raise TypeError(f"Unsupported public API default value: {value_type}")
|
|
|
|
|
|
def _parameter_records(
|
|
parameters: Iterable[inspect.Parameter],
|
|
) -> list[dict[str, object]]:
|
|
return [
|
|
{
|
|
"name": parameter.name,
|
|
"kind": parameter.kind.name,
|
|
"default": _default_contract(parameter.default),
|
|
}
|
|
for parameter in parameters
|
|
]
|
|
|
|
|
|
def _signature(value: Callable[..., Any]) -> inspect.Signature:
|
|
return inspect.signature(value)
|
|
|
|
|
|
def _parameter_contract(value: Callable[..., Any]) -> list[dict[str, object]]:
|
|
parameters = list(_signature(value).parameters.values())
|
|
if issubclass(type(value), type) and issubclass(cast(type, value), enum.Enum):
|
|
parameters = list(_signature(value.__new__).parameters.values())[1:]
|
|
return _parameter_records(parameters)
|
|
|
|
|
|
def _dataclass_field_contract(value: object) -> list[dict[str, object]]:
|
|
if not dataclasses.is_dataclass(value):
|
|
return []
|
|
result: list[dict[str, object]] = []
|
|
for field in dataclasses.fields(value):
|
|
if field.name.startswith("_"):
|
|
continue
|
|
if field.default_factory is not dataclasses.MISSING:
|
|
factory = cast(Callable[..., Any], field.default_factory)
|
|
default_contract: dict[str, object] = {
|
|
"kind": "factory",
|
|
"factory": f"{factory.__module__}.{factory.__qualname__}",
|
|
}
|
|
else:
|
|
default_contract = _default_contract(field.default)
|
|
result.append(
|
|
{
|
|
"name": field.name,
|
|
"init": field.init,
|
|
"default": default_contract,
|
|
}
|
|
)
|
|
return result
|
|
|
|
|
|
def _pydantic_model_field_contract(value: object) -> list[dict[str, object]] | None:
|
|
if not (isinstance(value, type) and issubclass(value, BaseModel)):
|
|
return None
|
|
result: list[dict[str, object]] = []
|
|
for name, field in value.model_fields.items():
|
|
if name.startswith("_"):
|
|
continue
|
|
if field.is_required():
|
|
default_contract: dict[str, object] = {"kind": "required"}
|
|
elif field.default_factory is not None:
|
|
factory = field.default_factory
|
|
default_contract = {
|
|
"kind": "factory",
|
|
"factory": f"{factory.__module__}.{factory.__qualname__}",
|
|
}
|
|
else:
|
|
default_contract = _default_contract(field.default)
|
|
result.append({"name": name, "default": default_contract})
|
|
return result
|
|
|
|
|
|
def _callable_kind(value: Callable[..., Any]) -> str | None:
|
|
if issubclass(type(value), type):
|
|
return "class"
|
|
if type(value) is FunctionType:
|
|
return "function"
|
|
return None
|
|
|
|
|
|
def _is_sdk_owned_callable(value: object) -> bool:
|
|
module_name = getattr(value, "__module__", None)
|
|
return (
|
|
_callable_kind(cast(Callable[..., Any], value)) is not None
|
|
and isinstance(module_name, str)
|
|
and (module_name == "agents" or module_name.startswith("agents."))
|
|
)
|
|
|
|
|
|
def _enum_member_contract(value: object) -> list[dict[str, object]] | None:
|
|
if not (issubclass(type(value), type) and issubclass(cast(type, value), enum.Enum)):
|
|
return None
|
|
enum_type = cast(type[enum.Enum], value)
|
|
members: list[dict[str, object]] = []
|
|
for name, member in enum_type.__members__.items():
|
|
member_value = member.value
|
|
if member_value is None or isinstance(member_value, bool | int | float | str):
|
|
value_contract: dict[str, object] = {
|
|
"kind": "literal",
|
|
"type": f"{type(member_value).__module__}.{type(member_value).__qualname__}",
|
|
"value": member_value,
|
|
}
|
|
else:
|
|
raise TypeError(
|
|
f"Unsupported public enum value for "
|
|
f"{enum_type.__module__}.{enum_type.__qualname__}."
|
|
f"{name}: {type(member_value).__module__}.{type(member_value).__qualname__}"
|
|
)
|
|
members.append({"name": name, "value": value_contract})
|
|
return members
|
|
|
|
|
|
def _class_member_contract(descriptor: object) -> dict[str, object] | None:
|
|
descriptor_type = type(descriptor)
|
|
if descriptor_type is staticmethod:
|
|
binding = "static"
|
|
function = object.__getattribute__(descriptor, "__func__")
|
|
skip_first = False
|
|
elif descriptor_type is classmethod:
|
|
binding = "class"
|
|
function = object.__getattribute__(descriptor, "__func__")
|
|
skip_first = True
|
|
elif type(descriptor) is FunctionType:
|
|
binding = "instance"
|
|
function = descriptor
|
|
skip_first = True
|
|
else:
|
|
return None
|
|
if type(function) is not FunctionType:
|
|
return None
|
|
try:
|
|
parameters = list(_signature(function).parameters.values())
|
|
except (TypeError, ValueError):
|
|
return None
|
|
if skip_first:
|
|
if not parameters:
|
|
return None
|
|
parameters = parameters[1:]
|
|
return {
|
|
"binding": binding,
|
|
"execution_kind": _function_execution_kind(function),
|
|
"parameters": _parameter_records(parameters),
|
|
}
|
|
|
|
|
|
def _function_execution_kind(value: object) -> str:
|
|
if inspect.isasyncgenfunction(value):
|
|
return "async_generator"
|
|
if inspect.iscoroutinefunction(value):
|
|
return "coroutine"
|
|
if inspect.isgeneratorfunction(value):
|
|
return "generator"
|
|
return "sync"
|
|
|
|
|
|
def _sdk_public_class_descriptor(value: type, name: str) -> object | None:
|
|
for owner in value.__mro__:
|
|
namespace = vars(owner)
|
|
if name not in namespace:
|
|
continue
|
|
owner_module = owner.__module__
|
|
if owner is value or (
|
|
isinstance(owner_module, str)
|
|
and (owner_module == "agents" or owner_module.startswith("agents."))
|
|
):
|
|
return cast(object, inspect.getattr_static(value, name))
|
|
return None
|
|
return None
|
|
|
|
|
|
def _public_class_member_contract(value: object) -> dict[str, dict[str, object]]:
|
|
if not issubclass(type(value), type):
|
|
return {}
|
|
class_value = cast(type, value)
|
|
value_identity = f"{class_value.__module__}.{class_value.__qualname__}"
|
|
candidate_names: list[str] = []
|
|
seen_names: set[str] = set()
|
|
|
|
def add_candidate_names(namespace: Mapping[str, object]) -> None:
|
|
for name in namespace:
|
|
if name in seen_names:
|
|
continue
|
|
seen_names.add(name)
|
|
candidate_names.append(name)
|
|
|
|
add_candidate_names(vars(class_value))
|
|
for base in class_value.__mro__[1:]:
|
|
base_module = base.__module__
|
|
if isinstance(base_module, str) and (
|
|
base_module == "agents" or base_module.startswith("agents.")
|
|
):
|
|
add_candidate_names(vars(base))
|
|
members: dict[str, dict[str, object]] = {}
|
|
for name in candidate_names:
|
|
if name.startswith("_"):
|
|
continue
|
|
descriptor = _sdk_public_class_descriptor(class_value, name)
|
|
if descriptor is None:
|
|
continue
|
|
try:
|
|
member = _class_member_contract(descriptor)
|
|
except TypeError as error:
|
|
raise TypeError(
|
|
f"Unable to contract public method {value_identity}.{name}: {error}"
|
|
) from None
|
|
if member is not None:
|
|
members[name] = member
|
|
return members
|
|
|
|
|
|
def _callable_contract(value: Callable[..., Any]) -> dict[str, Any]:
|
|
kind = _callable_kind(value)
|
|
if kind is None:
|
|
raise TypeError(f"Unsupported public callable type: {type(value)!r}")
|
|
contract: dict[str, Any] = {
|
|
"kind": kind,
|
|
"parameters": _parameter_contract(value),
|
|
"dataclass_fields": _dataclass_field_contract(value),
|
|
}
|
|
if kind == "function":
|
|
contract["execution_kind"] = _function_execution_kind(value)
|
|
model_fields = _pydantic_model_field_contract(value)
|
|
if model_fields is not None:
|
|
contract["model_fields"] = model_fields
|
|
enum_members = _enum_member_contract(value)
|
|
if enum_members is not None:
|
|
contract["enum_members"] = enum_members
|
|
if kind == "class":
|
|
contract["members"] = _public_class_member_contract(value)
|
|
return contract
|
|
|
|
|
|
def _merge_canonical_imports(
|
|
existing: Iterable[Mapping[str, str]], promoted: Iterable[Mapping[str, str]]
|
|
) -> list[dict[str, str]]:
|
|
result = [dict(entry) for entry in existing]
|
|
by_identity = {(entry["module"], entry["name"]): entry for entry in result}
|
|
for entry_value in promoted:
|
|
entry = dict(entry_value)
|
|
identity = (entry["module"], entry["name"])
|
|
previous = by_identity.get(identity)
|
|
if previous is not None:
|
|
if previous != entry:
|
|
raise ValueError(
|
|
"release policy canonical import conflicts with the released contract for "
|
|
f"{entry['module']}.{entry['name']}"
|
|
)
|
|
continue
|
|
result.append(entry)
|
|
by_identity[identity] = entry
|
|
return result
|
|
|
|
|
|
def _merge_public_properties(
|
|
existing: Iterable[Mapping[str, Any]], promoted: Iterable[Mapping[str, Any]]
|
|
) -> list[dict[str, Any]]:
|
|
result = [deepcopy(dict(entry)) for entry in existing]
|
|
by_identity = {_public_property_identity(entry): entry for entry in result}
|
|
for entry_value in promoted:
|
|
entry = deepcopy(dict(entry_value))
|
|
identity = _public_property_identity(entry)
|
|
previous = by_identity.get(identity)
|
|
if previous is None:
|
|
result.append(entry)
|
|
by_identity[identity] = entry
|
|
continue
|
|
previous_names = previous["names"]
|
|
for name in entry["names"]:
|
|
if name not in previous_names:
|
|
previous_names.append(name)
|
|
return result
|
|
|
|
|
|
def _merge_public_class_contracts(
|
|
existing: Iterable[Mapping[str, Any]], promoted: Iterable[Mapping[str, Any]]
|
|
) -> list[dict[str, Any]]:
|
|
result = [deepcopy(dict(entry)) for entry in existing]
|
|
by_identity = {(entry["module"], entry["class_name"]): entry for entry in result}
|
|
for entry_value in promoted:
|
|
entry = deepcopy(dict(entry_value))
|
|
identity = (entry["module"], entry["class_name"])
|
|
previous = by_identity.get(identity)
|
|
if previous is None:
|
|
result.append(entry)
|
|
by_identity[identity] = entry
|
|
continue
|
|
for field_name in ("abstract", "abstract_members"):
|
|
if field_name not in entry:
|
|
continue
|
|
previous_value = previous.setdefault(field_name, entry[field_name])
|
|
if previous_value != entry[field_name]:
|
|
raise ValueError(
|
|
"release policy public class contract conflicts with the released contract "
|
|
f"for {entry['module']}.{entry['class_name']} field {field_name}"
|
|
)
|
|
return result
|
|
|
|
|
|
def _public_property_identity(entry: Mapping[str, Any]) -> tuple[str, str, str]:
|
|
if "class_name" in entry:
|
|
return ("class_name", cast(str, entry["module"]), cast(str, entry["class_name"]))
|
|
return ("factory_name", cast(str, entry["module"]), cast(str, entry["factory_name"]))
|
|
|
|
|
|
def _annotation_contract(annotation: object) -> str:
|
|
if isinstance(annotation, ForwardRef):
|
|
annotation_text = annotation.__forward_arg__
|
|
elif isinstance(annotation, str):
|
|
annotation_text = annotation
|
|
else:
|
|
annotation_text = inspect.formatannotation(annotation)
|
|
for wrapper_name in ("Required", "NotRequired"):
|
|
for module_name in ("typing", "typing_extensions"):
|
|
qualified_prefix = f"{module_name}.{wrapper_name}["
|
|
if annotation_text.startswith(qualified_prefix):
|
|
return f"{wrapper_name}[{annotation_text.removeprefix(qualified_prefix)}"
|
|
return annotation_text
|
|
|
|
|
|
def _sorted_type_alias_members(members: Iterable[dict[str, object]]) -> list[dict[str, object]]:
|
|
return sorted(
|
|
members,
|
|
key=lambda member: (
|
|
cast(str, member["kind"]),
|
|
json.dumps(member, sort_keys=True, separators=(",", ":")),
|
|
),
|
|
)
|
|
|
|
|
|
def _is_type_alias_type(value: object) -> bool:
|
|
native_type_alias_type = getattr(typing, "TypeAliasType", typing_extensions.TypeAliasType)
|
|
return isinstance(value, typing_extensions.TypeAliasType | native_type_alias_type)
|
|
|
|
|
|
def _is_type_alias_annotation(annotation: object, module: object) -> bool:
|
|
if annotation is TypeAlias or annotation is typing_extensions.TypeAlias:
|
|
return True
|
|
if not isinstance(annotation, str):
|
|
return False
|
|
reference_parts = annotation.split(".")
|
|
if not reference_parts or not all(part.isidentifier() for part in reference_parts):
|
|
return False
|
|
missing = object()
|
|
resolved = getattr(module, reference_parts[0], missing)
|
|
for part in reference_parts[1:]:
|
|
if resolved is missing:
|
|
break
|
|
resolved = getattr(resolved, part, missing)
|
|
return resolved is TypeAlias or resolved is typing_extensions.TypeAlias
|
|
|
|
|
|
def _module_declares_type_alias(module: object, alias_name: str, value: object) -> bool:
|
|
annotations = getattr(module, "__annotations__", {})
|
|
if not isinstance(annotations, Mapping) or alias_name not in annotations:
|
|
return False
|
|
missing = object()
|
|
return (
|
|
_is_type_alias_annotation(annotations[alias_name], module)
|
|
and getattr(module, alias_name, missing) is value
|
|
)
|
|
|
|
|
|
class _ModuleBindingVisitor(ast.NodeVisitor):
|
|
def __init__(self, name: str):
|
|
self.name = name
|
|
self.count = 0
|
|
self.has_wildcard_import = False
|
|
self.from_imports: list[tuple[ast.ImportFrom, str]] = []
|
|
self._bindings_target_module = True
|
|
|
|
def _count(self, name: str | None) -> None:
|
|
if self._bindings_target_module:
|
|
self.count += name == self.name
|
|
|
|
def _visit_nested_scope(self, body: list[ast.stmt]) -> None:
|
|
bindings_target_module = _scope_declares_global(body, self.name)
|
|
previous_bindings_target_module = self._bindings_target_module
|
|
self._bindings_target_module = bindings_target_module
|
|
for statement in body:
|
|
self.visit(statement)
|
|
self._bindings_target_module = previous_bindings_target_module
|
|
|
|
def _visit_arguments(self, arguments: ast.arguments) -> None:
|
|
all_arguments = [
|
|
*arguments.posonlyargs,
|
|
*arguments.args,
|
|
*arguments.kwonlyargs,
|
|
]
|
|
if arguments.vararg is not None:
|
|
all_arguments.append(arguments.vararg)
|
|
if arguments.kwarg is not None:
|
|
all_arguments.append(arguments.kwarg)
|
|
for argument in all_arguments:
|
|
if argument.annotation is not None:
|
|
self.visit(argument.annotation)
|
|
for default in [*arguments.defaults, *arguments.kw_defaults]:
|
|
if default is not None:
|
|
self.visit(default)
|
|
|
|
def _visit_function_definition(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> None:
|
|
self._count(node.name)
|
|
for decorator in node.decorator_list:
|
|
self.visit(decorator)
|
|
self._visit_arguments(node.args)
|
|
if node.returns is not None:
|
|
self.visit(node.returns)
|
|
|
|
def _visit_comprehension(
|
|
self, generators: list[ast.comprehension], values: list[ast.expr]
|
|
) -> None:
|
|
for generator in generators:
|
|
self.visit(generator.iter)
|
|
for condition in generator.ifs:
|
|
self.visit(condition)
|
|
for value in values:
|
|
self.visit(value)
|
|
|
|
def visit_Name(self, node: ast.Name) -> None:
|
|
if isinstance(node.ctx, ast.Store | ast.Del):
|
|
self._count(node.id)
|
|
|
|
def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
|
|
self._visit_function_definition(node)
|
|
self._visit_nested_scope(node.body)
|
|
|
|
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
|
|
self._visit_function_definition(node)
|
|
self._visit_nested_scope(node.body)
|
|
|
|
def visit_ClassDef(self, node: ast.ClassDef) -> None:
|
|
self._count(node.name)
|
|
for decorator in node.decorator_list:
|
|
self.visit(decorator)
|
|
for base in node.bases:
|
|
self.visit(base)
|
|
for keyword in node.keywords:
|
|
self.visit(keyword.value)
|
|
self._visit_nested_scope(node.body)
|
|
|
|
def visit_Lambda(self, node: ast.Lambda) -> None:
|
|
self._visit_arguments(node.args)
|
|
|
|
def visit_ListComp(self, node: ast.ListComp) -> None:
|
|
self._visit_comprehension(node.generators, [node.elt])
|
|
|
|
def visit_SetComp(self, node: ast.SetComp) -> None:
|
|
self._visit_comprehension(node.generators, [node.elt])
|
|
|
|
def visit_DictComp(self, node: ast.DictComp) -> None:
|
|
self._visit_comprehension(node.generators, [node.key, node.value])
|
|
|
|
def visit_GeneratorExp(self, node: ast.GeneratorExp) -> None:
|
|
self._visit_comprehension(node.generators, [node.elt])
|
|
|
|
def visit_ExceptHandler(self, node: ast.ExceptHandler) -> None:
|
|
self._count(node.name)
|
|
self.generic_visit(node)
|
|
|
|
def visit_MatchAs(self, node: ast.MatchAs) -> None:
|
|
self._count(node.name)
|
|
self.generic_visit(node)
|
|
|
|
def visit_MatchStar(self, node: ast.MatchStar) -> None:
|
|
self._count(node.name)
|
|
|
|
def visit_MatchMapping(self, node: ast.MatchMapping) -> None:
|
|
self._count(node.rest)
|
|
self.generic_visit(node)
|
|
|
|
def visit_Import(self, node: ast.Import) -> None:
|
|
for imported in node.names:
|
|
self._count(imported.asname or imported.name.split(".", 1)[0])
|
|
|
|
def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
|
|
if not self._bindings_target_module:
|
|
return
|
|
for imported in node.names:
|
|
if imported.name == "*":
|
|
self.has_wildcard_import = True
|
|
continue
|
|
binding_name = imported.asname or imported.name
|
|
self._count(binding_name)
|
|
if binding_name == self.name:
|
|
self.from_imports.append((node, imported.name))
|
|
|
|
|
|
def _scope_declares_global(nodes: Iterable[ast.AST], name: str) -> bool:
|
|
for node in nodes:
|
|
if isinstance(node, ast.Global):
|
|
if name in node.names:
|
|
return True
|
|
continue
|
|
if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef | ast.Lambda):
|
|
continue
|
|
if _scope_declares_global(ast.iter_child_nodes(node), name):
|
|
return True
|
|
return False
|
|
|
|
|
|
def _direct_import_source(
|
|
module: object, export_name: str, *, package_root: str
|
|
) -> tuple[ModuleType, str] | None:
|
|
module_name = getattr(module, "__name__", None)
|
|
package_name = getattr(module, "__package__", None)
|
|
if not isinstance(module_name, str) or not isinstance(package_name, str):
|
|
return None
|
|
try:
|
|
module_tree = ast.parse(inspect.getsource(module))
|
|
except (OSError, SyntaxError, TypeError):
|
|
return None
|
|
|
|
bindings = _ModuleBindingVisitor(export_name)
|
|
bindings.visit(module_tree)
|
|
if bindings.count != 1 or bindings.has_wildcard_import or len(bindings.from_imports) != 1:
|
|
return None
|
|
statement, source_name = bindings.from_imports[0]
|
|
if statement.level:
|
|
relative_name = "." * statement.level + (statement.module or "")
|
|
try:
|
|
source_module_name = importlib.util.resolve_name(relative_name, package_name)
|
|
except ImportError:
|
|
return None
|
|
else:
|
|
source_module_name = statement.module
|
|
if source_module_name is None or not (
|
|
source_module_name == package_root or source_module_name.startswith(f"{package_root}.")
|
|
):
|
|
return None
|
|
source_module = sys.modules.get(source_module_name)
|
|
if not isinstance(source_module, ModuleType):
|
|
return None
|
|
return source_module, source_name
|
|
|
|
|
|
def _has_explicit_type_alias_declaration(
|
|
agents_module: object, export_name: str, value: object
|
|
) -> bool:
|
|
package_root = getattr(agents_module, "__name__", None)
|
|
module, alias_name = agents_module, export_name
|
|
visited_bindings: set[tuple[int, str]] = set()
|
|
missing = object()
|
|
while (id(module), alias_name) not in visited_bindings:
|
|
visited_bindings.add((id(module), alias_name))
|
|
if getattr(module, alias_name, missing) is not value:
|
|
return False
|
|
if _module_declares_type_alias(module, alias_name, value):
|
|
return True
|
|
if not isinstance(package_root, str):
|
|
return False
|
|
import_source = _direct_import_source(module, alias_name, package_root=package_root)
|
|
if import_source is None:
|
|
return False
|
|
module, alias_name = import_source
|
|
return False
|
|
|
|
|
|
def _is_public_type_alias(agents_module: object, export_name: str, value: object) -> bool:
|
|
return (
|
|
get_origin(value) is not None
|
|
or _is_type_alias_type(value)
|
|
or _has_explicit_type_alias_declaration(agents_module, export_name, value)
|
|
)
|
|
|
|
|
|
def _type_alias_definition(
|
|
value: object, *, visited_alias_ids: frozenset[int] = frozenset()
|
|
) -> dict[str, object]:
|
|
if value is Any:
|
|
return {"kind": "any"}
|
|
if _is_type_alias_type(value):
|
|
if value.__type_params__:
|
|
raise TypeError(f"generic public type alias is unsupported: {value.__name__}")
|
|
alias_id = id(value)
|
|
if alias_id in visited_alias_ids:
|
|
alias_name = getattr(value, "__name__", repr(value))
|
|
raise TypeError(f"recursive public type alias is unsupported: {alias_name}")
|
|
try:
|
|
alias_value = value.__value__
|
|
except Exception as error:
|
|
raise TypeError(
|
|
f"cannot resolve public type alias {value.__name__} at runtime: "
|
|
f"{type(error).__name__}: {error}"
|
|
) from None
|
|
return _type_alias_definition(alias_value, visited_alias_ids=visited_alias_ids | {alias_id})
|
|
origin = get_origin(value)
|
|
if origin is Literal:
|
|
literal_values: list[dict[str, object]] = []
|
|
for literal_value in get_args(value):
|
|
literal_contract = _default_contract(literal_value)
|
|
if literal_contract["kind"] not in {"literal", "enum"}:
|
|
raise TypeError(
|
|
"public type alias Literal members must use supported literal or enum values"
|
|
)
|
|
literal_values.append(literal_contract)
|
|
return {
|
|
"kind": "literal",
|
|
"values": _sorted_type_alias_members(literal_values),
|
|
}
|
|
if origin in {Union, UnionType}:
|
|
members = [
|
|
_type_alias_definition(member, visited_alias_ids=visited_alias_ids)
|
|
for member in get_args(value)
|
|
]
|
|
return {
|
|
"kind": "union",
|
|
"members": _sorted_type_alias_members(members),
|
|
}
|
|
if origin is Callable:
|
|
callable_args = get_args(value)
|
|
if len(callable_args) != 2:
|
|
raise TypeError(
|
|
"public Callable type aliases must declare parameters and a return type"
|
|
)
|
|
parameter_types, return_type = callable_args
|
|
if parameter_types is Ellipsis or not isinstance(parameter_types, list | tuple):
|
|
raise TypeError("public Callable type aliases must declare explicit parameter types")
|
|
return {
|
|
"kind": "callable",
|
|
"parameters": [
|
|
_type_alias_definition(parameter_type, visited_alias_ids=visited_alias_ids)
|
|
for parameter_type in parameter_types
|
|
],
|
|
"return": _type_alias_definition(return_type, visited_alias_ids=visited_alias_ids),
|
|
}
|
|
if origin is not None:
|
|
if not isinstance(origin, type) or not (
|
|
origin.__module__ == "agents" or origin.__module__.startswith("agents.")
|
|
):
|
|
raise TypeError(f"unsupported public generic type alias origin: {origin!r}")
|
|
return {
|
|
"kind": "generic",
|
|
"origin": f"{origin.__module__}.{origin.__qualname__}",
|
|
"arguments": [
|
|
_type_alias_definition(argument, visited_alias_ids=visited_alias_ids)
|
|
for argument in get_args(value)
|
|
],
|
|
}
|
|
if isinstance(value, type) and (
|
|
value.__module__ == "builtins"
|
|
or value.__module__ == "agents"
|
|
or value.__module__.startswith("agents.")
|
|
):
|
|
return {
|
|
"kind": "type",
|
|
"identity": f"{value.__module__}.{value.__qualname__}",
|
|
}
|
|
raise TypeError(f"unsupported public type alias member: {value!r}")
|
|
|
|
|
|
def _public_type_alias_contract(
|
|
policy_entries: Iterable[Mapping[str, str]],
|
|
agents_module: Any | None,
|
|
) -> list[dict[str, object]]:
|
|
entries: list[dict[str, object]] = []
|
|
missing = object()
|
|
for policy_entry in policy_entries:
|
|
module_name = policy_entry["module"]
|
|
alias_name = policy_entry["name"]
|
|
module = _import_contract_module(module_name, agents_module)
|
|
alias = getattr(module, alias_name, missing)
|
|
if alias is missing:
|
|
raise ValueError(
|
|
f"Cannot promote public type alias {module_name}.{alias_name} because it is missing"
|
|
)
|
|
try:
|
|
definition = _type_alias_definition(alias)
|
|
except TypeError as error:
|
|
raise ValueError(
|
|
f"Cannot promote public type alias {module_name}.{alias_name}: {error}"
|
|
) from None
|
|
entries.append({"definition": definition, "module": module_name, "name": alias_name})
|
|
return entries
|
|
|
|
|
|
def _merge_public_type_aliases(
|
|
existing: Iterable[Mapping[str, Any]], promoted: Iterable[Mapping[str, Any]]
|
|
) -> list[dict[str, Any]]:
|
|
result = [deepcopy(dict(entry)) for entry in existing]
|
|
identities = {(entry["module"], entry["name"]) for entry in result}
|
|
for entry_value in promoted:
|
|
entry = deepcopy(dict(entry_value))
|
|
identity = (entry["module"], entry["name"])
|
|
if identity not in identities:
|
|
result.append(entry)
|
|
identities.add(identity)
|
|
return result
|
|
|
|
|
|
def _typed_dict_field_is_required(typed_dict: type, name: str, annotation: object) -> bool:
|
|
if isinstance(annotation, ForwardRef):
|
|
annotation_text = annotation.__forward_arg__
|
|
if annotation_text.startswith(
|
|
("Required[", "typing.Required[", "typing_extensions.Required[")
|
|
):
|
|
return True
|
|
if annotation_text.startswith(
|
|
("NotRequired[", "typing.NotRequired[", "typing_extensions.NotRequired[")
|
|
):
|
|
return False
|
|
origin = get_origin(annotation)
|
|
if origin is Required:
|
|
return True
|
|
if origin is NotRequired:
|
|
return False
|
|
required_keys = getattr(typed_dict, "__required_keys__", frozenset())
|
|
optional_keys = getattr(typed_dict, "__optional_keys__", frozenset())
|
|
if name in required_keys:
|
|
return True
|
|
if name in optional_keys:
|
|
return False
|
|
return bool(getattr(typed_dict, "__total__", True))
|
|
|
|
|
|
def _typed_dict_field_contract(typed_dict: type, name: str) -> dict[str, object] | None:
|
|
annotation = getattr(typed_dict, "__annotations__", {}).get(name)
|
|
if annotation is None:
|
|
return None
|
|
return {
|
|
"name": name,
|
|
"required": _typed_dict_field_is_required(typed_dict, name, annotation),
|
|
"annotation": _annotation_contract(annotation),
|
|
}
|
|
|
|
|
|
def _public_typed_dict_contract(
|
|
policy_entries: Iterable[Mapping[str, Any]],
|
|
agents_module: Any | None,
|
|
) -> list[dict[str, Any]]:
|
|
entries: list[dict[str, Any]] = []
|
|
for policy_entry in policy_entries:
|
|
module_name = cast(str, policy_entry["module"])
|
|
class_name = cast(str, policy_entry["class_name"])
|
|
module = _import_contract_module(module_name, agents_module)
|
|
typed_dict = getattr(module, class_name, None)
|
|
if not typing_extensions.is_typeddict(typed_dict):
|
|
raise ValueError(
|
|
f"Cannot promote public TypedDict {module_name}.{class_name} because it is "
|
|
"missing or no longer a TypedDict"
|
|
)
|
|
fields: list[dict[str, object]] = []
|
|
for name in policy_entry["names"]:
|
|
field = _typed_dict_field_contract(typed_dict, name)
|
|
if field is None:
|
|
raise ValueError(
|
|
f"Cannot promote public TypedDict field {module_name}.{class_name}.{name} "
|
|
"because it is missing"
|
|
)
|
|
fields.append(field)
|
|
entries.append({"class_name": class_name, "fields": fields, "module": module_name})
|
|
return entries
|
|
|
|
|
|
def _merge_public_typed_dicts(
|
|
existing: Iterable[Mapping[str, Any]], promoted: Iterable[Mapping[str, Any]]
|
|
) -> list[dict[str, Any]]:
|
|
result = [deepcopy(dict(entry)) for entry in existing]
|
|
by_identity = {(entry["module"], entry["class_name"]): entry for entry in result}
|
|
for entry_value in promoted:
|
|
entry = deepcopy(dict(entry_value))
|
|
identity = (entry["module"], entry["class_name"])
|
|
previous = by_identity.get(identity)
|
|
if previous is None:
|
|
result.append(entry)
|
|
by_identity[identity] = entry
|
|
continue
|
|
previous_by_name = {field["name"]: field for field in previous["fields"]}
|
|
for field in entry["fields"]:
|
|
existing_field = previous_by_name.get(field["name"])
|
|
if existing_field is not None and existing_field != field:
|
|
raise ValueError(
|
|
"release policy public TypedDict field conflicts with the released contract "
|
|
f"for {entry['module']}.{entry['class_name']}.{field['name']}"
|
|
)
|
|
if existing_field is None:
|
|
previous["fields"].append(field)
|
|
previous_by_name[field["name"]] = field
|
|
return result
|
|
|
|
|
|
def _optional_dependency_unsupported_platforms(
|
|
contract: Mapping[str, Any],
|
|
) -> dict[str, tuple[str, ...]]:
|
|
value = contract.get("optional_dependency_unsupported_platforms", {})
|
|
if not isinstance(value, dict):
|
|
raise ValueError("optional_dependency_unsupported_platforms must be an object")
|
|
result: dict[str, tuple[str, ...]] = {}
|
|
for dependency_module, platforms in value.items():
|
|
if type(dependency_module) is not str or not dependency_module:
|
|
raise ValueError(
|
|
"optional_dependency_unsupported_platforms keys must be non-empty strings"
|
|
)
|
|
if (
|
|
not isinstance(platforms, list)
|
|
or not all(type(platform) is str and platform for platform in platforms)
|
|
or len(platforms) != len(set(platforms))
|
|
):
|
|
raise ValueError(
|
|
"optional_dependency_unsupported_platforms values must be lists of unique "
|
|
"non-empty strings"
|
|
)
|
|
result[dependency_module] = tuple(platforms)
|
|
return result
|
|
|
|
|
|
def _optional_dependency_is_available_for_contract(
|
|
dependency_module: str,
|
|
unsupported_platforms: Mapping[str, tuple[str, ...]],
|
|
) -> bool:
|
|
return not _optional_dependency_is_unsupported_for_contract(
|
|
dependency_module, unsupported_platforms
|
|
) and _optional_dependency_is_available(dependency_module)
|
|
|
|
|
|
def _optional_dependency_is_unsupported_for_contract(
|
|
dependency_module: str,
|
|
unsupported_platforms: Mapping[str, tuple[str, ...]],
|
|
) -> bool:
|
|
return sys.platform in unsupported_platforms.get(dependency_module, ())
|
|
|
|
|
|
def _optional_dependency_for_binding(
|
|
contract: Mapping[str, Any], module_name: str, binding_name: str
|
|
) -> str | None:
|
|
dependency = _optional_dependency_for_binding_in_modules(
|
|
contract.get("required_submodule_exports", {}), module_name, binding_name
|
|
)
|
|
if dependency is not None:
|
|
return dependency
|
|
canonical_dependencies = {
|
|
_optional_dependency_for_binding_in_modules(
|
|
contract.get("required_submodule_exports", {}), entry["module"], entry["name"]
|
|
)
|
|
for entry in contract.get("canonical_imports", [])
|
|
if entry["canonical_module"] == module_name and entry["canonical_name"] == binding_name
|
|
}
|
|
if canonical_dependencies and len(canonical_dependencies) == 1:
|
|
return next(iter(canonical_dependencies))
|
|
return None
|
|
|
|
|
|
def _optional_dependency_for_binding_in_modules(
|
|
modules: Mapping[str, Any], module_name: str, binding_name: str
|
|
) -> str | None:
|
|
module_contract = modules.get(module_name, {})
|
|
for field_name in ("optional_bindings", "optional_exports"):
|
|
dependency_module = module_contract.get(field_name, {}).get(binding_name)
|
|
if dependency_module is not None:
|
|
return cast(str, dependency_module)
|
|
return None
|
|
|
|
|
|
def _optional_dependency_for_module_import(
|
|
contract: Mapping[str, Any], module_name: str
|
|
) -> str | None:
|
|
modules = contract.get("required_submodule_exports", {})
|
|
module_contract = modules.get(module_name, {})
|
|
names = module_contract.get("names", [])
|
|
try:
|
|
optional_bindings = _optional_dependency_modules(
|
|
module_contract.get("optional_bindings", {}), field_name="optional_bindings"
|
|
)
|
|
optional_exports = _optional_dependency_modules(
|
|
module_contract.get("optional_exports", {}), field_name="optional_exports"
|
|
)
|
|
except ValueError:
|
|
return None
|
|
dependencies = {optional_bindings.get(name) or optional_exports.get(name) for name in names}
|
|
if names and len(dependencies) == 1 and None not in dependencies:
|
|
return cast(str, next(iter(dependencies)))
|
|
if names:
|
|
return None
|
|
canonical_dependencies = {
|
|
_optional_dependency_for_binding(contract, entry["module"], entry["name"])
|
|
for entry in contract.get("canonical_imports", [])
|
|
if entry["canonical_module"] == module_name
|
|
}
|
|
if canonical_dependencies and len(canonical_dependencies) == 1:
|
|
dependency = next(iter(canonical_dependencies))
|
|
if dependency is not None:
|
|
return dependency
|
|
return None
|
|
|
|
|
|
def _preserve_released_callable_for_promotion(
|
|
contract: Mapping[str, Any],
|
|
callables: dict[str, Any],
|
|
qualified_name: str,
|
|
*,
|
|
fail_if_missing: bool,
|
|
unavailable_reason: str,
|
|
) -> None:
|
|
released_callable = contract["callables"].get(qualified_name)
|
|
if released_callable is None:
|
|
if not fail_if_missing:
|
|
return
|
|
raise ValueError(
|
|
f"Cannot promote new canonical callable {qualified_name} because "
|
|
f"{unavailable_reason}. Ensure the binding is available and exposes an inspectable "
|
|
"signature on the release preparation host."
|
|
)
|
|
callables[qualified_name] = deepcopy(released_callable)
|
|
|
|
|
|
def _preserve_released_submodule_callables(
|
|
contract: Mapping[str, Any], callables: dict[str, Any], module_name: str
|
|
) -> None:
|
|
for qualified_name, released_callable in contract["callables"].items():
|
|
callable_module, _, _ = qualified_name.rpartition(".")
|
|
if callable_module == module_name:
|
|
callables.setdefault(qualified_name, deepcopy(released_callable))
|
|
|
|
|
|
def build_released_api_contract(
|
|
contract: dict[str, Any],
|
|
*,
|
|
baseline: str,
|
|
baseline_commit: str,
|
|
agents_module: Any | None = None,
|
|
release_policy: SubmoduleExportPolicy | None = None,
|
|
) -> dict[str, Any]:
|
|
"""Build the next rolling release contract from the current public surface."""
|
|
agents = agents_module or importlib.import_module("agents")
|
|
compatibility_errors = validate_released_api_contract(contract, agents_module=agents)
|
|
if compatibility_errors:
|
|
details = "\n".join(f"- {error}" for error in compatibility_errors)
|
|
raise ValueError(f"Cannot promote an incompatible released API contract:\n{details}")
|
|
|
|
current_exports = list(agents.__all__)
|
|
if not all(type(name) is str for name in current_exports):
|
|
raise ValueError("agents.__all__ must contain only strings")
|
|
if len(current_exports) != len(set(current_exports)):
|
|
raise ValueError("agents.__all__ must not contain duplicate exports")
|
|
|
|
missing_bindings = [name for name in current_exports if not hasattr(agents, name)]
|
|
if missing_bindings:
|
|
raise ValueError(f"agents.__all__ contains missing bindings: {missing_bindings!r}")
|
|
|
|
released_export_order = list(contract["required_top_level_exports"])
|
|
released_exports = set(released_export_order)
|
|
current_export_names = set(current_exports)
|
|
if release_policy is not None:
|
|
promoted_top_level_type_aliases = {
|
|
entry["name"]
|
|
for entry in release_policy.public_type_aliases
|
|
if entry["module"] == "agents"
|
|
}
|
|
missing_top_level_type_aliases = sorted(
|
|
name
|
|
for name in current_export_names - released_exports
|
|
if _is_public_type_alias(agents, name, getattr(agents, name))
|
|
and name not in promoted_top_level_type_aliases
|
|
)
|
|
if missing_top_level_type_aliases:
|
|
raise ValueError(
|
|
"Cannot promote new top-level type aliases without public_type_aliases policy "
|
|
"entries for module 'agents': "
|
|
f"{missing_top_level_type_aliases!r}"
|
|
)
|
|
ordered_exports = [name for name in released_export_order if name in current_export_names]
|
|
ordered_exports.extend(name for name in current_exports if name not in released_exports)
|
|
tracked_callables = set(contract["callables"])
|
|
callables: dict[str, Any] = {}
|
|
for name in ordered_exports:
|
|
value = getattr(agents, name)
|
|
kind = _callable_kind(value)
|
|
should_track = name in tracked_callables
|
|
if not should_track and kind is not None:
|
|
try:
|
|
_signature(value)
|
|
except (TypeError, ValueError):
|
|
continue
|
|
should_track = True
|
|
if should_track:
|
|
callables[name] = _callable_contract(value)
|
|
|
|
canonical_imports = _merge_canonical_imports(
|
|
contract["canonical_imports"],
|
|
release_policy.canonical_imports if release_policy is not None else (),
|
|
)
|
|
policy_unsupported_platforms = (
|
|
{
|
|
installation.dependency_module: installation.unsupported_platforms
|
|
for installation in release_policy.dependency_installations
|
|
if installation.unsupported_platforms
|
|
}
|
|
if release_policy is not None
|
|
else {}
|
|
)
|
|
top_level_callable_ids = {
|
|
id(getattr(agents, name)) for name in callables if not name.startswith("agents.")
|
|
}
|
|
for entry in canonical_imports:
|
|
module_name = entry["module"]
|
|
if module_name == "agents":
|
|
continue
|
|
qualified_name = f"{module_name}.{entry['name']}"
|
|
is_new_canonical_import = entry not in contract["canonical_imports"]
|
|
optional_dependency = (
|
|
_optional_dependency_for_binding_in_modules(
|
|
release_policy.modules, module_name, entry["name"]
|
|
)
|
|
if release_policy is not None
|
|
else None
|
|
)
|
|
if optional_dependency is not None and not _optional_dependency_is_available_for_contract(
|
|
optional_dependency, policy_unsupported_platforms
|
|
):
|
|
if _optional_dependency_is_unsupported_for_contract(
|
|
optional_dependency, policy_unsupported_platforms
|
|
):
|
|
_preserve_released_callable_for_promotion(
|
|
contract,
|
|
callables,
|
|
qualified_name,
|
|
fail_if_missing=is_new_canonical_import,
|
|
unavailable_reason=(
|
|
f"optional dependency {optional_dependency!r} is unsupported on "
|
|
f"{sys.platform!r}"
|
|
),
|
|
)
|
|
continue
|
|
try:
|
|
module = _import_contract_module(module_name, agents_module)
|
|
except Exception as error:
|
|
if _matches_platform_import_error(contract, module_name, error):
|
|
_preserve_released_callable_for_promotion(
|
|
contract,
|
|
callables,
|
|
qualified_name,
|
|
fail_if_missing=is_new_canonical_import,
|
|
unavailable_reason=(
|
|
f"module {module_name!r} has a declared import error on {sys.platform!r}"
|
|
),
|
|
)
|
|
continue
|
|
raise
|
|
value = getattr(module, entry["name"], None)
|
|
if value is None:
|
|
try:
|
|
_import_contract_module(entry["canonical_module"], agents_module)
|
|
except Exception as error:
|
|
if _matches_platform_import_error(contract, entry["canonical_module"], error):
|
|
_preserve_released_callable_for_promotion(
|
|
contract,
|
|
callables,
|
|
qualified_name,
|
|
fail_if_missing=is_new_canonical_import,
|
|
unavailable_reason=(
|
|
f"canonical module {entry['canonical_module']!r} has a declared "
|
|
f"import error on {sys.platform!r}"
|
|
),
|
|
)
|
|
continue
|
|
raise
|
|
continue
|
|
if id(value) in top_level_callable_ids:
|
|
continue
|
|
kind = _callable_kind(value)
|
|
if kind is None:
|
|
continue
|
|
try:
|
|
_signature(value)
|
|
except (TypeError, ValueError):
|
|
continue
|
|
callables[qualified_name] = _callable_contract(value)
|
|
|
|
updated = deepcopy(contract)
|
|
updated["baseline"] = baseline
|
|
updated["required_top_level_exports"] = ordered_exports
|
|
updated["callables"] = callables
|
|
updated["canonical_imports"] = canonical_imports
|
|
updated["public_class_contracts"] = _merge_public_class_contracts(
|
|
contract.get("public_class_contracts", []),
|
|
release_policy.public_class_contracts if release_policy is not None else (),
|
|
)
|
|
updated["public_properties"] = _merge_public_properties(
|
|
contract.get("public_properties", []),
|
|
release_policy.public_properties if release_policy is not None else (),
|
|
)
|
|
updated["public_type_aliases"] = _merge_public_type_aliases(
|
|
contract.get("public_type_aliases", []),
|
|
_public_type_alias_contract(release_policy.public_type_aliases, agents_module)
|
|
if release_policy is not None
|
|
else (),
|
|
)
|
|
updated["public_typed_dicts"] = _merge_public_typed_dicts(
|
|
contract.get("public_typed_dicts", []),
|
|
_public_typed_dict_contract(release_policy.public_typed_dicts, agents_module)
|
|
if release_policy is not None
|
|
else (),
|
|
)
|
|
if release_policy is not None:
|
|
updated["optional_dependency_unsupported_platforms"] = {
|
|
dependency_module: list(platforms)
|
|
for dependency_module, platforms in policy_unsupported_platforms.items()
|
|
}
|
|
excluded_submodule_exports = set(contract.get("submodule_export_exclusions", []))
|
|
public_modules = list(contract["public_modules"])
|
|
submodule_export_policy = release_policy.modules if release_policy is not None else None
|
|
if submodule_export_policy is not None:
|
|
invalid_policy_modules = sorted(
|
|
module_name
|
|
for module_name in submodule_export_policy
|
|
if not module_name.startswith("agents.")
|
|
)
|
|
if invalid_policy_modules:
|
|
raise ValueError(
|
|
"new submodule export policy modules must be under the agents package: "
|
|
f"{invalid_policy_modules!r}"
|
|
)
|
|
released_public_modules = set(public_modules)
|
|
public_modules.extend(sorted(set(submodule_export_policy) - released_public_modules))
|
|
unavailable_policy_dependencies = sorted(
|
|
{
|
|
dependency_module
|
|
for module_policy in submodule_export_policy.values()
|
|
for field_name in ("optional_bindings", "optional_exports")
|
|
for dependency_module in _optional_dependency_modules(
|
|
dict(module_policy.get(field_name, {})), field_name=field_name
|
|
).values()
|
|
if not _optional_dependency_is_unsupported_for_contract(
|
|
dependency_module, policy_unsupported_platforms
|
|
)
|
|
and not _optional_dependency_is_available(dependency_module)
|
|
}
|
|
)
|
|
if unavailable_policy_dependencies:
|
|
raise ValueError(
|
|
"submodule export policy dependency modules are unavailable: "
|
|
f"{unavailable_policy_dependencies!r}. Run `make sync` to install all "
|
|
"optional dependencies, or correct the dependency module names."
|
|
)
|
|
updated["public_modules"] = public_modules
|
|
required_submodule_exports: dict[str, dict[str, Any]] = {}
|
|
released_submodule_exports = contract.get("required_submodule_exports", {})
|
|
for module_name in public_modules:
|
|
if module_name == "agents" or module_name in excluded_submodule_exports:
|
|
continue
|
|
try:
|
|
module = _import_contract_module(module_name, agents_module)
|
|
except Exception as error:
|
|
if _matches_platform_import_error(contract, module_name, error):
|
|
_preserve_released_submodule_callables(contract, callables, module_name)
|
|
continue
|
|
if submodule_export_policy is not None and module_name in submodule_export_policy:
|
|
raise ValueError(
|
|
f"Cannot import submodule export policy module {module_name}: {error!r}"
|
|
) from None
|
|
raise
|
|
if submodule_export_policy is None:
|
|
module_policy = contract.get("required_submodule_exports", {}).get(module_name, {})
|
|
else:
|
|
module_policy = submodule_export_policy.get(module_name, {})
|
|
allowed_missing_optional_exports = {
|
|
name
|
|
for name, dependency_module in _optional_dependency_modules(
|
|
dict(module_policy.get("optional_exports", {})),
|
|
field_name="optional_exports",
|
|
).items()
|
|
if _optional_dependency_is_unsupported_for_contract(
|
|
dependency_module, policy_unsupported_platforms
|
|
)
|
|
}
|
|
module_contract = _submodule_export_contract(
|
|
module,
|
|
optional_bindings=module_policy.get("optional_bindings", {}),
|
|
optional_exports=module_policy.get("optional_exports", {}),
|
|
allowed_missing_optional_exports=allowed_missing_optional_exports,
|
|
)
|
|
if module_contract is not None:
|
|
required_submodule_exports[module_name] = module_contract
|
|
released_names = set(released_submodule_exports.get(module_name, {}).get("names", []))
|
|
for name in module_contract["names"]:
|
|
qualified_name = f"{module_name}.{name}"
|
|
was_tracked = qualified_name in tracked_callables
|
|
if name in released_names and not was_tracked:
|
|
continue
|
|
optional_dependency = _optional_dependency_for_binding_in_modules(
|
|
{module_name: module_contract}, module_name, name
|
|
)
|
|
if optional_dependency is not None and not (
|
|
_optional_dependency_is_available_for_contract(
|
|
optional_dependency, policy_unsupported_platforms
|
|
)
|
|
):
|
|
if was_tracked:
|
|
callables[qualified_name] = deepcopy(contract["callables"][qualified_name])
|
|
continue
|
|
value = getattr(module, name, None)
|
|
if value is None:
|
|
continue
|
|
if not was_tracked and not _is_sdk_owned_callable(value):
|
|
continue
|
|
kind = _callable_kind(value)
|
|
if kind is None:
|
|
continue
|
|
try:
|
|
_signature(value)
|
|
except (TypeError, ValueError):
|
|
if was_tracked:
|
|
callables[qualified_name] = deepcopy(contract["callables"][qualified_name])
|
|
continue
|
|
callables[qualified_name] = _callable_contract(value)
|
|
updated["required_submodule_exports"] = required_submodule_exports
|
|
|
|
updated_errors = validate_released_api_contract(updated, agents_module=agents)
|
|
if updated_errors:
|
|
details = "\n".join(f"- {error}" for error in updated_errors)
|
|
raise ValueError(f"Cannot promote an invalid released API contract:\n{details}")
|
|
|
|
surface_keys = (
|
|
"canonical_imports",
|
|
"callables",
|
|
"optional_dependency_unsupported_platforms",
|
|
"platform_import_errors",
|
|
"public_class_contracts",
|
|
"public_properties",
|
|
"public_type_aliases",
|
|
"public_typed_dicts",
|
|
"public_modules",
|
|
"required_submodule_exports",
|
|
"required_top_level_exports",
|
|
"submodule_export_exclusions",
|
|
)
|
|
surface_changed = any(updated.get(key) != contract.get(key) for key in surface_keys)
|
|
if baseline != contract["baseline"] or surface_changed:
|
|
updated["baseline_commit"] = baseline_commit
|
|
return updated
|
|
|
|
|
|
def _validate_parameter_contract(
|
|
name: str,
|
|
released: list[dict[str, object]],
|
|
current: list[dict[str, object]],
|
|
) -> list[str]:
|
|
errors: list[str] = []
|
|
positional_kinds = {"POSITIONAL_ONLY", "POSITIONAL_OR_KEYWORD"}
|
|
released_positional = [entry for entry in released if entry["kind"] in positional_kinds]
|
|
current_positional = [entry for entry in current if entry["kind"] in positional_kinds]
|
|
if current_positional[: len(released_positional)] != released_positional:
|
|
errors.append(
|
|
f"{name} changed its released positional parameter prefix: "
|
|
f"expected {released_positional!r}, got {current_positional!r}"
|
|
)
|
|
elif any(entry["kind"] == "VAR_POSITIONAL" for entry in released) and len(
|
|
current_positional
|
|
) != len(released_positional):
|
|
added = current_positional[len(released_positional) :]
|
|
errors.append(
|
|
f"{name} added positional parameters before its released variadic parameter: {added!r}"
|
|
)
|
|
|
|
current_by_name = {entry["name"]: entry for entry in current}
|
|
for entry in released:
|
|
if entry["kind"] in positional_kinds:
|
|
continue
|
|
current_entry = current_by_name.get(entry["name"])
|
|
if current_entry != entry:
|
|
errors.append(
|
|
f"{name}.{entry['name']} changed its released parameter contract: "
|
|
f"expected {entry!r}, got {current_entry!r}"
|
|
)
|
|
released_names = {entry["name"] for entry in released}
|
|
for entry in current:
|
|
if entry["name"] in released_names:
|
|
continue
|
|
if entry["kind"] in {"VAR_POSITIONAL", "VAR_KEYWORD"}:
|
|
continue
|
|
default = entry["default"]
|
|
if isinstance(default, dict) and default.get("kind") == "required":
|
|
errors.append(f"{name}.{entry['name']} added a required parameter")
|
|
return errors
|
|
|
|
|
|
def _validate_pydantic_model_field_contract(
|
|
name: str,
|
|
released: list[dict[str, object]],
|
|
current: list[dict[str, object]] | None,
|
|
) -> list[str]:
|
|
errors: list[str] = []
|
|
current_by_name = {cast(str, entry["name"]): entry for entry in current or []}
|
|
for entry in released:
|
|
current_entry = current_by_name.get(cast(str, entry["name"]))
|
|
if current_entry != entry:
|
|
errors.append(
|
|
f"{name}.{entry['name']} changed its released Pydantic model field contract: "
|
|
f"expected {entry!r}, got {current_entry!r}"
|
|
)
|
|
released_names = {entry["name"] for entry in released}
|
|
for entry in current or []:
|
|
if entry["name"] in released_names:
|
|
continue
|
|
default = entry["default"]
|
|
if isinstance(default, dict) and default.get("kind") == "required":
|
|
errors.append(f"{name}.{entry['name']} added a required Pydantic model field")
|
|
return errors
|
|
|
|
|
|
def _import_contract_module(module_name: str, agents_module: Any | None) -> Any:
|
|
if module_name == "agents" and agents_module is not None:
|
|
return agents_module
|
|
return importlib.import_module(module_name)
|
|
|
|
|
|
def _validate_public_property_contract(
|
|
contract: dict[str, Any],
|
|
agents_module: Any | None,
|
|
*,
|
|
unsupported_platforms: Mapping[str, tuple[str, ...]] | None = None,
|
|
) -> list[str]:
|
|
errors: list[str] = []
|
|
unsupported_platforms = unsupported_platforms or {}
|
|
for entry in contract.get("public_properties", []):
|
|
module_name = entry["module"]
|
|
owner_name = entry.get("class_name", entry.get("factory_name"))
|
|
optional_dependency = _optional_dependency_for_binding(contract, module_name, owner_name)
|
|
if optional_dependency is not None and not _optional_dependency_is_available_for_contract(
|
|
optional_dependency, unsupported_platforms
|
|
):
|
|
continue
|
|
try:
|
|
module = _import_contract_module(module_name, agents_module)
|
|
except Exception as error:
|
|
errors.append(f"Failed to import released module {module_name}: {error!r}")
|
|
continue
|
|
if "class_name" in entry:
|
|
class_value = getattr(module, owner_name, None)
|
|
if not isinstance(class_value, type):
|
|
errors.append(f"Missing released public class {module_name}.{owner_name}")
|
|
continue
|
|
else:
|
|
factory = getattr(module, owner_name, None)
|
|
if not callable(factory):
|
|
errors.append(f"Missing released public factory {module_name}.{owner_name}")
|
|
continue
|
|
try:
|
|
class_value = get_type_hints(factory)["return"]
|
|
except (KeyError, NameError, TypeError) as error:
|
|
errors.append(
|
|
f"Unable to resolve released public factory return type "
|
|
f"{module_name}.{owner_name}: {error!r}"
|
|
)
|
|
continue
|
|
if not isinstance(class_value, type):
|
|
errors.append(
|
|
f"Released public factory {module_name}.{owner_name} no longer returns a class"
|
|
)
|
|
continue
|
|
for property_name in entry["names"]:
|
|
descriptor = inspect.getattr_static(class_value, property_name, None)
|
|
if not isinstance(descriptor, property):
|
|
errors.append(
|
|
f"{module_name}.{owner_name}.{property_name} "
|
|
"removed or changed a released public property"
|
|
)
|
|
return errors
|
|
|
|
|
|
def _validate_public_class_contract(
|
|
contract: dict[str, Any],
|
|
agents_module: Any | None,
|
|
*,
|
|
unsupported_platforms: Mapping[str, tuple[str, ...]] | None = None,
|
|
) -> list[str]:
|
|
errors: list[str] = []
|
|
unsupported_platforms = unsupported_platforms or {}
|
|
for entry in contract.get("public_class_contracts", []):
|
|
module_name = entry["module"]
|
|
class_name = entry["class_name"]
|
|
optional_dependency = _optional_dependency_for_binding(contract, module_name, class_name)
|
|
if optional_dependency is not None and not _optional_dependency_is_available_for_contract(
|
|
optional_dependency, unsupported_platforms
|
|
):
|
|
continue
|
|
try:
|
|
module = _import_contract_module(module_name, agents_module)
|
|
except Exception as error:
|
|
errors.append(f"Failed to import released module {module_name}: {error!r}")
|
|
continue
|
|
class_value = getattr(module, class_name, None)
|
|
if not isinstance(class_value, type):
|
|
errors.append(f"Missing released public class {module_name}.{class_name}")
|
|
continue
|
|
if "abstract" in entry and inspect.isabstract(class_value) != entry["abstract"]:
|
|
expected_state = "abstract" if entry["abstract"] else "concrete"
|
|
current_state = "abstract" if inspect.isabstract(class_value) else "concrete"
|
|
errors.append(
|
|
f"{module_name}.{class_name} changed its released public class state: "
|
|
f"expected {expected_state}, got {current_state}"
|
|
)
|
|
if "abstract_members" in entry:
|
|
current_members = sorted(getattr(class_value, "__abstractmethods__", ()))
|
|
if current_members != entry["abstract_members"]:
|
|
errors.append(
|
|
f"{module_name}.{class_name} changed its released public abstract members: "
|
|
f"expected {entry['abstract_members']!r}, got {current_members!r}"
|
|
)
|
|
return errors
|
|
|
|
|
|
def _validate_public_typed_dict_contract(
|
|
contract: dict[str, Any],
|
|
agents_module: Any | None,
|
|
*,
|
|
unsupported_platforms: Mapping[str, tuple[str, ...]] | None = None,
|
|
) -> list[str]:
|
|
errors: list[str] = []
|
|
unsupported_platforms = unsupported_platforms or {}
|
|
for entry in contract.get("public_typed_dicts", []):
|
|
module_name = entry["module"]
|
|
class_name = entry["class_name"]
|
|
optional_dependency = _optional_dependency_for_binding(contract, module_name, class_name)
|
|
if optional_dependency is not None and not _optional_dependency_is_available_for_contract(
|
|
optional_dependency, unsupported_platforms
|
|
):
|
|
continue
|
|
try:
|
|
module = _import_contract_module(module_name, agents_module)
|
|
except Exception as error:
|
|
errors.append(f"Failed to import released module {module_name}: {error!r}")
|
|
continue
|
|
typed_dict = getattr(module, class_name, None)
|
|
if not typing_extensions.is_typeddict(typed_dict):
|
|
errors.append(f"Missing released public TypedDict {module_name}.{class_name}")
|
|
continue
|
|
for released_field in entry["fields"]:
|
|
current_field = _typed_dict_field_contract(typed_dict, released_field["name"])
|
|
if current_field != released_field:
|
|
errors.append(
|
|
f"{module_name}.{class_name}.{released_field['name']} changed its released "
|
|
f"TypedDict field contract: expected {released_field!r}, got "
|
|
f"{current_field!r}"
|
|
)
|
|
return errors
|
|
|
|
|
|
def _validate_public_type_alias_contract(
|
|
contract: dict[str, Any],
|
|
agents_module: Any | None,
|
|
*,
|
|
unsupported_platforms: Mapping[str, tuple[str, ...]] | None = None,
|
|
) -> list[str]:
|
|
errors: list[str] = []
|
|
unsupported_platforms = unsupported_platforms or {}
|
|
missing = object()
|
|
for entry in contract.get("public_type_aliases", []):
|
|
module_name = entry["module"]
|
|
alias_name = entry["name"]
|
|
optional_dependency = _optional_dependency_for_binding(contract, module_name, alias_name)
|
|
if optional_dependency is not None and not _optional_dependency_is_available_for_contract(
|
|
optional_dependency, unsupported_platforms
|
|
):
|
|
continue
|
|
try:
|
|
module = _import_contract_module(module_name, agents_module)
|
|
except Exception as error:
|
|
errors.append(f"Failed to import released module {module_name}: {error!r}")
|
|
continue
|
|
alias = getattr(module, alias_name, missing)
|
|
if alias is missing:
|
|
errors.append(f"Missing released public type alias {module_name}.{alias_name}")
|
|
continue
|
|
try:
|
|
current_definition = _type_alias_definition(alias)
|
|
except TypeError as error:
|
|
errors.append(
|
|
f"{module_name}.{alias_name} no longer has a supported released public type "
|
|
f"alias definition: {error}"
|
|
)
|
|
continue
|
|
if current_definition != entry["definition"]:
|
|
errors.append(
|
|
f"{module_name}.{alias_name} changed its released public type alias: "
|
|
f"expected {entry['definition']!r}, got {current_definition!r}"
|
|
)
|
|
return errors
|
|
|
|
|
|
def _submodule_export_contract(
|
|
module: object,
|
|
*,
|
|
optional_bindings: Mapping[str, str] | None = None,
|
|
optional_exports: Mapping[str, str] | None = None,
|
|
allowed_missing_optional_exports: Iterable[str] = (),
|
|
) -> dict[str, Any] | None:
|
|
exports = getattr(module, "__all__", None)
|
|
if exports is None:
|
|
return None
|
|
if not isinstance(exports, list | tuple) or not all(type(name) is str for name in exports):
|
|
raise ValueError("public module __all__ must contain only strings")
|
|
names = list(exports)
|
|
if len(names) != len(set(names)):
|
|
raise ValueError("public module __all__ must not contain duplicate exports")
|
|
optional_binding_modules = _optional_dependency_modules(
|
|
dict(optional_bindings or {}), field_name="optional_bindings"
|
|
)
|
|
optional_export_modules = _optional_dependency_modules(
|
|
dict(optional_exports or {}), field_name="optional_exports"
|
|
)
|
|
optional_binding_names = set(optional_binding_modules)
|
|
optional_export_names = set(optional_export_modules)
|
|
allowed_missing_names = set(allowed_missing_optional_exports)
|
|
unknown_optional_names = sorted(
|
|
(optional_binding_names | optional_export_names) - set(names) - allowed_missing_names
|
|
)
|
|
if unknown_optional_names:
|
|
raise ValueError(
|
|
f"optional submodule bindings are not exported: {unknown_optional_names!r}"
|
|
)
|
|
names.extend(
|
|
name
|
|
for name in optional_export_modules
|
|
if name in allowed_missing_names and name not in names
|
|
)
|
|
return {
|
|
"names": names,
|
|
"optional_bindings": {
|
|
name: optional_binding_modules[name] for name in names if name in optional_binding_names
|
|
},
|
|
"optional_exports": {
|
|
name: optional_export_modules[name] for name in names if name in optional_export_names
|
|
},
|
|
}
|
|
|
|
|
|
def _optional_dependency_modules(value: object, *, field_name: str) -> dict[str, str]:
|
|
if not isinstance(value, dict):
|
|
raise ValueError(
|
|
f"{field_name} must be an object mapping export names to dependency modules"
|
|
)
|
|
modules: dict[str, str] = {}
|
|
for name, module_name in value.items():
|
|
if type(name) is not str or not name:
|
|
raise ValueError(f"{field_name} export names must be non-empty strings")
|
|
if type(module_name) is not str or not module_name.strip():
|
|
raise ValueError(f"{field_name} dependency for {name!r} must be a non-empty string")
|
|
modules[name] = module_name
|
|
return modules
|
|
|
|
|
|
def _optional_dependency_is_available(module_name: str) -> bool:
|
|
if module_name in sys.modules:
|
|
return sys.modules[module_name] is not None
|
|
return find_spec(module_name) is not None
|
|
|
|
|
|
def _matches_platform_import_error(
|
|
contract: dict[str, Any], module_name: str, error: Exception
|
|
) -> bool:
|
|
allowed_error_types = {"ImportError": ImportError}
|
|
for entry in contract.get("platform_import_errors", []):
|
|
if entry["module"] != module_name or sys.platform not in entry["platforms"]:
|
|
continue
|
|
expected_error_type = allowed_error_types.get(entry["error_type"])
|
|
return (
|
|
expected_error_type is not None
|
|
and type(error) is expected_error_type
|
|
and entry["message_contains"] in str(error)
|
|
)
|
|
return False
|
|
|
|
|
|
def validate_released_api_contract(
|
|
contract: dict[str, Any],
|
|
*,
|
|
agents_module: Any | None = None,
|
|
) -> list[str]:
|
|
agents = agents_module or importlib.import_module("agents")
|
|
errors: list[str] = []
|
|
|
|
try:
|
|
unsupported_platforms = _optional_dependency_unsupported_platforms(contract)
|
|
except ValueError as error:
|
|
errors.append(f"Invalid released optional dependency platform declarations: {error}")
|
|
unsupported_platforms = {}
|
|
|
|
errors.extend(
|
|
_validate_public_class_contract(
|
|
contract,
|
|
agents_module,
|
|
unsupported_platforms=unsupported_platforms,
|
|
)
|
|
)
|
|
errors.extend(
|
|
_validate_public_property_contract(
|
|
contract,
|
|
agents_module,
|
|
unsupported_platforms=unsupported_platforms,
|
|
)
|
|
)
|
|
errors.extend(
|
|
_validate_public_type_alias_contract(
|
|
contract,
|
|
agents_module,
|
|
unsupported_platforms=unsupported_platforms,
|
|
)
|
|
)
|
|
errors.extend(
|
|
_validate_public_typed_dict_contract(
|
|
contract,
|
|
agents_module,
|
|
unsupported_platforms=unsupported_platforms,
|
|
)
|
|
)
|
|
|
|
missing_exports = sorted(set(contract["required_top_level_exports"]) - set(agents.__all__))
|
|
if missing_exports:
|
|
errors.append(f"Missing released top-level exports: {missing_exports!r}")
|
|
missing_bindings = sorted(
|
|
name for name in contract["required_top_level_exports"] if not hasattr(agents, name)
|
|
)
|
|
if missing_bindings:
|
|
errors.append(f"Missing released top-level bindings: {missing_bindings!r}")
|
|
|
|
imported_modules: dict[str, object] = {"agents": agents}
|
|
for module_name in contract["public_modules"]:
|
|
try:
|
|
imported_modules[module_name] = _import_contract_module(module_name, agents_module)
|
|
except Exception as error:
|
|
if _matches_platform_import_error(contract, module_name, error):
|
|
continue
|
|
optional_dependency = _optional_dependency_for_module_import(contract, module_name)
|
|
if optional_dependency is not None and not (
|
|
_optional_dependency_is_available_for_contract(
|
|
optional_dependency, unsupported_platforms
|
|
)
|
|
):
|
|
continue
|
|
errors.append(f"Failed to import released module {module_name}: {error!r}")
|
|
|
|
for module_name, released in contract.get("required_submodule_exports", {}).items():
|
|
module = imported_modules.get(module_name)
|
|
if module is None:
|
|
continue
|
|
try:
|
|
current = _submodule_export_contract(module)
|
|
except ValueError as error:
|
|
errors.append(f"Invalid released module exports for {module_name}: {error}")
|
|
continue
|
|
if current is None:
|
|
errors.append(f"Released module {module_name} no longer defines __all__")
|
|
continue
|
|
try:
|
|
optional_exports = _optional_dependency_modules(
|
|
released.get("optional_exports", {}), field_name="optional_exports"
|
|
)
|
|
optional_bindings = _optional_dependency_modules(
|
|
released.get("optional_bindings", {}), field_name="optional_bindings"
|
|
)
|
|
except ValueError as error:
|
|
errors.append(
|
|
f"Invalid released {module_name} optional dependency declarations: {error}"
|
|
)
|
|
continue
|
|
unknown_optional_names = sorted(
|
|
(set(optional_bindings) | set(optional_exports)) - set(released["names"])
|
|
)
|
|
if unknown_optional_names:
|
|
errors.append(
|
|
f"Invalid released {module_name} optional dependency declarations: "
|
|
f"names are not exported: {unknown_optional_names!r}"
|
|
)
|
|
continue
|
|
try:
|
|
unsupported_optional_exports = {
|
|
name
|
|
for name, dependency_module in optional_exports.items()
|
|
if _optional_dependency_is_unsupported_for_contract(
|
|
dependency_module, unsupported_platforms
|
|
)
|
|
}
|
|
unsupported_optional_bindings = {
|
|
name
|
|
for name, dependency_module in (optional_bindings | optional_exports).items()
|
|
if _optional_dependency_is_unsupported_for_contract(
|
|
dependency_module, unsupported_platforms
|
|
)
|
|
}
|
|
unavailable_optional_exports = {
|
|
name
|
|
for name, dependency_module in optional_exports.items()
|
|
if not _optional_dependency_is_available_for_contract(
|
|
dependency_module, unsupported_platforms
|
|
)
|
|
}
|
|
unavailable_optional_bindings = {
|
|
name
|
|
for name, dependency_module in (optional_bindings | optional_exports).items()
|
|
if not _optional_dependency_is_available_for_contract(
|
|
dependency_module, unsupported_platforms
|
|
)
|
|
}
|
|
except (AttributeError, ImportError, ValueError) as error:
|
|
errors.append(
|
|
f"Unable to inspect released {module_name} optional dependencies: {error!r}"
|
|
)
|
|
continue
|
|
current_names = set(current["names"])
|
|
for name in sorted(unavailable_optional_exports & current_names):
|
|
try:
|
|
getattr(module, name)
|
|
except (AttributeError, ImportError):
|
|
if name in unsupported_optional_exports:
|
|
errors.append(
|
|
f"Invalid released {module_name} optional dependency declaration: "
|
|
f"{name!r} remains in __all__ on an unsupported platform but its "
|
|
"binding is unavailable"
|
|
)
|
|
else:
|
|
errors.append(
|
|
f"Invalid released {module_name} optional dependency declaration: "
|
|
f"{name!r} remains in __all__ but its binding is unavailable; "
|
|
"declare it in optional_bindings instead of optional_exports"
|
|
)
|
|
else:
|
|
if name not in unsupported_optional_exports:
|
|
errors.append(
|
|
f"Invalid released {module_name} optional dependency declaration: "
|
|
f"{name!r} remains in __all__ and its binding resolves; remove its "
|
|
"optional declaration or correct its dependency module"
|
|
)
|
|
binding_only_names = set(optional_bindings) - set(optional_exports)
|
|
for name in sorted(unavailable_optional_bindings & binding_only_names):
|
|
if name not in current_names:
|
|
errors.append(
|
|
f"Invalid released {module_name} optional dependency declaration: "
|
|
f"{name!r} is absent from __all__; declare it in optional_exports "
|
|
"instead of optional_bindings"
|
|
)
|
|
continue
|
|
try:
|
|
getattr(module, name)
|
|
except (AttributeError, ImportError):
|
|
if name in unsupported_optional_bindings:
|
|
errors.append(
|
|
f"Invalid released {module_name} optional dependency declaration: "
|
|
f"{name!r} remains in __all__ on an unsupported platform but its "
|
|
"binding is unavailable"
|
|
)
|
|
else:
|
|
if name not in unsupported_optional_bindings:
|
|
errors.append(
|
|
f"Invalid released {module_name} optional dependency declaration: "
|
|
f"{name!r} remains in __all__ and its binding resolves; remove its "
|
|
"optional declaration or correct its dependency module"
|
|
)
|
|
missing_names = sorted(
|
|
set(released["names"]) - unavailable_optional_exports - current_names
|
|
)
|
|
if missing_names:
|
|
errors.append(f"Missing released {module_name} exports: {missing_names!r}")
|
|
missing_required_bindings = []
|
|
for name in released["names"]:
|
|
if name in unavailable_optional_bindings:
|
|
continue
|
|
try:
|
|
getattr(module, name)
|
|
except (AttributeError, ImportError):
|
|
missing_required_bindings.append(name)
|
|
if missing_required_bindings:
|
|
errors.append(
|
|
f"Missing released {module_name} bindings: {sorted(missing_required_bindings)!r}"
|
|
)
|
|
|
|
for entry in contract["canonical_imports"]:
|
|
optional_dependency = _optional_dependency_for_binding(
|
|
contract, entry["module"], entry["name"]
|
|
)
|
|
if optional_dependency is not None and not _optional_dependency_is_available_for_contract(
|
|
optional_dependency, unsupported_platforms
|
|
):
|
|
continue
|
|
try:
|
|
module = _import_contract_module(entry["module"], agents_module)
|
|
except Exception as error:
|
|
if _matches_platform_import_error(contract, entry["module"], error):
|
|
continue
|
|
errors.append(f"Failed to import released module {entry['module']}: {error!r}")
|
|
continue
|
|
try:
|
|
canonical = _import_contract_module(entry["canonical_module"], agents_module)
|
|
except Exception as error:
|
|
if _matches_platform_import_error(contract, entry["canonical_module"], error):
|
|
continue
|
|
errors.append(
|
|
f"Failed to import released module {entry['canonical_module']}: {error!r}"
|
|
)
|
|
continue
|
|
missing = object()
|
|
actual = getattr(module, entry["name"], missing)
|
|
expected = getattr(canonical, entry["canonical_name"], missing)
|
|
if actual is missing or expected is missing or actual is not expected:
|
|
errors.append(
|
|
f"{entry['module']}.{entry['name']} no longer resolves to "
|
|
f"{entry['canonical_module']}.{entry['canonical_name']}"
|
|
)
|
|
|
|
for name, released in contract["callables"].items():
|
|
if name.startswith("agents."):
|
|
module_name, _, binding_name = name.rpartition(".")
|
|
optional_dependency = _optional_dependency_for_binding(
|
|
contract, module_name, binding_name
|
|
)
|
|
if optional_dependency is not None and not (
|
|
_optional_dependency_is_available_for_contract(
|
|
optional_dependency, unsupported_platforms
|
|
)
|
|
):
|
|
continue
|
|
try:
|
|
module = _import_contract_module(module_name, agents_module)
|
|
except Exception as error:
|
|
if _matches_platform_import_error(contract, module_name, error):
|
|
continue
|
|
errors.append(f"Failed to import released module {module_name}: {error!r}")
|
|
continue
|
|
value = getattr(module, binding_name, None)
|
|
if value is None:
|
|
canonical_entry = next(
|
|
(
|
|
entry
|
|
for entry in contract["canonical_imports"]
|
|
if entry["module"] == module_name and entry["name"] == binding_name
|
|
),
|
|
None,
|
|
)
|
|
if canonical_entry is not None:
|
|
try:
|
|
_import_contract_module(canonical_entry["canonical_module"], agents_module)
|
|
except Exception as error:
|
|
if _matches_platform_import_error(
|
|
contract, canonical_entry["canonical_module"], error
|
|
):
|
|
continue
|
|
else:
|
|
module_name = "agents"
|
|
binding_name = name
|
|
value = getattr(agents, binding_name, None)
|
|
if value is None:
|
|
errors.append(f"Missing released callable {module_name}.{binding_name}")
|
|
continue
|
|
current_kind = _callable_kind(value)
|
|
if current_kind != released["kind"]:
|
|
errors.append(
|
|
f"Released callable {module_name}.{binding_name} changed kind from "
|
|
f"{released['kind']} to {current_kind or type(value).__name__}"
|
|
)
|
|
continue
|
|
released_execution_kind = released.get("execution_kind")
|
|
if released_execution_kind is not None:
|
|
current_execution_kind = _function_execution_kind(value)
|
|
if current_execution_kind != released_execution_kind:
|
|
errors.append(
|
|
f"{name} changed execution from "
|
|
f"{released_execution_kind} to {current_execution_kind}"
|
|
)
|
|
current_parameters = _parameter_contract(value)
|
|
errors.extend(
|
|
_validate_parameter_contract(name, released["parameters"], current_parameters)
|
|
)
|
|
current_fields = _dataclass_field_contract(value)
|
|
released_fields = released["dataclass_fields"]
|
|
if current_fields[: len(released_fields)] != released_fields:
|
|
errors.append(
|
|
f"{name} changed its released dataclass field prefix: "
|
|
f"expected {released_fields!r}, got {current_fields!r}"
|
|
)
|
|
for field in current_fields[len(released_fields) :]:
|
|
default = field["default"]
|
|
if field["init"] and isinstance(default, dict) and default.get("kind") == "required":
|
|
errors.append(f"{name}.{field['name']} added a required dataclass field")
|
|
released_model_fields = released.get("model_fields")
|
|
if released_model_fields is not None:
|
|
errors.extend(
|
|
_validate_pydantic_model_field_contract(
|
|
name,
|
|
cast(list[dict[str, object]], released_model_fields),
|
|
_pydantic_model_field_contract(value),
|
|
)
|
|
)
|
|
for member_name, released_member in released.get("members", {}).items():
|
|
descriptor = _sdk_public_class_descriptor(value, member_name)
|
|
current_member = _class_member_contract(descriptor)
|
|
if current_member is None:
|
|
errors.append(f"{name}.{member_name} removed a released public method")
|
|
continue
|
|
if current_member["binding"] != released_member["binding"]:
|
|
errors.append(
|
|
f"{name}.{member_name} changed binding from "
|
|
f"{released_member['binding']} to {current_member['binding']}"
|
|
)
|
|
continue
|
|
released_execution_kind = released_member.get("execution_kind")
|
|
if (
|
|
released_execution_kind is not None
|
|
and current_member["execution_kind"] != released_execution_kind
|
|
):
|
|
errors.append(
|
|
f"{name}.{member_name} changed execution from "
|
|
f"{released_execution_kind} to {current_member['execution_kind']}"
|
|
)
|
|
errors.extend(
|
|
_validate_parameter_contract(
|
|
f"{name}.{member_name}",
|
|
released_member["parameters"],
|
|
cast(list[dict[str, object]], current_member["parameters"]),
|
|
)
|
|
)
|
|
released_enum_members = released.get("enum_members")
|
|
if released_enum_members is not None:
|
|
current_enum_members = _enum_member_contract(value)
|
|
if current_enum_members is None:
|
|
errors.append(f"{name} is no longer an enum")
|
|
continue
|
|
current_enum_members_by_name = {
|
|
member["name"]: member["value"] for member in current_enum_members
|
|
}
|
|
for member in released_enum_members:
|
|
member_name = member["name"]
|
|
if member_name not in current_enum_members_by_name:
|
|
errors.append(f"{name}.{member_name} removed or renamed a released enum member")
|
|
continue
|
|
current_value = current_enum_members_by_name[member_name]
|
|
if current_value != member["value"]:
|
|
errors.append(
|
|
f"{name}.{member_name} changed its released enum value: "
|
|
f"expected {member['value']!r}, got {current_value!r}"
|
|
)
|
|
|
|
return errors
|
|
|
|
|
|
def _normalized_durable_state(payload: dict[str, Any]) -> dict[str, Any]:
|
|
normalized = deepcopy(payload)
|
|
normalized.pop("$schemaVersion", None)
|
|
return normalized
|
|
|
|
|
|
def _normalize_legacy_mount_credentials(payload: dict[str, Any]) -> dict[str, Any]:
|
|
from agents.sandbox._mount_security import REDACTED_MOUNT_AUTHORITY_KEY
|
|
|
|
normalized = deepcopy(payload)
|
|
sandbox = cast(dict[str, Any], normalized["sandbox"])
|
|
session_states = [cast(dict[str, Any], sandbox["session_state"])]
|
|
sessions_by_agent = cast(dict[str, dict[str, Any]], sandbox["sessions_by_agent"])
|
|
session_states.extend(
|
|
cast(dict[str, Any], entry["session_state"]) for entry in sessions_by_agent.values()
|
|
)
|
|
for session_state in session_states:
|
|
manifest = cast(dict[str, Any], session_state["manifest"])
|
|
entries = cast(dict[str, dict[str, Any]], manifest["entries"])
|
|
mount = entries["remote"]
|
|
mount["access_key_id"] = None
|
|
mount["secret_access_key"] = None
|
|
mount["session_token"] = None
|
|
strategy = cast(dict[str, Any], mount["mount_strategy"])
|
|
strategy["driver_options"] = {}
|
|
session_state[REDACTED_MOUNT_AUTHORITY_KEY] = True
|
|
return normalized
|
|
|
|
|
|
def _legacy_driver_option_errors(payload: dict[str, Any]) -> list[str]:
|
|
sandbox = cast(dict[str, Any], payload["sandbox"])
|
|
session_states = [("sandbox.session_state", cast(dict[str, Any], sandbox["session_state"]))]
|
|
sessions_by_agent = cast(dict[str, dict[str, Any]], sandbox["sessions_by_agent"])
|
|
session_states.extend(
|
|
(
|
|
f"sandbox.sessions_by_agent.{agent_id}.session_state",
|
|
cast(dict[str, Any], entry["session_state"]),
|
|
)
|
|
for agent_id, entry in sessions_by_agent.items()
|
|
)
|
|
errors: list[str] = []
|
|
for path, session_state in session_states:
|
|
manifest = cast(dict[str, Any], session_state["manifest"])
|
|
entries = cast(dict[str, dict[str, Any]], manifest["entries"])
|
|
mount = entries["remote"]
|
|
strategy = cast(dict[str, Any], mount["mount_strategy"])
|
|
if strategy.get("driver_options") != {}:
|
|
errors.append(f"{path}.manifest.entries.remote.mount_strategy.driver_options remained")
|
|
return errors
|
|
|
|
|
|
def _find_subset_errors(expected: object, actual: object, path: str = "state") -> list[str]:
|
|
if isinstance(expected, dict):
|
|
if not isinstance(actual, dict):
|
|
return [f"{path} changed type from mapping to {type(actual).__name__}"]
|
|
errors: list[str] = []
|
|
for key, value in expected.items():
|
|
if key not in actual:
|
|
errors.append(f"{path}.{key} was dropped")
|
|
continue
|
|
errors.extend(_find_subset_errors(value, actual[key], f"{path}.{key}"))
|
|
return errors
|
|
if isinstance(expected, list):
|
|
if not isinstance(actual, list):
|
|
return [f"{path} changed type from list to {type(actual).__name__}"]
|
|
if len(expected) != len(actual):
|
|
return [f"{path} changed length from {len(expected)} to {len(actual)}"]
|
|
errors = []
|
|
for index, (expected_item, actual_item) in enumerate(zip(expected, actual, strict=True)):
|
|
errors.extend(_find_subset_errors(expected_item, actual_item, f"{path}[{index}]"))
|
|
return errors
|
|
if type(expected) is not type(actual):
|
|
return [f"{path} changed type from {type(expected).__name__} to {type(actual).__name__}"]
|
|
if expected != actual:
|
|
return [f"{path} changed from {expected!r} to {actual!r}"]
|
|
return []
|
|
|
|
|
|
def _restore_agent(payload: dict[str, Any]) -> Any:
|
|
from agents import Agent, handoff
|
|
|
|
current_agent = payload.get("current_agent")
|
|
name = (
|
|
current_agent.get("name", "compat-agent")
|
|
if isinstance(current_agent, dict)
|
|
else "compat-agent"
|
|
)
|
|
identity = current_agent.get("identity") if isinstance(current_agent, dict) else None
|
|
if identity == f"{name}#2":
|
|
duplicate = Agent(name=name)
|
|
return Agent(name=name, handoffs=[handoff(duplicate)])
|
|
return Agent(name=name)
|
|
|
|
|
|
async def validate_historical_run_state_fixture(path: Path) -> list[str]:
|
|
from agents import RunState
|
|
from agents.run_state import CURRENT_SCHEMA_VERSION
|
|
|
|
errors: list[str] = []
|
|
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
historical = deepcopy(payload)
|
|
original_version = historical.get("$schemaVersion")
|
|
agent = _restore_agent(historical)
|
|
restored = await RunState.from_json(agent, payload)
|
|
canonical = restored.to_json()
|
|
|
|
if canonical.get("$schemaVersion") != CURRENT_SCHEMA_VERSION:
|
|
errors.append(
|
|
f"{path.name} rewrote as {canonical.get('$schemaVersion')!r}, "
|
|
f"expected {CURRENT_SCHEMA_VERSION!r}"
|
|
)
|
|
semantic_errors = _find_subset_errors(
|
|
_normalized_durable_state(historical),
|
|
_normalized_durable_state(canonical),
|
|
)
|
|
errors.extend(f"{path.name}: {error}" for error in semantic_errors)
|
|
|
|
expected_canonical = deepcopy(canonical)
|
|
rerestored = await RunState.from_json(agent, deepcopy(canonical))
|
|
recanonical = rerestored.to_json()
|
|
if recanonical != expected_canonical:
|
|
errors.append(
|
|
f"{path.name} was not idempotent after rewriting schema {original_version!r} "
|
|
f"to {CURRENT_SCHEMA_VERSION!r}"
|
|
)
|
|
return errors
|
|
|
|
|
|
async def validate_historical_resume_behavior(
|
|
path: Path,
|
|
*,
|
|
feature: str,
|
|
decision: str | None = None,
|
|
) -> list[str]:
|
|
from openai.types.responses import (
|
|
ResponseFunctionToolCall,
|
|
ResponseOutputMessage,
|
|
ResponseOutputText,
|
|
)
|
|
|
|
from agents import Agent, Runner, RunState, function_tool
|
|
from agents.items import ToolCallOutputItem, TResponseOutputItem
|
|
from agents.testing import ModelStep, ScriptedModel
|
|
|
|
invocation_count = 0
|
|
if feature == "canonical_invocation_identity":
|
|
|
|
def lookup_account(account_id: str) -> str:
|
|
nonlocal invocation_count
|
|
invocation_count += 1
|
|
return f"approved:{account_id}"
|
|
|
|
tool = function_tool(lookup_account, needs_approval=True)
|
|
model_turns: list[list[TResponseOutputItem]] = [
|
|
[
|
|
ResponseFunctionToolCall(
|
|
type="function_call",
|
|
name="lookup_account",
|
|
call_id="function-request-1",
|
|
status="completed",
|
|
arguments='{"account_id":"account-1"}',
|
|
)
|
|
]
|
|
]
|
|
expected_invocations = 1
|
|
expected_tool_output = "approved:account-1"
|
|
elif feature == "pending_tool_approval":
|
|
|
|
def historical_approval(account_id: str) -> str:
|
|
nonlocal invocation_count
|
|
invocation_count += 1
|
|
return f"approved:{account_id}"
|
|
|
|
tool = function_tool(historical_approval, needs_approval=True)
|
|
model_turns = []
|
|
if decision == "approve":
|
|
expected_invocations = 1
|
|
expected_tool_output = "approved:account-1"
|
|
elif decision == "reject":
|
|
expected_invocations = 0
|
|
expected_tool_output = "Candidate rejected historical approval"
|
|
else:
|
|
raise ValueError("pending_tool_approval requires an approve or reject decision")
|
|
else:
|
|
raise ValueError(f"Unsupported historical resume feature: {feature}")
|
|
|
|
final_message = ResponseOutputMessage(
|
|
id="historical-resume-final",
|
|
type="message",
|
|
role="assistant",
|
|
status="completed",
|
|
content=[
|
|
ResponseOutputText(
|
|
type="output_text",
|
|
text="resume complete",
|
|
annotations=[],
|
|
logprobs=[],
|
|
)
|
|
],
|
|
)
|
|
model_turns.append([final_message])
|
|
model = ScriptedModel(
|
|
[ModelStep(output=turn, response_id="queued-fake-response") for turn in model_turns]
|
|
)
|
|
agent = Agent(name="compat-agent", model=model, tools=[tool])
|
|
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
restored = await RunState.from_json(agent, payload)
|
|
if feature == "pending_tool_approval":
|
|
interruptions = restored.get_interruptions()
|
|
if len(interruptions) != 1:
|
|
return [f"{path.name} did not restore its historical pending approval"]
|
|
if decision == "approve":
|
|
restored.approve(interruptions[0])
|
|
else:
|
|
restored.reject(
|
|
interruptions[0],
|
|
rejection_message="Candidate rejected historical approval",
|
|
)
|
|
result = await Runner.run(agent, restored)
|
|
|
|
errors: list[str] = []
|
|
if result.interruptions:
|
|
errors.append(f"{path.name} interrupted instead of applying its historical decision")
|
|
if invocation_count != expected_invocations:
|
|
errors.append(
|
|
f"{path.name} invoked its approval-controlled tool {invocation_count} times, "
|
|
f"expected {expected_invocations}"
|
|
)
|
|
tool_outputs = [
|
|
item.output for item in result.new_items if isinstance(item, ToolCallOutputItem)
|
|
]
|
|
if expected_tool_output not in tool_outputs:
|
|
errors.append(
|
|
f"{path.name} did not preserve the historical tool decision output "
|
|
f"{expected_tool_output!r}"
|
|
)
|
|
if result.final_output != "resume complete":
|
|
errors.append(f"{path.name} did not complete its resumed run")
|
|
return errors
|
|
|
|
|
|
async def validate_legacy_credential_run_state_fixture(
|
|
path: Path,
|
|
*,
|
|
sentinels: Iterable[str],
|
|
) -> list[str]:
|
|
from agents import RunState
|
|
from agents.run_state import CURRENT_SCHEMA_VERSION
|
|
|
|
errors: list[str] = []
|
|
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
historical = deepcopy(payload)
|
|
agent = _restore_agent(payload)
|
|
restored = await RunState.from_json(agent, payload)
|
|
canonical = restored.to_json()
|
|
|
|
if canonical.get("$schemaVersion") != CURRENT_SCHEMA_VERSION:
|
|
errors.append(
|
|
f"{path.name} rewrote as {canonical.get('$schemaVersion')!r}, "
|
|
f"expected {CURRENT_SCHEMA_VERSION!r}"
|
|
)
|
|
semantic_errors = _find_subset_errors(
|
|
_normalized_durable_state(_normalize_legacy_mount_credentials(historical)),
|
|
_normalized_durable_state(canonical),
|
|
)
|
|
errors.extend(f"{path.name}: {error}" for error in semantic_errors)
|
|
if not semantic_errors:
|
|
errors.extend(f"{path.name}: {error}" for error in _legacy_driver_option_errors(canonical))
|
|
|
|
serialized_observables = json.dumps(canonical, sort_keys=True) + repr(restored._sandbox)
|
|
for sentinel in sentinels:
|
|
if sentinel in serialized_observables:
|
|
errors.append(f"{path.name} retained credential sentinel {sentinel!r}")
|
|
|
|
expected_canonical = deepcopy(canonical)
|
|
rerestored = await RunState.from_json(agent, deepcopy(canonical))
|
|
if rerestored.to_json() != expected_canonical:
|
|
errors.append(f"{path.name} was not idempotent after credential sanitization")
|
|
return errors
|