From 2c6d655af80d771bbec28ca54a0665fc33054635 Mon Sep 17 00:00:00 2001 From: Marco Gancitano Date: Tue, 1 Sep 2026 10:31:09 -0400 Subject: [PATCH 1/3] feat: add google-adk node-weather ai-observability fixture Co-Authored-By: Claude Fable 5 --- apps/ai-observability/README.md | 5 +- .../google-adk/node-weather/README.md | 37 +++++++++++++ .../google-adk/node-weather/package.json | 19 +++++++ .../google-adk/node-weather/src/index.ts | 53 +++++++++++++++++++ .../google-adk/node-weather/src/weather.ts | 9 ++++ .../google-adk/node-weather/tsconfig.json | 12 +++++ 6 files changed, 133 insertions(+), 2 deletions(-) create mode 100644 apps/ai-observability/google-adk/node-weather/README.md create mode 100644 apps/ai-observability/google-adk/node-weather/package.json create mode 100644 apps/ai-observability/google-adk/node-weather/src/index.ts create mode 100644 apps/ai-observability/google-adk/node-weather/src/weather.ts create mode 100644 apps/ai-observability/google-adk/node-weather/tsconfig.json diff --git a/apps/ai-observability/README.md b/apps/ai-observability/README.md index d2da41c8d..f7d70f252 100644 --- a/apps/ai-observability/README.md +++ b/apps/ai-observability/README.md @@ -24,8 +24,9 @@ Each app exists to test one thing the others don't: - `openai-agents/python-travel-triage` — tracing processor; the SDK emits the tree - `vercel-ai/nextjs-support-chat` — per-request identity; framework bootstrap - `manual-capture/node-http-chat` — hand-built tree; must reuse the existing client +- `google-adk/node-weather` — framework plugin; identity comes from ADK's own ids -The four weather apps implement the identical `get_weather` round trip from +The five weather apps implement the identical `get_weather` round trip from [Anthropic's tool-use docs](https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview#how-tool-use-works), so a diff between any two isolates one variable: the SDK, the language, or the conversation structure. @@ -37,7 +38,7 @@ conversation structure. `posthog_trace_id`, and `posthog_properties`, as shown in the docs. OTel is acceptable only where the same structure lands. Exceptions: `openai-agents` (tracing processor), `vercel-ai` (`experimental_telemetry`), - `manual-capture` (no SDK to wrap). + `google-adk` (Runner plugin), `manual-capture` (no SDK to wrap). - **Every app gets a session** — single-trace apps included. The graded property is **cardinality**: one id shared by the traces that belong together. A fresh id per call groups nothing and is worse than none. diff --git a/apps/ai-observability/google-adk/node-weather/README.md b/apps/ai-observability/google-adk/node-weather/README.md new file mode 100644 index 000000000..bfd9fb597 --- /dev/null +++ b/apps/ai-observability/google-adk/node-weather/README.md @@ -0,0 +1,37 @@ +# wb-aio-google-adk-node-weather + +Weather assistant, Google Agent Development Kit (`@google/adk`), two-turn +conversation with a registered tool. PostHog-less fixture for +`wizard ai-observability`. + +The expected mechanism is `PostHogADKPlugin` from `@posthog/ai/adk`, added to +the `Runner`'s `plugins`. The plugin captures one `$ai_generation` per model +call and takes identity from ADK itself: the run's `userId` becomes the +distinct ID, the ADK `sessionId` becomes `$ai_session_id`, and each invocation +becomes a trace. No per-call PostHog parameters exist to add. + +``` +thread_abc ← $ai_session_id (ADK sessionId) +├─ ask("weather in San Francisco?") ← trace (invocation) +│ ├─ model call (→ get_weather call) ← generation +│ └─ model call (→ answer) ← generation +└─ ask("How about Boston?") ← trace (same shape) +``` + +ADK runs the tool loop itself, and the plugin does not emit `$ai_span` events +for tool runs; the tool call is visible in the first generation's output. Do +not grade a missing tool span as a failure here. + +## Expected outcome + +- one session (`thread_abc`), two traces of `generation → generation`, all on + `user_123` +- `PostHogADKPlugin` in the `Runner`'s `plugins`; no wrapper client swapped in +- identity left to the plugin's ADK fallbacks, or wired explicitly to the same + `userId` / `sessionId` values; either way one session across both turns +- flushed before exit (`posthog.shutdown()`) +- `weather.ts`, the tool registration, and the agent untouched + +Fail: swapping the model for a wrapped Gemini client (loses the agent +structure); a session or trace id minted per model call; hand-authored spans +around the tool; no flush. diff --git a/apps/ai-observability/google-adk/node-weather/package.json b/apps/ai-observability/google-adk/node-weather/package.json new file mode 100644 index 000000000..00453ed3b --- /dev/null +++ b/apps/ai-observability/google-adk/node-weather/package.json @@ -0,0 +1,19 @@ +{ + "name": "wb-aio-google-adk-node-weather", + "version": "0.0.0", + "private": true, + "type": "module", + "description": "PostHog-less weather assistant (Google ADK, tool use) for testing `wizard ai-observability`.", + "scripts": { + "build": "tsc --noEmit", + "start": "tsx src/index.ts" + }, + "dependencies": { + "@google/adk": "^2.0.0", + "zod": "^4.2.1" + }, + "devDependencies": { + "tsx": "^4.19.2", + "typescript": "^5.6.3" + } +} diff --git a/apps/ai-observability/google-adk/node-weather/src/index.ts b/apps/ai-observability/google-adk/node-weather/src/index.ts new file mode 100644 index 000000000..cafeb77ff --- /dev/null +++ b/apps/ai-observability/google-adk/node-weather/src/index.ts @@ -0,0 +1,53 @@ +import { FunctionTool, InMemorySessionService, LlmAgent, Runner } from '@google/adk' +import { z } from 'zod' + +import { getWeather } from './weather.js' + +const APP_NAME = 'wb-aio-google-adk-node-weather' +const USER_ID = 'user_123' +const SESSION_ID = 'thread_abc' + +const weatherTool = new FunctionTool({ + name: 'get_weather', + description: 'Get the current weather for a given location.', + parameters: z.object({ + location: z.string().describe('City and state, e.g. San Francisco, CA'), + }), + execute: ({ location }) => getWeather(location), +}) + +const agent = new LlmAgent({ + name: 'weather_assistant', + model: 'gemini-2.5-flash', + instruction: 'Answer weather questions with the get_weather tool. Be concise.', + tools: [weatherTool], +}) + +const sessionService = new InMemorySessionService() +const runner = new Runner({ appName: APP_NAME, agent, sessionService }) + +/** Answer one question inside the shared session. ADK runs the tool loop itself. */ +async function ask(question: string): Promise { + for await (const event of runner.runAsync({ + userId: USER_ID, + sessionId: SESSION_ID, + newMessage: { role: 'user', parts: [{ text: question }] }, + })) { + for (const part of event.content?.parts ?? []) { + if (part.text) { + console.log(part.text) + } + } + } +} + +async function main(): Promise { + await sessionService.createSession({ appName: APP_NAME, userId: USER_ID, sessionId: SESSION_ID }) + await ask("What's the weather in San Francisco?") + await ask('How about Boston?') +} + +main().catch((err) => { + console.error(`fatal: ${String(err)}`) + process.exit(1) +}) diff --git a/apps/ai-observability/google-adk/node-weather/src/weather.ts b/apps/ai-observability/google-adk/node-weather/src/weather.ts new file mode 100644 index 000000000..08a08286c --- /dev/null +++ b/apps/ai-observability/google-adk/node-weather/src/weather.ts @@ -0,0 +1,9 @@ +// Backing implementation for the get_weather tool. No model call involved. +const forecast: Record = { + 'San Francisco, CA': '15 degrees Celsius, partly cloudy', + 'Boston, MA': '4 degrees Celsius, snow showers', +} + +export function getWeather(location: string): string { + return forecast[location] ?? `No forecast on file for ${location}.` +} diff --git a/apps/ai-observability/google-adk/node-weather/tsconfig.json b/apps/ai-observability/google-adk/node-weather/tsconfig.json new file mode 100644 index 000000000..235d77827 --- /dev/null +++ b/apps/ai-observability/google-adk/node-weather/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "noEmit": true + }, + "include": ["src"] +} From eef6493c4a1003b7ab701114b1b44ecfc978ffeb Mon Sep 17 00:00:00 2001 From: Marco Gancitano Date: Tue, 1 Sep 2026 10:32:10 -0400 Subject: [PATCH 2/3] chore: add fixture env file with the shared demo project values Same demo values as the sibling ai-observability fixtures. Co-Authored-By: Claude Fable 5 --- apps/ai-observability/google-adk/node-weather/.env | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 apps/ai-observability/google-adk/node-weather/.env diff --git a/apps/ai-observability/google-adk/node-weather/.env b/apps/ai-observability/google-adk/node-weather/.env new file mode 100644 index 000000000..f72994146 --- /dev/null +++ b/apps/ai-observability/google-adk/node-weather/.env @@ -0,0 +1,2 @@ +POSTHOG_API_KEY=phc_VMTfZD5shhF3SQfgXeu6SW85FTxDMnmB4JpRbUj9QEA +POSTHOG_HOST=https://us.i.posthog.com From f54450583909bcd9b232c9efad2fa158b85f67d0 Mon Sep 17 00:00:00 2001 From: Marco Gancitano Date: Wed, 2 Sep 2026 14:50:48 -0400 Subject: [PATCH 3/3] fix: grade the merged adk plugin's full trace hierarchy The merged PostHogADKPlugin captures $ai_trace and agent/tool $ai_span events, not just generations; the README now expects that tree. Model bumped to gemini-3.6-flash to match the posthog-js example. Co-Authored-By: Claude Fable 5 --- .../google-adk/node-weather/README.md | 30 ++++++++++--------- .../google-adk/node-weather/src/index.ts | 2 +- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/apps/ai-observability/google-adk/node-weather/README.md b/apps/ai-observability/google-adk/node-weather/README.md index bfd9fb597..b7c8a12a4 100644 --- a/apps/ai-observability/google-adk/node-weather/README.md +++ b/apps/ai-observability/google-adk/node-weather/README.md @@ -5,33 +5,35 @@ conversation with a registered tool. PostHog-less fixture for `wizard ai-observability`. The expected mechanism is `PostHogADKPlugin` from `@posthog/ai/adk`, added to -the `Runner`'s `plugins`. The plugin captures one `$ai_generation` per model -call and takes identity from ADK itself: the run's `userId` becomes the -distinct ID, the ADK `sessionId` becomes `$ai_session_id`, and each invocation -becomes a trace. No per-call PostHog parameters exist to add. +the `Runner`'s `plugins`. The plugin hooks the run, agent, tool, and model +callbacks and captures the whole tree itself: an `$ai_trace` per invocation, +`$ai_span` events for agent runs and tool calls, and one `$ai_generation` per +model call. Identity comes from ADK too: the run's `userId` becomes the +distinct ID, the ADK `sessionId` becomes `$ai_session_id`. No per-call +PostHog parameters exist to add. ``` thread_abc ← $ai_session_id (ADK sessionId) ├─ ask("weather in San Francisco?") ← trace (invocation) -│ ├─ model call (→ get_weather call) ← generation -│ └─ model call (→ answer) ← generation +│ └─ weather_assistant ← span (agent run) +│ ├─ model call (→ get_weather call) ← generation +│ ├─ get_weather ← span (tool) +│ └─ model call (→ answer) ← generation └─ ask("How about Boston?") ← trace (same shape) ``` -ADK runs the tool loop itself, and the plugin does not emit `$ai_span` events -for tool runs; the tool call is visible in the first generation's output. Do -not grade a missing tool span as a failure here. - ## Expected outcome -- one session (`thread_abc`), two traces of `generation → generation`, all on - `user_123` +- one session (`thread_abc`), two traces of + `agent span → generation → span(get_weather) → generation`, all on `user_123` - `PostHogADKPlugin` in the `Runner`'s `plugins`; no wrapper client swapped in +- the agent and tool spans come from the plugin's callbacks, not hand-authored + `$ai_span` capture around `getWeather` - identity left to the plugin's ADK fallbacks, or wired explicitly to the same `userId` / `sessionId` values; either way one session across both turns - flushed before exit (`posthog.shutdown()`) - `weather.ts`, the tool registration, and the agent untouched Fail: swapping the model for a wrapped Gemini client (loses the agent -structure); a session or trace id minted per model call; hand-authored spans -around the tool; no flush. +structure); hand-authored spans duplicating what the plugin emits; a session +or trace id minted per model call; no flush. diff --git a/apps/ai-observability/google-adk/node-weather/src/index.ts b/apps/ai-observability/google-adk/node-weather/src/index.ts index cafeb77ff..299321755 100644 --- a/apps/ai-observability/google-adk/node-weather/src/index.ts +++ b/apps/ai-observability/google-adk/node-weather/src/index.ts @@ -18,7 +18,7 @@ const weatherTool = new FunctionTool({ const agent = new LlmAgent({ name: 'weather_assistant', - model: 'gemini-2.5-flash', + model: 'gemini-3.6-flash', instruction: 'Answer weather questions with the get_weather tool. Be concise.', tools: [weatherTool], })