mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-26 20:45:37 +02:00
fix: hide disabled model-invocation skills from briefs (#5311)
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
@@ -668,10 +668,11 @@ func renderIssueContext(provider string, ctx TaskContextForEnv) string {
|
||||
b.WriteString("## Quick Start\n\n")
|
||||
fmt.Fprintf(&b, "Run `multica issue get %s --output json` to fetch the full issue details.\n\n", ctx.IssueID)
|
||||
|
||||
if len(ctx.AgentSkills) > 0 {
|
||||
skills := modelVisibleSkills(ctx.AgentSkills)
|
||||
if len(skills) > 0 {
|
||||
b.WriteString("## Agent Skills\n\n")
|
||||
b.WriteString("The following skills are available to you:\n\n")
|
||||
for _, skill := range ctx.AgentSkills {
|
||||
for _, skill := range skills {
|
||||
fmt.Fprintf(&b, "- **%s**\n", skill.Name)
|
||||
}
|
||||
b.WriteString("\n")
|
||||
@@ -692,9 +693,10 @@ func renderQuickCreateContext(ctx TaskContextForEnv) string {
|
||||
b.WriteString("> ")
|
||||
b.WriteString(ctx.QuickCreatePrompt)
|
||||
b.WriteString("\n\n")
|
||||
if len(ctx.AgentSkills) > 0 {
|
||||
skills := modelVisibleSkills(ctx.AgentSkills)
|
||||
if len(skills) > 0 {
|
||||
b.WriteString("## Agent Skills\n\n")
|
||||
for _, skill := range ctx.AgentSkills {
|
||||
for _, skill := range skills {
|
||||
fmt.Fprintf(&b, "- **%s**\n", skill.Name)
|
||||
}
|
||||
b.WriteString("\n")
|
||||
@@ -731,10 +733,11 @@ func renderAutopilotContext(ctx TaskContextForEnv) string {
|
||||
b.WriteString("\n\n")
|
||||
}
|
||||
|
||||
if len(ctx.AgentSkills) > 0 {
|
||||
skills := modelVisibleSkills(ctx.AgentSkills)
|
||||
if len(skills) > 0 {
|
||||
b.WriteString("## Agent Skills\n\n")
|
||||
b.WriteString("The following skills are available to you:\n\n")
|
||||
for _, skill := range ctx.AgentSkills {
|
||||
for _, skill := range skills {
|
||||
fmt.Fprintf(&b, "- **%s**\n", skill.Name)
|
||||
}
|
||||
b.WriteString("\n")
|
||||
|
||||
@@ -421,7 +421,8 @@ func writeSubIssueCreation(b *strings.Builder) {
|
||||
|
||||
// writeSkills emits the Skills section listing skill names + descriptions.
|
||||
func writeSkills(b *strings.Builder, provider string, ctx TaskContextForEnv) {
|
||||
if len(ctx.AgentSkills) == 0 {
|
||||
skills := modelVisibleSkills(ctx.AgentSkills)
|
||||
if len(skills) == 0 {
|
||||
return
|
||||
}
|
||||
b.WriteString("## Skills\n\n")
|
||||
@@ -435,7 +436,7 @@ func writeSkills(b *strings.Builder, provider string, ctx TaskContextForEnv) {
|
||||
default:
|
||||
b.WriteString("Detailed skill instructions are in `.agent_context/skills/`. Each subdirectory contains a `SKILL.md`.\n\n")
|
||||
}
|
||||
for _, skill := range ctx.AgentSkills {
|
||||
for _, skill := range skills {
|
||||
if desc := strings.TrimSpace(skill.Description); desc != "" {
|
||||
fmt.Fprintf(b, "- **%s** — %s\n", skill.Name, desc)
|
||||
} else {
|
||||
|
||||
47
server/internal/daemon/execenv/skill_visibility.go
Normal file
47
server/internal/daemon/execenv/skill_visibility.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package execenv
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func modelVisibleSkills(skills []SkillContextForEnv) []SkillContextForEnv {
|
||||
if len(skills) == 0 {
|
||||
return nil
|
||||
}
|
||||
visible := make([]SkillContextForEnv, 0, len(skills))
|
||||
for _, skill := range skills {
|
||||
if skillModelInvocationVisible(skill) {
|
||||
visible = append(visible, skill)
|
||||
}
|
||||
}
|
||||
return visible
|
||||
}
|
||||
|
||||
func skillModelInvocationVisible(skill SkillContextForEnv) bool {
|
||||
return !skillDisablesModelInvocation(skill.Content)
|
||||
}
|
||||
|
||||
func skillDisablesModelInvocation(content string) bool {
|
||||
fmBody, _, ok := frontmatterParts(content)
|
||||
if !ok || strings.TrimSpace(fmBody) == "" {
|
||||
return false
|
||||
}
|
||||
var data map[string]any
|
||||
if err := yaml.Unmarshal([]byte(fmBody), &data); err != nil {
|
||||
return false
|
||||
}
|
||||
value, ok := data["disable-model-invocation"]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
switch v := value.(type) {
|
||||
case bool:
|
||||
return v
|
||||
case string:
|
||||
return strings.EqualFold(strings.TrimSpace(v), "true")
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
226
server/internal/daemon/execenv/skill_visibility_test.go
Normal file
226
server/internal/daemon/execenv/skill_visibility_test.go
Normal file
@@ -0,0 +1,226 @@
|
||||
package execenv
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSkillModelInvocationVisibility(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
content string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "no frontmatter is visible",
|
||||
content: "body",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "disable true is hidden",
|
||||
content: `---
|
||||
name: hidden
|
||||
disable-model-invocation: true
|
||||
---
|
||||
|
||||
body`,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "disable quoted true is hidden",
|
||||
content: `---
|
||||
name: hidden
|
||||
disable-model-invocation: "true"
|
||||
---
|
||||
|
||||
body`,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "disable false is visible",
|
||||
content: `---
|
||||
name: visible
|
||||
disable-model-invocation: false
|
||||
---
|
||||
|
||||
body`,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "user invocable false is still visible",
|
||||
content: `---
|
||||
name: visible
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
body`,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "invalid frontmatter is visible",
|
||||
content: `---
|
||||
name: visible
|
||||
description: bad: value
|
||||
disable-model-invocation: true
|
||||
---
|
||||
|
||||
body`,
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
got := skillModelInvocationVisible(SkillContextForEnv{Content: tc.content})
|
||||
if got != tc.want {
|
||||
t.Errorf("skillModelInvocationVisible = %v, want %v", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderedSkillListsHideDisableModelInvocationSkills(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := TaskContextForEnv{
|
||||
IssueID: "issue-1",
|
||||
QuickCreatePrompt: "create something",
|
||||
AutopilotRunID: "run-1",
|
||||
AgentName: "Eve",
|
||||
AgentID: "eve-1",
|
||||
AgentSkills: []SkillContextForEnv{
|
||||
{
|
||||
Name: "Visible Skill",
|
||||
Description: "visible description",
|
||||
Content: `---
|
||||
name: visible-skill
|
||||
---
|
||||
|
||||
Visible body.`,
|
||||
},
|
||||
{
|
||||
Name: "Hidden Skill",
|
||||
Description: "hidden description",
|
||||
Content: `---
|
||||
name: hidden-skill
|
||||
disable-model-invocation: true
|
||||
---
|
||||
|
||||
Hidden body.`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
rendered := map[string]string{
|
||||
"runtime config": buildMetaSkillContent("codex", TaskContextForEnv{
|
||||
IssueID: ctx.IssueID,
|
||||
AgentName: ctx.AgentName,
|
||||
AgentID: ctx.AgentID,
|
||||
AgentSkills: ctx.AgentSkills,
|
||||
}),
|
||||
"issue context": renderIssueContext("codex", TaskContextForEnv{
|
||||
IssueID: ctx.IssueID,
|
||||
AgentSkills: ctx.AgentSkills,
|
||||
}),
|
||||
"quick create": renderQuickCreateContext(TaskContextForEnv{
|
||||
QuickCreatePrompt: ctx.QuickCreatePrompt,
|
||||
AgentSkills: ctx.AgentSkills,
|
||||
}),
|
||||
"autopilot": renderAutopilotContext(TaskContextForEnv{
|
||||
AutopilotRunID: ctx.AutopilotRunID,
|
||||
AgentSkills: ctx.AgentSkills,
|
||||
}),
|
||||
}
|
||||
|
||||
for name, out := range rendered {
|
||||
if !strings.Contains(out, "Visible Skill") {
|
||||
t.Errorf("%s missing visible skill:\n%s", name, out)
|
||||
}
|
||||
if strings.Contains(out, "Hidden Skill") || strings.Contains(out, "hidden description") {
|
||||
t.Errorf("%s advertised disable-model-invocation skill:\n%s", name, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderedSkillListsOmitSectionWhenOnlyDisabledSkills(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := TaskContextForEnv{
|
||||
IssueID: "issue-1",
|
||||
AgentSkills: []SkillContextForEnv{
|
||||
{
|
||||
Name: "Hidden Skill",
|
||||
Content: `---
|
||||
name: hidden-skill
|
||||
disable-model-invocation: true
|
||||
---
|
||||
|
||||
Hidden body.`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
runtimeConfig := buildMetaSkillContent("codex", ctx)
|
||||
if strings.Contains(runtimeConfig, "## Skills") {
|
||||
t.Errorf("runtime config should omit Skills section when every skill disables model invocation:\n%s", runtimeConfig)
|
||||
}
|
||||
|
||||
issueContext := renderIssueContext("codex", ctx)
|
||||
if strings.Contains(issueContext, "## Agent Skills") {
|
||||
t.Errorf("issue context should omit Agent Skills section when every skill disables model invocation:\n%s", issueContext)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteContextFilesHydratesDisableModelInvocationSkillButDoesNotAdvertiseIt(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dir := t.TempDir()
|
||||
ctx := TaskContextForEnv{
|
||||
IssueID: "issue-1",
|
||||
AgentSkills: []SkillContextForEnv{
|
||||
{
|
||||
Name: "Visible Skill",
|
||||
Content: `---
|
||||
name: visible-skill
|
||||
---
|
||||
|
||||
Visible body.`,
|
||||
},
|
||||
{
|
||||
Name: "Hidden Skill",
|
||||
Content: `---
|
||||
name: hidden-skill
|
||||
disable-model-invocation: true
|
||||
---
|
||||
|
||||
Hidden body.`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := writeContextFiles(dir, "", ctx, nil); err != nil {
|
||||
t.Fatalf("writeContextFiles failed: %v", err)
|
||||
}
|
||||
|
||||
issueContext, err := os.ReadFile(filepath.Join(dir, ".agent_context", "issue_context.md"))
|
||||
if err != nil {
|
||||
t.Fatalf("read issue_context.md: %v", err)
|
||||
}
|
||||
if strings.Contains(string(issueContext), "Hidden Skill") {
|
||||
t.Fatalf("issue_context.md advertised hidden skill:\n%s", string(issueContext))
|
||||
}
|
||||
|
||||
hiddenSkill, err := os.ReadFile(filepath.Join(dir, ".agent_context", "skills", "hidden-skill", "SKILL.md"))
|
||||
if err != nil {
|
||||
t.Fatalf("disabled skill should still be hydrated on disk: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(hiddenSkill), "disable-model-invocation: true") {
|
||||
t.Errorf("hidden skill frontmatter should be preserved on disk:\n%s", string(hiddenSkill))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user