Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
}
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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);
}
}
}

Expand Down Expand Up @@ -1652,6 +1705,20 @@ public void close() throws Exception {
}
}

void initTurnForTest(Consumer<AgentResponseChunk> 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");
Expand Down
Original file line number Diff line number Diff line change
@@ -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<AgentResponseChunk> 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());
}
}
}
2 changes: 2 additions & 0 deletions skills/antigravity-sdk-java/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
17 changes: 17 additions & 0 deletions skills/antigravity-sdk-java/references/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Loading