Skip to content

gemini-interactions: emitter still produces SDK 1.x events; migrate to SDK 2.x step.* / interaction.created|completed #277

Description

@tombeckenham

Summary

The Gemini Interactions mock emitter still produces the SDK 1.x event format. @google/genai shipped the "Interactions breaking changes (May 2026)" in v2, which renamed/reshaped the streamed event protocol. As of 1.33.0 (latest), dist/gemini-interactions.js still emits the old content.* / interaction.start / interaction.complete shapes, so a consumer migrated to the v2 (SDK 2.x) Interactions adapter can't be exercised against the mock — there is zero event_type overlap, the adapter's event switch hits its default for every event, and the assistant message comes back empty.

This blocks re-enabling the stateful-interactions e2e test in TanStack/ai#781 (which migrates geminiTextInteractions to @google/genai v2). That test is currently test.fixme purely because of this mock-vs-adapter wire-format mismatch — the adapter itself is covered by unit tests against hand-written 2.x events.

Ref: https://ai.google.dev/gemini-api/docs/interactions-breaking-changes-may-2026

Root cause: zero event-type overlap

Concern aimock emits today (SDK 1.x) v2 adapter expects (SDK 2.x)
interaction opened interaction.start interaction.created
content block opened content.start step.start
streamed delta content.delta step.delta
content block closed content.stop step.stop
interaction finished interaction.complete interaction.completed

The v2 adapter handles exactly these top-level event_types: interaction.created, step.start, step.delta, step.stop, interaction.status_update, interaction.completed, error.

What needs to change

Source: dist/gemini-interactions.js (and its src equivalent). Two builders need rewriting: buildInteractionsTextSSEEvents and buildInteractionsToolCallSSEEvents.

1. Lifecycle events (both builders)

Rename the envelope only; the interaction object keeps id / status / usage.

- { event_type: "interaction.start",    interaction: { id, status: "in_progress" } }
+ { event_type: "interaction.created",  interaction: { id, status: "in_progress" } }

- { event_type: "interaction.complete", interaction: { id, status, usage } }
+ { event_type: "interaction.completed", interaction: { id, status, usage } }

The id must stay populated on both interaction.created and interaction.completed (consumers surface it as the interaction id). Status mapping is unchanged: text → completed; tool calls → requires_action.

2. Text streaming — buildInteractionsTextSSEEvents

Inner delta shape is identical ({ type: "text", text }); only the envelope changes, plus an optional step.start/step.stop wrapper.

- { event_type: "content.start", index: 0, content: { type: "text" } }
+ { event_type: "step.start", index: 0, step: { type: "model_output" } }

- { event_type: "content.delta", index: 0, delta: { type: "text", text: slice } }
+ { event_type: "step.delta", index: 0, delta: { type: "text", text: slice } }

- { event_type: "content.stop", index: 0 }
+ { event_type: "step.stop", index: 0 }

Minimum viable fix: a v2 adapter lazily opens the assistant message on the first step.delta { type: "text" }, so renaming content.deltastep.delta alone makes text render. Emitting the step.start { type: "model_output" } / step.stop wrapper is the spec-faithful shape and is recommended.

3. Tool calls — buildInteractionsToolCallSSEEvents

The larger change. In 2.x the call identity (id, name, arguments) lives on step.start, not in a delta, and streamed argument fragments use a dedicated arguments_delta variant carrying a string fragment (not a parsed object).

  // per tool call, at step index `idx`:
- { event_type: "content.start", index: idx, content: { type: "function_call" } }
- { event_type: "content.delta", index: idx,
-   delta: { type: "function_call", id, name, arguments: argsObj } }
- { event_type: "content.stop",  index: idx }
+ { event_type: "step.start", index: idx,
+   step: { type: "function_call", id, name, arguments: {} } }
+ // optional: stream args as JSON-string fragments
+ { event_type: "step.delta", index: idx,
+   delta: { type: "arguments_delta", arguments: "<json-string fragment>" } }
+ { event_type: "step.stop",  index: idx }

Contract for tool calls:

  • step.start.step.id is the tool-call id; consumers map index → id so later arguments_delta / step.stop at the same index resolve correctly.
  • step.start.step.arguments may be {} (placeholder) when streaming, with the real args arriving as arguments_delta fragments; it may also be a fully populated object for non-streamed calls — both should be accepted.
  • arguments_delta.arguments is a string fragment; the concatenation across all fragments for a given index must be valid JSON by step.stop.

4. Reasoning (thought) — optional / not strictly required

For completeness, 2.x reasoning is step.start { step: { type: "thought", summary?: [{ type: "text", text }] } } then step.delta { delta: { type: "thought_summary", content: { text } } }.

How to verify

grep -E 'event_type: "(step|interaction\.created|interaction\.completed)' \
  dist/gemini-interactions.js

…should match after the change. Downstream, bumping aimock + un-fixme'ing tests/stateful-interactions.spec.ts in TanStack/ai should then pass.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions