Summary
What is the intended / recommended way to manage conversation history when a SequentialBuilder workflow is exposed as a chat agent via workflow.as_agent() and an AgentSession?
We traced multi-turn runs and found three independent history layers that can all be fed to the same model call. Combining the documented as_agent() + session pattern with the default AgentExecutor session causes duplicated user turns (and compounding history) by turn 2. Different chat products also need different answers (sometimes a participant should see its own prior drafts; sometimes it should not), so it is hard to pick one composition at build time.
Looking for maintainer / community guidance, and optionally a docs note next to Sequential orchestration and workflow.as_agent().
What we observed
Setup (matches Sequential orchestration + workflow as agent with session):
from agent_framework import Agent
from agent_framework.openai import OpenAIChatCompletionClient
from agent_framework.orchestrations import SequentialBuilder
client = OpenAIChatCompletionClient(model="...", api_key="...", base_url="...") # local Chat Completions, STORES_BY_DEFAULT=False
writer = Agent(client=client, name="writer", instructions="You are a concise copywriter...")
reviewer = Agent(client=client, name="reviewer", instructions="You are a thoughtful reviewer...")
workflow = SequentialBuilder(participants=[writer, reviewer]).build()
agent = workflow.as_agent()
session = agent.create_session()
await agent.run("Write a tagline for a budget-friendly eBike.", session=session)
await agent.run("Make it shorter.", session=session)
We captured AgentContext.messages (what AgentExecutor passed into agent.run) vs ChatContext.messages (what the chat client actually sent after HistoryProvider merge).
Turn 1 is clean:
| Layer |
Writer model input |
| AgentExecutor cache |
[user: Write a tagline...] |
| Chat client |
same |
Outer WorkflowAgent session after the run |
[user, reviewer] (terminal output only; writer draft is not stored) |
Writer inner AgentExecutor._session |
[user, writer draft] |
Turn 2 (Make it shorter.):
Writer cache (outer session + new user) — 3 messages, no writer draft:
user: Write a tagline...
assistant (reviewer): previous review
user: Make it shorter.
Writer chat client input (inner InMemoryHistoryProvider prepended) — 5 messages:
user: Write a tagline... ← writer inner session
assistant (writer): Big ride, small price. ← writer inner session
user: Write a tagline... ← outer session, duplicate
assistant (reviewer): previous review ← outer session
user: Make it shorter.
By turn 3 the inner session has stored the already-duplicated cache (store_inputs=True), so the writer prompt keeps growing.
On the wire this is role=user|assistant (optional name=writer|reviewer). Local models often ignore name, so the reviewer text looks like the writer's own previous assistant message. Make it shorter then has an ambiguous antecedent.
This is with a local Chat Completions client (STORES_BY_DEFAULT=False), so Agent.run(..., session=AgentExecutor._session) auto-injects InMemoryHistoryProvider. Related: service-side continuation plus full message replay is the other duplication path tracked in #3295.
Why this is not just “turn inner history off”
Some products need the writer to see its own unpublished drafts (never returned as workflow output). Others need a single user-visible transcript. Others need per-agent tool traces that should not leak into the outer session. That mix is hard to freeze into one SequentialBuilder graph.
So the question is not only “how do I stop the duplicate user line?”, but which layer is supposed to be the source of truth for which kind of memory?
Related framework pieces
| Mechanism |
What it seems to be for |
SequentialBuilder / AgentExecutor context_mode (full / last_agent / custom) |
Same-run handoff between participants (from_response). Does not apply to the first node, which gets list[Message] via _InputToConversation → from_messages. |
chain_only_agent_responses=True |
Same-run: only previous agent’s response, not full conversation. |
workflow.as_agent() + AgentSession |
Cross-turn user ↔ workflow output (sample: workflow_as_agent_with_session.py). Default output is the last participant only. |
output_from / intermediate_output_from |
What lands in the outer session / agent response. |
Per-participant AgentExecutor._session + auto InMemoryHistoryProvider |
Created unconditionally in AgentExecutor.__init__ (session or agent.create_session()). |
| Shared-thread sample |
azure_ai_agents_with_shared_session.py inserts an empty AgentExecutorRequest(messages=[]) specifically to prevent duplication when a shared session is the source of truth. |
InMemoryHistoryProvider(load_messages=False) |
Does not block auto-injection of a loading provider (persist-only does not satisfy the loading need). |
Questions for maintainers / community
-
Intended source of truth for SequentialBuilder + as_agent() + session: outer WorkflowAgent session, Sequential full_conversation / _cache, each participant’s inner session, or a documented combination?
-
Recommended default so the model sees one linear transcript (no duplicated user turns), while still allowing a participant to see its own prior assistant messages when those were workflow outputs (output_from="all")?
-
When a participant needs private memory (drafts / tool traces the user never saw), is the intended hook a ContextProvider / custom HistoryProvider with dedup against input_messages, rather than a second chat replay?
-
Should docs for Sequential orchestration and workflow.as_agent() call out that AgentExecutor always attaches an inner session, and that local clients will auto-inject InMemoryHistoryProvider, which stacks with the outer session?
-
Is there (or should there be) a first-class switch such as “participants are stateless; conversation is owned by the workflow agent session”, vs “participants keep private sessions; Sequential should not also replay full history”?
-
Does .NET AgentWorkflowBuilder.BuildSequential have an analogous composition, and is the guidance the same?
Environment
agent-framework-core 1.14.0 (workspace checkout of microsoft/agent-framework)
agent-framework-orchestrations 1.1.0
- Python 3.12+, Chat Completions-compatible local endpoint (repro does not require Foundry)
Happy to add a small tracing sample or a docs PR once the intended policy is clear.
Summary
What is the intended / recommended way to manage conversation history when a
SequentialBuilderworkflow is exposed as a chat agent viaworkflow.as_agent()and anAgentSession?We traced multi-turn runs and found three independent history layers that can all be fed to the same model call. Combining the documented
as_agent()+ session pattern with the defaultAgentExecutorsession causes duplicated user turns (and compounding history) by turn 2. Different chat products also need different answers (sometimes a participant should see its own prior drafts; sometimes it should not), so it is hard to pick one composition at build time.Looking for maintainer / community guidance, and optionally a docs note next to Sequential orchestration and
workflow.as_agent().What we observed
Setup (matches Sequential orchestration + workflow as agent with session):
We captured
AgentContext.messages(whatAgentExecutorpassed intoagent.run) vsChatContext.messages(what the chat client actually sent afterHistoryProvidermerge).Turn 1 is clean:
[user: Write a tagline...]WorkflowAgentsession after the run[user, reviewer](terminal output only; writer draft is not stored)AgentExecutor._session[user, writer draft]Turn 2 (
Make it shorter.):Writer cache (outer session + new user) — 3 messages, no writer draft:
user: Write a tagline...assistant(reviewer): previous reviewuser: Make it shorter.Writer chat client input (inner
InMemoryHistoryProviderprepended) — 5 messages:user: Write a tagline... ← writer inner sessionassistant(writer): Big ride, small price. ← writer inner sessionuser: Write a tagline... ← outer session, duplicateassistant(reviewer): previous review ← outer sessionuser: Make it shorter.By turn 3 the inner session has stored the already-duplicated cache (
store_inputs=True), so the writer prompt keeps growing.On the wire this is
role=user|assistant(optionalname=writer|reviewer). Local models often ignorename, so the reviewer text looks like the writer's own previous assistant message.Make it shorterthen has an ambiguous antecedent.This is with a local Chat Completions client (
STORES_BY_DEFAULT=False), soAgent.run(..., session=AgentExecutor._session)auto-injectsInMemoryHistoryProvider. Related: service-side continuation plus full message replay is the other duplication path tracked in #3295.Why this is not just “turn inner history off”
Some products need the writer to see its own unpublished drafts (never returned as workflow output). Others need a single user-visible transcript. Others need per-agent tool traces that should not leak into the outer session. That mix is hard to freeze into one
SequentialBuildergraph.So the question is not only “how do I stop the duplicate user line?”, but which layer is supposed to be the source of truth for which kind of memory?
Related framework pieces
SequentialBuilder/AgentExecutorcontext_mode(full/last_agent/custom)from_response). Does not apply to the first node, which getslist[Message]via_InputToConversation→from_messages.chain_only_agent_responses=Trueworkflow.as_agent()+AgentSessionworkflow_as_agent_with_session.py). Default output is the last participant only.output_from/intermediate_output_fromAgentExecutor._session+ autoInMemoryHistoryProviderAgentExecutor.__init__(session or agent.create_session()).azure_ai_agents_with_shared_session.pyinserts an emptyAgentExecutorRequest(messages=[])specifically to prevent duplication when a shared session is the source of truth.InMemoryHistoryProvider(load_messages=False)Questions for maintainers / community
Intended source of truth for
SequentialBuilder+as_agent()+session: outerWorkflowAgentsession, Sequentialfull_conversation/_cache, each participant’s inner session, or a documented combination?Recommended default so the model sees one linear transcript (no duplicated user turns), while still allowing a participant to see its own prior assistant messages when those were workflow outputs (
output_from="all")?When a participant needs private memory (drafts / tool traces the user never saw), is the intended hook a
ContextProvider/ customHistoryProviderwith dedup againstinput_messages, rather than a second chat replay?Should docs for Sequential orchestration and
workflow.as_agent()call out thatAgentExecutoralways attaches an inner session, and that local clients will auto-injectInMemoryHistoryProvider, which stacks with the outer session?Is there (or should there be) a first-class switch such as “participants are stateless; conversation is owned by the workflow agent session”, vs “participants keep private sessions; Sequential should not also replay full history”?
Does
.NETAgentWorkflowBuilder.BuildSequentialhave an analogous composition, and is the guidance the same?Environment
agent-framework-core1.14.0 (workspace checkout ofmicrosoft/agent-framework)agent-framework-orchestrations1.1.0Happy to add a small tracing sample or a docs PR once the intended policy is clear.