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"))
agent.think(state)gets the reply. Tool calls wait instate.pending_calls.- For each risky call,
agent.ask_humanasks the person.returns=Literal["yes", "no"]accepts only those answers. state.deny(call, reason)refuses the call. The model getsreasonas that call's result.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}")
Related¶
- Progress and questions: answer questions from somewhere other than the terminal
- Testing: test the approval flow with
FakeHuman