fix(agent): preserve multiple armed /use skills
This commit is contained in:
parent
ee29aaa871
commit
dd925c764b
2 changed files with 99 additions and 11 deletions
|
|
@ -3405,12 +3405,18 @@ func (al *AgentLoop) applyExplicitSkillCommand(
|
|||
if opts == nil || strings.TrimSpace(opts.SessionKey) == "" {
|
||||
return true, true, commandsUnavailableSkillMessage()
|
||||
}
|
||||
al.setPendingSkills(opts.SessionKey, []string{skillName})
|
||||
armedSkills := al.setPendingSkills(opts.SessionKey, []string{skillName})
|
||||
if len(armedSkills) == 1 {
|
||||
return true, true, fmt.Sprintf(
|
||||
"Skill %q is armed for your next message. Send your next prompt normally, or use /use clear to cancel.",
|
||||
skillName,
|
||||
)
|
||||
}
|
||||
return true, true, fmt.Sprintf(
|
||||
"Skills %s are armed for your next message. Send your next prompt normally, or use /use clear to cancel.",
|
||||
quoteSkillNames(armedSkills),
|
||||
)
|
||||
}
|
||||
|
||||
message := strings.TrimSpace(strings.Join(parts[2:], " "))
|
||||
if message == "" {
|
||||
|
|
@ -3540,24 +3546,27 @@ func buildUseCommandHelp(agent *AgentInstance) string {
|
|||
)
|
||||
}
|
||||
|
||||
func (al *AgentLoop) setPendingSkills(sessionKey string, skillNames []string) {
|
||||
func (al *AgentLoop) setPendingSkills(sessionKey string, skillNames []string) []string {
|
||||
sessionKey = strings.TrimSpace(sessionKey)
|
||||
if sessionKey == "" || len(skillNames) == 0 {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
filtered := make([]string, 0, len(skillNames))
|
||||
for _, name := range skillNames {
|
||||
name = strings.TrimSpace(name)
|
||||
if name != "" {
|
||||
filtered = append(filtered, name)
|
||||
merged := make([]string, 0, len(skillNames))
|
||||
if existingValue, ok := al.pendingSkills.Load(sessionKey); ok {
|
||||
if existingSkills, ok := existingValue.([]string); ok {
|
||||
merged = append(merged, existingSkills...)
|
||||
}
|
||||
}
|
||||
merged = append(merged, skillNames...)
|
||||
|
||||
filtered := uniqueSkillNames(merged)
|
||||
if len(filtered) == 0 {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
al.pendingSkills.Store(sessionKey, filtered)
|
||||
return append([]string(nil), filtered...)
|
||||
}
|
||||
|
||||
func (al *AgentLoop) takePendingSkills(sessionKey string) []string {
|
||||
|
|
@ -3587,6 +3596,31 @@ func (al *AgentLoop) clearPendingSkills(sessionKey string) {
|
|||
al.pendingSkills.Delete(sessionKey)
|
||||
}
|
||||
|
||||
func uniqueSkillNames(skillNames []string) []string {
|
||||
filtered := make([]string, 0, len(skillNames))
|
||||
seen := make(map[string]struct{}, len(skillNames))
|
||||
for _, name := range skillNames {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
if _, exists := seen[name]; exists {
|
||||
continue
|
||||
}
|
||||
seen[name] = struct{}{}
|
||||
filtered = append(filtered, name)
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
func quoteSkillNames(skillNames []string) string {
|
||||
quoted := make([]string, 0, len(skillNames))
|
||||
for _, name := range skillNames {
|
||||
quoted = append(quoted, fmt.Sprintf("%q", name))
|
||||
}
|
||||
return strings.Join(quoted, ", ")
|
||||
}
|
||||
|
||||
func mapCommandError(result commands.ExecuteResult) string {
|
||||
if result.Command == "" {
|
||||
return fmt.Sprintf("Failed to execute command: %v", result.Err)
|
||||
|
|
|
|||
|
|
@ -365,6 +365,60 @@ func TestApplyExplicitSkillCommand_ArmsSkillForNextMessage(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestApplyExplicitSkillCommand_AppendsPendingSkillsForNextMessage(t *testing.T) {
|
||||
al, cfg, _, _, cleanup := newTestAgentLoop(t)
|
||||
defer cleanup()
|
||||
|
||||
for _, skillName := range []string{"shell", "finance-news"} {
|
||||
skillDir := filepath.Join(cfg.Agents.Defaults.Workspace, "skills", skillName)
|
||||
if err := os.MkdirAll(skillDir, 0o755); err != nil {
|
||||
t.Fatalf("MkdirAll(%s) error = %v", skillName, err)
|
||||
}
|
||||
if err := os.WriteFile(
|
||||
filepath.Join(skillDir, "SKILL.md"),
|
||||
[]byte("# "+skillName+"\n\nSkill test fixture.\n"),
|
||||
0o644,
|
||||
); err != nil {
|
||||
t.Fatalf("WriteFile(%s/SKILL.md) error = %v", skillName, err)
|
||||
}
|
||||
}
|
||||
|
||||
agent := al.GetRegistry().GetDefaultAgent()
|
||||
if agent == nil {
|
||||
t.Fatal("expected default agent")
|
||||
}
|
||||
|
||||
opts := &processOptions{SessionKey: "agent:main:test"}
|
||||
_, handled, reply := al.applyExplicitSkillCommand("/use shell", agent, opts)
|
||||
if !handled {
|
||||
t.Fatal("expected /use shell to be handled")
|
||||
}
|
||||
if !strings.Contains(reply, `Skill "shell" is armed for your next message`) {
|
||||
t.Fatalf("unexpected first reply: %q", reply)
|
||||
}
|
||||
|
||||
_, handled, reply = al.applyExplicitSkillCommand("/use finance-news", agent, opts)
|
||||
if !handled {
|
||||
t.Fatal("expected /use finance-news to be handled")
|
||||
}
|
||||
if !strings.Contains(reply, `Skills "shell", "finance-news" are armed for your next message`) {
|
||||
t.Fatalf("unexpected second reply: %q", reply)
|
||||
}
|
||||
|
||||
_, handled, reply = al.applyExplicitSkillCommand("/use shell", agent, opts)
|
||||
if !handled {
|
||||
t.Fatal("expected duplicate /use shell to be handled")
|
||||
}
|
||||
if !strings.Contains(reply, `Skills "shell", "finance-news" are armed for your next message`) {
|
||||
t.Fatalf("unexpected duplicate reply: %q", reply)
|
||||
}
|
||||
|
||||
pending := al.takePendingSkills(opts.SessionKey)
|
||||
if !slices.Equal(pending, []string{"shell", "finance-news"}) {
|
||||
t.Fatalf("pending skills = %#v, want [shell finance-news]", pending)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyExplicitSkillCommand_InlineMessageMutatesOptions(t *testing.T) {
|
||||
al, cfg, _, _, cleanup := newTestAgentLoop(t)
|
||||
defer cleanup()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue