Agent SDK deep divev0.25.0 to v0.29.2April 7 to April 22, 2026

The April 2026 Claude Code update from inside an app that ships on top of it

Most write-ups of the April 2026 Claude Code release recap the official launch post: Opus 4.7 in the CLI, Visualizations, Cowork going GA, Managed Agents in beta. The other half of the story is the agent SDK that powers all of it. @agentclientprotocol/claude-agent-acp jumped from v0.25.0 on April 7 to v0.29.2 on April 20, and downstream apps had two weeks to absorb a model alias rename, a dynamic models payload, an exposed compaction stream, and at least nine session event types the stock agent silently consumes and drops.

Fazm is a consumer Mac app that runs this exact SDK as a subprocess for every paying user. This page is the story of what we patched and why.

M
Matthew Diakonov
11 min read
4.8from App Store reviews
Ships the v0.29.2 agent SDK in production
267-line patch live in /Users/matthewdi/fazm/acp-bridge/src/patched-acp-entry.mjs
9 dropped event types re-emitted to the consumer UI

Two updates, one month, one SDK rev

The CLI got the headlines. The npm package did the work. Every change a downstream app had to make in April traces back to a single tarball.

Try Fazm on macOS

The package nobody talks about

When you run claude-code in a terminal, what actually starts is a Node subprocess running this package. When you embed Claude Code in another app, you import the same package. The bin script is a thin entrypoint; the real surface is ClaudeAcpAgent exported from dist/acp-agent.js.

Anthropic shipped four minor versions in April: 0.26.0 on April 8, 0.27.x in the second week, 0.28.0 around April 15, and 0.29.2 on April 20. The first one Fazm absorbed was v0.25.0 on April 7. The big jump was v0.29.2.

claude-code-update.sh

How the bridge sits between the agent and the app

The Mac app does not talk to Anthropic. The Mac app talks to the agent, the agent talks to Anthropic. The patch sits between them and rewrites a few of the messages on the way back.

Fazm bridges the patched agent to the consumer UI

User chat input
Accessibility tree
MCP tools
patched-acp-entry.mjs
Streaming UI
Cost meter
Status badges

The 267-line patch, in full

The whole thing is one file: acp-bridge/src/patched-acp-entry.mjs. It imports the agent class directly from the dist path, wraps two prototype methods, and calls runAcp() at the bottom so the rest of the SDK behaves identically.

acp-bridge/src/patched-acp-entry.mjs

The nine dropped events

The stock ClaudeAcpAgent reads items off the SDK query iterator. Items it does not recognize as plain text or tool calls get consumed and dropped. Here is the full list as of v0.29.2.

1

compact_boundary

Emitted when the SDK rolls older messages into a compacted summary to stay under the model's context window. Trigger is auto, manual, or pre_compact and pre_tokens carries the size of the rolled-up window.

2

status

High-level lifecycle states the agent transitions through: starting, processing, finalizing, idle. Useful for swapping a generic spinner for an actual phase indicator.

3

task_started

Fires when the agent decomposes a request into one or more named subtasks. Carries task_id and a human-readable description that you can surface in the UI.

4

task_notification

Per-task progress: status (running, complete, error) and a free-form summary. Pairs with task_started for live subtask reporting.

5

api_retry

Fired when the underlying Anthropic API call returns a 429, 503, or transient error and the SDK is about to retry. Carries httpStatus, attempt, max_retries, and retry_delay_ms. Without this, your app looks frozen during a retry storm.

6

rate_limit_event

Distinct from api_retry. Carries the structured rate-limit info from the Claude account itself: status, resetsAt, rateLimitType, utilization, overageStatus. This is how an embedding app shows "you have 12 minutes until your weekly cap resets" instead of a generic 429.

7

tool_progress

Long-running tools (Bash, browser_navigate, MCP tool calls) emit elapsed_time_seconds heartbeats so the client can keep its watchdog alive. Without this you cannot distinguish a slow tool from a hung one.

8

tool_use_summary

After a sequence of related tool calls, the SDK emits a one-line summary plus the preceding tool_use_ids. This is the data you would otherwise have to synthesize by tailing logs.

9

compaction stream chunks

The SDK streams the compacted summary as content_block_delta items of type compaction_delta. Forwarding these lets the UI show the in-flight summary the way it would show a normal assistant response.

A query, end to end

Here is what one query looks like across the wire when the patch is in place. Notice that the cost field, the compaction boundary, and the rate-limit event all reach the client.

One Fazm chat turn through the patched v0.29.2 agent

Fazm UIBridge (Swift)patched-acpAnthropic APIuser messagesession/promptmessages.create (Opus 4.7)stream: text + tool_usesessionUpdate: text deltasstream: rate_limit_eventsessionUpdate: rate_limit (forwarded)stream: tool_progress (90s elapsed)sessionUpdate: tool_progress (forwarded)stream: result (cost=$0.0184)prompt() result + _meta.costUsdrender assistant + cost meter tick

