| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
RedisSessionService.append_event stored time.time() in session.last_update_time, so a session's activity time was the moment the event was appended rather than the moment the event happened. The three other session backends all store event.timestamp. last_update_time is the sort key list_sessions documents as "oldest first", so the divergence reorders a user's session list whenever an event predates its append. ADK re-delivers the same event object to a second session reference, and that event carries its original timestamp, so replayed events are the case where the two disagree most. The shared session-service contract harness already recorded this as a divergence with xfail(strict=True), so the entry is removed with the fix: a test that starts passing while its entry is still registered fails as XPASS(strict). The second Redis entry stays; it is a different defect.
|
Thank you @feiiiiii5 for your contribution! 🎉 Your changes have been successfully imported and merged via Copybara in commit def458b. Closing this PR as the changes are now in the main branch. |
Sorry, something went wrong.
…uery analytics (#15) * fix(plugins): record error-bearing tool results as TOOL_ERROR in BigQuery analytics BigQueryAgentAnalyticsPlugin wrote TOOL_ERROR only from on_tool_error_callback. A tool that failed without its exception reaching that callback was therefore recorded as TOOL_COMPLETED with status OK: an MCP tool returning a CallToolResult with isError set, and a raising tool whose error ReflectAndRetryToolPlugin answered first. after_tool_callback now records such a result as TOOL_ERROR, with the content, status, error message, span and latency of the row on_tool_error_callback writes, and leaves the result the model receives unchanged. The built-in rules match the ReflectAndRetryToolPlugin response type and an MCP isError of exactly True. A new BigQueryLoggerConfig.tool_result_classifier lets an application classify its own result shapes first. The MCP error text and the result stay out of the TOOL_ERROR row: error_message bypasses content_formatter and payload_column_denylist, and a formatter written for TOOL_COMPLETED rows would not scrub a result copied into TOOL_ERROR content. When the analytics plugin runs before the retry plugin, on_tool_error_callback has already recorded the failure. after_tool_callback now skips that call instead of writing a spurious TOOL_COMPLETED row that popped a span it did not own. Tools from ToolboxToolset and SkillToolset's own tools now get the TOOLBOX and SKILL tool_origin instead of UNKNOWN. Both are matched through sys.modules, so classification never imports toolbox_adk or skill_toolset. Fixes google#7112 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: stamp Redis sessions with the event timestamp Merge google#7293 Fixes google#7292 PiperOrigin-RevId: 990023711 * fix(plugins): classify BigQuery analytics tool results fail-closed after_tool_callback classified a tool result after it had closed the tool span, with no boundary of its own. Any of these dropped the call's outcome row: - a result whose ==, truth test or get() raised; - a tool_result_classifier verdict whose fields could not be read. The traceback _safe_callback then logged could carry the result. The built-in rules now read entries with dict.get, and compare and test only exact str and bool values, so none of the result's own code runs. The classifier's verdict is read inside the boundary around the classifier call. A second boundary around classification falls back to the TOOL_COMPLETED row with a constant warning. BaseException subclasses that are not Exceptions still propagate. Also from review: - recognize an MCP CallToolResult model, not only its dict dump, including one from the MCP SDK 2.x mcp_types package; - key the recorded-error marker by plugin instance, so two analytics plugins in one runner each record an error once; - call the classifier with the keyword arguments tool, tool_args, tool_context and result; - reject asynchronous or incompatible classifiers when the plugin is created, and close a coroutine a classifier returns; - record an exception without a message as its type name, and give an empty retry response a fixed message; - document registering the plugin before plugins that answer or re-raise tool callbacks, and why the per-tool error hooks are not consulted. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(plugins): read BigQuery analytics tool results field by field A hostile or malformed sibling field could still hide a well-formed error signal. Three reads could raise: - checking a field's type with isinstance() consulted the value's __class__; - looking an entry up hashed and compared the result's own keys; - vars() dispatched to a model subclass's __getattribute__. Any of them raising sent the whole result to the TOOL_COMPLETED fallback, so an MCP isError=True or a ReflectAndRetryToolPlugin answer was recorded as a success. Fields are now read without running any code of the result, its keys or its values: - types are checked with type.__subclasscheck__ on the actual type; - a dict result is scanned for exact-str keys instead of being looked up; - a model's fields come through the SDK class's own __dict__ descriptor. Each rule then runs on its own, so a failing one cannot hide another's signal. Every boundary logs its constant warning after leaving its except block; inside it, a failing log handler would print the caught exception. Also from review: - check a ReflectAndRetryToolPlugin answer before the classifier, so a classifier cannot hide a raised error in one plugin order only; - reject async generator classifiers and classifiers whose signature inspect cannot read, and close generator-based coroutines too; - keep the TOOL_ERROR row for a tool context that cannot take a weak reference; - state the plugin-order limits precisely, including unknown tool names and failures only a later plugin detects. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(plugins): keep BigQuery analytics outcome rows for unserializable results Two paths could still lose a tool call's outcome row and log an exception that can carry the payload through _safe_callback's traceback: - after_tool_callback serialized a TOOL_COMPLETED result outside any boundary. Classification runs none of the result's code, so a result whose attribute reads raise got past it and then raised from the serializer: a dict subclass or object whose __getattribute__ raises, a raising __class__, or a CallToolResult subclass that succeeded or whose error the classifier recorded as OK. - on_tool_error_callback marked the call as recorded before reading str(error). An error whose __str__ raised lost its TOOL_ERROR row, and once a later handler answered the error, after_tool_callback skipped the marked call, which was left with no outcome row at all. The result is now serialized inside a boundary that records the serializer's own sentinel and logs a constant warning after leaving the except block. The error message is read before the call is marked and falls back to the error's type name. Also from review: the classifier check unwraps functools.partial, so a partial of an object with an async __call__ is rejected, and any failure to read the classifier's signature raises ValueError. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * feat: honor tool_thread_pool_config for sync tools outside live mode A synchronous function tool blocks the event loop while it runs, and RunConfig.tool_thread_pool_config, the setting that moves tools off the loop, only applied in live mode. With it set, run_async now runs a synchronous function tool's function on the tool thread pool when an LlmAgent calls the tool, while async tools and tools used directly as Workflow nodes stay on the event loop, and without it nothing changes. Co-authored-by: George Weale <gweale@google.com> PiperOrigin-RevId: 990376105 * fix(plugins): release awaitables a BigQuery analytics classifier returns A synchronous tool_result_classifier that returned a failed asyncio Future, or a Task that failed later, had it dropped as it was. asyncio then logged the unretrieved exception, which can carry the tool result, with its traceback when the future was collected. That went past the plugin's constant warning, a content_formatter and the column denylist. A returned awaitable is now released without running its code. A coroutine is closed as before. An asyncio future or task that is still pending is cancelled, and the exception it holds, or ends with, is marked as retrieved. The built-in rule still records the call, and the warning stays constant. An async generator or any other awaitable runs nothing until it is iterated or awaited, so it is dropped as before. Also from review: - read the classifier's signature without evaluating its annotations, so Python 3.14 no longer rejects a classifier annotated with names imported only for type checking; - describe the error-flag rule as it works: any dict result whose top-level isError or is_error is True, not only an MCP dump. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * docs(plugins): say what cancelling a returned task runs Cancelling a pending task that a tool_result_classifier returned stops it before it runs only if it has not started yet. A task that is already running gets the cancellation delivered at its next await, and its own handlers then run on the loop. The docstring said the release ran none of the awaitable's code; it now says the awaitable is released without being awaited. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: drop unpairable trailing FRs in rearrange Merge google#6752 Fixes google#6751 PiperOrigin-RevId: 990390762 * fix: replay a parallel tool call that never ran when a sibling answered When a resumed model turn had parallel calls and only some had a response, the resume decision treated one answer as covering the whole turn, so it continued to the model and the call that never ran was dropped. The decision now replays just the calls with no response, and only when the agent has written nothing since the call, so answered calls do not run a second time. Close google#7108 Co-authored-by: George Weale <gweale@google.com> PiperOrigin-RevId: 990408003 * chore(scripts): detect added files through git only The new-file check read added files and commit messages from several version control systems. Contributors and CI use git through pre-commit, and jj's default colocated repositories already take the git path, so the rest only added code to maintain. Outside a git work tree the check now reports that it could not determine the added files. A `//`-prefixed .py path with no file behind it, the form Perforce depot paths take, is now refused. Co-authored-by: Xuan Yang <xygoogle@google.com> PiperOrigin-RevId: 990408500 * fix(tools): surface NodeTool failures to on_tool_error and return dict validation errors NodeTool swallowed every exception raised while running its node and returned a plain string, which the flow wrapped as {'result': '<string>'}. As a result: - on_tool_error callbacks (agent and plugin) never saw node failures, unlike every other BaseTool, and the original exception was lost behind the generic "Dynamic node <name> failed" wrapper. - Input validation errors reached the model as {'result': ...} instead of the {'error': ...} shape FunctionTool uses for argument validation errors. This change aligns NodeTool with FunctionTool: - Input schema validation errors return {'error': ...} with the same wording as FunctionTool, so the model can correct its arguments and retry. - When the node fails, the node's original exception is re-raised (chained from DynamicNodeFailError). The tool pipeline then runs on_tool_error callbacks with the real cause; if none handles it, the error propagates, as it does for FunctionTool. Node-level retry_config still applies first, inside run_node. Behavior change: an agent using a node as a tool without an on_tool_error callback now fails the run when the node fails, instead of passing an error string to the model. This matches FunctionTool. Co-authored-by: Shangjie Chen <deanchen@google.com> PiperOrigin-RevId: 990410996 * feat(mcp): add an opt-in modern-protocol connect path for MCP SDK 2.x ADK always brought a session up with `initialize()`, which pins the connection to the 2025 wire for its whole life even on SDK 2.x: the server issues an `Mcp-Session-Id` to route later requests back to one instance, and `clientInfo` is stated once rather than on each request. Setting `ADK_ENABLE_MCP_MODERN_PROTOCOL=1` probes `server/discover` first and falls back to the handshake on anything that is not a modern server, so a 2025-era server is unaffected. Off by default, and a no-op on SDK 1.x. Co-authored-by: Kathy Wu <wukathy@google.com> PiperOrigin-RevId: 990414661 * feat(workflow): propagate skip_summarization from node tools to tool response Propagate `ctx.actions.skip_summarization` from dynamically executed child nodes to the parent context in `run_node_internal`, and include `NodeTool` alongside `AgentTool` when attaching displayable tool output to `function_response` events with `skip_summarization=True`. This allows `@node` and `Workflow` tools to set `ctx.actions.skip_summarization = True` inside the node so their terminal output is emitted directly as the final user-visible text response without triggering a follow-up LLM summarization turn, while keeping `NodeTool` internal. Co-authored-by: Shangjie Chen <deanchen@google.com> PiperOrigin-RevId: 990434765 * fix(plugins): keep Pydantic from quoting tool results in warnings Pydantic still dumps a model whose field no longer matches its type, such as an MCP CallToolResult whose content list was appended to after validation, or an application model given a value of the wrong type. But by default it first emits a UserWarning that quotes the value. The plugin's dump of a tool result for the TOOL_COMPLETED row, and of the function response for the next LLM_REQUEST row, therefore printed the result to stderr or the py.warnings logger. That happened before content_formatter, the column denylist or log_multi_modal_content could apply. Both dumps now pass warnings=False, a per-call flag that changes no process-wide warning filter. A model_dump override that does not take the keyword is still called, without it, because it may be what leaves out fields that must not be reported. Also from review: - say that asyncio still reports a task that ignores its cancellation, and that an awaitable of another kind is left as it is, along with any future it wraps. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(eval): skip content-less events when mapping Vertex multi-turn turns Invocations now keep the final event with its content removed so the efficiency metrics can read its token usage, and the Vertex multi-turn facade sent that event as an empty agent message in every turn. Skips intermediate events without content when mapping a turn. Co-authored-by: George Weale <gweale@google.com> PiperOrigin-RevId: 990461145 * fix: detect a dead MCP session whose transport sits behind a dispatcher MCP SDK 2.x moved the transport off the client session and behind a dispatcher, so ADK's liveness check read attributes that no longer exist and a pooled session whose server had died looked healthy forever, failing every later call. A session with no streams of its own is now checked through its dispatcher's closed flag, which restores the existing reconnect path on both SDK majors. Co-authored-by: George Weale <gweale@google.com> PiperOrigin-RevId: 990472674 * fix: only apply --avatar_config to live sessions requesting video The server-wide avatar configuration added to `adk web` and `adk api_server` was attached to every /run_live session, including the default audio-only ones. Avatars are rendered as video, so only set `RunConfig.avatar_config` when the client requests the VIDEO modality, and say so in the `--avatar_config` help text. Also adds a test for the unreadable avatar configuration file path. Co-authored-by: Liang Wu <wuliang@google.com> PiperOrigin-RevId: 990498163 * fix: follow redirects when downloading skills in GcpSkillRegistry Merge google#6824 PiperOrigin-RevId: 990507320 * fix(plugins): leave a result model's own model_dump uncalled When a model_dump did not take warnings=False, the plugin called it again without the flag. An ordinary override that calls Pydantic's own with other arguments, such as super().model_dump(exclude=...), then let Pydantic quote a field that no longer fits its type in a UserWarning, before content_formatter ran. That happened whether the model was the result or was nested in it. A Pydantic model whose model_dump is an override is now not called. Whatever its signature, it may not pass the flag on, so it is recorded as the [UNSUPPORTED_OBJECT] sentinel. So is a model_dump that fails or does not take the flag. In those cases the serializer used to read the fields instead, which recorded what the dump leaves out. Pydantic's own model_dump, and an object that only looks like a model, are still called with the flag. An LLM_REQUEST row's function response part is dumped by Pydantic itself, which serializes nested models without calling their overrides. Also from review: the real-turn tests switch ADK's span content capture off. Once any test has installed a recording tracer, that capture dumps the same response with Pydantic's warnings on. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-authored-by: feiiiiii5 <feiiiiii5@users.noreply.github.com> Co-authored-by: George Weale <gweale@google.com> Co-authored-by: Aarav Mittal <137450929+a2105z@users.noreply.github.com> Co-authored-by: Xuan Yang <xygoogle@google.com> Co-authored-by: Shangjie Chen <deanchen@google.com> Co-authored-by: Kathy Wu <wukathy@google.com> Co-authored-by: Liang Wu <wuliang@google.com> Co-authored-by: Shinobu Aoki <aoki@codebee.jp>
…ytics error_message (#14) * fix(plugins): record content_formatter failure class in BigQuery rows When BigQueryLoggerConfig.content_formatter raised, or returned a type the parser cannot store, BigQueryAgentAnalyticsPlugin wrote the [FORMATTER_FAILED] sentinel, logged a constant warning, and left the row's error_message NULL, so the developer had no signal about why. Set error_message to a fixed-shape description that names only a class, for example "content_formatter raised ImportError" or "content_formatter returned unsupported type tuple", and never the exception message, args, or traceback, which can embed the content the formatter was protecting. Because type(name, ...) can mint a class named after that content, a name is used only when code chose it: a static type compiled into C, or a class bound under that name in its imported module. Any other class is described by its nearest such ancestor, for example "content_formatter raised <subclass of ValueError>". An event that already carries an error_message, such as a TOOL_ERROR, keeps it first, followed by "; " and the formatter failure. Add BigQueryLoggerConfig.debug_content_formatter_errors (default False), which attaches the traceback to the local formatter-failure warning for debugging. The traceback is never written to BigQuery, and the docstring warns that the process's log handlers can forward it, and the content it embeds, elsewhere. The fail-closed contract is unchanged: the sentinel content, the constant log text by default, and the formatter_failed drop counter. Refs: GoogleCloudPlatform/BigQuery-Agent-Analytics-SDK#485 (item A) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: stamp Redis sessions with the event timestamp Merge google#7293 Fixes google#7292 PiperOrigin-RevId: 990023711 * fix(plugins): stop formatter diagnostics leaking names or dropping rows Review of the previous commit found three ways the new content_formatter diagnostics could still leak payload text or drop a row. This closes them. Trusted labels only. A class created at runtime can be named after the content a formatter was protecting and then bound into its module, so "the class is bound under its name in its module" proved nothing, and such a name reached error_message. error_message now names a class only by a label that no runtime data can have chosen: the compiled name of a static C type, or a fixed string for an allowlisted class matched by identity (LlmRequest, types.Content, types.Part, pydantic BaseModel, and google.api_core GoogleAPICallError). Every other class, including every Python-defined library exception and every class the developer defines, is described by its nearest trusted ancestor, for example "content_formatter raised <subclass of ValueError>". Trade-off: fewer exact names, for example json.JSONDecodeError now reads "<subclass of ValueError>" and a google.api_core NotFound "<subclass of GoogleAPICallError>"; debug_content_formatter_errors shows the exact class locally. No class code runs while diagnosing. Flags, name, and MRO are read through type's own descriptors, and allowlist matching compares identity, so a metaclass __getattribute__, __eq__, or __hash__ can no longer run inside the fail-closed boundary. Such a hook could raise asyncio.CancelledError, which escaped the boundary's `except Exception` and dropped the row. Classification now cannot raise at all, so it needs no BaseException handler, and a genuine KeyboardInterrupt or SystemExit delivered by a signal still propagates. The exact-class check on formatter results also compares identity now, for the same reason. Best-effort debug traceback. With debug_content_formatter_errors, the traceback is rendered to text once, inside the plugin, and appended to the warning; handlers never receive the live exception, whose own code could otherwise fail inside a stock StreamHandler and drop the row. A rendering failure falls back to a constant placeholder. KeyboardInterrupt and SystemExit still propagate, because a signal can deliver them at any bytecode; any other BaseException raised while rendering, CancelledError included, can only come from the exception being rendered, since rendering never awaits. The warning is emitted after the except block, so a failing handler's handleError cannot reach the formatter's exception through its exception chain. Tests pin each guard: registered payload-named classes and their subclasses, a renamed allowlisted class, metaclass hooks raising CancelledError, SystemExit, or RuntimeError on both failure paths, unrenderable tracebacks under a stock StreamHandler, interrupt propagation, and a failing log handler. Removing any guard fails at least one test. Refs: GoogleCloudPlatform/BigQuery-Agent-Analytics-SDK#485 (item A) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(plugins): keep formatter diagnosis from ever dropping the row Review of the previous commit showed three more ways that diagnosing a content_formatter failure could escape and drop the row, even after the hook-by-hook fixes: - a rendering hook raising SystemExit or KeyboardInterrupt, which the previous policy re-raised as possibly genuine; - type's own descriptors raising TypeError once a meta-metaclass drops `type` from the failed class's metaclass MRO, which disproved "classification cannot raise"; - a log handler or filter raising while the warning is emitted, which also misattributed an unsupported result as "raised RuntimeError". Instead of patching one hook at a time, all of diagnosis (naming the failed class, rendering the debug traceback, and emitting the warning) now runs in _diagnose_formatter_failure, behind one boundary that contains whatever is raised, of any type, and falls back to a constant note. It runs only after the fail-closed state is settled: the formatter's try/except now only records the failure and writes the sentinel, and the failure is counted before diagnosis starts. Nothing diagnosis does can reach the row, the sentinel, the counter, or the callback's caller. BaseException is contained deliberately. An interrupt raised by a hook, handler, or filter cannot be told apart from one a signal handler delivered, and letting it through would let the content under redaction abort the agent run and lose the row. Diagnosis never awaits, so a real asyncio cancellation is never swallowed; a signal that lands inside the short window is absorbed, and the next is delivered normally. Interrupts raised by the formatter call itself still propagate, as before. Two inner guards remain where they keep the warning: labeling falls back to "<unknown class>" when type's descriptors raise, and rendering falls back to a placeholder. Diagnosis still runs after the except block, so a failing handler's handleError cannot print the formatter's exception. A result whose __class__ merely claims to be str is now reported as an unsupported result instead of "raised TypeError". Tests: a property test injects RuntimeError, CancelledError, KeyboardInterrupt, SystemExit, and another BaseException at each diagnostic step (labeling, rendering, a handler, a filter) on both paths and requires the sentinel row and its count. Also added: the meta-metaclass regression on both paths, rendering hooks raising interrupts, a stderr check that a failing handler never prints the formatter's exception, and a guard that interrupts from the formatter call still propagate. Removing the boundary or any guard fails a test. Refs: GoogleCloudPlatform/BigQuery-Agent-Analytics-SDK#485 (item A) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(plugins): close remaining formatter paths that leak or drop rows Review of the previous commit found three more paths, each also present on main, by which a content_formatter failure could still leak payload text or lose the row: - The failure warning can be logged while the caller is handling an exception, as ADK is when it runs error callbacks. A log handler that fails prints the exception being handled through handleError, so a closed stream could print the caller's exception, or the formatter's when it re-raised that one, to stderr. - The result check used isinstance(formatted, (dict, list)), which falls back to the object's own __class__. A returned object could raise CancelledError, SystemExit, or KeyboardInterrupt there and lose the row, or raise an ordinary exception that was then reported as the formatter having raised. - A rejected coroutine was released unstarted, so Python warned "coroutine '<name>' was never awaited", and the formatter can set that name from the content. Everything after the formatter call now runs in _settle_formatter_outcome, behind the one boundary that already contained diagnosis: judging the result, closing a rejected coroutine or generator, naming the class, rendering the debug traceback, and logging. The result is judged only by its real type, through issubclass(type(x), ...) and identity, which runs none of its code. A str, dict, or list subclass is still accepted; an object whose __class__ merely claims to be one is now a counted formatter failure rather than an [UNSUPPORTED_OBJECT] row. A rejected coroutine or generator is closed before release, which for an unstarted one runs no code and emits no warning. The warning is emitted while a constant stand-in exception, raised from None, is being handled, so a failing handler prints only that. The formatter call itself still lets KeyboardInterrupt, SystemExit, and CancelledError propagate. Docs: the content_formatter docstring now says "raises an Exception", explains judging by real type, and notes that interrupts from the call propagate. The boundary's docstring states plainly that a genuine signal delivered while it runs, including while a handler blocks, is absorbed. The debug flag notes that a failing handler echoes the rendered traceback to stderr. Tests: a caller handling a payload exception with a closed handler (re-raised and distinct); results whose __class__ property or __getattribute__ raises each interrupt type or claims to be a dict; an async formatter and a payload-named coroutine, with warnings recorded; and two more steps in the injection property test, judging the result and closing a rejected generator. Removing any guard fails a test. Refs: GoogleCloudPlatform/BigQuery-Agent-Analytics-SDK#485 (item A) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(plugins): honor genuine interrupts and isolate all plugin logging Review of the previous commit found two remaining gaps, and two guards that no test pinned: - A KeyboardInterrupt or SystemExit delivered while a content_formatter failure was being described was absorbed. A real SIGINT, or a SIGTERM whose handler calls sys.exit while a log handler blocks on I/O, left the row written but the process running, unaware of the signal. - Only the formatter-failure warning ran while a stand-in exception was handled. Any other plugin warning, such as the parser's, could still have a failing handler print the exception the caller is handling, as ADK is when it runs error callbacks. Interrupts are now sorted by the code that raised them, because Python cannot tell a signal from a direct raise. The failed class's and the rejected result's own code runs only while the debug traceback is rendered and while a rejected generator is closed. Each of those steps has its own guard, and whatever it raises, interrupts included, is contained, so the content under redaction cannot end the agent run. Everywhere else in the boundary only plugin code and the application's log filters and handlers run, so a KeyboardInterrupt or SystemExit there came from a signal or from the application. _settle_formatter_outcome returns it as a new exception without text (a SystemExit keeps an int exit code), and _log_event raises it once the row has been handed to the writer. CancelledError stays contained: nothing in the boundary awaits, so it cannot be a real cancellation. The trade-off is that a signal landing while payload-controlled code runs is still absorbed. The module logger's handle() now runs every record's filters and handlers while a constant stand-in exception is handled, with its __context__ cleared. So no plugin log call can print the caller's exception through handleError, or through a handler that walks __context__ and ignores __suppress_context__. Records are unchanged: Logger._log resolves exc_info and the calling function before handle() runs. This replaces the stand-in around the formatter warning alone. The boundary's docstring now bounds its never-drop claim to failed or rejected results. An admitted str, dict, or list subclass goes on to the parser, whose boundary catches Exception only. Tests: - the injection property test expects an interrupt from any step but closing to be raised after the row, as a new exception, and checks that a rejected generator's cleanup never reaches the unraisable hook; - a real SIGINT and SIGTERM delivered while the warning is emitted; - a parse-failure warning with a closed handler, and with a handler that walks __context__, while the caller handles a payload exception; - the plugin's error logs keep their own exc_info; - supported results (None, a str subclass whose hooks raise, dict and list subclasses, and exact Content, Part, and LlmRequest) pass through unchanged. Removing any new guard fails a test. Refs: GoogleCloudPlatform/BigQuery-Agent-Analytics-SDK#485 (item A) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * docs(plugins): note that closing a rejected coroutine is contained too The boundary's docstring said the rejected result's own code runs only while the debug traceback is rendered and while a rejected generator is closed. The same guard also covers closing a rejected coroutine, which runs the coroutine's own cleanup if it was started. Wording only. Refs: GoogleCloudPlatform/BigQuery-Agent-Analytics-SDK#485 (item A) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * feat: honor tool_thread_pool_config for sync tools outside live mode A synchronous function tool blocks the event loop while it runs, and RunConfig.tool_thread_pool_config, the setting that moves tools off the loop, only applied in live mode. With it set, run_async now runs a synchronous function tool's function on the tool thread pool when an LlmAgent calls the tool, while async tools and tools used directly as Workflow nodes stay on the event loop, and without it nothing changes. Co-authored-by: George Weale <gweale@google.com> PiperOrigin-RevId: 990376105 * fix: drop unpairable trailing FRs in rearrange Merge google#6752 Fixes google#6751 PiperOrigin-RevId: 990390762 * fix: replay a parallel tool call that never ran when a sibling answered When a resumed model turn had parallel calls and only some had a response, the resume decision treated one answer as covering the whole turn, so it continued to the model and the call that never ran was dropped. The decision now replays just the calls with no response, and only when the agent has written nothing since the call, so answered calls do not run a second time. Close google#7108 Co-authored-by: George Weale <gweale@google.com> PiperOrigin-RevId: 990408003 * chore(scripts): detect added files through git only The new-file check read added files and commit messages from several version control systems. Contributors and CI use git through pre-commit, and jj's default colocated repositories already take the git path, so the rest only added code to maintain. Outside a git work tree the check now reports that it could not determine the added files. A `//`-prefixed .py path with no file behind it, the form Perforce depot paths take, is now refused. Co-authored-by: Xuan Yang <xygoogle@google.com> PiperOrigin-RevId: 990408500 * fix(tools): surface NodeTool failures to on_tool_error and return dict validation errors NodeTool swallowed every exception raised while running its node and returned a plain string, which the flow wrapped as {'result': '<string>'}. As a result: - on_tool_error callbacks (agent and plugin) never saw node failures, unlike every other BaseTool, and the original exception was lost behind the generic "Dynamic node <name> failed" wrapper. - Input validation errors reached the model as {'result': ...} instead of the {'error': ...} shape FunctionTool uses for argument validation errors. This change aligns NodeTool with FunctionTool: - Input schema validation errors return {'error': ...} with the same wording as FunctionTool, so the model can correct its arguments and retry. - When the node fails, the node's original exception is re-raised (chained from DynamicNodeFailError). The tool pipeline then runs on_tool_error callbacks with the real cause; if none handles it, the error propagates, as it does for FunctionTool. Node-level retry_config still applies first, inside run_node. Behavior change: an agent using a node as a tool without an on_tool_error callback now fails the run when the node fails, instead of passing an error string to the model. This matches FunctionTool. Co-authored-by: Shangjie Chen <deanchen@google.com> PiperOrigin-RevId: 990410996 * feat(mcp): add an opt-in modern-protocol connect path for MCP SDK 2.x ADK always brought a session up with `initialize()`, which pins the connection to the 2025 wire for its whole life even on SDK 2.x: the server issues an `Mcp-Session-Id` to route later requests back to one instance, and `clientInfo` is stated once rather than on each request. Setting `ADK_ENABLE_MCP_MODERN_PROTOCOL=1` probes `server/discover` first and falls back to the handshake on anything that is not a modern server, so a 2025-era server is unaffected. Off by default, and a no-op on SDK 1.x. Co-authored-by: Kathy Wu <wukathy@google.com> PiperOrigin-RevId: 990414661 * feat(workflow): propagate skip_summarization from node tools to tool response Propagate `ctx.actions.skip_summarization` from dynamically executed child nodes to the parent context in `run_node_internal`, and include `NodeTool` alongside `AgentTool` when attaching displayable tool output to `function_response` events with `skip_summarization=True`. This allows `@node` and `Workflow` tools to set `ctx.actions.skip_summarization = True` inside the node so their terminal output is emitted directly as the final user-visible text response without triggering a follow-up LLM summarization turn, while keeping `NodeTool` internal. Co-authored-by: Shangjie Chen <deanchen@google.com> PiperOrigin-RevId: 990434765 * fix(eval): skip content-less events when mapping Vertex multi-turn turns Invocations now keep the final event with its content removed so the efficiency metrics can read its token usage, and the Vertex multi-turn facade sent that event as an empty agent message in every turn. Skips intermediate events without content when mapping a turn. Co-authored-by: George Weale <gweale@google.com> PiperOrigin-RevId: 990461145 * fix: detect a dead MCP session whose transport sits behind a dispatcher MCP SDK 2.x moved the transport off the client session and behind a dispatcher, so ADK's liveness check read attributes that no longer exist and a pooled session whose server had died looked healthy forever, failing every later call. A session with no streams of its own is now checked through its dispatcher's closed flag, which restores the existing reconnect path on both SDK majors. Co-authored-by: George Weale <gweale@google.com> PiperOrigin-RevId: 990472674 * fix: only apply --avatar_config to live sessions requesting video The server-wide avatar configuration added to `adk web` and `adk api_server` was attached to every /run_live session, including the default audio-only ones. Avatars are rendered as video, so only set `RunConfig.avatar_config` when the client requests the VIDEO modality, and say so in the `--avatar_config` help text. Also adds a test for the unreadable avatar configuration file path. Co-authored-by: Liang Wu <wuliang@google.com> PiperOrigin-RevId: 990498163 * fix: follow redirects when downloading skills in GcpSkillRegistry Merge google#6824 PiperOrigin-RevId: 990507320 * test: cover api server auto create session Merge google#6786 PiperOrigin-RevId: 990538126 * feat: propagate grounding metadata from MCP _meta Merge google#7047 Fixes google#6081 PiperOrigin-RevId: 990543079 * docs: explain local lockfile needed by tox in adk-setup skill Merge google#7318 PiperOrigin-RevId: 990578226 * feat: let BigQuery tools run where CMEK is required Merge google#7232 Fixes google#3931 PiperOrigin-RevId: 990587308 * feat: add the model consult session context handover layer When a small executor model escalates a hard step to a larger advisor model, the advisor has to be told what has happened so far, or it answers a question it does not understand. This adds the layer that turns a session event log into contents the advisor can read: - tool calls and tool results are flattened into plain text, so a model that was never given those tool declarations can still follow them; - consecutive events from the same role are merged into one turn, which third-party advisor models require; - the transcript is held to a character budget by keeping the head and the tail, marking the omitted middle, and trimming the newest turn when it does not fit on its own, so one long turn cannot silently multiply the cost of a consult; - thoughts, media parts, per-part length and event count are each configurable, and the in-flight escalation call itself is skipped. The layer is not wired into a tool yet; that follows in a later change. Co-authored-by: Xuan Yang <xygoogle@google.com> PiperOrigin-RevId: 990610529 * feat(tools): invoke advisor models without tools for model_consult Invoke the advisor `BaseLlm` via `generate_content_async(req, stream=False)` with `config.tools = []` and `config.tool_config = None` so mid-task consultations return text guidance without entering a tool loop, while still emitting standard OpenTelemetry client duration and token usage metrics: - `resolve_advisor_llm` and `resolve_thinking_level` for model and thinking-level normalization - `call_advisor` with automatic fallback retry when `thinking_config` is rejected, `MAX_TOKENS` truncation and thought-exhaustion handling across `Gemini` and `LiteLlm`, and OpenTelemetry metric emission - `AdvisorResult`, `AdvisorUsage`, and `AdvisorError` Co-authored-by: Xuan Yang <xygoogle@google.com> PiperOrigin-RevId: 990613834 * feat(tools): add ModelConsultTool with turn and session budgets Add `ModelConsultTool`, an ADK tool that lets an executor `LlmAgent` consult a stronger advisor model mid-generation without relinquishing control of the conversation. Key capabilities: - Per-turn (`max_uses`) and session-wide (`session_max_uses`) consult budgets stored in session state (`temp:` and persistent state keys) with `has_remaining_budget` helper and per-session concurrency/delta coordination for parallel tool calls. - Automatic context handover via `build_advisor_contents` (with both `'events'` and `'transcript'` handover modes on `ModelConsultContextConfig`). - Forwards the executor's `static_instruction`, state-interpolated `canonical_instruction`, and non-self tool inventory (`canonical_tools`) to the advisor system instruction. - Graceful degradation on budget exhaustion (`status='limit_reached'`), missing question (`status='invalid_request'`), and advisor runtime or timeout errors (`status='error'`). - Top-level lazy export of `ModelConsultTool` from `google.adk.tools`. Co-authored-by: Xuan Yang <xygoogle@google.com> PiperOrigin-RevId: 990614577 * docs(tools): add ModelConsultTool developer guide and sample agent Adds the developer guide and runnable order-support refund policy sample for `ModelConsultTool`: - Adds the `ModelConsultTool` unit guide under `docs/guides/tools/model_consult/model_consult_tool/index.md` covering getting started, how mid-generation escalation works, `ModelConsultTool` and `ModelConsultContextConfig` options, custom context budgets, custom `BaseLlm` advisors, and limitations, and registers it in `docs/guides/README.md`. - Adds a runnable e-commerce order support sample under `contributing/samples/tools/model_consult/` demonstrating `get_order`, `get_customer_profile`, and `issue_refund` paired with `ModelConsultTool`. Co-authored-by: Xuan Yang <xygoogle@google.com> PiperOrigin-RevId: 990615455 * fix: isolate and clean up single_turn LlmAgent node_input events Merge google#7320 Fixes google#7227 PiperOrigin-RevId: 990632092 * refactor(tools): extract shared URL validation helpers into _url_validator.py Extract the SSRF and URL target validation helpers (`_parse_request_target`, `_is_blocked_hostname`, `_is_blocked_address`, `_embedded_ipv4`, `_resolve_direct_addresses`, and `_reject_blocked_proxied_hostname`) into `_url_validator.py`. Previously, `ComputerUseToolset._wrap_navigate_with_url_validation` lazily imported private helpers from `load_web_page` inside the wrapper function to avoid pulling `requests` into the computer-use import path. Moving the validation logic into `_url_validator.py` allows `load_web_page`, `ComputerUseToolset`, and other outbound HTTP tools to import the shared validation helpers directly at the module level without extra runtime dependencies. Test files that patched `load_web_page.socket` were updated after private helpers were pruned from the `load_web_page` namespace. Co-authored-by: Jason Zhang <jasoncz@google.com> PiperOrigin-RevId: 990692502 * fix(plugins): chain re-raised interrupts to nothing, look up handle per call Review of the previous commit found two small hardening gaps: - The KeyboardInterrupt or SystemExit set aside while a content_formatter failure was described is raised after the row with `from None`. That only sets __suppress_context__: Python still records the exception the caller is handling, such as the error ADK passes to an error callback, as its __context__, so code that walks __context__ could reach it and its text. It is now raised while a context-free stand-in is handled, so that stand-in is its only link. - The stand-in wrapper bound Logger.handle when the plugin was imported, so a later class-level patch of Logger.handle, as instrumentation and test fixtures apply, never saw this logger's records. The class's handle is now looked up on each call, still inside the stand-in. The content_formatter docstring also states the downstream effect plainly: the row's status is left as the event set it, usually 'OK', so a query that counts any non-NULL error_message as an error, such as the BigQuery Agent Analytics SDK's error predicate, counts a formatter failure as an error. Behavior is unchanged. Tests: the re-raised interrupt links to neither the caller's exception nor anything chained to it (KeyboardInterrupt and SystemExit), and a class-level patch of Logger.handle made after import sees the plugin's formatter warning, handled while the stand-in is. Both fail before this change. Refs: GoogleCloudPlatform/BigQuery-Agent-Analytics-SDK#485 (item A) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-authored-by: feiiiiii5 <feiiiiii5@users.noreply.github.com> Co-authored-by: George Weale <gweale@google.com> Co-authored-by: Aarav Mittal <137450929+a2105z@users.noreply.github.com> Co-authored-by: Xuan Yang <xygoogle@google.com> Co-authored-by: Shangjie Chen <deanchen@google.com> Co-authored-by: Kathy Wu <wukathy@google.com> Co-authored-by: Liang Wu <wuliang@google.com> Co-authored-by: Shinobu Aoki <aoki@codebee.jp> Co-authored-by: YASHcode-IIITV <yashrajsiwal@gmail.com> Co-authored-by: Chaitanya Laxman <chaitanyalaxman118@gmail.com> Co-authored-by: Arjun Ganesh <iarjunganesh@gmail.com> Co-authored-by: Vishal Bulbule <vishal.bulbule@techtrapture.com> Co-authored-by: Abhay Joshi <abhayjoshi@google.com> Co-authored-by: Jason Zhang <jasoncz@google.com>
| Back | FazBrowse Home | New Git URL |
Please ensure you have read the contribution guide before creating a pull request.
Link to Issue or Description of Change
1. Link to an existing issue (if applicable):
Testing Plan
Problem:
RedisSessionService.append_event() wrote time.time() into session.last_update_time, so a session's activity time was the moment the event was appended rather than the moment the event happened. The other three backends store event.timestamp: InMemorySessionService at in_memory_session_service.py:357-364, SqliteSessionService at sqlite_session_service.py:433 and :515-519, DatabaseSessionService at database_session_service.py:976-1010.
last_update_time is the sort key list_sessions documents as "oldest first" (base_session_service.py:102-118), and the Redis backend sorts on it (_redis_session_service.py:316-318), so the divergence reorders a user's session list whenever an event predates its append. ADK re-delivers the same event object to a second session reference, and such an event carries its original timestamp, so two sessions that received the identical event report different activity times.
Solution:
Store event.timestamp, which is what the contract harness already specifies. The repository registers this as a known divergence in tests/unittests/sessions/_conformance.py, and that file's docstring states a divergence "becomes a defect anyone can pick up, and whoever fixes the backend has to delete the entry in the same change" (_conformance.py:15-31). Because the marker is xfail(strict=True), a test that starts passing while its entry is still registered fails as XPASS, so the source fix and the registry deletion ship together:
import time stays: create_session still uses time.time() for a brand-new session, correct there because no event exists yet. create_session, get_session and the other backends are untouched.
Unit Tests:
Added test_append_event_stamps_session_with_event_timestamp to tests/unittests/integrations/redis/test_redis_session_service.py. It pins time to a value 100 seconds past the event's own timestamp, so the previous implementation cannot agree by coincidence, and asserts both the caller's session and the reloaded session carry the event's timestamp.
Command outputBase at 9625b06c9a1be6b9ecd85225657690ae5c0e9d3e, CPython 3.14, new test without the source fix:
Same base, source fix without the registry deletion, which is what strict=True forbids:
With both halves, all six backends of the contract test plus the new one pass:
The redis parameter moves from XFAIL to PASSED once the registry entry is gone. Full session and Redis suites:
The one remaining xfail is the other Redis divergence named in #7292's scope note. Gates: pre-commit run --all-files passes for the three files in this diff; it also reports two pre-existing failures unrelated to it, trailing-whitespace on docs/guides/integrations/{e2b,gcs}/index.md and check-new-py-prefix on src/google/adk/memory/_sqlite_memory_service.py, both present on main at 9625b06. mypy reports no error in _redis_session_service.py.
Manual End-to-End (E2E) Tests:
Not applicable. The fixed path is covered by an offline unit test against FakeRedisAsync; it needs no external service, so there is no manual run to report.
Checklist
The second Redis entry in divergences is a separate defect: append_event writes the session key unconditionally, so appending to a deleted, expired or never-stored session recreates it instead of raising SessionNotFoundError. It overlaps open PR #7140, which owns those lines, so it is deliberately left registered here.
Additional context
A session persisted through one backend and read through another (src/google/adk/sessions/migration/) shows the Redis session jumping forward by however long an event sat in a queue, where SQLite does not. That is the migration-path form of the same defect.