refactor(agent): source discovery identity from AGENT.md frontmatter
This commit is contained in:
parent
bca131909d
commit
6429f6af9a
13 changed files with 281 additions and 160 deletions
|
|
@ -248,22 +248,20 @@ In other words: **channel + account form the candidate set; peer/guild/team then
|
||||||
|
|
||||||
### Agent Tool Allowlist
|
### Agent Tool Allowlist
|
||||||
|
|
||||||
You can restrict an individual agent to a subset of runtime tools with `agents.list[].tools`.
|
Per-agent tool declarations live in `AGENT.md` frontmatter, not in `config.json`.
|
||||||
|
|
||||||
If `tools` is omitted, the agent gets the normal globally enabled tool set. If `tools` is present, PicoClaw registers only the listed tools for that agent.
|
If `tools` is omitted from frontmatter, the agent gets the normal globally enabled tool set. If `tools` is present, PicoClaw registers only the listed runtime tools for that agent.
|
||||||
|
|
||||||
```json
|
```md
|
||||||
{
|
---
|
||||||
"agents": {
|
name: Research Agent
|
||||||
"list": [
|
description: Specialist for web research and in-depth analysis.
|
||||||
{
|
tools: [read_file, write_file, web_search, web_fetch, message]
|
||||||
"id": "research",
|
skills: [deep-research]
|
||||||
"name": "Research Agent",
|
mcpServers: [web-index]
|
||||||
"tools": ["read_file", "write_file", "web_search", "web_fetch", "message"]
|
---
|
||||||
}
|
|
||||||
]
|
You are the research agent.
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
|
|
@ -271,7 +269,7 @@ Notes:
|
||||||
- This is an allowlist, not a preference hint.
|
- This is an allowlist, not a preference hint.
|
||||||
- Tool names are matched against the runtime tool name 1:1.
|
- 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`.
|
- Use runtime tool names such as `web_search`, `web_fetch`, `spawn`, `subagent`, `send_file`.
|
||||||
- The `available_tools` field in Agent Discovery reflects the filtered runtime result.
|
- `available_tools` in Agent Discovery reflects the filtered runtime result, while `tools` reflects the identity declared in `AGENT.md`.
|
||||||
|
|
||||||
### Agent Discovery (Automatic)
|
### Agent Discovery (Automatic)
|
||||||
|
|
||||||
|
|
@ -284,9 +282,12 @@ Each entry includes:
|
||||||
| Field | Meaning |
|
| Field | Meaning |
|
||||||
|-------|---------|
|
|-------|---------|
|
||||||
| `id` | Stable agent id |
|
| `id` | Stable agent id |
|
||||||
| `name` | Human-friendly agent name |
|
| `name` | Agent identity name from `AGENT.md` frontmatter |
|
||||||
| `description` | Short capability summary |
|
| `description` | Agent identity description from `AGENT.md` frontmatter |
|
||||||
| `model` | Current model used by that agent |
|
| `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 |
|
| `available_tools` | Tool names currently visible to that agent |
|
||||||
| `channels` | Channels that route to that agent |
|
| `channels` | Channels that route to that agent |
|
||||||
|
|
||||||
|
|
@ -294,8 +295,8 @@ Important behavior:
|
||||||
|
|
||||||
- The discovery section includes the current agent's own entry, so the model has self-awareness.
|
- 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.
|
- `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.
|
||||||
- `description` is sourced from `AGENT.md` frontmatter `description` when available, otherwise from the first meaningful paragraph of `AGENT.md`, and finally `SOUL.md`.
|
- Identity fields (`name`, `description`, `tools`, `skills`, `mcpServers`, `model`) come from `AGENT.md` frontmatter.
|
||||||
- `name` comes from `agents.list[].name` first, then `AGENT.md` frontmatter `name`, then falls back to the agent id.
|
- `config.json` remains the infrastructure layer: workspace, default agent selection, routing, and subagent permissions.
|
||||||
- `channels` come from routing state:
|
- `channels` come from routing state:
|
||||||
- the default agent exposes enabled channels
|
- the default agent exposes enabled channels
|
||||||
- other agents expose channels that explicitly bind to them through `bindings`
|
- other agents expose channels that explicitly bind to them through `bindings`
|
||||||
|
|
@ -310,6 +311,9 @@ Example injected shape:
|
||||||
"id": "main",
|
"id": "main",
|
||||||
"name": "Main Assistant",
|
"name": "Main Assistant",
|
||||||
"description": "Generalist agent for day-to-day requests.",
|
"description": "Generalist agent for day-to-day requests.",
|
||||||
|
"tools": ["read_file", "write_file", "exec", "spawn"],
|
||||||
|
"skills": ["coordination"],
|
||||||
|
"mcpServers": ["filesystem"],
|
||||||
"model": "gpt-4o-mini",
|
"model": "gpt-4o-mini",
|
||||||
"available_tools": ["read_file", "write_file", "exec", "spawn"],
|
"available_tools": ["read_file", "write_file", "exec", "spawn"],
|
||||||
"channels": ["telegram", "discord"]
|
"channels": ["telegram", "discord"]
|
||||||
|
|
@ -318,6 +322,9 @@ Example injected shape:
|
||||||
"id": "research",
|
"id": "research",
|
||||||
"name": "Research Agent",
|
"name": "Research Agent",
|
||||||
"description": "Specialist for long-form investigation and web work.",
|
"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",
|
"model": "claude-sonnet-4.5",
|
||||||
"available_tools": ["web_search", "web_fetch", "read_file"],
|
"available_tools": ["web_search", "web_fetch", "read_file"],
|
||||||
"channels": ["telegram"]
|
"channels": ["telegram"]
|
||||||
|
|
|
||||||
|
|
@ -73,22 +73,20 @@ export PICOCLAW_BUILTIN_SKILLS=/path/to/skills
|
||||||
|
|
||||||
### Allowlist dei Tool per Agent
|
### Allowlist dei Tool per Agent
|
||||||
|
|
||||||
Puoi limitare un singolo agent a un sottoinsieme di tool runtime con `agents.list[].tools`.
|
La dichiarazione dei tool per-agent vive nel frontmatter di `AGENT.md`, non in `config.json`.
|
||||||
|
|
||||||
Se `tools` è omesso, l'agent riceve il normale set globale dei tool abilitati. Se `tools` è presente, PicoClaw registra per quell'agent solo i tool elencati.
|
Se `tools` è omesso nel frontmatter, l'agent riceve il normale set globale dei tool abilitati. Se `tools` è presente, PicoClaw registra per quell'agent solo i tool runtime elencati.
|
||||||
|
|
||||||
```json
|
```md
|
||||||
{
|
---
|
||||||
"agents": {
|
name: Research Agent
|
||||||
"list": [
|
description: Specialista per ricerca web e analisi approfondita.
|
||||||
{
|
tools: [read_file, write_file, web_search, web_fetch, message]
|
||||||
"id": "research",
|
skills: [deep-research]
|
||||||
"name": "Research Agent",
|
mcpServers: [web-index]
|
||||||
"tools": ["read_file", "write_file", "web_search", "web_fetch", "message"]
|
---
|
||||||
}
|
|
||||||
]
|
Sei l'agent di ricerca.
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Note:
|
Note:
|
||||||
|
|
@ -96,7 +94,7 @@ Note:
|
||||||
- È una allowlist reale, non un suggerimento per l'LLM.
|
- È una allowlist reale, non un suggerimento per l'LLM.
|
||||||
- I nomi dei tool fanno match 1:1 con il nome runtime del tool.
|
- 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`.
|
- Se ti serve controllo preciso, usa i nomi runtime effettivi come `web_search`, `web_fetch`, `spawn`, `subagent`, `send_file`.
|
||||||
- Il campo `available_tools` nella Agent Discovery riflette il risultato filtrato reale.
|
- `available_tools` nella Agent Discovery riflette il risultato runtime filtrato, mentre `tools` riflette l'identità dichiarata in `AGENT.md`.
|
||||||
|
|
||||||
### Discovery Multi-Agent (Automatica)
|
### Discovery Multi-Agent (Automatica)
|
||||||
|
|
||||||
|
|
@ -109,9 +107,12 @@ Ogni entry include:
|
||||||
| Campo | Significato |
|
| Campo | Significato |
|
||||||
|-------|-------------|
|
|-------|-------------|
|
||||||
| `id` | ID stabile dell'agent |
|
| `id` | ID stabile dell'agent |
|
||||||
| `name` | Nome leggibile dell'agent |
|
| `name` | Nome identitario da `AGENT.md` frontmatter |
|
||||||
| `description` | Riassunto breve delle capacità |
|
| `description` | Descrizione identitaria da `AGENT.md` frontmatter |
|
||||||
| `model` | Modello attualmente usato da quell'agent |
|
| `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 |
|
| `available_tools` | Tool attualmente visibili a quell'agent |
|
||||||
| `channels` | Canali instradati verso quell'agent |
|
| `channels` | Canali instradati verso quell'agent |
|
||||||
|
|
||||||
|
|
@ -119,8 +120,8 @@ Dettagli importanti:
|
||||||
|
|
||||||
- La sezione include anche l'entry dell'agent corrente, quindi c'è self-awareness.
|
- 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.
|
- `available_tools` è il campo più importante per delegare bene: l'LLM vede i tool reali del peer, non deve indovinarli dalla sola descrizione.
|
||||||
- `description` viene presa da `AGENT.md` frontmatter `description` quando presente; altrimenti dal primo paragrafo utile di `AGENT.md`, e in fallback da `SOUL.md`.
|
- I campi di identità (`name`, `description`, `tools`, `skills`, `mcpServers`, `model`) arrivano dal frontmatter di `AGENT.md`.
|
||||||
- `name` arriva prima da `agents.list[].name`, poi da `AGENT.md` frontmatter `name`, e in fallback dall'ID dell'agent.
|
- `config.json` resta il layer infrastrutturale: workspace, agent di default, routing e permessi di subagent.
|
||||||
- `channels` derivano dal routing:
|
- `channels` derivano dal routing:
|
||||||
- l'agent di default espone i canali abilitati
|
- l'agent di default espone i canali abilitati
|
||||||
- gli altri agent espongono i canali che hanno un binding esplicito verso di loro
|
- gli altri agent espongono i canali che hanno un binding esplicito verso di loro
|
||||||
|
|
@ -135,6 +136,9 @@ Forma dell'oggetto iniettato:
|
||||||
"id": "main",
|
"id": "main",
|
||||||
"name": "Main Assistant",
|
"name": "Main Assistant",
|
||||||
"description": "Agent generalista per richieste quotidiane.",
|
"description": "Agent generalista per richieste quotidiane.",
|
||||||
|
"tools": ["read_file", "write_file", "exec", "spawn"],
|
||||||
|
"skills": ["coordination"],
|
||||||
|
"mcpServers": ["filesystem"],
|
||||||
"model": "gpt-4o-mini",
|
"model": "gpt-4o-mini",
|
||||||
"available_tools": ["read_file", "write_file", "exec", "spawn"],
|
"available_tools": ["read_file", "write_file", "exec", "spawn"],
|
||||||
"channels": ["telegram", "discord"]
|
"channels": ["telegram", "discord"]
|
||||||
|
|
@ -143,6 +147,9 @@ Forma dell'oggetto iniettato:
|
||||||
"id": "research",
|
"id": "research",
|
||||||
"name": "Research Agent",
|
"name": "Research Agent",
|
||||||
"description": "Specialista per investigazioni e lavoro web.",
|
"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",
|
"model": "claude-sonnet-4.5",
|
||||||
"available_tools": ["web_search", "web_fetch", "read_file"],
|
"available_tools": ["web_search", "web_fetch", "read_file"],
|
||||||
"channels": ["telegram"]
|
"channels": ["telegram"]
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ type AgentFrontmatter struct {
|
||||||
MaxTurns *int `json:"maxTurns,omitempty"`
|
MaxTurns *int `json:"maxTurns,omitempty"`
|
||||||
Skills []string `json:"skills,omitempty"`
|
Skills []string `json:"skills,omitempty"`
|
||||||
MCPServers []string `json:"mcpServers,omitempty"`
|
MCPServers []string `json:"mcpServers,omitempty"`
|
||||||
Fields map[string]any `json:"fields,omitempty"`
|
Fields map[string]any `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// AgentPromptDefinition represents the parsed AGENT.md or AGENTS.md prompt file.
|
// AgentPromptDefinition represents the parsed AGENT.md or AGENTS.md prompt file.
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
|
@ -14,10 +15,8 @@ import (
|
||||||
// AgentDescriptor is the structured discovery payload injected into each
|
// 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 make concrete delegation decisions.
|
||||||
type AgentDescriptor struct {
|
type AgentDescriptor struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
AgentFrontmatter
|
||||||
Description string `json:"description"`
|
|
||||||
Model string `json:"model"`
|
|
||||||
AvailableTools []string `json:"available_tools"`
|
AvailableTools []string `json:"available_tools"`
|
||||||
Channels []string `json:"channels"`
|
Channels []string `json:"channels"`
|
||||||
}
|
}
|
||||||
|
|
@ -82,21 +81,12 @@ func (r *AgentRegistry) GetAgentDescriptor(agentID string) (*AgentDescriptor, bo
|
||||||
|
|
||||||
func (r *AgentRegistry) buildAgentDescriptorLocked(agent *AgentInstance) AgentDescriptor {
|
func (r *AgentRegistry) buildAgentDescriptorLocked(agent *AgentInstance) AgentDescriptor {
|
||||||
definition := loadAgentDefinition(agent.Workspace)
|
definition := loadAgentDefinition(agent.Workspace)
|
||||||
name := strings.TrimSpace(agent.Name)
|
|
||||||
if name == "" && definition.Agent != nil {
|
|
||||||
name = strings.TrimSpace(definition.Agent.Frontmatter.Name)
|
|
||||||
}
|
|
||||||
if name == "" {
|
|
||||||
name = agent.ID
|
|
||||||
}
|
|
||||||
|
|
||||||
return AgentDescriptor{
|
return AgentDescriptor{
|
||||||
ID: agent.ID,
|
ID: agent.ID,
|
||||||
Name: name,
|
AgentFrontmatter: descriptorFrontmatter(agent.ID, definition),
|
||||||
Description: agentDescriptionFromDefinition(definition),
|
AvailableTools: visibleToolNames(agent),
|
||||||
Model: strings.TrimSpace(agent.Model),
|
Channels: r.channelsForAgentLocked(agent.ID),
|
||||||
AvailableTools: visibleToolNames(agent),
|
|
||||||
Channels: r.channelsForAgentLocked(agent.ID),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -120,21 +110,25 @@ func visibleToolNames(agent *AgentInstance) []string {
|
||||||
return names
|
return names
|
||||||
}
|
}
|
||||||
|
|
||||||
func agentDescriptionFromDefinition(definition AgentContextDefinition) string {
|
func descriptorFrontmatter(agentID string, definition AgentContextDefinition) AgentFrontmatter {
|
||||||
|
frontmatter := AgentFrontmatter{}
|
||||||
if definition.Agent != nil {
|
if definition.Agent != nil {
|
||||||
if desc := strings.TrimSpace(definition.Agent.Frontmatter.Description); desc != "" {
|
frontmatter = definition.Agent.Frontmatter
|
||||||
return desc
|
frontmatter.Tools = append([]string(nil), frontmatter.Tools...)
|
||||||
}
|
frontmatter.Skills = append([]string(nil), frontmatter.Skills...)
|
||||||
if desc := firstMeaningfulParagraph(definition.Agent.Body); desc != "" {
|
frontmatter.MCPServers = append([]string(nil), frontmatter.MCPServers...)
|
||||||
return desc
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if definition.Soul != nil {
|
|
||||||
if desc := firstMeaningfulParagraph(definition.Soul.Content); desc != "" {
|
if strings.TrimSpace(frontmatter.Name) == "" {
|
||||||
return desc
|
frontmatter.Name = agentID
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return ""
|
if strings.TrimSpace(frontmatter.Description) == "" &&
|
||||||
|
definition.Source == AgentDefinitionSourceAgents &&
|
||||||
|
definition.Agent != nil {
|
||||||
|
frontmatter.Description = firstMeaningfulParagraph(definition.Agent.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
return frontmatter
|
||||||
}
|
}
|
||||||
|
|
||||||
func firstMeaningfulParagraph(content string) string {
|
func firstMeaningfulParagraph(content string) string {
|
||||||
|
|
@ -171,9 +165,10 @@ func firstMeaningfulParagraph(content string) string {
|
||||||
|
|
||||||
func (r *AgentRegistry) channelsForAgentLocked(agentID string) []string {
|
func (r *AgentRegistry) channelsForAgentLocked(agentID string) []string {
|
||||||
channels := make(map[string]struct{})
|
channels := make(map[string]struct{})
|
||||||
|
enabled := enabledChannelSet(r.cfg)
|
||||||
|
|
||||||
if defaultID := r.defaultAgentIDLocked(); defaultID != "" && defaultID == agentID {
|
if defaultID := r.defaultAgentIDLocked(); defaultID != "" && defaultID == agentID {
|
||||||
for _, channel := range enabledChannels(r.cfg) {
|
for channel := range enabled {
|
||||||
channels[channel] = struct{}{}
|
channels[channel] = struct{}{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -187,6 +182,9 @@ func (r *AgentRegistry) channelsForAgentLocked(agentID string) []string {
|
||||||
if channel == "" {
|
if channel == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if _, ok := enabled[channel]; !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
channels[channel] = struct{}{}
|
channels[channel] = struct{}{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -208,58 +206,42 @@ func enabledChannels(cfg *config.Config) []string {
|
||||||
return []string{}
|
return []string{}
|
||||||
}
|
}
|
||||||
|
|
||||||
enabled := make([]string, 0, 16)
|
value := reflect.ValueOf(cfg.Channels)
|
||||||
if cfg.Channels.WhatsApp.Enabled {
|
typ := value.Type()
|
||||||
enabled = append(enabled, "whatsapp")
|
enabled := make([]string, 0, typ.NumField())
|
||||||
}
|
for i := 0; i < typ.NumField(); i++ {
|
||||||
if cfg.Channels.Telegram.Enabled {
|
fieldValue := value.Field(i)
|
||||||
enabled = append(enabled, "telegram")
|
enabledField := fieldValue.FieldByName("Enabled")
|
||||||
}
|
if !enabledField.IsValid() || enabledField.Kind() != reflect.Bool || !enabledField.Bool() {
|
||||||
if cfg.Channels.Feishu.Enabled {
|
continue
|
||||||
enabled = append(enabled, "feishu")
|
}
|
||||||
}
|
name := jsonFieldName(typ.Field(i).Tag.Get("json"))
|
||||||
if cfg.Channels.Discord.Enabled {
|
if name == "" {
|
||||||
enabled = append(enabled, "discord")
|
continue
|
||||||
}
|
}
|
||||||
if cfg.Channels.MaixCam.Enabled {
|
enabled = append(enabled, name)
|
||||||
enabled = append(enabled, "maixcam")
|
|
||||||
}
|
|
||||||
if cfg.Channels.QQ.Enabled {
|
|
||||||
enabled = append(enabled, "qq")
|
|
||||||
}
|
|
||||||
if cfg.Channels.DingTalk.Enabled {
|
|
||||||
enabled = append(enabled, "dingtalk")
|
|
||||||
}
|
|
||||||
if cfg.Channels.Slack.Enabled {
|
|
||||||
enabled = append(enabled, "slack")
|
|
||||||
}
|
|
||||||
if cfg.Channels.Matrix.Enabled {
|
|
||||||
enabled = append(enabled, "matrix")
|
|
||||||
}
|
|
||||||
if cfg.Channels.LINE.Enabled {
|
|
||||||
enabled = append(enabled, "line")
|
|
||||||
}
|
|
||||||
if cfg.Channels.OneBot.Enabled {
|
|
||||||
enabled = append(enabled, "onebot")
|
|
||||||
}
|
|
||||||
if cfg.Channels.WeCom.Enabled {
|
|
||||||
enabled = append(enabled, "wecom")
|
|
||||||
}
|
|
||||||
if cfg.Channels.Weixin.Enabled {
|
|
||||||
enabled = append(enabled, "weixin")
|
|
||||||
}
|
|
||||||
if cfg.Channels.Pico.Enabled {
|
|
||||||
enabled = append(enabled, "pico")
|
|
||||||
}
|
|
||||||
if cfg.Channels.PicoClient.Enabled {
|
|
||||||
enabled = append(enabled, "pico_client")
|
|
||||||
}
|
|
||||||
if cfg.Channels.IRC.Enabled {
|
|
||||||
enabled = append(enabled, "irc")
|
|
||||||
}
|
}
|
||||||
|
sort.Strings(enabled)
|
||||||
return 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 {
|
func (r *AgentRegistry) workspaceForAgentIDLocked(agentID string) string {
|
||||||
agent, ok := r.agents[routing.NormalizeAgentID(agentID)]
|
agent, ok := r.agents[routing.NormalizeAgentID(agentID)]
|
||||||
if !ok || agent == nil {
|
if !ok || agent == nil {
|
||||||
|
|
@ -331,7 +313,7 @@ func formatAgentDiscoverySection(currentAgentID string, agents []AgentDescriptor
|
||||||
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(
|
header.WriteString(
|
||||||
"Delegate based on available_tools first, then model, channels, and description. Use only agent IDs listed here.\n\n",
|
"Delegate based on available_tools first, then skills, mcpServers, model, channels, and description. Use only agent IDs listed here.\n\n",
|
||||||
)
|
)
|
||||||
header.WriteString("```json\n")
|
header.WriteString("```json\n")
|
||||||
header.Write(encoded)
|
header.Write(encoded)
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,10 @@ func TestAgentRegistry_ListAgentsBuildsStructuredDescriptors(t *testing.T) {
|
||||||
"AGENT.md": `---
|
"AGENT.md": `---
|
||||||
name: Main Frontmatter Name
|
name: Main Frontmatter Name
|
||||||
description: Structured main agent
|
description: Structured main agent
|
||||||
|
model: main-frontmatter-model
|
||||||
|
tools: [read_file, write_file]
|
||||||
|
skills: [coordination]
|
||||||
|
mcpServers: [filesystem]
|
||||||
---
|
---
|
||||||
# Agent
|
# Agent
|
||||||
|
|
||||||
|
|
@ -22,21 +26,24 @@ Handle general requests.
|
||||||
defer cleanupWorkspace(t, mainWorkspace)
|
defer cleanupWorkspace(t, mainWorkspace)
|
||||||
|
|
||||||
supportWorkspace := setupWorkspace(t, map[string]string{
|
supportWorkspace := setupWorkspace(t, map[string]string{
|
||||||
"AGENT.md": `# Agent
|
"AGENT.md": `---
|
||||||
|
name: Support Frontmatter Name
|
||||||
|
description: Support frontmatter description
|
||||||
|
model: support-frontmatter-model
|
||||||
|
tools: [read_file]
|
||||||
|
skills: [support-playbook]
|
||||||
|
mcpServers: [support-db]
|
||||||
|
---
|
||||||
|
# Agent
|
||||||
|
|
||||||
Handle support tickets carefully.
|
Handle support tickets carefully.
|
||||||
`,
|
`,
|
||||||
"SOUL.md": "# Soul\nStay calm and precise.",
|
|
||||||
})
|
})
|
||||||
defer cleanupWorkspace(t, supportWorkspace)
|
defer cleanupWorkspace(t, supportWorkspace)
|
||||||
|
|
||||||
cfg := testCfg([]config.AgentConfig{
|
cfg := testCfg([]config.AgentConfig{
|
||||||
{ID: "main", Default: true, Name: "Configured Main", Workspace: mainWorkspace},
|
{ID: "main", Default: true, Name: "Configured Main", Workspace: mainWorkspace},
|
||||||
{
|
{ID: "support", Workspace: supportWorkspace},
|
||||||
ID: "support",
|
|
||||||
Workspace: supportWorkspace,
|
|
||||||
Model: &config.AgentModelConfig{Primary: "support-model"},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
cfg.Tools.ReadFile.Enabled = true
|
cfg.Tools.ReadFile.Enabled = true
|
||||||
cfg.Tools.WriteFile.Enabled = true
|
cfg.Tools.WriteFile.Enabled = true
|
||||||
|
|
@ -61,14 +68,23 @@ Handle support tickets carefully.
|
||||||
if descriptors[0].ID != "main" {
|
if descriptors[0].ID != "main" {
|
||||||
t.Fatalf("expected current workspace agent first, got %q", descriptors[0].ID)
|
t.Fatalf("expected current workspace agent first, got %q", descriptors[0].ID)
|
||||||
}
|
}
|
||||||
if descriptors[0].Name != "Configured Main" {
|
if descriptors[0].Name != "Main Frontmatter Name" {
|
||||||
t.Fatalf("expected config name to win, got %q", descriptors[0].Name)
|
t.Fatalf("expected frontmatter name to drive discovery, got %q", descriptors[0].Name)
|
||||||
}
|
}
|
||||||
if descriptors[0].Description != "Structured main agent" {
|
if descriptors[0].Description != "Structured main agent" {
|
||||||
t.Fatalf("expected frontmatter description, got %q", descriptors[0].Description)
|
t.Fatalf("expected frontmatter description, got %q", descriptors[0].Description)
|
||||||
}
|
}
|
||||||
if descriptors[0].Model != "gpt-4" {
|
if descriptors[0].Model != "main-frontmatter-model" {
|
||||||
t.Fatalf("expected inherited model, got %q", descriptors[0].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") ||
|
if !slices.Contains(descriptors[0].AvailableTools, "read_file") ||
|
||||||
!slices.Contains(descriptors[0].AvailableTools, "write_file") {
|
!slices.Contains(descriptors[0].AvailableTools, "write_file") {
|
||||||
|
|
@ -85,11 +101,20 @@ Handle support tickets carefully.
|
||||||
if !ok || support == nil {
|
if !ok || support == nil {
|
||||||
t.Fatal("expected support descriptor lookup to succeed")
|
t.Fatal("expected support descriptor lookup to succeed")
|
||||||
}
|
}
|
||||||
if support.Description != "Handle support tickets carefully." {
|
if support.Name != "Support Frontmatter Name" {
|
||||||
t.Fatalf("expected AGENT body fallback description, got %q", support.Description)
|
t.Fatalf("expected support frontmatter name, got %q", support.Name)
|
||||||
}
|
}
|
||||||
if support.Model != "support-model" {
|
if support.Description != "Support frontmatter description" {
|
||||||
t.Fatalf("expected explicit support model, got %q", support.Model)
|
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"}) {
|
if !slices.Equal(support.Channels, []string{"telegram"}) {
|
||||||
t.Fatalf("expected support channel binding, got %v", support.Channels)
|
t.Fatalf("expected support channel binding, got %v", support.Channels)
|
||||||
|
|
@ -100,6 +125,7 @@ func TestContextBuilder_BuildMessagesIncludesAgentDiscoverySection(t *testing.T)
|
||||||
mainWorkspace := setupWorkspace(t, map[string]string{
|
mainWorkspace := setupWorkspace(t, map[string]string{
|
||||||
"AGENT.md": `---
|
"AGENT.md": `---
|
||||||
description: Main agent
|
description: Main agent
|
||||||
|
skills: [coordination]
|
||||||
---
|
---
|
||||||
# Agent
|
# Agent
|
||||||
|
|
||||||
|
|
@ -111,6 +137,8 @@ Generalist.
|
||||||
researchWorkspace := setupWorkspace(t, map[string]string{
|
researchWorkspace := setupWorkspace(t, map[string]string{
|
||||||
"AGENT.md": `---
|
"AGENT.md": `---
|
||||||
description: Research specialist
|
description: Research specialist
|
||||||
|
skills: [deep-research]
|
||||||
|
mcpServers: [web-index]
|
||||||
---
|
---
|
||||||
# Agent
|
# Agent
|
||||||
|
|
||||||
|
|
@ -162,6 +190,12 @@ Investigate deeply.
|
||||||
!strings.Contains(systemPrompt, `"write_file"`) {
|
!strings.Contains(systemPrompt, `"write_file"`) {
|
||||||
t.Fatalf("expected visible tool list in discovery section, got %q", systemPrompt)
|
t.Fatalf("expected visible tool list 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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestContextBuilder_BuildMessagesOmitsAgentDiscoverySectionForSingleton(t *testing.T) {
|
func TestContextBuilder_BuildMessagesOmitsAgentDiscoverySectionForSingleton(t *testing.T) {
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,9 @@ func NewAgentInstance(
|
||||||
workspace := resolveAgentWorkspace(agentCfg, defaults)
|
workspace := resolveAgentWorkspace(agentCfg, defaults)
|
||||||
os.MkdirAll(workspace, 0o755)
|
os.MkdirAll(workspace, 0o755)
|
||||||
|
|
||||||
model := resolveAgentModel(agentCfg, defaults)
|
definition := loadAgentDefinition(workspace)
|
||||||
|
|
||||||
|
model := resolveAgentModel(agentCfg, defaults, definition)
|
||||||
fallbacks := resolveAgentFallbacks(agentCfg, defaults)
|
fallbacks := resolveAgentFallbacks(agentCfg, defaults)
|
||||||
|
|
||||||
restrict := defaults.RestrictToWorkspace
|
restrict := defaults.RestrictToWorkspace
|
||||||
|
|
@ -72,7 +74,7 @@ func NewAgentInstance(
|
||||||
// Compile path whitelist patterns from config.
|
// Compile path whitelist patterns from config.
|
||||||
allowReadPaths := buildAllowReadPatterns(cfg)
|
allowReadPaths := buildAllowReadPatterns(cfg)
|
||||||
allowWritePaths := compilePatterns(cfg.Tools.AllowWritePaths)
|
allowWritePaths := compilePatterns(cfg.Tools.AllowWritePaths)
|
||||||
agentToolAllowlist := resolveAgentToolAllowlist(agentCfg)
|
agentToolAllowlist := resolveAgentToolAllowlist(definition)
|
||||||
|
|
||||||
toolsRegistry := tools.NewToolRegistry()
|
toolsRegistry := tools.NewToolRegistry()
|
||||||
toolsRegistry.SetAllowlist(agentToolAllowlist)
|
toolsRegistry.SetAllowlist(agentToolAllowlist)
|
||||||
|
|
@ -125,8 +127,11 @@ func NewAgentInstance(
|
||||||
if agentCfg != nil {
|
if agentCfg != nil {
|
||||||
agentID = routing.NormalizeAgentID(agentCfg.ID)
|
agentID = routing.NormalizeAgentID(agentCfg.ID)
|
||||||
agentName = agentCfg.Name
|
agentName = agentCfg.Name
|
||||||
|
if definition.Agent != nil && strings.TrimSpace(definition.Agent.Frontmatter.Name) != "" {
|
||||||
|
agentName = strings.TrimSpace(definition.Agent.Frontmatter.Name)
|
||||||
|
}
|
||||||
subagents = agentCfg.Subagents
|
subagents = agentCfg.Subagents
|
||||||
skillsFilter = agentCfg.Skills
|
skillsFilter = resolveAgentSkillsFilter(agentCfg, definition)
|
||||||
}
|
}
|
||||||
|
|
||||||
maxIter := defaults.MaxToolIterations
|
maxIter := defaults.MaxToolIterations
|
||||||
|
|
@ -255,7 +260,14 @@ func resolveAgentWorkspace(agentCfg *config.AgentConfig, defaults *config.AgentD
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolveAgentModel resolves the primary model for an agent.
|
// resolveAgentModel resolves the primary model for an agent.
|
||||||
func resolveAgentModel(agentCfg *config.AgentConfig, defaults *config.AgentDefaults) string {
|
func resolveAgentModel(
|
||||||
|
agentCfg *config.AgentConfig,
|
||||||
|
defaults *config.AgentDefaults,
|
||||||
|
definition AgentContextDefinition,
|
||||||
|
) string {
|
||||||
|
if definition.Agent != nil && strings.TrimSpace(definition.Agent.Frontmatter.Model) != "" {
|
||||||
|
return strings.TrimSpace(definition.Agent.Frontmatter.Model)
|
||||||
|
}
|
||||||
if agentCfg != nil && agentCfg.Model != nil && strings.TrimSpace(agentCfg.Model.Primary) != "" {
|
if agentCfg != nil && agentCfg.Model != nil && strings.TrimSpace(agentCfg.Model.Primary) != "" {
|
||||||
return strings.TrimSpace(agentCfg.Model.Primary)
|
return strings.TrimSpace(agentCfg.Model.Primary)
|
||||||
}
|
}
|
||||||
|
|
@ -270,6 +282,19 @@ func resolveAgentFallbacks(agentCfg *config.AgentConfig, defaults *config.AgentD
|
||||||
return defaults.ModelFallbacks
|
return defaults.ModelFallbacks
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resolveAgentSkillsFilter(
|
||||||
|
agentCfg *config.AgentConfig,
|
||||||
|
definition AgentContextDefinition,
|
||||||
|
) []string {
|
||||||
|
if definition.Agent != nil && definition.Agent.Frontmatter.Skills != nil {
|
||||||
|
return append([]string(nil), definition.Agent.Frontmatter.Skills...)
|
||||||
|
}
|
||||||
|
if agentCfg == nil || agentCfg.Skills == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return append([]string(nil), agentCfg.Skills...)
|
||||||
|
}
|
||||||
|
|
||||||
func compilePatterns(patterns []string) []*regexp.Regexp {
|
func compilePatterns(patterns []string) []*regexp.Regexp {
|
||||||
compiled := make([]*regexp.Regexp, 0, len(patterns))
|
compiled := make([]*regexp.Regexp, 0, len(patterns))
|
||||||
for _, p := range patterns {
|
for _, p := range patterns {
|
||||||
|
|
|
||||||
|
|
@ -281,3 +281,42 @@ func TestNewAgentInstance_InvalidExecConfigDoesNotExit(t *testing.T) {
|
||||||
t.Fatal("read_file tool should still be registered")
|
t.Fatal("read_file tool should still be registered")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNewAgentInstance_UsesFrontmatterModelAndSkills(t *testing.T) {
|
||||||
|
workspace := setupWorkspace(t, map[string]string{
|
||||||
|
"AGENT.md": `---
|
||||||
|
model: frontmatter-model
|
||||||
|
skills: [frontmatter-skill]
|
||||||
|
---
|
||||||
|
# Agent
|
||||||
|
|
||||||
|
Use frontmatter identity.
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
defer cleanupWorkspace(t, workspace)
|
||||||
|
|
||||||
|
cfg := &config.Config{
|
||||||
|
Agents: config.AgentsConfig{
|
||||||
|
Defaults: config.AgentDefaults{
|
||||||
|
Workspace: workspace,
|
||||||
|
ModelName: "default-model",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
agent := NewAgentInstance(&config.AgentConfig{
|
||||||
|
ID: "research",
|
||||||
|
Workspace: workspace,
|
||||||
|
Model: &config.AgentModelConfig{
|
||||||
|
Primary: "config-model",
|
||||||
|
},
|
||||||
|
Skills: []string{"config-skill"},
|
||||||
|
}, &cfg.Agents.Defaults, cfg, &mockProvider{})
|
||||||
|
|
||||||
|
if agent.Model != "frontmatter-model" {
|
||||||
|
t.Fatalf("agent.Model = %q, want frontmatter-model", agent.Model)
|
||||||
|
}
|
||||||
|
if len(agent.SkillsFilter) != 1 || agent.SkillsFilter[0] != "frontmatter-skill" {
|
||||||
|
t.Fatalf("agent.SkillsFilter = %v, want [frontmatter-skill]", agent.SkillsFilter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -211,13 +211,31 @@ func TestAgentInstance_FallbackExplicitEmpty(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewAgentLoop_AgentToolAllowlistFiltersRuntimeTools(t *testing.T) {
|
func TestNewAgentLoop_AgentToolAllowlistFiltersRuntimeTools(t *testing.T) {
|
||||||
|
mainWorkspace := setupWorkspace(t, map[string]string{
|
||||||
|
"AGENT.md": "# Agent\nMain agent.\n",
|
||||||
|
})
|
||||||
|
defer cleanupWorkspace(t, mainWorkspace)
|
||||||
|
|
||||||
|
researchWorkspace := setupWorkspace(t, map[string]string{
|
||||||
|
"AGENT.md": `---
|
||||||
|
tools: [read_file, write_file, web_search, web_fetch, message]
|
||||||
|
skills: [deep-research]
|
||||||
|
---
|
||||||
|
# Agent
|
||||||
|
|
||||||
|
Research agent.
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
defer cleanupWorkspace(t, researchWorkspace)
|
||||||
|
|
||||||
cfg := testCfg([]config.AgentConfig{
|
cfg := testCfg([]config.AgentConfig{
|
||||||
{ID: "main", Default: true},
|
{ID: "main", Default: true, Workspace: mainWorkspace},
|
||||||
{
|
{
|
||||||
ID: "research",
|
ID: "research",
|
||||||
Tools: []string{"read_file", "write_file", "web_search", "web_fetch", "message"},
|
Workspace: researchWorkspace,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
cfg.Agents.Defaults.Workspace = mainWorkspace
|
||||||
cfg.Tools.ReadFile.Enabled = true
|
cfg.Tools.ReadFile.Enabled = true
|
||||||
cfg.Tools.WriteFile.Enabled = true
|
cfg.Tools.WriteFile.Enabled = true
|
||||||
cfg.Tools.ListDir.Enabled = true
|
cfg.Tools.ListDir.Enabled = true
|
||||||
|
|
@ -251,13 +269,30 @@ func TestNewAgentLoop_AgentToolAllowlistFiltersRuntimeTools(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewAgentLoop_AgentToolAllowlistRequiresExactRuntimeToolNames(t *testing.T) {
|
func TestNewAgentLoop_AgentToolAllowlistRequiresExactRuntimeToolNames(t *testing.T) {
|
||||||
|
mainWorkspace := setupWorkspace(t, map[string]string{
|
||||||
|
"AGENT.md": "# Agent\nMain agent.\n",
|
||||||
|
})
|
||||||
|
defer cleanupWorkspace(t, mainWorkspace)
|
||||||
|
|
||||||
|
researchWorkspace := setupWorkspace(t, map[string]string{
|
||||||
|
"AGENT.md": `---
|
||||||
|
tools: [web]
|
||||||
|
---
|
||||||
|
# Agent
|
||||||
|
|
||||||
|
Research agent.
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
defer cleanupWorkspace(t, researchWorkspace)
|
||||||
|
|
||||||
cfg := testCfg([]config.AgentConfig{
|
cfg := testCfg([]config.AgentConfig{
|
||||||
{ID: "main", Default: true},
|
{ID: "main", Default: true, Workspace: mainWorkspace},
|
||||||
{
|
{
|
||||||
ID: "research",
|
ID: "research",
|
||||||
Tools: []string{"web"},
|
Workspace: researchWorkspace,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
cfg.Agents.Defaults.Workspace = mainWorkspace
|
||||||
cfg.Tools.Web.Enabled = true
|
cfg.Tools.Web.Enabled = true
|
||||||
cfg.Tools.Web.DuckDuckGo.Enabled = true
|
cfg.Tools.Web.DuckDuckGo.Enabled = true
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,17 +3,15 @@ package agent
|
||||||
import (
|
import (
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/sipeed/picoclaw/pkg/config"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func resolveAgentToolAllowlist(agentCfg *config.AgentConfig) []string {
|
func resolveAgentToolAllowlist(definition AgentContextDefinition) []string {
|
||||||
if agentCfg == nil || agentCfg.Tools == nil {
|
if definition.Agent == nil || definition.Agent.Frontmatter.Tools == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
allowlist := make(map[string]struct{}, len(agentCfg.Tools))
|
allowlist := make(map[string]struct{}, len(definition.Agent.Frontmatter.Tools))
|
||||||
for _, raw := range agentCfg.Tools {
|
for _, raw := range definition.Agent.Frontmatter.Tools {
|
||||||
trimmed := strings.ToLower(strings.TrimSpace(raw))
|
trimmed := strings.ToLower(strings.TrimSpace(raw))
|
||||||
if trimmed == "" {
|
if trimmed == "" {
|
||||||
continue
|
continue
|
||||||
|
|
|
||||||
|
|
@ -248,7 +248,6 @@ type AgentConfig struct {
|
||||||
Name string `json:"name,omitempty"`
|
Name string `json:"name,omitempty"`
|
||||||
Workspace string `json:"workspace,omitempty"`
|
Workspace string `json:"workspace,omitempty"`
|
||||||
Model *AgentModelConfig `json:"model,omitempty"`
|
Model *AgentModelConfig `json:"model,omitempty"`
|
||||||
Tools []string `json:"tools,omitempty"`
|
|
||||||
Skills []string `json:"skills,omitempty"`
|
Skills []string `json:"skills,omitempty"`
|
||||||
Subagents *SubagentsConfig `json:"subagents,omitempty"`
|
Subagents *SubagentsConfig `json:"subagents,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -120,7 +120,6 @@ func TestAgentConfig_FullParse(t *testing.T) {
|
||||||
"primary": "claude-opus",
|
"primary": "claude-opus",
|
||||||
"fallbacks": ["haiku"]
|
"fallbacks": ["haiku"]
|
||||||
},
|
},
|
||||||
"tools": ["read_file", "web_search"],
|
|
||||||
"subagents": {
|
"subagents": {
|
||||||
"allow_agents": ["sales"]
|
"allow_agents": ["sales"]
|
||||||
}
|
}
|
||||||
|
|
@ -172,10 +171,6 @@ func TestAgentConfig_FullParse(t *testing.T) {
|
||||||
if len(support.Model.Fallbacks) != 1 || support.Model.Fallbacks[0] != "haiku" {
|
if len(support.Model.Fallbacks) != 1 || support.Model.Fallbacks[0] != "haiku" {
|
||||||
t.Errorf("support.Model.Fallbacks = %v", support.Model.Fallbacks)
|
t.Errorf("support.Model.Fallbacks = %v", support.Model.Fallbacks)
|
||||||
}
|
}
|
||||||
if len(support.Tools) != 2 || support.Tools[0] != "read_file" ||
|
|
||||||
support.Tools[1] != "web_search" {
|
|
||||||
t.Errorf("support.Tools = %v", support.Tools)
|
|
||||||
}
|
|
||||||
if support.Subagents == nil || len(support.Subagents.AllowAgents) != 1 {
|
if support.Subagents == nil || len(support.Subagents.AllowAgents) != 1 {
|
||||||
t.Errorf("support.Subagents = %+v", support.Subagents)
|
t.Errorf("support.Subagents = %+v", support.Subagents)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ func (r *ToolRegistry) SetAllowlist(names []string) {
|
||||||
|
|
||||||
allowlist := make(map[string]struct{}, len(names))
|
allowlist := make(map[string]struct{}, len(names))
|
||||||
for _, name := range names {
|
for _, name := range names {
|
||||||
trimmed := strings.TrimSpace(name)
|
trimmed := strings.ToLower(strings.TrimSpace(name))
|
||||||
if trimmed == "" {
|
if trimmed == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
@ -172,7 +172,7 @@ func (r *ToolRegistry) toolAllowedLocked(name string) bool {
|
||||||
if r.allowlist == nil {
|
if r.allowlist == nil {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
_, ok := r.allowlist[name]
|
_, ok := r.allowlist[strings.ToLower(strings.TrimSpace(name))]
|
||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,7 @@ func TestToolRegistry_RegisterAndGet(t *testing.T) {
|
||||||
|
|
||||||
func TestToolRegistry_AllowlistFiltersRegistrations(t *testing.T) {
|
func TestToolRegistry_AllowlistFiltersRegistrations(t *testing.T) {
|
||||||
r := NewToolRegistry()
|
r := NewToolRegistry()
|
||||||
r.SetAllowlist([]string{"allowed_tool"})
|
r.SetAllowlist([]string{"Allowed_Tool"})
|
||||||
|
|
||||||
r.Register(newMockTool("allowed_tool", "allowed"))
|
r.Register(newMockTool("allowed_tool", "allowed"))
|
||||||
r.Register(newMockTool("blocked_tool", "blocked"))
|
r.Register(newMockTool("blocked_tool", "blocked"))
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue