Skip to content

Commit a17483e

Browse files
authored
Merge pull request #634 from dgageot/remote-commands
Implement /commands everywhere
2 parents 6d28ce6 + 7dc3436 commit a17483e

10 files changed

Lines changed: 68 additions & 171 deletions

File tree

cagent-schema.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@
109109
}
110110
},
111111
"commands": {
112-
"description": "Named prompts for quick-start commands used with --command/-c",
112+
"description": "Named prompts for /commands",
113113
"oneOf": [
114114
{
115115
"type": "object",

cmd/root/run.go

Lines changed: 2 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99
"log/slog"
1010
"os"
1111
"path/filepath"
12-
"sort"
1312
"strings"
1413
"time"
1514

@@ -21,7 +20,6 @@ import (
2120
"github.com/docker/cagent/pkg/aliases"
2221
"github.com/docker/cagent/pkg/app"
2322
"github.com/docker/cagent/pkg/chat"
24-
"github.com/docker/cagent/pkg/config"
2523
"github.com/docker/cagent/pkg/content"
2624
"github.com/docker/cagent/pkg/evaluation"
2725
"github.com/docker/cagent/pkg/input"
@@ -42,12 +40,9 @@ var (
4240
useTUI bool
4341
remoteAddress string
4442
dryRun bool
45-
commandName string
4643
modelOverrides []string
4744
)
4845

49-
const commandListSentinel = "__LIST__"
50-
5146
// NewRunCmd creates a new run command
5247
func NewRunCmd() *cobra.Command {
5348
cmd := &cobra.Command{
@@ -68,12 +63,7 @@ func NewRunCmd() *cobra.Command {
6863
cmd.PersistentFlags().StringVar(&attachmentPath, "attach", "", "Attach an image file to the message")
6964
cmd.PersistentFlags().BoolVar(&useTUI, "tui", true, "Run the agent with a Terminal User Interface (TUI)")
7065
cmd.PersistentFlags().StringVar(&remoteAddress, "remote", "", "Use remote runtime with specified address (only supported with TUI)")
71-
cmd.PersistentFlags().StringVarP(&commandName, "command", "c", "", "Run a named command from the agent's commands section")
7266
cmd.PersistentFlags().StringArrayVar(&modelOverrides, "model", nil, "Override agent model: [agent=]provider/model (repeatable)")
73-
if f := cmd.PersistentFlags().Lookup("command"); f != nil {
74-
// Allow `-c` without value to list available commands
75-
f.NoOptDefVal = commandListSentinel
76-
}
7767
addGatewayFlags(cmd)
7868
addRuntimeConfigFlags(cmd)
7969

@@ -208,52 +198,6 @@ func doRunCommand(ctx context.Context, args []string, exec bool) error {
208198
slog.Debug("Skipping local agent file loading for remote runtime", "filename", agentFilename)
209199
}
210200

211-
// Resolve --command/-c into a first message if provided
212-
var commandFirstMessage *string
213-
if trimmed := strings.TrimSpace(commandName); trimmed != "" {
214-
// Handle listing commands when -c is provided without a value
215-
if trimmed == commandListSentinel {
216-
// If the next positional arg looks like a value (not a flag), treat it as the command value.
217-
if len(args) == 2 && !strings.HasPrefix(args[1], "-") {
218-
trimmed = args[1]
219-
// consume the positional so it won't be treated as a message later
220-
args = args[:1]
221-
} else {
222-
cmds, err := getCommandsForAgent(agentFilename, remoteAddress != "", agents, agentName)
223-
if err != nil {
224-
return err
225-
}
226-
if len(cmds) == 0 {
227-
return fmt.Errorf("no commands defined for agent '%s'", agentName)
228-
}
229-
printAvailableCommands(agentName, cmds)
230-
fmt.Println()
231-
return nil
232-
}
233-
}
234-
235-
if len(args) == 2 {
236-
return fmt.Errorf("cannot use --command (-c) together with a message argument")
237-
}
238-
239-
cmds, err := getCommandsForAgent(agentFilename, remoteAddress != "", agents, agentName)
240-
if err != nil {
241-
return err
242-
}
243-
if len(cmds) == 0 {
244-
return fmt.Errorf("agent '%s' has no commands", agentName)
245-
}
246-
if msg, ok := cmds[trimmed]; ok {
247-
commandFirstMessage = &msg
248-
} else {
249-
var names []string
250-
for k := range cmds {
251-
names = append(names, k)
252-
}
253-
return fmt.Errorf("'%s' is an unknown command.\n\nAvailable: %s", trimmed, strings.Join(names, ", "))
254-
}
255-
}
256-
257201
// Validate remote flag usage
258202
if remoteAddress != "" && (!useTUI || exec) {
259203
return fmt.Errorf("--remote flag can only be used with TUI mode")
@@ -329,10 +273,6 @@ func doRunCommand(ctx context.Context, args []string, exec bool) error {
329273

330274
// For `cagent run --tui=false`
331275
if !useTUI {
332-
// Inject first message for non-TUI if --command was used
333-
if commandFirstMessage != nil {
334-
args = []string{args[0], *commandFirstMessage}
335-
}
336276
return runWithoutTUI(ctx, agentFilename, rt, sess, args)
337277
}
338278

@@ -352,11 +292,6 @@ func doRunCommand(ctx context.Context, args []string, exec bool) error {
352292
}
353293
}
354294

355-
// Override firstMessage if --command was provided (cannot be combined with a message arg)
356-
if commandFirstMessage != nil {
357-
firstMessage = commandFirstMessage
358-
}
359-
360295
a := app.New("cagent", agentFilename, rt, agents, sess, firstMessage)
361296
m := tui.New(a)
362297

@@ -397,11 +332,12 @@ func runWithoutTUI(ctx context.Context, agentFilename string, rt runtime.Runtime
397332
return nil
398333
}
399334

335+
userInput = runtime.ResolveCommand(ctx, rt, userInput)
336+
400337
handled, err := runUserCommand(userInput, sess, rt, ctx)
401338
if err != nil {
402339
return err
403340
}
404-
405341
if handled {
406342
return nil
407343
}
@@ -839,52 +775,3 @@ func fileToDataURL(filePath string) (string, error) {
839775

840776
return dataURL, nil
841777
}
842-
843-
// getCommandsForAgent returns the commands map for the selected agent,
844-
// loading from the in-memory team for local runs or from the YAML file for remote runs.
845-
func getCommandsForAgent(agentFilename string, isRemote bool, agents *team.Team, agentName string) (map[string]string, error) {
846-
if !isRemote {
847-
if agents == nil {
848-
return nil, fmt.Errorf("failed to load agent team")
849-
}
850-
851-
ag, err := agents.Agent(agentName)
852-
if err != nil {
853-
return nil, err
854-
}
855-
856-
return ag.Commands(), nil
857-
}
858-
859-
parentDir := filepath.Dir(agentFilename)
860-
fileName := filepath.Base(agentFilename)
861-
root, err := os.OpenRoot(parentDir)
862-
if err != nil {
863-
return nil, fmt.Errorf("failed to open root: %w", err)
864-
}
865-
defer func() {
866-
if err := root.Close(); err != nil {
867-
slog.Error("Failed to close root", "error", err)
868-
}
869-
}()
870-
871-
cfg, err := config.LoadConfig(fileName, root)
872-
if err != nil {
873-
return nil, fmt.Errorf("failed to load agent config: %w", err)
874-
}
875-
876-
return cfg.Agents[agentName].Commands, nil
877-
}
878-
879-
// printAvailableCommands pretty-prints the agent's commands sorted by name.
880-
func printAvailableCommands(agentName string, cmds map[string]string) {
881-
fmt.Printf("Available commands for agent '%s':\n", agentName)
882-
var names []string
883-
for k := range cmds {
884-
names = append(names, k)
885-
}
886-
sort.Strings(names)
887-
for _, n := range names {
888-
fmt.Printf(" - %s: %s\n", n, cmds[n])
889-
}
890-
}

docs/USAGE.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ During CLI sessions, you can use special commands:
9696
| `add_date` | boolean | Add current date to context ||
9797
| `add_environment_info` | boolean | Add information about the environment (working dir, OS, git...) ||
9898
| `max_iterations` | int | Specifies how many times the agent can loop when using tools ||
99-
| `commands` | object/array | Named prompts for quick-start commands (used with `--command`) ||
99+
| `commands` | object/array | Named prompts for /commands ||
100100

101101
#### Example
102102

@@ -118,7 +118,6 @@ agents:
118118
119119
### Running with named commands
120120
121-
- Use `--command` (or `-c`) to send a predefined prompt from the agent config as the first message.
122121
- Example YAML forms supported:
123122
124123
```yaml
@@ -136,8 +135,8 @@ commands:
136135
Run:
137136
138137
```bash
139-
cagent run ./agent.yaml -c df
140-
cagent run ./agent.yaml --command ls
138+
cagent run ./agent.yaml /df
139+
cagent run ./agent.yaml /ls
141140
```
142141

143142
### Model Properties

pkg/app/app.go

Lines changed: 4 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package app
33
import (
44
"context"
55
"os/exec"
6-
"strings"
76
"time"
87

98
tea "github.com/charmbracelet/bubbletea/v2"
@@ -51,45 +50,13 @@ func (a *App) Title() string {
5150
}
5251

5352
// CurrentAgentCommands returns the commands for the active agent
54-
func (a *App) CurrentAgentCommands() map[string]string {
55-
if localRuntime, ok := a.runtime.(*runtime.LocalRuntime); ok {
56-
agent := localRuntime.CurrentAgent()
57-
if agent == nil {
58-
return nil
59-
}
60-
61-
return agent.Commands()
62-
}
63-
64-
// TODO(dga): not properly implemented for remote agents
65-
return map[string]string{}
53+
func (a *App) CurrentAgentCommands(ctx context.Context) map[string]string {
54+
return a.runtime.CurrentAgentCommands(ctx)
6655
}
6756

6857
// ResolveCommand converts /command to its prompt text
69-
func (a *App) ResolveCommand(input string) string {
70-
if !strings.HasPrefix(input, "/") {
71-
return input
72-
}
73-
74-
trimmed := strings.TrimSpace(input)
75-
parts := strings.Fields(trimmed)
76-
if len(parts) == 0 {
77-
return input
78-
}
79-
80-
cmdName := strings.TrimPrefix(parts[0], "/")
81-
commands := a.CurrentAgentCommands()
82-
83-
if prompt, ok := commands[cmdName]; ok {
84-
// If there are additional arguments, append them to the prompt
85-
if len(parts) > 1 {
86-
args := strings.Join(parts[1:], " ")
87-
return prompt + " " + args
88-
}
89-
return prompt
90-
}
91-
92-
return input
58+
func (a *App) ResolveCommand(ctx context.Context, userInput string) string {
59+
return runtime.ResolveCommand(ctx, a.runtime, userInput)
9360
}
9461

9562
// Run one agent loop

pkg/runtime/commands.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package runtime
2+
3+
import (
4+
"context"
5+
"strings"
6+
)
7+
8+
func ResolveCommand(ctx context.Context, rt Runtime, userInput string) string {
9+
if !strings.HasPrefix(userInput, "/") {
10+
return userInput
11+
}
12+
13+
cmd, rest, _ := strings.Cut(userInput, " ")
14+
prompt, found := rt.CurrentAgentCommands(ctx)[cmd[1:]]
15+
if found {
16+
userInput = prompt
17+
if rest != "" {
18+
userInput += " " + rest
19+
}
20+
}
21+
22+
return userInput
23+
}

pkg/runtime/remote_runtime.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,21 @@ func (r *RemoteRuntime) CurrentAgentName() string {
6262
return r.currentAgent
6363
}
6464

65+
func (r *RemoteRuntime) CurrentAgentCommands(ctx context.Context) map[string]string {
66+
cfg, err := r.client.GetAgent(ctx, r.agentFilename)
67+
if err != nil {
68+
return map[string]string{}
69+
}
70+
71+
for agentName, agent := range cfg.Agents {
72+
if agentName == r.currentAgent {
73+
return agent.Commands
74+
}
75+
}
76+
77+
return map[string]string{}
78+
}
79+
6580
// RunStream starts the agent's interaction loop and returns a channel of events
6681
func (r *RemoteRuntime) RunStream(ctx context.Context, sess *session.Session) <-chan Event {
6782
slog.Debug("Starting remote runtime stream", "agent", r.currentAgent, "session_id", r.sessionID)

pkg/runtime/runtime.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ type ElicitationRequestHandler func(ctx context.Context, message string, schema
6565
type Runtime interface {
6666
// CurrentAgentName returns the name of the currently active agent
6767
CurrentAgentName() string
68+
// CurrentAgentCommands returns the commands for the active agent
69+
CurrentAgentCommands(ctx context.Context) map[string]string
6870
// RunStream starts the agent's interaction loop and returns a channel of events
6971
RunStream(ctx context.Context, sess *session.Session) <-chan Event
7072
// Run starts the agent's interaction loop and returns the final messages
@@ -176,6 +178,10 @@ func (r *LocalRuntime) CurrentAgentName() string {
176178
return r.currentAgent
177179
}
178180

181+
func (r *LocalRuntime) CurrentAgentCommands(context.Context) map[string]string {
182+
return r.CurrentAgent().Commands()
183+
}
184+
179185
// CurrentAgent returns the current agent
180186
func (r *LocalRuntime) CurrentAgent() *agent.Agent {
181187
// We validated already that the agent exists

pkg/tui/components/editor/editor.go

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,13 @@ type Editor interface {
2828
// editor implements Editor
2929
type editor struct {
3030
textarea *textarea.Model
31-
resolver func(string) string
3231
width int
3332
height int
3433
working bool
3534
}
3635

3736
// New creates a new editor component
38-
func New(resolver func(string) string) Editor {
37+
func New() Editor {
3938
ta := textarea.New()
4039
ta.SetStyles(styles.InputStyle)
4140
ta.Placeholder = "Type your message here..."
@@ -49,7 +48,6 @@ func New(resolver func(string) string) Editor {
4948

5049
return &editor{
5150
textarea: ta,
52-
resolver: resolver,
5351
}
5452
}
5553

@@ -73,11 +71,9 @@ func (e *editor) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
7371
value := e.textarea.Value()
7472
if value != "" && !e.working {
7573
e.textarea.Reset()
76-
// Resolve command before sending
77-
if e.resolver != nil {
78-
value = e.resolver(value)
79-
}
80-
return e, core.CmdHandler(SendMsg{Content: value})
74+
return e, core.CmdHandler(SendMsg{
75+
Content: value,
76+
})
8177
}
8278
return e, nil
8379
case "ctrl+c":

0 commit comments

Comments
 (0)