diff --git a/src/google/adk/utils/instructions_utils.py b/src/google/adk/utils/instructions_utils.py index 505b5cf128..15b3f1f47f 100644 --- a/src/google/adk/utils/instructions_utils.py +++ b/src/google/adk/utils/instructions_utils.py @@ -79,7 +79,13 @@ async def _async_sub(pattern, repl_async_fn, string) -> str: return ''.join(result) async def _replace_match(match) -> str: - var_name = match.group().lstrip('{').rstrip('}').strip() + raw = match.group() + # Double (or more) braces are escape sequences: {{x}} → {x} + if raw.startswith('{{') and raw.endswith('}}'): + return raw[1:-1] + # Use slicing instead of lstrip/rstrip to avoid stripping extra braces + # from malformed inputs like '{var}}}'. + var_name = raw[1:-1].strip() optional = False if var_name.endswith('?'): optional = True diff --git a/tests/unittests/utils/test_instructions_utils.py b/tests/unittests/utils/test_instructions_utils.py index d76e5032ec..6d42f17035 100644 --- a/tests/unittests/utils/test_instructions_utils.py +++ b/tests/unittests/utils/test_instructions_utils.py @@ -267,3 +267,45 @@ async def test_inject_session_state_with_optional_missing_state_returns_empty(): instruction_template, invocation_context ) assert populated_instruction == "Optional value: " + + +@pytest.mark.asyncio +async def test_double_braces_escape_to_literal(): + """Double braces {{x}} should produce literal {x}, not a state lookup.""" + instruction_template = 'Generate a keyword like "roofing cost in {{city}}".' + invocation_context = await _create_test_readonly_context() + + populated_instruction = await instructions_utils.inject_session_state( + instruction_template, invocation_context + ) + assert populated_instruction == ( + 'Generate a keyword like "roofing cost in {city}".' + ) + + +@pytest.mark.asyncio +async def test_double_braces_mixed_with_state_variable(): + """Double braces should escape while single braces still resolve state.""" + instruction_template = "Hello {user_name}, use {{placeholder}} in prompts." + invocation_context = await _create_test_readonly_context( + state={"user_name": "Alice"} + ) + + populated_instruction = await instructions_utils.inject_session_state( + instruction_template, invocation_context + ) + assert populated_instruction == ( + "Hello Alice, use {placeholder} in prompts." + ) + + +@pytest.mark.asyncio +async def test_triple_braces_peel_one_layer(): + """Triple braces {{{x}}} should peel one layer to {{x}}.""" + instruction_template = "Escaped: {{{example}}}" + invocation_context = await _create_test_readonly_context() + + populated_instruction = await instructions_utils.inject_session_state( + instruction_template, invocation_context + ) + assert populated_instruction == "Escaped: {{example}}"