streamaccumulator

package
v0.7.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 29, 2026 License: Apache-2.0 Imports: 4 Imported by: 0

Documentation

Overview

Package streamaccumulator folds streaming content chunks into complete content blocks. It is a pure converter shared by the loop and the TUI/CLI live display path:

ThinkingChunk -> ThinkingBlock
TextChunk     -> TextBlock
ToolUseChunk  -> ToolUseBlock
RefusalChunk  -> RefusalBlock
ImageChunk    -> ImageBlock

It does NOT send events, validate tool permissions, decide turn failure, or know about the loop. Policy stays in the loop; this package only converts. It deliberately imports nothing beyond the standard library and internal/content (in particular, never internal/agent/loop or its event package) so it carries no dependency cycle.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Images

type Images struct {
	// contains filtered or unexported fields
}

Images folds streaming ImageChunk deltas into complete ImageBlocks. Like ToolUses it is keyed by the provider-supplied Index with a map rather than slice indexing, so a negative or huge Index — the value is provider/attacker-influenced — can NEVER panic or allocate an unbounded slice.

Per-image keying is load-bearing for correctness, not ordering: appending one image's bytes to another's produces a corrupt file that nothing downstream can detect or recover. Data fragments for an Index append in arrival order; URL and MediaType take the last non-empty value, since they arrive whole rather than fragmented. Blocks() emits the assembled blocks in ASCENDING Index order (the deterministic response order). The zero value is ready to use.

func (*Images) Add

func (a *Images) Add(chunk *content.ImageChunk)

Add folds one delta into the accumulator, bounds-safe on any Index value. The delta's bytes are appended into the part's own buffer, so the caller may reuse or mutate its slice after the call without rewriting accumulated bytes.

func (Images) Blocks

func (a Images) Blocks() []content.ImageBlock

Blocks returns the assembled ImageBlocks in ascending Index order, or nil if no chunk was received. Each block gets its OWN copy of the accumulated bytes, so mutating a returned block cannot corrupt the accumulator or a block handed to another caller. The bytes are used verbatim; this package never validates that they decode as the claimed MediaType.

func (Images) Empty

func (a Images) Empty() bool

Empty reports whether no chunk has been added yet.

type Refusal

type Refusal struct {
	// contains filtered or unexported fields
}

Refusal folds streamed RefusalChunk deltas into a single RefusalBlock. It mirrors Text because RefusalChunk mirrors TextChunk: refusal deltas carry no Index and concatenate losslessly (see content.RefusalChunk for why).

The received flag, not the accumulated string, decides whether a block exists. A provider may refuse with no explanation at all, and an empty refusal must still materialize a RefusalBlock: dropping it would restore the exact bug this type was added to fix — a refusal decoding as a successful empty reply. The zero value is ready to use.

func (*Refusal) Add

func (a *Refusal) Add(chunk *content.RefusalChunk)

Add appends one refusal delta to the accumulator.

func (Refusal) Block

func (a Refusal) Block() *content.RefusalBlock

Block returns the accumulated RefusalBlock, or nil if no chunk was received.

func (Refusal) Empty

func (a Refusal) Empty() bool

Empty reports whether no chunk has been added yet.

type Text

type Text struct {
	// contains filtered or unexported fields
}

Text folds streamed TextChunk deltas into a single TextBlock. The zero value is ready to use.

func (*Text) Add

func (a *Text) Add(chunk *content.TextChunk)

Add appends one text delta to the accumulator.

func (Text) Block

func (a Text) Block() *content.TextBlock

Block returns the accumulated TextBlock, or nil if no chunk was received.

func (Text) Empty

func (a Text) Empty() bool

Empty reports whether no chunk has been added yet.

type Thinking

type Thinking struct {
	// contains filtered or unexported fields
}

Thinking folds streaming ThinkingChunk deltas into complete ThinkingBlocks. It is keyed by the provider-supplied Index the same way ToolUses is, and for the same two reasons: a response may legitimately contain SEVERAL reasoning blocks (Anthropic interleaved thinking opens a fresh thinking / redacted_thinking block around every tool call), and the Index is provider/attacker-influenced, so a map — never slice indexing — keeps a negative or huge Index from panicking or allocating an unbounded slice.

Per-block keying is load-bearing for continuation state, not a nicety. Each reasoning block carries its OWN signature or opaque provider state, and the replayed sequence must match the sequence the model generated block-for-block. Folding N blocks into one builder with a last-write-wins signature would silently rebind the last signature to the concatenation of every block's text, which the provider rejects and which the non-streaming decoders never produce — breaking the invariant that streaming preserves the same continuation state as non-streaming.

Producers that emit a single reasoning block leave Index at zero, so every delta accumulates into that one block exactly as before. Blocks() emits the assembled blocks in ASCENDING Index order (the deterministic response order). The zero value is ready to use.

func (*Thinking) Add

func (a *Thinking) Add(chunk *content.ThinkingChunk)

Add folds one delta into the accumulator, bounds-safe on any Index value.

func (Thinking) Block

func (a Thinking) Block() *content.ThinkingBlock

Block returns the first assembled thinking block, or nil when empty. Deprecated: use Blocks to preserve multiple provider reasoning blocks. Single-block streams retain the exact behavior of the original API.

func (Thinking) Blocks

func (a Thinking) Blocks() []content.ThinkingBlock

Blocks returns the assembled ThinkingBlocks in ascending Index order, or nil if no chunk was received. Gaps in the Index sequence order the blocks but never materialize filler blocks: only indexes that actually received a delta appear.

func (Thinking) Empty

func (a Thinking) Empty() bool

Empty reports whether no chunk has been added yet.

type ToolUses

type ToolUses struct {
	// contains filtered or unexported fields
}

ToolUses folds streaming ToolUseChunk deltas into complete ToolUseBlocks. It is keyed by the provider-supplied Index (which is provider/attacker-influenced), so it uses a map rather than slice indexing: a negative or huge Index can NEVER panic or allocate an unbounded slice. The first delta for an Index typically carries ID/Name; later deltas carry InputJSON fragments to concatenate. Blocks() emits the assembled blocks in ASCENDING Index order (the deterministic response order). The zero value is ready to use.

func (*ToolUses) Add

func (a *ToolUses) Add(chunk *content.ToolUseChunk)

Add folds one delta into the accumulator, bounds-safe on any Index value.

func (ToolUses) Blocks

func (a ToolUses) Blocks() []content.ToolUseBlock

Blocks returns the assembled ToolUseBlocks in ascending Index order, or nil if no chunk was received. The raw concatenated Input is used verbatim; any validation or sanitization happens in the caller.

func (ToolUses) Empty

func (a ToolUses) Empty() bool

Empty reports whether no chunk has been added yet.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL