---
title: Messages
description: Send text, full turn payloads, client context, attachments, and HITL responses with eve/client.
url: "https://eve.dev/docs/guides/client/messages"
docs_index: /llms.txt
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

A message is one turn of input to a session. Create a session with its first turn, then send follow-ups through the returned `ClientSession`, which targets that one session ID.

## Send text

Pass a string to `send()` for plain text:

```ts
import { Client } from "eve/client";

const client = new Client({ baseUrl: "http://127.0.0.1:2000" });
const { session, delivery } = await client.sessions.create({
  message: "What is the weather in Brooklyn?",
});

// Metadata is available as soon as the POST succeeds.
console.log(delivery.sessionId);

const result = await delivery.result();
console.log(result.status, result.reply);
```

`delivery.result()` reads until the request finishes or pauses for input, then returns a `DeliveryResult`.
Import `Delivery`, `DeliveryReadOptions`, and `DeliveryResult` from `eve/sessions`:

| Field          | Meaning                                                                                                  |
| -------------- | -------------------------------------------------------------------------------------------------------- |
| `reply`        | The request's reply text, from the parts `turn.settled.reply` lists.                                     |
| `status`       | `awaiting-input`, `completed`, `failed`, or `cancelled` for this request.                                |
| `sessionEnded` | Whether the session ended when this request finished.                                                    |
| `deliveryId`   | The ID of the accepted message or control.                                                               |
| `outcome`      | What happened to the request: `handled`, `awaiting-input`, `applied`, `ignored`, `refused`, or `failed`. |
| `error`        | Required `ErrorInfo` when `status` is `failed`.                                                          |
| `interactions` | Approvals, questions, and sign-ins still waiting for a person, safe to display.                          |
| `sessionId`    | Session ID for streaming and inspection.                                                                 |
| `data`         | Structured output when the turn requested an [output schema](./output-schema).                           |

A failed request returns `status: "failed"` with `error`; inspect `error.code` and
`error.message` to recover. HTTP failures and reads interrupted before the request finishes throw instead.
An `awaiting-input` result is complete. Answering the prompt sends a new request with its own result.
Results don't include an event log; capture `delivery.stream()` when you need one.

`session.send()` retries `409 session_not_ready` with backoff for up to 20 seconds while the
session starts. The caller's abort signal cancels the wait. Other errors, `session.respond()`,
control methods, and raw HTTP requests are not retried.

| Error                        | When                                                                                                                                |
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `SessionNotActiveError`      | Unknown or terminal session, thrown immediately (`retryable: false`), or the `session_not_ready` retry expired (`retryable: true`). |
| `ClientSessionStrandedError` | [Stranded](../../concepts/execution-model-and-durability#stranded-sessions) session.                                                |

`ClientSessionStrandedError` is a `ClientError` with `code: "session_stranded"` and, when recorded,
the `eveVersion` that built the session. It is never retried, and the client never creates a
replacement: call `session.end()` and create a new session. Following the stranded session's
stream, `send()`, and `session.clear()` throw the same error, and a message accepted before the
session stranded is not redelivered. `session.stream({ follow: false })` and `session.snapshot()`
still read the recorded history.

The typed errors `eve/client` throws (`ClientError` and its subclasses, `SessionNotActiveError`,
`HealthResponseError`, and `AgentInfoResponseError`) extend `EveError` and have a stable snake\_case
`code`: `SessionNotActiveError` uses `session_not_active` or `session_not_ready`; `ClientError` carries
the response's code (such as `session_stranded` or `session_expired`), or `http_error` when the
response had none. Narrow with the static `is()` of the class you expect instead of `instanceof`,
so errors from a separately bundled copy of eve still match:

```ts
import { ClientError, SessionNotActiveError } from "eve/client";

try {
  await session.send("Continue.");
} catch (error) {
  if (SessionNotActiveError.is(error) && !error.retryable) {
    // Start a new session.
  } else if (ClientError.is(error) && error.code === "unauthorized") {
    // Sign in again.
  } else {
    throw error;
  }
}
```

## Send a full turn payload

Pass the full payload to `create()` for the first turn or `send()` for a follow-up:

```ts
const { session, delivery } = await client.sessions.create({
  message: "What should I do on this screen?",
  clientContext: {
    route: "/billing",
    plan: "pro",
    seatsUsed: 4,
  },
});

await delivery.result();
```

`clientContext` is ephemeral context for the current turn. A string becomes a user-role context message, an array of strings becomes several, and an object is JSON-serialized into one. Every model call in the turn sees it. It isn't saved to session history and doesn't start a turn by itself.

## Send attachments

`send()` accepts `MessageContent`, so a message can mix text and file parts. A file part's `data` is the file inline (bytes, base64, or a `data:` URL) or a URL. For a local file, send a base64 `data:` URL:

```ts
import { readFile } from "node:fs/promises";

const bytes = await readFile("report.pdf");
const reportDataUrl = `data:application/pdf;base64,${bytes.toString("base64")}`;

const response = await session.send([
  { type: "text", text: "Summarize this report." },
  {
    type: "file",
    data: reportDataUrl,
    mediaType: "application/pdf",
    filename: "report.pdf",
  },
]);

await response.result();
```

The stream confirms the turn with `delivery.consumed`, whose `data.parts` holds text and file
metadata for rendering attachments, never raw bytes or sandbox paths. See [Inbound
attachments](../../sandbox#inbound-attachments) for how eve stages files in the session sandbox.

## Answer human input requests

Tools can pause for approval or ask the user a question. The stream emits one `interaction.opened` per request, and the response's result lists the open requests in `interactions`. Reply through the same session with `answers`:

```ts
const response = await session.send("Run the deployment checks.");
const { interactions: pendingRequests } = await response.result();

const resumed = await session.respond(
  pendingRequests.map((request) => ({
    interactionId: request.interactionId,
    optionId: "approve",
  })),
);

await resumed.result();
```

The stream records a `response.submitted` event for each answer. Each request then ends with an `interaction.settled` event that carries its `interactionId`, final `outcome`, and the `response` that decided it. Save `interaction.settled`, not the client's optimistic state, when you rebuild message history.

Pass `clientContext`, `outputSchema`, headers, or stream options in the second argument to `send(message, options)` or `respond(answers, options)`.

## Reusable delivery reads

Call `stream()` to observe progress, then `result()` for the summary:

```ts
for await (const event of response.stream()) {
  console.log(event.type);
}
const result = await response.result();
```

Each read covers the request from when the server accepts it until it finishes, independently of other readers.
Repeated and concurrent reads are supported, and each may open another HTTP stream.

Pass `{ signal }` to `stream()` or `result()` to abort that read. An aborted or failed read doesn't
consume the handle or cancel the work, so retry the same delivery instead of sending again. Use
`session.cancel()` to cancel work. A signal passed to `send()` covers only submission, so pass it
to the read too when a timeout must cover both.

## What to read next

- [Continuations](./continuations): how the session cursor advances
- [Streaming](./streaming): handle events live instead of using `result()`
- [Tools](../../tools): configure approvals and question prompts

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)