Tool Design for Forge Agents
Status: Active Purpose: Guidelines for creating AI-friendly tools following context engineering principles Source: Agent Skills for Context Engineering
Core Principle
"If a human engineer cannot definitively say which tool should be used in a given situation, an agent cannot be expected to do better."
Tools are the interface between deterministic systems (Go handlers, database queries) and AI agents. Unlike APIs built for developers, agent tools must accommodate models that reason about intent and infer parameters from natural language.
Tool Description Engineering
The Four Questions
Every tool description must answer:
- What — What specifically does this tool accomplish?
- When — In what contexts should it be used?
- Inputs — What parameters does it require?
- Outputs — What format will it return?
Example: Poor vs Good Descriptions
❌ Poor (Vague):
var getTournamentTool = Tool{
Name: "get_tournament",
Description: "Get tournament info",
ArgsSchema: []byte(`{"type":"object","properties":{"id":{"type":"string"}}}`),
Handler: getTournamentHandler,
}
✅ Good (Explicit):
var getTournamentTool = Tool{
Name: "get_tournament",
Description: `Retrieve complete tournament details including name, status, teams, and match bracket.
When to use:
- User asks about a specific tournament
- Need to display tournament information
- Validating tournament exists before operations
Inputs:
- id (string, required): Tournament UUID from database
Returns:
- Tournament object with nested teams[] and matches[]
- Format: JSON with fields: id, name, game, format, status, max_teams, teams[], matches[]
- Errors: 404 if not found, 403 if user lacks access`,
ArgsSchema: []byte(`{
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid",
"description": "Tournament UUID"
}
},
"required": ["id"]
}`),
Handler: getTournamentHandler,
}
Tool Consolidation Principle
Favor Comprehensive Tools Over Many Narrow Ones
❌ Anti-Pattern: Tool Explosion
// Too many overlapping tools confuse AI
var tools = map[string]Tool{
"get_tournament_name": {...},
"get_tournament_status": {...},
"get_tournament_teams": {...},
"get_tournament_matches": {...},
"get_tournament_settings": {...},
}
✅ Pattern: Consolidated Tool
var tools = map[string]Tool{
"get_tournament": {
Name: "get_tournament",
Description: `Retrieve tournament with optional field filtering.
When to use:
- ANY query about tournament information
- Use 'fields' parameter to request only needed data
Inputs:
- id (string, required): Tournament UUID
- fields (string[], optional): Specific fields to return. Defaults to all.
Options: ["name", "status", "teams", "matches", "settings"]
Returns:
- Tournament object with requested fields
- Format: JSON, only includes requested fields to minimize response size`,
ArgsSchema: []byte(`{
"type": "object",
"properties": {
"id": {"type": "string", "format": "uuid"},
"fields": {
"type": "array",
"items": {"type": "string"},
"description": "Fields to include in response"
}
},
"required": ["id"]
}`),
Handler: getTournamentHandler,
},
}
Guideline: 10-20 tools per collection. Beyond that, use namespacing.
Primitive Exposure Pattern
Expose Proven Abstractions, Not Custom Tools
Context Engineering Insight:
"Models understand proven abstractions deeply and can chain primitives flexibly."
❌ Anti-Pattern: Custom Abstractions
// Creating custom tools for everything
var tools = map[string]Tool{
"search_tournaments_by_game": {...},
"search_tournaments_by_date": {...},
"search_tournaments_by_status": {...},
"search_active_tournaments": {...},
}
✅ Pattern: SQL Primitive
// Expose SQL primitive (proven, universal abstraction)
var tools = map[string]Tool{
"query_database": {
Name: "query_database",
Description: `Execute a SQL query against the database.
When to use:
- ANY read operation on tournament, team, match, or user data
- Complex queries requiring joins or filtering
- Aggregations and reports
Inputs:
- query (string, required): SQL SELECT statement
- params (array, optional): Parameterized query values ($1, $2, etc.)
Returns:
- Array of rows matching query
- Format: JSON array of objects
- Errors: SQL syntax errors with helpful hints
Safety:
- Only SELECT queries allowed (no INSERT/UPDATE/DELETE)
- Automatic row-level security applied based on user permissions
- Query timeout: 10 seconds
Examples:
- Get active tournaments: "SELECT * FROM tournaments WHERE status = 'in_progress'"
- Get team roster: "SELECT p.* FROM players p JOIN teams t ON p.team_id = t.id WHERE t.id = $1" with params ["team-uuid"]`,
ArgsSchema: []byte(`{
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "SQL SELECT statement"
},
"params": {
"type": "array",
"items": {"type": "string"},
"description": "Parameterized values"
}
},
"required": ["query"]
}`),
Handler: queryDatabaseHandler,
},
}
Why This Works:
- SQL is a 50-year-old standard — models trained on billions of SQL examples
- Flexible — handles infinite query variations without tool explosion
- Self-documenting — SQL syntax is the documentation
When NOT to Use:
- Write operations (use specific tools for INSERT/UPDATE/DELETE to enforce validation)
- Operations requiring complex business logic
Error Message Design
Enable Agent Recovery
❌ Poor Error Messages:
func getTournamentHandler(ctx context.Context, args json.RawMessage) (any, error) {
var req struct{ ID string }
json.Unmarshal(args, &req)
t, err := db.GetTournament(ctx, req.ID)
if err != nil {
return nil, err // "sql: no rows in result set" — unhelpful
}
return t, nil
}
✅ Good Error Messages:
func getTournamentHandler(ctx context.Context, args json.RawMessage) (any, error) {
var req struct{ ID string }
if err := json.Unmarshal(args, &req); err != nil {
return nil, &ToolError{
Code: "invalid_arguments",
Message: "ID must be a valid UUID",
Hint: "Check that the ID is formatted as a UUID (e.g., 550e8400-e29b-41d4-a716-446655440000)",
Retryable: false,
}
}
t, err := db.GetTournament(ctx, req.ID)
if err == sql.ErrNoRows {
return nil, &ToolError{
Code: "not_found",
Message: fmt.Sprintf("Tournament %s does not exist", req.ID),
Hint: "Use list_tournaments to see available tournaments, or verify the tournament ID is correct",
Retryable: false,
}
}
if err != nil {
return nil, &ToolError{
Code: "database_error",
Message: "Failed to retrieve tournament",
Hint: "This is a temporary issue. Try again in a moment.",
Retryable: true,
}
}
return t, nil
}
Error Structure:
type ToolError struct {
Code string `json:"code"` // Machine-readable category
Message string `json:"message"` // Human-readable summary
Hint string `json:"hint"` // Actionable guidance
Retryable bool `json:"retryable"` // Can the agent retry?
}
Error Categories:
invalid_arguments— Bad input format (not retryable)not_found— Resource doesn't exist (not retryable)forbidden— Insufficient permissions (not retryable)conflict— Business rule violation (not retryable, needs different approach)timeout— Operation took too long (retryable)database_error— Temporary infrastructure issue (retryable)unknown— Unexpected error (retryable, includes truncated error message)
Response Format Optimization
Give Agents Control Over Detail Level
Pattern: Format Options
var tools = map[string]Tool{
"list_tournaments": {
Name: "list_tournaments",
Description: `List tournaments with optional filtering and format control.
When to use:
- Browse available tournaments
- Find tournaments by criteria
Inputs:
- status (string, optional): Filter by status ("draft", "registration", "in_progress", "completed")
- game (string, optional): Filter by game name
- format (string, optional): Response detail level
- "compact" (default): id, name, status only (minimal tokens)
- "detailed": All fields including teams and matches (verbose)
Returns:
- Array of tournaments
- Format depends on 'format' parameter
- Compact format: ~50 tokens per tournament
- Detailed format: ~200 tokens per tournament
Guidance:
- Use "compact" for listing/browsing (saves context)
- Use "detailed" only when full information needed`,
ArgsSchema: []byte(`{
"type": "object",
"properties": {
"status": {"type": "string", "enum": ["draft", "registration", "in_progress", "completed"]},
"game": {"type": "string"},
"format": {"type": "string", "enum": ["compact", "detailed"], "default": "compact"}
}
}`),
Handler: listTournamentsHandler,
},
}
Implementation:
func listTournamentsHandler(ctx context.Context, args json.RawMessage) (any, error) {
var req struct {
Status string
Game string
Format string
}
json.Unmarshal(args, &req)
if req.Format == "" {
req.Format = "compact"
}
tournaments, _ := db.ListTournaments(ctx, req.Status, req.Game)
if req.Format == "compact" {
// Return minimal representation
type Compact struct {
ID string `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
}
result := make([]Compact, len(tournaments))
for i, t := range tournaments {
result[i] = Compact{ID: t.ID, Name: t.Name, Status: t.Status}
}
return result, nil
}
// Return full tournaments
return tournaments, nil
}
Naming Conventions
Consistent Patterns Reduce Cognitive Load
Verb-Noun Pattern:
- ✅
get_tournament(nottournament_get,fetchTournament) - ✅
list_matches(notmatches_list,getMatches) - ✅
create_team(notteam_create,addTeam) - ✅
update_score(notscore_update,recordScore) - ✅
delete_player(notplayer_delete,removePlayer)
Standard Verbs:
get_— Retrieve single resource by IDlist_— Retrieve multiple resources (with optional filtering)create_— Create new resourceupdate_— Modify existing resourcedelete_— Remove resourcesearch_— Complex queries across resourcesvalidate_— Check conditions without side effects
Parameter Naming:
- ✅
id(nottournament_id,tournamentId,tid) - ✅
status(notcurrent_status,state) - ✅
created_at(notcreatedAt,creation_date,timestamp)
Consistency Example:
var tools = map[string]Tool{
"get_tournament": {/* id → Tournament */},
"get_team": {/* id → Team */},
"get_match": {/* id → Match */},
"list_tournaments": {/* filters → Tournament[] */},
"list_teams": {/* filters → Team[] */},
"list_matches": {/* filters → Match[] */},
"create_tournament": {/* data → Tournament */},
"create_team": {/* data → Team */},
"create_match": {/* data → Match */},
}
Namespacing for Large Tool Sets
Beyond 20 Tools, Use Hierarchical Organization
Pattern: Dot-Notation Namespacing
var tools = map[string]Tool{
// Tournament namespace
"tournament.get": {...},
"tournament.list": {...},
"tournament.create": {...},
"tournament.start": {...},
"tournament.complete": {...},
// Team namespace
"team.get": {...},
"team.register": {...},
"team.add_player": {...},
"team.check_in": {...},
// Match namespace
"match.get": {...},
"match.record_result": {...},
"match.reschedule": {...},
// Bracket namespace (complex operations)
"bracket.generate": {...},
"bracket.seed_teams": {...},
"bracket.advance_winner": {...},
}
Why This Works:
- AI can pattern-match on prefixes (
tournament.*for tournament operations) - Clear domain boundaries
- Scales beyond 100 tools
Tool Testing Through Agents
Use AI to Find Tool Failures
Pattern: Agent-Driven Tool Validation
// internal/ai/tool_validator.go
type ToolValidationResult struct {
ToolName string
TestCases []TestCase
FailureRate float64
Improvements []string
}
type TestCase struct {
UserInput string
ExpectedTool string
ActualTool string
Success bool
Error string
}
func ValidateToolRegistry(ctx context.Context, ai *ai.Client, tools []Tool) (*ToolValidationResult, error) {
// Generate test cases
testCases := []string{
"Show me tournament details for ID abc123",
"List all active tournaments",
"Create a new tournament called Spring Championship",
"Record the score for match xyz789: Team A won 2-1",
}
results := &ToolValidationResult{TestCases: []TestCase{}}
for _, input := range testCases {
// Ask AI which tool it would use
toolCall, err := ai.ExtractToolCall(ctx, input, tools)
// Validate against expected tool
expected := determineExpectedTool(input)
success := toolCall.Name == expected
results.TestCases = append(results.TestCases, TestCase{
UserInput: input,
ExpectedTool: expected,
ActualTool: toolCall.Name,
Success: success,
})
}
// Calculate failure rate
failures := 0
for _, tc := range results.TestCases {
if !tc.Success {
failures++
}
}
results.FailureRate = float64(failures) / float64(len(results.TestCases))
// Ask AI for improvements if failure rate > 10%
if results.FailureRate > 0.1 {
improvements, _ := ai.SuggestToolImprovements(ctx, results)
results.Improvements = improvements
}
return results, nil
}
Context Engineering Insight:
"Claude can analyze tool failure patterns and suggest improvements, creating a feedback loop that reduces future failures by approximately 40%."
Forge-Specific Tool Registry Pattern
File-Based Tool Discovery
Pattern: Progressive Disclosure via File System
internal/
└── tools/
├── registry.go # Main registry
├── tournament/
│ ├── get.go # get_tournament tool
│ ├── list.go # list_tournaments tool
│ └── create.go # create_tournament tool
├── team/
│ ├── get.go
│ ├── register.go
│ └── add_player.go
└── match/
├── get.go
├── list.go
└── record_result.go
Implementation:
// internal/tools/registry.go
package tools
import (
"context"
"encoding/json"
"fmt"
"rally-hq/internal/tools/tournament"
"rally-hq/internal/tools/team"
"rally-hq/internal/tools/match"
)
type Tool struct {
Name string
Description string
ArgsSchema json.RawMessage
Handler func(context.Context, json.RawMessage) (any, error)
}
var Registry = map[string]Tool{
// Tournament tools
"get_tournament": tournament.GetTool,
"list_tournaments": tournament.ListTool,
"create_tournament": tournament.CreateTool,
// Team tools
"get_team": team.GetTool,
"register_team": team.RegisterTool,
// Match tools
"get_match": match.GetTool,
"record_result": match.RecordResultTool,
}
// ExecuteTool routes to handler with validation
func ExecuteTool(ctx context.Context, name string, args json.RawMessage) (any, error) {
tool, ok := Registry[name]
if !ok {
return nil, fmt.Errorf("unknown tool: %s", name)
}
// Validate args against schema
if err := validateJSON(args, tool.ArgsSchema); err != nil {
return nil, &ToolError{
Code: "invalid_arguments",
Message: fmt.Sprintf("Invalid arguments for %s", name),
Hint: err.Error(),
}
}
return tool.Handler(ctx, args)
}
Tool File Example:
// internal/tools/tournament/get.go
package tournament
import (
"context"
"encoding/json"
"rally-hq/internal/db"
)
var GetTool = Tool{
Name: "get_tournament",
Description: `Retrieve complete tournament details including name, status, teams, and match bracket.
When to use:
- User asks about a specific tournament
- Need to display tournament information
- Validating tournament exists before operations
Inputs:
- id (string, required): Tournament UUID from database
Returns:
- Tournament object with nested teams[] and matches[]
- Format: JSON with fields: id, name, game, format, status, max_teams, created_at, teams[], matches[]
- Errors: 404 if not found, 403 if user lacks access`,
ArgsSchema: []byte(`{
"type": "object",
"properties": {
"id": {"type": "string", "format": "uuid"}
},
"required": ["id"]
}`),
Handler: getTournamentHandler,
}
func getTournamentHandler(ctx context.Context, args json.RawMessage) (any, error) {
var req struct {
ID string `json:"id"`
}
if err := json.Unmarshal(args, &req); err != nil {
return nil, &ToolError{
Code: "invalid_arguments",
Message: "Failed to parse arguments",
Hint: "Ensure 'id' is a valid UUID string",
}
}
tournament, err := db.GetTournamentWithTeamsAndMatches(ctx, req.ID)
if err == sql.ErrNoRows {
return nil, &ToolError{
Code: "not_found",
Message: fmt.Sprintf("Tournament %s does not exist", req.ID),
Hint: "Use list_tournaments to see available tournaments",
}
}
if err != nil {
return nil, &ToolError{
Code: "database_error",
Message: "Failed to retrieve tournament",
Hint: "This is a temporary issue. Try again.",
Retryable: true,
}
}
return tournament, nil
}
Summary: Forge Tool Design Checklist
When creating a new tool:
- Description answers 4 questions: What, When, Inputs, Outputs
- Naming follows verb-noun convention:
get_,list_,create_, etc. - Error messages include actionable hints
- Schema uses standard parameter names:
id,status,created_at - Response format options if output can be large (compact vs detailed)
- Consolidate rather than create overlapping tools
- Prefer primitives (SQL, file system) over custom abstractions when possible
- File-based organization for progressive disclosure
- Test with AI to validate description clarity
References
- Agent Skills for Context Engineering
- Forge 12-Factor Agents — Factor 4: Tools Are Just Structured Output
- Forge Architecture — Tool patterns in context
- Context Engineering Analysis