Messaging
Human-like typing simulation with natural delays, typing indicators, and burst messaging.
Enable Messaging
Add messaging to the agent's addons:
metadata: {
className: 'ChatBot',
personality: 'A friendly assistant.',
instructions: 'Respond using send_message tool.',
tools: [],
addons: {
enabled: ['messaging'],
},
}This gives the agent a send_message tool. Messages go through a realistic pipeline: reading delay, typing indicator, then delivery.
send_message Tool
The agent calls this tool to send messages. It accepts an array of message objects for burst messaging (up to 10 per call).
{
name: 'send_message',
parameters: [{
name: 'messages',
type: 'array',
required: true,
items: {
type: 'object',
properties: {
message: { type: 'string', description: 'The message content' },
extra_args: { type: 'object', description: 'Additional parameters passed through to client' },
},
required: ['message'],
},
}],
}| Field | Type | Description |
|---|---|---|
| messages | array | Array of message objects. 1-10 items. |
| messages[].message | string | The message text content. Required. |
| messages[].extra_args | object? | Arbitrary metadata passed through to all client events. Optional. |
Each message in the array is delivered sequentially with its own typing cycle. A new send_message call automatically cancels any pending burst.
Server Events
Events emitted to the client via socket.on('event', ...). All messaging events have type: 'messaging'.
typing_start
Emitted when the agent starts typing a message. Show a typing indicator.
{
type: 'messaging',
event: 'typing_start',
extra_args?: Record<string, unknown> // From the message being typed
}message_sent
Emitted when a message has been "typed" and is ready to display. Hide the typing indicator and show the message.
{
type: 'messaging',
event: 'message_sent',
toolCallId: string, // ID for the entire burst
message: string, // The message content
messageIndex: number, // 0-based index within the burst
messageCount: number, // Total messages in the burst
extra_args?: Record<string, unknown> // From the corresponding message object
}typing_end
Emitted when typing stops without a message being sent (cancellation). Hide the typing indicator.
{
type: 'messaging',
event: 'typing_end',
reason: 'completed' | 'canceled',
extra_args?: Record<string, unknown>
}| Event | When | Client Action |
|---|---|---|
| typing_start | After reading delay, before each message | Show typing indicator |
| message_sent | After typing delay completes | Hide indicator, display message, send confirmation |
| typing_end | When pending message is canceled | Hide typing indicator |
Handling Events
socket.on('event', (data) => {
if (data.type === 'messaging') {
switch (data.event) {
case 'typing_start':
showTypingIndicator();
break;
case 'message_sent':
hideTypingIndicator();
displayMessage(data.message, data.extra_args);
// Send addon-tool-event confirmation (see next section)
socket.emit('message', {
type: 'addon-tool-event',
toolCallId: data.toolCallId,
data: { messageIndex: data.messageIndex, success: true },
});
break;
case 'typing_end':
hideTypingIndicator();
break;
}
}
});Client Confirmation
Send an addon-tool-event for each message_sent event you receive. The server tracks all confirmations and reports the tool result back to the agent only after every message in the burst has been confirmed.
socket.emit('message', {
type: 'addon-tool-event',
toolCallId: string, // From the message_sent event
data: {
messageIndex: number, // From the message_sent event
success: boolean, // Whether the message was handled successfully
context?: object, // Optional context merged into the tool result
error?: string, // Error message if success is false
},
});| Field | Type | Description |
|---|---|---|
| type | string | Always "addon-tool-event" |
| toolCallId | string | The toolCallId from message_sent |
| data.messageIndex | number | The messageIndex from message_sent (0-based) |
| data.success | boolean | Whether the client successfully processed this message |
| data.context | object? | Optional context merged into the tool result event |
| data.error | string? | Error message if success is false |
Send one confirmation per message_sent event. The agent won't receive the tool result until all messages in the burst are confirmed. If you send success: false for any message, the agent will see which messages failed and can retry or adjust.
Configuration
Customize timing behavior in the agent metadata:
addons: {
enabled: ['messaging'],
messaging: {
extraInstructions: 'Always reply formally.', // Appended to tool description
delays: {
readingDelayMs: 2000, // Pause before typing (default: 3000)
typingMode: 'exponential', // 'linear' or 'exponential' (default: 'linear')
typingWpm: 70, // Words per minute (default: 90)
exponentialScaling: 0.5, // Scaling for exponential mode (default: 0.5)
minTypingDelayMs: 500, // Floor for typing delay (default: 500)
maxTypingDelayMs: 30000, // Cap for typing delay (default: 30000)
burstPauseMs: 200, // Pause between burst messages (default: 200)
},
},
}| Parameter | Default | Description |
|---|---|---|
| extraInstructions | - | Extra text appended to the send_message tool description the agent sees |
| readingDelayMs | 3000 | Pause before typing starts (simulates reading) |
| typingMode | "linear" | linear: constant WPM. exponential: longer messages take disproportionately longer |
| typingWpm | 90 | Base typing speed in words per minute |
| exponentialScaling | 0.5 | Scaling factor for exponential mode. Higher = more aggressive slowdown on long messages |
| minTypingDelayMs | 500 | Minimum typing delay regardless of message length |
| maxTypingDelayMs | 30000 | Maximum typing delay cap |
| burstPauseMs | 200 | Pause between consecutive messages in a burst |
Cancellation
When a new event arrives while a message is pending, the pending message is automatically canceled (you'll receive a typing_end with reason: 'canceled'). Use skipCancelPendingMessage for background events that shouldn't interrupt typing:
socket.emit('message', {
type: 'context-update',
triggering: false,
name: 'background-update',
context: { ... },
description: 'Background status update',
skipCancelPendingMessage: true, // Won't interrupt typing
});extra_args
Each message object can include an extra_args object. This is passed through untouched to all messaging events (typing_start, message_sent, typing_end). Use it to attach client-side metadata like reply references, message formatting, or UI hints.
To make the agent use extra_args, describe the expected structure in extraInstructions:
addons: {
enabled: ['messaging'],
messaging: {
extraInstructions: `When replying to a specific message, include:
extra_args: { replyToId: "" }
Only use replyToId when directly responding to a specific message.` ,
},
}The agent will then include extra_args when appropriate, and you'll receive it in the message_sent event:
// message_sent event with extra_args
{
type: 'messaging',
event: 'message_sent',
toolCallId: 'tc_abc123',
message: 'Great point! I agree.',
messageIndex: 0,
messageCount: 1,
extra_args: { replyToId: 'msg-42' }
}Full Example: Chat with Replies
Complete integration showing agent creation, WebSocket connection, messaging events, confirmation, and reply support via extra_args.
1. Create Agent
const response = await fetch('https://api.humalike.tech/api/agents', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': apiKey,
},
body: JSON.stringify({
name: 'Chat Agent',
agentType: 'HUMA-0.1',
metadata: {
className: 'ChatBot',
personality: 'A friendly assistant who loves to chat.',
instructions: 'Respond using send_message. Be conversational.',
tools: [],
routerType: 'conversational',
addons: {
enabled: ['messaging'],
messaging: {
extraInstructions: `When replying to a specific message, include:
extra_args: { replyToId: "" }
Only use replyToId when directly responding to a specific message.` ,
delays: {
readingDelayMs: 1500,
typingMode: 'exponential',
typingWpm: 80,
},
},
},
},
}),
});
const agent = await response.json();2. Connect WebSocket
const socket = io('wss://api.humalike.tech', {
query: { agentId: agent.id, apiKey },
transports: ['websocket'],
});3. Handle Messaging Events
socket.on('event', (data) => {
if (data.type === 'messaging') {
switch (data.event) {
case 'typing_start':
showTypingIndicator();
break;
case 'message_sent':
hideTypingIndicator();
// Render message (with reply reference if present)
const replyToId = data.extra_args?.replyToId;
displayMessage({
id: crypto.randomUUID(),
text: data.message,
author: 'agent',
replyTo: replyToId || null,
});
// Confirm delivery for this message
socket.emit('message', {
type: 'addon-tool-event',
toolCallId: data.toolCallId,
data: { messageIndex: data.messageIndex, success: true },
});
break;
case 'typing_end':
hideTypingIndicator();
break;
}
}
});4. Send User Messages
function sendMessage(text) {
const messageId = crypto.randomUUID();
displayMessage({ id: messageId, text, author: 'user' });
socket.emit('message', {
type: 'context-update',
triggering: true,
name: 'new-message',
context: {
chatHistory: [{
id: messageId,
author: 'User',
content: text,
timestamp: new Date().toISOString(),
}],
user: { name: 'User' },
},
description: `User sent: "${text}"`,
});
}