2026-04-24 11:36:46 +00:00
|
|
|
package agent
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"fmt"
|
|
|
|
|
"strings"
|
2026-05-22 02:06:40 +00:00
|
|
|
|
|
|
|
|
"github.com/sipeed/picoclaw/pkg/tools"
|
2026-04-24 11:36:46 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type toolDiscoveryPromptContributor struct {
|
|
|
|
|
useBM25 bool
|
|
|
|
|
useRegex bool
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (c toolDiscoveryPromptContributor) PromptSource() PromptSourceDescriptor {
|
|
|
|
|
return PromptSourceDescriptor{
|
|
|
|
|
ID: PromptSourceToolDiscovery,
|
|
|
|
|
Owner: "tools",
|
|
|
|
|
Description: "Tool discovery instructions",
|
|
|
|
|
Allowed: []PromptPlacement{{Layer: PromptLayerCapability, Slot: PromptSlotTooling}},
|
|
|
|
|
StableByDefault: true,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (c toolDiscoveryPromptContributor) ContributePrompt(
|
|
|
|
|
_ context.Context,
|
2026-05-22 02:06:40 +00:00
|
|
|
req PromptBuildRequest,
|
2026-04-24 11:36:46 +00:00
|
|
|
) ([]PromptPart, error) {
|
2026-05-22 02:06:40 +00:00
|
|
|
if req.SuppressToolUseRule {
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
useBM25 := c.useBM25 && promptAllowsTool(req, tools.BM25SearchToolName)
|
|
|
|
|
useRegex := c.useRegex && promptAllowsTool(req, tools.RegexSearchToolName)
|
|
|
|
|
if !useBM25 && !useRegex {
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
content := formatToolDiscoveryRule(useBM25, useRegex)
|
2026-04-24 11:36:46 +00:00
|
|
|
if strings.TrimSpace(content) == "" {
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return []PromptPart{
|
|
|
|
|
{
|
|
|
|
|
ID: "capability.tool_discovery",
|
|
|
|
|
Layer: PromptLayerCapability,
|
|
|
|
|
Slot: PromptSlotTooling,
|
|
|
|
|
Source: PromptSource{ID: PromptSourceToolDiscovery, Name: "tool_registry:discovery"},
|
|
|
|
|
Title: "tool discovery",
|
|
|
|
|
Content: content,
|
|
|
|
|
Stable: true,
|
|
|
|
|
Cache: PromptCacheEphemeral,
|
|
|
|
|
},
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type mcpServerPromptContributor struct {
|
|
|
|
|
serverName string
|
|
|
|
|
toolCount int
|
|
|
|
|
deferred bool
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (c mcpServerPromptContributor) PromptSource() PromptSourceDescriptor {
|
|
|
|
|
return PromptSourceDescriptor{
|
|
|
|
|
ID: mcpPromptSourceID(c.serverName),
|
|
|
|
|
Owner: "mcp",
|
|
|
|
|
Description: fmt.Sprintf("MCP server %q capability prompt", c.serverName),
|
|
|
|
|
Allowed: []PromptPlacement{{Layer: PromptLayerCapability, Slot: PromptSlotMCP}},
|
|
|
|
|
StableByDefault: true,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (c mcpServerPromptContributor) ContributePrompt(
|
|
|
|
|
_ context.Context,
|
2026-05-22 02:06:40 +00:00
|
|
|
req PromptBuildRequest,
|
2026-04-24 11:36:46 +00:00
|
|
|
) ([]PromptPart, error) {
|
2026-05-22 02:06:40 +00:00
|
|
|
if req.SuppressToolUseRule {
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
2026-04-24 11:36:46 +00:00
|
|
|
serverName := strings.TrimSpace(c.serverName)
|
|
|
|
|
if serverName == "" || c.toolCount <= 0 {
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
2026-05-22 02:06:40 +00:00
|
|
|
if len(req.AllowedTools) > 0 &&
|
|
|
|
|
!promptAllowsToolPrefix(req, "mcp_"+promptSourceComponent(serverName)+"_") {
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
2026-04-24 11:36:46 +00:00
|
|
|
|
|
|
|
|
availability := "available as native tools"
|
|
|
|
|
if c.deferred {
|
|
|
|
|
availability = "hidden behind tool discovery until unlocked"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return []PromptPart{
|
|
|
|
|
{
|
|
|
|
|
ID: "capability.mcp." + promptSourceComponent(serverName),
|
|
|
|
|
Layer: PromptLayerCapability,
|
|
|
|
|
Slot: PromptSlotMCP,
|
|
|
|
|
Source: PromptSource{ID: mcpPromptSourceID(serverName), Name: "mcp:" + serverName},
|
|
|
|
|
Title: "MCP server capability",
|
|
|
|
|
Content: fmt.Sprintf(
|
|
|
|
|
"MCP server `%s` is connected. It contributes %d tool(s), currently %s.",
|
|
|
|
|
serverName,
|
|
|
|
|
c.toolCount,
|
|
|
|
|
availability,
|
|
|
|
|
),
|
|
|
|
|
Stable: true,
|
|
|
|
|
Cache: PromptCacheEphemeral,
|
|
|
|
|
},
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-07 11:16:30 +00:00
|
|
|
type agentDiscoveryPromptContributor struct {
|
2026-05-07 16:26:09 +00:00
|
|
|
agentID string
|
|
|
|
|
discover func(agentID string) []AgentDescriptor
|
2026-05-07 11:16:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (c agentDiscoveryPromptContributor) PromptSource() PromptSourceDescriptor {
|
|
|
|
|
return PromptSourceDescriptor{
|
|
|
|
|
ID: PromptSourceAgentDiscovery,
|
|
|
|
|
Owner: "agent",
|
|
|
|
|
Description: "Structured multi-agent discovery registry",
|
|
|
|
|
Allowed: []PromptPlacement{{Layer: PromptLayerCapability, Slot: PromptSlotTooling}},
|
|
|
|
|
StableByDefault: false,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (c agentDiscoveryPromptContributor) ContributePrompt(
|
|
|
|
|
_ context.Context,
|
2026-05-22 02:06:40 +00:00
|
|
|
req PromptBuildRequest,
|
2026-05-07 11:16:30 +00:00
|
|
|
) ([]PromptPart, error) {
|
2026-05-22 02:06:40 +00:00
|
|
|
if req.SuppressToolUseRule {
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
if !promptAllowsTool(req, "spawn") {
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
2026-05-07 11:16:30 +00:00
|
|
|
if c.discover == nil {
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
2026-05-07 16:26:09 +00:00
|
|
|
content := formatAgentDiscoverySection(c.discover(c.agentID))
|
2026-05-07 11:16:30 +00:00
|
|
|
if strings.TrimSpace(content) == "" {
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return []PromptPart{
|
|
|
|
|
{
|
|
|
|
|
ID: "capability.agent_discovery",
|
|
|
|
|
Layer: PromptLayerCapability,
|
|
|
|
|
Slot: PromptSlotTooling,
|
|
|
|
|
Source: PromptSource{ID: PromptSourceAgentDiscovery, Name: "agent:discovery"},
|
|
|
|
|
Title: "agent discovery",
|
|
|
|
|
Content: content,
|
|
|
|
|
Stable: false,
|
|
|
|
|
Cache: PromptCacheNone,
|
|
|
|
|
},
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-24 11:36:46 +00:00
|
|
|
func mcpPromptSourceID(serverName string) PromptSourceID {
|
|
|
|
|
return PromptSourceID("mcp:" + promptSourceComponent(serverName))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func promptSourceComponent(value string) string {
|
|
|
|
|
const maxLen = 64
|
|
|
|
|
|
|
|
|
|
value = strings.ToLower(strings.TrimSpace(value))
|
|
|
|
|
if value == "" {
|
|
|
|
|
return "unnamed"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var b strings.Builder
|
|
|
|
|
lastWasSep := false
|
|
|
|
|
for _, r := range value {
|
|
|
|
|
switch {
|
|
|
|
|
case r >= 'a' && r <= 'z':
|
|
|
|
|
b.WriteRune(r)
|
|
|
|
|
lastWasSep = false
|
|
|
|
|
case r >= '0' && r <= '9':
|
|
|
|
|
b.WriteRune(r)
|
|
|
|
|
lastWasSep = false
|
|
|
|
|
case r == '-' || r == '_':
|
|
|
|
|
if !lastWasSep && b.Len() > 0 {
|
|
|
|
|
b.WriteRune(r)
|
|
|
|
|
lastWasSep = true
|
|
|
|
|
}
|
|
|
|
|
default:
|
|
|
|
|
if !lastWasSep && b.Len() > 0 {
|
|
|
|
|
b.WriteRune('_')
|
|
|
|
|
lastWasSep = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result := strings.Trim(b.String(), "_")
|
|
|
|
|
if result == "" {
|
|
|
|
|
return "unnamed"
|
|
|
|
|
}
|
|
|
|
|
if len(result) > maxLen {
|
|
|
|
|
return result[:maxLen]
|
|
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
}
|
2026-05-22 02:06:40 +00:00
|
|
|
|
|
|
|
|
func promptAllowsTool(req PromptBuildRequest, name string) bool {
|
|
|
|
|
if len(req.AllowedTools) == 0 {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
allowed := cleanAllowedSet(req.AllowedTools)
|
|
|
|
|
_, ok := allowed[strings.ToLower(strings.TrimSpace(name))]
|
|
|
|
|
return ok
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func promptAllowsToolPrefix(req PromptBuildRequest, prefix string) bool {
|
|
|
|
|
if len(req.AllowedTools) == 0 {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
prefix = strings.ToLower(strings.TrimSpace(prefix))
|
|
|
|
|
if prefix == "" {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
for _, name := range req.AllowedTools {
|
|
|
|
|
if strings.HasPrefix(strings.ToLower(strings.TrimSpace(name)), prefix) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|