Description
Since core 1.15.0, an agent built with create_harness_agent(..., loop_should_continue=...) fails every request against a real provider client.
AgentLoopMiddleware stamps a private marker into the run's options to identify the runs it drives (_harness/_loop.py):
stamped_options = dict(context.options) if context.options is not None else {}
stamped_options[_LOOP_ITERATION_TOKEN_KEY] = object() # "_agent_loop_iteration"
context.options = stamped_options
Agent._prepare_run_context then forwards every option it does not recognise to the chat client, by design (_agents.py):
run_opts = {
..., # known keys popped from opts
**opts, # Remaining options are provider-specific
}
The private marker is not a known key, so it lands in the provider-specific bucket and is passed to the SDK as a keyword argument. Clients that hand unrecognised options to their SDK then raise.
This appears to be an unintended interaction between PR #7289 (which introduced the marker) and the long-standing "remaining options are provider-specific" passthrough. The marker is consumed by Agent._run_after_providers to decide whether a turn-scoped context provider should be deferred to the loop boundary, so it needs to reach that code — but not the transport.
The trigger is specifically an active loop predicate. Without loop_should_continue no loop middleware is wired, nothing is stamped, and a harness agent looks healthy — which is likely why this was not caught.
Worth noting: the repository's own harness sample is affected. python/samples/02-agents/harness/harness_research.py passes loop_should_continue=todos_remaining(looping_modes=["execute"]) to a real FoundryChatClient.
Code Sample
# Reproduces with no application code — three public APIs and any running Ollama:
import asyncio
from agent_framework import create_harness_agent, todos_remaining, tool
from agent_framework_ollama import OllamaChatClient
@tool
def get_weather(city: str) -> str:
"""Return the weather for a city."""
return f"It is sunny in {city}."
async def main() -> None:
agent = create_harness_agent(
OllamaChatClient(model="minimax-m3:cloud"),
tools=[get_weather],
loop_should_continue=todos_remaining(looping_modes=["execute"]),
)
await agent.run("What is the weather in Amsterdam?", session=agent.create_session())
asyncio.run(main())
# Removing only the `loop_should_continue` line makes it pass.
Error Messages / Stack Traces
Ollama:
agent_framework.exceptions.ChatClientException: ("Ollama chat request failed : AsyncClient.chat() got an unexpected keyword argument '_agent_loop_iteration'", TypeError("AsyncClient.chat() got an unexpected keyword argument '_agent_loop_iteration'"))
Azure AI Foundry, same run shape:
agent_framework.exceptions.ChatClientException: (" ... service failed to complete the prompt: AsyncResponses.create() got an unexpected keyword argument '_agent_loop_iteration'", TypeError("AsyncResponses.create() got an unexpected keyword argument '_agent_loop_iteration'"))
The marker arrives as a top-level keyword argument beside the real ones — captured at the SDK boundary:
kwarg keys: ['_agent_loop_iteration', 'messages', 'model', 'options', 'stream', 'tools']
_agents.py:1126 _run_non_streaming
...
agent_framework_ollama/_chat_client.py:385 _get_response
Package Versions
agent-framework-core: 1.15.0, agent-framework-ollama: 1.0.0b260813
Python Version
Python 3.12
Additional Context
Working on agent-framework-core: 1.14.0
Existing unit coverage does not catch this because test doubles accept **kwargs and silently absorb the marker; only a real SDK call raises. A regression test would need either a client whose signature rejects unknown keywords, or an assertion on the keys handed to the client.
Two directions, both plausible from the outside:
- Keep the marker off the options mapping entirely — a contextvar or a private attribute on the context would carry it to
_run_after_providers without passing through the provider-specific passthrough.
- Or strip known-private keys (
_-prefixed, or an explicit set) where run_opts is assembled, so the passthrough forwards only genuinely provider-specific options.
Description
Since core 1.15.0, an agent built with
create_harness_agent(..., loop_should_continue=...)fails every request against a real provider client.AgentLoopMiddlewarestamps a private marker into the run's options to identify the runs it drives (_harness/_loop.py):Agent._prepare_run_contextthen forwards every option it does not recognise to the chat client, by design (_agents.py):The private marker is not a known key, so it lands in the provider-specific bucket and is passed to the SDK as a keyword argument. Clients that hand unrecognised options to their SDK then raise.
This appears to be an unintended interaction between PR #7289 (which introduced the marker) and the long-standing "remaining options are provider-specific" passthrough. The marker is consumed by
Agent._run_after_providersto decide whether a turn-scoped context provider should be deferred to the loop boundary, so it needs to reach that code — but not the transport.The trigger is specifically an active loop predicate. Without
loop_should_continueno loop middleware is wired, nothing is stamped, and a harness agent looks healthy — which is likely why this was not caught.Worth noting: the repository's own harness sample is affected.
python/samples/02-agents/harness/harness_research.pypassesloop_should_continue=todos_remaining(looping_modes=["execute"])to a realFoundryChatClient.Code Sample
Error Messages / Stack Traces
Ollama: agent_framework.exceptions.ChatClientException: ("Ollama chat request failed : AsyncClient.chat() got an unexpected keyword argument '_agent_loop_iteration'", TypeError("AsyncClient.chat() got an unexpected keyword argument '_agent_loop_iteration'")) Azure AI Foundry, same run shape: agent_framework.exceptions.ChatClientException: (" ... service failed to complete the prompt: AsyncResponses.create() got an unexpected keyword argument '_agent_loop_iteration'", TypeError("AsyncResponses.create() got an unexpected keyword argument '_agent_loop_iteration'")) The marker arrives as a top-level keyword argument beside the real ones — captured at the SDK boundary: kwarg keys: ['_agent_loop_iteration', 'messages', 'model', 'options', 'stream', 'tools'] _agents.py:1126 _run_non_streaming ... agent_framework_ollama/_chat_client.py:385 _get_responsePackage Versions
agent-framework-core: 1.15.0, agent-framework-ollama: 1.0.0b260813
Python Version
Python 3.12
Additional Context
Working on agent-framework-core: 1.14.0
Existing unit coverage does not catch this because test doubles accept
**kwargsand silently absorb the marker; only a real SDK call raises. A regression test would need either a client whose signature rejects unknown keywords, or an assertion on the keys handed to the client.Two directions, both plausible from the outside:
_run_after_providerswithout passing through the provider-specific passthrough._-prefixed, or an explicit set) whererun_optsis assembled, so the passthrough forwards only genuinely provider-specific options.