Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/google/adk/agents/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
from ..utils import _lazy

if TYPE_CHECKING:
from ._callback_metadata import CallbackHook
from ._callback_metadata import CallbackInvocationInfo
from ._managed_agent import ManagedAgent
from .base_agent import BaseAgent
from .base_agent_config import BaseAgentConfig
Expand All @@ -42,6 +44,8 @@
'Agent': '.llm_agent',
'BaseAgent': '.base_agent',
'BaseAgentConfig': '.base_agent_config',
'CallbackHook': '._callback_metadata',
'CallbackInvocationInfo': '._callback_metadata',
'Context': '.context',
'InvocationContext': '.invocation_context',
'LiveRequest': '.live_request_queue',
Expand All @@ -61,6 +65,8 @@
__all__ = [
'Agent',
'BaseAgent',
'CallbackHook',
'CallbackInvocationInfo',
'Context',
'LlmAgent',
'LoopAgent',
Expand Down
43 changes: 43 additions & 0 deletions src/google/adk/agents/_callback_metadata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from __future__ import annotations

from dataclasses import dataclass
import enum


class CallbackHook(str, enum.Enum):
"""Lifecycle hooks that receive callback-scoped context metadata."""

BEFORE_AGENT = 'before_agent'
AFTER_AGENT = 'after_agent'
BEFORE_MODEL = 'before_model'
AFTER_MODEL = 'after_model'
BEFORE_TOOL = 'before_tool'
AFTER_TOOL = 'after_tool'
ON_MODEL_ERROR = 'on_model_error'
ON_TOOL_ERROR = 'on_tool_error'
ON_AGENT_ERROR = 'on_agent_error'


@dataclass(frozen=True)
class CallbackInvocationInfo:
"""Describes the lifecycle callback currently being invoked.

Attributes:
hook: The lifecycle hook for the active callback.
"""

hook: CallbackHook
23 changes: 13 additions & 10 deletions src/google/adk/agents/base_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
from ..utils._callback_pipeline import _stop_on_truthy
from ..utils.context_utils import Aclosing
from ..workflow import BaseNode
from ._callback_metadata import CallbackHook
from .base_agent_config import BaseAgentConfig as BaseAgentConfig
from .callback_context import CallbackContext
from .context import Context
Expand Down Expand Up @@ -528,11 +529,12 @@ async def _handle_before_agent_callback(
# callbacks.
callbacks = self.canonical_before_agent_callbacks
if not before_agent_callback_content and callbacks:
before_agent_callback_content = await _run_callbacks(
callbacks,
_stop_on_truthy,
callback_context=callback_context,
)
with callback_context._callback_scope(CallbackHook.BEFORE_AGENT):
before_agent_callback_content = await _run_callbacks(
callbacks,
_stop_on_truthy,
callback_context=callback_context,
)

# Process the override content if exists, and further process the state
# change if exists.
Expand Down Expand Up @@ -583,11 +585,12 @@ async def _handle_after_agent_callback(
# callbacks.
callbacks = self.canonical_after_agent_callbacks
if not after_agent_callback_content and callbacks:
after_agent_callback_content = await _run_callbacks(
callbacks,
_stop_on_truthy,
callback_context=callback_context,
)
with callback_context._callback_scope(CallbackHook.AFTER_AGENT):
after_agent_callback_content = await _run_callbacks(
callbacks,
_stop_on_truthy,
callback_context=callback_context,
)

# Process the override content if exists, and further process the state
# change if exists.
Expand Down
2 changes: 2 additions & 0 deletions src/google/adk/agents/callback_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

from __future__ import annotations

from ._callback_metadata import CallbackHook as CallbackHook
from ._callback_metadata import CallbackInvocationInfo as CallbackInvocationInfo
from .context import Context

# Keep ReadonlyContext for backward compatibility
Expand Down
22 changes: 22 additions & 0 deletions src/google/adk/agents/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,20 @@

from __future__ import annotations

from collections.abc import Iterator
from collections.abc import Mapping
from collections.abc import Sequence
from contextlib import contextmanager
from contextvars import ContextVar
from typing import Any
from typing import cast
from typing import TYPE_CHECKING

from opentelemetry import context as context_api
from typing_extensions import override

from ._callback_metadata import CallbackHook
from ._callback_metadata import CallbackInvocationInfo
from .readonly_context import ReadonlyContext

if TYPE_CHECKING:
Expand Down Expand Up @@ -229,6 +234,9 @@ def __init__(
self._output_for_ancestors = []
self._error: Exception | None = None
self._error_node_path: str = ''
self._callback_info: ContextVar[CallbackInvocationInfo | None] = ContextVar(
'callback_info', default=None
)

@property
@override
Expand All @@ -247,6 +255,20 @@ def function_call_id(self, value: str | None) -> None:
"""Sets the function call id of the current tool call."""
self._function_call_id = value

@property
def callback_info(self) -> CallbackInvocationInfo | None:
"""Returns metadata for the callback currently being invoked, if any."""
return self._callback_info.get()

@contextmanager
def _callback_scope(self, hook: CallbackHook) -> Iterator[None]:
"""Sets task-local callback metadata for the duration of a callback."""
token = self._callback_info.set(CallbackInvocationInfo(hook=hook))
try:
yield
finally:
self._callback_info.reset(token)

@property
def branch(self) -> str | None:
"""The branch path of the current invocation context."""
Expand Down
33 changes: 18 additions & 15 deletions src/google/adk/flows/llm_flows/_tool_caller.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
from google.genai import types

from . import _tool_error_handler
from ...agents._callback_metadata import CallbackHook
from ...agents.active_streaming_tool import ActiveStreamingTool
from ...events.event import Event
from ...live.live_request_queue import LiveRequestQueue
Expand Down Expand Up @@ -679,13 +680,14 @@ async def _prepare_single(
# Step 2: If no overrides are provided from the plugins, further run the
# canonical callback.
if override_response is None:
override_response = await _run_callbacks(
agent.canonical_before_tool_callbacks, # type: ignore[arg-type]
_stop_on_non_none,
tool=tool,
args=function_args,
tool_context=tool_context,
)
with tool_context._callback_scope(CallbackHook.BEFORE_TOOL):
override_response = await _run_callbacks(
agent.canonical_before_tool_callbacks, # type: ignore[arg-type]
_stop_on_non_none,
tool=tool,
args=function_args,
tool_context=tool_context,
)

# Handle tool lookup failure if before-tool callbacks did not override the
# response.
Expand Down Expand Up @@ -797,14 +799,15 @@ async def _run_with_trace() -> Event | None:
# Step 5: If no overrides are provided from the plugins, further run the
# canonical after_tool_callbacks.
if altered_function_response is None:
altered_function_response = await _run_callbacks(
agent.canonical_after_tool_callbacks, # type: ignore[arg-type]
_stop_on_non_none,
tool=tool,
args=function_args,
tool_context=tool_context,
tool_response=callback_tool_response,
)
with tool_context._callback_scope(CallbackHook.AFTER_TOOL):
altered_function_response = await _run_callbacks(
agent.canonical_after_tool_callbacks, # type: ignore[arg-type]
_stop_on_non_none,
tool=tool,
args=function_args,
tool_context=tool_context,
tool_response=callback_tool_response,
)

# Step 6: If alternative response exists from after_tool_callback, use it
# instead of the original function response.
Expand Down
18 changes: 10 additions & 8 deletions src/google/adk/flows/llm_flows/_tool_error_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from typing import Optional
from typing import TYPE_CHECKING

from ...agents._callback_metadata import CallbackHook
from ...tools.base_tool import BaseTool
from ...tools.tool_context import ToolContext
from ...utils._callback_pipeline import _run_callbacks
Expand Down Expand Up @@ -117,11 +118,12 @@ async def run_on_tool_error_callbacks(
if error_response is not None:
return error_response

return await _run_callbacks(
agent.canonical_on_tool_error_callbacks, # type: ignore[arg-type]
_stop_on_non_none,
tool=tool,
args=tool_args,
tool_context=tool_context,
error=error,
)
with tool_context._callback_scope(CallbackHook.ON_TOOL_ERROR):
return await _run_callbacks(
agent.canonical_on_tool_error_callbacks, # type: ignore[arg-type]
_stop_on_non_none,
tool=tool,
args=tool_args,
tool_context=tool_context,
error=error,
)
42 changes: 23 additions & 19 deletions src/google/adk/flows/llm_flows/base_llm_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from . import _live_llm_flow
from . import _output_schema_processor
from . import functions
from ...agents._callback_metadata import CallbackHook
from ...agents._streaming_mode import StreamingMode
from ...agents.base_agent import BaseAgent
from ...agents.callback_context import CallbackContext
Expand Down Expand Up @@ -257,12 +258,13 @@ async def _handle_before_model_callback(

# If no overrides are provided from the plugins, further run the canonical
# callbacks.
callback_response = await _run_callbacks(
agent.canonical_before_model_callbacks,
_stop_on_truthy,
callback_context=callback_context,
llm_request=llm_request,
)
with callback_context._callback_scope(CallbackHook.BEFORE_MODEL):
callback_response = await _run_callbacks(
agent.canonical_before_model_callbacks,
_stop_on_truthy,
callback_context=callback_context,
llm_request=llm_request,
)
if callback_response:
return callback_response
return None
Expand Down Expand Up @@ -327,12 +329,13 @@ async def _maybe_add_grounding_metadata(

# If no overrides are provided from the plugins, further run the canonical
# callbacks.
callback_response = await _run_callbacks(
agent.canonical_after_model_callbacks,
_stop_on_truthy,
callback_context=callback_context,
llm_response=llm_response,
)
with callback_context._callback_scope(CallbackHook.AFTER_MODEL):
callback_response = await _run_callbacks(
agent.canonical_after_model_callbacks,
_stop_on_truthy,
callback_context=callback_context,
llm_response=llm_response,
)
if callback_response:
return await _maybe_add_grounding_metadata(callback_response)
return await _maybe_add_grounding_metadata()
Expand Down Expand Up @@ -390,13 +393,14 @@ async def _run_on_model_error_callbacks(
if error_response is not None:
return error_response

return await _run_callbacks(
agent.canonical_on_model_error_callbacks,
_stop_on_non_none,
callback_context=callback_context,
llm_request=llm_request,
error=error,
)
with callback_context._callback_scope(CallbackHook.ON_MODEL_ERROR):
return await _run_callbacks(
agent.canonical_on_model_error_callbacks,
_stop_on_non_none,
callback_context=callback_context,
llm_request=llm_request,
error=error,
)

try:
async with _instrumentation.record_inference_telemetry(
Expand Down
Loading