Skip to content

Commit d241b03

Browse files
authored
Merge pull request #1546 from dgageot/cagent-new-2
Fix and improve `cagent new`
2 parents c078653 + 72549c1 commit d241b03

4 files changed

Lines changed: 478 additions & 180 deletions

File tree

cmd/root/new.go

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,17 @@ func newNewCmd() *cobra.Command {
2727
var flags newFlags
2828

2929
cmd := &cobra.Command{
30-
Use: "new",
31-
Short: "Create a new agent configuration",
32-
Long: `Create a new agent configuration by asking questions and generating a YAML file`,
30+
Use: "new [description]",
31+
Short: "Create a new agent configuration",
32+
Long: `Create a new agent configuration interactively.
33+
34+
The agent builder will ask questions about what you want the agent to do,
35+
then generate a YAML configuration file you can use with 'cagent run'.
36+
37+
Optionally provide a description as an argument to skip the initial prompt.`,
38+
Example: ` cagent new
39+
cagent new "a web scraper that extracts product prices"
40+
cagent new --model openai/gpt-4o "a code reviewer agent"`,
3341
GroupID: "core",
3442
RunE: flags.runNewCommand,
3543
}
@@ -50,6 +58,11 @@ func (f *newFlags) runNewCommand(cmd *cobra.Command, args []string) error {
5058
if err != nil {
5159
return err
5260
}
61+
defer func() {
62+
// Use a fresh context for cleanup since the original may be canceled
63+
cleanupCtx := context.WithoutCancel(ctx)
64+
_ = t.StopToolSets(cleanupCtx)
65+
}()
5366

5467
rt, err := runtime.New(t)
5568
if err != nil {

pkg/creator/agent.go

Lines changed: 124 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
// Package creator provides functionality to create agent configurations interactively.
2+
// It generates a special agent that helps users build their own agent YAML files.
13
package creator
24

35
import (
@@ -7,6 +9,8 @@ import (
79
"fmt"
810
"strings"
911

12+
"github.com/goccy/go-yaml"
13+
1014
"github.com/docker/cagent/pkg/config"
1115
"github.com/docker/cagent/pkg/config/latest"
1216
"github.com/docker/cagent/pkg/team"
@@ -18,95 +22,153 @@ import (
1822
//go:embed instructions.txt
1923
var agentBuilderInstructions string
2024

21-
type fsToolset struct {
22-
tools.ToolSet
23-
originalWriteFileHandler tools.ToolHandler
24-
path string
25-
}
26-
27-
func (f *fsToolset) Tools(ctx context.Context) ([]tools.Tool, error) {
28-
innerTools, err := f.ToolSet.Tools(ctx)
29-
if err != nil {
30-
return nil, err
31-
}
32-
33-
for i, tool := range innerTools {
34-
if tool.Name == builtin.ToolNameWriteFile {
35-
f.originalWriteFileHandler = tool.Handler
36-
innerTools[i].Handler = f.customWriteFileHandler
37-
}
38-
}
25+
// Constants for the creator agent configuration.
26+
const (
27+
creatorAgentName = "root"
28+
creatorAgentModel = "auto"
29+
creatorWelcomeMessage = "Hello! I'm here to create agents for you.\n\nCan you explain to me what the agent will be used for?"
30+
)
3931

40-
return innerTools, nil
41-
}
32+
// Agent creates and returns a team configured for the agent builder functionality.
33+
// The agent builder helps users create their own agent configurations interactively.
34+
//
35+
// Parameters:
36+
// - ctx: Context for the operation
37+
// - runConfig: Runtime configuration including working directory and environment
38+
// - modelNameOverride: Optional model override (empty string uses auto-selection)
39+
//
40+
// Returns the configured team or an error if configuration fails.
41+
func Agent(ctx context.Context, runConfig *config.RuntimeConfig, modelNameOverride string) (*team.Team, error) {
42+
instructions := buildInstructions(ctx, runConfig)
4243

43-
func (f *fsToolset) customWriteFileHandler(ctx context.Context, toolCall tools.ToolCall) (*tools.ToolCallResult, error) {
44-
var args struct {
45-
Path string `json:"path"`
46-
Content string `json:"content"`
47-
}
48-
if err := json.Unmarshal([]byte(toolCall.Function.Arguments), &args); err != nil {
49-
return nil, fmt.Errorf("failed to parse arguments: %w", err)
44+
configYAML, err := buildCreatorConfigYAML(instructions)
45+
if err != nil {
46+
return nil, fmt.Errorf("building creator config: %w", err)
5047
}
5148

52-
f.path = args.Path
49+
registry := createToolsetRegistry(runConfig.WorkingDir)
5350

54-
return f.originalWriteFileHandler(ctx, toolCall)
51+
return teamloader.Load(
52+
ctx,
53+
config.NewBytesSource("creator", configYAML),
54+
runConfig,
55+
teamloader.WithModelOverrides([]string{modelNameOverride}),
56+
teamloader.WithToolsetRegistry(registry),
57+
)
5558
}
5659

57-
func Agent(ctx context.Context, runConfig *config.RuntimeConfig, modelNameOverride string) (*team.Team, error) {
60+
// buildInstructions creates the full instruction set for the creator agent,
61+
// including provider-specific model configuration examples.
62+
func buildInstructions(ctx context.Context, runConfig *config.RuntimeConfig) string {
5863
usableProviders := config.AvailableProviders(ctx, runConfig.ModelsGateway, runConfig.EnvProvider())
5964

60-
// Provide soft guidance to prefer the selected providers
61-
instructions := agentBuilderInstructions
62-
instructions += "\n\nPreferred model providers to use: " + strings.Join(usableProviders, ", ")
63-
instructions += ". You must always use one or more of the following model configurations: \n"
65+
var b strings.Builder
66+
b.WriteString(agentBuilderInstructions)
67+
b.WriteString("\n\nPreferred model providers to use: ")
68+
b.WriteString(strings.Join(usableProviders, ", "))
69+
b.WriteString(". You must always use one or more of the following model configurations: \n")
6470

6571
for _, provider := range usableProviders {
6672
model := config.DefaultModels[provider]
6773
maxTokens := config.PreferredMaxTokens(provider)
68-
instructions += fmt.Sprintf(`
74+
fmt.Fprintf(&b, `
6975
models:
7076
%s:
7177
provider: %s
7278
model: %s
73-
max_tokens: %d\n`, provider, provider, model, maxTokens)
79+
max_tokens: %d
80+
`, provider, provider, model, *maxTokens)
7481
}
7582

76-
// Define a new agent configuration
77-
newAgentConfig := latest.Config{
78-
Agents: []latest.AgentConfig{{
79-
Name: "root",
80-
WelcomeMessage: "Hello! I'm here to create agents for you.\n\nCan you explain to me what the agent will be used for?",
81-
Instruction: instructions,
82-
Model: "auto",
83-
Toolsets: []latest.Toolset{
84-
{Type: "shell"},
85-
{Type: "filesystem"},
86-
},
87-
}},
83+
return b.String()
84+
}
85+
86+
// buildCreatorConfigYAML generates the YAML configuration for the creator agent.
87+
// It uses yaml.MapSlice to ensure proper indentation of multi-line strings.
88+
func buildCreatorConfigYAML(instructions string) ([]byte, error) {
89+
// Define available toolsets for the creator agent
90+
toolsets := []map[string]any{
91+
{"type": "shell"},
92+
{"type": "filesystem"},
8893
}
8994

90-
configAsJSON, err := json.Marshal(newAgentConfig)
91-
if err != nil {
92-
return nil, fmt.Errorf("marshalling config: %w", err)
95+
// Build the root agent configuration
96+
rootAgent := yaml.MapSlice{
97+
{Key: "model", Value: creatorAgentModel},
98+
{Key: "welcome_message", Value: creatorWelcomeMessage},
99+
{Key: "instruction", Value: instructions},
100+
{Key: "toolsets", Value: toolsets},
93101
}
94102

95-
// Custom tool registry to include fsToolset
96-
fsToolset := fsToolset{
97-
ToolSet: builtin.NewFilesystemTool(runConfig.WorkingDir),
103+
// Build the full config structure
104+
agentsConfig := yaml.MapSlice{
105+
{Key: creatorAgentName, Value: rootAgent},
106+
}
107+
108+
fullConfig := yaml.MapSlice{
109+
{Key: "agents", Value: agentsConfig},
110+
}
111+
112+
return yaml.Marshal(fullConfig)
113+
}
114+
115+
// createToolsetRegistry creates a custom toolset registry that wraps the filesystem
116+
// toolset to track file paths written by the agent.
117+
func createToolsetRegistry(workingDir string) *teamloader.ToolsetRegistry {
118+
tracker := &fileWriteTracker{
119+
ToolSet: builtin.NewFilesystemTool(workingDir),
98120
}
99121

100122
registry := teamloader.NewDefaultToolsetRegistry()
101123
registry.Register("filesystem", func(context.Context, latest.Toolset, string, *config.RuntimeConfig) (tools.ToolSet, error) {
102-
return &fsToolset, nil
124+
return tracker, nil
103125
})
104126

105-
return teamloader.Load(
106-
ctx,
107-
config.NewBytesSource("creator", configAsJSON),
108-
runConfig,
109-
teamloader.WithModelOverrides([]string{modelNameOverride}),
110-
teamloader.WithToolsetRegistry(registry),
111-
)
127+
return registry
128+
}
129+
130+
// fileWriteTracker wraps a filesystem toolset to track files written by the agent.
131+
// This allows the creator to know what files were created during the session.
132+
type fileWriteTracker struct {
133+
tools.ToolSet
134+
originalWriteFileHandler tools.ToolHandler
135+
path string
136+
}
137+
138+
// Tools returns the available tools, wrapping the write_file tool to track paths.
139+
func (t *fileWriteTracker) Tools(ctx context.Context) ([]tools.Tool, error) {
140+
innerTools, err := t.ToolSet.Tools(ctx)
141+
if err != nil {
142+
return nil, err
143+
}
144+
145+
for i, tool := range innerTools {
146+
if tool.Name == builtin.ToolNameWriteFile {
147+
t.originalWriteFileHandler = tool.Handler
148+
innerTools[i].Handler = t.trackWriteFile
149+
}
150+
}
151+
152+
return innerTools, nil
153+
}
154+
155+
// trackWriteFile intercepts write_file calls to track the path being written.
156+
func (t *fileWriteTracker) trackWriteFile(ctx context.Context, toolCall tools.ToolCall) (*tools.ToolCallResult, error) {
157+
var args struct {
158+
Path string `json:"path"`
159+
Content string `json:"content"`
160+
}
161+
if err := json.Unmarshal([]byte(toolCall.Function.Arguments), &args); err != nil {
162+
return nil, fmt.Errorf("failed to parse write_file arguments: %w", err)
163+
}
164+
165+
t.path = args.Path
166+
167+
return t.originalWriteFileHandler(ctx, toolCall)
168+
}
169+
170+
// LastWrittenPath returns the path of the last file written by the agent.
171+
// Returns an empty string if no file has been written yet.
172+
func (t *fileWriteTracker) LastWrittenPath() string {
173+
return t.path
112174
}

0 commit comments

Comments
 (0)