@@ -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+
1618var 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+
6490var 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
357384var 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
409479var 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+
509683func 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