Skip to content

Commit 82d0e7a

Browse files
authored
Merge pull request #927 from rumpl/feat-deferred
Add deferred tools
2 parents 09ef034 + e62896a commit 82d0e7a

File tree

7 files changed

+492
-1
lines changed

7 files changed

+492
-1
lines changed

cagent-schema.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,31 @@
432432
"type": "boolean",
433433
"description": "Whether to ignore VCS files (.git directories and .gitignore patterns) in filesystem operations. Default: true",
434434
"default": true
435+
},
436+
"defer": {
437+
"description": "Enable deferred loading for tools in this toolset. Set to true to defer all tools, or an array of tool names to defer only those tools. Deferred tools are not loaded into the agent's context immediately, but can be discovered and loaded on-demand using search_tool and add_tool.",
438+
"oneOf": [
439+
{
440+
"type": "boolean",
441+
"description": "Set to true to defer all tools"
442+
},
443+
{
444+
"type": "array",
445+
"description": "Array of tool names to defer",
446+
"items": {
447+
"type": "string"
448+
}
449+
}
450+
],
451+
"examples": [
452+
true,
453+
["read_file", "write_file"]
454+
]
455+
},
456+
"timeout": {
457+
"type": "integer",
458+
"description": "Timeout in seconds for the fetch tool",
459+
"minimum": 1
435460
}
436461
},
437462
"additionalProperties": false,

examples/deferred.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#!/usr/bin/env cagent run
2+
3+
agents:
4+
root:
5+
model: openai/gpt-4o
6+
description: Simple agent with filesystem access
7+
instruction: Use the tools to help the user with filesystem operations.
8+
toolsets:
9+
- type: filesystem
10+
defer: true
11+
- type: shell
12+
defer: true

pkg/config/latest/types.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,8 @@ type Toolset struct {
121121
Instruction string `json:"instruction,omitempty"`
122122
Toon string `json:"toon,omitempty"`
123123

124+
Defer DeferConfig `json:"defer,omitempty" yaml:"defer,omitempty"`
125+
124126
// For the `mcp` tool
125127
Command string `json:"command,omitempty"`
126128
Args []string `json:"args,omitempty"`
@@ -168,6 +170,49 @@ type Remote struct {
168170
Headers map[string]string `json:"headers,omitempty"`
169171
}
170172

173+
// DeferConfig represents the deferred loading configuration for a toolset.
174+
// It can be either a boolean (true to defer all tools) or a slice of strings
175+
// (list of tool names to defer).
176+
type DeferConfig struct {
177+
// DeferAll is true when all tools should be deferred
178+
DeferAll bool `json:"-"`
179+
// Tools is the list of specific tool names to defer (empty if DeferAll is true)
180+
Tools []string `json:"-"`
181+
}
182+
183+
func (d DeferConfig) IsEmpty() bool {
184+
return !d.DeferAll && len(d.Tools) == 0
185+
}
186+
187+
func (d *DeferConfig) UnmarshalYAML(unmarshal func(any) error) error {
188+
var b bool
189+
if err := unmarshal(&b); err == nil {
190+
d.DeferAll = b
191+
d.Tools = nil
192+
return nil
193+
}
194+
195+
var tools []string
196+
if err := unmarshal(&tools); err == nil {
197+
d.DeferAll = false
198+
d.Tools = tools
199+
return nil
200+
}
201+
202+
return nil
203+
}
204+
205+
func (d DeferConfig) MarshalYAML() ([]byte, error) {
206+
if d.DeferAll {
207+
return yaml.Marshal(true)
208+
}
209+
if len(d.Tools) == 0 {
210+
// Return false for empty config - this will be omitted by yaml encoder
211+
return yaml.Marshal(false)
212+
}
213+
return yaml.Marshal(d.Tools)
214+
}
215+
171216
// ThinkingBudget represents reasoning budget configuration.
172217
// It accepts either a string effort level or an integer token budget:
173218
// - String: "minimal", "low", "medium", "high" (for OpenAI)

pkg/teamloader/filter.go

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"github.com/docker/cagent/pkg/tools"
99
)
1010

11+
// WithToolsFilter creates a toolset that only includes the specified tools.
12+
// If no tool names are provided, all tools are included.
1113
func WithToolsFilter(inner tools.ToolSet, toolNames ...string) tools.ToolSet {
1214
if len(toolNames) == 0 {
1315
return inner
@@ -16,12 +18,28 @@ func WithToolsFilter(inner tools.ToolSet, toolNames ...string) tools.ToolSet {
1618
return &filterTools{
1719
ToolSet: inner,
1820
toolNames: toolNames,
21+
exclude: false,
22+
}
23+
}
24+
25+
// WithToolsExcludeFilter creates a toolset that excludes the specified tools.
26+
// If no tool names are provided, all tools are included.
27+
func WithToolsExcludeFilter(inner tools.ToolSet, toolNames ...string) tools.ToolSet {
28+
if len(toolNames) == 0 {
29+
return inner
30+
}
31+
32+
return &filterTools{
33+
ToolSet: inner,
34+
toolNames: toolNames,
35+
exclude: true,
1936
}
2037
}
2138

2239
type filterTools struct {
2340
tools.ToolSet
2441
toolNames []string
42+
exclude bool
2543
}
2644

2745
func (f *filterTools) Tools(ctx context.Context) ([]tools.Tool, error) {
@@ -32,7 +50,11 @@ func (f *filterTools) Tools(ctx context.Context) ([]tools.Tool, error) {
3250

3351
var filtered []tools.Tool
3452
for _, tool := range allTools {
35-
if !slices.Contains(f.toolNames, tool.Name) {
53+
contains := slices.Contains(f.toolNames, tool.Name)
54+
55+
// Exclude mode: keep only tools NOT in the list
56+
// Include mode: keep only tools in the list
57+
if (f.exclude && contains) || (!f.exclude && !contains) {
3658
slog.Debug("Filtering out tool", "tool", tool.Name)
3759
continue
3860
}

pkg/teamloader/teamloader.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,8 @@ func getToolsForAgent(ctx context.Context, a *latest.AgentConfig, parentDir stri
284284
warnings []string
285285
)
286286

287+
deferredToolset := builtin.NewDeferredToolset()
288+
287289
for i := range a.Toolsets {
288290
toolset := a.Toolsets[i]
289291

@@ -299,9 +301,25 @@ func getToolsForAgent(ctx context.Context, a *latest.AgentConfig, parentDir stri
299301
wrapped = WithInstructions(wrapped, toolset.Instruction)
300302
wrapped = WithToon(wrapped, toolset.Toon)
301303

304+
// Handle deferred tools
305+
if !toolset.Defer.IsEmpty() {
306+
deferredToolset.AddSource(wrapped, toolset.Defer.DeferAll, toolset.Defer.Tools)
307+
if toolset.Defer.DeferAll {
308+
// Don't add the wrapped toolset to toolSets - all its tools are deferred
309+
// TODO: maybe we _do_ want to add this toolset since it has instructions?
310+
continue
311+
} else {
312+
wrapped = WithToolsExcludeFilter(wrapped, toolset.Defer.Tools...)
313+
}
314+
}
315+
302316
toolSets = append(toolSets, wrapped)
303317
}
304318

319+
if deferredToolset.HasSources() {
320+
toolSets = append(toolSets, deferredToolset)
321+
}
322+
305323
if len(a.SubAgents) > 0 {
306324
toolSets = append(toolSets, builtin.NewTransferTaskTool())
307325
}

0 commit comments

Comments
 (0)