Skip to content

Commit 01473b9

Browse files
authored
Merge branch 'main' into fix/support-opentelemetry-1.43.0
2 parents 1fbcf1d + e737f22 commit 01473b9

24 files changed

Lines changed: 1054 additions & 105 deletions

File tree

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Managed Agent — System Instruction
2+
3+
> For setup, authentication, backends, and background on `ManagedAgent`, see the
4+
> [ManagedAgent guide](../../../../docs/guides/agents/managed_agent/index.md).
5+
6+
## Overview
7+
8+
This sample runs a `ManagedAgent` whose behavior is shaped by its `instruction`
9+
field. `instruction` is forwarded to the Managed Agents API as the interaction's
10+
system instruction (the same role `LlmAgent.instruction` plays for a local
11+
model). Here it pins a persona and output format, so its effect is visible in
12+
every reply.
13+
14+
`instruction` accepts either a plain string (which may embed `{state_var}`
15+
placeholders resolved from session state) or an `InstructionProvider` callable.
16+
This sample uses an **`InstructionProvider`**: `persona_instruction` takes a
17+
`ReadonlyContext`, reads the reply language from `state['response_language']`
18+
(defaulting to English), and returns the instruction string. Because a provider
19+
is invoked on every turn, the instruction is rebuilt each turn from the current
20+
state; unlike a string, it bypasses `{placeholder}` injection, so you build the
21+
final text yourself. A provider may also be `async` (return an awaitable `str`).
22+
23+
## Sample Inputs
24+
25+
- `What is the capital of France?`
26+
27+
The reply obeys the instruction: a single terse sentence ending with a
28+
relevant emoji, in the language from `state['response_language']` (English by
29+
default).
30+
31+
- `And Japan?`
32+
33+
A follow-up turn that reuses the recovered remote sandbox and previous
34+
interaction. The provider runs again on this turn, demonstrating that the
35+
system instruction is resolved and sent on chained turns too — and would pick
36+
up any change to `response_language` in session state.
37+
38+
## Graph
39+
40+
```mermaid
41+
graph LR
42+
User -->|message| ManagedAgent
43+
ManagedAgent -->|interactions.create + system_instruction| ManagedAgentsAPI
44+
ManagedAgentsAPI -->|streamed events| ManagedAgent
45+
ManagedAgent -->|reply shaped by the instruction| User
46+
```
47+
48+
## How To
49+
50+
- **Set the instruction**: pass `instruction=...` to `ManagedAgent`. A string is
51+
sent as-is (after `{placeholder}` resolution); an `InstructionProvider`
52+
callable is invoked per turn and bypasses placeholder injection.
53+
- **Use an `InstructionProvider`**: define a callable that takes a
54+
`ReadonlyContext` and returns a `str` (or an awaitable `str`), then pass it as
55+
`instruction`. Read `readonly_context.state` to build the instruction
56+
dynamically — here `state['response_language']` selects the reply language.
57+
- **Observe the effect**: every reply follows the persona/format the instruction
58+
specifies, on the first turn and on chained follow-up turns.
59+
- **Drive it**: a `ManagedAgent` is a `BaseAgent`, so a standard `Runner` runs it
60+
just like any other agent.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from . import agent
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""A ManagedAgent with an ``InstructionProvider`` system instruction.
16+
17+
``ManagedAgent.instruction`` is forwarded to the Managed Agents API as the
18+
interaction's system instruction, the same way ``LlmAgent.instruction`` shapes a
19+
local model. It accepts either a plain string (which may embed ``{state_var}``
20+
placeholders resolved from session state) or an ``InstructionProvider`` — a
21+
callable invoked with a ``ReadonlyContext`` that returns the instruction string
22+
and bypasses placeholder injection.
23+
24+
This sample uses an ``InstructionProvider`` so the instruction is built
25+
dynamically, per turn, from session state: it pins a terse persona and reads the
26+
reply language from ``state['response_language']`` (defaulting to English). The
27+
persona keeps the effect visible in every reply, and because the provider runs
28+
on every turn the instruction adapts if the state changes on a later turn.
29+
30+
Run with ``adk web`` / ``adk run
31+
contributing/samples/managed_agent/system_instruction``. See the README for the
32+
required environment / auth setup.
33+
"""
34+
35+
import os
36+
37+
from google.adk.agents import ManagedAgent
38+
from google.adk.agents.readonly_context import ReadonlyContext
39+
40+
# The Managed Agent id served by the Managed Agents API. Override with the
41+
# MANAGED_AGENT_ID environment variable if your project has access to a
42+
# different agent.
43+
_DEFAULT_AGENT_ID = 'antigravity-preview-05-2026'
44+
45+
46+
def persona_instruction(readonly_context: ReadonlyContext) -> str:
47+
"""Builds the system instruction dynamically from session state.
48+
49+
An ``InstructionProvider`` is any callable that takes a ``ReadonlyContext``
50+
and returns the instruction (a ``str``, or an awaitable ``str`` for async
51+
providers). It is invoked on every turn, so the instruction can adapt to the
52+
current session state, and — unlike a plain string — it bypasses
53+
``{placeholder}`` injection, leaving you to build the final string yourself.
54+
"""
55+
language = readonly_context.state.get('response_language', 'English')
56+
return (
57+
'You are a terse assistant. Always answer in a single sentence, in'
58+
f' {language}, and end every reply with a relevant emoji.'
59+
)
60+
61+
62+
root_agent = ManagedAgent(
63+
name='managed_persona_agent',
64+
agent_id=os.environ.get('MANAGED_AGENT_ID', _DEFAULT_AGENT_ID),
65+
# Provision a remote sandbox; the environment id is recovered from prior
66+
# events so follow-up turns reuse the same sandbox.
67+
environment={'type': 'remote'},
68+
# Pass an InstructionProvider callable instead of a plain string: it is
69+
# invoked per turn with a ReadonlyContext and returns the resolved
70+
# instruction that is forwarded as the interaction's system_instruction.
71+
instruction=persona_instruction,
72+
)

docs/guides/agents/managed_agent/index.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,28 @@ Response text appears in both places (the local ADK events and the remote
125125
interaction history), but ADK stores only the ids it needs to recover and reuse
126126
the remote state; it never re-sends prior turns.
127127

128+
### System instruction
129+
130+
Set `instruction` on `ManagedAgent` to send a system instruction to the Managed
131+
Agents API, mirroring `LlmAgent.instruction`. It accepts a plain string — which
132+
may embed `{state_var}` / `{artifact.name}` / `{var?}` placeholders resolved
133+
from session state and artifacts at request time — or an `InstructionProvider`
134+
callable that is invoked with a `ReadonlyContext` and bypasses placeholder
135+
injection. The resolved instruction is forwarded as the interaction's
136+
`system_instruction` on every turn, including chained turns; an empty
137+
`instruction` (the default) sends none.
138+
139+
```python
140+
from google.adk.agents import ManagedAgent
141+
142+
root_agent = ManagedAgent(
143+
name='managed_persona_agent',
144+
agent_id='antigravity-preview-05-2026',
145+
environment={'type': 'remote'},
146+
instruction='You are a terse assistant. Answer in a single sentence.',
147+
)
148+
```
149+
128150
## Advanced applications
129151

130152
### Tool encapsulation for orchestration
@@ -154,3 +176,4 @@ the remote state; it never re-sends prior turns.
154176

155177
* [Managed Agent Basic](../../../../contributing/samples/managed_agent/basic)
156178
* [Managed Agent Code Execution](../../../../contributing/samples/managed_agent/code_execution)
179+
* [Managed Agent System Instruction](../../../../contributing/samples/managed_agent/system_instruction)

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ classifiers = [
3131
]
3232
dynamic = [ "version" ]
3333
dependencies = [
34+
"aiohttp!=3.14.2",
3435
"aiosqlite>=0.21",
3536
"authlib>=1.6.6,<2",
3637
"click>=8.1.8,<9",

src/google/adk/a2a/converters/event_converter.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ def _serialize_metadata_value(value: Any) -> str:
9191
"""
9292
if hasattr(value, "model_dump"):
9393
try:
94-
return value.model_dump(exclude_none=True, by_alias=True)
94+
return value.model_dump(mode="json", exclude_none=True, by_alias=True)
9595
except Exception as e:
9696
logger.warning("Failed to serialize metadata value: %s", e)
9797
return str(value)
@@ -234,7 +234,13 @@ def convert_a2a_task_to_event(
234234
):
235235
message = a2a_task.status.message
236236
elif a2a_task.history:
237-
message = a2a_task.history[-1]
237+
# Only pick agent-role messages from history; a trailing user
238+
# message should not be misattributed as agent output.
239+
agent_messages = [
240+
m for m in a2a_task.history if m.role == _compat.ROLE_AGENT
241+
]
242+
if agent_messages:
243+
message = agent_messages[-1]
238244

