Skip to content

Commit bffc997

Browse files
authored
Merge pull request #197 from rumpl/feat-remote-tui
Connect to a remote API when running a TUI
2 parents 54c3793 + 1ef52c1 commit bffc997

11 files changed

Lines changed: 917 additions & 178 deletions

File tree

cmd/root/run.go

Lines changed: 100 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"github.com/docker/cagent/pkg/remote"
3030
"github.com/docker/cagent/pkg/runtime"
3131
"github.com/docker/cagent/pkg/session"
32+
"github.com/docker/cagent/pkg/team"
3233
"github.com/docker/cagent/pkg/teamloader"
3334
)
3435

@@ -38,6 +39,7 @@ var (
3839
attachmentPath string
3940
workingDir string
4041
useTUI bool
42+
remoteAddress string
4143
)
4244

4345
// NewRunCmd creates a new run command
@@ -60,6 +62,7 @@ func NewRunCmd() *cobra.Command {
6062
cmd.PersistentFlags().BoolVar(&autoApprove, "yolo", false, "Automatically approve all tool calls without prompting")
6163
cmd.PersistentFlags().StringVar(&attachmentPath, "attach", "", "Attach an image file to the message")
6264
cmd.PersistentFlags().BoolVar(&useTUI, "tui", true, "Run the agent with a Terminal User Interface (TUI)")
65+
cmd.PersistentFlags().StringVar(&remoteAddress, "remote", "", "Use remote runtime with specified address (only supported with TUI)")
6366
addGatewayFlags(cmd)
6467

6568
return cmd
@@ -130,71 +133,114 @@ func doRunCommand(ctx context.Context, args []string, exec bool) error {
130133
slog.Debug("Working directory set", "dir", absWd)
131134
}
132135

133-
// Determine how to obtain the agent definition
134-
ext := strings.ToLower(filepath.Ext(agentFilename))
135-
if ext == ".yaml" || ext == ".yml" || strings.HasPrefix(agentFilename, "/dev/fd/") {
136-
// Treat as local YAML file: resolve to absolute path so later chdir doesn't break it
137-
if !strings.Contains(agentFilename, "\n") {
138-
if abs, err := filepath.Abs(agentFilename); err == nil {
139-
agentFilename = abs
136+
// Skip agent file loading when using remote runtime
137+
var agents *team.Team
138+
var err error
139+
if remoteAddress == "" {
140+
// Determine how to obtain the agent definition
141+
ext := strings.ToLower(filepath.Ext(agentFilename))
142+
if ext == ".yaml" || ext == ".yml" || strings.HasPrefix(agentFilename, "/dev/fd/") {
143+
// Treat as local YAML file: resolve to absolute path so later chdir doesn't break it
144+
if !strings.Contains(agentFilename, "\n") {
145+
if abs, err := filepath.Abs(agentFilename); err == nil {
146+
agentFilename = abs
147+
}
140148
}
141-
}
142-
if !fileExists(agentFilename) {
143-
return fmt.Errorf("agent file not found: %s", agentFilename)
144-
}
145-
} else {
146-
// Treat as an OCI image reference. Try local store first, otherwise pull then load.
147-
a, err := fromStore(agentFilename)
148-
if err != nil {
149-
fmt.Println("Pulling agent ", agentFilename)
150-
if _, pullErr := remote.Pull(agentFilename); pullErr != nil {
151-
return fmt.Errorf("failed to pull OCI image %s: %w", agentFilename, pullErr)
149+
if !fileExists(agentFilename) {
150+
return fmt.Errorf("agent file not found: %s", agentFilename)
151+
}
152+
} else {
153+
// Treat as an OCI image reference. Try local store first, otherwise pull then load.
154+
a, err := fromStore(agentFilename)
155+
if err != nil {
156+
fmt.Println("Pulling agent ", agentFilename)
157+
if _, pullErr := remote.Pull(agentFilename); pullErr != nil {
158+
return fmt.Errorf("failed to pull OCI image %s: %w", agentFilename, pullErr)
159+
}
160+
// Retry after pull
161+
a, err = fromStore(agentFilename)
162+
if err != nil {
163+
return fmt.Errorf("failed to load agent from store after pull: %w", err)
164+
}
152165
}
153-
// Retry after pull
154-
a, err = fromStore(agentFilename)
166+
167+
// Write the fetched content to a temporary YAML file
168+
tmpFile, err := os.CreateTemp("", "agentfile-*.yaml")
155169
if err != nil {
156-
return fmt.Errorf("failed to load agent from store after pull: %w", err)
170+
return err
171+
}
172+
defer os.Remove(tmpFile.Name())
173+
if _, err := tmpFile.WriteString(a); err != nil {
174+
tmpFile.Close()
175+
return err
176+
}
177+
if err := tmpFile.Close(); err != nil {
178+
return err
157179
}
180+
agentFilename = tmpFile.Name()
158181
}
159182

160-
// Write the fetched content to a temporary YAML file
161-
tmpFile, err := os.CreateTemp("", "agentfile-*.yaml")
183+
agents, err = teamloader.Load(ctx, agentFilename, runConfig)
162184
if err != nil {
163185
return err
164186
}
165-
defer os.Remove(tmpFile.Name())
166-
if _, err := tmpFile.WriteString(a); err != nil {
167-
tmpFile.Close()
168-
return err
169-
}
170-
if err := tmpFile.Close(); err != nil {
171-
return err
172-
}
173-
agentFilename = tmpFile.Name()
187+
defer func() {
188+
if err := agents.StopToolSets(); err != nil {
189+
slog.Error("Failed to stop tool sets", "error", err)
190+
}
191+
}()
192+
} else {
193+
// For remote runtime, just store the original agent filename
194+
// The remote server will handle agent loading
195+
slog.Debug("Skipping local agent file loading for remote runtime", "filename", agentFilename)
174196
}
175197

176-
agents, err := teamloader.Load(ctx, agentFilename, runConfig)
177-
if err != nil {
178-
return err
198+
// Validate remote flag usage
199+
if remoteAddress != "" && (!useTUI || exec) {
200+
return fmt.Errorf("--remote flag can only be used with TUI mode")
179201
}
180-
defer func() {
181-
if err := agents.StopToolSets(); err != nil {
182-
slog.Error("Failed to stop tool sets", "error", err)
183-
}
184-
}()
185202

186203
tracer := otel.Tracer(APP_NAME)
187204

188-
rt, err := runtime.New(agents,
189-
runtime.WithCurrentAgent(agentName),
190-
runtime.WithAutoRunTools(autoApprove),
191-
runtime.WithTracer(tracer),
192-
)
193-
if err != nil {
194-
return fmt.Errorf("failed to create runtime: %w", err)
195-
}
205+
var sess *session.Session
206+
207+
// Create runtime based on whether remote flag is specified
208+
var rt runtime.Runtime
209+
if remoteAddress != "" && useTUI && !exec {
210+
// Create remote runtime for TUI mode
211+
remoteClient, err := runtime.NewClient(remoteAddress)
212+
if err != nil {
213+
return fmt.Errorf("failed to create remote client: %w", err)
214+
}
196215

197-
sess := session.New()
216+
sess, err = remoteClient.CreateSession(ctx)
217+
if err != nil {
218+
return err
219+
}
220+
221+
remoteRt, err := runtime.NewRemoteRuntime(remoteClient,
222+
runtime.WithRemoteCurrentAgent("root"),
223+
runtime.WithRemoteAgentFilename("pirate.yaml"),
224+
)
225+
if err != nil {
226+
return fmt.Errorf("failed to create remote runtime: %w", err)
227+
}
228+
rt = remoteRt
229+
slog.Debug("Using remote runtime", "address", remoteAddress, "agent", agentName)
230+
} else {
231+
// Create local runtime
232+
localRt, err := runtime.New(agents,
233+
runtime.WithCurrentAgent(agentName),
234+
runtime.WithAutoRunTools(autoApprove),
235+
runtime.WithTracer(tracer),
236+
)
237+
if err != nil {
238+
return fmt.Errorf("failed to create runtime: %w", err)
239+
}
240+
rt = localRt
241+
sess = session.New()
242+
slog.Debug("Using local runtime", "agent", agentName)
243+
}
198244

199245
// For `cagent exec`
200246
if exec {
@@ -204,12 +250,12 @@ func doRunCommand(ctx context.Context, args []string, exec bool) error {
204250
} else {
205251
execArgs = append(execArgs, "Follow the default instructions")
206252
}
207-
return runWithoutTUI(ctx, agentFilename, rt, sess, execArgs)
253+
return runWithoutTUI(ctx, agentFilename, rt, session.New(), execArgs)
208254
}
209255

210256
// For `cagent run --tui=false`
211257
if !useTUI {
212-
return runWithoutTUI(ctx, agentFilename, rt, sess, args)
258+
return runWithoutTUI(ctx, agentFilename, rt, session.New(), args)
213259
}
214260

215261
// The default is to use the TUI
@@ -228,7 +274,7 @@ func doRunCommand(ctx context.Context, args []string, exec bool) error {
228274
}
229275
}
230276

231-
a := app.New(agentFilename, rt, sess, firstMessage)
277+
a := app.New(agentFilename, rt, agents, sess, firstMessage)
232278
m := tui.New(a)
233279

234280
progOpts := []tea.ProgramOption{
@@ -251,7 +297,7 @@ func doRunCommand(ctx context.Context, args []string, exec bool) error {
251297
return err
252298
}
253299

254-
func runWithoutTUI(ctx context.Context, agentFilename string, rt *runtime.Runtime, sess *session.Session, args []string) error {
300+
func runWithoutTUI(ctx context.Context, agentFilename string, rt runtime.Runtime, sess *session.Session, args []string) error {
255301
sess.Title = "Running agent"
256302
// If the last received event was an error, return it. That way the exit code
257303
// will be non-zero if the agent failed.
@@ -442,7 +488,7 @@ func runWithoutTUI(ctx context.Context, agentFilename string, rt *runtime.Runtim
442488
return lastErr
443489
}
444490

445-
func runUserCommand(userInput string, sess *session.Session, rt *runtime.Runtime, ctx context.Context) (bool, error) {
491+
func runUserCommand(userInput string, sess *session.Session, rt runtime.Runtime, ctx context.Context) (bool, error) {
446492
yellow := color.New(color.FgYellow).SprintfFunc()
447493
switch userInput {
448494
case "/exit":

internal/app/app.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,18 @@ import (
1414

1515
type App struct {
1616
agentFilename string
17-
runtime *runtime.Runtime
17+
runtime runtime.Runtime
1818
team *team.Team
1919
session *session.Session
2020
firstMessage *string
2121
events chan tea.Msg
2222
}
2323

24-
func New(agentFilename string, rt *runtime.Runtime, sess *session.Session, firstMessage *string) *App {
24+
func New(agentFilename string, rt runtime.Runtime, agents *team.Team, sess *session.Session, firstMessage *string) *App {
2525
return &App{
2626
agentFilename: agentFilename,
2727
runtime: rt,
28-
team: rt.Team(),
28+
team: agents,
2929
session: sess,
3030
firstMessage: firstMessage,
3131
events: make(chan tea.Msg, 128),

internal/tui/components/tool/tool.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,14 @@ func (mv *toolModel) Render(width int) string {
9090
msg := mv.message
9191

9292
// Ask the tool what's its display name
93+
displayName := msg.ToolCall.Function.Name
9394
team := mv.app.Team()
94-
agent := team.Agent(msg.Sender)
95-
displayName := agent.ToolDisplayName(context.TODO(), msg.ToolCall.Function.Name)
95+
if team != nil {
96+
agent := team.Agent(msg.Sender)
97+
if agent != nil {
98+
displayName = agent.ToolDisplayName(context.TODO(), msg.ToolCall.Function.Name)
99+
}
100+
}
96101
content := fmt.Sprintf("%s %s", icon(msg.ToolStatus), styles.HighlightStyle.Render(displayName))
97102

98103
if msg.ToolCall.Function.Arguments != "" {

0 commit comments

Comments
 (0)