DEV Community

Cover image for Agent Skills 101: Giving Your AI Hands, Eyes, and Safety Rails
Shantanav Kapse
Shantanav Kapse

Posted on

Agent Skills 101: Giving Your AI Hands, Eyes, and Safety Rails

When I first started building with Large Language Models, I remember feeling a strange mix of awe and frustration.
You could ask an LLM to write a Shakespearean sonnet about Kubernetes, and it would do it in four seconds. But the moment you asked it to check the current weather, read a local SQLite database, or summarize a newly uploaded PDF, it hit an invisible wall. It was like chatting with a genius trapped inside a soundproof glass cube: brilliant, articulate, but completely disconnected from the outside world.

That wall disappears the moment you introduce agent skills.
Giving an agent "skills" (often called tool use or function calling) is what turns a passive text generator into an active problem solver. But moving from a toy demo to something reliable requires more than just passing a list of functions to an API endpoint.
In this guide, we'll walk through what agent skills actually are under the hood, explore battle-tested orchestration patterns, and cover the practical guardrails you need to keep your system safe and stable.

1. Traditional LLMs vs. Agentic Systems: The Mental Model

In a traditional setup, interaction is purely linear:

User Input -> [ LLM ] -> Output Text

You supply a prompt, the model calculates probability distributions over tokens, and it returns a stream of text. If the answer requires real-time data or an external calculation, the model has to guess-which usually leads to confident hallucinations.
An agentic architecture transforms this linear request into a continuous reasoning loop.
Instead of rushing to produce a final answer, the model follows four distinct steps:
Observe: Assess the user's request and examine the current state or environment.
Think: Reason about the goal. Does it know the answer, or does it need external data?
Act: If it needs data or needs to make a change, it triggers a skill (an API call, a database query, or a script).
Reflect: It evaluates the output returned by that skill and decides whether the task is complete or if another step is required.

2. What Is an "Agent Skill" Under the Hood?

There is no mystical AI magic happening inside a tool call. An agent skill consists of two simple pieces:
A plain code function: A Python or JavaScript function you wrote that does something concrete (like fetching an API or parsing a file).
A structured schema: A JSON specification telling the LLM what the tool is named, what it does, and what arguments it expects.
Here is what that looks like in Python:

import json

# 1. The actual executable function
def get_user_subscription(user_id: str) -> dict:
    """Fetches subscription status for a given user ID from our system."""
    database = {
        "usr_101": {"tier": "Pro", "status": "active", "renewal_days": 12},
        "usr_102": {"tier": "Free", "status": "active", "renewal_days": 0},
    }
    return database.get(user_id, {"error": "User not found"})

# 2. The schema the LLM reads to know this skill exists
subscription_tool_schema = {
    "type": "function",
    "function": {
        "name": "get_user_subscription",
        "description": "Look up account subscription tier and status using a user ID.",
        "parameters": {
            "type": "object",
            "properties": {
                "user_id": {
                    "type": "string",
                    "description": "The unique user ID, formatted like 'usr_123'",
                }
            },
            "required": ["user_id"],
        },
    },
}
Enter fullscreen mode Exit fullscreen mode

When you send this schema alongside your user prompt, the LLM doesn't execute the function itself. Instead, it outputs a structured JSON object saying:
"Hey, I don't know the answer directly, but please run get_user_subscription(user_id='usr_101') and show me what it returns."
Your application executes the function locally, feeds the result back into the LLM as an observation, and the agent delivers the final answer.

3. Orchestration Patterns: When One Agent Isn't Enough

When you are starting out, it is tempting to give a single agent twenty different skills and hope for the best.
In practice, this causes "context pollution." The model gets overwhelmed by too many tool definitions, forgets instructions, and hallucinates parameters. To build reliable workflows, we split responsibilities across multiple specialized agents.

Pattern A: Orchestrator-Worker
Think of this like a project manager working with senior engineers:

Orchestrator-Worker pattern

The Orchestrator receives the high-level prompt, devises a multi-step plan, and delegates subtasks.
Each Worker has access only to 2–3 skills relevant to its specific domain.
Once the workers finish, the orchestrator aggregates the results into a cohesive final output.
Pattern B: Generator-Critic
Ever write an email when you were tired, only to cringe when reading it the next morning? That is why you need a critic.
The Generator creates an initial draft, writes a SQL query, or proposes a code patch using its skills.
The Critic inspects that output against a rubric (e.g., checking for SQL injection vulnerabilities, syntax errors, or missed requirements).
If the critic spots an issue, it provides structured feedback back to the generator to retry. This self-correction loop catches bugs before your code touches production.

4. State Management: The "Zero-Database" Philosophy

When building agent workflows, a common beginner trap is immediately reaching for complex infrastructure: vector databases, dedicated caching layers, and external message queues.
Before adding heavy databases, consider the zero-database approach: keep your state in-memory using clean state graphs.
A state graph is a mental and architectural model where every step in your agent's process is a node, and the transitions between them are edges based on conditional logic.

