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
- Create a
HandoffBuilder workflow with CheckpointStorage and 2+ agents
- Leave at least one agent with an empty
context_providers list (the default)
- 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
Description
When agents participate in a
HandoffBuilderworkflow withCheckpointStorage, conversation history is managed by theHandoffAgentExecutorvia its internal_full_conversationlist. However, theAgentbase class auto-injects anInMemoryHistoryProviderwhencontext_providersis empty. This creates two independent history sources that compound on every turn, causing quadratic message growth and eventual API failures.Reproduction
HandoffBuilderworkflow withCheckpointStorageand 2+ agentscontext_providerslist (the default)Observed behavior
InMemoryHistoryProviderreplay)This results in:
"An assistant message with 'tool_calls' must be followed by tool messages"— the duplicated history placestool_callsmessages where the matchingtoolresult is displacedExpected behavior
Agents inside a
HandoffBuilderworkflow should not accumulate a second copy of conversation history. The executor is the single source of truth.Current workaround
Create a no-op
ContextProviderand assign it to every workflow participant that doesn't already have one: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)
HandoffBuilderorHandoffAgentExecutorshould disableInMemoryHistoryProviderauto-injection for all participants, since it owns the conversation state.Agent(..., history_provider=None)orAgent(..., auto_history=False)so developers can explicitly disable it without a dummy provider.HandoffBuilder/ orchestration docs explaining that agents with emptycontext_providerswill getInMemoryHistoryProviderauto-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.0agent-framework-orchestrations==1.0.0b260402agent-framework-openai==1.0.0/agent-framework-foundry==1.0.0OpenAIChatCompletionClientandFoundryChatClient