Skip to content

Commit 5ce4746

Browse files
committed
Friendly tools
Add the ability for the LLM to send a description of what a tool call is doing. Signed-off-by: Djordje Lukic <djordje.lukic@docker.com>
1 parent 40d3598 commit 5ce4746

File tree

11 files changed

+360
-65
lines changed

11 files changed

+360
-65
lines changed

pkg/config/latest/types.go

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -110,25 +110,26 @@ type ProviderConfig struct {
110110

111111
// AgentConfig represents a single agent configuration
112112
type AgentConfig struct {
113-
Name string
114-
Model string `json:"model,omitempty"`
115-
Description string `json:"description,omitempty"`
116-
WelcomeMessage string `json:"welcome_message,omitempty"`
117-
Toolsets []Toolset `json:"toolsets,omitempty"`
118-
Instruction string `json:"instruction,omitempty"`
119-
SubAgents []string `json:"sub_agents,omitempty"`
120-
Handoffs []string `json:"handoffs,omitempty"`
121-
RAG []string `json:"rag,omitempty"`
122-
AddDate bool `json:"add_date,omitempty"`
123-
AddEnvironmentInfo bool `json:"add_environment_info,omitempty"`
124-
CodeModeTools bool `json:"code_mode_tools,omitempty"`
125-
MaxIterations int `json:"max_iterations,omitempty"`
126-
NumHistoryItems int `json:"num_history_items,omitempty"`
127-
AddPromptFiles []string `json:"add_prompt_files,omitempty" yaml:"add_prompt_files,omitempty"`
128-
Commands types.Commands `json:"commands,omitempty"`
129-
StructuredOutput *StructuredOutput `json:"structured_output,omitempty"`
130-
Skills *bool `json:"skills,omitempty"`
131-
Hooks *HooksConfig `json:"hooks,omitempty"`
113+
Name string
114+
Model string `json:"model,omitempty"`
115+
Description string `json:"description,omitempty"`
116+
WelcomeMessage string `json:"welcome_message,omitempty"`
117+
Toolsets []Toolset `json:"toolsets,omitempty"`
118+
Instruction string `json:"instruction,omitempty"`
119+
SubAgents []string `json:"sub_agents,omitempty"`
120+
Handoffs []string `json:"handoffs,omitempty"`
121+
RAG []string `json:"rag,omitempty"`
122+
AddDate bool `json:"add_date,omitempty"`
123+
AddEnvironmentInfo bool `json:"add_environment_info,omitempty"`
124+
CodeModeTools bool `json:"code_mode_tools,omitempty"`
125+
ToolsWithDescriptions bool `json:"tools_with_descriptions,omitempty"`
126+
MaxIterations int `json:"max_iterations,omitempty"`
127+
NumHistoryItems int `json:"num_history_items,omitempty"`
128+
AddPromptFiles []string `json:"add_prompt_files,omitempty" yaml:"add_prompt_files,omitempty"`
129+
Commands types.Commands `json:"commands,omitempty"`
130+
StructuredOutput *StructuredOutput `json:"structured_output,omitempty"`
131+
Skills *bool `json:"skills,omitempty"`
132+
Hooks *HooksConfig `json:"hooks,omitempty"`
132133
}
133134

134135
// ModelConfig represents the configuration for a model

pkg/teamloader/teamloader.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,11 @@ func getToolsForAgent(ctx context.Context, a *latest.AgentConfig, parentDir stri
307307
}
308308
}
309309

310+
// Apply friendly tools wrapper if enabled
311+
if a.ToolsWithDescriptions {
312+
wrapped = tools.NewDescriptionToolSet(wrapped)
313+
}
314+
310315
toolSets = append(toolSets, wrapped)
311316
}
312317