# A simple in-memory state dictionary
class AgentState:
    def __init__(self, task: str):
        self.task = task
        self.history = []
        self.current_worker = "planner"
        self.intermediate_results = {}
        self.iterations = 0
Enter fullscreen mode Exit fullscreen mode

By passing a single structured state dictionary between functions:
You avoid race conditions and synchronization headaches.
Your workflow remains fully reproducible and testable in local development.
You can persist the entire state to a plain JSON file if you need persistence across app reboots.
Only reach for an external database when you have long-running tasks lasting days or when your history exceeds LLM context windows.

5. The "Infinite Loop" Problem & Circuit Breakers

Here is a rite of passage for every AI developer: you give an agent a code execution tool, run it against a tricky task, walk away to grab coffee, and return to find it has executed 78 consecutive tool calls trying to fix the same syntax error-burning through your monthly API budget in six minutes.
Agents are relentless optimizers. If a tool fails, they will often retry with minor variations indefinitely unless you put hard boundaries in place.
Enter the Circuit Breaker:

class CircuitBreaker:
    def __init__(self, max_steps: int = 8, max_consecutive_errors: int = 3):
        self.max_steps = max_steps
        self.max_consecutive_errors = max_consecutive_errors
        self.step_count = 0
        self.error_count = 0

    def record_step(self, is_error: bool = False):
        self.step_count += 1
        if is_error:
            self.error_count += 1
        else:
            self.error_count = 0  # Reset consecutive error counter

        if self.step_count >= self.max_steps:
            raise RuntimeError("Circuit breaker tripped: Maximum step limit reached.")

        if self.error_count >= self.max_consecutive_errors:
            raise RuntimeError("Circuit breaker tripped: Too many consecutive tool failures.")
Enter fullscreen mode Exit fullscreen mode

Before invoking any tool or calling the LLM, tick your circuit breaker. If the agent gets stuck in a recursive failure spiral, your circuit breaker halts execution gracefully, logs the trace, and notifies you.

6. Sandboxing: Never Give an Agent Bare Metal

If one of your agent's skills is run_python_code or run_shell_command, never execute those commands directly on your primary workstation or host server.
Even a well-intentioned model might run os.remove() on an unintended folder, clone huge repos that fill up disk space, or spawn zombie processes.
Subprocess Isolation: At a bare minimum, run code through a restricted Python subprocess with strict timeouts and memory limits:

import subprocess

def run_isolated_code(script: str, timeout_seconds: int = 5) -> str:
    try:
        result = subprocess.run(
            ["python3", "-c", script],
            capture_output=True,
            text=True,
            timeout=timeout_seconds,
            check=True
        )
        return result.stdout
    except subprocess.TimeoutExpired:
        return "Execution timed out."
    except subprocess.CalledProcessError as e:
        return f"Error: {e.stderr}"
Enter fullscreen mode Exit fullscreen mode

Container Sandboxes: For production environments, spin up lightweight, ephemeral Docker containers or micro-VMs that are destroyed the moment the agent finishes its run.

7. Headless Mock Testing: Don't Go Broke Testing

Testing an agent by running live queries through an LLM API endpoint is slow, non-deterministic, and expensive.
Instead, practice headless mock testing:
Separate your tool logic from your LLM calling logic.
Test your tools with standard unit tests just like any normal software module.
Mock the LLM's tool-call response using static JSON fixtures to verify that your orchestration loop parses arguments, updates state, and handles exceptions correctly without spending a penny.

def test_user_subscription_tool():
    # Unit test the skill directly without invoking any LLM
    result = get_user_subscription("usr_101")
    assert result["tier"] == "Pro"
    assert result["status"] == "active"
Enter fullscreen mode Exit fullscreen mode

If your tool functions aren't reliable in isolation, wrapping them in an AI prompt will only amplify the failures.

8. Human-in-the-Loop (HITL): The Final Checkpoint

A simple rule of thumb for designing agent skills:
Automate reads. Guard writes.
Read actions (searching docs, analyzing data, running calculations) are safe to run autonomously.
Write actions (sending an email, modifying a production database, executing a bank transfer) should always trigger a human confirmation prompt.

def execute_database_update(query: str, state: AgentState):
    if not state.is_human_approved:
        return "Action paused. Awaiting explicit human approval to run update query."
    # Run the database query...
Enter fullscreen mode Exit fullscreen mode

By designing your state machine to pause execution when encountering high-risk actions, you get all the speed advantages of automation without the risk of accidental chaos.

Quick Reference Summary

Component Responsibility Failure Mode to Watch For
Tool Schema Declares name, types, and utility to the model. Ambiguous descriptions causing inappropriate tool selection.
Orchestrator Deconstructs goals and delegates to workers. Attempting to execute tasks directly rather than delegating.
Circuit Breaker Tracks iterations and limits consecutive errors. Silent infinite retry loops that drain API balances.
Execution Sandbox Isolates runtime commands from host machine. Accidental filesystem corruption or unbound memory leaks.
HITL Interceptors Enforces human approval for destructive mutations. Autonomous writes leading to silent data corruption.

Top comments (0)