agent
JavaScript · package toolnexus · SPEC §7A · js/src/a2a.ts
function agent(opts: { card: string headers?: Record<string, string> timeout?: number pollEvery?: number}): AgentBuild an Agent descriptor — a URL to a remote peer’s Agent Card, plus how to talk to it. On its
own it does nothing; hand it to createToolkit({ agents: [...] })
or toolkit.addAgent(...) and every skill the peer advertises becomes a Tool your model can call.
When to use it
Section titled “When to use it”Use agent the moment another toolnexus toolkit — or any real A2A peer — is running somewhere
else (a different process, a different machine, a different language) and you want its
capabilities to show up in your model’s tool list, indistinguishable from a local tool.
Why this and not the alternative
Section titled “Why this and not the alternative”Prefer agent + createToolkit({ agents: [...] }) when:
- You want the peer’s skills merged with your MCP servers, local skills and own functions in one flat list, with the same name-collision precedence as every other source.
- You want lifecycle for free — A2A tools hold no live connection, but the toolkit is still the
one place your model’s whole tool surface comes from, and
tk.close()is still the one call that tears everything down. - You want
${ENV}header expansion and theSendMessage/GetTaskpoll loop handled for you — writing that JSON-RPC round trip by hand is exactly the bug this exists to prevent.
Examples
Section titled “Examples”1. The smallest useful call — build a descriptor, nothing connects yet
Section titled “1. The smallest useful call — build a descriptor, nothing connects yet”agent() is pure data assembly. No HTTP happens until something resolves it (a toolkit build, or
agentTools).
import assert from "node:assert"import { agent } from "toolnexus"
const reviewer = agent({ card: "http://127.0.0.1:9/.well-known/agent-card.json", pollEvery: 50 })
assert.equal(reviewer.card, "http://127.0.0.1:9/.well-known/agent-card.json")assert.equal(reviewer.pollEvery, 50)assert.equal(reviewer.timeout, undefined, "defaults are applied later, at resolution")
console.log("ok:", reviewer.card)2. A real, hermetic round trip — two local toolkits talking A2A
Section titled “2. A real, hermetic round trip — two local toolkits talking A2A”One toolkit serves itself as an A2A agent on an ephemeral local port
(startA2AServer under the hood, via toolkit.serve); a second
toolkit points agent() at that server’s own card URL and calls it like any other tool. Both sides
run in this one process — nothing leaves 127.0.0.1.
import assert from "node:assert"import http from "node:http"import { agent, createClient, createToolkit } from "toolnexus"
// A stub LLM the SERVED side's client talks to — canned, no tool calls.const llm = http.createServer((_req, res) => { res.writeHead(200, { "content-type": "application/json" }) res.end(JSON.stringify({ choices: [{ message: { content: "REVIEWED: looks good" } }], usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, }))})await new Promise<void>((r) => llm.listen(0, "127.0.0.1", r))const llmPort = (llm.address() as any).port
// Side A: a toolkit serving itself as the "reviewer" A2A agent.const served = await createToolkit({ skillsDir: "./examples/skills", builtins: false })const client = createClient({ baseUrl: `http://127.0.0.1:${llmPort}`, style: "openai", model: "x", apiKey: "k" })const srv = await served.serve("127.0.0.1:0", { client, a2a: { name: "reviewer", skills: ["hello-world"] } })
// Side B: a caller toolkit pointing agent() at Side A's card.const caller = await createToolkit({ builtins: false, agents: [agent({ card: srv.url + "/.well-known/agent-card.json", pollEvery: 10 })],})
try { const tool = caller.get("reviewer_hello-world") assert.ok(tool, "the peer's skill shows up as a local-shaped tool") assert.equal(tool!.source, "a2a")
const res = await caller.execute("reviewer_hello-world", { task: "review this PR" }) assert.equal(res.isError, false) assert.equal(res.output, "REVIEWED: looks good")
console.log("ok:", res.output)} finally { await caller.close() await srv.stop() await served.close() llm.close()}3. The full surface — headers, timeout, pollEvery, and a failing peer isolated
Section titled “3. The full surface — headers, timeout, pollEvery, and a failing peer isolated”${ENV} in headers expands at call time from the environment and is never logged. A peer that
never resolves (bad card URL) doesn’t take the whole toolkit down — it just contributes zero tools.
import assert from "node:assert"import { agent, createToolkit } from "toolnexus"
process.env.TN_DOCS_TOKEN = "secret-value"
const reachable = agent({ card: "http://127.0.0.1:9/.well-known/agent-card.json", // nothing listening — resolution fails headers: { Authorization: "Bearer ${TN_DOCS_TOKEN}" }, // expanded at call time, never logged timeout: 5_000, // overall poll budget, ms (default 300000) pollEvery: 250, // GetTask interval, ms (default 1000)})
// A failing agent is isolated — like a failing MCP server — never fatal to the whole build.const tk = await createToolkit({ builtins: false, agents: [reachable] })assert.deepEqual(tk.tools(), [], "an unreachable peer contributes zero tools, doesn't throw")
await tk.close()delete process.env.TN_DOCS_TOKENconsole.log("ok: unreachable peer isolated")Options
Section titled “Options”| Field | Type | What it does |
|---|---|---|
card |
string |
URL of the peer’s /.well-known/agent-card.json. Required. |
headers |
Record<string, string> |
Sent on every request. ${ENV_VAR} values expand at call time, never logged. |
timeout |
number |
Overall poll budget in ms. Default 300000. |
pollEvery |
number |
Interval between GetTask polls in ms. Default 1000. |
What you get back
Section titled “What you get back”An Agent descriptor — { card, headers?, timeout?, pollEvery? } — the same shape accepted by
createToolkit({ agents }), toolkit.addAgent(), and agentTools(). Nothing has connected yet.
See also
Section titled “See also”agentTools— Expand a remote agent card into one tool per advertised skill.parseAgentsConfig— Declare remote peers in config the way MCP servers are declared, with precedence rules.startA2AServer— the inbound side: expose a toolkit as a peer