From 52a8045bb670918ef8c1f970dad051092893ceee Mon Sep 17 00:00:00 2001 From: westey <164392973+westey-m@users.noreply.github.com> Date: Mon, 1 Jun 2026 18:20:39 +0100 Subject: [PATCH] Python: Add background agent support to harness agent (#6155) * Add background agent support to harness agent * Address PR comments --- .../core/agent_framework/_harness/_agent.py | 21 ++++- .../core/tests/core/test_harness_agent.py | 91 +++++++++++++++++++ 2 files changed, 111 insertions(+), 1 deletion(-) diff --git a/python/packages/core/agent_framework/_harness/_agent.py b/python/packages/core/agent_framework/_harness/_agent.py index ce218576db..5896f72141 100644 --- a/python/packages/core/agent_framework/_harness/_agent.py +++ b/python/packages/core/agent_framework/_harness/_agent.py @@ -14,12 +14,13 @@ import logging from collections.abc import Callable, Sequence from typing import TYPE_CHECKING, Any -from .._agents import Agent +from .._agents import Agent, SupportsAgentRun from .._clients import SupportsWebSearchTool from .._compaction import CompactionProvider, ContextWindowCompactionStrategy, ToolResultCompactionStrategy from .._feature_stage import ExperimentalFeature, experimental from .._sessions import ContextProvider, HistoryProvider, InMemoryHistoryProvider from .._skills import SkillsProvider +from ._background_agents import BackgroundAgentsProvider from ._memory import MemoryContextProvider, MemoryStore from ._mode import AgentModeProvider from ._todo import TodoProvider @@ -103,6 +104,8 @@ def _assemble_context_providers( memory_store: MemoryStore | None, skills_provider: SkillsProvider | None, skills_paths: Sequence[str] | None, + background_agents: Sequence[SupportsAgentRun] | None, + background_agents_instructions: str | None, extra_context_providers: Sequence[ContextProvider] | None, ) -> list[ContextProvider]: """Assemble the ordered list of context providers.""" @@ -130,6 +133,10 @@ def _assemble_context_providers( if skills_paths: providers.append(SkillsProvider.from_paths(*skills_paths)) + # Background agents are opt-in: only added when agents are provided. + if background_agents: + providers.append(BackgroundAgentsProvider(background_agents, instructions=background_agents_instructions)) + # Append any user-supplied additional providers. if extra_context_providers: providers.extend(extra_context_providers) @@ -165,6 +172,8 @@ def create_harness_agent( memory_store: MemoryStore | None = None, skills_provider: SkillsProvider | None = None, skills_paths: Sequence[str] | None = None, + background_agents: Sequence[SupportsAgentRun] | None = None, + background_agents_instructions: str | None = None, disable_web_search: bool = False, otel_provider_name: str | None = None, context_providers: Sequence[ContextProvider] | None = None, @@ -182,6 +191,7 @@ def create_harness_agent( - **AgentModeProvider** — plan/execute mode tracking - **MemoryContextProvider** — file-based durable memory (when ``memory_store`` provided) - **SkillsProvider** — skill discovery and progressive loading + - **BackgroundAgentsProvider** — delegate work to background sub-agents - **OpenTelemetry** — observability via ``AgentTelemetryLayer`` Each feature can be disabled or customized via keyword arguments. @@ -253,6 +263,13 @@ def create_harness_agent( skills_paths: Paths for file-based skill discovery (looks for SKILL.md files). Can be combined with ``skills_provider``. When neither ``skills_provider`` nor ``skills_paths`` is provided, no SkillsProvider is added. + background_agents: Collection of agents available for background task delegation. + When provided, a ``BackgroundAgentsProvider`` is automatically included, + enabling the agent to start, monitor, and retrieve results from background tasks. + Each agent must have a non-empty, unique name (case-insensitive). + background_agents_instructions: Optional instruction override for the + ``BackgroundAgentsProvider``. May include ``{background_agents}`` placeholder + which will be replaced with the agent listing. disable_web_search: When True, skip automatic web search tool inclusion. When False (default), the web search tool is automatically added if the client implements SupportsWebSearchTool. A warning is logged if the client @@ -302,6 +319,8 @@ def create_harness_agent( memory_store=memory_store, skills_provider=skills_provider, skills_paths=skills_paths, + background_agents=background_agents, + background_agents_instructions=background_agents_instructions, extra_context_providers=context_providers, ) diff --git a/python/packages/core/tests/core/test_harness_agent.py b/python/packages/core/tests/core/test_harness_agent.py index c53147fd15..58ef3f5f2d 100644 --- a/python/packages/core/tests/core/test_harness_agent.py +++ b/python/packages/core/tests/core/test_harness_agent.py @@ -394,3 +394,94 @@ def test_create_harness_agent_logs_warning_when_no_web_search(caplog: pytest.Log max_output_tokens=16_384, ) assert any("SupportsWebSearchTool" in msg for msg in caplog.messages) + + +# --- Background Agents Tests --- + + +class _FakeBackgroundAgent: + """Minimal agent stub satisfying SupportsAgentRun for background agents tests.""" + + def __init__(self, name: str, description: str | None = None): + self.id = f"agent-{name}" + self.name = name + self.description = description + + def create_session(self, *, session_id: str | None = None) -> AgentSession: + return AgentSession(session_id=session_id) + + def get_session(self, service_session_id: str, *, session_id: str | None = None) -> AgentSession: + return AgentSession(service_session_id=service_session_id, session_id=session_id) + + async def run(self, messages: Any = None, *, stream: bool = False, session: Any = None, **kwargs: Any) -> Any: + from agent_framework import AgentResponse + + return AgentResponse(messages=[], response_id="fake-bg-response") + + +def test_create_harness_agent_no_background_agents_by_default() -> None: + """No BackgroundAgentsProvider should be included when background_agents is not provided.""" + from agent_framework._harness._background_agents import BackgroundAgentsProvider + + 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, + ) + providers = agent.context_providers or [] + assert not any(isinstance(p, BackgroundAgentsProvider) for p in providers) + + +def test_create_harness_agent_adds_background_agents_provider() -> None: + """BackgroundAgentsProvider should be included when background_agents are provided.""" + from agent_framework._harness._background_agents import BackgroundAgentsProvider + + bg_agent = _FakeBackgroundAgent("WebSearcher", "Searches the web") + 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, + background_agents=[bg_agent], + ) + providers = agent.context_providers or [] + bg_providers = [p for p in providers if isinstance(p, BackgroundAgentsProvider)] + assert len(bg_providers) == 1 + + +def test_create_harness_agent_background_agents_custom_instructions() -> None: + """Custom instructions should be passed to BackgroundAgentsProvider.""" + from agent_framework._harness._background_agents import BackgroundAgentsProvider + + custom_instructions = "## Custom\n\nUse agents wisely.\n\n{background_agents}" + bg_agent = _FakeBackgroundAgent("Helper", "A helper agent") + 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, + background_agents=[bg_agent], + background_agents_instructions=custom_instructions, + ) + providers = agent.context_providers or [] + bg_providers = [p for p in providers if isinstance(p, BackgroundAgentsProvider)] + assert len(bg_providers) == 1 + # Verify the custom instructions were used (placeholder replaced with agent list). + assert "Custom" in bg_providers[0]._instructions + assert "Helper" in bg_providers[0]._instructions + + +def test_create_harness_agent_empty_background_agents_list() -> None: + """An empty background_agents list should NOT add a BackgroundAgentsProvider.""" + from agent_framework._harness._background_agents import BackgroundAgentsProvider + + 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, + background_agents=[], + ) + providers = agent.context_providers or [] + assert not any(isinstance(p, BackgroundAgentsProvider) for p in providers)