npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@silverprotocol/claude-agent-sdk

v0.12.11

Published

AgJSON normalizer for the Claude Agent SDK (SDKMessage → AgEvent[]) — the live path.

Readme

@silverprotocol/claude-agent-sdk

AgJSON normalizer for the Claude Agent SDK (SDKMessage → AgEvent[]) — the live path. It sits on top of your Claude Agent SDK run and translates its native event stream into framework-neutral AgJSON; it doesn't replace or wrap the SDK you already use.

Install

npm install @silverprotocol/claude-agent-sdk @anthropic-ai/claude-agent-sdk

@silverprotocol/core is a required peer (pulled in automatically). @anthropic-ai/claude-agent-sdk is the Claude Agent SDK itself — you need it to produce the stream. (The normalizer guards SDKMessage structurally, so if you only ever feed it already-captured events it's just a type-level peer.)

Upstream compatibility

Supported peer range: @anthropic-ai/claude-agent-sdk >=0.2.76 <0.4 (npm-enforced, optional — this package never imports the SDK).

Wire shapes outside the verified set never crash the normalizer, and the fixture-drift gate (scripts/check-fixture-drift.mjs) turns every newly-appeared upstream field into a tracked triage disposition rather than an unnoticed drop — carried losslessly where a carry channel exists (see sdk-surface.json for this facet's per-field dispositions, including its disclosed silently-dropped gaps).

| @anthropic-ai/claude-agent-sdk | verified | with | evidence | | --- | --- | --- | --- | | 0.3.292 | 2026-10-07 | 0.12.4 | peer sweep 0.3.280 -> 0.3.292 (bundled CLI 2.1.280 -> 2.1.292) with the facet's @anthropic-ai/sdk devDependency 0.93.0 -> 0.131.0, which alone decides the facet's types (the agent SDK takes it as a peer). SDKStartupFailureReason gains provider_not_allowed, classified not retriable. The newer Anthropic types surface a latent emission defect, fixed in this cohort: a tool_result image with a file source emitted a url source with no url, and now maps to AgSource's file arm. Live: echo-sonnet55 (claude-sonnet-5-5) captured green with the same event shape as echo-sonnet5; system/init permissionMode stays "default". Census: four new 0.3.292 fields (system/init per_turn_effort_active and view_mode; result usage.fallback_credit, first seen null; result safety_stops, first seen 0) are allowlisted with no AgJSON home; carry of the last two is decided when a non-null capture lands. | | 0.3.280 | 2026-09-23 | 0.6.3 | peer sweep 0.3.272 -> 0.3.280 (SEVEN published versions 0.3.273-0.3.278 and 0.3.280; 0.3.279 has an upstream CHANGELOG entry but was never published to npm; bundled CLI 2.1.272 -> 2.1.280). SDKMessage union UNCHANGED at 39 members and SDKAssistantMessageError unchanged at 13; sdk.d.ts diff 900 lines; agentSdkTypes.d.ts and extractFromBunfs.d.ts byte-identical. claude-opus-5-5 REQUIRES this version: older CLIs still send the id to the API but canonicalise it to claude-opus-5, so they price it at Opus 5 rates, default its effort to high instead of medium and report canonicalModel claude-opus-5. HEADLINE - CL-09, a wrong mapping that predates the cohort, LIVE-CONFIRMED with a deliberately invalid key: query() yielded an assistant frame with error authentication_failed and then a success result with is_error:true and api_error_status 401. Through 0.6.2 that pair produced turn.error AND THEN turn.done{success} on the same turnId, violating the one-close invariant, and reduce() folded the turn as a SUCCESS whose result was 'Failed to authenticate. API Error: 401 API key is invalid.' From 0.6.3 the assistant error frame stashes its close and the result frame emits exactly one turn.error, now carrying the result's usage (so a long agentic turn that fails on round N keeps the cost of its earlier rounds), on either result arm - which also removes a second double close, assistant error followed by an error-subtype result. A success result with is_error:true and NO preceding assistant error frame, which 0.6.2 closed as turn.done{success}, now closes as turn.error {code: api_error_code ?? 'api_error', retriable only for status 429 or >= 500, usage}; a stored 0.6.2 turn of that shape cannot be recovered from the AgJSON alone (its result is the error text). Flush emits a stashed close with no usage if no result arrives; the first error frame wins. Folding the same live frames: 0.6.2 -> [turn.error, turn.done{success}], folded success; 0.6.3 -> [turn.error authentication_failed, retriable false], folded error. CARRIES: usage_report (fixture-only), the undeclared api_error / api_error_params / api_error_code triad, result-meta apiErrorCode and startupFailureReason (retriable false except worktree_unverified), and the undeclared decision_reason_code on permission denials. 0.3.277 changed a carried value without a marker: total_cost_usd and modelUsage now continue across resume and fork (see the SDKResultMessage note). THINKING: with thinking.display unset, CLI 2.1.280 runs its connector_text selector, in which the API returns only between-tool narration and never a thinking summary; at most an empty thinking block carrying only its signature reaches the stream (the same selector exists in CLI 2.1.272, first-party path only). The 0.6.2 refresh of partials-fable51 therefore lost the corpus's only live thinking_delta frames - two EMPTY ones: no capture had ever carried summary text - and three 0.6.3 re-captures confirmed it is systematic, not chance. A new scenario knob (thinkingDisplay) sets thinking {type: adaptive, display: summarized}, and the new thinking-fable51 seed provides streamed summary text for the first time, deterministically: 13 thinking_delta frames, all with summary text, plus a signature_delta. The SDK typed a third display value, 'highlights', but only on setMaxThinkingTokens and its control request, where the API accepts it for Anthropic-hosted sessions only. LIVE at 0.3.280: echo-, partials- and app-update-sonnet5 and echo- and partials-fable51 refreshed; app-update-fable51 DELIBERATELY HELD at its 0.3.272 capture, as the corpus's only live narration_block_indexes evidence (three 0.3.280 re-captures produced no narration); NEW thinking-fable51, echo-, partials- and app-update-opus55, and echo-opus5 (Opus 5 is also a Claude Code 2.1.280 refusal-fallback target: bio and frontier_llm refusals fall back to it, cyber refusals to claude-opus-4-8). Census, after registering init mcp_servers[].source and plugins[], the stream-event cache_creation twins and the claude-opus-5-5 modelUsage family: ZERO drops / ZERO new fields on every claude seed. A replay surface guard now fails a refresh of any of the six single-surface seeds it names (thinking-fable51, app-update-fable51, thinking-gemini38, tool-error-gemini38, echo-gpt6sol/openai, app-spec-structured-result/openai) that drops the surface the seed exists for. Facet tests 168 -> 204. | | 0.3.272 | 2026-09-15 | 0.6.2 | peer sweep 0.3.261->0.3.272 (NINE published versions: 0.3.263/265/266/267/268/269/270/271/272; 0.3.262 and 0.3.264 were never published to npm; bundled CLI 2.1.261->2.1.272). 0.3.271->0.3.272 is .d.ts-byte-identical across all four declaration files, so the type audit was derived against 0.3.271 and carries forward unchanged. SDKMessage union UNCHANGED at 39 members (drift-gate-verified); sdk.d.ts diff 610 lines, independently re-derived from both tarballs; agentSdkTypes.d.ts and bridge.d.ts byte-identical. DECLARED stream-scope additions, all three now CARRIED: resume_reason (0.3.268) on SDKAssistantMessage, SDKPartialAssistantMessage and BOTH result arms -> the THIRD leg of the user_message_uuid turn-binding family, which 0.3.269 stamps under ONE rule ('first complete assistant message as well as first stream event when partial messages are on'), so it rides the SAME bag under the SAME once-per-message flag (renamed userMessageUuidCarried -> turnBindingCarried); written outside that guard it would double-emit message.metadata on every streamed lifecycle, and a regression test pins the single emission. result_index (0.3.268, both result arms) -> ext.anthropic.result-meta.resultIndex, guarded typeof==='number' because 0 is the first result of every run; a gap in the sequence is how a consumer detects a LOST result, which is why it is carried rather than allowlisted. local_command (0.3.268, SUCCESS arm only, undocumented upstream) -> result-meta.localCommand, read through the JSON boundary because the union declares no such property; its PRESENCE is the signal (it is the only thing separating a zero-round-trip slash-command success from an empty turn) while its value is deliberately coarse upstream (any user/project/third-party command slugs to 'custom', any MCP command to 'mcp'). SDKAssistantMessageError widened 11 -> 13 members (verification_required, cloud_credential_error): both are correctly NON-retriable (one needs an out-of-band human step, the other is a credential/billing-class failure re-sending cannot clear), so the expression is unchanged, but the decision is now recorded explicitly in the same false-by-omission voice as model_not_found / account_on_hold and pinned by an exhaustive 13-member set-invariance test. UNDECLARED runtime additions, found ONLY by live capture (absent from sdk.d.ts, so no type diff could have seen them): narration_block_indexes on the assistant wrapper -> CARRIED verbatim (it names which of this frame's content blocks are user-facing narration rather than private reasoning, i.e. Anthropic's thinking.display:'updates' progress updates; a rendering decision, so lossless carry over allowlist; a per-frame content fact like aborted, so deliberately NOT under the turn-binding flag, and it has no stream-event twin so it cannot double-carry); wire_tool_inputs, the verbatim wire copy of each tool call's input keyed by tool_use_id; and five null-only leaves (event.message.container, event.message.context_management, event.delta.container, usage.iterations[].model, event.usage.iterations[].model) allowlisted null-only as stream-event twins of already-allowlisted complete-frame siblings. CENSUS MACHINERY: wire_tool_inputs' keys are per-capture tool ids, so every capture minted a brand-new norm-path and the newFields gate could never be satisfied; normalizePath now collapses the key directly under an explicit ID_KEYED_CONTAINERS list to {id}, the same way it collapses [n] to []. The list is explicit, not a key-shape heuristic, precisely so meaningful map keys such as modelUsage.claude-sonnet-5 keep their own registry entries. LIVE at 0.3.272: all six claude seeds refreshed (echo-sonnet5, partials-sonnet5, app-update-sonnet5, echo-fable51, partials-fable51, app-update-fable51) - census ZERO drops / ZERO new fields on all six after triage. Facet tests 147 -> 168. WATCH ITEM CLOSED in part: narration_block_indexes is the first live evidence of the thinking-display 'updates' mode reaching this SDK's wire; the Agent SDK still types display as 'summarized'|'omitted'. | | 0.3.261 | 2026-09-05 | 0.6.1 | peer sweep 0.3.258->0.3.261 (three patches; bundled CLI 2.1.258->2.1.261) - SDKMessage union unchanged (39 members, drift-gate-verified); sdk.d.ts diff 358 lines / 30 hunks, independently re-derived. Stream-scope field additions: user_message_uuids?: string[] on SDKAssistantMessage (first reply frame), SDKPartialAssistantMessage (first non-ping event) and both result arms (0.3.259) -> carried on the SAME three channels as the singular (wrapperMetaRaw first-block providerMetadata / block-less message.metadata; the stream arm's single message.metadata emit; result-meta.userMessageUuids camelCase) under the shared once-per-message flag - a CONSISTENCY carry: the string-prompt capture path supplies no client uuid, so no cassette has ever exercised the singular or the plural; SDKThinkingTokensMessage.user_message_uuid (0.3.260) rides the existing whole-frame carry; SDKResultSuccess first_content_frame_ms / first_stream_post_ms / first_stream_post_ack_ms / first_stream_post_wall_ms (0.3.260, remote-session latency) and SDKAPIRetryMessage.no_response {waited_ms, retry_wait_ms} (0.3.261) are router-plane (allowlisted + registered); the stale ttft_ms allowlist reason corrected (the facet DOES read it off the partial envelope). Control-plane only: permissionPrompts 'none', pluginDelivery 'initialize', reload_output_styles control subtype, initialize plugins/plugins_applied, get_usage skip_behaviors, settings shapes, Artifact tool schema. @anthropic-ai/sdk 0.124.0 is Admin-API/compliance only; the facet devDep ^0.93.0 provably cannot float (only 0.93.0 exists in >=0.93.0 <0.94.0). LIVE at 0.3.261 (CC 2.1.261): echo-sonnet5, partials-sonnet5, echo-fable51, partials-fable51, app-update-fable51 refreshed and the lagging app-update-sonnet5 (0.3.217) refreshed - census ZERO drops / ZERO new fields on all six; agjson byte-identical after regen except capture-time value drift. 8 unit tests added. Watch item: the Agent SDK still types thinking display 'summarized'|'omitted' while @anthropic-ai/sdk 0.121.0+ types a third 'updates' mode - adoption would change thinking_delta text semantics under Fable's omitted display. | | 0.3.258 | 2026-09-02 | 0.5.4 | peer sweep 0.3.233->0.3.258 (20 npm versions; 17 GitHub releases) - SDKMessage union unchanged (39 members, drift-gate-verified) but SIX field-level stream additions the name-only gate cannot see (full d.ts diff, independently re-derived): ModelUsage.costBasis (0.3.246) + ModelUsage.thinkingTokens (0.3.257, subset of outputTokens) -> costBasis joins the per-model identity in ext.anthropic.result-meta, thinkingTokens -> usage.byModel[m].reasoningTokens; usage.output_tokens_details.thinking_tokens (real since the 0.3.257 fix; the old reviewed-any allowlist entry that would have masked it is DELETED) -> usage.reasoningTokens on message.end/turn.done (runtime-guarded; undeclared on the resolved @anthropic-ai/sdk 0.93.0 BetaUsage); user_message_uuid on assistant (first frame), stream_event (first non-ping) and both result arms -> wrapperMetaRaw / message.metadata / result-meta.userMessageUuid; queued_turn_count (both result arms, 0 real) -> result-meta.queuedTurnCount; tool_use_result.resourceLinks (0.3.257, runtime-only) -> tool.done providerMetadata.resourceLinks; SDKAssistantMessageError += account_on_hold (non-retriable, test); system/init effort (router-plane, allowlisted); Task ambient/is_backgrounded/spawn_depth/resource_links ride the existing whole-frame carry. LIVE CAPTURES at 0.3.258 (Claude Code 2.1.258): echo-sonnet5 + partials-sonnet5 REFRESHED, and the FIRST claude-fable-5-1 seed trio echo-fable51 / partials-fable51 / app-update-fable51 (model GA 2026-09-01; Fable 5.1 requires CC >=2.1.257) - census caught FOUR runtime-only surfaces undeclared in the d.ts: result.subagent_stats (carried whole as result-meta.subagentStats), init.messaging_socket_path (allowlisted), thinking_delta.estimated_tokens (carried on reasoning.delta providerMetadata; core assembler gained the slot), and the first live system/thinking_tokens frames (SDKThinkingTokensMessage promoted router-plane -> carried whole-frame: the only thinking-progress signal under Fable's display:omitted), plus first observation of the typed init.terminal_slash_commands[] (allowlisted). partials-fable51 is the corpus's first live thinking_delta/signature_delta coverage (66 thinking tokens on the result frame, real non-zero reasoningTokens). Out of stream scope: PreModelSwitch/PostModelSwitch hooks, systemPrompt custom/snapshot, perTaskStopAffordance, SDK MCP server timeout, update_settings control, settings shapes, sdk-tools schemas, origin.fromMode/'projects-relay' growth inside the disclosed unread origin. Peer @anthropic-ai/sdk stays >=0.93.0 (facet devDep ^0.93.0 resolves 0.93.0; 0.123.0's BetaFallbackBlock/fallback_credit/output_tokens_details are typed-only additions - a runtime fallback block already lands as provider-raw). Pin + 9 platform-binary exclusions bumped; facet devDep ^0.3.258; 8 unit tests + 3 seeds added; full gate suite green. | | 0.3.233 | 2026-08-15 | 0.4.4 | peer sweep: 0.3.229->0.3.233 (four patches) - SDKMessage union unchanged (39 members, drift-gate-verified); ONE stream-wire addition: SDKAssistantMessage gained the wrapper sibling context_usage (structured twin of the /context report) - now carried verbatim via the 0.3.217 first-block providerMetadata channel (JsonValue-validated; block-less frames ride message.metadata; tests added). Everything else in the .d.ts diff is out of stream scope: SDKControlInitializeResponse fast_mode_state (control-plane init response), settings shapes (marketplaces/policyHelpers), doc comments. Pin + platform-binary exclusions bumped; facet devDep ^0.3.233. | | 0.3.229 | 2026-08-13 | 0.4.2 | peer sweep: 0.3.226->0.3.229 (three patches) — SDKMessage union unchanged (39 members, drift-gate-verified), wire-no-op; pin + platform-binary exclusions bumped, full gate suite green (949 workspace tests). | | 0.3.226 | 2026-08-09 | 0.4.1 | peer sweep: 0.3.221->0.3.226 (five patches) — SDKMessage union unchanged (39 members, drift-gate-verified in sync), wire-no-op. Pin bumped exact with platform-binary exclusion refresh; full gate suite green (109 facet tests incl. the stream_event partials suite, 949 workspace). | | 0.3.221 | 2026-08-07 | 0.3.10 | workspace#7 stream_event support: SDKPartialAssistantMessage router-plane->handled (token-granular partials mapped to first-class lifecycles under the complete arm's own ids; complete-frame dedupe via content suppression; INV-MSG preserved — live capture corpus/partials-sonnet5, claude-sonnet-5, 28 native frames incl. mid-stream complete frames and tool_result-before-message_stop ordering). SDKStatusMessage router-plane->carried (census-caught in the same capture: system/status{status:'requesting'} — agent-state signal, guuey#91's status vocabulary). Scenario enrolled in CLAUDE_SEEDS + regen targets; 109 facet tests incl. reducer-state equivalence + byte-parity; corpus-wide fold gates green. | | 0.3.221 | 2026-08-04 | 0.3.9 | 0.3.220→0.3.221 verified a Node-stream wire no-op by full sdk.d.ts tarball diff (181 lines / 11 hunks, all accounted): SDKMessage union (38 arms), stream_event, and control types untouched; the deltas are sandbox credentials config-input widening (mode 'mask', extract/onExtractNoMatch/maskDuplicates/injectHosts), quote-style const rewrites, and two NEW browser-sdk.d.ts exports (G4_TEXT_ENVELOPE_ARM_PORTED, getSseDropCounts — flag-gated SSE frame-drop machinery, relevant only if a browser-SSE facet ever exists). Release notes (skills validation, external mcpServers pre-first-turn connect fix) alter no schema. Pin bumped; live re-capture in this cohort's capture step. | | 0.3.220 | 2026-07-29 | 0.3.7 | Live capture landed (echo-sonnet5 @ claude-sonnet-5, sdk 0.3.220): the result frame live-populates ALL THREE carried fields — fast_mode_disabled_reason='sdk_opt_in_required' plus canonicalModel/provider on BOTH modelUsage entries (claude-sonnet-5→{claude-sonnet-5, firstParty}; claude-haiku-4-5-20251001→{claude-haiku-4-5, firstParty}) — confirming the ext.anthropic.result-meta carry against real wire, not just synthetic frames. Census: 5 new norm-paths classified (1 value-verifying transform [].fast_mode_disabled_reason→[].fastModeDisabledReason + registry entries using the {} dynamic-key wildcard for the per-model paths — [].modelUsage.{*}.canonicalModel/provider, the wildcard's first production use); replay + census gates green, zero drops. | | 0.3.220 | 2026-07-29 | | fixture suite + drift gate + full 0.3.217->0.3.220 sdk.d.ts diff audit (SDKMessage union membership unchanged, no removals): every wire delta is a field addition, triaged and CARRIED in the same slice — fast_mode_disabled_reason (FastModeDisabledReason enum, on BOTH result arms) + ModelUsage canonicalModel/provider (billing rate-table selection) now ride ONE structured ext event ext.anthropic.result-meta emitted before the turn close (turn.done/turn.error carry no providerMetadata slot and AgUsage is a closed schema — first-class core slots are a spec-process decision, disclosed in the member notes); init's own fast_mode_disabled_reason copy stays inside the router-plane init no-op (same enum, redundant with the terminal result-frame copy this slice carries); value-level notes: result api_error_status now populates 429/529 where it was null (unread telemetry, unchanged disposition), init capabilities[] may contain 'interrupt_cancel_queued_v1' (open-set string[]; the facet never enum-gates it — init is a blanket no-op); five fixture tests added (success carry, identity-only carry, error-arm carry, absent-fields negative control, Reducer fold) | | 0.3.217 | 2026-07-25 | 0.3.7 | wrapper-carry slice: the 0.3.217 bump audit's two disclosed SDKAssistantMessage gaps CLOSED — resumed_from_incomplete_thinking (replay-load-bearing per its own doc) and aborted (interrupt-truncation signal) now carried losslessly via the supersedes first-block providerMetadata precedent (§8 item 8), merged into one bag with wire names verbatim; block-less aborted frames fall back to message.metadata; four fixture tests added (carry, merge-with-supersedes, block-less fallback, no-op negative control) | | 0.3.217 | 2026-07-25 | 0.3.6 | LIVE capture: app-update-sonnet5 standing seed (workspace#2 *_update / re-render carry-fidelity sequence — render_card then update_card against ONE ui resourceUri, claude-sonnet-5): two-beat tool sequence captured clean; the census also surfaced a sonnet-5 empty-thinking+signature frame folding correctly to reasoning.delta('') + reasoning.opaque (triaged via the thinking→delta transform); replay + census gates green | | 0.3.217 | 2026-07-22 | 0.3.5 | LIVE capture: echo-sonnet5 standing seed REFRESHED at 0.3.217 (claude-sonnet-5) — replay + census gates green (zero untriaged drops, zero new fields); none of the 0.3.217 d.ts additions (resumed_from_incomplete_thinking/aborted/timestamp) appeared on this scenario's wire | | 0.3.217 | 2026-07-22 | 0.3.5 | fixture suite + drift gate + full 0.3.207->0.3.217 sdk.d.ts diff audit (586-line diff; SDKMessage union membership byte-identical, TerminalReason/SDKAssistantMessageError/@anthropic-ai-sdk peer range unchanged, no removals on any wire arm): every wire delta is a field addition on an existing arm, triaged into member notes — SDKAssistantMessage resumed_from_incomplete_thinking (DISCLOSED GAP: replay-load-bearing per its own doc; providerMetadata carry deferred to its own slice) + aborted + timestamp; SDKResultSuccess user_message_uuid/request_sent_wall_ms (unread telemetry); tool_progress heartbeat/subagent_type/subagent_retry (router-plane re-confirmed); origin verifiedPeerPid/fromSession/subkind growth inside the already-disclosed unread origin; init plugins[].version | | 0.3.207 | 2026-07-13 | 0.3.3 | LIVE capture: echo-sonnet5 standing seed REFRESHED at 0.3.207 (claude-sonnet-5) — zero unparsed events; init's new capabilities[] observed live (interrupt_receipt_v1, msg_lifecycle_v1) and triaged per the pre-existing router-plane disposition the d.ts-diff audit seeded | | 0.3.207 | 2026-07-13 | 0.3.3 | fixture suite + drift gate + full 0.3.199->0.3.207 d.ts diff audit: one new arm (SDKBackgroundTasksChangedMessage, carried); two previously-UNVERIFIABLE arms now declared and re-triaged (control_request_progress router-plane CONFIRMED; conversation_reset UPGRADED to carried); field-level deltas triaged (SDKMessageOrigin observer kinds + peer body — disclosed gap; TerminalReason +6 values — unread, disclosed; init capabilities — router-plane); no removals | | 0.3.199 | 2026-07-03 | 0.1.0 | live capture: echo-sonnet5 corpus (claude-sonnet-5) + drift-gate member seeding at 0.3.199 | | 0.2.141 | 2026-06-24 | | live captures: app-spec + complete-result corpus (claude-sonnet-4-6), landed with the corpus-seed commit 738bcab; provenance sidecars recorded retroactively 2026-07-02 (kind:capture, capturedAt unrecorded) |

Usage

// `query` is the Claude Agent SDK's own streaming call — you keep using
// your framework as-is; this package only normalizes what it emits.
import { query } from "@anthropic-ai/claude-agent-sdk";
import { createClaudeNormalizer } from "@silverprotocol/claude-agent-sdk";

const n = createClaudeNormalizer();
const events = [];

for await (const native of query({ prompt: "call the echo tool" })) {
  events.push(...n.push(native)); // one SDKMessage → 0+ AgEvents
}
events.push(...n.flush());        // seal anything still open at end-of-stream

push() returns the AgEvent[] synthesized from each native SDKMessage; flush() drains buffered end-of-stream state. Any async/sync iterable of SDKMessage works — a live query() run, or messages you captured earlier. Malformed input never throws — it routes through the lossless ext.anthropic.unparsed channel instead.

Thread id

Pass the host's own thread id as createClaudeNormalizer({ threadId }). The normalizer stamps it as the threadId of every turn and message it opens, subagent turns included: the partition root a key-value store writes each unit under (spec §1.2). The Claude Agent SDK wire has no thread concept, only session_id, so with no threadId option the facet stamps the SDK's session_id (or, on a frame that carries none, the turn's own id) as a facet-local placeholder, not a partition root; a host that persists by threadId supplies its own.

Token streaming

Pass includePartialMessages: true to query() and the normalizer emits token-granular text.delta / reasoning.delta / tool.args.delta events as the SDK's stream_event partials arrive — instead of one burst at turn end:

const response = query({
  prompt: "call the echo tool",
  options: { includePartialMessages: true },
});

No other change is needed. The complete assistant message the SDK still emits after the partials is deduplicated automatically (nothing is folded twice), and the final reduced state is identical either way except for the assistant message's usage.outputTokens, which only the stream reports (see Usage). Time-to-first-token rides message.metadata as ttft_ms when the SDK reports it. With parallel tool calls the SDK can deliver one call's result while a later call's input is still streaming; the normalizer lands that result in its own tool message and never seals the streamed assistant message on a tool result: the message closes with the next message or the turn's close, so each call is started once and carries its complete input, and the message keeps its output count.

Then fold the resulting AgEvents into messages and turns with @silverprotocol/core's reduce() — the same client code regardless of which framework produced the stream.

Deferred tools

When a host's PreToolUse hook defers a tool call, the SDK's result names the parked call in deferred_tool_use, with stop_reason: "tool_deferred". The normalizer then emits one hitl.ask (kind: "approval", toolCallId = the parked call) and closes the turn with turn.done {outcome: {type: "paused", asks}, finishReason: "paused"}. The result's deferred_tool_use also rides ext.anthropic.result-meta verbatim. An error result is never a pause. The host answers through its own hook on a resumed session; the resumed invoke opens its own turn, and the call's result lands there as a role: "tool" message.

Usage

Usage follows the AgJSON draft.5 accounting rules (spec §4):

  • inputTokens includes cache tokens on every usage object the normalizer emits: turn, message and byModel entries. Anthropic's input_tokens excludes cache reads and writes, so the normalizer adds cache_read_input_tokens and cache_creation_input_tokens in. cacheReadTokens and cacheWriteTokens are breakdowns of inputTokens; never add them to it.
  • A turn terminal's token counters cover that turn, so its usage carries cumulative: false. Its costUsd is the SDK's total_cost_usd, a running estimate over the whole query() call, and carries costScope: "query": take the latest value rather than summing across turns. Per the SDK's docs, a resumed or forked session continues from the total its transcript saved, and a mid-session /clear resets it. The cost covers every call the query made, so it does not reconcile with one turn's token counters.
  • byModel entries are the SDK's per-model running totals over the same query() call (cumulative: true). They cover every model call in the query pipeline, subagents and internal calls such as compaction included, so they can exceed the turn's own counters.
  • Message-level usage is the message stream's running total (cumulative: true). Without partial messages, an assistant message's output_tokens is a placeholder for a count the SDK reports on the result, so message.end.usage omits outputTokens; the turn terminal carries the turn's output count.
  • Error results: a success-subtype result carries its usage whether it closes the turn as turn.done or, on an API error, as turn.error. An error-subtype result (error_max_turns, error_during_execution, …) carries its usage only when an API-error assistant message had already ended the turn; otherwise its turn.error carries none.

Turn trigger

When the frame that opens a top-level turn is an assistant message, a streamed stream_event whose event.type is message_start, or a success result, and it carries user_message_uuid, that turn's turn.start carries trigger: {kind: "user", ref: <that uuid>}, and the folded AgTurnRecord.trigger equals it. The SDK echoes a uuid when the host set uuid on the SDKUserMessage it sent in streaming input, and also for a meta turn the host vouches for. kind: "user" means a user-role message submitted with that uuid; it does not claim that a human typed it. Provenance, where the SDK reports it, stays on ext.anthropic.result-meta.origin. ref names one send: for a batch the host merged into one turn, it is the batch's last member, and the full list stays in user_message_uuids. Two turns in one fold can carry the same ref. The facet sets trigger only from the frame that opens the turn and never re-sends turn.start to add one. So a turn opened by a notice, a tool result or an error result carries no trigger, and neither does a turn whose uuid first appears on a later frame. A missing trigger does not mean the turn answers no send of yours. Turns the CLI starts itself and subagent turns never carry it.

Spec: silverprotocol.io/AgJSON — canonical in silverprotocol/AgJSON; wire version 1.0.0-draft.8.