Skip to content

Ask before a tool runs

The person approves risky tool calls before they run. Refused calls do not run, and the model is told why.

Yes or no

from pathlib import Path
from typing import Literal

from alpineagents import Agent, State, loop, tool

RISKY = {"write_file"}


@tool
def write_file(path: str, content: str) -> None:
    """Create a file, or replace its content"""
    Path(path).write_text(content)


@loop(until=State.is_answered, limit=30)
def careful(agent: Agent, state: State):
    agent.think(state)
    for call in state.pending_calls:
        if call.name in RISKY:
            question = f"Run {call.name}({call.args})?"
            answer = agent.ask_human(state, question, returns=Literal["yes", "no"])
            if answer == "no":
                state.deny(call, "The user declined this call")
    if state.wants_tools():
        agent.use_tools(state)


agent = Agent(model="claude-sonnet-5", tools=[write_file], loop=careful)
print(agent.run("Write a short README for this folder"))
  1. agent.think(state) gets the reply. Tool calls wait in state.pending_calls.
  2. For each risky call, agent.ask_human asks the person. returns=Literal["yes", "no"] accepts only those answers.
  3. state.deny(call, reason) refuses the call. The model gets reason as that call's result.
  4. agent.use_tools(state) runs the calls that are still pending. Denied calls are no longer pending.

The default human is the terminal, which shows yes/no after the question and asks again on any other answer.

Add "always"

Let the person approve a tool once for the rest of the run. Replace the loop with:

@loop(until=State.is_answered, limit=30)
def careful(agent: Agent, state: State):
    agent.think(state)
    allowed = state.data.setdefault("allowed", set())
    for call in state.pending_calls:
        if call.name in RISKY and call.name not in allowed:
            question = f"Run {call.name}({call.args})?"
            choices = Literal["yes", "no", "always"]
            answer = agent.ask_human(state, question, returns=choices)
            if answer == "always":
                allowed.add(call.name)
            elif answer == "no":
                state.deny(call, "The user declined this call")
    if state.wants_tools():
        agent.use_tools(state)
  • state.data["allowed"] keeps the approved tool names across turns.
  • The model never sees state.data.

Tell the model how to do it differently

A refusal with a reason from the person helps the model choose another way. Inside the loop body above:

how = agent.ask_human(state, "What should it do instead?")
state.deny(call, f"The user declined this call. They said: {how}")