0.8.0
A minor release that ships spec revision 1.0.0-draft.5 (AGJSON_VERSION). It’s a minor because the fold and the meaning of a field change. A turn’s closure now follows its turn record, so a messages.snapshot no longer reopens a closed turn, and a second terminal for a closed turn stops the fold and asks for a resync. A record event for a turn whose thread the reducer doesn’t know yet is held, instead of landing on a placeholder record. A Claude inputTokens now counts cache reads and writes. The release also adds toPersistable() for hosts that store folds, a threadId option on the OpenAI Agents SDK, Google ADK and Vercel AI SDK packages, MCP resource links as first-class blocks, and turns per generation for Google ADK Live sessions. Read the upgrading notes first.
Upgrading from 0.7.x
- Claude
inputTokensis cache-inclusive. In draft.5,inputTokenscounts every input token the provider processed, cache reads and writes included, withcacheReadTokensandcacheWriteTokensas its breakdown, never an addition. Anthropic reports input without the cache, so a ClaudeinputTokens(on the turn, on each message and in eachbyModelentry) now includes its cache reads and writes, and rises on cached calls. One with no cache counters is unchanged. In the echo capture, the turn’sinputTokensgoes from 4 to 4,056: 4 uncached tokens, plus 1,966 cache reads and 2,086 cache writes. A Claude turn terminal’s counters cover that turn, so its usage now readscumulative: false; 0.7.x marked ittrue. ItscostUsdis the SDK’s running estimate over the wholequery()call, so it carries the newcostScope: "query". Without partial messages, an assistant message’soutputTokensis a placeholder, somessage.end.usagenow omits it. Usage from the OpenAI Agents SDK, the Vercel AI SDK and Google ADK’sgenerateContentroute is unchanged; Gemini Live changes (see Google ADK).- Repairing stored Claude rows. A stored Claude turn record is from before 0.8.0 exactly when its top-level
usagecarries acostUsdkey (a0included) and nocostScope; a record withoutusagehas nothing to repair. To repair one, addcacheReadTokens + cacheWriteTokensto itsinputTokensand, for eachbyModelentry, that entry’s owncacheReadTokens + cacheWriteTokensto the entry’sinputTokens. Read its top-levelcumulativeasfalseand itscostUsdas carryingcostScope: "query", and leave each entry’scumulativeas stored. The record’s token counters and cost then read as a 0.8.0 record’s. A host that rewrites the row stores the repaired counters andcostScope: "query"together, which ends the marker, so the row is never repaired twice. A stored Claude message record has no exact marker:inputTokens < cacheReadTokens + cacheWriteTokensidentifies some older rows, not all.
- Repairing stored Claude rows. A stored Claude turn record is from before 0.8.0 exactly when its top-level
- A snapshot no longer reopens a closed turn. A turn is closed while the fold holds a turn record for it that carries an
outcome. Amessages.snapshotchanges closure only through the turn records it leaves: one that carries a turn with anoutcomekeeps that turn closed, and one that omitsturnskeeps every closure. After such a snapshot, amessage.start, atool.doneadopting a message, or a terminal for that turn parks the reducer, where 0.7.x folded it. To reopen a turn, carry its record without anoutcome(a snapshot withturns: []drops every record, and so every closure). No framework package’s output changes.- This reverses part of what draft.4 and 0.7.0 said. The draft.4 spec note and AgJSON#1 (section 4) said a turn stays closed “until a
messages.snapshot”, and the 0.7.0 note advised: whenneedsResyncis set, resume from a snapshot. A snapshot still clears a message’s seal, since it replaces the messages. It no longer clears a turn’s closure. Resuming from a snapshot of the fold still recovers from a gap or a re-delivery. It doesn’t license new content for a turn the snapshot records as closed: a finished invoke re-sent fromseq0 after a snapshot of its own fold parks, and the fold stays the snapshot’s.
- This reverses part of what draft.4 and 0.7.0 said. The draft.4 spec note and AgJSON#1 (section 4) said a turn stays closed “until a
- A second terminal for a closed turn parks. A
turn.done,turn.errororturn.abortfor a closed turn now setsneedsResyncand leaves the fold unchanged, whatever its outcome and whether or not it repeats the recorded one. In 0.7.x each terminal overwrote the record, so an error followed by a success read as a success. The one exception is a host’s paused refresh: a re-sentturn.start, then aturn.donewith apausedoutcome and refreshed asks, for a turn closed as paused. It replaces the outcome and asks, keeps the recorded usage, and the turn stays closed. The refresh still works when amessages.snapshotfalls between theturn.startand theturn.done. The one-terminal rule binds a host’s own appended events as well as a normalizer’s. Every committed golden folds as before. - Record events for a turn whose thread isn’t known are held. A
source,handoff,prompt.blocked,guardrail.result,agent.capabilitiesordisplay.requiredevent lands on a record carrying the turn’s thread. The thread comes from aturn.start,subagent.start,message.startormessages.snapshotof that turn. Until the reducer knows it, the landing is held andresult()omits the record.- Before (0.7.x): a record event for a turn not yet opened created a stub record whose
threadIdwas theturnId, or anunknown-turnrecord. - After (0.8.0): no turn record carries a
threadIdthat no event carried. A later opener sets a held record’s thread, and asubagent.startalso sets itsparentTurnId. - In the fold: a terminal or record event with no
turnIdresolves to the sole open turn, never a closed one. With no open turn, or several, its owner is unresolvable and the reducer parks. The reference reducer holds up to 64 turns and 1,024 landings; past either, it parks. Reducer.ensureTurn(), which minted those stubs, is removed.
- Before (0.7.x): a record event for a turn not yet opened created a stub record whose
- Ids change: Google ADK. Turn and message ids now carry a per-invoke stem, random by default, or the
invokeIdoption:turn_<invocationId>becomesturn_adk_<16 hex>_<invocationId>, and message ids follow.invokeId: "adk"doesn’t reproduce 0.7.x’s ids. The host-error sentinel’sinvocationIdno longer names the turn. This keeps ids unique when an invoke reuses aninvocationId, as a resumed ADK-Python run does; in 0.7.x two such invokes folded into oneReducerparked it. - Claude host-only fields moved. These are breaking if you read them:
- The CLI wrapper keys
context_usage,usage_report,user_message_uuid,user_message_uuids,resume_reason,abortedandsupersedesmove fromproviderMetadatato host-only_meta, on the first block’s start event, or on amessage.metadataevent when no block anchors them. On a frame whose first block is a tool call, they now always arrive onmessage.metadata. This retracts theproviderMetadatachannel described forabortedin 0.3.7, forcontext_usagein 0.4.4, and for theuser_message_uuidfamily,resume_reasonandusage_reportin 0.6.1–0.6.3. - A tool result’s
resourceLinksmoves fromtool.done.providerMetadata(0.5.4) totool.done._meta["anthropic/resourceLinks"], the SDK’s list, verbatim. The result’scontentstays the rendered text. - A permission denial’s context (
decisionReasonType,decisionReasonCode,decisionReason,agentId) now rides one record,tool.done._meta["anthropic/permissionDenied"], on the denial and on the closing tool result alike. That includesdecisionReasonCode, added in 0.6.3. - A notice’s
level,preventContinuationandtoolUseIdride the notice text block’s_meta. - Host records carry no durability promise (SPEC §2.1). A host may drop
_metawhen it persists a conversation, so a record read from the live stream can be absent after a reload.
- The CLI wrapper keys
- Pass
threadIdif you persist. A host that persists, routes or folds across invokes bythreadIdmust give the normalizer the thread it assigned the invoke to (SPEC §8.0 host obligation 6). Without the option, each package stamps a facet-local placeholder that is not a thread identity:"openai","google"or"vercel", or the Claude session id. Output without the option is unchanged.
Spec: AgJSON 1.0.0-draft.5
- Turn closure follows the fold’s turn records across a
messages.snapshot(§5.0 INV-MSG, §5, §10 item 27). - A second terminal for a closed turn is a
reduce()error and a snapshot resync, except the paused refresh (§5.0 INV-MSG and INV-TURN, §8.0 host obligation 5, §10 item 27). - Record events on unopened turns land on the turn’s known thread, never a placeholder, and a normalizer opens a turn before any event of it, record events included (§5.0 INV-OWNER and INV-TURN, §10 items 48 and 49). Erratum to draft.4: its note said a terminal for a turn seen only in a snapshot’s messages folds onto a record carrying that message’s
threadId; the 0.7.x reducer did so only when no record event had reached that turn first. - The input side of
AgUsage(§4; §8.0 items 4, 19, 24 and 29; §10 items 8, 21 and 50):inputTokensis cache-inclusive, the revision draft.3 deferred;cumulativebinds the object it sits on; the optionalcostScopenames a cost whose scope differs from its object’s counters. Over the Gemini Live API,thoughtsTokenCountalways folds intooutputTokens,totalTokensisinputTokens + outputTokens + (toolUseInputTokens ?? 0), and a differing provider total rides the new optionaltotalTokensRaw. display.requiredis recorded for the live render, not for replay (§5, §5.0 INV-FOLD, §13.3, §10 item 51). A host’s persistence and re-display of a turn’s grounding records follow the grounding provider’s terms, and the render duty is conditioned on them. A host’s storage projection may omitturns[].displayRequired, whichtoPersistable()does in one call. Nothing changes on the wire or in the fold.- MCP resource links in tool results (§8.0 item 31, §10 item 44): a normalizer emits an MCP
resource_linkpart as oneresource-linkblock, never aprovider-rawblock. The block gains MCP’s optionalname,title,descriptionandsize. A member that doesn’t fit the block rides oneprovider-rawright after it, and a part without a stringuristays oneprovider-raw. §13.4’s scheme validation now coversresource-linkuris and links a consumer takes from a tool result. Aresource-linkuri is carried byte for byte, so a signed or credential-bearing link is the host’s to handle: under §13.4 for its scheme, and with its own care when it fetches or persists it. - Host records are side metadata (§2.1, §8.0 items 19, 21 and 29, §10 item 45): values that never round-trip to the provider ride
_meta, notproviderMetadata. - The OpenAI handoff round release (§8.0 item 14, §10 item 46): when a handoff resolves a round, a call of that round with no result and no run-item stops being pending at
handoff_occurred, and the round’s deferred close is released. - A cross-invoke tool result lands in the later invoke’s own turn, as its own
role: "tool"message (§5.0 INV-XINV), and a Claude deferred tool closes the turn paused with one approval ask (§8.0 item 32; §10 item 47). - Remedy-shaped vendor data is advisory and never executed automatically, and a provider credit token is never emitted (§13.10, §10 item 52). A fix the framework waits on is a
hitl.ask; a fix it only reports rides, less what §13.10 forbids, in a carrier that folds (§8.0 item 33, §10 item 53). §13.6 namesmessageMetadata. No carry moved: the homes are the ones the 0.7.0 and 0.7.1 notes describe. retriableon the error outcome (§10 item 54):AgOutcome’s error variant gains an optionalretriable, copied fromturn.erroras the producer set it.- Whose value
threadIdis (§1.2; §8.0 Partition root and host obligation 6; §10 item 55): the host’s. No schema change. - Editorial: a key-replace
state.deltapatch never removes a member;handoff‘stransferandescalateare defined from the producers’ semantics;turn.done.messageMetadatareplaces the named message’s bag whole; §13.4 and §13.6 gain host-care sentences.
draft.4 envelopes remain accepted (same major, §12).
@silverprotocol/core
toPersistable(result)returns a deep copy of a fold withdisplayRequiredremoved from every turn record, and nothing else changed. Persist its result unless the grounding provider’s terms permit storingdisplayRequired.outcome.retriable. The error outcome recordsturn.error’sretriablewhen the producer set it; an absent one stays absent. The non-terminalerrorevent’sretriablenever folds. Two committed Claude golden folds, an API authentication failure and a deferred tool no longer available, now carryretriable: false. A reader that validates a storedAgTurnRecordwith an older core’s schema drops the field.AgUsagegains the optionalcostScopeandtotalTokensRaw. A core from 0.6.5 on passes them through as unknown fields; a core up to 0.6.4 drops them at ingest.- The
resource-linkblock gains the optionalname,title,descriptionandsize(an integer), and its schema is exported asAgResourceLinkBlock, so a normalizer can check each native member against it. StreamAssemblernever reopens a closed turn, so a late mergingturn.startor a secondsubagentStartfor a closed id no longer makesflush()emit a second terminal.openTurn()on a turn already seen now keeps a trigger passed to it. No shipped framework package reaches either path.Reducer.ensureTurn()is removed (see Upgrading).
Claude Agent SDK
- Usage accounting follows draft.5 (see Upgrading).
- A deferred tool pauses the turn. When a PreToolUse hook answers
defer, the result closes the turn frompush()with onehitl.ask(kind: "approval",askIdapproval_<call id>) andturn.donewith apausedoutcome naming it. The resumed invoke opens its own turn, and the call’s result lands there as arole: "tool"message. In 0.7.x the turn closed as a success withfinishReason: "unknown",finishReasonRaw: "tool_deferred"and no ask. Theext.anthropic.result-metacarry is unchanged. - Host-only fields moved to
_meta(see Upgrading). turn.start.trigger. A top-level turn’sturn.startcarriestrigger: {kind: "user", ref}from the CLI’suser_message_uuid, taken from the frame that opens the turn.kind: "user"means a user-role send the host submitted with that uuid, not human authorship. It’s never set from an error result, a nested frame, a notice or a later frame of the turn.- Hardens the
tool_result_metacarry: a provider credit token at any depth is removed before the entry is emitted, as for the other CLI wrapper carries. - The README documents the existing
threadIdoption as the thread root.
OpenAI Agents SDK
- A parallel handoff no longer leaves its source turn open. When the model requests several transfers in one response,
@openai/agentsfrom 0.8.1 runs the first and streams nothing for the others. Athandoff_occurred, each call of the source round that has no result and that no run-item named now stops being pending. It’s carried asext.openai.dropped-callinside the source turn and gets notool.done. Once nothing of the round is pending, the round’s deferred close is released in the samepush():message.end, then the deferredturn.done, with its outcome, finish reason and usage. In 0.7.x the source turn ended withturn.abort(stream-truncated) at flush, its usage on the round’smessage.end. A result that arrives later for a released call rides the sameext.openai.dropped-callkey after the turn’s terminal, never as atool.done. This replaces the parallel-handoff limit described in the 0.7.1 and 0.7.2 notes for@openai/agents0.8.1 and later.- Still deferred: when a filter removes
handoff_occurredor tool results; an approval pending beside the transfer on@openai/agents0.8.x, which closes paused at flush; and pending program, hosted-shell or tool-search calls.
- Still deferred: when a filter removes
createOpenaiNormalizer()takes an optional{ threadId }, stamped on every turn and message, nested handoff turns included. A handoff that streams before any turn of the invoke opened gets the parent idturn_<stem>_handoff_parent.
Vercel AI SDK
- An MCP tool error folds as an error.
@ai-sdk/mcpreturns an MCP result withisError: trueas ordinary tool output; it now folds asoutcome: "error"withisError: true, where 0.7.x folded it as a success. The facet reads MCP members only from a result@ai-sdk/mcpstamped, so an ordinary tool that returns an MCP-shaped object is unaffected. - An MCP Apps view locator rides
tool.done._meta. An MCP result’s_meta.uiis now carried unchanged ontool.done._meta, so it survives a later snapshot. - A result for a call made in a previous call (a tool result, tool error or denied approval that
streamTextdelivers before its first step) now names its own message,<toolCallId>:result, in this invoke’s turn, and folds as arole: "tool"message. In 0.7.x it had nomessageIdand parked the fold. - Anthropic stop details. A refusal’s
providerMetadata.anthropic.stopDetailson the finish step now rides the step’smessage.metadata, verbatim, with any provider credit token removed at any depth. In 0.7.x it was dropped. createVercelNormalizer()takes an optional{ threadId }, stamped on every entity.ext.vercel.*never follows it.
Google ADK
- Ids change to a per-invoke stem (see Upgrading).
- Live sessions get a turn per generation. On a barge-in, ADK sends
interrupted: truefirst, then the interrupted generation’s usage andturnComplete, then the reply. The interrupted generation now closes asturn.abort(interrupted) on its ownturnComplete, with its usage on itsmessage.end, and the reply opens the next turn,turn_<invokeId>_<invocationId>_g1. A live barge-in closes the interrupted generation and folds clean, as two live captures show, one of them with two barge-ins. This closes the known limit in the 0.7.1 note, where a real barge-in parked the reducer.- Still limited: a generation that follows a completed reply with no barge-in lands in the closed turn, and parks, unless you opt into
hostCompletion, which defers that close. When text is buffered at the interrupt, ADK yields only the text aggregate, without the flag, so the turn closes atflush()asstream-truncated. - A transcription reads once. ADK’s
finishedtranscription is no longer re-emitted when it repeats the chunks already streamed.
- Still limited: a generation that follows a completed reply with no barge-in lands in the closed turn, and parks, unless you opt into
- Gemini Live usage. Over the Live API,
outputTokensnow counts the response and the thoughts; 0.7.x counted the thoughts alone.totalTokensis derived asinputTokens + outputTokens + (toolUseInputTokens ?? 0), the total Gemini’sgenerateContentsurface reports for the same counts, not a billing figure. Google’s own total ridestotalTokensRawwhere it differs, and neither is emitted when Google reports no total. Each report’s per-modality breakdown ridesmessage.metadataasusageDetails, one verbatim entry per report. Live usage stored before 0.8.0 is unreliable. Usage on thegenerateContentroute is unchanged. - MCP resource links. An MCP
resource_linkpart of a tool result becomes oneresource-linkblock, with aprovider-rawright after it only for members that don’t fit. Image, audio and embedded-resource parts stayprovider-raw. createAdkNormalizer()takes an optional{ threadId }, stamped on every turn and message it opens, the host-error turn included.
Wire version
1.0.0-draft.5. Replaying the 73 recorded streams through the 0.7.2 and 0.8.0 packages, output is byte-identical for 14 of 15 OpenAI Agents SDK streams and 6 of 8 Vercel AI SDK streams. Every Claude stream changes (usage accounting), and so does every Google ADK stream (ids). @silverprotocol/richtext is unchanged apart from its version.