Skip to content

Python: [Bug]: agent-loop marker reaches the provider SDK (core 1.15.0) #7821

Description

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

agentsUsage: [Issues, PRs], Target: Single agentmiddlewareUsage: [Issues, PRs], Target: middleware related featurespythonUsage: [Issues, PRs], Target: PythonreproducedUsage: [Issues], Target: all issues that can be reproduced by the triage workflow

Type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions