Skip to content

Commit 9c8db20

Browse files
authored
Merge pull request #1822 from rumpl/remote-skills
Implement remote skills discovery with disk cache and dedicated tools
2 parents f39705d + 0ab3e49 commit 9c8db20

23 files changed

Lines changed: 1744 additions & 175 deletions

pkg/agent/agent.go

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ type Agent struct {
3838
tools []tools.Tool
3939
commands types.Commands
4040
pendingWarnings []string
41-
skillsEnabled bool
4241
hooks *latest.HooksConfig
4342
thinkingConfigured bool // true if thinking_budget was explicitly set in config
4443
}
@@ -193,11 +192,6 @@ func (a *Agent) Commands() types.Commands {
193192
return a.commands
194193
}
195194

196-
// SkillsEnabled returns whether skills discovery is enabled for this agent.
197-
func (a *Agent) SkillsEnabled() bool {
198-
return a.skillsEnabled
199-
}
200-
201195
// Hooks returns the hooks configuration for this agent.
202196
func (a *Agent) Hooks() *latest.HooksConfig {
203197
return a.hooks

pkg/agent/opts.go

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -147,12 +147,6 @@ func WithLoadTimeWarnings(warnings []string) Opt {
147147
}
148148
}
149149

150-
func WithSkillsEnabled(enabled bool) Opt {
151-
return func(a *Agent) {
152-
a.skillsEnabled = enabled
153-
}
154-
}
155-
156150
func WithHooks(hooks *latest.HooksConfig) Opt {
157151
return func(a *Agent) {
158152
a.hooks = hooks

pkg/app/app.go

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -172,36 +172,42 @@ func (a *App) CurrentAgentCommands(ctx context.Context) types.Commands {
172172

173173
// CurrentAgentSkills returns the available skills if skills are enabled for the current agent.
174174
func (a *App) CurrentAgentSkills() []skills.Skill {
175-
if a.runtime.CurrentAgentSkillsEnabled() {
176-
return skills.Load()
175+
st := a.runtime.CurrentAgentSkillsToolset()
176+
if st == nil {
177+
return nil
177178
}
178-
return nil
179+
return st.Skills()
179180
}
180181

181182
// ResolveSkillCommand checks if the input matches a skill slash command (e.g. /skill-name args).
182-
// If matched, it reads the skill file and returns the resolved prompt. Otherwise returns "".
183+
// If matched, it reads the skill content and returns the resolved prompt. Otherwise returns "".
183184
func (a *App) ResolveSkillCommand(input string) (string, error) {
184185
if !strings.HasPrefix(input, "/") {
185186
return "", nil
186187
}
187188

189+
st := a.runtime.CurrentAgentSkillsToolset()
190+
if st == nil {
191+
return "", nil
192+
}
193+
188194
cmd, arg, _ := strings.Cut(input[1:], " ")
189195
arg = strings.TrimSpace(arg)
190196

191-
for _, skill := range a.CurrentAgentSkills() {
197+
for _, skill := range st.Skills() {
192198
if skill.Name != cmd {
193199
continue
194200
}
195201

196-
content, err := os.ReadFile(skill.FilePath)
202+
content, err := st.ReadSkillContent(skill.Name)
197203
if err != nil {
198204
return "", fmt.Errorf("reading skill %q: %w", skill.Name, err)
199205
}
200206

201207
if arg != "" {
202-
return fmt.Sprintf("Use the following skill.\n\nUser's request: %s\n\n<skill name=%q>\n%s\n</skill>", arg, skill.Name, string(content)), nil
208+
return fmt.Sprintf("Use the following skill.\n\nUser's request: %s\n\n<skill name=%q>\n%s\n</skill>", arg, skill.Name, content), nil
203209
}
204-
return fmt.Sprintf("Use the following skill.\n\n<skill name=%q>\n%s\n</skill>", skill.Name, string(content)), nil
210+
return fmt.Sprintf("Use the following skill.\n\n<skill name=%q>\n%s\n</skill>", skill.Name, content), nil
205211
}
206212

207213
return "", nil

pkg/app/app_test.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/docker/cagent/pkg/session"
1313
"github.com/docker/cagent/pkg/sessiontitle"
1414
"github.com/docker/cagent/pkg/tools"
15+
"github.com/docker/cagent/pkg/tools/builtin"
1516
mcptools "github.com/docker/cagent/pkg/tools/mcp"
1617
)
1718

@@ -47,7 +48,10 @@ func (m *mockRuntime) SessionStore() session.Store { return nil }
4748
func (m *mockRuntime) Summarize(ctx context.Context, sess *session.Session, additionalPrompt string, events chan runtime.Event) {
4849
}
4950
func (m *mockRuntime) PermissionsInfo() *runtime.PermissionsInfo { return nil }
50-
func (m *mockRuntime) CurrentAgentSkillsEnabled() bool { return false }
51+
func (m *mockRuntime) CurrentAgentSkillsToolset() *builtin.SkillsToolset {
52+
return nil
53+
}
54+
5155
func (m *mockRuntime) CurrentMCPPrompts(context.Context) map[string]mcptools.PromptInfo {
5256
return make(map[string]mcptools.PromptInfo)
5357
}

pkg/config/config.go

Lines changed: 11 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -185,43 +185,19 @@ func validateProviderName(name string) error {
185185
return nil
186186
}
187187

188-
// validateSkillsConfiguration ensures that agents with skills enabled have the necessary tools
189-
func validateSkillsConfiguration(agentName string, agent *latest.AgentConfig) error {
190-
// Check if skills are enabled
191-
if agent.Skills == nil || !*agent.Skills {
192-
return nil
193-
}
194-
195-
// Skills are enabled, validate toolsets
196-
hasFilesystemToolset := false
197-
hasReadFileTool := false
198-
199-
for _, toolset := range agent.Toolsets {
200-
if toolset.Type == "filesystem" {
201-
hasFilesystemToolset = true
202-
203-
// Check if read_file tool is enabled
204-
// If no specific tools are listed, all tools are enabled
205-
if len(toolset.Tools) == 0 {
206-
hasReadFileTool = true
207-
break
208-
}
209-
210-
// Check if read_file is in the tools list
211-
if slices.Contains(toolset.Tools, "read_file") {
212-
hasReadFileTool = true
213-
break
188+
// validateSkillsConfiguration validates the skills configuration for an agent.
189+
func validateSkillsConfiguration(_ string, agent *latest.AgentConfig) error {
190+
for _, source := range agent.Skills.Sources {
191+
switch {
192+
case source == latest.SkillSourceLocal:
193+
// valid
194+
case strings.HasPrefix(source, "http://") || strings.HasPrefix(source, "https://"):
195+
if _, err := url.Parse(source); err != nil {
196+
return fmt.Errorf("agent '%s' has invalid skills source URL '%s': %w", agent.Name, source, err)
214197
}
198+
default:
199+
return fmt.Errorf("agent '%s' has unknown skills source '%s' (must be 'local' or an HTTP/HTTPS URL)", agent.Name, source)
215200
}
216201
}
217-
218-
if !hasFilesystemToolset {
219-
return fmt.Errorf("agent '%s' has skills enabled but does not have a 'filesystem' toolset configured", agentName)
220-
}
221-
222-
if !hasReadFileTool {
223-
return fmt.Errorf("agent '%s' has skills enabled but the 'filesystem' toolset does not include the 'read_file' tool", agentName)
224-
}
225-
226202
return nil
227203
}

0 commit comments

Comments
 (0)