Part 7: Implementing commands

Skills are loaded by the agent. Commands are invoked by the user. That is the main difference.

When the user types /kid, there is no inference step. The user is saying which command to run. The system should load that command, render its prompt, and give the rendered prompt to the agent.

Two implementation options

There are two practical ways to implement slash commands:

  • Intercept the user input before the runner sees it.
  • Expose command execution as a tool.

The first version is closer to the user-facing shape in coding agents. If the input starts with /, your application loads the command, replaces the input with the rendered prompt, and sends that prompt to the model.

The notebook follows the second version because it is easier to add without changing ToyAIKit's runner. We create an execute_command tool and instruct the agent to call it when the user asks for a slash command.

Command markdown format

Commands are markdown files with YAML frontmatter. Unlike skills, each command is usually one file, not a folder.

The /kid command has this shape:

---
description: The Claude Code Kid asks for random coding things to implement!
---

You are the Claude Code Kid - a wildly UNPREDICTABLE coding child!
Come up with something NEW and SURPRISING each time!

## YOUR MISSION:
Invent a COMPLETELY ORIGINAL coding request each time.

For the notebook demo, create a commands/ folder and download the two command files from the opening demo:

mkdir commands
cd commands

wget https://raw.githubusercontent.com/alexeygrigorev/claude-code-kid-parent/refs/heads/main/.claude/commands/kid.md
wget https://raw.githubusercontent.com/alexeygrigorev/claude-code-kid-parent/refs/heads/main/.claude/commands/parent.md

The command name comes from the filename. kid.md becomes /kid, and parent.md becomes /parent.

The Command data class

Create a small command object:

@dataclass
class Command:
    name: str
    description: str
    template: str

The command body is called a template because command files can contain placeholders such as $1, $2, and $ARGUMENTS. The notebook keeps template processing simple. The prototype implements the substitutions.

The command loader

Load one command by name:

class CommandLoader:
    def __init__(self, commands_dir: Path | str = None):
        self.commands_dir = Path(commands_dir)

    def load_command(self, name: str) -> Command:
        command_file = self.commands_dir / f"{name}.md"
        if not command_file.exists():
            return None

        parsed = frontmatter.load(command_file, encoding="utf-8")
        metadata = dict(parsed.metadata)

        return Command(
            name=name,
            description=metadata.get("description", ""),
            template=parsed.content,
        )

List all command files with *.md:

    def list_commands(self) -> list[Command]:
        """List all available commands."""
        commands = []

        if not self.commands_dir.exists():
            return commands

        for md_file in sorted(self.commands_dir.glob("*.md")):
            name = md_file.stem
            command = self.load_command(name)
            commands.append(command)

        return commands

Create the loader and test it:

command_loader = CommandLoader(Path("commands/"))
command_loader.load_command("kid")

The result should include the description from frontmatter and the command body as template.

The command tool

The notebook uses a placeholder processor that returns the template unchanged:

def process_template(template: str, arguments: str) -> str:
    # process $1, $2, $3, $ARGUMENTS
    return template

Wrap the loader as a tool:

class CommandsTool:
    def __init__(self, command_loader: CommandLoader):
        self.command_loader = command_loader

    def execute_command(self, name: str, arguments: str = "") -> str:
        """Execute a command by name and return the rendered prompt.

        If you see input starting with /command, use this tool to load and execute it.
        If the command doesn't exist, let the user know.

        Args:
            name: The command name (without the / prefix).
            arguments: Optional arguments to substitute into the template.

        Returns:
            The rendered command template as a string.
        """
        if name.startswith("/"):
            name = name.lstrip("/")

        command = self.command_loader.load_command(name)
        if not command:
            return f"Command not found: /{name}"

        return process_template(command.template, arguments)

The lstrip("/") is defensive. Smaller models sometimes include the slash even when the tool docstring asks for the command name without it.

Create the tool and test it directly:

commands_tool = CommandsTool(command_loader=command_loader)
commands_tool.execute_command("kid")

Then expose it to the agent:

tools_obj.add_tools(commands_tool)

Now the registry contains the coding tools, the skill tool, and execute_command.

Tell the agent not to confuse skills and commands

Update the injected prompt so the model knows the distinction:

SKILL_INJECTION_PROMPT = f"""
You have the following skills available which you can load with the skills tool:

{skill_loader.get_description()}

Don't confuse skills and commands:

- Skills are discovered automatically, without user explicitly asking for it
- Instructions to execute commands are given explicitly: "/test" -> "run the 'test' command"

When you see "/command", use the tools to execute the command "command"
""".strip()

Append it to the base prompt again:

AGENT_WITH_SKILLS_INSTRUCTIONS = (
    AGENT_INSTRUCTIONS + "\n\n" + SKILL_INJECTION_PROMPT
)

This is wordier than the skill-only version because gpt-4o-mini benefits from direct instructions. A stronger model may infer the pattern from tool descriptions, but the workshop keeps it explicit.

Run /kid

Create a new runner with the updated tools and prompt:

runner = OpenAIResponsesRunner(
    tools=tools_obj,
    developer_prompt=AGENT_WITH_SKILLS_INSTRUCTIONS,
    llm_client=llm_client,
    chat_interface=chat_interface,
)

Run it:

result = runner.run()

Ask the agent to run the kid command:

/kid

The agent should call execute_command, load kid.md, and return a playful app idea. The exact idea does not matter. What matters is that /kid becomes the command template before the agent acts on it.

If you try a missing command such as /test without a test.md file, the tool returns:

Command not found: /test

That is enough for the notebook version. The prototype adds a more realistic command layer, argument substitution, and catches /command before it reaches the model.

Continue with Part 8: The fuller prototype to connect the notebook code to the fuller implementation.

Questions & Answers (0)

Sign in to ask questions