# Streaming a response

Subscribe to the event feed, submit a prompt asynchronously, print tokens as they arrive — the pattern every nikcli interface is built on.

## The shape of it

`session.prompt` returns the finished message. That is fine for a script and wrong for an interface — nobody wants a blank screen for forty seconds.

Streaming is a different arrangement of the same endpoints: **subscribe to the event feed first**, then submit the prompt with `promptAsync`, which returns as soon as the work is accepted. Output arrives on the feed.

| Step | Call | Why |
| --- | --- | --- |
| 1 | `client.event.subscribe()` | Before submitting, or you miss the first tokens. |
| 2 | `client.session.promptAsync(…)` | Accepts the work and returns immediately. |
| 3 | `message.part.updated` | Carries each incremental `delta`. |
| 4 | `session.idle` | The turn is finished. |

## Subscribe first

Streaming methods resolve to an object holding an async iterable: you await the connection, then iterate the feed.

ts 

1

2

3

4

5

```
const { stream } = await client.event.subscribe()

for await (const event of stream) {
  console.log(event.type)
}
```

`event.subscribe()` is scoped to the resolved instance. `client.global.event()` is the same feed across every instance the server hosts — use it when one process watches several projects.

Order matters

Subscribing after submitting is a race you will lose on short answers. Open the feed, then submit.

## Submit without waiting

ts 

1

2

3

4

```
await client.session.promptAsync({
  sessionID: session.id,
  parts: [{ type: "text", text: "Explain what this repository does." }],
})
```

It takes the same input as `session.prompt` — including `agent`, `model` and `tools` — and differs only in returning before the work is done.

## Print the deltas

`message.part.updated` fires as a part changes. It carries the whole `part` plus an optional `delta` — the text added since the last event. Print the `delta`; printing `part.text` reprints the message from the start every time.

ts 

1

2

3

4

5

6

```
for await (const event of stream) {
  if (event.type !== "message.part.updated") continue
  if (event.properties.part.type !== "text") continue

  process.stdout.write(event.properties.delta ?? "")
}
```

delta is optional on purpose

A part can be replaced wholesale rather than appended to — a tool call completing, say — and then there is no delta. Fall back to re-rendering from `part` when you are painting a UI rather than appending to a terminal.

## Know when it is done

`session.idle` carries the `sessionID` that finished. Filter on your own session — on a shared feed, other sessions go idle too.

ts 

```
if (event.type === "session.idle" && event.properties.sessionID === session.id) {
  break
}
```

Watch `session.error` as well, or a failed turn looks identical to one that is still thinking.

## The whole thing

stream.ts

ts 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

```
import { createNikcliClient } from "@nikcli-ai/sdk/httpapi"

const client = createNikcliClient({
  baseUrl: "http://127.0.0.1:4096",
  directory: process.cwd(),
})

const { data: session, error } = await client.session.create({ title: "streamed" })
if (!session) throw error

// 1. open the feed before submitting anything
const { stream } = await client.event.subscribe()

// 2. submit without waiting for the turn to finish
await client.session.promptAsync({
  sessionID: session.id,
  parts: [{ type: "text", text: "Explain what this repository does." }],
})

// 3. print as it arrives, stop when the session goes idle
for await (const event of stream) {
  switch (event.type) {
    case "message.part.updated":
      if (event.properties.part.type === "text") {
        process.stdout.write(event.properties.delta ?? "")
      }
      break

    case "session.error":
      console.error("\n", event.properties.error)
      process.exit(1)

    case "session.idle":
      if (event.properties.sessionID === session.id) process.exit(0)
  }
}
```

## Useful events

The feed carries everything the server does, not just assistant text. These are the ones an interface usually handles.

| Event | Carries | Use for |
| --- | --- | --- |
| `message.part.updated` | `part`, `delta?` | Streaming output. |
| `message.updated` | The message | Role, model and token counts. |
| `message.part.removed` | ids | Dropping a part from the view. |
| `session.status` | idle / busy / retry | A spinner that tells the truth. |
| `session.idle` | `sessionID` | Turn finished. |
| `session.error` | The error | Turn failed. |
| `permission.asked` | The request | Prompting, or auto-replying. |
| `question.asked` | The questions | The agent needs an answer to continue. |
| `todo.updated` | The todo list | Progress for multi-step work. |
| `file.edited` | The path | Refreshing an editor view. |
| `session.diff` | Diff summary | A live change count. |

The full list is the `Event` union exported by the SDK, so your editor will enumerate it and narrow `properties` per `type`.

## Streaming with curl

The feed is plain server-sent events, so nothing about this requires the SDK. Note the `?token=` form — an `EventSource` cannot set headers, so the API accepts the token as a query parameter.

Terminal

bash 

```
# terminal 1 — watch the feed
curl -N "http://127.0.0.1:4096/event?directory=$PWD"

# terminal 2 — submit a prompt
curl -s -X POST "http://127.0.0.1:4096/session/$SESSION/prompt_async?directory=$PWD" \
  -H 'content-type: application/json' \
  -d '{"parts":[{"type":"text","text":"hello"}]}'
```

$ shell · paste into your terminal

## Failure modes

| Symptom | Cause |
| --- | --- |
| First tokens missing | Subscribed after submitting. |
| Text repeats and grows | Printing `part.text` instead of `delta`. |
| Loop never ends | Not handling `session.idle`, or not filtering it by `sessionID`. |
| Hangs mid-turn | A permission request nobody answered. Handle `permission.asked`. |
| Events from other work | Using `global.event()`, which spans every instance, instead of `event.subscribe()`. |
| Nothing at all | Wrong `directory`, so you are watching a different instance's feed. |

## Related

-   [Using the API The quickstart this guide continues from.](/docs/guides/api)
-   [API Reference The event endpoints themselves.](/docs/api/events)
-   [Client Client options, errors and method naming.](/docs/build/client)
-   [Sessions What a session is and what it holds.](/docs/sessions)

---

Source: https://nikcli-ai.dev/docs/guides/streaming
