Skip to content

Commit 1732b38

Browse files
authored
Merge branch 'main' into fix/databasesession-update-wrong-timezone
2 parents 3956f26 + 87cd310 commit 1732b38

30 files changed

Lines changed: 1817 additions & 949 deletions

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ You can install the latest stable version of ADK using `pip`:
6969
pip install google-adk
7070
```
7171

72+
To install optional integrations, you can use the following command:
73+
```bash
74+
pip install "google-adk[extensions]"
75+
```
76+
7277
The release cadence is roughly bi-weekly.
7378

7479
This version is recommended for most users as it represents the most recent official release.

contributing/samples/adk_triaging_agent/agent.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
"models": "xuanyang15",
3838
"services": "DeanChensj",
3939
"tools": "xuanyang15",
40-
"tracing": "jawoszek",
40+
"tracing": "mhenc",
4141
"web": "wyf7107",
4242
"workflow": "DeanChensj",
4343
}

src/google/adk/agents/base_agent.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import inspect
1818
import logging
19+
import sys
1920
from typing import Any
2021
from typing import AsyncGenerator
2122
from typing import Awaitable
@@ -285,7 +286,9 @@ async def run_async(
285286
Event: the events generated by the agent.
286287
"""
287288

288-
with tracer.start_as_current_span(f'invoke_agent {self.name}') as span:
289+
cm = tracer.start_as_current_span(f'invoke_agent {self.name}')
290+
span = cm.__enter__()
291+
try:
289292
ctx = self._create_invocation_context(parent_context)
290293
tracing.trace_agent_invocation(span, self, ctx)
291294
if event := await self._handle_before_agent_callback(ctx):
@@ -302,6 +305,23 @@ async def run_async(
302305

303306
if event := await self._handle_after_agent_callback(ctx):
304307
yield event
308+
except BaseException:
309+
try:
310+
cm.__exit__(*sys.exc_info())
311+
except ValueError:
312+
logger.warning(
313+
'Failed to detach context during generator cleanup, likely due to'
314+
' cancellation.'
315+
)
316+
raise
317+
else:
318+
try:
319+
cm.__exit__(None, None, None)
320+
except ValueError:
321+
logger.warning(
322+
'Failed to detach context during generator cleanup, likely due to'
323+
' cancellation.'
324+
)
305325

306326
@final
307327
async def run_live(

src/google/adk/agents/invocation_context.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from ..apps.app import EventsCompactionConfig
2929
from ..apps.app import ResumabilityConfig
3030
from ..artifacts.base_artifact_service import BaseArtifactService
31+
from ..auth.auth_credential import AuthCredential
3132
from ..auth.credential_service.base_credential_service import BaseCredentialService
3233
from ..events.event import Event
3334
from ..memory.base_memory_service import BaseMemoryService
@@ -214,6 +215,9 @@ class InvocationContext(BaseModel):
214215
canonical_tools_cache: Optional[list[BaseTool]] = None
215216
"""The cache of canonical tools for this invocation."""
216217

218+
credential_by_key: dict[str, AuthCredential] = Field(default_factory=dict)
219+
"""The resolved credentials for this invocation, keyed by credential_key."""
220+
217221
_invocation_cost_manager: _InvocationCostManager = PrivateAttr(
218222
default_factory=_InvocationCostManager
219223
)

src/google/adk/agents/readonly_context.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
if TYPE_CHECKING:
2323
from google.genai import types
2424

25+
from ..auth.auth_credential import AuthCredential
2526
from ..sessions.session import Session
2627
from .invocation_context import InvocationContext
2728
from .run_config import RunConfig
@@ -69,3 +70,7 @@ def user_id(self) -> str:
6970
def run_config(self) -> Optional[RunConfig]:
7071
"""The run config of the current invocation. READONLY field."""
7172
return self._invocation_context.run_config
73+
74+
def get_credential(self, key: str) -> Optional[AuthCredential]:
75+
"""Gets a resolved credential by key for this invocation."""
76+
return self._invocation_context.credential_by_key.get(key)

src/google/adk/cli/adk_web_server.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2075,6 +2075,7 @@ async def run_agent_live(
20752075
proactive_audio: bool | None = Query(default=None),
20762076
enable_affective_dialog: bool | None = Query(default=None),
20772077
enable_session_resumption: bool | None = Query(default=None),
2078+
save_live_blob: bool = Query(default=False),
20782079
) -> None:
20792080
ws_origin = websocket.headers.get("origin")
20802081
if ws_origin is not None and not _is_request_origin_allowed(
@@ -2117,6 +2118,7 @@ async def forward_events():
21172118
if enable_session_resumption is not None
21182119
else None
21192120
),
2121+
save_live_blob=save_live_blob,
21202122
)
21212123
async with Aclosing(
21222124
runner.run_live(

src/google/adk/cli/browser/index.html

Lines changed: 2 additions & 2 deletions
Large diffs are not rendered by default.

src/google/adk/cli/browser/main-3OBAHHYV.js renamed to src/google/adk/cli/browser/main-TCIQIOZ3.js

Lines changed: 186 additions & 186 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/google/adk/environment/_local_environment.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -123,23 +123,24 @@ async def execute(
123123
)
124124

125125
@override
126-
async def read_file(self, path: str) -> bytes:
126+
async def read_file(self, path: str | Path) -> bytes:
127127
if self._working_dir is None:
128128
raise RuntimeError('`working_dir` is not set. Call initialize() first.')
129129

130-
path = self._resolve_path(path)
131-
return await asyncio.to_thread(self._sync_read, path)
130+
resolved = self._resolve_path(path)
131+
return await asyncio.to_thread(self._sync_read, resolved)
132132

133133
@override
134-
async def write_file(self, path: str, content: str | bytes) -> None:
134+
async def write_file(self, path: str | Path, content: str | bytes) -> None:
135135
if self._working_dir is None:
136136
raise RuntimeError('`working_dir` is not set. Call initialize() first.')
137137

138-
path = self._resolve_path(path)
139-
return await asyncio.to_thread(self._sync_write, path, content)
138+
resolved = self._resolve_path(path)
139+
return await asyncio.to_thread(self._sync_write, resolved, content)
140140

141-
def _resolve_path(self, path: str) -> str:
141+
def _resolve_path(self, path: str | Path) -> str:
142142
"""Resolve a relative path against the working directory."""
143+
path = str(path)
143144
if os.path.isabs(path):
144145
return path
145146
return os.path.join(self._working_dir, path)

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

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import asyncio
1919
import inspect
2020
import logging
21+
import sys
2122
from typing import AsyncGenerator
2223
from typing import Optional
2324
from typing import TYPE_CHECKING
@@ -143,10 +144,11 @@ async def _resolve_toolset_auth(
143144
if not auth_config:
144145
continue
145146

147+
auth_config_copy = auth_config.model_copy(deep=True)
146148
try:
147-
credential = await CredentialManager(auth_config).get_auth_credential(
148-
callback_context
149-
)
149+
credential = await CredentialManager(
150+
auth_config_copy
151+
).get_auth_credential(callback_context)
150152
except ValueError as e:
151153
# Validation errors from CredentialManager should be logged but not
152154
# block the flow - the toolset may still work without auth
@@ -158,14 +160,16 @@ async def _resolve_toolset_auth(
158160
credential = None
159161

160162
if credential:
161-
# Populate in-place for toolset to use in get_tools()
162-
auth_config.exchanged_auth_credential = credential
163+
# Store in invocation context to avoid data leakage and race conditions
164+
invocation_context.credential_by_key[auth_config.credential_key] = (
165+
credential
166+
)
163167
else:
164168
# Need auth - will interrupt
165169
toolset_id = (
166170
f'{TOOLSET_AUTH_CREDENTIAL_ID_PREFIX}{type(tool_union).__name__}'
167171
)
168-
pending_auth_requests[toolset_id] = auth_config
172+
pending_auth_requests[toolset_id] = auth_config_copy
169173

170174
if not pending_auth_requests:
171175
return
@@ -1165,7 +1169,9 @@ async def _call_llm_async(
11651169
) -> AsyncGenerator[LlmResponse, None]:
11661170

11671171
async def _call_llm_with_tracing() -> AsyncGenerator[LlmResponse, None]:
1168-
with tracer.start_as_current_span('call_llm') as span:
1172+
cm = tracer.start_as_current_span('call_llm')
1173+
span = cm.__enter__()
1174+
try:
11691175
# Runs before_model_callback inside the call_llm span so
11701176
# plugins observe the same span as after/error callbacks.
11711177
if response := await self._handle_before_model_callback(
@@ -1258,6 +1264,23 @@ async def _call_llm_with_tracing() -> AsyncGenerator[LlmResponse, None]:
12581264
llm_response = altered
12591265

12601266
yield llm_response
1267+
except BaseException:
1268+
try:
1269+
cm.__exit__(*sys.exc_info())
1270+
except ValueError:
1271+
logger.warning(
1272+
'Failed to detach context during generator cleanup, likely due to'
1273+
' cancellation.'
1274+
)
1275+
raise
1276+
else:
1277+
try:
1278+
cm.__exit__(None, None, None)
1279+
except ValueError:
1280+
logger.warning(
1281+
'Failed to detach context during generator cleanup, likely due to'
1282+
' cancellation.'
1283+
)
12611284

12621285
async with Aclosing(_call_llm_with_tracing()) as agen:
12631286
async for event in agen:

0 commit comments

Comments
 (0)