Visitar URL original
Python: [Bug]: AgentExecutor `full_conversation` drops the user turn and the approval-request turn after a tool-approval resume · Issue #10288 · microsoft/agent-framework · GitHub
Skip to content

Python: [Bug]: AgentExecutor full_conversation drops the user turn and the approval-request turn after a tool-approval resume #10288

Open

Description

Observed Behavior

When an agent inside an AgentExecutor pauses for tool approval and the workflow resumes with the approval response, the AgentExecutorResponse.full_conversation sent downstream only has the approval response and the final reply. The original user message is gone. So is the assistant turn that held the function call and approval request.

A downstream AgentExecutor uses the default context_mode="full", so it gets this cut-down history as its input:

downstream full_conversation:
   ('user', ['function_approval_response'], '')
   ('assistant', ['text'], 'Deleted the DB as approved.')

With a real provider, the next agent gets a tool approval/result that matches no call and has no user question.

Expected Behavior

full_conversation after the resume should hold the whole turn, the same as when no approval is needed:

('user', ['text'], 'please delete the db')
('assistant', ['function_approval_request'], '')      # or the function_call
('user', ['function_approval_response'], '')
('assistant', ['text'], 'Deleted the DB as approved.')

Steps to Reproduce

  1. Build a workflow: AgentExecutor(agent) → downstream executor (or a second AgentExecutor).
  2. Run it with a user message. The agent returns a function_approval_request, so the workflow pauses.
  3. Resume with wf.run(responses={request_id: approval_response}).
  4. Look at AgentExecutorResponse.full_conversation downstream.

Minimal Reproduction

Offline, no API keys needed. Run with the repo's dev environment.

"""AgentExecutorResponse.full_conversation after an approval resume drops the original user turn
and the tool-call/approval-request turn."""
import asyncio
from typing import Any
from agent_framework import (AgentExecutor, AgentExecutorResponse, AgentResponse, AgentResponseUpdate,
                             AgentSession, BaseAgent, Content, Executor, Message, ResponseStream,
                             WorkflowBuilder, WorkflowContext, handler)

class ApprovalAgent(BaseAgent):
    def __init__(self) -> None:
        super().__init__(name="approver")
        self.calls = 0
    def run(self, messages=None, *, stream=False, session: AgentSession | None = None, **kw: Any):
        self.calls += 1
        if self.calls == 1:
            call = Content.from_function_call(call_id="c1", name="delete_db", arguments="{}")
            contents = [Content.from_function_approval_request(id="r1", function_call=call)]
        else:
            contents = [Content.from_text("Deleted the DB as approved.")]
        async def _run():
            return AgentResponse(messages=[Message(role="assistant", contents=contents)])
        async def _stream():
            yield AgentResponseUpdate(role="assistant", contents=contents)
        return ResponseStream(_stream(), finalizer=AgentResponse.from_updates) if stream else _run()

class Capture(Executor):
    seen: list = []
    @handler
    async def h(self, r: AgentExecutorResponse, ctx: WorkflowContext[None, str]) -> None:
        Capture.seen = [(m.role, [c.type for c in m.contents], m.text) for m in r.full_conversation]
        await ctx.yield_output("ok")

async def main():
    ae = AgentExecutor(ApprovalAgent(), id="agent")
    cap = Capture(id="cap")
    wf = WorkflowBuilder(start_executor=ae).add_edge(ae, cap).build()
    r = await asyncio.wait_for(wf.run("please delete the db"), 5)
    req = r.get_request_info_events()[0]
    await asyncio.wait_for(wf.run(responses={req.request_id: req.data.to_function_approval_response(approved=True)}), 5)
    print("downstream full_conversation:")
    for row in Capture.seen:
        print("  ", row)
    print("expected: starts with ('user', ['text'], 'please delete the db') and includes the approval request")

asyncio.run(main())

class RecordingAgent(BaseAgent):
    def __init__(self) -> None:
        super().__init__(name="reviewer")
        self.received: list = []
    def run(self, messages=None, *, stream=False, session=None, **kw: Any):
        self.received = [(m.role, [c.type for c in m.contents], m.text) for m in messages]
        async def _run():
            return AgentResponse(messages=[Message(role="assistant", contents=[Content.from_text("reviewed")])])
        return _run()

async def chained():
    a = AgentExecutor(ApprovalAgent(), id="agent")
    rec = RecordingAgent()
    b = AgentExecutor(rec, id="reviewer")  # default context_mode="full"
    wf = WorkflowBuilder(start_executor=a).add_edge(a, b).build()
    r = await asyncio.wait_for(wf.run("please delete the db"), 5)
    req = r.get_request_info_events()[0]
    await asyncio.wait_for(wf.run(responses={req.request_id: req.data.to_function_approval_response(approved=True)}), 5)
    print("\nchained AgentExecutor (context_mode='full') received:")
    for row in rec.received:
        print("  ", row)

asyncio.run(chained())

Root cause (what I found)

In _workflows/_agent_executor.py:

  • _run_agent_and_emit returns early when the agent asks for user input, so the cache from before the pause is not emitted.
  • On resume, _resume_with_pending_responses replaces the cache with only the replies (line ~552):
    self._cache = normalize_messages_input(messages)
  • _run_agent_and_emit then rebuilds the conversation from that cache (line ~782):
    self._full_conversation = [*self._cache, *(list(response.messages) if response else [])]
  • So the pre-pause user input and the paused response's messages never make it into full_conversation.

I couldn't find a test covering full_conversation across an approval resume.

Possible fix

Keep the pre-pause input and the paused response's messages when the executor pauses for user input, and prepend them when building _full_conversation on resume. I'm happy to open a PR with a regression test if the direction looks right.

Package Versions

agent-framework-core 1.25.0 (main @ 22b5e75)

Python Version

3.13.11

Operating System

macOS

Regression

Unknown

Additional Context

AI assistance: the issue analysis, the repro scripts and this write-up were prepared with an AI coding assistant (Claude Code). The output shown above comes from running the repros against main @ 22b5e75.

Activity

  1. added
    pythonUsage: [Issues, PRs], Target: Python
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on Nov 12, 2026
  2. added
    reproducedUsage: [Issues], Target: all issues that can be reproduced by the triage workflow
    workflowsUsage: [Issues, PRs], Target: Workflows
    on Nov 12, 2026
  3. github-actions commented on Nov 12, 2026

    @github-actions
    Contributor

    Reproduced on main @ 22b5e75 with an independently written offline script (fake approval-requesting agent → capture executor). After the approval resume, downstream full_conversation was [('user', ['function_approval_response']), ('assistant', ['text'])]: the original user message and the approval-request turn are missing. This matches the cause in _workflows/_agent_executor.py, where the resume path replaces _cache with only the responses and _full_conversation is rebuilt from it.

    Support boundary: unclear; no maintained spec or doc was checked that defines full_conversation across an approval resume, so a maintainer needs to confirm the intended contract. Routed to workflows; priority and scope are for maintainers. Evidence: https://github.com/microsoft/agent-framework/actions/runs/43162812784

    Generated by Issue Routing for #10288 · copilot · auto · ~24 AIC · ⌖ ~1 AIC · ⊞ 5.2K · ◷

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

Metadata

Metadata

Labels

pythonUsage: [Issues, PRs], Target: PythonreproducedUsage: [Issues], Target: all issues that can be reproduced by the triage workflowtriageUsage: [Issues], Target: All issues that still need to be triagedworkflowsUsage: [Issues, PRs], Target: Workflows

Type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions