> ## Documentation Index
> Fetch the complete documentation index at: https://docs.thesys.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Events

> Listen to lifecycle events from the embedded agent — thread changes, message flow, tool execution, and errors.

The embed widget emits events at key points in the agent's lifecycle, letting you react to user actions, track conversation flow, and handle errors in your application.

There are two ways to listen to events: **inline callbacks** passed to `embedWidget()`, and the **`on()` method** on the widget instance.

<Warning>
  **Data events carry payload only when the parent origin is allowlisted.** By default, events that contain conversation data (`generationEnded`, `userMessageSent`, `threadChanged`, etc.) still fire but their payload is stripped to just the event type. To receive the full payload — including the assistant's message body for live rendering — you must add your parent page's origin to the playground's **Allowed Parent Origins** list. See [Receiving Message Data](#receiving-message-data) below.
</Warning>

***

## Quick Start

```javascript theme={null}
import { embedWidget } from 'agent-embed-widget';
import 'agent-embed-widget/dist/agent-embed-widget.css';

const widget = embedWidget({
  url: 'https://console.thesys.dev/app/your-slug',

  // Inline callbacks
  onGenerationStarted: (threadId, messageId) => {
    console.log('Agent is thinking…', { threadId, messageId });
  },
  onGenerationEnded: (threadId, messageId) => {
    console.log('Agent replied', { threadId, messageId });
  },
});

// Or subscribe dynamically with on()
const unsub = widget.on('userMessageSent', (message, threadId) => {
  analytics.track('user_message', { message, threadId });
});

// Later: stop listening
unsub();
```

***

## Event Reference

### Thread Events

#### `threadChanged`

Fires when the active conversation thread changes — for example, when the user selects a different thread from the thread list, or a new thread is created after the first message is sent.

| Parameter  | Type     | Description                       |
| ---------- | -------- | --------------------------------- |
| `threadId` | `string` | The ID of the newly active thread |

```javascript theme={null}
widget.on('threadChanged', (threadId) => {
  console.log('Switched to thread:', threadId);
});
```

#### `newThread`

Fires when the user clicks "New Chat" to start a fresh conversation. At this point no thread has been created on the backend yet — the thread is created once the first message is sent (which then fires `threadChanged`).

```javascript theme={null}
widget.on('newThread', () => {
  console.log('User started a new chat');
});
```

***

### Message Events

#### `userMessageSent`

Fires when the user sends a message to the agent.

| Parameter  | Type     | Description                        |
| ---------- | -------- | ---------------------------------- |
| `message`  | `string` | The message content                |
| `threadId` | `string` | The thread the message was sent in |

```javascript theme={null}
widget.on('userMessageSent', (message, threadId) => {
  console.log('User said:', message, 'in thread:', threadId);
});
```

#### `generationStarted`

Fires when the agent begins generating a response.

| Parameter   | Type     | Description                                     |
| ----------- | -------- | ----------------------------------------------- |
| `threadId`  | `string` | The thread the response is being generated in   |
| `messageId` | `string` | The ID of the assistant message being generated |

```javascript theme={null}
widget.on('generationStarted', (threadId, messageId) => {
  showTypingIndicator(threadId);
});
```

#### `generationEnded`

Fires when the agent finishes generating a response. The `messageId` is the persisted backend ID that you can use with the Thesys API to fetch the message content. When your parent origin is allowlisted, `message` contains the **full persisted assistant message** so you can render the response live in another panel without an additional API call.

| Parameter   | Type                            | Description                                                                                          |
| ----------- | ------------------------------- | ---------------------------------------------------------------------------------------------------- |
| `threadId`  | `string \| undefined`           | The thread the response was generated in                                                             |
| `messageId` | `string \| undefined`           | The backend ID of the completed assistant message                                                    |
| `message`   | `AssistantMessage \| undefined` | The full persisted assistant message item (id, role, content, status, conversation\_id, metadata, …) |

<Note>
  All three arguments are `undefined` unless the embedding page's origin is on the playground's **Allowed Parent Origins** list. See [Receiving Message Data](#receiving-message-data).
</Note>

```javascript theme={null}
widget.on('generationEnded', (threadId, messageId, message) => {
  hideTypingIndicator(threadId);

  if (message) {
    // Render the assistant's reply in a side panel
    renderAssistantMessage(message);
  }
});
```

**`AssistantMessage` shape**

```typescript theme={null}
interface AssistantMessage {
  id: string;
  role: 'assistant';
  type: 'message';
  content: Array<Record<string, unknown>>;
  status?: Record<string, unknown>;
  conversation_id?: string;
  created_at?: number;
  metadata?: Record<string, unknown>;
}
```

***

### Tool Execution Events

These events fire when the agent uses a tool (web search, image search, artifact builder, etc.) during response generation. Internal tools used by the system are excluded — only user-facing tools trigger these events.

#### `toolExecutionStarted`

Fires when the agent starts executing a tool.

| Parameter  | Type     | Description                         |
| ---------- | -------- | ----------------------------------- |
| `toolName` | `string` | The name of the tool being executed |
| `threadId` | `string` | The thread the tool is executing in |

```javascript theme={null}
widget.on('toolExecutionStarted', (toolName, threadId) => {
  console.log(`Tool "${toolName}" started in thread ${threadId}`);
});
```

#### `toolExecutionEnded`

Fires when a tool finishes executing. If the tool failed, the `error` parameter contains the error message.

| Parameter  | Type                  | Description                                              |
| ---------- | --------------------- | -------------------------------------------------------- |
| `toolName` | `string`              | The name of the tool that finished                       |
| `threadId` | `string`              | The thread the tool executed in                          |
| `error`    | `string \| undefined` | Error message if the tool failed, `undefined` on success |

```javascript theme={null}
widget.on('toolExecutionEnded', (toolName, threadId, error) => {
  if (error) {
    console.error(`Tool "${toolName}" failed:`, error);
  } else {
    console.log(`Tool "${toolName}" completed successfully`);
  }
});
```

***

### Error Events

#### `agentError`

Fires when an error occurs during message processing or response streaming.

| Parameter | Type     | Description                                                          |
| --------- | -------- | -------------------------------------------------------------------- |
| `code`    | `string` | Error code (`MESSAGE_PROCESSING_ERROR` or `STREAM_PROCESSING_ERROR`) |
| `message` | `string` | Human-readable error description                                     |

```javascript theme={null}
widget.on('agentError', (code, message) => {
  errorReporter.capture({ code, message });
});
```

#### `identityTokenError`

Fires when a [BYOI](/agent-builder/byoi) identity token is invalid or cannot be refreshed. This is separate from the normal token refresh flow — it indicates a persistent authentication failure.

| Parameter | Type     | Description                             |
| --------- | -------- | --------------------------------------- |
| `code`    | `string` | Error code identifying the failure type |
| `message` | `string` | Human-readable error description        |

```javascript theme={null}
widget.on('identityTokenError', (code, message) => {
  console.error('Identity token error:', code, message);
  // Redirect user to re-authenticate
  window.location.href = '/login';
});
```

***

## Inline Callbacks

All events can also be passed as callbacks directly to `embedWidget()`. Callback names use the `on` prefix with PascalCase:

```javascript theme={null}
const widget = embedWidget({
  url: 'https://console.thesys.dev/app/your-slug',
  onThreadChanged: (threadId) => { /* ... */ },
  onNewThread: () => { /* ... */ },
  onUserMessageSent: (message, threadId) => { /* ... */ },
  onGenerationStarted: (threadId, messageId) => { /* ... */ },
  onGenerationEnded: (threadId, messageId, message) => { /* ... */ },
  onAgentError: (code, message) => { /* ... */ },
  onToolExecutionStarted: (toolName, threadId) => { /* ... */ },
  onToolExecutionEnded: (toolName, threadId, error) => { /* ... */ },
  onIdentityTokenError: (code, message) => { /* ... */ },
});
```

Both inline callbacks and `on()` listeners fire for the same event — you can use either or both.

***

## `widget.on(event, callback)`

Subscribe to events dynamically after the widget is created. Returns an **unsubscribe function**.

```javascript theme={null}
const unsubscribe = widget.on('generationEnded', (threadId, messageId) => {
  console.log('Done:', messageId);
});

// Stop listening
unsubscribe();
```

Multiple listeners can be registered for the same event. Each call to `on()` returns its own independent unsubscribe function.

***

## Receiving Message Data

For security, the published agent does **not** broadcast conversation data (assistant messages, thread IDs, user messages, tool names, error details) to arbitrary embedders. Without explicit opt-in, every data event still fires — but with its payload stripped to just the event type:

```javascript theme={null}
// Default behavior (no allowlisted origins): all data events fire as
//   { type: 'THESYS_GENERATION_ENDED' }
//   { type: 'THESYS_USER_MESSAGE_SENT' }
//   ...
// All payload fields are undefined in your callbacks.
```

To receive the full payload — including the assistant `message` for live rendering — add your parent page's origin to the playground's **Allowed Parent Origins** list:

1. Open your playground in the [Thesys Console](https://console.thesys.dev/).
2. Click **Deploy**, expand the **Embed on your website** section.
3. Under **Allowed parent origins**, add each origin that should receive full event payloads (for example, `https://app.example.com` or `http://localhost:3000`).
4. Republish the playground.

Once allowlisted, your origin will receive the full payload for every data event. Origins not on the list will continue to receive stripped events. The handshake events (`APP_READY`, widget open/close/toggle, identity-token refresh) are unaffected and always work.

### What counts as a "data event"?

| Event                                                                                                                                                                     | Class        | Notes                                                                 |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | --------------------------------------------------------------------- |
| `threadChanged`, `newThread`, `userMessageSent`, `generationStarted`, `generationEnded`, `agentError`, `toolExecutionStarted`, `toolExecutionEnded`, `identityTokenError` | **Data**     | Payload only delivered to allowlisted origins                         |
| `THESYS_APP_READY`, `THESYS_WIDGET_OPEN/CLOSE/TOGGLE`, `THESYS_IDENTITY_TOKEN_REFRESH_NEEDED`                                                                             | **Protocol** | Required for widget functionality, always delivered with full payload |

<Tip>
  Origins must be exact matches: `scheme://host[:port]` with no path or trailing slash. `https://app.example.com` and `https://app.example.com:443` are treated as different origins.
</Tip>

***

## Typical Event Flow

Here's the sequence of events during a normal conversation turn:

```
newThread                          // User clicks "New Chat"
  → userMessageSent               // User sends first message
  → threadChanged (threadId)      // Backend creates the thread
  → generationStarted             // Agent begins responding
    → toolExecutionStarted        // (if the agent uses a tool)
    → toolExecutionEnded          //
  → generationEnded               // Agent finishes responding
```

For subsequent messages in the same thread:

```
userMessageSent
  → generationStarted
  → generationEnded
```

***

## Direct Iframe Integration

If you're embedding the agent via a **direct iframe** instead of the embed widget npm package, you can listen for the same events using the browser's `postMessage` API. Each event is sent as a message with a `type` field prefixed with `THESYS_`.

<Tip>
  For full details on `postMessage` usage, origin validation, and URL parameters, see the [PostMessage Protocol](/agent-builder/advanced/postmessage-protocol) page.
</Tip>

### Event Mapping

| Embed Widget Event     | postMessage `type`                     | Payload                                                              |
| ---------------------- | -------------------------------------- | -------------------------------------------------------------------- |
| `threadChanged`        | `THESYS_THREAD_CHANGED`                | `{ threadId: string }`                                               |
| `newThread`            | `THESYS_NEW_THREAD`                    | *(none)*                                                             |
| `userMessageSent`      | `THESYS_USER_MESSAGE_SENT`             | `{ message: string, threadId: string }`                              |
| `generationStarted`    | `THESYS_GENERATION_STARTED`            | `{ threadId: string, messageId: string }`                            |
| `generationEnded`      | `THESYS_GENERATION_ENDED`              | `{ threadId: string, messageId: string, message: AssistantMessage }` |
| `toolExecutionStarted` | `THESYS_TOOL_EXECUTION_STARTED`        | `{ toolName: string, threadId: string }`                             |
| `toolExecutionEnded`   | `THESYS_TOOL_EXECUTION_ENDED`          | `{ toolName: string, threadId: string, error?: string }`             |
| `agentError`           | `THESYS_AGENT_ERROR`                   | `{ code: string, message: string }`                                  |
| `identityTokenError`   | `THESYS_IDENTITY_TOKEN_ERROR`          | `{ code: string, message: string }`                                  |
| *(widget-only)*        | `THESYS_APP_READY`                     | *(none)* — the agent has finished loading                            |
| *(widget-only)*        | `THESYS_WIDGET_CLOSE`                  | *(none)* — the agent requested to close                              |
| *(widget-only)*        | `THESYS_WIDGET_OPEN`                   | *(none)* — the agent requested to open                               |
| *(widget-only)*        | `THESYS_WIDGET_TOGGLE`                 | *(none)* — the agent requested to toggle visibility                  |
| *(widget-only)*        | `THESYS_IDENTITY_TOKEN_REFRESH_NEEDED` | *(none)* — token expired, parent should refresh                      |
| *(parent → iframe)*    | `THESYS_RESET_THREAD`                  | *(none)* — start a new conversation thread                           |
| *(parent → iframe)*    | `THESYS_TOGGLE_SIDEBAR`                | *(none)* — open or close the conversation history sidebar            |

### Example

```javascript theme={null}
const iframe = document.getElementById('thesys-agent');
const expectedOrigin = new URL(iframe.src).origin;

window.addEventListener('message', (event) => {
  if (event.origin !== expectedOrigin) return;
  if (!event.data?.type) return;

  switch (event.data.type) {
    case 'THESYS_APP_READY':
      console.log('Agent loaded');
      break;
    case 'THESYS_THREAD_CHANGED':
      console.log('Thread:', event.data.threadId);
      break;
    case 'THESYS_NEW_THREAD':
      console.log('New chat started');
      break;
    case 'THESYS_USER_MESSAGE_SENT':
      console.log('User said:', event.data.message);
      break;
    case 'THESYS_GENERATION_STARTED':
      console.log('Generating…', event.data.messageId);
      break;
    case 'THESYS_GENERATION_ENDED':
      console.log('Done:', event.data.messageId);
      // event.data.message is the full assistant message — only present
      // when this origin is on the playground's allowed parent origins list.
      if (event.data.message) {
        renderAssistantMessage(event.data.message);
      }
      break;
    case 'THESYS_TOOL_EXECUTION_STARTED':
      console.log('Tool started:', event.data.toolName);
      break;
    case 'THESYS_TOOL_EXECUTION_ENDED':
      console.log('Tool ended:', event.data.toolName, event.data.error);
      break;
    case 'THESYS_AGENT_ERROR':
      console.error('Error:', event.data.code, event.data.message);
      break;
    case 'THESYS_IDENTITY_TOKEN_ERROR':
      console.error('Token error:', event.data.code, event.data.message);
      break;
    case 'THESYS_IDENTITY_TOKEN_REFRESH_NEEDED':
      // Fetch a new token and send THESYS_IDENTITY_TOKEN_REFRESHED back
      break;
  }
});
```

***

## Example: Analytics Integration

```javascript theme={null}
import { embedWidget } from 'agent-embed-widget';
import 'agent-embed-widget/dist/agent-embed-widget.css';

const widget = embedWidget({
  url: 'https://console.thesys.dev/app/your-slug',
});

widget.on('userMessageSent', (message, threadId) => {
  analytics.track('agent_message_sent', {
    threadId,
    messageLength: message.length,
  });
});

widget.on('generationEnded', (threadId, messageId, message) => {
  analytics.track('agent_response_received', {
    threadId,
    messageId,
    contentLength: message?.content?.length,
  });
});

widget.on('toolExecutionStarted', (toolName, threadId) => {
  analytics.track('agent_tool_used', { toolName, threadId });
});

widget.on('agentError', (code, message) => {
  analytics.track('agent_error', { code, message });
});
```
