From 9216f8254a98be63a81fab86314a8b59200ff026 Mon Sep 17 00:00:00 2001 From: trangevi Date: Mon, 6 Apr 2026 14:11:15 -0700 Subject: [PATCH 1/3] Files and monitor changes Signed-off-by: trangevi --- .../azure.ai.agents/internal/cmd/files.go | 21 +++------- .../internal/cmd/files_test.go | 19 +++++++++ .../azure.ai.agents/internal/cmd/monitor.go | 1 - .../pkg/agents/agent_api/operations.go | 40 +++++++++---------- 4 files changed, 44 insertions(+), 37 deletions(-) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/files.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/files.go index 0df74355a51..763b48aa249 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/files.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/files.go @@ -78,9 +78,11 @@ azd environment. Use --agent-name to select a specific agent when the project has multiple azure.ai.agent services. The session ID is automatically resolved from the last invoke session, or can be overridden with --session.`, PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - // Chain with parent's PersistentPreRunE (root sets NoPrompt) - if parent := cmd.Parent(); parent != nil && parent.PersistentPreRunE != nil { - if err := parent.PersistentPreRunE(cmd, args); err != nil { + // Chain with root's PersistentPreRunE (root sets NoPrompt). + // Note: cmd.Parent() would return the "files" command itself when + // a subcommand runs, causing infinite recursion. + if root := cmd.Root(); root != nil && root.PersistentPreRunE != nil { + if err := root.PersistentPreRunE(cmd, args); err != nil { return err } } @@ -138,13 +140,6 @@ func resolveFilesContext(ctx context.Context, flags *filesFlags) (*filesContext, info.ServiceName, ) } - if info.Version == "" { - return nil, fmt.Errorf( - "agent version not found in azd environment for service %q\n\n"+ - "Run 'azd deploy' to deploy the agent, or check that the service is configured in azure.yaml", - info.ServiceName, - ) - } endpoint, err := resolveAgentEndpoint(ctx, "", "") if err != nil { @@ -251,7 +246,6 @@ func (a *FilesUploadAction) Run(ctx context.Context) error { err = agentClient.UploadSessionFile( ctx, a.Name, - a.Version, a.sessionID, remotePath, DefaultVNextAgentAPIVersion, @@ -338,7 +332,6 @@ func (a *FilesDownloadAction) Run(ctx context.Context) error { body, err := agentClient.DownloadSessionFile( ctx, a.Name, - a.Version, a.sessionID, a.flags.file, DefaultVNextAgentAPIVersion, @@ -448,7 +441,6 @@ func (a *FilesListAction) Run(ctx context.Context) error { fileList, err := agentClient.ListSessionFiles( ctx, a.Name, - a.Version, a.sessionID, a.remotePath, DefaultVNextAgentAPIVersion, @@ -568,7 +560,6 @@ func (a *FilesRemoveAction) Run(ctx context.Context) error { err = agentClient.RemoveSessionFile( ctx, a.Name, - a.Version, a.sessionID, a.remotePath, a.flags.recursive, @@ -645,7 +636,6 @@ func (a *FilesMkdirAction) Run(ctx context.Context) error { err = agentClient.MkdirSessionFile( ctx, a.Name, - a.Version, a.sessionID, a.remotePath, DefaultVNextAgentAPIVersion, @@ -729,7 +719,6 @@ func (a *FilesStatAction) Run(ctx context.Context) error { fileInfo, err := agentClient.StatSessionFile( ctx, a.Name, - a.Version, a.sessionID, a.remotePath, DefaultVNextAgentAPIVersion, diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/files_test.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/files_test.go index 9abdc9d3729..02291665c52 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/files_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/files_test.go @@ -8,10 +8,29 @@ import ( "azureaiagent/internal/pkg/agents/agent_api" + "github.com/spf13/cobra" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +func TestFilesCommand_PersistentPreRunE_NoRecursion(t *testing.T) { + // Regression: PersistentPreRunE previously used cmd.Parent() which returned + // the "files" command itself when a subcommand ran, causing infinite recursion + // (stack overflow). The fix uses cmd.Root() to chain to the root command. + root := NewRootCommand() + + // Override the list subcommand's RunE so it doesn't need a real environment. + filesCmd, _, _ := root.Find([]string{"files"}) + require.NotNil(t, filesCmd) + for _, sub := range filesCmd.Commands() { + sub.RunE = func(cmd *cobra.Command, args []string) error { return nil } + } + + root.SetArgs([]string{"files", "list"}) + // If the bug is present this will stack-overflow instead of returning. + _ = root.Execute() +} + func TestFilesCommand_HasSubcommands(t *testing.T) { cmd := newFilesCommand() diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/monitor.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/monitor.go index ad2ede2a555..cb44a272ce6 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/monitor.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/monitor.go @@ -152,7 +152,6 @@ func (a *MonitorAction) Run(ctx context.Context) error { body, err = agentClient.GetAgentSessionLogStream( ctx, a.Name, - a.Version, a.flags.sessionID, DefaultVNextAgentAPIVersion, a.flags.logType, diff --git a/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_api/operations.go b/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_api/operations.go index 24b6a14a08c..a0cfd5dbf75 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_api/operations.go +++ b/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_api/operations.go @@ -880,7 +880,7 @@ func (r *cancelOnCloseReader) Close() error { // follow controls whether to stream indefinitely (true) or fetch and exit (false). func (c *AgentClient) GetAgentSessionLogStream( ctx context.Context, - agentName, agentVersion, sessionID, apiVersion string, + agentName, sessionID, apiVersion string, kind string, tail int, follow bool, @@ -890,7 +890,7 @@ func (c *AgentClient) GetAgentSessionLogStream( return nil, fmt.Errorf("invalid endpoint URL: %w", err) } - u.Path += fmt.Sprintf("/agents/%s/versions/%s/sessions/%s:logstream", agentName, agentVersion, sessionID) + u.Path += fmt.Sprintf("/agents/%s/sessions/%s:logstream", agentName, sessionID) query := u.Query() query.Set("api-version", apiVersion) @@ -987,7 +987,7 @@ func (c *AgentClient) GetAgentContainerOperation(ctx context.Context, agentName, // body is the file content to upload. func (c *AgentClient) UploadSessionFile( ctx context.Context, - agentName, agentVersion, sessionID, remotePath, apiVersion string, + agentName, sessionID, remotePath, apiVersion string, body io.ReadSeeker, ) error { u, err := url.Parse(c.endpoint) @@ -996,8 +996,8 @@ func (c *AgentClient) UploadSessionFile( } u.Path += fmt.Sprintf( - "/agents/%s/versions/%s/sessions/%s/files", - agentName, agentVersion, sessionID, + "/agents/%s/endpoint/sessions/%s/files", + agentName, sessionID, ) query := u.Query() @@ -1034,7 +1034,7 @@ func (c *AgentClient) UploadSessionFile( // Returns an io.ReadCloser with the file content; the caller must close it. func (c *AgentClient) DownloadSessionFile( ctx context.Context, - agentName, agentVersion, sessionID, remotePath, apiVersion string, + agentName, sessionID, remotePath, apiVersion string, ) (io.ReadCloser, error) { u, err := url.Parse(c.endpoint) if err != nil { @@ -1042,8 +1042,8 @@ func (c *AgentClient) DownloadSessionFile( } u.Path += fmt.Sprintf( - "/agents/%s/versions/%s/sessions/%s/files", - agentName, agentVersion, sessionID, + "/agents/%s/endpoint/sessions/%s/files", + agentName, sessionID, ) query := u.Query() @@ -1077,7 +1077,7 @@ func (c *AgentClient) DownloadSessionFile( // remotePath is the directory path to list (empty string for root). func (c *AgentClient) ListSessionFiles( ctx context.Context, - agentName, agentVersion, sessionID, remotePath, apiVersion string, + agentName, sessionID, remotePath, apiVersion string, ) (*SessionFileList, error) { u, err := url.Parse(c.endpoint) if err != nil { @@ -1085,8 +1085,8 @@ func (c *AgentClient) ListSessionFiles( } u.Path += fmt.Sprintf( - "/agents/%s/versions/%s/sessions/%s/files/list", - agentName, agentVersion, sessionID, + "/agents/%s/endpoint/sessions/%s/files", + agentName, sessionID, ) query := u.Query() @@ -1131,7 +1131,7 @@ func (c *AgentClient) ListSessionFiles( // recursive controls whether to recursively remove directories. func (c *AgentClient) RemoveSessionFile( ctx context.Context, - agentName, agentVersion, sessionID, remotePath string, + agentName, sessionID, remotePath string, recursive bool, apiVersion string, ) error { @@ -1141,8 +1141,8 @@ func (c *AgentClient) RemoveSessionFile( } u.Path += fmt.Sprintf( - "/agents/%s/versions/%s/sessions/%s/files", - agentName, agentVersion, sessionID, + "/agents/%s/endpoint/sessions/%s/files", + agentName, sessionID, ) query := u.Query() @@ -1174,7 +1174,7 @@ func (c *AgentClient) RemoveSessionFile( // MkdirSessionFile creates a directory in a session's filesystem. func (c *AgentClient) MkdirSessionFile( ctx context.Context, - agentName, agentVersion, sessionID, remotePath string, + agentName, sessionID, remotePath string, apiVersion string, ) error { u, err := url.Parse(c.endpoint) @@ -1183,8 +1183,8 @@ func (c *AgentClient) MkdirSessionFile( } u.Path += fmt.Sprintf( - "/agents/%s/versions/%s/sessions/%s/files/mkdir", - agentName, agentVersion, sessionID, + "/agents/%s/endpoint/sessions/%s/files/mkdir", + agentName, sessionID, ) query := u.Query() @@ -1224,7 +1224,7 @@ func (c *AgentClient) MkdirSessionFile( // StatSessionFile returns file/directory metadata from a session's filesystem. func (c *AgentClient) StatSessionFile( ctx context.Context, - agentName, agentVersion, sessionID, remotePath, apiVersion string, + agentName, sessionID, remotePath, apiVersion string, ) (*SessionFileInfo, error) { u, err := url.Parse(c.endpoint) if err != nil { @@ -1232,8 +1232,8 @@ func (c *AgentClient) StatSessionFile( } u.Path += fmt.Sprintf( - "/agents/%s/versions/%s/sessions/%s/files/stat", - agentName, agentVersion, sessionID, + "/agents/%s/endpoint/sessions/%s/files/stat", + agentName, sessionID, ) query := u.Query() From b10140a8ad92061cabbcf22d4b12958479365065 Mon Sep 17 00:00:00 2001 From: trangevi Date: Wed, 8 Apr 2026 09:05:38 -0700 Subject: [PATCH 2/3] Update show Signed-off-by: trangevi --- .../azure.ai.agents/internal/cmd/show.go | 59 ++++++++++++++-- .../azure.ai.agents/internal/cmd/show_test.go | 69 +++++++++++++++++++ 2 files changed, 122 insertions(+), 6 deletions(-) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/show.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/show.go index 195967c3b1e..542edcaecc4 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/show.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/show.go @@ -9,6 +9,7 @@ import ( "fmt" "os" "text/tabwriter" + "time" "azureaiagent/internal/pkg/agents/agent_api" @@ -24,7 +25,8 @@ type showFlags struct { // ShowAction handles the execution of the show command. type ShowAction struct { *AgentContext - flags *showFlags + flags *showFlags + azdClient *azdext.AzdClient } func newShowCommand() *cobra.Command { @@ -32,11 +34,8 @@ func newShowCommand() *cobra.Command { cmd := &cobra.Command{ Use: "show [name]", - Short: "Show the status of a hosted agent deployment.", - Long: `Show the status of a hosted agent deployment. - -Retrieves the runtime status of a hosted agent container, including its current state, -replica configuration, and any error messages. + Short: "Show the status of a hosted agent.", + Long: `Show the status of a hosted agent. The agent name and version are resolved automatically from the azure.yaml service configuration and the current azd environment. Optionally specify the service name @@ -91,6 +90,7 @@ configuration and the current azd environment. Optionally specify the service na action := &ShowAction{ AgentContext: agentContext, flags: flags, + azdClient: azdClient, } return action.Run(ctx) @@ -109,6 +109,22 @@ func (a *ShowAction) Run(ctx context.Context) error { return err } + if isVNextEnabled(ctx, a.azdClient) { + version, err := agentClient.GetAgentVersion( + ctx, a.Name, a.Version, DefaultAgentAPIVersion, + ) + if err != nil { + return fmt.Errorf("failed to get agent version: %w", err) + } + + switch a.flags.output { + case "table": + return printAgentVersionTable(version) + default: + return printAgentVersionJSON(version) + } + } + container, err := agentClient.GetAgentContainer(ctx, a.Name, a.Version, DefaultAgentAPIVersion) if err != nil { return fmt.Errorf("failed to get agent container status: %w", err) @@ -167,3 +183,34 @@ func printStatusTable(container *agent_api.AgentContainerObject) error { return w.Flush() } + +func printAgentVersionJSON(version *agent_api.AgentVersionObject) error { + jsonBytes, err := json.MarshalIndent(version, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal agent version to JSON: %w", err) + } + fmt.Println(string(jsonBytes)) + return nil +} + +func printAgentVersionTable(version *agent_api.AgentVersionObject) error { + w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + fmt.Fprintln(w, "FIELD\tVALUE") + fmt.Fprintln(w, "-----\t-----") + + fmt.Fprintf(w, "ID\t%s\n", version.ID) + fmt.Fprintf(w, "Name\t%s\n", version.Name) + fmt.Fprintf(w, "Version\t%s\n", version.Version) + if version.Description != nil { + fmt.Fprintf(w, "Description\t%s\n", *version.Description) + } + if version.CreatedAt != 0 { + ts := time.Unix(version.CreatedAt, 0).UTC().Format(time.RFC3339) + fmt.Fprintf(w, "Created At\t%s\n", ts) + } + for k, v := range version.Metadata { + fmt.Fprintf(w, "Metadata[%s]\t%s\n", k, v) + } + + return w.Flush() +} diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/show_test.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/show_test.go index 0e5af4cd7f9..6e6a60ce204 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/show_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/show_test.go @@ -175,3 +175,72 @@ func TestShowCommand_DefaultOutputFlag(t *testing.T) { output, _ := cmd.Flags().GetString("output") assert.Equal(t, "json", output) } + +func TestPrintAgentVersionJSON(t *testing.T) { + version := &agent_api.AgentVersionObject{ + Object: "agent.version", + ID: "ver-123", + Name: "my-agent", + Version: "1", + CreatedAt: 1735689600, // 2025-01-01T00:00:00Z + } + + err := printAgentVersionJSON(version) + require.NoError(t, err) +} + +func TestPrintAgentVersionJSON_Format(t *testing.T) { + desc := "A test agent" + version := &agent_api.AgentVersionObject{ + Object: "agent.version", + ID: "ver-456", + Name: "test-agent", + Version: "2", + Description: &desc, + Metadata: map[string]string{"env": "prod"}, + CreatedAt: 1735689600, + } + + jsonBytes, err := json.MarshalIndent(version, "", " ") + require.NoError(t, err) + + var result map[string]any + err = json.Unmarshal(jsonBytes, &result) + require.NoError(t, err) + + assert.Equal(t, "agent.version", result["object"]) + assert.Equal(t, "ver-456", result["id"]) + assert.Equal(t, "test-agent", result["name"]) + assert.Equal(t, "2", result["version"]) + assert.Equal(t, "A test agent", result["description"]) + metadata := result["metadata"].(map[string]any) + assert.Equal(t, "prod", metadata["env"]) +} + +func TestPrintAgentVersionTable(t *testing.T) { + desc := "A test agent" + version := &agent_api.AgentVersionObject{ + Object: "agent.version", + ID: "ver-789", + Name: "my-agent", + Version: "3", + Description: &desc, + Metadata: map[string]string{"env": "staging"}, + CreatedAt: 1735689600, + } + + err := printAgentVersionTable(version) + require.NoError(t, err) +} + +func TestPrintAgentVersionTable_MinimalFields(t *testing.T) { + version := &agent_api.AgentVersionObject{ + Object: "agent.version", + ID: "ver-min", + Name: "minimal-agent", + Version: "1", + } + + err := printAgentVersionTable(version) + require.NoError(t, err) +} From 441098db78a653cc4952ab8edd16b2d6d4b2bc97 Mon Sep 17 00:00:00 2001 From: trangevi Date: Wed, 8 Apr 2026 09:14:41 -0700 Subject: [PATCH 3/3] PR comment Signed-off-by: trangevi --- .../azure.ai.agents/internal/cmd/files.go | 3 +-- .../azure.ai.agents/internal/cmd/monitor.go | 13 ++++++------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/files.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/files.go index 763b48aa249..3daa92504d6 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/files.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/files.go @@ -73,7 +73,7 @@ func newFilesCommand() *cobra.Command { Upload, download, list, and remove files in the session-scoped filesystem of a hosted agent. This is useful for debugging, seeding data, and agent setup. -Agent details (name, version, endpoint) are automatically resolved from the +Agent details (name, endpoint) are automatically resolved from the azd environment. Use --agent-name to select a specific agent when the project has multiple azure.ai.agent services. The session ID is automatically resolved from the last invoke session, or can be overridden with --session.`, @@ -155,7 +155,6 @@ func resolveFilesContext(ctx context.Context, flags *filesFlags) (*filesContext, AgentContext: &AgentContext{ ProjectEndpoint: endpoint, Name: info.AgentName, - Version: info.Version, }, sessionID: sessionID, }, nil diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/monitor.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/monitor.go index cb44a272ce6..69dc206c924 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/monitor.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/monitor.go @@ -93,13 +93,6 @@ configuration and the current azd environment. Optionally specify the service na info.ServiceName, ) } - if info.Version == "" { - return fmt.Errorf( - "agent version could not be resolved from azd environment for service '%s'\n\n"+ - "Run 'azd deploy' first to deploy the agent, or check your azd environment values", - info.ServiceName, - ) - } agentContext, err := newAgentContext(ctx, "", "", info.AgentName, info.Version) if err != nil { @@ -159,6 +152,12 @@ func (a *MonitorAction) Run(ctx context.Context) error { a.flags.follow, ) } else { + if a.Version == "" { + return fmt.Errorf( + "agent version is required for container log streaming\n\n" + + "Run 'azd deploy' first to deploy the agent, or check your azd environment values", + ) + } body, err = agentClient.GetAgentContainerLogStream( ctx, a.Name,