Skip to content

Add unified ToolsConfig struct to replace map[string]any pattern#4158

Merged
pelikhan merged 2 commits into
mainfrom
copilot/create-unified-toolsconfig-struct
Nov 17, 2025
Merged

Add unified ToolsConfig struct to replace map[string]any pattern#4158
pelikhan merged 2 commits into
mainfrom
copilot/create-unified-toolsconfig-struct

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Nov 16, 2025

Replaces pervasive map[string]any pattern in tool configuration with strongly-typed ToolsConfig struct. Eliminates runtime type assertions while maintaining full backward compatibility.

Changes

  • Enhanced existing Tools struct with comprehensive documentation and ToolsConfig type alias
  • Added conversion helpers: ParseToolsConfig() for map→struct, ToMap() for struct→map
  • Updated 2 functions to use typed config with backward compatibility wrappers:
    • extractCacheMemoryConfig(*ToolsConfig) + extractCacheMemoryConfigFromMap(map[string]any)
    • expandNeutralToolsToCodexTools(*ToolsConfig) + expandNeutralToolsToCodexToolsFromMap(map[string]any)
  • Added migration pattern documentation with usage examples for future conversions

Usage

// Before: runtime type assertions required
func processTools(tools map[string]any) {
    if github, ok := tools["github"].(map[string]any); ok {
        if allowed, ok := github["allowed"].([]any); ok {
            // more type assertions...
        }
    }
}

// After: strongly-typed access
func processTools(config *ToolsConfig) {
    if config.GitHub != nil {
        for _, tool := range config.GitHub.Allowed {
            // direct string access
        }
    }
}

Migration Path

Establishes pattern for converting remaining ~30 functions that accept map[string]any. Wrapper functions enable gradual migration without breaking existing code.

Original prompt

This section details on the original issue you should resolve

<issue_title>[task] Create unified ToolsConfig struct in tools_types.go</issue_title>
<issue_description>## Objective
Create a parent ToolsConfig struct that unifies existing individual tool configuration types, replacing the pervasive map[string]any pattern used in tool configuration parsing.

Context

Related to discussion #4082. The Typist analysis found that individual tool config structs already exist in pkg/workflow/tools_types.go (GitHubToolConfig, PlaywrightToolConfig, BashToolConfig, etc.), but functions still accept map[string]any for tools configuration. This creates unnecessary type assertions and runtime validation.

Approach

  1. Define ToolsConfig struct in pkg/workflow/tools_types.go that contains all existing tool config types
  2. Include a CustomMCP map for custom MCP server configurations
  3. Add helper functions to convert map[string]any to ToolsConfig for backward compatibility during migration
  4. Update 2-3 functions initially to use the new type (e.g., extractCacheMemoryConfig, expandNeutralToolsToCodexTools)
  5. Document the new struct and its usage patterns

Files to Modify

  • pkg/workflow/tools_types.go (add the new ToolsConfig struct)
  • pkg/workflow/cache.go (update extractCacheMemoryConfig signature)
  • pkg/workflow/codex_engine.go (update expandNeutralToolsToCodexTools signature)

Implementation Example

// In pkg/workflow/tools_types.go
type ToolsConfig struct {
    GitHub      *GitHubToolConfig      // Already exists
    Playwright  *PlaywrightToolConfig   // Already exists
    Bash        *BashToolConfig         // Already exists
    WebFetch    *WebFetchToolConfig     // Already exists
    WebSearch   *WebSearchToolConfig    // Already exists
    Edit        *EditToolConfig         // Already exists
    CacheMemory *CacheMemoryToolConfig  // Already exists
    
    // Custom MCP tools
    CustomMCP   map[string]MCPServerConfig
}

// Helper function for backward compatibility
func ParseToolsConfig(toolsMap map[string]any) (*ToolsConfig, error) {
    // Implementation that extracts each tool config from the map
}

Acceptance Criteria

  • ToolsConfig struct defined in pkg/workflow/tools_types.go
  • All existing tool config types included as fields
  • CustomMCP map added for flexibility
  • Helper function ParseToolsConfig implemented for conversion
  • At least 2-3 functions updated to use the new type
  • Documentation added explaining the new pattern
  • All tests pass
  • No behavioral changes

Estimated Effort

4-5 hours

Notes

This creates the foundation for incrementally migrating the remaining ~30 functions that currently accept map[string]any for tools configuration. Follow-up issues can tackle the broader migration.

AI generated by Plan Command for discussion #4082</issue_description>

Comments on the Issue (you are @copilot in this section)


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

