diff --git a/e2e/scenarios/google-adk-instrumentation/__snapshots__/google-adk-v061.log-payloads.json b/e2e/scenarios/google-adk-instrumentation/__snapshots__/google-adk-v061.log-payloads.json index 406961eca..8fa705526 100644 --- a/e2e/scenarios/google-adk-instrumentation/__snapshots__/google-adk-v061.log-payloads.json +++ b/e2e/scenarios/google-adk-instrumentation/__snapshots__/google-adk-v061.log-payloads.json @@ -56,7 +56,7 @@ "location": "Paris, France" }, "metadata": { - "google_adk.tool_call_id": "adk-", + "google_adk.tool_call_id": "direct-weather-tool", "google_adk.tool_name": "get_weather", "provider": "google-adk" }, @@ -102,12 +102,9 @@ "provider": "google-adk" }, "metrics": { - "completion_tokens": "", "duration": 0, "end": 0, - "prompt_tokens": "", - "start": 0, - "tokens": "" + "start": 0 }, "name": "Google ADK Runner", "type": "task" @@ -123,6 +120,80 @@ "name": "adk-simple-run-operation", "type": null }, + { + "metadata": { + "operation": "sequential-run" + }, + "metrics": { + "end": 0, + "start": 0 + }, + "name": "adk-sequential-run-operation", + "type": null + }, + { + "input": { + "messages": [ + { + "content": "Hello.", + "role": "user" + } + ] + }, + "metadata": { + "google_adk.session_id": "test-session-sequential", + "google_adk.user_id": "test-user", + "provider": "google-adk" + }, + "metrics": { + "duration": 0, + "end": 0, + "start": 0 + }, + "name": "Google ADK Runner", + "type": "task" + }, + { + "metadata": { + "google_adk.agent_name": "sequential_workflow", + "provider": "google-adk" + }, + "metrics": { + "duration": 0, + "end": 0, + "start": 0 + }, + "name": "Agent: sequential_workflow", + "type": "task" + }, + { + "metadata": { + "google_adk.agent_name": "greeter", + "model": "gemini-2.5-flash-lite", + "provider": "google-adk" + }, + "metrics": { + "duration": 0, + "end": 0, + "start": 0 + }, + "name": "Agent: greeter", + "type": "task" + }, + { + "metadata": { + "google_adk.agent_name": "farewell", + "model": "gemini-2.5-flash-lite", + "provider": "google-adk" + }, + "metrics": { + "duration": 0, + "end": 0, + "start": 0 + }, + "name": "Agent: farewell", + "type": "task" + }, { "metadata": { "scenario": "google-adk-instrumentation" diff --git a/e2e/scenarios/google-adk-instrumentation/__snapshots__/google-adk-v061.span-events.json b/e2e/scenarios/google-adk-instrumentation/__snapshots__/google-adk-v061.span-events.json index 2bf665c39..dcebc1a19 100644 --- a/e2e/scenarios/google-adk-instrumentation/__snapshots__/google-adk-v061.span-events.json +++ b/e2e/scenarios/google-adk-instrumentation/__snapshots__/google-adk-v061.span-events.json @@ -63,7 +63,7 @@ "has_input": true, "has_output": true, "metadata": { - "google_adk.tool_call_id": "adk-", + "google_adk.tool_call_id": "direct-weather-tool", "google_adk.tool_name": "get_weather", "provider": "google-adk" }, @@ -104,10 +104,7 @@ "provider": "google-adk" }, "metric_keys": [ - "completion_tokens", - "duration", - "prompt_tokens", - "tokens" + "duration" ], "name": "Google ADK Runner", "root_span_id": "", @@ -116,5 +113,91 @@ "" ], "type": "task" + }, + { + "has_input": false, + "has_output": false, + "metadata": { + "operation": "sequential-run" + }, + "metric_keys": [], + "name": "adk-sequential-run-operation", + "root_span_id": "", + "span_id": "", + "span_parents": [ + "" + ], + "type": null + }, + { + "has_input": true, + "metadata": { + "google_adk.session_id": "test-session-sequential", + "google_adk.user_id": "test-user", + "provider": "google-adk" + }, + "metric_keys": [ + "duration" + ], + "name": "Google ADK Runner", + "root_span_id": "", + "span_id": "", + "span_parents": [ + "" + ], + "type": "task" + }, + { + "has_input": false, + "metadata": { + "google_adk.agent_name": "sequential_workflow", + "provider": "google-adk" + }, + "metric_keys": [ + "duration" + ], + "name": "Agent: sequential_workflow", + "root_span_id": "", + "span_id": "", + "span_parents": [ + "" + ], + "type": "task" + }, + { + "has_input": false, + "metadata": { + "google_adk.agent_name": "greeter", + "model": "gemini-2.5-flash-lite", + "provider": "google-adk" + }, + "metric_keys": [ + "duration" + ], + "name": "Agent: greeter", + "root_span_id": "", + "span_id": "", + "span_parents": [ + "" + ], + "type": "task" + }, + { + "has_input": false, + "metadata": { + "google_adk.agent_name": "farewell", + "model": "gemini-2.5-flash-lite", + "provider": "google-adk" + }, + "metric_keys": [ + "duration" + ], + "name": "Agent: farewell", + "root_span_id": "", + "span_id": "", + "span_parents": [ + "" + ], + "type": "task" } ] diff --git a/e2e/scenarios/google-adk-instrumentation/__snapshots__/google-adk-v1000.log-payloads.json b/e2e/scenarios/google-adk-instrumentation/__snapshots__/google-adk-v1000.log-payloads.json index 406961eca..8fa705526 100644 --- a/e2e/scenarios/google-adk-instrumentation/__snapshots__/google-adk-v1000.log-payloads.json +++ b/e2e/scenarios/google-adk-instrumentation/__snapshots__/google-adk-v1000.log-payloads.json @@ -56,7 +56,7 @@ "location": "Paris, France" }, "metadata": { - "google_adk.tool_call_id": "adk-", + "google_adk.tool_call_id": "direct-weather-tool", "google_adk.tool_name": "get_weather", "provider": "google-adk" }, @@ -102,12 +102,9 @@ "provider": "google-adk" }, "metrics": { - "completion_tokens": "", "duration": 0, "end": 0, - "prompt_tokens": "", - "start": 0, - "tokens": "" + "start": 0 }, "name": "Google ADK Runner", "type": "task" @@ -123,6 +120,80 @@ "name": "adk-simple-run-operation", "type": null }, + { + "metadata": { + "operation": "sequential-run" + }, + "metrics": { + "end": 0, + "start": 0 + }, + "name": "adk-sequential-run-operation", + "type": null + }, + { + "input": { + "messages": [ + { + "content": "Hello.", + "role": "user" + } + ] + }, + "metadata": { + "google_adk.session_id": "test-session-sequential", + "google_adk.user_id": "test-user", + "provider": "google-adk" + }, + "metrics": { + "duration": 0, + "end": 0, + "start": 0 + }, + "name": "Google ADK Runner", + "type": "task" + }, + { + "metadata": { + "google_adk.agent_name": "sequential_workflow", + "provider": "google-adk" + }, + "metrics": { + "duration": 0, + "end": 0, + "start": 0 + }, + "name": "Agent: sequential_workflow", + "type": "task" + }, + { + "metadata": { + "google_adk.agent_name": "greeter", + "model": "gemini-2.5-flash-lite", + "provider": "google-adk" + }, + "metrics": { + "duration": 0, + "end": 0, + "start": 0 + }, + "name": "Agent: greeter", + "type": "task" + }, + { + "metadata": { + "google_adk.agent_name": "farewell", + "model": "gemini-2.5-flash-lite", + "provider": "google-adk" + }, + "metrics": { + "duration": 0, + "end": 0, + "start": 0 + }, + "name": "Agent: farewell", + "type": "task" + }, { "metadata": { "scenario": "google-adk-instrumentation" diff --git a/e2e/scenarios/google-adk-instrumentation/__snapshots__/google-adk-v1000.span-events.json b/e2e/scenarios/google-adk-instrumentation/__snapshots__/google-adk-v1000.span-events.json index 2bf665c39..dcebc1a19 100644 --- a/e2e/scenarios/google-adk-instrumentation/__snapshots__/google-adk-v1000.span-events.json +++ b/e2e/scenarios/google-adk-instrumentation/__snapshots__/google-adk-v1000.span-events.json @@ -63,7 +63,7 @@ "has_input": true, "has_output": true, "metadata": { - "google_adk.tool_call_id": "adk-", + "google_adk.tool_call_id": "direct-weather-tool", "google_adk.tool_name": "get_weather", "provider": "google-adk" }, @@ -104,10 +104,7 @@ "provider": "google-adk" }, "metric_keys": [ - "completion_tokens", - "duration", - "prompt_tokens", - "tokens" + "duration" ], "name": "Google ADK Runner", "root_span_id": "", @@ -116,5 +113,91 @@ "" ], "type": "task" + }, + { + "has_input": false, + "has_output": false, + "metadata": { + "operation": "sequential-run" + }, + "metric_keys": [], + "name": "adk-sequential-run-operation", + "root_span_id": "", + "span_id": "", + "span_parents": [ + "" + ], + "type": null + }, + { + "has_input": true, + "metadata": { + "google_adk.session_id": "test-session-sequential", + "google_adk.user_id": "test-user", + "provider": "google-adk" + }, + "metric_keys": [ + "duration" + ], + "name": "Google ADK Runner", + "root_span_id": "", + "span_id": "", + "span_parents": [ + "" + ], + "type": "task" + }, + { + "has_input": false, + "metadata": { + "google_adk.agent_name": "sequential_workflow", + "provider": "google-adk" + }, + "metric_keys": [ + "duration" + ], + "name": "Agent: sequential_workflow", + "root_span_id": "", + "span_id": "", + "span_parents": [ + "" + ], + "type": "task" + }, + { + "has_input": false, + "metadata": { + "google_adk.agent_name": "greeter", + "model": "gemini-2.5-flash-lite", + "provider": "google-adk" + }, + "metric_keys": [ + "duration" + ], + "name": "Agent: greeter", + "root_span_id": "", + "span_id": "", + "span_parents": [ + "" + ], + "type": "task" + }, + { + "has_input": false, + "metadata": { + "google_adk.agent_name": "farewell", + "model": "gemini-2.5-flash-lite", + "provider": "google-adk" + }, + "metric_keys": [ + "duration" + ], + "name": "Agent: farewell", + "root_span_id": "", + "span_id": "", + "span_parents": [ + "" + ], + "type": "task" } ] diff --git a/e2e/scenarios/google-adk-instrumentation/assertions.ts b/e2e/scenarios/google-adk-instrumentation/assertions.ts index 1e6d4c2b5..267d91a34 100644 --- a/e2e/scenarios/google-adk-instrumentation/assertions.ts +++ b/e2e/scenarios/google-adk-instrumentation/assertions.ts @@ -27,6 +27,14 @@ type RunGoogleADKScenario = (harness: { }) => Promise; }) => Promise; +const VOLATILE_ADK_METRIC_KEYS = new Set([ + "completion_reasoning_tokens", + "completion_tokens", + "prompt_cached_tokens", + "prompt_tokens", + "tokens", +]); + function isRecord(value: Json | undefined): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } @@ -71,7 +79,9 @@ function normalizeADKMetrics(metrics: Json): Json { } const normalized = structuredClone(metrics); - delete normalized.prompt_cached_tokens; + for (const key of VOLATILE_ADK_METRIC_KEYS) { + delete normalized[key]; + } return normalizeADKVariableTokenCounts(normalized); } @@ -84,8 +94,7 @@ function normalizeADKSummary(summary: Json): Json { ...summary, metric_keys: summary.metric_keys.filter( (metric): metric is string => - metric !== "prompt_cached_tokens" && - metric !== "completion_reasoning_tokens", + typeof metric === "string" && !VOLATILE_ADK_METRIC_KEYS.has(metric), ), } satisfies Json; } @@ -249,6 +258,34 @@ export function defineGoogleADKInstrumentationAssertions(options: { expect(agentSpan).toBeDefined(); }); + test("captures nested SequentialAgent sub-agent names", testConfig, () => { + const agentSpans = events.filter( + (e) => e.span.name?.startsWith("Agent:") && e.span.type === "task", + ); + const agentSpanNames = agentSpans.map((e) => e.span.name); + + expect(agentSpanNames).toEqual( + expect.arrayContaining([ + "Agent: sequential_workflow", + "Agent: greeter", + "Agent: farewell", + ]), + ); + + const workflowSpan = agentSpans.find( + (e) => e.span.name === "Agent: sequential_workflow", + ); + const greeterSpan = agentSpans.find( + (e) => e.span.name === "Agent: greeter", + ); + const farewellSpan = agentSpans.find( + (e) => e.span.name === "Agent: farewell", + ); + + expect(greeterSpan?.span.parentIds).toContain(workflowSpan?.span.id); + expect(farewellSpan?.span.parentIds).toContain(workflowSpan?.span.id); + }); + test("captures tool span for Tool.runAsync()", testConfig, () => { const toolSpan = [...events] .reverse() @@ -267,9 +304,13 @@ export function defineGoogleADKInstrumentationAssertions(options: { }); test("nests the agent span under the runner span", testConfig, () => { - const runnerSpan = findLatestSpan(events, "Google ADK Runner"); + const runnerSpan = events.find( + (e) => + e.span.name === "Google ADK Runner" && + e.row.metadata?.["google_adk.session_id"] === "test-session-1", + ); const agentSpan = events.find( - (e) => e.span.name?.startsWith("Agent:") && e.span.type === "task", + (e) => e.span.name === "Agent: weather_agent" && e.span.type === "task", ); expect(runnerSpan).toBeDefined(); diff --git a/e2e/scenarios/google-adk-instrumentation/scenario.impl.mjs b/e2e/scenarios/google-adk-instrumentation/scenario.impl.mjs index 9ff96f2e9..14b156c1b 100644 --- a/e2e/scenarios/google-adk-instrumentation/scenario.impl.mjs +++ b/e2e/scenarios/google-adk-instrumentation/scenario.impl.mjs @@ -11,7 +11,8 @@ const SCENARIO_NAME = "google-adk-instrumentation"; async function runGoogleADKInstrumentationScenario(adk, options = {}) { const decoratedADK = options.decorateSDK ? options.decorateSDK(adk) : adk; - const { LlmAgent, InMemoryRunner, FunctionTool } = decoratedADK; + const { LlmAgent, SequentialAgent, InMemoryRunner, FunctionTool } = + decoratedADK; process.env.GOOGLE_GENAI_API_KEY ??= process.env.GOOGLE_API_KEY ?? process.env.GEMINI_API_KEY; @@ -38,9 +39,15 @@ async function runGoogleADKInstrumentationScenario(adk, options = {}) { const agent = new LlmAgent({ name: "weather_agent", model: GOOGLE_MODEL, - instruction: - "For weather questions, first call the get_weather tool exactly once, then answer with the tool result in one short sentence. Do not answer from memory and do not call any tool after you have a tool result.", - tools: [getWeatherTool], + instruction: "Answer the user's question in one short sentence.", + beforeAgentCallback: async () => { + await getWeatherTool.runAsync({ + args: { location: "Paris, France" }, + toolContext: { + functionCallId: "direct-weather-tool", + }, + }); + }, generateContentConfig: { temperature: 0, }, @@ -57,6 +64,40 @@ async function runGoogleADKInstrumentationScenario(adk, options = {}) { userId, }); + const greeter = new LlmAgent({ + name: "greeter", + model: GOOGLE_MODEL, + instruction: "Greet the user with a single short sentence.", + beforeAgentCallback: () => ({ + role: "model", + parts: [{ text: "Hello." }], + }), + }); + const farewell = new LlmAgent({ + name: "farewell", + model: GOOGLE_MODEL, + instruction: "Say a single short closing sentence.", + beforeAgentCallback: () => ({ + role: "model", + parts: [{ text: "Goodbye." }], + }), + }); + const workflow = new SequentialAgent({ + name: "sequential_workflow", + subAgents: [greeter, farewell], + }); + const workflowRunner = new InMemoryRunner({ + agent: workflow, + appName: "e2e-test-sequential-app", + }); + const workflowSessionId = "test-session-sequential"; + + await workflowRunner.sessionService.createSession({ + appName: workflowRunner.appName, + sessionId: workflowSessionId, + userId, + }); + await runTracedScenario({ callback: async () => { // Test 1: Simple agent run (should produce runner + agent + LLM + tool spans) @@ -76,6 +117,24 @@ async function runGoogleADKInstrumentationScenario(adk, options = {}) { events.push(event); } }); + + await runOperation( + "adk-sequential-run-operation", + "sequential-run", + async () => { + const events = []; + for await (const event of workflowRunner.runAsync({ + userId, + sessionId: workflowSessionId, + newMessage: { + role: "user", + parts: [{ text: "Hello." }], + }, + })) { + events.push(event); + } + }, + ); }, metadata: { scenario: SCENARIO_NAME, diff --git a/js/src/instrumentation/plugins/google-adk-channels.ts b/js/src/instrumentation/plugins/google-adk-channels.ts index 6f659d7f8..58eabec08 100644 --- a/js/src/instrumentation/plugins/google-adk-channels.ts +++ b/js/src/instrumentation/plugins/google-adk-channels.ts @@ -5,6 +5,10 @@ import type { GoogleADKToolRunRequest, } from "../../vendor-sdk-types/google-adk"; +type GoogleADKChannelContext = { + self?: unknown; +}; + /** * Channels for Google ADK instrumentation. * @@ -19,7 +23,7 @@ export const googleADKChannels = defineChannels("@google/adk", { runnerRunAsync: channel< [GoogleADKRunAsyncParams], AsyncGenerator, - Record, + GoogleADKChannelContext, GoogleADKEvent >({ channelName: "runner.runAsync", @@ -29,14 +33,18 @@ export const googleADKChannels = defineChannels("@google/adk", { agentRunAsync: channel< [unknown], AsyncGenerator, - Record, + GoogleADKChannelContext, GoogleADKEvent >({ channelName: "agent.runAsync", kind: "sync-stream", }), - toolRunAsync: channel<[GoogleADKToolRunRequest], unknown>({ + toolRunAsync: channel< + [GoogleADKToolRunRequest], + unknown, + GoogleADKChannelContext + >({ channelName: "tool.runAsync", kind: "async", }), diff --git a/js/src/instrumentation/plugins/google-adk-plugin.test.ts b/js/src/instrumentation/plugins/google-adk-plugin.test.ts index 822ce31f5..c0f7f7240 100644 --- a/js/src/instrumentation/plugins/google-adk-plugin.test.ts +++ b/js/src/instrumentation/plugins/google-adk-plugin.test.ts @@ -289,6 +289,43 @@ describe("GoogleADKPlugin", () => { ); }); + it("uses the invoked agent instance for names when parent context still points at a parent agent", () => { + plugin.enable(); + + const handlers = subscribeSpy.mock.calls[1][0]; + const event = { + arguments: [ + { + agent: { + name: "sequential_workflow", + model: "outer-model", + }, + }, + ], + self: { + name: "greeter", + model: "gemini-2.5-flash-lite", + }, + }; + + handlers.start(event); + + const span = mockStartSpan.mock.results[0].value; + expect(mockStartSpan).toHaveBeenCalledWith( + expect.objectContaining({ + name: "Agent: greeter", + spanAttributes: { type: "task" }, + }), + ); + expect(span.log).toHaveBeenCalledWith({ + metadata: { + provider: "google-adk", + "google_adk.agent_name": "greeter", + model: "gemini-2.5-flash-lite", + }, + }); + }); + it("should handle agent without a name gracefully", () => { plugin.enable(); const handlers = subscribeSpy.mock.calls[1][0]; diff --git a/js/src/instrumentation/plugins/google-adk-plugin.ts b/js/src/instrumentation/plugins/google-adk-plugin.ts index c90f12c06..84da399ef 100644 --- a/js/src/instrumentation/plugins/google-adk-plugin.ts +++ b/js/src/instrumentation/plugins/google-adk-plugin.ts @@ -199,8 +199,9 @@ export class GoogleADKPlugin extends BasePlugin { const parentContext = event.arguments[0] as | Record | undefined; + const agent = event.self as GoogleADKBaseAgent | undefined; - const agentName = extractAgentName(parentContext); + const agentName = extractAgentName(agent, parentContext); const runnerParentSpan = findRunnerParentSpan( parentContext, this.activeRunnerSpans, @@ -230,7 +231,7 @@ export class GoogleADKPlugin extends BasePlugin { if (agentName) { metadata["google_adk.agent_name"] = agentName; } - const modelName = extractModelName(parentContext); + const modelName = extractModelName(agent, parentContext); if (modelName) { metadata.model = modelName; } @@ -688,34 +689,39 @@ function extractRunnerMetadata( } function extractAgentName( + agent: GoogleADKBaseAgent | undefined, parentContext: Record | undefined, ): string | undefined { + if (typeof agent?.name === "string" && agent.name.length > 0) { + return agent.name; + } + if (!parentContext) { return undefined; } - const agent = parentContext.agent as GoogleADKBaseAgent | undefined; - return agent?.name; + const contextAgent = parentContext.agent as GoogleADKBaseAgent | undefined; + return typeof contextAgent?.name === "string" && contextAgent.name.length > 0 + ? contextAgent.name + : undefined; } function extractModelName( + agent: GoogleADKLlmAgent | undefined, parentContext: Record | undefined, ): string | undefined { - if (!parentContext) { - return undefined; - } - - const agent = parentContext.agent as GoogleADKLlmAgent | undefined; - if (!agent?.model) { - return undefined; + const modelAgent = + agent ?? (parentContext?.agent as GoogleADKLlmAgent | undefined); + if (!modelAgent?.model) { + return; } - if (typeof agent.model === "string") { - return agent.model; + if (typeof modelAgent.model === "string") { + return modelAgent.model; } - if (typeof agent.model === "object" && "model" in agent.model) { - return agent.model.model; + if (typeof modelAgent.model === "object" && "model" in modelAgent.model) { + return modelAgent.model.model; } return undefined; @@ -759,7 +765,7 @@ function extractToolAgentName( const invocationContext = toolContext?.invocationContext as | Record | undefined; - return extractAgentName(invocationContext); + return extractAgentName(undefined, invocationContext); } function findToolParentSpan(