Skip to content

feat: add an interactive chat command - #86

Open
ConnorMoss02 wants to merge 10 commits into
a2aproject:mainfrom
ConnorMoss02:feat/34-chat
Open

ConnorMoss02 wants to merge 10 commits into
a2aproject:mainfrom
ConnorMoss02:feat/34-chat

Conversation

@ConnorMoss02

@ConnorMoss02 ConnorMoss02 commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Adds a2a chat. It reads a message, sends it, prints the reply and loops. The context id carries across turns and an input-required task continues with your next message.

--stream works like it does for send, and --context-id and --task-id pick up an existing conversation. Ctrl-C cancels a running turn, and a failed turn prints the error without ending the chat. It stops on auth-required and prints the ids on /quit, EOF or Ctrl-C at the prompt. When stdin or stdout isn't a terminal it sends once and exits.

Fixes #34

@yarolegovich yarolegovich left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for the PR! overall looks good, but I'd encourage to polish the overall chat UX further before merging it

Comment thread internal/cli/chat.go Outdated
Short: "Chat with an agent, carrying the conversation across turns",
Long: "Chat with an agent interactively. Each reply continues the same conversation: the context id is carried " +
"across turns, and an input-required task is continued with your next message. Type " + chatQuit + " or press " +
"Ctrl-D to exit; the context and task ids are printed so you can resume with send.\n\n" +

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's wrap the context so that a running turn gets cancelled through ctx ( signal.NotifyContext(cmd.Context(), os.Interrupt)). this will however require moving lines.Scan() blocking call into a background goroutine

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 936c233. Ctrl-C now cancels the running turn and drops you back at the prompt. Ctrl-C at the prompt exits and prints the ids. Input reads on a goroutine so the interrupt gets noticed.

Comment thread internal/cli/chat.go
defer cancel()

result, err := s.client.SendMessage(ctx, &a2a.SendMessageRequest{Message: msg, Tenant: s.cfg.tenant})
if err != nil {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd probably keep the chat session open on send message errors

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 7d78c4c. A failed turn prints the error like send does and the chat keeps going. Auth-required still ends it.

Comment thread internal/cli/chat.go Outdated
const chatQuit = "/quit"

func newChatCmd(cfg *globalConfig, isTerminal func() bool) *cobra.Command {
return &cobra.Command{

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's add support for --context-id and --task-id to allow resuming a task in chat mode

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in 2554dae. Same flags and help text as send, and the first message goes out with those ids.

Comment thread internal/cli/chat.go Outdated
ctx, cancel := context.WithTimeout(ctx, s.cfg.timeout)
defer cancel()

result, err := s.client.SendMessage(ctx, &a2a.SendMessageRequest{Message: msg, Tenant: s.cfg.tenant})

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we're not in a rush with releasing chat support, so would be nice to have --stream option supported here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in e5288f1. I pulled the streaming code out of send into sendStreaming so both commands use it.

@ConnorMoss02

Copy link
Copy Markdown
Contributor Author

I also took a stab at sending CancelTask when a turn is interrupted (663dd60), so the agent stops working on it too. Happy to roll it back if you'd rather keep that out of this PR.

Comment thread internal/cli/chat.go
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
interactive := deps.isTerminal()
if !interactive && len(args) == 0 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would just always fail for non-interactive. otherwise it's a send but with less functionality

Comment thread internal/cli/chat.go

cmd := &cobra.Command{
Use: "chat [message]",
Short: "Chat with an agent, carrying the conversation across turns",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Short: "Chat with an agent, carrying the conversation across turns",
Short: "Chat interactively with an agent",

Comment thread internal/cli/chat.go
"github.com/a2aproject/a2a-go/v2/a2aclient"
)

const chatQuit = "/quit"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/exit feels more natural to me. Can we have both /exit & /quit ?

Comment thread internal/cli/chat.go
Comment on lines +48 to +52
Long: "Chat with an agent interactively. Each reply continues the same conversation: the context id is carried " +
"across turns, and an input-required task is continued with your next message. Type " + chatQuit + " or press " +
"Ctrl-D to exit; the context and task ids are printed so you can resume with send, or with chat --context-id and --task-id. " +
"Ctrl-C cancels a running turn and asks the agent to cancel its task, or exits at the prompt.\n\n" +
"When standard input or output is not a terminal, chat sends [message] once, like send, and reads no input.",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Long: "Chat with an agent interactively. Each reply continues the same conversation: the context id is carried " +
"across turns, and an input-required task is continued with your next message. Type " + chatQuit + " or press " +
"Ctrl-D to exit; the context and task ids are printed so you can resume with send, or with chat --context-id and --task-id. " +
"Ctrl-C cancels a running turn and asks the agent to cancel its task, or exits at the prompt.\n\n" +
"When standard input or output is not a terminal, chat sends [message] once, like send, and reads no input.",
Long: "Start an interactive chat session with an agent. Chat reuses the same context ID " +
"across turns and continues tasks that require input.\n\n" +
"Controls:\n" +
" " + chatQuit + ", Ctrl-D Exit and print the context and task IDs to resume later\n" +
" Ctrl-C Cancel an active turn (and its agent task), or exit at the prompt\n\n" +
"Non-interactive mode sends [message] once and exits. Use resume --context-id and --task-id to resume a conversation.",

Comment thread internal/cli/chat.go
Comment on lines +88 to +91
f.BoolVar(&flags.stream, "stream", false, "Use streaming response. Falls back to polling and synthetic events if a server does not support streaming.")
f.StringVar(&flags.taskID, "task-id", "", "Task ID to continue an existing task")
f.StringVar(&flags.contextID, "context-id", "", "Context ID to group this turn under")
f.DurationVar(&flags.pollInterval, "poll-interval", 2*time.Second, "Duration between GetTask requests in polling fallback mode.")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
f.BoolVar(&flags.stream, "stream", false, "Use streaming response. Falls back to polling and synthetic events if a server does not support streaming.")
f.StringVar(&flags.taskID, "task-id", "", "Task ID to continue an existing task")
f.StringVar(&flags.contextID, "context-id", "", "Context ID to group this turn under")
f.DurationVar(&flags.pollInterval, "poll-interval", 2*time.Second, "Duration between GetTask requests in polling fallback mode.")
f.BoolVar(&flags.stream, "stream", false, "Stream responses (falls back to polling if unsupported)")
f.StringVar(&flags.taskID, "task-id", "", "Task ID to continue an existing task")
f.StringVar(&flags.contextID, "context-id", "", "Context ID to continue an existing conversation")
f.DurationVar(&flags.pollInterval, "poll-interval", 2*time.Second, "Interval between GetTask requests when polling")

@msampathkumar

Copy link
Copy Markdown
Member

@ConnorMoss02 Thank you for working on this. We appreciate this work :)

I have shared a few comment to help with the help messages.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: add interactive chat for multi-turn agent sessions

3 participants