diff --git a/python/packages/core/agent_framework/__init__.py b/python/packages/core/agent_framework/__init__.py index 60150f3ca1..28fc1be881 100644 --- a/python/packages/core/agent_framework/__init__.py +++ b/python/packages/core/agent_framework/__init__.py @@ -149,7 +149,6 @@ "._harness._todo": ( "DEFAULT_TODO_SOURCE_ID", "TodoFileStore", - "TodoInput", "TodoItem", "TodoProvider", "TodoSessionStore", @@ -546,7 +545,6 @@ "SwitchCaseEdgeGroupDefault", "TextSpanRegion", "TodoFileStore", - "TodoInput", "TodoItem", "TodoProvider", "TodoSessionStore", diff --git a/python/packages/core/agent_framework/__init__.pyi b/python/packages/core/agent_framework/__init__.pyi index ee72c15cc0..d4547535e8 100644 --- a/python/packages/core/agent_framework/__init__.pyi +++ b/python/packages/core/agent_framework/__init__.pyi @@ -108,7 +108,6 @@ from ._harness._mode import DEFAULT_MODE_SOURCE_ID, AgentModeProvider, get_agent from ._harness._todo import ( DEFAULT_TODO_SOURCE_ID, TodoFileStore, - TodoInput, TodoItem, TodoProvider, TodoSessionStore, @@ -513,7 +512,6 @@ __all__ = [ "SwitchCaseEdgeGroupDefault", "TextSpanRegion", "TodoFileStore", - "TodoInput", "TodoItem", "TodoProvider", "TodoSessionStore", diff --git a/python/packages/core/agent_framework/_harness/_mode.py b/python/packages/core/agent_framework/_harness/_mode.py index 5fdc88501c..a3ab0ad620 100644 --- a/python/packages/core/agent_framework/_harness/_mode.py +++ b/python/packages/core/agent_framework/_harness/_mode.py @@ -6,7 +6,6 @@ from collections.abc import Mapping, Sequence from typing import Any, cast -from .._feature_stage import ExperimentalFeature, experimental from .._sessions import AgentSession, ContextProvider, SessionContext from .._tools import tool from .._types import Message @@ -29,7 +28,7 @@ '[Mode changed: The operating mode has been switched from "{previous_mode}" to "{current_mode}". ' 'You must now adjust your behavior to match the "{current_mode}" mode.]' ) -DEFAULT_MODE_DESCRIPTIONS: dict[str, str] = { +DEFAULT_MODE_MAP: dict[str, str] = { "plan": ( "Use this mode when analyzing requirements, breaking down tasks, and creating plans. " "This is the interactive mode — ask clarifying questions, discuss options, and get user approval before " @@ -115,7 +114,6 @@ def _resolve_default_mode(default_mode: str | None, *, available_modes: Mapping[ return _normalize_mode(default_mode, available_modes=available_modes) -@experimental(feature_id=ExperimentalFeature.HARNESS) def get_agent_mode( session: AgentSession, *, @@ -137,7 +135,7 @@ def get_agent_mode( Returns: The current mode string. """ - normalized_modes = _normalize_available_modes(tuple(available_modes or DEFAULT_MODE_DESCRIPTIONS)) + normalized_modes = _normalize_available_modes(tuple(available_modes or DEFAULT_MODE_MAP)) normalized_default_mode = _resolve_default_mode(default_mode, available_modes=normalized_modes) provider_state = _get_mode_state(session, source_id=source_id) current_mode = provider_state.get("current_mode") @@ -152,7 +150,6 @@ def get_agent_mode( return normalized_default_mode -@experimental(feature_id=ExperimentalFeature.HARNESS) def set_agent_mode( session: AgentSession, mode: str, @@ -182,7 +179,7 @@ def set_agent_mode( Raises: ValueError: The requested mode is not configured. """ - normalized_modes = _normalize_available_modes(tuple(available_modes or DEFAULT_MODE_DESCRIPTIONS)) + normalized_modes = _normalize_available_modes(tuple(available_modes or DEFAULT_MODE_MAP)) normalized_mode = _normalize_mode(mode, available_modes=normalized_modes) provider_state = _get_mode_state(session, source_id=source_id) previous_mode = provider_state.get("current_mode") @@ -196,7 +193,6 @@ def set_agent_mode( return normalized_mode -@experimental(feature_id=ExperimentalFeature.HARNESS) class AgentModeProvider(ContextProvider): """Track the agent's operating mode in session state and provide mode tools. @@ -204,7 +200,7 @@ class AgentModeProvider(ContextProvider): The current mode is persisted in the ``AgentSession`` state and is included in the instructions provided to the agent on each invocation. - The set of available modes is configurable with ``mode_descriptions``. By default, two modes are provided: + The set of available modes is configurable with ``mode_instructions``. By default, two modes are provided: ``"plan"`` (interactive planning) and ``"execute"`` (autonomous execution). This provider exposes the following tools to the agent: @@ -220,7 +216,7 @@ def __init__( source_id: str = DEFAULT_MODE_SOURCE_ID, *, default_mode: str | None = None, - mode_descriptions: Mapping[str, str] | None = None, + mode_instructions: Mapping[str, str] | None = None, instructions: str | None = None, ) -> None: """Initialize a new agent mode provider. @@ -230,8 +226,8 @@ def __init__( Keyword Args: default_mode: Initial mode used when no mode is stored yet. When omitted, the first entry of - ``mode_descriptions`` is used. - mode_descriptions: Mapping of supported modes to descriptions of when and how to use each mode. + ``mode_instructions`` is used. + mode_instructions: Mapping of supported modes to instructions on when and how to use each mode. instructions: Custom instructions for using the mode tools. The instructions can contain an ``{available_modes}`` placeholder for the configured list of modes and a ``{current_mode}`` placeholder for the currently active mode. When omitted, the provider uses a default set of instructions. @@ -240,11 +236,13 @@ def __init__( ValueError: No modes are configured, or the default mode is not configured. """ super().__init__(source_id) - mode_descriptions = dict(DEFAULT_MODE_DESCRIPTIONS if mode_descriptions is None else mode_descriptions) - self._mode_display_names = _normalize_available_modes(tuple(mode_descriptions)) + mode_instructions = dict(DEFAULT_MODE_MAP if mode_instructions is None else mode_instructions) + self._mode_display_names = _normalize_available_modes(tuple(mode_instructions)) if not self._mode_display_names: - raise ValueError("mode_descriptions must contain at least one mode.") - self.mode_descriptions = {mode.strip().lower(): description for mode, description in mode_descriptions.items()} + raise ValueError("mode_instructions must contain at least one mode.") + self.mode_instructions = { + mode.strip().lower(): mode_instruction for mode, mode_instruction in mode_instructions.items() + } self.available_modes = tuple(self._mode_display_names) self.default_mode = _resolve_default_mode(default_mode, available_modes=self._mode_display_names) self.instructions = instructions @@ -252,8 +250,8 @@ def __init__( def _build_instructions(self, current_mode: str) -> str: """Build the mode guidance injected for the current session.""" mode_lines = "".join( - f"#### {self._mode_display_names[mode]}\n\n{description}\n\n" - for mode, description in self.mode_descriptions.items() + f"#### {self._mode_display_names[mode]}\n\n{mode_instruction}\n\n" + for mode, mode_instruction in self.mode_instructions.items() ) instructions = self.instructions or DEFAULT_MODE_INSTRUCTIONS return instructions.replace("{available_modes}", mode_lines).replace("{current_mode}", current_mode) diff --git a/python/packages/core/agent_framework/_harness/_todo.py b/python/packages/core/agent_framework/_harness/_todo.py index 733f05fb6c..4dcec64d6f 100644 --- a/python/packages/core/agent_framework/_harness/_todo.py +++ b/python/packages/core/agent_framework/_harness/_todo.py @@ -47,7 +47,6 @@ ) -@experimental(feature_id=ExperimentalFeature.HARNESS) class TodoItem(SerializationMixin): """Represent one todo item tracked for the current session.""" @@ -106,7 +105,6 @@ def __repr__(self) -> str: ) -@experimental(feature_id=ExperimentalFeature.HARNESS) class TodoInput(SerializationMixin): """Describe one todo item to create.""" @@ -142,7 +140,6 @@ def from_dict( return cls(title=title, description=description) -@experimental(feature_id=ExperimentalFeature.HARNESS) class TodoCompleteInput(SerializationMixin): """Describe one todo item to mark as complete.""" @@ -227,7 +224,6 @@ def _safe_next_id(items: list[TodoItem], next_id: int) -> int: return max(next_id, max((item.id for item in items), default=0) + 1) -@experimental(feature_id=ExperimentalFeature.HARNESS) class TodoStore(ABC): """Abstract backing store for session todo items.""" @@ -245,7 +241,6 @@ async def load_items(self, session: AgentSession, *, source_id: str) -> list[Tod return items -@experimental(feature_id=ExperimentalFeature.HARNESS) class TodoSessionStore(TodoStore): """Store todo state inside ``AgentSession.state``.""" @@ -447,7 +442,6 @@ def _save_state_sync(state_path: Path, payload: str) -> None: temp_path.unlink(missing_ok=True) -@experimental(feature_id=ExperimentalFeature.HARNESS) class TodoProvider(ContextProvider): """Provide todo management tools and instructions to an agent. diff --git a/python/packages/core/tests/core/test_harness_mode.py b/python/packages/core/tests/core/test_harness_mode.py index a6bf6fadda..c4c221f33f 100644 --- a/python/packages/core/tests/core/test_harness_mode.py +++ b/python/packages/core/tests/core/test_harness_mode.py @@ -11,7 +11,6 @@ Agent, AgentModeProvider, AgentSession, - ExperimentalFeature, Message, SupportsChatGetResponse, get_agent_mode, @@ -61,22 +60,18 @@ def test_agent_mode_helpers_reject_non_dict_provider_state() -> None: assert session.state[DEFAULT_MODE_SOURCE_ID] == "unrelated state" -def test_agent_mode_context_provider_validates_configuration_and_is_experimental() -> None: - """Mode provider should validate configuration and expose HARNESS experimental metadata.""" +def test_agent_mode_context_provider_validates_configuration() -> None: + """Mode provider should validate configuration; graduated types carry no experimental metadata.""" with pytest.raises(ValueError, match="at least one mode"): - AgentModeProvider(mode_descriptions={}) + AgentModeProvider(mode_instructions={}) with pytest.raises(ValueError, match="Invalid mode"): AgentModeProvider(default_mode="ship") - assert AgentModeProvider.__feature_id__ == ExperimentalFeature.HARNESS.value # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] - assert get_agent_mode.__feature_id__ == ExperimentalFeature.HARNESS.value # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] - assert set_agent_mode.__feature_id__ == ExperimentalFeature.HARNESS.value # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] - assert ".. warning:: Experimental" in AgentModeProvider.__doc__ # type: ignore[operator] # pyrefly: ignore[not-iterable] # ty: ignore[unsupported-operator] - assert get_agent_mode.__doc__ is not None - assert ".. warning:: Experimental" in get_agent_mode.__doc__ - assert set_agent_mode.__doc__ is not None - assert ".. warning:: Experimental" in set_agent_mode.__doc__ + for graduated in (AgentModeProvider, get_agent_mode, set_agent_mode): + assert not hasattr(graduated, "__feature_id__") + assert AgentModeProvider.__doc__ is not None + assert ".. warning:: Experimental" not in AgentModeProvider.__doc__ async def test_external_read_with_provider_config_preserves_nondefault_mode( @@ -129,7 +124,7 @@ async def test_agent_mode_context_provider_normalizes_custom_modes( """Mode provider should accept differently-cased custom modes and display configured names.""" session = AgentSession(session_id="session-1") provider = AgentModeProvider( - default_mode="Draft", mode_descriptions={"Draft": "Draft it.", "Final": "Finalize it."} + default_mode="Draft", mode_instructions={"Draft": "Draft it.", "Final": "Finalize it."} ) agent = Agent(client=chat_client_base, context_providers=[provider]) @@ -162,7 +157,7 @@ async def test_agent_mode_context_provider_serializes_tool_outputs_as_json( """Mode tools should serialize JSON correctly for mode names with quotes.""" session = AgentSession(session_id="session-1") mode_name = 'edit "preview"' - provider = AgentModeProvider(default_mode=mode_name, mode_descriptions={mode_name: "Preview edits."}) + provider = AgentModeProvider(default_mode=mode_name, mode_instructions={mode_name: "Preview edits."}) agent = Agent(client=chat_client_base, context_providers=[provider]) _, options = await agent._prepare_session_and_messages( # pyright: ignore[reportPrivateUsage] @@ -221,7 +216,7 @@ def test_default_mode_falls_back_to_first_available_mode() -> None: assert get_agent_mode(session, available_modes=("draft", "final")) == "draft" - provider = AgentModeProvider(mode_descriptions={"Draft": "Draft it.", "Final": "Finalize it."}) + provider = AgentModeProvider(mode_instructions={"Draft": "Draft it.", "Final": "Finalize it."}) assert provider.default_mode == "draft" diff --git a/python/packages/core/tests/core/test_harness_todo.py b/python/packages/core/tests/core/test_harness_todo.py index cac4e9c6fd..891ea7d94d 100644 --- a/python/packages/core/tests/core/test_harness_todo.py +++ b/python/packages/core/tests/core/test_harness_todo.py @@ -16,12 +16,12 @@ Message, SupportsChatGetResponse, TodoFileStore, - TodoInput, TodoItem, TodoProvider, TodoSessionStore, TodoStore, ) +from agent_framework._harness._todo import TodoInput def _tool_by_name(tools: list[object], name: str) -> object: @@ -366,12 +366,14 @@ async def test_todo_provider_serializes_concurrent_mutations( assert {item["id"] for item in payload if item["is_complete"]} == {1, 2, 3, 4, 5} -def test_todo_harness_classes_are_marked_experimental() -> None: - """Todo harness public classes should expose HARNESS experimental metadata.""" - assert TodoStore.__feature_id__ == ExperimentalFeature.HARNESS.value # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] - assert TodoItem.__feature_id__ == ExperimentalFeature.HARNESS.value # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] - assert TodoInput.__feature_id__ == ExperimentalFeature.HARNESS.value # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] - assert TodoSessionStore.__feature_id__ == ExperimentalFeature.HARNESS.value # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] +def test_todo_harness_graduated_classes_are_not_experimental() -> None: + """Graduated todo harness types should carry no experimental metadata; TodoFileStore stays experimental.""" + for graduated in (TodoStore, TodoItem, TodoInput, TodoSessionStore, TodoProvider): + assert not hasattr(graduated, "__feature_id__") + assert TodoProvider.__doc__ is not None + assert ".. warning:: Experimental" not in TodoProvider.__doc__ + + # TodoFileStore remains opt-in and experimental. assert TodoFileStore.__feature_id__ == ExperimentalFeature.HARNESS.value # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] - assert TodoProvider.__feature_id__ == ExperimentalFeature.HARNESS.value # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] - assert ".. warning:: Experimental" in TodoProvider.__doc__ # type: ignore[operator] # pyrefly: ignore[not-iterable] # ty: ignore[unsupported-operator] + assert TodoFileStore.__doc__ is not None + assert ".. warning:: Experimental" in TodoFileStore.__doc__