Interactions

The Interactions API models a conversation as a single resource — an interaction — rather than a list of messages you resend on every call. Each interaction has a lifecycle (create, fetch, cancel, delete) and can either be persisted server-side or replayed statelessly from your own history.

The Interaction Lifecycle

//> using dep com.softwaremill.sttp.ai::gemini:0.8.0

import sttp.ai.gemini.GeminiSyncClient
import sttp.ai.gemini.requests.InteractionRequest

object LifecycleExample:
  def main(args: Array[String]): Unit =
    val gemini = GeminiSyncClient.fromEnv
    try {
      // Create
      val created = gemini.createInteraction(
        InteractionRequest.simple("gemini-2.5-flash", "Reply with exactly one word: ping")
      )
      println(s"status: ${created.status}")

      // Get (only meaningful when the interaction was stored server-side; created.id is None for stateless responses)
      val id = created.id.getOrElse(throw new RuntimeException("interaction was not stored"))
      val fetched = gemini.getInteraction(id)
      println(fetched.outputText)

      // Cancel a still-running interaction (e.g. one created with background = Some(true))
      // gemini.cancelInteraction(id)

      // Delete
      gemini.deleteInteraction(id)
    } finally gemini.close()

createInteraction/getInteraction/cancelInteraction return an InteractionResponse; deleteInteraction returns Unit. All four throw a GeminiException subclass on failure on the sync client, or hand back an Either[GeminiException, A] on the async GeminiClient (see basics.md).

InteractionResponse.id is an Option[String] — it is absent on stateless (store = false) responses, since there is nothing server-side to identify.

store: server-side persistence

The API’s own default is store = true — every interaction you create is persisted server-side unless you opt out. Pass store = Some(false) to skip persistence (nothing to fetch or delete afterwards):

//> using dep com.softwaremill.sttp.ai::gemini:0.8.0

import sttp.ai.gemini.requests.InteractionRequest

object StoreExample:
  def main(args: Array[String]): Unit =
    val request = InteractionRequest
      .simple("gemini-2.5-flash", "Reply with exactly one word: stored")
      .copy(store = Some(true)) // the default; shown here for clarity

    val ephemeral = InteractionRequest
      .simple("gemini-2.5-flash", "Reply with exactly one word: ephemeral")
      .copy(store = Some(false))

Multi-turn conversations

There are two ways to continue a conversation, matching the two store modes:

  • Server-side state — set store = Some(true) (or leave it unset) on the first call, then pass the returned interaction’s id as previousInteractionId on the next request. Gemini reconstructs the conversation from what it already persisted.

  • Stateless replay — set store = Some(false) and pass the entire conversation so far as InteractionInput.StepsInput(steps). This is what GeminiAgent’s backend always does internally (see tool-calling.md), since an agent loop shouldn’t depend on server-side state surviving between iterations.

//> using dep com.softwaremill.sttp.ai::gemini:0.8.0

import sttp.ai.gemini.GeminiSyncClient
import sttp.ai.gemini.models.{Content, InteractionInput, Step}
import sttp.ai.gemini.requests.InteractionRequest

object MultiTurnExample:
  def main(args: Array[String]): Unit =
    val gemini = GeminiSyncClient.fromEnv
    try {
      // Server-side: continue via previousInteractionId
      val first = gemini.createInteraction(InteractionRequest.simple("gemini-2.5-flash", "My name is Ada."))
      val second = gemini.createInteraction(
        InteractionRequest
          .simple("gemini-2.5-flash", "What is my name?")
          .copy(previousInteractionId = first.id)
      )
      println(second.outputText)

      // Stateless: replay the whole conversation as steps
      val steps = List(
        Step.userText("My name is Ada."),
        Step.ModelOutput(List(Content.Text("Nice to meet you, Ada!"))),
        Step.userText("What is my name?")
      )
      val replayed = gemini.createInteraction(
        InteractionRequest(
          model = "gemini-2.5-flash",
          input = InteractionInput.StepsInput(steps),
          store = Some(false)
        )
      )
      println(replayed.outputText)
    } finally gemini.close()

Step and Content: the conversation model

Step is the unit of both request input (when using InteractionInput.StepsInput) and response steps:

sealed trait Step
object Step {
  case class UserInput(content: List[Content]) extends Step
  case class ModelOutput(content: List[Content]) extends Step
  case class FunctionCall(id: String, name: String, arguments: io.circe.Json) extends Step
  case class FunctionResult(callId: String, name: String, result: io.circe.Json) extends Step
  case class Thought(signature: Option[String], content: Option[List[Content]]) extends Step // model reasoning, returned by the live API
  case class Unknown(raw: io.circe.Json) extends Step // forward-compat fallback for step types this client doesn't know yet

  def userText(text: String): Step = UserInput(List(Content.Text(text)))
}

The set of Step subtypes is open: the live API can return Thought steps (and, if it introduces new step types, they decode as Unknown rather than failing). Any match over Step should handle Thought/Unknown explicitly, or fall back to a wildcard case.

Content is the payload carried by UserInput/ModelOutput steps — plain text or a media reference (inline base64 data or a remote uri, with an optional mimeType):

sealed trait Content
object Content {
  case class Text(text: String) extends Content
  case class Image(data: Option[String] = None, uri: Option[String] = None, mimeType: Option[String] = None) extends Content
  case class Audio(data: Option[String] = None, uri: Option[String] = None, mimeType: Option[String] = None) extends Content
  case class Video(data: Option[String] = None, uri: Option[String] = None, mimeType: Option[String] = None) extends Content
  case class Document(data: Option[String] = None, uri: Option[String] = None, mimeType: Option[String] = None) extends Content
}

InteractionResponse exposes two convenience accessors built on top of steps:

  • outputText: String — all text content of the last ModelOutput step, concatenated

  • functionCalls: List[Step.FunctionCall] — every function call the model requested, in order

See tool-calling.md for how to answer those function calls with Step.FunctionResult.