Vis

Java and Clojure SDK

Create a session on a running gateway, submit a task and read the answer from a Java or Clojure application. The gateway runs the tools and owns the project files; your JVM application acts as its client.

Prepare the JVM classpath

Use JDK 25 and the Clojure CLI. The published Vis library includes its runtime dependencies; you do not need a Vis source checkout. Java calls the public com.blockether.vis.core namespace through clojure.java.api.Clojure; there is no standalone Java-only SDK.

Add the published library to your application's deps.edn:

{:mvn/repos {"jitpack" {:url "https://jitpack.io"}}
 :deps {com.blockether/vis {:mvn/version "0.2.3"}}}

Keep the JitPack entry: the Clojure CLI does not inherit repositories from a dependency's POM. From the directory containing deps.edn, resolve the dependencies and prepare the classpath:

export VIS_CLASSPATH="$(clojure -Spath)"

The client loads the engine's JVM libraries even though the gateway performs the actual tasks. For a Python client, use the Python SDK.

Connect from Java

Start a gateway with a configured provider. For a local gateway using its default state directory, set these in your private terminal before launching Java:

export VIS_GATEWAY_URL=http://127.0.0.1:7890
export VIS_GATEWAY_TOKEN="$(cat "$HOME/.vis/gateway.token")"
export VIS_PROJECT_ROOT="$PWD"

Do not print or commit the token. For a remote connection, use its HTTPS origin, a securely supplied token and a project path on the gateway machine. See remote access. Requests can use that machine's tools and incur model charges.

The JVM client reads these settings itself and uses one process-wide gateway target. Without an explicit URL it can discover or start a local gateway; this example requires the URL to avoid that side effect. Do not switch targets by changing environment settings per request.

Save this as VisExample.java. It creates a session, waits for a task and prints the returned content blocks. It uses the public facade rather than internal transport namespaces.

// VisExample.java
import clojure.java.api.Clojure;
import clojure.lang.IFn;
import clojure.lang.IPersistentMap;
import clojure.lang.Keyword;
import clojure.lang.PersistentArrayMap;
import java.util.Map;

public final class VisExample {
    private static IFn api(String name) {
        return Clojure.var("com.blockether.vis.core", name);
    }

    private static IPersistentMap options(String key, String value) {
        return PersistentArrayMap.create(Map.of(Keyword.intern(key), value));
    }

    public static void main(String[] args) {
        String gatewayUrl = System.getenv("VIS_GATEWAY_URL");
        if (gatewayUrl == null || gatewayUrl.isBlank() || args.length != 2) {
            throw new IllegalArgumentException(
                "Set VIS_GATEWAY_URL; pass the gateway project path and request");
        }
        Clojure.var("clojure.core", "require").invoke(
            Clojure.read("com.blockether.vis.core"));
        String sessionId = null;
        try {
            Map<?, ?> session = (Map<?, ?>) api("gateway-create-session!")
                .invoke(options("root", args[0]));
            sessionId = (String) session.get("id");
            System.out.println("Session: " + sessionId);
            Map<?, ?> result = (Map<?, ?>) api("gateway-submit-turn-sync!")
                .invoke(sessionId, options("request", args[1]));
            if (result.get("error") != null) {
                throw new IllegalStateException(result.get("error").toString());
            }
            System.out.println(result.get("content"));
            Map<?, ?> turn = (Map<?, ?>) api("gateway-get-turn")
                .invoke(sessionId, result.get("session_turn_id"));
            System.out.println("Status: " + turn.get("status"));
        } finally {
            try {
                api("gateway-release-session!").invoke(sessionId);
            } finally {
                Clojure.var("clojure.core", "shutdown-agents").invoke();
            }
        }
    }
}

The options use Clojure keyword keys; gateway records returned here use string keys. Passing an ordinary Java map with "root" as the option key is not equivalent to passing :root. Do not use Clojure.read to parse untrusted requests; this example reads only a fixed namespace symbol.

Save VisExample.java beside deps.edn, then compile and run it on macOS or Linux:

javac -cp "$VIS_CLASSPATH" VisExample.java
java -cp ".:$VIS_CLASSPATH" VisExample \
  "$VIS_PROJECT_ROOT" "Summarize this project without changing files."

You should see a session ID, the content blocks and Status: completed. The example reads the canonical turn record for its status. The synchronous result is different: it has no status on success and uses needs_input for a suspended turn. A returned Java method alone does not establish task success.

gateway-release-session! releases runtime resources for that session and the process's client lease; it does not delete the saved conversation or stop the gateway. Passing null releases only the lease. shutdown-agents is appropriate for this one-shot program; do not call it after every request in a long-lived JVM application. Likewise, release the shared client lease only when your application is finished with it, not while other requests are using it.

Call the same API from Clojure

With the same deps.edn and connection environment, save this as task.clj in that directory and run clojure -M task.clj:

(require '[com.blockether.vis.core :as vis])

(try
  (let [session (vis/gateway-create-session! {:root (System/getenv "VIS_PROJECT_ROOT")})
        sid (get session "id")]
    (try
      (let [result (vis/gateway-submit-turn-sync!
                     sid {:request "Summarize this project without changing files."})
            turn (vis/gateway-get-turn sid (get result "session_turn_id"))]
        (prn (select-keys turn ["status" "content"])))
      (finally
        (vis/gateway-release-session! sid))))
  (finally
    (shutdown-agents)))

For a long-lived application, use gateway-submit-turn! and gateway-get-turn when you need to manage waiting yourself. The synchronous submission also accepts :on-event, a function called with gateway events. Keep event handling short and do not log credentials or private tool results.

Package a JVM application or a native runtime

Your Java application can run on the JVM while the gateway runs as a prebuilt native executable. Java does not need GraalVM to connect to a native gateway. Update the service and application independently and keep credentials out of jars. For a private child process rather than a shared service, the Python SDK's Agent and LocalEngine already implement process ownership and the stdio protocol.

Use a prebuilt Vis engine for either connection mode. You do not rebuild Vis merely to connect from Java or Clojure, wrap it from Python or host a gateway.

Only if you are adding Java/Clojure capabilities inside the engine, follow Native builds for JVM extensions. That workflow compiles Vis with your code, its Clojure AOT classes, resources and reachability metadata. It does not compile arbitrary SDK applications or provide a Java-only embedding recipe. Keep an external client and the engine as separate processes.

See also