From ec7d434f6ad88ed836df1973c3e26e3ec99d949a Mon Sep 17 00:00:00 2001 From: Guillaume Laforge Date: Tue, 22 Sep 2026 00:42:10 +0200 Subject: [PATCH] Fix prompt echo and sanitize harness logging output - Filter stepUpdate deltas by source (ignoring SOURCE_USER and SOURCE_SYSTEM deltas) to prevent prompt repetition in response text. - Route TARGET_ENVIRONMENT action deltas to thoughts rather than user-facing response text. - Connect currentThoughtsPublisher to reactively stream thinking deltas. - Change ProcessBuilder error redirection to PIPE and consume stdout/stderr line-by-line via SLF4J, silencing raw Go glog logs from the console. - Add StepUpdateFilterTest covering source and target delta filtering. - Update README.md, SKILL.md, and api-reference.md with harness logging configuration and Java 24+ sun.misc.Unsafe JVM options. --- README.md | 21 +++ .../io/github/glaforge/antigravity/Agent.java | 99 +++++++++++--- .../antigravity/StepUpdateFilterTest.java | 127 ++++++++++++++++++ skills/antigravity-sdk-java/SKILL.md | 2 + .../references/api-reference.md | 17 +++ 5 files changed, 250 insertions(+), 16 deletions(-) create mode 100644 antigravity-sdk-wrapper/src/test/java/io/github/glaforge/antigravity/StepUpdateFilterTest.java diff --git a/README.md b/README.md index e015d47..b18f0ef 100644 --- a/README.md +++ b/README.md @@ -757,6 +757,27 @@ try (Agent agent = new Agent(config)) { } ``` +### 22. Harness Process Logging & Diagnostics + +The SDK embeds and manages the native Go harness (`localharness`) behind the scenes. Harness standard output and standard error streams are routed cleanly through **SLF4J**: +- Informational logs (such as CDP discovery and internal harness checks) are logged at `DEBUG` level. +- Warnings are logged at `WARN` level. +- Errors and fatal messages are logged at `ERROR` level. + +By default, your application console remains clean and free of harness startup output. To inspect internal Go harness communications, set `DEBUG` level for `io.github.glaforge.antigravity.Agent` in your logging configuration (e.g. `logback.xml` or `simplelogger.properties`): + +```properties +org.slf4j.simpleLogger.log.io.github.glaforge.antigravity.Agent=debug +``` + +### 23. Running on Java 24+ / GraalVM + +Starting in Java 24 (JEP 471), the JVM prints a terminally deprecated warning when libraries perform memory-access via `sun.misc.Unsafe` (used internally by Protobuf). To silence this warning in Maven projects or standalone runtimes, add the following flag to `.mvn/jvm.config` or pass it to `java`: + +```text +--sun-misc-unsafe-memory-access=allow +``` + ## License This project is licensed under the [Apache License, Version 2.0](LICENSE). diff --git a/antigravity-sdk-wrapper/src/main/java/io/github/glaforge/antigravity/Agent.java b/antigravity-sdk-wrapper/src/main/java/io/github/glaforge/antigravity/Agent.java index e9baf66..a1df30c 100644 --- a/antigravity-sdk-wrapper/src/main/java/io/github/glaforge/antigravity/Agent.java +++ b/antigravity-sdk-wrapper/src/main/java/io/github/glaforge/antigravity/Agent.java @@ -42,9 +42,12 @@ import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentHashMap; +import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.StandardCopyOption; import java.util.concurrent.TimeUnit; @@ -427,18 +430,27 @@ public Agent build() throws Exception { * if an error occurs during initialization */ public Agent(AgentConfig config) throws Exception { + this(config, true); + } + + Agent(AgentConfig config, boolean startProcess) throws Exception { this.config = config; this.policies = config.getPolicies(); for (Object tool : config.getToolInstances()) { this.registerTools(tool); } + if (!startProcess) { + this.goProcess = null; + return; + } + // 1. Resolve and extract (or reuse cached) localharness binary File binaryFile = PlatformResolver.resolveBinary(); // 2. Spawn process ProcessBuilder pb = new ProcessBuilder(binaryFile.getAbsolutePath()) - .redirectError(ProcessBuilder.Redirect.INHERIT); + .redirectError(ProcessBuilder.Redirect.PIPE); if (config.getEnvironmentVariables() != null && !config.getEnvironmentVariables().isEmpty()) { pb.environment().putAll(config.getEnvironmentVariables()); } @@ -498,20 +510,37 @@ public Agent(AgentConfig config) throws Exception { String apiKey = resolvedApiKey != null ? resolvedApiKey : "placeholder"; Thread stdoutConsumer = new Thread(() -> { - try { - is.transferTo(System.err); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + log.debug("[localharness stdout] {}", line); + } } catch (Exception e) { } - }); + }, "antigravity-harness-stdout"); stdoutConsumer.setDaemon(true); stdoutConsumer.start(); Thread stderrConsumer = new Thread(() -> { - try { - this.goProcess.getErrorStream().transferTo(System.err); + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(this.goProcess.getErrorStream(), StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + if (line.contains("ERROR: logging before google.Init: I") || line.contains("[CDP Discovery]") + || line.contains("permissions: skipping check")) { + log.debug("[localharness] {}", line); + } else if (line.contains("ERROR: logging before google.Init: W")) { + log.warn("[localharness] {}", line); + } else if (line.contains("ERROR: logging before google.Init: E") + || line.contains("ERROR: logging before google.Init: F")) { + log.error("[localharness] {}", line); + } else { + log.debug("[localharness stderr] {}", line); + } + } } catch (Exception e) { } - }); + }, "antigravity-harness-stderr"); stderrConsumer.setDaemon(true); stderrConsumer.start(); @@ -1110,7 +1139,7 @@ private void warnIfSandboxUnavailable(SandboxStatus status) { } } - private void handleIncomingMessage(WebSocket webSocket, String message) { + void handleIncomingMessage(WebSocket webSocket, String message) { try { JsonNode payload = jsonMapper.readTree(message); @@ -1157,16 +1186,40 @@ private void handleIncomingMessage(WebSocket webSocket, String message) { } if (stepUpdate.has("textDelta") || stepUpdate.has("thinkingDelta")) { - String textDelta = stepUpdate.path("textDelta").asText(""); - String thinkingDelta = stepUpdate.path("thinkingDelta").asText(""); + String source = stepUpdate.path("source").asText(""); + if (!"SOURCE_USER".equals(source) && !"SOURCE_SYSTEM".equals(source)) { + String target = stepUpdate.path("target").asText(""); + String textDelta = stepUpdate.path("textDelta").asText(""); + String thinkingDelta = stepUpdate.path("thinkingDelta").asText(""); + + if ("TARGET_ENVIRONMENT".equals(target)) { + if (currentThoughts != null && !textDelta.isEmpty()) { + if (currentThoughts.length() > 0 && !currentThoughts.toString().endsWith("\n")) { + currentThoughts.append("\n"); + } + currentThoughts.append(textDelta); + } + if (currentThoughtsPublisher != null && !textDelta.isEmpty()) { + currentThoughtsPublisher.submit(textDelta); + } + if (currentChunkConsumer != null && !textDelta.isEmpty()) { + currentChunkConsumer.accept(new AgentResponseChunk("", textDelta)); + } + } else { + if (currentText != null && !hasStructuredOutput) + currentText.append(textDelta); - if (currentText != null && !hasStructuredOutput) - currentText.append(textDelta); - if (currentThoughts != null) - currentThoughts.append(thinkingDelta); + if (currentChunkConsumer != null && (!textDelta.isEmpty() || !thinkingDelta.isEmpty())) { + currentChunkConsumer.accept(new AgentResponseChunk(textDelta, thinkingDelta)); + } + } - if (currentChunkConsumer != null && (!textDelta.isEmpty() || !thinkingDelta.isEmpty())) { - currentChunkConsumer.accept(new AgentResponseChunk(textDelta, thinkingDelta)); + if (currentThoughts != null && !thinkingDelta.isEmpty()) { + currentThoughts.append(thinkingDelta); + } + if (currentThoughtsPublisher != null && !thinkingDelta.isEmpty()) { + currentThoughtsPublisher.submit(thinkingDelta); + } } } @@ -1652,6 +1705,20 @@ public void close() throws Exception { } } + void initTurnForTest(Consumer chunkConsumer) { + this.currentText = new StringBuilder(); + this.currentThoughts = new StringBuilder(); + this.currentChunkConsumer = chunkConsumer; + } + + String getCurrentTextForTest() { + return this.currentText != null ? this.currentText.toString() : ""; + } + + String getCurrentThoughtsForTest() { + return this.currentThoughts != null ? this.currentThoughts.toString() : ""; + } + private static String resolveGeminiApiKey() { String envKey = System.getenv("GEMINI_API_KEY"); String propKey = System.getProperty("GEMINI_API_KEY"); diff --git a/antigravity-sdk-wrapper/src/test/java/io/github/glaforge/antigravity/StepUpdateFilterTest.java b/antigravity-sdk-wrapper/src/test/java/io/github/glaforge/antigravity/StepUpdateFilterTest.java new file mode 100644 index 0000000..fa42f5a --- /dev/null +++ b/antigravity-sdk-wrapper/src/test/java/io/github/glaforge/antigravity/StepUpdateFilterTest.java @@ -0,0 +1,127 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.glaforge.antigravity; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import java.util.ArrayList; +import java.util.List; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Tag("unit") +public class StepUpdateFilterTest { + + @Test + public void testStepUpdateIgnoresUserAndSystemDeltas() throws Exception { + AgentConfig config = AgentConfig.builder().instructions("Test assistant").build(); + try (Agent agent = new Agent(config, false)) { + List chunks = new ArrayList<>(); + agent.initTurnForTest(chunks::add); + + // 1. Simulate harness broadcasting SOURCE_USER echo + String userEchoPayload = """ + { + "stepUpdate": { + "cascadeId": "test-cascade", + "source": "SOURCE_USER", + "stepIndex": 0, + "textDelta": "What is the current weather in Paris?" + } + } + """; + agent.handleIncomingMessage(null, userEchoPayload); + + // Must not append user query to response text or emit chunks + assertEquals("", agent.getCurrentTextForTest(), "User prompt echo must not be appended to response text"); + assertTrue(chunks.isEmpty(), "User prompt echo must not be emitted to streaming chunk consumer"); + + // 2. Simulate harness broadcasting SOURCE_SYSTEM event + String systemPayload = """ + { + "stepUpdate": { + "source": "SOURCE_SYSTEM", + "stepIndex": 1, + "textDelta": "System environment initialized" + } + } + """; + agent.handleIncomingMessage(null, systemPayload); + assertEquals("", agent.getCurrentTextForTest(), "System step deltas must not be appended to response text"); + assertTrue(chunks.isEmpty(), "System step deltas must not be emitted to streaming chunk consumer"); + + // 3. Simulate harness broadcasting SOURCE_MODEL streaming deltas + String modelDelta1 = """ + { + "stepUpdate": { + "source": "SOURCE_MODEL", + "stepIndex": 2, + "thinkingDelta": "Checking weather for Paris", + "textDelta": "The current weather in Paris " + } + } + """; + agent.handleIncomingMessage(null, modelDelta1); + + assertEquals("The current weather in Paris ", agent.getCurrentTextForTest()); + assertEquals("Checking weather for Paris", agent.getCurrentThoughtsForTest()); + assertEquals(1, chunks.size()); + assertEquals("The current weather in Paris ", chunks.get(0).textDelta()); + assertEquals("Checking weather for Paris", chunks.get(0).thoughtsDelta()); + + // 4. Simulate second SOURCE_MODEL chunk + String modelDelta2 = """ + { + "stepUpdate": { + "source": "SOURCE_MODEL", + "stepIndex": 2, + "textDelta": "is 22°C and Sunny." + } + } + """; + agent.handleIncomingMessage(null, modelDelta2); + + assertEquals("The current weather in Paris is 22°C and Sunny.", agent.getCurrentTextForTest()); + assertEquals(2, chunks.size()); + assertEquals("is 22°C and Sunny.", chunks.get(1).textDelta()); + + // 5. Simulate TARGET_ENVIRONMENT action step (e.g. tool execution + // title/rationale) + agent.initTurnForTest(chunks::add); + chunks.clear(); + + String envDelta = """ + { + "stepUpdate": { + "source": "SOURCE_MODEL", + "target": "TARGET_ENVIRONMENT", + "stepIndex": 1, + "textDelta": "Tokyo weather check" + } + } + """; + agent.handleIncomingMessage(null, envDelta); + + assertEquals("", agent.getCurrentTextForTest(), + "TARGET_ENVIRONMENT text must not be appended to response text"); + assertEquals("Tokyo weather check", agent.getCurrentThoughtsForTest(), + "TARGET_ENVIRONMENT text must be routed to thoughts"); + assertEquals(1, chunks.size()); + assertEquals("", chunks.get(0).textDelta()); + assertEquals("Tokyo weather check", chunks.get(0).thoughtsDelta()); + } + } +} diff --git a/skills/antigravity-sdk-java/SKILL.md b/skills/antigravity-sdk-java/SKILL.md index b16c497..ad2f3d5 100644 --- a/skills/antigravity-sdk-java/SKILL.md +++ b/skills/antigravity-sdk-java/SKILL.md @@ -15,6 +15,8 @@ Before executing tasks with the Antigravity Java SDK, verify the environment: - **Check Dependencies**: - Standard (default): `io.github.glaforge:antigravity-sdk-wrapper` (lightweight ~140 KB, on-demand automatic harness download into `~/.antigravity/bin/`). - Offline / Air-Gapped (optional): `io.github.glaforge:antigravity-sdk-harness` with matching platform classifier (e.g. `osx-aarch64`, `linux-x86_64`, or `all`). +- **Java 24+ / GraalVM Runtimes**: When using Java 24+ or GraalVM (JEP 471), silence Protobuf `sun.misc.Unsafe` deprecation warnings by adding `--sun-misc-unsafe-memory-access=allow` to `.mvn/jvm.config`. +- **Harness Logging**: The native Go harness stdout/stderr streams are piped to SLF4J (`DEBUG` for info/stdout, `WARN` for warnings, `ERROR` for errors), keeping the console clean by default. Enable `DEBUG` on `io.github.glaforge.antigravity.Agent` to inspect harness internals. - **API Key Setup**: A valid `GEMINI_API_KEY` environment variable is required to access Gemini models. - If credentials are missing, actively help the user get set up by providing the Google AI Studio link: `https://aistudio.google.com/app/api-keys`. - **Vertex AI (Gemini Enterprise Agent Platform)**: Uses Application Default Credentials (ADC). Instruct the user to run `gcloud auth application-default login` and set environment variables `GOOGLE_CLOUD_PROJECT` and `GOOGLE_CLOUD_LOCATION`. diff --git a/skills/antigravity-sdk-java/references/api-reference.md b/skills/antigravity-sdk-java/references/api-reference.md index 4145722..50ef299 100644 --- a/skills/antigravity-sdk-java/references/api-reference.md +++ b/skills/antigravity-sdk-java/references/api-reference.md @@ -726,6 +726,23 @@ try (Agent agent = new Agent(config)) { } ``` +--- + +## 15. Harness Process Logging & Java 24+ Runtimes +### SLF4J Harness Diagnostics +The embedded `localharness` stdout and stderr are consumed line-by-line and routed through SLF4J: +- Informational output (`glog` INFO, CDP discovery, permission checks) -> `log.debug(...)` +- Warnings -> `log.warn(...)` +- Fatal errors -> `log.error(...)` +To view raw harness diagnostics, configure `io.github.glaforge.antigravity.Agent` logger level to `DEBUG`: +```properties +org.slf4j.simpleLogger.log.io.github.glaforge.antigravity.Agent=debug +``` +### Java 24+ / GraalVM Deprecation Flag +Under Java 24+ (JEP 471), protobuf memory-access via `sun.misc.Unsafe` produces a deprecation warning. Silence it by creating `.mvn/jvm.config`: +```text +--sun-misc-unsafe-memory-access=allow +```