239245
# Convert message if available
240246
if message:
@@ -288,6 +294,8 @@ def convert_a2a_message_to_event(
288294
if a2a_message is None:
289295
raise ValueError("A2A message cannot be None")
290296

297+
genai_role = _compat.role_to_str(a2a_message.role)
298+
291299
if not a2a_message.parts:
292300
logger.warning(
293301
"A2A message has no parts, creating event with empty content"
@@ -300,7 +308,7 @@ def convert_a2a_message_to_event(
300308
),
301309
author=author or "a2a agent",
302310
branch=invocation_context.branch if invocation_context else None,
303-
content=genai_types.Content(role="model", parts=[]),
311+
content=genai_types.Content(role=genai_role, parts=[]),
304312
)
305313

306314
try:
@@ -355,7 +363,7 @@ def convert_a2a_message_to_event(
355363
if long_running_tool_ids
356364
else None,
357365
content=genai_types.Content(
358-
role="model",
366+
role=genai_role,
359367
parts=output_parts,
360368
),
361369
)

src/google/adk/a2a/converters/part_converter.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,9 @@ def apply_meta(p: a2a_types.Part, meta: dict[str, Any]) -> None:
239239
meta = {}
240240
if part.video_metadata:
241241
meta[_get_adk_metadata_key('video_metadata')] = (
242-
part.video_metadata.model_dump(by_alias=True, exclude_none=True)
242+
part.video_metadata.model_dump(
243+
mode='json', by_alias=True, exclude_none=True
244+
)
243245
)
244246
if part.part_metadata:
245247
meta.update(part.part_metadata)
@@ -274,7 +276,7 @@ def apply_meta(p: a2a_types.Part, meta: dict[str, Any]) -> None:
274276
).decode('utf-8')
275277
if part.part_metadata:
276278
meta.update(part.part_metadata)
277-
data_dict = val.model_dump(by_alias=True, exclude_none=True)
279+
data_dict = val.model_dump(mode='json', by_alias=True, exclude_none=True)
278280
return _compat.make_data_part(data=data_dict, metadata=meta)
279281

