Skip to content

agent

JavaScript · package toolnexus · SPEC §7A · js/src/a2a.ts

function agent(opts: {
card: string
headers?: Record<string, string>
timeout?: number
pollEvery?: number
}): Agent

Build 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.

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.

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 the SendMessage/GetTask poll loop handled for you — writing that JSON-RPC round trip by hand is exactly the bug this exists to prevent.

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_TOKEN
console.log("ok: unreachable peer isolated")
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.

An Agent descriptor — { card, headers?, timeout?, pollEvery? } — the same shape accepted by createToolkit({ agents }), toolkit.addAgent(), and agentTools(). Nothing has connected yet.

  • 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