Thuta Learning
AdvancedAIbeginner

Local AI Agents and Tool Calling

What you'll walk away with

  • Explain the core ideas behind Local AI Agents and Tool Calling
  • Read the diagram and trace how data or requests flow through the architecture
  • Decide what this means for your own hardware and use case

Build the mental model

A plain LLM is a function: text goes in, text comes out, and nothing in the world changes as a result. An agent wraps that same LLM with four additional pieces

  • Memory — state carried across steps
  • Tools — functions the model can invoke, like reading a file or calling an API
  • Planning — breaking a goal into steps
  • Execution — actually running those steps and feeding results back

The LLM itself doesn't change; what changes is that its text output can now cause real side effects.

User request

The user sends a request to the LLM.

LLM decides

Given a list of available tools and their descriptions, the LLM decides whether answering requires calling one, and if so, emits a structured call (tool name plus arguments) instead of a plain-text answer.

Program executes

The surrounding program — not the model — actually executes that tool, since the LLM has no direct access to your filesystem, network, or shell; it only ever produces text describing what it wants to happen.

Result fed back

The tool's result is fed back into the conversation, and the LLM either calls another tool or produces a final answer.

Because the model decides which tool to call based on its own (sometimes wrong, sometimes manipulated) reasoning, the execution layer around it — not the model's judgment — is where safety has to live.

  • Least-privilege access — grant only the specific tools a task needs, nothing broader
  • A sandboxed workspace — a directory or container the agent cannot escape
  • Read-only tools by default, with write/delete access opt-in
  • An explicit allowlist of permitted tools rather than an open-ended shell
  • Validation of tool arguments before execution
  • Audit logging of every call
  • Timeouts and network restrictions

An agent with unrestricted file or system access is not a more capable assistant — it is a standing risk.

Agent
An LLM combined with memory, tools, planning, and an execution layer that can carry out multi-step tasks with real side effects, rather than only producing a single text response.
Tool Calling
The mechanism by which an LLM emits a structured request to run a specific function with specific arguments, which the surrounding program executes and feeds the result of back into the conversation.
text
THE TOOL-CALLING LOOP
---------------------
THE TOOL-CALLING LOOP
------------------------

  user request
       |
       v
     LLM  -----------------------------+
       |  decides a tool is needed     |
       v                               |
  tool call (name + arguments)         |
       |                               |
       v                               |
  [ CONFIRMATION GATE ]  <-- required for destructive tools
       |  (blocked until user confirms)
       v
  tool executes (outside the model)
       |
       v
  result returned to LLM  -------------+
       |
       v
  LLM responds to user (or calls another tool)

Connect it to a real scenario

The code example is a minimal version of a pattern every real agent framework implements in some form: a lookup table deciding whether a requested tool call is even allowed to run, checked before any execution happens, not after.

First, default-deny: a tool name that isn't in the allowlist at all is rejected outright, rather than the system trying to guess whether it's safe — `send_email` in the example fails simply because it was never granted, regardless of what arguments came with it.

Second, a separate destructive flag: tools that change or delete state (here, `delete_file` and `run_shell_command`) require an explicit `confirmed=True` on top of being allowlisted, modeling the idea that read access and write/delete access should never share the same bar.

A real implementation would check this before every single tool call an agent makes, log the decision either way, and treat `confirmed=True` as something only a human (or a policy the human explicitly set) can supply.

Never Let the Model Confirm Itself

`confirmed=True` should never be something the LLM can set for itself by including it in its own tool-call arguments — that would let a manipulated model confirm its own destructive actions.

Unrestricted Agent Access Is a Risk, Not a Feature

An agent that can read, write, and delete files or run arbitrary shell commands with no allowlist, no sandboxing, and no confirmation step is not a more powerful assistant — it is equivalent to giving an unattended, occasionally-wrong script full access to your system. Treat every destructive capability as opt-in and explicitly scoped, never as a default.

Try the working example

python
ALLOWED_TOOLS = {
    "read_file": {"destructive": False},
    "list_directory": {"destructive": False},
    "web_search": {"destructive": False},
    "delete_file": {"destructive": True},
    "run_shell_command": {"destructive": True},
}


def check_permission(tool_name, allowed_tools, confirmed=False):
    if tool_name not in allowed_tools:
        return False, "not in allowlist"
    if allowed_tools[tool_name]["destructive"] and not confirmed:
        return False, "destructive tool requires confirmed=True"
    return True, "permitted"


calls = [
    ("read_file", False),
    ("send_email", False),       # not in allowlist at all
    ("delete_file", False),      # destructive, no confirmation
    ("delete_file", True),       # destructive, confirmed
]

for tool_name, confirmed in calls:
    ok, reason = check_permission(tool_name, ALLOWED_TOOLS, confirmed=confirmed)
    print(f"tool={tool_name!r} confirmed={confirmed} -> allowed={ok} ({reason})")
You should see
Running the four example calls through check_permission produces:

tool='read_file' confirmed=False -> allowed=True (permitted)
tool='send_email' confirmed=False -> allowed=False (not in allowlist)
tool='delete_file' confirmed=False -> allowed=False (destructive tool requires confirmed=True)
tool='delete_file' confirmed=True -> allowed=True (permitted)

read_file is allowed immediately because it's both allowlisted and non-destructive. send_email is denied purely for not being in the allowlist at all — its destructive flag is never even checked. delete_file is denied without confirmation and allowed with it, showing the two-layer check (allowlist membership, then a separate confirmation requirement for destructive tools) working as designed.

5-minute try-it

Extend check_permission so that non-destructive tools can also be restricted by an optional allowed_paths argument (for example, read_file should only succeed if the requested path is inside a specific sandbox directory). Write test calls showing a read_file request inside the sandbox succeeding and one outside the sandbox being denied, even though read_file itself is allowlisted and non-destructive.

One important caution

Letting the LLM's own output set a 'confirmed' or 'approved' flag on a destructive tool call — if the model can grant its own permission, the confirmation step is decorative, not a real gate.

Giving an agent a general-purpose shell tool 'to keep things flexible' instead of specific narrow tools — an open shell defeats least-privilege and an allowlist at the same time.

Anthropic — Tool Use OverviewLocal AI / Local LLM

Easy traps

  • Letting the LLM's own output set a 'confirmed' or 'approved' flag on a destructive tool call — if the model can grant its own permission, the confirmation step is decorative, not a real gate.
  • Giving an agent a general-purpose shell tool 'to keep things flexible' instead of specific narrow tools — an open shell defeats least-privilege and an allowlist at the same time.
  • Try a new model or tool at a small scale before wiring it into a production or daily-use workflow.

Exercise

Extend check_permission so that non-destructive tools can also be restricted by an optional allowed_paths argument (for example, read_file should only succeed if the requested path is inside a specific sandbox directory). Write test calls showing a read_file request inside the sandbox succeeding and one outside the sandbox being denied, even though read_file itself is allowlisted and non-destructive.

You'll know it worked when: Running the four example calls through check_permission produces: tool='read_file' confirmed=False -> allowed=True (permitted) tool='send_email' confirmed=False -> allowed=False (not in allowlist) tool='delete_file' confirmed=False -> allowed=False (destructive tool requires confirmed=True) tool='delete_file' confirmed=True -> allowed=True (permitted) read_file is allowed immediately because it's both allowlisted and non-destructive. send_email is denied purely for not being in the allowlist at all — its destructive flag is never even checked. delete_file is denied without confirmation and allowed with it, showing the two-layer check (allowlist membership, then a separate confirmation requirement for destructive tools) working as designed.

Local AI Agents and Tool Calling | Thuta Learning