- Created ToolsConfig type alias for existing Tools struct
- Added ParseToolsConfig() helper function to convert map[string]any to *ToolsConfig
- Added ToMap() method to convert back to map[string]any for backward compatibility
- Updated extractCacheMemoryConfig to use *ToolsConfig with backward compatibility wrapper
- Updated expandNeutralToolsToCodexTools to use *ToolsConfig with backward compatibility wrapper
- Added comprehensive documentation explaining the migration pattern
- Added tests for ParseToolsConfig and ToMap functions
- All existing tests pass without modifications

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Create unified ToolsConfig struct in tools_types.go Add unified ToolsConfig struct to replace map[string]any pattern Nov 16, 2025
Copilot AI requested a review from pelikhan November 16, 2025 23:39
@pelikhan pelikhan marked this pull request as ready for review November 16, 2025 23:50
Copilot AI review requested due to automatic review settings November 16, 2025 23:50
@pelikhan
Copy link
Copy Markdown
Collaborator

@copilot recompile

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR introduces a unified ToolsConfig struct to replace the pervasive map[string]any pattern used throughout the codebase for tool configuration. The change enhances type safety by providing strongly-typed access to tool configurations while maintaining full backward compatibility through wrapper functions.

Key Changes

  • Enhanced the existing Tools struct with comprehensive documentation and created ToolsConfig as a type alias
  • Added ParseToolsConfig() to convert from map[string]any to strongly-typed ToolsConfig, and ToMap() for the reverse conversion
  • Updated two functions (extractCacheMemoryConfig and expandNeutralToolsToCodexTools) to use ToolsConfig internally with backward-compatible wrappers that accept map[string]any

