02

The Tool-Using Response Loop

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

The enterprise problem and today’s slice

Enterprise problem: A safe response agent is a bounded model–tool loop, not a model with direct provider access: the mailbox owner needs scheduling completed without exceeded authority, repeated timeout effects, or missing proof.

Whole-course context: The incoming artifact is Day 01’s validated respond route and zero-side-effect trace; today replaces that response placeholder with a bounded model–tool loop while preserving application-owned control flow.

Today’s slice: Expose typed read and write capabilities, execute calls through a policy-enforcing tool boundary, and stop when the model requests no further tool.

End-of-day evidence: A correlated trace proves Alice’s availability lookup and single calendar mutation, while denial and replay probes record no additional side effect.

Still unsolved: Durable preferences, episodic learning, procedural updates, background extraction, and production-scale retrieval remain outside this slice.

A response agent is a loop

A scheduling request cannot be completed reliably by one completion because each tool result changes the next valid choice. The smallest complete response model is propose → execute → observe, repeated until the latest model message contains no tool call.

  • General rule: Alternate reasoning and action; never let a model claim a tool result it has not observed.
  • Simple example: The model reads availability, sees 14:00, then chooses that slot.
  • Realistic example: Alice’s request causes an availability read, one authorized meeting write, and a confirmation grounded in the provider reference.
  • Failure mode: The model drafts “scheduled” before creation, or loops forever after repeated tool errors.
  • Decision rule: Use the loop when later steps depend on external results. Use a fixed pipeline when the step sequence is known and model choice adds no value; always cap iterations, time, cost, and tool errors.

This alternating pattern is often called ReAct-style execution. The important property is not the label but the interface: the model returns a proposal, application code returns an observed result, and graph state carries both.

Expose narrow operational tools

A broad tool hides authorization, validation, and retry semantics, so operators cannot tell which capability produced an effect. Narrow typed tools make capability and authority reviewable.

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)
  • General rule: One tool should express one bounded capability with typed inputs and a result the model can verify.
  • Simple example: A read-only availability tool cannot create a meeting.
  • Realistic example: The calendar write accepts attendee, date, time, and runtime context; it does not accept an unconstrained “do scheduling” prompt.
  • Failure mode: do_email_work(request: str) combines reads, writes, recipients, and approvals behind one opaque string.
  • Decision rule: Split tools when operations differ in authority, side-effect risk, retry safety, owner, or audit requirements. Avoid exposing a tool merely because the model might find it convenient.

Removing a tool removes the model’s path to that effect for this execution. It does not by itself grant authorization to tools that remain.

Add the model and tool nodes

Tool definitions alone do not create a loop, and careless state updates can erase the history needed for the next decision. The graph must route tool proposals to an executor and append results before invoking the model again.

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")

The messages field needs an append reducer in production so each node contributes messages rather than replacing history. Route unknown tools and malformed arguments to a contained error result; do not improvise a substitute capability.

Put authorization inside the tool boundary

A syntactically valid model call can still target the wrong tenant or resource, so tool selection must never be treated as permission. Every side-effecting tool independently resolves authenticated context, checks policy, validates arguments, and writes audit evidence.

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,
    )
  • General rule: The authenticated runtime supplies identity and tenant scope; the model supplies task arguments, never authority.
  • Simple example: A valid time with a wrong-tenant calendar ID is denied.
  • Realistic example: Alice’s event is created only after calendar.event.create is allowed on the owner’s calendar.
  • Failure mode: The prompt says “only use my calendar,” but the connector trusts a model-provided calendar ID.
  • Decision rule: Enforce authorization at the last trusted boundary before the effect. If the provider supports narrower delegated credentials, combine them with application policy rather than choosing one or the other.

Make retries safe with idempotency

Provider timeouts are ambiguous: the event may exist even though the runtime did not receive confirmation. Retrying a write without a stable identity can create duplicate meetings or emails.

  • General rule: Claim a stable idempotency key before a non-idempotent effect and return the first terminal result for replays.
  • Simple example: Two deliveries of the same webhook yield one calendar event.
  • Realistic example: The key combines tenant, source message, action kind, attendee, and intended slot; a retry after timeout reconciles the original provider reference.
  • Failure mode: The key includes wall-clock time, so every retry appears new, or it is too broad and suppresses a genuinely different meeting.
  • Decision rule: Reads may usually retry within bounded limits. Writes require provider idempotency or an application ledger plus reconciliation; if neither exists, stop for human recovery after an uncertain outcome.

Trace, test, and stop the loop

Tool-call accuracy alone can look healthy while denied calls mutate state or replay creates duplicates. Test the observable effect and the terminal condition for the same recurring request.

For “Find a suitable time tomorrow and invite Alice,” triage supplies respond. The model calls availability; the tool returns 10:30, 14:00, and 16:00. The model chooses 14:00, the tool boundary authorizes and claims idempotency, the provider returns one event reference, and the next model message contains no tool call. The graph ends with a grounded confirmation.

Now run the negative pair: use a wrong tenant and replay the accepted action. Require a denial before provider contact, one unchanged provider event, the original replay result, and a successful authorized control. Also test unknown tools, malformed arguments, provider timeout, maximum-iteration exhaustion, and a tool result that requires another step.

The action at the end of this day is concrete: do not add memory until the happy trace proves read-before-write and the negative trace proves no additional side effect.

Key takeaways

An agent loop is safe only when proposals, authority, execution, and evidence remain separate. The model chooses among capabilities; trusted application boundaries decide whether an effect may occur.

  • The response agent alternates model reasoning with tool execution until no tool calls remain.
  • Narrow tools expose bounded capabilities and make side effects observable.
  • Tool code—not the model—enforces identity, tenant scope, policy, validation, and audit.
  • Stable idempotency keys make retries distinguishable from new logical actions.
  • The strongest tests assert both the intended effect and the absence of forbidden or duplicate effects.

Checklist

A plausible final message does not prove that the tool loop behaved correctly. Complete these effect-level checks before continuing to cross-request memory.

  • [ ] I can draw and bound the propose → execute → observe loop.
  • [ ] I can explain why one broad “do work” tool is weaker than narrow typed tools.
  • [ ] I can distinguish tool selection, authentication, and authorization.
  • [ ] I can show how one stable key prevents a duplicate meeting after replay.
  • [ ] I can produce a correlated happy trace and a denial/replay trace with provider event counts.