From 397464dbed45e650f65afe4671e00f5b7a38f85d Mon Sep 17 00:00:00 2001 From: David Gageot Date: Sat, 31 Jan 2026 09:46:22 +0100 Subject: [PATCH 1/2] Fix cagent new Signed-off-by: David Gageot --- pkg/creator/agent.go | 186 ++++++++++++++++++++++------------ pkg/creator/agent_test.go | 205 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 329 insertions(+), 62 deletions(-) create mode 100644 pkg/creator/agent_test.go diff --git a/pkg/creator/agent.go b/pkg/creator/agent.go index aa86b0bab..eac940ee8 100644 --- a/pkg/creator/agent.go +++ b/pkg/creator/agent.go @@ -1,3 +1,5 @@ +// Package creator provides functionality to create agent configurations interactively. +// It generates a special agent that helps users build their own agent YAML files. package creator import ( @@ -7,6 +9,8 @@ import ( "fmt" "strings" + "github.com/goccy/go-yaml" + "github.com/docker/cagent/pkg/config" "github.com/docker/cagent/pkg/config/latest" "github.com/docker/cagent/pkg/team" @@ -18,95 +22,153 @@ import ( //go:embed instructions.txt var agentBuilderInstructions string -type fsToolset struct { - tools.ToolSet - originalWriteFileHandler tools.ToolHandler - path string -} - -func (f *fsToolset) Tools(ctx context.Context) ([]tools.Tool, error) { - innerTools, err := f.ToolSet.Tools(ctx) - if err != nil { - return nil, err - } - - for i, tool := range innerTools { - if tool.Name == builtin.ToolNameWriteFile { - f.originalWriteFileHandler = tool.Handler - innerTools[i].Handler = f.customWriteFileHandler - } - } +// Constants for the creator agent configuration. +const ( + creatorAgentName = "root" + creatorAgentModel = "auto" + creatorWelcomeMessage = "Hello! I'm here to create agents for you.\n\nCan you explain to me what the agent will be used for?" +) - return innerTools, nil -} +// Agent creates and returns a team configured for the agent builder functionality. +// The agent builder helps users create their own agent configurations interactively. +// +// Parameters: +// - ctx: Context for the operation +// - runConfig: Runtime configuration including working directory and environment +// - modelNameOverride: Optional model override (empty string uses auto-selection) +// +// Returns the configured team or an error if configuration fails. +func Agent(ctx context.Context, runConfig *config.RuntimeConfig, modelNameOverride string) (*team.Team, error) { + instructions := buildInstructions(ctx, runConfig) -func (f *fsToolset) customWriteFileHandler(ctx context.Context, toolCall tools.ToolCall) (*tools.ToolCallResult, error) { - var args struct { - Path string `json:"path"` - Content string `json:"content"` - } - if err := json.Unmarshal([]byte(toolCall.Function.Arguments), &args); err != nil { - return nil, fmt.Errorf("failed to parse arguments: %w", err) + configYAML, err := buildCreatorConfigYAML(instructions) + if err != nil { + return nil, fmt.Errorf("building creator config: %w", err) } - f.path = args.Path + registry := createToolsetRegistry(runConfig.WorkingDir) - return f.originalWriteFileHandler(ctx, toolCall) + return teamloader.Load( + ctx, + config.NewBytesSource("creator", configYAML), + runConfig, + teamloader.WithModelOverrides([]string{modelNameOverride}), + teamloader.WithToolsetRegistry(registry), + ) } -func Agent(ctx context.Context, runConfig *config.RuntimeConfig, modelNameOverride string) (*team.Team, error) { +// buildInstructions creates the full instruction set for the creator agent, +// including provider-specific model configuration examples. +func buildInstructions(ctx context.Context, runConfig *config.RuntimeConfig) string { usableProviders := config.AvailableProviders(ctx, runConfig.ModelsGateway, runConfig.EnvProvider()) - // Provide soft guidance to prefer the selected providers - instructions := agentBuilderInstructions - instructions += "\n\nPreferred model providers to use: " + strings.Join(usableProviders, ", ") - instructions += ". You must always use one or more of the following model configurations: \n" + var b strings.Builder + b.WriteString(agentBuilderInstructions) + b.WriteString("\n\nPreferred model providers to use: ") + b.WriteString(strings.Join(usableProviders, ", ")) + b.WriteString(". You must always use one or more of the following model configurations: \n") for _, provider := range usableProviders { model := config.DefaultModels[provider] maxTokens := config.PreferredMaxTokens(provider) - instructions += fmt.Sprintf(` + fmt.Fprintf(&b, ` models: %s: provider: %s model: %s - max_tokens: %d\n`, provider, provider, model, maxTokens) + max_tokens: %d +`, provider, provider, model, *maxTokens) } - // Define a new agent configuration - newAgentConfig := latest.Config{ - Agents: []latest.AgentConfig{{ - Name: "root", - WelcomeMessage: "Hello! I'm here to create agents for you.\n\nCan you explain to me what the agent will be used for?", - Instruction: instructions, - Model: "auto", - Toolsets: []latest.Toolset{ - {Type: "shell"}, - {Type: "filesystem"}, - }, - }}, + return b.String() +} + +// buildCreatorConfigYAML generates the YAML configuration for the creator agent. +// It uses yaml.MapSlice to ensure proper indentation of multi-line strings. +func buildCreatorConfigYAML(instructions string) ([]byte, error) { + // Define available toolsets for the creator agent + toolsets := []map[string]any{ + {"type": "shell"}, + {"type": "filesystem"}, } - configAsJSON, err := json.Marshal(newAgentConfig) - if err != nil { - return nil, fmt.Errorf("marshalling config: %w", err) + // Build the root agent configuration + rootAgent := yaml.MapSlice{ + {Key: "model", Value: creatorAgentModel}, + {Key: "welcome_message", Value: creatorWelcomeMessage}, + {Key: "instruction", Value: instructions}, + {Key: "toolsets", Value: toolsets}, } - // Custom tool registry to include fsToolset - fsToolset := fsToolset{ - ToolSet: builtin.NewFilesystemTool(runConfig.WorkingDir), + // Build the full config structure + agentsConfig := yaml.MapSlice{ + {Key: creatorAgentName, Value: rootAgent}, + } + + fullConfig := yaml.MapSlice{ + {Key: "agents", Value: agentsConfig}, + } + + return yaml.Marshal(fullConfig) +} + +// createToolsetRegistry creates a custom toolset registry that wraps the filesystem +// toolset to track file paths written by the agent. +func createToolsetRegistry(workingDir string) *teamloader.ToolsetRegistry { + tracker := &fileWriteTracker{ + ToolSet: builtin.NewFilesystemTool(workingDir), } registry := teamloader.NewDefaultToolsetRegistry() registry.Register("filesystem", func(context.Context, latest.Toolset, string, *config.RuntimeConfig) (tools.ToolSet, error) { - return &fsToolset, nil + return tracker, nil }) - return teamloader.Load( - ctx, - config.NewBytesSource("creator", configAsJSON), - runConfig, - teamloader.WithModelOverrides([]string{modelNameOverride}), - teamloader.WithToolsetRegistry(registry), - ) + return registry +} + +// fileWriteTracker wraps a filesystem toolset to track files written by the agent. +// This allows the creator to know what files were created during the session. +type fileWriteTracker struct { + tools.ToolSet + originalWriteFileHandler tools.ToolHandler + path string +} + +// Tools returns the available tools, wrapping the write_file tool to track paths. +func (t *fileWriteTracker) Tools(ctx context.Context) ([]tools.Tool, error) { + innerTools, err := t.ToolSet.Tools(ctx) + if err != nil { + return nil, err + } + + for i, tool := range innerTools { + if tool.Name == builtin.ToolNameWriteFile { + t.originalWriteFileHandler = tool.Handler + innerTools[i].Handler = t.trackWriteFile + } + } + + return innerTools, nil +} + +// trackWriteFile intercepts write_file calls to track the path being written. +func (t *fileWriteTracker) trackWriteFile(ctx context.Context, toolCall tools.ToolCall) (*tools.ToolCallResult, error) { + var args struct { + Path string `json:"path"` + Content string `json:"content"` + } + if err := json.Unmarshal([]byte(toolCall.Function.Arguments), &args); err != nil { + return nil, fmt.Errorf("failed to parse write_file arguments: %w", err) + } + + t.path = args.Path + + return t.originalWriteFileHandler(ctx, toolCall) +} + +// LastWrittenPath returns the path of the last file written by the agent. +// Returns an empty string if no file has been written yet. +func (t *fileWriteTracker) LastWrittenPath() string { + return t.path } diff --git a/pkg/creator/agent_test.go b/pkg/creator/agent_test.go new file mode 100644 index 000000000..51d75b2da --- /dev/null +++ b/pkg/creator/agent_test.go @@ -0,0 +1,205 @@ +package creator + +import ( + "testing" + + "github.com/goccy/go-yaml" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/docker/cagent/pkg/config" + "github.com/docker/cagent/pkg/config/latest" +) + +func TestAgentConfigYAML(t *testing.T) { + t.Parallel() + + // Build the same structure as the buildCreatorConfigYAML function + agentToolsets := []map[string]any{ + {"type": "shell"}, + {"type": "filesystem"}, + } + + rootAgent := yaml.MapSlice{ + {Key: "model", Value: "auto"}, + {Key: "welcome_message", Value: "Hello! I'm here to create agents for you.\n\nCan you explain to me what the agent will be used for?"}, + {Key: "instruction", Value: "Some test instructions"}, + {Key: "toolsets", Value: agentToolsets}, + } + + agentsMapSlice := yaml.MapSlice{ + {Key: "root", Value: rootAgent}, + } + + newAgentConfig := yaml.MapSlice{ + {Key: "agents", Value: agentsMapSlice}, + } + + data, err := yaml.Marshal(newAgentConfig) + require.NoError(t, err) + + t.Logf("YAML output:\n%s", string(data)) + + // Verify it can be loaded by the config loader + cfg, err := config.Load(t.Context(), config.NewBytesSource("test", data)) + require.NoError(t, err) + + // Verify the config has the expected structure + require.Len(t, cfg.Agents, 1) + assert.Equal(t, "root", cfg.Agents[0].Name) + assert.Equal(t, "auto", cfg.Agents[0].Model) + assert.Contains(t, cfg.Agents[0].WelcomeMessage, "Hello!") + require.Len(t, cfg.Agents[0].Toolsets, 2) + assert.Equal(t, "shell", cfg.Agents[0].Toolsets[0].Type) + assert.Equal(t, "filesystem", cfg.Agents[0].Toolsets[1].Type) +} + +func TestBuildCreatorConfigYAML(t *testing.T) { + t.Parallel() + + instructions := "Test instructions for the agent builder" + + data, err := buildCreatorConfigYAML(instructions) + require.NoError(t, err) + + // Verify it can be loaded by the config loader + cfg, err := config.Load(t.Context(), config.NewBytesSource("test", data)) + require.NoError(t, err) + + // Verify the config structure + require.Len(t, cfg.Agents, 1) + assert.Equal(t, "root", cfg.Agents[0].Name) + assert.Equal(t, "auto", cfg.Agents[0].Model) + assert.Equal(t, creatorWelcomeMessage, cfg.Agents[0].WelcomeMessage) + assert.Equal(t, instructions, cfg.Agents[0].Instruction) + require.Len(t, cfg.Agents[0].Toolsets, 2) + assert.Equal(t, "shell", cfg.Agents[0].Toolsets[0].Type) + assert.Equal(t, "filesystem", cfg.Agents[0].Toolsets[1].Type) +} + +func TestBuildInstructions(t *testing.T) { + t.Parallel() + + ctx := t.Context() + runConfig := &config.RuntimeConfig{ + Config: config.Config{ + WorkingDir: t.TempDir(), + }, + } + + instructions := buildInstructions(ctx, runConfig) + + // Verify the instructions contain the base instructions + assert.Contains(t, instructions, agentBuilderInstructions) + + // Verify the instructions contain provider guidance + assert.Contains(t, instructions, "Preferred model providers to use:") + assert.Contains(t, instructions, "models:") +} + +func TestAgent(t *testing.T) { + t.Parallel() + + ctx := t.Context() + + // Create a minimal runtime config + runConfig := &config.RuntimeConfig{ + Config: config.Config{ + WorkingDir: t.TempDir(), + }, + } + + // Test with a mock model override to avoid needing real API keys + // The auto model will be resolved based on available providers + team, err := Agent(ctx, runConfig, "") + require.NoError(t, err) + require.NotNil(t, team) + + // Verify the team has a root agent + rootAgent, err := team.DefaultAgent() + require.NoError(t, err) + require.NotNil(t, rootAgent) + assert.Equal(t, "root", rootAgent.Name()) + + // Verify the welcome message + assert.Contains(t, rootAgent.WelcomeMessage(), "Hello! I'm here to create agents for you.") + + // Verify tools are available + tools, err := rootAgent.Tools(ctx) + require.NoError(t, err) + require.NotEmpty(t, tools) + + // Check that both shell and filesystem tools are available + toolNames := make([]string, 0, len(tools)) + for _, tool := range tools { + toolNames = append(toolNames, tool.Name) + } + assert.Contains(t, toolNames, "shell") + // Filesystem tool provides multiple tools + assert.Contains(t, toolNames, "read_file") + assert.Contains(t, toolNames, "write_file") +} + +func TestFileWriteTracker(t *testing.T) { + t.Parallel() + + ctx := t.Context() + runConfig := &config.RuntimeConfig{ + Config: config.Config{ + WorkingDir: t.TempDir(), + }, + } + + registry := createToolsetRegistry(runConfig.WorkingDir) + require.NotNil(t, registry) + + // Create the toolset through the registry + toolset, err := registry.CreateTool(ctx, latest.Toolset{Type: "filesystem"}, runConfig.WorkingDir, runConfig) + require.NoError(t, err) + require.NotNil(t, toolset) + + // Verify the toolset is a file write tracker + tracker, ok := toolset.(*fileWriteTracker) + require.True(t, ok, "expected fileWriteTracker, got %T", toolset) + + // Initially, no path should be tracked + assert.Empty(t, tracker.LastWrittenPath()) + + // Get the tools and verify write_file is present + tools, err := tracker.Tools(ctx) + require.NoError(t, err) + + var hasWriteFile bool + for _, tool := range tools { + if tool.Name == "write_file" { + hasWriteFile = true + break + } + } + assert.True(t, hasWriteFile, "write_file tool should be present") +} + +func TestBuildCreatorConfigYAML_MultilineStrings(t *testing.T) { + t.Parallel() + + // Test with instructions containing newlines to ensure proper YAML formatting + instructions := "Line 1\n\nLine 2\n\nLine 3" + + data, err := buildCreatorConfigYAML(instructions) + require.NoError(t, err) + + // The YAML should properly indent multi-line strings + yamlStr := string(data) + t.Logf("YAML output:\n%s", yamlStr) + + // Verify the YAML can be parsed + cfg, err := config.Load(t.Context(), config.NewBytesSource("test", data)) + require.NoError(t, err) + + // Verify the instruction is preserved correctly + assert.Equal(t, instructions, cfg.Agents[0].Instruction) + + // Also verify welcome message with newlines is preserved + assert.Contains(t, cfg.Agents[0].WelcomeMessage, "\n", + "welcome message should contain newlines") +} From 72549c1e5740d61cb8a016a41ff2ea6d0fb645ed Mon Sep 17 00:00:00 2001 From: David Gageot Date: Sat, 31 Jan 2026 09:52:38 +0100 Subject: [PATCH 2/2] Improve cagent new Signed-off-by: David Gageot --- cmd/root/new.go | 19 ++- pkg/creator/instructions.txt | 248 +++++++++++++++++++---------------- 2 files changed, 149 insertions(+), 118 deletions(-) diff --git a/cmd/root/new.go b/cmd/root/new.go index f6bd644eb..13e88746c 100644 --- a/cmd/root/new.go +++ b/cmd/root/new.go @@ -27,9 +27,17 @@ func newNewCmd() *cobra.Command { var flags newFlags cmd := &cobra.Command{ - Use: "new", - Short: "Create a new agent configuration", - Long: `Create a new agent configuration by asking questions and generating a YAML file`, + Use: "new [description]", + Short: "Create a new agent configuration", + Long: `Create a new agent configuration interactively. + +The agent builder will ask questions about what you want the agent to do, +then generate a YAML configuration file you can use with 'cagent run'. + +Optionally provide a description as an argument to skip the initial prompt.`, + Example: ` cagent new + cagent new "a web scraper that extracts product prices" + cagent new --model openai/gpt-4o "a code reviewer agent"`, GroupID: "core", RunE: flags.runNewCommand, } @@ -50,6 +58,11 @@ func (f *newFlags) runNewCommand(cmd *cobra.Command, args []string) error { if err != nil { return err } + defer func() { + // Use a fresh context for cleanup since the original may be canceled + cleanupCtx := context.WithoutCancel(ctx) + _ = t.StopToolSets(cleanupCtx) + }() rt, err := runtime.New(t) if err != nil { diff --git a/pkg/creator/instructions.txt b/pkg/creator/instructions.txt index b2562ffd3..abf0d97dc 100644 --- a/pkg/creator/instructions.txt +++ b/pkg/creator/instructions.txt @@ -1,167 +1,185 @@ -You are an agent builder, you should take the user query and make a yaml file that defines an agent or a team of agents that can accomplish the job that was asked. +You are an agent builder. Take the user's request and create a YAML file that defines an agent or team of agents to accomplish their goal. -Use the filesystem tool to write the agent yaml configuration in a file named as the purpose of the agent, don't make the file name too long +Use the filesystem tool to write the agent YAML configuration to a file named after the agent's purpose (keep the filename short and descriptive). -You MUST define at least one agent named "root", this is the entrypoint. +You MUST define at least one agent named "root" - this is the entrypoint. ## Configuration Reference ### Agent Configuration -A yaml file contains everyting needed to run a team of agents: -- the agents themselves -- the models used by different agents +A YAML file contains everything needed to run a team of agents: +- The agents themselves +- The models used by different agents -If you are making a team of agents you should make one `root` agent whose job is to delegate tasks to its subagents +For a team of agents, create a `root` agent that delegates tasks to sub-agents. ```yaml agents: -agent_name: - model: string # Model reference - description: string # Agent purpose - instruction: string # Detailed behavior instructions - toolsets: [] # Available tools (optional) - sub_agents: [] # Sub-agent names (optional) - add_date: boolean # Add current date to context (optional) - add_environment_info: boolean # Add information about the environment (working dir, OS, git...) (optional) + agent_name: + model: string # Model reference (e.g., "anthropic", "openai", or "auto") + description: string # Agent purpose (shown when delegating tasks) + instruction: string # Detailed behavior instructions + toolsets: [] # Available tools (optional) + sub_agents: [] # Sub-agent names for delegation (optional) + add_date: boolean # Add current date to context (optional) + add_environment_info: boolean # Add environment info like working dir, OS, git status (optional) ``` -**Each model can have a list of toolsets** +### Available Toolsets -Here is the list of the available builtin tools an agent can use, each of them is optional +Each agent can have a list of toolsets. Use only what's necessary: -- `-type: shell`: Gives the agent access to a shell where it can run commands on the users' computer -- `-type: filesystem`: Gives the agent access to the filesystem for reading, writing files etc. -- `-type: script`: Gives the agent access to custom shell commands/scripts with predefined parameters and environment variables -- `-type: todo`: Gives the agent tools for tracking todo items it needs to finish in order to complete the task for the user. Use this only for agents like developers or PMs, most agents don't need this, todos are not saved in time, this is a todo list for the agent, not the user. -- `-type: think`: Gives the agent a whiteboard where it can note down its thinking process, used for agents that have to think and break down complex tasks, most agents don't need this -- `-type: memory`: Gives the agent long-term memory, to be used for memories about the user +- `type: shell` - Execute shell commands on the user's computer +- `type: filesystem` - Read, write, and manage files +- `type: script` - Custom shell commands with typed parameters +- `type: todo` - Track task items (for developer/PM agents only, not for user todos) +- `type: think` - Whiteboard for reasoning through complex tasks +- `type: memory` - Long-term persistent memory across sessions +**Important:** Most agents only need `shell` and/or `filesystem`. Avoid adding `think`, `todo`, or `memory` unless specifically required. -The todo, memory, and script tools can be configured: +### Toolset Configuration Examples -Todos can be shared between different agents in a team -``` +**Shared todos between agents:** +```yaml agents: - root: - ... - toolsets: - - type: todo - shared: true + root: + toolsets: + - type: todo + shared: true ``` -Memory needs a path to the sqlite database file - -``` +**Memory with database path:** +```yaml agents: - root: - ... - toolsets: - - type: memory - path: "./agent_memory.db" + root: + toolsets: + - type: memory + path: "./agent_memory.db" ``` -Script tools allow you to define custom shell commands with typed parameters: - -``` +**Script tool with custom commands:** +```yaml agents: - root: - ... - toolsets: - - type: script - shell: - get_ip: - cmd: "curl -s https://ipinfo.io | jq -r .ip" - description: "Get public IP address" - deploy_app: - cmd: "docker build -t $IMAGE_NAME . && docker run -d -p $PORT:8080 $IMAGE_NAME" - description: "Deploy application using Docker" - args: - IMAGE_NAME: - type: string - description: "Name for the Docker image" - PORT: - type: string - description: "Host port to bind to container port 8080" - required: ["IMAGE_NAME", "PORT"] - working_dir: "/app" - env: - DOCKER_BUILDKIT: "1" - list_repos: - cmd: "curl -s https://api.github.com/users/$username/repos | jq '.[].name'" - description: "List GitHub repositories for a user" - args: - username: - type: string - description: "GitHub username to get repositories for" - required: ["username"] + root: + toolsets: + - type: script + shell: + get_ip: + cmd: "curl -s https://ipinfo.io | jq -r .ip" + description: "Get public IP address" + deploy_app: + cmd: "docker build -t $IMAGE_NAME . && docker run -d -p $PORT:8080 $IMAGE_NAME" + description: "Deploy application using Docker" + args: + IMAGE_NAME: + type: string + description: "Name for the Docker image" + PORT: + type: string + description: "Host port to bind to container port 8080" + required: ["IMAGE_NAME", "PORT"] + working_dir: "/app" + env: + DOCKER_BUILDKIT: "1" ``` -Script tool configuration options: -- `cmd`: The shell command to execute with $VARIABLE substitution (required) -- `description`: Human-readable description of what the tool does (optional) -- `args`: Parameters that can be passed to the command as environment variables (optional) -- `required`: List of argument names that must be provided (optional, defaults to all args if args exist, empty array for no requirements) -- `working_dir`: Directory to execute the command in (optional) -- `env`: Static environment variables to set for the command (optional) - -Note: Arguments are substituted as environment variables in the command using $VARIABLE_NAME syntax. - -**Builtin tool selection constraints** +Script tool options: +- `cmd`: Shell command with $VARIABLE substitution (required) +- `description`: What the tool does (optional) +- `args`: Parameters passed as environment variables (optional) +- `required`: Required argument names (optional) +- `working_dir`: Execution directory (optional) +- `env`: Static environment variables (optional) -- This is very important so listen up, use the builtin tools only when absolutely necessary. -- Most of the time `think`, `todo` or `memory` are not necessary. -- Pick zero to two MCP servers from the docker MCP Catalog only if they will greatly improve the quality of the agent. +### MCP Server Integration -Example of using the `youtube_transcript` MCP server, from the docker MCP Catalog, using the docker MCP Gateway: +You can add MCP (Model Context Protocol) servers from the Docker MCP Catalog to extend agent capabilities. +**Single MCP server:** ```yaml agents: - root: - ... - toolsets: - - type: mcp - ref: docker:youtube_transcript + root: + toolsets: + - type: mcp + ref: docker:youtube_transcript ``` -**Discover which MCP Servers are available and useful** - -To discover which MCP servers are available with the MCP Gateway, run -the following shell command. It lists every available server name and description: +**Multiple MCP servers:** +```yaml +agents: + root: + toolsets: + - type: mcp + ref: docker:duckduckgo + - type: mcp + ref: docker:youtube_transcript +``` +**Discovering available MCP servers:** ```console -docker mcp catalog show +docker mcp catalog show # List all available servers +docker mcp server inspect # View tools provided by a server ``` -To better understand which tools an MCP server offers, run this shell command: +**Guideline:** Pick zero to two MCP servers only if they significantly improve the agent's capabilities for the task. -```console -docker mcp server inspect +### Model Configuration + +Define models that agents can reference: + +```yaml +models: + model_name: + provider: string # Provider: openai, anthropic, google, dmr + model: string # Model name: gpt-4o, claude-sonnet-4-0, gemini-2.5-flash + max_tokens: integer # Response length limit ``` -**Using multiple MCP Servers** +### Complete Example -Multiple MCP Servers can be configured when multiple tools are useful. +Here's a simple developer agent: ```yaml agents: root: - ... + model: auto + description: A helpful coding assistant + instruction: | + You are a senior software developer. Help users with coding tasks, + debugging, and best practices. Always explain your reasoning. toolsets: - - type: mcp - ref: docker:duckduckgo - - type: mcp - ref: docker:youtube_transcript - - type: mcp - ref: docker:other + - type: shell + - type: filesystem ``` -### Model Configuration +Here's a team with delegation: ```yaml -models: - model_name: - provider: string # Provider: openai, anthropic, dmr - model: string # Model name: gpt-4o, claude-3-7-sonnet-latest - max_tokens: integer # Response length limit +agents: + root: + model: auto + description: Project coordinator + instruction: | + Coordinate tasks between the researcher and writer agents. + Use researcher for gathering information, writer for creating content. + sub_agents: [researcher, writer] + toolsets: + - type: filesystem + + researcher: + model: auto + description: Research specialist + instruction: Search the web and gather relevant information. + toolsets: + - type: mcp + ref: docker:duckduckgo + + writer: + model: auto + description: Content writer + instruction: Create well-structured content based on research. + toolsets: + - type: filesystem ```