Skip to content

Commit 3f8e654

Browse files
authored
fix: migrate Gemini Interactions emitter to SDK 2.x event protocol (#277) (#279)
## Summary Migrates the Gemini **Interactions** mock from the SDK 1.x event/response format to SDK 2.x (the "Interactions breaking changes, May 2026" shapes in `@google/genai` v2) — covering **both** the streamed SSE emitter **and** the non-streaming (unary) JSON response builders. Previously the streamed path produced `interaction.start`/`interaction.complete` + `content.start`/`content.delta`/`content.stop` (**zero `event_type` overlap** with the v2 adapter — a migrated consumer hit its `switch` default for every event and rendered an empty assistant message), and the non-streaming path emitted a 1.x `outputs` envelope the v2 consumer never reads. Both paths now speak v2. Closes #277. ## What changed — streamed SSE **`src/gemini-interactions.ts`** — rewrote the three SSE builders: - Lifecycle: `interaction.start`/`complete` → `interaction.created`/`completed` (id stays populated on both; status text→`completed`, tool calls→`requires_action`). - Text: `content.start/delta/stop` → `step.start { step: { type: "model_output" } }` / `step.delta { delta: { type: "text", text } }` / `step.stop`. - Tool calls: call identity (`id`, `name`) now lives on `step.start` with an `arguments: {}` placeholder; arguments stream as `step.delta { delta: { type: "arguments_delta", arguments: "<json-string fragment>" } }`, valid JSON by `step.stop`. - `writeGeminiInteractionsSSEStream` now counts `step.delta` (not `content.delta`) for the `truncateAfterChunks` budget. **`src/stream-collapse.ts`** — `collapseGeminiInteractionsSSE` now parses 2.x events (assemble tool calls from `step.start` identity + `arguments_delta` string fragments keyed by index; nested `thought_summary.content.text`), while keeping 1.x parsing for previously recorded fixtures. Hardened against silent data loss: malformed assembled args and index-less / uncorrelated `step.start`/`arguments_delta` are flagged via `droppedChunks`/`firstDroppedSample` rather than written silently. ## What changed — non-streaming (unary) responses The `@google/genai` v2 Interactions consumer (`extractTextFromInteraction` in `packages/ai-gemini/src/experimental/text-interactions/adapter.ts`, TanStack/ai#781) reads a non-streaming interaction from `interaction.output_text` (string fast-path) then walks `interaction.steps` — `model_output` steps' `content[]` text parts and `function_call` steps — and **never** reads `outputs`. So a v2 consumer doing a non-streaming completion got a silently empty result: the same failure class the streamed path fixed. These builders are reachable (`if (!streaming)` routing in `src/gemini-interactions.ts`). Migrated `outputs → steps`: - `buildInteractionsTextResponse` → `{ id, status: "completed", model, role: "model", output_text: content, steps: [{ type: "model_output", content: [{ type: "text", text: content }] }], usage }`. - `buildInteractionsToolCallResponse` → `steps: toolCalls.map(... { type: "function_call", id, name, arguments: <parsed> })`, status `requires_action`; preserves the malformed-args `try/catch` + `logger.warn` guard (extracted into a shared `buildFunctionCallStep` helper). - `buildInteractionsContentWithToolCallsResponse` → `output_text` + a `model_output` text step followed by `function_call` steps. ## Tests + drift baseline Updated unit/integration/collapse tests and `src/__tests__/drift/sdk-shapes.ts` (`geminiInteractionsStreamEventShapes`, `geminiInteractionsResponseShape`, `geminiInteractionsToolCallResponseShape`) to 2.x; added round-trip, multiple/interleaved tool calls, ordering, malformed-args, usage/status, and 1.x backward-compat coverage; flipped the non-streaming unit/integration assertions to the v2 `steps`/`output_text` shape. ## Verification ```bash grep -E 'event_type: "(step|interaction\.created|interaction\.completed)' dist/gemini-interactions.js ``` matches after build (0 legacy shapes remain). Full suite: **3982 passing / 44 skipped**, prettier + eslint clean, build OK. Red → green proof for the non-streaming migration (assertions run against the real builder output / real server JSON, not a re-implementation): ```text # RED (against the old `outputs` source, before the builder change) × response builders > builds text response → expected undefined to be 'Hello!' // Object.is equality × Gemini Interactions — non-streaming > returns tool call response → Target cannot be null or undefined. Tests 2 failed | 97 skipped (99) # (full file pre-fix: 15 failed | 84 passed (99)) # GREEN (after migrating the builders to `steps`) ✓ src/__tests__/gemini-interactions.test.ts (99 tests | 97 skipped) Tests 2 passed | 97 skipped (99) # (full file: 99 passed (99); full suite: 3982 passed | 44 skipped; eslint + prettier clean; build OK) ``` This unblocks re-enabling the `stateful-interactions` e2e test in TanStack/ai#781 once aimock is bumped/released. ## Notes / scope - One ambiguous case is deliberately not signalled: a streamed `function_call` whose `step.start` carries an empty `{}` placeholder but never receives an `arguments_delta` finalizes to `"{}"`. On the wire that is byte-identical to a legitimately empty-args call, so flagging it would false-positive on every legitimate empty-args call. - Empty-text non-streaming responses emit `output_text: ""` and a single `model_output` step whose text part is `""` (mirrors the streamed empty-content behavior). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
2 parents 538a061 + ef3a3fc commit 3f8e654

5 files changed

Lines changed: 577 additions & 222 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## [Unreleased]
44

5+
### Fixed
6+
7+
- Gemini Interactions mock now emits the SDK 2.x event protocol on both paths — streamed SSE (`step.*`, `interaction.created`/`completed`, tool args via `arguments_delta`) and non-streaming responses (`steps`/`output_text`); legacy 1.x recorded fixtures still parse (#279)
8+
59
## [1.33.0] - 2026-06-23
610

711
### Added

src/__tests__/drift/sdk-shapes.ts

Lines changed: 32 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1587,7 +1587,8 @@ export function geminiInteractionsResponseShape(): ShapeNode {
15871587
status: "completed",
15881588
model: "gemini-2.5-flash",
15891589
role: "model",
1590-
outputs: [{ type: "text", text: "Hello!" }],
1590+
output_text: "Hello!",
1591+
steps: [{ type: "model_output", content: [{ type: "text", text: "Hello!" }] }],
15911592
usage: { total_input_tokens: 0, total_output_tokens: 0, total_tokens: 0 },
15921593
});
15931594
}
@@ -1598,7 +1599,7 @@ export function geminiInteractionsToolCallResponseShape(): ShapeNode {
15981599
status: "requires_action",
15991600
model: "gemini-2.5-flash",
16001601
role: "model",
1601-
outputs: [
1602+
steps: [
16021603
{
16031604
type: "function_call",
16041605
id: "call_abc123",
@@ -1613,43 +1614,43 @@ export function geminiInteractionsToolCallResponseShape(): ShapeNode {
16131614
export function geminiInteractionsStreamEventShapes(): SSEEventShape[] {
16141615
return [
16151616
{
1616-
type: "interaction.start",
1617+
type: "interaction.created",
16171618
dataShape: extractShape({
1618-
event_type: "interaction.start",
1619+
event_type: "interaction.created",
16191620
interaction: { id: "int_abc123", status: "in_progress" },
16201621
event_id: "evt_1",
16211622
}),
16221623
},
16231624
{
1624-
type: "content.start",
1625+
type: "step.start",
16251626
dataShape: extractShape({
1626-
event_type: "content.start",
1627+
event_type: "step.start",
16271628
index: 0,
1628-
content: { type: "text" },
1629+
step: { type: "model_output" },
16291630
event_id: "evt_2",
16301631
}),
16311632
},
16321633
{
1633-
type: "content.delta",
1634+
type: "step.delta",
16341635
dataShape: extractShape({
1635-
event_type: "content.delta",
1636+
event_type: "step.delta",
16361637
index: 0,
16371638
delta: { type: "text", text: "Hello" },
16381639
event_id: "evt_3",
16391640
}),
16401641
},
16411642
{
1642-
type: "content.stop",
1643+
type: "step.stop",
16431644
dataShape: extractShape({
1644-
event_type: "content.stop",
1645+
event_type: "step.stop",
16451646
index: 0,
16461647
event_id: "evt_4",
16471648
}),
16481649
},
16491650
{
1650-
type: "interaction.complete",
1651+
type: "interaction.completed",
16511652
dataShape: extractShape({
1652-
event_type: "interaction.complete",
1653+
event_type: "interaction.completed",
16531654
interaction: {
16541655
id: "int_abc123",
16551656
status: "completed",
@@ -1664,48 +1665,51 @@ export function geminiInteractionsStreamEventShapes(): SSEEventShape[] {
16641665
export function geminiInteractionsToolCallStreamEventShapes(): SSEEventShape[] {
16651666
return [
16661667
{
1667-
type: "interaction.start",
1668+
type: "interaction.created",
16681669
dataShape: extractShape({
1669-
event_type: "interaction.start",
1670+
event_type: "interaction.created",
16701671
interaction: { id: "int_abc123", status: "in_progress" },
16711672
event_id: "evt_1",
16721673
}),
16731674
},
16741675
{
1675-
type: "content.start",
1676+
type: "step.start",
16761677
dataShape: extractShape({
1677-
event_type: "content.start",
1678+
event_type: "step.start",
16781679
index: 0,
1679-
content: { type: "function_call" },
1680+
step: {
1681+
type: "function_call",
1682+
id: "call_abc123",
1683+
name: "get_weather",
1684+
arguments: {},
1685+
},
16801686
event_id: "evt_2",
16811687
}),
16821688
},
16831689
{
1684-
type: "content.delta",
1690+
type: "step.delta",
16851691
dataShape: extractShape({
1686-
event_type: "content.delta",
1692+
event_type: "step.delta",
16871693
index: 0,
16881694
delta: {
1689-
type: "function_call",
1690-
id: "call_abc123",
1691-
name: "get_weather",
1692-
arguments: { city: "Paris" },
1695+
type: "arguments_delta",
1696+
arguments: '{"city":"Paris"}',
16931697
},
16941698
event_id: "evt_3",
16951699
}),
16961700
},
16971701
{
1698-
type: "content.stop",
1702+
type: "step.stop",
16991703
dataShape: extractShape({
1700-
event_type: "content.stop",
1704+
event_type: "step.stop",
17011705
index: 0,
17021706
event_id: "evt_4",
17031707
}),
17041708
},
17051709
{
1706-
type: "interaction.complete",
1710+
type: "interaction.completed",
17071711
dataShape: extractShape({
1708-
event_type: "interaction.complete",
1712+
event_type: "interaction.completed",
17091713
interaction: {
17101714
id: "int_abc123",
17111715
status: "requires_action",

0 commit comments

Comments
 (0)