Cost in dollars, captured at the source

The Claude SDK puts a per-result total_cost_usd field on the result item. The stock agent reads the result for its assistant text and discards the rest. The patched session.query.next stashes the field on the session, computes the delta against the cumulative session cost, and the patched prompt() attaches it to its return value. Below is the actual log line shape Fazm writes for every query.

patched-acp cost log
0lines in the patch
0dropped events re-emitted
0minor SDK versions in 13 days
0Fazm production releases shipped in April on top of it

What this looks like in production

Numbers that come straight from the patched agent on a paying user's Mac, not from a benchmark.

Per-query cost

$0

Typical Opus 4.7 turn with cached context. Captured from item.value.total_cost_usd.

Cache read tokens

0

The accessibility tree and prior turns get read from cache, not re-billed.

Tool watchdog

0s

Default per-tool wall-clock budget. MCP tools get 120s, internal tools get 10s.

Stock agent vs the patched bridge

A practical diff of what the embedding app sees with and without the patch in v0.29.2. None of these are SDK bugs; they are just events the stock ClaudeAcpAgent chooses not to forward.

FeatureStock claude-agent-acp v0.29.2Patched (Fazm)
Per-query USD costConsumed and dropped from item.value.total_cost_usd; never reaches the clientCaptured in patched session.query.next; attached as _meta.costUsd to prompt() result
Compaction visibilitySilent. The UI sees a long pause between assistant messagescompact_boundary + compaction_delta both forwarded as sessionUpdate so the UI can render the summary
API retry feedbackLogged to stderr only; the client sees no eventapi_retry forwarded with httpStatus, attempt, max_retries, retry_delay_ms
Rate-limit metadatarate_limit_event item type emitted by SDK but not forwarded by agentForwarded with utilization, overageStatus, isUsingOverage, resetsAt, surpassedThreshold
Long-tool progresstool_progress dropped; client cannot tell slow from hungForwarded with elapsed_time_seconds so a watchdog can keep the spinner alive
Subtask reportingtask_started and task_notification droppedBoth forwarded with task_id, status, summary
Tool timeout safetyNo wall-clock budget; a hung MCP tool can block the entire sessionPer-tool budget (10s internal / 2 min MCP / 5 min default) with synthesized error to unblock the model
Opus model alias"opus" no longer accepted in v0.29; user preferences silently breaknormalizeModelId in ShortcutSettings.swift maps "opus" -> "default" on load

Architecture decisions that survived the bump

A few choices in the bridge made the v0.25 to v0.29 jump painless. Most of them predate the SDK bump by months.

Long-lived subprocess, not request-per-turn

The agent runs as a Node child process spawned by the Mac app at launch. It stays alive across queries, owns conversation history, and never re-replays prior turns. Killing it requires kill(-pid) because the SDK creates its own process group.

JSON-RPC over stdio, not HTTP

Every message between Fazm and the agent is a single line of JSON on stdin/stdout. No localhost server, no port collisions, no auth tokens to manage. The native side parses with createInterface from readline.

Per-tool wall-clock timeouts

The patched bridge tracks every running tool and enforces a wall-clock budget: 10s for internal tools, 2 min for MCP tools, 5 min for everything else. When the budget elapses the bridge synthesizes a tool_use error so the model can recover and the UI unblocks.

9 dropped event types, re-emitted

compact_boundary, status, task_started, task_notification, api_retry, rate_limit_event, tool_progress, tool_use_summary, and compaction_delta. None of these reach the client through the stock agent. All nine are re-emitted as ACP sessionUpdate notifications by the patch.

Cost in USD, captured at item.value.total_cost_usd

Per-query USD cost is on the result item but consumed and dropped by the stock agent. The patched session.query.next stashes it on the session, the patched prompt() attaches it to the return value, and the Swift bridge writes it into APIClient.recordLlmUsage along with input/output/cache token counts.

The rollout, day by day

Anthropic's own April calendar and the Fazm release cadence overlapped almost one-for-one. Here is the SDK side of it.

1

April 7 - claude-agent-acp v0.25.0

First ACP bump in April. Improved error handling for credit exhaustion and rate limits. Onboarding stopped silently failing when new users ran out of built-in credits and now showed an actual prompt to connect a personal Claude account or skip. Fazm shipped this as v2.1.2 the same day.

2

April 8 to 19 - v0.26.x to v0.28.x

Three intermediate minor versions. Internal SDK refactors that changed how session events are forwarded. The dropped-events list grew from 4 types to 9 over this window. None of them shipped to Fazm production individually because the patch surface was still moving.

3

April 20 - claude-agent-acp v0.29.2