280282
logger.warning(

src/google/adk/a2a/converters/to_adk_event.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,11 @@ def _create_event(
191191
long_running_function_ids: Optional[set[str]] = None,
192192
partial: bool = False,
193193
content_role: str = "model",
194+
grounding_metadata: Any = None,
195+
custom_metadata: Any = None,
196+
usage_metadata: Any = None,
197+
error_code: Any = None,
198+
citation_metadata: Any = None,
194199
) -> Optional[Event]:
195200
"""Creates an ADK event from parts and metadata."""
196201
event_actions = actions or EventActions()
@@ -220,6 +225,11 @@ def _create_event(
220225
else None
221226
),
222227
partial=partial,
228+
grounding_metadata=grounding_metadata,
229+
custom_metadata=custom_metadata,
230+
usage_metadata=usage_metadata,
231+
error_code=error_code,
232+
citation_metadata=citation_metadata,
223233
)
224234

225235
return event
@@ -241,6 +251,28 @@ def _parse_adk_metadata_value(value: Any) -> Any:
241251
return value
242252

243253

254+
def _extract_genai_metadata(
255+
metadata_dict: dict[str, Any], key: str, model_class: Any
256+
) -> Any:
257+
raw = metadata_dict.get(_get_adk_metadata_key(key))
258+
if raw is None:
259+
return None
260+
parsed = _parse_adk_metadata_value(raw)
261+
if not isinstance(parsed, dict) and model_class:
262+
return None
263+
if not model_class:
264+
return parsed
265+
try:
266+
return model_class.model_validate(parsed)
267+
except ValidationError as error:
268+
logger.warning(
269+
"Ignoring invalid ADK %s metadata: %d validation errors",
270+
key,
271+
error.error_count(),
272+
)
273+
return None
274+
275+
244276
def _extract_event_actions(metadata: Any) -> EventActions:
245277
"""Extracts ADK event actions from A2A metadata.
246278
@@ -576,12 +608,28 @@ def convert_a2a_artifact_update_to_event(
576608
output_parts, _ = _convert_a2a_parts_to_adk_parts(
577609
a2a_artifact_update.artifact.parts, part_converter
578610
)
611+
metadata_dict = _compat.meta_to_dict(a2a_artifact_update.artifact.metadata)
579612
return _create_event(
580613
output_parts,
581614
invocation_context,
582615
author,
583616
_extract_event_actions(a2a_artifact_update.artifact.metadata),
584617
partial=not a2a_artifact_update.last_chunk,
618+
grounding_metadata=_extract_genai_metadata(
619+
metadata_dict, "grounding_metadata", genai_types.GroundingMetadata
620+
),
621+
custom_metadata=_extract_genai_metadata(
622+
metadata_dict, "custom_metadata", None
623+
),
624+
usage_metadata=_extract_genai_metadata(
625+
metadata_dict,
626+
"usage_metadata",
627+
genai_types.GenerateContentResponseUsageMetadata,
628+
),
629+
error_code=_extract_genai_metadata(metadata_dict, "error_code", None),
630+
citation_metadata=_extract_genai_metadata(
631+
metadata_dict, "citation_metadata", genai_types.CitationMetadata
632+
),
585633
)
586634
except Exception as e:
587635
logger.error("Failed to convert A2A artifact update to event: %s", e)

src/google/adk/agents/invocation_context.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from pydantic import ConfigDict
2525
from pydantic import Field
2626
from pydantic import PrivateAttr
27+
from typing_extensions import override
2728

2829
from ..apps._configs import EventsCompactionConfig
2930
from ..apps._configs import ResumabilityConfig
@@ -272,6 +273,12 @@ class InvocationContext(BaseModel):
272273
of this invocation.
273274
"""
274275

276+
@override
277+
def model_post_init(self, __context: Any) -> None:
278+
super().model_post_init(__context)
279+
if self.run_config and self.run_config.custom_metadata:
280+
self._custom_metadata.update(self.run_config.custom_metadata)
281+
275282
@property
276283
def is_resumable(self) -> bool:
277284
"""Returns whether the current invocation is resumable."""

0 commit comments

Comments
 (0)