Project Doc

Tool Design for Forge Agents

Context-aware tool creation patterns

Updated: December 2025 Source: TOOL-DESIGN.md

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:

  1. What — What specifically does this tool accomplish?
  2. When — In what contexts should it be used?
  3. Inputs — What parameters does it require?
  4. 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 (not tournament_get, fetchTournament)
  • list_matches (not matches_list, getMatches)
  • create_team (not team_create, addTeam)
  • update_score (not score_update, recordScore)
  • delete_player (not player_delete, removePlayer)

Standard Verbs:

  • get_ — Retrieve single resource by ID
  • list_ — Retrieve multiple resources (with optional filtering)
  • create_ — Create new resource
  • update_ — Modify existing resource
  • delete_ — Remove resource
  • search_ — Complex queries across resources
  • validate_ — Check conditions without side effects

Parameter Naming:

  • id (not tournament_id, tournamentId, tid)
  • status (not current_status, state)
  • created_at (not createdAt, 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