Skip to content

Commit 08d4d0c

Browse files
Ambient Code Botclaude
andcommitted
feat(cli): add agent stop command, --all/-A for start and stop
- `agent stop <name-or-id>`: stops the active session for an agent. Idempotent — succeeds if already stopped or no session exists. - `agent start --all / -A`: starts all agents in the project. Server-side idempotent — returns existing active session. - `agent stop --all / -A`: stops all agents in the project. Idempotent — skips agents without active sessions. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 6d61555 commit 08d4d0c

1 file changed

Lines changed: 199 additions & 20 deletions

File tree

  • components/ambient-cli/cmd/acpctl/agent

components/ambient-cli/cmd/acpctl/agent/cmd.go

Lines changed: 199 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import (
1313
"github.com/spf13/cobra"
1414
)
1515

16+
var activePhases = map[string]bool{"Pending": true, "Creating": true, "Running": true}
17+
1618
var Cmd = &cobra.Command{
1719
Use: "agent",
1820
Short: "Manage project-scoped agents",
@@ -24,7 +26,8 @@ Subcommands:
2426
create Create an agent in a project
2527
update Update an agent's name, prompt, labels, or annotations
2628
delete Delete an agent
27-
start Start a new session for an agent
29+
start Start a session for an agent (idempotent)
30+
stop Stop the running session for an agent (idempotent)
2831
start-preview Preview start context (dry run)`,
2932
RunE: func(cmd *cobra.Command, args []string) error {
3033
return cmd.Help()
@@ -61,6 +64,29 @@ func resolveAgent(ctx context.Context, client *sdkclient.Client, projectID, agen
6164
return pa.ID, nil
6265
}
6366

67+
func resolveAgentFull(ctx context.Context, client *sdkclient.Client, projectID, agentArg string) (*sdktypes.Agent, error) {
68+
if agentArg == "" {
69+
return nil, fmt.Errorf("agent name or ID is required")
70+
}
71+
pa, err := client.Agents().GetInProject(ctx, projectID, agentArg)
72+
if err != nil {
73+
pa, err = client.Agents().GetByProject(ctx, projectID, agentArg)
74+
if err != nil {
75+
return nil, fmt.Errorf("agent %q not found in project %q", agentArg, projectID)
76+
}
77+
}
78+
return pa, nil
79+
}
80+
81+
func allAgentsInProject(ctx context.Context, client *sdkclient.Client, projectID string) ([]sdktypes.Agent, error) {
82+
opts := sdktypes.NewListOptions().Size(500).Build()
83+
list, err := client.Agents().ListByProject(ctx, projectID, opts)
84+
if err != nil {
85+
return nil, fmt.Errorf("list agents: %w", err)
86+
}
87+
return list.Items, nil
88+
}
89+
6490
var listArgs struct {
6591
projectID string
6692
outputFormat string
@@ -352,15 +378,22 @@ var agentStartArgs struct {
352378
projectID string
353379
prompt string
354380
outputFormat string
381+
all bool
355382
}
356383

357384
var agentStartCmd = &cobra.Command{
358-
Use: "start <name-or-id>",
359-
Short: "Start a new session for an agent",
360-
Args: cobra.ExactArgs(1),
385+
Use: "start [name-or-id]",
386+
Short: "Start a session for an agent (idempotent)",
387+
Long: `Start a session for an agent. If the agent already has an active
388+
session (Pending, Creating, or Running), returns it without creating a
389+
new one. Use --all / -A to start all agents in the project.
390+
391+
This operation is idempotent — calling it multiple times is safe.`,
392+
Args: cobra.MaximumNArgs(1),
361393
Example: ` acpctl agent start api
362394
acpctl agent start api --prompt "fix the bug"
363-
acpctl agent start <id> --project-id <id>`,
395+
acpctl agent start --all
396+
acpctl agent start -A --prompt "run tests"`,
364397
RunE: func(cmd *cobra.Command, args []string) error {
365398
projectID, err := resolveProject(agentStartArgs.projectID)
366399
if err != nil {
@@ -380,30 +413,67 @@ var agentStartCmd = &cobra.Command{
380413
ctx, cancel := context.WithTimeout(context.Background(), cfg.GetRequestTimeout())
381414
defer cancel()
382415

416+
if agentStartArgs.all {
417+
if len(args) > 0 {
418+
return fmt.Errorf("cannot specify agent name with --all")
419+
}
420+
return startAllAgents(ctx, cmd, client, projectID)
421+
}
422+
423+
if len(args) == 0 {
424+
return fmt.Errorf("agent name or ID is required (or use --all)")
425+
}
426+
383427
agentID, err := resolveAgent(ctx, client, projectID, args[0])
384428
if err != nil {
385429
return err
386430
}
387431

388-
resp, err := client.Agents().Start(ctx, projectID, agentID, agentStartArgs.prompt)
389-
if err != nil {
390-
return fmt.Errorf("start agent: %w", err)
391-
}
432+
return startSingleAgent(ctx, cmd, client, projectID, agentID, args[0])
433+
},
434+
}
392435

393-
if agentStartArgs.outputFormat == "json" {
394-
printer := output.NewPrinter(output.FormatJSON, cmd.OutOrStdout())
395-
if resp.Session != nil {
396-
return printer.PrintJSON(resp.Session)
397-
}
398-
return printer.PrintJSON(resp)
399-
}
436+
func startSingleAgent(ctx context.Context, cmd *cobra.Command, client *sdkclient.Client, projectID, agentID, displayName string) error {
437+
resp, err := client.Agents().Start(ctx, projectID, agentID, agentStartArgs.prompt)
438+
if err != nil {
439+
return fmt.Errorf("start agent: %w", err)
440+
}
441+
442+
if agentStartArgs.outputFormat == "json" {
443+
printer := output.NewPrinter(output.FormatJSON, cmd.OutOrStdout())
400444
if resp.Session != nil {
401-
fmt.Fprintf(cmd.OutOrStdout(), "session/%s started (phase: %s)\n", resp.Session.ID, resp.Session.Phase)
402-
} else {
403-
fmt.Fprintf(cmd.OutOrStdout(), "agent/%s started\n", args[0])
445+
return printer.PrintJSON(resp.Session)
404446
}
447+
return printer.PrintJSON(resp)
448+
}
449+
if resp.Session != nil {
450+
fmt.Fprintf(cmd.OutOrStdout(), "session/%s started (phase: %s)\n", resp.Session.ID, resp.Session.Phase)
451+
} else {
452+
fmt.Fprintf(cmd.OutOrStdout(), "agent/%s started\n", displayName)
453+
}
454+
return nil
455+
}
456+
457+
func startAllAgents(ctx context.Context, cmd *cobra.Command, client *sdkclient.Client, projectID string) error {
458+
agents, err := allAgentsInProject(ctx, client, projectID)
459+
if err != nil {
460+
return err
461+
}
462+
if len(agents) == 0 {
463+
fmt.Fprintln(cmd.OutOrStdout(), "no agents in project")
405464
return nil
406-
},
465+
}
466+
var failed int
467+
for _, a := range agents {
468+
if err := startSingleAgent(ctx, cmd, client, projectID, a.ID, a.Name); err != nil {
469+
fmt.Fprintf(cmd.ErrOrStderr(), "agent/%s: %v\n", a.Name, err)
470+
failed++
471+
}
472+
}
473+
if failed > 0 {
474+
return fmt.Errorf("%d of %d agents failed to start", failed, len(agents))
475+
}
476+
return nil
407477
}
408478

409479
var startPreviewArgs struct {
@@ -506,13 +576,118 @@ var sessionsCmd = &cobra.Command{
506576
},
507577
}
508578

579+
var agentStopArgs struct {
580+
projectID string
581+
all bool
582+
}
583+
584+
var agentStopCmd = &cobra.Command{
585+
Use: "stop [name-or-id]",
586+
Short: "Stop the running session for an agent (idempotent)",
587+
Long: `Stop the active session for an agent. If the agent has no active
588+
session, prints a message and succeeds. Use --all / -A to stop all
589+
agents in the project.
590+
591+
This operation is idempotent — calling it multiple times is safe.`,
592+
Args: cobra.MaximumNArgs(1),
593+
Example: ` acpctl agent stop api
594+
acpctl agent stop --all
595+
acpctl agent stop -A`,
596+
RunE: func(cmd *cobra.Command, args []string) error {
597+
projectID, err := resolveProject(agentStopArgs.projectID)
598+
if err != nil {
599+
return err
600+
}
601+
602+
client, err := connection.NewClientFromConfig()
603+
if err != nil {
604+
return err
605+
}
606+
607+
cfg, err := config.Load()
608+
if err != nil {
609+
return err
610+
}
611+
612+
ctx, cancel := context.WithTimeout(context.Background(), cfg.GetRequestTimeout())
613+
defer cancel()
614+
615+
if agentStopArgs.all {
616+
if len(args) > 0 {
617+
return fmt.Errorf("cannot specify agent name with --all")
618+
}
619+
return stopAllAgents(ctx, cmd, client, projectID)
620+
}
621+
622+
if len(args) == 0 {
623+
return fmt.Errorf("agent name or ID is required (or use --all)")
624+
}
625+
626+
agent, err := resolveAgentFull(ctx, client, projectID, args[0])
627+
if err != nil {
628+
return err
629+
}
630+
631+
return stopSingleAgent(ctx, cmd, client, agent)
632+
},
633+
}
634+
635+
func stopSingleAgent(ctx context.Context, cmd *cobra.Command, client *sdkclient.Client, agent *sdktypes.Agent) error {
636+
if agent.CurrentSessionID == "" {
637+
fmt.Fprintf(cmd.OutOrStdout(), "agent/%s has no active session\n", agent.Name)
638+
return nil
639+
}
640+
641+
sess, err := client.Sessions().Get(ctx, agent.CurrentSessionID)
642+
if err != nil {
643+
fmt.Fprintf(cmd.OutOrStdout(), "agent/%s session/%s not found — already cleaned up\n", agent.Name, agent.CurrentSessionID)
644+
return nil
645+
}
646+
647+
if !activePhases[sess.Phase] {
648+
fmt.Fprintf(cmd.OutOrStdout(), "agent/%s session/%s already %s\n", agent.Name, sess.ID, sess.Phase)
649+
return nil
650+
}
651+
652+
stopped, err := client.Sessions().Stop(ctx, sess.ID)
653+
if err != nil {
654+
return fmt.Errorf("stop agent/%s session/%s: %w", agent.Name, sess.ID, err)
655+
}
656+
657+
fmt.Fprintf(cmd.OutOrStdout(), "agent/%s session/%s stopped (phase: %s)\n", agent.Name, stopped.ID, stopped.Phase)
658+
return nil
659+
}
660+
661+
func stopAllAgents(ctx context.Context, cmd *cobra.Command, client *sdkclient.Client, projectID string) error {
662+
agents, err := allAgentsInProject(ctx, client, projectID)
663+
if err != nil {
664+
return err
665+
}
666+
if len(agents) == 0 {
667+
fmt.Fprintln(cmd.OutOrStdout(), "no agents in project")
668+
return nil
669+
}
670+
var failed int
671+
for i := range agents {
672+
if err := stopSingleAgent(ctx, cmd, client, &agents[i]); err != nil {
673+
fmt.Fprintf(cmd.ErrOrStderr(), "agent/%s: %v\n", agents[i].Name, err)
674+
failed++
675+
}
676+
}
677+
if failed > 0 {
678+
return fmt.Errorf("%d of %d agents failed to stop", failed, len(agents))
679+
}
680+
return nil
681+
}
682+
509683
func init() {
510684
Cmd.AddCommand(listCmd)
511685
Cmd.AddCommand(getCmd)
512686
Cmd.AddCommand(createCmd)
513687
Cmd.AddCommand(updateCmd)
514688
Cmd.AddCommand(deleteCmd)
515689
Cmd.AddCommand(agentStartCmd)
690+
Cmd.AddCommand(agentStopCmd)
516691
Cmd.AddCommand(startPreviewCmd)
517692
Cmd.AddCommand(sessionsCmd)
518693

@@ -542,6 +717,10 @@ func init() {
542717
agentStartCmd.Flags().StringVar(&agentStartArgs.projectID, "project-id", "", "Project ID (defaults to configured project)")
543718
agentStartCmd.Flags().StringVar(&agentStartArgs.prompt, "prompt", "", "Task prompt for this run")
544719
agentStartCmd.Flags().StringVarP(&agentStartArgs.outputFormat, "output", "o", "", "Output format: json")
720+
agentStartCmd.Flags().BoolVarP(&agentStartArgs.all, "all", "A", false, "Start all agents in the project")
721+
722+
agentStopCmd.Flags().StringVar(&agentStopArgs.projectID, "project-id", "", "Project ID (defaults to configured project)")
723+
agentStopCmd.Flags().BoolVarP(&agentStopArgs.all, "all", "A", false, "Stop all agents in the project")
545724

546725
startPreviewCmd.Flags().StringVar(&startPreviewArgs.projectID, "project-id", "", "Project ID (defaults to configured project)")
547726

0 commit comments

Comments
 (0)