From c160b7bedd30fc230a30724b1d1820561d287bc9 Mon Sep 17 00:00:00 2001 From: stakeswky Date: Wed, 25 Feb 2026 08:42:11 +0800 Subject: [PATCH 1/2] fix: restore double-brace escaping in instruction templates Double braces (e.g. {{city}}) should produce literal {city} in the rendered instruction, not trigger a session state lookup. This escaping worked in v1.25.0 but regressed in v1.25.1 because the regex r'{+[^{}]*}+' matches {{city}} and _replace_match strips all braces before looking up the variable name. The fix adds an early return in _replace_match: when the matched text starts with {{ and ends with }}, peel one layer of braces and return the literal string without any state/artifact lookup. Fixes #4606 --- src/google/adk/utils/instructions_utils.py | 6 ++- .../utils/test_instructions_utils.py | 42 +++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/google/adk/utils/instructions_utils.py b/src/google/adk/utils/instructions_utils.py index 505b5cf128..c4a44e1fbe 100644 --- a/src/google/adk/utils/instructions_utils.py +++ b/src/google/adk/utils/instructions_utils.py @@ -79,7 +79,11 @@ 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] + var_name = raw.lstrip('{').rstrip('}').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}}" From 302c57516b1c24e2fa37de412cb1d7e8014296de Mon Sep 17 00:00:00 2001 From: stakeswky Date: Wed, 25 Feb 2026 09:37:15 +0800 Subject: [PATCH 2/2] refactor: use slicing instead of lstrip/rstrip for safer brace parsing Per review suggestion, replace lstrip('{').rstrip('}') with raw[1:-1] to avoid stripping extra braces from malformed inputs. --- src/google/adk/utils/instructions_utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/google/adk/utils/instructions_utils.py b/src/google/adk/utils/instructions_utils.py index c4a44e1fbe..15b3f1f47f 100644 --- a/src/google/adk/utils/instructions_utils.py +++ b/src/google/adk/utils/instructions_utils.py @@ -83,7 +83,9 @@ async def _replace_match(match) -> str: # Double (or more) braces are escape sequences: {{x}} → {x} if raw.startswith('{{') and raw.endswith('}}'): return raw[1:-1] - var_name = raw.lstrip('{').rstrip('}').strip() + # 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