Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/http_client/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ func main() {
}

// Discover tools
tools, err := client.SearchTools("", 10)
tools, err := client.SearchTools("http", 10)
if err != nil {
log.Fatalf("search: %v", err)
}
Expand Down
61 changes: 39 additions & 22 deletions src/tag/tag_search.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func NewTagSearchStrategy(repo ToolRepository, descriptionWeight float64) *TagSe
// SearchTools returns tools ordered by relevance to the query, using explicit tags and description keywords.
func (s *TagSearchStrategy) SearchTools(ctx context.Context, query string, limit int) ([]Tool, error) {
// Normalize query
queryLower := strings.ToLower(query)
queryLower := strings.ToLower(strings.TrimSpace(query))
words := s.wordRegex.FindAllString(queryLower, -1)
queryWordSet := make(map[string]struct{}, len(words))
for _, w := range words {
Expand All @@ -43,57 +43,74 @@ func (s *TagSearchStrategy) SearchTools(ctx context.Context, query string, limit
return nil, err
}

// SUTCP each tool
type sUTCPdTool struct {
t Tool
sUTCP float64
// Compute SUTCP score for each tool
type scoredTool struct {
tool Tool
score float64
}
var sUTCPd []sUTCPdTool
var scored []scoredTool

for _, t := range tools {
var sUTCP float64
var score float64

// SUTCP from tags
// Match against tags
for _, tag := range t.Tags {
tagLower := strings.ToLower(tag)

// Direct substring match
if strings.Contains(queryLower, tagLower) {
sUTCP += 1.0
score += 1.0
}
// Partial matches on tag words

// Word-level overlap
tagWords := s.wordRegex.FindAllString(tagLower, -1)
for _, w := range tagWords {
if _, ok := queryWordSet[w]; ok {
sUTCP += s.descriptionWeight
score += s.descriptionWeight
}
}
}

// SUTCP from description
// Match against description
if t.Description != "" {
descWords := s.wordRegex.FindAllString(strings.ToLower(t.Description), -1)
for _, w := range descWords {
if len(w) > 2 {
if _, ok := queryWordSet[w]; ok {
sUTCP += s.descriptionWeight
score += s.descriptionWeight
}
}
}
}

sUTCPd = append(sUTCPd, sUTCPdTool{t: t, sUTCP: sUTCP})
scored = append(scored, scoredTool{tool: t, score: score})
}

// Sort by descending sUTCP
sort.Slice(sUTCPd, func(i, j int) bool {
return sUTCPd[i].sUTCP > sUTCPd[j].sUTCP
// Sort descending by score
sort.Slice(scored, func(i, j int) bool {
return scored[i].score > scored[j].score
})

// Return up to limit
// Collect only positive matches
var result []Tool
for i, st := range sUTCPd {
if i >= limit {
break
for _, st := range scored {
if st.score > 0 {
result = append(result, st.tool)
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Nov 19, 2025

Choose a reason for hiding this comment

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

Appending the tool before testing len(result) >= limit lets SearchTools return a result even when limit <= 0, breaking the limit contract for zero/negative requests.

Prompt for AI agents
Address the following comment on src/tag/tag_search.go at line 98:

<comment>Appending the tool before testing `len(result) &gt;= limit` lets SearchTools return a result even when `limit &lt;= 0`, breaking the limit contract for zero/negative requests.</comment>

<file context>
@@ -43,57 +43,74 @@ func (s *TagSearchStrategy) SearchTools(ctx context.Context, query string, limit
-			break
+	for _, st := range scored {
+		if st.score &gt; 0 {
+			result = append(result, st.tool)
+			if len(result) &gt;= limit {
+				break
</file context>
Fix with Cubic

if len(result) >= limit {
break
}
}
result = append(result, st.t)
}

// If no matches, fallback to top N (for discoverability)
if len(result) == 0 && len(scored) > 0 {
for i, st := range scored {
if i >= limit {
break
}
result = append(result, st.tool)
}
}

return result, nil
}
25 changes: 10 additions & 15 deletions utcp_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -517,26 +517,21 @@ func (c *UtcpClient) CallTool(
return fn(ctx, args)
}

func (c *UtcpClient) SearchTools(query string, limit int) ([]Tool, error) {
tools, err := c.searchStrategy.SearchTools(context.Background(), query, limit)
func (c *UtcpClient) SearchTools(providerPrefix string, limit int) ([]Tool, error) {
all, err := c.toolRepository.GetTools(context.Background())
if err != nil {
return nil, err
}

// Convert []*Tool to []Tool if needed
result := make([]Tool, len(tools))
for i, tool := range tools {
switch t := any(tool).(type) {
case Tool:
result[i] = t
case *Tool:
result[i] = *t
default:
// fallback (shouldn't happen)
result[i] = Tool{}
var filtered []Tool
for _, t := range all {
if strings.HasPrefix(t.Name, providerPrefix+".") {
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Nov 19, 2025

Choose a reason for hiding this comment

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

SearchTools now requires a provider prefix and breaks all existing query-based discovery (e.g., passing empty string) by always returning an error.

Prompt for AI agents
Address the following comment on utcp_client.go at line 527:

<comment>SearchTools now requires a provider prefix and breaks all existing query-based discovery (e.g., passing empty string) by always returning an error.</comment>

<file context>
@@ -517,26 +517,21 @@ func (c *UtcpClient) CallTool(
-			result[i] = Tool{}
+	var filtered []Tool
+	for _, t := range all {
+		if strings.HasPrefix(t.Name, providerPrefix+&quot;.&quot;) {
+			filtered = append(filtered, t)
 		}
</file context>
Fix with Cubic

filtered = append(filtered, t)
}
}
return result, nil
if len(filtered) == 0 {
return nil, fmt.Errorf("no tools found for provider %q", providerPrefix)
}
return filtered, nil
Comment on lines +520 to +534

Choose a reason for hiding this comment

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

P0 Badge Remove query-based search functionality

The new SearchTools implementation no longer delegates to the configured searchStrategy; it simply filters tools whose names begin with providerPrefix and returns an error when none match. Existing callers still pass natural‑language queries or an empty string to discover tools (for example src/plugins/codemode/orchestrator.go invokes client.SearchTools("", limit) and the codemode plugin passes user queries). Those calls now either fail with "no tools found" or only match provider names, so tag/description search is effectively disabled. This regressions breaks tool discovery across the SDK. Consider keeping the strategy-based search and providing provider filtering as a separate helper.

Useful? React with 👍 / 👎.

}

// ----- variable substitution src -----
Expand Down
Loading