A2A — call remote agents, or be one
An agent is just another tool source. Point toolnexus at a remote A2A agent and each of its
advertised skills becomes a tool (named <agent>_<skill>, source: "a2a"). The same toolkit can
turn around and serve itself as an A2A agent, so other agents — toolnexus or not — can call it.
It’s a genuine, minimal subset of real A2A (verified against a2a-python): JSON-RPC 2.0, the Agent
Card at /.well-known/agent-card.json, and SendMessage → poll GetTask. No streaming / push /
auth in v1.
Outbound — a remote agent’s skills become tools
Section titled “Outbound — a remote agent’s skills become tools”import { createToolkit, agent } from "toolnexus"
const tk = await createToolkit({ agents: [agent({ card: "https://peer.example.com/.well-known/agent-card.json" })],})// each advertised skill is now a tool named <agent>_<skill>, source "a2a"
await tk.addAgent("https://other.example.com/.well-known/agent-card.json") // or add one at runtimefrom toolnexus import create_toolkit, agent
tk = await create_toolkit( agents=[agent("https://peer.example.com/.well-known/agent-card.json")],)
await tk.add_agent("https://other.example.com/.well-known/agent-card.json") # or at runtimetk, _ := toolnexus.CreateToolkit(ctx, toolnexus.Options{ Agents: []toolnexus.Agent{{Card: "https://peer.example.com/.well-known/agent-card.json"}},})
tk.AddAgent(ctx, "https://other.example.com/.well-known/agent-card.json", nil) // or at runtimeToolkit tk = Toolkit.create(new Toolkit.Options() .agents(List.of(A2A.agent("https://peer.example.com/.well-known/agent-card.json", null, null, null))));
tk.addAgent(A2A.agent("https://other.example.com/.well-known/agent-card.json", null, null, null)); // runtimeawait using var tk = await Toolkit.CreateAsync(new Toolkit.Options{ Agents = new List<Agent> { new() { Card = "https://peer.example.com/.well-known/agent-card.json" } },});
await tk.AddAgentAsync("https://other.example.com/.well-known/agent-card.json"); // or at runtime{:ok, tk} = Toolnexus.create_toolkit( agents: [Toolnexus.A2A.agent(card: "https://peer.example.com/.well-known/agent-card.json")])# each advertised skill is now a tool named <agent>_<skill>, source "a2a"
Toolnexus.Toolkit.add_agent(tk, "https://other.example.com/.well-known/agent-card.json") # runtime(require '[toolnexus.a2a :as a2a] '[toolnexus.core :as tn] '[toolnexus.tool :as tool])
(def tk (tn/build {:agents [{:card "https://peer.example.com/.well-known/agent-card.json"}]}));; each advertised skill is now a tool named <agent>_<skill>, source "a2a"
;; a toolkit is a plain value here, so "add one at runtime" is adding its tools to it(def tk2 (tool/add-tools tk (a2a/agent-tools {:card "https://other.example.com/.well-known/agent-card.json"})))The agents block mirrors mcpServers — a config list you can also load from a top-level agents
key. Each descriptor takes optional headers (${ENV} expanded at call time, never logged), a poll
timeout, and pollEvery interval.
Inbound — serve your toolkit as an agent
Section titled “Inbound — serve your toolkit as an agent”The other direction: expose your toolkit as an A2A agent. The Agent Card is built from your
SKILL.md skills (never raw tools), and each inbound SendMessage is fulfilled by a client running
the tool-calling loop.
const llm = createClient({ baseUrl, style: "openai", model })const handle = await tk.serve("127.0.0.1:0", { client: llm, a2a: { name: "my-agent", store: "memory" }, // store: "memory" | "file:<dir>" | your own TaskStore})// handle.url now speaks A2A; stop with handle.stop()from toolnexus import A2AConfig
llm = create_client(base_url=base, style="openai", model=model)handle = await tk.serve( "127.0.0.1:0", client=llm, a2a=A2AConfig(name="my-agent", store="memory"),)llm := toolnexus.CreateClient(toolnexus.ClientOptions{ BaseURL: baseURL, Style: toolnexus.StyleOpenAI, Model: model,})handle, _ := tk.Serve("127.0.0.1:0", toolnexus.ServeOptions{ Client: llm, A2A: &toolnexus.A2AConfig{Name: "my-agent", Store: "memory"},})LlmClient llm = LlmClient.create(new LlmClient.Options() .baseUrl(base) .style("openai") .model(model));A2AServer.ServeHandle handle = tk.serve("127.0.0.1:0", new Toolkit.ServeOptions() .client(llm) .a2a(new A2AServer.A2AConfig().name("my-agent")));var handle = await tk.ServeAsync("127.0.0.1:0", new Toolkit.ServeOptions{ Client = ClientFor(llm), A2A = new A2AConfig { Name = "my-agent" },});llm = Toolnexus.Client.create(base_url: base, style: "openai", model: model)handle = Toolnexus.Toolkit.serve(tk, "127.0.0.1:0", client: llm, a2a: %{name: "my-agent", store: "memory"})# handle.url now speaks A2A; Toolnexus.Serve.stop(handle)(require '[toolnexus.client :as client] '[toolnexus.serve :as serve])
(def llm (client/create-client {:base-url base-url :style "openai" :model model}))
(def handle (serve/serve tk {:port 0 ; 0 ⇒ an OS-assigned port :host "127.0.0.1" :client llm :a2a {:name "my-agent"} :skills (:skills tk) ; what the Agent Card advertises :store "memory"})) ; "memory" | "file:<dir>" | your own TaskStore
;; (:url handle) now speaks A2A; stop with (serve/stop! handle)Tasks persist through a pluggable store
Section titled “Tasks persist through a pluggable store”A served agent fulfils each request as a Task. The store selects where tasks live:
"memory" (default), "file:<dir>", or your own TaskStore (get(id) / save(task)) — so a
served agent can survive restarts and be scaled across processes.
Both directions exist in all seven ports. The same serve(...) can co-mount an
MCP server endpoint, so what you assemble is consumable as an agent and as an
MCP server at once.