Part 7: Implementing commands

The agent loads skills on its own, while you invoke commands yourself. That's the main difference.

When you type /kid, there's no inference step. You're saying which command to run, and the system should load that command, render its prompt, and hand 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 form in coding agents. When the input starts with /, your application loads the command, replaces the input with the rendered prompt, and sends that prompt to the model.

In the notebook you follow the second version because it's easier to add without changing ToyAIKit's runner. We create an execute_command tool and instruct the agent to call it when you ask 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 uses this structure:

---
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, so 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. In the notebook you keep template processing simple, then 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

In the notebook you use 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, because 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,
)

Start the command-aware runner:

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 doesn't matter. You want to see /kid become 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's enough for the notebook version. The prototype adds a more realistic command layer with argument substitution, and catches /command before it reaches the model.

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

Questions & Answers

Sign up to ask questions, track your progress, and get access to other workshops · Already have an account? Sign in