Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
70 changes: 67 additions & 3 deletions cli/azd/extensions/azure.ai.agents/internal/cmd/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
Expand Down Expand Up @@ -103,12 +104,21 @@ func saveContextValue(
}
configPath, err := resolveConfigPath(ctx, azdClient)
if err != nil {
log.Printf("saveContextValue: failed to resolve config path: %v", err)
return
}
if err := saveContextValueToPath(configPath, agentKey, value, storeField); err != nil {
log.Printf("saveContextValue: failed to persist %s for %q: %v", storeField, agentKey, err)
}
}

// saveContextValueToPath persists a value into the named field of the local config at configPath.
// This is the path-based implementation used by saveContextValue and directly in tests.
func saveContextValueToPath(configPath, agentKey, value, storeField string) error {
agentCtx := loadLocalContext(configPath)
store := contextMap(agentCtx, storeField)
store[agentKey] = value
_ = saveLocalContext(agentCtx, configPath)
return saveLocalContext(agentCtx, configPath)
}

// resolveLocalAgentKey builds the storage key for local mode from the azd project config.
Expand Down Expand Up @@ -139,6 +149,9 @@ func resolveStoredID(
generateIfMissing bool,
) (string, error) {
if explicit != "" {
// Persist the explicit ID so that subsequent commands (e.g. monitor, invoke)
// can find it without the user passing it again.
persistExplicitID(ctx, azdClient, agentKey, explicit, storeField)
return explicit, nil
Comment thread
therealjohn marked this conversation as resolved.
}
if forceNew && !generateIfMissing {
Expand Down Expand Up @@ -173,7 +186,56 @@ func resolveStoredID(
return newID, nil
}

// contextMap returns the named map from AgentLocalContext, initializing it if nil.
// persistExplicitID saves a user-provided explicit ID to the local config.
// Errors are logged for debug visibility but do not block the caller.
func persistExplicitID(
ctx context.Context,
azdClient *azdext.AzdClient,
agentKey string,
value string,
storeField string,
) {
saveContextValue(ctx, azdClient, agentKey, value, storeField)
}

// resolveStoredIDFromPath is a testable variant of resolveStoredID that operates on a
// config file path directly, removing the need for an azdClient.
func resolveStoredIDFromPath(
configPath string,
agentKey string,
explicit string,
forceNew bool,
storeField string,
generateIfMissing bool,
) (string, error) {
if explicit != "" {
if err := saveContextValueToPath(configPath, agentKey, explicit, storeField); err != nil {
log.Printf("resolveStoredIDFromPath: failed to persist explicit %s for %q: %v", storeField, agentKey, err)
}
return explicit, nil
}
if forceNew && !generateIfMissing {
return "", nil
}

agentCtx := loadLocalContext(configPath)
store := contextMap(agentCtx, storeField)
if !forceNew {
if id, ok := store[agentKey]; ok {
return id, nil
}
}

if !generateIfMissing {
return "", nil
}

newID := uuid.NewString()
store[agentKey] = newID
_ = saveLocalContext(agentCtx, configPath)

return newID, nil
}
func contextMap(agentCtx *AgentLocalContext, field string) map[string]string {
switch field {
case "sessions":
Expand Down Expand Up @@ -292,7 +354,7 @@ func fetchOpenAPISpec(
}

// resolveConversationID resolves a Foundry conversation ID.
// When explicit is provided, it is returned directly.
// When explicit is provided, it is persisted and returned directly.
// If no conversation is found (or forceNew is true), it attempts to create one and persist it.
// Returns empty string when conversation creation fails, allowing invoke to continue without multi-turn memory.
func resolveConversationID(
Expand All @@ -305,6 +367,8 @@ func resolveConversationID(
bearerToken string,
) (string, error) {
if explicit != "" {
// Persist the explicit conversation ID so subsequent commands can find it.
saveContextValue(ctx, azdClient, agentName, explicit, "conversations")
return explicit, nil
}
configPath, err := resolveConfigPath(ctx, azdClient)
Expand Down
65 changes: 65 additions & 0 deletions cli/azd/extensions/azure.ai.agents/internal/cmd/monitor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,71 @@ func TestValidateMonitorFlags_ErrorMessages(t *testing.T) {
assert.Contains(t, err.Error(), "badtype")
}

func TestExplicitSessionID_PersistedAndReusable(t *testing.T) {
t.Parallel()

dir := t.TempDir()
configPath := filepath.Join(dir, ConfigFile)

agentName := "my-agent"
explicitSID := "user-provided-session-id"

// Before: no sessions stored
agentCtx := loadLocalContext(configPath)
assert.Empty(t, agentCtx.Sessions)

// Call resolveStoredIDFromPath with an explicit ID — this is the code path
// that resolveStoredID follows when the user passes --session-id.
got, err := resolveStoredIDFromPath(configPath, agentName, explicitSID, false, "sessions", false)
require.NoError(t, err)
assert.Equal(t, explicitSID, got)

// Verify the ID was persisted (as monitor's resolveMonitorSession would load it)
loaded := loadLocalContext(configPath)
require.NotNil(t, loaded.Sessions)
assert.Equal(t, explicitSID, loaded.Sessions[agentName])
}
Comment thread
therealjohn marked this conversation as resolved.

func TestExplicitConversationID_PersistedAndReusable(t *testing.T) {
t.Parallel()

dir := t.TempDir()
configPath := filepath.Join(dir, ConfigFile)

agentName := "my-agent"
explicitConvID := "user-provided-conv-id"

got, err := resolveStoredIDFromPath(configPath, agentName, explicitConvID, false, "conversations", false)
require.NoError(t, err)
assert.Equal(t, explicitConvID, got)

loaded := loadLocalContext(configPath)
require.NotNil(t, loaded.Conversations)
assert.Equal(t, explicitConvID, loaded.Conversations[agentName])
}

func TestExplicitID_OverwritesPreviousValue(t *testing.T) {
t.Parallel()

dir := t.TempDir()
configPath := filepath.Join(dir, ConfigFile)

agentName := "my-agent"

// Persist an initial session ID via resolveStoredIDFromPath
got, err := resolveStoredIDFromPath(configPath, agentName, "old-session-id", false, "sessions", false)
require.NoError(t, err)
assert.Equal(t, "old-session-id", got)

// Persist a different explicit session ID — should overwrite the previous one
got, err = resolveStoredIDFromPath(configPath, agentName, "new-session-id", false, "sessions", false)
require.NoError(t, err)
assert.Equal(t, "new-session-id", got)

loaded := loadLocalContext(configPath)
assert.Equal(t, "new-session-id", loaded.Sessions[agentName])
}

func TestValidateMonitorFlags_SessionDoesNotAffectValidation(t *testing.T) {
t.Parallel()

Expand Down
Loading