Repository navigation
v0.27.0
Rust: an ACP turn the agent never ends now fails after permission_timeout (rust only, #124).
Every other port bounds the whole session/prompt turn by the permission timeout, as SPEC §7 says
("the turn's permission timeout still applies"). Rust bounded only the host's on_permission
callback, so an agent that stalled mid-turn, or never answered, hung the turn forever. It now fails
with AcpError::PermissionTimeout after permission_timeout (default 30 s) from the prompt being sent.
A turn that legitimately runs longer than that needs a larger permission_timeout, same as in Go.
ACP: cancel a running turn in js, python, java and csharp (#124). Before, only golang and rust could
stop an ACP turn; the other ports either ignored cancellation or dropped the turn without telling the
agent, which kept working. Now a cancelled turn sends the session/cancel notification, waits up to the
request timeout for the agent to end the turn, and raises the cancellation error; the process and
session stay warm for the next turn. Per port: js generate(request, {signal}) /
generateStream(request, emit, {signal}) rejects with an AbortError; python
generate(request, cancel=event) (a threading.Event) raises the new ACPCancelledError; csharp
honours the CancellationToken it already took; java reacts to the thread interrupt that
LlmClient cancellation already uses.
- java behaviour change: an interrupted turn now throws
LlmClient.CancelledExceptioninstead of
IOException("...interrupted"). - Not done: elixir (#169) and clojure (#170). In js and python, cancelling a run of
createInProcessClientdoes not yet reach the ACP turn — pass the signal/event to the ACP client
yourself; wiring it through changes the in-process seam in every port and needs an OpenSpec change.
Tool-scheduling tests are now mutation-checked in every port (#129). Each scheduler was broken on
purpose (unsafe calls overlapping, the cap ignored, everything serial, results in completion order) and
the tests had to fail. Results-in-completion-order went unnoticed outside golang and csharp, so new tests
finish calls out of order and check each output stays in emitted order. No library behaviour changed.
Gap: in js the streaming modes are not covered by the reordering mutation.
mcpHandlerwith the 2026-07-28 opt-in no longer rejects legacy clients that send the 2026 version
header (issue #150, all eight ports). On a mount listing2026-07-28next to legacy versions, a request
carryingMCP-Protocol-Version: 2026-07-28but noMcp-Methodheader and no_metaversion was routed to
the 2026 wire and failed with HTTP 400-32020; hosts had to rewrite the header. Such a request is now
served by the legacy wire, which accepts that header value when2026-07-28is listed. A real 2026 client
always sendsMcp-Method, so it is unaffected; 2026-only mounts are unchanged.initializestill answers
the newest listed legacy version when asked for2026-07-28(that wire has noinitialize; use
server/discover), and SPEC §7C.2 and the GoProtocolVersionsdoc now say so. No hint is added to the
initializeresult: theio.modelcontextprotocol/_metaprefix is reserved by MCP.
Faster cold start for MCP in Java and C#, with no change in behaviour (Java and C# only). Java no
longer builds the MCP SDK's default JSON-schema validator (about 220 networknt classes) for every
client. It is now resolved the first time a tool result is actually validated against an output
schema. The SDK's Jackson 3 mapper is built on first use of the legacy SDK path, not when McpSource
loads, and a config with only stdio servers no longer builds an OAuth token store. C# now creates
the shared MCP HttpClient on the first remote request, so a host with only stdio servers does not
load the HTTP/TLS stack at startup. Validation, auth and the wire format are unchanged. The second
process that the Java and C# stdio probe used to spawn for older servers (the larger cost at startup)
is gone too: #167 now reuses the probe child (see the entry below). Not done: Java still loads two
Jackson stacks.
-
Benchmarks (harness only, no published numbers changed): the fake model server
(benchmarks/mock_llm.py) now speaks HTTP/1.1 keep-alive, so frameworks that pool connections
reuse them across LLM calls as they would against a real provider, instead of paying a TCP connect
per call. It also lets two frameworks that previously could not run do so: smolagents (tool results
arrive as roleuserand the agent stops only on afinal_answertool call; the mock now handles
both) and AutoAgents (the mock sentinput_tokensalongsideprompt_tokens, which AutoAgents rejects
as a duplicate field; usage now carries the OpenAI-standard keys only). The standing tables were
re-run against it on 2026-11-08 (see the docs entry below).
Faster load of pre-2026 stdio MCP servers: one process start instead of two. Until now, when a
stdio server answered theserver/discoverprobe with an error (every server older than MCP
2026-07-28 does), the client killed that process and started a second one for the old protocol. A JVM
server paid that twice (two spawns 152 ms apart in a Java profile; 35–55 ms in C#, ~20 ms in Clojure).
Now the same process carries on intoinitialize. A server that never answers the probe is still
killed and restarted aftermin(timeout, 5 s), as before. Discover-first, the 2026 path and HTTP are
unchanged; SPEC §2.2a amended. Shared fixtureexamples/mcp-2026/legacy-stdio/counts process starts.
If a port cannot hand the open process over, it respawns as before (Rust logs a warning on stderr when this happens). Port coverage and any gap:openspec/changes/reuse-stdio-discover-probe/tasks.md. -
Docs: the docs-site hero video is re-cut for 0.26.0 (80 s, new narrator). It now covers eight
languages including Rust, typed decisions (createClassifier), media generation, and serving the
toolkit over MCP, A2A and ACP. Its benchmark claims come only from the Nov 2026 standing page: lowest
in Python, Rust and Elixir, level in JavaScript and .NET, and behind in Go and on the JVM. -
Faster start in JavaScript, fewer allocations per turn in Go (javascript and golang only; no
behaviour change). Importingtoolnexusin JavaScript no longer loads code a host may never use: the
MCP server transport and@hono/node-serverload on the firstserve()with an MCP profile orasMcpServer(), the remote MCP
transports (streamable-HTTP, SSE) only when a remote server falls back to the SDK path, the
MCP 2026 client on the first MCP load, andyamlon the first skill parsed. A bare import drops
from 377 to 284 modules. Every public signature and export is unchanged (the built.d.tsfiles are
byte-identical);yamlis loaded with a synchronousrequire, solistSkills/loadSkillsstay
synchronous. Not done:buildMcpServeris synchronous and returns the SDKServer, so the SDK
server core still loads at import. In Go, the non-streaming OpenAI and Anthropic loops marshal the
tools array once perRuninstead of once per turn, when noBeforeLLMhook orBodyTransformcan
see or change it, and the MCP progress token is built withoutfmt. A test pins the request-body
bytes, and they are unchanged. Not done in Go: the request body is still amap[string]any, because
BodyTransformreceives that map; the per-callMetadatamap, the idle timer and the progress token
stay, because each is observable (returned to the caller, the §2 idle timeout, and sent on the wire). -
Docs / benchmarks: the Nov 2026 standing page gains native-tool rows for toolnexus in Python,
JavaScript, Java and C#, so .NET is finally compared like for like with Microsoft.Extensions.AI and
Semantic Kernel (which only run native tools): toolnexus native is lowest at 0.59 ms p50 with the lowest
init and RSS, a tie with Semantic Kernel inside noise. It also adds a skills row against Spring AI 2's
SkillsTool(spring-ai-agent-utils 0.13.0) over the sharedhello-worldskill: a latency tie with our
fullconfig, at about 30% of Spring's memory. Measured 2026-11-08 on a busy machine and kept apart from
the 2026-11-07 rows; the C# MCP figure from that sitting is too noisy to claim. No Python or Java
competitor has a native row in the harness yet. -
Docs / benchmarks: quiet re-run of the Nov 2026 standing page (2026-11-08, every row started at
1-minute load < 4, keep-alive mock). All per-language tables now come from one sitting of the full suite,
with toolnexusmain(the 0.27.0 candidate) and smolagents and AutoAgents, which run for the first time. A
new table puts 0.26.0 andmainside by side per port: MCP cold init drops 14–20 ms in Python, JS, Go,
Elixir, Clojure and Rust and 14 ms in C# (the stdio probe child is reused, #167; JS/Go lazy loading, #166;
C# lazyHttpClient, #171), and per-request latency is unchanged. Where we still lose: Eino in Go,
LangChain4j on the JVM, AutoAgents in Rust (new, by 8%), and C# MCP cold init against Microsoft Agent
Framework. Java did not show #171's cold-start gain in this run (init 462 vs 413 ms, one sample per
process), so the page does not claim it. .NET MCP is now ahead of Microsoft Agent Framework (0.52 vs
0.62 ms). Clojure remains the slowest port; its fix is deferred to issue #168. Raw rows are in
benchmarks/results-2026-11.jsonunderrun: 2026-11-08-quiet; earlier sittings are kept.
Security: the classifier no longer sends a default API key to a host that is not its own (issue #155).
Until now the classifier read TYPESAFE_API_KEY (or the backend's variable) and sent it as a bearer
token to whatever baseUrl pointed at, so a custom backend, or any backend with a baseUrl override,
leaked the key to a self-hosted or third-party server whenever the variable was set. It now follows the
§8 client rule: a caller-named apiKeyEnv goes where the caller says; the backend's own variable goes
only to that backend's own https host (api.typesafe.ai, openrouter.ai and subdomains,
ai-gateway.vercel.sh, api.cloudflare.com); custom has no default key. Anything else sends no
Authorization (no error: keyless servers stay legal, and headers still work). All eight ports
carried the same flaw. Upgrade note: a self-hosted endpoint that relied on the implicit
TYPESAFE_API_KEY must now name apiKeyEnv (or send a header). Shared fixture
examples/judge/backends/key-routing.json.
Cloudflare classifier errors now say why (issue #156). A failed Workers AI envelope reports
classifier: cloudflare: envelope not successful: 2021 Insufficient balance (errors[0] code and
message, redacted and capped like any backend cause) instead of the fixed text, so a missing AI Gateway
balance is distinguishable from a bad model id. All eight ports; fixture cloudflare-replies.json.
Change: openspec/changes/fix-classifier-key-and-errors.
A declined durable resume now tells the model why (issue #125). Resuming a halted run with
answerDeclined(id, reason) through runWithAnswer / askWithAnswer used to fill every call's slot
with "no result supplied on resume for ", so the reason the human gave never reached the
model. Each outstanding call now gets the error result relay declined for <name>: <reason> (reason
defaults to declined), the same text the inline waitFor path already produced, so an answer means
the same thing whether the host waited inline or durably. SPEC §10; fixture
conformance/resume_decline/cases.json. Only golang and rust ship the durable resume entry point, so
only they were affected and fixed; the other six ports have no durable resume yet and must replay the
same fixture when they add one. Change: openspec/changes/archive/2026-11-07-fix-resume-decline-reason.
Oversized skill descriptions are now reported (issue #84). The skills catalog sends each
description in full on every turn, and nothing told the author when one was huge: a 4,443-character
description is cut to ~1,536 by Claude Code but paid in full here. The loader result and the
listSkills inventory now carry warnings, next to skipped: one
{ location, reason: "oversized-description", chars } per skill whose description is longer than
1,024 Unicode code points (the published Agent Skills limit; exactly 1,024 does not warn). Each port
also logs it through the channel it already uses for duplicate skill names, never stdout. Nothing is
truncated: the catalog and the skill tool output are byte-identical to before. All eight ports;
fixture conformance/skill_description/. Not done: the threshold is fixed, not configurable.
Change: openspec/changes/archive/2026-11-07-add-skill-description-length-warning.
Docs: a new "Where toolnexus stands — Nov 2026" page (Benchmarks & comparison). It has a feature
matrix re-checked against official docs on 2026-11-07, covering MCP client/server, MCP 2026-07-28, A2A,
skills, ACP, judges, media and languages. Cells we could not confirm say unverified. It also has new
fake-model overhead benchmarks with pinned versions for 53 framework configurations in eight languages,
including Rust (vs rig and swiftide) and new rows for Microsoft Agent Framework, Agno, OpenAI Agents
JS, LangGraph.js, Google ADK Go/Java, Genkit Go and Koog. The page says where we lose: Eino in Go,
LangChain4j on the JVM, a tie in .NET, and Clojure as our slowest port. It ends with a ranked list of
gaps. Raw results are in benchmarks/results-2026-11.json, and the August results.json is kept as
it was. The page also lists what we could not run and why: smolagents and AutoAgents fail on the
shared mock's wire format, and Google ADK TS has no OpenAI-compatible model. The July comparison page
now points to the new one.