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 25c032f5251..4dd0c4f0a7a 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/helpers.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/helpers.go @@ -9,6 +9,7 @@ import ( "encoding/json" "fmt" "io" + "log" "net/http" "os" "path/filepath" @@ -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. @@ -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 } if forceNew && !generateIfMissing { @@ -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": @@ -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( @@ -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) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/monitor_test.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/monitor_test.go index 93e7d1d4418..fed7778af41 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/monitor_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/monitor_test.go @@ -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]) +} + +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()