Skip to content

Commit 720af3d

Browse files
haranrkcopybara-github
authored andcommitted
feat(agents): add instruction field to ManagedAgent
Add an `instruction` field (str or InstructionProvider) to ManagedAgent and a `canonical_instruction` resolver mirroring LlmAgent. This task only adds the field and resolver; forwarding it to the Managed Agents API is a follow-up change. Co-authored-by: Haran Rajkumar <haranrk@google.com> PiperOrigin-RevId: 951193016
1 parent 8219774 commit 720af3d

2 files changed

Lines changed: 79 additions & 0 deletions

File tree

src/google/adk/agents/_managed_agent.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
from ..utils.content_utils import to_user_content
5252
from ..utils.context_utils import Aclosing
5353
from ..utils.env_utils import is_enterprise_mode_enabled
54+
from ..utils.instructions_utils import InstructionProvider
5455
from .base_agent import BaseAgent
5556
from .context import Context
5657
from .invocation_context import InvocationContext
@@ -144,6 +145,16 @@ class ManagedAgent(BaseAgent):
144145
agent_config: Optional[CreateAgentInteractionAgentConfigParam] = None
145146
"""Runtime configuration passed to interactions.create."""
146147

148+
instruction: Union[str, InstructionProvider] = ''
149+
"""The system instruction sent to the Managed Agent.
150+
151+
A plain string may embed ``{var}``, ``{artifact.name}``, or ``{var?}``
152+
placeholders that are resolved from session state / artifacts at request time
153+
(see ``inject_session_state``). An ``InstructionProvider`` callable is invoked
154+
with a ``ReadonlyContext`` and bypasses placeholder injection (it manages
155+
state itself). Empty by default, in which case no system instruction is sent.
156+
"""
157+
147158
tools: list[
148159
Union[types.Tool, BaseTool, Callable[..., Any], RemoteMcpServer]
149160
] = Field(default_factory=list)
@@ -197,6 +208,29 @@ def api_client(self) -> Client:
197208
)
198209
return self._api_client
199210

211+
async def canonical_instruction(
212+
self, ctx: ReadonlyContext
213+
) -> tuple[str, bool]:
214+
"""Resolves ``self.instruction`` for the current context.
215+
216+
Mirrors ``LlmAgent.canonical_instruction``.
217+
218+
Args:
219+
ctx: The read-only context used to resolve an InstructionProvider.
220+
221+
Returns:
222+
A tuple of (instruction, bypass_state_injection).
223+
``bypass_state_injection``
224+
is True when the instruction came from an ``InstructionProvider`` callable
225+
(which manages state itself), False for a plain string.
226+
"""
227+
if isinstance(self.instruction, str):
228+
return self.instruction, False
229+
instruction = self.instruction(ctx)
230+
if inspect.isawaitable(instruction):
231+
instruction = await instruction
232+
return instruction, True
233+
200234
async def _resolve_backend_tools(
201235
self, ctx: InvocationContext
202236
) -> list[ToolParam]:

tests/unittests/agents/test_managed_agent.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1044,3 +1044,48 @@ def test_run_async_forwards_mcp_server_param():
10441044
mcp = [t for t in create_kwargs['tools'] if t['type'] == 'mcp_server'][0]
10451045
assert mcp['url'] == 'https://mcp.example.com/mcp'
10461046
assert mcp['headers'] == {'X-Goog-Api-Key': 'k'}
1047+
1048+
1049+
def test_canonical_instruction_str():
1050+
agent = ManagedAgent(
1051+
name='mgr',
1052+
agent_id='agents/a',
1053+
instruction='hello',
1054+
api_client=_FakeClient(),
1055+
)
1056+
1057+
text, bypass = asyncio.run(agent.canonical_instruction(MagicMock()))
1058+
1059+
assert text == 'hello'
1060+
assert bypass is False
1061+
1062+
1063+
def test_canonical_instruction_sync_provider():
1064+
agent = ManagedAgent(
1065+
name='mgr',
1066+
agent_id='agents/a',
1067+
instruction=lambda ctx: 'from provider',
1068+
api_client=_FakeClient(),
1069+
)
1070+
1071+
text, bypass = asyncio.run(agent.canonical_instruction(MagicMock()))
1072+
1073+
assert text == 'from provider'
1074+
assert bypass is True
1075+
1076+
1077+
def test_canonical_instruction_async_provider():
1078+
async def provider(ctx):
1079+
return 'async provider'
1080+
1081+
agent = ManagedAgent(
1082+
name='mgr',
1083+
agent_id='agents/a',
1084+
instruction=provider,
1085+
api_client=_FakeClient(),
1086+
)
1087+
1088+
text, bypass = asyncio.run(agent.canonical_instruction(MagicMock()))
1089+
1090+
assert text == 'async provider'
1091+
assert bypass is True

0 commit comments

Comments
 (0)