The big one. Models picker becomes dynamic via the models_available notification. The Opus alias renames from "opus" to "default". Compaction is exposed as a stream. Fazm shipped this as v2.4.0 the same day along with custom MCP server support and a new patched-acp-entry.mjs that handles all 9 dropped event types.

4

April 22 - downstream cleanup as v2.4.1

No new SDK release. Fixed the partial-models payload bug (Smart label drifting to Sonnet for users without Opus access during a rollout window), fixed the paywall blocking users mid-onboarding because of the new dynamic-credits state, and fixed a revoked-sign-in retry loop that came from the new auth-required notification path in v0.29.

Pinned dependencies, end of April

The full set of npm pins that ship inside the Fazm app bundle as of April 22, 2026.

@agentclientprotocol/claude-agent-acp@0.29.2@playwright/mcp@0.0.68node v22 (bundled)ws@8.20.0zod@4macOS 13+Apple Silicon + Intelstdio JSON-RPC

Why the model alias rename matters

Before v0.29 you sent modelId: "opus" and got the latest Opus. After v0.29 you send "default" and the agent picks the latest Opus your account has access to. Any app that stored the user's preference as the literal string "opus" in UserDefaults silently broke until it migrated.

Fazm's migration is one function:

Desktop/Sources/FloatingControlBar/ShortcutSettings.swift

The unreleased CHANGELOG.json line for the next Fazm version is literally about this exact migration: "Fixed Smart (Opus) model preference not persisting after app update, now correctly maps stored "opus" to the new ACP model ID."

The takeaway

The April 2026 Claude Code update is two things stitched together: a model and CLI refresh that everyone wrote about, and an agent SDK rewrite that no one wrote about. For an embedding app, the second one is the one that matters.

If you embed @agentclientprotocol/claude-agent-acp in your own product, the 267-line patch in acp-bridge/src/patched-acp-entry.mjs is a copy-paste starting point. If you just want a Mac app that already runs it, install Fazm.

Embedding Claude Code in your own app?

Book 20 minutes and we'll walk through the patch, the timeouts, and what we learned absorbing four SDK bumps in two weeks.

Book a call

Frequently asked questions

What actually shipped in the Claude Code update in April 2026?

Two things, and they are easy to confuse. The first is the consumer-facing CLI: Claude Code the terminal app got the Opus 4.7 model on April 16, the new Visualizations renderer, Claude Cowork going GA on macOS and Windows, and Managed Agents in public beta. The second, and the one this page focuses on, is the agent SDK that powers all of it: @agentclientprotocol/claude-agent-acp jumped from v0.25.0 on April 7 to v0.29.2 on April 20. That is four minor version bumps in 13 days, and every one of them changed how a downstream app embeds the agent.

What is @agentclientprotocol/claude-agent-acp and why does it matter for the April 2026 update?

It is the npm package that implements the Agent Client Protocol on top of Anthropic's Claude SDK. When you run claude-code in a terminal, it spawns this package as a JSON-RPC subprocess and talks to it over stdio. Any app that wants to embed Claude Code as an agent (not just call the API) imports this package and runs it. Fazm imports it directly: see the dependencies block in /Users/matthewdi/fazm/acp-bridge/package.json, which pins "@agentclientprotocol/claude-agent-acp": "^0.29.2" and resolves to the v0.29.2 tarball in package-lock.json. The April 2026 update is mostly a rewrite of how this package surfaces session events.

What is the dropped-events problem in v0.29 and what events get dropped?

The stock @agentclientprotocol/claude-agent-acp builds a Claude SDK query iterator and pulls items off it inside ClaudeAcpAgent. Items it does not recognize as plain text or tool calls get silently consumed and dropped. In v0.29 the Claude SDK started emitting nine types of items the agent does not forward: type="system" with subtypes compact_boundary, status, task_started, task_notification, and api_retry; type="rate_limit_event"; type="tool_progress"; type="tool_use_summary"; and type="stream_event" with a content_block of type="compaction". From inside a consumer app this is invisible until your users complain that the spinner stopped moving for 90 seconds during a compaction or that they can't tell why a tool call is taking forever. The fix is to monkeypatch session.query.next and re-emit the dropped items as ACP sessionUpdate notifications.

How do you actually monkeypatch the Claude Code agent in production?

You don't import @agentclientprotocol/claude-agent-acp from the package root, because that auto-runs the agent. You import the inner module the package's bin script imports, then wrap the prototype methods. Fazm's patch lives at acp-bridge/src/patched-acp-entry.mjs (267 lines). It does: import { ClaudeAcpAgent, runAcp } from "@agentclientprotocol/claude-agent-acp/dist/acp-agent.js", saves the original createSession, replaces ClaudeAcpAgent.prototype.createSession with a wrapper that intercepts the query iterator, and finally calls runAcp() to start the agent normally. The Swift side passes -e patched-acp-entry.mjs to node instead of the default bin. The patched entrypoint is bundled into the .app at build time.

