From 8846983f5fede5d9a3252ad9ead85dfaeb25c7fe Mon Sep 17 00:00:00 2001 From: John Miller Date: Sun, 19 Apr 2026 22:43:37 -0400 Subject: [PATCH 1/4] refactor: update createConversation function to include agentName parameter. Fixes # --- cli/azd/extensions/azure.ai.agents/internal/cmd/helpers.go | 2 +- cli/azd/extensions/azure.ai.agents/internal/cmd/invoke.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/helpers.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/helpers.go index 58a04ec76ae..f1ed415ecc7 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/helpers.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/helpers.go @@ -386,7 +386,7 @@ func resolveConversationID( } // Create and persist a new conversation for multi-turn memory. - newConvID, err := createConversation(ctx, endpoint, bearerToken) + newConvID, err := createConversation(ctx, endpoint, agentName, bearerToken) if err != nil { return "", fmt.Errorf("failed to create conversation: %w", err) } diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke.go index 825d90c7b0e..1f965c3b98b 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke.go @@ -911,8 +911,8 @@ func handleInvocationLRO( } // createConversation creates a new Foundry conversation for multi-turn memory. -func createConversation(ctx context.Context, endpoint string, bearerToken string) (string, error) { - url := fmt.Sprintf("%s/openai/conversations?api-version=%s", endpoint, DefaultAgentAPIVersion) +func createConversation(ctx context.Context, projectEndpoint, agentName, bearerToken string) (string, error) { + url := fmt.Sprintf("%s/agents/%s/endpoint/protocols/openai/v1/conversations", projectEndpoint, agentName) req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader([]byte("{}"))) if err != nil { return "", err From 60ba2d0f1797422b44b39f7995feeb8511702260 Mon Sep 17 00:00:00 2001 From: John Miller Date: Sun, 19 Apr 2026 23:03:48 -0400 Subject: [PATCH 2/4] fix: update createConversation URL to include api-version query parameter --- cli/azd/extensions/azure.ai.agents/internal/cmd/invoke.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke.go index 1f965c3b98b..24ff70333e4 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke.go @@ -912,7 +912,7 @@ func handleInvocationLRO( // createConversation creates a new Foundry conversation for multi-turn memory. func createConversation(ctx context.Context, projectEndpoint, agentName, bearerToken string) (string, error) { - url := fmt.Sprintf("%s/agents/%s/endpoint/protocols/openai/v1/conversations", projectEndpoint, agentName) + url := fmt.Sprintf("%s/agents/%s/endpoint/protocols/openai/conversations?api-version=v1", projectEndpoint, agentName) req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader([]byte("{}"))) if err != nil { return "", err From d89bc51997d17115d4664745f2f724fb61fdeb05 Mon Sep 17 00:00:00 2001 From: John Miller Date: Mon, 20 Apr 2026 08:41:56 -0400 Subject: [PATCH 3/4] feat: add ConversationsAPIVersion constant and implement createConversation tests --- .../internal/cmd/agent_context.go | 3 + .../azure.ai.agents/internal/cmd/helpers.go | 4 +- .../azure.ai.agents/internal/cmd/invoke.go | 13 +- .../internal/cmd/invoke_test.go | 124 ++++++++++++++++++ 4 files changed, 137 insertions(+), 7 deletions(-) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/agent_context.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/agent_context.go index f2433f04d85..4da475ec24d 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/agent_context.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/agent_context.go @@ -18,6 +18,9 @@ import ( const DefaultAgentAPIVersion = "2025-05-15-preview" const DefaultVNextAgentAPIVersion = "2025-11-15-preview" +// ConversationsAPIVersion is the API version used by the Foundry Conversations protocol. +const ConversationsAPIVersion = "v1" + // AgentContext holds the common properties of a hosted agent. type AgentContext struct { ProjectEndpoint string diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/helpers.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/helpers.go index f1ed415ecc7..4f3c4c7e0aa 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/helpers.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/helpers.go @@ -363,7 +363,7 @@ func resolveConversationID( agentName string, explicit string, forceNew bool, - endpoint string, + projectEndpoint string, bearerToken string, ) (string, error) { if explicit != "" { @@ -386,7 +386,7 @@ func resolveConversationID( } // Create and persist a new conversation for multi-turn memory. - newConvID, err := createConversation(ctx, endpoint, agentName, bearerToken) + newConvID, err := createConversation(ctx, projectEndpoint, agentName, bearerToken) if err != nil { return "", fmt.Errorf("failed to create conversation: %w", err) } diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke.go index 24ff70333e4..cd8ddf86a8c 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke.go @@ -359,7 +359,7 @@ func (a *InvokeAction) responsesRemote(ctx context.Context) error { return fmt.Errorf("agent name is required; provide as the first argument or define an azure.ai.agent service in azure.yaml") } - endpoint, err := resolveAgentEndpoint(ctx, "", "") + projectEndpoint, err := resolveAgentEndpoint(ctx, "", "") if err != nil { return err } @@ -417,7 +417,7 @@ func (a *InvokeAction) responsesRemote(ctx context.Context) error { name, a.flags.conversation, a.flags.newConversation, - endpoint, + projectEndpoint, token.Token, ) if err != nil { @@ -440,10 +440,10 @@ func (a *InvokeAction) responsesRemote(ctx context.Context) error { if vnext { url = fmt.Sprintf( "%s/agents/%s/endpoint/protocols/openai/responses?api-version=%s", - endpoint, name, DefaultAgentAPIVersion, + projectEndpoint, name, DefaultAgentAPIVersion, ) } else { - url = fmt.Sprintf("%s/openai/responses?api-version=%s", endpoint, DefaultAgentAPIVersion) + url = fmt.Sprintf("%s/openai/responses?api-version=%s", projectEndpoint, DefaultAgentAPIVersion) } req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(payload)) if err != nil { @@ -912,7 +912,10 @@ func handleInvocationLRO( // createConversation creates a new Foundry conversation for multi-turn memory. func createConversation(ctx context.Context, projectEndpoint, agentName, bearerToken string) (string, error) { - url := fmt.Sprintf("%s/agents/%s/endpoint/protocols/openai/conversations?api-version=v1", projectEndpoint, agentName) + url := fmt.Sprintf( + "%s/agents/%s/endpoint/protocols/openai/conversations?api-version=%s", + projectEndpoint, agentName, ConversationsAPIVersion, + ) req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader([]byte("{}"))) if err != nil { return "", err diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_test.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_test.go index 5b3d78851f6..3011436b344 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_test.go @@ -828,3 +828,127 @@ type pollStep struct { retryAfter string repeat bool } + +func TestCreateConversation(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + agentName string + statusCode int + body string + wantID string + wantErr bool + errContain string + }{ + { + name: "success returns conversation ID", + agentName: "my-agent", + statusCode: 200, + body: `{"id":"conv-abc123"}`, + wantID: "conv-abc123", + }, + { + name: "HTTP 400 returns error", + agentName: "my-agent", + statusCode: 400, + body: `{"error":"bad request"}`, + wantErr: true, + errContain: "failed with HTTP 400", + }, + { + name: "HTTP 500 returns error", + agentName: "my-agent", + statusCode: 500, + body: `{"error":"internal"}`, + wantErr: true, + errContain: "failed with HTTP 500", + }, + { + name: "response missing id field", + agentName: "my-agent", + statusCode: 200, + body: `{"status":"ok"}`, + wantErr: true, + errContain: "missing 'id' field", + }, + { + name: "response with non-string id", + agentName: "my-agent", + statusCode: 200, + body: `{"id":12345}`, + wantErr: true, + errContain: "missing 'id' field", + }, + { + name: "invalid JSON response", + agentName: "my-agent", + statusCode: 200, + body: `not-json`, + wantErr: true, + errContain: "invalid character", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Verify request method + if r.Method != http.MethodPost { + t.Errorf("method = %s, want POST", r.Method) + } + + // Verify path includes the agent name and conversations endpoint + wantPath := "/agents/" + tt.agentName + + "/endpoint/protocols/openai/conversations" + if r.URL.Path != wantPath { + t.Errorf("path = %s, want %s", r.URL.Path, wantPath) + } + + // Verify api-version query parameter uses the constant + if got := r.URL.Query().Get("api-version"); got != ConversationsAPIVersion { + t.Errorf("api-version = %q, want %q", got, ConversationsAPIVersion) + } + + // Verify auth header + if got := r.Header.Get("Authorization"); got != "Bearer test-token" { + t.Errorf("Authorization = %q, want %q", got, "Bearer test-token") + } + + // Verify content type + if got := r.Header.Get("Content-Type"); got != "application/json" { + t.Errorf("Content-Type = %q, want %q", got, "application/json") + } + + w.WriteHeader(tt.statusCode) + w.Write([]byte(tt.body)) + })) + defer srv.Close() + + id, err := createConversation( + t.Context(), srv.URL, tt.agentName, "test-token", + ) + + if tt.wantErr { + if err == nil { + t.Fatal("expected error, got nil") + } + if tt.errContain != "" && + !strings.Contains(err.Error(), tt.errContain) { + t.Errorf("error = %q, want substring %q", + err.Error(), tt.errContain) + } + return + } + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if id != tt.wantID { + t.Errorf("id = %q, want %q", id, tt.wantID) + } + }) + } +} From 4cc7b293df14590e32fbf9d3a76604405a89a71d Mon Sep 17 00:00:00 2001 From: John Miller Date: Mon, 20 Apr 2026 08:48:40 -0400 Subject: [PATCH 4/4] fix: handle error from Write in TestCreateConversation --- cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_test.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_test.go index 3011436b344..38695480a99 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_test.go @@ -923,7 +923,7 @@ func TestCreateConversation(t *testing.T) { } w.WriteHeader(tt.statusCode) - w.Write([]byte(tt.body)) + _, _ = w.Write([]byte(tt.body)) })) defer srv.Close()