Repository navigation
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.
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