# Gemini 2.5 empty-response quirk — follow-up

> **Status:** known issue, mitigated at the consumer level (showcase). Pending a
> proper transport-level fix in a follow-up PR.

## Symptom

On the first `generateContent` call against `gemini-2.5-flash` (and reportedly
also `gemini-2.5-pro`), the API occasionally returns a 200 response with **no
output** when:

- `contents` contains only user-role messages — i.e. no prior model turn, AND
- the request has a non-trivial `tools` array and/or a long `systemInstruction`.

The shape of the broken response:

```json
{
  "candidates": [{
    "content": { "role": "model" },
    "finishReason": "STOP",
    "index": 0
  }],
  "usageMetadata": {
    "promptTokenCount": 2373,
    "totalTokenCount": 2373
  }
}
```

Note `totalTokenCount === promptTokenCount` — Gemini emitted **zero** output
tokens. `finishReason` is `STOP` (normal termination), not a safety block. The
`candidatesTokenCount` field is missing entirely.

In `ChatDriver` this hits the empty-response retry path. Three retries later we
give up and surface "Remote agent returned no response." The second user turn
always succeeds because there is now a prior model turn in history (even our
own "Remote agent returned no response" reply is enough of an anchor).

## Known mitigations

Only one workaround has been reported as effective: **inject a prior model turn
into the request**. Confirmed ineffective in community reports:

- Lowering `temperature`.
- Setting `thinkingConfig: { thinkingBudget: 0 }`.

References:

- <https://discuss.ai.google.dev/t/finishreason-stop-but-parts-is-missing-inside-candidate/99331>
- <https://github.com/livekit/agents/issues/4706>
- <https://github.com/BerriAI/litellm/issues/24442>

## Current mitigation (showcase only)

The showcase `genesis-assistant` configures a minimal `primerHistory` on the
trades agent — a benign `[user: "Hi", assistant: "Hi — how can I help…"]`
exchange. That single fake model turn is enough to satisfy Gemini.

We deliberately keep the primer **innocuous**: any text resembling a system
update or directive reinforcement (e.g. `[SYSTEM UPDATE]: Your core behavioral
directives are…`) is caught by Gemini's prompt-injection safety training and
rejected. Plain greeting language is fine.

## Proposed follow-up: fix in `gemini-transport`

The right place to absorb this is `packages/foundation/foundation-ai/src/transports/gemini-transport.ts`
— specifically `toGeminiContents`. If the assembled `contents` array contains
**only** user-role entries (no model turn at all) at send time, prepend a
synthetic anchor pair:

```ts
contents.unshift(
  { role: 'user', parts: [{ text: 'Hi' }] },
  { role: 'model', parts: [{ text: 'Hi! How can I help today?' }] },
);
```

Properties of this approach:

- **Transport-scoped** — only applies when sending to Gemini; other transports
  (Claude, OpenAI, etc.) are unaffected.
- **Transparent to the framework** — `ChatDriver` and the rest of the UI never
  see the synthetic turns. They are not stored in `this.history`, not rendered,
  not surfaced to consumers.
- **Self-cleaning** — once the conversation has any real model turn, the
  injection no-ops.

### Why we did **not** ship this now

A consumer-level fix is more visible and easier to reason about. Hiding the
workaround inside the transport risks masking related issues — e.g. if the
shape of system prompts or tool sets ever evolves such that Gemini fails
elsewhere, an always-on anchor would silently hide the regression. Surfacing
the workaround as an explicit `primerHistory` on each agent gives consumers a
chance to notice and remove it when it's no longer needed.

The transport-level fix is worth doing **once** the quirk is confirmed against
Google's official guidance or a forum/issue closeout — at which point we can
also remove the per-agent primers.

## Acceptance criteria for the follow-up PR

- [ ] Add the anchor-injection logic in `gemini-transport.ts:toGeminiContents`.
- [ ] Unit test: contents containing only user messages get the anchor prepended.
- [ ] Unit test: contents containing any model message do **not** get the anchor.
- [ ] Remove the showcase `tradesPrimerHistory` and confirm "Book a trade" works
      on the first turn.
- [ ] Add an integration note / TODO so the workaround can be ripped out when
      Google ships a fix.
