Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
d39479d
feat: implement session management commands for hosted agents
therealjohn Apr 6, 2026
5a3997d
feat: enhance session management with agent version handling and erro…
therealjohn Apr 7, 2026
1b8592a
feat: rename session command to sessions and update related documenta…
therealjohn Apr 8, 2026
5f6c57b
fix: use correct endpoint for files
therealjohn Apr 8, 2026
6ae0e9f
fix: default target path is relative to home, not full local path
therealjohn Apr 8, 2026
d0edf46
feat: implement error handling for session retrieval and deletion, in…
therealjohn Apr 8, 2026
cd26efa
feat: files remove -> files delete to be consistent with SDK and sess…
therealjohn Apr 8, 2026
1c342a4
feat: add positional args for file commands
therealjohn Apr 8, 2026
29fc917
feat: rename --session flag to --session-id across commands. Fixes #7…
therealjohn Apr 8, 2026
05e7200
feat: update command documentation for session and file operations, e…
therealjohn Apr 8, 2026
6c9ec38
fix: remove unused body field from fakeTransport struct in operations…
therealjohn Apr 8, 2026
76b01d7
fix: replace context.Background() with t.Context() in GetSession test
therealjohn Apr 8, 2026
857d4ee
feat: update documentation to replace --session with --session-id for…
therealjohn Apr 8, 2026
d4057b6
feat: enhance session deletion message and add tests for session crea…
therealjohn Apr 9, 2026
e342bdb
Update cli/azd/extensions/azure.ai.agents/internal/cmd/session.go
therealjohn Apr 9, 2026
d06a926
feat: improve error handling for session deletion and add tests for 4…
therealjohn Apr 9, 2026
be003b4
PR comments
trangevi Apr 14, 2026
891e8f9
PR comment
trangevi Apr 14, 2026
dd7d7b3
Linter
trangevi Apr 14, 2026
79dc1e4
Merge branch 'main' into session-crud
trangevi Apr 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 88 additions & 46 deletions cli/azd/extensions/azure.ai.agents/internal/cmd/files.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,13 @@ func newFilesCommand() *cobra.Command {
Hidden: !isVNextEnabled(context.Background()),
Long: `Manage files in a hosted agent session.

Upload, download, list, and remove files in the session-scoped filesystem
Upload, download, list, and delete files in the session-scoped filesystem
of a hosted agent. This is useful for debugging, seeding data, and agent setup.

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.`,
from the last invoke session, or can be overridden with --session-id.`,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
// Chain with root's PersistentPreRunE (root sets NoPrompt).
// Note: cmd.Parent() would return the "files" command itself when
Expand Down Expand Up @@ -111,7 +111,7 @@ from the last invoke session, or can be overridden with --session.`,
// addFilesFlags registers the common flags on a cobra command.
func addFilesFlags(cmd *cobra.Command, flags *filesFlags) {
cmd.Flags().StringVarP(&flags.agentName, "agent-name", "n", "", "Agent name (matches azure.yaml service name; auto-detected when only one exists)")
cmd.Flags().StringVarP(&flags.session, "session", "s", "", "Session ID override (defaults to last invoke session)")
cmd.Flags().StringVarP(&flags.session, "session-id", "s", "", "Session ID override (defaults to last invoke session)")
}

// filesContext holds the resolved agent context and session for file operations.
Expand Down Expand Up @@ -179,28 +179,39 @@ func newFilesUploadCommand() *cobra.Command {
flags := &filesUploadFlags{}

cmd := &cobra.Command{
Use: "upload",
Use: "upload [file]",
Short: "Upload a file to a hosted agent session.",
Long: `Upload a file to a hosted agent session.

Reads a local file and uploads it to the specified remote path
in the session's filesystem. If --target-path is not provided,
the remote path defaults to the local file path.
the remote path defaults to the local filename.

Agent details are automatically resolved from the azd environment.`,
Example: ` # Upload a file (remote path defaults to local path)
azd ai agent files upload --file ./data/input.csv
Example: ` # Upload a file (remote path defaults to filename)
azd ai agent files upload ./data/input.csv

# Upload to a specific remote path
azd ai agent files upload --file ./input.csv --target-path /data/input.csv
azd ai agent files upload ./input.csv --target-path /data/input.csv

# Upload with explicit agent name and session
azd ai agent files upload --file ./input.csv --agent-name my-agent --session <session-id>`,
# Upload with flags
azd ai agent files upload --file ./input.csv --agent-name my-agent`,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := azdext.WithAccessToken(cmd.Context())
logCleanup := setupDebugLogging(cmd.Flags())
defer logCleanup()

if len(args) > 0 && flags.file == "" {
Comment thread
trangevi marked this conversation as resolved.
flags.file = args[0]
}
if flags.file == "" {
return fmt.Errorf(
"file path is required as a positional argument " +
"or via --file",
)
}

fc, err := resolveFilesContext(ctx, &flags.filesFlags)
if err != nil {
return err
Expand All @@ -217,9 +228,8 @@ Agent details are automatically resolved from the azd environment.`,
}

addFilesFlags(cmd, &flags.filesFlags)
cmd.Flags().StringVarP(&flags.file, "file", "f", "", "Local file path to upload (required)")
cmd.Flags().StringVarP(&flags.targetPath, "target-path", "t", "", "Remote destination path (defaults to local file path)")
_ = cmd.MarkFlagRequired("file")
cmd.Flags().StringVarP(&flags.file, "file", "f", "", "Local file path to upload")
cmd.Flags().StringVarP(&flags.targetPath, "target-path", "t", "", "Remote destination path (defaults to local filename)")

return cmd
}
Expand All @@ -228,7 +238,7 @@ Agent details are automatically resolved from the azd environment.`,
func (a *FilesUploadAction) Run(ctx context.Context) error {
remotePath := a.flags.targetPath
if remotePath == "" {
remotePath = a.flags.file
remotePath = filepath.Base(a.flags.file)
}

//nolint:gosec // G304: file path is provided by the user via CLI flag
Expand All @@ -255,7 +265,7 @@ func (a *FilesUploadAction) Run(ctx context.Context) error {
return fmt.Errorf("failed to upload file: %w", err)
}

fmt.Printf("Uploaded %s %s\n", a.flags.file, remotePath)
fmt.Printf("Uploaded %s -> %s\n", a.flags.file, remotePath)
return nil
}

Expand All @@ -278,7 +288,7 @@ func newFilesDownloadCommand() *cobra.Command {
flags := &filesDownloadFlags{}

cmd := &cobra.Command{
Use: "download",
Use: "download [file]",
Short: "Download a file from a hosted agent session.",
Long: `Download a file from a hosted agent session.

Expand All @@ -288,18 +298,29 @@ the local path defaults to the basename of the remote file.

Agent details are automatically resolved from the azd environment.`,
Example: ` # Download a file (local path defaults to remote filename)
azd ai agent files download --file /data/output.csv
azd ai agent files download /data/output.csv

# Download to a specific local path
azd ai agent files download --file /data/output.csv --target-path ./output.csv
azd ai agent files download /data/output.csv --target-path ./output.csv

# Download with explicit session
azd ai agent files download --file /data/output.csv --session <session-id>`,
# Download with flags
azd ai agent files download --file /data/output.csv --session-id <session-id>`,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := azdext.WithAccessToken(cmd.Context())
logCleanup := setupDebugLogging(cmd.Flags())
defer logCleanup()

if len(args) > 0 && flags.file == "" {
flags.file = args[0]
}
if flags.file == "" {
return fmt.Errorf(
"file path is required as a positional argument " +
"or via --file",
)
}

fc, err := resolveFilesContext(ctx, &flags.filesFlags)
if err != nil {
return err
Expand All @@ -316,9 +337,8 @@ Agent details are automatically resolved from the azd environment.`,
}

addFilesFlags(cmd, &flags.filesFlags)
cmd.Flags().StringVarP(&flags.file, "file", "f", "", "Remote file path to download (required)")
cmd.Flags().StringVarP(&flags.file, "file", "f", "", "Remote file path to download")
cmd.Flags().StringVarP(&flags.targetPath, "target-path", "t", "", "Local destination path (defaults to remote filename)")
_ = cmd.MarkFlagRequired("file")

return cmd
}
Expand Down Expand Up @@ -358,7 +378,7 @@ func (a *FilesDownloadAction) Run(ctx context.Context) error {
return fmt.Errorf("failed to write file: %w", err)
}

fmt.Printf("Downloaded %s %s\n", a.flags.file, targetPath)
fmt.Printf("Downloaded %s -> %s\n", a.flags.file, targetPath)
return nil
}

Expand All @@ -381,8 +401,9 @@ func newFilesListCommand() *cobra.Command {
flags := &filesListFlags{}

cmd := &cobra.Command{
Use: "list [remote-path]",
Short: "List files in a hosted agent session.",
Use: "list [remote-path]",
Aliases: []string{"ls"},
Short: "List files in a hosted agent session.",
Long: `List files in a hosted agent session.

Lists files and directories at the specified path in the session's filesystem.
Expand All @@ -399,7 +420,7 @@ Agent details are automatically resolved from the azd environment.`,
azd ai agent files list /data --output table

# List with explicit session
azd ai agent files list --session <session-id>`,
azd ai agent files list --session-id <session-id>`,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := azdext.WithAccessToken(cmd.Context())
Expand Down Expand Up @@ -508,27 +529,39 @@ func newFilesRemoveCommand() *cobra.Command {
var filePath string

cmd := &cobra.Command{
Use: "remove",
Short: "Remove a file or directory from a hosted agent session.",
Long: `Remove a file or directory from a hosted agent session.
Use: "delete [file]",
Aliases: []string{"remove", "rm"},
Short: "Delete a file or directory from a hosted agent session.",
Long: `Delete a file or directory from a hosted agent session.

Removes the specified file or directory from the session's filesystem.
Use --recursive to remove directories and their contents.
Deletes the specified file or directory from the session's filesystem.
Use --recursive to delete directories and their contents.

Agent details are automatically resolved from the azd environment.`,
Example: ` # Remove a file (agent auto-detected)
azd ai agent files remove --file /data/old-file.csv
Example: ` # Delete a file (agent auto-detected)
azd ai agent files delete /data/old-file.csv

# Remove a directory recursively
azd ai agent files remove --file /data/temp --recursive
# Delete a directory recursively
azd ai agent files delete /data/temp --recursive

# Remove with explicit session
azd ai agent files remove --file /data/old-file.csv --session <session-id>`,
# Delete with flags
azd ai agent files delete --file /data/old-file.csv --session-id <session-id>`,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := azdext.WithAccessToken(cmd.Context())
logCleanup := setupDebugLogging(cmd.Flags())
defer logCleanup()

if len(args) > 0 && filePath == "" {
filePath = args[0]
}
if filePath == "" {
return fmt.Errorf(
"file path is required as a positional argument " +
"or via --file",
)
}

fc, err := resolveFilesContext(ctx, &flags.filesFlags)
if err != nil {
return err
Expand All @@ -546,9 +579,8 @@ Agent details are automatically resolved from the azd environment.`,
}

addFilesFlags(cmd, &flags.filesFlags)
cmd.Flags().StringVarP(&filePath, "file", "f", "", "Remote file or directory path to remove")
_ = cmd.MarkFlagRequired("file")
cmd.Flags().BoolVar(&flags.recursive, "recursive", false, "Recursively remove directories and their contents")
cmd.Flags().StringVarP(&filePath, "file", "f", "", "Remote file or directory path to delete")
cmd.Flags().BoolVar(&flags.recursive, "recursive", false, "Recursively delete directories and their contents")

return cmd
}
Expand Down Expand Up @@ -590,7 +622,7 @@ func newFilesMkdirCommand() *cobra.Command {
var dirPath string

cmd := &cobra.Command{
Use: "mkdir",
Use: "mkdir [dir]",
Short: "Create a directory in a hosted agent session.",
Long: `Create a directory in a hosted agent session.

Expand All @@ -599,15 +631,26 @@ Parent directories are created as needed.

Agent details are automatically resolved from the azd environment.`,
Example: ` # Create a directory (agent auto-detected)
azd ai agent files mkdir --dir /data/output
azd ai agent files mkdir /data/output

# Create with explicit session
azd ai agent files mkdir --dir /data/output --session <session-id>`,
# Create with flags
azd ai agent files mkdir --dir /data/output --session-id <session-id>`,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := azdext.WithAccessToken(cmd.Context())
logCleanup := setupDebugLogging(cmd.Flags())
defer logCleanup()

if len(args) > 0 && dirPath == "" {
dirPath = args[0]
}
if dirPath == "" {
return fmt.Errorf(
"directory path is required as a positional " +
"argument or via --dir",
)
}

fc, err := resolveFilesContext(ctx, flags)
if err != nil {
return err
Expand All @@ -625,7 +668,6 @@ Agent details are automatically resolved from the azd environment.`,

addFilesFlags(cmd, flags)
cmd.Flags().StringVarP(&dirPath, "dir", "d", "", "Remote directory path to create")
_ = cmd.MarkFlagRequired("dir")

return cmd
}
Expand Down Expand Up @@ -685,7 +727,7 @@ Agent details are automatically resolved from the azd environment.`,
azd ai agent files stat /data/output.csv --output table

# Get metadata with explicit session
azd ai agent files stat /data/output.csv --session <session-id>`,
azd ai agent files stat /data/output.csv --session-id <session-id>`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := azdext.WithAccessToken(cmd.Context())
Expand Down
14 changes: 7 additions & 7 deletions cli/azd/extensions/azure.ai.agents/internal/cmd/files_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func TestFilesCommand_HasSubcommands(t *testing.T) {
assert.Contains(t, names, "upload")
assert.Contains(t, names, "download")
assert.Contains(t, names, "list")
assert.Contains(t, names, "remove")
assert.Contains(t, names, "delete")
}

func TestFilesUploadCommand_MissingFile(t *testing.T) {
Expand All @@ -59,7 +59,7 @@ func TestFilesUploadCommand_MissingFile(t *testing.T) {
func TestFilesUploadCommand_HasFlags(t *testing.T) {
cmd := newFilesUploadCommand()

for _, name := range []string{"file", "target-path", "agent-name", "session"} {
for _, name := range []string{"file", "target-path", "agent-name", "session-id"} {
f := cmd.Flags().Lookup(name)
require.NotNil(t, f, "expected flag %q", name)
assert.Equal(t, "", f.DefValue)
Expand All @@ -79,7 +79,7 @@ func TestFilesDownloadCommand_MissingFile(t *testing.T) {
func TestFilesDownloadCommand_HasFlags(t *testing.T) {
cmd := newFilesDownloadCommand()

for _, name := range []string{"file", "target-path", "agent-name", "session"} {
for _, name := range []string{"file", "target-path", "agent-name", "session-id"} {
f := cmd.Flags().Lookup(name)
require.NotNil(t, f, "expected flag %q", name)
assert.Equal(t, "", f.DefValue)
Expand All @@ -100,7 +100,7 @@ func TestFilesListCommand_OptionalRemotePath(t *testing.T) {
assert.NotNil(t, cmd.Args)
}

func TestFilesRemoveCommand_MissingFile(t *testing.T) {
func TestFilesDeleteCommand_MissingFile(t *testing.T) {
cmd := newFilesRemoveCommand()

// Missing required --file flag
Expand All @@ -110,10 +110,10 @@ func TestFilesRemoveCommand_MissingFile(t *testing.T) {
assert.Contains(t, err.Error(), "file")
}

func TestFilesRemoveCommand_HasFlags(t *testing.T) {
func TestFilesDeleteCommand_HasFlags(t *testing.T) {
cmd := newFilesRemoveCommand()

for _, name := range []string{"file", "recursive", "agent-name", "session"} {
for _, name := range []string{"file", "recursive", "agent-name", "session-id"} {
f := cmd.Flags().Lookup(name)
require.NotNil(t, f, "expected flag %q", name)
}
Expand All @@ -135,7 +135,7 @@ func TestFilesMkdirCommand_MissingDir(t *testing.T) {
func TestFilesMkdirCommand_HasFlags(t *testing.T) {
cmd := newFilesMkdirCommand()

for _, name := range []string{"dir", "agent-name", "session"} {
for _, name := range []string{"dir", "agent-name", "session-id"} {
f := cmd.Flags().Lookup(name)
require.NotNil(t, f, "expected flag %q", name)
assert.Equal(t, "", f.DefValue)
Expand Down
Loading
Loading