Kathy Wu
90d · built 2026-09-30
Performance
What Kathy Wu shipped in the selected window, measured in ETV, and how it compares with the 90 days before it.
Effective capacity
+5.7engineers
delivers like 6.7 (6.7x pre-AI)
Output (ETV)
19.3ETV
+180.1% vs 6.9 prior
Features share
14.8%
−20.8 pp vs prior window
Fixes share
25.5%
+11.3 pp vs prior window
Work mix
14.8% Features7.3% Maintenance50.3% Tests2.1% Docs25.5% Fixes
67 commits over 90 days, ending 2026-09-30.
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 | 66 | 19.3 |
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
- 1.2ETVfix: resolve Agent Registry MCP bindings for the deployed agent at runtime `AgentRegistry.get_mcp_toolset()` without an `auth_scheme` took the auth provider from the first binding on the first page of ListBindings whose target matched the MCP server. ListBindings covers the whole project, so the choice ignored which agent a binding was made for, and a stale binding could win over the current one. The lookup also ran where the toolset was built, usually the machine deploying the agent, so its result was frozen into the deployed agent. - In Agent Engine, the toolset resolves its binding again the first time it authenticates. It finds the serving Reasoning Engine's agent with ListAgents, then calls FetchAvailableBindings for that agent. That returns only bindings made for this agent or for no agent in particular, and drops auth providers the agent cannot retrieve credentials from. A binding made for this agent wins. If no binding is available, the toolset stops using an auth provider, so deleting a binding revokes it. If the lookup fails, the binding chosen at deploy time stays. - ListBindings still picks the binding when the toolset is built. It now reads every page, and when a resource is bound to several auth providers it uses the most recently updated binding and logs a warning naming them. The runtime lookup runs as the deployed agent and needs `agentregistry.agents.list` and `agentregistry.bindings.fetchAvailable` (both in `roles/agentregistry.viewer`). Co-authored-by: Kathy Wu <wukathy@google.com> PiperOrigin-RevId: 987806020github.com-google-adk-python · 9115d612 · 2026-09-24
- 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.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: let SkillToolset notice that a loaded skill has changed A session can outlive several releases of the skills it loaded. What the model was told is fixed in the transcript, so a skill edited since goes on being followed as it was written then, and its scripts in the sandbox stay the old ones. `SkillLifecycleConfig(revalidate_skills=True)` records a digest of each skill as it is loaded -- instructions, frontmatter and every resource -- and compares it against the current definition on each request. A skill that no longer matches is re-stated in the request, and its resources are rewritten into the environment. Off by default: it costs a registry lookup per active skill per turn, and re-stating a skill invalidates the prompt cache. Co-authored-by: Kathy Wu <wukathy@google.com> PiperOrigin-RevId: 985596282github.com-google-adk-python · f848b7e3 · 2026-09-21
- 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.6ETVfeat: add an opt-in unload_skill tool to SkillToolset load_skill appends to _adk_activated_skill_<agent> and nothing ever removes it, so a skill's adk_additional_tools stay declared on every later request for the rest of the session. unload_skill drops the name from that list, releasing those tools. It reads and writes session state only, so it also works for a skill that has since left the registry. Co-authored-by: Kathy Wu <wukathy@google.com> PiperOrigin-RevId: 982662378github.com-google-adk-python · ce53a36c · 2026-09-16