Where is the per-query USD cost in the v0.29 SDK and why isn't it surfaced by default?

It lives on the SDK result object as item.value.total_cost_usd when item.value.type === "result" and subtype === "success". The stock @agentclientprotocol/claude-agent-acp consumes the result item to get the assistant's last text but throws the cost field away. Fazm captures it inside the patched session.query.next: it reads item.value.total_cost_usd, subtracts the previous session cost (stored as session._sessionCostUsd), and stores the delta as session._lastCostUsd. The patched ClaudeAcpAgent.prototype.prompt then attaches a _meta.costUsd field to its return value. From there the Swift bridge writes it into APIClient.recordLlmUsage(...) along with input/output/cache token counts. Per-query cost down to the cent is the difference between knowing whether your built-in credits will last the trial and finding out at the paywall.

What changed about the Opus model identifier in v0.29?

Before v0.29, when you wanted Opus you sent modelId: "opus" in the session/setModel call. After v0.29 the SDK accepts "default" as the alias for the latest Opus, and "opus" is no longer a valid alias on its own. Any app that stored the user's model preference as the literal string "opus" silently broke until it migrated. Fazm's migration is one function, normalizeModelId at /Users/matthewdi/fazm/Desktop/Sources/FloatingControlBar/ShortcutSettings.swift line 174, which does: if modelId.contains("opus") { return "default" }. The unreleased CHANGELOG.json line for the next version is literally about this exact migration.

What is the dynamic models_available payload and why did it ship in this update?

Before v0.29, an embedding app shipped a hard-coded list of supported Claude models and updated it on every Anthropic release. After v0.29 the agent emits a session/update notification with method models_available containing the full list of models the user's account has access to, scoped to whatever credit pool they are using. The downstream app builds its model picker from that payload at runtime. The Fazm release notes for v2.4.0 on April 20 say it directly: "Available AI models now populate dynamically from the agent, so newly released Claude models appear without an app update." The handler is at ACPBridge.swift line 1131 (case "models_available"). When Anthropic flipped the switch on Opus 4.7 on April 16, the new model showed up in the picker the same day with no app update required.

What is the partial models_available bug that v2.4.1 fixed?

During the rollout window of a new Anthropic release, the SDK occasionally returns a subset of models in models_available, for example only Sonnet during the hour before Opus comes back online for a region. Fazm has a substring-matched family table at ShortcutSettings.swift that maps modelId.contains("opus") to the Smart label, modelId.contains("sonnet") to Fast, and modelId.contains("haiku") to Scary. The first version of the labeler walked the returned list and assigned Smart to whatever model came back ranked highest. For an Opus-less payload that briefly meant Smart pointed at Sonnet. v2.4.1 on April 22 anchored Smart to the opus substring so a payload with no opus model now leaves Smart undefined rather than reassigning the label.

How does the Agent Client Protocol differ from just calling the Anthropic API?

The Anthropic API is request and response: send messages, get a single completion back. The Agent Client Protocol is a bidirectional JSON-RPC session: the agent owns conversation history, tool calling, planning, compaction, and rate-limit handling. The client is the embedding app and it gets streaming sessionUpdate notifications as the agent works. The client never re-sends history; the agent's session is the source of truth. That is why ACPBridge.swift uses one persistent session per Fazm chat thread and never injects prior turns into the system prompt. The per-query Anthropic API calls are an internal detail of the agent and the cost is the aggregate across however many tool-use rounds happened.

Why is Fazm using accessibility APIs and not screenshots if the new Claude has Visualizations?

Visualizations is an output feature. It lets Claude render charts and diagrams inside its own response. It does not change how the agent reads what is happening on your Mac. The input side is still the agent's job, and a screenshot pipeline pays the OCR and re-segmentation cost on every turn. Fazm hands the agent the actual macOS accessibility tree (AXUIElement structures from the Accessibility API) for whatever app is in focus. That tree is structured text, not pixels. When Anthropic ships a smarter Opus, the input quality stays the same; only the model gets better at acting on it. A Visualizations chart in the response is rendered the same way regardless.

Where can I read the patched ACP entry source?

It is in the Fazm repo at acp-bridge/src/patched-acp-entry.mjs. The file is 267 lines, MIT-style, and the structure is small enough to copy into your own embedding app: import ClaudeAcpAgent and runAcp from the dist/acp-agent.js path, save originalCreateSession = ClaudeAcpAgent.prototype.createSession, replace it with a wrapper that calls the original then intercepts session.query.next, then call runAcp() at the bottom. The Swift bridge that spawns it is Desktop/Sources/Chat/ACPBridge.swift; look at the spawn logic around line 532 (acpEntry = join(__dirname, "patched-acp-entry.mjs")) for the wiring.