Skip to content

Commit e55c60d

Browse files
authored
More LLM detections for other CLI agents + alter TL behavior in the presence of LLMs (#51054)
1 parent 9735fb9 commit e55c60d

File tree

12 files changed

+167
-58
lines changed

12 files changed

+167
-58
lines changed

documentation/project-docs/telemetry.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ Every telemetry event automatically includes these common properties:
6161
| **Telemetry Profile** | Custom telemetry profile (if set via env var) | Custom value or null |
6262
| **Docker Container** | Whether running in Docker container | `True` or `False` |
6363
| **CI** | Whether running in CI environment | `True` or `False` |
64-
| **LLM** | Detected LLM/assistant environment identifiers (comma-separated) | `claude` or `cursor` |
64+
| **LLM** | Detected LLM/assistant environment identifiers (comma-separated) | `claude`, `cursor`, `gemini`, `copilot`, `generic_agent` |
6565
| **Current Path Hash** | SHA256 hash of current directory path | Hashed value |
6666
| **Machine ID** | SHA256 hash of machine MAC address (or GUID if unavailable) | Hashed value |
6767
| **Machine ID Old** | Legacy machine ID for compatibility | Hashed value |

src/Cli/Microsoft.DotNet.Cli.Utils/Constants.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,14 @@ public static class Constants
3535
public const string Identity = nameof(Identity);
3636
public const string FullPath = nameof(FullPath);
3737

38+
// MSBuild CLI flags
39+
40+
/// <summary>
41+
/// Disables the live-updating node display in the terminal logger, which is useful for LLM/agentic environments.
42+
/// </summary>
43+
public const string TerminalLogger_DisableNodeDisplay = "-tlp:DISABLENODEDISPLAY";
44+
45+
3846
public static readonly string ProjectArgumentName = "<PROJECT>";
3947
public static readonly string SolutionArgumentName = "<SLN_FILE>";
4048
public static readonly string ToolPackageArgumentName = "<PACKAGE_ID>";

src/Cli/Microsoft.DotNet.Cli.Utils/MSBuildForwardingAppWithoutLogging.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ public MSBuildForwardingAppWithoutLogging(MSBuildArgs msbuildArgs, string? msbui
5656
msbuildArgs.OtherMSBuildArgs.Add("-nologo");
5757
}
5858
string? tlpDefault = TerminalLoggerDefault;
59-
// new for .NET 9 - default TL to auto (aka enable in non-CI scenarios)
6059
if (string.IsNullOrWhiteSpace(tlpDefault))
6160
{
6261
tlpDefault = "auto";

src/Cli/dotnet/Commands/MSBuild/MSBuildForwardingApp.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System.Diagnostics;
55
using System.Reflection;
6+
using Microsoft.DotNet.Cli.Commands.Run;
67
using Microsoft.DotNet.Cli.Utils;
78
using Microsoft.DotNet.Cli.Utils.Extensions;
89

@@ -14,6 +15,9 @@ public class MSBuildForwardingApp : CommandBase
1415

1516
private readonly MSBuildForwardingAppWithoutLogging _forwardingAppWithoutLogging;
1617

18+
/// <summary>
19+
/// Adds the CLI's telemetry logger to the MSBuild arguments if telemetry is enabled.
20+
/// </summary>
1721
private static MSBuildArgs ConcatTelemetryLogger(MSBuildArgs msbuildArgs)
1822
{
1923
if (Telemetry.Telemetry.CurrentSessionId != null)
@@ -45,8 +49,9 @@ public MSBuildForwardingApp(IEnumerable<string> rawMSBuildArgs, string? msbuildP
4549

4650
public MSBuildForwardingApp(MSBuildArgs msBuildArgs, string? msbuildPath = null, bool includeLogo = false)
4751
{
52+
var modifiedMSBuildArgs = CommonRunHelpers.AdjustMSBuildForLLMs(ConcatTelemetryLogger(msBuildArgs));
4853
_forwardingAppWithoutLogging = new MSBuildForwardingAppWithoutLogging(
49-
ConcatTelemetryLogger(msBuildArgs),
54+
modifiedMSBuildArgs,
5055
msbuildPath: msbuildPath,
5156
includeLogo: includeLogo);
5257

src/Cli/dotnet/Commands/Run/CommonRunHelpers.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,29 @@ public static string GetPropertiesLaunchSettingsPath(string directoryPath, strin
2626

2727
public static string GetFlatLaunchSettingsPath(string directoryPath, string projectNameWithoutExtension)
2828
=> Path.Join(directoryPath, $"{projectNameWithoutExtension}.run.json");
29+
30+
31+
/// <summary>
32+
/// Applies adjustments to MSBuild arguments to better suit LLM/agentic environments, if such an environment is detected.
33+
/// </summary>
34+
public static MSBuildArgs AdjustMSBuildForLLMs(MSBuildArgs msbuildArgs)
35+
{
36+
if (new Telemetry.LLMEnvironmentDetectorForTelemetry().IsLLMEnvironment())
37+
{
38+
// disable the live-update display of the TerminalLogger, which wastes tokens
39+
return msbuildArgs.CloneWithAdditionalArgs(Constants.TerminalLogger_DisableNodeDisplay);
40+
}
41+
else
42+
{
43+
return msbuildArgs;
44+
}
45+
}
46+
47+
/// <summary>
48+
/// Creates a TerminalLogger or ConsoleLogger based on the provided MSBuild arguments.
49+
/// If the environment is detected to be an LLM environment, the logger is adjusted to
50+
/// better suit that environment.
51+
/// </summary>
52+
public static Microsoft.Build.Framework.ILogger GetConsoleLogger(MSBuildArgs args) =>
53+
Microsoft.Build.Logging.TerminalLogger.CreateTerminalOrConsoleLogger([.. AdjustMSBuildForLLMs(args).OtherMSBuildArgs]);
2954
}

src/Cli/dotnet/Commands/Run/RunCommand.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -483,7 +483,9 @@ static ICommand CreateCommandForCscBuiltProgram(string entryPointFileFullPath, s
483483
static void InvokeRunArgumentsTarget(ProjectInstance project, bool noBuild, FacadeLogger? binaryLogger, MSBuildArgs buildArgs)
484484
{
485485
List<ILogger> loggersForBuild = [
486-
TerminalLogger.CreateTerminalOrConsoleLogger([$"--verbosity:{LoggerVerbosity.Quiet.ToString().ToLowerInvariant()}", ..buildArgs.OtherMSBuildArgs])
486+
CommonRunHelpers.GetConsoleLogger(
487+
buildArgs.CloneWithExplicitArgs([$"--verbosity:{LoggerVerbosity.Quiet.ToString().ToLowerInvariant()}", ..buildArgs.OtherMSBuildArgs])
488+
)
487489
];
488490
if (binaryLogger is not null)
489491
{

src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ public override int Execute()
190190
var verbosity = MSBuildArgs.Verbosity ?? MSBuildForwardingAppWithoutLogging.DefaultVerbosity;
191191
var consoleLogger = minimizeStdOut
192192
? new SimpleErrorLogger()
193-
: TerminalLogger.CreateTerminalOrConsoleLogger([$"--verbosity:{verbosity}", .. MSBuildArgs.OtherMSBuildArgs]);
193+
: CommonRunHelpers.GetConsoleLogger(MSBuildArgs.CloneWithExplicitArgs([$"--verbosity:{verbosity}", .. MSBuildArgs.OtherMSBuildArgs]));
194194
var binaryLogger = GetBinaryLogger(MSBuildArgs.OtherMSBuildArgs);
195195

196196
CacheInfo? cache = null;

src/Cli/dotnet/Telemetry/EnvironmentDetectionRule.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Collections.Generic;
66
using System.Linq;
7+
using Microsoft.DotNet.Cli.Utils;
78

89
namespace Microsoft.DotNet.Cli.Telemetry;
910

@@ -33,8 +34,7 @@ public BooleanEnvironmentRule(params string[] variables)
3334

3435
public override bool IsMatch()
3536
{
36-
return _variables.Any(variable =>
37-
bool.TryParse(Environment.GetEnvironmentVariable(variable), out bool value) && value);
37+
return _variables.Any(variable => Env.GetEnvironmentVariableAsBool(variable));
3838
}
3939
}
4040

@@ -81,12 +81,12 @@ public override bool IsMatch()
8181
/// <typeparam name="T">The type of the result value.</typeparam>
8282
internal class EnvironmentDetectionRuleWithResult<T> where T : class
8383
{
84-
private readonly string[] _variables;
84+
private readonly EnvironmentDetectionRule _rule;
8585
private readonly T _result;
8686

87-
public EnvironmentDetectionRuleWithResult(T result, params string[] variables)
87+
public EnvironmentDetectionRuleWithResult(T result, EnvironmentDetectionRule rule)
8888
{
89-
_variables = variables ?? throw new ArgumentNullException(nameof(variables));
89+
_rule = rule ?? throw new ArgumentNullException(nameof(rule));
9090
_result = result ?? throw new ArgumentNullException(nameof(result));
9191
}
9292

@@ -96,8 +96,8 @@ public EnvironmentDetectionRuleWithResult(T result, params string[] variables)
9696
/// <returns>The result value if the rule matches; otherwise, null.</returns>
9797
public T? GetResult()
9898
{
99-
return _variables.Any(variable => !string.IsNullOrEmpty(Environment.GetEnvironmentVariable(variable)))
100-
? _result
99+
return _rule.IsMatch()
100+
? _result
101101
: null;
102102
}
103-
}
103+
}

src/Cli/dotnet/Telemetry/ILLMEnvironmentDetector.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,13 @@ namespace Microsoft.DotNet.Cli.Telemetry;
55

66
internal interface ILLMEnvironmentDetector
77
{
8+
/// <summary>
9+
/// Checks the current environment for known indicators of LLM usage and returns a string identifying the LLM environment if detected.
10+
/// </summary>
811
string? GetLLMEnvironment();
9-
}
12+
13+
/// <summary>
14+
/// Returns true if the current environment is detected to be an LLM/agentic environment, false otherwise.
15+
/// </summary>
16+
bool IsLLMEnvironment();
17+
}
Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,30 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System;
5-
using System.Linq;
6-
74
namespace Microsoft.DotNet.Cli.Telemetry;
85

96
internal class LLMEnvironmentDetectorForTelemetry : ILLMEnvironmentDetector
107
{
118
private static readonly EnvironmentDetectionRuleWithResult<string>[] _detectionRules = [
129
// Claude Code
13-
new EnvironmentDetectionRuleWithResult<string>("claude", "CLAUDECODE"),
10+
new EnvironmentDetectionRuleWithResult<string>("claude", new AnyPresentEnvironmentRule("CLAUDECODE")),
1411
// Cursor AI
15-
new EnvironmentDetectionRuleWithResult<string>("cursor", "CURSOR_EDITOR")
12+
new EnvironmentDetectionRuleWithResult<string>("cursor", new AnyPresentEnvironmentRule("CURSOR_EDITOR")),
13+
// Gemini
14+
new EnvironmentDetectionRuleWithResult<string>("gemini", new BooleanEnvironmentRule("GEMINI_CLI")),
15+
// GitHub Copilot
16+
new EnvironmentDetectionRuleWithResult<string>("copilot", new BooleanEnvironmentRule("GITHUB_COPILOT_CLI_MODE")),
17+
// (proposed) generic flag for Agentic usage
18+
new EnvironmentDetectionRuleWithResult<string>("generic_agent", new BooleanEnvironmentRule("AGENT_CLI")),
1619
];
1720

21+
/// <inheritdoc/>
1822
public string? GetLLMEnvironment()
1923
{
2024
var results = _detectionRules.Select(r => r.GetResult()).Where(r => r != null).ToArray();
2125
return results.Length > 0 ? string.Join(", ", results) : null;
2226
}
23-
}
27+
28+
/// <inheritdoc/>
29+
public bool IsLLMEnvironment() => !string.IsNullOrEmpty(GetLLMEnvironment());
30+
}

0 commit comments

Comments
 (0)