The beauty of open source lies in its ability to breed innovation, which is why I was excited to discover Goose, an open source AI developer agent I could contribute to. Until now, many AI developer tools I've used have been closed source, limiting users to whatever features the product offers.
While watching other engineers interact with Goose, I was pleasantly surprised to learn that anyone can extend its capabilities. This means I can take Goose's base functionality and customize it for my own use cases, no matter how unconventional they might be. I've seen engineers use it for practical purposes, like summarizing repositories or integrating with the GitHub CLI, as well as experimental ones, like having Goose browse the web for plushy toys or enabling voice interactions.
Excited by these possibilities, I set out to create a toolkit of my own. I had ambitious ideas. I wanted to use Goose to:
- Assist in training AI models that interpret American Sign Language
- Generate memes
- Create a Sonic Pi integration for live coding music from the terminal
- Help plan Thanksgiving dinner
Unfortunately, I failed at all of these attempts. I initially thought the problem was that I'm not a Pythonista, but I quickly realized I didn't fully understand how to create toolkits—and if I didn't, perhaps others were in the same boat. In this blog post, I'll share how to create your first toolkit using a simple but practical example: a to-do list manager.
What Are Toolkits? 🤔
The Official Definition
According to the official Goose documentation, toolkits are plugins that "provide Goose with tools (functions) it can call and optionally will load additional context into the system prompt (such as 'The Github CLI is called via gh and you should use it to run git commands'). Toolkits can do basically anything, from calling external APIs, to taking screenshots, to summarizing your current project."
My Definition
Think of toolkits like applications on your phone. At baseline, your phone can make calls, but you can download apps that extend its capabilities—from playing games to taking pictures and listening to music. Similarly, while Goose starts with basic capabilities like managing sessions and versions, toolkits extend its functionality to:
- Take screenshots for debugging
- Interact with the GitHub CLI
- Manage Jira projects
- Summarize repositories
Want to see a toolkit in action? Check out my previous blog post where I use the screen toolkit to fix UI bugs.
How to Create Your Own Toolkit 📝
Step 1: Install Goose
To install Goose, run the following commands in your terminal:
brew install pipx
pipx ensurepath
pipx install goose-ai
Step 2: Fork the Goose Plugin Repository
The Goose plugin repo is where your toolkit will live. Follow the directions outlined in the README to get started.
Step 3: Start a Development Session
Now, you can create a Goose development session. This is different from starting a regular session with Goose - you're starting a session where you want to build with Goose:
uv run goose session start
Note: You may need to install uv first.
Step 4: Create Your Toolkit File
Create a file called mytodo.py
in this directory goose-plugins/src/goose_plugins/toolkits
Step 5: Set Up Boilerplate Code
Add the following lines of code to your mytodo.py
file:
from goose.toolkit.base import tool, Toolkit
class MyTodoToolkit(Toolkit):
"""A simple to-do list toolkit for managing tasks."""
def __init__(self, *args: tuple, **kwargs: dict) -> None:
super().__init__(*args, **kwargs)
# Initialize tasks as a list of dictionaries with 'description' and 'completed' fields
self.tasks = []
So far, in this file, we've:
- Imported the base toolkit library
- Created a class called
MyTodoToolkit
which extends theToolkit
class - Initialized an empty dictionary that will hold our list of tasks
Step 6: Add Your Toolkit's First Action
Let's start with writing a method to add tasks to our list. Add the following code to mytodo.py
:
@tool
def add_task(self, task: str) -> str:
"""Add a new task to the to-do list.
Args:
task (str): The task description to add to the list.
"""
self.tasks.append({"description": task, "completed": False})
self.notifier.log(f"Added task: '{task}'")
return f"Added task: '{task}'"
Let's break down what's happening in this method:
The @tool Decorator: This decorator tags a function as a tool, allowing it to be automatically identified and managed within a toolkit.
Method Definition: We created a method called
add_task
. In toolkits, you create methods to define specific actions that the toolkit can perform. In this case, we want our task management toolkit to be able to add tasks.-
Docstring: The docstring is required by Goose - it's not just for documentation. Goose uses it to:
- Validate the method's functionality
- Check if the parameters match
According to the creator of Goose, Bradley Axen, the docstrings often help Goose produce more refined results when processing commands.
Step 7: Complete Your Toolkit
Now you can add more methods that handle removing tasks, updating tasks, listing all tasks, and listing completed tasks. Here's what your completed mytodo.py
file should look like:
from goose.toolkit.base import tool, Toolkit
class MyTodoToolkit(Toolkit):
"""A simple to-do list toolkit for managing tasks."""
def __init__(self, *args: tuple, **kwargs: dict) -> None:
super().__init__(*args, **kwargs)
# Initialize tasks as a list of dictionaries with 'description' and 'completed' fields
self.tasks = []
@tool
def add_task(self, task: str) -> str:
"""Add a new task to the to-do list.
Args:
task (str): The task description to add to the list.
"""
# Store task as dictionary with description and completion status
self.tasks.append({"description": task, "completed": False})
self.notifier.log(f"Added task: '{task}'")
return f"Added task: '{task}'"
@tool
def list_tasks(self) -> str:
"""List all tasks in the to-do list."""
if not self.tasks:
self.notifier.log("No tasks in the to-do list.")
return "No tasks in the to-do list. Give user instructions on how to add tasks."
task_list = []
for index, task in enumerate(self.tasks, start=1):
status = "✓" if task["completed"] else " "
task_list.append(f"{index}. [{status}] {task['description']}")
self.notifier.log("\n".join(task_list))
return f"Tasks listed successfully: {task_list}"
@tool
def remove_task(self, task_number: int) -> str:
"""Remove a task from the to-do list by its number.
Args:
task_number (int): The index number of the task to remove (starting from 1).
"""
try:
removed_task = self.tasks.pop(task_number - 1)
self.notifier.log(f"Removed task: '{removed_task['description']}'")
return f"Removed task: '{removed_task['description']}'"
except IndexError:
self.notifier.log("Invalid task number. Please try again.")
return "User input invalid task number and needs to try again."
@tool
def mark_as_complete(self, task_number: int) -> str:
"""Mark a task as complete by its number.
Args:
task_number (int): The index number of the task to mark as complete (starting from 1).
Raises:
IndexError: If the task number is invalid.
"""
try:
self.tasks[task_number - 1]["completed"] = True
self.notifier.log(f"Marked task {task_number} as complete: '{self.tasks[task_number - 1]['description']}'")
return f"Marked task {task_number} as complete: '{self.tasks[task_number - 1]['description']}'"
except IndexError:
self.notifier.log("Invalid task number. Please try again.")
return "User input invalid task number and needs to try again."
@tool
def list_completed_tasks(self) -> str:
"""List all completed tasks."""
completed_tasks = [task for task in self.tasks if task["completed"]]
if not completed_tasks:
self.notifier.log("No completed tasks.")
return "No completed tasks. Provide instructions for marking tasks as complete."
task_list = []
for index, task in enumerate(completed_tasks, start=1):
task_list.append(f"{index}. [✓] {task['description']}")
self.notifier.log("\n".join(task_list))
return f"Tasks listed successfully: {task_list}"
@tool
def update_task(self, task_number: int, new_description: str) -> str:
"""Update the description of a task by its number.
Args:
task_number (int): The index number of the task to update (starting from 1).
new_description (str): The new description for the task.
Raises:
IndexError: If the task number is invalid.
"""
try:
old_description = self.tasks[task_number - 1]["description"]
self.tasks[task_number - 1]["description"] = new_description
self.notifier.log(f"Updated task {task_number} from '{old_description}' to '{new_description}'")
return f"Updated task {task_number} successfully."
except IndexError:
self.notifier.log("Invalid task number. Unable to update.")
return "Invalid task number. Unable to update."
Step 8: Make Your Toolkit Available to Others
We now have to make the Toolkit available to other users by adding it to our pyproject.toml
:
[project.entry-points."goose.toolkit"]
developer = "goose.toolkit.developer:Developer"
github = "goose.toolkit.github:Github"
# Add a line like this - the key becomes the name used in profiles
mytodo = "goose_plugins.toolkits.mytodo:MyTodoToolkit"
This follows the format module.submodule:ClassName
:
-
goose_plugins
is the base module -
toolkits
is a submodule within goose_plugins -
mytodo
is a further submodule within toolkits -
MyTodoToolkit
is the class name
Step 9: Enable Your Toolkit
Now, you can elect to use this toolkit by adding it to your ~/.config/goose/profiles.yaml
:
default:
provider: openai
processor: gpt-4o
accelerator: gpt-4o-mini
moderator: truncate
toolkits:
- name: developer
requires: {}
- name: mytodo
requires: {}
Now you can use the toolkit by prompting it in natural language to add tasks to your to-do list, mark tasks as complete, and update tasks. Here's an example of how it works:
Reference my Pull Request 👀
You can take a look at my PR to see the full implementation
The Future of Toolkits 🚀
The way we build toolkits is evolving. Check out the product roadmap plans to see what's coming next.
Join Our Community 🤝
The Goose Open Source Community continues to grow, and I invite you join the fun by:
Can't wait to see what you build!
Top comments (0)