Skip to content

.NET: Python: Best practice for conversation history when SequentialBuilder is used as workflow.as_agent() with a session? #7756

Description

@likebean

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:

  1. user: Write a tagline...
  2. assistant (reviewer): previous review
  3. user: Make it shorter.

Writer chat client input (inner InMemoryHistoryProvider prepended) — 5 messages:

  1. user: Write a tagline... ← writer inner session
  2. assistant (writer): Big ride, small price. ← writer inner session
  3. user: Write a tagline... ← outer session, duplicate
  4. assistant (reviewer): previous review ← outer session
  5. 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 _InputToConversationfrom_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

  1. 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?

  2. 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")?

  3. 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?

  4. 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?

  5. 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”?

  6. 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.

Activity

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

Metadata

Metadata

Labels

.NETUsage: [Issues, PRs], Target: .NetorchestrationUsage: [Issues, PRs], Target: multi-agent orchestration (high-level patterns)pythonUsage: [Issues, PRs], Target: Python

Type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions