feat(labs): add SnowflakeCortexAgent (Cortex Agents Run API SSE → ADK events) - #7015
Draft
miewone wants to merge 13 commits into
Draft
feat(labs): add SnowflakeCortexAgent (Cortex Agents Run API SSE → ADK events)#7015miewone wants to merge 13 commits into
miewone wants to merge 13 commits into
Conversation
|
Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). View this failed invocation of the CLA check for more information. For the most up to date status, view the checks section at the bottom of the pull request. |
This comment was marked as resolved.
This comment was marked as resolved.
Add after_tool_callback and plugin hooks so applications can transform Snowflake tool results and persist state and artifacts through ToolContext. Run callbacks on full normalized results before applying size limits, and correlate results with their original calls by tool_use_id. Keep remote execution owned by Snowflake: replacements affect ADK events and history, not Snowflake's internal results or final answer. Cover callback ordering, replacement, pairing, failure and cancellation, result size bounds, and Runner/API state and artifact readback. Document the callback contract and remote-execution limitations. Validation: 187 Snowflake tests passed; applicable pre-commit hooks passed. Mypy: no issues in 5 integration source files. Related to google#7014
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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):
2. Or, if no issue exists, describe the change:
Problem:
Snowflake Cortex Agents stream typed SSE events (status, thinking/text deltas, server-side tool use/result, citations, warnings, tables/charts, final response, thread metadata). Through Managed MCP +
McpToolsetthe whole run collapses into a singleFunctionResponse, so ADK applications cannot stream progressively, observe tool use, keep citations structured, or continue a Snowflake thread across turns. Applications also need ADK after-tool callbacks to postprocess server-side tool results and update session state or save artifacts.Solution:
SnowflakeCortexAgent(BaseAgent)undergoogle.adk.labs.snowflakecalls the Cortex Agents Run REST API directly (httpx, no new dependency), parses SSE incrementally, and yields ADK events following theAntigravityAgentprecedent: SSE-gated partial deltas,FunctionCall/FunctionResponseevents for server-side tool trace, one non-partial final event carrying the answer, namespacedcustom_metadata, and the thread cursor asstate_delta. Root-agent only in this PR; composition guards rejectsub_agentsand parent registration.Remote tool result callbacks
after_tool_callbackaccepts a synchronous or asynchronous function, or an ordered list, using theLlmAgentargument convention:tool,args,tool_context, andtool_response. ADK plugin after-tool callbacks run first in registration order, even when no agent callback is configured. The first non-None result, including{}, replaces the result and stops the applicable chain; a plugin replacement skips agent callbacks. ReturningNonecontinues the chain and retains in-place mutations.Callbacks receive the full normalized result before truncation, paired with the original call name and arguments by
tool_use_id. Duplicate results with the same ID invoke callbacks only once per invocation. Results without an identifiable prior call, including results arriving before their calls, remain visible but skip callbacks.ToolContextuses the real session and services, so callback state and artifact deltas accompany the resultingFunctionResponsethrough Runner persistence. Callback objects are excluded from serialization, repr, and web graph output.Callback result overrides and
ToolContextchanges affect ADK-side events, session state, artifacts, and application output. WithLlmAgent, an after-tool callback can replace a local tool result before the next ADK-controlled model request consumes it. Here, Snowflake owns the remote loop and may already have consumed the original result. These callbacks cannot retroactively change that result or Snowflake's final answer. Remote tool approval, blocking execution withbefore_tool_callback, client-side execution, and pause/resume are outside this scope. The adapter does not re-execute remote tools locally.The size limit is applied after callbacks, including to arbitrary replacement fields. Callback-authored state and artifacts have separate lifetimes and are not bounded by
max_tool_result_bytes. A callback exception or cancellation prevents that result and the final cursor update from being emitted; earlier events remain stored and artifacts already saved are not rolled back.Files:
src/google/adk/labs/snowflake/(_snowflake_cortex_agent.py,_client.py,_sse_parser.py,_event_converter.py,README.md)tests/unittests/labs/snowflake/contributing/samples/integrations/snowflake_cortex_agent/docs/guides/labs/snowflake/snowflake_cortex_agent/index.md(listed indocs/guides/README.md)Testing Plan
Unit Tests:
Coverage (mock SSE, no Snowflake access):
data:, non-JSON data,[DONE]/done, terminalerror, unknown events in order, buffer size limitssequence_number, thinking gating bystreaming_mode,tool_use/tool_resultcorrelation bytool_use_id, dedupe, result truncation atmax_tool_result_bytes,client_side_execute=true/permissionrejected, finalresponseaggregation, annotations/warnings/tables/charts/suggested queries metadata (including Cortex Analyst suggestion deltas assembled per index), unknown event passthroughhttpx.MockTransport): thread creation request/response and unusablethread_id; run request body (thread_id,parent_message_id,messages,stream),Accept: text/event-stream, URL-encoded object names; strict Snowflake id validation before any request; 401/403/429/5xx and non-event-stream answers raiseCortexApiErrorwith status, Snowflake code and request id (never the token); connect/read timeouts and dropped connections raiseCortexTransportError; leaving the stream early closes the upstream response; cancel is best effort; a sharedhttpx.AsyncClientis left openparent_message_id=0; second turn reuses the thread with the last assistant id; theRunnerpersists the cursor between turns; user metadata id is never stored; cursor unchanged on terminalerror, on a stream cut before the finalresponse(CortexTransportError), when the finalstatusis notcompleted, and when no assistant id was seen;[DONE]is optional once the finalresponsearrived; per-agent state keys; fingerprint mismatch and malformed cursors fail closed before any request, without quoting ids;sub_agentsandparent_agentrejected, also afterclone();header_providerabsent fromrepr,model_dumpand theadk webagent graph; SSE mode streams partial deltas,NONEyields only persisted events; tool trace recorded asFunctionCall/FunctionResponse; reading stops at[DONE]; disconnect closes the upstream response and cancels the run ({thread_id}-{user_message_id}) unlesscancel_on_disconnect=False; no cancel before the user message id is known (no run id yet) or after the run finished; a 409 from the cancel endpoint is swallowed;cleanup()leaves a shared client openSnowflakeCortexAgentand ADK Runner withhttpx.MockTransport): single/list and sync/async agent callbacks, ADK positional fallback, plugin registration order and plugin-before-agent execution, first non-None replacement including{}, unchanged results onNone, original name/arguments/ID, duplicate and out-of-order results, missing call metadata, state/artifact readback, two-turn SSE/NONE continuity, callback exceptions/cancellation without a successful result or cursor advancement, size bounds after replacement, serialization/graph exclusion, and no local tool re-execution./runand/run_sseroutes return callback-modified results consistent with session/artifact readback, including the existing SSE split into content and artifact-action frames.Verified on 2026-09-07 at commit
bcaba313. All 187 Snowflake tests passed in each environment. The fulltests/unittestssuite was also rerun sequentially for every supported Python version:Total: 70,422 passed, zero failures. All five tox environments returned exit code 0. Existing skip/xfail/xpass outcomes are included above; no test expectations or eval thresholds were relaxed.
Exact full-suite command (the result and log paths are local verification artifacts):
faulthandler_timeout=120enables diagnostic stack dumps for a long-running test; it does not change test selection or pass/fail expectations.Checks for the callback changes also passed:
Mypy reported no issues in the two changed source files, and all applicable pre-commit hooks passed.
Manual End-to-End (E2E) Tests:
Setup: a Snowflake account with a Cortex Agent object, a semantic view and an OAuth/PAT token. Sample:
contributing/samples/integrations/snowflake_cortex_agent.export SNOWFLAKE_ACCOUNT_URL=... SNOWFLAKE_DATABASE=... SNOWFLAKE_SCHEMA=... \ SNOWFLAKE_CORTEX_AGENT=... SNOWFLAKE_TOKEN=... adk web contributing/samples/integrationsRemote callback live verification — 2026-09-07
Verified at
bcaba313, using the realSnowflakeCortexAgentthrough ADK Runner with SSE streaming on Python 3.11.14. A uniquely identified user/session created a fresh Snowflake thread and continued it for a second turn. The Cortex Agent already had the server-siderequest_required_filterscustom tool registered; no Snowflake Agent, UDF, or permission configuration was changed.The local verification harness configured an observing plugin and an async agent
after_tool_callback. The callback checked the original tool name, arguments, andtool_use_id, parsed the JSON-string envelope intool_response['content'][*]['json']['result'], wrote a callback counter throughToolContext.state, saved a receipt artifact, and returned the result withadk_callback_verified=True. After each turn, the harness read the session and artifact back through their services and compared the stored events with Runner output.request_required_filtersrequest_required_filters0Sanitized completion record:
{"verified": true, "total_callbacks": 2, "target_envelopes": 2, "connections_closed": true}The harness used a 120-second request timeout, a 150-second per-turn timeout, and a 360-second process limit. HTTP connections and Runner resources were closed. Account details, credentials, keys, SQL, business payloads, answer text, and actual thread/session IDs are omitted from this evidence.
Exact live command (the credential-dependent harness and its output are local evaluation artifacts, not files shipped in this PR):
timeout 360s .venv/bin/python .local_eval/snowflake_cortex_agent/run_live_after_tool_callback.py > .local_eval/snowflake_cortex_agent/verification_20260907_full/live.jsonlTo reproduce the checks with another Cortex Agent, configure an equivalent callback and plugin on the sample agent, run two prompts that invoke a registered server-side tool through Runner, and compare callback counts, FunctionCall/FunctionResponse IDs, replacement output, session/artifact readback, and the second request's thread cursor. The nested envelope assertion above is specific to the tested custom tool; the adapter does not depend on that tool or its schema.
This live run verifies plugin-before-agent ordering and agent result replacement. Multiple-plugin ordering, plugin replacement short-circuiting, exceptions/cancellation, duplicate/out-of-order/missing events, oversized results, and NONE mode are covered by MockTransport tests using the real ADK Runner; these conditions were not forced on the remote server. Response-match scores and the existing eval PASS display were not used as callback-compatibility evidence.
Earlier adapter live verification — 2026-09-04
The following traces cover streaming, server-side tool projection, thread continuity, and cancellation behavior before after-tool callbacks were added.
Verified on 2026-09-04 against a real Snowflake account (Key Pair JWT auth,
X-Snowflake-Role/X-Snowflake-Warehouseheaders). Account host, object names, semantic view name, Snowflake ids, SQL and rows are masked.Run A, two turns in one ADK session through
Runnerwith SSE streaming:Run B, one turn where the Cortex Agent executed SQL and built a chart:
Observed: thread creation and two-turn continuity, cursor committed with the assistant message id only, streamed text equal to the final text, four server-side tool calls recorded as
FunctionCall/FunctionResponseand paired bytool_use_id, oversized tool result reduced to its key sizes,response.warning,response.table,response.chartandresponse.suggested_querieson the final event.Not exercised live:
response.text.annotation(the test agent has no Cortex Search).The cancel path was exercised after the client closed the SSE stream mid-run. Snowflake answered the follow-up cancel with
409 399528 Agent run was already completed, the same answer as for a finished run. A 409 alone does not show whether closing the stream ended the run or it finished on its own, so the adapter treats it as a benign best-effort outcome.run_idwas confirmed to be<thread_id>-<user_message_id>.adk evalover the same agent (2 cases, 3 turns) also completed every turn.Checklist
Additional context
AntigravityAgent(feat(labs): add experimental Antigravity SDK agent wrapper #6040) for external-runtime tool trace asFunctionCall/FunctionResponse, SSE-gated partials, per-agent state key.AgentToolevent propagation (Feature Request: Support Event Streaming propagation from AgentTool (Sub-agents) to Runner #3984). The adapter remains root-agent only; composition is outside this PR.NO_UNIT_GUIDE=Private transport, parser, and converter modules (
_client.py,_sse_parser.py,_event_converter.py) are documented through the SnowflakeCortexAgent unit guide atdocs/guides/labs/snowflake/snowflake_cortex_agent/index.md.