Kathy Wu
90d · built 2026-09-08
Performance
What Kathy Wu shipped in the selected window, measured in ETV, and how it compares with the 90 days before it.
Effective capacity
+4.3engineers
delivers like 5.3 (5.3x pre-AI)
Output (ETV)
15.4ETV
+133.1% vs 6.6 prior
Features share
15.6%
−20.5 pp vs prior window
Fixes share
26.0%
+16.1 pp vs prior window
Work mix
15.6% Features8.8% Maintenance47.5% Tests2.1% Docs26% Fixes
61 commits over 90 days, ending 2026-09-08.
Daily performance
Daily ETV, stacked by Features, Maintenance, Tests, Docs and Fixes.
Repository spread
Where this developer's commits land. Concentrated work (top1 > 80%) vs polymath spread (top1 < 30%).
| Repo | Commits | ETV |
|---|---|---|
| adk-python | 62 | 15.4 |
Most impactful commits
Top 10 by ETV in the last 90 days.
- 1.6ETVfeat(mcp): support MCP SDK 2.x alongside 1.x Widen the pin to `mcp>=1.24,<3` and make the MCP tooling run on either major. 2.0 renamed three symbols and two model fields; ADK already read those fields under both spellings, so what remained was the import surface, one timeout type, and the tests. The `dependencies` seam resolves the moved names once, at import: `McpError` became `MCPError`, and `mcp.server.fastmcp.FastMCP` became `mcp.server.mcpserver.MCPServer`. The other fourteen names ADK uses sit at the same path in both. The seam also picks the HTTP client library. 2.x is built against `httpx2` rather than `httpx`, and the client ADK constructs is passed to the SDK's transport, so the two must agree. The choice keys off the installed SDK, not off which library is importable -- a 1.x install can have `httpx2` present for unrelated reasons. That pairing costs one thing on 2.x. The OpenTelemetry httpx instrumentor is built against `httpx`; handed an `httpx2` client it wraps without complaint and then fails on the first request. MCP HTTP calls go untraced under 2.x rather than broken, until an `httpx2` instrumentor exists. `ClientSession(read_timeout_seconds=...)` is the one place the majors accept disjoint types: `timedelta` in 1.x, `float` in 2.x. The seam publishes which, and the single existing conversion point reads it. BREAKING CHANGE: under MCP SDK 2.x, fields a server sends that the SDK does not declare no longer reach the caller. 2.x closed its models, so an unknown key on a `CallToolResult`, or on a tool declaration read via `raw_mcp_tool`, is discarded during validation before ADK sees it. `_meta` is unaffected: it is a declared field and still arrives. Nothing in ADK can restore the others; a server that relies on vendor extensions should move them under `_meta`. On 1.x they still arrive as before. Also under 2.x: the HTTP client types on the public surface become `httpx2` ones. A caller passing its own `httpx_client_factory` or `mtls_transport` built on `httpx` hands the SDK's transport a client it cannot use. Those callers have to follow the SDK. On 1.x nothing changes. `McpToolset.read_resource` hands back the SDK's own content models rather than a dump, so their field spellings follow the resolved major: `mimeType` on 1.x, `mime_type` on 2.x. It is the same unnormalized escape hatch as `raw_mcp_tool`. ADK's own reader, `load_mcp_resource_tool`, takes both spellings; a caller reading the models directly has to as well. And 2.x retyped `Resource.uri` from `AnyUrl` to `str`, so the URI reaches callers as the server declared it -- `get_resource_info()["uri"]` is `file://report.json` where 1.x normalized it to `file://report.json/`. A caller comparing that string, or keying a cache on it, sees a different value. ADK feeds it straight back to `read_resource`, so its own round trip is unaffected. `McpTool` now puts a dumped tool in the MCP Apps widget payload rather than the model. The payload is a plain dict, so a model left in it was serialized by whichever sink wrote the event, and those disagree: the sinks passing `by_alias` published `inputSchema` while the session stores published `input_schema`. One dump means every sink sees the 1.x spellings. A consumer reading `payload["tool"]` gets a dict on both majors now; every serialized form of it already was one. The result dict is unchanged on both, and on 1.x that is now true by construction: the normalizing is gated on the major, so a 1.x install makes exactly the dump call ADK always made. It has to be gated. 1.x models are `extra="allow"`, so a vendor field a server sent arrives as an extra, and walking it would rename keys inside data ADK is only passing through -- and would drop a vendor field that happened to be called `resultType`. 2.x models are closed, so no extra can reach the dump and every key the walk sees is a declared field. On 2.x the dump goes by alias. 2.x renames the wire fields to snake_case -- `isError` and `structuredContent`, and `mimeType` on nested content -- and both majors alias every one of them to the 1.x camelCase spelling, so one aliased dump restores the whole tree rather than a key at a time. `meta` is put back afterwards, at every level: it aliases to `_meta` under both majors, so it is not a 2.x difference and must not follow, and sixty-odd models declare it, content blocks included. That walk stops at every entry holding server data rather than models -- a `_meta` payload, `structuredContent`, and the two schema fields, where a property may legally be called `_meta`. 2.x's `resultType` is dropped there too: 1.x has no such field, and acting on `input_required` is a feature rather than compatibility. `McpToolset.get_resource_info` returns a dumped `Resource`, which carries `mimeType`, so it needed the same treatment. All three dump sites share one gated helper now, so a fourth cannot be written without it. One more latent bug goes with all of this: `load_mcp_resource_tool` read `mimeType` unconditionally, and 2.x renamed it. The `except` around that read turned the `AttributeError` into placeholder text, so every binary resource looked undecodable. It reads both spellings now. Co-authored-by: Kathy Wu <wukathy@google.com> PiperOrigin-RevId: 975915236github.com-google-adk-python · 856acf21 · 2026-09-03
- 0.8ETVrefactor(types): type the integrations, skills and MCP tool packages for strict mypy Co-authored-by: Kathy Wu <wukathy@google.com> PiperOrigin-RevId: 961158237github.com-google-adk-python · 352d11d3 · 2026-08-07
- 0.8ETVfeat(mcp): report MCP HTTP exchanges as OpenTelemetry log records The MCP toolset captured HTTP request and response detail into `invocation_context.custom_metadata["http_debug_info"]`, an in-process buffer reachable only from agent callbacks. The same response hook now also emits an `adk.experimental.mcp.http.client.response.end` OTel log record per exchange, so the detail reaches whatever OTel backend is configured without any callback code. Reporting is off unless `ADK_EXPERIMENTAL_TELEMETRY` asks for it: the event is experimental, and recording an exchange can mean draining the response body. Attributes follow the OTel HTTP and MCP conventions. The session id and the negotiated protocol version are read off their headers and recorded as `mcp.session.id` and `mcp.protocol.version`. No header is recorded that `OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST` or `..._RESPONSE` does not name, the same env vars `opentelemetry-instrumentation-httpx` reads. Redaction runs first, so allowlisting a credential header yields the marker, not the secret. Bodies go in the record body under `http.{request,response}.body.content` and appear only when `ADK_CAPTURE_MCP_HTTP_BODIES` is set, since MCP payloads carry user data. `custom_metadata["http_debug_info"]` is unchanged, and keeps its own DEBUG gate. Also hardened on both sinks: `proxy-authorization`, `x-api-key` and `api-key` join the redacted header set; URL userinfo is stripped; and the SSE content-type check is case-insensitive. Co-authored-by: Kathy Wu <wukathy@google.com> PiperOrigin-RevId: 970667020github.com-google-adk-python · 85a6fa1e · 2026-08-25
- 0.8ETVfix: Prevent continuation forgery in tool confirmation An attacker who could manipulate or inject events into the session history could execute unauthorized tools by forging a tool confirmation response. This fixes the vulnerability by: - When resolving confirmation targets, the processor verifies if the tool is registered in the executing agent's tools_dict - Validate that the tool actually requires confirmation, supporting both static definitions and dynamic confirmation requests - Verify that the original tool call event exists in the session history with the matching ID, and that its name and arguments match the confirmation request's originalFunctionCall exactly to prevent argument tampering. Co-authored-by: Kathy Wu <wukathy@google.com> PiperOrigin-RevId: 949112553github.com-google-adk-python · c03f3337 · 2026-07-16
- 0.7ETVfeat: support auth_scheme and auth_credential in RemoteA2aAgent `RemoteA2aAgent` could not authenticate its calls; a caller had to bake a static token into a custom `httpx_client`. Every other ADK component with a remote endpoint takes an `auth_scheme`/`auth_credential` pair. Accept that pair and an optional `credential_key`. `CredentialManager` resolves the credential once per invocation, and the headers go on the card fetch and the message send; with nothing to send, the agent emits `adk_request_credential`. The interceptors and the derived key are per agent, so one agent's token cannot reach another agent's host. `build_auth_headers` also stops sending `Bearer None` for a tokenless OAuth2 credential. The interactive round trip needs an `LlmAgent` parent; the `AgentRegistry` path resolves server-side. Co-authored-by: Kathy Wu <wukathy@google.com> PiperOrigin-RevId: 968025847github.com-google-adk-python · d42c634b · 2026-08-20
- 0.7ETVfeat: HTTP traces for MCP server requests/errors Co-authored-by: Kathy Wu <wukathy@google.com> PiperOrigin-RevId: 938200678github.com-google-adk-python · 4c4f77ae · 2026-06-25
- 0.7ETVfix: guard reads in the local API server against DNS rebinding _OriginCheckMiddleware returned early for GET/HEAD/OPTIONS, and again whenever a request carried no Origin, so only writes were validated. Every read endpoint of `adk web` / `adk api_server` was served to a page that reached the server by rebinding a hostname to 127.0.0.1. Origin cannot close that, because browsers omit it on requests they consider same-origin, as they do a rebound page's. So check Host on every request: a loopback bind is reachable only from this machine, so a request naming any other host was pointed here by rebound DNS. Only the real Host header counts, since it is a forbidden request header whereas a same-origin fetch() can set X-Forwarded-Host or Forwarded freely. The safe-method exemption is gone, and the /run_live handshake gets the same check. Serving another hostname from a loopback bind now means naming it in --allow_origins, which vouches for that origin's host rather than switching the guard off; only "*" opts out entirely. Both checks key off the bind address instead of scope["server"] - ASGI servers fill that from the accepted socket, so a --host=0.0.0.0 server reports 127.0.0.1 for any loopback connection and looked local-only behind a same-machine proxy. The bind arrives through a new get_fast_api_app(bind_host=...) that the CLI passes. The existing host parameter keeps its 127.0.0.1 default and its meaning: an embedder that binds elsewhere without passing it would otherwise have the guard keyed to a loopback bind it does not have, and reject all of its own traffic. Co-authored-by: Kathy Wu <wukathy@google.com> PiperOrigin-RevId: 964277197github.com-google-adk-python · 2cf4fd1d · 2026-08-13
- 0.6ETVfeat: Migrate McpToolset to AsyncAuthorizedSession for mTLS support Use Google's official async client to support mTLS for GCP API calls. This resolves the issue where we were failing mTLS policy due to unbound tokens. Note: This CL also removes the legacy bound-token patch from mcp_tool.py and mcp_toolset.py, which constitutes a behavior change beyond the mTLS feature. Co-authored-by: Kathy Wu <wukathy@google.com> PiperOrigin-RevId: 931326860github.com-google-adk-python · fe497a4f · 2026-06-12
- 0.6ETVfix: fence relayed agent output so it cannot pose as instructions When one agent hands off to another, `_present_other_agent_message` replays the first agent's turn to the second as a `role="user"` message -- the same channel the real user speaks on -- interpolating the text straight into `[agent] said: ...`. Nothing marks where the quoted transcript ends, so a payload the first agent was talked into emitting reads to the second agent as a fresh directive. Anyone who can chat to a low-privilege front-end agent can therefore aim instructions at whatever tools the agent it transfers to holds. Every relayed payload -- text, thoughts, tool arguments, tool results -- is now quoted between explicit markers, and the leading part of the message states that what sits between them is data to read and not instructions to follow. Markers occurring inside a payload are elided first, so quoted content cannot close its own block and carry on speaking as the framework. The markers, the preamble and the quoting helpers live in `flows/llm_flows/_fencing.py`. The unit tests and the conformance harness both have to spell the expected framing, so it sits in a module of its own rather than inside `contents.py`, where they would have to reach for private names. This raises the bar rather than closing the class: a model can still be talked round by text it was told to distrust. What it removes is the structural ambiguity that made a relayed payload indistinguishable from a user turn. Relayed turns now cost the preamble plus two marker lines per part, and anything matching on the old `For context: [x] said: y` shape needs updating. The conformance replay harness is one such matcher, and now reduces a relayed turn to the payload it carries before comparing, so recordings cut before the fencing still replay. Co-authored-by: Kathy Wu <wukathy@google.com> PiperOrigin-RevId: 966694665github.com-google-adk-python · 9ffe8be6 · 2026-08-18
- 0.5ETVfeat: let McpToolset reuse the MCP server's tool list `get_tools()` sends a `tools/list` request every time it runs. `BaseToolset` memoizes the result for the rest of an invocation, so repeated LLM steps within one turn share a listing, but that cache is keyed on the invocation ID and lives on the toolset instance: every new turn pays the round trip again, an agent that hands each sub-agent its own `McpToolset` pays it once per sub-agent, and anything wrapped in `AgentTool` gets a fresh invocation ID per call and so never hits the cache. Listing happens during agent setup, so the cost lands on every chat request. Add an opt-in `tool_list_cache_ttl_seconds` that reuses the response for that long. Entries are keyed by the session pool key, so they never outlive the identity they were fetched with: a `header_provider` that distinguishes tenants gets an entry per tenant, and one that mints a fresh value per request gets no reuse, which is also the case where the session pool already thrashes. Only the round trip is skipped. Tools are rebuilt and `tool_filter` re-evaluated on every call, so a context-dependent filter keeps deciding per call. The cache is bounded rather than left to the TTL. A read only evicts the key it was asked for, so the per-request-header case above would otherwise accumulate an entry per call for the life of the toolset. Each write sweeps whatever has expired and then caps the cache at 64 entries, evicting least-recently-used, so the footprint holds even when every key is still inside its TTL. Reuse stays off by default. ADK does not subscribe to `notifications/tools/list_changed`, so a server that adds or removes a tool goes unnoticed until the entry expires, and the TTL is how a caller says how stale a tool list may get. Co-authored-by: Kathy Wu <wukathy@google.com> PiperOrigin-RevId: 962353730github.com-google-adk-python · 9cd59753 · 2026-08-10