Skip to content

Commit b40accd

Browse files
fix(state): update state key references in instructions (#493)
* fix: update state key references * update docs to include info on state injection in instructions * update java snippet * add docs on InstructionProvider * add tooltip on direct state modification --------- Co-authored-by: Lavi Nigam <[email protected]>
1 parent a3354e3 commit b40accd

File tree

5 files changed

+121
-35
lines changed

5 files changed

+121
-35
lines changed

docs/agents/custom-agents.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,9 @@ Let's illustrate the power of custom agents with an example pattern: a multi-sta
197197

198198
These are standard `LlmAgent` definitions, responsible for specific tasks. Their `output key` parameter is crucial for placing results into the `session.state` where other agents or the custom orchestrator can access them.
199199

200+
!!! tip "Direct State Injection in Instructions"
201+
Notice the `story_generator`'s instruction. The `{var}` syntax is a placeholder. Before the instruction is sent to the LLM, the ADK framework automatically replaces (Example:`{topic}`) with the value of `session.state['topic']`. This is the recommended way to provide context to an agent, using templating in the instructions. For more details, see the [State documentation](../sessions/state.md#accessing-session-state-in-agent-instructions).
202+
200203
=== "Python"
201204

202205
```python

docs/agents/multi-agents.md

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ ADK includes specialized agents derived from `BaseAgent` that don't perform task
9191
from google.adk.agents import SequentialAgent, LlmAgent
9292

9393
step1 = LlmAgent(name="Step1_Fetch", output_key="data") # Saves output to state['data']
94-
step2 = LlmAgent(name="Step2_Process", instruction="Process data from state key 'data'.")
94+
step2 = LlmAgent(name="Step2_Process", instruction="Process data from {data}.")
9595

9696
pipeline = SequentialAgent(name="MyPipeline", sub_agents=[step1, step2])
9797
# When pipeline runs, Step2 can access the state['data'] set by Step1.
@@ -105,7 +105,7 @@ ADK includes specialized agents derived from `BaseAgent` that don't perform task
105105
import com.google.adk.agents.LlmAgent;
106106

107107
LlmAgent step1 = LlmAgent.builder().name("Step1_Fetch").outputKey("data").build(); // Saves output to state.get("data")
108-
LlmAgent step2 = LlmAgent.builder().name("Step2_Process").instruction("Process data from state key 'data'.").build();
108+
LlmAgent step2 = LlmAgent.builder().name("Step2_Process").instruction("Process data from {data}.").build();
109109

110110
SequentialAgent pipeline = SequentialAgent.builder().name("MyPipeline").subAgents(step1, step2).build();
111111
// When pipeline runs, Step2 can access the state.get("data") set by Step1.
@@ -243,7 +243,7 @@ The most fundamental way for agents operating within the same invocation (and th
243243
from google.adk.agents import LlmAgent, SequentialAgent
244244

245245
agent_A = LlmAgent(name="AgentA", instruction="Find the capital of France.", output_key="capital_city")
246-
agent_B = LlmAgent(name="AgentB", instruction="Tell me about the city stored in state key 'capital_city'.")
246+
agent_B = LlmAgent(name="AgentB", instruction="Tell me about the city stored in {capital_city}.")
247247

248248
pipeline = SequentialAgent(name="CityInfo", sub_agents=[agent_A, agent_B])
249249
# AgentA runs, saves "Paris" to state['capital_city'].
@@ -265,7 +265,7 @@ The most fundamental way for agents operating within the same invocation (and th
265265

266266
LlmAgent agentB = LlmAgent.builder()
267267
.name("AgentB")
268-
.instruction("Tell me about the city stored in state key 'capital_city'.")
268+
.instruction("Tell me about the city stored in {capital_city}.")
269269
.outputKey("capital_city")
270270
.build();
271271

@@ -524,8 +524,8 @@ By combining ADK's composition primitives, you can implement various established
524524
from google.adk.agents import SequentialAgent, LlmAgent
525525

526526
validator = LlmAgent(name="ValidateInput", instruction="Validate the input.", output_key="validation_status")
527-
processor = LlmAgent(name="ProcessData", instruction="Process data if state key 'validation_status' is 'valid'.", output_key="result")
528-
reporter = LlmAgent(name="ReportResult", instruction="Report the result from state key 'result'.")
527+
processor = LlmAgent(name="ProcessData", instruction="Process data if {validation_status} is 'valid'.", output_key="result")
528+
reporter = LlmAgent(name="ReportResult", instruction="Report the result from {result}.")
529529

530530
data_pipeline = SequentialAgent(
531531
name="DataPipeline",
@@ -550,13 +550,13 @@ By combining ADK's composition primitives, you can implement various established
550550

551551
LlmAgent processor = LlmAgent.builder()
552552
.name("ProcessData")
553-
.instruction("Process data if state key 'validation_status' is 'valid'")
553+
.instruction("Process data if {validation_status} is 'valid'")
554554
.outputKey("result") // Saves its main text output to session.state["result"]
555555
.build();
556556

557557
LlmAgent reporter = LlmAgent.builder()
558558
.name("ReportResult")
559-
.instruction("Report the result from state key 'result'")
559+
.instruction("Report the result from {result}")
560560
.build();
561561

562562
SequentialAgent dataPipeline = SequentialAgent.builder()
@@ -593,7 +593,7 @@ By combining ADK's composition primitives, you can implement various established
593593

594594
synthesizer = LlmAgent(
595595
name="Synthesizer",
596-
instruction="Combine results from state keys 'api1_data' and 'api2_data'."
596+
instruction="Combine results from {api1_data} and {api2_data}."
597597
)
598598

599599
overall_workflow = SequentialAgent(
@@ -630,7 +630,7 @@ By combining ADK's composition primitives, you can implement various established
630630

631631
LlmAgent synthesizer = LlmAgent.builder()
632632
.name("Synthesizer")
633-
.instruction("Combine results from state keys 'api1_data' and 'api2_data'.")
633+
.instruction("Combine results from {api1_data} and {api2_data}.")
634634
.build();
635635

636636
SequentialAgent overallWorfklow = SequentialAgent.builder()
@@ -747,7 +747,7 @@ By combining ADK's composition primitives, you can implement various established
747747

748748
reviewer = LlmAgent(
749749
name="FactChecker",
750-
instruction="Review the text in state key 'draft_text' for factual accuracy. Output 'valid' or 'invalid' with reasons.",
750+
instruction="Review the text in {draft_text} for factual accuracy. Output 'valid' or 'invalid' with reasons.",
751751
output_key="review_status"
752752
)
753753

@@ -776,7 +776,7 @@ By combining ADK's composition primitives, you can implement various established
776776

777777
LlmAgent reviewer = LlmAgent.builder()
778778
.name("FactChecker")
779-
.instruction("Review the text in state key 'draft_text' for factual accuracy. Output 'valid' or 'invalid' with reasons.")
779+
.instruction("Review the text in {draft_text} for factual accuracy. Output 'valid' or 'invalid' with reasons.")
780780
.outputKey("review_status")
781781
.build();
782782

@@ -940,7 +940,7 @@ By combining ADK's composition primitives, you can implement various established
940940
# Agent that proceeds based on human decision
941941
process_decision = LlmAgent(
942942
name="ProcessDecision",
943-
instruction="Check state key 'human_decision'. If 'approved', proceed. If 'rejected', inform user."
943+
instruction="Check {human_decision}. If 'approved', proceed. If 'rejected', inform user."
944944
)
945945

946946
approval_workflow = SequentialAgent(
@@ -984,7 +984,7 @@ By combining ADK's composition primitives, you can implement various established
984984
// Agent that proceeds based on human decision
985985
LlmAgent processDecision = LlmAgent.builder()
986986
.name("ProcessDecision")
987-
.instruction("Check state key 'human_decision'. If 'approved', proceed. If 'rejected', inform user.")
987+
.instruction("Check {human_decision}. If 'approved', proceed. If 'rejected', inform user.")
988988
.build();
989989

990990
SequentialAgent approvalWorkflow = SequentialAgent.builder()

docs/sessions/state.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,101 @@ Prefixes on state keys define their scope and persistence behavior, especially w
6767

6868
**How the Agent Sees It:** Your agent code interacts with the *combined* state through the single `session.state` collection (dict/ Map). The `SessionService` handles fetching/merging state from the correct underlying storage based on prefixes.
6969

70+
### Accessing Session State in Agent Instructions
71+
72+
When working with `LlmAgent` instances, you can directly inject session state values into the agent's instruction string using a simple templating syntax. This allows you to create dynamic and context-aware instructions without relying solely on natural language directives.
73+
74+
#### Using `{key}` Templating
75+
76+
To inject a value from the session state, enclose the key of the desired state variable within curly braces: `{key}`. The framework will automatically replace this placeholder with the corresponding value from `session.state` before passing the instruction to the LLM.
77+
78+
**Example:**
79+
80+
```python
81+
from google.adk.agents import LlmAgent
82+
83+
story_generator = LlmAgent(
84+
name="StoryGenerator",
85+
model="gemini-2.0-flash",
86+
instruction="""Write a short story about a cat, focusing on the theme: {topic}."""
87+
)
88+
89+
# Assuming session.state['topic'] is set to "friendship", the LLM
90+
# will receive the following instruction:
91+
# "Write a short story about a cat, focusing on the theme: friendship."
92+
```
93+
94+
#### Important Considerations
95+
96+
* Key Existence: Ensure that the key you reference in the instruction string exists in the session.state. If the key is missing, the agent might misbehave or throw an error.
97+
* Data Types: The value associated with the key should be a string or a type that can be easily converted to a string.
98+
* Escaping: If you need to use literal curly braces in your instruction (e.g., for JSON formatting), you'll need to escape them.
99+
100+
#### Bypassing State Injection with `InstructionProvider`
101+
102+
In some cases, you might want to use `{{` and `}}` literally in your instructions without triggering the state injection mechanism. For example, you might be writing instructions for an agent that helps with a templating language that uses the same syntax.
103+
104+
To achieve this, you can provide a function to the `instruction` parameter instead of a string. This function is called an `InstructionProvider`. When you use an `InstructionProvider`, the ADK will not attempt to inject state, and your instruction string will be passed to the model as-is.
105+
106+
The `InstructionProvider` function receives a `ReadonlyContext` object, which you can use to access session state or other contextual information if you need to build the instruction dynamically.
107+
108+
=== "Python"
109+
110+
```python
111+
from google.adk.agents import LlmAgent
112+
from google.adk.agents.readonly_context import ReadonlyContext
113+
114+
# This is an InstructionProvider
115+
def my_instruction_provider(context: ReadonlyContext) -> str:
116+
# You can optionally use the context to build the instruction
117+
# For this example, we'll return a static string with literal braces.
118+
return "This is an instruction with {{literal_braces}} that will not be replaced."
119+
120+
agent = LlmAgent(
121+
model="gemini-2.0-flash",
122+
name="template_helper_agent",
123+
instruction=my_instruction_provider
124+
)
125+
```
126+
127+
If you want to both use an `InstructionProvider` *and* inject state into your instructions, you can use the `inject_session_state` utility function.
128+
129+
=== "Python"
130+
131+
```python
132+
from google.adk.agents import LlmAgent
133+
from google.adk.agents.readonly_context import ReadonlyContext
134+
from google.adk.utils import instructions_utils
135+
136+
async def my_dynamic_instruction_provider(context: ReadonlyContext) -> str:
137+
template = "This is a {adjective} instruction with {{literal_braces}}."
138+
# This will inject the 'adjective' state variable but leave the literal braces.
139+
return await instructions_utils.inject_session_state(template, context)
140+
141+
agent = LlmAgent(
142+
model="gemini-2.0-flash",
143+
name="dynamic_template_helper_agent",
144+
instruction=my_dynamic_instruction_provider
145+
)
146+
```
147+
148+
**Benefits of Direct Injection**
149+
150+
* Clarity: Makes it explicit which parts of the instruction are dynamic and based on session state.
151+
* Reliability: Avoids relying on the LLM to correctly interpret natural language instructions to access state.
152+
* Maintainability: Simplifies instruction strings and reduces the risk of errors when updating state variable names.
153+
154+
**Relation to Other State Access Methods**
155+
156+
This direct injection method is specific to LlmAgent instructions. Refer to the following section for more information on other state access methods.
157+
70158
### How State is Updated: Recommended Methods
71159

160+
!!! note "The Right Way to Modify State"
161+
When you need to change the session state, the correct and safest method is to **directly modify the `state` object on the `Context`** provided to your function (e.g., `callback_context.state['my_key'] = 'new_value'`). This is considered "direct state manipulation" in the right way, as the framework automatically tracks these changes.
162+
163+
This is critically different from directly modifying the `state` on a `Session` object you retrieve from the `SessionService` (e.g., `my_session.state['my_key'] = 'new_value'`). **You should avoid this**, as it bypasses the ADK's event tracking and can lead to lost data. The "Warning" section at the end of this page has more details on this important distinction.
164+
72165
State should **always** be updated as part of adding an `Event` to the session history using `session_service.append_event()`. This ensures changes are tracked, persistence works correctly, and updates are thread-safe.
73166

74167
**1\. The Easy Way: `output_key` (for Agent Text Responses)**

examples/java/snippets/src/main/java/agents/StoryFlowAgentExample.java

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public static void main(String[] args) {
6363
.instruction(
6464
"""
6565
You are a story writer. Write a short story (around 100 words) about a cat,
66-
based on the topic provided in session state with key 'topic'
66+
based on the topic: {topic}
6767
""")
6868
.inputSchema(null)
6969
.outputKey("current_story") // Key for storing output in session state
@@ -76,8 +76,7 @@ You are a story writer. Write a short story (around 100 words) about a cat,
7676
.description("Critiques the story.")
7777
.instruction(
7878
"""
79-
You are a story critic. Review the story provided in
80-
session state with key 'current_story'. Provide 1-2 sentences of constructive criticism
79+
You are a story critic. Review the story: {current_story}. Provide 1-2 sentences of constructive criticism
8180
on how to improve it. Focus on plot or character.
8281
""")
8382
.inputSchema(null)
@@ -91,9 +90,7 @@ You are a story writer. Write a short story (around 100 words) about a cat,
9190
.description("Revises the story based on criticism.")
9291
.instruction(
9392
"""
94-
You are a story reviser. Revise the story provided in
95-
session state with key 'current_story', based on the criticism in
96-
session state with key 'criticism'. Output only the revised story.
93+
You are a story reviser. Revise the story: {current_story}, based on the criticism: {criticism}. Output only the revised story.
9794
""")
9895
.inputSchema(null)
9996
.outputKey("current_story") // Overwrites the original story
@@ -106,8 +103,7 @@ You are a story writer. Write a short story (around 100 words) about a cat,
106103
.description("Checks grammar and suggests corrections.")
107104
.instruction(
108105
"""
109-
You are a grammar checker. Check the grammar of the story
110-
provided in session state with key 'current_story'. Output only the suggested
106+
You are a grammar checker. Check the grammar of the story: {current_story}. Output only the suggested
111107
corrections as a list, or output 'Grammar is good!' if there are no errors.
112108
""")
113109
.outputKey("grammar_suggestions")
@@ -120,8 +116,7 @@ You are a story writer. Write a short story (around 100 words) about a cat,
120116
.description("Analyzes the tone of the story.")
121117
.instruction(
122118
"""
123-
You are a tone analyzer. Analyze the tone of the story
124-
provided in session state with key 'current_story'. Output only one word: 'positive' if
119+
You are a tone analyzer. Analyze the tone of the story: {current_story}. Output only one word: 'positive' if
125120
the tone is generally positive, 'negative' if the tone is generally negative, or 'neutral'
126121
otherwise.
127122
""")

examples/python/snippets/agents/custom-agent/storyflow_agent.py

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -171,17 +171,15 @@ async def _run_async_impl(
171171
story_generator = LlmAgent(
172172
name="StoryGenerator",
173173
model=GEMINI_2_FLASH,
174-
instruction="""You are a story writer. Write a short story (around 100 words) about a cat,
175-
based on the topic provided in session state with key 'topic'""",
174+
instruction="""You are a story writer. Write a short story (around 100 words), on the following topic: {topic}""",
176175
input_schema=None,
177176
output_key="current_story", # Key for storing output in session state
178177
)
179178

180179
critic = LlmAgent(
181180
name="Critic",
182181
model=GEMINI_2_FLASH,
183-
instruction="""You are a story critic. Review the story provided in
184-
session state with key 'current_story'. Provide 1-2 sentences of constructive criticism
182+
instruction="""You are a story critic. Review the story provided: {{current_story}}. Provide 1-2 sentences of constructive criticism
185183
on how to improve it. Focus on plot or character.""",
186184
input_schema=None,
187185
output_key="criticism", # Key for storing criticism in session state
@@ -190,18 +188,16 @@ async def _run_async_impl(
190188
reviser = LlmAgent(
191189
name="Reviser",
192190
model=GEMINI_2_FLASH,
193-
instruction="""You are a story reviser. Revise the story provided in
194-
session state with key 'current_story', based on the criticism in
195-
session state with key 'criticism'. Output only the revised story.""",
191+
instruction="""You are a story reviser. Revise the story provided: {{current_story}}, based on the criticism in
192+
{{criticism}}. Output only the revised story.""",
196193
input_schema=None,
197194
output_key="current_story", # Overwrites the original story
198195
)
199196

200197
grammar_check = LlmAgent(
201198
name="GrammarCheck",
202199
model=GEMINI_2_FLASH,
203-
instruction="""You are a grammar checker. Check the grammar of the story
204-
provided in session state with key 'current_story'. Output only the suggested
200+
instruction="""You are a grammar checker. Check the grammar of the story provided: {current_story}. Output only the suggested
205201
corrections as a list, or output 'Grammar is good!' if there are no errors.""",
206202
input_schema=None,
207203
output_key="grammar_suggestions",
@@ -210,8 +206,7 @@ async def _run_async_impl(
210206
tone_check = LlmAgent(
211207
name="ToneCheck",
212208
model=GEMINI_2_FLASH,
213-
instruction="""You are a tone analyzer. Analyze the tone of the story
214-
provided in session state with key 'current_story'. Output only one word: 'positive' if
209+
instruction="""You are a tone analyzer. Analyze the tone of the story provided: {current_story}. Output only one word: 'positive' if
215210
the tone is generally positive, 'negative' if the tone is generally negative, or 'neutral'
216211
otherwise.""",
217212
input_schema=None,

0 commit comments

Comments
 (0)