mirror of
https://github.com/ollama/ollama.git
synced 2026-07-23 09:10:53 -05:00
feat: reserve built-in agent slash commands
This commit is contained in:
@@ -325,6 +325,31 @@ func (c *SkillCatalog) Diagnostics() []error {
|
|||||||
return append([]error(nil), c.diagnostics...)
|
return append([]error(nil), c.diagnostics...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExcludeNames removes skills whose names are reserved by a caller. It returns
|
||||||
|
// the excluded names in sorted order.
|
||||||
|
func (c *SkillCatalog) ExcludeNames(names []string) []string {
|
||||||
|
if c == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
reserved := make(map[string]struct{}, len(names))
|
||||||
|
for _, name := range names {
|
||||||
|
name = strings.TrimPrefix(strings.ToLower(strings.TrimSpace(name)), "/")
|
||||||
|
if name != "" {
|
||||||
|
reserved[name] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var excluded []string
|
||||||
|
for name := range c.skills {
|
||||||
|
if _, ok := reserved[name]; !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
delete(c.skills, name)
|
||||||
|
excluded = append(excluded, name)
|
||||||
|
}
|
||||||
|
sort.Strings(excluded)
|
||||||
|
return excluded
|
||||||
|
}
|
||||||
|
|
||||||
func (c *SkillCatalog) Load(name string) (Skill, error) {
|
func (c *SkillCatalog) Load(name string) (Skill, error) {
|
||||||
name = strings.TrimSpace(name)
|
name = strings.TrimSpace(name)
|
||||||
if !skillName.MatchString(name) {
|
if !skillName.MatchString(name) {
|
||||||
|
|||||||
@@ -257,6 +257,30 @@ func TestLoadDefaultSkillsPrecedenceAndCollisions(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSkillCatalogExcludeNames(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
for _, name := range []string{"release-notes", "system", "exit"} {
|
||||||
|
writeCatalogSkill(t, dir, name, "instructions")
|
||||||
|
}
|
||||||
|
catalog, err := DiscoverSkills(dir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got, want := strings.Join(catalog.ExcludeNames([]string{"/system", "EXIT"}), ","), "exit,system"; got != want {
|
||||||
|
t.Fatalf("excluded skills = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
if _, err := catalog.Load("system"); err == nil {
|
||||||
|
t.Fatal("excluded system skill should not load")
|
||||||
|
}
|
||||||
|
if _, err := catalog.Load("exit"); err == nil {
|
||||||
|
t.Fatal("excluded exit skill should not load")
|
||||||
|
}
|
||||||
|
if _, err := catalog.Load("release-notes"); err != nil {
|
||||||
|
t.Fatalf("non-conflicting skill should remain available: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSkillContentListsDirectoryAndResources(t *testing.T) {
|
func TestSkillContentListsDirectoryAndResources(t *testing.T) {
|
||||||
root := t.TempDir()
|
root := t.TempDir()
|
||||||
skillDir := filepath.Join(root, "pdf-processing")
|
skillDir := filepath.Join(root, "pdf-processing")
|
||||||
|
|||||||
@@ -87,6 +87,9 @@ func GenerateAgentTUI(cmd *cobra.Command, client *api.Client, opts agentTUIOptio
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("load agent skills: %w", err)
|
return fmt.Errorf("load agent skills: %w", err)
|
||||||
}
|
}
|
||||||
|
if ignored := skillCatalog.ExcludeNames(agentchat.BuiltinSlashCommandNames()); len(ignored) > 0 {
|
||||||
|
fmt.Fprintf(os.Stderr, "\033[1mwarning:\033[0m ignoring agent skill(s): %s\n", strings.Join(ignored, ", "))
|
||||||
|
}
|
||||||
for _, diagnostic := range skillCatalog.Diagnostics() {
|
for _, diagnostic := range skillCatalog.Diagnostics() {
|
||||||
fmt.Fprintf(os.Stderr, "\033[1mwarning:\033[0m ignored invalid agent skill: %v\n", diagnostic)
|
fmt.Fprintf(os.Stderr, "\033[1mwarning:\033[0m ignored invalid agent skill: %v\n", diagnostic)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,6 +95,39 @@ func TestAgentSkillSystemContextRequiresAvailableEnabledSkillTool(t *testing.T)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAgentSkillCommandCollisionsAreIgnored(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
for _, name := range []string{"release-notes", "system", "exit"} {
|
||||||
|
if err := os.Mkdir(filepath.Join(dir, name), 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
content := "---\nname: " + name + "\ndescription: Test skill.\n---\nInstructions."
|
||||||
|
if err := os.WriteFile(filepath.Join(dir, name, "SKILL.md"), []byte(content), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catalog, err := coreagent.DiscoverSkills(dir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ignored := catalog.ExcludeNames(agentchat.BuiltinSlashCommandNames())
|
||||||
|
if got, want := strings.Join(ignored, ", "), "exit, system"; got != want {
|
||||||
|
t.Fatalf("ignored skills = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
if _, err := catalog.Load("release-notes"); err != nil {
|
||||||
|
t.Fatalf("non-conflicting skill should remain available: %v", err)
|
||||||
|
}
|
||||||
|
if context := catalog.SystemContext(); !strings.Contains(context, "release-notes: Test skill.") || strings.Contains(context, "system: Test skill.") || strings.Contains(context, "exit: Test skill.") {
|
||||||
|
t.Fatalf("skill context = %q", context)
|
||||||
|
}
|
||||||
|
for _, name := range []string{"system", "exit"} {
|
||||||
|
if _, err := catalog.Load(name); err == nil {
|
||||||
|
t.Fatalf("conflicting skill %q should be ignored", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestAgentSelectionItemsUseLaunchSections(t *testing.T) {
|
func TestAgentSelectionItemsUseLaunchSections(t *testing.T) {
|
||||||
items := agentSelectionItems([]agentchat.ModelOption{
|
items := agentSelectionItems([]agentchat.ModelOption{
|
||||||
{Name: "glm-5.2:cloud", Description: "cloud", Recommended: true, Cloud: true},
|
{Name: "glm-5.2:cloud", Description: "cloud", Recommended: true, Cloud: true},
|
||||||
|
|||||||
@@ -64,6 +64,24 @@ var chatSlashCommands = []chatSlashCommand{
|
|||||||
{name: "/save", usage: "/save <filename>", description: "save request JSON; saved as <filename>.json"},
|
{name: "/save", usage: "/save <filename>", description: "save request JSON; saved as <filename>.json"},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BuiltinSlashCommandNames returns the names reserved by built-in slash
|
||||||
|
// commands, including aliases.
|
||||||
|
func BuiltinSlashCommandNames() []string {
|
||||||
|
names := make(map[string]struct{})
|
||||||
|
for _, command := range chatSlashCommands {
|
||||||
|
names[strings.TrimPrefix(command.name, "/")] = struct{}{}
|
||||||
|
for _, alias := range command.aliases {
|
||||||
|
names[strings.TrimPrefix(alias, "/")] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reserved := make([]string, 0, len(names))
|
||||||
|
for name := range names {
|
||||||
|
reserved = append(reserved, name)
|
||||||
|
}
|
||||||
|
sort.Strings(reserved)
|
||||||
|
return reserved
|
||||||
|
}
|
||||||
|
|
||||||
func (m *chatModel) handleSubmit() (tea.Model, tea.Cmd) {
|
func (m *chatModel) handleSubmit() (tea.Model, tea.Cmd) {
|
||||||
m.syncInputPlaceholders()
|
m.syncInputPlaceholders()
|
||||||
input := strings.TrimSpace(string(m.input))
|
input := strings.TrimSpace(string(m.input))
|
||||||
|
|||||||
Reference in New Issue
Block a user