Skip to content

Python: InMemoryHistoryProvider auto-injection causes message duplication in HandoffBuilder workflows #5147

@dantelmomsft

Description

@dantelmomsft

Description

When agents participate in a HandoffBuilder workflow with CheckpointStorage, conversation history is managed by the HandoffAgentExecutor via its internal _full_conversation list. However, the Agent base class auto-injects an InMemoryHistoryProvider when context_providers is empty. This creates two independent history sources that compound on every turn, causing quadratic message growth and eventual API failures.

Reproduction

  1. Create a HandoffBuilder workflow with CheckpointStorage and 2+ agents
  2. Leave at least one agent with an empty context_providers list (the default)
  3. Run a multi-turn conversation (3+ user messages)

Observed behavior

  • Turn 1: Agent receives N messages (correct)
  • Turn 2: Agent receives ~2N messages (executor history + InMemoryHistoryProvider replay)
  • Turn 3: Agent receives ~3N messages
  • Each turn inflates the message list further

This results in:

  • The LLM re-requesting tool calls it already completed (duplicate task progress in the UI)
  • OpenAI 400 errors: "An assistant message with 'tool_calls' must be followed by tool messages" — the duplicated history places tool_calls messages where the matching tool result is displaced
  • Token usage growing quadratically instead of linearly

Expected behavior

Agents inside a HandoffBuilder workflow should not accumulate a second copy of conversation history. The executor is the single source of truth.

Current workaround

Create a no-op ContextProvider and assign it to every workflow participant that doesn't already have one:

class NoHistoryProvider(ContextProvider):
    def __init__(self):
        super().__init__("no_history_provider")

    async def before_run(self, *, agent, session, context, state):
        pass
Agent(
    client=client,
    instructions="...",
    name="my_agent",
    context_providers=[NoHistoryProvider()]  # prevents auto-injection
)

This is fragile — every new agent added to a workflow must remember to include it, and there's no framework-level indication that auto-injection is inappropriate.

For a real-world example of this workaround applied to a multi-agent banking assistant, see:
https://github.com/Azure-Samples/agent-openai-python-banking-assistant/blob/main/app/backend/app/agents/azure_chat/handoff_orchestrator.py#L57

Suggested resolution (pick one or combine)

Option Description
A. Suppress auto-injection inside workflows HandoffBuilder or HandoffAgentExecutor should disable InMemoryHistoryProvider auto-injection for all participants, since it owns the conversation state.
B. Add an explicit opt-out Provide Agent(..., history_provider=None) or Agent(..., auto_history=False) so developers can explicitly disable it without a dummy provider.
C. Document the interaction At minimum, add a warning in the HandoffBuilder / orchestration docs explaining that agents with empty context_providers will get InMemoryHistoryProvider auto-injected, which conflicts with the executor's history management. Include the no-op provider pattern as a recommended practice.

Environment

  • agent-framework-core==1.0.0
  • agent-framework-orchestrations==1.0.0b260402
  • agent-framework-openai==1.0.0 / agent-framework-foundry==1.0.0
  • Reproducible with both OpenAIChatCompletionClient and FoundryChatClient

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions