Skip to content

Python: AG-UI + HistoryProvider: compose history+input without duplicating turns (UI vs LLM context authorities) #8135

Closed

Description

@likebean

Summary

When hosting AG-UI with a durable HistoryProvider (UI transcript vs LLM working memory as two authorities), the framework should dedupe when assembling model context (history + run input). Today that composition is a blind concatenate; hosts must invent wrappers (e.g. “latest user turn only”) to avoid stacking the same turns twice.

Two authorities (intended split)

Store Authority Role
AG-UI thread snapshot UI transcript Hydrate / refresh / Allow-Deny cards; client often re-sends a full messages list
HistoryProvider LLM context Prior turns loaded via before_run / per-service-call middleware into SessionContext

This is a common production shape (also reflected in the ag_ui_assistant_ui_chat sample, which adds a host-level LatestUserTurnAgent to truncate AG-UI input before Agent.run).

What the framework does today

  1. AG-UI adapter (run_agent_stream) passes the reconstructed AG-UI message list straight into agent.run(messages, …) — no “latest turn only” truncation inside agent_framework_ag_ui.
  2. Agent core builds model messages as SessionContext.get_messages(include_input=True) → context (history) then extend(input_messages) with no filter_new_messages.
  3. filter_new_messages exists and is used on persist (save_messages) to avoid superlinear growth when replaying transcripts — it is not applied when composing what the model sees.
  4. AG-UI session continuation deliberately excludes HistoryProvider source ids from serialized continuation state, so the default examples (no durable history) stay correct: empty history + full AG-UI messages each request. That default does not cover hosts that attach a durable, load_messages=True HistoryProvider.

Failure mode

With durable HistoryProvider + AG-UI full (or turn-sized) input:

HistoryProvider:  [user, assistant(tool_call), …]   # already saved (e.g. after an interrupt turn)
run input:        [user, assistant(tool_call), tool(result), …]  # same turn again from AG-UI / approval resolve
model sees:       history + input  → duplicated user / tool-call turns

HITL / approval resume makes this especially visible: the interrupt turn is often already in history, while Allow continues with the same user + assistant(tool_call) plus the new tool result.

Write-path dedupe may keep the DB clean; the prompt still stacks.

Expected behavior

When AG-UI (or any caller) coexists with a HistoryProvider that loads prior turns:

  • Model context should be equivalent to history + filter_new_messages(history, input) (or an equivalent overlap/suffix algorithm), so turns already present in history are not appended again from input_messages.
  • Ideally this lives in one place (e.g. get_messages(include_input=True) and/or PerServiceCallHistoryPersistingMiddleware reconstruction), so every host does not reimplement truncation wrappers.
  • Persist behavior can stay as today (filter_new_messages on save).

Optional / related: document the two-authority model explicitly for AG-UI hosts (snapshot = UI, HistoryProvider = LLM), and that relying on “empty history because continuation excludes history keys” is insufficient once a durable provider is attached.

Suggested direction (non-prescriptive)

  • Apply the existing filter_new_messages(existing=history, incoming=input) (or shared overlap logic) at compose time when include_input=True and history context is non-empty.
  • Add a regression test: durable HistoryProvider already contains [u, a]; run input is [u, a, tool]; messages sent to the chat client are [u, a, tool] once — not [u, a, u, a, tool].
  • Keep default AG-UI examples (no durable history) unchanged.

Environment

  • Package: agent-framework / agent-framework-ag-ui (Python)
  • Related host workaround pattern: sample python/samples/05-end-to-end/ag_ui_assistant_ui_chat (LatestUserTurnAgent truncates so HistoryProvider is not duplicated into the prompt)

Related

Activity

  1. added
    pythonUsage: [Issues, PRs], Target: Python
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on Sep 8, 2026
  2. changed the title [-]AG-UI + HistoryProvider: compose history+input without duplicating turns (UI vs LLM context authorities)[/-] [+]Python: AG-UI + HistoryProvider: compose history+input without duplicating turns (UI vs LLM context authorities)[/+] on Sep 8, 2026
  3. added
    agentsUsage: [Issues, PRs], Target: Single agent
    middlewareUsage: [Issues, PRs], Target: middleware related features
    on Sep 8, 2026
  4. removed
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on Sep 8, 2026
  5. Shivani767 commented on Sep 9, 2026

    @Shivani767
    Contributor

    Thanks for opening this. I agree that the duplication is better handled at the context composition boundary rather than requiring AG-UI hosts to truncate the input themselves.

    I’d like to take a look at implementing this by reusing the existing filter_new_messages(history, input) logic when include_input=True. The idea would be:

    • keep the existing persist-time deduplication unchanged
    • when building model context, use the loaded history as the existing context and only append the portion of input_messages that is not already represented there
    • leave the current behavior unchanged when there is no loaded history, which should preserve the existing AG-UI examples
    • add a regression test covering the resume case where history contains [user, assistant(tool_call)] and the incoming AG-UI messages contain [user, assistant(tool_call), tool(result)]

    I’ll first trace the SessionContext/get_messages() and middleware paths to make sure the deduplication is applied at the right layer and doesn’t affect callers that intentionally provide new messages.

    If that approach sounds reasonable, I’d be happy to work on a PR for this.

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: Python

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions