# Agent Streaming

> Handle real-time message updates and tool calls.

**URL:** https://inference.sh/docs/api/agent/streaming
**Last updated:** 2026-10-12

---

Handle real-time message updates and tool calls.

Both SDKs read one server-sent event stream per chat (`/chats/{id}/stream`). It carries three kinds of updates: `chat_messages` (a message as stored, whole, every time it changes), `chats` and `agent_runs` (run state), and `delta` (token batches for the message being generated). `onMessage` always gives you the complete message so far, so there is nothing to concatenate; `onDelta` gives you the increments. Messages with `role: "event"` (for example [hook handler runs](/docs/api/agent/lifecycle-hooks#hook-runs-in-the-transcript)) arrive on the same channel but are not LLM turns — filter or render them separately if you only care about user/assistant/tool content.

---

## Basic Streaming

Pass callbacks to `sendMessage` and it waits until the agent's turn is over, delivering updates as they arrive.

<div data-type="codegroup">

```python Python
# Tokens as they are generated. d.delta is this batch,
# d.output everything received so far for d.message_id.
def on_delta(d):
    print(d.delta.get("response", ""), end="", flush=True)

# Each message once it is stored (user, assistant, tool, event, …).
def on_message(message):
    if message.get("role") == "assistant" and message.get("status") == "ready":
        print(f"\n[done] {message['id']}")

agent.send_message("Tell me a story", on_delta=on_delta, on_message=on_message)
```

```typescript JavaScript
await agent.sendMessage('Tell me a story', {
  // Tokens as they are generated. d.delta is this batch,
  // d.output everything received so far for d.messageId.
  onDelta: (d) => process.stdout.write(d.delta.response ?? ''),
  // Each message once it is stored (user, assistant, tool, event, …).
  onMessage: (message) => {
    if (message.role === 'assistant' && message.status === 'ready') {
      console.log(`\n[done] ${message.id}`);
    }
  },
});
```

</div>

`onDelta` is streaming-only. With `stream: false` the SDK polls the chat instead, there are no deltas, and each message arrives whole through `onMessage`.

---

## Tool Call Callbacks

A client tool (one the agent declares but your code executes) reaches you through `onToolCall` with `{ id, name, args }`. Execute it and submit the result; the agent continues from there.

<div data-type="codegroup">

```python Python
def on_tool_call(tool_call):
    print(f"[tool] {tool_call.name} {tool_call.args}")
    result = my_tools[tool_call.name](**tool_call.args)
    agent.submit_tool_result(tool_call.id, str(result))

agent.send_message("What's 42 * 17?", on_tool_call=on_tool_call)
```

```typescript JavaScript
await agent.sendMessage("What's 42 * 17?", {
  onToolCall: async (call) => {
    console.log(`[tool] ${call.name}`, call.args);
    const result = await myTools[call.name](call.args);
    await agent.submitToolResult(call.id, String(result));
  },
});
```

</div>

Each invocation is dispatched once. A widget interaction submits an action instead of a string: `submitToolResult(id, { action: { type: 'confirm' }, form_data: { ... } })`.

---

## Without Callbacks

With no callbacks, `sendMessage` returns as soon as the server has accepted the message; the agent keeps working in the background. Use this when another consumer renders the chat, or read the result later:

<div data-type="codegroup">

```python Python
agent.send_message("Summarise this thread")

# Iterate the stream yourself; it ends when the chat goes idle.
for message in agent.stream_messages():
    print(message.get("status"))

# Or run to completion and take the structured output
output = agent.run("Extract the action items")
```

```typescript JavaScript
await agent.sendMessage('Summarise this thread');

// Attach to the running chat later
agent.startStreaming({ onDelta: (d) => process.stdout.write(d.delta.response ?? '') });

// Or run to completion and take the structured output
const output = await agent.run('Extract the action items');
```

</div>

A turn keeps running on the server when the client disconnects. A dropped `sendMessage` or `send_message` stream does not stop the agent; attach again with `startStreaming()` or `stream_messages()`, or poll the chat's messages. See [Reconnection](/docs/api/rest/streaming#reconnection).

`run()` always polls and returns `chat.output`, the result of the agent's finish tool, or `null` when it finished without calling it.

---

## Async

<div data-type="codegroup">

```python Python
from inferencesh import AsyncInference
import asyncio

async def main():
    client = AsyncInference(api_key="inf_...")
    agent = client.agent("my-org/assistant")

    await agent.send_message("Hello")
    async for message in agent.stream_messages():
        print(message.get("status"))

asyncio.run(main())
```

```typescript JavaScript
// JavaScript/TypeScript is async by default
import { inference } from '@inferencesh/sdk';

const client = inference({ apiKey: 'inf_...' });
const agent = client.agent('my-org/assistant');

await agent.sendMessage('Hello', { onDelta: (d) => process.stdout.write(d.delta.response ?? '') });
```

</div>

---

## Send Options

<div data-type="codegroup">

```python Python
agent.send_message(
    "Hello",
    files=None,                   # bytes or data-URI strings, uploaded first
    on_delta=on_delta,            # AgentDelta(message_id, delta, output, seq)
    on_message=on_message,        # ChatMessageDTO, whole message each update
    on_tool_call=on_tool_call,    # ToolCallInfo(id, name, args)
)
```

```typescript JavaScript
interface SendMessageOptions {
  files?: (Blob | FileDTO)[];                      // Blobs are uploaded first
  onDelta?: (delta: AgentDelta) => void;           // streaming only
  onMessage?: (message: ChatMessageDTO) => void;
  onChat?: (chat: ChatDTO) => void;
  onToolCall?: (call: { id: string; name: string; args: Record<string, unknown> }) => void;
  stream?: boolean;                                // false = poll instead of SSE
  pollIntervalMs?: number;                         // polling mode only
  signal?: AbortSignal;                            // stop waiting for the turn
}

interface AgentDelta {
  messageId: string;  // the chat message the tokens belong to
  delta: LLMDelta;    // this batch: response, reasoning, tool_calls
  output: LLMOutput;  // everything received for the message so far
  seq: number;        // producer sequence, per message
}
```

</div>

A turn that stops on a tool approval or an authorization the agent needs stays open: the JavaScript SDK keeps waiting for it to be resolved, from your `onToolCall` handler or from the inference.sh app. A client that cannot resolve it passes a `signal` and aborts; `sendMessage` then rejects with `signal.reason` while the agent stays parked server-side. The Python SDK returns from `send_message` at that point instead.
