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.
Serving your agents
Section titled “Serving your agents”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 flowsSwap 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.
Endpoints
Section titled “Endpoints”For an agent named <name>, the server mounts:
| Endpoint | Mounted when | Purpose |
|---|---|---|
POST /<name> | Always | Runs one turn |
POST /<name>/getSnapshot | Server-managed agents | Reads back a stored snapshot by snapshotId or sessionId |
POST /<name>/abort | The store supports change notifications | Marks 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.
Running a turn
Section titled “Running a turn”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 */ }}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"}}Streaming
Section titled “Streaming”Request Server-Sent Events with Accept: text/event-stream or ?stream=true:
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().
Reading a snapshot
Section titled “Reading a snapshot”POST /<name>/getSnapshot (server-managed agents only) reads back a stored snapshot. This is the main way to poll a detached turn over HTTP.
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":[]}}}Aborting a turn
Section titled “Aborting a turn”POST /<name>/abort marks a pending snapshot aborted:
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.
Error responses
Section titled “Error responses”Errors use a structured envelope:
{"error":{"status":"INVALID_ARGUMENT","message":"…","details":{"stack":"…"}}}status maps to an HTTP code the same way on both plugins:
status | HTTP code |
|---|---|
INVALID_ARGUMENT, FAILED_PRECONDITION, OUT_OF_RANGE | 400 |
UNAUTHENTICATED | 401 |
PERMISSION_DENIED | 403 |
NOT_FOUND | 404 |
ALREADY_EXISTS, ABORTED | 409 |
RESOURCE_EXHAUSTED | 429 |
UNIMPLEMENTED | 501 |
UNAVAILABLE | 503 |
DEADLINE_EXCEEDED | 504 |
| anything else | 500 |
See Error Handling for where these statuses come from.
Calling a served agent from Java
Section titled “Calling a served agent from Java”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 snapshotIdFor 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();Passing headers
Section titled “Passing headers”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:
// ClientRemoteAgentOptions opts = RemoteAgentOptions.builder() .url("http://localhost:8080/echoAgent") .headers(Map.of("Authorization", "Bearer sk-…")) .build();// Server — inside an AgentFn or tool handlerMap<String, String> headers = (Map<String, String>) fnCtx.context().getContext().get("headers");String token = headers != null ? headers.get("Authorization") : null;See also
Section titled “See also”- Agents Overview — Defining agents and the chat API
- Run and Stream — The
send/sendStreamsemanticsAgentChatwraps over HTTP - Sessions — Snapshot lifecycle and aborting
- Session Stores — Which stores support the
/abortendpoint - Background Execution — Detaching turns and polling over HTTP
- Error Handling — The status codes behind the error envelope
- Jetty Server — Jetty-specific setup and flow endpoints
- Spring Boot — Spring-specific setup and flow endpoints