0.7.0
A minor release that ships spec revision 1.0.0-draft.4 (AGJSON_VERSION). An event a 0.7.0 reader doesn’t know now keeps its place in the sequence instead of stalling the fold. The reference Reducer now stops and asks for a resync on delivery faults it used to fold silently. The framework packages close turns, tool calls and handoffs more honestly. The changes to the fold rules, with before-and-after examples, are tracked in AgJSON#1. Read the upgrading notes first: several ids change.
Upgrading from 0.6.x
- Upgrade the framework packages no later than
@silverprotocol/core. If you fold several invokes into oneReducer, a 0.7.0 core stops and asks for a resync on amessage.startfor a turn that has already closed, and a closed turn stays closed across invokes. The framework packages up to 0.6.7 reuse turn ids from one invoke to the next, so a 0.7.0 core folding their output parks on the second invoke. Upgrade them together with core, or before it. - Honour
needsResync. The reference reducer now parks on a re-delivered event, and on a block id or a finaltool.donerepeated within one invoke; 0.6.7 folded these silently. It also extends the closure rule. 0.6.7 already resynced on a new block or a delta into a sealed message, and on a new block into a closed turn. 0.7.0 adds block-finalizing events (text.end,reasoning.end,reasoning.opaque,tool.args.assembled), a delta into a still-open message of a closed turn and amessage.startfor a closed turn, and it keeps a turn closed across the invokes of one fold. WhenneedsResyncis set, resume from a snapshot. - Ids change. If you store or compare them:
- Claude: every turn id changes. A turn is now
turn_<first assistant message id>(or the id of the notice or result frame that opens it), and each subagent run gets its own id. In 0.6.7 every turn in an invoke wasturn_<session_id>, so a multi-turn invoke folded into a single turn record. - OpenAI Agents SDK and Vercel AI SDK: turn and message ids they mint now carry a per-invoke stem, random by default, or the new
invokeIdoption.invokeId: "vercel"reproduces 0.6.7’s Vercel ids exactly.invokeId: "openai"reproduces OpenAI’s turn ids, but handoff turns becometurn_openai_handoff_<n>. - Google ADK: streaming block ids count per invoke, so a second turn continues at
text:1instead of reopeningtext:0. The id-less fallback turn isturn_adk_<random>instead ofturn_adk. - Google ADK asks: approval and authentication asks are now keyed by the reserved call ADK creates for the pause. Their
askIdisapproval_<reservedId>/auth_<reservedId>,toolCallIdis the reserved id, and the original call id moves tometadata.originalFunctionCallId. A host that answers asks byaskIdmust use the new ids.
- Claude: every turn id changes. A turn is now
- Handle
paused. An OpenAI Agents SDK approval pause now closes its turn aspaused, naming its ask. In 0.6.7 it folded as a success. - ADK tool failures now fold as failures. An ADK tool result carrying an
errormember now folds asoutcome: "error"withisError: true, and a declined approval as"denied"; in 0.6.7 both folded as"ok". A tool of yours that returns{error: …}as ordinary data now reads as a failure. - Two things moved. These are breaking if you read them:
- OpenAI misalignment. Since 0.6.1,
@silverprotocol/openai-agentshas emitted OpenAI’serror.misalignmentas a turn-scopedext.openai.misalignmentevent. From 0.7.0 it arrives as an adapter notice, just before theturn.errorit explains. The notice is one text block carryingdetailed_explanation(orerror.messagewhen there is none), and the whole misalignment object is on that block’s_meta["openai/misalignment"]. The notice is part of the transcript, so it’s persisted with it.ext.openai.misalignmentis no longer emitted. - Claude CLI wrapper fields.
narration_block_indexes,api_error,api_error_paramsandapi_error_codemove fromproviderMetadatato host-only_meta, on the first block’s start event (folded onto that block), or onmessage.metadatawhen the first block isn’t text or thinking.estimated_tokensmoves toreasoning.delta’s_metaand is now live-only.providerMetadatais kept for values that must round-trip to the provider.
- OpenAI misalignment. Since 0.6.1,
Spec: AgJSON 1.0.0-draft.4
- Forward-compatible ingest. A well-formed event (an object with a string
typeand a numericseq) that this version can’t validate (an unknown event type, an unknown value, an unknown block type) is no longer dropped.ingestAgEvent(s)returns it in place asext.agjson.ignored {seq, ignoredType, raw}. That event never folds but keeps itsseqslot, so the reducer doesn’t see a false gap.rawis live-only; don’t persist it. Input that isn’t a well-formed event is still dropped, and is now reported through the optionalonRejectcallback. Ingest never throws. - Closed value sets are frozen for 1.x. New values go to open companions instead. The first is
turn.done.finishReasonRaw: the framework’s native finish value, set when the mappedfinishReasonloses it. It’s recorded on the turn record. All four framework packages set it; Google ADK also keepsmessage.metadata.rawFinishReasonfor one more release. phaseon text and reasoning blocks. A new optional open string;"interim"marks narration between tool calls. It’s set on*.startand may be replaced on*.end. The OpenAI Agents SDK sets it for commentary, the Vercel AI SDK for an OpenAI commentary text part, and the Claude Agent SDK for narration blocks the CLI marks (which the current CLI doesn’t yet do).- Stored records and inputs follow the forward-compatibility rule too.
readStoredAgMessage,readStoredAgMessagesandreadStoredAgMemoryRecordsread what you persisted. An element they can’t read, such as a content block of a type this version doesn’t define, is left out and reported with its position and its stored value, and a stored record that isn’t a message at all is reported whole; everything else reads normally, unknown fields included. Don’t persist the view they return.checkAgInputchecks an input before you act on any of it. A value outside a closed set, or any other schema failure, rejects the whole input with a typed result (malformed,unknown-valueormajor-mismatch); an unknown object field never does. It checksprotocol, thenversion, then the rest: an input whoseprotocolis"agjson"but whose major version differs ismajor-mismatch, whatever else it carries. Otherwise a malformed value in any part it checks makes the resultmalformed, even if the input also carries an unknown value. Members that only an unknownkindwould select, and the fields of a block of an unknown type, aren’t checked. - New:
uiResources.viewMessageTurns. An optional client capability. A client sets it onAgClientCapabilitiesto say it delivers a message sent from a view (an MCP Appsui/messageor an OpenAI AppssendFollowUpMessage) as the user message of a laterkind: "start"input: at once when no turn is in progress, otherwise after that turn ends. Such a client never cancels a turn in progress to deliver it, and it answers the view with an error, never a success, for a message it doesn’t deliver (for example, one the user declines). The flag applies only to the input that carries it, so an agent host shouldn’t rely on this delivery for an input without it. Cores before 0.7.0 ignore it. Discussion: AgJSON#2. - Erratum to draft.2: “additive” there meant an absent field or role is byte-identical to draft.1. Under draft.4’s rules a new
AgRolevalue is not additive for consumers, and a consumer from before draft.2 can’t foldnoticemessages.
Fold changes (@silverprotocol/core)
These change what reduce() builds from a stream. They are tracked, with their spec text, in AgJSON#1, along with the ignored-event slot above and the nested-turn terminals below.
- Tool results kept open are snapshots. A later
tool.donefor a result kept open withmore: truereplaces the result’s payload as a unit:content,outcome,isError,structuredContent,uiData,sideData,errorText,errorCodeandpendingInput. A payload field the later event omits is cleared, including when the finaltool.donereports an error._meta,toolMetadataanddynamicare kept unless re-sent,providerMetadatamerges by key, and an explicituiData: nullis stored as a value. A producer must carry an MCP Apps result’s_meta.uiunchanged ontool.done._meta, so a view keyed on_meta.ui.resourceUrisurvives. The Vercel AI SDK is the one framework package that keeps results open: a generator tool that yields and then throws now folds to the error alone. state.deltaobject patches replace each top-level key whole, the way ADK applies a state delta:{cfg:{a:1,b:2}}then{cfg:{a:5}}now folds to{cfg:{a:5}}, not{cfg:{a:5,b:2}}.nullis stored as a value, a scalar patch does nothing, and JSON Patch arrays are unchanged. On core 0.6.x the one-level merge stays; from 0.7.0, anything the old merge left behind in a key clears the next time that key is written.- A re-delivered event never folds twice, and the other delivery faults listed under Upgrading now park the fold instead of corrupting it.
- A terminal (
turn.done,turn.errororturn.abort) for a turn the reducer never saw opened folds onto no turn record and doesn’t park. In 0.6.7 such a terminal minted a turn record of its own. A turn counts as seen opened once aturn.startorsubagent.startopens it, or once a foldedmessages.snapshotcarries it; a snapshot that carriesturnsreplaces that set. A terminal for a turn that was seen opened but has no record gets one only when the latest snapshot carrying that turn gives it athreadId.
@silverprotocol/core: new APIs and fixes
withAtomicPush(createInner, opts?)wraps a normalizer so eachpush()is atomic. If handling one native event throws, that event’s partial output is discarded (noseqis used) and oneerror {message: "normalizer error", code}takes its place. The OpenAI, Claude and Google packages now use it.StreamAssembler.checkpoint()/rollback()give the same guarantee to a normalizer that can’t journal; the Vercel package uses them.toJsonValueSafe(v)turns any live value into JSON without throwing: cycles become"[Circular]", a value past the depth cap becomes"[MaxDepth]", and anErrorbecomes{name, message}plus its own enumerable fields.toJsonValueSafeWithIssues(v)also reports what it replaced, andisJsonValue(v)is a type guard.Reducer.push()keeps its own copy. It now folds a copy of each event, so a host that reuses or changes an event object after pushing it no longer changes the fold.result()already returned a copy, so reading was never affected.validateHitlAnswernow rejects an answer whosestatusisn’t defined (unknown-status) before dispatch. 0.6.7 could read it as a grant.reduce()no longer leaves an explicitproviderMetadata: undefinedkey on a block.
Claude Agent SDK
- One turn id per turn, unique across invokes (see Upgrading). A multi-turn invoke now folds into one turn record per turn, keeping each turn’s outcome and usage. New option:
invokeId. - Subagent runs close on their own. A run opens once and closes once with its own
turn.done,turn.errororturn.abort(with nousage) right beforesubagent.done, as draft.4 requires. It closes on the spawning task’s result, on atask_notification, or at flush. A background sub-run stays open until itstask_notificationarrives; the parent’s result no longer closes it. A nested API error no longer closes the parent turn. In 0.6.7 every nested message opened and closed its own bracket. - Every turn opens with
turn.start, including one that starts with a result (a startup failure, a local command, a result-only API error) and a resumed invoke that starts with a tool result. - A denied tool call closes once, as
denied. A harness-denied call used to close aserrorand then get a seconddeniedclose from the result’s denials. An error-subtype result now emits its denials too. - More CLI facts reach you.
diagnostics,error_details,advisor_modelandattribution_agentnow ride host-only_metabeside the fields that moved there (see Upgrading). A non-nullstop_details, for example a refusal’s category and its fallbacks, folds onto its message throughturn.done.messageMetadata. Wrapper fields on a first block that isn’t text (a compaction, a content block, an MCP tool result) now ridemessage.metadata; 0.6.7 dropped them. Unknown top-level frame types (for examplecommand_lifecycle) and the CLI’s synthetic user frames rideext.anthropic.frame.deferred_tool_useridesext.anthropic.result-meta.deferredToolUse, and an error close addsapiErrorStatusandstopReasonthere. - A result-only error close says why. Its
codeis the API error code, else the CLI’sterminal_reason, else"api_error", and itsmessageis the result text, else the code. 0.6.7 could close with an empty message. finishReasonRawcarries the nativestop_reasonwhenfinishReasonfalls back to"unknown".- A stream that ends early closes cleanly. When a stream is cut short,
flush()closes the blocks still open without inventing content: no tool input assembled from partial JSON, no reasoning signature and no compaction block. push()never throws. Each frame is read as plain JSON first. A frame that still can’t be handled is rolled back and replaced by one coreerrorevent, and emitted values never share objects with the frame you pushed.- Hardens how verbatim vendor carries (
ext.anthropic.*frames, provider-raw blocks and CLI wrapper fields) are copied.
OpenAI Agents SDK
- An approval pause stays paused. When a stream ends while the facet is still holding a round open, it releases that round, in order and before the live response closes. While a call still waits for approval, the round comes out as
paused, naming its approval ask (askIdapproval_<callId>,kind: "approval"). When the round’s own terminal isn’t a success, it comes out as that terminal. Otherwise its usage goes onmessage.end, followed byturn.abort(stream-truncated). An Agents SDK approval pause, which used to fold as a success, now folds aspausedwith its ask. - Handoffs close cleanly. A handoff now closes both its transfer call (the
transfer_to_<agent>call gets itstool.donefrom the SDK’shandoff_occurred) and its nested turn (turn.donewith success andfinishReason: "unknown", thensubagent.done), and the source agent’s turn closes as a success at the handoff, before the target agent’s rounds. In 0.6.7 the transfer call never got a result, the nested turn had no outcome, and the source agent’s turn stayed open until the end of the stream, closing after the target agent’s rounds. One limit: when the model requests several handoffs in one response, the SDK sends the results of the ignored calls only to the model, so the source turn still ends withturn.abort(stream-truncated) at flush. - An approval resume opens its own turn. A resumed run’s leading tool results now open
turn.start, each as its own tool message, followed by the resumed response in the same turn. In 0.6.7 they arrived with no turn open, and the fold parked. - Commentary is marked
phase: "interim"on its text block.providerMetadata.phasestill carries OpenAI’s value, and a null or empty phase is no longer emitted. finishReasonRawcarries the native finish reason when the mapping falls back to"other"or"unknown".push()never throws. The normalizer is wrapped inwithAtomicPush, and each native event is read as plain JSON once. In 0.6.7, pushing live Agents SDK objects threw for six of nine live shapes.ext.openai.unparsednow carries a copy, not your object.createOpenaiNormalizer()takes an optional{ invokeId }.
Vercel AI SDK
- Ids are unique across invokes, with a per-invoke stem, or
invokeId(see Upgrading). Step ids still repeat; they’re live-only and never folded. finishReasonRawcarries the finish part’srawFinishReasonwhen the mapping falls back to"other"or"unknown".- An OpenAI commentary text part opens with
phase: "interim". - A cycle or
BigIntin a live value degrades only its own node ("[Circular]", or the number’s decimal string); 0.6.7 turned the whole value into"[object Object]". - Each stream part is a transaction. A part that throws midway is rolled back (no
seqis used) and replaced by one coreerrorevent. createVercelNormalizer()takes an optional{ invokeId }.
Google ADK
- Tool failures fold as failures (see Upgrading), in this order: an MCP result with
isErroris an error; ADK’s pause placeholder stays"ok"; a declined approval is"denied"; any other result with a truthyerrormember is an error, witherrorTextanderrorCode; everything else is"ok". - Pauses: each pause is keyed by its reserved call: an approval request becomes an approval ask, an authentication request an auth ask, and an input request a text or form ask. A Workflow function node’s authentication request now closes its turn as paused; in 0.6.7 it aborted at flush.
- Opt-in host completion. Create the normalizer with
{ hostCompletion: true }and push{ type: "__host_complete__" }afterrunAsync()returns normally, and a completed Workflow closes its turn as a success frompush(). Without the option, a completed Workflow still aborts at flush, as before. Turn it on only if you push the sentinel: with the option on and no sentinel, even a plain agent’s turn aborts at flush. - Host-error sentinel. Push
{ type: "__host_error__", code, message }(or the caughtError) and every open turn closes asturn.error, innermost first. With no turn open, a new turn is opened to carry the error. In 0.6.7 a caught throw could only rideext.google.unparsed, and the turn aborted at flush. finishReasonRawis set on the lossy mappings, andmessage.metadata.rawFinishReason/rawErrorCodestay for one more release.- Block ids count per invoke, and the fallback turn id is unique (see Upgrading).
push()now runs insidewithAtomicPush, with the same guarantees as 0.6.7’s guard. A native event with nothing serializable now ridesext.google.unparsedwithnative: null; 0.6.7 sent{reason: "not-serializable"}.- Security advisory GHSA-w78f-8q9f-jwpg. Versions before 0.7.0 could carry credential material from ADK events into the events they emit, and 0.7.0 completes the fix, including how ADK shared state is carried. If you ran an affected version, the advisory lists which versions are affected and what to do.
Wire version
AGJSON_VERSION is 1.0.0-draft.4. draft.3 envelopes remain accepted (same major). The compatibility tables on each npm page are unchanged.