Skip to content

Serve over HTTP

Any agent you define can be served over HTTP by the Jetty or Spring plugin. Both plugins speak the same wire format, so the same Java client works unchanged against either one.

Agent endpoints are mounted automatically for every registered agent when the plugin starts — there’s no separate opt-in beyond adding the plugin and starting it:

import com.google.genkit.plugins.jetty.JettyPlugin;
import com.google.genkit.plugins.jetty.JettyPluginOptions;
JettyPlugin jetty = new JettyPlugin(JettyPluginOptions.builder().port(8080).build());
Genkit genkit = Genkit.builder()
.options(GenkitOptions.builder().experimental(true).build())
.plugin(jetty)
.build();
// Define your agent(s) before starting.
Agent<Map<String, Object>> echoAgent = genkit.beta().defineCustomAgent(
CustomAgentConfig.<Map<String, Object>>builder()
.name("echoAgent")
.store(new FileSessionStore<>("./.snapshots"))
.build(),
(sess, fnCtx) -> AgentResult.builder()
.message(Message.model("echo: " + sess.getMessages().get(sess.getMessages().size() - 1).getText()))
.finishReason(AgentFinishReason.STOP)
.build());
jetty.start(); // mounts /echoAgent (and its companions) alongside any flows

Swap in SpringPlugin.create() for the same result. Agent endpoints are mounted at the root path (/<agentName>), separate from flow endpoints, on the same port. See Jetty Server and Spring Boot for plugin-specific setup.

For an agent named <name>, the server mounts:

EndpointMounted whenPurpose
POST /<name>AlwaysRuns one turn
POST /<name>/getSnapshotServer-managed agentsReads back a stored snapshot by snapshotId or sessionId
POST /<name>/abortThe store supports change notificationsMarks a pending snapshot aborted

A client-managed agent (no store) only mounts the turn endpoint, since there’s no server-side snapshot to fetch or abort.

POST /<name> runs exactly one turn. The JSON body is an envelope with the turn input, optional resume/session init, and an optional context map:

{"data": { /* AgentInput */ }, "init": { /* AgentInit */ }, "context": { /* optional */ }}
Terminal window
curl -X POST http://localhost:8080/echoAgent \
-H "Content-Type: application/json" \
-d '{"data":{"message":{"role":"user","content":[{"text":"Hello, agent!"}]}}}'
{"result":{"sessionId":"876d78e6-…","snapshotId":"ca350aa8-…","message":{"text":"echo: Hello, agent!","role":"model","content":[{"text":"echo: Hello, agent!"}]},"finishReason":"stop"}}

Request Server-Sent Events with Accept: text/event-stream or ?stream=true:

Terminal window
curl -N -X POST "http://localhost:8080/echoAgent?stream=true" \
-H "Content-Type: application/json" \
-d '{"data":{"message":{"role":"user","content":[{"text":"Hello again"}]}}}'

The response is text/event-stream: zero or more chunk frames, then exactly one terminal frame carrying the final result:

data: {"message": <chunk>}
data: {"result": <AgentOutput>}

A failed turn sends data: {"error": {...}} as the terminal frame instead. How much each frame carries depends on the agent — a model-backed agent streams model text as it’s generated, while a custom agent streams only what it pushes through ctx.sendChunk().

POST /<name>/getSnapshot (server-managed agents only) reads back a stored snapshot. This is the main way to poll a detached turn over HTTP.

Terminal window
curl -X POST http://localhost:8080/echoAgent/getSnapshot \
-H "Content-Type: application/json" \
-d '{"data":{"snapshotId":"ca350aa8-…"}}'
{"result":{"snapshotId":"ca350aa8-…","sessionId":"876d78e6-…","status":"completed","finishReason":"stop","state":{"messages":[/* … */],"artifacts":[]}}}

POST /<name>/abort marks a pending snapshot aborted:

Terminal window
curl -X POST http://localhost:8080/echoAgent/abort \
-H "Content-Type: application/json" \
-d '{"data":{"snapshotId":"29b02672-…"}}'
{"result":{"snapshotId":"29b02672-…","status":"aborted"}}

This is most useful for detached turns: a background turn that checks ctx.isAborted() can stop early, and once aborted the snapshot stays aborted even if the work finishes. The endpoint needs a store that supports change notifications (InMemorySessionStore does not; see Session Stores). See Sessions for the full abort behavior.

Errors use a structured envelope:

{"error":{"status":"INVALID_ARGUMENT","message":"","details":{"stack":""}}}

status maps to an HTTP code the same way on both plugins:

statusHTTP code
INVALID_ARGUMENT, FAILED_PRECONDITION, OUT_OF_RANGE400
UNAUTHENTICATED401
PERMISSION_DENIED403
NOT_FOUND404
ALREADY_EXISTS, ABORTED409
RESOURCE_EXHAUSTED429
UNIMPLEMENTED501
UNAVAILABLE503
DEADLINE_EXCEEDED504
anything else500

See Error Handling for where these statuses come from.

RemoteAgent.chat(...) gives you an AgentChat backed by HTTP — the same client works against a Jetty- or Spring-backed server:

import com.google.genkit.client.RemoteAgent;
import com.google.genkit.client.RemoteAgentOptions;
import com.google.genkit.ai.agent.AgentChat;
import com.google.genkit.ai.agent.AgentResponse;
AgentChat<Map<String, Object>> chat = RemoteAgent.chat(
RemoteAgentOptions.builder()
.url("http://localhost:8080/echoAgent")
.build());
AgentResponse<Map<String, Object>> resp = chat.send("hello");
System.out.println(resp.text()); // "echo: hello"
System.out.println(chat.snapshotId()); // the next send() resumes from here automatically
chat.abort(); // POSTs to /echoAgent/abort with the tracked snapshotId

For a client-managed agent, set serverManaged(false) so state round-trips in the request instead of being stored server-side:

RemoteAgentOptions opts = RemoteAgentOptions.builder()
.url("http://localhost:8080/counterAgent")
.serverManaged(false)
.build();

Headers you set with RemoteAgentOptions.headers(...) are sent as real HTTP request headers, and the server makes them available to your AgentFn and tools through the request context under a "headers" key (standard framing headers like content-type and host are excluded). This is a natural place to carry an auth token:

// Client
RemoteAgentOptions opts = RemoteAgentOptions.builder()
.url("http://localhost:8080/echoAgent")
.headers(Map.of("Authorization", "Bearer sk-…"))
.build();
// Server — inside an AgentFn or tool handler
Map<String, String> headers =
(Map<String, String>) fnCtx.context().getContext().get("headers");
String token = headers != null ? headers.get("Authorization") : null;