mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
refactor(agent): move delegation details out of discovery prompt
This commit is contained in:
+6
-28
@@ -269,7 +269,7 @@ Notes:
|
||||
- This is an allowlist, not a preference hint.
|
||||
- Tool names are matched against the runtime tool name 1:1.
|
||||
- Use runtime tool names such as `web_search`, `web_fetch`, `spawn`, `subagent`, `send_file`.
|
||||
- `available_tools` in Agent Discovery reflects the filtered runtime result, while `tools` reflects the identity declared in `AGENT.md`.
|
||||
- Tool declarations in `AGENT.md` are used by runtime/tooling, but they are not injected into the discovery prompt.
|
||||
|
||||
### Agent Discovery (Automatic)
|
||||
|
||||
@@ -284,56 +284,34 @@ Each entry includes:
|
||||
| `id` | Stable agent id |
|
||||
| `name` | Agent identity name from `AGENT.md` frontmatter |
|
||||
| `description` | Agent identity description from `AGENT.md` frontmatter |
|
||||
| `tools` | Declared tool identity from `AGENT.md` frontmatter |
|
||||
| `skills` | Declared skill identity from `AGENT.md` frontmatter |
|
||||
| `mcpServers` | Declared MCP server identity from `AGENT.md` frontmatter |
|
||||
| `model` | Declared model from `AGENT.md` frontmatter |
|
||||
| `available_tools` | Tool names currently visible to that agent |
|
||||
| `channels` | Channels that route to that agent |
|
||||
|
||||
Important behavior:
|
||||
|
||||
- The discovery section includes the current agent's own entry, so the model has self-awareness.
|
||||
- `available_tools` is the most important field for delegation. It reflects the tools the target agent can actually use, not just a natural-language description.
|
||||
- Identity fields (`name`, `description`, `tools`, `skills`, `mcpServers`, `model`) come from `AGENT.md` frontmatter.
|
||||
- Discovery is intentionally lightweight. It gives the model only the identity it needs to choose a peer: `id`, `name`, and `description`.
|
||||
- `config.json` remains the infrastructure layer: workspace, default agent selection, routing, and subagent permissions.
|
||||
- `channels` come from routing state:
|
||||
- the default agent exposes enabled channels
|
||||
- other agents expose channels that explicitly bind to them through `bindings`
|
||||
- `AGENT.md` remains the identity layer. Runtime/tool code can still use its `tools`, `skills`, `mcpServers`, and `model` fields when delegation happens.
|
||||
|
||||
Example injected shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"current_agent_id": "main",
|
||||
"agents": [
|
||||
{
|
||||
"id": "main",
|
||||
"name": "Main Assistant",
|
||||
"description": "Generalist agent for day-to-day requests.",
|
||||
"tools": ["read_file", "write_file", "exec", "spawn"],
|
||||
"skills": ["coordination"],
|
||||
"mcpServers": ["filesystem"],
|
||||
"model": "gpt-4o-mini",
|
||||
"available_tools": ["read_file", "write_file", "exec", "spawn"],
|
||||
"channels": ["telegram", "discord"]
|
||||
"description": "Generalist agent for day-to-day requests."
|
||||
},
|
||||
{
|
||||
"id": "research",
|
||||
"name": "Research Agent",
|
||||
"description": "Specialist for long-form investigation and web work.",
|
||||
"tools": ["read_file", "web_search", "web_fetch", "message"],
|
||||
"skills": ["deep-research"],
|
||||
"mcpServers": ["web-index"],
|
||||
"model": "claude-sonnet-4.5",
|
||||
"available_tools": ["web_search", "web_fetch", "read_file"],
|
||||
"channels": ["telegram"]
|
||||
"description": "Specialist for long-form investigation and web work."
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
In practice, this means a generalist agent can see that a peer has `["web_search", "web_fetch"]` while it only has local file tools, and can decide to delegate to that peer instead of guessing.
|
||||
In practice, this means a generalist agent can choose a peer based on its role description, then call `spawn` with the peer's `agent_id`. The runtime resolves the rest.
|
||||
|
||||
### 🔒 Security Sandbox
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ Note:
|
||||
- È una allowlist reale, non un suggerimento per l'LLM.
|
||||
- I nomi dei tool fanno match 1:1 con il nome runtime del tool.
|
||||
- Se ti serve controllo preciso, usa i nomi runtime effettivi come `web_search`, `web_fetch`, `spawn`, `subagent`, `send_file`.
|
||||
- `available_tools` nella Agent Discovery riflette il risultato runtime filtrato, mentre `tools` riflette l'identità dichiarata in `AGENT.md`.
|
||||
- Le dichiarazioni dei tool in `AGENT.md` sono usate dal runtime e dai tool, ma non vengono iniettate nel prompt di discovery.
|
||||
|
||||
### Discovery Multi-Agent (Automatica)
|
||||
|
||||
@@ -109,56 +109,34 @@ Ogni entry include:
|
||||
| `id` | ID stabile dell'agent |
|
||||
| `name` | Nome identitario da `AGENT.md` frontmatter |
|
||||
| `description` | Descrizione identitaria da `AGENT.md` frontmatter |
|
||||
| `tools` | Tool dichiarati nel frontmatter di `AGENT.md` |
|
||||
| `skills` | Skill dichiarate nel frontmatter di `AGENT.md` |
|
||||
| `mcpServers` | Server MCP dichiarati nel frontmatter di `AGENT.md` |
|
||||
| `model` | Modello dichiarato nel frontmatter di `AGENT.md` |
|
||||
| `available_tools` | Tool attualmente visibili a quell'agent |
|
||||
| `channels` | Canali instradati verso quell'agent |
|
||||
|
||||
Dettagli importanti:
|
||||
|
||||
- La sezione include anche l'entry dell'agent corrente, quindi c'è self-awareness.
|
||||
- `available_tools` è il campo più importante per delegare bene: l'LLM vede i tool reali del peer, non deve indovinarli dalla sola descrizione.
|
||||
- I campi di identità (`name`, `description`, `tools`, `skills`, `mcpServers`, `model`) arrivano dal frontmatter di `AGENT.md`.
|
||||
- La discovery è volutamente leggera. Fornisce al modello solo l'identità necessaria per scegliere un peer: `id`, `name`, `description`.
|
||||
- `config.json` resta il layer infrastrutturale: workspace, agent di default, routing e permessi di subagent.
|
||||
- `channels` derivano dal routing:
|
||||
- l'agent di default espone i canali abilitati
|
||||
- gli altri agent espongono i canali che hanno un binding esplicito verso di loro
|
||||
- `AGENT.md` resta il layer di identità. Il codice runtime e i tool possono comunque usare `tools`, `skills`, `mcpServers` e `model` quando avviene la delega.
|
||||
|
||||
Forma dell'oggetto iniettato:
|
||||
|
||||
```json
|
||||
{
|
||||
"current_agent_id": "main",
|
||||
"agents": [
|
||||
{
|
||||
"id": "main",
|
||||
"name": "Main Assistant",
|
||||
"description": "Agent generalista per richieste quotidiane.",
|
||||
"tools": ["read_file", "write_file", "exec", "spawn"],
|
||||
"skills": ["coordination"],
|
||||
"mcpServers": ["filesystem"],
|
||||
"model": "gpt-4o-mini",
|
||||
"available_tools": ["read_file", "write_file", "exec", "spawn"],
|
||||
"channels": ["telegram", "discord"]
|
||||
"description": "Agent generalista per richieste quotidiane."
|
||||
},
|
||||
{
|
||||
"id": "research",
|
||||
"name": "Research Agent",
|
||||
"description": "Specialista per investigazioni e lavoro web.",
|
||||
"tools": ["read_file", "web_search", "web_fetch", "message"],
|
||||
"skills": ["deep-research"],
|
||||
"mcpServers": ["web-index"],
|
||||
"model": "claude-sonnet-4.5",
|
||||
"available_tools": ["web_search", "web_fetch", "read_file"],
|
||||
"channels": ["telegram"]
|
||||
"description": "Specialista per investigazioni e lavoro web."
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
In pratica, un agent generalista può vedere che un peer ha `["web_search", "web_fetch"]` mentre lui ha solo tool locali, e scegliere di delegare a quel peer in modo esplicito invece di andare a tentativi.
|
||||
In pratica, un agent generalista sceglie un peer in base alla descrizione del suo ruolo, poi chiama `spawn` con l'`agent_id` del peer. Il runtime risolve il resto.
|
||||
|
||||
### 🔒 Sandbox di Sicurezza
|
||||
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user