A2AServer.FileTaskStore
Java · package io.github.muthuishere:toolnexus · SPEC §7B · A2AServer.java
public interface TaskStore { Map<String, Object> get(String id); void save(Map<String, Object> task);}
public static final class InMemoryTaskStore implements TaskStore { /* default */ }public static final class FileTaskStore implements TaskStore { public FileTaskStore(String dir)}
public static TaskStore resolveStore(Object store) // null|"memory" | "file:<dir>" | a TaskStoreEvery Task A2AServer.start creates — submitted → working →
completed/failed — is read and written through a pluggable TaskStore. The default is an
in-memory map (Tasks live only for the process lifetime); FileTaskStore writes one JSON file
per Task id under a directory, atomically, so a served agent’s in-flight or suspended work
survives a restart.
When to use it
Section titled “When to use it”Reach for FileTaskStore (via a2a.store("file:<dir>")) whenever a served toolkit’s Tasks
should outlive the process — a long poll from a slow peer, or a Task that suspended on
§10 pending and is waiting on a human. For anything else, the
default in-memory store is fine and needs no configuration. Implement TaskStore yourself to
back Tasks with a database or a distributed cache instead of the filesystem.
Why this and not the alternative
Section titled “Why this and not the alternative”Examples
Section titled “Examples”1. The smallest useful call — save and read back directly
Section titled “1. The smallest useful call — save and read back directly”import io.github.muthuishere.toolnexus.*;import java.nio.file.Files;import java.util.LinkedHashMap;import java.util.Map;
public class Example { public static void main(String[] args) throws Exception { java.nio.file.Path dir = Files.createTempDirectory("toolnexus-task-store"); A2AServer.FileTaskStore store = new A2AServer.FileTaskStore(dir.toString());
Map<String, Object> task = new LinkedHashMap<>(); task.put("id", "t1"); task.put("status", Map.of("state", "working")); store.save(task);
Map<String, Object> back = store.get("t1"); if (back == null) throw new AssertionError("expected the task back"); if (!"working".equals(((Map<?, ?>) back.get("status")).get("state"))) throw new AssertionError(back); if (store.get("nope") != null) throw new AssertionError("unknown id should be null");
System.out.println("ok: " + back); }}2. The realistic case — resolveStore selects memory, file, or a custom store
Section titled “2. The realistic case — resolveStore selects memory, file, or a custom store”import io.github.muthuishere.toolnexus.*;import java.nio.file.Files;import java.util.LinkedHashMap;import java.util.Map;import java.util.concurrent.ConcurrentHashMap;import java.util.concurrent.atomic.AtomicInteger;
public class Example { public static void main(String[] args) throws Exception { // null / "memory" ⇒ the default in-memory store. A2AServer.TaskStore mem = A2AServer.resolveStore(null); if (!(mem instanceof A2AServer.InMemoryTaskStore)) throw new AssertionError(mem.getClass()); A2AServer.TaskStore mem2 = A2AServer.resolveStore("memory"); if (!(mem2 instanceof A2AServer.InMemoryTaskStore)) throw new AssertionError(mem2.getClass());
// "file:<dir>" ⇒ a FileTaskStore rooted at <dir>. java.nio.file.Path dir = Files.createTempDirectory("toolnexus-task-store"); A2AServer.TaskStore file = A2AServer.resolveStore("file:" + dir); if (!(file instanceof A2AServer.FileTaskStore)) throw new AssertionError(file.getClass());
// any TaskStore object ⇒ used as-is. AtomicInteger saves = new AtomicInteger(); Map<String, Map<String, Object>> backing = new ConcurrentHashMap<>(); A2AServer.TaskStore custom = new A2AServer.TaskStore() { @Override public Map<String, Object> get(String id) { return backing.get(id); } @Override public void save(Map<String, Object> task) { saves.incrementAndGet(); backing.put(String.valueOf(task.get("id")), task); } }; A2AServer.TaskStore resolved = A2AServer.resolveStore(custom); if (resolved != custom) throw new AssertionError("custom store used as-is");
Map<String, Object> t = new LinkedHashMap<>(); t.put("id", "t9"); t.put("status", Map.of("state", "submitted")); resolved.save(t); if (saves.get() != 1) throw new AssertionError(saves.get());
System.out.println("ok: memory, file, and a custom store all resolve correctly"); }}3. The full surface — wired into toolkit.serve, a task persisted on disk
Section titled “3. The full surface — wired into toolkit.serve, a task persisted on disk”a2a.store("file:<dir>") is the ergonomic path — every Task a served toolkit creates lands as
<dir>/<id>.json, readable outside the process.
import com.sun.net.httpserver.HttpServer;import io.github.muthuishere.toolnexus.*;import java.io.OutputStream;import java.net.InetSocketAddress;import java.net.URI;import java.net.http.HttpClient;import java.net.http.HttpRequest;import java.net.http.HttpResponse;import java.nio.charset.StandardCharsets;import java.nio.file.Files;import java.nio.file.Path;import java.util.Map;
public class Example { public static void main(String[] args) throws Exception { HttpServer llm = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); llm.createContext("/", ex -> { try { ex.getRequestBody().readAllBytes(); byte[] b = "{\"choices\":[{\"message\":{\"content\":\"filed\"}}]}".getBytes(StandardCharsets.UTF_8); ex.getResponseHeaders().add("Content-Type", "application/json"); ex.sendResponseHeaders(200, b.length); try (OutputStream os = ex.getResponseBody()) { os.write(b); } } catch (Exception ignored) { } }); llm.start(); LlmClient client = LlmClient.create(new LlmClient.Options() .baseUrl("http://127.0.0.1:" + llm.getAddress().getPort()) .style("openai").model("test-model").apiKey("test-key"));
Path dir = Files.createTempDirectory("toolnexus-serve-store"); try (Toolkit tk = Toolkit.create(new Toolkit.Options().builtins(false).skillsDir("examples/skills"))) { A2AServer.A2AConfig a2a = new A2AServer.A2AConfig().name("filing-desk").store("file:" + dir); A2AServer.ServeHandle handle = tk.serve("127.0.0.1:0", new Toolkit.ServeOptions().client(client).a2a(a2a)); try { HttpClient http = HttpClient.newHttpClient(); String req = "{\"jsonrpc\":\"2.0\",\"id\":\"1\",\"method\":\"SendMessage\",\"params\":" + "{\"message\":{\"role\":\"user\",\"parts\":[{\"kind\":\"text\",\"text\":\"go\"}]}}}"; HttpResponse<String> sent = http.send( HttpRequest.newBuilder(URI.create(handle.url() + "/")) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(req)) .build(), HttpResponse.BodyHandlers.ofString()); Map<String, Object> submitted = (Map<String, Object>) Json.toMap(sent.body()).get("result"); String id = String.valueOf(submitted.get("id"));
Path file = dir.resolve(Tool.sanitize(id) + ".json"); long deadline = System.currentTimeMillis() + 5000; while (!Files.exists(file) || !onDiskState(file).equals("completed")) { if (System.currentTimeMillis() > deadline) throw new AssertionError("task never landed on disk"); Thread.sleep(20); }
System.out.println("ok: task " + id + " persisted at " + file.getFileName()); } finally { handle.stop(); } } finally { llm.stop(0); } }
private static String onDiskState(Path file) throws Exception { Map<String, Object> onDisk = Json.toMap(Files.readString(file)); Object status = onDisk.get("status"); return status instanceof Map ? String.valueOf(((Map<?, ?>) status).get("state")) : ""; }}Fields and overloads
Section titled “Fields and overloads”| Member | Type | What it is |
|---|---|---|
TaskStore.get(id) |
Map<String,Object> |
The current Task, or null if unknown. |
TaskStore.save(task) |
void |
Persist, keyed by task.get("id"). |
InMemoryTaskStore |
class | The default — a ConcurrentHashMap, process-lifetime only. |
FileTaskStore(dir) |
class | One <sanitize(id)>.json file per Task, atomic write (temp file + move). |
resolveStore(store) |
TaskStore |
null/"memory" ⇒ in-memory; "file:<dir>" ⇒ FileTaskStore; a TaskStore object ⇒ used as-is. |
A2AConfig.store |
Object |
Any of the above, passed straight to resolveStore. |
See also
Section titled “See also”A2AServer.start— reads and writes every Task through the resolved store.A2AServer.buildAgentCard— the other half of what a served agent exposes.suspension/pending— why a served Task might need to survive a restart in the first place.