0.7.0

AgJSON draft.4: forward-compatible ingest and stricter folds

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 one Reducer, a 0.7.0 core stops and asks for a resync on a message.start for 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 final tool.done repeated 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 a message.start for a closed turn, and it keeps a turn closed across the invokes of one fold. When needsResync is 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 was turn_<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 invokeId option. invokeId: "vercel" reproduces 0.6.7’s Vercel ids exactly. invokeId: "openai" reproduces OpenAI’s turn ids, but handoff turns become turn_openai_handoff_<n>.
    • Google ADK: streaming block ids count per invoke, so a second turn continues at text:1 instead of reopening text:0. The id-less fallback turn is turn_adk_<random> instead of turn_adk.
    • Google ADK asks: approval and authentication asks are now keyed by the reserved call ADK creates for the pause. Their askId is approval_<reservedId> / auth_<reservedId>, toolCallId is the reserved id, and the original call id moves to metadata.originalFunctionCallId. A host that answers asks by askId must use the new ids.
  • Handle paused. An OpenAI Agents SDK approval pause now closes its turn as paused, 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 error member now folds as outcome: "error" with isError: 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-agents has emitted OpenAI’s error.misalignment as a turn-scoped ext.openai.misalignment event. From 0.7.0 it arrives as an adapter notice, just before the turn.error it explains. The notice is one text block carrying detailed_explanation (or error.message when 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.misalignment is no longer emitted.
    • Claude CLI wrapper fields. narration_block_indexes, api_error, api_error_params and api_error_code move from providerMetadata to host-only _meta, on the first block’s start event (folded onto that block), or on message.metadata when the first block isn’t text or thinking. estimated_tokens moves to reasoning.delta’s _meta and is now live-only. providerMetadata is kept for values that must round-trip to the provider.

Spec: AgJSON 1.0.0-draft.4

  • Forward-compatible ingest. A well-formed event (an object with a string type and a numeric seq) 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 as ext.agjson.ignored {seq, ignoredType, raw}. That event never folds but keeps its seq slot, so the reducer doesn’t see a false gap. raw is live-only; don’t persist it. Input that isn’t a well-formed event is still dropped, and is now reported through the optional onReject callback. 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 mapped finishReason loses it. It’s recorded on the turn record. All four framework packages set it; Google ADK also keeps message.metadata.rawFinishReason for one more release.
  • phase on text and reasoning blocks. A new optional open string; "interim" marks narration between tool calls. It’s set on *.start and 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, readStoredAgMessages and readStoredAgMemoryRecords read 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. checkAgInput checks 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-value or major-mismatch); an unknown object field never does. It checks protocol, then version, then the rest: an input whose protocol is "agjson" but whose major version differs is major-mismatch, whatever else it carries. Otherwise a malformed value in any part it checks makes the result malformed, even if the input also carries an unknown value. Members that only an unknown kind would 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 on AgClientCapabilities to say it delivers a message sent from a view (an MCP Apps ui/message or an OpenAI Apps sendFollowUpMessage) as the user message of a later kind: "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 AgRole value is not additive for consumers, and a consumer from before draft.2 can’t fold notice messages.

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.done for a result kept open with more: true replaces the result’s payload as a unit: content, outcome, isError, structuredContent, uiData, sideData, errorText, errorCode and pendingInput. A payload field the later event omits is cleared, including when the final tool.done reports an error. _meta, toolMetadata and dynamic are kept unless re-sent, providerMetadata merges by key, and an explicit uiData: null is stored as a value. A producer must carry an MCP Apps result’s _meta.ui unchanged on tool.done._meta, so a view keyed on _meta.ui.resourceUri survives. 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.delta object 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}}. null is 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.error or turn.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 a turn.start or subagent.start opens it, or once a folded messages.snapshot carries it; a snapshot that carries turns replaces 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 a threadId.

@silverprotocol/core: new APIs and fixes

  • withAtomicPush(createInner, opts?) wraps a normalizer so each push() is atomic. If handling one native event throws, that event’s partial output is discarded (no seq is used) and one error {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 an Error becomes {name, message} plus its own enumerable fields. toJsonValueSafeWithIssues(v) also reports what it replaced, and isJsonValue(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.
  • validateHitlAnswer now rejects an answer whose status isn’t defined (unknown-status) before dispatch. 0.6.7 could read it as a grant.
  • reduce() no longer leaves an explicit providerMetadata: undefined key 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.error or turn.abort (with no usage) right before subagent.done, as draft.4 requires. It closes on the spawning task’s result, on a task_notification, or at flush. A background sub-run stays open until its task_notification arrives; 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 as error and then get a second denied close from the result’s denials. An error-subtype result now emits its denials too.
  • More CLI facts reach you. diagnostics, error_details, advisor_model and attribution_agent now ride host-only _meta beside the fields that moved there (see Upgrading). A non-null stop_details, for example a refusal’s category and its fallbacks, folds onto its message through turn.done.messageMetadata. Wrapper fields on a first block that isn’t text (a compaction, a content block, an MCP tool result) now ride message.metadata; 0.6.7 dropped them. Unknown top-level frame types (for example command_lifecycle) and the CLI’s synthetic user frames ride ext.anthropic.frame. deferred_tool_use rides ext.anthropic.result-meta.deferredToolUse, and an error close adds apiErrorStatus and stopReason there.
  • A result-only error close says why. Its code is the API error code, else the CLI’s terminal_reason, else "api_error", and its message is the result text, else the code. 0.6.7 could close with an empty message.
  • finishReasonRaw carries the native stop_reason when finishReason falls 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 core error event, 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 (askId approval_<callId>, kind: "approval"). When the round’s own terminal isn’t a success, it comes out as that terminal. Otherwise its usage goes on message.end, followed by turn.abort (stream-truncated). An Agents SDK approval pause, which used to fold as a success, now folds as paused with its ask.
  • Handoffs close cleanly. A handoff now closes both its transfer call (the transfer_to_<agent> call gets its tool.done from the SDK’s handoff_occurred) and its nested turn (turn.done with success and finishReason: "unknown", then subagent.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 with turn.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.phase still carries OpenAI’s value, and a null or empty phase is no longer emitted.
  • finishReasonRaw carries the native finish reason when the mapping falls back to "other" or "unknown".
  • push() never throws. The normalizer is wrapped in withAtomicPush, 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.unparsed now 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.
  • finishReasonRaw carries the finish part’s rawFinishReason when the mapping falls back to "other" or "unknown".
  • An OpenAI commentary text part opens with phase: "interim".
  • A cycle or BigInt in 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 seq is used) and replaced by one core error event.
  • createVercelNormalizer() takes an optional { invokeId }.

Google ADK

  • Tool failures fold as failures (see Upgrading), in this order: an MCP result with isError is an error; ADK’s pause placeholder stays "ok"; a declined approval is "denied"; any other result with a truthy error member is an error, with errorText and errorCode; 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__" } after runAsync() returns normally, and a completed Workflow closes its turn as a success from push(). 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 caught Error) and every open turn closes as turn.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 ride ext.google.unparsed, and the turn aborted at flush.
  • finishReasonRaw is set on the lossy mappings, and message.metadata.rawFinishReason / rawErrorCode stay for one more release.
  • Block ids count per invoke, and the fallback turn id is unique (see Upgrading).
  • push() now runs inside withAtomicPush, with the same guarantees as 0.6.7’s guard. A native event with nothing serializable now rides ext.google.unparsed with native: 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.