diff --git a/.changes/next-release/enhancement-UserAgent-53908.json b/.changes/next-release/enhancement-UserAgent-53908.json new file mode 100644 index 000000000000..1d6f18c4192e --- /dev/null +++ b/.changes/next-release/enhancement-UserAgent-53908.json @@ -0,0 +1,5 @@ +{ + "type": "enhancement", + "category": "User Agent", + "description": "Expose ``is_agentic_caller`` so callers can detect the agentic callers already reported in the User-Agent." +} diff --git a/awscli/botocore/useragent.py b/awscli/botocore/useragent.py index 51fddfb8bf2a..56bca025adb7 100644 --- a/awscli/botocore/useragent.py +++ b/awscli/botocore/useragent.py @@ -170,6 +170,23 @@ def _agentic_env_var_is_set(env_var, expected_value): return value == expected_value +def is_agentic_caller(): + """Return True if the current process appears to be driven by a known + agentic caller (e.g. Claude Code, Gemini CLI, Codex). + + Detection uses the same environment-variable signals that populate the + ``AGENTIC_CALLER_*`` metrics in the User-Agent header, so callers that + want to adapt their behavior for agents stay in sync with what the SDK + already reports. + """ + return any( + _agentic_env_var_is_set(env_var, expected_value) + for env_var, _feature_id, expected_value in ( + _USERAGENT_AGENTIC_CALLER_ENV_VAR_MAPPINGS + ) + ) + + def _register_agentic_caller_env_features(): for ( env_var, diff --git a/tests/unit/botocore/test_useragent.py b/tests/unit/botocore/test_useragent.py index 53f884647a1e..07665d9a8173 100644 --- a/tests/unit/botocore/test_useragent.py +++ b/tests/unit/botocore/test_useragent.py @@ -22,6 +22,7 @@ UserAgentComponent, UserAgentComponentSizeConfig, UserAgentString, + is_agentic_caller, register_feature_id, sanitize_user_agent_string_component, ) @@ -78,6 +79,40 @@ def test_sanitize_ua_string_component(raw_str, allow_hash, expected_str): assert actual_str == expected_str +def test_is_agentic_caller_false_when_no_env_set(): + # clear_agentic_caller_env fixture unsets all agentic caller env vars. + assert is_agentic_caller() is False + + +@pytest.mark.parametrize( + 'env_var, value', + [ + ('CLAUDECODE', '1'), + ('GEMINI_CLI', '1'), + ('CODEX_THREAD_ID', 'some-thread-id'), + ('KIRO_SESSION_ID', 'some-session-id'), + ('OPENCODE', 'anything'), + ('CURSOR_AGENT', '1'), + ('PI_CODING_AGENT', 'true'), + ], +) +def test_is_agentic_caller_true_when_env_set(monkeypatch, env_var, value): + monkeypatch.setenv(env_var, value) + assert is_agentic_caller() is True + + +def test_is_agentic_caller_false_when_value_does_not_match(monkeypatch): + # CLAUDECODE only counts when it equals '1'. + monkeypatch.setenv('CLAUDECODE', '0') + assert is_agentic_caller() is False + + +def test_is_agentic_caller_false_when_env_empty(monkeypatch): + # An env var expecting any non-empty value must not match ''. + monkeypatch.setenv('CODEX_THREAD_ID', '') + assert is_agentic_caller() is False + + def test_basic_user_agent_string(): ua = UserAgentString( platform_name='linux',