Repository navigation
v0.12.0
A feature release: a new model-call gateway, resumable SSE, one
cancellation seam across REST, MCP, jobs and agent turns, and plugins that
can be mounted into a running app. Every dependency moves to its latest
version, majors included, and the Node floor rises to 22.18.
⚠️ Breaking
Node.js 22.18 or newer
engines.node is now >=22.18 on every @agentback/* package and on
create-agentback's templates (was >=22.13). 22.18 is the first 22.x that
ships import.meta.main (backported from 24.2). @types/node is pinned to
~22.18.x, so the compiler rejects any Node API the floor does not have.
Node 22.13–22.17 users get an engines warning (a hard failure under
engine-strict). CI tests Node 22.18, 24 and 26.
✨ New
@agentback/model-gateway — the model-call seam
Language-model middleware, not a gateway.call() API: wrapModel(model, opts) returns a drop-in model, so generateText, streamText, ToolLoopAgent
and @agentback/agents all route through it unchanged. The stack is
accounting → fallback → retry → breaker → provider:
- Retry takes 408/409/429/5xx only, always jitters, and honours
Retry-After; withretryon, the gateway owns retrying, so the AI SDK does
not retry the whole stack again. - Breaker counts provider health only: a 4xx is neutral, and half-open
admits exactly one probe, whose lease expires so a stalled probe cannot wedge
the circuit. - Fallback sits outside the breaker; secondaries are called raw.
- Accounting bills tokens, not calls, including streams (tapped for their
finishpart). The principal comes from an ambientwithModelScope();
@agentback/agentsopens one per turn, andgenerate({tokenBudget})caps a
turn.
ai is an optional peer, imported lazily. See
docs/concepts/model-gateway.md.
Resumable SSE
@get(..., {streamOf, resumable: {windowMs?, maxEvents?}}) puts an id: on
every SSE frame and keeps the producer running for windowMs (default 30s)
after a disconnect. A reconnecting EventSource resumes through the standard
Last-Event-ID header with no client code, and the handler is not
re-invoked, so a browser refresh no longer throws away an in-flight agent
turn. Both hosts implement it.
- A
Last-Event-IDthat cannot be honoured (unknown, expired, trimmed, or
another route's or principal's stream) is an HTTP 409; the handler is
never re-run for it. - Live streams are capped server-wide (
rest.resumable.maxLiveStreams, default
1000): past it a new stream gets 503 +Retry-After. A resume is never
capped. - A slow reader now pauses the producer (backpressure), and
stop()ends open
streams. - The buffer is in memory, so resumption is single-process.
One cancellation seam: CoreBindings.ABORT_SIGNAL
Every entry point that owns a cancellable unit of work binds an AbortSignal
into its context, and DI carries it to anything nested: REST (client
disconnect), MCP (notifications/cancelled), callTool({signal}), and agent
turns, which forward it to the model so an abandoned caller stops the
generation instead of paying for it. Inject it with {optional: true}. A queue
job gets JobContext.signal as a field. See
docs/concepts/cancellation.md.
Job timeouts: WorkerOptions.timeoutMs
An opt-in wall-clock backstop for a job attempt. A timed-out attempt is
abandoned (its signal aborts, it is recorded failed, the seat is freed) and
never retried, even under attempts: 3. cancel() on a started job now aborts
that attempt's signal when the job runs in this process.
Plugins mount into a running app
@agentback/plugin mounts now mean bound, lifecycle-started and served, or
the mount fails and unwinds all three. Routes, MCP tools, resources and prompts
added after app.start() are served; MCP now derives tools, resources and
prompts per request, so addition and retraction are one mechanism.
installSteps in @agentback/common
An install* helper written as an async generator yields the inverse of each
step as it lands, so a failure part-way rolls back exactly the steps that ran.
A failed rollback is attached to the thrown error (rollbackFailure) instead
of being swallowed.
🐛 Fixes
@agentback/context/core: a binding that resolves tonullno longer
misaligns aContextView; one null observer binding used to crash
app.start()with aTypeError.@agentback/mcp:@toolaccepts anoutput:schema without aninput:
schema (previously a type error, which pushed tools to return JSON text and
losestructuredContent). The SDK request signal is re-expressed as an
AbortErrorwith anAbortReasonsmessage, the same shape on every host.@agentback/messaging: a handler whose ownfetchtimes out is retried
underattemptsagain; only an attempt the runtime abandoned is terminal.@agentback/agents: a streamed turn the token budget refused is a failed
turn and is not charged against quota.@agentback/core: a partial lifecycle notify resolves only the observers
it selected.
Behaviour changes
@agentback/messaging-bullmq on BullMQ 6 and ioredis 6
enqueue(q, data, {repeat})now creates a BullMQ job scheduler. BullMQ 6
removed legacy repeatable jobs. The port'srepeatoption keeps working: the
scheduler id isrepeat.key, elsejobId, else one derived from the cadence,
so re-enqueueing the same key updates the schedule in place.delayMsbecomes
the scheduler's start date. The exportedmapEnqueueOptions()no longer
returns arepeatfield.QueueAdmin.stats()counts a paused queue's jobs aswaiting, which is
how BullMQ 6 reports them. The totals are unchanged.- ioredis 6 speaks RESP3 by default. Pass
connection: {url, options: {protocol: 2}}to keep the RESP2 wire protocol.
A client passed asconnection.clientis now typed as an ioredis 6Redis. - BullMQ 6 makes
ioredisan optional peer;@agentback/messaging-bullmq
already depends on it directly, so nothing changes for consumers.
MCP @tool input guardrail accepts object intersections
Zod 4.6 lowers z.intersection(z.object(…), z.object(…)) to a single object
schema. The guardrail checks the emitted schema, so such an input: is now
accepted. An intersection Zod cannot merge (for example with a record) still
lowers to allOf and is still rejected at registration.
Packages
@agentback/model-gateway: new.@agentback/command: the optional@toon-format/toonpeer is widened to
^2.3.0 || ^4.0.0(tested against 4.1).create-agentback: new apps get TypeScript 7, vitest 5, zod 4.6 and
@types/node ~22.18. A scaffolded app runs only thetscCLI, so it does not
need the TypeScript 6 side-by-side install this repo keeps for eslint.
Dependencies
| Package | From | To |
|---|---|---|
| zod | 4.4 | 4.6 |
@modelcontextprotocol/* (v2) |
2.0 | 2.1 |
@modelcontextprotocol/ext-apps (example) |
1.7 | 2.0 (now on the v2 SDK) |
| ai | 7.0.18 | 7.0.114 |
| bullmq | 5.79 | 6.3 |
| ioredis | 5.11 | 6.0 |
| dotenv | 17 | 18 |
| nanoid | 5 | 6 |
| React (UI packages) | 19.2 | 19.3 |
| vitest | 4.1 | 5.0 |
| pnpm | 11.10 | 12.6 |
| eslint / typescript-eslint | 10.6 / 8.63 | 10.11 / 8.70 |
Peer ranges other than the toon widening are unchanged.
🛠 Repo and CI
- CI now runs
pnpm lintandpnpm check:generated(the chat dock's embedded
framework guide must matchSKILL.md). - The release workflow's version step and
pnpm cleanno longer trip pnpm's
workspace-cycle check, and the workflow's default npm dist-tag islatest.