Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion cmd/root/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type runExecFlags struct {
remoteAddress string
connectRPC bool
modelOverrides []string
promptFiles []string
dryRun bool
runConfig config.RuntimeConfig
sessionDB string
Expand Down Expand Up @@ -82,6 +83,7 @@ func addRunOrExecFlags(cmd *cobra.Command, flags *runExecFlags) {
cmd.PersistentFlags().BoolVar(&flags.autoApprove, "yolo", false, "Automatically approve all tool calls without prompting")
cmd.PersistentFlags().BoolVar(&flags.hideToolResults, "hide-tool-results", false, "Hide tool call results")
cmd.PersistentFlags().StringVar(&flags.attachmentPath, "attach", "", "Attach an image file to the message")
cmd.PersistentFlags().StringArrayVar(&flags.promptFiles, "prompt-file", nil, "Append file contents to the prompt (repeatable)")
cmd.PersistentFlags().StringArrayVar(&flags.modelOverrides, "model", nil, "Override agent model: [agent=]provider/model (repeatable)")
cmd.PersistentFlags().BoolVar(&flags.dryRun, "dry-run", false, "Initialize the agent without executing anything")
cmd.PersistentFlags().StringVar(&flags.remoteAddress, "remote", "", "Use remote runtime with specified address")
Expand Down Expand Up @@ -257,7 +259,14 @@ func (f *runExecFlags) runOrExec(ctx context.Context, out *cli.Printer, args []s
}

func (f *runExecFlags) loadAgentFrom(ctx context.Context, agentSource config.Source) (*teamloader.LoadResult, error) {
result, err := teamloader.LoadWithConfig(ctx, agentSource, &f.runConfig, teamloader.WithModelOverrides(f.modelOverrides))
opts := []teamloader.Opt{
teamloader.WithModelOverrides(f.modelOverrides),
}
if len(f.promptFiles) > 0 {
opts = append(opts, teamloader.WithPromptFiles(f.promptFiles))
}

result, err := teamloader.LoadWithConfig(ctx, agentSource, &f.runConfig, opts...)
if err != nil {
return nil, err
}
Expand Down
27 changes: 26 additions & 1 deletion pkg/teamloader/teamloader.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ func isThinkingBudgetDisabled(tb *latest.ThinkingBudget) bool {

type loadOptions struct {
modelOverrides []string
promptFiles []string
toolsetRegistry *ToolsetRegistry
}

Expand All @@ -55,6 +56,15 @@ func WithModelOverrides(overrides []string) Opt {
}
}

// WithPromptFiles adds additional prompt files to all agents.
// These are merged with any prompt files defined in the agent config.
func WithPromptFiles(files []string) Opt {
return func(opts *loadOptions) error {
opts.promptFiles = files
return nil
}
}

// WithToolsetRegistry allows using a custom toolset registry instead of the default
func WithToolsetRegistry(registry *ToolsetRegistry) Opt {
return func(opts *loadOptions) error {
Expand Down Expand Up @@ -143,14 +153,29 @@ func LoadWithConfig(ctx context.Context, agentSource config.Source, runConfig *c
skillsEnabled = *agentConfig.Skills
}

// Merge CLI prompt files with agent config prompt files, deduplicating
promptFiles := append([]string{}, agentConfig.AddPromptFiles...)
promptFiles = append(promptFiles, loadOpts.promptFiles...)

// Deduplicate to avoid redundant context (saves tokens)
seen := make(map[string]bool)
unique := promptFiles[:0]
for _, f := range promptFiles {
if !seen[f] {
seen[f] = true
unique = append(unique, f)
}
}
promptFiles = unique

opts := []agent.Opt{
agent.WithName(agentConfig.Name),
agent.WithDescription(expander.Expand(ctx, agentConfig.Description)),
agent.WithWelcomeMessage(expander.Expand(ctx, agentConfig.WelcomeMessage)),
agent.WithAddDate(agentConfig.AddDate),
agent.WithAddEnvironmentInfo(agentConfig.AddEnvironmentInfo),
agent.WithAddDescriptionParameter(agentConfig.AddDescriptionParameter),
agent.WithAddPromptFiles(agentConfig.AddPromptFiles),
agent.WithAddPromptFiles(promptFiles),
agent.WithMaxIterations(agentConfig.MaxIterations),
agent.WithNumHistoryItems(agentConfig.NumHistoryItems),
agent.WithCommands(expander.ExpandCommands(ctx, agentConfig.Commands)),
Expand Down
111 changes: 111 additions & 0 deletions pkg/teamloader/teamloader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -287,3 +287,114 @@ func TestIsThinkingBudgetDisabled(t *testing.T) {
})
}
}

func TestWithPromptFiles(t *testing.T) {
t.Setenv("OPENAI_API_KEY", "dummy")

tests := []struct {
name string
cliPromptFiles []string
expected []string
}{
{
name: "no CLI prompt files",
cliPromptFiles: nil,
expected: []string{}, // basic.yaml has no add_prompt_files
},
{
name: "single CLI prompt file",
cliPromptFiles: []string{"AGENTS.md"},
expected: []string{"AGENTS.md"},
},
{
name: "multiple CLI prompt files",
cliPromptFiles: []string{"AGENTS.md", "CLAUDE.md"},
expected: []string{"AGENTS.md", "CLAUDE.md"},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
agentSource, err := config.Resolve("testdata/basic.yaml", nil)
require.NoError(t, err)

var opts []Opt
if len(tt.cliPromptFiles) > 0 {
opts = append(opts, WithPromptFiles(tt.cliPromptFiles))
}

team, err := Load(t.Context(), agentSource, &config.RuntimeConfig{}, opts...)
require.NoError(t, err)

rootAgent, err := team.Agent("root")
require.NoError(t, err)

assert.Equal(t, tt.expected, rootAgent.AddPromptFiles())
})
}
}

func TestWithPromptFilesMergesWithConfig(t *testing.T) {
t.Setenv("OPENAI_API_KEY", "dummy")

// Create a temp agent file with add_prompt_files configured
tempDir := t.TempDir()
agentFile := filepath.Join(tempDir, "agent.yaml")
agentYAML := `version: "2"
agents:
root:
model: openai/gpt-4o
instruction: test
add_prompt_files:
- config-file.md
`
require.NoError(t, os.WriteFile(agentFile, []byte(agentYAML), 0o644))

agentSource, err := config.Resolve(agentFile, nil)
require.NoError(t, err)

// Load with CLI prompt files - should merge with config
team, err := Load(t.Context(), agentSource, &config.RuntimeConfig{},
WithPromptFiles([]string{"cli-file.md"}))
require.NoError(t, err)

rootAgent, err := team.Agent("root")
require.NoError(t, err)

// Config files come first, then CLI files
expected := []string{"config-file.md", "cli-file.md"}
assert.Equal(t, expected, rootAgent.AddPromptFiles())
}

func TestWithPromptFilesDeduplicates(t *testing.T) {
t.Setenv("OPENAI_API_KEY", "dummy")

// Create a temp agent file with add_prompt_files configured
tempDir := t.TempDir()
agentFile := filepath.Join(tempDir, "agent.yaml")
agentYAML := `version: "2"
agents:
root:
model: openai/gpt-4o
instruction: test
add_prompt_files:
- AGENTS.md
- CLAUDE.md
`
require.NoError(t, os.WriteFile(agentFile, []byte(agentYAML), 0o644))

agentSource, err := config.Resolve(agentFile, nil)
require.NoError(t, err)

// CLI specifies a file that's already in config - should deduplicate
team, err := Load(t.Context(), agentSource, &config.RuntimeConfig{},
WithPromptFiles([]string{"AGENTS.md", "extra.md"}))
require.NoError(t, err)

rootAgent, err := team.Agent("root")
require.NoError(t, err)

// AGENTS.md should only appear once (from config), extra.md added at end
expected := []string{"AGENTS.md", "CLAUDE.md", "extra.md"}
assert.Equal(t, expected, rootAgent.AddPromptFiles())
}
Loading