@@ -351,6 +356,7 @@ func createRAGToolsForAgent(agentConfig *latest.AgentConfig, allManagers map[str
351356

352357
// Create a separate tool for this RAG source
353358
ragTool := builtin.NewRAGTool(mgr, toolName)
359+
354360
ragTools = append(ragTools, ragTool)
355361

356362
slog.Debug("Created RAG tool for agent",

pkg/tools/builtin/filesystem.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ func (t *FilesystemTool) Tools(context.Context) ([]tools.Tool, error) {
207207
Annotations: tools.ToolAnnotations{
208208
Title: "Edit",
209209
},
210+
WithDescription: true,
210211
},
211212
{
212213
Name: ToolNameListDirectory,
@@ -219,6 +220,7 @@ func (t *FilesystemTool) Tools(context.Context) ([]tools.Tool, error) {
219220
ReadOnlyHint: true,
220221
Title: "List Directory",
221222
},
223+
WithDescription: true,
222224
},
223225
{
224226
Name: ToolNameReadFile,
@@ -256,6 +258,7 @@ func (t *FilesystemTool) Tools(context.Context) ([]tools.Tool, error) {
256258
ReadOnlyHint: true,
257259
Title: "Search Files Content",
258260
},
261+
WithDescription: true,
259262
},
260263
{
261264
Name: ToolNameWriteFile,
@@ -267,6 +270,7 @@ func (t *FilesystemTool) Tools(context.Context) ([]tools.Tool, error) {
267270
Annotations: tools.ToolAnnotations{
268271
Title: "Write",
269272
},
273+
WithDescription: true,
270274
},
271275
}, nil
272276
}

pkg/tools/builtin/shell.go

Lines changed: 39 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -422,48 +422,53 @@ func (t *ShellTool) Tools(context.Context) ([]tools.Tool, error) {
422422

423423
return []tools.Tool{
424424
{
425-
Name: ToolNameShell,
426-
Category: "shell",
427-
Description: shellDesc,
428-
Parameters: tools.MustSchemaFor[RunShellArgs](),
429-
OutputSchema: tools.MustSchemaFor[string](),
430-
Handler: tools.NewHandler(t.handler.RunShell),
431-
Annotations: tools.ToolAnnotations{Title: "Shell"},
425+
Name: ToolNameShell,
426+
Category: "shell",
427+
Description: shellDesc,
428+
Parameters: tools.MustSchemaFor[RunShellArgs](),
429+
OutputSchema: tools.MustSchemaFor[string](),
430+
Handler: tools.NewHandler(t.handler.RunShell),
431+
Annotations: tools.ToolAnnotations{Title: "Shell"},
432+
WithDescription: true,
432433
},
433434
{
434-
Name: ToolNameRunShellBackground,
435-
Category: "shell",
436-
Description: `Starts a shell command in the background and returns immediately with a job ID. Use this for long-running processes like servers, watches, or any command that should run while other tasks are performed.`,
437-
Parameters: tools.MustSchemaFor[RunShellBackgroundArgs](),
438-
OutputSchema: tools.MustSchemaFor[string](),
439-
Handler: tools.NewHandler(t.handler.RunShellBackground),
440-
Annotations: tools.ToolAnnotations{Title: "Background Job"},
435+
Name: ToolNameRunShellBackground,
436+
Category: "shell",
437+
Description: `Starts a shell command in the background and returns immediately with a job ID. Use this for long-running processes like servers, watches, or any command that should run while other tasks are performed.`,
438+
Parameters: tools.MustSchemaFor[RunShellBackgroundArgs](),
439+
OutputSchema: tools.MustSchemaFor[string](),
440+
Handler: tools.NewHandler(t.handler.RunShellBackground),
441+
Annotations: tools.ToolAnnotations{Title: "Background Job"},
442+
WithDescription: true,
441443
},
442444
{
443-
Name: ToolNameListBackgroundJobs,
444-
Category: "shell",
445-
Description: `Lists all background jobs with their status, runtime, and other information.`,
446-
OutputSchema: tools.MustSchemaFor[string](),
447-
Handler: tools.NewHandler(t.handler.ListBackgroundJobs),
448-
Annotations: tools.ToolAnnotations{Title: "List Background Jobs", ReadOnlyHint: true},
445+
Name: ToolNameListBackgroundJobs,
446+
Category: "shell",
447+
Description: `Lists all background jobs with their status, runtime, and other information.`,
448+
OutputSchema: tools.MustSchemaFor[string](),
449+
Handler: tools.NewHandler(t.handler.ListBackgroundJobs),
450+
Annotations: tools.ToolAnnotations{Title: "List Background Jobs", ReadOnlyHint: true},
451+
WithDescription: true,
449452
},
450453
{
451-
Name: ToolNameViewBackgroundJob,
452-
Category: "shell",
453-
Description: `Views the output and status of a specific background job by job ID.`,
454-
Parameters: tools.MustSchemaFor[ViewBackgroundJobArgs](),
455-
OutputSchema: tools.MustSchemaFor[string](),
456-
Handler: tools.NewHandler(t.handler.ViewBackgroundJob),
457-
Annotations: tools.ToolAnnotations{Title: "View Background Job Output", ReadOnlyHint: true},
454+
Name: ToolNameViewBackgroundJob,
455+
Category: "shell",
456+
Description: `Views the output and status of a specific background job by job ID.`,
457+
Parameters: tools.MustSchemaFor[ViewBackgroundJobArgs](),
458+
OutputSchema: tools.MustSchemaFor[string](),
459+
Handler: tools.NewHandler(t.handler.ViewBackgroundJob),
460+
Annotations: tools.ToolAnnotations{Title: "View Background Job Output", ReadOnlyHint: true},
461+
WithDescription: true,
458462
},
459463
{
460-
Name: ToolNameStopBackgroundJob,
461-
Category: "shell",
462-
Description: `Stops a running background job by job ID. The process and all its child processes will be terminated.`,
463-
Parameters: tools.MustSchemaFor[StopBackgroundJobArgs](),
464-
OutputSchema: tools.MustSchemaFor[string](),
465-
Handler: tools.NewHandler(t.handler.StopBackgroundJob),
466-
Annotations: tools.ToolAnnotations{Title: "Stop Background Job"},
464+
Name: ToolNameStopBackgroundJob,
465+
Category: "shell",
466+
Description: `Stops a running background job by job ID. The process and all its child processes will be terminated.`,
467+
Parameters: tools.MustSchemaFor[StopBackgroundJobArgs](),
468+
OutputSchema: tools.MustSchemaFor[string](),
469+
Handler: tools.NewHandler(t.handler.StopBackgroundJob),
470+
Annotations: tools.ToolAnnotations{Title: "Stop Background Job"},
471+
WithDescription: true,
467472
},
468473
}, nil
469474
}

pkg/tools/description.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package tools
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
)
7+
8+
const (
9+
// DescriptionParam is the parameter name for the description
10+
DescriptionParam = "description"
11+
)
12+
13+
// DescriptionToolSet wraps a ToolSet and adds a "description" parameter to all tools.
14+
// This allows the LLM to provide context about what it's doing with each tool call.
15+
type DescriptionToolSet struct {
16+
inner ToolSet
17+
}
18+
19+
// NewDescriptionToolSet creates a new DescriptionToolSet wrapping the given ToolSet.
20+
func NewDescriptionToolSet(inner ToolSet) *DescriptionToolSet {
21+
return &DescriptionToolSet{inner: inner}
22+
}
23+
24+
func (f *DescriptionToolSet) Tools(ctx context.Context) ([]Tool, error) {
25+
tools, err := f.inner.Tools(ctx)
26+
if err != nil {
27+
return nil, err
28+
}
29+
30+
result := make([]Tool, len(tools))
31+
for i, tool := range tools {
32+
result[i] = f.addDescriptionParam(tool)
33+
}
34+
return result, nil
35+
}
36+
37+
func (f *DescriptionToolSet) Instructions() string {
38+
return f.inner.Instructions()
39+
}
40+
41+
func (f *DescriptionToolSet) Start(ctx context.Context) error {
42+
return f.inner.Start(ctx)
43+
}
44+
45+
func (f *DescriptionToolSet) Stop(ctx context.Context) error {
46+
return f.inner.Stop(ctx)
47+
}
48+
49+
func (f *DescriptionToolSet) SetElicitationHandler(handler ElicitationHandler) {
50+
f.inner.SetElicitationHandler(handler)
51+
}
52+
53+
func (f *DescriptionToolSet) SetOAuthSuccessHandler(handler func()) {
54+
f.inner.SetOAuthSuccessHandler(handler)
55+
}
56+
57+
func (f *DescriptionToolSet) SetManagedOAuth(managed bool) {
58+
f.inner.SetManagedOAuth(managed)
59+
}
60+
61+
func (f *DescriptionToolSet) addDescriptionParam(tool Tool) Tool {
62+
if !tool.WithDescription {
63+
return tool
64+
}
65+
66+
schema, err := SchemaToMap(tool.Parameters)
67+
if err != nil {
68+
return tool
69+
}
70+
71+
properties, ok := schema["properties"].(map[string]any)
72+
if !ok {
73+
properties = make(map[string]any)
74+
schema["properties"] = properties
75+
}
76+
77+
properties[DescriptionParam] = map[string]any{
78+
"type": "string",
79+
"description": "A brief, human-readable description of what this tool call is doing",
80+
}
81+
82+
tool.Parameters = schema
83+
return tool
84+
}
85+
86+
// ExtractDescription extracts the description from tool call arguments.
87+
func ExtractDescription(arguments string) string {
88+
var args map[string]any
89+
if err := json.Unmarshal([]byte(arguments), &args); err != nil {
90+
return ""
91+
}
92+
93+
if desc, ok := args[DescriptionParam].(string); ok {
94+
return desc
95+
}
96+
return ""
97+
}

0 commit comments

Comments
 (0)