feat(plugins): port DebugLoggingPlugin from adk-python - #1432
Open
svetanis wants to merge 1 commit into
Open
Conversation
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:
When an agent misbehaves, adk-java has no way to capture what happened in a form you can read
afterwards or attach to a bug report. The contents sent to the model, what came back, each tool call
and its result, the events the runner yielded — all of it is observable only while the run is
happening, and nothing produces a structured record. adk-python ships
DebugLoggingPluginforexactly this; adk-java has no equivalent.
Solution:
A port of that plugin. New files in a new
com.google.adk.plugins.debugloggingsubpackage, noexisting file modified, no existing API changed, and no new dependency —
jackson-dataformat-yamlis already a compile-scope dependency ofcore. Registration uses thesurface that is already there:
Twelve new files, the same count
plugins/agentanalyticsalready ships:DebugLoggingPlugin.javaDebugYamlWriter.javaDebugEntry.java,InvocationDebugState.java,DebugTraceRecorder.javaTracePayload.java(sealed, + 8 nested)datacan takeContentTrace.java(nestingPartTraceand its 6 part kinds),EventTrace.java,LlmRequestTrace.java,LlmResponseTrace.java,UsageTrace.javaSafeSerializer.javaThe PR adds exactly one public type,
DebugLoggingPlugin. Everything else is package-private.All twelve hooks are observe-only — eleven return
Maybe.empty(), andafterRunCallbackreturnsa
Completablethat only writes the file. Failures inside are logged, not thrown.The trace curates rather than dumps, which is why this is not a generic
ObjectMapperover ADK'stypes. Tool declarations are recorded as names only; inline data as
mime_typewith_data_omitted: trueand no component for the bytes at all; grounding metadata ashas_grounding_metadata: true; requested auth configs as a count. Each matches upstream.One difference from adk-python.
onUserMessageCallbackfires beforebeforeRunCallback, andupstream opens its per-invocation state only in the later hook, so its
user_messageentry isdropped. Here either hook opens the invocation, so the user's message is recorded. A test and a demo
row pin it.
On the write path:
afterRunCallbackreturnsCompletable, so the write runs onSchedulers.io();Runner.runAgentWithUpdatedSessionjoins itonto the event stream with
concatWith, so a caller that drainsrunAsynchas the file on diskwhen it returns.
synchronized. Two invocations finishing together would otherwise interleavetheir documents into a file that no longer parses — a wide window on the JVM, with the write on
Schedulers.io().Ported from
adk-python/src/google/adk/plugins/debug_logging_plugin.py(572 lines).Testing Plan
Unit Tests:
Ninety-seven tests, all new — these are new classes, so there is no before/after state to contrast.
They assert on serialized output, not on intermediate maps, and the writer and plugin tests
assert on the re-parsed YAML, because "the trace can be read back" is the actual promise:
EventTraceTest(17)false; the actions block only when non-empty; auth configs as a count; the live action maps copiedLlmExchangeTraceTest(15)includeSystemInstruction=falserecords presence, not text; response schema and grounding metadata each reduced to a booleanSafeSerializerTest(12)ContentTraceTest(12)TracePayloadTest(12)data: {}for markers;args: {}kept when a tool takes noneDebugLoggingPluginTest(9)Maybehooks return empty; the user message survives arriving before the run opens; both config switches; the state is released after the writeDebugTraceRecorderTest(9)entries()is a snapshot, not the live queue; a session without app name or user id omits those keysDebugYamlWriterTest(8)---each; all four scalar-lookalikes survive as strings; multi-line text as a literal block; an unwritable path logs instead of throwing; eight threads appending at once still yield eight intact documentsUsageTraceTest(3)Manual End-to-End (E2E) Tests:
One real invocation against
gemini-2.5-flashthrough anInMemoryRunner, with one tool. Two modelcalls, 15 entries, one document — excerpt:
Eight further scenarios were run offline against scripted models, covering the entry sequence, both
tool-failure shapes, a model error, append across two invocations, two invocations racing on one
file, a blob in the conversation, and the configuration switches.
Checklist