Description
inject_session_state() in instructions_utils.py applies template variable substitution using the regex r'{+[^{}]*}+' across the entire instruction string, including embedded tool descriptions, JSON schemas, and other content that the agent author did not intend as template variables. This causes KeyError crashes when instruction text contains literal {identifier} patterns — such as documentation describing interpolation syntax (e.g., ${expression}).
This has already caused a downstream bug in the A2UI project, where the formatString function description in basic_catalog.json contains ${expression} as documentation text. The ADK's regex matches it, expression passes str.isidentifier(), and the lookup against an empty session state raises:
KeyError: 'Context variable not found: `expression`.'
Current Behavior
inject_session_state() scans the full instruction string with r'{+[^{}]*}+'
- Any match where the inner text passes
_is_valid_state_name() (i.e., str.isidentifier()) is treated as a template variable
- If that variable doesn't exist in session state, a
KeyError is raised
- There is no way to escape or opt out of this behavior for literal text that happens to match the pattern
Expected Behavior
Instruction text containing literal {identifier} patterns (especially in embedded tool descriptions, JSON schemas, or documentation) should not crash the agent. The template engine should either:
- Only substitute explicitly opted-in template variables — e.g., require a more specific delimiter like
{{var}} or ${var} that is less likely to collide with documentation text
- Gracefully handle missing variables — treat unresolved identifiers as literal text (return
match.group() as-is) instead of raising KeyError, or at minimum log a warning. The ? suffix opt-in for optional variables exists, but the default should arguably be lenient since instruction strings often contain content the author doesn't control.
- Provide an escape mechanism — allow literal braces to be escaped (e.g.,
\{expression\} or {{expression}} passes through without substitution)
Reproduction
from google.adk.agents import Agent
# This instruction contains a literal {expression} in documentation text
agent = Agent(
model="gemini-2.0-flash",
name="test_agent",
instruction="The formatString supports interpolation via ${expression} syntax.",
)
# Running this agent via `adk run` will crash on the first message with:
# KeyError: 'Context variable not found: `expression`.'
Impact
- A2UI integration is broken when using
adk run (see google/A2UI#1388). The A2UI team had to work around this by changing their spec documentation to use ${<expression>} so it fails the isidentifier() check.
- Any agent that embeds third-party tool descriptions, JSON schemas, or documentation examples into its instruction string is vulnerable to this class of crash.
- The existing A2A server samples work around it by pre-seeding
"expression": "{expression}" into session state — a fragile hack.
Relevant Code
src/google/adk/utils/instructions_utils.py Lines 85-128
async def _replace_match(match) -> str:
var_name = match.group().lstrip('{').rstrip('}').strip()
# ...
else:
if not _is_valid_state_name(var_name):
return match.group() # ← safe: invalid identifiers pass through
if var_name in invocation_context.session.state:
value = invocation_context.session.state[var_name]
# ...
else:
# ...
raise KeyError(f'Context variable not found: `{var_name}`.') # ← crash
return await _async_sub(r'{+[^{}]*}+', _replace_match, template)
Related Issues
Suggested Fix
The least disruptive fix would be to change the default behavior for unresolved-but-valid identifiers from raising KeyError to returning the match as-is (same as what happens for invalid identifiers), and log a debug warning. This makes the template engine lenient by default while preserving the ? suffix mechanism for authors who explicitly want empty-string fallback behavior.
else:
- if optional:
- logger.debug(
- 'Context variable %s not found, replacing with empty string',
- var_name,
- )
- return ''
- else:
- raise KeyError(f'Context variable not found: `{var_name}`.')
+ if optional:
+ logger.debug(
+ 'Context variable %s not found, replacing with empty string',
+ var_name,
+ )
+ return ''
+ else:
+ logger.debug(
+ 'Context variable %s not found in session state, returning as-is',
+ var_name,
+ )
+ return match.group()
Description
inject_session_state()ininstructions_utils.pyapplies template variable substitution using the regexr'{+[^{}]*}+'across the entire instruction string, including embedded tool descriptions, JSON schemas, and other content that the agent author did not intend as template variables. This causesKeyErrorcrashes when instruction text contains literal{identifier}patterns — such as documentation describing interpolation syntax (e.g.,${expression}).This has already caused a downstream bug in the A2UI project, where the
formatStringfunction description inbasic_catalog.jsoncontains${expression}as documentation text. The ADK's regex matches it,expressionpassesstr.isidentifier(), and the lookup against an empty session state raises:Current Behavior
inject_session_state()scans the full instruction string withr'{+[^{}]*}+'_is_valid_state_name()(i.e.,str.isidentifier()) is treated as a template variableKeyErroris raisedExpected Behavior
Instruction text containing literal
{identifier}patterns (especially in embedded tool descriptions, JSON schemas, or documentation) should not crash the agent. The template engine should either:{{var}}or${var}that is less likely to collide with documentation textmatch.group()as-is) instead of raisingKeyError, or at minimum log a warning. The?suffix opt-in for optional variables exists, but the default should arguably be lenient since instruction strings often contain content the author doesn't control.\{expression\}or{{expression}}passes through without substitution)Reproduction
Impact
adk run(see google/A2UI#1388). The A2UI team had to work around this by changing their spec documentation to use${<expression>}so it fails theisidentifier()check."expression": "{expression}"into session state — a fragile hack.Relevant Code
src/google/adk/utils/instructions_utils.pyLines 85-128Related Issues
Suggested Fix
The least disruptive fix would be to change the default behavior for unresolved-but-valid identifiers from raising
KeyErrorto returning the match as-is (same as what happens for invalid identifiers), and log a debug warning. This makes the template engine lenient by default while preserving the?suffix mechanism for authors who explicitly want empty-string fallback behavior.