# Reasoning and Tool Calls

Before an agent answers, it often reasons, and it may call tools: search the web, look up an order, or ask the user's device for their location. The AI components show these steps in the reply, in the order they happened, with the final answer after them.

An agent describes each step as a custom attachment on its reply message:

- `ai_reasoning` - a round of the model's reasoning.
- `ai_tool_call` - a tool the agent called, run by the agent's backend or by a user's device.

The order of the attachments is the order of the steps, and the final answer stays in the message text. The agent's backend adds the steps to the reply's `attachments` and updates them as it works, with the same message updates that stream the text.

>
> **Info:** The components and types on this page are available from version 7.15.0 of the `stream-chat-android-ai-compose` artifact. See [Installation](https://getstream.io/chat/docs/sdk/android/guides/ai-integrations/#installation).
>

## Show the steps of a reply

`AIMessagePart.parts` decodes the steps among a message's attachments, skipping attachments that aren't AI steps, such as images. `AIMessageParts` shows them in order. Render it before the reply's text:

```kotlin
// Stream Chat moves an attachment's `name` out of its extraData. A tool call's step needs it back.
fun Message.aiParts(): List<AIMessagePart> = AIMessagePart.parts(
    attachments.map { it.type.orEmpty() to it.extraData + ("name" to it.name) },
)

@Composable
fun AssistantReply(message: Message) {
    Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
        AIMessageParts(parts = message.aiParts())
        StreamingText(
            text = message.text,
            animate = message.extraData["generating"] == true,
        )
    }
}
```

The Stream Chat Android SDK lifts an attachment's `name` field into `Attachment.name`, so put it back into the payload, as above. Without it, every tool call loses its name. If you have each attachment's payload as JSON text instead, use `AIMessagePart.partsFromJson`.

Each step shows through `AIMessagePartItem`: a round of reasoning as a `StreamingReasoning`, a tool call as an `AIToolCall`, and a neutral placeholder ("A step this app version can't show") for a kind of step it doesn't know. The [Compose integration](https://getstream.io/chat/docs/sdk/android/guides/ai-integrations/compose-integration/) shows where to render it in the Chat SDK's message list, and the [conversation view](https://getstream.io/chat/docs/sdk/android/guides/ai-integrations/conversation-view/) renders the steps for you.

### Steps are an open set

New kinds of steps can appear without breaking your code. A part's `kind` is an `AIMessagePart.Kind` holding the attachment type (`Kind.Reasoning`, `Kind.ToolCall`, or any other `ai_` type), and the kinds the SDK reads have typed views, `part.reasoning` and `part.toolCall`. Anything else keeps its `payload`, a map of its fields. Statuses and executors are open in the same way, so give a `when` over them an `else` branch.

Decoding is lenient: missing fields get defaults, a field of the wrong type or an empty string reads as missing, and a step in a newer format version (`v`) keeps its payload but has no typed view (`isSupported` is `false`). Each step has a stable `id` (a tool call uses the model provider's tool-call ID), so the list updates smoothly while it streams.

To show some steps your own way, such as reasoning your backend streams in full, or a kind of step of your own, render each part yourself and fall back to `AIMessagePartItem` for the rest:

```kotlin
AIMessageParts(parts = parts) { part ->
    val reasoning = part.reasoning
    when {
        reasoning != null -> StreamingReasoning(part = reasoning, text = fullReasoning[reasoning.id])
        part.kind == AIMessagePart.Kind("ai_citation") -> CitationCard(payload = part.payload)
        else -> AIMessagePartItem(part = part)
    }
}
```

Steps are keyed by their kind and `id`, so each keeps its state, such as whether its reasoning is open, as the reply updates.

## Reasoning

A round of reasoning (`AIReasoningPart`) carries a capped `preview` of the model's thoughts (the latest ones while it streams, the opening once it's done) and a one-line `summary`. `AIMessagePartItem` shows it with `StreamingReasoning`: while the model thinks, the reasoning streams into an open panel under a "Thinking... 7s" header, and once it's done, the view folds into "Thought for 12s" and the summary. Tapping the header opens or closes the reasoning.

You can also use `StreamingReasoning` on its own, with reasoning you get some other way, for example streamed from your backend:

```kotlin
StreamingReasoning(
    text = reasoning,
    isThinking = answer.isEmpty(),
    durationSeconds = thinkingSeconds,
)
```

Reasoning can run to tens of kilobytes and grow many times a second, so the view only lays out what changes: it splits the text into paragraphs that render lazily, so only the paragraph still being written is laid out again. Blank lines separate paragraphs, and inline Markdown (bold, italics, code and links) is rendered.

| Parameter            | Description                                                                  | Default                               |
| -------------------- | ---------------------------------------------------------------------------- | ------------------------------------- |
| `text`               | The reasoning so far.                                                        | -                                     |
| `isThinking`         | Whether the model is still thinking. While it is, the header counts seconds. | -                                     |
| `durationSeconds`    | How long the model thought, shown as "Thought for 12s" once it's done.       | `null`                                |
| `summary`            | A one-line summary shown beside the header once the model is done.           | `null`                                |
| `footnote`           | A note under the open reasoning, such as how long it's kept.                 | `null`                                |
| `initiallyExpanded`  | Whether the reasoning is open once the model is done.                        | `false`                               |
| `showsLiveReasoning` | Whether the reasoning is open while the model thinks.                        | `true`                                |
| `maxExpandedHeight`  | How tall the reasoning grows before it scrolls.                              | `260.dp`                              |
| `textStyle`          | The style of the reasoning. The header uses it in a medium weight.           | `MaterialTheme.typography.bodyMedium` |
| `colors`             | The palette of the view.                                                     | `StreamingReasoningDefaults.colors()` |

The overload that takes a `part: AIReasoningPart` reads `isThinking`, `durationSeconds` and `summary` from the step, and shows its `preview` unless you pass the full reasoning as `text`.

Once the reader opens or closes the reasoning themselves, the view keeps their choice. `StreamingReasoningDefaults.foldAnimationSpec()` is the spring the view folds and opens with: use it for your own changes that should move along with it. `StreamingReasoningDefaults.FoldDurationMillis` (450) is how long the fold takes, so a reply that shows its answer only after it lets the reasoning fold first, rather than the two moving against each other.

## Tool calls

`AIToolCall` shows a single tool call (`AIToolCallPart`): what it's doing (its `display_title`, such as "Checking your location", or else its name), its outcome (`summary`), and its duration once it's finished. Its icon follows the status: a spinner while it runs, a hand while it waits for approval, a device while it waits for the user's device to run it, and a check mark, an exclamation mark or a cross once it completed, failed or was cancelled. A status the SDK doesn't know shows as still running.

```kotlin
part.toolCall?.let { call ->
    AIToolCall(part = call)
}
```

Without a `summary`, a call that waits, failed or was cancelled shows its status in words instead, such as "Waiting for approval" or "Didn't complete".

When the agent asks a user's device to run a tool, `AIClientToolRunner` runs it there and sends the result back, as described in [Client-Side Tools](https://getstream.io/chat/docs/sdk/android/guides/ai-integrations/client-side-tools/#run-tool-calls-from-agent-steps).

## Tool approvals

Some tool calls should wait for the user: the agent wants their location, or to send an email on their behalf. The agent's backend writes such a call with status `awaiting_approval`, addresses it to that user (`target_user_id`, and `target_client_id` for a client tool), and adds the question as its `approval`:

```json
{
  "type": "ai_tool_call",
  "id": "toolu_01A",
  "name": "get_location",
  "status": "awaiting_approval",
  "executor": "client",
  "target_user_id": "u_123",
  "target_client_id": "android-7F3A9C2E-4B1D-4E8A-9F60-2C5D8B7A1E34",
  "approval": {
    "title": "Share your location?",
    "message": "Only your city is shared.",
    "reason": "to check the local weather",
    "allow_title": "Share location",
    "decline_title": "Don't share"
  }
}
```

`AIToolApprovalPrompt` asks the question under the call. It shows only to that user, on the install the call names (or on any of their devices, for a server tool), and only while the call waits, so it can sit under every tool call. Give it an `AIToolApprover`, which says who is signed in on this device and sends their answer to your backend:

```kotlin
class AssistantViewModel(
    userId: String,
    clientId: String,
    private val backend: AgentBackend,
) : ViewModel() {

    val approver = AIToolApprover(
        userId = userId,
        clientId = clientId,
        scope = viewModelScope,
    ) { call, allowed ->
        backend.answerApproval(toolCallId = call.id, allowed = allowed)
    }
}
```

```kotlin
AIToolCall(part = call)
AIToolApprovalPrompt(call = call, approver = approver)
```

`clientId` is this install's identifier, from `AIClientIdentity.installId(context)`, as described in [Client-Side Tools](https://getstream.io/chat/docs/sdk/android/guides/ai-integrations/client-side-tools/#register-the-tools). The approver keeps where each answer is, so create it where it outlives the screen, such as a `ViewModel`, and pass a `scope` that runs on the main thread.

Passing the approver to `AIMessageParts(parts = parts, approver = approver)` does this for every tool call of the reply. While the answer is on its way, and until the call's step changes, the buttons are disabled. If `decide` throws, the card says the answer couldn't be sent, and the user can answer again.

Your backend holds the call until it gets the answer, and accepts one only from the targeted user (and install, for a client tool), only while the call waits, and only once. It then updates the step:

- **Allowed:** `approval.decision` becomes `allowed` and the call goes on. A client tool's call moves to `awaiting_client`, so the device's `AIClientToolRunner` runs it.
- **Declined:** the call becomes `cancelled`, with `approval.decision` set to `declined`, and never runs. `isDeclined` tells it apart from other cancelled calls, and `AIToolCall` shows it as "Declined".

The question is visible to every channel member, so keep private data out of it.

To ask in your own design, pass the content. It gets the question, where the answer is (`AIToolApprovalState`, with `isSending` and `failed`), and a function that answers:

```kotlin
AIToolApprovalPrompt(call = call, approver = approver) { approval, state, decide ->
    MyApprovalCard(
        title = approval.title,
        isBusy = state.isSending,
        onAllow = { decide(true) },
        onDecline = { decide(false) },
    )
}
```

`AIToolApprovalCard` is the default design. Without `allow_title` and `decline_title`, its buttons read "Allow" and "Don't allow".

## Step format

The steps are custom attachments, with their fields at the top level of the attachment. Every step has:

| Field  | Description                                                                                                                       |
| ------ | --------------------------------------------------------------------------------------------------------------------------------- |
| `type` | The kind of step: `ai_reasoning`, `ai_tool_call`, or a kind of your own starting with `ai_`.                                      |
| `id`   | The step's stable identity. Tool calls use the model provider's tool-call ID. Without one, the step's type and position are used. |
| `v`    | The step's format version, `1` by default. It changes only when a kind changes incompatibly.                                      |

A round of reasoning (`ai_reasoning`) also has:

| Field         | Description                                                                   |
| ------------- | ----------------------------------------------------------------------------- |
| `status`      | `streaming` while the model thinks, or `completed` (the default).             |
| `summary`     | A one-line summary of the reasoning, once it's done.                          |
| `preview`     | A capped excerpt: the latest thoughts while streaming, the opening once done. |
| `duration_ms` | How long the model thought, in milliseconds.                                  |

A tool call (`ai_tool_call`) also has:

| Field              | Description                                                                                                                                  |
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`             | The tool's name.                                                                                                                             |
| `display_title`    | What the call is doing, in words for people, such as "Checking your location".                                                               |
| `status`           | `running` (the default), `awaiting_approval`, `awaiting_client`, `completed`, `failed` or `cancelled`.                                       |
| `executor`         | Who runs the tool: `server` (the default) or `client`.                                                                                       |
| `target_user_id`   | The user whose device must run a client tool, or who must approve the call.                                                                  |
| `target_client_id` | The install that must run a client tool, copied from the `client_id` in the custom data of the user's message.                               |
| `arguments`        | The call's arguments as a JSON object, present for client tools, which need them to run. `AIToolCallPart.arguments` holds them as JSON text. |
| `summary`          | A short, shareable outcome, such as "Found your location".                                                                                   |
| `duration_ms`      | How long the call took, in milliseconds.                                                                                                     |
| `approval`         | The question the call asks before it runs: `title`, `message`, `reason`, `allow_title`, `decline_title`, and the user's `decision`.          |

An `approval` without a `title` asks nothing. Every channel member can see the steps, their arguments and their summaries, so keep private data out of them.

## Colors and texts

Each view takes its palette from the current `MaterialTheme`, and you can change one color at a time:

- `StreamingReasoningDefaults.colors(...)`: `title`, `text`, `footnote`, `shimmer` and `rule`.
- `AIToolCallDefaults.colors(...)`: `title`, `detail`, `accent`, `success` and `failure`.
- `AIToolApprovalDefaults.colors(...)`: `title`, `message`, `background`, `border`, `accent` and `failure`.

`AIMessageParts` and `AIMessagePartItem` take all three as `reasoningColors`, `toolCallColors` and `toolApprovalColors`, plus a `textStyle` for the steps. Their texts, such as "Thinking...", "Waiting for approval" or "Allow", are string resources whose names start with `stream_ai_compose_reasoning_`, `stream_ai_compose_tool_call_` and `stream_ai_compose_tool_approval_`, which you can override in your app's `strings.xml`.

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/android/guides/ai-integrations/reasoning-and-tool-calls/](https://getstream.io/chat/docs/sdk/android/guides/ai-integrations/reasoning-and-tool-calls/).