Skip to content

Commit 62a711c

Browse files
committed
feat(tools): add response_scheduling to control Live function response behavior
1 parent 4100a24 commit 62a711c

4 files changed

Lines changed: 92 additions & 0 deletions

File tree

src/google/adk/flows/llm_flows/functions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1189,6 +1189,10 @@ def __build_response_event(
11891189
parts=function_response_parts,
11901190
)
11911191
part_function_response.function_response.id = tool_context.function_call_id
1192+
if tool.response_scheduling is not None:
1193+
part_function_response.function_response.scheduling = (
1194+
tool.response_scheduling
1195+
)
11921196

11931197
content = types.Content(
11941198
role='user',

src/google/adk/tools/base_tool.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,19 +90,34 @@ class BaseTool(ABC):
9090
NOTE: the entire dict must be JSON serializable.
9191
"""
9292

93+
response_scheduling: Optional[types.FunctionResponseScheduling] = None
94+
"""Controls when the model reacts to the tool's response.
95+
96+
This is primarily used for Live (bidi) streaming:
97+
- ``SILENT``: feeds the response back without triggering a model turn.
98+
- ``WHEN_IDLE``: defers the reaction until the model is idle.
99+
- ``INTERRUPT``: reacts immediately.
100+
101+
When set, this value is applied to the emitted ``FunctionResponse``. It is
102+
ignored by models that don't support response scheduling.
103+
``None`` preserves the default behavior.
104+
"""
105+
93106
def __init__(
94107
self,
95108
*,
96109
name,
97110
description,
98111
is_long_running: bool = False,
99112
custom_metadata: Optional[dict[str, Any]] = None,
113+
response_scheduling: Optional[types.FunctionResponseScheduling] = None,
100114
):
101115
self.name = name
102116
self.description = description
103117
self.is_long_running = is_long_running
104118
self._defers_response = False
105119
self.custom_metadata = custom_metadata
120+
self.response_scheduling = response_scheduling
106121

107122
def _get_declaration(self) -> Optional[types.FunctionDeclaration]:
108123
"""Gets the OpenAPI specification of this tool in the form of a FunctionDeclaration.

tests/unittests/flows/llm_flows/test_functions_simple.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1584,3 +1584,64 @@ async def test_detection_exception_does_not_break_tool_call(
15841584
assert len(recorded_calls) == 1
15851585
assert recorded_calls[0]['error_type'] is None
15861586
assert recorded_calls[0]['error'] is None
1587+
1588+
1589+
@pytest.mark.asyncio
1590+
async def test_response_scheduling_applied_to_function_response():
1591+
"""response_scheduling on a tool is stamped onto the FunctionResponse part."""
1592+
1593+
def simple_fn(**kwargs) -> dict:
1594+
return {'result': 'test'}
1595+
1596+
tool = FunctionTool(simple_fn)
1597+
tool.response_scheduling = types.FunctionResponseScheduling.SILENT
1598+
model = testing_utils.MockModel.create(responses=[])
1599+
agent = Agent(name='test_agent', model=model, tools=[tool])
1600+
invocation_context = await testing_utils.create_invocation_context(
1601+
agent=agent, user_content=''
1602+
)
1603+
1604+
function_call = types.FunctionCall(name=tool.name, args={}, id='fc_test')
1605+
event = Event(
1606+
invocation_id=invocation_context.invocation_id,
1607+
author=agent.name,
1608+
content=types.Content(parts=[types.Part(function_call=function_call)]),
1609+
)
1610+
1611+
result_event = await handle_function_calls_async(
1612+
invocation_context, event, {tool.name: tool}
1613+
)
1614+
1615+
assert result_event is not None
1616+
function_response = result_event.content.parts[0].function_response
1617+
assert function_response.scheduling is types.FunctionResponseScheduling.SILENT
1618+
1619+
1620+
@pytest.mark.asyncio
1621+
async def test_response_scheduling_unset_by_default():
1622+
"""Without response_scheduling, the FunctionResponse part leaves it unset."""
1623+
1624+
def simple_fn(**kwargs) -> dict:
1625+
return {'result': 'test'}
1626+
1627+
tool = FunctionTool(simple_fn)
1628+
model = testing_utils.MockModel.create(responses=[])
1629+
agent = Agent(name='test_agent', model=model, tools=[tool])
1630+
invocation_context = await testing_utils.create_invocation_context(
1631+
agent=agent, user_content=''
1632+
)
1633+
1634+
function_call = types.FunctionCall(name=tool.name, args={}, id='fc_test')
1635+
event = Event(
1636+
invocation_id=invocation_context.invocation_id,
1637+
author=agent.name,
1638+
content=types.Content(parts=[types.Part(function_call=function_call)]),
1639+
)
1640+
1641+
result_event = await handle_function_calls_async(
1642+
invocation_context, event, {tool.name: tool}
1643+
)
1644+
1645+
assert result_event is not None
1646+
function_response = result_event.content.parts[0].function_response
1647+
assert function_response.scheduling is None

tests/unittests/tools/test_base_tool.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,3 +155,15 @@ async def run_async(self, **kwargs):
155155
t2 = SimpleTool(name='test2', description='desc')
156156
t2._defers_response = True
157157
assert t2._defers_response is True
158+
159+
160+
def test_response_scheduling_defaults_to_none():
161+
"""response_scheduling defaults to None, preserving existing behavior."""
162+
163+
class SimpleTool(BaseTool):
164+
165+
async def run_async(self, **kwargs):
166+
pass
167+
168+
t = SimpleTool(name='test', description='desc')
169+
assert t.response_scheduling is None

0 commit comments

Comments
 (0)