You added a human approval gate to your agent last year. The code still runs — mostly — but if you cracked open the framework docs today you'd find that the exact function you called has been renamed, deprecated, or replaced. This isn't drift at the edges. Between late 2025 and mid-2026, three of the five major agent frameworks changed the public API for tool approval, and the tutorials ranking on your search still teach the old names.

Here's the current map. Skim the table, then take the two-line fix for whichever framework you're on.

The one-glance version#

FrameworkOld (stale)Current (mid-2026)
Vercel AI SDKneedsApproval on the tooltoolApproval on the call
LangChain / LangGraphHumanInterruptConfigHumanInTheLoopMiddleware
Pydantic AIDeferredToolCallsDeferredToolRequests
OpenAI Agents SDKneedsApprovalneedsApproval (+ RunState)
Claude Agent SDKad-hoc promptscanUseTool + hooks

The APIs converged on the same idea — pause a tool call, ask a human, resume — which we covered when tool approval became a framework default. What changed since is the spelling. Below, the current spelling for each, with runnable code.

1. Vercel AI SDK: needsApprovaltoolApproval (the biggest churn)#

If you learned the AI SDK 6 pattern, you put needsApproval inside the tool definition. In AI SDK 7 that's deprecated for streamText / generateText / ToolLoopAgent — it survives only as a compatibility fallback. The current API is toolApproval, set at the call level, so the same tool can be gated differently in different contexts:

await streamText({
  model: yourModel,
  tools: { deleteFile },
  toolApproval: {
    // return undefined to auto-approve, 'user-approval' to gate
    deleteFile: async ({ path }) =>
      path.startsWith('/tmp/') ? undefined : 'user-approval',
  },
});

The approval statuses are 'not-applicable' | 'approved' | 'denied' | 'user-approval'. The stream emits a tool-approval-request part (approvalId, toolCallId, toolName, input); you answer with a tool-approval-response (approvalId, approved, optional reason). On the client, useChat exposes addToolApprovalResponse({ id, approved }), and the tool part surfaces with state: 'approval-requested'. One exception worth knowing: the durable WorkflowAgent still uses needsApproval by design — so "deprecated" applies to the streaming path, not the workflow path.

2. LangChain / LangGraph: middleware replaced the config object#

Two layers, and the high-level one moved. The low-level primitive is unchanged: interrupt() plus Command(resume=…), which requires a checkpointer to survive a restart —

from langgraph.types import interrupt

@tool
def send_email(to: str, subject: str, body: str):
    decision = interrupt({"action": "send_email", "to": to,
                          "message": "Approve sending this email?"})
    return "sent" if decision.get("action") == "approve" else "cancelled"

# resume durably — checkpointer + thread_id required:
graph.invoke(Command(resume={"action": "approve"}),
             config={"configurable": {"thread_id": "wf-123"}})

The high-level path is what changed. LangChain v1 retired HumanInterruptConfig / ActionRequest in favor of HumanInTheLoopMiddleware with an interrupt_on map, wired through create_agent:

from langchain.agents import create_agent
from langchain.agents.middleware import HumanInTheLoopMiddleware
from langgraph.checkpoint.memory import InMemorySaver

agent = create_agent(
    model="...",
    tools=[write_file, execute_sql, read_data],
    middleware=[HumanInTheLoopMiddleware(interrupt_on={
        "write_file": True,                                   # always ask
        "execute_sql": {"allowed_decisions": ["approve", "reject"]},
        "read_data": False,                                   # auto-run
    })],
    checkpointer=InMemorySaver(),
)
agent.invoke(Command(resume={"decisions": [{"type": "approve"}]}), config=config)

The rename in the v1 migration notes: HumanInterruptConfigInterruptOnConfig, HumanInterruptHITLRequest. The four decision types are approve / edit / reject / respond (respond returns human text as the tool result).

3. Pydantic AI: DeferredToolCallsDeferredToolRequests, plus an inline handler#

