02

The Tool-Using Response Loop

Open the response box: let the model reason, call one bounded capability, inspect the result, and continue until the job is finished.

A response agent is a loop

An email classified as respond often needs more than a single completion. A scheduling request may require reading availability, selecting a suitable slot, creating an invitation, and drafting a confirmation. Each result changes what the next step should be.

The response agent is therefore a loop between a tool-enabled model and a tool executor. The model proposes a call. Application code validates and executes it. The result is appended to graph state, and the model runs again. Execution ends only when the latest model message contains no more tool calls.

This is often called a ReAct-style loop: reasoning and action alternate. The graph makes that alternation visible and bounded.

┌────────────────┐   tool call   ┌────────────────┐
│  AGENT MODEL   │ ────────────▶ │   TOOL NODE    │
└───────▲────────┘               └───────┬────────┘
        │             result             │
        └────────────────────────────────┘
                repeat or finish

Expose narrow operational tools

Tools are the agent’s capabilities and its authority boundary. A model cannot schedule a meeting merely because it can describe one; a calendar tool must exist and accept the call. Removing a tool removes that capability from the current execution.

from langchain_core.tools import tool

@tool
def write_email(recipient: str, subject: str, body: str) -> str:
    """Prepare an email draft for explicit delivery."""
    return f"Draft prepared for {recipient}"

@tool
def check_calendar_availability(date: str) -> list[str]:
    """Return free slots for one ISO date."""
    return ["10:30", "14:00", "16:00"]

@tool
def schedule_meeting(attendee: str, date: str, time: str) -> str:
    """Create one meeting using an approved time slot."""
    return f"Meeting scheduled with {attendee} at {time}"

tools = [write_email, check_calendar_availability, schedule_meeting]
agent_model = model.bind_tools(tools)

Good contracts use specific names and typed arguments. A single do_email_work(request: str) tool hides too much: authorization, validation, retries, and audit become ambiguous. Narrow tools make each side effect observable.

Add the model and tool nodes

The response node invokes the tool-enabled model with accumulated messages. The prebuilt ToolNode executes declared calls and returns tool messages to state.

from langgraph.graph import END
from langgraph.prebuilt import ToolNode

def response_agent(state: EmailState):
    response = agent_model.invoke(state["messages"])
    return {"messages": [response]}

def route_agent(state: EmailState):
    last_message = state["messages"][-1]
    return "tools" if last_message.tool_calls else END

builder.add_node("response_agent", response_agent)
builder.add_node("tools", ToolNode(tools))
builder.add_conditional_edges("response_agent", route_agent)
builder.add_edge("tools", "response_agent")

In production, messages should use an append reducer so each node contributes new messages instead of replacing history. The code here focuses on the topology: model, tool, result, model again.

Put policy inside the tool boundary

The model’s decision to call a tool is not authorization. Every side-effecting tool must independently verify the authenticated actor, tenant, resource scope, and current policy. It should validate arguments and record an audit event before or atomically with the change.

A calendar tool might require these inputs even if the model never sees all of them:

async def schedule_meeting(args, context):
    policy.require(
        actor=context.actor,
        action="calendar.event.create",
        resource=context.calendar_id,
        tenant=context.tenant_id,
    )
    slot = MeetingSlot.model_validate(args)
    return await calendar.create_event(
        slot,
        idempotency_key=context.idempotency_key,
    )

Authentication proves who initiated the request. Authorization proves that actor may perform this action on this resource. The LLM supplies neither.

Make retries safe with idempotency

Networks fail after a provider accepts a request but before the agent receives the response. Blind retrying can send an email twice or create duplicate meetings. A stable idempotency key lets the tool provider—or an application-side record—recognize the same logical action.

The key should derive from a durable request or action identifier, not from the current wall-clock time. Retrying a read is usually safe; retrying a write requires this explicit design.

request ─▶ authorize ─▶ validate ─▶ claim idempotency key
                                        │
                         ┌───────────────┴──────────────┐
                         ▼                              ▼
                    first call                    replayed call
                         │                              │
                         ▼                              ▼
                  perform effect                return old result

Trace a scheduling request

For “Find a suitable time tomorrow and invite Alice,” triage selects respond. The response agent calls check_calendar_availability(date="2026-08-06"). The tool returns three slots. The model chooses 14:00 and calls schedule_meeting with Alice, the date, the time, and the execution context’s idempotency key. After confirmation, the model produces a final message and the graph ends.

At this point the system can act, but it cannot remember that the user avoids early meetings. It chose 14:00 from current availability, not from durable preference. That missing continuity is the boundary for Day 3.

Failure modes to test

Graph tests should cover a model that asks for an unknown tool, malformed tool arguments, a denied authorization decision, a provider timeout, a duplicate webhook, and a tool result that requires another reasoning step. A model evaluation alone cannot prove these runtime behaviors.

The highest-risk assertion is negative: a denied or replayed action produces no additional side effect. Tool-call accuracy is useful, but “nothing happened when it should not” is often the safety property that matters most.

Key takeaways

  • The response agent alternates model reasoning with tool execution until no tool calls remain.
  • Tools define bounded capabilities; no exposed tool means no authorized path to that effect.
  • Tool code—not the model—enforces identity, tenant scope, policy, validation, and audit.
  • Side-effecting operations need stable idempotency keys before retries are safe.
  • Test graph paths and observable effects, including denial and replay behavior.

Checklist

  • [ ] I can draw the model → tool → result → model loop.
  • [ ] I can explain why one broad “do work” tool is weaker than narrow typed tools.
  • [ ] I can distinguish tool selection from authorization.
  • [ ] I can describe how an idempotency key prevents a duplicate meeting.
  • [ ] I can name one negative side-effect test for the response path.