Repository navigation
feat(plugins): port DebugLoggingPlugin from adk-python - #1432
Conversation
|
Hi @svetanis, thank you for your contribution. We truly appreciate you taking the time to submit this PR. It is currently under review by our team and we will update you if any additional information is required. Thank you! |
f5c551e to
71ed59e
Compare
71ed59e to
aa77a43
Compare
| * limitations under the License. | ||
| */ | ||
|
|
||
| package com.google.adk.plugins.debuglogging; |
There was a problem hiding this comment.
HIGH: Please match the structure of the Kotlin port: one DebugLoggingPlugin class in the main plugins package (L17, L66-L71), with DebugEntry and InvocationDebugState as private nested classes (L490-L505) and the remaining logic in private helpers. In Java, that means placing com.google.adk.plugins.DebugLoggingPlugin next to LoggingPlugin and folding in the 11 package-private types, which reduces 1,673 production lines across 12 files to roughly Kotlin's 544.
| * MarkerTrace} serves {@code agent_end} and {@code invocation_end}, which upstream records with no | ||
| * payload at all. | ||
| */ | ||
| sealed interface TracePayload |
There was a problem hiding this comment.
HIGH: Kotlin applies the same curation (tool names only, _data_omitted, has_grounding_metadata, and counts) without defining a separate type per payload: each entry's data is an ordered map built by a null-dropping helper, dataOf, plus small private serializers for content and actions (L423-L464). Please use the same approach with LinkedHashMap<String, Object> and one YAML mapper instead of this sealed interface, the *Trace records with Optional components and isEmpty() checks, MarkerTrace, and the DebugEntry.Type enum.
| public DebugLoggingPlugin( | ||
| String name, Path outputPath, boolean includeSessionState, boolean includeSystemInstruction) { |
There was a problem hiding this comment.
MEDIUM: Two adjacent booleans make calls like new DebugLoggingPlugin(name, path, true, false) error-prone. Kotlin provides Java callers with a builder, DebugLoggingPlugin.builder(), supporting outputPath, includeSessionState, includeSystemInstruction, and name, matching ADK Java's ContextFilterPlugin.builder(). Please replace this constructor and the Path one with a builder, while keeping the no-argument constructor for default settings.
| Files.writeString( | ||
| outputPath, | ||
| document, | ||
| StandardCharsets.UTF_8, | ||
| StandardOpenOption.CREATE, | ||
| StandardOpenOption.APPEND); |
There was a problem hiding this comment.
HIGH: The trace contains full prompts, tool arguments, and session state, yet the file is created using the process umask (producing rw-r--r-- under umask 022, verified via InMemoryRunner). Kotlin creates the file and restricts it to the owner before the first write (L170-L175, L351-L363) and tests this behavior (restrictsOutputFileToOwner), and Python now opens the file with mode 0o600 (L152-L154, L622-L626). Please create the file with owner-only permissions, for example with PosixFilePermissions.asFileAttribute(...) where POSIX is supported, and include the corresponding test.
| .enable(YAMLGenerator.Feature.MINIMIZE_QUOTES) | ||
| .enable(YAMLGenerator.Feature.ALWAYS_QUOTE_NUMBERS_AS_STRINGS) |
There was a problem hiding this comment.
MEDIUM: With MINIMIZE_QUOTES, tool strings such as 0x1F, 1_000, 2026-01-01, and .inf are written bare. PyYAML parses them back as 31, 1000, a date, and inf, while Jackson's own YAML reader fails on the entire document at .inf. Kotlin dumps with SnakeYAML, which quotes any string that would be parsed as another type (L75-L85). In Jackson, dropping MINIMIZE_QUOTES and ALWAYS_QUOTE_NUMBERS_AS_STRINGS quotes every string, while LITERAL_BLOCK_STYLE still renders multi-line text as |-.
| * to file rather than the plugin's internal bookkeeping. | ||
| */ | ||
| @RunWith(JUnit4.class) | ||
| public class DebugLoggingPluginTest { |
There was a problem hiding this comment.
MEDIUM: Kotlin tests this plugin in one test class that exercises the public hooks and inspects the generated file (DebugLoggingPluginTest.kt). Most of the nine test classes here target package-private types that will be removed once the plugin follows Kotlin. Please consolidate the behavioral test cases (hook order, user message before beforeRunCallback, both switches, missing state, unwritable path, concurrent appends, and scalar-like strings) into this class, and add tests for owner-only file permissions and strings like .inf and 0x1F.
| /** | ||
| * Registers {@code state} unless this invocation already has one. | ||
| * | ||
| * <p>"Unless" is load-bearing. {@code Runner.runAsync} invokes {@code onUserMessageCallback} | ||
| * <b>before</b> {@code beforeRunCallback}, so either hook may be the first to reach an | ||
| * invocation. Both call this, the second finds the state already open, and the user message — the | ||
| * first thing anyone reading a debug trace looks for — is recorded rather than filed against an | ||
| * invocation that does not exist yet. | ||
| * | ||
| * <p>The state is still created once, still keyed by invocation id, and still holds the same | ||
| * header. This is the port's one behavioral difference from adk-python, which opens the state in | ||
| * the later hook only. | ||
| */ |
There was a problem hiding this comment.
MEDIUM: Several Javadocs here span multiple paragraphs of design rationale, reference adk-python's private helpers (_add_entry, _serialize_content), and use em-dashes. ADK keeps doc comments to about three sentences focused on why, leaving detailed design rationale for the PR description. See Kotlin's KDoc for the expected length (L42-L65, L91-L92). Please trim the comments accordingly when merging into one file.
|
|
||
| static SessionStateTrace from(Session session) { | ||
| return new SessionStateTrace( | ||
| SafeSerializer.serializeMap(session.state()), session.events().size()); |
There was a problem hiding this comment.
LOW: Session.events() is deprecated; use session.immutableEvents().size() for the count.
| SafeSerializer.serializeMap(actions.stateDelta()), | ||
| ImmutableMap.copyOf(actions.artifactDelta()), | ||
| actions.transferToAgent(), | ||
| actions.escalate(), |
| record FunctionCallTrace( | ||
| Optional<String> id, | ||
| Optional<String> name, | ||
| @JsonInclude(JsonInclude.Include.NON_EMPTY) ImmutableMap<String, Object> args) { |
There was a problem hiding this comment.
aa77a43 to
3c3633e
Compare
|
Thanks for the review. All 13 comments are addressed in a single commit rebased on main:
Also aligned with Kotlin: tool names in declaration order, tool confirmations as a count, |
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. One new class,
com.google.adk.plugins.DebugLoggingPlugin, next toLoggingPlugin, no existing 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:
The PR adds two public types,
DebugLoggingPluginand itsBuilder. Everything else is private.All twelve hooks are observe-only — eleven return
Maybe.empty(), andafterRunCallbackreturnsa
Completablethat only writes the file. A failed write is 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 key for the bytes at all; grounding metadata ashas_grounding_metadata: true; requested auth configs as a count. Each matches upstream.Differences 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. Timestamps are UTC instants, every single-line string value is quoted, and an entry with
no data omits the
datakey.On the write path:
afterRunCallbackreturnsCompletable.fromAction, asLoggingPlugindoes, so the plugin does not move the end of the run onto another thread;the runner joins it onto the event stream with
concatWith, so a caller that drainsrunAsynchasthe file on disk when it returns.
even in two plugin instances, each append one intact document.
Ported from
adk-python/src/google/adk/plugins/debug_logging_plugin.py.Testing Plan
Unit Tests:
Fifty-one tests in one class, all new. Most drive the hooks and assert on the re-parsed YAML,
because "the trace can be read back" is the actual promise.
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