Commit 3f8e654
authored
## 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)
5 files changed
Lines changed: 577 additions & 222 deletions
File tree
- src
- __tests__
- drift
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
5 | 9 | | |
6 | 10 | | |
7 | 11 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1587 | 1587 | | |
1588 | 1588 | | |
1589 | 1589 | | |
1590 | | - | |
| 1590 | + | |
| 1591 | + | |
1591 | 1592 | | |
1592 | 1593 | | |
1593 | 1594 | | |
| |||
1598 | 1599 | | |
1599 | 1600 | | |
1600 | 1601 | | |
1601 | | - | |
| 1602 | + | |
1602 | 1603 | | |
1603 | 1604 | | |
1604 | 1605 | | |
| |||
1613 | 1614 | | |
1614 | 1615 | | |
1615 | 1616 | | |
1616 | | - | |
| 1617 | + | |
1617 | 1618 | | |
1618 | | - | |
| 1619 | + | |
1619 | 1620 | | |
1620 | 1621 | | |
1621 | 1622 | | |
1622 | 1623 | | |
1623 | 1624 | | |
1624 | | - | |
| 1625 | + | |
1625 | 1626 | | |
1626 | | - | |
| 1627 | + | |
1627 | 1628 | | |
1628 | | - | |
| 1629 | + | |
1629 | 1630 | | |
1630 | 1631 | | |
1631 | 1632 | | |
1632 | 1633 | | |
1633 | | - | |
| 1634 | + | |
1634 | 1635 | | |
1635 | | - | |
| 1636 | + | |
1636 | 1637 | | |
1637 | 1638 | | |
1638 | 1639 | | |
1639 | 1640 | | |
1640 | 1641 | | |
1641 | 1642 | | |
1642 | | - | |
| 1643 | + | |
1643 | 1644 | | |
1644 | | - | |
| 1645 | + | |
1645 | 1646 | | |
1646 | 1647 | | |
1647 | 1648 | | |
1648 | 1649 | | |
1649 | 1650 | | |
1650 | | - | |
| 1651 | + | |
1651 | 1652 | | |
1652 | | - | |
| 1653 | + | |
1653 | 1654 | | |
1654 | 1655 | | |
1655 | 1656 | | |
| |||
1664 | 1665 | | |
1665 | 1666 | | |
1666 | 1667 | | |
1667 | | - | |
| 1668 | + | |
1668 | 1669 | | |
1669 | | - | |
| 1670 | + | |
1670 | 1671 | | |
1671 | 1672 | | |
1672 | 1673 | | |
1673 | 1674 | | |
1674 | 1675 | | |
1675 | | - | |
| 1676 | + | |
1676 | 1677 | | |
1677 | | - | |
| 1678 | + | |
1678 | 1679 | | |
1679 | | - | |
| 1680 | + | |
| 1681 | + | |
| 1682 | + | |
| 1683 | + | |
| 1684 | + | |
| 1685 | + | |
1680 | 1686 | | |
1681 | 1687 | | |
1682 | 1688 | | |
1683 | 1689 | | |
1684 | | - | |
| 1690 | + | |
1685 | 1691 | | |
1686 | | - | |
| 1692 | + | |
1687 | 1693 | | |
1688 | 1694 | | |
1689 | | - | |
1690 | | - | |
1691 | | - | |
1692 | | - | |
| 1695 | + | |
| 1696 | + | |
1693 | 1697 | | |
1694 | 1698 | | |
1695 | 1699 | | |
1696 | 1700 | | |
1697 | 1701 | | |
1698 | | - | |
| 1702 | + | |
1699 | 1703 | | |
1700 | | - | |
| 1704 | + | |
1701 | 1705 | | |
1702 | 1706 | | |
1703 | 1707 | | |
1704 | 1708 | | |
1705 | 1709 | | |
1706 | | - | |
| 1710 | + | |
1707 | 1711 | | |
1708 | | - | |
| 1712 | + | |
1709 | 1713 | | |
1710 | 1714 | | |
1711 | 1715 | | |
| |||
0 commit comments