Skip to content

inject_session_state raises KeyError on literal {identifier} patterns in instruction text #5706

Closed

Description

@brucearctor

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

  1. inject_session_state() scans the full instruction string with r'{+[^{}]*}+'
  2. Any match where the inner text passes _is_valid_state_name() (i.e., str.isidentifier()) is treated as a template variable
  3. If that variable doesn't exist in session state, a KeyError is raised
  4. 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:

  1. 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
  2. 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.
  3. 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()

Activity

  1. added theissue type on May 15, 2026
  2. added
    core[Component] This issue is related to the core interface and implementation
    on May 15, 2026
  3. klateefa commented on May 15, 2026

    @klateefa

    @brucearctor Thanks for picking this up and opening a PR for it. The proposed approach of returning the original match instead of raising a KeyError makes sense here, since instruction strings can legitimately contain literal patterns like ${expression} in docs, schemas, or tool descriptions. It’s a minimal and backward-compatible fix that prevents unexpected crashes while preserving existing substitution behavior. Please try this approach and let me know if the issue still persists.

  4. surajksharma07 commented on May 27, 2026

    @surajksharma07
    Collaborator

    @brucearctor — just following up here. The root cause is confirmed: instructions_utils.py raises KeyError at line 122 for any valid identifier not found in session state rather than returning the match as-is the way invalid identifiers already do — so a literal ${expression} in documentation text hits this and crashes.

    PR #5711 by @zhoufengen targets exactly the fix you proposed (returning match.group() instead of raising), though it hasn't merged yet.

    Since we haven't heard back since klateefa's comment two weeks ago, going ahead and closing this out — feel free to reopen or track the fix via PR #5711!

  5. added a commit that references this issue on Sep 19, 2026
    0caecd2
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

core[Component] This issue is related to the core interface and implementationrequest clarification[Status] The maintainer need clarification or more information from the author

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions