refactor(agent): move delegation details out of discovery prompt

This commit is contained in:
afjcjsbx
2026-03-29 22:57:57 +02:00
parent 6429f6af9a
commit 0ef25f779e
6 changed files with 48 additions and 278 deletions
+1 -7
View File
@@ -22,7 +22,6 @@ import (
type ContextBuilder struct {
workspace string
agentID string
skillsLoader *skills.SkillsLoader
memory *MemoryStore
toolDiscoveryBM25 bool
@@ -60,11 +59,6 @@ func (cb *ContextBuilder) WithSplitOnMarker(enabled bool) *ContextBuilder {
return cb
}
func (cb *ContextBuilder) WithAgentIdentity(agentID string) *ContextBuilder {
cb.agentID = strings.TrimSpace(agentID)
return cb
}
func (cb *ContextBuilder) WithAgentDiscovery(
discover func(workspace string) []AgentDescriptor,
) *ContextBuilder {
@@ -200,7 +194,7 @@ func (cb *ContextBuilder) buildAgentDiscoveryContext() string {
if cb.agentDiscovery == nil {
return ""
}
return formatAgentDiscoverySection(cb.agentID, cb.agentDiscovery(cb.workspace))
return formatAgentDiscoverySection(cb.agentDiscovery(cb.workspace))
}
// BuildSystemPromptWithCache returns the cached system prompt if available
+25 -138
View File
@@ -2,23 +2,19 @@ package agent
import (
"encoding/json"
"fmt"
"path/filepath"
"reflect"
"sort"
"strings"
"github.com/sipeed/picoclaw/pkg/config"
"github.com/sipeed/picoclaw/pkg/routing"
)
// AgentDescriptor is the structured discovery payload injected into each
// agent's system prompt so the LLM can make concrete delegation decisions.
// agent's system prompt so the LLM can choose a peer by identity.
type AgentDescriptor struct {
ID string `json:"id"`
AgentFrontmatter
AvailableTools []string `json:"available_tools"`
Channels []string `json:"channels"`
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
}
// ListAgents returns structured descriptors for every agent in the current
@@ -81,54 +77,34 @@ func (r *AgentRegistry) GetAgentDescriptor(agentID string) (*AgentDescriptor, bo
func (r *AgentRegistry) buildAgentDescriptorLocked(agent *AgentInstance) AgentDescriptor {
definition := loadAgentDefinition(agent.Workspace)
name, description := descriptorIdentity(agent.ID, definition)
return AgentDescriptor{
ID: agent.ID,
AgentFrontmatter: descriptorFrontmatter(agent.ID, definition),
AvailableTools: visibleToolNames(agent),
Channels: r.channelsForAgentLocked(agent.ID),
ID: agent.ID,
Name: name,
Description: description,
}
}
func visibleToolNames(agent *AgentInstance) []string {
if agent == nil || agent.Tools == nil {
return []string{}
}
defs := agent.Tools.ToProviderDefs()
names := make([]string, 0, len(defs))
for _, def := range defs {
name := strings.TrimSpace(def.Function.Name)
if name == "" {
continue
}
names = append(names, name)
}
if names == nil {
return []string{}
}
return names
}
func descriptorFrontmatter(agentID string, definition AgentContextDefinition) AgentFrontmatter {
frontmatter := AgentFrontmatter{}
func descriptorIdentity(agentID string, definition AgentContextDefinition) (string, string) {
name := agentID
description := ""
if definition.Agent != nil {
frontmatter = definition.Agent.Frontmatter
frontmatter.Tools = append([]string(nil), frontmatter.Tools...)
frontmatter.Skills = append([]string(nil), frontmatter.Skills...)
frontmatter.MCPServers = append([]string(nil), frontmatter.MCPServers...)
if trimmed := strings.TrimSpace(definition.Agent.Frontmatter.Name); trimmed != "" {
name = trimmed
}
if trimmed := strings.TrimSpace(definition.Agent.Frontmatter.Description); trimmed != "" {
description = trimmed
}
}
if strings.TrimSpace(frontmatter.Name) == "" {
frontmatter.Name = agentID
}
if strings.TrimSpace(frontmatter.Description) == "" &&
if description == "" &&
definition.Source == AgentDefinitionSourceAgents &&
definition.Agent != nil {
frontmatter.Description = firstMeaningfulParagraph(definition.Agent.Body)
description = firstMeaningfulParagraph(definition.Agent.Body)
}
return frontmatter
return name, description
}
func firstMeaningfulParagraph(content string) string {
@@ -163,85 +139,6 @@ func firstMeaningfulParagraph(content string) string {
return ""
}
func (r *AgentRegistry) channelsForAgentLocked(agentID string) []string {
channels := make(map[string]struct{})
enabled := enabledChannelSet(r.cfg)
if defaultID := r.defaultAgentIDLocked(); defaultID != "" && defaultID == agentID {
for channel := range enabled {
channels[channel] = struct{}{}
}
}
if r.cfg != nil {
for _, binding := range r.cfg.Bindings {
if routing.NormalizeAgentID(binding.AgentID) != agentID {
continue
}
channel := strings.ToLower(strings.TrimSpace(binding.Match.Channel))
if channel == "" {
continue
}
if _, ok := enabled[channel]; !ok {
continue
}
channels[channel] = struct{}{}
}
}
if len(channels) == 0 {
return []string{}
}
result := make([]string, 0, len(channels))
for channel := range channels {
result = append(result, channel)
}
sort.Strings(result)
return result
}
func enabledChannels(cfg *config.Config) []string {
if cfg == nil {
return []string{}
}
value := reflect.ValueOf(cfg.Channels)
typ := value.Type()
enabled := make([]string, 0, typ.NumField())
for i := 0; i < typ.NumField(); i++ {
fieldValue := value.Field(i)
enabledField := fieldValue.FieldByName("Enabled")
if !enabledField.IsValid() || enabledField.Kind() != reflect.Bool || !enabledField.Bool() {
continue
}
name := jsonFieldName(typ.Field(i).Tag.Get("json"))
if name == "" {
continue
}
enabled = append(enabled, name)
}
sort.Strings(enabled)
return enabled
}
func enabledChannelSet(cfg *config.Config) map[string]struct{} {
channels := enabledChannels(cfg)
result := make(map[string]struct{}, len(channels))
for _, channel := range channels {
result[channel] = struct{}{}
}
return result
}
func jsonFieldName(tag string) string {
name := strings.TrimSpace(strings.Split(tag, ",")[0])
if name == "" || name == "-" {
return ""
}
return name
}
func (r *AgentRegistry) workspaceForAgentIDLocked(agentID string) string {
agent, ok := r.agents[routing.NormalizeAgentID(agentID)]
if !ok || agent == nil {
@@ -283,17 +180,15 @@ func cleanWorkspacePath(path string) string {
return filepath.Clean(path)
}
func formatAgentDiscoverySection(currentAgentID string, agents []AgentDescriptor) string {
func formatAgentDiscoverySection(agents []AgentDescriptor) string {
if len(agents) <= 1 {
return ""
}
payload := struct {
CurrentAgentID string `json:"current_agent_id"`
Agents []AgentDescriptor `json:"agents"`
Agents []AgentDescriptor `json:"agents"`
}{
CurrentAgentID: strings.TrimSpace(currentAgentID),
Agents: agents,
Agents: agents,
}
encoded, err := json.MarshalIndent(payload, "", " ")
@@ -303,17 +198,9 @@ func formatAgentDiscoverySection(currentAgentID string, agents []AgentDescriptor
var header strings.Builder
header.WriteString("# Agent Discovery\n\n")
if payload.CurrentAgentID != "" {
fmt.Fprintf(
&header,
"You are agent %q. This registry is authoritative for the current PicoClaw instance and includes your own entry.\n",
payload.CurrentAgentID,
)
} else {
header.WriteString("This registry is authoritative for the current PicoClaw instance.\n")
}
header.WriteString("This registry is authoritative for the current PicoClaw instance.\n")
header.WriteString(
"Delegate based on available_tools first, then skills, mcpServers, model, channels, and description. Use only agent IDs listed here.\n\n",
"Choose a peer based on its description. Use only agent IDs listed here when calling spawn.\n\n",
)
header.WriteString("```json\n")
header.Write(encoded)
+8 -73
View File
@@ -1,7 +1,6 @@
package agent
import (
"slices"
"strings"
"testing"
@@ -13,10 +12,6 @@ func TestAgentRegistry_ListAgentsBuildsStructuredDescriptors(t *testing.T) {
"AGENT.md": `---
name: Main Frontmatter Name
description: Structured main agent
model: main-frontmatter-model
tools: [read_file, write_file]
skills: [coordination]
mcpServers: [filesystem]
---
# Agent
@@ -29,10 +24,6 @@ Handle general requests.
"AGENT.md": `---
name: Support Frontmatter Name
description: Support frontmatter description
model: support-frontmatter-model
tools: [read_file]
skills: [support-playbook]
mcpServers: [support-db]
---
# Agent
@@ -45,18 +36,6 @@ Handle support tickets carefully.
{ID: "main", Default: true, Name: "Configured Main", Workspace: mainWorkspace},
{ID: "support", Workspace: supportWorkspace},
})
cfg.Tools.ReadFile.Enabled = true
cfg.Tools.WriteFile.Enabled = true
cfg.Channels.Telegram.Enabled = true
cfg.Bindings = []config.AgentBinding{
{
AgentID: "support",
Match: config.BindingMatch{
Channel: "telegram",
AccountID: "*",
},
},
}
registry := NewAgentRegistry(cfg, &mockRegistryProvider{})
@@ -74,28 +53,6 @@ Handle support tickets carefully.
if descriptors[0].Description != "Structured main agent" {
t.Fatalf("expected frontmatter description, got %q", descriptors[0].Description)
}
if descriptors[0].Model != "main-frontmatter-model" {
t.Fatalf("expected frontmatter model, got %q", descriptors[0].Model)
}
if !slices.Equal(descriptors[0].Tools, []string{"read_file", "write_file"}) {
t.Fatalf("expected declared frontmatter tools, got %v", descriptors[0].Tools)
}
if !slices.Equal(descriptors[0].Skills, []string{"coordination"}) {
t.Fatalf("expected frontmatter skills, got %v", descriptors[0].Skills)
}
if !slices.Equal(descriptors[0].MCPServers, []string{"filesystem"}) {
t.Fatalf("expected frontmatter mcpServers, got %v", descriptors[0].MCPServers)
}
if !slices.Contains(descriptors[0].AvailableTools, "read_file") ||
!slices.Contains(descriptors[0].AvailableTools, "write_file") {
t.Fatalf("expected visible file tools in descriptor, got %v", descriptors[0].AvailableTools)
}
if !slices.Equal(descriptors[0].Channels, []string{"telegram"}) {
t.Fatalf(
"expected default agent to cover enabled telegram channel, got %v",
descriptors[0].Channels,
)
}
support, ok := registry.GetAgentDescriptor("support")
if !ok || support == nil {
@@ -107,25 +64,12 @@ Handle support tickets carefully.
if support.Description != "Support frontmatter description" {
t.Fatalf("expected support frontmatter description, got %q", support.Description)
}
if support.Model != "support-frontmatter-model" {
t.Fatalf("expected support frontmatter model, got %q", support.Model)
}
if !slices.Equal(support.Skills, []string{"support-playbook"}) {
t.Fatalf("expected support skills, got %v", support.Skills)
}
if !slices.Equal(support.MCPServers, []string{"support-db"}) {
t.Fatalf("expected support mcpServers, got %v", support.MCPServers)
}
if !slices.Equal(support.Channels, []string{"telegram"}) {
t.Fatalf("expected support channel binding, got %v", support.Channels)
}
}
func TestContextBuilder_BuildMessagesIncludesAgentDiscoverySection(t *testing.T) {
mainWorkspace := setupWorkspace(t, map[string]string{
"AGENT.md": `---
description: Main agent
skills: [coordination]
---
# Agent
@@ -136,9 +80,8 @@ Generalist.
researchWorkspace := setupWorkspace(t, map[string]string{
"AGENT.md": `---
name: Research Agent
description: Research specialist
skills: [deep-research]
mcpServers: [web-index]
---
# Agent
@@ -178,23 +121,18 @@ Investigate deeply.
if !strings.Contains(systemPrompt, "# Agent Discovery") {
t.Fatalf("expected discovery section in system prompt, got %q", systemPrompt)
}
if !strings.Contains(systemPrompt, `"current_agent_id": "main"`) {
t.Fatalf("expected current agent id in discovery section, got %q", systemPrompt)
}
if !strings.Contains(systemPrompt, `"id": "main"`) ||
!strings.Contains(systemPrompt, `"id": "research"`) {
t.Fatalf("expected self and peer descriptors in discovery section, got %q", systemPrompt)
}
if !strings.Contains(systemPrompt, `"available_tools": [`) ||
!strings.Contains(systemPrompt, `"read_file"`) ||
!strings.Contains(systemPrompt, `"write_file"`) {
t.Fatalf("expected visible tool list in discovery section, got %q", systemPrompt)
if !strings.Contains(systemPrompt, `"name": "main"`) ||
!strings.Contains(systemPrompt, `"description": "Research specialist"`) {
t.Fatalf("expected minimal identity fields in discovery section, got %q", systemPrompt)
}
if !strings.Contains(systemPrompt, `"skills": [`) || !strings.Contains(systemPrompt, `"deep-research"`) {
t.Fatalf("expected frontmatter skills in discovery section, got %q", systemPrompt)
}
if !strings.Contains(systemPrompt, `"mcpServers": [`) || !strings.Contains(systemPrompt, `"web-index"`) {
t.Fatalf("expected frontmatter mcpServers in discovery section, got %q", systemPrompt)
for _, forbidden := range []string{`"current_agent_id"`, `"available_tools"`, `"model"`, `"channels"`, `"skills"`, `"mcpServers"`, `"tools"`} {
if strings.Contains(systemPrompt, forbidden) {
t.Fatalf("did not expect %s in discovery section, got %q", forbidden, systemPrompt)
}
}
}
@@ -239,7 +177,4 @@ Generalist.
if strings.Contains(systemPrompt, "# Agent Discovery") {
t.Fatalf("did not expect discovery section for singleton registry, got %q", systemPrompt)
}
if strings.Contains(systemPrompt, `"current_agent_id": "main"`) {
t.Fatalf("did not expect discovery payload for singleton registry, got %q", systemPrompt)
}
}
+2 -4
View File
@@ -54,11 +54,9 @@ func NewAgentRegistry(
}
}
for id, instance := range registry.agents {
for _, instance := range registry.agents {
if instance.ContextBuilder != nil {
instance.ContextBuilder.
WithAgentIdentity(id).
WithAgentDiscovery(registry.ListAgents)
instance.ContextBuilder.WithAgentDiscovery(registry.ListAgents)
}
}