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
1 change: 1 addition & 0 deletions python/packages/core/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ agent_framework/
- **Descriptions & index** - `file_memory_write` accepts an optional `description`, stored in a companion `<stem>_description.md` sidecar. After each write/delete the provider rebuilds a capped (50-entry) `memories.md` index, and `before_run` injects that index as a `user` context message so the model knows what memories exist. Sidecars and the index are internal files hidden from `file_memory_ls`/`file_memory_grep` and rejected as write targets.
- **`DEFAULT_FILE_MEMORY_SOURCE_ID`** / **`DEFAULT_FILE_MEMORY_INSTRUCTIONS`** - Public defaults for the provider's source id and instruction banner.
- **Harness wiring** - `create_harness_agent` includes the `FileMemoryProvider` by default; the `FileAccessProvider` is opt-in and added only when a `file_access_store` is supplied (no implicit `{cwd}/working` store is created). Disable file memory via `disable_file_memory`; override its backing store via `file_memory_store`. When no file-memory store is supplied, the default is `FileSystemAgentFileStore` rooted at `{cwd}/agent-file-memory`. `create_harness_agent` also wires in `MessageInjectionMiddleware` by default (mirroring the .NET harness's `UseMessageInjection`); it is always on with no opt-out because it is a no-op when no messages are queued for the session.
- **Experimental-feature gating** - `create_harness_agent` itself is released (no longer `@experimental`), but a few features it can wire in remain experimental or pre-release: **background agents** (`background_agents`), **file access** (`file_access_store`), **looping** (`loop_should_continue`), and the **shell tooling** (`shell_executor`, from the pre-release `agent-framework-tools` package). Enabling any of them emits a single `ExperimentalWarning` (naming the responsible parameter). The three experimental harness providers share one `HARNESS` dedup key so the downstream experimental provider does not warn a second time; the shell tooling uses a separate dedup key (it is not a `HARNESS`-decorated feature).

### Tool Approval Harness (`_harness/_tool_approval.py`)

Expand Down
39 changes: 39 additions & 0 deletions python/packages/core/agent_framework/_feature_stage.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,45 @@ def _warn_on_feature_use(
_WARNED_FEATURES.add(warning_key)


def warn_experimental_feature(
message: str,
*,
feature_id: str | Enum,
category: type[Warning] = ExperimentalWarning,
) -> bool:
"""Emit a one-time feature-stage warning for a feature not gated by a decorator.

Some released APIs opt callers into experimental behaviour through individual
parameters, which Python cannot decorate on their own. Call this to warn once per
``feature_id`` with a custom ``message`` (pointing at the caller's call site) and to
seed the shared dedup registry, so a downstream decorated provider for the same
``feature_id`` does not warn a second time.

Returns ``True`` when a warning was emitted, ``False`` when it was already emitted for
this ``feature_id``/``category`` earlier in the process.
"""
normalized_feature_id = _normalize_feature_id(feature_id)
warning_key = (category, normalized_feature_id)
if warning_key in _WARNED_FEATURES:
return False

user_frame = _resolve_user_frame()
if user_frame is None:
# Last-resort fallback: emit at the immediate caller of this helper.
warnings.warn(message, category=category, stacklevel=2)
else:
filename, lineno, module = user_frame
warnings.warn_explicit(
message,
category=category,
filename=filename,
lineno=lineno,
module=module,
)
_WARNED_FEATURES.add(warning_key)
return True


def _add_runtime_warning(
obj: FeatureStageT,
*,
Expand Down
61 changes: 59 additions & 2 deletions python/packages/core/agent_framework/_harness/_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from .._agents import Agent, SupportsAgentRun
from .._clients import SupportsShellTool, SupportsWebSearchTool
from .._compaction import CompactionProvider, ContextWindowCompactionStrategy
from .._feature_stage import ExperimentalFeature, experimental
from .._feature_stage import ExperimentalFeature, warn_experimental_feature
from .._sessions import ContextProvider, HistoryProvider, InMemoryHistoryProvider, MessageInjectionMiddleware
from .._skills import SkillsProvider
from .._types import ChatOptions
Expand Down Expand Up @@ -269,8 +269,35 @@ def _assemble_shell(
default="ChatOptions[None]",
)

# Dedup label for the pre-release shell tooling (provided by the alpha-stage
# agent-framework-tools package). It is not a feature-stage-decorated feature, so it uses a
# label distinct from ExperimentalFeature.HARNESS to avoid suppressing unrelated HARNESS warnings.
_SHELL_TOOLING_FEATURE_ID = "SHELL_TOOLING"


def _warn_experimental_harness_params(
param_names: Sequence[str],
*,
feature_id: str,
detail: str,
) -> None:
"""Emit a single ExperimentalWarning when experimental harness features are enabled.

``create_harness_agent`` itself is released, but some of the features it can wire in
remain experimental or pre-release. When a caller opts into one of those features, warn
once (pointing at the caller's call site) naming the responsible parameter(s), and seed a
per-feature dedup key so the downstream experimental provider does not warn again.
"""
if not param_names:
return
joined = ", ".join(repr(name) for name in param_names)
warn_experimental_feature(
f"[{feature_id}] create_harness_agent parameter(s) {joined} enable "
f"{detail} that may change or be removed in future versions without notice.",
feature_id=feature_id,
)


@experimental(feature_id=ExperimentalFeature.HARNESS)
def create_harness_agent(
client: SupportsChatGetResponse[OptionsCoT],
*,
Expand Down Expand Up @@ -334,6 +361,14 @@ def create_harness_agent(

Each feature can be disabled or customized via keyword arguments.

.. note:: Experimental features

``create_harness_agent`` is released, but a few of the features it can wire in are
still experimental or pre-release: **background agents** (``background_agents``),
**file access** (``file_access_store``), **looping** (``loop_should_continue``), and
the **shell tooling** (``shell_executor``, provided by the pre-release
``agent-framework-tools`` package). Enabling any of them emits an ``ExperimentalWarning``.

Examples:
Basic usage:

Expand Down Expand Up @@ -502,6 +537,28 @@ def create_harness_agent(
):
raise ValueError("max_output_tokens must be less than max_context_window_tokens.")

# Warn when opting into harness features that are still experimental. create_harness_agent
# itself is released, but background agents, file access, and looping remain experimental,
# and the shell tooling is provided by the pre-release agent-framework-tools package.
experimental_params: list[str] = []
if background_agents:
experimental_params.append("background_agents")
if file_access_store is not None:
experimental_params.append("file_access_store")
if loop_should_continue is not None:
experimental_params.append("loop_should_continue")
_warn_experimental_harness_params(
experimental_params,
feature_id=ExperimentalFeature.HARNESS.value,
detail="experimental harness features",
)
if shell_executor is not None:
_warn_experimental_harness_params(
["shell_executor"],
feature_id=_SHELL_TOOLING_FEATURE_ID,
detail="pre-release shell tooling from the agent-framework-tools package",
)

# Build history provider.
resolved_history = history_provider or InMemoryHistoryProvider()

Expand Down
1 change: 1 addition & 0 deletions python/packages/core/agent_framework/_harness/_agent.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ from ._tool_approval import ToolApprovalRuleCallback

DEFAULT_HARNESS_INSTRUCTIONS: str
HARNESS_AGENT_PROVIDER_NAME: str
_SHELL_TOOLING_FEATURE_ID: str

OptionsCoT = TypeVar(
"OptionsCoT",
Expand Down
159 changes: 159 additions & 0 deletions python/packages/core/tests/core/test_harness_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

import importlib.util
import warnings
from collections.abc import AsyncIterable, Awaitable, Mapping, Sequence
from pathlib import Path
from typing import Any
Expand Down Expand Up @@ -1271,3 +1272,161 @@ def _next_message(**kwargs: Any) -> Any:
loop_max_iterations=5,
)
assert _find_loop_middleware(agent) is None


# --- Experimental graduation / gating Tests ---


def _clear_harness_experimental_dedup() -> None:
"""Drop the shared HARNESS dedup key so ExperimentalWarning can fire again in a test."""
from agent_framework._feature_stage import _WARNED_FEATURES, ExperimentalFeature, ExperimentalWarning

_WARNED_FEATURES.discard((ExperimentalWarning, ExperimentalFeature.HARNESS.value))


def _clear_harness_shell_dedup() -> None:
"""Drop the shell-tooling dedup key so ExperimentalWarning can fire again in a test."""
from agent_framework._feature_stage import _WARNED_FEATURES, ExperimentalWarning
from agent_framework._harness._agent import _SHELL_TOOLING_FEATURE_ID

_WARNED_FEATURES.discard((ExperimentalWarning, _SHELL_TOOLING_FEATURE_ID))


def test_create_harness_agent_is_not_experimental() -> None:
"""create_harness_agent is graduated and should no longer carry experimental metadata."""
assert getattr(create_harness_agent, "__feature_stage__", None) is None
assert getattr(create_harness_agent, "__feature_id__", None) is None


def test_create_harness_agent_graduated_features_emit_no_experimental_warning() -> None:
"""Using only graduated features must not emit an ExperimentalWarning."""
from agent_framework._feature_stage import ExperimentalWarning

_clear_harness_experimental_dedup()
with warnings.catch_warnings():
warnings.simplefilter("error", ExperimentalWarning)
create_harness_agent(
client=_FakeChatClient(),
max_context_window_tokens=128_000,
max_output_tokens=16_384,
disable_web_search=True,
disable_file_memory=True,
)


def test_create_harness_agent_background_agents_emits_experimental_warning() -> None:
"""Opting into background agents (still experimental) should warn and still wire the provider."""
from agent_framework._feature_stage import ExperimentalWarning
from agent_framework._harness._background_agents import BackgroundAgentsProvider

_clear_harness_experimental_dedup()
bg_agent = _FakeBackgroundAgent("WebSearcher", "Searches the web")
with pytest.warns(ExperimentalWarning, match="background_agents"):
agent = create_harness_agent(
client=_FakeChatClient(),
max_context_window_tokens=128_000,
max_output_tokens=16_384,
disable_web_search=True,
disable_file_memory=True,
background_agents=[bg_agent], # type: ignore[list-item] # pyrefly: ignore[bad-argument-type] # ty: ignore[invalid-argument-type]
)
assert any(isinstance(p, BackgroundAgentsProvider) for p in (agent.context_providers or []))


def test_create_harness_agent_file_access_store_emits_experimental_warning() -> None:
"""Opting into file access (still experimental) should warn and still wire the provider."""
from agent_framework._feature_stage import ExperimentalWarning

_clear_harness_experimental_dedup()
store = InMemoryAgentFileStore()
_clear_harness_experimental_dedup()
with pytest.warns(ExperimentalWarning, match="file_access_store"):
agent = create_harness_agent(
client=_FakeChatClient(),
max_context_window_tokens=128_000,
max_output_tokens=16_384,
disable_web_search=True,
disable_file_memory=True,
file_access_store=store,
)
assert any(isinstance(p, FileAccessProvider) for p in (agent.context_providers or []))


def test_create_harness_agent_loop_should_continue_emits_experimental_warning() -> None:
"""Opting into looping (still experimental) should warn and still wire the loop middleware."""
from agent_framework import AgentLoopMiddleware
from agent_framework._feature_stage import ExperimentalWarning

def _should_continue(**kwargs: Any) -> bool:
return False

_clear_harness_experimental_dedup()
with pytest.warns(ExperimentalWarning, match="loop_should_continue"):
agent = create_harness_agent(
client=_FakeChatClient(), # type: ignore[arg-type]
max_context_window_tokens=128_000,
max_output_tokens=16_384,
disable_web_search=True,
disable_file_memory=True,
loop_should_continue=_should_continue,
)
assert agent.middleware is not None
assert isinstance(agent.middleware[0], AgentLoopMiddleware)


@_requires_shell_tools
def test_create_harness_agent_shell_executor_emits_experimental_warning() -> None:
"""Opting into shell tooling (pre-release) should warn and still wire the shell tool."""
from agent_framework._feature_stage import ExperimentalWarning

_clear_harness_experimental_dedup()
_clear_harness_shell_dedup()
client = _FakeShellClient()
with pytest.warns(ExperimentalWarning, match="shell_executor"):
agent = create_harness_agent(
client=client,
max_context_window_tokens=128_000,
max_output_tokens=16_384,
disable_web_search=True,
disable_file_memory=True,
shell_executor=_FakeShellTool(),
)
assert "shell_tool_instance" in agent.default_options.get("tools", [])


@_requires_shell_tools
def test_create_harness_agent_shell_dedup_does_not_suppress_harness_warning() -> None:
"""The shell tooling uses a dedup key separate from HARNESS.

Enabling shell tooling must only seed the SHELL_TOOLING dedup key, so a subsequent
opt-in to an experimental HARNESS feature (e.g. ``background_agents``) must still warn.
This guards the separate-dedup-key strategy against regressions.
"""
from agent_framework._feature_stage import ExperimentalWarning

_clear_harness_experimental_dedup()
_clear_harness_shell_dedup()

# Enabling shell tooling seeds only the SHELL_TOOLING dedup key, not HARNESS.
with pytest.warns(ExperimentalWarning, match="shell_executor"):
create_harness_agent(
client=_FakeShellClient(),
max_context_window_tokens=128_000,
max_output_tokens=16_384,
disable_web_search=True,
disable_file_memory=True,
shell_executor=_FakeShellTool(),
)

# HARNESS was never seeded by the shell warning, so opting into an experimental
# HARNESS feature afterwards must still emit its own ExperimentalWarning.
bg_agent = _FakeBackgroundAgent("WebSearcher", "Searches the web")
with pytest.warns(ExperimentalWarning, match="background_agents"):
create_harness_agent(
client=_FakeChatClient(),
max_context_window_tokens=128_000,
max_output_tokens=16_384,
disable_web_search=True,
disable_file_memory=True,
background_agents=[bg_agent], # type: ignore[list-item] # pyrefly: ignore[bad-argument-type] # ty: ignore[invalid-argument-type]
)
Loading