Skip to content

fix(a2a): percent-encode context id fields so ids with separators round-trip#6407

Open
anxkhn wants to merge 1 commit into
google:mainfrom
anxkhn:fix/a2a-context-id-roundtrip-separator
Open

fix(a2a): percent-encode context id fields so ids with separators round-trip#6407
anxkhn wants to merge 1 commit into
google:mainfrom
anxkhn:fix/a2a-context-id-roundtrip-separator

Conversation

@anxkhn

@anxkhn anxkhn commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Please ensure you have read the contribution guide before creating a pull request.

Link to Issue or Description of Change

1. Link to an existing issue (if applicable):

  • N/A (no dedicated issue; the bug is described below per the issue-template structure, as CONTRIBUTING allows for non-doc PRs).

2. Or, if no issue exists, describe the change:

Problem:

The A2A context id codec in src/google/adk/a2a/converters/utils.py is not
invertible when app_name, user_id, or session_id contains the /
separator. _to_a2a_context_id joins [ADK, app_name, user_id, session_id] on
ADK_CONTEXT_ID_SEPARATOR ("/"), and _from_a2a_context_id splits on "/"
and requires exactly four parts, otherwise it returns (None, None, None). So
when any field contains /, the join produces more than four segments and the
inverse silently fails:

from google.adk.a2a.converters.utils import (
    _to_a2a_context_id,
    _from_a2a_context_id,
)

cid = _to_a2a_context_id("app", "user", "projects/p/sessions/s")
print(_from_a2a_context_id(cid))
# expected: ('app', 'user', 'projects/p/sessions/s')
# actual:   (None, None, None)

A returned (None, None, None) means A2A session resolution silently fails for
those ids. Fully-qualified resource paths are a realistic source of embedded
/ (for example resource paths used as a user_id, or an Agent Engine session
path used as a session_id).

The decode path also had a stale docstring that still described the old
ADK$app_name$user_id$session_id format, and an unreachable except ValueError
around str.split (which never raises), which masked the failure mode.

Solution:

Percent-encode each field on encode (quote(field, safe="")) and decode it on
decode (unquote(...)). This is the smallest change that:

  • keeps the exact four-part ADK/<app>/<user>/<session> structure (so the
    existing "too many parts" rejection still holds),
  • is invertible for a separator in ANY field (a bounded split(sep, 3) would
    only fix session_id, not user_id/app_name, and would break the existing
    too-many-parts test),
  • leaves ids without special characters byte-identical to the previous format,
    so there is no breaking change on the wire for the common case.

I also fixed the stale $-format docstring and removed the dead
except ValueError.

Testing Plan

Unit Tests:

  • I have added or updated unit tests for my change.
  • All unit tests pass locally.

Added a parametrized regression test
test_roundtrip_context_id_with_separator_in_ids covering / in session_id,
in user_id, in app_name, and a multi-slash session id. It fails on the
current code (each case returns (None, None, None)) and passes with the fix.
Also updated test_to_a2a_context_id_special_characters to expect the
percent-encoded form (%40 for @).

$ pytest tests/unittests/a2a/converters/test_utils.py -q
24 passed

$ pytest tests/unittests/a2a/ -q
366 passed

pre-commit run --files src/google/adk/a2a/converters/utils.py tests/unittests/a2a/converters/test_utils.py
passes (ruff, isort, pyink, addlicense, ADK compliance checks) with no
reformatting.

Manual End-to-End (E2E) Tests:

This is a self-contained, pure-function correctness fix in the A2A context id
codec with no runtime/UI surface, so it is exercised by the unit tests above
rather than an adk web/runner flow. The snippet in the Problem section is a
minimal manual repro (returns (None, None, None) before the fix, the original
ids after).

Checklist

  • I have read the CONTRIBUTING.md document.
  • I have performed a self-review of my own code.
  • I have commented my code, particularly in hard-to-understand areas.
  • I have added tests that prove my fix is effective or that my feature works.
  • New and existing unit tests pass locally with my changes.
  • I have manually tested my changes end-to-end.
  • Any dependent changes have been merged and published in downstream modules. (N/A)

Additional context

The percent-encode approach was chosen over a bounded left split because it fixes
every field (not just session_id) and preserves the existing four-part
validation (including the "too many parts" rejection test). Ids without special
characters are unchanged on the wire.

…nd-trip

_to_a2a_context_id joins [ADK, app_name, user_id, session_id] on "/" and
_from_a2a_context_id splits on "/" and requires exactly four parts. When any of
app_name, user_id or session_id contains "/" (for example fully-qualified
resource paths used as ids), the join produces more than four segments and the
inverse silently returns (None, None, None), so A2A session resolution fails for
those ids.

Percent-encode each field on encode and decode it on decode. This keeps the
exact four-part structure, remains invertible for a separator in any field, and
leaves ids without special characters byte-identical to the previous format.
Also fix the stale docstring that still described the old "$"-delimited format
and drop the unreachable except ValueError (str.split never raises).

Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com>
@adk-bot adk-bot added the core [Component] This issue is related to the core interface and implementation label Jul 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core [Component] This issue is related to the core interface and implementation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants