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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions python/packages/core/agent_framework/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,6 @@
"._harness._todo": (
"DEFAULT_TODO_SOURCE_ID",
"TodoFileStore",
"TodoInput",
"TodoItem",
"TodoProvider",
"TodoSessionStore",
Expand Down Expand Up @@ -546,7 +545,6 @@
"SwitchCaseEdgeGroupDefault",
"TextSpanRegion",
"TodoFileStore",
"TodoInput",
"TodoItem",
"TodoProvider",
"TodoSessionStore",
Expand Down
2 changes: 0 additions & 2 deletions python/packages/core/agent_framework/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -513,7 +512,6 @@ __all__ = [
"SwitchCaseEdgeGroupDefault",
"TextSpanRegion",
"TodoFileStore",
"TodoInput",
"TodoItem",
"TodoProvider",
"TodoSessionStore",
Expand Down
32 changes: 15 additions & 17 deletions python/packages/core/agent_framework/_harness/_mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 "
Expand Down Expand Up @@ -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,
*,
Expand All @@ -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")
Expand All @@ -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,
Expand Down Expand Up @@ -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")
Expand All @@ -196,15 +193,14 @@ 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.

The ``AgentModeProvider`` enables agents to operate in distinct modes during long-running complex tasks.
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:
Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -240,20 +236,22 @@ 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

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)
Expand Down
6 changes: 0 additions & 6 deletions python/packages/core/agent_framework/_harness/_todo.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@
)


@experimental(feature_id=ExperimentalFeature.HARNESS)
class TodoItem(SerializationMixin):
"""Represent one todo item tracked for the current session."""

Expand Down Expand Up @@ -106,7 +105,6 @@ def __repr__(self) -> str:
)


@experimental(feature_id=ExperimentalFeature.HARNESS)
class TodoInput(SerializationMixin):
"""Describe one todo item to create."""

Expand Down Expand Up @@ -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."""

Expand Down Expand Up @@ -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."""

Expand All @@ -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``."""

Expand Down Expand Up @@ -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.

Expand Down
25 changes: 10 additions & 15 deletions python/packages/core/tests/core/test_harness_mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
Agent,
AgentModeProvider,
AgentSession,
ExperimentalFeature,
Message,
SupportsChatGetResponse,
get_agent_mode,
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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])

Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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"


Expand Down
20 changes: 11 additions & 9 deletions python/packages/core/tests/core/test_harness_todo.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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__
Loading