The type got renamed in PR #5459 — old tutorials referencing DeferredToolCalls won't import. The stop-the-world, cross-process flow: mark the tool requires_approval=True, add DeferredToolRequests to output_type, the run ends with that output, and you resume a new run with deferred_tool_results=DeferredToolResults(approvals={…}) using ToolApproved(override_args=…) / ToolDenied(message=…).

New in 2026 is a path that doesn't end the run — the HandleDeferredToolCalls capability resolves approvals in a handler inline:

from pydantic_ai import Agent, DeferredToolRequests, DeferredToolResults, ToolDenied
from pydantic_ai.capabilities import HandleDeferredToolCalls

async def handle_deferred(ctx, requests: DeferredToolRequests) -> DeferredToolResults:
    approvals = {}
    for call in requests.approvals:
        approvals[call.tool_call_id] = (
            ToolDenied('Deleting files is not allowed')
            if call.tool_name == 'delete_file' else True)
    return requests.build_results(approvals=approvals)

agent = Agent('...', capabilities=[HandleDeferredToolCalls(handler=handle_deferred)])

@agent.tool_plain(requires_approval=True)
def delete_file(path: str) -> str: ...

4. OpenAI Agents SDK: still needsApproval, resumed via RunState#

The odd one out — its API held stable. needsApproval (a boolean or an async predicate) is still current; the run surfaces result.interruptions, and you approve or reject each, then resume:

let result = await runner.run(agent);
if (result.interruptions.length > 0) {
  result.interruptions.forEach(i => result.state.approve(i)); // or .reject(i)
  result = await runner.run(agent, result.state);             // resume
}

// durable across processes:
const serialized = result.state.toString();
const restored   = await RunState.fromString(agent, serialized);

That toString() / RunState.fromString() pair is the whole durability story: the pause is an object you can drop in a database and resume in a different process later.

5. Claude Agent SDK: canUseTool plus hooks#

Runtime approval is the canUseTool (TS) / can_use_tool (Python) callback, which can also mutate the tool input before it runs:

async def can_use_tool(tool_name, input_data, context):
    if await ask_user(f"Allow {tool_name}?"):
        return PermissionResultAllow(updated_input=input_data)
    return PermissionResultDeny(message="User denied this action")

options = ClaudeAgentOptions(can_use_tool=can_use_tool)

For approval logic that must run on every call regardless of allow-rules, use a PreToolUse hook (a hook deny wins even under bypassPermissions). And for true durability — the process exits and resumes much later — return the defer hook decision so the session persists rather than blocking a live callback.

The decision under the syntax: where does the pause live?#

Renames are the easy part. The load-bearing choice is where your paused approval state lives, and the five frameworks split cleanly into two families:

ModelHow the pause is heldFrameworksRight when…
In-process pauseSits in the running process; durable only with a checkpointer or persisted sessionLangGraph interrupt(), Claude canUseTool (defer)Approvals resolve in seconds and your process stays up
Serialize-and-resumeAn object you can store in a DB and resume in another process laterOpenAI RunState, Pydantic DeferredToolRequests, Vercel WorkflowAgentApprovals take minutes-to-hours and you redeploy often

If a human might take twenty minutes to click approve and your app redeploys twice a day, an in-memory pause loses the pending request on the next deploy — the failure mode we walked through in making human approval survive an agent restart. Choose the serialize-and-resume family, or add a checkpointer, before you ship.

While you're in there: gate what the model sees, too#

The same agents drowning in approvals are usually drowning in tools. Approval gating controls what the agent is allowed to do; tool search and defer_loading control what it's allowed to see. Pair them: defer-load your tool definitions so only the relevant handful enter context, then gate the dangerous few behind a durable approval. See fewer tools, approve the risky ones, and keep the pause somewhere a redeploy can't erase — that's the mid-2026 shape of a safe agent, current API names and all.