Reviewed Changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
pkg/workflow/tools_types.go Added ToolsConfig type with documentation, ParseToolsConfig() and ToMap() conversion helpers, and helper methods HasTool()/GetToolNames()
pkg/workflow/tools_types_test.go Added comprehensive tests for ParseToolsConfig() and ToMap() round-trip conversion
pkg/workflow/cache.go Updated extractCacheMemoryConfig() to use ToolsConfig, added extractCacheMemoryConfigFromMap() wrapper for backward compatibility
pkg/workflow/codex_engine.go Updated expandNeutralToolsToCodexTools() to use ToolsConfig, added expandNeutralToolsToCodexToolsFromMap() wrapper
pkg/workflow/compiler.go Updated to use backward-compatible wrapper function extractCacheMemoryConfigFromMap()
pkg/workflow/*_test.go Updated test files to call the backward-compatible wrapper functions instead of the original functions

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +256 to +259
func (e *CodexEngine) expandNeutralToolsToCodexToolsFromMap(tools map[string]any) map[string]any {
toolsConfig, _ := ParseToolsConfig(tools)
result := e.expandNeutralToolsToCodexTools(toolsConfig)
return result.ToMap()
Copy link

Copilot AI Nov 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error is discarded when calling ParseToolsConfig. If the function signature includes an error return (as it currently does), the error should be checked or explicitly documented why it can be ignored.

Since ParseToolsConfig currently never returns an error (just returns config, nil), either:

  1. Check the error: if err != nil { return nil, err }
  2. Use blank identifier if intentional: toolsConfig, _ := ParseToolsConfig(tools)
  3. Remove error from ParseToolsConfig signature

The current code silently ignores potential future errors.

Suggested change
func (e *CodexEngine) expandNeutralToolsToCodexToolsFromMap(tools map[string]any) map[string]any {
toolsConfig, _ := ParseToolsConfig(tools)
result := e.expandNeutralToolsToCodexTools(toolsConfig)
return result.ToMap()
func (e *CodexEngine) expandNeutralToolsToCodexToolsFromMap(tools map[string]any) (map[string]any, error) {
toolsConfig, err := ParseToolsConfig(tools)
if err != nil {
return nil, err
}
result := e.expandNeutralToolsToCodexTools(toolsConfig)
return result.ToMap(), nil

Copilot uses AI. Check for mistakes.
// - Built-in tool fields use pointers to distinguish between "not configured" (nil)
// and "configured with defaults" (non-nil but empty struct)
// - The Custom map stores MCP server configurations that aren't built-in tools
// - The raw map is preserved for perfect round-trip conversion when needed
Copy link

Copilot AI Nov 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation at line 63 claims "The raw map is preserved for perfect round-trip conversion when needed", but the ToMap() implementation doesn't guarantee perfect round-trip conversion for all cases.

When the raw map is not available (nil), ToMap() reconstructs the map from struct fields (lines 112-153), but this reconstruction is inconsistent:

  • Some tools return their struct representation (e.g., result["github"] = t.GitHub)
  • Others extract inner values (e.g., result["bash"] = t.Bash.AllowedCommands)

This means: NewTools(map) -> ToMap() might not produce an identical map structure, breaking the "perfect round-trip" guarantee claimed in the documentation.

Either fix the ToMap() implementation to ensure true round-trip conversion, or update the documentation to accurately reflect the current behavior and limitations.

Suggested change
// - The raw map is preserved for perfect round-trip conversion when needed
// - The raw map is preserved for round-trip conversion when available; if the raw map is not present, ToMap() reconstructs the map from struct fields, which may not be a perfect round-trip in all cases.

Copilot uses AI. Check for mistakes.
Comment on lines +196 to +210
result := &ToolsConfig{
GitHub: toolsConfig.GitHub,
Bash: toolsConfig.Bash,
WebFetch: toolsConfig.WebFetch,
WebSearch: toolsConfig.WebSearch,
Edit: toolsConfig.Edit,
Playwright: toolsConfig.Playwright,
AgenticWorkflows: toolsConfig.AgenticWorkflows,
CacheMemory: toolsConfig.CacheMemory,
SafetyPrompt: toolsConfig.SafetyPrompt,
Timeout: toolsConfig.Timeout,
StartupTimeout: toolsConfig.StartupTimeout,
Custom: make(map[string]any),
raw: make(map[string]any),
}
Copy link

Copilot AI Nov 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The expandNeutralToolsToCodexTools function performs a shallow copy of pointer fields (lines 197-207), which means modifications to the copied config can affect the original. This is problematic when modifying Playwright configuration (lines 225-231).

For example, if toolsConfig.Playwright.AllowedDomains is modified after copying, it would affect both the original and result configs since they share the same slice.

Consider deep copying slice fields when creating defensive copies:

// Deep copy Playwright config if present
if toolsConfig.Playwright != nil {
    result.Playwright = &PlaywrightToolConfig{
        Version:        toolsConfig.Playwright.Version,
        AllowedDomains: append([]string{}, toolsConfig.Playwright.AllowedDomains...),
        Args:           append([]string{}, toolsConfig.Playwright.Args...),
    }
}

Apply similar deep copying for other fields with slice/map types (GitHub, Bash, etc.).

Copilot uses AI. Check for mistakes.
Comment on lines +114 to +154
if t.GitHub != nil {
result["github"] = t.GitHub
}
if t.Bash != nil {
result["bash"] = t.Bash.AllowedCommands
}
if t.WebFetch != nil {
result["web-fetch"] = t.WebFetch
}
if t.WebSearch != nil {
result["web-search"] = t.WebSearch
}
if t.Edit != nil {
result["edit"] = t.Edit
}
if t.Playwright != nil {
result["playwright"] = t.Playwright
}
if t.AgenticWorkflows != nil {
result["agentic-workflows"] = t.AgenticWorkflows.Enabled
}
if t.CacheMemory != nil {
result["cache-memory"] = t.CacheMemory.Raw
}
if t.SafetyPrompt != nil {
result["safety-prompt"] = *t.SafetyPrompt
}
if t.Timeout != nil {
result["timeout"] = *t.Timeout
}
if t.StartupTimeout != nil {
result["startup-timeout"] = *t.StartupTimeout
}

// Add custom tools
for name, config := range t.Custom {
result[name] = config
}

return result
}
Copy link

Copilot AI Nov 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ToMap() method has inconsistent behavior for different tool types. It returns the raw underlying value for some tools (Bash.AllowedCommands, AgenticWorkflows.Enabled, CacheMemory.Raw) but returns the struct itself for others (GitHub, WebFetch, Playwright, etc.).

This inconsistency can break round-trip conversion. For example:

toolsMap := map[string]any{
    "github": map[string]any{"allowed": []any{"get_issue"}},
}
config := NewTools(toolsMap)
result := config.ToMap()
// result["github"] is now *GitHubToolConfig, not map[string]any

Consider standardizing the behavior. If the intention is to preserve the original map format, all tools should return their original representation, not the struct. If using the raw map when available (line 107-109) is the intended pattern, ensure it's properly populated for all cases.

Copilot uses AI. Check for mistakes.
Comment on lines +94 to +97
func ParseToolsConfig(toolsMap map[string]any) (*ToolsConfig, error) {
config := NewTools(toolsMap)
return config, nil
}
Copy link

Copilot AI Nov 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ParseToolsConfig never returns an error but still has error in its return signature. This makes the API misleading since callers might expect error handling to be necessary.

Either remove the error return type:

func ParseToolsConfig(toolsMap map[string]any) *ToolsConfig {
    return NewTools(toolsMap)
}

Or add validation that could return an error (e.g., validating tool configurations). If validation is intended for the future, add a comment explaining this design choice.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Nov 16, 2025

@copilot recompile

Recompile completed successfully. All 80 workflow files compiled with 0 errors and 15 warnings (warnings are pre-existing). No lock file changes were needed.

@pelikhan pelikhan merged commit 2f82668 into main Nov 17, 2025
97 of 103 checks passed
@pelikhan pelikhan deleted the copilot/create-unified-toolsconfig-struct branch November 17, 2025 00:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[task] Create unified ToolsConfig struct in tools_types.go

3 participants